利用 FFmpeg 在 Android 上做視頻編輯

轉(zhuǎn)載請(qǐng)聯(lián)系: 微信號(hào): michaelzhoujay
原文請(qǐng)?jiān)L問(wèn)我的博客


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

最近項(xiàng)目中有需要對(duì)視頻進(jìn)行編輯的需求,總體分析有如下技術(shù)上需要實(shí)現(xiàn)的點(diǎn):

    1.需要支持視頻尺寸裁剪,給出左上角和右下角的坐標(biāo)后裁剪兩個(gè)點(diǎn)描述的區(qū)域;

    2.需要支持幀預(yù)覽,裁剪前需要向用戶(hù)展示時(shí)間線(xiàn)上的預(yù)覽圖;

    3.需要支持截取視頻,給出開(kāi)始時(shí)間和結(jié)束時(shí)間后截取這兩個(gè)時(shí)間點(diǎn)之間的視頻段落。

MediaCodec 方案

首先,按照 Android 官方的文檔推薦,當(dāng)然首推 MediaCodec。

MediaCodec 編解碼
  1. MediaCodec 尺寸裁減

    首先用 inputBuffers 讀取幀數(shù)據(jù)到 outputBuffers,如果需要使用 MediaCodec 裁減尺寸,按照上圖 MediaCodec 的流程以及官方的文檔,需要在處理 output buffer 時(shí)將每一幀的數(shù)據(jù)處理為 bitmap 然后根據(jù)左上角的坐標(biāo)和右下角的坐標(biāo)對(duì)圖像進(jìn)行裁減 Bitmap.createBitmap
    實(shí)際上這樣裁減的過(guò)程還是在利用 CPU 來(lái)進(jìn)行裁減

  2. MediaCodec 取幀

    使用MediaMetadataRetriever

  3. MediaCodec 截取

    截取實(shí)際上在第一步的 output 就可以做了,因?yàn)?outputbuffer 里每一幀的數(shù)據(jù)就有時(shí)間戳信息,MediaCodec.BufferInfo.presentationTimeUs


MediaCodec 的問(wèn)題

怎么樣,看起來(lái)這套方案還是不錯(cuò)的,但是實(shí)際操作下來(lái)有幾個(gè)嚴(yán)重的問(wèn)題:

  1. 首先不是所有設(shè)備的 DSP 芯片都支持你需要的 codec 對(duì)應(yīng)的編碼器,而且編碼器支持特性相當(dāng)有限:
    具體參考微信團(tuán)隊(duì)對(duì) MediaCodec 編碼器的研究

如果使用MediaCodec來(lái)編碼H264視頻流,對(duì)于H264格式來(lái)說(shuō),會(huì)有一些針對(duì)壓縮率以及碼率相關(guān)的視頻質(zhì)量設(shè)置,典型的諸如Profile(baseline, main, high),Profile Level, Bitrate mode(CBR, CQ, VBR),合理配置這些參數(shù)可以讓我們?cè)谕鹊拇a率下,獲得更高的壓縮率,從而提升視頻的質(zhì)量,Android也提供了對(duì)應(yīng)的API進(jìn)行設(shè)置,可以設(shè)置到MediaFormat中這些設(shè)置項(xiàng):

MediaFormat.KEY_BITRATE_MODE

MediaFormat.KEY_PROFILE

MediaFormat.KEY_LEVEL

但問(wèn)題是,對(duì)于Profile,Level, Bitrate mode這些設(shè)置,在大部分手機(jī)上都是不支持的,即使是設(shè)置了最終也不會(huì)生效,例如設(shè)置了Profile為high,最后出來(lái)的視頻依然還會(huì)是Baseline....

  1. 其次,MediaMetadataRetriever 實(shí)測(cè)也不太好用,在某些機(jī)型上會(huì)出現(xiàn)取不到幀的情況。
    于是決定棄用 MediaCodec 轉(zhuǎn)投如日中天的 FFmpeg。

FFmpeg

FFmpeg 由于其豐富的 codec 插件,詳細(xì)的文檔說(shuō)明,并且與其調(diào)試復(fù)雜量大的編解碼代碼(是的,用 MediaCodec 實(shí)現(xiàn)起來(lái)十分啰嗦和繁瑣)還是不如調(diào)試一行 ffmpeg 命令來(lái)的簡(jiǎn)單。

利用 FFmpeg 做視頻編輯大家一般都會(huì)去參考這個(gè) repo ,但是他的 asset 里面的 ffmpeg 大小高達(dá) 18MB,即使壓縮進(jìn) APK 包里也會(huì)達(dá)到 9MB。對(duì) APK 大小敏感的開(kāi)發(fā)者肯定頗有微詞。
ffmpeg-android-java 的原理很簡(jiǎn)單,交叉編譯好可執(zhí)行的 ffmpeg 二進(jìn)制文件放到 asset 里,安裝后釋放二進(jìn)制文件到 /data/data/ 里,用 Shell command 的形式去執(zhí)行這個(gè)文件,好處是沒(méi)有任何依賴(lài)(依賴(lài)全打進(jìn)二進(jìn)制了),穩(wěn)定可靠(不需要?jiǎng)討B(tài)加載)。
壞處就很明顯了,因?yàn)槭嵌M(jìn)制文件,所以 size 會(huì)很大。

于是,果斷放棄這種方式,轉(zhuǎn)而編譯 ffmpeg 的 so 庫(kù),動(dòng)態(tài)加載然后執(zhí)行命令。聽(tīng)起來(lái)不錯(cuò),對(duì)不對(duì)?動(dòng)態(tài)庫(kù)的大小肯定比 ffmpeg-android-java 的 executable 要小多了,而且自己編譯 ffmpeg 還能對(duì)其進(jìn)行裁減。


交叉編譯 FFmpeg 及 x264

相信很多開(kāi)發(fā)者都會(huì)使用 ijkplayer,ijkplayer 底層也用到了 ffmpeg,ijk使用的是 so 庫(kù)的形式,libffmpeg.so。所以最理想的狀態(tài)是,重新編譯一個(gè)公共的 libffmpeg.so,這個(gè) libffmpeg.so 即有 ijk 需要的 decoders 和視頻編輯模塊需要的 encoders。但是一旦 ijk 或者 ffmpeg 有升級(jí)就會(huì)很麻煩,因?yàn)榈弥匦戮幾g一次 ffmpeg,而且還得 fork ijkplayer,然后每當(dāng) ijk 更新的時(shí)候?qū)?ijkplayer master 合并到你 fork 分支,視頻播放又是很常用的模塊,很難做到“無(wú)痛”升級(jí)。

如果不動(dòng) ijk 的 ffmpeg,單獨(dú)為視頻編輯模塊編譯一個(gè) ffmpeg.so ,與視頻播放模塊隔離開(kāi),這樣就可以無(wú)痛升級(jí) ijk 依賴(lài) ffmpeg 的視頻播放庫(kù)了。但是,問(wèn)題來(lái)了,如果存在兩個(gè) ffmpeg 的話(huà)不可避免的會(huì)存在冗余。所以編譯視頻編輯模塊的 ffmpeg 時(shí),要裁剪他的 encoders 和 decoders 盡量做到兩個(gè) ffmpeg 模塊是正交的就 ok了。

交叉編譯 FFmpeg 的過(guò)程就不贅述,網(wǎng)上有太多教程,這里簡(jiǎn)單記錄一下編譯的步驟:

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

  2. 編寫(xiě)編譯腳本:

#!/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編譯腳本
  1. 找到x264 repo 的根目錄下的 configure 文件,找到 echo "SONAME=libx264.so.$API" >> config.mak 改為 echo "SONAME=libx264-$API.so" >> config.mak

  2. 執(zhí)行編譯腳本進(jìn)行編譯,結(jié)果在會(huì)在 build/ 文件夾下

  3. 接下來(lái)編譯 FFmpeg, 先同步 ffmpeg 的 repo

  4. 編寫(xiě)編譯腳本:

#!/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編譯庫(kù)
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 編譯腳本
  1. 執(zhí)行編譯腳本,編譯結(jié)果會(huì)在 /Users/xxx/ffmpegbuilddir/ffmpeg-install-dir/arm/ 目錄下

  2. 到此,你已經(jīng)擁有了能在 arm 平臺(tái)上 load 的 so 文件


編寫(xiě) jni 來(lái)調(diào)用 ffmpeg

在上面的編譯腳本中,我們考慮到 so 的輸出大小,configure 中有這么一行 --disable-ffmpeg,意為不編譯 ffmpeg 的可執(zhí)行文件,這樣我們就沒(méi)有 ffmpeg 的執(zhí)行入口,相當(dāng)于沒(méi)有 main()函數(shù)。所以,我們需要為這些 so 文件編寫(xiě)一個(gè)命令執(zhí)行的入口,這方面也有超多的教程,過(guò)程就不深究了,同樣這里也只記錄一下編譯步驟:

  1. 在你的 Android Studio 工程里新建一個(gè)目錄,例如: jni/

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

  3. 編寫(xiě) 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 目錄結(jié)構(gòu)
  1. 編寫(xiě) java 代碼,聲明 Java native method
Java 代碼
  1. 修改 ffmpeg.c 文件,綁定 jni 方法名與 ffmpeg.c 的方法名
綁定方法名稱(chēng)
  1. 在 jni 目錄下執(zhí)行 ndk-build APP_ABI=armeabi
編譯 libffmpegjni.so
  1. 在 libs/armeabi 目錄下得到 libffmpegjni.so

  2. 到這里,你已經(jīng)擁有了可以動(dòng)態(tài) load 的 so 庫(kù),并且可以執(zhí)行 ffmpeg command 了!


集成 FFmpegMediaMetadataRetriever

相信很多開(kāi)發(fā)者對(duì)這個(gè)庫(kù)都不會(huì)陌生FFmpegMediaMetadataRetriever,正如上面所說(shuō),原生的 MediaMetadataRetriever 不太好用,這個(gè)開(kāi)源庫(kù)被我們用來(lái)取預(yù)覽幀:給出時(shí)間點(diǎn),返回 bitmap。

然而,這個(gè)庫(kù)引進(jìn)來(lái)后,聰明的你應(yīng)該發(fā)現(xiàn)了他也編譯了一個(gè) ffmpeg 放在了 aar 中,大小約為4MB。

其實(shí),上面步驟走完后,你應(yīng)該立即想到“可以直接復(fù)用已經(jīng)編譯好的 ffmpeg”,安裝包立即節(jié)約4MB!

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

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

  2. 打開(kāi)上面章節(jié)我們編寫(xiě)的 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)
  1. 重新執(zhí)行 ndk-build APP_ABI=armeabi ,將在 libs/armeabi 下得到 lib ffmpeg_mediametadataretriever_jni.so
ffmpeg_mediametadataretriever_jni 編譯結(jié)果
  1. 將 FFmpegMediaMetadataRetriever repo 中 的 Java 類(lèi)FFmpegMediaMetadataRetriever.java拷貝到你的項(xiàng)目中,注意要改一下 so load 的過(guò)程:
修改 FFmpegMediaMetadataRetriever.java
  1. 到這里,你已經(jīng)或得了可以運(yùn)行的 FFmpegMediaMetadataRetriever,并且復(fù)用了用于視頻編輯模塊的 ffmpeg

后續(xù)

如果你需要任何幫助,可以參考我的開(kāi)源庫(kù)zhoulujue/ffmpeg-commands-executor-library, fork 的 dxjia/ffmpeg-commands-executor-library 倉(cāng)庫(kù)。

自己完全控制 ffmpeg 有一個(gè)很大的好處,就是可以根據(jù)需求的變化來(lái)調(diào)整所引入的 ffmpeg codec 插件。
例如,需要增加對(duì) gif 編輯的支持,只需要添加一個(gè) encoder 和 decoder 就 OK 了。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容