From aa26e03ca16ffe8fae5b6a73404e4c00e3ef8c19 Mon Sep 17 00:00:00 2001 From: eric Date: Sat, 13 Sep 2025 10:15:00 +0800 Subject: [PATCH] 's'' --- back | 162 +++++++++++++++++++++++++++++++++ back2 | 203 ++++++++++++++++++++++++++++++++++++++++++ douyin_to_youtube.log | 71 +++++++++++++++ live.py | 191 ++++++++++++++++++++++++++++----------- 4 files changed, 576 insertions(+), 51 deletions(-) create mode 100644 back create mode 100644 back2 create mode 100644 douyin_to_youtube.log diff --git a/back b/back new file mode 100644 index 0000000..b91cef9 --- /dev/null +++ b/back @@ -0,0 +1,162 @@ +import os +import subprocess +import threading +import asyncio +import uuid +import queue +import time +import signal +import sys +import spider +import stream + +class AdaptiveHLS2YouTube: + def __init__(self, max_bitrate=6000, min_bitrate=3000, audio_bitrate=192, + delay_sec=30, segment_time=2, check_interval=5): + self.processes = {} + self.stop_flag = threading.Event() + self.delay_sec = delay_sec + self.segment_time = segment_time + self.check_interval = check_interval + self.buffer_queue = queue.Queue() + self.max_bitrate = max_bitrate + self.min_bitrate = min_bitrate + self.audio_bitrate = audio_bitrate + self.current_bitrate = max_bitrate + + def start_stream(self, real_url, youtube_key, room_name=None): + room_name = room_name or f"抖音_{str(uuid.uuid4())[:8]}" + print(f"[{room_name}] 启动推流,目标 YouTube Key: {youtube_key}") + threading.Thread(target=self._pull_thread, args=(real_url,), daemon=True).start() + threading.Thread(target=self._push_thread, args=(youtube_key, room_name), daemon=True).start() + threading.Thread(target=self._abr_thread, daemon=True).start() + + def _pull_thread(self, real_url): + cmd = [ + "ffmpeg", + "-rw_timeout", "50000000", + "-user_agent", "Mozilla/5.0", + "-headers", "Referer: https://www.douyin.com/", + "-fflags", "+nobuffer+genpts", + "-flags", "low_delay", + "-reconnect", "1", + "-reconnect_at_eof", "1", + "-reconnect_streamed", "1", + "-reconnect_delay_max", "10", + "-i", real_url, + "-c", "copy", + "-f", "mpegts", + "-" + ] + try: + proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL) + while not self.stop_flag.is_set(): + data = proc.stdout.read(188*50) + if not data: + time.sleep(0.05) + continue + self.buffer_queue.put(data) + except Exception as e: + print(f"拉流异常: {e}") + + def _push_thread(self, youtube_key, room_name): + min_buffer = max(1, self.delay_sec // self.segment_time) + while not self.stop_flag.is_set(): + if self.buffer_queue.qsize() < min_buffer: + time.sleep(0.05) + continue + + cmd = [ + "ffmpeg", + "-re", + "-f", "mpegts", + "-i", "-", + "-c:v", "libx264", + "-preset", "veryfast", + "-tune", "zerolatency", + "-b:v", f"{self.current_bitrate}k", + "-maxrate", f"{self.current_bitrate}k", + "-bufsize", f"{self.current_bitrate*2}k", + "-r", "30", + "-g", "60", + "-c:a", "aac", + "-b:a", f"{self.audio_bitrate}k", + "-f", "flv", + f"rtmp://a.rtmp.youtube.com/live2/{youtube_key}" + ] + try: + proc = subprocess.Popen(cmd, stdin=subprocess.PIPE) + self.processes[room_name] = proc + while not self.stop_flag.is_set(): + if not self.buffer_queue.empty(): + data = self.buffer_queue.get() + proc.stdin.write(data) + proc.stdin.flush() + else: + time.sleep(0.01) + except Exception as e: + print(f"[{room_name}] 推流异常: {e}") + + def _abr_thread(self): + # 动态码率调节 + while not self.stop_flag.is_set(): + queue_len = self.buffer_queue.qsize() + if queue_len > 100: # 队列积压大 → 网络慢 → 降低码率 + new_bitrate = max(self.min_bitrate, self.current_bitrate - 500) + elif queue_len < 30: # 队列空 → 网络好 → 提高码率 + new_bitrate = min(self.max_bitrate, self.current_bitrate + 500) + else: + new_bitrate = self.current_bitrate + if new_bitrate != self.current_bitrate: + print(f"[ABR] 调整码率: {self.current_bitrate}k -> {new_bitrate}k") + self.current_bitrate = new_bitrate + time.sleep(self.check_interval) + + def stop_all(self): + print("停止所有推流...") + self.stop_flag.set() + for proc in self.processes.values(): + if proc.poll() is None: + proc.terminate() + +# ----------------- 抖音解析逻辑 ----------------- +async def get_douyin_real_url(record_url, record_quality="原画"): + if 'v.douyin.com' not in record_url and '/user/' not in record_url: + json_data = await spider.get_douyin_stream_data(url=record_url) + else: + json_data = await spider.get_douyin_app_stream_data(url=record_url) + port_info = await stream.get_douyin_stream_url(json_data, record_quality, None) + return port_info.get("record_url") + +# ----------------- 启动推流 ----------------- +def start_douyin_to_youtube(record_url, youtube_key, room_name=None, delay_sec=30): + try: + real_url = asyncio.run(get_douyin_real_url(record_url)) + except Exception as e: + print(f"获取真实直播流失败: {e}") + return + if not real_url: + print("获取真实直播流失败") + return + print(f"实际直播流: {real_url}") + hls2yt = AdaptiveHLS2YouTube(delay_sec=delay_sec) + hls2yt.start_stream(real_url, youtube_key, room_name) + return hls2yt + +# ----------------- 示例 ----------------- +if __name__ == "__main__": + DOUYIN_URL = "https://live.douyin.com/640087893839" + YOUTUBE_KEY = "qxvb-r47b-r5ju-6ud3-6k7z" + + hls_instance = start_douyin_to_youtube(DOUYIN_URL, YOUTUBE_KEY, delay_sec=30) + + def signal_handler(sig, frame): + if hls_instance: + hls_instance.stop_all() + sys.exit(0) + + signal.signal(signal.SIGINT, signal_handler) + signal.signal(signal.SIGTERM, signal_handler) + + while True: + time.sleep(60) diff --git a/back2 b/back2 new file mode 100644 index 0000000..fce5c1e --- /dev/null +++ b/back2 @@ -0,0 +1,203 @@ +import os +import subprocess +import threading +import asyncio +import uuid +import queue +import time +import signal +import sys +import shutil +import spider +import stream + +class AdaptiveHLS2YouTube: + def __init__(self, max_bitrate=6000, min_bitrate=3000, audio_bitrate=192, + delay_sec=30, segment_time=2, check_interval=5): + self.processes = {} + self.stop_flag = threading.Event() + self.delay_sec = delay_sec + self.segment_time = segment_time + self.check_interval = check_interval + self.buffer_queue = queue.Queue() + self.max_bitrate = max_bitrate + self.min_bitrate = min_bitrate + self.audio_bitrate = audio_bitrate + self.current_bitrate = max_bitrate + + def start_stream(self, real_url, youtube_key, room_name=None): + room_name = room_name or f"抖音_{str(uuid.uuid4())[:8]}" + print(f"[{room_name}] 启动推流,目标 YouTube Key: {youtube_key}") + threading.Thread(target=self._pull_thread, args=(real_url,), daemon=True).start() + threading.Thread(target=self._push_thread, args=(youtube_key, room_name), daemon=True).start() + threading.Thread(target=self._abr_thread, daemon=True).start() + + def _pull_thread(self, real_url): + cmd = [ + "ffmpeg", + "-rw_timeout", "50000000", + "-user_agent", "Mozilla/5.0", + "-headers", "Referer: https://www.douyin.com/", + "-fflags", "+nobuffer+genpts", + "-flags", "low_delay", + "-reconnect", "1", + "-reconnect_at_eof", "1", + "-reconnect_streamed", "1", + "-reconnect_delay_max", "10", + "-i", real_url, + "-c", "copy", + "-f", "mpegts", + "-" + ] + try: + proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL) + print("[拉流] FFmpeg 启动成功") + while not self.stop_flag.is_set(): + data = proc.stdout.read(188*50) + if not data: + time.sleep(0.05) + continue + self.buffer_queue.put(data) + except Exception as e: + print(f"拉流异常: {e}") + + def _push_thread(self, youtube_key, room_name): + min_buffer = max(1, self.delay_sec // self.segment_time) + while not self.stop_flag.is_set(): + if self.buffer_queue.qsize() < min_buffer: + time.sleep(0.05) + continue + + cmd = [ + "ffmpeg", + "-re", + "-f", "mpegts", + "-i", "-", + "-c:v", "libx264", + "-preset", "veryfast", + "-tune", "zerolatency", + "-b:v", f"{self.current_bitrate}k", + "-maxrate", f"{self.current_bitrate}k", + "-bufsize", f"{self.current_bitrate*4}k", + "-r", "30", + "-g", "60", + "-c:a", "aac", + "-b:a", f"{self.audio_bitrate}k", + "-ar", "44100", + "-ac", "2", + "-f", "flv", + "-rtmp_live", "live", + f"rtmp://a.rtmp.youtube.com/live2/{youtube_key}" + ] + try: + proc = subprocess.Popen(cmd, stdin=subprocess.PIPE, stderr=subprocess.PIPE) + self.processes[room_name] = proc + + # 读取 stderr 打印日志 + def read_stderr(): + while proc.poll() is None: + line = proc.stderr.readline() + if line: + print(f"[{room_name} FFmpeg]: {line.decode(errors='ignore').strip()}") + threading.Thread(target=read_stderr, daemon=True).start() + + while not self.stop_flag.is_set(): + if not self.buffer_queue.empty(): + data = self.buffer_queue.get() + proc.stdin.write(data) + proc.stdin.flush() + else: + time.sleep(0.01) + except Exception as e: + print(f"[{room_name}] 推流异常: {e}") + + def _abr_thread(self): + while not self.stop_flag.is_set(): + queue_len = self.buffer_queue.qsize() + if queue_len > 100: + new_bitrate = max(self.min_bitrate, self.current_bitrate - 500) + elif queue_len < 30: + new_bitrate = min(self.max_bitrate, self.current_bitrate + 500) + else: + new_bitrate = self.current_bitrate + if new_bitrate != self.current_bitrate: + print(f"[ABR] 调整码率: {self.current_bitrate}k -> {new_bitrate}k") + self.current_bitrate = new_bitrate + time.sleep(self.check_interval) + + def stop_all(self): + print("停止所有推流...") + self.stop_flag.set() + for proc in self.processes.values(): + if proc.poll() is None: + proc.terminate() + + +# ----------------- 抖音解析逻辑 ----------------- +async def get_douyin_real_url(record_url, record_quality="原画"): + if 'v.douyin.com' not in record_url and '/user/' not in record_url: + json_data = await spider.get_douyin_stream_data(url=record_url) + else: + json_data = await spider.get_douyin_app_stream_data(url=record_url) + port_info = await stream.get_douyin_stream_url(json_data, record_quality, None) + return port_info.get("record_url") + + +# ----------------- 启动推流 ----------------- +def start_douyin_to_youtube(record_url, youtube_key, room_name=None, delay_sec=30): + try: + real_url = asyncio.run(get_douyin_real_url(record_url)) + except Exception as e: + print(f"获取真实直播流失败: {e}") + return + if not real_url: + print("获取真实直播流失败") + return + print(f"实际直播流: {real_url}") + hls2yt = AdaptiveHLS2YouTube(delay_sec=delay_sec) + hls2yt.start_stream(real_url, youtube_key, room_name) + return hls2yt + + +# ----------------- 控制台监控面板 ----------------- +def monitor_panel_thread(hls_instance, interval=2, max_width=50): + term_width, _ = shutil.get_terminal_size() + bar_width = min(max_width, term_width - 30) + + while not hls_instance.stop_flag.is_set(): + queue_len = hls_instance.buffer_queue.qsize() + bitrate = hls_instance.current_bitrate + active_push = any(proc and proc.poll() is None for proc in hls_instance.processes.values()) + + queue_bar_len = min(bar_width, queue_len) + queue_bar = "#" * queue_bar_len + "-" * (bar_width - queue_bar_len) + + print("\r", end="") + print(f"[监控] 队列: [{queue_bar}] {queue_len:4} | 码率: {bitrate:4}k | 推流活跃: {active_push}", end="") + time.sleep(interval) + + +# ----------------- 示例 ----------------- +if __name__ == "__main__": + DOUYIN_URL = "https://live.douyin.com/848698179845" + YOUTUBE_KEY = "qxvb-r47b-r5ju-6ud3-6k7z" + + hls_instance = start_douyin_to_youtube(DOUYIN_URL, YOUTUBE_KEY, delay_sec=30) + if not hls_instance: + print("推流实例创建失败,程序退出") + sys.exit(1) + + # 启动实时监控面板 + threading.Thread(target=monitor_panel_thread, args=(hls_instance,), daemon=True).start() + + def signal_handler(sig, frame): + if hls_instance: + hls_instance.stop_all() + sys.exit(0) + + signal.signal(signal.SIGINT, signal_handler) + signal.signal(signal.SIGTERM, signal_handler) + + print("程序已启动,按 Ctrl+C 停止...") + while True: + time.sleep(60) diff --git a/douyin_to_youtube.log b/douyin_to_youtube.log new file mode 100644 index 0000000..9cef94b --- /dev/null +++ b/douyin_to_youtube.log @@ -0,0 +1,71 @@ +2025-09-13 09:42:21,441 [INFO] httpx: HTTP Request: GET https://live.douyin.com/848698179845 "HTTP/2 200 OK" +2025-09-13 09:42:22,592 [INFO] httpx: HTTP Request: HEAD http://pull-hls-l13.douyincdn.com/stage/stream-117958962069897656_or4.m3u8?arch_hrchy=w1&exp_hrchy=w1&expire=1758332541&major_anchor_level=common&sign=5d71c5252347afa2a3b465f133ed58f2&t_id=037-20250913094221234B680EF32A4439DD87-GEORFp&codec=h264 "HTTP/1.1 200 OK" +2025-09-13 09:42:22,597 [INFO] __main__: ʵֱ: http://pull-hls-l13.douyincdn.com/stage/stream-117958962069897656_or4.m3u8?arch_hrchy=w1&exp_hrchy=w1&expire=1758332541&major_anchor_level=common&sign=5d71c5252347afa2a3b465f133ed58f2&t_id=037-20250913094221234B680EF32A4439DD87-GEORFp&codec=h264 +2025-09-13 09:42:22,602 [INFO] __main__: [_7dbe64a7] Ŀ YouTube Key: qxvb-r47b-r5ju-6ud3-6k7z +2025-09-13 09:42:22,604 [INFO] __main__: [_7dbe64a7] ʵֱ: http://pull-hls-l13.douyincdn.com/stage/stream-117958962069897656_or4.m3u8?arch_hrchy=w1&exp_hrchy=w1&expire=1758332541&major_anchor_level=common&sign=5d71c5252347afa2a3b465f133ed58f2&t_id=037-20250913094221234B680EF32A4439DD87-GEORFp&codec=h264 +2025-09-13 09:42:25,655 [INFO] __main__: [_7dbe64a7] PID: 7728 +2025-09-13 09:42:40,372 [INFO] __main__: [_7dbe64a7] PID: 19568 +2025-09-13 09:42:44,609 [INFO] __main__: [_7dbe64a7] [ABR] : 6000k -> 5500k (: 12.80) +2025-09-13 09:42:49,613 [INFO] __main__: [_7dbe64a7] [ABR] : 5500k -> 5000k (: 13.13) +2025-09-13 09:42:54,616 [INFO] __main__: [_7dbe64a7] [ABR] : 5000k -> 4500k (: 11.67) +2025-09-13 09:42:59,619 [INFO] __main__: [_7dbe64a7] [ABR] : 4500k -> 4000k (: 12.93) +2025-09-13 09:43:04,620 [INFO] __main__: [_7dbe64a7] [ABR] : 4000k -> 3500k (: 13.33) +2025-09-13 09:43:09,622 [INFO] __main__: [_7dbe64a7] [ABR] : 3500k -> 3000k (: 13.33) +2025-09-13 09:44:04,081 [ERROR] __main__: [_7dbe64a7] д쳣: [Errno 22] Invalid argument +2025-09-13 09:44:04,583 [ERROR] __main__: [_7dbe64a7] д쳣: [Errno 22] Invalid argument +2025-09-13 09:44:05,085 [ERROR] __main__: [_7dbe64a7] д쳣: [Errno 22] Invalid argument +2025-09-13 09:44:05,586 [ERROR] __main__: [_7dbe64a7] д쳣: [Errno 22] Invalid argument +2025-09-13 09:44:06,088 [ERROR] __main__: [_7dbe64a7] д쳣: [Errno 22] Invalid argument +2025-09-13 09:44:06,589 [ERROR] __main__: [_7dbe64a7] д쳣: [Errno 22] Invalid argument +2025-09-13 09:44:07,091 [ERROR] __main__: [_7dbe64a7] д쳣: [Errno 22] Invalid argument +2025-09-13 09:44:07,593 [ERROR] __main__: [_7dbe64a7] д쳣: [Errno 22] Invalid argument +2025-09-13 09:44:08,095 [ERROR] __main__: [_7dbe64a7] д쳣: [Errno 22] Invalid argument +2025-09-13 09:44:08,597 [ERROR] __main__: [_7dbe64a7] д쳣: [Errno 22] Invalid argument +2025-09-13 09:44:09,098 [ERROR] __main__: [_7dbe64a7] д쳣: [Errno 22] Invalid argument +2025-09-13 09:44:09,602 [ERROR] __main__: [_7dbe64a7] д쳣: [Errno 22] Invalid argument +2025-09-13 09:44:10,107 [ERROR] __main__: [_7dbe64a7] д쳣: [Errno 22] Invalid argument +2025-09-13 09:44:10,615 [ERROR] __main__: [_7dbe64a7] д쳣: [Errno 22] Invalid argument +2025-09-13 09:44:11,166 [ERROR] __main__: [_7dbe64a7] д쳣: [Errno 22] Invalid argument +2025-09-13 09:44:11,725 [INFO] __main__: յź 2ֹͣ +2025-09-13 09:44:11,726 [INFO] __main__: ֹͣ... +2025-09-13 09:44:11,738 [INFO] __main__: ֹͣ +2025-09-13 09:46:22,098 [INFO] httpx: HTTP Request: GET https://live.douyin.com/848698179845 "HTTP/2 200 OK" +2025-09-13 09:46:24,076 [INFO] httpx: HTTP Request: HEAD http://pull-hls-l13.douyincdn.com/stage/stream-117958962069897656_or4.m3u8?arch_hrchy=w1&exp_hrchy=w1&expire=1758332782&major_anchor_level=common&sign=4c8db99e7a0c26a419faf2a1e34eae74&t_id=037-20250913094621681A1B914C390282BA5E-Rxo2Sa&codec=h264 "HTTP/1.1 200 OK" +2025-09-13 09:46:24,081 [INFO] __main__: ʵֱ: http://pull-hls-l13.douyincdn.com/stage/stream-117958962069897656_or4.m3u8?arch_hrchy=w1&exp_hrchy=w1&expire=1758332782&major_anchor_level=common&sign=4c8db99e7a0c26a419faf2a1e34eae74&t_id=037-20250913094621681A1B914C390282BA5E-Rxo2Sa&codec=h264 +2025-09-13 09:46:24,083 [INFO] __main__: [_ea368365] Ŀ YouTube Key: qxvb-r47b-r5ju-6ud3-6k7z +2025-09-13 09:46:24,084 [INFO] __main__: [_ea368365] ʵֱ: http://pull-hls-l13.douyincdn.com/stage/stream-117958962069897656_or4.m3u8?arch_hrchy=w1&exp_hrchy=w1&expire=1758332782&major_anchor_level=common&sign=4c8db99e7a0c26a419faf2a1e34eae74&t_id=037-20250913094621681A1B914C390282BA5E-Rxo2Sa&codec=h264 +2025-09-13 09:46:26,500 [INFO] __main__: [_ea368365] PID: 9880 +2025-09-13 09:46:29,093 [WARNING] __main__: [_ea368365] ʼ岻㣬ӳ +2025-09-13 09:46:39,096 [INFO] __main__: [_ea368365] [ABR] : 6000k -> 5800k (: 13.60) +2025-09-13 09:46:49,098 [INFO] __main__: [_ea368365] [ABR] : 5800k -> 5600k (: 19.27) +2025-09-13 09:46:59,100 [INFO] __main__: [_ea368365] [ABR] : 5600k -> 5400k (: 27.73) +2025-09-13 09:47:09,102 [INFO] __main__: [_ea368365] [ABR] : 5400k -> 5200k (: 33.30) +2025-09-13 09:47:19,104 [INFO] __main__: [_ea368365] [ABR] : 5200k -> 5000k (: 33.33) +2025-09-13 09:47:29,105 [INFO] __main__: [_ea368365] [ABR] : 5000k -> 4800k (: 33.33) +2025-09-13 09:47:39,107 [INFO] __main__: [_ea368365] [ABR] : 4800k -> 4600k (: 33.33) +2025-09-13 09:47:49,109 [INFO] __main__: [_ea368365] [ABR] : 4600k -> 4400k (: 33.33) +2025-09-13 09:47:59,112 [INFO] __main__: [_ea368365] [ABR] : 4400k -> 4200k (: 33.33) +2025-09-13 09:48:09,115 [INFO] __main__: [_ea368365] [ABR] : 4200k -> 4000k (: 33.33) +2025-09-13 09:48:19,120 [INFO] __main__: [_ea368365] [ABR] : 4000k -> 3800k (: 33.33) +2025-09-13 09:48:29,123 [INFO] __main__: [_ea368365] [ABR] : 3800k -> 3600k (: 33.33) +2025-09-13 09:48:39,127 [INFO] __main__: [_ea368365] [ABR] : 3600k -> 3400k (: 33.33) +2025-09-13 09:48:49,129 [INFO] __main__: [_ea368365] [ABR] : 3400k -> 3200k (: 33.33) +2025-09-13 09:48:59,132 [INFO] __main__: [_ea368365] [ABR] : 3200k -> 3000k (: 33.33) +2025-09-13 09:49:35,107 [INFO] __main__: յź 2ֹͣ +2025-09-13 09:49:35,108 [INFO] __main__: ֹͣ... +2025-09-13 09:49:35,119 [INFO] __main__: ֹͣ +2025-09-13 09:49:39,251 [INFO] httpx: HTTP Request: GET https://live.douyin.com/848698179845 "HTTP/2 200 OK" +2025-09-13 09:49:40,464 [INFO] httpx: HTTP Request: HEAD http://pull-hls-l13.douyincdn.com/stage/stream-117958962069897656_or4.m3u8?arch_hrchy=w1&exp_hrchy=w1&expire=1758332979&major_anchor_level=common&sign=1b74463ce878bc0174a578edcfa719c1&t_id=037-202509130949385D15825682C4A5F8F47C-iIt4V6&codec=h264 "HTTP/1.1 200 OK" +2025-09-13 09:49:40,469 [INFO] __main__: ʵֱ: http://pull-hls-l13.douyincdn.com/stage/stream-117958962069897656_or4.m3u8?arch_hrchy=w1&exp_hrchy=w1&expire=1758332979&major_anchor_level=common&sign=1b74463ce878bc0174a578edcfa719c1&t_id=037-202509130949385D15825682C4A5F8F47C-iIt4V6&codec=h264 +2025-09-13 09:49:40,470 [INFO] __main__: [_d46e59a8] Ŀ YouTube Key: qxvb-r47b-r5ju-6ud3-6k7z +2025-09-13 09:49:40,470 [INFO] __main__: [_d46e59a8] ʵֱ: http://pull-hls-l13.douyincdn.com/stage/stream-117958962069897656_or4.m3u8?arch_hrchy=w1&exp_hrchy=w1&expire=1758332979&major_anchor_level=common&sign=1b74463ce878bc0174a578edcfa719c1&t_id=037-202509130949385D15825682C4A5F8F47C-iIt4V6&codec=h264 +2025-09-13 09:49:42,555 [INFO] __main__: [_d46e59a8] PID: 20372 +2025-09-13 09:49:50,472 [WARNING] __main__: [_d46e59a8] ʼ岻㣨дС: 0ӳ +2025-09-13 09:50:00,475 [INFO] __main__: [_d46e59a8] [ABR] : 6000k -> 5900k (: 7.03) +2025-09-13 09:50:10,476 [INFO] __main__: [_d46e59a8] [ABR] : 5900k -> 5800k (: 18.30) +2025-09-13 09:50:20,478 [INFO] __main__: [_d46e59a8] [ABR] : 5800k -> 5700k (: 23.90) +2025-09-13 09:50:30,480 [INFO] __main__: [_d46e59a8] [ABR] : 5700k -> 5600k (: 30.57) +2025-09-13 09:50:40,481 [INFO] __main__: [_d46e59a8] [ABR] : 5600k -> 5500k (: 33.33) +2025-09-13 09:50:42,864 [INFO] __main__: յź 2ֹͣ +2025-09-13 09:50:42,866 [INFO] __main__: ֹͣ... +2025-09-13 09:50:42,877 [INFO] __main__: ֹͣ diff --git a/live.py b/live.py index 171f964..a3d8953 100644 --- a/live.py +++ b/live.py @@ -7,25 +7,32 @@ import queue import time import signal import sys +import shutil import spider import stream -class HLS2YouTube: - def __init__(self, delay_sec=30): +class AdaptiveHLS2YouTube: + def __init__(self, max_bitrate=6000, min_bitrate=3000, audio_bitrate=192, + delay_sec=30, segment_time=2, check_interval=5): self.processes = {} self.stop_flag = threading.Event() self.delay_sec = delay_sec - self.segment_time = 2 # 内部缓冲每个小片段2秒 + self.segment_time = segment_time + self.check_interval = check_interval self.buffer_queue = queue.Queue() + self.max_bitrate = max_bitrate + self.min_bitrate = min_bitrate + self.audio_bitrate = audio_bitrate + self.current_bitrate = max_bitrate def start_stream(self, real_url, youtube_key, room_name=None): room_name = room_name or f"抖音_{str(uuid.uuid4())[:8]}" print(f"[{room_name}] 启动推流,目标 YouTube Key: {youtube_key}") threading.Thread(target=self._pull_thread, args=(real_url,), daemon=True).start() threading.Thread(target=self._push_thread, args=(youtube_key, room_name), daemon=True).start() + threading.Thread(target=self._abr_thread, daemon=True).start() def _pull_thread(self, real_url): - # 拉流存入内存队列 cmd = [ "ffmpeg", "-rw_timeout", "50000000", @@ -42,53 +49,108 @@ class HLS2YouTube: "-f", "mpegts", "-" ] - try: - proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL) - while not self.stop_flag.is_set(): - data = proc.stdout.read(188*50) # 每次读小片段 - if not data: - time.sleep(0.1) - continue - self.buffer_queue.put(data) - except Exception as e: - print(f"拉流异常: {e}") + + while not self.stop_flag.is_set(): + try: + proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + print("[拉流] FFmpeg 启动成功") + + def read_stderr(): + while proc.poll() is None: + line = proc.stderr.readline() + if line: + print(f"[拉流 FFmpeg]: {line.decode(errors='ignore').strip()}") + threading.Thread(target=read_stderr, daemon=True).start() + + while not self.stop_flag.is_set(): + data = proc.stdout.read(188*20) + if data: + self.buffer_queue.put(data) + else: + time.sleep(0.01) + + if proc.poll() is not None: + print("[拉流] FFmpeg 退出,5 秒后重启...") + time.sleep(5) + + except Exception as e: + print(f"[拉流] 异常: {e}, 5 秒后重试") + time.sleep(5) def _push_thread(self, youtube_key, room_name): - min_buffer = self.delay_sec // self.segment_time - cmd = [ - "ffmpeg", - "-re", - "-f", "mpegts", - "-i", "-", - "-c:v", "libx264", - "-preset", "veryfast", - "-tune", "zerolatency", - "-b:v", "3000k", - "-maxrate", "3000k", - "-bufsize", "6000k", - "-r", "30", - "-g", "60", - "-c:a", "aac", - "-b:a", "128k", - "-f", "flv", - f"rtmp://a.rtmp.youtube.com/live2/{youtube_key}" - ] - try: - proc = subprocess.Popen(cmd, stdin=subprocess.PIPE) - self.processes[room_name] = proc - # 等待队列蓄水 - while self.buffer_queue.qsize() < min_buffer and not self.stop_flag.is_set(): - time.sleep(0.5) - # 持续推送 - while not self.stop_flag.is_set(): - if not self.buffer_queue.empty(): - data = self.buffer_queue.get() - proc.stdin.write(data) - proc.stdin.flush() - else: - time.sleep(0.05) - except Exception as e: - print(f"[{room_name}] 推流异常: {e}") + min_buffer = max(1, self.delay_sec // self.segment_time) + + while not self.stop_flag.is_set(): + if self.buffer_queue.qsize() < min_buffer: + time.sleep(0.05) + continue + + cmd = [ + "ffmpeg", + "-re", + "-f", "mpegts", + "-i", "-", + "-c:v", "libx264", + "-preset", "veryfast", + "-tune", "zerolatency", + "-b:v", f"{self.current_bitrate}k", + "-maxrate", f"{self.current_bitrate}k", + "-bufsize", f"{self.current_bitrate*4}k", + "-r", "30", + "-g", "60", + "-c:a", "aac", + "-b:a", f"{self.audio_bitrate}k", + "-ar", "44100", + "-ac", "2", + "-f", "flv", + "-rtmp_live", "live", + f"rtmp://a.rtmp.youtube.com/live2/{youtube_key}" + ] + + try: + proc = subprocess.Popen(cmd, stdin=subprocess.PIPE, stderr=subprocess.PIPE) + self.processes[room_name] = proc + + def read_stderr(): + while proc.poll() is None: + line = proc.stderr.readline() + if line: + print(f"[{room_name} FFmpeg]: {line.decode(errors='ignore').strip()}") + threading.Thread(target=read_stderr, daemon=True).start() + + while not self.stop_flag.is_set() and proc.poll() is None: + if not self.buffer_queue.empty(): + data = self.buffer_queue.get() + try: + proc.stdin.write(data) + proc.stdin.flush() + except BrokenPipeError: + print(f"[{room_name}] FFmpeg stdin 已断开,重启推流") + break + else: + time.sleep(0.01) + + if proc.poll() is not None: + print(f"[{room_name}] 推流 FFmpeg 退出,5 秒后重启...") + time.sleep(5) + + except Exception as e: + print(f"[{room_name}] 推流异常: {e}, 5 秒后重试") + time.sleep(5) + + def _abr_thread(self): + while not self.stop_flag.is_set(): + queue_len = self.buffer_queue.qsize() + if queue_len > 100: + new_bitrate = max(self.min_bitrate, self.current_bitrate - 500) + elif queue_len < 30: + new_bitrate = min(self.max_bitrate, self.current_bitrate + 500) + else: + new_bitrate = self.current_bitrate + if new_bitrate != self.current_bitrate: + print(f"[ABR] 调整码率: {self.current_bitrate}k -> {new_bitrate}k") + self.current_bitrate = new_bitrate + time.sleep(self.check_interval) def stop_all(self): print("停止所有推流...") @@ -97,6 +159,7 @@ class HLS2YouTube: if proc.poll() is None: proc.terminate() + # ----------------- 抖音解析逻辑 ----------------- async def get_douyin_real_url(record_url, record_quality="原画"): if 'v.douyin.com' not in record_url and '/user/' not in record_url: @@ -106,6 +169,7 @@ async def get_douyin_real_url(record_url, record_quality="原画"): port_info = await stream.get_douyin_stream_url(json_data, record_quality, None) return port_info.get("record_url") + # ----------------- 启动推流 ----------------- def start_douyin_to_youtube(record_url, youtube_key, room_name=None, delay_sec=30): try: @@ -117,16 +181,40 @@ def start_douyin_to_youtube(record_url, youtube_key, room_name=None, delay_sec=3 print("获取真实直播流失败") return print(f"实际直播流: {real_url}") - hls2yt = HLS2YouTube(delay_sec=delay_sec) + hls2yt = AdaptiveHLS2YouTube(delay_sec=delay_sec) hls2yt.start_stream(real_url, youtube_key, room_name) return hls2yt + +# ----------------- 控制台监控面板 ----------------- +def monitor_panel_thread(hls_instance, interval=2, max_width=50): + term_width, _ = shutil.get_terminal_size() + bar_width = min(max_width, term_width - 30) + + while not hls_instance.stop_flag.is_set(): + queue_len = hls_instance.buffer_queue.qsize() + bitrate = hls_instance.current_bitrate + active_push = any(proc and proc.poll() is None for proc in hls_instance.processes.values()) + + queue_bar_len = min(bar_width, queue_len) + queue_bar = "#" * queue_bar_len + "-" * (bar_width - queue_bar_len) + + print("\r", end="") + print(f"[监控] 队列: [{queue_bar}] {queue_len:4} | 码率: {bitrate:4}k | 推流活跃: {active_push}", end="") + time.sleep(interval) + + # ----------------- 示例 ----------------- if __name__ == "__main__": - DOUYIN_URL = "https://live.douyin.com/104914538985" + DOUYIN_URL = "https://live.douyin.com/848698179845" YOUTUBE_KEY = "qxvb-r47b-r5ju-6ud3-6k7z" hls_instance = start_douyin_to_youtube(DOUYIN_URL, YOUTUBE_KEY, delay_sec=30) + if not hls_instance: + print("推流实例创建失败,程序退出") + sys.exit(1) + + threading.Thread(target=monitor_panel_thread, args=(hls_instance,), daemon=True).start() def signal_handler(sig, frame): if hls_instance: @@ -136,5 +224,6 @@ if __name__ == "__main__": signal.signal(signal.SIGINT, signal_handler) signal.signal(signal.SIGTERM, signal_handler) + print("程序已启动,按 Ctrl+C 停止...") while True: time.sleep(60)