Android, CMake and FFmpeg. Part Three: Throwing libx264 in the Mix
Originally published here.
Series Contents
- Part One: CMake in Android Cookbook
- Part Two: Building FFmpeg
- Part Three: Throwing libx264 in the Mix [you are here]
Throwing libx264 in the Mix
Preface
This article was long coming. I actually intended to follow up my very original article on FFmpeg with a continuation on how to integrate libx264, but due to some circumstances, I wasn’t able to do it at that time.
Anyway, the main purpose of the article is to show how to integrate external libraries into FFmpeg. While the main focus is on libx264, the general process should be extensible to any other library (hopefully).
Getting started
Before we start let’s make sure we all on the same page here:
- Make sure you have NDK installed. The version used in this article is 21.1.6352462.
- Make sure you have CMake installed. The version used in this article is 3.10.2.
- Make sure you have your C++ enabled project ready.
- Make sure you read the previous article in the series.
That’s pretty much all you need to get started.
Just give me the code
In case you’re not interested in the material in the article and would rather play with code, you can check out according to the article version of FFmpeg Development Kit.
Setting up
In this section, I will describe some groundwork before we will proceed to actual libx264 integration.
Adding switch to Gradle
Let’s add a switch to Gradle that will allow us to switch libx264 on and off. Remember that libx264 is distributed under GPL, so I think it’s a good idea to have an easy way to remove libx264 integration in case some legal problems arise.
Navigate your module’s build.gradle and add the following:
flavorDimensions "non-free"
productFlavors {
free {
dimension "non-free"
buildConfigField "boolean", "LIB_X264_ENABLED", "false"
externalNativeBuild {
cmake {
arguments "-DLIB_X264_ENABLED:BOOL=OFF", "-DANDROID_ARM_NEON=ON"
}
}
}
nonFree {
dimension "non-free"
buildConfigField "boolean", "LIB_X264_ENABLED", "true"
externalNativeBuild {
cmake {
arguments '-DLIB_X264_ENABLED:BOOL=ON', "-DANDROID_ARM_NEON=ON"
}
}
}
}
Nothing overly complicated here: we add a new flavor, add BuildConfig field that will indicate wherever libx264 is present or not for our Java/Kotlin code and we add a new flag for CMake which will indicate the same thing for the CMake configuration.
ffmpeg.cmake
We need a small adjustment to the ffmpeg.cmake. Navigate to the ExternalProject_Add call and at the very and of it, below
LOG_INSTALL 1
Add:
DEPENDS ${FFMPEG_DEPENDS}
That will allow us to specify targets that must be built before FFmpeg is built (libx264 in our case).
CMakeLists.txt
Now navigate to the CMakeLists.txt and just before the line:
include(ffmpeg.cmake)
Add following code:
IF (${LIB_X264_ENABLED})
include(libx264.cmake) set(FFMPEG_DEPENDS libx264_target)
set(FFMPEG_EXTRA_C_FLAGS "-I${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/include")
set(FFMPEG_EXTRA_LD_FLAGS "-L${CMAKE_LIBRARY_OUTPUT_DIRECTORY}") list(APPEND FFMPEG_CONFIGURE_EXTRAS --enable-gpl --enable-libx264 --enable-nonfree)
list(APPEND FFMPEG_LIBS postproc x264)
ENDIF()
Don’t worry about “libx264.cmake” for now, we will create it in the next step. For now, as you can see we set a new variable FFMPEG_DEPENDS
which will allow us to indicate that we need to build something before FFmpeg is built. We also add an include path and the library path to the FFMPEG_EXTRA_C_FLAGS
and FFMPEG_EXTRA_LD_FLAGS
.
The last but not least: we need to make sure FFmpeg is configured with proper flags and also add libraries to link against (postproc and x264).
libx264.cmake
Now, to the actual building. First of all, create a new file called “libx264.cmake”. By analogy with “ffmpeg.cmake” it’s going to be the file that will take care of building libx264 for us. Let’s start from the beginning (i.e. fetching).
Fetching sources
Add the following code to the newly created file:
cmake_minimum_required(VERSION 3.10.2)set(LIBX264_URL https://code.videolan.org/videolan/x264/-/archive/master/x264-master.tar.bz2)get_filename_component(LIBX264_ARCHIVE_NAME ${LIBX264_URL} NAME)
get_filename_component(LIBX264_NAME ${LIBX264_URL} NAME_WE)IF (NOT EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/${LIBX264_NAME})
file(DOWNLOAD ${LIBX264_URL} ${CMAKE_CURRENT_SOURCE_DIR}/${LIBX264_ARCHIVE_NAME}) execute_process(
COMMAND ${CMAKE_COMMAND} -E tar xzf ${CMAKE_CURRENT_SOURCE_DIR}/${LIBX264_ARCHIVE_NAME}
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
)
As you can see the process is very similar to the one we have for FFmpeg, with the exception that we don’t supply a version here and thus name handling works a little bit different.
Next, as with FFmpeg, we have to patch sources to make the library work properly on Android. You see by default libx264 creates a shared library with version suffix and libx264.so is a mere symlink. Android won’t load that properly (well it will, one time, but it seems to be removing symlinks after this). So let’s modify Makefile and “configure” script to get rid of that:
file(READ ${CMAKE_CURRENT_SOURCE_DIR}/${LIBX264_NAME}/configure configure_src)
string(REPLACE "echo \"SONAME=libx264.so.$API\" >> config.mak" "echo \"SONAME=libx264.so\" >> config.mak" configure_src "${configure_src}")
file(WRITE ${CMAKE_CURRENT_SOURCE_DIR}/${LIBX264_NAME}/configure "${configure_src}") file(READ ${CMAKE_CURRENT_SOURCE_DIR}/${LIBX264_NAME}/Makefile makefile_src)
string(REPLACE "ln -f -s $(SONAME) $(DESTDIR)$(libdir)/libx264.$(SOSUFFIX)" "# ln -f -s $(SONAME) $(DESTDIR)$(libdir)/libx264.$(SOSUFFIX)" makefile_src "${makefile_src}")
file(WRITE ${CMAKE_CURRENT_SOURCE_DIR}/${LIBX264_NAME}/Makefile "${makefile_src}")
ENDIF()
What we do here is we modify configure to have SONAME
without suffix and we modify Makefile to don’t create a symlink (i.e. we just comment it out in install step). That’s it for the fetching process.
Copying build system
The same way we had build system script for FFmpeg we will have it for libx264. Let’s create file called “libx264_build_system.cmake”. It will be almost the same as “ffmpeg_build_system.cmake” so I’ll just provide full code here:
cmake_minimum_required(VERSION 3.10.2)if (${STEP} STREQUAL configure)
# Encoding string to list
string(REPLACE "|" ";" CONFIGURE_EXTRAS_ENCODED "${CONFIGURE_EXTRAS}")
list(REMOVE_ITEM CONFIGURE_EXTRAS_ENCODED "")set(CONFIGURE_COMMAND
./configure
--extra-cflags=${C_FLAGS}
--extra-ldflags=${LD_FLAGS}
--extra-asflags=${AS_FLAGS}
--sysroot=${SYSROOT}
--host=${HOST}
--enable-pic
--enable-shared
--libdir=${PREFIX}
--prefix=${PREFIX}
${CONFIGURE_EXTRAS_ENCODED}
) execute_process(COMMAND ${CONFIGURE_COMMAND})
elseif(${STEP} STREQUAL build)
execute_process(COMMAND ${HOST_TOOLCHAIN}/make -j${NJOBS})
elseif(${STEP} STREQUAL install)
execute_process(COMMAND ${HOST_TOOLCHAIN}/make install)
endif()
And in “libx264.cmake” add following code:
file(
COPY ${CMAKE_CURRENT_SOURCE_DIR}/libx264_build_system.cmake
DESTINATION ${CMAKE_CURRENT_SOURCE_DIR}/${LIBX264_NAME}
FILE_PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE
)
Now we should have our build system script in the folder with sources.
Setting build tools
Similarly to the FFmpeg, we want to set our build tools for libx264:
set(LIBX264_CC ${CMAKE_C_COMPILER})
set(LIBX264_AR ${ANDROID_AR})
set(LIBX264_AS ${ANDROID_ASM_COMPILER})
set(LIBX264_RANLIB ${ANDROID_TOOLCHAIN_PREFIX}ranlib${ANDROID_TOOLCHAIN_SUFFIX})
set(LIBX264_STRIP ${ANDROID_TOOLCHAIN_ROOT}/bin/llvm-strip${ANDROID_TOOLCHAIN_SUFFIX})
Note that for libx264 we don’t need the NM tool.
Setting flags
This section is the same as in “ffmpeg.cmake”:
string(REPLACE " -Wl,--fatal-warnings" "" LIBX264_LD_FLAGS ${CMAKE_SHARED_LINKER_FLAGS})string(STRIP ${CMAKE_C_FLAGS} LIBX264_C_FLAGS)
string(STRIP ${LIBX264_LD_FLAGS} LIBX264_LD_FLAGS)set(LIBX264_C_FLAGS "${LIBX264_C_FLAGS} --target=${ANDROID_LLVM_TRIPLE} --gcc-toolchain=${ANDROID_TOOLCHAIN_ROOT}")
set(LIBX264_ASM_FLAGS "${CMAKE_ASM_FLAGS} --target=${ANDROID_LLVM_TRIPLE}")
set(LIBX264_LD_FLAGS "${LIBX264_C_FLAGS} ${LIBX264_LD_FLAGS}")
Misc variables
Again, pretty much the same:
set(NJOBS 4)
set(HOST_BIN ${ANDROID_NDK}/prebuilt/${ANDROID_HOST_TAG}/bin)
Patching up x86
Well, this is where the fun begins I guess. Both FFmpeg and libx264 require patching on x86, but in a different manner. Where FFmpeg just requires you to disable assembly to comply with PIC, libx264 has PIC assembly on x86, but it uses too many registers (NDK passes -mstackrealign which takes up an additional register). And x86_64 requires NASM. YASM, unfortunately, is outdated and won’t work. So, what we will do is to check if the host has NASM installed and if not or ABI is x86 — we disable assembly:
set(LIBX264_CONFIGURE_EXTRAS "")
IF (${CMAKE_ANDROID_ARCH_ABI} MATCHES ^x86)
find_program(NASM_EXE nasm) IF (NASM_EXE)
SET(LIBX264_AS ${NASM_EXE})
SET(LIBX264_ASM_FLAGS "")
ENDIF() IF(NOT NASM_EXE OR ${CMAKE_ANDROID_ARCH_ABI} STREQUAL x86)
list(APPEND LIBX264_CONFIGURE_EXTRAS --disable-asm)
ENDIF()
ENDIF()
Note that we remove assembly flags for NASM — libx264 will set proper flags by itself.
Encoding extras
The same as in “ffmpeg.cmake”:
string(REPLACE ";" "|" LIBX264_CONFIGURE_EXTRAS_ENCODED "${LIBX264_CONFIGURE_EXTRAS}")
ExternalProject_Add
And now, finally, the main part of the script — defining build process itself:
ExternalProject_Add(libx264_target
PREFIX libx264_pref
URL ${CMAKE_CURRENT_SOURCE_DIR}/${LIBX264_NAME}
DOWNLOAD_NO_EXTRACT 1
CONFIGURE_COMMAND ${CMAKE_COMMAND} -E env
CC=${LIBX264_CC}
AS=${LIBX264_AS}
AR=${LIBX264_AR}
RANLIB=${LIBX264_RANLIB}
STRIP=${LIBX264_STRIP}
${CMAKE_COMMAND}
-DSTEP:STRING=configure
-DHOST:STRING=${ANDROID_LLVM_TRIPLE}
-DSYSROOT:STRING=${CMAKE_SYSROOT}
-DC_FLAGS:STRING=${LIBX264_C_FLAGS}
-DAS_FLAGS:STRING=${LIBX264_ASM_FLAGS}
-DLD_FLAGS:STRING=${LIBX264_LD_FLAGS}
-DPREFIX:STRING=${CMAKE_LIBRARY_OUTPUT_DIRECTORY}
-DCONFIGURE_EXTRAS:STRING=${LIBX264_CONFIGURE_EXTRAS_ENCODED}
-P libx264_build_system.cmake
BUILD_COMMAND ${CMAKE_COMMAND}
-DSTEP:STRING=build
-DNJOBS:STRING=${NJOBS}
-DHOST_TOOLCHAIN:STRING=${HOST_BIN}
-P libx264_build_system.cmake
BUILD_IN_SOURCE 1
INSTALL_COMMAND ${CMAKE_COMMAND}
-DSTEP:STRING=install
-DHOST_TOOLCHAIN:STRING=${HOST_BIN}
-P libx264_build_system.cmake
LOG_BUILD 1
LOG_INSTALL 1
LOG_CONFIGURE 1
)
It is in general very similar to the “ffmpeg.cmake” version, however, note that here we export all build tools as environment variables for the configuration step. We do it because libx264 doesn’t allow us to provide it as “configure” params. The only parameter that kind of allows us to do that is “cross-prefix”, but it won’t work with NASM on x86_64 then.
Closing words
This series of articles was long coming and I’m glad I finally was able to finish it, even replacing ndk-build with CMake.
I do hope it will help out other programmers, smashing their head against the wall of native development, as I was, to finish their work faster and easier!
See you in the next articles!