You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在ANativeWindow渲染解码后H264视频同时写入文件且无需新增编解码器?

解决H264解码渲染Surface同时写入文件的缓冲区问题

你遇到的核心问题其实是当MediaCodec配置了Surface作为输出目标时,输出缓冲区的ByteBuffer并不包含实际的像素数据——硬件解码器会直接将解码后的帧数据写入Surface关联的GPU内存,而用户空间拿到的ByteBuffer只是一个空的占位符(所以大小始终是8),自然无法写出有效文件。

不用新建解码器的话,有两种可靠的解决方案,根据你的API版本选择:

方案1:API 23+ 使用AMediaCodec_getOutputImage直接获取帧数据

从Android N API(23)开始,NDK的AMediaCodec支持直接获取输出图像的原生数据,不需要额外的OpenGL操作,步骤如下:

  1. 替换缓冲区获取方式
    当你成功dequeue到输出缓冲区索引index后,放弃使用AMediaCodec_getOutputBuffer,改用图像获取接口:

    AMediaImage* image = AMediaCodec_getOutputImage(d->codec, index);
    if (image == nullptr) {
        // 处理API版本不足等错误
        AMediaCodec_releaseOutputBuffer(d->codec, index, true);
        continue;
    }
    
  2. 读取YUV帧数据
    H264解码后通常输出YUV420_888格式,需要从图像的三个平面分别读取分量,注意使用每个平面的stride(步长)而非直接用图像宽度——硬件会做内存对齐,直接按宽高读取会拿到无效数据:

    AMediaImagePlane planes[3];
    AMediaImage_getPlanes(image, planes);
    
    // 读取Y分量
    uint8_t* y_data = planes[0].data;
    int y_stride = planes[0].stride;
    for (int i = 0; i < image->height; i++) {
        fwrite(y_data + i * y_stride, 1, image->width, output_file);
    }
    
    // 读取UV分量(高度为图像的1/2)
    uint8_t* u_data = planes[1].data;
    int u_stride = planes[1].stride;
    uint8_t* v_data = planes[2].data;
    int v_stride = planes[2].stride;
    for (int i = 0; i < image->height/2; i++) {
        fwrite(u_data + i * u_stride, 1, image->width/2, output_file);
        fwrite(v_data + i * v_stride, 1, image->width/2, output_file);
    }
    
  3. 释放资源并完成渲染
    读取完数据后必须先关闭图像对象,再释放缓冲区并允许渲染到Surface:

    AMediaImage_close(image);
    AMediaCodec_releaseOutputBuffer(d->codec, index, true);
    

方案2:兼容低版本(API <23) 使用SurfaceTexture + OpenGL离屏渲染

如果需要支持API 23以下的设备,就得通过SurfaceTexture将Surface的帧数据回读到CPU内存,步骤稍复杂:

  1. 创建SurfaceTexture与关联Surface
    先初始化EGL上下文(需要EGLDisplay、EGLContext和EGLSurface),再创建SurfaceTexture和对应的Surface,将这个Surface配置给你的MediaCodec作为输出目标:

    // 假设已完成EGL环境初始化
    SurfaceTexture* st = new SurfaceTexture(0);
    Surface* codec_output_surface = new Surface(st);
    // 将codec_output_surface传入AMediaCodec_configure
    
  2. 更新纹理并读取数据
    当dequeue到输出缓冲区后,先释放并允许渲染,再更新SurfaceTexture的纹理:

    AMediaCodec_releaseOutputBuffer(d->codec, index, true);
    st->updateTexImage();
    float transform_mtx[16];
    st->getTransformMatrix(transform_mtx);
    
  3. 用OpenGL实现双渲染

    • 创建一个离屏Framebuffer Object(FBO),绑定纹理作为颜色附件
    • 将SurfaceTexture的纹理绑定到OpenGL纹理单元,通过着色器先绘制到离屏FBO,用glReadPixels读取像素数据写入文件
    • 再将同一纹理绘制到实际的显示Surface,完成屏幕渲染

关键注意事项

  • 绝对不要尝试用AMediaCodec_getOutputBuffer获取数据——当Codec绑定Surface时,这个方法返回的ByteBuffer是空的,仅作为占位符存在
  • 方案1需确保设备API版本≥23,否则AMediaCodec_getOutputImage会返回nullptr
  • 写入文件时要注意YUV格式的一致性(比如YUV420_888和NV21的存储差异),如需转RGB还要做颜色空间转换
  • EGL和MediaCodec的操作要在同一线程(通常是渲染线程)执行,避免线程安全问题

内容的提问来源于stack exchange,提问作者E.Abdel

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.25 08:02:48