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.
|
**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`
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user