如何用FFmpegFrameRecorder和AVPackets保存MediaCodec编码字节为AVI/MKV
用FFmpegFrameRecorder将MediaCodec编码字节保存为AVI/MKV
核心思路
你已经拿到MediaCodec输出的H.264编码字节,要封装成AVI/MKV,关键是把这些字节转换成FFmpeg的AVPacket,再通过FFmpegFrameRecorder写入容器。以下是具体实现步骤:
1. 准备工作与Recorder初始化
首先确保引入JavaCV依赖(FFmpegFrameRecorder属于JavaCV),然后初始化Recorder并配置核心参数,尤其要传入H.264必需的SPS/PPS数据:
// 提前定义变量 private var recorder: FFmpegFrameRecorder? = null private var spsPps: ByteArray? = null private val width = 1920 // 你的编码宽度 private val height = 1080 // 你的编码高度 private val frameRate = 30 // 视频帧率 private val outputPath = "/sdcard/output.mkv" // 输出路径,换成".avi"即可生成AVI格式 // 初始化Recorder的方法 fun initRecorder() { recorder = FFmpegFrameRecorder(outputPath, width, height) recorder?.apply { setFormat("mkv") // 输出容器格式,可选"avi" setVideoCodec(avcodec.AV_CODEC_ID_H264) // 匹配输入的H.264编码流 setFrameRate(frameRate.toDouble()) setVideoBitrate(2000000) // 视频码率,按需调整 // 传入SPS/PPS:必须在start()前设置,否则容器无法识别H.264流 spsPps?.let { spsPpsData -> val codecCtx = videoCodecContext codecCtx.extradata = avutil.av_malloc(spsPpsData.size + FFmpeg.EXTRA_DATA_PADDING_SIZE) avutil.av_memcpy(codecCtx.extradata, spsPpsData, spsPpsData.size) codecCtx.extradata_size = spsPpsData.size } try { start() } catch (e: Exception) { e.printStackTrace() } } }
2. 提取SPS/PPS数据
MediaCodec会在编码开始时输出包含SPS/PPS的配置帧,需要先保存这些数据:
myH264EncodeConsumer.setOnH264EncodeResultListener(object : MyH264EncodeConsumer.OnH264EncodeResultListener { override fun onEncodeResult( data: ByteArray?, offset: Int, length: Int, timestamp: Long, bufferInfo: BufferInfo ) { if (data == null || length <= 0) return // 提取SPS/PPS配置帧 if (bufferInfo.flags and MediaCodec.BUFFER_FLAG_CODEC_CONFIG != 0) { spsPps = data.copyOfRange(offset, offset + length) // 提取完配置帧后初始化Recorder(如果未初始化) if (recorder == null) { initRecorder() } return // 配置帧无需写入容器 } // 普通编码帧处理逻辑写在这里 } })
3. 转换编码字节为AVPacket并写入
在普通编码帧的处理逻辑中,将MediaCodec输出的字节封装成AVPacket,设置时间戳后写入Recorder:
// 接上面的回调逻辑,处理普通编码帧 // 创建AVPacket val packet = avcodec.av_packet_alloc() // 分配内存并填充编码数据 val buffer = avutil.av_malloc(length) avutil.av_memcpy(buffer, data, length) packet.data(buffer) packet.size(length) // 转换时间戳:MediaCodec的timestamp是微秒,转成FFmpeg的time_base格式 val codecCtx = recorder?.videoCodecContext ?: return val timeBase = codecCtx.time_base // 微秒单位转FFmpeg时间单位 val pts = avutil.av_rescale_q(timestamp * 1000, avutil.av_make_q(1, 1000000), timeBase) packet.pts(pts) packet.dts(pts) // H.264帧的dts通常与pts一致,按需调整 packet.duration(avutil.av_rescale_q(bufferInfo.duration * 1000, avutil.av_make_q(1, 1000000), timeBase)) packet.stream_index(0) // 视频流索引,默认0 // 写入Recorder try { recorder?.recordPacket(packet) } catch (e: Exception) { e.printStackTrace() } finally { // 必须手动释放资源,避免内存泄漏 avcodec.av_packet_free(packet) avutil.av_free(buffer) }
4. 释放资源
在结束录制时,记得停止并释放Recorder:
fun stopRecording() { recorder?.apply { try { stop() release() } catch (e: Exception) { e.printStackTrace() } } recorder = null }
注意事项
- SPS/PPS必须传入:H.264编码的视频流依赖这两个参数,容器才能正确解析视频,否则播放时会花屏或无法播放。
- 时间戳转换要准确:FFmpeg的时间戳基于
time_base,必须用av_rescale_q做单位转换,否则视频会出现播放速度异常。 - 资源手动释放:FFmpeg的原生对象(AVPacket、AVBuffer)不会自动GC,必须调用对应的free方法。
内容的提问来源于stack exchange,提问作者190re5ha
相关产品推荐
相关产品推荐

