Scrcpy投屏原理浅析-尝试用Flutter重写它的客户端
继上篇控制篇的后续文章,其中还会用到 Texture 外接纹理,在 Flutter 中进行软解码
前面相关文章
参考文章
Scrcpy 服务端
咱们还是从它的源码入手,scrcpy 有一个 main 函数的,这样才能被 app_process 直接启动,所以我们直接看它 main 函数
1 | public static void main(String... args) throws Exception { |
它是没有注释的(头大,不写注释),我自己加了些 print 来调试,unlinkSelf 是它删除自己的一个方法,我方便调试便注释了
main 函数都比较简单,Options 是它自己封装的一个参数的对象,简单说就是将命令行中启动时的参数封装到了对象里面
1 | CLASSPATH=/data/local/tmp/scrcpy-server.jar app_process ./ com.genymobile.scrcpy.Server 1.12.1 0 8000000 0 true - true true |
也就是
1 | 1.12.1 0 8000000 0 true - true true |
createOptions 返回的就是这个对象
随后调用了 scrcpy 这个函数,并将解析后的参数对象传了进去
scrcpy 函数
1 | private static void scrcpy(Options options) throws IOException { |
其中 try 中的语句调用的函数
1 | public static DesktopConnection open(Device device, boolean tunnelForward) throws IOException { |
open 函数负责创建两个 socket 并阻塞一直等到这两个 socket 分别被连接,随后 open 函数内发送了该设备的名称,宽度,高度,这些在投屏的视频流的解析需要用到
发送设备名与宽高的函数
1 | private void send(String deviceName, int width, int height) throws IOException { |
DEVICE_NAME_FIELD_LENGTH 是一个常量为 64,所以在视频流的 socket 被连接后,首先发送了一个字符 0(貌似是为了客户端检测连接成功,类似于一次握手的感觉),随后发送了 68 个字节长度的字符,64 个字节为设备名,4 个字节为设备的宽高,它这里也是用的移位运算将大于 255 的数存进了两个字节,所以我们在客户端也要对应的解析。
Flutter 客户端
理一下大致思路,第一个 socket 是视频输出,第二个是对设备的控制
Flutter 是不能直接解码视频的,这也是我上一篇文章提到的重要性。
所以就是
Flutter–>Plugin–>安卓原生调用 ffmpeg 相关解码且连接第一个 socket
dart 中连接第二个 socket 对设备进行控制
c++中
所以我们在 native 中去连接第一个 socket,并立即解码服务端的视频流数据
自定义一个 c++ socket 类(网上找的轮子自己改了一点)
ScoketConnection.cpp
1 | // |
头文件
1 | // |
为了方便在 cpp 中对 socket 的连接
连接与解码对应的函数
1 | SocketConnection *socketConnection; |
LOGD 是调用的 java 的 Log.d
连接成功后需要将前面服务端传出的一个字符 0 给读取掉
1 | uint8_t zeroChar[1]; |
随即接收设备信息
1 | uint8_t deviceInfo[68]; |
看一下调试图
接着就是解码了,这儿的解码也有注意的地方,我将整个代码贴出来
1 | SocketConnection *socketConnection; |
留意上面的注释都是不能放开的,对音视频解码有过了解的就会知道注释部分其实是正常播放视频一定会走的流程,在这是行不通的
如何渲染
通过前面文章提到的 Flutter 创建 Surface 方法,获取到对应的 textureId 后交给 Flutter 渲染出对应的画面,将其对象传到上面函数体内,由 cpp 直接操作这个窗口,
dart 中
1 | init() async { |
NetworkManager 是一个简单的 socket 封装类
1 | class NetworkManager { |
我们在 C++中获取到了设备的大小,但是 dart 这边并没有拿到,而 Flutter Texture 这个 Widget 会默认撑满整个屏幕
所以我们在 dart 也需要拿到设备的宽高
1 | ProcessResult _result = await Process.run( |
$currentIp 是当前设备的 ip,为了防止有多个设备连接的情况,adb 是交叉编译到安卓设备的,
最后我们用 AspectRatio 将 Texture 包起来,
1 | AspectRatio( |
这样在客户端 Texture 的显示也会保持远程设备屏幕的比例
控制
当然会用 GestureDetector
如下
1 | GestureDetector( |
ok,最后效果(允许我用上篇帖子的 gif)
个人感觉是因为 Texture cpu->gpu->cpu 的开销,画面显示有延迟,控制其实是没有延迟的
最后总结一下
- 显示有延迟
- 目前只能在安卓局域网或者 otg 控制安卓
- Linux 等桌面端由于没有视频播放的方案(我正尝试能否也创建一个 opengl 的 surface,但最后都没有成功,实现了一个极其劣质的播放器没法用),但 linux 通过这样的方案控制端是行得通的
开源需要等我处理本地的一些问题才行
Scrcpy投屏原理浅析-尝试用Flutter重写它的客户端
http://blog.nightmare.press/2020/03/31/Scrcpy投屏原理浅析-尝试用Flutter重写它的客户端/