前言
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_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代码层面做这类转换。