【Android 音视频开发打怪升级:FFmpeg音视频编解码篇】一、FFmpeg so库编译

11,758 阅读13分钟

【声 明】

首先,这一系列文章均基于自己的理解和实践,可能有不对的地方,欢迎大家指正。
其次,这是一个入门系列,涉及的知识也仅限于够用,深入的知识网上也有许许多多的博文供大家学习了。
最后,写文章过程中,会借鉴参考其他人分享的文章,会在文章最后列出,感谢这些作者的分享。

码字不易,转载请注明出处!

教程代码:【Github传送门

目录

一、Android音视频硬解码篇:
二、使用OpenGL渲染视频画面篇
三、Android FFmpeg音视频解码篇

本文你可以了解到

使用 GCCCLANG 交叉编译出Android平台可以使用的FFmpeg so库。为了很好的迈出 FFmpeg 开发的第一步,不仅要知其然,更要知其所以然。不仅要知道怎么样能成功编译,更要知道为什么能成功编译。在开始动手之前,建议先通读整篇文章,相信本文定可以让你有所感悟。

一、前言

网上其实已经有很多的关于FFmpeg so库编译的分享,但是大部分都是直接把配置文件的内容贴出来。我想大部分去搜索 「如何编译FFmpeg so库」的人,对交叉编译这个东东都是比较陌生的。

特别对于移动端开发者来说,大部分人大多数时候都是在Java层做开发,很少接触到NDK层的东西。如果直接去看一份交叉编译的配置,估计会很上头。

通常情况下,在一篇FFmpeg编译的文章下面都会有很多的类似「为什么按照楼主的配置还是无法编译成功?」的评论,那为什么人家可以编译成功,我们copy下来却不可以呢?

原因有非常多,大部分其实集中在以下几个方面:

1. 无脑copy,祈求有一个傻瓜式的配置可以成功编译;
2. FFmpeg版本和NDK版本很多,每一个版本都可能需要不一样的配置;
3. 不了解每个配置项的意义,即使好运配置对了, 但是稍微一修改,又无法正常编译了。

为什么FFmpeg让人觉得很难搞?

我想主要是因为迈出第一步就很困难,连so库都编译不出来,后面的都是扯淡了。

二、什么是交叉编译

定义

引自百度百科的定义:交叉编译,是在一个平台上生成另一个平台上的可执行代码。

什么意思呢?说白了,就是在一个机器上生成一个程序,这个程序可以跑在另外一个机器上。举栗:在PC上编译一个apk,这个apk可以跑在Android手机上,这其实就是一个交叉编译的过程。

为什么要交叉编译

我们知道,PC上的软件是直接在PC上编译生成的,那为什么Android上的软件不能在Android上自己编译生成呢?

理论上是可以,但是Android手机上的资源有限啊,在PC上编译一个apk都要那么久,你可以想象在Android手机上编译一个apk要多久吗?或者你能想象在手机上敲代码的情景吗?

那我们会想既然PC上资源那么丰富,那可不可以利用PC来编译出在手机上可以运行的软件呢?

于是,交叉编译出现。

交叉编译需要的什么

编译环境

我们知道PC上的环境和手机上的运行环境是绝然不同的,如果使用PC上的环境直接编译的话,可以想象这个编译出来的App,分分钟就会挂掉。

所以,交叉编译最重要的是,要配置好编译过程中使用到的相关的环境,而这个环境其实就是目标机器(比如Android手机)正在运行的环境。

编译工具链

对于C/C++的编译,通常有两个工具 GCCCLANG

GCC 可能大家都有听说过,这是一个老牌的编译工具,不仅可以编译C/C++,也可以编译Java,Object-C,Go等语言。

CLANG 则是一个效率更高的C/C++编译工具,并且兼容GCC,Google在很早以前就开始建议使用clang进行编译,并且在 ndk 17 以后,把 GCC 移除了,全面推行使用 CLANG

三、如何交叉编译FFmpeg

FFmpeg是什么

鼎鼎大名的FFmpeg,不说在音视频界如雷贯耳,就算一个不开发音视频的开发者也都是略有耳闻。

官方简介

A complete, cross-platform solution to record, convert and stream audio and video.

翻译过来就是:FFmpeg是一套集录制、转换以及流化音视频的完整的跨平台解决方案。

从这段简介可以看到FFmpeg有以下特点:

  1. 功能强大:录制、解码、编码、编辑、推流等等
  2. 跨平台

编译流程

从前面的介绍,基本上可以总结出FFmepg编译的基本流程:

  1. 选择编译工具
  2. 配置交叉编译环境
  3. 配置编译参数(比如去掉一些不需要的功能)
  4. 启动编译

流程就是这么简单,接下来就来详细看看,如何通过 CLANGGCC 两种方式来编译。

四、使用CLANG编译FFmpeg

注:本文编译平台为Mac,建议使用Mac或者Linux进行编译,据说Windows有很多坑。

下载Android NDK

Android 的 NDK 已经迭代了很多版本,在 r17c 以后,Google正式移除 GCC ,不再支持 GCC ,新版本的 NDK 都是使用 CLANG 进行编译。

这里就使用目前最新的 NDK r20b 版本来编译。

NDK 下载地址:Android-NDK

NDK 目录

NDK r20b 目录

最主要的就是这两个路径:

编译工具链目录:
toolchains/llvm/prebuilt/darwin-x86_64/bin

交叉编译环境目录:
toolchains/llvm/prebuilt/darwin-x86_64/sysroot
  • 编译工具路径

编译工具

根据不同的CPU架构区和不同的Android版本,区分了不同的clang工具,根据自己需要选择就好了。

本文选择 CPU 架构 armv7a,Android版本 21:

armv7a-linux-androideabi21-clang
armv7a-linux-androideabi21-clang++
  • 编译环境路径

toolchains/llvm/prebuilt/darwin-x86_64/sysroot 目录下,包含了两个目录: usr/includeusr/lib,分别对应了 头文件库文件

库文件和头文件

下载FFmpeg源码

FFmpeg官网下载,直接DownLoad即可。

本文使用的是目前最新的版本 ffmpeg-4.2.2

下载好源码后,进入根目录,找到一个名为 congfigure 的文件,这是一个shell脚本,用于生成一些 FFmpeg 编译需要的配置文件。

这个文件非常重要,FFmpeg 的编译配置就是靠它完成的。 后面我们将对其中一些重要的内容进行分析,这是理解 FFmpeg 编译配置的关键。

有了以上基础以后,就可以对FFmpeg进行编译了。

配置脚本

  • 修改 configure 脚本
  1. 新增 cross_prefix_clang 参数

打开(注:不是双击运行) ffmpeg-4.2.2 根目录下的 configure 文件,搜索 CMDLINE_SET ,可以找到以下代码,然后新增一个命令行选项:cross_prefix_clang

CMDLINE_SET="
    $PATHS_LIST
    ar
    arch
    as
    assert_level
    build_suffix
    cc
    objcc
    cpu
    cross_prefix
    # 新增命令行参数
    cross_prefix_clang
    custom_allocator
    cxx
    dep_cc
    # 省略其他.....
"
  1. 修改编译工具路径设置

搜索 ar_default="${cross_prefix}${ar_default}" , 找到以下代码

ar_default="${cross_prefix}${ar_default}"
cc_default="${cross_prefix}${cc_default}"
cxx_default="${cross_prefix}${cxx_default}"
nm_default="${cross_prefix}${nm_default}"
pkg_config_default="${cross_prefix}${pkg_config_default}"

将中间两行修改为

ar_default="${cross_prefix}${ar_default}"
#------------------------------------------------
cc_default="${cross_prefix_clang}${cc_default}"
cxx_default="${cross_prefix_clang}${cxx_default}"
#------------------------------------------------
nm_default="${cross_prefix}${nm_default}"
pkg_config_default="${cross_prefix}${pkg_config_default}"

至于为什么这么修改,将在后面的 configure 分析中详细讲解

  • 新建编译配置脚本

ffmpeg-4.2.2 根目录下新建 shell 脚本,命名为: build_android_clang.sh

#!/bin/bash
set -x
# 目标Android版本
API=21
CPU=armv7-a
#so库输出目录
OUTPUT=/Users/cxp/Desktop/FFmpeg/ffmpeg-4.2.2/android/$CPU
# NDK的路径,根据自己的NDK位置进行设置
NDK=/Users/cxp/Desktop/FFmpeg/android-ndk-r20b
# 编译工具链路径
TOOLCHAIN=$NDK/toolchains/llvm/prebuilt/darwin-x86_64
# 编译环境
SYSROOT=$TOOLCHAIN/sysroot

function build
{
  ./configure \
  --prefix=$OUTPUT \
  --target-os=android \
  --arch=arm \
  --cpu=armv7-a \
  --enable-asm \
  --enable-neon \
  --enable-cross-compile \
  --enable-shared \
  --disable-static \
  --disable-doc \
  --disable-ffplay \
  --disable-ffprobe \
  --disable-symver \
  --disable-ffmpeg \
  --sysroot=$SYSROOT \
  --cross-prefix=$TOOLCHAIN/bin/arm-linux-androideabi- \
  --cross-prefix-clang=$TOOLCHAIN/bin/armv7a-linux-androideabi$API- \
  --extra-cflags="-fPIC"

  make clean all
  # 这里是定义用几个CPU编译
  make -j12
  make install
}

build

这个shell脚本,大体上其实还是很容易懂的,比如

--disabble-static --enable-shared 分别用于禁止输出静态库,以及输出动态库;

--arch --cpu 用于配置输出的so库是什么架构的;

--prefix 用于配置输出的so库的存放路径。

接下来重点来讲一下几个选项:

  • target-os

--target-os=android:在旧版本的 FFmpeg 中,对Android平台的支持并不是很完善,并没有 android 这个target,所以在一些比较老的文章中都会提到,编译Android平台的so库,需要对 configure 做以下修改,否则会按照 linux 标准的方式输出so库,其命名方式和Android的so不一样,Android是无法加载的。

SLIBNAME_WITH_VERSION='$(SLIBNAME).$(LIBVERSION)'
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_VERSION='$(SLIBNAME).$(LIBVERSION)'
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加入了 android 这个 target所以我们再也不需要手动去修改了

  • sysroot

--sysroot=$SYSROOT: 用于配置交叉编译环境的 根路径 ,编译的时候会默认从这个路径下去寻找 usr/include usr/lib 这两个路径,进而找到相关的头文件和库文件。

r20b 版本的 NDK 系统的头文件和库文件就是在 $SYSYROOT/usr/include$SYSYROOT/usr/lib 中。

基本上很多新手在编译的时候都会出现找不到各种头文件,导致编译失败。所以当编译出现找不到头文件的时候,首先要检查的就是这个路径。

一点疑问

在使用最新的 ndk r20b 版本进行编译的时候发现,即使不配置 sysroot 也可以正常编译,怀疑 Android 的 clang 工具是否经过了处理,会自动去寻找对应的路径。 目前没有从 configure 文件中找到原因。
如有知情者的,还望告知呀~。

说到 sysroot 就不得不提到另外一个参数 -isysyroot ,这个参数也让我困惑了很久,因为很少文章会提到这个两个参数的联系和区别,然而这个参数也很导致让人很莫名奇妙的编译失败。

  • extra-cflags

介绍 -isysroot 之前,先看看这个 extra-cflags 选项。

这个选项的作用是,给编译器指定除了 sysroot 之外的头文件搜索路径。比如:

--extra-cflags="-I$SYSROOT/usr/include"

# 其中 -I 用于区分不同的路径

-isysroot 是这个选项的一个配置。比如

--extra-cflags="-isysroot $SYSROOT"

-isysroot 的作用就是,把后面的路径设置为默认的头文件搜索路径,这时候,前面 sysroot 配置路径就不再作为 头文件 默认的搜索路径了,不过依然是 库文件 默认的搜索路径。

可以看到,这两个配置从某种程度上说是一样的:

--extra-cflags="-I$SYSROOT/usr/include"

约等于

--extra-cflags="-isysroot $SYSROOT"

  • extra-ldflags

这个和上面的 extra-cflags 作用是类似的,不过是用于配置额外的 库文件 搜索路径,如

--extra-ldflags="-L$SYSROOT/usr/lib"
# 其中 -L 用于区分不同的路径

可以看到 extra-cflags extra-ldflags 结合起来可以替代 sysroot

  • cross-prefix

这个选项直译为 交叉编译前缀,指的是交叉编译工具的前缀。

这个选项经常和另外一个选项 cc 一起出现搭配使用。

这是什么意思呢?网上有的文章对于 cc 这个选项经常出现两种配置方式:

一种是只配置 cross-prefix ,没有配置 cc ,比如本文。

另一种是既配置 cross-prefix ,又配置 cc

比如:

--cc=$TOOLCHAIN/bin/arm-linux-androideabi-gcc \
--cross-prefix=$TOOLCHAIN/bin/arm-linux-androideabi- \

这是两种完全不同的配置方式,但是很神奇的是有时候他们都能成功编译,有时候又会出现找不到编译链工具的错误。

为了搞明白 cross-prefix cc 这两个选项的配置到底有什么影响,到底应该怎么使用这两个配置,我特地仔细的去看了 FFmpeg 根目录下的 configure 配置脚本,找到了一些蛛丝马迹。

分析 configure 配置脚本

注:以下分析基于ffmpeg-4.2.2版本,其他版本可能有所不同,掌握基本原理即可。

  • 获取用户配置选项

打开(注:不是双击运行)configure shell脚本,首先来看看 configure 是如何获取用户配置的编译选项的。

搜索 for opt do,可以找到以下代码

for opt do
    optval="${opt#*=}"
    case "$opt" in
        --extra-ldflags=*)
            add_ldflags $optval
        ;;
        --extra-ldexeflags=*)
            add_ldexeflags $optval
        ;;
        --extra-ldsoflags=*)
            add_ldsoflags $optval
        ;;
        --extra-ldlibflags=*)
            warn "The --extra-ldlibflags option is only provided for compatibility and will be\n"\
                 "removed in the future. Use --extra-ldsoflags instead."
            add_ldsoflags $optval
        ;;
        --extra-libs=*)
            add_extralibs $optval
        ;;
        --disable-devices)
            disable $INDEV_LIST $OUTDEV_LIST
        ;;
        --enable-debug=*)
            debuglevel="$optval"
        ;;
        
        # 省略中间一些代码...
        
        *)
            optname="${opt%%=*}"
            optname="${optname#--}"
            optname=$(echo "$optname" | sed 's/-/_/g')
            if is_in $optname $CMDLINE_SET; then
                eval $optname='$optval'
            elif is_in $optname $CMDLINE_APPEND; then
                append $optname "$optval"
            else
                die_unknown $opt
            fi
        ;;
    esac
done

这个shell脚本的代码有很多特有的语法,也不用钻牛角尖,能大概看明白就可以了。

for循环的首行 通过分割 = 获取到用户设置的选项值 optval

下面除了一些特殊的选项,我们看看最后的通配符 *) ,这段代码的目的,其实就是把用户配置的选项和值关联起来。

比如 --cpu=armv7-a ,前面三行就是把 cpu 分割出来,赋值给 optname,再把 optval 赋值给 cpu,说白了就是初始化了 cpu 这个变量为 armv7-a

  • Android相关的配置

搜索 android 关键字,可以找到以下代码

# ffmpeg-4.2.2/configure

if test "$target_os" = android; then
    cc_default="clang"
fi

ar_default="${cross_prefix}${ar_default}"
cc_default="${cross_prefix}${cc_default}"
cxx_default="${cross_prefix}${cxx_default}"
nm_default="${cross_prefix}${nm_default}"
pkg_config_default="${cross_prefix}${pkg_config_default}"

当你配置了 --target-os=android 的时候,FFmpeg默认的编译工具为 clang

cc_default 其实就是配置项 cc 的默认值,可以看到 cc_default 在这里和 cross_prefix 做了拼接。这里就是为什么说 cross_prefix 是交叉编译工具前缀。

拼接完是这样的:

cc_defalut=$TOOLCHAIN/bin/arm-linux-androideabi-$cc

看下 ar_default cc_default cxx_default这些默认值是什么。

搜索 cc_default 可以找到以下代码

# ffmpeg-4.2.2/configure

ar_default="ar"
cc_default="gcc"
cxx_default="g++"
host_cc_default="gcc"

可以看到,FFmpeg 默认的编译工具是 GCC

当你编译 Android 平台的库时,由于 configure 强制设置 cc_default="clang",所以:

  1. 当你使用 GCC 作为编译工具时,必须配置 cc 选项,或修改 configure 中的 cc_default="clang"cc_default="gcc" ;

  2. 当你使用 CLANG 作为编译工具时,可以不配置 cc 选项。

仔细想想会发现,为什么当 cc 配置为下边的值时,也可以正常编译呢?

--cc=$TOOLCHAIN/bin/arm-linux-androideabi-gcc

这时 cc_defalut 不就等于

cc_defalut=$TOOLCHAIN/bin/arm-linux-androideabi-$TOOLCHAIN/bin/arm-linux-androideabi-gcc

这个路径肯定是错的啊!

这就要来看到底 cc_default 是怎么使用的了。

  • 初始化变量

搜索 set_default arch ,可以看到以下代码,在这里 configure 重新设置了 cc 的默认值。

set_default arch cc cxx doxygen pkg_config ranlib strip sysinclude \
    target_exec x86asmexe nvcc

这里调用了一个叫 set_default 的函数,来看看这个函数的实现

set_default(){
    for opt; do
        eval : \${$opt:=\?{opt}_default}
    done
}

这也是一个看不太懂的shell语法,大概的意思就是:for循环获取所有的输入参数变量,然后给这个变量赋值。

比如 set_default cc ,意思就是 cc=cc_default ,不过有一点要注意的是中间这个符号 :=

这个符号类似Java中的三目运算符:

opt != null? opt:opt_defalut

也就是说,如果参数为空,将 xx_default 赋值给 xx

这就可以解释上面的疑问了。

  1. 当配置
--cc=$TOOLCHAIN/bin/arm-linux-androideabi-gcc

set_default cc 等于没有用了。因为经过 for 循环获取了用户的配置以后, cc 不为空。 set_default 后,cc 的值是不会改变的。

  1. cc 不配置的时候,FFmpeg 根据默认的拼接方式,把拼接好的路径设置给 cc

  2. 但是,不能配置 cc=gcc 这种,这样,最后 cc 的值就只有 gcc ,肯定是不能正确找到编译工具的。

  • 为什么要加入 corss-prefix-clang 这个选项

现在可以来解释为什么前面需要修改 configure 配置脚本了。

原始的配置是这样的

ar_default="${cross_prefix}${ar_default}"
cc_default="${cross_prefix}${cc_default}"
cxx_default="${cross_prefix}${cxx_default}"
nm_default="${cross_prefix}${nm_default}"
pkg_config_default="${cross_prefix}${pkg_config_default}"

也就是说,默认的 cc ar nm 路径前缀是一样的,但是 Android NDK 的路径却是这样的

NDK clang路径

看到了不?ar/nmcc的前缀是不一样的,前者是 arm-linux-androideabi- , 后者是 armv7a-linux-androideabi16-

因此,需要对 cccxx 两个前缀进行修改,为此新加了 cross_prefix_clange 来进行单独配置。

这里只是针对 NDK r20b 的情况,不同的 NDK 版本可能有所不同,根据这个原理去设置即可。

综上,解释了一些编译 FFmpeg 常用的配置选项,并且从原理上弄明白为何要这样配置,基本上搞清楚了这些,想要组合两个不同版本的FFmpeg和NDK来编译,都会比较容易实现。

启动编译

打开cmd终端,cd 到 FFmpeg 所在目录

输入 ./build_android_clang.sh

等待编译完成,将会在 ffmpeg/android/armv7-a目录下得到 includelib 两个目录,分别是 头文件so库文件

生成的so

生成的头文件

五、使用 GCC 编译FFmpeg

目前大部分网上的文章都是使用 GCC 来编译 FFmpeg 的,下面就来看看如何配置 GCC 的编译参数。

下载 Android NDK r17b

前面就说过,NDK r17c 以后,Googole 就移除了 GCC,所以要使用 GCC 只能下载 r17c 及以前的版本,本文使用 r17c 来编译。

根据自己编译平台选择对应的版本:NDK r17c

本文选择的是 Mac 版本:Mac OS X。

NDK 相关的环境路径

NDK r17c 目录

NDK r20b 相比,NDK r17c的目录稍微有些变化。

  • 交叉编译环境路径
# 库文件路径
android-ndk-r17c/platforms/android-21/arch-arm/usr/lib
# 头文件路径
android-ndk-r17c/sysroot/usr/include
  • GCC 工具链路径
android-ndk-r17c/toolchains/arm-linux-androideabi-4.9/prebuilt/darwin-x86_64/bin

可以看到,Google 将 头文件库文件 分离了,这也是很多新手在编译的时候一直没有配对路径,导致编译失败的原因。

新建编译配置脚本

FFmpeg 的版本依然是使用上面的 ffmpeg-4.2.2 , 当然,这次不需要修改 configure 了。

根据前面介绍的知识,很容易就能写出编译配置了

ffmpeg-4.2.2 根目录新建脚本: build_android_gcc.sh

#!/bin/bash
set -x
API=21
CPU=armv7-a
#so库输出目录
OUTPUT=/Users/cxp/Desktop/FFmpeg/ffmpeg-4.2.2/android/$CPU
# NDK的路径,根据自己的安装位置进行设置
NDK=/Users/cxp/Desktop/FFmpeg/android-ndk-r17c
# 库文件
SYSROOT=$NDK/platforms/android-$API/arch-arm
# 头文件
ISYSROOT=$NDK/sysroot/usr/include
# 汇编头文件
ASM=$ISYSROOT/arm-linux-androideabi
TOOLCHAIN=$NDK/toolchains/arm-linux-androideabi-4.9/prebuilt/darwin-x86_64

function build
{
./configure \
  --prefix=$OUTPUT \
  --target-os=android \
  --arch=arm \
  --cpu=armv7-a \
  --enable-asm \
  --enable-cross-compile \
  --enable-shared \
  --disable-static \
  --disable-doc \
  --disable-ffplay \
  --disable-ffprobe \
  --disable-symver \
  --disable-ffmpeg \
  --sysroot=$SYSROOT \
  --cc=$TOOLCHAIN/bin/arm-linux-androideabi-gcc \
  --cross-prefix=$TOOLCHAIN/bin/arm-linux-androideabi- \
  --extra-cflags="-I$ISYSROOT -I$ASM -fPIC"

make clean all
# 这里是定义用几个CPU编译
make -j12
make
make install
}
build

可以看到,在基本上配置和使用 CLANG 进行编译差不多。

有以下不同:

  1. 多了 cc 配置。因为如果不配置 cc 默认为 clang (参考前文的分析);
  2. 多了 extra-cflags 的配置,因为 SYSROOT 中只包含了 库文件 ,需要额外配置 头文件 的搜索路径;汇编头文件 的路径也不在 SYSROOT 中,也需要额外配置 ASM

启动编译

打开 cmd 终端,cd 到 ffmpeg-4.2.2 目录

执行 ./build_android_gcc.sh

六、总结

通过对 configure 的分析,可以让我们更加清晰的理解每个参数配置项的意义,以及如何搭配使用这些配置。只要清楚了各个配置的含义,无论版本怎么变化,都可很快的写出编译脚本。

当了,本文只是介绍了最基础的配置方案,你还可以通过更多的 --disable-xxx 选项实现对 FFmpeg 的裁剪,或者通过 --enable-xxx 选项,开启一些高级功能。

参考文章

FFmpeg源代码简单分析:configure

编译 FFmpeg 之 clang