- ndk:r14
- FFmpeg版本: 3.2.5
- Android Studio: 2.3.2
本文是经过实战总结出的经验,本文将用两种方式编译可以在Android下执行命令的FFmpeg,一种是传统的ndk-build工具,一种是cmake工具,经过我的项目实战,非常推荐cmake,因为AS 2.2以后对它支持的非常好,你可以非常方便的像debug Java代码一样去debug Native代码。本文以是在假设已经编译好了全架构的 FFmpeg 的基础上进行的,编译步骤: 编译Android下可用的全平台FFmpeg(包含libx264与libfdk-aac)。
所谓传统,必然就稍微没那么智能了,我们一步一步的搞。
编写一个 native 函数,如果只是测试我们在MainActivity里面搞就行了:
public native int ffmpegRun(String[] cmd);
新建jni目录,在目录下新建文件: jx_ffmpeg_cmd_run.c;
编码对应的 JNI 接口:
#include
JNIEXPORT jint JNICALL
Java_com_mabeijianxi_jianxiffmpegcmd_MainActivity_ffmpegRun(JNIEnv *env, jobject instance, jobjectArray cmd) {
// TODO
}
找到我们FFmpeg编译后的根目录,然后 copy:
cmdutils.c cmdutils.h cmdutils_common_opts.h config.h ffmpeg.c ffmpeg.h ffmpeg_filter.c ffmpeg_opt.c(注意需要编译后才会有config.h)到 jni 目录下,再进入到我们的编译后的产物目录,把include文件夹与所有的 .so 动态库也 copy 到jni目录下。完成后你jni目录结构应该如下图:
文件修改
修改ffmpeg.c与ffmpeg.h
找到ffmpeg.c,把int main(int argc, char argv) 改名为 int jxRun(int argc, char argv)
找到ffmpeg.h, 在文件末尾添加函数申明: int jxRun(int argc, char **argv);
1)修改cmdutils.c 和 cmdutils.h
找到cmdutils.c中的exit_program函数
修改前:
修改后:
2)找到cmdutils.h中exit_program的申明,也把返回类型修改为int。
修改前:
修改后:
很多教程都只修改到这里,基本没什么问题,但是你实际运行的时候会发现如果连续多次执行命令会有问题的,通过源码我们可以知道,FFmpeg每次执行完命令后会调用 ffmpeg_cleanup 函数清理内存,并且会调用exit(0)结束当前进程,但是经过我们的修改,exit()的代码已经被删掉,我们在Android中自然不能结束当前进程了,所以有些变量的值还在内存中,这样就会导致下次执行的时候可能会出错。我也尝试过fork一个进程给ffmpeg执行,完事后通过 信号来进程间通信,这样管用但是很麻烦,我们其实只需要简单的重设一些变量即可。
打开ffmpeg.c找到刚修改的jxRun函数,然后在 return 前加上如下代码即可:
nb_filtergraphs = 0;
progress_avio = NULL;
input_streams = NULL;
nb_input_streams = 0;
input_files = NULL;
nb_input_files = 0;
output_streams = NULL;
nb_output_streams = 0;
output_files = NULL;
nb_output_files = 0;
编写调用函数
我们上面只在jx_ffmpeg_cmd_run.c新建了一个JNI接口函数,还没有实现逻辑,我们实现后的代码如下:
**
* Created by jianxi on 2017/6/4.
* https://github.com/mabeijianxi
* [email protected]
*
#include "ffmpeg.h"
#include
JNIEXPORT jint JNICALL
Java_com_mabeijianxi_jianxiffmpegcmd_MainActivity_ffmpegRun(JNIEnv *env, jobject type,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 jxRun(argc,argv);
}
APP_ABI := armeabi-v7a
APP_PLATFORM := android-14
3)编码Android.mk,其内容如下(不明含义的可看Android下玩JNI的新老三种姿势LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := libavcodec
LOCAL_SRC_FILES := libavcodec.so
include $(PREBUILT_SHARED_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE := libavfilter
LOCAL_SRC_FILES := libavfilter.so
include $(PREBUILT_SHARED_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE := libavformat
LOCAL_SRC_FILES := libavformat.so
include $(PREBUILT_SHARED_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE := libavutil
LOCAL_SRC_FILES := libavutil.so
include $(PREBUILT_SHARED_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE := libswresample
LOCAL_SRC_FILES := libswresample.so
include $(PREBUILT_SHARED_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE := libswscale
LOCAL_SRC_FILES := libswscale.so
include $(PREBUILT_SHARED_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE := jxffmpegrun
LOCAL_SRC_FILES := cmdutils.c ffmpeg.c ffmpeg_filter.c ffmpeg_opt.c jx_ffmpeg_cmd_run.c
# 这里的地址改成自己的 FFmpeg 源码目录
LOCAL_C_INCLUDES :=/Users/jianxi/Downloads/code/ffmpeg-3.2.5
LOCAL_LDLIBS := -llog -lz -ldl
LOCAL_SHARED_LIBRARIES :=libavcodec libavfilter libavformat libavutil libswresample libswscale
include $(BUILD_SHARED_LIBRARY)
9. 开始编译:
我们打开终端,然后cd 到jni目录下,然后执行ndk-build,如果你没配置其环境变量,你选择写入ndk-build的全路径。
如果顺利你可以在命令结束后看到如下输出:
并且你的lib下会有如下产物:
这里就先不上测试的效果图了,等后面一起搞
有好多步骤都是很上面一样的,一会儿我就快乐的copy下来哈
新建完成后你会发现帮你生成好了接口,这时我们只需修改native函数即可:
修改前:
public native String stringFromJNI();
修改后
public native int ffmpegRun(String[] cmd);
修改native文件与函数:
你会发现这时多了一个cpp的文件夹,里面多了一个native-lib.cpp的文件,我们修改其名为jx_ffmpeg_cmd_run.c,然后修改里面的函数,修改后的函数应该如下:
#include
JNIEXPORT jint JNICALL
Java_com_mabeijianxi_jianxiffmpegcmd_MainActivity_ffmpegRun(JNIEnv *env, jobject instance, jobjectArray cmd) {
// TODO
}
文件修改
修改ffmpeg.c与ffmpeg.h
找到ffmpeg.c,把int main(int argc, char argv) 改名为 int jxRun(int argc, char argv)
找到ffmpeg.h, 在文件末尾添加函数申明: int jxRun(int argc, char **argv);
1)修改cmdutils.c 和 cmdutils.h
找到cmdutils.c中的exit_program函数
修改前:
修改后:
2)找到cmdutils.h中exit_program的申明,也把返回类型修改为int。
修改前:
修改后:
很多教程都只修改到这里,基本没什么问题,但是你实际运行的时候会发现如果连续多次执行命令会有问题的,通过源码我们可以知道,FFmpeg每次执行完命令后会调用 ffmpeg_cleanup 函数清理内存,并且会调用exit(0)结束当前进程,但是经过我们的修改,exit()的代码已经被删掉,我们在Android中自然不能结束当前进程了,所以有些变量的值还在内存中,这样就会导致下次执行的时候可能会出错。我也尝试过fork一个进程给ffmpeg执行,完事后通过 信号来进程间通信,这样管用但是很麻烦,我们其实只需要简单的重设一些变量即可。
打开ffmpeg.c找到刚修改的jxRun函数,然后在 return 前加上如下代码即可:
nb_filtergraphs = 0;
progress_avio = NULL;
input_streams = NULL;
nb_input_streams = 0;
input_files = NULL;
nb_input_files = 0;
output_streams = NULL;
nb_output_streams = 0;
output_files = NULL;
nb_output_files = 0;
编写调用函数
我们上面只在jx_ffmpeg_cmd_run.c新建了一个JNI接口函数,还没有实现逻辑,我们实现后的代码如下:
\**
\* Created by jianxi on 2017/6/4.
\* https://github.com/mabeijianxi
\* [email protected]
*/
#include "ffmpeg.h"
#include
JNIEXPORT jint JNICALL
Java_com_mabeijianxi_jianxiffmpegcmd_MainActivity_ffmpegRun(JNIEnv *env, jobject type,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 jxRun(argc,argv);
}
# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html
# Sets the minimum version of CMake required to build the native library.
cmake_minimum_required(VERSION 3.4.1)
# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.
add_library( # Sets the name of the library.
jxffmpegrun
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
src/main/cpp/cmdutils.c
src/main/cpp/ffmpeg.c
src/main/cpp/ffmpeg_filter.c
src/main/cpp/ffmpeg_opt.c
src/main/cpp/jx_ffmpeg_cmd_run.c
)
add_library(
avcodec
SHARED
IMPORTED
)
add_library(
avfilter
SHARED
IMPORTED
)
add_library(
avformat
SHARED
IMPORTED
)
add_library(
avutil
SHARED
IMPORTED
)
add_library(
swresample
SHARED
IMPORTED
)
add_library(
swscale
SHARED
IMPORTED
)
add_library(
fdk-aac
SHARED
IMPORTED
)
if(${ANDROID_ABI} STREQUAL "armeabi")
set_target_properties(
avcodec
PROPERTIES IMPORTED_LOCATION
${CMAKE_SOURCE_DIR}/src/main/jniLibs/armeabi/libavcodec.so
)
set_target_properties(
avfilter
PROPERTIES IMPORTED_LOCATION
${CMAKE_SOURCE_DIR}/src/main/jniLibs/armeabi/libavfilter.so
)
set_target_properties(
avformat
PROPERTIES IMPORTED_LOCATION
${CMAKE_SOURCE_DIR}/src/main/jniLibs/armeabi/libavformat.so
)
set_target_properties(
avutil
PROPERTIES IMPORTED_LOCATION
${CMAKE_SOURCE_DIR}/src/main/jniLibs/armeabi/libavutil.so
)
set_target_properties(
swresample
PROPERTIES IMPORTED_LOCATION
${CMAKE_SOURCE_DIR}/src/main/jniLibs/armeabi/libswresample.so
)
set_target_properties(
swscale
PROPERTIES IMPORTED_LOCATION
${CMAKE_SOURCE_DIR}/src/main/jniLibs/armeabi/libswscale.so
)
set_target_properties(
fdk-aac
PROPERTIES IMPORTED_LOCATION
${CMAKE_SOURCE_DIR}/src/main/jniLibs/armeabi/libfdk-aac.so
)
endif(${ANDROID_ABI} STREQUAL "armeabi")
if(${ANDROID_ABI} STREQUAL "armeabi-v7a")
set_target_properties(
avcodec
PROPERTIES IMPORTED_LOCATION
${CMAKE_SOURCE_DIR}/src/main/jniLibs/armeabi-v7a/libavcodec.so
)
set_target_properties(
avfilter
PROPERTIES IMPORTED_LOCATION
${CMAKE_SOURCE_DIR}/src/main/jniLibs/armeabi-v7a/libavfilter.so
)
set_target_properties(
avformat
PROPERTIES IMPORTED_LOCATION
${CMAKE_SOURCE_DIR}/src/main/jniLibs/armeabi-v7a/libavformat.so
)
set_target_properties(
avutil
PROPERTIES IMPORTED_LOCATION
${CMAKE_SOURCE_DIR}/src/main/jniLibs/armeabi-v7a/libavutil.so
)
set_target_properties(
swresample
PROPERTIES IMPORTED_LOCATION
${CMAKE_SOURCE_DIR}/src/main/jniLibs/armeabi-v7a/libswresample.so
)
set_target_properties(
swscale
PROPERTIES IMPORTED_LOCATION
${CMAKE_SOURCE_DIR}/src/main/jniLibs/armeabi-v7a/libswscale.so
)
set_target_properties(
fdk-aac
PROPERTIES IMPORTED_LOCATION
${CMAKE_SOURCE_DIR}/src/main/jniLibs/armeabi-v7a/libfdk-aac.so
)
endif(${ANDROID_ABI} STREQUAL "armeabi-v7a")
if(${ANDROID_ABI} STREQUAL "arm64-v8a")
set_target_properties(
avcodec
PROPERTIES IMPORTED_LOCATION
${CMAKE_SOURCE_DIR}/src/main/jniLibs/arm64-v8a/libavcodec.so
)
set_target_properties(
avfilter
PROPERTIES IMPORTED_LOCATION
${CMAKE_SOURCE_DIR}/src/main/jniLibs/arm64-v8a/libavfilter.so
)
set_target_properties(
avformat
PROPERTIES IMPORTED_LOCATION
${CMAKE_SOURCE_DIR}/src/main/jniLibs/arm64-v8a/libavformat.so
)
set_target_properties(
avutil
PROPERTIES IMPORTED_LOCATION
${CMAKE_SOURCE_DIR}/src/main/jniLibs/arm64-v8a/libavutil.so
)
set_target_properties(
swresample
PROPERTIES IMPORTED_LOCATION
${CMAKE_SOURCE_DIR}/src/main/jniLibs/arm64-v8a/libswresample.so
)
set_target_properties(
swscale
PROPERTIES IMPORTED_LOCATION
${CMAKE_SOURCE_DIR}/src/main/jniLibs/arm64-v8a/libswscale.so
)
set_target_properties(
fdk-aac
PROPERTIES IMPORTED_LOCATION
${CMAKE_SOURCE_DIR}/src/main/jniLibs/arm64-v8a/libfdk-aac.so
)
endif(${ANDROID_ABI} STREQUAL "arm64-v8a")
if(${ANDROID_ABI} STREQUAL "x86")
set_target_properties(
avcodec
PROPERTIES IMPORTED_LOCATION
${CMAKE_SOURCE_DIR}/src/main/jniLibs/x86/libavcodec.so
)
set_target_properties(
avfilter
PROPERTIES IMPORTED_LOCATION
${CMAKE_SOURCE_DIR}/src/main/jniLibs/x86/libavfilter.so
)
set_target_properties(
avformat
PROPERTIES IMPORTED_LOCATION
${CMAKE_SOURCE_DIR}/src/main/jniLibs/x86/libavformat.so
)
set_target_properties(
avutil
PROPERTIES IMPORTED_LOCATION
${CMAKE_SOURCE_DIR}/src/main/jniLibs/x86/libavutil.so
)
set_target_properties(
swresample
PROPERTIES IMPORTED_LOCATION
${CMAKE_SOURCE_DIR}/src/main/jniLibs/x86/libswresample.so
)
set_target_properties(
swscale
PROPERTIES IMPORTED_LOCATION
${CMAKE_SOURCE_DIR}/src/main/jniLibs/x86/libswscale.so
)
set_target_properties(
fdk-aac
PROPERTIES IMPORTED_LOCATION
${CMAKE_SOURCE_DIR}/src/main/jniLibs/x86/libfdk-aac.so
)
endif(${ANDROID_ABI} STREQUAL "x86")
if(${ANDROID_ABI} STREQUAL "x86_64")
set_target_properties(
avcodec
PROPERTIES IMPORTED_LOCATION
${CMAKE_SOURCE_DIR}/src/main/jniLibs/x86_64/libavcodec.so
)
set_target_properties(
avfilter
PROPERTIES IMPORTED_LOCATION
${CMAKE_SOURCE_DIR}/src/main/jniLibs/x86_64/libavfilter.so
)
set_target_properties(
avformat
PROPERTIES IMPORTED_LOCATION
${CMAKE_SOURCE_DIR}/src/main/jniLibs/x86_64/libavformat.so
)
set_target_properties(
avutil
PROPERTIES IMPORTED_LOCATION
${CMAKE_SOURCE_DIR}/src/main/jniLibs/x86_64/libavutil.so
)
set_target_properties(
swresample
PROPERTIES IMPORTED_LOCATION
${CMAKE_SOURCE_DIR}/src/main/jniLibs/x86_64/libswresample.so
)
set_target_properties(
swscale
PROPERTIES IMPORTED_LOCATION
${CMAKE_SOURCE_DIR}/src/main/jniLibs/x86_64/libswscale.so
)
set_target_properties(
fdk-aac
PROPERTIES IMPORTED_LOCATION
${CMAKE_SOURCE_DIR}/src/main/jniLibs/x86_64/libfdk-aac.so
)
endif(${ANDROID_ABI} STREQUAL "x86_64")
include_directories(
/Users/jianxi/Downloads/code/ffmpeg-3.2.5/
)
# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.
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 )
# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.
target_link_libraries( # Specifies the target library.
jxffmpegrun
fdk-aac
avcodec
avfilter
avformat
avutil
swresample
swscale
# Links the target library to the log library
# included in the NDK.
${log-lib} )
当然你还需要修改脚本里面的一些文件路径。
8 . 修改当前Module下的build.gradle脚本:
我所谓的全架构其实是排除了 mips 的,觉得没什么卵用,所以我们还需要添加过滤:
修改前:
修改后:
做到这步我们就可以直接点击同步按钮了:
完成后就不会再爆红了,这时候是只能直接运行安装的,但是还没有输入命令,所以没什么意义,接下来将开始使用我们做好的工具。
我们要用动态库肯定得放入默认目录,或者在gradle.build中修改其路径,这里我就直接放入
jniLibs 里面了,如图:
然后我就是在java里面调用了,搞了一个进度条,一个按钮。没有什么技术含量,直接贴代码了哈:
public class MainActivity extends AppCompatActivity {
static {
System.loadLibrary("jxffmpegrun");
System.loadLibrary("avcodec");
System.loadLibrary("avformat");
System.loadLibrary("avutil");
System.loadLibrary("swscale");
System.loadLibrary("fdk-aac");
}
private ProgressBar pb;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
pb = (ProgressBar) findViewById(R.id.pb);
}
public void onClick(View v){
pb.setVisibility(View.VISIBLE);
new Thread(new Runnable() {
@Override
public void run() {
String basePath = Environment.getExternalStorageDirectory().getPath();
String cmd_transcoding = String.format("ffmpeg -i %s -c:v libx264 %s -c:a libfdk_aac %s",
basePath+"/"+"girl.mp4",
"-crf 40",
basePath+"/"+"my_girl.mp4");
int i = jxFFmpegCMDRun(cmd_transcoding);
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
pb.setVisibility(View.GONE);
Toast.makeText(MainActivity.this,"ok了",Toast.LENGTH_SHORT).show();
}
});
}
}).start();
}
public int jxFFmpegCMDRun(String cmd){
String regulation="[ \\t]+";
final String[] split = cmd.split(regulation);
return ffmpegRun(split);
}
public native int ffmpegRun(String[] cmd);
}
再上面代码中我们指定编码器压缩了一个mp4的文件,压缩前叫girl.mp4,压缩后叫my_girl.mp4,如图所示,我们发现其缩小了近80%。
这里输入路径不能每次是同一个,不然会报错,也就是说,运行了一次上面的命令后,输出的名字不能再叫my_girl.mp4了,你可以叫your_girl.mp4。然后还是那句话推荐用cMake,你看我们通过cMake编译可以直接debug native
是不是很hi,然后我们在开发中可能会发现有些命令执行不了,这时候你首先需要检查命令,然后确保没问题后,你需要确定你在编译FFmpeg的时候是否开启了此功能,很关键,确定办法很简单,一是检查FFmpeg的编译脚本,二是通过FFmpeg的函数获取编译信息,我工程里面已经内含这个jni接口,有兴趣的基友可以下载来try一try。如果有兴趣进一步探索FFmpeg在Android上的实战运用,可以看我的开源项目和其讲解博客,分别是:https://github.com/mabeijianxi/small-video-record、利用FFmpeg玩转Android视频录制与压缩(一)、利用FFmpeg玩转Android视频录制与压缩(二)。
本文所有源码地址:https://github.com/mabeijianxi/FFmpeg4Android