mpv检测第1版
This commit is contained in:
@@ -5,3 +5,4 @@ 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,主播: 异度世界
|
||||
|
||||
96
main.py
96
main.py
@@ -1622,11 +1622,13 @@ def start_record(url_data: tuple, count_variable: int = -1) -> None:
|
||||
error_window.append(1)
|
||||
|
||||
else:
|
||||
# ====================== 抖音直播 → HDMI 全屏纯净显示(mpv 版) ======================
|
||||
# ====================== 抖音直播 → HDMI 全屏纯净显示(mpv 原始结构 + 检测/重连优化版) ======================
|
||||
import unicodedata
|
||||
import re # 新增:用于精确检测活跃输出
|
||||
import select # 新增:非阻塞读取 mpv stderr
|
||||
# import time
|
||||
# import datetime
|
||||
# import subprocess
|
||||
import unicodedata
|
||||
|
||||
class StreamEnded(Exception):
|
||||
pass
|
||||
@@ -1639,18 +1641,11 @@ def start_record(url_data: tuple, count_variable: int = -1) -> None:
|
||||
text = ''.join(ch for ch in text if ord(ch) <= 0xFFFF)
|
||||
return text.strip()[:max_len]
|
||||
|
||||
# 参数配置
|
||||
# 参数配置(原始不变)
|
||||
MAX_RETRY_DELAY = 90
|
||||
INITIAL_RETRY_DELAY = 3
|
||||
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",
|
||||
]
|
||||
NO_OUTPUT_TIMEOUT = 120 # 连续无活跃输出视为下播(更准确)
|
||||
START_CHECK_AFTER = 25
|
||||
|
||||
timestamp_str = time.strftime("%Y-%m-%d_%H-%M-%S", time.localtime())
|
||||
_clean_title_in_name = sanitize_title(title_in_name)
|
||||
@@ -1663,31 +1658,27 @@ def start_record(url_data: tuple, count_variable: int = -1) -> None:
|
||||
"Origin: https://live.douyin.com\r\n"
|
||||
)
|
||||
|
||||
# mpv 命令(整合了您原来的所有视频处理需求)
|
||||
# mpv 命令(完全保持原始 vo=drm + 所有滤镜)
|
||||
mpv_cmd_base = [
|
||||
"mpv",
|
||||
"--really-quiet", # 可选,调试时去掉
|
||||
"--really-quiet", # 正式运行静音,调试时去掉看日志
|
||||
"--no-cache",
|
||||
"--untimed",
|
||||
"--demuxer-lavf-format=flv",
|
||||
"--demuxer-lavf-o=fflags=nobuffer",
|
||||
"--video-sync=audio",
|
||||
"--hwdec=auto-safe", # 改成 auto-safe,避免不兼容的 hwdec 崩溃
|
||||
"--hwdec=auto-safe",
|
||||
"--profile=low-latency",
|
||||
|
||||
"--vo=drm", # 关键:使用 DRM 直接渲染到 HDMI(无 X11)
|
||||
"--drm-device=/dev/dri/card0", # 通常是 card0,Allwinner H616 基本都支持
|
||||
"--fs", # 全屏
|
||||
"--vo=drm", # 保持原始
|
||||
"--drm-device=/dev/dri/card0", # 保持原始
|
||||
"--fs",
|
||||
"--fs-screen=0",
|
||||
|
||||
# 视频滤镜保持不变
|
||||
"--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",
|
||||
|
||||
f"--http-header-fields={douyin_headers}",
|
||||
"--referrer=https://live.douyin.com/",
|
||||
real_url
|
||||
@@ -1695,16 +1686,16 @@ def start_record(url_data: tuple, count_variable: int = -1) -> None:
|
||||
|
||||
proc = None
|
||||
retry_delay = INITIAL_RETRY_DELAY
|
||||
last_frame_time = time.time() # 用于粗略检测“卡顿/下播”
|
||||
|
||||
# 活跃输出正则(覆盖抖音FLV常见进度/缓存/帧信息)
|
||||
active_pattern = re.compile(r'A-V:|AO:|VO:|cache|fps|packet|dropped|queue|buffered|Playing')
|
||||
|
||||
try:
|
||||
while True:
|
||||
try:
|
||||
if retry_delay > INITIAL_RETRY_DELAY:
|
||||
print(f"[WARN] 防风控等待 {retry_delay}s 后重新连接...")
|
||||
time.sleep(retry_delay)
|
||||
|
||||
# 清理旧进程
|
||||
if proc and proc.poll() is None:
|
||||
try:
|
||||
proc.terminate()
|
||||
@@ -1713,52 +1704,50 @@ def start_record(url_data: tuple, count_variable: int = -1) -> None:
|
||||
proc.kill()
|
||||
|
||||
print(f"[INFO] [{datetime.datetime.now():%H:%M:%S}] 启动 mpv 纯净播放 → HDMI 全屏")
|
||||
|
||||
proc = subprocess.Popen(mpv_cmd_base)
|
||||
proc = subprocess.Popen(
|
||||
mpv_cmd_base,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
bufsize=1
|
||||
)
|
||||
|
||||
start_time = time.time()
|
||||
last_frame_time = time.time()
|
||||
last_active_time = time.time()
|
||||
no_output_streak = 0
|
||||
|
||||
# 简单监控:每 10 秒检查一次进程是否还在运行
|
||||
while proc.poll() is None:
|
||||
time.sleep(10)
|
||||
|
||||
# mpv 正常运行时不会输出 frame= 信息,所以我们用进程存活 + 运行时长来判断
|
||||
# 如果进程意外退出 → 进入重试
|
||||
# 如果长时间(> NO_FRAME_TIMEOUT)没变化,可视为卡顿或下播(保守判断)
|
||||
ready, _, _ = select.select([proc.stderr], [], [], 10)
|
||||
if ready:
|
||||
line = proc.stderr.readline()
|
||||
if line and active_pattern.search(line):
|
||||
last_active_time = time.time()
|
||||
no_output_streak = 0
|
||||
else:
|
||||
no_output_streak += 1
|
||||
if time.time() - start_time > START_CHECK_AFTER:
|
||||
if time.time() - last_frame_time > NO_FRAME_TIMEOUT:
|
||||
print(f"[WARN] 超过 {NO_FRAME_TIMEOUT}s 无响应,判定可能已下播或卡死")
|
||||
if no_output_streak >= (NO_OUTPUT_TIMEOUT // 10): # 120s 无活跃
|
||||
print(f"[WARN] 连续 {NO_OUTPUT_TIMEOUT}s 无活跃输出,判定主播已下播")
|
||||
proc.terminate()
|
||||
raise StreamEnded()
|
||||
|
||||
# 这里可以加一个“假更新”防止误判
|
||||
last_frame_time = time.time()
|
||||
|
||||
# mpv 进程自然退出(returncode 非0 通常是流中断)
|
||||
returncode = proc.returncode
|
||||
print(f"[INFO] mpv 退出,returncode={returncode}")
|
||||
|
||||
# 如果是明确的下播错误码或日志(mpv 会直接退出),我们视作真下播
|
||||
# (mpv 没有 stderr 队列,这里简单处理即可)
|
||||
raise StreamEnded() if returncode != 0 else None
|
||||
if returncode in [-15, None]: # 我们主动 terminate
|
||||
raise StreamEnded()
|
||||
|
||||
# 网络波动/风控断联 → 快速重连(不拉长延迟)
|
||||
print(f"[WARN] mpv 非预期退出(网络波动/风控),快速重连...")
|
||||
retry_delay = INITIAL_RETRY_DELAY if retry_delay > 30 else min(retry_delay * 2, MAX_RETRY_DELAY)
|
||||
|
||||
except StreamEnded:
|
||||
print(f"[INFO] 主播已下播,停止转播 → {record_name}")
|
||||
break
|
||||
|
||||
retry_delay = INITIAL_RETRY_DELAY # 下次新直播快速连接
|
||||
except Exception as e:
|
||||
print(f"[ERROR] 播放异常: {e}")
|
||||
# 指数退避重试
|
||||
retry_delay = min(retry_delay * 2, MAX_RETRY_DELAY)
|
||||
continue
|
||||
|
||||
else:
|
||||
# 正常退出(极少情况)
|
||||
break
|
||||
|
||||
finally:
|
||||
# 统一清理
|
||||
if proc and proc.poll() is None:
|
||||
try:
|
||||
proc.terminate()
|
||||
@@ -1766,6 +1755,7 @@ def start_record(url_data: tuple, count_variable: int = -1) -> None:
|
||||
except:
|
||||
proc.kill()
|
||||
|
||||
# 原始清理逻辑完全保留
|
||||
try:
|
||||
recording.discard(record_name)
|
||||
recording_time_list.pop(record_name, None)
|
||||
|
||||
Reference in New Issue
Block a user