天天看點

音視訊探索(2):AAC編碼解析1.AAC編碼格式分析2. MP4封裝格式分析3.将H.264和AAC封裝成MP4檔案

1.AAC編碼格式分析

1.1 AAC簡介

 進階音頻編碼(

AdvancedAudio Coding,AAC

)一種基于MPEG-4的音頻編碼技術,它由杜比實驗室、AT&T等公司共同研發,目的是替換MP3編碼方式。作為一種高壓縮比的音頻壓縮算法,AAC的資料壓縮比約為18:1,壓縮後的音質可以同未壓縮的CD音質相媲美。是以,相對于MP3、WMA等音頻編碼标準來說,在相同品質下碼率更低,有效地節約了傳輸帶寬,被廣泛得應用于網際網路流媒體、IPTV等領域(低碼率,高音質)。主要有以下特點:

  • 比特率:AAC- 最高512kbps(雙聲道時)/MP3- 32~320kbps
  • 采樣率:AAC- 最高96kHz / MP3 - 最高48kHz
  • 聲道數:AAC– 最高48個全音域聲道/MP3 - 兩聲道
  • 采樣精度:AAC- 最高32bit / MP3 - 最高16bit

 AAC的不足之處是,它屬于有損壓縮的格式,相對于APE和FLAC等主流無損壓縮,音色“飽滿度”差距比較大。另外,除了流媒體網絡傳輸,其所能支援的裝置較少。

1.2 AAC編碼封裝格式

音頻資料在壓縮編碼之前,要先進行采樣與量化,以樣值的形式存在。音頻壓縮編碼的輸出碼流,以音頻幀的形式存在。每個音頻幀包含若幹個音頻采樣的壓縮資料,AAC的一個音頻幀包含960或1024個樣值,這些壓縮編碼後的音頻幀稱為原始資料塊(RawData Block),由于原始資料塊以幀的形式存在,即簡稱為原始幀。原始幀是可變的,如果對原始幀進行ADTS的封裝,得到的原始幀為ADTS幀;如果對原始幀進行ADIF封裝,得到的原始幀為ADIF幀。它們的差別如下:

  • ADIF:

    AudioData Interchange Format

    ,音頻資料交換格式。這種格式明确解碼必須在明确定義的音頻資料流的開始處進行,常用于磁盤檔案中;
  • ADTS:

    AudioData Transport Stream

    ,音頻資料傳輸流。這種格式的特點是它一個有同步字的比特流,且允許在音頻資料流的任意幀解碼,也就是說,它每一幀都有資訊頭。

     一個AAC原始資料庫長度是可變的,對原始幀加上ADTS頭進行ADTS封裝就形成了ADTS幀。AAC音頻的每一幀(ADTS幀)體由ADTS Header和AAC Audio Data(包含1~4個音頻原始幀)組成,其中,ADTS Header占7個位元組或9個位元組,由兩部分組成:固定頭資訊(adts_fixed_header)、可變頭資訊(adts_variable_header)。固定頭資訊中的資料每一幀都是相同的,主要定義了音頻的采樣率、聲道數、幀長度等關鍵資訊,這是解碼AAC所需關鍵資訊;可變頭資訊則在幀與幀之間可變。

【騰訊文檔】FFmpegWebRTCRTMPRTSPHLSRTP播放器-音視訊流媒體進階開發-資料領取

https://docs.qq.com/doc/DYU5ORlBOdkpCUkNx

音視訊探索(2):AAC編碼解析1.AAC編碼格式分析2. MP4封裝格式分析3.将H.264和AAC封裝成MP4檔案

https://docs.qq.com/doc/DYU5ORlBOdkpCUkNx

音視訊探索(2):AAC編碼解析1.AAC編碼格式分析2. MP4封裝格式分析3.将H.264和AAC封裝成MP4檔案

音視訊探索(2):AAC編碼解析1.AAC編碼格式分析2. MP4封裝格式分析3.将H.264和AAC封裝成MP4檔案

下面是多個ADTS幀組成的AAC資料流結構,示意圖如下:

音視訊探索(2):AAC編碼解析1.AAC編碼格式分析2. MP4封裝格式分析3.将H.264和AAC封裝成MP4檔案

a)  固定資訊頭

音視訊探索(2):AAC編碼解析1.AAC編碼格式分析2. MP4封裝格式分析3.将H.264和AAC封裝成MP4檔案

說明:

  • syncword:占12bits。同步頭,表示一個ADTS幀的開始,總是0xFFF。正是因為它的存在,才支援解碼任意幀;
  • ID:            占1bit。MPEG的版本,0為MPGE-4,1為MPGE-2;
  • Layer:      占2bits。總是”00”;
  • protection_absent:占1bit。=0時,ADTS Header長度占9位元組;=1時,ADTS Header占7位元組
  • profile:     占2bit。使用哪個級别的AAC,值00、01、10分别對應Mainprofile、LC、SSR;
  • sampling_frequency_index:占4bits。表示使用的采樣率下标,通過這個下标在Sampling Frequencies[ ]數組中查找得知采樣率的值,如0xb,對應的采樣率為8000Hz;
  • 音視訊探索(2):AAC編碼解析1.AAC編碼格式分析2. MP4封裝格式分析3.将H.264和AAC封裝成MP4檔案
  • channel_configuration:表示聲道數,如1-單聲道,2-立體聲

(b)可變資訊頭

音視訊探索(2):AAC編碼解析1.AAC編碼格式分析2. MP4封裝格式分析3.将H.264和AAC封裝成MP4檔案

說明:

  • frame_length:占13bits。表示一個ADTS幀的長度,即ADTS頭(7或9位元組)+sizeof(AAC Frame);
  • adts_buffer_fullness:占11bits。值0x7FF,說明是碼率可變的碼流
  • number_of_raw_data_blocks_In_frame:占2bits。表示ADTS幀中有(number_of_raw_data_blocks_In_frame+1)個AAC原始幀

(3)  将AAC打包成ADTS格式

 衆所周知,在使用MediaCodec将PCM壓縮編碼為AAC時,編碼器輸出的AAC是沒有ADTS頭的原始幀,如果我們直接儲存為AAC檔案或推流,VLC等工具是無法将AAC資料流解碼播放的。是以,我們需要對MediaCodec編碼PCM輸出的AAC原始幀添加ADTS資料頭,然後再進行檔案儲存或者推流。MediaCodec部分代碼如下:

private void encodeBytes(byte[] audioBuf, int readBytes) {
	ByteBuffer[] inputBuffers = mAudioEncoder.getInputBuffers();
	ByteBuffer[] outputBuffers = mAudioEncoder.getOutputBuffers();
	int inputBufferIndex = mAudioEncoder.dequeueInputBuffer(TIMES_OUT);
	if(inputBufferIndex >= 0){
		ByteBuffer inputBuffer  = null;
		if(!isLollipop()){
			inputBuffer = inputBuffers[inputBufferIndex];
		}else{
			inputBuffer = mAudioEncoder.getInputBuffer(inputBufferIndex);
		}
		if(audioBuf==null || readBytes<=0){
			mAudioEncoder.queueInputBuffer(inputBufferIndex,0,0,getPTSUs(),MediaCodec.BUFFER_FLAG_END_OF_STREAM);
		}else{
			inputBuffer.clear();
			inputBuffer.put(audioBuf);
			mAudioEncoder.queueInputBuffer(inputBufferIndex,0,readBytes,getPTSUs(),0);
		}
	}

	// 傳回一個輸出緩存區句柄,當為-1時表示目前沒有可用的輸出緩存區
	// mBufferInfo參數包含被編碼好的資料,timesOut參數為逾時等待的時間
	MediaCodec.BufferInfo  mBufferInfo = new MediaCodec.BufferInfo();
	int outputBufferIndex = -1;
	do{
		outputBufferIndex = mAudioEncoder.dequeueOutputBuffer(mBufferInfo,TIMES_OUT);
		if(outputBufferIndex == MediaCodec. INFO_TRY_AGAIN_LATER){
			Log.i(TAG,"獲得編碼器輸出緩存區逾時");
		}else if(outputBufferIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED){
		   
		}else if(outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED){
		  
		}else{
			if((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0){
				mBufferInfo.size = 0;
			}
			if((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0){
				break;
			}
			// 擷取一個隻讀的輸出緩存區inputBuffer ,它包含被編碼好的資料
			ByteBuffer mBuffer = ByteBuffer.allocate(10240);
			ByteBuffer outputBuffer = null;
			if(!isLollipop()){
				outputBuffer  = outputBuffers[outputBufferIndex];
			}else{
				outputBuffer  = mAudioEncoder.getOutputBuffer(outputBufferIndex);
			}
			if(mBufferInfo.size != 0){	
                Log.i(TAG,"AAC流添加ADTS頭,緩存到mBuffer");		
				mBuffer.clear();
                    // 拷貝outputBuffer編碼好的AAC原始幀到mBuffer,從第8個位元組存放
                    // mBuffer的前7個位元組留用(數組下标0~6)
				outputBuffer.get(mBuffer.array(), 7, mBufferInfo.size);
				outputBuffer.clear();
                    // 将buffer的position置7 + mBufferInfo.size
				mBuffer.position(7 + mBufferInfo.size);
                    // 添加ADTS頭,其中(mBufferInfo.size + 7)為ADTS幀長度
				addADTStoPacket(mBuffer.array(), mBufferInfo.size + 7);
                    // 将buffer的position置0
				mBuffer.flip();

				    // 推流AAC
				...
			}        
			mAudioEncoder.releaseOutputBuffer(outputBufferIndex,false);
		}
	}while (outputBufferIndex >= 0);
}


//----------------------------添加ADTS頭,7個位元組-------------------------------
    private void addADTStoPacket(byte[] packet, int packetLen) {
        int profile = 2;
        int chanCfg = 1;
        int sampleRate = mSamplingRateIndex ;
        packet[0] = (byte) 0xFF;    
        packet[1] = (byte) 0xF1;  
        packet[2] = (byte) (((profile - 1) << 6) + (sampleRate << 2) + (chanCfg>> 2));
        packet[3] = (byte) (((chanCfg & 3) << 6) + (packetLen >> 11));
        packet[4] = (byte) ((packetLen & 0x7FF) >> 3);
        packet[5] = (byte) (((packetLen & 7) << 5) + 0x1F);
        packet[6] = (byte) 0xFC;
    }

注釋:mSamplingRateIndex 為采樣率的下标
    public static final int[] AUDIO_SAMPLING_RATES = { 96000, // 0
            88200, // 1
            64000, // 2
            48000, // 3
            44100, // 4
            32000, // 5
            24000, // 6
            22050, // 7
            16000, // 8
            12000, // 9
            11025, // 10
            8000, // 11
            7350, // 12
            -1, // 13
            -1, // 14
            -1, // 15
    };
複制代碼
           

 或許,addADTStoPacket方法中對每個位元組的指派有點不了解,這裡我們參照FFmpeg中的源碼,對ADTS頭的指派作進一步解釋,以便于加深了解。在FFmpeg的libavformat/adtsenc.c源碼中,可以找到函數adts_write_frame_header(),它的源碼如下:

static int adts_write_frame_header(ADTSContext *ctx,
                                   uint8_t *buf, int size, int pce_size)
{
    PutBitContext pb;

    unsigned full_frame_size = (unsigned)ADTS_HEADER_SIZE + size + pce_size;
    if (full_frame_size > ADTS_MAX_FRAME_BYTES) {
        av_log(NULL, AV_LOG_ERROR, "ADTS frame size too large: %u (max %d)\n",
               full_frame_size, ADTS_MAX_FRAME_BYTES);
        return AVERROR_INVALIDDATA;
    }

    init_put_bits(&pb, buf, ADTS_HEADER_SIZE);

    /* adts_fixed_header */
    // 添加ADTS頭,put_bits函數第二個參數為字段所占bits,第三個參數為value
    // 注:put_bits函數定義在libavcodec/put_bits.h中
    put_bits(&pb, 12, 0xfff);   /* syncword */
    put_bits(&pb, 1, 0);        /* ID */
    put_bits(&pb, 2, 0);        /* layer */
    put_bits(&pb, 1, 1);        /* protection_absent */
    put_bits(&pb, 2, ctx->objecttype); /* profile_objecttype */
    put_bits(&pb, 4, ctx->sample_rate_index); // 采樣率
    put_bits(&pb, 1, 0);        /* private_bit */
    put_bits(&pb, 3, ctx->channel_conf); /* 通道,channel_configuration */
    put_bits(&pb, 1, 0);        /* original_copy */
    put_bits(&pb, 1, 0);        /* home */

    /* adts_variable_header */
    put_bits(&pb, 1, 0);        /* copyright_identification_bit */
    put_bits(&pb, 1, 0);        /* copyright_identification_start */
    put_bits(&pb, 13, full_frame_size); /* aac_frame_length,ADTS幀長度 */
    put_bits(&pb, 11, 0x7ff);   /* adts_buffer_fullness */
    put_bits(&pb, 2, 0);        /* number_of_raw_data_blocks_in_frame */

    flush_put_bits(&pb);

    return 0;
}
複制代碼
           

 從adts_write_frame_header()來看,除了profile、sampling_frequency_index、channel_configuration以及acc_frame_length值可能會因為編碼器的配置不一樣而不用,其他字段基本相同,甚至profile也可以直接設定預設值。既然如此,畫個大概

音視訊探索(2):AAC編碼解析1.AAC編碼格式分析2. MP4封裝格式分析3.将H.264和AAC封裝成MP4檔案

下面是使用UtraEdit軟體打開aac檔案,一個ADTS幀表現如下:

音視訊探索(2):AAC編碼解析1.AAC編碼格式分析2. MP4封裝格式分析3.将H.264和AAC封裝成MP4檔案

2. MP4封裝格式分析

 由于MP4格式較為複雜,本文隻對其做個簡單的介紹。MP4封裝格式是基于QuickTime容器格式定義,媒體描述與媒體資料分開,目前被廣泛應用于封裝h.263視訊和AAC音頻,是高清視訊/HDV的代表。MP4檔案中所有資料都封裝在box中(d對應QuickTime中的atom),即MP4檔案是由若幹個box組成,每個box有長度和類型,每個box中還可以包含另外的子box。box的基本結構如下:

音視訊探索(2):AAC編碼解析1.AAC編碼格式分析2. MP4封裝格式分析3.将H.264和AAC封裝成MP4檔案

其中,size指明了整個box所占用的大小,包括header部分。如果box很大(例如存放具體視訊資料的mdatbox),超過了uint32的最大數值,size就被設定為1,并用接下來的8位uint64來存放大小。通常,一個MP4檔案由若幹box組成,常見的mp4檔案結構:

音視訊探索(2):AAC編碼解析1.AAC編碼格式分析2. MP4封裝格式分析3.将H.264和AAC封裝成MP4檔案

一般來說,解析媒體檔案,最關心的部分是視訊檔案的寬高、時長、碼率、編碼格式、幀清單、關鍵幀清單,以及所對應的時戳和在檔案中的位置,這些資訊,在mp4中,是以特定的算法分開存放在stblbox下屬的幾個box中的,需要解析stbl下面所有的box,來還原媒體資訊。下表是對于以上幾個重要的box存放資訊的說明:

音視訊探索(2):AAC編碼解析1.AAC編碼格式分析2. MP4封裝格式分析3.将H.264和AAC封裝成MP4檔案

3.将H.264和AAC封裝成MP4檔案

 為了深入的了解H.264、AAC編碼格式,接下來我們将通過AndroidAPI中提供的MediaCodec和MediaMuxer實作對硬體采集的YUV格式視訊資料和PCM格式音頻資料進行壓縮編碼,并将編碼好的資料封裝成MP4格式檔案。MediaCodec被引入于Android4.1,它能夠通路系統底層的硬體編碼器,我們可以通過指定MIME類型指定相應編碼器,來實作對采集音、視訊進行編解碼;MediaMuxer是一個混合器,它能夠将H.264視訊流和ACC音頻流混合封裝成一個MP4檔案,也可以隻輸入H.264視訊流。

3.1 将YUV視訊資料編碼為H.264

 首先,建立并配置一個MediaCodec對象,通過指定該對象MIME類型為"video/avc",将其映射到底層的H.264硬體編碼器。然後再調用MediaCodec的configure方法來對編碼器進行配置,比如指定視訊編碼器的碼率、幀率、顔色格式等資訊。

MediaFormatmFormat = MediaFormat.createVideoFormat(“"video/avc"”, 640 ,480);
//碼率,600kbps-5000kbps,根據分辨率、網絡情況而定
mFormat.setInteger(MediaFormat.KEY_BIT_RATE,BIT_RATE);     
//幀率,15-30fps
mFormat.setInteger(MediaFormat.KEY_FRAME_RATE,FRAME_RATE);
//顔色格式,COLOR_FormatYUV420Planar或COLOR_FormatYUV420SemiPlanar
mFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT,mColorFormat);
//關鍵幀時間間隔,即編碼一次關鍵幀的時間間隔
mFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL,FRAME_INTERVAL);         
//配置、啟動編碼器
MediaCodec mVideoEncodec = MediaCodec.createByCodecName(mCodecInfo.getName());   
mVideoEncodec.configure(mFormat,null,null,MediaCodec.CONFIGURE_FLAG_ENCODE);    
mVideoEncodec.start();
複制代碼
           

 其次,每個編譯器都擁有多個輸入、輸出緩存區,當API<=20時,可以通過getInputBuffers()和getOutputBuffers()方法來獲得編碼器擁有的所有輸入/輸出緩存區。當通過MediaCodec的start()方法啟動編碼器後,APP此時并沒有擷取所需的輸入、輸出緩沖區,還需要調用MediaCodec的dequeueInputBuffer(long)和dequeueOutputBuffer(MediaCodec.BufferInfo,long)來對APP和緩存區進行綁定,然後傳回與輸入/輸出緩存區對應的句柄。APP一旦擁有了可用的輸入緩存區,就可以将有效的資料流填充到緩存區中,并通過MediaCodec的queueInputBuffer(int,int,int,long,int)方法将資料流(塊)送出到編碼器中自動進行編碼處理。

ByteBuffer[]inputBuffers = mVideoEncodec.getInputBuffers();
//傳回編碼器的一個輸入緩存區句柄,-1表示目前沒有可用的輸入緩存區
intinputBufferIndex = mVideoEncodec.dequeueInputBuffer(TIMES_OUT);
if(inputBufferIndex>= 0){
    // 綁定一個被空的、可寫的輸入緩存區inputBuffer到用戶端
    ByteBuffer inputBuffer  = null;
    if(!isLollipop()){
          inputBuffer =inputBuffers[inputBufferIndex];
     }else{
          inputBuffer = mVideoEncodec.getInputBuffer(inputBufferIndex);
     }
     // 向輸入緩存區寫入有效原始資料,并送出到編碼器中進行編碼處理
     inputBuffer.clear();
     inputBuffer.put(mFrameData);         
     mVideoEncodec.queueInputBuffer(inputBufferIndex,0,mFrameData.length,getPTSUs(),0);
}
複制代碼
           

 原始資料流被編碼處理後,編碼好的資料會儲存到被APP綁定的輸出緩存區,通過調用MediaCodec的dequeueOutputBuffer(MediaCodec.BufferInfo,long)實作。當輸出緩存區的資料被處理完畢後(比如推流、混合成MP4),就可以調用MediaCodec的releaseOutputBuffer(int,boolean)方法将輸出緩存區還給編碼器。

// 傳回一個輸出緩存區句柄,當為-1時表示目前沒有可用的輸出緩存區
// mBufferInfo參數包含被編碼好的資料,timesOut參數為逾時等待的時間
MediaCodec.BufferInfo  mBufferInfo = new MediaCodec.BufferInfo();
int outputBufferIndex = -1;
do{
        outputBufferIndex = mVideoEncodec.dequeueOutputBuffer(mBufferInfo,TIMES_OUT);
        if(outputBufferIndex == MediaCodec. INFO_TRY_AGAIN_LATER){
      Log.e(TAG,"獲得編碼器輸出緩存區逾時");
        }else if(outputBufferIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED){
        // 如果API小于21,APP需要重新綁定編碼器的輸入緩存區;
        // 如果API大于21,則無需處理INFO_OUTPUT_BUFFERS_CHANGED
        if(!isLollipop()){
            outputBuffers = mVideoEncodec.getOutputBuffers();
        }
    }else if(outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED){
        // 編碼器輸出緩存區格式改變,通常在存儲資料之前且隻會改變一次
        // 這裡設定混合器視訊軌道,如果音頻已經添加則啟動混合器(保證音視訊同步)
        MediaFormat newFormat = mVideoEncodec.getOutputFormat();
        MediaMuxerUtils mMuxerUtils = muxerRunnableRf.get();
        if(mMuxerUtils != null){
            mMuxerUtils.setMediaFormat(MediaMuxerUtils.TRACK_VIDEO,newFormat);
        }
        Log.i(TAG,"編碼器輸出緩存區格式改變,添加視訊軌道到混合器");
    }else{
        // 擷取一個隻讀的輸出緩存區inputBuffer ,它包含被編碼好的資料
        ByteBuffer outputBuffer = null;
        if(!isLollipop()){
            outputBuffer  = outputBuffers[outputBufferIndex];
        }else{
            outputBuffer  = mVideoEncodec.getOutputBuffer(outputBufferIndex);
        }             
                        // 如果API<=19,需要根據BufferInfo的offset偏移量調整ByteBuffer的位置
                        // 并且限定将要讀取緩存區資料的長度,否則輸出資料會混亂
                        if (isKITKAT()) {
                                outputBuffer.position(mBufferInfo.offset);
                                outputBuffer.limit(mBufferInfo.offset + mBufferInfo.size);
                        }
                        // 根據NALU類型判斷關鍵幀
                        MediaMuxerUtils mMuxerUtils = muxerRunnableRf.get();
                        int type = outputBuffer.get(4) & 0x1F;
                        if(type==7 || type==8){
                                Log.i(TAG, "------PPS、SPS幀(非圖像資料),忽略-------");
                                mBufferInfo.size = 0;
                        }else if (type == 5) {
                                Log.i(TAG, "------I幀(關鍵幀),添加到混合器-------");
                                if(mMuxerUtils != null && mMuxerUtils.isMuxerStarted()){
                                        mMuxerUtils.addMuxerData(new MediaMuxerUtils.MuxerData(
                                                        MediaMuxerUtils.TRACK_VIDEO, outputBuffer,
                                                        mBufferInfo));
                                        prevPresentationTimes = mBufferInfo.presentationTimeUs;
                                        isAddKeyFrame  = true;
                                }
                        }else{
                                 if(isAddKeyFrame){
                                         Log.d(TAG, "------非I幀(type=1),添加到混合器-------");
                                                if(mMuxerUtils != null&&mMuxerUtils.isMuxerStarted()){
                                                        mMuxerUtils.addMuxerData(new MediaMuxerUtils.MuxerData(
                                                                        MediaMuxerUtils.TRACK_VIDEO, outputBuffer,
                                                                        mBufferInfo));
                                                        prevPresentationTimes = mBufferInfo.presentationTimeUs;
                                                }
                                 }
                        }				
                        // 處理結束,釋放輸出緩存區資源
                        mVideoEncodec.releaseOutputBuffer(outputBufferIndex, false);
                }
        } while (outputBufferIndex >= 0);
複制代碼
           

 這裡有幾點需要說明下,因為如果處理不當,可能會導緻MediaMuxer合成MP4檔案失敗或者錄制的MP4檔案播放時開始會出現大量馬賽克或者音視訊不同步異常。

a) 如何保證音、視訊同步?

 要保證錄制的MP4檔案能夠音視訊同步,需要做到兩點:其一當我們獲得輸出緩存區的句柄outputBufferIndex等于MediaCodec.INFO_OUTPUT_FORMAT_CHANGED,需要将視訊軌道(MediaFormat)設定給MediaMuxer,同時隻有在确定音頻軌道也被添加後,才能啟動MediaMuxer混合器;其二就是傳入MediaCodec的queueInputBuffer中PTUs時間參數應該是單調遞增的,比如:

long prevPresentationTimes= mBufferInfo.presentationTimeUs;
private long getPTSUs(){
      longresult = System.nanoTime()/1000;
      if(result< prevPresentationTimes){
             result= (prevPresentationTimes  - result ) +result;
      }
      returnresult;
}
複制代碼
           

b)  錄制的MP4檔案播放的前幾幀有馬賽克?

 出現馬賽克的原因主要是因為MP4檔案的第一幀不是關鍵幀(I幀),根據H.264編碼原理可以知道,H.264碼流的一個序列是由SPS、PPS、關鍵幀、B幀、P幀…構造,而B幀、P幀是預測幀,承載的圖像資訊是不全的,是以一幀圖像沒有資訊的部分就會出現馬賽克。為此,我們可以使用丢幀政策來處理,即如果是普通幀就丢棄,隻有在關鍵幀已經插入的情況下才開始插普通幀。需要注意的是,由于MediaMuxer不需要SPS、PPS,如果當遇到SPS、PPS幀時忽略即可。

c)  stop muxer failed異常,導緻合成的MP4檔案無效?

 MediaMuxer報stop muxer failed異常通常是由于沒有正确插入同步幀(關鍵幀)所引起的

d)  錄制的視訊畫面出行花屏、疊影

  對YUV資料進行編碼出現花屏或疊影情況,是由于Camera采集YUV圖像幀顔色空間與MediaCodec編碼器所需輸入的顔色空間不同所導緻的,也就是說Camera支援的顔色空間為YV12(YUV4:2:0planar)和NV21(YUV4:2:0 semi-planar),而MediaCodec編碼器支援的顔色空間則為COLOR_FormatYUV420Planar(I420)、COLOR_FormatYUV420SemiPlanar (NV12)等格式,不同的Android裝置的編碼器所支援的顔色空間會有所不同,其中I420顔色格式(YYYYUU VV)與YV12(YYYY VV UU)資料結構相似,是一種标準的YUV420顔色格式。

3.2 将PCM音頻資料編碼為AAC

 由于使用MediaCodec編碼音視訊的原理是一緻的,這裡就不做過多介紹,相關音頻參數配置,可參照我這篇博文。另外,這裡是使用AudioRecord來獲得PCM音頻流,也比較簡單,詳情可參考這篇博文。代碼如下:

MediaCodec mMediaCodec =MediaCodec.createEncoderByType("audio/mp4a-latm");
MediaFormatformat = new MediaFormat();
format.setString(MediaFormat.KEY_MIME,"audio/mp4a-latm");      // 編碼器類型,AAC
format.setInteger(MediaFormat.KEY_BIT_RATE,16000);                 // 比特率,16kbps
format.setInteger(MediaFormat.KEY_CHANNEL_COUNT,1);        // 聲道數,1
format.setInteger(MediaFormat.KEY_SAMPLE_RATE,8000);          // 采樣率8000Hz
format.setInteger(MediaFormat.KEY_AAC_PROFILE,
           MediaCodecInfo.CodecProfileLevel.AACObjectLC);// 晶片支援的AAC級别,LC
format.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE,1600); // 最大緩存,1600
mMediaCodec.configure(format,null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
mMediaCodec.start();
 
/**
 * 使用AudioRecord錄制PCM格式音頻
*/
Process.setThreadPriority(Process.THREAD_PRIORITY_AUDIO);
intbufferSize = AudioRecord.getMinBufferSize(samplingRate,
AudioFormat.CHANNEL_IN_MONO,AudioFormat.ENCODING_PCM_16BIT);
if(bufferSize< 1600){
       bufferSize = 1600;
}
//配置錄音裝置的音頻源、采樣率、單聲道、采樣精度
intsamplingRate = 8000;
AudioRecord  mAudioRecord = newAudioRecord(MediaRecorder.AudioSource.MIC,
samplingRate,AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, bufferSize);
mAudioRecord.startRecording();
MediaCodec編碼核心與視訊相似,由于MediaMuxer不需要ADTS資訊頭,這裡就沒有在每桢資料添加資訊頭

byte[] audioBuf = new byte[AUDIO_BUFFER_SIZE];
int readBytes = mAudioRecord.read(audioBuf, 0,AUDIO_BUFFER_SIZE);
if (readBytes > 0) {
try {
	ByteBuffer[] inputBuffers = mAudioEncoder.getInputBuffers();
        ByteBuffer[] outputBuffers = mAudioEncoder.getOutputBuffers();
        //傳回編碼器的一個輸入緩存區句柄,-1表示目前沒有可用的輸入緩存區
        int inputBufferIndex = mAudioEncoder.dequeueInputBuffer(TIMES_OUT);
        if(inputBufferIndex >= 0){
            // 綁定一個被空的、可寫的輸入緩存區inputBuffer到用戶端
            ByteBuffer inputBuffer  = null;
            if(!isLollipop()){
                inputBuffer = inputBuffers[inputBufferIndex];
            }else{
                inputBuffer = mAudioEncoder.getInputBuffer(inputBufferIndex);
            }
            // 向輸入緩存區寫入有效原始資料,并送出到編碼器中進行編碼處理
            if(audioBuf==null || readBytes<=0){
            	mAudioEncoder.queueInputBuffer(inputBufferIndex,0,0,getPTSUs(),MediaCodec.BUFFER_FLAG_END_OF_STREAM);
            }else{
                inputBuffer.clear();
                inputBuffer.put(audioBuf);
                mAudioEncoder.queueInputBuffer(inputBufferIndex,0,readBytes,getPTSUs(),0);
            }
        }

        // 傳回一個輸出緩存區句柄,當為-1時表示目前沒有可用的輸出緩存區
        // mBufferInfo參數包含被編碼好的資料,timesOut參數為逾時等待的時間
        MediaCodec.BufferInfo  mBufferInfo = new MediaCodec.BufferInfo();
        int outputBufferIndex = -1;
        do{
        	outputBufferIndex = mAudioEncoder.dequeueOutputBuffer(mBufferInfo,TIMES_OUT);
        	if(outputBufferIndex == MediaCodec. INFO_TRY_AGAIN_LATER){
                Log.i(TAG,"獲得編碼器輸出緩存區逾時");
            }else if(outputBufferIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED){
                // 如果API小于21,APP需要重新綁定編碼器的輸入緩存區;
                // 如果API大于21,則無需處理INFO_OUTPUT_BUFFERS_CHANGED
                if(!isLollipop()){
                    outputBuffers = mAudioEncoder.getOutputBuffers();
                }
            }else if(outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED){
                // 編碼器輸出緩存區格式改變,通常在存儲資料之前且隻會改變一次
                // 這裡設定混合器視訊軌道,如果音頻已經添加則啟動混合器(保證音視訊同步)
                MediaFormat newFormat = mAudioEncoder.getOutputFormat();
                MediaMuxerUtils mMuxerUtils = muxerRunnableRf.get();
                if(mMuxerUtils != null){
                    mMuxerUtils.setMediaFormat(MediaMuxerUtils.TRACK_AUDIO,newFormat);
                }
                Log.i(TAG,"編碼器輸出緩存區格式改變,添加視訊軌道到混合器");
            }else{
                // 當flag屬性置為BUFFER_FLAG_CODEC_CONFIG後,說明輸出緩存區的資料已經被消費了
                if((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0){
                    Log.i(TAG,"編碼資料被消費,BufferInfo的size屬性置0");
                    mBufferInfo.size = 0;
                }
                // 資料流結束标志,結束本次循環
                if((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0){
                    Log.i(TAG,"資料流結束,退出循環");
                    break;
                }
                // 擷取一個隻讀的輸出緩存區inputBuffer ,它包含被編碼好的資料
                ByteBuffer outputBuffer = null;
                if(!isLollipop()){
                    outputBuffer  = outputBuffers[outputBufferIndex];
                }else{
                    outputBuffer  = mAudioEncoder.getOutputBuffer(outputBufferIndex);
                }
                if(mBufferInfo.size != 0){
                    // 擷取輸出緩存區失敗,抛出異常
                    if(outputBuffer == null){
                        throw new RuntimeException("encodecOutputBuffer"+outputBufferIndex+"was null");
                    }
                    // 如果API<=19,需要根據BufferInfo的offset偏移量調整ByteBuffer的位置
                    //并且限定将要讀取緩存區資料的長度,否則輸出資料會混亂
                    if(isKITKAT()){
                        outputBuffer.position(mBufferInfo.offset);
                        outputBuffer.limit(mBufferInfo.offset+mBufferInfo.size);
                    }
                    // 對輸出緩存區的H.264資料進行混合處理
                    MediaMuxerUtils mMuxerUtils = muxerRunnableRf.get();
                    mBufferInfo.presentationTimeUs = getPTSUs();
                    if(mMuxerUtils != null && mMuxerUtils.isMuxerStarted()){
                        Log.d(TAG,"------混合音頻資料-------");
                        mMuxerUtils.addMuxerData(new MediaMuxerUtils.MuxerData(MediaMuxerUtils.TRACK_AUDIO,outputBuffer,mBufferInfo));
                        prevPresentationTimes = mBufferInfo.presentationTimeUs;
                    }
                }
                // 處理結束,釋放輸出緩存區資源
                mAudioEncoder.releaseOutputBuffer(outputBufferIndex,false);
            }
        }while (outputBufferIndex >= 0);
			} catch (IllegalStateException e) {
				// 捕獲因中斷線程并停止混合dequeueOutputBuffer報的狀态異常
				e.printStackTrace();
			} catch (NullPointerException e) {
				// 捕獲因中斷線程并停止混合MediaCodec為NULL異常
				e.printStackTrace();
    }
}
複制代碼
           

 如果是使用AAC資料來進行推流,這就需要為每桢音頻資料添加ADTS頭。參考ADTS頭資訊格式,以及ffmpeg函數中的相關設定,在Java中ADTS資訊頭配置資訊可為:

private void addADTStoPacket(byte[] packet, int packetLen) {
     packet[0] = (byte) 0xFF;		
     packet[1] = (byte) 0xF1;
     packet[2] = (byte) (((2 - 1) << 6) + (mSamplingRateIndex << 2) + (1 >> 2));
     packet[3] = (byte) (((1 & 3) << 6) + (packetLen >> 11));
     packet[4] = (byte) ((packetLen & 0x7FF) >> 3);
     packet[5] = (byte) (((packetLen & 7) << 5) + 0x1F);
     packet[6] = (byte) 0xFC;
}
&emsp;其中,packetLen為原始幀資料長度,mSamplingRateIndex為自定義采樣率數組下标;

    public static final int[] AUDIO_SAMPLING_RATES = {96000, // 0
            88200, // 1
            64000, // 2
            48000, // 3
            44100, // 4
            32000, // 5
            24000, // 6
            22050, // 7
            16000, // 8
            12000, // 9
            11025, // 10
            8000, // 11
            7350, // 12
            -1, // 13
            -1, // 14
            -1, // 15
};
複制代碼
           

3.3 使用MediaMuxer混合H.264+AAC生成MP4檔案

 MediaMuxer的使用比較簡單,但需要嚴格按照以下三個步驟進行:

 第一步:配置混合器音、視訊軌道

public synchronized voidsetMediaFormat(int index, MediaFormat mediaFormat) {
      if (mediaMuxer == null) {
             return;
      }
      // 設定視訊軌道格式
      if (index == TRACK_VIDEO) {
             if (videoMediaFormat ==null) {
                    videoMediaFormat =mediaFormat;
                    videoTrackIndex =mediaMuxer.addTrack(mediaFormat);
                    isVideoAdd = true;
                    Log.i(TAG, "添加視訊軌道");
             }
      } else {
             if (audioMediaFormat ==null) {
                    audioMediaFormat =mediaFormat;
                    audioTrackIndex =mediaMuxer.addTrack(mediaFormat);
                    isAudioAdd = true;
                    Log.i(TAG, "添加音頻軌道");
             }
      }
      // 啟動混合器
      startMediaMuxer();
}
複制代碼
           

 第二步:音、視訊軌道均添加,啟動混合器

private void startMediaMuxer() {
          if (mediaMuxer == null) {
                 return;
          }
          if (isMuxerFormatAdded()) {
                 mediaMuxer.start();
                 isMediaMuxerStart = true;
                 Log.i(TAG, "啟動混合器,開始等待資料輸入.....");
          }
   }
複制代碼
           

 第三步:添加音視訊資料到混合器

public void addMuxerData(MuxerData data){
      int track = 0;
      if (data.trackIndex ==TRACK_VIDEO) {
             track = videoTrackIndex;
      } else {
             track = audioTrackIndex;
      }
      try {
             ByteBuffer outputBuffer =data.byteBuf;
             BufferInfo bufferInfo =data.bufferInfo;
             if(isMediaMuxerStart&& bufferInfo.size != 0){
                    outputBuffer.position(bufferInfo.offset);
                    outputBuffer.limit(bufferInfo.offset+ bufferInfo.size);
                    Log.i(TAG, "寫入混合資料+"+data.trackIndex+",大小-->"+ bufferInfo.size);
                    mediaMuxer.writeSampleData(track,outputBuffer,bufferInfo);
             }
      if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0){
               Log.i(TAG,"BUFFER_FLAG_END_OF_STREAM received");
      }
      } catch (Exception e) {
             Log.e("TAG","寫入混合資料失敗!" +e.toString());
//                   restartMediaMuxer();
      }
}
           

繼續閱讀