目录

二十二深入了解AVFoundation-编辑视频变速功能-实战在Demo中实现视频变速

(二十二)深入了解AVFoundation-编辑:视频变速功能-实战在Demo中实现视频变速

一. 引言

视频变速(Speed Ramp)是视频编辑中最常见的特效之一:

  • 慢动作(Slow Motion):强调细节,让观众捕捉到肉眼难以察觉的瞬间;
  • 快动作(Fast Motion):压缩时长,强化节奏,常用于 vlog、综艺片段。

在 AVFoundation 中,视频变速的本质是 对音视频轨道的时间线进行重新映射。通过调整时间范围(CMTimeRange)与目标时长(toDuration),即可让视频和音频实现同步的快进或慢放效果。

本文将通过一个完整的 Demo 实战,展示如何在 iOS 中使用 AVFoundation 实现视频的变速处理,涵盖从模型设计到合成器构建的完整流程。

二. 核心思路回顾

要实现变速,必须同时处理 视频轨道 与 音频轨道,以保证二者的同步:

1. 视频轨道

  • 借助 AVMutableVideoCompositionInstruction 和 AVMutableVideoCompositionLayerInstruction,保证变速后的视频能够正常渲染。
  • 关键在于:
  1. videoTrack.scaleTimeRange():调整视频的时间范围到新的时长;
  2. instruction.timeRange:指定变速后的可见区间;
  3. 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: PHTimeLine
    private var videoComposition: AVMutableVideoComposition
    private var audioMix: AVAudioMix?
    
    init(timeLine: PHTimeLine) {
        self.timeLine = timeLine
        self.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.0

timeLine.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 片段被拉长

https://i-blog.csdnimg.cn/direct/dc107c1069364129a139d85a350b0875.jpeg

六. 结语

在本文中,我们完整实现了 视频变速处理,并通过 Demo 验证了其效果。核心思想是:

  • 视频轨道:通过 scaleTimeRange 拉伸或压缩片段时长,并结合 AVMutableVideoCompositionInstruction 和 AVMutableVideoCompositionLayerInstruction 确保画面渲染正确。
  • 音频轨道:相比视频更为简单,仅需调整 timeRange 即可保证与视频保持同步。

在实战中,我们实现了 局部变速 的支持,例如在第 2 秒 ~ 5 秒区间执行 2 倍慢动作,总时长相应调整为 18 秒。通过这种方式,我们不仅能够处理 整体变速,也能灵活地在指定区间内应用变速效果。

不过,本篇案例依然有一个简化假设:从一个起点到一个终点单段变速。而在实际开发中,用户往往希望在多个不同的区间应用不同的变速效果(例如“先快 → 再慢 → 再恢复正常”)。这会带来更复杂的轨道管理、区间拼接和指令叠加问题。