From c49574cb931f0140c854df08544d1d5d7fec2c3c Mon Sep 17 00:00:00 2001 From: Bram Date: Wed, 11 Feb 2026 21:21:04 +0100 Subject: [PATCH] continuous stream --- Dockers/self-hosted-iptv/iptv/streamer.py | 80 +++++++++++++++++++++-- 1 file changed, 73 insertions(+), 7 deletions(-) diff --git a/Dockers/self-hosted-iptv/iptv/streamer.py b/Dockers/self-hosted-iptv/iptv/streamer.py index ff9d3d8..8f93bbf 100644 --- a/Dockers/self-hosted-iptv/iptv/streamer.py +++ b/Dockers/self-hosted-iptv/iptv/streamer.py @@ -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", "-", ],