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

@@ -1143,8 +1143,10 @@ func (a *App) connectWS() {
} }
err := conn.ReadJSON(&msg) err := conn.ReadJSON(&msg)
if err != nil { if err != nil {
log.Printf("[WS] ReadJSON error: %v", err)
break break
} }
log.Printf("[WS] Received event: %s", msg.Event)
switch msg.Event { switch msg.Event {
case "start_screenshot_stream": case "start_screenshot_stream":
@@ -1276,26 +1278,39 @@ func (a *App) startWebcamStream() {
go func() { go func() {
ticker := time.NewTicker(250 * time.Millisecond) ticker := time.NewTicker(250 * time.Millisecond)
defer ticker.Stop() defer ticker.Stop()
emptyCount := 0
sentCount := 0
for { for {
select { select {
case <-ticker.C: case <-ticker.C:
frame := camera.GetFrame() frame := camera.GetFrame()
if frame == "" { if frame == "" {
emptyCount++
if emptyCount <= 20 || emptyCount%100 == 0 {
log.Printf("[WS] Webcam: no frame available (empty count: %d)", emptyCount)
}
continue continue
} }
emptyCount = 0
sentCount++
a.mu.Lock() a.mu.Lock()
conn := a.wsConn conn := a.wsConn
a.mu.Unlock() a.mu.Unlock()
if conn != nil { if conn != nil {
_ = conn.WriteJSON(map[string]any{ err := conn.WriteJSON(map[string]any{
"event": "webcam_stream_frame", "event": "webcam_stream_frame",
"data": map[string]any{ "data": map[string]any{
"imageBuffer": frame, "imageBuffer": frame,
}, },
}) })
if sentCount <= 5 {
log.Printf("[WS] Webcam frame #%d sent (len=%d, err=%v)", sentCount, len(frame), err)
}
} else if sentCount <= 5 {
log.Printf("[WS] Webcam frame #%d: no WS connection", sentCount)
} }
case <-a.streamCamStop: case <-a.streamCamStop:
return return

View File

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