s''
This commit is contained in:
69
main.py
69
main.py
@@ -28,7 +28,7 @@ from src.utils import logger, Color, check_md5
|
||||
from ffmpeg_install import check_ffmpeg, ffmpeg_path, current_env_path
|
||||
|
||||
# --------------------- 基础环境 ---------------------
|
||||
version = "relay-1.0.1"
|
||||
version = "relay-1.1.0"
|
||||
os.environ['PATH'] = ffmpeg_path + os.pathsep + current_env_path
|
||||
script_path = os.path.split(os.path.realpath(sys.argv[0]))[0]
|
||||
config_file = f'{script_path}/config/config.ini'
|
||||
@@ -60,6 +60,10 @@ def _safe_int(v, lo, hi, default):
|
||||
except Exception:
|
||||
return default
|
||||
|
||||
def _round_res(w: int, h: int) -> Tuple[int, int]:
|
||||
# 强制偶数,避免 1088/1918 这类非标准尺寸
|
||||
return (w & ~1, h & ~1)
|
||||
|
||||
def _build_youtube_rtmp_url(key: str, secure: bool, ingest: str | None):
|
||||
node = (ingest or "a").strip().lower()
|
||||
node = node if node.isalpha() and len(node) == 1 else "a"
|
||||
@@ -243,10 +247,11 @@ def build_youtube_output() -> Tuple[str, list, list]:
|
||||
"-ac", "2",
|
||||
]
|
||||
|
||||
# 可选缩放/留黑边 + 避免 1088x1920(加 crop 强制精确尺寸)
|
||||
# 可选缩放/留黑边 + 避免 1088x1920(crop 强制精确尺寸)+ 偶数对齐
|
||||
if res_str:
|
||||
try:
|
||||
w, h = map(int, res_str.lower().split("x"))
|
||||
w, h = _round_res(w, h)
|
||||
vf = (
|
||||
f"scale={w}:{h}:force_original_aspect_ratio=decrease,"
|
||||
f"pad={w}:{h}:(ow-iw)/2:(oh-ih)/2,"
|
||||
@@ -278,9 +283,9 @@ def launch_ffmpeg(input_url: str,
|
||||
# 输入侧容错/重连/探测
|
||||
base = [
|
||||
"ffmpeg", "-nostdin", "-hide_banner",
|
||||
"-stats", # 输出统计信息
|
||||
"-stats_period", "60", # 每60秒一次
|
||||
"-progress", "pipe:1", # 状态写到stdout,便于reader捕获
|
||||
"-stats",
|
||||
"-stats_period", "60",
|
||||
"-progress", "pipe:1",
|
||||
"-loglevel", "info",
|
||||
|
||||
# 输入容错 & 自动重连
|
||||
@@ -301,7 +306,6 @@ def launch_ffmpeg(input_url: str,
|
||||
"-probesize", "10000000",
|
||||
]
|
||||
|
||||
|
||||
# 平台 headers(referer/origin)要在 -i 之前
|
||||
if extra_headers:
|
||||
base += ["-headers", extra_headers]
|
||||
@@ -316,9 +320,10 @@ def launch_ffmpeg(input_url: str,
|
||||
# 输入
|
||||
base += ["-re", "-i", input_url]
|
||||
|
||||
# 通用容错
|
||||
# 通用容错 + 时间戳修复
|
||||
base += [
|
||||
"-fflags", "+genpts+discardcorrupt+nobuffer",
|
||||
"-fflags", "+genpts+igndts+discardcorrupt+nobuffer",
|
||||
"-xerror",
|
||||
"-use_wallclock_as_timestamps", "1",
|
||||
"-avioflags", "direct",
|
||||
"-rtbufsize", "100M",
|
||||
@@ -340,7 +345,6 @@ def launch_ffmpeg(input_url: str,
|
||||
|
||||
creationflags = 0
|
||||
if os.name == "nt":
|
||||
# 防止子进程挂住控制台(可选)
|
||||
creationflags = subprocess.CREATE_NEW_PROCESS_GROUP
|
||||
|
||||
p = subprocess.Popen(
|
||||
@@ -348,7 +352,7 @@ def launch_ffmpeg(input_url: str,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
bufsize=0, # 避免行缓冲告警 & 尽快刷新
|
||||
bufsize=0, # 立即刷新,避免行缓冲告警
|
||||
env=env,
|
||||
creationflags=creationflags
|
||||
)
|
||||
@@ -363,14 +367,20 @@ def launch_ffmpeg(input_url: str,
|
||||
break
|
||||
text = line.decode(errors="ignore").strip()
|
||||
p.last_out_ts = time.time()
|
||||
|
||||
# 输出关键统计,便于观察码率/帧率
|
||||
if ("bitrate=" in text) or ("frame=" in text) or ("speed=" in text):
|
||||
logger.info(f"[ffmpeg统计] {text}")
|
||||
|
||||
# 常见错误提示
|
||||
if "Server disconnected" in text:
|
||||
logger.error(f"[YouTube输出异常] {text}")
|
||||
logger.error(f"[YouTube/RTMP输出异常] {text}")
|
||||
elif "not supported" in text or "Invalid" in text or "invalid" in text:
|
||||
logger.error(f"[转码异常] {text}")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
import threading
|
||||
t = threading.Thread(target=_reader, daemon=True)
|
||||
t.start()
|
||||
|
||||
@@ -450,7 +460,8 @@ def main():
|
||||
|
||||
# 顺序轮询索引
|
||||
idx = 0
|
||||
current_url = None
|
||||
current_url: Optional[str] = None
|
||||
current_real_url: Optional[str] = None # 实际播放地址(flv/m3u8)
|
||||
current_proc: Optional[subprocess.Popen] = None
|
||||
current_platform = ""
|
||||
current_anchor = ""
|
||||
@@ -460,9 +471,10 @@ def main():
|
||||
scan_interval_when_idle = 2 # 没有任何可用源时的扫描间隔秒
|
||||
last_md5 = "" # URL 文件变化时热更新
|
||||
|
||||
# watchdog 参数
|
||||
watchdog_grace = 60 # 推流开始 60s 内不做静默判定
|
||||
watchdog_silence = 300 # 5 分钟完全无输出才重启
|
||||
# watchdog 参数(更稳)
|
||||
watchdog_grace = 120 # 推流开始 120s 内不做静默判定
|
||||
watchdog_silence = 600 # 10 分钟完全无输出才重启
|
||||
refresh_real_url_every = 300 # 每 5 分钟刷新一次真实播放地址,避免 token 过期
|
||||
|
||||
try:
|
||||
while True:
|
||||
@@ -479,6 +491,7 @@ def main():
|
||||
stop_ffmpeg(current_proc)
|
||||
current_proc = None
|
||||
current_url = None
|
||||
current_real_url = None
|
||||
except Exception as e:
|
||||
logger.warning(f"URL 列表热更新失败:{e}")
|
||||
|
||||
@@ -489,6 +502,7 @@ def main():
|
||||
stop_ffmpeg(current_proc)
|
||||
current_proc = None
|
||||
current_url = None
|
||||
current_real_url = None
|
||||
continue
|
||||
|
||||
# 检查是否停播(依赖 spider)
|
||||
@@ -506,13 +520,32 @@ def main():
|
||||
dur_str = f"{h:02d}:{m:02d}:{s:02d}"
|
||||
print(f"\r[{now_str()}] 正在转播({dur_str}):{current_platform} | {current_anchor} | {current_url}", end="")
|
||||
|
||||
# ✅ watchdog(推流开始 > 60s 且 5 分钟完全无输出)
|
||||
# 定期刷新真实播放地址,避免 token 过期
|
||||
try:
|
||||
if elapsed >= refresh_real_url_every and (elapsed % refresh_real_url_every) < check_interval_live:
|
||||
new_port = asyncio.run(fetch_port_info(current_url, record_quality_code))
|
||||
new_real_url = select_source_url(current_url, new_port)
|
||||
if new_real_url and new_real_url != current_real_url:
|
||||
color.print_colored(f"\n[{now_str()}] 刷新播放地址(token 变化),重启 ffmpeg…", color.YELLOW)
|
||||
stop_ffmpeg(current_proc)
|
||||
out_url, v_args, a_args = build_youtube_output()
|
||||
headers = headers_for_platform(current_platform, current_url)
|
||||
current_proc = launch_ffmpeg(new_real_url, out_url, headers, v_args=v_args, a_args=a_args)
|
||||
current_real_url = new_real_url
|
||||
start_time = time.time()
|
||||
time.sleep(check_interval_live)
|
||||
continue
|
||||
except Exception as e:
|
||||
logger.warning(f"刷新播放地址失败:{e}")
|
||||
|
||||
# ✅ watchdog(推流开始 > 120s 且 10 分钟完全无输出)
|
||||
silence = time.time() - getattr(current_proc, "last_out_ts", 0)
|
||||
if elapsed >= watchdog_grace and silence > watchdog_silence:
|
||||
color.print_colored(f"\n[{now_str()}] 超过5分钟未见 ffmpeg 输出,判定异常,重启…", color.YELLOW)
|
||||
color.print_colored(f"\n[{now_str()}] 超过10分钟未见 ffmpeg 输出,判定异常,重启…", color.YELLOW)
|
||||
stop_ffmpeg(current_proc)
|
||||
current_proc = None
|
||||
current_url = None
|
||||
current_real_url = None
|
||||
time.sleep(1)
|
||||
continue
|
||||
|
||||
@@ -523,6 +556,7 @@ def main():
|
||||
stop_ffmpeg(current_proc)
|
||||
current_proc = None
|
||||
current_url = None
|
||||
current_real_url = None
|
||||
|
||||
else:
|
||||
# 未在推流:从 urls 顺序找第一个 is_live 的
|
||||
@@ -560,6 +594,7 @@ def main():
|
||||
out_url, v_args, a_args = build_youtube_output()
|
||||
current_proc = launch_ffmpeg(real_url, out_url, headers, v_args=v_args, a_args=a_args)
|
||||
current_url = url
|
||||
current_real_url = real_url
|
||||
start_time = time.time()
|
||||
color.print_colored(f"[{now_str()}] 开始转播:{current_platform} | {current_anchor} | {url}", color.GREEN)
|
||||
found = True
|
||||
|
||||
Reference in New Issue
Block a user