目录
- 为什么要在 iOS 上使用 FFmpeg?
- 准备工作:安装 Homebrew 和 FFmpeg
- 核心步骤:为 iOS 编译 FFmpeg
- 为什么需要自己编译?
- 编译脚本详解
- 编译与常见问题
- 将编译好的库集成到 Xcode 项目
- 创建新项目
- 添加文件和配置
- 链接库和设置 Header Search Paths
- 实战:编写第一个 FFmpeg 程序
- 目标:获取视频文件信息
- 代码实现
- 运行与调试
- 进阶:视频压缩(转码)实战
- 目标:将视频压缩为 H.264 格式
- 关键 API 解析
- 完整代码示例
- 重要注意事项
- 性能与线程:GCD 的使用
- 内存管理:
av_free和avformat_close_input - 安全与隐私:
Info.plist配置 - App Store 审核指南:关于加密库的说明
- 学习资源与总结
为什么要在 iOS 上使用 FFmpeg?
iOS 系统自带的 AVFoundation 框架功能强大,能满足大部分音视频播放、录制和编辑的需求,但在以下场景,FFmpeg 依然是不可替代的利器:

- 极致的格式支持:支持几乎所有已知的音视频格式、编码器和容器格式,而
AVFoundation只支持主流的几种。 - 高度定制的编解码:可以精确控制编码参数(如码率、帧率、分辨率、编码Profile/Level),实现更精细的视频压缩或优化。
- 强大的处理能力:提供复杂的音视频处理功能,如滤镜、流媒体推拉、格式转换等,这些在
AVFoundation中实现起来非常复杂。 - 服务器端逻辑迁移:如果你的后端服务是用 FFmpeg 处理视频,为了保持逻辑一致性,也可以将其部分功能迁移到客户端。
准备工作:安装 Homebrew 和 FFmpeg
在编译之前,我们需要在 Mac 上安装 FFmpeg 的命令行工具,这可以帮助我们了解其功能和参数。
-
安装 Homebrew (如果尚未安装) 打开终端,运行以下命令:
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
-
安装 FFmpeg 安装完成后,使用 Homebrew 安装 FFmpeg:
brew install ffmpeg
现在你可以在终端里使用 ffmpeg -version 来查看安装是否成功。

核心步骤:为 iOS 编译 FFmpeg
为什么需要自己编译?
苹果的 iOS 系统是闭源的,并且有严格的架构限制(如 arm64, arm64-simulator),FFmpeg 官方提供的预编译版本通常只包含通用的 x86_64 架构,无法在真机上运行,我们必须为 iOS 的目标架构(arm64, arm64-simulator, x86_64)手动编译出对应的库文件(.a 文件)。
编译脚本详解
手动编译 FFmpeg 非常繁琐,推荐使用现成的编译脚本,这里我们使用 tmm1/homebrew-ffmpeg 项目中的脚本,它非常流行且可靠。
-
克隆脚本 在终端中,进入你希望存放代码的目录,然后克隆:
(图片来源网络,侵删)git clone https://github.com/tmm1/homebrew-ffmpeg.git cd homebrew-ffmpeg
-
修改编译配置(可选但推荐) 脚本默认会编译一个非常全面的 FFmpeg,包含很多你可能用不到的组件(如
libass,libbluray),这会导致最终生成的库文件非常大,为了减小 App 体积,我们应该只编译我们需要的部分。打开
ffmpeg.rb文件,找到depends_on部分,注释掉你不需要的库依赖,如果我们只需要最基础的 H.264/H.265 编解码,可以这样做:# ffmpeg.rb class Ffmpeg < Formula # ... 其他代码 ... depends_on "aom" => :optional # depends_on "dav1d" => :optional # depends_on "fontconfig" => :optional # depends_on "frei0r" => :optional # depends_on "harfbuzz" => :optional # depends_on "jpeg-turbo" => :optional # depends_on "libass" => :optional # depends_on "libbluray" => :optional # depends_on "libbs2b" => :optional # depends_on "libebur128" => :optional # depends_on "libgsm" => :optional # depends_on "libmodplug" => :optional # depends_on "librist" => :optional # depends_on "librsvg" => :optional # depends_on "libsoxr" => :optional # depends_on "libssh" => :optional # depends_on "libvidstab" => :optional # depends_on "libvmaf" => :optional # depends_on "libvorbis" => :optional # depends_on "libvpx" => :optional # depends_on "libwebp" => :optional # depends_on "openh264" => :optional # depends_on "openjpeg" => :optional # depends_on "opus" => :optional # depends_on "rav1e" => :optional # depends_on "rtmpdump" => :optional # depends_on "rubberband" => :optional # depends_on "sdl2" => :optional # depends_on "snappy" => :optional # depends_on "speex" => :optional # depends_on "srt" => :optional # depends_on "svt-av1" => :optional # depends_on "tesseract" => :optional # depends_on "theora" => :optional # depends_on "twolame" => :optional # depends_on "vid.stab" => :optional # depends_on "x265" => :optional # depends_on "xvid" => :optional # depends_on "xz" => :optional # depends_on "zeromq" => :optional # depends_on "zimg" => :optional depends_on "bzip2" depends_on "frei0r" => :optional depends_on "lame" => :optional depends_on "libogg" => :optional depends_on "librist" => :optional depends_on "libsamplerate" => :optional depends_on "libvorbis" => :optional depends_on "libvpx" => :optional depends_on "openh264" => :optional depends_on "opus" => :optional depends_on "rtmpdump" => :optional depends_on "SDL2" => :optional depends_on "soxr" => :optional depends_on "speex" => :optional depends_on "theora" => :optional depends_on "x264" # <-- 如果你需要 x264 编码器,保留这个 depends_on "x265" # <-- 如果你需要 x265 编码器,保留这个 # ... 其他代码 ... end
注意:
x264和x265是非常优秀的 H.264 和 H.265 软件编码库,如果你需要进行视频压缩,强烈建议保留它们。 -
运行编译脚本 脚本会自动检测你连接的 iOS 设备和模拟器,并分别进行编译。
brew install ffmpeg
这个过程会持续很长时间(可能需要 30 分钟到 1 小时以上),请耐心等待,编译成功后,你会在以下目录找到编译好的文件:
/usr/local/Cellar/ffmpeg/4.x.x/lib(版本号可能不同) 在这个目录下,你会看到ffmpeg,ffprobe等命令行工具,以及我们需要的静态库文件,它们按架构分在子目录中。
编译与常见问题
- 找不到 iOS SDK 路径:脚本通常能自动找到,但如果遇到问题,可以手动指定:
SDKROOT=$(xcrun --show-sdk-path --sdk iphonesimulator) brew install ffmpeg
- 编译失败:通常是某个依赖库编译失败,检查终端输出的错误信息,确保你的 Mac 系统和 Xcode 是最新版本。
将编译好的库集成到 Xcode 项目
现在我们有了 FFmpeg 的库文件,需要将它们集成到你的 iOS App 项目中。
-
创建新项目 打开 Xcode,创建一个新的 "Single View App" 项目,命名为
FFmpegDemo。 -
添加文件和配置
-
在 Finder 中,导航到
/usr/local/Cellar/ffmpeg/4.x.x/lib。 -
你会看到类似
arm64,arm64-simulator,x86_64的文件夹,我们需要将它们合并成一个通用的库。 -
在 Xcode 项目中,创建一个名为
Frameworks的文件夹,然后在Frameworks下创建一个名为lib的新文件夹。 -
将
arm64,arm64-simulator,x86_64文件夹中的所有.a文件(如libavcodec.a,libavformat.a,libavutil.a等)复制到 Xcode 项目的Frameworks/lib文件夹中。 -
重要:在 Xcode 中,选中刚刚复制的所有
.a文件,在 File Inspector 中确保 "Target Membership" 勾选了你的主 Target。 -
添加头文件: 在 Finder 中,找到 FFmpeg 的头文件,通常在
/usr/local/Cellar/ffmpeg/4.x.x/include目录下。 将整个ffmpeg文件夹复制到你的 Xcode 项目中(放在Frameworks文件夹下)。
-
-
链接库和设置 Header Search Paths
- 选择你的项目 Target -> "Build Settings"。
- 搜索 "Header Search Paths":
点击 号,添加路径:
$(PROJECT_DIR)/Frameworks/ffmpeg确保选中 "Recursive"。
- 搜索 "Other Linker Flags":
点击 号,添加:
-lavcodec-lavformat-lavutil-lswscale(如果需要图像缩放等)-lswresample(如果需要音频重采样)- 注意:
-l后面跟的是库文件名去掉lib前缀和.a后缀。
- 注意:
你的项目已经配置好了,可以开始写代码了。
实战:编写第一个 FFmpeg 程序
目标:获取一个本地视频文件的基本信息(如时长、分辨率、编码格式等)。
-
准备视频文件 将一个
.mp4或.mov文件拖入 Xcode 项目,并确保 "Copy items if needed" 已勾选,Target 也已勾选。 -
代码实现 打开
ViewController.m,在viewDidLoad方法中添加以下代码:#import "ViewController.h" // 导入 FFmpeg 的头文件 #include <libavformat/avformat.h> #include <libavcodec/avcodec.h> #include <libavutil/avutil.h> @interface ViewController () @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; // 将视频文件名转换为 C 字符串 const char *filePath = [[NSBundle mainBundle].pathForResource @"test_video" ofType:@"mp4"].UTF8String; // 1. 注册所有组件和格式 av_register_all(); // 2. 打开输入文件 AVFormatContext *pFormatCtx = NULL; if (avformat_open_input(&pFormatCtx, filePath, NULL, NULL) != 0) { NSLog(@"无法打开文件: %s", filePath); return; } // 3. 获取流信息 if (avformat_find_stream_info(pFormatCtx, NULL) < 0) { NSLog(@"无法获取流信息"); avformat_close_input(&pFormatCtx); return; } // 4. 打印文件信息 av_dump_format(pFormatCtx, 0, filePath, 0); // 5. 关闭文件 avformat_close_input(&pFormatCtx); NSLog(@"FFmpeg 信息打印完成!"); } @end -
运行与调试 在模拟器或真机上运行 App,打开 Xcode 的调试控制台(View -> Debug Area -> Console),你将看到 FFmpeg 打印出的详细视频信息,证明了 FFmpeg 已经成功集成并运行。
进阶:视频压缩(转码)实战
目标:将输入视频压缩为指定码率的 H.264 MP4 文件。
这是一个更复杂的流程,涉及解复用、解码、编码、再复用等多个步骤。
关键 API 解析
avformat_open_input/avformat_close_input: 打开和关闭媒体文件。avformat_find_stream_info: 获取文件中的流信息。av_find_best_stream: 找到最佳的视频流。avcodec_find_decoder/avcodec_find_encoder: 查找解码器和编码器。avcodec_open2/avcodec_close: 打开和关闭编解码器。avformat_alloc_output_context2: 创建输出上下文。avio_open: 打开输出文件。avformat_write_header/av_interleaved_write_frame/av_write_trailer: 写入文件头、数据帧和文件尾。avcodec_send_packet/avcodec_receive_frame: 发送编码数据包到编码器,从编码器接收解码后的帧。av_frame_alloc/av_frame_free: 分配和释放 AVFrame 结构体。av_packet_alloc/av_packet_free: 分配和释放 AVPacket 结构体。
完整代码示例
这个例子比较长,但包含了转码的核心逻辑,请将代码添加到你的 ViewController.m 中,并创建一个转码方法。
// 在 ViewController.m 中添加
- (void)convertVideo {
NSString *inputPath = [[NSBundle mainBundle] pathForResource:@"test_video" ofType:@"mp4"];
NSString *outputPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject stringByAppendingPathComponent:@"output.mp4"];
const char *in_filename = inputPath.UTF8String;
const char *out_filename = outputPath.UTF8String;
AVFormatContext *pFormatCtx = NULL, *pOutFormatCtx = NULL;
AVStream *video_stream = NULL, *out_video_stream = NULL;
AVCodecContext *pCodecCtx = NULL, *pOutCodecCtx = NULL;
AVCodec *pCodec = NULL, *pOutCodec = NULL;
AVFrame *pFrame = NULL, *pFrameRGB = NULL;
AVPacket *packet = NULL;
int video_stream_index = -1;
int out_video_stream_index = -1;
int ret, got_picture, frame_count;
int width, height;
struct SwsContext *sws_ctx = NULL;
// 1. 打开输入文件
if (avformat_open_input(&pFormatCtx, in_filename, NULL, NULL) != 0) {
NSLog(@"Could not open input file.");
return;
}
if (avformat_find_stream_info(pFormatCtx, NULL) < 0) {
NSLog(@"Could not find stream information.");
return;
}
// 2. 查找视频流
for (int i = 0; i < pFormatCtx->nb_streams; i++) {
if (pFormatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
video_stream_index = i;
video_stream = pFormatCtx->streams[i];
break;
}
}
if (video_stream_index == -1) {
NSLog(@"Could not find video stream.");
return;
}
// 3. 获取解码器上下文
pCodecCtx = avcodec_alloc_context3(NULL);
if (!pCodecCtx) {
NSLog(@"Could not allocate codec context.");
return;
}
avcodec_parameters_to_context(pCodecCtx, video_stream->codecpar);
// 4. 查找并打开解码器
pCodec = avcodec_find_decoder(pCodecCtx->codec_id);
if (!pCodec) {
NSLog(@"Unsupported codec!");
return;
}
if (avcodec_open2(pCodecCtx, pCodec, NULL) < 0) {
NSLog(@"Could not open codec.");
return;
}
// 5. 设置输出
avformat_alloc_output_context2(&pOutFormatCtx, NULL, NULL, out_filename);
if (!pOutFormatCtx) {
NSLog(@"Could not create output context.");
return;
}
// 6. 查找编码器并创建输出流
pOutCodec = avcodec_find_encoder_by_name("libx264"); // 使用 x264 编码器
if (!pOutCodec) {
NSLog(@"Could not find x264 encoder.");
return;
}
out_video_stream = avformat_new_stream(pOutFormatCtx, pOutCodec);
if (!out_video_stream) {
NSLog:@"Failed allocating output stream.";
return;
}
pOutCodecCtx = avcodec_alloc_context3(pOutCodec);
if (!pOutCodecCtx) {
NSLog:@"Failed to allocate the encoder context.");
return;
}
// 设置编码器参数
pOutCodecCtx->codec_type = AVMEDIA_TYPE_VIDEO;
pOutCodecCtx->pix_fmt = AV_PIX_FMT_YUV420P; // x264 支持的像素格式
pOutCodecCtx->width = pCodecCtx->width;
pOutCodecCtx->height = pCodecCtx->height;
pOutCodecCtx->time_base = av_inv_q(pCodecCtx->framerate); // 设置时间基
pOutCodecCtx->framerate = pCodecCtx->framerate;
pOutCodecCtx->bit_rate = 500000; // 设置目标码率,500 kbps
pOutCodecCtx->gop_size = 12; // GOP大小
pOutCodecCtx->max_b_frames = 1;
if (pOutCodecCtx->codec_id == AV_CODEC_ID_H264) {
pOutCodecCtx->profile = FF_PROFILE_H264_BASELINE;
pOutCodecCtx->level = 30; // Level 3.0
}
// 打开编码器
if (avcodec_open2(pOutCodecCtx, pOutCodec, NULL) < 0) {
NSLog(@"Could not open output codec.");
return;
}
// 将编码器参数复制到输出流
avcodec_parameters_from_context(out_video_stream->codecpar, pOutCodecCtx);
out_video_stream->time_base = pOutCodecCtx->time_base;
// 打开输出文件
if (!(pOutFormatCtx->oformat->flags & AVFMT_NOFILE)) {
if (avio_open(&pOutFormatCtx->pb, out_filename, AVIO_FLAG_WRITE) < 0) {
NSLog(@"Could not open output file.");
return;
}
}
// 写入文件头
if (avformat_write_header(pOutFormatCtx, NULL) < 0) {
NSLog(@"Error occurred when opening output file.");
return;
}
// 7. 准备转换
width = pCodecCtx->width;
height = pCodecCtx->height;
pFrame = av_frame_alloc();
pFrameRGB = av_frame_alloc();
packet = av_packet_alloc();
// 由于原始帧可能是 YUV 或 RGB,我们统一转换为 RGB 以方便处理(这里简化处理,实际应直接处理 YUV)
// int numBytes = av_image_get_buffer_size(AV_PIX_FMT_RGB24, width, height, 1);
// uint8_t *buffer = (uint8_t *)av_malloc(numBytes*sizeof(uint8_t));
// av_image_fill_arrays(pFrameRGB->data, pFrameRGB->linesize, buffer, AV_PIX_FMT_RGB24, width, height, 1);
sws_ctx = sws_getContext(width, height, pCodecCtx->pix_fmt,
width, height, AV_PIX_FMT_YUV420P,
SWS_BILINEAR, NULL, NULL, NULL);
// 8. 读取、解码、编码、写入
frame_count = 0;
while (av_read_frame(pFormatCtx, packet) >= 0) {
if (packet->stream_index == video_stream_index) {
// 发送数据包到解码器
ret = avcodec_send_packet(pCodecCtx, packet);
if (ret < 0) continue;
// 从解码器接收帧
while (ret >= 0) {
ret = avcodec_receive_frame(pCodecCtx, pFrame);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) break;
if (ret < 0) {
NSLog(@"Error during decoding.");
break;
}
// 转换格式(如果需要)
// sws_scale(sws_ctx, pFrame->data, pFrame->linesize, 0, height, pFrameRGB->data, pFrameRGB->linesize);
// 编码
avcodec_send_frame(pOutCodecCtx, pFrame);
while (ret >= 0) {
ret = avcodec_receive_frame(pOutCodecCtx, pFrame);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) break;
if (ret < 0) {
NSLog(@"Error during encoding.");
break;
}
// 写入输出文件
av_packet_unref(packet);
av_init_packet(packet);
if (avcodec_send_frame(pOutCodecCtx, pFrame) < 0) {
NSLog(@"Error sending frame to encoder.");
}
while (avcodec_receive_packet(pOutCodecCtx, packet) >= 0) {
av_packet_rescale_ts(packet, pOutCodecCtx->time_base, out_video_stream->time_base);
packet->stream_index = out_video_stream_index;
av_interleaved_write_frame(pOutFormatCtx, packet);
}
}
av_frame_unref(pFrame);
}
}
av_packet_unref(packet);
}
// 9. 写入文件尾并清理资源
av_write_trailer(pOutFormatCtx);
if (pOutFormatCtx && !(pOutFormatCtx->oformat->flags & AVFMT_NOFILE)) {
avio_closep(&pOutFormatCtx->pb);
}
avformat_free_context(pOutFormatCtx);
avcodec_free_context(&pCodecCtx);
avcodec_free_context(&pOutCodecCtx);
av_frame_free(&pFrame);
av_frame_free(&pFrameRGB);
av_packet_free(&packet);
sws_freeContext(sws_ctx);
NSLog(@"视频转码完成,输出文件路径: %@", outputPath);
}
然后在 viewDidLoad 中调用 [self convertVideo];。
重要注意事项
- 性能与线程:FFmpeg 的 API 不是线程安全的,所有操作(打开、读取、解码、编码、写入)都应该在同一个串行队列中完成,不要在主线程上进行耗时操作,否则会导致 App 卡顿。
- 内存管理:FFmpeg 使用 C 语言,没有 ARC,所有通过
av_malloc,avformat_open_input等函数分配的内存,都必须通过对应的av_free,avformat_close_input等函数手动释放,内存泄漏是 FFmpeg 新手最常遇到的问题。 - 安全与隐私:如果你的 App 需要访问相册或文件,请在
Info.plist中添加相应的权限描述,如Privacy - Photo Library Additions Usage Description。 - App Store 审核指南:FFmpeg 本身是开源的,但它的某些组件(如
libx264)可能涉及专利,虽然 Apple 官方文档中提到可以使用这些库,但在提交审核时,如果被问到,需要能解释清楚你的代码是合法合规的,只要你的 App 功能正常,不会有太大问题。
学习资源与总结
- 官方文档:FFmpeg Documentation 是最权威的资料,虽然有些晦涩,但必须查阅。
- FFmpeg 官方示例:FFmpeg libavutil, libavcodec, and libavformat API usage examples 包含了大量代码示例,是学习的最佳材料。
- 博客和教程:搜索 "iOS FFmpeg tutorial" 可以找到很多优秀的第三方教程。
在 iOS 上使用 FFmpeg 是一个强大但复杂的过程,核心步骤可以概括为:
- 编译:为 iOS 目标架构编译出静态库。
- 集成:将库和头文件正确添加到 Xcode 项目并配置 Build Settings。
- 编码:理解 FFmpeg 的“解复用 -> 解码 -> (处理) -> 编码 -> 复用”的数据流,并使用 GCD 在后台线程安全地执行这些操作。
- 调试:利用 Xcode 的调试工具和 FFmpeg 的日志功能,仔细检查内存泄漏和错误码。
希望这份详尽的教程能帮助你顺利地在 iOS 项目中上手 FFmpeg!
