From d4158e7f1a9b186212e104640a224481a9b2cc8a Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 30 Dec 2025 06:28:06 +0000 Subject: [PATCH] =?UTF-8?q?mpv=E6=96=AD=E7=BA=BF=E9=87=8D=E5=90=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/URL_config.ini | 9 +-- main.py | 165 ++++++++++++++++++------------------------ 2 files changed, 73 insertions(+), 101 deletions(-) diff --git a/config/URL_config.ini b/config/URL_config.ini index 9eeebf0..299cd39 100644 --- a/config/URL_config.ini +++ b/config/URL_config.ini @@ -1,8 +1 @@ -; 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,主播: 内向小学生 -https://live.douyin.com/9638535297,主播: 异度世界 +https://live.douyin.com/9638535297,主播: 异度世界 diff --git a/main.py b/main.py index fa24bab..57f9445 100644 --- a/main.py +++ b/main.py @@ -1623,11 +1623,12 @@ def start_record(url_data: tuple, count_variable: int = -1) -> None: else: - # ====================== 抖音 → HDMI 转播(mpv 低延迟 + 稳定重连 + 完整下播检测) ====================== + # ====================== 抖音 → HDMI 转播(FIFO + mpv 常驻 + 超强稳定重连 + 完整下播检测) ====================== import queue import unicodedata - import platform import traceback + # import os + # import datetime class StreamEnded(Exception): """主播真实下播时抛出,用于彻底退出循环""" @@ -1700,7 +1701,7 @@ def start_record(url_data: tuple, count_variable: int = -1) -> None: stream_title = sanitize_title(f"{anchor_name}{('_' + _clean_title) if _clean_title else ''}_{timestamp_str}") print(f"[INFO] {rec_info} 开始转播到 HDMI: {stream_title}") - # 编码参数(全志 A733 无 NVIDIA 硬件加速,使用 ultrafast 软件编码降低 CPU 负载) + # 编码参数保持原样 codec_v = "libx264" preset_v = "ultrafast" tune_param = ["-tune", "zerolatency"] @@ -1711,25 +1712,39 @@ def start_record(url_data: tuple, count_variable: int = -1) -> None: "Origin: https://live.douyin.com\r\n" ) - # ====================== mpv 命令(DRM 直驱 HDMI + 硬件解码 + 极致低延迟) ====================== + # ====================== FIFO 路径 ====================== + FIFO_PATH = "/tmp/douyin_hdmi.fifo" + if not os.path.exists(FIFO_PATH): + try: + os.mkfifo(FIFO_PATH) + print(f"[INFO] 创建 FIFO 文件: {FIFO_PATH}") + except Exception as e: + print(f"[ERROR] 创建 FIFO 失败: {e}") + # 如果创建失败,继续尝试使用(某些系统可能已存在) + + # ====================== mpv 命令(常驻) ====================== mpv_cmd = [ "mpv", - "-", # 从管道读取 FLV - "--vo=drm", # DRM 直接驱动 HDMI(无桌面环境最佳) - "--hwdec=v4l2m2m", # Allwinner VPU 硬件解码 H.264(大幅降低 CPU) + FIFO_PATH, + "--vo=drm", + "--hwdec=v4l2m2m", "--profile=low-latency", - "--no-cache", - "--untimed", - "--framedrop=vo", - "--video-sync=display-resample", + "--cache=yes", + "--cache-secs=20", "--demuxer-lavf-format=flv", + "--demuxer-lavf-o=fflags=+nobuffer", + "--untimed", + "--framedrop=decoder", + "--video-sync=display-resample", "--quiet", + "--loop-file=no", + "--no-input-default-bindings", "--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" ] - # ====================== FFmpeg 命令(720p 轻量转码) ====================== + # ====================== FFmpeg 命令(输出到 FIFO) ====================== ffmpeg_command = [ "ffmpeg", "-y", @@ -1741,7 +1756,8 @@ def start_record(url_data: tuple, count_variable: int = -1) -> None: "-reconnect", "1", "-reconnect_at_eof", "1", "-reconnect_streamed", "1", - "-reconnect_delay_max", "10", + "-reconnect_delay_max", "15", + "-timeout", "30000000", "-fflags", "+genpts+discardcorrupt+nobuffer+flush_packets", "-flags", "low_delay", "-err_detect", "ignore_err", @@ -1749,7 +1765,6 @@ def start_record(url_data: tuple, count_variable: int = -1) -> None: "-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", @@ -1777,53 +1792,41 @@ def start_record(url_data: tuple, count_variable: int = -1) -> None: "-flags", "+global_header", "-flvflags", "no_duration_filesize", - "-max_muxing_queue_size", "1024", # 移到输出选项末尾,防止 Broken pipe + "-max_muxing_queue_size", "1024", "-f", "flv", - "pipe:1" + FIFO_PATH ] - # ====================== 主循环:重连 + 下播检测 ====================== + # ====================== 主循环 ====================== proc = None mpv_proc = None - stderr_q = None - reader_t = None retry_delay = INITIAL_RETRY_DELAY try: + # 启动 mpv(常驻,只启动一次) + if mpv_proc is None or (mpv_proc.poll() is not None): + print(f"[INFO][{datetime.datetime.now():%H:%M:%S}] 启动 mpv 常驻播放 HDMI (FIFO: {FIFO_PATH})") + mpv_proc = subprocess.Popen( + mpv_cmd, + stdin=subprocess.DEVNULL, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL + ) + while True: try: if retry_delay > INITIAL_RETRY_DELAY: - print(f"[WARN] 防风控等待 {retry_delay} 秒后重连抖音源...") + print(f"[WARN] 等待 {retry_delay} 秒后重连抖音源...") time.sleep(retry_delay) + retry_delay = min(retry_delay * 2, MAX_RETRY_DELAY) - # 清理旧 mpv(先关闭 stdin,避免 ffmpeg 写 Broken pipe) - if mpv_proc and mpv_proc.poll() is None: - try: - mpv_proc.stdin.close() - mpv_proc.wait(timeout=5) - except: - mpv_proc.terminate() - mpv_proc = None - - # 清理旧 ffmpeg cleanup_proc(proc) - stderr_q = queue.Queue() - print(f"[INFO][{datetime.datetime.now():%H:%M:%S}] 启动抖音拉流 → HDMI 全屏显示 (mpv)") - - # 先启动 mpv - mpv_proc = subprocess.Popen( - mpv_cmd, - stdin=subprocess.PIPE, - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL - ) - - # 再启动 ffmpeg,输出到 mpv + print(f"[INFO][{datetime.datetime.now():%H:%M:%S}] 启动 FFmpeg 拉流 → FIFO → mpv") proc = subprocess.Popen( ffmpeg_command, stdin=subprocess.DEVNULL, - stdout=mpv_proc.stdin, + stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, text=True, bufsize=1, @@ -1831,76 +1834,74 @@ def start_record(url_data: tuple, count_variable: int = -1) -> None: ) proc.last_out_ts = time.time() - # 启动 stderr 读取线程 + stderr_q = queue.Queue() reader_t = threading.Thread(target=_stderr_reader_thread, args=(proc, stderr_q)) reader_t.daemon = True reader_t.start() start_time = last_frame_time = time.time() - # 监控循环:检测下播、卡顿、重连 while True: try: line = stderr_q.get(timeout=STDERR_QUEUE_TIMEOUT) except queue.Empty: - # 长时间无日志 → 僵死,重启 if time.time() - proc.last_out_ts > STALE_OUTPUT_SECONDS: - print("[WARN] FFmpeg 长时间无输出,强制重启进程") - cleanup_proc(proc) - if mpv_proc: - try: - mpv_proc.stdin.close() - except: - pass - mpv_proc = None + print("[WARN] FFmpeg 长时间无输出,强制重启 FFmpeg") break continue - if line is None: # reader 线程结束 - if proc.poll() is not None: - break - continue + if line is None: + break line = line.strip() if line.startswith("__ERR__READER__"): - print(f"[ERROR] stderr 读取异常: {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("[INFO] 检测到主播下播关键字,停止转播") - cleanup_proc(proc) raise StreamEnded() - # 更新最近帧时间 if re.search(r'frame=\s*\d+', line_lower) or "fps=" in line_lower: last_frame_time = time.time() + retry_delay = INITIAL_RETRY_DELAY - # 长时间无新帧 → 判定下播 if time.time() - start_time > START_CHECK_AFTER: if time.time() - last_frame_time > NO_FRAME_TIMEOUT: print(f"[INFO] {NO_FRAME_TIMEOUT}s 无新帧,判定主播已下播") - cleanup_proc(proc) raise StreamEnded() except StreamEnded: - print(f"[INFO] 主播已下播,停止 HDMI 转播 → {record_name}") - break - + raise except Exception as e: - print(f"[ERROR] 转播过程异常: {e}") + print(f"[ERROR] FFmpeg 运行异常: {e}") traceback.print_exc() - retry_delay = min(retry_delay * 2, MAX_RETRY_DELAY) continue - # ====================== 正常结束清理 ====================== + except StreamEnded: + print(f"[INFO] 主播已下播,停止 HDMI 转播 → {record_name}") + + except KeyboardInterrupt: + print("[INFO] 用户手动中断 (Ctrl+C)") + + except Exception as e: + print(f"[FATAL] HDMI 转播异常: {e}") + traceback.print_exc() + + finally: cleanup_proc(proc) + if mpv_proc and mpv_proc.poll() is None: + print("[INFO] 终止 mpv 进程...") + mpv_proc.terminate() + try: + mpv_proc.wait(timeout=5) + except: + mpv_proc.kill() + try: recording.discard(record_name) recording_time_list.pop(record_name, None) @@ -1913,28 +1914,6 @@ def start_record(url_data: tuple, count_variable: int = -1) -> None: pass color_obj.print_colored(f"[{record_name}] HDMI 转播已彻底停止并清理完毕\n", color_obj.GREEN) - return - - except Exception as e: - print(f"[FATAL] HDMI 转播最外层异常: {e}") - traceback.print_exc() - cleanup_proc(proc) - if mpv_proc and mpv_proc.poll() is None: - try: - mpv_proc.stdin.close() - except: - mpv_proc.terminate() - 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 - color_obj.print_colored(f"[{record_name}] HDMI 转播异常退出但已清理\n", color_obj.RED) - return count_time = time.time()