Android studio 3.0 集成 FFmpeg - 从编译到配置

本贴最后更新于 2240 天前,其中的信息可能已经时移世易

这篇文章的重点在于编译 FFmpeg 库和 Android studio 3.0 中配置 FFmpeg 相关文件,所以如果是需要了解 FFmpeg 的实际应用的可以不用继续往下看了。因为我自己在整个过程中遇到很多的坑,所以我在整个过程中都用云笔记记录了下来,希望帮助到后来需要的同学,文章涉及的所有步骤我都是亲自尝试过的,如果有不正确的地方,烦请指正。

环境准备

1. ubuntu
阿里云服务器 Ubuntu 16.04.3 LTS
2. ndk
android-ndk-r15c

国内可能无法访问,可以通过如下命令==下载==:

wget https://dl.google.com/android/repository/android-ndk-r15c-linux-x86_64.zip

如果需要下载其他版本的可以查阅这篇博客

下载完成后,使用如下命令==解压==:

sudo unzip android-ndk-r15c-linux-x86_64.zip

然后把 ndk 的路径==加入环境变量==,使用如下命令:

vim /etc/profile

然后在文件末尾加入 ndk 的路径,内容如下:

export ANDROID_NDK=/home/download/android-ndk-r15c
export PATH=$ANDROID_NDK:$PATH

然后使用如下命令,是环境变量生效:

source /etc/profile

==NOTE==:上面这个命令只是临时生效,重新打开一个终端就失效了,优点是不用重启系统就能马上生效

3. ffmpeg

到官网下载 ffmpeg,或者使用命令下载,我这里下载的是 4.0.2 的版本:

wget https://ffmpeg.org/releases/ffmpeg-4.0.2.tar.bz2

然后解压:

tar -jxvf ffmpeg-4.0.2.tar.bz2

开始编译

进入 ffmpeg 解压完成后的根目录,为了方便多次编译,我们可以将编译的命令写入一个 shell 脚本中,以后每次更改编译参数重新运行脚本就可以了。

编译脚本:

注意:
  1. shell 脚本的格式问题,windows 上创建的 shell 脚本在 linux 上可能因为格式问题识别不了;
  2. ./configure \和下面的参数之间不能有空格;
  3. 编译平台对应的 android 版本最好使用 android-9,不然到低于这个版本的 android 平台上可以会报错;
  4. make 到一半,卡住不动,可能是 linux 内存不够了,通过命令 free -m 查看内存,-m 表示以 M 为单位显示内存。

android 项目中使用 FFmpeg

上面编译完成之后会在编译脚本指定的 PREFIX 的路径下生成 include、lib、share 三个文件夹,include 中是 FFmpeg 的方法的头文件,lib 是生成的 so 动态链接库,share 里面有一些 FFmpeg 的示例程序。

这里我们使用 Android Studio 3.0 来创建 android 工程,从 as 2.2 之后,我们开始用 cmake 编译 jni,所以我们用 CMakeLists.txt 来代替 Android.mk。略过使用 as 创建 android ndk 项目,创建完成后,我们开始配置。

1. 导入 ffmpeg 相关文件

在 src/main 下新建 ffmpeg 文件夹,然后将编译生成的包含所有 ffmpeg 头文件的整个 include 文件夹拷贝到该目录下,因为我们是编译的 armeabi-v7a 平台下的 so 库,所以在 ffmpeg 目录下新建 armeabi-v7a 文件夹,然后将所有 so 库拷贝到该文件夹下,最终目录结构如下:

imagepng

2. 配置 CMakeList.txt
# CMake的最低版本
cmake_minimum_required(VERSION 3.4.1)

add_library( native-lib
             SHARED
             src/main/cpp/native-lib.cpp )

find_library( log-lib
              log )

set(JNI_LIBS_DIR ${CMAKE_SOURCE_DIR}/src/main/ffmpeg)

add_library(avutil
            SHARED
            IMPORTED )
set_target_properties(avutil
                      PROPERTIES IMPORTED_LOCATION
                      ${JNI_LIBS_DIR}/${ANDROID_ABI}/libavutil.so )

add_library(swresample
            SHARED
            IMPORTED )
set_target_properties(swresample
                      PROPERTIES IMPORTED_LOCATION
                      ${JNI_LIBS_DIR}/${ANDROID_ABI}/libswresample.so )

add_library(swscale
            SHARED
            IMPORTED )
set_target_properties(swscale
                      PROPERTIES IMPORTED_LOCATION
                      ${JNI_LIBS_DIR}/${ANDROID_ABI}/libswscale.so )

add_library(avcodec
            SHARED
            IMPORTED )
set_target_properties(avcodec
                      PROPERTIES IMPORTED_LOCATION
                      ${JNI_LIBS_DIR}/${ANDROID_ABI}/libavcodec.so )

add_library(avformat
            SHARED
            IMPORTED )
set_target_properties(avformat
                      PROPERTIES IMPORTED_LOCATION
                      ${JNI_LIBS_DIR}/${ANDROID_ABI}/libavformat.so )

add_library(avfilter
            SHARED
            IMPORTED )
set_target_properties(avfilter
                      PROPERTIES IMPORTED_LOCATION
                      ${JNI_LIBS_DIR}/${ANDROID_ABI}/libavfilter.so )

add_library(avdevice
            SHARED
            IMPORTED )
set_target_properties(avdevice
                      PROPERTIES IMPORTED_LOCATION
                      ${JNI_LIBS_DIR}/${ANDROID_ABI}/libavdevice.so )

include_directories(src/main/ffmpeg/include)

target_link_libraries(  native-lib
                        avutil
                        swresample
                        swscale
                        avcodec
                        avformat
                        avfilter
                        avdevice
                        android
                        ${log-lib})

说明:

  • cmake_minimum_required:指定 cmake 的最低版本
  • set():这里相当于定义一个变量,后面通过 ${var}来引用这个变量,我们这里是指定了 ffmpeg 库文件根目录
  • add_library 和 set_target_properties:添加库文件,ffmpeg 的 8 个 so 库我们都需要通过这两个方法引入
  • include_directories:这里==指定 ffmpeg 的头文件的路径==,如果这里不指定的话,我们在编写代码时 include 头文件需要写很长的路径,当然还有个更重要的原因是,ffmpeg 本身的头文件之间互相引用就是默认这个路径作为根目录的,如果不指定这个目录,编译的时候会在 ffmpeg 的头文件里面报错,说找不到其他头文件,ffmpeg 的自己的头文件引用是下面的格式:
#include "libavcodec/avcodec.h"
#include "libavutil/dict.h"
#include "libavutil/log.h"

看到这里我们就能明白为什么一定要指定这个头文件的根目录了

  • target_link_libraries:链接 so 库。这里有个要注意的地方就是:如果我们在编写代码时使用了 ANativeWindow 相关的方法的话,需要在链接库文件的时候加入 android 这个库文件,不然会报 undefined reference to 的错
3. 修改 build.gradle 文件
android {
    ......
    defaultConfig {
        externalNativeBuild {
            cmake {
                cppFlags "-fexceptions"
                abiFilters "armeabi-v7a"
            }
        }
    }
	// 加上这个,打包apk的时候才会将ffmpeg的so库打包进libs目录里面
	sourceSets {
		main {
			jni.srcDirs = []
			jniLibs.srcDirs = ['src/main/ffmpeg']
		}
	}
    ......
}
4. 编写 cpp 文件使用 ffmpeg

上面配置完成之后,我们就可以编写代码,然后在代码中使用 ffmpeg 的方法了。

在 src/main/java 的包下创建我们的 java 类,然后在类中编写 native 方法,然后通过 System.loadLibrary 加载我们需要的 so 库,如下所示:

static {
        System.loadLibrary("avutil");
        System.loadLibrary("swresample");
        System.loadLibrary("swscale");
        System.loadLibrary("avcodec");
        System.loadLibrary("avformat");
        System.loadLibrary("avfilter");
        System.loadLibrary("avdevice");
    }

public native void decode(String input, String output);

然后在命令行下,进入项目根目录 app/src/main/java/目录下,使用命令生成头文件:

javah 包名.类名

然后我们在创建 ndk 项目时自动生成的 cpp 文件里面加入一个方法,方法名和参数就是我们刚生成的头文件里面声明的方法,示例如下:

#include <jni.h>
#include <string>
#include <android/log.h>
#include <android/native_window_jni.h>
#include "libyuv.h"
extern "C"
{
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libavutil/imgutils.h"
}


// __android_log_print()需要<android/log.h>头文件
#define LOGI(FORMAT,...) __android_log_print(ANDROID_LOG_INFO, "sisyphus", FORMAT, ##__VA_ARGS__)
#define LOGE(FORMAT,...) __android_log_print(ANDROID_LOG_ERROR, "sisyphus", FORMAT, ##__VA_ARGS__)

extern "C"
JNIEXPORT jstring

JNICALL
Java_com_sisyphus_ffmpegdemo_MainActivity_stringFromJNI(
        JNIEnv *env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}

extern "C"
JNIEXPORT void JNICALL
Java_com_sisyphus_ffmpegdemo_FFmpegPlayer_decode(JNIEnv *env, jobject instance, jstring input_,
                                                 jstring output_, jobject surface) {
    const char *input = env->GetStringUTFChars(input_, 0);
    const char *output = env->GetStringUTFChars(output_, 0);

    av_register_all();

    env->ReleaseStringUTFChars(input_, input);
    env->ReleaseStringUTFChars(output_, output);
}

这里还有最后一个需要注意的地方就是:因为 ffmpeg 是用 c 语言编写的,我们是在 cpp 文件里面使用的,所以我们在 include 他的头文件时需要将头文件包含在 extern "C" {}里面,如下:

extern "C"
{
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libavutil/imgutils.h"
}

不然的话,虽然我们在编写代码的时候不会报错,在最后编译的时候就会报错说我们使用的方法都没有定义。

5. 编译

点击菜单 Build->Make Project 编译项目,最后会在项目根目录的\app\build\intermediates\cmake\debug\obj\armeabi-v7a 下生成一个 so 库。

打完收工

  • FFmpeg

    FFmpeg 是一套可以用来记录、转换数字音频、视频,并能将其转化为流的开源计算机程序。

    23 引用 • 32 回帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...