ffmpeg开发之旅(3):AAC编码格式分析与MP4文件封装(MediaCodec+MediaMuxer)

(原文链接:http://blog.csdn.net/andrexpert/article/details/72523408)

1.  AAC编码格式分析

(1)  AAC简介

高级音频编码(AdvancedAudio Coding,AAC)一种基于MPEG-4的音频编码技术,它由杜比实验室、AT&T等公司共同研发,目的是替换MP3编码方式。作为一种高压缩比的音频压缩算法,AAC的数据压缩比约为18:1,压缩后的音质可以同未压缩的CD音质相媲美。因此,相对于MP3、WMA等音频编码标准来说,在相同质量下码率更低,有效地节约了传输带宽,被广泛得应用于互联网流媒体、IPTV等领域(低码率,高音质)。主要有以下特点:

a) 比特率:AAC- 最高512kbps(双声道时)/MP3- 32~320kbps

b)  采样率:AAC- 最高96kHz / MP3 - 最高48kHz

c) 声道数:AAC– 最高48个全音域声道/MP3 - 两声道

d) 采样精度:AAC- 最高32bit / MP3 - 最高16bit

AAC的不足之处是,它属于有损压缩的格式,相对于APE和FLAC等主流无损压缩,音色“饱满度”差距比较大。另外,除了流媒体网络传输,其所能支持的设备较少。

(2)  AAC编码封装格式

音频数据在压缩编码之前,要先进行采样与量化,以样值的形式存在。音频压缩编码的输出码流,以音频帧的形式存在。每个音频帧包含若干个音频采样的压缩数据,AAC的一个音频帧包含960或1024个样值,这些压缩编码后的音频帧称为原始数据块(RawData Block),由于原始数据块以帧的形式存在,即简称为原始帧。原始帧是可变的,如果对原始帧进行ADTS的封装,得到的原始帧为ADTS帧;如果对原始帧进行ADIF封装,得到的原始帧为ADIF帧。它们的区别如下:

a)  ADIF:AudioData Interchange Format,音频数据交换格式。这种格式明确解码必须在明确定义的音频数据流的开始处进行,常用于磁盘文件中;

b)  ADTS:AudioData Transport Stream,音频数据传输流。这种格式的特点是它一个有同步字的比特流,且允许在音频数据流的任意帧解码,也就是说,它每一帧都有信息头。

ADTS封装格式的码流以帧为单位,一个ADTS帧由帧头、帧净荷组成。其中,帧头定义了音频采样率、音频声道数、帧长度等关键信息,它由两部分组成,共占7个字节:固定头信息adts_fixed_header、可变头信息adts_variable_header。固定头信息中的数据每一帧都相同,而可变头信息则在帧与帧之间可变;帧净荷主要由1~4个原始帧组成,它包含的数据用于解析与解码。

a)  固定信息头

说明:

*syncword:同步头,表示一个ADTS帧的开始,固定值11111111 1111(0xFFF);

*ID:表示MPEG的版本,0为MPGE-4,0为MPGE-2;

*Layer:默认”00”;

*profile:表示使用哪个级别的AAC,值00、01、10分别对应Mainprofile、LC、SSR;

*sampling_frequency_index:表示使用的采样率下标,通过这个下标在Sampling Frequencies[ ]数组中查找得知采样率的值,如1011,对应的采样率为8000Hz;

*channel_configuration:表示声道数

注:有些芯片只支持AAC LC

(b)可变信息头

说明:

*frame_length:一个ADTS帧的长度包括ADTS头和AAC原始流

*adts_buffer_fullness:0x7FF说明是码率可变的码流

(3)  将AAC打包成ADTS格式

通过上述对ADTS封装格式的了解,我们只需要获得相关的音频采样率、声道数、元数据长度、AAC格式类型等信息,就可以在每个ACC原始流前面添加ADTS头。以下是ffmpeg中添加ADTS信息头核心代码:

[cpp] view plaincopy
  1. intff_adts_write_frame_header(ADTSContext *ctx,
  2. uint8_t *buf,int size, int pce_size)
  3. {
  4. PutBitContext pb;
  5. init_put_bits(&pb, buf,ADTS_HEADER_SIZE);
  6. /* adts_fixed_header */
  7. put_bits(&pb, 12, 0xfff);   /* syncword */
  8. put_bits(&pb, 1, 0);        /* ID */
  9. put_bits(&pb, 2, 0);        /* layer */
  10. put_bits(&pb, 1, 1);        /* protection_absent */
  11. put_bits(&pb, 2, ctx->objecttype);/* profile_objecttype */
  12. put_bits(&pb, 4,ctx->sample_rate_index);
  13. put_bits(&pb, 1, 0);        /* private_bit */
  14. put_bits(&pb, 3, ctx->channel_conf);/* channel_configuration */
  15. put_bits(&pb, 1, 0);        /* original_copy */
  16. put_bits(&pb, 1, 0);        /* home */
  17. /* adts_variable_header */
  18. put_bits(&pb, 1, 0);        /* copyright_identification_bit */
  19. put_bits(&pb, 1, 0);        /* copyright_identification_start */
  20. put_bits(&pb, 13, ADTS_HEADER_SIZE +size + pce_size); /* aac_frame_length */
  21. put_bits(&pb, 11, 0x7ff);   /* adts_buffer_fullness */
  22. put_bits(&pb, 2, 0);        /* number_of_raw_data_blocks_in_frame */
  23. flush_put_bits(&pb);
  24. return 0;
  25. }

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视频流。

(1)  将YUV视频数据编码为H.264

首先,创建并配置一个MediaCodec对象,通过指定该对象MIME类型为"video/avc",将其映射到底层的H.264硬件编码器。然后再调用MediaCodec的configure方法来对编码器进行配置,比如指定视频编码器的码率、帧率、颜色格式等信息。

[java] view plaincopy
  1. MediaFormatmFormat = MediaFormat.createVideoFormat(“"video/avc"”, 640 ,480);
  2. //码率,600kbps-5000kbps,根据分辨率、网络情况而定
  3. mFormat.setInteger(MediaFormat.KEY_BIT_RATE,BIT_RATE);
  4. //帧率,15-30fps
  5. mFormat.setInteger(MediaFormat.KEY_FRAME_RATE,FRAME_RATE);
  6. //颜色格式,COLOR_FormatYUV420Planar或COLOR_FormatYUV420SemiPlanar
  7. mFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT,mColorFormat);
  8. //关键帧时间间隔,即编码一次关键帧的时间间隔
  9. mFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL,FRAME_INTERVAL);
  10. //配置、启动编码器
  11. MediaCodec mVideoEncodec = MediaCodec.createByCodecName(mCodecInfo.getName());
  12. mVideoEncodec.configure(mFormat,null,null,MediaCodec.CONFIGURE_FLAG_ENCODE);
  13. 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)方法将数据流(块)提交到编码器中自动进行编码处理。

[java] view plaincopy
  1. ByteBuffer[]inputBuffers = mVideoEncodec.getInputBuffers();
  2. //返回编码器的一个输入缓存区句柄,-1表示当前没有可用的输入缓存区
  3. intinputBufferIndex = mVideoEncodec.dequeueInputBuffer(TIMES_OUT);
  4. if(inputBufferIndex>= 0){
  5. // 绑定一个被空的、可写的输入缓存区inputBuffer到客户端
  6. ByteBuffer inputBuffer  = null;
  7. if(!isLollipop()){
  8. inputBuffer =inputBuffers[inputBufferIndex];
  9. }else{
  10. inputBuffer = mVideoEncodec.getInputBuffer(inputBufferIndex);
  11. }
  12. // 向输入缓存区写入有效原始数据,并提交到编码器中进行编码处理
  13. inputBuffer.clear();
  14. inputBuffer.put(mFrameData);
  15. mVideoEncodec.queueInputBuffer(inputBufferIndex,0,mFrameData.length,getPTSUs(),0);
  16. }

原始数据流被编码处理后,编码好的数据会保存到被APP绑定的输出缓存区,通过调用MediaCodec的dequeueOutputBuffer(MediaCodec.BufferInfo,long)实现。当输出缓存区的数据被处理完毕后(比如推流、混合成MP4),就可以调用MediaCodec的releaseOutputBuffer(int,boolean)方法将输出缓存区还给编码器。

[java] view plaincopy
  1. // 返回一个输出缓存区句柄,当为-1时表示当前没有可用的输出缓存区
  2. // mBufferInfo参数包含被编码好的数据,timesOut参数为超时等待的时间
  3. MediaCodec.BufferInfo  mBufferInfo = new MediaCodec.BufferInfo();
  4. int outputBufferIndex = -1;
  5. do{
  6. outputBufferIndex = mVideoEncodec.dequeueOutputBuffer(mBufferInfo,TIMES_OUT);
  7. if(outputBufferIndex == MediaCodec. INFO_TRY_AGAIN_LATER){
  8. Log.e(TAG,"获得编码器输出缓存区超时");
  9. }else if(outputBufferIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED){
  10. // 如果API小于21,APP需要重新绑定编码器的输入缓存区;
  11. // 如果API大于21,则无需处理INFO_OUTPUT_BUFFERS_CHANGED
  12. if(!isLollipop()){
  13. outputBuffers = mVideoEncodec.getOutputBuffers();
  14. }
  15. }else if(outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED){
  16. // 编码器输出缓存区格式改变,通常在存储数据之前且只会改变一次
  17. // 这里设置混合器视频轨道,如果音频已经添加则启动混合器(保证音视频同步)
  18. MediaFormat newFormat = mVideoEncodec.getOutputFormat();
  19. MediaMuxerUtils mMuxerUtils = muxerRunnableRf.get();
  20. if(mMuxerUtils != null){
  21. mMuxerUtils.setMediaFormat(MediaMuxerUtils.TRACK_VIDEO,newFormat);
  22. }
  23. Log.i(TAG,"编码器输出缓存区格式改变,添加视频轨道到混合器");
  24. }else{
  25. // 获取一个只读的输出缓存区inputBuffer ,它包含被编码好的数据
  26. ByteBuffer outputBuffer = null;
  27. if(!isLollipop()){
  28. outputBuffer  = outputBuffers[outputBufferIndex];
  29. }else{
  30. outputBuffer  = mVideoEncodec.getOutputBuffer(outputBufferIndex);
  31. }
  32. // 如果API<=19,需要根据BufferInfo的offset偏移量调整ByteBuffer的位置
  33. // 并且限定将要读取缓存区数据的长度,否则输出数据会混乱
  34. if (isKITKAT()) {
  35. outputBuffer.position(mBufferInfo.offset);
  36. outputBuffer.limit(mBufferInfo.offset + mBufferInfo.size);
  37. }
  38. // 根据NALU类型判断关键帧
  39. MediaMuxerUtils mMuxerUtils = muxerRunnableRf.get();
  40. int type = outputBuffer.get(4) & 0x1F;
  41. if(type==7 || type==8){
  42. Log.i(TAG, "------PPS、SPS帧(非图像数据),忽略-------");
  43. mBufferInfo.size = 0;
  44. }else if (type == 5) {
  45. Log.i(TAG, "------I帧(关键帧),添加到混合器-------");
  46. if(mMuxerUtils != null && mMuxerUtils.isMuxerStarted()){
  47. mMuxerUtils.addMuxerData(new MediaMuxerUtils.MuxerData(
  48. MediaMuxerUtils.TRACK_VIDEO, outputBuffer,
  49. mBufferInfo));
  50. prevPresentationTimes = mBufferInfo.presentationTimeUs;
  51. isAddKeyFrame  = true;
  52. }
  53. }else{
  54. if(isAddKeyFrame){
  55. Log.d(TAG, "------非I帧(type=1),添加到混合器-------");
  56. if(mMuxerUtils != null&&mMuxerUtils.isMuxerStarted()){
  57. mMuxerUtils.addMuxerData(new MediaMuxerUtils.MuxerData(
  58. MediaMuxerUtils.TRACK_VIDEO, outputBuffer,
  59. mBufferInfo));
  60. prevPresentationTimes = mBufferInfo.presentationTimeUs;
  61. }
  62. }
  63. }
  64. // 处理结束,释放输出缓存区资源
  65. mVideoEncodec.releaseOutputBuffer(outputBufferIndex, false);
  66. }
  67. } while (outputBufferIndex >= 0);

这里有几点需要说明下,因为如果处理不当,可能会导致MediaMuxer合成MP4文件失败或者录制的MP4文件播放时开始会出现大量马赛克或者音视频不同步异常。

a) 如何保证音、视频同步?

要保证录制的MP4文件能够音视频同步,需要做到两点:其一当我们获得输出缓存区的句柄outputBufferIndex等于MediaCodec.INFO_OUTPUT_FORMAT_CHANGED,需要将视频轨道(MediaFormat)设置给MediaMuxer,同时只有在确定音频轨道也被添加后,才能启动MediaMuxer混合器;其二就是传入MediaCodec的queueInputBuffer中PTUs时间参数应该是单调递增的,比如:

[java] view plaincopy
  1. long prevPresentationTimes= mBufferInfo.presentationTimeUs;
  2. private long getPTSUs(){
  3. longresult = System.nanoTime()/1000;
  4. if(result< prevPresentationTimes){
  5. result= (prevPresentationTimes  - result ) +result;
  6. }
  7. returnresult;
  8. }

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颜色格式。

(2) 将PCM音频数据编码为AAC

由于使用MediaCodec编码音视频的原理是一致的,这里就不做过多介绍,相关音频参数配置,可参照我这篇博文。另外,这里是使用AudioRecord来获得PCM音频流,也比较简单,详情可参考这篇博文。代码如下:

[java] view plaincopy
  1. MediaCodec mMediaCodec =MediaCodec.createEncoderByType("audio/mp4a-latm");
  2. MediaFormatformat = new MediaFormat();
  3. format.setString(MediaFormat.KEY_MIME,"audio/mp4a-latm");      // 编码器类型,AAC
  4. format.setInteger(MediaFormat.KEY_BIT_RATE,16000);                 // 比特率,16kbps
  5. format.setInteger(MediaFormat.KEY_CHANNEL_COUNT,1);        // 声道数,1
  6. format.setInteger(MediaFormat.KEY_SAMPLE_RATE,8000);          // 采样率8000Hz
  7. format.setInteger(MediaFormat.KEY_AAC_PROFILE,
  8. MediaCodecInfo.CodecProfileLevel.AACObjectLC);// 芯片支持的AAC级别,LC
  9. format.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE,1600); // 最大缓存,1600
  10. mMediaCodec.configure(format,null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
  11. mMediaCodec.start();
  12. /**
  13. * 使用AudioRecord录制PCM格式音频
  14. */
  15. Process.setThreadPriority(Process.THREAD_PRIORITY_AUDIO);
  16. intbufferSize = AudioRecord.getMinBufferSize(samplingRate,
  17. AudioFormat.CHANNEL_IN_MONO,AudioFormat.ENCODING_PCM_16BIT);
  18. if(bufferSize< 1600){
  19. bufferSize = 1600;
  20. }
  21. //配置录音设备的音频源、采样率、单声道、采样精度
  22. intsamplingRate = 8000;
  23. AudioRecord  mAudioRecord = newAudioRecord(MediaRecorder.AudioSource.MIC,
  24. samplingRate,AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, bufferSize);
  25. mAudioRecord.startRecording();

MediaCodec编码核心与视频相似,由于MediaMuxer不需要ADTS信息头,这里就没有在每桢数据添加信息头

[java] view plaincopy
  1. byte[] audioBuf = new byte[AUDIO_BUFFER_SIZE];
  2. int readBytes = mAudioRecord.read(audioBuf, 0,AUDIO_BUFFER_SIZE);
  3. if (readBytes > 0) {
  4. try {
  5. ByteBuffer[] inputBuffers = mAudioEncoder.getInputBuffers();
  6. ByteBuffer[] outputBuffers = mAudioEncoder.getOutputBuffers();
  7. //返回编码器的一个输入缓存区句柄,-1表示当前没有可用的输入缓存区
  8. int inputBufferIndex = mAudioEncoder.dequeueInputBuffer(TIMES_OUT);
  9. if(inputBufferIndex >= 0){
  10. // 绑定一个被空的、可写的输入缓存区inputBuffer到客户端
  11. ByteBuffer inputBuffer  = null;
  12. if(!isLollipop()){
  13. inputBuffer = inputBuffers[inputBufferIndex];
  14. }else{
  15. inputBuffer = mAudioEncoder.getInputBuffer(inputBufferIndex);
  16. }
  17. // 向输入缓存区写入有效原始数据,并提交到编码器中进行编码处理
  18. if(audioBuf==null || readBytes<=0){
  19. mAudioEncoder.queueInputBuffer(inputBufferIndex,0,0,getPTSUs(),MediaCodec.BUFFER_FLAG_END_OF_STREAM);
  20. }else{
  21. inputBuffer.clear();
  22. inputBuffer.put(audioBuf);
  23. mAudioEncoder.queueInputBuffer(inputBufferIndex,0,readBytes,getPTSUs(),0);
  24. }
  25. }
  26. // 返回一个输出缓存区句柄,当为-1时表示当前没有可用的输出缓存区
  27. // mBufferInfo参数包含被编码好的数据,timesOut参数为超时等待的时间
  28. MediaCodec.BufferInfo  mBufferInfo = new MediaCodec.BufferInfo();
  29. int outputBufferIndex = -1;
  30. do{
  31. outputBufferIndex = mAudioEncoder.dequeueOutputBuffer(mBufferInfo,TIMES_OUT);
  32. if(outputBufferIndex == MediaCodec. INFO_TRY_AGAIN_LATER){
  33. Log.i(TAG,"获得编码器输出缓存区超时");
  34. }else if(outputBufferIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED){
  35. // 如果API小于21,APP需要重新绑定编码器的输入缓存区;
  36. // 如果API大于21,则无需处理INFO_OUTPUT_BUFFERS_CHANGED
  37. if(!isLollipop()){
  38. outputBuffers = mAudioEncoder.getOutputBuffers();
  39. }
  40. }else if(outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED){
  41. // 编码器输出缓存区格式改变,通常在存储数据之前且只会改变一次
  42. // 这里设置混合器视频轨道,如果音频已经添加则启动混合器(保证音视频同步)
  43. MediaFormat newFormat = mAudioEncoder.getOutputFormat();
  44. MediaMuxerUtils mMuxerUtils = muxerRunnableRf.get();
  45. if(mMuxerUtils != null){
  46. mMuxerUtils.setMediaFormat(MediaMuxerUtils.TRACK_AUDIO,newFormat);
  47. }
  48. Log.i(TAG,"编码器输出缓存区格式改变,添加视频轨道到混合器");
  49. }else{
  50. // 当flag属性置为BUFFER_FLAG_CODEC_CONFIG后,说明输出缓存区的数据已经被消费了
  51. if((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0){
  52. Log.i(TAG,"编码数据被消费,BufferInfo的size属性置0");
  53. mBufferInfo.size = 0;
  54. }
  55. // 数据流结束标志,结束本次循环
  56. if((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0){
  57. Log.i(TAG,"数据流结束,退出循环");
  58. break;
  59. }
  60. // 获取一个只读的输出缓存区inputBuffer ,它包含被编码好的数据
  61. ByteBuffer outputBuffer = null;
  62. if(!isLollipop()){
  63. outputBuffer  = outputBuffers[outputBufferIndex];
  64. }else{
  65. outputBuffer  = mAudioEncoder.getOutputBuffer(outputBufferIndex);
  66. }
  67. if(mBufferInfo.size != 0){
  68. // 获取输出缓存区失败,抛出异常
  69. if(outputBuffer == null){
  70. throw new RuntimeException("encodecOutputBuffer"+outputBufferIndex+"was null");
  71. }
  72. // 如果API<=19,需要根据BufferInfo的offset偏移量调整ByteBuffer的位置
  73. //并且限定将要读取缓存区数据的长度,否则输出数据会混乱
  74. if(isKITKAT()){
  75. outputBuffer.position(mBufferInfo.offset);
  76. outputBuffer.limit(mBufferInfo.offset+mBufferInfo.size);
  77. }
  78. // 对输出缓存区的H.264数据进行混合处理
  79. MediaMuxerUtils mMuxerUtils = muxerRunnableRf.get();
  80. mBufferInfo.presentationTimeUs = getPTSUs();
  81. if(mMuxerUtils != null && mMuxerUtils.isMuxerStarted()){
  82. Log.d(TAG,"------混合音频数据-------");
  83. mMuxerUtils.addMuxerData(new MediaMuxerUtils.MuxerData(MediaMuxerUtils.TRACK_AUDIO,outputBuffer,mBufferInfo));
  84. prevPresentationTimes = mBufferInfo.presentationTimeUs;
  85. }
  86. }
  87. // 处理结束,释放输出缓存区资源
  88. mAudioEncoder.releaseOutputBuffer(outputBufferIndex,false);
  89. }
  90. }while (outputBufferIndex >= 0);
  91. } catch (IllegalStateException e) {
  92. // 捕获因中断线程并停止混合dequeueOutputBuffer报的状态异常
  93. e.printStackTrace();
  94. } catch (NullPointerException e) {
  95. // 捕获因中断线程并停止混合MediaCodec为NULL异常
  96. e.printStackTrace();
  97. }
  98. }

如果是使用AAC数据来进行推流,这就需要为每桢音频数据添加ADTS头。参考ADTS头信息格式,以及ffmpeg函数中的相关设置,在Java中ADTS信息头配置信息可为:

[java] view plaincopy
  1. private void addADTStoPacket(byte[] packet, int packetLen) {
  2. packet[0] = (byte) 0xFF;
  3. packet[1] = (byte) 0xF1;
  4. packet[2] = (byte) (((2 - 1) << 6) + (mSamplingRateIndex << 2) + (1 >> 2));
  5. packet[3] = (byte) (((1 & 3) << 6) + (packetLen >> 11));
  6. packet[4] = (byte) ((packetLen & 0x7FF) >> 3);
  7. packet[5] = (byte) (((packetLen & 7) << 5) + 0x1F);
  8. packet[6] = (byte) 0xFC;
  9. }

其中,packetLen为原始帧数据长度,mSamplingRateIndex为自定义采样率数组下标;

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

(3)使用MediaMuxer混合H.264+AAC生成MP4文件

MediaMuxer的使用比较简单,但需要严格按照以下三个步骤进行:

第一步:配置混合器音、视频轨道

[java] view plaincopy
  1. public synchronized voidsetMediaFormat(int index, MediaFormat mediaFormat) {
  2. if (mediaMuxer == null) {
  3. return;
  4. }
  5. // 设置视频轨道格式
  6. if (index == TRACK_VIDEO) {
  7. if (videoMediaFormat ==null) {
  8. videoMediaFormat =mediaFormat;
  9. videoTrackIndex =mediaMuxer.addTrack(mediaFormat);
  10. isVideoAdd = true;
  11. Log.i(TAG, "添加视频轨道");
  12. }
  13. } else {
  14. if (audioMediaFormat ==null) {
  15. audioMediaFormat =mediaFormat;
  16. audioTrackIndex =mediaMuxer.addTrack(mediaFormat);
  17. isAudioAdd = true;
  18. Log.i(TAG, "添加音频轨道");
  19. }
  20. }
  21. // 启动混合器
  22. startMediaMuxer();
  23. }

第二步:音、视频轨道均添加,启动混合器

[java] view plaincopy
  1. private void startMediaMuxer() {
  2. if (mediaMuxer == null) {
  3. return;
  4. }
  5. if (isMuxerFormatAdded()) {
  6. mediaMuxer.start();
  7. isMediaMuxerStart = true;
  8. Log.i(TAG, "启动混合器,开始等待数据输入.....");
  9. }
  10. }

第三步:添加音视频数据到混合器

[java] view plaincopy
  1. public void addMuxerData(MuxerData data){
  2. int track = 0;
  3. if (data.trackIndex ==TRACK_VIDEO) {
  4. track = videoTrackIndex;
  5. } else {
  6. track = audioTrackIndex;
  7. }
  8. try {
  9. ByteBuffer outputBuffer =data.byteBuf;
  10. BufferInfo bufferInfo =data.bufferInfo;
  11. if(isMediaMuxerStart&& bufferInfo.size != 0){
  12. outputBuffer.position(bufferInfo.offset);
  13. outputBuffer.limit(bufferInfo.offset+ bufferInfo.size);
  14. Log.i(TAG, "写入混合数据+"+data.trackIndex+",大小-->"+ bufferInfo.size);
  15. mediaMuxer.writeSampleData(track,outputBuffer,bufferInfo);
  16. }
  17. if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0){
  18. Log.i(TAG,"BUFFER_FLAG_END_OF_STREAM received");
  19. }
  20. } catch (Exception e) {
  21. Log.e("TAG","写入混合数据失败!" +e.toString());
  22. restartMediaMuxer();
  23. }
  24. }

效果演示:

3. 开源项目:AndroidRecordMp4

1. 添加依赖

(1) 在工程build.gradle中添加

allprojects {repositories {...maven { url 'https://jitpack.io' }}}

(2) 在module的gradle中添加

dependencies {compile 'com.github.jiangdongguo:AndroidRecordMp4:v1.0.0'
}

2. 使用方法

(1) 初始化引擎

 RecordMp4 mRecMp4 = RecordMp4.getRecordMp4Instance();mRecMp4.init(this);  // 上下文

(2) 配置编码参数

  EncoderParams mParams = new EncoderParams();mParams.setVideoPath(RecordMp4.ROOT_PATH+ File.separator + System.currentTimeMillis() + ".mp4");    // 视频文件路径mParams.setFrameWidth(CameraManager.PREVIEW_WIDTH);             // 分辨率mParams.setFrameHeight(CameraManager.PREVIEW_HEIGHT);mParams.setBitRateQuality(H264EncodeConsumer.Quality.MIDDLE);   // 视频编码码率mParams.setFrameRateDegree(H264EncodeConsumer.FrameRate._30fps);// 视频编码帧率mParams.setFrontCamera((mRecMp4!=null&&mRecMp4.isFrontCamera()) ? true:false);       // 摄像头方向mParams.setPhoneHorizontal(false);  // 是否为横屏拍摄mParams.setAudioBitrate(AACEncodeConsumer.DEFAULT_BIT_RATE);        // 音频比特率mParams.setAudioSampleRate(AACEncodeConsumer.DEFAULT_SAMPLE_RATE);  // 音频采样率mParams.setAudioChannelConfig(AACEncodeConsumer.CHANNEL_IN_MONO);// 单声道mParams.setAudioChannelCount(AACEncodeConsumer.CHANNEL_COUNT_MONO);       // 单声道通道数量mParams.setAudioFormat(AACEncodeConsumer.ENCODING_PCM_16BIT);       // 采样精度为16位mParams.setAudioSouce(AACEncodeConsumer.SOURCE_MIC);                // 音频源为MICmRecMp4.setEncodeParams(getEncodeParams());

(3) 开始 /停止录制

 mRecMp4.startRecord();mRecMp4.stopRecord();

(4) Camera渲染

public class MainActivity extends Activity implements SurfaceHolder.Callback{@Overridepublic void surfaceCreated(SurfaceHolder surfaceHolder) {if(mRecMp4 != null){mRecMp4.startCamera(surfaceHolder);}}@Overridepublic void surfaceChanged(SurfaceHolder surfaceHolder, int i, int i1, int i2) {}@Overridepublic void surfaceDestroyed(SurfaceHolder surfaceHolder) {if(mRecMp4 != null){mRecMp4.stopCamera();}}

(5) 摄像头控制

// 对焦mRecMp4.enableFocus(new CameraManager.OnCameraFocusResult() {@Overridepublic void onFocusResult(boolean result) {if(result){showMsg("对焦成功");}}});// 切换摄像头if(mRecMp4 != null){mRecMp4.switchCamera();}// 切换分辨率if(mRecMp4 != null){mRecMp4.setPreviewSize(1280,720);}  

(6) JPG图片抓拍

      mRecMp4.capturePicture(picPath, new SaveYuvImageTask.OnSaveYuvResultListener() {@Overridepublic void onSaveResult(boolean result, String savePath) {Log.i("MainActivity","抓拍结果:"+result+"保存路径:"+savePath);}});

最后,不要忘记添加权限哈

   <uses-permission android:name="android.permission.RECORD_AUDIO"/><uses-permission android:name="android.permission.CAMERA"/><uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> 

Github项目地址:https://github.com/jiangdongguo/AndroidRecordMp4,欢迎大家star & clone~


参考

音频编码格式介绍:https://wenku.baidu.com/view/0e8115fcfab069dc502201f0.html?re=view

MP4封装格式:http://blog.csdn.net/u010246197/article/details/52924365

ffmpeg开发之旅(3):AAC编码格式分析与MP4文件封装(MediaCodec+MediaMuxer)相关推荐

  1. Android直播开发之旅(3):AAC编码格式分析与MP4文件封装(MediaCodec+MediaMuxer)

    Android直播开发之旅(3):AAC编码格式分析与MP4文件封装(MediaCodec+MediaMuxer) (码字不易,转载请声明出处:http://blog.csdn.net/andrexp ...

  2. ffmpeg开发之旅(4):MP3编码格式分析与lame库编译封装

    ffmpeg开发之旅(4):MP3编码格式分析与lame库编译封装 原文链接:http://blog.csdn.net/andrexpert/article/77683776 一.Mp3编码格式分析 ...

  3. FFmpeg开发之旅(三)---理解过滤图并使用字幕过滤器

    [写在前面] 首先,抛开字幕本身的格式不说. 一般的字幕分三种,内封字幕.内嵌字幕和外挂字幕. 而本篇所讲的是外挂字幕,主要内容有: 1.FFmpeg过滤图基础. 2.使用FFmpeg字幕过滤器添加字 ...

  4. ffmpeg源码分析及mp4文件解析

    一.mp4文件的组织 1. mp4文件的box(ffmpeg中叫atom) mp4是由一系列的box组成的,每个box的header是8个字节(4字节的长度,4字节的type) 第一个box比较特殊, ...

  5. javacv利用ffmpeg实现录屏和录音,输出为mp4文件

    前言 不知道怎么使用配置javacv的看上一篇文章----使用javacv中的ffmpeg实现录屏,结果连运行都失败了,现在终于解决了 这篇博客是上篇博客代码的改进,因为上篇博客的代码有很多bug. ...

  6. h264和aac格式介绍及mp4文件的封装

    mp4封装 目录 h264视频流格式介绍 aac音频流格式介绍 h264视频文件读取 通过帧索引解析h264文件 通过解析h264结构读取文件 aac音频文件读取 mp4封装 初始化 数据封装 关闭m ...

  7. FFmpeg开发之旅(二)---音频解码

    [写在前面] 前面我介绍了视频解码的流程,发现基础讲得有点少. 因此这里附上一些额外的基础内容:理解PCM音频数据格式 本篇主要内容: 1.FFmpeg音频解码基本流程 2.libswresample ...

  8. Android直播开发之旅(17):使用FFmpeg提取MP4中的H264和AAC

    最近在开发中遇到了一个问题,即无法提取到MP4中H264流的关键帧进行处理,且保存到本地的AAC音频也无法正常播放.经过调试分析发现,这是由于解封装MP4得到的H264和AAC是ES流,它们缺失解码时 ...

  9. Android直播开发之旅(7):Android视频直播核心技术(架构)详解

    (转载请声明出处:http://blog.csdn.net/andrexpert/article/details/76919535) 一.直播架构解析 目前主流的直播架构中主要有两种方案,即流媒体转发 ...

最新文章

  1. Ubuntu 12.10 拨号上网及停用方法
  2. 学习ASP.NET Core Razor 编程系列九——增加查询功能
  3. Mac系统的JDK8默认安装路径
  4. ios可变数组的操作
  5. openresty开发系列4--nginx的配置文件说明
  6. cocos ScrollView(滚动容器)加载大量item导致的卡顿问题解决方案
  7. Siebel Issue:Siebel菜单栏无法在IE7/8下展开问题解决方案
  8. php获取虚拟机ip,php如何获取用户的ip地址
  9. 根据另外一个表来更新,增加字段
  10. java 日期 年数_关于java:为什么不赞成使用“新日期(整数年,整数月,整数日)”?...
  11. MySQL千万级大表优化解决方案
  12. 【人脸识别】基于matlab ksvd字典学习人脸表情识别【含Matlab源码 460期】
  13. 安装vue-devtool
  14. 【Java教程】UDP实现群聊聊天室
  15. VESA编程——GUI离我们并不遥远
  16. 架构必看:12306抢票亿级流量架构演进(图解+秒懂+史上最全)
  17. JavaScript判断浏览器版本
  18. Thinkpad E575重装系统,无法找到系统盘,无法开机
  19. SNMP协议——网络管理概述
  20. IP地址更改小工具(bat命令)

热门文章

  1. git 加速 看一下效果
  2. javascript 知识
  3. 第一章: HTML、CSS、JavaScript入门篇
  4. 影视广告之神器Cinema 4D
  5. java泛型数组:Type parameter ‘T‘ cannot be instantiated directly
  6. 关于bootstrap--表格(table的各种样式)
  7. 程序员练级(转自酷壳)
  8. 计算机教师职业幸福感,教师职业幸福感
  9. 被雷总“随手拍”的超级月亮惊呆了...
  10. 分布式系统上下层概念抽象-(2)