This commit is contained in:
eric
2025-09-27 16:22:38 +08:00
parent d507ccff4c
commit 34990a29de
2 changed files with 83 additions and 40 deletions

120
main.py
View File

@@ -258,67 +258,102 @@ def launch_ffmpeg(input_url: str, out_url: str, extra_headers: Optional[str] = N
"""
启动一个 ffmpeg 进程:输入 input_url输出 RTMP 到 out_url。
"""
import threading
user_agent = ("Mozilla/5.0 (Linux; Android 11; SM-G973U) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/87.0.4280.141 Mobile Safari/537.36")
rw_timeout = "15000000"
analyzeduration = "20000000"
probesize = "10000000"
# —— 输入侧容错/重连/时间戳修复 ——
rw_timeout = "15000000"
analyzeduration = "20000000"
probesize = "10000000"
base = [
"ffmpeg", "-nostdin", "-hide_banner",
# 超时和重连
"-rw_timeout", rw_timeout,
# 输入容错 & 自动重连
"-rw_timeout", "15000000",
"-timeout", "10000000",
"-reconnect", "1",
"-reconnect_streamed", "1",
"-reconnect_at_eof", "1",
"-reconnect_delay_max", "60",
"-reconnect_delay_max", "30",
# UA + 协议
# User-Agent 和协议
"-user_agent", user_agent,
"-protocol_whitelist", "rtmp,crypto,file,http,https,tcp,tls,udp,rtp,httpproxy",
"-protocol_whitelist", "file,http,https,tcp,tls,crypto,udp,rtmp,rtp,httpproxy",
# 缓冲和分析
"-thread_queue_size", "1024",
"-analyzeduration", analyzeduration,
"-probesize", probesize,
"-fflags", "+discardcorrupt",
# 缓冲 & 探测优化
"-thread_queue_size", "4096",
"-analyzeduration", "10000000",
"-probesize", "10000000",
# 输入
"-i", input_url,
# 输入
"-re", "-i", input_url,
# 强制视频转码成 YouTube 最兼容的格式
# 容错标志:防止丢包、断音直接崩溃
"-fflags", "+genpts+discardcorrupt+nobuffer",
"-use_wallclock_as_timestamps", "1",
# 视频编码
"-c:v", "libx264",
"-preset", "veryfast",
"-pix_fmt", "yuv420p",
"-tune", "zerolatency",
"-profile:v", "high",
"-level", "4.1",
"-r", "30", # 固定帧率
"-g", "60", # GOP=2s
"-level:v", "4.1",
"-pix_fmt", "yuv420p",
"-r", "30",
"-g", "60",
"-b:v", "5000k",
"-maxrate", "5000k",
"-bufsize", "10000k",
"-vf", "scale=1080:1920:force_original_aspect_ratio=decrease,pad=1080:1920:(ow-iw)/2:(oh-ih)/2",
# 音频转码成 AAC
# 音频编码
"-c:a", "aac",
"-b:a", "128k",
"-ar", "44100",
"-ac", "2",
# 避免缓冲过长导致 "no data"
"-muxdelay", "0",
"-shortest",
"-f", "flv", out_url
# 输出到 YouTube
"-f", "flv", out_url,
"-fflags", "+genpts+discardcorrupt+nobuffer+flush_packets",
"-avioflags", "direct",
"-rtbufsize", "100M",
]
# 平台需要自定义 header如 referer/origin
if extra_headers:
# 插到 -user_agent 后、-thread_queue_size 前
i1 = base.index("-user_agent") + 2
i2 = base.index("-thread_queue_size")
base[i1:i1] = ["-headers", extra_headers]
# 代理
if proxy_addr:
base = ["ffmpeg", "-http_proxy", proxy_addr] + base[1:]
# 打印命令便于排障
print("ffmpeg cmd:", " ".join(base if len(" ".join(base)) < 500 else base[:30] + ["..."]))
logger.info(f"启动推流:{input_url} -> YouTube")
p = subprocess.Popen(base, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
p = subprocess.Popen(base, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, bufsize=1)
# —— 启动 watchdog若 20s 没有任何输出,判定卡死,主循环会重启它 ——
p.last_out_ts = time.time()
def _reader():
try:
for line in iter(p.stdout.readline, b''):
if not line:
break
p.last_out_ts = time.time() # 任何输出都视为“仍在工作”
except Exception:
pass
t = threading.Thread(target=_reader, daemon=True)
t.start()
return p
def stop_ffmpeg(p: Optional[subprocess.Popen]):
@@ -422,12 +457,24 @@ def main():
except Exception as e:
logger.warning(f"URL 列表热更新失败:{e}")
if current_proc and current_url:
# 已在推流:检测是否仍直播 & 进程状态
if current_proc.poll() is not None:
color.print_colored(f"[{now_str()}] ffmpeg 退出,尝试切源", color.YELLOW)
current_proc = None
current_url = None
color.print_colored(f"[{now_str()}] ffmpeg 进程退出,重启推流", color.YELLOW)
stop_ffmpeg(current_proc)
current_proc = launch_ffmpeg(real_url, out_url, headers)
current_proc.last_out_ts = time.time()
continue
# 检测 ffmpeg 是否卡住10 秒无输出)
no_out_for = time.time() - getattr(current_proc, "last_out_ts", time.time())
if no_out_for > 20: # ✅ 改成 20 秒
color.print_colored(f"\n[{now_str()}] ffmpeg 20s 无输出,尝试重启当前源…", color.YELLOW)
stop_ffmpeg(current_proc)
current_proc = launch_ffmpeg(real_url, out_url, headers)
current_proc.last_out_ts = time.time()
continue
# 轻量检测:仍直播?
@@ -436,27 +483,22 @@ def main():
still_live = bool(port.get("is_live"))
except Exception as e:
logger.error(f"检测当前源失败:{e}")
still_live = False
still_live = True # ⚠️ 出错时默认仍在播,避免误切
if still_live:
elapsed = int(time.time() - start_time)
h, m = divmod(elapsed, 3600)
m, s = divmod(m, 60)
dur_str = f"{h:02d}:{m:02d}:{s:02d}"
print(
f"\r[{now_str()}] 正在转播({dur_str}){current_platform} | {current_anchor} | {current_url}",
end=""
)
print(f"\r[{now_str()}] 正在转播({dur_str}){current_platform} | {current_anchor} | {current_url}", end="")
time.sleep(check_interval_live)
continue
else:
color.print_colored(f"\n[{now_str()}] 当前源已停播,切换下一个…", color.YELLOW)
color.print_colored(f"\n[{now_str()}] 主播停播,切换下一个", color.YELLOW)
stop_ffmpeg(current_proc)
current_proc = None
current_url = None
# 落下去选择下一个
else:
# 未在推流:从 urls 顺序找第一个 is_live 的
if not urls: