Как обновить CALayer с помощью CVPixelBuffer/IOSurface?

У меня есть CVPixelBuffer с поддержкой IOSurface, который обновляется из внешнего источника со скоростью 30 кадров в секунду. Я хочу отобразить предварительный просмотр данных изображения в NSView — как мне лучше всего это сделать?

Я могу напрямую установить .contents CALayer в представлении, но это обновляется только при первом обновлении моего представления (или, скажем, если я изменяю размер представления). Я внимательно изучил документы, но не могу понять, как правильно вызывать needDisplay на уровне или представлении, чтобы инфраструктура представления знала, что нужно обновляться, особенно когда обновления поступают извне представления.

В идеале я бы просто привязал 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))
    }

}

Что мне не хватает?


person andrew    schedule 16.11.2020    source источник


Ответы (3)


Возможно, я ошибаюсь, но я думаю, что вы обновляете свой 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
person LombaX    schedule 04.12.2020
comment
Это хорошая мысль. Я пробовал запускать все это из основного потока - без разницы. Другой момент заключается в том, что вы можете напрямую назначить IOSurface .contents CALayer (что работает), просто обновление не происходит. - person andrew; 06.12.2020
comment
вы можете попробовать добавить CATransaction.flush() после .contents = ... ? Всегда на главной ветке. Я просматриваю некоторые из своих старых проектов (цель C) и нашел... может быть, это поможет - person LombaX; 07.12.2020
comment
@andrew Я провел несколько тестов и добавил в ответ больше деталей. Я думаю, что вы не можете напрямую назначить IOSurface содержимому CALayer, в моем случае это не работает даже в первый раз, но я всегда должен проходить через CGImage. Посмотрите на другой раздел catch в моем ответе с примером кода. - person LombaX; 07.12.2020
comment
Вы пытались определить PixelBuffer с помощью IOSurface, совместимого с CoreAnimation? Это то, что сработало для меня. Попробуйте var bufferAttributes = [kCVPixelBufferIOSurfaceCoreAnimationCompatibilityKey:true] as CFDictionary; let err = CVPixelBufferCreate(kCFAllocatorDefault, width, height, kCVPixelFormatType_32BGRA, bufferAttributes, &pb) См. russbishop.net/cross-process-rendering. - person andrew; 07.12.2020

Если у вас есть некоторые IBActions, которые обновляют его, создайте наблюдаемую переменную с блоком didSet и всякий раз, когда запускается IBAction, изменяйте ее значение. Также не забудьте написать код, который вы хотите запускать при обновлении в этом блоке.

Я бы предложил сделать переменную Int, установить для нее значение по умолчанию 0 и добавлять к ней 1 каждый раз, когда она обновляется.

И вы можете преобразовать NSView в NSImageView для той части, где вы просите показать данные IMAGE на NSView, чтобы это сработало.

person nerdintown    schedule 30.11.2020

Вам нужно преобразовать буфер пикселей в 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);
}
person Community    schedule 05.12.2020
comment
Это похоже на решение, которое я использую, используя VTCreateCGImageFromPixelBuffer, но оно очень сильно загружает процессор. Вы можете напрямую назначить IOSurface на CALayer.contents, и он отобразится - это просто маркировка грязных вещей, с которыми мне трудно. - person andrew; 06.12.2020