's'
This commit is contained in:
679
README.md
Normal file
679
README.md
Normal file
@@ -0,0 +1,679 @@
|
||||

|
||||
|
||||
## 💡简介
|
||||
[](https://www.python.org/downloads/release/python-3116/)
|
||||
[](https://github.com/ihmily/DouyinLiveRecorder)
|
||||
[](https://hub.docker.com/r/ihmily/douyin-live-recorder/tags)
|
||||

|
||||
[](https://github.com/ihmily/DouyinLiveRecorder/releases/latest)
|
||||
[](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。
|
||||
|
||||
 
|
||||
|
||||
直播间链接示例:
|
||||
|
||||
```
|
||||
抖音:
|
||||
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
|
||||
```
|
||||
|
||||
 
|
||||
|
||||
## 🎃源码运行
|
||||
使用源码运行,可参考下面的步骤。
|
||||
|
||||
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` 运行。
|
||||
|
||||
 
|
||||
## 🐋容器运行
|
||||
|
||||
在运行命令之前,请确保您的机器上安装了 [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` 格式保存**。
|
||||
|
||||
 
|
||||
|
||||
## 🤖相关项目
|
||||
|
||||
- StreamCap: https://github.com/ihmily/StreamCap
|
||||
- streamget: https://github.com/ihmily/streamget
|
||||
|
||||
 
|
||||
|
||||
## ❤️贡献者
|
||||
|
||||
   [](https://github.com/ihmily)
|
||||
[](https://github.com/iridescentGray)
|
||||
[](https://github.com/annidy)
|
||||
[](https://github.com/wwkk2580)
|
||||
[](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>
|
||||
[](https://github.com/justdoiting)
|
||||
[](https://github.com/dhbxs)
|
||||
[](https://github.com/wujiyu115)
|
||||
[](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>
|
||||
|
||||
   [](https://github.com/HoratioShaw)
|
||||
[](https://github.com/nov30th)
|
||||
[](https://github.com/727155455)
|
||||
[](https://github.com/nixingshiguang)
|
||||
[](https://github.com/1411430556)
|
||||
[](https://github.com/Ovear)
|
||||
 
|
||||
|
||||
## ⏳提交日志
|
||||
|
||||
- 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>
|
||||
 
|
||||
|
||||
## 有问题可以提issue, 我会在这里持续添加更多直播平台的录制 欢迎Star
|
||||
####
|
||||
69
StopRecording.vbs
Normal file
69
StopRecording.vbs
Normal 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
|
||||
295
msg_push.py
Normal file
295
msg_push.py
Normal 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
23
pyproject.toml
Normal 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
7
requirements.txt
Normal 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
227
robust_relay.py
Normal 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
111
wending
Normal 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}")
|
||||
Reference in New Issue
Block a user