• 基于 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等相关技术,全面提升测试开发工程师的技术实力

    点击获取更多信息

  • 相关阅读:
    ContextMenustrip 控件
    Toolstrip 工具栏控件
    Menustrip控件和ContextMenustrip控件
    TabControl 选项卡控件
    GroupBox 分组框控件
    Pnel控件
    【bzoj3427】Poi2013 Bytecomputer dp
    【bzoj3174】[Tjoi2013]拯救小矮人 贪心+dp
    【bzoj1334】[Baltic2008]Elect 背包dp
    【bzoj1369】[Baltic2003]Gem 树形dp
  • 原文地址:https://www.cnblogs.com/hogwarts/p/15984676.html
Copyright © 2020-2023  润新知