Вот ссылка на гифку проблемы:
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)
}
}
Затем я беру этот экспортированный файл и загружаю его в объект сопоставления, который применяет «медленное движение» к клипу на основе заданных ему сопоставлений времени. Результатом является AV-композиция:
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, чтобы играть в AVPlayer
s, который подключен к AVPlayerLayer
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 (), иногда зависает при поиске во время одного из CMTimeMapping
сегментов. Единственная разница между зависшим файлом и файлом, который не зависает, заключается в том, что он был экспортирован в NSTemporaryDirectory через AVAssetExportSession для объединения двух видеодорожек в одну. Они оба одинаковой продолжительности. Я также уверен, что замораживается только видеослой, а не звук, потому что, если я добавлю BoundaryTimeObservers
к плееру, который замораживает, он все равно попадает в них и зацикливается. Также звук зацикливается правильно.
Для меня самое странное то, что видео «возобновляется», если оно проходит мимо места, где оно было приостановлено, чтобы начать поиск после «замораживания». Я застрял на этом несколько дней и очень хотел бы получить совет.
Другие странные вещи, на которые следует обратить внимание: - Даже несмотря на то, что CMTimeMapping оригинала и экспортированного актива имеют одинаковую продолжительность, вы заметите, что замедленное движение визуализированного актива более «прерывистое», чем у оригинала. - Звук продолжается, когда видео зависает. - видео почти всегда зависает во время участков с замедленным движением (из-за объектов CMTimeMapping, основанных на сегментах - отрендеренное видео, кажется, должно воспроизводиться «в догонялки» в начале. даже если я вызываю воспроизведение после того, как оба завершили поиск, кажется, что Мне кажется, что правая сторона играет быстрее в начале в качестве догоняющего. Странно то, что сегменты точно такие же, просто ссылаются на два отдельных исходных файла. Один находится в библиотеке ресурсов, другой в NSTemporaryDirectory - мне кажется, что AVPlayer и AVPlayerItemStatus имеют значение «readyToPlay», прежде чем я вызываю воспроизведение. - Кажется, что он «размораживается», если проигрыватель продолжает ПРОШЛО точку, в которой он заблокирован. - Я попытался добавить наблюдателей для «AVPlayerItemPlaybackDidStall», но он так и не был вызван.
Ваше здоровье!