(十六)深入了解 AVFoundation - 编辑:音视频裁剪与拼接的Demo项目实现

引言

在上一篇文章中,我们通过最基础的方式实现了视频的时间裁剪多段拼接,了解了 AVFoundation 在处理音视频编辑时的强大能力。然而,如果将这些能力真正应用到实际项目中,我们很快就会发现 ------ 简单的代码片段远远不够。

我们需要一个可维护、可扩展、可复用的系统:

  • 如何组织多个视频或音频片段?
  • 如何统一处理播放与导出?
  • 如何将"剪辑结构"独立出来,方便后续添加滤镜、特效或转场?

为此,我构建了一个轻量、清晰的剪辑导出系统 Demo,基于 AVFoundation + 面向协议的架构设计,实现了基本的播放与导出功能,同时也为未来的功能扩展(如滤镜叠加、字幕合成、多轨编辑)留下了充足空间。

本篇文章将带你逐步拆解这个 Demo 的核心模块设计与实现,理解其中的架构思路与关键代码。无论你是希望做出一个短视频剪辑工具,还是只是想对 AVFoundation 有更深理解,相信这篇内容都能为你提供实用的参考。

一、系统结构概览

我们希望构建一个具备播放和导出能力的剪辑系统 ,并且具备良好的扩展性。因此,整个系统采用了协议驱动的分层架构,将核心功能划分为三个模块:

1. 媒体资源模型:PHMediaItem

用于描述每一段要参与编辑的音视频素材,是整个系统的输入单元。它抽象了素材的基本信息,如资源本体(AVAsset)和剪辑区间(CMTimeRange)。

派生类:

  • PHVideoItem:用于表示视频片段;
  • PHAudioItem:用于表示音频片段。

后续可以在这层添加视频音量、滤镜参数、速度调整等编辑信息。

2. 剪辑结构协议:PHComposition

定义了系统中最重要的两个能力:

  • makePlayableItem():构建一个可直接用于 AVPlayer 播放的 AVPlayerItem;
  • makeExportSession(to:):构建一个用于导出的 AVAssetExportSession 实例。

也就是说,所有的编辑结构(Composition)都需要实现"可播放"和"可导出"的能力

我们提供了一个默认实现类 PHBasicComposition,用于构建最基础的时间线拼接结构(只做剪辑与拼接,不加特效)。

3. 构建器协议:PHCompositionBuilder

为了实现解耦,我们定义了一个构建器协议 PHCompositionBuilder,它的职责是根据媒体资源生成一个 PHComposition 实例:

Swift 复制代码
protocol PHCompositionBuilder {
    func buildComposition() -> PHComposition
}

对应的默认实现是 PHBasicCompositionBuilder,它接收一个媒体时间线(PHTimeline)并构建出一个 PHBasicComposition。
模块关系图

通过这种结构划分,我们做到了:

  • 职责清晰:每个类/协议只负责一件事;
  • 解耦灵活:后续可以自由替换不同的 CompositionBuilder 或 Composition;
  • 易于扩展:未来添加滤镜、转场、字幕、变速等功能时,只需要在对应模块中扩展即可,不影响系统其他部分。

接下来,我们将从输入模型 PHMediaItem 开始,一步步解析每个模块的实现方式和背后的设计考量。

二、PHMediaItem:统一的媒体资源描述

在音视频剪辑中,我们通常需要处理多个不同的媒体资源:视频、背景音乐、人声、效果音......

为了统一处理这些资源,系统中定义了一个基础模型类:PHMediaItem。

2.1 基类定义

Swift 复制代码
class PHMediaItem {
    /// 资源标题,仅用于标识或调试
    var title: String?
    
    /// 媒体资源(视频或音频)
    var asset: AVAsset?
    
    /// 参与剪辑的时间范围(支持裁剪)
    var timeRange: CMTimeRange = .zero
}

这个类封装了编辑过程中需要关心的最核心信息

  • asset 是原始媒体资源,可以来自本地视频、录音文件、照片 Live Photo 等;
  • timeRange 表示从这个资源中截取哪一段进行参与(默认为 .zero 可视为未设定);
  • title 虽非必需,但便于调试或用于展示。

示例:我们可以定义一个视频资源,裁剪其第 3~8 秒参与拼接:

Swift 复制代码
let videoItem = PHVideoItem()
videoItem.title = "开场动画"
videoItem.asset = AVAsset(url: url)
videoItem.timeRange = CMTimeRange(start: .seconds(3), duration: .seconds(5))

2.2 子类:视频和音频的区分

为了在后续组合中按类型添加到不同轨道(视频轨 / 音频轨),我们为 PHMediaItem 增加了两个空实现的子类:

Swift 复制代码
/// 视频资源
class PHVideoItem: PHMediaItem {}

/// 音频资源
class PHAudioItem: PHMediaItem {}

虽然目前这两个子类没有添加额外字段,但这样设计有以下优势:

  • 类型明确:在处理时可通过类型判断来区分媒体轨道;
  • 扩展空间大:后续可在 PHVideoItem 中加入滤镜、缩放、变速参数,在 PHAudioItem 中加入淡入淡出、音量、循环等;
  • 逻辑隔离:不同媒体类型拥有自己的配置,代码结构更清晰。

三、PHComposition 协议:定义输出能力

在上一节中,我们定义了 PHMediaItem 作为统一的媒体输入模型。接下来要解决的问题是 ------ 如何将这些媒体片段组合成一个可播放、可导出的作品?

为此,我们定义了一个关键协议:PHComposition。

3.1 协议定义

Swift 复制代码
protocol PHComposition {
    /// 构建可用于 AVPlayer 播放的 AVPlayerItem
    func makePlayableItem() -> AVPlayerItem?
    
    /// 构建用于导出的 AVAssetExportSession
    func makeExportSession(to outputURL: URL) -> AVAssetExportSession?
}

通过这个协议,我们明确约定了两个最核心的功能:

  • makePlayableItem():将编辑结果转换成 AVPlayerItem,用于实时预览;
  • makeExportSession(to:):生成一个可执行导出的 AVAssetExportSession,并可指定输出地址。

这两个接口成为整个系统向"用户端"输出的唯一窗口。

3.2 默认实现:PHBaseComposition

Swift 复制代码
class PHBaseComposition: NSObject, PHComposition {
    
    private let composition: AVMutableComposition
    
    init(compostion: AVMutableComposition) {
        self.composition = compostion
    }
    
    func makePlayableItem() -> AVPlayerItem? {
        return AVPlayerItem(asset: composition)
    }
    
    func makeExportSession(to outputURL: URL) -> AVAssetExportSession? {
        let session = AVAssetExportSession(asset: composition, presetName: AVAssetExportPresetHighestQuality)
        session?.outputURL = outputURL
        session?.outputFileType = .mp4
        return session
    }
}

该实现类接收一个已拼接好的 AVMutableComposition 实例,并通过封装提供标准的播放和导出接口。

后续如需实现滤镜、水印等特殊效果,可扩展新的 PHComposition 实现类,保持输出接口不变。

3.3 接口的好处

定义 PHComposition 协议而非直接暴露 AVMutableComposition 有三大优势:

  1. 封装稳定输出接口:无论后端结构如何变化,播放与导出接口始终一致;
  2. 便于扩展和替换:后续可以实现多个 PHComposition,用于不同场景(如特效视频、转场合成等);
  3. 更好的测试与解耦:调用端无需关心内部轨道结构,只需使用协议即可完成播放或导出。

四、PHCompositionBuilder:组合器的实现与职责

在结构上,PHMediaItem 是输入,PHComposition 是输出,而中间这一步------如何将素材拼接起来,恰恰由 组合器(Builder) 负责完成。

4.1 协议定义

我们为组合逻辑定义了一个协议 PHCompositionBuilder:

Swift 复制代码
protocol PHCompositionBuilder {
    /// 构建出最终的 Composition 实例
    func buildComposition() -> PHComposition?
}

这个协议的职责非常单一:根据一组素材,输出一个可播放 / 可导出的剪辑结构

4.2 默认实现:PHBaseCompositionBuilder

我们在 Demo 中提供了一个默认实现类:PHBaseCompositionBuilder,用于实现基础的"顺序拼接"功能。

Swift 复制代码
class PHBaseCompositionBuilder: NSObject, PHCompositionBuilder {

    /// 输入时间线(含视频、音频资源)
    var timeLine: PHTimeLine

    /// 内部组合结果
    private var composition = AVMutableComposition()

    init(timeLine: PHTimeLine) {
        self.timeLine = timeLine
    }

    func buildComposition() -> PHComposition? {
        // 添加视频轨道
        addCompositionTrack(mediaType: .video, mediaItems: timeLine.videoItmes)
        // 添加音频轨道
        addCompositionTrack(mediaType: .audio, mediaItems: timeLine.audioItems)
        
        return PHBaseComposition(compostion: self.composition)
    }
}

4.3 核心拼接逻辑:addCompositionTrack

Swift 复制代码
private func addCompositionTrack(mediaType: AVMediaType, mediaItems: [PHMediaItem]?) {
    guard let mediaItems = mediaItems, !mediaItems.isEmpty else { return }

    guard let compositionTrack = composition.addMutableTrack(withMediaType: mediaType,
                                                              preferredTrackID: kCMPersistentTrackID_Invalid) else { return }

    var cursorTime = CMTime.zero

    for item in mediaItems {
        guard let asset = item.asset,
              let assetTrack = asset.tracks(withMediaType: mediaType).first else { continue }

        do {
            try compositionTrack.insertTimeRange(item.timeRange, of: assetTrack, at: cursorTime)
        } catch {
            print("insert error: \(error)")
        }

        // 累加时间:顺序拼接
        cursorTime = CMTimeAdd(cursorTime, item.timeRange.duration)
    }
}

这个方法的逻辑非常清晰:

  • 依次读取每个素材的剪辑区间;
  • 将其插入到对应轨道的时间线上;
  • 按顺序向后推进,完成"顺序拼接"。

4.4 时间线:PHTimeLine 的作用

为了结构清晰,我们在 PHBaseCompositionBuilder 中引入了一个时间线模型 PHTimeLine:

Swift 复制代码
class PHTimeLine: NSObject {
    /// 视频资源数组
    var videoItmes = [PHVideoItem]()
    /// 音频资源数组
    var audioItems = [PHAudioItem]()
    
}

将所有素材归类管理,并作为构建器的输入源。这样可以做到:

  • 素材集中管理,类型清晰分离
  • 方便后期做 UI 拖拽编辑
  • 利于时间线可视化、序列化保存等扩展需求

至此,我们完成了从媒体素材到最终合成结构的关键路径:

PHTimeLine → PHCompositionBuilder → PHComposition

整个流程封装清晰、职责明确,便于后续功能迭代和结构扩展。

五、PHComposition 的播放与导出

前几节中我们通过 PHCompositionBuilder 构建出了一个符合 PHComposition 协议的实例。在这个协议中,我们预留了两个至关重要的接口:

Swift 复制代码
protocol PHComposition {
    func makePlayableItem() -> AVPlayerItem?
    func makeExportSession(to outputURL: URL) -> AVAssetExportSession?
}

这一节将分别说明这两个方法的使用方式,以及它们在项目中的实际意义。

5.1 播放:makePlayableItem

播放功能是我们最常用于预览剪辑结果的形式,makePlayableItem() 方法返回一个标准的 AVPlayerItem,可以直接交给 AVPlayer 使用。

示例用法:

Swift 复制代码
if let playerItem = composition.makePlayableItem() {
    let player = AVPlayer(playerItem: playerItem)
    player.play()
}

这种方式适合:

  • 实时预览剪辑效果;
  • 为用户提供拖拽时间线后的反馈;
  • 播放拼接完成但未导出的临时作品。

5.2 导出:makeExportSession

最终如果用户希望将编辑结果保存为一个完整的视频文件,我们就要使用 makeExportSession(to:) 方法,返回一个 AVAssetExportSession 实例:

示例用法:

Swift 复制代码
if let exportSession = composition.makeExportSession(to: outputURL) {
    exportSession.exportAsynchronously {
        switch exportSession.status {
        case .completed:
            print("导出完成:\(outputURL)")
        case .failed:
            print("导出失败:\(exportSession.error?.localizedDescription ?? "未知错误")")
        default:
            break
        }
    }
}

默认实现中我们使用的导出配置是:

Swift 复制代码
AVAssetExportPresetHighestQuality

输出格式则设为 .mp4,适配性和播放兼容性都很好。你也可以根据需求自定义导出配置,例如压缩、转码为 HEVC 等。

5.3 为什么封装成协议?

将播放和导出都封装进 PHComposition 的协议,有如下优势:

  • 统一使用接口:无论底层是怎样实现的剪辑逻辑(视频拼接 / 多轨混合 / 特效渲染),调用层始终只关心这两个输出;
  • 拓展性好:未来可实现更多 PHComposition 子类,如 PHFilterComposition、PHTransitionComposition 等,实现滤镜/转场逻辑但仍复用相同播放与导出方式;
  • 利于解耦和测试:开发中可以随时替换不同组合实现进行测试,而不影响使用逻辑。

5.4 可选扩展点

如果你希望让导出支持:

  • 进度监听;
  • 可取消任务;
  • 支持 AVVideoComposition(加滤镜、旋转等);

你完全可以在 PHBaseComposition 中重写 makeExportSession 方法,嵌入更多配置项,实现更丰富的导出策略。

Demo地址:https://download.csdn.net/download/weixin_39339407/91058307

相关推荐
iphone1084 小时前
单视频二维码生成与列表二维码生成(完整版)
音视频·视频转二维码·视频二维码·视频生成二维码
苗杨6 小时前
【Faster-Whisper】离线识别本地视频并生成字幕
python·whisper·音视频
Silicore_Emma8 小时前
芯谷科技--双通道音频功率放大器D2025
科技·音视频·车载音响·双通道·功率放大
weixin_4462608514 小时前
苹果新专利!XR视频传输效率提升40%
音视频·xr
Everbrilliant8915 小时前
音视频之H.264视频编码传输及其在移动通信中的应用
音视频·h.264·h.264视频编码传输·h.264移动通信的应用·h.264容错技术·h.264精确码率控制算法
蹦极的考拉1 天前
在使用 HTML5 的 <video> 标签嵌入视频时,有时会遇到无法播放 MP4 文件的问题
前端·音视频·html5
SuperW1 天前
RV1126+OPENCV在视频中添加时间戳
人工智能·opencv·音视频
二蛋和他的大花1 天前
鸿蒙运动开发实战:打造专属运动视频播放器
华为·音视频·harmonyos
趣浪吧1 天前
【JSON-To-Video】AI智能体开发:为视频图片元素添加动效(滑入、旋转、滑出),附代码
人工智能·ai·aigc·音视频·视频