第 1 章:全局总览——scrcpy 到底是什么系统
一句话定义:scrcpy 是一个由桌面原生客户端和 Android shell 服务端组成的、通过 ADB 建链、以自定义二进制协议传输媒体与控制消息的低延迟远程显示系统。
一、不要被“投屏工具”四个字骗了
从用户视角,scrcpy 做三件事:显示屏幕、播放声音、把键鼠送回设备。但源码必须同时解决九类问题:设备发现、服务端部署、端口隧道、屏幕或相机采集、硬件编码、网络封帧、客户端解码、窗口渲染、输入注入。录制、虚拟显示、V4L2 和 USB HID 又横切其中。
最重要的认识是:它不是把 Android UI 远程搬到桌面运行。Android 仍负责产生画面和处理输入,桌面端只是媒体消费者与控制生产者。
二、两端四层
这张图可以再归纳成四层责任:
| 层 | 桌面端 | Android 端 | 关键边界 |
|---|---|---|---|
| 编排层 | CLI、设备选择、推送 jar、启动与退出 | 参数解析、任务装配 | 命令行参数 |
| 传输层 | ADB tunnel、socket、demux | LocalSocket、Streamer | 自定义二进制协议 |
| 媒体层 | FFmpeg 解码/封装、SDL 播放 | Surface/AudioRecord、MediaCodec | 编码包与时间戳 |
| 控制层 | SDL 事件、processor 策略 | Controller、系统服务 | 双向控制消息 |
客户端的聚合对象 struct scrcpy 把这些组件作为值成员放在一起,见 app/src/scrcpy.c。这不是面向对象继承树,而是一个显式的组件容器。
三、三条正向数据流
3.1 视频流
ScreenCapture、CameraCapture或NewDisplayCapture把图像绘制到编码器输入Surface;SurfaceEncoder从MediaCodec取出压缩包;Streamer写 codec id、session meta 和逐帧 12 字节头;- 客户端
demuxer还原AVPacket; - 包可直接进入
recorder,也可进入decoder; - 解码帧再送给
screen或 Linuxv4l2_sink。
设备端装配发生在 Server.scrcpy() 的视频分支,客户端分流发生在 scrcpy() 的 demux/decoder/recorder 装配段。
3.2 音频流
音频可以来自系统播放、麦克风、语音通话等 source。编码格式默认 Opus,也支持 AAC、FLAC 和原始 PCM。客户端解码后并不直接交给声卡,而先经过 audio_regulator:它维持目标缓冲量,并用重采样补偿设备时钟与电脑音频时钟的漂移。
3.3 控制流
控制是双向的:
- 桌面 → 设备:按键、文本、触摸、滚轮、剪贴板、显示电源、UHID、启动应用等 23 种消息;
- 设备 → 桌面:剪贴板内容、剪贴板 ACK、UHID output 三种消息。
两种方向共用一条全双工 socket,但各自有独立消息格式和读写线程。客户端枚举见 control_msg.h,反向消息见 device_msg.h。
四、为什么是三条 socket
把视频、音频和控制多路复用进一条连接看起来更“标准”,却会带来额外帧类型、长度管理、优先级和队头阻塞。scrcpy 的选择是让底层 TCP/LocalSocket 各自承担顺序与流控:
- 视频带宽大,允许跳过展示帧,但不能拖慢控制;
- 音频对连续性敏感,失败后通常可以仅禁用音频;
- 控制消息小,但必须低延迟,部分消息绝不能因队列满而丢失。
代价是通道建立顺序必须严格一致。Android 端按 video → audio → control 顺序连接或接受,桌面端也按同样的启用组合分配 first socket,见 DesktopConnection.open() 与 sc_server_connect_to()。
五、低延迟不是一个开关
scrcpy 的低延迟来自一组一致的取舍:
| 环节 | 选择 | 牺牲 |
|---|---|---|
| Android 编码 | Surface 输入、实时优先级、低延迟 hint | 厂商编码器差异更难处理 |
| 传输 | 原始 socket + 极薄帧头 | 协议需双端手工维护 |
| 解码 | FFmpeg AV_CODEC_FLAG_LOW_DELAY | 更少的重排序余量 |
| 视频显示 | 单帧缓冲,消费者慢就覆盖旧帧 | 画面可能跳帧 |
| 音频播放 | 小目标缓冲 + 渐进调速 | 算法复杂,需处理 underflow |
| 控制 | 独立通道和发送线程 | 生命周期对象更多 |
因此“低延迟”不是某个 low_latency=true,而是从采集到 UI 的每一层都拒绝无界排队。
六、server 为什么不是普通 APK
设备端通过 adb shell CLASSPATH=/data/local/tmp/scrcpy-server.jar app_process / com.genymobile.scrcpy.Server ... 启动,命令由 execute_server() 组装。
这带来三个关键结果:
- 不需要安装一个有 Activity 的常驻应用;
- 进程以 ADB shell 身份运行,可以使用 shell 已有的系统权限;
- 没有正常 Android app 的
Application/Context生命周期,所以源码必须构造FakeContext、反射系统服务,并填充ActivityThread内部状态。
这种模式是 scrcpy 强大能力的来源,也是它兼容性代码集中的根因。
七、最值得迁移的三个设计
显式装配胜过全局魔法
scrcpy.c 逐个初始化组件、连接 sink、启动线程,并用布尔值记录哪些资源已成功创建。代码很长,却把真实拓扑和所有权放在一个可审计的位置。
数据层次决定扩展点
要原样录制,接在 packet 层;要显示或输出原始帧,接在 frame 层;要改变输入注入,替换 processor。扩展点不是为了抽象而抽象,而是沿数据形态自然切开。
失败隔离按能力划分
音频不可用可降级为纯视频;缺少本地解码器可禁用对应 sink;连接或控制通道失败则终止。每个失败是否致命,取决于它是否破坏用户请求的核心能力,而不是一律退出。
八、本章小结
scrcpy 的核心不是某个编码器,而是双端分工 + 三通道隔离 + 两级媒体分流 + 策略化输入。接下来进入项目目录,看这些职责怎样落进 259 个核心文件,而不是被文件数量淹没。