返回

AVFoundation视频变速:平滑变速技巧与实战

IOS

AVFoundation:视频变速处理的实践

在处理视频编辑时,有时需要实现动态变速效果,例如在一段视频的不同部分应用不同的播放速度。这个问题可以使用 AVFoundation 框架来解决。实现过程中可能出现视频卡顿、速度切换不流畅的问题。

问题分析

问题的核心在于如何使用 AVMutableCompositionTrackscaleTimeRange(_:toDuration:) 方法,精确地控制视频各部分的速度。以下几点可能导致实现效果不佳:

  1. 速度变化区间过大 :当两个速度控制点之间的速度差异较大时,如果过渡区域过少,会出现明显的跳跃感。
  2. 过渡算法不平滑 :线性插值简单直接,但效果并不理想,过渡时可能会产生顿挫感。
  3. 时间映射不精确scaleTimeRange 方法需要提供正确的时间区间和目标时长,任何细微偏差都可能导致时间错位,播放时出现卡顿。
  4. 分段数量不足 : 将时间区间分为更小的片段进行处理能实现更精细的速度调整,但如果片段数量太少,仍会出现不连贯。

解决方案一:精确的时间分段和缓冲

解决思路是将速度变化区间划分成更多小段,并在速度切换时采用更精细的插值算法,以此达到平滑变速的目的。 核心在于控制好每个小段的时长和速度。以下是代码示例:

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)
         }
        }
 }

步骤:

  1. 传入至少包含两个速度控制点(SpeedControlPoint 结构体)。
  2. 对控制点按照时间排序。
  3. 将每两个控制点之间的时间区间分成多个片段(例子中使用 numSegments = 100),增加分割数量使过渡平缓。
  4. 计算每一小段的速度(可以应用更平滑的缓和函数,这里使用线性插值)。
  5. 使用计算出的速度和时间区间, 调用 scaleTimeRange(_:toDuration:) 应用到 compositionTrack 。

额外提示:

  • 增加 numSegments 可以提高变速平滑度,但同时也会增加计算量。
  • 可以尝试不同的缓和函数,如三次贝塞尔曲线,得到更流畅的速度过渡效果。

解决方案二: 使用AVMutableVideoCompositionAVVideoCompositionInstruction

此方案直接处理视频帧级别的调整,而非仅操作时间线。这提供了一种更灵活的方式来实现变速控制,特别是对于非线性变速来说。以下示例展示了基本的处理流程,需要自定义实现时间到速度映射关系,在需要更高精度的速度控制或者处理非常规的变速曲线时适用。

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
}

步骤:

  1. 构建时间分段: 通过 buildSpeedControlSegments 将时间轴分解为多个具有不同速度的小段。这个分段越细,变速效果越平滑。
  2. 创建视频组合: 利用 AVMutableComposition 创建视频片段。
  3. 创建 AVMutableVideoCompositionInstruction: 为每个速度段创建一个 AVVideoCompositionInstruction。 关键是通过设置 layerInstruction,来改变每个 layer 的时间速度
  4. 计算新速度下实际时间 根据每个 segment 的实际 duration, 以及 speed ,计算缩放之后实际 duration. 并移动时间偏移量.

安全提示:

  • 务必确保每个分段的起始时间正确,避免出现错位和重复。
  • 确保传入的速度控制点有序,防止出现速度控制错乱。
  • 如果输入参数不满足,输出结果应该正确处理异常情况,保证程序健壮。

以上解决方案为视频动态变速提供了一种可行方法, 在具体应用时,请注意根据实际需要调整相关参数,并在性能和效果之间找到平衡点。