使用 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 的原始文件加载到 AVPlayerItem
s 以在连接到 AVPlayerLayer
的 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() 函数循环映射得很好。但是,同样通过 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
这里是问题的 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 的原始文件加载到 AVPlayerItem
s 以在连接到 AVPlayerLayer
的 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() 函数循环映射得很好。但是,同样通过 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