This commit is contained in:
eric
2026-03-29 02:57:08 -05:00
parent ef87c43b33
commit e3ac026710
15 changed files with 799 additions and 222 deletions

78
web.py
View File

@@ -1,6 +1,7 @@
from __future__ import annotations
import asyncio
import json
import re
import sys
from contextlib import asynccontextmanager
@@ -9,7 +10,7 @@ from typing import Optional
from pydantic import BaseModel, Field
from fastapi import FastAPI, Query, Request
from fastapi import FastAPI, Query, Request, WebSocket
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse, JSONResponse, Response
from fastapi.staticfiles import StaticFiles
@@ -28,6 +29,7 @@ from src.android_control import (
android_ui_nodes,
list_android_devices,
)
from src.scrcpy_stream import cleanup_scrcpy_session, open_scrcpy_h264_stream, scrcpy_stream_info
from src.control_plane import (
delete_root_file,
list_root_files,
@@ -480,6 +482,80 @@ async def get_android_display_metrics(serial: str = Query(..., description="Andr
return JSONResponse({"error": str(exc)}, status_code=400)
@app.get("/android/scrcpy/info")
async def get_android_scrcpy_info():
"""是否具备 scrcpy-server 与版本提示(真 · WebSocket 流依赖此 jar"""
return scrcpy_stream_info()
@app.websocket("/android/scrcpy/ws")
async def android_scrcpy_ws(websocket: WebSocket):
"""浏览器 WebSocket推送设备端 scrcpy-server raw_stream H.264 字节流(与官方 scrcpy 同源编码)。"""
await websocket.accept()
serial = (websocket.query_params.get("serial") or "").strip()
ms_raw = (websocket.query_params.get("max_size") or "1920").strip()
try:
max_size = max(0, min(int(ms_raw), 8192))
except ValueError:
max_size = 1920
writer: asyncio.StreamWriter | None = None
port: int | None = None
try:
port, _scid, reader, writer = await open_scrcpy_h264_stream(serial, max_size=max_size)
except ValueError as exc:
await websocket.send_text(json.dumps({"error": str(exc), "phase": "serial"}))
await websocket.close(code=4400)
return
except Exception as exc:
await websocket.send_text(json.dumps({"error": str(exc), "phase": "setup"}))
await websocket.close(code=4500)
return
async def pump() -> None:
try:
while True:
chunk = await reader.read(64 * 1024)
if not chunk:
break
await websocket.send_bytes(chunk)
except Exception:
pass
async def wait_client_disconnect() -> None:
try:
while True:
msg = await websocket.receive()
if msg.get("type") == "websocket.disconnect":
return
except Exception:
return
pump_task = asyncio.create_task(pump())
disc_task = asyncio.create_task(wait_client_disconnect())
try:
_done, pending = await asyncio.wait(
{pump_task, disc_task},
return_when=asyncio.FIRST_COMPLETED,
)
for t in pending:
t.cancel()
await asyncio.gather(*pending, return_exceptions=True)
finally:
if not pump_task.done():
pump_task.cancel()
await asyncio.gather(pump_task, return_exceptions=True)
if not disc_task.done():
disc_task.cancel()
await asyncio.gather(disc_task, return_exceptions=True)
if port is not None:
await cleanup_scrcpy_session(port, writer)
try:
await websocket.close()
except Exception:
pass
@app.post("/android/action")
async def post_android_action(request: Request):
try: