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
All checks were successful
Deploy on Master Change / deploy (push) Successful in 42s
This commit is contained in:
@@ -1143,8 +1143,10 @@ func (a *App) connectWS() {
|
||||
}
|
||||
err := conn.ReadJSON(&msg)
|
||||
if err != nil {
|
||||
log.Printf("[WS] ReadJSON error: %v", err)
|
||||
break
|
||||
}
|
||||
log.Printf("[WS] Received event: %s", msg.Event)
|
||||
|
||||
switch msg.Event {
|
||||
case "start_screenshot_stream":
|
||||
@@ -1276,26 +1278,39 @@ func (a *App) startWebcamStream() {
|
||||
go func() {
|
||||
ticker := time.NewTicker(250 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
emptyCount := 0
|
||||
sentCount := 0
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
frame := camera.GetFrame()
|
||||
if frame == "" {
|
||||
emptyCount++
|
||||
if emptyCount <= 20 || emptyCount%100 == 0 {
|
||||
log.Printf("[WS] Webcam: no frame available (empty count: %d)", emptyCount)
|
||||
}
|
||||
continue
|
||||
}
|
||||
emptyCount = 0
|
||||
sentCount++
|
||||
|
||||
a.mu.Lock()
|
||||
conn := a.wsConn
|
||||
a.mu.Unlock()
|
||||
|
||||
if conn != nil {
|
||||
_ = conn.WriteJSON(map[string]any{
|
||||
err := conn.WriteJSON(map[string]any{
|
||||
"event": "webcam_stream_frame",
|
||||
"data": map[string]any{
|
||||
"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:
|
||||
return
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user