爆裂吧现实

不想死就早点睡

0%

在Android上使用FFmpeg压缩视频

前几天项目需要压缩视频,Github上找了许多库,要么就是太大,要么就是质量不高,其实我只需要压缩视频,最好的方案还是定制编译一个 FFmpegAndroid 用。

本项目使用 FFmpeglibx264(一个第三方的视频编码器) 来编译出可以在 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 环境
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.shlibx264 是一个开源的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=""

# 因为当前目录在 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 做如下修改:

1
2
3
echo "SOSUFFIX=so" >> config.mak
echo "SONAME=libx264.so.$API" >> config.mak
echo "SOFLAGS=-shared -Wl,-soname,\$(SONAME) $SOFLAGS" >> config.mak

修改成

1
2
3
echo "SOSUFFIX=so" >> config.mak
echo "SONAME=libx264-$API.so" >> config.mak
echo "SOFLAGS=-shared -Wl,-soname,\$(SONAME) $SOFLAGS" >> config.mak

别忘了给 build_x264.shsetting.sh 赋予可执行权限 (chmod +x build_x264.sh setting.sh)

修改完后就可以执行脚本命令了

1
./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,我可以在这方面下手,指定 decoderencoder,因为我需要的是视频压缩,所以编码器(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"

# 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 了,编译时间有点久,可以学学我的某个同学,一编译就起来泡泡妹子,有说有笑。

编译完成后你的目录应该是下面那个样子:

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 {
...
// 启用 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拷贝以下文件:

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){
...

/* 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) 这个函数声明的锅!看下面这个函数的声明:

1
2
3
4
/**
* 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

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");
}

/**
* 执行指令
* @param command
* @return 命令返回结果
*/
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