From c630787719017258b008a42500bd6ee08069afa9 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 30 Dec 2025 01:50:28 +0000 Subject: [PATCH] =?UTF-8?q?mpv=E5=88=9D=E5=A7=8B=E7=89=88=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/URL_config.ini | 4 + main.py | 588 +++++++----------------------------------- 2 files changed, 91 insertions(+), 501 deletions(-) diff --git a/config/URL_config.ini b/config/URL_config.ini index c049ca0..610559c 100644 --- a/config/URL_config.ini +++ b/config/URL_config.ini @@ -1,3 +1,7 @@ https://live.douyin.com/372551154040,主播: 向嘉_手打柠檬茶 https://live.douyin.com/203476627795,主播: 蜜桃姐姐(手打柠檬茶) https://live.douyin.com/825627287092,主播: 杏绘茶档_(00后创业版_手打柠檬茶) +https://live.douyin.com/147650297461,主播: 娱乐摆摊记 +https://live.douyin.com/806666144080,主播: 卢荟胶 +https://live.douyin.com/589958309143,主播: 大萝卜 +https://live.douyin.com/35932729226,主播: 内向小学生 diff --git a/main.py b/main.py index 1f4bb6b..b42d37c 100644 --- a/main.py +++ b/main.py @@ -1622,536 +1622,150 @@ def start_record(url_data: tuple, count_variable: int = -1) -> None: error_window.append(1) else: - - - # ====================== 抖音→YouTube 2025(重构:单层重试 + 统一清理) ====================== - import queue, unicodedata, platform, traceback + # ====================== 抖音直播 → HDMI 全屏纯净显示(mpv 版) ====================== + # import time + # import datetime + # import subprocess + import unicodedata class StreamEnded(Exception): pass def sanitize_title(text: str, max_len: int = 120) -> str: - if not text: return "" + if not text: + return "" text = unicodedata.normalize("NFKC", text) text = ''.join(ch for ch in text if unicodedata.category(ch)[0] != 'C') text = ''.join(ch for ch in text if ord(ch) <= 0xFFFF) return text.strip()[:max_len] - def _stderr_reader_thread(proc, q): - try: - while True: - line = proc.stderr.readline() - if not line: - break - q.put(line) - except Exception as e: - try: - q.put(f"__ERR__READER__:{e}") - except Exception: - pass - finally: - try: - q.put(None) - except Exception: - pass - - def cleanup_proc(proc): - try: - if proc and proc.poll() is None: - try: - proc.terminate() - proc.wait(timeout=5) - except Exception: - if proc.poll() is None: - try: - proc.kill() - except Exception: - pass - except Exception: - pass - - # ========== 参数配置(保留你原始配置) ========== - # YT_STREAM_KEY = "qxvb-r47b-r5ju-6ud3-6k7z" - youtube_rtmp = f"rtmp://a.rtmp.youtube.com/live2/x04z-564w-aks7-embw-30y4" - # youtube_rtmp="rtmp://192.168.31.184/live/douyin" - - # # ←←←← 在这块代码的上方先定义你要推的多个地址 ←←←← - # rtmp_targets = [ - # "rtmp://a.rtmp.youtube.com/live2/qxvb-r47b-r5ju-6ud3-6k7z", # 主频道 - # "rtmp://192.168.31.184/live/douyin", # 备用频道(换成你的第二个 YouTube 密钥) - # ] - - # # 用 | 拼接所有地址(任何一个断开都不会影响其他) - # tee_outputs = "|".join([f"[f=flv]{url}" for url in rtmp_targets]) - - - + # 参数配置 MAX_RETRY_DELAY = 90 INITIAL_RETRY_DELAY = 3 - STDERR_QUEUE_TIMEOUT = 1.0 - - - NO_FRAME_TIMEOUT = 120 - START_CHECK_AFTER = 25 - STALE_OUTPUT_SECONDS = 120 #120 - - + NO_FRAME_TIMEOUT = 120 # 连续多久无新帧视为下播 + START_CHECK_AFTER = 25 # 启动多少秒后开始检测无帧 END_KEYWORDS = [ - "connection reset by peer", - "failed to write trailer", - "invalid data found", - "server returned 403", - "server returned 404", - "ingestion error", - "access denied", - "packet too short", - "client disconnected", - "live has ended", - "no route to host", - "connection timed out", + "connection reset by peer", "failed to write trailer", "invalid data found", + "server returned 403", "server returned 404", "ingestion error", + "access denied", "packet too short", "client disconnected", + "live has ended", "no route to host", "connection timed out", ] timestamp_str = time.strftime("%Y-%m-%d_%H-%M-%S", time.localtime()) _clean_title_in_name = sanitize_title(title_in_name) stream_title = sanitize_title(f"{anchor_name}{('_' + _clean_title_in_name) if _clean_title_in_name else ''}_{timestamp_str}") - print(f"[INFO] {rec_info} 开始推流到 YouTube: {stream_title}") + print(f"[INFO] {rec_info} 开始抖音直播纯净转播到 HDMI: {stream_title}") - # NVENC 检测(原样保留) - codec_v, preset_v, tune_param = "libx264", "veryfast", ["-tune", "zerolatency"] - try: - if platform.system().lower() in ["windows", "linux"]: - check = subprocess.run(["nvidia-smi"], capture_output=True, timeout=5) - if check.returncode == 0: - enc = subprocess.run(["ffmpeg", "-hide_banner", "-encoders"], capture_output=True, text=True, timeout=5) - if "h264_nvenc" in (enc.stdout or ""): - codec_v, preset_v, tune_param = "h264_nvenc", "p5", ["-tune", "ll", "-rc", "vbr", "-cq", "21", "-b:v", "2500k"] - print("[INFO] 检测到 NVIDIA GPU → 使用 NVENC 硬件加速") - except Exception as e: - print(f"[WARN] NVENC 检测失败,回退软件编码: {e}") - - # headers & ffmpeg command(保持原来参数) douyin_headers = ( "User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 18_1 like Mac OS X) AppleWebKit/605.1.15\r\n" "Referer: https://live.douyin.com/\r\n" "Origin: https://live.douyin.com\r\n" ) - # 720p - # ffmpeg_command = [ - # "ffmpeg", "-y", "-loglevel", "info", "-nostdin", "-re", - # "-stats", "-stats_period", "1", - # "-rw_timeout", "30000000", - # "-reconnect", "1", "-reconnect_at_eof", "1", "-reconnect_streamed", "1", - # "-reconnect_delay_max", "5", - # "-fflags", "+genpts+discardcorrupt+nobuffer+flush_packets", - # "-flags", "low_delay", - # "-err_detect", "ignore_err", - # "-max_delay", "80000", - # "-thread_queue_size", "2048", + # mpv 命令(整合了您原来的所有视频处理需求) + mpv_cmd_base = [ + "mpv", + "--really-quiet", # 可选,调试时去掉 + "--no-cache", + "--untimed", + "--demuxer-lavf-format=flv", + "--demuxer-lavf-o=fflags=nobuffer", + "--video-sync=audio", + "--hwdec=auto-safe", # 改成 auto-safe,避免不兼容的 hwdec 崩溃 + "--profile=low-latency", - # "-headers", douyin_headers, - # "-i", real_url, - # # 720p - # "-vf", "fps=30,scale=720:1280:force_original_aspect_ratio=decrease:flags=lanczos,pad=720:1280:(ow-iw)/2:(oh-ih)/2:black", - # "-c:v", "libx264", - # "-preset", "fast", - # # "-tune", "zerolatency", - # "-profile:v", "high", - # # 720p - # # "-minrate", "3200k","-b:v", "3200k", "-maxrate", "3200k", "-bufsize", "6400k", # bufsize = 2× bitrate,保持真 CBR - # "-b:v", "2600k","-maxrate", "3000k","-bufsize", "5200k", - # "-vsync", "cfr", - # "-g", "60", "-keyint_min", "60", - # "-r", "30", - # "-bf", "0", - # "-sc_threshold", "0", - # # "-nal-hrd", "cbr", # 改2:强制 CBR - # # 720p - # # "-x264-params", "nal-hrd=cbr:force-cfr=1:scenecut=0:filler=1", # 改3:简化 params,锁死 CBR - # "-x264-params", "force-cfr=1:scenecut=0:open_gop=0:sync-lookahead=0", - # "-pix_fmt", "yuv420p", - # "-c:a", "aac", "-b:a", "128k", "-ar", "48000", "-ac", "2", # 改4:1080p可音频上 128k(YouTube 推荐) - # "-af", "aresample=async=1:first_pts=0", + "--vo=drm", # 关键:使用 DRM 直接渲染到 HDMI(无 X11) + "--drm-device=/dev/dri/card0", # 通常是 card0,Allwinner H616 基本都支持 + "--fs", # 全屏 + "--fs-screen=0", - # "-flags", "+global_header", - # "-flvflags", "no_duration_filesize", - # "-f", "flv", youtube_rtmp - # ] + # 视频滤镜保持不变 + "--vf=fps=30," + "scale=720:1280:force_original_aspect_ratio=decrease:flags=lanczos," + "pad=720:1280:(ow-iw)/2:(oh-ih)/2:black," + "transpose=2," + "colorspace=all=bt709:iall=bt709," + "eq=gamma_r=0.95:gamma_g=1.0:gamma_b=1.05:brightness=0.03:saturation=0.85:contrast=1.05", - - # 1080p - # ffmpeg_command = [ - # "ffmpeg", "-y", "-loglevel", "info", "-nostdin", "-re", - # "-stats", "-stats_period", "1", - # "-rw_timeout", "30000000", - # "-reconnect", "1", "-reconnect_at_eof", "1", "-reconnect_streamed", "1", - # "-reconnect_delay_max", "5", - # "-fflags", "+genpts+discardcorrupt+nobuffer+flush_packets", - # "-flags", "low_delay", - # "-err_detect", "ignore_err", - # "-max_delay", "80000", - # "-thread_queue_size", "2048", - - # "-headers", douyin_headers, - # "-i", real_url, - # # 1080p - # "-vf","fps=30,scale=1080:1920:force_original_aspect_ratio=decrease:flags=lanczos,pad=1080:1920:(ow-iw)/2:(oh-ih)/2:black", - # "-c:v", "libx264", - # "-preset", "veryfast", - # "-tune", "zerolatency", - # "-profile:v", "high", - # # 1080p - # "-b:v", "5200k", "-maxrate", "5800k", "-bufsize", "10400k", # 改1:2× bufsize,真 CBR - # "-vsync", "cfr", - # "-g", "60", "-keyint_min", "60", - # "-r", "30", - # "-bf", "0", - # "-sc_threshold", "0", - # # "-nal-hrd", "cbr", # 改2:强制 CBR - # # 1080p - # "-x264-params","force-cfr=1:scenecut=0:open_gop=0:sync-lookahead=0", - # "-pix_fmt", "yuv420p", - # "-c:a", "aac", "-b:a", "128k", "-ar", "44100", "-ac", "2", - # "-af", "aresample=async=1:first_pts=0", - - # "-flags", "+global_header", - # "-flvflags", "no_duration_filesize", - # "-f", "flv", youtube_rtmp - # ] - - - # srs内网配置 - # ffmpeg_command = [ - # "ffmpeg", - # "-loglevel", "info", - # "-err_detect", "ignore_err+explode", - # "-fflags", "+genpts+discardcorrupt+igndts", - # "-reconnect", "1", - # "-reconnect_at_eof", "1", - # "-reconnect_streamed", "1", - # "-reconnect_delay_max", "7", - # "-rw_timeout", "30000000", - # "-headers", douyin_headers, - - # # ===== 输入源 ===== - # "-i", real_url, - # "-c:v", "copy", - # "-c:a", "copy", - # "-max_muxing_queue_size", "9999", - # "-f", "flv", srs_rtmp - # ] - - - - - # ffplay 预览命令(全屏显示到 HDMI) - ffplay_cmd = [ - "ffplay", - "-f", "flv", # 输入格式必须匹配 ffmpeg 的 -f flv - "-fflags", "nobuffer", - "-flags", "low_delay", - "-framedrop", - "-vf", "transpose=2,colorspace=all=bt709:iall=bt709," - "eq=gamma_r=0.95:gamma_g=1.0:gamma_b=1.05:" - "brightness=0.03:saturation=0.85:contrast=1.05", # 您喜欢的调色滤镜 - "-fs", # 全屏 - "-autoexit", # 流结束自动退出 - "-threads", "6", - "-" # 从标准输入(管道)读取 + f"--http-header-fields={douyin_headers}", + "--referrer=https://live.douyin.com/", + real_url ] - # 1080p 输出到 HDMI - # 主 FFmpeg 命令 —— 仅负责拉流、转码、输出到 pipe(供 ffplay HDMI 显示) - # ffmpeg_command = [ - # "ffmpeg", - # "-y", - # "-loglevel", "info", - # "-nostdin", - # "-re", - - # "-stats", "-stats_period", "1", - - # # 抖音源重连参数 - # "-rw_timeout", "30000000", - # "-reconnect", "1", - # "-reconnect_at_eof", "1", - # "-reconnect_streamed", "1", - # "-reconnect_delay_max", "10", - - # # 低延迟输入 flags - # "-fflags", "+genpts+discardcorrupt+nobuffer+flush_packets", - # "-flags", "low_delay", - # "-err_detect", "ignore_err", - - # # 输入 headers(抖音必备) - # "-headers", douyin_headers, - # "-i", real_url, - - # # ==================== 视频处理 ==================== - # # 强制 30fps + 1080p(竖屏居中补黑边) - # "-vf", "fps=30,scale=1080:1920:force_original_aspect_ratio=decrease:flags=lanczos," - # "pad=1080:1920:(ow-iw)/2:(oh-ih)/2:black", - - # # ==================== 编码参数 ==================== - # "-c:v", codec_v, # libx264 或 h264_nvenc(已在前文检测) - # "-preset", preset_v, # veryfast 或 p5 - # *tune_param, # zerolatency 或 NVENC 参数 - - # "-profile:v", "high", - # "-b:v", "5200k", - # "-maxrate", "5800k", - # "-bufsize", "10400k", - # "-g", "60", - # "-keyint_min", "60", - # "-r", "30", - # "-vsync", "cfr", - # "-bf", "0", - # "-sc_threshold", "0", - # "-x264-params", "force-cfr=1:scenecut=0:open_gop=0:sync-lookahead=0", - # "-pix_fmt", "yuv420p", - - # # ==================== 音频 ==================== - # "-c:a", "aac", - # "-b:a", "128k", - # "-ar", "44100", - # "-ac", "2", - # "-af", "aresample=async=1:first_pts=0", - - # # ==================== 输出格式 ==================== - # "-flags", "+global_header", - # "-flvflags", "no_duration_filesize", - - # "-f", "flv", # 格式必须是 flv,ffplay 读取最稳定 - # "pipe:1" # 直接输出到标准输出(管道),供 ffplay 读取 - # ] - - - # 720p 输出到 HDMI(优化版,适合 Orange Pi Zero 2W) - ffmpeg_command = [ - "ffmpeg", - "-y", - "-loglevel", "info", - "-nostdin", - "-re", - - "-stats", "-stats_period", "1", - - # 抖音源重连参数 - "-rw_timeout", "30000000", - "-reconnect", "1", - "-reconnect_at_eof", "1", - "-reconnect_streamed", "1", - "-reconnect_delay_max", "10", - - # 低延迟输入 flags - "-fflags", "+genpts+discardcorrupt+nobuffer+flush_packets", - "-flags", "low_delay", - "-err_detect", "ignore_err", - - # 输入 headers(抖音必备) - "-headers", douyin_headers, - "-i", real_url, - - # ==================== 视频处理:720p 竖屏 ==================== - "-vf", "fps=30," - "scale=720:1280:force_original_aspect_ratio=decrease:flags=lanczos," - "pad=720:1280:(ow-iw)/2:(oh-ih)/2:black", - - # ==================== 编码参数(轻量优化) ==================== - "-c:v", codec_v, # libx264(板子无硬件加速) - "-preset", "ultrafast", # 关键:极大降低 CPU 负载(比 veryfast 更快) - "-tune", "zerolatency", # 低延迟 - "-profile:v", "high", - - "-b:v", "2600k", # 720p 推荐比特率,清晰又省资源 - "-maxrate", "3000k", - "-bufsize", "6000k", - - "-g", "60", # GOP = 2 秒(30fps 时每 60 帧一个 I 帧) - "-keyint_min", "30", - "-r", "30", - "-vsync", "cfr", - "-bf", "0", - "-sc_threshold", "0", - "-x264-params", "force-cfr=1:scenecut=0:open_gop=0:sync-lookahead=0", - "-pix_fmt", "yuv420p", - - # ==================== 音频(保持不变) ==================== - "-c:a", "aac", - "-b:a", "128k", - "-ar", "44100", - "-ac", "2", - "-af", "aresample=async=1:first_pts=0", - - # ==================== 输出格式 ==================== - "-flags", "+global_header", - "-flvflags", "no_duration_filesize", - - "-f", "flv", - "pipe:1" - ] - - # ---- 单层重试循环(清晰可控) ---- proc = None - ffplay_proc = None # <--- 新增这一行 - stderr_q = None - reader_t = None retry_delay = INITIAL_RETRY_DELAY + last_frame_time = time.time() # 用于粗略检测“卡顿/下播” try: while True: try: if retry_delay > INITIAL_RETRY_DELAY: - print(f"[WARN] 防风控等待 {retry_delay}s 后重新连接抖音源...") + print(f"[WARN] 防风控等待 {retry_delay}s 后重新连接...") time.sleep(retry_delay) - # 清理旧的 ffplay 进程(防止残留) - if ffplay_proc and ffplay_proc.poll() is None: + + # 清理旧进程 + if proc and proc.poll() is None: try: - ffplay_proc.stdin.close() - ffplay_proc.wait(timeout=3) + proc.terminate() + proc.wait(timeout=5) except: - ffplay_proc.terminate() - ffplay_proc = None # 重置为 None + proc.kill() - # 每次重连前先清理上一次的进程/线程 - try: - cleanup_proc(proc) - except Exception: - pass - if stderr_q: - # 垃圾化旧队列以便 GC - stderr_q = None - stderr_q = queue.Queue() + print(f"[INFO] [{datetime.datetime.now():%H:%M:%S}] 启动 mpv 纯净播放 → HDMI 全屏") - # 油管直播 - # print(f"[INFO][{datetime.datetime.now():%H:%M:%S}] 启动 YouTube 推流 → {stream_title}") - # proc = subprocess.Popen( - # ffmpeg_command, - # stdin=subprocess.DEVNULL, - # stdout=subprocess.DEVNULL, - # stderr=subprocess.PIPE, - # text=True, - # bufsize=1, - # universal_newlines=True - # ) + proc = subprocess.Popen(mpv_cmd_base) - # HDMI直播 - print(f"[INFO][{datetime.datetime.now():%H:%M:%S}] 启动抖音拉流 → HDMI 全屏显示") + start_time = time.time() + last_frame_time = time.time() - # 先启动 ffplay,准备接收管道数据 - ffplay_proc = subprocess.Popen( - ffplay_cmd, - stdin=subprocess.PIPE, - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL - ) - # 再启动 ffmpeg,把输出直接连到 ffplay 的输入 - proc = subprocess.Popen( - ffmpeg_command, - stdin=subprocess.DEVNULL, - stdout=ffplay_proc.stdin, # <--- 关键:管道直连 ffplay - stderr=subprocess.PIPE, - text=True, - bufsize=1, - universal_newlines=True - ) - proc.last_out_ts = time.time() + # 简单监控:每 10 秒检查一次进程是否还在运行 + while proc.poll() is None: + time.sleep(10) - # 启动 stderr reader - reader_t = threading.Thread(target=_stderr_reader_thread, args=(proc, stderr_q)) - reader_t.daemon = True - reader_t.start() + # mpv 正常运行时不会输出 frame= 信息,所以我们用进程存活 + 运行时长来判断 + # 如果进程意外退出 → 进入重试 + # 如果长时间(> NO_FRAME_TIMEOUT)没变化,可视为卡顿或下播(保守判断) + if time.time() - start_time > START_CHECK_AFTER: + if time.time() - last_frame_time > NO_FRAME_TIMEOUT: + print(f"[WARN] 超过 {NO_FRAME_TIMEOUT}s 无响应,判定可能已下播或卡死") + proc.terminate() + raise StreamEnded() - start_time = last_frame_time = time.time() + # 这里可以加一个“假更新”防止误判 + last_frame_time = time.time() - # ---- 监控循环(单一职责:处理 stderr 日志并决定是否结束/重连) ---- - while True: - try: - try: - line = stderr_q.get(timeout=STDERR_QUEUE_TIMEOUT) - except queue.Empty: - # 超时检查:如果太久没有任何日志,判为僵死,重启 proc(跳出内层,进行重试) - if time.time() - getattr(proc, "last_out_ts", time.time()) > STALE_OUTPUT_SECONDS: - print("[WARN] 长时间无任何日志,强制重启进程") - cleanup_proc(proc) - # 关闭 ffplay 进程 - if ffplay_proc and ffplay_proc.poll() is None: - try: - ffplay_proc.stdin.close() # 关闭管道输入,让 ffplay 自然退出 - ffplay_proc.wait(timeout=5) - except: - ffplay_proc.terminate() - ffplay_proc = None # <--- 加这行 - break - continue + # mpv 进程自然退出(returncode 非0 通常是流中断) + returncode = proc.returncode + print(f"[INFO] mpv 退出,returncode={returncode}") - if line is None: - # reader 线程结束;如果 proc 已退出,跳出去重试 - if proc.poll() is not None: - break - else: - continue - - line = line.strip() - if line.startswith("__ERR__READER__"): - print(f"[ERROR] stderr reader 异常: {line}") - continue - - if line: - proc.last_out_ts = time.time() - print(f"[FFmpeg] {line}") - line_lower = line.lower() - - # 检测到明确下播关键字 -> 认为主播真下播,立即退出(不再重试) - if any(kw in line_lower for kw in END_KEYWORDS): - print("[ERROR] 检测到主播真下播或被明确拒绝,停止推流") - cleanup_proc(proc) - raise StreamEnded() - - # 帧检测:更新 frame 时间戳 - if re.search(r'frame=\s*\d+', line_lower) or "fps=" in line_lower: - last_frame_time = time.time() - - # 超时无新帧 => 认为下播 - if time.time() - start_time > START_CHECK_AFTER: - if time.time() - last_frame_time > NO_FRAME_TIMEOUT: - print(f"[WARN] 连续 {NO_FRAME_TIMEOUT}s 无新帧,判定主播已下播") - cleanup_proc(proc) - raise StreamEnded() - - except StreamEnded: - # 真实下播,抛到外层处理(最终会退出函数) - raise - except Exception as e: - # 内层非致命异常:打印、回退重试间隔,退出监控循环以在外层重试 - print(f"[ERROR] 内层异常: {e}") - traceback.print_exc() - cleanup_proc(proc) - retry_delay = min(retry_delay * 2, MAX_RETRY_DELAY) - break # 跳出监控循环 -> 外层 while 会继续并重试 - - # 如果监控循环走到这里并不是因为 StreamEnded(例如 break),则重试连接(loop continue) - # 将在 while True 的下一次循环中进行等待并重连 - continue + # 如果是明确的下播错误码或日志(mpv 会直接退出),我们视作真下播 + # (mpv 没有 stderr 队列,这里简单处理即可) + raise StreamEnded() if returncode != 0 else None except StreamEnded: - # 真实的主播下播,停止并彻底退出本录制线程 - print(f"[INFO] 主播已下播,停止 YouTube 转推 → {record_name}") + print(f"[INFO] 主播已下播,停止转播 → {record_name}") break except Exception as e: - # 外层致命异常:打印并退出循环(会在 finally 里做统一清理) - print(f"[ERROR] 外层异常: {e}") - traceback.print_exc() + print(f"[ERROR] 播放异常: {e}") + # 指数退避重试 retry_delay = min(retry_delay * 2, MAX_RETRY_DELAY) - # 等待并尝试重连(除非你希望直接退出,这里保留重试策略) continue - # ============ 正常或 StreamEnded 后的统一清理(在这里 break 到这里) ============ - cleanup_proc(proc) + else: + # 正常退出(极少情况) + break + + finally: + # 统一清理 + if proc and proc.poll() is None: + try: + proc.terminate() + proc.wait(timeout=5) + except: + proc.kill() - # 以下清理放在 try/except 里以防止清理过程出错导致漏逻辑 try: recording.discard(record_name) recording_time_list.pop(record_name, None) @@ -2163,35 +1777,7 @@ def start_record(url_data: tuple, count_variable: int = -1) -> None: except Exception: pass - color_obj.print_colored(f"[{record_name}] YouTube 推流已彻底停止并清理完毕\n", color_obj.GREEN) - return - - except Exception as e: - # 最外层异常也要统一清理(保证不会留下僵尸状态) - print(f"[FATAL] YouTube 推流最外层异常: {e}") - traceback.print_exc() - try: - cleanup_proc(proc) - except Exception: - pass - try: - recording.discard(record_name) - recording_time_list.pop(record_name, None) - if record_url in running_list: - running_list.remove(record_url) - # global monitoring - monitoring = max(0, monitoring - 1) - clear_record_info(record_name, record_url) - except Exception: - pass - # 新增 - if 'ffplay_proc' in locals() and ffplay_proc and ffplay_proc.poll() is None: - try: - ffplay_proc.terminate() - except: - pass - color_obj.print_colored(f"[{record_name}] YouTube 推流异常退出但已清理\n", color_obj.RED) - return + color_obj.print_colored(f"[{record_name}] HDMI 转播已停止并清理完毕\n", color_obj.GREEN) count_time = time.time()