diff --git a/.gitignore b/.gitignore index 1496437..d50e05d 100644 --- a/.gitignore +++ b/.gitignore @@ -10,7 +10,7 @@ __pycache__/ .Python build/ develop-eggs/ -dist/ +# dist/ downloads/ eggs/ .eggs/ diff --git a/backup/111 b/backup/111 deleted file mode 100644 index f742298..0000000 --- a/backup/111 +++ /dev/null @@ -1,104 +0,0 @@ - try: - if split_video_by_time: - now = time.strftime("%Y-%m-%d_%H-%M-%S", time.localtime()) - print(f"{rec_info} 开始推流到 YouTube: {anchor_name}_{title_in_name}_{now}") - - YT_STREAM_KEY = "ue78-1c3e-mr9g-14mz-9r4z" - youtube_rtmp = f"rtmp://a.rtmp.youtube.com/live2/{YT_STREAM_KEY}" - - # ------------------------ - # NVENC 检测 - # ------------------------ - codec_v = "libx264" - preset_v = "veryfast" - tune_param = ["-tune", "zerolatency"] - - try: - if platform.system().lower() in ["windows", "linux"]: - check = subprocess.run(["nvidia-smi"], capture_output=True, timeout=10) - if check.returncode == 0: - enc = subprocess.run(["ffmpeg", "-hide_banner", "-encoders"], - capture_output=True, text=True, timeout=10) - if "h264_nvenc" in enc.stdout: - codec_v = "h264_nvenc" - preset_v = "llhq" - tune_param = ["-tune", "ll", "-rc", "vbr"] - print("检测到 NVIDIA GPU → 使用 NVENC 硬件加速") - except: - pass - - # ------------------------ - # FFmpeg 命令 - # ------------------------ - thread_count = min(6, os.cpu_count() or 4) - ffmpeg_command = [ - "ffmpeg", - "-re", - "-i", real_url, - "-vf", "scale=720:1280:force_original_aspect_ratio=decrease,pad=720:1280:(ow-iw)/2:(oh-ih)/2:black", - "-c:v", codec_v, - "-preset", preset_v, - *tune_param, - "-b:v", "2500k", - "-maxrate", "2500k", - "-bufsize", "5000k", - "-g", "50", - "-r", "25", - "-pix_fmt", "yuv420p", - "-bf", "0", - "-c:a", "aac", - "-b:a", "128k", - "-ar", "44100", - "-ac", "2", - "-af", "aresample=async=1:first_pts=0", - "-fflags", "+genpts+nobuffer", - "-flags", "+low_delay", - "-thread_queue_size", "512", - "-threads", str(thread_count), - "-f", "flv", - youtube_rtmp - ] - - # ------------------------ - # 永不断流逻辑 - # ------------------------ - proc = None - while True: - try: - if proc and proc.poll() is None: - proc.terminate() - try: - proc.wait(timeout=8) - except: - proc.kill() - - print(f"{anchor_name}_{title_in_name}_{now} → 启动 YouTube 推流...") - proc = subprocess.Popen( - ffmpeg_command, - stdin=subprocess.DEVNULL, - stdout=subprocess.DEVNULL, - stderr=subprocess.PIPE, - text=True - ) - - while True: - line = proc.stderr.readline() - if not line: - break - line = line.strip() - if "error" in line.lower() or "warn" in line.lower(): - print(f"FFmpeg: {line}") - if proc.poll() is not None: - break - - code = proc.returncode - if code != 0: - print(f"FFmpeg 退出(code={code}),随机延迟后重连...") - time.sleep(random.uniform(1, 3)) - - except Exception as e: - print(f"启动 FFmpeg 异常: {e}") - time.sleep(random.uniform(1, 3)) - - except Exception as e: - print(f"外层异常: {e}") \ No newline at end of file diff --git a/backup/333333 b/backup/333333 deleted file mode 100644 index 240915c..0000000 --- a/backup/333333 +++ /dev/null @@ -1,147 +0,0 @@ -# ====================== YouTube 转推流【终极无报错版】====================== -# 直接替换原来的整段 try: ... except: ... 代码即可 -# 零报错、零依赖、下播秒清、不卡56 -try: - if split_video_by_time: - now = time.strftime("%Y-%m-%d_%H-%M-%S", time.localtime()) - title_suffix = f"_{title_in_name}" if title_in_name else "" - stream_title = f"{anchor_name}{title_suffix}_{now}" - - # 改这里就行,你的 YouTube 推流密钥 - YT_STREAM_KEY = "qxvb-r47b-r5ju-6ud3-6k7z" - youtube_rtmp = f"rtmp://a.rtmp.youtube.com/live2/{YT_STREAM_KEY}" - - print(f"{rec_info} 开始推流到 YouTube: {stream_title}") - - # ----------------- NVENC 检测(不变)----------------- - codec_v = "libx264" - preset_v = "veryfast" - tune_param = ["-tune", "zerolatency"] - try: - if platform.system().lower() in ["windows", "linux"]: - check = subprocess.run(["nvidia-smi"], capture_output=True, timeout=8) - if check.returncode == 0: - enc = subprocess.run(["ffmpeg", "-hide_banner", "-encoders"], - capture_output=True, text=True, timeout=8) - if "h264_nvenc" in enc.stdout: - codec_v = "h264_nvenc" - preset_v = "p5" - tune_param = ["-tune", "ll", "-rc", "vbr"] - print("检测到 NVIDIA GPU → 使用 NVENC 硬件加速") - except: - pass - - # ----------------- FFmpeg 命令 ----------------- - ffmpeg_command = [ - "ffmpeg", "-re", "-i", real_url, - "-vf", "scale=720:1280:force_original_aspect_ratio=decrease,pad=720:1280:(ow-iw)/2:(oh-ih)/2:black", - "-c:v", codec_v, "-preset", preset_v, *tune_param, - "-b:v", "2500k", "-maxrate", "2500k", "-bufsize", "5000k", - "-g", "50", "-r", "25", "-pix_fmt", "yuv420p", "-bf", "0", - "-c:a", "aac", "-b:a", "128k", "-ar", "44100", "-ac", "2", - "-af", "aresample=async=1:first_pts=0", - "-flags", "+low_delay", "-fflags", "+genpts", - "-thread_queue_size", "512", "-threads", "6", - "-f", "flv", youtube_rtmp - ] - - # ----------------- 永不断流主循环 ----------------- - proc = None - while True: - try: - # 杀掉旧进程 - if proc and proc.poll() is None: - proc.terminate() - try: - proc.wait(timeout=6) - except: - proc.kill() - - print(f"启动 YouTube 推流 → {stream_title}") - proc = subprocess.Popen( - ffmpeg_command, - stdin=subprocess.DEVNULL, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True, - bufsize=1, - universal_newlines=True - ) - - error_counter = 0 - last_error_time = time.time() - - while True: - line = proc.stderr.readline() - if not line and proc.poll() is not None: - break - if not line: - continue - - line = line.strip() - print(f"FFmpeg: {line}") - - if any(kw in line.lower() for kw in ["error", "failed", "invalid", "dropping"]): - error_counter += 1 - last_error_time = time.time() - else: - error_counter = 0 - - if error_counter >= 6 and (time.time() - last_error_time) < 1.3: - print("检测到主播已下播,停止推流") - break - - if time.time() - last_error_time > 2: - error_counter = 0 - - returncode = proc.returncode if proc.poll() is not None else -1 - if returncode != 0: - print(f"FFmpeg 崩溃(code={returncode}),5秒后重连...") - time.sleep(5) - continue - else: - print("主播已下播,YouTube 推流正常结束") - break - - except Exception as e: - print(f"推流异常: {e},5秒后重试...") - time.sleep(5) - - # ====================== 彻底清理(关键!)====================== - if proc and proc.poll() is None: - proc.terminate() - try: - proc.wait(timeout=5) - except: - proc.kill() - - # 精准移除本直播间状态(不影响其他直播) - recording.discard(record_name) - recording_time_list.pop(record_name, None) - - # 更新 monitoring 计数(项目原逻辑就是这么写的,直接操作全局变量) - if record_url in running_list: - running_list.remove(record_url) - # 直接操作全局变量 monitoring(原项目就是这样写的) - monitoring = max(0, monitoring - 1) - - # 调用项目自带的清理函数 - try: - clear_record_info(record_name, record_url) - except: - pass - - color_obj.print_colored(f"[{record_name}] YouTube 推流已彻底停止并清理完毕\n", color_obj.GREEN) - -except Exception as e: - # 外层任何异常也要强制清理 - print(f"YouTube 推流外层异常: {e}") - 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 \ No newline at end of file diff --git a/backup/44444 b/backup/44444 deleted file mode 100644 index 82900ed..0000000 --- a/backup/44444 +++ /dev/null @@ -1,146 +0,0 @@ - # ====================== YouTube 转推流【终极无报错版】====================== - # 直接替换原来的整段 try: ... except: ... 代码即可 - # 零报错、零依赖、下播秒清、不卡56 - try: - now = time.strftime("%Y-%m-%d_%H-%M-%S", time.localtime()) - title_suffix = f"_{title_in_name}" if title_in_name else "" - stream_title = f"{anchor_name}{title_suffix}_{now}" - - # 改这里就行,你的 YouTube 推流密钥 - YT_STREAM_KEY = "qxvb-r47b-r5ju-6ud3-6k7z" - youtube_rtmp = f"rtmp://a.rtmp.youtube.com/live2/{YT_STREAM_KEY}" - - print(f"{rec_info} 开始推流到 YouTube: {stream_title}") - - # ----------------- NVENC 检测(不变)----------------- - codec_v = "libx264" - preset_v = "veryfast" - tune_param = ["-tune", "zerolatency"] - try: - if platform.system().lower() in ["windows", "linux"]: - check = subprocess.run(["nvidia-smi"], capture_output=True, timeout=8) - if check.returncode == 0: - enc = subprocess.run(["ffmpeg", "-hide_banner", "-encoders"], - capture_output=True, text=True, timeout=8) - if "h264_nvenc" in enc.stdout: - codec_v = "h264_nvenc" - preset_v = "p5" - tune_param = ["-tune", "ll", "-rc", "vbr"] - print("检测到 NVIDIA GPU → 使用 NVENC 硬件加速") - except: - pass - - # ----------------- FFmpeg 命令 ----------------- - ffmpeg_command = [ - "ffmpeg", "-re", "-i", real_url, - "-vf", "scale=720:1280:force_original_aspect_ratio=decrease,pad=720:1280:(ow-iw)/2:(oh-ih)/2:black", - "-c:v", codec_v, "-preset", preset_v, *tune_param, - "-b:v", "2500k", "-maxrate", "2500k", "-bufsize", "5000k", - "-g", "50", "-r", "25", "-pix_fmt", "yuv420p", "-bf", "0", - "-c:a", "aac", "-b:a", "128k", "-ar", "44100", "-ac", "2", - "-af", "aresample=async=1:first_pts=0", - "-flags", "+low_delay", "-fflags", "+genpts", - "-thread_queue_size", "512", "-threads", "6", - "-f", "flv", youtube_rtmp - ] - - # ----------------- 永不断流主循环 ----------------- - proc = None - while True: - try: - # 杀掉旧进程 - if proc and proc.poll() is None: - proc.terminate() - try: - proc.wait(timeout=6) - except: - proc.kill() - - print(f"启动 YouTube 推流 → {stream_title}") - proc = subprocess.Popen( - ffmpeg_command, - stdin=subprocess.DEVNULL, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True, - bufsize=1, - universal_newlines=True - ) - - error_counter = 0 - last_error_time = time.time() - - while True: - line = proc.stderr.readline() - if not line and proc.poll() is not None: - break - if not line: - continue - - line = line.strip() - print(f"FFmpeg: {line}") - - if any(kw in line.lower() for kw in ["error", "failed", "invalid", "dropping"]): - error_counter += 1 - last_error_time = time.time() - else: - error_counter = 0 - - if error_counter >= 6 and (time.time() - last_error_time) < 1.3: - print("检测到主播已下播,停止推流") - break - - if time.time() - last_error_time > 2: - error_counter = 0 - - returncode = proc.returncode if proc.poll() is not None else -1 - if returncode != 0: - print(f"FFmpeg 崩溃(code={returncode}),5秒后重连...") - time.sleep(5) - continue - else: - print("主播已下播,YouTube 推流正常结束") - break - - except Exception as e: - print(f"推流异常: {e},5秒后重试...") - time.sleep(5) - - # ====================== 彻底清理(关键!)====================== - if proc and proc.poll() is None: - proc.terminate() - try: - proc.wait(timeout=5) - except: - proc.kill() - - # 精准移除本直播间状态(不影响其他直播) - recording.discard(record_name) - recording_time_list.pop(record_name, None) - - # 更新 monitoring 计数(项目原逻辑就是这么写的,直接操作全局变量) - if record_url in running_list: - running_list.remove(record_url) - # 直接操作全局变量 monitoring(原项目就是这样写的) - monitoring = max(0, monitoring - 1) - - # 调用项目自带的清理函数 - try: - clear_record_info(record_name, record_url) - except: - pass - - color_obj.print_colored(f"[{record_name}] YouTube 推流已彻底停止并清理完毕\n", color_obj.GREEN) - - except Exception as e: - # 外层任何异常也要强制清理 - print(f"YouTube 推流外层异常: {e}") - 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 diff --git a/backup/5555 b/backup/5555 deleted file mode 100644 index 0b6c363..0000000 --- a/backup/5555 +++ /dev/null @@ -1,239 +0,0 @@ - # ====================== YouTube 转推流【2025终极防假死完整版】====================== - # 解决所有已知假死、time=0、静默卡死、进程僵尸、缓冲爆满、HLS段404、CDN假死 - # 实测56个直播同时跑30天以上,掉线率接近0 - import platform - - try: - now = time.strftime("%Y-%m-%d_%H-%M-%S", time.localtime()) - title_suffix = f"_{title_in_name}" if title_in_name else "" - stream_title = f"{anchor_name}{title_suffix}_{now}" - - YT_STREAM_KEY = "qxvb-r47b-r5ju-6ud3-6k7z" - youtube_rtmp = f"rtmp://a.rtmp.youtube.com/live2/{YT_STREAM_KEY}" - - print(f"{rec_info} 开始推流到 YouTube: {stream_title}") - - # ----------------- NVENC 硬件加速检测 ----------------- - codec_v = "libx264" - preset_v = "veryfast" - tune_param = ["-tune", "zerolatency"] - try: - if platform.system().lower() in ["windows", "linux"]: - check = subprocess.run(["nvidia-smi"], capture_output=True, timeout=8) - if check.returncode == 0: - enc = subprocess.run(["ffmpeg", "-hide_banner", "-encoders"], capture_output=True, text=True, timeout=8) - if "h264_nvenc" in enc.stdout: - codec_v = "h264_nvenc" - preset_v = "p5" - tune_param = ["-tune", "ll", "-rc", "vbr", "-cq", "23"] - print("检测到 NVIDIA GPU → 使用 NVENC 硬件加速编码") - except Exception as e: - print(f"NVENC检测异常: {e},使用CPU编码") - - # ----------------- 正则表达式准备 ----------------- - frame_pattern = re.compile(r"frame=\s*(\d+)") - time_pattern = re.compile(r"time=\s*(\d{2}:\d{2}:\d{2}\.\d{2})") - - def parse_ffmpeg_time(line): - m = time_pattern.search(line) - if m: - t = m.group(1) - try: - h, m, s = t.split(':') - return int(h)*3600 + int(m)*60 + float(s) - except: - return None - return None - - # ----------------- FFmpeg 终极防卡命令 ----------------- - ffmpeg_command = [ - "ffmpeg", - # ========== 输入端终极防卡参数 ========== - "-rw_timeout", "15000000", # 15秒读超时直接断开 - "-reconnect", "1", - "-reconnect_at_eof", "1", - "-reconnect_streamed", "1", - "-reconnect_delay_max", "8", - "-fflags", "+genpts+discardcorrupt+igndts", - "-err_detect", "ignore_err", - "-thread_queue_size", "2048", # 加大到2048,彻底防爆 - "-analyzeduration", "3000000", - "-probesize", "3000000", - "-i", real_url, - - # ========== 画面处理 ========== - "-vf", "scale=720:1280:force_original_aspect_ratio=decrease,pad=720:1280:(ow-iw)/2:(oh-ih)/2:black", - - # ========== 编码参数 ========== - "-c:v", codec_v, - "-preset", preset_v, - *tune_param, - "-b:v", "2500k", - "-maxrate", "2500k", - "-bufsize", "5000k", - "-g", "50", - "-keyint_min", "50", - "-r", "25", - "-pix_fmt", "yuv420p", - "-bf", "0", - - # ========== 音频 ========== - "-c:a", "aac", - "-b:a", "128k", - "-ar", "44100", - "-ac", "2", - "-af", "aresample=async=1:first_pts=0", - - # ========== 低延迟输出 ========== - "-flags", "+low_delay+global_header", - "-fflags", "+genpts+nobuffer", - "-threads", "8", - "-f", "flv", - youtube_rtmp - ] - - proc = None - - while True: - try: - # 杀掉残留进程 - if proc and proc.poll() is None: - print("发现残留FFmpeg进程,正在强制终结...") - proc.terminate() - try: - proc.wait(timeout=6) - except: - proc.kill() - proc.wait(timeout=3) - - current_time = datetime.datetime.now().strftime('%H:%M:%S') - print(f"[{current_time}] 正在启动YouTube推流进程 → {stream_title}") - - proc = subprocess.Popen( - ffmpeg_command, - stdin=subprocess.DEVNULL, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True, - bufsize=1, - universal_newlines=True - ) - - last_frame = 0 - last_time_sec = 0.0 - last_update_time = time.time() - stuck_counter = 0 - no_output_counter = 0 - - while True: - line = proc.stderr.readline() - if line == '' and proc.poll() is not None: - break - if not line: - time.sleep(0.01) - no_output_counter += 1 - - # 连续60秒完全没输出 = 彻底僵尸 - if no_output_counter > 6000: # 0.01s × 6000 = 60秒 - print("【致命】FFmpeg连续60秒无任何输出,判定为彻底僵尸进程,强制重启!") - break - continue - - no_output_counter = 0 - line = line.strip() - print(f"FFmpeg: {line}") - - # 解析进度 - frame_match = frame_pattern.search(line) - current_time_sec = parse_ffmpeg_time(line) - - updated = False - - if frame_match: - current_frame = int(frame_match.group(1)) - if current_frame > last_frame: - last_frame = current_frame - last_update_time = time.time() - stuck_counter = 0 - updated = True - - if current_time_sec and current_time_sec > last_time_sec + 0.5: - last_time_sec = current_time_sec - last_update_time = time.time() - stuck_counter = 0 - updated = True - - if updated: - continue - - # 超过28秒没有任何进度增长 → 判定为假死 - if time.time() - last_update_time > 28: - stuck_counter += 1 - if stuck_counter >= 2: - print(f"【严重】检测到FFmpeg假死({int(time.time() - last_update_time)}秒无进度),强制重启进程!") - break - - # 错误关键词快速退出 - if any(kw in line.lower() for kw in [ - "error", "failed", "invalid data", "connection timed out", - "server error", "403 forbidden", "401 unauthorized", - "members only", "private video", "live stream ended" - ]): - print("检测到致命错误或下播关键词,准备退出推流...") - time.sleep(4) - break - - # 退出内层循环 → 需要重启或结束 - returncode = proc.poll() - if returncode is not None: - if returncode == 0: - print("主播已下播,FFmpeg正常结束,推流停止") - break - else: - print(f"FFmpeg异常退出(returncode={returncode}),8秒后自动重连...") - time.sleep(8) - continue - else: - print("FFmpeg被强制中断,8秒后重新启动...") - time.sleep(8) - continue - - except Exception as inner_e: - print(f"推流内层循环异常: {inner_e}") - time.sleep(8) - continue - - # ====================== 彻底清理进程和状态 ====================== - print("正在执行最终清理,确保无僵尸进程...") - - if proc and proc.poll() is None: - print("发现残留FFmpeg进程,执行kill -9") - proc.kill() - try: - proc.wait(timeout=6) - except: - pass - - # 清理全局状态(一个都不漏) - try: - recording.discard(record_name) - except: - pass - try: - recording_time_list.pop(record_name, None) - except: - pass - try: - if record_url in running_list: - running_list.remove(record_url) - monitoring = max(0, monitoring - 1) - except: - pass - try: - clear_record_info(record_name, record_url) - except: - pass - - color_obj.print_colored(f"[{record_name}] YouTube推流已彻底停止并完成全部资源清理\n", color_obj.GREEN) - - except Exception as outer_e: \ No newline at end of file diff --git a/backup/55555 b/backup/55555 deleted file mode 100644 index 1a1bc67..0000000 --- a/backup/55555 +++ /dev/null @@ -1,257 +0,0 @@ -# ====================== YouTube 转推流【2025终极防假死完整版】====================== - # 解决所有已知假死、time=0、静默卡死、进程僵尸、缓冲爆满、HLS段404、CDN假死 - # 实测56个直播同时跑30天以上,掉线率接近0 - import platform - - try: - now = time.strftime("%Y-%m-%d_%H-%M-%S", time.localtime()) - title_suffix = f"_{title_in_name}" if title_in_name else "" - stream_title = f"{anchor_name}{title_suffix}_{now}" - - YT_STREAM_KEY = "qxvb-r47b-r5ju-6ud3-6k7z" - youtube_rtmp = f"rtmp://a.rtmp.youtube.com/live2/{YT_STREAM_KEY}" - - print(f"{rec_info} 开始推流到 YouTube: {stream_title}") - - # ----------------- NVENC 硬件加速检测 ----------------- - codec_v = "libx264" - preset_v = "veryfast" - tune_param = ["-tune", "zerolatency"] - try: - if platform.system().lower() in ["windows", "linux"]: - check = subprocess.run(["nvidia-smi"], capture_output=True, timeout=8) - if check.returncode == 0: - enc = subprocess.run(["ffmpeg", "-hide_banner", "-encoders"], capture_output=True, text=True, timeout=8) - if "h264_nvenc" in enc.stdout: - codec_v = "h264_nvenc" - preset_v = "p5" - tune_param = ["-tune", "ll", "-rc", "vbr", "-cq", "23"] - print("检测到 NVIDIA GPU → 使用 NVENC 硬件加速编码") - except Exception as e: - print(f"NVENC检测异常: {e},使用CPU编码") - - # ----------------- 正则表达式准备 ----------------- - frame_pattern = re.compile(r"frame=\s*(\d+)") - time_pattern = re.compile(r"time=\s*(\d{2}:\d{2}:\d{2}\.\d{2})") - - def parse_ffmpeg_time(line): - m = time_pattern.search(line) - if m: - t = m.group(1) - try: - h, m, s = t.split(':') - return int(h)*3600 + int(m)*60 + float(s) - except: - return None - return None - - # ----------------- FFmpeg 终极防卡命令 ----------------- - ffmpeg_command = [ - "ffmpeg", - # ========== 输入端终极防卡参数 ========== - "-rw_timeout", "15000000", # 15秒读超时直接断开 - "-reconnect", "1", - "-reconnect_at_eof", "1", - "-reconnect_streamed", "1", - "-reconnect_delay_max", "8", - "-fflags", "+genpts+discardcorrupt+igndts", - "-err_detect", "ignore_err", - "-thread_queue_size", "2048", # 加大到2048,彻底防爆 - "-analyzeduration", "3000000", - "-probesize", "3000000", - "-i", real_url, - - # ========== 画面处理 ========== - "-vf", "scale=720:1280:force_original_aspect_ratio=decrease,pad=720:1280:(ow-iw)/2:(oh-ih)/2:black", - - # ========== 编码参数 ========== - "-c:v", codec_v, - "-preset", preset_v, - *tune_param, - "-b:v", "2500k", - "-maxrate", "2500k", - "-bufsize", "5000k", - "-g", "50", - "-keyint_min", "50", - "-r", "25", - "-pix_fmt", "yuv420p", - "-bf", "0", - - # ========== 音频 ========== - "-c:a", "aac", - "-b:a", "128k", - "-ar", "44100", - "-ac", "2", - "-af", "aresample=async=1:first_pts=0", - - # ========== 低延迟输出 ========== - "-flags", "+low_delay+global_header", - "-fflags", "+genpts+nobuffer", - "-threads", "8", - "-f", "flv", - youtube_rtmp - ] - - proc = None - - while True: - try: - # 杀掉残留进程 - if proc and proc.poll() is None: - print("发现残留FFmpeg进程,正在强制终结...") - proc.terminate() - try: - proc.wait(timeout=6) - except: - proc.kill() - proc.wait(timeout=3) - - current_time = datetime.datetime.now().strftime('%H:%M:%S') - print(f"[{current_time}] 正在启动YouTube推流进程 → {stream_title}") - - proc = subprocess.Popen( - ffmpeg_command, - stdin=subprocess.DEVNULL, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True, - bufsize=1, - universal_newlines=True - ) - - last_frame = 0 - last_time_sec = 0.0 - last_update_time = time.time() - stuck_counter = 0 - no_output_counter = 0 - - while True: - line = proc.stderr.readline() - if line == '' and proc.poll() is not None: - break - if not line: - time.sleep(0.01) - no_output_counter += 1 - - # 连续60秒完全没输出 = 彻底僵尸 - if no_output_counter > 6000: # 0.01s × 6000 = 60秒 - print("【致命】FFmpeg连续60秒无任何输出,判定为彻底僵尸进程,强制重启!") - break - continue - - no_output_counter = 0 - line = line.strip() - print(f"FFmpeg: {line}") - - # 解析进度 - frame_match = frame_pattern.search(line) - current_time_sec = parse_ffmpeg_time(line) - - updated = False - - if frame_match: - current_frame = int(frame_match.group(1)) - if current_frame > last_frame: - last_frame = current_frame - last_update_time = time.time() - stuck_counter = 0 - updated = True - - if current_time_sec and current_time_sec > last_time_sec + 0.5: - last_time_sec = current_time_sec - last_update_time = time.time() - stuck_counter = 0 - updated = True - - if updated: - continue - - # 超过28秒没有任何进度增长 → 判定为假死 - if time.time() - last_update_time > 28: - stuck_counter += 1 - if stuck_counter >= 2: - print(f"【严重】检测到FFmpeg假死({int(time.time() - last_update_time)}秒无进度),强制重启进程!") - break - - # 错误关键词快速退出 - if any(kw in line.lower() for kw in [ - "error", "failed", "invalid data", "connection timed out", - "server error", "403 forbidden", "401 unauthorized", - "members only", "private video", "live stream ended" - ]): - print("检测到致命错误或下播关键词,准备退出推流...") - time.sleep(4) - break - - # 退出内层循环 → 需要重启或结束 - returncode = proc.poll() - if returncode is not None: - if returncode == 0: - print("主播已下播,FFmpeg正常结束,推流停止") - break - else: - print(f"FFmpeg异常退出(returncode={returncode}),8秒后自动重连...") - time.sleep(8) - continue - else: - print("FFmpeg被强制中断,8秒后重新启动...") - time.sleep(8) - continue - - except Exception as inner_e: - print(f"推流内层循环异常: {inner_e}") - time.sleep(8) - continue - - # ====================== 彻底清理进程和状态 ====================== - print("正在执行最终清理,确保无僵尸进程...") - - if proc and proc.poll() is None: - print("发现残留FFmpeg进程,执行kill -9") - proc.kill() - try: - proc.wait(timeout=6) - except: - pass - - # 清理全局状态(一个都不漏) - try: - recording.discard(record_name) - except: - pass - try: - recording_time_list.pop(record_name, None) - except: - pass - try: - if record_url in running_list: - running_list.remove(record_url) - monitoring = max(0, monitoring - 1) - except: - pass - try: - clear_record_info(record_name, record_url) - except: - pass - - color_obj.print_colored(f"[{record_name}] YouTube推流已彻底停止并完成全部资源清理\n", color_obj.GREEN) - - except Exception as outer_e: - print(f"YouTube推流最外层异常: {outer_e}") - # 就算最外层炸了,也要强行清理 - try: - if 'proc' in locals() and proc and proc.poll() is None: - proc.kill() - except: - pass - 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 - finally: - color_obj.print_colored(f"[{record_name}] 异常退出但已强制清理完毕\n", color_obj.RED) diff --git a/backup/66666 b/backup/66666 deleted file mode 100644 index ed865f4..0000000 --- a/backup/66666 +++ /dev/null @@ -1,197 +0,0 @@ -import platform -import time -import datetime -import subprocess - -# ====================== 抖音 → YouTube 转推核心代码(终极稳定版)====================== -try: - # 时间戳和标题设置 - timestamp_str = time.strftime("%Y-%m-%d_%H-%M-%S", time.localtime()) - title_suffix = f"_{title_in_name}" if title_in_name else "" - stream_title = f"{anchor_name}{title_suffix}_{timestamp_str}" - - # 你的 YouTube 推流密钥(请务必换成自己的!) - YT_STREAM_KEY = "x04z-564w-aks7-embw-30y4" # food频道 - youtube_rtmp = f"rtmp://a.rtmp.youtube.com/live2/{YT_STREAM_KEY}" - - print(f"{rec_info} 开始推流到 YouTube: {stream_title}") - - # ----------------- 自动检测并启用 NVENC(有N卡就硬件加速)----------------- - codec_v = "libx264" - preset_v = "veryfast" - tune_param = ["-tune", "zerolatency"] - try: - if platform.system().lower() in ["windows", "linux"]: - check = subprocess.run(["nvidia-smi"], capture_output=True, timeout=8) - if check.returncode == 0: - enc = subprocess.run(["ffmpeg", "-hide_banner", "-encoders"], capture_output=True, text=True, timeout=8) - if "h264_nvenc" in enc.stdout: - codec_v = "h264_nvenc" - preset_v = "p5" - tune_param = ["-tune", "ll", "-rc", "vbr", "-cq", "23"] - print("检测到 NVIDIA GPU → 使用 NVENC 硬件加速") - except Exception as e: - print(f"NVENC检测异常: {e},使用CPU编码") - - # ----------------- FFmpeg 推流命令(已极致优化)----------------- - ffmpeg_command = [ - "ffmpeg", - "-rw_timeout", "15000000", - "-reconnect", "1", - "-reconnect_at_eof", "1", - "-reconnect_streamed", "1", - "-reconnect_delay_max", "8", - "-fflags", "+genpts+discardcorrupt+igndts", - "-err_detect", "ignore_err", - "-thread_queue_size", "2048", - "-analyzeduration", "3000000", - "-probesize", "3000000", - "-i", real_url, - "-vf", "scale=720:1280:force_original_aspect_ratio=decrease,pad=720:1280:(ow-iw)/2:(oh-ih)/2:black", - "-c:v", codec_v, "-preset", preset_v, *tune_param, - "-b:v", "2500k", "-maxrate", "2500k", "-bufsize", "5000k", - "-g", "50", "-r", "25", "-pix_fmt", "yuv420p", "-bf", "0", - "-c:a", "aac", "-b:a", "128k", "-ar", "44100", "-ac", "2", - "-af", "aresample=async=1:first_pts=0", - "-flags", "+low_delay+global_header", - "-fflags", "+genpts+nobuffer", - "-threads", "8", - "-f", "flv", youtube_rtmp - ] - - proc = None - start_time = time.time() # 本次推流开始时间 - - while True: - try: - # 清理上一次残留进程 - if proc and proc.poll() is None: - proc.terminate() - try: proc.wait(timeout=6) - except: proc.kill() - - print(f"[{datetime.datetime.now().strftime('%H:%M:%S')}] 启动YouTube推流 → {stream_title}") - proc = subprocess.Popen( - ffmpeg_command, - stdin=subprocess.DEVNULL, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True, - bufsize=1, - universal_newlines=True - ) - - # 时间戳属性 - proc.last_out_ts = time.time() - last_frame_time = time.time() # ← 关键:最后一帧时间 - consecutive_404_count = 0 - - # ====================== 核心循环(已加入YouTube救命机制)====================== - while True: - line = proc.stderr.readline() - if line == '' and proc.poll() is not None: - break - if not line: - time.sleep(0.01) - continue - - line = line.strip() - if line: - proc.last_out_ts = time.time() - - # 检测到新帧就刷新时间(最准确的标志 - if "frame=" in line or "fps=" in line: - last_frame_time = time.time() - - print(f"FFmpeg: {line}") - - line_lower = line.lower() - - # 404 连续计数(8次真下播) - is_404 = "404" in line_lower and ("not found" in line_lower or "http @" in line_lower) - if is_404: - consecutive_404_count += 1 - print(f"【404计数】第 {consecutive_404_count} 次(连续8次即退出)") - if consecutive_404_count >= 8: - print("【真下播】连续8次404,直播已结束,停止推流") - if proc and proc.poll() is None: - proc.kill() - break - # 不清零 - - # 致命错误直接退出 - if any(kw in line_lower for kw in [ - "403 forbidden", "401 unauthorized", "members only", - "private video", "live stream ended", "ingestion error" - ]): - print("【致命错误】检测到封禁/私密/下播,立即停止推流") - if proc and proc.poll() is None: - proc.kill() - break - - # YouTube救命机制:12秒无新帧就必须处决!(YouTube最多忍15~20秒) - if time.time() - start_time > 25: # 25秒后进入战斗模式 - if time.time() - last_frame_time > 12: # 12秒没新帧 → 立刻重启 - print(f"【YouTube救命】已 {time.time()-last_frame_time:.1f}秒无新帧,立即重启FFmpeg避免直播被杀!") - if proc and proc.poll() is None: - proc.kill() - break - - # 双保险:20秒完全没日志输出也处决 - if time.time() - start_time > 20: - if time.time() - proc.last_out_ts > 20: - print("【真死流】20秒无任何输出,强制重启进程") - if proc and proc.poll() is None: - proc.kill() - break - - # 进程退出判断 - returncode = proc.poll() - if returncode is not None: - if returncode == 0: - print("主播正常下播,YouTube推流结束") - break - else: - print(f"FFmpeg异常退出(code={returncode}),3秒后自动重启推流...") - time.sleep(3) - continue - - except Exception as e: - print(f"推流过程中出现异常: {e}") - time.sleep(5) - continue - - # ====================== 彻底清理 ====================== - if proc and proc.poll() is None: - print("发现残留FFmpeg进程,执行kill") - proc.kill() - - try: recording.discard(record_name) - except: pass - try: recording_time_list.pop(record_name, None) - except: pass - try: - if record_url in running_list: - running_list.remove(record_url) - monitoring = max(0, monitoring - 1) - except: pass - try: clear_record_info(record_name, record_url) - except: pass - - color_obj.print_colored(f"[{record_name}] YouTube推流已彻底停止并清理完毕\n", color_obj.GREEN) - -except Exception as e: - print(f"推流最外层异常: {e}") - try: - if 'proc' in locals() and proc and proc.poll() is None: - proc.kill() - except: pass - 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}] 异常退出但已清理完毕\n", color_obj.RED) \ No newline at end of file diff --git a/backup/7777 b/backup/7777 deleted file mode 100644 index b6c717d..0000000 --- a/backup/7777 +++ /dev/null @@ -1,203 +0,0 @@ -else: - # ====================== 抖音→YouTube 2025(终极精简版) ====================== - import queue - import time - import datetime - import subprocess - import threading - import unicodedata - import platform - import re - import traceback - - # --------------------- 配置区(可外部传入) --------------------- - YT_STREAM_KEY = "qxvb-r47b-r5ju-6ud3-6k7z" - YOUTUBE_RTMP = f"rtmp://a.rtmp.youtube.com/live2/{YT_STREAM_KEY}" - - MAX_RETRY_DELAY = 90 - INITIAL_RETRY_DELAY = 3 - NO_FRAME_TIMEOUT = 120 # 超过多久无新帧视为下播 - START_CHECK_AFTER = 25.0 # 前多少秒不判无帧(避免开播卡顿误判) - STALE_OUTPUT_SECONDS = 40 # 多久无任何日志视为 ffmpeg 僵死 - STDERR_TIMEOUT = 1.0 - - 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 refused" - ] - - 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" - ) - - # --------------------- 工具函数 --------------------- - def sanitize_title(text: str, max_len: int = 120) -> str: - if not text: - return "" - text = unicodedata.normalize("NFKC", text) - text = ''.join(c for c in text if unicodedata.category(c)[0] != 'C') - text = ''.join(c for c in text if ord(c) <= 0xFFFF) - return text.strip()[:max_len] - - def detect_nvenc() -> tuple[str, str, list[str]]: - """返回 (codec, preset, extra_params)""" - if platform.system().lower() not in ("windows", "linux"): - return "libx264", "veryfast", ["-tune", "zerolatency"] - - try: - if subprocess.run(["nvidia-smi"], capture_output=True, timeout=5).returncode != 0: - return "libx264", "veryfast", ["-tune", "zerolatency"] - - result = subprocess.run( - ["ffmpeg", "-hide_banner", "-encoders"], - capture_output=True, text=True, timeout=8 - ) - if "h264_nvenc" in result.stdout: - print("[INFO] 检测到 NVIDIA GPU → 使用 NVENC 硬件加速") - return "h264_nvenc", "p5", ["-tune", "ll", "-rc", "vbr", "-cq", "21", "-b:v", "2500k"] - except Exception as e: - print(f"[WARN] NVENC 检测失败,回落软件编码: {e}") - return "libx264", "veryfast", ["-tune", "zerolatency"] - - def build_ffmpeg_command(real_url: str): - codec_v, preset_v, tune_params = detect_nvenc() - return [ - "ffmpeg", "-y", "-loglevel", "info", - "-rw_timeout", "15000000", - "-reconnect", "1", "-reconnect_at_eof", "1", "-reconnect_streamed", "1", - "-reconnect_delay_max", "15", - "-fflags", "+genpts+discardcorrupt", "-err_detect", "ignore_err", - "-thread_queue_size", "8192", - "-headers", DOUYIN_HEADERS, - "-i", real_url, - "-vf", "scale=720:1280:force_original_aspect_ratio=decrease,pad=720:1280:(ow-iw)/2:(oh-ih)/2:black", - "-c:v", codec_v, "-preset", preset_v, *tune_params, - "-b:v", "2500k", "-maxrate", "2500k", "-bufsize", "5000k", - "-g", "50", "-r", "25", "-pix_fmt", "yuv420p", - "-c:a", "aac", "-b:a", "128k", "-ar", "44100", "-ac", "2", - "-af", "aresample=async=1:first_pts=0", - "-flags", "+global_header", - "-f", "flv", YOUTUBE_RTMP - ] - - # --------------------- 标题生成 --------------------- - 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} 开始推流到 YouTube: {stream_title}") - - # --------------------- 主逻辑(单层循环 + 单一 finally) --------------------- - proc = None - reader_thread = None - should_exit = False # 替代 StreamEnded 异常 - retry_delay = INITIAL_RETRY_DELAY - - def cleanup_resources(): - """统一清理(只调用一次)""" - nonlocal proc, reader_thread - if proc and proc.poll() is None: - try: proc.terminate(); proc.wait(timeout=5) - except: proc.kill() - if reader_thread and reader_thread.is_alive(): - reader_thread.join(timeout=1) - - # 全局状态清理(只执行一次) - 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: pass - - try: - ffmpeg_cmd = build_ffmpeg_command(real_url) - - while not should_exit: - if retry_delay > INITIAL_RETRY_DELAY: - print(f"[WARN] 防风控等待 {retry_delay}s 后重新连接...") - time.sleep(retry_delay) - - # 清理上一次的残留 - cleanup_resources() - proc = None - reader_thread = None - - stderr_q = queue.Queue() - - print(f"[INFO][{datetime.datetime.now():%H:%M:%S}] 启动 YouTube 推流 → {stream_title}") - proc = subprocess.Popen( - ffmpeg_cmd, - stdin=subprocess.DEVNULL, - stdout=subprocess.DEVNULL, - stderr=subprocess.PIPE, - text=True, - bufsize=1, - universal_newlines=True - ) - reader_thread = threading.Thread( - target=lambda p=proc, q=stderr_q: ( - [q.put(l) for l in iter(p.stderr.readline, "")] or q.put(None) - ) - ) - reader_thread.daemon = True - reader_thread.start() - - start_time = last_frame_time = last_log_time = time.time() - - while True: - try: - line = stderr_q.get(timeout=STDERR_TIMEOUT) - except queue.Empty: - # 超时检查僵死 - if time.time() - last_log_time > STALE_OUTPUT_SECONDS: - print("[WARN] FFmpeg 长时间无输出,强制重启") - break - if time.time() - start_time > START_CHECK_AFTER and time.time() - last_frame_time > NO_FRAME_TIMEOUT: - print(f"[INFO] 连续 {NO_FRAME_TIMEOUT}s 无新帧,判定主播已下播") - should_exit = True - break - continue - - if line is None: # stderr 关闭 - break - - line = line.rstrip() - if line: - last_log_time = time.time() - print(f"[FFmpeg] {line}") - - lower = line.lower() - if any(kw in lower for kw in END_KEYWORDS): - print("[INFO] 检测到主播真下播关键字,停止转推") - should_exit = True - break - - if re.search(r'frame=\s*\d+', lower) or "fps=" in lower: - last_frame_time = time.time() - - # 内层循环退出:判断是否真下播 - if should_exit: - break - - # 网络波动等情况 → 指数退避重试 - retry_delay = min(retry_delay * 2, MAX_RETRY_DELAY) - - # --------------------- 正常退出 --------------------- - color_obj.print_colored(f"[{record_name}] YouTube 推流已彻底停止并清理完毕\n", color_obj.GREEN) - - except Exception as e: - print(f"[FATAL] YouTube 推流意外崩溃: {e}") - traceback.print_exc() - color_obj.print_colored(f"[{record_name}] YouTube 推流异常退出但已清理\n", color_obj.RED) - - finally: - # 无论如何只清理一次 - cleanup_resources() - return \ No newline at end of file diff --git a/backup/888 b/backup/888 deleted file mode 100644 index 55ea46a..0000000 --- a/backup/888 +++ /dev/null @@ -1,19 +0,0 @@ -ffmpeg_command = [ - "ffmpeg", "-loglevel", "info", - "-rw_timeout", "15000000", - "-reconnect", "1", "-reconnect_at_eof", "1", "-reconnect_streamed", "1", - "-reconnect_delay_max", "15", - "-fflags", "+genpts+discardcorrupt", - "-err_detect", "ignore_err", - "-thread_queue_size", "8192", - "-headers", douyin_headers, - "-i", real_url, - "-vf", "scale=720:1280:force_original_aspect_ratio=decrease,pad=720:1280:(ow-iw)/2:(oh-ih)/2:black", - "-c:v", codec_v, "-preset", preset_v, *tune_param, - "-b:v", "2500k", "-maxrate", "2500k", "-bufsize", "5000k", - "-g", "50", "-r", "25", "-pix_fmt", "yuv420p", - "-c:a", "aac", "-b:a", "128k", "-ar", "44100", "-ac", "2", - "-af", "aresample=async=1:first_pts=0", - "-flags", "+global_header", - "-f", "flv", youtube_rtmp - ] \ No newline at end of file diff --git a/backup/msg_push.py b/backup/msg_push.py deleted file mode 100644 index c682b20..0000000 --- a/backup/msg_push.py +++ /dev/null @@ -1,295 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -Author: Hmily -GitHub: https://github.com/ihmily -Date: 2023-09-03 19:18:36 -Update: 2025-01-23 17:16:12 -Copyright (c) 2023-2024 by Hmily, All Rights Reserved. -""" -from typing import Dict, Any -import json -import base64 -import urllib.request -import urllib.error -import smtplib -from email.header import Header -from email.mime.multipart import MIMEMultipart -from email.mime.text import MIMEText - -no_proxy_handler = urllib.request.ProxyHandler({}) -opener = urllib.request.build_opener(no_proxy_handler) -headers: Dict[str, str] = {'Content-Type': 'application/json'} - - -def dingtalk(url: str, content: str, number: str = None, is_atall: bool = False) -> Dict[str, Any]: - success = [] - error = [] - api_list = url.replace(',', ',').split(',') if url.strip() else [] - for api in api_list: - json_data = { - 'msgtype': 'text', - 'text': { - 'content': content, - }, - "at": { - "atMobiles": [ - number - ], - "isAtAll": is_atall - }, - } - try: - data = json.dumps(json_data).encode('utf-8') - req = urllib.request.Request(api, data=data, headers=headers) - response = opener.open(req, timeout=10) - json_str = response.read().decode('utf-8') - json_data = json.loads(json_str) - if json_data['errcode'] == 0: - success.append(api) - else: - error.append(api) - print(f'钉钉推送失败, 推送地址:{api}, {json_data["errmsg"]}') - except Exception as e: - error.append(api) - print(f'钉钉推送失败, 推送地址:{api}, 错误信息:{e}') - return {"success": success, "error": error} - - -def xizhi(url: str, title: str, content: str) -> Dict[str, Any]: - success = [] - error = [] - api_list = url.replace(',', ',').split(',') if url.strip() else [] - for api in api_list: - json_data = { - 'title': title, - 'content': content - } - try: - data = json.dumps(json_data).encode('utf-8') - req = urllib.request.Request(api, data=data, headers=headers) - response = opener.open(req, timeout=10) - json_str = response.read().decode('utf-8') - json_data = json.loads(json_str) - if json_data['code'] == 200: - success.append(api) - else: - error.append(api) - print(f'微信推送失败, 推送地址:{api}, 失败信息:{json_data["msg"]}') - except Exception as e: - error.append(api) - print(f'微信推送失败, 推送地址:{api}, 错误信息:{e}') - return {"success": success, "error": error} - - -def send_email(email_host: str, login_email: str, email_pass: str, sender_email: str, sender_name: str, - to_email: str, title: str, content: str, smtp_port: str = None, open_ssl: bool = True) -> Dict[str, Any]: - receivers = to_email.replace(',', ',').split(',') if to_email.strip() else [] - - try: - message = MIMEMultipart() - send_name = base64.b64encode(sender_name.encode("utf-8")).decode() - message['From'] = f'=?UTF-8?B?{send_name}?= <{sender_email}>' - message['Subject'] = Header(title, 'utf-8') - if len(receivers) == 1: - message['To'] = receivers[0] - - t_apart = MIMEText(content, 'plain', 'utf-8') - message.attach(t_apart) - - if open_ssl: - smtp_port = int(smtp_port) or 465 - smtp_obj = smtplib.SMTP_SSL(email_host, smtp_port) - else: - smtp_port = int(smtp_port) or 25 - smtp_obj = smtplib.SMTP(email_host, smtp_port) - smtp_obj.login(login_email, email_pass) - smtp_obj.sendmail(sender_email, receivers, message.as_string()) - return {"success": receivers, "error": []} - except smtplib.SMTPException as e: - print(f'邮件推送失败, 推送邮箱:{to_email}, 错误信息:{e}') - return {"success": [], "error": receivers} - - -def tg_bot(chat_id: int, token: str, content: str) -> Dict[str, Any]: - try: - json_data = { - "chat_id": chat_id, - 'text': content - } - url = f'https://api.telegram.org/bot{token}/sendMessage' - data = json.dumps(json_data).encode('utf-8') - req = urllib.request.Request(url, data=data, headers=headers) - response = urllib.request.urlopen(req, timeout=15) - json_str = response.read().decode('utf-8') - _json_data = json.loads(json_str) - return {"success": [1], "error": []} - except Exception as e: - print(f'tg推送失败, 聊天ID:{chat_id}, 错误信息:{e}') - return {"success": [], "error": [1]} - - -def bark(api: str, title: str = "message", content: str = 'test', level: str = "active", - badge: int = 1, auto_copy: int = 1, sound: str = "", icon: str = "", group: str = "", - is_archive: int = 1, url: str = "") -> Dict[str, Any]: - success = [] - error = [] - api_list = api.replace(',', ',').split(',') if api.strip() else [] - for _api in api_list: - json_data = { - "title": title, - "body": content, - "level": level, - "badge": badge, - "autoCopy": auto_copy, - "sound": sound, - "icon": icon, - "group": group, - "isArchive": is_archive, - "url": url - } - try: - data = json.dumps(json_data).encode('utf-8') - req = urllib.request.Request(_api, data=data, headers=headers) - response = opener.open(req, timeout=10) - json_str = response.read().decode("utf-8") - json_data = json.loads(json_str) - if json_data['code'] == 200: - success.append(_api) - else: - error.append(_api) - print(f'Bark推送失败, 推送地址:{_api}, 失败信息:{json_data["message"]}') - except Exception as e: - error.append(api) - print(f'Bark推送失败, 推送地址:{_api}, 错误信息:{e}') - return {"success": success, "error": error} - - -def ntfy(api: str, title: str = "message", content: str = 'test', tags: str = 'tada', priority: int = 3, - action_url: str = "", attach: str = "", filename: str = "", click: str = "", icon: str = "", - delay: str = "", email: str = "", call: str = "") -> Dict[str, Any]: - success = [] - error = [] - api_list = api.replace(',', ',').split(',') if api.strip() else [] - tags = tags.replace(',', ',').split(',') if tags else ['partying_face'] - actions = [{"action": "view", "label": "view live", "url": action_url}] if action_url else [] - for _api in api_list: - server, topic = _api.rsplit('/', maxsplit=1) - json_data = { - "topic": topic, - "title": title, - "message": content, - "tags": tags, - "priority": priority, - "attach": attach, - "filename": filename, - "click": click, - "actions": actions, - "markdown": False, - "icon": icon, - "delay": delay, - "email": email, - "call": call - } - - try: - data = json.dumps(json_data, ensure_ascii=False).encode('utf-8') - req = urllib.request.Request(server, data=data, headers=headers) - response = opener.open(req, timeout=10) - json_str = response.read().decode("utf-8") - json_data = json.loads(json_str) - if "error" not in json_data: - success.append(_api) - else: - error.append(_api) - print(f'ntfy推送失败, 推送地址:{_api}, 失败信息:{json_data["error"]}') - except urllib.error.HTTPError as e: - error.append(_api) - error_msg = e.read().decode("utf-8") - print(f'ntfy推送失败, 推送地址:{_api}, 错误信息:{json.loads(error_msg)["error"]}') - except Exception as e: - error.append(api) - print(f'ntfy推送失败, 推送地址:{_api}, 错误信息:{e}') - return {"success": success, "error": error} - - -def pushplus(token: str, title: str, content: str) -> Dict[str, Any]: - """ - PushPlus推送通知 - API文档: https://www.pushplus.plus/doc/ - """ - success = [] - error = [] - token_list = token.replace(',', ',').split(',') if token.strip() else [] - - for _token in token_list: - json_data = { - 'token': _token, - 'title': title, - 'content': content - } - - try: - url = 'https://www.pushplus.plus/send' - data = json.dumps(json_data).encode('utf-8') - req = urllib.request.Request(url, data=data, headers=headers) - response = opener.open(req, timeout=10) - json_str = response.read().decode('utf-8') - json_data = json.loads(json_str) - - if json_data.get('code') == 200: - success.append(_token) - else: - error.append(_token) - print(f'PushPlus推送失败, Token:{_token}, 失败信息:{json_data.get("msg", "未知错误")}') - except Exception as e: - error.append(_token) - print(f'PushPlus推送失败, Token:{_token}, 错误信息:{e}') - - return {"success": success, "error": error} - - -if __name__ == '__main__': - send_title = '直播通知' # 标题 - send_content = '张三 开播了!' # 推送内容 - - # 钉钉推送通知 - webhook_api = '' # 替换成自己Webhook链接,参考文档:https://open.dingtalk.com/document/robots/custom-robot-access - phone_number = '' # 被@用户的手机号码 - is_atall = '' # 是否@全体 - # dingtalk(webhook_api, send_content, phone_number) - - # 微信推送通知 - # 替换成自己的单点推送接口,获取地址:https://xz.qqoq.net/#/admin/one - # 当然也可以使用其他平台API 如server酱 使用方法一样 - xizhi_api = 'https://xizhi.qqoq.net/xxxxxxxxx.send' - # xizhi(xizhi_api, send_content) - - # telegram推送通知 - tg_token = '' # tg搜索"BotFather"获取的token值 - tg_chat_id = 000000 # tg搜索"userinfobot"获取的chat_id值,即可发送推送消息给你自己,如果下面的是群组id则发送到群 - # tg_bot(tg_chat_id, tg_token, send_content) - - # email_message( - # email_host="smtp.qq.com", - # login_email="", - # email_pass="", - # sender_email="", - # sender_name="", - # to_email="", - # title="", - # content="", - # ) - - bark_url = 'https://xxx.xxx.com/key/' - # bark(bark_url, send_title, send_content) - - ntfy( - api="https://ntfy.sh/xxxxx", - title="直播推送", - content="xxx已开播", - ) - - # PushPlus推送通知 - pushplus_token = '' # 替换成自己的PushPlus Token,获取地址:https://www.pushplus.plus/ - # pushplus(pushplus_token, send_title, send_content) diff --git a/backup/robust_relay.py b/backup/robust_relay.py deleted file mode 100644 index 000d753..0000000 --- a/backup/robust_relay.py +++ /dev/null @@ -1,227 +0,0 @@ -# robust_relay.py -# 高稳定性 YouTube 转播模块(防误判下播 + 看门狗 + 自动重连 + token刷新) -# 作者:融合 A+B 代码精华,专为抖音/抖音国际版长期无人值守转播设计 - -import subprocess -import time -import threading -import os -import signal -import datetime -from typing import List, Optional, Callable - -# ==================== 配置常量(可全局覆盖) ==================== -WATCHDOG_SILENCE = 600 # 10分钟无输出 → 软重启 -WATCHDOG_GRACE = 120 # 启动宽限期 -REFRESH_URL_EVERY = 300 # 每5分钟强制刷新 real_url(防token过期) -OFFLINE_DEBOUNCE = 3 # 连续3次API返回下播才认为真下播 -OFFLINE_WINDOW = 120 # 最近120秒有活跃就忽略下播信号 -SOFT_RESTART_COOLDOWN = 90 # 软重启最小间隔 - -# ==================== 轻量探活 ==================== -def _probe_alive(url: str, timeout: float = 5.0) -> bool: - if not url: - return False - try: - import urllib.request, contextlib - opener = urllib.request.build_opener() - if hasattr(urllib.request, 'getproxies'): - proxy = urllib.request.getproxies().get('http') - if proxy: - opener.add_handler(urllib.request.ProxyHandler({"http": proxy, "https": proxy})) - req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"}, method="HEAD") - with contextlib.closing(opener.open(req, timeout=timeout)) as r: - if r.code >= 400: - return False - head = r.read(256) - if url.lower().endswith(".m3u8"): - return b"#EXTM3U" in head.upper() - if url.lower().endswith(".flv"): - return head.startswith(b"FLV") - return len(head) > 0 - except: - return False - -# ==================== 增强版 ffmpeg 启动器 ==================== -def _launch_ffmpeg( - input_url: str, - youtube_rtmp: str, - anchor_name: str, - logger: Callable[[str], None], - extra_cmd: List[str] = None -) -> subprocess.Popen: - - cmd = [ - "ffmpeg", "-nostdin", "-hide_banner", "-stats", "-stats_period", "60", "-loglevel", "info", - - # 输入端超强重连 - "-reconnect", "1", "-reconnect_streamed", "1", "-reconnect_at_eof", "1", - "-reconnect_delay_max", "30", "-rw_timeout", "20000000", - - # 极致低延迟 + 时间戳修复 - "-fflags", "+genpts+igndts+flush_packets", "-flags", "+global_header", - "-thread_queue_size", "4096", "-analyzeduration", "0", "-probesize", "32", - - "-i", input_url, - - # copy 模式(质量无损) - "-c:v", "copy", "-c:a", "copy", "-bsf:a", "aac_adtstoasc", - "-map", "0:v?", "-map", "0:a?", - - # YouTube 强制参数 - "-f", "mpegts", "-g", "2", "-rtmp_buffer", "1000", "-rtmp_live", "live", - "-max_delay", "0", "-tune", "zerolatency", "-avoid_negative_ts", "make_zero", - "-muxdelay", "0", "-muxpreload", "0", - "-fflags", "+nobuffer+flush_packets", "-avioflags", "direct", - ] - - if extra_cmd: - cmd.extend(extra_cmd) - - cmd.append(youtube_rtmp) - - creationflags = subprocess.CREATE_NEW_PROCESS_GROUP if os.name == "nt" else 0 - proc = subprocess.Popen( - cmd, - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - bufsize=0, - creationflags=creationflags - ) - - # 用于看门狗 - proc.last_output_ts = time.time() - - def _reader(): - try: - for line in proc.stdout: # type: ignore - if not line: - break - text = line.decode(errors="ignore").rstrip() - proc.last_output_ts = time.time() - if any(k in text for k in ["bitrate=", "frame=", "speed=", "fps="]): - logger(f"[{anchor_name}] {text}") - except: - pass - threading.Thread(target=_reader, daemon=True).start() - - return proc - -# ==================== 主函数:高稳定性转播(替代原 check_subprocess) ==================== -def robust_youtube_relay( - anchor_name: str, - real_url: str, - youtube_rtmp: str, - logger: Callable[[str], None], - get_latest_stream_url: Callable[[], Optional[str]], # 用于刷新 real_url(如有token过期) - is_stream_still_live: Callable[[], bool], # 检查主播是否在播(API) - extra_ffmpeg_args: List[str] = None, # 可选:加 -f segment 分段保存 - heartbeat_callback: Callable[[int], None] = None # 可选:每分钟回调已运行秒数 -) -> bool: - """ - 返回值: - True = 主播已下播(正常结束) - False = 发生未知错误(建议计数错误次数) - """ - proc: Optional[subprocess.Popen] = None - start_time = time.time() - last_refresh = 0.0 - last_soft_restart = 0.0 - offline_count = 0 - last_live_ts = time.time() - - try: - proc = _launch_ffmpeg(real_url, youtube_rtmp, anchor_name, logger, extra_ffmpeg_args) - logger(f"[{anchor_name}] 已开始高稳定性转播 → {youtube_rtmp.split('/')[-1]}") - - while True: - now = time.time() - elapsed = int(now - start_time) - silence = now - proc.last_output_ts if proc else 0 - - # 1. 进程意外退出 → 重启 - if proc and proc.poll() is not None: - logger(f"[{anchor_name}] ffmpeg 意外退出,3秒后重启...") - time.sleep(3) - proc = _launch_ffmpeg(real_url, youtube_rtmp, anchor_name, logger, extra_ffmpeg_args) - start_time = time.time() - continue - - # 2. 看门狗:长时间无输出 → 软重启 - if elapsed > WATCHDOG_GRACE and silence > WATCHDOG_SILENCE: - if now - last_soft_restart > SOFT_RESTART_COOLDOWN: - logger(f"[{anchor_name}] 检测到卡顿({silence:.0f}s无输出),软重启 ffmpeg...") - last_soft_restart = now - if proc and proc.stdin: - proc.stdin.write(b'q') - proc.stdin.flush() - time.sleep(4) - proc = _launch_ffmpeg(real_url, youtube_rtmp, anchor_name, logger, extra_ffmpeg_args) - start_time = time.time() - continue - - # 3. 定期刷新播放地址(防token过期) - if now - last_refresh > REFRESH_URL_EVERY: - new_url = get_latest_stream_url() - if new_url and new_url != real_url: - logger(f"[{anchor_name}] 检测到新的播放地址,切换中...") - real_url = new_url - if proc and proc.stdin: - proc.stdin.write(b'q') - time.sleep(3) - proc = _launch_ffmpeg(real_url, youtube_rtmp, anchor_name, logger, extra_ffmpeg_args) - start_time = time.time() - last_refresh = now - - # 4. 下播防抖判定(核心防误判) - try: - still_live = is_stream_still_live() - except: - still_live = True # 接口异常不判下播 - - if not still_live: - # 交叉验证:流还能访问 → 忽略API误判 - if _probe_alive(real_url): - logger(f"[{anchor_name}] API判下播但流仍活跃,忽略本次误判") - still_live = True - last_live_ts = now - - if still_live: - offline_count = 0 - last_live_ts = now - else: - if now - last_live_ts < OFFLINE_WINDOW: - logger(f"[{anchor_name}] 短期掉线,忽略") - else: - offline_count += 1 - if offline_count >= OFFLINE_DEBOUNCE: - logger(f"[{anchor_name}] 连续{offline_count}次确认下播,结束转播") - if proc and proc.stdin: - proc.stdin.write(b'q') - return True - else: - logger(f"[{anchor_name}] 疑似下播 {offline_count}/{OFFLINE_DEBOUNCE},继续观察") - - # 5. 心跳日志 & 回调 - if elapsed % 60 < 2: - h, m = divmod(elapsed // 60, 60) - logger(f"[{anchor_name}] 正在转播 {h:02d}:{m:02d}") - if heartbeat_callback: - heartbeat_callback(elapsed) - - time.sleep(8) - - except KeyboardInterrupt: - logger(f"[{anchor_name}] 手动终止转播") - return True - except Exception as e: - logger(f"[{anchor_name}] 严重错误: {e}") - return False - finally: - if proc: - try: - proc.stdin.write(b'q') if proc.stdin else None - proc.wait(timeout=8) - except: - proc.kill() \ No newline at end of file diff --git a/backup/wending b/backup/wending deleted file mode 100644 index d706019..0000000 --- a/backup/wending +++ /dev/null @@ -1,111 +0,0 @@ - - try: - if split_video_by_time: - now = time.strftime("%Y-%m-%d_%H-%M-%S", time.localtime()) - print(f"{rec_info} 开始推流到 YouTube: {anchor_name}_{title_in_name}_{now}") - - YT_STREAM_KEY = "ue78-1c3e-mr9g-14mz-9r4z" - youtube_rtmp = f"rtmp://a.rtmp.youtube.com/live2/{YT_STREAM_KEY}" - - # ------------------------ - # NVENC 检测 - # ------------------------ - codec_v = "libx264" - preset_v = "veryfast" - tune_param = ["-tune", "zerolatency"] - - try: - if platform.system().lower() in ["windows", "linux"]: - check = subprocess.run(["nvidia-smi"], capture_output=True, timeout=10) - if check.returncode == 0: - enc = subprocess.run(["ffmpeg", "-hide_banner", "-encoders"], - capture_output=True, text=True, timeout=10) - if "h264_nvenc" in enc.stdout: - codec_v = "h264_nvenc" - preset_v = "p5" - tune_param = ["-tune", "ll", "-rc", "vbr"] - print("检测到 NVIDIA GPU → 使用 NVENC 硬件加速") - except: - pass # 异常自动降级 - - # ------------------------ - # FFmpeg 命令(稳定推流) - # ------------------------ - ffmpeg_command = [ - "ffmpeg", - "-re", - "-i", real_url, - - "-vf", "scale=720:1280:force_original_aspect_ratio=decrease,pad=720:1280:(ow-iw)/2:(oh-ih)/2:black", - - "-c:v", codec_v, - "-preset", preset_v, - *tune_param, - - "-b:v", "2500k", - "-maxrate", "2500k", - "-bufsize", "5000k", - "-g", "50", - "-r", "25", - "-pix_fmt", "yuv420p", - "-bf", "0", - - "-c:a", "aac", - "-b:a", "128k", - "-ar", "44100", - "-ac", "2", - "-af", "aresample=async=1:first_pts=0", - - "-flags", "+low_delay", - "-fflags", "+genpts", - "-thread_queue_size", "512", - "-threads", "6", - - "-f", "flv", - youtube_rtmp - ] - - # ------------------------ - # 永不断流重连逻辑 - # ------------------------ - proc = None - while True: - try: - if proc and proc.poll() is None: - proc.terminate() - try: - proc.wait(timeout=8) - except: - proc.kill() - - print(f"{anchor_name}_{title_in_name}_{now} → 启动 YouTube 推流...") - proc = subprocess.Popen( - ffmpeg_command, - stdin=subprocess.DEVNULL, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True - ) - - # 打印 FFmpeg 错误日志,便于排查 - while True: - line = proc.stderr.readline() - if not line: - break - line = line.strip() - if line: - print(f"FFmpeg: {line}") - if proc.poll() is not None: - break - - code = proc.returncode - if code != 0: - print(f"FFmpeg 退出(code={code}),5秒后重连...") - time.sleep(5) - - except Exception as e: - print(f"启动 FFmpeg 异常: {e}") - time.sleep(5) - - except Exception as e: - print(f"外层异常: {e}") \ No newline at end of file diff --git a/dist/douyin_srs_ffplay.py b/dist/douyin_srs_ffplay.py new file mode 100644 index 0000000..36982ef --- /dev/null +++ b/dist/douyin_srs_ffplay.py @@ -0,0 +1,3 @@ +# Pyarmor 9.2.3 (trial), 000000, non-profits, 2026-01-22T02:38:28.386679 +from pyarmor_runtime_000000 import __pyarmor__ +__pyarmor__(__name__, __file__, b'PY000000\x00\x03\x0c\x00\xcb\r\r\n\x80\x00\x01\x00\x08\x00\x00\x00\x04\x00\x00\x00@\x00\x00\x00\\-\x00\x00\x12\t\x04\x00\xfdx\xfdN\xc4\x84\nH\x7f8Oh\xa61E-\x00\x00\x00\x00\x00\x00\x00\x00k\xb0\xdc 6C\xeaP&$\x0e\xeat\xcf\xeb\x93\xe3\xdc\x9a8\x0e\x96g\x921\t?\xee\x9cS\x9f\xe5\xc1\xf1`\xf5\x8f\x16$\xa8\x05i\xefn\xc8M/\x99\xc7\x8c\x8b\x83\xff\xf7\xd7\xfb\xb42X%-\xf4\x92+\xae4\x9bKAF.\xd3\x9d8\x87\x9a\x93U1\xa3\x80\t\x07\xa5\xa9\x0e!\x1b\x1b\x08\xba\xdd~\x0f\x1b\x08\xef\xee\xc2\xc7\x8c\xd7\xf8\x97-J\x0b)\'~\xf6e\xfaE\x96\xa8\xacu\x1b\xc2\xbd\x9a\x9c8*\xb6\xb9y\x1cq.\xd3$1\x97\x18\x9f\xd3\x89e\xf2\x9e\x8d\xf2\x19e\xfcX\x0f@\xe2\xa0\xb0%\x94\xe0N\x14\xaa\x94\xbb\xbc\xe3\xe4\xf8\xe5\x04Ry\x86\xe3\xb5\x08\xda[\xc2$$\xc5\x8e\x0eC\x86W\x82\xd5\x03\xc0\xb3\x9c\xa9\xac\xe5\x96\xd8\r\xe52(\xd0\xfcCgQ\x17\xfc\xde\xdc+\xa9\xda\xd3$\xdba\r\xb5I8\x1e\xf6\x00&\x7f\xcd.\xe0q`)\x1fOy\xef,\x84\xcak\x18\x08\xa7\xc4\xfd\xc8\xcck\xd8\x80q$\xc1\x80M_\xe8\x01\x03\xbc\x86y\xad\x03;\xf7\xbf\x0cF\xa8=\xb2\xde\xb3Z\xe1\x1bF6:\x92n\x86]xSM\xb1V\xa3\x94\xb9\xffp\xd5\xf94\x00Q;|K\xdf,G&\xb2\xc4;\xd3?tl\xe3\xabYE\xfa<\xe3\xf4\xdc\xb3\xccMz\x80:r\xbb 3t\xac\xc9\x1b0\xee{\x95\xdb\xc4\xf5\xb8\x05&1\xc2I\xbc\xb9L\xa03\xb5B\xddK\x11)Vtx\xc7\xee\x80qT\xb84\x8fXF\x81\xbd\xcb\xb2n.`\x16U\xfc\xee\xc6\x9b\x16~r,\x07\xdc1\xf3\xd7\x99\xf6\x06\xfabUS\xcem\xfd!\xd9\xad\xce\xd0\x92F\x12\xdbg-,\xa5C\x912\xa8x]\x16"\xdb\xb9\x07\xe8\xf2xM3\x90c\xd9\xe3\x01\xa9w[\xd1\xa2Gc\x9f\x1d\x9bL?\xc7\xf7\x88\x02dE\x8fF\xfd\x9f\xfc\xca%\xaeH\x86\x9f.\x95\xaa\xd4\xdd\x1aD#"b\\TH\xa5\x17r@\x8d\x14Xi\xcc\xe3\x9c\xe3\xad\\\x90\xbe2H\xb3\x91\xdb\xe1A\xa4[\xfa`\x95\x93-~\x88Z\x86*P\xc9\xbd\xc4\xb3\xc1\xc9\xc6\xea;\xc1\xd5\xd6\xcc)\xd1\x07\x81\x18\x971 U\xb5?}\xd92\x03]\x93\x0c\x00\xd3\x1cXe\xb69\xd8\xbf\x12\x8f\x07B\xbc\x9b\x97\x1f\xb8\x99\xa2\xff\xdez\x93J\xb2;*\x9ci\xe1_\xef\xd73\x8f\xf8S\xef\x02+\xfb\xd2z\nt\x10\xa1\x87\xea\xd7\x06\x05\x170\x893\xc2; H\x9d\x82\xe7h\x08\x8b\xbe\xf9T\x95\x12\xdb\x1bE\xea\x9a\xcdR|\xa7\xdd\x8d\xf3\xf4\xa3\x04s\xf2a\x07)JRf\xc5d\xec\xd0^\xb8\x17=k\xd5\xbe\xd9\xa77*\xe8\xec\x94\xf3\x95\x82e{\xcfa\x99%9\xf9\xcc\xd6\xd3\xc8a\xa5JD=b\xd0{\xd98\xf5\r\xec\x1bR\xd1\xf6\xfc_\x91\x8bq\xe07I_\xb0\xa1\x00\xe4\x90\xb3$;I*\xb3\xb3&@\x0b_\x02\x12\xc0\xc89OJ\xd7?O#\xb7F`\x03\x15\x90\xa6o\x90[\x85\xc6\xb6/y\xf3\x03\xa8%\xa8\xee-dO0\xc9Ve\xa8&\x91-\x12E\xe5]Q\xbb\x91\xc4\tF\n\xeb\xba\xce\x1f!\x8fw,\xa2\xfb\xde\xe7n\x80\xb5O|\xef\x08\x9e\x9ew\xde"\xcc\xff\'\xde=\xa4\xbe\xd0[\x9c\xa1\x0f%<\xbd\'(4\x1dU[\xb9\x87\xc0\x8d\x12?\x1ab\xe3\xca\xd5vE\xca\xb2a\x13\x83G@\xce\x8f\xba\xb6\x06\x06\xceItd\x91\x88\x87>\xc7\xf2\xca\n\x16\xc7?\xfe\xc7\x8f):\x1c\x86\x99\x86*\xec:\xf1\x80\xe4)\xde\xfe\x03H\x14\x1b!\xbe[o\xf1&\nF:9K)\xd5\xfc\xd2\xdfd\xd1\x94E\xc0S\xa2A"Nt\n\x0b\x050`}\x01\xd9\xc3H\xcc\xce\xd0S\xb3p\x00X\xda3\x1b\xe8\xd6r\xcd\xdc\xbdzv\x8dE\x06\to\xe8\xd7\x9bc\xcf\xa6q\x02ky\xe5z\xdc\x91g^\xae5o\x81\xc9t\xa0\x1f\xa8@S\x008\x9d\xb7O\xca\x885q3S\x15)\xcac\xac4\xde\xfc:\xb2\xea\xd00\x04\xa6\x00\xd0\x9a\xd8\xcc\xdd\xc6.|\xa5\xb1r%\x1a^\x01\x1cGPa\xb6\x8f=\xa5H\x93)\xff\x03\x8d\xbb\x12\xab\x0b;\x1b\xacYf\x82\x89\xb5\xdf,\xe6E\x80hh\xadr\x11\xdcb\xc5\x11\xa4\xd0\x1c=\xa7\xe8\xc0\x8b\xbfN\xaa\x94\\\r\x8e9\xc1 z\xd8\xb8y6%\x86\x1eE\xb9\x18P\xf8f\xca\xde\x00\xbf\x1f\x117\xb6\x9br^khO\x9e\xc8!":z\x1e\x0ec\xd2\x86\xea\x00\x8bQ@8w@?\xae\xde\x14\x9e\xb9,I2E\xfa\xf7j\xf7\xd1\x9ao\x83,\x81Ves\tf\x11\xd8\xe2eN\x98A\x1d|\x12r4+\x8c\x95\xd5\xd3N\xef6\x85W0+e\x82T\xcc\xb3*>9\xf6\\b\xd3t\xd3|\x07\xcd\x93;w\xb9d\x83\x05Q\'\xe9\xf89D\x1c\xd8)\xec6L*\xbd\x85Vtn\xe5R\xf7\xc9\x19n\xc6\x00\x93\x93i\xea\x99\xbd!x\xa6\xf0|\x9a\xd93\n\x0f:\xf5d\xa6(\x16\xe9\x08\x17a\xe3G\xbd\xe2R\xd3Z\x9dP\x1c\xd1\xfa\x91\xff$|\x82^B \x91\xf1q\x85kv6\x11E\xfc\x87\xc5]\xc6\x0bf\x04\x04\x0b\x93\xeaB\xbb\x7f\xcb0X7\xad\xa3\xee\xf3\x88\xd3\xb4\x9e:\x9a\xff\xae\x7f\xac\x93\xa0\xef\x06\x06\xe9\xdda~\xed\xfdZ\xb1\x0e_\xdfB\x08\xe8&\xb6)x\xcf\xcd\xc7KFsm\x8f(^L\x00\x8a\xd9\x99\xde\xf6\xc4\xdc\\*\x84z\xf0r8\x94X\xeb\x9b\xf5H\xa1Z\x99i\x07\xd4\xdc\xa8\x05\x9a\x89\x8d\x9f\x96y9\xae\xbe=\xe7F"\xba\xb3-\x8b\xff\x86\xfa\x8e6W\xb2\x0b\xcd\xce\xaf[\x95nm\xd0\x94\xfe\x1aG~\xe8,\xd6b\xa4\x8a\xf0\x87\x94\x91=\x88=\xcf\xe7\xe1\t\x07\x116\x1a8\xc5\x1d\x9d\xa7\xe1\xd0[\xe6\x1cN\x8e\xaa\xa0|c\t\xebN\x1b\xbc&\xd4\xc9\x80\xb5\x87\xde\xc6\xc8\x95\xbd\xdb\x10\xd7\xb7dFX\x00\xe4\xbe\xd3\x043\x16\xcah\xc4@\x8e\xa0\xc6>p\xd6\x7f\x9f\xc7\xc9\xc4\xdf[\xba\xecs\xdf~\x92jdY\xb6\xe0\x11W\xad^\x85O\x9e\xf9k\x18\xbbZ\x9e\x18\xa6\xde\xfbm\x0fU\xbe\x0b\xf1\xdcW\xe9 \xa9_\xe9.9\xb9\x03@L\x8f\x88\xbd\xff`\t\xc3\x93I\x85\x99\x8dQ\x9c\x1c\xc6\x16\xb8\xb8H\x97I#\xcd!\xd3\x91\x0e\x16\xec\x15\x93\xc4\xf7\x98g\xa7\xed\x06^XU.{\xee\x98\x89\xbc\xc8\x16B\xb88aI\xaa\xa4\xf6\xa9\\\xd9\xd4\xc7\xc2\x7f\xc8\x01A\xedeoa\xd8N\x0b\xab05DbM_mt\xbb\xfe\xf6\xdc\xf4\xf7}k\xc0\x00\xd4A\xbb\xc5j-\xf6\x07\xaa\x10V&g[o\xa7T\xd8kk-\x9d\x8c\xa7\xd4\xc4F\t\xd1/\xac\xd1\xa5>G\xfbl\xd3\x0f\xbd\x16\x9d\x02\x7f\xe09\xd0\xe2\xf93\x8f@\xcb\x84\xa7\xa6\x16~\x1a\xa6\xd6c\x12@=!\xa4\xf4\x9e$\xf0\'t\xb7\x82\xc4Y\xc2\x11vL}\xed\xf0biI22\x16/*\x16\x1e?\x92\x80\x93O> N\xa8\xae\xc6\xb3\xb9\xe0s\x8cz\x85\x86.^\xacUJ\x0fz\xed\xf1\x12\x92\xbf\xe3\x1a\xe6N\xff\xaf\xael\xa0b\xeb\xf1\x98,\xd6:\xbb6d\xc2\xda\xda#\xa0\x91\x10\x9c\x0bF\xe6\xd1cK\x1b\xc8=\x8f\xe4\xa1\xf0\xc4\xaa\x17\x7f6#\x15d\xd60\xf5\x0b\xf9<\x84\xc9\xd6\x0e\xeeZ\xd8\x8bkd\xd0;\xd4\xb9U\xbb\xb9\xc1Qp\xecBC\xcd\xb0\xf63Z8\xb4^\xdek(\x0f`dh\xe1\x8f\xf08\xcd\x05Bs\xfe\xf2\xad\xa2\xb3\xe5\xd5\x89E\x9c\xcd@j\xee\xd4~\xf3\xe9+\xfd=\xca;l\x9c\x9e\xb65\xbe\xcd\\FjL\xf2\x84-\xf7u\xd2uU\xf0\xac\x03\x82\x17O\xeb\xca\xbb\x86\rq-g\xe66\x08`\x06\xd2\xfcWYy\xa1C\xd5c\x14\xc7\xb8\x99F\xfe3\x05\xaf\x17\xd3?\x1f\xf2!\xfdZt\x90\x03\xbb\xb9\x1b[s\xb9zV\xbf\x97h\xf9\x9b\x00c\xe1I\xc2\x85\xdd\xebw\xea\xca\x02\x03\x88\xb9\x089\xfajjg6\xa5\x0f\xee\x1ca;\xb4\xa8v\x07\xb8\xb9\xa8\xe4b\xbe\xed\xf8\xe3%\x08^-\xa8\xdf[i\xf3\xa2N\x8c\xaa\xd7k\x95*\xa2\xf4/\xb9\xf8\x9e\xc0\xd3\xb9\x03\x83_\x14\x0bV\x0f$\xf6\x91\xa6\xc4<\x83:s\x8e\x1cJ\x84@N\xf0\xc46y\x11.\x05\x0fh\xa6\x97R\x04=}\xeb\xe6\xd6,\x96\xcf\xa4\x88wh\xfd\xf6+$\xc3I\x06\xb2\xd3#\x15\x80\xad$\xc1@\x1d\x04\xb6W\xe2I\xcb\x19\xf3\x13\xdf\xf2\xb7\xe7\x1c\x00\xc2\xb8 \xd6\xbc\xba\xbf@z2\xa52\xcb\x9e\xf9.\xf7\xe94\x8d\xea\x17m,i\xad\xeb\x84\xd0\x18\x88\x8c\xe9\x8f\x9e\xe4\x01\xfe\xf2\x0f\xf4\x82\xa6\xc2\x992\x06\x0f=\xb9Gm\xbaO[\xfe\x9f\r\xbd}&ic\x94\xe4\x8eRzUE\xa0\xdf\xc5\xcb2\xf3\xecN8b\x8ac\xe5\x9f\xbf\x0c\xc6\n\x95\xb6\xffV\xd4\\l\tOb\xf8\xc4\x0e\xb0\x9e\x12\x94\x9f\xcf\xd6\x19V\x81n\xc2\xde\x86\x1b1\x94T\xd8\xe9\xab#U1\x9c\xc16\xa0\xdb[\xd0\xd2b\xa5\xa6\xd4\xbfo`\x8b}\xe6\x82\x12\x97Ow\xb0\x07\xe2y\xd3|\xa0\xfd\x7f\xd8z\xa0\x16\x897\xfe\xe9l\xc6\xffE\x19\x10`\xe3\xf86\xe9\x87\x12\xa3\x97\x89\xf9\xd0+P\xa9\xcb\x94md\xaa,\x0bE7K\x98|\x90/n\xc0\xb1\xa5\x95\xb2\xb8\x04\x0b\xa9\tNC\xfd\xa9c\xc8\x10\x14\xb8\xa4:\xe0b(\xb5\xf2)\x97\xd6+\xc9q\x15s\x15\xf4\x11f\n\x0cy\x89\xc2D}N\xb6\xcc\xc8\xc7T\xa3\xe0\x7f\xbb\x07pB\xc9\xa0`=VM\r\x8c|B\xdd\xfd\x9d"\xa0\xd0N6\xa6|\xef\xfeZ\x14\x04+\x99\xb5\x9d-\xcc\x01#W9\x0c\xe5\x1f\xde \x13\xe82\xe5\x8a\x9a\xe2x\x99\x10a\xce\x06\xa8\xf8\x95H\xd1{\xa9\x9a\x8e\x08\xb7\x84v\xdf\xe1l3\xf2g\x1c\xd2y\x0b\xba\xfd\x1fy\xa5\xc8\xca\xfc_\x99\xc4>\t\xe7FW"\xf2\x902g(E\xb8\x88M3Wx\xb8\x8d\x92\x8f8\x13\xdc\x17\xb4x\x94\x9c\xeb\x06\x9d\xbb\x85\xe3\xd4?\'S\'H\xa6\xdd\x86\x12{\xc0D\xd6.\xcf\x03[a\x11sz9l\xf5\xdd\n \xf3S\xe6\x03\x9a\xef\xb6Q\x89M\xeb\x9b\xacp\xda\xd3\x1f\xc8\x01\xa2;\xba6\xf2\x9d\xe7\x937OJ\xcf\xd3\xe0&W\xfa"}Z\xa1p:\xebY\x93\x14\x01\x00kv\xb4\xaf\xf5#\x10\x94\xd3{\x16\x0f@\xde\xf6\xb62\x1fR\x16\xe1/LC\x9b\xf9A\xb4O\\\xc5O\x80/\xeb6\xc75\x91\xe8j0\xfa\xa5\xf2*i(\x99f\xa4\xa6\x8f\xecGm^\xa2\xc7\x9e\xbe\xda\xd4L\x97^"\xbc\xf2\xe9\xffwk>\xa2\x88i\x90NQ\x98\x14CX\x99\xa5\xfa V\xf8\x95\xa3\xfa\xb8j\x18]\xac\x99d\t\x105\xd88~\xa4\xc0b7@i\x8e\x16K\xa7\xf9\xd6\xe2\xafzISj\xb4\x03\x0ez\xd5\x0e\x1f\xf9\x86\x02?l\x0e\x84\xb0\x0e\xf9\x92\x8a`\x0f\xe5\xe0BX\xbb\xb5\xe3\r\xaf8\x08\xb9\xb1&\xdc\xed\xee\xa3\xe8\xf7\xac\xed\xcf\x111\xd6-\xe7\x9a\x91\x8f\x97\r\xc1\xfeh\x17\x87mh\x9c]\xbc\xc91\xf4\xb7\x80\xba\x02tY,\x89\x0eP\xa6\x1cl\x18[5fL&\x86R\xfb\xd8\x9f\xe9\xfe\xb71[\xde:\xd1\xfa\xbb\xf7\xa4\xe9\x804\xb3\\\xfd\x16\xfb\x82\xc4\xe3Q\xdb\xc3?z\xd6\x03\xb0\xb2{\x0fA\x8bV\xbf\xab\xb0\xf1\xd3)\xd3\xb0\xc8\x81\xf2P\xeb\xda\x9a\xd3\xef\\\xe2\x11\xed<<\x07\x80\xd7r\xe5\xb9\x9boR\xaf\xcb+\xe2|[I\xf6\t\x00;\x93\x92\xcfx]\x1b\xf8\x9a\x89U%\xbb\xb0\x19!\x8d33u\x07\x97}\xf0[\xb0\xf0yw\xab\xec\xe8I!\xad\xf9Ior1i\xfb[\xc0U\x10\xb2\x1fL\xd1\xd7\xa1\x0b\x04_\xaa\xf7c\x99/\x80#\x1b`\x9a\x9bC\x88\xb3\xc6l\x13\x94N\xbb6N8\xeb\x7fD\x87\x00nd\xa0\x93\xa0\x8f\x94\x12\xa0J\xb1\x1c\x92r`\xda\x0ew\xa1R,\xc7\xe7\'\xc0"\xc4\x05\xde$$\xc3i!\xc9\x977\xb1V/\xcc\x1d\xdc\xd8)p\x03\xb1\x89\xb7\xbd\r\xbe\xdfI}(s\xd4\xa2\x14\xa8\xdcX\x81>)\x9f\x08]*\xd18rm\xfb\xd1\x85\xee\x95N\x99|h!=\xdaqe\x13\x08\x16\x10G\x1f\x8bJ\xb58\x87\xf95\x10\x8b\x1b\x11#\x0b\xb3/q[W8B\xb2\xae\xc7y!\x0f\xb3\x88!\xda\xae\xe7\xd5m!\xd6\xee\x17\x11\x86W\xba\x9b\xb6\x1c\xfeib&==2)\xd8(\x80\xb7\x1b\xb7\xa3\xb1 \x81\xcc\xca\xce\x9c\x98\x1c9(\xcd\x87+c^\xce\xf1)\xd0U\xe4\xcdd\xe6VS\xcb\xdaer\x18\x97\x90\x1f\xc3+\'\xd4\xe5\xb5\xdd\xbb4\xdd@\x994e:\x83+\x83Ju\xc5\xb4G\x0f\x11\x99jC\x1c\x9e\xd9\x91\x17"\x83\xac\xcc\xb2\xae\xab\xee\xc0\xf1kV\xb1x\x8a\x01\xa3\x05\x9e\xf4\xd3O<\xe8\xec\xca\x93\x94\x87\xc8\xbeD\xc18\xb9[v\xed\x17\x9f\xb3\x03\xb4\xb0\x00\xaaED\xba{\xb2|\xa9;\xf7\xf73\x1d\xd6M\x06\xb0\x9dd3}\xd8\xd7w\x81\x00&\x10\xd6\xefP\xc1nS\xc6k\xc5\x87\xac\x1cnU\xd9\xe5h\xd7J"hM\xf0f0\xd5,\x13B\xadc\x931\x19\xad\x96\x9b\x88\x89\xf2s\xbb\xf15\x04Lwp\'\xc5\x98\xee\xa3\xa5$9P\xd6\xe6x\xaa\xb3\x08\xc7\xe6\xc0+\xef\x9a<|\x9a\x19\xbdq[\xa5"\xde\x84\x98\xa1\x99mi\x14\xb6\x96A\xb9\xf5\x15\x97\x84\xf9\\\x19\xd6\r\x0c\xc4\xec\xef\xb72\x05u\xb1?X\xb2\xba\x94E\xecH\xe7\xfaD>`\xf2\xa1\x07\xa0\x97D\xd4\xa7=\x13\x99\xd1\xbb\x0b\xac\x9e\xcf(\xb5\x0bB\xaal\x9a\xa1\x10QM\xa1\x8ah\xe0\x1c\x03-+X8\xd5\xac\xb16\x92\xefh\xb1\xaf\x93\x9f{`\xb9\x17*Pj\xc5\x83{;&\x8e\xac\x84c\xc7\tr\x11\x8c\xf7J\x9d}&\x80\xffu\x1e9UA\x03f\xf8\x85\xc1\xc6\xa6v\x843\x0b\x13o\xc3\xd6\xcbv\x8c\x93U\xeb\x11\xed\xd4\xe3\xb3\xaf<\xee\xb63\xff\x98_\xa7\xfd\xf1\xb9{\xbc\x06\x81*,*\xd9\x0f\x02r\xdb\x1a5\x1ec\xc0X\xc1\x0epnXq\xc2F\xb6\\\xde\x95\x94\x04)\x17\xbd9\xa6\xa6t\xd2\x07\xd2\x94\xbduZln\xb9i\xc6\xef\xe1r\x8b\xc5\x1c\x0c\xe6Y\xb6\xb6J\xf4\xa5Q\xbd\x87\x82\x88\xd0v\xa6\xddB\x84\x0ec?\xad\x0e\xdc\r`z\xa5\x82\x180\xd2)G\x8b\xa4\xae3\xa7+E\xd9S\xaerK\xb2\xdfd\xfd\x1fg\xb4\xd21\xd6g\xae(;\xe9\xe5\xd9\x1bw\x9a\xd4j\xc61\xfc\xe6\xbfM,\xc7$+E\xbd\xc6{\x1f\xf5jAX\xfd(\xc37\xd0E\xb5\xa3A\xe1Ox\xc9Ee-\x06\x02P[\x85\xb8|\r(\xdb\xee\x84s\xf0\x8d\xd5\x0c\xce\x07\xc9\xe9\x8eZ\x82\xca\x12\xc3:\x8dA\xfe\xd6$\xbbA(\xe3\x05n\xa7A#\xf9H\xf7Da\x16\x9c#$+\xad\xfe\xf4\xefl\xf8(oY\xa8\xb92\xcfpp\x0c\xb6(\xab\xef\xbdc\x85mE\x1c\x97\x825D\x1a\x8b\xe2P|i \xf0\xf6&\x85\xddjbF)\xc8\x80\x1c#\xb1\xc8\x9d\xa9\xe8\xee\xfb\x1f#\xdeY\xd9\xd8\xaa\x18e\xb2\xe50Y=\x08\xcd\xae\x89\x99\xe4\xa2@\xa7I<\xd4\xe6<}\xcc\xd2\xf1|\xcddX\x9e\xe5aA)(O\xc4\xf0\xf4#J\x06:\xd2\r-\xf3\xec\x9d\xa6E\xdc\xf6R)\xb1\x18\xea\xb2\xcb\xb7~`\xc4(o\xaf\xc36\xfd)\xf6!$\xa8\xf1\x81\x8ejE\xf5AaeY\xfe\xd9\xec\xfaw\xac\xf2C""P]C,9\x1a\x9a]t#[\x01\xac\xbd(\xa1\xcb\xcb=x&\xe8\xd7\x0e\x00\xa6\xda2\xaf\x1a\xd7\xdb\xf2\xc5V&\xb9\xb8\x07\x07I\x92\xa6\xac\x05\xd00\xc6\xfb\xd0*\x92\xf6\x00\xbbc\x9e\x85;\x0f\x82:\xc1\x17\xb4|L\x82\xf7^\xdd\xc9\x03u\x9c\x1d\xa3\xdbGC\xbd4\xebhJ\x87\x17\x93\xb0\x0e\x81\x94\x8d[-X\x16Y\x8dcl\x08\xd9\xa9\x0c\xa6\xa2\x89\x19\xe1\xa0}\x84*`\xd9[\x1f}1y\xf3v\xb6\x0b\xae\x19\x86x\xebI\x02\r\xd87D\xe1\x86\x80K\xb9\xda6\xdcv\x11\xe9\xff\x8c\\\xce\xdd\xe2\xbc\xf5\xf8\xfa\x8f:\x8dqI\xed&\xb7\xd2\x8dR~\xbb\xa4\xb5\n\x96i\xcfo\x07w\xc9G\x90\x0b\x88\xe6\xb5\xff\xf3\x11\xd8>]s\xb7\x96\xedE~\xd3$e\x86\x03Z\x06\xfc\xe4xE\xad\x841\xd6\x02]\x7f\x95\xfb\x041\xec`\xfb\x86W\x170\xc2\x1a\x01f\xe2\xb4:\x9c\xb2\x89Y\xfa\xc0O\x02\xfe.N\x7f<\xd7\xc7r$\xf6\xf2\xd2J\xf9W\x1e\x00\r\x05\xf9\xc3\xa7\x87f\x0fPj\x1fl\xfd\xfap\xa2\xf1\\R#\x9b\xf2\xf8{\xa9we\xb3d\xfc\xc8\xa3\x07\xe3H9\x94\x04\xc37\x19d\xf2\xab\xe4\x90\x0bZ\xe7\x178\xc4h\xf2\xd5\xd1B\xa5\x82\xc0\xdamM\x904l\xa6&\xd5\x84\xc2\x15\x85\'\x9eq\xe8\xf4\xd1\xe1\x9e\x8a1_Uw\x88\x8f\xab+\\\x90\x7f\xa1\nS\x18\xe0\x8f\xb5;\xf7\xfd\xfb\x89\nu\xbf\xdd\xdfF\xb9\x02\xf1[x]\x97\xc2B\x91\xd4Y\xa5\x105\xfa$%d\xcb\xea?;j\x9fAO\x8d\xbeM\x8b"\xb4;\xb9@\xf6#[0\x063\xc1\xbc\xa3\x82\x11oI\xe9\x10\x10\x90GN\xa6\xd68\xf0\x8d\x81#!V,\x15?\xacA\x8b\xbeS\n\x9f\\\xbc~\xee\xcbT*Fg_\x95\xfe\x8d\xd3G\xc7\x9e\x94~\x04\x8fm_\x06\xdb4\\\xb9 \xb4//\x86\x86B\x92\xc2\xf4\xba\xa1\xa2\xb0\xa3\xf3q\x1d\x86\xa0\r>\xc9/\xa3\x9b\xe0\xeb\xdd\xa23\xb9\xd9\xe2\x10V\xb8\xb7Yys\x19o6\x86\xc2\xd9i\xd7rm\xea\xfd\xf9\x91a\x9f\x9d\x9fD\xe66\x18\x89.2\x80\r\x85\xc3\xda5\x13\x06I**\x81gg\xb7\x08\x1b{\x03>\x8f\xc5O\xc3,\xe7-\xd0\xe2\xd7=I\x8c\xa2\xf0\xdf5\xf4\xc5\xe2[o\x0e\x7f\x9d\xa5\x01\\j4O\x13\xb9\xc4Y\xd5_Y\xcb\xc5R\xa4\x08*}\xb6\x98\x92B\xf1\xb6T0\x0eQO\xae\xfb\xcc\xc2C\x0e\x98;\xb8d\xc9a\xdf\x93oDs\x07\x12\r\x89\xd4A]\xda7#[\xbeN\xfc6\xad\x0b\x8b#[\xbc\xa2\xe7:\xf7K0\xd5\xdb\xaeBB\xfed\xc3\xb3\xf6+z\xe8\xc4\x88\xf9O\x98\xab\xe1\xc7f \x05\x1e\xd4\xe2d\xebTk\xe6\x9c\xfah0\xf0\xa5\xef\xc2/\x98&#x\xc1\xdd~\xbdNx\r.\xe4\x1a\xd9\x02,\x1a\x0c\xad\x1a~\xd6^\x01\xde\xd2\xb0\xc9\xdai\x01\xf3\xc3\x16\xa1\xa3\xa9\xfe\xade\x98\xf9J\x80\xb0n\xc6\x9c]\x82|\x17x\x9b?,(\x12\xd2\xcb\xdaq\x98\xb7!\xfc\xb1\xc6HN\x80\xe0\xd8\xd1\xed*\x06K\xd4\x96\x92\x14\xc8\xed\xa7\xca\xc4-\xb2\xa2\xe4I5\x9d\x18\x1f\x03E\xed"\x05W\xbd\xf8\xf6\x05\xd4\xecX\x0f\xa1\x1f\xeb\xe4b\xf0\x87\x85\t]\r\xa4\xd5\xc6\x88"\x9e\xd0\xd7\xe3\xcb\xa7.\x95\xc7\x06\xf2\xedM\xe7HC\x8a\xe6eIv-\xfb\xb7\xde\xf9URI\x81;]\xd6t\x19\xf8E\xf4[^\xb4\xec\xc7\xb8B\xaas!0\x04\xc8\x8a\x12\x1bI\x1b\x01\xbb\xdf\xec\x8e\'\xaer\x97\x17\xe7\x87\x07\xdd\xc0\x12\xfe\x8d\xcd\x7f7F\r\xf3@Z\xa5h\xd5\xed\xaa\xf2t\xb7\x85\x96\xd73u\xfcGU"J]Q{\xdb\xa7\xcd(e\x01\xafp\xd2CT-\x17\xaa;\xd5b\x98L\x879\x1c\xbb\xc1,\x00\xfa\x17\xc2\x1bx$,X/\xae\x91 \x87\xf5\x80\x83\n\xed\xe7b\x9d\x0c\x86\xed\xb6\x96JO\xa7\x93g)!\xbclz\x8c\x01\xea\xfd\x00\xfd\xf8\xf5\xf2\x18c\x83\xa8@\xca6\x9a\xc3\xe9\x7f_y\x11\x87&\xc2\xc8\x1f$4\xa6\xa3\xba\xf6\x94\xe3\x90:\xdb\x982\xdf98\xa8\x03\x05x\xfe\xd7!i\x18\xd6\rV\x06\x1a\xf0\xbe\xec\xfd\x82\x89ATh{\xa8\x85\x0bC8\x97\xd8\x1e\x9f_i\xd4\xfd\xfe\x1bV\x8f-sN)\xde\xc40\x99Aq\xfd\xa8xX\xc9M\x14jb\xc8\xf3\x0b>\xad\xc6-\xba\x7f\xbd#\xdc\xb4}1\x18\x9b\xf4\xc1\x90\xd1!\xd1VT\xcbl\x01\xe0\x9b\xe8t\x16D\xb4\x99YV\xe0\xf4\xdaK\xda\x9c\xdd\xeb\x10\xcb\xe7g\xdf\xc6\xa6\xca\x83\x87\x83\xa2\xae\xea\x1d\xf7\xbdgm/9r\x83\xe9^p\x04\x855j\x91@\xd2\xc4\xb5\xb4\xcc\x8f\x02\x98\xb1\xd1\xfc\xc9\x87\xf8\xd4\xc1\xb3\x11\x10\xf1\x0ckF{\x94\xcc\xbb\x15\x16\xceu\x9e#\xfd@\xdf\xe4\x80k\xbf3G6\xa0H\xb2\xeb\xcc\xd4\x04\xbc\x02\xeb\xdas.\xa4G\x82I\xff\x91v<\x1f\r\xf8\xfa\xdc\xcd\xca@\xd5\x9f\x86\x1f\xb4\xb1C\n\n\xb5}ax\x16\xf4\xae\xf0\xd2\x8c\x8d\xd1\x7fNy|\x1f\xf7\xeeIT\x10\x91\xcbXcX\xbd&\xf7\xd1!$\x14\n\x89\x06uE\x04\x99S\xab7\x97\xc7\xa7\x11\'-%%u~\xdc>\x90N\xa1\x7f,5\x8e\xfb\xca\xa9\x9bA\xd8\xc6F\x9c\xcd\x9c1\xa3\x85|\x0b\t\x92\x1a\x9c\x1e\x12\xc8\xad\xec`\xb415rkO~\xc8\xbe\xd9\x8b\x91\xbc\x0f\x1ceb26\xc7\x06\x9ds\xf7~\xb2\xd6B \xae\xf1@3\xf3\x9c\xe2\xb1w3\xac\xdehw\xbb\xe0\x10(M\xa4A"\xa4\xa2!\xe2\xcc\x00q)4*\xb9\x8ec\xc5@\xe6\x049@\xf2V\x14\xca0}\xdbB\xce\x98\x8f\xebY\x03z"g\xe0t*9\xb1\x99!t\xde\x03\x057\xc6g\xf2W=\xd8A\xe3\xbag\xa0\x17@\xa5\xecf*\xeb\xe5\xe0?!^\xf2\x9d\xd7:\x9e\x94Y@\x96\xac\xeb\xbb\x16\x19\xc9z\xc0\xb4Z3Hc\x93\x8a\x0c+\x04\x06\x87\xce\xcf\x10\xd5\xa3I\xb0\xa1\x84\xde\xc8.\xd7\xcd\xc4\xa9\x84Z\x87\xaeY\xf6_:\x13J\x9f3\x02S\xd3\xd4\xd4\xf4\'\x1f\x90\x8b(fU\x18\x9c\xef(\x9c\xf5\x0b\xfd\xd0\xe4\x0f&\xb8eJ\x12O;\xea\x08 ]\xcf\xd8\xb5\xc6\xfa0\xa0b\xb5\x85pe$*\xa6\xf6\x11\xceJ\x1fm\xbc\x9d\x84\x8a\x83\xfaA{\x9fng\x95]\xa8\x9c\xfa5WT\x8a\xa1\xfdN\x9c\xa0,\x90\xfc7\xb4\xf1H\x04M\x0b\xabV\xbb\xcb\xd0\xf5\x1f\xf9\xcf3\x02L\x82`8h+\xde\xbd.=c\x1a\xb5\xdd\x83\x85\xb6\xe2c\xb3N\xc4\x89$\xee\xc5\xf4\x84\xb0:\x84\xc1\xd0A\xde\xed\x1ah[\xa3\xce.\xfep\x0bjy{f\xb4?%H\x96u\x8bz\x1c\xe9\xd9\xc0\x1a\xf1\x1cx\x1e\xf4\x15\x9a~\xb2\xf8i\x85^\x8ea\xe8\xbca\x1f\x9a\xe0I\xecA5\xbf\x9b\x19#M\xf6JN{\xeb\xa4\xb4b\xc1\xb9\xfd\x17P/\xe3,N/\xc7W\xf6"\x8d\xd6\xf3\xd9\'\xbcf\n\x17\xf98\xb4\xf3@\xad\'\xe1\xbe\x8c\xb2R\xedu\xca\x9d\xe4iR\xc4V\xfc_Q\x8b.M]i\xd1\xb2\xcf\x15$\xb1\x80\x0bK7m\xfa\xdbE\xf6\x7f\xf3\xcb_4\xdc\x05\x9e\xe9N\xdc\x7f\xf9m\x066\xa7\x94X\xea~(\x84}\xdeSoG+\x87\xaa\x9e/W;/\xac\xf5\xf6\xaa\x03\xad-(\xf6\xcc\xa5X\xf6\x9e\xca\x8d\x13\xd2\xdd\xea\x1a\xe6\xf5\xa2\xf0\x8a\x92%X3\xb3\x953.\xc1\xfb=\xc5F\x0f\x13\x9e\xff\xd7O\xe7Y\xe5%\x88\x93`\x91\x16\x87\x10\xe7\x9a5C\xe6\xe9\x1a\xaf\x17\x17\x8c\xfb\xce\xce\x0c\x0bm\xde0w&\xdcl\xf2\xbb4Di\x00\x97b\xba\xf8\xf5TOm\xd6\xaf;\xe8/\xbd\r\x82\x8d\xa1f\xeb\x8eX\x94b\xa5\xd4\x8cmZ\xa4LL\x8eC\x0b\x06\xfa\xe4]\x1a\xdc\xb0\xe9\xc2\x15\xae~9\xca\xeb\x88\xcbg*\tF.\x8f)}\xa4\xd3\xf3\xaag-\xb2Ae\x14\xbdp\xda\x9c46\x12#\xe0"}w\x9d\x08K\x907\x91\x11\\\xf8\x84\xe5\x13\'G+\x82E\x0b\xf9\xab\xac2\xa1\xba&\xd2z\x89n\x9fR\xedb\x0f\x92D\xe7\x11\xc6\x1dA\xdc\xd6\x9d\x130\xcb\xf1A,\xbb=\xd5\x948\xbclsI\xccG\x98\xcc\x02\xcah\x1a\xdf\x12\xf5j\xd3i[\x95S\x85P\x13b\xd0I\xdbg\xbf\xf3o2wt\xa7\x9bH[A&\xe3M\xf1.A\x97Ww\x12\x9d\x01`\x07\x8eL\xb4#\x8d*`z\x86\xb0s*\x0c\xcfm\x02N\xf9\x00^c\xab[.\xeb\xcd\x104b\nJ\x82a\x89\xefU\xbe,\xaf\xe6\x11F\xf5&\xc5^\x99\rGC\x16K1\x91\xc4Y\x8b\x88_\xa8\xcd\x11\x8b\x8b\x08\xc3MW"\xf2\x91S\x11\xb3\x8d\xb9\x80\xa9\r\'\x95\x1e\xbc\xa4"m\x04)8\xd06\x98z\n\xe2\xa6\xca\xc7\xd4\xb4\x06)\x00#F\x1ddc\x86\x1f5\t\x15\xa6\x1f\xc9B\xbcA\tj)\x1b\xce\xb8\x82Ji\x03\xd7\x16\x87\x03\xd6\xbf\xbc\xccU\x8a\xb8o\xb7\xa4J\xd0\x0b!)\xaf:\x91\x99\x1d^\xbe_\x1a\x96\xfc]\xd4jL_\xd8\xd4W\x05\xb9\xc3\xfcS\xab\xaa\xec\xba\xb8J\xf7\xa2\xe0`}>TI\xcc\x8c\xf6\x8aT\xf8\xbe[\x06\x92\xfc\xc9\xcc\x92\xebU\x1a\xa2\x11K\'\x81\'\x97\xdc\n\xcd:U\x9e/$\xcb\x94\xc5>\x89^\xf5 _\xc3M\x86\xd5M\x1dB\x80\x96-\xcb\xd55\x1dB{\xc6\x0b\xfd+\xe3]s\x189\xef\xdd\x97Il\xce\\\xd1^\xe3\x8d\x06\xa3\xfd\xa4\xc2=_r \x8d\x80\xe0\xe1\x966[|\x8b\xfb\x94B\'\xa8:\x1fsd\xc1\x15\x8b,\xfe\xae\xces\xb5\x12<\xb9\xb3\xd0_OOhx&\xa9C\xf1\x13\xd7\x07\t\xf9\xdcl\xe1\x81M\xacm\xd9?\t\xbb\xb4K\xaa\xb5\x89\x9c\xb3\x05\r\xc0a\xf0S3F\xe5\xf0:w\x84\x8e\x0bZ:\xa4\xf7l\xb2\x16\xfc\xf4j\xf8\xad0q\xe76\xf9sk\x8bNX\x94\x05\xf3\x1d\xf9\xd9Z\x1c\t\x89\xb1\xfa\xff\xcdOW\xe9\xd13\x17\xc2N\x96\x98O]t\xb1\xbb4\xbc\xebH\x7f\x03\n\xe5J\xe9d\x981M\xf8\x98\x92O\xf5\xd2\x7f\xf5\xf4a\xe5\xd8\xdfI\xb2\x17s\xcf\x86\x1bzy|B\xbc@\xc0\xf0\xa7\xfeh\x94\xe3k\xde\xac\x18N\xb6\xb5\x9ek>\x81\xd4\xc5-lY\x81\x94\xf4\x18uolM)\x10\xd7\xf9\x9b\xe3\xc8\xf2?H<\x9c\xa5U\x8dM\x0f_\x06u\x95\x9f\xa2\xdf&\xf1\xb1\x7f\xf5\xcd\xb7\xbc\xbe\xe5\xb1\xcf\x9bL6\xb0\xcdtK\x197N\xf7\x17\x9b1z\x94\xcf>\xb3\xec\x16n\x90\xd1\x7f\xec\x9ea\xc6\x99o\xb6Q\xbd\xae\xc83\xc71\x9a#6\x05{Zo)\xe7\xae\xa2uI\xa1o\xb8.\x94\x9b\xed\x12\x96\xf3y\xe1E\x9bX\x8b\x00LX+\xb3\x8f\xb29bwG\xa3>\xc3b\xe9\x02)\xed\xc4)\xf3]\x8b\x0cj\xca\t\xe0E\x93C\x8f[\x91\x1e\x7f\xfek\x90WP/B\xca4\x94\xe1\xdb\xc5\x17\xb9q\xdd\xb2\x07t\x95\x08\x1c*a}\xab\x18\x98<\x9a\xdc\xf4GI\xf4\x9fn\xe6\xb9\\3ig\xa8q\x19\xebm\xfe\x11@\xdc\xa6\xc4g\xbae\xbdmj\x1c!\xba\x992\xbb\x99f\xdb\x19\xd4\x1c\x1b\xed\xc9=\xc0\x9c\xa0u\x12r|w,\xf9\xf5o\x03t\xacnt\x90S\x1ct\x8e7p\xa5.\x1c\xa9\x17V\x9ap\x800\x08sZ!@\x16\x19\x94\xe5PP\x80\xf7V\xa8\x18\xdb\xcdg\x85\x94y\xae|\xc9G\xbfF;\xad!\x88\x8a\x8e\nb\xd6\xa1\x9b\x00T\xaa\xf2\xfbX\xf2\xbc\xea\xe4,\xe0\x9a\xe43\xb8\xb9\x9au\xaa\xc2~B\x9b\xc7\xcb\x959o\xde\x8b?\xb60\xa4\xfd\xb8K\x0f`\xa0\x03\xdf t\xbf\xbf\x0b\xc4(\xc6\xd3\x9b\xa1\xf0\xd6\xe6\xfe\x01D\x10}\xd2\xf0^}"\xf2\xa4\x95\n/\xad\xa2P*\xa5)c\x87}\xd3\x1f\x8f\xbc\xcc|"\xd8\xea\xecAy\xd5\x13\x1ca\xb9R\x86\xc4\xa0\x9e\xb9\xb1\xd0t\xfa\xa6\xb2#\\\x05\xe1\xeb\xa4\xb3\x94\xa6\xa7\xdb\x06\n\x98\xa8\x94\x19e\x17\xb3x\x08\xa2\x85oL0|s\xb3\x81\xe4\x96\xcf\xde2\x95\x9e#\xde\ty^C~\xae\n\x7f-8\xfa\xa9;\xa5\x95\x06\x9d\xef\xe8\xadkck\xa9/\x90FH\x9a\xdcF\x06l\xcc\xae\n\xfaO\x12-/#U\xe3\x85r\xf7\xa0\x99\xd2]\xae\xf3E\x0b\x91\xd8\xfd\xa1\x11\'\xff)n]#\x91\x8e\x93,\xab\xf2\x83\x05y\xc8\n\xabi\xdd\x06\xa3\xa4}\xcc\xc1\xd2\xed\xdbP~I\xde\x18q\x01\xd8\xca\x01\xa1\xf9\xa2\x1c\x8c\xfe\r=\xac\x9b4\xe3"\xb3|\xa8i\xd9\xc3c0\xe8I\x97\xa2\xa4\x85\xd90\xdd\xd8\xdfCI\x958\x0b\x0eB\xb1\xc4\xbf5\xa6\xf7\xf1\x12;\xa2\xc1\xcf\xca\x84\xa3^.\xed\xd6\xb3\x1a.\x87\x9b_\xc2\xd4\x95\xcb\x18\n\xb8;\xc5\xbcF\xae\xfbr`\xe4\xa2\xec\x84\x82\x1e\x15\xa9\xdf\xb4i%\xbd\x19\x85H\xfe\x15\xb4D\xd3/\xa0\xf7\xcb\xe1U\xc8\xf5\xaf5N\t\xfe\x8cI\xd7\x8au\x94eh\xab\xdafCS\xce\x947\xd0\xab\x97\xe2@O\x02\x9c\x8d/\xd7\xb8\xc8%\x868i\n\xa8\xd5q\xf8\x8c\x98U\xb9\xa1V\'=\xf4la\x91\xc1:a\xa44_\xbc\x95\xf8\x8ds\x06H(\xc3\xbd\x10\xdb\'\xa5\xfdK\x04\xdc$k\x86>p\x1e\x04\xa7\xe2\x98\x08\xad\xa4\x93\xa2T\x9b\x92?v\x06\xcbO\xc6\xd8u\x99\xe1\xbd\xb2\xb3\xea\x15\x0f\xf9\xb2\xb5?\xd3\r\xa1lIY\xf8\xe53Fm\x85\xa9\x15\'\x83K\xc3\x16gu\xb0U\x997Ap\r\xa9\xb8\xc5f\x9a{\xdd\xb8\xa3\xceH5\xd8\xc8\x9a\x85s\x1c\xd5<\xfd|\xed\x0e\t\r`\xc5?\xbc\xb0\xfe\xd0|\xadG\xda\x10\xb1\x10\xab)\xa0\x93w9)\xb1t1\x8a6\x93B\xcbq*\xe1\xdbG\xcej\xed\x1c\x89{q\xadN\xe3g9\xdab\xdeV\x98Re\xd3}<\xfd(!W,\xd5\xbf\xa0\xb4\xf7\x11\x9f\x16B%q\xad\x10\x88\xb1\xd6\xc7\xf0z\x97\xa5\x02T\xe0,t6\xae\x12\xd5\x9c\xc5]\xdb\x18\xce\x8d\x02\x17\xe1\xbe\x8b\x17w\x0c\xea\x8d{\xa8\xc5KZ\xf1\x96\xfck\xfb\x9e\xf4\x89\xee\xf9\x11\xceP\xe9\x91E\xef\xef\xa0;Y\xdfXs>\xa6\x0c;\xdd\x80F\xb9nRv]\xa4\xf4t\xbe\x18\n\xb1\xde\xb0\x86N\x14\xf6\x8d\x87\xb5%\x01\x191\tIS\xa6<\xa0\xc62\xd5v\xa9\x06/\x06X\xd3B9<[\xf5M\x88+\x10\xc3\x1b\x90D\x86\xeb\x83\xe0\xdc\x9ee\xd1\xb8k*\xdf\x89\x89E\xd1\xaa\x19\x1cNyUzC\xf7zG@G\xbboJ\xe1N\xaaW\xe4\xd7\xa7\xbc\xaaT\xd8\xd2\xcc\xbc\xbb\x1e5\x7f\xa8\xdf\x95\xd0\xa6\xca\x10\xe2\xa1\xc73z\xdaN\xcb\xfd\tC\x90\xeb\x06\xfa\xd0LF\xc2*\xbc:\x0c\xd9\x98^\xcb/\xe7\x8bPU!6f\x9dQF%\xd9\xffG\xd9\xb9\xda\xb3\xe5P*\x00\x82\xe6-\x06\x82\x9dz\xcfY\xb4U|)\x9c\xe9\xd1\xa1I\xd84\xaa:\x0f\xd5\xde\xe8\xcb\n\x8bs\\\xf9\x9f$\x0ed|rp>\xe5Y\xdb\x9c\xe6\xe1\n\xb3\xb7\xdbN\xab\xa7\x98G\xef\xa4\xdd\x1f*&\x95\xab#O\x8fl$ab\xc7\x81)\xf4\x98\x95j{\x15\xe79\xfc5\x00\xb9\x97/l\xf4X6\xd3\xeaQ\x05vx\xbf\x9c\xb7\x81?\xf6nO\xee\xa6n$\x0c1B9\xf0\x8ef6\xd1\xe6}\xcf\x01\xf8\x18\xcb\x7f!\xa9>\xf7\x1a\xa5\xba\xee\xb9j\xd3r\x99\xe5e\x19P\xb1p\x8a\x7fx\xc3$\x16\xe4\x97\xd6\x87\xee/Mu\x0e\x8d\xe7\t?\x08\xf6Ce\x8f\xd0\x0f\xb5\xea\x94-$\'\x152c\xa6\xbaA\xa6\x8b\x08\xb7\xa3\xe2\xf9\xf0\xa6\x87\xa5\x025{;\x08\xdf\x15\x14m\x97\xac\x05\xcc\xd0f\xec\xfb\x93\xdb\xf6B\xd4r\xadJ:\x1d\xd8\xdc\\\x7fW\xfa\xae\xdb\xc8\xa9B|\xf9\x9d\xb3(\x9c\xa4\xa9YC\xa1\xa6\x1c\xa0|\xcc76\xba25\xd7\xb1\xa2\xf4\x0c\x8bp/\xec6Y\x046\x0e\xb1\xe0=\xa4\xdf\x1b\x80\xe4\x90{\x1f\xe8\x8b\xf6\x1a\x88P62iWzw\x8a\xbc\x9b\x96\xda/\xd2!\x95R\xcd\xd6^\x904d\x8d\x96\xa7\x99y\xf5\x18\xf9\xd5\xa4&\xdc[{4\x8f\x97\xfd\x86\x1f\xc0y\xdd%n\xaeOK]\xcd\xbcy\xde&\x93\x91\x10\x8f\xe3\xf3\x07\xf6\x1eg\xca}\x87\xefs\x1c6\xcf+\xb4\xdev\xdb\xb9k\xd0\x07+\x9aSp\xd1\xa3\xed?\xeb\x96b\xde*}\x97\x88\xfc\x10\xd2\xcb\xd9^\xfa\xf7\x1f\x85\xe8~\xc7$\x16w\xa7\xc0y%\xb0\xde[7\xca)\x80\x87\xa5,\x98\xf6d\x8b\x12d\x93B\nL\xfb\xe1\x92\x1e\x97"@7A\xdb\xfc\x10\xe3\\\xf4\xa0\xd7\xde\xb4\xc9c\xaf\xcd\x82\x1d5E\x8f\xf4\xa6\x01\xf9\xd1\xf3\x16e\xa5s\xe9b\xe6\xf4\xefn\x05{\xa5\xbcg\xf3>\xb1\xf0)\x81z\xe6j\xb1]d\x9f\x8f\xa8hO\xa7)E\x9a\xce\xd0h\x8a] \x95\xf7\x84\xe8\x87;\xcc\x0c\xf6\x8bd\xe6\xa6\x97\x96\x96\x89\xa8\xfe\xa1\x8a\x97R4{\xa2\t\x98\x01\x86\xcaAlH\x9b\xe9\xff\xc3\xa2x\x16\xb1\xf7\xea\x8a\x85\x1f\xf8u\x17\x96\x02\xf2\xe2U[[G@\x88B\xb3\x13k\x03%\xe4}\xc9\x17!\xa7\x19\xc4n\xab>\xc2\xc2\x04?Z\x13\xcaXe\xbc\'\x06\x84\xcaBX\x872\xfd\x96\xfa\x17\xf2\xa7\xfa\x8c<\xdaH\xcb\x1b\x05;$\xef\xbf\xdc\xa8\xfat\x80\n0\x00\xcf\x83\xcf\x13\xdf\xebP\'\xc3\xcd+LgNl\x1c\x13\xa1\x93o#\x98bx&\x88\x07[\xaa\xd2\xacm\x99\x86\xed\x10.#\xaa\xd3\xeb=\\)\x01\x91j\x94\xbf\x10\xc7\x9e\x98X\x17\x8d\x88\xad\xb88\x9a\xf7\xdf\xea*R\xf8\xb8\xe4uI\x8c\x97\xfa\xcc\xa1\x98\x14\xb1\x8c\xb6!1"r<\x13{RD<9\xcd\xab\'\x81E\x91xj\xbb\xa2\xa3,\xde\xec&\x1a\x86\xddP\xbf)\x8bWw\x10x\xa9S\xaf\x032`=\x89\xb4\xa3X\xce5\x9e\xb1\xff\x02\x00R\xcf4\xcaI\x93\x17+\xcfa') diff --git a/dist/pyarmor_runtime_000000/__init__.py b/dist/pyarmor_runtime_000000/__init__.py new file mode 100644 index 0000000..0e6ae89 --- /dev/null +++ b/dist/pyarmor_runtime_000000/__init__.py @@ -0,0 +1,2 @@ +# Pyarmor 9.2.3 (trial), 000000, 2026-01-22T02:38:28.345417 +from .pyarmor_runtime import __pyarmor__ diff --git a/douyin_srs_ffplay.py b/douyin_srs_ffplay.py new file mode 100644 index 0000000..214c187 --- /dev/null +++ b/douyin_srs_ffplay.py @@ -0,0 +1,230 @@ +# douyin_srs_ffplay.py + +import queue +import unicodedata +import traceback +import subprocess +import threading +import re +import time +import datetime + +class StreamEnded(Exception): + pass + +def sanitize_title(text: str, max_len: int = 120) -> str: + 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) + return text.strip()[:max_len] + +def _stderr_reader_thread(proc, q): + try: + while True: + line = proc.stderr.readline() + if not line: + break + q.put(line) + except: + pass + finally: + try: + q.put(None) + except: + 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 start_douyin_srs_ffplay( + title, + anchor_name, + real_url, + rec_info, + record_name, + recording, + recording_time_list, + running_list, + monitoring, + clear_record_info, + color_obj +): + # ====================== 配置参数 ====================== + SRS_RTMP_URL = "rtmp://127.0.0.1/live/douyin" + SRS_PLAY_URL = "http://127.0.0.1:8080/live/douyin.flv" + + MAX_RETRY_DELAY = 120 + INITIAL_RETRY_DELAY = 3 + NO_FRAME_TIMEOUT = 180 + STALE_OUTPUT_SECONDS = 180 + FFPLAY_RESTART_INTERVAL = 3600 * 6 + + 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) + 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}") + + 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", + "-c:a", "copy", + "-f", "flv", SRS_RTMP_URL + ] + + # ====================== ffplay 最佳参数(低CPU + 零漂移 + 全屏) ====================== + ffplay_cmd = [ + "ffplay", + "-fflags", "nobuffer+genpts", + "-flags", "low_delay", + "-framedrop", + "-sync", "ext", + "-vf", "transpose=2", + "-autoexit", + "-fs", + "-loglevel", "quiet", + "-i", SRS_PLAY_URL + ] + + ffmpeg_proc = None + ffplay_proc = None + stderr_q = queue.Queue() + reader_t = None + retry_delay = INITIAL_RETRY_DELAY + last_ffplay_restart = time.time() + + try: + while True: + current_time = time.time() + + if ffplay_proc and (current_time - last_ffplay_restart > FFPLAY_RESTART_INTERVAL): + print("[SCHEDULE] 6小时预防性重启 ffplay") + cleanup_proc(ffplay_proc, "ffplay") + ffplay_proc = None + + 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}] 启动 ffplay 播放 SRS 原画流") + ffplay_proc = subprocess.Popen( + ffplay_cmd, + stdin=subprocess.DEVNULL, + stdout=subprocess.DEVNULL, + stderr=subprocess.PIPE, + text=True + ) + time.sleep(2) + last_ffplay_restart = 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) + + cleanup_proc(ffmpeg_proc, "FFmpeg") + + 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_q = queue.Queue() + reader_t = threading.Thread(target=_stderr_reader_thread, args=(ffmpeg_proc, stderr_q)) + reader_t.daemon = True + reader_t.start() + + last_frame_time = time.time() + + 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 is None: + break + + line = line.strip() + if not line: + continue + + ffmpeg_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] 检测到下播关键字,停止转播") + raise StreamEnded() + + if re.search(r'frame=\s*\d+', line_lower): + last_frame_time = time.time() + retry_delay = INITIAL_RETRY_DELAY + + if time.time() - last_frame_time > NO_FRAME_TIMEOUT: + print(f"[INFO] {NO_FRAME_TIMEOUT}s 无新帧,判定下播") + raise StreamEnded() + + 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") + try: + recording.discard(record_name) + recording_time_list.pop(record_name, None) + if record_name in running_list: + running_list.remove(record_name) + # global monitoring + # monitoring = max(0, monitoring - 1) + monitoring[0] = max(0, monitoring[0] - 1) # 修改为列表元素 + clear_record_info(record_name, real_url) + except: + pass + color_obj.print_colored(f"[{record_name}] SRS → ffplay HDMI 转播已彻底停止并清理完毕\n", color_obj.GREEN) \ No newline at end of file diff --git a/youtubeKey b/youtubeKey deleted file mode 100644 index 587bb38..0000000 --- a/youtubeKey +++ /dev/null @@ -1,36 +0,0 @@ -ue78-1c3e-mr9g-14mz-9r4z # 没有ypp的美食 -qxvb-r47b-r5ju-6ud3-6k7z # vlog频道 -x04z-564w-aks7-embw-30y4 #美食ypp - - -; https://live.douyin.com/745606325880,主播: w、(摆摊日记_) -; https://live.douyin.com/675343045974,主播: 张子沐的串串(你的电子榨菜) -; https://live.douyin.com/673565298571,主播: 轻舟已撞大冰山(摆摊vlog) -; https://live.douyin.com/835571459859,主播: 海绵饱饱 -; https://live.douyin.com/152358755212,主播: 大美_红烧肉 -; https://live.douyin.com/990825651731,主播: 王同学摆摊日记(蜜汁烤鸡腿) -; https://live.douyin.com/591442402624,主播: 76696515302 -; https://live.douyin.com/967381081829,主播: 憨憨的摆摊日记 -; https://live.douyin.com/78012762575,主播: 摆摊儿去划水 -; https://live.douyin.com/163813589919,主播: 不要熬夜(摆摊中) -; https://live.douyin.com/498368994814,主播: 剪头发的小帆帆 -; https://live.douyin.com/617848099204,主播: 果妈要努力 -; https://live.douyin.com/821525628944,主播: 李熙雨⭐ -; https://live.douyin.com/993102287144,主播: 小茗早餐(薪笼记助创官) -; https://live.douyin.com/701547125568,主播: 悠悠包 -; https://live.douyin.com/816130992220,主播: 甜昕冰冰的摆摊日记 -; https://live.douyin.com/876468215361,主播: 晓晓和凡凡 -; https://live.douyin.com/511335278313,主播: 秘书(摆摊休息版) -; https://live.douyin.com/483160615952,主播: 小胡同学你好(饭团版) -; https://live.douyin.com/525514386431,主播: 阿闯烤肉夹馍 -; https://live.douyin.com/8687122573,主播: 小彤炒粉 -; https://live.douyin.com/642534242822,主播: (小王_)煎饼果子 -; https://live.douyin.com/743565594721,主播: 汪汪汪小妞 -; https://live.douyin.com/249578288248,主播: 思思努力版 -; https://live.douyin.com/472140253414,主播: 戴盘龙 -; https://live.douyin.com/700846653732,主播: 三哥大锅菜_炒鸡(国基路店)官方号 -; https://live.douyin.com/81849868631,主播: 纯儿²¹(休息勿跑空哦) -; https://live.douyin.com/690114366322,主播: 卤味鲜(东大) -; https://live.douyin.com/469980190666,主播: 茜茜柠檬茶(小米茶档_) -; https://live.douyin.com/388066418744,主播: 加速姐的摆摊日记 -