使用 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
然后我将此文件以及也已映射到慢动作的原始文件加载到AVPlayerItem
s 中,以便在我的应用程序中连接到AVPlayerLayer
s 的AVPlayer
s 中播放:
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 冻结的主要内容,如果未能解决你的问题,请参考以下文章