From 46fb194a02843df221f050fb24ad14e235c89ee1 Mon Sep 17 00:00:00 2001
From: eric
Date: Sat, 28 Mar 2026 17:16:38 -0500
Subject: [PATCH] 's'
---
config/hardware.env.example | 14 +
docs/install-profiles.md | 7 +
douyin_youtube_ffplay.py | 46 ++--
ecosystem.config.cjs | 30 ++-
main.py | 152 +++++------
scripts/hardware_probe.py | 16 +-
src/async_thread_loop.py | 18 ++
src/hub_routes.py | 26 +-
src/web_process_backend.py | 57 ++++-
src/youtube_ini_sync.py | 24 ++
tiktok.py | 152 +++++------
tools/deploy_live_paramiko.py | 11 +-
.../live-product/LiveAndroidWorkstation.tsx | 157 ++++++++----
.../live-product/LiveControlApp.tsx | 33 ++-
.../live-product/LiveProcessLiveLogCard.tsx | 241 ++++++++++++++++++
.../live-product/LiveTiktokHdmiView.tsx | 48 +++-
.../live-product/LiveYoutubeUnmannedView.tsx | 82 +++++-
web-console/lib/youtubeConfigUtils.ts | 18 ++
web.py | 6 +-
youtube.py | 155 +++++------
20 files changed, 962 insertions(+), 331 deletions(-)
create mode 100644 config/hardware.env.example
create mode 100644 src/async_thread_loop.py
create mode 100644 src/youtube_ini_sync.py
create mode 100644 web-console/components/live-product/LiveProcessLiveLogCard.tsx
diff --git a/config/hardware.env.example b/config/hardware.env.example
new file mode 100644
index 0000000..e38d73e
--- /dev/null
+++ b/config/hardware.env.example
@@ -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
diff --git a/docs/install-profiles.md b/docs/install-profiles.md
index 54cdf6d..30840b2 100644
--- a/docs/install-profiles.md
+++ b/docs/install-profiles.md
@@ -36,6 +36,13 @@ Use this when the device should only run the Douyin or YouTube business stack.
- Builds the web console and enables `live-console.service`
- Generates the hardware profile for adaptive ARM or X86 defaults
+### 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__`)。
+- **硬件编码**:`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.
diff --git a/douyin_youtube_ffplay.py b/douyin_youtube_ffplay.py
index 4317531..71e2a9d 100644
--- a/douyin_youtube_ffplay.py
+++ b/douyin_youtube_ffplay.py
@@ -70,7 +70,6 @@ def start_douyin_youtube_ffplay(
recording: set,
recording_time_list: dict,
running_list: list,
- monitoring: list,
clear_record_info,
color_obj,
):
@@ -125,18 +124,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",
@@ -364,13 +367,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:
@@ -406,13 +416,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)
\ No newline at end of file
+ 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__")
\ No newline at end of file
diff --git a/ecosystem.config.cjs b/ecosystem.config.cjs
index 39abeba..48da343 100644
--- a/ecosystem.config.cjs
+++ b/ecosystem.config.cjs
@@ -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:每路需要独立 config/youtube.ini + URL_config.ini(或独立工作目录),
+ * 请为每路复制一份目录或使用符号链接,再为每个 PM2 app 设置不同的 cwd,进程名与控制台 Pro 线路一致(如 youtube2__xxx)。
+ * 音频处理链与 GitLab 分支 ffmpegAudioRMFoode 中 douyin_youtube_ffplay 对齐;Linux ARM/x86 编码由 config/hardware.env 与硬件探测辅助。
*/
const path = require("path");
const root = __dirname;
@@ -18,8 +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 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 第二路示例:把 /opt/live/lane2 换成你独立配置目录(内含 config/youtube.ini、URL_config.ini)——
+ // {
+ // name: "youtube2__lane2",
+ // cwd: "/opt/live/lane2",
+ // script: "youtube.py",
+ // interpreter: py,
+ // env: { PYTHONUNBUFFERED: "1" },
+ // },
// 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" },
],
diff --git a/main.py b/main.py
index 2417f86..f27ffbd 100644
--- a/main.py
+++ b/main.py
@@ -13,6 +13,8 @@ from __future__ import annotations
import asyncio
import os
+
+from src.async_thread_loop import run_async_coro
import sys
import builtins
import subprocess
@@ -669,27 +671,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平台")
@@ -697,23 +699,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
@@ -722,52 +724,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,
@@ -777,51 +779,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直播平台")
@@ -829,7 +831,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,
@@ -841,7 +843,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:
@@ -850,7 +852,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
))
@@ -858,7 +860,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,
@@ -878,7 +880,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,
@@ -886,7 +888,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(
@@ -897,36 +899,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直播平台")
@@ -934,7 +936,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直播平台")
@@ -942,113 +944,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'])
@@ -1056,16 +1058,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'
))
@@ -1073,7 +1075,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:
@@ -1081,34 +1083,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:
diff --git a/scripts/hardware_probe.py b/scripts/hardware_probe.py
index 9cae8ff..6bf46be 100644
--- a/scripts/hardware_probe.py
+++ b/scripts/hardware_probe.py
@@ -146,15 +146,29 @@ def build_profile() -> dict:
def suggest_video_encoder(profile: dict) -> str:
- """在 Linux 上根据 ffmpeg 能力与探测结果给出可选硬件编码器名(供 douyin→YouTube 读取 LIVE_VIDEO_ENCODER)。"""
+ """在 Linux 上根据 ffmpeg 能力与探测结果给出可选硬件编码器名(供 douyin→YouTube 读取 LIVE_VIDEO_ENCODER)。
+
+ 优先级:RKMPP(常见 ARM SoC)→ V4L2M2M(通用 ARM/部分板)→ VA-API(x86 核显)→ QSV(Intel x86)。
+ NVIDIA 由 douyin_youtube_ffplay 内单独检测 h264_nvenc,此处不重复写入。
+ """
+ if platform.system().lower() != "linux":
+ return ""
text = run_text(["ffmpeg", "-hide_banner", "-encoders"])
if not text:
return ""
hwaccels = set(profile.get("ffmpeg_hwaccels") or [])
+ gpu = profile.get("gpu") or {}
+ render_ok = bool(gpu.get("render_nodes"))
+ machine = platform.machine().lower()
+
if "h264_rkmpp" in text and Path("/dev/mpp_service").exists():
return "h264_rkmpp"
if "h264_v4l2m2m" in text and "v4l2m2m" in hwaccels:
return "h264_v4l2m2m"
+ if "h264_vaapi" in text and render_ok and "vaapi" in hwaccels:
+ return "h264_vaapi"
+ if machine in ("x86_64", "amd64") and "h264_qsv" in text:
+ return "h264_qsv"
return ""
diff --git a/src/async_thread_loop.py b/src/async_thread_loop.py
new file mode 100644
index 0000000..157b54e
--- /dev/null
+++ b/src/async_thread_loop.py
@@ -0,0 +1,18 @@
+"""Per-thread asyncio event loop reuse (avoids asyncio.run() overhead in recorder threads)."""
+from __future__ import annotations
+
+import asyncio
+import threading
+from typing import Any, Coroutine, TypeVar
+
+_T = TypeVar("_T")
+_tls = threading.local()
+
+
+def run_async_coro(coro: Coroutine[Any, Any, _T]) -> _T:
+ loop: asyncio.AbstractEventLoop | None = getattr(_tls, "loop", None)
+ if loop is None or loop.is_closed():
+ loop = asyncio.new_event_loop()
+ _tls.loop = loop
+ asyncio.set_event_loop(loop)
+ return loop.run_until_complete(coro)
diff --git a/src/hub_routes.py b/src/hub_routes.py
index 7bc8d50..74c0aef 100644
--- a/src/hub_routes.py
+++ b/src/hub_routes.py
@@ -2,6 +2,7 @@
from __future__ import annotations
import json
+from pathlib import Path
from typing import Any, Optional
from fastapi import APIRouter
@@ -19,22 +20,30 @@ from src.control_plane import (
)
from src.live_config import business_config, load_hub_config_snapshot, services_config_list
from src.web_process_backend import WebProcessBackend
+from src.youtube_ini_sync import write_youtube_ini_stream_key
router = APIRouter(prefix="/hub", tags=["Live Hub"])
_process_backend: WebProcessBackend | None = None
_monitor_entries: list[dict[str, Any]] = []
+_repo_root: Path | None = None
-def configure_hub(process_backend: WebProcessBackend, monitor_entries: list[dict[str, Any]]) -> None:
- global _process_backend, _monitor_entries
+def configure_hub(
+ process_backend: WebProcessBackend,
+ monitor_entries: list[dict[str, Any]],
+ repo_root: Optional[Path] = None,
+) -> None:
+ global _process_backend, _monitor_entries, _repo_root
_process_backend = process_backend
_monitor_entries = list(monitor_entries)
+ _repo_root = repo_root.resolve() if repo_root is not None else None
class BusinessDouyinPatch(BaseModel):
streams: Optional[dict[str, str]] = None
ffmpeg: Optional[dict[str, Any]] = None
+ sync_youtube_ini: Optional[bool] = None
@router.get("/dashboard")
@@ -142,4 +151,15 @@ async def hub_business_douyin_patch(body: BusinessDouyinPatch) -> dict[str, str]
cur_ff.update(body.ffmpeg)
current["ffmpeg"] = cur_ff
path.write_text(json.dumps(current, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
- return {"message": "business/douyinyoutube.json updated"}
+ msg = "business/douyinyoutube.json updated"
+ if body.sync_youtube_ini and _repo_root is not None:
+ streams = current.get("streams") if isinstance(current.get("streams"), dict) else {}
+ raw_key = streams.get("youtube_rtmp_key")
+ key_val = raw_key.strip() if isinstance(raw_key, str) else ""
+ if key_val:
+ msg = f"{msg}; {write_youtube_ini_stream_key(_repo_root, key_val)}"
+ else:
+ msg = f"{msg}; sync_youtube_ini skipped (no youtube_rtmp_key)"
+ elif body.sync_youtube_ini and _repo_root is None:
+ msg = f"{msg}; sync_youtube_ini skipped (repo root not configured)"
+ return {"message": msg}
diff --git a/src/web_process_backend.py b/src/web_process_backend.py
index 21f2dfa..27a6ef1 100644
--- a/src/web_process_backend.py
+++ b/src/web_process_backend.py
@@ -49,8 +49,7 @@ async def run_pm2(cmd: str, timeout: float = 15.0) -> str:
return text
-def _linux_ffmpeg_pids_for_stack(process_name: str, project_root: Path | None) -> list[int]:
- """仅匹配与本项目直播栈相关的 ffmpeg(命令行含 YouTube 推流、抖音拉流或项目路径),避免 killall 误伤整机其它 ffmpeg。"""
+def _ffmpeg_cmdline_markers(process_name: str, project_root: Path | None) -> list[str]:
markers: list[str] = [
process_name,
"a.rtmp.youtube.com",
@@ -64,7 +63,12 @@ def _linux_ffmpeg_pids_for_stack(process_name: str, project_root: Path | None) -
markers.append(str(project_root.resolve()))
except OSError:
pass
- markers = [m for m in markers if m]
+ return [m for m in markers if m]
+
+
+def _linux_ffmpeg_pids_for_stack(process_name: str, project_root: Path | None) -> list[int]:
+ """仅匹配与本项目直播栈相关的 ffmpeg(命令行含 YouTube 推流、抖音拉流或项目路径),避免 killall 误伤整机其它 ffmpeg。"""
+ markers = _ffmpeg_cmdline_markers(process_name, project_root)
seen: set[int] = set()
proc_root = Path("/proc")
if not proc_root.is_dir():
@@ -97,16 +101,45 @@ def _signal_pids(pids: Iterable[int], sig: int) -> None:
pass
-async def force_kill_ffmpeg(process_name: str, project_root: Path | None = None) -> None:
- """结束与本业务相关的 ffmpeg:Linux 按 /proc 命令行匹配 + 可选 pkill;不再使用 killall -9 ffmpeg。"""
- if sys.platform == "win32":
- _ = process_name
- proc2 = await asyncio.create_subprocess_shell(
- "taskkill /IM ffmpeg.exe /F /T 2>nul || exit /b 0",
- stdout=asyncio.subprocess.DEVNULL,
- stderr=asyncio.subprocess.DEVNULL,
+def _windows_kill_stack_ffmpeg(process_name: str, project_root: Path | None) -> None:
+ """仅结束命令行命中业务标记的 ffmpeg.exe,避免 taskkill /IM 误杀其它 FFmpeg。"""
+ markers = _ffmpeg_cmdline_markers(process_name, project_root)
+ if not markers:
+ return
+ env = os.environ.copy()
+ env["DOUYIN_FFMPEG_MARKERS"] = json.dumps(markers, ensure_ascii=False)
+ ps = r"""
+$raw = $env:DOUYIN_FFMPEG_MARKERS
+if (-not $raw) { exit 0 }
+$m = $raw | ConvertFrom-Json
+foreach ($proc in Get-CimInstance Win32_Process -Filter "Name = 'ffmpeg.exe'") {
+ $cmd = $proc.CommandLine
+ if (-not $cmd) { continue }
+ foreach ($x in $m) {
+ if ($cmd -like ('*' + $x + '*')) {
+ Stop-Process -Id $proc.ProcessId -Force -ErrorAction SilentlyContinue
+ break
+ }
+ }
+}
+"""
+ try:
+ subprocess.run(
+ ["powershell", "-NoProfile", "-NonInteractive", "-Command", ps],
+ env=env,
+ capture_output=True,
+ text=True,
+ timeout=45,
+ check=False,
)
- await proc2.wait()
+ except (OSError, subprocess.TimeoutExpired):
+ pass
+
+
+async def force_kill_ffmpeg(process_name: str, project_root: Path | None = None) -> None:
+ """结束与本业务相关的 ffmpeg:Linux 按 /proc 命令行匹配 + 可选 pkill;Windows 按命令行标记筛选。"""
+ if sys.platform == "win32":
+ await asyncio.to_thread(_windows_kill_stack_ffmpeg, process_name, project_root)
return
def collect() -> list[int]:
diff --git a/src/youtube_ini_sync.py b/src/youtube_ini_sync.py
new file mode 100644
index 0000000..8bb0bd9
--- /dev/null
+++ b/src/youtube_ini_sync.py
@@ -0,0 +1,24 @@
+"""Write YouTube stream key into repo config/youtube.ini while preserving other keys."""
+from __future__ import annotations
+
+import configparser
+from pathlib import Path
+
+
+def write_youtube_ini_stream_key(repo_root: Path, stream_key_or_url: str) -> str:
+ root = repo_root.resolve()
+ cfg_path = root / "config" / "youtube.ini"
+ value = (stream_key_or_url or "").strip()
+ if not value:
+ return "sync skipped: empty youtube_rtmp_key"
+
+ cfg_path.parent.mkdir(parents=True, exist_ok=True)
+ parser = configparser.ConfigParser()
+ if cfg_path.is_file():
+ parser.read(cfg_path, encoding="utf-8")
+ if not parser.has_section("youtube"):
+ parser.add_section("youtube")
+ parser.set("youtube", "key", value)
+ with cfg_path.open("w", encoding="utf-8") as fh:
+ parser.write(fh)
+ return f"wrote {cfg_path}"
diff --git a/tiktok.py b/tiktok.py
index b7cd0be..82a202c 100644
--- a/tiktok.py
+++ b/tiktok.py
@@ -13,6 +13,8 @@ from __future__ import annotations
import asyncio
import os
+
+from src.async_thread_loop import run_async_coro
import sys
import builtins
import subprocess
@@ -673,27 +675,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平台")
@@ -701,23 +703,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
@@ -726,52 +728,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,
@@ -781,51 +783,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直播平台")
@@ -833,7 +835,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,
@@ -845,7 +847,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:
@@ -854,7 +856,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
))
@@ -862,7 +864,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,
@@ -882,7 +884,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,
@@ -890,7 +892,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(
@@ -901,36 +903,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直播平台")
@@ -938,7 +940,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直播平台")
@@ -946,113 +948,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'])
@@ -1060,16 +1062,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'
))
@@ -1077,7 +1079,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:
@@ -1085,34 +1087,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:
diff --git a/tools/deploy_live_paramiko.py b/tools/deploy_live_paramiko.py
index 58fe635..48d095a 100644
--- a/tools/deploy_live_paramiko.py
+++ b/tools/deploy_live_paramiko.py
@@ -26,14 +26,21 @@ REPO_ROOT = Path(__file__).resolve().parent.parent
FILES = [
"src/android_control.py",
+ "src/async_thread_loop.py",
+ "src/youtube_ini_sync.py",
"src/control_plane.py",
"src/web_process_backend.py",
"src/hub_routes.py",
"src/live_config/__init__.py",
"src/live_config/loader.py",
"web.py",
+ "youtube.py",
+ "tiktok.py",
"main.py",
"scripts/hardware_probe.py",
+ "config/hardware.env.example",
+ "ecosystem.config.cjs",
+ "docs/install-profiles.md",
"douyin_youtube_ffplay.py",
"scripts/linux/install_hub.sh",
"scripts/linux/reinstall_live_edge.sh",
@@ -58,6 +65,7 @@ FILES = [
"web-console/components/live-product/LiveControlApp.tsx",
"web-console/components/live-product/LiveAndroidWorkstation.tsx",
"web-console/components/live-product/LiveYoutubeUnmannedView.tsx",
+ "web-console/components/live-product/LiveProcessLiveLogCard.tsx",
"web-console/components/live-product/LiveTiktokHdmiView.tsx",
"web-console/lib/panelUrls.ts",
"web-console/lib/youtubeProChannels.ts",
@@ -222,8 +230,9 @@ def main() -> int:
_safe_print(f"已上传: {rel}")
py_compile = (
- f"cd {REMOTE_BASE} && python3 -m py_compile web.py "
+ f"cd {REMOTE_BASE} && python3 -m py_compile web.py youtube.py tiktok.py "
"src/control_plane.py src/web_process_backend.py src/hub_routes.py "
+ "src/async_thread_loop.py src/youtube_ini_sync.py "
"src/live_config/__init__.py src/live_config/loader.py "
"scripts/hardware_probe.py douyin_youtube_ffplay.py main.py"
)
diff --git a/web-console/components/live-product/LiveAndroidWorkstation.tsx b/web-console/components/live-product/LiveAndroidWorkstation.tsx
index e663b9d..11003f7 100644
--- a/web-console/components/live-product/LiveAndroidWorkstation.tsx
+++ b/web-console/components/live-product/LiveAndroidWorkstation.tsx
@@ -1,6 +1,6 @@
"use client";
-import { Copy, Layers, Loader2, Package, Terminal } from "lucide-react";
+import { Copy, ExternalLink, Layers, Loader2, Package, RefreshCw, Terminal } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
type DeviceRow = { serial: string; model?: string; state: string };
@@ -84,9 +84,14 @@ export function LiveAndroidWorkstation({
const [openUrl, setOpenUrl] = useState("https://");
const [adbInputText, setAdbInputText] = useState("");
const [mirrorMode, setMirrorMode] = useState<"host" | "webusb">("host");
+ const [mirrorReloadKey, setMirrorReloadKey] = useState(0);
+ const [logcatLive, setLogcatLive] = useState(false);
const [pending, setPending] = useState(null);
const PANDA_WEB_SCRCPY = "https://pandatestgrid.github.io/panda-web-scrcpy/";
+ const LINK_SCRCPY = "https://github.com/Genymobile/scrcpy";
+ const LINK_WS_SCRCPY = "https://github.com/NetrisTV/ws-scrcpy";
+ const LINK_PANDA_GH = "https://github.com/PandaTestGrid/panda-web-scrcpy";
const lock = !!(busy || pending);
@@ -157,26 +162,36 @@ export function LiveAndroidWorkstation({
}
};
- const refreshLogcat = async () => {
- if (!androidSerial) {
- notify(t("请选择设备", "Pick a device"));
- return;
- }
- const n = Math.min(3000, Math.max(1, parseInt(logcatLines, 10) || 200));
- setPending("logcat");
- setLogcatErr(null);
- try {
- const data = await fetchJson<{ text?: string }>(
- `/android/logcat?serial=${encodeURIComponent(androidSerial)}&lines=${n}`,
- );
- setLogcatText(data.text ?? "");
- } catch (e) {
- setLogcatText("");
- setLogcatErr((e as Error).message);
- } finally {
- setPending(null);
- }
- };
+ const refreshLogcat = useCallback(
+ async (opts?: { silent?: boolean }) => {
+ if (!androidSerial) {
+ return;
+ }
+ const n = Math.min(3000, Math.max(1, parseInt(logcatLines, 10) || 200));
+ const silent = !!opts?.silent;
+ if (!silent) setPending("logcat");
+ setLogcatErr(null);
+ try {
+ const data = await fetchJson<{ text?: string }>(
+ `/android/logcat?serial=${encodeURIComponent(androidSerial)}&lines=${n}`,
+ );
+ setLogcatText(data.text ?? "");
+ } catch (e) {
+ setLogcatText("");
+ setLogcatErr((e as Error).message);
+ } finally {
+ if (!silent) setPending(null);
+ }
+ },
+ [androidSerial, fetchJson, logcatLines],
+ );
+
+ useEffect(() => {
+ if (!logcatLive || !androidSerial || !adbAvailable) return;
+ const id = window.setInterval(() => void refreshLogcat({ silent: true }), 3000);
+ void refreshLogcat({ silent: true });
+ return () => clearInterval(id);
+ }, [logcatLive, androidSerial, adbAvailable, refreshLogcat]);
const loadUiNodes = async () => {
if (!androidSerial) {
@@ -260,10 +275,10 @@ export function LiveAndroidWorkstation({
))}
{adbAvailable && devices.length === 0 ? (
-
+
{t(
- "未检测到已授权设备:请 USB 连接并开启「USB 调试」,手机上允许本计算机调试。",
- "No authorized device: enable USB debugging and authorize this computer.",
+ "未检测到已授权设备:双头 USB 接板子时请开「USB 调试」并授权;Redroid / 容器安卓可先 `adb connect IP:5555` 再刷新本页。",
+ "No device: enable USB debugging for dual-USB to the board; for Redroid use adb connect IP:5555 then refresh.",
)}
) : null}
@@ -293,22 +308,40 @@ export function LiveAndroidWorkstation({
>
WebUSB · Panda
-
- GitHub
-
+
{mirrorMode === "host" ? (
-
+
{t(
- "手机接在 live 板子 USB 时使用本页:先在 ws-scrcpy 里选设备并连接;黑屏多为未连 WebSocket / 屏幕休眠——可先 Wake 再刷新或新窗口打开面板。",
- "USB to the board: pick device in ws-scrcpy; black screen often means WS down or screen off—Wake, refresh, or open panel in a new tab.",
+ "板载 USB:投屏协议来自开源 scrcpy(见上方仓库链接);浏览器内常用 ws-scrcpy。黑屏时先 Wake,再点「重载投屏」或新窗口打开面板。",
+ "Board USB: scrcpy protocol; browser uses ws-scrcpy. Black screen: Wake, reload mirror, or open panel in new tab.",
)}
@@ -323,19 +356,30 @@ export function LiveAndroidWorkstation({
) : null}
{scrcpyUrl ? (
-
- {t("新窗口打开 :5000", "Open :5000 tab")}
-
+ <>
+
setMirrorReloadKey((k) => k + 1)}
+ className="inline-flex items-center gap-1 rounded-lg border border-white/15 bg-black/30 px-3 py-1.5 text-[11px] text-zinc-200"
+ >
+
+ {t("重载投屏", "Reload mirror")}
+
+
+ {t("新窗口打开", "New tab")}
+
+ >
) : null}
{scrcpyUrl ? (
-
{t("抖音源站(业务元数据)", "Douyin source (hub metadata)")}
-
{t("写入 business JSON。", "Writes business JSON.")}
+
{t("业务元数据(配置中心)", "Hub business metadata")}
+
+ {t("与 YouTube 页共用一份 business JSON;HDMI/TikTok 场景可只填抖音 URL。", "Shared business JSON with YouTube page.")}
+
+
{t("抖音直播间 URL", "Douyin live URL")}
setDouyinUrl(e.target.value)}
placeholder="https://live.douyin.com/..."
/>
+
+ {t("YouTube 串流密钥(声明式,可选)", "YouTube key (optional declarative)")}
+
+
setBusinessYoutubeKey(e.target.value)}
+ placeholder="xxxx-xxxx…"
+ />
+
+ setSyncYoutubeIni(e.target.checked)}
+ />
+ {t("同时写入 config/youtube.ini 的 key=", "Also sync key= to config/youtube.ini")}
+
+
+
);
}
diff --git a/web-console/components/live-product/LiveYoutubeUnmannedView.tsx b/web-console/components/live-product/LiveYoutubeUnmannedView.tsx
index d41baf0..7caea65 100644
--- a/web-console/components/live-product/LiveYoutubeUnmannedView.tsx
+++ b/web-console/components/live-product/LiveYoutubeUnmannedView.tsx
@@ -9,6 +9,7 @@ import {
parseYoutubeStreamKeySuffix,
type YoutubeIniFields,
} from "@/lib/youtubeConfigUtils";
+import { LiveProcessLiveLogCard } from "@/components/live-product/LiveProcessLiveLogCard";
import {
defaultPm2NameForChannel,
loadYoutubeProChannels,
@@ -37,6 +38,10 @@ type Props = {
setUrlConfig: (v: string) => void;
douyinUrl: string;
setDouyinUrl: (v: string) => void;
+ businessYoutubeKey: string;
+ setBusinessYoutubeKey: (v: string) => void;
+ syncYoutubeIni: boolean;
+ setSyncYoutubeIni: (v: boolean) => void;
onRunProcess: (a: "start" | "stop" | "restart") => void;
onSaveUrlConfig: (content?: string) => void;
onSaveBusinessMeta: () => void;
@@ -53,6 +58,10 @@ export function LiveYoutubeUnmannedView({
setUrlConfig,
douyinUrl,
setDouyinUrl,
+ businessYoutubeKey,
+ setBusinessYoutubeKey,
+ syncYoutubeIni,
+ setSyncYoutubeIni,
onRunProcess,
onSaveUrlConfig,
onSaveBusinessMeta,
@@ -169,7 +178,8 @@ export function LiveYoutubeUnmannedView({
setYtIniLoading(true);
setYtIniStatus(null);
try {
- await fetchJson("/save_config", {
+ const params = new URLSearchParams({ process: currentProc });
+ await fetchJson(`/save_config?${params}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ content }),
@@ -236,8 +246,8 @@ export function LiveYoutubeUnmannedView({
{t(
- "对齐桌面客户端「直播」页:单路配置与多路 Pro 编排,无账号登录。",
- "Desktop-style live control: single or Pro lanes, no account login.",
+ "对齐 d2y 桌面懒人包「直播」页:单路 / Pro、无 Google 登录;Linux 推荐 PM2 或 systemd 托管下方所选进程。底部为实时日志(轮询 PM2/本地日志文件)。",
+ "Like d2y desktop: single/Pro, no Google login; on Linux use PM2/systemd for the selected process. Bottom: live logs from PM2/local log files.",
)}
@@ -497,12 +507,20 @@ export function LiveYoutubeUnmannedView({
{t("将本线路密钥写入服务器 youtube.ini", "Push key → server youtube.ini")}
-
- {t(
- "服务器仅一份 youtube.ini:推送会覆盖;多路并行请在板上为每路配置独立 PM2 与独立 config 目录,或轮流推送。",
- "Server has one youtube.ini; overwrite. For true parallel lanes use separate PM2 + config dirs on the host.",
- )}
-
+
+
+ {t("覆盖警告", "Overwrite warning")}
+
+
+ {t(
+ "服务器上通常只有一份 youtube.ini:点此推送会立刻覆盖其中 key=;多路并行请在板上为每路使用独立 PM2 与独立 config 目录,或轮流推送后再开播。",
+ "Hosts usually have a single youtube.ini: this overwrites key=. For parallel lanes use separate PM2 + config dirs per lane, or push then start one at a time.",
+ )}
+
+
)}
+
+
);
}
diff --git a/web-console/lib/youtubeConfigUtils.ts b/web-console/lib/youtubeConfigUtils.ts
index 509011e..e76b51a 100644
--- a/web-console/lib/youtubeConfigUtils.ts
+++ b/web-console/lib/youtubeConfigUtils.ts
@@ -43,6 +43,24 @@ export function extractDouyinLiveUrls(urlIni: string): string[] {
return Array.from(new Set(out)).slice(0, 16);
}
+/** URL_config 文本中提取抖音房间线索(与 d2y electron 一致) */
+export function extractDouyinRoomHints(urlIni: string): string[] {
+ const out: string[] = [];
+ for (const line of urlIni.split(/\r?\n/)) {
+ const t = line.trim();
+ if (!t || t.startsWith("#")) continue;
+ if (!/douyin\.com|v\.douyin\.com/i.test(t)) continue;
+ const m = t.match(/live\.douyin\.com\/([a-zA-Z0-9_-]+)/i);
+ if (m) {
+ out.push(m[1]);
+ continue;
+ }
+ const n = t.match(/(\d{8,})/);
+ out.push(n ? n[1] : t.slice(0, 96));
+ }
+ return Array.from(new Set(out)).slice(0, 12);
+}
+
export type YoutubeIniFields = {
key: string;
rtmps: boolean;
diff --git a/web.py b/web.py
index f1a45c3..b5f019c 100644
--- a/web.py
+++ b/web.py
@@ -102,8 +102,10 @@ def discover_script_entries() -> tuple:
if path.is_file() and path.resolve() != web_path:
add(path.name, "youtube")
+ # douyin_youtube_ffplay.py 为推流实现库,由 youtube.py 动态 import;单独执行会立即退出,勿当作 PM2/控制台进程。
+ _douyin_youtube_skip = frozenset({"douyin_youtube_ffplay.py"})
for path in sorted(base.glob("douyin_youtube*.py")):
- if path.is_file() and path.resolve() != web_path:
+ if path.is_file() and path.resolve() != web_path and path.name not in _douyin_youtube_skip:
add(path.name, "douyin_youtube")
if sys.platform == "win32":
@@ -178,7 +180,7 @@ async def lifespan(app: FastAPI):
app = FastAPI(title="多进程录制控制台", lifespan=lifespan)
-configure_hub(process_backend, list(PROCESS_MONITOR))
+configure_hub(process_backend, list(PROCESS_MONITOR), BASE_DIR)
app.include_router(hub_router)
app.add_middleware(
diff --git a/youtube.py b/youtube.py
index 4c801f7..343ca4c 100644
--- a/youtube.py
+++ b/youtube.py
@@ -13,6 +13,8 @@ from __future__ import annotations
import asyncio
import os
+
+from src.async_thread_loop import run_async_coro
import sys
import builtins
import subprocess
@@ -673,27 +675,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平台")
@@ -701,23 +703,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
@@ -726,52 +728,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,
@@ -781,51 +783,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直播平台")
@@ -833,7 +835,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,
@@ -845,7 +847,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:
@@ -854,7 +856,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
))
@@ -862,7 +864,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,
@@ -882,7 +884,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,
@@ -890,7 +892,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(
@@ -901,36 +903,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直播平台")
@@ -938,7 +940,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直播平台")
@@ -946,113 +948,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'])
@@ -1060,16 +1062,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'
))
@@ -1077,7 +1079,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:
@@ -1085,34 +1087,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:
@@ -1639,9 +1641,8 @@ def start_record(url_data: tuple, count_variable: int = -1) -> None:
recording=recording,
recording_time_list=recording_time_list,
running_list=running_list,
- monitoring=monitoring, # 假设外部 monitoring 是 list,如 [count]
clear_record_info=clear_record_info,
- color_obj=color_obj
+ color_obj=color_obj,
)
count_time = time.time()