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

前言

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

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 格式:async

参考 Android: MediaCodec视频文件硬件解码,高效率获得YUV格式帧,快速保存JPEG图片 得知 API 21 新加入了MediaCodec的全部硬件解码都支持的 COLOR_FormatYUV420Flexible 格式。它并非一种肯定的 YUV420 格式,而是包含了 COLOR_FormatYUV411PlanarCOLOR_FormatYUV411PackedPlanarCOLOR_FormatYUV420PlanarCOLOR_FormatYUV420PackedPlanar,COLOR_FormatYUV420SemiPlanarCOLOR_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 格式帧

既然将解码后的帧格式锁定为上面说到的几种,那离获得标准的 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 以后解码速度确实快了许多,但在差一些的设备(例如我那不知名品牌的平板)上面,硬解的表现明显的低于软解。目前来看,网上众多评价说的硬解有坑的说法仍是有道理的,但即使有坑,这解码速度仍是让我欲罢不能啊~

相关文章
相关标签/搜索