本文从简书迁移,原文地址:www.jianshu.com/p/1ff123409…html
由于项目中须要对解码后的 YUV420P 格式数据作一些处理,在以前是使用 ffmpeg 软解的方式获得 YUV420P,但随着图像像素的提高,ffmpeg 的效率已经影响到软件的体验了,故使用 Android 上 MediaCodec 硬解的方式提升效率。java
参考 MediaCodec 的官方文档:android
In broad terms, a codec processes input data to generate output data. It processes data asynchronously and uses a set of input and output buffers. At a simplistic level, you request (or receive) an empty input buffer, fill it up with data and send it to the codec for processing. The codec uses up the data and transforms it into one of its empty output buffers. Finally, you request (or receive) a filled output buffer, consume its contents and release it back to the codec.数组
意思是,MediaCodec 采用异步的方式处理数据,并使用一组输入和输出缓冲区。开发者在使用的时候经过请求一个空的输入缓冲区,往其中填充数据以后放回编解码器中,编解码器处理完输入数据后将处理结果输出到一个空的输出缓冲区中。开发者经过请求输出缓存区使用完其内容后,将其释放回编解码器:缓存
private static final long DEFAULT_TIMEOUT_US = 1000 * 10;
private static final String MIME_TYPE = "video/avc";
private static final int VIDEO_WIDTH = 1520;
private static final int VIDEO_HEIGHT = 1520;
private MediaCodec mCodec;
private MediaCodec.BufferInfo bufferInfo;
public void initCodec() {
try {
mCodec = MediaCodec.createDecoderByType(MIME_TYPE);
} catch (IOException e) {
e.printStackTrace();
}
bufferInfo = new MediaCodec.BufferInfo();
MediaFormat mediaFormat = MediaFormat.createVideoFormat(MIME_TYPE, VIDEO_WIDTH, VIDEO_HEIGHT);
mCodec.configure(mediaFormat, null, null, 0);
mCodec.start();
}
public void release() {
if (null != mCodec) {
mCodec.stop();
mCodec.release();
mCodec = null;
}
}
复制代码
public void decode(byte[] h264Data) {
int inputBufferIndex = mCodec.dequeueInputBuffer(DEFAULT_TIMEOUT_US);
if (inputBufferIndex >= 0) {
ByteBuffer inputBuffer;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
inputBuffer = mCodec.getInputBuffer(inputBufferIndex);
} else {
inputBuffer = mCodec.getInputBuffers()[inputBufferIndex];
}
if (inputBuffer != null) {
inputBuffer.clear();
inputBuffer.put(h264Data, 0, h264Data.length);
mCodec.queueInputBuffer(inputBufferIndex, 0, h264Data.length, 0, 0);
}
}
int outputBufferIndex = mCodec.dequeueOutputBuffer(bufferInfo, DEFAULT_TIMEOUT_US);
ByteBuffer outputBuffer;
while (outputBufferIndex > 0) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
outputBuffer = mCodec.getOutputBuffer(outputBufferIndex);
} else {
outputBuffer = mCodec.getOutputBuffers()[outputBufferIndex];
}
if (outputBuffer != null) {
outputBuffer.position(0);
outputBuffer.limit(bufferInfo.offset + bufferInfo.size);
byte[] yuvData = new byte[outputBuffer.remaining()];
outputBuffer.get(yuvData);
if (null!=onDecodeCallback) {
onDecodeCallback.onFrame(yuvData);
}
mCodec.releaseOutputBuffer(outputBufferIndex, false);
outputBuffer.clear();
}
outputBufferIndex = mCodec.dequeueOutputBuffer(bufferInfo, DEFAULT_TIMEOUT_US);
}
}
复制代码
public interface OnDecoderCallback {
void onFrame(byte[] yuvData);
}
复制代码
有几个要注意的地方:异步
在实际的测试过程当中发现各家厂商的 Android 设备 MediaCodec 解码获得的 YUV 数据格式不尽相同,例如在个人测试机(某一不知名品牌的平板)上解码获得的是标准的 YUV420P 格式,而在另外一台测试机(华为荣耀note8)上解码获得的倒是 NV12 格式:async
参考 Android: MediaCodec视频文件硬件解码,高效率获得YUV格式帧,快速保存JPEG图片 得知 API 21 新加入了MediaCodec的全部硬件解码都支持的 COLOR_FormatYUV420Flexible
格式。它并非一种肯定的 YUV420 格式,而是包含了 COLOR_FormatYUV411Planar
、COLOR_FormatYUV411PackedPlanar
、COLOR_FormatYUV420Planar
、COLOR_FormatYUV420PackedPlanar
,COLOR_FormatYUV420SemiPlanar
和 COLOR_FormatYUV420PackedSemiPlanar
这几种,因此只能确保解码后的帧格式是这几种中的其中一种。MediaCodecInfo 源码中能够看到,在API 21引入 YUV420Flexible 的同时,它所包含的这些格式都 deprecated 掉了:ide
指定帧格式只须要在配置 MediaCodec 以前指定就能够了,在上面的 initCodec 中更新以下:测试
MediaFormat mediaFormat = MediaFormat.createVideoFormat(MIME_TYPE, width, height);
mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible);
mCodec.configure(mediaFormat, null, null, 0);
mCodec.start();
复制代码
可能因为我这边测试机较少的问题,我在使用的时候不指定帧格式也能达到一样的效果。ui
既然将解码后的帧格式锁定为上面说到的几种,那离获得标准的 YUV420P 格式帧就只有一步之遥了。
能够经过 mCodec.getOutputFormat().getInteger(MediaFormat.KEY_COLOR_FORMAT)
获得解码获得的帧格式,这里获得的就是 COLOR_FORMATYUV411PLANAR
、COLOR_FORMATYUV411PACKEDPLANAR
、COLOR_FORMATYUV420PLANAR
、COLOR_FORMATYUV420PACKEDPLANAR
,COLOR_FORMATYUV420SEMIPLANAR
和 COLOR_FORMATYUV420PACKEDSEMIPLANAR
中的其中一个,接下来只须要把对应的类型转化成标准的 YUV420P 数据就 OK 了:
MediaFormat mediaFormat = mCodec.getOutputFormat();
switch (mediaFormat.getInteger(MediaFormat.KEY_COLOR_FORMAT)) {
case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV411Planar:
break;
case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV411PackedPlanar:
break;
case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedPlanar:
break;
case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar:
yuvData = yuv420spToYuv420P(yuvData, mediaFormat.getInteger(MediaFormat.KEY_WIDTH), mediaFormat.getInteger(MediaFormat.KEY_HEIGHT));
break;
case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedSemiPlanar:
break;
case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar:
default:
break;
}
复制代码
附上 yuv420sp 转 Yuv420P 的方法:
private static byte[] yuv420spToYuv420P(byte[] yuv420spData, int width, int height) {
byte[] yuv420pData = new byte[width * height * 3 / 2];
int ySize = width * height;
System.arraycopy(yuv420spData, 0, yuv420pData, 0, ySize); //拷贝 Y 份量
for (int j = 0, i = 0; j < ySize / 2; j += 2, i++) {
yuv420pData[ySize + i] = yuv420spData[ySize + j]; //U 份量
yuv420pData[ySize * 5 / 4 + i] = yuv420spData[ySize + j + 1]; //V 份量
}
return yuv420pData;
}
复制代码
本文给出 java 层面转换思路,但实际使用的时候建议在 native 层转换,或者使用时直接兼容不一样 YUV 格式,毕竟多这一步转换,对效率仍是会有比较大的影响的。
使用 MediaCodec 以后解码速度确实快了许多,但在差一些的设备(例如我那不知名品牌的平板)上面,硬解的表现明显的低于软解。目前来看,网上众多评价说的硬解有坑的说法仍是有道理的,但即使有坑,这解码速度仍是让我欲罢不能啊~