您的位置:首页 > 移动开发 > Android开发

编译Android下可执行命令的FFmpeg

2017-12-07 15:31 513 查看


请尊重原创,转载请注明出处:http://blog.csdn.net/mabeijianxi/article/details/72904694

http://blog.csdn.net/mabeijianxi/article/details/74544879   编译Android下可用的全平台FFmpeg(包含libx264与libfdk-aac)


本人环境与工具:

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)


二、传统ndk-build命令编译

所谓传统,必然就稍微没那么智能了,我们一步一步的搞。

打开你养家糊口的Android Studio,娴熟的新建一个项目;

编写一个 native 函数,如果只是测试我们在MainActivity里面搞就行了:

public native int ffmpegRun(String[] cmd);


新建jni目录,在目录下新建文件: jx_ffmpeg_cmd_run.c;

编码对应的 JNI 接口:
#include <jni.h><br>
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 * mabeijianxi@gmail.com
*
#include "ffmpeg.h"
#include <jni.h>
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);
}


编写Application.mk与Android.mk

1)在jni目录下新建Application.mk与Android.mk

2)编写Application.mk,内容如下:
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下会有如下产物:



这里就先不上测试的效果图了,等后面一起搞


三、cMake编译

有好多步骤都是很上面一样的,一会儿我就快乐的copy下来哈

打开你养家糊口的Android Studio,版本最好大于2.2,很关键。娴熟的新建一个项目,但是与以往不同,你最好勾选上 C++ 支持与 C++ standard选项时选择 C++
11,如下图:





新建完成后你会发现帮你生成好了接口,这时我们只需修改native函数即可:

修改前:

public native String stringFromJNI();


修改后

public native int ffmpegRun(String[] cmd);


修改native文件与函数:

你会发现这时多了一个cpp的文件夹,里面多了一个native-lib.cpp的文件,我们修改其名为jx_ffmpeg_cmd_run.c,然后修改里面的函数,修改后的函数应该如下:

#include <jni.h>
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)到 cpp 目录下,再进入到我们的编译后的产物目录,把include文件夹
copy 到cpp目录下。完成后你cpp目录结构应该如下图:



文件修改

修改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 \* mabeijianxi@gmail.com
*/
#include "ffmpeg.h"
#include <jni.h>
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);
}


编写cmake编译脚本:

在没有编写脚本的时候你的代码应该是一片红,没关系,马上我们就干掉它。

打开你当前Module下的CMakeLists.txt然后填写如下脚本(内容解释可看Android下玩JNI的新老三种姿势):

# 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
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: