如何解决导出会话中未显示核心动画输出的 Mac Catalyst 框架错误?

How do I work around a Mac Catalyst framework bug where no Core Animation output is shown in an export session?

提问人:Curiosity 提问时间:10/23/2023 最后编辑:Curiosity 更新时间:11/1/2023 访问量:127

问:

这被验证为框架错误(发生在 Mac Catalyst 上,但不发生在 iOS 或 iPadOS 上),罪魁祸首似乎是?AVVideoCompositionCoreAnimationTool

/// Exports a video with the target animating.
    func exportVideo() {
        let destinationURL = createExportFileURL(from: Date())
        guard let videoURL = Bundle.main.url(forResource: "black_video", withExtension: "mp4") else {
            delegate?.exporterDidFailExporting(exporter: self)
            print("Can't find video")
            return
        }

        // Initialize the video asset
        let asset = AVURLAsset(url: videoURL, options: [AVURLAssetPreferPreciseDurationAndTimingKey: true])
        guard let assetVideoTrack: AVAssetTrack = asset.tracks(withMediaType: AVMediaType.video).first,
              let assetAudioTrack: AVAssetTrack = asset.tracks(withMediaType: AVMediaType.audio).first else { return }
        let composition = AVMutableComposition()
        guard let videoCompTrack = composition.addMutableTrack(withMediaType: AVMediaType.video, preferredTrackID: Int32(kCMPersistentTrackID_Invalid)),
              let audioCompTrack = composition.addMutableTrack(withMediaType: AVMediaType.audio, preferredTrackID: Int32(kCMPersistentTrackID_Invalid)) else { return }
        videoCompTrack.preferredTransform = assetVideoTrack.preferredTransform

        // Get the duration
        let videoDuration = asset.duration.seconds

        // Get the video rect
        let videoSize = assetVideoTrack.naturalSize.applying(assetVideoTrack.preferredTransform)
        let videoRect = CGRect(origin: .zero, size: videoSize)

        // Initialize the target layers and animations
        animationLayers = TargetView.initTargetViewAndAnimations(atPoint: CGPoint(x: videoRect.midX, y: videoRect.midY), atSecondsIntoVideo: 2, videoRect: videoRect)

        // Set the playback speed
        let duration = CMTime(seconds: videoDuration,
                              preferredTimescale: CMTimeScale(600))
        let appliedRange = CMTimeRange(start: .zero, end: duration)
        videoCompTrack.scaleTimeRange(appliedRange, toDuration: duration)
        audioCompTrack.scaleTimeRange(appliedRange, toDuration: duration)

        // Create the video layer.
        let videolayer = CALayer()
        videolayer.frame = CGRect(origin: .zero, size: videoSize)

        // Create the parent layer.
        let parentlayer = CALayer()
        parentlayer.frame = CGRect(origin: .zero, size: videoSize)
        parentlayer.addSublayer(videolayer)

        let times = timesForEvent(startTime: 0.1, endTime: duration.seconds - 0.01)
        let timeRangeForCurrentSlice = times.timeRange
        // Insert the relevant video track segment
        do {
            try videoCompTrack.insertTimeRange(timeRangeForCurrentSlice, of: assetVideoTrack, at: .zero)
            try audioCompTrack.insertTimeRange(timeRangeForCurrentSlice, of: assetAudioTrack, at: .zero)
        }
        catch let compError {
            print("TrimVideo: error during composition: \(compError)")
            delegate?.exporterDidFailExporting(exporter: self)
            return
        }

        // Add all the non-nil animation layers to be exported.
        for layer in animationLayers.compactMap({ $0 }) {
            parentlayer.addSublayer(layer)
        }

        // Configure the layer composition.
        let layerComposition = AVMutableVideoComposition()
        layerComposition.frameDuration = CMTimeMake(value: 1, timescale: 30)
        layerComposition.renderSize = videoSize
        layerComposition.animationTool = AVVideoCompositionCoreAnimationTool(
            postProcessingAsVideoLayer: videolayer,
            in: parentlayer)
        let instructions = initVideoCompositionInstructions(
            videoCompositionTrack: videoCompTrack, assetVideoTrack: assetVideoTrack)
        layerComposition.instructions = instructions

        // Creates the export session and exports the video asynchronously.
        guard let exportSession = initExportSession(
                composition: composition,
                destinationURL: destinationURL,
                layerComposition: layerComposition) else {
            delegate?.exporterDidFailExporting(exporter: self)
            return
        }
        // Execute the exporting
        exportSession.exportAsynchronously(completionHandler: {
            if let error = exportSession.error {
                print("Export error: \(error), \(error.localizedDescription)")
            }
            self.delegate?.exporterDidFinishExporting(exporter: self, with: destinationURL)
        })
    }

不确定如何实现执行与此可重现案例相同的动画的自定义合成器:

class AnimationCreator: NSObject {

    // MARK: - Target Animations

    /// Creates the target animations.
    static func addAnimationsToTargetView(_ targetView: TargetView, startTime: Double) {
        // Add the appearance animation
        AnimationCreator.addAppearanceAnimation(on: targetView, defaultBeginTime: AVCoreAnimationBeginTimeAtZero, startTime: startTime)
        // Add the pulse animation.
        AnimationCreator.addTargetPulseAnimation(on: targetView, defaultBeginTime: AVCoreAnimationBeginTimeAtZero, startTime: startTime)
        // Add the rotation animation
        AnimationCreator.addRotationAnimation(to: targetView, beginTime: AVCoreAnimationBeginTimeAtZero, startTime: startTime)
    }

    /// Adds the appearance animation to the target
    private static func addAppearanceAnimation(on targetView: TargetView, defaultBeginTime: Double = 0, startTime: Double = 0) {
        // Starts the target transparent and then turns it opaque at the specified time
        targetView.targetImageView.layer.opacity = 0
        let appear = CABasicAnimation(keyPath: "opacity")
        appear.duration = .greatestFiniteMagnitude // stay on screen forever
        appear.fromValue = 1.0 // Opaque
        appear.toValue = 1.0 // Opaque
        appear.beginTime = defaultBeginTime + startTime
        targetView.targetImageView.layer.add(appear, forKey: "appear")
    }

    /// Adds a pulsing animation to the target.
    private static func addTargetPulseAnimation(on targetView: TargetView, defaultBeginTime: Double = 0, startTime: Double = 0) {
        let targetPulse = CABasicAnimation(keyPath: "transform.scale")
        targetPulse.fromValue = 1 // Regular size
        targetPulse.toValue = 1.1 // Slightly larger size
        targetPulse.duration = 0.4
        targetPulse.beginTime = defaultBeginTime + startTime
        targetPulse.autoreverses = true
        targetPulse.repeatCount = .greatestFiniteMagnitude
        targetView.targetImageView.layer.add(targetPulse, forKey: "pulse_animation")
    }

    /// Adds a spinning animation to the target.
    private static func addRotationAnimation(to targetView: TargetView, beginTime: Double = 0, startTime: Double = 0) {
        let rotation: CABasicAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
        rotation.toValue = Double.pi * 2 // rotate in a complete circle
        rotation.duration = 1.0
        rotation.isCumulative = true
        rotation.repeatCount = .greatestFiniteMagnitude
        rotation.beginTime = beginTime
        targetView.targetImageView.layer.add(rotation, forKey: "rotation_animation")
    }
}

在一个更大的项目中,我试图改变

    /// Creates the score change animation for a new point.
    func initScoreChangeStartAnimation(scoreViewLayer: CALayer, startTime: Double, duration: Double) {
        let scoreAnimation = CABasicAnimation(keyPath: "opacity")
        scoreAnimation.duration = duration - VideoHighlightReelOverlayUIConstants.pointChangeAnimationDuration
        scoreAnimation.fromValue = 1
        scoreAnimation.toValue = 1
        scoreAnimation.beginTime = startTime

        // Add the animation to the score view layer.
        scoreViewLayer.add(scoreAnimation, forKey: "scoreChangeStart")
    }

    /// Creates the score change animation for the score of the next point.
    func initScoreChangeEndAnimation(scoreViewLayer: CALayer, startTime: Double, duration: Double) {
        let scoreAnimation = CABasicAnimation(keyPath: "opacity")
        scoreAnimation.duration = VideoHighlightReelOverlayUIConstants.pointChangeAnimationDuration
        scoreAnimation.fromValue = 1
        scoreAnimation.toValue = 1
        scoreAnimation.beginTime = startTime + (duration - VideoHighlightReelOverlayUIConstants.pointChangeAnimationDuration)

        // Add the animation to the score view layer.
        scoreViewLayer.add(scoreAnimation, forKey: "scoreChangeEnd")
    }

    /// Creates the score change animation for a new point.
    func initScoreChangeStartAnimation(scoreViewLayer: CALayer, startTime: Double, duration: Double) {
        let scoreAnimation = CAKeyframeAnimation(keyPath: "opacity")
        let mainDuration = duration - VideoHighlightReelOverlayUIConstants.pointChangeAnimationDuration
        
        scoreAnimation.values = [1.0, 1.0]
        scoreAnimation.keyTimes = [0, NSNumber(value: mainDuration/duration)]
        scoreAnimation.duration = duration
        scoreAnimation.beginTime = startTime
        
        // Add the animation to the score view layer.
        scoreViewLayer.add(scoreAnimation, forKey: "scoreChangeStart")
    }

    /// Creates the score change animation for the score of the next point.
    func initScoreChangeEndAnimation(scoreViewLayer: CALayer, startTime: Double, duration: Double) {
        let scoreAnimation = CAKeyframeAnimation(keyPath: "opacity")
        let mainDuration = VideoHighlightReelOverlayUIConstants.pointChangeAnimationDuration
        
        scoreAnimation.values = [1.0, 1.0]
        scoreAnimation.keyTimes = [NSNumber(value: (duration - mainDuration)/duration), 1]
        scoreAnimation.duration = duration
        scoreAnimation.beginTime = startTime
        
        // Add the animation to the score view layer.
        scoreViewLayer.add(scoreAnimation, forKey: "scoreChangeEnd")
    }

但它不会出现在 Mac Catalyst 导出中。我还注释掉了 .CABasicAnimation

iOS Swift Core-动画 金属 MAC-催化剂

评论

0赞 Jeshua Lacock 10/25/2023
如果你提供一个完整的可重复项目,我会看一看。
0赞 Curiosity 10/26/2023
嗨,@JeshuaLacock,链接在这里!drive.google.com/file/d/12ep1H9JECFoBI23Xan-sMpDPPXswSuE6/......
0赞 Jeshua Lacock 10/27/2023
我收到一个黑色视频,但不确定它应该做什么?我打算将它与工作版本进行比较,但目标仅适用于 Mac。
0赞 Curiosity 10/27/2023
预期行为是在 iOS 设备或模拟器上运行时。它被设置为 Mac Catalyst 应用程序,因此它应该在 Mac、iPad 或 iPhone 上运行。
0赞 Jeshua Lacock 10/27/2023
我的错 - 目标的名称“Mac”让我失望了。你是对的 - 动画在 iPad 上对我有用,但在 Mac 上没有。我去看看。

答:

2赞 Jeshua Lacock 10/30/2023 #1

我无法确定为什么不起作用,但确实有效。下面验证了工作代码。如果您有任何问题,请告诉我。CABasicAnimationCAKeyframeAnimation

没有什么是你不能做的,所以这可以按照你的要求,实现一个功能齐全的解决方法。事实上,能力要强得多。CABasicAnimationCAKeyframeAnimationCAKeyframeAnimation

import UIKit
import AVFoundation

class AnimationCreator: NSObject {

    // MARK: - Target Animations

    /// Creates the target animations.
    static func addAnimationsToTargetView(_ targetView: TargetView, startTime: Double) {
        // Add the appearance animation
        AnimationCreator.addAppearanceAnimation(on: targetView, defaultBeginTime: AVCoreAnimationBeginTimeAtZero, startTime: startTime)
        // Add the pulse animation.
        AnimationCreator.addTargetPulseAnimation(on: targetView, defaultBeginTime: AVCoreAnimationBeginTimeAtZero, startTime: startTime)
        // Add the rotation animation
        AnimationCreator.addRotationAnimation(to: targetView, beginTime: AVCoreAnimationBeginTimeAtZero, startTime: startTime)
    }

    /// Adds the appearance animation to the target
    private static func addAppearanceAnimation(on targetView: TargetView, defaultBeginTime: Double = 0, startTime: Double = 0) {
        // Starts the target transparent and then turns it opaque at the specified time
        targetView.targetImageView.layer.opacity = 1.0

        let animation = CAKeyframeAnimation(keyPath: #keyPath(CALayer.opacity))
        animation.duration = 2.25
        animation.repeatCount = .zero
        animation.values = [
            CGFloat(0.0),
            CGFloat(1.0)
            ] as [CGFloat]
        animation.beginTime = AVCoreAnimationBeginTimeAtZero
        animation.isRemovedOnCompletion = false
        targetView.targetImageView.layer.add(animation, forKey: nil)
    }

    /// Adds a pulsing animation to the target.
    private static func addTargetPulseAnimation(on targetView: TargetView, defaultBeginTime: Double = 0, startTime: Double = 0) {
        let animation = CAKeyframeAnimation(keyPath: "transform.scale")
        animation.duration = 2.25
        animation.repeatCount = .infinity
        animation.values = [1.0, 1.1]
        animation.beginTime = AVCoreAnimationBeginTimeAtZero
        animation.isRemovedOnCompletion = false
        targetView.targetImageView.layer.add(animation, forKey: nil)
    }

    /// Adds a spinning animation to the target.
    private static func addRotationAnimation(to targetView: TargetView, beginTime: Double = 0, startTime: Double = 0) {
        let animation = CAKeyframeAnimation(keyPath: "transform.rotation")
        animation.duration = 1.0
        animation.repeatCount = .infinity
        animation.values = [0.0, Double.pi * 2]
        animation.beginTime = AVCoreAnimationBeginTimeAtZero
        animation.isRemovedOnCompletion = false
        targetView.targetImageView.layer.add(animation, forKey: nil)
    }
}

评论

0赞 Curiosity 11/1/2023
嗨,Jeshua,不幸的是,我无法在更大的项目中使用它,并为有关它的问题添加了详细信息。我试图通过注释掉其他实例来隔离问题。由于它是一个大型应用程序,我无法提供更多上下文,但如果您有任何指导,我们将不胜感激。CABasicAnimation
0赞 Jeshua Lacock 11/2/2023
在您发布的代码中,您将不透明度从 1.0 动画化为 1.0,这意味着它不会执行任何操作。.scoreAnimation.values = [1.0, 1.0]