Android JNI 篇 - ffmpeg 獲取音視頻縮略圖

還是老規矩先上效果圖:

GIF.gif

一、使用 FFmpegMediaMetadataRetriever 初步解決問題

Android 原生的 SDK 提供的 api 中并沒有提供在線獲取縮略圖的接口(只有本地的)
本地獲取音視頻縮略圖代碼(溫習一下):
  private InputStream getAudioInPutream(String imageUri) throws IOException {

    InputStream ret = null;
    MediaMetadataRetriever  fmmr;
    try {
        fmmr = new MediaMetadataRetriever();
        fmmr.setDataSource(imageUri);
        //先獲取封面
        byte[] embedPic = fmmr.getEmbeddedPicture();
        ret = new ByteArrayInputStream(embedPic);
    } catch (Exception ex) {
        ex.printStackTrace();
    } finally {
          if(fmmr != null) {
            fmmr.release();
          }
    }

    if (ret != null) {
        return ret;
    } else {
        throw new IOException("Request video thumb failed by ffmpeg");
    }


}
靈機一動的我,好像 ffmpeg 可以獲取音視頻縮略圖。 趕緊找找有沒有人這么干了,結果還真有在 github 上找到一個 開源項目

https://github.com/wseemann/FFmpegMediaMetadataRetriever

大神終究還是大神。666。好了廢話不說直接用起來:

   private Bitmap getVideoThumBitmap(String imageUri) throws IOException {

    ImageInputStream ret = null;
    Bitmap ret = null;
    try {
        fmmr = new FFmpegMediaMetadataRetriever();

        fmmr.setDataSource(imageUri);

        //獲取視頻的時長
        String value = fmmr.extractMetadata(FFmpegMediaMetadataRetriever.METADATA_KEY_DURATION);

        long frameTime = -1;
        if (value != null) {
            try {
                frameTime = Integer.parseInt(value);
            } catch (NumberFormatException e) {
            }
        }
        if (frameTime > 0) {
            frameTime = (frameTime / 2) * 1000;
        } else {
            frameTime = 4000000;
        }
        //獲取視頻縮略圖
        ret = fmmr.getFrameAtTime(frameTime, FFmpegMediaMetadataRetriever.OPTION_CLOSEST_SYNC)
    } catch (Exception ex) {
        ex.printStackTrace();
    } finally {
        try {
            if (fmmr != null) {
                fmmr.release();
            }
        } catch (Throwable e) {
        }

    }

    return ret;
}
用的感覺還不錯。過了幾天老板把我叫過去懟了一頓,加了這視頻縮略圖 app 用著用著怎么閃退了。而且還怎么慢,切換界面還不能取消之前請求...(心中一千萬個草擬嗎奔騰而過..)
好吧來看看 log,尼瑪 ffmpeg 的 log 一點也沒有打印(原因是編譯so庫的時候 沒有--enable-debug 因為不加 log 可以減少 so 的體積)... 好吧看來這個庫還是不完美,還是的自己填上這個坑。看了一下作者這個庫,ffmpeg 庫已經三年沒有更新了在這日新月異科技迅猛發達的時代,這是大件事.. 意思就是要我編譯一個 最新的版本的 ffmpeg 換上?

二、優化 FFmpegMediaMetadataRetriever —— 替換新版本 ffmpeg

來吧動手起來,先到官網下載最新的
ffmpeg 源碼
搭建 VMware + Ubuntu 環境(這個網上自己找下教程),下載 linux 平臺下的 ndk。 搞定!!
接下來就是寫好編譯腳本放 ffmpeg 源碼的根目錄,腳本語言 :build_android.sh 代碼 。

接下來編譯:

  1. ctrl+alt+t 打開 ubuntu 終端
  2. cd 到 build_android.sh 文件所在的目錄
  3. sudo ./build_android.sh (此處用超級權限去執行)
  4. 等3分鐘..
  5. 編譯結束
  6. make
  7. sudo make install (打包 so 和頭文件.h )
好了,居然出現錯誤(尼瑪我這是官網的代碼還出錯) :
libavcodec/hevc_mvs.c: In function 'derive_spatial_merge_candidates':
libavcodec/hevc_mvs.c:368:23: error: 'y0000000' undeclared (first use in this function)
libavcodec/hevc_mvs.c:368:23: note: each undeclared identifier is reported only once for each function it appears in
libavcodec/hevc_mvs.c:368:23: error: 'x0000000' undeclared (first use in this function)

其實是宏定義的問題,解決方法:

這個文件中 ffmpeg\libavcodec\hevc_mvs.c 里面 TAB_MVF_PU(B0)換成 TAB_MVF_PU(0)
為什么呢?

因為編譯的時候,B0 = 0000000 通過 TAB_MVF_PU 宏定義里面用了連接符##,得出的變量名為 x0000000 或者 y0000000,其實 ffmepg 團隊寫這代碼想要的是 x0y0 這兩個變量名。
好了解決,后面有這個問題直接按照上面的方法 把 B0 換成 0 即可。

好了so 出來了


廢了我大半天功夫,終于搞出來了。同一份 so 只要名字最長的那個,因為最后的鏈接都是鏈接到名字最長的那個 so,重命名 libavcodec.so.57.107.100libavcodec.so(后面三個 so 同樣去掉版本號) 然后替換到原工程FFmpegMediaMetadataRetriever 相同位置下,以及把 include 文件夾替換掉,好了一跑,果然不閃退了,視頻縮略圖獲取更加快了。新版本果然優化了很多東西,至于是什么,后面有空再去研究..

三、優化 FFmpegMediaMetadataRetriever —— 加入中斷機制

終于替換掉了舊版本 ffmpeg,可是還是無法中斷縮略圖的獲取,原項目里面沒有 取消的縮略圖的 api ,畢竟視頻獲取縮略圖還是挺耗時的,因為要去解析幀數據。如果大量請求 視頻縮略圖,切換界面后,或者在一個 RecyclerView 快速上拉又不能取消請求。只能等著前面的縮略圖一個個加載出來,才能加載到當前界面,這體驗簡直不能忍。
為了解決這問題,就要去修改原作者的 jni 代碼 。動起手來!!
找到耗時的接口:
  1. avformat_open_input // 超級耗時接口,有時候能耗時10s+
  2. avformat_find_stream_info // 第二耗時接口
  3. av_read_frame // 第三耗時接口
java 層加入取消接口:
  nativeCancelRequest();
接口具體的實現是通過:
1. 設置一個結構體
typedef struct {
   time_t lasttime; //記錄請求開始當下時間
   int timeout; //超時時間 單位 秒
   int interrupt; //1 表示退出請求,0 繼續請求
} Runner;
2.通過 ffmpeg 回調的接口,不斷訪問 Runner 是否需要取消
// 回調函數
int interruptCallback(void *p) {
    Runner *r = (Runner *) p;
    if (r->lasttime > 0) {
        if ((time(NULL) - r->lasttime > r->timeout)
            || (r->interrupt == 1)) {
            
            return 1;
        }
    }

    return 0;
}

 state->pFormatCtx->interrupt_callback.callback = interruptCallback;
3.需要取消的時候 通過接口 nativeCancelRequest 把對應線程的 Runner 中的 interrupt 置 1.
完美退出!!
好了具體細節看我的源代碼。FFmpegMediaMetadataRetriever
最后祝大家元旦快樂。迎接新的 2018 年!!
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。