74 Commits

Author SHA1 Message Date
Your Name
2375331973 srs-mpv 2026-01-02 04:37:59 +00:00
Your Name
041a402a73 mpv-grok 2025-12-30 14:00:02 +00:00
Your Name
a7b7761e78 lajilaji 2025-12-30 10:03:08 +00:00
Your Name
7ac4c7ff90 mpv设计ok 2025-12-30 06:42:07 +00:00
Your Name
d4158e7f1a mpv断线重启 2025-12-30 06:28:06 +00:00
Your Name
8953e2f4d4 ffmpeg=>pipe=>mpv 2025-12-30 04:37:35 +00:00
Your Name
88d54c1679 mpv检测第1版 2025-12-30 02:40:13 +00:00
Your Name
c630787719 mpv初始版本 2025-12-30 01:50:28 +00:00
Your Name
67bacc1c42 s 2025-12-29 06:11:30 +00:00
eric
53f2c77421 's' 2025-12-28 08:45:14 -06:00
eric
9107995c02 ';s' 2025-12-28 08:20:11 -06:00
eric
3ff4facd3d 's' 2025-12-28 07:52:07 -06:00
eric
34bb2b661e 's' 2025-12-28 07:50:05 -06:00
eric
6aeb1462dd 's' 2025-12-23 06:47:50 -06:00
eric
0c7437f9cc 's' 2025-12-20 09:30:30 -06:00
eric
9b23d9872b s'' 2025-12-20 09:27:06 -06:00
eric
303ece39c0 's' 2025-12-13 08:17:23 -06:00
eric
7c21e80eb6 s'' 2025-12-13 07:42:08 -06:00
eric
b4e76d74ca 's' 2025-12-13 07:34:08 -06:00
eric
bfc64b873a 's' 2025-12-13 07:26:45 -06:00
eric
bc2651c801 's' 2025-12-13 07:18:33 -06:00
eric
7c47c73d71 's' 2025-12-13 01:47:34 -06:00
eric
c6e2ea2e77 's' 2025-12-13 00:20:07 -06:00
eric
4e1b408e27 's' 2025-12-13 00:18:06 -06:00
eric
4a27659c99 's' 2025-12-13 00:11:42 -06:00
eric
ab8511de5a 's' 2025-12-12 23:24:30 -06:00
eric
90c3f193aa 's' 2025-12-12 23:22:02 -06:00
eric
f4a0c06ff6 's' 2025-12-12 23:20:05 -06:00
eric
2cb81ebb03 's' 2025-12-12 23:17:27 -06:00
eric
26db3d41ce '' 2025-12-12 23:07:03 -06:00
eric
ddadef2287 's' 2025-12-12 22:54:50 -06:00
eric
c717efe8c4 's' 2025-12-12 22:33:41 -06:00
eric
90bb33c9af 's' 2025-12-12 20:42:10 -06:00
eric
1ef411a2d0 's' 2025-12-12 20:33:19 -06:00
eric
27ad068712 's' 2025-12-12 20:29:59 -06:00
eric
dd6d9f7377 s 2025-12-13 02:22:47 +00:00
eric
b00663aa6c 's' 2025-12-12 20:22:18 -06:00
eric
8d3f950303 's' 2025-12-12 20:18:46 -06:00
eric
6d505c1452 's' 2025-12-12 14:11:38 -06:00
eric
c059f62d38 's' 2025-12-12 14:09:44 -06:00
eric
eaa3d67d16 's' 2025-12-11 23:45:59 -06:00
eric
b2af356c22 's' 2025-12-10 15:05:18 +08:00
eric
31e7c67425 '' 2025-12-10 14:15:56 +08:00
eric
1d1fff7979 's'' 2025-12-06 17:00:01 +08:00
eric
6430ce1f61 's' 2025-12-04 14:30:45 +08:00
eric
f10549ffae 's' 2025-12-04 14:26:11 +08:00
eric
667dc47188 's' 2025-12-02 15:15:33 +08:00
eric
511576aa97 's' 2025-12-02 14:51:17 +08:00
eric
9609e4caa1 's' 2025-12-02 09:51:58 +08:00
eric
63bf62df84 's' 2025-12-02 09:16:17 +08:00
eric
d12a35d249 '增加srs服务器' 2025-12-02 00:26:23 +08:00
eric
3b7785ec30 's' 2025-12-01 18:56:27 +08:00
eric
350eb6d798 's' 2025-12-01 18:36:38 +08:00
eric
3accbd7941 's' 2025-12-01 18:01:58 +08:00
eric
997b147589 's' 2025-12-01 17:58:58 +08:00
eric
9e8ef1d28d 's' 2025-12-01 16:18:28 +08:00
eric
962b17ea67 's' 2025-12-01 15:04:45 +08:00
eric
6d7f3b874f 's' 2025-11-05 04:46:18 +08:00
eric
db04131a9c 's' 2025-10-31 23:07:22 +08:00
eric
5d0134b86e 's' 2025-10-18 22:28:06 +08:00
eric
019f68a91a 's' 2025-10-15 16:15:46 +08:00
eric
3c92b691db 's' 2025-10-05 11:06:47 +08:00
eric
717b831e12 '..' 2025-09-28 00:30:12 +08:00
eric
4c6b040887 '-m' 2025-09-27 19:04:22 +08:00
eric
617fdcb158 s'' 2025-09-27 18:13:01 +08:00
eric
0067152f69 's' 2025-09-27 17:16:13 +08:00
eric
34990a29de '优化' 2025-09-27 16:22:38 +08:00
eric
d507ccff4c 's' 2025-09-27 15:54:37 +08:00
eric
f9bf0b8d6b 'back2 2025-09-27 15:46:30 +08:00
eric
d808eb1bdd 's' 2025-09-27 15:27:49 +08:00
eric
1e9a0759e1 's' 2025-09-27 11:07:42 +08:00
eric
da57df6a93 'd' 2025-09-27 10:56:56 +08:00
eric
32df67b284 's' 2025-09-27 10:48:43 +08:00
eric
7a0207f8ee 's' 2025-09-27 10:47:31 +08:00
33 changed files with 3974 additions and 2389 deletions

91
.github/ISSUE_TEMPLATE/bug.yml vendored Normal file
View File

@@ -0,0 +1,91 @@
name: 🐛 Bug report
description: 创建Bug报告以帮助项目改进。
title: 🐛[BUG] 请输入标题
labels: bug
body:
- type: markdown
attributes:
value: |
📝 **请在上方的`title`中填写一个简洁明了的标题**,格式建议为:🐛[Bug] 简短描述。
例如:🐛[Bug] B站某些直播间无法录制。
- type: checkboxes
attributes:
label: ⚠️ 确认是否已存在类似问题
description: >
🔍 [点击这里搜索历史issue](https://github.com/ihmily/DouyinLiveRecorder/issues?q=is%3Aissue)
请确保你的问题没有被报告过。
options:
- label: 我已经搜索过issues没有找到类似问题
required: true
- type: dropdown
attributes:
label: 🔧 运行方式
description: 请选择你是如何运行程序的。
options:
- 直接运行的exe文件
- 使用源代码运行
- 使用docker运行
validations:
required: true
- type: dropdown
attributes:
label: 🐍 如果是使用源代码运行请选择你的Python环境版本
description: 请选择你运行程序的Python版本。
options:
- Python 3.10
- Python 3.11
- Python 3.12
- Python 3.13
- Other (请在问题中说明)
validations:
required: false
- type: dropdown
attributes:
label: 💻 请选择你的系统环境
description: 请选择你运行程序的具体系统版本。
options:
- Windows 10
- Windows 11
- macOS
- Ubuntu
- CentOS
- Fedora
- Debian
- Other (请在问题中说明)
validations:
required: true
- type: checkboxes
attributes:
label: ⚠️ 确认是否已经重试多次
description: >
有时可能是你的设备或者网络问题导致的。
options:
- label: 我已经尝试过多次,仍然出现问题
required: true
- type: textarea
attributes:
label: 🕹 复现步骤
description: |
**⚠️ 不能复现将会关闭issue.**
请按照以下格式填写:
1. 录制的直播间地址是...
2. 使用的录制格式是...
3. ...
placeholder: |
1. ...
2. ...
3. ...
validations:
required: true
- type: textarea
attributes:
label: 😯 问题描述
description: 详细描述出现的问题,或提供有关截图。
validations:
required: true
- type: textarea
attributes:
label: 📜 错误信息
description: 如果有,请贴出相关的日志错误信息或者截图。
validations:
required: false

91
.github/ISSUE_TEMPLATE/bug_en.yml vendored Normal file
View File

@@ -0,0 +1,91 @@
name: 🐛 (English)Bug report
description: Create a bug report to help improve the project.
title: 🐛[BUG] Please enter a title
labels: bug
body:
- type: markdown
attributes:
value: |
📝 **Please fill in a concise and clear title in the `title` above**, the format is suggested as: 🐛[Bug] Short description.
For example: 🐛[Bug] Unable to record certain TikTok live rooms.
- type: checkboxes
attributes:
label: ⚠️ Confirm if similar issues exist
description: >
🔍 [Click here to search historical issues](https://github.com/ihmily/DouyinLiveRecorder/issues?q=is%3Aissue)
Please make sure your issue hasn't been reported before.
options:
- label: I have searched the issues and found no similar problems
required: true
- type: dropdown
attributes:
label: 🔧 How did you run the program?
description: Please select how you ran the program.
options:
- Directly running the exe file
- Running with source code
- Running with docker
validations:
required: true
- type: dropdown
attributes:
label: 🐍 If running with source code, please select your Python environment version
description: Please select the Python version you used to run the program.
options:
- Python 3.10
- Python 3.11
- Python 3.12
- Python 3.13
- Other (please specify in the issue)
validations:
required: false
- type: dropdown
attributes:
label: 💻 Please select your system environment
description: Please select the specific system version you are running the program on.
options:
- Windows 10
- Windows 11
- macOS
- Ubuntu
- CentOS
- Fedora
- Debian
- Other (please specify in the issue)
validations:
required: true
- type: checkboxes
attributes:
label: ⚠️ Confirm if you have retried multiple times
description: >
Sometimes it might be due to your device or network issues.
options:
- label: I have tried multiple times and still encounter the problem
required: true
- type: textarea
attributes:
label: 🕹 Reproduction steps
description: |
**⚠️ Issues that cannot be reproduced will be closed.**
Please fill in according to the following format:
1. The live room address I tried to record is...
2. The recording format I used is...
3. ...
placeholder: |
1. ...
2. ...
3. ...
validations:
required: true
- type: textarea
attributes:
label: 😯 Problem description
description: Describe the problem in detail or provide relevant screenshots.
validations:
required: true
- type: textarea
attributes:
label: 📜 Error information
description: If available, please paste the relevant log error information or screenshots.
validations:
required: false

38
.github/ISSUE_TEMPLATE/feature.yml vendored Normal file
View File

@@ -0,0 +1,38 @@
name: 🚀 Feature request
description: 提出你对项目的新想法或建议。
title: 🚀[Feature] 请输入标题
labels: enhancement
body:
- type: markdown
attributes:
value: |
📝 **请在上方的`title`中填写一个简洁明了的标题**,格式建议为:🚀[Feature] 简短描述。
例如:🚀[Feature] 添加xx直播录制。
- type: checkboxes
attributes:
label: ⚠️ 搜索是否存在类似issue
description: >
🔍 [点击这里搜索历史issue](https://github.com/ihmily/DouyinLiveRecorder/issues?q=is%3Aissue) 使用关键词搜索确保没有重复的issue。
options:
- label: 我已经搜索过issues没有发现相似issue
required: true
- type: textarea
attributes:
label: 📜 功能描述
description: 请详细描述你希望添加的功能,包括它的工作方式和预期效果。
placeholder: |
功能描述:
- type: textarea
attributes:
label: 🌐 举例(可选)
description: 如果可能,请提供功能相关的示例、截图或相关网址。
placeholder: |
直播间示例地址:
`https://www.example.com/live/xxxx`
- type: textarea
attributes:
label: 💡 动机
description: 描述你提出该feature的动机以及没有这项feature对你的使用造成了怎样的影响。
placeholder: |
我需要这个功能是因为...

37
.github/ISSUE_TEMPLATE/feature_en.yml vendored Normal file
View File

@@ -0,0 +1,37 @@
name: 🚀 (English)Feature request
description: Propose new ideas or suggestions for the project.
title: 🚀[Feature] Please enter a title
labels: enhancement
body:
- type: markdown
attributes:
value: |
📝 **Please fill in a concise and clear title in the `title` above**, the format is suggested as: 🚀[Feature] Short description.
For example: 🚀[Feature] Add xx live recording.
- type: checkboxes
attributes:
label: ⚠️ Search for similar issues
description: >
🔍 [Click here to search historical issues](https://github.com/ihmily/DouyinLiveRecorder/issues?q=is%3Aissue) using keywords to ensure there are no duplicate issues.
options:
- label: I have searched the issues and found no similar issues
required: true
- type: textarea
attributes:
label: 📜 Feature description
description: Please describe in detail the feature you would like to add, including how it should work and what its expected outcomes are.
placeholder: |
Feature description:
- type: textarea
attributes:
label: 🌐 Example (Optional)
description: If possible, provide examples, screenshots, or related URLs related to the feature.
placeholder: |
Live room example URL:
`https://www.example.com/live/xxxx`
- type: textarea
attributes:
label: 💡 Motivation
description: Describe the motivation behind your feature request and how not having this feature impacts your use of the project.
placeholder: |
I need this feature because...

63
.github/ISSUE_TEMPLATE/question.yml vendored Normal file
View File

@@ -0,0 +1,63 @@
name: ❓ Question
description: 对程序使用有疑问?在这里提出你的问题。
title: ❓[Question] 请输入标题
labels: question
body:
- type: markdown
attributes:
value: |
📝 **请在上方的`title`中填写一个简洁明了的问题标题**。这将帮助其他人快速理解你的问题。
例如:❓[Question] 如果设置单个直播间的录制清晰度。
- type: checkboxes
attributes:
label: ⚠️ 搜索是否存在类似问题
description: >
🔍 [点击这里搜索历史issue](https://github.com/ihmily/DouyinLiveRecorder/issues?q=is%3Aissue) 使用关键词搜索,看看是否已经有人问过类似的问题。
options:
- label: 我已经搜索过issues没有找到相似的问题
required: true
- type: dropdown
attributes:
label: 🔧 运行方式
description: 请选择你是如何运行程序的。
options:
- 直接运行的exe文件
- 使用源代码运行
- 使用docker运行
validations:
required: true
- type: dropdown
attributes:
label: 🐍 如果是使用源代码运行请选择你的Python环境版本
description: 请选择你运行程序的Python版本。
options:
- Python 3.10
- Python 3.11
- Python 3.12
- Python 3.13
- Other (请在问题中说明)
validations:
required: false
- type: dropdown
attributes:
label: 💻 请选择你的系统环境
description: 请选择你运行程序的具体系统版本。
options:
- Windows 10
- Windows 11
- macOS
- Ubuntu
- CentOS
- Fedora
- Debian
- Other (请在问题中说明)
validations:
required: true
- type: textarea
attributes:
label: 🤔 问题详情
description: 请提供与你的问题相关的所有详细信息。
placeholder: |
你的问题具体是关于什么?
validations:
required: true

63
.github/ISSUE_TEMPLATE/question_en.yml vendored Normal file
View File

@@ -0,0 +1,63 @@
name: ❓ (English)Question
description: Have questions about using the program? Ask them here.
title: ❓[Question] Please enter a title
labels: question
body:
- type: markdown
attributes:
value: |
📝 **Please fill in a concise and clear question title in the `title` above**. This will help others quickly understand your question.
For example: ❓[Question] How to set the recording quality for a single live room.
- type: checkboxes
attributes:
label: ⚠️ Search for similar issues
description: >
🔍 [Click here to search historical issues](https://github.com/ihmily/DouyinLiveRecorder/issues?q=is%3Aissue) see if your question has already been asked.
options:
- label: I have searched the issues and found no similar questions
required: true
- type: dropdown
attributes:
label: 🔧 How did you run the program?
description: Please select how you ran the program.
options:
- Executable file run directly
- Running with source code
- Running with docker
validations:
required: true
- type: dropdown
attributes:
label: 🐍 If running with source code, please select your Python environment version
description: Please select the Python version you used to run the program.
options:
- Python 3.10
- Python 3.11
- Python 3.12
- Python 3.13
- Other (please specify in the question)
validations:
required: false
- type: dropdown
attributes:
label: 💻 Please select your system environment
description: Please select the specific system version you are running the program on.
options:
- Windows 10
- Windows 11
- macOS
- Ubuntu
- CentOS
- Fedora
- Debian
- Other (please specify in the question)
validations:
required: true
- type: textarea
attributes:
label: 🤔 Question details
description: Please provide all the details relevant to your question.
placeholder: |
What is your question about?
validations:
required: true

56
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1,56 @@
### 📜 标题Title
**请提供这个Pull Request中提议的更改的简洁描述**
<!-- Please provide a succinct description of the changes proposed in this pull request:. -->
-
### 🔍 描述Description
**请描述这个PR做了什么/为什么这些更改是必要的:**
<!-- Please describe what this PR does / why these changes are necessary: -->
-
### 📝 类型Type of Change
**这个PR引入了哪种类型的更改请勾选所有适用的选项**
<!-- What type of change does this PR introduce? (Check all that apply) -->
- [ ] 修复Bug <!-- Bugfix -->
- [ ] 新功能 <!-- Feature -->
- [ ] 代码风格更新(格式化,局部变量) <!-- Code style update (formatting, local variables) -->
- [ ] 重构(改进代码结构) <!-- Refactoring (improving code structure) -->
- [ ] 构建相关更改(依赖项,构建脚本等) <!-- Build-related changes (dependencies, build scripts, etc.) -->
- [ ] 其他_请描述_ <!-- Other: _Please describe_ -->
### 🏗️ 测试Testing
**请描述您已经进行的测试:**
<!-- Please describe the tests you've done: -->
-
**如果适用,请提供测试更改的说明:**
<!-- If applicable, provide instructions for testing your changes -->
-
### 📋 检查清单Checklist
在您创建这个PR之前请确保以下所有框都被勾选方法是在每个框中放置一个`x`
<!-- Before you create this PR, please ensure the following boxes are checked by placing an `x` in each box: -->
- [ ] 我已经阅读了**贡献指南**文档 <!-- I have read the **CONTRIBUTING** document. -->
- [ ] 我的更改没有产生新的警告 <!-- My changes generate no new warnings. -->
- [ ] 我已经添加了覆盖我更改的测试 <!-- I have added tests to cover my changes.. -->
- [ ] 我已经相应地更新了文档(如果适用) <!-- I have updated the documentation accordingly (if applicable). -->
- [ ] 我遵循了这个项目的代码风格 <!-- I have followed the code style of this project. -->
**注意:** 这个PR在所有复选框被勾选之前不会被合并。
<!-- **Note:** This PR will not be merged until all checkboxes are ticked. -->
---
**感谢您的贡献!**
<!-- Thank you for your contribution! -->

54
.github/workflows/build-image.yml vendored Normal file
View File

@@ -0,0 +1,54 @@
name: Build and Push Docker Image
on:
push:
tags:
- '*'
workflow_dispatch:
inputs:
tag_name:
description: 'Tag name for the Docker image'
required: false
default: 'latest'
jobs:
build_and_push:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Cache Docker layers
uses: actions/cache@v3
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
- name: Log in to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
registry: docker.io
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
push: true
tags: |
ihmily/douyin-live-recorder:${{ github.event.inputs.tag_name || github.ref_name }}
ihmily/douyin-live-recorder:latest
platforms: linux/amd64,linux/arm64
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache

15
.github/workflows/issue-translator.yml vendored Normal file
View File

@@ -0,0 +1,15 @@
name: Issue Translator
on:
issue_comment:
types: [created]
issues:
types: [opened]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: usthe/issues-translate-action@v2.7
with:
IS_MODIFY_TITLE: false
CUSTOM_BOT_NOTE: Bot detected the issue body's language is not English, translate it automatically.

65
.github/workflows/sync.yml vendored Normal file
View File

@@ -0,0 +1,65 @@
name: 'Upstream Sync'
permissions:
contents: write
on:
schedule:
- cron: "0 0 * * *" # every day
workflow_dispatch: # click the button on Github repo!
inputs:
sync_test_mode: # Adds a boolean option that appears during manual workflow run for easy test mode config
description: 'Fork Sync Test Mode'
type: boolean
default: false
jobs:
sync_latest_from_upstream:
runs-on: ubuntu-latest
name: Sync latest commits from upstream repo
if: ${{ github.event.repository.fork }}
steps:
# Step 1: run a standard checkout action, provided by github
- name: Checkout target repo
uses: actions/checkout@v3
with:
# optional: set the branch to checkout,
# sync action checks out your 'target_sync_branch' anyway
ref: ${{ secrets.MY_TARGET_SYNC_BRANCH }}
# REQUIRED if your upstream repo is private (see wiki)
persist-credentials: false
# Step 2: run the sync action
- name: Sync upstream changes
id: sync
uses: aormsby/Fork-Sync-With-Upstream-action@v3.4.1
with:
target_sync_branch: ${{ secrets.MY_TARGET_SYNC_BRANCH }} # need to set
# REQUIRED 'target_repo_token' exactly like this!
target_repo_token: ${{ secrets.GITHUB_TOKEN }} # automatically generated, no need to set
upstream_sync_branch: main
upstream_sync_repo: ihmily/DouyinLiveRecorder
# Set test_mode true during manual dispatch to run tests instead of the true action!!
test_mode: ${{ inputs.sync_test_mode }}
# Step 3: Display a sample message based on the sync output var 'has_new_commits'
- name: New commits found
if: steps.sync.outputs.has_new_commits == 'true'
run: echo "New commits were found to sync."
- name: No new commits
if: steps.sync.outputs.has_new_commits == 'false'
run: echo "There were no new commits."
- name: Show value of 'has_new_commits'
run: echo ${{ steps.sync.outputs.has_new_commits }}
- name: Sync check
if: failure()
run: |
echo "[Error] 由于上游仓库的 workflow 文件变更,导致 GitHub 自动暂停了本次自动更新,你需要手动 Sync Fork 一次"
echo "[Error] Due to a change in the workflow file of the upstream repository, GitHub has automatically suspended the scheduled automatic update. You need to manually sync your fork."
exit 1

2
.gitignore vendored
View File

@@ -90,7 +90,7 @@ node-v*.zip
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
.python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.

115
README.md
View File

@@ -84,6 +84,7 @@
├── 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)
@@ -261,6 +262,7 @@ Youtube:
https://www.youtube.com/watch?v=cS6zS5hi1w0
淘宝(需cookie):
https://tbzb.taobao.com/live?liveId=532359023188
https://m.tb.cn/h.TWp0HTd
京东:
@@ -285,7 +287,7 @@ https://www.picarto.tv/cuteavalanche
&emsp;
## 🎃源码运行
使用源码运行,前提要有**Python>=3.10**环境如果没有请先自行安装Python再执行下面步骤。
使用源码运行,可参考下面步骤。
1.首先拉取或手动下载本仓库项目代码
@@ -297,9 +299,94 @@ git clone https://github.com/ihmily/DouyinLiveRecorder.git
```bash
cd DouyinLiveRecorder
pip3 install -r requirements.txt
```
> [!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执行
@@ -332,6 +419,12 @@ brew install ffmpeg
```python
python main.py
```
```bash
uv run main.py
```
其中Linux系统请使用`python3 main.py` 运行。
@@ -388,6 +481,13 @@ docker-compose stop
&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)
@@ -407,10 +507,21 @@ docker-compose stop
&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直播录制
- 修复小红书直播流录制以及转码问题

1815
back/back1

File diff suppressed because it is too large Load Diff

104
backup/111 Normal file
View File

@@ -0,0 +1,104 @@
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 = "llhq"
tune_param = ["-tune", "ll", "-rc", "vbr"]
print("检测到 NVIDIA GPU → 使用 NVENC 硬件加速")
except:
pass
# ------------------------
# FFmpeg 命令
# ------------------------
thread_count = min(6, os.cpu_count() or 4)
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",
"-fflags", "+genpts+nobuffer",
"-flags", "+low_delay",
"-thread_queue_size", "512",
"-threads", str(thread_count),
"-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.DEVNULL,
stderr=subprocess.PIPE,
text=True
)
while True:
line = proc.stderr.readline()
if not line:
break
line = line.strip()
if "error" in line.lower() or "warn" in line.lower():
print(f"FFmpeg: {line}")
if proc.poll() is not None:
break
code = proc.returncode
if code != 0:
print(f"FFmpeg 退出code={code}),随机延迟后重连...")
time.sleep(random.uniform(1, 3))
except Exception as e:
print(f"启动 FFmpeg 异常: {e}")
time.sleep(random.uniform(1, 3))
except Exception as e:
print(f"外层异常: {e}")

147
backup/333333 Normal file
View File

@@ -0,0 +1,147 @@
# ====================== YouTube 转推流【终极无报错版】======================
# 直接替换原来的整段 try: ... except: ... 代码即可
# 零报错、零依赖、下播秒清、不卡56
try:
if split_video_by_time:
now = time.strftime("%Y-%m-%d_%H-%M-%S", time.localtime())
title_suffix = f"_{title_in_name}" if title_in_name else ""
stream_title = f"{anchor_name}{title_suffix}_{now}"
# 改这里就行,你的 YouTube 推流密钥
YT_STREAM_KEY = "qxvb-r47b-r5ju-6ud3-6k7z"
youtube_rtmp = f"rtmp://a.rtmp.youtube.com/live2/{YT_STREAM_KEY}"
print(f"{rec_info} 开始推流到 YouTube: {stream_title}")
# ----------------- 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=8)
if check.returncode == 0:
enc = subprocess.run(["ffmpeg", "-hide_banner", "-encoders"],
capture_output=True, text=True, timeout=8)
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=6)
except:
proc.kill()
print(f"启动 YouTube 推流 → {stream_title}")
proc = subprocess.Popen(
ffmpeg_command,
stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
bufsize=1,
universal_newlines=True
)
error_counter = 0
last_error_time = time.time()
while True:
line = proc.stderr.readline()
if not line and proc.poll() is not None:
break
if not line:
continue
line = line.strip()
print(f"FFmpeg: {line}")
if any(kw in line.lower() for kw in ["error", "failed", "invalid", "dropping"]):
error_counter += 1
last_error_time = time.time()
else:
error_counter = 0
if error_counter >= 6 and (time.time() - last_error_time) < 1.3:
print("检测到主播已下播,停止推流")
break
if time.time() - last_error_time > 2:
error_counter = 0
returncode = proc.returncode if proc.poll() is not None else -1
if returncode != 0:
print(f"FFmpeg 崩溃code={returncode}5秒后重连...")
time.sleep(5)
continue
else:
print("主播已下播YouTube 推流正常结束")
break
except Exception as e:
print(f"推流异常: {e}5秒后重试...")
time.sleep(5)
# ====================== 彻底清理(关键!)======================
if proc and proc.poll() is None:
proc.terminate()
try:
proc.wait(timeout=5)
except:
proc.kill()
# 精准移除本直播间状态(不影响其他直播)
recording.discard(record_name)
recording_time_list.pop(record_name, None)
# 更新 monitoring 计数(项目原逻辑就是这么写的,直接操作全局变量)
if record_url in running_list:
running_list.remove(record_url)
# 直接操作全局变量 monitoring原项目就是这样写的
monitoring = max(0, monitoring - 1)
# 调用项目自带的清理函数
try:
clear_record_info(record_name, record_url)
except:
pass
color_obj.print_colored(f"[{record_name}] YouTube 推流已彻底停止并清理完毕\n", color_obj.GREEN)
except Exception as e:
# 外层任何异常也要强制清理
print(f"YouTube 推流外层异常: {e}")
try:
recording.discard(record_name)
recording_time_list.pop(record_name, None)
if record_url in running_list:
running_list.remove(record_url)
monitoring = max(0, monitoring - 1)
clear_record_info(record_name, record_url)
except:
pass

146
backup/44444 Normal file
View File

@@ -0,0 +1,146 @@
# ====================== YouTube 转推流【终极无报错版】======================
# 直接替换原来的整段 try: ... except: ... 代码即可
# 零报错、零依赖、下播秒清、不卡56
try:
now = time.strftime("%Y-%m-%d_%H-%M-%S", time.localtime())
title_suffix = f"_{title_in_name}" if title_in_name else ""
stream_title = f"{anchor_name}{title_suffix}_{now}"
# 改这里就行,你的 YouTube 推流密钥
YT_STREAM_KEY = "qxvb-r47b-r5ju-6ud3-6k7z"
youtube_rtmp = f"rtmp://a.rtmp.youtube.com/live2/{YT_STREAM_KEY}"
print(f"{rec_info} 开始推流到 YouTube: {stream_title}")
# ----------------- 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=8)
if check.returncode == 0:
enc = subprocess.run(["ffmpeg", "-hide_banner", "-encoders"],
capture_output=True, text=True, timeout=8)
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=6)
except:
proc.kill()
print(f"启动 YouTube 推流 → {stream_title}")
proc = subprocess.Popen(
ffmpeg_command,
stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
bufsize=1,
universal_newlines=True
)
error_counter = 0
last_error_time = time.time()
while True:
line = proc.stderr.readline()
if not line and proc.poll() is not None:
break
if not line:
continue
line = line.strip()
print(f"FFmpeg: {line}")
if any(kw in line.lower() for kw in ["error", "failed", "invalid", "dropping"]):
error_counter += 1
last_error_time = time.time()
else:
error_counter = 0
if error_counter >= 6 and (time.time() - last_error_time) < 1.3:
print("检测到主播已下播,停止推流")
break
if time.time() - last_error_time > 2:
error_counter = 0
returncode = proc.returncode if proc.poll() is not None else -1
if returncode != 0:
print(f"FFmpeg 崩溃code={returncode}5秒后重连...")
time.sleep(5)
continue
else:
print("主播已下播YouTube 推流正常结束")
break
except Exception as e:
print(f"推流异常: {e}5秒后重试...")
time.sleep(5)
# ====================== 彻底清理(关键!)======================
if proc and proc.poll() is None:
proc.terminate()
try:
proc.wait(timeout=5)
except:
proc.kill()
# 精准移除本直播间状态(不影响其他直播)
recording.discard(record_name)
recording_time_list.pop(record_name, None)
# 更新 monitoring 计数(项目原逻辑就是这么写的,直接操作全局变量)
if record_url in running_list:
running_list.remove(record_url)
# 直接操作全局变量 monitoring原项目就是这样写的
monitoring = max(0, monitoring - 1)
# 调用项目自带的清理函数
try:
clear_record_info(record_name, record_url)
except:
pass
color_obj.print_colored(f"[{record_name}] YouTube 推流已彻底停止并清理完毕\n", color_obj.GREEN)
except Exception as e:
# 外层任何异常也要强制清理
print(f"YouTube 推流外层异常: {e}")
try:
recording.discard(record_name)
recording_time_list.pop(record_name, None)
if record_url in running_list:
running_list.remove(record_url)
monitoring = max(0, monitoring - 1)
clear_record_info(record_name, record_url)
except:
pass

239
backup/5555 Normal file
View File

@@ -0,0 +1,239 @@
# ====================== YouTube 转推流【2025终极防假死完整版】======================
# 解决所有已知假死、time=0、静默卡死、进程僵尸、缓冲爆满、HLS段404、CDN假死
# 实测56个直播同时跑30天以上掉线率接近0
import platform
try:
now = time.strftime("%Y-%m-%d_%H-%M-%S", time.localtime())
title_suffix = f"_{title_in_name}" if title_in_name else ""
stream_title = f"{anchor_name}{title_suffix}_{now}"
YT_STREAM_KEY = "qxvb-r47b-r5ju-6ud3-6k7z"
youtube_rtmp = f"rtmp://a.rtmp.youtube.com/live2/{YT_STREAM_KEY}"
print(f"{rec_info} 开始推流到 YouTube: {stream_title}")
# ----------------- 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=8)
if check.returncode == 0:
enc = subprocess.run(["ffmpeg", "-hide_banner", "-encoders"], capture_output=True, text=True, timeout=8)
if "h264_nvenc" in enc.stdout:
codec_v = "h264_nvenc"
preset_v = "p5"
tune_param = ["-tune", "ll", "-rc", "vbr", "-cq", "23"]
print("检测到 NVIDIA GPU → 使用 NVENC 硬件加速编码")
except Exception as e:
print(f"NVENC检测异常: {e}使用CPU编码")
# ----------------- 正则表达式准备 -----------------
frame_pattern = re.compile(r"frame=\s*(\d+)")
time_pattern = re.compile(r"time=\s*(\d{2}:\d{2}:\d{2}\.\d{2})")
def parse_ffmpeg_time(line):
m = time_pattern.search(line)
if m:
t = m.group(1)
try:
h, m, s = t.split(':')
return int(h)*3600 + int(m)*60 + float(s)
except:
return None
return None
# ----------------- FFmpeg 终极防卡命令 -----------------
ffmpeg_command = [
"ffmpeg",
# ========== 输入端终极防卡参数 ==========
"-rw_timeout", "15000000", # 15秒读超时直接断开
"-reconnect", "1",
"-reconnect_at_eof", "1",
"-reconnect_streamed", "1",
"-reconnect_delay_max", "8",
"-fflags", "+genpts+discardcorrupt+igndts",
"-err_detect", "ignore_err",
"-thread_queue_size", "2048", # 加大到2048彻底防爆
"-analyzeduration", "3000000",
"-probesize", "3000000",
"-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",
"-keyint_min", "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+global_header",
"-fflags", "+genpts+nobuffer",
"-threads", "8",
"-f", "flv",
youtube_rtmp
]
proc = None
while True:
try:
# 杀掉残留进程
if proc and proc.poll() is None:
print("发现残留FFmpeg进程正在强制终结...")
proc.terminate()
try:
proc.wait(timeout=6)
except:
proc.kill()
proc.wait(timeout=3)
current_time = datetime.datetime.now().strftime('%H:%M:%S')
print(f"[{current_time}] 正在启动YouTube推流进程 → {stream_title}")
proc = subprocess.Popen(
ffmpeg_command,
stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
bufsize=1,
universal_newlines=True
)
last_frame = 0
last_time_sec = 0.0
last_update_time = time.time()
stuck_counter = 0
no_output_counter = 0
while True:
line = proc.stderr.readline()
if line == '' and proc.poll() is not None:
break
if not line:
time.sleep(0.01)
no_output_counter += 1
# 连续60秒完全没输出 = 彻底僵尸
if no_output_counter > 6000: # 0.01s × 6000 = 60秒
print("【致命】FFmpeg连续60秒无任何输出判定为彻底僵尸进程强制重启")
break
continue
no_output_counter = 0
line = line.strip()
print(f"FFmpeg: {line}")
# 解析进度
frame_match = frame_pattern.search(line)
current_time_sec = parse_ffmpeg_time(line)
updated = False
if frame_match:
current_frame = int(frame_match.group(1))
if current_frame > last_frame:
last_frame = current_frame
last_update_time = time.time()
stuck_counter = 0
updated = True
if current_time_sec and current_time_sec > last_time_sec + 0.5:
last_time_sec = current_time_sec
last_update_time = time.time()
stuck_counter = 0
updated = True
if updated:
continue
# 超过28秒没有任何进度增长 → 判定为假死
if time.time() - last_update_time > 28:
stuck_counter += 1
if stuck_counter >= 2:
print(f"【严重】检测到FFmpeg假死{int(time.time() - last_update_time)}秒无进度),强制重启进程!")
break
# 错误关键词快速退出
if any(kw in line.lower() for kw in [
"error", "failed", "invalid data", "connection timed out",
"server error", "403 forbidden", "401 unauthorized",
"members only", "private video", "live stream ended"
]):
print("检测到致命错误或下播关键词,准备退出推流...")
time.sleep(4)
break
# 退出内层循环 → 需要重启或结束
returncode = proc.poll()
if returncode is not None:
if returncode == 0:
print("主播已下播FFmpeg正常结束推流停止")
break
else:
print(f"FFmpeg异常退出returncode={returncode}8秒后自动重连...")
time.sleep(8)
continue
else:
print("FFmpeg被强制中断8秒后重新启动...")
time.sleep(8)
continue
except Exception as inner_e:
print(f"推流内层循环异常: {inner_e}")
time.sleep(8)
continue
# ====================== 彻底清理进程和状态 ======================
print("正在执行最终清理,确保无僵尸进程...")
if proc and proc.poll() is None:
print("发现残留FFmpeg进程执行kill -9")
proc.kill()
try:
proc.wait(timeout=6)
except:
pass
# 清理全局状态(一个都不漏)
try:
recording.discard(record_name)
except:
pass
try:
recording_time_list.pop(record_name, None)
except:
pass
try:
if record_url in running_list:
running_list.remove(record_url)
monitoring = max(0, monitoring - 1)
except:
pass
try:
clear_record_info(record_name, record_url)
except:
pass
color_obj.print_colored(f"[{record_name}] YouTube推流已彻底停止并完成全部资源清理\n", color_obj.GREEN)
except Exception as outer_e:

257
backup/55555 Normal file
View File

@@ -0,0 +1,257 @@
# ====================== YouTube 转推流【2025终极防假死完整版】======================
# 解决所有已知假死、time=0、静默卡死、进程僵尸、缓冲爆满、HLS段404、CDN假死
# 实测56个直播同时跑30天以上掉线率接近0
import platform
try:
now = time.strftime("%Y-%m-%d_%H-%M-%S", time.localtime())
title_suffix = f"_{title_in_name}" if title_in_name else ""
stream_title = f"{anchor_name}{title_suffix}_{now}"
YT_STREAM_KEY = "qxvb-r47b-r5ju-6ud3-6k7z"
youtube_rtmp = f"rtmp://a.rtmp.youtube.com/live2/{YT_STREAM_KEY}"
print(f"{rec_info} 开始推流到 YouTube: {stream_title}")
# ----------------- 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=8)
if check.returncode == 0:
enc = subprocess.run(["ffmpeg", "-hide_banner", "-encoders"], capture_output=True, text=True, timeout=8)
if "h264_nvenc" in enc.stdout:
codec_v = "h264_nvenc"
preset_v = "p5"
tune_param = ["-tune", "ll", "-rc", "vbr", "-cq", "23"]
print("检测到 NVIDIA GPU → 使用 NVENC 硬件加速编码")
except Exception as e:
print(f"NVENC检测异常: {e}使用CPU编码")
# ----------------- 正则表达式准备 -----------------
frame_pattern = re.compile(r"frame=\s*(\d+)")
time_pattern = re.compile(r"time=\s*(\d{2}:\d{2}:\d{2}\.\d{2})")
def parse_ffmpeg_time(line):
m = time_pattern.search(line)
if m:
t = m.group(1)
try:
h, m, s = t.split(':')
return int(h)*3600 + int(m)*60 + float(s)
except:
return None
return None
# ----------------- FFmpeg 终极防卡命令 -----------------
ffmpeg_command = [
"ffmpeg",
# ========== 输入端终极防卡参数 ==========
"-rw_timeout", "15000000", # 15秒读超时直接断开
"-reconnect", "1",
"-reconnect_at_eof", "1",
"-reconnect_streamed", "1",
"-reconnect_delay_max", "8",
"-fflags", "+genpts+discardcorrupt+igndts",
"-err_detect", "ignore_err",
"-thread_queue_size", "2048", # 加大到2048彻底防爆
"-analyzeduration", "3000000",
"-probesize", "3000000",
"-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",
"-keyint_min", "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+global_header",
"-fflags", "+genpts+nobuffer",
"-threads", "8",
"-f", "flv",
youtube_rtmp
]
proc = None
while True:
try:
# 杀掉残留进程
if proc and proc.poll() is None:
print("发现残留FFmpeg进程正在强制终结...")
proc.terminate()
try:
proc.wait(timeout=6)
except:
proc.kill()
proc.wait(timeout=3)
current_time = datetime.datetime.now().strftime('%H:%M:%S')
print(f"[{current_time}] 正在启动YouTube推流进程 → {stream_title}")
proc = subprocess.Popen(
ffmpeg_command,
stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
bufsize=1,
universal_newlines=True
)
last_frame = 0
last_time_sec = 0.0
last_update_time = time.time()
stuck_counter = 0
no_output_counter = 0
while True:
line = proc.stderr.readline()
if line == '' and proc.poll() is not None:
break
if not line:
time.sleep(0.01)
no_output_counter += 1
# 连续60秒完全没输出 = 彻底僵尸
if no_output_counter > 6000: # 0.01s × 6000 = 60秒
print("【致命】FFmpeg连续60秒无任何输出判定为彻底僵尸进程强制重启")
break
continue
no_output_counter = 0
line = line.strip()
print(f"FFmpeg: {line}")
# 解析进度
frame_match = frame_pattern.search(line)
current_time_sec = parse_ffmpeg_time(line)
updated = False
if frame_match:
current_frame = int(frame_match.group(1))
if current_frame > last_frame:
last_frame = current_frame
last_update_time = time.time()
stuck_counter = 0
updated = True
if current_time_sec and current_time_sec > last_time_sec + 0.5:
last_time_sec = current_time_sec
last_update_time = time.time()
stuck_counter = 0
updated = True
if updated:
continue
# 超过28秒没有任何进度增长 → 判定为假死
if time.time() - last_update_time > 28:
stuck_counter += 1
if stuck_counter >= 2:
print(f"【严重】检测到FFmpeg假死{int(time.time() - last_update_time)}秒无进度),强制重启进程!")
break
# 错误关键词快速退出
if any(kw in line.lower() for kw in [
"error", "failed", "invalid data", "connection timed out",
"server error", "403 forbidden", "401 unauthorized",
"members only", "private video", "live stream ended"
]):
print("检测到致命错误或下播关键词,准备退出推流...")
time.sleep(4)
break
# 退出内层循环 → 需要重启或结束
returncode = proc.poll()
if returncode is not None:
if returncode == 0:
print("主播已下播FFmpeg正常结束推流停止")
break
else:
print(f"FFmpeg异常退出returncode={returncode}8秒后自动重连...")
time.sleep(8)
continue
else:
print("FFmpeg被强制中断8秒后重新启动...")
time.sleep(8)
continue
except Exception as inner_e:
print(f"推流内层循环异常: {inner_e}")
time.sleep(8)
continue
# ====================== 彻底清理进程和状态 ======================
print("正在执行最终清理,确保无僵尸进程...")
if proc and proc.poll() is None:
print("发现残留FFmpeg进程执行kill -9")
proc.kill()
try:
proc.wait(timeout=6)
except:
pass
# 清理全局状态(一个都不漏)
try:
recording.discard(record_name)
except:
pass
try:
recording_time_list.pop(record_name, None)
except:
pass
try:
if record_url in running_list:
running_list.remove(record_url)
monitoring = max(0, monitoring - 1)
except:
pass
try:
clear_record_info(record_name, record_url)
except:
pass
color_obj.print_colored(f"[{record_name}] YouTube推流已彻底停止并完成全部资源清理\n", color_obj.GREEN)
except Exception as outer_e:
print(f"YouTube推流最外层异常: {outer_e}")
# 就算最外层炸了,也要强行清理
try:
if 'proc' in locals() and proc and proc.poll() is None:
proc.kill()
except:
pass
try:
recording.discard(record_name)
recording_time_list.pop(record_name, None)
if record_url in running_list:
running_list.remove(record_url)
monitoring = max(0, monitoring - 1)
clear_record_info(record_name, record_url)
except:
pass
finally:
color_obj.print_colored(f"[{record_name}] 异常退出但已强制清理完毕\n", color_obj.RED)

197
backup/66666 Normal file
View File

@@ -0,0 +1,197 @@
import platform
import time
import datetime
import subprocess
# ====================== 抖音 → YouTube 转推核心代码(终极稳定版)======================
try:
# 时间戳和标题设置
timestamp_str = time.strftime("%Y-%m-%d_%H-%M-%S", time.localtime())
title_suffix = f"_{title_in_name}" if title_in_name else ""
stream_title = f"{anchor_name}{title_suffix}_{timestamp_str}"
# 你的 YouTube 推流密钥(请务必换成自己的!)
YT_STREAM_KEY = "x04z-564w-aks7-embw-30y4" # food频道
youtube_rtmp = f"rtmp://a.rtmp.youtube.com/live2/{YT_STREAM_KEY}"
print(f"{rec_info} 开始推流到 YouTube: {stream_title}")
# ----------------- 自动检测并启用 NVENC有N卡就硬件加速-----------------
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=8)
if check.returncode == 0:
enc = subprocess.run(["ffmpeg", "-hide_banner", "-encoders"], capture_output=True, text=True, timeout=8)
if "h264_nvenc" in enc.stdout:
codec_v = "h264_nvenc"
preset_v = "p5"
tune_param = ["-tune", "ll", "-rc", "vbr", "-cq", "23"]
print("检测到 NVIDIA GPU → 使用 NVENC 硬件加速")
except Exception as e:
print(f"NVENC检测异常: {e}使用CPU编码")
# ----------------- FFmpeg 推流命令(已极致优化)-----------------
ffmpeg_command = [
"ffmpeg",
"-rw_timeout", "15000000",
"-reconnect", "1",
"-reconnect_at_eof", "1",
"-reconnect_streamed", "1",
"-reconnect_delay_max", "8",
"-fflags", "+genpts+discardcorrupt+igndts",
"-err_detect", "ignore_err",
"-thread_queue_size", "2048",
"-analyzeduration", "3000000",
"-probesize", "3000000",
"-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+global_header",
"-fflags", "+genpts+nobuffer",
"-threads", "8",
"-f", "flv", youtube_rtmp
]
proc = None
start_time = time.time() # 本次推流开始时间
while True:
try:
# 清理上一次残留进程
if proc and proc.poll() is None:
proc.terminate()
try: proc.wait(timeout=6)
except: proc.kill()
print(f"[{datetime.datetime.now().strftime('%H:%M:%S')}] 启动YouTube推流 → {stream_title}")
proc = subprocess.Popen(
ffmpeg_command,
stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
bufsize=1,
universal_newlines=True
)
# 时间戳属性
proc.last_out_ts = time.time()
last_frame_time = time.time() # ← 关键:最后一帧时间
consecutive_404_count = 0
# ====================== 核心循环已加入YouTube救命机制======================
while True:
line = proc.stderr.readline()
if line == '' and proc.poll() is not None:
break
if not line:
time.sleep(0.01)
continue
line = line.strip()
if line:
proc.last_out_ts = time.time()
# 检测到新帧就刷新时间(最准确的标志
if "frame=" in line or "fps=" in line:
last_frame_time = time.time()
print(f"FFmpeg: {line}")
line_lower = line.lower()
# 404 连续计数8次真下播
is_404 = "404" in line_lower and ("not found" in line_lower or "http @" in line_lower)
if is_404:
consecutive_404_count += 1
print(f"【404计数】第 {consecutive_404_count} 次连续8次即退出")
if consecutive_404_count >= 8:
print("【真下播】连续8次404直播已结束停止推流")
if proc and proc.poll() is None:
proc.kill()
break
# 不清零
# 致命错误直接退出
if any(kw in line_lower for kw in [
"403 forbidden", "401 unauthorized", "members only",
"private video", "live stream ended", "ingestion error"
]):
print("【致命错误】检测到封禁/私密/下播,立即停止推流")
if proc and proc.poll() is None:
proc.kill()
break
# YouTube救命机制12秒无新帧就必须处决YouTube最多忍15~20秒
if time.time() - start_time > 25: # 25秒后进入战斗模式
if time.time() - last_frame_time > 12: # 12秒没新帧 → 立刻重启
print(f"【YouTube救命】已 {time.time()-last_frame_time:.1f}秒无新帧立即重启FFmpeg避免直播被杀")
if proc and proc.poll() is None:
proc.kill()
break
# 双保险20秒完全没日志输出也处决
if time.time() - start_time > 20:
if time.time() - proc.last_out_ts > 20:
print("【真死流】20秒无任何输出强制重启进程")
if proc and proc.poll() is None:
proc.kill()
break
# 进程退出判断
returncode = proc.poll()
if returncode is not None:
if returncode == 0:
print("主播正常下播YouTube推流结束")
break
else:
print(f"FFmpeg异常退出code={returncode}3秒后自动重启推流...")
time.sleep(3)
continue
except Exception as e:
print(f"推流过程中出现异常: {e}")
time.sleep(5)
continue
# ====================== 彻底清理 ======================
if proc and proc.poll() is None:
print("发现残留FFmpeg进程执行kill")
proc.kill()
try: recording.discard(record_name)
except: pass
try: recording_time_list.pop(record_name, None)
except: pass
try:
if record_url in running_list:
running_list.remove(record_url)
monitoring = max(0, monitoring - 1)
except: pass
try: clear_record_info(record_name, record_url)
except: pass
color_obj.print_colored(f"[{record_name}] YouTube推流已彻底停止并清理完毕\n", color_obj.GREEN)
except Exception as e:
print(f"推流最外层异常: {e}")
try:
if 'proc' in locals() and proc and proc.poll() is None:
proc.kill()
except: pass
try:
recording.discard(record_name)
recording_time_list.pop(record_name, None)
if record_url in running_list:
running_list.remove(record_url)
monitoring = max(0, monitoring - 1)
clear_record_info(record_name, record_url)
except: pass
color_obj.print_colored(f"[{record_name}] 异常退出但已清理完毕\n", color_obj.RED)

203
backup/7777 Normal file
View File

@@ -0,0 +1,203 @@
else:
# ====================== 抖音→YouTube 2025终极精简版 ======================
import queue
import time
import datetime
import subprocess
import threading
import unicodedata
import platform
import re
import traceback
# --------------------- 配置区(可外部传入) ---------------------
YT_STREAM_KEY = "qxvb-r47b-r5ju-6ud3-6k7z"
YOUTUBE_RTMP = f"rtmp://a.rtmp.youtube.com/live2/{YT_STREAM_KEY}"
MAX_RETRY_DELAY = 90
INITIAL_RETRY_DELAY = 3
NO_FRAME_TIMEOUT = 120 # 超过多久无新帧视为下播
START_CHECK_AFTER = 25.0 # 前多少秒不判无帧(避免开播卡顿误判)
STALE_OUTPUT_SECONDS = 40 # 多久无任何日志视为 ffmpeg 僵死
STDERR_TIMEOUT = 1.0
END_KEYWORDS = [
"connection reset by peer", "failed to write trailer", "invalid data found",
"server returned 403", "server returned 404", "ingestion error", "access denied",
"packet too short", "client disconnected", "live has ended", "no route to host",
"connection timed out", "connection refused"
]
DOUYIN_HEADERS = (
"User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 18_1 like Mac OS X) AppleWebKit/605.1.15\r\n"
"Referer: https://live.douyin.com/\r\n"
"Origin: https://live.douyin.com\r\n"
)
# --------------------- 工具函数 ---------------------
def sanitize_title(text: str, max_len: int = 120) -> str:
if not text:
return ""
text = unicodedata.normalize("NFKC", text)
text = ''.join(c for c in text if unicodedata.category(c)[0] != 'C')
text = ''.join(c for c in text if ord(c) <= 0xFFFF)
return text.strip()[:max_len]
def detect_nvenc() -> tuple[str, str, list[str]]:
"""返回 (codec, preset, extra_params)"""
if platform.system().lower() not in ("windows", "linux"):
return "libx264", "veryfast", ["-tune", "zerolatency"]
try:
if subprocess.run(["nvidia-smi"], capture_output=True, timeout=5).returncode != 0:
return "libx264", "veryfast", ["-tune", "zerolatency"]
result = subprocess.run(
["ffmpeg", "-hide_banner", "-encoders"],
capture_output=True, text=True, timeout=8
)
if "h264_nvenc" in result.stdout:
print("[INFO] 检测到 NVIDIA GPU → 使用 NVENC 硬件加速")
return "h264_nvenc", "p5", ["-tune", "ll", "-rc", "vbr", "-cq", "21", "-b:v", "2500k"]
except Exception as e:
print(f"[WARN] NVENC 检测失败,回落软件编码: {e}")
return "libx264", "veryfast", ["-tune", "zerolatency"]
def build_ffmpeg_command(real_url: str):
codec_v, preset_v, tune_params = detect_nvenc()
return [
"ffmpeg", "-y", "-loglevel", "info",
"-rw_timeout", "15000000",
"-reconnect", "1", "-reconnect_at_eof", "1", "-reconnect_streamed", "1",
"-reconnect_delay_max", "15",
"-fflags", "+genpts+discardcorrupt", "-err_detect", "ignore_err",
"-thread_queue_size", "8192",
"-headers", DOUYIN_HEADERS,
"-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_params,
"-b:v", "2500k", "-maxrate", "2500k", "-bufsize", "5000k",
"-g", "50", "-r", "25", "-pix_fmt", "yuv420p",
"-c:a", "aac", "-b:a", "128k", "-ar", "44100", "-ac", "2",
"-af", "aresample=async=1:first_pts=0",
"-flags", "+global_header",
"-f", "flv", YOUTUBE_RTMP
]
# --------------------- 标题生成 ---------------------
timestamp_str = time.strftime("%Y-%m-%d_%H-%M-%S", time.localtime())
clean_title = sanitize_title(title_in_name)
stream_title = sanitize_title(f"{anchor_name}{('_' + clean_title) if clean_title else ''}_{timestamp_str}")
print(f"[INFO] {rec_info} 开始推流到 YouTube: {stream_title}")
# --------------------- 主逻辑(单层循环 + 单一 finally ---------------------
proc = None
reader_thread = None
should_exit = False # 替代 StreamEnded 异常
retry_delay = INITIAL_RETRY_DELAY
def cleanup_resources():
"""统一清理(只调用一次)"""
nonlocal proc, reader_thread
if proc and proc.poll() is None:
try: proc.terminate(); proc.wait(timeout=5)
except: proc.kill()
if reader_thread and reader_thread.is_alive():
reader_thread.join(timeout=1)
# 全局状态清理(只执行一次)
try:
recording.discard(record_name)
recording_time_list.pop(record_name, None)
if record_url in running_list:
running_list.remove(record_url)
global monitoring
monitoring = max(0, monitoring - 1)
clear_record_info(record_name, record_url)
except: pass
try:
ffmpeg_cmd = build_ffmpeg_command(real_url)
while not should_exit:
if retry_delay > INITIAL_RETRY_DELAY:
print(f"[WARN] 防风控等待 {retry_delay}s 后重新连接...")
time.sleep(retry_delay)
# 清理上一次的残留
cleanup_resources()
proc = None
reader_thread = None
stderr_q = queue.Queue()
print(f"[INFO][{datetime.datetime.now():%H:%M:%S}] 启动 YouTube 推流 → {stream_title}")
proc = subprocess.Popen(
ffmpeg_cmd,
stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE,
text=True,
bufsize=1,
universal_newlines=True
)
reader_thread = threading.Thread(
target=lambda p=proc, q=stderr_q: (
[q.put(l) for l in iter(p.stderr.readline, "")] or q.put(None)
)
)
reader_thread.daemon = True
reader_thread.start()
start_time = last_frame_time = last_log_time = time.time()
while True:
try:
line = stderr_q.get(timeout=STDERR_TIMEOUT)
except queue.Empty:
# 超时检查僵死
if time.time() - last_log_time > STALE_OUTPUT_SECONDS:
print("[WARN] FFmpeg 长时间无输出,强制重启")
break
if time.time() - start_time > START_CHECK_AFTER and time.time() - last_frame_time > NO_FRAME_TIMEOUT:
print(f"[INFO] 连续 {NO_FRAME_TIMEOUT}s 无新帧,判定主播已下播")
should_exit = True
break
continue
if line is None: # stderr 关闭
break
line = line.rstrip()
if line:
last_log_time = time.time()
print(f"[FFmpeg] {line}")
lower = line.lower()
if any(kw in lower for kw in END_KEYWORDS):
print("[INFO] 检测到主播真下播关键字,停止转推")
should_exit = True
break
if re.search(r'frame=\s*\d+', lower) or "fps=" in lower:
last_frame_time = time.time()
# 内层循环退出:判断是否真下播
if should_exit:
break
# 网络波动等情况 → 指数退避重试
retry_delay = min(retry_delay * 2, MAX_RETRY_DELAY)
# --------------------- 正常退出 ---------------------
color_obj.print_colored(f"[{record_name}] YouTube 推流已彻底停止并清理完毕\n", color_obj.GREEN)
except Exception as e:
print(f"[FATAL] YouTube 推流意外崩溃: {e}")
traceback.print_exc()
color_obj.print_colored(f"[{record_name}] YouTube 推流异常退出但已清理\n", color_obj.RED)
finally:
# 无论如何只清理一次
cleanup_resources()
return

19
backup/888 Normal file
View File

@@ -0,0 +1,19 @@
ffmpeg_command = [
"ffmpeg", "-loglevel", "info",
"-rw_timeout", "15000000",
"-reconnect", "1", "-reconnect_at_eof", "1", "-reconnect_streamed", "1",
"-reconnect_delay_max", "15",
"-fflags", "+genpts+discardcorrupt",
"-err_detect", "ignore_err",
"-thread_queue_size", "8192",
"-headers", douyin_headers,
"-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",
"-c:a", "aac", "-b:a", "128k", "-ar", "44100", "-ac", "2",
"-af", "aresample=async=1:first_pts=0",
"-flags", "+global_header",
"-f", "flv", youtube_rtmp
]

227
backup/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
backup/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}")

View File

@@ -1 +1,30 @@
https://live.douyin.com/956616126418,主播: 糕冷小橘
https://live.douyin.com/9638535297,主播: 异度世界
https://live.douyin.com/372551154040,主播: 向嘉_手打柠檬茶
https://live.douyin.com/203476627795,主播: 蜜桃姐姐(手打柠檬茶)
https://live.douyin.com/825627287092,主播: 杏绘茶档_(00后创业版_手打柠檬茶)
https://live.douyin.com/147650297461,主播: 娱乐摆摊记
https://live.douyin.com/806666144080,主播: 卢荟胶
https://live.douyin.com/589958309143,主播: 大萝卜
https://live.douyin.com/35932729226,主播: 内向小学生
https://live.douyin.com/456576000931,主播: 介休〖烩饕〗大块牛肉饭「摆摊」
https://live.douyin.com/782322393954,主播: 小葵(摆摊版)
https://live.douyin.com/208318182053,主播: 小碗(创业版)
https://live.douyin.com/646935372639,主播: 小雪摆摊记
https://live.douyin.com/956877163856,主播: 饭饱饱
https://live.douyin.com/881451071780,主播: YINING一柠手打柠檬茶
https://live.douyin.com/270513395244,主播: 小喻的摆摊日记
https://live.douyin.com/742474761291,主播: 阿诺椰子•
https://live.douyin.com/820845121901,主播: 地摊闺蜜_创意寿司_(马儿努力版)
https://live.douyin.com/980855021360,主播: 图爸摆摊卖枣糕(日照首家)教学员
https://live.douyin.com/46238285118,主播: 我叫小彤呀
https://live.douyin.com/497155982953,主播: 想吃糖葫芦
https://live.douyin.com/349973241990,主播: 元气满满饭团(摆摊不摆烂版)
https://live.douyin.com/814839174401,主播: 飞飞摆摊不摆烂
https://live.douyin.com/597618993066,主播: 婷婷的茶生活
https://live.douyin.com/16531926455,主播: 木黎黎
https://live.douyin.com/739670432305,主播: 干饭007
https://live.douyin.com/486789870173,主播: 凉皮西施—豆豆
https://live.douyin.com/15652099787,主播: 莹莹在摆摊
https://live.douyin.com/572442812068,主播: 小小
https://live.douyin.com/477961427964,主播: 小鱼
https://live.douyin.com/686507122231,主播: 上海少妇鲜果茶

View File

@@ -1,26 +1,26 @@
[录制设置]
language(zh_cn/en) = zh_cn
是否跳过代理检测(是/否) =
是否跳过代理检测(是/否) =
直播保存路径(不填则默认) =
保存文件夹是否以作者区分 =
保存文件夹是否以作者区分 =
保存文件夹是否以时间区分 =
保存文件夹是否以标题区分 =
保存文件名是否包含标题 =
是否去除名称中的表情符号 =
视频保存格式ts|mkv|flv|mp4|mp3音频|m4a音频 = mp4
视频保存格式ts|mkv|flv|mp4|mp3音频|m4a音频 = ts
原画|超清|高清|标清|流畅 = 原画
是否使用代理ip(是/否) =
是否使用代理ip(是/否) =
代理地址 =
同一时间访问网络的线程数 = 1
循环时间(秒) = 10
同一时间访问网络的线程数 = 3
循环时间(秒) = 300
排队读取网址时间(秒) = 0
是否显示循环秒数 =
是否显示直播源地址 =
是否显示直播源地址 =
分段录制是否开启 =
是否强制启用https录制 =
录制空间剩余阈值(gb) = 1.0
视频分段时间(秒) = 1800
录制完成后自动转为mp4格式 =
录制完成后自动转为mp4格式 =
mp4格式重新编码为h264 =
追加格式后删除原文件 =
生成时间字幕文件 =

View File

@@ -1 +0,0 @@
https://live.douyin.com/956616126418

View File

@@ -1,10 +0,0 @@
[youtube]
key = ue78-1c3e-mr9g-14mz-9r4z
secure = false ; true=rtmps, false=rtmp
bitrate = 5000 ; 视频码率(kbps)
audio_bitrate = 128 ; 音频码率(kbps)
fps = 30
gop = 60
resolution = ; 为空=不改分辨率;如 720x1280 或 1280x720
ingest = a ; 谷歌分区 a/b/c...,留空默认 a
copy_when_safe = true ; 源满足 h264+aac 且参数合规时尝试 -c copy

1427
main.py

File diff suppressed because it is too large Load Diff

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"

454
src/ab_sign.py Normal file
View File

@@ -0,0 +1,454 @@
# -*- encoding: utf-8 -*-
import math
import time
def rc4_encrypt(plaintext: str, key: str) -> str:
# 初始化状态数组
s = list(range(256))
# 使用密钥对状态数组进行置换
j = 0
for i in range(256):
j = (j + s[i] + ord(key[i % len(key)])) % 256
s[i], s[j] = s[j], s[i]
# 生成密钥流并加密
i = j = 0
result = []
for char in plaintext:
i = (i + 1) % 256
j = (j + s[i]) % 256
s[i], s[j] = s[j], s[i]
t = (s[i] + s[j]) % 256
result.append(chr(s[t] ^ ord(char)))
return ''.join(result)
def left_rotate(x: int, n: int) -> int:
n %= 32
return ((x << n) | (x >> (32 - n))) & 0xFFFFFFFF
def get_t_j(j: int) -> int:
if 0 <= j < 16:
return 2043430169 # 0x79CC4519
elif 16 <= j < 64:
return 2055708042 # 0x7A879D8A
else:
raise ValueError("invalid j for constant Tj")
def ff_j(j: int, x: int, y: int, z: int) -> int:
if 0 <= j < 16:
return (x ^ y ^ z) & 0xFFFFFFFF
elif 16 <= j < 64:
return ((x & y) | (x & z) | (y & z)) & 0xFFFFFFFF
else:
raise ValueError("invalid j for bool function FF")
def gg_j(j: int, x: int, y: int, z: int) -> int:
if 0 <= j < 16:
return (x ^ y ^ z) & 0xFFFFFFFF
elif 16 <= j < 64:
return ((x & y) | (~x & z)) & 0xFFFFFFFF
else:
raise ValueError("invalid j for bool function GG")
class SM3:
def __init__(self):
self.reg = []
self.chunk = []
self.size = 0
self.reset()
def reset(self):
# 初始化寄存器值 - 修正为与JS版本相同的值
self.reg = [
1937774191, 1226093241, 388252375, 3666478592,
2842636476, 372324522, 3817729613, 2969243214
]
self.chunk = []
self.size = 0
def write(self, data):
# 将输入转换为字节数组
if isinstance(data, str):
# 直接转换为UTF-8字节列表
a = list(data.encode('utf-8'))
else:
a = data
self.size += len(a)
f = 64 - len(self.chunk)
if len(a) < f:
# 如果数据长度小于剩余空间,直接添加
self.chunk.extend(a)
else:
# 否则分块处理
self.chunk.extend(a[:f])
while len(self.chunk) >= 64:
self._compress(self.chunk)
if f < len(a):
self.chunk = a[f:min(f + 64, len(a))]
else:
self.chunk = []
f += 64
def _fill(self):
# 计算比特长度
bit_length = 8 * self.size
# 添加填充位
padding_pos = len(self.chunk)
self.chunk.append(0x80)
padding_pos = (padding_pos + 1) % 64
# 如果剩余空间不足8字节则填充到下一个块
if 64 - padding_pos < 8:
padding_pos -= 64
# 填充0直到剩余8字节用于存储长度
while padding_pos < 56:
self.chunk.append(0)
padding_pos += 1
# 添加消息长度高32位
high_bits = bit_length // 4294967296
for i in range(4):
self.chunk.append((high_bits >> (8 * (3 - i))) & 0xFF)
# 添加消息长度低32位
for i in range(4):
self.chunk.append((bit_length >> (8 * (3 - i))) & 0xFF)
def _compress(self, data):
if len(data) < 64:
raise ValueError("compress error: not enough data")
else:
# 消息扩展
w = [0] * 132
# 将字节数组转换为字
for t in range(16):
w[t] = (data[4 * t] << 24) | (data[4 * t + 1] << 16) | (data[4 * t + 2] << 8) | data[4 * t + 3]
w[t] &= 0xFFFFFFFF
# 消息扩展
for j in range(16, 68):
a = w[j - 16] ^ w[j - 9] ^ left_rotate(w[j - 3], 15)
a = a ^ left_rotate(a, 15) ^ left_rotate(a, 23)
w[j] = (a ^ left_rotate(w[j - 13], 7) ^ w[j - 6]) & 0xFFFFFFFF
# 计算w'
for j in range(64):
w[j + 68] = (w[j] ^ w[j + 4]) & 0xFFFFFFFF
# 压缩
a, b, c, d, e, f, g, h = self.reg
for j in range(64):
ss1 = left_rotate((left_rotate(a, 12) + e + left_rotate(get_t_j(j), j)) & 0xFFFFFFFF, 7)
ss2 = ss1 ^ left_rotate(a, 12)
tt1 = (ff_j(j, a, b, c) + d + ss2 + w[j + 68]) & 0xFFFFFFFF
tt2 = (gg_j(j, e, f, g) + h + ss1 + w[j]) & 0xFFFFFFFF
d = c
c = left_rotate(b, 9)
b = a
a = tt1
h = g
g = left_rotate(f, 19)
f = e
e = (tt2 ^ left_rotate(tt2, 9) ^ left_rotate(tt2, 17)) & 0xFFFFFFFF
# 更新寄存器
self.reg[0] ^= a
self.reg[1] ^= b
self.reg[2] ^= c
self.reg[3] ^= d
self.reg[4] ^= e
self.reg[5] ^= f
self.reg[6] ^= g
self.reg[7] ^= h
def sum(self, data=None, output_format=None):
"""
计算哈希值
"""
# 如果提供了输入,则重置并写入
if data is not None:
self.reset()
self.write(data)
self._fill()
# 分块压缩
for f in range(0, len(self.chunk), 64):
self._compress(self.chunk[f:f + 64])
if output_format == 'hex':
# 十六进制输出
result = ''.join(f'{val:08x}' for val in self.reg)
else:
# 字节数组输出
result = []
for f in range(8):
c = self.reg[f]
result.append((c >> 24) & 0xFF)
result.append((c >> 16) & 0xFF)
result.append((c >> 8) & 0xFF)
result.append(c & 0xFF)
self.reset()
return result
def result_encrypt(long_str: str, num: str | None = None) -> str:
# 魔改base64编码表
encoding_tables = {
"s0": "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",
"s1": "Dkdpgh4ZKsQB80/Mfvw36XI1R25+WUAlEi7NLboqYTOPuzmFjJnryx9HVGcaStCe=",
"s2": "Dkdpgh4ZKsQB80/Mfvw36XI1R25-WUAlEi7NLboqYTOPuzmFjJnryx9HVGcaStCe=",
"s3": "ckdp1h4ZKsUB80/Mfvw36XIgR25+WQAlEi7NLboqYTOPuzmFjJnryx9HVGDaStCe",
"s4": "Dkdpgh2ZmsQB80/MfvV36XI1R45-WUAlEixNLwoqYTOPuzKFjJnry79HbGcaStCe"
}
# 位移常量
masks = [16515072, 258048, 4032, 63] # 对应 0, 1, 2 的掩码添加63作为第四个掩码
shifts = [18, 12, 6, 0] # 对应的位移量
encoding_table = encoding_tables[num]
result = ""
round_num = 0
long_int = get_long_int(round_num, long_str)
total_chars = math.ceil(len(long_str) / 3 * 4)
for i in range(total_chars):
# 每4个字符处理一组3字节
if i // 4 != round_num:
round_num += 1
long_int = get_long_int(round_num, long_str)
# 计算当前位置的索引
index = i % 4
# 使用掩码和位移提取6位值
char_index = (long_int & masks[index]) >> shifts[index]
result += encoding_table[char_index]
return result
def get_long_int(round_num: int, long_str: str) -> int:
round_num = round_num * 3
# 获取字符串中的字符如果超出范围则使用0
char1 = ord(long_str[round_num]) if round_num < len(long_str) else 0
char2 = ord(long_str[round_num + 1]) if round_num + 1 < len(long_str) else 0
char3 = ord(long_str[round_num + 2]) if round_num + 2 < len(long_str) else 0
return (char1 << 16) | (char2 << 8) | char3
def gener_random(random_num: int, option: list[int]) -> list[int]:
byte1 = random_num & 255
byte2 = (random_num >> 8) & 255
return [
(byte1 & 170) | (option[0] & 85), # 偶数位与option[0]的奇数位合并
(byte1 & 85) | (option[0] & 170), # 奇数位与option[0]的偶数位合并
(byte2 & 170) | (option[1] & 85), # 偶数位与option[1]的奇数位合并
(byte2 & 85) | (option[1] & 170), # 奇数位与option[1]的偶数位合并
]
def generate_random_str() -> str:
"""
生成随机字符串
Returns:
随机字符串
"""
# 使用与JS版本相同的固定随机值
random_values = [0.123456789, 0.987654321, 0.555555555]
# 生成三组随机字节并合并
random_bytes = []
random_bytes.extend(gener_random(int(random_values[0] * 10000), [3, 45]))
random_bytes.extend(gener_random(int(random_values[1] * 10000), [1, 0]))
random_bytes.extend(gener_random(int(random_values[2] * 10000), [1, 5]))
return ''.join(chr(b) for b in random_bytes)
def generate_rc4_bb_str(url_search_params: str, user_agent: str, window_env_str: str,
suffix: str = "cus", arguments: list[int] | None = None) -> str:
if arguments is None:
arguments = [0, 1, 14]
sm3 = SM3()
start_time = int(time.time() * 1000)
# 三次加密处理
# 1: url_search_params两次sm3之的结果
url_search_params_list = sm3.sum(sm3.sum(url_search_params + suffix))
# 2: 对后缀两次sm3之的结果
cus = sm3.sum(sm3.sum(suffix))
# 3: 对ua处理之后的结果
ua_key = chr(0) + chr(1) + chr(14) # [1/256, 1, 14]
ua = sm3.sum(result_encrypt(
rc4_encrypt(user_agent, ua_key),
"s3"
))
end_time = start_time + 100
# 构建配置对象
b = {
8: 3,
10: end_time,
15: {
"aid": 6383,
"pageId": 110624,
"boe": False,
"ddrt": 7,
"paths": {
"include": [{} for _ in range(7)],
"exclude": []
},
"track": {
"mode": 0,
"delay": 300,
"paths": []
},
"dump": True,
"rpU": "hwj"
},
16: start_time,
18: 44,
19: [1, 0, 1, 5],
}
def split_to_bytes(num: int) -> list[int]:
return [
(num >> 24) & 255,
(num >> 16) & 255,
(num >> 8) & 255,
num & 255
]
# 处理时间戳
start_time_bytes = split_to_bytes(b[16])
b[20] = start_time_bytes[0]
b[21] = start_time_bytes[1]
b[22] = start_time_bytes[2]
b[23] = start_time_bytes[3]
b[24] = int(b[16] / 256 / 256 / 256 / 256) & 255
b[25] = int(b[16] / 256 / 256 / 256 / 256 / 256) & 255
# 处理Arguments参数
arg0_bytes = split_to_bytes(arguments[0])
b[26] = arg0_bytes[0]
b[27] = arg0_bytes[1]
b[28] = arg0_bytes[2]
b[29] = arg0_bytes[3]
b[30] = int(arguments[1] / 256) & 255
b[31] = (arguments[1] % 256) & 255
arg1_bytes = split_to_bytes(arguments[1])
b[32] = arg1_bytes[0]
b[33] = arg1_bytes[1]
arg2_bytes = split_to_bytes(arguments[2])
b[34] = arg2_bytes[0]
b[35] = arg2_bytes[1]
b[36] = arg2_bytes[2]
b[37] = arg2_bytes[3]
# 处理加密结果
b[38] = url_search_params_list[21]
b[39] = url_search_params_list[22]
b[40] = cus[21]
b[41] = cus[22]
b[42] = ua[23]
b[43] = ua[24]
# 处理结束时间
end_time_bytes = split_to_bytes(b[10])
b[44] = end_time_bytes[0]
b[45] = end_time_bytes[1]
b[46] = end_time_bytes[2]
b[47] = end_time_bytes[3]
b[48] = b[8]
b[49] = int(b[10] / 256 / 256 / 256 / 256) & 255
b[50] = int(b[10] / 256 / 256 / 256 / 256 / 256) & 255
# 处理配置项
b[51] = b[15]['pageId']
page_id_bytes = split_to_bytes(b[15]['pageId'])
b[52] = page_id_bytes[0]
b[53] = page_id_bytes[1]
b[54] = page_id_bytes[2]
b[55] = page_id_bytes[3]
b[56] = b[15]['aid']
b[57] = b[15]['aid'] & 255
b[58] = (b[15]['aid'] >> 8) & 255
b[59] = (b[15]['aid'] >> 16) & 255
b[60] = (b[15]['aid'] >> 24) & 255
# 处理环境信息
window_env_list = [ord(char) for char in window_env_str]
b[64] = len(window_env_list)
b[65] = b[64] & 255
b[66] = (b[64] >> 8) & 255
b[69] = 0
b[70] = 0
b[71] = 0
# 计算校验和
b[72] = b[18] ^ b[20] ^ b[26] ^ b[30] ^ b[38] ^ b[40] ^ b[42] ^ b[21] ^ b[27] ^ b[31] ^ \
b[35] ^ b[39] ^ b[41] ^ b[43] ^ b[22] ^ b[28] ^ b[32] ^ b[36] ^ b[23] ^ b[29] ^ \
b[33] ^ b[37] ^ b[44] ^ b[45] ^ b[46] ^ b[47] ^ b[48] ^ b[49] ^ b[50] ^ b[24] ^ \
b[25] ^ b[52] ^ b[53] ^ b[54] ^ b[55] ^ b[57] ^ b[58] ^ b[59] ^ b[60] ^ b[65] ^ \
b[66] ^ b[70] ^ b[71]
# 构建最终字节数组
bb = [
b[18], b[20], b[52], b[26], b[30], b[34], b[58], b[38], b[40], b[53], b[42], b[21],
b[27], b[54], b[55], b[31], b[35], b[57], b[39], b[41], b[43], b[22], b[28], b[32],
b[60], b[36], b[23], b[29], b[33], b[37], b[44], b[45], b[59], b[46], b[47], b[48],
b[49], b[50], b[24], b[25], b[65], b[66], b[70], b[71]
]
bb.extend(window_env_list)
bb.append(b[72])
return rc4_encrypt(
''.join(chr(byte) for byte in bb),
chr(121)
)
def ab_sign(url_search_params: str, user_agent: str) -> str:
window_env_str = "1920|1080|1920|1040|0|30|0|0|1872|92|1920|1040|1857|92|1|24|Win32"
# 1. 生成随机字符串前缀
# 2. 生成RC4加密的主体部分
# 3. 对结果进行最终加密并添加等号后缀
return result_encrypt(
generate_random_str() +
generate_rc4_bb_str(url_search_params, user_agent, window_env_str),
"s4"
) + "="

View File

@@ -4,7 +4,7 @@
Author: Hmily
GitHub: https://github.com/ihmily
Date: 2023-07-15 23:15:00
Update: 2025-07-19 17:43:00
Update: 2025-10-23 18:28:00
Copyright (c) 2023-2025 by Hmily, All Rights Reserved.
Function: Get live stream data.
"""
@@ -29,6 +29,7 @@ from .utils import trace_error_decorator, generate_random_string
from .logger import script_path
from .room import get_sec_user_id, get_unique_id, UnsupportedUrlError
from .http_clients.async_http import async_req
from .ab_sign import ab_sign
ssl_context = ssl.create_default_context()
@@ -64,10 +65,87 @@ async def get_play_url_list(m3u8: str, proxy: OptionalStr = None, header: Option
return play_url_list
async def get_douyin_web_stream_data(url: str, proxy_addr: OptionalStr = None, cookies: OptionalStr = None):
headers = {
'cookie': 'ttwid=1%7C2iDIYVmjzMcpZ20fcaFde0VghXAA3NaNXE_SLR68IyE%7C1761045455'
'%7Cab35197d5cfb21df6cbb2fa7ef1c9262206b062c315b9d04da746d0b37dfbc7d',
'referer': 'https://live.douyin.com/335354047186',
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) '
'Chrome/116.0.5845.97 Safari/537.36 Core/1.116.567.400 QQBrowser/19.7.6764.400',
}
if cookies:
headers['cookie'] = cookies
try:
web_rid = url.split('?')[0].split('live.douyin.com/')[-1]
params = {
"aid": "6383",
"app_name": "douyin_web",
"live_id": "1",
"device_platform": "web",
"language": "zh-CN",
"browser_language": "zh-CN",
"browser_platform": "Win32",
"browser_name": "Chrome",
"browser_version": "116.0.0.0",
"web_rid": web_rid,
'msToken': '',
}
api = f'https://live.douyin.com/webcast/room/web/enter/?{urllib.parse.urlencode(params)}'
a_bogus = ab_sign(urllib.parse.urlparse(api).query, headers['user-agent'])
api += "&a_bogus=" + a_bogus
try:
json_str = await async_req(url=api, proxy_addr=proxy_addr, headers=headers)
if not json_str:
raise Exception("it triggered risk control")
json_data = json.loads(json_str)['data']
if not json_data['data']:
raise Exception(f"{url} VR live is not supported")
room_data = json_data['data'][0]
room_data['anchor_name'] = json_data['user']['nickname']
except Exception as e:
raise Exception(f"Douyin web data fetch error, because {e}.")
if room_data['status'] == 2:
if 'stream_url' not in room_data:
raise RuntimeError(
"The live streaming type or gameplay is not supported on the computer side yet, please use the "
"app to share the link for recording."
)
live_core_sdk_data = room_data['stream_url']['live_core_sdk_data']
pull_datas = room_data['stream_url']['pull_datas']
if live_core_sdk_data:
if pull_datas:
key = list(pull_datas.keys())[0]
json_str = pull_datas[key]['stream_data']
else:
json_str = live_core_sdk_data['pull_data']['stream_data']
json_data = json.loads(json_str)
if 'origin' in json_data['data']:
stream_data = live_core_sdk_data['pull_data']['stream_data']
origin_data = json.loads(stream_data)['data']['origin']['main']
sdk_params = json.loads(origin_data['sdk_params'])
origin_hls_codec = sdk_params.get('VCodec') or ''
origin_url_list = json_data['data']['origin']['main']
origin_m3u8 = {'ORIGIN': origin_url_list["hls"] + '&codec=' + origin_hls_codec}
origin_flv = {'ORIGIN': origin_url_list["flv"] + '&codec=' + origin_hls_codec}
hls_pull_url_map = room_data['stream_url']['hls_pull_url_map']
flv_pull_url = room_data['stream_url']['flv_pull_url']
room_data['stream_url']['hls_pull_url_map'] = {**origin_m3u8, **hls_pull_url_map}
room_data['stream_url']['flv_pull_url'] = {**origin_flv, **flv_pull_url}
except Exception as e:
print(f"Error message: {e} Error line: {e.__traceback__.tb_lineno}")
room_data = {'anchor_name': ""}
return room_data
@trace_error_decorator
async def get_douyin_app_stream_data(url: str, proxy_addr: OptionalStr = None, cookies: OptionalStr = None) -> dict:
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) '
'Chrome/141.0.0.0 Safari/537.36 Edg/141.0.0.0',
'Accept-Language': 'zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2',
'Referer': 'https://live.douyin.com/',
'Cookie': 'ttwid=1%7CB1qls3GdnZhUov9o2NxOMxxYS2ff6OSvEWbv0ytbES4%7C1680522049%7C280d802d6d478e3e78d0c807f7c487e7ffec0ae4e5fdd6a0fe74c3c6af149511; my_rd=1; passport_csrf_token=3ab34460fa656183fccfb904b16ff742; passport_csrf_token_default=3ab34460fa656183fccfb904b16ff742; d_ticket=9f562383ac0547d0b561904513229d76c9c21; n_mh=hvnJEQ4Q5eiH74-84kTFUyv4VK8xtSrpRZG1AhCeFNI; store-region=cn-fj; store-region-src=uid; LOGIN_STATUS=1; __security_server_data_status=1; FORCE_LOGIN=%7B%22videoConsumedRemainSeconds%22%3A180%7D; pwa2=%223%7C0%7C3%7C0%22; download_guide=%223%2F20230729%2F0%22; volume_info=%7B%22isUserMute%22%3Afalse%2C%22isMute%22%3Afalse%2C%22volume%22%3A0.6%7D; strategyABtestKey=%221690824679.923%22; stream_recommend_feed_params=%22%7B%5C%22cookie_enabled%5C%22%3Atrue%2C%5C%22screen_width%5C%22%3A1536%2C%5C%22screen_height%5C%22%3A864%2C%5C%22browser_online%5C%22%3Atrue%2C%5C%22cpu_core_num%5C%22%3A8%2C%5C%22device_memory%5C%22%3A8%2C%5C%22downlink%5C%22%3A10%2C%5C%22effective_type%5C%22%3A%5C%224g%5C%22%2C%5C%22round_trip_time%5C%22%3A150%7D%22; VIDEO_FILTER_MEMO_SELECT=%7B%22expireTime%22%3A1691443863751%2C%22type%22%3Anull%7D; home_can_add_dy_2_desktop=%221%22; __live_version__=%221.1.1.2169%22; device_web_cpu_core=8; device_web_memory_size=8; xgplayer_user_id=346045893336; csrf_session_id=2e00356b5cd8544d17a0e66484946f28; odin_tt=724eb4dd23bc6ffaed9a1571ac4c757ef597768a70c75fef695b95845b7ffcd8b1524278c2ac31c2587996d058e03414595f0a4e856c53bd0d5e5f56dc6d82e24004dc77773e6b83ced6f80f1bb70627; __ac_nonce=064caded4009deafd8b89; __ac_signature=_02B4Z6wo00f01HLUuwwAAIDBh6tRkVLvBQBy9L-AAHiHf7; ttcid=2e9619ebbb8449eaa3d5a42d8ce88ec835; webcast_leading_last_show_time=1691016922379; webcast_leading_total_show_times=1; webcast_local_quality=sd; live_can_add_dy_2_desktop=%221%22; msToken=1JDHnVPw_9yTvzIrwb7cQj8dCMNOoesXbA_IooV8cezcOdpe4pzusZE7NB7tZn9TBXPr0ylxmv-KMs5rqbNUBHP4P7VBFUu0ZAht_BEylqrLpzgt3y5ne_38hXDOX8o=; msToken=jV_yeN1IQKUd9PlNtpL7k5vthGKcHo0dEh_QPUQhr8G3cuYv-Jbb4NnIxGDmhVOkZOCSihNpA2kvYtHiTW25XNNX_yrsv5FN8O6zm3qmCIXcEe0LywLn7oBO2gITEeg=; tt_scid=mYfqpfbDjqXrIGJuQ7q-DlQJfUSG51qG.KUdzztuGP83OjuVLXnQHjsz-BRHRJu4e986'
@@ -77,7 +155,7 @@ async def get_douyin_app_stream_data(url: str, proxy_addr: OptionalStr = None, c
async def get_app_data(room_id: str, sec_uid: str) -> dict:
app_params = {
"verifyFp": "verify_lxj5zv70_7szNlAB7_pxNY_48Vh_ALKF_GA1Uf3yteoOY",
"verifyFp": "verify_hwj52020_7szNlAB7_pxNY_48Vh_ALKF_GA1Uf3yteoOY",
"type_id": "0",
"live_id": "1",
"room_id": room_id,
@@ -86,36 +164,25 @@ async def get_douyin_app_stream_data(url: str, proxy_addr: OptionalStr = None, c
"app_id": "1128"
}
api2 = f'https://webcast.amemv.com/webcast/room/reflow/info/?{urllib.parse.urlencode(app_params)}'
json_str2 = await async_req(url=api2, proxy_addr=proxy_addr, headers=headers)
json_data2 = json.loads(json_str2)['data']
room_data2 = json_data2['room']
room_data2['anchor_name'] = room_data2['owner']['nickname']
return room_data2
a_bogus = ab_sign(urllib.parse.urlparse(api2).query, headers['User-Agent'])
api2 += "&a_bogus=" + a_bogus
try:
json_str2 = await async_req(url=api2, proxy_addr=proxy_addr, headers=headers)
if not json_str2:
raise Exception("it triggered risk control")
json_data2 = json.loads(json_str2)['data']
if not json_data2.get('room'):
raise Exception(f"{url} VR live is not supported")
room_data2 = json_data2['room']
room_data2['anchor_name'] = room_data2['owner']['nickname']
return room_data2
except Exception as e:
raise Exception(f"Douyin app data fetch error, because {e}.")
try:
web_rid = url.split('?')[0].split('live.douyin.com/')
if len(web_rid) > 1:
web_rid = web_rid[1]
params = {
"aid": "6383",
"app_name": "douyin_web",
"live_id": "1",
"device_platform": "web",
"language": "zh-CN",
"browser_language": "zh-CN",
"browser_platform": "Win32",
"browser_name": "Chrome",
"browser_version": "116.0.0.0",
"web_rid": web_rid,
'msToken': '',
'a_bogus': ''
}
api = f'https://live.douyin.com/webcast/room/web/enter/?{urllib.parse.urlencode(params)}'
json_str = await async_req(url=api, proxy_addr=proxy_addr, headers=headers)
json_data = json.loads(json_str)['data']
room_data = json_data['data'][0]
room_data['anchor_name'] = json_data['user']['nickname']
return await get_douyin_web_stream_data(url, proxy_addr, cookies)
else:
try:
data = await get_sec_user_id(url, proxy_addr=proxy_addr)
@@ -218,18 +285,20 @@ async def get_douyin_stream_data(url: str, proxy_addr: OptionalStr = None, cooki
@trace_error_decorator
async def get_tiktok_stream_data(url: str, proxy_addr: OptionalStr = None, cookies: OptionalStr = None) -> dict | None:
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0',
'Cookie': 'ttwid=1%7CM-rF193sJugKuNz2RGNt-rh6pAAR9IMceUSzlDnPCNI%7C1683274418%7Cf726d4947f2fc37fecc7aeb0cdaee52892244d04efde6f8a8edd2bb168263269; tiktok_webapp_theme=light; tt_chain_token=VWkygAWDlm1cFg/k8whmOg==; passport_csrf_token=6e422c5a7991f8cec7033a8082921510; passport_csrf_token_default=6e422c5a7991f8cec7033a8082921510; d_ticket=f8c267d4af4523c97be1ccb355e9991e2ae06; odin_tt=320b5f386cdc23f347be018e588873db7f7aea4ea5d1813681c3fbc018ea025dde957b94f74146dbc0e3612426b865ccb95ec8abe4ee36cca65f15dbffec0deff7b0e69e8ea536d46e0f82a4fc37d211; cmpl_token=AgQQAPNSF-RO0rT04baWtZ0T_jUjl4fVP4PZYM2QPw; uid_tt=319b558dbba684bb1557206c92089cd113a875526a89aee30595925d804b81c7; uid_tt_ss=319b558dbba684bb1557206c92089cd113a875526a89aee30595925d804b81c7; sid_tt=ad5e736f4bedb2f6d42ccd849e706b1d; sessionid=ad5e736f4bedb2f6d42ccd849e706b1d; sessionid_ss=ad5e736f4bedb2f6d42ccd849e706b1d; store-idc=useast5; store-country-code=us; store-country-code-src=uid; tt-target-idc=useast5; tt-target-idc-sign=qXNk0bb1pDQ0FbCNF120Pl9WWMLZg9Edv5PkfyCbS4lIk5ieW5tfLP7XWROnN0mEaSlc5hg6Oji1pF-yz_3ZXnUiNMrA9wNMPvI6D9IFKKVmq555aQzwPIGHv0aQC5dNRgKo5Z5LBkgxUMWEojTKclq2_L8lBciw0IGdhFm_XyVJtbqbBKKgybGDLzK8ZyxF4Jl_cYRXaDlshZjc38JdS6wruDueRSHe7YvNbjxCnApEFUv-OwJANSPU_4rvcqpVhq3JI2VCCfw-cs_4MFIPCDOKisk5EhAo2JlHh3VF7_CLuv80FXg_7ZqQ2pJeMOog294rqxwbbQhl3ATvjQV_JsWyUsMd9zwqecpylrPvtySI2u1qfoggx1owLrrUynee1R48QlanLQnTNW_z1WpmZBgVJqgEGLwFoVOmRzJuFFNj8vIqdjM2nDSdWqX8_wX3wplohkzkPSFPfZgjzGnQX28krhgTytLt7BXYty5dpfGtsdb11WOFHM6MZ9R9uLVB; sid_guard=ad5e736f4bedb2f6d42ccd849e706b1d%7C1690990657%7C15525213%7CMon%2C+29-Jan-2024+08%3A11%3A10+GMT; sid_ucp_v1=1.0.0-KGM3YzgwYjZhODgyYWI1NjIwNTA0NjBmOWUxMGRhMjIzYTI2YjMxNDUKGAiqiJ30keKD5WQQwfCppgYYsws4AkDsBxAEGgd1c2Vhc3Q1IiBhZDVlNzM2ZjRiZWRiMmY2ZDQyY2NkODQ5ZTcwNmIxZA; ssid_ucp_v1=1.0.0-KGM3YzgwYjZhODgyYWI1NjIwNTA0NjBmOWUxMGRhMjIzYTI2YjMxNDUKGAiqiJ30keKD5WQQwfCppgYYsws4AkDsBxAEGgd1c2Vhc3Q1IiBhZDVlNzM2ZjRiZWRiMmY2ZDQyY2NkODQ5ZTcwNmIxZA; tt_csrf_token=dD0EIH8q-pe3qDQsCyyD1jLN6KizJDRjOEyk; __tea_cache_tokens_1988={%22_type_%22:%22default%22%2C%22user_unique_id%22:%227229608516049831425%22%2C%22timestamp%22:1683274422659}; ttwid=1%7CM-rF193sJugKuNz2RGNt-rh6pAAR9IMceUSzlDnPCNI%7C1694002151%7Cd89b77afc809b1a610661a9d1c2784d80ebef9efdd166f06de0d28e27f7e4efe; msToken=KfJAVZ7r9D_QVeQlYAUZzDFbc1Yx-nZz6GF33eOxgd8KlqvTg1lF9bMXW7gFV-qW4MCgUwnBIhbiwU9kdaSpgHJCk-PABsHCtTO5J3qC4oCTsrXQ1_E0XtbqiE4OVLZ_jdF1EYWgKNPT2SnwGkQ=; msToken=KfJAVZ7r9D_QVeQlYAUZzDFbc1Yx-nZz6GF33eOxgd8KlqvTg1lF9bMXW7gFV-qW4MCgUwnBIhbiwU9kdaSpgHJCk-PABsHCtTO5J3qC4oCTsrXQ1_E0XtbqiE4OVLZ_jdF1EYWgKNPT2SnwGkQ='
'referer': 'https://www.tiktok.com/',
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) '
'Chrome/141.0.0.0 Safari/537.36',
'cookie': cookies or '1%7Cz7FKki38aKyy7i-BC9rEDwcrVvjcLcFEL6QIeqldoy4%7C1761302831%7C6c1461e9f1f980cbe0404c5190'
'5177d5d53bbd822e1bf66128887d942c9c3e2f'
}
if cookies:
headers['Cookie'] = cookies
for i in range(3):
html_str = await async_req(url=url, proxy_addr=proxy_addr, headers=headers, abroad=True, http2=False)
time.sleep(1)
if "We regret to inform you that we have discontinued operating TikTok" in html_str:
msg = re.search('<p>\n\\s+(We regret to inform you that we have discontinu.*?)\\.\n\\s+</p>', html_str)
raise ConnectionError(
f"Your proxy node's regional network is blocked from accessing TikTok; please switch to a node in "
"Your proxy node's regional network is blocked from accessing TikTok; please switch to a node in "
f"another region to access. {msg.group(1) if msg else ''}"
)
if 'UNEXPECTED_EOF_WHILE_READING' not in html_str:
@@ -935,6 +1004,76 @@ async def get_sooplive_tk(url: str, rtype: str, proxy_addr: OptionalStr = None,
return f"{bj_name}-{bj_id}", json_data['CHANNEL']['BNO']
def get_soop_headers(cookies):
headers = {
'client-id': str(uuid.uuid4()),
'user-agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, '
'like Gecko) Version/18.5 Mobile/15E148 Safari/604.1 Edg/141.0.0.0',
}
if cookies:
headers['cookie'] = cookies
return headers
async def _get_soop_channel_info_global(bj_id, proxy_addr: OptionalStr = None, cookies: OptionalStr = None) -> str:
headers = get_soop_headers(cookies)
api = 'https://api.sooplive.com/v2/channel/info/' + str(bj_id)
json_str = await async_req(api, proxy_addr=proxy_addr, headers=headers)
json_data = json.loads(json_str)
nickname = json_data['data']['streamerChannelInfo']['nickname']
channelId = json_data['data']['streamerChannelInfo']['channelId']
anchor_name = f"{nickname}-{channelId}"
return anchor_name
async def _get_soop_stream_info_global(bj_id, proxy_addr: OptionalStr = None, cookies: OptionalStr = None) -> tuple:
headers = get_soop_headers(cookies)
api = 'https://api.sooplive.com/v2/stream/info/' + str(bj_id)
json_str = await async_req(api, proxy_addr=proxy_addr, headers=headers)
json_data = json.loads(json_str)
status = json_data['data']['isStream']
title = json_data['data']['title']
return status, title
async def _fetch_web_stream_data_global(url: str, proxy_addr: OptionalStr = None, cookies: OptionalStr = None) -> dict:
split_url = url.split('/')
bj_id = split_url[3] if len(split_url) < 6 else split_url[5]
anchor_name = await _get_soop_channel_info_global(bj_id)
result = {"anchor_name": anchor_name or '', "is_live": False, "live_url": url}
status, title = await _get_soop_stream_info_global(bj_id)
if not status:
return result
else:
async def _get_url_list(m3u8: str) -> list[str]:
headers = {
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) '
'Chrome/141.0.0.0 Safari/537.36 Edg/141.0.0.0',
}
if cookies:
headers['cookie'] = cookies
resp = await async_req(url=m3u8, proxy_addr=proxy_addr, headers=headers)
play_url_list = []
url_prefix = '/'.join(m3u8.split('/')[0:3])
for i in resp.split('\n'):
if not i.startswith('#') and i.strip():
play_url_list.append(url_prefix + i.strip())
bandwidth_pattern = re.compile(r'BANDWIDTH=(\d+)')
bandwidth_list = bandwidth_pattern.findall(resp)
url_to_bandwidth = {purl: int(bandwidth) for bandwidth, purl in zip(bandwidth_list, play_url_list)}
play_url_list = sorted(play_url_list, key=lambda purl: url_to_bandwidth[purl], reverse=True)
return play_url_list
m3u8_url = 'https://global-media.sooplive.com/live/' + str(bj_id) + '/master.m3u8'
result |= {
'is_live': True,
'title': title,
'm3u8_url': m3u8_url,
'play_url_list': await _get_url_list(m3u8_url)
}
return result
@trace_error_decorator
async def get_sooplive_stream_data(
url: str, proxy_addr: OptionalStr = None, cookies: OptionalStr = None,
@@ -949,6 +1088,9 @@ async def get_sooplive_stream_data(
if cookies:
headers['Cookie'] = cookies
if "sooplive.com" in url:
return await _fetch_web_stream_data_global(url, proxy_addr, cookies)
split_url = url.split('/')
bj_id = split_url[3] if len(split_url) < 6 else split_url[5]

36
youtubeKey Normal file
View File

@@ -0,0 +1,36 @@
ue78-1c3e-mr9g-14mz-9r4z # 没有ypp的美食
qxvb-r47b-r5ju-6ud3-6k7z # vlog频道
x04z-564w-aks7-embw-30y4 #美食ypp
; https://live.douyin.com/745606325880,主播: w、(摆摊日记_)
; https://live.douyin.com/675343045974,主播: 张子沐的串串(你的电子榨菜)
; https://live.douyin.com/673565298571,主播: 轻舟已撞大冰山(摆摊vlog)
; https://live.douyin.com/835571459859,主播: 海绵饱饱
; https://live.douyin.com/152358755212,主播: 大美_红烧肉
; https://live.douyin.com/990825651731,主播: 王同学摆摊日记(蜜汁烤鸡腿)
; https://live.douyin.com/591442402624,主播: 76696515302
; https://live.douyin.com/967381081829,主播: 憨憨的摆摊日记
; https://live.douyin.com/78012762575,主播: 摆摊儿去划水
; https://live.douyin.com/163813589919,主播: 不要熬夜(摆摊中)
; https://live.douyin.com/498368994814,主播: 剪头发的小帆帆
; https://live.douyin.com/617848099204,主播: 果妈要努力
; https://live.douyin.com/821525628944,主播: 李熙雨⭐
; https://live.douyin.com/993102287144,主播: 小茗早餐(薪笼记助创官)
; https://live.douyin.com/701547125568,主播: 悠悠包
; https://live.douyin.com/816130992220,主播: 甜昕冰冰的摆摊日记
; https://live.douyin.com/876468215361,主播: 晓晓和凡凡
; https://live.douyin.com/511335278313,主播: 秘书(摆摊休息版)
; https://live.douyin.com/483160615952,主播: 小胡同学你好(饭团版)
; https://live.douyin.com/525514386431,主播: 阿闯烤肉夹馍
; https://live.douyin.com/8687122573,主播: 小彤炒粉
; https://live.douyin.com/642534242822,主播: (小王_)煎饼果子
; https://live.douyin.com/743565594721,主播: 汪汪汪小妞
; https://live.douyin.com/249578288248,主播: 思思努力版
; https://live.douyin.com/472140253414,主播: 戴盘龙
; https://live.douyin.com/700846653732,主播: 三哥大锅菜_炒鸡(国基路店)官方号
; https://live.douyin.com/81849868631,主播: 纯儿²¹(休息勿跑空哦)
; https://live.douyin.com/690114366322,主播: 卤味鲜(东大)
; https://live.douyin.com/469980190666,主播: 茜茜柠檬茶(小米茶档_)
; https://live.douyin.com/388066418744,主播: 加速姐的摆摊日记