一只海星的主页

手把手教你使用FFmepg 将H265+AAC音视频流封装成MP4-三步搞定!

背景

FFmpeg作为应用最为广泛的音视频解决方案,适用范围及广,接口众多,如果是初学,很容易被众多Context晕得云里雾里,本文为小白教程,主要记录怎么讲原始音视频流封装成标准的MP4格式!

网上搜索的资料大部分都是读取其他格式文件编码成H264或者读取H264文件再封装成MP4!
举个栗子,FFmpeg的sample例子muxing.c就是读取一个gif文件,再编码成H264,然后封装成MP4,如果你只想将已有的原始音视频流封装成MP4的话,参考起来略显吃力!

所有了这个教程!封装MP4只需要三步!
第一、打开冰箱(avformat_write_header)
第二、把大象放进去(av_interleaved_write_frame)
第三、关上冰箱(av_write_trailer)

FFmpeg基础知识

此章资料全部摘自大神 雷霄骅,人生苦短,及时行乐!

FFmpeg常见的结构体

AVFormatContext:统领全局的基本结构体。主要用于处理封装格式(FLV/MKV/RMVB等)。

AVIOContext:输入输出对应的结构体,用于输入输出(读写文件,RTMP协议等)。

AVStream,AVCodecContext:视音频流对应的结构体,用于视音频编解码。

AVFrame:存储非压缩的数据(视频对应RGB/YUV像素数据,音频对应PCM采样数据)

AVPacket:存储压缩数据(视频对应H.264等码流数据,音频对应AAC/MP3等码流数据)

他们之间的关系如下图所示

环境

操作系统:window10
ffmpeg 版本:ffmpeg-4.2.2-win64
视频源文件格式:HEVC(H265)
音频源文件格式:AAC

FFmpeg 封装MP4完整步骤解析

  • ## 新建FFmpeg全局Context
    创建全局Context指针;指定源文件格式
    /* allocate the output media context */
    avformat_alloc_output_context2(&pFormatCtx, NULL, NULL, m_Filename);
    if (!pFormatCtx) {
        HERR("Failed to alloc mp4 avformat ctx !");
        return HS_FAILED;
    }

    / * Source stream format */
    pFormatCtx->oformat->video_codec = AV_CODEC_ID_HEVC;
    pFormatCtx->oformat->audio_codec = AV_CODEC_ID_AAC;

添加视频和音频文件流信息

重点解释一下time_base, time_base是时间戳单位,很重要!
ffmpeg定义:#define AV_TIME_BASE 1000000
FFmpeg里使用的基础时间单位是1/1000000,也就是1us(微妙),所有我们所有时间戳都转成微妙数量级!
这里音频和视频使用的不同的表现方式!
视频time_base设置成1微妙,视频帧率25fps即1000000/25=40*1000us,视频时间戳就是这个单位累加;
音频帧率非整数,所以将音频time_base设置成一个音频帧的时间!即10241000/DEFAULT_AUDIO_SAMPLE_RATE1000;所以音频帧就是从零开始累加!

    pVStream = avformat_new_stream(pFormatCtx, NULL);
    if (!pVStream) {
        HERR("Could not allocate pVStream");
        return -1;
    }

    pAStream = avformat_new_stream(pFormatCtx, NULL);
    if (!pVStream) {
        HERR("Could not allocate pAStream");
        return -1;
    }

    /* Video Stream info */
    pVStream->codecpar->codec_id = AV_CODEC_ID_HEVC;
    pVStream->codecpar->codec_type = AVMEDIA_TYPE_VIDEO;
    pVStream->codecpar->width = DEFAULT_VIDOE_WIDTH;
    pVStream->codecpar->height = DEFAULT_VIDOE_HIGHT;
    pVStream->time_base.num = 1;
    pVStream->time_base.den = 1000000;

    /* Audio Stream info  */
    pAStream->codecpar->codec_id = AV_CODEC_ID_AAC;
    pAStream->codecpar->codec_type = AVMEDIA_TYPE_AUDIO;
    pAStream->codecpar->channels = 2;
    pAStream->codecpar->sample_rate = DEFAULT_AUDIO_SAMPLE_RATE;
    pAStream->time_base.num = 1;
    pAStream->time_base.den = 1024*1000/DEFAULT_AUDIO_SAMPLE_RATE*1000;

新建Mp4文件

pFmtOutCtx = pFormatCtx->oformat;
    /* open the output file, if needed */
    if (!(pFmtOutCtx->flags & AVFMT_NOFILE)) {
        ret = avio_open(&pFormatCtx->pb, m_Filename, AVIO_FLAG_WRITE);
        if (ret < 0) {
            HERR("Could not open '%s'", m_Filename);
            return HS_FAILED;
        }
    }

写Mp4封装头信息

这里的实现就是遍历pFormatCtx里的streams的信息添加到MP4头基本信息里

/* Write the stream header, if any. */
    ret = avformat_write_header(pFormatCtx, NULL);
    if (ret < 0) {
        char buf[64] = { 0 };
        av_strerror(ret, buf, 64);
        HERR("Error occurred when opening output file: %d %s", ret, buf);
        return HS_FAILED;
    }

写入源文件

这里需要指定时间戳PTS具体原因已经在第二步详细说明!
FFmpeg使用AVPacket表示一帧数据,关键参数如下:
stream_index:流索引,就是第二步添加的Stream,一般是从0开始
pts/dts:视频时间戳和解码时间戳
duration:帧持续时间,这里指的是本帧数据,不是全部文件
flags:标记I帧
data:源文件数据指针
size:源文件数据长度

    if(Type == A_FRAME){
        m_Packet.stream_index = pAStream->index;
        m_Packet.pts = m_AudioIndex++ * 1000;
        m_Packet.dts = m_Packet.pts;
        m_Packet.duration = 1000;
    }else{
        m_Packet.stream_index = pVStream->index;
        m_Packet.pts = m_VideoIndex++*(1000/DEFAULT_VIDEO_FRAMERATE) * 1000;
        m_Packet.dts = m_Packet.pts;
        m_Packet.duration = 1000/DEFAULT_VIDEO_FRAMERATE * 1000;

        //I Frame
        if (Type == I_FRAME) {
            m_Packet.flags = AV_PKT_FLAG_KEY;
        }
    }

    m_Packet.data = (uint8_t*)Date;
    m_Packet.size = Size;

    return av_interleaved_write_frame(pFormatCtx, &m_Packet);

结束文件写入

    /* 关闭写MP4 */
    av_write_trailer(pFormatCtx);
    /* 是否资源 */
    avformat_free_context(pFormatCtx);
    av_free_packet(&m_Packet);

结果

参考

FFmpeg源代码简单分析:avformat_write_header()
FFmpeg源代码简单分析:常见结构体的初始化和销毁(AVFormatContext,AVFrame等)

2 thoughts on “手把手教你使用FFmepg 将H265+AAC音视频流封装成MP4-三步搞定!

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注