MediaCodec 高效解碼得到標準 YUV420P 格式幀

Snail_YC發表於2019-03-01

前言

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

因為專案中需要對解碼後的 YUV420P 格式資料做一些處理,在之前是使用 ffmpeg 軟解的方式得到 YUV420P,但隨著影像畫素的提升,ffmpeg 的效率已經影響到軟體的體驗了,故使用 Android 上 MediaCodec 硬解的方式提高效率。

概述

參考 MediaCodec 的官方文件

In broad terms, a codec processes input data to generate output data. It processes data asynchronously and uses a set of input and output buffers. At a simplistic level, you request (or receive) an empty input buffer, fill it up with data and send it to the codec for processing. The codec uses up the data and transforms it into one of its empty output buffers. Finally, you request (or receive) a filled output buffer, consume its contents and release it back to the codec.

意思是,MediaCodec 採用非同步的方式處理資料,並使用一組輸入和輸出緩衝區。開發者在使用的時候通過請求一個空的輸入緩衝區,往其中填充資料之後放回編解碼器中,編解碼器處理完輸入資料後將處理結果輸出到一個空的輸出緩衝區中。開發者通過請求輸出快取區使用完其內容後,將其釋放回編解碼器:

MediaCodec 工作原理

解碼程式碼

初始化

private static final long DEFAULT_TIMEOUT_US = 1000 * 10;
private static final String MIME_TYPE = "video/avc";
private static final int VIDEO_WIDTH = 1520;
private static final int VIDEO_HEIGHT = 1520;

private MediaCodec mCodec;
private MediaCodec.BufferInfo bufferInfo;

public void initCodec() {
    try {
        mCodec = MediaCodec.createDecoderByType(MIME_TYPE);
    } catch (IOException e) {
        e.printStackTrace();
    }
    bufferInfo = new MediaCodec.BufferInfo();
    MediaFormat mediaFormat = MediaFormat.createVideoFormat(MIME_TYPE, VIDEO_WIDTH, VIDEO_HEIGHT);
    mCodec.configure(mediaFormat, null, null, 0);
    mCodec.start();
}
public void release() {
    if (null != mCodec) {
        mCodec.stop();
        mCodec.release();
        mCodec = null;
    }
}
複製程式碼

解碼

public void decode(byte[] h264Data) {
    int inputBufferIndex = mCodec.dequeueInputBuffer(DEFAULT_TIMEOUT_US);
    if (inputBufferIndex >= 0) {
        ByteBuffer inputBuffer;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            inputBuffer = mCodec.getInputBuffer(inputBufferIndex);
        } else {
            inputBuffer = mCodec.getInputBuffers()[inputBufferIndex];
        }
        if (inputBuffer != null) {
            inputBuffer.clear();
            inputBuffer.put(h264Data, 0, h264Data.length);
            mCodec.queueInputBuffer(inputBufferIndex, 0, h264Data.length, 0, 0);
        }
    }
    int outputBufferIndex = mCodec.dequeueOutputBuffer(bufferInfo, DEFAULT_TIMEOUT_US);
    ByteBuffer outputBuffer;
    while (outputBufferIndex > 0) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            outputBuffer = mCodec.getOutputBuffer(outputBufferIndex);
        } else {
            outputBuffer = mCodec.getOutputBuffers()[outputBufferIndex];
        }
        if (outputBuffer != null) {
            outputBuffer.position(0);
            outputBuffer.limit(bufferInfo.offset + bufferInfo.size);
            byte[] yuvData = new byte[outputBuffer.remaining()];
            outputBuffer.get(yuvData);

            if (null!=onDecodeCallback) {
                onDecodeCallback.onFrame(yuvData);
            }
            mCodec.releaseOutputBuffer(outputBufferIndex, false);
            outputBuffer.clear();
        }
        outputBufferIndex = mCodec.dequeueOutputBuffer(bufferInfo, DEFAULT_TIMEOUT_US);
    }
}
複製程式碼

解碼回撥介面

public interface OnDecoderCallback {
    void onFrame(byte[] yuvData);
}
複製程式碼

有幾個要注意的地方:

  1. 使用 dequeueInputBuffer(long timeoutUs) 請求一個輸入緩衝區,timeoutUs 為等待時間,單位微秒,設定為-1代表無限等待,這裡不建議設定為-1,在有些機器上會一直阻塞;返回的整型變數為請求到的輸入緩衝區的 index。
  2. getInputBuffers() 得到的是輸入緩衝區陣列,通過 index 可以得到當前請求到的輸入緩衝區,在使用之前要 clear 一下,避免之前的資料造成影響。
  3. 同理,dequeueOutputBuffer(BufferInfo info, long timeoutUs) 用於請求一個裝載輸出資料的輸出緩衝區的 index,BufferInfo 用於儲存輸出緩衝區的資訊
  4. 注意一定要呼叫 releaseOutputBuffer(int index, boolean render) 釋放緩衝區;如果你配置編解碼器的時候指定一個有效的 surface 時,將 render 設定為 true 將首先把緩衝區傳送到 surface 渲染,這裡單純為了得到 YUV 資料,不做渲染,直接設定為 false。

使用時遇到的問題

在實際的測試過程中發現各家廠商的 Android 裝置 MediaCodec 解碼得到的 YUV 資料格式不盡相同,例如在我的測試機(某一不知名品牌的平板)上解碼得到的是標準的 YUV420P 格式,而在另一臺測試機(華為榮耀note8)上解碼得到的卻是 NV12 格式:

MediaCodec 高效解碼得到標準 YUV420P 格式幀

參考 Android: MediaCodec視訊檔案硬體解碼,高效率得到YUV格式幀,快速儲存JPEG圖片 得知 API 21 新加入了MediaCodec的所有硬體解碼都支援的 COLOR_FormatYUV420Flexible 格式。它並不是一種確定的 YUV420 格式,而是包含了 COLOR_FormatYUV411PlanarCOLOR_FormatYUV411PackedPlanarCOLOR_FormatYUV420PlanarCOLOR_FormatYUV420PackedPlanar,COLOR_FormatYUV420SemiPlanarCOLOR_FormatYUV420PackedSemiPlanar 這幾種,所以只能確保解碼後的幀格式是這幾種中的其中一種。MediaCodecInfo 原始碼中可以看到,在API 21引入 YUV420Flexible 的同時,它所包含的這些格式都 deprecated 掉了:

MediaCodec 高效解碼得到標準 YUV420P 格式幀

指定幀格式

指定幀格式只需要在配置 MediaCodec 之前指定就可以了,在上面的 initCodec 中更新如下:

    MediaFormat mediaFormat = MediaFormat.createVideoFormat(MIME_TYPE, width, height);
    mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible);
    mCodec.configure(mediaFormat, null, null, 0);
    mCodec.start();
複製程式碼

可能由於我這邊測試機較少的問題,我在使用的時候不指定幀格式也能達到同樣的效果。

得到 YUV420P 格式幀

既然將解碼後的幀格式鎖定為上面說到的幾種,那離得到標準的 YUV420P 格式幀就只有一步之遙了。

可以通過 mCodec.getOutputFormat().getInteger(MediaFormat.KEY_COLOR_FORMAT) 得到解碼得到的幀格式,這裡得到的就是 COLOR_FORMATYUV411PLANARCOLOR_FORMATYUV411PACKEDPLANARCOLOR_FORMATYUV420PLANARCOLOR_FORMATYUV420PACKEDPLANAR,COLOR_FORMATYUV420SEMIPLANARCOLOR_FORMATYUV420PACKEDSEMIPLANAR 中的其中一個,接下來只需要把對應的型別轉化成標準的 YUV420P 資料就 OK 了:

MediaFormat mediaFormat = mCodec.getOutputFormat();
switch (mediaFormat.getInteger(MediaFormat.KEY_COLOR_FORMAT)) {
    case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV411Planar:
        break;
    case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV411PackedPlanar:
        break;
    case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedPlanar:
        break;
    case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar:
        yuvData = yuv420spToYuv420P(yuvData, mediaFormat.getInteger(MediaFormat.KEY_WIDTH), mediaFormat.getInteger(MediaFormat.KEY_HEIGHT));
        break;
    case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedSemiPlanar:
        break;
    case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar:
    default:
        break;
}
複製程式碼

附上 yuv420sp 轉 Yuv420P 的方法:

private static byte[] yuv420spToYuv420P(byte[] yuv420spData, int width, int height) {
    byte[] yuv420pData = new byte[width * height * 3 / 2];
    int ySize = width * height;
    System.arraycopy(yuv420spData, 0, yuv420pData, 0, ySize);   //拷貝 Y 分量

    for (int j = 0, i = 0; j < ySize / 2; j += 2, i++) {
        yuv420pData[ySize + i] = yuv420spData[ySize + j];   //U 分量
        yuv420pData[ySize * 5 / 4 + i] = yuv420spData[ySize + j + 1];   //V 分量
    }
    return yuv420pData;
}
複製程式碼

最後說兩句

本文給出 java 層面轉換思路,但實際使用的時候建議在 native 層轉換,或者使用時直接相容不同 YUV 格式,畢竟多這一步轉換,對效率還是會有比較大的影響的。

使用 MediaCodec 之後解碼速度確實快了許多,但在差一些的裝置(例如我那不知名品牌的平板)上面,硬解的表現明顯的低於軟解。目前來看,網上眾多評價說的硬解有坑的說法還是有道理的,但即便有坑,這解碼速度還是讓我欲罷不能啊~

相關文章