【AVD】FFmpeg + MediaCodec 實現 Android 硬體解碼,中間有個大坑

語言: CN / TW / HK

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 手機上流暢播放呢?是不是還有什麼引數可以繼續調整優化呢?希望有高手能給出一些提示或建議。