From 34990a29deb04864cec94f69553def0bc1d3c8e6 Mon Sep 17 00:00:00 2001 From: eric Date: Sat, 27 Sep 2025 16:22:38 +0800 Subject: [PATCH] =?UTF-8?q?'=E4=BC=98=E5=8C=96'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/URL_config.ini | 3 +- main.py | 120 ++++++++++++++++++++++++++++-------------- 2 files changed, 83 insertions(+), 40 deletions(-) diff --git a/config/URL_config.ini b/config/URL_config.ini index 4514dfd..008e741 100644 --- a/config/URL_config.ini +++ b/config/URL_config.ini @@ -1,4 +1,5 @@ -https://live.douyin.com/60379224194 +https://live.douyin.com/898038766356 +https://live.douyin.com/60379224194 https://live.douyin.com/72212955083 https://live.douyin.com/621118100231 https://live.douyin.com/280422307519 diff --git a/main.py b/main.py index a822e3b..23ed698 100644 --- a/main.py +++ b/main.py @@ -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: