Поиск AVComposition с CMTimeMapping приводит к зависанию AVPlayerLayer

Вот ссылка на гифку проблемы:

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 в AVPlayerItems, чтобы играть в AVPlayers, который подключен к AVPlayerLayers в моем приложении:

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», но он так и не был вызван.

Ваше здоровье!


person Daniel Williams    schedule 25.01.2017    source источник


Ответы (2)


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

person Daniel Williams    schedule 30.01.2017

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

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

person Dave Lyon    schedule 26.01.2017
comment
Очень ценю ответ. К сожалению, нет кубиков, так как второй игрок все равно зависает. Я могу сказать, что сам игрок все еще играет, потому что наблюдатель граничного времени попадает в соответствующее место, что сигнализирует игроку 1 о перезапуске. Также звук перезапускается с самого начала соответствующим образом, в то время как только видео блокируется до тех пор, пока не пройдет «замороженный» кадр. - person Daniel Williams; 26.01.2017