FFmpeg 呼叫 Android MediaCodec 進行硬解碼(附原始碼)

語言: CN / TW / HK

FFmpeg 在 3.1 版本之後支援呼叫平臺硬體進行解碼,也就是說可以通過 FFmpeg 的 C 程式碼去呼叫 Android 上的 MediaCodec 了。

在官網上有對應說明,地址如下:

https://trac.ffmpeg.org/wiki/HWAccelIntro

image.png

從圖中可以看到,不僅僅是 Android 上支援 MediaCodec,iOS 上也支援 VideoToolbox,連 Windows 上的 Direct3D 11 都有支援了。

注意:Android MediaCodec 目前僅支援解碼,還不支援編碼呢。

不過,為了驗證是否可行,做個簡單的演示,最後會有完整的的程式碼給出。

首先是 FFmpeg 的編譯。它的編譯有很多開關選項,要確保打開了 mediacodec 相關的選項,具體如下:

```cpp

--enable-mediacodec

--enable-decoder=h264_mediacodec

--enable-decoder=hevc_mediacodec

--enable-decoder=mpeg4_mediacodec

--enable-hwaccel=h264_mediacodec

```

可以看出 mediacodec 支援的編碼格式有 h264、hevc、mpeg4 三種可選,不在範圍內的就還是考慮軟解吧。

關於如何編譯,就不詳細闡述了,後面再專門寫一篇來介紹

編譯出對應的 so 之後,可以列印一下 AVCodec 支援的格式列表,看看有沒有 mediacodec 。

具體程式碼如下:

```cpp

char info[40000] = {0};

AVCodec *c_temp = av_codec_next(NULL);

while (c_temp != NULL) {

if (c_temp->decode != NULL) {

sprintf(info, "%s[Dec]", info);

} else {

sprintf(info, "%s[Enc]", info);

}

switch (c_temp->type) {

case AVMEDIA_TYPE_VIDEO:

sprintf(info, "%s[Video]", info);

break;

case AVMEDIA_TYPE_AUDIO:

sprintf(info, "%s[Audio]", info);

break;

default:

sprintf(info, "%s[Other]", info);

break;

}

sprintf(info, "%s %10s\n", info, c_temp->name);

c_temp = c_temp->next;

}

```

通過 AVCodec 的 next 指標進行遍歷,然後打印出結果,看到下面的內容說明編譯成功了。

image.png

支援的格式裡面已經有了 h264_mediacodec 和 mpeg4_mediacodec 了。


接下來就進行解碼了。關於 FFmpeg 解碼的 API 呼叫,在公眾號以前釋出的文章中說過多次,就不詳細講解流程了,簡單概況一下:

  1. 首先通過 avformat_open_input 方法開啟檔案,得到 AVFormatContext 。

  2. 然後通過 avformat_find_stream_info 查詢檔案的視訊流資訊。

  3. 得到檔案相關資訊和視訊流資訊,主要還是為了得到編碼格式資訊,然後好找到對應的解碼器。也可以通過 avcodec_find_decoder_by_name 方法直接找具體的解碼器。

  4. 有了解碼器就可以建立解碼上下文 AVCodecContext,並通過 avcodec_open2 方法開啟解碼器

  5. 然後通過 av_read_frame 讀取檔案的內容好進行下一步的解碼。

  6. 接下來就是熟悉的 avcodec_send_packet 傳送給解碼器,avcodec_receive_frame 從解碼器取回解碼後的資料。

重點講解一下呼叫硬體解碼和普通解碼的一些區別:

第一步是要在 so 載入的 JNI_OnLoad 方法中將 JavaVM 設定給 FFmpeg 。

```cpp

jint JNI_OnLoad(JavaVM vm, void res) {

av_jni_set_java_vm(vm, 0);

return JNI_VERSION_1_4;

}

```

缺少這一步就不能反射呼叫 Java 方法了。


接下來還是判斷硬體解碼型別支不支援,上面是通過 AVCodec 來判斷的,實際上 FFmpeg 都給出了硬體型別的定義,在 AVHWDeviceType 列舉變數中。

```cpp

enum AVHWDeviceType {

AV_HWDEVICE_TYPE_NONE,

AV_HWDEVICE_TYPE_VDPAU,

AV_HWDEVICE_TYPE_CUDA,

AV_HWDEVICE_TYPE_VAAPI,

AV_HWDEVICE_TYPE_DXVA2,

AV_HWDEVICE_TYPE_QSV,

AV_HWDEVICE_TYPE_VIDEOTOOLBOX,

AV_HWDEVICE_TYPE_D3D11VA,

AV_HWDEVICE_TYPE_DRM,

AV_HWDEVICE_TYPE_OPENCL,

AV_HWDEVICE_TYPE_MEDIACODEC,

AV_HWDEVICE_TYPE_VULKAN,

};

```

通過 av_hwdevice_get_type_name 方法可以將這些列舉值轉換成對應的字串,比如 AV_HWDEVICE_TYPE_MEDIACODEC 對應的字串就是 mediacodec ,其實在原始碼裡面也是有的:

```cpp

static const char *const hw_type_names[] = {

[AV_HWDEVICE_TYPE_CUDA] = "cuda",

[AV_HWDEVICE_TYPE_DRM] = "drm",

[AV_HWDEVICE_TYPE_DXVA2] = "dxva2",

[AV_HWDEVICE_TYPE_D3D11VA] = "d3d11va",

[AV_HWDEVICE_TYPE_OPENCL] = "opencl",

[AV_HWDEVICE_TYPE_QSV] = "qsv",

[AV_HWDEVICE_TYPE_VAAPI] = "vaapi",

[AV_HWDEVICE_TYPE_VDPAU] = "vdpau",

[AV_HWDEVICE_TYPE_VIDEOTOOLBOX] = "videotoolbox",

[AV_HWDEVICE_TYPE_MEDIACODEC] = "mediacodec",

[AV_HWDEVICE_TYPE_VULKAN] = "vulkan",

};

```

和遍歷 AVCodec 一樣,也要遍歷 FFmpeg 是否支援 mediacodec 。

```cpp

type = av_hwdevice_find_type_by_name(mediacodec);

if (type == AV_HWDEVICE_TYPE_NONE) {

LOGE("Device type %s is not supported.\n", mediacodec);

LOGE("Available device types:");

while((type = av_hwdevice_iterate_types(type)) != AV_HWDEVICE_TYPE_NONE)

LOGE(" %s", av_hwdevice_get_type_name(type));

LOGE("\n");

return -1;

}

```

確定支援 mediacodec ,那麼解碼就可以用了。前面提到,獲取檔案資訊主要是為了開啟解碼器的,但比如檔案編碼格式的 H.264 ,而支援 H.264 的解碼器除了軟解,還有 mediacodec 要怎麼選擇呢?

為了方便,直接 avcodec_find_decoder_by_name 找到 mediacodec 的解碼器就行。

```cpp

if (!(decoder = avcodec_find_decoder_by_name("h264_mediacodec"))) {

LOGE("avcodec_find_decoder_by_name failed.\n");

return -1;

}

```

找到解碼器之後,還要得到該解碼器的一些配置資訊,比如解碼出的格式是什麼樣子的?mediacodec 解碼就是 NV21 這種。

```cpp

for (i = 0;; i++) {

// 解碼器的配置

const AVCodecHWConfig *config = avcodec_get_hw_config(decoder, i);

if (!config) {

LOGE("Decoder %s does not support device type %s.\n",

decoder->name, av_hwdevice_get_type_name(type));

return -1;

}

if (config->methods & AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX &&

config->device_type == type) {

// 硬解的格式

hw_pix_fmt = config->pix_fmt;

break;

}

}

```

目前 mediacodec 解碼還只有 buffer 模式,沒有直接解紋理的那種。

接下來就是給解碼上下文 AVCodecContext 新增一些硬體解碼的上下文。

```cpp

static int hw_decoder_init(AVCodecContext *ctx, const enum AVHWDeviceType type)

{

int err = 0;

if ((err = av_hwdevice_ctx_create(&hw_device_ctx, type,

NULL, NULL, 0)) < 0) {

LOGE("Failed to create specified HW device.\n");

return err;

}

// 硬解解碼的上下文

ctx->hw_device_ctx = av_buffer_ref(hw_device_ctx);

return err;

}

```

完成了這一系列操作之後,就是正常的解碼了,拿到解碼後的 AVFrame 內容。

如果 AVFrame 格式和硬體解碼的配置格式一樣,那麼要用 av_hwframe_transfer_data 方法將它做一下轉換,轉成正常的 YUV 格式。

```cpp

if (frame->format == hw_pix_fmt) {

/ retrieve data from GPU to CPU /

if ((ret = av_hwframe_transfer_data(sw_frame, frame, 0)) < 0) {

LOGE("Error transferring the data to system memory\n");

goto fail;

}

tmp_frame = sw_frame;

} else

tmp_frame = frame;

```

等完成這一些操作之後,就已經解碼成功了,實際執行也是 OK 的。

歡迎關注微信公眾號 音視訊開發進階,閱讀更多音視訊開發文章~~