直接使用FFmpeg
因为FFmpeg是一套集录制、转换以及流化音视频的完整的跨平台解决方案,如果我们开发者想直接在自己开发的Android应用中使用ffmpeg的提供的功能,则需要引入so静态库,比如制作一些音视频编辑应用。
交叉编译生成,so动态库
编译工具链
对于C/C 的编译,通常有两个工具 GCC 和 CLANG 。
如果有用过c/c 的开发者应该都知道GCC,是一个编译工具,不仅可以编译C/C ,也可以编译Java,Object-C,Go等语言。
CLANG 则是更高效的C/C 编译工具,Google在ndk 17 以后,把 GCC 移除了,全面推行使用 CLANG 。 所以网上一些比较旧的ffmpeg编译教程没来得及更新,我们容易踩坑,导致ffmpeg源码编译失败。
使用CLANG编译FFmpeg
笔者的本文用的编译环境是:
编译机器: Mac OS Big Sur Version 11.1
NDK版本:android-ndk-r21d-darwin-x86_64.zip
测试机:华为Mate 30
FFmpeg版本:目前最新版本4.2.2
本文是使用目前最新的 NDK r21d 版本来编译。
NDK 下载地址:Android-NDK
NDK目录
代码语言:txt复制编译工具链目录:
toolchains/llvm/prebuilt/darwin-x86_64/bin
交叉编译环境目录:
toolchains/llvm/prebuilt/darwin-x86_64/sysroot
如下图所示
因为笔者的测试机器是华为的Mate 30,所以选择 CPU 架构 aarch64,Android版本 29,我们可以按照自己的实际需求选择编译工具
代码语言:txt复制aarch64-linux-androideabi21-clang
aarch64-linux-androideabi21-clang
下载FFmpeg源码
FFmpeg官网下载,直接DownLoad即可。 本文使用的是目前最新的版本 ffmpeg-4.2.2。 下载解压源码后,进入根目录,找到congfigure 的文件,它是一个shell脚本,用于生成一些 FFmpeg 编译需要的配置文件。这个文件非常重要,FFmpeg 的编译配置是依赖它完成的。
修改 configure 脚本 (可以用Subline打开)
我们需要修改ffmpeg-4.2.2 根目录下的 configure 文件,实际上是因为Google 在新版ndk把 GCC 移除了,全面推行使用 CLANG,所以我们需要把编译工具配置进行修改。修改流程如下:
1.新增 cross_prefix_clang 参数
我们可以搜索 CMDLINE_SET ,可以找到以下代码,然后新增一个命令行选项:cross_prefix_clang
2.修改编译工具路径设置
我们可以搜索 ar_default="${cross_prefix}${ar_default}" , 找到以下代码:
代码语言:txt复制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}"
将中间两行修改为:
代码语言:txt复制ar_default="${cross_prefix}${ar_default}"
#------------------------------------------------
cc_default="${cross_prefix_clang}${cc_default}"
cxx_default="${cross_prefix_clang}${cxx_default}"
#------------------------------------------------
nm_default="${cross_prefix}${nm_default}"
pkg_config_default="${cross_prefix}${pkg_config_default}"
3.新建编译配置脚本
在 ffmpeg-4.2.2 根目录下新建 shell 脚本,命名为: build_android_clang.sh,脚本代码如下:
代码语言:txt复制#!/bin/bash
set -x
# 目标Android版本
API=29
ARCH=arm64
CPU=armv8-a
TOOL_CPU_NAME=aarch64
#so库输出目录
OUTPUT=/Users/pj1053/Downloads/ffmpeg_source/ffmpeg/android/$CPU
# NDK的路径,根据自己的NDK位置进行设置
NDK=/Users/pj1053/Downloads/android-ndk-r21d
# 编译工具链路径
TOOLCHAIN=$NDK/toolchains/llvm/prebuilt/darwin-x86_64
# 编译环境
SYSROOT=$TOOLCHAIN/sysroot
TOOL_PREFIX="$TOOLCHAIN/bin/$TOOL_CPU_NAME-linux-android"
CC="$TOOL_PREFIX$API-clang"
CXX="$TOOL_PREFIX$API-clang "
OPTIMIZE_CFLAGS="-march=$CPU"
function build
{
./configure
--prefix=$OUTPUT
--target-os=android
--arch=$ARCH
--cpu=$CPU
--disable-asm
--enable-neon
--enable-cross-compile
--enable-shared
--disable-static
--disable-doc
--disable-ffplay
--disable-ffprobe
--disable-symver
--disable-ffmpeg
--cc=$CC
--cxx=$CXX
--sysroot=$SYSROOT
--extra-cflags="-Os -fpic $OPTIMIZE_CFLAGS"
make clean all
# 这里是定义用几个CPU编译
make -j8
make install
}
build
其中有两个地方要修改成自己的ffmpeg源码项目和NDK编译工具的本地路径,如下图:
4.添加脚本权限
编写完脚本文件,需要添加权限。
代码语言:txt复制chmod x build_android_clang.sh
5.脚本执行
添加脚本权限之后,我们可以直接运行脚本。
代码语言:txt复制 ./build_android_clang.sh
等待编译完成,将会在 当前文件夹的/android/armv8-a目录下得到 include 和 lib 两个目录,分别是 头文件 和 so库文件,就是我们需要编译生成的ffmpeg静态库文件和头文件。
使用FFmpeg so动态库
1.使用Android Studio 创建Native C 工程
新建项目的时候有一个选项是选择Native C 的模板
点击next,配置项目的信息
点击next,选择使用哪种C 标准,选择Toolchain Default会使用默认的CMake设置即可
点击finish即可完成工程的创建。
2.工程结构
这时候主工程目录下会有cpp文件夹
cpp文件夹:存放C/C 代码文件,native-lib.cpp文件默认生成的;
cpp文件夹下有两个文件,一个是native-lib.cpp文件,一个是CMakeLists.txt文件。CMakeLists.txt文件是cmake脚本配置文件,cmake会根据该脚本文件中的指令去编译相关的C/C 源文件,并将编译后产物生成共享库或静态块,然后Gradle将其打包到APK中。
3.Java调用native层c/c 代码
在MainActivity.java,static{}语句中使用了加载so库,在类加载中只执行一次。
代码语言:txt复制 static {
System.loadLibrary("native-lib");
}
然后,编写了原生的函数,函数名中要带有native。
代码语言:txt复制public native String stringFromJNI();
最后,编写相对应的c函数,注意函数名的构成Java_com_pengjie0668_demo_myapplication_MainActivity_stringFromJNI加上包名、类型、方法名的下划线连成一起。
注意:要按照jni的规范定义方法(Java包名类名native方法名,其中包名中的点用代替)
native-lib.cpp文件
代码语言:txt复制#include <jni.h>
#include <string>
extern "C" JNIEXPORT jstring JNICALL
Java_com_pengjie0668_demo_myapplication_MainActivity_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
std::string hello = "Hello from C ";
return env->NewStringUTF(hello.c_str());
}
4.引入 FFmpeg so库
- 添加ffmpeg so库文件
首先,在 app/src/main/ 目录下,新建文件夹,并命名为 jniLibs ,接着,在 jniLibs 目录下,新建 arm64-v8a 目录,
最后把 FFmpeg 编译得到的所有 so 库粘贴到 arm64-v8a 目录。如图:
- 添加 FFmpeg so库的头文件
在 cpp 目录下,新建 ffmpeg 目录,然后把编译时生成的 include 文件粘贴进来。
- 配置CMakeLists.txt
上面已经把 so 和 头文件 放置到对应的目录中了,但是编译器是不会把它们编译、链接、并打包到 Apk 中的,我们还需要在 CMakeLists.txt 中显性的把相关的 so 添加和链接起来。完整的 CMakeLists.txt 如下:
代码语言:javascript复制cmake_minimum_required(VERSION 3.10.2)
# 支持gnu 11
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=gnu 11")
# 1. 定义so库和头文件所在目录,方面后面使用
set(ffmpeg_lib_dir ${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI})
set(ffmpeg_head_dir ${CMAKE_SOURCE_DIR})
# 2. 添加头文件目录
include_directories(${ffmpeg_head_dir}/include)
# 3. 添加ffmpeg相关的so库
add_library( avutil
SHARED
IMPORTED )
set_target_properties( avutil
PROPERTIES IMPORTED_LOCATION
${ffmpeg_lib_dir}/libavutil.so )
add_library( swresample
SHARED
IMPORTED )
set_target_properties( swresample
PROPERTIES IMPORTED_LOCATION
${ffmpeg_lib_dir}/libswresample.so )
add_library( avcodec
SHARED
IMPORTED )
set_target_properties( avcodec
PROPERTIES IMPORTED_LOCATION
${ffmpeg_lib_dir}/libavcodec.so )
add_library( avfilter
SHARED
IMPORTED)
set_target_properties( avfilter
PROPERTIES IMPORTED_LOCATION
${ffmpeg_lib_dir}/libavfilter.so )
add_library( swscale
SHARED
IMPORTED)
set_target_properties( swscale
PROPERTIES IMPORTED_LOCATION
${ffmpeg_lib_dir}/libswscale.so )
add_library( avformat
SHARED
IMPORTED)
set_target_properties( avformat
PROPERTIES IMPORTED_LOCATION
${ffmpeg_lib_dir}/libavformat.so )
add_library( avdevice
SHARED
IMPORTED)
set_target_properties( avdevice
PROPERTIES IMPORTED_LOCATION
${ffmpeg_lib_dir}/libavdevice.so )
# 查找代码中使用到的系统库
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 )
# 配置目标so库编译信息
add_library( # Sets the name of the library.
native-lib
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
native-lib.cpp
)
# 指定编译目标库时,cmake要链接的库
target_link_libraries(
# 指定目标库,native-lib 是在上面 add_library 中配置的目标库
native-lib
# 4. 连接 FFmpeg 相关的库
avutil
swresample
avcodec
avfilter
swscale
avformat
avdevice
# Links the target library to the log library
# included in the NDK.
${log-lib} )
5.验证使用 FFmpeg
要检查 FFmpeg 是否可以使用,可以通过获取 FFmpeg 基础信息来验证。
(1)在 native-lib.cpp 中添加对应的 JNI 层方法。
代码语言:javascript复制#include <jni.h>
#include <string>
#include <unistd.h>
extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavfilter/avfilter.h>
#include <libavcodec/jni.h>
JNIEXPORT jstring JNICALL
Java_com_cxp_learningvideo_FFmpegActivity_ffmpegInfo(JNIEnv *env, jobject /* this */) {
char info[40000] = {0};
AVCodec *c_temp = av_codec_next(NULL);
while (c_temp != NULL) {
if (c_temp->decode != NULL) {
sprintf(info, "%sdecode:", info);
} else {
sprintf(info, "%sencode:", info);
}
switch (c_temp->type) {
case AVMEDIA_TYPE_VIDEO:
sprintf(info, "%s(video):", info);
break;
case AVMEDIA_TYPE_AUDIO:
sprintf(info, "%s(audio):", info);
break;
default:
sprintf(info, "%s(other):", info);
break;
}
sprintf(info, "%s[%s]n", info, c_temp->name);
c_temp = c_temp->next;
}
return env->NewStringUTF(info);
}
}
首先,我们看到代码被包裹在 extern "C" { } 当中,和前面的系统创建的稍微有些不同,通过这个大括号包裹,我们就不需要每个方法都添加单独的 extern "C" 开头了。 另外,由于 FFmpeg 是使用 C 语言编写的,所在 C 文件中引用 #include 的时候,也需要包裹在 extern "C" { },才能正确的编译。
(2)在 MainActivity 中添加一个外部方法 ffmpegInfo
代码语言:javascript复制 public native String ffmpegInfo();
(3)如果一切正常,App运行后,就会显示出 FFmpeg 音视频编解码器的信息
小结:
使用Android NDK工具对ffmpeg 源码进行交叉编译动态库的原理比较简单,但是在实践操作过程中,需要主要编译工具中路径的设置,和编译脚本内参数的设置。
Github Demo下载链接