【AVD】FFmpeg + MediaCodec 實現 Android 硬體解碼,中間有個大坑
highlight: a11y-dark
本文已參與「新人創作禮」活動, 一起開啟掘金創作之路。
最近在做移動端音視訊編解碼,首先要實現的是移動端視訊的解碼功能。純的 FFmpeg 方法在移動端也能實現,但是效率上的確要慢一些,1080p 的視訊還好,但是上到 2k、4k,那個解碼速度(以肉眼可見的速度解碼一幀),就沒法忍受了。因此要搞移動端硬體解碼,以加速解碼速度,同時釋放部分 CPU 資源。
參考 FFmpeg 原始碼中 examples
參考 FFmpeg 官方原始碼中的 examples 的相關功能實現,來實現自己的程式設計,應該是最快的思路。但是,關於視訊解碼,FFmpeg 官方原始碼中,有 decode_video.c
demuxing_decoding.c
hw_decode.c
,這三個解碼相關的檔案。
其實前兩個檔案的方案差不多,只不過第一個針對裸 h264 流,而第二個是針對帶封裝的視訊檔案。建議新手可以參考第二個方案。
至於第三個檔案,hw_decode.c
,看起來是一個硬體解碼的 demo,當然,它的確也是(笑),然而,這裡,我們卻不能參考這個。參考這個檔案,我們可以實現在 Linux 或者 Windows 平臺上,利用 cuvid 或者 NVIDIA 、Intel 等硬體廠商實現的硬解碼功能,實現硬體解碼。但是,在 Android 平臺使用 MediaCodec 的解碼,卻沒有實現。
在嘗試參考 hw_decode.c
實現 MediaCodec 硬解碼的過程中,在 195 行 avcodec_get_hw_config
這一步失敗,沒有任何 config 列表可供選擇。
因此,我們還是參考 demuxing_decoding.c
來實現 Android 平臺 MediaCodec 硬解碼。
Then,Why?MediaCodec 架構簡析
因為 Android 是個平臺,其硬體廠商多種多樣,而 MediaCodec 並非是一個硬體廠商,因此它並不提供硬體編解碼方案。實際上,MediaCodec 更像是一箇中間層,通過 openMAX 繼承硬體廠商的硬體編解碼能力,最終,硬體廠商通過提供符合 openMAX 規範的硬體編解碼庫。因此,如果仿照 hw_encode.c
來實現,必然會在 avcodec_get_hw_config
這一層找不到合適的配置。
一處改動
那麼,我們就完全可以參考 demuxing_decoding.c
來實現 Android 平臺 MediaCodec 硬解碼功能。其實,基本上全文拷貝到 Android native 程式碼中,即可使用,只需要改動一處。即 165 行的 dec = avcodec_find_decoder(st->codecpar->codec_id);
改為 dec = avcodec_find_decoder_by_name("h264_mediacodec");
即可。
為了方便,我們只解碼檔案中的視訊流,同時簡化整個流程,基本上,完整程式碼如下:
```cpp static AVFrame decode_frame = nullptr; int testFFmpegMediaCodec(bool sw) { string filename = "/sdcard/pav/hd.mp4"; AVFormatContext fmt_ctx_ = nullptr; int ret = 0; if (ret = avformat_open_input(&fmt_ctx_, filename.c_str(), nullptr, nullptr) < 0) { LOGE("Failed open file %s, ret = %d", filename.c_str(), ret); return -1; } if (avformat_find_stream_info(fmt_ctx_, nullptr) < 0) { LOGE("Failed to find stream information."); return -1; } int vst_idx = av_find_best_stream(fmt_ctx_, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, false); if (vst_idx < 0) { LOGE("Failed to find video stream from file %s", fmt_ctx_->filename); return -1; } AVCodec pCodec = avcodec_find_decoder_by_name("h264_mediacodec"); AVCodecContext codec_context = avcodec_alloc_context3(pCodec); avcodec_parameters_to_context(codec_context, fmt_ctx_->streams[vst_idx]->codecpar); if (ret = avcodec_open2(codec_context, pCodec, nullptr)) { LOGE("Failed to open avcodec. ret = %d", ret); return -1; } decode_frame = av_frame_alloc(); if (!decode_frame) { LOGE("Failed to allocate frame."); return -1; } decode_frame->format = codec_context->pix_fmt; decode_frame->width = codec_context->width; decode_frame->height = codec_context->height; av_frame_get_buffer(decode_frame, 0); ret = av_image_alloc(video_dst_data, video_dst_linesize, decode_frame->width, decode_frame->height, (AVPixelFormat)decode_frame->format, 1); if (ret < 0) { LOGE("Could not allocate raw video buffer."); return -1; } else { LOGD("We allocate %d for raw video buffer.", ret); }
AVPacket pkt;
av_init_packet(&pkt);
pkt.data = nullptr;
pkt.size = 0;
while (av_read_frame(fmt_ctx_, &pkt) >= 0) {
if (pkt.stream_index == vst_idx) {
ret = avcodec_send_packet(codec_context, pkt);
if (ret < 0) {
LOGE("Error submitting a packet for decoding (%s)", av_err2str(ret));
continue;
}
while (ret >= 0) {
ret = avcodec_receive_frame(codec_context, decode_frame);
if (ret < 0) {
if (ret == AVERROR_EOF) return 0;
if (ret == AVERROR(EAGAIN)) break;
}
// process with decode_frame
av_frame_unref(decode_frame);
break;
}
}
av_packet_unref(&pkt);
}
return 0;
} ```
一個大坑
以上程式碼僅為示例,不保證能直接執行,可能需要做些微小的調整。然而,即使以上程式碼沒有問題,這個 Android MediaCodec 硬解碼功能仍然不能實現。這其中,有個大坑。
當我把程式碼調整好執行起來之後,發現,程式在解碼完第一幀之後,就一直報錯:Error submitting a packet for decoding,EAGAIN。這個問題困擾了我很久,直到有網友提到說,需要先把緩衝區裡的已解碼的幀資料取出來,然後才能再次送入資料進行解碼。
最簡單快捷的改法,就是把 avcodec_send_packet
後面對 ret < 0
時的 continue
去掉,去掉之後,果然能持續解碼了。但是會丟掉好多幀,因為當 ret < 0
時,讀取到的 packet 並未成功送入解碼佇列中去。
因此,這裡需要修改一下整個解碼邏輯。修改為,先去試圖取資料,取不到了,再讀資料送入解碼器佇列。即修改為如下:
cpp
while (true) {
ret = avcodec_receive_frame(codec_context, decode_frame);
if (ret == 0) {
// process with decode_frame
av_frame_unref(decode_frame);
continue;
} else if (ret == AVERROR(EAGAIN)) {
ret = av_read_frame(fmt_ctx_, &pkt);
if (ret == AVERROR_EOF) return 0;
if (pkt.stream_index != vst_idx) {
av_packet_unref(&pkt); // 注意這一句,缺失將造成記憶體洩漏
continue;
}
ret = avcodec_send_packet(codec_context, pkt);
if (ret < 0) {
LOGE("Error submitting a packet for decoding (%s)", av_err2str(ret));
continue;
}
}
}
僅為示例程式碼。具體細節請自行處理。
結語
至此,利用 FFmpeg + MediaCodec 實現的 C++ 層 Android 硬解碼功能就能正常實現了。 我在 小米 MIX 2S 手機上做了下簡單的測試,1080 p 的 h264 視訊解碼完成能達到 25 倍速(即 25 秒鐘的視訊解碼完成需要 1 秒鐘),而 4k 的 h264 視訊能達到 3.5 倍速。
然而,4k 的 h264 視訊解碼時有較大的概率出現花屏現象。其花屏的概率、程度,可能與手機的效能、狀態均有關係。第一次調通硬解碼時解了下 4k 的一段視訊,發現花屏嚴重。而第二天,剛開始工作時解了下同一段視訊,發現前面若干幀基本沒有花屏現象,大約從三百幀開始,出現小的花屏,最後幾幀花屏嚴重。
對於花屏這個問題,有網友稱移動平臺解碼 4k 視訊的確很吃力,很費勁,甚至解不動。如果這樣的話,那麼為什麼那段 4k 的視訊能在 小米 MIX 2S 手機上流暢播放呢?是不是還有什麼引數可以繼續調整優化呢?希望有高手能給出一些提示或建議。