代码之家  ›  专栏  ›  技术社区  ›  Ian Bytchek

制作CIContext.render(CIImage,CVPixelBuffer)使用AVAssetWriter

  •  0
  • Ian Bytchek  · 技术社区  · 5 年前

    我想用核心图像来处理 CGImage 对象并将其转换为QuickTime电影 马科斯 . 下面的代码演示了需要什么,但是 output contains a lot of blank (black) frames :

    import AppKit
    import AVFoundation
    import CoreGraphics
    import Foundation
    import CoreVideo
    import Metal
    
    // Video output url.
    let url: URL = try! FileManager.default.url(for: .downloadsDirectory, in: .userDomainMask, appropriateFor: nil, create: false).appendingPathComponent("av.mov")
    try? FileManager.default.removeItem(at: url)
    
    // Video frame size, total frame count, frame rate and frame image.
    let frameSize: CGSize = CGSize(width: 2000, height: 1000)
    let frameCount: Int = 100
    let frameRate: Double = 1 / 30
    let frameImage: CGImage
    
    frameImage = NSImage(size: frameSize, flipped: false, drawingHandler: {
        NSColor.red.setFill()
        $0.fill()
        return true
    }).cgImage(forProposedRect: nil, context: nil, hints: nil)!
    
    let pixelBufferAttributes: [CFString: Any]
    let outputSettings: [String: Any]
    
    pixelBufferAttributes = [
        kCVPixelBufferPixelFormatTypeKey: Int(kCVPixelFormatType_32ARGB),
        kCVPixelBufferWidthKey: Float(frameSize.width),
        kCVPixelBufferHeightKey: Float(frameSize.height),
        kCVPixelBufferMetalCompatibilityKey: true,
        kCVPixelBufferCGImageCompatibilityKey: true,
        kCVPixelBufferCGBitmapContextCompatibilityKey: true,
    ]
    
    outputSettings = [
        AVVideoCodecKey: AVVideoCodecType.h264,
        AVVideoWidthKey: Int(frameSize.width),
        AVVideoHeightKey: Int(frameSize.height),
    ]
    
    let writer: AVAssetWriter = try! AVAssetWriter(outputURL: url, fileType: .mov)
    let input: AVAssetWriterInput = AVAssetWriterInput(mediaType: .video, outputSettings: outputSettings)
    let pixelBufferAdaptor: AVAssetWriterInputPixelBufferAdaptor = AVAssetWriterInputPixelBufferAdaptor(assetWriterInput: input, sourcePixelBufferAttributes: pixelBufferAttributes as [String: Any])
    
    input.expectsMediaDataInRealTime = true
    
    precondition(writer.canAdd(input))
    writer.add(input)
    
    precondition(writer.startWriting())
    writer.startSession(atSourceTime: CMTime.zero)
    
    let colorSpace: CGColorSpace = CGColorSpace(name: CGColorSpace.sRGB) ?? CGColorSpaceCreateDeviceRGB()
    let context = CIContext(mtlDevice: MTLCreateSystemDefaultDevice()!)
    
    Swift.print("Starting the render…")
    
    // Preferred scenario: using CoreImage to fill the buffer from the pixel buffer adapter. Shows that
    // CIImage + AVAssetWriterInputPixelBufferAdaptor are not working together.
    
    for frameNumber in 0 ..< frameCount {
        var pixelBuffer: CVPixelBuffer?
        guard let pixelBufferPool: CVPixelBufferPool = pixelBufferAdaptor.pixelBufferPool else { preconditionFailure() }
        precondition(CVPixelBufferPoolCreatePixelBuffer(nil, pixelBufferPool, &pixelBuffer) == kCVReturnSuccess)
    
        precondition(CVPixelBufferLockBaseAddress(pixelBuffer!, []) == kCVReturnSuccess)
        defer { precondition(CVPixelBufferUnlockBaseAddress(pixelBuffer!, []) == kCVReturnSuccess) }
    
        let ciImage = CIImage(cgImage: frameImage)
        context.render(ciImage, to: pixelBuffer!)
    
        // 💥 This fails – the pixel buffer doesn't get filled. AT ALL! Why? How to make it work?
        let bytes = UnsafeBufferPointer(start: CVPixelBufferGetBaseAddress(pixelBuffer!)!.assumingMemoryBound(to: UInt8.self), count: CVPixelBufferGetDataSize(pixelBuffer!))
        precondition(bytes.contains(where: { $0 != 0 }))
    
        while !input.isReadyForMoreMediaData { Thread.sleep(forTimeInterval: 10 / 1000) }
        precondition(pixelBufferAdaptor.append(pixelBuffer!, withPresentationTime: CMTime(seconds: Double(frameNumber) * frameRate, preferredTimescale: 600)))
    }
    
    
    // Unpreferred scenario: using CoreImage to fill the manually created buffer. Proves that CIImage 
    // can fill buffer and working.
    
    // for frameNumber in 0 ..< frameCount {
    //     var pixelBuffer: CVPixelBuffer?
    //     precondition(CVPixelBufferCreate(nil, frameImage.width, frameImage.height, kCVPixelFormatType_32ARGB, pixelBufferAttributes as CFDictionary, &pixelBuffer) == kCVReturnSuccess)
    //
    //     precondition(CVPixelBufferLockBaseAddress(pixelBuffer!, []) == kCVReturnSuccess)
    //     defer { precondition(CVPixelBufferUnlockBaseAddress(pixelBuffer!, []) == kCVReturnSuccess) }
    //
    //     let ciImage = CIImage(cgImage: frameImage)
    //     context.render(ciImage, to: pixelBuffer!)
    //
    //     // ✅ This passes.
    //     let bytes = UnsafeBufferPointer(start: CVPixelBufferGetBaseAddress(pixelBuffer!)!.assumingMemoryBound(to: UInt8.self), count: CVPixelBufferGetDataSize(pixelBuffer!))
    //     precondition(bytes.contains(where: { $0 != 0 }))
    //
    //     while !input.isReadyForMoreMediaData { Thread.sleep(forTimeInterval: 10 / 1000) }
    //     precondition(pixelBufferAdaptor.append(pixelBuffer!, withPresentationTime: CMTime(seconds: Double(frameNumber) * frameRate, preferredTimescale: 600)))
    // }
    
    
    // Unpreferred scenario: using CoreGraphics to fill the buffer from the pixel buffer adapter. Shows that
    // buffer from pixel buffer adapter can be filled and working.
    
    // for frameNumber in 0 ..< frameCount {
    //     var pixelBuffer: CVPixelBuffer?
    //     guard let pixelBufferPool: CVPixelBufferPool = pixelBufferAdaptor.pixelBufferPool else { preconditionFailure() }
    //     precondition(CVPixelBufferPoolCreatePixelBuffer(nil, pixelBufferPool, &pixelBuffer) == kCVReturnSuccess)
    //
    //     precondition(CVPixelBufferLockBaseAddress(pixelBuffer!, []) == kCVReturnSuccess)
    //     defer { precondition(CVPixelBufferUnlockBaseAddress(pixelBuffer!, []) == kCVReturnSuccess) }
    //
    //     guard let context: CGContext = CGContext(data: CVPixelBufferGetBaseAddress(pixelBuffer!), width: frameImage.width, height: frameImage.height, bitsPerComponent: 8, bytesPerRow: CVPixelBufferGetBytesPerRow(pixelBuffer!), space: colorSpace, bitmapInfo: CGImageAlphaInfo.premultipliedFirst.rawValue) else { preconditionFailure() }
    //     context.clear(CGRect(origin: .zero, size: frameSize))
    //     context.draw(frameImage, in: CGRect(origin: .zero, size: frameSize))
    //
    //     // ✅ This passes.
    //     let bytes = UnsafeBufferPointer(start: CVPixelBufferGetBaseAddress(pixelBuffer!)!.assumingMemoryBound(to: UInt8.self), count: CVPixelBufferGetDataSize(pixelBuffer!))
    //     precondition(bytes.contains(where: { $0 != 0 }))
    //
    //     while !input.isReadyForMoreMediaData { Thread.sleep(forTimeInterval: 10 / 1000) }
    //     precondition(pixelBufferAdaptor.append(pixelBuffer!, withPresentationTime: CMTime(seconds: Double(frameNumber) * frameRate, preferredTimescale: 600)))
    // }
    
    let semaphore = DispatchSemaphore(value: 0)
    
    input.markAsFinished()
    writer.endSession(atSourceTime: CMTime(seconds: Double(frameCount) * frameRate, preferredTimescale: 600))
    writer.finishWriting(completionHandler: { semaphore.signal() })
    
    semaphore.wait()
    
    Swift.print("Successfully finished rendering to \(url.path)")
    

    CGContext ,但我 CIContext 为了利用GPU . 问题似乎出在 AVAssetWriterInputPixelBufferAdaptor 的缓冲池。致使 上下文 放入单独创建的缓冲区中并将它们附加到适配器中可以工作,但效率很低。致使 上下文 适配器池提供的缓冲区中没有数据写入缓冲区 完全 ,它实际上包含了所有的零,好像两个是不兼容的!但是,使用 CGImage公司 工作,以便手动复制数据。

    主要的观察是 CIContext.render 显示为异步工作,或者缓冲区被填充和数据写入视频流之间出现问题。换句话说,当缓冲区被刷新时,缓冲区中没有数据。以下是指向这个方向的:

    1. 移除缓冲区锁定会导致几乎所有帧都被写入,除了前几帧之外,上面的代码实际上会生成一个 correct output
    2. 使用不同的编解码器,比如ProRes422,几乎所有的帧都被正确地写入,上面的代码也只产生了一些空白 correct output ,但较大和复杂的图像会导致跳过帧。

    P、 大多数iOS的例子都使用了几乎相同的实现,而且似乎工作得非常好。我发现了一个 hint macOS可能会有所不同,但看不到任何官方文档。

    0 回复  |  直到 5 年前
        1
  •  4
  •   Frank Rupprecht    5 年前

    对于您的用例,最好使用 pull-style APIs AVAssetWriterInput 因为您不需要实时处理任何媒体(就像从相机拍摄时一样)。

    因此,与其在输入没有准备好时暂停线程,不如等待它拉下一帧。记住也要设置 expectsMediaDataInRealTime false 在这种情况下。

    我认为你目前的方法的主要问题是,当作者还没有准备好的时候,你暂停了视频处理正在进行的线程。

    (顺便说一句:你可以创造 CIImage CIImage(color:) CGImage

        2
  •  2
  •   Ian Bytchek    4 年前

    在与苹果开发人员技术支持人员交谈后发现:

    核心图像延迟渲染直到客户端请求访问帧缓冲区,即。 CVPixelBufferLockBaseAddress .

    所以,解决办法很简单 打电话后 CIContext.render

    for frameNumber in 0 ..< frameCount {
        var pixelBuffer: CVPixelBuffer?
        guard let pixelBufferPool: CVPixelBufferPool = pixelBufferAdaptor.pixelBufferPool else { preconditionFailure() }
        precondition(CVPixelBufferPoolCreatePixelBuffer(nil, pixelBufferPool, &pixelBuffer) == kCVReturnSuccess)
    
        let ciImage = CIImage(cgImage: frameImage)
        context.render(ciImage, to: pixelBuffer!)
    
        precondition(CVPixelBufferLockBaseAddress(pixelBuffer!, []) == kCVReturnSuccess)
        defer { precondition(CVPixelBufferUnlockBaseAddress(pixelBuffer!, []) == kCVReturnSuccess) }
    
        let bytes = UnsafeBufferPointer(start: CVPixelBufferGetBaseAddress(pixelBuffer!)!.assumingMemoryBound(to: UInt8.self), count: CVPixelBufferGetDataSize(pixelBuffer!))
        precondition(bytes.contains(where: { $0 != 0 }))
    
        while !input.isReadyForMoreMediaData { Thread.sleep(forTimeInterval: 10 / 1000) }
        precondition(pixelBufferAdaptor.append(pixelBuffer!, withPresentationTime: CMTime(seconds: Double(frameNumber) * frameRate, preferredTimescale: 600)))
    }