我想用核心图像来处理
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
显示为异步工作,或者缓冲区被填充和数据写入视频流之间出现问题。换句话说,当缓冲区被刷新时,缓冲区中没有数据。以下是指向这个方向的:
-
移除缓冲区锁定会导致几乎所有帧都被写入,除了前几帧之外,上面的代码实际上会生成一个
correct output
-
使用不同的编解码器,比如ProRes422,几乎所有的帧都被正确地写入,上面的代码也只产生了一些空白
correct output
,但较大和复杂的图像会导致跳过帧。
P、 大多数iOS的例子都使用了几乎相同的实现,而且似乎工作得非常好。我发现了一个
hint
macOS可能会有所不同,但看不到任何官方文档。