continuous stream
Build and Push Docker Images / build-and-push (push) Successful in 22s

This commit is contained in:
2026-02-11 22:14:14 +01:00
parent bc4a834e46
commit 6348ecb732
4 changed files with 71 additions and 12 deletions
+4
View File
@@ -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. **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: 3. In your IPTV client or **Plex DVR**, add:
- **Playlist URL:** `http://<host>:8080/playlist.m3u` - **Playlist URL:** `http://<host>:8080/playlist.m3u`
- **EPG (XMLTV) URL:** `http://<host>:8080/epg.xml` - **EPG (XMLTV) URL:** `http://<host>:8080/epg.xml`
+7
View File
@@ -60,3 +60,10 @@ TMDB_IMAGE_STILL_SIZE = "w500"
# Preferred audio languages for streaming (in order): Dutch → Flemish → default # Preferred audio languages for streaming (in order): Dutch → Flemish → default
PREFERRED_AUDIO_LANGUAGES = ("nld", "dut", "nl", "vls") 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"
+47 -11
View File
@@ -7,7 +7,13 @@ import tempfile
from pathlib import Path from pathlib import Path
from urllib.parse import unquote 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__) logger = logging.getLogger(__name__)
@@ -196,11 +202,24 @@ def write_concat_list_from_current(paths: list[Path], durations: list[float], fd
fd.flush() 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: 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 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 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. 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: if not paths:
return 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) write_concat_list_from_current(paths, durations, f)
list_path = f.name list_path = f.name
try: try:
proc = subprocess.Popen( base_args = [
[ "ffmpeg",
"ffmpeg", "-fflags", "+nobuffer+flush_packets",
"-fflags", "+nobuffer+flush_packets", "-stream_loop", "-1",
"-stream_loop", "-1", "-f", "concat", "-safe", "0", "-i", list_path,
"-f", "concat", "-safe", "0", "-i", list_path, ]
"-c", "copy", 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", "-avoid_negative_ts", "make_zero",
"-muxdelay", "0", "-muxdelay", "0", "-muxpreload", "0",
"-muxpreload", "0",
"-max_muxing_queue_size", "1024", "-max_muxing_queue_size", "1024",
"-f", "mpegts", "-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, stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
start_new_session=True, start_new_session=True,
+13 -1
View File
@@ -7,7 +7,14 @@ with randomized order so the same show never appears twice in a row.
import logging import logging
import os 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.cache import CacheManager
from iptv.channel import ChannelRepository from iptv.channel import ChannelRepository
from iptv.schedule import ScheduleBuilder from iptv.schedule import ScheduleBuilder
@@ -34,6 +41,11 @@ def main():
EPG_TIMESPAN_DAYS, EPG_TIMESPAN_DAYS,
SCHEDULE_CACHE_TTL_SEC / 3600, 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() channel_repo = ChannelRepository()
schedule_builder = ScheduleBuilder() schedule_builder = ScheduleBuilder()