You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Mac Catalyst导出无Core Animation输出的框架问题求助

Mac Catalyst下AVVideoCompositionCoreAnimationTool导出无Core Animation输出问题

问题概述

仅在Mac Catalyst平台存在视频导出会话中无Core Animation输出的问题,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")
    }
}

已尝试的解决方案

将分数变化的CABasicAnimation替换为CAKeyframeAnimation,但Mac Catalyst导出的视频中仍无动画显示,且已注释其他CABasicAnimation实例:

原代码

/// 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")
}

请求协助

请提供该问题的解决办法,或指导如何实现对应的自定义合成器以完成动画导出。

内容的提问来源于stack exchange,提问作者Curiosity

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.07 17:17:02