85 lines
2.6 KiB
Python
85 lines
2.6 KiB
Python
"""
|
|
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()
|