Files
gitlab-instance-0a899031_dy…/backup/back2
2025-09-15 11:58:48 +08:00

204 lines
7.5 KiB
Plaintext

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)