Я хочу использовать Core Image для обработки группы CGImage
объектов и превращения их в фильм QuickTime на macOS .Следующий код демонстрирует, что нужно, но вывод содержит много пустых (черных) фреймов :
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
, чтобы использовать графический процессор .Кажется, проблема в пиксельных буферах, предоставляемых буферным пулом AVAssetWriterInputPixelBufferAdaptor
.Рендеринг CIContext
в индивидуально созданные буферы и добавление их в адаптер работает, но крайне неэффективно.Рендеринг CIContext
в буферы, предоставляемые пулом адаптера, не приводит к записи данных в буфер вообще , он буквально содержит все нули, как если бы два несовместимы!Однако рендеринг с использованием CGImage
работает так же, как и копирование данных вручную.
Основное наблюдение заключается в том, что CIContext.render
работает асинхронно или что-то не так между заполнением буфера и записью данных в видео.поток.Другими словами, в буфере нет данных, когда он очищается.Следующее является своего рода указанием в этом направлении:
- Снятие блокировки буфера приводит к тому, что записываются почти все кадры, кроме первых нескольких, приведенный выше код фактически выдает правильный вывод , но с фактическими данными поведение такое же, как описано.
- Использование другого кодека, такого как ProRes422, приводит к тому, что почти все кадры записываются правильно, всего с несколькими пробелами - также приведенный выше код выдает правильновыведите , но более крупные и сложные изображения приводят к пропущенным кадрам.
Что не так с этим кодом и как правильно это сделать?
PS В большинстве примеров iOS используется довольнопочти такая же реализация и, кажется, работает отлично.Я обнаружил подсказку , что она может отличаться для macOS, но не вижу официальной документации по этому вопросу.