文末獲取測試開發高級實戰技能系統進階指南!
前言
感謝 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等相關技術,全面提升測試開發工程師的技術實力
