128 lines
4.6 KiB
Python
128 lines
4.6 KiB
Python
"""Isolated Windows Graphics Capture producer for the existing FFmpeg recorder.
|
|
|
|
Never falls back to capturing the desktop: window scope remains window scope.
|
|
The parent places this worker in its kill-on-close job before sending `start`;
|
|
the encoder then inherits the job. Only FFmpeg progress goes to stdout.
|
|
"""
|
|
|
|
import argparse
|
|
import ctypes
|
|
from ctypes import wintypes
|
|
import subprocess
|
|
import sys
|
|
import threading
|
|
import time
|
|
|
|
|
|
def has_picture(frame) -> bool:
|
|
"""Reject a blank initial capture, including black frames with only a cursor."""
|
|
import numpy as np
|
|
|
|
sample = frame[:: max(1, frame.shape[0] // 64), :: max(1, frame.shape[1] // 64), :3]
|
|
return float(np.mean(np.max(sample, axis=2) > 8)) > 0.001
|
|
|
|
|
|
def record_window(hwnd: int, width: int, height: int, fps: int, command: list[str]) -> None:
|
|
import cv2
|
|
from windows_capture import WindowsCapture
|
|
|
|
ctypes.windll.user32.IsWindow.argtypes = [wintypes.HWND]
|
|
ctypes.windll.user32.IsIconic.argtypes = [wintypes.HWND]
|
|
if not ctypes.windll.user32.IsWindow(hwnd) or ctypes.windll.user32.IsIconic(hwnd):
|
|
raise RuntimeError("目标窗口不存在或已最小化,未开始录制")
|
|
stop = threading.Event()
|
|
ready = threading.Event()
|
|
lock = threading.Lock()
|
|
latest = [None]
|
|
error = []
|
|
capture = WindowsCapture(
|
|
window_hwnd=hwnd,
|
|
cursor_capture=True,
|
|
draw_border=False,
|
|
minimum_update_interval=max(1, 1000 // fps),
|
|
)
|
|
|
|
@capture.event
|
|
def on_frame_arrived(frame, control):
|
|
try:
|
|
resized = cv2.resize(frame.frame_buffer, (width, height), interpolation=cv2.INTER_AREA)
|
|
if not ready.is_set() and not has_picture(resized):
|
|
return
|
|
with lock:
|
|
latest[0] = resized.tobytes()
|
|
ready.set()
|
|
except (ValueError, RuntimeError, cv2.error) as exc:
|
|
error.append(str(exc))
|
|
stop.set()
|
|
control.stop()
|
|
|
|
@capture.event
|
|
def on_closed():
|
|
error.append("目标窗口已关闭,录像已停止")
|
|
stop.set()
|
|
|
|
def commands():
|
|
for line in sys.stdin:
|
|
if line.strip() == "q":
|
|
break
|
|
stop.set()
|
|
|
|
threading.Thread(target=commands, daemon=True).start()
|
|
control = capture.start_free_threaded()
|
|
encoder = None
|
|
try:
|
|
deadline = time.monotonic() + 5
|
|
while not ready.wait(0.05):
|
|
if stop.is_set() or time.monotonic() >= deadline:
|
|
raise RuntimeError("未取得可辨识的窗口画面;不会将黑屏标记为录制正常")
|
|
encoder = subprocess.Popen(command, stdin=subprocess.PIPE, creationflags=0x08000000)
|
|
print(f"capture_started_at={time.time()}", flush=True)
|
|
next_frame = time.monotonic()
|
|
while not stop.is_set():
|
|
if control.is_finished():
|
|
raise RuntimeError("窗口采集线程已停止,保留已录制的片段")
|
|
if not ctypes.windll.user32.IsWindow(hwnd) or ctypes.windll.user32.IsIconic(hwnd):
|
|
raise RuntimeError("窗口已关闭或最小化,停止录制以避免黑屏或冻结画面")
|
|
with lock:
|
|
data = latest[0]
|
|
encoder.stdin.write(data)
|
|
next_frame += 1 / fps
|
|
# A slow encoder must not build an unbounded queue or a busy catch-up loop.
|
|
next_frame = max(next_frame, time.monotonic())
|
|
stop.wait(max(0, next_frame - time.monotonic()))
|
|
if error:
|
|
raise RuntimeError(error[-1])
|
|
finally:
|
|
try:
|
|
control.stop()
|
|
finally:
|
|
if encoder is not None:
|
|
try:
|
|
encoder.stdin.close()
|
|
encoder.wait(timeout=4)
|
|
except (OSError, subprocess.TimeoutExpired):
|
|
encoder.kill()
|
|
encoder.wait(timeout=2)
|
|
if encoder is not None and encoder.returncode:
|
|
raise RuntimeError(f"窗口编码进程退出:{encoder.returncode}")
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser()
|
|
for key in ("hwnd", "width", "height", "fps"):
|
|
parser.add_argument(f"--{key}", type=int, required=True)
|
|
parser.add_argument("command", nargs=argparse.REMAINDER)
|
|
args = parser.parse_args()
|
|
if sys.stdin.readline().strip() != "start":
|
|
return 1
|
|
try:
|
|
record_window(args.hwnd, args.width, args.height, args.fps, args.command[1:])
|
|
except (OSError, RuntimeError, ImportError, ValueError, subprocess.SubprocessError) as exc:
|
|
print(f"Windows Graphics Capture: {exc}", file=sys.stderr, flush=True)
|
|
return 1
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|