MediaCodec 解碼後數據對齊致使的綠邊問題

前言

本文從簡書遷移,原文地址:www.jianshu.com/p/ac53e9595…java

Android 使用 MediaCodec 解碼 h264 數據後會有個數據對齊的問題。ide

簡單說就是 MediaCodec 使用 GPU 進行解碼,而解碼後的輸出數據是有一個對齊規則的,不一樣設備表現不一,如寬高都是 16 位對齊,或 32 位、64 位、128 位,固然也可能出現相似寬度以 128 位對齊而高度是 32 位對齊的狀況。測試

例子

簡單起見先畫個 16 位對齊的:this

假設須要解碼的圖像寬高爲 15*15,在使用 16 位對齊的設備進行硬解碼後,輸出的 YUV 數據將會是 16*16 的,而多出來的寬高將自動填充。這時候若是按照 15*15 的大小取出 YUV 數據進行渲染,表現爲花屏,而按照 16*16 的方式渲染,則出現綠邊(如上圖)。spa

怎麼去除綠邊呢?很簡單,把原始圖像摳出來就好了(廢話)。調試

以上面爲例子,分別取出 YUV 數據的話,能夠這麼作:code

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);
        }
    }
}
複製代碼

其實就是逐行摳出有效數據啦~orm

問題

那如今的問題就剩怎麼知道解碼後輸出數據的寬高了。cdn

起初我用華爲榮耀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 大小,同時也知道寬高的對齊無非就是 1六、3二、6四、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;
}&emsp;
複製代碼

最後說兩句

文中只提供了我的處理的思路,實際使用的時候,還要考慮顏色格式以及效率的問題,我的不建議在java代碼層面作這類轉換。

相關文章
相關標籤/搜索