如何使用CVPixelBuffer/IOSurface更新CALayer?

9
我有一个由外部源以每秒30帧更新的基于IOSurface的CVPixelBuffer。我想在NSView中呈现图像数据的预览 - 对我来说最好的方法是什么?
我可以直接设置视图上CALayer的.contents,但这只会在视图第一次更新时(或者例如我调整视图大小时)更新。我一直在查阅文档,但我无法找出正确的needsDisplay调用方式,以便让视图基础结构知道何时刷新自身,尤其是当更新来自视图外部时。
理想情况下,我只需将IOSurface绑定到我的图层,所做的任何更改都会传播,但我不确定是否可能。
class VideoPreviewController: NSViewController, VideoFeedConsumer {
    let customLayer : CALayer = CALayer()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do view setup here.
        print("Loaded our video preview")
        
        view.layer?.addSublayer(customLayer)
        customLayer.frame = view.frame
        
        // register our view with the browser service
        VideoFeedBrowser.instance.registerConsumer(self)
    }
    
    override func viewWillDisappear() {
        // deregister our view from the video feed
        VideoFeedBrowser.instance.deregisterConsumer(self)

        super.viewWillDisappear()
    }
    
    // This callback gets called at 30fps whenever the pixelbuffer is updated
    @objc func updateFrame(pixelBuffer: CVPixelBuffer) {

        guard let surface = CVPixelBufferGetIOSurface(pixelBuffer)?.takeUnretainedValue() else {
            print("pixelbuffer isn't IOsurface backed! noooooo!")
            return;
        }

        // Try and tell the view to redraw itself with new contents?
        // These methods don't work
        //self.view.setNeedsDisplay(self.view.visibleRect)
        //self.customLayer.setNeedsDisplay()
        self.customLayer.contents = surface

    }
    
}

这是我尝试基于 NSView 进行缩放的版本,而不是基于 NSViewController 的版本,但它也不能正确更新(或缩放)。
class VideoPreviewThumbnail: NSView, VideoFeedConsumer {
   

    required init?(coder decoder: NSCoder) {
        super.init(coder: decoder)
        self.wantsLayer = true
        
        // register our view with the browser service
        VideoFeedBrowser.instance.registerConsumer(self)
    }
    
    override init(frame frameRect: NSRect) {
        super.init(frame: frameRect)
        self.wantsLayer = true
        
        // register our view with the browser service
        VideoFeedBrowser.instance.registerConsumer(self)
    }
    
    deinit{
        VideoFeedBrowser.instance.deregisterConsumer(self)
    }
    
    override func updateLayer() {
        // Do I need to put something here?
        print("update layer")
    }
    
    @objc
    func updateFrame(pixelBuffer: CVPixelBuffer) {
        guard let surface = CVPixelBufferGetIOSurface(pixelBuffer)?.takeUnretainedValue() else {
            print("pixelbuffer isn't IOsurface backed! noooooo!")
            return;
        }
        self.layer?.contents = surface
        self.layer?.transform = CATransform3DMakeScale(
            self.frame.width / CGFloat(CVPixelBufferGetWidth(pixelBuffer)),
            self.frame.height / CGFloat(CVPixelBufferGetHeight(pixelBuffer)),
            CGFloat(1))
    }

}

我缺少什么?

4个回答

1
我自己遇到了这个问题,解决方法是使用双缓冲的IOSurface源:使用两个IOSurface对象而不是一个,并渲染到当前表面,将表面设置为图层内容,然后在下一次渲染通行证中使用备用(后/前)表面,然后交换。
似乎将CALayer.contents两次设置为相同的CVPixelBufferRef没有效果。 然而,如果您在两个IOSurfaceRef之间交替,则可以奇妙地工作。
也可能通过将其设置为nil然后重置来使图层内容无效。 我没有尝试过那种情况,但正在使用双缓冲技术。

1

也许我错了,但我认为你正在后台线程上更新NSView。(我假设回调updateFrame在后台线程上)

如果我是对的,当你想要更新NSView时,将你的pixelBuffer转换为任何你想要的东西(NSImage?),然后将其分派到主线程。

伪代码(我不经常使用CVPixelBuffer,所以我不确定这是否是将其转换为NSImage的正确方法)

let ciImage = CIImage(cvImageBuffer: pixelBuffer)
let context = CIContext(options: nil)

let width = CVPixelBufferGetWidth(pixelBuffer)
let height = CVPixelBufferGetHeight(pixelBuffer)

let cgImage = context.createCGImage(ciImage, from: CGRect(x: 0, y: 0, width: width, height: height))

let nsImage = NSImage(cgImage: cgImage, size: CGSize(width: width, height: height))

DispatchQueue.main.async {
    // assign the NSImage to your NSView here
}

另一个问题是:我做了一些测试,似乎无法将IOSurface直接分配给CALayer的内容。
我尝试了这个:
    let textureImageWidth = 1024
    let textureImageHeight = 1024

    let macPixelFormatString = "ARGB"
    var macPixelFormat: UInt32 = 0
    for c in macPixelFormatString.utf8.reversed() {
       macPixelFormat *= 256
       macPixelFormat += UInt32(c)
    }

    let ioSurface = IOSurfaceCreate([kIOSurfaceWidth: textureImageWidth,
                    kIOSurfaceHeight: textureImageHeight,
                    kIOSurfaceBytesPerElement: 4,
                    kIOSurfaceBytesPerRow: textureImageWidth * 4,
                    kIOSurfaceAllocSize: textureImageWidth * textureImageHeight * 4,
                    kIOSurfacePixelFormat: macPixelFormat] as CFDictionary)!

    IOSurfaceLock(ioSurface, IOSurfaceLockOptions.readOnly, nil)
    let test = CIImage(ioSurface: ioSurface)
    IOSurfaceUnlock(ioSurface, IOSurfaceLockOptions.readOnly, nil)
    
    v1?.layer?.contents = ioSurface

这里的v1是我的视图,但没有效果。

即使使用CIImage也没有效果(只有最后几行)。

    IOSurfaceLock(ioSurface, IOSurfaceLockOptions.readOnly, nil)
    let test = CIImage(ioSurface: ioSurface)
    IOSurfaceUnlock(ioSurface, IOSurfaceLockOptions.readOnly, nil)
    
    v1?.layer?.contents = test

如果我创建一个CGImage,它就可以工作。
    IOSurfaceLock(ioSurface, IOSurfaceLockOptions.readOnly, nil)
    let test = CIImage(ioSurface: ioSurface)
    IOSurfaceUnlock(ioSurface, IOSurfaceLockOptions.readOnly, nil)
    
    let context = CIContext.init()
    let img = context.createCGImage(test, from: test.extent)
    v1?.layer?.contents = img

这是一个很好的想法。我已经尝试从主线程运行所有内容 - 没有区别。另一点是,您可以直接将IOSurface分配给CALayer的.contents(这有效),只是更新没有发生。 - andrew
1
你可以尝试在 .contents = ... 后面添加 CATransaction.flush() 吗?请确保始终在主线程上执行。我在查看我的一些旧项目(Objective-C)时发现了这个方法...也许它有所帮助。 - LombaX
@andrew 我进行了一些测试并在答案中添加了更多细节。我认为你不能直接将IOSurface分配给CALayer内容,在我的情况下,即使第一次也不起作用,但我始终需要通过CGImage传递。请查看我的答案中的“另一个陷阱”部分,其中包含示例代码。 - LombaX
你尝试过使用与CoreAnimation兼容的IOSurface定义PixelBuffer吗?这对我很有效。尝试以下代码:var bufferAttributes = [kCVPixelBufferIOSurfaceCoreAnimationCompatibilityKey:true] as CFDictionary; let err = CVPixelBufferCreate(kCFAllocatorDefault, width, height, kCVPixelFormatType_32BGRA, bufferAttributes, &pb)参考链接:http://www.russbishop.net/cross-process-rendering - andrew

0

如果您有一些更新它的 IBActions,那么创建一个带有 didSet 块的观察变量,并在每次触发 IBAction 时更改其值。还要记得在该块中编写要运行的代码。

我建议将变量设置为 Int,将其默认值设置为 0,并在每次更新时添加 1

对于您询问如何在 NSView 上显示图像数据的部分,您可以将其转换为 NSImageView,以完成此任务。


0

你需要将像素缓冲区转换为CGImage并将其转换为层,以便您可以更改主视图的层。 请尝试此代码

@objc
func updateFrame(pixelBuffer: CVPixelBuffer) {
    guard let surface = CVPixelBufferGetIOSurface(pixelBuffer)?.takeUnretainedValue() else {
        print("pixelbuffer isn't IOsurface backed! noooooo!")
        return;
    }
    void *baseAddr = CVPixelBufferGetBaseAddress(pixelBuffer);
    size_t width = CVPixelBufferGetWidth(pixelBuffer);
    size_t height = CVPixelBufferGetHeight(pixelBuffer);
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    CGContextRef cgContext = CGBitmapContextCreate(baseAddr, width, height, 8, CVPixelBufferGetBytesPerRow(pixelBuffer), colorSpace, kCGImageAlphaNoneSkipLast);
    CGImageRef cgImage = CGBitmapContextCreateImage(cgContext);
    CGContextRelease(cgContext);
    
    let outputImage = UIImage(cgImage: outputCGImage, scale: 1, orientation: img.imageOrientation)
    let newLayer:CGLayer = CGLayer.init(cgImage: outputImage)
    self.layer = newLayer
    
    CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
    CVPixelBufferRelease(pixelBuffer);
}

1
这与我使用VTCreateCGImageFromPixelBuffer的解决方案类似,但它非常消耗CPU。您可以直接将IOSurface分配给CALayer.contents并进行渲染 - 我只是在标记脏东西方面遇到了困难。 - andrew

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接