編譯並裁剪 FFmpeg 在 Android 上作視頻編輯

轉載請聯繫: 微信號: michaelzhoujayhtml

原文請訪問個人博客java


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

最近項目中有須要對視頻進行編輯的需求,整體分析有以下技術上須要實現的點:android

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

2. 須要支持幀預覽,裁剪前須要向用戶展現時間線上的預覽圖;

3. 須要支持截取視頻,給出開始時間和結束時間後截取這兩個時間點之間的視頻段落。
複製代碼

MediaCodec 方案

首先,按照 Android 官方的文檔推薦,固然首推 MediaCodec。git

MediaCodec 編解碼

  1. MediaCodec 尺寸裁減github

    首先用 inputBuffers 讀取幀數據到 outputBuffers,若是須要使用 MediaCodec 裁減尺寸,按照上圖 MediaCodec 的流程以及官方的文檔,須要在處理 output buffer 時將每一幀的數據處理爲 bitmap 而後根據左上角的座標和右下角的座標對圖像進行裁減 Bitmap.createBitmap 實際上這樣裁減的過程仍是在利用 CPU 來進行裁減bash

  2. MediaCodec 取幀微信

    使用MediaMetadataRetrieverapp

  3. MediaCodec 截取ide

    截取實際上在第一步的 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 了。

相關文章
相關標籤/搜索