Невозможно отобразить анимированный CALayer в видео, используя AVVideoCompositionCoreAnimationTool - PullRequest
0 голосов
/ 20 сентября 2018

ОБНОВЛЕНИЕ 6: Мне удалось полностью решить мою проблему, но я все еще хотел бы лучшего объяснения, чем, как я предполагаю, причина, по которой это не сработало, если я ошибаюсь

I 'я пытался анимировать спрайт-лист поверх видео, но каждый раз, когда я экспортирую видео, конечным результатом является пример видео, с которого я начинаю.

Вот мой код:

Сначала запустите мой пользовательский CALayerдля работы со своими собственными спрайт-листами

class SpriteLayer: CALayer {
    var frameIndex: Int

    override init() {
        // Using 0 as a default state
        self.frameIndex = 0
        super.init()
    }

    required init?(coder aDecoder: NSCoder) {
        self.frameIndex = 0
        super.init(coder: aDecoder)
    }

    override func display() {
        let currentFrameIndex = self.frameIndex
        if currentFrameIndex == 0 {
            return
        }
        let frameSize = self.contentsRect.size
        self.contentsRect = CGRect(x: 0, y: CGFloat(currentFrameIndex - 1) * frameSize.height, width: frameSize.width, height: frameSize.height)
    }

    override func action(forKey event: String) -> CAAction? {
        if event == "contentsRect" {
            return nil
        }
        return super.action(forKey: event)
    }

    override class func needsDisplay(forKey key: String) -> Bool {
        return key == "frameIndex"
    }
}

Gif - это базовый класс, в котором нет ничего сложного, и он прекрасно работает.gif.Strip - это UIImage вертикального листа спрайта, представляющего рисунок.

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

func convertAndExport(to url :URL , completion: @escaping () -> Void ) {
        // Get Initial info and make sure our destination is available
        self.outputURL = url
        let stripCgImage = self.gif.strip!.cgImage!
        // This is used to time how long the export took
        let start = DispatchTime.now()
        do {
            try FileManager.default.removeItem(at: outputURL)
        } catch {
            print("Remove Error: \(error.localizedDescription)")
            print(error)
        }
        // Find and load "sample.mp4" as a AVAsset
        let videoPath = Bundle.main.path(forResource: "sample", ofType: "mp4")!
        let videoUrl = URL(fileURLWithPath: videoPath)
        let videoAsset = AVAsset(url: videoUrl)
        // Start a new mutable Composition with the same base video track
        let mixComposition = AVMutableComposition()
        let compositionVideoTrack = mixComposition.addMutableTrack(withMediaType: .video, preferredTrackID: kCMPersistentTrackID_Invalid)!
        let clipVideoTrack = videoAsset.tracks(withMediaType: .video).first!
        do {
            try compositionVideoTrack.insertTimeRange(CMTimeRangeMake(kCMTimeZero, videoAsset.duration), of: clipVideoTrack, at: kCMTimeZero)
        } catch {
            print("Insert Error: \(error.localizedDescription)")
            print(error)
            return
        }
        compositionVideoTrack.preferredTransform = clipVideoTrack.preferredTransform
        // Quick access to the video size
        let videoSize = clipVideoTrack.naturalSize
        // Setup CALayer and it's animation
        let aLayer = SpriteLayer()
        aLayer.contents = stripCgImage
        aLayer.frame = CGRect(x: 0, y: 0, width: videoSize.width, height: videoSize.height)
        aLayer.opacity = 1.0
        aLayer.masksToBounds = true
        aLayer.bounds = CGRect(x: 0, y: 0, width: videoSize.width, height: videoSize.height)
        aLayer.contentsRect = CGRect(x: 0, y: 0, width: 1, height: 1.0 / 3.0)
        let spriteAnimation = CABasicAnimation(keyPath: "frameIndex")
        spriteAnimation.fromValue = 1
        spriteAnimation.toValue = 4
        spriteAnimation.duration = 2.25
        spriteAnimation.repeatCount = .infinity
        spriteAnimation.autoreverses = false
        spriteAnimation.beginTime = AVCoreAnimationBeginTimeAtZero
        aLayer.add(spriteAnimation, forKey: nil)
        // Setup Layers for AVVideoCompositionCoreAnimationTool
        let parentLayer = CALayer()
        let videoLayer = CALayer()
        parentLayer.frame = CGRect(x: 0, y: 0, width: videoSize.width, height: videoSize.height)
        videoLayer.frame = CGRect(x: 0, y: 0, width: videoSize.width, height: videoSize.height)
        parentLayer.addSublayer(videoLayer)
        parentLayer.addSublayer(aLayer)
        // Create the mutable video composition
        let videoComp = AVMutableVideoComposition()
        videoComp.renderSize = videoSize
        videoComp.frameDuration = CMTimeMake(1, 30)
        videoComp.animationTool = AVVideoCompositionCoreAnimationTool(postProcessingAsVideoLayer: videoLayer, in: parentLayer)
        // Set the video composition to apply to the composition's video track
        let instruction = AVMutableVideoCompositionInstruction()
        instruction.timeRange = CMTimeRangeMake(kCMTimeZero, mixComposition.duration)
        let videoTrack = mixComposition.tracks(withMediaType: .video).first!
        let layerInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: videoTrack)
        instruction.layerInstructions = [layerInstruction]
        videoComp.instructions = [instruction]
        // Initialize export session
        let assetExport = AVAssetExportSession(asset: mixComposition, presetName: AVAssetExportPresetPassthrough)!
        assetExport.videoComposition = videoComp
        assetExport.outputFileType = AVFileType.mp4
        assetExport.outputURL = self.outputURL
        assetExport.shouldOptimizeForNetworkUse = true
        // Export
        assetExport.exportAsynchronously {
            let status = assetExport.status
            switch status {
            case .failed:
                print("Export Failed")
                print("Export Error: \(assetExport.error!.localizedDescription)")
                print(assetExport.error!)
            case .unknown:
                print("Export Unknown")
            case .exporting:
                print("Export Exporting")
            case .waiting:
                print("Export Waiting")
            case .cancelled:
                print("Export Cancelled")
            case .completed:
                let end = DispatchTime.now()
                let nanoTime = end.uptimeNanoseconds - start.uptimeNanoseconds
                let timeInterval = Double(nanoTime) / 1_000_000_000
                // Function is now over, we can print how long it took
                print("Time to generate video: \(timeInterval) seconds")
                completion()
            }
        }
}

РЕДАКТИРОВАТЬ: я основал свой код на следующих ссылках

ОБНОВЛЕНИЕ 1: я попытался удалить часть CABasicAnimation моего кода и поигралс моим CALayer, но безрезультатно. Я не могу даже заставить изображение отображаться. Чтобы проверить это, я попытался анимировать этот лист спрайта, используя CAKeyframeAnimation on contentsRect в Xcode Playground, и он работал нормально, так чтоЯ не думаю, что проблема связана с CABasicAnimation и, возможно, даже не с самим CALayer. Я мог бы действительно использовать некоторую помощь в этом, потому что я не понимаю, почему я не могу даже заставить изображение показывать поверх моегоПример видео на экспорт.

ОБНОВЛЕНИЕ 2: В ответ на комментарий Мэтта я попытался на время забыть о листе спрайта и изменил его на CATextLayer, но все еще не вижу ничего в моем видео (в нем есть темные изображения, поэтому белый текст должен быть

let aLayer = CATextLayer()
aLayer.string = "This is a test"
aLayer.fontSize = videoSize.height / 6
aLayer.alignmentMode = kCAAlignmentCenter
aLayer.foregroundColor = UIColor.white.cgColor
aLayer.bounds = CGRect(x: 0, y: 0, width: videoSize.width, height: videoSize.height / 6)

ОБНОВЛЕНИЕ 3: По запросу Мэтта я попытался изменить parentLayer.addSublayer(aLayer) на videoLayer.addSublayer(aLayer), но все равно ничего не изменилось, но я так и думал, потому что документация дляAVVideoCompositionCoreAnimationTool выглядит следующим образом

convenience init(postProcessingAsVideoLayer videoLayer: CALayer, 
              in animationLayer: CALayer)

означает, что мой parentLayer это animationLayer и, вероятно, означает, что любые анимации должны быть выполнены в этом слое.

ОБНОВЛЕНИЕ 4: Я начинаючтобы сойти с ума здесь, я на данный момент отказался от идеи показа текста или анимированного изображения, я просто хочу повлиять на свое видео любым возможным способом, поэтому я изменил aLayer на это:

let aLayer = CALayer()
aLayer.frame = CGRect(x: 0, y: 0, width: videoSize.width, height: videoSize.height)
aLayer.backgroundColor = UIColor.white.cgColor

Ну, это абсолютно ничего не делает, я все еще получаю свой пример видео на своем outputUrl (я начал тестировать это на игровой площадке со следующим кодом, если вы хотите «поиграть» вместе)

import PlaygroundSupport
import UIKit
import Foundation
import AVFoundation

func convertAndExport(to url :URL , completion: @escaping () -> Void ) {
    let start = DispatchTime.now()
    do {
        try FileManager.default.removeItem(at: url)
    } catch {
        print("Remove Error: \(error.localizedDescription)")
        print(error)
    }

    let videoPath = Bundle.main.path(forResource: "sample", ofType: "mp4")!
    let videoUrl = URL(fileURLWithPath: videoPath)
    let videoAsset = AVURLAsset(url: videoUrl)
    let mixComposition = AVMutableComposition()
    let compositionVideoTrack = mixComposition.addMutableTrack(withMediaType: .video, preferredTrackID: kCMPersistentTrackID_Invalid)!
    let clipVideoTrack = videoAsset.tracks(withMediaType: .video).first!

    do {
        try compositionVideoTrack.insertTimeRange(CMTimeRangeMake(kCMTimeZero, videoAsset.duration), of: clipVideoTrack, at: kCMTimeZero)
    } catch {
        print("Insert Error: \(error.localizedDescription)")
        print(error)
        return
    }
    compositionVideoTrack.preferredTransform = clipVideoTrack.preferredTransform
    let videoSize = clipVideoTrack.naturalSize
    print("Video Size Detected: \(videoSize.width) x \(videoSize.height)")

    let aLayer = CALayer()
    aLayer.frame = CGRect(x: 0, y: 0, width: videoSize.width, height: videoSize.height)
    aLayer.backgroundColor = UIColor.white.cgColor

    let parentLayer = CALayer()
    let videoLayer = CALayer()
    parentLayer.frame = CGRect(x: 0, y: 0, width: videoSize.width, height: videoSize.height)
    videoLayer.frame = CGRect(x: 0, y: 0, width: videoSize.width, height: videoSize.height)
    parentLayer.addSublayer(videoLayer)
    parentLayer.addSublayer(aLayer)
    aLayer.setNeedsDisplay()
    let videoComp = AVMutableVideoComposition()
    videoComp.renderSize = videoSize
    videoComp.frameDuration = CMTimeMake(1, 30)
    videoComp.animationTool = AVVideoCompositionCoreAnimationTool(postProcessingAsVideoLayer: videoLayer, in: parentLayer)

    let instruction = AVMutableVideoCompositionInstruction()
    instruction.timeRange = CMTimeRangeMake(kCMTimeZero, mixComposition.duration)
    let videoTrack = mixComposition.tracks(withMediaType: .video).first!
    let layerInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: videoTrack)
    instruction.layerInstructions = [layerInstruction]
    videoComp.instructions = [instruction]

    let assetExport = AVAssetExportSession(asset: mixComposition, presetName: AVAssetExportPresetPassthrough)!
    assetExport.videoComposition = videoComp
    assetExport.outputFileType = AVFileType.mp4
    assetExport.outputURL = url
    assetExport.shouldOptimizeForNetworkUse = true

    assetExport.exportAsynchronously {
        let status = assetExport.status
        switch status {
        case .failed:
            print("Export Failed")
            print("Export Error: \(assetExport.error!.localizedDescription)")
            print(assetExport.error!)
        case .unknown:
            print("Export Unknown")
        case .exporting:
            print("Export Exporting")
        case .waiting:
            print("Export Waiting")
        case .cancelled:
            print("Export Cancelled")
        case .completed:
            let end = DispatchTime.now()
            let nanoTime = end.uptimeNanoseconds - start.uptimeNanoseconds
            let timeInterval = Double(nanoTime) / 1_000_000_000
            print("Time to generate video: \(timeInterval) seconds")
            completion()
        }
    }
}

let outputUrl = FileManager.default.temporaryDirectory.appendingPathComponent("test.mp4")
convertAndExport(to: outputUrl) {
    print(outputUrl)
}

Пожалуйста, кто-нибудь поможетя нижеи что я делаю не так ...

ОБНОВЛЕНИЕ 5: я запускаю все, кроме тестов на игровых площадках с iPad Air 2 (так что без симулятора), потому что я использую камеру, чтобы делать снимки, а затем сшивать их вСпрайт лист, который я тогда планировал анимировать на видео, которое я отправил по электронной почте.Я начал проводить тестирование Playground, потому что каждый тест с iPad требовал от меня прохождения всего цикла приложения (обратный отсчет, фотографии, форма, отправка / получение электронной почты)

1 Ответ

0 голосов
/ 24 сентября 2018

Хорошо, наконец-то все заработало, как я всегда хотел.

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

  • Прежде всего
let assetExport = AVAssetExportSession(asset: mixComposition, presetName: AVAssetExportPresetPassthrough)!

Мне нужно было использовать AVAssetExportPresetHighestQuality вместо AVAssetExportPresetPassthrough.Я предполагаю, что предустановка «passthrough» означает, что вы не выполняете никакого перекодирования, поэтому установив его на самое высокое (не среднее, потому что мое экспортированное видео имеет размер более 400x400), я смог перекодировать свое видео.Я предполагаю, что это было то, что мешало экспортированному видео содержать какие-либо из CALayer, которые я пробовал (даже покрытие видео белым).

  • Во-вторых (не уверен, действительно ли это влияетно я попробую позже)
parentLayer.addSublayer(aLayer)

Я заменил это на:

videoLayer.addSublayer(aLayer)

Не уверен, что это действительно имеет значение, но я понимаю, что это был на самом деле слой анимациидля AVVideoCompositionCoreAnimationTool и parentLayer был просто контейнер, который не должен содержать больше, но я, вероятно, ошибаюсь.

  • Третье изменение, которое я сделал
let spriteAnimation = CABasicAnimation(keyPath: "frameIndex")
spriteAnimation.fromValue = 1
spriteAnimation.toValue = 4
spriteAnimation.duration = 2.25
spriteAnimation.repeatCount = .infinity
spriteAnimation.autoreverses = false
spriteAnimation.beginTime = AVCoreAnimationBeginTimeAtZero
aLayer.add(spriteAnimation, forKey: nil)

Я изменил это на следующее:

let animation = CAKeyframeAnimation(keyPath: #keyPath(CALayer.contentsRect))
animation.duration = 2.25
animation.calculationMode = kCAAnimationDiscrete
animation.repeatCount = .infinity
animation.values = [
    CGRect(x: 0, y: 0, width: 1, height: 1/3.0),
    CGRect(x: 0, y: 1/3.0, width: 1, height: 1/3.0),
    CGRect(x: 0, y: 2/3.0, width: 1, height: 1/3.0)
    ] as [CGRect]
animation.beginTime = AVCoreAnimationBeginTimeAtZero
animation.fillMode = kCAFillModeBackwards
animation.isRemovedOnCompletion = false
aLayer.add(animation, forKey: nil)

Это изменение в основном удаляло мои пользовательские анимации для листа спрайта (так как оно всегда будет таким же, я сначала хотел рабочий пример, затем я обобщу его ивероятно, добавьте его в мой частный пользовательский интерфейс).Но самое главное animation.isRemovedOnCompletion = false Я заметил, что удаление этого делает так, чтобы анимация просто не воспроизводилась на экспортированном видео.Так что для тех, у кого CABasicAnimation нет анимации на видео после экспорта, попробуйте посмотреть, правильно ли установлен isRemovedOnCompletion на вашей анимации.

Я думаю, что это почти все, что я сделал.

Хотя я технически ответил на мой вопрос, моя награда остается понять, как AVVideoCompositionCoreAnimationTool и AVAssetExport работают и почему я должен был внести изменения, которые я сделал, чтобы наконец заставить его работать, если кто-то заинтересован в объяснении.

Еще раз спасибо Мэтту, вы помогли мне, показав мне, как вы это сделали.

...