fix: resolve memory leaks and crash in macOS camera and screen capture
All checks were successful
Deploy on Master Change / deploy (push) Successful in 42s

This commit is contained in:
2026-07-01 13:11:59 +07:00
parent 44888520cb
commit d94c1733be
3 changed files with 96 additions and 28 deletions

View File

@@ -9,10 +9,12 @@
static AVCaptureSession *captureSession = nil;
static AVCaptureVideoDataOutput *videoOutput = nil;
static dispatch_queue_t captureQueue = nil;
static CIContext *sharedCIContext = nil;
// Latest frame stored as JPEG base64
// Latest frame stored as JPEG base64 (owning reference)
static NSString *latestFrameBase64 = nil;
static NSLock *frameLock = nil;
static int frameCount = 0;
// Delegate that receives sample buffers
@interface CameraFrameDelegate : NSObject <AVCaptureVideoDataOutputSampleBufferDelegate>
@@ -23,29 +25,57 @@ static NSLock *frameLock = nil;
- (void)captureOutput:(AVCaptureOutput *)output
didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
fromConnection:(AVCaptureConnection *)connection {
@autoreleasepool {
CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
if (!imageBuffer) {
NSLog(@"[CAMERA] didOutputSampleBuffer: imageBuffer is NULL");
return;
}
CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
if (!imageBuffer) return;
CIImage *ciImage = [CIImage imageWithCVPixelBuffer:imageBuffer];
if (!sharedCIContext) {
return;
}
CGImageRef cgImage = [sharedCIContext createCGImage:ciImage fromRect:ciImage.extent];
if (!cgImage) {
NSLog(@"[CAMERA] didOutputSampleBuffer: cgImage is NULL");
return;
}
CIImage *ciImage = [CIImage imageWithCVPixelBuffer:imageBuffer];
CIContext *context = [CIContext contextWithOptions:nil];
CGImageRef cgImage = [context createCGImage:ciImage fromRect:ciImage.extent];
if (!cgImage) return;
// Convert to JPEG NSData (quality 0.4)
NSBitmapImageRep *rep = [[NSBitmapImageRep alloc] initWithCGImage:cgImage];
CGImageRelease(cgImage);
if (!rep) {
return;
}
// Convert to JPEG NSData (quality 0.4)
NSBitmapImageRep *rep = [[NSBitmapImageRep alloc] initWithCGImage:cgImage];
CGImageRelease(cgImage);
NSDictionary *props = @{NSImageCompressionFactor: @(0.4)};
NSData *jpegData = [rep representationUsingType:NSBitmapImageFileTypeJPEG properties:props];
if (!jpegData) {
NSLog(@"[CAMERA] didOutputSampleBuffer: jpegData is NULL");
[rep release];
return;
}
NSDictionary *props = @{NSImageCompressionFactor: @(0.4)};
NSData *jpegData = [rep representationUsingType:NSBitmapImageFileTypeJPEG properties:props];
if (!jpegData) return;
NSString *b64 = [jpegData base64EncodedStringWithOptions:0];
// Create a retained string (ownership transfer)
NSString *dataUrl = [[NSString alloc] initWithFormat:@"data:image/jpeg;base64,%@", b64];
[rep release];
NSString *b64 = [jpegData base64EncodedStringWithOptions:0];
NSString *dataUrl = [NSString stringWithFormat:@"data:image/jpeg;base64,%@", b64];
[frameLock lock];
if (latestFrameBase64) {
[latestFrameBase64 release];
}
latestFrameBase64 = dataUrl; // retained copy
frameCount++;
int fc = frameCount;
[frameLock unlock];
[frameLock lock];
latestFrameBase64 = dataUrl;
[frameLock unlock];
// Log first few frames to confirm camera is working
if (fc <= 3) {
NSLog(@"[CAMERA] Frame #%d captured, size=%lu bytes", fc, (unsigned long)jpegData.length);
}
}
}
@end
@@ -56,39 +86,51 @@ static CameraFrameDelegate *frameDelegate = nil;
// StartNativeCamera: returns 0 on success, -1 on failure
int StartNativeCamera(void) {
NSLog(@"[CAMERA] StartNativeCamera called");
if (captureSession && captureSession.isRunning) {
NSLog(@"[CAMERA] Session already running");
return 0; // already running
}
if (!frameLock) {
frameLock = [[NSLock alloc] init];
}
frameCount = 0;
// Check camera permission
if (@available(macOS 10.14, *)) {
AVAuthorizationStatus status = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo];
NSLog(@"[CAMERA] Camera permission status: %ld (0=NotDetermined, 1=Restricted, 2=Denied, 3=Authorized)", (long)status);
if (status == AVAuthorizationStatusDenied || status == AVAuthorizationStatusRestricted) {
NSLog(@"[CAMERA] Camera permission denied (status=%ld)", (long)status);
return -1;
}
if (status == AVAuthorizationStatusNotDetermined) {
// Request permission synchronously via semaphore
NSLog(@"[CAMERA] Requesting camera permission...");
dispatch_semaphore_t sem = dispatch_semaphore_create(0);
__block BOOL granted = NO;
[AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL g) {
granted = g;
NSLog(@"[CAMERA] Permission request result: %@", g ? @"GRANTED" : @"DENIED");
dispatch_semaphore_signal(sem);
}];
dispatch_semaphore_wait(sem, dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC));
dispatch_semaphore_wait(sem, dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC));
if (!granted) {
NSLog(@"[CAMERA] Camera permission not granted");
NSLog(@"[CAMERA] Camera permission not granted after request");
return -1;
}
}
}
if (!sharedCIContext) {
sharedCIContext = [[CIContext contextWithOptions:nil] retain];
}
captureSession = [[AVCaptureSession alloc] init];
captureSession.sessionPreset = AVCaptureSessionPresetLow; // 320×240-ish
NSLog(@"[CAMERA] Session created with preset Low");
// Find default video device
AVCaptureDevice *camera = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
@@ -97,6 +139,7 @@ int StartNativeCamera(void) {
captureSession = nil;
return -1;
}
NSLog(@"[CAMERA] Found camera: %@", camera.localizedName);
NSError *error = nil;
AVCaptureDeviceInput *input = [AVCaptureDeviceInput deviceInputWithDevice:camera error:&error];
@@ -108,6 +151,7 @@ int StartNativeCamera(void) {
if ([captureSession canAddInput:input]) {
[captureSession addInput:input];
NSLog(@"[CAMERA] Input added to session");
} else {
NSLog(@"[CAMERA] Cannot add camera input to session");
captureSession = nil;
@@ -127,6 +171,7 @@ int StartNativeCamera(void) {
if ([captureSession canAddOutput:videoOutput]) {
[captureSession addOutput:videoOutput];
NSLog(@"[CAMERA] Output added to session");
} else {
NSLog(@"[CAMERA] Cannot add video output to session");
captureSession = nil;
@@ -134,21 +179,25 @@ int StartNativeCamera(void) {
}
[captureSession startRunning];
NSLog(@"[CAMERA] Native camera capture started");
NSLog(@"[CAMERA] Session startRunning called, isRunning=%d", captureSession.isRunning);
return 0;
}
void StopNativeCamera(void) {
NSLog(@"[CAMERA] StopNativeCamera called");
if (captureSession && captureSession.isRunning) {
[captureSession stopRunning];
NSLog(@"[CAMERA] Native camera capture stopped");
NSLog(@"[CAMERA] Session stopped");
}
captureSession = nil;
videoOutput = nil;
frameDelegate = nil;
[frameLock lock];
latestFrameBase64 = nil;
if (latestFrameBase64) {
[latestFrameBase64 release];
latestFrameBase64 = nil;
}
[frameLock unlock];
}
@@ -156,11 +205,13 @@ void StopNativeCamera(void) {
char* GetLatestCameraFrame(void) {
[frameLock lock];
NSString *frame = latestFrameBase64;
latestFrameBase64 = nil; // consume
latestFrameBase64 = nil; // transfers ownership to caller
[frameLock unlock];
if (!frame || frame.length == 0) {
if (!frame) {
return NULL;
}
return strdup([frame UTF8String]);
char *res = strdup([frame UTF8String]);
[frame release]; // Release since we had ownership
return res;
}

View File

@@ -63,6 +63,7 @@ static unsigned char* CaptureFullDesktop(int* outLen, int quality) {
NSData *jpegData = [rep representationUsingType:NSBitmapImageFileTypeJPEG properties:props];
if (!jpegData || jpegData.length == 0) {
[rep release];
*outLen = 0;
return NULL;
}
@@ -70,6 +71,7 @@ static unsigned char* CaptureFullDesktop(int* outLen, int quality) {
*outLen = (int)jpegData.length;
unsigned char *buf = (unsigned char*)malloc(jpegData.length);
memcpy(buf, jpegData.bytes, jpegData.length);
[rep release];
return buf;
}
}