Files
gitlab-instance-0a899031_dy…/backup/back9
2025-09-16 21:45:03 +08:00

276 lines
12 KiB
Plaintext
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 time
import signal
import sys
import logging
import platform
import aiohttp
import spider # 你的 spider 模块
import stream # 你的 stream 模块
# ----------------- Logging Setup -----------------
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
datefmt="%H:%M:%S"
)
IS_WINDOWS = platform.system() == "Windows"
# ----------------- Douyin to YouTube Streamer -----------------
class DouyinToYouTubeStreamer:
def __init__(self, bitrate=5000, audio_bitrate=128, resolution='720x1280',
fps=30, gop=60, max_minutes=None, enable_recording=False):
self.push_proc = None
self.record_proc = None
self.stop_flag = threading.Event()
self.bitrate = bitrate
self.audio_bitrate = audio_bitrate
self.resolution = resolution
self.fps = fps
self.gop = gop
self.start_time = None
self.room_name = None
self.pushing = False
self.out_file = None
self.max_minutes = max_minutes # None 表示无限制
self.enable_recording = enable_recording
os.makedirs("records", exist_ok=True)
# ----------------- 启动推流 -----------------
def start_stream(self, real_url, youtube_key, room_name=None):
self.room_name = room_name or f"抖音_{str(uuid.uuid4())[:8]}"
logging.info(f"[{self.room_name}] 启动低延迟推流,目标 YouTube Key: {youtube_key}")
threading.Thread(target=self._run_stream_loop,
args=(real_url, youtube_key), daemon=True).start()
threading.Thread(target=self._monitor_thread, daemon=True).start()
# ----------------- 构建推流命令 -----------------
def _build_push_cmd(self, real_url, youtube_key):
user_agent = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36")
headers = "Referer: https://www.douyin.com/\r\nOrigin: https://www.douyin.com\r\n"
cmd = [
'ffmpeg', '-y',
'-rw_timeout', '10000000',
'-analyzeduration', '1000000',
'-probesize', '5000000',
'-user_agent', user_agent,
'-headers', headers,
'-protocol_whitelist', 'file,pipe,http,https,tcp,tls,udp,rtp,crypto',
'-fflags', '+discardcorrupt+genpts',
'-flags', 'low_delay',
'-http_seekable', '0',
'-i', real_url,
'-c:v', 'libx264', '-preset', 'veryfast', '-tune', 'zerolatency',
'-b:v', f'{self.bitrate}k',
'-maxrate', f'{self.bitrate}k',
'-bufsize', f'{self.bitrate * 2}k',
'-r', str(self.fps),
'-g', str(self.gop),
'-s', self.resolution,
'-c:a', 'aac', '-b:a', f'{self.audio_bitrate}k', '-ar', '44100', '-ac', '2',
'-f', 'flv',
f'rtmp://a.rtmp.youtube.com/live2/{youtube_key}'
]
return cmd
# ----------------- 构建录制命令 -----------------
def _build_record_cmd(self, real_url):
base_name = f"{self.room_name}_{int(time.time())}.mp4"
self.out_file = os.path.join("records", base_name).replace("\\", "/")
cmd = [
'ffmpeg', '-y',
'-i', real_url,
'-c:v', 'libx264',
'-preset', 'veryfast',
'-tune', 'zerolatency',
'-c:a', 'aac',
'-b:a', f'{self.audio_bitrate}k',
'-ar', '44100',
'-ac', '2',
'-movflags', '+faststart+frag_keyframe',
self.out_file
]
return cmd
# ----------------- 推流循环 -----------------
def _run_stream_loop(self, real_url, youtube_key):
while not self.stop_flag.is_set():
try:
self.start_time = time.time()
self.pushing = False
# ---- 启动推流 ----
push_cmd = self._build_push_cmd(real_url, youtube_key)
self.push_proc = subprocess.Popen(
push_cmd, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE,
bufsize=1, universal_newlines=True
)
logging.info(f"[{self.room_name}] 推流进程 PID: {self.push_proc.pid}")
# ---- 启动本地录制 ----
if self.enable_recording:
record_cmd = self._build_record_cmd(real_url)
self.record_proc = subprocess.Popen(
record_cmd, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE,
bufsize=1, universal_newlines=True
)
logging.info(f"[{self.room_name}] 本地录制开始: {self.out_file}")
# ---- monitor stderr 阻塞读取Windows兼容 ----
def monitor_proc(proc, name):
try:
while not self.stop_flag.is_set() and proc and proc.poll() is None:
line = proc.stderr.readline()
if not line:
continue
if name == "push" and not self.pushing and "frame=" in line and "fps=" in line:
self.pushing = True
logging.info(f"[{self.room_name}] 推流到 YouTube 成功")
except Exception as e:
logging.error(f"[{self.room_name} {name} stderr异常: {e}")
threading.Thread(target=monitor_proc, args=(self.push_proc, "push"), daemon=True).start()
if self.enable_recording:
threading.Thread(target=monitor_proc, args=(self.record_proc, "record"), daemon=True).start()
# ---- 循环检查进程状态 ----
while not self.stop_flag.is_set():
elapsed = time.time() - self.start_time
if self.max_minutes and elapsed >= self.max_minutes * 60:
logging.warning(f"[{self.room_name}] 超过 {self.max_minutes} 分钟,自动停止推流和录制")
self.stop()
break
if self.push_proc.poll() is not None:
logging.warning(f"[{self.room_name}] 推流进程退出5秒后重启...")
time.sleep(5)
break
if self.enable_recording and self.record_proc and self.record_proc.poll() is not None:
logging.warning(f"[{self.room_name}] 录制进程退出5秒后重启...")
time.sleep(5)
break
time.sleep(2)
except Exception as e:
logging.error(f"[{self.room_name}] FFmpeg异常: {e}")
finally:
self._terminate_procs()
if not self.stop_flag.is_set():
time.sleep(1)
def _terminate_procs(self):
for proc in [self.push_proc, self.record_proc]:
if proc and proc.poll() is None:
proc.terminate()
try:
proc.wait(timeout=2)
except subprocess.TimeoutExpired:
proc.kill()
self.push_proc, self.record_proc = None, None
# ----------------- 监控线程 -----------------
def _monitor_thread(self):
while not self.stop_flag.is_set():
uptime = int(time.time() - self.start_time) if self.start_time else 0
m, s = divmod(uptime, 60)
status = "推流中" if self.pushing else "启动中"
print(f"\r[{self.room_name}] {status} | 运行时间: {m:02d}:{s:02d} | 码率: {self.bitrate}k", end="")
time.sleep(5)
# ----------------- 停止 -----------------
def stop(self):
logging.info(f"[{self.room_name}] 停止推流和录制...")
self.stop_flag.set()
self._terminate_procs()
# ----------------- Douyin Stream Parsing -----------------
async def get_douyin_real_url(record_url, record_quality="原画", proxy_addr=None, cookies=''):
headers = {
"User-Agent": ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36"),
"Referer": "https://www.douyin.com/",
"Origin": "https://www.douyin.com",
"Accept": "application/json, text/javascript, */*; q=0.01",
}
session_args = {"headers": headers}
if proxy_addr:
session_args["proxy"] = proxy_addr
try:
async with aiohttp.ClientSession(**session_args) as session:
for attempt in range(3):
try:
async with session.get(record_url, cookies=cookies) as resp:
text = await resp.text()
if not text or "captcha" in text.lower():
logging.warning(f"第{attempt+1}次尝试获取流失败,返回内容为空或验证码,重试...")
await asyncio.sleep(1)
continue
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, proxy_addr=proxy_addr, cookies=cookies)
else:
json_data = await spider.get_douyin_app_stream_data(
url=record_url, proxy_addr=proxy_addr, cookies=cookies)
port_info = await stream.get_douyin_stream_url(
json_data, record_quality, proxy_addr)
if port_info and port_info.get("record_url"):
return port_info.get("record_url")
else:
logging.warning(f"第{attempt+1}次尝试解析失败port_info为空")
await asyncio.sleep(1)
except Exception as e:
logging.warning(f"第{attempt+1}次尝试抓取抖音流异常: {e}")
await asyncio.sleep(1)
logging.error("多次尝试后仍获取抖音真实流失败")
return None
except Exception as e:
logging.error(f"初始化 aiohttp session 失败: {e}")
return None
def start_douyin_to_youtube(record_url, youtube_key, room_name=None,
proxy_addr=None, cookies='', bitrate=5000, enable_recording=False):
real_url = asyncio.run(get_douyin_real_url(record_url, proxy_addr=proxy_addr, cookies=cookies))
if not real_url:
logging.error("获取真实直播流失败")
return None
logging.info(f"实际直播流: {real_url}")
streamer = DouyinToYouTubeStreamer(bitrate=bitrate, enable_recording=enable_recording, max_minutes=None)
streamer.start_stream(real_url, youtube_key, room_name)
return streamer
# ----------------- Main Program -----------------
if __name__ == "__main__":
DOUYIN_URL = "https://live.douyin.com/851742623054"
YOUTUBE_KEY = "qxvb-r47b-r5ju-6ud3-6k7z"
# enable_recording=True 可开启本地录制
streamer_instance = start_douyin_to_youtube(DOUYIN_URL, YOUTUBE_KEY, enable_recording=False)
if not streamer_instance:
logging.error("推流实例创建失败,程序退出")
sys.exit(1)
def signal_handler(sig, frame):
if streamer_instance:
streamer_instance.stop()
sys.exit(0)
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
logging.info("程序已启动,低延迟推流中,按 Ctrl+C 停止...")
while True:
time.sleep(60)