scrcpy 开发者指南
概览
该应用程序由两部分组成:
- 服务器(scrcpy-server
),在设备上执行,
- 客户端(scrcpy
二进制文件),在主机计算机上执行。
客户端负责将服务器推送到设备并启动其执行。
客户端和服务器使用单独的套接字建立视频、音频和控制的通信。它们中的任何一个都可以禁用(但不是全部),因此有1个、2个或3个套接字。
服务器最初通过第一个套接字发送设备名称(用于scrcpy窗口标题),然后每个套接字用于其自己的目的。所有读写操作都是由每个套接字的专用线程进行的,无论是在客户端还是服务器上。
如果启用视频,则服务器发送设备的屏幕的原始视频流(默认为H.264),每个数据包还有一些额外的头。客户端解码视频帧,并尽快显示它们,而不进行缓冲(除非指定--display-buffer=delay
)。客户端不知道设备的旋转(这由服务器处理),它只知道它接收到的视频帧的尺寸。
同样,如果启用音频,则服务器发送设备的音频输出(或者如果指定--audio-source=mic
,则是麦克风)的原始音频流(默认为OPUS),每个数据包都有一些额外的头。客户端解码流,尝试通过维持平均缓冲来保持最小的延迟。scrcpy v2.0发布的博客文章提供了有关音频功能的更多详细信息。
如果启用控制,则客户端捕获相关的键盘和鼠标事件,将其传输到服务器,服务器将这些事件注入到设备中。这是唯一一个在两个方向上使用的套接字:输入事件从客户端发送到设备,当设备剪贴板更改时,新内容从设备发送到客户端以支持无缝复制粘贴。
请注意,客户端-服务器角色在应用级别上表示:
- 服务器_提供_视频和音频流,并处理客户端的请求,
- 客户端_控制_设备通过服务器。
但是,默认情况下(当未设置--force-adb-forward
时),网络级别上的角色是反转的:
- 客户端打开一个服务器套接字并在启动服务器之前监听端口,
- 服务器连接到客户端。
这种角色反转确保了连接不会因无竞争条件的情况下失败而失败。
服务器
权限
捕获屏幕需要一些权限,这些权限被授予shell
。
服务器是一个Java应用程序(带有public static void main(String... args)
方法),针对Android框架编译,并作为shell
在Android设备上执行。
要运行这样的Java应用程序,类必须dexed(通常,到classes.dex
)。如果my.package.MainClass
是主类,编译成classes.dex
,推送到设备上的/data/local/tmp
,那么可以用以下命令运行它:
adb shell CLASSPATH=/data/local/tmp/classes.dex app_process / my.package.MainClass
路径/data/local/tmp
是推送服务器的好候选,因为它对shell
是可读写的,但不具有全局可写性,所以恶意应用程序可能无法在客户端执行前替换服务器。
代替原始的_jar_文件,app_process
接受包含classes.dex
的_jar_文件(例如,一个APK)。为了简单起见,并受益于gradle构建系统,服务器被构建为一个(未签名的)APK(重命名为scrcpy-server.jar
)。
隐藏方法
尽管是针对Android框架编译的,[隐藏]方法和类不能直接访问(并且它们可能因不同的Android版本而有所不同)。
不过,可以使用反射调用它们。与隐藏组件的通信由_包装器_类和aidl提供。
执行
服务器由客户端基本上通过执行以下命令启动:
adb push scrcpy-server /data/local/tmp/scrcpy-server.jar
adb forward tcp:27183 localabstract:scrcpy
adb shell CLASSPATH=/data/local/tmp/scrcpy-server.jar app_process / com.genymobile.scrcpy.Server 2.1
示例中的第一个参数(2.1
)是客户端的scrcpy版本。如果客户端和服务器没有精确相同的版本,服务器会失败。客户端和服务器之间的协议可能会随着版本的更新而变化(见下面的协议),并且没有向后或向前兼容性(使用不同的客户端和服务器版本没有意义)。这个检查允许检测配置错误(错误地运行较旧或较新的服务器)。
接下来是一系列的任何数量的参数,以key=value
对的形式。它们的顺序是无关紧要的。可能的键和相关联的值类型可以在服务器和客户端代码中找到。
例如,如果我们执行scrcpy -m1920 --no-audio
,那么服务器执行将如下所示:
# scid是一个随机数,用于标识同一设备上运行的不同客户端
adb shell CLASSPATH=/data/local/tmp/scrcpy-server.jar app_process / com.genymobile.scrcpy.Server 2.1 scid=12345678 log_level=info audio=false max_size=1920
组件
执行时,其main()
方法在("main"线程)上执行。
它解析参数,与客户端建立连接并启动其他"组件":
- 视频流媒体:它捕获视频屏幕并在_videov_套接字上发送编码的视频数据包(来自_videov_线程)。
- 音频流媒体:它使用多个线程捕获原始数据包,提交给编码器,并检索编码的数据包,然后将其发送到_audiov_套接字上。
- 控制器:它在一个线程上的控制_控制_套接字上接收控制消息(通常是输入事件),并在相同的控制_控制_套接字上从另一个线程发送设备消息(例如向客户端传输设备剪贴板内容)。因此,控制_控制_套接字用于两个方向(与_videov_和_audiov_套接字相反)。
屏幕视频编码
编码由[ScreenEncoder
][ScreenEncoder]管理。
视频使用[MediaCodec
][MediaCodec] API进行编码。编解码器对与显示器关联的Surface
的内容进行编码,并将编码的数据包发送到客户端(在_videov_套接字上)。
[ScreenEncoder
][ScreenEncoder]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java
[MediaCodec
][MediaCodec]: https://developer.android.com/reference/android/media/MediaCodec.html
在设备旋转(或折叠)时,编码会重置并重新启动。
只有当表面发生变化时才会产生新的帧。这避免了发送不必要的帧,但默认情况下可能会有缺点:
- 如果设备屏幕没有变化,则启动时不发送任何帧,
- 在快速运动变化后,最后一张帧的质量可能很差。
这两个问题都通过标志[KEY_REPEAT_PREVIOUS_FRAME_AFTER
][repeat-flag]得到了解决。
音频编码
类似地,音频使用[AudioRecord
][AudioRecord]进行捕获,并使用[MediaCodec
][MediaCodec]异步API进行编码。
更多细节可在介绍音频功能的博客帖子scrcpy2中找到。
输入事件注入
控制消息从客户端通过[Controller
][Controller]接收(在单独的线程中运行)。有多种类型的输入事件:
- 键盘码(参见[
KeyEvent
][KeyEvent]), - 文本(特殊字符可能不能直接通过键盘码处理),
- 鼠标移动/点击,
- 鼠标滚动,
- 其他命令(例如切换屏幕亮度和复制剪贴板)。
其中一些需要将输入事件注入系统。为此,它们使用隐藏方法[InputManager.InjectInputEvent()
](由InputManager
包装器暴露出来)。
[Controller
][Controller]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/server/src/main/java/com/genymobile/scrcpy/Controller.java
[KeyEvent
][KeyEvent]: https://developer.android.com/reference/android/view/KeyEvent.html
[MotionEvent
][MotionEvent]: https://developer.android.com/reference/android/view/MotionEvent.html
客户端
客户端依赖于SDL,它提供了跨平台的UI、输入事件、线程管理等API。
视频和音频流由FFmpeg解码。
初始化
客户端解析命令行参数,然后运行两种代码路径之一:
- "正常"模式下的scrcpy([
scrcpy.c
][scrcpy.c]) - OTG模式下的scrcpy([
scrcpy_otg.c
][scrcpy_otg.c])
在本文档的其余部分,我们假设使用的是"正常"模式(阅读OTG模式的代码)。
启动时,客户端:
- 打开_videov_、_audiov_和控制_套接字;
- 将服务器推送到设备上并启动;
- 初始化其组件(解复用器、解码器、录制器等)。
视频和音频流
根据传递给scrcpy
的参数,可能会使用几个组件。
这里是视频和音频组件的概述:
V4L2 sink
/
decoder
/ \
视频 -------------> 解复用器 显示
\
录像机
/
音频 -------------> 解复用器
\
解码器 --- 音频播放器
解复用器负责提取视频和音频数据包(读取一些头文件,将视频流分割成正确的边界等)。
解复用后的数据包可能会被发送到解码器(每个流一个,以产生帧)和录像机(接收视频和音频流以记录单个文件)。数据包在设备上编码(通过MediaCodec
),但在录制时,它们会在客户端异步地多路复用到容器中(MKV或MP4)。
视频帧被发送到屏幕/显示上以在scrcpy窗口中渲染。它们也可能被发送到V4L2目标。
音频“帧”(一组解码的样本)被发送到音频播放器。
控制器
控制器负责向设备发送控制消息。它在一个单独的线程中运行,以避免主线程上的I/O。
在主线程上接收到的SDL事件,输入管理器会创建适当的控制消息。它负责将SDL事件转换为Android事件。然后,它将控制消息推送到控制器持有的队列中。在其自己的线程中,控制器从队列中取出消息,将其序列化并发送到客户端。
协议
客户端和服务器之间的协议必须被视为内部协议:它可能会(并且将会)因为任何原因而随时更改。从版本到版本,所有内容都可能发生变化(套接字数量、必须打开的套接字顺序、线上的数据格式等)。客户端必须始终与匹配的服务器版本一起运行。
本节记录了scrcpy v2.1中的当前协议。
连接
首先,客户端设置adb隧道:
# 默认情况下是反向重定向:计算机监听,设备连接
adb reverse localabstract:scrcpy_<SCID> tcp:27183
# 如果设置了--force-adb forward,作为备选方案(或者),前向重定向:
# 设备监听,计算机连接
adb forward tcp:27183 localabstract:scrcpy_<SCID>
(<SCID>
是一个31位随机数,所以当多个scrcpy实例同时启动时,对于同一个设备不会失败。)
然后,最多打开3个套接字,按顺序:
- 视频套接字
- 音频套接字
- 控制套接字
每个套接字都可以被禁用(分别通过--no-video
、--no-audio
和--no-control
直接或间接禁用)。例如,如果设置了--no-audio
,那么首先打开视频套接字,然后打开控制套接字。
在第一个打开的套接字上(无论它是哪一个),如果隧道是前向的,那么设备会向客户端发送一个伪字节。这允许检测连接错误(只要有一个adb前向重定向,即使设备端没有监听,客户端的连接也不会失败)。
在这个第一个套接字上,设备还会向客户端发送一些元数据(目前只有设备名称,用作窗口标题,但将来可能会有其他字段)。
然后每个套接字都被用于其预期的目的。
视频和音频
在视频和音频套接字上,设备首先发送一些编解码器元数据:
- 在视频套接字上,12字节:
- 编解码器ID (
u32
)(H264、H265或AV1) - 初始视频宽度 (
u32
) - 初始视频高度 (
u32
) - 在音频套接字上,4字节:
- 编解码器ID (
u32
)(OPUS、AAC或RAW)
然后,每个由MediaCodec
产生的数据包都会被发送,前面加上一个12字节的帧头:
- 配置数据包标志 (
u1
) - 关键帧标志 (
u1
) - 实时传输协议时间戳 (
u62
) - 数据包大小 (
u32
)
这里是描述帧头的模式:
[. . . . . . . .|. . . .]. . . . . . . . . . . . . . ...
<-------------> <-----> <-----------------------------...
PTS packet raw packet
size
<--------------------->
帧头
PTS的最高有效位用于数据包标志:
字节 7 字节 6 字节 5 字节 4 字节 3 字节 2 字节 1 字节 0
CK...... ........ ........ ........ ........ ........ ........ ........
^^<------------------------------------------------------------------->
|| PTS
| `- 关键帧
`-- 配置数据包
控制
控制消息通过自定义的二进制协议发送。
这个协议的唯一文档是双方各自的单元测试集:
独立服务器
尽管该服务器是为scrcpy客户端设计的,但它可以与使用相同协议的任何客户端一起使用。
为了简化起见,一些特定于服务器的选项已被添加以轻松产生原始流:
send_device_meta=false
:禁用第一个套接字上发送的设备元数据(实际上,设备名称)send_frame_meta=false
:禁用每个数据包的12字节头部send_dummy_byte
:禁用正向连接上发送的伪字节send_codec_meta
:禁用编解码器信息(以及视频的初始设备大小)raw_stream
:禁用上述所有选项
具体来说,如何在TCP套接字上暴露一个原始的H.264流:
adb push scrcpy-server-v2.1 /data/local/tmp/scrcpy-server-manual.jar
adb forward tcp:1234 localabstract:scrcpy
adb shell CLASSPATH=/data/local/tmp/scrcpy-server-manual.jar \
app_process / com.genymobile.scrcpy.Server 2.1 \
tunnel_forward=true audio=false control=false cleanup=false \
raw_stream=true max_size=1920
一旦客户端通过TCP在端口1234上建立连接,设备将开始流式传输视频。例如,VLC可以播放视频(尽管你会经历非常高的延迟,更多细节在这里):
vlc -Idummy --demux=h264 --network-caching=0 tcp://localhost:1234
丑陋的技巧
要了解更多细节,请阅读代码!
如果你发现了一个错误,或者有一个很棒的实现想法,请讨论并贡献它!
调试服务器
服务器在启动时由客户端推送到设备上。
要调试它,在配置期间启用服务器调试器:
meson setup x -Dserver_debugger=true
# 或者,如果x已经配置好了
meson configure x -Dserver_debugger=true
如果您的设备运行的是Android 8或更低版本,请在server_debugger_method
中设置为old
:
meson setup x -Dserver_debugger=true -Dserver_debugger_method=old
# 或者,如果x已经配置好了
meson configure x -Dserver_debugger=true -Dserver_debugger_method=old
然后重新编译。
当你启动scrcpy时,它将在设备的端口5005上启动一个调试器。 将那个端口重定向到计算机:
adb forward tcp:5005 tcp:5005
在Android Studio中,运行 > 调试 > 编辑配置... 在左侧,点击+
,远程,填写表单:
- 主机:
localhost
- 端口:
5005
然后点击_调试_。