This commit is contained in:
@@ -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`
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user