Android FFmpeg 编译、集成与实战详解 – wiki基地


Android FFmpeg 编译、集成与实战详解

在移动互联网时代,多媒体内容消费已成为用户日常生活中不可或缺的一部分。从短视频、直播到在线教育,音视频技术的应用无处不在。FFmpeg 作为一套领先的开源音视频处理框架,因其强大的功能、广泛的格式支持和高度的灵活性,成为了众多音视频应用开发者的首选。然而,将 FFmpeg 移植到 Android 平台并非一件轻而易举的事情,它涉及到复杂的交叉编译、JNI 集成以及性能优化等多个环节。

本文将深入探讨 Android 平台下 FFmpeg 的编译、集成及实战应用,旨在为开发者提供一套全面而详尽的指南。

第一章:FFmpeg 核心概念与 Android 平台编译概述

1.1 FFmpeg 简介及其在 Android 的价值

FFmpeg 是一套能够记录、转换数字音频、视频,并将其流化处理的开源计算机程序。它包含了众多音视频处理库,如 libavcodec(编解码库)、libavformat(格式处理库)、libavutil(工具库)、libswscale(图像缩放与像素格式转换库)、libswresample(音频重采样库)以及 libavfilter(音视频滤镜库)等。凭借这些库,FFmpeg 能够实现几乎所有的音视频处理任务,包括:

  • 视频播放器: 解码各种格式的视频流,并通过 OpenGL ES 或 SurfaceTexture 进行渲染。
  • 音频播放器: 解码音频流,并通过 AudioTrack 进行播放。
  • 转码器: 将一种音视频格式转换为另一种,如 MP4 转 FLV。
  • 录制器: 捕获摄像头或麦克风数据并编码保存。
  • 流媒体客户端/服务器: 支持 RTSP、RTMP、HLS 等协议进行推拉流。
  • 滤镜处理: 对音视频进行裁剪、缩放、水印、美颜等处理。
  • 截帧与生成缩略图: 从视频中提取指定帧并保存为图片。

在 Android 平台上集成 FFmpeg,可以极大地增强应用的音视频处理能力,实现自定义播放器、视频编辑、直播推流等高级功能,突破系统 MediaCodec 的限制,提供更灵活、更强大的解决方案。

1.2 交叉编译的核心挑战

FFmpeg 本身是 C/C++ 代码编写的,要在 Android 设备上运行,就需要将其编译成适用于 Android CPU 架构(如 ARMv7-A, ARM64, x86, x86_64)的原生库(.so.a 文件)。这个过程称为交叉编译。其主要挑战包括:

  1. 环境配置: 需要配置 Android NDK (Native Development Kit),提供交叉编译工具链(编译器、链接器等)。
  2. 配置脚本: FFmpeg 的 configure 脚本需要识别 Android 目标平台,并使用正确的交叉编译参数。
  3. 依赖管理: FFmpeg 可能依赖一些第三方库(如 x264, fdk-aac, openssl 等),这些库也需要进行交叉编译。
  4. ABI 兼容性: Android 设备有多种 CPU 架构(ABI),需要为每种架构分别编译。
  5. 库裁剪: FFmpeg 功能强大但体积庞大,通常需要根据实际需求裁剪不必要的模块,以减小最终库的体积。
  6. JNI 接口: 编译好的原生库需要通过 Java Native Interface (JNI) 与 Java 层进行通信。

第二章:FFmpeg 在 Android 平台的编译实践

2.1 准备编译环境

在开始编译之前,确保您的开发环境已具备以下条件:

  1. 操作系统: 推荐使用 Linux (Ubuntu/Debian) 或 macOS。Windows 下需要额外配置 Cygwin 或 MinGW,或者使用 WSL (Windows Subsystem for Linux)。
  2. Android SDK & NDK:
    • 安装 Android Studio。
    • 通过 Android Studio SDK Manager 下载 Android SDK。
    • 同样在 SDK Manager 中下载 Android NDK。建议使用较新版本的 NDK (例如 r21b 或更高),它们提供了统一的工具链,简化了配置。
    • 确保 ANDROID_SDK_ROOTANDROID_NDK_HOME 环境变量已正确设置,或者在编译脚本中指定其路径。
  3. 其他工具: git (用于下载 FFmpeg 源码)、makeautoconfautomakelibtoolpkg-config 等。在 Linux 上,可以通过包管理器安装:
    bash
    sudo apt-get update
    sudo apt-get install git build-essential automake autoconf libtool pkg-config

2.2 获取 FFmpeg 源码

从 FFmpeg 官方仓库克隆源码。建议使用稳定版本的分支或标签,以避免 master 分支可能存在的兼容性问题。
“`bash
git clone https://git.ffmpeg.org/ffmpeg.git ffmpeg
cd ffmpeg

切换到稳定版本,例如 n4.4

git checkout n4.4

“`

2.3 交叉编译脚本编写

这是最核心的部分。我们将编写一个 shell 脚本来自动化编译过程。该脚本需要遍历不同的 Android ABI,为每个 ABI 配置并编译 FFmpeg。

关键编译参数解析:

  • --prefix: 指定编译结果(库文件和头文件)的安装路径。
  • --enable-cross-compile: 启用交叉编译。
  • --target-os=android: 指定目标操作系统为 Android。
  • --arch, --cpu: 指定目标架构和 CPU 类型(如 arm, armv7-a, aarch64 等)。
  • --sysroot: 指定 Android NDK 的系统根目录,包含了目标平台的 C 库、头文件等。
  • --cc, --cxx, --ld, --ar, --strip: 指定交叉编译工具链中的编译器、链接器、打包器等。
  • --disable-everything: 默认禁用所有组件。这是推荐的做法,然后根据需要启用特定组件,以减小最终库的体积。
  • --enable-shared / --enable-static:
    • --enable-shared: 编译为动态链接库(.so),运行时需要加载。
    • --enable-static: 编译为静态链接库(.a),在链接时直接嵌入到最终的 .so 文件中。通常推荐静态编译 FFmpeg 自身的库,然后将所有这些静态库链接到你自己的 JNI 动态库中,这样可以避免 FFmpeg 库之间的依赖问题,并简化部署。
  • --enable-gpl, --enable-nonfree: 如果你集成了 GPL 或非免费许可的组件(如 x264, fdk-aac),则需要启用这些选项。这会影响你的应用许可证。
  • --enable-decoder=..., --enable-encoder=..., --enable-parser=..., --enable-demuxer=..., --enable-muxer=...: 根据你的需求精确启用编解码器、解析器、解复用器、复用器等。
  • --enable-filter=...: 启用特定的滤镜。
  • --extra-cflags, --extra-ldflags: 额外的 C 编译器和链接器标志。例如,为了支持 Android log 库,可能需要添加 -llog
  • --disable-programs, --disable-doc: 禁用编译 FFmpeg 命令行工具和文档,进一步减小体积。

编译脚本示例 (build_ffmpeg.sh):

“`bash

!/bin/bash

配置变量

NDK_ROOT=”/Users/your_user/Library/Android/sdk/ndk/21.4.7075529″ # 替换为你的 NDK 路径
API=21 # Android API 级别,建议21或更高
BUILD_DIR=$(pwd)/android_build # 编译输出目录
FFMPEG_SOURCE_DIR=$(pwd) # FFmpeg 源码目录

清理旧的编译结果

rm -rf $BUILD_DIR
mkdir -p $BUILD_DIR

定义编译目标 ABI 列表

ABIS=”armeabi-v7a arm64-v8a x86 x86_64″

function build_one_abi {
ABI=$1
echo “==================== Start building for $ABI ====================”

# 根据 ABI 配置工具链和目录
case $ABI in
    armeabi-v7a)
        TOOLCHAIN_NAME="arm-linux-androideabi"
        HOST="arm-linux-androideabi"
        CROSS_PREFIX="${TOOLCHAIN_NAME}-"
        EXTRA_CFLAGS="-march=armv7-a -mfloat-abi=softfp -mfpu=neon -D__ANDROID_API__=$API"
        EXTRA_LDFLAGS=""
        ;;
    arm64-v8a)
        TOOLCHAIN_NAME="aarch64-linux-android"
        HOST="aarch64-linux-android"
        CROSS_PREFIX="${TOOLCHAIN_NAME}-"
        EXTRA_CFLAGS="-D__ANDROID_API__=$API"
        EXTRA_LDFLAGS=""
        ;;
    x86)
        TOOLCHAIN_NAME="i686-linux-android"
        HOST="i686-linux-android"
        CROSS_PREFIX="${TOOLCHAIN_NAME}-"
        EXTRA_CFLAGS="-D__ANDROID_API__=$API"
        EXTRA_LDFLAGS=""
        ;;
    x86_64)
        TOOLCHAIN_NAME="x86_64-linux-android"
        HOST="x86_64-linux-android"
        CROSS_PREFIX="${TOOLCHAIN_NAME}-"
        EXTRA_CFLAGS="-D__ANDROID_API__=$API"
        EXTRA_LDFLAGS=""
        ;;
    *)
        echo "Unknown ABI: $ABI"
        exit 1
        ;;
esac

TOOLCHAIN_PATH="${NDK_ROOT}/toolchains/llvm/prebuilt/darwin-x86_64/bin" # 替换为你的主机操作系统,例如 linux-x86_64 或 windows-x86_64
SYSROOT="${NDK_ROOT}/toolchains/llvm/prebuilt/darwin-x86_64/sysroot"

export PATH="${TOOLCHAIN_PATH}:$PATH"

# 编译选项
FFMPEG_CONFIGURE_FLAGS=" \
    --prefix=${BUILD_DIR}/${ABI} \
    --enable-cross-compile \
    --target-os=android \
    --arch=${HOST%%-*} \
    --cpu=${HOST%%-*} \
    --enable-static \
    --disable-shared \
    --disable-doc \
    --disable-programs \
    --disable-ffmpeg \
    --disable-ffplay \
    --disable-ffprobe \
    --disable-postproc \
    --disable-debug \
    --disable-symver \
    --enable-gpl \
    --enable-nonfree \
    --enable-jni \
    --pkg-config-flags=--static \
    --sysroot=${SYSROOT} \
    --cc=${TOOLCHAIN_PATH}/${CROSS_PREFIX}clang \
    --cxx=${TOOLCHAIN_PATH}/${CROSS_PREFIX}clang++ \
    --ld=${TOOLCHAIN_PATH}/${CROSS_PREFIX}ld \
    --ar=${TOOLCHAIN_PATH}/${CROSS_PREFIX}ar \
    --as=${TOOLCHAIN_PATH}/${CROSS_PREFIX}as \
    --strip=${TOOLCHAIN_PATH}/${CROSS_PREFIX}strip \
    --nm=${TOOLCHAIN_PATH}/${CROSS_PREFIX}nm \
    --ranlib=${TOOLCHAIN_PATH}/${CROSS_PREFIX}ranlib \
    --extra-cflags="-fPIC $EXTRA_CFLAGS" \
    --extra-ldflags="$EXTRA_LDFLAGS -L${SYSROOT}/usr/lib/${TOOLCHAIN_NAME}/${API} -landroid -llog -lz" \
    \
    --enable-decoder=h264,hevc,mpeg4,vp8,vp9,aac,mp3 \
    --enable-encoder=h264_mediacodec,aac_mediacodec \
    --enable-parser=h264,hevc \
    --enable-demuxer=mp4,flv,hls,rtsp,rtmp \
    --enable-muxer=mp4 \
    --enable-protocol=file,http,https,rtmp,rtsp \
    --enable-filter=scale,crop,setpts \
    --enable-avdevice \
    --enable-swresample \
    --enable-swscale \
    --enable-avfilter \
    --enable-small \
"

# 如果需要外部库,如 x264, fdk-aac,需要先编译这些库,并在这里添加 --enable-libx264 等选项
# 并在 extra-cflags/ldflags 中指定其头文件和库路径
# 例如: --enable-libx264 --enable-libfdk-aac

# 执行配置
${FFMPEG_SOURCE_DIR}/configure $FFMPEG_CONFIGURE_FLAGS || { echo "Configure failed for $ABI"; exit 1; }

# 执行编译
make clean
make -j8 || { echo "Make failed for $ABI"; exit 1; }
make install || { echo "Make install failed for $ABI"; exit 1; }

echo "==================== Finished building for $ABI ===================="

}

遍历所有 ABI 进行编译

for abi in $ABIS; do
build_one_abi $abi
done

echo “==================== All FFmpeg ABIs built successfully! ====================”
``
**重要提示:**
* 请务必根据您的实际环境修改
NDK_ROOTTOOLCHAIN_PATHTOOLCHAIN_PATH中的darwin-x86_64需要替换为您的操作系统架构,例如linux-x86_64windows-x86_64
*
–cpu=${HOST%%-*}这部分在 NDK r19+ 统一工具链下通常会由 NDK 自动处理,旧版本 NDK 可能需要更精确指定。
*
–enable-decoder=…等选项请根据你的实际需求进行精简,过多的启用会导致库文件体积膨胀。
* 如果需要第三方库(如
x264fdk-aac),你需要额外下载并交叉编译这些库,然后将它们的头文件和库路径添加到FFMPEG_CONFIGURE_FLAGS中的–extra-cflags–extra-ldflags,并启用对应的–enable-libxxx选项。这部分内容通常比 FFmpeg 自身的编译更复杂,可以参考网上关于 FFmpeg 编译x264` 等的教程。

2.4 执行编译

保存上述脚本为 build_ffmpeg.sh,赋予执行权限,然后运行:
bash
chmod +x build_ffmpeg.sh
./build_ffmpeg.sh

这个过程可能耗时较长,取决于你的机器性能和启用的组件数量。编译成功后,你会在 android_build 目录下看到每个 ABI 对应的文件夹,其中包含 lib (静态库 .a) 和 include (头文件) 目录。

第三章:FFmpeg 在 Android 项目中的集成

编译好的 FFmpeg 库需要集成到 Android 项目中,并通过 JNI 与 Java/Kotlin 代码进行交互。

3.1 JNI 接口设计

JNI (Java Native Interface) 是连接 Java 代码和 C/C++ 代码的桥梁。你需要编写 C/C++ 代码来调用 FFmpeg 库,并暴露 Java 方法供 Android 应用层调用。

JNI 核心步骤:

  1. 定义 Native 方法: 在 Java/Kotlin 类中声明 native 方法。
    “`java
    // FFMpegJni.java
    public class FFMpegJni {
    static {
    System.loadLibrary(“ffmpeg_native”); // 加载你自己的 JNI 库
    }

    public native String getFFmpegVersion();
    public native int initPlayer(String dataSource, Surface surface);
    public native void play();
    public native void stop();
    // ... 其他方法
    

    }
    2. **生成 JNI 头文件(可选,但推荐理解):** 编译 Java 代码后,可以使用 `javah` (JDK 8 及以前) 或手动遵循 JNI 规范命名 C/C++ 函数。
    `Java_包名_类名_方法名`。
    例如:`Java_com_example_ffmpegdemo_FFMpegJni_getFFmpegVersion`
    3. **编写 C/C++ JNI 代码:** 创建一个 `ffmpeg_jni.cpp` 文件,实现 `Java_...` 函数。
    cpp
    // ffmpeg_jni.cpp

    include

    include

    include

    include // 用于 Surface 渲染

    // FFmpeg 头文件
    extern “C” {

    include “libavcodec/avcodec.h”

    include “libavformat/avformat.h”

    include “libavutil/avutil.h”

    include “libswscale/swscale.h”

    include “libswresample/swresample.h”

    }

    define TAG “FFmpegNative”

    define LOGD(…) android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS)

    define LOGE(…) android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS)

    // 全局变量或结构体,用于保存 FFmpeg 上下文
    // 实际项目中需要更完善的封装和线程安全处理
    AVFormatContext g_formatCtx = nullptr;
    AVCodecContext
    g_videoCodecCtx = nullptr;
    AVCodecContext g_audioCodecCtx = nullptr;
    ANativeWindow
    g_nativeWindow = nullptr;
    int g_videoStreamIdx = -1;
    int g_audioStreamIdx = -1;
    SwsContext g_swsContext = nullptr; // 用于视频像素格式转换
    SwrContext
    g_swrContext = nullptr; // 用于音频重采样

    extern “C” JNIEXPORT jstring JNICALL
    Java_com_example_ffmpegdemo_FFMpegJni_getFFmpegVersion(JNIEnv* env, jclass clazz) {
    return env->NewStringUTF(av_version_info());
    }

    extern “C” JNIEXPORT jint JNICALL
    Java_com_example_ffmpegdemo_FFMpegJni_initPlayer(JNIEnv env, jclass clazz, jstring jdataSource, jobject jsurface) {
    const char
    dataSource = env->GetStringUTFChars(jdataSource, 0);
    LOGD(“Initializing player with data source: %s”, dataSource);

    // 1. 注册所有组件 (FFmpeg 4.0 以后大部分无需手动注册)
    // av_register_all(); // Deprecated
    avformat_network_init(); // 注册网络协议
    
    // 2. 打开输入文件或流
    g_formatCtx = avformat_alloc_context();
    if (avformat_open_input(&g_formatCtx, dataSource, nullptr, nullptr) != 0) {
        LOGE("Failed to open input stream: %s", dataSource);
        env->ReleaseStringUTFChars(jdataSource, dataSource);
        return -1;
    }
    
    // 3. 查找流信息
    if (avformat_find_stream_info(g_formatCtx, nullptr) < 0) {
        LOGE("Failed to find stream information.");
        avformat_close_input(&g_formatCtx);
        env->ReleaseStringUTFChars(jdataSource, dataSource);
        return -1;
    }
    
    // 4. 查找视频和音频流
    for (int i = 0; i < g_formatCtx->nb_streams; ++i) {
        if (g_formatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO && g_videoStreamIdx == -1) {
            g_videoStreamIdx = i;
        } else if (g_formatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO && g_audioStreamIdx == -1) {
            g_audioStreamIdx = i;
        }
    }
    
    if (g_videoStreamIdx == -1 && g_audioStreamIdx == -1) {
        LOGE("No video or audio stream found.");
        avformat_close_input(&g_formatCtx);
        env->ReleaseStringUTFChars(jdataSource, dataSource);
        return -1;
    }
    
    // 5. 初始化视频解码器
    if (g_videoStreamIdx != -1) {
        AVCodecParameters *codecPar = g_formatCtx->streams[g_videoStreamIdx]->codecpar;
        AVCodec *decoder = avcodec_find_decoder(codecPar->codec_id);
        if (!decoder) {
            LOGE("Failed to find video decoder for id: %d", codecPar->codec_id);
            // 错误处理...
        }
        g_videoCodecCtx = avcodec_alloc_context3(decoder);
        avcodec_parameters_to_context(g_videoCodecCtx, codecPar);
        if (avcodec_open2(g_videoCodecCtx, decoder, nullptr) < 0) {
            LOGE("Failed to open video decoder.");
            // 错误处理...
        }
    
        // 获取 Surface
        g_nativeWindow = ANativeWindow_fromSurface(env, jsurface);
        if (!g_nativeWindow) {
            LOGE("Failed to get ANativeWindow from Surface.");
            // 错误处理...
        }
        // 设置缓冲区大小和格式
        ANativeWindow_setBuffersGeometry(g_nativeWindow,
                                         g_videoCodecCtx->width,
                                         g_videoCodecCtx->height,
                                         AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM); // 示例,根据实际渲染方式调整
        g_swsContext = sws_getContext(g_videoCodecCtx->width, g_videoCodecCtx->height, g_videoCodecCtx->pix_fmt,
                                      g_videoCodecCtx->width, g_videoCodecCtx->height, AV_PIX_FMT_RGBA, // 目标格式
                                      SWS_BICUBIC, nullptr, nullptr, nullptr);
    }
    
    // 6. 初始化音频解码器 (类似视频解码器,省略部分代码)
    if (g_audioStreamIdx != -1) {
        // ... 查找解码器,打开解码器,初始化swrContext
    }
    
    env->ReleaseStringUTFChars(jdataSource, dataSource);
    LOGD("Player initialized successfully.");
    return 0;
    

    }

    extern “C” JNIEXPORT void JNICALL
    Java_com_example_ffmpegdemo_FFMpegJni_play(JNIEnv env, jclass clazz) {
    LOGD(“Starting playback…”);
    AVPacket
    packet = av_packet_alloc();
    AVFrame *frame = av_frame_alloc();
    ANativeWindow_Buffer windowBuffer;

    while (av_read_frame(g_formatCtx, packet) >= 0) {
        if (packet->stream_index == g_videoStreamIdx) {
            if (avcodec_send_packet(g_videoCodecCtx, packet) == 0) {
                while (avcodec_receive_frame(g_videoCodecCtx, frame) == 0) {
                    // 视频帧处理:转换为 ANativeWindow 可接受的格式并渲染
                    ANativeWindow_lock(g_nativeWindow, &windowBuffer, nullptr);
                    uint8_t *dst_data[4];
                    int dst_linesize[4];
                    av_image_alloc(dst_data, dst_linesize,
                                   g_videoCodecCtx->width, g_videoCodecCtx->height,
                                   AV_PIX_FMT_RGBA, 1); // 目标格式
    
                    sws_scale(g_swsContext, (const uint8_t *const *)frame->data, frame->linesize,
                              0, frame->height, dst_data, dst_linesize);
    
                    // 将 dst_data 复制到 windowBuffer.bits
                    // 注意:这里需要根据实际的像素格式和步幅进行复制
                    // 简单示例(RGBA):
                    for (int h = 0; h < g_videoCodecCtx->height; h++) {
                        memcpy(static_cast<uint8_t*>(windowBuffer.bits) + h * windowBuffer.stride * 4,
                               dst_data[0] + h * dst_linesize[0],
                               g_videoCodecCtx->width * 4);
                    }
                    ANativeWindow_unlockAndPost(g_nativeWindow);
                    av_freep(&dst_data[0]); // 释放临时分配的内存
                }
            }
        } else if (packet->stream_index == g_audioStreamIdx) {
            // 音频帧处理 (解码、重采样,然后通过 AudioTrack 播放,此处省略具体实现)
            if (avcodec_send_packet(g_audioCodecCtx, packet) == 0) {
                while (avcodec_receive_frame(g_audioCodecCtx, frame) == 0) {
                    // TODO: 音频重采样并播放到 AudioTrack
                }
            }
        }
        av_packet_unref(packet); // 释放 packet 引用
    }
    LOGD("Playback finished.");
    // 释放资源
    av_packet_free(&packet);
    av_frame_free(&frame);
    // TODO: 调用 stop() 清理所有资源
    

    }

    extern “C” JNIEXPORT void JNICALL
    Java_com_example_ffmpegdemo_FFMpegJni_stop(JNIEnv* env, jclass clazz) {
    LOGD(“Stopping player and cleaning up resources.”);
    if (g_swsContext) { sws_free_context(g_swsContext); g_swsContext = nullptr; }
    if (g_swrContext) { swr_free(g_swrContext); g_swrContext = nullptr; }
    if (g_videoCodecCtx) { avcodec_close(g_videoCodecCtx); avcodec_free_context(&g_videoCodecCtx); }
    if (g_audioCodecCtx) { avcodec_close(g_audioCodecCtx); avcodec_free_context(&g_audioCodecCtx); }
    if (g_formatCtx) { avformat_close_input(&g_formatCtx); avformat_free_context(g_formatCtx); g_formatCtx = nullptr; }
    if (g_nativeWindow) { ANativeWindow_release(g_nativeWindow); g_nativeWindow = nullptr; }
    avformat_network_deinit();
    LOGD(“Resources cleaned up.”);
    }

    // TODO: 实现其他 JNI 方法
    “`
    这个示例是一个简化版,实际的播放器需要更复杂的线程管理、音视频同步、缓冲区管理、错误处理和用户交互逻辑。

3.2 CMake 构建配置

Android Studio 推荐使用 CMake 管理原生库构建。在 app 模块的 build.gradle 中配置 externalNativeBuild,并编写 CMakeLists.txt

将编译好的 FFmpeg 库引入项目:

  1. 复制 FFmpeg 编译结果:android_build 目录下的所有 ABI 文件夹(armeabi-v7a, arm64-v8a 等)复制到你的 Android 项目 app/src/main/cpp/ffmpeg_libs 目录下。
    每个 ABI 目录内应包含 includelib 两个子目录。
    app/src/main/cpp/
    ├── CMakeLists.txt
    ├── ffmpeg_jni.cpp
    └── ffmpeg_libs/
    ├── armeabi-v7a/
    │ ├── include/
    │ └── lib/ (包含 libavcodec.a, libavformat.a 等)
    ├── arm64-v8a/
    │ ├── include/
    │ └── lib/
    └── ...

  2. CMakeLists.txt 配置:

    “`cmake

    设置 CMake 最低版本

    cmake_minimum_required(VERSION 3.10.2)

    项目名称

    project(“ffmpeg_native_project”)

    定义各个 FFmpeg 库的名称

    set(FFMPEG_LIBS
    avcodec
    avformat
    avutil
    swresample
    swscale
    avfilter
    )

    遍历 FFmpeg_LIBS,为每个 ABI 配置对应的静态库

    foreach(LIB_NAME IN LISTS FFMPEG_LIBS)
    # 定义 FFmpeg 库的导入目标
    add_library(ffmpeg-${LIB_NAME} STATIC IMPORTED)

    # 设置每个 ABI 的库文件路径
    set_target_properties(ffmpeg-${LIB_NAME} PROPERTIES
        IMPORTED_LOCATION                 ${CMAKE_SOURCE_DIR}/ffmpeg_libs/${ANDROID_ABI}/lib/lib${LIB_NAME}.a
        IMPORTED_LINK_INTERFACE_LANGUAGES "C"
    )
    

    endforeach()

    查找 Android 提供的 log 库

    find_library(log-lib log)

    查找 Android 提供的 native window 库

    find_library(android-lib android)

    查找 Android 提供的 zlib 库 (FFmpeg 可能依赖)

    find_library(z-lib z)

    添加你的 JNI 源文件

    add_library(ffmpeg_native SHARED ffmpeg_jni.cpp)

    指定头文件路径

    包含 NDK 平台头文件

    target_include_directories(ffmpeg_native PRIVATE
    ${CMAKE_SOURCE_DIR} # 你的 JNI C++ 源文件可能需要的头文件
    ${CMAKE_SOURCE_DIR}/ffmpeg_libs/${ANDROID_ABI}/include # FFmpeg 头文件
    ${NDK_ROOT}/sysroot/usr/include # NDK 公共头文件
    ${NDK_ROOT}/sysroot/usr/include/$(TOOLCHAIN_NAME) # 针对特定工具链的头文件
    )

    链接你的原生库和 FFmpeg 静态库以及 Android 系统库

    target_link_libraries(ffmpeg_native
    # 链接 FFmpeg 库
    ${CMAKE_SOURCE_DIR}/ffmpeg_libs/${ANDROID_ABI}/lib/libavcodec.a
    ${CMAKE_SOURCE_DIR}/ffmpeg_libs/${ANDROID_ABI}/lib/libavformat.a
    ${CMAKE_SOURCE_DIR}/ffmpeg_libs/${ANDROID_ABI}/lib/libavutil.a
    ${CMAKE_SOURCE_DIR}/ffmpeg_libs/${ANDROID_ABI}/lib/libswresample.a
    ${CMAKE_SOURCE_DIR}/ffmpeg_libs/${ANDROID_ABI}/lib/libswscale.a
    ${CMAKE_SOURCE_DIR}/ffmpeg_libs/${ANDROID_ABI}/lib/libavfilter.a
    # 链接其他 FFmpeg 依赖的静态库,如 x264, fdk-aac 等
    # ${CMAKE_SOURCE_DIR}/ffmpeg_libs/${ANDROID_ABI}/lib/libx264.a

    # 链接 Android 系统库
    ${log-lib}
    ${android-lib}
    ${z-lib}
    m # 数学库
    # 如果启用了网络功能,需要链接 c
    # c # C标准库
    # dl # 动态链接库
    

    )

    注意:在较新版本的 CMake 和 Android NDK 中,

    可以直接使用 find_package(FFmpeg COMPONENTS avcodec avformat …) 来查找 FFmpeg 库,

    但前提是 FFmpeg 被正确地安装到了系统路径或你的 CMake FIND_PATH 中。

    对于交叉编译的静态库,上述直接指定路径的方式更常见和可靠。

    ``
    **注意:** 上述
    target_link_libraries中直接指定.a文件路径的方式是针对每个 ABI 分别链接的,如果你希望更简洁,可以通过add_library(ffmpeg-avcodec STATIC IMPORTED)这种方式定义每个 FFmpeg 库为导入目标,然后target_link_libraries(ffmpeg_native ffmpeg-avcodec ffmpeg-avformat …)。为了完整性,这里给出了直接链接.a` 的示例,也列出了导入目标的定义方法,你可以选择一种。

3.3 Gradle 配置

app/build.gradle 中,配置 externalNativeBuild 块,指定 CMakeLists.txt 的路径。

“`gradle
android {
compileSdk 34 // 或更高

defaultConfig {
    applicationId "com.example.ffmpegdemo"
    minSdk 21
    targetSdk 34
    versionCode 1
    versionName "1.0"

    testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

    externalNativeBuild {
        cmake {
            cppFlags "-std=c++17" // 根据你的 C++ 版本调整
            arguments "-DANDROID_ARM_NEON=TRUE" // 如果需要启用 NEON 优化
            arguments "-DANDROID_PLATFORM=android-${minSdk}" // 指定目标 Android API 级别
        }
    }
    ndk {
        // 指定你希望打包的 ABI。如果只编译了部分 ABI,只列出那些。
        abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
    }
}

buildTypes {
    release {
        minifyEnabled false
        proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
    }
}

// 指定 CMakeLists.txt 的路径
externalNativeBuild {
    cmake {
        path file('src/main/cpp/CMakeLists.txt')
        version "3.10.2" // 或你的 CMake 版本
    }
}

}

dependencies {
implementation ‘androidx.appcompat:appcompat:1.6.1’
implementation ‘com.google.android.material:material:1.11.0’
implementation ‘androidx.constraintlayout:constraintlayout:2.1.4’
testImplementation ‘junit:junit:4.13.2’
androidTestImplementation ‘androidx.test.ext:junit:1.1.5’
androidTestImplementation ‘androidx.test.espresso:espresso-core:3.5.1’
}

“`

同步 Gradle,Android Studio 将会自动构建原生库,并将生成的 libffmpeg_native.so 打包到 APK 中。

第四章:FFmpeg 实战应用案例

4.1 基础视频播放器

基于第二、三章的编译和集成,我们可以构建一个简单的视频播放器。核心流程已在 JNI 示例代码中展示,这里更详细地描述其逻辑:

  1. 初始化 FFmpeg:
    • avformat_network_init() (如果涉及网络流)。
    • 打开输入文件/流 (avformat_open_input)。
    • 查找流信息 (avformat_find_stream_info)。
    • 查找视频和音频流 (av_find_best_stream 或遍历)。
  2. 解码器初始化:
    • 为每个流(视频和音频)找到对应的解码器 (avcodec_find_decoder)。
    • 创建解码器上下文 (avcodec_alloc_context3),并从流参数中复制信息 (avcodec_parameters_to_context)。
    • 打开解码器 (avcodec_open2)。
  3. 视频渲染准备:
    • 通过 ANativeWindow_fromSurface() 获取 Android Surface 对应的原生窗口。
    • 设置原生窗口的缓冲区几何形状 (ANativeWindow_setBuffersGeometry),以匹配视频帧的宽度、高度和像素格式(通常是 RGBA)。
    • 初始化 sws_getContext() 用于将 FFmpeg 解码出的 YUV 格式帧转换为 RGBA 格式。
  4. 音频播放准备:
    • 使用 swr_alloc_set_opts() 初始化 SwrContext,用于音频重采样(将解码出的音频格式转换为 AudioTrack 支持的格式,如 S16LE, 2声道, 44100Hz)。
    • 创建 Android AudioTrack 实例,准备播放。
  5. 播放循环:
    • 循环读取数据包 (av_read_frame)。
    • 根据 packet->stream_index 判断是视频包还是音频包。
    • 视频包处理:
      • 发送数据包给视频解码器 (avcodec_send_packet)。
      • 从解码器接收解码后的帧 (avcodec_receive_frame)。
      • 使用 sws_scale() 将 YUV 帧转换为 RGBA。
      • 锁定 ANativeWindow 缓冲区 (ANativeWindow_lock),将 RGBA 数据复制到缓冲区中。
      • 解锁并提交缓冲区 (ANativeWindow_unlockAndPost),完成一帧视频的显示。
    • 音频包处理:
      • 发送数据包给音频解码器 (avcodec_send_packet)。
      • 从解码器接收解码后的帧 (avcodec_receive_frame)。
      • 使用 swr_convert() 对音频帧进行重采样。
      • 将重采样后的数据写入 AudioTrack 进行播放。
    • 释放数据包 (av_packet_unref)。
  6. 音视频同步:
    • 这是播放器最复杂的部分。通常通过维护视频和音频的播放时间戳(PTS)来实现。
    • 视频帧显示时,判断其 PTS 与当前音频播放时间的差距,进行适当的等待或丢帧。
    • 音频播放时,根据其 PTS 决定是否需要加快或减慢播放速度(通常不直接控制音频速度,而是让音频作为主时钟,视频追随)。
  7. 资源释放:
    • 播放结束后,关闭所有解码器 (avcodec_close),释放上下文 (avcodec_free_context)。
    • 关闭输入文件 (avformat_close_input),释放上下文 (avformat_free_context)。
    • 释放 SwrContext (swr_free) 和 SwsContext (sws_free_context)。
    • 释放 ANativeWindow (ANativeWindow_release) 和 AudioTrack
    • avformat_network_deinit()

4.2 视频转码与编辑

FFmpeg 的转码功能非常强大,可以实现格式转换、分辨率调整、码率控制、音量调节等。

  1. 打开输入和输出:
    • 打开输入文件 (avformat_open_input)。
    • 创建输出上下文 (avformat_alloc_output_context2),指定输出格式。
    • 打开输出文件 (avio_open)。
  2. 创建新的流:
    • 遍历输入流,为需要转码的流创建新的输出流 (avformat_new_stream)。
    • 为每个新的输出流找到合适的编码器 (avcodec_find_encoder)。
    • 创建编码器上下文 (avcodec_alloc_context3),设置编码参数(分辨率、码率、GOP等)。
    • 打开编码器 (avcodec_open2)。
    • 将编码器参数复制到输出流参数 (avcodec_parameters_from_context)。
    • 对于不需要转码的流(例如,只需要复制音频流),可以直接将输入流的参数复制到输出流。
  3. 写入文件头 (avformat_write_header)。
  4. 转码循环:
    • 循环读取输入包 (av_read_frame)。
    • 解码: 如果需要对数据进行修改(如缩放、滤镜、重采样),则需要先解码。avcodec_send_packet -> avcodec_receive_frame
    • 处理:
      • 视频:sws_scale 进行缩放、像素格式转换;avfilter 进行滤镜处理。
      • 音频:swr_convert 进行重采样。
    • 编码: 将处理后的帧发送给输出流的编码器。avcodec_send_frame -> avcodec_receive_packet
    • 写入输出: av_interleaved_write_frame 将编码后的包写入输出文件。
    • 注意时间戳同步: 转码过程中需要正确处理 PTS/DTS,通常需要进行时间基转换。
  5. 写入文件尾 (av_write_trailer)。
  6. 资源释放。

4.3 视频滤镜处理

FFmpeg 强大的 libavfilter 库可以实现各种音视频滤镜效果。

  1. 创建滤镜图 (avfilter_graph_alloc)。
  2. 创建输入输出滤镜:
    • avfilter_get_by_name("buffer") 作为输入滤镜 (buffersrc),接收原始帧。
    • avfilter_get_by_name("buffersink") 作为输出滤镜 (buffersink),输出处理后的帧。
  3. 构建滤镜链:
    • 使用 avfilter_graph_parse_ptravfilter_graph_create_filter 手动创建并连接中间滤镜(如 scale, crop, overlay 等)。
    • 例如:"[in]scale=iw/2:ih/2[out]" 将视频缩小一半。
  4. 配置滤镜图 (avfilter_graph_config)。
  5. 处理循环:
    • 解码原始帧。
    • 将原始帧发送给 buffersrc (av_buffersrc_add_frame)。
    • buffersink 获取处理后的帧 (av_buffersink_get_frame)。
    • 对处理后的帧进行编码或渲染。

4.4 视频截帧/缩略图生成

  1. 打开输入文件,查找视频流。
  2. 初始化视频解码器。
  3. 寻找目标帧:
    • 可以使用 av_seek_frame 跳转到视频的特定时间戳。
    • 或者循环读取帧,直到达到指定时间戳或帧数。
  4. 解码目标帧: 读取数据包,解码视频帧。
  5. 像素格式转换: 使用 sws_scale 将解码出的 YUV 帧转换为常见的图像格式,如 RGB24 或 RGBA。
  6. 保存为图片: 将 RGB/RGBA 数据写入文件,可以手动写入 BMP/PPM 格式,或结合 libjpeg/libpng 等库生成 JPEG/PNG 文件。

第五章:性能优化与常见问题

5.1 性能优化

  1. 按需编译: 编译 FFmpeg 时,只启用你真正需要的组件 (--enable-decoder=h264, --disable-everything 后启用),减小库体积,提高加载速度。
  2. 选择正确的像素格式: FFmpeg 内部通常使用 YUV 格式处理视频,尽量避免不必要的像素格式转换,只在渲染前转换为目标格式。
  3. 多线程优化: FFmpeg 内部许多组件支持多线程 (codecCtx->thread_count = N),合理配置可以提高解码速度。
  4. 硬件加速: Android NDK 提供了 MediaCodec API,可以利用设备的硬件解码器。FFmpeg 在编译时可以启用对 MediaCodec 的支持(如 --enable-mediacodec, --enable-decoder=h264_mediacodec),在 JNI 层通过 AV_HWACCEL_DUMMYAV_HWACCEL_MEDIACODEC 实现硬件加速,将解码工作交给硬件,显著提升性能并降低功耗。
  5. 内存管理: FFmpeg 涉及到大量的内存操作,确保正确分配和释放 AVFrame, AVPacket, AVCodecContext 等资源,避免内存泄漏。使用 av_frame_free, av_packet_free, av_freep 等函数。

5.2 常见问题与解决方案

  1. 链接错误 (Undefined references):
    • 原因: CMakeLists.txt 中没有链接所有必需的 FFmpeg 库或 Android 系统库。
    • 解决方案: 仔细检查 target_link_libraries 中是否包含了所有的 libavcodec.a, libavformat.a 等,以及 log, android, z, m 等 NDK 库。
  2. 编译失败 (FFmpeg configure 错误):
    • 原因: NDK_ROOTTOOLCHAIN_PATH 设置错误;NDK 版本与 FFmpeg 不兼容;缺少编译依赖工具;configure 参数设置有误。
    • 解决方案: 仔细检查脚本路径,尝试不同 NDK 版本,确保所有依赖工具已安装。查阅 config.log 文件获取详细错误信息。
  3. 运行时崩溃 (JNI SIGSEGV):
    • 原因: JNI 层 C/C++ 代码存在内存访问错误(空指针、越界)、线程安全问题、FFmpeg API 调用顺序错误、JNIEnv 在多线程中不当使用。
    • 解决方案: 使用 Android Studio 的 Logcat 观察崩溃日志,分析堆栈信息。在 C++ 代码中打印详细日志,使用 LOGD, LOGE 宏进行调试。确保 JNI 回调在主线程执行,或者在子线程中通过 AttachCurrentThread 获取 JNIEnv。
  4. 库文件体积过大:
    • 原因: 编译时启用了过多的 FFmpeg 组件。
    • 解决方案: 严格遵循 --disable-everything 原则,只启用必需的编解码器、复用/解复用器、协议和滤镜。考虑移除不常用的模块,例如 --disable-hwaccels (如果不需要硬件加速)。
  5. 音视频不同步:
    • 原因: 播放逻辑中没有正确处理音视频时间戳,没有设置主时钟。
    • 解决方案: 建立音视频同步机制,通常以音频为基准,视频追随音频。计算帧的 PTS,并根据 PTS 延迟或丢弃帧。

总结

将 FFmpeg 引入 Android 平台是一项复杂但非常有价值的工作。它赋予了 Android 应用无与伦比的音视频处理能力,可以实现高度定制化的播放器、强大的视频编辑功能和专业的流媒体解决方案。从环境准备、交叉编译、JNI 集成到实际应用开发,每一步都需要细致入微的理解和实践。

虽然本文提供了详细的指南和示例,但 FFmpeg 库本身是一个庞大的项目,其 API 细节和使用技巧还需要开发者深入学习官方文档。掌握 FFmpeg 在 Android 上的编译与集成,将为您的移动多媒体开发打开全新的大门,助您构建出功能强大、性能卓越的音视频应用。

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部