Skip to content

第 1 章:全局总览——scrcpy 到底是什么系统

一句话定义:scrcpy 是一个由桌面原生客户端和 Android shell 服务端组成的、通过 ADB 建链、以自定义二进制协议传输媒体与控制消息的低延迟远程显示系统。

一、不要被“投屏工具”四个字骗了

从用户视角,scrcpy 做三件事:显示屏幕、播放声音、把键鼠送回设备。但源码必须同时解决九类问题:设备发现、服务端部署、端口隧道、屏幕或相机采集、硬件编码、网络封帧、客户端解码、窗口渲染、输入注入。录制、虚拟显示、V4L2 和 USB HID 又横切其中。

最重要的认识是:它不是把 Android UI 远程搬到桌面运行。Android 仍负责产生画面和处理输入,桌面端只是媒体消费者与控制生产者。

二、两端四层

┌──────────────────────────── 桌面端(C) ────────────────────────────┐ │ CLI / options │ │ ADB 编排 ── socket 接入 ── demux / decode ── SDL 窗口与音频 │ │ └──────────────→ recorder / V4L2 │ │ SDL 输入 ── input_manager ── SDK / UHID / AOA processor │ └───────────────────────────┬────────────────────────────────────────┘ │ ADB reverse/forward + 3 sockets ┌───────────────────────────▼────────────────────────────────────────┐ │ Android 端(Java,shell UID) │ │ DesktopConnection / Streamer │ │ SurfaceCapture ── MediaCodec ── video socket │ │ AudioCapture ── MediaCodec ── audio socket │ │ control socket ── Controller ── InputManager / UHID / 系统服务 │ └────────────────────────────────────────────────────────────────────┘>

这张图可以再归纳成四层责任:

桌面端Android 端关键边界
编排层CLI、设备选择、推送 jar、启动与退出参数解析、任务装配命令行参数
传输层ADB tunnel、socket、demuxLocalSocket、Streamer自定义二进制协议
媒体层FFmpeg 解码/封装、SDL 播放Surface/AudioRecord、MediaCodec编码包与时间戳
控制层SDL 事件、processor 策略Controller、系统服务双向控制消息

客户端的聚合对象 struct scrcpy 把这些组件作为值成员放在一起,见 app/src/scrcpy.c。这不是面向对象继承树,而是一个显式的组件容器。

三、三条正向数据流

3.1 视频流

  1. ScreenCaptureCameraCaptureNewDisplayCapture 把图像绘制到编码器输入 Surface
  2. SurfaceEncoderMediaCodec 取出压缩包;
  3. Streamer 写 codec id、session meta 和逐帧 12 字节头;
  4. 客户端 demuxer 还原 AVPacket
  5. 包可直接进入 recorder,也可进入 decoder
  6. 解码帧再送给 screen 或 Linux v4l2_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() 组装。

这带来三个关键结果:

  1. 不需要安装一个有 Activity 的常驻应用;
  2. 进程以 ADB shell 身份运行,可以使用 shell 已有的系统权限;
  3. 没有正常 Android app 的 Application/Context 生命周期,所以源码必须构造 FakeContext、反射系统服务,并填充 ActivityThread 内部状态。

这种模式是 scrcpy 强大能力的来源,也是它兼容性代码集中的根因。

七、最值得迁移的三个设计

显式装配胜过全局魔法

scrcpy.c 逐个初始化组件、连接 sink、启动线程,并用布尔值记录哪些资源已成功创建。代码很长,却把真实拓扑和所有权放在一个可审计的位置。

数据层次决定扩展点

要原样录制,接在 packet 层;要显示或输出原始帧,接在 frame 层;要改变输入注入,替换 processor。扩展点不是为了抽象而抽象,而是沿数据形态自然切开。

失败隔离按能力划分

音频不可用可降级为纯视频;缺少本地解码器可禁用对应 sink;连接或控制通道失败则终止。每个失败是否致命,取决于它是否破坏用户请求的核心能力,而不是一律退出。

八、本章小结

scrcpy 的核心不是某个编码器,而是双端分工 + 三通道隔离 + 两级媒体分流 + 策略化输入。接下来进入项目目录,看这些职责怎样落进 259 个核心文件,而不是被文件数量淹没。

基于 scrcpy 4.1 源码的独立学习笔记