working the player
This commit is contained in:
@@ -13,40 +13,78 @@ import ffmpeg
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
GLOBAL_ARGS = ("-hide_banner", "-loglevel", "warning")
|
||||
GLOBAL_ARGS = ("-hide_banner",)
|
||||
# Note: scene detection needs -loglevel info for showinfo filter output.
|
||||
# Individual pipelines can override with .global_args()
|
||||
QUIET_ARGS = ("-hide_banner", "-loglevel", "warning")
|
||||
|
||||
|
||||
def receive_and_record(stream_url, output_path):
|
||||
"""Receive mpegts stream and write to a single growing file.
|
||||
"""Receive mpegts stream and write to MKV 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).
|
||||
MKV (Matroska) is used because:
|
||||
- Handles incomplete writes gracefully (like OBS default)
|
||||
- Proper timestamps for seeking and duration detection
|
||||
- mpv plays growing MKV files better than 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)
|
||||
ffmpeg.output(
|
||||
stream, str(output_path),
|
||||
c="copy",
|
||||
f="matroska",
|
||||
flush_packets=1,
|
||||
)
|
||||
.global_args(*QUIET_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.
|
||||
def receive_record_and_relay(stream_url, output_path, relay_url):
|
||||
"""Receive TCP stream, write to MKV, and relay to UDP loopback for live display.
|
||||
|
||||
Uses ffmpeg select filter with scene detection and a max-interval fallback.
|
||||
start_time: skip to this position before processing (avoids re-scanning).
|
||||
Uses ffmpeg tee via merge_outputs: one ffmpeg process handles both outputs
|
||||
from the same decoded input, keeping them in sync with identical timestamps.
|
||||
"""
|
||||
stream = ffmpeg.input(stream_url, fflags="nobuffer", flags="low_delay")
|
||||
file_out = ffmpeg.output(
|
||||
stream, str(output_path),
|
||||
c="copy", f="matroska", flush_packets=1,
|
||||
)
|
||||
relay_out = ffmpeg.output(
|
||||
stream, relay_url,
|
||||
c="copy", f="mpegts",
|
||||
)
|
||||
return ffmpeg.merge_outputs(file_out, relay_out).global_args(*QUIET_ARGS)
|
||||
|
||||
|
||||
def extract_scene_frames(input_path, output_dir, scene_threshold=0.10,
|
||||
start_number=1, start_time=0.0, duration=None):
|
||||
"""Extract frames from a file on scene change only (no interval fallback).
|
||||
|
||||
Frames are a chronological storyboard — captured whenever content changes
|
||||
meaningfully vs the previous frame. No periodic fallback so static content
|
||||
produces no spurious frames.
|
||||
|
||||
start_time/duration: applied via the select filter expression (NOT as -ss/-t
|
||||
input options, which break h264 scene detection on MKV).
|
||||
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)
|
||||
scene_expr = f"gt(scene,{scene_threshold})"
|
||||
|
||||
stream = ffmpeg.input(str(input_path), **input_opts)
|
||||
# Add time range filter if specified (incremental processing)
|
||||
time_conditions = []
|
||||
if start_time > 0:
|
||||
time_conditions.append(f"gte(t,{start_time})")
|
||||
if duration is not None:
|
||||
time_conditions.append(f"lte(t,{start_time + duration})")
|
||||
|
||||
if time_conditions:
|
||||
time_filter = "*".join(time_conditions)
|
||||
select_expr = f"({scene_expr})*{time_filter}"
|
||||
else:
|
||||
select_expr = scene_expr
|
||||
|
||||
stream = ffmpeg.input(str(input_path))
|
||||
stream = stream.filter("select", select_expr).filter("showinfo")
|
||||
|
||||
output = (
|
||||
@@ -61,7 +99,14 @@ def extract_scene_frames(input_path, output_dir, scene_threshold=0.3,
|
||||
)
|
||||
|
||||
log.info("extract_scene_frames: %s", " ".join(output.compile()))
|
||||
stdout, stderr = output.run(capture_stdout=True, capture_stderr=True)
|
||||
try:
|
||||
stdout, stderr = output.run(capture_stdout=True, capture_stderr=True)
|
||||
except ffmpeg.Error as e:
|
||||
# ffmpeg may exit non-zero on growing files (corrupt tail) but still
|
||||
# produce valid frames. Return the stderr for parsing anyway.
|
||||
log.debug("ffmpeg exited with error (may still have valid frames)")
|
||||
stdout = e.stdout or b""
|
||||
stderr = e.stderr or b""
|
||||
return stdout.decode("utf-8", errors="replace"), stderr.decode("utf-8", errors="replace")
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user