iOS 10.0 - 10.1: AVPlayerLayer не показывает видео после использования AVVideoCompositionCoreAnimationTool, только аудио

Вот полный проект, если вы хотите запустить его самостоятельно: https://www.dropbox.com/s/5p384mogjzflvqk/AVPlayerLayerSoundOnlyBug_iOS10.zip?dl=0

Это новая проблема в iOS 10, и она была исправлена ​​в iOS 10.2. После экспорта видео с использованием AVAssetExportSession и AVVideoCompositionCoreAnimationTool для компоновки слоя поверх видео во время экспорта видео, воспроизводимые в AVPlayerLayer, не воспроизводятся. Похоже, что это не вызвано достижением ограничения конвейера AV-кодирования / декодирования, потому что это часто происходит после одного экспорта, который, насколько я знаю, только раскручивает 2 конвейера: один для AVAssetExportSession и другой для AVPlayer. Я также правильно устанавливаю рамку слоя, как вы можете увидеть, запустив приведенный ниже код, который дает слою синий фон, который вы можете видеть.

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

Любые идеи о том, что вызывает это или как я могу исправить или обойти это? Я что-то напутал или пропустил важный шаг или деталь? Любая помощь или указатели на документацию очень ценятся.

import UIKit
import AVFoundation

/* After exporting an AVAsset using AVAssetExportSession with AVVideoCompositionCoreAnimationTool, we
 * will attempt to play a video using an AVPlayerLayer with a blue background.
 *
 * If you see the blue background and hear audio you're experiencing the missing-video bug. Otherwise
 * try hitting the button again.
 */

class ViewController: UIViewController {
    private var playerLayer: AVPlayerLayer?
    private let button = UIButton()
    private let indicator = UIActivityIndicatorView(activityIndicatorStyle: .gray)

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = UIColor.white
        button.setTitle("Cause Trouble", for: .normal)
        button.setTitleColor(UIColor.black, for: .normal)
        button.addTarget(self, action: #selector(ViewController.buttonTapped), for: .touchUpInside)
        view.addSubview(button)
        button.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            button.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            button.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -16),
        ])

        indicator.hidesWhenStopped = true
        view.insertSubview(indicator, belowSubview: button)
        indicator.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            indicator.centerXAnchor.constraint(equalTo: button.centerXAnchor),
            indicator.centerYAnchor.constraint(equalTo: button.centerYAnchor),
        ])
    }

    func buttonTapped() {
        button.isHidden = true
        indicator.startAnimating()
        playerLayer?.removeFromSuperlayer()

        let sourcePath = Bundle.main.path(forResource: "video.mov", ofType: nil)!
        let sourceURL = URL(fileURLWithPath: sourcePath)
        let sourceAsset = AVURLAsset(url: sourceURL)

        //////////////////////////////////////////////////////////////////////
        // STEP 1: Export a video using AVVideoCompositionCoreAnimationTool //
        //////////////////////////////////////////////////////////////////////
        let exportSession = { () -> AVAssetExportSession in
            let sourceTrack = sourceAsset.tracks(withMediaType: AVMediaTypeVideo).first!

            let parentLayer = CALayer()
            parentLayer.frame = CGRect(origin: .zero, size: CGSize(width: 1280, height: 720))
            let videoLayer = CALayer()
            videoLayer.frame = parentLayer.bounds
            parentLayer.addSublayer(videoLayer)

            let composition = AVMutableVideoComposition(propertiesOf: sourceAsset)
            composition.animationTool = AVVideoCompositionCoreAnimationTool(postProcessingAsVideoLayer: videoLayer, in: parentLayer)
            let layerInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: sourceTrack)
            layerInstruction.setTransform(sourceTrack.preferredTransform, at: kCMTimeZero)
            let instruction = AVMutableVideoCompositionInstruction()
            instruction.timeRange = CMTimeRange(start: kCMTimeZero, duration: sourceAsset.duration)
            instruction.layerInstructions = [layerInstruction]
            composition.instructions = [instruction]

            let e = AVAssetExportSession(asset: sourceAsset, presetName: AVAssetExportPreset1280x720)!
            e.videoComposition = composition
            e.outputFileType = AVFileTypeQuickTimeMovie
            e.timeRange = CMTimeRange(start: kCMTimeZero, duration: sourceAsset.duration)
            let outputURL = URL(fileURLWithPath: NSTemporaryDirectory().appending("/out2.mov"))
            _ = try? FileManager.default.removeItem(at: outputURL)
            e.outputURL = outputURL
            return e
        }()

        print("Exporting asset...")
        exportSession.exportAsynchronously {
            assert(exportSession.status == .completed)

            //////////////////////////////////////////////
            // STEP 2: Play a video in an AVPlayerLayer //
            //////////////////////////////////////////////
            DispatchQueue.main.async {
                // Reuse player layer, shouldn't be hitting the AV pipeline limit
                let playerItem = AVPlayerItem(asset: sourceAsset)
                let layer = self.playerLayer ?? AVPlayerLayer()
                if layer.player == nil {
                    layer.player = AVPlayer(playerItem: playerItem)
                }
                else {
                    layer.player?.replaceCurrentItem(with: playerItem)
                }
                layer.backgroundColor = UIColor.blue.cgColor
                if UIDeviceOrientationIsPortrait(UIDevice.current.orientation) {
                    layer.frame = self.view.bounds
                    layer.bounds.size.height = layer.bounds.width * 9.0 / 16.0
                }
                else {
                    layer.frame = self.view.bounds.insetBy(dx: 0, dy: 60)
                    layer.bounds.size.width = layer.bounds.height * 16.0 / 9.0
                }
                self.view.layer.insertSublayer(layer, at: 0)
                self.playerLayer = layer

                layer.player?.play()
                print("Playing a video in an AVPlayerLayer...")

                self.button.isHidden = false
                self.indicator.stopAnimating()
            }
        }
    }
}

4 ответа

Решение

Ответ для меня в этом случае заключается в том, чтобы обойти проблему с AVVideoCompositionCoreAnimationTool с помощью пользовательского класса композитинга видео, реализующего AVVideoCompositing протокол и пользовательские инструкции композиции, реализующие AVVideoCompositionInstruction протокол. Потому что мне нужно наложить CALayer поверх видео я включаю этот слой в экземпляр инструкции по композиции.

Вам нужно установить пользовательский композитор для вашей видео композиции следующим образом:

composition.customVideoCompositorClass = CustomVideoCompositor.self

и затем установите свои собственные инструкции на нем:

let instruction = CustomVideoCompositionInstruction(...) // whatever parameters you need and are required by the instruction protocol
composition.instructions = [instruction]

РЕДАКТИРОВАТЬ: Вот рабочий пример того, как использовать пользовательский композитор для наложения слоя на видео с помощью графического процессора: https://github.com/samsonjs/LayerVideoCompositor... оригинальный ответ продолжается ниже

Что касается самого композитора, вы можете реализовать его, если посмотрите соответствующие сессии WWDC и посмотрите их пример кода. Я не могу опубликовать тот, который я написал здесь, но я использую CoreImage, чтобы сделать тяжелую работу при обработке AVAsynchronousVideoCompositionRequestубедитесь, что вы используете контекст OpenGL CoreImage для лучшей производительности (если вы сделаете это на процессоре, это будет ужасно медленным). Вам также может понадобиться пул с автоматическим выпуском, если во время экспорта вы получаете всплеск использования памяти.

Если вы накладываете CALayer как я, то обязательно установите layer.isGeometryFlipped = true когда вы визуализируете этот слой в CGImage перед отправкой в ​​CoreImage. И убедитесь, что вы кешируете CGImage от кадра к кадру в вашем композиторе.

У нас была такая же проблема на iOS 10 и 10.1. Выглядит исправлено с iOS 10.2 бета 3, хотя

Чтобы расширить ответ Сами Самхури, вот небольшой пример проекта, который я разработал, который использует пользовательский AVVideoCompositing класс с пользовательскими инструкциями, которые реализуют AVVideoCompositionInstructionProtocol

https://github.com/claygarrett/CustomVideoCompositor

Проект позволяет размещать водяной знак поверх видео, но идея может распространяться на все, что вам нужно. Это предотвращает появление ошибки, связанной с AVPlayer.

Еще одно интересное решение в отдельном потоке, которое может помочь: воспроизведение AVPlayer завершается неудачно, когда AVAssetExportSession активен с iOS 10

Я встречал эту проблему на iOS 10.1, и проблема была исправлена ​​на iOS 10.2. Один из способов решить эту проблему на iOS 10.1 - отложить несколько секунд, чтобы добавить playerLayer в слой контейнера.

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