编译带文字水印功能的FFmpeg
需求
我的需求是:在Android设备上使用Camera来采集摄像头画面,得到YUV数据,使用FFmpeg给YUV添加水印,然后再编码为H264,然后网络发送或保存MP4。前提条件是要先编译出FFmpeg,且需要启用drawtext功能,而这个功能又依赖于FreeType和HarfBuzz。
关于 HarfBuzz 和 FreeType 的关系:
FreeType是一个字体渲染引擎(font rasterizer),它负责把字体文件(如 TTF、OTF)里的矢量字形(glyph)渲染成像素点(bitmap)或者轮廓(outline)数据。HarfBuzz是一个文字排版引擎(text shaping engine),它负责处理复杂文本的排版规则,例如中文、阿拉伯文、印地文等需要连字(ligature)、上下文形态(contextual forms)、方向控制(bidirectional)等特殊排版的情况。
简单来说:
HarfBuzz→ 计算字形位置和形态(shaping)FreeType→ 渲染字形为像素/轮廓(rasterization)
在实际使用中,通常的流程是:
文本 → `HarfBuzz`(计算每个字符对应的 glyph 以及它们的 x/y 位置) → `FreeType`(把 glyph 渲染成像素或路径) → 显示到屏幕或图像
可以理解为 HarfBuzz 负责排版,FreeType 负责画图。
1、下载
1.1 下载Linux版本NDK
我们需要在Linux系统下编译FFmpeg,所以需要用到Linux版本的NDK,官网下载地址:https://developer.android.google.cn/ndk/downloads,截图如下:

当前NDK最新的LTS版本是r27d,LTS即Long-Term Support(长期支持)的意思。
最新的稳定版本是r29,但它并不是长期支持版本,所以我选择下载r27d版本。
1.2、下载FreeType
FreeType官网:https://freetype.org/,在首页,它显示了当前FreeType的最新版本为2.14.1版本,是2025-09-11发布的,如下:

点击左侧菜单Download,然后选择稳定版本下载,如下:

打开这个链接后,会显示很多的版本,最新版本并不是排在最前面的,因为默认是按名字排序的,我们点一下Date,以便按日期排序,此时比较前面的位置就能看到最新版本,如下:

- ft2141.zip,这是Windows下使用的,
ft就是FreeType的缩写,2141即2.14.1版本,这个名字搞特殊,可能就是希望在按文件名排序的时候,所有的Windows平台的能排在一起。 - freetype-2.14.1.tar.xz Linux平台下使用的,使用
xz压缩格式,比gz压缩比高。所以我选择下载这个。 - freetype-2.14.1.tar.gz Linux平台下使用的,使用
gz压缩格式,比xz压缩比低。 - 在这个列表中可以看到还有提供demo、doc,以及一些很旧的版本:
freetype-old,还有.sig结尾的是签名文件,用来验证源代码完整性的和真实性的,具体验证方法可问AI。
1.3、下载FFmpeg
FFmpeg的代码并不托管在Github,Github上面的只是一个镜像,官方代码是托管在:https://code.ffmpeg.org/FFmpeg/FFmpeg,这是FFmpeg自己的官方代码托管网站,所以,如果发现有Bug,可以在这上面提,而不是在Github上。有点神奇,这个官网竟然支持中文。点击 "标签",可选择需要的版本来下载,如下:



这里我选择下载8.0版本。但是下载会报错,估计是网站问题,可以从Github镜像上下载,也可以从Gitee镜像上下载,Gitee是国内网站,所以我选择它,Gitee下载更快更稳定:https://gitee.com/mirrors/ffmpeg/tags,截图如下:

如上图,点击 "下载" 按钮进行下载,下载的是一个zip文件,它并没有提供.tar.gz文件,如果需要可以到Github上下载。
2. 编译
2.1、初始化配置
前面我们下载到了android-ndk-r27d-linux.zip、freetype-2.14.1.tar.xz、ffmpeg-n8.0.zip,这里我使用的是VMware虚拟机,安装Ubuntu系统,使用的是ubuntu-24.04-live-server-amd64.iso,这些文件我在百度网盘也备份了:https://pan.baidu.com/s/120kCQsrn4wZ_fl0QSG9xCA?pwd=f7ug
启动Ubuntu虚拟机,使用Xshell连接后,复制下面命令:
groovy
# 设置源代码路径
export output=$HOME/ffmpeg-output
export sources=$HOME/ffmpeg-sources
export freetype=$sources/freetype-2.14.1
export harfbuzz=$freetype/subprojects/harfbuzz-11.4.1
export ffmpeg=$sources/ffmpeg-n8.0
# 设置编译路径
export build=$output/build
export harfbuzz_build=$build/harfbuzz
export freetype_build=$build/freetype
export ffmpeg_build=$build/ffmpeg
# 设置安装路径
export install=$output/install
export harfbuzz_install=$install/harfbuzz
export freetype_install=$install/freetype
export ffmpeg_install=$install/ffmpeg
# 设置依赖路径
export harfbuzz_pkg_config=$harfbuzz_install/lib/pkgconfig
export freetype_pkg_config=$freetype_install/lib/pkgconfig
export ffmpeg_pkg_config=$ffmpeg_install/lib/pkgconfig
export PKG_CONFIG_PATH=$harfbuzz_pkg_config:$freetype_pkg_config:$ffmpeg_pkg_config
# 设置交叉编译工具链路径
export ndk=$sources/android-ndk-r27d
export api=21
export ndktoolchain=$ndk/toolchains/llvm/prebuilt/linux-x86_64/bin
export target=aarch64-linux-android$api
export cc=$ndktoolchain/$target-clang
export cxx=$ndktoolchain/$target-clang++
export nm=$ndktoolchain/llvm-nm
export ar=$ndktoolchain/llvm-ar
export ranlib=$ndktoolchain/llvm-ranlib
export strip=$ndktoolchain/llvm-strip
# 创建ffmpeg编译目录:
mkdir -p $ffmpeg_build
# 创建源代码目录
mkdir $sources
使用Xfpt把android-ndk-r27d-linux.zip、freetype-2.14.1.tar.xz、ffmpeg-n8.0.zip上传到$HOME/ffmpeg-sources目录。
解压这3个压缩包:
groovy
cd $sources
unzip android-ndk-r27d-linux.zip
unzip ffmpeg-n8.0.zip
tar -xf freetype-2.14.1.tar.xz
安装基本工具,这些工具在我的Ubuntu中自带全都有,但是执行一下也没问题,它会安装没有的,或者安装更新的版本:
groovy
sudo apt install -y git wget tar yasm pkg-config build-essential autoconf automake cmake libtool
编译FreeType和HarfBuzz时,我们使用mesion+ninja的方式,所以安装meson、ninja(这两个系统并没有自带):
groovy
sudo apt update && sudo apt install -y meson ninja-build
提一下,这里我希望的目录结构是这样的:
groovy
ffmpeg-output
|-build
|-ffmpeg
|-freetype
|-harfbuzz
|-install
|-ffmpeg
|-freetype
|-harfbuzz
ffmpeg-sources
|-android-ndk-r27d
|-ffmpeg-n8.0
|-freetype-2.14.1
ffmpeg-sources用于保存下载的源代码,我顺便把ndk也放这里了ffmpeg-output/build用于保存编译输出,这里面有so和头文件,但是一般不直接使用这里面的。ffmpeg-output/install用于保存安装输出,在这里面就有so和头文件。
一般编译都是这个流程:
- 安装编译工具
- 配置编译选项
- 编译
- 安装
2.2、编译FreeType
创建交叉编译配置文件:
groovy
vim $freetype/android-arm64.txt
粘贴下面内容:
groovy
[binaries]
c = '/home/even/ffmpeg-sources/android-ndk-r27d/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android21-clang'
cpp = '/home/even/ffmpeg-sources/android-ndk-r27d/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android21-clang++'
ar = '/home/even/ffmpeg-sources/android-ndk-r27d/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar'
strip = '/home/even/ffmpeg-sources/android-ndk-r27d/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip'
pkg-config='/usr/bin/pkg-config'
[built-in options]
c_args = ['--sysroot=/home/even/ffmpeg-sources/android-ndk-r27d/toolchains/llvm/prebuilt/linux-x86_64/sysroot']
cpp_args = ['--sysroot=/home/even/ffmpeg-sources/android-ndk-r27d/toolchains/llvm/prebuilt/linux-x86_64/sysroot']
default_library = 'shared'
backend = 'ninja'
prefix = '/home/even/ffmpeg-output/install/freetype'
[host_machine]
system = 'android'
cpu_family = 'aarch64'
cpu = 'armv8-a'
endian = 'little'
配置FreeType:
groovy
# 需要从github下载HarfBuzz,所以需要设置代理,避免下载失败
export https_proxy="http://192.168.1.129:8082" && \
cd $freetype && \
meson setup $freetype_build \
--cross-file=android-arm64.txt \
--buildtype=release \
-Dharfbuzz=enabled \
-Dpng=disabled \
-Dtests=disabled
编译和安装FreeType:
groovy
cd $freetype_build && \
meson compile && \
meson install
此时查看安装目录,能看到生成了libfreetype.so、libharfbuzz.so、libharfbuzz-subset.so
2.3、编译HarfBuzz
创建交叉编译配置文件:
groovy
vim $harfbuzz/android-arm64.txt
粘贴下面内容:
groovy
[binaries]
c = '/home/even/ffmpeg-sources/android-ndk-r27d/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android21-clang'
cpp = '/home/even/ffmpeg-sources/android-ndk-r27d/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android21-clang++'
ar = '/home/even/ffmpeg-sources/android-ndk-r27d/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar'
strip = '/home/even/ffmpeg-sources/android-ndk-r27d/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip'
pkg-config='/usr/bin/pkg-config'
[built-in options]
c_args = ['--sysroot=/home/even/ffmpeg-sources/android-ndk-r27d/toolchains/llvm/prebuilt/linux-x86_64/sysroot']
cpp_args = ['--sysroot=/home/even/ffmpeg-sources/android-ndk-r27d/toolchains/llvm/prebuilt/linux-x86_64/sysroot']
default_library = 'shared'
backend = 'ninja'
prefix = '/home/even/ffmpeg-output/install/harfbuzz'
[host_machine]
system = 'android'
cpu_family = 'aarch64'
cpu = 'armv8-a'
endian = 'little'
HarfBuzz配置:
groovy
cd $harfbuzz && \
meson setup $harfbuzz_build \
--cross-file=android-arm64.txt \
--buildtype=release \
-Ddocs=disabled \
-Dtests=disabled \
-Dbenchmark=disabled \
-Dglib=disabled \
-Dfreetype=enabled
编译和安装HarfBuzz,并删除多余的文件:
groovy
cd $harfbuzz_build && \
meson compile && \
meson install && \
rm -rf $freetype_install/include/harfbuzz && \
rm -rf $freetype_install/lib/libharfbuzz.so && \
rm -rf $freetype_install/lib/libharfbuzz-subset.so && \
rm -rf $freetype_install/lib/pkgconfig/harfbuzz.pc && \
rm -rf $freetype_install/lib/pkgconfig/harfbuzz-subset.pc && \
rm -rf $harfbuzz_install/lib/libharfbuzz-subset.so && \
rm -rf $harfbuzz_install/lib/pkgconfig/harfbuzz-subset.pc
此时查看安装目录,能看到生成了libharfbuzz.so
2.4、编译FFmpeg
配置ffmpeg
groovy
cd $ffmpeg_build && \
$ffmpeg/configure \
--prefix=$ffmpeg_install \
--enable-shared \
--disable-static \
--disable-doc \
--disable-programs \
--disable-avdevice \
--disable-swresample \
--enable-filter=drawtext \
--enable-gpl \
--enable-libfreetype \
--enable-libharfbuzz \
--arch=aarch64 \
--target-os=android \
--extra-ldflags="-lm" \
--cc=$cc \
--cxx=$cxx \
--ar=$ar \
--nm=$nm \
--ranlib=$ranlib \
--strip=$strip \
--enable-cross-compile \
--pkg-config=/usr/bin/pkg-config
编译和安装FFmpeg:
groovy
cd $ffmpeg_build && \
make -j$(nproc) && \
make install
此时查看安装目录,能看到生成了libavcodec.so、libavfilter.so、libavformat.so、libavutil.so、libswscale.so
在Android项目中使用FFmpeg
-
创建一个名为
FFmpegVersion的Native项目。 -
复制头文件:在
cpp目录下创建一个ffmpeg目录,并在这个目录下创建一个include目录,然后把FFmpeg、FreeType和HarfBuzz的头文件复制过来,如下:
-
复制
so文件:在main目录下创建一个jniLibs目录,并在这个目录下创建一个arm64-v8a目录,然后把FFmpeg、FreeType和HarfBuzz的so文件复制过来,如下:
-
配置
CMakeLists,下面加有注释的是我添加的,其它都是原来就有的:groovycmake_minimum_required(VERSION 3.22.1) project("ffmpegversion") # 添加头文件目录 include_directories( ${CMAKE_SOURCE_DIR}/ffmpeg/include ${CMAKE_SOURCE_DIR}/ffmpeg/include/freetype2 ) # 把arm64-v8a目录的路径保存到FFMPEG_LIBS_DIR变量中,方便复用 set(FFMPEG_LIBS_DIR ${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI}) add_library(${CMAKE_PROJECT_NAME} SHARED native-lib.cpp) # 指定要链接到目标库的依赖库 target_link_libraries(${CMAKE_PROJECT_NAME} android log ${FFMPEG_LIBS_DIR}/libharfbuzz.so ${FFMPEG_LIBS_DIR}/libfreetype.so ${FFMPEG_LIBS_DIR}/libavutil.so ${FFMPEG_LIBS_DIR}/libavformat.so ${FFMPEG_LIBS_DIR}/libavcodec.so ${FFMPEG_LIBS_DIR}/libavfilter.so ${FFMPEG_LIBS_DIR}/libswscale.so) -
在原有的
navtive-lib.cpp代码中添加打印FFmpeg、FreeType、HarfBuzz的版本号:groovy#include <jni.h> #include <string> #include <android/log.h> extern "C" { #include "libavutil/avutil.h" #include "libavcodec/avcodec.h" #include "freetype2/ft2build.h" #include FT_FREETYPE_H #include "harfbuzz/hb.h" } #define LOG_TAG "FFMPEG_JNI" #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__) void printVersion() { FT_Library ft; FT_Init_FreeType(&ft); int major, minor, patch; FT_Library_Version(ft, &major, &minor, &patch); FT_Done_FreeType(ft); LOGI("FreeType version: %d.%d.%d", major, minor, patch); LOGI("HarfBuzz version: %s", hb_version_string()); LOGI("FFmpeg version:%s", av_version_info()); LOGI("FFmpeg build config:%s", avcodec_configuration()); } extern "C" JNIEXPORT jstring JNICALL Java_cn_android666_sdcard_ffmpegversion_MainActivity_stringFromJNI( JNIEnv* env, jobject /* this */) { printVersion(); std::string hello = "Hello from C++"; return env->NewStringUTF(hello.c_str()); }-
这里需要注意,导入关于
FFmpeg、FreeType、HarfBuzz的头文件时,要使用extern "C",因为这些库都是用C语言写的,而我们是在C++上使用它们,如果不包起来的话,在IDE上没显示报错,但是编译的时候报一些奇奇怪怪的错又很难理解。 -
另外导入
FreeType头文件时比较特殊,如下:groovy#include "freetype2/ft2build.h" #include FT_FREETYPE_H查看
ft2build.h,这个头文件很简单,里面就一个导入语句:#include <freetype/config/ftheader.h>,可以看到,它是从freetype目录开始的,这就是为什么我们在CMakeLists中设置头文件目录时需要单独给FreeType多添加一个目录的原因,如下:groovyinclude_directories( ${CMAKE_SOURCE_DIR}/ffmpeg/include ${CMAKE_SOURCE_DIR}/ffmpeg/include/freetype2 )我们在导入头文件时,都会基于这里设置的目录为相对目录进行查找,如果只设置一个目录(
${CMAKE_SOURCE_DIR}/ffmpeg/include)的话,则#include <freetype/config/ftheader.h>将会出错,因为在${CMAKE_SOURCE_DIR}/ffmpeg/include目录下并没有freetype目录,它是位于${CMAKE_SOURCE_DIR}/ffmpeg/include/freetype2/freetype,也就是说,要相对于freetype2目录才能找得到,所以freetype2目录需要单独设置一个。再来看
#include FT_FREETYPE_H,FT_FREETYPE_H这是一个宏,它定义在ftheader.h,如下:groovy#define FT_FREETYPE_H <freetype/freetype.h>所以,最终效果就是要导入
freetype.h,至于为什么要搞得这么奇怪,大家可以自己去问AI,或者大家只需要记住,如果要使用FreeType就这样导入头文件就行:groovy#include "freetype2/ft2build.h" #include FT_FREETYPE_H
-
-
运行项目,打印结果如下:
groovyFreeType version: 2.14.1 HarfBuzz version: 11.4.1 FFmpeg version:8.0 FFmpeg build config:--prefix=/home/even/ffmpeg-output/install/ffmpeg --enable-shared --disable-static --disable-doc --disable-programs --disable-avdevice --disable-swresample --enable-filter=drawtext --enable-gpl --enable-libfreetype --enable-libharfbuzz --arch=aarch64 --target-os=android --extra-ldflags=-lm --cc=/home/even/ffmpeg-sources/android-ndk-r27d/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android21-clang --cxx=/home/even/ffmpeg-sources/android-ndk-r27d/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android21-clang++ --ar=/home/even/ffmpeg-sources/android-ndk-r27d/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar --nm=/home/even/ffmpeg-sources/android-ndk-r27d/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-nm --ranlib=/home/even/ffmpeg-sources/android-ndk-r27d/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ranlib --strip=/home/even/ffmpeg-sources/android-ndk-r27d/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip --enable-cross-compile --pkg-config=/usr/bin/pkg-config除了打印版本,我还打印了
FFmpeg的编译配置,这其实就是我们编译FFmpeg时,调用configure脚本时的参数信息,即使时间久了,通过查看这个信息就能知道当前编译的FFmpeg启用了什么功能和禁用了什么功能。完整代码可以参考:https://gitee.com/daizhufei/FFmpegVersion,大家如果不想自己编译,也可以直接使用这里面的so和头文件,但我只编译了64位的,因为目前的Android手机基本上都是64位的了,如果需要32位的则需要自己去编译了,把我的教程复制给AI,问它如何修改编译为
armeabi-v7a的即可,需要改动的地方应该不多。
命令解释
1、命令执行
-
多条命令可以复制到
Xshell一次执行groovyexport output=$HOME/ffmpeg-output export sources=$HOME/ffmpeg-sources
两条命令会被依次执行,即便第一条命令执行失败,也会执行第二条命令
-
多条命令有依赖关系
groovymeson compile && meson install如果第一条命令执行失败,则第二条命令就不会被执行。
-
一条命令分多行来写
groovymeson setup $freetype_build \ --cross-file=android-arm64.txt \ -Dharfbuzz=enabled \ -Dpng=disabled在末尾使用
\,然后换行,\后面不要再有任何字符或空格。像这种命令可以一次性复制并粘贴到
Xshell中执行。 -
可以使用
#给命令添加注释groovy# 设置编译路径 export build=$output/build export harfbuzz_build=$build/harfbuzz # 设置安装路径 export install=$output/install export harfbuzz_install=$install/harfbuzz也可以这样:
groovycd $HOME # 进入主目录 ls # 显示当前目录内容但是在\后不能写注释,也不能有任何字符,如下是错误的:
groovymeson setup $freetype_build \ # 设置构建目录 --cross-file=android-arm64.txt \ # 设置交叉编译配置文件 -Dharfbuzz=enabled \ # 启用harfbuzz
2、FFmpeg配置命令
groovy
cd $ffmpeg_build && \ # 进入 FFmpeg 的构建目,这个目录需要自己提前创建
$ffmpeg/configure \ # 执行 FFmpeg 的 configure 脚本
--prefix=$ffmpeg_install \ # 编译完成后的输出安装路径,lib/.so、include/.h 会安装到这个路径
--enable-shared \ # 启用构建动态库(libxxx.so)
--disable-static \ # 禁用构建静态库(libxxx.a)
--disable-doc \ # 不生成文档(manual、HTML 等),减少不必要的构建时间和文件体积。
--disable-programs \ # 不编译 FFmpeg 的可执行程序:ffmpeg、ffprobe、ffplay
--disable-avdevice \ # 禁用 libavdevice(例如摄像头、音频输入设备)
--disable-swresample \ # 禁用音频重采样模块。如果你只做视频处理、不需要音频,禁用它能减少编译体积与依赖。
--enable-filter=drawtext \ # 启用 drawtext 滤镜
--enable-gpl \ # 启用 GPL 授权的功能。drawtext 在启用 HarfBuzz + FreeType 时属于 GPL,因此必须打开。
--enable-libfreetype \ # 告诉 FFmpeg 启用 FreeType,并使用 pkg-config 查找 freetype2.
--enable-libharfbuzz \ # 告诉 FFmpeg 启用 HarfBuzz,并使用 pkg-config 查找 harfbuzz.
--arch=aarch64 \ # 目标 CPU 架构是 ARM64(aarch64),对应 Android arm64-v8a
--target-os=android \ # 设置目标操作系统为 Android,configure 会自动调整 NDK 的一些规则。
--extra-ldflags="-lm" \ # 链接时额外加上 -lm(数学库)。
--enable-cross-compile \ # 显式告诉 FFmpeg 这是交叉编译
--pkg-config=/usr/bin/pkg-config # 强制使用系统的 pkg-config(不使用 NDK 内置的,内置它也没有)
另外还有几个参数是设置NDK工具链的路径,而不是使用系统默认的gcc,如下:
groovy
--cc=$cc \ # C 编译器,用于编译.c源代码,在 NDK ARM64 中一般是:aarch64-linux-android21-clang
--cxx=$cxx \ # C++ 编译器,用于编译.cxx、.cpp、.cc源代码,在 NDK ARM64 中一般是:aarch64-linux-android21-clang++
--ar=$ar \ # 静态库打包工具(Archiver),源代码编译为.o后,通过ar工具把多个.o文件打包成.a,nr在 NDK 中通常是:llvm-ar
--nm=$nm \ # 符号查看工具,用于查看 .o/.a/.so 中包含哪些符号(函数名、变量名),nm在 NDK 中通常是:llvm-nm
--ranlib=$ranlib \ # 静态库索引生成工具,用于为 静态库(.a) 生成一个索引,方便链接器快速定位符号。在 NDK 中通常是:llvm-ranlib
--strip=$strip \ # 剥离调试符号,用于从 .so 中删除调试符号、无用符号,使 .so 更小。在 NDK 中通常是:llvm-strip
# 这些工具一起构成了 Android NDK 的 交叉编译工具链(toolchain),让你能在 Linux/Windows 电脑上为 Android ARM64 编译 FFmpeg。
3、HarfBuzz配置命令
groovy
cd $harfbuzz && \ # 进入到harfbuzz主目录
meson setup $harfbuzz_build \ # 创建构建目录并生成 Ninja 构建文件,因为最终的编译和安装是由Ninja工具来完成的。
--cross-file=android-arm64.txt \ # 告诉 Meson 我们正在交叉编译,并使用该 cross file
--buildtype=release \ # 设置构建类型为 release,它会开启 O2 或 O3 优化(根据 Meson 默认),禁用调试符号(可以进一步用 strip 删除)
-Ddocs=disabled \ # 禁用 HarfBuzz 文档构建:不安装 HTML 文档、不安装 man 手册,节省构建时间
-Dtests=disabled \ # 禁用完整 test suite:不编译测试可执行程序、不安装测试数据,节省构建时间
-Dbenchmark=disabled \ # 禁用性能测试工具(hb-shape-benchmark 之类),体积减少(少一些可执行文件),大幅减少构建时间
-Dglib=disabled \ # 禁用 glib 支持
-Dfreetype=enabled # 启用 FreeType 集成
4、一些官方指南
meson使用官方指南:https://mesonbuild.com/Quick-guide.html
FreeType编译官方指南:https://freetype.org/freetype2/docs/ft2faq.html#builds
FFmpeg编译官方指南:https://trac.ffmpeg.org/wiki/CompilationGuide
其它一些细节
1、关于FreeType支持的构建工具
官网中关于编译FreeType的链接在Documentation菜单中的FreeType FAQ链接中,如下:

点击 FreeType FAQ 后就可以看到编译和配置相关的链接了,如下:

这个编译与配置,是非常的不够详细,看得人头痛,根本找不到自己需要的内容,还是不看了。
点击左侧的 Development 菜单,显示 Support Tools 有GNU Make、Meson和CMake,如下图:

如上图,对于Meson,翻译为中文如下:
目前,对 Meson 的支持仍处于实验阶段,仅适用于库本身,不包括示例程序。计划最终解决这个问题,使 Meson 成为一个一流的(首选的)构建系统。
对于 first-class 翻译为首选的不知道对不对,但最少可以理解为是官方支持的一级构建系统,所以这里我就使用了meson来编译,meson需要依赖python,而Ubuntu自带python,我使用的是Ubuntu Server 24.04系统,自带的python版本为 3.12.3,所以不需要安装Python了,直接安装meson和ninja即可:
groovy
sudo apt update && sudo apt install -y meson ninja-build
meson就像Java项目中的Gradle,只是一个构建工具,所以需要一个真正干活的人,这个人就是ninja。
2、libharfbuzz-subset.so
libharfbuzz-subset.so主要用于修改字体或生成字体子集,比如你知道你的应用只会用到某些固定的文字,如果你使用一个字体文件,这里面很多字是用不到的,你确切的知道你只会用某一个小范围的字,则可以使用libharfbuzz.so生成只包含特定数量文字的字体文件,这样字体文件的大小就可以非常小。而我的需求是,在视频上显示文字水印,而显示的文字没办法知道它具体只使用哪些文字,比如显示一个登录用户的名称,这个名称可能会是任意的名字,所以我就没办法使用libharfbuzz-subset.so来生成子集字体来使用,所以就没必要使用libharfbuzz-subset.so了,AI说可添加-Dsubset=disabled选项来禁止,但是我试了不行,然后AI又说是因为FreeType自动使用了HarfBuzz的11.x版本,这个版本不支持-Dsubset=disabled,而12.x的版本支持,大家可以试一下,我懒得试了,我手动删除就行了。
3、libpng16.so
编译FreeType它默认会启用png功能,会自动生成libpng16.so,16为png的版本号,这个库我们也是用不到的,所以主动加-Dpng=disabled来禁用它。
4、drawtext的使用
FFmpeg drawtext的使用官方教程:FFmpeg Filters Documentation
5、FFmpeg的编译与安装命令
groovy
make -j$(nproc)
-j表示并行编译,$(nproc) 为读取系统的CPU核数,如为4,则表示最多同时运行4个任务。
安装ffmpeg,即安装lib(so)和include(头文件)到prefix指定的路径下:
groovy
make install
6、为什么FFmpeg新版本要强制依赖HarfBuzz
以前旧的版本的ffmpeg只依赖FreeType,后来变成了必须依赖FreeType和HuffBuzz,那么问题来了,比如视频文字水印,就是一行水平显示的中文文字,需要什么排版吗?需要用得上HarfBuzz吗?如果不是必须的,那为什么不把HarfBuzz依赖弄成可选的?下面是来自AI的分析:
一行水平中文文字的特点
- 中文文字水印通常:
- 每个字独立显示
- 水平排列
- 不涉及连字、上下文替换、复杂脚本 shaping
- 也就是说,这种简单场景 FreeType 就够了 :
- 每个中文字渲染成 glyph bitmap
- 直接按固定字宽或字体自带 advance 渲染即可
- 不会有复杂的字形替换或 ligature 问题
- 结论:从技术上讲,这种场景 Harfbuzz 并非必须
那为什么 FFmpeg 8.0 强制依赖 HarfBuzz?
- FFmpeg 团队在设计上:
- 想让
drawtext成为 全面支持 OpenType 的文字渲染引擎 - 所有字符统一通过 HarfBuzz 处理 glyph shaping
- 优点:
- 中文、日文、阿拉伯文等复杂脚本都能正确显示
- 字间距和替代字形都准确
- 缺点:
- 对简单场景来说,多了不必要的依赖
- 编译时必须同时存在 FreeType + HarfBuzz
- 想让
- 设计哲学:
- 统一内部逻辑,减少分支代码
- 不用再区分"简单文字" vs "复杂文字"两套渲染流程
- 所以做成 必需依赖,而不是可选
7、路径配置
使用环境变量设置路径,就是为了复用路径,避免重复的路径每次用的时候都重复写,但是在编译FreeType和HarfBuzz时,需要创建一个交叉编译的配置文件,里面的路径没办法复用,所以如果路径不同,这个文件中的每个路径都要记得修改。
踩坑
1、设置了环境变量toolchain
设置:
groovy
export toolchain=/home/even/android-ndk-r27d/toolchains/llvm/prebuilt/linux-x86_64/bin
之后,运行FFmpeg的configure报错:
Unknown toolchain /home/even/android-ndk-r27d/toolchains/llvm/prebuilt/linux-x86_64/bin
交叉编译我都是通过给configure配置:
bash
./configure \
--cc=$CC \
--cxx=$CXX \
--ar=$AR \
--nm=$NM \
--ranlib=$RANLIB \
--strip=$STRIP \
--enable-cross-compile
我以为configure只会使用这里传入的参数,没想到它还会私自读取环境变量,比如读取小写的toolchain环境变量,所以导致报错,因为它需要的toolchain并不是用于设置我们的交叉工具链的路径,而是一个工具链的名称,而我们设置了一个路径,所以就报错了,而且它名称不是随便给的,有固定值:
| toolchain 名字 | 意义 |
|---|---|
| generic | 默认通用工具链 |
| msvc | Microsoft Visual C++ |
| valgrind-memcheck | 使用 valgrind |
其它任何值 → 直接报错 Unknown toolchain。
NDK交叉编译工具并不能说指定一个名字或一个路径就能完成,必须是每一个工具单独指定,比如:
bash
./configure \
--cc=$CC \
--cxx=$CXX \
--ar=$AR \
--nm=$NM \
--ranlib=$RANLIB \
--strip=$STRIP \
--enable-cross-compile
如果设置了小写toolchain环境变量,则把它清除即可:
bash
unset toolchain
configure 除了会读取toolchain环境变量,还会读取其它的一些环境变量,包括大写、小写,所以使用export设置环境变量时要小心,一不小心设置了一个configure会读取的变量就完了,因为你不设置它它会有一个正确的默认值没事,你一设置它就不正确了。
我看网上教程都是这么写的,使用export设置环境变量,再通过configure的类似--cc=$CC的方式读取环境变量来使用,说白了它设置环境变量只是为了复用一些路径,避免重复写:
groovy
export NDK=/home/even/android-ndk-r27d
export API=21
export TOOLCHAIN=$NDK/toolchains/llvm/prebuilt/linux-x86_64/bin
export TARGET=aarch64-linux-android
export NM=$TOOLCHAIN/llvm-nm
export RANLIB=$TOOLCHAIN/llvm-ranlib
export AR=$TOOLCHAIN/llvm-ar
export CC=$TOOLCHAIN/$TARGET$API-clang
export CXX=$TOOLCHAIN/$TARGET$API-clang++
export STRIP=$TOOLCHAIN/llvm-strip
为了预防某些环境变量被configure直接读取,或者别的程序会使用到,导致程序运行不正常,所以可以给设置的环境变量加一个特殊的前缀,比如:
groovy
export NDK_CC=$TOOLCHAIN/$TARGET$API-clang
export NDK_CXX=$TOOLCHAIN/$TARGET$API-clang++
或者把命令写到shell脚本,然后就可以使用变量来声明各种路径,并且能复用路径,就不需要使用export来设置环境变量了。
2、依赖找不到问题
编译FFmpeg,它依赖FreeType和HarfBuzz。编译FreeType,它依赖HarfBuzz。似乎Linux里面,查找依赖习惯通过pkg-config工具来查找,只用于查找第三方提供的依赖,这个工具默认会在几个系统路径下查找,估计一般的安装就会把生成的依赖安装到这些系统路径,但是我们编译so时,一般会指定一个安装路径,以方便我们获取so,而且也没必要安装到系统路径下面。所以我们需要通过设置PKG_CONFIG_PATH来设置依赖的路径,我们编译并安装后,除了会生成so,在so的目录下还有一个pkgconfig目录,这个目录下保存有.pc文件,pkg-config工具就是读取.pc文件才能知道某个依赖的so和对应头文件的位置,所以PKG_CONFIG_PATH这个路径需要设置到pkgconfig目录下,如果有多个依赖,每个依赖的pkgconfig目录都在不同位置,则可以给PKG_CONFIG_PATH设置多个路径,路径之间以冒号分隔。
所以总结就是,通过pkg-config工具和设置PKG_CONFIG_PATH环境变量来确保编译时能找到需要的依赖。
当我们调用configure时,如果设置了--cross-prefix就可能导致出现问题,交叉编译的工具,除了NDK之外,还有别的,别的交叉编译工具,指定一个前缀后,估计configure就能通过这个前缀来拼接出所有需要的交叉工具链的工具(路径+工具文件名),比如C和C++工具。但是NDK这个交叉工具比较特殊,它不按套路出牌,所以根据前缀拼出来的工具都是错误的,所以需要我们分别设置交叉编译的每一个工具,比如:
groovy
./configure \
--cc=$CC \
--cxx=$CXX \
--ar=$AR \
--nm=$NM \
--ranlib=$RANLIB \
--strip=$STRIP \
--cross-prefix=$TOOLCHAIN/bin/$TARGET- \
另外可能还有个知识点,一般的交叉编译工具都会提供一个自己的pkg-config,以方便配置为读取自己指定位置的依赖,所以设置了--cross-prefix之后,那么configure就会找这个前缀对应的pkg-config,即:$cross-prefix + pkg-config,但是NDK比较特殊,NDK一般使用CMake做为构建工作,所以依赖也由CMake管理设置,所以NDK并没有提供自己的pkg-config工具,但是configure并不知道啊,只要你设置了--cross-prefix,它就知道你是交叉编译了,就会按照这个前缀拼接出一个pkg-config工具并使用它,然而它并不存在,而且configure也不会因为拼接的pkg-config不存在而退而使用系统自带的pkg-config,所以在这种情况下,它没办法去查找依赖,但是它报的异常比较诡异,并不是提示找不到pkg-config工具,而是提示找不到freetype2,让人摸不着头脑。
解决方案就是可以明确指定configure使用哪一个pkg-config工具,如下:
groovy
./configure \
--cc=$CC \
--cxx=$CXX \
--ar=$AR \
--nm=$NM \
--ranlib=$RANLIB \
--strip=$STRIP \
--cross-prefix=$TOOLCHAIN/bin/$TARGET- \
--pkg-config=/usr/bin/pkg-config
换句话来说,对于NDK这种交叉编译工具,cross-prefix是没有用的,所以可以不使用它,但是如果你不使用它,configure就不知道是交叉编译,虽然你指定了cc、cxx等工具,但它可以是一个普通的编译啊,那么在编译前,它会尝试创建可运行文件进行测试,即可在Ubuntu系统上可执行的文件,然而使用NDK中的交叉编译工具是没办法创建出可运行文件的,所以编译就会失败,Linux版本的NDK就是用于在x86-64的CPU架构的Linux系统下,可以编译出在ARM架构上运行.so或.a,不能编译出可以直接在x86-64的Linux上运行的程序。后来查了资料,不指定--cross-prefix也可以,只要指定--enable-cross-compile也行,相当于告诉./configure我这是要交叉编译,所以它就不会创建可运行文件进行测试了。既然指定--cross-prefix没有意义,那就不要用它了,使用--enable-cross-compile即可,示例如下:
groovy
./configure \
--cc=$CC \
--cxx=$CXX \
--ar=$AR \
--nm=$NM \
--ranlib=$RANLIB \
--strip=$STRIP \
--enable-cross-compile \
--pkg-config=/usr/bin/pkg-config
由于我们没有设置cross-prefix了,所以它也不会自动去拼接出一个pkg-config工具了,那么它就会自动使用系统中默认的pkg-config工具,所以此时我们可以不设置--pkg-config=/usr/bin/pkg-config,虽然可以不设置了,但是我们还是设置一下吧,以便平时能明确知道它是使用pkg-config来寻找依赖的。
3、找不到hb_ft.h
C和Java不太一样,C喜欢使用宏来控制编译,比如FreeType不启用HarfBuzz,则它在编译FreeType的时候,就不会编译FreeType中调用HarfBuzz的代码,通过宏实现的,既然你不启用HarfBuzz,所以也没必要导出FreeType中关于调用HarfBuzz的相关头文件了。
在编译FreeType的时候,我们需要启用HarfBuzz,因为FFmpeg需要它。只需要启用HarfBuzz,FreeType的编译配置会自动下载HarfBuzz并编译它,它从https://github.com/harfbuzz/harfbuzz/releases/download/11.4.1/harfbuzz-11.4.1.tar.xz下载的,下载后保存在`FreeType`根目录下的`subprojects`目录中。
在编译FFmpeg的时候,会报一个错误,提示找不到hb_ft.h,这个文件,看名字是hb(HarfBuzz)调用ft(FreeType)的头文件,这说明在编译HarfBuzz的时候,也可以启用FreeType,这样HarfBuzz的内部可以直接调用FreeType的API,但是我们使用编译FreeType顺带自动编译出来的HarfBuzz,它是没有依赖FreeType的,所以它不会导出hb_ft.h,解决方案就是手动编译HarfBuzz并启用FreeType,这样编译出来的HarfBuzz的so就会带有hb_ft.h。
这里需要注意的事,如果编译HarfBuzz并启用FreeType,则它依赖FreeType,也是通过pkg-config来查找FreeType依赖的,所以在编译了FreeType后,需要设置PKG_CONFIG_PATH来指向FreeType的pkgconfig目录,以便能找到依赖,如果找不到,也会导致它自动从网上下载一个。在手动编译HarfBuzz后,需要删除掉编译FreeType时顺带编译出来的HarfBuzz,避免后面编译FFmpeg时依赖到这个不完整的HarfBuzz。
总结就是:这里我们需要FreeType依赖HarfBuzz,也需要HarfBuzz依赖FreeType,相互依赖。
另外有一个小知识点,编译FreeType并自动顺带编译的HarfBuzz的so非常的大,因为你启用HarfBuzz,FreeType会假设你需要使用到HarfBuzz的所有功能模块,所以编译的是完整功能的so,所以就很大。后来我发现,在编译FreeType时,只要加入了--buildtype=release,则顺带编译出来的HarfBuzz的so也是非常小了,只有1M多。
4、使用FFmpeg添加中文水印
添加英文水印是没问题的,添加中文水印时就有问题,比如我设置水印为 "你123456",水印显示为 "你1234",如果设置水印为 "你好123456",则水印显示为 "你好12",规律就是每增加一个中文,总的字符长度就会少两个。相同的源代码,编译为Windows版本或Linux版本使用,水印又是正常的,当时也困扰了我很久,我以为是我代码写的不对才有问题的,后来才发现是它本身的Bug。这是新版本才有的,不知道从哪个版本开始,FFmpeg开始依赖HarfBuzz,而且是强制依赖,从这个时候开始应该就有这个Bug了。所以解决方案也很简单,使用一个旧一点的不依赖HarfBuzz的版本即可。比如我验证过 FFmpeg 4.4.4版本是没问题的,4.x版本的最新版本是4.4.6,所以大家也可以用这个版本,它只依赖 FreeType,不需要依赖HarfBuzz,所以在编译FreeType的时候要禁用HarfBuzz。大家按照前面的教程自己下载新的版本重新编译一次即可。
Bug我也在FFmpeg官方提了,没想到第二天就有国内大神修复了,感兴趣的可以去看看:靠bug运行的程序之二------FFmpeg drawtext。大神已经把修复提交上去了,所以大家也可以等新版本,再有新的8.x版本应该就是已经修复Bug的版本了。目前官网上只有两个8.x的版本,如下:
