MediaCodec 解码后数据对齐导致的绿边问题

06/19 08:30
阅读数 143

前言

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

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

例子

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

假设需要解码的图像宽高为 1515,在使用 16 位对齐的设备进行硬解码后,输出的 YUV 数据将会是 1616 的,而多出来的宽高将自动填充。

这时候如果按照 1515 的大小取出 YUV 数据进行渲染,表现为花屏,而按照 1616 的方式渲染,则出现绿边(如上图)。

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

以上面为例子,分别取出 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做测试机,解码 1520x1520 后直接按照 1520x1520 的方式渲染是没问题的,包括解码后给的 buffer 大小也是 3465600(也就是 152015203/2)。

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

使用 yuvplayer 查看数据最终确定 1536x1536 方式渲染是没问题的,那么 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(153615363/2),而当我按照上面的方法得到 1536 这个值之后,渲染出来的图像跟上面的花屏差不多,谁能想到他按照 1520x1520 的方式渲染才是正常的。

这里得到结论:通过解码后 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_STRIDE 和 KEY_SLICE_HEIGHT 可以得到原始输出 buffer 的对齐后的宽高,但在某些设备上可能会获得 0,这种情况下要么它跟图像的值相等,要么就是对齐后的某值。

OK,那么当 KEY_STRIDE 和 KEY_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");
}

//解码后数据对齐的宽高,在有些设备上会返回0
int 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代码层面做这类转换。

作者:超兽

原文:https://www.jianshu.com/p/ac53e9595940


技术交流,欢迎加我微信:ezglumes ,拉你入技术交流群。


推荐阅读:

移动端技术交流喊你入群啦~~~

推荐几个堪称教科书级别的 Android 音视频入门项目

OpenGL ES 学习资源分享

OpenGL 实现视频编辑中的转场效果

喜欢就点个 「在看」  ▽

本文分享自微信公众号 - 音视频开发进阶(glumes_blog)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

展开阅读全文
打赏
0
0 收藏
分享
加载中
更多评论
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部