Compare commits
74 Commits
main
...
srs-ffpaly
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2375331973 | ||
|
|
041a402a73 | ||
|
|
a7b7761e78 | ||
|
|
7ac4c7ff90 | ||
|
|
d4158e7f1a | ||
|
|
8953e2f4d4 | ||
|
|
88d54c1679 | ||
|
|
c630787719 | ||
|
|
67bacc1c42 | ||
|
|
53f2c77421 | ||
|
|
9107995c02 | ||
|
|
3ff4facd3d | ||
|
|
34bb2b661e | ||
|
|
6aeb1462dd | ||
|
|
0c7437f9cc | ||
|
|
9b23d9872b | ||
|
|
303ece39c0 | ||
|
|
7c21e80eb6 | ||
|
|
b4e76d74ca | ||
|
|
bfc64b873a | ||
|
|
bc2651c801 | ||
|
|
7c47c73d71 | ||
|
|
c6e2ea2e77 | ||
|
|
4e1b408e27 | ||
|
|
4a27659c99 | ||
|
|
ab8511de5a | ||
|
|
90c3f193aa | ||
|
|
f4a0c06ff6 | ||
|
|
2cb81ebb03 | ||
|
|
26db3d41ce | ||
|
|
ddadef2287 | ||
|
|
c717efe8c4 | ||
|
|
90bb33c9af | ||
|
|
1ef411a2d0 | ||
|
|
27ad068712 | ||
|
|
dd6d9f7377 | ||
|
|
b00663aa6c | ||
|
|
8d3f950303 | ||
|
|
6d505c1452 | ||
|
|
c059f62d38 | ||
|
|
eaa3d67d16 | ||
|
|
b2af356c22 | ||
|
|
31e7c67425 | ||
|
|
1d1fff7979 | ||
|
|
6430ce1f61 | ||
|
|
f10549ffae | ||
|
|
667dc47188 | ||
|
|
511576aa97 | ||
|
|
9609e4caa1 | ||
|
|
63bf62df84 | ||
|
|
d12a35d249 | ||
|
|
3b7785ec30 | ||
|
|
350eb6d798 | ||
|
|
3accbd7941 | ||
|
|
997b147589 | ||
|
|
9e8ef1d28d | ||
|
|
962b17ea67 | ||
|
|
6d7f3b874f | ||
|
|
db04131a9c | ||
|
|
5d0134b86e | ||
|
|
019f68a91a | ||
|
|
3c92b691db | ||
|
|
717b831e12 | ||
|
|
4c6b040887 | ||
|
|
617fdcb158 | ||
|
|
0067152f69 | ||
|
|
34990a29de | ||
|
|
d507ccff4c | ||
|
|
f9bf0b8d6b | ||
|
|
d808eb1bdd | ||
|
|
1e9a0759e1 | ||
|
|
da57df6a93 | ||
|
|
32df67b284 | ||
|
|
7a0207f8ee |
91
.github/ISSUE_TEMPLATE/bug.yml
vendored
Normal file
91
.github/ISSUE_TEMPLATE/bug.yml
vendored
Normal 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
91
.github/ISSUE_TEMPLATE/bug_en.yml
vendored
Normal 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
38
.github/ISSUE_TEMPLATE/feature.yml
vendored
Normal 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
37
.github/ISSUE_TEMPLATE/feature_en.yml
vendored
Normal 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
63
.github/ISSUE_TEMPLATE/question.yml
vendored
Normal 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
63
.github/ISSUE_TEMPLATE/question_en.yml
vendored
Normal 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
56
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal 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
54
.github/workflows/build-image.yml
vendored
Normal 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
15
.github/workflows/issue-translator.yml
vendored
Normal 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
65
.github/workflows/sync.yml
vendored
Normal 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
2
.gitignore
vendored
@@ -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
115
README.md
@@ -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
|
||||
 
|
||||
|
||||
## 🎃源码运行
|
||||
使用源码运行,前提要有**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
|
||||
|
||||
 
|
||||
|
||||
## 🤖相关项目
|
||||
|
||||
- StreamCap: https://github.com/ihmily/StreamCap
|
||||
- streamget: https://github.com/ihmily/streamget
|
||||
|
||||
 
|
||||
|
||||
## ❤️贡献者
|
||||
|
||||
   [](https://github.com/ihmily)
|
||||
@@ -407,10 +507,21 @@ docker-compose stop
|
||||
|
||||
   [](https://github.com/HoratioShaw)
|
||||
[](https://github.com/nov30th)
|
||||
[](https://github.com/727155455)
|
||||
[](https://github.com/nixingshiguang)
|
||||
[](https://github.com/1411430556)
|
||||
[](https://github.com/Ovear)
|
||||
 
|
||||
|
||||
## ⏳提交日志
|
||||
|
||||
- 20251024
|
||||
- 修复抖音风控无法获取数据问题
|
||||
|
||||
- 新增soop.com录制支持
|
||||
|
||||
- 修复bigo录制
|
||||
|
||||
- 20250127
|
||||
- 新增淘宝、京东、faceit直播录制
|
||||
- 修复小红书直播流录制以及转码问题
|
||||
|
||||
1815
back/back1
1815
back/back1
File diff suppressed because it is too large
Load Diff
104
backup/111
Normal file
104
backup/111
Normal 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
147
backup/333333
Normal 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
146
backup/44444
Normal 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
239
backup/5555
Normal 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
257
backup/55555
Normal 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
197
backup/66666
Normal 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
203
backup/7777
Normal 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
19
backup/888
Normal 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
227
backup/robust_relay.py
Normal file
@@ -0,0 +1,227 @@
|
||||
# robust_relay.py
|
||||
# 高稳定性 YouTube 转播模块(防误判下播 + 看门狗 + 自动重连 + token刷新)
|
||||
# 作者:融合 A+B 代码精华,专为抖音/抖音国际版长期无人值守转播设计
|
||||
|
||||
import subprocess
|
||||
import time
|
||||
import threading
|
||||
import os
|
||||
import signal
|
||||
import datetime
|
||||
from typing import List, Optional, Callable
|
||||
|
||||
# ==================== 配置常量(可全局覆盖) ====================
|
||||
WATCHDOG_SILENCE = 600 # 10分钟无输出 → 软重启
|
||||
WATCHDOG_GRACE = 120 # 启动宽限期
|
||||
REFRESH_URL_EVERY = 300 # 每5分钟强制刷新 real_url(防token过期)
|
||||
OFFLINE_DEBOUNCE = 3 # 连续3次API返回下播才认为真下播
|
||||
OFFLINE_WINDOW = 120 # 最近120秒有活跃就忽略下播信号
|
||||
SOFT_RESTART_COOLDOWN = 90 # 软重启最小间隔
|
||||
|
||||
# ==================== 轻量探活 ====================
|
||||
def _probe_alive(url: str, timeout: float = 5.0) -> bool:
|
||||
if not url:
|
||||
return False
|
||||
try:
|
||||
import urllib.request, contextlib
|
||||
opener = urllib.request.build_opener()
|
||||
if hasattr(urllib.request, 'getproxies'):
|
||||
proxy = urllib.request.getproxies().get('http')
|
||||
if proxy:
|
||||
opener.add_handler(urllib.request.ProxyHandler({"http": proxy, "https": proxy}))
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"}, method="HEAD")
|
||||
with contextlib.closing(opener.open(req, timeout=timeout)) as r:
|
||||
if r.code >= 400:
|
||||
return False
|
||||
head = r.read(256)
|
||||
if url.lower().endswith(".m3u8"):
|
||||
return b"#EXTM3U" in head.upper()
|
||||
if url.lower().endswith(".flv"):
|
||||
return head.startswith(b"FLV")
|
||||
return len(head) > 0
|
||||
except:
|
||||
return False
|
||||
|
||||
# ==================== 增强版 ffmpeg 启动器 ====================
|
||||
def _launch_ffmpeg(
|
||||
input_url: str,
|
||||
youtube_rtmp: str,
|
||||
anchor_name: str,
|
||||
logger: Callable[[str], None],
|
||||
extra_cmd: List[str] = None
|
||||
) -> subprocess.Popen:
|
||||
|
||||
cmd = [
|
||||
"ffmpeg", "-nostdin", "-hide_banner", "-stats", "-stats_period", "60", "-loglevel", "info",
|
||||
|
||||
# 输入端超强重连
|
||||
"-reconnect", "1", "-reconnect_streamed", "1", "-reconnect_at_eof", "1",
|
||||
"-reconnect_delay_max", "30", "-rw_timeout", "20000000",
|
||||
|
||||
# 极致低延迟 + 时间戳修复
|
||||
"-fflags", "+genpts+igndts+flush_packets", "-flags", "+global_header",
|
||||
"-thread_queue_size", "4096", "-analyzeduration", "0", "-probesize", "32",
|
||||
|
||||
"-i", input_url,
|
||||
|
||||
# copy 模式(质量无损)
|
||||
"-c:v", "copy", "-c:a", "copy", "-bsf:a", "aac_adtstoasc",
|
||||
"-map", "0:v?", "-map", "0:a?",
|
||||
|
||||
# YouTube 强制参数
|
||||
"-f", "mpegts", "-g", "2", "-rtmp_buffer", "1000", "-rtmp_live", "live",
|
||||
"-max_delay", "0", "-tune", "zerolatency", "-avoid_negative_ts", "make_zero",
|
||||
"-muxdelay", "0", "-muxpreload", "0",
|
||||
"-fflags", "+nobuffer+flush_packets", "-avioflags", "direct",
|
||||
]
|
||||
|
||||
if extra_cmd:
|
||||
cmd.extend(extra_cmd)
|
||||
|
||||
cmd.append(youtube_rtmp)
|
||||
|
||||
creationflags = subprocess.CREATE_NEW_PROCESS_GROUP if os.name == "nt" else 0
|
||||
proc = subprocess.Popen(
|
||||
cmd,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
bufsize=0,
|
||||
creationflags=creationflags
|
||||
)
|
||||
|
||||
# 用于看门狗
|
||||
proc.last_output_ts = time.time()
|
||||
|
||||
def _reader():
|
||||
try:
|
||||
for line in proc.stdout: # type: ignore
|
||||
if not line:
|
||||
break
|
||||
text = line.decode(errors="ignore").rstrip()
|
||||
proc.last_output_ts = time.time()
|
||||
if any(k in text for k in ["bitrate=", "frame=", "speed=", "fps="]):
|
||||
logger(f"[{anchor_name}] {text}")
|
||||
except:
|
||||
pass
|
||||
threading.Thread(target=_reader, daemon=True).start()
|
||||
|
||||
return proc
|
||||
|
||||
# ==================== 主函数:高稳定性转播(替代原 check_subprocess) ====================
|
||||
def robust_youtube_relay(
|
||||
anchor_name: str,
|
||||
real_url: str,
|
||||
youtube_rtmp: str,
|
||||
logger: Callable[[str], None],
|
||||
get_latest_stream_url: Callable[[], Optional[str]], # 用于刷新 real_url(如有token过期)
|
||||
is_stream_still_live: Callable[[], bool], # 检查主播是否在播(API)
|
||||
extra_ffmpeg_args: List[str] = None, # 可选:加 -f segment 分段保存
|
||||
heartbeat_callback: Callable[[int], None] = None # 可选:每分钟回调已运行秒数
|
||||
) -> bool:
|
||||
"""
|
||||
返回值:
|
||||
True = 主播已下播(正常结束)
|
||||
False = 发生未知错误(建议计数错误次数)
|
||||
"""
|
||||
proc: Optional[subprocess.Popen] = None
|
||||
start_time = time.time()
|
||||
last_refresh = 0.0
|
||||
last_soft_restart = 0.0
|
||||
offline_count = 0
|
||||
last_live_ts = time.time()
|
||||
|
||||
try:
|
||||
proc = _launch_ffmpeg(real_url, youtube_rtmp, anchor_name, logger, extra_ffmpeg_args)
|
||||
logger(f"[{anchor_name}] 已开始高稳定性转播 → {youtube_rtmp.split('/')[-1]}")
|
||||
|
||||
while True:
|
||||
now = time.time()
|
||||
elapsed = int(now - start_time)
|
||||
silence = now - proc.last_output_ts if proc else 0
|
||||
|
||||
# 1. 进程意外退出 → 重启
|
||||
if proc and proc.poll() is not None:
|
||||
logger(f"[{anchor_name}] ffmpeg 意外退出,3秒后重启...")
|
||||
time.sleep(3)
|
||||
proc = _launch_ffmpeg(real_url, youtube_rtmp, anchor_name, logger, extra_ffmpeg_args)
|
||||
start_time = time.time()
|
||||
continue
|
||||
|
||||
# 2. 看门狗:长时间无输出 → 软重启
|
||||
if elapsed > WATCHDOG_GRACE and silence > WATCHDOG_SILENCE:
|
||||
if now - last_soft_restart > SOFT_RESTART_COOLDOWN:
|
||||
logger(f"[{anchor_name}] 检测到卡顿({silence:.0f}s无输出),软重启 ffmpeg...")
|
||||
last_soft_restart = now
|
||||
if proc and proc.stdin:
|
||||
proc.stdin.write(b'q')
|
||||
proc.stdin.flush()
|
||||
time.sleep(4)
|
||||
proc = _launch_ffmpeg(real_url, youtube_rtmp, anchor_name, logger, extra_ffmpeg_args)
|
||||
start_time = time.time()
|
||||
continue
|
||||
|
||||
# 3. 定期刷新播放地址(防token过期)
|
||||
if now - last_refresh > REFRESH_URL_EVERY:
|
||||
new_url = get_latest_stream_url()
|
||||
if new_url and new_url != real_url:
|
||||
logger(f"[{anchor_name}] 检测到新的播放地址,切换中...")
|
||||
real_url = new_url
|
||||
if proc and proc.stdin:
|
||||
proc.stdin.write(b'q')
|
||||
time.sleep(3)
|
||||
proc = _launch_ffmpeg(real_url, youtube_rtmp, anchor_name, logger, extra_ffmpeg_args)
|
||||
start_time = time.time()
|
||||
last_refresh = now
|
||||
|
||||
# 4. 下播防抖判定(核心防误判)
|
||||
try:
|
||||
still_live = is_stream_still_live()
|
||||
except:
|
||||
still_live = True # 接口异常不判下播
|
||||
|
||||
if not still_live:
|
||||
# 交叉验证:流还能访问 → 忽略API误判
|
||||
if _probe_alive(real_url):
|
||||
logger(f"[{anchor_name}] API判下播但流仍活跃,忽略本次误判")
|
||||
still_live = True
|
||||
last_live_ts = now
|
||||
|
||||
if still_live:
|
||||
offline_count = 0
|
||||
last_live_ts = now
|
||||
else:
|
||||
if now - last_live_ts < OFFLINE_WINDOW:
|
||||
logger(f"[{anchor_name}] 短期掉线,忽略")
|
||||
else:
|
||||
offline_count += 1
|
||||
if offline_count >= OFFLINE_DEBOUNCE:
|
||||
logger(f"[{anchor_name}] 连续{offline_count}次确认下播,结束转播")
|
||||
if proc and proc.stdin:
|
||||
proc.stdin.write(b'q')
|
||||
return True
|
||||
else:
|
||||
logger(f"[{anchor_name}] 疑似下播 {offline_count}/{OFFLINE_DEBOUNCE},继续观察")
|
||||
|
||||
# 5. 心跳日志 & 回调
|
||||
if elapsed % 60 < 2:
|
||||
h, m = divmod(elapsed // 60, 60)
|
||||
logger(f"[{anchor_name}] 正在转播 {h:02d}:{m:02d}")
|
||||
if heartbeat_callback:
|
||||
heartbeat_callback(elapsed)
|
||||
|
||||
time.sleep(8)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
logger(f"[{anchor_name}] 手动终止转播")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger(f"[{anchor_name}] 严重错误: {e}")
|
||||
return False
|
||||
finally:
|
||||
if proc:
|
||||
try:
|
||||
proc.stdin.write(b'q') if proc.stdin else None
|
||||
proc.wait(timeout=8)
|
||||
except:
|
||||
proc.kill()
|
||||
111
backup/wending
Normal file
111
backup/wending
Normal file
@@ -0,0 +1,111 @@
|
||||
|
||||
try:
|
||||
if split_video_by_time:
|
||||
now = time.strftime("%Y-%m-%d_%H-%M-%S", time.localtime())
|
||||
print(f"{rec_info} 开始推流到 YouTube: {anchor_name}_{title_in_name}_{now}")
|
||||
|
||||
YT_STREAM_KEY = "ue78-1c3e-mr9g-14mz-9r4z"
|
||||
youtube_rtmp = f"rtmp://a.rtmp.youtube.com/live2/{YT_STREAM_KEY}"
|
||||
|
||||
# ------------------------
|
||||
# NVENC 检测
|
||||
# ------------------------
|
||||
codec_v = "libx264"
|
||||
preset_v = "veryfast"
|
||||
tune_param = ["-tune", "zerolatency"]
|
||||
|
||||
try:
|
||||
if platform.system().lower() in ["windows", "linux"]:
|
||||
check = subprocess.run(["nvidia-smi"], capture_output=True, timeout=10)
|
||||
if check.returncode == 0:
|
||||
enc = subprocess.run(["ffmpeg", "-hide_banner", "-encoders"],
|
||||
capture_output=True, text=True, timeout=10)
|
||||
if "h264_nvenc" in enc.stdout:
|
||||
codec_v = "h264_nvenc"
|
||||
preset_v = "p5"
|
||||
tune_param = ["-tune", "ll", "-rc", "vbr"]
|
||||
print("检测到 NVIDIA GPU → 使用 NVENC 硬件加速")
|
||||
except:
|
||||
pass # 异常自动降级
|
||||
|
||||
# ------------------------
|
||||
# FFmpeg 命令(稳定推流)
|
||||
# ------------------------
|
||||
ffmpeg_command = [
|
||||
"ffmpeg",
|
||||
"-re",
|
||||
"-i", real_url,
|
||||
|
||||
"-vf", "scale=720:1280:force_original_aspect_ratio=decrease,pad=720:1280:(ow-iw)/2:(oh-ih)/2:black",
|
||||
|
||||
"-c:v", codec_v,
|
||||
"-preset", preset_v,
|
||||
*tune_param,
|
||||
|
||||
"-b:v", "2500k",
|
||||
"-maxrate", "2500k",
|
||||
"-bufsize", "5000k",
|
||||
"-g", "50",
|
||||
"-r", "25",
|
||||
"-pix_fmt", "yuv420p",
|
||||
"-bf", "0",
|
||||
|
||||
"-c:a", "aac",
|
||||
"-b:a", "128k",
|
||||
"-ar", "44100",
|
||||
"-ac", "2",
|
||||
"-af", "aresample=async=1:first_pts=0",
|
||||
|
||||
"-flags", "+low_delay",
|
||||
"-fflags", "+genpts",
|
||||
"-thread_queue_size", "512",
|
||||
"-threads", "6",
|
||||
|
||||
"-f", "flv",
|
||||
youtube_rtmp
|
||||
]
|
||||
|
||||
# ------------------------
|
||||
# 永不断流重连逻辑
|
||||
# ------------------------
|
||||
proc = None
|
||||
while True:
|
||||
try:
|
||||
if proc and proc.poll() is None:
|
||||
proc.terminate()
|
||||
try:
|
||||
proc.wait(timeout=8)
|
||||
except:
|
||||
proc.kill()
|
||||
|
||||
print(f"{anchor_name}_{title_in_name}_{now} → 启动 YouTube 推流...")
|
||||
proc = subprocess.Popen(
|
||||
ffmpeg_command,
|
||||
stdin=subprocess.DEVNULL,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True
|
||||
)
|
||||
|
||||
# 打印 FFmpeg 错误日志,便于排查
|
||||
while True:
|
||||
line = proc.stderr.readline()
|
||||
if not line:
|
||||
break
|
||||
line = line.strip()
|
||||
if line:
|
||||
print(f"FFmpeg: {line}")
|
||||
if proc.poll() is not None:
|
||||
break
|
||||
|
||||
code = proc.returncode
|
||||
if code != 0:
|
||||
print(f"FFmpeg 退出(code={code}),5秒后重连...")
|
||||
time.sleep(5)
|
||||
|
||||
except Exception as e:
|
||||
print(f"启动 FFmpeg 异常: {e}")
|
||||
time.sleep(5)
|
||||
|
||||
except Exception as e:
|
||||
print(f"外层异常: {e}")
|
||||
@@ -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,主播: 上海少妇鲜果茶
|
||||
|
||||
@@ -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 = 否
|
||||
追加格式后删除原文件 = 是
|
||||
生成时间字幕文件 = 否
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
https://live.douyin.com/956616126418
|
||||
@@ -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
|
||||
23
pyproject.toml
Normal file
23
pyproject.toml
Normal file
@@ -0,0 +1,23 @@
|
||||
[project]
|
||||
name = "DouyinLiveRecorder"
|
||||
version = "4.0.7"
|
||||
description = "可循环值守和多人录制的直播录制软件, 支持抖音、TikTok、Youtube、快手、虎牙、斗鱼、B站、小红书、pandatv、sooplive、flextv、popkontv、twitcasting、winktv、百度、微博、酷狗、17Live、Twitch、Acfun、CHZZK、shopee等40+平台直播录制"
|
||||
readme = "README.md"
|
||||
authors = [{name = "Hmily"}]
|
||||
license = { text = "MIT" }
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"requests>=2.31.0",
|
||||
"loguru>=0.7.3",
|
||||
"pycryptodome>=3.20.0",
|
||||
"distro>=1.9.0",
|
||||
"tqdm>=4.67.1",
|
||||
"httpx[http2]>=0.28.1",
|
||||
"PyExecJS>=1.5.1"
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
"Homepage" = "https://github.com/ihmily/DouyinLiveRecorder"
|
||||
"Documentation" = "https://github.com/ihmily/DouyinLiveRecorder"
|
||||
"Repository" = "https://github.com/ihmily/DouyinLiveRecorder"
|
||||
"Issues" = "https://github.com/ihmily/DouyinLiveRecorder/issues"
|
||||
454
src/ab_sign.py
Normal file
454
src/ab_sign.py
Normal 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"
|
||||
) + "="
|
||||
212
src/spider.py
212
src/spider.py
@@ -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,38 +65,19 @@ async def get_play_url_list(m3u8: str, proxy: OptionalStr = None, header: Option
|
||||
return play_url_list
|
||||
|
||||
|
||||
@trace_error_decorator
|
||||
async def get_douyin_app_stream_data(url: str, proxy_addr: OptionalStr = None, cookies: OptionalStr = None) -> dict:
|
||||
async def get_douyin_web_stream_data(url: str, proxy_addr: OptionalStr = None, cookies: OptionalStr = None):
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.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'
|
||||
'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
|
||||
|
||||
async def get_app_data(room_id: str, sec_uid: str) -> dict:
|
||||
app_params = {
|
||||
"verifyFp": "verify_lxj5zv70_7szNlAB7_pxNY_48Vh_ALKF_GA1Uf3yteoOY",
|
||||
"type_id": "0",
|
||||
"live_id": "1",
|
||||
"room_id": room_id,
|
||||
"sec_user_id": sec_uid,
|
||||
"version_code": "99.99.99",
|
||||
"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
|
||||
headers['cookie'] = cookies
|
||||
|
||||
try:
|
||||
web_rid = url.split('?')[0].split('live.douyin.com/')
|
||||
if len(web_rid) > 1:
|
||||
web_rid = web_rid[1]
|
||||
web_rid = url.split('?')[0].split('live.douyin.com/')[-1]
|
||||
params = {
|
||||
"aid": "6383",
|
||||
"app_name": "douyin_web",
|
||||
@@ -108,14 +90,99 @@ async def get_douyin_app_stream_data(url: str, proxy_addr: OptionalStr = None, c
|
||||
"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)}'
|
||||
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) 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'
|
||||
}
|
||||
if cookies:
|
||||
headers['Cookie'] = cookies
|
||||
|
||||
async def get_app_data(room_id: str, sec_uid: str) -> dict:
|
||||
app_params = {
|
||||
"verifyFp": "verify_hwj52020_7szNlAB7_pxNY_48Vh_ALKF_GA1Uf3yteoOY",
|
||||
"type_id": "0",
|
||||
"live_id": "1",
|
||||
"room_id": room_id,
|
||||
"sec_user_id": sec_uid,
|
||||
"version_code": "99.99.99",
|
||||
"app_id": "1128"
|
||||
}
|
||||
api2 = f'https://webcast.amemv.com/webcast/room/reflow/info/?{urllib.parse.urlencode(app_params)}'
|
||||
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:
|
||||
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
36
youtubeKey
Normal 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,主播: 加速姐的摆摊日记
|
||||
|
||||
Reference in New Issue
Block a user