使用 CMTimeMapping 寻求 AVComposition 会导致 AVPlayerLayer 冻结

Seeking AVComposition with CMTimeMapping causes AVPlayerLayer to freeze

这里是问题的 GIF link:

https://gifyu.com/images/ScreenRecording2017-01-25at02.20PM.gif

我正在从相机胶卷中取出 PHAsset,将其添加到可变合成中,添加另一个视频轨道,操纵添加的轨道,然后通过 AVAssetExportSession 将其导出。结果是一个带有 .mov 文件扩展名的 quicktime 文件保存在 NSTemporaryDirectory():

guard let exporter = AVAssetExportSession(asset: mergedComposition, presetName: AVAssetExportPresetHighestQuality) else {
        fatalError()
}

exporter.outputURL = temporaryUrl
exporter.outputFileType = AVFileTypeQuickTimeMovie
exporter.shouldOptimizeForNetworkUse = true
exporter.videoComposition = videoContainer

// Export the new video
delegate?.mergeDidStartExport(session: exporter)
exporter.exportAsynchronously() { [weak self] in
     DispatchQueue.main.async {
        self?.exportDidFinish(session: exporter)
    }
}

然后我将这个导出的文件加载到一个映射器对象中,该对象根据给定的一些时间映射将 'slow motion' 应用于剪辑。这里的结果是一个 AVComposition:

func compose() -> AVComposition {
    let composition = AVMutableComposition(urlAssetInitializationOptions: [AVURLAssetPreferPreciseDurationAndTimingKey: true])

    let emptyTrack = composition.addMutableTrack(withMediaType: AVMediaTypeVideo, preferredTrackID: kCMPersistentTrackID_Invalid)
    let audioTrack = composition.addMutableTrack(withMediaType: AVMediaTypeAudio, preferredTrackID: kCMPersistentTrackID_Invalid)

    let asset = AVAsset(url: url)
    guard let videoAssetTrack = asset.tracks(withMediaType: AVMediaTypeVideo).first else { return composition }

    var segments: [AVCompositionTrackSegment] = []
    for map in timeMappings {

        let segment = AVCompositionTrackSegment(url: url, trackID: kCMPersistentTrackID_Invalid, sourceTimeRange: map.source, targetTimeRange: map.target)
        segments.append(segment)
    }

    emptyTrack.preferredTransform = videoAssetTrack.preferredTransform
    emptyTrack.segments = segments

    if let _ = asset.tracks(withMediaType: AVMediaTypeVideo).first {
        audioTrack.segments = segments
    }

    return composition.copy() as! AVComposition
}

然后我将此文件以及也已映射到 slowmo 的原始文件加载到 AVPlayerItems 以在连接到 AVPlayerLayerAVPlayers 中播放在我的应用程序中:

let firstItem = AVPlayerItem(asset: originalAsset)
let player1 = AVPlayer(playerItem: firstItem)
firstItem.audioTimePitchAlgorithm = AVAudioTimePitchAlgorithmVarispeed
player1.actionAtItemEnd = .none
firstPlayer.player = player1

// set up player 2
let secondItem = AVPlayerItem(asset: renderedVideo)
secondItem.seekingWaitsForVideoCompositionRendering = true //tried false as well
secondItem.audioTimePitchAlgorithm = AVAudioTimePitchAlgorithmVarispeed
secondItem.videoComposition = nil // tried AVComposition(propertiesOf: renderedVideo) as well

let player2 = AVPlayer(playerItem: secondItem)
player2.actionAtItemEnd = .none
secondPlayer.player = player2

然后我有一个开始和结束时间来一遍又一遍地循环播放这些视频。我不使用 PlayerItemDidReachEnd 因为我对最后不感兴趣,我对用户输入的时间感兴趣。我什至使用 dispatchGroup 确保 两个玩家在尝试重播视频之前都已完成搜索:

func playAllPlayersFromStart() {

    let dispatchGroup = DispatchGroup()

    dispatchGroup.enter()

    firstPlayer.player?.currentItem?.seek(to: startTime, toleranceBefore: kCMTimeZero, toleranceAfter: kCMTimeZero, completionHandler: { _ in
        dispatchGroup.leave()
    })

    DispatchQueue.global().async { [weak self] in
        guard let startTime = self?.startTime else { return }
        dispatchGroup.wait()

        dispatchGroup.enter()

        self?.secondPlayer.player?.currentItem?.seek(to: startTime, toleranceBefore: kCMTimeZero, toleranceAfter: kCMTimeZero, completionHandler: { _ in
            dispatchGroup.leave()
        })


        dispatchGroup.wait()

        DispatchQueue.main.async { [weak self] in
            self?.firstPlayer.player?.play()
            self?.secondPlayer.player?.play()
        }
    }

}

这里奇怪的部分是原始资产,它也通过我的 compose() 函数循环映射得很好。但是,同样通过 compose() 函数 运行 的 renderedVideo 在 CMTimeMapping 片段之一中搜索时有时会冻结。冻结的文件和不冻结的文件之间的唯一区别是,一个文件已通过 AVAssetExportSession 导出到 NSTemporaryDirectory 以将两个视频轨道合并为一个。它们的持续时间相同。我还确定只是视频层冻结而不是音频,因为如果我将 BoundaryTimeObservers 添加到冻结的播放器,它仍然会击中它们并循环播放。音频循环也正确。

对我来说最奇怪的部分是视频 'resumes' 如果它经过它暂停的地方开始搜索 'freeze'。我已经坚持了好几天了,真的很想得到一些指导。

其他需要注意的奇怪事项: - 尽管原始资产与导出资产的 CMTimeMapping 的持续时间完全相同,但您会注意到渲染资产的慢动作斜坡比原始资产多 'choppy'。 - 视频冻结时音频继续。 - 视频几乎只会在慢动作部分冻结(由基于片段的 CMTimeMapping 对象引起) - 渲染视频似乎必须在开头播放 'catch up'。尽管我是在双方都完成搜索后才开始比赛,但在我看来,右侧在开始追赶时速度更快。奇怪的是,这些段完全相同,只是引用了两个单独的源文件。一个位于资产库中,另一个位于 NSTemporaryDirectory 中 - 在我看来,AVPlayer 和 AVPlayerItemStatus 在我调用播放之前是 'readyToPlay'。 - 似乎 'unfreeze' 如果玩家继续前进超过它锁定的点。 - 我试图为 'AVPlayerItemPlaybackDidStall' 添加观察者,但从未被调用。

干杯!

似乎有可能在您的 playAllPlayersFromStart() 方法中,startTime 变量可能在分派的两个任务之间发生了变化(如果该值基于清理进行更新,则这种情况尤其可能发生)。

如果您在函数的开头制作 startTime 的本地副本,然后在两个块中都使用它,您的运气可能会更好。

问题出在 AVAssetExportSession 中。令我惊讶的是,更改 exporter.canPerformMultiplePassesOverSourceMediaData = true 解决了这个问题。虽然文档非常稀疏,甚至声称 'setting this property to true may have no effect',但它似乎确实解决了这个问题。非常非常非常奇怪!我认为这是一个错误,并将提交雷达。以下是 属性 上的文档:canPerformMultiplePassesOverSourceMediaData