在 Android 4.1 版本提供了 MediaCodec 接口来访问设备的编解码器,不一样于 FFmpeg 的软件编解码,它采用的是硬件编解码能力,所以在速度上会比软解更具备优点,可是因为 Android 的碎片化问题,机型众多,版本各异,致使 MediaCodec 在机型兼容性上须要花精力去适配,而且编解码流程不可控,全交由厂商的底层硬件去实现,最终获得的视频质量不必定很理想。git
虽然 MediaCodec 仍然存在必定的弊端,可是对于快速实现编解码需求,仍是很值得参考的。github
以将相机预览的 YUV 数据编码成 H264 视频流为例来解析 MediaCodec 的使用。面试
下图展现了 MediaCodec 的工做方式,一个典型的生产者消费者模型,两边的 Client 分别表明输入端和输出端,输入端将数据交给 MediaCodec 进行编码或者解码,而输出端就获得编码或者解码后的内容。ide
输入端和输出端是经过输入队列缓冲区和输出队列缓冲区,两条缓冲区队列的形式来和 MediaCodec 传递数据。编码
首先从输入队列中出队获得一个可用的缓冲区,将它填满数据以后,再将缓冲区入队,交由 MediaCodec 去处理。spa
MediaCodec 处理完了以后,再从输出队列中出队获得一个可用的缓冲区,这个缓冲里面的数据就是编码或者解码后的数据了,把这些数据进行相应的处理以后,还须要释放这个缓冲区,让它回到队列中去,可供下一次使用。线程
另外,MediaCodec 也存在相应的 生命周期,以下图所示:code
当建立了 MediaCodec 以后,是处于未初始化的 Uninitialized
状态,调用 configure 方法以后就处于 Configured
状态,调用了 start 方法以后,就处于 Executing
状态。orm
在 Executing
状态下开始处理数据,它又有三个子状态,分别是:视频
当一调用 start 方法以后,就进入了 Flushed
状态,从输入缓冲区队列中取出一个缓冲区就进入了 Running
状态,当入队的缓冲区带有 EOS
标志时, 就会切换到 End of Stream
状态, MediaCodec 再也不接受入队的缓冲区,可是仍然会对已入队的且没有进行编解码操做的缓冲区进行操做、输出,直到输出的缓冲区带有 EOS
标志,表示编解码操做完成了。
在 Executing
状态下能够调用 flush 方法,使 MediaCodec 切换到 Flushed
状态。
在 Executing
状态下能够调用 stop 方法,使 MediaCodec 切换到 Uninitialized
状态,而后再次调用 configure 方法进入 Configured
状态。另外,当调用 reset 方法也会进入到 Uninitialized
状态。
当再也不须要 MediaCodec 时,调用 release 方法将它释放掉,进入 Released
状态。
当 MediaCodec 工做发生异常时,会进入到 Error
状态,此时仍是能够经过 reset 方法恢复过来,进入 Uninitialized
状态。
理解了 MediaCodec 的生命周期和工做流程以后,就能够上手来进行编码工做了。
以 MediaCodec 同步调用为例,使用过程以下:
// 建立 MediaCodec,此时是 Uninitialized 状态 MediaCodec codec = MediaCodec.createByCodecName(name); // 调用 configure 进入 Configured 状态 codec.configure(format, …); MediaFormat outputFormat = codec.getOutputFormat(); // option B // 调用 start 进入 Executing 状态,开始编解码工做 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 } } // 调用 stop 方法进入 Uninitialized 状态 codec.stop(); // 调用 release 方法释放,结束操做 codec.release();
首先须要建立并设置好 MediaFormat 对象,它表示媒体数据格式的相关信息,对于视频主要有如下信息要设置:
其中,码率就是指单位传输时间传送的数据位数,通常用 kbps
即千位每秒来表示。而帧率就是指每秒显示的帧数。
其实对于码率有三种模式能够控制:
对于颜色格式,因为是将 YUV 数据编码成 H264,而 YUV 格式又有不少,这又涉及到机型兼容性问题。在对相机编码时要作好格式的处理,好比相机使用的是 NV21
格式,MediaFormat 使用的是 COLOR_FormatYUV420SemiPlanar
,也就是 NV12
模式,那么就得作一个转换,把 NV21
转换到 NV12
。
对于 I 帧间隔,也就是隔多久出现一个 H264 编码中的 I 帧。
完整 MediaFormat 设置示例:
MediaFormat mediaFormat = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, width, height); mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar); // 马率 mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, width * height * 5); // 调整码率的控流模式 mediaFormat.setInteger(MediaFormat.KEY_BITRATE_MODE, MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR); // 设置帧率 mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 30); // 设置 I 帧间隔 mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
当开始编解码操做时,开启编解码线程,处理相机预览返回的 YUV 数据。
在这里用到了相机的一个封装库:
https://github.com/glumes/EzC...
编解码操做代码以下:
while (isEncoding) { // YUV 颜色格式转换 if (!mEncodeDataQueue.isEmpty()) { input = mEncodeDataQueue.poll(); byte[] yuv420sp = new byte[mWidth * mHeight * 3 / 2]; NV21ToNV12(input, yuv420sp, mWidth, mHeight); input = yuv420sp; } if (input != null) { try { // 从输入缓冲区队列中拿到可用缓冲区,填充数据,再入队 ByteBuffer[] inputBuffers = mMediaCodec.getInputBuffers(); ByteBuffer[] outputBuffers = mMediaCodec.getOutputBuffers(); int inputBufferIndex = mMediaCodec.dequeueInputBuffer(-1); if (inputBufferIndex >= 0) { // 计算时间戳 pts = computePresentationTime(generateIndex); ByteBuffer inputBuffer = inputBuffers[inputBufferIndex]; inputBuffer.clear(); inputBuffer.put(input); mMediaCodec.queueInputBuffer(inputBufferIndex, 0, input.length, pts, 0); generateIndex += 1; } MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); int outputBufferIndex = mMediaCodec.dequeueOutputBuffer(bufferInfo, TIMEOUT_USEC); // 从输出缓冲区队列中拿到编码好的内容,对内容进行相应处理后在释放 while (outputBufferIndex >= 0) { ByteBuffer outputBuffer = outputBuffers[outputBufferIndex]; byte[] outData = new byte[bufferInfo.size]; outputBuffer.get(outData); // flags 利用位操做,定义的 flag 都是 2 的倍数 if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) { // 配置相关的内容,也就是 SPS,PPS mOutputStream.write(outData, 0, outData.length); } else if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0) { // 关键帧 mOutputStream.write(outData, 0, outData.length); } else { // 非关键帧和SPS、PPS,直接写入文件,多是B帧或者P帧 mOutputStream.write(outData, 0, outData.length); } mMediaCodec.releaseOutputBuffer(outputBufferIndex, false); outputBufferIndex = mMediaCodec.dequeueOutputBuffer(bufferInfo, TIMEOUT_USEC); } } catch (IOException e) { Log.e(TAG, e.getMessage()); } } else { try { Thread.sleep(500); } catch (InterruptedException e) { Log.e(TAG, e.getMessage()); } } }
首先,要把要把相机的 NV21
格式转换成 NV12
格式,而后 经过 dequeueInputBuffer
方法去从可用的输入缓冲区队列中出队取出缓冲区,填充完数据后再经过 queueInputBuffer
方法入队。
dequeueInputBuffer
返回缓冲区索引,若是索引小于 0 ,则表示当前没有可用的缓冲区。它的参数 timeoutUs
表示超时时间 ,毕竟用的是 MediaCodec 的同步模式,若是没有可用缓冲区,就会阻塞指定参数时间,若是参数为负数,则会一直阻塞下去。
queueInputBuffer
方法将数据入队时,除了要传递出队时的索引值,而后还须要传入当前缓冲区的时间戳 presentationTimeUs
和当前缓冲区的一个标识 flag
。
其中,时间戳一般是缓冲区渲染的时间,而标识则有多种标识,标识当前缓冲区属于那种类型:
在编码的时候能够计算当前缓冲区的时间戳,也能够直接传递 0 就行了,对于标识也能够直接传递 0 做为参数。
把数据传入给 MediaCodec 以后,经过 dequeueOutputBuffer
方法取出编解码后的数据,除了指定超时时间外,还须要传入 MediaCodec.BufferInfo
对象,这个对象里面有着编码后数据的长度、偏移量以及标识符。
取出 MediaCodec.BufferInfo
内的数据以后,根据不一样的标识符进行不一样的操做:
00 00 00 01 67
和 00 00 00 01 68
开头的数据,这个数据是必需要有的,它里面有着视频的宽、高信息。00 00 00 01 65
的数据,对于返回的 flags ,不符合预约义的标识,则能够直接写入,那些数据可能表明的是 H264 中的 P 帧 或者 B 帧。
对于编解码后的数据,进行操做后,经过 releaseOutputBuffer
方法释放对应的缓冲区,其中第二个参数 render
表明是否要渲染到 surface 上,这里暂时不须要就为 false 。
当想要中止编码时,经过 MediaCodec 的 stop
方法切换到 Uninitialized
状态,而后再调用 release
方法释放掉。
这里并无采用使用 BUFFER_FLAG_END_OF_STREAM
标识符的方式来中止编码,而是直接切换状态了,在经过 Surface 方式进行录制时,再去采用这种方式了。
对于 MediaCodec 硬编码解析之相机内容编码成 H264 文件就到这里了,主要仍是讲述了关于 MediaCodec 的使用,一旦熟悉使用了,完成编码工做也就很简单了。
阿里P6P7【安卓】进阶资料分享+加薪跳槽必备面试题