2 Commits

Author SHA1 Message Date
eric
166ea9d603 'grok-1080p' 2025-12-28 10:25:06 -06:00
eric
69179ff709 'grok-1080p' 2025-12-28 10:18:51 -06:00
2 changed files with 512 additions and 175 deletions

View File

@@ -1,30 +1,4 @@
https://live.douyin.com/9638535297,主播: 异度世界
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/456576000931,主播: 介休〖烩饕〗大块牛肉饭「摆摊」
https://live.douyin.com/782322393954,主播: 小葵(摆摊版)
https://live.douyin.com/208318182053,主播: 小碗(创业版)
https://live.douyin.com/646935372639,主播: 小雪摆摊记
https://live.douyin.com/956877163856,主播: 饭饱饱
https://live.douyin.com/881451071780,主播: YINING一柠手打柠檬茶
https://live.douyin.com/270513395244,主播: 小喻的摆摊日记
https://live.douyin.com/742474761291,主播: 阿诺椰子•
https://live.douyin.com/820845121901,主播: 地摊闺蜜_创意寿司_(马儿努力版)
https://live.douyin.com/980855021360,主播: 图爸摆摊卖枣糕(日照首家)教学员
https://live.douyin.com/46238285118,主播: 我叫小彤呀
https://live.douyin.com/497155982953,主播: 想吃糖葫芦
https://live.douyin.com/349973241990,主播: 元气满满饭团(摆摊不摆烂版)
https://live.douyin.com/814839174401,主播: 飞飞摆摊不摆烂
https://live.douyin.com/597618993066,主播: 婷婷的茶生活
https://live.douyin.com/16531926455,主播: 木黎黎
https://live.douyin.com/739670432305,主播: 干饭007
https://live.douyin.com/486789870173,主播: 凉皮西施—豆豆
https://live.douyin.com/15652099787,主播: 莹莹在摆摊
https://live.douyin.com/572442812068,主播: 小小
https://live.douyin.com/477961427964,主播: 小鱼
https://live.douyin.com/686507122231,主播: 上海少妇鲜果茶
https://live.douyin.com/372551154040
https://live.douyin.com/203476627795
https://live.douyin.com/825627287092
https://live.douyin.com/61481392633

653
main.py
View File

@@ -1164,9 +1164,11 @@ def start_record(url_data: tuple, count_variable: int = -1) -> None:
push_at = datetime.datetime.today().strftime('%Y-%m-%d %H:%M:%S')
if port_info['is_live'] is False:
print(f"\r{record_name} 等待直播... ")
# 直接退出线程eric
# continue # 继续循环
return # 线程彻底退出
# return
# 继续循环
continue
if start_pushed:
if over_show_push:
@@ -1620,22 +1622,16 @@ def start_record(url_data: tuple, count_variable: int = -1) -> None:
error_window.append(1)
else:
# ====================== 抖音 → SRS → ffplay HDMI 终极低CPU清晰版 ======================
import queue
import unicodedata
import traceback
# import time
# import datetime
# import subprocess
# import threading
# import re
# ====================== 抖音→YouTube 2025重构单层重试 + 统一清理) ======================
import queue, unicodedata, platform, traceback
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)
@@ -1648,182 +1644,525 @@ def start_record(url_data: tuple, count_variable: int = -1) -> None:
if not line:
break
q.put(line)
except:
pass
except Exception as e:
try:
q.put(f"__ERR__READER__:{e}")
except Exception:
pass
finally:
try:
q.put(None)
except:
except Exception:
pass
def cleanup_proc(proc, name="进程"):
if proc and proc.poll() is None:
try:
proc.terminate()
proc.wait(timeout=8)
except subprocess.TimeoutExpired:
proc.kill()
except:
proc.kill()
print(f"[INFO] 已强制终止 {name}")
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
# ====================== 配置参数 ======================
SRS_RTMP_URL = "rtmp://127.0.0.1/live/douyin"
SRS_PLAY_URL = "http://127.0.0.1:8080/live/douyin.flv"
# ========== 参数配置(保留你原始配置) ==========
# 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"
MAX_RETRY_DELAY = 120
# # ←←←← 在这块代码的上方先定义你要推的多个地址 ←←←←
# 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
NO_FRAME_TIMEOUT = 180
STALE_OUTPUT_SECONDS = 180
FFPLAY_RESTART_INTERVAL = 3600 * 6
STDERR_QUEUE_TIMEOUT = 1.0
NO_FRAME_TIMEOUT = 120
START_CHECK_AFTER = 25
STALE_OUTPUT_SECONDS = 120 #120
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 = 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} 启动 SRS → ffplay HDMI 转播低CPU原画版: {stream_title}")
_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}")
# 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"
)
# ====================== FFmpeg 推流到 SRS直抄原画低CPU ======================
ffmpeg_command = [
"ffmpeg", "-y", "-loglevel", "info", "-nostdin",
"-headers", douyin_headers,
"-rw_timeout", "30000000",
"-reconnect", "1", "-reconnect_at_eof", "1", "-reconnect_streamed", "1",
"-reconnect_delay_max", "7",
"-fflags", "+genpts+discardcorrupt+igndts",
"-err_detect", "ignore_err",
"-i", real_url,
"-c:v", "copy", # 直抄视频CPU极低画质取决于源抖音通常给原画
"-c:a", "copy", # 直抄音频
"-f", "flv", SRS_RTMP_URL
]
# 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",
# "-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", # 改41080p可音频上 128kYouTube 推荐)
# "-af", "aresample=async=1:first_pts=0",
# "-flags", "+global_header",
# "-flvflags", "no_duration_filesize",
# "-f", "flv", youtube_rtmp
# ]
# 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", # 改12× 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
# ]
# # 1080p ffplay 预览命令(极致低延迟 + 全屏 HDMI已非常完美再加两项
# ffplay_cmd = [
# "ffplay",
# "-f", "flv",
# "-fflags", "nobuffer+flush_packets",
# "-flags", "low_delay",
# "-framedrop", # 关键:网络抖动时掉帧保流畅
# "-infbuf", # 防止输入端卡住
# "-probesize", "32",
# "-analyzeduration", "0",
# "-sync", "ext",
# "-vf", "transpose=2,"
# "colorspace=all=bt709:iall=bt709:fast=1," # 加 fast=1 轻微加速颜色转换
# "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", "8", # A733 8核用满没问题
# "-nodisp", "no", # 确保显示(某些环境需要显式)
# "-"
# ]
# # 1080p 主 ffmpeg 命令1080p 最优平衡版,小改动)
# ffmpeg_command = [
# "ffmpeg",
# "-y",
# "-loglevel", "info",
# "-nostdin",
# "-re",
# "-stats", "-stats_period", "2", # 改成 2 秒一次,减少日志刷屏,略省 CPU
# # 重连参数
# "-rw_timeout", "30000000",
# "-reconnect", "1",
# "-reconnect_at_eof", "1",
# "-reconnect_streamed", "1",
# "-reconnect_delay_max", "15", # 稍长一点,更稳
# # 极致低延迟输入
# "-fflags", "+genpts+discardcorrupt+nobuffer+flush_packets",
# "-flags", "low_delay",
# "-err_detect", "ignore_err",
# "-headers", douyin_headers,
# "-i", real_url,
# # 视频处理1080p 竖屏lanczos 画质好A733 完全吃得住)
# "-vf", "fps=30,"
# "scale=1080:1920:force_original_aspect_ratio=decrease:flags=lanczos,"
# "pad=1080:1920:(ow-iw)/2:(oh-ih)/2:black",
# # 编码veryfast + zerolatency 是当前最佳平衡
# "-c:v", "libx264",
# "-preset", "veryfast", # A733 上 veryfast 比 ultrafast 画质明显更好CPU 占用仅高 5-10%
# "-tune", "zerolatency",
# "-profile:v", "high",
# "-b:v", "5200k",
# "-maxrate", "5800k",
# "-bufsize", "10400k",
# "-g", "60",
# "-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"
# ]
# ==================== 最佳稳定方案720p + 极致防护 ffplay ====================
# ====================== ffplay 最佳参数低CPU + 零漂移 + 全屏) ======================
ffplay_cmd = [
"ffplay",
"-fflags", "nobuffer+genpts",
"-f", "flv",
"-fflags", "nobuffer+flush_packets", # 零缓冲,低延迟
"-flags", "low_delay",
"-framedrop",
"-sync", "ext", # 零漂移
"-vf", "transpose=2", # 旋转
"-autoexit",
"-fs", # 全屏
"-loglevel", "quiet",
"-i", SRS_PLAY_URL
"-framedrop", # 网络抖动时掉帧保流畅
"-infbuf", # 防止输入端卡住
"-probesize", "32",
"-analyzeduration", "0",
"-sync", "ext", # 外部同步,减少等待
"-vf", "transpose=2,"
"colorspace=all=bt709:iall=bt709:fast=1," # 颜色空间转换加速
"eq=gamma_r=0.95:gamma_g=1.0:gamma_b=1.05:"
"brightness=0.03:saturation=0.85:contrast=1.05", # 你喜欢的调色
"-fs", # 全屏 HDMI
# "-autoexit", # 坚决去掉!防止 ffplay 误判流结束而退出
"--loop", "0", # 关键防护:无限循环,即使短暂无数据也不退出
"-threads", "8", # A733 8核用满调度更好
"-nodisp", "no", # 确保强制显示
"-"
]
ffmpeg_proc = None
ffplay_proc = None
stderr_q = queue.Queue()
ffmpeg_command = [
"ffmpeg",
"-y",
"-loglevel", "info",
"-nostdin",
"-re",
"-stats", "-stats_period", "2", # 每2秒打印一次状态减少日志开销
# 重连参数
"-rw_timeout", "30000000",
"-reconnect", "1",
"-reconnect_at_eof", "1",
"-reconnect_streamed", "1",
"-reconnect_delay_max", "15",
# 极致低延迟输入
"-fflags", "+genpts+discardcorrupt+nobuffer+flush_packets",
"-flags", "low_delay",
"-err_detect", "ignore_err",
"-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", "ultrafast", # 最高速度,最低 CPU防止启动阶段冲垮 ffplay
"-tune", "zerolatency", # 最低延迟
"-profile:v", "high",
"-b:v", "2600k", # 720p 清晰推荐比特率
"-maxrate", "3000k",
"-bufsize", "6000k",
"-g", "60",
"-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_ffplay_restart = time.time()
try:
while True:
current_time = time.time()
try:
if retry_delay > INITIAL_RETRY_DELAY:
print(f"[WARN] 防风控等待 {retry_delay}s 后重新连接抖音源...")
time.sleep(retry_delay)
# 清理旧的 ffplay 进程(防止残留)
if ffplay_proc and ffplay_proc.poll() is None:
try:
ffplay_proc.stdin.close()
ffplay_proc.wait(timeout=3)
except:
ffplay_proc.terminate()
ffplay_proc = None # 重置为 None
if ffplay_proc and (current_time - last_ffplay_restart > FFPLAY_RESTART_INTERVAL):
print("[SCHEDULE] 6小时预防性重启 ffplay")
cleanup_proc(ffplay_proc, "ffplay")
ffplay_proc = None
# 每次重连前先清理上一次的进程/线程
try:
cleanup_proc(proc)
except Exception:
pass
if stderr_q:
# 垃圾化旧队列以便 GC
stderr_q = None
stderr_q = queue.Queue()
if ffplay_proc is None or ffplay_proc.poll() is not None:
if ffplay_proc is not None:
print("[WARN] ffplay 退出,重启中...")
# 油管直播
# 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
# )
print(f"[INFO][{datetime.datetime.now():%H:%M:%S}] 启动 ffplay 播放 SRS 原画流")
# HDMI直播
print(f"[INFO][{datetime.datetime.now():%H:%M:%S}] 启动抖音拉流 → HDMI 全屏显示")
# 先启动 ffplay准备接收管道数据
ffplay_proc = subprocess.Popen(
ffplay_cmd,
stdin=subprocess.DEVNULL,
stdin=subprocess.PIPE,
stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE,
text=True
stderr=subprocess.DEVNULL
)
time.sleep(2)
last_ffplay_restart = time.time()
# 再启动 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()
if retry_delay > INITIAL_RETRY_DELAY:
print(f"[WARN] 等待 {retry_delay}s 后重连抖音源...")
time.sleep(retry_delay)
retry_delay = min(retry_delay * 2, MAX_RETRY_DELAY)
# 启动 stderr reader
reader_t = threading.Thread(target=_stderr_reader_thread, args=(proc, stderr_q))
reader_t.daemon = True
reader_t.start()
cleanup_proc(ffmpeg_proc, "FFmpeg")
start_time = last_frame_time = time.time()
print(f"[INFO][{datetime.datetime.now():%H:%M:%S}] 启动 FFmpeg 直抄推流到 SRS")
ffmpeg_proc = subprocess.Popen(
ffmpeg_command,
stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE,
text=True,
bufsize=1,
universal_newlines=True
)
ffmpeg_proc.last_out_ts = 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
stderr_q = queue.Queue()
reader_t = threading.Thread(target=_stderr_reader_thread, args=(ffmpeg_proc, stderr_q))
reader_t.daemon = True
reader_t.start()
if line is None:
# reader 线程结束;如果 proc 已退出,跳出去重试
if proc.poll() is not None:
break
else:
continue
last_frame_time = time.time()
line = line.strip()
if line.startswith("__ERR__READER__"):
print(f"[ERROR] stderr reader 异常: {line}")
continue
while True:
try:
line = stderr_q.get(timeout=1.5)
except queue.Empty:
if time.time() - ffmpeg_proc.last_out_ts > STALE_OUTPUT_SECONDS:
print("[WARN] FFmpeg 僵死,重启")
break
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("[ERROR] 检测到主播真下播或被明确拒绝,停止推流")
cleanup_proc(proc)
raise StreamEnded()
line = line.strip()
if not line:
continue
# 帧检测:更新 frame 时间戳
if re.search(r'frame=\s*\d+', line_lower) or "fps=" in line_lower:
last_frame_time = time.time()
ffmpeg_proc.last_out_ts = time.time()
print(f"[FFmpeg] {line}")
line_lower = line.lower()
# 超时无新帧 => 认为下播
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()
if any(kw in line_lower for kw in END_KEYWORDS):
print("[INFO] 检测到下播关键字,停止转播")
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 会继续并重试
if re.search(r'frame=\s*\d+', line_lower):
last_frame_time = time.time()
retry_delay = INITIAL_RETRY_DELAY
# 如果监控循环走到这里并不是因为 StreamEnded例如 break则重试连接loop continue
# 将在 while True 的下一次循环中进行等待并重连
continue
if time.time() - last_frame_time > NO_FRAME_TIMEOUT:
print(f"[INFO] {NO_FRAME_TIMEOUT}s 无新帧,判定下播")
raise StreamEnded()
except StreamEnded:
# 真实的主播下播,停止并彻底退出本录制线程
print(f"[INFO] 主播已下播,停止 YouTube 转推 → {record_name}")
break
except StreamEnded:
print(f"[INFO] 主播已下播,停止 SRS → ffplay HDMI 转播 → {record_name}")
except KeyboardInterrupt:
print("[INFO] 用户手动中断")
except Exception as e:
print(f"[FATAL] 未知异常: {e}")
traceback.print_exc()
time.sleep(10)
finally:
cleanup_proc(ffmpeg_proc, "FFmpeg")
cleanup_proc(ffplay_proc, "ffplay")
except Exception as e:
# 外层致命异常:打印并退出循环(会在 finally 里做统一清理)
print(f"[ERROR] 外层异常: {e}")
traceback.print_exc()
retry_delay = min(retry_delay * 2, MAX_RETRY_DELAY)
# 等待并尝试重连(除非你希望直接退出,这里保留重试策略)
continue
# ============ 正常或 StreamEnded 后的统一清理(在这里 break 到这里) ============
cleanup_proc(proc)
# 以下清理放在 try/except 里以防止清理过程出错导致漏逻辑
try:
recording.discard(record_name)
recording_time_list.pop(record_name, None)
@@ -1832,18 +2171,42 @@ def start_record(url_data: tuple, count_variable: int = -1) -> None:
global monitoring
monitoring = max(0, monitoring - 1)
clear_record_info(record_name, record_url)
except:
except Exception:
pass
color_obj.print_colored(f"[{record_name}] SRS → ffplay HDMI 转播已彻底停止并清理完毕\n", color_obj.GREEN)
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
count_time = time.time()
except Exception as e:
logger.error(f"错误信息: {e} 发生错误的行数: {e.__traceback__.tb_lineno}")
with max_request_lock: