编译Android下可执行命令的FFmpeg


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


本人环境与工具:


  • 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命令编译


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

  1. 打开你养家糊口的Android Studio,娴熟的新建一个项目;
  2. 编写一个 native 函数,如果只是测试我们在MainActivity里面搞就行了:

    public native int ffmpegRun(String[] cmd);
  3. 新建jni目录,在目录下新建文件: jx_ffmpeg_cmd_run.c;

  4. 编码对应的 JNI 接口:

    #include 
    JNIEXPORT jint JNICALL Java_com_mabeijianxi_jianxiffmpegcmd_MainActivity_ffmpegRun(JNIEnv *env, jobject instance, jobjectArray cmd) { // TODO }
  5. 找到我们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目录结构应该如下图:


  6. 文件修改
    修改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;
  7. 编写调用函数
    我们上面只在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);
    }
  8. 编写Application.mkAndroid.mk
    1)在jni目录下新建Application.mkAndroid.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下来哈

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




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

    public native String stringFromJNI();

    修改后

    public native int ffmpegRun(String[] cmd);
  3. 修改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
    }
  4. 找到我们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目录结构应该如下图:

  1. 文件修改
    修改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;
  2. 编写调用函数
    我们上面只在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);
    }
  3. 编写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

你可能感兴趣的:(音视频)