以下是问题的GIF链接:
https://gifyu.com/images/ScreenRecording2017-01-25at02.20PM.gif
我从相机胶卷中取出 PHAsset
,将其添加到可变组合中,添加另一个视频轨道,操纵添加的轨道,然后通过 AVAssetExportSession
导出它 . 结果是一个quicktime文件,其中.mov文件扩展名保存在 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
}
然后我加载这个文件以及原来的文件,它也被映射到slowmo到 AVPlayerItem
s,以便在我的应用程序中连接到 AVPlayerLayer
的 AVPlayer
中播放:
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()函数运行的renderedVideo有时会冻结 . 冻结的文件与未冻结的文件之间的唯一区别是,已经通过AVAssetExportSession将一个文件导出到NSTemporaryDirectory,以将两个视频轨道合并为一个 . 他们也确定它只是视频层冻结而不是音频,因为如果我将 BoundaryTimeObservers
添加到冻结的播放器,它仍然会击中它们并循环播放 . 音频也正常循环 .
对我来说,最奇怪的部分是视频“恢复”,如果它超过了“冻结”后它暂停开始搜索的位置 . 我已经坚持了好几天,真的很喜欢一些指导 .
其他奇怪的事情需要注意: - 即使原始CMIFM对出口资产的CMTimeMapping是完全相同的持续时间,您会注意到渲染资产的慢动作斜坡比原始资产更加“波动” . - 视频冻结时音频继续播放 . - 视频几乎只会在慢动作部分冻结(由基于片段的CMTimeMapping对象引起 - 渲染的视频似乎必须在开头播放'赶上' . 即使我在两个人完成搜索后都在调用播放,它似乎我认为右侧在开始时起到更快的作用 . 奇怪的是,这些段完全相同,仅引用两个独立的源文件 . 一个位于资产库中,另一个位于NSTemporaryDirectory中 - 在我看来在我调用play之前,AVPlayer和AVPlayerItemStatus是'readyToPlay' . 如果玩家继续PAST它锁定的点,它似乎'解冻' . - 我试图为'AVPlayerItemPlaybackDidStall'添加观察者,但它从未被调用过 .
干杯!
2 回答
问题出在AVAssetExportSession中 . 令我惊讶的是,更改
exporter.canPerformMultiplePassesOverSourceMediaData = true
解决了这个问题 . 尽管文档非常稀少甚至声称'setting this property to true may have no effect',但它似乎确实解决了这个问题 . 非常非常非常奇怪!我认为这是一个错误,并将提交雷达 . 以下是该 properties 的文件:canPerformMultiplePassesOverSourceMediaData在您的
playAllPlayersFromStart()
方法中,startTime
变量可能在调度的两个任务之间发生了变化(如果该值基于清理更新,则特别有可能) .如果你在函数的开头制作
startTime
的本地副本,然后在两个块中使用它,你可能会有更好的运气 .