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)
|
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
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user