基于 Scrcpy 的远程调试方案,补齐 STF 短板!
文末获取测试开发高级实战技能系统进阶指南!
前言
感谢 STF 的开源,让 Android 设备远程控制变得简单,STF 通过 minicap 和 minitouch 实现设备的显示和控制。
但 STF 在实际使用中,遇到一些棘手的问题:
1. 电视不支持 minitouch
2. 新手机比如 mi8 mi9 不支持 minicap
3. Android 发布新版本需要适配 minicap
本文分享一个新的方法,来弥补这些不足。
演示效果如下, 由于图片较大,手机党建议完全加载完在播放,否则会卡 ,电脑的录屏软件很不给力,渣渣画质请见谅…
访问以下链接,可获取 Gif 大图。
https://github.com/wenxiaomao1023/scrcpy/blob/master/assets/out.gif
Scrcpy 特性
app目录 :运行在PC端,对于web远程控制,这部分是不需要的
server目录 :运行在手机端,提供屏幕数据,接收并响应控制事件
Scrcpy 对比 minicap
1. 获取 frame 数据方式是一致的(sdk19以上)
2. Scrcpy 将 frame 编码h264
3. minicap 将 frame 编码jpeg
Scrcpy 处理方式看起来会更好,但是有一个问题,他的设计是将屏幕数据直接发给 PC,然后在 PC 上解码显示,这种方式在网页上却很不好展示。
调研与尝试
1. Broadway 在前端解码 h264 并显示。
2. wfs.js 在前端将 h264 转成 mp4 送给 h5 MSE 实现播放,这种类似直播,B 站 flv.js 那种。
以上两种尝试都获得了图像,但个人感觉,以上两个方案感觉都有坑,还需要大量优化才能脱坑。
解决方法
当前摸索出的解决方法,Scrcpy 将 frame 编码 jpeg 发给前端然后通过画布展示,浏览器兼容好,可行性高,minicap
也是这么做的,修改方式见如下(放在 Github):
https://github.com/wenxiaomao1023/scrcpy/commit/dce39887f562cd33ad75e12b95778be00955011a
当前已实现的功能
1. 使用 ImageReader 获取 frame 数据,通过 libjpeg-turbo 编码 jpeg
2. 控制帧率,压缩率,缩放比例,可以减少带宽占用,提高流畅性
3. 考虑到当前大多是 minicap 的方案,所以 scrcpy 返回的屏幕数据格式兼容了 minicap
的数据格式(banner+jpegsize+jpegdata),移植改动会很小
优点
1. 德芙般丝滑,手机播放视频一点不卡,web 端展示也很流畅(30 - 50 FPS)
2. 支持电视 touch
3. 支持 mi8,mi9 等图像展示,不必在适配 minicap.so 啦,耶!✌️
缺点
1. 最低支持 Android5.0,由于还依赖 android.system.Os,若想兼容低版本设备需要配合 minicap 使用。
编译 libjpeg-turbo
我已经编好了ARMv7 (32-bit)和ARMv8 (64-bit),GitHub 地址如下:
https://github.com/wenxiaomao1023/scrcpy/tree/master/server/libs/libturbojpeg/prebuilt
如果你需要其他平台,可参考此文档 Building libjpeg-turbo for Android 部分:
https://github.com/libjpeg-turbo/libjpeg-turbo/blob/master/BUILDING.md
如果不需要, 可跳过此步骤
编译Scrcpy代码
ninja 编译方式
Android SDK 测试里有 ninja,如
Android/Sdk/cmake/3.6.4111459/bin/ninja,加到环境变量里即可,meson 需要安装。
如果不想安装这些,可以往下看,用 gradle 编译;
git clone https://github.com/wenxiaomao1023/scrcpy.git
cd scrcpy
meson x --buildtype release --strip -Db_lto=true
ninja -Cx
编译后会在 scrcpy 目录下生成
x/server/scrcpy-server.jar
server/jniLibs/armeabi-v7a/libcompress.so
server/jniLibs/arm64-v8a/libcompress.so
gradle编译方式
git clone https://github.com/wenxiaomao1023/scrcpy.git
cd scrcpy/server
../gradlew assembleDebug
编译后会在scrcpy目录下生成:
server/build/outputs/apk/debug/server-debug.apk
server/jniLibs/armeabi-v7a/libcompress.so
server/jniLibs/arm64-v8a/libcompress.so
server/build/outputs/apk/debug/server-debug.apk 和 x/server/scrcpy-
server.jar 是一样的,下文中都按 scrcpy-server.jar 命名方式进行说明.
启动 scrcpy-server.jar
# 先看下设备的abi,
adb shell getprop ro.product.cpu.abi
# armeabi-v7a
adb push scrcpy/server/jniLibs/armeabi-v7a/libcompress.so /data/local/tmp/
adb push scrcpy/server/libs/libturbojpeg/prebuilt/armeabi-v7a/libturbojpeg.so /data/local/tmp/
adb push scrcpy/x/server/scrcpy-server.jar /data/local/tmp/
adb shell chmod 777 /data/local/tmp/scrcpy-server.jar
adb shell LD_LIBRARY_PATH=/system/lib:/vendor/lib:/data/local/tmp CLASSPATH=/data/local/tmp/scrcpy-server.jar app_process / com.genymobile.scrcpy.Server
# arm64-v8a
adb push server/jniLibs/arm64-v8a/libcompress.so /data/local/tmp/
adb push server/libs/libturbojpeg/prebuilt/arm64-v8a/libturbojpeg.so /data/local/tmp/
adb push scrcpy/x/server/scrcpy-server.jar /data/local/tmp/
adb shell chmod 777 /data/local/tmp/scrcpy-server.jar
adb shell LD_LIBRARY_PATH=/system/lib64:/vendor/lib64:/data/local/tmp CLASSPATH=/data/local/tmp/scrcpy-server.jar app_process / com.genymobile.scrcpy.Server
app_process / com.genymobile.scrcpy.Server
这个命令可以设置如下参数,建议使用命令如下,压缩质量60,最高24帧,缩放为屏幕长宽除以2
app_process / com.genymobile.scrcpy.Server -Q 60 -r 24 -P 2
Usage: %s [-h]
-Q <value>: JPEG quality (0-100).
-r <value>: Frame rate (frames/s).
-P <value>: Display projection (scale 1,2,4...).
-h: Show help.
启动 app.js
scrcpy-server.jar 兼容了 minicap 数据格式,可以直接用 minicap 的 demo app.js 看效果。
https://github.com/openstf/minicap/tree/master/example
https://github.com/openstf/minicap/blob/master/example/app.js
需要把 app.js 改一下,多一个连接,修改如下
// 原始代码默认的图像socket
var stream = net.connect({
port: 1717
})
// 修改1 加一个控制socket
var controlStream = net.connect({
port: 1717
})
git clone https://github.com/openstf/minicap.git
cd minicap/example
npm install
# 注意这里要改为localabstract:scrcpy
adb forward tcp:1717 localabstract:scrcpy
node app.js
Scrcpy touch
Scrcpy touch的实现可以参考如下实现,当前实现常用的三种事件消息:
// 键值 HOME,BACK,MENU等
CONTROL_MSG_TYPE_INJECT_KEYCODE
// 点击和滑动
CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT
// 鼠标滚轮滚动
CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT
后端提供 json 格式接口
package main
import (
"errors"
"net"
"github.com/qiniu/log"
"bytes"
"encoding/binary"
)
type MessageType int8
const (
CONTROL_MSG_TYPE_INJECT_KEYCODE MessageType = iota
CONTROL_MSG_TYPE_INJECT_TEXT
CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT
CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT
CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON
CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL
CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL
CONTROL_MSG_TYPE_GET_CLIPBOARD
CONTROL_MSG_TYPE_SET_CLIPBOARD
CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE
)
type PositionType struct {
X int32 `json:"x"`
Y int32 `json:"y"`
Width int16 `json:"width"`
Height int16 `json:"height"`
}
type Message struct {
Msg_type MessageType `json:"msg_type"`
// CONTROL_MSG_TYPE_INJECT_KEYCODE
Msg_inject_keycode_action int8 `json:"msg_inject_keycode_action"`
Msg_inject_keycode_keycode int32 `json:"msg_inject_keycode_keycode"`
Msg_inject_keycode_metastate int32 `json:"msg_inject_keycode_metastate"`
// CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT
Msg_inject_touch_action int8 `json:"msg_inject_touch_action"`
Msg_inject_touch_pointerid int64 `json:"msg_inject_touch_pointerid"`
Msg_inject_touch_position PositionType `json:"msg_inject_touch_position"`
Msg_inject_touch_pressure uint16 `json:"msg_inject_touch_pressure"`
Msg_inject_touch_buttons int32 `json:"msg_inject_touch_buttons"`
// CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT
Msg_inject_scroll_position PositionType `json:"msg_inject_scroll_position"`
Msg_inject_scroll_horizontal int32 `json:"msg_inject_scroll_horizontal"`
Msg_inject_scroll_vertical int32 `json:"msg_inject_scroll_vertical"`
}
type KeycodeMessage struct {
Msg_type MessageType `json:"msg_type"`
Msg_inject_keycode_action int8 `json:"msg_inject_keycode_action"`
Msg_inject_keycode_keycode int32 `json:"msg_inject_keycode_keycode"`
Msg_inject_keycode_metastate int32 `json:"msg_inject_keycode_metastate"`
}
type TouchMessage struct {
Msg_type MessageType `json:"msg_type"`
Msg_inject_touch_action int8 `json:"msg_inject_touch_action"`
Msg_inject_touch_pointerid int64 `json:"msg_inject_touch_pointerid"`
Msg_inject_touch_position PositionType `json:"msg_inject_touch_position"`
Msg_inject_touch_pressure uint16 `json:"msg_inject_touch_pressure"`
Msg_inject_touch_buttons int32 `json:"msg_inject_touch_buttons"`
}
type ScrollMessage struct {
Msg_type MessageType `json:"msg_type"`
Msg_inject_scroll_position PositionType `json:"msg_inject_scroll_position"`
Msg_inject_scroll_horizontal int32 `json:"msg_inject_scroll_horizontal"`
Msg_inject_scroll_vertical int32 `json:"msg_inject_scroll_vertical"`
}
func drainScrcpyRequests(conn net.Conn, reqC chan Message) error {
for req := range reqC {
var err error
switch req.Msg_type {
case CONTROL_MSG_TYPE_INJECT_KEYCODE:
t := KeycodeMessage{
Msg_type: req.Msg_type,
Msg_inject_keycode_action: req.Msg_inject_keycode_action,
Msg_inject_keycode_keycode: req.Msg_inject_keycode_keycode,
Msg_inject_keycode_metastate: req.Msg_inject_keycode_metastate,
}
buf := &bytes.Buffer{}
err := binary.Write(buf, binary.BigEndian, t)
if err != nil {
log.Debugf("CONTROL_MSG_TYPE_INJECT_KEYCODE error: %s", err)
log.Debugf("%s",buf.Bytes())
break
}
_, err = conn.Write(buf.Bytes())
case CONTROL_MSG_TYPE_INJECT_TEXT:
case CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT:
var pointerid int64 = -1
var pressure uint16 = 65535
var buttons int32 = 1
req.Msg_inject_touch_pointerid = pointerid
req.Msg_inject_touch_pressure = pressure
req.Msg_inject_touch_buttons = buttons
t := TouchMessage{
Msg_type: req.Msg_type,
Msg_inject_touch_action: req.Msg_inject_touch_action,
Msg_inject_touch_pointerid: req.Msg_inject_touch_pointerid,
Msg_inject_touch_position: PositionType{
X: req.Msg_inject_touch_position.X,
Y: req.Msg_inject_touch_position.Y,
Width: req.Msg_inject_touch_position.Width,
Height: req.Msg_inject_touch_position.Height,
},
Msg_inject_touch_pressure: req.Msg_inject_touch_pressure,
Msg_inject_touch_buttons: req.Msg_inject_touch_buttons,
}
buf := &bytes.Buffer{}
err := binary.Write(buf, binary.BigEndian, t)
if err != nil {
log.Debugf("CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT error: %s", err)
log.Debugf("%s",buf.Bytes())
break
}
_, err = conn.Write(buf.Bytes())
case CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT:
t := ScrollMessage{
Msg_type: req.Msg_type,
Msg_inject_scroll_position: PositionType{
X: req.Msg_inject_scroll_position.X,
Y: req.Msg_inject_scroll_position.Y,
Width: req.Msg_inject_scroll_position.Width,
Height: req.Msg_inject_scroll_position.Height,
},
Msg_inject_scroll_horizontal: req.Msg_inject_scroll_horizontal,
Msg_inject_scroll_vertical: req.Msg_inject_scroll_vertical,
}
buf := &bytes.Buffer{}
err := binary.Write(buf, binary.BigEndian, t)
if err != nil {
log.Debugf("CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT error: %s", err)
log.Debugf("%s",buf.Bytes())
break
}
_, err = conn.Write(buf.Bytes())
case CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON:
case CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL:
case CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL:
case CONTROL_MSG_TYPE_GET_CLIPBOARD:
case CONTROL_MSG_TYPE_SET_CLIPBOARD:
case CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE:
default:
err = errors.New("unsupported msg type")
}
if err != nil {
return err
}
}
return nil
}
前端调用
let scrcpyKey = (key) => {
ws.send(JSON.stringify({
"msg_type": 0,
"msg_inject_keycode_action": 0,
"msg_inject_keycode_keycode": key,
"msg_inject_keycode_metastate": 0
}))
ws.send(JSON.stringify({
"msg_type": 0,
"msg_inject_keycode_action": 1,
"msg_inject_keycode_keycode": key,
"msg_inject_keycode_metastate": 0
}))
}
let scrcpyTouchDown = (touch) => {
ws.send(JSON.stringify({
"msg_type": 2,
"msg_inject_touch_action": 0,
"msg_inject_touch_position": {
"x": touch.x, "y": touch.y, "width": touch.w, "height": touch.h
}}));
}
let scrcpyTouchMove = (touch) => {
ws.send(JSON.stringify({
"msg_type": 2,
"msg_inject_touch_action": 2,
"msg_inject_touch_position": {
"x": touch.x, "y": touch.y, "width": touch.w, "height": touch.h
}
}));
}
let scrcpyTouchUp = (touch) => {
ws.send(JSON.stringify({
"msg_type": 2,
"msg_inject_touch_action": 1,
"msg_inject_touch_position": {
"x": touch.x, "y": touch.y, "width": touch.w, "height": touch.h
}
}));
}
//向下滚动
let scrcpyScrollDown = (touch) => {
ws.send(JSON.stringify({
"msg_type": 3,
"msg_inject_scroll_position": {
"x": touch.x, "y": touch.y, "width": touch.w, "height": touch.h
},
"msg_inject_scroll_horizontal": 0,
"msg_inject_scroll_vertical": -1,
}));
}
//向上滚动
let scrcpyScrollUp = (touch) => {
ws.send(JSON.stringify({
"msg_type": 3,
"msg_inject_scroll_position": {
"x": touch.x, "y": touch.y, "width": touch.w, "height": touch.h
},
"msg_inject_scroll_horizontal": 0,
"msg_inject_scroll_vertical": 1,
}));
}
以上,项目还在开发阶段,欢迎反馈问题 : )
** _
来霍格沃兹测试开发学社,学习更多软件测试与测试开发的进阶技术,知识点涵盖web自动化测试 app自动化测试、接口自动化测试、测试框架、性能测试、安全测试、持续集成/持续交付/DevOps,测试左移、测试右移、精准测试、测试平台开发、测试管理等内容,课程技术涵盖bash、pytest、junit、selenium、appium、postman、requests、httprunner、jmeter、jenkins、docker、k8s、elk、sonarqube、jacoco、jvm-sandbox等相关技术,全面提升测试开发工程师的技术实力
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了