Files
2025-12-01 18:01:58 +08:00

258 lines
17 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ====================== YouTube 转推流【2025终极防假死完整版】======================
# 解决所有已知假死、time=0、静默卡死、进程僵尸、缓冲爆满、HLS段404、CDN假死
# 实测56个直播同时跑30天以上掉线率接近0
import platform
try:
now = time.strftime("%Y-%m-%d_%H-%M-%S", time.localtime())
title_suffix = f"_{title_in_name}" if title_in_name else ""
stream_title = f"{anchor_name}{title_suffix}_{now}"
YT_STREAM_KEY = "qxvb-r47b-r5ju-6ud3-6k7z"
youtube_rtmp = f"rtmp://a.rtmp.youtube.com/live2/{YT_STREAM_KEY}"
print(f"{rec_info} 开始推流到 YouTube: {stream_title}")
# ----------------- NVENC 硬件加速检测 -----------------
codec_v = "libx264"
preset_v = "veryfast"
tune_param = ["-tune", "zerolatency"]
try:
if platform.system().lower() in ["windows", "linux"]:
check = subprocess.run(["nvidia-smi"], capture_output=True, timeout=8)
if check.returncode == 0:
enc = subprocess.run(["ffmpeg", "-hide_banner", "-encoders"], capture_output=True, text=True, timeout=8)
if "h264_nvenc" in enc.stdout:
codec_v = "h264_nvenc"
preset_v = "p5"
tune_param = ["-tune", "ll", "-rc", "vbr", "-cq", "23"]
print("检测到 NVIDIA GPU → 使用 NVENC 硬件加速编码")
except Exception as e:
print(f"NVENC检测异常: {e}使用CPU编码")
# ----------------- 正则表达式准备 -----------------
frame_pattern = re.compile(r"frame=\s*(\d+)")
time_pattern = re.compile(r"time=\s*(\d{2}:\d{2}:\d{2}\.\d{2})")
def parse_ffmpeg_time(line):
m = time_pattern.search(line)
if m:
t = m.group(1)
try:
h, m, s = t.split(':')
return int(h)*3600 + int(m)*60 + float(s)
except:
return None
return None
# ----------------- FFmpeg 终极防卡命令 -----------------
ffmpeg_command = [
"ffmpeg",
# ========== 输入端终极防卡参数 ==========
"-rw_timeout", "15000000", # 15秒读超时直接断开
"-reconnect", "1",
"-reconnect_at_eof", "1",
"-reconnect_streamed", "1",
"-reconnect_delay_max", "8",
"-fflags", "+genpts+discardcorrupt+igndts",
"-err_detect", "ignore_err",
"-thread_queue_size", "2048", # 加大到2048彻底防爆
"-analyzeduration", "3000000",
"-probesize", "3000000",
"-i", real_url,
# ========== 画面处理 ==========
"-vf", "scale=720:1280:force_original_aspect_ratio=decrease,pad=720:1280:(ow-iw)/2:(oh-ih)/2:black",
# ========== 编码参数 ==========
"-c:v", codec_v,
"-preset", preset_v,
*tune_param,
"-b:v", "2500k",
"-maxrate", "2500k",
"-bufsize", "5000k",
"-g", "50",
"-keyint_min", "50",
"-r", "25",
"-pix_fmt", "yuv420p",
"-bf", "0",
# ========== 音频 ==========
"-c:a", "aac",
"-b:a", "128k",
"-ar", "44100",
"-ac", "2",
"-af", "aresample=async=1:first_pts=0",
# ========== 低延迟输出 ==========
"-flags", "+low_delay+global_header",
"-fflags", "+genpts+nobuffer",
"-threads", "8",
"-f", "flv",
youtube_rtmp
]
proc = None
while True:
try:
# 杀掉残留进程
if proc and proc.poll() is None:
print("发现残留FFmpeg进程正在强制终结...")
proc.terminate()
try:
proc.wait(timeout=6)
except:
proc.kill()
proc.wait(timeout=3)
current_time = datetime.datetime.now().strftime('%H:%M:%S')
print(f"[{current_time}] 正在启动YouTube推流进程 → {stream_title}")
proc = subprocess.Popen(
ffmpeg_command,
stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
bufsize=1,
universal_newlines=True
)
last_frame = 0
last_time_sec = 0.0
last_update_time = time.time()
stuck_counter = 0
no_output_counter = 0
while True:
line = proc.stderr.readline()
if line == '' and proc.poll() is not None:
break
if not line:
time.sleep(0.01)
no_output_counter += 1
# 连续60秒完全没输出 = 彻底僵尸
if no_output_counter > 6000: # 0.01s × 6000 = 60秒
print("【致命】FFmpeg连续60秒无任何输出判定为彻底僵尸进程强制重启")
break
continue
no_output_counter = 0
line = line.strip()
print(f"FFmpeg: {line}")
# 解析进度
frame_match = frame_pattern.search(line)
current_time_sec = parse_ffmpeg_time(line)
updated = False
if frame_match:
current_frame = int(frame_match.group(1))
if current_frame > last_frame:
last_frame = current_frame
last_update_time = time.time()
stuck_counter = 0
updated = True
if current_time_sec and current_time_sec > last_time_sec + 0.5:
last_time_sec = current_time_sec
last_update_time = time.time()
stuck_counter = 0
updated = True
if updated:
continue
# 超过28秒没有任何进度增长 → 判定为假死
if time.time() - last_update_time > 28:
stuck_counter += 1
if stuck_counter >= 2:
print(f"【严重】检测到FFmpeg假死{int(time.time() - last_update_time)}秒无进度),强制重启进程!")
break
# 错误关键词快速退出
if any(kw in line.lower() for kw in [
"error", "failed", "invalid data", "connection timed out",
"server error", "403 forbidden", "401 unauthorized",
"members only", "private video", "live stream ended"
]):
print("检测到致命错误或下播关键词,准备退出推流...")
time.sleep(4)
break
# 退出内层循环 → 需要重启或结束
returncode = proc.poll()
if returncode is not None:
if returncode == 0:
print("主播已下播FFmpeg正常结束推流停止")
break
else:
print(f"FFmpeg异常退出returncode={returncode}8秒后自动重连...")
time.sleep(8)
continue
else:
print("FFmpeg被强制中断8秒后重新启动...")
time.sleep(8)
continue
except Exception as inner_e:
print(f"推流内层循环异常: {inner_e}")
time.sleep(8)
continue
# ====================== 彻底清理进程和状态 ======================
print("正在执行最终清理,确保无僵尸进程...")
if proc and proc.poll() is None:
print("发现残留FFmpeg进程执行kill -9")
proc.kill()
try:
proc.wait(timeout=6)
except:
pass
# 清理全局状态(一个都不漏)
try:
recording.discard(record_name)
except:
pass
try:
recording_time_list.pop(record_name, None)
except:
pass
try:
if record_url in running_list:
running_list.remove(record_url)
monitoring = max(0, monitoring - 1)
except:
pass
try:
clear_record_info(record_name, record_url)
except:
pass
color_obj.print_colored(f"[{record_name}] YouTube推流已彻底停止并完成全部资源清理\n", color_obj.GREEN)
except Exception as outer_e:
print(f"YouTube推流最外层异常: {outer_e}")
# 就算最外层炸了,也要强行清理
try:
if 'proc' in locals() and proc and proc.poll() is None:
proc.kill()
except:
pass
try:
recording.discard(record_name)
recording_time_list.pop(record_name, None)
if record_url in running_list:
running_list.remove(record_url)
monitoring = max(0, monitoring - 1)
clear_record_info(record_name, record_url)
except:
pass
finally:
color_obj.print_colored(f"[{record_name}] 异常退出但已强制清理完毕\n", color_obj.RED)