Files

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())