杰瑞科技汇

iOS下FFmpeg怎么用?新手入门教程?

目录

  1. 为什么要在 iOS 上使用 FFmpeg?
  2. 准备工作:安装 Homebrew 和 FFmpeg
  3. 核心步骤:为 iOS 编译 FFmpeg
    • 为什么需要自己编译?
    • 编译脚本详解
    • 编译与常见问题
  4. 将编译好的库集成到 Xcode 项目
    • 创建新项目
    • 添加文件和配置
    • 链接库和设置 Header Search Paths
  5. 实战:编写第一个 FFmpeg 程序
    • 目标:获取视频文件信息
    • 代码实现
    • 运行与调试
  6. 进阶:视频压缩(转码)实战
    • 目标:将视频压缩为 H.264 格式
    • 关键 API 解析
    • 完整代码示例
  7. 重要注意事项
    • 性能与线程:GCD 的使用
    • 内存管理av_freeavformat_close_input
    • 安全与隐私Info.plist 配置
    • App Store 审核指南:关于加密库的说明
  8. 学习资源与总结

为什么要在 iOS 上使用 FFmpeg?

iOS 系统自带的 AVFoundation 框架功能强大,能满足大部分音视频播放、录制和编辑的需求,但在以下场景,FFmpeg 依然是不可替代的利器:

iOS下FFmpeg怎么用?新手入门教程?-图1
(图片来源网络,侵删)
  • 极致的格式支持:支持几乎所有已知的音视频格式、编码器和容器格式,而 AVFoundation 只支持主流的几种。
  • 高度定制的编解码:可以精确控制编码参数(如码率、帧率、分辨率、编码Profile/Level),实现更精细的视频压缩或优化。
  • 强大的处理能力:提供复杂的音视频处理功能,如滤镜、流媒体推拉、格式转换等,这些在 AVFoundation 中实现起来非常复杂。
  • 服务器端逻辑迁移:如果你的后端服务是用 FFmpeg 处理视频,为了保持逻辑一致性,也可以将其部分功能迁移到客户端。

准备工作:安装 Homebrew 和 FFmpeg

在编译之前,我们需要在 Mac 上安装 FFmpeg 的命令行工具,这可以帮助我们了解其功能和参数。

  1. 安装 Homebrew (如果尚未安装) 打开终端,运行以下命令:

    /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
  2. 安装 FFmpeg 安装完成后,使用 Homebrew 安装 FFmpeg:

    brew install ffmpeg

现在你可以在终端里使用 ffmpeg -version 来查看安装是否成功。

iOS下FFmpeg怎么用?新手入门教程?-图2
(图片来源网络,侵删)

核心步骤:为 iOS 编译 FFmpeg

为什么需要自己编译?

苹果的 iOS 系统是闭源的,并且有严格的架构限制(如 arm64, arm64-simulator),FFmpeg 官方提供的预编译版本通常只包含通用的 x86_64 架构,无法在真机上运行,我们必须为 iOS 的目标架构(arm64, arm64-simulator, x86_64)手动编译出对应的库文件(.a 文件)。

编译脚本详解

手动编译 FFmpeg 非常繁琐,推荐使用现成的编译脚本,这里我们使用 tmm1/homebrew-ffmpeg 项目中的脚本,它非常流行且可靠。

  1. 克隆脚本 在终端中,进入你希望存放代码的目录,然后克隆:

    iOS下FFmpeg怎么用?新手入门教程?-图3
    (图片来源网络,侵删)
    git clone https://github.com/tmm1/homebrew-ffmpeg.git
    cd homebrew-ffmpeg
  2. 修改编译配置(可选但推荐) 脚本默认会编译一个非常全面的 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

    注意x264x265 是非常优秀的 H.264 和 H.265 软件编码库,如果你需要进行视频压缩,强烈建议保留它们。

  3. 运行编译脚本 脚本会自动检测你连接的 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 项目中。

  1. 创建新项目 打开 Xcode,创建一个新的 "Single View App" 项目,命名为 FFmpegDemo

  2. 添加文件和配置

    • 在 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 文件夹下)。

  3. 链接库和设置 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 程序

目标:获取一个本地视频文件的基本信息(如时长、分辨率、编码格式等)。

  1. 准备视频文件 将一个 .mp4.mov 文件拖入 Xcode 项目,并确保 "Copy items if needed" 已勾选,Target 也已勾选。

  2. 代码实现 打开 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
  3. 运行与调试 在模拟器或真机上运行 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 功能正常,不会有太大问题。

学习资源与总结

在 iOS 上使用 FFmpeg 是一个强大但复杂的过程,核心步骤可以概括为:

  1. 编译:为 iOS 目标架构编译出静态库。
  2. 集成:将库和头文件正确添加到 Xcode 项目并配置 Build Settings。
  3. 编码:理解 FFmpeg 的“解复用 -> 解码 -> (处理) -> 编码 -> 复用”的数据流,并使用 GCD 在后台线程安全地执行这些操作。
  4. 调试:利用 Xcode 的调试工具和 FFmpeg 的日志功能,仔细检查内存泄漏和错误码。

希望这份详尽的教程能帮助你顺利地在 iOS 项目中上手 FFmpeg!

分享:
扫描分享到社交APP
上一篇
下一篇