This commit is contained in:
@@ -272,12 +272,14 @@ def build_epg_xml() -> str:
|
||||
return '<?xml version="1.0" encoding="UTF-8"?>\n' + ET.tostring(root, encoding="unicode", default_namespace=None)
|
||||
|
||||
|
||||
def _escape_path(p: Path) -> str:
|
||||
return str(p.resolve()).replace("\\", "\\\\").replace("'", "'\\''")
|
||||
|
||||
|
||||
def write_concat_list(paths: list[Path], fd) -> None:
|
||||
"""Write FFmpeg concat demuxer list to file. Escapes paths for safety."""
|
||||
for p in paths:
|
||||
# Concat format: file 'path' — escape ' as '\''
|
||||
path_str = str(p.resolve()).replace("\\", "\\\\").replace("'", "'\\''")
|
||||
fd.write(f"file '{path_str}'\n")
|
||||
fd.write(f"file '{_escape_path(p)}'\n")
|
||||
fd.flush()
|
||||
|
||||
|
||||
@@ -286,18 +288,57 @@ def _schedule_epoch_utc() -> datetime:
|
||||
return datetime.now(timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
|
||||
|
||||
def _current_position_in_cycle(durations: list[float]) -> tuple[int, float]:
|
||||
"""
|
||||
Return (file_index, offset_sec_in_file) for 'now' in the schedule cycle.
|
||||
Schedule epoch = midnight UTC; cycle = one full playlist.
|
||||
"""
|
||||
epoch = _schedule_epoch_utc()
|
||||
now = datetime.now(timezone.utc)
|
||||
cycle_duration = sum(durations)
|
||||
if cycle_duration <= 0:
|
||||
return 0, 0.0
|
||||
offset_sec = (now - epoch).total_seconds() % cycle_duration
|
||||
cumul = 0.0
|
||||
for i, dur in enumerate(durations):
|
||||
if offset_sec < cumul + dur:
|
||||
return i, offset_sec - cumul
|
||||
cumul += dur
|
||||
return len(durations) - 1, durations[-1]
|
||||
|
||||
|
||||
def write_concat_list_from_current(paths: list[Path], durations: list[float], fd) -> None:
|
||||
"""
|
||||
Write a concat list that starts at the current position in the schedule and
|
||||
loops seamlessly. Uses inpoint/outpoint so we join mid-file, then repeat.
|
||||
Cycle: current file (offset→end), next files…, start…current-1, current file (0→offset).
|
||||
"""
|
||||
n = len(paths)
|
||||
if n == 0:
|
||||
return
|
||||
idx, offset_in_file = _current_position_in_cycle(durations)
|
||||
for i in range(n):
|
||||
j = (idx + i) % n
|
||||
fd.write(f"file '{_escape_path(paths[j])}'\n")
|
||||
if i == 0 and offset_in_file >= 0.5:
|
||||
fd.write(f"inpoint {offset_in_file:.2f}\n")
|
||||
if offset_in_file >= 0.5:
|
||||
fd.write(f"file '{_escape_path(paths[idx])}'\n")
|
||||
fd.write(f"outpoint {offset_in_file:.2f}\n")
|
||||
fd.flush()
|
||||
|
||||
|
||||
def stream_live_channel(paths: list[Path], wfile, durations: list[float]) -> None:
|
||||
"""
|
||||
Run FFmpeg to output a continuous MPEG-TS stream from paths, looping forever.
|
||||
Stops when wfile write fails (client disconnect).
|
||||
(No -ss seek: seeking into a looping concat input is unreliable, so stream
|
||||
always starts from playlist start; EPG order and durations still match.)
|
||||
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.
|
||||
"""
|
||||
if not paths:
|
||||
return
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as f:
|
||||
write_concat_list(paths, f)
|
||||
write_concat_list_from_current(paths, durations, f)
|
||||
list_path = f.name
|
||||
try:
|
||||
proc = subprocess.Popen(
|
||||
|
||||
Reference in New Issue
Block a user