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++ 的项目

简单的Android视频转码器[1]:把FFMpeg移植到Android_第1张图片
图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目录下,最后得到的项目目录结构为
简单的Android视频转码器[1]:把FFMpeg移植到Android_第2张图片
图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,如果没有提示有问题的话,恭喜,但是,大概率会遇到
简单的Android视频转码器[1]:把FFMpeg移植到Android_第3张图片
图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源码

更多相关文章

  1. 如何使用Android Studio打开一个App项目,导入Android App项目需要
  2. Android教程(1) - HelloWorld及Android项目结构介绍
  3. 使用 Xcode 和 Android Studio 管理 iOS 和 Android 项目版本
  4. 总结了近百个Android优秀开源项目,覆盖Android开发的每个
  5. Android应用程序如何访问/sys和/proc等目录下的系统文件
  6. Android蓝牙通讯模块源码(Android蓝牙开发浅析 续)
  7. android使用webview预览png,pdf,doc,xls,txt,等文件
  8. Android小项目之三 splash界面
  9. Android读取工程内嵌资源文件的两种方法

随机推荐

  1. 三星Tizen手机官网现身 上市或面临风险
  2. 几行代码,让你的app动感起来--Android(安
  3. Android异步通信:图文详解Handler机制工作
  4. android基础知识16:多分辨率屏显设计及其
  5. Android手势学习之单点手势
  6. 你真的了解Android系统框架的四层结构吗?
  7. Android(安卓)Studio Jni开发(二)实现Nativ
  8. android 输入法联想问题
  9. 没有新意的Google IO 2017(下)
  10. 使用AudioRecord实现暂停录音功能