Search code examples
iosvideoswift5cifilteravvideocomposition

switch CIFilters to video


can someone help me to understand the correct way to switch CIFilters without restart video player?

I have a local video playing inside a view. If I tap a cell in collection view, video will change the CIFilter.

my code

let filter = CIFilter(name: "CIPhotoEffectNoir")!
let asset = AVAsset(url: fooURL)
let item = AVPlayerItem(asset: asset)
item.videoComposition = AVVideoComposition(asset: asset,  applyingCIFiltersWithHandler: { request in
    let source = request.sourceImage.clampedToExtent()
    filter.setValue(source, forKey: kCIInputImageKey)

    let output = filter.outputImage

    request.finish(with: output!, context: nil)
})

player = AVPlayer(playerItem: item)
let playerLayer = AVPlayerLayer(player: player)
playerLayer.frame = videoViewDetail.bounds
videoViewDetail.layer.addSublayer(playerLayer)
player?.play()

code works great and effect is correct apply

but i have a collectionview with many CIFilters. On tap to a cell i cannot find the way to switch the filter associated to video. If i recreate a new player with the new filter and I add it as substitute to the current "addsublayer" the player will restart video.

@IBAction func tap(_ sender: UITapGestureRecognizer) {

    let location = sender.location(in: self.collectionView)
    let indexPath = self.collectionView.indexPathForItem(at: location)

    if let index = indexPath {
        // code to switch to another CIFilter like for example "CISepiaTone"
    }
}

What is the best way to change CIFilters to the playing video without restart the video Is it possible to save the video with the new filter?

Thanks!


Solution

  • The handler block that you supply when creating the AVVideoComposition is continuously called for every video frame the player is showing. That means you just have to switch the filter that is used inside that block.

    The easiest way to achieve that is to reference a filter from outside the block instead of capturing it. Then you can just change the reference at runtime.

    For example, assuming your code is run inside some view controller method, you can do the following:

    class MyViewController: UIViewController {
    
        var filter: CIFilter = CIFilter(name: "CIPhotoEffectNoir")!
    
        func createPlayer() {
            let asset = AVAsset(url: fooURL)
            let item = AVPlayerItem(asset: asset)
            item.videoComposition = AVVideoComposition(asset: asset,  applyingCIFiltersWithHandler: { [weak self] request in
                guard let self = self else {
                    request.finish(with error: SomeError)
                    return
                }
                let source = request.sourceImage.clampedToExtent()
                self.filter.setValue(source, forKey: kCIInputImageKey)
    
                let output = self.filter.outputImage
    
                request.finish(with: output!, context: nil)
            })
    
            player = AVPlayer(playerItem: item)
            let playerLayer = AVPlayerLayer(player: player)
            playerLayer.frame = videoViewDetail.bounds
            videoViewDetail.layer.addSublayer(playerLayer)
            player?.play()
        }
    
        // ...
    
    }
    

    Then you can easily change the filter by just changing self.filter.

    (You may want to synchronize access to the filter though, to avoid concurrency issues.)