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

如何用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.20 00:33:10