编译最新版本FFmpeg为so

编译带文字水印功能的FFmpeg

需求

我的需求是:在Android设备上使用Camera来采集摄像头画面,得到YUV数据,使用FFmpeg给YUV添加水印,然后再编码为H264,然后网络发送或保存MP4。前提条件是要先编译出FFmpeg,且需要启用drawtext功能,而这个功能又依赖于FreeTypeHarfBuzz

关于 HarfBuzzFreeType 的关系

  • FreeType 是一个字体渲染引擎(font rasterizer),它负责把字体文件(如 TTF、OTF)里的矢量字形(glyph)渲染成像素点(bitmap)或者轮廓(outline)数据。
  • HarfBuzz 是一个文字排版引擎(text shaping engine),它负责处理复杂文本的排版规则,例如中文、阿拉伯文、印地文等需要连字(ligature)、上下文形态(contextual forms)、方向控制(bidirectional)等特殊排版的情况。

简单来说:

  1. HarfBuzz → 计算字形位置和形态(shaping)
  2. 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版本是r27dLTSLong-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的代码并不托管在GithubGithub上面的只是一个镜像,官方代码是托管在: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.zipfreetype-2.14.1.tar.xzffmpeg-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

使用Xfptandroid-ndk-r27d-linux.zipfreetype-2.14.1.tar.xzffmpeg-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

编译FreeTypeHarfBuzz时,我们使用mesion+ninja的方式,所以安装mesonninja(这两个系统并没有自带):

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和头文件。

一般编译都是这个流程:

  1. 安装编译工具
  2. 配置编译选项
  3. 编译
  4. 安装

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.solibharfbuzz.solibharfbuzz-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.solibavfilter.solibavformat.solibavutil.solibswscale.so

在Android项目中使用FFmpeg

  1. 创建一个名为FFmpegVersionNative项目。

  2. 复制头文件:在cpp目录下创建一个ffmpeg目录,并在这个目录下创建一个include目录,然后把FFmpegFreeTypeHarfBuzz的头文件复制过来,如下:

  3. 复制so文件:在main目录下创建一个jniLibs目录,并在这个目录下创建一个arm64-v8a目录,然后把FFmpegFreeTypeHarfBuzzso文件复制过来,如下:

  4. 配置CMakeLists,下面加有注释的是我添加的,其它都是原来就有的:

    groovy 复制代码
    cmake_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)
  5. 在原有的navtive-lib.cpp代码中添加打印FFmpegFreeTypeHarfBuzz的版本号:

    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());
    }
    • 这里需要注意,导入关于FFmpegFreeTypeHarfBuzz的头文件时,要使用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多添加一个目录的原因,如下:

      groovy 复制代码
      include_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_HFT_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
  6. 运行项目,打印结果如下:

    groovy 复制代码
    FreeType 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一次执行

    groovy 复制代码
    export output=$HOME/ffmpeg-output
    export sources=$HOME/ffmpeg-sources

​ 两条命令会被依次执行,即便第一条命令执行失败,也会执行第二条命令

  • 多条命令有依赖关系

    groovy 复制代码
    meson compile && meson install

    如果第一条命令执行失败,则第二条命令就不会被执行。

  • 一条命令分多行来写

    groovy 复制代码
    meson 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

    也可以这样:

    groovy 复制代码
    cd $HOME # 进入主目录
    ls # 显示当前目录内容

    但是在\后不能写注释,也不能有任何字符,如下是错误的:

    groovy 复制代码
    meson 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 ToolsGNU MakeMesonCMake,如下图:

如上图,对于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自动使用了HarfBuzz11.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,后来变成了必须依赖FreeTypeHuffBuzz,那么问题来了,比如视频文字水印,就是一行水平显示的中文文字,需要什么排版吗?需要用得上HarfBuzz吗?如果不是必须的,那为什么不把HarfBuzz依赖弄成可选的?下面是来自AI的分析:

一行水平中文文字的特点

  • 中文文字水印通常:
    • 每个字独立显示
    • 水平排列
    • 不涉及连字、上下文替换、复杂脚本 shaping
  • 也就是说,这种简单场景 FreeType 就够了
    • 每个中文字渲染成 glyph bitmap
    • 直接按固定字宽或字体自带 advance 渲染即可
    • 不会有复杂的字形替换或 ligature 问题
  • 结论:从技术上讲,这种场景 Harfbuzz 并非必须

那为什么 FFmpeg 8.0 强制依赖 HarfBuzz?

  • FFmpeg 团队在设计上:
    • 想让 drawtext 成为 全面支持 OpenType 的文字渲染引擎
    • 所有字符统一通过 HarfBuzz 处理 glyph shaping
    • 优点:
      • 中文、日文、阿拉伯文等复杂脚本都能正确显示
      • 字间距和替代字形都准确
    • 缺点:
      • 对简单场景来说,多了不必要的依赖
      • 编译时必须同时存在 FreeType + HarfBuzz
  • 设计哲学:
    • 统一内部逻辑,减少分支代码
    • 不用再区分"简单文字" vs "复杂文字"两套渲染流程
    • 所以做成 必需依赖,而不是可选

7、路径配置

使用环境变量设置路径,就是为了复用路径,避免重复的路径每次用的时候都重复写,但是在编译FreeTypeHarfBuzz时,需要创建一个交叉编译的配置文件,里面的路径没办法复用,所以如果路径不同,这个文件中的每个路径都要记得修改。

踩坑

1、设置了环境变量toolchain

设置:

groovy 复制代码
export toolchain=/home/even/android-ndk-r27d/toolchains/llvm/prebuilt/linux-x86_64/bin

之后,运行FFmpegconfigure报错:

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,它依赖FreeTypeHarfBuzz。编译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就不知道是交叉编译,虽然你指定了cccxx等工具,但它可以是一个普通的编译啊,那么在编译前,它会尝试创建可运行文件进行测试,即可在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需要它。只需要启用HarfBuzzFreeType的编译配置会自动下载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来指向FreeTypepkgconfig目录,以便能找到依赖,如果找不到,也会导致它自动从网上下载一个。在手动编译HarfBuzz后,需要删除掉编译FreeType时顺带编译出来的HarfBuzz,避免后面编译FFmpeg时依赖到这个不完整的HarfBuzz

总结就是:这里我们需要FreeType依赖HarfBuzz,也需要HarfBuzz依赖FreeType,相互依赖。

另外有一个小知识点,编译FreeType并自动顺带编译的HarfBuzz的so非常的大,因为你启用HarfBuzzFreeType会假设你需要使用到HarfBuzz的所有功能模块,所以编译的是完整功能的so,所以就很大。后来我发现,在编译FreeType时,只要加入了--buildtype=release,则顺带编译出来的HarfBuzzso也是非常小了,只有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的版本,如下:

相关推荐
feiyangqingyun2 小时前
祖传独创/全网唯一/Qt结合ffmpeg实现读取ts文件节目流/动态切换多节目/实时切换不同轨道
qt·ffmpeg·节目流
i***586715 小时前
从MySQL5.7平滑升级到MySQL8.0的最佳实践分享
ffmpeg
Everbrilliant891 天前
FFmpeg解码视频数据ANativeWindow播放
ffmpeg·音视频·ffmpeg视频解码·anativewindow·threadsafequeue·解码线程·渲染线程
hjjdebug2 天前
ffmpeg 问答系列->demux 部分
ffmpeg·基本概念·流参数
chjqxxxx2 天前
php使用ffmpeg实现视频随机截图并转成图片
ffmpeg·php·音视频
陈陈陈建蕾4 天前
Mac使用FFmpeg进行屏幕录制,并使用VLC本地播放
ffmpeg·github
vivo互联网技术4 天前
Android动效探索:彻底弄清如何让你的视频更加酷炫
android·ffmpeg·跨平台·图形·mediaplayer·纹理·opengl es·坐标系
stereohomology5 天前
ffmpeg视频mp4到gif用大模型很方便
ffmpeg·音视频
f***45325 天前
从MySQL5.7平滑升级到MySQL8.0的最佳实践分享
ffmpeg