1. 环境准备:搭建NDK与FFmpeg编译环境

在开始编译FFmpeg之前,我们需要先准备好开发环境。这里我推荐使用Ubuntu系统进行编译,因为Linux环境下工具链更完整,遇到问题也更容易找到解决方案。不过如果你习惯使用MacOS,大部分命令也是通用的。

首先需要安装必要的依赖库,打开终端执行以下命令:

sudo apt-get update
sudo apt-get install -y build-essential git cmake make yasm nasm pkg-config

这些基础工具是编译过程中必不可少的。其中yasm和nasm是汇编编译器,FFmpeg为了提高编解码效率使用了大量汇编指令,缺少这两个工具会导致编译失败。

接下来需要下载NDK工具链。我建议使用NDK r20b版本,这个版本对FFmpeg 4.2.2的兼容性较好。可以从官网下载后解压到指定目录:

wget https://dl.google.com/android/repository/android-ndk-r20b-linux-x86_64.zip
unzip android-ndk-r20b-linux-x86_64.zip -d ~/android

设置NDK环境变量,方便后续脚本调用:

export NDK=~/android/android-ndk-r20b
export PATH=$NDK:$PATH

然后是下载FFmpeg源码。这里我们使用4.2.2版本:

wget https://ffmpeg.org/releases/ffmpeg-4.2.2.tar.bz2
tar -xvjf ffmpeg-4.2.2.tar.bz2
cd ffmpeg-4.2.2

1.1 配置编译工具链

NDK从r18开始就移除了gcc工具链,全面转向clang。所以在配置时需要特别注意工具链的选择。我们使用NDK自带的clang进行编译:

TOOLCHAIN=$NDK/toolchains/llvm/prebuilt/linux-x86_64

这个路径下包含了针对不同Android CPU架构的clang编译器。后续我们会根据目标平台选择对应的编译器。

2. 编写编译脚本:支持多平台架构

现在我们来编写编译脚本,支持armeabi-v7a和arm64-v8a两种架构。创建一个build_android.sh文件:

#!/bin/bash

echo ">>>>>>>>> 开始编译FFmpeg <<<<<<<<<"
echo ">>>>>>>>> 编译环境:NDK r20b + FFmpeg 4.2.2 <<<<<<<<<"

# 设置NDK路径
export NDK=/root/android/ndk/android-ndk-r20b
TOOLCHAIN=$NDK/toolchains/llvm/prebuilt/linux-x86_64

function build_android {
    echo "开始编译 $CPU"
    
    ./configure \
    --prefix=$PREFIX \
    --enable-neon \
    --enable-hwaccels \
    --enable-gpl \
    --enable-postproc \
    --enable-shared \
    --disable-static \
    --disable-debug \
    --enable-small \
    --enable-jni \
    --enable-mediacodec \
    --enable-decoder=h264_mediacodec \
    --disable-doc \
    --disable-ffplay \
    --disable-ffprobe \
    --disable-avdevice \
    --disable-symver \
    --cross-prefix=$CROSS_PREFIX \
    --target-os=android \
    --arch=$ARCH \
    --cpu=$CPU \
    --cc=$CC \
    --cxx=$CXX \
    --enable-cross-compile \
    --sysroot=$SYSROOT \
    --extra-cflags="-Os -fpic $OPTIMIZE_CFLAGS" \
    --extra-ldflags="$ADDI_LDFLAGS"

    make clean
    make -j8
    make install
    
    echo "编译成功 $CPU"
}

# 编译arm64-v8a
ARCH=arm64
CPU=armv8-a
API=21
CC=$TOOLCHAIN/bin/aarch64-linux-android$API-clang
CXX=$TOOLCHAIN/bin/aarch64-linux-android$API-clang++
SYSROOT=$NDK/toolchains/llvm/prebuilt/linux-x86_64/sysroot
CROSS_PREFIX=$TOOLCHAIN/bin/aarch64-linux-android-
PREFIX=$(pwd)/android/$CPU
OPTIMIZE_CFLAGS="-march=$CPU"

build_android

# 编译armeabi-v7a
ARCH=arm
CPU=armv7-a
API=16
CC=$TOOLCHAIN/bin/armv7a-linux-androideabi$API-clang
CXX=$TOOLCHAIN/bin/armv7a-linux-androideabi$API-clang++
SYSROOT=$NDK/toolchains/llvm/prebuilt/linux-x86_64/sysroot
CROSS_PREFIX=$TOOLCHAIN/bin/arm-linux-androideabi-
PREFIX=$(pwd)/android/$CPU
OPTIMIZE_CFLAGS="-mfloat-abi=softfp -mfpu=vfp -marm -march=$CPU"

build_android

给脚本添加执行权限:

chmod +x build_android.sh

2.1 关键配置参数解析

这个脚本中有几个关键配置需要特别注意:

  1. --enable-shared:生成动态库(.so文件),如果只需要静态库可以改为--enable-static
  2. --enable-neon:启用NEON指令集加速,对armeabi-v7a架构性能提升明显
  3. --enable-jni:启用JNI支持,方便Java层调用
  4. --enable-mediacodec:启用Android硬解码支持
  5. --disable-avdevice:禁用设备相关模块,在Android上通常不需要
  6. --extra-cflags:添加编译优化选项,-Os表示优化大小,-fpic生成位置无关代码

3. 常见编译问题与解决方案

在实际编译过程中,可能会遇到各种问题。这里我总结几个常见的错误和解决方法。

3.1 nasm/yasm not found

错误信息:

nasm/yasm not found or too old. Use --disable-x86asm for a crippled build.

解决方法:

sudo apt-get install yasm nasm

如果还是报错,可以临时禁用x86汇编:

./configure --disable-x86asm

3.2 链接器错误

有时会遇到类似下面的链接错误:

aarch64-linux-android-ld: cannot find -lc

这是因为没有正确设置sysroot路径。确保在configure参数中正确指定了:

--sysroot=$NDK/toolchains/llvm/prebuilt/linux-x86_64/sysroot

3.3 版本兼容性问题

如果遇到GLIBC版本不兼容的问题,比如:

libc.so.6: version `GLIBC_2.18' not found

可以考虑在Docker容器中使用统一的环境进行编译,避免主机环境差异带来的问题。

4. 集成到Android项目

编译完成后,会在android目录下生成对应架构的动态库。接下来我们看看如何集成到Android Studio项目中。

4.1 项目结构配置

将编译好的so库放入Android项目的libs目录:

app/
  libs/
    arm64-v8a/
      libavcodec.so
      libavformat.so
      ...
    armeabi-v7a/
      libavcodec.so
      libavformat.so
      ...

在app的build.gradle中配置:

android {
    sourceSets {
        main {
            jniLibs.srcDirs = ['libs']
        }
    }
}

4.2 CMake配置

创建CMakeLists.txt文件:

cmake_minimum_required(VERSION 3.4.1)

# 设置FFmpeg头文件路径
include_directories(${CMAKE_SOURCE_DIR}/include)

# 添加FFmpeg库
add_library(avcodec SHARED IMPORTED)
set_target_properties(avcodec PROPERTIES IMPORTED_LOCATION
    ${CMAKE_SOURCE_DIR}/libs/${CMAKE_ANDROID_ARCH_ABI}/libavcodec.so)

add_library(avformat SHARED IMPORTED)
set_target_properties(avformat PROPERTIES IMPORTED_LOCATION
    ${CMAKE_SOURCE_DIR}/libs/${CMAKE_ANDROID_ARCH_ABI}/libavformat.so)

# 添加你的本地库
add_library(native-lib SHARED native-lib.cpp)

# 链接库
target_link_libraries(native-lib
    avcodec
    avformat
    avutil
    swresample
    swscale
    log)

4.3 简单测试代码

创建一个简单的JNI函数测试FFmpeg版本:

#include <jni.h>
#include <string>

extern "C" {
#include <libavutil/avutil.h>
}

extern "C" JNIEXPORT jstring JNICALL
Java_com_example_ffmpegdemo_MainActivity_getFFmpegVersion(
        JNIEnv* env,
        jobject /* this */) {
    return env->NewStringUTF(av_version_info());
}

在Java层调用:

public class MainActivity extends AppCompatActivity {
    static {
        System.loadLibrary("native-lib");
    }
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        TextView tv = findViewById(R.id.sample_text);
        tv.setText(getFFmpegVersion());
    }
    
    public native String getFFmpegVersion();
}

如果一切正常,运行应用后会显示FFmpeg的版本信息,表明集成成功。

5. 进阶配置与优化

5.1 启用硬件加速

为了充分利用Android设备的硬件编解码能力,可以在编译时启用mediacodec:

--enable-mediacodec \
--enable-decoder=h264_mediacodec \
--enable-decoder=hevc_mediacodec \
--enable-decoder=mpeg4_mediacodec \
--enable-decoder=vp8_mediacodec \
--enable-decoder=vp9_mediacodec

5.2 裁剪FFmpeg模块

FFmpeg功能强大但体积较大,可以通过禁用不需要的模块来减小库文件大小:

--disable-avdevice \
--disable-postproc \
--disable-swresample \
--disable-swscale \
--disable-avfilter \
--disable-everything \
--enable-decoder=h264 \
--enable-decoder=aac \
--enable-demuxer=mov \
--enable-demuxer=mpegts \
--enable-protocol=file

5.3 性能优化选项

在extra-cflags中添加优化参数:

--extra-cflags="-O3 -fstrict-aliasing -fPIC -DANDROID -D__ANDROID__ -D__ANDROID_API__=$API"

其中:

  • -O3:最高级别优化
  • -fstrict-aliasing:启用严格别名规则
  • -fPIC:生成位置无关代码
  • -DANDROID:定义Android平台宏

6. 实际项目中的经验分享

在真实项目中集成FFmpeg时,有几个坑需要特别注意:

  1. ABI兼容性:现在Google Play要求应用必须支持64位架构,所以至少要编译arm64-v8a版本。如果还要兼容老设备,可以同时提供armeabi-v7a版本。

  2. 动态库加载顺序:FFmpeg各个库之间有依赖关系,加载顺序很重要。正确的顺序是:

    System.loadLibrary("avutil");
    System.loadLibrary("swresample");
    System.loadLibrary("avcodec");
    System.loadLibrary("avformat");
    System.loadLibrary("swscale");
    System.loadLibrary("avfilter");
    
  3. 版本管理:建议将编译好的so库和头文件放入版本控制系统,方便团队共享。同时记录编译时的配置参数,便于后续维护。

  4. 调试技巧:遇到native crash时,可以使用addr2line工具定位问题:

    $NDK/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android-addr2line -e libnative-lib.so <address>
    
  5. 内存管理:FFmpeg有自己的内存管理机制,使用av_malloc/av_free等函数分配释放内存。如果与Java层交互,要特别注意内存泄漏问题。

Logo

火山引擎视频云技术社区,是面向 AI 音视频开发者的技术交流平台。这里汇聚源自抖音、豆包等亿级 DAU 产品的 RTC、直播、点播、AI 媒体处理、音视频互动技术,提供接入指南、最佳实践、性能调优、场景案例、Demo 代码、开源项目、白皮书和 API 文档。社区汇聚官方工程师与一线开发者,为 AI 视频通话、数字人、AI 视频处理等应用的开发与落地提供技术支持。

更多推荐