10 Commits

Author SHA1 Message Date
eric
d8705d4847 's' 2026-03-19 03:27:15 -05:00
eric
def681cbe3 's' 2026-03-19 03:24:58 -05:00
eric
7a03a670bc 's' 2026-03-19 03:21:09 -05:00
eric
1a2b43fd11 's' 2026-03-19 03:17:56 -05:00
eric
136362a34a 's' 2026-03-19 03:14:57 -05:00
eric
9ccafcc0d5 's' 2026-03-19 02:50:52 -05:00
eric
43a07f144f 's' 2026-03-19 02:46:53 -05:00
eric
fccb46ae7b 's' 2026-03-19 02:44:19 -05:00
eric
ea99d733dc s'' 2026-03-19 02:32:38 -05:00
eric
8c14404831 's' 2026-03-19 02:30:31 -05:00
7 changed files with 1042 additions and 663 deletions

834
README.md
View File

@@ -1,679 +1,259 @@
![video_spider](https://socialify.git.ci/ihmily/DouyinLiveRecorder/image?font=Inter&forks=1&language=1&owner=1&pattern=Circuit%20Board&stargazers=1&theme=Light)
# DouyinLiveRecorder 转播版
## 💡简介
[![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)
[![Python Version](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
[![Supported Platforms](https://img.shields.io/badge/platforms-Windows%20%7C%20Ubuntu-blue.svg)](https://github.com/ihmily/DouyinLiveRecorder)
[![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
一款**简易**的可循环值守的直播录制工具基于FFmpeg实现多平台直播源录制支持自定义配置录制以及直播状态推送
基于 [DouyinLiveRecorder](https://github.com/ihmily/DouyinLiveRecorder) 的**转播增强版**,将原本的录制下载功能扩展为**实时推流转播**,支持抖音/TikTok 等直播推送到 YouTube、HDMI 等目标
</div>
## 与原版的主要区别
## 😺已支持平台
| 功能 | 原版 DouyinLiveRecorder | 本转播版 |
|------|-------------------------|----------|
| 核心用途 | 录制直播到本地文件 | 实时转播到 YouTube / HDMI |
| 抖音 → YouTube | 不支持 | ✅ 支持,含 AI 人声分离防版权 |
| 抖音 → HDMI/SRS | 不支持 | ✅ 支持TikTok 模式) |
| 录制到本地 | ✅ 支持 | ✅ 保留支持 |
| 平台支持 | 40+ 平台 | 继承全部平台 |
- [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>
## 功能特性
## 🎈项目结构
### 转播模式
- **YouTube 转播**:抖音直播 → YouTube Live支持 AI 人声分离Demucs降低音乐版权识别
- **TikTok/HDMI 转播**:抖音直播 → SRS 推流 → HDMI 采集卡或 OBS
- **本地录制**:保留原版全部录制能力
### 防版权识别YouTube 专用)
- **Demucs AI 人声分离**:只保留人声,去除背景音乐,从源头避免 Content ID 匹配
- **NVIDIA GPU 加速**:自动检测 CUDA优先使用 GPU 加速 Demucs
- **无 GPU 回退**:无显卡时自动回退到 CPU不报错
- **可配置开关**:可在 `config/youtube.ini` 中关闭 Demucs使用原始音频
### 循环值守与多直播间
- 自动轮询直播间状态
- 主播下播后自动停止推流,并快速重检
- 支持多直播间 URL 配置,单路推流时自动切换
- 直播状态推送钉钉、微信、Telegram 等)
---
## 项目结构
```
.
── 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)
...
── main.py # 主入口(本地录制 + SRS 转播)
├── youtube.py # YouTube 转播入口
├── tiktok.py # TikTok/HDMI 转播入口
├── douyin_youtube_ffplay.py # 抖音 → YouTube 推流(含 Demucs 管道)
├── douyin_srs_ffplay.py # 抖音 → SRS/HDMI 推流
├── web.py # Web 配置界面
├── config/
├── config.ini # 通用配置
├── youtube.ini # YouTube 推流配置stream key、Demucs 开关)
── URL_config.ini # 直播间 URL 列表
├── src/ # 爬虫、流解析等核心逻辑
├── backup/ # 备份与辅助模块
├── requirements.txt
├── pyproject.toml
└── README.md
```
</div>
---
## 🌱使用说明
## 快速开始
- 对于只想使用录制软件的小白用户,进入[Releases](https://github.com/ihmily/DouyinLiveRecorder/releases) 中下载最新发布的 zip压缩包即可里面有打包好的录制软件。有些电脑可能会报毒直接忽略即可如果下载时被浏览器屏蔽请更换浏览器下载
### 1. 环境要求
- 压缩包解压后,在 `config` 文件夹内的 `URL_config.ini` 中添加录制直播间地址,一行一个直播间地址。如果要自定义配置录制,可以修改`config.ini` 文件,推荐将录制格式修改为`ts`
- 以上步骤都做好后,就可以运行`DouyinLiveRecorder.exe` 程序进行录制了。录制的视频文件保存在同目录下的 `downloads` 文件夹内。
- **Python** >= 3.10
- **FFmpeg**(需支持 libx264、h264_nvenc
- **可选**NVIDIA GPU + CUDA用于 Demucs 加速)
- 另外如果需要录制TikTok、AfreecaTV等海外平台请在配置文件中设置开启代理并添加proxy_addr链接 如:`127.0.0.1:7890` (这只是示例地址,具体根据实际填写)。
### 2. 安装依赖
- 假如`URL_config.ini`文件中添加的直播间地址,有个别直播间暂时不想录制又不想移除链接,可以在对应直播间的链接开头加上`#`,那么将停止该直播间的监测以及录制。
```bash
# 克隆或下载项目
cd douyinyoutube
- 软件默认录制清晰度为 `原画` ,如果要单独设置某个直播间的录制画质,可以在添加直播间地址时前面加上画质即可,如`超清https://live.douyin.com/745964462470` 记得中间要有`,` 分隔。
# 创建虚拟环境(推荐)
python -m venv .venv
# Windows:
.venv\Scripts\activate
# Ubuntu/Linux:
source .venv/bin/activate
- 如果要长时间挂着软件循环监测直播最好循环时间设置长一点咱也不差没录制到的那几分钟避免因请求频繁导致被官方封禁IP 。
- 要停止直播录制Windows平台可执行StopRecording.vbs脚本文件或者在录制界面使用 `Ctrl+C ` 组合键中断录制,若要停止其中某个直播间的录制,可在`URL_config.ini`文件中的地址前加#,会自动停止对应直播间的录制并正常保存已录制的视频。
- 最后欢迎右上角给本项目一个star同时也非常乐意大家提交pr。
&emsp;
直播间链接示例:
# 安装基础依赖
pip install -r requirements.txt
# 若使用 YouTube 转播 + Demucs 防版权,需额外安装:
pip install -r requirements-youtube.txt
# GPU 版本NVIDIA需先安装 CUDA 版 PyTorch
# pip install torch torchaudio --index-url https://download.pytorch.org/whl/cu118
```
抖音:
### 3. 配置
#### YouTube 转播
1. 编辑 `config/youtube.ini`
```ini
[youtube]
# YouTube 直播 stream key从 YouTube 直播控制台获取)
key = xxxx-xxxx-xxxx-xxxx
# 是否启用 Demucs 人声分离(防版权识别)
# 是 = 启用,否 = 使用原始音频
enable_demucs =
# Demucs 是否优先使用 GPU有 NVIDIA 显卡时)
# 是 = 优先 GPU否 = 强制 CPU
demucs_use_gpu =
```
2. 编辑 `config/URL_config.ini`,添加抖音直播间地址:
```ini
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
https://live.douyin.com/xxx,主播名
```
&emsp;
3.`config/config.ini` 中将 `视频保存格式` 设为 `ts`(触发 YouTube 推流逻辑)
## 🎃源码运行
使用源码运行,可参考下面的步骤。
#### TikTok/HDMI 转播
1.首先拉取或手动下载本仓库项目代码
- 使用 `tiktok.py` 入口,配置 SRS 推流地址
- 详见 `tiktok/` 目录说明
### 4. 运行
```bash
git clone https://github.com/ihmily/DouyinLiveRecorder.git
# YouTube 转播
python youtube.py
# 或使用 uv
uv run youtube.py
```
2.进入项目文件夹,安装依赖
---
## 详细配置说明
### config/youtube.ini
| 配置项 | 说明 | 默认值 |
|--------|------|--------|
| key | YouTube 直播 stream key | 必填 |
| enable_demucs | 是否启用 Demucs 人声分离 | 是 |
| demucs_use_gpu | 是否优先使用 NVIDIA GPU | 是 |
### config/config.ini
- **视频保存格式**:设为 `ts`youtube.py 会走 YouTube 推流逻辑,而非本地录制
- **循环时间**:轮询间隔(秒)
- **代理**:海外平台需配置代理
### URL_config.ini 格式
```
# 单行一个直播间
https://live.douyin.com/745964462470
# 可加画质和备注
超清,https://live.douyin.com/xxx
https://live.douyin.com/yyy,主播名
# 注释掉不监测
# https://live.douyin.com/zzz
```
---
## Demucs 防版权说明
### 原理
- 使用 [Demucs](https://github.com/facebookresearch/demucs) 对混音进行人声分离
- 只保留人声轨道推送到 YouTube背景音乐被物理移除
- Content ID 无法匹配已移除的音乐
### 延迟与性能
- **延迟**:约 48 秒(与 chunk 大小相关)
- **GPU**:有 NVIDIA 显卡时自动使用 CUDA 加速
- **CPU**:无 GPU 时自动回退,不影响运行
### 回退机制
1. **Demucs 未安装**:自动使用原始 FFmpeg 推流
2. **GPU 不可用**:自动使用 CPU 运行 Demucs
3. **enable_demucs = 否**:直接使用原始音频
---
## 平台兼容性
| 平台 | 录制 | YouTube 转播 | Demucs |
|------|------|--------------|--------|
| Windows | ✅ | ✅ | ✅CPU/GPU |
| Ubuntu Server | ✅ | ✅ | ✅(优先) |
| 其他 Linux | ✅ | ✅ | ✅ |
---
## Web 配置界面
```bash
cd DouyinLiveRecorder
python web.py
```
> [!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`
- 浏览器访问配置页面
- 支持编辑 `youtube.ini``URL_config.ini`
- 选择 youtube / tiktok / obs 等模式
<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执行
### 1. Demucs 安装失败
```bash
yum install epel-release
yum install ffmpeg
# 仅 CPU
pip install demucs torch torchaudio
# 使用国内镜像
pip install demucs torch torchaudio -i https://pypi.tuna.tsinghua.edu.cn/simple
```
Ubuntu则执行
### 2. 无 NVIDIA 显卡
```bash
apt update
apt install ffmpeg
```
- 程序会自动使用 CPU 运行 Demucs
- 可在 `youtube.ini` 中设置 `demucs_use_gpu = 否` 以跳过 GPU 检测
macOS 执行
### 3. 推流中断
**如果已经安装 Homebrew 请跳过这一步**
- 检查网络与代理
- 检查 YouTube stream key 是否有效
- 查看控制台日志定位错误
```bash
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
```
### 4. 抖音风控
```bash
brew install ffmpeg
```
- 适当增大循环时间
- 确保 Cookie 有效(录制抖音需配置)
4.运行程序
---
```python
python main.py
## 致谢
```
- 原项目:[ihmily/DouyinLiveRecorder](https://github.com/ihmily/DouyinLiveRecorder)
- 人声分离:[facebookresearch/demucs](https://github.com/facebookresearch/demucs)
```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
####
MIT License

View File

@@ -1,3 +1,27 @@
[youtube]
# 只写 stream key推荐方式
key = x04z-564w-aks7-embw-30y4
key = x04z-564w-aks7-embw-30y4
# ===== 优先:推流画质 =====
# 视频码率 MbpsYouTube 推荐 2.5,可设 4-4.5 追求更高画质)
video_bitrate_mbps = 2.5
# 推流状态日志间隔(秒),每 N 秒打印帧率/码率/网络等
stats_interval_sec = 10
# ===== 其次:音频版权处理 =====
# 是否启用 Demucs 人声分离(防 YouTube Content ID 版权识别)
# 若推流几十秒后断流(YouTube 显示 no data),可设为 否 使用原始音频
# 是 = 启用,只保留人声;否 = 使用原始音频
enable_demucs =
# Demucs 是否优先使用 NVIDIA GPU 加速
# 是 = 有显卡则用 GPU否 = 强制 CPU无显卡时自动回退到 CPU
demucs_use_gpu =
# 人声混合比例0.5-1.0),改善自然度、减少尖锐
# 0.75 = 75%人声+25%混合推荐1.0 = 纯人声
demucs_vocal_mix = 0.75
# 混合来源bass=低音无混响无重复other=其他(可能带混响导致重复感)
demucs_mix_source = bass

456
demucs_pipeline.py Normal file
View File

@@ -0,0 +1,456 @@
# demucs_pipeline.py
# Demucs 人声分离管道FFmpeg 拉流 → Demucs 分离人声 → FFmpeg 推流
# 兼容 Windows / Ubuntu支持 NVIDIA GPU 加速与 CPU 回退
"""
Demucs 音频管道模块
用于将混音分离为人声,推送到 YouTube 时避免 Content ID 识别背景音乐。
"""
import os
import sys
import socket
import threading
import subprocess
import queue
import time
import platform
try:
from stream_stats import StreamStats, parse_and_maybe_print
_STREAM_STATS_AVAILABLE = True
except ImportError:
_STREAM_STATS_AVAILABLE = False
# 可选依赖Demucs 和 PyTorch
DEMUCS_AVAILABLE = False
DEMUCS_MODEL = None
TORCH_AVAILABLE = False
try:
import torch
TORCH_AVAILABLE = True
except ImportError:
pass
try:
from demucs.pretrained import get_model
from demucs.apply import apply_model
DEMUCS_AVAILABLE = True
except ImportError:
pass
def _check_demucs_available():
"""检查 Demucs 是否可用"""
return DEMUCS_AVAILABLE and TORCH_AVAILABLE
def _load_demucs_model(use_gpu: bool):
"""加载 Demucs 模型,支持 GPU/CPU 回退"""
global DEMUCS_MODEL
if not _check_demucs_available():
return None
try:
# htdemucs 较轻量适合实时htdemucs_ft 质量更好但更慢
model_name = "htdemucs"
device = "cpu"
if use_gpu and TORCH_AVAILABLE and torch.cuda.is_available():
try:
device = "cuda"
except Exception:
device = "cpu"
model = get_model(model_name)
model.eval()
return model.to(device)
except Exception as e:
print(f"[WARN] Demucs 模型加载失败,回退原始音频: {e}")
return None
# Demucs 模型采样率
DEMUCS_SAMPLE_RATE = 44100
DEMUCS_CHANNELS = 2
# 每次处理的音频长度1.0 更易实时2.0 质量更好但 P40 可能跟不上
DEMUCS_CHUNK_SEC = 1.0
# 字节 per sample (s16le: 2 bytes)
BYTES_PER_SAMPLE = 2
def _demucs_separate_vocals(model, audio_chunk, device, vocal_mix_ratio=1.0, mix_source="bass"):
"""
对音频块进行人声分离,返回人声轨道。
vocal_mix_ratio: 1.0=纯人声0.75=75%人声+25%混合(更自然)
mix_source: "bass"=与低音混合(少混响无重复感)"other"=与其他混合(可能带混响)
"""
import torch
# audio_chunk: numpy or tensor, shape (channels, samples)
if hasattr(audio_chunk, 'numpy'):
wav = audio_chunk
else:
import numpy as np
wav = torch.from_numpy(audio_chunk).float()
# Demucs 期望 (batch, channels, samples)
if wav.dim() == 2:
wav = wav.unsqueeze(0)
with torch.no_grad():
out = apply_model(model, wav, device=device, progress=False, overlap=0.1)
# out: (batch, sources, channels, samples)
# sources: [drums=0, bass=1, other=2, vocals=3]
vocals = out[0, 3]
if vocal_mix_ratio >= 1.0:
return vocals.cpu().numpy()
# 混合 bass 或 otherbass 无混响、无重复感other 可能带混响导致"重复一遍"
blend = out[0, 1] if mix_source == "bass" else out[0, 2]
mixed = vocals * vocal_mix_ratio + blend * (1.0 - vocal_mix_ratio)
return mixed.cpu().numpy()
def _run_demucs_worker(audio_read_pipe, audio_write_socket, model, device, stop_event, vocal_mix_ratio=1.0, mix_source="bass"):
"""Demucs 工作线程:从 FFmpeg 读音频,分离人声,写入 socket"""
import numpy as np
chunk_samples = int(DEMUCS_SAMPLE_RATE * DEMUCS_CHUNK_SEC * DEMUCS_CHANNELS)
chunk_bytes = chunk_samples * BYTES_PER_SAMPLE
buf = b""
def read_audio(size):
if hasattr(audio_read_pipe, 'recv'):
return audio_read_pipe.recv(size)
return audio_read_pipe.read(size)
try:
while not stop_event.is_set():
try:
data = read_audio(65536)
if not data:
break
buf += data
except (socket.error, OSError, ValueError):
break
while len(buf) >= chunk_bytes:
chunk = buf[:chunk_bytes]
buf = buf[chunk_bytes:]
# s16le -> float
arr = np.frombuffer(chunk, dtype=np.int16)
arr = arr.reshape(-1, DEMUCS_CHANNELS).T.astype(np.float32) / 32768.0
try:
vocals = _demucs_separate_vocals(model, arr, device, vocal_mix_ratio, mix_source)
# float -> s16le
out = (vocals * 32768).astype(np.int16).tobytes()
try:
audio_write_socket.sendall(out)
except (socket.error, OSError):
return
except Exception as e:
print(f"[WARN] Demucs 处理异常: {e}")
# 回退:直接透传原始音频
try:
audio_write_socket.sendall(chunk)
except (socket.error, OSError):
return
# 剩余 buffer
if buf and audio_write_socket:
try:
arr = np.frombuffer(buf, dtype=np.int16)
arr = arr.reshape(-1, DEMUCS_CHANNELS).T.astype(np.float32) / 32768.0
vocals = _demucs_separate_vocals(model, arr, device, vocal_mix_ratio, mix_source)
out = (vocals * 32768).astype(np.int16).tobytes()
audio_write_socket.sendall(out)
except Exception:
pass
except Exception as e:
print(f"[WARN] Demucs worker 异常: {e}")
finally:
try:
audio_write_socket.shutdown(socket.SHUT_WR)
audio_write_socket.close()
except Exception:
pass
def run_demucs_pipeline(
real_url: str,
youtube_rtmp: str,
douyin_headers: str,
nvenc_params: dict,
end_keywords: list,
stderr_queue_timeout: float,
no_frame_timeout: int,
start_check_after: int,
stale_output_seconds: int,
stream_ended_exception,
cleanup_proc_fn,
stderr_reader_fn,
vocal_mix_ratio: float = 0.75,
mix_source: str = "bass",
stats_interval_sec: int = 10,
):
"""
运行 Demucs 管道:拉流 → 人声分离 → 推流
若主播下播应停止则 raise stream_ended_exception
"""
# 检查 Demucs
use_gpu = nvenc_params.get("demucs_use_gpu", True)
model = _load_demucs_model(use_gpu)
if model is None:
raise RuntimeError("Demucs 模型不可用,将回退原始推流")
device = next(model.parameters()).device
device_str = "cuda" if "cuda" in str(device) else "cpu"
src_name = "低音" if mix_source == "bass" else "其他"
mix_info = "纯人声" if vocal_mix_ratio >= 1.0 else f"{int(vocal_mix_ratio*100)}%人声+{int((1-vocal_mix_ratio)*100)}%{src_name}"
print(f"[INFO] Demucs 人声分离已启用 ({device_str}),模式: {mix_info},低通 9kHz 降尖锐")
# TCP 服务器用于音频(跨平台)
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(("127.0.0.1", 0))
port = server.getsockname()[1]
server.listen(1)
tcp_audio_url = f"tcp://127.0.0.1:{port}"
stop_event = threading.Event()
conn = [None] # 用 list 以便在闭包中修改
def accept_conn():
server.settimeout(30)
try:
c, _ = server.accept()
conn[0] = c
except Exception:
pass
finally:
try:
server.close()
except Exception:
pass
accept_thread = threading.Thread(target=accept_conn, daemon=True)
accept_thread.start()
time.sleep(0.2) # 确保 server 已 listen
# FFmpeg 拉流(视频)
ffmpeg_v_cmd = [
"ffmpeg", "-y", "-loglevel", "info", "-nostdin", "-re",
"-stats", "-stats_period", "1",
"-rw_timeout", "30000000",
"-reconnect", "1", "-reconnect_at_eof", "1", "-reconnect_streamed", "1",
"-reconnect_delay_max", "5",
"-fflags", "+genpts+discardcorrupt+nobuffer+flush_packets",
"-err_detect", "ignore_err",
"-headers", douyin_headers,
"-i", real_url,
"-map", "0:v", "-c:v", "copy", "-f", "nut", "pipe:1",
]
# FFmpeg 拉流(音频)
ffmpeg_a_cmd = [
"ffmpeg", "-y", "-loglevel", "error", "-nostdin", "-re",
"-rw_timeout", "30000000",
"-reconnect", "1", "-reconnect_at_eof", "1", "-reconnect_streamed", "1",
"-headers", douyin_headers,
"-i", real_url,
"-map", "0:a", "-f", "s16le", "-ac", str(DEMUCS_CHANNELS),
"-ar", str(DEMUCS_SAMPLE_RATE), "pipe:1",
]
# FFmpeg 推流:音频 tcp 放第一FFmpeg 按顺序打开输入pipe:0 会阻塞直到有数据,
# 若 pipe 在前则永远连不上 tcp导致 accept 超时)
vf = "fps=30,scale=720:1280:force_original_aspect_ratio=decrease:flags=lanczos,pad=720:1280:(ow-iw)/2:(oh-ih)/2:black"
ffmpeg_out_cmd = [
"ffmpeg", "-y", "-loglevel", "info", "-nostdin",
"-f", "s16le", "-ac", str(DEMUCS_CHANNELS), "-ar", str(DEMUCS_SAMPLE_RATE),
"-i", tcp_audio_url,
"-f", "nut", "-i", "pipe:0",
"-map", "1:v", "-map", "0:a",
"-vf", vf,
"-c:v", nvenc_params.get("codec_v", "libx264"),
"-preset", nvenc_params.get("preset_v", "fast"),
"-profile:v", "high",
"-b:v", f"{nvenc_params.get('video_bitrate_k', 4500)}k",
"-maxrate", f"{nvenc_params.get('video_bitrate_k', 4500)+500}k",
"-bufsize", f"{nvenc_params.get('video_bitrate_k', 4500)*2}k",
"-fps_mode", "cfr", "-g", "60", "-keyint_min", "60", "-r", "30",
"-bf", "0",
"-pix_fmt", "yuv420p",
"-c:a", "aac", "-b:a", "128k", "-ar", "48000", "-ac", "2",
"-af", "lowpass=f=9000,aresample=async=1:first_pts=0",
"-flags", "+global_header", "-flvflags", "no_duration_filesize",
"-f", "flv", youtube_rtmp,
]
# NVENC 用 tune_paramlibx264 用 sc_threshold + x264-params
codec = nvenc_params.get("codec_v", "libx264")
if nvenc_params.get("tune_param") and codec == "h264_nvenc":
idx = ffmpeg_out_cmd.index("-profile:v")
for p in reversed(nvenc_params["tune_param"]):
ffmpeg_out_cmd.insert(idx, p)
elif codec == "libx264":
idx = ffmpeg_out_cmd.index("-bf")
ffmpeg_out_cmd.insert(idx + 2, "-sc_threshold")
ffmpeg_out_cmd.insert(idx + 3, "0")
idx = ffmpeg_out_cmd.index("-pix_fmt")
ffmpeg_out_cmd.insert(idx, "-x264-params")
ffmpeg_out_cmd.insert(idx + 1, "force-cfr=1:scenecut=0:open_gop=0:sync-lookahead=0")
proc_v = None
proc_a = None
proc_out = None
demucs_thread = None
try:
proc_v = subprocess.Popen(
ffmpeg_v_cmd,
stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
bufsize=1024 * 1024,
)
proc_a = subprocess.Popen(
ffmpeg_a_cmd,
stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
bufsize=1024 * 1024,
)
proc_out = subprocess.Popen(
ffmpeg_out_cmd,
stdin=subprocess.PIPE,
stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE,
bufsize=1024 * 1024,
)
# 等待 TCP 连接FFmpeg_out 会连接)
accept_thread.join(timeout=15)
if conn[0] is None:
raise RuntimeError("Demucs 管道: 音频 TCP 连接超时")
# 视频转发线程
def copy_video():
try:
while proc_v.poll() is None and proc_out.poll() is None:
chunk = proc_v.stdout.read(1024 * 256)
if not chunk:
break
proc_out.stdin.write(chunk)
except Exception:
pass
finally:
try:
proc_out.stdin.close()
except Exception:
pass
copy_t = threading.Thread(target=copy_video, daemon=True)
copy_t.start()
# Demucs 工作线程(从 proc_a 读,处理后写 conn
# proc_a 输出是 stdoutconn 是 socket。需要把 proc_a.stdout 的数据通过 socket 发到 FFmpeg。
# FFmpeg_out 的第二个输入是 tcp已经连接了 conn。所以我们从 proc_a 读Demucs 处理,写 conn。
# 但 proc_a.stdout 是 pipeconn 是 socket。我们需要一个线程读 proc_a 并写 conn。
# 注意proc_a 输出到 stdout我们读。conn 是 FFmpeg 作为 client 连上来的,所以 FFmpeg 在读,我们写 conn。
demucs_thread = threading.Thread(
target=_run_demucs_worker,
args=(proc_a.stdout, conn[0], model, device, stop_event, vocal_mix_ratio, mix_source),
daemon=True,
)
demucs_thread.start()
# 监控 proc_v 的 stderr与源连接用于检测下播
stderr_q = queue.Queue()
reader_t = threading.Thread(target=stderr_reader_fn, args=(proc_v, stderr_q))
reader_t.daemon = True
reader_t.start()
# 监控 proc_out 的 stderr推流到 YouTube解析状态用于详细日志
stream_stats = None
if _STREAM_STATS_AVAILABLE:
stream_stats = StreamStats(interval_sec=stats_interval_sec)
stream_stats.start_periodic_printer()
print(f"[INFO] 推流状态监控已启用,每 {stats_interval_sec}s 输出帧率/码率/网络等")
def _proc_out_stderr_reader():
try:
for line in iter(proc_out.stderr.readline, b""):
if line:
decoded = line.decode("utf-8", errors="replace").strip()
if decoded:
if stream_stats and parse_and_maybe_print(decoded, stream_stats, "[YouTube推流]", verbose=False):
pass # 已由 parse 处理,进度行不重复打印
else:
print(f"[YouTube推流] {decoded}")
except Exception:
pass
proc_out_reader_t = threading.Thread(target=_proc_out_stderr_reader, daemon=True)
proc_out_reader_t.start()
start_time = last_frame_time = time.time()
proc_v.last_out_ts = time.time()
while True:
try:
line = stderr_q.get(timeout=stderr_queue_timeout)
except queue.Empty:
# 若 proc_out推流进程已异常退出立即报告
if proc_out.poll() is not None and proc_out.returncode != 0:
print(f"[ERROR] YouTube 推流进程已退出,返回码: {proc_out.returncode},可能是 Demucs 处理跟不上或网络问题")
print(f"[TIP] 若频繁断流,可尝试 config/youtube.ini 中设置 enable_demucs = 否 使用原始音频")
break
if time.time() - getattr(proc_v, "last_out_ts", time.time()) > stale_output_seconds:
print("[WARN] 长时间无任何日志,强制重启进程")
break
continue
if line is None:
if proc_v.poll() is not None:
break
continue
# proc_v 的 stderr 为 bytes需解码为 str
if isinstance(line, bytes):
line = line.decode("utf-8", errors="replace")
line = line.strip()
if line.startswith("__ERR__READER__"):
print(f"[ERROR] stderr reader 异常: {line}")
continue
if line:
proc_v.last_out_ts = time.time()
if stream_stats:
stream_stats.parse_lag_event(line) # 源端抖动统计
print(f"[FFmpeg] {line}")
line_lower = line.lower()
if any(kw in line_lower for kw in end_keywords):
print("[ERROR] 检测到主播真下播或被明确拒绝,停止推流")
raise stream_ended_exception()
if "frame=" in line_lower or "fps=" in line_lower:
last_frame_time = time.time()
if time.time() - start_time > start_check_after:
if time.time() - last_frame_time > no_frame_timeout:
print(f"[WARN] 连续 {no_frame_timeout}s 无新帧,判定主播已下播")
raise stream_ended_exception()
except stream_ended_exception:
raise
except Exception as e:
print(f"[ERROR] Demucs 管道异常: {e}")
import traceback
traceback.print_exc()
finally:
stop_event.set()
if stream_stats is not None:
stream_stats.stop()
if proc_out is not None and proc_out.poll() is not None and proc_out.returncode != 0:
print(f"[INFO] YouTube 推流进程退出码: {proc_out.returncode}")
cleanup_proc_fn(proc_v)
cleanup_proc_fn(proc_a)
cleanup_proc_fn(proc_out)
try:
if conn[0]:
conn[0].close()
except Exception:
pass
try:
server.close()
except Exception:
pass

View File

@@ -11,6 +11,23 @@ import re
import os
import configparser
# Demucs 管道(可选,用于防版权)
try:
from demucs_pipeline import (
_check_demucs_available,
run_demucs_pipeline,
)
_DEMUCS_MODULE_AVAILABLE = True
except ImportError:
_DEMUCS_MODULE_AVAILABLE = False
_check_demucs_available = lambda: False
try:
from stream_stats import StreamStats, parse_and_maybe_print
_STREAM_STATS_AVAILABLE = True
except ImportError:
_STREAM_STATS_AVAILABLE = False
class StreamEnded(Exception):
"""主播真实下播的专用异常"""
@@ -85,19 +102,58 @@ def start_douyin_youtube_ffplay(
default_key = "qxvb-r47b-r5ju-6ud3-6k7z" # 备用默认 key
youtube_rtmp = rtmp_base + default_key # 默认值
enable_demucs = False
demucs_use_gpu = True
demucs_vocal_mix = 0.75 # 0.75=更自然少尖锐1.0=纯人声
demucs_mix_source = "bass" # bass=无混响无重复other=可能带混响
video_bitrate_mbps = 2.5 # YouTube 推荐 2.5,可设 4-4.5 追求更高画质
stats_interval_sec = 10 # 推流状态日志间隔
if os.path.exists(config_file):
config = configparser.ConfigParser()
config.read(config_file, encoding="utf-8")
if config.has_section("youtube"):
for key_name, value in config.items("youtube"):
value = value.strip()
if value:
if value.startswith("rtmp://"):
youtube_rtmp = value
else:
youtube_rtmp = rtmp_base + value
key_lower = key_name.strip().lower()
val = value.strip()
if key_lower == "enable_demucs":
enable_demucs = val.lower() in ("1", "true", "yes", "")
elif key_lower == "demucs_use_gpu":
demucs_use_gpu = val.lower() not in ("0", "false", "no", "")
elif key_lower == "demucs_vocal_mix":
try:
v = float(val)
if 0.5 <= v <= 1.0:
demucs_vocal_mix = v
except ValueError:
pass
elif key_lower == "demucs_mix_source":
if val.lower() in ("bass", "other"):
demucs_mix_source = val.lower()
elif key_lower == "video_bitrate_mbps":
try:
v = float(val)
if 2.0 <= v <= 8.0:
video_bitrate_mbps = v
except ValueError:
pass
elif key_lower == "stats_interval_sec":
try:
v = int(val)
if 5 <= v <= 120:
stats_interval_sec = v
except ValueError:
pass
elif val and key_lower in ("key", "stream_key", "streamkey"):
youtube_rtmp = val if val.startswith("rtmp://") else rtmp_base + val
print(f"[INFO] 从 {config_file} 读取 YouTube stream key: {youtube_rtmp}")
break # 只取第一个有效 key保持单路推流不加多路逻辑
# 兼容:若上面未匹配到 key取第一个非配置项的值作为 stream key
if youtube_rtmp == rtmp_base + default_key:
for k, v in config.items("youtube"):
if k.strip().lower() not in ("enable_demucs", "demucs_use_gpu", "demucs_vocal_mix", "demucs_mix_source", "video_bitrate_mbps", "stats_interval_sec") and v.strip():
v = v.strip()
youtube_rtmp = v if v.startswith("rtmp://") else rtmp_base + v
print(f"[INFO] 从 {config_file} 读取 YouTube stream key: {youtube_rtmp}")
break
# ====================== 常量配置 ======================
MAX_RETRY_DELAY = 90
@@ -135,6 +191,7 @@ def start_douyin_youtube_ffplay(
print(f"[INFO] {rec_info} 开始推流到 YouTube: {stream_title}")
# ====================== NVENC 检测 ======================
# FFmpeg NVENC 需驱动 570+,否则会报 "Driver does not support the required nvenc API version"
codec_v = "libx264"
preset_v = "veryfast"
tune_param = ["-tune", "zerolatency"]
@@ -144,13 +201,40 @@ def start_douyin_youtube_ffplay(
if check.returncode == 0:
enc = subprocess.run(["ffmpeg", "-hide_banner", "-encoders"], capture_output=True, text=True, timeout=5)
if "h264_nvenc" in (enc.stdout or ""):
codec_v = "h264_nvenc"
preset_v = "p5"
tune_param = ["-tune", "ll", "-rc", "vbr", "-cq", "21", "-b:v", "2500k"]
print("[INFO] 检测到 NVIDIA GPU → 使用 NVENC 硬件加速")
# 检查驱动版本NVENC API 13.0 需 570+
driver_ok = False
try:
ver_out = subprocess.run(
["nvidia-smi", "--query-gpu=driver_version", "--format=csv,noheader"],
capture_output=True, text=True, timeout=5
)
if ver_out.returncode == 0 and ver_out.stdout:
ver_str = ver_out.stdout.strip().split("\n")[0].strip()
major = int(ver_str.split(".")[0]) if ver_str else 0
driver_ok = major >= 570
if not driver_ok:
print(f"[INFO] NVIDIA 驱动 {ver_str} 低于 570使用软件编码 (libx264)")
except Exception:
pass
if driver_ok:
codec_v = "h264_nvenc"
preset_v = "p5"
br_k = int(video_bitrate_mbps * 1000)
tune_param = ["-tune", "ll", "-rc", "vbr", "-b:v", f"{br_k}k", "-maxrate", f"{br_k+500}k", "-bufsize", f"{br_k*2}k"]
print("[INFO] 检测到 NVIDIA GPU → 使用 NVENC 硬件加速")
except Exception as e:
print(f"[WARN] NVENC 检测失败,回退软件编码: {e}")
br_k = int(video_bitrate_mbps * 1000)
nvenc_params = {
"codec_v": codec_v,
"preset_v": preset_v,
"tune_param": tune_param,
"demucs_use_gpu": demucs_use_gpu,
"video_bitrate_k": br_k,
}
use_demucs = enable_demucs and _DEMUCS_MODULE_AVAILABLE and _check_demucs_available()
# 720p (最原始)
ffmpeg_command = [
"ffmpeg", "-y", "-loglevel", "info", "-nostdin", "-re",
@@ -172,10 +256,9 @@ def start_douyin_youtube_ffplay(
"-preset", "fast",
# "-tune", "zerolatency",
"-profile:v", "high",
# 720p
# "-minrate", "3200k","-b:v", "3200k", "-maxrate", "3200k", "-bufsize", "6400k", # bufsize = 2× bitrate保持真 CBR
"-b:v", "2600k","-maxrate", "3000k","-bufsize", "5200k",
"-vsync", "cfr",
# 720p画质优先YouTube 推荐 3-8 Mbps
"-b:v", f"{br_k}k", "-maxrate", f"{br_k+500}k", "-bufsize", f"{br_k*2}k",
"-fps_mode", "cfr",
"-g", "60", "-keyint_min", "60",
"-r", "30",
"-bf", "0",
@@ -435,22 +518,55 @@ def start_douyin_youtube_ffplay(
stderr_q = queue.Queue()
print(f"[INFO][{datetime.datetime.now():%H:%M:%S}] 启动 YouTube 推流 → {stream_title}")
proc = subprocess.Popen(
ffmpeg_command,
stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE,
text=True,
bufsize=1,
universal_newlines=True
)
proc.last_out_ts = time.time()
reader_t = threading.Thread(target=_stderr_reader_thread, args=(proc, stderr_q))
reader_t.daemon = True
reader_t.start()
proc = None
if use_demucs:
try:
run_demucs_pipeline(
real_url=real_url,
youtube_rtmp=youtube_rtmp,
douyin_headers=douyin_headers,
nvenc_params=nvenc_params,
end_keywords=END_KEYWORDS,
stderr_queue_timeout=STDERR_QUEUE_TIMEOUT,
no_frame_timeout=NO_FRAME_TIMEOUT,
start_check_after=START_CHECK_AFTER,
stale_output_seconds=STALE_OUTPUT_SECONDS,
stream_ended_exception=StreamEnded,
cleanup_proc_fn=cleanup_proc,
stderr_reader_fn=_stderr_reader_thread,
vocal_mix_ratio=demucs_vocal_mix,
mix_source=demucs_mix_source,
stats_interval_sec=stats_interval_sec,
)
except StreamEnded:
raise
except Exception as e:
print(f"[WARN] Demucs 管道异常,回退原始推流: {e}")
use_demucs = False
if not use_demucs:
proc = subprocess.Popen(
ffmpeg_command,
stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE,
text=True,
bufsize=1,
universal_newlines=True
)
proc.last_out_ts = time.time()
reader_t = threading.Thread(target=_stderr_reader_thread, args=(proc, stderr_q))
reader_t.daemon = True
reader_t.start()
start_time = last_frame_time = time.time()
stream_stats = None
if _STREAM_STATS_AVAILABLE:
stream_stats = StreamStats(interval_sec=stats_interval_sec)
stream_stats.start_periodic_printer()
print(f"[INFO] 推流状态监控已启用,每 {stats_interval_sec}s 输出帧率/码率/网络等")
while True:
try:
@@ -468,15 +584,22 @@ def start_douyin_youtube_ffplay(
break
continue
line = line.strip()
if line.startswith("__ERR__READER__"):
print(f"[ERROR] stderr reader 异常: {line}")
line_str = line.strip() if isinstance(line, str) else line.decode("utf-8", errors="replace").strip()
if line_str.startswith("__ERR__READER__"):
print(f"[ERROR] stderr reader 异常: {line_str}")
continue
if line:
if line_str:
proc.last_out_ts = time.time()
print(f"[FFmpeg] {line}")
line_lower = line.lower()
if stream_stats:
stream_stats.parse_lag_event(line_str)
if stream_stats.parse_ffmpeg_progress(line_str):
pass # 进度行由周期摘要代替,不重复打印
else:
print(f"[FFmpeg] {line_str}")
else:
print(f"[FFmpeg] {line_str}")
line_lower = line_str.lower()
if any(kw in line_lower for kw in END_KEYWORDS):
print("[ERROR] 检测到主播真下播或被明确拒绝,停止推流")
@@ -500,6 +623,9 @@ def start_douyin_youtube_ffplay(
cleanup_proc(proc)
retry_delay = min(retry_delay * 2, MAX_RETRY_DELAY)
break
finally:
if stream_stats is not None:
stream_stats.stop()
continue

12
requirements-youtube.txt Normal file
View File

@@ -0,0 +1,12 @@
# YouTube 转播 + Demucs 人声分离(防版权)所需依赖
# 在 requirements.txt 基础上额外安装:
# pip install -r requirements.txt -r requirements-youtube.txt
# CPU 版本
demucs>=4.0.0
torch>=2.0.0
torchaudio>=2.0.0
# 若使用 NVIDIA GPU请安装 CUDA 版 PyTorch 后覆盖:
# pip uninstall torch torchaudio
# pip install torch torchaudio --index-url https://download.pytorch.org/whl/cu118

View File

@@ -4,4 +4,11 @@ pycryptodome>=3.20.0
distro>=1.9.0
tqdm>=4.67.1
httpx[http2]>=0.28.1
PyExecJS>=1.5.1
PyExecJS>=1.5.1
# 可选YouTube 转播时 Demucs 人声分离(防版权识别)
# pip install demucs torch torchaudio
# GPU 版本: pip install torch torchaudio --index-url https://download.pytorch.org/whl/cu118
# demucs>=4.0.0
# torch>=2.0.0
# torchaudio>=2.0.0

174
stream_stats.py Normal file
View File

@@ -0,0 +1,174 @@
# stream_stats.py
# 推流状态监控:解析 FFmpeg 输出,统计帧率、码率、网络等,便于调试
"""
推流状态监控模块
解析 FFmpeg stderr 输出,提取:帧率、码率、编码速度、源延迟事件
可选Linux 网络上下行速度
"""
import re
import time
import threading
import platform
class StreamStats:
"""推流状态统计,线程安全"""
def __init__(self, interval_sec=10):
self.interval_sec = interval_sec
self._lock = threading.Lock()
self.frame = 0
self.fps = 0.0
self.bitrate_kbps = 0.0
self.speed = 0.0
self.encode_time = ""
self.size_kb = 0
self.quality_q = 0.0
self.lag_events = 0 # "Resumed reading" 次数,表示源端抖动
self.last_update_ts = 0.0
self.start_ts = time.time()
self._stop = threading.Event()
self._printer_thread = None
# 网络统计 (Linux)
self._net_last = None
self._net_last_ts = 0
self.rx_mbps = 0.0
self.tx_mbps = 0.0
def parse_ffmpeg_progress(self, line: str) -> bool:
"""解析 FFmpeg 进度行,返回是否成功解析"""
# frame= 12345 fps= 30 q=28.0 size= 12345kB time=00:06:51.50 bitrate=2460.0kbits/s speed=1.00x
m = re.search(r'frame=\s*(\d+)', line, re.I)
if not m:
return False
with self._lock:
try:
self.frame = int(m.group(1))
except (ValueError, IndexError):
pass
m2 = re.search(r'fps=\s*([\d.]+)', line, re.I)
if m2:
self.fps = float(m2.group(1))
m3 = re.search(r'bitrate=\s*([\d.]+)\s*kbits/s', line, re.I)
if m3:
self.bitrate_kbps = float(m3.group(1))
m4 = re.search(r'speed=\s*([\d.]+)x', line, re.I)
if m4:
self.speed = float(m4.group(1))
m5 = re.search(r'time=([^\s]+)', line, re.I)
if m5:
self.encode_time = m5.group(1).strip()
m6 = re.search(r'size=\s*(\d+)\s*kB', line, re.I)
if m6:
self.size_kb = int(m6.group(1))
m7 = re.search(r'q=([\d.]+)', line, re.I)
if m7:
self.quality_q = float(m7.group(1))
self.last_update_ts = time.time()
return True
def parse_lag_event(self, line: str) -> bool:
"""检测源端延迟事件 Resumed reading at pts"""
if "resumed reading at pts" in line.lower():
with self._lock:
self.lag_events += 1
return True
return False
def _read_net_bytes(self):
"""读取 /proc/net/dev 总字节数 (Linux)"""
try:
with open("/proc/net/dev", "r") as f:
content = f.read()
except Exception:
return None, None
rx_total = tx_total = 0
for line in content.split("\n")[2:]: # 跳过表头
line = line.strip()
if not line or ":" not in line:
continue
parts = line.split()
if len(parts) >= 10:
try:
rx_total += int(parts[1])
tx_total += int(parts[9])
except (ValueError, IndexError):
pass
return rx_total, tx_total
def update_network(self):
"""更新网络速度 (Linux)"""
if platform.system() != "Linux":
return
rx, tx = self._read_net_bytes()
if rx is None:
return
now = time.time()
with self._lock:
if self._net_last is not None:
dt = now - self._net_last_ts
if dt > 0.5: # 至少 0.5 秒间隔
drx = (rx - self._net_last[0]) / (1024 * 1024) / dt
dtx = (tx - self._net_last[1]) / (1024 * 1024) / dt
self.rx_mbps = round(drx, 2)
self.tx_mbps = round(dtx, 2)
self._net_last = (rx, tx)
self._net_last_ts = now
def format_summary(self) -> str:
"""生成状态摘要字符串"""
self.update_network()
with self._lock:
elapsed = time.time() - self.start_ts
parts = [
f"推流时长 {int(elapsed)}s",
f"帧率 {self.fps:.1f} fps",
f"已推帧 {self.frame}",
f"码率 {self.bitrate_kbps:.0f} kbps",
f"编码速度 {self.speed:.2f}x",
]
if self.lag_events > 0:
parts.append(f"源抖动 {self.lag_events}")
if platform.system() == "Linux":
parts.append(f"下行 {self.rx_mbps:.2f} MB/s 上行 {self.tx_mbps:.2f} MB/s")
return " | ".join(parts)
def start_periodic_printer(self):
"""启动周期性打印线程"""
def _run():
while not self._stop.is_set():
self._stop.wait(self.interval_sec)
if self._stop.is_set():
break
if self.last_update_ts > 0 and (time.time() - self.last_update_ts) < 60:
print(f"[推流状态] {self.format_summary()}")
self._printer_thread = threading.Thread(target=_run, daemon=True)
self._printer_thread.start()
def stop(self):
"""停止周期性打印"""
self._stop.set()
def parse_and_maybe_print(line: str, stats: StreamStats, prefix: str = "[FFmpeg]", verbose=True) -> bool:
"""
解析一行 FFmpeg 输出,更新 stats。
若为进度行则更新不打印;若为 lag 事件则更新并可选打印;其他行原样打印。
verbose: 是否打印非进度行
"""
line_stripped = line.strip() if isinstance(line, str) else line.decode("utf-8", errors="replace").strip()
if not line_stripped:
return False
# 进度行:更新 stats不重复打印由周期摘要代替
if stats.parse_ffmpeg_progress(line_stripped):
return True
# 源端延迟事件
if stats.parse_lag_event(line_stripped):
if verbose:
print(f"{prefix} {line_stripped}")
return True
if verbose:
print(f"{prefix} {line_stripped}")
return False