0

I'm trying to port some ObjC-Code to Swift. The code is part of the Flutter Camera-Plugin (line 265). The plugin allows a live preview of the camera.

As a Swift programmer I have no idea what is happening here. What does OSAtomicCompareAndSwapPtrBarrier() do and how can this code be transferred to Swift?

- (CVPixelBufferRef)copyPixelBuffer {
  CVPixelBufferRef pixelBuffer = _latestPixelBuffer;
  while (!OSAtomicCompareAndSwapPtrBarrier(pixelBuffer, nil, (void **)&_latestPixelBuffer)) {
    pixelBuffer = _latestPixelBuffer;
  }
  return pixelBuffer;
}
hendra
  • 2,531
  • 5
  • 22
  • 34

2 Answers2

3

OSAtomic is used when there are multiple threads and when you need to ensure that between the time you assign and check a variable, things haven't changed. Let me give you a detailed explanation of why it was required here but in summary you don't need to worry about this in Swift as the Swift Runtime will take care of this particular scenario.

Detailed Explanation: There are two active threads (DispatchQueues), one that captures CVPixelBuffer captureOutput: and the other one that calls copyPixelBuffer: to pass on the CVPixelBuffer to flutter.

When captureOutput: is called, the current frame is extracted using CMSampleBufferGetImageBuffer, which requires the caller to call CFRetain to keep the buffer in memory and when the buffer is no longer required, CFRelease is to be called to release the memory. Luckily, Flutter automatically calls CFRelease on the passed on pixelBuffer, so we just have to call CFRetain and don't have to worry about releasing the buffer as flutter will do that for us. However, if a new frame is acquired before the old one is handed over to Flutter, we would have to call CFRelease on the old one, as it would never be given to flutter.

So how do we know if the buffer has been handed over to Flutter? Well, whenever the buffer is handed over to flutter, we can set _latestPixelBuffer = nil; and whenever a new buffer is produced we can check if _latestPixelBuffer != nil we can call CFRelease on it. So without OSAtomic, the function would (wrongly) look like:

- (void)captureOutput:(AVCaptureOutput *)output
    didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
           fromConnection:(AVCaptureConnection *)connection {

    CVPixelBufferRef newBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
    CFRetain(newBuffer);

    // WRONG IMPLEMENTATION FOLLOWS

    CVPixelBufferRef old = _latestPixelBuffer;
    // Reference Point 1
    _latestPixelBuffer = newBuffer;

    if (old != nil) {
      CFRelease(old);
    }

- (CVPixelBufferRef)copyPixelBuffer {
  CVPixelBufferRef pixelBuffer = _latestPixelBuffer;

  // WRONG IMPLEMENTATION
  _latestPixelBuffer = nil;

  return pixelBuffer;
}

Since, it is multithreaded (in fact flutter uses GPU thread), there can be a scenario that by the time we reach // Reference Point 1, where we have already copied the old frame's reference in old and are about to save the new frame in _latestPixelBuffer but before we can execute the line, flutter's thread calls copyPixelBuffer. So we hand over the old frame to Flutter and now it would be flutter's responsibility to call CFRelease on it. However, our thread, would still see _old still referring to the old frame (as that line was executed before flutter had called) and hence we would accidentally call CFRelease on it.

So what we need is a guarantee that inside captureOutput: when we assign _latestPixelBuffer = newBuffer, there is no way flutter's thread can get the old buffer OR if it does, then we want our old to refer to nil as well so that we don't call CFRelease on it. This is where OSAtomicCompareAndSwapPtrBarrier comes in handy.

- (void)captureOutput:(AVCaptureOutput *)output
    didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
           fromConnection:(AVCaptureConnection *)connection {

    CVPixelBufferRef newBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
    CFRetain(newBuffer);
    CVPixelBufferRef old = _latestPixelBuffer;
    while (!OSAtomicCompareAndSwapPtrBarrier(old, newBuffer, (void **)&_latestPixelBuffer)) {
      old = _latestPixelBuffer;
    }
    if (old != nil) {
      CFRelease(old);
    }

It will check if _latestPixelBuffer == old, only then it will assign _latestPixelBuffer = newBuffer. If during this time flutter's thread has been called, _latestPixelBuffer would have been set to nil inside copyPixelBuffer: and OSAtomic will fail since _latestPixelBuffer != old anymore. This is where the body of the while loop comes into play and here we set old = _latestPixelBuffer; again but this time old will get the new value set by flutter's thread. Now, when OSAtomic will be called, it will set _latestPixelBuffer = newBuffer.

For this to work, all threads that change the value of _latestPixelBuffer need to use OSAtomicCompareAndSwapPtrBarrier. That is the reason copyPixelBuffer: also uses this function:

- (CVPixelBufferRef)copyPixelBuffer {
  CVPixelBufferRef pixelBuffer = _latestPixelBuffer;
  while (!OSAtomicCompareAndSwapPtrBarrier(pixelBuffer, nil, (void **)&_latestPixelBuffer)) {
    pixelBuffer = _latestPixelBuffer;
  }

  return pixelBuffer;
}

In Swift, we don't call CFRetain on the return value of CMSampleBufferGetImageBuffer as the Swift Runtime does that for us. Since, the runtime calls CFRetain for us, it is the runtime's responsibility to call CFRelease on it as well. However, in copyPixelBuffer, we will need to return Unmanaged<CVPixelBuffer>.passRetained(latestPixelBuffer!) so that Flutter can call CFRelease on it.

dr ganjoo
  • 624
  • 7
  • 5
  • That was an interesting read, but I'm still unclear on how to move forward implementing this method in Swift. Any additional help would be appreciated. – Luke Pighetti Aug 26 '19 at 00:04
  • 1
    Very good explanation of how things work in camera plugin; cheers! I have a doubt and I think your opinion could be golden, could you please check https://stackoverflow.com/questions/62795428/whats-the-best-way-to-launch-a-periodic-task-in-ios-camera-to-use-frames-data ? Thanks in advance! – fff Jul 08 '20 at 13:02
  • Your explanation was great. But as @LukePighetti mentions, it would be helpful if you could add a working definition for the copyPixelBuffer property in Swift. – Jon Scalet Jul 17 '21 at 11:32
  • 1
    +1 for the good explanation... For me not fully understanding is as a result of not fully grounded in swift or objective C but i got the idea and that fixed my problem – Paul Okeke Aug 03 '21 at 12:54
1

in captureOutput delegate method

let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer)!

and then

public func copyPixelBuffer() -> Unmanaged<CVPixelBuffer>? {
    if(pixelBuffer == nil){
        return nil
    }
    return  Unmanaged<CVPixelBuffer>.passRetained(pixelBuffer)
}
sector
  • 11
  • 1
  • 2