Files
2025-09-15 11:58:48 +08:00

248 lines
9.1 KiB
Python

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=5000, min_bitrate=3000, audio_bitrate=192,
delay_sec=40, 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(maxsize=800)
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",
"-f", "hls", # 强制 HLS
"-i", real_url,
"-c", "copy",
"-f", "mpegts",
"-"
]
try:
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=10**8)
print("[拉流] FFmpeg 启动成功")
def read_stderr():
while proc.poll() is None:
try:
line = proc.stderr.readline()
if not line:
continue
print(f"[拉流FFmpeg]: {line.decode(errors='ignore').strip()}")
except Exception:
continue
threading.Thread(target=read_stderr, daemon=True).start()
while not self.stop_flag.is_set():
try:
data = proc.stdout.read(188 * 5000)
if not data:
time.sleep(0.05)
continue
self.buffer_queue.put_nowait(data)
except queue.Full:
continue
except Exception as e:
print(f"[拉流异常] {e}")
time.sleep(1)
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",
"-x264-params", "keyint=60:scenecut=0",
"-b:v", f"{self.current_bitrate}k",
"-maxrate", f"{self.current_bitrate}k",
"-bufsize", f"{self.current_bitrate*4}k",
"-r", "30",
"-g", "60",
"-s", "720x1280",
"-c:a", "aac",
"-b:a", f"{self.audio_bitrate}k",
"-ar", "44100",
"-ac", "2",
"-strict", "-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, bufsize=10**8)
self.processes[room_name] = proc
def read_stderr():
while proc.poll() is None:
try:
line = proc.stderr.readline()
if not line:
continue
print(f"[{room_name} FFmpeg]: {line.decode(errors='ignore').strip()}")
except Exception:
continue
threading.Thread(target=read_stderr, daemon=True).start()
while not self.stop_flag.is_set():
try:
data = self.buffer_queue.get(timeout=0.5)
if not data:
continue
try:
proc.stdin.write(data)
proc.stdin.flush()
except (BrokenPipeError, OSError):
print(f"[{room_name}] 推流管道断开,准备重启")
break
except queue.Empty:
continue
except Exception as e:
print(f"[{room_name}] 推流异常: {e}")
break
except Exception as e:
print(f"[{room_name}] 推流进程启动失败: {e}")
time.sleep(5)
def _abr_thread(self):
while not self.stop_flag.is_set():
try:
queue_len = self.buffer_queue.qsize()
if queue_len > 200:
new_bitrate = max(self.min_bitrate, self.current_bitrate - 500)
elif queue_len < 20:
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
except Exception:
pass
time.sleep(max(self.check_interval, 10))
def stop_all(self):
print("停止所有推流...")
self.stop_flag.set()
for proc in self.processes.values():
try:
if proc and proc.poll() is None:
proc.terminate()
except Exception:
continue
async def get_douyin_real_url(record_url, record_quality="原画"):
try:
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")
except Exception as e:
print(f"获取抖音真实地址失败: {e}")
return None
def start_douyin_to_youtube(record_url, youtube_key, room_name=None, delay_sec=40):
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(fallback=(80, 20))
bar_width = min(max_width, term_width - 30)
while not hls_instance.stop_flag.is_set():
try:
queue_len = hls_instance.buffer_queue.qsize()
bitrate = hls_instance.current_bitrate
active_push = sum(1 for proc in hls_instance.processes.values() if proc and proc.poll() is None)
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="")
except Exception:
pass
time.sleep(interval)
if __name__ == "__main__":
DOUYIN_URL = "https://live.douyin.com/97282309368"
YOUTUBE_KEY = "qxvb-r47b-r5ju-6ud3-6k7z"
hls_instance = start_douyin_to_youtube(DOUYIN_URL, YOUTUBE_KEY, delay_sec=40)
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)