Skip to content

Commit

Permalink
mac-virtualcam: Fix IOSurface memory leak
Browse files Browse the repository at this point in the history
This change fixes a memory leak in the mac-virtualcam plugin that causes
OBS to not release the CVPixelBuffers (and underlying IOSurfaces)
it emits to the virtual camera consumers.

Pull request #6573 (Avoid
transcoding where possible) updated the mac-virtualcam to share the
virtual camera feed with other processes via IOSurfaces.

Although the changes work correctly, users have observed that OBS memory
usage keeps increasing when the virtual camera is active until OBS runs
out of memory or the consuming application is closed.
See the report by @SciTechNick for more information:
#6573 (comment)

After some debugging, I have found that the plugin is leaking Mach ports
associated with IOSurfaces, preventing them from being re-used. The
previous approach using `NSMachPort` does not seem to properly release
the Mach port allocated via `CVPixelBufferGetIOSurface` and
`IOSurfaceLookupFromMachPort`. Instead, we must explicitly deallocate
the port using `mach_port_deallocate`.

I have tested the changes on a Macbook Pro (M1) running macOS Monterey with
Google Chrome, Zoom, and Cameo. OBS shows no signs of memory leakage
after multiple minutes.
  • Loading branch information
fabianishere committed Jun 22, 2022
1 parent 889ca0f commit 1083df5
Show file tree
Hide file tree
Showing 3 changed files with 38 additions and 8 deletions.
21 changes: 19 additions & 2 deletions plugins/mac-virtualcam/src/dal-plugin/OBSDALMachClient.mm
Original file line number Diff line number Diff line change
Expand Up @@ -101,14 +101,32 @@ - (void)handlePortMessage:(NSPortMessage *)message
break;
case MachMsgIdFrame:
VLog(@"Received frame message");
if (components.count >= 4) {

if (components.count < 4) {
break;
}

@autoreleasepool {
NSMachPort *framePort = (NSMachPort *)components[0];

if (!framePort) {
return;
}

IOSurfaceRef surface = IOSurfaceLookupFromMachPort(
[framePort machPort]);
mach_port_deallocate(mach_task_self(),
[framePort machPort]);

if (!surface) {
ELog(@"Failed to obtain IOSurface from Mach port");
return;
}

CVPixelBufferRef frame;
CVPixelBufferCreateWithIOSurface(kCFAllocatorDefault,
surface, NULL, &frame);
CFRelease(surface);

uint64_t timestamp;
[components[1] getBytes:&timestamp
Expand All @@ -131,7 +149,6 @@ - (void)handlePortMessage:(NSPortMessage *)message
fpsDenominator:fpsDenominator];

CVPixelBufferRelease(frame);
CFRelease(surface);
}
break;
case MachMsgIdStop:
Expand Down
3 changes: 3 additions & 0 deletions plugins/mac-virtualcam/src/dal-plugin/OBSDALStream.mm
Original file line number Diff line number Diff line change
Expand Up @@ -397,7 +397,10 @@ - (void)queuePixelBuffer:(CVPixelBufferRef)frame
}

err = CMSimpleQueueEnqueue(self.queue, sampleBuffer);

if (err != noErr) {
CFRelease(sampleBuffer);

DLog(@"CMSimpleQueueEnqueue err %d", err);
return;
}
Expand Down
22 changes: 16 additions & 6 deletions plugins/mac-virtualcam/src/obs-plugin/OBSDALMachServer.mm
Original file line number Diff line number Diff line change
Expand Up @@ -147,23 +147,33 @@ - (void)sendPixelBuffer:(CVPixelBufferRef)frame
dataWithBytes:&fpsDenominator
length:sizeof(fpsDenominator)];

NSPort *framePort = [NSMachPort
portWithMachPort:IOSurfaceCreateMachPort(
CVPixelBufferGetIOSurface(
frame))];
IOSurfaceRef surface = CVPixelBufferGetIOSurface(frame);

if (!surface) {
blog(LOG_ERROR,
"unable to access IOSurface associated with CVPixelBuffer");
return;
}

mach_port_t framePort = IOSurfaceCreateMachPort(surface);

if (!framePort) {
blog(LOG_ERROR,
"unable to allocate mach port for pixel buffer");
"unable to allocate mach port for IOSurface");
return;
}

[self sendMessageToClientsWithMsgId:MachMsgIdFrame
components:@[
framePort, timestampData,
[NSMachPort
portWithMachPort:framePort
options:NSMachPortDeallocateNone],
timestampData,
fpsNumeratorData,
fpsDenominatorData
]];

mach_port_deallocate(mach_task_self(), framePort);
}
}

Expand Down

0 comments on commit 1083df5

Please sign in to comment.