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代碼層面做這類轉換。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 原文:https://developer.android.com/reference/android/media/...
    thebestofrocky閱讀 6,118評論 0 6
  • 本篇文章是基于谷歌有關Graphic的一篇概覽文章的翻譯:http://source.android.com/de...
    lee_3do閱讀 7,177評論 2 21
  • 教程一:視頻截圖(Tutorial 01: Making Screencaps) 首先我們需要了解視頻文件的一些基...
    90后的思維閱讀 4,749評論 0 3
  • HTC(高通) 編碼端:zhanganl encode frame.width():360 frame.heig...
    ai___believe閱讀 2,607評論 0 3
  • 前兩天看了一個新聞,一個小學生給市長寫信,說看了林志玲的內衣廣告很沖動,希望不要再看到。后來娛樂記者還把這個新聞告...
    繼續海闊天空閱讀 573評論 0 2