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 文件)。这个过程称为交叉编译。其主要挑战包括:
- 环境配置: 需要配置 Android NDK (Native Development Kit),提供交叉编译工具链(编译器、链接器等)。
- 配置脚本: FFmpeg 的
configure脚本需要识别 Android 目标平台,并使用正确的交叉编译参数。 - 依赖管理: FFmpeg 可能依赖一些第三方库(如
x264,fdk-aac,openssl等),这些库也需要进行交叉编译。 - ABI 兼容性: Android 设备有多种 CPU 架构(ABI),需要为每种架构分别编译。
- 库裁剪: FFmpeg 功能强大但体积庞大,通常需要根据实际需求裁剪不必要的模块,以减小最终库的体积。
- JNI 接口: 编译好的原生库需要通过 Java Native Interface (JNI) 与 Java 层进行通信。
第二章:FFmpeg 在 Android 平台的编译实践
2.1 准备编译环境
在开始编译之前,确保您的开发环境已具备以下条件:
- 操作系统: 推荐使用 Linux (Ubuntu/Debian) 或 macOS。Windows 下需要额外配置 Cygwin 或 MinGW,或者使用 WSL (Windows Subsystem for Linux)。
- Android SDK & NDK:
- 安装 Android Studio。
- 通过 Android Studio SDK Manager 下载 Android SDK。
- 同样在 SDK Manager 中下载 Android NDK。建议使用较新版本的 NDK (例如 r21b 或更高),它们提供了统一的工具链,简化了配置。
- 确保
ANDROID_SDK_ROOT和ANDROID_NDK_HOME环境变量已正确设置,或者在编译脚本中指定其路径。
- 其他工具:
git(用于下载 FFmpeg 源码)、make、autoconf、automake、libtool、pkg-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 编译器和链接器标志。例如,为了支持 Androidlog库,可能需要添加-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_ROOT
**重要提示:**
* 请务必根据您的实际环境修改和TOOLCHAIN_PATH。TOOLCHAIN_PATH中的darwin-x86_64需要替换为您的操作系统架构,例如linux-x86_64或windows-x86_64。–cpu=${HOST%%-*}
*这部分在 NDK r19+ 统一工具链下通常会由 NDK 自动处理,旧版本 NDK 可能需要更精确指定。–enable-decoder=…
*等选项请根据你的实际需求进行精简,过多的启用会导致库文件体积膨胀。x264
* 如果需要第三方库(如、fdk-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 核心步骤:
-
定义 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++ 函数。cpp
`Java_包名_类名_方法名`。
例如:`Java_com_example_ffmpegdemo_FFMpegJni_getFFmpegVersion`
3. **编写 C/C++ JNI 代码:** 创建一个 `ffmpeg_jni.cpp` 文件,实现 `Java_...` 函数。
// ffmpeg_jni.cppinclude
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 库引入项目:
-
复制 FFmpeg 编译结果: 将
android_build目录下的所有 ABI 文件夹(armeabi-v7a,arm64-v8a等)复制到你的 Android 项目app/src/main/cpp/ffmpeg_libs目录下。
每个 ABI 目录内应包含include和lib两个子目录。
app/src/main/cpp/
├── CMakeLists.txt
├── ffmpeg_jni.cpp
└── ffmpeg_libs/
├── armeabi-v7a/
│ ├── include/
│ └── lib/ (包含 libavcodec.a, libavformat.a 等)
├── arm64-v8a/
│ ├── include/
│ └── lib/
└── ... -
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 示例代码中展示,这里更详细地描述其逻辑:
- 初始化 FFmpeg:
avformat_network_init()(如果涉及网络流)。- 打开输入文件/流 (
avformat_open_input)。 - 查找流信息 (
avformat_find_stream_info)。 - 查找视频和音频流 (
av_find_best_stream或遍历)。
- 解码器初始化:
- 为每个流(视频和音频)找到对应的解码器 (
avcodec_find_decoder)。 - 创建解码器上下文 (
avcodec_alloc_context3),并从流参数中复制信息 (avcodec_parameters_to_context)。 - 打开解码器 (
avcodec_open2)。
- 为每个流(视频和音频)找到对应的解码器 (
- 视频渲染准备:
- 通过
ANativeWindow_fromSurface()获取 AndroidSurface对应的原生窗口。 - 设置原生窗口的缓冲区几何形状 (
ANativeWindow_setBuffersGeometry),以匹配视频帧的宽度、高度和像素格式(通常是 RGBA)。 - 初始化
sws_getContext()用于将 FFmpeg 解码出的 YUV 格式帧转换为 RGBA 格式。
- 通过
- 音频播放准备:
- 使用
swr_alloc_set_opts()初始化SwrContext,用于音频重采样(将解码出的音频格式转换为AudioTrack支持的格式,如 S16LE, 2声道, 44100Hz)。 - 创建 Android
AudioTrack实例,准备播放。
- 使用
- 播放循环:
- 循环读取数据包 (
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)。
- 循环读取数据包 (
- 音视频同步:
- 这是播放器最复杂的部分。通常通过维护视频和音频的播放时间戳(PTS)来实现。
- 视频帧显示时,判断其 PTS 与当前音频播放时间的差距,进行适当的等待或丢帧。
- 音频播放时,根据其 PTS 决定是否需要加快或减慢播放速度(通常不直接控制音频速度,而是让音频作为主时钟,视频追随)。
- 资源释放:
- 播放结束后,关闭所有解码器 (
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 的转码功能非常强大,可以实现格式转换、分辨率调整、码率控制、音量调节等。
- 打开输入和输出:
- 打开输入文件 (
avformat_open_input)。 - 创建输出上下文 (
avformat_alloc_output_context2),指定输出格式。 - 打开输出文件 (
avio_open)。
- 打开输入文件 (
- 创建新的流:
- 遍历输入流,为需要转码的流创建新的输出流 (
avformat_new_stream)。 - 为每个新的输出流找到合适的编码器 (
avcodec_find_encoder)。 - 创建编码器上下文 (
avcodec_alloc_context3),设置编码参数(分辨率、码率、GOP等)。 - 打开编码器 (
avcodec_open2)。 - 将编码器参数复制到输出流参数 (
avcodec_parameters_from_context)。 - 对于不需要转码的流(例如,只需要复制音频流),可以直接将输入流的参数复制到输出流。
- 遍历输入流,为需要转码的流创建新的输出流 (
- 写入文件头 (
avformat_write_header)。 - 转码循环:
- 循环读取输入包 (
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,通常需要进行时间基转换。
- 循环读取输入包 (
- 写入文件尾 (
av_write_trailer)。 - 资源释放。
4.3 视频滤镜处理
FFmpeg 强大的 libavfilter 库可以实现各种音视频滤镜效果。
- 创建滤镜图 (
avfilter_graph_alloc)。 - 创建输入输出滤镜:
avfilter_get_by_name("buffer")作为输入滤镜 (buffersrc),接收原始帧。avfilter_get_by_name("buffersink")作为输出滤镜 (buffersink),输出处理后的帧。
- 构建滤镜链:
- 使用
avfilter_graph_parse_ptr或avfilter_graph_create_filter手动创建并连接中间滤镜(如scale,crop,overlay等)。 - 例如:
"[in]scale=iw/2:ih/2[out]"将视频缩小一半。
- 使用
- 配置滤镜图 (
avfilter_graph_config)。 - 处理循环:
- 解码原始帧。
- 将原始帧发送给
buffersrc(av_buffersrc_add_frame)。 - 从
buffersink获取处理后的帧 (av_buffersink_get_frame)。 - 对处理后的帧进行编码或渲染。
4.4 视频截帧/缩略图生成
- 打开输入文件,查找视频流。
- 初始化视频解码器。
- 寻找目标帧:
- 可以使用
av_seek_frame跳转到视频的特定时间戳。 - 或者循环读取帧,直到达到指定时间戳或帧数。
- 可以使用
- 解码目标帧: 读取数据包,解码视频帧。
- 像素格式转换: 使用
sws_scale将解码出的 YUV 帧转换为常见的图像格式,如 RGB24 或 RGBA。 - 保存为图片: 将 RGB/RGBA 数据写入文件,可以手动写入 BMP/PPM 格式,或结合 libjpeg/libpng 等库生成 JPEG/PNG 文件。
第五章:性能优化与常见问题
5.1 性能优化
- 按需编译: 编译 FFmpeg 时,只启用你真正需要的组件 (
--enable-decoder=h264,--disable-everything后启用),减小库体积,提高加载速度。 - 选择正确的像素格式: FFmpeg 内部通常使用 YUV 格式处理视频,尽量避免不必要的像素格式转换,只在渲染前转换为目标格式。
- 多线程优化: FFmpeg 内部许多组件支持多线程 (
codecCtx->thread_count = N),合理配置可以提高解码速度。 - 硬件加速: Android NDK 提供了 MediaCodec API,可以利用设备的硬件解码器。FFmpeg 在编译时可以启用对 MediaCodec 的支持(如
--enable-mediacodec,--enable-decoder=h264_mediacodec),在 JNI 层通过AV_HWACCEL_DUMMY或AV_HWACCEL_MEDIACODEC实现硬件加速,将解码工作交给硬件,显著提升性能并降低功耗。 - 内存管理: FFmpeg 涉及到大量的内存操作,确保正确分配和释放
AVFrame,AVPacket,AVCodecContext等资源,避免内存泄漏。使用av_frame_free,av_packet_free,av_freep等函数。
5.2 常见问题与解决方案
- 链接错误 (Undefined references):
- 原因:
CMakeLists.txt中没有链接所有必需的 FFmpeg 库或 Android 系统库。 - 解决方案: 仔细检查
target_link_libraries中是否包含了所有的libavcodec.a,libavformat.a等,以及log,android,z,m等 NDK 库。
- 原因:
- 编译失败 (FFmpeg configure 错误):
- 原因:
NDK_ROOT或TOOLCHAIN_PATH设置错误;NDK 版本与 FFmpeg 不兼容;缺少编译依赖工具;configure参数设置有误。 - 解决方案: 仔细检查脚本路径,尝试不同 NDK 版本,确保所有依赖工具已安装。查阅
config.log文件获取详细错误信息。
- 原因:
- 运行时崩溃 (JNI SIGSEGV):
- 原因: JNI 层 C/C++ 代码存在内存访问错误(空指针、越界)、线程安全问题、FFmpeg API 调用顺序错误、JNIEnv 在多线程中不当使用。
- 解决方案: 使用 Android Studio 的 Logcat 观察崩溃日志,分析堆栈信息。在 C++ 代码中打印详细日志,使用
LOGD,LOGE宏进行调试。确保 JNI 回调在主线程执行,或者在子线程中通过AttachCurrentThread获取 JNIEnv。
- 库文件体积过大:
- 原因: 编译时启用了过多的 FFmpeg 组件。
- 解决方案: 严格遵循
--disable-everything原则,只启用必需的编解码器、复用/解复用器、协议和滤镜。考虑移除不常用的模块,例如--disable-hwaccels(如果不需要硬件加速)。
- 音视频不同步:
- 原因: 播放逻辑中没有正确处理音视频时间戳,没有设置主时钟。
- 解决方案: 建立音视频同步机制,通常以音频为基准,视频追随音频。计算帧的 PTS,并根据 PTS 延迟或丢弃帧。
总结
将 FFmpeg 引入 Android 平台是一项复杂但非常有价值的工作。它赋予了 Android 应用无与伦比的音视频处理能力,可以实现高度定制化的播放器、强大的视频编辑功能和专业的流媒体解决方案。从环境准备、交叉编译、JNI 集成到实际应用开发,每一步都需要细致入微的理解和实践。
虽然本文提供了详细的指南和示例,但 FFmpeg 库本身是一个庞大的项目,其 API 细节和使用技巧还需要开发者深入学习官方文档。掌握 FFmpeg 在 Android 上的编译与集成,将为您的移动多媒体开发打开全新的大门,助您构建出功能强大、性能卓越的音视频应用。