博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
MediaCodec 解码后数据对齐导致的绿边问题
阅读量:6937 次
发布时间:2019-06-27

本文共 5051 字,大约阅读时间需要 16 分钟。

前言

本文从简书迁移,原文地址:

Android 使用 MediaCodec 解码 h264 数据后会有个数据对齐的问题。

简单说就是 MediaCodec 使用 GPU 进行解码,而解码后的输出数据是有一个对齐规则的,不同设备表现不一,如宽高都是 16 位对齐,或 32 位、64 位、128 位,当然也可能出现类似宽度以 128 位对齐而高度是 32 位对齐的情况。

例子

简单起见先画个 16 位对齐的:

假设需要解码的图像宽高为 15*15,在使用 16 位对齐的设备进行硬解码后,输出的 YUV 数据将会是 16*16 的,而多出来的宽高将自动填充。这时候如果按照 15*15 的大小取出 YUV 数据进行渲染,表现为花屏,而按照 16*16 的方式渲染,则出现绿边(如上图)。

怎么去除绿边呢?很简单,把原始图像抠出来就行了(废话)。

以上面为例子,分别取出 YUV 数据的话,可以这么做:

int width = 15, height = 15;int alignWidth = 16, alignHeight = 16;//假设 outData 是解码后对齐数据byte[] outData = new byte[alignWidth * alignHeight * 3 / 2];byte[] yData = new byte[width * height];byte[] uData = new byte[width * height / 4];byte[] vData = new byte[width * height / 4];yuvCopy(outData, 0, alignWidth, alignHeight, yData, width, height);yuvCopy(outData, alignWidth * alignHeight, alignWidth / 2, alignHeight / 2, uData, width / 2, height / 2);yuvCopy(outData, alignWidth * alignHeight * 5 / 4, alignWidth / 2, alignHeight / 2, vData, width / 2, height / 2);...private static void yuvCopy(byte[] src, int offset, int inWidth, int inHeight, byte[] dest, int outWidth, int outHeight) {    for (int h = 0; h < inHeight; h++) {        if (h < outHeight) {            System.arraycopy(src, offset + h * inWidth, dest, h * outWidth, outWidth);        }    }}复制代码

其实就是逐行抠出有效数据啦~

问题

那现在的问题就剩怎么知道解码后输出数据的宽高了。

起初我用华为荣耀note8做测试机,解码 1520*1520 后直接按照 1520*1520 的方式渲染是没问题的,包括解码后给的 buffer 大小也是 3465600(也就是 1520*1520*3/2)。

而当我使用OPPO R11,解码后的 buffer 大小则为 3538944(1536*1536*3/2),这时候再按照 1520*1520 的方式渲染的话,图像是这样的:

使用 yuvplayer 查看数据最终确定 1536*1536 方式渲染是没问题的,那么 1536 这个值在代码中怎么得到的呢?

我们可以拿到解码后的 buffer 大小,同时也知道宽高的对齐无非就是 16、32、64、128 这几个值,那很简单了,根据原来的宽高做对齐一个个找,如下(不着急,后面还有坑,这里先给出第一版解决方案):

align:for (int w = 16; w <= 128; w = w << 1) {    for (int h = 16; h <= w; h = h << 1) {        alignWidth = ((width - 1) / w + 1) * w;        alignHeight = ((height - 1) / h + 1) * h;        int size = alignWidth * alignHeight * 3 / 2;        if (size == bufferSize) {            break align;        }    }}复制代码

代码比较简单,大概就是从 16 位对齐开始一个个尝试,最终得到跟 bufferSize 相匹配的宽高。

当我屁颠屁颠的把 apk 发给老大之后,现实又无情地甩了我一巴掌,还好我在自己新买的手机上面调试了一下啊哈哈哈哈哈~

你以为华为的机子表现都是一样的吗?错了,我的华为mate9就不是酱紫的,它解出来的 buffer 大小是 3538944(1536*1536*3/2),而当我按照上面的方法得到 1536 这个值之后,渲染出来的图像跟上面的花屏差不多,谁能想到他按照 1520*1520 的方式渲染才是正常的。

这里得到结论:通过解码后 buffer 的 size 来确定对齐宽高的方法是不可靠的。

解决方案

就在我快绝望的时候,我在官方文档上发现这个(网上资料太少了,事实证明官方文档的资料才最可靠):

Accessing Raw Video ByteBuffers on Older Devices

Prior to LOLLIPOP and Image support, you need to use the KEY_STRIDE and KEY_SLICE_HEIGHT output format values to understand the layout of the raw output buffers.

Note that on some devices the slice-height is advertised as 0. This could mean either that the slice-height is the same as the frame height, or that the slice-height is the frame height aligned to some value (usually a power of 2). Unfortunately, there is no standard and simple way to tell the actual slice height in this case. Furthermore, the vertical stride of the U plane in planar formats is also not specified or defined, though usually it is half of the slice height.

大致就是使用 KEY_STRIDEKEY_SLICE_HEIGHT 可以得到原始输出 buffer 的对齐后的宽高,但在某些设备上可能会获得 0,这种情况下要么它跟图像的值相等,要么就是对齐后的某值。

OK,那么当 KEY_STRIDEKEY_SLICE_HEIGHT 能拿到数据的时候我们使用他们,拿不到的时候再用第一个解决方案:

//视频宽高,如果存在裁剪范围的话,宽等于右边减左边坐标,高等于底部减顶部width = format.getInteger(MediaFormat.KEY_WIDTH);if (format.containsKey("crop-left") && format.containsKey("crop-right")) {    width = format.getInteger("crop-right") + 1 - format.getInteger("crop-left");}height = format.getInteger(MediaFormat.KEY_HEIGHT);if (format.containsKey("crop-top") && format.containsKey("crop-bottom")) {    height = format.getInteger("crop-bottom") + 1 - format.getInteger("crop-top");}//解码后数据对齐的宽高,在有些设备上会返回0int keyStride = format.getInteger(MediaFormat.KEY_STRIDE);int keyStrideHeight = format.getInteger(MediaFormat.KEY_SLICE_HEIGHT);// 当对齐后高度返回0的时候,分两种情况,如果对齐后宽度有给值,// 则只需要计算高度从16字节对齐到128字节对齐这几种情况下哪个值跟对齐后宽度相乘再乘3/2等于对齐后大小,// 如果计算不出则默认等于视频宽高。// 当对齐后宽度也返回0,这时候也要对宽度做对齐处理,原理同上alignWidth = keyStride;alignHeight = keyStrideHeight;if (alignHeight == 0) {    if (alignWidth == 0) {        align:        for (int w = 16; w <= 128; w = w << 1) {            for (int h = 16; h <= w; h = h << 1) {                alignWidth = ((videoWidth - 1) / w + 1) * w;                alignHeight = ((videoHeight - 1) / h + 1) * h;                int size = alignWidth * alignHeight * 3 / 2;                if (size == bufferSize) {                    break align;                }            }        }    } else {        for (int h = 16; h <= 128; h = h << 1) {            alignHeight = ((videoHeight - 1) / h + 1) * h;            int size = alignWidth * alignHeight * 3 / 2;            if (size == bufferSize) {                break;            }        }    }    int size = alignWidth * alignHeight * 3 / 2;    if (size != bufferSize) {        alignWidth = videoWidth;        alignHeight = videoHeight;    }}int size = videoWidth * videoHeight * 3 / 2;if (size == bufferSize) {    alignWidth = videoWidth;    alignHeight = videoHeight;} 复制代码

最后说两句

文中只提供了个人处理的思路,实际使用的时候,还要考虑颜色格式以及效率的问题,个人不建议在java代码层面做这类转换。

转载地址:http://mfbnl.baihongyu.com/

你可能感兴趣的文章
kafka查看topic
查看>>
清理SQL Server服务器名称列表
查看>>
[吴恩达机器学习笔记]14降维1-2降维的应用数据压缩与数据可视化
查看>>
Python:线程同步
查看>>
bootstrap
查看>>
CI框架主题切换的功能
查看>>
P4971 断罪者
查看>>
bzoj 1834 [ZJOI2010] network 网络扩容(费用流)
查看>>
lua学习笔记
查看>>
MapReduce中的Join算法
查看>>
Deep Q-Network 学习笔记(六)—— 改进④:dueling dqn
查看>>
topcoder srm 490 div1
查看>>
并发与并行
查看>>
拷贝控制示例
查看>>
会议08
查看>>
第一节:python读取excel文件
查看>>
vbox导入虚拟电脑网卡MAC问题
查看>>
Java知多少(83)面板基础:JPanel和JScrollPane
查看>>
Prolog学习:数独和八皇后问题
查看>>
最长上升子序列(LIS) Medium2
查看>>