This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
@@ -142,6 +143,53 @@ def _escape_path(p: Path) -> str:
|
||||
return str(p.resolve()).replace("\\", "\\\\").replace("'", "'\\''")
|
||||
|
||||
|
||||
def _get_keyframe_at_or_before(path: Path, time_sec: float) -> float | None:
|
||||
"""
|
||||
Return the PTS (seconds) of the keyframe at or just before time_sec.
|
||||
Returns None on failure (probe error, timeout, no keyframes).
|
||||
Aligning cuts to keyframes avoids corrupt output at concat transitions.
|
||||
"""
|
||||
try:
|
||||
out = subprocess.run(
|
||||
[
|
||||
"ffprobe",
|
||||
"-v", "error",
|
||||
"-select_streams", "v:0",
|
||||
"-skip_frame", "nokey",
|
||||
"-show_entries", "frame=pkt_pts_time",
|
||||
"-of", "csv=p=0",
|
||||
str(path),
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=15,
|
||||
)
|
||||
if out.returncode != 0 or not out.stdout:
|
||||
return None
|
||||
keyframes = []
|
||||
for line in out.stdout.strip().splitlines():
|
||||
line = line.strip()
|
||||
if line:
|
||||
try:
|
||||
t = float(line)
|
||||
keyframes.append(t)
|
||||
except ValueError:
|
||||
continue
|
||||
if not keyframes:
|
||||
return None
|
||||
keyframes = sorted(set(keyframes))
|
||||
# Last keyframe at or before time_sec
|
||||
best = 0.0
|
||||
for kf in keyframes:
|
||||
if kf <= time_sec:
|
||||
best = kf
|
||||
else:
|
||||
break
|
||||
return best
|
||||
except (subprocess.TimeoutExpired, ValueError, FileNotFoundError):
|
||||
return None
|
||||
|
||||
|
||||
def write_concat_list(paths: list[Path], fd) -> None:
|
||||
"""Write FFmpeg concat demuxer list to file."""
|
||||
for p in paths:
|
||||
@@ -179,20 +227,38 @@ def _current_position_in_cycle(durations: list[float]) -> tuple[int, float]:
|
||||
def write_concat_list_from_current(paths: list[Path], durations: list[float], fd) -> None:
|
||||
"""
|
||||
Write a concat list that starts at the current position in the schedule and
|
||||
loops seamlessly. Uses inpoint/outpoint so we join mid-file, then repeat.
|
||||
loops seamlessly. Uses inpoint/outpoint aligned to keyframes so we never
|
||||
cut mid-GOP (which causes corrupt packets and stream crashes at transitions).
|
||||
"""
|
||||
n = len(paths)
|
||||
if n == 0:
|
||||
return
|
||||
idx, offset_in_file = _current_position_in_cycle(durations)
|
||||
# For testing: start first episode at last N minutes (env TEST_START_LAST_MINUTES=3)
|
||||
try:
|
||||
test_min = int(os.environ.get("TEST_START_LAST_MINUTES", "0") or "0")
|
||||
if test_min > 0 and idx < len(durations):
|
||||
offset_in_file = max(0.0, durations[idx] - test_min * 60)
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
# Align cuts to keyframes to avoid corrupt output at file boundaries
|
||||
inpoint_sec = None
|
||||
outpoint_sec = None
|
||||
if offset_in_file >= 0.5:
|
||||
kf = _get_keyframe_at_or_before(paths[idx], offset_in_file)
|
||||
if kf is not None and kf >= 0.5:
|
||||
inpoint_sec = kf
|
||||
outpoint_sec = kf
|
||||
# If keyframe probe failed, skip inpoint/outpoint — start from file start
|
||||
# to avoid mid-GOP cuts that crash the stream
|
||||
for i in range(n):
|
||||
j = (idx + i) % n
|
||||
fd.write(f"file '{_escape_path(paths[j])}'\n")
|
||||
if i == 0 and offset_in_file >= 0.5:
|
||||
fd.write(f"inpoint {offset_in_file:.2f}\n")
|
||||
if offset_in_file >= 0.5:
|
||||
if i == 0 and inpoint_sec is not None:
|
||||
fd.write(f"inpoint {inpoint_sec:.2f}\n")
|
||||
if outpoint_sec is not None:
|
||||
fd.write(f"file '{_escape_path(paths[idx])}'\n")
|
||||
fd.write(f"outpoint {offset_in_file:.2f}\n")
|
||||
fd.write(f"outpoint {outpoint_sec:.2f}\n")
|
||||
fd.flush()
|
||||
|
||||
|
||||
@@ -212,14 +278,14 @@ def stream_live_channel(paths: list[Path], wfile, durations: list[float]) -> Non
|
||||
proc = subprocess.Popen(
|
||||
[
|
||||
"ffmpeg",
|
||||
"-fflags", "+nobuffer+flush_packets",
|
||||
"-fflags", "+nobuffer+flush_packets+genpts",
|
||||
"-stream_loop", "-1",
|
||||
"-f", "concat", "-safe", "0", "-i", list_path,
|
||||
"-c", "copy",
|
||||
"-avoid_negative_ts", "make_zero",
|
||||
"-muxdelay", "0",
|
||||
"-muxpreload", "0",
|
||||
"-max_muxing_queue_size", "1024",
|
||||
"-max_muxing_queue_size", "2048",
|
||||
"-f", "mpegts",
|
||||
"-",
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user