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 工作原理

解码代码

初始化

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

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

指定帧格式

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容