""" Thin wrapper around ffmpeg-python for building and running ffmpeg pipelines. All ffmpeg command construction goes through this module. Uses ffmpeg-python's own run/run_async for subprocess management. """ import logging import signal import subprocess import ffmpeg log = logging.getLogger(__name__) GLOBAL_ARGS = ("-hide_banner", "-loglevel", "warning") def receive_and_record(stream_url, output_path): """Receive mpegts stream and write to a single growing file. mpv reads this file for DVR-style playback. ffmpeg scene detection runs on this file for frame extraction. Audio is preserved in the recording (muxed mpegts). """ stream = ffmpeg.input(stream_url, fflags="nobuffer", flags="low_delay") return ( ffmpeg.output(stream, str(output_path), c="copy", f="mpegts") .global_args(*GLOBAL_ARGS) ) def extract_scene_frames(input_path, output_dir, scene_threshold=0.3, max_interval=30, start_number=1, start_time=0.0): """Extract frames from a file on scene change. Uses ffmpeg select filter with scene detection and a max-interval fallback. start_time: skip to this position before processing (avoids re-scanning). Returns (stdout, stderr) as decoded strings for timestamp parsing. """ select_expr = ( f"gt(scene,{scene_threshold})" f"+gte(t-prev_selected_t,{max_interval})" ) input_opts = {} if start_time > 0: input_opts["ss"] = str(start_time) stream = ffmpeg.input(str(input_path), **input_opts) stream = stream.filter("select", select_expr).filter("showinfo") output = ( ffmpeg.output( stream, str(output_dir / "F%04d.jpg"), vsync="vfr", **{"q:v": "2"}, start_number=start_number, ) .global_args(*GLOBAL_ARGS) ) log.info("extract_scene_frames: %s", " ".join(output.compile())) stdout, stderr = output.run(capture_stdout=True, capture_stderr=True) return stdout.decode("utf-8", errors="replace"), stderr.decode("utf-8", errors="replace") def run_async(output_node, pipe_stdout=False, pipe_stderr=False): """Start an ffmpeg pipeline asynchronously via ffmpeg-python's run_async.""" log.info("run_async: %s", " ".join(output_node.compile())) return output_node.run_async( pipe_stdout=pipe_stdout, pipe_stderr=pipe_stderr, ) def stop_proc(proc, timeout=5): """Gracefully stop an ffmpeg subprocess.""" if proc and proc.poll() is None: proc.send_signal(signal.SIGINT) try: proc.wait(timeout=timeout) except subprocess.TimeoutExpired: proc.kill()