132 lines
4.7 KiB
Python
132 lines
4.7 KiB
Python
import ffmpeg
|
||
import threading
|
||
import time
|
||
import sys
|
||
from datetime import datetime
|
||
|
||
LOGLEVEL = "info" # поставь "debug" для диагностики
|
||
|
||
# 1) ВХОД от vMix: ffmpeg слушает, vMix звонит
|
||
IN_URL = "srt://0.0.0.0:9000?mode=listener&reuseaddr=1&transtype=live"
|
||
|
||
# 2) ЛОКАЛЬНЫЙ буфер (UDP внутри одного ПК)
|
||
# Любой свободный порт, лучше 10000+
|
||
LOCAL_UDP_OUT = "udp://127.0.0.1:10000?pkt_size=1316&fifo_size=5000000&overrun_nonfatal=1"
|
||
LOCAL_UDP_IN = "udp://127.0.0.1:10000?fifo_size=5000000&overrun_nonfatal=1"
|
||
|
||
# 3) ВЫХОД: вариант A (рекомендую для VLC/мобилы) — OUT listener, клиенты подключаются caller
|
||
OUT_URL = "srt://0.0.0.0:9001?mode=listener&reuseaddr=1&transtype=live"
|
||
|
||
# Если тебе нужен OUT caller на конкретное устройство, используй вместо этого:
|
||
# OUT_URL = "srt://10.10.1.238:9001?mode=caller&latency=200000&transtype=live&connect_timeout=5000"
|
||
|
||
def now():
|
||
return datetime.now().strftime("%H:%M:%S")
|
||
|
||
def pump_stderr(proc, tag):
|
||
"""Читает stderr, чтобы процесс не подвис из-за заполненного буфера."""
|
||
try:
|
||
for raw in iter(proc.stderr.readline, b""):
|
||
if not raw:
|
||
break
|
||
line = raw.decode("utf-8", errors="replace").rstrip("\n")
|
||
if line:
|
||
print(f"[{now()}] {tag}: {line}")
|
||
except Exception:
|
||
pass
|
||
|
||
def start_ingest():
|
||
"""
|
||
SRT-IN -> UDP localhost (не зависит от OUT!)
|
||
"""
|
||
stream = (
|
||
ffmpeg
|
||
.input(IN_URL)
|
||
.output(LOCAL_UDP_OUT, format="mpegts", codec="copy")
|
||
.global_args(
|
||
"-hide_banner",
|
||
"-loglevel", LOGLEVEL,
|
||
"-fflags", "+genpts",
|
||
"-flags", "low_delay",
|
||
)
|
||
)
|
||
|
||
proc = stream.run_async(pipe_stderr=True)
|
||
threading.Thread(target=pump_stderr, args=(proc, "INGEST"), daemon=True).start()
|
||
return proc
|
||
|
||
def start_egress():
|
||
"""
|
||
UDP localhost -> SRT-OUT
|
||
(можно падать/перезапускаться сколько угодно — IN не трогаем)
|
||
"""
|
||
stream = (
|
||
ffmpeg
|
||
.input(LOCAL_UDP_IN, format="mpegts")
|
||
.output(OUT_URL, format="mpegts", codec="copy")
|
||
.global_args(
|
||
"-hide_banner",
|
||
"-loglevel", LOGLEVEL,
|
||
"-fflags", "+genpts",
|
||
"-flags", "low_delay",
|
||
)
|
||
)
|
||
|
||
proc = stream.run_async(pipe_stderr=True)
|
||
threading.Thread(target=pump_stderr, args=(proc, "EGRESS"), daemon=True).start()
|
||
return proc
|
||
|
||
def run_forever():
|
||
# INGEST: держим почти “вечно”, перезапускаем только если упал вход
|
||
ingest_backoff = 1.0
|
||
ingest_backoff_max = 10.0
|
||
|
||
# EGRESS: можно перезапускать часто (если клиент/сеть отвалилась)
|
||
egress_backoff = 0.5
|
||
egress_backoff_max = 5.0
|
||
|
||
ingest_proc = None
|
||
|
||
while True:
|
||
# 1) Убедимся, что INGEST жив
|
||
if ingest_proc is None or ingest_proc.poll() is not None:
|
||
if ingest_proc is not None:
|
||
print(f"[{now()}] INGEST exited rc={ingest_proc.returncode}")
|
||
print(f"[{now()}] Restarting INGEST in {ingest_backoff:.1f}s...\n")
|
||
time.sleep(ingest_backoff)
|
||
ingest_backoff = min(ingest_backoff * 1.5, ingest_backoff_max)
|
||
|
||
print(f"[{now()}] Starting INGEST...")
|
||
try:
|
||
ingest_proc = start_ingest()
|
||
ingest_backoff = 1.0 # сброс после успешного запуска
|
||
except FileNotFoundError:
|
||
print(f"[{now()}] ERROR: ffmpeg.exe not found (PATH).")
|
||
sys.exit(2)
|
||
except Exception as e:
|
||
print(f"[{now()}] ERROR starting INGEST: {e}")
|
||
time.sleep(ingest_backoff)
|
||
continue
|
||
|
||
# 2) Поднимаем/держим EGRESS отдельно
|
||
print(f"[{now()}] Starting EGRESS...")
|
||
try:
|
||
egress_proc = start_egress()
|
||
except Exception as e:
|
||
print(f"[{now()}] ERROR starting EGRESS: {e}")
|
||
time.sleep(egress_backoff)
|
||
egress_backoff = min(egress_backoff * 1.5, egress_backoff_max)
|
||
continue
|
||
|
||
rc = egress_proc.wait()
|
||
print(f"[{now()}] EGRESS exited rc={rc}")
|
||
print(f"[{now()}] Restarting EGRESS in {egress_backoff:.1f}s...\n")
|
||
time.sleep(egress_backoff)
|
||
egress_backoff = min(egress_backoff * 1.5, egress_backoff_max)
|
||
|
||
if __name__ == "__main__":
|
||
try:
|
||
run_forever()
|
||
except KeyboardInterrupt:
|
||
sys.exit(0)
|