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)
|
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:
|
def write_concat_list(paths: list[Path], fd) -> None:
|
||||||
"""Write FFmpeg concat demuxer list to file. Escapes paths for safety."""
|
"""Write FFmpeg concat demuxer list to file. Escapes paths for safety."""
|
||||||
for p in paths:
|
for p in paths:
|
||||||
# Concat format: file 'path' — escape ' as '\''
|
fd.write(f"file '{_escape_path(p)}'\n")
|
||||||
path_str = str(p.resolve()).replace("\\", "\\\\").replace("'", "'\\''")
|
|
||||||
fd.write(f"file '{path_str}'\n")
|
|
||||||
fd.flush()
|
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)
|
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:
|
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.
|
Run FFmpeg to output a continuous MPEG-TS stream. Concat list starts at
|
||||||
Stops when wfile write fails (client disconnect).
|
current position in schedule (midnight UTC cycle) so each new client joins
|
||||||
(No -ss seek: seeking into a looping concat input is unreliable, so stream
|
where the 'channel' is now; then the list loops. EPG and stream stay in sync.
|
||||||
always starts from playlist start; EPG order and durations still match.)
|
|
||||||
"""
|
"""
|
||||||
if not paths:
|
if not paths:
|
||||||
return
|
return
|
||||||
|
|
||||||
with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as f:
|
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
|
list_path = f.name
|
||||||
try:
|
try:
|
||||||
proc = subprocess.Popen(
|
proc = subprocess.Popen(
|
||||||
|
|||||||
Reference in New Issue
Block a user