Android碎片化编解码器适配技巧 - 第4节
Surface 输入输出模式适配
MediaCodec 原生支持 Surface 作为输入和输出,这是碎片化场景下最高效的路径。Surface 既是 BufferProducer(输入)也是 BufferConsumer(输出),通过与 TextureView 或 GLSurfaceView 协作,可以绕过显式的 ByteBuffer 拷贝,实现零拷贝渲染。
核心概念:MediaCodec 的 Surface 模式利用硬件直接向 GPU 纹理推送 / 拉取帧,无需 CPU 搬运数据。掌握这一模式是处理 4K、高帧率、多路同步播放等场景的前提。
原理
MediaCodec 的 createInputSurface() 和 setSurface() 将输入/输出直接绑定到图形缓冲区队列。输入 Surface 允许任意 EGL 上下文向编码器推送帧;输出 Surface 通过 SurfaceTexture 将解码帧映射为 GL 纹理,再通过 TextureView 或 GLSurfaceView 渲染。
| 角色 | Surface 类型 | 生产者 / 消费者 |
|---|---|---|
| 编码器输入 | InputSurface(来自 createInputSurface()) | App / 相机 → 编码器 |
| 编码器输出 | 用户 ByteBuffer 或 Surface(需要 setSurface()) | 编码器 → 文件 / 网络 |
| 解码器输入 | 用户 ByteBuffer(来自文件/网络) | 文件 → 解码器 |
| 解码器输出 | Surface(通过 configure() 传入) | 解码器 → SurfaceTexture → TextureView / GLSurfaceView |
实现流程
4.1 编码器使用 InputSurface
- 创建 MediaCodec 编码器并配置为 Surface 输入模式。
- 调用
mediaCodec.createInputSurface()获得 Surface。 - 在 EGL 线程上将 Surface 设为渲染目标(如 GLSurfaceView 的 EGLContext 共享后可跨线程)。
- 渲染每一帧后调用
eglSwapBuffers()将数据送入编码器。 - 编码器异步回调中取出编码后的 BitStream。
4.2 解码器使用 OutputSurface
- 配置解码器时传入 Surface 对象(通常来自
SurfaceTexture或TextureView.SurfaceTextureListener)。 - 注册 SurfaceTexture 的
OnFrameAvailableListener以获知有新帧。 - 在 GLThread 上更新纹理:
surfaceTexture.updateTexImage()并触发重绘。 - 或者使用 GLSurfaceView 的 Renderer,在
onDrawFrame()中直接消费 SurfaceTexture 纹理。
与 TextureView / GLSurfaceView 协作
推荐实践:解码输出 Surface 绑定到 TextureView 的 SurfaceTexture,可以减少 SurfaceView 带来的 Z-order 问题。GLSurfaceView 适合需要自定义 EGL 配置或混合多路视频的场景。
TextureView 模式
// 获取 TextureView 的 SurfaceTexture
TextureView textureView = findViewById(R.id.texture_view);
SurfaceTexture surfaceTexture = textureView.getSurfaceTexture();
// 传入解码器配置
MediaCodec decoder = MediaCodec.createDecoderByType(mimeType);
MediaFormat format = MediaFormat.createVideoFormat(mimeType, width, height);
decoder.configure(format, new Surface(surfaceTexture), null, 0);
decoder.start();
// 监听新帧
surfaceTexture.setOnFrameAvailableListener(surfaceTexture -> {
// 在主线程或渲染线程请求重绘
textureView.postInvalidate();
});
GLSurfaceView 模式
class VideoRenderer implements GLSurfaceView.Renderer {
SurfaceTexture surfaceTexture;
MediaCodec decoder;
int textureId;
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
// 创建纹理并绑定到 SurfaceTexture
int[] textures = new int[1];
GLES20.glGenTextures(1, textures, 0);
textureId = textures[0];
surfaceTexture = new SurfaceTexture(textureId);
// 配置解码器使用新 Surface
Surface outputSurface = new Surface(surfaceTexture);
decoder.configure(format, outputSurface, null, 0);
decoder.start();
}
@Override
public void onDrawFrame(GL10 gl) {
// 每帧更新纹理并绘制
surfaceTexture.updateTexImage();
// 绘制到屏幕 ...
}
}
调试要点
- Surface 有效性:调用
isValid()检查 Surface 是否被销毁;在 onPause 中释放,onResume 中重建。 - EGL 上下文共享:若跨线程传递 Surface,需确保 EGLContext 共享(
EGL14.eglCreateContext()传入 sharedContext)。 - 帧率监测:在
OnFrameAvailableListener中记录时间戳,排查 Surface 队列阻塞或丢帧。 - 日志过滤:查看
MediaCodec内部日志(通过MediaCodec.INFO_OUTPUT_FORMAT_CHANGED和drain耗时)。
常见错误:Surface 被提前销毁而 MediaCodec 仍在使用 → 导致
Surface has been abandoned 崩溃。务必保证 Surface 的生命周期长于 MediaCodec 的编解码过程,或在 dequeue 暂停时先暂停 Surface 使用。
性能与稳定性优化
| 问题 | 优化方案 |
|---|---|
| Surface 重建频繁 | 复用 Surface 对象,使用 SurfaceHolder.Callback 或 TextureView.SurfaceTextureListener 回调重建而不是每次都新建。 |
| EGL 线程切换开销 | 为 InputSurface 创建专用 EGL 线程,与 GLSurfaceView 共享 EGLContext,避免帧拷贝。 |
| 解码帧延迟高 | 增大解码器 Buffer 个数(mediacodec.setVideoScalingMode()),并使用 OutputSurface.release() 及时归还。 |
| 多路同步 | 使用 ImageReader 作为中间 Surface 可获取 CPU 访问的帧,用于同步不同解码器的时间戳。 |
部署注意事项
- 部分低端设备不支持 Surface 作为编码器输入(API 21+ 基本支持,但需检查
MediaCodecInfo.CodecCapabilities.FEATURE_SurfaceInput)。 - 解码器输出 Surface 的格式可能为
OMX.google.*软件解码器,无法直接绑定到 GPU 纹理,此时应降级为 ByteBuffer 模式。 - GLSurfaceView 在 Android 10+ 中推荐使用
GLSurfaceView.onResume()/onPause()正确管理渲染循环,避免 Surface 丢失。 - 使用
SurfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS)在某些旧设备上可提升 Surface 解码性能。