Skip to content

第 6 章:Android 视频采集——把三种来源压成一个 Surface

视频模块最漂亮的抽象不是 MediaCodec,而是 SurfaceCapture:编码器只要求“有人能把图像持续画到这个 Surface”,至于来源是真实显示、相机还是新虚拟显示并不重要。

一、统一抽象:SurfaceCapture

SurfaceCapture 定义六个核心阶段:

  1. init(control, constraints):只调用一次,绑定重置控制器和初始约束;
  2. prepare():每次编码会话前计算资源与尺寸;
  3. start(surface):开始把像素写入编码器 Surface;
  4. stop():结束本次捕获;
  5. release():永久释放;
  6. applyNewVideoConstraints():编码失败后尝试降级尺寸。

它还暴露 getSize()isClosed() 以及可选的相机 torch/zoom 能力。这样 SurfaceEncoder 不需要针对每种源写三套编码循环。

二、三个实现解决三类问题

实现来源关键 Android API独特难点
ScreenCapture已有 displaySurfaceControl / display token旋转、裁剪、显示属性变化
CameraCaptureCamera2CameraDevice / CaptureSession选相机、尺寸/FPS、异步关闭、torch/zoom
NewDisplayCapture新虚拟 displayDisplayManager / VirtualDisplay创建生命周期、IME、resize、启动 app

选择发生在 Server.scrcpy()video_source=display 再根据 new_display 分流,否则使用 camera。

三、编码器循环的真正结构

SurfaceEncoder.streamCapture() 不是“一次 configure 后永远 dequeue”,而是外层可重启会话:

创建 MediaCodec 与基础 MediaFormat → 查询硬件尺寸 alignment → capture.init() → 写 codec id → [循环] 消费 reset reason capture.prepare() 得到新 size codec.configure(width,height) createInputSurface() capture.start(surface) codec.start() 写 session meta dequeue / write packets,直到 EOS 或 reset capture.stop() / codec.stop() / codec.reset() → release codec 与 capture

MediaCodec 对象复用、每次 reset(),既支持旋转/resize,又避免进程级重建和 socket 断线。

四、为什么用 Surface 输入

MediaCodecCOLOR_FormatSurface 让显示系统或 Camera2 直接把图像送入编码器,省去 CPU 侧读取 RGBA、颜色转换和再次复制。格式还设置:

  • 名义 frame rate 60(编码器配置必填,实际内容仍可变帧率);
  • I-frame 间隔 10 秒;
  • 100 ms 无新画面时重复前帧;
  • 实时优先级;
  • 支持时设置 latency=1;
  • 私有/公开 max-fps-to-encoder 限速。

createFormat()。重复前帧不只是视觉优化,它确保首帧尽快出现,并帮助长时间静止后恢复质量。

五、显示采集与变换

ScreenCapture 在 prepare 阶段读取 display 大小、旋转、裁剪和请求角度,计算最终 video size 与仿射变换。捕获开始时,它把编码器 Surface 绑定为 display projection 的输出。

屏幕旋转不是简单交换 width/height。系统必须同时保证:

  • 编码尺寸满足硬件 alignment;
  • crop 在源坐标系中有效;
  • 角度和方向锁定组合正确;
  • 控制端触摸坐标仍可映射回当前 display;
  • display 属性变化能触发 capture reset。

实现入口见 ScreenCapture.init()prepare()

六、相机采集不是屏幕采集的特例

相机路径要先选择 camera id,再在可用输出尺寸中根据显式 size、max-size、宽高比和 high-speed 模式筛选。CameraCapture 使用独立 HandlerThread 承接 Camera2 回调,并用锁/条件等待异步 onOpenedonConfigured 或错误。

相机 zoom 和 torch 不经过重建整条控制管线:控制消息抵达服务端后调用当前 SurfaceCapture 的能力方法。相机实现对 zoom 值做 clamp,再更新 repeating request;代码见 CameraCapture

Android 12 之前直接拒绝 camera source,这个版本约束在进入任务装配前验证,而不是等 Camera2 深处报晦涩异常。

七、虚拟显示为何需要 controller 协作

NewDisplayCapture 创建 display 后,系统才知道真实 virtual display id。输入控制和启动 app 都需要这个 id,因此 capture 会回调 Controller.onNewVirtualDisplay(),同时提供 PositionMapper

这形成一个有意识的晚绑定:

  1. controller 可以先创建;
  2. video capture 异步创建 virtual display;
  3. display id 出现后原子发布给 controller;
  4. startApp 最多等待 1 秒获取 id;
  5. 触摸使用 mapper 转换坐标。

虚拟显示 resize 由桌面窗口反向发消息,请求新宽高;NewDisplayCapture.requestResize() 更新约束并触发 capture reset,见 NewDisplayCapture

八、reset reason 是状态变化的语义

CaptureControl 不只存一个 boolean,而按位记录原因:终止、显示属性变化、客户端 resize、客户端 reset 等。SurfaceEncoder 消费原因后可判断新 session 是否属于 client_resized

原因位还能跨失败重试保留:如果一次重建因编码器异常失败,下一次 retry 仍携带原 reset 原因,不会把用户 resize 误认成设备旋转。

九、编码失败如何逐级降级

Android 硬件编码器的声明能力并不总可靠。scrcpy 的策略分三层:

  1. 初次不按声明 capabilities 过度约束,只尊重 alignment;
  2. 失败后再应用 encoder capabilities;
  3. 仍失败则从 2560、1920、1600、1280、1024、800 中选择比当前更小的 max size。

如果首帧已经成功后偶发错误,先等待 50 ms 重试;连续三次才最终失败。逻辑见 prepareRetry()

这是“乐观运行、证据触发降级”:不因厂商报告保守而提前损失分辨率,也不因一次瞬态错误立刻退出。

十、packet 输出与错误语义

编码循环无限等待 dequeueOutputBuffer。拿到包后标记 config/keyframe,把 presentationTimeUs 原样写入协议。broken pipe 被视为正常关闭,不打印堆栈;其他 IO 或配置问题通知 TerminationListener 为 fatal。

值得注意的是 video 处理器结束总是 onTerminated(true),因为失去视频通常意味着主要镜像能力终止。音频处理器则能报告非 fatal 禁用,下一章会看到这一区别。

十一、本章小结

Android 视频侧把“来源差异”封装在 SurfaceCapture,把“编码与重启”封装在 SurfaceEncoder,再用 reset reason 与 session meta 把动态尺寸传播到桌面。下一章跨过 socket,分析客户端如何以 source/sink 图同时满足播放、录制、V4L2 和丢旧帧保低延迟。

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