(二十二)深入了解AVFoundation-编辑:视频变速功能-实战在Demo中实现视频变速
一. 引言
视频变速(Speed Ramp)是视频编辑中最常见的特效之一:
- 慢动作(Slow Motion):强调细节,让观众捕捉到肉眼难以察觉的瞬间;
- 快动作(Fast Motion):压缩时长,强化节奏,常用于 vlog、综艺片段。
在 AVFoundation 中,视频变速的本质是 对音视频轨道的时间线进行重新映射。通过调整时间范围(CMTimeRange)与目标时长(toDuration),即可让视频和音频实现同步的快进或慢放效果。
本文将通过一个完整的 Demo 实战,展示如何在 iOS 中使用 AVFoundation 实现视频的变速处理,涵盖从模型设计到合成器构建的完整流程。
二. 核心思路回顾
要实现变速,必须同时处理 视频轨道 与 音频轨道,以保证二者的同步:
1. 视频轨道
- 借助 AVMutableVideoCompositionInstruction 和 AVMutableVideoCompositionLayerInstruction,保证变速后的视频能够正常渲染。
- 关键在于:
- videoTrack.scaleTimeRange():调整视频的时间范围到新的时长;
- instruction.timeRange:指定变速后的可见区间;
- videoComposition:控制整体渲染(帧率、尺寸等)。
2. 音频轨道
- 音频不涉及渲染和图层,处理方式更简单。
- 只需调用 audioTrack.scaleTimeRange(),将某一时间段的音频拉伸或压缩到新的时长,从而实现变速效果。
三. Demo 架构设计
在本次 Demo 中,我们延续前文的 时间线(TimeLine)驱动合成 的设计思路。通过定义模型与 Builder,将变速逻辑解耦,使其可以灵活扩展。
1. PHSpeedItem —— 变速模型
class PHSpeedItem: PHMediaItem {/// 变速的倍速var scaleSpeed: Float = 2.0
}
PHSpeedItem 继承自 PHMediaItem,用于描述一段变速操作。它包含了:
- startTime:变速的起始时间点;
- timeRange:变速的持续时长;
- scaleSpeed:变速的倍速(例如 2.0 表示慢两倍,0.5 表示快两倍)。
这样,我们就能精确地定义:从视频的某个时间点开始,持续多少秒,需要以什么速度播放。
2. PHTimeLine —— 时间线模型
class PHTimeLine: NSObject {var videoItmes = [PHVideoItem]()var audioItems = [PHAudioItem]()var musicItems = [PHMusicItem]()var maskItem = PHMaskItem(text: "PHVideoExample",image: UIImage(named: "mask1"),bounds: CGRect(x: 0, y: 0, width: 1280, height: 720))var seepItems = [PHSpeedItem]()
}
PHTimeLine 是整个编辑流程的核心。它聚合了:
- 视频轨道(videoItems)
- 音频轨道(audioItems)
- 背景音乐(musicItems)
- 水印(maskItem)
- 变速效果(seepItems)
通过在 buildTimeLine 时一次性构建这些属性,后续的 CompositionBuilder 只需要解析 timeLine 即可。
3. PHSpeedCompositionBuilder —— 合成器
class PHSpeedCompositionBuilder: PHComositionBuilder {private let composition = AVMutableComposition()private let timeLine: PHTimeLineprivate var videoComposition: AVMutableVideoCompositionprivate var audioMix: AVAudioMix?init(timeLine: PHTimeLine) {self.timeLine = timeLineself.videoComposition = AVMutableVideoComposition()videoComposition.frameDuration = CMTime(value: 1, timescale: 30)videoComposition.renderSize = CGSize(width: 1280, height: 720)}func buildComposition() -> (any PHComposition)? {// 添加视频 & 音频轨道guard let videoTrack = self.addTrack(with: .video, mediaItems: self.timeLine.videoItmes),let audioTrack = self.addTrack(with: .audio, mediaItems: self.timeLine.audioItems) else {return nil}// 遍历变速片段for seepItem in timeLine.seepItems {self.applySpeed(to: videoTrack,audioTrack: audioTrack,startTime: seepItem.startTime,duration: seepItem.timeRange.duration,scaleSpeed: seepItem.scaleSpeed)}return PHSpeedComposition(composition: composition,videoComposition: videoComposition,audioMix: audioMix)}
}
在 PHSpeedCompositionBuilder 中,我们做了三件核心的事:
- 初始化渲染配置:帧率(30fps)、渲染尺寸(1280x720)。
- 加载轨道:将视频、音频素材插入到 AVMutableComposition。
- 应用变速:遍历 seepItems,对每个变速区间调用 applySpeed,从而实现快动作/慢动作。
四. 变速核心实现
视频变速的核心逻辑集中在 applySpeed 方法中。它的作用是:
- 调整 视频轨道 的播放时长,实现快动作或慢动作;
- 调整 音频轨道 的播放时长,保持与视频同步;
- 更新 视频合成指令,确保渲染时长正确。
来看代码:
/// 应用变速效果
/// - Parameters:
/// - videoTrack: 视频轨道
/// - audioTrack: 音频轨道
/// - startTime: 变速开始时间
/// - duration: 变速持续时间
/// - scaleSpeed: 变速比例,例如 2.0 表示慢动作,0.5 表示快动作
private func applySpeed(to videoTrack: AVMutableCompositionTrack,audioTrack: AVMutableCompositionTrack,startTime: CMTime,duration: CMTime,scaleSpeed: Float) {let instruction = AVMutableVideoCompositionInstruction()// 计算变速后的时长let scaledDuration = CMTimeMultiplyByFloat64(duration, multiplier: Float64(scaleSpeed))let totalDuration = CMTimeSubtract(videoTrack.timeRange.duration, duration) + scaledDuration// 设置渲染时长instruction.timeRange = CMTimeRange(start: .zero, duration: totalDuration)let layerInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: videoTrack)instruction.layerInstructions = [layerInstruction]videoComposition.instructions.append(instruction)// 视频变速:修改时间区间videoTrack.scaleTimeRange(CMTimeRange(start: startTime, duration: duration),toDuration: scaledDuration)// 音频变速:保持同步let audioDuration = CMTimeMultiplyByFloat64(duration, multiplier: Float64(scaleSpeed))audioTrack.scaleTimeRange(CMTimeRange(start: startTime, duration: duration),toDuration: audioDuration)print("视频轨道总时长: \(CMTimeGetSeconds(videoTrack.timeRange.duration)) 秒")
}
1. 核心计算公式
let scaledDuration = duration * scaleSpeed
let totalDuration = (原始总时长 - duration) + scaledDuration
- scaledDuration:表示 变速区间在新速度下的时长;
- totalDuration:表示 整条视频在变速后新的总时长。
👉 这一步非常关键。如果直接把 instruction.timeRange 设置为 scaledDuration,就会出现画面丢失的问题。必须用 totalDuration 来覆盖渲染范围,确保整个视频能正常播放。
2. 视频轨道处理
videoTrack.scaleTimeRange(CMTimeRange(start: startTime, duration: duration),toDuration: scaledDuration)
这一步会将 startTime ~ duration 的视频片段 拉伸/压缩 到新的时长,实现快/慢动作。
- scaleSpeed = 2.0 → 时长 *2 → 慢动作;
- scaleSpeed = 0.5 → 时长 *0.5 → 快动作。
3. 音频轨道处理
audioTrack.scaleTimeRange(CMTimeRange(start: startTime, duration: duration),toDuration: audioDuration)
音频的逻辑与视频相同,只是 不需要渲染指令。通过调整 timeRange,即可保证音视频同步。
4. 视频 vs 音频 的区别
- 视频轨道:必须结合 AVMutableVideoCompositionInstruction 来更新渲染时长,否则会丢画面。
- 音频轨道:只需调整 scaleTimeRange 即可,不需要合成指令。
五. 实战效果验证
在前面,我们已经完成了 PHSpeedCompositionBuilder 的核心实现。现在,只需要在 Demo 中构建一个 PHTimeLine,并添加 PHSpeedItem,就能快速验证效果。
1. 构建 TimeLine
// 构建带变速的时间线
let timeLine = PHTimeLine.buildTimeLine(with: items)// 假设我们在第 2 秒开始,持续 3 秒,并让视频变慢 2 倍
let speedItem = PHSpeedItem()
speedItem.startTime = CMTime(seconds: 2, preferredTimescale: 600)
speedItem.timeRange = CMTimeRange(start: speedItem.startTime,duration: CMTime(seconds: 3, preferredTimescale: 600))
speedItem.scaleSpeed = 2.0timeLine.seepItems = [speedItem]
2. 构建 Composition
let builder = PHSpeedCompositionBuilder(timeLine: timeLine)
guard let composition = builder.buildComposition() else {print("❌ 构建 Composition 失败")return
}
3. 播放或导出
如果原始视频是 15 秒:
- 在第 2 ~ 5 秒的区间变慢 2 倍 → 该区间时长变为 6 秒;
- 总时长 = 15 - 3 + 6 = 18 秒;
- 播放时可以清晰看到 2s → 5s 片段被拉长。
六. 结语
在本文中,我们完整实现了 视频变速处理,并通过 Demo 验证了其效果。核心思想是:
- 视频轨道:通过 scaleTimeRange 拉伸或压缩片段时长,并结合 AVMutableVideoCompositionInstruction 和 AVMutableVideoCompositionLayerInstruction 确保画面渲染正确。
- 音频轨道:相比视频更为简单,仅需调整 timeRange 即可保证与视频保持同步。
在实战中,我们实现了 局部变速 的支持,例如在第 2 秒 ~ 5 秒区间执行 2 倍慢动作,总时长相应调整为 18 秒。通过这种方式,我们不仅能够处理 整体变速,也能灵活地在指定区间内应用变速效果。
不过,本篇案例依然有一个简化假设:从一个起点到一个终点单段变速。而在实际开发中,用户往往希望在多个不同的区间应用不同的变速效果(例如“先快 → 再慢 → 再恢复正常”)。这会带来更复杂的轨道管理、区间拼接和指令叠加问题。