Android直播開發之旅(17):使用FFmpeg提取MP4中的H264和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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章