tam
This commit is contained in:
46
client/internal/camera/camera_darwin.go
Normal file
46
client/internal/camera/camera_darwin.go
Normal file
@@ -0,0 +1,46 @@
|
||||
//go:build darwin
|
||||
|
||||
package camera
|
||||
|
||||
/*
|
||||
#cgo LDFLAGS: -framework AVFoundation -framework Foundation -framework CoreImage -framework CoreMedia -framework CoreVideo -framework AppKit
|
||||
#include <stdlib.h>
|
||||
|
||||
int StartNativeCamera(void);
|
||||
void StopNativeCamera(void);
|
||||
char* GetLatestCameraFrame(void);
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// StartCapture starts native camera capture via AVCaptureSession
|
||||
func StartCapture() error {
|
||||
ret := C.StartNativeCamera()
|
||||
if ret != 0 {
|
||||
log.Println("[CAMERA] Failed to start native camera capture")
|
||||
return fmt.Errorf("failed to start camera (code %d)", int(ret))
|
||||
}
|
||||
log.Println("[CAMERA] Native camera capture started successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
// StopCapture stops native camera capture
|
||||
func StopCapture() {
|
||||
C.StopNativeCamera()
|
||||
log.Println("[CAMERA] Native camera capture stopped")
|
||||
}
|
||||
|
||||
// GetFrame returns the latest camera frame as a data:image/jpeg;base64,... string
|
||||
// Returns empty string if no frame is available
|
||||
func GetFrame() string {
|
||||
cstr := C.GetLatestCameraFrame()
|
||||
if cstr == nil {
|
||||
return ""
|
||||
}
|
||||
defer C.free(unsafe.Pointer(cstr))
|
||||
return C.GoString(cstr)
|
||||
}
|
||||
166
client/internal/camera/camera_darwin.m
Normal file
166
client/internal/camera/camera_darwin.m
Normal file
@@ -0,0 +1,166 @@
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <CoreImage/CoreImage.h>
|
||||
#import <AppKit/AppKit.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
// ── Shared state ──────────────────────────────────────────────
|
||||
static AVCaptureSession *captureSession = nil;
|
||||
static AVCaptureVideoDataOutput *videoOutput = nil;
|
||||
static dispatch_queue_t captureQueue = nil;
|
||||
|
||||
// Latest frame stored as JPEG base64
|
||||
static NSString *latestFrameBase64 = nil;
|
||||
static NSLock *frameLock = nil;
|
||||
|
||||
// ── Delegate that receives sample buffers ─────────────────────
|
||||
@interface CameraFrameDelegate : NSObject <AVCaptureVideoDataOutputSampleBufferDelegate>
|
||||
@end
|
||||
|
||||
@implementation CameraFrameDelegate
|
||||
|
||||
- (void)captureOutput:(AVCaptureOutput *)output
|
||||
didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
|
||||
fromConnection:(AVCaptureConnection *)connection {
|
||||
|
||||
CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
|
||||
if (!imageBuffer) 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);
|
||||
|
||||
NSDictionary *props = @{NSImageCompressionFactor: @(0.4)};
|
||||
NSData *jpegData = [rep representationUsingType:NSBitmapImageFileTypeJPEG properties:props];
|
||||
if (!jpegData) return;
|
||||
|
||||
NSString *b64 = [jpegData base64EncodedStringWithOptions:0];
|
||||
NSString *dataUrl = [NSString stringWithFormat:@"data:image/jpeg;base64,%@", b64];
|
||||
|
||||
[frameLock lock];
|
||||
latestFrameBase64 = dataUrl;
|
||||
[frameLock unlock];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
static CameraFrameDelegate *frameDelegate = nil;
|
||||
|
||||
// ── C-exported functions ──────────────────────────────────────
|
||||
|
||||
// StartNativeCamera: returns 0 on success, -1 on failure
|
||||
int StartNativeCamera(void) {
|
||||
if (captureSession && captureSession.isRunning) {
|
||||
return 0; // already running
|
||||
}
|
||||
|
||||
if (!frameLock) {
|
||||
frameLock = [[NSLock alloc] init];
|
||||
}
|
||||
|
||||
// Check camera permission
|
||||
if (@available(macOS 10.14, *)) {
|
||||
AVAuthorizationStatus status = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo];
|
||||
if (status == AVAuthorizationStatusDenied || status == AVAuthorizationStatusRestricted) {
|
||||
NSLog(@"[CAMERA] Camera permission denied (status=%ld)", (long)status);
|
||||
return -1;
|
||||
}
|
||||
if (status == AVAuthorizationStatusNotDetermined) {
|
||||
// Request permission synchronously via semaphore
|
||||
dispatch_semaphore_t sem = dispatch_semaphore_create(0);
|
||||
__block BOOL granted = NO;
|
||||
[AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL g) {
|
||||
granted = g;
|
||||
dispatch_semaphore_signal(sem);
|
||||
}];
|
||||
dispatch_semaphore_wait(sem, dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC));
|
||||
if (!granted) {
|
||||
NSLog(@"[CAMERA] Camera permission not granted");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
captureSession = [[AVCaptureSession alloc] init];
|
||||
captureSession.sessionPreset = AVCaptureSessionPresetLow; // 320×240-ish
|
||||
|
||||
// Find default video device
|
||||
AVCaptureDevice *camera = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
|
||||
if (!camera) {
|
||||
NSLog(@"[CAMERA] No camera device found");
|
||||
captureSession = nil;
|
||||
return -1;
|
||||
}
|
||||
|
||||
NSError *error = nil;
|
||||
AVCaptureDeviceInput *input = [AVCaptureDeviceInput deviceInputWithDevice:camera error:&error];
|
||||
if (error || !input) {
|
||||
NSLog(@"[CAMERA] Cannot create camera input: %@", error);
|
||||
captureSession = nil;
|
||||
return -1;
|
||||
}
|
||||
|
||||
if ([captureSession canAddInput:input]) {
|
||||
[captureSession addInput:input];
|
||||
} else {
|
||||
NSLog(@"[CAMERA] Cannot add camera input to session");
|
||||
captureSession = nil;
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Video data output
|
||||
videoOutput = [[AVCaptureVideoDataOutput alloc] init];
|
||||
videoOutput.videoSettings = @{
|
||||
(NSString *)kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_32BGRA)
|
||||
};
|
||||
videoOutput.alwaysDiscardsLateVideoFrames = YES;
|
||||
|
||||
captureQueue = dispatch_queue_create("com.simplecare.camera", DISPATCH_QUEUE_SERIAL);
|
||||
frameDelegate = [[CameraFrameDelegate alloc] init];
|
||||
[videoOutput setSampleBufferDelegate:frameDelegate queue:captureQueue];
|
||||
|
||||
if ([captureSession canAddOutput:videoOutput]) {
|
||||
[captureSession addOutput:videoOutput];
|
||||
} else {
|
||||
NSLog(@"[CAMERA] Cannot add video output to session");
|
||||
captureSession = nil;
|
||||
return -1;
|
||||
}
|
||||
|
||||
[captureSession startRunning];
|
||||
NSLog(@"[CAMERA] Native camera capture started");
|
||||
return 0;
|
||||
}
|
||||
|
||||
void StopNativeCamera(void) {
|
||||
if (captureSession && captureSession.isRunning) {
|
||||
[captureSession stopRunning];
|
||||
NSLog(@"[CAMERA] Native camera capture stopped");
|
||||
}
|
||||
captureSession = nil;
|
||||
videoOutput = nil;
|
||||
frameDelegate = nil;
|
||||
|
||||
[frameLock lock];
|
||||
latestFrameBase64 = nil;
|
||||
[frameLock unlock];
|
||||
}
|
||||
|
||||
// GetLatestCameraFrame: returns a C-string (caller must free) or NULL
|
||||
char* GetLatestCameraFrame(void) {
|
||||
[frameLock lock];
|
||||
NSString *frame = latestFrameBase64;
|
||||
latestFrameBase64 = nil; // consume
|
||||
[frameLock unlock];
|
||||
|
||||
if (!frame || frame.length == 0) {
|
||||
return NULL;
|
||||
}
|
||||
return strdup([frame UTF8String]);
|
||||
}
|
||||
20
client/internal/camera/camera_other.go
Normal file
20
client/internal/camera/camera_other.go
Normal file
@@ -0,0 +1,20 @@
|
||||
//go:build !darwin
|
||||
|
||||
package camera
|
||||
|
||||
import "log"
|
||||
|
||||
// StartCapture is a no-op on non-darwin platforms (Windows uses different webcam API)
|
||||
func StartCapture() error {
|
||||
log.Println("[CAMERA] Native camera capture not supported on this platform")
|
||||
return nil
|
||||
}
|
||||
|
||||
// StopCapture is a no-op on non-darwin platforms
|
||||
func StopCapture() {
|
||||
}
|
||||
|
||||
// GetFrame always returns empty on non-darwin platforms
|
||||
func GetFrame() string {
|
||||
return ""
|
||||
}
|
||||
Reference in New Issue
Block a user