Files
gitlab-instance-0a899031_dy…/backup/back10
2025-09-18 22:41:14 +08:00

251 lines
10 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 spider
import stream
import aiohttp
# ----------------- Logging Setup -----------------
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
datefmt="%H:%M:%S"
)
IS_WINDOWS = platform.system() == "Windows"
# ----------------- Douyin Recorder (推流已删除增加9:16裁16:9) -----------------
class DouyinRecorder:
def __init__(self, audio_bitrate=128, max_minutes=8):
self.record_proc = None
self.stop_flag = threading.Event()
self.audio_bitrate = audio_bitrate
self.start_time = None
self.room_name = None
self.out_file = None
self.max_minutes = max_minutes
os.makedirs("records", exist_ok=True)
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("\\", "/")
# 处理竖屏视频裁切成16:9并两边使用模糊背景填充
# 输入假设是720x1280竖屏输出16:9 1280x720
# 原理1) 原视频缩放高度720保持宽度自适应
# 2) 原视频模糊填充16:9背景
vf_filter = (
"split [a][b];"
"[a] scale=-1:720 [main];"
"[b] scale=1280:720, boxblur=luma_radius=20:luma_power=1:chroma_radius=20:chroma_power=1 [bg];"
"[bg][main] overlay=(W-w)/2:(H-h)/2"
)
cmd = [
'ffmpeg', '-y',
'-i', real_url,
'-c:v', 'libx264',
'-preset', 'veryfast',
'-tune', 'zerolatency',
'-vf', vf_filter,
'-c:a', 'aac',
'-b:a', f'{self.audio_bitrate}k',
'-ar', '44100',
'-ac', '2',
'-movflags', '+faststart+frag_keyframe',
self.out_file
]
return cmd
# ----------------- 录制循环 -----------------
def start_record(self, real_url, room_name=None):
self.room_name = room_name or f"抖音_{str(uuid.uuid4())[:8]}"
logging.info(f"[{self.room_name}] 启动本地录制")
threading.Thread(target=self._run_record_loop, args=(real_url,), daemon=True).start()
threading.Thread(target=self._monitor_thread, daemon=True).start()
def _run_record_loop(self, real_url):
while not self.stop_flag.is_set():
try:
self.start_time = time.time()
# ---- 启动本地录制 ----
record_cmd = self._build_record_cmd(real_url)
self.record_proc = subprocess.Popen(
record_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE
)
logging.info(f"[{self.room_name}] 本地录制开始: {self.out_file}")
# ---- 监控 stderr ----
def monitor_proc(proc):
try:
for line in iter(proc.stderr.readline, b''):
if self.stop_flag.is_set():
break
if line:
decoded = line.decode('utf-8', errors='ignore').strip()
logging.debug(f"[{self.room_name} record]: {decoded}")
except Exception as e:
logging.error(f"[{self.room_name} record stderr异常: {e}")
threading.Thread(target=monitor_proc, args=(self.record_proc,), daemon=True).start()
# ---- 循环检查 ----
while not self.stop_flag.is_set():
elapsed = time.time() - self.start_time
if elapsed >= self.max_minutes * 60:
logging.warning(f"[{self.room_name}] 超过 {self.max_minutes} 分钟,自动停止录制")
self.stop()
break
if self.record_proc.poll() is not None:
logging.warning(f"[{self.room_name}] FFmpeg 录制进程退出1秒后重启...")
break
time.sleep(2)
except Exception as e:
logging.error(f"[{self.room_name}] FFmpeg录制异常: {e}")
finally:
self._terminate_proc()
self._finalize_recording()
if not self.stop_flag.is_set():
time.sleep(1)
def _terminate_proc(self):
if self.record_proc and self.record_proc.poll() is None:
self.record_proc.terminate()
try:
self.record_proc.wait(timeout=2)
except subprocess.TimeoutExpired:
self.record_proc.kill()
self.record_proc = 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.record_proc and self.record_proc.poll() is None else "启动中"
print(f"\r[{self.room_name}] {status} | 运行时间: {m:02d}:{s:02d} | 音频码率: {self.audio_bitrate}k", end="")
time.sleep(2)
# ----------------- 停止 -----------------
def stop(self):
logging.info(f"[{self.room_name}] 停止录制...")
self.stop_flag.set()
self._terminate_proc()
self._finalize_recording()
def _finalize_recording(self):
if self.out_file and os.path.exists(self.out_file):
try:
if self.record_proc and self.record_proc.poll() is None:
sig = signal.CTRL_BREAK_EVENT if IS_WINDOWS else signal.SIGINT
self.record_proc.send_signal(sig)
try:
self.record_proc.wait(timeout=5)
except subprocess.TimeoutExpired:
self.record_proc.kill()
except Exception:
pass
elapsed = int(time.time() - self.start_time) if self.start_time else 0
suffix = f"_{elapsed // 60}min" if elapsed >= 60 else f"_{elapsed}s"
new_name = self.out_file.replace(".mp4", f"{suffix}.mp4")
try:
os.rename(self.out_file, new_name)
except FileExistsError:
new_name = self.out_file.replace(".mp4", f"_{uuid.uuid4().hex[:4]}.mp4")
os.rename(self.out_file, new_name)
size_mb = os.path.getsize(new_name) / (1024 * 1024)
logging.info(f"[{self.room_name}] 本地录制已结束: {new_name} | 大小: {size_mb:.2f} MB")
# ----------------- 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_record(record_url, room_name=None, proxy_addr=None, cookies='', audio_bitrate=128):
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}")
recorder = DouyinRecorder(audio_bitrate=audio_bitrate)
recorder.start_record(real_url, room_name)
return recorder
# ----------------- Main Program -----------------
if __name__ == "__main__":
DOUYIN_URL = "https://live.douyin.com/530395023516"
recorder_instance = start_douyin_record(DOUYIN_URL)
if not recorder_instance:
logging.error("录制实例创建失败,程序退出")
sys.exit(1)
def signal_handler(sig, frame):
if recorder_instance:
recorder_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)