's'
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -173,6 +173,9 @@ node_modules/
|
|||||||
# Next 开发缓存(静态导出目录 web-console/out 可纳入版本库以便无 Node 环境直接部署)
|
# Next 开发缓存(静态导出目录 web-console/out 可纳入版本库以便无 Node 环境直接部署)
|
||||||
web-console/.next/
|
web-console/.next/
|
||||||
|
|
||||||
|
# scrcpy-server:由服务启动或 Docker 构建从 Genymobile Release 自动拉取(可纳入制品库而不提交)
|
||||||
|
vendor/scrcpy-server.jar
|
||||||
|
|
||||||
# Paramiko 部署本地日志(可能含敏感命令回显)
|
# Paramiko 部署本地日志(可能含敏感命令回显)
|
||||||
.deploy_logs/
|
.deploy_logs/
|
||||||
|
|
||||||
|
|||||||
@@ -24,5 +24,8 @@ RUN pip install --no-cache-dir -r requirements.txt
|
|||||||
COPY . .
|
COPY . .
|
||||||
COPY --from=console /src/web-console/out ./web-console/out
|
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
|
EXPOSE 8101
|
||||||
CMD ["sh", "-c", "exec python -m uvicorn web:app --host 0.0.0.0 --port ${PORT}"]
|
CMD ["sh", "-c", "exec python -m uvicorn web:app --host 0.0.0.0 --port ${PORT}"]
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ from pathlib import Path
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from src.android_control import _adb_prefix, _adb_text, _run_cmd, adb_available
|
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
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
REMOTE_JAR = "/data/local/tmp/scrcpy-web-hub.jar"
|
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_path": str(jar) if jar else None,
|
||||||
"server_version": ver,
|
"server_version": ver,
|
||||||
"remote_jar": REMOTE_JAR,
|
"remote_jar": REMOTE_JAR,
|
||||||
"hint_zh": "将官方 scrcpy 发行包中的 scrcpy-server(或 scrcpy-server.jar)放到项目 vendor/scrcpy-server.jar,"
|
"hint_zh": (
|
||||||
"或设置环境变量 SCRCPY_SERVER_JAR;版本字符串须与该 jar 完全一致(默认可配 vendor/scrcpy-server.version 或 SCRCPY_SERVER_VERSION)。",
|
"服务启动时会尝试从 Genymobile 官方 Release 拉取与 vendor/scrcpy-server.version 匹配的 scrcpy-server;"
|
||||||
"hint_en": "Place scrcpy-server from the official scrcpy release in vendor/scrcpy-server.jar, "
|
"离线环境可预置 vendor/scrcpy-server.jar 或设置 SCRCPY_SERVER_JAR。"
|
||||||
"or set SCRCPY_SERVER_JAR; the version string must match the jar (vendor/scrcpy-server.version or SCRCPY_SERVER_VERSION).",
|
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")
|
raise RuntimeError("adb is not installed")
|
||||||
|
|
||||||
jar = resolve_scrcpy_server_jar()
|
jar = resolve_scrcpy_server_jar()
|
||||||
|
if not jar:
|
||||||
|
ensure_scrcpy_jar(BASE_DIR)
|
||||||
|
jar = resolve_scrcpy_server_jar()
|
||||||
if not jar:
|
if not jar:
|
||||||
raise RuntimeError(
|
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()
|
version = default_server_version()
|
||||||
|
|||||||
85
src/scrcpy_vendor.py
Normal file
85
src/scrcpy_vendor.py
Normal file
@@ -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())
|
||||||
@@ -881,7 +881,10 @@ export function LiveAndroidStreamConsole({
|
|||||||
</div>
|
</div>
|
||||||
{scrcpyInfo && !scrcpyInfo.server_jar ? (
|
{scrcpyInfo && !scrcpyInfo.server_jar ? (
|
||||||
<p className="mt-2 text-[10px] text-amber-200/80">
|
<p className="mt-2 text-[10px] text-amber-200/80">
|
||||||
{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}
|
{scrcpyInfo.server_version ? ` (${t("期望版本", "version")} ${scrcpyInfo.server_version})` : null}
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
7
web.py
7
web.py
@@ -30,6 +30,7 @@ from src.android_control import (
|
|||||||
list_android_devices,
|
list_android_devices,
|
||||||
)
|
)
|
||||||
from src.scrcpy_stream import cleanup_scrcpy_session, open_scrcpy_h264_stream, scrcpy_stream_info
|
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 (
|
from src.control_plane import (
|
||||||
delete_root_file,
|
delete_root_file,
|
||||||
list_root_files,
|
list_root_files,
|
||||||
@@ -341,6 +342,12 @@ async def lifespan(app: FastAPI):
|
|||||||
except Exception:
|
except Exception:
|
||||||
# PM2/Shell 异常时不阻塞整站启动(否则反代持续 502)
|
# PM2/Shell 异常时不阻塞整站启动(否则反代持续 502)
|
||||||
pass
|
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
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user