实时AAC音频/本地AAC音视频硬解码详细介绍附带Demo

  • 一、使用AAC音频硬解码的背景
    • 开发成本
    • 维护成本
  • 二、使用AAC音频硬解码的优缺点
    • 优点
    • 缺点
  • 三、AAC音频硬解码的API介绍
    • MediaCodec 方法介绍
    • MediaCodec 参数介绍
  • 四、AAC音频硬解码
    • 本地音视频文件里的AAC音频硬解码介绍,MediaExtractor方法详解
    • 实时AAC音频硬解码介绍
  • 五、音视频编解码的CSD参数
    • 音频编解码的CSD参数介绍
    • 视频编解码的CSD参数介绍
  • 六、代码示例
    • 本地音视频文件中的AAC音频硬解码
    • 实时AAC音频文件的硬解码
  • 七、Demo地址
    • AacDecoder
    • 感谢信,致热爱编程的你

一、使用AAC音频硬解码的背景

因为各种原因,在日常的开发中开发者或多或少都要接触一些音视频编解码相关的功能,所以有时候选择编解码工具就变得尤为重要,取决于你的项目属性又或者知识广度等等,下面作者结合自己的实际项目经验给大家分析一下

开发成本

开发成本在企业管理者的角度来说尤为重要,关系到企业的盈利与生存。所以为了降低成本很多开发者会考虑去使用Android 原生提供的一些API,而不是去使用第三方的一些开源库或者收费库,因为那样急需要花费额外的金钱并且还需要花费时间与精力去熟悉,所以也不推荐,除非时间和成本都在允许的范围内

维护成本

当项目迭代至成熟期时,维护成本就成了后续开发者要关注的事情,首先假设我们使用了第三方的库,如果你的产品已经卖出去了,而这时候第三方库不维护并且出现了一个致命的问题,那这样就会导致卖出去的产品都会被投诉并且短时间内还要花时间去移除之前使用的第三方库,如果耦合性过多,将导致无法挽回的经济损失。而如果使用的是Android 原生的API的话,因为本身是做产品的,所以只考虑当前设备,无须关心移植到其他平台或其他系统版本,前期做稳定,后期就不会有任何问题

二、使用AAC音频硬解码的优缺点

优点

开发方便快捷,有成熟的API调用,使用简单,网上也有大部分的参考资料

缺点

可移植性差,如果公司其他项目需要移植到新的硬件平台时,会有兼容性问题,大部分需要向原厂提工单才可解决

三、AAC音频硬解码的API介绍

MediaCodec 方法介绍

MediaCodec是Android原生提供的API,支持音视频的硬编码和硬解码,Android常用的源文件格式与编码后格式是音频的PCM编码成AAC,视频的NV21/YV12编码成H264,值得一提的是在选择和设置视频编码质量的时候,MediaFormat.KEY_PROFILE 在官方API介绍中,其可以控制视频的质量,实际则是Android7.0以下默认baseline,不管怎么设置都是默认baseline,所以这个变量属性,作者采用了删除线,在视频编码时,不推荐大家使用,避免出现问题

getInputBuffers()

从当前编解码器中获取输入缓冲区数组,用于向输入缓冲区中添加要编解码的数据

getOutputBuffers()

从当前编解码器中获取输出缓冲区数组,用于提取编解码之后的数据缓冲区

dequeueInputBuffer(long timeoutUs)

获取输入缓冲区数组中待使用(空闲)的缓冲区数组下标索引,timeoutUs为0时立即返回,小于0时表示一直等待直至输入缓冲区数组中有可用的缓冲区为止,大于0则表示等待时间为timeoutUs

getInputBuffer(int index)

获取输入缓冲区数组中待使用(空闲)的缓冲区,index参数为dequeueInputBuffer(long timeoutUs)的返回值,返回值大于等于0即表示有可用的输入缓冲区

queueInputBuffer(int index, int offset, int size, long presentationTimeUs, int flags)

向输入缓冲区数组中添加要编解码的数据,index参数为dequeueInputBuffer(long timeoutUs)的返回值,offset为要编解码数据的起始偏移,size为要编解码数据的长度,presentationTimeUs为PTS,flags为标记,正常使用时可默认填0,编解码至结尾时可填MediaCodec.BUFFER_FLAG_END_OF_STREAM值

dequeueOutputBuffer(BufferInfo info, long timeoutUs)

从输出缓冲区数组中获取编解码成功的缓冲区下标索引,info参数表示传入一个BufferInfo Java bean class , 编解码器会把处理完后的数据信息等以bean类型返回给开发者,timeoutUs意义跟之前介绍的dequeueInputBuffer(long timeoutUs)方法大致相同,返回值大于等于0即表示有可用的输出缓冲区

getOutputBuffer(int index)

获取输出缓冲区数组中编解码完成的缓冲区,index参数为dequeueOutputBuffer(BufferInfo info, long timeoutUs)方法的返回值,返回值大于等于0即表示有可用的输出缓冲区

releaseOutputBuffer(int index, boolean render)

释放编解码器输出缓冲区数组中的缓冲区,index为要释放的缓冲区数组下标索引,它为dequeueOutputBuffer(BufferInfo info, long timeoutUs)方法的返回值,render参数为渲染控制,如果在编解码时设置了可用的surface,render为true时则表示将此数据缓冲区输出到surface渲染

stop()

关闭编解码

release()

释放编解码资源

MediaCodec 参数介绍

本篇文章关于MediaCodec 参数的介绍只描述日常开发中出现频率最频繁的,其他一些参数很少使用或者使用之后没效果,这里就不再做过多阐述

MediaFormat.KEY_AAC_PROFILE

要使用的AAC配置文件的键(仅AAC音频格式时使用),常量在android.media.MediaCodecInfo.CodecProfileLevel 中声明,音频编码中最常用的变量是MediaCodecInfo.CodecProfileLevel.AACObjectLC

MediaFormat.KEY_CHANNEL_MASK

音频内容的通道组成的键,在音频编码中需要根据硬件支持去有选择性的选择支持范围内的通道号

MediaFormat.KEY_BIT_RATE

音视频平均比特率,以位/秒为单位(bit/s)的键

MediaFormat.KEY_CHANNEL_COUNT

音频通道数的键

MediaFormat.KEY_COLOR_FORMAT

输入视频源的颜色格式,日常开发中可根据查询设备颜色格式支持进行选择

MediaFormat.KEY_FRAME_RATE

视频帧速率的键,以帧/秒(frame/s)为单位

MediaFormat.KEY_I_FRAME_INTERVAL

关键帧间隔的键

MediaFormat.KEY_MAX_INPUT_SIZE

编解码器中数据缓冲区最大大小的键,以字节(byte)为单位

四、AAC音频硬解码

本地音视频文件里的AAC音频硬解码介绍,MediaExtractor方法详解

解析本地音视频文件里的AAC音频,需要我们借助一些MediaCodec之外的API即MediaExtractor,如果不熟悉或之前没使用过,没关系!作者会在本篇文章中做一个详细的概述,帮助你加深印象

setDataSource(String path)

设置音视频文件的绝对路径或音视频文件的http地址,path参数可以是本地音视频文件的绝对路径或网络上的音视频文件http地址

getTrackCount()

获取音视频数据中的轨道数,正常情况下的音视频有audio/xxx及video/xxx

getTrackFormat(int index)

获取音视频数据中音频或视频的 android.media.MediaFormat,这个很重要后面还会有代码示例来介绍,index参数为音频或视频数据轨道的索引,返回值是 android.media.MediaFormat

selectTrack(int index)

选择要extract的数据轨道,index参数为指定的音频或视频轨道的索引,后面也是会通过代码示例详细介绍

readSampleData(ByteBuffer byteBuf, int offset)

读取音频或视频轨道中的数据到给定的 ByteBuffer 缓冲区中,byteBuf参数为要保存数据的目标缓冲区,offset参数为音频或视频的数据起始偏移量,返回值为int类型,大于0表示还有数据未处理完,否则表示数据已经全部处理完成

getSampleTime()

获取该帧音频或视频的的时间戳即PTS,返回值为long类型,以微秒(us)为单位,如无可用返回-1

advance()

此方法表示开始处理下一帧音频或视频,如果还有数据返回true,已无数据则返回false

release()

释放资源,在 advance() 返回 false或中断read操作后使用,表示数据处理完毕或不再读取数据

实时AAC音频硬解码介绍

实时AAC音频硬解码其实跟本地音视频AAC音频硬解码大同小异,唯一差异就是实时的不需要去使用MediaExtractor进行音频轨与视频轨进行分离,可以直接使用MediaCodec进行音频硬解码,但需要解析实时流里的ADTS音频头,否则MediaCodec解码器是无法识别出该数据源是否是AAC音频。正常情况下需要开发者解析ADTS头中的一些关键信息,如采样率索引(可根据采样率进行换算)、通道数。

下面作者就给大家介绍关于ADTS头的解析及ADTS其他位的意义:

ADTS头的解析及ADTS其他位的意义

ADTS头结构:
AAAAAAAA AAAABCCD EEFFFFGH HHIJKLMM MMMMMMMM MMMOOOOO OOOOOOPP (QQQQQQQQ QQQQQQQQ)

序号 字段 位数 含义
A syncword 12 AAC音频头固定起始标记
B MPEG Version 1 0是MPEG-4,1是MPEG-2
C Layer 2 always 0
D protection absent 1 没有CRC设置为1,如果有CRC则设置为0
E profile 2 the MPEG-4 Audio Object Type minus 1
F Sampling Frequency Index 4 MPEG-4 Sampling Frequency Index(15 is forbidden)
G private bit 1 编码时设置为0,解码时忽略
H Channel Configuration 3 MPEG-4 Channel Configuration (in the case of 0, the channel configuration is sent via an inband PCE)
I originality 1 编码时设置为0,解码时忽略
J home 1 编码时设置为0,解码时忽略
K copyrighted id bit 1 编码时设置为0,解码时忽略
L copyright id start 1 编码时设置为0,解码时忽略
M frame length 13 此值必须包含7或9个字节的标头长度:FrameLength = header(protection absent == 1?7:9)+ size(AAC Frame Length)
O Buffer fullness 11 No Description
P rdbs 2 ADTS帧中的AAC帧数(RDBs)减去1,为获得最大兼容性,请始终为每个ADTS帧使用1个AAC帧
Q crc 16 如果 protection absent 字段为0,表示携带CRC 2字节的数据

在实时AAC音频硬解码时,我们只需要解析采样率索引(可根据采样率进行换算)、通道数即可,音频采样率索引见MPEG-4 Sampling Frequency Index,接下来还会向各位介绍更重要的音视频参数

五、音视频编解码的CSD参数

音频编解码的CSD参数介绍

在Android中如果调用麦克风进行录音,结合视频使用MediaMuxer进行音视频合成时,是需要开发者传入CSD参数的,否则Android在播放或展示时会出现不识别等其他问题,所以需要开发者在编解码时需要调用MediaFormat设置CSD参数

在音频编解码中,CSD参数只需要设置一个,那就是csd-0即ADTS音频头,在解析本地音视频中的AAC音频时,开发者可以调用MediaFormat取到这个csd-0参数对应的ADTS音频头,然后进行后续的其他操作,后续代码示例还会再次介绍。如果解析的是实时AAC音频,那就需要参照第四步骤对ADTS头进行解析,然后计算CSD参数并设置到MediaFormat中,然后配置到MediaCodec中进行解码,具体算法将在后面的代码示例中提到

视频编解码的CSD参数介绍

在Android中如果调用摄像头进行录像,结合音频使用MediaMuxer进行音视频合成时,是需要开发者传入CSD参数的,否则Android在播放或展示时会出现不识别等其他问题,所以需要开发者在编解码时需要调用MediaFormat设置CSD参数

在视频编解码中,CSD参数需要设置2个,那就是csd-0、csd-1即sps视频头和pps视频头,在解码本地h264编码视频时可以调用MediaFormat获取sps视频头和pps视频头,减少sps/pps视频头运算和查找的操作,简单快捷且高效!具体使用会在代码示例中再次提及

六、代码示例

本地音视频文件中的AAC音频硬解码

    /*** set decode file path** @param decodeFilePath decode file path*/public void setDecodeFilePath(String decodeFilePath) {if (TextUtils.isEmpty(decodeFilePath)) {throw new RuntimeException("decode file path must not be null!");}mediaExtractor = getMediaExtractor(decodeFilePath);}

上述代码片段为设置一个需要解码的文件的绝对路径,路径为null时抛出一个运行时异常,提示路径不能为null,然后就是获取MediaExtractor对象,为提取音频做准备

    /*** get media extractor** @param videoPath need extract of tht video file absolute path* @return {@link MediaExtractor} media extractor instance object* @throws IOException*/protected MediaExtractor getMediaExtractor(String videoPath) {MediaExtractor mMediaExtractor = new MediaExtractor();try {// set file pathmMediaExtractor.setDataSource(videoPath);// get source file track countint trackCount = mMediaExtractor.getTrackCount();for (int i = 0; i < trackCount; i++) {// get current media track media formatMediaFormat mediaFormat = mMediaExtractor.getTrackFormat(i);// if media format object not be nullif (mediaFormat != null) {// get media mime typeString mimeType = mediaFormat.getString(MediaFormat.KEY_MIME);// media mime type match audioif (mimeType.startsWith(AUDIO_MIME_TYE)) {// set media track is audiomMediaExtractor.selectTrack(i);// you can using media format object call getByteBuffer method and input key "csd-0" get it value , if you want.// it is aac adts audio header.adtsAudioHeader = mediaFormat.getByteBuffer(CSD_MIME_TYPE_0).array();// get audio samplesampleRate = mediaFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE);// get audio channel countchannelCount = mediaFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT);return mMediaExtractor;}// >>>>>>>>>>> expand start >>>>>>>>>>>// media mime type match video// else if (mimeType.startsWith(VIDEO_MIME_TYE)) {// get video sps header// byte[] spsVideoHeader = mediaFormat.getByteBuffer(CSD_MIME_TYPE_0).array();// get video pps header// byte[] ppsVideoHeader = mediaFormat.getByteBuffer(CSD_MIME_TYPE_1).array();// }// <<<<<<<<<<< expand end <<<<<<<<<<<}}} catch (IOException e) {Log.d(TAG, "happened io exception : " + e.toString());if (mMediaExtractor != null) {mMediaExtractor.release();}}return null;}

上述代码片段为获取MediaExtractor对象,在设置文件路径后调用其getTrackCount()方法获取文件的所有轨道数,再使用for循环去逐一匹配我们需要的媒体源轨道,调用其getTrackFormat(int index)方法获取该轨道的MediaFormat,最后再去匹配该轨道MediaFormat的mime type,如果匹配到其mime type以关注的mime type字符开始时,获取其csd-0参数的值(音频中对应ADTS头)、采样率、通道数并调用selectTrack(int index)方法将该轨道设置为选定的轨道。

视频相关的参数获取也在代码片段中的expand范围内给出,大家可以了解一下,作者也将其添加上来了,只不过是在代码中注释了,为的就是给大家拓展一下这方面的知识

    @Overridepublic void start() {if (mediaExtractor == null) {Log.e(TAG, "media extractor is null , so return!");return;}if (adtsAudioHeader == null || adtsAudioHeader.length == 0) {Log.e(TAG, "aac audio adts header is null , so return!");return;}aacDecoder = createDefaultDecoder();if (aacDecoder == null) {Log.e(TAG, "aac audio decoder is null , so return!");return;}if (worker == null) {isDecoding = true;worker = new Thread(this, TAG);worker.start();}}

上述代码片段为准备开始提取AAC音频并进行MediaCodec硬解码,首先判断前面代码片段中MediaExtractor对象是否为空,完事在判断获取轨道时的ADTS头是否正常取到,最后生成一个AAC音频解码器,如果生成无异常,开启一个工作线程进行音频的提取和解码

    /*** create default aac decoder** @return {@link MediaCodec} aac audio decoder*/private MediaCodec createDefaultDecoder() {try {MediaFormat mediaFormat = new MediaFormat();mediaFormat.setString(MediaFormat.KEY_MIME, AUDIO_DECODE_MIME_TYPE);mediaFormat.setInteger(MediaFormat.KEY_SAMPLE_RATE, sampleRate);mediaFormat.setInteger(MediaFormat.KEY_CHANNEL_COUNT, channelCount);ByteBuffer byteBuffer = ByteBuffer.allocate(adtsAudioHeader.length);byteBuffer.put(adtsAudioHeader);byteBuffer.flip();mediaFormat.setByteBuffer(CSD_MIME_TYPE_0, byteBuffer);MediaCodec aacDecoder = MediaCodec.createDecoderByType(AUDIO_DECODE_MIME_TYPE);aacDecoder.configure(mediaFormat, null, null, 0);aacDecoder.start();return aacDecoder;} catch (IOException e) {Log.e(TAG, "create aac audio decoder happened io exception : " + e.toString());}return null;}

上述代码片段为创建音频解码器,sampleRate、channelCount、adtsAudioHeader都是前面代码片段中通过MediaExtractor从文件的媒体轨道中的MediaFormat获取的

    /*** aac audio format decode to pcm audi format*/private void aacDecodeToPcm() {isLowVersion = android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.LOLLIPOP;ByteBuffer[] aacDecodeInputBuffers = null;ByteBuffer[] aacDecodeOutputBuffers = null;if (isLowVersion) {aacDecodeInputBuffers = aacDecoder.getInputBuffers();aacDecodeOutputBuffers = aacDecoder.getOutputBuffers();}MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();// initialization audio track , use for play pcm audio data// audio output channel param channelConfig according device support selectint buffsize = AudioTrack.getMinBufferSize(sampleRate, channelConfig, AudioFormat.ENCODING_PCM_16BIT);AudioTrack audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, sampleRate, channelConfig,AudioFormat.ENCODING_PCM_16BIT, buffsize, AudioTrack.MODE_STREAM);audioTrack.play();Log.d(TAG, "aac audio decode thread start");while (isDecoding) {// This method will return immediately if timeoutUs == 0// wait indefinitely for the availability of an input buffer if timeoutUs < 0// wait up to "timeoutUs" microseconds if timeoutUs > 0.int aacDecodeInputBuffersIndex = aacDecoder.dequeueInputBuffer(2000);// no such buffer is currently available , if aacDecodeInputBuffersIndex is -1if (aacDecodeInputBuffersIndex >= 0) {ByteBuffer sampleDataBuffer;if (isLowVersion) {sampleDataBuffer = aacDecodeInputBuffers[aacDecodeInputBuffersIndex];} else {sampleDataBuffer = aacDecoder.getInputBuffer(aacDecodeInputBuffersIndex);}int sampleDataSize = mediaExtractor.readSampleData(sampleDataBuffer, 0);if (sampleDataSize < 0) {aacDecoder.queueInputBuffer(aacDecodeInputBuffersIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);} else {try {long presentationTimeUs = mediaExtractor.getSampleTime();aacDecoder.queueInputBuffer(aacDecodeInputBuffersIndex, 0, sampleDataSize, presentationTimeUs, 0);mediaExtractor.advance();} catch (Exception e) {Log.e(TAG, "aac decode to pcm happened Exception : " + e.toString());continue;}}int aacDecodeOutputBuffersIndex = aacDecoder.dequeueOutputBuffer(info, 2000);if (aacDecodeOutputBuffersIndex >= 0) {if (((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0)) {Log.d(TAG, "aac decode thread read sample data done!");break;} else {ByteBuffer pcmOutputBuffer;if (isLowVersion) {pcmOutputBuffer = aacDecodeOutputBuffers[aacDecodeOutputBuffersIndex];} else {pcmOutputBuffer = aacDecoder.getOutputBuffer(aacDecodeOutputBuffersIndex);}ByteBuffer copyBuffer = ByteBuffer.allocate(pcmOutputBuffer.remaining());copyBuffer.put(pcmOutputBuffer);copyBuffer.flip();final byte[] pcm = new byte[info.size];copyBuffer.get(pcm);copyBuffer.clear();audioTrack.write(pcm, 0, info.size);aacDecoder.releaseOutputBuffer(aacDecodeOutputBuffersIndex, false);}}}}Log.d(TAG, "aac audio decode thread stop");aacDecoder.stop();aacDecoder.release();aacDecoder = null;mediaExtractor.release();mediaExtractor = null;isDecoding = false;worker = null;}

上述代码片段稍长,作者就做一个简单的概括吧,先获取MediaCodec的输入输出缓冲区数组,然后读取文件的音频轨道数据填充到可用的输入缓冲区中,在进行音频的硬解码,最后从解码成功后存放的输出缓冲区数组中拿到解码后的PCM数据,通过AudioTrack播放出来,这个播放动作是为了验证解码出来的数据是否有异常

实时AAC音频文件的硬解码

实时的解码先不上代码,而是先帮助大家理解,我们需要怎么去解析AAC音频的ADTS头?要取哪些对我们有用的字节?别急,作者会细细的说

FF F1 6C 40 18 02 3C

上述数据是作者从实际项目开发中提取出来的AAC实时流的ADTS音频头,想通过这样的方式来解答之前提到的两个问题,这段字符表示7个16进制的字节,将其进行补全则为如下数据:

0xFF 0xF1 0x6C 0x40 0x18 0x02 0x3C

根据最前面提到的ADTS头结构可知,我们只需要关注7个字节中的前面个4字节,也就是0~3字节即可并取出其对应位的值用于生成解码器,所以我们只需要关心如下数据:

0xFF 0xF1 0x6C 0x40

然后接下来一一解析给大家看,首先是0xFF 0xF1

        // 解析 0xFF 0xF1// 将第0字节0xFF和第1字节0xF1通过位运算,将其转换成int类型,即65521// 再将65521 转换成二进制类型,即 1111111111110001// syncword : 111111111111 (即固定0xfff) 12位// MPEG Version: 0 (表 MPEG-4) 1位// Layer: 00 (固定 0) 2位// protection absent : 1 (表无CRC数据) 1位

接下来再解析0x6C 0x40,计算到前面低10位就行了,后续位用不上

        // 解析 0x6C 0x40// 将第2字节0x6C和第3字节0x40通过位运算,将其转换成int类型,即27712// 再将27712 转换成二进制类型,即 110110001000000,因不足16位,所以在高位补0,满足16位,补足后 0110110001000000// profile : 01 (aac profile) 2位// Sampling Frequency Index : 1011 (值为11,即采样率8000) 4位// private bit :0 (编码时设为0 ,解码可忽略) 1位// Channel Configuration : 001 (通道参数) 3位// ......

结合作者刚刚举例的案例,也可以自己写出ADTS音频头解析,下面作者开始贴实时AAC音频硬解码的代码片段

    @Overridepublic void start() {if (worker == null) {isDecoding = true;waitTimeSum = 0;worker = new Thread(this, TAG);worker.start();}}

上述代码片段为启动工作线程,开始进行MediaCodec硬解码操作

    @Overridepublic void run() {final long timeOut = 5 * 1000;final long waitTime = 500;while (isDecoding) {while (!aacFrameQueue.isEmpty()) {byte[] aac = aacFrameQueue.poll();if (aac != null) {if (!hasAacDecoder(aac)) {Log.d(TAG, "aac decoder create failure , so break!");break;}// todo decode aac audio data.// remove aac audio adts headerbyte[] aacTemp = new byte[aac.length - 7];// data copySystem.arraycopy(aac, 7, aacTemp, 0, aacTemp.length);// decode aac audiodecode(aacTemp, aacTemp.length);}}// Waiting for next framesynchronized (decodeLock) {try {// isEmpty() may take some time, so we set timeout to detect next framedecodeLock.wait(waitTime);waitTimeSum += waitTime;if (waitTimeSum >= timeOut) {Log.d(TAG, "realtime aac decode thread read timeout , so break!");break;}} catch (InterruptedException ie) {worker.interrupt();}}}Log.d(TAG, "realtime aac decode thread stop!");if (aacDecoder != null) {aacDecoder.stop();aacDecoder.release();aacDecoder = null;}if (audioTrack != null) {audioTrack.stop();audioTrack.release();audioTrack = null;}aacFrameQueue.clear();adtsAudioHeader = null;isDecoding = false;worker = null;}

上述代码片段为线程执行解码,判断AAC队列中是否有数据,如果有就取出一个数据,先判空然后再检测AAC的ADTS音频头是否符合规范,如不符合或创建解码器发生异常都将直接退出循环结束线程工作,如果队列中没有数据则等待500ms,继续轮询队列里的数据,当线程工作结束,释放相关API的资源,任何时候都要对相关的一些创建操作进行回收且形成闭环,避免发生内存泄漏

    /*** put realtime aac audio data** @param aac aac audio data*/public void putAacData(byte[] aac) {if (isDecoding) {aacFrameQueue.add(aac);synchronized (decodeLock) {waitTimeSum = 0;decodeLock.notifyAll();}}}

上述代码为添加AAC实时数据到缓存队列中,这个数据可以是来自TCP等的实时流媒体数据,如果当前解码工作线程正在解码,则添加一个AAC到缓存队列中,重置等待时间并且唤醒等待中的对象锁,让线程拿到锁后继续执行

    /*** @param aac aac audio data* @return true means has aad decoder*/private boolean hasAacDecoder(byte[] aac) {if (aacDecoder != null) {return true;}return checkAacAdtsHeader(aac);}

上述代码片段为校验AAC的ADTS音频头,如果accDecoder非空表示之前已经判断过,该AAC数据为正常AAC数据,这里不考虑极端情况,AAC音频混搭其他格式的音频,这样会导致播放出问题,正常情况交互下也不会这样干!如果accDecoder为空则先对ADTS进行一次校验

    /*** check aac adts audio header** @param aac aac audio data*/private boolean checkAacAdtsHeader(byte[] aac) {byte[] dtsFixedHeader = new byte[2];System.arraycopy(aac, 0, dtsFixedHeader, 0, dtsFixedHeader.length);int bitMoveValue = dtsFixedHeader.length * 8 - ADTS_HEADER_START_FLAG_BIT_SIZE;int adtsFixedHeaderValue = bytesToInt(dtsFixedHeader);int syncwordValue = ADTS_HEADER_START_FLAG << bitMoveValue;boolean isAdtsHeader = (adtsFixedHeaderValue & syncwordValue) >> bitMoveValue == ADTS_HEADER_START_FLAG;if (!isAdtsHeader) {Log.e(TAG, "adts header start flag not match , so return!");return false;}System.arraycopy(aac, 2, dtsFixedHeader, 0, dtsFixedHeader.length);return parseAdtsHeaderKeyData(dtsFixedHeader);}

上述代码片段为取出AAC音频中的第0、1两个字节,因为short双字节转换成Int不会造成精度丢失,先进行数据拷贝,完后计算数据bit的左右移动值,然后将第0、1两个字节转换成int类型,经过位运算得到ADTS的syncword即AAC的ADTS固定标识,如匹配不上表示不是AAC数据直接return,否则接着处理第2、3两个字节然后将其拷贝到数组中,接着再进行ADTS音频头的关键数据的解析

    /*** parse adts header key byte array data** @param adtsHeaderValue adts fixed header byte array*/private boolean parseAdtsHeaderKeyData(byte[] adtsHeaderValue) {int adtsFixedHeaderValue = bytesToInt(adtsHeaderValue);// bitMoveValue = 16(2 * 8) - 2(aac profile 3bit)int bitMoveValue = adtsHeaderValue.length * 8 - ADTS_HEADER_PROFILE_BIT_SIZE;// profile : 01 (aac profile) 2 bitint audioProfile = adtsFixedHeaderValue & (ADTS_HEADER_PROFILE_FLAG << bitMoveValue);// 1: AAC Main -- MediaCodecInfo.CodecProfileLevel.AACObjectMain// 2: AAC LC (Low Complexity)  -- MediaCodecInfo.CodecProfileLevel.AACObjectLC// 3: AAC SSR (Scalable Sample Rate) -- MediaCodecInfo.CodecProfileLevel.AACObjectSSRaudioProfile = audioProfile >> bitMoveValue;// bitMoveValue = 16(2 * 8) - 2(aac profile 3bit) - 4(Sampling Frequency Index 4 bit)bitMoveValue -= ADTS_HEADER_SAMPLE_INDEX_BIT_SIZE;// Sampling Frequency Index : 1011 (value is 11,sample rate 8000) 4 bitint sampleIndex = adtsFixedHeaderValue & (ADTS_HEADER_SAMPLE_INDEX_FLAG << bitMoveValue);sampleIndex = sampleIndex >> bitMoveValue;sampleRate = samplingFrequencys[sampleIndex];// private bit :0 (encoding set 0 ,decoding ignore) 1 bit// Channel Configuration : 001 (Channel Configuration) 3 bit// bitMoveValue = bitMoveValue - 1(private bit 1bit) +  3(Channel Configuration 3bit)bitMoveValue -= (1 + ADTS_HEADER_CHANNEL_CONFIG_BIT_SIZE);channelConfig = adtsFixedHeaderValue & (ADTS_HEADER_SAMPLE_INDEX_FLAG << bitMoveValue);channelConfig = channelConfig >> bitMoveValue;// ......// create csd-0(audio adts header)adtsAudioHeader = new byte[2];adtsAudioHeader[0] = (byte) ((audioProfile << 3) | (sampleIndex >> 1));adtsAudioHeader[1] = (byte) ((byte) ((sampleIndex << 7) & 0x80) | (channelConfig << 3));Log.d(TAG, "audioProfile = " + audioProfile + " , sampleIndex = " + sampleIndex + "(" + sampleRate + ")" + " , channelConfig = " + channelConfig+ " , audio csd-0 = " + Utils.bytesToHexStringNo0xChar(adtsAudioHeader));return createDefaultDecoder();}

上述代码片段为先将前面取到的AAC的第2、3字节转换成int类型,接着计算数据位的位移值,然后分别计算audioProfile、sampleIndex、sampleRate、channelConfig,最后再根据这些参数中的部分参数进行音频csd-0配置头的计算,如果一切正常,最后会调用createDefaultDecoder方法进行解码器的创建

    /*** create default decoder*/private boolean createDefaultDecoder() {if (adtsAudioHeader == null || adtsAudioHeader.length == 0) {Log.e(TAG, "realtime aac decoder create failure , adts audio header is null , so return false!");return false;}try {aacDecoder = MediaCodec.createDecoderByType(AUDIO_DECODE_MIME_TYPE);MediaFormat mediaFormat = new MediaFormat();mediaFormat.setString(MediaFormat.KEY_MIME, AUDIO_DECODE_MIME_TYPE);mediaFormat.setInteger(MediaFormat.KEY_SAMPLE_RATE, sampleRate);mediaFormat.setInteger(MediaFormat.KEY_CHANNEL_COUNT, channelConfig);ByteBuffer byteBuffer = ByteBuffer.allocate(adtsAudioHeader.length);byteBuffer.put(adtsAudioHeader);byteBuffer.flip();mediaFormat.setByteBuffer(CSD_MIME_TYPE_0, byteBuffer);aacDecoder.configure(mediaFormat, null, null, 0);} catch (IOException e) {Log.e(TAG, "realtime aac decoder create failure , happened exception : " + e.toString());if (aacDecoder != null) {aacDecoder.stop();aacDecoder.release();}aacDecoder = null;}if (aacDecoder == null) {return false;}// initialization audio track , use for play pcm audio data// audio output channel param channelConfig according device support selectint buffsize = AudioTrack.getMinBufferSize(sampleRate, channelConfig, AudioFormat.ENCODING_PCM_16BIT);// author using channelConfig is AudioFormat.CHANNEL_OUT_MONOaudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, sampleRate, channelConfig,AudioFormat.ENCODING_PCM_16BIT, buffsize, AudioTrack.MODE_STREAM);audioTrack.play();aacDecoder.start();return true;}

上述代码片段为如果前面代码的音频csd-0配置头不合法则直接return,接着就是把之前步骤解析AAC音频头得到的参数设置到解码器当中,如发生异常则return false,否则创建一个AudioTrack进行解码音频数据后的播放,验证数据是否正常被解析

    /*** aac audio data decode** @param buf    aac audio data* @param length aac audio data length*/private void decode(byte[] buf, int length) {try {ByteBuffer[] codecInputBuffers = aacDecoder.getInputBuffers();ByteBuffer[] codecOutputBuffers = aacDecoder.getOutputBuffers();long kTimeOutUs = 0;int inputBufIndex = aacDecoder.dequeueInputBuffer(kTimeOutUs);if (inputBufIndex >= 0) {ByteBuffer dstBuf = codecInputBuffers[inputBufIndex];dstBuf.clear();dstBuf.put(buf, 0, length);aacDecoder.queueInputBuffer(inputBufIndex, 0, length, 0, 0);}ByteBuffer outputBuffer;MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();int outputBufferIndex = aacDecoder.dequeueOutputBuffer(info, kTimeOutUs);while (outputBufferIndex >= 0) {outputBuffer = codecOutputBuffers[outputBufferIndex];byte[] outData = new byte[info.size];outputBuffer.get(outData);outputBuffer.clear();if (audioTrack != null) {audioTrack.write(outData, 0, info.size);}aacDecoder.releaseOutputBuffer(outputBufferIndex, false);outputBufferIndex = aacDecoder.dequeueOutputBuffer(info, kTimeOutUs);}} catch (Exception e) {Log.e(TAG, "realtime aac decode happened exception : " + e.toString());}}

最后代码片段就是介绍MediaCodec的硬解码,这里就大概描述一下,因为跟本地音视频解码是一样的流程了,首先是获取输入输出缓冲区数组,然后将要解码的AAC音频数据填充到可用的输入缓冲区中,具体那个输入缓冲区可用,可以调用dequeueInputBuffer方法,该方法返回值大于等于0时表示该返回值对应的输入缓冲区数组的索引,然后就是解码了,从输出缓冲区数组中取得已经解码成功的输出缓冲区,可以调dequeueOutputBuffer方法获取它的有效索引,最后就是取出解码后的PCM数据进行播放,播放完毕后释放索引对应的输出缓冲区

七、Demo地址

AacDecoder

感谢信,致热爱编程的你

感谢各位粉丝、看管老爷们一直以来的支持和厚爱!以后作者出博客只出精品只出对大家有用的干货!让你在看博客的同时也能一起思考问题,从而达到边读博客边提升自身的知识软实力!在即将到来的新年,作者在此给你们提前拜一个早年了,祝大家新年快乐,完事如果,工作如意,身体健康!

实时AAC音频/本地AAC音视频硬解码详细介绍附带Demo相关推荐

  1. android硬编码封装mp4,【Android 音视频开发打怪升级:音视频硬解码篇】四、音视频解封和封装:生成一个MP4...

    [声 明] 首先,这一系列文章均基于自己的理解和实践,可能有不对的地方,欢迎大家指正. 其次,这是一个入门系列,涉及的知识也仅限于够用,深入的知识网上也有许许多多的博文供大家学习了. 最后,写文章过程 ...

  2. 【Android 音视频开发-音视频硬解码篇】1.音视频基础知识

    这是一个入门系列,涉及的知识也仅限于够用. 最后,写文章过程中,会借鉴参考其他人分享的文章,会在文章最后列出,感谢这些作者的分享. 本文你可以了解到 作为开篇的文章,我们先来看看音视频由什么构成的,以 ...

  3. 音视频硬解码篇——音视频基础知识

    时至今日,短视频App可谓是如日中天,一片兴兴向荣.随着短视频的兴起,音视频开发也越来越受到重视,但是由于音视频开发涉及知识面比较广,入门门槛相对较高,让许许多多开发者望而生畏. 一.视频是什么? 不 ...

  4. 【Android 音视频开发打怪升级:FFmpeg音视频编解码篇】一、FFmpeg so库编译

    转自:https://juejin.im/post/5e130df55188253a8b42ebac 目录 一.Android音视频硬解码篇: 1,音视频基础知识 2,音视频硬解码流程:封装基础解码框 ...

  5. 音视频编解码类开源项目的18个实时

    实时音视频的开发学习有很多可以参考的开源项目. 一个实时音视频应用共包括几个环节:采集.编码.前后处理.传输.解码.缓冲.渲染等很多环节.每一个细分环节,还有更细分的技术模块.比如,前后处理环节有美颜 ...

  6. 搬砖: web音频流转发之音视频直播

    web音频流转发之音频源 音频 javascript html5 2.1k 次阅读  ·  读完需要 16 分钟 前言 web音频流转发之音视频直播 web音频流转发之AudioNode app能直播 ...

  7. linux视音频解码教程,音视频编解码:NVIDIA Jetson Linux Multimedia API(总结)

    音视频编解码:NVIDIA Jetson Linux Multimedia API(总结) 2020年12月02日 | 萬仟网IT编程 | 我要评论 一.官网链接官网链接:https://docs.n ...

  8. 基于FFmpeg 实现RTSP, 音视频编解码,视频流添加文字,音视频合成MP4

    前言: 最近闲这没事,整理了一下之前开发过的音视频编解码库,主要基于ffmpeg,实现音视频的编解码.视频流添加文字,音视频同步到MP4等功能.有需要的小伙伴可以参考参考,如果写的有什么不对的地方,欢 ...

  9. 音视频编解码学习详解

    音视频编解码学习详解 目录(?)[+] 编解码学习笔记二codec类型 编解码学习笔记三Mpeg系列Mpeg 1和Mpeg 2 编解码学习笔记四Mpeg系列Mpeg 4 编解码学习笔记五Mpeg系列A ...

  10. 音视频编解码知识学习详解(分多部分进行详细分析)

    1. 常用的基本知识 基本概念 编解码 编解码器(codec)指的是一个能够对一个信号或者一个数据流进行变换的设备或者程序.这里指的变换既包括将信号或者数据流进行编码(通常是为了传输.存储或者加密)或 ...

最新文章

  1. 计算机会计课程试题及答案,计算机会计第2次作业_报表_附答案
  2. 操作系统导论中文版 pdf_一分钟带你认识微软操作系统 Windows 10
  3. 面试题:求最长非重复子序列
  4. 给网游写一个挂吧(四) – 调用游戏函数
  5. 【转】10分钟精通SharePoint - VS开发模板
  6. edit with idle 没反应_搬个家,猫咪不吃不喝甚至猝死?可能是你没做好“前戏”...
  7. 用ASP.Net写一个发送ICQ信息的程序
  8. asp.net电子影像相册_大连孕妈看过来 | 290元=孕中期四维排畸+孕妇写真+胎宝电子影集...
  9. python给ppt表格加边框_向ppt里插入图片和表格
  10. oracle 保留池,oracle的内存结构之--查看内存信息+保留池和循环池(摘自文平书)...
  11. 【嵌入式模块】DS1302 时钟定时芯片
  12. bat自动输入密码_如何给电脑文件夹设置密码?一学就会
  13. 【NLP】自然语言处理的语料库与词库
  14. 第79句 How Silicon Valley Puts the ‘Con’ in Consent硅谷的许可骗术
  15. java https pfx_使用HttpClient携带pfx证书调用HTTPS协议的WebService
  16. 牛客 BL1 扭蛋机
  17. Impala时间转换to_date、to_timestamp
  18. php怎么改北京时间,php如何设置北京时间
  19. 楼盘字典为什么能成为贝壳的超级护城河?
  20. (百度之星资格赛) 度度熊与邪恶大魔王 (dp)

热门文章

  1. 字节跳动Java实习面试题目大全
  2. 为 iPhone 制作 30s 以上的铃声
  3. Matlab coder生成C++代码
  4. 计算机应用个人职业发展规划简短,个人职业生涯规划
  5. 如何制作一个可控制的人体骨骼模型
  6. html快闪软件制作,快闪文字视频制作
  7. 怎样把计算机添加到网络打印机,怎么将电脑打印机设置成连接网络打印机
  8. 龙卷风路径_龙卷风接连来袭 我国哪些地方最易发生强龙卷?
  9. iTop-4412 SCP 精英版 linux-4.14.12 内核移植(2)
  10. 1.Linux系统编程入门