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.
**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://<host>:8080/playlist.m3u`
- **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 = ("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 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,
+13 -1
View File
@@ -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()