轉(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 尺寸裁減
首先用 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)行裁減 -
MediaCodec 取幀
-
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)題:
- 首先不是所有設(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....
- 其次,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)單記錄一下編譯的步驟:
同步 x264 的 repo,這里我選擇的是 YIXIA INC 的 mirror.
編寫(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 repo 的根目錄下的 configure 文件,找到
echo "SONAME=libx264.so.$API" >> config.mak改為echo "SONAME=libx264-$API.so" >> config.mak執(zhí)行編譯腳本進(jìn)行編譯,結(jié)果在會(huì)在
build/文件夾下接下來(lái)編譯 FFmpeg, 先同步 ffmpeg 的 repo
編寫(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!"

執(zhí)行編譯腳本,編譯結(jié)果會(huì)在 /Users/xxx/ffmpegbuilddir/ffmpeg-install-dir/arm/ 目錄下
到此,你已經(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ò)程就不深究了,同樣這里也只記錄一下編譯步驟:
在你的 Android Studio 工程里新建一個(gè)目錄,例如: jni/
將 ffmpeg repo 中的 ffmpeg.c、ffmpeg.h、FFmpegNativeHelper.c、cmdutils.c、ffmpeg_opt.c、ffmpeg_filter.c、show_func_wrapper.c 拷貝到 jni
編寫(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)

- 編寫(xiě) java 代碼,聲明 Java native method

- 修改 ffmpeg.c 文件,綁定 jni 方法名與 ffmpeg.c 的方法名

- 在 jni 目錄下執(zhí)行
ndk-build APP_ABI=armeabi

在 libs/armeabi 目錄下得到 libffmpegjni.so
到這里,你已經(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!
同樣的這里也只記錄步驟:
將 FFmpegMediaMetadataRetriever repo 下
FFmpegMediaMetadataRetriever/gradle/fmmr-library/library/src/main/jni/metadata的 .c 、.h、.cpp 文件都拷貝到上述的 jni 文件夾中打開(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)
- 重新執(zhí)行
ndk-build APP_ABI=armeabi,將在libs/armeabi下得到 lib ffmpeg_mediametadataretriever_jni.so

- 將 FFmpegMediaMetadataRetriever repo 中 的 Java 類(lèi)
FFmpegMediaMetadataRetriever.java拷貝到你的項(xiàng)目中,注意要改一下 so load 的過(guò)程:

- 到這里,你已經(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 了。