第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)
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 |
我曾经在一个直播项目里,因为忘了调用 avformat_network_init,导致网络流一直打不开。排查了整整一天才发现是这个问题。所以,遇到网络相关的功能,记得先初始化网络模块。
另外,建议你在调试阶段把日志级别调到最高。FFmpeg内部有很多有用的调试信息,默认是关闭的。加上 av_log_set_level(AV_LOG_DEBUG),能帮你快速定位问题。
好了,FFmpeg集成的基础内容就这些。记住,编译环境配置是最容易出问题的环节。如果你按照上面的步骤来,应该能顺利跑通。遇到具体问题,多看看FFmpeg的日志输出,它会把问题原因说得明明白白。
公众号:蓝海资料掘金营,微信deep3321