(十六)深入了解 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 实例:
protocol PHCompositionBuilder {func buildComposition() -> PHComposition
}
对应的默认实现是 PHBasicCompositionBuilder,它接收一个媒体时间线(PHTimeline)并构建出一个 PHBasicComposition。

通过这种结构划分,我们做到了:
- 职责清晰:每个类/协议只负责一件事;
- 解耦灵活:后续可以自由替换不同的 CompositionBuilder 或 Composition;
- 易于扩展:未来添加滤镜、转场、字幕、变速等功能时,只需要在对应模块中扩展即可,不影响系统其他部分。
接下来,我们将从输入模型 PHMediaItem 开始,一步步解析每个模块的实现方式和背后的设计考量。
二、PHMediaItem:统一的媒体资源描述
在音视频剪辑中,我们通常需要处理多个不同的媒体资源:视频、背景音乐、人声、效果音……
为了统一处理这些资源,系统中定义了一个基础模型类:PHMediaItem。
2.1 基类定义
class PHMediaItem {/// 资源标题,仅用于标识或调试var title: String?/// 媒体资源(视频或音频)var asset: AVAsset?/// 参与剪辑的时间范围(支持裁剪)var timeRange: CMTimeRange = .zero
}
这个类封装了编辑过程中需要关心的最核心信息:
- asset 是原始媒体资源,可以来自本地视频、录音文件、照片 Live Photo 等;
- timeRange 表示从这个资源中截取哪一段进行参与(默认为 .zero 可视为未设定);
- title 虽非必需,但便于调试或用于展示。
示例:我们可以定义一个视频资源,裁剪其第 3~8 秒参与拼接:
let videoItem = PHVideoItem()
videoItem.title = "开场动画"
videoItem.asset = AVAsset(url: url)
videoItem.timeRange = CMTimeRange(start: .seconds(3), duration: .seconds(5))
2.2 子类:视频和音频的区分
为了在后续组合中按类型添加到不同轨道(视频轨 / 音频轨),我们为 PHMediaItem 增加了两个空实现的子类:
/// 视频资源
class PHVideoItem: PHMediaItem {}/// 音频资源
class PHAudioItem: PHMediaItem {}
虽然目前这两个子类没有添加额外字段,但这样设计有以下优势:
- ✅ 类型明确:在处理时可通过类型判断来区分媒体轨道;
- ✅ 扩展空间大:后续可在 PHVideoItem 中加入滤镜、缩放、变速参数,在 PHAudioItem 中加入淡入淡出、音量、循环等;
- ✅ 逻辑隔离:不同媒体类型拥有自己的配置,代码结构更清晰。
三、PHComposition 协议:定义输出能力
在上一节中,我们定义了 PHMediaItem 作为统一的媒体输入模型。接下来要解决的问题是 —— 如何将这些媒体片段组合成一个可播放、可导出的作品?
为此,我们定义了一个关键协议:PHComposition。
3.1 协议定义
protocol PHComposition {/// 构建可用于 AVPlayer 播放的 AVPlayerItemfunc makePlayableItem() -> AVPlayerItem?/// 构建用于导出的 AVAssetExportSessionfunc makeExportSession(to outputURL: URL) -> AVAssetExportSession?
}
通过这个协议,我们明确约定了两个最核心的功能:
- makePlayableItem():将编辑结果转换成 AVPlayerItem,用于实时预览;
- makeExportSession(to:):生成一个可执行导出的 AVAssetExportSession,并可指定输出地址。
这两个接口成为整个系统向“用户端”输出的唯一窗口。
3.2 默认实现:PHBaseComposition
class PHBaseComposition: NSObject, PHComposition {private let composition: AVMutableCompositioninit(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 = outputURLsession?.outputFileType = .mp4return session}
}
该实现类接收一个已拼接好的 AVMutableComposition 实例,并通过封装提供标准的播放和导出接口。
后续如需实现滤镜、水印等特殊效果,可扩展新的 PHComposition 实现类,保持输出接口不变。
3.3 接口的好处
定义 PHComposition 协议而非直接暴露 AVMutableComposition 有三大优势:
- ✅ 封装稳定输出接口:无论后端结构如何变化,播放与导出接口始终一致;
- ✅ 便于扩展和替换:后续可以实现多个 PHComposition,用于不同场景(如特效视频、转场合成等);
- ✅ 更好的测试与解耦:调用端无需关心内部轨道结构,只需使用协议即可完成播放或导出。
四、PHCompositionBuilder:组合器的实现与职责
在结构上,PHMediaItem 是输入,PHComposition 是输出,而中间这一步——如何将素材拼接起来,恰恰由 组合器(Builder) 负责完成。
4.1 协议定义
我们为组合逻辑定义了一个协议 PHCompositionBuilder:
protocol PHCompositionBuilder {/// 构建出最终的 Composition 实例func buildComposition() -> PHComposition?
}
这个协议的职责非常单一:根据一组素材,输出一个可播放 / 可导出的剪辑结构。
4.2 默认实现:PHBaseCompositionBuilder
我们在 Demo 中提供了一个默认实现类:PHBaseCompositionBuilder,用于实现基础的“顺序拼接”功能。
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
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.zerofor 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:
class PHTimeLine: NSObject {/// 视频资源数组var videoItmes = [PHVideoItem]()/// 音频资源数组var audioItems = [PHAudioItem]()}
将所有素材归类管理,并作为构建器的输入源。这样可以做到:
- 素材集中管理,类型清晰分离;
- 方便后期做 UI 拖拽编辑;
- 利于时间线可视化、序列化保存等扩展需求。
至此,我们完成了从媒体素材到最终合成结构的关键路径:
PHTimeLine → PHCompositionBuilder → PHComposition
整个流程封装清晰、职责明确,便于后续功能迭代和结构扩展。
五、PHComposition 的播放与导出
前几节中我们通过 PHCompositionBuilder 构建出了一个符合 PHComposition 协议的实例。在这个协议中,我们预留了两个至关重要的接口:
protocol PHComposition {func makePlayableItem() -> AVPlayerItem?func makeExportSession(to outputURL: URL) -> AVAssetExportSession?
}
这一节将分别说明这两个方法的使用方式,以及它们在项目中的实际意义。
5.1 播放:makePlayableItem
播放功能是我们最常用于预览剪辑结果的形式,makePlayableItem() 方法返回一个标准的 AVPlayerItem,可以直接交给 AVPlayer 使用。
示例用法:
if let playerItem = composition.makePlayableItem() {let player = AVPlayer(playerItem: playerItem)player.play()
}
这种方式适合:
- 实时预览剪辑效果;
- 为用户提供拖拽时间线后的反馈;
- 播放拼接完成但未导出的临时作品。
5.2 导出:makeExportSession
最终如果用户希望将编辑结果保存为一个完整的视频文件,我们就要使用 makeExportSession(to:) 方法,返回一个 AVAssetExportSession 实例:
示例用法:
if let exportSession = composition.makeExportSession(to: outputURL) {exportSession.exportAsynchronously {switch exportSession.status {case .completed:print("导出完成:\(outputURL)")case .failed:print("导出失败:\(exportSession.error?.localizedDescription ?? "未知错误")")default:break}}
}
默认实现中我们使用的导出配置是:
AVAssetExportPresetHighestQuality
输出格式则设为 .mp4,适配性和播放兼容性都很好。你也可以根据需求自定义导出配置,例如压缩、转码为 HEVC 等。
5.3 为什么封装成协议?
将播放和导出都封装进 PHComposition 的协议,有如下优势:
- 统一使用接口:无论底层是怎样实现的剪辑逻辑(视频拼接 / 多轨混合 / 特效渲染),调用层始终只关心这两个输出;
- 拓展性好:未来可实现更多 PHComposition 子类,如 PHFilterComposition、PHTransitionComposition 等,实现滤镜/转场逻辑但仍复用相同播放与导出方式;
- 利于解耦和测试:开发中可以随时替换不同组合实现进行测试,而不影响使用逻辑。
5.4 可选扩展点
如果你希望让导出支持:
- 进度监听;
- 可取消任务;
- 支持 AVVideoComposition(加滤镜、旋转等);
你完全可以在 PHBaseComposition 中重写 makeExportSession 方法,嵌入更多配置项,实现更丰富的导出策略。
Demo地址:https://download.csdn.net/download/weixin_39339407/91058307