Ian*_*hek 5 core-graphics core-image avfoundation metal
我想使用 Core Image 处理一堆对象并将它们转换为macOSCGImage上的 QuickTime 影片。以下代码演示了所需内容,但输出包含大量空白(黑色)帧:
import AppKit\nimport AVFoundation\nimport CoreGraphics\nimport Foundation\nimport CoreVideo\nimport Metal\n\n// Video output url.\nlet url: URL = try! FileManager.default.url(for: .downloadsDirectory, in: .userDomainMask, appropriateFor: nil, create: false).appendingPathComponent("av.mov")\ntry? FileManager.default.removeItem(at: url)\n\n// Video frame size, total frame count, frame rate and frame image.\nlet frameSize: CGSize = CGSize(width: 2000, height: 1000)\nlet frameCount: Int = 100\nlet frameRate: Double = 1 / 30\nlet frameImage: CGImage\n\nframeImage = NSImage(size: frameSize, flipped: false, drawingHandler: {\n NSColor.red.setFill()\n $0.fill()\n return true\n}).cgImage(forProposedRect: nil, context: nil, hints: nil)!\n\nlet pixelBufferAttributes: [CFString: Any]\nlet outputSettings: [String: Any]\n\npixelBufferAttributes = [\n kCVPixelBufferPixelFormatTypeKey: Int(kCVPixelFormatType_32ARGB),\n kCVPixelBufferWidthKey: Float(frameSize.width),\n kCVPixelBufferHeightKey: Float(frameSize.height),\n kCVPixelBufferMetalCompatibilityKey: true,\n kCVPixelBufferCGImageCompatibilityKey: true,\n kCVPixelBufferCGBitmapContextCompatibilityKey: true,\n]\n\noutputSettings = [\n AVVideoCodecKey: AVVideoCodecType.h264,\n AVVideoWidthKey: Int(frameSize.width),\n AVVideoHeightKey: Int(frameSize.height),\n]\n\nlet writer: AVAssetWriter = try! AVAssetWriter(outputURL: url, fileType: .mov)\nlet input: AVAssetWriterInput = AVAssetWriterInput(mediaType: .video, outputSettings: outputSettings)\nlet pixelBufferAdaptor: AVAssetWriterInputPixelBufferAdaptor = AVAssetWriterInputPixelBufferAdaptor(assetWriterInput: input, sourcePixelBufferAttributes: pixelBufferAttributes as [String: Any])\n\ninput.expectsMediaDataInRealTime = true\n\nprecondition(writer.canAdd(input))\nwriter.add(input)\n\nprecondition(writer.startWriting())\nwriter.startSession(atSourceTime: CMTime.zero)\n\nlet colorSpace: CGColorSpace = CGColorSpace(name: CGColorSpace.sRGB) ?? CGColorSpaceCreateDeviceRGB()\nlet context = CIContext(mtlDevice: MTLCreateSystemDefaultDevice()!)\n\nSwift.print("Starting the render\xe2\x80\xa6")\n\n// Preferred scenario: using CoreImage to fill the buffer from the pixel buffer adapter. Shows that\n// CIImage + AVAssetWriterInputPixelBufferAdaptor are not working together.\n\nfor frameNumber in 0 ..< frameCount {\n var pixelBuffer: CVPixelBuffer?\n guard let pixelBufferPool: CVPixelBufferPool = pixelBufferAdaptor.pixelBufferPool else { preconditionFailure() }\n precondition(CVPixelBufferPoolCreatePixelBuffer(nil, pixelBufferPool, &pixelBuffer) == kCVReturnSuccess)\n\n precondition(CVPixelBufferLockBaseAddress(pixelBuffer!, []) == kCVReturnSuccess)\n defer { precondition(CVPixelBufferUnlockBaseAddress(pixelBuffer!, []) == kCVReturnSuccess) }\n\n let ciImage = CIImage(cgImage: frameImage)\n context.render(ciImage, to: pixelBuffer!)\n\n // This fails \xe2\x80\x93 the pixel buffer doesn\'t get filled. AT ALL! Why? How to make it work?\n let bytes = UnsafeBufferPointer(start: CVPixelBufferGetBaseAddress(pixelBuffer!)!.assumingMemoryBound(to: UInt8.self), count: CVPixelBufferGetDataSize(pixelBuffer!))\n precondition(bytes.contains(where: { $0 != 0 }))\n\n while !input.isReadyForMoreMediaData { Thread.sleep(forTimeInterval: 10 / 1000) }\n precondition(pixelBufferAdaptor.append(pixelBuffer!, withPresentationTime: CMTime(seconds: Double(frameNumber) * frameRate, preferredTimescale: 600)))\n}\n\n\n// Unpreferred scenario: using CoreImage to fill the manually created buffer. Proves that CIImage \n// can fill buffer and working.\n\n// for frameNumber in 0 ..< frameCount {\n// var pixelBuffer: CVPixelBuffer?\n// precondition(CVPixelBufferCreate(nil, frameImage.width, frameImage.height, kCVPixelFormatType_32ARGB, pixelBufferAttributes as CFDictionary, &pixelBuffer) == kCVReturnSuccess)\n//\n// precondition(CVPixelBufferLockBaseAddress(pixelBuffer!, []) == kCVReturnSuccess)\n// defer { precondition(CVPixelBufferUnlockBaseAddress(pixelBuffer!, []) == kCVReturnSuccess) }\n//\n// let ciImage = CIImage(cgImage: frameImage)\n// context.render(ciImage, to: pixelBuffer!)\n//\n// // \xe2\x9c\x85 This passes.\n// let bytes = UnsafeBufferPointer(start: CVPixelBufferGetBaseAddress(pixelBuffer!)!.assumingMemoryBound(to: UInt8.self), count: CVPixelBufferGetDataSize(pixelBuffer!))\n// precondition(bytes.contains(where: { $0 != 0 }))\n//\n// while !input.isReadyForMoreMediaData { Thread.sleep(forTimeInterval: 10 / 1000) }\n// precondition(pixelBufferAdaptor.append(pixelBuffer!, withPresentationTime: CMTime(seconds: Double(frameNumber) * frameRate, preferredTimescale: 600)))\n// }\n\n\n// Unpreferred scenario: using CoreGraphics to fill the buffer from the pixel buffer adapter. Shows that\n// buffer from pixel buffer adapter can be filled and working.\n\n// for frameNumber in 0 ..< frameCount {\n// var pixelBuffer: CVPixelBuffer?\n// guard let pixelBufferPool: CVPixelBufferPool = pixelBufferAdaptor.pixelBufferPool else { preconditionFailure() }\n// precondition(CVPixelBufferPoolCreatePixelBuffer(nil, pixelBufferPool, &pixelBuffer) == kCVReturnSuccess)\n//\n// precondition(CVPixelBufferLockBaseAddress(pixelBuffer!, []) == kCVReturnSuccess)\n// defer { precondition(CVPixelBufferUnlockBaseAddress(pixelBuffer!, []) == kCVReturnSuccess) }\n//\n// 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() }\n// context.clear(CGRect(origin: .zero, size: frameSize))\n// context.draw(frameImage, in: CGRect(origin: .zero, size: frameSize))\n//\n// // \xe2\x9c\x85 This passes.\n// let bytes = UnsafeBufferPointer(start: CVPixelBufferGetBaseAddress(pixelBuffer!)!.assumingMemoryBound(to: UInt8.self), count: CVPixelBufferGetDataSize(pixelBuffer!))\n// precondition(bytes.contains(where: { $0 != 0 }))\n//\n// while !input.isReadyForMoreMediaData { Thread.sleep(forTimeInterval: 10 / 1000) }\n// precondition(pixelBufferAdaptor.append(pixelBuffer!, withPresentationTime: CMTime(seconds: Double(frameNumber) * frameRate, preferredTimescale: 600)))\n// }\n\nlet semaphore = DispatchSemaphore(value: 0)\n\ninput.markAsFinished()\nwriter.endSession(atSourceTime: CMTime(seconds: Double(frameCount) * frameRate, preferredTimescale: 600))\nwriter.finishWriting(completionHandler: { semaphore.signal() })\n\nsemaphore.wait()\n\nSwift.print("Successfully finished rendering to \\(url.path)")\nRun Code Online (Sandbox Code Playgroud)\n\n然而,以下内容适用于CGContext,但我需要 CIContext 才能使用 GPU。问题似乎出在AVAssetWriterInputPixelBufferAdaptor缓冲池提供的像素缓冲区上。渲染CIContext到单独创建的缓冲区并将其附加到适配器是可行的,但效率非常低。渲染到适配器池提供的缓冲区中会导致根本CIContext没有数据写入缓冲区,它实际上包含全零,就好像两个不兼容一样!然而,使用渲染是有效的,就像手动复制数据一样。CGImage
主要观察结果是,CIContext.render似乎异步工作,或者缓冲区填充和数据写入视频流之间出现问题。换句话说,当缓冲区被刷新时,缓冲区中没有数据。以下内容有点指向这个方向:
这段代码有什么问题,正确的方法是什么?
\n\nPS 大多数 iOS 示例都使用几乎相同的实现,并且看起来工作得很好。我发现一个提示,它可能在 macOS 上有所不同,但看不到任何关于此的官方文档。
\n与 Apple 开发人员技术支持交谈后发现:
Core Image 推迟渲染,直到客户端请求访问帧缓冲区,即
CVPixelBufferLockBaseAddress。
因此,解决方案很简单,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)))
}
Run Code Online (Sandbox Code Playgroud)
| 归档时间: |
|
| 查看次数: |
3091 次 |
| 最近记录: |