當前位置:網站首頁>FFmpeg 調用 MediaCodec 硬解碼到 Surface 上

FFmpeg 調用 MediaCodec 硬解碼到 Surface 上

2022-01-27 17:21:59 音視頻開發進階

這是關於 FFmpeg 和 MediaCodec 愛恨情仇系列的第三篇文章了。

之前寫了 FFmpeg 調用 MediaCodec 進行硬解碼的內容。

另外也給出了 FFmpeg 的編譯脚本,輕松搞定編譯問題。

眾所周知,MediaCodec 的解碼能力不僅可以解碼出 YUV 數據,還能直接解碼到 Surface 上。

在短視頻領域中,MediaCodec 解碼到 Surface 上的能力反而更加常用,這樣就能將畫面轉到 OES 紋理上,從而進行後續各種渲染操作。

之前介紹的 FFmpeg 調用 MediaCodec 進行硬解碼只是解碼出了 Buffer 數據,沒有把解碼到 Surface 上的能力用起來。

再看了更多資料之後,發現 FFmpeg 調用 MediaCodec 已經可以解碼到 Surface 上。

具體參考的是這篇郵件內容:

http://mplayerhq.hu/pipermail/ffmpeg-devel/2016-March/191700.html

在這裏面詳細介紹了這種能力,挑重點截圖一下:

f0e50be0cebe54b22a5ac4ceb5b57634.png

圖片內容介紹的很詳細,按照步驟實踐就好了。

代碼實踐

如果熟悉了 FFmpeg 調用 MediaCodec 解碼 Buffer 數據的流程,那麼解碼到 Surface 只是在流程上稍微改動一點就行。

首先要准備好 Surface 對象,在 Java 上層構建好 Surface 對象通過 NDK 傳到 Native 層,傳下來的是一個 jobject 對象。

如果不熟悉 NDK 的話,可以看看我在慕課網上的錄制的免費課程:

Android NDK 免費視頻在線學習!!!

其次是兩個新的函數方法:

a9b0e820260f99dd86fc43af1e8feac7.png

av_mediacodec_alloc_context 和 av_mediacodec_default_init 方法就是讓 Surface 和 AVMediaCodecContext 、AVCodecContext 三者之間產生關聯。

具體就是 AVCodecContext 持有 AVMediaCodecContext ,AVMediaCodecContext 持有 Surface 。

至於為什麼要關聯,因為在 FFmpeg 源碼裏要根據 Surface 是否為 nullptr 對 MediaCodec 的初始化和解碼後的數據做不同處理。

感興趣的可以閱讀這塊的源碼,內容不多通俗易懂。

等到解碼之後,FFmpeg 同樣會返回一個 AVFrame 數據,只不過它的 data 字段不再是 Buffer 內容了。

AVFrame 的格式不再是 NV12 (解碼 Buffer 數據的話就是 NV12),而是自定義的 AV_PIX_FMT_MEDIACODEC ,代錶走的 Surface 模式。

if (s->surface) {
   // surface 不為 null 的情况下:
   // 通過 mediacodec_wrap_hw_buffer 對數據進行處理
  if ((ret = mediacodec_wrap_hw_buffer(avctx, s, index, &info, frame)) < 0) {
    av_log(avctx, AV_LOG_ERROR, "Failed to wrap MediaCodec buffer\n");
       return ret;
  }
}

Surface 模式下對數據的處理是 mediacodec_wrap_hw_buffer 函數,Buffer 模式就是 mediacodec_wrap_sw_buffer 函數了。

同時,真正解碼後的數據存儲在 AVFrame->data[3] 字段上,這個字段是個老員工了。

一般解碼非 Buffer 數據的情况,都會將特殊的內容保存到 data[3] 上,比如 Window 上的硬解,部分源碼如下:

static int mediacodec_wrap_hw_buffer(AVCodecContext *avctx,
                                  MediaCodecDecContext *s,
                                  ssize_t index,
                                  FFAMediaCodecBufferInfo *info,
                                  AVFrame *frame)
// 省略部分源碼
// 創建   AVMediaCodecBuffer 對象
buffer = av_mallocz(sizeof(AVMediaCodecBuffer));
frame->buf[0] = av_buffer_create(NULL,
                       0,
                       mediacodec_buffer_release,
                       buffer,
                       AV_BUFFER_FLAG_READONLY);
// buffer 相關屬性賦值
buffer->ctx = s;
buffer->serial = atomic_load(&s->serial);
if (s->delay_flush)
    ff_mediacodec_dec_ref(s);
// index 索引,代錶 mediacodec 中 buffer 的索引
buffer->index = index;
buffer->pts = info->presentationTimeUs;
// 將 buffer 賦值給 data[3] 字段
frame->data[3] = (uint8_t *)buffer;

有了 AVFrame 數據之後,Surface 上還是沒有畫面。

回想一下,在 MediaCodec 上想要數據渲染到 Surface 還得調用一個 releaseOutputBuffer 方法,其中第二個參數要傳 true 才可以。

public void releaseOutputBuffer (int index, boolean render)

同樣,在 FFmpeg 中也有這麼個方法。

int av_mediacodec_release_buffer(AVMediaCodecBuffer *buffer, int render);

buffer 就是 frame->data[3] 的內容,render 的含義和 releaseOutputBuffer 中的含義一致。

另外 releaseOutputBuffer 方法第一個參數 index 其實就已經在 buffer 中賦值過了。

這樣一來,解碼後就可以直接上屏渲染展示啦。

AVMediaCodecBuffer *buffer = (AVMediaCodecBuffer *) frame->data[3];
 av_mediacodec_release_buffer(buffer, 1);

完整代碼實踐可以在公眾號 音視頻開發進階 回複 1019 獲取。

經過測試驗證確實可行,不過直接不斷解碼上屏的速度是很快的,可不止視頻播放 30ms 一幀的速度哦,想要來做播放器的話,還得自己管理控制一下了。

另外,完整代碼演示中直接解碼到了 SurfaceView 的 Surface 上。

除此之外,還可以解碼到 SurfaceTexture 構造的 Surface 上,這樣就可以用到 SurfaceTexture 的 OnFrameAvailableListener 回調方法, 並且還可以用 attachToGLContext 方法關聯到 OpenGL 環境上,每次解碼時通過 updateTexImage 更新畫面,實現解碼到 OES 紋理的目標,具體操作起來也是很容易方便。

5e106b83e2adda04a1920ddcb434f20e.png

技術交流,歡迎加我微信:ezglumes ,拉你入技術交流群。

e08a839df93a6727c6e4c52636b39a69.png

私信領取相關資料

推薦閱讀:

音視頻開發工作經驗分享 || 視頻版

OpenGL ES 學習資源分享

開通專輯 | 細數那些年寫過的技術文章專輯

NDK 學習進階免費視頻來了

你想要的音視頻開發資料庫來了

推薦幾個堪稱教科書級別的 Android 音視頻入門項目

覺得不錯,點個在看唄~

9e645cf83a8236fab42839ed45a213d2.gif

版權聲明
本文為[音視頻開發進階]所創,轉載請帶上原文鏈接,感謝
https://cht.chowdera.com/2022/01/202201271721585353.html

隨機推薦