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);
三、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集成全流程。你可以把它当作一个检查清单:
五、避坑指南
最后,分享几个我踩过的坑:
- ABI不匹配:编译的.so文件架构必须和设备的CPU架构一致。arm64的设备不能加载armv7a的库。
- 内存泄漏:FFmpeg的API很多需要手动释放内存。建议用RAII思想封装,或者写一个统一的清理函数。
- ANR问题:解码是耗时操作,必须在子线程执行。我见过有人直接在UI线程解码,结果界面卡死。
- 日志太多:FFmpeg默认会输出大量日志。建议在release版本中关闭日志,或者重定向到文件。
好了,FFmpeg在Android中的集成就讲到这里。说白了,就是三步:编译出.so、加载库、调用API。每一步都有坑,但只要你按照我上面说的来,基本不会出大问题。