基於 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.apkx/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  

訪問 http://127.0.0.1:9002

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等相關技術,全面提升測試開發工程師的技術實力

點擊獲取更多信息


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM