This commit is contained in:
eric
2025-12-01 16:18:28 +08:00
parent 962b17ea67
commit 9e8ef1d28d
8 changed files with 3666 additions and 0 deletions

679
README.md Normal file
View File

@@ -0,0 +1,679 @@
![video_spider](https://socialify.git.ci/ihmily/DouyinLiveRecorder/image?font=Inter&forks=1&language=1&owner=1&pattern=Circuit%20Board&stargazers=1&theme=Light)
## 💡简介
[![Python Version](https://img.shields.io/badge/python-3.11.6-blue.svg)](https://www.python.org/downloads/release/python-3116/)
[![Supported Platforms](https://img.shields.io/badge/platforms-Windows%20%7C%20Linux-blue.svg)](https://github.com/ihmily/DouyinLiveRecorder)
[![Docker Pulls](https://img.shields.io/docker/pulls/ihmily/douyin-live-recorder?label=Docker%20Pulls&color=blue&logo=docker)](https://hub.docker.com/r/ihmily/douyin-live-recorder/tags)
![GitHub issues](https://img.shields.io/github/issues/ihmily/DouyinLiveRecorder.svg)
[![Latest Release](https://img.shields.io/github/v/release/ihmily/DouyinLiveRecorder)](https://github.com/ihmily/DouyinLiveRecorder/releases/latest)
[![Downloads](https://img.shields.io/github/downloads/ihmily/DouyinLiveRecorder/total)](https://github.com/ihmily/DouyinLiveRecorder/releases/latest)
一款**简易**的可循环值守的直播录制工具基于FFmpeg实现多平台直播源录制支持自定义配置录制以及直播状态推送。
</div>
## 😺已支持平台
- [x] 抖音
- [x] TikTok
- [x] 快手
- [x] 虎牙
- [x] 斗鱼
- [x] YY
- [x] B站
- [x] 小红书
- [x] bigo
- [x] blued
- [x] SOOP(原AfreecaTV)
- [x] 网易cc
- [x] 千度热播
- [x] PandaTV
- [x] 猫耳FM
- [x] Look直播
- [x] WinkTV
- [x] TTingLive(原Flextv)
- [x] PopkonTV
- [x] TwitCasting
- [x] 百度直播
- [x] 微博直播
- [x] 酷狗直播
- [x] TwitchTV
- [x] LiveMe
- [x] 花椒直播
- [x] 流星直播
- [x] ShowRoom
- [x] Acfun
- [x] 映客直播
- [x] 音播直播
- [x] 知乎直播
- [x] CHZZK
- [x] 嗨秀直播
- [x] vv星球直播
- [x] 17Live
- [x] 浪Live
- [x] 畅聊直播
- [x] 飘飘直播
- [x] 六间房直播
- [x] 乐嗨直播
- [x] 花猫直播
- [x] Shopee
- [x] Youtube
- [x] 淘宝
- [x] 京东
- [x] Faceit
- [x] 咪咕
- [x] 连接直播
- [x] 来秀直播
- [x] Picarto
- [ ] 更多平台正在更新中
</div>
## 🎈项目结构
```
.
└── DouyinLiveRecorder/
├── /config -> (config record)
├── /logs -> (save runing log file)
├── /backup_config -> (backup file)
├── /douyinliverecorder -> (package)
├── initializer.py-> (check and install nodejs)
├── spider.py-> (get live data)
├── stream.py-> (get live stream address)
├── utils.py -> (contains utility functions)
├── logger.py -> (logger handdle)
├── room.py -> (get room info)
├── ab_sign.py-> (generate dy token)
├── /javascript -> (some decrypt code)
├── main.py -> (main file)
├── ffmpeg_install.py -> (ffmpeg install script)
├── demo.py -> (call package test demo)
├── msg_push.py -> (send live status update message)
├── ffmpeg.exe -> (record video)
├── index.html -> (play m3u8 and flv video)
├── requirements.txt -> (library dependencies)
├── docker-compose.yaml -> (Container Orchestration File)
├── Dockerfile -> (Application Build Recipe)
├── StopRecording.vbs -> (stop recording script on Windows)
...
```
</div>
## 🌱使用说明
- 对于只想使用录制软件的小白用户,进入[Releases](https://github.com/ihmily/DouyinLiveRecorder/releases) 中下载最新发布的 zip压缩包即可里面有打包好的录制软件。有些电脑可能会报毒直接忽略即可如果下载时被浏览器屏蔽请更换浏览器下载
- 压缩包解压后,在 `config` 文件夹内的 `URL_config.ini` 中添加录制直播间地址,一行一个直播间地址。如果要自定义配置录制,可以修改`config.ini` 文件,推荐将录制格式修改为`ts`
- 以上步骤都做好后,就可以运行`DouyinLiveRecorder.exe` 程序进行录制了。录制的视频文件保存在同目录下的 `downloads` 文件夹内。
- 另外如果需要录制TikTok、AfreecaTV等海外平台请在配置文件中设置开启代理并添加proxy_addr链接 如:`127.0.0.1:7890` (这只是示例地址,具体根据实际填写)。
- 假如`URL_config.ini`文件中添加的直播间地址,有个别直播间暂时不想录制又不想移除链接,可以在对应直播间的链接开头加上`#`,那么将停止该直播间的监测以及录制。
- 软件默认录制清晰度为 `原画` ,如果要单独设置某个直播间的录制画质,可以在添加直播间地址时前面加上画质即可,如`超清https://live.douyin.com/745964462470` 记得中间要有`,` 分隔。
- 如果要长时间挂着软件循环监测直播最好循环时间设置长一点咱也不差没录制到的那几分钟避免因请求频繁导致被官方封禁IP 。
- 要停止直播录制Windows平台可执行StopRecording.vbs脚本文件或者在录制界面使用 `Ctrl+C ` 组合键中断录制,若要停止其中某个直播间的录制,可在`URL_config.ini`文件中的地址前加#,会自动停止对应直播间的录制并正常保存已录制的视频。
- 最后欢迎右上角给本项目一个star同时也非常乐意大家提交pr。
&emsp;
直播间链接示例:
```
抖音:
https://live.douyin.com/745964462470
https://v.douyin.com/iQFeBnt/
https://live.douyin.com/yall1102 (链接+抖音号)
https://v.douyin.com/CeiU5cbX (主播主页地址)
TikTok:
https://www.tiktok.com/@pearlgaga88/live
快手:
https://live.kuaishou.com/u/yall1102
虎牙:
https://www.huya.com/52333
斗鱼:
https://www.douyu.com/3637778?dyshid=
https://www.douyu.com/topic/wzDBLS6?rid=4921614&dyshid=
YY:
https://www.yy.com/22490906/22490906
B站:
https://live.bilibili.com/320
小红书(直播间分享地址):
http://xhslink.com/xpJpfM
bigo直播:
https://www.bigo.tv/cn/716418802
buled直播:
https://app.blued.cn/live?id=Mp6G2R
SOOP:
https://play.sooplive.co.kr/sw7love
网易cc:
https://cc.163.com/583946984
千度热播:
https://qiandurebo.com/web/video.php?roomnumber=33333
PandaTV:
https://www.pandalive.co.kr/live/play/bara0109
猫耳FM:
https://fm.missevan.com/live/868895007
Look直播:
https://look.163.com/live?id=65108820&position=3
WinkTV:
https://www.winktv.co.kr/live/play/anjer1004
FlexTV(TTinglive)::
https://www.flextv.co.kr/channels/593127/live
PopkonTV:
https://www.popkontv.com/live/view?castId=wjfal007&partnerCode=P-00117
https://www.popkontv.com/channel/notices?mcid=wjfal007&mcPartnerCode=P-00117
TwitCasting:
https://twitcasting.tv/c:uonq
百度直播:
https://live.baidu.com/m/media/pclive/pchome/live.html?room_id=9175031377&tab_category
微博直播:
https://weibo.com/l/wblive/p/show/1022:2321325026370190442592
酷狗直播:
https://fanxing2.kugou.com/50428671?refer=2177&sourceFrom=
TwitchTV:
https://www.twitch.tv/gamerbee
LiveMe:
https://www.liveme.com/zh/v/17141543493018047815/index.html
花椒直播:
https://www.huajiao.com/l/345096174
流星直播:
https://www.7u66.com/100960
ShowRoom:
https://www.showroom-live.com/room/profile?room_id=480206 (主播主页地址)
Acfun:
https://live.acfun.cn/live/179922
映客直播:
https://www.inke.cn/liveroom/index.html?uid=22954469&id=1720860391070904
音播直播:
https://live.ybw1666.com/800002949
知乎直播:
https://www.zhihu.com/people/ac3a467005c5d20381a82230101308e9 (主播主页地址)
CHZZK:
https://chzzk.naver.com/live/458f6ec20b034f49e0fc6d03921646d2
嗨秀直播:
https://www.haixiutv.com/6095106
VV星球直播:
https://h5webcdn-pro.vvxqiu.com//activity/videoShare/videoShare.html?h5Server=https://h5p.vvxqiu.com&roomId=LP115924473&platformId=vvstar
17Live:
https://17.live/en/live/6302408
浪Live:
https://www.lang.live/en-US/room/3349463
畅聊直播:
https://live.tlclw.com/106188
飘飘直播:
https://m.pp.weimipopo.com/live/preview.html?uid=91648673&anchorUid=91625862&app=plpl
六间房直播:
https://v.6.cn/634435
乐嗨直播:
https://www.lehaitv.com/8059096
花猫直播:
https://h.catshow168.com/live/preview.html?uid=19066357&anchorUid=18895331
Shopee:
https://sg.shp.ee/GmpXeuf?uid=1006401066&session=802458
Youtube:
https://www.youtube.com/watch?v=cS6zS5hi1w0
淘宝(需cookie):
https://tbzb.taobao.com/live?liveId=532359023188
https://m.tb.cn/h.TWp0HTd
京东:
https://3.cn/28MLBy-E
Faceit:
https://www.faceit.com/zh/players/Compl1/stream
连接直播:
https://show.lailianjie.com/10000258
咪咕直播:
https://www.miguvideo.com/p/live/120000541321
来秀直播:
https://www.imkktv.com/h5/share/video.html?uid=1845195&roomId=1710496
Picarto:
https://www.picarto.tv/cuteavalanche
```
&emsp;
## 🎃源码运行
使用源码运行,可参考下面的步骤。
1.首先拉取或手动下载本仓库项目代码
```bash
git clone https://github.com/ihmily/DouyinLiveRecorder.git
```
2.进入项目文件夹,安装依赖
```bash
cd DouyinLiveRecorder
```
> [!TIP]
> - 不论你是否已安装 **Python>=3.10** 环境, 都推荐使用 [**uv**](https://github.com/astral-sh/uv) 运行, 因为它可以自动管理虚拟环境和方便地管理 **Python** 版本, **不过这完全是可选的**<br />
> 使用以下命令安装
> ```bash
> # 在 macOS 和 Linux 上安装 uv
> curl -LsSf https://astral.sh/uv/install.sh | sh
> ```
> ```powershell
> # 在 Windows 上安装 uv
> powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
> ```
> - 如果安装依赖速度太慢, 你可以考虑使用国内 pip 镜像源:<br />
> 在 `pip` 命令使用 `-i` 参数指定, 如 `pip3 install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple`<br />
> 或者在 `uv` 命令 `--index` 选项指定, 如 `uv sync --index https://pypi.tuna.tsinghua.edu.cn/simple`
<details>
<summary>如果已安装 <b>Python>=3.10</b> 环境</summary>
- :white_check_mark: 在虚拟环境中安装 (推荐)
1. 创建虚拟环境
- 使用系统已安装的 Python, 不使用 uv
```bash
python -m venv .venv
```
- 使用 uv, 默认使用系统 Python, 你可以添加 `--python` 选项指定 Python 版本而不使用系统 Python [uv官方文档](https://docs.astral.sh/uv/concepts/python-versions/)
```bash
uv venv
```
2. 在终端激活虚拟环境 (在未安装 uv 或你想要手动激活虚拟环境时执行, 若已安装 uv, 可以跳过这一步, uv 会自动激活并使用虚拟环境)
**Bash** 中
```bash
source .venv/Scripts/activate
```
**Powershell** 中
```powershell
.venv\Scripts\activate.ps1
```
**Windows CMD** 中
```bat
.venv\Scripts\activate.bat
```
3. 安装依赖
```bash
# 使用 pip (若安装太慢或失败, 可使用 `-i` 指定镜像源)
pip3 install -U pip && pip3 install -r requirements.txt
# 或者使用 uv (可使用 `--index` 指定镜像源)
uv sync
# 或者
uv pip sync requirements.txt
```
- :x: 在系统 Python 环境中安装 (不推荐)
```bash
pip3 install -U pip && pip3 install -r requirements.txt
```
</details>
<details>
<summary>如果未安装 <b>Python>=3.10</b> 环境</summary>
你可以使用 [**uv**](https://github.com/astral-sh/uv) 安装依赖
```bash
# uv 将使用 3.10 及以上的最新 python 发行版自动创建并使用虚拟环境, 可使用 --python 选项指定 python 版本, 参见 https://docs.astral.sh/uv/reference/cli/#uv-sync--python 和 https://docs.astral.sh/uv/reference/cli/#uv-pip-sync--python
uv sync
# 或
uv pip sync requirements.txt
```
</details>
3.安装[FFmpeg](https://ffmpeg.org/download.html#build-linux)如果是Windows系统这一步可跳过。对于Linux系统执行以下命令安装
CentOS执行
```bash
yum install epel-release
yum install ffmpeg
```
Ubuntu则执行
```bash
apt update
apt install ffmpeg
```
macOS 执行
**如果已经安装 Homebrew 请跳过这一步**
```bash
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
```
```bash
brew install ffmpeg
```
4.运行程序
```python
python main.py
```
```bash
uv run main.py
```
其中Linux系统请使用`python3 main.py` 运行。
&emsp;
## 🐋容器运行
在运行命令之前,请确保您的机器上安装了 [Docker](https://docs.docker.com/get-docker/) 和 [Docker Compose](https://docs.docker.com/compose/install/)
1.快速启动
最简单方法是运行项目中的 [docker-compose.yaml](https://github.com/ihmily/DouyinLiveRecorder/blob/main/docker-compose.yaml) 文件,只需简单执行以下命令:
```bash
docker-compose up
```
可选 `-d` 在后台运行。
2.构建镜像(可选)
如果你只想简单的运行程序则不需要做这一步。Docker镜像仓库中代码版本可能不是最新的如果要运行本仓库主分支最新代码可以本地自定义构建通过修改 [docker-compose.yaml](https://github.com/ihmily/DouyinLiveRecorder/blob/main/docker-compose.yaml) 文件,如将镜像名修改为 `douyin-live-recorder:latest`,并取消 `# build: .` 注释,然后再执行
```bash
docker build -t douyin-live-recorder:latest .
docker-compose up
```
或者直接使用下面命令进行构建并启动
```bash
docker-compose -f docker-compose.yaml up
```
3.停止容器实例
```bash
docker-compose stop
```
4.注意事项
①在docker容器内运行本程序之前请先在配置文件中添加要录制的直播间地址。
②在容器内时,如果手动中断容器运行停止录制,会导致正在录制的视频文件损坏!
**无论哪种运行方式,为避免手动中断或者异常中断导致录制的视频文件损坏的情况,推荐使用 `ts` 格式保存**。
&emsp;
## 🤖相关项目
- StreamCap: https://github.com/ihmily/StreamCap
- streamget: https://github.com/ihmily/streamget
&emsp;
## ❤️贡献者
&ensp;&ensp; [![Hmily](https://github.com/ihmily.png?size=50)](https://github.com/ihmily)
[![iridescentGray](https://github.com/iridescentGray.png?size=50)](https://github.com/iridescentGray)
[![annidy](https://github.com/annidy.png?size=50)](https://github.com/annidy)
[![wwkk2580](https://github.com/wwkk2580.png?size=50)](https://github.com/wwkk2580)
[![missuo](https://github.com/missuo.png?size=50)](https://github.com/missuo)
<a href="https://github.com/xueli12" target="_blank"><img src="https://github.com/xueli12.png?size=50" alt="xueli12" style="width:53px; height:51px;" /></a>
<a href="https://github.com/kaine1973" target="_blank"><img src="https://github.com/kaine1973.png?size=50" alt="kaine1973" style="width:53px; height:51px;" /></a>
<a href="https://github.com/yinruiqing" target="_blank"><img src="https://github.com/yinruiqing.png?size=50" alt="yinruiqing" style="width:53px; height:51px;" /></a>
<a href="https://github.com/Max-Tortoise" target="_blank"><img src="https://github.com/Max-Tortoise.png?size=50" alt="Max-Tortoise" style="width:53px; height:51px;" /></a>
[![justdoiting](https://github.com/justdoiting.png?size=50)](https://github.com/justdoiting)
[![dhbxs](https://github.com/dhbxs.png?size=50)](https://github.com/dhbxs)
[![wujiyu115](https://github.com/wujiyu115.png?size=50)](https://github.com/wujiyu115)
[![zhanghao333](https://github.com/zhanghao333.png?size=50)](https://github.com/zhanghao333)
<a href="https://github.com/gyc0123" target="_blank"><img src="https://github.com/gyc0123.png?size=50" alt="gyc0123" style="width:53px; height:51px;" /></a>
&ensp;&ensp; [![HoratioShaw](https://github.com/HoratioShaw.png?size=50)](https://github.com/HoratioShaw)
[![nov30th](https://github.com/nov30th.png?size=50)](https://github.com/nov30th)
[![727155455](https://github.com/727155455.png?size=50)](https://github.com/727155455)
[![nixingshiguang](https://github.com/nixingshiguang.png?size=50)](https://github.com/nixingshiguang)
[![1411430556](https://github.com/1411430556.png?size=50)](https://github.com/1411430556)
[![Ovear](https://github.com/Ovear.png?size=50)](https://github.com/Ovear)
&emsp;
## ⏳提交日志
- 20251024
- 修复抖音风控无法获取数据问题
- 新增soop.com录制支持
- 修复bigo录制
- 20250127
- 新增淘宝、京东、faceit直播录制
- 修复小红书直播流录制以及转码问题
- 修复畅聊、VV星球、flexTV直播录制
- 修复批量微信直播推送
- 新增email发送ssl和port配置
- 新增强制转h264配置
- 更新ffmpeg版本
- 重构包为异步函数!
- 20241130
- 新增shopee、youtube直播录制
- 新增支持自定义m3u8、flv地址录制
- 新增自定义执行脚本支持python、bat、bash等
- 修复YY直播、花椒直播和小红书直播录制
- 修复b站标题获取错误
- 修复log日志错误
- 20241030
- 新增嗨秀直播、vv星球直播、17Live、浪Live、SOOP、畅聊直播(原时光直播)、飘飘直播、六间房直播、乐嗨直播、花猫直播等10个平台直播录制
- 修复小红书直播录制,支持小红书作者主页地址录制直播
- 新增支持ntfy消息推送以及新增支持批量推送多个地址逗号分隔多个推送地址)
- 修复Liveme直播录制、twitch直播录制
- 新增Windows平台一键停止录制VB脚本程序
- 20241005
- 新增邮箱和Bark推送
- 新增直播注释停止录制
- 优化分段录制
- 重构部分代码
- 20240928
- 新增知乎直播、CHZZK直播录制
- 修复音播直播录制
- 20240903
- 新增抖音双屏录制、音播直播录制
- 修复PandaTV、bigo直播录制
- 20240713
- 新增映客直播录制
- 20240705
- 新增时光直播录制
- 20240701
- 修复虎牙直播录制2分钟断流问题
- 新增自定义直播推送内容
- 20240621
- 新增Acfun、ShowRoom直播录制
- 修复微博录制、新增直播源线路
- 修复斗鱼直播60帧录制
- 修复酷狗直播录制
- 修复TikTok部分无法解析直播源
- 修复抖音无法录制连麦直播
- 20240510
- 修复部分虎牙直播间录制错误
- 20240508
- 修复花椒直播录制
- 更改文件路径解析方式 [@kaine1973](https://github.com/kaine1973)
- 20240506
- 修复抖音录制画质解析bug
- 修复虎牙录制 60帧最高画质问题
- 新增流星直播录制
- 20240427
- 新增LiveMe、花椒直播录制
- 20240425
- 新增TwitchTV直播录制
- 20240424
- 新增酷狗直播录制、优化PopkonTV直播录制
- 20240423
- 新增百度直播录制、微博直播录制
- 修复斗鱼录制直播回放的问题
- 新增直播源地址显示以及输出到日志文件设置
- 20240311
- 修复海外平台录制bug增加画质选择增强录制稳定性
- 修复虎牙录制bug (虎牙`一起看`频道 有特殊限制,有时无法录制)
- 20240309
- 修复虎牙直播、小红书直播和B站直播录制
- 新增5个直播平台录制包括winktv、flextv、look、popkontv、twitcasting
- 新增部分海外平台账号密码配置实现自动登录并更新配置文件中的cookie
- 新增自定义配置需要使用代理录制的平台
- 新增只推送开播消息不进行录制设置
- 修复了一些bug
- 20240209
- 优化AfreecaTV录制新增账号密码登录获取cookie以及持久保存
- 修复了小红书直播因官方更新直播域名,导致无法录制直播的问题
- 修复了更新URL配置文件的bug
- 最后,祝大家新年快乐!
<details><summary>点击展开更多提交日志</summary>
- 20240129
- 新增猫耳FM直播录制
- 20240127
- 新增千度热播直播录制、新增pandaTV(韩国)直播录制
- 新增telegram直播状态消息推送修复了某些bug
- 新增自定义设置不同直播间的录制画质(即每个直播间录制画质可不同)
- 修改录制视频保存路径为 `downloads` 文件夹,并且分平台进行保存。
- 20240114
- 新增网易cc直播录制优化ffmpeg参数修改AfreecaTV输入直播地址格式
- 修改日志记录器 @[iridescentGray](https://github.com/iridescentGray)
- 20240102
- 修复Linux上运行新增docker配置文件
- 20231210
- 修复录制分段bug修复bigo录制检测bug
- 新增自定义修改录制主播名
- 新增AfreecaTV直播录制修复某些可能会发生的bug
- 20231207
- 新增blued直播录制修复YY直播录制新增直播结束消息推送
- 20231206
- 新增bigo直播录制
- 20231203
- 新增小红书直播录制(全网首发),目前小红书官方没有切换清晰度功能,因此直播录制也只有默认画质
- 小红书录制暂时无法循环监测,每次主播开启直播,都要重新获取一次链接
- 获取链接的方式为 将直播间转发到微信,在微信中打开后,复制页面的链接。
- 20231030
- 本次更新只是进行修复,没时间新增功能。
- 欢迎各位大佬提pr 帮忙更新维护
- 20230930
- 新增抖音从接口获取直播流,增强稳定性
- 修改快手获取直播流的方式,改用从官方接口获取
- 祝大家中秋节快乐!
- 20230919
- 修复了快手版本更新后录制出错的问题增加了其自动获取cookie(~~稳定性未知~~)
- 修复了TikTok显示正在直播但不进行录制的问题
- 20230907
- 修复了因抖音官方更新了版本导致的录制出错以及短链接转换出错
- 修复B站无法录制原画视频的bug
- 修改了配置文件字段新增各平台自定义设置Cookie
- 20230903
- 修复了TikTok录制时报644无法录制的问题
- 新增直播状态推送到钉钉和微信的功能,如有需要请看 [设置推送教程](https://d04vqdiqwr3.feishu.cn/docx/XFPwdDDvfobbzlxhmMYcvouynDh?from=from_copylink)
- 最近比较忙,其他问题有时间再更新
- 20230816
- 修复斗鱼直播(官方更新了字段)和快手直播录制出错的问题
- 20230814
- 新增B站直播录制
- 写了一个在线播放M3U8和FLV视频的网页源码打开即可食用
- 20230812
- 新增YY直播录制
- 20230808
- 修复主播重新开播无法再次录制的问题
- 20230807
- 新增了斗鱼直播录制
- 修复显示录制完成之后会重新开始录制的问题
- 20230805
- 新增了虎牙直播录制其暂时只能用flv视频流进行录制
- Web API 新增了快手和虎牙这两个平台的直播流解析TikTok要代理
- 20230804
- 新增了快手直播录制,优化了部分代码
- 上传了一个自动化获取抖音直播间页面Cookie的代码可以用于录制
- 20230803
- 通宵更新
- 新增了国际版抖音TikTok的直播录制去除冗余 简化了部分代码
- 20230724
- 新增了一个通过抖音直播间地址获取直播视频流链接的API接口上传即可用
</details>
&emsp;
## 有问题可以提issue, 我会在这里持续添加更多直播平台的录制 欢迎Star
####

69
StopRecording.vbs Normal file
View File

@@ -0,0 +1,69 @@
'********************************************************************************************/
'* File Name : StopRecording.vbs
'* Created Date : 2024-10-15 01:50:30
'* Author : Hmily
'* GitHub : http://github.com/ihmily
'* Description : This script is designed to terminate the process of live recording
'********************************************************************************************/
Dim objWMIService, colProcesses, objProcess
Dim intResponse
strComputer = "."
On Error Resume Next
intResponse = MsgBox("确定要结束所有后台直播录制进程吗?", vbYesNo + vbQuestion, "确认结束进程")
If intResponse = vbYes Then
Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")
If Err.Number <> 0 Then
Err.Clear
End If
Set colProcesses = objWMIService.ExecQuery("Select * from Win32_Process Where Name = 'ffmpeg.exe'")
Set colProcesses2 = objWMIService.ExecQuery("Select * from Win32_Process Where Name = 'pythonw.exe'")
Set colProcesses3 = objWMIService.ExecQuery("Select * from Win32_Process Where Name = 'DouyinLiveRecorder.exe'")
If Err.Number <> 0 Then
Err.Clear
End If
If Not objWMIService Is Nothing And Not colProcesses Is Nothing And Not colProcesses2 Is Nothing Then
If colProcesses2.Count = 0 And colProcesses3.Count = 0 Then
MsgBox "没有找到录制程序的进程", vbExclamation, "提示信息"
WScript.Quit(1)
Else
For Each objProcess in colProcesses
objProcess.Terminate()
If Err.Number <> 0 Then
objShell.Run "taskkill /f /im " & objProcess.Name, 0, True
Err.Clear
End If
Next
End If
Else
objShell.Run "taskkill /f /im " & objProcess.Name, 0, True
End If
MsgBox "已成功结束正在录制直播的进程!" & vbCrLf & "关闭此窗口30秒后自动停止录制程序", vbInformation, "提示信息"
WScript.Sleep 10000
If colProcesses3.Count <> 0 Then
Set colProcesses_ = colProcesses3
Else
Set colProcesses_ = colProcesses2
End If
For Each objProcess in colProcesses_
objProcess.Terminate()
If Err.Number <> 0 Then
objShell.Run "taskkill /f /im " & objProcess.Name, 0, True
Err.Clear
End If
Next
Else
MsgBox "已取消结束录制操作", vbExclamation, "提示信息"
End If
On Error GoTo 0
Set objWMIService = Nothing
Set colProcesses = Nothing
Set colProcesses2 = Nothing
Set colProcesses3 = Nothing
Set objProcess = Nothing
Set objShell = Nothing

2255
main.py Normal file

File diff suppressed because it is too large Load Diff

295
msg_push.py Normal file
View File

@@ -0,0 +1,295 @@
# -*- coding: utf-8 -*-
"""
Author: Hmily
GitHub: https://github.com/ihmily
Date: 2023-09-03 19:18:36
Update: 2025-01-23 17:16:12
Copyright (c) 2023-2024 by Hmily, All Rights Reserved.
"""
from typing import Dict, Any
import json
import base64
import urllib.request
import urllib.error
import smtplib
from email.header import Header
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
no_proxy_handler = urllib.request.ProxyHandler({})
opener = urllib.request.build_opener(no_proxy_handler)
headers: Dict[str, str] = {'Content-Type': 'application/json'}
def dingtalk(url: str, content: str, number: str = None, is_atall: bool = False) -> Dict[str, Any]:
success = []
error = []
api_list = url.replace('', ',').split(',') if url.strip() else []
for api in api_list:
json_data = {
'msgtype': 'text',
'text': {
'content': content,
},
"at": {
"atMobiles": [
number
],
"isAtAll": is_atall
},
}
try:
data = json.dumps(json_data).encode('utf-8')
req = urllib.request.Request(api, data=data, headers=headers)
response = opener.open(req, timeout=10)
json_str = response.read().decode('utf-8')
json_data = json.loads(json_str)
if json_data['errcode'] == 0:
success.append(api)
else:
error.append(api)
print(f'钉钉推送失败, 推送地址:{api}, {json_data["errmsg"]}')
except Exception as e:
error.append(api)
print(f'钉钉推送失败, 推送地址:{api}, 错误信息:{e}')
return {"success": success, "error": error}
def xizhi(url: str, title: str, content: str) -> Dict[str, Any]:
success = []
error = []
api_list = url.replace('', ',').split(',') if url.strip() else []
for api in api_list:
json_data = {
'title': title,
'content': content
}
try:
data = json.dumps(json_data).encode('utf-8')
req = urllib.request.Request(api, data=data, headers=headers)
response = opener.open(req, timeout=10)
json_str = response.read().decode('utf-8')
json_data = json.loads(json_str)
if json_data['code'] == 200:
success.append(api)
else:
error.append(api)
print(f'微信推送失败, 推送地址:{api}, 失败信息:{json_data["msg"]}')
except Exception as e:
error.append(api)
print(f'微信推送失败, 推送地址:{api}, 错误信息:{e}')
return {"success": success, "error": error}
def send_email(email_host: str, login_email: str, email_pass: str, sender_email: str, sender_name: str,
to_email: str, title: str, content: str, smtp_port: str = None, open_ssl: bool = True) -> Dict[str, Any]:
receivers = to_email.replace('', ',').split(',') if to_email.strip() else []
try:
message = MIMEMultipart()
send_name = base64.b64encode(sender_name.encode("utf-8")).decode()
message['From'] = f'=?UTF-8?B?{send_name}?= <{sender_email}>'
message['Subject'] = Header(title, 'utf-8')
if len(receivers) == 1:
message['To'] = receivers[0]
t_apart = MIMEText(content, 'plain', 'utf-8')
message.attach(t_apart)
if open_ssl:
smtp_port = int(smtp_port) or 465
smtp_obj = smtplib.SMTP_SSL(email_host, smtp_port)
else:
smtp_port = int(smtp_port) or 25
smtp_obj = smtplib.SMTP(email_host, smtp_port)
smtp_obj.login(login_email, email_pass)
smtp_obj.sendmail(sender_email, receivers, message.as_string())
return {"success": receivers, "error": []}
except smtplib.SMTPException as e:
print(f'邮件推送失败, 推送邮箱:{to_email}, 错误信息:{e}')
return {"success": [], "error": receivers}
def tg_bot(chat_id: int, token: str, content: str) -> Dict[str, Any]:
try:
json_data = {
"chat_id": chat_id,
'text': content
}
url = f'https://api.telegram.org/bot{token}/sendMessage'
data = json.dumps(json_data).encode('utf-8')
req = urllib.request.Request(url, data=data, headers=headers)
response = urllib.request.urlopen(req, timeout=15)
json_str = response.read().decode('utf-8')
_json_data = json.loads(json_str)
return {"success": [1], "error": []}
except Exception as e:
print(f'tg推送失败, 聊天ID{chat_id}, 错误信息:{e}')
return {"success": [], "error": [1]}
def bark(api: str, title: str = "message", content: str = 'test', level: str = "active",
badge: int = 1, auto_copy: int = 1, sound: str = "", icon: str = "", group: str = "",
is_archive: int = 1, url: str = "") -> Dict[str, Any]:
success = []
error = []
api_list = api.replace('', ',').split(',') if api.strip() else []
for _api in api_list:
json_data = {
"title": title,
"body": content,
"level": level,
"badge": badge,
"autoCopy": auto_copy,
"sound": sound,
"icon": icon,
"group": group,
"isArchive": is_archive,
"url": url
}
try:
data = json.dumps(json_data).encode('utf-8')
req = urllib.request.Request(_api, data=data, headers=headers)
response = opener.open(req, timeout=10)
json_str = response.read().decode("utf-8")
json_data = json.loads(json_str)
if json_data['code'] == 200:
success.append(_api)
else:
error.append(_api)
print(f'Bark推送失败, 推送地址:{_api}, 失败信息:{json_data["message"]}')
except Exception as e:
error.append(api)
print(f'Bark推送失败, 推送地址:{_api}, 错误信息:{e}')
return {"success": success, "error": error}
def ntfy(api: str, title: str = "message", content: str = 'test', tags: str = 'tada', priority: int = 3,
action_url: str = "", attach: str = "", filename: str = "", click: str = "", icon: str = "",
delay: str = "", email: str = "", call: str = "") -> Dict[str, Any]:
success = []
error = []
api_list = api.replace('', ',').split(',') if api.strip() else []
tags = tags.replace('', ',').split(',') if tags else ['partying_face']
actions = [{"action": "view", "label": "view live", "url": action_url}] if action_url else []
for _api in api_list:
server, topic = _api.rsplit('/', maxsplit=1)
json_data = {
"topic": topic,
"title": title,
"message": content,
"tags": tags,
"priority": priority,
"attach": attach,
"filename": filename,
"click": click,
"actions": actions,
"markdown": False,
"icon": icon,
"delay": delay,
"email": email,
"call": call
}
try:
data = json.dumps(json_data, ensure_ascii=False).encode('utf-8')
req = urllib.request.Request(server, data=data, headers=headers)
response = opener.open(req, timeout=10)
json_str = response.read().decode("utf-8")
json_data = json.loads(json_str)
if "error" not in json_data:
success.append(_api)
else:
error.append(_api)
print(f'ntfy推送失败, 推送地址:{_api}, 失败信息:{json_data["error"]}')
except urllib.error.HTTPError as e:
error.append(_api)
error_msg = e.read().decode("utf-8")
print(f'ntfy推送失败, 推送地址:{_api}, 错误信息:{json.loads(error_msg)["error"]}')
except Exception as e:
error.append(api)
print(f'ntfy推送失败, 推送地址:{_api}, 错误信息:{e}')
return {"success": success, "error": error}
def pushplus(token: str, title: str, content: str) -> Dict[str, Any]:
"""
PushPlus推送通知
API文档: https://www.pushplus.plus/doc/
"""
success = []
error = []
token_list = token.replace('', ',').split(',') if token.strip() else []
for _token in token_list:
json_data = {
'token': _token,
'title': title,
'content': content
}
try:
url = 'https://www.pushplus.plus/send'
data = json.dumps(json_data).encode('utf-8')
req = urllib.request.Request(url, data=data, headers=headers)
response = opener.open(req, timeout=10)
json_str = response.read().decode('utf-8')
json_data = json.loads(json_str)
if json_data.get('code') == 200:
success.append(_token)
else:
error.append(_token)
print(f'PushPlus推送失败, Token{_token}, 失败信息:{json_data.get("msg", "未知错误")}')
except Exception as e:
error.append(_token)
print(f'PushPlus推送失败, Token{_token}, 错误信息:{e}')
return {"success": success, "error": error}
if __name__ == '__main__':
send_title = '直播通知' # 标题
send_content = '张三 开播了!' # 推送内容
# 钉钉推送通知
webhook_api = '' # 替换成自己Webhook链接,参考文档https://open.dingtalk.com/document/robots/custom-robot-access
phone_number = '' # 被@用户的手机号码
is_atall = '' # 是否@全体
# dingtalk(webhook_api, send_content, phone_number)
# 微信推送通知
# 替换成自己的单点推送接口,获取地址https://xz.qqoq.net/#/admin/one
# 当然也可以使用其他平台API 如server酱 使用方法一样
xizhi_api = 'https://xizhi.qqoq.net/xxxxxxxxx.send'
# xizhi(xizhi_api, send_content)
# telegram推送通知
tg_token = '' # tg搜索"BotFather"获取的token值
tg_chat_id = 000000 # tg搜索"userinfobot"获取的chat_id值即可发送推送消息给你自己如果下面的是群组id则发送到群
# tg_bot(tg_chat_id, tg_token, send_content)
# email_message(
# email_host="smtp.qq.com",
# login_email="",
# email_pass="",
# sender_email="",
# sender_name="",
# to_email="",
# title="",
# content="",
# )
bark_url = 'https://xxx.xxx.com/key/'
# bark(bark_url, send_title, send_content)
ntfy(
api="https://ntfy.sh/xxxxx",
title="直播推送",
content="xxx已开播",
)
# PushPlus推送通知
pushplus_token = '' # 替换成自己的PushPlus Token获取地址https://www.pushplus.plus/
# pushplus(pushplus_token, send_title, send_content)

23
pyproject.toml Normal file
View File

@@ -0,0 +1,23 @@
[project]
name = "DouyinLiveRecorder"
version = "4.0.7"
description = "可循环值守和多人录制的直播录制软件, 支持抖音、TikTok、Youtube、快手、虎牙、斗鱼、B站、小红书、pandatv、sooplive、flextv、popkontv、twitcasting、winktv、百度、微博、酷狗、17Live、Twitch、Acfun、CHZZK、shopee等40+平台直播录制"
readme = "README.md"
authors = [{name = "Hmily"}]
license = { text = "MIT" }
requires-python = ">=3.10"
dependencies = [
"requests>=2.31.0",
"loguru>=0.7.3",
"pycryptodome>=3.20.0",
"distro>=1.9.0",
"tqdm>=4.67.1",
"httpx[http2]>=0.28.1",
"PyExecJS>=1.5.1"
]
[project.urls]
"Homepage" = "https://github.com/ihmily/DouyinLiveRecorder"
"Documentation" = "https://github.com/ihmily/DouyinLiveRecorder"
"Repository" = "https://github.com/ihmily/DouyinLiveRecorder"
"Issues" = "https://github.com/ihmily/DouyinLiveRecorder/issues"

7
requirements.txt Normal file
View File

@@ -0,0 +1,7 @@
requests>=2.31.0
loguru>=0.7.3
pycryptodome>=3.20.0
distro>=1.9.0
tqdm>=4.67.1
httpx[http2]>=0.28.1
PyExecJS>=1.5.1

227
robust_relay.py Normal file
View File

@@ -0,0 +1,227 @@
# robust_relay.py
# 高稳定性 YouTube 转播模块(防误判下播 + 看门狗 + 自动重连 + token刷新
# 作者:融合 A+B 代码精华,专为抖音/抖音国际版长期无人值守转播设计
import subprocess
import time
import threading
import os
import signal
import datetime
from typing import List, Optional, Callable
# ==================== 配置常量(可全局覆盖) ====================
WATCHDOG_SILENCE = 600 # 10分钟无输出 → 软重启
WATCHDOG_GRACE = 120 # 启动宽限期
REFRESH_URL_EVERY = 300 # 每5分钟强制刷新 real_url防token过期
OFFLINE_DEBOUNCE = 3 # 连续3次API返回下播才认为真下播
OFFLINE_WINDOW = 120 # 最近120秒有活跃就忽略下播信号
SOFT_RESTART_COOLDOWN = 90 # 软重启最小间隔
# ==================== 轻量探活 ====================
def _probe_alive(url: str, timeout: float = 5.0) -> bool:
if not url:
return False
try:
import urllib.request, contextlib
opener = urllib.request.build_opener()
if hasattr(urllib.request, 'getproxies'):
proxy = urllib.request.getproxies().get('http')
if proxy:
opener.add_handler(urllib.request.ProxyHandler({"http": proxy, "https": proxy}))
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"}, method="HEAD")
with contextlib.closing(opener.open(req, timeout=timeout)) as r:
if r.code >= 400:
return False
head = r.read(256)
if url.lower().endswith(".m3u8"):
return b"#EXTM3U" in head.upper()
if url.lower().endswith(".flv"):
return head.startswith(b"FLV")
return len(head) > 0
except:
return False
# ==================== 增强版 ffmpeg 启动器 ====================
def _launch_ffmpeg(
input_url: str,
youtube_rtmp: str,
anchor_name: str,
logger: Callable[[str], None],
extra_cmd: List[str] = None
) -> subprocess.Popen:
cmd = [
"ffmpeg", "-nostdin", "-hide_banner", "-stats", "-stats_period", "60", "-loglevel", "info",
# 输入端超强重连
"-reconnect", "1", "-reconnect_streamed", "1", "-reconnect_at_eof", "1",
"-reconnect_delay_max", "30", "-rw_timeout", "20000000",
# 极致低延迟 + 时间戳修复
"-fflags", "+genpts+igndts+flush_packets", "-flags", "+global_header",
"-thread_queue_size", "4096", "-analyzeduration", "0", "-probesize", "32",
"-i", input_url,
# copy 模式(质量无损)
"-c:v", "copy", "-c:a", "copy", "-bsf:a", "aac_adtstoasc",
"-map", "0:v?", "-map", "0:a?",
# YouTube 强制参数
"-f", "mpegts", "-g", "2", "-rtmp_buffer", "1000", "-rtmp_live", "live",
"-max_delay", "0", "-tune", "zerolatency", "-avoid_negative_ts", "make_zero",
"-muxdelay", "0", "-muxpreload", "0",
"-fflags", "+nobuffer+flush_packets", "-avioflags", "direct",
]
if extra_cmd:
cmd.extend(extra_cmd)
cmd.append(youtube_rtmp)
creationflags = subprocess.CREATE_NEW_PROCESS_GROUP if os.name == "nt" else 0
proc = subprocess.Popen(
cmd,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
bufsize=0,
creationflags=creationflags
)
# 用于看门狗
proc.last_output_ts = time.time()
def _reader():
try:
for line in proc.stdout: # type: ignore
if not line:
break
text = line.decode(errors="ignore").rstrip()
proc.last_output_ts = time.time()
if any(k in text for k in ["bitrate=", "frame=", "speed=", "fps="]):
logger(f"[{anchor_name}] {text}")
except:
pass
threading.Thread(target=_reader, daemon=True).start()
return proc
# ==================== 主函数:高稳定性转播(替代原 check_subprocess ====================
def robust_youtube_relay(
anchor_name: str,
real_url: str,
youtube_rtmp: str,
logger: Callable[[str], None],
get_latest_stream_url: Callable[[], Optional[str]], # 用于刷新 real_url如有token过期
is_stream_still_live: Callable[[], bool], # 检查主播是否在播API
extra_ffmpeg_args: List[str] = None, # 可选:加 -f segment 分段保存
heartbeat_callback: Callable[[int], None] = None # 可选:每分钟回调已运行秒数
) -> bool:
"""
返回值:
True = 主播已下播(正常结束)
False = 发生未知错误(建议计数错误次数)
"""
proc: Optional[subprocess.Popen] = None
start_time = time.time()
last_refresh = 0.0
last_soft_restart = 0.0
offline_count = 0
last_live_ts = time.time()
try:
proc = _launch_ffmpeg(real_url, youtube_rtmp, anchor_name, logger, extra_ffmpeg_args)
logger(f"[{anchor_name}] 已开始高稳定性转播 → {youtube_rtmp.split('/')[-1]}")
while True:
now = time.time()
elapsed = int(now - start_time)
silence = now - proc.last_output_ts if proc else 0
# 1. 进程意外退出 → 重启
if proc and proc.poll() is not None:
logger(f"[{anchor_name}] ffmpeg 意外退出3秒后重启...")
time.sleep(3)
proc = _launch_ffmpeg(real_url, youtube_rtmp, anchor_name, logger, extra_ffmpeg_args)
start_time = time.time()
continue
# 2. 看门狗:长时间无输出 → 软重启
if elapsed > WATCHDOG_GRACE and silence > WATCHDOG_SILENCE:
if now - last_soft_restart > SOFT_RESTART_COOLDOWN:
logger(f"[{anchor_name}] 检测到卡顿({silence:.0f}s无输出软重启 ffmpeg...")
last_soft_restart = now
if proc and proc.stdin:
proc.stdin.write(b'q')
proc.stdin.flush()
time.sleep(4)
proc = _launch_ffmpeg(real_url, youtube_rtmp, anchor_name, logger, extra_ffmpeg_args)
start_time = time.time()
continue
# 3. 定期刷新播放地址防token过期
if now - last_refresh > REFRESH_URL_EVERY:
new_url = get_latest_stream_url()
if new_url and new_url != real_url:
logger(f"[{anchor_name}] 检测到新的播放地址,切换中...")
real_url = new_url
if proc and proc.stdin:
proc.stdin.write(b'q')
time.sleep(3)
proc = _launch_ffmpeg(real_url, youtube_rtmp, anchor_name, logger, extra_ffmpeg_args)
start_time = time.time()
last_refresh = now
# 4. 下播防抖判定(核心防误判)
try:
still_live = is_stream_still_live()
except:
still_live = True # 接口异常不判下播
if not still_live:
# 交叉验证:流还能访问 → 忽略API误判
if _probe_alive(real_url):
logger(f"[{anchor_name}] API判下播但流仍活跃忽略本次误判")
still_live = True
last_live_ts = now
if still_live:
offline_count = 0
last_live_ts = now
else:
if now - last_live_ts < OFFLINE_WINDOW:
logger(f"[{anchor_name}] 短期掉线,忽略")
else:
offline_count += 1
if offline_count >= OFFLINE_DEBOUNCE:
logger(f"[{anchor_name}] 连续{offline_count}次确认下播,结束转播")
if proc and proc.stdin:
proc.stdin.write(b'q')
return True
else:
logger(f"[{anchor_name}] 疑似下播 {offline_count}/{OFFLINE_DEBOUNCE},继续观察")
# 5. 心跳日志 & 回调
if elapsed % 60 < 2:
h, m = divmod(elapsed // 60, 60)
logger(f"[{anchor_name}] 正在转播 {h:02d}:{m:02d}")
if heartbeat_callback:
heartbeat_callback(elapsed)
time.sleep(8)
except KeyboardInterrupt:
logger(f"[{anchor_name}] 手动终止转播")
return True
except Exception as e:
logger(f"[{anchor_name}] 严重错误: {e}")
return False
finally:
if proc:
try:
proc.stdin.write(b'q') if proc.stdin else None
proc.wait(timeout=8)
except:
proc.kill()

111
wending Normal file
View File

@@ -0,0 +1,111 @@
try:
if split_video_by_time:
now = time.strftime("%Y-%m-%d_%H-%M-%S", time.localtime())
print(f"{rec_info} 开始推流到 YouTube: {anchor_name}_{title_in_name}_{now}")
YT_STREAM_KEY = "ue78-1c3e-mr9g-14mz-9r4z"
youtube_rtmp = f"rtmp://a.rtmp.youtube.com/live2/{YT_STREAM_KEY}"
# ------------------------
# NVENC 检测
# ------------------------
codec_v = "libx264"
preset_v = "veryfast"
tune_param = ["-tune", "zerolatency"]
try:
if platform.system().lower() in ["windows", "linux"]:
check = subprocess.run(["nvidia-smi"], capture_output=True, timeout=10)
if check.returncode == 0:
enc = subprocess.run(["ffmpeg", "-hide_banner", "-encoders"],
capture_output=True, text=True, timeout=10)
if "h264_nvenc" in enc.stdout:
codec_v = "h264_nvenc"
preset_v = "p5"
tune_param = ["-tune", "ll", "-rc", "vbr"]
print("检测到 NVIDIA GPU → 使用 NVENC 硬件加速")
except:
pass # 异常自动降级
# ------------------------
# FFmpeg 命令(稳定推流)
# ------------------------
ffmpeg_command = [
"ffmpeg",
"-re",
"-i", real_url,
"-vf", "scale=720:1280:force_original_aspect_ratio=decrease,pad=720:1280:(ow-iw)/2:(oh-ih)/2:black",
"-c:v", codec_v,
"-preset", preset_v,
*tune_param,
"-b:v", "2500k",
"-maxrate", "2500k",
"-bufsize", "5000k",
"-g", "50",
"-r", "25",
"-pix_fmt", "yuv420p",
"-bf", "0",
"-c:a", "aac",
"-b:a", "128k",
"-ar", "44100",
"-ac", "2",
"-af", "aresample=async=1:first_pts=0",
"-flags", "+low_delay",
"-fflags", "+genpts",
"-thread_queue_size", "512",
"-threads", "6",
"-f", "flv",
youtube_rtmp
]
# ------------------------
# 永不断流重连逻辑
# ------------------------
proc = None
while True:
try:
if proc and proc.poll() is None:
proc.terminate()
try:
proc.wait(timeout=8)
except:
proc.kill()
print(f"{anchor_name}_{title_in_name}_{now} → 启动 YouTube 推流...")
proc = subprocess.Popen(
ffmpeg_command,
stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
# 打印 FFmpeg 错误日志,便于排查
while True:
line = proc.stderr.readline()
if not line:
break
line = line.strip()
if line:
print(f"FFmpeg: {line}")
if proc.poll() is not None:
break
code = proc.returncode
if code != 0:
print(f"FFmpeg 退出code={code}5秒后重连...")
time.sleep(5)
except Exception as e:
print(f"启动 FFmpeg 异常: {e}")
time.sleep(5)
except Exception as e:
print(f"外层异常: {e}")