FFmpeg交叉编译与接入Android工程

记录FFmpeg的交叉编译与将动态库接入Android工程

Posted by 袁平 on November 17, 2018

前言

本文主要讲解如何在Linux上编译FFmpeg, 以及将编译出来的动态库(so)接入已有的Android工程


正文


一. FFmpeg交叉编译

本文选用的FFmpeg版本是FFmpeg 4.0, NDK版本是Android-ndk-r15c, 注意在编译FFmpeg时, 对NDK版本有要求, 另外, 笔者最开始选用最新版本的NDK, 总是编译不过, 去查证了一下, 发现从Androud-ndk-r16开始, 其用于交叉编译都有问题了, 所以选用较低版本的Androud-ndk; 本文选用的FFmpeg版本和NDK版本亲测可编译通过; 先提供下载地址:

FFmpeg 4.0

Android-ndk-r15c

笔者选用的编译环境是ArchLinux, Windows上的编译的话, 大体流程不变, 环境配置需要自己弄啦~

在开始之前, 需要先了解一下相关知识:

  1. JNI和NDK
  2. CPU架构
  3. 交叉编译
  4. FFmpeg简介

这里推荐官方文档, 对以上方面讲解比较详细; 下面简要介绍重点地方

1.1 基础知识

1.1.1 JNI和NDK

JNI就是Java Native Interface, 即Java层和Native层通信的接口; 通过JNI, Java可以实现和其他语言之间的互相调用(注意其他语言不仅仅只限于C/C++, 虽然大多时候是C/C++); Java是跨平台的, 但是C/C++不是跨平台的, 所以JNI也使得Java需要考虑和特定平台相关的特性

NDK就是Native Development Kit, 是Android平台提供的一个工具集, 提供与本地代码之间的交互

详细可参见博客

1.1.2 CPU架构

不同的手机使用不同的CPU, 不同的CPU有不同的指令集, 每种CPU及其指令集有其自己的应用程序二进制接口(Application Binary InterfaceABI); ABI定义了机器代码与系统运行时的交互方式; ABI与指令集的对应关系如下表(摘自官方文档)

ABI与指令集对应关系

常见的CPU架构有x86, x86-64以及arm等(x86-64也是基于x86的), 其中, x86主要是针对PC端, arm主要针对移动端; Android系统目前支持ARMv5, ARMv7, ARMv8, ` x86, x86_64, MIPS以及MIPS64共七种CPU`架构

Android中应用安装时, Package Manager Service会去扫描APK, 只有该设备CPU架构支持的.so文件才会被安装, 另外还可以定义.so文件的对应安装优先级

1.1.3 交叉编译

在某个平台上编译该平台上的可执行文件叫本地编译; 如果在一个平台上编译在其他平台上的可执行程序则叫交叉编译; 交叉编译是随着嵌入式系统的发展而发展的, 因为嵌入式系统的处理能力, 内存等均有限, 所以有时候需要在其他平台上编译好后导入嵌入式系统中, 此时就需要交叉编译

交叉编译最主要的是环境, 即交叉编译链; 对本文来说, 编译FFmpeg需要准备NDK, NDK中提供了交叉编译链; 即android-ndk-r15c/toolchains/下提供的各种平台相关的编译工具链

本文使用的示例是在x86Linux下编译arm-v7a架构的动态库

1.1.4 FFmpeg

推荐官方文档

基本用法


1.2 FFmpeg交叉编译

在开始之前需要先配置一下NDK的环境变量, 在/etc/profile文件中添加PATH

FFmpeg编译生成的动态库默认格式为xx.so.版本号, 但是Android工程中只支持以.so结尾的动态库, 所以需要修改FFmpeg的配置文件, 修改其生成库文件名的格式; 编辑FFmpeg目录下的configure文件, 修改如下:

# 将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)'

为了减小APK大小, 我们只将需要的功能开启即可; 配置参数较多, 我们写一个脚本如下(名为build-script.sh)

注: 下面的注释在运行脚本时需删除~

#!/bin/bash

NDK=/home/yuanping/Software/NDK/android-ndk-r15c  # NDK所在路径, 注意替换为你的
SYSROOT=$NDK/platforms/android-19/arch-arm/
TOOLCHAIN=$NDK/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86_64 # 交叉编译链, 这里使用的是arm, 如果需要编译其他平台的, 更换即可
function build_one
{
./configure \
--prefix=$PREFIX \
--enable-shared \  # 生成动态库
--disable-static \ # 禁止生成静态库
--disable-doc \  # 关闭不需要的功能, 下同
--disable-ffplay \
--disable-ffprobe \
--disable-doc \
--disable-symver \
--disable-ffmpeg \
--enable-small \
--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 -j3
make install
}
CPU=armv7-a # CPU架构
PREFIX=$(pwd)/android/$CPU # 生成动态库所在路径
ADDI_CFLAGS="-marm"
build_one

之后运行该脚本即可, 注意添加可执行权限(sudo chmod a+x build-script.sh)

等待一会应该就好啦, 然后在FFmpeg目录下有一个android目录, 生成的动态库就在其中啦~; 笔者生成的如下:

Armv7-a


二. 动态库接入Android工程

在开始接入之前, 需要先配置Android Studio的环境; 主要是在SDK Manager中下载CMake, LLDB; 这里可以不用下载NDK, 最好使用与编译版本一致的NDK, 以免出现兼容性问题

SDKManager

配置NDK路径, 如下:

NDK路径

Android项目接入JNI有两种方法, 一种是通过cmakeCMakeLists.txt配置文件来指定; 另一种是通过ndk-buildAndroid.mk, Application.mk配置文件来指定; 官方推荐使用第一种; 关于第二种接入方式, 可以参见官方文档配置

这里主要讲解第一种方式, 参见官方文档

如果是创建的新项目的话, 可以直接在创建项目的时候选择include C++ surpport, 如下图; 如果项目已经创建了, 也不要紧, 下面讲解的就是这种情况

JNI新项目

我们先来看一下完整的目录结构如下:

目录结构

main目录下创建一个jni目录(其他目录名也可以, 但是要注意后文的更改), 在jni下创建一个ffmpeg目录, 将上面编译好的android/armv7-a/lib目录下的.so文件拷贝到Android工程的ffmpeg/armeabi-v7a下(新建armeabi-v7a目录, 注意目录名一定要是armeabi-v7a, 和CPU架构对应), 需要注意的是生成的动态链接库中有一些不带版本号的是指向另一个待版本号的软链接, 如下举例的两个so文件, 其中libavcodec.so文件是软链接, 指向libavcodec-58.so, 所以拷贝时不需要拷贝这些软链接咯

动态链接库

再将android/armv7-a目录下的include文件夹拷贝到ffmpeg下; 创建好后目录结构如下:

创建jni目录

app目录下创建CMakeLists.txt(一定要是这个名字咯), 如下:

创建CMakeLists

内容如下:

cmake_minimum_required(VERSION 3.4.1) # cmake最低版本

add_library( # Sets the name of the library.
             wlffmpeg

             # Sets the library as a shared library.
             SHARED

             # Provides a relative path to your source file(s).
             src/main/jni/ffmpeg/player.c )

add_library( avcodec-58  # 库名字
             SHARED
             IMPORTED)
set_target_properties( avcodec-58
                       PROPERTIES IMPORTED_LOCATION
                       ${CMAKE_SOURCE_DIR}/src/main/jni/ffmpeg/armeabi-v7a/libavcodec-58.so) 

add_library( avdevice-58
             SHARED
             IMPORTED)
set_target_properties( avdevice-58
                       PROPERTIES IMPORTED_LOCATION
                       ${CMAKE_SOURCE_DIR}/src/main/jni/ffmpeg/armeabi-v7a/libavdevice-58.so)

add_library( avfilter-7
             SHARED
             IMPORTED)
set_target_properties( avfilter-7
                       PROPERTIES IMPORTED_LOCATION
                       ${CMAKE_SOURCE_DIR}/src/main/jni/ffmpeg/armeabi-v7a/libavfilter-7.so)

add_library( avformat-58
             SHARED
             IMPORTED)
set_target_properties( avformat-58
                       PROPERTIES IMPORTED_LOCATION
                       ${CMAKE_SOURCE_DIR}/src/main/jni/ffmpeg/armeabi-v7a/libavformat-58.so)

add_library( avutil-56
             SHARED
             IMPORTED)
set_target_properties( avutil-56
                       PROPERTIES IMPORTED_LOCATION
                       ${CMAKE_SOURCE_DIR}/src/main/jni/ffmpeg/armeabi-v7a/libavutil-56.so)

add_library( swresample-3
             SHARED
             IMPORTED)
set_target_properties( swresample-3
                       PROPERTIES IMPORTED_LOCATION
                       ${CMAKE_SOURCE_DIR}/src/main/jni/ffmpeg/armeabi-v7a/libswresample-3.so)

add_library( swscale-5
             SHARED
             IMPORTED)
set_target_properties( swscale-5
                       PROPERTIES IMPORTED_LOCATION
                       ${CMAKE_SOURCE_DIR}/src/main/jni/ffmpeg/armeabi-v7a/libswscale-5.so)


find_library( # Sets the name of the path variable.
              log-lib  # Android内置的log模块, 用于将JNI层的log打到AS控制台

              # Specifies the name of the NDK library that
              # you want CMake to locate.
              log )

include_directories(src/main/jni/ffmpeg/include)

target_link_libraries( # Specifies the target library.  # 链接

                       wlffmpeg  
                       avcodec-58
                       avdevice-58
                       avfilter-7
                       avformat-58
                       avutil-56
                       swresample-3
                       swscale-5

                       # Links the target library to the log library
                       # included in the NDK.
                       ${log-lib} )

关于CMake配置可以参见文档

在模块级别的build.gradle下配置:

android {
    ...
    defaultConfig {
        ...
        externalNativeBuild {
            cmake {
                cppFlags "" 
            }
            ndk {
                abiFilters "armeabi-v7a" # 指定CPU架构
            }
        }
        sourceSets {
            main {
                jniLibs.srcDirs = ['src/main/jni/ffmpeg']  # 指定jni路径
            }
        }
    }
    externalNativeBuild {
        cmake {
            path 'CMakeLists.txt' # 指定cmake的配置文件路径
        }
    }
}

创建FFmpeg.java类, 使用静态代码块加载动态链接库, 定义native方法playMyMedia(), 如下:

public class FFmpeg {
    static {
        System.loadLibrary("avutil-56");
        System.loadLibrary("swresample-3");
        System.loadLibrary("avcodec-58");
        System.loadLibrary("avformat-58");
        System.loadLibrary("swscale-5");
        System.loadLibrary("avfilter-7");
        System.loadLibrary("avdevice-58");
        System.loadLibrary("wlffmpeg"); // 注意不要忘了加载这个库
    }

    public native void playMyMedia(String url);
}

playMyMedia()上快捷键Alt + Enter选择create function xxx, 可以自动创建对应的.c文件, 当然该.c文件也可以自命名, 此处命名为player.c; player.c的完整代码如下: 该代码摘自博客

#include <jni.h>
#include "libavformat/avformat.h"
#include <android/log.h>
#define LOGI(FORMAT,...) __android_log_print(ANDROID_LOG_INFO,"HusterYP",FORMAT,##__VA_ARGS__);  // 输出到AS的log中
#define LOGE(FORMAT,...) __android_log_print(ANDROID_LOG_ERROR,"HusterYP",FORMAT,##__VA_ARGS__);

JNIEXPORT void JNICALL
Java_com_gif_ping_jnidemo_FFmpeg_playMyMedia(JNIEnv *env, jobject instance, jstring url_) {
    const char *url = (*env)->GetStringUTFChars(env, url_, 0);
    LOGI("url:%s", url);
    av_register_all();
    AVCodec *c_temp = av_codec_next(NULL);
    while (c_temp != NULL)
    {
        switch (c_temp->type)
        {
            case AVMEDIA_TYPE_VIDEO:
                LOGI("[Video]:%s", c_temp->name);
                break;
            case AVMEDIA_TYPE_AUDIO:
                LOGI("[Audio]:%s", c_temp->name);
                break;
            default:
                LOGI("[Other]:%s", c_temp->name);
                break;
        }
        c_temp = c_temp->next;
    }
    (*env)->ReleaseStringUTFChars(env, url_, url);
}

然后在MainActivity中使用, 也比较简单; 注意添加网络权限, 在log中就可以看到输出的视频信息啦~

public class MainActivity extends AppCompatActivity {

    FFmpeg mFFmpeg;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        init();
    }

    private void init() {
        mFFmpeg = new FFmpeg();
        mFFmpeg.playMyMedia("http://video.xxx"); // 随便找一个视频url啦~
    }
}

到这里, 项目应该就能跑起来啦~

Android工程参见JNIDemo

FFmpeg编译参见: https://github.com/HusterYP/FFmpeg


三. 参考链接

Cross Compiling FFmpeg 4.0 for Android

Android 集成 FFmpeg (一) 基础知识及简单调用

Android Studio通过cmake创建FFmpeg项目