AVVideoCompositionCoreAnimationTool is not animating custom properties during export with AVAssetExportSession

I'm trying to add an animated CALayer over my video and export it with AVAssetExportSession.

I'm animating the layer using CABasicAnimation set to my custom property. However, it seems that func draw(in ctx: CGContext) is never called during an export for my custom layer, and no animation is played.

I found out that animating standard properties like borderWidth works fine, but custom properties are ignored. Can someone help with that?

func export(standard: Bool) {
    print("Exporting...")
    let composition = AVMutableComposition()
    //composition.naturalSize = CGSize(width: 300, height: 300)
    
    // Video track
    let videoTrack = composition.addMutableTrack(withMediaType: .video,
                                                 preferredTrackID: CMPersistentTrackID(1))!
    let _videoAssetURL = Bundle.main.url(forResource: "emptyVideo", withExtension: "mov")!
    let _emptyVideoAsset = AVURLAsset(url: _videoAssetURL)
    let _emptyVideoTrack = _emptyVideoAsset.tracks(withMediaType: .video)[0]
    try! videoTrack.insertTimeRange(CMTimeRange(start: .zero, duration: _emptyVideoAsset.duration),
                                    of: _emptyVideoTrack, at: .zero)
    
    // Root Layer
    let rootLayer = CALayer()
    rootLayer.frame = CGRect(origin: .zero, size: composition.naturalSize)
    
    // Video layer
    let video = CALayer()
    video.frame = CGRect(origin: .zero, size: composition.naturalSize)
    rootLayer.addSublayer(video)
    
    // Animated layer
    let animLayer = CustomLayer()
    animLayer.progress = 0.0
    animLayer.frame = CGRect(origin: .zero, size: composition.naturalSize)
    rootLayer.addSublayer(animLayer)
    
    animLayer.borderColor = UIColor.green.cgColor
    animLayer.borderWidth = 0.0
    let key = standard ? "borderWidth" : "progress"
    let anim = CABasicAnimation(keyPath: key)
    anim.fromValue = 0.0
    anim.toValue = 50.0
    anim.duration = 6.0
    anim.beginTime = AVCoreAnimationBeginTimeAtZero
    anim.isRemovedOnCompletion = false
    animLayer.add(anim, forKey: nil)
    
    // Video Composition
    let videoComposition = AVMutableVideoComposition(propertiesOf: composition)
    videoComposition.renderSize = composition.naturalSize
    videoComposition.frameDuration = CMTime(value: 1, timescale: 30)
    
    // Animation tool
    let animTool = AVVideoCompositionCoreAnimationTool(postProcessingAsVideoLayer: video,
                                                       in: rootLayer)
    
    videoComposition.animationTool  = animTool
    
    // Video instruction > Basic
    let videoInstruction = AVMutableVideoCompositionInstruction()
    videoInstruction.timeRange = CMTimeRange(start: .zero, duration: composition.duration)
    videoComposition.instructions = [videoInstruction]
    
    // Video-instruction > Layer instructions
    let layerInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: videoTrack)
    videoInstruction.layerInstructions = [layerInstruction]
    
    // Session
    let exportSession = AVAssetExportSession(asset: composition,
                                             presetName: AVAssetExportPresetHighestQuality)!
    exportSession.videoComposition = videoComposition
    exportSession.shouldOptimizeForNetworkUse = true
    var url = FileManager.default.temporaryDirectory.appendingPathComponent("\(arc4random()).mov")
    url = URL(fileURLWithPath: url.path)
    exportSession.outputURL = url
    exportSession.outputFileType = .mov
    _session = exportSession
    
    exportSession.exportAsynchronously {
        if let error = exportSession.error {
            print("Fail. \(error)")
        } else {
            print("Ok")
            print(url)
            DispatchQueue.main.async {
                let vc = AVPlayerViewController()
                vc.player = AVPlayer(url: url)
                self.present(vc, animated: true) {
                    vc.player?.play()
                }
            }
        }
    }
}

CustomLayer:

class CustomLayer: CALayer {
    
    @NSManaged var progress: CGFloat
    
    override init() {
        super.init()
    }
    
    override init(layer: Any) {
        let l = layer as! CustomLayer
        super.init(layer: layer)
        
        print("Copy. \(progress) \(l.progress)")
        self.progress = l.progress
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
    }
    
    override class func needsDisplay(forKey key: String) -> Bool {
        let needsDisplayKeys = ["progress"]
        if needsDisplayKeys.contains(key) {
            return true
        }
        return super.needsDisplay(forKey: key)
    }
    
    override func display() {
        print("Display. \(progress) | \(presentation()?.progress)")
        super.display()
    }
    
    override func draw(in ctx: CGContext) {
        // Save / restore ctx
        ctx.saveGState()
        defer { ctx.restoreGState() }
        
        print("Draw. \(progress)")
        ctx.move(to: .zero)
        ctx.addLine(to: CGPoint(x: bounds.size.width * progress,
                                y: bounds.size.height * progress))
        ctx.setStrokeColor(UIColor.red.cgColor)
        ctx.setLineWidth(40)
        ctx.strokePath()
    }
}

Here's a full sample project if someone is interested: https://www.dropbox.com/s/evkm60wkeb2xrzh/BrokenAnimation.zip?dl=0

  • Do you have any updates?

Add a Comment