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

然后点击_调试_。