26、FFmpeg在Android中的集成:FFmpeg编译、FFmpeg命令行调用、FFmpeg API调用

说实话,FFmpeg在Android上的集成,是很多音视频开发者的第一道坎。

我记得刚入行那会儿,光编译就折腾了两天。不是缺头文件,就是链接报错。后来我才明白,这玩意儿不是简单的「复制粘贴」就能跑起来的。今天我就把这几年的经验捋一捋,带你走一遍完整的集成流程。

一、FFmpeg编译:从源码到.so文件

Android用的是Linux内核,但CPU架构是ARM。所以你不能直接拿PC上的FFmpeg库用,得交叉编译。

1. 环境准备

你需要三样东西:

  • NDK:Android原生开发工具包。我个人习惯用r21版本,比较稳定。
  • FFmpeg源码:去官网下载,我建议用4.4版本,API稳定,坑少。
  • Linux/Mac环境:Windows也能搞,但麻烦。我一般用Ubuntu 20.04。

2. 编写编译脚本

FFmpeg的编译靠的是configure脚本。你需要传一堆参数,告诉它目标平台是Android。

#!/bin/bash
NDK=/home/user/android-ndk-r21e
API=21
ARCH=arm64
PREFIX=$(pwd)/android/$ARCH

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

./configure \
    --prefix=$PREFIX \
    --target-os=android \
    --arch=$ARCH \
    --enable-cross-compile \
    --cross-prefix=$TOOLCHAIN/bin/aarch64-linux-android- \
    --cc=$TOOLCHAIN/bin/aarch64-linux-android$API-clang \
    --disable-static \
    --enable-shared \
    --disable-doc \
    --disable-ffmpeg \
    --disable-ffplay \
    --disable-ffprobe \
    --disable-avdevice \
    --disable-postproc \
    --enable-gpl \
    --enable-version3

make -j4
make install

这里有几个关键点:

  • --target-os=android:告诉FFmpeg目标系统是Android。
  • --cc:指定clang编译器。NDK r18以后默认用clang,别用gcc了。
  • --enable-shared:生成.so动态库。Android不支持静态库加载。
注意: 我曾经因为没加--disable-ffmpeg,编译出来一堆命令行工具,APK体积直接飙到80MB。如果你只需要库,记得把这些都关了。

3. 多架构编译

Android设备有armv7a、arm64、x86等多种架构。我建议至少编译arm64和armv7a两个版本。

架构 ARCH参数 适用设备
arm64-v8a arm64 主流手机(2015年后)
armeabi-v7a armv7a 老设备、低端机
x86_64 x86_64 模拟器

编译完成后,你会得到一堆.so文件:libavcodec.so、libavformat.so、libavutil.so、libswscale.so、libswresample.so。这些就是我们要的。

二、FFmpeg命令行调用:快速验证的利器

你可能会问:Android上怎么跑FFmpeg命令行?

嗯,其实有两种方式。一种是直接编译出ffmpeg可执行文件,用ProcessBuilder调用。另一种是把命令行封装成API调用。我个人更推荐后者,但命令行方式在调试阶段非常有用。

1. 编译ffmpeg可执行文件

在configure脚本里,把--disable-ffmpeg去掉,改成--enable-ffmpeg。编译完成后,你会得到一个ffmpeg二进制文件。

2. 在Android中调用

public class FFmpegCommand {
    public static int execute(String[] cmd) {
        Process process = null;
        try {
            process = Runtime.getRuntime().exec(cmd);
            BufferedReader reader = new BufferedReader(
                new InputStreamReader(process.getInputStream()));
            String line;
            while ((line = reader.readLine()) != null) {
                Log.d("FFmpeg", line);
            }
            return process.waitFor();
        } catch (Exception e) {
            e.printStackTrace();
            return -1;
        }
    }
}

调用示例:

String[] cmd = {"ffmpeg", "-i", "/sdcard/input.mp4", 
                 "-vf", "scale=640:480", "/sdcard/output.mp4"};
int result = FFmpegCommand.execute(cmd);
小技巧: 我在项目中遇到过,直接传路径会报权限问题。建议先把文件复制到应用的私有目录(getFilesDir()),再操作。

三、FFmpeg API调用:真正的集成方式

命令行方式虽然简单,但性能差、控制力弱。真正的生产环境,都是用API调用的。

1. 加载动态库

在Java/Kotlin中加载.so文件:

public class FFmpegNative {
    static {
        System.loadLibrary("avcodec");
        System.loadLibrary("avformat");
        System.loadLibrary("avutil");
        System.loadLibrary("swscale");
        System.loadLibrary("swresample");
    }
    
    public static native int decodeVideo(String inputPath, String outputPath);
}

2. JNI接口实现

在C/C++层,你需要写JNI函数来调用FFmpeg的API。这里以视频解码为例:

#include <jni.h>
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>

extern "C" JNIEXPORT jint JNICALL
Java_com_example_ffmpeg_FFmpegNative_decodeVideo(
    JNIEnv* env, jobject thiz, 
    jstring inputPath, jstring outputPath) {
    
    const char* input = env->GetStringUTFChars(inputPath, nullptr);
    
    AVFormatContext* fmtCtx = nullptr;
    avformat_open_input(&fmtCtx, input, nullptr, nullptr);
    avformat_find_stream_info(fmtCtx, nullptr);
    
    // 找到视频流
    int videoStream = -1;
    for (int i = 0; i < fmtCtx->nb_streams; i++) {
        if (fmtCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
            videoStream = i;
            break;
        }
    }
    
    // 打开解码器
    AVCodecParameters* codecParams = fmtCtx->streams[videoStream]->codecpar;
    const AVCodec* codec = avcodec_find_decoder(codecParams->codec_id);
    AVCodecContext* codecCtx = avcodec_alloc_context3(codec);
    avcodec_parameters_to_context(codecCtx, codecParams);
    avcodec_open2(codecCtx, codec, nullptr);
    
    // 解码循环
    AVPacket* packet = av_packet_alloc();
    AVFrame* frame = av_frame_alloc();
    
    while (av_read_frame(fmtCtx, packet) >= 0) {
        if (packet->stream_index == videoStream) {
            avcodec_send_packet(codecCtx, packet);
            while (avcodec_receive_frame(codecCtx, frame) == 0) {
                // 处理解码后的帧
                // 可以保存为YUV或RGB
            }
        }
        av_packet_unref(packet);
    }
    
    // 清理资源
    av_frame_free(&frame);
    av_packet_free(&packet);
    avcodec_free_context(&codecCtx);
    avformat_close_input(&fmtCtx);
    
    env->ReleaseStringUTFChars(inputPath, input);
    return 0;
}

3. 在Android中调用

String input = getFilesDir() + "/input.mp4";
String output = getFilesDir() + "/output.yuv";
int result = FFmpegNative.decodeVideo(input, output);
if (result == 0) {
    Log.d("FFmpeg", "解码成功");
} else {
    Log.e("FFmpeg", "解码失败");
}
核心要点: FFmpeg的API调用遵循「打开-处理-关闭」的模式。每一步都要检查返回值,否则很容易出现野指针或内存泄漏。我曾经因为忘了调用av_packet_unref,导致内存暴涨,最后OOM崩溃。

四、知识体系总览

下面这张图,是我自己总结的FFmpeg集成全流程。你可以把它当作一个检查清单:

FFmpeg Android集成知识体系 1. 交叉编译 2. 命令行调用 3. API调用 编译子项 • NDK + clang • 多架构支持 • 动态库(.so) 命令行子项 • ProcessBuilder • 权限管理 • 调试利器 API子项 • JNI接口 • 解码/编码 • 内存管理 Android音视频处理能力

五、避坑指南

最后,分享几个我踩过的坑:

  1. ABI不匹配:编译的.so文件架构必须和设备的CPU架构一致。arm64的设备不能加载armv7a的库。
  2. 内存泄漏:FFmpeg的API很多需要手动释放内存。建议用RAII思想封装,或者写一个统一的清理函数。
  3. ANR问题:解码是耗时操作,必须在子线程执行。我见过有人直接在UI线程解码,结果界面卡死。
  4. 日志太多:FFmpeg默认会输出大量日志。建议在release版本中关闭日志,或者重定向到文件。
我的建议: 刚开始集成时,先用命令行方式跑通流程,确认FFmpeg功能正常。然后再切换到API方式。这样出了问题,你能快速定位是FFmpeg的问题,还是JNI封装的问题。

好了,FFmpeg在Android中的集成就讲到这里。说白了,就是三步:编译出.so、加载库、调用API。每一步都有坑,但只要你按照我上面说的来,基本不会出大问题。


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