使用 CIFilters 在 CALayer 层次结构中渲染视频

Rendering a video in a CALayer hierarchy using CIFilters

在我的 iOS 应用程序的 UI 中,我显示了 CALayer 的复杂层次结构。其中一个图层是 AVPlayerLayer,它显示实时应用了 CIFilter 秒的视频(使用 AVVideoComposition(asset:, applyingCIFiltersWithHandler:))。

现在我想将这个图层合成导出到一个视频文件中。 AVFoundation 中有两个工具似乎很有用:

AAVVideoCompositionCoreAnimationTool 允许在(可能是动画的)CALayer 层次结构中渲染视频

BAVVideoComposition(asset:, applyingCIFiltersWithHandler:),我也在 UI 中使用它来将 CIFilter 应用于视频资产。

但是,这两个工具不能同时使用:如果我启动一个结合了这些工具的 AVAssetExportSessionAVFoundation 会抛出一个 NSInvalidArgumentException:

Expecting video composition to contain only AVCoreImageFilterVideoCompositionInstruction

我尝试如下解决此限制:

解决方法 1

1) 使用 AVAssetReaderAVAssetWriter

设置导出

2) 从资产 reader 获取样本缓冲区并应用 CIFilter,将结果保存在 CGImage.

3) 将层级中的CGImage设置为视频层的content。现在图层层次结构 "looks like" 最终视频的一帧。

4) 使用 CVPixelBufferGetBaseAddress 从资产编写器获取每个帧的 CVPixelBuffer 数据,并使用该数据创建 CGContext

5) 使用 CALayer.render(in ctx: CGContext).

将图层渲染到该上下文

此设置有效,但速度极慢 - 导出 5 秒的视频有时需要一分钟。看起来 CoreGraphics 调用是这里的瓶颈(我想那是因为使用这种方法合成发生在 CPU 上?)

解决方法 2

另一种方法可以分两步完成:首先,将源视频与应用于文件的过滤器一起保存,如 B,然后使用该视频文件以将视频嵌入层合成中,如 A。但是,由于它使用了两次传递,我想这并没有达到预期的效率。

总结

将此视频导出到文件的好方法是什么,最好一次通过?如何同时使用 CIFilters 和 AVVideoCompositionCoreAnimationTool?有没有一种结合这些工具的本机方法可以在 AVFoundation 中设置 "pipeline"?

实现此目的的方法是使用自定义 AVVideoCompositing。该对象允许您合成(在本例中应用 CIFilter)每个视频帧。

下面是一个将 CIPhotoEffectNoir 效果应用于整个视频的示例实现:

class VideoFilterCompositor: NSObject, AVVideoCompositing {

    var sourcePixelBufferAttributes: [String : Any]? = [kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA]
    var requiredPixelBufferAttributesForRenderContext: [String : Any] = [kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA]
    private var renderContext: AVVideoCompositionRenderContext?

    func renderContextChanged(_ newRenderContext: AVVideoCompositionRenderContext) {
        renderContext = newRenderContext
    }

    func cancelAllPendingVideoCompositionRequests() {
    }

    private let filter = CIFilter(name: "CIPhotoEffectNoir")!
    private let context = CIContext()
    func startRequest(_ asyncVideoCompositionRequest: AVAsynchronousVideoCompositionRequest) {
        guard let track = asyncVideoCompositionRequest.sourceTrackIDs.first?.int32Value, let frame = asyncVideoCompositionRequest.sourceFrame(byTrackID: track) else {
            asyncVideoCompositionRequest.finish(with: NSError(domain: "VideoFilterCompositor", code: 0, userInfo: nil))
            return
        }
        filter.setValue(CIImage(cvPixelBuffer: frame), forKey: kCIInputImageKey)
        if let outputImage = filter.outputImage, let outBuffer = renderContext?.newPixelBuffer() {
            context.render(outputImage, to: outBuffer)
            asyncVideoCompositionRequest.finish(withComposedVideoFrame: outBuffer)
        } else {
            asyncVideoCompositionRequest.finish(with: NSError(domain: "VideoFilterCompositor", code: 0, userInfo: nil))
        }
    }

}

如果您需要在不同时间使用不同的过滤器,您可以使用自定义 AVVideoCompositionInstructionProtocol,您可以从 AVAsynchronousVideoCompositionRequest

接下来,您需要将此与您的 AVMutableVideoComposition 一起使用,因此:

let videoComposition = AVMutableVideoComposition()
videoComposition.customVideoCompositorClass = VideoFilterCompositor.self
//Add your animator tool as usual
let animator = AVVideoCompositionCoreAnimationTool(postProcessingAsVideoLayer: v, in: p)
videoComposition.animationTool = animator
//Finish setting up the composition

有了这个,您应该能够使用常规 AVAssetExportSession 导出视频,设置其 videoComposition