Поиск AVComposition с CMTimeMapping вызывает зависание AVPlayerLayer

Вот ссылка на 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играть в AVPlayerс, который связан с AVPlayerLayerв моем приложении:

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(), зацикливается отлично. Однако renderedVideo, который также запускался через функцию compose(), иногда зависает при поиске во время одного из CMTimeMapping сегменты. Единственное различие между файлом, который замораживается, и файлом, который не замораживается, состоит в том, что он был экспортирован в NSTeilitaryDirectory через AVAssetExportSession для объединения двух видеодорожек в одну. Они оба одинаковой продолжительности. Я также уверен, что только видео слой замерзает, а не звук, потому что если я добавлю BoundaryTimeObservers игрок, который замерзает, все равно попадает в них и зацикливается. Также аудио петли правильно.

Для меня самая странная часть в том, что видео "возобновляется", если оно проходит через то место, где оно остановилось, чтобы начать поиск после "замораживания". Я застрял на этом в течение нескольких дней и очень хотел бы некоторое руководство.

Другие странные вещи, на которые следует обратить внимание: - Несмотря на то, что CMTimeMapping оригинала и экспортируемого актива имеет одинаковую длительность, вы заметите, что замедленное движение визуализируемого актива более "прерывисто", чем оригинал. - Аудио продолжается, когда видео останавливается. - видео почти всегда останавливается во время фрагментов с медленным движением (вызванных объектами CMTimeMapping на основе сегментов - воспроизводимое видео, похоже, должно воспроизводиться "наверстать" в начале. Даже если я вызываю play после того, как оба закончили поиск, кажется, мне, что правая сторона играет быстрее в начале, как догоняет. Странная часть в том, что сегменты одинаковы, просто ссылаются на два отдельных исходных файла. Один находится в библиотеке ресурсов, другой в NSTeoraryDirectory - Мне кажется, что AVPlayer и AVPlayerItemStatus - это "readyToPlay" до того, как я вызову play. - Кажется, "разморозка" происходит, если игрок продолжает PAST до точки, которую он заблокировал. - Я пытался добавить наблюдателей для "AVPlayerItemPlaybackDidStall", но он никогда не вызывался.

Ура!

2 ответа

Решение

Проблема была в AVAssetExportSession. К моему удивлению, меняется exporter.canPerformMultiplePassesOverSourceMediaData = true исправил проблему. Хотя документация довольно скудная и даже заявления о том, что "установка этого свойства в true может не иметь никакого эффекта", похоже, это действительно решает проблему. Очень, очень, очень странно! Я считаю это ошибкой и буду регистрировать радар. Вот документы по этому свойству: canPerformMultiplePassesOverSourceMediaData

Кажется возможным, что в вашем playAllPlayersFromStart() метод, который startTime переменная могла измениться между двумя отправленными задачами (это было бы особенно вероятно, если бы это значение обновлялось на основе очистки).

Если вы делаете локальную копию startTime в начале функции, а затем использовать ее в обоих блоках, вам может повезти.

Другие вопросы по тегам