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

Posted

技术标签:

【中文标题】使用 CMTimeMapping 寻求 AVComposition 会导致 AVPlayerLayer 冻结【英文标题】:Seeking AVComposition with CMTimeMapping causes AVPlayerLayer to freeze 【发布时间】:2017-01-25 22:35:05 【问题描述】:

这里是问题的 GIF 链接:

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)
    

然后我把这个导出的文件加载到一个映射器对象中,该对象根据给定的一些时间映射对剪辑应用“慢动作”。这里的结果是一个 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

然后我将此文件以及也已映射到慢动作的原始文件加载到AVPlayerItems 中,以便在我的应用程序中连接到AVPlayerLayers 的AVPlayers 中播放:

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() 函数进行了映射,循环非常好。但是,在 CMTimeMapping 段之一期间搜索时,也通过 compose() 函数运行的渲染视频有时会冻结。冻结的文件和不冻结的文件之间的唯一区别是一个已通过 AVAssetExportSession 导出到 NSTemporaryDirectory 以将两个视频轨道合并为一个。它们的持续时间相同。我也确定只有视频层冻结而不是音频,因为如果我将BoundaryTimeObservers 添加到冻结的播放器中,它仍然会击中它们并循环。音频也可以正常循环播放。

对我来说,最奇怪的部分是视频“恢复”,如果它在“冻结”后经过暂停开始搜索的位置。我已经坚持了好几天了,真的很想得到一些指导。

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

干杯!

【问题讨论】:

【参考方案1】:

问题出在 AVAssetExportSession 中。令我惊讶的是,更改 exporter.canPerformMultiplePassesOverSourceMediaData = true 解决了这个问题。尽管文档非常稀少,甚至声称“将此属性设置为 true 可能没有效果”,但它似乎确实解决了这个问题。非常非常非常奇怪!我认为这是一个错误,并将提交雷达。以下是该物业的文档:canPerformMultiplePassesOverSourceMediaData

【讨论】:

【参考方案2】:

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

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

【讨论】:

非常感谢您的回复。不幸的是,没有骰子,因为第二个玩家仍然冻结。我可以告诉玩家本身仍在玩,因为边界时间观察器在适当的位置被击中,这表明玩家 1 重新开始。此外,音频会从头开始适当地重新启动,而只有视频会锁定,直到它通过“冻结”帧。

以上是关于使用 CMTimeMapping 寻求 AVComposition 会导致 AVPlayerLayer 冻结的主要内容,如果未能解决你的问题,请参考以下文章

使用 RestKit 在 post 请求中寻求帮助

急!!!寻求redis大咖!

寻求关于使用 DAWG 实现文字游戏的建议

寻求特定时间线后的 MPMoviePlayerController 空白帧

寻求使用python在一个大文件中进行正则表达式

连接 SDK 寻求跳转到 0:00