MediaCodec简介
MediaCodec是Android提供的硬件编解码器,它可以利用设备的硬件来完成编解码,从而大大提高编解码的效率,还可以降低电量的使用。
MediaCodec通常与MediaExtractor、MediaMuxer、AudioTrack结合使用,能够编解码诸如H.264、H.265、AAC、3gp等常见的音视频格式。广义而言,MediaCodec的工作原理就是处理输入数据以产生输出数据。具体来说,MediaCodec在编解码的过程中使用了一组输入/输出缓存区来同步或异步处理数据:
首先,客户端向输入缓存区写入要编解码的数据并将其提交给编解码器,待编解码器处理完毕后转存到编码器的输出缓存区,同时收回客户端对输入缓存区的所有权;
然后,客户端从输出缓存区读取编码好的数据进行处理,待处理完毕后编解码器收回客户端对输出缓存区的所有权。
不断重复整个过程,直至编码器停止工作或者异常退出。
工作原理图如下:
image.png
MediaCodec的工作状态
在整个编解码过程中,MediaCodec的使用会经历配置、启动、数据处理、停止、释放几个过程。相应的状态可归纳为停止(Stopped)、执行(Executing)、以及释放(Released)三个状态,而Stopped状态又可细分为未初始化(Uninitialized)、配置(Configured)、异常( Error),Executing状态也可细分为读写数据(Flushed)、运行(Running)和流结束(End-of-Stream)。MediaCodec整个状态结构图如下:
image.png
从上图可知,当MediaCodec被创建后会进入未初始化状态,待设置好配置信息并调用start()启动后,MediaCodec会进入运行状态,并且可进行数据读写操作。如果在这个过程中出现了错误,MediaCodec会进入Stopped状态,可以使用reset方法来重置编解码器,否则MediaCodec所持有的资源最终会被释放。当然,如果MediaCodec正常使用完毕,我们也可以向编解码器发送EOS指令,同时调用stop和release方法终止编解码器的使用。
MediaCodec API 说明
主要API说明:
getInputBuffers:获取需要编码数据的输入流队列,返回的是一个ByteBuffer数组
queueInputBuffer:在指定索引处填充一定范围的输入缓冲区后,将其提交给组件
dequeueInputBuffer:返回要填充有效数据的输入缓冲区的索引
getOutputBuffers:获取编解码之后的数据输出流队列,返回的是一个ByteBuffer数组
dequeueOutputBuffer:使输出缓冲区出队列,最多阻塞"timeoutUs"微秒。
releaseOutputBuffer:处理完成,释放ByteBuffer数据
MediaCodec的基本使用
- 创建并配置一个 MediaCodec 对象
- 如果输入缓冲区就绪,读取一个输入块,并复制到输入缓冲区中
- 如果输出缓冲区就绪,复制输出缓冲区的数据
- 循环2.3步直到完成
- 释放 MediaCodec 对象
MediaCodec初始化
MediaCodec主要提供了createEncoderByType(String type)
、createDecoderByType(String type)
两个方法来创建编解码器,它们均需要传入一个MIME类型多媒体格式。常见的MIME类型多媒体格式如下:
● "video/x-vnd.on2.vp8" - VP8 video (i.e. video in .webm)
● "video/x-vnd.on2.vp9" - VP9 video (i.e. video in .webm)
● "video/avc" - H.264/AVC video
● "video/mp4v-es" - MPEG4 video
● "video/3gpp" - H.263 video
● "audio/3gpp" - AMR narrowband audio
● "audio/amr-wb" - AMR wideband audio
● "audio/mpeg" - MPEG1/2 audio layer III
● "audio/mp4a-latm" - AAC audio (note, this is raw AAC packets, not packaged in LATM!)
● "audio/vorbis" - vorbis audio
● "audio/g711-alaw" - G.711 alaw audio
● "audio/g711-mlaw" - G.711 ulaw audio
MediaCodec还提供了一个createByCodecName (String name)
方法,支持使用组件的具体名称来创建编解码器。但是该方法使用起来有些麻烦,且官方是建议最好是配合MediaCodecList使用,因为MediaCodecList记录了所有可用的编解码器。
MediaCodec配置和启动
编解码器配置使用的是MediaCodec的configure方法,该方法首先对MediaFormat存储的数据map进行提取,然后调用本地方法native-configure实现对编解码器的配置工作。
在配置时,configure(format, surface, crypto, flags)
方法需要传入format、surface、crypto、flags参数。
- format为MediaFormat的实例,它使用"key-value"键值对的形式存储多媒体数据格式信息;
- surface用于指明解码器的数据源来自于该surface;
- crypto用于指定一个MediaCrypto对象,以便对媒体数据进行安全解密;
- flags指明配置的是编码器(CONFIGURE_FLAG_ENCODE)。
其中MediaFormat必须配置以下几项,否则运行configure
出错:采样率,比特率,通道个数。
下面是编码为AAC的配置
MediaFormat mediaFormat = MediaFormat.createAudioFormat(MINE_TYPE, SAMPLE_RATE, CHANNEL_CONFIG_IN); MediaFormat mediaFormat = MediaFormat.createAudioFormat(MINE_TYPE, SAMPLE_RATE, CHANNEL_CONFIG_IN); //指定比特率 mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, 128 * 1024); //指定采样率 mediaFormat.setInteger(MediaFormat.KEY_SAMPLE_RATE, SAMPLE_RATE); //指定通道个数 mediaFormat.setInteger(MediaFormat.KEY_CHANNEL_COUNT, CHANNEL_CONFIG_IN); //指定PROFILE mediaFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectERLC); //指定缓冲区最大长度 mediaFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 10 * 1024); //应用配置 mMediaCodecEncoder.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
当编解码器配置完毕后,就可以调用MediaCodec的start()方法,该方法会调用低层native_start()方法来启动编码器,并调用低层方法ByteBuffer[] getBuffers(input)来开辟一系列输入、输出缓存区。
可以看一下start方法的源码,开辟了一块输入和一块输出缓存区:
public final void start() { native_start(); synchronized(mBufferLock) { cacheBuffers(true /* input */); cacheBuffers(false /* input */); } }
MediaCodec数据处理
MediaCodec支持两种模式编解码器,即同步synchronous、异步asynchronous。
- 同步模式是指编解码器数据的输入和输出是同步的,编解码器只有处理输出完毕才会再次接收输入数据;
- 异步编解码器数据的输入和输出是异步的,编解码器不会等待输出数据处理完毕才再次接收输入数据。
我们主要介绍下同步编解码,因为这种方式我们用得比较多。我们知道当编解码器被启动后,每个编解码器都会拥有一组输入和输出缓存区,但是这些缓存区暂时无法被使用,只有通过MediaCodec的dequeueInputBuffer
和dequeueOutputBuffer
方法获取输入输出缓存区授权,通过返回的ID来操作这些缓存区。
示例如下:
ByteBuffer[] inputBuffers = mediaCodec.getInputBuffers(); //所有的输入缓冲区 ByteBuffer[]outputBuffers = mediaCodec.getOutputBuffers();//所有的输出缓冲区 //注:一次编码,只使用一个缓冲区,所以需要获取缓冲区的索引 //获取可用的输入缓冲区索引 int inputBufferIndex = mediaCodec.dequeueInputBuffer(-1); if (inputBufferIndex >= 0) { //写原始数据到输入缓冲区 ByteBuffer inputBuffer = inputBuffers[inputBufferIndex]; inputBuffer.clear(); inputBuffer.put(data); mediaCodec.queueInputBuffer(inputBufferIndex, 0, len, System.nanoTime(), 0); } MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); //获取可用输出缓冲区的索引 int outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, 0); while (outputBufferIndex >= 0) { //循环读取完输出缓冲区的数据 ByteBuffer outputBuffer = outputBuffers[outputBufferIndex]; if (outputAACDelegate != null) { int outPacketSize = bufferInfo.size + 7;// 7为ADTS头部的大小 outputBuffer.position(bufferInfo.offset); outputBuffer.limit(bufferInfo.offset + bufferInfo.size); byte[] outData = new byte[outPacketSize]; addADTStoPacket(outData, outPacketSize);//添加ADTS 代码后面会贴上 outputBuffer.get(outData, 7, bufferInfo.size);//将编码得到的AAC数据 取出到byte[]中 偏移量offset=7 outputBuffer.position(bufferInfo.offset); outputAACDelegate.outputAACPacket(outData); //写入到文件 } mediaCodec.releaseOutputBuffer(outputBufferIndex, false); outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, 0); }
释放资源
mediaCodec.stop(); mediaCodec.release();