1. 简介

H.264是比较多开发者使用较多的一种数字视频压缩格式,主要用于直播流的传输与视频网站的视频流传输,也有不少开发者开始使用H.265进行视频压缩,性能较H.264提升较大。本篇文章着重介绍使用MediaCodec硬件H.264裸字节流数据的实现方式,有关于更多H.264的介绍可以查看参考文章中H.264的结构介绍。

2.使用MediaCodec硬解码

2.1 MediaCodec介绍

  • 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 来提供各种音视频编解码功能,由参数配置来决定采用何种编解码算法、是否采用硬件编解码加速等。

2.2 MediaCodec 工作流程

 

Android音视频--H.264视频流解码_移动开发

 

 

编解码器处理输入数据并产生输出数据,MediaCodec 使用输入输出缓存,异步处理数据。简要地说,一般的处理步骤如下

  • 请求一个空的输入 input buffer
  • 填入数据、并将其交给 MediaCodec
  • MediaCodec 处理数据后,将处理后的数据放在一个空的 output buffer
  • 获取填充数据了的 output buffer,得到其中的数据,然后将其返还给 MediaCodec

2.3 MediaCodec API 说明

MediaCodec可以处理具体的视频流,主要有这几个方法:

  • configure:配置为编码器start:成功地配置组件后,调用start方法。
  • getInputBuffers:获取需要编码数据的输入流队列,返回的是一个ByteBuffer数组
  • queueInputBuffer:输入流入队列dequeueInputBuffer:从输入流队列中取数据进行编码操作
  • getOutputBuffers:获取编解码之后的数据输出流队列,返回的是一个ByteBuffer数组
  • dequeueOutputBuffer:从输出队列中取出编码操作之后的数据
  • releaseOutputBuffer:处理完成,释放ByteBuffer数据
  • stop:完成解码/编码任务后,需注意的是codec任然处于活跃状态且准备重新start。
  • flush:冲洗组件的输入和输出端口release:释放codec实例使用的资源。
  • reset:使codec返回到初始(未初始化)状态。

2.4 Talk is cheap, Show me the code

初始化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();
}
}