Files
gitlab-instance-0a899031_do…/src/scrcpy_stream.py
2026-03-29 03:51:50 -05:00

210 lines
7.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""在宿主上拉起官方 scrcpy-serverraw_stream经 ADB forward 读 H.264,再推到 WebSocket。
与桌面端 [escrcpy](https://github.com/viarotel-org/escrcpy) / [scrcpy](https://github.com/Genymobile/scrcpy)
使用同一套 scrcpy-server版本号须与 jar 内 BuildConfig 完全一致(见官方 doc/develop.md
"""
from __future__ import annotations
import asyncio
import json
import os
import random
import re
import shutil
import socket
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"
_VENDOR_JAR = BASE_DIR / "vendor" / "scrcpy-server.jar"
_VERSION_FILE = BASE_DIR / "vendor" / "scrcpy-server.version"
def default_server_version() -> str:
v = (os.environ.get("SCRCPY_SERVER_VERSION") or "").strip()
if v:
return v
try:
if _VERSION_FILE.is_file():
line = _VERSION_FILE.read_text(encoding="utf-8").splitlines()[0].strip()
if line:
return line
except OSError:
pass
return "3.3.3"
def resolve_scrcpy_server_jar() -> Path | None:
env = (os.environ.get("SCRCPY_SERVER_JAR") or "").strip()
if env:
p = Path(env).expanduser()
if p.is_file():
return p.resolve()
if _VENDOR_JAR.is_file():
return _VENDOR_JAR.resolve()
exe = shutil.which("scrcpy")
if exe:
parent = Path(exe).resolve().parent
for name in ("scrcpy-server", "scrcpy-server.jar"):
cand = parent / name
if cand.is_file():
return cand.resolve()
return None
def scrcpy_stream_info() -> dict[str, Any]:
jar = resolve_scrcpy_server_jar()
ver = default_server_version()
return {
"adb_ok": adb_available(),
"server_jar": jar is not None,
"server_path": str(jar) if jar else None,
"server_version": ver,
"remote_jar": REMOTE_JAR,
"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."
),
}
_SERIAL_SAFE = re.compile(r"^[A-Za-z0-9._:\-]+$")
def _validate_serial(serial: str) -> str:
s = (serial or "").strip()
if not s or len(s) > 256 or not _SERIAL_SAFE.match(s):
raise ValueError("Invalid device serial")
return s
def _pick_local_port() -> int:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
s.bind(("127.0.0.1", 0))
_host, port = s.getsockname()
return int(port)
finally:
s.close()
async def _adb_forward_add(serial: str, local_port: int, socket_suffix: str) -> None:
spec = f"tcp:{local_port}"
abstract = f"localabstract:scrcpy_{socket_suffix}"
code, _out, err = await _adb_text(serial, "forward", spec, abstract, timeout=25.0)
if code != 0:
raise RuntimeError(err or f"adb forward {spec} {abstract} failed")
async def _adb_forward_remove(local_port: int) -> None:
spec = f"tcp:{local_port}"
await _adb_text(None, "forward", "--remove", spec, timeout=15.0)
async def _push_server(serial: str, jar: Path) -> None:
code, _out, err = await _run_cmd(
_adb_prefix(serial) + ["push", str(jar), REMOTE_JAR],
timeout=120.0,
)
if code != 0:
raise RuntimeError(err or "adb push scrcpy-server failed")
async def _start_server_process(serial: str, version: str, scid_hex: str, max_size: int) -> asyncio.subprocess.Process:
"""在设备上后台启动 app_processtunnel_forward 模式下等待宿主机连入 forward 端口。"""
# 与 DesktopConnection.getSocketName 一致scrcpy_%08x
args_line = (
f"{version} tunnel_forward=true audio=false control=false cleanup=false raw_stream=true "
f"max_size={max_size} scid={scid_hex}"
)
# 使用 sh -c 与后台 &,避免阻塞 adb shell
inner = (
f"CLASSPATH={REMOTE_JAR} /system/bin/app_process / com.genymobile.scrcpy.Server {args_line} "
">/dev/null 2>&1 &"
)
proc = await asyncio.create_subprocess_exec(
*_adb_prefix(serial),
"shell",
"sh",
"-c",
inner,
stdout=asyncio.subprocess.DEVNULL,
stderr=asyncio.subprocess.PIPE,
)
_stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=15.0)
rc = int(proc.returncode or 0)
if rc != 0:
msg = (stderr or b"").decode("utf-8", "replace").strip()
raise RuntimeError(msg or f"app_process start failed (code {rc})")
return proc
async def open_scrcpy_h264_stream(
serial: str,
*,
max_size: int = 1920,
connect_attempts: int = 22,
connect_delay: float = 0.28,
) -> tuple[int, str, asyncio.StreamReader, asyncio.StreamWriter]:
"""建立 forward + 启动 server并返回 (local_port, scid_hex, reader, writer)。"""
_validate_serial(serial)
if not adb_available():
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 不可用:自动拉取失败或处于离线;请检查网络后重启服务,或预置 vendor/scrcpy-server.jar",
)
version = default_server_version()
scid = random.randint(0x1000_0000, 0x7FFF_FFFF)
scid_hex = f"{scid:08x}"
port = _pick_local_port()
await _push_server(serial, jar)
await _adb_forward_add(serial, port, scid_hex)
await _start_server_process(serial, version, scid_hex, max(0, min(int(max_size), 8192)))
last_err: str | None = None
for attempt in range(connect_attempts):
await asyncio.sleep(connect_delay)
try:
reader, writer = await asyncio.wait_for(
asyncio.open_connection("127.0.0.1", port),
timeout=4.0,
)
return port, scid_hex, reader, writer
except (OSError, asyncio.TimeoutError) as exc:
last_err = str(exc)
continue
await _adb_forward_remove(port)
raise RuntimeError(last_err or "Could not connect to scrcpy forward port")
async def cleanup_scrcpy_session(local_port: int, writer: asyncio.StreamWriter | None) -> None:
if writer is not None and not writer.is_closing():
writer.close()
try:
await writer.wait_closed()
except OSError:
pass
await _adb_forward_remove(local_port)