技术栈

主页 > 移动开发 >

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

本系列第二篇:Android 集成 FFmpeg (二) 以命令方式调用 FFmpeg

前言

网上关于 Android 集成 FFmpeg 的文章很多,但大多数都只介绍了步骤,没有说明背后的原理,若之前没有集成底层库的经验,那就会“神知无知”的走一步看一步,出错几率很大,出错了也不知道原因,然后会乱猜“这篇教程有问题“,“换个版本估计可以”,甚至“电脑有问题,重装下系统试试”。

为什么会出现这种情况,答案很简单:欲速则不达,要实现 Android 端集成 FFmpeg 功能,那就要掌握必需的基础知识,如果连 JNI、NDK都不了解,一上来就参考几秒钟搜出来的集成步骤开始集成,那只会被各种莫名其妙的异常完虐,如果运气好很快就实现了功能呢? 在我看来这是更大的损失,这些你没有掌握的知识如此接近却又悄悄溜走。

那么在 Android 端集成 FFmpeg 需要掌握哪些基础知识呢?个人认为以下内容是需要了解的:

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

以下知识点阐述是经过反复推敲的,不是随意复制而来,其中融入自己的理解,以个人易于理解的方式记录下来,希望能给你带来帮助。

1.JNI

JNI,即 Java Native Interface ,是 Java 提供用来与其他语言通信的 api ,“其他语言”意味不止局限于 C 或 C++ ,也可以调用除 C 和 C++ 之外的语言,只是大多数情况下调用 C 或 C++ ; “通信”意味着 Java 和 其他语言之间可以相互调用,不止局限于 Java 调用其他语言,其他语言也可以主动调用 Java .

Java 虚拟机实现了跨平台特性, 无法很好的实现与操作系统相关的本地操作,而 C 或 C++ 可以,同时代表着 C 或 C++ 不具备 Java 的跨平台能力,那么当我们在程序中使用 JNI 功能时,就必须关注程序的平台可移植性, JNI 标准要求本地代码至少能工作在任何Java 虚拟机环境。

简而言之,跨平台的 Java 调用了不跨平台的 C/C++,使程序丧失了跨平台性,这就是 JNI 的副作用,所以可以不使用 JNI 时就尽量避免。而大多数不可避免的情况是:已存在用 C/C++ 写的程序/库或者 Java 语言不支持程序所要实现的特性,比如 ffmpeg 是由 C 编写的,则必须要通过 JNI 实现调用。

JNI 的实现步骤很简单,如下:

  1. 编写带有 native 方法的 Java 类
  2. 生成该类扩展名为 .h 的头文件
  3. 创建该头文件的 C/C++ 文件,实现 native 方法
  4. 将该 C/C++ 文件编译成动态链接库
  5. 在Java 程序中加载该动态链接库

动态链接库是一组源代码的模块,其中包含可供应用程序调用的函数。比如 Windows 下的 .dll 文件就是一种动态链接库,也就是说 Java 程序在 Windows 中运行,所需的动态链接库就是 .dll 文件; 如果 Java 程序在 Linux 中运行,所需的动态链接库就是 .so 文件 ,这里 JNI 的副作用已初见端倪,本来无视操作系统的 Java ,因为 JNI ,就要考虑运行环境是 Windows 还是 Linux 。除此之外,还要考虑 CPU 架构,这也是 Android 中使用 JNI 主要需考虑的 so 库兼容型问题。

对于 Java 程序来说,需要的仅仅是编译后的动态链接库,不需要 C/C++ 文件和 .h 头文件。Android 亦如此,网上很多 Android NDK 教程会把需要编译的 C/C++ 源码放入 Android 工程中,形成类似这样的工程结构:

这对新手来说可能会产生误导,误以为 Android 工程需要 C/C++ 文件或 .h 头文件或者其他的文件,要清楚的是, Android 工程需要的仅仅是编译后的 .so 库,所以我们可以在工程之外编译完后,只将 .so 库移植到工程中。那为什么大多数教程会把源码先移植到 Android 工程中再去编译呢?目的只有一个:节省目录的切换以及 .so 库的复制时间,实际上这些时间微乎其微,我推荐新手将编译操作于工程外进行,更易理解。

2.CPU 架构

我们都知道 CPU 是什么,那 CPU 架构到底是什么呢?回归到“架构”这个词本身含义,CPU 架构就是 CPU 的框架结构、设计方案,处理器厂商以某种架构为基础,生产自己的 CPU,就好比“总-分-总”是文章的一种架构,多篇文章可以都基于“总-分-总”架构。

常见的 CPU 架构有 x86、x86-64 以及 arm 等, x86-64 其实也是基于 x86 架构,只是在 x86 的基础上做了一些扩展,以支持 64 位程序的应用,常见的 Intel 、AMD 处理器都是基于 x86 架构的。

而 x86 架构主打的是 pc 端,对于移动端,arm 架构处于霸主地位 ,由于其体积小、低功耗、低成本、高性能的优点,被广泛应用在嵌入式系统中,目前大多数安卓、苹果手机的 CPU 都基于 arm 架构,此处所说的 arm 架构指 arm 系列架构,其中包括 ARMv5 、ARMv7 等等。

最后再看 Android 端 , Android 系统目前支持 ARMv5、ARMv7、ARMv8、 x86 、x86_64、MIPS 以及 MIPS64 共七种 CPU 架构,也就是说除此之外其他 CPU 架构的硬件并不能运行 Android 系统。

3.交叉编译

在某个平台上,编译该平台的可执行程序,叫做本地编译,比如在 Windows 平台上编译 Windows 自身的可执行程序;在 x86 平台上,编译 x86 平台自身的可执行程序。

在某个平台上,编译另一种平台的可执行程序,就是交叉编译,比如在 x86 平台上,编译 arm 平台的可执行程序,这也是 Android 端使用最多的交叉编译类型。

在交叉编译时,由于主机与目标的体系架构、环境不同,所以交叉编译比本地编译复杂很多,需要一些工具来解决主机与目标不同特性的问题,这些工具构成的工具集就叫做交叉编译链。

既然交叉编译比本地复杂很多,那为什么不使用本地编译,比如在 arm 平台编译 arm 平台的可执行程序呢?这是因为目标平台存储空间和计算能力通常是有限的,而编译过程需要较大的存储空间和较快的计算能力,但目标平台无法提供。

4.NDK

我们需要的是 arm 平台的动态库,而这一编译过程往往是在 x86 平台上进行,所以属于交叉编译,需要交叉编译链来实现,所以 NDK(Native Development Kit )中提供了交叉编译链,方便开发。

Android 中包括七种 CPU 架构,NDK 中自然就有与之对应的交叉编译链,以下是 Android 官网对此的表格描述:

除此之外,NDK 还提供了一些原生标头和共享库文件,包括 C/C++ 支持库、从 C/C++ 代码中可以向 Android 系统输出日志的 < android/log.h > 等等,可以点击这里了解更多,总之,NDK 是用来帮助我们实现交叉编译的工具。

在实际使用时,比较重要的是 Android.mk 语法,内容并不多,但你必须了解,不然只复制别人的配置很容易出错,关键是你无法真正的掌握这部分知识,而最好的学习方法就是仔细阅读几遍 Android.mk 官网教程

另外还需要了解什么是 ABI ,ABI 即 application binary interface ,应用程序二进制接口,顾名思义,“二进制接口”说明这是程序与系统之间的底层接口,它定义了程序如何与系统交互。我们应该指定每个 CPU 架构所对应的 ABI,所以 Android 中就出现了 armeabi 、armeabi-v7a、arm64-v8a、x86、x86_64、mips 以及 mips64 目录来区分不同的 ABI ,我们将编译好的动态库放入对应 CPU 架构的 ABI 目录中就可以了。

掌握了以上知识点,才能知道 Android 集成 FFmpeg本质上是在做什么,为什么要这样做。不只是集成 FFmpeg,这些知识对于任何底层库的集成都是通用、必要的。

5.FFmpeg 简介

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

FFmpeg 被很多开源项目和软件使用,比如暴风影音、QQ影音、格式工厂等,另外在我常用的 App 中也发现了它的身影:

其实写到这里,有点犹豫非操作步骤的内容是否叙述过多了,后来想想,这正是这篇文章的初衷,尽量全面清楚,透过表面操作看本质,另外最重要的一个目标,就是争取做到授人以渔,我相信这也是大家对所有技术教程文章的一个美好愿景。

下面分为 3 个部分介绍 FFmpeg的编译及 Android 端的简单调用:

  1. 准备
  2. 编译 FFmpeg
  3. 编译动态库及调用

1.准备

  • Linux 环境(Ubuntu 16.04):从个人经验而谈,准备 Linux 环境比 Windows 环境编译的工作量小。
  • 下载NDK(android-ndk-r14b) :网上有些教程美言曰“NDK向后兼容”,其实查阅 NDK 版本更新说明就能解决。
  • 下载 FFmpeg (ffmpeg-3.3.3): 官网下载链接:https://ffmpeg.org/download.html

2.编译 FFmpeg

编译环境为 x86 的 Linux ,运行环境为 arm 架构的 Android 系统,目标是把 FFmpeg 源码编译成 Android 端可调用的动态库,这属于交叉编译,所以需要 NDK 提供的交叉编译工具,这是这一步骤的本质意义。

Android 工程中只支持导入 .so 结尾的动态库,形如:libavcodec-57.so 。但是FFmpeg 编译生成的动态库默认格式为 xx.so.版本号 ,形如:libavcodec.so.57 , 所以需要修改 FFmpeg 根目录下的 configure 文件,使其生成以 .so 结尾格式的动态库:

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

在编译 FFmpeg 之前,我们需要修改 FFmpeg 的编译选项,主要目的如下:

  • 规定编译方式,使其通过交叉编译生成我们需要的动态库。
  • 选择所需功能,针对需求定制 FFmpeg 功能,精简动态库。

比如我们需要对 mp3 文件进行剪切、合并等操作,则应开启 mp3 格式编码与解码功能( FFmpeg 本身不支持 mp3 格式编码,需要引入 libmp3lame 库)。

怎么修改 FFmpeg 的编译选项呢?在 FFmpeg 根目录下通过 ./configure 命令进行设置,但是为了方便记录与修改,我们选择在根目录下建立一个脚本文件来运行 ./configure 命令。

针对所需功能,脚本文件如下 :

#!/bin/bash  
NDK=/home/yhao/Android/android-ndk-r14b
SYSROOT=$NDK/platforms/android-14/arch-arm/
TOOLCHAIN=$NDK/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86_64

CPU=arm
PREFIX=$(pwd)/Android/$CPU
MP3LAME=/home/yhao/sf/lame-3.99.5/android

./configure 
    --prefix=$PREFIX           #规定编译文件在哪里生成
    --enable-cross-compile     #启用交叉编译方式
    --cross-prefix=$TOOLCHAIN/bin/arm-linux-androideabi-   #交叉编译链
    --target-os=linux          #目标系统
    --arch=arm                 #目标平台架构
    --sysroot=$SYSROOT         #交叉编译环境
    --extra-cflags="-I${MP3LAME}/include"                  #额外需要的头文件
    --extra-ldflags="-L${MP3LAME}/lib"                     #额外需要的库                 
    --enable-shared            #生成动态库(共享库)
    --disable-static           #禁止生成静态库
    --disable-doc              #禁用不需要的功能,下同
    --disable-ffserver 
    --disable-parsers 
    --disable-protocols 
    --disable-indevs 
    --disable-bsfs 
    --disable-muxers 
    --disable-demuxers 
    --disable-hwaccels 
    --disable-decoders 
    --disable-encoders 
    --enable-parser=mpegaudio  #启用需要的功能,下同
    --enable-protocol=http 
    --enable-protocol=file 
    --enable-libmp3lame 
    --enable-encoder=libmp3lame 
    --enable-encoder=png 
    --enable-demuxer=mp3 
    --enable-muxer=mp3 
    --enable-decoder=mjpeg 
    --enable-decoder=mp3

除强迫症风格的注释之外,再对个别配置进行说明:

  • --sysroot=$SYSROOT : 前言中提到 NDK 除了提供 交叉编译链 以外,还提供一些原生标头和共享库文件,通过此配置指定了交叉编译环境,使其在编译过程中能够引用到 NDK 提供的原生标头和共享库文件,其中 android-14 目录指定了生成动态库最低支持的 Android 版本,嗯~ ,这里可以说它是向后兼容的。

  • --target-os=linux :Android 内核为 Linux ,故在此指定为 linux ,如果要编译的目标系统为 ios ,则指定为 darwin 。

  • --cross-prefix=$TOOLCHAIN/bin/arm-linux-androideabi- : 类似于通配符方式指定 bin 目录下以 arm-linux-androideabi-
    开头的交叉编译工具,假如不支持这种配置方式则需分别指定,比如在交叉编译 libmp3lame 时就是分别指定的:

这里写图片描述

这里写图片描述

  • --extra-cflags 和 --extra-ldflags : 由于开启 mp3 编码需要引入 libmp3lame 库,所以需要指定编译好的 libmp3lame 头文件和库文件的路径,这样在编译时才能正确引用到 libmp3lame 。这里是我编译好的 libmp3lame 库,下载后指定对应路径即可 。

  • FFmpeg 功能的开启和禁用 : 在 FFmpeg 源码根目录下通过 ./configure --help 命令查看所有配置选项,针对需求配置,这里针对 mp3 文件操作的功能进行配置,禁用了很多不需要的功能,大幅精简动态库,从而减小 APK 大小。

接下来运行该脚本文件使配置生效,比如我的脚本文件名为 config.sh :

sh config.sh

运行成功后会输出生效的配置,可以看到此时支持的编解码:

这里写图片描述

这里写图片描述

ok ,现在已经配置完编译选项了,接下来就可以开始编译了啦~

sudo make -j4

编译完成不要忘记安装:

sudo make install

然后就可以看到成功生成:

这里写图片描述

这里写图片描述

3.编译动态库及调用

你可能会疑问,上一步已经编译 FFmpeg 源码生成动态库了,为什么这一步还是“编译动态库”呢?其实这个问题等效于:上一步中生成的动态库可以直接在 Android 工程中使用吗?

答案是否定的,回到文章开头 JNI 的使用步骤,在编写带有 native 方法的 Java 类后,紧接着就是用 C/C++ 实现本地接口,这是 Java 与 C/C++ 交互的必要通道。

所以接下来需要编写本地接口,在本地接口中调用上一步编译好的 FFmpeg 动态库,然后将本地接口也编译成动态库,供 Android 调用,这一步还需要“编译动态库”。

网上有些教程在这一步把 FFmpeg 源码、动态库全部复制到 Android 工程中,然后在工程中新建本地接口、mk文件...... 花里胡哨的看的我头皮发麻~ 这一步我推荐在 Android 工程之外进行,直到生成最终可用的动态库之后,再拷贝到 Android 工程中直接使用。

首先新建一个文件夹,取名随意,比如 “ndkBuild ”,这个目录就作为我们的工作空间,然后在 ndkBuild 下新建 jni 文件夹, 作为编译工作目录。 OK~ 接下来按照前言中的 jni 步骤来划分操作:

1. 编写带有 native 方法的 Java 类

package com.jni;

public class FFmpeg {
    
    public static native void run();

}

2. 生成该类扩展名为 .h 的头文件

在 Android Studio 的 Terminal 中 切换到 java 目录下,运行 javah 命令生成头文件:

javah -classpath .  com.jni.FFmpeg

网上很多教程需要先生成 .class 文件,而我在 java 1.8 环境下亲测以上一句命令即可。

3. 创建该头文件的 C/C++ 文件,实现 native 方法

将生成的 com_jni_FFmpeg.h 文件剪切到 ndkBuild 的 jni 目录下,创建对应的 C 文件 com_jni_FFmpeg.c :

#include <android/log.h>
#include "com_jni_FFmpeg.h"

#include <stdlib.h>
#include <stdbool.h>
#include <stdio.h>
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libavfilter/avfilter.h"

JNIEXPORT void JNICALL Java_com_jni_FFmpeg_run(JNIEnv *env, jclass obj) {
  
    char info[40000] = {0};
    av_register_all();
    AVCodec *c_temp = av_codec_next(NULL);
    while(c_temp != NULL){
       if(c_temp->decode!=NULL){
          sprintf(info,"%s[Dec]",info);
       }else{
          sprintf(info,"%s[Enc]",info);
       }
       switch(c_temp->type){
        case AVMEDIA_TYPE_VIDEO:
          sprintf(info,"%s[Video]",info);
          break;
        case AVMEDIA_TYPE_AUDIO:
          sprintf(info,"%s[Audio]",info);
          break;
        default:
          sprintf(info,"%s[Other]",info);
          break;
       }
       sprintf(info,"%s[%10s]
",info,c_temp->name);
       c_temp=c_temp->next;
    }
__android_log_print(ANDROID_LOG_INFO,"myTag","info:
%s",info);
}

这段程序用于输出 FFmpeg 支持的编解码信息,通过 < android/log.h > 的 __android_log_print 方法可以直接将信息输出到 Android Studio 的 logcat 。

4. 将该 C/C++ 文件编译成动态链接库

编译 FFmpeg 源码时的实际入口是通过 FFmpeg 提供的 makefile ,而在这一步,将直接使用 NDK 提供的编译方法,需要提供 Application.mk 和 Android.mk 文件。

首先在 jni 目录下创建 Application.mk 文件 :

APP_ABI := armeabi
APP_PLATFORM=android-14

APP_ABI 表示编译生成 armeabi 架构的 so 库,APP_PLATFORM 表示最低支持的 Android 版本。

然后在 jni 目录下创建 Android.mk 文件:

LOCAL_PATH:= $(call my-dir)

INCLUDE_PATH:=/home/yhao/sf/ffmpeg-3.3.3/Android/arm/include
FFMPEG_LIB_PATH:=/home/yhao/sf/ffmpeg-3.3.3/Android/arm/lib

include $(CLEAR_VARS)
LOCAL_MODULE:= libavcodec
LOCAL_SRC_FILES:= $(FFMPEG_LIB_PATH)/libavcodec-57.so
LOCAL_EXPORT_C_INCLUDES := $(INCLUDE_PATH)
include $(PREBUILT_SHARED_LIBRARY)
 
include $(CLEAR_VARS)
LOCAL_MODULE:= libavformat
LOCAL_SRC_FILES:= $(FFMPEG_LIB_PATH)/libavformat-57.so
LOCAL_EXPORT_C_INCLUDES := $(INCLUDE_PATH)
include $(PREBUILT_SHARED_LIBRARY)
 
include $(CLEAR_VARS)
LOCAL_MODULE:= libswscale
LOCAL_SRC_FILES:= $(FFMPEG_LIB_PATH)/libswscale-4.so
LOCAL_EXPORT_C_INCLUDES := $(INCLUDE_PATH)
include $(PREBUILT_SHARED_LIBRARY)
 
include $(CLEAR_VARS)
LOCAL_MODULE:= libavutil
LOCAL_SRC_FILES:= $(FFMPEG_LIB_PATH)/libavutil-55.so
LOCAL_EXPORT_C_INCLUDES := $(INCLUDE_PATH)
include $(PREBUILT_SHARED_LIBRARY)
 
include $(CLEAR_VARS)
LOCAL_MODULE:= libavfilter
LOCAL_SRC_FILES:= $(FFMPEG_LIB_PATH)/libavfilter-6.so
LOCAL_EXPORT_C_INCLUDES := $(INCLUDE_PATH)
include $(PREBUILT_SHARED_LIBRARY)
 
include $(CLEAR_VARS)
LOCAL_MODULE:= libswresample
LOCAL_SRC_FILES:= $(FFMPEG_LIB_PATH)/libswresample-2.so
LOCAL_EXPORT_C_INCLUDES := $(INCLUDE_PATH)
include $(PREBUILT_SHARED_LIBRARY)

include $(CLEAR_VARS)
LOCAL_MODULE:= libpostproc
LOCAL_SRC_FILES:= $(FFMPEG_LIB_PATH)/libpostproc-54.so
LOCAL_EXPORT_C_INCLUDES := $(INCLUDE_PATH)
include $(PREBUILT_SHARED_LIBRARY)

include $(CLEAR_VARS)
LOCAL_MODULE:= libavdevice
LOCAL_SRC_FILES:= $(FFMPEG_LIB_PATH)/libavdevice-57.so
LOCAL_EXPORT_C_INCLUDES := $(INCLUDE_PATH)
include $(PREBUILT_SHARED_LIBRARY)

include $(CLEAR_VARS)
LOCAL_MODULE := ffmpeg
LOCAL_SRC_FILES := com_jni_FFmpeg.c 
LOCAL_C_INCLUDES := /home/yhao/sf/ffmpeg-3.3.3
LOCAL_LDLIBS := -lm -llog
LOCAL_SHARED_LIBRARIES := libavcodec libavfilter libavformat libavutil libswresample libswscale libavdevice
include $(BUILD_SHARED_LIBRARY)

对于 Android.mk 文件前面提到过,最好的学习方法就是仔细阅读几遍 Android.mk 官网教程 。 简要说明一下,此文件将之前编译好的 FFmepg 动态库通过 NDK 提供的预编译方式编译,通过 prebuilt 这个单词也能猜到它的含义,点击了解 NDK 预编译, 最后将 com_jni_FFmpeg.c 编译成名为 ffmpeg 的动态库。

此时 jni 目录下应有以下四个文件:

请无视 Android.mk 文件上的小锁儿~ 。然后在 jni 目录下运行 (注意要把 ndk 添加到环境变量) :

ndk-build

大功告成 ,此时在 ndkBuild 目录下生成了 libs 和 obj 目录,而 Android 需要的最终的动态库就在 libs 目录下:

5. 在Java 程序中加载该动态链接库

将 libs 目录下的 armeabi 文件夹整体拷贝到 Android Studio 工程的 libs 文件夹下,当然如果你的工程已经存在 armeabi 目录,就把该目录下的动态库拷贝到工程的 armeabi 目录下。

在 FFmpeg 中加载动态库:

package com.jni;


public class FFmpeg {

    static {
        System.loadLibrary("avutil-55");
        System.loadLibrary("avcodec-57");
        System.loadLibrary("avformat-57");
        System.loadLibrary("avdevice-57");
        System.loadLibrary("swresample-2");
        System.loadLibrary("swscale-4");
        System.loadLibrary("postproc-54");
        System.loadLibrary("avfilter-6");
        System.loadLibrary("ffmpeg");
    }

    public static native void run();
}

记得在应用的 build.gradle 文件中 android 节点下添加动态库加载路径:

    sourceSets {
        main {
            jniLibs.srcDirs = ['libs']
        }
    }

然后在程序中调用 FFmpeg.run() 方法 ,可以看到 logcat 输出了 FFmpeg 的编解码信息:

总结

这篇文章较多篇幅介绍了 JNI 相关的基础知识,对 JNI 经验缺乏的人应该会有较多帮助,尤其是交叉编译和 CPU 架构部分,如果一个月前的我看到估计都感激涕零了,当初一脸茫然的按照网上的教程集成,殊不知不了解这些知识是真的愣头青。

此文中仅仅实现了 Android 端获取 FFmpeg 编解码信息,而要实际使用的话,就要掌握 FFmpeg 提供的函数或者通过命令方式调用,后者难度较小,另外还有 libmp3lame 库的编译,下篇文章一起总结。

责任编辑:admin     二维码分享:
本文标签: 架构JavaincludeFFmpegAndroid编译