MediaCodec 高效解码得到标准 YUV420P 格式帧

前言

因为项目中需要对解码后的 YUV420P 格式数据做一些处理,在之前是使用 ffmpeg 软解的方式得到 YUV420P,但随着图像像素的提升,ffmpeg 的效率已经影响到软件的体验了,故使用 Android 上 MediaCodec 硬解的方式提高效率。

概述

参考 MediaCodec 的官方文档:

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 采用异步的方式处理数据,并使用一组输入和输出缓冲区。开发者在使用的时候通过请求一个空的输入缓冲区,往其中填充数据之后放回编解码器中,编解码器处理完输入数据后将处理结果输出到一个空的输出缓冲区中。开发者通过请求输出缓存区使用完其内容后,将其释放回编解码器:

MediaCodec 高效解码得到标准 YUV420P 格式帧_第1张图片
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);
}

有几个要注意的地方:

  1. 使用 dequeueInputBuffer(long timeoutUs) 请求一个输入缓冲区,timeoutUs 为等待时间,单位微秒,设置为-1代表无限等待,这里不建议设置为-1,在有些机器上会一直阻塞;返回的整型变量为请求到的输入缓冲区的 index。
  2. getInputBuffers() 得到的是输入缓冲区数组,通过 index 可以得到当前请求到的输入缓冲区,在使用之前要 clear 一下,避免之前的数据造成影响。
  3. 同理,dequeueOutputBuffer(BufferInfo info, long timeoutUs) 用于请求一个装载输出数据的输出缓冲区的 index,BufferInfo 用于储存输出缓冲区的信息
  4. 注意一定要调用 releaseOutputBuffer(int index, boolean render) 释放缓冲区;如果你配置编解码器的时候指定一个有效的 surface 时,将 render 设置为 true 将首先把缓冲区发送到 surface 渲染,这里单纯为了得到 YUV 数据,不做渲染,直接设置为 false。

使用时遇到的问题

在实际的测试过程中发现各家厂商的 Android 设备 MediaCodec 解码得到的 YUV 数据格式不尽相同,例如在我的测试机(某一不知名品牌的平板)上解码得到的是标准的 YUV420P 格式,而在另一台测试机(华为荣耀note8)上解码得到的却是 NV12 格式:

MediaCodec 高效解码得到标准 YUV420P 格式帧_第2张图片

参考 Android: MediaCodec视频文件硬件解码,高效率得到YUV格式帧,快速保存JPEG图片 得知 API 21 新加入了MediaCodec的所有硬件解码都支持的 COLOR_FormatYUV420Flexible 格式。它并不是一种确定的 YUV420 格式,而是包含了 COLOR_FormatYUV411PlanarCOLOR_FormatYUV411PackedPlanarCOLOR_FormatYUV420PlanarCOLOR_FormatYUV420PackedPlanar,COLOR_FormatYUV420SemiPlanarCOLOR_FormatYUV420PackedSemiPlanar 这几种,所以只能确保解码后的帧格式是这几种中的其中一种。MediaCodecInfo 源码中可以看到,在API 21引入 YUV420Flexible 的同时,它所包含的这些格式都 deprecated 掉了:

MediaCodec 高效解码得到标准 YUV420P 格式帧_第3张图片

指定帧格式

指定帧格式只需要在配置 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();

可能由于我这边测试机较少的问题,我在使用的时候不指定帧格式也能达到同样的效果。

得到 YUV420P 格式帧

既然将解码后的帧格式锁定为上面说到的几种,那离得到标准的 YUV420P 格式帧就只有一步之遥了。

可以通过 mCodec.getOutputFormat().getInteger(MediaFormat.KEY_COLOR_FORMAT) 得到解码得到的帧格式,这里得到的就是 COLOR_FORMATYUV411PLANARCOLOR_FORMATYUV411PACKEDPLANARCOLOR_FORMATYUV420PLANARCOLOR_FORMATYUV420PACKEDPLANAR,COLOR_FORMATYUV420SEMIPLANARCOLOR_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 之后解码速度确实快了许多,但在差一些的设备(例如我那不知名品牌的平板)上面,硬解的表现明显的低于软解。目前来看,网上众多评价说的硬解有坑的说法还是有道理的,但即便有坑,这解码速度还是让我欲罢不能啊~

你可能感兴趣的:(MediaCodec 高效解码得到标准 YUV420P 格式帧)