diff --git a/config/runtime.env.example b/config/runtime.env.example index 7690f81..4134559 100644 --- a/config/runtime.env.example +++ b/config/runtime.env.example @@ -10,3 +10,9 @@ 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 diff --git a/docs/install-profiles.md b/docs/install-profiles.md index 30840b2..1a66be3 100644 --- a/docs/install-profiles.md +++ b/docs/install-profiles.md @@ -34,8 +34,19 @@ Use this when the device should only run the Douyin or YouTube business stack. - 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. + ### PM2:单频道与多频道 Pro(Linux ARM / x86_64) - **单频道**:在项目根启用一个业务进程即可,例如取消注释 `ecosystem.config.cjs` 里的 `youtube` 或 `tiktok`,执行 `pm2 start ecosystem.config.cjs && pm2 save`。 diff --git a/scripts/linux/install_business.sh b/scripts/linux/install_business.sh index 8c41e8d..74dca6d 100644 --- a/scripts/linux/install_business.sh +++ b/scripts/linux/install_business.sh @@ -30,4 +30,6 @@ install_browser_stack install_neko_browser prepare_srs run_hardware_probe +# 与 install-hub 一致:装 Caddy live-edge,http://live.local:80 → 本机控制平面 PORT(默认 8001) +install_live_edge_proxy print_summary "business" diff --git a/src/scrcpy_stream.py b/src/scrcpy_stream.py index ba57882..85b01b1 100644 --- a/src/scrcpy_stream.py +++ b/src/scrcpy_stream.py @@ -67,14 +67,14 @@ def scrcpy_stream_info() -> dict[str, Any]: "server_version": ver, "remote_jar": REMOTE_JAR, "hint_zh": ( - "服务启动时会尝试从 Genymobile 官方 Release 拉取与 vendor/scrcpy-server.version 匹配的 scrcpy-server;" - "离线环境可预置 vendor/scrcpy-server.jar 或设置 SCRCPY_SERVER_JAR。" + "启动时会拉取与 vendor/scrcpy-server.version 匹配的 scrcpy-server(默认同源 GitHub);" + "内网可设 SCRCPY_SERVER_DOWNLOAD_URL(支持 {tag});离线可预置 jar 或 SCRCPY_SERVER_JAR。" if not jar else "scrcpy-server 已就绪。" ), "hint_en": ( - "On startup the hub tries to download the matching scrcpy-server from Genymobile releases; " - "for offline use place vendor/scrcpy-server.jar or set SCRCPY_SERVER_JAR." + "Startup fetches scrcpy-server for vendor/scrcpy-server.version (default: GitHub). " + "Use SCRCPY_SERVER_DOWNLOAD_URL with {tag} for mirrors; offline: vendor jar or SCRCPY_SERVER_JAR." if not jar else "scrcpy-server is ready." ), diff --git a/src/scrcpy_vendor.py b/src/scrcpy_vendor.py index e90519d..bf02967 100644 --- a/src/scrcpy_vendor.py +++ b/src/scrcpy_vendor.py @@ -3,6 +3,7 @@ from __future__ import annotations import os import sys +import time import urllib.error import urllib.request from pathlib import Path @@ -10,6 +11,8 @@ from pathlib import Path # 官方发行资产名:scrcpy-server-v{version}(无扩展名,实为 JAR) _GITHUB_DL = "https://github.com/Genymobile/scrcpy/releases/download/v{tag}/scrcpy-server-v{tag}" _MIN_BYTES = 8_000 +_PER_URL_ATTEMPTS = 3 +_PER_URL_DELAY_S = 2.0 def _read_version(repo_root: Path) -> str: @@ -24,10 +27,70 @@ def _read_version(repo_root: Path) -> str: return "3.3.3" +def _expand_download_url(template: str, tag: str) -> str: + return template.replace("{tag}", tag).replace("{version}", tag) + + +def _download_urls_to_try(tag: str) -> list[str]: + """顺序:SCRCPY_SERVER_DOWNLOAD_URL → SCRCPY_SERVER_DOWNLOAD_URLS →(默认可选)GitHub。""" + urls: list[str] = [] + single = (os.environ.get("SCRCPY_SERVER_DOWNLOAD_URL") or "").strip() + if single: + urls.append(_expand_download_url(single, tag)) + raw = (os.environ.get("SCRCPY_SERVER_DOWNLOAD_URLS") or "").strip() + if raw: + for part in raw.split(","): + u = part.strip() + if not u: + continue + uu = _expand_download_url(u, tag) + if uu not in urls: + urls.append(uu) + no_github = os.environ.get("SCRCPY_SERVER_DOWNLOAD_NO_GITHUB", "").strip().lower() in ( + "1", + "true", + "yes", + "on", + ) + gh = _GITHUB_DL.format(tag=tag) + if not no_github and gh not in urls: + urls.append(gh) + seen: set[str] = set() + out: list[str] = [] + for u in urls: + if u not in seen: + seen.add(u) + out.append(u) + return out + + +def _fetch_url(url: str) -> tuple[bool, bytes | None, str]: + req = urllib.request.Request( + url, + headers={"User-Agent": "douyinyoutube-live-hub/scrcpy-bootstrap"}, + method="GET", + ) + last_err = "" + for attempt in range(1, _PER_URL_ATTEMPTS + 1): + try: + with urllib.request.urlopen(req, timeout=120) as resp: + data = resp.read() + return True, data, "" + except (urllib.error.HTTPError, urllib.error.URLError, TimeoutError, OSError) as e: + last_err = str(e) + if attempt < _PER_URL_ATTEMPTS: + time.sleep(_PER_URL_DELAY_S) + return False, None, last_err + + def ensure_scrcpy_jar(repo_root: Path) -> tuple[bool, str]: """ - 若 vendor/scrcpy-server.jar 不存在或过小,则从 Genymobile GitHub Release 下载。 - 可通过环境变量 SKIP_SCRCPY_SERVER_FETCH=1 跳过。 + 若 vendor/scrcpy-server.jar 不存在或过小,则按 URL 列表下载。 + 环境变量: + SKIP_SCRCPY_SERVER_FETCH=1 — 跳过 + SCRCPY_SERVER_DOWNLOAD_URL — 优先下载地址,可用 {tag}/{version} + SCRCPY_SERVER_DOWNLOAD_URLS — 逗号分隔多条,先于 GitHub + SCRCPY_SERVER_DOWNLOAD_NO_GITHUB=1 — 仅用上面自定义 URL,不回退 GitHub """ if os.environ.get("SKIP_SCRCPY_SERVER_FETCH", "").strip().lower() in ("1", "true", "yes", "on"): return True, "skip fetch (SKIP_SCRCPY_SERVER_FETCH)" @@ -44,34 +107,32 @@ def ensure_scrcpy_jar(repo_root: Path) -> tuple[bool, str]: return True, "vendor/scrcpy-server.jar ok" tag = _read_version(repo_root) - url = _GITHUB_DL.format(tag=tag) + urls = _download_urls_to_try(tag) + if not urls: + return False, "未配置任何下载源(SCRCPY_SERVER_DOWNLOAD_NO_GITHUB=1 且未设置 SCRCPY_SERVER_DOWNLOAD_URL(S))" vendor_dir.mkdir(parents=True, exist_ok=True) tmp = dest.with_suffix(".jar.part") - try: - req = urllib.request.Request( - url, - headers={"User-Agent": "douyinyoutube-live-hub/scrcpy-bootstrap"}, - method="GET", - ) - with urllib.request.urlopen(req, timeout=120) as resp: - data = resp.read() - except (urllib.error.HTTPError, urllib.error.URLError, TimeoutError, OSError) as e: - return False, f"download failed ({url}): {e}" + last_fail = "" + for url in urls: + ok, data, err = _fetch_url(url) + if not ok or data is None: + last_fail = f"{url}: {err}" + continue + if len(data) < _MIN_BYTES: + last_fail = f"{url}: too small ({len(data)} bytes)" + continue + if not data.startswith(b"PK"): + last_fail = f"{url}: not a JAR (missing PK header)" + continue + try: + tmp.write_bytes(data) + os.replace(tmp, dest) + except OSError as e: + return False, f"write vendor/scrcpy-server.jar failed: {e}" + return True, f"fetched scrcpy-server v{tag} -> vendor/scrcpy-server.jar (via {url})" - if len(data) < _MIN_BYTES: - return False, f"download too small ({len(data)} bytes), check version {tag}" - - if not data.startswith(b"PK"): - return False, "downloaded file does not look like a JAR (expected PK header)" - - try: - tmp.write_bytes(data) - os.replace(tmp, dest) - except OSError as e: - return False, f"write vendor/scrcpy-server.jar failed: {e}" - - return True, f"fetched scrcpy-server v{tag} -> vendor/scrcpy-server.jar" + return False, f"download failed after {len(urls)} source(s): {last_fail}" def main() -> int: diff --git a/web-console/components/live-product/LiveAndroidStreamConsole.tsx b/web-console/components/live-product/LiveAndroidStreamConsole.tsx index 58a5e35..b4e3d21 100644 --- a/web-console/components/live-product/LiveAndroidStreamConsole.tsx +++ b/web-console/components/live-product/LiveAndroidStreamConsole.tsx @@ -882,8 +882,8 @@ export function LiveAndroidStreamConsole({ {scrcpyInfo && !scrcpyInfo.server_jar ? (

{t( - "未检测到 scrcpy-server:服务启动时会自动从官方 Release 拉取;若仍失败请检查宿主机出网或重启 web 进程。", - "scrcpy-server missing: it is fetched on hub startup from official releases; check outbound HTTPS or restart web.", + "未检测到 scrcpy-server:启动时会自动拉取;内网可在 runtime.env 配置 SCRCPY_SERVER_DOWNLOAD_URL(支持 {tag})。", + "scrcpy-server missing: auto-fetched on startup; set SCRCPY_SERVER_DOWNLOAD_URL ({tag}) for mirrors.", )} {scrcpyInfo.server_version ? ` (${t("期望版本", "version")} ${scrcpyInfo.server_version})` : null}