From 6348ecb732235f54cf0425e64152d340a52503c7 Mon Sep 17 00:00:00 2001 From: Bram Date: Wed, 11 Feb 2026 22:14:14 +0100 Subject: [PATCH] continuous stream --- Dockers/self-hosted-iptv/README.md | 4 ++ Dockers/self-hosted-iptv/iptv/config.py | 7 +++ Dockers/self-hosted-iptv/iptv/streamer.py | 58 ++++++++++++++++++----- Dockers/self-hosted-iptv/main.py | 14 +++++- 4 files changed, 71 insertions(+), 12 deletions(-) diff --git a/Dockers/self-hosted-iptv/README.md b/Dockers/self-hosted-iptv/README.md index 0e07d0d..518d2b6 100644 --- a/Dockers/self-hosted-iptv/README.md +++ b/Dockers/self-hosted-iptv/README.md @@ -42,6 +42,10 @@ Series are matched by folder name (e.g. `series/W817` → search "W817"); episod **Logging:** Set `LOG_LEVEL=DEBUG` (e.g. in your `docker run` with `-e LOG_LEVEL=DEBUG`) to see where time is spent: schedule building (including ffprobe per video), EPG per-channel programme count and duration, and TMDB cache hits (memory/disk) vs API requests and rate-limit waits. +**Stream crashes when switching episodes?** If the live stream freezes or crashes when transitioning between videos (e.g. different episodes with different resolutions), enable normalization to re-encode everything to a uniform format: +- `NORMALIZE_LIVE_STREAM=1` — enables re-encoding (uses more CPU, produces a stable continuous stream) +- `NORMALIZE_TARGET_RES=1920x1080` — target resolution (default). Use `1280x720` for lower CPU usage. + 3. In your IPTV client or **Plex DVR**, add: - **Playlist URL:** `http://:8080/playlist.m3u` - **EPG (XMLTV) URL:** `http://:8080/epg.xml` diff --git a/Dockers/self-hosted-iptv/iptv/config.py b/Dockers/self-hosted-iptv/iptv/config.py index 01d700b..03ba04d 100644 --- a/Dockers/self-hosted-iptv/iptv/config.py +++ b/Dockers/self-hosted-iptv/iptv/config.py @@ -60,3 +60,10 @@ TMDB_IMAGE_STILL_SIZE = "w500" # Preferred audio languages for streaming (in order): Dutch → Flemish → default PREFERRED_AUDIO_LANGUAGES = ("nld", "dut", "nl", "vls") + +# Live stream normalization: re-encode to uniform format to avoid crashes when +# switching between episodes with different resolutions/codecs. Set +# NORMALIZE_LIVE_STREAM=1 to enable. Uses more CPU but produces a stable stream. +NORMALIZE_LIVE_STREAM = os.environ.get("NORMALIZE_LIVE_STREAM", "").lower() in ("1", "true", "yes") +# Target resolution when normalizing (e.g. "1920x1080" or "1280x720") +NORMALIZE_TARGET_RES = os.environ.get("NORMALIZE_TARGET_RES", "1920x1080").strip() or "1920x1080" diff --git a/Dockers/self-hosted-iptv/iptv/streamer.py b/Dockers/self-hosted-iptv/iptv/streamer.py index 74a4595..d7c5923 100644 --- a/Dockers/self-hosted-iptv/iptv/streamer.py +++ b/Dockers/self-hosted-iptv/iptv/streamer.py @@ -7,7 +7,13 @@ import tempfile from pathlib import Path from urllib.parse import unquote -from .config import VIDEO_EXTENSIONS, MOVIES_ROOT, SERIES_ROOT +from .config import ( + VIDEO_EXTENSIONS, + MOVIES_ROOT, + SERIES_ROOT, + NORMALIZE_LIVE_STREAM, + NORMALIZE_TARGET_RES, +) logger = logging.getLogger(__name__) @@ -196,11 +202,24 @@ def write_concat_list_from_current(paths: list[Path], durations: list[float], fd fd.flush() +def _parse_resolution(res: str) -> tuple[int, int]: + """Parse 'WxH' into (width, height). Default 1920x1080 on parse error.""" + try: + w, h = res.strip().lower().split("x") + return int(w), int(h) + except (ValueError, AttributeError): + return 1920, 1080 + + def stream_live_channel(paths: list[Path], wfile, durations: list[float]) -> None: """ Run FFmpeg to output a continuous MPEG-TS stream. Concat list starts at current position in schedule (midnight UTC cycle) so each new client joins where the 'channel' is now; then the list loops. EPG and stream stay in sync. + + When NORMALIZE_LIVE_STREAM is enabled, videos are re-encoded to a uniform + format (resolution, codec). This prevents crashes when switching between + episodes with different resolutions or codec parameters. """ if not paths: return @@ -209,20 +228,37 @@ def stream_live_channel(paths: list[Path], wfile, durations: list[float]) -> Non write_concat_list_from_current(paths, durations, f) list_path = f.name try: - proc = subprocess.Popen( - [ - "ffmpeg", - "-fflags", "+nobuffer+flush_packets", - "-stream_loop", "-1", - "-f", "concat", "-safe", "0", "-i", list_path, - "-c", "copy", + base_args = [ + "ffmpeg", + "-fflags", "+nobuffer+flush_packets", + "-stream_loop", "-1", + "-f", "concat", "-safe", "0", "-i", list_path, + ] + if NORMALIZE_LIVE_STREAM: + w, h = _parse_resolution(NORMALIZE_TARGET_RES) + # Scale to fit target res, pad to exact size. Re-encode for uniform output. + vf = f"scale={w}:{h}:force_original_aspect_ratio=decrease,pad={w}:{h}:(ow-iw)/2:(oh-ih)/2" + output_args = [ + "-vf", vf, + "-c:v", "libx264", "-preset", "ultrafast", "-tune", "zerolatency", + "-c:a", "aac", "-ac", "2", "-b:a", "128k", "-avoid_negative_ts", "make_zero", - "-muxdelay", "0", - "-muxpreload", "0", + "-muxdelay", "0", "-muxpreload", "0", "-max_muxing_queue_size", "1024", "-f", "mpegts", "-", - ], + ] + else: + output_args = [ + "-c", "copy", + "-avoid_negative_ts", "make_zero", + "-muxdelay", "0", "-muxpreload", "0", + "-max_muxing_queue_size", "1024", + "-f", "mpegts", + "-", + ] + proc = subprocess.Popen( + base_args + output_args, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, start_new_session=True, diff --git a/Dockers/self-hosted-iptv/main.py b/Dockers/self-hosted-iptv/main.py index 1020f1b..8bb3555 100644 --- a/Dockers/self-hosted-iptv/main.py +++ b/Dockers/self-hosted-iptv/main.py @@ -7,7 +7,14 @@ with randomized order so the same show never appears twice in a row. import logging import os -from iptv.config import CHANNELS_FILE, EPG_TIMESPAN_DAYS, SCHEDULE_CACHE_TTL_SEC, init_epg_timespan +from iptv.config import ( + CHANNELS_FILE, + EPG_TIMESPAN_DAYS, + SCHEDULE_CACHE_TTL_SEC, + NORMALIZE_LIVE_STREAM, + NORMALIZE_TARGET_RES, + init_epg_timespan, +) from iptv.cache import CacheManager from iptv.channel import ChannelRepository from iptv.schedule import ScheduleBuilder @@ -34,6 +41,11 @@ def main(): EPG_TIMESPAN_DAYS, SCHEDULE_CACHE_TTL_SEC / 3600, ) + if NORMALIZE_LIVE_STREAM: + logger.info( + "Live stream normalization enabled (target: %s) — prevents crashes when switching episodes", + NORMALIZE_TARGET_RES, + ) channel_repo = ChannelRepository() schedule_builder = ScheduleBuilder()