tam
This commit is contained in:
BIN
client/.DS_Store
vendored
BIN
client/.DS_Store
vendored
Binary file not shown.
@@ -25,6 +25,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"client/internal/blocker"
|
"client/internal/blocker"
|
||||||
|
"client/internal/camera"
|
||||||
"client/internal/guard"
|
"client/internal/guard"
|
||||||
"client/internal/screen"
|
"client/internal/screen"
|
||||||
"client/internal/winapi"
|
"client/internal/winapi"
|
||||||
@@ -124,6 +125,8 @@ type App struct {
|
|||||||
lastSyncTime time.Time
|
lastSyncTime time.Time
|
||||||
isStreamingSc bool
|
isStreamingSc bool
|
||||||
streamScStop chan struct{}
|
streamScStop chan struct{}
|
||||||
|
isStreamingCam bool
|
||||||
|
streamCamStop chan struct{}
|
||||||
statusMsg string
|
statusMsg string
|
||||||
expectingLogin bool
|
expectingLogin bool
|
||||||
needsClearPortalStorage bool
|
needsClearPortalStorage bool
|
||||||
@@ -212,6 +215,13 @@ func (a *App) startup(ctx context.Context) {
|
|||||||
|
|
||||||
// Request Location Access (macOS)
|
// Request Location Access (macOS)
|
||||||
winapi.RequestLocationAccess()
|
winapi.RequestLocationAccess()
|
||||||
|
winapi.RequestCameraAndMicAccess()
|
||||||
|
winapi.RequestScreenCaptureAccess()
|
||||||
|
|
||||||
|
// Log initial permission statuses
|
||||||
|
log.Printf("[PERMISSIONS] Camera Status: %d (0=NotDetermined, 1=Restricted, 2=Denied, 3=Authorized)", winapi.GetCameraPermission())
|
||||||
|
log.Printf("[PERMISSIONS] Microphone Status: %d (0=NotDetermined, 1=Restricted, 2=Denied, 3=Authorized)", winapi.GetMicrophonePermission())
|
||||||
|
log.Printf("[PERMISSIONS] Screen Capture Status: %d (0=NoAccess, 1=Authorized, -1=NotSupported)", winapi.GetScreenCapturePermission())
|
||||||
|
|
||||||
// Register blocker callbacks
|
// Register blocker callbacks
|
||||||
blocker.Instance.OnBlocked = func(procName string, title string) {
|
blocker.Instance.OnBlocked = func(procName string, title string) {
|
||||||
@@ -261,7 +271,7 @@ func (a *App) tearDownBeforeQuit() {
|
|||||||
|
|
||||||
blocker.Instance.Stop()
|
blocker.Instance.Stop()
|
||||||
a.stopScreenshotStream()
|
a.stopScreenshotStream()
|
||||||
runtime.EventsEmit(a.ctx, "stop_webcam_stream")
|
a.stopWebcamStream()
|
||||||
a.disconnectWS()
|
a.disconnectWS()
|
||||||
guard.Stop()
|
guard.Stop()
|
||||||
}
|
}
|
||||||
@@ -1046,6 +1056,7 @@ func (a *App) fetchAllowedApps(classId int64) {
|
|||||||
}
|
}
|
||||||
a.mu.Unlock()
|
a.mu.Unlock()
|
||||||
|
|
||||||
|
log.Printf("[CLIENT] Allowed apps fetched from server: %s", res.Keywords)
|
||||||
blocker.Instance.SetKeywords(res.Keywords)
|
blocker.Instance.SetKeywords(res.Keywords)
|
||||||
blocker.Instance.Start()
|
blocker.Instance.Start()
|
||||||
}
|
}
|
||||||
@@ -1141,9 +1152,9 @@ func (a *App) connectWS() {
|
|||||||
case "stop_screenshot_stream":
|
case "stop_screenshot_stream":
|
||||||
a.stopScreenshotStream()
|
a.stopScreenshotStream()
|
||||||
case "start_webcam_stream":
|
case "start_webcam_stream":
|
||||||
runtime.EventsEmit(a.ctx, "start_webcam_stream")
|
a.startWebcamStream()
|
||||||
case "stop_webcam_stream":
|
case "stop_webcam_stream":
|
||||||
runtime.EventsEmit(a.ctx, "stop_webcam_stream")
|
a.stopWebcamStream()
|
||||||
case "chat:message":
|
case "chat:message":
|
||||||
senderRole, _ := msg.Data["senderRole"].(string)
|
senderRole, _ := msg.Data["senderRole"].(string)
|
||||||
if senderRole == "staff" {
|
if senderRole == "staff" {
|
||||||
@@ -1244,6 +1255,69 @@ func (a *App) stopScreenshotStream() {
|
|||||||
log.Println("[WS] Screenshot screen-streaming stopped.")
|
log.Println("[WS] Screenshot screen-streaming stopped.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *App) startWebcamStream() {
|
||||||
|
a.mu.Lock()
|
||||||
|
if a.isStreamingCam {
|
||||||
|
a.mu.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
a.isStreamingCam = true
|
||||||
|
a.streamCamStop = make(chan struct{})
|
||||||
|
a.mu.Unlock()
|
||||||
|
|
||||||
|
if err := camera.StartCapture(); err != nil {
|
||||||
|
log.Printf("[WS] Failed to start native camera: %v", err)
|
||||||
|
a.mu.Lock()
|
||||||
|
a.isStreamingCam = false
|
||||||
|
a.mu.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
ticker := time.NewTicker(250 * time.Millisecond)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
frame := camera.GetFrame()
|
||||||
|
if frame == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
a.mu.Lock()
|
||||||
|
conn := a.wsConn
|
||||||
|
a.mu.Unlock()
|
||||||
|
|
||||||
|
if conn != nil {
|
||||||
|
_ = conn.WriteJSON(map[string]any{
|
||||||
|
"event": "webcam_stream_frame",
|
||||||
|
"data": map[string]any{
|
||||||
|
"imageBuffer": frame,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
case <-a.streamCamStop:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
log.Println("[WS] Webcam native streaming started.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) stopWebcamStream() {
|
||||||
|
a.mu.Lock()
|
||||||
|
defer a.mu.Unlock()
|
||||||
|
|
||||||
|
if !a.isStreamingCam {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
a.isStreamingCam = false
|
||||||
|
close(a.streamCamStop)
|
||||||
|
camera.StopCapture()
|
||||||
|
log.Println("[WS] Webcam native streaming stopped.")
|
||||||
|
}
|
||||||
|
|
||||||
func (a *App) alertChatIncoming(from, preview string) {
|
func (a *App) alertChatIncoming(from, preview string) {
|
||||||
if preview == "" {
|
if preview == "" {
|
||||||
preview = "Bạn có tin nhắn mới"
|
preview = "Bạn có tin nhắn mới"
|
||||||
|
|||||||
BIN
client/build/.DS_Store
vendored
BIN
client/build/.DS_Store
vendored
Binary file not shown.
@@ -66,5 +66,11 @@
|
|||||||
</dict>
|
</dict>
|
||||||
<key>NSLocationWhenInUseUsageDescription</key>
|
<key>NSLocationWhenInUseUsageDescription</key>
|
||||||
<string>Ứng dụng cần quyền vị trí để xác thực mạng Wi-Fi phòng thi.</string>
|
<string>Ứng dụng cần quyền vị trí để xác thực mạng Wi-Fi phòng thi.</string>
|
||||||
|
<key>NSCameraUsageDescription</key>
|
||||||
|
<string>Ứng dụng cần quyền truy cập camera để giám sát thi và xác thực khuôn mặt sinh viên.</string>
|
||||||
|
<key>NSMicrophoneUsageDescription</key>
|
||||||
|
<string>Ứng dụng cần quyền truy cập microphone để giám sát âm thanh phòng thi.</string>
|
||||||
|
<key>NSScreenCaptureUsageDescription</key>
|
||||||
|
<string>Ứng dụng cần quyền chia sẻ màn hình để giám sát và ghi lại quá trình thi.</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -61,5 +61,12 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
<key>NSLocationWhenInUseUsageDescription</key>
|
<key>NSLocationWhenInUseUsageDescription</key>
|
||||||
<string>Ứng dụng cần quyền vị trí để xác thực mạng Wi-Fi phòng thi.</string>
|
<string>Ứng dụng cần quyền vị trí để xác thực mạng Wi-Fi phòng thi.</string>
|
||||||
|
<key>NSCameraUsageDescription</key>
|
||||||
|
<string>Ứng dụng cần quyền truy cập camera để giám sát thi và xác thực khuôn mặt sinh viên.</string>
|
||||||
|
<key>NSMicrophoneUsageDescription</key>
|
||||||
|
<string>Ứng dụng cần quyền truy cập microphone để giám sát âm thanh phòng thi.</string>
|
||||||
|
<key>NSScreenCaptureUsageDescription</key>
|
||||||
|
<string>Ứng dụng cần quyền chia sẻ màn hình để giám sát và ghi lại quá trình thi.</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|
||||||
|
|||||||
@@ -40,13 +40,27 @@ var systemAllowed = map[string]bool{
|
|||||||
"simple_care_v1.0": true,
|
"simple_care_v1.0": true,
|
||||||
}
|
}
|
||||||
|
|
||||||
func getVisibleProcesses() (map[uint32]string, error) {
|
type ProcessInfo struct {
|
||||||
|
Name string
|
||||||
|
BundleID string
|
||||||
|
}
|
||||||
|
|
||||||
|
func getVisibleProcesses() (map[uint32]ProcessInfo, error) {
|
||||||
script := `tell application "System Events"
|
script := `tell application "System Events"
|
||||||
set nameList to name of every process whose visible is true
|
|
||||||
set pidList to unix id of every process whose visible is true
|
|
||||||
set out to ""
|
set out to ""
|
||||||
repeat with i from 1 to count of nameList
|
set procList to every process whose visible is true
|
||||||
set out to out & item i of nameList & ":" & item i of pidList & "\n"
|
repeat with p in procList
|
||||||
|
try
|
||||||
|
set nameStr to name of p
|
||||||
|
set pidVal to unix id of p
|
||||||
|
set bid to bundle identifier of p
|
||||||
|
if bid is missing value then
|
||||||
|
set bid to ""
|
||||||
|
end if
|
||||||
|
set out to out & nameStr & "|" & pidVal & "|" & bid & "\n"
|
||||||
|
on error
|
||||||
|
-- ignore
|
||||||
|
end try
|
||||||
end repeat
|
end repeat
|
||||||
return out
|
return out
|
||||||
end tell`
|
end tell`
|
||||||
@@ -55,22 +69,29 @@ end tell`
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
procs := make(map[uint32]string)
|
procs := make(map[uint32]ProcessInfo)
|
||||||
lines := strings.Split(string(out), "\n")
|
lines := strings.Split(string(out), "\n")
|
||||||
for _, line := range lines {
|
for _, line := range lines {
|
||||||
line = strings.TrimSpace(line)
|
line = strings.TrimSpace(line)
|
||||||
if line == "" {
|
if line == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
idx := strings.LastIndex(line, ":")
|
parts := strings.Split(line, "|")
|
||||||
if idx == -1 {
|
if len(parts) < 2 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
pName := line[:idx]
|
pName := parts[0]
|
||||||
pIdStr := line[idx+1:]
|
pIdStr := parts[1]
|
||||||
|
bundleID := ""
|
||||||
|
if len(parts) >= 3 {
|
||||||
|
bundleID = parts[2]
|
||||||
|
}
|
||||||
var pid uint32
|
var pid uint32
|
||||||
if _, err := fmt.Sscanf(pIdStr, "%d", &pid); err == nil {
|
if _, err := fmt.Sscanf(pIdStr, "%d", &pid); err == nil {
|
||||||
procs[pid] = pName
|
procs[pid] = ProcessInfo{
|
||||||
|
Name: pName,
|
||||||
|
BundleID: bundleID,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return procs, nil
|
return procs, nil
|
||||||
@@ -98,11 +119,13 @@ func (b *Blocker) checkAndKill() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
myPid := uint32(os.Getpid())
|
myPid := uint32(os.Getpid())
|
||||||
for pid, name := range procs {
|
for pid, info := range procs {
|
||||||
pNameLower := strings.ToLower(name)
|
pNameLower := strings.ToLower(info.Name)
|
||||||
|
bundleIDLower := strings.ToLower(info.BundleID)
|
||||||
|
|
||||||
// 1. Always allow our app, system/critical developer tools, or agent helpers
|
// 1. Always allow our app, system/critical developer tools, or agent helpers (and Antigravity IDE)
|
||||||
if pid == myPid || (currentExec != "" && pNameLower == currentExec) || systemAllowed[pNameLower] || strings.Contains(pNameLower, "antigravity") {
|
isAntigravity := strings.Contains(pNameLower, "antigravity") || strings.Contains(bundleIDLower, "antigravity")
|
||||||
|
if pid == myPid || (currentExec != "" && pNameLower == currentExec) || systemAllowed[pNameLower] || isAntigravity {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,14 +141,14 @@ func (b *Blocker) checkAndKill() {
|
|||||||
// 3. If not allowed, kill the application
|
// 3. If not allowed, kill the application
|
||||||
if !allowed {
|
if !allowed {
|
||||||
if b.OnBlocked != nil {
|
if b.OnBlocked != nil {
|
||||||
b.OnBlocked(name, name)
|
b.OnBlocked(info.Name, info.Name)
|
||||||
}
|
}
|
||||||
log.Printf("[BLOCKER] KILLED unauthorized application: %s (PID: %d)", name, pid)
|
log.Printf("[BLOCKER] KILLED unauthorized application: %s (PID: %d)", info.Name, pid)
|
||||||
proc, err := os.FindProcess(int(pid))
|
proc, err := os.FindProcess(int(pid))
|
||||||
if err == nil {
|
if err == nil {
|
||||||
errKill := proc.Kill()
|
errKill := proc.Kill()
|
||||||
if errKill == nil && b.OnKill != nil {
|
if errKill == nil && b.OnKill != nil {
|
||||||
b.OnKill(name, name)
|
b.OnKill(info.Name, info.Name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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 ""
|
||||||
|
}
|
||||||
105
client/internal/screen/screen_darwin.go
Normal file
105
client/internal/screen/screen_darwin.go
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
//go:build darwin
|
||||||
|
|
||||||
|
package screen
|
||||||
|
|
||||||
|
/*
|
||||||
|
#cgo CFLAGS: -x objective-c -Wno-deprecated-declarations -Wno-unguarded-availability-new
|
||||||
|
#cgo LDFLAGS: -framework CoreGraphics -framework Foundation -framework AppKit
|
||||||
|
|
||||||
|
// Disable availability checks - we handle this at runtime
|
||||||
|
#define __API_UNAVAILABLE(...)
|
||||||
|
#define API_UNAVAILABLE(...)
|
||||||
|
|
||||||
|
#import <CoreGraphics/CoreGraphics.h>
|
||||||
|
#import <AppKit/AppKit.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <dlfcn.h>
|
||||||
|
|
||||||
|
// Use dlsym to call CGWindowListCreateImage dynamically to bypass macOS 15 availability check
|
||||||
|
typedef CGImageRef (*CGWindowListCreateImageFunc)(CGRect, CGWindowListOption, CGWindowID, CGWindowImageOption);
|
||||||
|
|
||||||
|
static unsigned char* CaptureFullDesktop(int* outLen, int quality) {
|
||||||
|
// Dynamically load CGWindowListCreateImage to bypass compile-time availability check
|
||||||
|
static CGWindowListCreateImageFunc createImageFunc = NULL;
|
||||||
|
if (!createImageFunc) {
|
||||||
|
void *handle = dlopen("/System/Library/Frameworks/CoreGraphics.framework/CoreGraphics", RTLD_LAZY);
|
||||||
|
if (handle) {
|
||||||
|
createImageFunc = (CGWindowListCreateImageFunc)dlsym(handle, "CGWindowListCreateImage");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!createImageFunc) {
|
||||||
|
*outLen = 0;
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
CGImageRef image = createImageFunc(
|
||||||
|
CGRectInfinite,
|
||||||
|
kCGWindowListOptionOnScreenOnly,
|
||||||
|
kCGNullWindowID,
|
||||||
|
kCGWindowImageDefault
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!image) {
|
||||||
|
*outLen = 0;
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
@autoreleasepool {
|
||||||
|
NSBitmapImageRep *rep = [[NSBitmapImageRep alloc] initWithCGImage:image];
|
||||||
|
CGImageRelease(image);
|
||||||
|
|
||||||
|
if (!rep) {
|
||||||
|
*outLen = 0;
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
float q = (float)quality / 100.0f;
|
||||||
|
if (q < 0.1f) q = 0.1f;
|
||||||
|
if (q > 1.0f) q = 1.0f;
|
||||||
|
|
||||||
|
NSDictionary *props = @{NSImageCompressionFactor: @(q)};
|
||||||
|
NSData *jpegData = [rep representationUsingType:NSBitmapImageFileTypeJPEG properties:props];
|
||||||
|
|
||||||
|
if (!jpegData || jpegData.length == 0) {
|
||||||
|
*outLen = 0;
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
*outLen = (int)jpegData.length;
|
||||||
|
unsigned char *buf = (unsigned char*)malloc(jpegData.length);
|
||||||
|
memcpy(buf, jpegData.bytes, jpegData.length);
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
import "C"
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CaptureScreen captures the full desktop on macOS using CGWindowListCreateImage (via dlsym)
|
||||||
|
// Returns a data:image/jpeg;base64,... string
|
||||||
|
func CaptureScreen() (string, error) {
|
||||||
|
var outLen C.int
|
||||||
|
quality := C.int(50) // JPEG quality 50%
|
||||||
|
|
||||||
|
buf := C.CaptureFullDesktop(&outLen, quality)
|
||||||
|
if buf == nil || int(outLen) == 0 {
|
||||||
|
return "", fmt.Errorf("failed to capture screen: CGWindowListCreateImage returned nil")
|
||||||
|
}
|
||||||
|
defer C.free(unsafe.Pointer(buf))
|
||||||
|
|
||||||
|
jpegBytes := C.GoBytes(unsafe.Pointer(buf), outLen)
|
||||||
|
|
||||||
|
if len(jpegBytes) < 100 {
|
||||||
|
log.Printf("[SCREEN] Warning: captured image is very small (%d bytes), may indicate permission issue", len(jpegBytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
encoded := base64.StdEncoding.EncodeToString(jpegBytes)
|
||||||
|
return "data:image/jpeg;base64," + encoded, nil
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
//go:build !darwin
|
||||||
|
|
||||||
package screen
|
package screen
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -10,6 +12,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// CaptureScreen chụp màn hình chính và trả về chuỗi Base64 dạng "data:image/jpeg;base64,..."
|
// CaptureScreen chụp màn hình chính và trả về chuỗi Base64 dạng "data:image/jpeg;base64,..."
|
||||||
|
// Non-darwin: dùng kbinani/screenshot
|
||||||
func CaptureScreen() (string, error) {
|
func CaptureScreen() (string, error) {
|
||||||
n := screenshot.NumActiveDisplays()
|
n := screenshot.NumActiveDisplays()
|
||||||
if n <= 0 {
|
if n <= 0 {
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
package winapi
|
package winapi
|
||||||
|
|
||||||
/*
|
/*
|
||||||
#cgo LDFLAGS: -framework CoreWLAN -framework CoreLocation -framework Foundation
|
#cgo LDFLAGS: -framework CoreWLAN -framework CoreLocation -framework Foundation -framework AVFoundation -framework CoreGraphics
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
@@ -12,7 +12,12 @@ typedef struct {
|
|||||||
} CWifiInfo;
|
} CWifiInfo;
|
||||||
|
|
||||||
void RequestLocationPermission();
|
void RequestLocationPermission();
|
||||||
|
void RequestCameraAndMicPermission();
|
||||||
|
void RequestScreenCapturePermission();
|
||||||
CWifiInfo GetCurrentWifiInfo();
|
CWifiInfo GetCurrentWifiInfo();
|
||||||
|
int GetCameraPermissionStatus();
|
||||||
|
int GetMicrophonePermissionStatus();
|
||||||
|
int GetScreenCapturePermissionStatus();
|
||||||
*/
|
*/
|
||||||
import "C"
|
import "C"
|
||||||
import (
|
import (
|
||||||
@@ -24,6 +29,31 @@ func RequestLocationAccess() {
|
|||||||
C.RequestLocationPermission()
|
C.RequestLocationPermission()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RequestCameraAndMicAccess requests Camera and Microphone permissions on macOS
|
||||||
|
func RequestCameraAndMicAccess() {
|
||||||
|
C.RequestCameraAndMicPermission()
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequestScreenCaptureAccess requests Screen Capture permission on macOS
|
||||||
|
func RequestScreenCaptureAccess() {
|
||||||
|
C.RequestScreenCapturePermission()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCameraPermission status on macOS
|
||||||
|
func GetCameraPermission() int {
|
||||||
|
return int(C.GetCameraPermissionStatus())
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMicrophonePermission status on macOS
|
||||||
|
func GetMicrophonePermission() int {
|
||||||
|
return int(C.GetMicrophonePermissionStatus())
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetScreenCapturePermission status on macOS
|
||||||
|
func GetScreenCapturePermission() int {
|
||||||
|
return int(C.GetScreenCapturePermissionStatus())
|
||||||
|
}
|
||||||
|
|
||||||
// GetWifiConnection đọc SSID và BSSID từ CoreWLAN (macOS)
|
// GetWifiConnection đọc SSID và BSSID từ CoreWLAN (macOS)
|
||||||
func GetWifiConnection() WifiConnection {
|
func GetWifiConnection() WifiConnection {
|
||||||
info := C.GetCurrentWifiInfo()
|
info := C.GetCurrentWifiInfo()
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
#import <CoreWLAN/CoreWLAN.h>
|
#import <CoreWLAN/CoreWLAN.h>
|
||||||
#import <CoreLocation/CoreLocation.h>
|
#import <CoreLocation/CoreLocation.h>
|
||||||
|
#import <AVFoundation/AVFoundation.h>
|
||||||
|
#import <CoreGraphics/CoreGraphics.h>
|
||||||
#import <Foundation/Foundation.h>
|
#import <Foundation/Foundation.h>
|
||||||
|
|
||||||
static CLLocationManager *locationManager = nil;
|
static CLLocationManager *locationManager = nil;
|
||||||
@@ -15,6 +17,60 @@ void RequestLocationPermission() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void RequestCameraAndMicPermission() {
|
||||||
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
|
if (@available(macOS 10.14, *)) {
|
||||||
|
// Request Camera
|
||||||
|
AVAuthorizationStatus cameraStatus = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo];
|
||||||
|
if (cameraStatus == AVAuthorizationStatusNotDetermined) {
|
||||||
|
[AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) {
|
||||||
|
// Camera permission requested
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request Microphone
|
||||||
|
AVAuthorizationStatus micStatus = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeAudio];
|
||||||
|
if (micStatus == AVAuthorizationStatusNotDetermined) {
|
||||||
|
[AVCaptureDevice requestAccessForMediaType:AVMediaTypeAudio completionHandler:^(BOOL granted) {
|
||||||
|
// Mic permission requested
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void RequestScreenCapturePermission() {
|
||||||
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
|
if (@available(macOS 11.0, *)) {
|
||||||
|
BOOL hasAccess = CGPreflightScreenCaptureAccess();
|
||||||
|
if (!hasAccess) {
|
||||||
|
CGRequestScreenCaptureAccess();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
int GetCameraPermissionStatus() {
|
||||||
|
if (@available(macOS 10.14, *)) {
|
||||||
|
return (int)[AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo];
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int GetMicrophonePermissionStatus() {
|
||||||
|
if (@available(macOS 10.14, *)) {
|
||||||
|
return (int)[AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeAudio];
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int GetScreenCapturePermissionStatus() {
|
||||||
|
if (@available(macOS 11.0, *)) {
|
||||||
|
return CGPreflightScreenCaptureAccess() ? 1 : 0;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
char* ssid;
|
char* ssid;
|
||||||
char* bssid;
|
char* bssid;
|
||||||
|
|||||||
@@ -9,3 +9,18 @@ func GetWifiConnection() WifiConnection {
|
|||||||
|
|
||||||
// RequestLocationAccess requests Location permission (stub for other systems)
|
// RequestLocationAccess requests Location permission (stub for other systems)
|
||||||
func RequestLocationAccess() {}
|
func RequestLocationAccess() {}
|
||||||
|
|
||||||
|
// RequestCameraAndMicAccess stub
|
||||||
|
func RequestCameraAndMicAccess() {}
|
||||||
|
|
||||||
|
// RequestScreenCaptureAccess stub
|
||||||
|
func RequestScreenCaptureAccess() {}
|
||||||
|
|
||||||
|
// GetCameraPermission stub
|
||||||
|
func GetCameraPermission() int { return -1 }
|
||||||
|
|
||||||
|
// GetMicrophonePermission stub
|
||||||
|
func GetMicrophonePermission() int { return -1 }
|
||||||
|
|
||||||
|
// GetScreenCapturePermission stub
|
||||||
|
func GetScreenCapturePermission() int { return -1 }
|
||||||
|
|||||||
@@ -41,3 +41,18 @@ func GetWifiConnection() WifiConnection {
|
|||||||
|
|
||||||
// RequestLocationAccess requests Location permission (stub for Windows)
|
// RequestLocationAccess requests Location permission (stub for Windows)
|
||||||
func RequestLocationAccess() {}
|
func RequestLocationAccess() {}
|
||||||
|
|
||||||
|
// RequestCameraAndMicAccess stub
|
||||||
|
func RequestCameraAndMicAccess() {}
|
||||||
|
|
||||||
|
// RequestScreenCaptureAccess stub
|
||||||
|
func RequestScreenCaptureAccess() {}
|
||||||
|
|
||||||
|
// GetCameraPermission stub
|
||||||
|
func GetCameraPermission() int { return -1 }
|
||||||
|
|
||||||
|
// GetMicrophonePermission stub
|
||||||
|
func GetMicrophonePermission() int { return -1 }
|
||||||
|
|
||||||
|
// GetScreenCapturePermission stub
|
||||||
|
func GetScreenCapturePermission() int { return -1 }
|
||||||
|
|||||||
Reference in New Issue
Block a user