FFmpeg 調(diào)用 Android MediaCodec 進(jìn)行硬解碼(附源碼)
FFmpeg 在 3.1 版本之后支持調(diào)用平臺硬件進(jìn)行解碼,也就是說可以通過 FFmpeg 的 C 代碼去調(diào)用 Android 上的 MediaCodec 了。
在官網(wǎng)上有對應(yīng)說明,地址如下:
https://trac.ffmpeg.org/wiki/HWAccelIntro

注意:Android MediaCodec 目前僅支持解碼,還不支持編碼呢。
不過,為了驗(yàn)證是否可行,做個簡單的演示,最后會有完整的的代碼給出。
首先是 FFmpeg 的編譯。它的編譯有很多開關(guān)選項(xiàng),要確保打開了 mediacodec 相關(guān)的選項(xiàng),具體如下:
--enable-mediacodec
--enable-decoder=h264_mediacodec
--enable-decoder=hevc_mediacodec
--enable-decoder=mpeg4_mediacodec
--enable-hwaccel=h264_mediacodec
可以看出 mediacodec 支持的編碼格式有 h264、hevc、mpeg4 三種可選,不在范圍內(nèi)的就還是考慮軟解吧。
關(guān)于如何編譯,就不詳細(xì)闡述了,后面再專門寫一篇來介紹。
編譯出對應(yīng)的 so 之后,可以打印一下 AVCodec 支持的格式列表,看看有沒有 mediacodec 。
具體代碼如下:
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 指針進(jìn)行遍歷,然后打印出結(jié)果,看到下面的內(nèi)容說明編譯成功了。

接下來就進(jìn)行解碼了。關(guān)于 FFmpeg 解碼的 API 調(diào)用,在公眾號以前發(fā)布的文章中說過多次,就不詳細(xì)講解流程了。
FFmpeg音頻處理——音頻混合、拼接、剪切、轉(zhuǎn)碼
簡單概況一下:
首先通過 avformat_open_input 方法打開文件,得到 AVFormatContext 。 然后通過 avformat_find_stream_info 查找文件的視頻流信息。 得到文件相關(guān)信息和視頻流信息,主要還是為了得到編碼格式信息,然后好找到對應(yīng)的解碼器。也可以通過 avcodec_find_decoder_by_name 方法直接找具體的解碼器。 有了解碼器就可以創(chuàng)建解碼上下文 AVCodecContext,并通過 avcodec_open2 方法打開解碼器 然后通過 av_read_frame 讀取文件的內(nèi)容好進(jìn)行下一步的解碼。 接下來就是熟悉的 avcodec_send_packet 發(fā)送給解碼器,avcodec_receive_frame 從解碼器取回解碼后的數(shù)據(jù)。
重點(diǎn)講解一下調(diào)用硬件解碼和普通解碼的一些區(qū)別:
第一步是要在 so 加載的 JNI_OnLoad 方法中將 JavaVM 設(shè)置給 FFmpeg 。
jint?JNI_OnLoad(JavaVM?*vm,?void?*res)?{
????av_jni_set_java_vm(vm,?0);
????return?JNI_VERSION_1_4;
}
缺少這一步就不能反射調(diào)用 Java 方法了。
接下來還是判斷硬件解碼類型支不支持,上面是通過 AVCodec 來判斷的,實(shí)際上 FFmpeg 都給出了硬件類型的定義,在 AVHWDeviceType 枚舉變量中。
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 方法可以將這些枚舉值轉(zhuǎn)換成對應(yīng)的字符串,比如 AV_HWDEVICE_TYPE_MEDIACODEC 對應(yīng)的字符串就是 mediacodec ,其實(shí)在源碼里面也是有的:
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 。
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 的解碼器就行。
if?(!(decoder?=?avcodec_find_decoder_by_name("h264_mediacodec")))?{
????LOGE("avcodec_find_decoder_by_name?failed.\n");
????return?-1;
}
找到解碼器之后,還要得到該解碼器的一些配置信息,比如解碼出的格式是什么樣子的?mediacodec 解碼就是 NV21 這種。
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 添加一些硬件解碼的上下文。
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 內(nèi)容。
如果 AVFrame 格式和硬件解碼的配置格式一樣,那么要用 av_hwframe_transfer_data 方法將它做一下轉(zhuǎn)換,轉(zhuǎn)成正常的 YUV 格式。
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;
等完成這一些操作之后,就已經(jīng)解碼成功了,實(shí)際運(yùn)行也是 OK 的。
獲取完整源碼的話,可以關(guān)注微信公眾號:音視頻開發(fā)進(jìn)階,回復(fù) 1019 獲取下載地址。

技術(shù)交流,歡迎加我微信:ezglumes ,拉你入技術(shù)交流群。
推薦閱讀:
音視頻開發(fā)工作經(jīng)驗(yàn)分享 || 視頻版
開通專輯 | 細(xì)數(shù)那些年寫過的技術(shù)文章專輯
NDK 學(xué)習(xí)進(jìn)階免費(fèi)視頻來了
推薦幾個堪稱教科書級別的 Android 音視頻入門項(xiàng)目
覺得不錯,點(diǎn)個在看唄~

