From a7b7761e78301beb10efc7cbdf35b0f4f2429b2b Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 30 Dec 2025 10:03:08 +0000 Subject: [PATCH] lajilaji --- config/URL_config.ini | 2 + main.py | 98 ++++++++++++++++--------------------------- 2 files changed, 39 insertions(+), 61 deletions(-) diff --git a/config/URL_config.ini b/config/URL_config.ini index 299cd39..0c42bd0 100644 --- a/config/URL_config.ini +++ b/config/URL_config.ini @@ -1 +1,3 @@ https://live.douyin.com/9638535297,主播: 异度世界 +https://live.douyin.com/481942173701,主播: 王火火拌辣串 +https://live.douyin.com/342979433932,主播: 七七古早芋泥饼 diff --git a/main.py b/main.py index 6b92e5a..1b9b8b7 100644 --- a/main.py +++ b/main.py @@ -1622,18 +1622,15 @@ def start_record(url_data: tuple, count_variable: int = -1) -> None: error_window.append(1) else: - # ====================== 抖音 → HDMI 转播(FIFO + mpv 常驻 + 卡死自动重启 + 超强稳定重连) ====================== import queue import unicodedata import traceback # import os # import datetime - class StreamEnded(Exception): """主播真实下播时抛出,用于彻底退出循环""" pass - def sanitize_title(text: str, max_len: int = 120) -> str: if not text: return "" @@ -1641,7 +1638,6 @@ def start_record(url_data: tuple, count_variable: int = -1) -> None: 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: @@ -1659,7 +1655,6 @@ def start_record(url_data: tuple, count_variable: int = -1) -> None: q.put(None) except: pass - def cleanup_proc(proc): try: if proc and proc.poll() is None: @@ -1671,39 +1666,32 @@ def start_record(url_data: tuple, count_variable: int = -1) -> None: proc.kill() except: pass - # ====================== 配置参数 ====================== - MAX_RETRY_DELAY = 90 # 最大重连等待时间(秒),指数退避上限,防止抖音风控 - INITIAL_RETRY_DELAY = 3 # 首次重连等待时间(秒),从这个值开始翻倍 - STDERR_QUEUE_TIMEOUT = 1.0 # 读取 FFmpeg stderr 的队列超时(秒),用于及时发现卡顿 - - NO_FRAME_TIMEOUT = 120 # 连续多少秒无新帧(frame= 未增长)就判定主播下播(秒) - START_CHECK_AFTER = 25 # 开播后至少等多少秒才开始无帧检测,避免刚开播误判(秒) - STALE_OUTPUT_SECONDS = 120 # FFmpeg 连续多少秒完全无日志输出就认为僵死,需要重启(秒) - + MAX_RETRY_DELAY = 90 # 最大重连等待时间(秒),指数退避上限,防止抖音风控 + INITIAL_RETRY_DELAY = 3 # 首次重连等待时间(秒),从这个值开始翻倍 + STDERR_QUEUE_TIMEOUT = 1.0 # 读取 FFmpeg stderr 的队列超时(秒),用于及时发现卡顿 + NO_FRAME_TIMEOUT = 120 # 连续多少秒无新帧(frame= 未增长)就判定主播下播(秒) + START_CHECK_AFTER = 25 # 开播后至少等多少秒才开始无帧检测,避免刚开播误判(秒) + STALE_OUTPUT_SECONDS = 120 # FFmpeg 连续多少秒完全无日志输出就认为僵死,需要重启(秒) 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", ] - timestamp_str = time.strftime("%Y-%m-%d_%H-%M-%S", time.localtime()) _clean_title = sanitize_title(title_in_name) stream_title = sanitize_title(f"{anchor_name}{('_' + _clean_title) if _clean_title else ''}_{timestamp_str}") print(f"[INFO] {rec_info} 开始转播到 HDMI: {stream_title}") - # 编码参数(完全保留原样) codec_v = "libx264" preset_v = "ultrafast" tune_param = ["-tune", "zerolatency"] - 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" ) - # ====================== FIFO ====================== FIFO_PATH = "/tmp/douyin_hdmi.fifo" if not os.path.exists(FIFO_PATH): @@ -1712,30 +1700,27 @@ def start_record(url_data: tuple, count_variable: int = -1) -> None: print(f"[INFO] 创建 FIFO 文件: {FIFO_PATH}") except Exception as e: print(f"[ERROR] 创建 FIFO 失败(可能已存在): {e}") - # ====================== mpv 命令 ====================== + # ====================== mpv ====================== mpv_cmd = [ "mpv", FIFO_PATH, - "--vo=drm", # 不支持会自动降级 + "--vo=drm", "--hwdec=v4l2m2m", "--profile=low-latency", "--cache=yes", - "--cache-secs=20", + "--cache-secs=5", # 缓存越小延迟越低 "--demuxer-lavf-format=flv", "--demuxer-lavf-o=fflags=+nobuffer", - "--untimed", "--framedrop=decoder", - "--video-sync=display-resample", + "--video-sync=audio", # 音频同步视频 "--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" + "--fullscreen", + "--video-unscaled=no" ] - # ====================== FFmpeg 命令(输出到 FIFO) ====================== ffmpeg_command = [ "ffmpeg", "-y", @@ -1752,13 +1737,14 @@ def start_record(url_data: tuple, count_variable: int = -1) -> None: "-fflags", "+genpts+discardcorrupt+nobuffer+flush_packets", "-flags", "low_delay", "-err_detect", "ignore_err", - "-headers", douyin_headers, "-i", real_url, - - "-vf", "fps=30,scale=720:1280:force_original_aspect_ratio=decrease:flags=lanczos," - "pad=720:1280:(ow-iw)/2:(oh-ih)/2:black", - + # ================= 视频处理(保持比例 + 顺时针旋转90度 + 全屏 HDMI 720x1280) ================= + "-vf", ( + "transpose=-1," # 顺时针旋转90度 + "scale=iw*min(720/iw\\,1280/ih):ih*min(720/iw\\,1280/ih):flags=lanczos," + "pad=720:1280:(720-iw*min(720/iw\\,1280/ih))/2:(1280-ih*min(720/iw\\,1280/ih))/2:black" + ), "-c:v", codec_v, "-preset", preset_v, *tune_param, @@ -1774,26 +1760,34 @@ def start_record(url_data: tuple, count_variable: int = -1) -> None: "-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", + "-ar", "48000", "-ac", "2", "-af", "aresample=async=1:first_pts=0", - - "-flags", "+global_header", + # ================= 输出 ================= "-flvflags", "no_duration_filesize", "-max_muxing_queue_size", "1024", "-f", "flv", FIFO_PATH ] + + + + + + + + + + # ====================== 主循环 ====================== proc = None mpv_proc = None retry_delay = INITIAL_RETRY_DELAY - last_buffer_warning_time = 0 # 避免频繁重启 - + last_buffer_warning_time = 0 # 避免频繁重启 try: while True: # 启动/重启 mpv(常驻) @@ -1807,17 +1801,14 @@ def start_record(url_data: tuple, count_variable: int = -1) -> None: stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL ) - time.sleep(1) # 给 mpv 启动时间 - + time.sleep(1) # 给 mpv 启动时间 # 重连等待 if retry_delay > INITIAL_RETRY_DELAY: print(f"[WARN] 等待 {retry_delay} 秒后重连抖音源...") time.sleep(retry_delay) retry_delay = min(retry_delay * 2, MAX_RETRY_DELAY) - # 清理旧 FFmpeg cleanup_proc(proc) - print(f"[INFO][{datetime.datetime.now():%H:%M:%S}] 启动 FFmpeg 拉流 → FIFO") proc = subprocess.Popen( ffmpeg_command, @@ -1829,15 +1820,12 @@ def start_record(url_data: tuple, count_variable: int = -1) -> None: universal_newlines=True ) proc.last_out_ts = time.time() - 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() last_buffer_warning_time = 0 - while True: try: line = stderr_q.get(timeout=STDERR_QUEUE_TIMEOUT) @@ -1846,19 +1834,15 @@ def start_record(url_data: tuple, count_variable: int = -1) -> None: print("[WARN] FFmpeg 长时间无输出,强制重启 FFmpeg") break continue - if line is None: break - line = line.strip() if line.startswith("__ERR__READER__"): continue - if line: proc.last_out_ts = time.time() print(f"[FFmpeg] {line}") line_lower = line.lower() - # 关键:检测 mpv 卡死(buffers queued 1000) if "1000 buffers queued" in line_lower: current_time = time.time() @@ -1867,35 +1851,28 @@ def start_record(url_data: tuple, count_variable: int = -1) -> None: last_buffer_warning_time = current_time if mpv_proc and mpv_proc.poll() is None: mpv_proc.kill() - mpv_proc = None # 外层循环会重新启动 - break # 退出内循环,触发重启 - + mpv_proc = None # 外层循环会重新启动 + break # 退出内循环,触发重启 # 真实下播检测 if any(kw in line_lower for kw in END_KEYWORDS): print("[INFO] 检测到主播下播关键字,停止转播") raise StreamEnded() - # 有新帧到来 if re.search(r'frame=\s*\d+', 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 无新帧,判定主播已下播") raise StreamEnded() - 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: @@ -1905,7 +1882,6 @@ def start_record(url_data: tuple, count_variable: int = -1) -> None: mpv_proc.wait(timeout=5) except: mpv_proc.kill() - try: recording.discard(record_name) recording_time_list.pop(record_name, None) @@ -1916,15 +1892,15 @@ def start_record(url_data: tuple, count_variable: int = -1) -> None: clear_record_info(record_name, record_url) except: pass - color_obj.print_colored(f"[{record_name}] HDMI 转播已彻底停止并清理完毕\n", color_obj.GREEN) - count_time = time.time() + + except Exception as e: logger.error(f"错误信息: {e} 发生错误的行数: {e.__traceback__.tb_lineno}") with max_request_lock: