Skip to content

系列二 | Android 实时屏幕共享与 MediaCodec 硬件编解码优化实践

作为移动端开源生态的代表,Android 系统的硬件编解码接口(MediaCodec)一直以“多坑”、“设备碎片化严重”而著称。在开发低延迟远程协助系统时,Android 设备不仅需要作为受控端(发送端)进行高效投屏,还常常作为控制端(接收端)来解码渲染另一台设备的画面。

要在这两端都实现丝滑的低延迟体验,就必须完美解决 Android 平台的几个硬伤:前台服务采集权限限制、屏幕宽高比旋转导致的画面撕裂,以及硬件解码器偶发的卡死与花屏问题。

本文将分享易连系统在 Android 端如何深度结合 MediaProjectionOpenGL ESMediaCodec 打造高效、稳定的音视频闭环管线。


1. 受控端采集:MediaProjection 与前台服务的严格绑定

在 Android 10 (API 29) 及以上系统中,谷歌对隐私安全实施了更严苛的管控。要在应用外截取屏幕,必须通过 MediaProjection API,且该操作必须绑定一个声明为 mediaProjection 类型的 前台服务(Foreground Service)

业务构建流程:

  1. 权限申请:通过 MediaProjectionManager.createScreenCaptureIntent() 发起系统级投屏授权弹窗,用户点击同意后获取一个 Intent 凭证。
  2. 服务启动:立即通过 startForegroundService 启动我们的协助前台服务。
  3. 类型声明:在 AndroidManifest.xml 中,服务必须显式标注:
    xml
    <service
        android:name=".AssistScreenCaptureService"
        android:foregroundServiceType="mediaProjection" />
  4. 构建 Projection:在服务中利用申请到的 Intent 凭证初始化 MediaProjection 实例,以此作为全局屏幕像素流的采集源。

2. 零 CPU 拷贝:MediaCodec 表面输入模式(Surface Mode)

传统的 Android 截屏方案使用 ImageReader 循环读取像素,这需要将图像数据从 GPU 拷贝到 CPU 内存,转换成 YUV 格式,然后再作为字节数组送入 MediaCodec。这种做法在 1080p 分辨率下会导致 CPU 占用飙升,手机严重发热并频繁掉帧。

易连助手采用了 表面模式(Surface Mode) 的零拷贝架构:

[ 屏幕采集 (MediaProjection) ]

              ├─ (直接绘制像素到 Surface)

    [ 硬件编码器输入 Surface ] (由 createInputSurface() 创建)

              ▼ (底层显存直接硬编)
       [ MediaCodec ] (COLOR_FormatSurface 格式)
  1. 配置编码器:初始化 MediaCodec 视频编码器(类型为 video/avc),并将输入颜色格式设置为 MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface
  2. 创建输入表面:在调用 codec.configure(...) 之后,通过 codec.createInputSurface() 从编码器获取一个专属的 Surface
  3. 直接投射:调用 mediaProjection.createVirtualDisplay(...) 创建虚拟显示器,将上述获取的编码器 Surface 直接作为输出载体。 通过这种配置,Android 系统底层的显示系统(SurfaceFlinger)直接将屏幕合成后的帧像素写入硬件编码器的 GPU 缓存中,CPU 自始至终不参与图像数据的搬运,实现了物理级别的零拷贝硬件编码

3. 画面无撕裂:OpenGL ES 动态旋转与缩放旋转器

当 Android 手机在协助过程中发生横竖屏物理旋转时,VirtualDisplay 发送给编码器的像素分辨率宽高比会瞬间反转(例如从 1080x2400 变为 2400x1080)。由于 MediaCodec 在配置后无法动态更改输入 Surface 的物理宽高,直接旋转会导致视频流画面严重变形拉伸,或者接收端因帧尺寸不匹配而崩溃。

为了完美解决此问题,我们设计了 AssistCapturePortraitLandscapeRotator(OpenGL 帧转换器)

[ MediaProjection 采集源 ] ──► [ OES 外部纹理 (Texture) ]

                                      ▼ (OpenGL 渲染管线)
                          [ 矩阵变换: 旋转 / 保持纵横比等比缩放 ]

                                      ▼ (EGLSurface 绘制)
                          [ MediaCodec 输入 Surface (固定宽高) ]
  1. 中间图层:我们不直接把 VirtualDisplay 绑定给编码器,而是将其渲染输出到一个 OpenGL 的 OES 外部纹理(SamplerExternalOES)
  2. EGL Context 管理:创建一个独立的背景 EGL 渲染线程,并将 MediaCodec 的输入 Surface 包装为一个 EGLSurface
  3. 矩阵投影变换:当手机发生旋转时,OpenGL 线程捕获旋转角度,重新计算正交投影矩阵(Orthographic Projection Matrix):
    • 等比缩放:根据目标编码分辨率,对画面进行等比例缩放并居中,四周自动补充黑边,避免画面变形。
    • 矩阵旋转:在顶点着色器(Vertex Shader)中应用旋转矩阵,在 GPU 中完成帧像素的物理转角。
  4. 无缝渲染:通过 eglSwapBuffers 将变换后的帧画面写入编码器的 Input Surface。这保证了发送出的视频流物理宽高始终恒定,消除了画面变形与渲染撕裂。

4. 控制端解码:MediaCodec 三轨输出与热重置恢复

当 Android 设备充当协助者控制端时,接收到 peer 发送来的 H.264 裸流,需要使用 MediaCodec 解码器进行解析还原。我们为其设计了灵活的三轨输出通道:

  1. 直通 Surface 渲染模式(Zero-Copy rendering):这是最快、延迟最低的路径。解码器配置时直接绑定一个 UI 层的 SurfaceViewTextureView 的 Surface。解码完成的帧数据直接由 GPU 硬件合成为显示画面,延迟几乎为零。
  2. ImageReader 监听模式:当需要截取当前协助画面生成缩略图、计算触控点击映射坐标、或进行视觉标记时,解码器会将输出重定向到 ImageReader(配置为 COLOR_FormatFormatSurface 色彩格式),从中提取 HardwareBuffer 转换为高保真 Bitmap。
  3. ByteBuffer 传统模式:用于不支持上述硬件特性的老旧 Android 设备的兼容性软件提取路径。

鲁棒性防花屏重置机制

移动网络(如 4G/5G 切换)极易发生丢包,导致解码器收到破损的数据帧,引发严重的画面绿屏、花屏或者解码器线程挂起(Codec Hang)。

易连助手通过监测解码回调和延迟 telemetry 建立了一套热重置恢复方案

  • 异常捕获:当解码器抛出 MediaCodec.CodecException 或连续几帧由于关键帧丢失导致解码时延大幅攀升时,触发 requiresMediaCodecDecoderHardReset
  • 热重置(Hot Reset):在不销毁 UI 渲染视图的前提下,瞬间执行 codec.flush()codec.stop(),重新实例化解码器。
  • 请求同步:重置完成后,向发送端极速发送一个自定义的信令包,要求发送端立刻强制生成一个 H.264 IDR 关键帧,实现画面闪电恢复。

这套精心设计的 Android 端采集和编解码管线,极大增强了易连系统在弱网移动环境下的生存能力和交互稳定性。在下一篇中,我们将跨越到桌面端,深度解构 Windows DXGI 与 Linux PipeWire 的高性能实现。

Released under the MIT License. Terms | Privacy