第1章 FFmpeg集成:从编译到解码实战

FFmpeg在Android上的集成,说实话,是很多NDK开发者绕不过去的一道坎。我最早接触这个的时候,光编译就折腾了两天。后来做短视频项目,又踩了不少坑。今天我把这些经验整理出来,希望能帮你少走弯路。

1.1 FFmpeg编译为Android库

先把FFmpeg源码下载下来。我习惯用4.4版本,稳定,坑少。你可以在官网或者GitHub上找到它。

编译FFmpeg,说白了就是配置交叉编译环境。Android NDK里提供了工具链,我们直接用就行。下面是我常用的编译脚本:

#!/bin/bash
NDK=/path/to/android-ndk-r21e
API=21
ARCH=arm64
PLATFORM=$NDK/platforms/android-$API/arch-$ARCH
TOOLCHAIN=$NDK/toolchains/aarch64-linux-android-4.9/prebuilt/linux-x86_64

./configure \
    --prefix=$PWD/android/$ARCH \
    --enable-shared \
    --disable-static \
    --disable-doc \
    --disable-ffmpeg \
    --disable-ffplay \
    --disable-ffprobe \
    --disable-avdevice \
    --disable-postproc \
    --enable-cross-compile \
    --cross-prefix=$TOOLCHAIN/bin/aarch64-linux-android- \
    --target-os=android \
    --arch=aarch64 \
    --sysroot=$PLATFORM

make -j4
make install
小提示: 如果你只需要解码功能,记得加上 --enable-decoder=h264,aac 之类的选项。这样编译出来的库会小很多。我有个项目就是靠这个把so体积从30MB降到了8MB。

编译完成后,你会得到一堆.so文件。嗯,这里要注意:FFmpeg的库之间有依赖关系。比如libavformat依赖libavcodec和libavutil。加载的时候顺序不能乱。

1.2 CMakeLists.txt配置

接下来是CMake配置。我个人习惯把FFmpeg的库放在 app/src/main/jniLibs 目录下,然后通过CMake引入。

cmake_minimum_required(VERSION 3.10)
project("ffmpeg_demo")

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

# 设置FFmpeg库路径
set(FFMPEG_LIB_DIR ${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI})

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

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

add_library(avutil SHARED IMPORTED)
set_target_properties(avutil PROPERTIES IMPORTED_LOCATION
    ${FFMPEG_LIB_DIR}/libavutil.so)

add_library(swresample SHARED IMPORTED)
set_target_properties(swresample PROPERTIES IMPORTED_LOCATION
    ${FFMPEG_LIB_DIR}/libswresample.so)

# 你自己的native代码
add_library(native-lib SHARED
    native-lib.cpp)

# 链接FFmpeg库
target_link_libraries(native-lib
    avcodec
    avformat
    avutil
    swresample
    log)
注意: 库的加载顺序很重要。我曾经因为顺序搞反,导致运行时直接崩溃。正确的顺序是:avutil -> swresample -> avcodec -> avformat。在Java层加载时也要按这个顺序。

1.3 音视频解码示例

写一个简单的解码器,把视频文件解码成YUV帧。这是最基础的功能,也是很多项目的起点。

#include <jni.h>
#include <string>
#include <android/log.h>

extern "C" {
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libswscale/swscale.h>
}

#define LOG_TAG "FFmpegDemo"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)

extern "C" JNIEXPORT jint JNICALL
Java_com_example_ffmpegdemo_MainActivity_decodeVideo(
    JNIEnv* env, jobject thiz, jstring input_path) {

    const char* path = env->GetStringUTFChars(input_path, nullptr);

    // 初始化FFmpeg
    av_register_all();

    // 打开输入文件
    AVFormatContext* fmt_ctx = nullptr;
    if (avformat_open_input(&fmt_ctx, path, nullptr, nullptr) != 0) {
        LOGI("无法打开文件: %s", path);
        return -1;
    }

    // 查找流信息
    if (avformat_find_stream_info(fmt_ctx, nullptr) < 0) {
        LOGI("无法获取流信息");
        return -1;
    }

    // 查找视频流
    int video_stream_idx = -1;
    for (int i = 0; i < fmt_ctx->nb_streams; i++) {
        if (fmt_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
            video_stream_idx = i;
            break;
        }
    }

    if (video_stream_idx == -1) {
        LOGI("没有找到视频流");
        return -1;
    }

    // 获取解码器
    AVCodecParameters* codec_params = fmt_ctx->streams[video_stream_idx]->codecpar;
    AVCodec* codec = avcodec_find_decoder(codec_params->codec_id);
    if (!codec) {
        LOGI("找不到解码器");
        return -1;
    }

    // 打开解码器
    AVCodecContext* codec_ctx = avcodec_alloc_context3(codec);
    avcodec_parameters_to_context(codec_ctx, codec_params);
    if (avcodec_open2(codec_ctx, codec, nullptr) < 0) {
        LOGI("无法打开解码器");
        return -1;
    }

    // 分配帧和包
    AVFrame* frame = av_frame_alloc();
    AVPacket* packet = av_packet_alloc();

    // 读取并解码帧
    int frame_count = 0;
    while (av_read_frame(fmt_ctx, packet) >= 0) {
        if (packet->stream_index == video_stream_idx) {
            if (avcodec_send_packet(codec_ctx, packet) == 0) {
                while (avcodec_receive_frame(codec_ctx, frame) == 0) {
                    LOGI("解码到第 %d 帧", ++frame_count);
                    // 这里可以处理YUV数据
                }
            }
        }
        av_packet_unref(packet);
    }

    // 清理资源
    av_frame_free(&frame);
    av_packet_free(&packet);
    avcodec_free_context(&codec_ctx);
    avformat_close_input(&fmt_ctx);

    env->ReleaseStringUTFChars(input_path, path);
    return frame_count;
}

这段代码的核心逻辑其实很简单:打开文件 -> 找视频流 -> 初始化解码器 -> 循环读取和解码。我在项目中遇到过一个问题:有些手机解码H.265的视频特别慢。后来发现是硬解码没开启,加上 av_hwdevice_ctx_create 就好了。

1.4 常见问题排查

集成FFmpeg时,你可能会遇到下面这些问题。我把它们列出来,方便你对照排查。

问题 原因 解决方案
编译时找不到头文件 include路径没配置对 检查CMakeLists.txt中的include_directories
运行时so加载失败 库依赖顺序不对 按avutil->swresample->avcodec->avformat顺序加载
解码出来的画面是绿的 颜色空间转换不对 检查sws_getContext的参数设置
内存泄漏 av_frame_unref没调用 确保每个av_frame_alloc都有对应的av_frame_free
某些格式解码失败 编译时没开启对应解码器 重新编译,加上--enable-decoder=xxx
核心要点: FFmpeg集成最关键的三个环节:编译配置要精准、CMake链接要正确、资源释放要彻底。任何一个环节出问题,都会导致崩溃或功能异常。

我曾经在一个直播项目里,因为忘了调用 avformat_network_init,导致网络流一直打不开。排查了整整一天才发现是这个问题。所以,遇到网络相关的功能,记得先初始化网络模块。

另外,建议你在调试阶段把日志级别调到最高。FFmpeg内部有很多有用的调试信息,默认是关闭的。加上 av_log_set_level(AV_LOG_DEBUG),能帮你快速定位问题。

FFmpeg集成核心流程 1. 编译FFmpeg 2. CMake配置 3. 解码实现 配置交叉编译 选择解码器 生成.so文件 设置头文件路径 链接动态库 配置加载顺序 打开输入文件 查找视频流 循环解码帧 常见问题排查 • 编译失败 → 检查NDK路径和API级别 • 运行时崩溃 → 确认库加载顺序和ABI匹配 • 解码异常 → 验证颜色空间和内存管理 • 性能问题 → 考虑硬解码和线程优化

好了,FFmpeg集成的基础内容就这些。记住,编译环境配置是最容易出问题的环节。如果你按照上面的步骤来,应该能顺利跑通。遇到具体问题,多看看FFmpeg的日志输出,它会把问题原因说得明明白白。


公众号:蓝海资料掘金营,微信deep3321