STF 基于 Scrcpy 的远程调试方案

wenxiaomao · 2019年12月13日 · 最后由 ReachForAStar 回复于 2021年04月08日 · 2300 次阅读
本帖已被设为精华帖!

基于 Scrcpy 的远程调试方案

前言

感谢 STF 的开源,让 Android 设备远程控制变得简单,STF 通过 minicap 和 minitouch 实现设备的显示和控制。
STF 在实际使用中会发现一些棘手的问题
1.电视不支持 minitouch
2.新手机比如 mi8 mi9 不支持 minicap
3.Android 发布新版本需要适配 minicap

分享一个新的方法,来弥补这些不足
演示效果如下,由于图片较大(14MB),手机党请谨慎点击如下链接,建议完全加载完在播放,否则会卡,电脑的录屏软件很不给力,渣渣画质。。。
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 也是这么做的,修改方式见如下
https://github.com/wenxiaomao1023/scrcpy/commit/46d1c009d8ce559dc1ac1cdfceb234f1c7728498

当前已实现的功能

1.使用 ImageReader 获取 frame 数据,通过 libjpeg-turbo 编码 jpeg
2.控制帧率,压缩率,缩放比例,可以减少带宽占用,提高流畅性
3.考虑到当前大多是 minicap 的方案,所以 scrcpy 返回的屏幕数据格式兼容了 minicap 的数据格式(banner+jpegsize+jpegdata),移植改动会很小
4.最低支持到 Android4.4(adb forward 命令有变化,见下文,默认端口 6612)
5.返回旋转状态(为了替换 STFService.apk)
6.添加获取 DumpHierarchy 信息(启动命令加-D 参数),获取界面布局信息为录制 Case 功能准备

优点

1.德芙般丝滑,手机播放视频一点不卡,web 端展示也很流畅(30 - 50 FPS)
2.支持电视 touch
3.支持 mi8,mi9 等图像展示,不必在适配 minicap.so 啦,耶!

缺点

1.最低支持 android5.0,由于还依赖 android.system.Os,若想兼容低版本设备需要配合 minicap 使用,当前最低可支持到 Android4.4
2.流量问题,如果你的网站部署在内网,建议参考此方案,如果部署在公网,建议采用原生的 Scrcpy 方式

编译 libjpeg-turbo

我已经编好了 ARMv7 (32-bit) 和 ARMv8 (64-bit)
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

新版本添加-L 参数,LD_LIBRARY_PATH 的值等于-L 参数的返回值,并追加:/data/local/tmp

# 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 CLASSPATH=/data/local/tmp/scrcpy-server.jar app_process / com.genymobile.scrcpy.Server -L
# LD_LIBRARY_PATH的值为上步-L的返回值加:/data/local/tmp(注意有个英文冒号)
adb shell LD_LIBRARY_PATH=???:/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 CLASSPATH=/data/local/tmp/scrcpy-server.jar app_process / com.genymobile.scrcpy.Server -L
# LD_LIBRARY_PATH的值为上步-L的返回值加:/data/local/tmp(注意有个英文冒号)
adb shell LD_LIBRARY_PATH=???:/data/local/tmp CLASSPATH=/data/local/tmp/scrcpy-server.jar app_process / com.genymobile.scrcpy.Server
# Android4.4设备
# 前几步同上(查看abi push libcompress.so libturbojpeg.so,push scrcpy-server.jar,chmod scrcpy-server.jar)
# 不同的地方在于需要指定ANDROID_DATA参数,否则启动会报错Dex cache directory isn't writable: /data/dalvik-cache (Permission denied) uid=2000 gid=2000
# 见解决方法https://stackoverflow.com/questions/21757935/running-android-java-based-command-line-utility-from-adb-shell
adb shell mkdir -p /data/local/tmp/dalvik-cache
adb shell ANDROID_DATA=/data/local/tmp CLASSPATH=/data/local/tmp/scrcpy-server.jar app_process / com.genymobile.scrcpy.Server -L
# LD_LIBRARY_PATH的值为上步-L的返回值加:/data/local/tmp(注意有个英文冒号)
adb shell ANDROID_DATA=/data/local/tmp LD_LIBRARY_PATH=???:/data/local/tmp CLASSPATH=/data/local/tmp/scrcpy-server.jar app_process / com.genymobile.scrcpy.Server

app_process / com.genymobile.scrcpy.Server 这个命令可以设置如下参数,建议使用命令如下,压缩质量 60,最高 24 帧,480P
app_process / com.genymobile.scrcpy.Server -Q 60 -r 24 -P 480

Usage: [-h]

jpeg:
  -r <value>:    Frame rate (frames/sec).
  -P <value>:    Display projection (1080, 720, 480, 360...).
  -Q <value>:    JPEG quality (0-100).

  -c:            Control only.
  -L:            Library path.
  -D:            Dump window hierarchy.
  -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,在默认的net.connect后再net.connect一次(必须)
var controlStream = net.connect({
    port: 1717
})

// 修改2 新版本除了返回图像,还会返回旋转和层级(如果-D),需要容错处理,屏蔽掉exit(),并加else
if (frameBody[0] !== 0xFF || frameBody[1] !== 0xD8) {
    console.error('Frame body does not start with JPG header', frameBody)
    // process.exit(1) //屏蔽exit()
}
else { // 添加else
    ws.send(frameBody, {
        binary: true
    })
}
git clone https://github.com/openstf/minicap.git
cd minicap/example
npm install
# 注意这里要改为localabstract:scrcpy(旧版本)
# 注意这里要改为tcp:6612(新版本)
adb forward tcp:1717 tcp:6612
node app.js

访问 http://127.0.0.1:9002
如果有时刷新会没效果,只显示一个红框,先确认是否执行了 adb forward tcp:1717 tcp:6612,如果是概率性刷新不出图像这是正常的,客户端连接失败需要做重连操作,例子里并没有,所以简单解决办法是重启下 scrcpy-server.jar,然后刷新网页

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,
    }));
}

项目还在开发阶段,欢迎反馈问题 : )

收工~

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 81 条回复 时间 点赞
思寒_seveniruby 将本帖设为了精华贴 12月15日 12:02

很不错的样子

编译后的 jar 怎么集成至 stf 替换目前的 minicap?

可以找您定做个方案吗?18514000999 VX

@codeskyblue 这下可以接入 atxserver2 了👏 👏 👏

手机上的 cpu 资源占用怎么样

大浪 回复

没有用 STF,没法帮你了

codeskyblue 回复

CPU 占用还是比较高的,目前看界面效果还可以接受

wenxiaomao 回复

只要不比 minicap 高太多就行

一看名字就是猜到了,扫了下二维码还真是,你从乐视出来后也有几年几年没见了。

不错的方案,不过昨天 Win 下编译一堆问题,看来 Win 环境还是不适合。

试了一下,主要是启动 db shell LD_LIBRARY_PATH=/system/lib64:/vendor/lib64:/data/local/tmp CLASSPATH=/data/local/tmp/scrcpy-server.jar app_process / com.genymobile.scrcpy.Server 命令的时候会报这个问题

CANNOT LINK EXECUTABLE "/system/bin/dex2oat": "/system/lib64/libart-compiler.so" is 64-bit instead of 32-bit

会影响吗

仅楼主可见
simple [精彩盘点] TesterHome 社区 2019 年 度精华帖 中提及了此贴 12月24日 14:45
zerkzzzz 回复

请问下你用的什么设备,我只在小米设备上做了验证,没有其他品牌的
可以提供下 abi 信息吗 adb shell getprop | grep abi
或将此参数改为这样再试下呢 LD_LIBRARY_PATH=/system/lib:/vendor/lib:/data/local/tmp

zerkzzzz 回复

看下 logcat 是否有报错 adb shell logcat -v threadtime | grep scrcpy
感觉与之前启动报错有关,scrcpy-server 没有起来,scrcpy touch 可用吗?

wenxiaomao 回复

编译完成,启动 app_process 后正常情况下 logcat 的日志是
12-28 14:36:09.564 7554 7554 I scrcpy : Options frame rate: 24 (1 ~ 100)
12-28 14:36:09.565 7554 7554 I scrcpy : Options quality: 50 (1 ~ 100)
12-28 14:36:09.565 7554 7554 I scrcpy : Options scale: 2 (1,2,4...)

然后实际要打开页面去链接才会有 fps 变化是吗?
后面启动 app.js,感觉不是很清晰,还请能否出个 demo 呢?是否做了其他变更?
scrcpy 用 openstf minicap 的测试代码可以支持吗?

adb shell getprop | grep abi 的信息如下,用的是 coolpad 手机

node app.js 执行之后访问端口,控制台就出现这个问题,看起来是 stream 断开了 是否 forward 需要特殊设置?

12-28 14:42:08.296 7554 7554 D scrcpy : Screen streaming stopped
12-28 14:42:08.298 7554 7744 I scrcpy : =========================================>>>
12-28 14:42:08.298 7554 7744 I scrcpy : head: 0
12-28 14:42:08.298 7554 7744 I scrcpy : rawBuffer.length: 1024
12-28 14:42:08.299 7554 7744 I scrcpy : rawBuffer: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
12-28 14:42:08.299 7554 7744 I scrcpy : =========================================<<<
12-28 14:42:08.299 7554 7744 D scrcpy : Controller stopped
12-28 14:42:08.300 7554 7744 D scrcpy : E:Controller socket closed

仅楼主可见
21楼 已删除
zerkzzzz 回复

图片不清晰请尝试将启动命名修改为如下,主要是修改这 2 个参数-Q 和-P
app_process / com.genymobile.scrcpy.Server -Q 100 -r 60 -P 1

看日志 scrcpy-server 启动正常,后台报错应该和你刷新网页有关,要注意下 minicap 提供的 demo 只是一次请求有效,没有做界面刷新处理,所以如果你刷新了界面,需要将 scrcpy-server.jar 重新启动一次,forward 不用变,只需 forward 一次即可,希望可以帮到你

wenxiaomao 回复

可以加您微信吗 大神

可以问下,通过这种方式的实现,有办法在 scrcoy 运行过程中更改清晰度吗?

大神你好,想问下 controller 方面可以直接通过端口发送控制消息吗?就是利用 scrcpy 的 controller 线程进行 touch

大浪 回复

需要修改一下 stf adb.push 那一块的代码,和 stream.js 的代码,另外 scrcpy 的设备控制模块要去掉,不然画面传输也起不来。

qchuang 回复

你这边成功了么?写个文档学习下呀👍

大浪 回复

我这边现在是基于 scrcpy + inputmanager,但是 web 端解析播放.h264 这一块跟楼主一样遇到了很多坑,还是爬坑中,爬完坑可以写个文档大家一起交流一下。

wpx 回复

目前运行过程中是不改清晰度的,当屏幕旋转会按照启动时配置的参数值重新设置清晰度,可以在这里改清晰度

Zayoi 回复

可以,按照控制消息的格式,向 socket 发数据即可,按照 server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java next() 读取的格式写将数据发到 socket 就可以控制了

修改问题,添加更新,欢迎反馈 : )
启动命令有变化,adb forward 命令有变化,默认端口 6612,详见上文
4.最低支持到 Android4.4
5.返回旋转状态(为了替换 STFService.apk)
6.添加获取 DumpHierarchy 信息(启动命令加-D 参数),获取界面布局信息为录制 Case 功能准备

32楼 已删除
33楼 已删除
50楼 已删除

现在项目代码里的 ImageReader 的 Surface 如何获取屏幕内容,执行代码总是进入不了 ImageReader 的监听方法 onImageAvailable, 刚入门的安卓小白,请大神指点一二,非常感谢

37楼 已删除

LD_LIBRARY_PATH 中/system/lib 的作用是什么呢,我在使用时,会由于这个库导致一些手机上出现错误

wpx 回复

指定加载库的位置,新版本请使用-L 参数获取 LD_LIBRARY_PATH 的值,如果还会出现错误,请帮忙贴下错误日志和设备信息

请问 app.js 为啥要 // 修改 1 加一个控制 socket,在默认的 net.connect 后再 net.connect 一次(必须)

Jacc 回复

第一个 net.connect 是传图像和旋转状态等信息,第二个 net.connect 是传控制信息 (点击滑动等)

其实对于 minicap 或者 scrcpy 总有个想法,是否可以开发个 app 来控制转发原始数据流到指定域名接口,那任何设备就可以插拔式的做云真机

qchuang 回复

大胸弟,怎样了 成功了没

你好请问这个框架支持电视机安卓系统的自动化吗

为啥没人尝试直播 sdk 呢,手机端采用推流,客户端采用拉流,可以支持同一个设备同时被多方同时看 😬

请问下你做的这个是一套远程真机操作系统吗


这两部分代码在哪个工程或者文件里面添加呢

wenxiaomao 回复

现在 web 端可以展示出手机页面了,但是还不可以操作手机页面,这个应该怎么修改呢, 主要是为了解决手机没有触屏模块,远程 stf 无法操作手机的问题。

支持使用 SurfaceView 播放视频的播放器截图不?

请问目前的方案能够实现类似于 Minicap 截图的功能么?

51楼 已删除

writeRotation(fd)
新版屏幕方向这个如果不需要的话可以注释掉,这样就不需要修改 app.js

楼主你好,我记得在 18 年还是 19 年初,你发的是可以基于 web 远程调试的,现在已经不更了吗?

你好,大佬,我想问下,目前你这个项目 scrcpy 有持续跟官网保持同步更新吗?因为我现在也是用这个项目的,用 win 编译的。另外也想请教下,官网的项目如何进行修改进行 win 编译可用的。感谢~

scrcpy 是使用什么语言

学到了🚗

仅楼主可见
仅楼主可见

接口测试工具可以试用一下国产的接口测试和接口文档生产工具 apipost:https://www.apipost.cn/?dt=2020

yca 基于 Scrcpy 的云测平台改造方案 中提及了此贴 12月30日 09:51

ninja编译方式编译后没有出现下面的文件!!!!!!
x/server/scrcpy-server.jar
server/jniLibs/armeabi-v7a/libcompress.so
server/jniLibs/arm64-v8a/libcompress.so
楼主编译好的arm64-v8aarmeabi-v7a文件下也没有libcompress.so文件
请问大佬arm64-v8a文件下的libjpeg.so是不是libcompress.so文件,只是名字不同

多谢! 图片模块已经移植测试完成,明天开始移植 touch 模块

793136254 回复

你好可以加你的 QQ 了解下图片模块(我重新编译楼主的代码没有出现 libcompress.so 文件)qq:1137136417

请问下有没有尝试过使用 scrcpy 替换 atxserver 中的 minicap,暂时还不知道怎么弄,想请教下,请问有没有可以加你的微信或者 QQ 吗

GJHYY 回复

图片模块跟 minicap 连接基本上是一样的 ,只需要判断一下 frameBodyLength,如果不是图片的话解析成 orientation 就可以了。

Kyle 回复

楼主这个方案已经实现了,自己编译一下 so jar,修改一下连接方式就可以了

68楼 已删除
69楼 已删除
gkd 回复

已加你的 QQ,能直接请教下吗

gkd 回复

我编译成功但是连接失败报错,可以加下你微信请教下🎊

GJHYY 回复

socket 需要连接两次

Kyle 回复

公司 QQ 用不了

gkd 回复

怎么连接可以发一下吗?我根据帖子连接还是失败😭

gkd 回复

能不能详细讲下如果我想用 scrcpy 替换 ATX 的 minicap 需要如何操作,或者有没有相关的文章推荐下也可以,谢谢了

gkd 回复

谢谢,连接成功💯

Kyle 回复

这篇楼主已经讲的很详细了,而且替换起来工作量是最小的

793136254 回复

你好,请问你这个 touch 模块移植完成没

GJHYY 回复

没有,最近在忙别的

可以把编译好的文件直接放到项目里吗,我折腾了很久在 windows 上面也没能编译成功

touch 移植完成,再次感谢楼主,少走了很多弯路

这个怎么适配 ios 系统?

问下,当我把 scrcpy 视频流转到可接收的 minicap 图片,发现有概率性的,截出来的图是灰色的,也就是图片显示异常,灰色的一个背景图~不知道是否有遇到

chenyouan 回复

你看那个灰色的图片是不是只有几 byte

gkd 回复

是的,类似没有 rgb 的过滤,这个要如何处理。我做自动化的时候,步骤都要截图,然后就发现回传的图片是灰屏的

86楼 已删除

请问下按照大佬的步骤执行 ,最后运行 adb shell LD_LIBRARY_PATH=/oem/lib64:/system/lib64:/vendor/lib64:/data/local/tmp CLASSPATH=/data/local/tmp/scrcpy-server.jar app_process / com.genymobile.scrcpy.Server
返回下面这个
[server] INFO: Options frame rate: 24 (1 ~ 60)
[server] INFO: Options quality: 60 (1 ~ 100)
[server] INFO: Options projection: 480 (1080, 720, 480, 360...)
[server] INFO: Options control only: false (true / false)
然后运行 node app.js,只返回 listen on 9002
两个运行中的窗口都没有打印收到了图片的日志,是哪步操作有什么问题么,127.0.0.1:9002 网页打不开,ws:/127.0.0.1:1717 也连不上

ReachForAStar 回复

端口 转发了 吗

哲豪 回复

启动后 scrcpy-server.jar 后,运行的这个命令 adb forward tcp:1717 tcp:6612

ReachForAStar 回复

试试 再 重启下 jar ,然后在刷新页面

哲豪 回复

一顿修改之后网页上能看到空白红框,但每次刷新都会 [server] INFO: alive: false,然后就 scrcpy 的进程就断开了😭 😭
[server] INFO: videoChannel: java.nio.channels.SocketChannel[connected local=/127.0.0.1:6612 remote=/127.0.0.1:41711]
[server] WARN: Unknown event type: 71
[server] INFO: realWidth: 1080, realHeight: 1920, desiredWidth: 480, desiredHeight: 856
[server] INFO: banner
{
version: 1
size: 24
real width: 1080
real height: 1920
desired width: 480
desired height: 856
orientation: 0
quirks: 2
}

[server] INFO: realWidth: 1080, realHeight: 1920, desiredWidth: 480, desiredHeight: 856
[server] INFO: hander message: { when=-1ms what=1 obj=rotation target=com.genymobile.scrcpy.ScreenEncoder$1 }
[server] INFO: hander message: { when=-1ms what=1 obj=image target=com.genymobile.scrcpy.ScreenEncoder$1 }
[server] INFO: alive: false

ReachForAStar 回复

目前看上去是通的,app.js 修改了吗

哲豪 回复

😭 😊 哇,谢谢大佬,调了两天了,刚刚突然调通了

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册