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

前言

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");
}

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

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

推荐阅读更多精彩内容

  • 原文:https://developer.android.com/reference/android/media/...
    thebestofrocky阅读 6,066评论 0 6
  • 本篇文章是基于谷歌有关Graphic的一篇概览文章的翻译:http://source.android.com/de...
    lee_3do阅读 7,117评论 2 21
  • 教程一:视频截图(Tutorial 01: Making Screencaps) 首先我们需要了解视频文件的一些基...
    90后的思维阅读 4,693评论 0 3
  • HTC(高通) 编码端:zhanganl encode frame.width():360 frame.heig...
    ai___believe阅读 2,548评论 0 3
  • 前两天看了一个新闻,一个小学生给市长写信,说看了林志玲的内衣广告很冲动,希望不要再看到。后来娱乐记者还把这个新闻告...
    继续海阔天空阅读 552评论 0 2