Android碎片化编解码器适配技巧 - 第4节

Surface 输入输出模式适配

MediaCodec 原生支持 Surface 作为输入和输出,这是碎片化场景下最高效的路径。Surface 既是 BufferProducer(输入)也是 BufferConsumer(输出),通过与 TextureView 或 GLSurfaceView 协作,可以绕过显式的 ByteBuffer 拷贝,实现零拷贝渲染。

核心概念:MediaCodec 的 Surface 模式利用硬件直接向 GPU 纹理推送 / 拉取帧,无需 CPU 搬运数据。掌握这一模式是处理 4K、高帧率、多路同步播放等场景的前提。
Surface 输入输出模式架构 App / 编码器 App / 解码器 MediaCodec (Surface 模式) Input Surface Output Surface SurfaceTexture (GL 纹理) GLSurfaceView (EGL 上下文) TextureView / 显示屏 GLSurfaceView / 显示屏 编码路径 解码路径

原理

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

  1. 创建 MediaCodec 编码器并配置为 Surface 输入模式。
  2. 调用 mediaCodec.createInputSurface() 获得 Surface。
  3. 在 EGL 线程上将 Surface 设为渲染目标(如 GLSurfaceView 的 EGLContext 共享后可跨线程)。
  4. 渲染每一帧后调用 eglSwapBuffers() 将数据送入编码器。
  5. 编码器异步回调中取出编码后的 BitStream。

4.2 解码器使用 OutputSurface

  1. 配置解码器时传入 Surface 对象(通常来自 SurfaceTexture 或 TextureView.SurfaceTextureListener)。
  2. 注册 SurfaceTexture 的 OnFrameAvailableListener 以获知有新帧。
  3. 在 GLThread 上更新纹理:surfaceTexture.updateTexImage() 并触发重绘。
  4. 或者使用 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 解码性能。