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

Posted Engineer-Jsp

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了实时AAC音频/本地AAC音视频硬解码详细介绍附带Demo相关的知识,希望对你有一定的参考价值。

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

一、使用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)

序号字段位数含义
Asyncword12AAC音频头固定起始标记
BMPEG Version10是MPEG-4,1是MPEG-2
CLayer2always 0
Dprotection absent1没有CRC设置为1,如果有CRC则设置为0
Eprofile2the MPEG-4 Audio Object Type minus 1
FSampling Frequency Index4MPEG-4 Sampling Frequency Index(15 is forbidden)
Gprivate bit1编码时设置为0,解码时忽略
HChannel Configuration3MPEG-4 Channel Configuration (in the case of 0, the channel configuration is sent via an inband PCE)
Ioriginality1编码时设置为0,解码时忽略
Jhome1编码时设置为0,解码时忽略
Kcopyrighted id bit1编码时设置为0,解码时忽略
Lcopyright id start1编码时设置为0,解码时忽略
Mframe length13此值必须包含7或9个字节的标头长度:FrameLength = header(protection absent == 1?7:9)+ size(AAC Frame Length)
OBuffer fullness11No Description
Prdbs2ADTS帧中的AAC帧数(RDBs)减去1,为获得最大兼容性,请始终为每个ADTS帧使用1个AAC帧
Qcrc16如果 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-0csd-1sps视频头和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 path
            mMediaExtractor.setDataSource(videoPath);
            // get source file track count
            int trackCount = mMediaExtractor.getTrackCount();
            for (int i = 0; i < trackCount; i++) 
                // get current media track media format
                MediaFormat mediaFormat = mMediaExtractor.getTrackFormat(i);
                // if media format object not be null
                if (mediaFormat != null) 
                    // get media mime type
                    String mimeType = mediaFormat.getString(MediaFormat.KEY_MIME);
                    // media mime type match audio
                    if (mimeType.startsWith(AUDIO_MIME_TYE)) 
                        // set media track is audio
                        mMediaExtractor.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 sample
                        sampleRate = mediaFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE);
                        // get audio channel count
                        channelCount = 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范围内给出,大家可以了解一下,作者也将其添加上来了,只不过是在代码中注释了,为的就是给大家拓展一下这方面的知识

    @Override
    public 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;
    

上述代码片段为创建音频解码器,sampleRatechannelCountadtsAudioHeader都是前面代码片段中通过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 select
        int 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 -1
            if (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音频硬解码的代码片段

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

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

    @Override
    public 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(TAGandroid音视频音频硬编解码pcm&aac&wav

RTSP实时音视频(H264/H265/AAC)开发实战项目

解码aac,并生成wav文件

iOS平台上音频编码成aac

多媒体开发(13):iOS上音频编码成aac

直播-拉流和推流概述 转载