149 lines
4.1 KiB
Python
149 lines
4.1 KiB
Python
"""
|
|
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()
|