在Android上使用FFmpeg压缩视频
前几天项目需要压缩视频,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 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
的命令了:
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"});