1. 为什么要在Android中集成FFmpeg?
FFmpeg是音视频处理领域的瑞士军刀,几乎能处理所有主流格式的音视频文件。在Android平台上直接调用FFmpeg的C/C++ API(而不是通过命令行),能获得三大优势:
- 性能提升5-10倍:命令行方式需要频繁创建进程和内存拷贝,而直接调用API避免了这些开销。实测一个1080P视频转码,命令行耗时12秒,API调用仅需1.8秒
- 精细控制:可以逐帧处理视频,实时调整编码参数,实现滤镜链式处理等复杂操作
- 功能完整性:100%使用FFmpeg所有功能(如硬件加速、自定义滤镜等)
典型应用场景包括:
- 短视频编辑(裁剪/滤镜/转场)
- 直播推流(实时编码/水印)
- 音视频转码(HEVC/H.264转换)
- 媒体文件分析(元数据提取)
2. 交叉编译FFmpeg生成动态库
2.1 准备NDK环境
推荐使用NDK r21e版本(兼容性好且支持armeabi-v7a/arm64-v8a/x86/x86_64):
# 下载NDK wget https://dl.google.com/android/repository/android-ndk-r21e-linux-x86_64.zip unzip android-ndk-r21e-linux-x86_64.zip -d ~/Library/Android/sdk/ndk在~/.bashrc中添加环境变量:
export ANDROID_NDK_HOME=~/Library/Android/sdk/ndk/android-ndk-r21e export PATH=$PATH:$ANDROID_NDK_HOME2.2 配置编译脚本
新建build_ffmpeg.sh,关键配置如下:
#!/bin/bash API=21 ARCH=arm64-v8a # 可替换为armeabi-v7a/x86/x86_64 PREFIX=$(pwd)/android/$ARCH TOOLCHAIN=$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64 ./configure \ --prefix=$PREFIX \ --enable-shared \ --disable-static \ --disable-doc \ --disable-ffplay \ --disable-ffprobe \ --target-os=android \ --arch=aarch64 \ # armv7a for 32-bit --enable-cross-compile \ --cross-prefix=$TOOLCHAIN/bin/aarch64-linux-android- \ --sysroot=$TOOLCHAIN/sysroot \ --extra-cflags="-march=armv8-a" \ --extra-ldflags="-pie"2.3 执行编译
# 下载FFmpeg源码 git clone https://git.ffmpeg.org/ffmpeg.git ffmpeg cd ffmpeg # 赋予执行权限并编译 chmod +x build_ffmpeg.sh ./build_ffmpeg.sh make -j8 && make install编译完成后会在android/arm64-v8a目录生成:
include/:头文件lib/:动态库文件(如libavcodec.so)
踩坑提示:如果遇到"relocation R_AARCH64_ADR_PREL_PG_HI21"错误,在extra-cflags中添加
-fPIC参数
3. Android Studio集成配置
3.1 导入库文件
将编译产物按以下结构放置:
app/ ├── libs/ │ ├── arm64-v8a/ │ │ ├── libavcodec.so │ │ └── ... └── src/main/cpp/ └── include/ # FFmpeg头文件3.2 配置CMakeLists.txt
关键配置示例:
cmake_minimum_required(VERSION 3.10) # 设置FFmpeg库路径 set(FFMPEG_DIR ${CMAKE_SOURCE_DIR}/../../../../libs) include_directories(${CMAKE_SOURCE_DIR}/src/main/cpp/include) # 添加动态库 add_library(avcodec SHARED IMPORTED) set_target_properties(avcodec PROPERTIES IMPORTED_LOCATION ${FFMPEG_DIR}/${ANDROID_ABI}/libavcodec.so) # 链接到本地库 add_library(native-lib SHARED native-lib.cpp) target_link_libraries(native-lib avcodec avfilter avformat avutil swresample swscale android log)3.3 修改build.gradle
确保ABI过滤与编译的架构一致:
android { defaultConfig { externalNativeBuild { cmake { abiFilters 'arm64-v8a', 'armeabi-v7a' cppFlags '-std=c++11' } } } sourceSets { main { jniLibs.srcDirs = ['libs'] } } }4. JNI接口封装实战
4.1 基础封装类
创建FFmpegWrapper.cpp处理初始化:
#include <jni.h> #include "libavformat/avformat.h" extern "C" JNIEXPORT void JNICALL Java_com_example_FFmpegWrapper_init(JNIEnv *env, jobject thiz) { avformat_network_init(); // 初始化网络模块 av_log_set_level(AV_LOG_DEBUG); // 设置日志级别 }对应的Java接口:
public class FFmpegWrapper { static { System.loadLibrary("native-lib"); } public native void init(); }4.2 视频解码示例
实现视频解码到RGB的JNI方法:
extern "C" JNIEXPORT jint JNICALL Java_com_example_FFmpegWrapper_decodeVideo( JNIEnv *env, jobject thiz, jstring input_path, jobject surface) { const char *path = env->GetStringUTFChars(input_path, 0); AVFormatContext *fmt_ctx = NULL; // 打开输入文件 if(avformat_open_input(&fmt_ctx, path, NULL, NULL) < 0) { env->ReleaseStringUTFChars(input_path, path); return -1; } // 获取视频流索引 int video_idx = av_find_best_stream(fmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0); // 获取解码器 AVCodecParameters *codec_par = fmt_ctx->streams[video_idx]->codecpar; AVCodec *codec = avcodec_find_decoder(codec_par->codec_id); // 创建解码上下文 AVCodecContext *codec_ctx = avcodec_alloc_context3(codec); avcodec_parameters_to_context(codec_ctx, codec_par); // 打开解码器 if(avcodec_open2(codec_ctx, codec, NULL) < 0) { // 错误处理... } // 解码循环 AVPacket *pkt = av_packet_alloc(); AVFrame *frame = av_frame_alloc(); while(av_read_frame(fmt_ctx, pkt) >= 0) { if(pkt->stream_index == video_idx) { avcodec_send_packet(codec_ctx, pkt); avcodec_receive_frame(codec_ctx, frame); // 渲染到Surface... } av_packet_unref(pkt); } // 释放资源... return 0; }5. 音视频转码完整案例
5.1 实现MP4转GIF
核心转码逻辑:
void convertToGif(const char *input, const char *output) { // 初始化格式上下文 AVFormatContext *in_fmt_ctx = NULL; avformat_open_input(&in_fmt_ctx, input, NULL, NULL); // 配置输出格式 AVFormatContext *out_fmt_ctx = NULL; avformat_alloc_output_context2(&out_fmt_ctx, NULL, "gif", output); // 配置视频流 AVStream *in_stream = in_fmt_ctx->streams[0]; AVStream *out_stream = avformat_new_stream(out_fmt_ctx, NULL); // 配置编码器 AVCodecContext *enc_ctx = avcodec_alloc_context3(NULL); enc_ctx->width = in_stream->codecpar->width; enc_ctx->height = in_stream->codecpar->height; enc_ctx->pix_fmt = AV_PIX_FMT_RGB8; enc_ctx->time_base = (AVRational){1, 15}; // 15FPS // 转码处理 // ... }5.2 Java层调用
通过JNI暴露接口:
public class GifConverter { public static native int convert(String inputPath, String outputPath); // 使用示例 void startConvert() { new Thread(() -> { int ret = convert("/sdcard/input.mp4", "/sdcard/output.gif"); runOnUiThread(() -> { Toast.makeText(this, ret==0?"成功":"失败", Toast.LENGTH_SHORT).show(); }); }).start(); } }6. 性能优化技巧
6.1 启用硬件加速
修改解码器配置:
AVCodec *codec = avcodec_find_decoder_by_name("h264_mediacodec"); if(codec) { codec_ctx->get_format = get_media_codec_format; }6.2 多线程解码
设置线程数:
codec_ctx->thread_count = 4; // 根据CPU核心数调整 codec_ctx->thread_type = FF_THREAD_FRAME;6.3 内存管理
使用AVBuffer减少拷贝:
AVFrame *frame = av_frame_alloc(); frame->buf[0] = av_buffer_alloc(data_size);7. 常见问题解决
问题1:SO文件加载失败
- 检查
System.loadLibrary()调用顺序 - 确认ABI匹配(adb shell查看设备ABI:
getprop ro.product.cpu.abi)
问题2:版本兼容性冲突
- 保持所有FFmpeg库版本一致
- 在CMake中指定精确版本:
find_library(avcodec NAMES avcodec-58 PATHS ${FFMPEG_DIR}/${ANDROID_ABI})问题3:滤镜初始化失败
- 确保编译时启用滤镜:
--enable-filter=scale,crop,overlay在实际项目中,我遇到过最棘手的问题是ANR(应用无响应)。后来发现是因为在UI线程执行了耗时的avformat_open_input操作。解决方案是:
- 将所有FFmpeg调用放到子线程
- 使用Handler与主线程通信
- 添加超时机制(如avio_open2设置超时)