利用 FFmpeg 在 Android 上做视频编辑

转载请联系: 微信号: 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编译脚本
  1. 找到x264 repo 的根目录下的 configure 文件,找到 echo "SONAME=libx264.so.$API" >> config.mak 改为 echo "SONAME=libx264-$API.so" >> config.mak

  2. 执行编译脚本进行编译,结果在会在 build/ 文件夹下

  3. 接下来编译 FFmpeg, 先同步 ffmpeg 的 repo

  4. 编写编译脚本:

#!/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 编译脚本
  1. 执行编译脚本,编译结果会在 /Users/xxx/ffmpegbuilddir/ffmpeg-install-dir/arm/ 目录下

  2. 到此,你已经拥有了能在 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 目录结构
  1. 编写 java 代码,声明 Java native method
Java 代码
  1. 修改 ffmpeg.c 文件,绑定 jni 方法名与 ffmpeg.c 的方法名
绑定方法名称
  1. 在 jni 目录下执行 ndk-build APP_ABI=armeabi
编译 libffmpegjni.so
  1. 在 libs/armeabi 目录下得到 libffmpegjni.so

  2. 到这里,你已经拥有了可以动态 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)
  1. 重新执行 ndk-build APP_ABI=armeabi ,将在 libs/armeabi 下得到 lib ffmpeg_mediametadataretriever_jni.so
ffmpeg_mediametadataretriever_jni 编译结果
  1. 将 FFmpegMediaMetadataRetriever repo 中 的 Java 类FFmpegMediaMetadataRetriever.java拷贝到你的项目中,注意要改一下 so load 的过程:
修改 FFmpegMediaMetadataRetriever.java
  1. 到这里,你已经或得了可以运行的 FFmpegMediaMetadataRetriever,并且复用了用于视频编辑模块的 ffmpeg

后续

如果你需要任何帮助,可以参考我的开源库zhoulujue/ffmpeg-commands-executor-library, fork 的 dxjia/ffmpeg-commands-executor-library 仓库。

自己完全控制 ffmpeg 有一个很大的好处,就是可以根据需求的变化来调整所引入的 ffmpeg codec 插件。
例如,需要增加对 gif 编辑的支持,只需要添加一个 encoder 和 decoder 就 OK 了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,319评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,801评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,567评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,156评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,019评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,090评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,500评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,192评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,474评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,566评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,338评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,212评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,572评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,890评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,169评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,478评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,661评论 2 335

推荐阅读更多精彩内容