mpv设计ok

This commit is contained in:
Your Name
2025-12-30 06:42:07 +00:00
parent d4158e7f1a
commit 7ac4c7ff90

188
main.py
View File

@@ -1623,7 +1623,7 @@ def start_record(url_data: tuple, count_variable: int = -1) -> None:
else:
# ====================== 抖音 → HDMI 转播FIFO + mpv 常驻 + 超强稳定重连 + 完整下播检测 ======================
# ====================== 抖音 → HDMI 转播FIFO + mpv 常驻 + 卡死自动重启 + 超强稳定重连) ======================
import queue
import unicodedata
import traceback
@@ -1673,27 +1673,19 @@ def start_record(url_data: tuple, count_variable: int = -1) -> None:
pass
# ====================== 配置参数 ======================
MAX_RETRY_DELAY = 90
INITIAL_RETRY_DELAY = 3
STDERR_QUEUE_TIMEOUT = 1.0
MAX_RETRY_DELAY = 90 # 最大重连等待时间(秒),指数退避上限,防止抖音风控
INITIAL_RETRY_DELAY = 3 # 首次重连等待时间(秒),从这个值开始翻倍
STDERR_QUEUE_TIMEOUT = 1.0 # 读取 FFmpeg stderr 的队列超时(秒),用于及时发现卡顿
NO_FRAME_TIMEOUT = 120 # 连续多少秒无新帧视为下播
START_CHECK_AFTER = 25 # 开始检测无帧的最短时间(避免刚开播误判)
STALE_OUTPUT_SECONDS = 120 # 长时间无任何日志输出,视为僵死
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",
"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())
@@ -1701,7 +1693,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}")
# 编码参数保持原样
# 编码参数(完全保留原样
codec_v = "libx264"
preset_v = "ultrafast"
tune_param = ["-tune", "zerolatency"]
@@ -1712,21 +1704,20 @@ def start_record(url_data: tuple, count_variable: int = -1) -> None:
"Origin: https://live.douyin.com\r\n"
)
# ====================== FIFO 路径 ======================
# ====================== 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}")
# 如果创建失败,继续尝试使用(某些系统可能已存在)
print(f"[ERROR] 创建 FIFO 失败(可能已存在): {e}")
# ====================== mpv 命令(常驻) ======================
# ====================== mpv 命令 ======================
mpv_cmd = [
"mpv",
FIFO_PATH,
"--vo=drm",
"--vo=drm", # 不支持会自动降级
"--hwdec=v4l2m2m",
"--profile=low-latency",
"--cache=yes",
@@ -1766,7 +1757,7 @@ def start_record(url_data: tuple, count_variable: int = -1) -> None:
"-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",
"pad=720:1280:(ow-iw)/2:(oh-ih)/2:black",
"-c:v", codec_v,
"-preset", preset_v,
@@ -1801,86 +1792,100 @@ def start_record(url_data: tuple, count_variable: int = -1) -> None:
proc = None
mpv_proc = None
retry_delay = INITIAL_RETRY_DELAY
last_buffer_warning_time = 0 # 避免频繁重启
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} 秒后重连抖音源...")
time.sleep(retry_delay)
retry_delay = min(retry_delay * 2, MAX_RETRY_DELAY)
cleanup_proc(proc)
print(f"[INFO][{datetime.datetime.now():%H:%M:%S}] 启动 FFmpeg 拉流 → FIFO → mpv")
proc = subprocess.Popen(
ffmpeg_command,
# 启动/重启 mpv常驻
if mpv_proc is None or mpv_proc.poll() is not None:
if mpv_proc is not None:
print("[WARN] mpv 进程已退出,准备重新启动")
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.PIPE,
text=True,
bufsize=1,
universal_newlines=True
stderr=subprocess.DEVNULL
)
proc.last_out_ts = time.time()
time.sleep(1) # 给 mpv 启动时间
stderr_q = queue.Queue()
reader_t = threading.Thread(target=_stderr_reader_thread, args=(proc, stderr_q))
reader_t.daemon = True
reader_t.start()
# 重连等待
if retry_delay > INITIAL_RETRY_DELAY:
print(f"[WARN] 等待 {retry_delay} 秒后重连抖音源...")
time.sleep(retry_delay)
retry_delay = min(retry_delay * 2, MAX_RETRY_DELAY)
start_time = last_frame_time = time.time()
# 清理旧 FFmpeg
cleanup_proc(proc)
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 长时间无输出,强制重启 FFmpeg")
break
continue
print(f"[INFO][{datetime.datetime.now():%H:%M:%S}] 启动 FFmpeg 拉流 → FIFO")
proc = subprocess.Popen(
ffmpeg_command,
stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE,
text=True,
bufsize=1,
universal_newlines=True
)
proc.last_out_ts = time.time()
if line is None:
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)
except queue.Empty:
if time.time() - proc.last_out_ts > STALE_OUTPUT_SECONDS:
print("[WARN] FFmpeg 长时间无输出,强制重启 FFmpeg")
break
continue
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()
if line is None:
break
if any(kw in line_lower for kw in END_KEYWORDS):
print("[INFO] 检测到主播下播关键字,停止转播")
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()
if current_time - last_buffer_warning_time > 10:
print("[CRITICAL] 检测到 mpv 卡死1000 buffers queued强制重启 mpv")
last_buffer_warning_time = current_time
if mpv_proc and mpv_proc.poll() is None:
mpv_proc.kill()
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()
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 无新帧,判定主播已下播")
raise StreamEnded()
except StreamEnded:
raise
except Exception as e:
print(f"[ERROR] FFmpeg 运行异常: {e}")
traceback.print_exc()
continue
except StreamEnded:
print(f"[INFO] 主播已下播,停止 HDMI 转播 → {record_name}")
@@ -1893,7 +1898,6 @@ def start_record(url_data: tuple, count_variable: int = -1) -> None:
finally:
cleanup_proc(proc)
if mpv_proc and mpv_proc.poll() is None:
print("[INFO] 终止 mpv 进程...")
mpv_proc.terminate()