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

栏目: IOS · Android · 发布时间: 5年前

内容简介:本文从简书迁移,原文地址:因为项目中需要对解码后的 YUV420P 格式数据做一些处理,在之前是使用 ffmpeg 软解的方式得到 YUV420P,但随着图像像素的提升,ffmpeg 的效率已经影响到软件的体验了,故使用 Android 上 MediaCodec 硬解的方式提高效率。参考

本文从简书迁移,原文地址: www.jianshu.com/p/1ff123409…

因为项目中需要对解码后的 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 格式帧

解码代码

初始化

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 格式帧

参考 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 格式帧

指定帧格式

指定帧格式只需要在配置 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 之后解码速度确实快了许多,但在差一些的设备(例如我那不知名品牌的平板)上面,硬解的表现明显的低于软解。目前来看,网上众多评价说的硬解有坑的说法还是有道理的,但即便有坑,这解码速度还是让我欲罢不能啊~


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

创业之初你不可不知的融资知识

创业之初你不可不知的融资知识

桂曙光 / 机械工业出版社 / 2010-6-1 / 48.00元

从零到精通 成功融资必读书 像小说一样好看的趣味融资书 手把手教你找到VC拿到钱 本书以创业者寻找风险投资的逻辑顺序为主线,运用理论分析和实例剖析相结合的手法,将简洁、通俗的语言与丰富的图表工具相结合,辅以中肯的建议,同时运用大量鲜活的、有代表性的成败案例,为读者解读创业之初企业有效成功融资的途径和方法,帮助你的企业开创新的辉煌。一起来看看 《创业之初你不可不知的融资知识》 这本书的介绍吧!

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器

html转js在线工具
html转js在线工具

html转js在线工具