FFmpeg从入门到入魔(3):提取MP4中的H.264和AAC

最近在开发中遇到了一个问题,即无法提取到MP4中H264流的关键帧进行处理,且保存到本地的AAC音频也无法正常播放。经过调试分析发现,这是由于解封装MP4得到的H264和AAC是ES流,它们缺失解码时必要的起始码/SPS/PPSadts头。虽说在Android直播开发之旅(3):AAC编码格式分析与MP4文件封装一文中对MP4有过简单的介绍,但为了搞清楚这个问题的来龙去脉,本文的开始还是有必要进一步阐述MP4格式的封装规则,然后再给出解决上述问题的方案和实战案例。

1. MP4格式解析

1.1 MP4简介

 MP4封装格式是基于QuickTime容器格式定义,媒体描述与媒体数据分开,目前被广泛应用于封装h.264视频和aac音频,是高清视频/HDV的代表。MP4文件中所有数据都封装在Box中(d对应QuickTime中的atom),即MP4文件是由若干个Box组成,每个Box有长度和类型,每个Box中还可以包含另外的子Box,因此,这种包含子Box的也可被称为container Box。Box的基本结构如下图所示:

Box基本结构

 从上图可知,Box的基本结构由两部分组成:BoxHeaderBoxDataBoxHeadersizetypelargesize(由size的值确定是否存在)组成,它们分别占4bytes、4bytes、8bytes,其中,size表示的是整个Box的大小(BoxHeader+BoxData),假如Box的大小超过了uint32的最大值,size会被置为1,这时将由largesize来表示Box的大小。type表示Box的类型,主要有ftyp、moov、mdat等。largesize表示当size=1时,用它代替size来存储Box的大小;BoxData存储的是真实数据(不一定是音视频数据),大小由真实数据决定。

1.2 MP4结构分析

 Box是构成MP4文件的基本单元,一个MP4文件由若干个Box组成,且每个Box中还可以包括另外的子Box。MP4格式结构中包括三个最顶层的Box,即ftypmoovmdat,其中,ftyp是整个MP4文件的第一个Box,也是唯一的一个,它主要用于确定当前文件的类型(比如MP4);moov保存了视频的基本信息,比如时间信息、trak信息以及媒体索引等;mdat保存视频和音频数据。需要注意的是,moov Box和mdat Box在文件中出现的顺序不是固定的,但是ftyp Box必须是第一个出现。MP4文件的结构如下图所示:

 当然,我们也可以使用MP4Info软件打开一个MP4文件来观察MP4的结构。从下图可以看到,该软件不仅能够看到MP4文件的Box结构,还列出了音频数据的格式(mp4a)、采样率、通道数量、比特率和视频的格式(AVC1)、宽高、比特率、帧率等信息。需要注意的是,由于录制设备的不同,生成的MP4文件可能会包含类型为free的Box,这类Box通常出现在moov于mdata之间,它的数据通常为全0,其作用相当于占位符,在实时拍摄视频时随着moov类型数据的增多会分配给moov使用,如果没有free预留的空间,则需要不停的向后移动mdat数据以腾出更更多的空间给moov。 在这里插入图片描述

  • ftype box

 ftyp就是一个由四个字符组成的码字,用来标识编码类型、兼容性或者媒体文件的用途,它存在于MP4文件和MOV文件中,当然也存在于3GP文件中。因此,在MP4文件中,ftyp类型Box被放在文件的最开始,用于标志文件类型为MP4,这类Box在文件中有且只有一个。我们利用WinHex工具打开一个MP4文件,就可以看到ftyp Box的具体细节,如下图所示: 在这里插入图片描述

 根据Box的基本结构可知,Box由BoxHeader和BoxData构成,其中,BoxHeader又由size、type以及largesize(可选)组成。由上图可以知道,ftyp Box头部信息为0x00 00 00 18 66 74 79 70,其中,0x00 00 00 18这四个字节表示ftyp Box整个Box的大小size=24字节;0x66 74 79 70表示该Box为ftyp类型,它们组成了ftyp的头部。0x6D 70 34 32(十六进制)表示major brand,这里为"mp42"且不同的文件该值可能不一样;0x00 00 00 00表示minor version。

  • moov box

 moov类型box主要用于存储媒体的时间信息、trak信息和媒体索引等信息。从MP4Info软件打开的文件可知,moov Box是紧接着ftyp Box的,因此,该Box头部为0x00 00 28 D1 6D 6F 6F 76,其中,0x00 00 28 D1表示整个moov Box的大小size=10449字节,0x6D 6F 6F 76表示当前Box为moov类型,而剩下的字节数据即为BoxData。另外,moov Box还包含了mvhdtrak等类型子Box,其中,mvhd Box的类型标志为0x6D 76 68 64,该Box存储的是文件的总体信息,如时长、创建的时间等;trak Box的类型标志位0x74 72 61 6B,该类型的Box存储的是视频索引或者音频索引信息。moov box结构如下图所示: 在这里插入图片描述

 一般来说,解析媒体文件,最关心的部分是视频文件的宽高、时长、码率、编码格式、帧列表、关键帧列表,以及所对应的时戳和在文件中的位置,这些信息,在mp4中,是以特定的算法分开存放在stbl box下属的几个box中的,需要解析stbl下面所有的box,来还原媒体信息。下表是对于以上几个重要的box存放信息的说明

stbl box

  • mdat box

 mdata类型Box存储所有媒体数据,其类型标志位0x 6D 64 61 74。mdata中的媒体数据没有同步字,没有分隔符,只能根据索引(位于moov中)进行访问。mdat的位置比较灵活,可以位于moov之前,也可以位于moov之后,但必须和stbl中的信息保持一致。mdat Box的BoxHeader如下图所示: 在这里插入图片描述

1.3 MP4中的H.264码流分析

 在对MP4文件结构的分析中,我们可以知道MP4文件中所有的多媒体数据都是存储在mdata Box中,且mdata中的媒体数据没有同步字,没有分隔符,只能根据索引(位于moov中)进行访问,也就意味着mdata Box存储的H264码流和aac码流可能没有使用起始码(0x00 00 00 01或0x00 00 01)或adts头进行分割,这一点可以通过mp4info软件解析MP4文件得到其封装的音、视频数据格式为mp4aAVC1得到证实。根据H.264编码格式相关资料可知,H.264视频编码格式主要分为两种形式,即带起始码的H.264码流不带起始码的H.264码流,其中,前者就是我们比较熟悉的H264X264;后者就是指AVC1。H.264编码格式的media subtypes: 在这里插入图片描述  **MP4容器格式存储H.264数据,没有开始代码。相反,每个NALU都以长度字段为前缀,以字节为单位给出NALU的长度。长度字段的大小可以不同,通常是1、2或4个字节。**另外,在标准H264中,SPS和PPS存在于NALU header中,而在MP4文件中,SPS和PPS存在于AVCDecoderConfigurationRecord结构中, 序列参数集SPS作用于一系列连续的编码图像,而图像参数集PPS作用于编码视频序列中一个或多个独立的图像。 如果解码器没能正确接收到这两个参数集,那么其他NALU 也是无法解码的。具体来说,MP4文件中H.264的SPS、PPS存储在avcC Box中(moov->trak->mdia->minf->stbl->stsd->avc1->avcC)。AVCDecoderConfigurationRecordj结构如下: 在这里插入图片描述  从上图我们可知:

  • 0x00 00 00 2E:表示avcC Box的长度size,即占46个字节;
  • 0x61 76 63 43:为avcC Box的类型type标志,它与0x00 00 00 2E组成avcC Box的HeaderData;
  • 0x00 17:表示sps的长度,即占23字节;
  • 0x67 64 ... 80 01:sps的内容;
  • 0x00 04:表示pps的长度,即占4字节;
  • 0x68 EF BC B0:pps的内容;

2. 使用FFmpeg拆解MP4

 假如我们需要提取MP4中的H.264流保存到本地文件,这个本地文件应该是无法被解码播放的,因为保存的H.264文件没有SPS、PPS以及每个NALU缺少起始码。幸运的是,FFmpeg为我们提供了一个名为 h264_mp4toannexb过滤器,该过滤器实现了对SPS、PPS的提取和对起始码的添加。对于MP4文件来说,在FFmpeg中一个AVPacket可能包含一个或者多个NALU,比如sps、pps和I帧可能存在同一个NALU中,并且每个NALU前面是没有起始码的,取而代之的是表述该NALU长度信息,占4个字节。AVPacket.data结构如下: 在这里插入图片描述 2.1 h264_mp4toannexb过滤器

 FFmpeg提供了多种用于处理某些格式的封装转换bit stream过滤器,比如aac_adtstoasch264_mp4toannexb等,可以通过在源码中运行**./configure --list-bsfs**查看。本小节主要讲解如何使用h264_mp4toannexb过滤器将H264码流的MP4封装格式转换为annexb格式,即AVC1->H264。 在这里插入图片描述 (1)初始化h264_mp4toannexb过滤器

 该过程主要包括创建指定名称的过滤器AVBitStreamFilter为过滤器创建上下文结构体AVBSFContext复制上下文参数以及初始化AVBSFContext等操作。具体代码如下:

/** (1) 创建h264_mp4toannexb 比特流过滤器结构体AVBitStreamFilter
 *  声明位于../libavcodec/avcodec.h
 *  typedef struct AVBitStreamFilter {
 *       // 过滤器名称
 *       const char *name;
 *       // 过滤器支持的编码器ID列表
 *       const enum AVCodecID *codec_ids;
 *       const AVClass *priv_class;
 *       ...
 *   }
 * */
const AVBitStreamFilter *avBitStreamFilter = av_bsf_get_by_name("h264_mp4toannexb");
if(! avBitStreamFilter) {
    RLOG_E("get AVBitStreamFilter failed");
    return -1;
}
/** (2)创建给定过滤器上下文结构体AVBSFContext,该结构体存储了过滤器的状态
 *  声明在../libavcodec/avcodec.h
 *  typedef struct AVBSFContext {
 *       ...
 *       const struct AVBitStreamFilter *filter;
 *       // 输入输出流参数信息
 *       // 调用av_bsf_alloc()后被创建分配
 *       // 调用av_bsf_init()后被初始化
 *       AVCodecParameters *par_in;
 *       AVCodecParameters *par_out;
 *       // 输入输出packet的时间基
 *       // 在调用av_bsf_init()之前被设置
 *       AVRational time_base_in;
 *       AVRational time_base_out;
 *  }
 * */
ret = av_bsf_alloc(avBitStreamFilter, &avBSFContext);
if(ret < 0) {
    RLOG_E_("av_bsf_alloc failed,err = %d", ret);
    return ret;
}
/** (3) 拷贝输入流相关参数到过滤器的AVBSFContext*/
int ret = avcodec_parameters_copy(gavBSFContext->par_in,
                                  inputFormatCtx->streams[id_video_stream] ->codecpar);
if(ret < 0) {
    RLOG_E_("copy codec params to filter failed,err = %d", ret);
    return ret;
}
/**(4) 使过滤器进入准备状态。在所有参数被设置完毕后调用*/
ret = av_bsf_init(avBSFContext);
if(ret < 0) {
    RLOG_E_("Prepare the filter failed,err = %d", ret);
    return ret;
}
复制代码

(2)处理AVPackt

 该过程主要是将解封装得到的H.264数据包AVPacket,通过av_bsf_send_packet函数提交给过滤器处理,待处理完毕后,再调用av_bsf_receive_packet读取处理后的数据。需要注意的是,输入一个packet可能会产生 多个输出packets,因此,我们可能需要反复调用av_bsf_receive_packet直到读取到所有的输出packets,即等待该函数返回0。具体代码如下:

/**(5) 将输入packet提交到过滤器处理*/
int ret = av_bsf_send_packet(avBSFContext, avPacket);
if(ret < 0) {
    av_packet_unref(avPacket);
    av_init_packet(avPacket);
    return ret;
}
/**(6) 循环读取过滤器,直到返回0标明读取完毕*/
for(;;) {
    int flags = av_bsf_receive_packet(avBSFContext, avPacket);
    if(flags == EAGAIN) {
        continue;
    } else {
        break;
    }
}
复制代码

(3) 释放过滤器所分配的所有资源

/**(7) 释放过滤器资源*/
if(avBSFContext) {
    av_bsf_free(&avBSFContext);
}
复制代码
2.2 实战演练:保存MP4中的H.264和AAC到本地文件

(1) 执行流程图 在这里插入图片描述 (2) 代码实现

  • ffmepeg_dexmuxer.cpp:FFmpeg功能函数
// ffmpeg调用功能函数
// Created by Jiangdg on 2019/9/25.
//

#include "ffmpeg_demuxer.h"

FFmpegDexmuer g_demuxer;

int createDemuxerFFmpeg(char * url) {
    if(! url) {
        RLOG_E("createRenderFFmpeg failed,url can not be null");
        return -1;
    }
    // 初始化ffmpeg引擎
    av_register_all();
    avcodec_register_all();
    av_log_set_level(AV_LOG_VERBOSE);
    g_demuxer.avPacket = av_packet_alloc();
    av_init_packet(g_demuxer.avPacket);
    g_demuxer.id_video_stream = -1;
    g_demuxer.id_audio_stream = -1;

    // 打开输入URL
    g_demuxer.inputFormatCtx = avformat_alloc_context();
    if(! g_demuxer.inputFormatCtx) {
        releaseDemuxerFFmpeg();
        RLOG_E("avformat_alloc_context failed.");
        return -1;
    }
    int ret = avformat_open_input(&g_demuxer.inputFormatCtx, url, NULL, NULL);
    if(ret < 0) {
        releaseDemuxerFFmpeg();
        RLOG_E_("avformat_open_input failed,err=%d", ret);
        return -1;
    }
    ret = avformat_find_stream_info(g_demuxer.inputFormatCtx, NULL);
    if(ret < 0) {
        releaseDemuxerFFmpeg();
        RLOG_E_("avformat_find_stream_info failed,err=%d", ret);
        return -1;
    }
    // 获取音视频stream id
    for(int i=0; i<g_demuxer.inputFormatCtx->nb_streams; i++) {
        AVStream *avStream = g_demuxer.inputFormatCtx->streams[i];
        if(! avStream) {
            continue;
        }
        AVMediaType type = avStream ->codecpar->codec_type;
        if(g_demuxer.id_video_stream == -1 || g_demuxer.id_audio_stream == -1) {
            if(type == AVMEDIA_TYPE_VIDEO) {
                g_demuxer.id_video_stream = i;
            }
            if(type == AVMEDIA_TYPE_AUDIO) {
                g_demuxer.id_audio_stream = i;
            }
        }

    }

    // 初始化h264_mp4toannexb过滤器
    // 该过滤器用于将H264的封装格式由mp4模式转换为annexb模式
    const AVBitStreamFilter *avBitStreamFilter = av_bsf_get_by_name("h264_mp4toannexb");
    if(! avBitStreamFilter) {
        releaseDemuxerFFmpeg();
        RLOG_E("get AVBitStreamFilter failed");
        return -1;
    }
    ret = av_bsf_alloc(avBitStreamFilter, &g_demuxer.avBSFContext);
    if(ret < 0) {
        releaseDemuxerFFmpeg();
        RLOG_E_("av_bsf_alloc failed,err = %d", ret);
        return ret;
    }
    ret = avcodec_parameters_copy(g_demuxer.avBSFContext->par_in,
          g_demuxer.inputFormatCtx->streams[g_demuxer.id_video_stream] ->codecpar);
    if(ret < 0) {
        releaseDemuxerFFmpeg();
        RLOG_E_("copy codec params to filter failed,err = %d", ret);
        return ret;
    }
    ret = av_bsf_init(g_demuxer.avBSFContext);
    if(ret < 0) {
        releaseDemuxerFFmpeg();
        RLOG_E_("Prepare the filter failed,err = %d", ret);
        return ret;
    }
    return ret;
}

int readDataFromAVPacket() {
    int ret = -1;
    // 成功,返回AVPacket数据大小
    if(g_demuxer.avPacket) {
        ret = av_read_frame(g_demuxer.inputFormatCtx, g_demuxer.avPacket);
        if(ret == 0) {
            return g_demuxer.avPacket->size;
        }
    }
    return ret;
}

int handlePacketData(uint8_t *out, int size) {
    if(!g_demuxer.avPacket || !out) {
        return -1;
    }
    // h264封装格式转换:mp4模式->annexb模式
    int stream_index = g_demuxer.avPacket->stream_index;
    if(stream_index == getVideoStreamIndex()) {
        int ret = av_bsf_send_packet(g_demuxer.avBSFContext, g_demuxer.avPacket);
        if(ret < 0) {
            av_packet_unref(g_demuxer.avPacket);
            av_init_packet(g_demuxer.avPacket);
            return ret;
        }

        for(;;) {
            int flags = av_bsf_receive_packet(g_demuxer.avBSFContext, g_demuxer.avPacket);
            if(flags == EAGAIN) {
                continue;
            } else {
                break;
            }
        }
        memcpy(out, g_demuxer.avPacket->data, size);
    } else if(stream_index == getAudioStreamIndex()){
        memcpy(out, g_demuxer.avPacket->data, size);
    }
    av_packet_unref(g_demuxer.avPacket);
    av_init_packet(g_demuxer.avPacket);
    // 返回AVPacket的数据类型
    return stream_index;
}

void releaseDemuxerFFmpeg() {
    if(g_demuxer.inputFormatCtx) {
        avformat_close_input(&g_demuxer.inputFormatCtx);
        avformat_free_context(g_demuxer.inputFormatCtx);
    }
    if(g_demuxer.avPacket) {
        av_packet_free(&g_demuxer.avPacket);
        g_demuxer.avPacket = NULL;
    }
    if(g_demuxer.avBSFContext) {
        av_bsf_free(&g_demuxer.avBSFContext);
    }
    RLOG_I("release FFmpeg engine over!");
}

int getVideoStreamIndex() {
    return g_demuxer.id_video_stream;
}

int getAudioStreamIndex() {
    return g_demuxer.id_audio_stream;
}

int getAudioSampleRateIndex() {
    int rates[] = {96000, 88200, 64000,48000, 44100,
                   32000, 24000, 22050, 16000, 12000,
                   11025, 8000, 7350, -1, -1, -1};
    int sampe_rate = g_demuxer.inputFormatCtx->streams[getAudioStreamIndex()]
        ->codecpar->sample_rate;
    for (int index = 0; index < 16; index++) {
        if(sampe_rate == rates[index]) {
            return index;
        }
    }
    return -1;
}

int getAudioProfile() {
    return g_demuxer.inputFormatCtx->streams[getAudioStreamIndex()]->codecpar->profile;
}

int getAudioChannels() {
    return g_demuxer.inputFormatCtx->streams[getAudioStreamIndex()]->codecpar->channels;
}
复制代码
  • native_dexmuxer.cpp:Java层调用接口、处理MP4子线程
// 解码、渲染子线程(部分代码)
// ffmpeg错误码:https://my.oschina.net/u/3700450/blog/1545657
// Created by Jiangdg on 2019/9/23.
//

void *save_thread(void * args) {
    // 主线程与子线程分离
    // 子线程结束后,资源自动回收
    pthread_detach(pthread_self());
    DemuxerThreadParams * params = (DemuxerThreadParams *)args;
    if(! params) {
        RLOG_E("pass parms to demuxer thread failed");
        return NULL;
    }
    // 将当前线程绑定到JavaVM,从JVM中获取JNIEnv*
    JNIEnv *env = NULL;
    jmethodID id_cb = NULL;
    if(g_jvm && global_cb_obj) {
        if(g_jvm->GetEnv(reinterpret_cast<void **>(env), JNI_VERSION_1_4) > 0) {
            RLOG_E("get JNIEnv from JVM failed.");
            return NULL;
        }
        if(JNI_OK != g_jvm->AttachCurrentThread(&env, NULL)) {
            RLOG_E("attach thread failed");
            return NULL;
        }
        jclass cb_cls = env->GetObjectClass(global_cb_obj);
        id_cb = env->GetMethodID(cb_cls, "onCallback", "(I)V");
    }

    // 打开输入流
    RLOG_I_("#### input url = %s", params->url);
    int ret = createDemuxerFFmpeg(params->url);
    if(ret < 0) {
        if(params) {
            free(params->url);
            free(params->h264path);
            free(params);
        }
        if(id_cb && g_jvm) {
            env->CallVoidMethod(global_cb_obj, id_cb, -1);
            env->DeleteGlobalRef(global_cb_obj);
            g_jvm->DetachCurrentThread();
        }
        return NULL;
    }
    // 打开文件
    RLOG_I_("#### h264 save path = %s", params->h264path);
    RLOG_I_("#### aac save path = %s", params->aacpath);
    FILE * h264file = fopen(params->h264path, "wb+");
    FILE * aacfile = fopen(params->aacpath, "wb+");
    if(h264file == NULL || aacfile == NULL) {
        RLOG_E("open save file failed");
        if(params) {
            free(params->url);
            free(params->h264path);
            free(params->aacpath);
            free(params);
        }
        releaseDemuxerFFmpeg();
        if(id_cb && g_jvm) {
            env->CallVoidMethod(global_cb_obj, id_cb, -2);
            env->DeleteGlobalRef(global_cb_obj);
            g_jvm->DetachCurrentThread();
        }
        return NULL;
    }
    int size = -1;
    int audio_profile = getAudioProfile();
    int rate_index = getAudioSampleRateIndex();
    int audio_channels = getAudioChannels();
    if(id_cb) {
        env->CallVoidMethod(global_cb_obj, id_cb, 0);
    }
    bool is_reading = false;
    while ((size = readDataFromAVPacket()) > 0) {
        if(g_exit) {
            break;
        }
        if(! is_reading) {
            is_reading = true;
            if(id_cb) {
                env->CallVoidMethod(global_cb_obj, id_cb, 1);
            }
        }
        uint8_t *out_buffer = (uint8_t *)malloc(size * sizeof(uint8_t));
        memset(out_buffer, 0, size * sizeof(uint8_t));
        int stream_index = handlePacketData(out_buffer, size);
        if(stream_index < 0) {
            continue;
        }
        if(stream_index == getVideoStreamIndex()) {
            fwrite(out_buffer, size,1, h264file);
            RLOG_I_("--->write a video data,size=%d", size);
        } else if(stream_index == getAudioStreamIndex()) {
            // 添加adts头部
            int adtslen = 7;
            uint8_t *ret = (uint8_t *)malloc(size * sizeof(uint8_t) + adtslen * sizeof(char));
            memset(ret, 0, size * sizeof(uint8_t) + adtslen * sizeof(char));
            char * adts = (char *)malloc(adtslen * sizeof(char));
            adts[0] = 0xFF;
            adts[1] = 0xF1;
            adts[2] = (((audio_profile - 1) << 6) + (rate_index << 2) + (audio_channels >> 2));
            adts[3] = (((audio_channels & 3) << 6) + (size >> 11));
            adts[4] = ((size & 0x7FF) >> 3);
            adts[5] = (((size & 7) << 5) + 0x1F);
            adts[6] = 0xFC;

            memcpy(ret, adts, adtslen);
            memcpy(ret+adtslen, out_buffer, size);
            fwrite(ret, size+adtslen, 1, aacfile);
            free(adts);
            free(ret);
            RLOG_I_("--->write a AUDIO data, header=%d, size=%d", adtslen, size);
        }
        free(out_buffer);
    }
    // 释放资源
    if(h264file) {
        fclose(h264file);
    }
    if(aacfile) {
        fclose(aacfile);
    }
    if(params) {
        free(params->url);
        free(params->h264path);
        free(params->aacpath);
        free(params);
    }
    releaseDemuxerFFmpeg();
    if(id_cb && g_jvm) {
        env->CallVoidMethod(global_cb_obj, id_cb, 2);
        env->DeleteGlobalRef(global_cb_obj);
        g_jvm->DetachCurrentThread();
    }
    RLOG_I("##### stop save success.");
    return NULL;
}
复制代码

注:这里只贴了核心代码,具体细节请看Github:DemoDemuxerMP4

猜你喜欢

转载自juejin.im/post/7031847485635035149