編譯並裁剪 FFmpeg 在 Android 上做視訊編輯

Michael周發表於2018-06-12

轉載請聯絡: 微訊號: michaelzhoujay

原文請訪問我的部落格


眾所周知,Android 對涉及底層硬體的 API 控制力都比較弱,從其難用的 Camera/Camera2、MediaCodec 等 API 就可見一斑。

最近專案中有需要對視訊進行編輯的需求,總體分析有如下技術上需要實現的點:

1. 需要支援視訊尺寸裁剪,給出左上角和右下角的座標後裁剪兩個點描述的區域;

2. 需要支援幀預覽,裁剪前需要向使用者展示時間線上的預覽圖;

3. 需要支援擷取視訊,給出開始時間和結束時間後擷取這兩個時間點之間的視訊段落。
複製程式碼

MediaCodec 方案

首先,按照 Android 官方的文件推薦,當然首推 MediaCodec。

MediaCodec 編解碼

  1. MediaCodec 尺寸裁減

    首先用 inputBuffers 讀取幀資料到 outputBuffers,如果需要使用 MediaCodec 裁減尺寸,按照上圖 MediaCodec 的流程以及官方的文件,需要在處理 output buffer 時將每一幀的資料處理為 bitmap 然後根據左上角的座標和右下角的座標對影象進行裁減 Bitmap.createBitmap 實際上這樣裁減的過程還是在利用 CPU 來進行裁減

  2. MediaCodec 取幀

    使用MediaMetadataRetriever

  3. MediaCodec 擷取

    擷取實際上在第一步的 output 就可以做了,因為 outputbuffer 裡每一幀的資料就有時間戳資訊,MediaCodec.BufferInfo.presentationTimeUs


MediaCodec 的問題

怎麼樣,看起來這套方案還是不錯的,但是實際操作下來有幾個嚴重的問題:

  1. 首先不是所有裝置的 DSP 晶片都支援你需要的 codec 對應的編碼器,而且編碼器支援特性相當有限: 具體參考微信團隊對 MediaCodec 編碼器的研究

如果使用MediaCodec來編碼H264視訊流,對於H264格式來說,會有一些針對壓縮率以及位元速率相關的視訊質量設定,典型的諸如Profile(baseline, main, high),Profile Level, Bitrate mode(CBR, CQ, VBR),合理配置這些引數可以讓我們在同等的位元速率下,獲得更高的壓縮率,從而提升視訊的質量,Android也提供了對應的API進行設定,可以設定到MediaFormat中這些設定項:

MediaFormat.KEY_BITRATE_MODE

MediaFormat.KEY_PROFILE

MediaFormat.KEY_LEVEL

但問題是,對於Profile,Level, Bitrate mode這些設定,在大部分手機上都是不支援的,即使是設定了最終也不會生效,例如設定了Profile為high,最後出來的視訊依然還會是Baseline....

  1. 其次,MediaMetadataRetriever 實測也不太好用,在某些機型上會出現取不到幀的情況。 於是決定棄用 MediaCodec 轉投如日中天的 FFmpeg。

FFmpeg

FFmpeg 由於其豐富的 codec 外掛,詳細的文件說明,並且與其除錯複雜量大的編解碼程式碼(是的,用 MediaCodec 實現起來十分囉嗦和繁瑣)還是不如除錯一行 ffmpeg 命令來的簡單。

利用 FFmpeg 做視訊編輯大家一般都會去參考這個 repo ,但是他的 asset 裡面的 ffmpeg 大小高達 18MB,即使壓縮排 APK 包裡也會達到 9MB。對 APK 大小敏感的開發者肯定頗有微詞。 ffmpeg-android-java 的原理很簡單,交叉編譯好可執行的 ffmpeg 二進位制檔案放到 asset 裡,安裝後釋放二進位制檔案到 /data/data/ 裡,用 Shell command 的形式去執行這個檔案,好處是沒有任何依賴(依賴全打進二進位制了),穩定可靠(不需要動態載入)。 壞處就很明顯了,因為是二進位制檔案,所以 size 會很大。

於是,果斷放棄這種方式,轉而編譯 ffmpeg 的 so 庫,動態載入然後執行命令。聽起來不錯,對不對?動態庫的大小肯定比 ffmpeg-android-java 的 executable 要小多了,而且自己編譯 ffmpeg 還能對其進行裁減。


交叉編譯 FFmpeg 及 x264

相信很多開發者都會使用 ijkplayer,ijkplayer 底層也用到了 ffmpeg,ijk使用的是 so 庫的形式,libffmpeg.so。所以最理想的狀態是,重新編譯一個公共的 libffmpeg.so,這個 libffmpeg.so 即有 ijk 需要的 decoders 和視訊編輯模組需要的 encoders。但是一旦 ijk 或者 ffmpeg 有升級就會很麻煩,因為得重新編譯一次 ffmpeg,而且還得 fork ijkplayer,然後每當 ijk 更新的時候將 ijkplayer master 合併到你 fork 分支,視訊播放又是很常用的模組,很難做到“無痛”升級。

如果不動 ijk 的 ffmpeg,單獨為視訊編輯模組編譯一個 ffmpeg.so ,與視訊播放模組隔離開,這樣就可以無痛升級 ijk 依賴 ffmpeg 的視訊播放庫了。但是,問題來了,如果存在兩個 ffmpeg 的話不可避免的會存在冗餘。所以編譯視訊編輯模組的 ffmpeg 時,要裁剪他的 encoders 和 decoders 儘量做到兩個 ffmpeg 模組是正交的就 ok了。

交叉編譯 FFmpeg 的過程就不贅述,網上有太多教程,這裡簡單記錄一下編譯的步驟:

  1. 同步 x264 的 repo,這裡我選擇的是 YIXIA INC 的 mirror.

  2. 編寫編譯指令碼:

    #!/bin/bash
    
    if [ -z "$ANDROID_NDK" ]; then
    	echo "You must define ANDROID_NDK before starting."
    	echo "They must point to your NDK directories.\n"
    	exit 1
    fi
    
    # Detect OS
    OS=`uname`
    HOST_ARCH=`uname -m`
    export CCACHE=; type ccache >/dev/null 2>&1 && export CCACHE=ccache
    if [ $OS == 'Linux' ]; then
    	export HOST_SYSTEM=linux-$HOST_ARCH
    elif [ $OS == 'Darwin' ]; then
    	export HOST_SYSTEM=darwin-$HOST_ARCH
    fi
    
    NDK=/Users/xxx/Library/Android/sdk/ndk-bundle
    
    SOURCE=`pwd`
    PREFIX=$SOURCE/build/android
    TOOLCHAIN=$NDK/toolchains/arm-linux-androideabi-4.9/prebuilt/darwin-x86_64
    SYSROOT=$NDK/platforms/android-16/arch-arm/
    ADDI_CFLAGS="-marm"
    #EXTRA_CFLAGS="-march=armv7-a -mfloat-abi=softfp -mfpu=neon -D__ARM_ARCH_7__ -D__ARM_ARCH_7A__"
    #EXTRA_LDFLAGS="-nostdlib"
    
    ./configure  --prefix=$PREFIX \
    	--cross-prefix=$TOOLCHAIN/bin/arm-linux-androideabi- \
    	--enable-pic \
    	--enable-shared \
    	--enable-static \
    	--enable-strip \
    	--disable-cli \
    	--host=arm-linux \
    	--sysroot=$SYSROOT \
    	--extra-cflags="-Os -fpic $ADDI_CFLAGS $EXTRA_CFLAGS" \
    	--extra-ldflags="$ADDI_LDFLAGS $EXTRA_LDFLAGS"
    
    make clean
    make STRIP= -j4 install || exit 1
    複製程式碼

    x264編譯指令碼

  3. 找到x264 repo 的根目錄下的 configure 檔案,找到 echo "SONAME=libx264.so.$API" >> config.mak 改為 echo "SONAME=libx264-$API.so" >> config.mak

  4. 執行編譯指令碼進行編譯,結果在會在 build/ 資料夾下

  5. 接下來編譯 FFmpeg, 先同步 ffmpeg 的 repo

  6. 編寫編譯指令碼:

    #!/bin/bash
    export TMPDIR=/Users/xxx/ffmpegbuilddir/temp/
    NDK=/Users/xxx/Library/Android/sdk/ndk-bundle
    SYSROOT=$NDK/platforms/android-16/arch-arm/
    TOOLCHAIN=$NDK/toolchains/arm-linux-androideabi-4.9/prebuilt/darwin-x86_64
    
    CPU=arm
    PREFIX=/Users/xxx/ffmpegbuilddir/ffmpeg-install-dir/arm/
    ADDI_CFLAGS="-marm"
    
    # 加入x264編譯庫
    EXTRA_DIR=./../path/to/your/x264/repo/build/android
    EXTRA_CFLAGS="-I./${EXTRA_DIR}/include"
    EXTRA_LDFLAGS="-L./${EXTRA_DIR}/lib"
    
    function build_one
    {
    ./configure \
    --prefix=$PREFIX \
    --enable-gpl \
    --enable-libx264 \
    --enable-shared \
    --enable-filter=crop \
    --enable-filter=rotate \
    --enable-filter=scale \
    --disable-encoders \
    --enable-encoder=mpeg4 \
    --enable-encoder=aac \
    --enable-encoder=png \
    --enable-encoder=libx264 \
    --enable-encoder=gif \
    --disable-decoders \
    --enable-decoder=mpeg4 \
    --enable-decoder=h264 \
    --enable-decoder=aac \
    --enable-decoder=gif \
    --enable-parser=h264 \
    --disable-static \
    --disable-doc \
    --disable-ffmpeg \
    --disable-ffplay \
    --disable-ffprobe \
    --disable-ffserver \
    --disable-doc \
    --disable-symver \
    --enable-small \
    --cross-prefix=$TOOLCHAIN/bin/arm-linux-androideabi- \
    --target-os=linux \
    --arch=arm \
    --enable-cross-compile \
    --sysroot=$SYSROOT \
    --extra-cflags="-Os -fpic $ADDI_CFLAGS $EXTRA_CFLAGS" \
    --extra-ldflags="$ADDI_LDFLAGS $EXTRA_LDFLAGS" \
    $ADDITIONAL_CONFIGURE_FLAG
    make clean
    make
    make install
    
    }
    
    build_one
    say "Your building has been completed!"
    複製程式碼

    FFmpeg shared lib 編譯指令碼

  7. 執行編譯指令碼,編譯結果會在 /Users/xxx/ffmpegbuilddir/ffmpeg-install-dir/arm/ 目錄下

  8. 到此,你已經擁有了能在 arm 平臺上 load 的 so 檔案


編寫 jni 來呼叫 ffmpeg

在上面的編譯指令碼中,我們考慮到 so 的輸出大小,configure 中有這麼一行 --disable-ffmpeg,意為不編譯 ffmpeg 的可執行檔案,這樣我們就沒有 ffmpeg 的執行入口,相當於沒有 main()函式。所以,我們需要為這些 so 檔案編寫一個命令執行的入口,這方面也有超多的教程,過程就不深究了,同樣這裡也只記錄一下編譯步驟:

  1. 在你的 Android Studio 工程裡新建一個目錄,例如: jni/

  2. 將 ffmpeg repo 中的 ffmpeg.c、ffmpeg.h、FFmpegNativeHelper.c、cmdutils.c、ffmpeg_opt.c、ffmpeg_filter.c、show_func_wrapper.c 拷貝到 jni

  3. 編寫 makefile:

    ifeq ($(APP_ABI), x86)
    LIB_NAME_PLUS := x86
    else
    LIB_NAME_PLUS := armeabi
    endif
    
    LOCAL_PATH:= $(call my-dir)
    
    include $(CLEAR_VARS)
    LOCAL_MODULE := x264-prebuilt-$(LIB_NAME_PLUS)
    LOCAL_SRC_FILES := prebuilt/$(LIB_NAME_PLUS)/libx264-148.so
    include $(PREBUILT_SHARED_LIBRARY)
    
    include $(CLEAR_VARS)
    LOCAL_MODULE:= avcodec-prebuilt-$(LIB_NAME_PLUS)
    LOCAL_SRC_FILES:= prebuilt/$(LIB_NAME_PLUS)/libavcodec-57.so
    include $(PREBUILT_SHARED_LIBRARY)
    
    include $(CLEAR_VARS)
    LOCAL_MODULE:= avdevice-prebuilt-$(LIB_NAME_PLUS)
    LOCAL_SRC_FILES:= prebuilt/$(LIB_NAME_PLUS)/libavdevice-57.so
    include $(PREBUILT_SHARED_LIBRARY)
    
    include $(CLEAR_VARS)
    LOCAL_MODULE:= avfilter-prebuilt-$(LIB_NAME_PLUS)
    LOCAL_SRC_FILES:= prebuilt/$(LIB_NAME_PLUS)/libavfilter-6.so
    include $(PREBUILT_SHARED_LIBRARY)
    
    include $(CLEAR_VARS)
    LOCAL_MODULE:= avformat-prebuilt-$(LIB_NAME_PLUS)
    LOCAL_SRC_FILES:= prebuilt/$(LIB_NAME_PLUS)/libavformat-57.so
    include $(PREBUILT_SHARED_LIBRARY)
    
    include $(CLEAR_VARS)
    LOCAL_MODULE :=  avutil-prebuilt-$(LIB_NAME_PLUS)
    LOCAL_SRC_FILES := prebuilt/$(LIB_NAME_PLUS)/libavutil-55.so
    include $(PREBUILT_SHARED_LIBRARY)
    
    include $(CLEAR_VARS)
    LOCAL_MODULE := swresample-prebuilt-$(LIB_NAME_PLUS)
    LOCAL_SRC_FILES := prebuilt/$(LIB_NAME_PLUS)/libswresample-2.so
    include $(PREBUILT_SHARED_LIBRARY)
    
    include $(CLEAR_VARS)
    LOCAL_MODULE := swscale-prebuilt-$(LIB_NAME_PLUS)
    LOCAL_SRC_FILES := prebuilt/$(LIB_NAME_PLUS)/libswscale-4.so
    include $(PREBUILT_SHARED_LIBRARY)
    
    include $(CLEAR_VARS)
    LOCAL_MODULE := postproc-prebuilt-$(LIB_NAME_PLUS)
    LOCAL_SRC_FILES := prebuilt/$(LIB_NAME_PLUS)/libpostproc-54.so
    include $(PREBUILT_SHARED_LIBRARY)
    
    include $(CLEAR_VARS)
    
    LOCAL_MODULE := libffmpegjni
    
    ifeq ($(APP_ABI), x86)
    TARGET_ARCH:=x86
    TARGET_ARCH_ABI:=x86
    else
    LOCAL_ARM_MODE := arm
    endif
    
    LOCAL_SRC_FILES := FFmpegNativeHelper.c \
                       cmdutils.c \
                       ffmpeg_opt.c \
                       ffmpeg_filter.c \
                       show_func_wrapper.c
    
    LOCAL_LDLIBS := -L$(SYSROOT)/usr/lib -llog -lz
    
    LOCAL_SHARED_LIBRARIES:= avcodec-prebuilt-$(LIB_NAME_PLUS) \
                             avdevice-prebuilt-$(LIB_NAME_PLUS) \
                             avfilter-prebuilt-$(LIB_NAME_PLUS) \
                             avformat-prebuilt-$(LIB_NAME_PLUS) \
                             avutil-prebuilt-$(LIB_NAME_PLUS) \
                             swresample-prebuilt-$(LIB_NAME_PLUS) \
                             swscale-prebuilt-$(LIB_NAME_PLUS) \
                             postproc-prebuilt-$(LIB_NAME_PLUS) \
                             x264-prebuilt-$(LIB_NAME_PLUS)
    
    LOCAL_C_INCLUDES += -L$(SYSROOT)/usr/include
    LOCAL_C_INCLUDES += $(LOCAL_PATH)/include
    
    ifeq ($(APP_ABI), x86)
    LOCAL_CFLAGS := -DUSE_X86_CONFIG
    else
    LOCAL_CFLAGS := -DUSE_ARM_CONFIG
    endif
    
    include $(BUILD_SHARED_LIBRARY)
    複製程式碼

    Jni 目錄結構

  4. 編寫 java 程式碼,宣告 Java native method

    Java 程式碼

  5. 修改 ffmpeg.c 檔案,繫結 jni 方法名與 ffmpeg.c 的方法名

    繫結方法名稱

  6. 在 jni 目錄下執行 ndk-build APP_ABI=armeabi

    編譯 libffmpegjni.so

  7. 在 libs/armeabi 目錄下得到 libffmpegjni.so

  8. 到這裡,你已經擁有了可以動態 load 的 so 庫,並且可以執行 ffmpeg command 了!


整合 FFmpegMediaMetadataRetriever

相信很多開發者對這個庫都不會陌生FFmpegMediaMetadataRetriever,正如上面所說,原生的 MediaMetadataRetriever 不太好用,這個開源庫被我們用來取預覽幀:給出時間點,返回 bitmap。

然而,這個庫引進來後,聰明的你應該發現了他也編譯了一個 ffmpeg 放在了 aar 中,大小約為4MB。

其實,上面步驟走完後,你應該立即想到“可以直接複用已經編譯好的 ffmpeg”,安裝包立即節約4MB!

同樣的這裡也只記錄步驟:

  1. 將 FFmpegMediaMetadataRetriever repo 下 FFmpegMediaMetadataRetriever/gradle/fmmr-library/library/src/main/jni/metadata 的 .c 、.h、.cpp 檔案都拷貝到上述的 jni 資料夾中

  2. 開啟上面章節我們編寫的 makefile,新增如下程式碼:

    include $(CLEAR_VARS)
    LOCAL_MODULE  := ffmpeg_mediametadataretriever_jni
    
    ifeq ($(APP_ABI), x86)
    TARGET_ARCH:=x86
    TARGET_ARCH_ABI:=x86
    else
    LOCAL_ARM_MODE := arm
    endif
    
    LOCAL_SRC_FILES  :=  wseemann_media_MediaMetadataRetriever.cpp \
                         mediametadataretriever.cpp \
                         ffmpeg_mediametadataretriever.c \
                         ffmpeg_utils.c
    
    LOCAL_LDLIBS := -L$(SYSROOT)/usr/lib -llog -lz
    LOCAL_LDLIBS += -landroid
    LOCAL_LDLIBS += -ljnigraphics
    
    LOCAL_SHARED_LIBRARIES:= avcodec-prebuilt-$(LIB_NAME_PLUS) \
                             avdevice-prebuilt-$(LIB_NAME_PLUS) \
                             avfilter-prebuilt-$(LIB_NAME_PLUS) \
                             avformat-prebuilt-$(LIB_NAME_PLUS) \
                             avutil-prebuilt-$(LIB_NAME_PLUS) \
                             swresample-prebuilt-$(LIB_NAME_PLUS) \
                             swscale-prebuilt-$(LIB_NAME_PLUS) \
                             postproc-prebuilt-$(LIB_NAME_PLUS) \
                             x264-prebuilt-$(LIB_NAME_PLUS)
    
    LOCAL_C_INCLUDES += -L$(SYSROOT)/usr/include
    LOCAL_C_INCLUDES += $(LOCAL_PATH)/include
    
    ifeq ($(APP_ABI), x86)
    LOCAL_CFLAGS := -DUSE_X86_CONFIG
    else
    LOCAL_CFLAGS := -DUSE_ARM_CONFIG
    endif
    
    include $(BUILD_SHARED_LIBRARY)
    複製程式碼
  3. 重新執行 ndk-build APP_ABI=armeabi ,將在 libs/armeabi 下得到 lib ffmpeg_mediametadataretriever_jni.so

    ffmpeg_mediametadataretriever_jni 編譯結果

  4. 將 FFmpegMediaMetadataRetriever repo 中 的 Java 類FFmpegMediaMetadataRetriever.java拷貝到你的專案中,注意要改一下 so load 的過程:

    修改 FFmpegMediaMetadataRetriever.java

  5. 到這裡,你已經或得了可以執行的 FFmpegMediaMetadataRetriever,並且複用了用於視訊編輯模組的 ffmpeg


後續

如果你需要任何幫助,可以參考我的開源庫zhoulujue/ffmpeg-commands-executor-library, fork 的 dxjia/ffmpeg-commands-executor-library 倉庫。

自己完全控制 ffmpeg 有一個很大的好處,就是可以根據需求的變化來調整所引入的 ffmpeg codec 外掛。 例如,需要增加對 gif 編輯的支援,只需要新增一個 encoder 和 decoder 就 OK 了。

編譯並裁剪 FFmpeg 在 Android 上做視訊編輯

相關文章