如何在ANativeWindow渲染解码后H264视频同时写入文件且无需新增编解码器?
你遇到的核心问题其实是当MediaCodec配置了Surface作为输出目标时,输出缓冲区的ByteBuffer并不包含实际的像素数据——硬件解码器会直接将解码后的帧数据写入Surface关联的GPU内存,而用户空间拿到的ByteBuffer只是一个空的占位符(所以大小始终是8),自然无法写出有效文件。
不用新建解码器的话,有两种可靠的解决方案,根据你的API版本选择:
方案1:API 23+ 使用AMediaCodec_getOutputImage直接获取帧数据
从Android N API(23)开始,NDK的AMediaCodec支持直接获取输出图像的原生数据,不需要额外的OpenGL操作,步骤如下:
替换缓冲区获取方式
当你成功dequeue到输出缓冲区索引index后,放弃使用AMediaCodec_getOutputBuffer,改用图像获取接口:AMediaImage* image = AMediaCodec_getOutputImage(d->codec, index); if (image == nullptr) { // 处理API版本不足等错误 AMediaCodec_releaseOutputBuffer(d->codec, index, true); continue; }读取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); }释放资源并完成渲染
读取完数据后必须先关闭图像对象,再释放缓冲区并允许渲染到Surface:AMediaImage_close(image); AMediaCodec_releaseOutputBuffer(d->codec, index, true);
方案2:兼容低版本(API <23) 使用SurfaceTexture + OpenGL离屏渲染
如果需要支持API 23以下的设备,就得通过SurfaceTexture将Surface的帧数据回读到CPU内存,步骤稍复杂:
创建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更新纹理并读取数据
当dequeue到输出缓冲区后,先释放并允许渲染,再更新SurfaceTexture的纹理:AMediaCodec_releaseOutputBuffer(d->codec, index, true); st->updateTexImage(); float transform_mtx[16]; st->getTransformMatrix(transform_mtx);用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

