该篇内容为原博客博文,原上传于2022年2月27日。可能已过时,仅作补档用途,谨慎参考!
前言
上次,我们初步对FFmpeg进行了交叉编译及简单移植。然而,FFmpeg的强大之处在于使用简单的命令行完成艰巨的音视频处理。如果仅仅是集成众多动态库,我们只能使用库中提供的函数编写代码完成功能,相比于使用命令行,对于初学者来说是比较复杂和困难的方法。所以这一次,我们将尝试移植FFmpeg的命令行,直接通过命令调用FFmpeg。然而,笔者的初次移植血泪史揭示,现有的大量博客都是过时的,按照他们的步骤来做,会导致移植失败,白白浪费心力。需要在一般步骤的基础上,加上其他必要的操作。
在之前的步骤中,我们并没有深入讨论源码目录下的configure文件本身的内容,也没有提到编译脚本为什么这么写,哪些参数是必须配置的。接下来,我们打开configure文件,首先对FFmpeg编译生成的动态库的格式进行设置:
1 2
| vim configure :/SLIBNAME_WITH_MAJOR
|
注意本文基于ffmpeg5.0,其他版本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)'
|
这一步是许多博客都提到过的,其原因在于,FFmpeg默认编译生成的so库会在后缀后面带上版本号,如libavcodec.so.57.107.100。Android Studio不支持导入这样的库。实际上,FFmpeg也会生成一般命名的so库,只是经过上面的修改后,在安装目录下的lib目录中就只会生成一般命名的so库了,这会使得lib目录更加简洁美观。
接下来,我们找到if test "$target_os" = android处,注意到这里对应上了我们在编译脚本中配置的参数target-os=android,这里规定了一些默认配置,然而仔细观察,不难发现其中有些地方是有问题的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| if test "$target_os" = android; then cc_default="clang" fi
ar_default="${cross_prefix}${ar_default}" cc_default="${cross_prefix}${cc_default}" cxx_default="${cross_prefix}${cxx_default}" nm_default="${cross_prefix}${nm_default}" pkg_config_default="${cross_prefix}${pkg_config_default}" if ${cross_prefix}${ranlib_default} 2>&1 | grep -q "\-D "; then ranlib_default="${cross_prefix}${ranlib_default} -D" else ranlib_default="${cross_prefix}${ranlib_default}" fi strip_default="${cross_prefix}${strip_default}" windres_default="${cross_prefix}${windres_default}"
|
首先,上面的脚本没有设置当 target-os 为 android 时,c++ 的默认编译器;此外,其将ar,cc,cxx等等需要的参数均设置成以 cross_prefix 为前缀,然而找到我们工具链的bin目录下(如笔者这里是ndk/21.4.7075529/toolchains/llvm/prebuilt/linux-x86_64/bin),可以发现相应的工具前缀是llvm-,而不是脚本里设置的cross_prefix,即aarch64-linux-android-。如果不修改,就会在最后移植时导致一系列问题。此外,其对pkg_config的配置也有问题。我们的工具链并没有内置pkg_config,所以这里也要改成我们自己安装的pkg_config。我们考虑以上列出的问题,将以上代码修改如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| if test "$target_os" = android; then cc_default="clang" cxx_default="clang++" fi
ar_default="llvm-${ar_default}" cc_default="${cross_prefix}${cc_default}" cxx_default="${cross_prefix}${cxx_default}" nm_default="llvm-${nm_default}" pkg_config_default="${pkg_config_default}" if ${cross_prefix}${ranlib_default} 2>&1 | grep -q "\-D "; then ranlib_default="llvm-${ranlib_default} -D" else ranlib_default="llvm-${ranlib_default}" fi strip_default="llvm-${strip_default}" windres_default="${cross_prefix}${windres_default}"
|
注意到我们将涉及到前缀问题的参数均修改为了正确的前缀(有的参数暂时用不到,就没有修改),此外,我们将cxx_default重写为clang++。我们没有改写下面的cc_default和cxx_default,因为我们的脚本中已经设置了正确的cc和cxx,不会用到错误的默认值。到这里,configure就算改好了。这里我列出的代码仅以我自己的配置为准,务必要根据自己的工具链选择是否改写。
我们再回头看一眼编译脚本:
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
| TOOLCHAIN=$HOME/Android/Sdk/ndk/21.4.7075529/toolchains/llvm/prebuilt/linux-x86_64
export PATH=$PATH:$TOOLCHAIN/bin
CROSS_PREFIX=aarch64-linux-android CROSS_PREFIX_CLANG=aarch64-linux-android21 PREFIX=$HOME/open/FFmpeg/src/ffmpeg-5.0-arm64 ANDROID_API=21 ARCH=aarch64 CPU=armv8-a
# configure默认的CC与CXX路径是错误的,需要修正 export CC=$CROSS_PREFIX_CLANG-clang export CXX=$CROSS_PREFIX_CLANG-clang++
./configure \ --prefix=$PREFIX \ --enable-shared \ --enable-postproc \ --enable-gpl \ --enable-jni \ --disable-static \ --enable-cross-compile \ --disable-doc \ --cross-prefix=$CROSS_PREFIX- \ --cc=$CC \ --cxx=$CXX \ --sysroot=$TOOLCHAIN/sysroot \ --extra-cflags="-D__ANDROID_API__=$ANDROID_API -U_FILE_OFFSET_BITS" \ --arch=$ARCH \ --cpu=$CPU \ --target-os=android
make clean make -j4 make install
echo "build android ffmpeg finished."
|
运行脚本,我们像上次一样,在安装目录得到了bin,include,lib,share几个目录,表示编译成功。
移植命令行
首先,我们像上次一样,拷贝lib目录下所有so库至我们的Android项目中。接下来,我们首先考虑如何实现命令行的移植。我们在使用ffmpeg命令的时候,实际上是在将命令行参数传给ffmpeg的main函数,并由main函数具体执行命令。那么我们要做的事情也就清晰了,即在android项目中使用ffmpeg的main函数,并向其传递参数。接下来的步骤如下:
我们拷贝整个include目录。这里我直接将libs和include目录放在和src同级(注意我们不建议在实际项目中这样组织目录层次。官方推荐的做法是将原生相关的文件统一放在cpp目录下管理)。可以看见,在include目录中,是我们所需要的众多头文件。
接下来,我们回到ffmpeg源码目录,这里有我们需要用到的一些文件。首先是fftools文件夹,这里是命令行调用ffmpeg的工具。我们复制其中的cmdutils.h和ffmpeg.h到android项目的include文件夹下(别忘了include是我们存放众多头文件的地方),再把cmdutils.c, ffmpeg.c, ffmpeg_filter.c, ffmpeg_hw.c, ffmpeg_opt.c五个文件复制到libs目录下。此外,我们还需要源码目录下编译后生成的config.h,将它也复制到include目录下。注意这里文件存放的位置不是绝对的,只要保证能合理地编写CMakeLists即可。
到此,我们的目录结构如下:


接着,我们还需要修改其中的一部分代码,以使其适应Android APP下的应用情景。参考博客:https://blog.csdn.net/yhaolpz/article/details/77146156#commentBox
- 修改日志输出源
我们希望能在调用ffmpeg时,将其日志输出导入至AS的logcat中,方便我们的开发与调试。为此,我们首先引入android的log库。在CMakeLists中新增如下代码:
1 2 3 4 5 6
| find_library(log-lib log) ... target_link_libraries( ... ${log-lib} )
|
接下来,在include目录写一个androidlog.h文件:
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
|
打开ffmpeg.c,引入头文件#include "androidlog.h",并实现原本为空的logg_callback_null函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 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); } }
|
在ffmpeg.c的main函数第一行加入如下代码:
1
| av_log_set_callback(log_callback_null);
|
这样,以后在调用ffmpeg命令时,就可以像在shell里一样在logcat获得ffmpeg输出的日志信息。
- 执行命令后清除数据并保留进程
在ffmpeg.c的ffmpeg_cleanup中末尾加上如下代码:
1 2 3 4 5
| nb_filtergraphs = 0; nb_output_files = 0; nb_output_streams = 0; nb_input_files = 0; nb_input_streams = 0;
|
在ffmpeg.c的main函数的return语句调用前增加如下代码:
再修改cmdutils.c中的exit_program函数如下:
1 2 3 4
| int exit_program(int ret) { return ret; }
|
注意由于我们修改了函数的返回值,故还需要到cmdutils.h中修改对应的函数声明:
1
| int exit_program(int ret);
|
以上都完成后,不要忘了在ffmpeg.h中加上对main函数的声明,否则我们调用不到ffmpeg.c的main函数。大多数博客都喜欢将main函数的函数名改写为run, exec之类的其他名称,这个因人而异,修不修改没有问题。
到此,对ffmpeg源文件的修改就结束了,大多数博客也就到此结束了,他们可以愉快地在android中使用ffmpeg命令了。但此时如果我们尝试启动APP,会发现AS报出一长串的错误,都是找不到头文件。此时不要慌张,回到ffmpeg源码目录下寻找缺失的头文件并逐一复制即可。这里我丢失的头文件如下,可供参考:
- compat目录下:va_copy.h
- libavcodec目录下:matops.h
- libavformat目录下:os_support.h
- libavutil目录下:aarch64等架构目录(主要需要的是其中的timer.h),internal.h,libm.h,thread.h,timer.h
再次启动APP,应该编译通过,APP正常启动。如果你的AS依然报错,且不是丢失头文件,而是各种语法问题(如内联汇编限制符),你可以先尝试修正,如果始终不行,请检查ffmpeg编译脚本是否正确编写,目标架构等参数是否配置正确。
我们可以编写一个测试用例,这里我之前有一个将wav格式音频转码为aac格式的业务需求,是使用android的MediaCodec API实现的,其代码逻辑复杂冗长,我们尝试使用简单的ffmpeg命令实现这个需求。
编写如下工具类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| object FFmpegUtils { init { System.loadLibrary("jnitest") }
fun WAV2AAC(pcmPath: String, aacPath: String) { val command = arrayOf( "ffmpeg", "-i", pcmPath, aacPath ) exec(command) }
private external fun exec(args: Array<String>) }
|
我们先是加载了native库,并且声明了一个native方法,用于接收命令参数。exec函数的原形如下:
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
| JNIEXPORT void JNICALL Java_com_eynnzerr_medialearning_utils_FFmpegUtils_exec(JNIEnv *env, jobject thiz, jobjectArray args) { int i = 0; int argc = 0; char **argv = NULL; jstring *strr = NULL;
if (args != NULL) { argc = (*env)->GetArrayLength(env, args); argv = (char **) malloc(sizeof(char *) * argc); strr = (jstring *) malloc(sizeof(jstring) * argc);
av_log(NULL, AV_LOG_INFO, "argc is %d", argc);
for (i = 0; i < argc; i ++) { strr[i] = (jstring) (*env)->GetObjectArrayElement(env, args, i); argv[i] = (char *) (*env)->GetStringUTFChars(env, strr[i], 0); av_log(NULL, AV_LOG_INFO, "receive argv: %s", argv[i]); } }
main(argc, argv);
for(i = 0; i < argc; i ++) { (*env)->ReleaseStringUTFChars(env, strr[i], argv[i]); } free(argv); free(strr); }
|
实际测试,完美运行,成功移植FFmpeg命令行至Android平台。