Code FFmpeg on Android

移植FFmpeg到Android上

因为Mix Music解码的需求,所以得选择合适的解码工具.尝试了4种解码方式,最后还是FFmpeg的效果最好

  • MediaCodec配合MediaExtractor进行解码操作
  • MediaCodec不用MediaExtractor进行解码操作
    • 在给Bytebuffer填充数据后,MediaCodec处理数据的时候总出错,大概是因为没有跳过非帧数据的部分
  • 使用LAME进行解码操作
    • 不幸的是LAME中的hip_decode()也是只能处理帧数据,需要手动跳过非帧数据.当手动跳过非帧数据后,最终发现速度并没有提升多少(尽管已经设置了不同大小的buffer)
  • 使用FFmpeg进行解码操作
    • 编译的时候真的是各种错误,头文件明明就在那里呆的好好的,编译器还是报找不到函数的错误.好在最终效果令人非常满意,3秒钟解码一个音频文件.

这是项目的GitHub地址,如果你的ffmpeg实在是编译不过去,可以去这里下载,然后再进行编译.

编译出ffmpeg的so文件

要编译FFmpeg,第一步就是先获取源码.直接去官网就可以下载.
解压后得到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
└───ffmpeg
├───compat
├───doc
├───ffbuild
├───ffmpeg
├───fftools
├───libavcodec
├───libavdevice
├───libavfilter
├───libavformat
├───libavresample
├───libavutil
├───libpostproc
├───libswresample
├───libswscale
├───presets
├───tests
└───tools

打开configure文件找到

1
2
3
4
SLIBNAME_WITH_MAJOR='$(SLIBNAME).$(LIBMAJOR)'  
LIB_INSTALL_EXTRA_CMD='$$(RANLIB) "$(LIBDIR)/$(LIBNAME)"'
SLIB_INSTALL_NAME='$(SLIBNAME_WITH_VERSION)'
SLIB_INSTALL_LINKS='$(SLIBNAME_WITH_MAJOR) $(SLIBNAME)'

将内容修改为

1
2
3
4
SLIBNAME_WITH_MAJOR='$(SLIBPREF)$(FULLNAME)-$(LIBMAJOR)$(SLIBSUF)'  
LIB_INSTALL_EXTRA_CMD='$$(RANLIB) "$(LIBDIR)/$(LIBNAME)"'
SLIB_INSTALL_NAME='$(SLIBNAME_WITH_MAJOR)'
SLIB_INSTALL_LINKS='$(SLIBNAME)'

这是因为默认生成的文件名称中版本号位于最后,不符合Android命名规范.

在ffmpeg目录下创建build.sh文件
内容为

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

NDK=F:/android-ndk-r14b
# 指向你NDK的路径
SYSROOT=$NDK/platforms/android-21/arch-arm/
# 指定逻辑目录,按照自身需要进行修改
TOOLCHAIN=$NDK/toolchains/arm-linux-androideabi-4.9/prebuilt/windows-x86_64
# windows平台为windows-x86_64,linux为linux-x86_64
CPU=arm
PREFIX=$(pwd)/android/$CPU
ADDI_CFLAGS="-marm"
# build_one中,因为我只需要使用mp3的decode和pcm_s16le的encode,所以关闭了其他的decode和encode来实现压缩体积,集体配置参数可参考
# https://www.cnblogs.com/azraelly/archive/2012/12/31/2840541.html
function build_one
{
./configure \
--prefix=$PREFIX \
--enable-shared \
--disable-static \
--enable-asm \
--disable-doc \
--disable-gpl \
--enable-small \
--disable-encoders \
--disable-decoders \
--disable-ffmpeg \
--enable-encoder=pcm_s16le \
--enable-decoder=mp3 \
--disable-ffplay \
--disable-ffprobe \
--disable-ffserver \
--disable-doc \
--disable-symver \
--enable-jni \
--cross-prefix=$TOOLCHAIN/bin/arm-linux-androideabi- \
--target-os=android \
--arch=arm \
--enable-cross-compile \
--sysroot=$SYSROOT \
--extra-cflags="-Os -fpic $ADDI_CFLAGS" \
--extra-ldflags="$ADDI_LDFLAGS" \
$ADDITIONAL_CONFIGURE_FLAG
make clean
make
make install
}
build_one

更加不幸的是.sh文件现在还不能运行,得先下载MinGW,记得build.sh文件不编辑的时候,不要挂在后台处在打开状态
下载好后,直接打开
勾选上如图所示的选项
pic3

在左上角Installation菜单中点击Apply Changes,然后等待下载并自动安装(可能需要酸酸乳,因为网络质量不是很高)

下载完毕后,直接进度你的MinGW文件夹,找到msys文件夹并进入.双击msys.bat就会启动一个长的很像CMD的程序,至少在打开路径上都是使用cd命令

cd进入到之前build.sh的文件夹,输入./build.sh运行build.sh,它就会自动进行编译操作.如果遇到了各种奇怪问题,不要放弃,因为你的问题前人已经犯过,你只需要善用搜索引擎,踩着他们的脚印即可.

因为我在编译的时候也遇到了各种各样的问题,最后通过更换NDK版本解决了问题.
等待一段时间后,在ffmpeg目录下就会多出android文件夹,我们需要用到的so文件就在这个ffmpeg/android/arm/lib文件夹内

编译完成后我们得到了

1
2
3
4
5
6
7
8
9
10
include
lib
libavcodec.so
libavdevice.so
libavfilter.so
libavformat.so
libavutil.so
libpostproc.so
libswresample.so
libswscale.so

编译安卓可以调用的so文件

终于到了要调用FFmpeg的阶段了.
和常规NDK编译的需求一样,需要创建一个jni文件夹.把上一步得到的include文件夹和那些so文件复制进来.

还需要把位于ffmpeg根目录中的

  • cmdutils.c和cmdutils.h
  • ffmpeg.h和ffmpeg.c
  • ffmpeg_opt.c
  • ffmpeg_hw.c
  • ffmpeg_filter.c
  • va_copy.h
  • config.h 这个文件只有你之前手动编译了ffmpeg才会得到
    也复制到jni文件夹内

创建Application.mk

1
2
3
4

APP_ABI := armeabi-v7a armeabi
APP_MODULES := libffmpegjni
APP_CFLAGS += -DSTDC_HEADERS

以及Android.mk

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
LOCAL_PATH:= $(call my-dir)

include $(CLEAR_VARS)
LOCAL_MODULE:= avcodec-prebuilt-armeabi
LOCAL_SRC_FILES:= prebuilt/armeabi/libavcodec.so
include $(PREBUILT_SHARED_LIBRARY)

include $(CLEAR_VARS)
LOCAL_MODULE:= avdevice-prebuilt-armeabi
LOCAL_SRC_FILES:= prebuilt/armeabi/libavdevice.so
include $(PREBUILT_SHARED_LIBRARY)

include $(CLEAR_VARS)
LOCAL_MODULE:= avfilter-prebuilt-armeabi
LOCAL_SRC_FILES:= prebuilt/armeabi/libavfilter.so
include $(PREBUILT_SHARED_LIBRARY)

include $(CLEAR_VARS)
LOCAL_MODULE:= avformat-prebuilt-armeabi
LOCAL_SRC_FILES:= prebuilt/armeabi/libavformat.so
include $(PREBUILT_SHARED_LIBRARY)

include $(CLEAR_VARS)
LOCAL_MODULE := avutil-prebuilt-armeabi
LOCAL_SRC_FILES := prebuilt/armeabi/libavutil.so
include $(PREBUILT_SHARED_LIBRARY)

include $(CLEAR_VARS)
LOCAL_MODULE := swresample-prebuilt-armeabi
LOCAL_SRC_FILES := prebuilt/armeabi/libswresample.so
include $(PREBUILT_SHARED_LIBRARY)

include $(CLEAR_VARS)
LOCAL_MODULE := swscale-prebuilt-armeabi
LOCAL_SRC_FILES := prebuilt/armeabi/libswscale.so
include $(PREBUILT_SHARED_LIBRARY)

include $(CLEAR_VARS)

LOCAL_MODULE := libffmpegjni

LOCAL_ARM_MODE := arm

LOCAL_SRC_FILES := ffmpegJni.c \
ffmpeg.c \
cmdutils.c \
ffmpeg_opt.c \
ffmpeg_filter.c \
ffmpeg_hw.c

LOCAL_LDLIBS := -L$(SYSROOT)/usr/lib -llog -lz

LOCAL_SHARED_LIBRARIES:= avcodec-prebuilt-armeabi \
avdevice-prebuilt-armeabi \
avfilter-prebuilt-armeabi \
avformat-prebuilt-armeabi \
avutil-prebuilt-armeabi \
swresample-prebuilt-armeabi \
swscale-prebuilt-armeabi

LOCAL_C_INCLUDES += -L$(SYSROOT)/usr/include
LOCAL_C_INCLUDES += $(LOCAL_PATH)/include

LOCAL_CFLAGS := -DUSE_ARM_CONFIG

include $(BUILD_SHARED_LIBRARY)

在ffmpeg.c中有些东西需要我们修改下,因为正常情况下ffmpeg执行一条命令,它就会自动退出,我们当然不希望这样.所以需要对他的推出操作进行处理.
打开ffmpeg.c,找到如下内容并修改

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
/* parse options and open all input/output files */
ret = ffmpeg_parse_options(argc, argv);
if (ret < 0){
// exit_program(1);
return 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);
return 1;
}

/* file converter / grab */
if (nb_output_files <= 0) {
av_log(NULL, AV_LOG_FATAL, "At least one output file must be specified\n");
//exit_program(1);
return 1;
}

if (nb_input_files == 0) {
av_log(NULL, AV_LOG_FATAL, "At least one input file must be specified\n");
//exit_program(1);
return 1;
}

把下面的内容,添加到static void ffmpeg_cleanup(int ret)函数末尾

1
2
3
4
5
6
ffmpeg_exited = 1;
nb_filtergraphs = 0;
nb_output_files = 0;
nb_output_streams = 0;
nb_input_files = 0;
nb_input_streams = 0;

为了获取到ffmpeg的处理进度
修改log_callback_null函数内容为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static void log_callback_null(void *ptr, int level, const char *fmt, va_list vl)
{
static int print_prefix = 1;
static int count;
static char prev[1024];
char line[1024];
static int is_atty;
av_log_format_line(ptr, level, fmt, vl, line, sizeof(line), &print_prefix);
strcpy(prev, line);
if (level <= AV_LOG_WARNING){
XLOGE("%s", line);
}else{
XLOGD("%s", line);
callJavaMethod(line);// 调用java方法,反馈进度
}
}

并再main函数的开始处添加

1
av_log_set_callback(log_callback_null);

再打开cmdutils.c,对exit_program进行修改,别忘记修改头文件中的exit_program

1
2
3
4
int exit_program(int ret)
{
return ret;
}

然后创建一个名为ffmpegJni.c的文件和它的头文件ffmpegJni.h

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
#include "logjni.h"
#include "ffmpegJni.h"
#include <stdlib.h>
#include <stdbool.h>

int main(int argc, char **argv);
static JavaVM *jvm = NULL;
static jclass m_clazz = NULL;
static jclass m_jobj = NULL;
static JNIEnv *m_env = NULL;
// 注意这里的Java_com_chaosgoo_ffmpegproject_ffmpegJni_main,请按照你的项目名称进行修改
JNIEXPORT jint JNICALL Java_com_chaosgoo_ffmpegproject_ffmpegJni_main(JNIEnv *env, jclass obj, jobjectArray commands){
(*env)->GetJavaVM(env, &jvm); //获取JVM虚拟机
jclass clazz = (*env)->GetObjectClass(env,obj); //获取调用此方法的java类
m_jobj = obj;
m_clazz = (*env)->NewGlobalRef(env, clazz); //将这个类赋值给m_clazz
m_env = env; //复制到全局变量m_env
int argc = (*env)->GetArrayLength(env, commands);
char *argv[argc];
int i;
int result = 0;
for (i = 0; i < argc; i++)
{
jstring js = (jstring)(*env)->GetObjectArrayElement(env, commands, i);
argv[i] = (char *)(*env)->GetStringUTFChars(env, js, 0);
}
LOGD("----------begin---------");
int ret = main(argc, argv);
ffmpegJniDone(1);
return ret;
}


void callJavaMethod(char *ret)
{
int ss = 0;
char *q = strstr(ret, "time=");
if (q != NULL)
{
//LOGE("遇到time=");
char str[14] = {0};
strncpy(str, q, 13);
int h = (str[5] - '0') * 10 + (str[6] - '0');
int m = (str[8] - '0') * 10 + (str[9] - '0');
int s = (str[11] - '0') * 10 + (str[12] - '0');
ss = s + m * 60 + h * 60 * 60;
}
else
{
return;
}

if (m_clazz == NULL)
{
LOGE("---------------m_clazz isNULL---------------");
return;
}
//获取方法ID (I)V指的是方法签名 通过javap -s -public FFmpegCmd 命令生成
jmethodID methodID = (*m_env)->GetMethodID(m_env, m_clazz, "onProgress", "(I)V");
if (methodID == NULL)
{
LOGE("---------------methodID isNULL---------------");
return;
}
LOGE("---------------Call Method---------------");
//调用该java方法
(*m_env)->CallVoidMethod(m_env,m_jobj, methodID, ss);
}

void ffmpegJniDone(int i){
jmethodID methodID = (*m_env)->GetMethodID(m_env, m_clazz, "onFinish", "(I)V");
(*m_env)->CallVoidMethod(m_env,m_jobj, methodID, i);
// 完成任务后的调用java方法,告知其已经完成
}

logjni.h是为了将ffmpeg自身的输出变为Android Log输出的一个头文件,内容为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#ifdef ANDROID
#include <android/log.h>
#ifndef LOG_TAG
#define MY_TAG "MYTAG"
#define AV_TAG "AVLOG"
#endif
#define LOGE(format, ...) __android_log_print(ANDROID_LOG_ERROR, MY_TAG, format, ##__VA_ARGS__)
#define LOGD(format, ...) __android_log_print(ANDROID_LOG_DEBUG, MY_TAG, format, ##__VA_ARGS__)
#define XLOGD(...) __android_log_print(ANDROID_LOG_INFO,AV_TAG,__VA_ARGS__)
#define XLOGE(...) __android_log_print(ANDROID_LOG_ERROR,AV_TAG,__VA_ARGS__)
#else
#define LOGE(format, ...) printf(MY_TAG format "\n", ##__VA_ARGS__)
#define LOGD(format, ...) printf(MY_TAG format "\n", ##__VA_ARGS__)
#define XLOGE(format, ...) fprintf(stdout, AV_TAG ": " format "\n", ##__VA_ARGS__)
#define XLOGI(format, ...) fprintf(stderr, AV_TAG ": " format "\n", ##__VA_ARGS__)
#endif

到这一步就可以执行ndk-build命令进行编译了.
cmd 进入之前创建的jni文件夹,输入ndk-build (别忘记配置好ndk的环境变量)后,就会自动的进行编译操作,

如依然提示各种缺少文件,可以到之前解压出的ffmpeg文件夹里面去寻找到他们,然后复制到对应的文件夹中
最后,编译完成后,就会多出一个libffmpegjni.so文件
接下来就是Android端进行调用了
创建一个FFmpegJni类

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
public class FFmpegJni {
private float totalDecodeTime = 0f;
private ProgressBar progressBar;

private String fileInPath;
private String fileOutPath;

MediaPlayer mediaPlayer = new MediaPlayer();

public FFmpegJni(String inPath, String outPath, ProgressBar progressBar){
fileInPath = inPath;
fileOutPath = outPath;
this.progressBar = progressBar;
}
// 初始化相关设定
public void init(){
try{
mediaPlayer.setDataSource(fileInPath);
mediaPlayer.prepare();
totalDecodeTime = mediaPlayer.getDuration()/1000;
Log.d("ffmpeg", "init: "+totalDecodeTime);
}catch (Exception e){
e.printStackTrace();
}
}
//执行解码操作
public void decode(){
new Thread(new Runnable() {
@Override
public void run() {
String[] cmd = {"ffmpeg", "-i",fileInPath, "-f", "s16be" ,"-ar" ,"44100" ,"-acodec","pcm_s16le", fileOutPath};
main(cmd);
}
}).start();
}

// 更新进度条
public void onProgress(int second) {
Log.d("ffmpeg", "onProgress: "+ ((second/this.totalDecodeTime) * 100));
progressBar.setProgress((int)((second/this.totalDecodeTime) * 100));
}


// NDK 函数
public native int main(String[] commands);
// 导入so文件
static {
System.loadLibrary("avutil");
System.loadLibrary("swresample");
System.loadLibrary("avcodec");
System.loadLibrary("avformat");
System.loadLibrary("swscale");
System.loadLibrary("avfilter");
System.loadLibrary("avdevice");
System.loadLibrary("ffmpegjni");
}
}

然后再mainacitivity中调用即可,FFmpegJni的main函数接收命令.

参考资料