音视频探索(2):AAC编码解析

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所需关键信息;可变头信息则在帧与帧之间可变。

 下面是多个ADTS帧组成的AAC数据流结构,示意图如下:

11.jpg a)  固定信息头

22.png

说明:

  • 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;

44.png

  • channel_configuration:表示声道数,如1-单声道,2-立体声

(b)可变信息头

33.png

说明:

  • 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也可以直接设置默认值。既然如此,画个大概

77.png

下面是使用UtraEdit软件打开aac文件,一个ADTS帧表现如下:

88.png

2. MP4封装格式分析

 由于MP4格式较为复杂,本文只对其做个简单的介绍。MP4封装格式是基于QuickTime容器格式定义,媒体描述与媒体数据分开,目前被广泛应用于封装h.263视频和AAC音频,是高清视频/HDV的代表。MP4文件中所有数据都封装在box中(d对应QuickTime中的atom),即MP4文件是由若干个box组成,每个box有长度和类型,每个box中还可以包含另外的子box。box的基本结构如下:

99.jfif  其中,size指明了整个box所占用的大小,包括header部分。如果box很大(例如存放具体视频数据的mdatbox),超过了uint32的最大数值,size就被设置为1,并用接下来的8位uint64来存放大小。通常,一个MP4文件由若干box组成,常见的mp4文件结构:

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

111.jfif

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();
      }
}
复制代码

Github项目地址:github.com/jiangdonggu…

猜你喜欢

转载自juejin.im/post/7032170229732442148