""" Thin wrapper around ffmpeg-python for building and running ffmpeg pipelines. All ffmpeg command construction goes through this module so manager.py and other consumers never build raw CLI arg lists. """ import logging import os import signal import subprocess from pathlib import Path import ffmpeg log = logging.getLogger(__name__) def receive_and_segment(stream_url, segment_dir, segment_duration=60): """Receive mpegts stream and save as segmented .ts files. Returns an ffmpeg-python output node (not yet running). """ stream = ffmpeg.input(stream_url, fflags="nobuffer", flags="low_delay") return ffmpeg.output( stream, str(segment_dir / "segment_%04d.ts"), c="copy", f="segment", segment_time=segment_duration, reset_timestamps=1, ) def receive_and_segment_with_monitor(stream_url, segment_dir, fifo_path, segment_duration=60): """Receive stream, save segments AND tee to a named pipe for monitoring. Returns an ffmpeg-python merged output node. """ if not fifo_path.exists(): os.mkfifo(str(fifo_path)) stream = ffmpeg.input(stream_url, fflags="nobuffer", flags="low_delay") out_segments = ffmpeg.output( stream, str(segment_dir / "segment_%04d.ts"), c="copy", f="segment", segment_time=segment_duration, reset_timestamps=1, ) out_monitor = ffmpeg.output( stream, str(fifo_path), c="copy", f="mpegts", ) return ffmpeg.merge_outputs(out_segments, out_monitor) def extract_scene_frames(input_path, output_dir, scene_threshold=0.3, max_interval=30, start_number=1): """Extract frames from a file on scene change. Uses ffmpeg select filter with scene detection and a max-interval fallback. Returns (process_result, stderr) for timestamp parsing. """ select_expr = ( f"gt(scene\\,{scene_threshold})" f"+gte(t-prev_selected_t\\,{max_interval})" ) stream = ffmpeg.input(str(input_path)) 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, ) return run_sync(output, timeout=120) def extract_audio_pcm(input_path): """Extract audio as 16kHz mono PCM wav, returning an output node for piping. Use run_async with pipe_stdout=True to stream PCM data. """ stream = ffmpeg.input(str(input_path)) return ffmpeg.output( stream.audio, "pipe:", vn=None, acodec="pcm_s16le", ar=16000, ac=1, f="wav", ) def run_async(output_node, pipe_stdout=False, pipe_stderr=False): """Start an ffmpeg pipeline asynchronously. Returns subprocess.Popen.""" cmd = compile_cmd(output_node) log.info("run_async: %s", " ".join(str(c) for c in cmd)) return subprocess.Popen( cmd, stdout=subprocess.PIPE if pipe_stdout else subprocess.DEVNULL, stderr=subprocess.PIPE if pipe_stderr else subprocess.DEVNULL, ) def run_sync(output_node, timeout=None): """Run an ffmpeg pipeline synchronously. Returns (stdout, stderr) as strings.""" cmd = compile_cmd(output_node) log.info("run_sync: %s", " ".join(str(c) for c in cmd)) result = subprocess.run( cmd, capture_output=True, text=True, timeout=timeout, ) return result.stdout, result.stderr def compile_cmd(output_node): """Compile an ffmpeg-python node to a command list, adding global flags.""" cmd = output_node.compile() # Insert global flags after 'ffmpeg' idx = 1 for flag in ["-hide_banner", "-loglevel", "warning"]: cmd.insert(idx, flag) idx += 1 return cmd 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()