第 6 章:Android 视频采集——把三种来源压成一个 Surface
视频模块最漂亮的抽象不是
MediaCodec,而是SurfaceCapture:编码器只要求“有人能把图像持续画到这个 Surface”,至于来源是真实显示、相机还是新虚拟显示并不重要。
一、统一抽象:SurfaceCapture
SurfaceCapture 定义六个核心阶段:
init(control, constraints):只调用一次,绑定重置控制器和初始约束;prepare():每次编码会话前计算资源与尺寸;start(surface):开始把像素写入编码器 Surface;stop():结束本次捕获;release():永久释放;applyNewVideoConstraints():编码失败后尝试降级尺寸。
它还暴露 getSize()、isClosed() 以及可选的相机 torch/zoom 能力。这样 SurfaceEncoder 不需要针对每种源写三套编码循环。
二、三个实现解决三类问题
| 实现 | 来源 | 关键 Android API | 独特难点 |
|---|---|---|---|
ScreenCapture | 已有 display | SurfaceControl / display token | 旋转、裁剪、显示属性变化 |
CameraCapture | Camera2 | CameraDevice / CaptureSession | 选相机、尺寸/FPS、异步关闭、torch/zoom |
NewDisplayCapture | 新虚拟 display | DisplayManager / VirtualDisplay | 创建生命周期、IME、resize、启动 app |
选择发生在 Server.scrcpy():video_source=display 再根据 new_display 分流,否则使用 camera。
三、编码器循环的真正结构
SurfaceEncoder.streamCapture() 不是“一次 configure 后永远 dequeue”,而是外层可重启会话:
把 MediaCodec 对象复用、每次 reset(),既支持旋转/resize,又避免进程级重建和 socket 断线。
四、为什么用 Surface 输入
MediaCodec 的 COLOR_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 回调,并用锁/条件等待异步 onOpened、onConfigured 或错误。
相机 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。
这形成一个有意识的晚绑定:
- controller 可以先创建;
- video capture 异步创建 virtual display;
- display id 出现后原子发布给 controller;
startApp最多等待 1 秒获取 id;- 触摸使用 mapper 转换坐标。
虚拟显示 resize 由桌面窗口反向发消息,请求新宽高;NewDisplayCapture.requestResize() 更新约束并触发 capture reset,见 NewDisplayCapture。
八、reset reason 是状态变化的语义
CaptureControl 不只存一个 boolean,而按位记录原因:终止、显示属性变化、客户端 resize、客户端 reset 等。SurfaceEncoder 消费原因后可判断新 session 是否属于 client_resized。
原因位还能跨失败重试保留:如果一次重建因编码器异常失败,下一次 retry 仍携带原 reset 原因,不会把用户 resize 误认成设备旋转。
九、编码失败如何逐级降级
Android 硬件编码器的声明能力并不总可靠。scrcpy 的策略分三层:
- 初次不按声明 capabilities 过度约束,只尊重 alignment;
- 失败后再应用 encoder capabilities;
- 仍失败则从 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 和丢旧帧保低延迟。