前几天项目需要压缩视频,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 则指定 x86CPU=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 去执行 configcd ../x264function 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_FLAGmake clean
make -j4
make install
}# 执行编译指令build_x264写完之后就可以编译 x264 库了,编译之前还有一点要注意的是,默认编译出来的文件后缀并不是 *.so,这 Android 是识别不了的,需要对 x264 源码里面的 config 做如下修改:
将
echo "SOSUFFIX=so" >> config.makecho "SONAME=libx264.so.$API" >> config.makecho "SOFLAGS=-shared -Wl,-soname,\$(SONAME) $SOFLAGS" >> config.mak
修改成
echo "SOSUFFIX=so" >> config.makecho "SONAME=libx264-$API.so" >> config.makecho "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 需要链接 x264LIB_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 ../ffmpegfunction 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.hcmdutils.ccmdutils.hconfig.hffmpeg_filter.cffmpeg_opt.cffmpeg-lib.cffmpeg.cffmpeg.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"});
作者:voiddog
链接:https://www.jianshu.com/p/ceaa286d8aff
共同学习,写下你的评论
评论加载中...
作者其他优质文章