This commit is contained in:
eric
2025-09-27 18:13:01 +08:00
parent 0067152f69
commit 617fdcb158
3 changed files with 635 additions and 23 deletions

69
main.py
View File

@@ -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",
]
# 可选缩放/留黑边 + 避免 1088x1920crop 强制精确尺寸)
# 可选缩放/留黑边 + 避免 1088x1920crop 强制精确尺寸)+ 偶数对齐
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",
]
# 平台 headersreferer/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