系列二 | Android 实时屏幕共享与 MediaCodec 硬件编解码优化实践
作为移动端开源生态的代表,Android 系统的硬件编解码接口(MediaCodec)一直以“多坑”、“设备碎片化严重”而著称。在开发低延迟远程协助系统时,Android 设备不仅需要作为受控端(发送端)进行高效投屏,还常常作为控制端(接收端)来解码渲染另一台设备的画面。
要在这两端都实现丝滑的低延迟体验,就必须完美解决 Android 平台的几个硬伤:前台服务采集权限限制、屏幕宽高比旋转导致的画面撕裂,以及硬件解码器偶发的卡死与花屏问题。
本文将分享易连系统在 Android 端如何深度结合 MediaProjection、OpenGL ES 和 MediaCodec 打造高效、稳定的音视频闭环管线。
1. 受控端采集:MediaProjection 与前台服务的严格绑定
在 Android 10 (API 29) 及以上系统中,谷歌对隐私安全实施了更严苛的管控。要在应用外截取屏幕,必须通过 MediaProjection API,且该操作必须绑定一个声明为 mediaProjection 类型的 前台服务(Foreground Service)。
业务构建流程:
- 权限申请:通过
MediaProjectionManager.createScreenCaptureIntent()发起系统级投屏授权弹窗,用户点击同意后获取一个Intent凭证。 - 服务启动:立即通过
startForegroundService启动我们的协助前台服务。 - 类型声明:在
AndroidManifest.xml中,服务必须显式标注:xml<service android:name=".AssistScreenCaptureService" android:foregroundServiceType="mediaProjection" /> - 构建 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 格式)- 配置编码器:初始化 MediaCodec 视频编码器(类型为
video/avc),并将输入颜色格式设置为MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface。 - 创建输入表面:在调用
codec.configure(...)之后,通过codec.createInputSurface()从编码器获取一个专属的Surface。 - 直接投射:调用
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 (固定宽高) ]- 中间图层:我们不直接把 VirtualDisplay 绑定给编码器,而是将其渲染输出到一个 OpenGL 的 OES 外部纹理(SamplerExternalOES)。
- EGL Context 管理:创建一个独立的背景 EGL 渲染线程,并将 MediaCodec 的输入 Surface 包装为一个
EGLSurface。 - 矩阵投影变换:当手机发生旋转时,OpenGL 线程捕获旋转角度,重新计算正交投影矩阵(Orthographic Projection Matrix):
- 等比缩放:根据目标编码分辨率,对画面进行等比例缩放并居中,四周自动补充黑边,避免画面变形。
- 矩阵旋转:在顶点着色器(Vertex Shader)中应用旋转矩阵,在 GPU 中完成帧像素的物理转角。
- 无缝渲染:通过
eglSwapBuffers将变换后的帧画面写入编码器的 Input Surface。这保证了发送出的视频流物理宽高始终恒定,消除了画面变形与渲染撕裂。
4. 控制端解码:MediaCodec 三轨输出与热重置恢复
当 Android 设备充当协助者控制端时,接收到 peer 发送来的 H.264 裸流,需要使用 MediaCodec 解码器进行解析还原。我们为其设计了灵活的三轨输出通道:
- 直通 Surface 渲染模式(Zero-Copy rendering):这是最快、延迟最低的路径。解码器配置时直接绑定一个 UI 层的
SurfaceView或TextureView的 Surface。解码完成的帧数据直接由 GPU 硬件合成为显示画面,延迟几乎为零。 - ImageReader 监听模式:当需要截取当前协助画面生成缩略图、计算触控点击映射坐标、或进行视觉标记时,解码器会将输出重定向到
ImageReader(配置为COLOR_FormatFormatSurface色彩格式),从中提取HardwareBuffer转换为高保真 Bitmap。 - 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 的高性能实现。
