diff --git a/.gitignore b/.gitignore index 73b5482..920b054 100644 --- a/.gitignore +++ b/.gitignore @@ -173,6 +173,9 @@ node_modules/ # Next 开发缓存(静态导出目录 web-console/out 可纳入版本库以便无 Node 环境直接部署) web-console/.next/ +# scrcpy-server:由服务启动或 Docker 构建从 Genymobile Release 自动拉取(可纳入制品库而不提交) +vendor/scrcpy-server.jar + # Paramiko 部署本地日志(可能含敏感命令回显) .deploy_logs/ diff --git a/Dockerfile b/Dockerfile index 860a6bd..7213a01 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,5 +24,8 @@ RUN pip install --no-cache-dir -r requirements.txt COPY . . COPY --from=console /src/web-console/out ./web-console/out +# 构建期尝试拉取 scrcpy-server;失败不阻断构建(容器启动时 lifespan 会再试) +RUN sh -c "python -m src.scrcpy_vendor || echo '[docker] scrcpy-server: build-time fetch skipped'" + EXPOSE 8101 CMD ["sh", "-c", "exec python -m uvicorn web:app --host 0.0.0.0 --port ${PORT}"] diff --git a/src/scrcpy_stream.py b/src/scrcpy_stream.py index 9e4269f..ba57882 100644 --- a/src/scrcpy_stream.py +++ b/src/scrcpy_stream.py @@ -17,6 +17,7 @@ from pathlib import Path from typing import Any from src.android_control import _adb_prefix, _adb_text, _run_cmd, adb_available +from src.scrcpy_vendor import ensure_scrcpy_jar BASE_DIR = Path(__file__).resolve().parent.parent REMOTE_JAR = "/data/local/tmp/scrcpy-web-hub.jar" @@ -65,10 +66,18 @@ def scrcpy_stream_info() -> dict[str, Any]: "server_path": str(jar) if jar else None, "server_version": ver, "remote_jar": REMOTE_JAR, - "hint_zh": "将官方 scrcpy 发行包中的 scrcpy-server(或 scrcpy-server.jar)放到项目 vendor/scrcpy-server.jar," - "或设置环境变量 SCRCPY_SERVER_JAR;版本字符串须与该 jar 完全一致(默认可配 vendor/scrcpy-server.version 或 SCRCPY_SERVER_VERSION)。", - "hint_en": "Place scrcpy-server from the official scrcpy release in vendor/scrcpy-server.jar, " - "or set SCRCPY_SERVER_JAR; the version string must match the jar (vendor/scrcpy-server.version or SCRCPY_SERVER_VERSION).", + "hint_zh": ( + "服务启动时会尝试从 Genymobile 官方 Release 拉取与 vendor/scrcpy-server.version 匹配的 scrcpy-server;" + "离线环境可预置 vendor/scrcpy-server.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." + if not jar + else "scrcpy-server is ready." + ), } @@ -156,9 +165,12 @@ async def open_scrcpy_h264_stream( raise RuntimeError("adb is not installed") jar = resolve_scrcpy_server_jar() + if not jar: + ensure_scrcpy_jar(BASE_DIR) + jar = resolve_scrcpy_server_jar() if not jar: raise RuntimeError( - "scrcpy-server not found; set SCRCPY_SERVER_JAR or add vendor/scrcpy-server.jar", + "scrcpy-server 不可用:自动拉取失败或处于离线;请检查网络后重启服务,或预置 vendor/scrcpy-server.jar", ) version = default_server_version() diff --git a/src/scrcpy_vendor.py b/src/scrcpy_vendor.py new file mode 100644 index 0000000..e90519d --- /dev/null +++ b/src/scrcpy_vendor.py @@ -0,0 +1,85 @@ +"""自动准备官方 scrcpy-server(与 vendor/scrcpy-server.version 版本一致),供 Web 真流 adb push 使用。""" +from __future__ import annotations + +import os +import sys +import urllib.error +import urllib.request +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 + + +def _read_version(repo_root: Path) -> str: + env = (os.environ.get("SCRCPY_SERVER_VERSION") or "").strip().lstrip("v") + if env: + return env + vf = repo_root / "vendor" / "scrcpy-server.version" + if vf.is_file(): + line = vf.read_text(encoding="utf-8").splitlines()[0].strip().lstrip("v") + if line: + return line + return "3.3.3" + + +def ensure_scrcpy_jar(repo_root: Path) -> tuple[bool, str]: + """ + 若 vendor/scrcpy-server.jar 不存在或过小,则从 Genymobile GitHub Release 下载。 + 可通过环境变量 SKIP_SCRCPY_SERVER_FETCH=1 跳过。 + """ + if os.environ.get("SKIP_SCRCPY_SERVER_FETCH", "").strip().lower() in ("1", "true", "yes", "on"): + return True, "skip fetch (SKIP_SCRCPY_SERVER_FETCH)" + + vendor_dir = repo_root / "vendor" + dest = vendor_dir / "scrcpy-server.jar" + env_jar = (os.environ.get("SCRCPY_SERVER_JAR") or "").strip() + if env_jar: + p = Path(env_jar).expanduser() + if p.is_file() and p.stat().st_size >= _MIN_BYTES: + return True, "using SCRCPY_SERVER_JAR" + + if dest.is_file() and dest.stat().st_size >= _MIN_BYTES: + return True, "vendor/scrcpy-server.jar ok" + + tag = _read_version(repo_root) + url = _GITHUB_DL.format(tag=tag) + 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}" + + 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" + + +def main() -> int: + root = Path(__file__).resolve().parent.parent + ok, msg = ensure_scrcpy_jar(root) + print(msg, file=sys.stderr if not ok else sys.stdout) + return 0 if ok else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/web-console/components/live-product/LiveAndroidStreamConsole.tsx b/web-console/components/live-product/LiveAndroidStreamConsole.tsx index cfcaf31..58a5e35 100644 --- a/web-console/components/live-product/LiveAndroidStreamConsole.tsx +++ b/web-console/components/live-product/LiveAndroidStreamConsole.tsx @@ -881,7 +881,10 @@ export function LiveAndroidStreamConsole({ {scrcpyInfo && !scrcpyInfo.server_jar ? (
- {t("未检测到 scrcpy-server:请放置 vendor/scrcpy-server.jar 或设置 SCRCPY_SERVER_JAR。", "No scrcpy-server jar: add vendor/scrcpy-server.jar or SCRCPY_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.", + )} {scrcpyInfo.server_version ? ` (${t("期望版本", "version")} ${scrcpyInfo.server_version})` : null}
) : null} diff --git a/web.py b/web.py index 7fb683b..93226f6 100644 --- a/web.py +++ b/web.py @@ -30,6 +30,7 @@ from src.android_control import ( list_android_devices, ) from src.scrcpy_stream import cleanup_scrcpy_session, open_scrcpy_h264_stream, scrcpy_stream_info +from src.scrcpy_vendor import ensure_scrcpy_jar from src.control_plane import ( delete_root_file, list_root_files, @@ -341,6 +342,12 @@ async def lifespan(app: FastAPI): except Exception: # PM2/Shell 异常时不阻塞整站启动(否则反代持续 502) pass + try: + ok, msg = await asyncio.to_thread(ensure_scrcpy_jar, BASE_DIR) + if not ok: + print(f"[web] scrcpy-server: {msg}", file=sys.stderr) + except Exception as exc: + print(f"[web] scrcpy-server bootstrap: {exc}", file=sys.stderr) yield