MediaCodec类Android提供的用于访问低层多媒体编/解码器接口,它是Android低层多媒体架构的一部分,通常与MediaExtractor、MediaMuxer、AudioTrack结合使用,能够编解码诸如H.264、H.265、AAC、3gp等常见的音视频格式。
广义而言,MediaCodec的工作原理就是处理输入数据以产生输出数据。具体来说,MediaCodec在编解码的过程中使用了一组输入/输出缓存区来同步或异步处理数据:首先,客户端向获取到的编解码器输入缓存区写入要编解码的数据并将其提交给编解码器,待编解码器处理完毕后将其转存到编码器的输出缓存区,同时收回客户端对输入缓存区的所有权;然后,客户端从获取到编解码输出缓存区读取编码好的数据进行处理,待处理完毕后编解码器收回客户端对输出缓存区的所有权。不断重复整个过程,直至编码器停止工作或者异常退出。
mediacodec的作用是处理输入的数据生成输出数据。首先生成一个输入数据缓冲区,将数据填入缓冲区提供给codec,codec会采用异步的方式处理这些输入的数据,然后将填满输出缓冲区提供给消费者,消费者消费完后将缓冲区返还给codec。
二,MediaCodec编码过程在整个编解码过程中,MediaCodec的使用会经历配置、启动、数据处理、停止、释放几个过程,相应的状态可归纳为停止(Stopped),执行(Executing)以及释放(Released)三个状态,而Stopped状态又可细分为未初始化(Uninitialized)、配置(Configured)、异常( Error),Executing状态也可细分为读写数据(Flushed)、运行(Running)和流结束(End-of-Stream)。
MediaCodec整个状态结构图如下:
???从上图可知,当MediaCodec被创建后会进入未初始化状态,待设置好配置信息并调用start()启动后,MediaCodec会进入运行状态,并且可进行数据读写操作。如果在这个过程中出现了错误,MediaCodec会进入Stopped状态,我们就是要使用reset方法来重置编解码器,否则MediaCodec所持有的资源最终会被释放。当然,如果MediaCodec正常使用完毕,我们也可以向编解码器发送EOS指令,同时调用stop和release方法终止编解码器的使用。
三,MediaCodec API 说明MediaCodec可以处理具体的视频流,主要有这几个方法:
- ?getInputBuffers:获取需要编码数据的输入流队列,返回的是一个ByteBuffer数组 ?queueInputBuffer:输入流入队列 ?dequeueInputBuffer:从输入流队列中取数据进行编码操作 ?getOutputBuffers:获取编解码之后的数据输出流队列,返回的是一个ByteBuffer数组 ?dequeueOutputBuffer:从输出队列中取出编码操作之后的数据 ?releaseOutputBuffer:处理完成,释放ByteBuffer数据
所有的同步模式的 MediaCodec API都遵循一个模式:
创建并配置一个 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记录了所有可用的编解码器。当然,我们也可以使用该类对传入的minmeType参数进行判断,以匹配出MediaCodec对该mineType类型的编解码器是否支持。
以指定MIME类型为“video/avc”为例,代码如下:
private static MediaCodecInfo selectCodec(String mimeType) { ????// 获取所有支持编解码器数量 ????int numCodecs = MediaCodecList.getCodecCount(); ????for (int i = 0; i < numCodecs; i++) { ???????// 编解码器相关性信息存储在MediaCodecInfo中 ????????MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(i); ????????// 判断是否为编码器 ????????if (!codecInfo.isEncoder()) { ????????????continue; ????????} ???????// 获取编码器支持的MIME类型,并进行匹配 ????????String[] types = codecInfo.getSupportedTypes(); ????????for (int j = 0; j < types.length; j++) { ????????????if (types[j].equalsIgnoreCase(mimeType)) { ????????????????return codecInfo; ????????????} ????????} ????} ????return null; }
(2) 配置、启动编/解码器 编解码器配置使用的是MediaCodec的configure方法,该方法首先对MediaFormat存储的数据map进行提取,然后调用本地方法native-configure实现对编解码器的配置工作。在配置时,configure方法需要传入format、surface、crypto、flags参数,其中format为MediaFormat的实例,它使用”key-value”键值对的形式存储多媒体数据格式信息;surface用于指明解码器的数据源来自于该surface;crypto用于指定一个MediaCrypto对象,以便对媒体数据进行安全解密;flags指明配置的是编码器(CONFIGURE_FLAG_ENCODE)。
MediaFormat mFormat = MediaFormat.createVideoFormat("video/avc", 640 ,480); ????// 创建MediaFormatmFormat.setInteger(MediaFormat.KEY_BIT_RATE,600); ??????// 指定比特率mFormat.setInteger(MediaFormat.KEY_frame_RATE,30); ?// 指定帧率mFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT,mColorFormat); ?// 指定编码器颜色格式 ?mFormat.setInteger(MediaFormat.KEY_I_frame_INTERVAL,10); // 指定关键帧时间间隔mVideoEncodec.configure(mFormat,null,null,MediaCodec.CONFIGURE_FLAG_ENCODE);
以上代码是在编码H.264时的配置方法,createVideoFormat(“video/avc”, 640 ,480)为”video/avc”类型(即H.264)编码器的MediaFormat对象,需要指定视频数据的宽高,如果编解码音频数据,则调用MediaFormat的createAudioFormat(String mime, int sampleRate,int channelCount)的方法。除了一些诸如视频帧率、音频采样率等配置参数,这里需要着重讲解一下
MediaFormat.KEY_COLOR_FORMAT配置属性,该属性用于指明video编码器的颜色格式,具体选择哪种颜色格式与输入的视频数据源颜色格式有关。比如,我们都知道Camera预览采集的图像流通常为NV21或YV12,那么编码器需要指定相应的颜色格式,否则编码得到的数据可能会出现花屏、叠影、颜色失真等现象。MediaCodecInfo.CodecCapabilities.存储了编码器所有支持的颜色格式,常见颜色格式映射如下:
原始数据 编码器 NV12(YUV420sp) ———> COLOR_FormatYUV420PackedSemiPlanar NV21 ———-> COLOR_FormatYUV420SemiPlanar YV12(I420) ———-> COLOR_FormatYUV420Planar
当编解码器配置完毕后,就可以调用MediaCodec的start()方法,该方法会调用低层native_start()方法来启动编码器,并调用低层方法ByteBuffer[] getBuffers(input)来开辟一系列输入、输出缓存区。
start()方法源码如下:
public final void start() { ???????native_start(); ???????synchronized(mBufferLock) { ???????????cacheBuffers(true ); ???????????cacheBuffers(false ); ???????} }
(3) 数据处理 MediaCodec支持两种模式编解码器,即同步synchronous、异步asynchronous,所谓同步模式是指编解码器数据的输入和输出是同步的,编解码器只有处理输出完毕才会再次接收输入数据;而异步编解码器数据的输入和输出是异步的,编解码器不会等待输出数据处理完毕才再次接收输入数据。这里,我们主要介绍下同步编解码,因为这种方式我们用得比较多。我们知道当编解码器被启动后,每个编解码器都会拥有一组输入和输出缓存区,但是这些缓存区暂时无法被使用,只有通过MediaCodec的
dequeueInputBuffer/dequeueOutputBuffer方法获取输入输出缓存区授权,通过返回的ID来操作这些缓存区。下面我们通过一段官方提供的代码,进行扩展分析:
MediaCodec codec = MediaCodec.createByCodecName(name); codec.configure(format, …); MediaFormat outputFormat = codec.getOutputFormat(); // option B codec.start(); for (;;) { ??int inputBufferId = codec.dequeueInputBuffer(timeoutUs); ??if (inputBufferId >= 0) { ????ByteBuffer inputBuffer = codec.getInputBuffer(…); ????// fill inputBuffer with valid data ????… ????codec.queueInputBuffer(inputBufferId, …); ??} ??int outputBufferId = codec.dequeueOutputBuffer(…); ??if (outputBufferId >= 0) { ????ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId); ????MediaFormat bufferFormat = codec.getOutputFormat(outputBufferId); // option A ????// bufferFormat is identical to outputFormat ????// outputBuffer is ready to be processed or rendered. ????… ????codec.releaseOutputBuffer(outputBufferId, …); ??} else if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { ????// Subsequent data will conform to new format. ????// Can ignore if using getOutputFormat(outputBufferId) ????outputFormat = codec.getOutputFormat(); // option B ??} } codec.stop(); codec.release();
从上面代码可知,当编解码器start后,会进入一个for(;;)循环,该循环是一个死循环,以实现不断地去从编解码器的输入缓存池中获取包含数据的一个缓存区,然后再从输出缓存池中获取编解码好的输出数据。