跳转到内容

面向开发者的 scrcpy 文档

该应用由两部分组成:

  • 服务端(scrcpy-server),在设备上运行;
  • 客户端(scrcpy 可执行文件),在宿主电脑上运行。

客户端负责将服务端推送到设备并启动其执行。

客户端与服务端分别使用视频、音频与控制三类独立套接字进行通信。其中任意一种可以被禁用(但不能全部禁用),因此会有 1、2 或 3 个套接字。

服务端首先在第一个套接字上发送设备名称(用于设置 scrcpy 窗口标题),随后每个套接字用于其各自的用途。客户端与服务端两侧,每个套接字的读写都由独立线程执行。

若启用视频,服务端会发送设备屏幕的原始视频流(默认 H.264),并在每个包中附带一些额外的头部。客户端对视频帧进行解码,并尽快显示且不进行缓冲(除非指定了 --video-buffer=delay),以尽量降低时延。客户端无需了解设备的旋转状态(由服务端处理),它只知晓所接收视频帧的尺寸。

同样地,若启用音频,服务端会发送设备音频输出的原始音频流(默认 OPUS;若指定 --audio-source=mic 则为麦克风输入),并在每个包中附带一些额外的头部。客户端对音频流进行解码,并通过维持平均缓冲量来尽量保持较低时延。scrcpy v2.0 发布的博客文章对音频功能有更多细节。

若启用控制,客户端会捕获相关键盘与鼠标事件,并传输给服务端,由服务端注入到设备。该套接字是唯一双向使用的:输入事件从客户端发送到设备;当设备剪贴板发生变化时,新内容会从设备发送到客户端,以支持无缝的复制粘贴。

需要注意的是,客户端与服务端的角色定义体现在应用层:

  • 服务端负责“提供”视频与音频流,并处理来自客户端的请求;
  • 客户端通过服务端来“控制”设备。

不过,在网络层默认情况下(未设置 --force-adb-forward),两者角色是反转的:

  • 客户端在启动服务端之前,先打开一个服务器套接字并监听某端口;
  • 服务端主动连接到客户端。

这种角色反转保证了在不进行轮询的情况下,连接不会因竞争条件而失败。

捕获屏幕需要一定权限,这些权限被授予给 shell 用户。

服务端是一个 Java 应用(包含 public static void main(String... args) 方法),基于 Android 框架编译,并以 shell 身份在 Android 设备上执行。

要运行此类 Java 应用,其类必须被 dex(通常生成 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 可读写,但非全员可写,因此恶意应用无法在客户端执行前替换服务端文件。

除了原始的 dex 文件,app_process 也接受包含 classes.dexjar(例如一个 APK)。为简化并受益于 Gradle 构建系统,服务端会构建为未签名的 APK(重命名为 scrcpy-server.jar)。

尽管是基于 Android 框架编译,隐藏的方法与类并不可直接访问(且不同 Android 版本间可能有所差异)。

不过,可以通过反射进行调用。与隐藏组件的通信由[包装类]wrappersaidl 提供。

客户端基本上通过执行以下命令来启动服务端:

Terminal window
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 时,服务端的实际执行将类似:

Terminal window
# scid is a random number to identify different clients running on the same device
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() 方法(在“主”线程)会解析参数、与客户端建立连接并启动其它“组件”:

  • 视频推流器:捕获屏幕并在 video 套接字(由 video 线程)上发送编码后的视频包;
  • 音频推流器:使用多个线程捕获原始音频包,提交编码并取回编码包,在 audio 套接字上发送;
  • 控制器:在一个线程上通过 control 套接字接收_控制消息_(通常为输入事件),并在另一个线程上通过同一个 control 套接字发送_设备消息_(例如将设备剪贴板内容传给客户端)。因此 control 套接字是双向使用的(不同于 videoaudio 套接字)。

编码由 ScreenEncoder 管理。

视频通过 MediaCodec API 进行编码。编码器对与显示相关联的 Surface 内容进行编码,并将编码包写入客户端(通过 video 套接字)。

设备旋转(或折叠)时,编码会话会被重置并重新启动。

仅在 surface 发生变化时才会产生新帧。这样避免发送不必要的帧,但默认情况下也有一些缺点:

  • 如果设备屏幕在启动时没有变化,则不会发送任何帧;
  • 在快速运动变化后,最后一帧的质量可能较差。

这两个问题通过标志位 KEY_REPEAT_PREVIOUS_FRAME_AFTER 得到解决

同样地,音频通过 AudioRecord 进行采集,并借助 MediaCodec 异步 API 进行编码

关于音频功能的更多细节可参考介绍该功能的博客文章

Controller(在独立线程运行)从客户端接收_控制消息_。输入事件类型包括:

  • 键码(参见 KeyEvent);
  • 文本(某些特殊字符无法直接通过键码处理);
  • 鼠标移动/点击;
  • 鼠标滚动;
  • 其他命令(例如点亮屏幕或复制剪贴板)。

其中部分需要向系统注入输入事件。为此,它们使用_隐藏_方法 InputManager.injectInputEvent()(由 InputManager 包装器 暴露)。

客户端基于 SDL,它提供跨平台的 UI、输入事件、线程等 API。

视频与音频流由 FFmpeg 解码。

客户端解析命令行参数,然后执行两种代码路径之一

In the remaining of this document, we assume that the “normal” mode is used (read the code for the OTG mode).

On startup, the client:

  • opens the video, audio and control sockets;
  • pushes and starts the server on the device;
  • initializes its components (demuxers, decoders, recorder…).

Depending on the arguments passed to scrcpy, several components may be used. Here is an overview of the video and audio components:

V4L2 sink
/
decoder
/ \
VIDEO -------------> demuxer display
\
recorder
/
AUDIO -------------> demuxer
\
decoder --- audio player

The demuxer is responsible to extract video and audio packets (read some header, split the video stream into packets at correct boundaries, etc.).

The demuxed packets may be sent to a decoder (one per stream, to produce frames) and to a recorder (receiving both video and audio stream to record a single file). The packets are encoded on the device (by MediaCodec), but when recording, they are muxed (asynchronously) into a container (MKV or MP4) on the client side.

Video frames are sent to the screen/display to be rendered in the scrcpy window. They may also be sent to a V4L2 sink.

Audio “frames” (an array of decoded samples) are sent to the audio player.

The controller is responsible to send control messages to the device. It runs in a separate thread, to avoid I/O on the main thread.

On SDL event, received on the main thread, the input manager creates appropriate control messages. It is responsible to convert SDL events to Android events. It then pushes the control messages to a queue hold by the controller. On its own thread, the controller takes messages from the queue, that it serializes and sends to the client.

The protocol between the client and the server must be considered internal: it may (and will) change at any time for any reason. Everything may change (the 套接字数量、套接字打开的顺序以及线上的数据格式等会随版本变化。客户端必须始终与匹配的服务端版本一起运行。

本节记录 scrcpy v2.1 的当前协议。

首先,客户端建立一个 adb 隧道:

Terminal window
# By default, a reverse redirection: the computer listens, the device connects
adb reverse localabstract:scrcpy_<SCID> tcp:27183
# As a fallback (or if --force-adb forward is set), a forward redirection:
# the device listens, the computer connects
adb forward tcp:27183 localabstract:scrcpy_<SCID>

<SCID> 是一个 31 位随机数,以避免同一设备上多个 scrcpy 实例“同时”启动时发生冲突。)

随后将按以下顺序最多打开 3 个套接字:

  • 一个 video 套接字
  • 一个 audio 套接字
  • 一个 control 套接字

它们都可被禁用(分别通过 --no-video--no-audio--no-control,直接或间接)。例如,若设置了 --no-audio,则先打开 video 套接字,再打开 control 套接字。

在打开的_第一个_套接字上(无论是哪一个),如果隧道是 forward,设备会向客户端发送一个占位字节。这用于检测连接错误(只要存在 adb forward 重定向,客户端连接不会失败,即使设备端没有任何监听)。

仍在这个_第一个_套接字上,设备会向客户端发送一些元数据(目前仅设备名称,用作窗口标题;未来可能增加更多字段)。

更多细节可参阅客户端服务端的实现代码。

随后,各套接字用于其各自用途。

videoaudio 套接字上,设备首先发送一些编解码器元数据

  • video 套接字上发送 12 字节:
    • 编解码器 ID(u32)(H264、H265 或 AV1)
    • 初始视频宽度(u32
    • 初始视频高度(u32
  • audio 套接字上发送 4 字节:
    • 编解码器 ID(u32)(OPUS、AAC 或 RAW)

随后,会发送由 MediaCodec 产生的每个数据包,并以 12 字节的帧头作为前缀:

  • 配置包标志(u1
  • 关键帧标志(u1
  • PTS(u62
  • 包大小(u32

以下为帧头的示意图:

[. . . . . . . .|. . . .]. . . . . . . . . . . . . . . ...
<-------------> <-----> <-----------------------------...
PTS packet raw packet
size
<--------------------->
frame header
The most significant bits of the PTS are used for packet flags:
byte 7 byte 6 byte 5 byte 4 byte 3 byte 2 byte 1 byte 0
CK...... ........ ........ ........ ........ ........ ........ ........
^^<------------------------------------------------------------------->
|| PTS
| `- key frame
`-- config packet

控制消息通过自定义二进制协议传输。

该协议的唯一文档是两侧的一组单元测试:

尽管服务端是为 scrcpy 客户端设计的,但任何使用相同协议的客户端都可以使用它。

为简化操作,添加了一些服务端特定选项,便于生成原始流:

  • send_device_meta=false:禁用在_第一个_套接字上发送的设备元数据(实际为设备名称)
  • send_frame_meta=false:禁用每个数据包的 12 字节头部
  • send_dummy_byte:禁用在 forward 连接上发送的占位字节
  • send_codec_meta:禁用编解码器信息(以及视频的初始设备尺寸)
  • raw_stream:禁用以上所有功能

具体来说,以下演示了如何在 TCP 套接字上暴露一个原始 H.264 流:

Terminal window
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

想了解更多细节,请直接阅读源码!

如果你发现了 bug,或有很棒的想法想要实现,欢迎讨论与贡献 ;-)

启动时,客户端会将服务端推送到设备。

要进行调试,请在配置期间启用服务端调试器:

Terminal window
meson setup x -Dserver_debugger=true
# or, if x is already configured
meson configure x -Dserver_debugger=true

随后重新编译并运行 scrcpy。

对于 Android < 11,设备会在 5005 端口上启动调试器并等待: 将该端口转发到电脑:

Terminal window
adb forward tcp:5005 tcp:5005

对于 Android >= 11,首先查找监听端口:

Terminal window
adb jdwp
# press Ctrl+C to interrupt

然后转发得到的 PID:

Terminal window
adb forward tcp:5005 jdwp:XXXX # replace XXXX

在 Android Studio 中,依次点击 Run > Debug > Edit configurations… 左侧点击 +,选择 Remote,并填写表单:

  • Host:localhost
  • Port:5005

最后点击 Debug