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
https://docs.qq.com/doc/DYU5ORlBOdkpCUkNx
下面是多個ADTS幀組成的AAC資料流結構,示意圖如下:
a) 固定資訊頭
說明:
- 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;
- channel_configuration:表示聲道數,如1-單聲道,2-立體聲
(b)可變資訊頭
說明:
- 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也可以直接設定預設值。既然如此,畫個大概
下面是使用UtraEdit軟體打開aac檔案,一個ADTS幀表現如下:
2. MP4封裝格式分析
由于MP4格式較為複雜,本文隻對其做個簡單的介紹。MP4封裝格式是基于QuickTime容器格式定義,媒體描述與媒體資料分開,目前被廣泛應用于封裝h.263視訊和AAC音頻,是高清視訊/HDV的代表。MP4檔案中所有資料都封裝在box中(d對應QuickTime中的atom),即MP4檔案是由若幹個box組成,每個box有長度和類型,每個box中還可以包含另外的子box。box的基本結構如下:
其中,size指明了整個box所占用的大小,包括header部分。如果box很大(例如存放具體視訊資料的mdatbox),超過了uint32的最大數值,size就被設定為1,并用接下來的8位uint64來存放大小。通常,一個MP4檔案由若幹box組成,常見的mp4檔案結構:
一般來說,解析媒體檔案,最關心的部分是視訊檔案的寬高、時長、碼率、編碼格式、幀清單、關鍵幀清單,以及所對應的時戳和在檔案中的位置,這些資訊,在mp4中,是以特定的算法分開存放在stblbox下屬的幾個box中的,需要解析stbl下面所有的box,來還原媒體資訊。下表是對于以上幾個重要的box存放資訊的說明:
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;
}
 其中,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();
}
}