Files
gitlab-instance-0a899031_dy…/live.py
2025-09-13 15:02:30 +08:00

234 lines
9.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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()
self.max_bitrate = max_bitrate
self.min_bitrate = min_bitrate
self.audio_bitrate = audio_bitrate
self.current_bitrate = max_bitrate
# ----------------- 稳定拉流线程 -----------------
def _pull_thread(self, real_url):
"""稳定拉取抖音 HLS保证 buffer_queue 有数据"""
while not self.stop_flag.is_set():
cmd = [
"ffmpeg",
"-rw_timeout", "50000000",
"-user_agent", "Mozilla/5.0",
"-headers", "Referer: https://www.douyin.com/",
"-protocol_whitelist", "file,http,https,tcp,tls,crypto",
"-fflags", "+nobuffer+genpts",
"-flags", "low_delay",
"-i", real_url,
"-c:v", "libx264",
"-preset", "veryfast",
"-tune", "zerolatency",
"-c:a", "aac",
"-b:a", f"{self.audio_bitrate}k",
"-f", "mpegts",
"-"
]
try:
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
print("[拉流] FFmpeg 启动成功")
# 打印拉流日志
def log_stderr():
while proc.poll() is None and not self.stop_flag.is_set():
line = proc.stderr.readline()
if line:
print(f"[拉流 FFmpeg]: {line.decode(errors='ignore').strip()}")
threading.Thread(target=log_stderr, daemon=True).start()
while not self.stop_flag.is_set():
try:
data = proc.stdout.read(188*2000) # 一次读大块
if not data:
raise EOFError("拉流 EOF")
self.buffer_queue.put(data, timeout=1)
except queue.Full:
time.sleep(0.05) # 队列满,短暂等待
except EOFError:
print("[拉流] 遇到 EOF重启 FFmpeg")
break # 跳出内循环,重新启动 FFmpeg
except Exception as e:
print(f"[拉流] 异常: {e}")
finally:
try:
if proc and proc.poll() is None:
proc.kill()
except:
pass
time.sleep(1) # 重连前等待
# ----------------- 推流线程 -----------------
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",
"-s", "720x1280",
"-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():
try:
data = self.buffer_queue.get(timeout=1)
proc.stdin.write(data)
proc.stdin.flush()
except queue.Empty:
continue
except Exception as e:
print(f"[{room_name}] 推流异常: {e}")
# ----------------- ABR 自动码率 -----------------
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 - 300)
elif queue_len < 30:
new_bitrate = min(self.max_bitrate, self.current_bitrate + 300)
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 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 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=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()
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/299336549416"
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)