阶段2: USB 远程 adb server 打通(220 host 网络 5037 全接口监听,无需改 220)+ 自建远程看屏(MJPEG 流 + u2 触控 tap/swipe/key/text,ws-scrcpy 不在 npm 改自建)
This commit is contained in:
+123
@@ -974,6 +974,129 @@ def api_uiauto_screenshot():
|
||||
return jsonify({"ok": False, "error": data}), 503
|
||||
|
||||
|
||||
# ================== API:远程看屏(阶段 2) ==================
|
||||
# MJPEG 实时画面流 + u2 触控注入。数据源 u2(atx-agent minicap 截图,单帧 ~200ms),
|
||||
# 零新依赖(原计划 ws-scrcpy 不在 npm 分发,GitHub 下载在国内不可靠,改自建)。
|
||||
# 坐标约定:前端以 <img> 显示尺寸归一化后映射到设备原生分辨率(naturalWidth/Height)。
|
||||
_SCREEN_JPEG_QUALITY = 65
|
||||
_SCREEN_FRAME_GAP = 0.05 # 帧间最小间隔(秒),防止空转烧 CPU
|
||||
|
||||
|
||||
@app.route("/api/screen/stream")
|
||||
@perm_required(PERM_DEVICES)
|
||||
def api_screen_stream():
|
||||
"""远程看屏:MJPEG 实时画面流(multipart/x-mixed-replace)。
|
||||
|
||||
?serial=xxx 设备 serial
|
||||
浏览器 <img> 直接渲染;客户端断开(GeneratorExit)自动停止,不占资源。
|
||||
设备离线/atx-agent 无响应时流自然结束,前端提示重新连接。
|
||||
"""
|
||||
serial = request.args.get("serial", "")
|
||||
if not serial:
|
||||
return jsonify({"ok": False, "error": "缺少 serial"}), 400
|
||||
|
||||
def generate():
|
||||
import io
|
||||
import uiautomator2 as u2
|
||||
try:
|
||||
d = u2.connect(serial)
|
||||
except Exception as e:
|
||||
_log.warning(f"远程看屏 {serial} u2 连接失败: {e}")
|
||||
return
|
||||
while True:
|
||||
try:
|
||||
img = d.screenshot()
|
||||
if img is None:
|
||||
break
|
||||
if img.mode != "RGB":
|
||||
img = img.convert("RGB")
|
||||
buf = io.BytesIO()
|
||||
img.save(buf, "JPEG", quality=_SCREEN_JPEG_QUALITY)
|
||||
yield (b"--frame\r\nContent-Type: image/jpeg\r\n\r\n"
|
||||
+ buf.getvalue() + b"\r\n")
|
||||
except GeneratorExit:
|
||||
break
|
||||
except Exception as e:
|
||||
_log.debug(f"远程看屏 {serial} 流中断: {e}")
|
||||
break
|
||||
time.sleep(_SCREEN_FRAME_GAP)
|
||||
|
||||
return Response(generate(),
|
||||
mimetype="multipart/x-mixed-replace; boundary=frame")
|
||||
|
||||
|
||||
def _screen_get_device(serial):
|
||||
"""远程看屏用的 u2 连接(连接失败抛异常由调用方转 503)。"""
|
||||
import uiautomator2 as u2
|
||||
return u2.connect(serial)
|
||||
|
||||
|
||||
@app.route("/api/screen/tap", methods=["POST"])
|
||||
@perm_required(PERM_DEVICES)
|
||||
def api_screen_tap():
|
||||
"""点击:{serial, x, y}(设备原生分辨率坐标)。"""
|
||||
data = request.json or {}
|
||||
serial, x, y = data.get("serial", ""), data.get("x"), data.get("y")
|
||||
if not serial or x is None or y is None:
|
||||
return jsonify({"ok": False, "error": "缺少 serial/x/y"}), 400
|
||||
try:
|
||||
_screen_get_device(serial).click(int(x), int(y))
|
||||
return jsonify({"ok": True})
|
||||
except Exception as e:
|
||||
return jsonify({"ok": False, "error": f"点击失败: {e}"}), 503
|
||||
|
||||
|
||||
@app.route("/api/screen/swipe", methods=["POST"])
|
||||
@perm_required(PERM_DEVICES)
|
||||
def api_screen_swipe():
|
||||
"""滑动:{serial, x1, y1, x2, y2, duration?}(设备原生分辨率坐标)。"""
|
||||
data = request.json or {}
|
||||
serial = data.get("serial", "")
|
||||
x1, y1, x2, y2 = (data.get(k) for k in ("x1", "y1", "x2", "y2"))
|
||||
if not serial or None in (x1, y1, x2, y2):
|
||||
return jsonify({"ok": False, "error": "缺少 serial/x1/y1/x2/y2"}), 400
|
||||
try:
|
||||
_screen_get_device(serial).swipe(int(x1), int(y1), int(x2), int(y2),
|
||||
duration=float(data.get("duration", 0.2)))
|
||||
return jsonify({"ok": True})
|
||||
except Exception as e:
|
||||
return jsonify({"ok": False, "error": f"滑动失败: {e}"}), 503
|
||||
|
||||
|
||||
_SCREEN_KEYS = {"back", "home", "recent", "menu", "power", "volume_up",
|
||||
"volume_down", "enter", "delete", "search", "camera"}
|
||||
|
||||
|
||||
@app.route("/api/screen/key", methods=["POST"])
|
||||
@perm_required(PERM_DEVICES)
|
||||
def api_screen_key():
|
||||
"""按键:{serial, key}(back/home/recent/menu/power 等,见 _SCREEN_KEYS)。"""
|
||||
data = request.json or {}
|
||||
serial, key = data.get("serial", ""), (data.get("key") or "").strip().lower()
|
||||
if not serial or key not in _SCREEN_KEYS:
|
||||
return jsonify({"ok": False, "error": "缺少 serial 或不支持的按键"}), 400
|
||||
try:
|
||||
_screen_get_device(serial).press(key)
|
||||
return jsonify({"ok": True})
|
||||
except Exception as e:
|
||||
return jsonify({"ok": False, "error": f"按键失败: {e}"}), 503
|
||||
|
||||
|
||||
@app.route("/api/screen/text", methods=["POST"])
|
||||
@perm_required(PERM_DEVICES)
|
||||
def api_screen_text():
|
||||
"""输入文字:{serial, text}(需焦点在输入框,u2 send_keys)。"""
|
||||
data = request.json or {}
|
||||
serial, text = data.get("serial", ""), (data.get("text") or "").strip()
|
||||
if not serial or not text:
|
||||
return jsonify({"ok": False, "error": "缺少 serial/text"}), 400
|
||||
try:
|
||||
_screen_get_device(serial).send_keys(text)
|
||||
return jsonify({"ok": True})
|
||||
except Exception as e:
|
||||
return jsonify({"ok": False, "error": f"输入失败: {e}"}), 503
|
||||
|
||||
|
||||
# ================== API:应用管理(APK 上传/安装)==================
|
||||
@app.route("/api/apks")
|
||||
@login_required
|
||||
|
||||
Reference in New Issue
Block a user