简单的Android视频转码器[1]:把FFMpeg移植到Android
1 项目介绍
1.1 项目介绍
FFMpeg是做音视频开发的同学都会接触的一个开源项目,现将其移植到Android上,写一个简单的视频格式转码工具,作为自己Android jni开发的一个入门学习和Android 开发的练习。为了简化开发,项目中使用命令行的方式调用ffmpeg而不是直接用ffmpeg提供的函数进行本地开发。
除了视频转换格式外,项目还设计了视频GIF截取,视频压缩等等功能,这些都是使用ffmpeg很容易实现的功能。
1.2 开发环境说明
- Ubuntu 18.04
- Android Studio 3.2
- jdk1.8.0_191
- Android NDK, Revision 15c (July 2017)
- FFmpeg 4.0.3 “Wu”
如果你按照本篇的步骤来进行FFMpeg的移植,请确保你的开发环境使用的版本和上述的一致。
1.3 特别感谢
特别感谢几位博主的分享,
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
本篇中主要的配置修改均参考他们的博客,在此表示感谢。
2 JNI,NDK,ABI,.so,CMakeLists.txt
在开始之前有必要了解几个概念,
1.Android:JNI 与 NDK到底是什么?(含实例教学),
2.Android的.so文件、ABI和CPU的关系
3.关于Android的.so文件你所需要知道的
4.CMakeLists.txt 语法介绍与实例演练
几位博主都写得很清晰,需要的同学请移步去看看。
3 选择合适的NDK
从这里下载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。
4 FFMpeg源码编译
4.1 下载和必要条件
在这里下载FFMpeg源码,注意版本的选择,本项目使用的 FFmpeg 4.0.3 “Wu” 这个版本,如果使用其他版本,则不保证你按照我分享的步骤来做而不出错。
关于下载编译源码所需的注意的点,参考我之前的分享
Linux 下ffmpeg的安装
下载好FFMpeg的源码和编译所需的工具后,进入下一步。
4.2 修改configure
解压你下载的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)'
4.3 准备编译脚本
假设/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/bashNDK=/home/your-user-name/Android/Sdk/ndk-bundleSYSROOT=$NDK/platforms/android-19/arch-arm/TOOLCHAIN=$NDK/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86_64function 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=armPREFIX=$(pwd)/android/$CPUADDI_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/ 目录下有
- bin
- include
- lib
- share
其中include中的头文件和lib中的库文件,是下一步所需的。
5 创建项目,导入文件
5.1 创建支持C++ 的项目,导入库文件,头文件
5.1.1 创建支持C++ 的项目
图1:新建Android 项目
如图1,新建一个Android项目,注意勾选“Include C++ support”,然后一路点next即可,最后完成即可。
5.1.2 导入库文件
然后在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文件放进去。
5.1.3 导入头文件
然后把4.3 中的到的include复制到app/src/main/cpp下
5.2 修改FFMpeg源码
实现命令行使用FFMpeg的大概的思路是,利用从FFMpeg源码中“搬运”的部分代码,编译成一个可供Android调用本地动态库,通过jni调用来实现我们的目的。
从你的FFMpeg源码的/fftools/目录下,复制如下的几个文件(比如/home/your-user-name/Downloads/ffmpeg-4.0/fftools/)
- cmdutils.c
- cmdutils.h
- ffmpeg.c
- ffmpeg.h
- ffmpeg_filter.c
- ffmpeg_opt.c
5.2.1 修改cmdutils
修改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);}
即注释掉退出函数的内容
5.2.2 修改ffmpeg.c
修改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
5.3 编写ffmpeg_cmd.c
直接复制cpp目录下,Android Studio生成的native-lib.cpp,重命名为ffmpeg_cmd.c(名称随意),native-lib.cpp的内容如下
#include #include extern "C" JNIEXPORT jstring JNICALLJava_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 jintJNICALLJava_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.4 导入C代码
把5.2,5.3中的C文件复制到cpp目录下,最后得到的项目目录结构为
图2 项目目录结构
6 修改CmakeLists.txt,build项目
# 设置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目录 armeabiif(${ANDROID_ABI} STREQUAL "armeabi")set(CPU_DIR armeabi)endif(${ANDROID_ABI} STREQUAL "armeabi")# armeabi-v7aif(${ANDROID_ABI} STREQUAL "armeabi-v7a")set(CPU_DIR armeabi-v7a)endif(${ANDROID_ABI} STREQUAL "armeabi-v7a")# arm64-v8aif(${ANDROID_ABI} STREQUAL "arm64-v8a")set(CPU_DIR arm64-v8a)endif(${ANDROID_ABI} STREQUAL "arm64-v8a")# x86if(${ANDROID_ABI} STREQUAL "x86")set(CPU_DIR x86)endif(${ANDROID_ABI} STREQUAL "x86")# x86_64if(${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。
7 测试
打开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,说明移植没什么大问题。
8 总结
自己在移植时,各种报错,一大原因是版本问题,ndk的版本,android studio的版本,ffmpeg的版本,如果你参考本篇的步骤,请确保你使用的版本和我的是一致的;另外就是细心问题,移植过程涉及到许多的配置,修改,需要耐心细致;最后是善用搜索,但需要结合自己的实际问题。
如果是刚刚接触NDK,FFMpeg,JNI这方面的内容的话,很难“一次点亮”,多试几次。通过这个移植的过程,也可以基本对Android本地开发的流程,CMakeLists.txt的语法等有个了解。如果有问题,欢迎评论留言。
最后,能力有限,本篇提供的方法仅供参考,望指正海涵。
Reference
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源码
更多相关文章
- 如何使用Android Studio打开一个App项目,导入Android App项目需要
- Android教程(1) - HelloWorld及Android项目结构介绍
- 使用 Xcode 和 Android Studio 管理 iOS 和 Android 项目版本
- 总结了近百个Android优秀开源项目,覆盖Android开发的每个
- Android应用程序如何访问/sys和/proc等目录下的系统文件
- Android蓝牙通讯模块源码(Android蓝牙开发浅析 续)
- android使用webview预览png,pdf,doc,xls,txt,等文件
- Android小项目之三 splash界面
- Android读取工程内嵌资源文件的两种方法