H.264是比较多开发者使用较多的一种数字视频压缩格式,主要用于直播流的传输与视频网站的视频流传输,也有不少开发者开始使用H.265进行视频压缩,性能较H.264提升较大。本篇文章着重介绍使用MediaCodec硬件H.264裸字节流数据的实现方式,有关于更多H.264的介绍可以查看参考文章中H.264的结构介绍。
MediaCodec类Android提供的用于访问低层多媒体编/解码器接口,它是Android低层多媒体架构的一部分,通常与MediaExtractor、MediaMuxer、AudioTrack结合使用,能够编解码诸如H.264、H.265、AAC、3gp等常见的音视频格式。
Android 底层多媒体模块采用的是 OpenMax 框架,任何 Android 底层编解码模块的实现,都必须遵循 OpenMax 标准。Google 官方默认提供了一系列的软件编解码器:包括:OMX.google.h264.encoder,OMX.google.h264.encoder, OMX.google.aac.encoder, OMX.google.aac.decoder 等等,而硬件编解码功能,则需要由芯片厂商依照 OpenMax 框架标准来完成,所以,一般采用不同芯片型号的手机,硬件编解码的实现和性能是不同的。
Android 应用层统一由 MediaCodec API 来提供各种音视频编解码功能,由参数配置来决定采用何种编解码算法、是否采用硬件编解码加速等。
编解码器处理输入数据并产生输出数据,MediaCodec 使用输入输出缓存,异步处理数据。简要地说,一般的处理步骤如下
请求一个空的输入 input buffer填入数据、并将其交给 MediaCodecMediaCodec 处理数据后,将处理后的数据放在一个空的 output buffer获取填充数据了的 output buffer,得到其中的数据,然后将其返还给 MediaCodecMediaCodec可以处理具体的视频流,主要有这几个方法:
configure:配置为编码器start:成功地配置组件后,调用start方法。getInputBuffers:获取需要编码数据的输入流队列,返回的是一个ByteBuffer数组queueInputBuffer:输入流入队列dequeueInputBuffer:从输入流队列中取数据进行编码操作getOutputBuffers:获取编解码之后的数据输出流队列,返回的是一个ByteBuffer数组dequeueOutputBuffer:从输出队列中取出编码操作之后的数据releaseOutputBuffer:处理完成,释放ByteBuffer数据stop:完成解码/编码任务后,需注意的是codec任然处于活跃状态且准备重新start。flush:冲洗组件的输入和输出端口release:释放codec实例使用的资源。reset:使codec返回到初始(未初始化)状态。初始化MediaCodec
/** * 视频类型 */ private final static String MIME_TYPE = "video/avc"; /** * 初始化播放 */ private void initVideo(SurfaceHolder holder) { try { // 初始化MediaCodec,方法有两种,分别是通过名称和类型来创建 // 这里使用通过类型来创建 mMediaCodec = MediaCodec.createDecoderByType(MIME_TYPE); // 获取视频的宽高 mVideoHeight = holder.getSurfaceFrame().width(); mVideoWidth = holder.getSurfaceFrame().height(); // MediaFormat,这个类包含了比特率、帧率、关键帧间隔时间等,其中比特率如果太低就会造成类似马赛克的现象。 mMediaFormat = MediaFormat.createVideoFormat(MIME_TYPE, 1080, 1920); // 设置比特率 mMediaFormat.setInteger(KEY_BIT_RATE, mVideoHeight * mVideoWidth * 5); // 设置帧率 mMediaFormat.setInteger(KEY_FRAME_RATE, 30); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { // 描述编码器要使用的所需比特率模式的键 // BITRATE_MODE_CQ: 表示完全不控制码率,尽最大可能保证图像质量 //BITRATE_MODE_CBR: 表示编码器会尽量把输出码率控制为设定值 //BITRATE_MODE_VBR: 表示编码器会根据图像内容的复杂度(实际上是帧间变化量的大小)来动态调整输出码率,图像复杂则码率高,图像简单则码率低; mMediaFormat.setInteger(MediaFormat.KEY_BITRATE_MODE, MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR); } mMediaFormat.setInteger(KEY_I_FRAME_INTERVAL, 1); byte[] headerSps = {0, 0, 0, 1, 103, 66, 0, 41, -115, -115, 64, 80, 30, -48, 15, 8, -124, 83, -128}; byte[] headerPps = {0, 0, 0, 1, 104, -54, 67, -56}; mMediaFormat.setByteBuffer("csd-0", ByteBuffer.wrap(headerSps)); mMediaFormat.setByteBuffer("csd-1", ByteBuffer.wrap(headerPps)); mMediaCodec.configure(mMediaFormat, holder.getSurface(), null, 0); mMediaCodec.start(); } catch (IOException e) { e.printStackTrace(); } }视频解码部分代码
将接收到或从文件读取到的byte[]传入onFrame中
/** * 解码数据并显示视频 * buf 视频数据组 * offset 数据偏移量 * length 有效长度 */ private void onFrame(byte[] buf, int offset, int length) { try { ByteBuffer[] inputBuffers = mMediaCodec.getInputBuffers(); int inputBufferIndex = mMediaCodec.dequeueInputBuffer(0); if (inputBufferIndex >= 0) { ByteBuffer inputBuffer = inputBuffers[inputBufferIndex]; inputBuffer.clear(); inputBuffer.put(buf, offset, length); mMediaCodec.queueInputBuffer(inputBufferIndex, 0, length, mCount * 30, 0); mCount++; } MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); int outputBufferIndex = mMediaCodec.dequeueOutputBuffer(bufferInfo, 0); while (outputBufferIndex >= 0) { mMediaCodec.releaseOutputBuffer(outputBufferIndex, true); outputBufferIndex = mMediaCodec.dequeueOutputBuffer(bufferInfo, 0); if (!isPlayingSound) { mHandler.postDelayed(() -> isPlayingSound = true, 1000); } } } catch (Throwable t) { t.printStackTrace(); } }
