返回 解决方案二: 使用
AVFoundation视频变速:平滑变速技巧与实战
IOS
2024-12-29 09:33:08
AVFoundation:视频变速处理的实践
在处理视频编辑时,有时需要实现动态变速效果,例如在一段视频的不同部分应用不同的播放速度。这个问题可以使用 AVFoundation 框架来解决。实现过程中可能出现视频卡顿、速度切换不流畅的问题。
问题分析
问题的核心在于如何使用 AVMutableCompositionTrack
的 scaleTimeRange(_:toDuration:)
方法,精确地控制视频各部分的速度。以下几点可能导致实现效果不佳:
- 速度变化区间过大 :当两个速度控制点之间的速度差异较大时,如果过渡区域过少,会出现明显的跳跃感。
- 过渡算法不平滑 :线性插值简单直接,但效果并不理想,过渡时可能会产生顿挫感。
- 时间映射不精确 :
scaleTimeRange
方法需要提供正确的时间区间和目标时长,任何细微偏差都可能导致时间错位,播放时出现卡顿。 - 分段数量不足 : 将时间区间分为更小的片段进行处理能实现更精细的速度调整,但如果片段数量太少,仍会出现不连贯。
解决方案一:精确的时间分段和缓冲
解决思路是将速度变化区间划分成更多小段,并在速度切换时采用更精细的插值算法,以此达到平滑变速的目的。 核心在于控制好每个小段的时长和速度。以下是代码示例:
func applyDynamicSpeedControl(to compositionTrack: AVMutableCompositionTrack, using controlPoints: [SpeedControlPoint]) {
guard controlPoints.count >= 2 else { return }
let sortedControlPoints = controlPoints.sorted { $0.time < $1.time }
for i in 0..<(sortedControlPoints.count - 1) {
let startPoint = sortedControlPoints[i]
let endPoint = sortedControlPoints[i + 1]
let timeRange = CMTimeRange(start: startPoint.time, duration: CMTimeSubtract(endPoint.time, startPoint.time))
let numSegments = 100 // 可调整为更大的值
let segmentDuration = CMTimeDivideByFloat64(timeRange.duration, divisor: Double(numSegments))
for j in 0..<numSegments {
let t = Double(j) / Double(numSegments)
// 改用缓和函数,例如 easeInOut,使过渡更平滑。这里使用线性函数举例
let currentSpeed = startPoint.speed + (endPoint.speed - startPoint.speed) * t
let segmentStart = CMTimeAdd(timeRange.start, CMTimeMultiplyByFloat64(segmentDuration, multiplier: Double(j)))
let mappedDuration = CMTimeMultiplyByFloat64(segmentDuration, multiplier: 1.0 / currentSpeed)
// 时间段转换应该基于片段时长
let mappedTimeRange = CMTimeRange(start: segmentStart, duration: segmentDuration)
compositionTrack.scaleTimeRange(mappedTimeRange, toDuration:mappedDuration)
}
}
}
步骤:
- 传入至少包含两个速度控制点(
SpeedControlPoint
结构体)。 - 对控制点按照时间排序。
- 将每两个控制点之间的时间区间分成多个片段(例子中使用
numSegments = 100
),增加分割数量使过渡平缓。 - 计算每一小段的速度(可以应用更平滑的缓和函数,这里使用线性插值)。
- 使用计算出的速度和时间区间, 调用
scaleTimeRange(_:toDuration:)
应用到 compositionTrack 。
额外提示:
- 增加
numSegments
可以提高变速平滑度,但同时也会增加计算量。 - 可以尝试不同的缓和函数,如三次贝塞尔曲线,得到更流畅的速度过渡效果。
解决方案二: 使用AVMutableVideoComposition
和 AVVideoCompositionInstruction
此方案直接处理视频帧级别的调整,而非仅操作时间线。这提供了一种更灵活的方式来实现变速控制,特别是对于非线性变速来说。以下示例展示了基本的处理流程,需要自定义实现时间到速度映射关系,在需要更高精度的速度控制或者处理非常规的变速曲线时适用。
func applyVariableSpeed(videoAsset: AVAsset, speedControlPoints:[SpeedControlPoint], completion:@escaping(AVComposition?, AVVideoComposition?,Error?) -> Void){
//1. 创建 composition 和视频轨道
let composition = AVMutableComposition()
guard let videoTrack = videoAsset.tracks(withMediaType: .video).first , let compositionVideoTrack = composition.addMutableTrack(withMediaType: .video, preferredTrackID: kCMPersistentTrackID_Invalid) else {
completion(nil, nil, NSError(domain: "Invalid Video Input", code: -1))
return
}
try? compositionVideoTrack.insertTimeRange(CMTimeRangeMake(start: .zero, duration: videoAsset.duration), of: videoTrack, at: .zero)
//2.创建 videoComposition and instructions
let videoComposition = AVMutableVideoComposition()
videoComposition.renderSize = videoTrack.naturalSize
videoComposition.frameDuration = CMTimeMake(value: 1, timescale: 30)
var instructions: [AVVideoCompositionInstruction] = []
// 将速度区间分为多段处理,得到更精细控制
var currentTime: CMTime = .zero
let segments = buildSpeedControlSegments(controlPoints: speedControlPoints, duration: videoAsset.duration)
for segment in segments {
//创建 Instruction 和 layeredInstruction
let videoCompositionInstruction = AVMutableVideoCompositionInstruction()
videoCompositionInstruction.timeRange = CMTimeRange(start: currentTime, duration:segment.duration)
let layerInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: compositionVideoTrack)
let scaledDuration = CMTimeMultiplyByFloat64(segment.duration, multiplier: 1.0/segment.speed)
// 应用速度变化
layerInstruction.setTransformRamp(fromStartTransform: compositionVideoTrack.preferredTransform, toEndTransform: compositionVideoTrack.preferredTransform , timeRange:CMTimeRange(start: .zero, duration:segment.duration))
layerInstruction.setOpacityRamp(fromStartOpacity: 1.0, toEndOpacity: 1.0, timeRange: CMTimeRange(start: .zero, duration: segment.duration) )
layerInstruction.setCropRectangleRamp(fromStartCropRectangle: compositionVideoTrack.naturalSize.bounds, toEndCropRectangle: compositionVideoTrack.naturalSize.bounds , timeRange:CMTimeRange(start: .zero, duration: segment.duration) )
videoCompositionInstruction.layerInstructions = [layerInstruction]
instructions.append(videoCompositionInstruction)
currentTime = CMTimeAdd(currentTime, scaledDuration)
}
videoComposition.instructions = instructions;
completion(composition, videoComposition,nil)
}
// 定义一个 helper 方法用来产生速度变化区间
struct SpeedSegment{
let duration: CMTime
let speed: Double
}
func buildSpeedControlSegments(controlPoints:[SpeedControlPoint],duration:CMTime) -> [SpeedSegment] {
var segments : [SpeedSegment] = []
let sortedControlPoints = controlPoints.sorted { $0.time < $1.time }
var currentTime : CMTime = .zero
for i in 0..<(sortedControlPoints.count-1) {
let start = sortedControlPoints[i]
let end = sortedControlPoints[i+1]
let timeRange = CMTimeRange(start: start.time, duration: CMTimeSubtract(end.time, start.time))
let segmentCount = 100 // 增加 segment 数量使变化平缓
let segmentDuration = CMTimeDivideByFloat64(timeRange.duration, divisor: Double(segmentCount))
for j in 0 ..< segmentCount {
let progress = Double(j) / Double(segmentCount)
let currentSpeed = start.speed + (end.speed - start.speed) * progress
segments.append(SpeedSegment(duration: segmentDuration, speed: currentSpeed))
currentTime = CMTimeAdd(currentTime, segmentDuration )
}
}
return segments
}
步骤:
- 构建时间分段: 通过
buildSpeedControlSegments
将时间轴分解为多个具有不同速度的小段。这个分段越细,变速效果越平滑。 - 创建视频组合: 利用
AVMutableComposition
创建视频片段。 - 创建
AVMutableVideoCompositionInstruction
: 为每个速度段创建一个AVVideoCompositionInstruction
。 关键是通过设置layerInstruction
,来改变每个 layer 的时间速度 - 计算新速度下实际时间 根据每个 segment 的实际 duration, 以及 speed ,计算缩放之后实际 duration. 并移动时间偏移量.
安全提示:
- 务必确保每个分段的起始时间正确,避免出现错位和重复。
- 确保传入的速度控制点有序,防止出现速度控制错乱。
- 如果输入参数不满足,输出结果应该正确处理异常情况,保证程序健壮。
以上解决方案为视频动态变速提供了一种可行方法, 在具体应用时,请注意根据实际需要调整相关参数,并在性能和效果之间找到平衡点。