前几天项目需要压缩视频,Github
上找了许多库,要么就是太大,要么就是质量不高,其实我只需要压缩视频,最好的方案还是定制编译一个 FFmpeg
给 Android
用。
本项目使用 FFmpeg
和 libx264
(一个第三方的视频编码器) 来编译出可以在 Android
上使用的动态库
一、下载源码 创建一个叫 FFmpegAndroid
的目录,下载 libx264
的源码 和ffmpeg
的源码 ,然后在 FFmpegAndroid
文件夹下建立一个 bulid
文件夹,用于存放编译脚本和输出
1 2 3 4 --- FFmpegAndroid |-- ffmpeg |-- x264 |-- build
二、编译 FFmpeg 编译 x264 编码器 先在 build
文件夹下建立 setting.sh
, 用于申明一些公用的环境变量,比如 $NDK
、$CPU
…
setting.sh
1 2 3 4 5 6 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=armv7-a
然后建立 libx264
的编译脚本 build_x264.sh
,libx264
是一个开源的H.264编码器,据说是最好的视频有损编码器。ffmpeg
默认不自带,但是支持 x264
作为第三方编码器编译。
build_x264.sh
./config 内的# 注释必须在运行的时候去掉
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 #!/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="" cd ../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_FLAG make clean make -j4 make install } build_x264
写完之后就可以编译 x264
库了,编译之前还有一点要注意的是,默认编译出来的文件后缀并不是 *.so
,这 Android
是识别不了的,需要对 x264
源码里面的 config
做如下修改:
将
1 2 3 echo "SOSUFFIX=so" >> config.makecho "SONAME=libx264.so.$API " >> config.makecho "SOFLAGS=-shared -Wl,-soname,\$(SONAME) $SOFLAGS " >> config.mak
修改成
1 2 3 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
文件夹目录下应该有个 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 内的# 注释必须在运行的时候去掉**
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 #!/bin/bash . setting.sh echo "use toolchain: $TOOLCHAIN " echo "use system root: $SYSROOT " LIB_DIR=$(pwd )/lib; PREFIX=$LIB_DIR /ffmpeg/$CPU INC="$LIB_DIR /x264/$CPU /include" 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" 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 \ --enable -libx264 \ --enable -encoder=libx264 \ --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 \ --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 " \ --extra-ldflags="-Wl,-L$LIB " make clean make -j16 make install } build_arm
这次编译不用静态库的原因是,静态库链接是有顺序要求的,这里模块太多,我也不知道哪个模块依赖哪个模块,所以直接上动态库
脚本写完后,就可以 run 了,编译时间有点久,可以学学我的某个同学,一编译就起来泡泡妹子,有说有笑。
编译完成后你的目录应该是下面那个样子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 --- 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 文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 apply plugin: 'com.android.library' android { ... defaultConfig { ... externalNativeBuild { cmake { cppFlags "-std=c++11" } ndk { abiFilters "armeabi-v7a" } } } ... externalNativeBuild { cmake { path "CMakeLists.txt" } } }
这样 lib module 就支持 c++ 了,方便吧!比以前的 Android.mk 不知道方便多少
然后在模块的 src/main
下面新建一个 cpp
目录,用于存放 c++ 代码,从ffmpeg
拷贝以下文件:
1 2 3 4 5 6 7 8 9 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 认识它们
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 # 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
:
1 2 3 int run_ffmpeg_command (int argc, char **argv) { ... }
改了以后,我们就可以调用 run_ffmpeg_command
然后传入参数,相当于在 pc 执行 ffmpeg
命令。不过现在还不能执行,这是个坑点,仔细看 run_ffmpeg_command
函数,在程序结束的时候,或者中途出现错误的时候,都会调用 exit_program(int)
,这个函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 int run_ffmpeg_command (int argc, char **argv) { ... 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)
这个函数声明的锅!看下面这个函数的声明:
1 2 3 4 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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
1 2 3 4 5 6 7 8 9 10 11 12 13 public class FFmpegNativeBridge { static { System.loadLibrary("ffmpeg-lib" ); } public static native int runCommand (String[] command) ; }
ffmpeg-lib.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include <jni.h> #include "ffmpeg.h" JNIEXPORT jint JNICALL Java_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
的命令了:
1 2 3 4 5 6 7 8 9 10 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" });
关于这些参数,可以去查 FFmpeg
的官网 ,本项目源码地址Github