Android, CMake and FFmpeg. Part Two: Building FFmpeg

Ilia Kosynkin
14 min readJun 1, 2020

--

Originally published here.

Series Preface

This series of three articles (series content below) aims to help Android developers without much of experience in the native development to integrate external native libraries (in this case FFmpeg and libx264, but it should be relatively easy to extend to other projects) and streamline the whole process by using CMake instead of previously used ndk-build.

Series Contents

  1. Part One: CMake in Android Cookbook [you are here]
  2. Part Two: Building FFmpeg
  3. Part Three: Throwing libx264 in the Mix

CMake in Android Cookbook

Preface

It’s important to highlight that it is not really an article or a tutorial on either CMake nor its specifics on the Android platform. As the name, “Cookbook”, suggests it’s supposed to be used as a reference to be returned to multiple times and it’s mixed both from “beginners” tips and more advanced stuff (so if it seems that a section talks about something you already know, feel free to skip it).

It’s also worth noting that while it’s part of a series, mainly because it is a necessary foundation to make sense of what is happening in next parts, I do hope it will be useful to everybody, who is working or plans to work in CMake!

CMake: getting started

I think the best way to describe CMake is “more elaborate version of configure-based approach”. Essentially, it configures projects for specific build systems (Make, Ninja, Visual Studio, XCode etc.) using a specially designed scripting language (note that Android Studio uses Ninja, the build system designed for fast, iterative builds).

How a project is generated is defined by a file called CMakeLists.txt, which you can think of as a project file (or script, rather) of sorts. If you used the Native C++ template in the New Project Wizard in Android Studio it should’ve already generated the bare bones CMake project for you.

If you have an existing project you wish to integrate native code to, you can follow this guide to navigate you through.

The next section will present basic syntax and some commands that will be used later on.

CMake: syntax and commands reference

Variables

CMake has a relatively complex system for variables. I suggest referring to set documentation for additional details.

Let’s start with the basics.

Basics

set(<name>, <value>) – sets value to the variable, where <value> can be string, number or list.

To set a list simply call set(<name>, <value_0> <value_1> … <value_n>). Note that lists are handled as strings by CMake internally, with elements separated by “;”. So calls set(list, a b c) and set(list, “a;b;c”) are the same.

To get a variable you just need to put its name in an expansion operator like this: ${<name>}.

Important note: people who worked with shell scripts before might tend to use “${<name>}” to make sure value is expanded correctly without really thinking if they need a string here or not. I strongly advise from doing this: CMake has a lot of internal magic, which splits arguments for commands and saves them into the cache and there is a good chance you’re going to break it by making it treat some parts of command as unsplittable values. A good rule of thumb would be: don’t use quotes unless it doesn’t work without them. While you’re probably not going to run into issues in simple cases, in more complex scenarios (for example making custom targets) it might create all kinds of issues and cause hours of painstaking debugging to fix the issue.

Setting and getting environment variables

To set environment variables in code use: set(ENV{<name>} <value>).

To get environment variable use: $ENV{<name>}.

Setting variables in script invocation

Often you would need to invoke CMake script with certain flags or values. You can do so with -D switch. For example:

${CMAKE_COMMAND} -DNAME:STRING=value -P script.cmake

Will invoke script.cmake with variable NAME set to “value”.

To set environment variables for the script use “-E env” (I suggest looking at Command-Line Tool Mode for CMake for more details) switch which allows you to modify the environment for the invocation:

${CMAKE_COMMAND} -E env PATH=some_path:$ENV{PATH} <COMMAND>

Controlling execution flow

Controlling program flow (if, for and while) is more in line with what you would expect from a programming language, but there are also a few differences to the syntax you would usually see.

Note that in order to break loops you can use break() and continue().

IF

Controlling flow with if:

IF(<condition>)
<code>
ELSEIF(<condition>)
<code>
ENDIF()

Possible conditions are:

  • <variable>|<value> LESS|GREATER|EQUAL <variable>|<value> for numbers
  • <variable>|<value> STREQUAL|MATCHES <variable>|<value> for strings
  • EXISTS <path> check if path exists
  • NOT <condition> invert
  • <condition> AND|OR <condition> logic operators

This is most definitely not a complete list of operators, but most useful ones, in my opinion. Note that <variable> here is variable name, not expansion (i.e. name, not ${name}).

FOREACH

To iterate over list use following syntax:

FOREACH(<variable> IN <list>)
<loop>
ENDFOREACH(<variable>)

To iterate with index you have following options:

FOREACH(<variable> RANGE <total>)
<loop>
ENDFOREACH(<variable>)

or with specific start, stop and optional step:

FOREACH(<variable> RANGE <start> <stop> [step])
<loop>
ENDFOREACH(<variable>)

WHILE

This is pretty straightforward, you can use it in following way:

WHILE(<condition>)
<code>
ENDWHILE()

Useful commands

I think it’s important to note that everything described above (including set) are also commands. But I think it’s easier mentally to distinguish syntax constructions and other commands, especially when you’re just starting out and trying to apply knowledge from other languages to CMake.

file()

File is a very useful command that allows you to manipulate local and remote file systems. I’ll give general outline of available subcommands that I find the most useful:

  • file(READ <path> <variable>) – reads file at path to the variable.
  • file(WRITE|APPEND <path> <content>) – writes/appends content to file.
  • file(GLOB <variable> <expr>) – collects files matching <expr> into <variable> in specified folder.
  • file(GLOB_RECURSE <variable> <expr>) – collects files matching <expr> into <variable> in specified folder and all subfolders.
  • file(TO_CMAKE_PATH <path> <variable>) – converts path to internal CMake format. Helpful on Windows which has backslashes in the path.
  • file(DOWNLOAD <url> <path>) – download file from <url> to <path>.

In order to find files under a certain folder with GLOB you can use expressions like “path/expression”, i.e. just prefix your expression with path, otherwise it will run in the current directory. However, I should note that if you want to do this to add sources to the target — it’s not advised to do so by CMake creators. The main reason is that CMake won’t pick up if you have changed the target sources and will not prompt you to re-run configuration. If you work with co-workers, after they download changes it won’t prompt them either to re-run configuration, so it might create some weird issues with the project not building properly. It doesn’t necessarily mean you absolutely should not use it ever, just be aware of that possibility and make a conscious decision.

find_library and find_program

Well those two are pretty straightforward: they find a library (to link against) or program (to use).

You can run them with: find_(library|program)(<variable> <name>) and it will run search through PATH and CMake search locations and if the library/program is found its path will be put into <variable>.

get_filename_component

A super useful command to deal with paths/urls. Basic usage is:

get_filename_component(<variable> <path/url> <mode>)

It will put part of the path/url specified by mode into <variable>.

Mode is one of the following:

  • DIRECTORY — path to file directory.
  • NAME — file name (with extension).
  • EXT — file extension.
  • NAME_WE — file name (without extension).
  • LAST_EXT — last file extension (i.e. c from a.b.c).
  • NAME_WLE — file name (with last extension).

include

Includes CMake file or module into the project. For example: include(your_file.cmake) or include(CMakeModule) (a little bit more about built-in modules later on).

Important note: it preserves the scope of the parent project, which effectively means all variables and functions declared before include() invocation are also available in the included script.

math

Allows you to evaluate mathematical expression. Example usage:

math(EXPR <variable> “100 * 2”)

will put 200 into <variable>.

Note that the result must be representable as a 64-bit signed integer.

list

List is a helper command to work with lists which has a lot of useful sub-commands.

  • list(LENGTH <list> <variable>) – puts length of <list> into <variable>.
  • list(GET <list> <index> <variable>) – puts element at <index> from <list> to <variable>. You can specify multiple indexes to get a subset of the original list.
  • list(APPEND <list> <element>) – appends an <element> to <list>. You can specify multiple elements to append them to list.
  • list(REMOVE_AT <list> <index>) – removes element at <index> from <list>. You can pass multiple indexes to remove multiple items.

string

Very useful command to operate on strings.

  • string(REPLACE <match_string> <replace_string> <variable> <input>) – finds <match_string> in <input>, replaces it with <replace_string> and puts result into <variable>.
  • string(REGEX REPLACE <match_expr> <replace_expr> <variable> <input>) – finds expression <match_expr> in <input>, replaces it with <replace_expr> and places result in the <variable>.
  • string(APPEND <value> <input>) – appends string to the string.
  • string(STRIP <string> <variable>) – strips whitespaces from <string> and puts result into <variable>.

add_dependecies

Allows you to specify dependencies between targets. Basic usage looks like:

add_dependecies(<target> [<dependent_target_1> … <dependent_target_n>])

All targets in <dependent_target> list will be brought up to date before <target> is built.

add_executable

Creates a target for building an executable. Basic usage is:

add_executable(<name> [sources])

add_library

Creates a target for building a library. Basic usage is:

add_library(<name> STATIC|SHARED|MODULE [sources])

add_subdirectory

Adds subdirectory to the project. Subdirectory must have its own CMakeLists.txt in it. Effectively it allows you to create sub-modules of the project. However note that in contrast with include() add_subirectory creates its own scope, instead of inheriting parent’s. Basic usage is:

add_subdirectory(<path>)

Path can be absolute or relative.

include_directories

Adds directories to include search paths for the current CMakeLists. Note that include_directories will affect all targets declared after its invocation. Keep it in mind to avoid getting include errors. Basic usage:

include_directories([path_1 … path_n])

Command supports relative paths.

link_directories

Adds directories to library search paths for the current CMakeLists. Note that link_directories will affect all targets declared after its invocation. Keep it in mind to avoid linker errors. Basic usage is:

link_directories([path_1 … path_n])

Command supports relative paths.

target_link_libraries

Allows you to link specific library to the target. Note that generally command is quite complex, but in most basic and common form it looks like:

target_link_libraries(<target_name> [lib_name_1 … lib_name_n])

Library name might be either name or full path to the library.

CMake: phases and targets

When working with CMake it’s important to remember that it works in two phases and be aware of which phase you’re targeting.

The first phase is the configuration phase itself; it defines how the project is going to be built and usually takes up most of the space of a script(s). However, there is one caveat: CMake script can be (and also often is) executed in the build phase.

So what is the build phase? The build phase is the process of building targets, defined in the configuration process.

What are targets? Targets essentially are commands executed by a build system during the build process. It may be a little bit cryptic, so let’s consider how you define target in CMake. Usual ways to define targets would be commands: add_library, add_executable and add_custom_target. I think the first two are self-explanatory and pretty much explain what targets are — it’s a sequence of commands defined to assemble a certain piece of software (library or executable).

What about add_custom_target then? You can think about it as about a built-in way to extend the build process with custom commands. Where add_library or add_executable will strictly build a library or executable, add_custom_target allows you to specify a custom command to be run during build time. One example of such a command would be a clean target, which usually deletes files left after previous builds.

The last piece in the targets’ puzzle is the fact that you can define relationships between targets with add_dependecies function. That way you can make sure targets are executed in a specific order. All together this system provides you with a strong and flexible suite to build pretty much anything the way you want.

CMake: built-in modules

A module is essentially a collection of commands (or library if you will), which simplifies certain tasks. You can check the reference for a full list of available modules.

ExternalProject

Pretty much a “must-have” module if you’re integrating any external project into yours, especially if the integrated project is not CMake-based. While default implementation actually expects CMake project, by tweaking a few parameters you can easily (well, I guess easier is a better word) integrate configure-based projects. Essentially, you can think about ExternalProject as about system for managing dependencies (like we have Gradle in Android): it allows you to download, unpack, configure, build and install any project you can get sources of.

Let’s start with really basic examples.

Basics

First of all make sure you have included the module like this:

include(ExternalProject)

Next let’s define project we want to add:

ExternalProject_Add(
example
PREFIX example_prefix
URL https://www.example.com/sources.tar.gz
)

That’s a really barebones example which probably won’t really occur in the real life, but let’s go over some basic properties we used here:

  • The first parameter, which is required, is the project name. It will be used to create target for the external project which you can use later on.
  • PREFIX — is essentially a folder where all operations regarding external project are going to take place. Those operations are: download, configuration, build etc.
  • URL — URL to the sources. Note that while usually some remote endpoint is provided to download the archive with sources, you can actually provide a path to the local archive (or folder, but it requires a few additional tricks).

Using git

Quite often you might want to use a git repository with specific version to build:

ExternalProject_Add(
example
PREFIX example_prefix
GIT_REPOSITORY https://github.com/Examples/git-example.git
GIT_TAG origin/1.2.3
GIT_SHALLOW 1
)
  • GIT_REPOSITORY — quite straightforward, just a URL to repo or git@ URL.
  • GIT_TAG — despite its name, it’s not tag necessarily. It can be branch, tag or commit.
  • GIT_SHALLOW — if set, downloads only specific tag, without entire repo history.

Using local folder

In Android environment, which usually builds few architectures you probably would like to avoid additional overhead of downloading and unpacking sources for every architecture:

ExternalProject_Add(
example
PREFIX example_prefix
URL ${CMAKE_CURRENT_SOURCE_DIR}/sources
DOWNLOAD_NO_EXTRACT 1
)

In this case we supply a path to the local folder with sources. DOWNLOAD_NO_EXTRACT makes ExternalProject skip the extract step and just copy sources folder to the prefix source folder.

Building in local folder

While it might not be a great idea for many projects (mainly because there are usually some left-overs which are left by build and they might conflict with future build, while usually you want to make clean builds), in some cases it might be worth it to at least try it out (for example, if you’re really trying to squeeze out every little bit of performance from build process).

ExternalProject_Add(
example
SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/sources
DOWNLOAD_COMMAND “”
BUILD_IN_SOURCE 1
)

As you can see we don’t supply PREFIX here, instead we set SOURCE_DIR directly. We also override DOWNLOAD_COMMAND to empty string, so we don’t try to download anything. Setting BUILD_IN_SOURCE prevents ExternalProject from creating additional build folder and builds in SOURCE_DIR directly instead.

Additional notes

The built-in steps that ExternalProject executes: DOWNLOAD, UPDATE, CONFIGURE, BUILD and INSTALL (in this order). You can override any of those command with according *_COMMAND parameter and log any of those with LOG_* 1 parameter.

If you using default behavior which builds project as CMake project you can modify behavior with custom arguments like following:

CMAKE_ARGS -DSHARED:BOOL=ON

Or alternatively you might use CMAKE_CACHE_ARGS to force set cache variables.

CMake and Android

Toolchain file

CMake allows to integrate custom platforms with so called toolchain: file which specifies some behavior and flags specific to platform. On Android this file is located ${NDK_LOCATION}/build/cmake/android.toolchain.cmake or you can find the latest version online here.

If you do anything in CMake in Android I highly recommend to have this file opened all the time, since you probably going to constantly look up variables and other stuff.

Important directories

Another Android specific thing would be building different architectures. In your module folder there should be hidden folder called “cxx”. To get to actual build folder navigate to cxx/cmake/${variantName}/${abi}. This folder is quite important because:

  1. Prefix folders of all external projects you added are going to be located here and you probably want to have those open to read logs in case something fails.
  2. CMake generates build files there. Android is using Ninja so files you’re interested in would be: build.ninja and rules.ninja. You might be interested in those to check how your library is built, which flags are used etc. In general it’s a good source of information in case you stuck and need some insight in how it’s actually works under the hood.
  3. All other files, while being much less useful than prefixes and generated build files, still might be of use in certain use cases.

Another important directory you might want to check out is ${CMAKE_LIBRARY_OUTPUT_DIRECTORY} which by default is located at ${MODULE_PATH}/build/intermediates/cmake/${varianName}/obj/${abi}. This is directory where you want to output your shared libs so build system will automatically pick them up for distribution. I also prefer to put “include” folder here as well since it seems more intuitive for me, but you definitely can you use any other directory you want.

Important variables

Android toolchain file defines many useful variables. Some of them are listed below:

  • CMAKE_LIBRARY_OUTPUT_DIRECTORY — directory where your built artifacts will be outputted to.
  • CMAKE_CURRENT_SOURCE_DIR — directory of current CMake file.
  • ANDROID_TOOLCHAIN_PREFIX — path to the Android built toolchain for current ABI.
  • ANDROID_TOOLCHAIN_ROOT — root of the toolchain. You usually interested in “bin” directory which has all the build tools.
  • ANDROID_TOOLCHAIN_SUFFIX — suffix for toolchain. Empty on *nix host systems, set to “.exe” for Windows.
  • CMAKE_C_COMPILER — C compiler, set by toolchain file.
  • CMAKE_CXX_COMPILER — CXX compiler, set by toolchain file.
  • ANDROID_ASM_COMPILER — assembler, set by toolchain file.
  • ANDROID_AR — archive tool, set by toolchain file.
  • CMAKE_C_FLAGS — C compiler flags, set by toolchain file.
  • CMAKE_CXX_FLAGS — CXX compiler flags, set by toolchain file.
  • CMAKE_ASM_FLAGS — assembler flags, set by toolchain file.
  • CMAKE_SHARED_LINKER_FLAGS — linker flags, set by toolchain files.
  • ANDROID_LLVM_TRIPLE — target triple (eg. aarch64-none-linux-android21 and yes I know it’s not a triple).
  • CMAKE_SYSTEM_PROCESSOR — target processor (eg. i686).
  • CMAKE_ANDROID_ARCH_ABI — current ABI, set by toolchain file.

Additional notes

This section accumulates some additional advices that hopefully will help out other developers to don’t spend half of a day trying to understand what is going on:

  • Android toolchain sets “-Wl,–fatal-warnings” without ability to turn it off. This flag makes all linker warnings to be treated as errors. While it might be good for library you’re writing, it’s probably not a good idea to have it turned on for external libraries, since you have no control over them. If you get errors logs like “warning treated as error” I recommend removing this flag from the CMAKE_SHARED_LINKER_FLAGS.
  • When integrating configure based projects with ExternalProject you will have to pass all flags (c, cxx, ld and asm flags) manually. Note that flags you receive from toolchain doesn’t have “–target” and “–gcc-toolchain” in them and those required by CLang to properly generate libs (especially if you have assembly files in the library). Make sure to append those flags to all relevant flags (for instance for FFmpeg you want to append those to c, ld and asm flags).
  • Make sure that you’re not only specifying proper target for the compiler, but also provide proper target/host to the library itself. Some libraries have different sets of assembly files for different architectures. Look up –help of configure script to see if there are any flags named “target” or “host” or similar. If those are present make sure to properly specify them, otherwise you might end up with conflict where library provides assembly files for you machine (which is default value of host), but assembler expects files for target (phone).
  • PIC and x86. Android forces PIC from Marshmallow and up, however some libraries (notable FFmpeg) does not provide support for PIC on x86 (reasoning being that the performance would be too bad). If you run in such situation check if you can disable assembly for the library (check for flag in the configure –help).
  • You can pass additional arguments to CMake in gradle by setting property arguments in externalNativeBuild.cmake closure (either in defaultConfig or declaration of your flavor/build type) and you can set specific ABIs you’re building for setting property abiFilters in ndk closure.

References

  1. https://www.selectiveintellect.net/blog/2016/7/29/using-cmake-to-add-third-party-libraries-to-your-project-1
  2. http://kai-wolf.me/cpp/android/cmake/gradle/2019/02/18/working-with-cpp-in-android-effectively/
  3. https://github.com/Kitware/CMake/blob/master/Modules/ExternalProject.cmake
  4. https://trac.ffmpeg.org/ticket/4928

--

--