前几天项目需要压缩视频,Github
上找了许多库,要么就是太大,要么就是质量不高,其实我只需要压缩视频,最好的方案还是定制编译一个 FFmpeg
给 Android
用。
本项目使用
FFmpeg
和libx264
(一个第三方的视频编码器) 来编译出可以在Android
上使用的动态库
一、下载源码
创建一个叫 FFmpegAndroid
的目录,下载 libx264
的源码和ffmpeg
的源码,然后在 FFmpegAndroid
文件夹下建立一个 bulid
文件夹,用于存放编译脚本和输出
--- FFmpegAndroid |-- ffmpeg |-- x264 |-- build
二、编译 FFmpeg
编译 x264 编码器
先在 build
文件夹下建立 setting.sh
, 用于申明一些公用的环境变量,比如 $NDK
、$CPU
...
setting.sh
# ndk 环境 NDK=$HOME/Library/Android/sdk/ndk-bundle SYSROOT=$NDK/platforms/android-14/arch-arm/ TOOLCHAIN=$NDK/toolchains/arm-linux-androideabi-4.9/prebuilt/darwin-x86_64 # cpu 架构平台,若要编译 x86 则指定 x86 CPU=armv7-a
然后建立 libx264
的编译脚本 build_x264.sh
,libx264
是一个开源的H.264编码器,据说是最好的视频有损编码器。ffmpeg
默认不自带,但是支持 x264
作为第三方编码器编译。
build_x264.sh
./config 内的# 注释必须在运行的时候去掉
#!/bin/bash # 引入需要的环境变量 . setting.sh # 输出下看看对不对,可以去掉,这里调试用 echo "use toolchain: $TOOLCHAIN" echo "use system root: $SYSROOT" # 输出文件的前缀,也就是指定最后静态库输出到那里 PREFIX=$(pwd)/lib/x264/$CPU # 优化参数 OPTIMIZE_CFLAGS="-mfloat-abi=softfp -mfpu=vfp -marm -march=$CPU " ADDI_CFLAGS="" ADDI_LDFLAGS="" # 因为当前目录在 build 目录,需要切换到 x264 去执行 config cd ../x264 function build_x264 { ./configure \ --prefix=$PREFIX \ # 不编译动态库 --disable-shared \ --disable-asm \ # 编译静态库 --enable-static \ --enable-pic \ --enable-strip \ --host=arm-linux-androideabi \ --cross-prefix=$TOOLCHAIN/bin/arm-linux-androideabi- \ --sysroot=$SYSROOT \ --extra-cflags="-Os -fpic $ADDI_CFLAGS $OPTIMIZE_CFLAGS" \ --extra-ldflags="$ADDI_LDFLAGS" \ $ADDITIONAL_CONFIGURE_FLAG make clean make -j4 make install } # 执行编译指令 build_x264
写完之后就可以编译 x264
库了,编译之前还有一点要注意的是,默认编译出来的文件后缀并不是 *.so
,这 Android
是识别不了的,需要对 x264
源码里面的 config
做如下修改:
将
echo "SOSUFFIX=so" >> config.mak echo "SONAME=libx264.so.$API" >> config.mak echo "SOFLAGS=-shared -Wl,-soname,\$(SONAME) $SOFLAGS" >> config.mak
修改成
echo "SOSUFFIX=so" >> config.mak echo "SONAME=libx264-$API.so" >> config.mak echo "SOFLAGS=-shared -Wl,-soname,\$(SONAME) $SOFLAGS" >> config.mak
别忘了给
build_x264.sh
和setting.sh
赋予可执行权限 (chmod +x build_x264.sh setting.sh
)
修改完后就可以执行脚本命令了
./build_x264.sh
等待一段时间后,build
文件夹目录下应该有个 lib
目录(build 脚本里面 prefix 指定的目录),里面存放了 x264
的静态库
这里为什么编译成静态库而不是动态库呢?静态库可以把内容编译到待会儿要编译
ffmpeg
的so库里去,不需要单独加载libx264.so
了,如果你硬要编译成动态库也可以,加载ffmpeg.so
的时候加载libx264.so
就可以
至此,x264
编码器编译完毕
编译 FFmpeg
同样在 build
文件夹下建立编译脚本 build_ffmpeg.sh
,编译 ffmpeg
比编译 x264
略微麻烦点,首先肯定不能全功能编译,那还不如直接去网上找一个编译好的,要自己定制哪些组件需要,哪些组件不需要
FFmpeg它主要含有以下几个核心库:
libavcodec-提供了更加全面的编解码实现的合集
libavformat-提供了更加全面的音视频容器格式的封装和解析以及所支持的协议
libavutil-提供了一些公共函数
libavfilter-提供音视频的过滤器,如视频加水印、音频变声等
libavdevice-提供支持众多设备数据的输入与输出,如读取摄像头数据、屏幕录制
libswresample,libavresample-提供音频的重采样工具
libswscale-提供对视频图像进行色彩转换、缩放以及像素格式转换,如图像的YUV转换
libpostproc-多媒体后处理器
如果不修改什么配置,直接编译的话,我发现 libavcodec.so
有 7.8MB,我可以在这方面下手,指定 decoder
和 encoder
,因为我需要的是视频压缩,所以编码器(encoder
)我就只需要 x264
(视频编码) 和 aac
(音频编码),至于解码器,挑几个常用的就可以了
查看编码器和解码器种类,可以通过 ./config --list-decoders 或 ./config --list-encoers 命令实现(ffmpeg目录下)
./config 内的# 注释必须在运行的时候去掉
#!/bin/bash # 导入环境变量 . setting.sh # 输出,调试用 echo "use toolchain: $TOOLCHAIN" echo "use system root: $SYSROOT" # x264库所在的位置,ffmpeg 需要链接 x264 LIB_DIR=$(pwd)/lib; # ffmpeg编译输出前缀 PREFIX=$LIB_DIR/ffmpeg/$CPU # x264的头文件地址 INC="$LIB_DIR/x264/$CPU/include" # x264的静态库地址 LIB="$LIB_DIR/x264/$CPU/lib" # 输出调试 echo "include dir: $INC" echo "lib dir: $LIB" # 编译优化参数 FF_EXTRA_CFLAGS="-march=$CPU -mfpu=vfpv3-d16 -mfloat-abi=softfp -mthumb" # 编译优化参数,-I$INC 指定 x264 头文件路径 FF_CFLAGS="-O3 -Wall -pipe \ -ffast-math \ -fstrict-aliasing -Werror=strict-aliasing \ -Wno-psabi -Wa,--noexecstack \ -DANDROID \ -I$INC" cd ../ffmpeg function build_arm { ./configure \ # 这里需要启动生成动态库 --enable-shared \ # 静态库就不生成了 --disable-static \ --disable-ffmpeg \ --disable-ffplay \ --disable-ffprobe \ --disable-ffserver \ --disable-symver \ # 禁用全部的编码 --disable-encoders \ # 启用 x264 这个库 --enable-libx264 \ # 启用 x264 编码 --enable-encoder=libx264 \ # 启用 aac 音频编码 --enable-encoder=aac \ # 启用几个图片编码,由于生成视频预览 --enable-encoder=mjpeg \ --enable-encoder=png \ # 禁用全部的解码器 --disable-decoders \ # 启用几个常用的解码 --enable-decoder=aac \ --enable-decoder=aac_latm \ --enable-decoder=h264 \ --enable-decoder=mpeg4 \ --enable-decoder=mjpeg \ --enable-decoder=png \ --disable-demuxers \ --enable-demuxer=image2 \ --enable-demuxer=h264 \ --enable-demuxer=aac \ --enable-demuxer=avi \ --enable-demuxer=mpc \ --enable-demuxer=mov \ --disable-parsers \ --enable-parser=aac \ --enable-parser=ac3 \ --enable-parser=h264 \ # 这几个库应该需要,没怎么测试,反正很小就加上了 --enable-avresample \ --enable-small \ --enable-avfilter \ # 这两个是链接 x264 静态库需要 --enable-gpl \ --enable-yasm \ # 编译输出前缀 --prefix=$PREFIX \ --cross-prefix=$TOOLCHAIN/bin/arm-linux-androideabi- \ --target-os=linux \ --arch=arm \ --enable-cross-compile \ --sysroot=$SYSROOT \ --extra-cflags="$FF_CFLAGS $FF_EXTRA_CFLAGS" \ # 指定 x264 静态库位置 --extra-ldflags="-Wl,-L$LIB" make clean make -j16 make install } build_arm
这次编译不用静态库的原因是,静态库链接是有顺序要求的,这里模块太多,我也不知道哪个模块依赖哪个模块,所以直接上动态库
脚本写完后,就可以 run 了,编译时间有点久,可以学学我的某个同学,一编译就起来泡泡妹子,有说有笑。
编译完成后你的目录应该是下面那个样子:
--- FFmpegAndroid |-- ffmpeg |-- x264 |-- build |-- build_ffmpeg.sh |-- build_x264.sh |-- lib |-- ffmpeg/armv7-a |-- include (ffmpeg so库的头文件) |-- lib (ffmpeg so库) |-- libavcodec-57.so |-- libavdevice-57.so |-- libavcodec-57.so |-- libavfilter-6.so |-- libavformat-57.so |-- libavresample-3.so |-- libavutil-55.so |-- libpostproc-54.so |-- libresample-2.so |-- libswscale-4.so |-- x264 (x264的静态库和头文件)
后面的版本号不一样没关系,这由
ffmpeg
版本决定的
库编译完了,这些 so 库就是在 Android 可用的动态库,接下来就可以准备 JNI 编程了
三、在 Android 里使用 FFmpeg
前面已经把 FFmpeg
各个核心库编译出来了,但是我肯定不会在里面直接用核心库内的函数来用,ffmpeg
本来是一个在 pc 端的命令,命令里面可以填写各种参数,比如 ffmpeg -i a.mp4 -c:v x264 -c:a aac b.mp4
,就是把 a.mp4 用 x264
(视频)、aac
(音频) 编码成 b.mp4
。
ffmpeg
是由 ffmpeg.c
编译出来的,想要在 Android 里面用 ffmpeg
命令,只要修改 ffmpeg.c
里面的 main 函数,比如修改成 int run_ffmpeg_command(int args, char **argv)
,然后用 JNI 暴露给 java 调用,就可以在 Android 使用 ffmpeg
命令了
在 FFmpegAndroid 建立一个 Android 工程,然后新建一个 ffmpeg 的 lib module
对于 NDK 开发,AndroidStudio 2.2 以后就有较好的支持,直接修改支持库的 build.gradle 文件
apply plugin: 'com.android.library'android { ... defaultConfig { ... // 启用 c++ 支持 externalNativeBuild { cmake { cppFlags "-std=c++11" } ndk { abiFilters "armeabi-v7a" } } } ... // 指定 CMakeList 文件 externalNativeBuild { cmake { path "CMakeLists.txt" } }}
这样 lib module 就支持 c++ 了,方便吧!比以前的 Android.mk 不知道方便多少
然后在模块的 src/main
下面新建一个 cpp
目录,用于存放 c++ 代码,从ffmpeg
拷贝以下文件:
cmdutils_common_opts.h cmdutils.c cmdutils.h config.h ffmpeg_filter.c ffmpeg_opt.c ffmpeg-lib.c ffmpeg.c ffmpeg.h
然后在 CMakeList.txt 里面配置这些文件,好让 AndroidStudio 认识它们
# For more information about using CMake with Android Studio, read the # documentation: https://d.android.com/studio/projects/add-native-code.html # Sets the minimum version of CMake required to build the native library. cmake_minimum_required(VERSION 3.4.1) # Creates and names a library, sets it as either STATIC # or SHARED, and provides the relative paths to its source code. # You can define multiple libraries, and CMake builds them for you. # Gradle automatically packages shared libraries with your APK. add_library( # Sets the name of the library. ffmpeg-lib # Sets the library as a shared library. SHARED # Provides a relative path to your source file(s). src/main/cpp/cmdutils.c src/main/cpp/ffmpeg.c src/main/cpp/ffmpeg_filter.c src/main/cpp/ffmpeg_opt.c # 此文件是用于暴露 ffmpeg.c 的 main 函数用 src/main/cpp/ffmpeg-lib.c) set(FFMPEG_LIB_DIR /Users/qigengxin/Documents/Github/FFmpegAndroid/build/lib/ffmpeg/armv7-a/lib) add_library( avcodec SHARED IMPORTED ) set_target_properties( avcodec PROPERTIES IMPORTED_LOCATION ${FFMPEG_LIB_DIR}/libavcodec-57.so ) add_library( avdevice SHARED IMPORTED ) set_target_properties( avdevice PROPERTIES IMPORTED_LOCATION ${FFMPEG_LIB_DIR}/libavdevice-57.so ) add_library( avfilter SHARED IMPORTED ) set_target_properties( avfilter PROPERTIES IMPORTED_LOCATION ${FFMPEG_LIB_DIR}/libavfilter-6.so ) add_library( avformat SHARED IMPORTED ) set_target_properties( avformat PROPERTIES IMPORTED_LOCATION ${FFMPEG_LIB_DIR}/libavformat-57.so ) add_library( avresample SHARED IMPORTED ) set_target_properties( avresample PROPERTIES IMPORTED_LOCATION ${FFMPEG_LIB_DIR}/libavresample-3.so ) add_library( avutil SHARED IMPORTED ) set_target_properties( avutil PROPERTIES IMPORTED_LOCATION ${FFMPEG_LIB_DIR}/libavutil-55.so ) add_library( postproc SHARED IMPORTED ) set_target_properties( postproc PROPERTIES IMPORTED_LOCATION ${FFMPEG_LIB_DIR}/libpostproc-54.so ) add_library( swresample SHARED IMPORTED ) set_target_properties( swresample PROPERTIES IMPORTED_LOCATION ${FFMPEG_LIB_DIR}/libswresample-2.so ) add_library( swscale SHARED IMPORTED ) set_target_properties( swscale PROPERTIES IMPORTED_LOCATION ${FFMPEG_LIB_DIR}/libswscale-4.so ) include_directories( ../../ffmpeg ) # Searches for a specified prebuilt library and stores the path as a # variable. Because CMake includes system libraries in the search path by # default, you only need to specify the name of the public NDK library # you want to add. CMake verifies that the library exists before # completing its build. find_library( # Sets the name of the path variable. log-lib # Specifies the name of the NDK library that # you want CMake to locate. log ) # Specifies libraries CMake should link to your target library. You # can link multiple libraries, such as libraries you define in this # build script, prebuilt third-party libraries, or system libraries. target_link_libraries( # Specifies the target library. ffmpeg-lib avcodec avutil avfilter swscale swresample avresample postproc avformat avdevice # Links the target library to the log library # included in the NDK. ${log-lib} )
刷新下 gradle,就可以写 c++ 代码了。先看下 ffmpeg.c
这个文件,原先的指令其实调用的就是 main 函数,我们先把 main 函数改成自己自定义的函数 run_ffmpeg_command
:
int run_ffmpeg_command(int argc, char **argv){ ...}
改了以后,我们就可以调用 run_ffmpeg_command
然后传入参数,相当于在 pc 执行 ffmpeg
命令。不过现在还不能执行,这是个坑点,仔细看 run_ffmpeg_command
函数,在程序结束的时候,或者中途出现错误的时候,都会调用 exit_program(int)
,这个函数:
int run_ffmpeg_command(int argc, char **argv){ ... /* parse options and open all input/output files */ ret = ffmpeg_parse_options(argc, argv); if (ret < 0){ exit_program(1); } ... if (nb_output_files <= 0 && nb_input_files == 0) { show_usage(); av_log(NULL, AV_LOG_WARNING, "Use -h to get full help or, even better, run 'man %s'\n", program_name); exit_program(1); } exit_program(received_nb_signals ? 255 : main_return_code); return main_return_code;}
exit_program(int)
函数是什么,跳过去看一下发现里面就是清理资源然后 exit(int)
,这里就要注意这个 exit 函数了,除非我们是多进程方式调用 run_ffmpeg_command
,如果我们在 app 的进程调用,执行了 exit 就会结束 app 的进程!
这不是我想看到的,最好的方法是另开一个进程调用,但是这样就涉及到了进程间的通信问题,麻烦,不想写!反正只是跑一个压缩指令嘛,直接改 ffmpeg.c
,首先把 exit(int)
函数给注释掉,然后返回一个 code,run_ffmpeg_command
函数里面只要涉及到 exit_program(int)
函数调用的地方都写成 return exit_program(int)
,不过要注意,有如下几个坑点:
修改 ffmpeg.c 坑点一
调试的时候发现 return exit_program(int);
语句并不会结束当前函数并返回,而是继续往下执行了,当时一脸楞逼,我艹!!这是什么鬼??为什么我 return 了没有用?找了半天后才发现是 exit_program(int)
这个函数声明的锅!看下面这个函数的声明:
/** * Wraps exit with a program-specific cleanup routine. */int exit_program(int ret) av_noreturn;
函数后面有个奇怪的 av_noreturn
声明,网上查了一下才知道,这个是给编译器的注解,这货的锅,去掉就好了。
修改 ffmpeg.c 坑点二
其实 exit_program(int)
这个函数不只是在 run_ffmpeg_command
里面调用,其它各种函数里面都有,如果都要修改的话必须一层一层的 return (C语言里面没有异常啊),很麻烦,但是如果没有改好的话就很容易 crash,这是个要解决的问题,首先 run_ffmpeg_command
里面的 exit_program
都要改成 return 方式
然后因为最终目的是压缩视频,参数集是固定的,所以不用考虑编码不支持,或参数匹配不到的情况,只需要考虑文件读写的问题,就是输入文件不存在的时候,或者输出路径不合法的时候,不能让程序异常退出,而是返回错误码,这个需要改 ffmpeg_opt.c
这个文件
ffmpeg_opt.c
static int open_files(OptionGroupList *l, const char *inout, int (*open_file)(OptionsContext*, const char*)){ ...}static int open_input_file(OptionsContext *o, const char *filename){ ...}static int open_outout_file(OptionsContext *o, const char *filename){ ...}static int init_output_filter(OutputFilter *ofilter, OptionsContext *o, AVFormatContext *oc){ ...}
目前我项目中就只改了这几个函数内的 exit_program
,测试可行,也可以参考本项目的代码,链接在文末
最后就是暴露 run_ffmpeg_command
方法给 java 调用了,这个和普通的 JNI 编程一样,建一个 native 的方法,创建 cpp 代码。。。没啥东西,直接上代码
FFmpegNativeBridge
public class FFmpegNativeBridge { static { System.loadLibrary("ffmpeg-lib"); } /** * 执行指令 * @param command * @return 命令返回结果 */ public static native int runCommand(String[] command);}
ffmpeg-lib.c
#include <jni.h>#include "ffmpeg.h"JNIEXPORT jint JNICALLJava_org_voiddog_ffmpeg_FFmpegNativeBridge_runCommand(JNIEnv *env, jclass type, jobjectArray command) { int argc = (*env)->GetArrayLength(env, command); char *argv[argc]; jstring jsArray[argc]; int i; for (i = 0; i < argc; i++) { jsArray[i] = (jstring) (*env)->GetObjectArrayElement(env, command, i); argv[i] = (char *) (*env)->GetStringUTFChars(env, jsArray[i], 0); } int ret = run_ffmpeg_command(argc,argv); for (i = 0; i < argc; ++i) { (*env)->ReleaseStringUTFChars(env, jsArray[i], argv[i]); } return ret;}
运行前先需要把 ffmpeg
编译出来的一堆 so
库放到 jniLibs
内,不然运行的时候会出现动态库无法加载的异常。最后就可以在 Android
内用 ffmpeg
的命令了:
int ret = FFmpegNativeBridge.runCommand(new String[]{"ffmpeg", "-i", "/storage/emulated/0/DCIM/Camera/VID_20170527_175421.mp4", "-y", "-c:v", "libx264", "-c:a", "aac", "-vf", "scale=480:-2", "-preset", "ultrafast", "-crf", "28", "-b:a", "128k", "/storage/emulated/0/Download/a.mp4"});