(十六)深入了解 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

相关推荐
朱古力(音视频开发)4 小时前
NDI开发指南
fpga开发·音视频·实时音视频·视频编解码·流媒体
科技资讯快报10 小时前
法国声学智慧 ,音响品牌SK (SINGKING AUDIO) 重构专业音频边界
重构·音视频
云霄IT12 小时前
python之使用ffmpeg下载直播推流视频rtmp、m3u8协议实时获取时间进度
python·ffmpeg·音视频
WSSWWWSSW15 小时前
Jupyter Notebook 中显示图片、音频、视频的方法汇总
ide·人工智能·jupyter·音视频·python notebook
sukalot19 小时前
window显示驱动开发—Direct3D 11 视频播放改进
驱动开发·音视频
ls_qq_267081347020 小时前
cocos打包web - ios设备息屏及前后台切换音频播放问题
前端·ios·音视频·cocos-creator
科技资讯快报1 天前
法式基因音响品牌SK(SINGKING AUDIO)如何以硬核科技重塑专业音频版图
科技·音视频
天天向上10241 天前
vue2 使用liveplayer加载视频
音视频
WSSWWWSSW1 天前
华为昇腾NPU卡 文生视频[T2V]大模型WAN2.1模型推理使用
人工智能·大模型·音视频·显卡·文生视频·文生音频·文生音乐