前言
Android 使用 MediaCodec 解碼 h264 數(shù)據(jù)后會有個數(shù)據(jù)對齊的問題。
簡單說就是 MediaCodec 使用 GPU 進行解碼,而解碼后的輸出數(shù)據(jù)是有一個對齊規(guī)則的外驱,不同設(shè)備表現(xiàn)不一,如寬高都是 16 位對齊援奢,或 32 位驻债、64 位挚躯、128 位强衡,當(dāng)然也可能出現(xiàn)類似寬度以 128 位對齊而高度是 32 位對齊的情況。
例子
簡單起見先畫個 16 位對齊的:
假設(shè)需要解碼的圖像寬高為 15*15码荔,在使用 16 位對齊的設(shè)備進行硬解碼后漩勤,輸出的 YUV 數(shù)據(jù)將會是 16*16 的感挥,而多出來的寬高將自動填充。這時候如果按照 15*15 的大小取出 YUV 數(shù)據(jù)進行渲染越败,表現(xiàn)為花屏触幼,而按照 16*16 的方式渲染,則出現(xiàn)綠邊(如上圖)究飞。
怎么去除綠邊呢置谦?很簡單,把原始圖像摳出來就行了(廢話)亿傅。
以上面為例子媒峡,分別取出 YUV 數(shù)據(jù)的話,可以這么做:
int width = 15, height = 15;
int alignWidth = 16, alignHeight = 16;
//假設(shè) outData 是解碼后對齊數(shù)據(jù)
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);
}
}
}
其實就是逐行摳出有效數(shù)據(jù)啦~
問題
那現(xiàn)在的問題就剩怎么知道解碼后輸出數(shù)據(jù)的寬高了葵擎。
起初我用華為榮耀note8做測試機谅阿,解碼 1520*1520 后直接按照 1520*1520 的方式渲染是沒問題的,包括解碼后給的 buffer 大小也是 3465600(也就是 1520*1520*3/2)酬滤。
而當(dāng)我使用OPPO R11签餐,解碼后的 buffer 大小則為 3538944(1536*1536*3/2),這時候再按照 1520*1520 的方式渲染的話盯串,圖像是這樣的:
使用 yuvplayer 查看數(shù)據(jù)最終確定 1536*1536 方式渲染是沒問題的氯檐,那么 1536 這個值在代碼中怎么得到的呢?
我們可以拿到解碼后的 buffer 大小嘴脾,同時也知道寬高的對齊無非就是 16男摧、32、64译打、128 這幾個值,那很簡單了拇颅,根據(jù)原來的寬高做對齊一個個找奏司,如下(不著急,后面還有坑樟插,這里先給出第一版解決方案):
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 相匹配的寬高黄锤。
當(dāng)我屁顛屁顛的把 apk 發(fā)給老大之后搪缨,現(xiàn)實又無情地甩了我一巴掌,還好我在自己新買的手機上面調(diào)試了一下啊哈哈哈哈哈~
你以為華為的機子表現(xiàn)都是一樣的嗎鸵熟?錯了副编,我的華為mate9就不是醬紫的,它解出來的 buffer 大小是 3538944(1536*1536*3/2)流强,而當(dāng)我按照上面的方法得到 1536 這個值之后痹届,渲染出來的圖像跟上面的花屏差不多呻待,誰能想到他按照 1520*1520 的方式渲染才是正常的。
這里得到結(jié)論:通過解碼后 buffer 的 size 來確定對齊寬高的方法是不可靠的队腐。
解決方案
就在我快絕望的時候蚕捉,我在官方文檔上發(fā)現(xiàn)這個(網(wǎng)上資料太少了,事實證明官方文檔的資料才最可靠):
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 的對齊后的寬高柴淘,但在某些設(shè)備上可能會獲得 0迫淹,這種情況下要么它跟圖像的值相等,要么就是對齊后的某值为严。
OK千绪,那么當(dāng) KEY_STRIDE
和 KEY_SLICE_HEIGHT
能拿到數(shù)據(jù)的時候我們使用他們,拿不到的時候再用第一個解決方案:
//視頻寬高梗脾,如果存在裁剪范圍的話荸型,寬等于右邊減左邊坐標(biāo),高等于底部減頂部
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");
}
//解碼后數(shù)據(jù)對齊的寬高炸茧,在有些設(shè)備上會返回0
int keyStride = format.getInteger(MediaFormat.KEY_STRIDE);
int keyStrideHeight = format.getInteger(MediaFormat.KEY_SLICE_HEIGHT);
// 當(dāng)對齊后高度返回0的時候瑞妇,分兩種情況,如果對齊后寬度有給值梭冠,
// 則只需要計算高度從16字節(jié)對齊到128字節(jié)對齊這幾種情況下哪個值跟對齊后寬度相乘再乘3/2等于對齊后大小辕狰,
// 如果計算不出則默認等于視頻寬高。
// 當(dāng)對齊后寬度也返回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代碼層面做這類轉(zhuǎn)換。