FFMpeg是做音视频开发的同学都会接触的一个开源项目,现将其移植到Android上,写一个简单的视频格式转码工具,作为自己Android jni开发的一个入门学习和Android 开发的练习。为了简化开发,项目中使用命令行的方式调用ffmpeg而不是直接用ffmpeg提供的函数进行本地开发。
除了视频转换格式外,项目还设计了视频GIF截取,视频压缩等等功能,这些都是使用ffmpeg很容易实现的功能。
如果你按照本篇的步骤来进行FFMpeg的移植,请确保你的开发环境使用的版本和上述的一致。
特别感谢几位博主的分享,
1.最简单的基于FFmpeg的移动端例子:Android HelloWorld
2.编译FFmpeg4.0.1并移植到Android app中使用(最详细的FFmpeg-Android编译教程)
3.Android NDK开发(四) 将FFmpeg移植到Android平台
4.Cross Compiling FFMpeg 4-0 for Android
本篇中主要的配置修改均参考他们的博客,在此表示感谢。
在开始之前有必要了解几个概念,
1.Android:JNI 与 NDK到底是什么?(含实例教学),
2.Android的.so文件、ABI和CPU的关系
3.关于Android的.so文件你所需要知道的
4.CMakeLists.txt 语法介绍与实例演练
几位博主都写得很清晰,需要的同学请移步去看看。
从这里下载NDK
最新版本的是r19,不过在本项目中却不适用,本项目使用的是Android NDK, Revision 15c (July 2017),如果你使用的是其他版本,则在下一步编译FFMpeg和后面集成过程中,有大概率会遇到各种问题。
需要注意的是,Android Studio 3.2 中创建支持C++项目会默认下载并使用最新版本的NDK,而不是我们需要的版本。一个做法是替换(你可以在 第五步 创建项目,导入库文件 时再来替换)/home/your-user-name/Sdk/ndk-bundle(这里是默认的Sdk文件所在路径,如果你在初次安装Android Studio时有指定路径,那么ndk-bundle则在那个路径下)中的文件为我们下载的ndk中的文件。
比如/home/your-user-name/Downloads/android-ndk-r15c-linux-x86_64/是你下载的ndk的解压后的路径,你需要,把/home/your-user-name/Downloads/android-ndk-r15c-linux-x86_64/android-ndk-r15c 内的文件覆盖到/home/your-user-name/Sdk/ndk-bundle。
在这里下载FFMpeg源码,注意版本的选择,本项目使用的 FFmpeg 4.0.3 “Wu” 这个版本,如果使用其他版本,则不保证你按照我分享的步骤来做而不出错。
关于下载编译源码所需的注意的点,参考我之前的分享
Linux 下ffmpeg的安装
下载好FFMpeg的源码和编译所需的工具后,进入下一步。
解压你下载的ffmpeg源码,进入ffmpeg所在目录,找到configure这个文件,查找到如下内容,
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)'
把上面内容替换为
SLIBNAME_WITH_MAJOR='$(SLIBPREF)$(FULLNAME)-$(LIBMAJOR)$(SLIBSUF)'
LIB_INSTALL_EXTRA_CMD='$$(RANLIB)"$(LIBDIR)/$(LIBNAME)"'
SLIB_INSTALL_NAME='$(SLIBNAME_WITH_MAJOR)'
SLIB_INSTALL_LINKS='$(SLIBNAME)'
假设/home/your-user-name/Downloads/ffmpeg-4.0 是你下载的ffmpeg源码解压后的路径,切换到这个目录下,创建一个名为build.sh的文件(文件名随意)
$ cd /home/your-user-name/Downloads/ffmpeg-4.0
$ vim build.sh
build.sh的内容为
#!/bin/bash
NDK=/home/your-user-name/Android/Sdk/ndk-bundle
SYSROOT=$NDK/platforms/android-19/arch-arm/
TOOLCHAIN=$NDK/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86_64
function build_one
{
./configure \
--prefix=$PREFIX \
--enable-shared \
--disable-static \
--disable-doc \
--disable-ffplay \
--disable-ffprobe \
--disable-doc \
--disable-symver \
--enable-protocol=concat \
--enable-protocol=file \
--enable-muxer=mp4 \
--enable-demuxer=mpegts \
--cross-prefix=$TOOLCHAIN/bin/arm-linux-androideabi- \
--target-os=linux \
--arch=arm \
--enable-cross-compile \
--sysroot=$SYSROOT \
--extra-cflags="-Os -fpic $ADDI_CFLAGS" \
--extra-ldflags="$ADDI_LDFLAGS" \
$ADDITIONAL_CONFIGURE_FLAG
#make clean all
#make -j4
#make install
}
CPU=arm
PREFIX=$(pwd)/android/$CPU
ADDI_CFLAGS="-marm"
build_one
注意替换 NDK=/home/your-user-name/Android/Sdk/ndk-bundle 为你自己的ndk所在路径,这里只编译了arm架构的,也可以编译x86等架构的cpu使用的库,需要修改function build_one中的架构参数。
#make clean all
#make -j4
#make install
build.sh中注释了上面的三句话,而是在执行了bulid.sh后再在命令行中一步步手动执行这三句,当然也可以去掉注释直接在build.sh中执行。
使用
$ sudo chmod +x build.sh
$ ./build.sh
来执行build.sh这个脚本(chmod +x 用来赋予可执行权限)
如果你没有在build.sh中执行下面的命令的话,再
$ make clean all
$ make -j4
$ make install
如果一切顺利的话,在/home/your-user-name/Downloads/ffmpeg-4.0/android/arm/ 目录下有
其中include中的头文件和lib中的库文件,是下一步所需的。
如图1,新建一个Android项目,注意勾选“Include C++ support”,然后一路点next即可,最后完成即可。
然后在app/src/main 下创建jniLibs/armeabi-v7a,把4.3 中lib目录下的.so文件拷到 armeabi-v7a,鉴于主流手机CPU架构都是armeabi-v7a的,所以这里只提供了armeabi-v7a的库。如果要支持其他架构的,在4.3的编译脚本中修改CPU架构参数后,把生成的lib文件放到jniLibs下的对应的目录中。比如要支持x86的(如Android模拟器),则在jniLibs下创建x86目录,并把生成的.so文件放进去。
然后把4.3 中的到的include复制到app/src/main/cpp下
实现命令行使用FFMpeg的大概的思路是,利用从FFMpeg源码中“搬运”的部分代码,编译成一个可供Android调用本地动态库,通过jni调用来实现我们的目的。
从你的FFMpeg源码的/fftools/目录下,复制如下的几个文件(比如/home/your-user-name/Downloads/ffmpeg-4.0/fftools/)
修改cmdutils.h中的
void show_help_children(const AVClass *class, int flags);
为
void show_help_children(const AVClass *clazz, int flags);
修改cmdutils.c中的
void exit_program(int ret)
{
if (program_exit)
program_exit(ret);
exit(ret);
}
为
void exit_program(int ret)
{
//if (program_exit)
// program_exit(ret);
//exit(ret);
}
即注释掉退出函数的内容
修改ffmpeg.c的入口函数int main(int argc, char **argv)
为int run(int argc, char **argv)
(也可以随意,取一个其他名字,只要不是main就行)
并在ffmpeg.h添加这个函数的申明
int run(int argc, char **argv);
并注释掉ffmpeg.c末尾的
exit_program(received_nb_signals ? 255 : main_return_code);
找到函数static void ffmpeg_cleanup(int ret)
在其末尾添加
nb_filtergraphs = 0;
nb_output_files = 0;
nb_output_streams = 0;
nb_input_files = 0;
nb_input_streams = 0;
如果你需要输出ffmpeg执行的日志,在ffmpeg.c中添加下面的函数
static void my_av_log_callback(void *ptr, int level, const char *fmt, va_list vl) {
FILE *fp = fopen("/storage/emulated/0/Android/data/com.your_package_name.demo/files/log/ffmpegDemolog.txt","a+");
if (fp) {
vfprintf(fp,fmt,vl);
fflush(fp);
fclose(fp);
}
}
然后在你修改后的
int run(int argc, char **argv);
中调用
av_log_set_callback(my_av_log_callback);
在ffmpeg命令执行后,ffmpeg的日志会输出到my_av_log_callback函数中指定的文件
/storage/emulated/0/Android/data/com.your_package_name.demo/files/log/ffmpegDemolog.txt
直接复制cpp目录下,Android Studio生成的native-lib.cpp,重命名为ffmpeg_cmd.c(名称随意),native-lib.cpp的内容如下
#include
#include
extern "C" JNIEXPORT jstring JNICALL
Java_com_yourusername_ffmpeg_MainActivity_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
观察其中的函数名的格式,类型名的格式,修改后得到
#include
#include "ffmpeg.h"
JNIEXPORT jint
JNICALL
Java_com_yourusername_ffmpeg_MainActivity_runFFMpegCMD(
JNIEnv *env, jclass obj, jobjectArray commands) {
int argc = (*env)->GetArrayLength(env, commands);
char *argv[argc];
int i;
for (i = 0; i < argc; i++) {
jstring js = (jstring) (*env)->GetObjectArrayElement(env, commands, i);
argv[i] = (char *) (*env)->GetStringUTFChars(env, js, 0);
}
return run(argc, argv);
}
native方法的名称为runFFMpegCMD,java代码调用这个方法时,传入需要执行的命令(如ffmpeg -v)的string数组,在这个方法中,再由run(修改的ffmpeg.c的入口函数)函数执行这个命令。
把5.2,5.3中的C文件复制到cpp目录下,最后得到的项目目录结构为
图2 项目目录结构
# 设置Cmake版本
cmake_minimum_required(VERSION 3.4.1)
# 设置cpp目录路径
set(CPP_DIR ${CMAKE_SOURCE_DIR}/src/main/cpp)
# 设置jniLibs目录路径
set(LIBS_DIR ${CMAKE_SOURCE_DIR}/src/main/jniLibs)
# 设置CPU目录 armeabi
if(${ANDROID_ABI} STREQUAL "armeabi")
set(CPU_DIR armeabi)
endif(${ANDROID_ABI} STREQUAL "armeabi")
# armeabi-v7a
if(${ANDROID_ABI} STREQUAL "armeabi-v7a")
set(CPU_DIR armeabi-v7a)
endif(${ANDROID_ABI} STREQUAL "armeabi-v7a")
# arm64-v8a
if(${ANDROID_ABI} STREQUAL "arm64-v8a")
set(CPU_DIR arm64-v8a)
endif(${ANDROID_ABI} STREQUAL "arm64-v8a")
# x86
if(${ANDROID_ABI} STREQUAL "x86")
set(CPU_DIR x86)
endif(${ANDROID_ABI} STREQUAL "x86")
# x86_64
if(${ANDROID_ABI} STREQUAL "x86_64")
set(CPU_DIR x86_64)
endif(${ANDROID_ABI} STREQUAL "x86_64")
# 添加库
add_library( # 库名称
ffmpeg
# 动态库,生成so文件
SHARED
# 源码
${CPP_DIR}/cmdutils.c
${CPP_DIR}/ffmpeg.c
${CPP_DIR}/ffmpeg_filter.c
${CPP_DIR}/ffmpeg_opt.c
${CPP_DIR}/ffmpeg_cmd.c )
# 用于各种类型声音、图像编解码
add_library( # 库名称
avcodec
# 动态库,生成so文件
SHARED
# 表示该库是引用的不是生成的
IMPORTED )
# 引用库文件
set_target_properties( # 库名称
avcodec
# 库的路径
PROPERTIES IMPORTED_LOCATION
${LIBS_DIR}/${CPU_DIR}/libavcodec.so )
# 用于各种音视频封装格式的生成和解析,读取音视频帧等功能
add_library( avformat
SHARED
IMPORTED )
set_target_properties( avformat
PROPERTIES IMPORTED_LOCATION
${LIBS_DIR}/${CPU_DIR}/libavformat.so )
# 包含一些公共的工具函数
add_library( avutil
SHARED
IMPORTED )
set_target_properties( avutil
PROPERTIES IMPORTED_LOCATION
${LIBS_DIR}/${CPU_DIR}/libavutil.so )
# 提供了各种音视频过滤器
add_library( avfilter
SHARED
IMPORTED )
set_target_properties( avfilter
PROPERTIES IMPORTED_LOCATION
${LIBS_DIR}/${CPU_DIR}/libavfilter.so )
# 用于音频重采样,采样格式转换和混合
add_library( swresample
SHARED
IMPORTED )
set_target_properties( swresample
PROPERTIES IMPORTED_LOCATION
${LIBS_DIR}/${CPU_DIR}/libswresample.so )
# 用于视频场景比例缩放、色彩映射转换
add_library( swscale
SHARED
IMPORTED )
set_target_properties( swscale
PROPERTIES IMPORTED_LOCATION
${LIBS_DIR}/${CPU_DIR}/libswscale.so )
add_library( avdevice
SHARED
IMPORTED )
set_target_properties( avdevice
PROPERTIES IMPORTED_LOCATION
${LIBS_DIR}/${CPU_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 )
# 引用源码 ../代表上级目录
include_directories( ../../ffmpeg-4.0/
${CPP_DIR}/include/ )
# 关联库
target_link_libraries( ffmpeg
avcodec
avformat
avutil
avfilter
swresample
swscale
avdevice
${log-lib})
其中需要注意的点是
# 引用源码 ../代表上级目录
include_directories( ../../ffmpeg-4.0/
${CPP_DIR}/include/ )
将FFMpeg的源码放在你的Android项目的同级目录下,否则build时可能会提示部分头文件缺失。比如你的项目为/home/your-user-name/AndroidStudioProjects/FFMpegAndroid,则FFMpeg源码应该在/home/your-user-name/AndroidStudioProjects/ffmpeg-4.0
修改app的build.gradle中的android闭包
android {
compileSdkVersion 28
defaultConfig {
applicationId "com.example.renkangchen.ffmpegdemo"
minSdkVersion 15
targetSdkVersion 28
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
externalNativeBuild {
cmake {
cppFlags ""
arguments '-DANDROID_ARM_MODE=arm'
}
}
ndk {
abiFilters 'armeabi-v7a'
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
externalNativeBuild {
cmake {
path "CMakeLists.txt"
}
}
sourceSets {
main {
jniLibs.srcDirs = ['src/main/jniLibs']
}
}
}
修改的内容有三处
cmake {
cppFlags ""
arguments '-DANDROID_ARM_MODE=arm'
}
ndk {
abiFilters 'armeabi-v7a'
}
main {
jniLibs.srcDirs = ['src/main/jniLibs']
}
}
对照你自己的build.gradle修改即可,修改好后,点击Build->Rebuild Project,如果没有提示有问题的话,恭喜,但是,大概率会遇到
图3:Build command failed.
错误提示应该都能看明白,“在执行make某某库的过程中出错了”,其中的[1/6]表示第几个出现问题,一个个排查吧,warning和note先不用管,看error。
打开MainActivity.java可以观察一下Android Studio自动生成的代码是怎样调用native方法的,照猫画虎即可。
先
static {
System.loadLibrary("ffmpeg");
}
再
public native int runFFMpegCMD(String[] cmd);
最后调用
final String cmd = "ffmpeg -i " + path + "/test.mp4 -vframes 100 -y -f gif -s 480×320 " + path + "/video_100.gif";
int a = runFFMpegCMD(CMDUtils.splitCmd(cmd));
其中CMDUtils.splitCmd为
public static String[] splitCmd(String cmd) {
String regulation = "[ \\t]+";
final String[] split = cmd.split(regulation);
return split;
}
具体,可以写个按钮事件,按下按钮就执行截取GIF的命令,
@Override
public void onClick(View v) {
final String cmd = "ffmpeg -i " + path + "/test.mp4 -vframes 100 -y -f gif -s 480×320 " + path + "/video_100.gif";
new Thread() {
@Override
public void run() {
super.run();
int a = runFFMpegCMD(CMDUtils.splitCmd(cmd));
}
}.start();
}
其中的path可以是
private String path = Environment.getExternalStorageDirectory().getAbsolutePath();
即,在测试的时候,复制一个mp4视频文件到你的手机外存的根目录下,命名为test.mp4,运行截图的命令,如果能得到GIF,说明移植没什么大问题。
自己在移植时,各种报错,一大原因是版本问题,ndk的版本,android studio的版本,ffmpeg的版本,如果你参考本篇的步骤,请确保你使用的版本和我的是一致的;另外就是细心问题,移植过程涉及到许多的配置,修改,需要耐心细致;最后是善用搜索,但需要结合自己的实际问题。
如果是刚刚接触NDK,FFMpeg,JNI这方面的内容的话,很难“一次点亮”,多试几次。通过这个移植的过程,也可以基本对Android本地开发的流程,CMakeLists.txt的语法等有个了解。如果有问题,欢迎评论留言。
最后,能力有限,本篇提供的方法仅供参考,望指正海涵。
1.最简单的基于FFmpeg的移动端例子:Android HelloWorld
2.编译FFmpeg4.0.1并移植到Android app中使用(最详细的FFmpeg-Android编译教程)
3.Android NDK开发(四) 将FFmpeg移植到Android平台
4.Cross Compiling FFMpeg 4-0 for Android
5.Android:JNI 与 NDK到底是什么?(含实例教学),
6.Android的.so文件、ABI和CPU的关系
7.关于Android的.so文件你所需要知道的
8.CMakeLists.txt 语法介绍与实例演练
9.Linux 下ffmpeg的安装
10.下载NDK
11.下载FFMpeg源码