Compare commits
37 Commits
linuxHDMI
...
codexHDMI2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
090578a27a | ||
|
|
4a1c675a60 | ||
|
|
4fee2eb702 | ||
|
|
3dc82e1158 | ||
|
|
e20c1a0b0b | ||
|
|
63443ea854 | ||
|
|
7118bfd591 | ||
|
|
85ff79fd10 | ||
|
|
915412a819 | ||
|
|
7cab3f27b8 | ||
|
|
22061fb3c8 | ||
|
|
895dafc0e0 | ||
|
|
fbdcf4cc73 | ||
|
|
e3ac026710 | ||
|
|
ef87c43b33 | ||
|
|
16dc9abbbb | ||
|
|
c3ee09674f | ||
|
|
784a4468b8 | ||
|
|
8bce529255 | ||
|
|
af927c6dca | ||
|
|
e0b3ac43fc | ||
|
|
8001baabf8 | ||
|
|
513c2b7b8c | ||
|
|
e050afbac1 | ||
|
|
46fb194a02 | ||
|
|
37ab25911a | ||
|
|
6f79f259a1 | ||
|
|
d74f3046a3 | ||
|
|
92ecc14e43 | ||
|
|
8053c4cb6d | ||
|
|
d34ecab19c | ||
|
|
ec5dc7f31e | ||
|
|
d9eceb2169 | ||
|
|
5a75f18887 | ||
|
|
9b6570d06e | ||
|
|
f4794e720a | ||
|
|
b84f9b751c |
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
||||
*.sh text eol=lf
|
||||
15
.gitignore
vendored
@@ -14,8 +14,9 @@ develop-eggs/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
# 仅忽略仓库根目录的 lib(勿用 lib/ 否则会误伤 scripts/linux/lib)
|
||||
/lib/
|
||||
/lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
@@ -172,8 +173,18 @@ node_modules/
|
||||
# Next 开发缓存(静态导出目录 web-console/out 可纳入版本库以便无 Node 环境直接部署)
|
||||
web-console/.next/
|
||||
|
||||
# scrcpy-server:由服务启动或 Docker 构建从 Genymobile Release 自动拉取(可纳入制品库而不提交)
|
||||
vendor/scrcpy-server.jar
|
||||
|
||||
# Paramiko 部署本地日志(可能含敏感命令回显)
|
||||
.deploy_logs/
|
||||
|
||||
# 无 PM2 时的本地进程状态
|
||||
.process_runner/
|
||||
|
||||
# 本机端口/主机覆盖(从 config/runtime.env.example 复制)
|
||||
config/runtime.env
|
||||
config/system-stack.env
|
||||
config/hardware.env
|
||||
config/hardware-profile.json
|
||||
config/hub_state.sqlite
|
||||
|
||||
@@ -24,5 +24,8 @@ RUN pip install --no-cache-dir -r requirements.txt
|
||||
COPY . .
|
||||
COPY --from=console /src/web-console/out ./web-console/out
|
||||
|
||||
# 构建期尝试拉取 scrcpy-server;失败不阻断构建(容器启动时 lifespan 会再试)
|
||||
RUN sh -c "python -m src.scrcpy_vendor || echo '[docker] scrcpy-server: build-time fetch skipped'"
|
||||
|
||||
EXPOSE 8101
|
||||
CMD ["sh", "-c", "exec python -m uvicorn web:app --host 0.0.0.0 --port ${PORT}"]
|
||||
|
||||
52
README.md
@@ -1,5 +1,57 @@
|
||||

|
||||
|
||||
## Linux Super Hub Profiles
|
||||
|
||||
This fork now includes a modular Linux deployment layout for ARM and X86 nodes.
|
||||
|
||||
### Live Platform(四层架构)
|
||||
|
||||
- **配置中心**:`/opt/live/config`(模板见 `live-platform/config/defaults/`,开发时回退到仓库内同路径)
|
||||
- **一键脚本**:`./install-core.sh`(系统 + 基础服务,与业务解耦)、`./install-live.sh`(仅无人直播业务)、`./doctor.sh`(体检)
|
||||
- **统一 API**:`GET /hub/dashboard`、`GET /hub/config` 等(见 [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md))
|
||||
- **产品级 Web**:根路径 `/` 为 Live Hub 控制中心;`/console` 为高级全功能页
|
||||
|
||||
- `install-hub.sh`: reusable Linux super hub with `live.local`, ShellCrash, WebTTY, Cockpit, File Browser, Homepage and Netdata
|
||||
- `install-business.sh`: Douyin / YouTube / HDMI business module with ffmpeg, mpv, ADB, Chromium, web-scrcpy and SRS
|
||||
- `install-all.sh`: installs both layers without tightly coupling them
|
||||
|
||||
Detailed docs:
|
||||
|
||||
- [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md)
|
||||
- [`docs/install-profiles.md`](docs/install-profiles.md)
|
||||
- [`docs/super-hub-architecture.md`](docs/super-hub-architecture.md)
|
||||
|
||||
### Default LAN Entrypoints
|
||||
|
||||
After `install-hub.sh` or `install-all.sh`, the default service map is:
|
||||
|
||||
- **`http://live.local`** (port 80, via **Caddy** `live-edge.service`) → same UI/API as the control plane
|
||||
- `http://live.local:8001` for direct access to FastAPI (no edge proxy)
|
||||
- `http://live.local/shellcrash` or `:8001/shellcrash` — ShellCrash 编辑器
|
||||
- `http://live.local/android` or `:8001/android` — 安卓中心
|
||||
- `http://live.local:3080` — Homepage 聚合(默认不再占用 80)
|
||||
- `http://live.local:7681` for WebTTY
|
||||
- `http://live.local:8082` for File Browser with `live / 12345678`
|
||||
- `http://live.local:19999` for Netdata
|
||||
|
||||
### ShellCrash Web Control
|
||||
|
||||
ShellCrash is no longer treated as a jump back to the terminal menu. The control plane now exposes:
|
||||
|
||||
- direct service start, stop, and restart
|
||||
- managed roots for `cfg`, `yaml`, `json`, `list`, and `env` files
|
||||
- in-browser create, edit, delete, and import-or-replace flows for YAML and related config files
|
||||
|
||||
### Android Device Center
|
||||
|
||||
The Android panel now combines multiple layers instead of showing only raw `web-scrcpy`:
|
||||
|
||||
- ADB device discovery and overview
|
||||
- screenshot-based preview with tap, double tap, and long press
|
||||
- package search and app launch / stop / clear actions
|
||||
- UI node extraction with quick jump actions
|
||||
- embedded raw `web-scrcpy` for full interactive fallback
|
||||
|
||||
## 💡简介
|
||||
[](https://www.python.org/downloads/release/python-3116/)
|
||||
[](https://github.com/ihmily/DouyinLiveRecorder)
|
||||
|
||||
1
backup/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# 使 backup 成为显式包,便于 from backup.msg_push import ...
|
||||
@@ -80,7 +80,7 @@ def _launch_ffmpeg(
|
||||
|
||||
cmd.append(youtube_rtmp)
|
||||
|
||||
creationflags = subprocess.CREATE_NEW_PROCESS_GROUP if os.name == "nt" else 0
|
||||
creationflags = 0
|
||||
proc = subprocess.Popen(
|
||||
cmd,
|
||||
stdin=subprocess.PIPE,
|
||||
|
||||
BIN
codex-live-stack.tgz
Normal file
@@ -1,39 +1,2 @@
|
||||
https://live.douyin.com/925673220466,主播: 兜有米麻糍
|
||||
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,主播: 加速姐的摆摊日记
|
||||
https://live.douyin.com/661694696687,主播: 小米摆摊记
|
||||
https://live.douyin.com/228132412943,主播: 00后摆摊小可爱
|
||||
https://live.douyin.com/876677865786,主播: 牛哄哄
|
||||
https://live.douyin.com/573735790640,主播: 小董卤串(餐饮)
|
||||
https://live.douyin.com/260608582882,主播: 阿伟在炒饭(90後炒饭)
|
||||
https://live.douyin.com/210443559964,主播: 小小诺
|
||||
https://live.douyin.com/305363702471,主播: 嘉嘉在摆摊
|
||||
https://live.douyin.com/143239713951,主播: ɞ_大馋丫头的烤肠
|
||||
# 每行一条源站直播页 URL(如抖音直播间),由控制台保存或手工编辑
|
||||
# 部署后请删除本注释并填入实际地址,或仅在 Web 控制台「直播地址列表」中填写
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
[录制设置]
|
||||
[录制设置]
|
||||
language(zh_cn/en) = zh_cn
|
||||
是否跳过代理检测(是/否) = 是
|
||||
直播保存路径(不填则默认) =
|
||||
@@ -49,7 +49,7 @@ SMTP邮件服务器端口 =
|
||||
发件人邮箱 =
|
||||
发件人显示昵称 =
|
||||
收件人邮箱 =
|
||||
ntfy推送地址 = https://ntfy.sh/xxxx
|
||||
ntfy推送地址 =
|
||||
ntfy推送标签 = tada
|
||||
ntfy推送邮箱 =
|
||||
pushplus推送token =
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"networkName": "lr_f976289ba590",
|
||||
"networkSecret": "5f86ed227e8cfcf7d8b70b287502b682aa8d3ca0b0c560ed",
|
||||
"networkName": "",
|
||||
"networkSecret": "",
|
||||
"peerUrls": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"redirect_uri": "http://127.0.0.1:8001/youtube/oauth/callback",
|
||||
"web": {
|
||||
"client_id": "305371487651-iuqafultpqp0rvitojr70560k1foet0n.apps.googleusercontent.com",
|
||||
"client_secret": "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",
|
||||
"client_id": "",
|
||||
"client_secret": "",
|
||||
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
||||
"token_uri": "https://oauth2.googleapis.com/token",
|
||||
"redirect_uris": ["http://127.0.0.1:8001/youtube/oauth/callback"]
|
||||
|
||||
14
config/hardware.env.example
Normal file
@@ -0,0 +1,14 @@
|
||||
# 复制为 config/hardware.env 或安装后由 scripts/hardware_probe.py --write 生成
|
||||
# 抖音→YouTube 推流(douyin_youtube_ffplay.py)在 Linux 上会读取本文件中的 LIVE_VIDEO_ENCODER。
|
||||
#
|
||||
# —— ARM(RK3588 等)常见:h264_rkmpp(需 /dev/mpp_service 与 ffmpeg 含 rkmpp)
|
||||
# —— ARM 通用 V4L2:h264_v4l2m2m
|
||||
# —— x86_64 Intel/AMD 核显(VA-API):h264_vaapi(需 /dev/dri render 节点)
|
||||
# —— x86_64 Intel Quick Sync:h264_qsv(需 ffmpeg 编译带 libmfx/oneVPL)
|
||||
# —— NVIDIA:一般无需写此处,推流脚本会自行检测 nvidia-smi + h264_nvenc
|
||||
#
|
||||
# LIVE_VIDEO_ENCODER=h264_v4l2m2m
|
||||
# LIVE_HW_DECODER=cpu
|
||||
# LIVE_HDMI_PLAYER=mpv-drm
|
||||
# LIVE_BROWSER_HWACCEL=auto
|
||||
# LIVE_STABILITY_MODE=safe
|
||||
@@ -5,7 +5,14 @@
|
||||
HOST=0.0.0.0
|
||||
|
||||
# Web 控制台(FastAPI + 静态页)端口;与同机 SRS / Clash / Neko 等冲突时请改成未占用端口,例如 8101、9200
|
||||
# 若 Nginx/Caddy 反代出现 502,请核对 proxy_pass 端口与这里一致(Docker Compose 默认常为 8101,PM2 ecosystem 默认 8001)
|
||||
PORT=8001
|
||||
|
||||
# 开发模式时 Next 端口(一般无需改)
|
||||
# NEXT_DEV_PORT=3000
|
||||
|
||||
# scrcpy-server 自动下载(安卓 Web 真流):默认同版本走 GitHub;内网可镜像后设下列之一(支持 {tag} / {version})
|
||||
# SCRCPY_SERVER_DOWNLOAD_URL=https://你的镜像/scrcpy-server-v{tag}
|
||||
# SCRCPY_SERVER_DOWNLOAD_URLS=https://a/v{tag},https://b/v{tag}
|
||||
# SCRCPY_SERVER_DOWNLOAD_NO_GITHUB=1
|
||||
# SKIP_SCRCPY_SERVER_FETCH=1
|
||||
|
||||
107
config/system-stack.env.example
Normal file
@@ -0,0 +1,107 @@
|
||||
# Copy to config/system-stack.env before first Linux deployment when you need custom values.
|
||||
# Profile model:
|
||||
# - install-hub.sh: base Linux super hub, web admin, ShellCrash, monitoring, beginner tools
|
||||
# - install-business.sh: Douyin / YouTube / HDMI business module only
|
||||
# - install-all.sh: hub first, then business module
|
||||
|
||||
# LAN identity
|
||||
HOSTNAME_ALIAS=live
|
||||
|
||||
# Shared control plane
|
||||
PORT=8001
|
||||
|
||||
# Caddy 将 :80 反代到 PORT(http://live.local 无端口验收)
|
||||
ENABLE_LIVE_EDGE_PROXY=1
|
||||
|
||||
# Hub web services
|
||||
WEBTTY_PORT=7681
|
||||
# 默认 3080,避免与 Caddy 占用 80(live.local 无端口访问控制台)
|
||||
HOMEPAGE_PORT=3080
|
||||
FILEBROWSER_PORT=8082
|
||||
NETDATA_PORT=19999
|
||||
|
||||
# Business streaming services
|
||||
SRS_HTTP_PORT=8080
|
||||
SRS_API_PORT=1985
|
||||
SRS_RTMP_PORT=1935
|
||||
SRS_SRT_PORT=10080
|
||||
SRS_WEBRTC_PORT=8088
|
||||
SRS_WEBRTC_UDP_PORT=8000
|
||||
|
||||
# ShellCrash
|
||||
SHELLCRASH_DIR=/etc/ShellCrash
|
||||
|
||||
# Browser / Android lab
|
||||
BROWSER_PROFILE_DIR=/var/lib/live-browser/profile
|
||||
BROWSER_START_URL=https://www.tiktok.com/live
|
||||
BROWSER_REMOTE_DEBUG_PORT=9222
|
||||
BROWSER_LOCALE=zh-CN
|
||||
|
||||
# WebTTY credentials
|
||||
WEBTTY_USER=live
|
||||
WEBTTY_PASS=12345678
|
||||
|
||||
# File Browser credentials
|
||||
FILEBROWSER_USER=live
|
||||
FILEBROWSER_PASS=12345678
|
||||
|
||||
# Wi-Fi onboarding
|
||||
WIFI_CONNECTION_NAME=live-wifi
|
||||
WIFI_SSID=live
|
||||
WIFI_PSK=12345678
|
||||
DISABLE_IPV6=1
|
||||
|
||||
# ws-scrcpy 克隆地址(可改为 NetrisTV 上游:https://github.com/NetrisTV/ws-scrcpy.git)
|
||||
# ANDROID_PANEL_GIT_URL=https://github.com/baixin1228/web-scrcpy.git
|
||||
|
||||
# 可选:Docker Neko 浏览器桌面(m1k1o/neko),与 SRS 8080 错开端口
|
||||
# 策略/启动参数对齐 x86 参考机:油猴强制安装、扩展白名单、用户数据持久化目录见 NEKO_PROFILE_HOST_DIR
|
||||
# WebRTC 画面:NEKO_WEBRTC_NAT1TO1 必须为本机在局域网中的 IP(如 192.168.x.x),与参考机填网关 IP 同理
|
||||
# 设为 1 才会安装/启动 Neko 容器;不需要浏览器桌面可改回 0
|
||||
ENABLE_NEKO=1
|
||||
NEKO_HTTP_PORT=9200
|
||||
# Neko 浏览器登录:用户名为 live / admin,口令与全站策略一致(默认 12345678)
|
||||
NEKO_USER_PASS=12345678
|
||||
NEKO_ADMIN_PASS=12345678
|
||||
# 持久化 Chromium 配置(油猴、YouTube 登录、脚本);可从参考机 rsync 整个目录覆盖以迁移
|
||||
NEKO_PROFILE_HOST_DIR=
|
||||
NEKO_SHM_SIZE=2gb
|
||||
NEKO_DESKTOP_SCREEN=1920x1080@30
|
||||
NEKO_WEBRTC_EPR=52000-52100
|
||||
NEKO_WEBRTC_UDP_RANGE=52000-52100
|
||||
NEKO_WEBRTC_ICELITE=1
|
||||
NEKO_WEBRTC_NAT1TO1=
|
||||
NEKO_PLUGINS_ENABLED=true
|
||||
|
||||
# Android gateway mode(仅双网口直连安卓板时需要;单网口留空 ANDROID_DOWNLINK_IF 即可,控制台为「跳过」)
|
||||
ANDROID_GATEWAY_NAME=live-android-gateway
|
||||
ANDROID_UPLINK_IF=eth0
|
||||
ANDROID_DOWNLINK_IF=
|
||||
ANDROID_GATEWAY_CIDR=192.168.51.1/24
|
||||
ANDROID_PANEL_PORT=5000
|
||||
ANDROID_PANEL_DIR=/opt/live-modules/web-scrcpy
|
||||
ANDROID_PANEL_VIDEO_BIT_RATE=1024000
|
||||
|
||||
# Optional modules
|
||||
# Shared
|
||||
ENABLE_SHELLCRASH=1
|
||||
ENABLE_WEBTTY=1
|
||||
ENABLE_BROWSER=1
|
||||
ENABLE_MDNS=1
|
||||
ENABLE_WIFI_PROFILE=1
|
||||
ENABLE_ANDROID_PANEL=1
|
||||
# Hub profile defaults
|
||||
ENABLE_COCKPIT=1
|
||||
ENABLE_FILEBROWSER=1
|
||||
ENABLE_HOMEPAGE=1
|
||||
ENABLE_NETDATA=1
|
||||
# Business profile defaults
|
||||
ENABLE_SRS=1
|
||||
|
||||
# Future modules (recommended next wave, not yet wired into the core installer)
|
||||
# ENABLE_CADDY=0
|
||||
# ENABLE_WG_EASY=0
|
||||
# ENABLE_FRP=0
|
||||
# ENABLE_MATRIX=0
|
||||
# ENABLE_UPTIME_KUMA=0
|
||||
# ENABLE_N8N=0
|
||||
@@ -1 +0,0 @@
|
||||
9d0103f49a3fea59311c014b9468e3ad2bd14d4f8334a7ec
|
||||
@@ -1,7 +1,6 @@
|
||||
[youtube]
|
||||
# 必填:YouTube 直播推流 key(在 YouTube 工作室 → 创建 → 流式传输 获取)
|
||||
; key = qxvb-r47b-r5ju-6ud3-6k7z
|
||||
key = x04z-564w-aks7-embw-30y4
|
||||
key =
|
||||
|
||||
# 可选:是否使用 RTMPS 加密推流,填 否 则用 RTMP(默认 RTMPS)
|
||||
; rtmps = 否
|
||||
@@ -10,4 +9,4 @@ key = x04z-564w-aks7-embw-30y4
|
||||
; bitrate = 2500
|
||||
|
||||
# 可选:软编 speed 长期小于 1 时开启——轻量音频链(无 arnndn/长 EQ),减轻 CPU。填 1 或 是
|
||||
fast_audio = 1
|
||||
; fast_audio = 1
|
||||
|
||||
8
dev.sh
@@ -3,6 +3,10 @@
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")"
|
||||
export PORT="${PORT:-8001}"
|
||||
PY=python3
|
||||
command -v python3 >/dev/null 2>&1 || PY=python
|
||||
if [[ -x ".venv/bin/python" ]]; then
|
||||
PY=".venv/bin/python"
|
||||
else
|
||||
PY=python3
|
||||
command -v python3 >/dev/null 2>&1 || PY=python
|
||||
fi
|
||||
exec "$PY" scripts/launch.py --dev --port "$PORT"
|
||||
|
||||
45
docs/ARCHITECTURE.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# Live Platform 四层架构
|
||||
|
||||
## 分层
|
||||
|
||||
| 层 | 职责 | 代码/路径 |
|
||||
|----|------|-----------|
|
||||
| Core OS | 用户、SSH、mDNS、Wi‑Fi、WebTTY、Docker、systemd、日志 | `scripts/linux/install_hub.sh` 及系统包 |
|
||||
| Infra Service | ShellCrash、Caddy、Netdata、SRS、Neko、ws-scrcpy、FRP、WireGuard 占位 | `services/*`、`scripts/linux/service_ctl.sh` |
|
||||
| Business | 无人直播 douyinyoutube(FFmpeg/HDMI/推流),可脱离控制台运行 | 仓库根业务脚本、`install-live.sh` |
|
||||
| Web Console | 仅通过 API 与配置中心「控制」,不实现业务算法 | `web.py`、`web-console`、`/hub/*` API |
|
||||
|
||||
## 配置中心
|
||||
|
||||
- 安装目标:`/opt/live/config/`
|
||||
- 默认模板:`live-platform/config/defaults/`
|
||||
- 文件:`system.json`、`network.json`、`services/*.json`、`business/*.json`
|
||||
- 生成兼容层:`live-platform/tools/render_stack_env.py` → `/opt/live/generated/system-stack.env`(供现有 shell 读取,逐步淘汰分散 `.env`)
|
||||
|
||||
## 一键脚本(幂等)
|
||||
|
||||
| 脚本 | 作用 |
|
||||
|------|------|
|
||||
| `./install-core.sh` | 同步 JSON → `/opt/live/config`,生成 env,调用 `install_hub.sh`(无业务) |
|
||||
| `./install-live.sh` | 仅业务模块,调用 `install_business.sh` |
|
||||
| `./doctor.sh` | 网络、端口、adb、ffmpeg、docker、API 健康检查 |
|
||||
|
||||
## API
|
||||
|
||||
- `GET /hub/dashboard`:快照 + 服务 + 安卓 + 直播进程状态
|
||||
- `GET /hub/config`:配置中心只读聚合
|
||||
- `POST /hub/business/douyinyoutube`:业务元数据(如抖音 URL 声明)
|
||||
- 既有:`/service_action`、`/android/*`、`/process_monitor`、`/save_url_config` 等
|
||||
|
||||
## 前端
|
||||
|
||||
- `/`:`LiveControlApp` 产品壳(总览、服务、直播、安卓、网络、设置)
|
||||
- `/console`:高级遗留控制台(全功能)
|
||||
- 开发:`middleware` 将 `/hub` 代理到本机 FastAPI
|
||||
|
||||
## live.local(无端口验收)
|
||||
|
||||
- **`install_hub.sh` 末尾**执行 **`install_live_edge_proxy`**:安装/启用 **`live-edge.service`(Caddy)**,将 **`http://live.local`(:80)** 反代到 **`127.0.0.1:${PORT}`**(默认 8001)。
|
||||
- **Homepage** 默认改绑 **`HOMEPAGE_PORT=3080`**,避免与 Caddy 抢 80。
|
||||
- 仅更新反代:`sudo bash scripts/linux/reinstall_live_edge.sh`
|
||||
- `ENABLE_LIVE_EDGE_PROXY=0` 可关闭(`system-stack.env`)。
|
||||
120
docs/install-profiles.md
Normal file
@@ -0,0 +1,120 @@
|
||||
# Linux Install Profiles
|
||||
|
||||
## Quick Start
|
||||
|
||||
Use one of these entrypoints from the repo root on Debian, Ubuntu, or Armbian:
|
||||
|
||||
```bash
|
||||
sudo bash install-hub.sh
|
||||
sudo bash install-business.sh
|
||||
sudo bash install-all.sh
|
||||
```
|
||||
|
||||
### After `git pull` (no full reinstall)
|
||||
|
||||
刷新依赖、重建前端、重启 `live-console` 与 `live-edge`(**不要**为此再跑 `install-all.sh`,除非你要重做整机栈):
|
||||
|
||||
```bash
|
||||
cd /home/live/douyinyoutube # 或你的克隆路径
|
||||
sudo bash upgrade-live.sh
|
||||
```
|
||||
|
||||
若仓库只更新了 **`live-console.service` 模板**(例如 `StartLimitIntervalSec`),可单独刷新单元:
|
||||
|
||||
```bash
|
||||
sudo bash refresh-live-unit.sh
|
||||
```
|
||||
|
||||
## What Each Script Does
|
||||
|
||||
### `install-hub.sh`
|
||||
|
||||
Use this when the Linux device should become the reusable super hub first.
|
||||
|
||||
- Creates and refreshes the `live` account
|
||||
- Installs the shared control plane runtime
|
||||
- Enables `live.local`
|
||||
- Creates the default Wi-Fi profile `live / 12345678` when `wlan0` exists
|
||||
- Installs ShellCrash, WebTTY, Cockpit, File Browser, Homepage, and Netdata
|
||||
- Installs the Android web panel based on `web-scrcpy`
|
||||
- Builds the web console and enables `live-console.service`
|
||||
|
||||
### `install-business.sh`
|
||||
|
||||
Use this when the device should only run the Douyin or YouTube business stack.
|
||||
|
||||
- Creates and refreshes the `live` account
|
||||
- Installs the shared control plane runtime
|
||||
- Installs ffmpeg, mpv, ADB, Chromium
|
||||
- Installs the Android web panel based on `web-scrcpy`
|
||||
- Starts SRS
|
||||
- Builds the web console and enables `live-console.service`
|
||||
- Installs **Caddy** `live-edge.service` so **`http://live.local`** (port 80) reverse-proxies to the control plane (same as hub)
|
||||
- Generates the hardware profile for adaptive ARM or X86 defaults
|
||||
|
||||
### `http://live.local` returns 502
|
||||
|
||||
502 means the edge proxy reached nothing usable on the upstream port (default `127.0.0.1:8001`).
|
||||
|
||||
1. Check the control plane: `sudo systemctl status live-console.service` and `journalctl -u live-console.service -n 80 --no-pager`.
|
||||
2. Check the edge: `sudo systemctl status live-edge.service` and confirm `config/system-stack.env` **`PORT`** matches what Caddy uses (regenerate with `sudo bash scripts/linux/reinstall_live_edge.sh` from the repo root).
|
||||
3. Quick probe: `curl -sf http://127.0.0.1:8001/health` — if this fails, fix `live-console` first (Python venv, `web-console/out`, or `scripts/launch.py` errors in the journal).
|
||||
|
||||
**Older business installs** (before `install_live_edge_proxy` was included) never installed Caddy; if you still see 502 on port 80, you likely had a prior hub/edge install—run `sudo bash scripts/linux/reinstall_live_edge.sh` after pulling the latest repo.
|
||||
|
||||
### Browser: `live.local` — **refused to connect** (not 502)
|
||||
|
||||
That means the TCP connection was **rejected or nothing listens** on the address you used (often **port 80** on the box’s IP).
|
||||
|
||||
1. On the **device**, run: `sudo bash probe-live.sh` (or `scripts/linux/probe_live_access.sh`) and read the `:80` / `:8001` lines.
|
||||
2. If **`:8001` works** but **`:80` fails**: `live-edge` (Caddy) is down — `sudo systemctl restart live-edge.service` or `sudo bash scripts/linux/reinstall_live_edge.sh`.
|
||||
3. If **both fail**: `live-console` is down — `sudo systemctl restart live-console.service` and `journalctl -u live-console.service -n 80`.
|
||||
4. From a **laptop/phone**, open **`http://<device-LAN-IP>:8001/`** first (same Wi‑Fi). If that works but `http://live.local` does not, the problem is **mDNS** (Windows often needs Bonjour) or you’re not on the same LAN — use the IP bookmark.
|
||||
5. **Firewall** (nftables/ufw) blocking **80/8001** from Wi‑Fi will also show as “refused” from other hosts while loopback still works.
|
||||
|
||||
### PM2:单频道与多频道 Pro(Linux ARM / x86_64)
|
||||
|
||||
- **单频道**:在项目根启用一个业务进程即可,例如取消注释 `ecosystem.config.cjs` 里的 `youtube` 或 `tiktok`,执行 `pm2 start ecosystem.config.cjs && pm2 save`。
|
||||
- **多频道 Pro**:每路直播需要**独立的** `config/youtube.ini` 与 `config/URL_config.ini`(或整份独立工作目录)。为第二路及以后各复制一份目录(或符号链接脚本外的配置),在 PM2 里为每个 app 设置不同的 `cwd`,`name` 与 Web 控制台 Pro 线路建议名一致(如 `youtube2__<channelId>`)。
|
||||
- **硬件编码**:`sudo python3 scripts/hardware_probe.py --write` 会写入 `config/hardware.env` 与 `hardware-profile.json`;推流侧可选 `LIVE_VIDEO_ENCODER`(RKMPP / V4L2M2M / VA-API / QSV 等,见 `config/hardware.env.example`)。NVIDIA 一般由 `douyin_youtube_ffplay.py` 自动走 NVENC。
|
||||
- **音频链**:与 GitLab 分支 `ffmpegAudioRMFoode` 中 `douyin_youtube_ffplay.py` 的 FFmpeg 处理思路一致(`fast_audio`、arnndn、EQ 等);当前主线在 Linux 上还包含板载编码探测与推流稳定性增强,以前端无需对齐 Electron 为准。
|
||||
|
||||
### `install-all.sh`
|
||||
|
||||
Use this when the node should be both the super hub and the livestream business node.
|
||||
|
||||
- Runs `install-hub.sh`
|
||||
- Runs `install-business.sh`
|
||||
- Reuses shared modules instead of replacing them
|
||||
|
||||
## Shared Config
|
||||
|
||||
All install profiles read the same shared file:
|
||||
|
||||
- `config/system-stack.env`
|
||||
|
||||
If the file does not exist, the installer copies:
|
||||
|
||||
- `config/system-stack.env.example`
|
||||
|
||||
This keeps host identity, ports, Wi-Fi, ShellCrash path, and module toggles centralized.
|
||||
|
||||
## Default Access URLs
|
||||
|
||||
With the default hostname and ports, a fresh hub exposes:
|
||||
|
||||
- `http://live.local:8001` main control plane
|
||||
- `http://live.local:8001/shellcrash` ShellCrash editor route
|
||||
- `http://live.local:8001/android` Android device center route
|
||||
- `http://live.local:7681` WebTTY
|
||||
- `http://live.local:8082` File Browser, default login `live / 12345678`
|
||||
- `http://live.local:19999` Netdata
|
||||
|
||||
The ShellCrash and Android pages are separate routes on purpose, so they can be bookmarked directly instead of sharing the same tabbed root URL.
|
||||
|
||||
## Separation Rules
|
||||
|
||||
- The hub profile must keep working without SRS.
|
||||
- The business profile must keep working without Cockpit, Homepage, File Browser, or Netdata.
|
||||
- The control plane should show degraded modules as unavailable instead of crashing.
|
||||
- Every service must be removable by systemd or compose boundaries.
|
||||
103
docs/super-hub-architecture.md
Normal file
@@ -0,0 +1,103 @@
|
||||
# Super Hub Architecture
|
||||
|
||||
## Goal
|
||||
|
||||
Build the ARM or X86 node as a reusable Linux super hub instead of a single-purpose
|
||||
streaming box. The Douyin or YouTube workflow is one optional business module that can
|
||||
be installed alone or layered on top of the hub later.
|
||||
|
||||
## Install Profiles
|
||||
|
||||
- `install-hub.sh`
|
||||
- Base Linux super hub
|
||||
- Web control plane
|
||||
- mDNS `live.local`
|
||||
- Wi-Fi onboarding profile
|
||||
- ShellCrash
|
||||
- WebTTY
|
||||
- Cockpit
|
||||
- File Browser
|
||||
- Homepage
|
||||
- Netdata
|
||||
- Hardware probe
|
||||
|
||||
- `install-business.sh`
|
||||
- Shared control plane runtime
|
||||
- ffmpeg, mpv, ADB, Chromium
|
||||
- SRS
|
||||
- Hardware probe and adaptive decode defaults
|
||||
- Douyin or YouTube business orchestration
|
||||
|
||||
- `install-all.sh`
|
||||
- Runs the hub profile first
|
||||
- Runs the business profile second
|
||||
- Safe to rerun because the shared pieces are idempotent
|
||||
|
||||
## Layers
|
||||
|
||||
1. Base Node
|
||||
- Debian / Ubuntu / Armbian
|
||||
- `live` user
|
||||
- mDNS with `live.local`
|
||||
- Wi-Fi bootstrap
|
||||
- hardware probe and graceful degradation
|
||||
|
||||
2. Control Plane
|
||||
- FastAPI backend
|
||||
- Next.js panel
|
||||
- managed config roots
|
||||
- service registry and status polling
|
||||
|
||||
3. Hub Modules
|
||||
- ShellCrash
|
||||
- WebTTY
|
||||
- Cockpit
|
||||
- File Browser
|
||||
- Homepage
|
||||
- Netdata
|
||||
- Android Web Panel
|
||||
|
||||
4. Business Modules
|
||||
- Douyin / YouTube relay app
|
||||
- SRS
|
||||
- HDMI playback bridge
|
||||
- Android / ADB integration
|
||||
- Chromium persistent profile
|
||||
- Browser-based device control via web-scrcpy
|
||||
|
||||
5. Future Modules
|
||||
- Caddy
|
||||
- WireGuard
|
||||
- FRP
|
||||
- Matrix
|
||||
- Uptime Kuma
|
||||
- n8n
|
||||
- AI and media workloads
|
||||
|
||||
## Design Rules
|
||||
|
||||
- `install-hub.sh` must never hard-depend on business services such as SRS.
|
||||
- `install-business.sh` must work on a clean Linux system without requiring the hub profile first.
|
||||
- If the hub profile already exists, the business profile must reuse it instead of replacing it.
|
||||
- Each service stays in its own systemd unit or compose file so it can be removed without breaking unrelated modules.
|
||||
- The control plane edits only managed roots and never exposes arbitrary filesystem write access.
|
||||
- ARM and X86 share one control plane, then diverge only through the generated hardware profile.
|
||||
|
||||
## Current Implementation
|
||||
|
||||
- `scripts/linux/lib/common.sh` contains the shared install functions used by every profile.
|
||||
- `scripts/linux/install_hub.sh` installs the reusable Linux super hub.
|
||||
- `scripts/linux/install_business.sh` installs the Douyin or YouTube business module.
|
||||
- `scripts/linux/install_stack.sh` remains as a compatibility entrypoint for `hub`, `business`, and `all`.
|
||||
- `src/control_plane.py` exposes managed roots, hardware profile loading, and service actions.
|
||||
- `scripts/hardware_probe.py` generates safe runtime defaults for ARM and X86.
|
||||
- `services/` keeps dockerized modules isolated instead of merging them into one monolith.
|
||||
|
||||
## Recommended Next Modules
|
||||
|
||||
- Caddy for unified reverse proxy and TLS
|
||||
- wg-easy for WireGuard mesh management
|
||||
- FRP panel for tunnel management
|
||||
- Uptime Kuma for alerting
|
||||
- n8n for workflow automation
|
||||
- Matrix plus Element for messaging and operations collaboration
|
||||
4
doctor.sh
Normal file
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
ROOT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
exec bash "$ROOT_DIR/live-platform/scripts/doctor.sh" "$@"
|
||||
@@ -1,4 +1,5 @@
|
||||
# douyin_youtube_ffplay.py
|
||||
# Linux:仓库内推流实现,编码由 config/hardware.env(LIVE_VIDEO_ENCODER)与可选 NVENC 探测决定。
|
||||
import queue
|
||||
import unicodedata
|
||||
import platform
|
||||
@@ -17,6 +18,41 @@ class StreamEnded(Exception):
|
||||
pass
|
||||
|
||||
|
||||
_FFMPEG_STATS_PERIOD_OK: bool | None = None
|
||||
|
||||
|
||||
def _require_ffmpeg_stats_period() -> None:
|
||||
"""推流依赖 -stats_period(ffmpeg 约 4.4+);Debian 11 默认 4.3 需升级。"""
|
||||
global _FFMPEG_STATS_PERIOD_OK
|
||||
if _FFMPEG_STATS_PERIOD_OK is True:
|
||||
return
|
||||
if _FFMPEG_STATS_PERIOD_OK is False:
|
||||
raise RuntimeError(
|
||||
"ffmpeg 不支持 -stats_period。板端执行: sudo bash scripts/linux/upgrade_ffmpeg_newer.sh"
|
||||
)
|
||||
try:
|
||||
r = subprocess.run(
|
||||
["ffmpeg", "-hide_banner", "-h", "full"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30,
|
||||
)
|
||||
blob = (r.stdout or "") + (r.stderr or "")
|
||||
if "-stats_period" in blob:
|
||||
_FFMPEG_STATS_PERIOD_OK = True
|
||||
return
|
||||
except FileNotFoundError:
|
||||
_FFMPEG_STATS_PERIOD_OK = False
|
||||
raise RuntimeError("未找到 ffmpeg,请先安装。") from None
|
||||
except Exception as e:
|
||||
print(f"[WARN] 无法检测 ffmpeg 是否支持 -stats_period: {e}")
|
||||
return
|
||||
_FFMPEG_STATS_PERIOD_OK = False
|
||||
raise RuntimeError(
|
||||
"ffmpeg 不支持 -stats_period(需约 4.4+)。板端执行: sudo bash scripts/linux/upgrade_ffmpeg_newer.sh"
|
||||
)
|
||||
|
||||
|
||||
def sanitize_title(text: str, max_len: int = 120) -> str:
|
||||
if not text:
|
||||
return ""
|
||||
@@ -70,7 +106,6 @@ def start_douyin_youtube_ffplay(
|
||||
recording: set,
|
||||
recording_time_list: dict,
|
||||
running_list: list,
|
||||
monitoring: list,
|
||||
clear_record_info,
|
||||
color_obj,
|
||||
):
|
||||
@@ -125,18 +160,22 @@ def start_douyin_youtube_ffplay(
|
||||
STALE_OUTPUT_SECONDS = 40
|
||||
RECONNECT_DELAY_MAX = 10
|
||||
|
||||
END_KEYWORDS = [
|
||||
# 明确结束 / 平台拒绝:停止整条转推(与瞬时网络错误区分)
|
||||
TERMINAL_KEYWORDS = [
|
||||
"live has ended",
|
||||
"server returned 404",
|
||||
"ingestion error",
|
||||
"access denied",
|
||||
"server returned 403",
|
||||
"client disconnected",
|
||||
]
|
||||
# 瞬时错误:结束当前 ffmpeg 进程后由外层循环退避重连
|
||||
TRANSIENT_KEYWORDS = [
|
||||
"connection reset by peer",
|
||||
"failed to write trailer",
|
||||
"invalid data found",
|
||||
"server returned 403",
|
||||
"server returned 404",
|
||||
"server returned 500",
|
||||
"ingestion error",
|
||||
"access denied",
|
||||
"packet too short",
|
||||
"client disconnected",
|
||||
"live has ended",
|
||||
"no route to host",
|
||||
"connection timed out",
|
||||
"connection refused",
|
||||
@@ -157,24 +196,74 @@ def start_douyin_youtube_ffplay(
|
||||
stream_title = sanitize_title(f"{anchor_name}{('_' + clean_title) if clean_title else ''}_{timestamp_str}")
|
||||
print(f"[INFO] {rec_info} 开始推流到 YouTube: {stream_title}")
|
||||
|
||||
# ====================== NVENC 检测(符合 YouTube 推荐比特率)======================
|
||||
# ====================== 视频编码:hardware.env(ARM)→ NVENC → libx264 ======================
|
||||
def _load_simple_env(path: str) -> dict[str, str]:
|
||||
out: dict[str, str] = {}
|
||||
if not os.path.isfile(path):
|
||||
return out
|
||||
with open(path, "r", encoding="utf-8", errors="ignore") as fh:
|
||||
for line in fh:
|
||||
line = line.strip()
|
||||
if not line or line.startswith("#") or "=" not in line:
|
||||
continue
|
||||
k, _, v = line.partition("=")
|
||||
key = k.strip()
|
||||
if key.startswith("#"):
|
||||
continue
|
||||
out[key] = v.strip().strip('"').strip("'")
|
||||
return out
|
||||
|
||||
codec_v = "libx264"
|
||||
# ultrafast+zerolatency:在软编下尽量让 speed≥1.0;画质略降、直播可接受
|
||||
video_args = ["-preset", "ultrafast", "-tune", "zerolatency", "-profile:v", "high", "-level", "4.0",
|
||||
"-b:v", b_v, "-maxrate", maxrate, "-bufsize", bufsize]
|
||||
try:
|
||||
if platform.system().lower() in ["windows", "linux"]:
|
||||
check = subprocess.run(["nvidia-smi"], capture_output=True, timeout=5)
|
||||
if check.returncode == 0:
|
||||
enc = subprocess.run(["ffmpeg", "-hide_banner", "-encoders"], capture_output=True, text=True, timeout=5)
|
||||
if "h264_nvenc" in (enc.stdout or ""):
|
||||
codec_v = "h264_nvenc"
|
||||
video_args = ["-profile:v", "high", "-level", "4.0",
|
||||
"-tune", "ll", "-rc", "cbr", "-b:v", b_v,
|
||||
"-maxrate", maxrate, "-bufsize", bufsize]
|
||||
print("[INFO] 检测到 NVIDIA GPU → 使用 NVENC 硬件加速")
|
||||
except Exception as e:
|
||||
print(f"[WARN] NVENC 检测失败,回退软件编码: {e}")
|
||||
video_args = [
|
||||
"-preset", "ultrafast", "-tune", "zerolatency", "-profile:v", "high", "-level", "4.0",
|
||||
"-b:v", b_v, "-maxrate", maxrate, "-bufsize", bufsize,
|
||||
]
|
||||
|
||||
_config_dir = os.path.dirname(os.path.abspath(config_file))
|
||||
_hw_env = _load_simple_env(os.path.join(_config_dir, "hardware.env"))
|
||||
_want_enc = (_hw_env.get("LIVE_VIDEO_ENCODER") or "").strip()
|
||||
_hw_enc_active = False
|
||||
if _want_enc and platform.system().lower() == "linux":
|
||||
try:
|
||||
enc = subprocess.run(
|
||||
["ffmpeg", "-hide_banner", "-encoders"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=6,
|
||||
)
|
||||
blob = (enc.stdout or "") + (enc.stderr or "")
|
||||
if _want_enc in blob and _want_enc.startswith("h264_"):
|
||||
codec_v = _want_enc
|
||||
video_args = ["-b:v", b_v, "-maxrate", maxrate, "-bufsize", bufsize]
|
||||
_hw_enc_active = True
|
||||
print(f"[INFO] 使用 config/hardware.env LIVE_VIDEO_ENCODER={codec_v}(板载硬件编码)")
|
||||
except Exception as e:
|
||||
print(f"[WARN] LIVE_VIDEO_ENCODER 校验失败: {e}")
|
||||
|
||||
if not _hw_enc_active:
|
||||
try:
|
||||
if platform.system().lower() == "linux":
|
||||
check = subprocess.run(["nvidia-smi"], capture_output=True, timeout=5)
|
||||
if check.returncode == 0:
|
||||
enc = subprocess.run(
|
||||
["ffmpeg", "-hide_banner", "-encoders"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5,
|
||||
)
|
||||
if "h264_nvenc" in (enc.stdout or ""):
|
||||
codec_v = "h264_nvenc"
|
||||
video_args = [
|
||||
"-profile:v", "high", "-level", "4.0",
|
||||
"-tune", "ll", "-rc", "cbr", "-b:v", b_v,
|
||||
"-maxrate", maxrate, "-bufsize", bufsize,
|
||||
]
|
||||
print("[INFO] 检测到 NVIDIA GPU → 使用 NVENC 硬件加速")
|
||||
except Exception as e:
|
||||
print(f"[WARN] NVENC 检测失败,回退软件编码: {e}")
|
||||
|
||||
_cpu_n = os.cpu_count() or 4
|
||||
_filter_threads = str(min(4, max(1, _cpu_n // 2)))
|
||||
|
||||
# ====================== 开源音频处理:背景音乐弱化 + 防版权 ======================
|
||||
_script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
@@ -226,32 +315,62 @@ def start_douyin_youtube_ffplay(
|
||||
if codec_v == "libx264":
|
||||
_video_mid += ["-x264-params", "force-cfr=1:scenecut=0:open_gop=0:sync-lookahead=0"]
|
||||
# -re:按源时间戳节奏拉流,避免突发过快;不用 realtime/arealtime(输入抖动时易让 YouTube 判为数据不足)
|
||||
ffmpeg_command = [
|
||||
"ffmpeg", "-y", "-loglevel", "info", "-nostdin",
|
||||
"-stats", "-stats_period", "1",
|
||||
"-rw_timeout", "50000000",
|
||||
"-reconnect", "1", "-reconnect_at_eof", "1", "-reconnect_streamed", "1",
|
||||
"-reconnect_delay_max", str(RECONNECT_DELAY_MAX),
|
||||
"-fflags", "+genpts+discardcorrupt",
|
||||
"-err_detect", "ignore_err",
|
||||
"-max_delay", "500000",
|
||||
"-thread_queue_size", "2048",
|
||||
"-filter_threads", "4",
|
||||
"-headers", douyin_headers,
|
||||
"-re",
|
||||
"-i", real_url,
|
||||
# bilinear 比 lanczos 轻很多,利于 speed 稳定在 ≥1.0x
|
||||
"-vf", "fps=30,scale=720:1280:force_original_aspect_ratio=decrease:flags=bilinear,pad=720:1280:(ow-iw)/2:(oh-ih)/2:black",
|
||||
"-c:v", codec_v,
|
||||
*video_args,
|
||||
*_video_mid,
|
||||
"-pix_fmt", "yuv420p",
|
||||
"-c:a", "aac", "-b:a", "128k", "-ar", "48000", "-ac", "2",
|
||||
"-af", _af_chain,
|
||||
"-flags", "+global_header",
|
||||
"-flvflags", "no_duration_filesize",
|
||||
"-f", "flv", youtube_rtmp
|
||||
]
|
||||
_vf_pad = (
|
||||
"fps=30,scale=720:1280:force_original_aspect_ratio=decrease:flags=bilinear,"
|
||||
"pad=720:1280:(ow-iw)/2:(oh-ih)/2:black"
|
||||
)
|
||||
_input_path = os.path.normpath(os.path.expanduser(real_url))
|
||||
_input_is_file = os.path.isfile(_input_path)
|
||||
if _input_is_file:
|
||||
ffmpeg_command = [
|
||||
"ffmpeg", "-y", "-loglevel", "info", "-nostdin",
|
||||
"-stats", "-stats_period", "1",
|
||||
"-fflags", "+genpts+discardcorrupt",
|
||||
"-err_detect", "ignore_err",
|
||||
"-thread_queue_size", "2048",
|
||||
"-filter_threads", _filter_threads,
|
||||
"-re",
|
||||
"-i", _input_path,
|
||||
"-vf", _vf_pad,
|
||||
"-c:v", codec_v,
|
||||
*video_args,
|
||||
*_video_mid,
|
||||
"-pix_fmt", "yuv420p",
|
||||
"-c:a", "aac", "-b:a", "128k", "-ar", "48000", "-ac", "2",
|
||||
"-af", _af_chain,
|
||||
"-flags", "+global_header",
|
||||
"-flvflags", "no_duration_filesize",
|
||||
"-f", "flv", youtube_rtmp,
|
||||
]
|
||||
else:
|
||||
ffmpeg_command = [
|
||||
"ffmpeg", "-y", "-loglevel", "info", "-nostdin",
|
||||
"-stats", "-stats_period", "1",
|
||||
"-rw_timeout", "50000000",
|
||||
"-reconnect", "1", "-reconnect_at_eof", "1", "-reconnect_streamed", "1",
|
||||
"-reconnect_delay_max", str(RECONNECT_DELAY_MAX),
|
||||
"-fflags", "+genpts+discardcorrupt",
|
||||
"-err_detect", "ignore_err",
|
||||
"-max_delay", "500000",
|
||||
"-thread_queue_size", "2048",
|
||||
"-filter_threads", _filter_threads,
|
||||
"-headers", douyin_headers,
|
||||
"-re",
|
||||
"-i", real_url,
|
||||
# bilinear 比 lanczos 轻很多,利于 speed 稳定在 ≥1.0x
|
||||
"-vf", _vf_pad,
|
||||
"-c:v", codec_v,
|
||||
*video_args,
|
||||
*_video_mid,
|
||||
"-pix_fmt", "yuv420p",
|
||||
"-c:a", "aac", "-b:a", "128k", "-ar", "48000", "-ac", "2",
|
||||
"-af", _af_chain,
|
||||
"-flags", "+global_header",
|
||||
"-flvflags", "no_duration_filesize",
|
||||
"-f", "flv", youtube_rtmp,
|
||||
]
|
||||
|
||||
_require_ffmpeg_stats_period()
|
||||
|
||||
proc = None
|
||||
stderr_q = None
|
||||
@@ -314,13 +433,20 @@ def start_douyin_youtube_ffplay(
|
||||
print(f"[FFmpeg] {line}")
|
||||
line_lower = line.lower()
|
||||
|
||||
if any(kw in line_lower for kw in END_KEYWORDS):
|
||||
print("[ERROR] 检测到主播真下播或被明确拒绝,停止推流")
|
||||
if any(kw in line_lower for kw in TERMINAL_KEYWORDS):
|
||||
print("[ERROR] 判定直播结束或平台明确拒绝,停止推流")
|
||||
cleanup_proc(proc)
|
||||
raise StreamEnded()
|
||||
|
||||
if any(kw in line_lower for kw in TRANSIENT_KEYWORDS):
|
||||
print(f"[WARN] 瞬时错误,将退避重试: {line[:160]}")
|
||||
cleanup_proc(proc)
|
||||
retry_delay = min(max(retry_delay, INITIAL_RETRY_DELAY) * 2, MAX_RETRY_DELAY)
|
||||
break
|
||||
|
||||
if re.search(r'frame=\s*\d+', line_lower) or "fps=" in line_lower:
|
||||
last_frame_time = time.time()
|
||||
retry_delay = INITIAL_RETRY_DELAY
|
||||
|
||||
if time.time() - start_time > START_CHECK_AFTER:
|
||||
if time.time() - last_frame_time > NO_FRAME_TIMEOUT:
|
||||
@@ -356,13 +482,19 @@ def start_douyin_youtube_ffplay(
|
||||
cleanup_proc(proc)
|
||||
|
||||
try:
|
||||
recording.discard(record_name)
|
||||
recording_time_list.pop(record_name, None)
|
||||
if real_url in running_list:
|
||||
running_list.remove(real_url)
|
||||
monitoring[0] = max(0, monitoring[0] - 1)
|
||||
# 统一由 clear_record_info 维护 running_list / monitoring / url_comments
|
||||
clear_record_info(record_name, real_url)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
color_obj.print_colored(f"[{record_name}] YouTube 推流已彻底停止并清理完毕\n", color_obj.GREEN)
|
||||
color_obj.print_colored(f"[{record_name}] YouTube 推流已彻底停止并清理完毕\n", color_obj.GREEN)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 兼容旧 PM2/脚本名:实际业务入口为 youtube.py(监控 URL_config + 调用本模块推流)
|
||||
import runpy
|
||||
from pathlib import Path
|
||||
|
||||
_root = Path(__file__).resolve().parent
|
||||
runpy.run_path(str(_root / "youtube.py"), run_name="__main__")
|
||||
@@ -1,6 +1,11 @@
|
||||
/**
|
||||
* PM2:在项目根执行 `pm2 start ecosystem.config.cjs` 后 `pm2 save`
|
||||
* web 使用 scripts/launch.py:若缺少 web-console/out 会自动 npm build 再起 uvicorn
|
||||
*
|
||||
* 单频道:通常只启一个 youtube.py(或 tiktok.py)+ web。
|
||||
* 多频道 Pro:同一项目根下按 PM2 名拆分文件:config/youtube.<name>.ini 与 config/URL_config.<name>.ini(控制台保存时自动创建)。
|
||||
* PM2 进程名须与控制台线路一致(如 youtube2__ch_xxx),并在 env 中设置 LIVE_PM2_PROCESS_NAME 与同名字符串,供 youtube.py 读取正确 ini。
|
||||
* 音频处理链与 GitLab 分支 ffmpegAudioRMFoode 中 douyin_youtube_ffplay 对齐;Linux ARM/x86 编码由 config/hardware.env 与硬件探测辅助。
|
||||
*/
|
||||
const path = require("path");
|
||||
const root = __dirname;
|
||||
@@ -18,9 +23,29 @@ module.exports = {
|
||||
PORT: process.env.PORT || "8001",
|
||||
},
|
||||
},
|
||||
// { name: "youtube", cwd: root, script: py, args: "youtube.py" },
|
||||
// { name: "tiktok", cwd: root, script: py, args: "tiktok.py" },
|
||||
// Linux: { name: "obs", cwd: root, script: "bash", args: `-lc ${JSON.stringify(path.join(root, "obs.sh"))}` },
|
||||
// Win: { name: "obs", cwd: root, script: "cmd.exe", args: "/c obs.bat" },
|
||||
// —— 单频道直播(Linux ARM/x86 通用,二选一或按需启用)——
|
||||
// {
|
||||
// name: "youtube",
|
||||
// cwd: root,
|
||||
// script: "youtube.py",
|
||||
// interpreter: py,
|
||||
// env: { PYTHONUNBUFFERED: "1" },
|
||||
// },
|
||||
// {
|
||||
// name: "tiktok",
|
||||
// cwd: root,
|
||||
// script: "tiktok.py",
|
||||
// interpreter: py,
|
||||
// env: { PYTHONUNBUFFERED: "1" },
|
||||
// },
|
||||
// —— Pro 第二路示例:与 web 控制台「多频道 Pro」线路 PM2 名一致 ——
|
||||
// {
|
||||
// name: "youtube2__lane2",
|
||||
// cwd: root,
|
||||
// script: "youtube.py",
|
||||
// interpreter: py,
|
||||
// env: { PYTHONUNBUFFERED: "1", LIVE_PM2_PROCESS_NAME: "youtube2__lane2" },
|
||||
// },
|
||||
// { name: "obs", cwd: root, script: "bash", args: `-lc ${JSON.stringify(path.join(root, "obs.sh"))}` },
|
||||
],
|
||||
};
|
||||
|
||||
@@ -6,112 +6,18 @@ GitHub: https://github.com/ihmily
|
||||
Copyright (c) 2024 by Hmily, All Rights Reserved.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import platform
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
import requests
|
||||
from tqdm import tqdm
|
||||
from src.logger import logger
|
||||
|
||||
current_platform = platform.system()
|
||||
execute_dir = os.path.split(os.path.realpath(sys.argv[0]))[0]
|
||||
current_env_path = os.environ.get('PATH')
|
||||
ffmpeg_path = os.path.join(execute_dir, 'ffmpeg')
|
||||
|
||||
|
||||
def unzip_file(zip_path: str | Path, extract_to: str | Path, delete: bool = True) -> None:
|
||||
if not os.path.exists(extract_to):
|
||||
os.makedirs(extract_to)
|
||||
|
||||
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
|
||||
zip_ref.extractall(extract_to)
|
||||
|
||||
if delete and os.path.exists(zip_path):
|
||||
os.remove(zip_path)
|
||||
|
||||
|
||||
def get_lanzou_download_link(url: str, password: str | None = None) -> str | None:
|
||||
try:
|
||||
headers = {
|
||||
'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6',
|
||||
'Origin': 'https://wweb.lanzouv.com',
|
||||
'Referer': 'https://wweb.lanzouv.com/iXncv0dly6mh',
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) '
|
||||
'Chrome/121.0.0.0 Safari/537.36 Edg/121.0.0.0',
|
||||
}
|
||||
response = requests.get(url, headers=headers)
|
||||
sign = re.search("var skdklds = '(.*?)';", response.text).group(1)
|
||||
data = {
|
||||
'action': 'downprocess',
|
||||
'sign': sign,
|
||||
'p': password,
|
||||
'kd': '1',
|
||||
}
|
||||
response = requests.post('https://wweb.lanzouv.com/ajaxm.php', headers=headers, data=data)
|
||||
json_data = response.json()
|
||||
download_url = json_data['dom'] + "/file/" + json_data['url']
|
||||
response = requests.get(download_url, headers=headers)
|
||||
return response.url
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to obtain ffmpeg download address. {e}")
|
||||
|
||||
|
||||
def install_ffmpeg_windows():
|
||||
try:
|
||||
logger.warning("ffmpeg is not installed.")
|
||||
logger.debug("Installing the latest version of ffmpeg for Windows...")
|
||||
ffmpeg_url = get_lanzou_download_link('https://wweb.lanzouv.com/iHAc22ly3r3g', 'eots')
|
||||
if ffmpeg_url:
|
||||
full_file_name = 'ffmpeg_latest_build_20250124.zip'
|
||||
version = 'v20250124'
|
||||
zip_file_path = Path(execute_dir) / full_file_name
|
||||
if Path(zip_file_path).exists():
|
||||
logger.debug("ffmpeg installation file already exists, start install...")
|
||||
else:
|
||||
response = requests.get(ffmpeg_url, stream=True)
|
||||
total_size = int(response.headers.get('Content-Length', 0))
|
||||
block_size = 1024
|
||||
|
||||
with tqdm(total=total_size, unit="B", unit_scale=True,
|
||||
ncols=100, desc=f'Downloading ffmpeg ({version})') as t:
|
||||
with open(zip_file_path, 'wb') as f:
|
||||
for data in response.iter_content(block_size):
|
||||
t.update(len(data))
|
||||
f.write(data)
|
||||
|
||||
unzip_file(zip_file_path, execute_dir)
|
||||
os.environ['PATH'] = ffmpeg_path + os.pathsep + current_env_path
|
||||
result = subprocess.run(["ffmpeg", "-version"], capture_output=True)
|
||||
if result.returncode == 0:
|
||||
logger.debug('ffmpeg installation was successful')
|
||||
else:
|
||||
logger.error('ffmpeg installation failed. Please manually install ffmpeg by yourself')
|
||||
return True
|
||||
else:
|
||||
logger.error("Please manually install ffmpeg by yourself")
|
||||
except Exception as e:
|
||||
logger.error(f"type: {type(e).__name__}, ffmpeg installation failed {e}")
|
||||
|
||||
|
||||
def install_ffmpeg_mac():
|
||||
logger.warning("ffmpeg is not installed.")
|
||||
logger.debug("Installing the stable version of ffmpeg for macOS...")
|
||||
try:
|
||||
result = subprocess.run(["brew", "install", "ffmpeg"], capture_output=True)
|
||||
if result.returncode == 0:
|
||||
logger.debug('ffmpeg installation was successful. Restart for changes to take effect.')
|
||||
return True
|
||||
else:
|
||||
logger.error("ffmpeg installation failed")
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.error(f"Failed to install ffmpeg using Homebrew. {e}")
|
||||
logger.error("Please install ffmpeg manually or check your Homebrew installation.")
|
||||
except Exception as e:
|
||||
logger.error(f"An unexpected error occurred: {e}")
|
||||
current_env_path = os.environ.get("PATH") or ""
|
||||
ffmpeg_path = os.path.join(execute_dir, "ffmpeg")
|
||||
|
||||
|
||||
def install_ffmpeg_linux():
|
||||
@@ -159,15 +65,9 @@ def install_ffmpeg_linux():
|
||||
|
||||
|
||||
def install_ffmpeg() -> bool:
|
||||
if current_platform == "Windows":
|
||||
return install_ffmpeg_windows()
|
||||
elif current_platform == "Linux":
|
||||
if current_platform == "Linux":
|
||||
return install_ffmpeg_linux()
|
||||
elif current_platform == "Darwin":
|
||||
return install_ffmpeg_mac()
|
||||
else:
|
||||
logger.debug(f"ffmpeg auto installation is not supported on this platform: {current_platform}. "
|
||||
f"Please install ffmpeg manually.")
|
||||
logger.debug("ffmpeg 自动安装仅支持 Linux,请使用系统包管理器安装 ffmpeg。")
|
||||
return False
|
||||
|
||||
|
||||
|
||||
4
install-all.sh
Normal file
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
ROOT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
exec bash "$ROOT_DIR/scripts/linux/install_stack.sh" all "$@"
|
||||
4
install-business.sh
Normal file
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
ROOT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
exec bash "$ROOT_DIR/scripts/linux/install_business.sh" "$@"
|
||||
4
install-core.sh
Normal file
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
ROOT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
exec bash "$ROOT_DIR/live-platform/scripts/install-core.sh" "$@"
|
||||
4
install-hub.sh
Normal file
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
ROOT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
exec bash "$ROOT_DIR/scripts/linux/install_hub.sh" "$@"
|
||||
4
install-live.sh
Normal file
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
ROOT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
exec bash "$ROOT_DIR/live-platform/scripts/install-live.sh" "$@"
|
||||
42
install-oneclick.sh
Normal file
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env bash
|
||||
# 一键安装入口:Ubuntu/Debian 系 + 常见 ARM/x86_64,再调用现有 install-all(hub + business)
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
log() {
|
||||
printf '[oneclick] %s\n' "$*"
|
||||
}
|
||||
|
||||
if [ ! -f /etc/os-release ]; then
|
||||
log "未找到 /etc/os-release,仅支持 Debian/Ubuntu 系。"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# shellcheck source=/dev/null
|
||||
. /etc/os-release
|
||||
|
||||
ID_LIKE="${ID_LIKE:-}"
|
||||
if ! echo "${ID:-} ${ID_LIKE}" | grep -qiE 'debian|ubuntu'; then
|
||||
log "当前发行版: ${PRETTY_NAME:-unknown}"
|
||||
log "脚本仅在 Debian/Ubuntu 及其衍生版上测试;如需其它系统请直接阅读 scripts/linux/*.sh 手工执行。"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ARCH="$(uname -m 2>/dev/null || true)"
|
||||
case "${ARCH}" in
|
||||
x86_64 | amd64 | aarch64 | arm64 | armv7l | armv6l) ;;
|
||||
*)
|
||||
log "未识别的机器架构: ${ARCH:-?},将继续尝试安装(请自行确认依赖是否可用)。"
|
||||
;;
|
||||
esac
|
||||
|
||||
log "ROOT_DIR=${ROOT_DIR}"
|
||||
log "架构=${ARCH} 系统=${PRETTY_NAME:-$ID}"
|
||||
|
||||
if [ ! -x "${ROOT_DIR}/install-all.sh" ]; then
|
||||
log "缺少可执行的 install-all.sh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exec bash "${ROOT_DIR}/install-all.sh" "$@"
|
||||
19
live-platform/config/defaults/business/douyinyoutube.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"id": "douyinyoutube",
|
||||
"name": "无人直播(抖音/TikTok → YouTube / SRS / HDMI)",
|
||||
"enabled": true,
|
||||
"layer": "business",
|
||||
"install_marker": "/opt/live/business/douyinyoutube/.installed",
|
||||
"ffmpeg": {
|
||||
"template_id": "default",
|
||||
"auto_restart": true,
|
||||
"retry_seconds": 15
|
||||
},
|
||||
"streams": {
|
||||
"douyin_input_url": "",
|
||||
"youtube_rtmp_key": "",
|
||||
"srs_publish_url": "",
|
||||
"hdmi_device": ""
|
||||
},
|
||||
"note": "具体推流参数仍可由 PM2 脚本与 config/*.ini 管理;此处为声明式元数据,供控制台与恢复策略使用"
|
||||
}
|
||||
16
live-platform/config/defaults/network.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"version": 1,
|
||||
"wifi": {
|
||||
"enabled": true,
|
||||
"connection_name": "live-wifi",
|
||||
"ssid": "live",
|
||||
"psk": "12345678"
|
||||
},
|
||||
"ipv6": {
|
||||
"disable": true
|
||||
},
|
||||
"android_proxy": {
|
||||
"mode": "gateway_optional",
|
||||
"description": "可选透明网关或 ShellCrash 规则;具体由 ShellCrash 与 network 脚本实现"
|
||||
}
|
||||
}
|
||||
12
live-platform/config/defaults/services/android_panel.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"id": "android_panel",
|
||||
"layer": "infra",
|
||||
"enabled": true,
|
||||
"manager": "systemd",
|
||||
"ports": { "http": 5000 },
|
||||
"env": {
|
||||
"ENABLE_ANDROID_PANEL": "1",
|
||||
"ANDROID_PANEL_PORT": "5000",
|
||||
"ANDROID_PANEL_DIR": "/opt/live-modules/web-scrcpy"
|
||||
}
|
||||
}
|
||||
9
live-platform/config/defaults/services/caddy.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"id": "caddy",
|
||||
"layer": "infra",
|
||||
"enabled": false,
|
||||
"manager": "systemd",
|
||||
"description": "可选:统一 live.local:80 入口,反代控制台静态 + /api → FastAPI",
|
||||
"ports": { "http": 80, "https": 443 },
|
||||
"template": "/opt/live/templates/Caddyfile"
|
||||
}
|
||||
13
live-platform/config/defaults/services/filebrowser.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"id": "filebrowser",
|
||||
"layer": "infra",
|
||||
"enabled": true,
|
||||
"manager": "docker",
|
||||
"ports": { "http": 8082 },
|
||||
"env": {
|
||||
"ENABLE_FILEBROWSER": "1",
|
||||
"FILEBROWSER_PORT": "8082",
|
||||
"FILEBROWSER_USER": "live",
|
||||
"FILEBROWSER_PASS": "12345678"
|
||||
}
|
||||
}
|
||||
8
live-platform/config/defaults/services/frp.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"id": "frp",
|
||||
"layer": "infra",
|
||||
"enabled": false,
|
||||
"manager": "systemd",
|
||||
"description": "占位:内网穿透,由运维填入 frpc 配置后启用",
|
||||
"ports": {}
|
||||
}
|
||||
8
live-platform/config/defaults/services/homepage.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"id": "homepage",
|
||||
"layer": "infra",
|
||||
"enabled": true,
|
||||
"manager": "docker",
|
||||
"ports": { "http": 3080 },
|
||||
"env": { "ENABLE_HOMEPAGE": "1", "HOMEPAGE_PORT": "3080" }
|
||||
}
|
||||
12
live-platform/config/defaults/services/neko.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"id": "neko",
|
||||
"layer": "infra",
|
||||
"enabled": true,
|
||||
"manager": "systemd",
|
||||
"ports": { "http": 9200 },
|
||||
"env": {
|
||||
"ENABLE_NEKO": "1",
|
||||
"NEKO_HTTP_PORT": "9200",
|
||||
"NEKO_PROFILE_HOST_DIR": "/opt/live-modules/neko-profile"
|
||||
}
|
||||
}
|
||||
8
live-platform/config/defaults/services/netdata.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"id": "netdata",
|
||||
"layer": "infra",
|
||||
"enabled": true,
|
||||
"manager": "native",
|
||||
"ports": { "http": 19999 },
|
||||
"env": { "NETDATA_PORT": "19999", "ENABLE_NETDATA": "1" }
|
||||
}
|
||||
11
live-platform/config/defaults/services/shellcrash.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"id": "shellcrash",
|
||||
"layer": "infra",
|
||||
"enabled": true,
|
||||
"manager": "systemd",
|
||||
"ports": {},
|
||||
"env": {
|
||||
"SHELLCRASH_DIR": "/etc/ShellCrash",
|
||||
"ENABLE_SHELLCRASH": "1"
|
||||
}
|
||||
}
|
||||
16
live-platform/config/defaults/services/srs.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"id": "srs",
|
||||
"layer": "infra",
|
||||
"enabled": true,
|
||||
"manager": "docker",
|
||||
"ports": {
|
||||
"http": 8080,
|
||||
"rtmp": 1935,
|
||||
"api": 1985
|
||||
},
|
||||
"env": {
|
||||
"SRS_HTTP_PORT": "8080",
|
||||
"SRS_RTMP_PORT": "1935",
|
||||
"SRS_API_PORT": "1985"
|
||||
}
|
||||
}
|
||||
13
live-platform/config/defaults/services/webtty.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"id": "webtty",
|
||||
"layer": "core",
|
||||
"enabled": true,
|
||||
"manager": "systemd",
|
||||
"ports": { "http": 7681 },
|
||||
"env": {
|
||||
"ENABLE_WEBTTY": "1",
|
||||
"WEBTTY_PORT": "7681",
|
||||
"WEBTTY_USER": "live",
|
||||
"WEBTTY_PASS": "12345678"
|
||||
}
|
||||
}
|
||||
8
live-platform/config/defaults/services/wireguard.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"id": "wireguard",
|
||||
"layer": "infra",
|
||||
"enabled": false,
|
||||
"manager": "systemd",
|
||||
"description": "占位:VPN 接口,与 ShellCrash 并存时需规划路由",
|
||||
"ports": {}
|
||||
}
|
||||
21
live-platform/config/defaults/system.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"version": 1,
|
||||
"platform": {
|
||||
"arch": "auto",
|
||||
"description": "auto = detect via uname -m (aarch64/arm64/x86_64)"
|
||||
},
|
||||
"identity": {
|
||||
"hostname_alias": "live",
|
||||
"mdns_enabled": true
|
||||
},
|
||||
"paths": {
|
||||
"state_dir": "/opt/live/state",
|
||||
"logs_dir": "/opt/live/logs",
|
||||
"generated_dir": "/opt/live/generated"
|
||||
},
|
||||
"web_console": {
|
||||
"api_port": 8001,
|
||||
"public_host": "live.local",
|
||||
"note": "生产环境由 Caddy :80 反代到静态资源与 API"
|
||||
}
|
||||
}
|
||||
13
live-platform/config/templates/Caddyfile.sample
Normal file
@@ -0,0 +1,13 @@
|
||||
# 将本文件复制到 /opt/live/templates/Caddyfile 并按实际路径修改
|
||||
# 目标:http://live.local → 静态控制台 + /api/* → FastAPI
|
||||
|
||||
{$LIVE_DOMAIN:live.local} {
|
||||
encode gzip
|
||||
# 静态(Next export 由 web.py 同端口托管时可改为 reverse_proxy)
|
||||
handle_path /api/* {
|
||||
reverse_proxy 127.0.0.1:8001
|
||||
}
|
||||
handle {
|
||||
reverse_proxy 127.0.0.1:8001
|
||||
}
|
||||
}
|
||||
68
live-platform/scripts/doctor.sh
Normal file
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env bash
|
||||
# 一键体检:网络 / 端口 / adb / ffmpeg / docker / 服务状态(模块化输出)
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
REPO="$(cd "$ROOT/.." && pwd)"
|
||||
# shellcheck source=/dev/null
|
||||
source "$ROOT/scripts/lib/common.sh"
|
||||
|
||||
echo "=== Live Platform Doctor ==="
|
||||
echo "arch: $(detect_arch)"
|
||||
echo "hostname: $(hostname -f 2>/dev/null || hostname)"
|
||||
|
||||
section() { echo ""; echo "--- $1 ---"; }
|
||||
|
||||
section "loopback / DNS"
|
||||
ping -c1 -W2 127.0.0.1 >/dev/null 2>&1 && echo "ping 127.0.0.1: ok" || echo "ping 127.0.0.1: fail"
|
||||
getent hosts live.local >/dev/null 2>&1 && echo "live.local resolves: yes" || echo "live.local resolves: (optional mdns)"
|
||||
|
||||
section "config"
|
||||
if [[ -f /opt/live/config/system.json ]]; then
|
||||
echo "system.json: present"
|
||||
else
|
||||
echo "system.json: MISSING (run install-core)"
|
||||
fi
|
||||
if [[ -f /opt/live/generated/system-stack.env ]]; then
|
||||
echo "generated env: present"
|
||||
else
|
||||
echo "generated env: missing"
|
||||
fi
|
||||
|
||||
section "ports (LISTEN)"
|
||||
command -v ss >/dev/null 2>&1 && ss -tlnp 2>/dev/null | head -n 40 || netstat -tlnp 2>/dev/null | head -n 40 || true
|
||||
|
||||
section "adb"
|
||||
if command -v adb >/dev/null 2>&1; then
|
||||
adb devices -l || true
|
||||
else
|
||||
echo "adb not installed"
|
||||
fi
|
||||
|
||||
section "ffmpeg"
|
||||
command -v ffmpeg >/dev/null 2>&1 && ffmpeg -version | head -n1 || echo "ffmpeg missing"
|
||||
|
||||
section "docker"
|
||||
if command -v docker >/dev/null 2>&1; then
|
||||
docker info >/dev/null 2>&1 && echo "docker: daemon ok" || echo "docker: daemon unreachable"
|
||||
else
|
||||
echo "docker not installed"
|
||||
fi
|
||||
|
||||
section "proxy hint (ShellCrash)"
|
||||
if [[ -d /etc/ShellCrash ]]; then
|
||||
echo "ShellCrash dir: /etc/ShellCrash"
|
||||
else
|
||||
echo "ShellCrash dir: not found"
|
||||
fi
|
||||
|
||||
if [[ -f "$REPO/web.py" ]] && command -v curl >/dev/null 2>&1; then
|
||||
section "API"
|
||||
PORT="${PORT:-8001}"
|
||||
curl -sS -m 3 "http://127.0.0.1:${PORT}/health" && echo "" || echo "health check failed on :$PORT"
|
||||
section "Port 80 (live-edge / Caddy)"
|
||||
systemctl is-active live-edge.service 2>/dev/null || echo "live-edge.service: inactive"
|
||||
curl -sS -m 5 -o /dev/null -w "GET http://127.0.0.1/ → http_code=%{http_code}\n" "http://127.0.0.1/" || echo "curl :80 failed"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== Doctor finished ==="
|
||||
42
live-platform/scripts/install-core.sh
Normal file
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env bash
|
||||
# 幂等:同步配置中心默认 JSON → /opt/live/config,生成 system-stack.env,安装 Hub(无业务模块)
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
REPO="$(cd "$ROOT/.." && pwd)"
|
||||
# shellcheck source=/dev/null
|
||||
source "$ROOT/scripts/lib/common.sh"
|
||||
|
||||
ARCH="$(detect_arch)"
|
||||
echo "[live-platform] install-core arch=$ARCH"
|
||||
|
||||
ensure_live_dirs
|
||||
|
||||
if [[ ! -f /opt/live/config/system.json ]]; then
|
||||
cp -a "$ROOT/config/defaults/system.json" /opt/live/config/system.json
|
||||
fi
|
||||
if [[ ! -f /opt/live/config/network.json ]]; then
|
||||
cp -a "$ROOT/config/defaults/network.json" /opt/live/config/network.json
|
||||
fi
|
||||
if command -v rsync >/dev/null 2>&1; then
|
||||
rsync -a --ignore-existing "$ROOT/config/defaults/services/" /opt/live/config/services/
|
||||
rsync -a --ignore-existing "$ROOT/config/defaults/business/" /opt/live/config/business/
|
||||
else
|
||||
cp -n "$ROOT/config/defaults/services/"*.json /opt/live/config/services/ 2>/dev/null || true
|
||||
cp -n "$ROOT/config/defaults/business/"*.json /opt/live/config/business/ 2>/dev/null || true
|
||||
fi
|
||||
|
||||
export LIVE_CONFIG_ROOT="${LIVE_CONFIG_ROOT:-/opt/live/config}"
|
||||
python3 "$ROOT/tools/render_stack_env.py" /opt/live/generated/system-stack.env
|
||||
|
||||
if [[ -n "${LINK_REPO_CONFIG:-}" ]]; then
|
||||
mkdir -p "$REPO/config"
|
||||
ln -sf /opt/live/generated/system-stack.env "$REPO/config/system-stack.env"
|
||||
fi
|
||||
|
||||
if [[ -x "$REPO/scripts/linux/install_hub.sh" ]]; then
|
||||
bash "$REPO/scripts/linux/install_hub.sh" "$@"
|
||||
else
|
||||
echo "[live-platform] WARN: scripts/linux/install_hub.sh missing, skip hub packages"
|
||||
fi
|
||||
|
||||
echo "[live-platform] install-core done. Env: /opt/live/generated/system-stack.env"
|
||||
16
live-platform/scripts/install-live.sh
Normal file
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env bash
|
||||
# 幂等:仅无人直播业务模块(与 install-core 解耦)
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
REPO="$(cd "$ROOT/.." && pwd)"
|
||||
|
||||
if [[ -x "$REPO/scripts/linux/install_business.sh" ]]; then
|
||||
bash "$REPO/scripts/linux/install_business.sh" "$@"
|
||||
else
|
||||
echo "[live-platform] ERROR: scripts/linux/install_business.sh not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p /opt/live/business/douyinyoutube
|
||||
touch /opt/live/business/douyinyoutube/.installed
|
||||
echo "[live-platform] install-live done"
|
||||
29
live-platform/scripts/lib/common.sh
Normal file
@@ -0,0 +1,29 @@
|
||||
# shellcheck shell=bash
|
||||
# 被 install-core / install-live / doctor 引用;禁止写业务逻辑,仅路径与权限。
|
||||
|
||||
live_platform_root() {
|
||||
local here
|
||||
here="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
printf '%s\n' "$here"
|
||||
}
|
||||
|
||||
repo_root() {
|
||||
local lp
|
||||
lp="$(live_platform_root)"
|
||||
cd "$lp/.." && pwd
|
||||
}
|
||||
|
||||
detect_arch() {
|
||||
case "$(uname -m)" in
|
||||
aarch64|arm64) echo "arm64" ;;
|
||||
x86_64|amd64) echo "amd64" ;;
|
||||
*) echo "generic" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
ensure_live_dirs() {
|
||||
sudo mkdir -p /opt/live/config/services /opt/live/config/business \
|
||||
/opt/live/generated /opt/live/state /opt/live/logs /opt/live/templates 2>/dev/null || \
|
||||
mkdir -p /opt/live/config/services /opt/live/config/business \
|
||||
/opt/live/generated /opt/live/state /opt/live/logs /opt/live/templates
|
||||
}
|
||||
46
live-platform/tools/render_caddyfile.py
Normal file
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env python3
|
||||
"""生成 Caddy 配置:HTTP :80 → 本机 FastAPI+静态(PORT,默认 8001)。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def main() -> int:
|
||||
alias = "live"
|
||||
port = "8001"
|
||||
out = Path("/opt/live/generated/Caddyfile")
|
||||
|
||||
cfg = Path("/opt/live/config/system.json")
|
||||
if cfg.is_file():
|
||||
data = json.loads(cfg.read_text(encoding="utf-8"))
|
||||
ident = data.get("identity") or {}
|
||||
web = data.get("web_console") or {}
|
||||
alias = str(ident.get("hostname_alias") or alias)
|
||||
if web.get("api_port") is not None:
|
||||
port = str(web["api_port"])
|
||||
|
||||
if len(sys.argv) >= 2:
|
||||
alias = sys.argv[1]
|
||||
if len(sys.argv) >= 3:
|
||||
port = sys.argv[2]
|
||||
if len(sys.argv) >= 4:
|
||||
out = Path(sys.argv[3])
|
||||
|
||||
host = f"{alias}.local"
|
||||
body = f"""# Generated by live-platform/tools/render_caddyfile.py - do not hand-edit
|
||||
# http://{host}/ and http://<LAN-IP>/ -> 127.0.0.1:{port}
|
||||
|
||||
{host}:80, :80 {{
|
||||
\treverse_proxy 127.0.0.1:{port}
|
||||
}}
|
||||
"""
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
out.write_text(body, encoding="utf-8")
|
||||
print(str(out))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
97
live-platform/tools/render_stack_env.py
Normal file
@@ -0,0 +1,97 @@
|
||||
#!/usr/bin/env python3
|
||||
"""从 /opt/live/config(或 LIVE_CONFIG_ROOT)JSON 配置生成 system-stack.env,供现有 service_ctl / install 脚本读取。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def config_root() -> Path:
|
||||
env = os.environ.get("LIVE_CONFIG_ROOT", "").strip()
|
||||
if env:
|
||||
return Path(env)
|
||||
if Path("/opt/live/config/system.json").is_file():
|
||||
return Path("/opt/live/config")
|
||||
here = Path(__file__).resolve()
|
||||
return here.parent.parent / "config" / "defaults"
|
||||
|
||||
|
||||
def load_json(path: Path) -> dict:
|
||||
if not path.is_file():
|
||||
return {}
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def collect_env(root: Path) -> dict[str, str]:
|
||||
lines: dict[str, str] = {}
|
||||
|
||||
sys_j = load_json(root / "system.json")
|
||||
web = sys_j.get("web_console") or {}
|
||||
ident = sys_j.get("identity") or {}
|
||||
if ident.get("hostname_alias"):
|
||||
lines["HOSTNAME_ALIAS"] = str(ident["hostname_alias"])
|
||||
if web.get("api_port") is not None:
|
||||
lines["PORT"] = str(web["api_port"])
|
||||
|
||||
net = load_json(root / "network.json")
|
||||
wifi = net.get("wifi") or {}
|
||||
if wifi.get("connection_name"):
|
||||
lines["WIFI_CONNECTION_NAME"] = str(wifi["connection_name"])
|
||||
if wifi.get("ssid"):
|
||||
lines["WIFI_SSID"] = str(wifi["ssid"])
|
||||
if wifi.get("psk"):
|
||||
lines["WIFI_PSK"] = str(wifi["psk"])
|
||||
if net.get("ipv6", {}).get("disable"):
|
||||
lines["DISABLE_IPV6"] = "1"
|
||||
|
||||
svc_dir = root / "services"
|
||||
if svc_dir.is_dir():
|
||||
for f in sorted(svc_dir.glob("*.json")):
|
||||
data = load_json(f)
|
||||
env = data.get("env") or {}
|
||||
if not isinstance(env, dict):
|
||||
continue
|
||||
for k, v in env.items():
|
||||
if v is not None and str(v) != "":
|
||||
lines[str(k)] = str(v)
|
||||
|
||||
ports = {}
|
||||
if svc_dir.is_dir():
|
||||
for f in sorted(svc_dir.glob("*.json")):
|
||||
data = load_json(f)
|
||||
pid = data.get("id") or f.stem
|
||||
pr = data.get("ports") or {}
|
||||
if isinstance(pr, dict) and "http" in pr:
|
||||
ports[pid] = pr["http"]
|
||||
# 固定映射:homepage 使用 HOMEPAGE_PORT
|
||||
if "homepage" in ports:
|
||||
lines.setdefault("HOMEPAGE_PORT", str(ports["homepage"]))
|
||||
|
||||
lines.setdefault("ENABLE_MDNS", "1")
|
||||
lines.setdefault("ENABLE_WIFI_PROFILE", "1")
|
||||
lines.setdefault("ENABLE_BROWSER", "1")
|
||||
lines.setdefault("ENABLE_COCKPIT", "1")
|
||||
return lines
|
||||
|
||||
|
||||
def main() -> int:
|
||||
root = config_root()
|
||||
out = Path(os.environ.get("LIVE_ENV_OUT", "/opt/live/generated/system-stack.env"))
|
||||
if len(sys.argv) > 1:
|
||||
out = Path(sys.argv[1])
|
||||
lines = collect_env(root)
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
body = "\n".join(f"{k}={v}" for k, v in sorted(lines.items())) + "\n"
|
||||
out.write_text(
|
||||
"# Generated by live-platform/tools/render_stack_env.py — do not hand-edit\n" + body,
|
||||
encoding="utf-8",
|
||||
)
|
||||
# 开发树:可选同步到仓库 config(由 install 脚本 ln -sf)
|
||||
print(str(out))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
246
main.py
@@ -8,8 +8,13 @@ Update: 2025-10-23 19:48:05
|
||||
Copyright (c) 2023-2025 by Hmily, All Rights Reserved.
|
||||
Function: Record live stream video.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
from src.async_thread_loop import run_async_coro
|
||||
import sys
|
||||
import builtins
|
||||
import subprocess
|
||||
@@ -78,8 +83,7 @@ rstr = r"[\/\\\:\*\??\"\<\>\|&#.。,, ~!· ]"
|
||||
default_path = f'{script_path}/downloads'
|
||||
os.makedirs(default_path, exist_ok=True)
|
||||
file_update_lock = threading.Lock()
|
||||
os_type = os.name
|
||||
clear_command = "cls" if os_type == 'nt' else "clear"
|
||||
clear_command = "clear"
|
||||
color_obj = utils.Color()
|
||||
os.environ['PATH'] = ffmpeg_path + os.pathsep + current_env_path
|
||||
|
||||
@@ -91,10 +95,6 @@ def signal_handler(_signal, _frame):
|
||||
signal.signal(signal.SIGTERM, signal_handler)
|
||||
|
||||
|
||||
|
||||
import shutil
|
||||
|
||||
|
||||
# 全局变量(只初始化一次)
|
||||
_last_lines_count = 0
|
||||
|
||||
@@ -183,8 +183,7 @@ def display_info2() -> None:
|
||||
try:
|
||||
sys.stdout.flush()
|
||||
time.sleep(5)
|
||||
if Path(sys.executable).name != 'pythonw.exe':
|
||||
os.system(clear_command)
|
||||
os.system(clear_command)
|
||||
print(f"\r共监测{monitoring}个直播中", end=" | ")
|
||||
print(f"同一时间访问网络的线程数: {max_request}", end=" | ")
|
||||
print(f"是否开启代理录制: {'是' if use_proxy else '否'}", end=" | ")
|
||||
@@ -266,15 +265,6 @@ def delete_line(file_path: str, del_line: str, delete_all: bool = False) -> None
|
||||
f.write(txt_line)
|
||||
|
||||
|
||||
def get_startup_info(system_type: str):
|
||||
if system_type == 'nt':
|
||||
startup_info = subprocess.STARTUPINFO()
|
||||
startup_info.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
||||
else:
|
||||
startup_info = None
|
||||
return startup_info
|
||||
|
||||
|
||||
def segment_video(converts_file_path: str, segment_save_file_path: str, segment_format: str, segment_time: str,
|
||||
is_original_delete: bool = True) -> None:
|
||||
try:
|
||||
@@ -293,7 +283,7 @@ def segment_video(converts_file_path: str, segment_save_file_path: str, segment_
|
||||
segment_save_file_path,
|
||||
]
|
||||
_output = subprocess.check_output(
|
||||
ffmpeg_command, stderr=subprocess.STDOUT, startupinfo=get_startup_info(os_type)
|
||||
ffmpeg_command, stderr=subprocess.STDOUT
|
||||
)
|
||||
if is_original_delete:
|
||||
time.sleep(1)
|
||||
@@ -328,7 +318,7 @@ def converts_mp4(converts_file_path: str, is_original_delete: bool = True) -> No
|
||||
"-f", "mp4", converts_file_path.rsplit('.', maxsplit=1)[0] + ".mp4",
|
||||
]
|
||||
_output = subprocess.check_output(
|
||||
ffmpeg_command, stderr=subprocess.STDOUT, startupinfo=get_startup_info(os_type)
|
||||
ffmpeg_command, stderr=subprocess.STDOUT
|
||||
)
|
||||
if is_original_delete:
|
||||
time.sleep(1)
|
||||
@@ -348,7 +338,7 @@ def converts_m4a(converts_file_path: str, is_original_delete: bool = True) -> No
|
||||
"-n", "-vn",
|
||||
"-c:a", "aac", "-bsf:a", "aac_adtstoasc", "-ab", "320k",
|
||||
converts_file_path.rsplit('.', maxsplit=1)[0] + ".m4a",
|
||||
], stderr=subprocess.STDOUT, startupinfo=get_startup_info(os_type))
|
||||
], stderr=subprocess.STDOUT)
|
||||
if is_original_delete:
|
||||
time.sleep(1)
|
||||
if os.path.exists(converts_file_path):
|
||||
@@ -445,7 +435,7 @@ def push_message(record_name: str, live_url: str, content: str) -> None:
|
||||
def run_script(command: str) -> None:
|
||||
try:
|
||||
process = subprocess.Popen(
|
||||
command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, startupinfo=get_startup_info(os_type)
|
||||
command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
||||
)
|
||||
stdout, stderr = process.communicate()
|
||||
stdout_decoded = stdout.decode('utf-8')
|
||||
@@ -510,7 +500,7 @@ def check_subprocess(record_name: str, record_url: str, ffmpeg_command: list, sa
|
||||
script_command: str | None = None) -> bool:
|
||||
save_file_path = ffmpeg_command[-1]
|
||||
process = subprocess.Popen(
|
||||
ffmpeg_command, stdin=subprocess.PIPE, stderr=subprocess.STDOUT, startupinfo=get_startup_info(os_type)
|
||||
ffmpeg_command, stdin=subprocess.PIPE, stderr=subprocess.STDOUT
|
||||
)
|
||||
|
||||
subs_file_path = save_file_path.rsplit('.', maxsplit=1)[0]
|
||||
@@ -527,12 +517,7 @@ def check_subprocess(record_name: str, record_url: str, ffmpeg_command: list, sa
|
||||
color_obj.print_colored(f"[{record_name}]录制时已被注释,本条线程将会退出", color_obj.YELLOW)
|
||||
clear_record_info(record_name, record_url)
|
||||
# process.terminate()
|
||||
if os.name == 'nt':
|
||||
if process.stdin:
|
||||
process.stdin.write(b'q')
|
||||
process.stdin.close()
|
||||
else:
|
||||
process.send_signal(signal.SIGINT)
|
||||
process.send_signal(signal.SIGINT)
|
||||
process.wait()
|
||||
return True
|
||||
time.sleep(1)
|
||||
@@ -670,27 +655,27 @@ def start_record(url_data: tuple, count_variable: int = -1) -> None:
|
||||
platform = '抖音直播'
|
||||
with semaphore:
|
||||
if 'v.douyin.com' not in record_url and '/user/' not in record_url:
|
||||
json_data = asyncio.run(spider.get_douyin_web_stream_data(
|
||||
json_data = run_async_coro(spider.get_douyin_web_stream_data(
|
||||
url=record_url,
|
||||
proxy_addr=proxy_address,
|
||||
cookies=dy_cookie))
|
||||
else:
|
||||
json_data = asyncio.run(spider.get_douyin_app_stream_data(
|
||||
json_data = run_async_coro(spider.get_douyin_app_stream_data(
|
||||
url=record_url,
|
||||
proxy_addr=proxy_address,
|
||||
cookies=dy_cookie))
|
||||
port_info = asyncio.run(
|
||||
port_info = run_async_coro(
|
||||
stream.get_douyin_stream_url(json_data, record_quality, proxy_address))
|
||||
|
||||
elif record_url.find("https://www.tiktok.com/") > -1:
|
||||
platform = 'TikTok直播'
|
||||
with semaphore:
|
||||
if global_proxy or proxy_address:
|
||||
json_data = asyncio.run(spider.get_tiktok_stream_data(
|
||||
json_data = run_async_coro(spider.get_tiktok_stream_data(
|
||||
url=record_url,
|
||||
proxy_addr=proxy_address,
|
||||
cookies=tiktok_cookie))
|
||||
port_info = asyncio.run(
|
||||
port_info = run_async_coro(
|
||||
stream.get_tiktok_stream_url(json_data, record_quality, proxy_address))
|
||||
else:
|
||||
logger.error("错误信息: 网络异常,请检查网络是否能正常访问TikTok平台")
|
||||
@@ -698,23 +683,23 @@ def start_record(url_data: tuple, count_variable: int = -1) -> None:
|
||||
elif record_url.find("https://live.kuaishou.com/") > -1:
|
||||
platform = '快手直播'
|
||||
with semaphore:
|
||||
json_data = asyncio.run(spider.get_kuaishou_stream_data(
|
||||
json_data = run_async_coro(spider.get_kuaishou_stream_data(
|
||||
url=record_url,
|
||||
proxy_addr=proxy_address,
|
||||
cookies=ks_cookie))
|
||||
port_info = asyncio.run(stream.get_kuaishou_stream_url(json_data, record_quality))
|
||||
port_info = run_async_coro(stream.get_kuaishou_stream_url(json_data, record_quality))
|
||||
|
||||
elif record_url.find("https://www.huya.com/") > -1:
|
||||
platform = '虎牙直播'
|
||||
with semaphore:
|
||||
if record_quality not in ['OD', 'BD', 'UHD']:
|
||||
json_data = asyncio.run(spider.get_huya_stream_data(
|
||||
json_data = run_async_coro(spider.get_huya_stream_data(
|
||||
url=record_url,
|
||||
proxy_addr=proxy_address,
|
||||
cookies=hy_cookie))
|
||||
port_info = asyncio.run(stream.get_huya_stream_url(json_data, record_quality))
|
||||
port_info = run_async_coro(stream.get_huya_stream_url(json_data, record_quality))
|
||||
else:
|
||||
port_info = asyncio.run(spider.get_huya_app_stream_url(
|
||||
port_info = run_async_coro(spider.get_huya_app_stream_url(
|
||||
url=record_url,
|
||||
proxy_addr=proxy_address,
|
||||
cookies=hy_cookie
|
||||
@@ -723,52 +708,52 @@ def start_record(url_data: tuple, count_variable: int = -1) -> None:
|
||||
elif record_url.find("https://www.douyu.com/") > -1:
|
||||
platform = '斗鱼直播'
|
||||
with semaphore:
|
||||
json_data = asyncio.run(spider.get_douyu_info_data(
|
||||
json_data = run_async_coro(spider.get_douyu_info_data(
|
||||
url=record_url, proxy_addr=proxy_address, cookies=douyu_cookie))
|
||||
port_info = asyncio.run(stream.get_douyu_stream_url(
|
||||
port_info = run_async_coro(stream.get_douyu_stream_url(
|
||||
json_data, video_quality=record_quality, cookies=douyu_cookie, proxy_addr=proxy_address
|
||||
))
|
||||
|
||||
elif record_url.find("https://www.yy.com/") > -1:
|
||||
platform = 'YY直播'
|
||||
with semaphore:
|
||||
json_data = asyncio.run(spider.get_yy_stream_data(
|
||||
json_data = run_async_coro(spider.get_yy_stream_data(
|
||||
url=record_url, proxy_addr=proxy_address, cookies=yy_cookie))
|
||||
port_info = asyncio.run(stream.get_yy_stream_url(json_data))
|
||||
port_info = run_async_coro(stream.get_yy_stream_url(json_data))
|
||||
|
||||
elif record_url.find("https://live.bilibili.com/") > -1:
|
||||
platform = 'B站直播'
|
||||
with semaphore:
|
||||
json_data = asyncio.run(spider.get_bilibili_room_info(
|
||||
json_data = run_async_coro(spider.get_bilibili_room_info(
|
||||
url=record_url, proxy_addr=proxy_address, cookies=bili_cookie))
|
||||
port_info = asyncio.run(stream.get_bilibili_stream_url(
|
||||
port_info = run_async_coro(stream.get_bilibili_stream_url(
|
||||
json_data, video_quality=record_quality, cookies=bili_cookie, proxy_addr=proxy_address))
|
||||
|
||||
elif record_url.find("http://xhslink.com/") > -1 or \
|
||||
record_url.find("https://www.xiaohongshu.com/") > -1:
|
||||
platform = '小红书直播'
|
||||
with semaphore:
|
||||
port_info = asyncio.run(spider.get_xhs_stream_url(
|
||||
port_info = run_async_coro(spider.get_xhs_stream_url(
|
||||
record_url, proxy_addr=proxy_address, cookies=xhs_cookie))
|
||||
retry += 1
|
||||
|
||||
elif record_url.find("www.bigo.tv/") > -1 or record_url.find("slink.bigovideo.tv/") > -1:
|
||||
platform = 'Bigo直播'
|
||||
with semaphore:
|
||||
port_info = asyncio.run(spider.get_bigo_stream_url(
|
||||
port_info = run_async_coro(spider.get_bigo_stream_url(
|
||||
record_url, proxy_addr=proxy_address, cookies=bigo_cookie))
|
||||
|
||||
elif record_url.find("https://app.blued.cn/") > -1:
|
||||
platform = 'Blued直播'
|
||||
with semaphore:
|
||||
port_info = asyncio.run(spider.get_blued_stream_url(
|
||||
port_info = run_async_coro(spider.get_blued_stream_url(
|
||||
record_url, proxy_addr=proxy_address, cookies=blued_cookie))
|
||||
|
||||
elif record_url.find("sooplive.co.kr/") > -1 or record_url.find("sooplive.com/") > -1:
|
||||
platform = 'SOOP'
|
||||
with semaphore:
|
||||
if global_proxy or proxy_address:
|
||||
json_data = asyncio.run(spider.get_sooplive_stream_data(
|
||||
json_data = run_async_coro(spider.get_sooplive_stream_data(
|
||||
url=record_url, proxy_addr=proxy_address,
|
||||
cookies=sooplive_cookie,
|
||||
username=sooplive_username,
|
||||
@@ -778,51 +763,51 @@ def start_record(url_data: tuple, count_variable: int = -1) -> None:
|
||||
utils.update_config(
|
||||
config_file, 'Cookie', 'sooplive_cookie', json_data['new_cookies']
|
||||
)
|
||||
port_info = asyncio.run(stream.get_stream_url(json_data, record_quality, spec=True))
|
||||
port_info = run_async_coro(stream.get_stream_url(json_data, record_quality, spec=True))
|
||||
else:
|
||||
logger.error("错误信息: 网络异常,请检查本网络是否能正常访问SOOP平台")
|
||||
|
||||
elif record_url.find("cc.163.com/") > -1:
|
||||
platform = '网易CC直播'
|
||||
with semaphore:
|
||||
json_data = asyncio.run(spider.get_netease_stream_data(
|
||||
json_data = run_async_coro(spider.get_netease_stream_data(
|
||||
url=record_url, cookies=netease_cookie))
|
||||
port_info = asyncio.run(stream.get_netease_stream_url(json_data, record_quality))
|
||||
port_info = run_async_coro(stream.get_netease_stream_url(json_data, record_quality))
|
||||
|
||||
elif record_url.find("qiandurebo.com/") > -1:
|
||||
platform = '千度热播'
|
||||
with semaphore:
|
||||
port_info = asyncio.run(spider.get_qiandurebo_stream_data(
|
||||
port_info = run_async_coro(spider.get_qiandurebo_stream_data(
|
||||
url=record_url, proxy_addr=proxy_address, cookies=qiandurebo_cookie))
|
||||
|
||||
elif record_url.find("www.pandalive.co.kr/") > -1:
|
||||
platform = 'PandaTV'
|
||||
with semaphore:
|
||||
if global_proxy or proxy_address:
|
||||
json_data = asyncio.run(spider.get_pandatv_stream_data(
|
||||
json_data = run_async_coro(spider.get_pandatv_stream_data(
|
||||
url=record_url,
|
||||
proxy_addr=proxy_address,
|
||||
cookies=pandatv_cookie
|
||||
))
|
||||
port_info = asyncio.run(stream.get_stream_url(json_data, record_quality, spec=True))
|
||||
port_info = run_async_coro(stream.get_stream_url(json_data, record_quality, spec=True))
|
||||
else:
|
||||
logger.error("错误信息: 网络异常,请检查本网络是否能正常访问PandaTV直播平台")
|
||||
|
||||
elif record_url.find("fm.missevan.com/") > -1:
|
||||
platform = '猫耳FM直播'
|
||||
with semaphore:
|
||||
port_info = asyncio.run(spider.get_maoerfm_stream_url(
|
||||
port_info = run_async_coro(spider.get_maoerfm_stream_url(
|
||||
url=record_url, proxy_addr=proxy_address, cookies=maoerfm_cookie))
|
||||
|
||||
elif record_url.find("www.winktv.co.kr/") > -1:
|
||||
platform = 'WinkTV'
|
||||
with semaphore:
|
||||
if global_proxy or proxy_address:
|
||||
json_data = asyncio.run(spider.get_winktv_stream_data(
|
||||
json_data = run_async_coro(spider.get_winktv_stream_data(
|
||||
url=record_url,
|
||||
proxy_addr=proxy_address,
|
||||
cookies=winktv_cookie))
|
||||
port_info = asyncio.run(stream.get_stream_url(json_data, record_quality, spec=True))
|
||||
port_info = run_async_coro(stream.get_stream_url(json_data, record_quality, spec=True))
|
||||
else:
|
||||
logger.error("错误信息: 网络异常,请检查本网络是否能正常访问WinkTV直播平台")
|
||||
|
||||
@@ -830,7 +815,7 @@ def start_record(url_data: tuple, count_variable: int = -1) -> None:
|
||||
platform = 'FlexTV'
|
||||
with semaphore:
|
||||
if global_proxy or proxy_address:
|
||||
json_data = asyncio.run(spider.get_flextv_stream_data(
|
||||
json_data = run_async_coro(spider.get_flextv_stream_data(
|
||||
url=record_url,
|
||||
proxy_addr=proxy_address,
|
||||
cookies=flextv_cookie,
|
||||
@@ -842,7 +827,7 @@ def start_record(url_data: tuple, count_variable: int = -1) -> None:
|
||||
config_file, 'Cookie', 'flextv_cookie', json_data['new_cookies']
|
||||
)
|
||||
if 'play_url_list' in json_data:
|
||||
port_info = asyncio.run(stream.get_stream_url(json_data, record_quality, spec=True))
|
||||
port_info = run_async_coro(stream.get_stream_url(json_data, record_quality, spec=True))
|
||||
else:
|
||||
port_info = json_data
|
||||
else:
|
||||
@@ -851,7 +836,7 @@ def start_record(url_data: tuple, count_variable: int = -1) -> None:
|
||||
elif record_url.find("look.163.com/") > -1:
|
||||
platform = 'Look直播'
|
||||
with semaphore:
|
||||
port_info = asyncio.run(spider.get_looklive_stream_url(
|
||||
port_info = run_async_coro(spider.get_looklive_stream_url(
|
||||
url=record_url, proxy_addr=proxy_address, cookies=look_cookie
|
||||
))
|
||||
|
||||
@@ -859,7 +844,7 @@ def start_record(url_data: tuple, count_variable: int = -1) -> None:
|
||||
platform = 'PopkonTV'
|
||||
with semaphore:
|
||||
if global_proxy or proxy_address:
|
||||
port_info = asyncio.run(spider.get_popkontv_stream_url(
|
||||
port_info = run_async_coro(spider.get_popkontv_stream_url(
|
||||
url=record_url,
|
||||
proxy_addr=proxy_address,
|
||||
access_token=popkontv_access_token,
|
||||
@@ -879,7 +864,7 @@ def start_record(url_data: tuple, count_variable: int = -1) -> None:
|
||||
elif record_url.find("twitcasting.tv/") > -1:
|
||||
platform = 'TwitCasting'
|
||||
with semaphore:
|
||||
json_data = asyncio.run(spider.get_twitcasting_stream_url(
|
||||
json_data = run_async_coro(spider.get_twitcasting_stream_url(
|
||||
url=record_url,
|
||||
proxy_addr=proxy_address,
|
||||
cookies=twitcasting_cookie,
|
||||
@@ -887,7 +872,7 @@ def start_record(url_data: tuple, count_variable: int = -1) -> None:
|
||||
username=twitcasting_username,
|
||||
password=twitcasting_password
|
||||
))
|
||||
port_info = asyncio.run(stream.get_stream_url(json_data, record_quality, spec=False))
|
||||
port_info = run_async_coro(stream.get_stream_url(json_data, record_quality, spec=False))
|
||||
|
||||
if port_info and port_info.get('new_cookies'):
|
||||
utils.update_config(
|
||||
@@ -898,36 +883,36 @@ def start_record(url_data: tuple, count_variable: int = -1) -> None:
|
||||
elif record_url.find("live.baidu.com/") > -1:
|
||||
platform = '百度直播'
|
||||
with semaphore:
|
||||
json_data = asyncio.run(spider.get_baidu_stream_data(
|
||||
json_data = run_async_coro(spider.get_baidu_stream_data(
|
||||
url=record_url,
|
||||
proxy_addr=proxy_address,
|
||||
cookies=baidu_cookie))
|
||||
port_info = asyncio.run(stream.get_stream_url(json_data, record_quality))
|
||||
port_info = run_async_coro(stream.get_stream_url(json_data, record_quality))
|
||||
|
||||
elif record_url.find("weibo.com/") > -1:
|
||||
platform = '微博直播'
|
||||
with semaphore:
|
||||
json_data = asyncio.run(spider.get_weibo_stream_data(
|
||||
json_data = run_async_coro(spider.get_weibo_stream_data(
|
||||
url=record_url, proxy_addr=proxy_address, cookies=weibo_cookie))
|
||||
port_info = asyncio.run(stream.get_stream_url(
|
||||
port_info = run_async_coro(stream.get_stream_url(
|
||||
json_data, record_quality, hls_extra_key='m3u8_url'))
|
||||
|
||||
elif record_url.find("kugou.com/") > -1:
|
||||
platform = '酷狗直播'
|
||||
with semaphore:
|
||||
port_info = asyncio.run(spider.get_kugou_stream_url(
|
||||
port_info = run_async_coro(spider.get_kugou_stream_url(
|
||||
url=record_url, proxy_addr=proxy_address, cookies=kugou_cookie))
|
||||
|
||||
elif record_url.find("www.twitch.tv/") > -1:
|
||||
platform = 'TwitchTV'
|
||||
with semaphore:
|
||||
if global_proxy or proxy_address:
|
||||
json_data = asyncio.run(spider.get_twitchtv_stream_data(
|
||||
json_data = run_async_coro(spider.get_twitchtv_stream_data(
|
||||
url=record_url,
|
||||
proxy_addr=proxy_address,
|
||||
cookies=twitch_cookie
|
||||
))
|
||||
port_info = asyncio.run(stream.get_stream_url(json_data, record_quality, spec=True))
|
||||
port_info = run_async_coro(stream.get_stream_url(json_data, record_quality, spec=True))
|
||||
else:
|
||||
logger.error("错误信息: 网络异常,请检查本网络是否能正常访问TwitchTV直播平台")
|
||||
|
||||
@@ -935,7 +920,7 @@ def start_record(url_data: tuple, count_variable: int = -1) -> None:
|
||||
if global_proxy or proxy_address:
|
||||
platform = 'LiveMe'
|
||||
with semaphore:
|
||||
port_info = asyncio.run(spider.get_liveme_stream_url(
|
||||
port_info = run_async_coro(spider.get_liveme_stream_url(
|
||||
url=record_url, proxy_addr=proxy_address, cookies=liveme_cookie))
|
||||
else:
|
||||
logger.error("错误信息: 网络异常,请检查本网络是否能正常访问LiveMe直播平台")
|
||||
@@ -943,113 +928,113 @@ def start_record(url_data: tuple, count_variable: int = -1) -> None:
|
||||
elif record_url.find("www.huajiao.com/") > -1:
|
||||
platform = '花椒直播'
|
||||
with semaphore:
|
||||
port_info = asyncio.run(spider.get_huajiao_stream_url(
|
||||
port_info = run_async_coro(spider.get_huajiao_stream_url(
|
||||
url=record_url, proxy_addr=proxy_address, cookies=huajiao_cookie))
|
||||
|
||||
elif record_url.find("7u66.com/") > -1:
|
||||
platform = '流星直播'
|
||||
with semaphore:
|
||||
port_info = asyncio.run(spider.get_liuxing_stream_url(
|
||||
port_info = run_async_coro(spider.get_liuxing_stream_url(
|
||||
url=record_url, proxy_addr=proxy_address, cookies=liuxing_cookie))
|
||||
|
||||
elif record_url.find("showroom-live.com/") > -1:
|
||||
platform = 'ShowRoom'
|
||||
with semaphore:
|
||||
json_data = asyncio.run(spider.get_showroom_stream_data(
|
||||
json_data = run_async_coro(spider.get_showroom_stream_data(
|
||||
url=record_url, proxy_addr=proxy_address, cookies=showroom_cookie))
|
||||
port_info = asyncio.run(stream.get_stream_url(json_data, record_quality, spec=True))
|
||||
port_info = run_async_coro(stream.get_stream_url(json_data, record_quality, spec=True))
|
||||
|
||||
elif record_url.find("live.acfun.cn/") > -1 or record_url.find("m.acfun.cn/") > -1:
|
||||
platform = 'Acfun'
|
||||
with semaphore:
|
||||
json_data = asyncio.run(spider.get_acfun_stream_data(
|
||||
json_data = run_async_coro(spider.get_acfun_stream_data(
|
||||
url=record_url, proxy_addr=proxy_address, cookies=acfun_cookie))
|
||||
port_info = asyncio.run(stream.get_stream_url(
|
||||
port_info = run_async_coro(stream.get_stream_url(
|
||||
json_data, record_quality, url_type='flv', flv_extra_key='url'))
|
||||
|
||||
elif record_url.find("live.tlclw.com/") > -1:
|
||||
platform = '畅聊直播'
|
||||
with semaphore:
|
||||
port_info = asyncio.run(spider.get_changliao_stream_url(
|
||||
port_info = run_async_coro(spider.get_changliao_stream_url(
|
||||
url=record_url, proxy_addr=proxy_address, cookies=changliao_cookie))
|
||||
|
||||
elif record_url.find("ybw1666.com/") > -1:
|
||||
platform = '音播直播'
|
||||
with semaphore:
|
||||
port_info = asyncio.run(spider.get_yinbo_stream_url(
|
||||
port_info = run_async_coro(spider.get_yinbo_stream_url(
|
||||
url=record_url, proxy_addr=proxy_address, cookies=yinbo_cookie))
|
||||
|
||||
elif record_url.find("www.inke.cn/") > -1:
|
||||
platform = '映客直播'
|
||||
with semaphore:
|
||||
port_info = asyncio.run(spider.get_yingke_stream_url(
|
||||
port_info = run_async_coro(spider.get_yingke_stream_url(
|
||||
url=record_url, proxy_addr=proxy_address, cookies=yingke_cookie))
|
||||
|
||||
elif record_url.find("www.zhihu.com/") > -1:
|
||||
platform = '知乎直播'
|
||||
with semaphore:
|
||||
port_info = asyncio.run(spider.get_zhihu_stream_url(
|
||||
port_info = run_async_coro(spider.get_zhihu_stream_url(
|
||||
url=record_url, proxy_addr=proxy_address, cookies=zhihu_cookie))
|
||||
|
||||
elif record_url.find("chzzk.naver.com/") > -1:
|
||||
platform = 'CHZZK'
|
||||
with semaphore:
|
||||
json_data = asyncio.run(spider.get_chzzk_stream_data(
|
||||
json_data = run_async_coro(spider.get_chzzk_stream_data(
|
||||
url=record_url, proxy_addr=proxy_address, cookies=chzzk_cookie))
|
||||
port_info = asyncio.run(stream.get_stream_url(json_data, record_quality, spec=True))
|
||||
port_info = run_async_coro(stream.get_stream_url(json_data, record_quality, spec=True))
|
||||
|
||||
elif record_url.find("www.haixiutv.com/") > -1:
|
||||
platform = '嗨秀直播'
|
||||
with semaphore:
|
||||
port_info = asyncio.run(spider.get_haixiu_stream_url(
|
||||
port_info = run_async_coro(spider.get_haixiu_stream_url(
|
||||
url=record_url, proxy_addr=proxy_address, cookies=haixiu_cookie))
|
||||
|
||||
elif record_url.find("vvxqiu.com/") > -1:
|
||||
platform = 'VV星球'
|
||||
with semaphore:
|
||||
port_info = asyncio.run(spider.get_vvxqiu_stream_url(
|
||||
port_info = run_async_coro(spider.get_vvxqiu_stream_url(
|
||||
url=record_url, proxy_addr=proxy_address, cookies=vvxqiu_cookie))
|
||||
|
||||
elif record_url.find("17.live/") > -1:
|
||||
platform = '17Live'
|
||||
with semaphore:
|
||||
port_info = asyncio.run(spider.get_17live_stream_url(
|
||||
port_info = run_async_coro(spider.get_17live_stream_url(
|
||||
url=record_url, proxy_addr=proxy_address, cookies=yiqilive_cookie))
|
||||
|
||||
elif record_url.find("www.lang.live/") > -1:
|
||||
platform = '浪Live'
|
||||
with semaphore:
|
||||
port_info = asyncio.run(spider.get_langlive_stream_url(
|
||||
port_info = run_async_coro(spider.get_langlive_stream_url(
|
||||
url=record_url, proxy_addr=proxy_address, cookies=langlive_cookie))
|
||||
|
||||
elif record_url.find("m.pp.weimipopo.com/") > -1:
|
||||
platform = '漂漂直播'
|
||||
with semaphore:
|
||||
port_info = asyncio.run(spider.get_pplive_stream_url(
|
||||
port_info = run_async_coro(spider.get_pplive_stream_url(
|
||||
url=record_url, proxy_addr=proxy_address, cookies=pplive_cookie))
|
||||
|
||||
elif record_url.find(".6.cn/") > -1:
|
||||
platform = '六间房直播'
|
||||
with semaphore:
|
||||
port_info = asyncio.run(spider.get_6room_stream_url(
|
||||
port_info = run_async_coro(spider.get_6room_stream_url(
|
||||
url=record_url, proxy_addr=proxy_address, cookies=six_room_cookie))
|
||||
|
||||
elif record_url.find("lehaitv.com/") > -1:
|
||||
platform = '乐嗨直播'
|
||||
with semaphore:
|
||||
port_info = asyncio.run(spider.get_haixiu_stream_url(
|
||||
port_info = run_async_coro(spider.get_haixiu_stream_url(
|
||||
url=record_url, proxy_addr=proxy_address, cookies=lehaitv_cookie))
|
||||
|
||||
elif record_url.find("h.catshow168.com/") > -1:
|
||||
platform = '花猫直播'
|
||||
with semaphore:
|
||||
port_info = asyncio.run(spider.get_pplive_stream_url(
|
||||
port_info = run_async_coro(spider.get_pplive_stream_url(
|
||||
url=record_url, proxy_addr=proxy_address, cookies=huamao_cookie))
|
||||
|
||||
elif record_url.find("live.shopee") > -1 or record_url.find("shp.ee/") > -1:
|
||||
platform = 'shopee'
|
||||
with semaphore:
|
||||
port_info = asyncio.run(spider.get_shopee_stream_url(
|
||||
port_info = run_async_coro(spider.get_shopee_stream_url(
|
||||
url=record_url, proxy_addr=proxy_address, cookies=shopee_cookie))
|
||||
if port_info.get('uid'):
|
||||
new_record_url = record_url.split('?')[0] + '?' + str(port_info['uid'])
|
||||
@@ -1057,16 +1042,16 @@ def start_record(url_data: tuple, count_variable: int = -1) -> None:
|
||||
elif record_url.find("www.youtube.com/") > -1 or record_url.find("youtu.be/") > -1:
|
||||
platform = 'Youtube'
|
||||
with semaphore:
|
||||
json_data = asyncio.run(spider.get_youtube_stream_url(
|
||||
json_data = run_async_coro(spider.get_youtube_stream_url(
|
||||
url=record_url, proxy_addr=proxy_address, cookies=youtube_cookie))
|
||||
port_info = asyncio.run(stream.get_stream_url(json_data, record_quality, spec=True))
|
||||
port_info = run_async_coro(stream.get_stream_url(json_data, record_quality, spec=True))
|
||||
|
||||
elif record_url.find("tb.cn") > -1:
|
||||
platform = '淘宝直播'
|
||||
with semaphore:
|
||||
json_data = asyncio.run(spider.get_taobao_stream_url(
|
||||
json_data = run_async_coro(spider.get_taobao_stream_url(
|
||||
url=record_url, proxy_addr=proxy_address, cookies=taobao_cookie))
|
||||
port_info = asyncio.run(stream.get_stream_url(
|
||||
port_info = run_async_coro(stream.get_stream_url(
|
||||
json_data, record_quality,
|
||||
url_type='all', hls_extra_key='hlsUrl', flv_extra_key='flvUrl'
|
||||
))
|
||||
@@ -1074,7 +1059,7 @@ def start_record(url_data: tuple, count_variable: int = -1) -> None:
|
||||
elif record_url.find("3.cn") > -1 or record_url.find("m.jd.com") > -1:
|
||||
platform = '京东直播'
|
||||
with semaphore:
|
||||
port_info = asyncio.run(spider.get_jd_stream_url(
|
||||
port_info = run_async_coro(spider.get_jd_stream_url(
|
||||
url=record_url, proxy_addr=proxy_address, cookies=jd_cookie))
|
||||
|
||||
elif record_url.find("faceit.com/") > -1:
|
||||
@@ -1082,34 +1067,34 @@ def start_record(url_data: tuple, count_variable: int = -1) -> None:
|
||||
with semaphore:
|
||||
if global_proxy or proxy_address:
|
||||
with semaphore:
|
||||
json_data = asyncio.run(spider.get_faceit_stream_data(
|
||||
json_data = run_async_coro(spider.get_faceit_stream_data(
|
||||
url=record_url, proxy_addr=proxy_address, cookies=faceit_cookie))
|
||||
port_info = asyncio.run(stream.get_stream_url(json_data, record_quality, spec=True))
|
||||
port_info = run_async_coro(stream.get_stream_url(json_data, record_quality, spec=True))
|
||||
else:
|
||||
logger.error("错误信息: 网络异常,请检查本网络是否能正常访问faceit直播平台")
|
||||
|
||||
elif record_url.find("www.miguvideo.com") > -1 or record_url.find("m.miguvideo.com") > -1:
|
||||
platform = '咪咕直播'
|
||||
with semaphore:
|
||||
port_info = asyncio.run(spider.get_migu_stream_url(
|
||||
port_info = run_async_coro(spider.get_migu_stream_url(
|
||||
url=record_url, proxy_addr=proxy_address, cookies=migu_cookie))
|
||||
|
||||
elif record_url.find("show.lailianjie.com") > -1:
|
||||
platform = '连接直播'
|
||||
with semaphore:
|
||||
port_info = asyncio.run(spider.get_lianjie_stream_url(
|
||||
port_info = run_async_coro(spider.get_lianjie_stream_url(
|
||||
url=record_url, proxy_addr=proxy_address, cookies=lianjie_cookie))
|
||||
|
||||
elif record_url.find("www.imkktv.com") > -1:
|
||||
platform = '来秀直播'
|
||||
with semaphore:
|
||||
port_info = asyncio.run(spider.get_laixiu_stream_url(
|
||||
port_info = run_async_coro(spider.get_laixiu_stream_url(
|
||||
url=record_url, proxy_addr=proxy_address, cookies=laixiu_cookie))
|
||||
|
||||
elif record_url.find("www.picarto.tv") > -1:
|
||||
platform = 'Picarto'
|
||||
with semaphore:
|
||||
port_info = asyncio.run(spider.get_picarto_stream_url(
|
||||
port_info = run_async_coro(spider.get_picarto_stream_url(
|
||||
url=record_url, proxy_addr=proxy_address, cookies=picarto_cookie))
|
||||
|
||||
elif record_url.find(".m3u8") > -1 or record_url.find(".flv") > -1:
|
||||
@@ -1620,23 +1605,42 @@ def start_record(url_data: tuple, count_variable: int = -1) -> None:
|
||||
error_window.append(1)
|
||||
|
||||
else:
|
||||
import sys
|
||||
sys.path.insert(0, './dist')
|
||||
from douyin_srs_ffplay import start_douyin_srs_ffplay
|
||||
# 调用
|
||||
start_douyin_srs_ffplay(
|
||||
title=title_in_name,
|
||||
anchor_name=anchor_name,
|
||||
real_url=real_url,
|
||||
rec_info=rec_info,
|
||||
record_name=record_name,
|
||||
recording=recording,
|
||||
recording_time_list=recording_time_list,
|
||||
running_list=running_list,
|
||||
monitoring=monitoring,
|
||||
clear_record_info=clear_record_info,
|
||||
color_obj=color_obj
|
||||
)
|
||||
# 可选:项目 dist/ 下提供 douyin_srs_ffplay;否则使用仓库内 YouTube 推流
|
||||
_repo_root = Path(__file__).resolve().parent
|
||||
_dist = _repo_root / "dist"
|
||||
if _dist.is_dir():
|
||||
sys.path.insert(0, str(_dist))
|
||||
try:
|
||||
from douyin_srs_ffplay import start_douyin_srs_ffplay
|
||||
|
||||
start_douyin_srs_ffplay(
|
||||
title=title_in_name,
|
||||
anchor_name=anchor_name,
|
||||
real_url=real_url,
|
||||
rec_info=rec_info,
|
||||
record_name=record_name,
|
||||
recording=recording,
|
||||
recording_time_list=recording_time_list,
|
||||
running_list=running_list,
|
||||
monitoring=monitoring,
|
||||
clear_record_info=clear_record_info,
|
||||
color_obj=color_obj,
|
||||
)
|
||||
except ImportError:
|
||||
from douyin_youtube_ffplay import start_douyin_youtube_ffplay
|
||||
|
||||
start_douyin_youtube_ffplay(
|
||||
title=title_in_name,
|
||||
anchor_name=anchor_name,
|
||||
real_url=real_url,
|
||||
rec_info=rec_info,
|
||||
record_name=record_name,
|
||||
recording=recording,
|
||||
recording_time_list=recording_time_list,
|
||||
running_list=running_list,
|
||||
clear_record_info=clear_record_info,
|
||||
color_obj=color_obj,
|
||||
)
|
||||
count_time = time.time()
|
||||
|
||||
|
||||
@@ -2201,4 +2205,4 @@ while True:
|
||||
t2.start()
|
||||
first_run = False
|
||||
|
||||
time.sleep(3)
|
||||
time.sleep(3)
|
||||
|
||||
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 9.5 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 8.9 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 73 KiB |
16
mobile/android/.gitignore
vendored
@@ -1,16 +0,0 @@
|
||||
# OSX
|
||||
#
|
||||
.DS_Store
|
||||
|
||||
# Android/IntelliJ
|
||||
#
|
||||
build/
|
||||
.idea
|
||||
.gradle
|
||||
local.properties
|
||||
*.iml
|
||||
*.hprof
|
||||
.cxx/
|
||||
|
||||
# Bundle artifacts
|
||||
*.jsbundle
|
||||
@@ -1,182 +0,0 @@
|
||||
apply plugin: "com.android.application"
|
||||
apply plugin: "org.jetbrains.kotlin.android"
|
||||
apply plugin: "com.facebook.react"
|
||||
|
||||
def projectRoot = rootDir.getAbsoluteFile().getParentFile().getAbsolutePath()
|
||||
|
||||
/**
|
||||
* This is the configuration block to customize your React Native Android app.
|
||||
* By default you don't need to apply any configuration, just uncomment the lines you need.
|
||||
*/
|
||||
react {
|
||||
entryFile = file(["node", "-e", "require('expo/scripts/resolveAppEntry')", projectRoot, "android", "absolute"].execute(null, rootDir).text.trim())
|
||||
reactNativeDir = new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim()).getParentFile().getAbsoluteFile()
|
||||
hermesCommand = new File(["node", "--print", "require.resolve('hermes-compiler/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim()).getParentFile().getAbsolutePath() + "/hermesc/%OS-BIN%/hermesc"
|
||||
codegenDir = new File(["node", "--print", "require.resolve('@react-native/codegen/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim()).getParentFile().getAbsoluteFile()
|
||||
|
||||
enableBundleCompression = (findProperty('android.enableBundleCompression') ?: false).toBoolean()
|
||||
// Use Expo CLI to bundle the app, this ensures the Metro config
|
||||
// works correctly with Expo projects.
|
||||
cliFile = new File(["node", "--print", "require.resolve('@expo/cli', { paths: [require.resolve('expo/package.json')] })"].execute(null, rootDir).text.trim())
|
||||
bundleCommand = "export:embed"
|
||||
|
||||
/* Folders */
|
||||
// The root of your project, i.e. where "package.json" lives. Default is '../..'
|
||||
// root = file("../../")
|
||||
// The folder where the react-native NPM package is. Default is ../../node_modules/react-native
|
||||
// reactNativeDir = file("../../node_modules/react-native")
|
||||
// The folder where the react-native Codegen package is. Default is ../../node_modules/@react-native/codegen
|
||||
// codegenDir = file("../../node_modules/@react-native/codegen")
|
||||
|
||||
/* Variants */
|
||||
// The list of variants to that are debuggable. For those we're going to
|
||||
// skip the bundling of the JS bundle and the assets. By default is just 'debug'.
|
||||
// If you add flavors like lite, prod, etc. you'll have to list your debuggableVariants.
|
||||
// debuggableVariants = ["liteDebug", "prodDebug"]
|
||||
|
||||
/* Bundling */
|
||||
// A list containing the node command and its flags. Default is just 'node'.
|
||||
// nodeExecutableAndArgs = ["node"]
|
||||
|
||||
//
|
||||
// The path to the CLI configuration file. Default is empty.
|
||||
// bundleConfig = file(../rn-cli.config.js)
|
||||
//
|
||||
// The name of the generated asset file containing your JS bundle
|
||||
// bundleAssetName = "MyApplication.android.bundle"
|
||||
//
|
||||
// The entry file for bundle generation. Default is 'index.android.js' or 'index.js'
|
||||
// entryFile = file("../js/MyApplication.android.js")
|
||||
//
|
||||
// A list of extra flags to pass to the 'bundle' commands.
|
||||
// See https://github.com/react-native-community/cli/blob/main/docs/commands.md#bundle
|
||||
// extraPackagerArgs = []
|
||||
|
||||
/* Hermes Commands */
|
||||
// The hermes compiler command to run. By default it is 'hermesc'
|
||||
// hermesCommand = "$rootDir/my-custom-hermesc/bin/hermesc"
|
||||
//
|
||||
// The list of flags to pass to the Hermes compiler. By default is "-O", "-output-source-map"
|
||||
// hermesFlags = ["-O", "-output-source-map"]
|
||||
|
||||
/* Autolinking */
|
||||
autolinkLibrariesWithApp()
|
||||
}
|
||||
|
||||
/**
|
||||
* Set this to true in release builds to optimize the app using [R8](https://developer.android.com/topic/performance/app-optimization/enable-app-optimization).
|
||||
*/
|
||||
def enableMinifyInReleaseBuilds = (findProperty('android.enableMinifyInReleaseBuilds') ?: false).toBoolean()
|
||||
|
||||
/**
|
||||
* The preferred build flavor of JavaScriptCore (JSC)
|
||||
*
|
||||
* For example, to use the international variant, you can use:
|
||||
* `def jscFlavor = 'org.webkit:android-jsc-intl:+'`
|
||||
*
|
||||
* The international variant includes ICU i18n library and necessary data
|
||||
* allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that
|
||||
* give correct results when using with locales other than en-US. Note that
|
||||
* this variant is about 6MiB larger per architecture than default.
|
||||
*/
|
||||
def jscFlavor = 'io.github.react-native-community:jsc-android:2026004.+'
|
||||
|
||||
android {
|
||||
ndkVersion rootProject.ext.ndkVersion
|
||||
|
||||
buildToolsVersion rootProject.ext.buildToolsVersion
|
||||
compileSdk rootProject.ext.compileSdkVersion
|
||||
|
||||
namespace 'com.douyin.recorder.remote'
|
||||
defaultConfig {
|
||||
applicationId 'com.douyin.recorder.remote'
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 1
|
||||
versionName "1.0.0"
|
||||
|
||||
buildConfigField "String", "REACT_NATIVE_RELEASE_LEVEL", "\"${findProperty('reactNativeReleaseLevel') ?: 'stable'}\""
|
||||
}
|
||||
signingConfigs {
|
||||
debug {
|
||||
storeFile file('debug.keystore')
|
||||
storePassword 'android'
|
||||
keyAlias 'androiddebugkey'
|
||||
keyPassword 'android'
|
||||
}
|
||||
}
|
||||
buildTypes {
|
||||
debug {
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
release {
|
||||
// Caution! In production, you need to generate your own keystore file.
|
||||
// see https://reactnative.dev/docs/signed-apk-android.
|
||||
signingConfig signingConfigs.debug
|
||||
def enableShrinkResources = findProperty('android.enableShrinkResourcesInReleaseBuilds') ?: 'false'
|
||||
shrinkResources enableShrinkResources.toBoolean()
|
||||
minifyEnabled enableMinifyInReleaseBuilds
|
||||
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
|
||||
def enablePngCrunchInRelease = findProperty('android.enablePngCrunchInReleaseBuilds') ?: 'true'
|
||||
crunchPngs enablePngCrunchInRelease.toBoolean()
|
||||
}
|
||||
}
|
||||
packagingOptions {
|
||||
jniLibs {
|
||||
def enableLegacyPackaging = findProperty('expo.useLegacyPackaging') ?: 'false'
|
||||
useLegacyPackaging enableLegacyPackaging.toBoolean()
|
||||
}
|
||||
}
|
||||
androidResources {
|
||||
ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:!CVS:!thumbs.db:!picasa.ini:!*~'
|
||||
}
|
||||
}
|
||||
|
||||
// Apply static values from `gradle.properties` to the `android.packagingOptions`
|
||||
// Accepts values in comma delimited lists, example:
|
||||
// android.packagingOptions.pickFirsts=/LICENSE,**/picasa.ini
|
||||
["pickFirsts", "excludes", "merges", "doNotStrip"].each { prop ->
|
||||
// Split option: 'foo,bar' -> ['foo', 'bar']
|
||||
def options = (findProperty("android.packagingOptions.$prop") ?: "").split(",");
|
||||
// Trim all elements in place.
|
||||
for (i in 0..<options.size()) options[i] = options[i].trim();
|
||||
// `[] - ""` is essentially `[""].filter(Boolean)` removing all empty strings.
|
||||
options -= ""
|
||||
|
||||
if (options.length > 0) {
|
||||
println "android.packagingOptions.$prop += $options ($options.length)"
|
||||
// Ex: android.packagingOptions.pickFirsts += '**/SCCS/**'
|
||||
options.each {
|
||||
android.packagingOptions[prop] += it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// The version of react-native is set by the React Native Gradle Plugin
|
||||
implementation("com.facebook.react:react-android")
|
||||
|
||||
def isGifEnabled = (findProperty('expo.gif.enabled') ?: "") == "true";
|
||||
def isWebpEnabled = (findProperty('expo.webp.enabled') ?: "") == "true";
|
||||
def isWebpAnimatedEnabled = (findProperty('expo.webp.animated') ?: "") == "true";
|
||||
|
||||
if (isGifEnabled) {
|
||||
// For animated gif support
|
||||
implementation("com.facebook.fresco:animated-gif:${expoLibs.versions.fresco.get()}")
|
||||
}
|
||||
|
||||
if (isWebpEnabled) {
|
||||
// For webp support
|
||||
implementation("com.facebook.fresco:webpsupport:${expoLibs.versions.fresco.get()}")
|
||||
if (isWebpAnimatedEnabled) {
|
||||
// Animated webp support
|
||||
implementation("com.facebook.fresco:animated-webp:${expoLibs.versions.fresco.get()}")
|
||||
}
|
||||
}
|
||||
|
||||
if (hermesEnabled.toBoolean()) {
|
||||
implementation("com.facebook.react:hermes-android")
|
||||
} else {
|
||||
implementation jscFlavor
|
||||
}
|
||||
}
|
||||
14
mobile/android/app/proguard-rules.pro
vendored
@@ -1,14 +0,0 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# By default, the flags in this file are appended to flags specified
|
||||
# in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt
|
||||
# You can edit the include path and order by changing the proguardFiles
|
||||
# directive in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# react-native-reanimated
|
||||
-keep class com.swmansion.reanimated.** { *; }
|
||||
-keep class com.facebook.react.turbomodule.** { *; }
|
||||
|
||||
# Add any project specific keep options here:
|
||||
@@ -1,7 +0,0 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
||||
|
||||
<application android:usesCleartextTraffic="true" tools:targetApi="28" tools:ignore="GoogleAppIndexingWarning" tools:replace="android:usesCleartextTraffic" />
|
||||
</manifest>
|
||||
@@ -1,7 +0,0 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
||||
|
||||
<application android:usesCleartextTraffic="true" tools:targetApi="28" tools:ignore="GoogleAppIndexingWarning" tools:replace="android:usesCleartextTraffic" />
|
||||
</manifest>
|
||||
@@ -1,33 +0,0 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
|
||||
<uses-permission android:name="android.permission.CAMERA"/>
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" tools:replace="android:maxSdkVersion"/>
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
||||
<uses-permission android:name="android.permission.VIBRATE"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="32" tools:replace="android:maxSdkVersion"/>
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<data android:scheme="https"/>
|
||||
</intent>
|
||||
</queries>
|
||||
<application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="true" android:theme="@style/AppTheme" android:supportsRtl="true" android:enableOnBackInvokedCallback="false">
|
||||
<meta-data android:name="expo.modules.updates.ENABLED" android:value="false"/>
|
||||
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH" android:value="ALWAYS"/>
|
||||
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS" android:value="0"/>
|
||||
<activity android:name=".MainActivity" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|uiMode|smallestScreenSize" android:launchMode="singleTask" android:windowSoftInputMode="adjustResize" android:theme="@style/Theme.App.SplashScreen" android:exported="true" android:screenOrientation="portrait">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<data android:scheme="mobile"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -1,65 +0,0 @@
|
||||
package com.douyin.recorder.remote
|
||||
import expo.modules.splashscreen.SplashScreenManager
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
|
||||
import com.facebook.react.ReactActivity
|
||||
import com.facebook.react.ReactActivityDelegate
|
||||
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
|
||||
import com.facebook.react.defaults.DefaultReactActivityDelegate
|
||||
|
||||
import expo.modules.ReactActivityDelegateWrapper
|
||||
|
||||
class MainActivity : ReactActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
// Set the theme to AppTheme BEFORE onCreate to support
|
||||
// coloring the background, status bar, and navigation bar.
|
||||
// This is required for expo-splash-screen.
|
||||
// setTheme(R.style.AppTheme);
|
||||
// @generated begin expo-splashscreen - expo prebuild (DO NOT MODIFY) sync-f3ff59a738c56c9a6119210cb55f0b613eb8b6af
|
||||
SplashScreenManager.registerOnActivity(this)
|
||||
// @generated end expo-splashscreen
|
||||
super.onCreate(null)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the main component registered from JavaScript. This is used to schedule
|
||||
* rendering of the component.
|
||||
*/
|
||||
override fun getMainComponentName(): String = "main"
|
||||
|
||||
/**
|
||||
* Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate]
|
||||
* which allows you to enable New Architecture with a single boolean flags [fabricEnabled]
|
||||
*/
|
||||
override fun createReactActivityDelegate(): ReactActivityDelegate {
|
||||
return ReactActivityDelegateWrapper(
|
||||
this,
|
||||
BuildConfig.IS_NEW_ARCHITECTURE_ENABLED,
|
||||
object : DefaultReactActivityDelegate(
|
||||
this,
|
||||
mainComponentName,
|
||||
fabricEnabled
|
||||
){})
|
||||
}
|
||||
|
||||
/**
|
||||
* Align the back button behavior with Android S
|
||||
* where moving root activities to background instead of finishing activities.
|
||||
* @see <a href="https://developer.android.com/reference/android/app/Activity#onBackPressed()">onBackPressed</a>
|
||||
*/
|
||||
override fun invokeDefaultOnBackPressed() {
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
|
||||
if (!moveTaskToBack(false)) {
|
||||
// For non-root activities, use the default implementation to finish them.
|
||||
super.invokeDefaultOnBackPressed()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Use the default back button implementation on Android S
|
||||
// because it's doing more than [Activity.moveTaskToBack] in fact.
|
||||
super.invokeDefaultOnBackPressed()
|
||||
}
|
||||
}
|
||||