轉載請聯繫: 微信號: michaelzhoujayhtml
原文請訪問個人博客java
衆所周知,Android 對涉及底層硬件的 API 控制力都比較弱,從其難用的 Camera/Camera二、MediaCodec 等 API 就可見一斑。linux
最近項目中有須要對視頻進行編輯的需求,整體分析有以下技術上須要實現的點:android
1. 須要支持視頻尺寸裁剪,給出左上角和右下角的座標後裁剪兩個點描述的區域;
2. 須要支持幀預覽,裁剪前須要向用戶展現時間線上的預覽圖;
3. 須要支持截取視頻,給出開始時間和結束時間後截取這兩個時間點之間的視頻段落。
複製代碼
首先,按照 Android 官方的文檔推薦,固然首推 MediaCodec。git
MediaCodec 尺寸裁減github
首先用 inputBuffers 讀取幀數據到 outputBuffers,若是須要使用 MediaCodec 裁減尺寸,按照上圖 MediaCodec 的流程以及官方的文檔,須要在處理 output buffer 時將每一幀的數據處理爲 bitmap 而後根據左上角的座標和右下角的座標對圖像進行裁減 Bitmap.createBitmap 實際上這樣裁減的過程仍是在利用 CPU 來進行裁減bash
MediaCodec 取幀微信
MediaCodec 截取ide
截取實際上在第一步的 output 就能夠作了,由於 outputbuffer 裏每一幀的數據就有時間戳信息,MediaCodec.BufferInfo.presentationTimeUs
怎麼樣,看起來這套方案仍是不錯的,可是實際操做下來有幾個嚴重的問題:
若是使用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....
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 還能對其進行裁減。
相信不少開發者都會使用 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 的過程就不贅述,網上有太多教程,這裏簡單記錄一下編譯的步驟:
同步 x264 的 repo,這裏我選擇的是 YIXIA INC 的 mirror.
編寫編譯腳本:
#!/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
執行編譯腳本進行編譯,結果在會在 build/
文件夾下
接下來編譯 FFmpeg, 先同步 ffmpeg 的 repo
編寫編譯腳本:
#!/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!"
複製代碼
執行編譯腳本,編譯結果會在 /Users/xxx/ffmpegbuilddir/ffmpeg-install-dir/arm/ 目錄下
到此,你已經擁有了能在 arm 平臺上 load 的 so 文件
在上面的編譯腳本中,咱們考慮到 so 的輸出大小,configure 中有這麼一行 --disable-ffmpeg
,意爲不編譯 ffmpeg 的可執行文件,這樣咱們就沒有 ffmpeg 的執行入口,至關於沒有 main()
函數。因此,咱們須要爲這些 so 文件編寫一個命令執行的入口,這方面也有超多的教程,過程就不深究了,一樣這裏也只記錄一下編譯步驟:
在你的 Android Studio 工程裏新建一個目錄,例如: jni/
將 ffmpeg repo 中的 ffmpeg.c、ffmpeg.h、FFmpegNativeHelper.c、cmdutils.c、ffmpeg_opt.c、ffmpeg_filter.c、show_func_wrapper.c 拷貝到 jni
編寫 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)
複製代碼
編寫 java 代碼,聲明 Java native method
修改 ffmpeg.c 文件,綁定 jni 方法名與 ffmpeg.c 的方法名
在 jni 目錄下執行 ndk-build APP_ABI=armeabi
在 libs/armeabi 目錄下獲得 libffmpegjni.so
到這裏,你已經擁有了能夠動態 load 的 so 庫,而且能夠執行 ffmpeg command 了!
相信不少開發者對這個庫都不會陌生FFmpegMediaMetadataRetriever,正如上面所說,原生的 MediaMetadataRetriever 不太好用,這個開源庫被咱們用來取預覽幀:給出時間點,返回 bitmap。
然而,這個庫引進來後,聰明的你應該發現了他也編譯了一個 ffmpeg 放在了 aar 中,大小約爲4MB。
其實,上面步驟走完後,你應該當即想到「能夠直接複用已經編譯好的 ffmpeg」,安裝包當即節約4MB!
一樣的這裏也只記錄步驟:
將 FFmpegMediaMetadataRetriever repo 下 FFmpegMediaMetadataRetriever/gradle/fmmr-library/library/src/main/jni/metadata
的 .c 、.h、.cpp 文件都拷貝到上述的 jni 文件夾中
打開上面章節咱們編寫的 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)
複製代碼
從新執行 ndk-build APP_ABI=armeabi
,將在 libs/armeabi
下獲得 lib ffmpeg_mediametadataretriever_jni.so
將 FFmpegMediaMetadataRetriever repo 中 的 Java 類FFmpegMediaMetadataRetriever.java
拷貝到你的項目中,注意要改一下 so load 的過程:
到這裏,你已經或得了能夠運行的 FFmpegMediaMetadataRetriever,而且複用了用於視頻編輯模塊的 ffmpeg
若是你須要任何幫助,能夠參考個人開源庫zhoulujue/ffmpeg-commands-executor-library, fork 的 dxjia/ffmpeg-commands-executor-library 倉庫。
本身徹底控制 ffmpeg 有一個很大的好處,就是能夠根據需求的變化來調整所引入的 ffmpeg codec 插件。 例如,須要增長對 gif 編輯的支持,只須要添加一個 encoder 和 decoder 就 OK 了。