当前位置: 首页 > news >正文

(十六)深入了解 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 有三大优势:

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

四、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

相关文章:

  • 如何将文件从安卓设备传输到电脑?
  • 如何使用 USB 数据线将文件从 PC 传输到 iPhone
  • 编程马拉松的定义、运作与发展
  • Linux内存进阶
  • OpenAI的Prompt工程
  • ABP VNext + MongoDB 数据存储:多模型支持与 NoSQL 扩展
  • 动态规划算法思路详解
  • 基于微信小程序在肠造口病人健康宣教中的应用
  • C#编程与1200PLC S7通信
  • 单点登录我们的
  • AI正在重构SaaS的底层逻辑:从“买软件”到“写软件”的范式转移
  • C++ std::set的用法
  • ​​​​​​​神经网络基础讲解 一
  • Python爬虫(六):Scrapy框架
  • 深入解析connect函数:阻塞与非阻塞模式下的行为差异
  • SQL注入安全研究
  • 在 Mac 上配置 Charles,抓取 iOS 手机端接口请求
  • 机器学习赋能多尺度材料模拟:前沿技术会议邀您共探
  • 【深度学习】生成对抗网络(GANs)深度解析:从理论到实践的革命性生成模型
  • buildroot 升级 OPENSSH
  • 国土分局网站建设方案/中国十大流量网站
  • 网站建设 英语词汇/百度一下就一个
  • wordpress输入命令/榆林市网站seo
  • ps如何做音乐网站/找客户资源的软件哪个最靠谱
  • 怎样能创建一个网站/疫情优化调整
  • 网页打不开但是有网什么原因win10/百度seo整站优化