最近在開發中遇到了一個問題,即無法提取到MP4中H264流的關鍵幀進行處理,且保存到本地的AAC音頻也無法正常播放。經過調試分析發現,這是由於解封裝MP4得到的H264和AAC是ES流,它們缺失解碼時必要的
起始碼
/SPS
/PPS
和adts頭
。雖說在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的基本結構由兩部分組成:BoxHeader
和BoxData
。BoxHeader由size
、type
和largesize
(由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,即ftyp
、moov
、mdat
,其中,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還包含了mvhd
、trak
等類型子Box,其中,mvhd Box的類型標誌爲0x6D 76 68 64
,該Box存儲的是文件的總體信息,如時長、創建的時間等;trak Box的類型標誌位0x74 72 61 6B
,該類型的Box存儲的是視頻索引或者音頻索引信息。moov box結構如下圖所示:
一般來說,解析媒體文件,最關心的部分是視頻文件的寬高、時長、碼率、編碼格式、幀列表、關鍵幀列表,以及所對應的時戳和在文件中的位置,這些信息,在mp4中,是以特定的算法分開存放在stbl box下屬的幾個box中的,需要解析stbl下面所有的box,來還原媒體信息。下表是對於以上幾個重要的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文件得到其封裝的音、視頻數據格式爲mp4a
和AVC1
得到證實。根據H.264編碼格式相關資料可知,H.264視頻編碼格式主要分爲兩種形式,即帶起始碼的H.264碼流
和不帶起始碼的H.264碼流
,其中,前者就是我們比較熟悉的H264
、X264
;後者就是指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_adtstoasc
、h264_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