This commit is contained in:
Michael Freno
2026-01-30 12:20:56 -05:00
parent 7d6e51a183
commit 6e41c4059c
7 changed files with 55 additions and 39 deletions

View File

@@ -10,16 +10,20 @@ import Combine
import Foundation import Foundation
protocol CameraSessionDelegate: AnyObject { protocol CameraSessionDelegate: AnyObject {
nonisolated func cameraSession( @MainActor func cameraSession(
_ manager: CameraSessionManager, _ manager: CameraSessionManager,
didOutput pixelBuffer: CVPixelBuffer, didOutput pixelBuffer: CVPixelBuffer,
imageSize: CGSize imageSize: CGSize
) )
} }
private struct PixelBufferBox: @unchecked Sendable {
let buffer: CVPixelBuffer
}
final class CameraSessionManager: NSObject, ObservableObject { final class CameraSessionManager: NSObject, ObservableObject {
@Published private(set) var isRunning = false @Published private(set) var isRunning = false
weak var delegate: CameraSessionDelegate? nonisolated(unsafe) weak var delegate: CameraSessionDelegate?
private var captureSession: AVCaptureSession? private var captureSession: AVCaptureSession?
private var videoOutput: AVCaptureVideoDataOutput? private var videoOutput: AVCaptureVideoDataOutput?
@@ -116,6 +120,11 @@ extension CameraSessionManager: AVCaptureVideoDataOutputSampleBufferDelegate {
height: CVPixelBufferGetHeight(pixelBuffer) height: CVPixelBufferGetHeight(pixelBuffer)
) )
delegate?.cameraSession(self, didOutput: pixelBuffer, imageSize: size) let bufferBox = PixelBufferBox(buffer: pixelBuffer)
DispatchQueue.main.async { [weak self, bufferBox] in
guard let self else { return }
self.delegate?.cameraSession(self, didOutput: bufferBox.buffer, imageSize: size)
}
} }
} }

View File

@@ -161,7 +161,7 @@ class EyeTrackingService: NSObject, ObservableObject {
} }
extension EyeTrackingService: CameraSessionDelegate { extension EyeTrackingService: CameraSessionDelegate {
nonisolated func cameraSession( @MainActor func cameraSession(
_ manager: CameraSessionManager, _ manager: CameraSessionManager,
didOutput pixelBuffer: CVPixelBuffer, didOutput pixelBuffer: CVPixelBuffer,
imageSize: CGSize imageSize: CGSize
@@ -174,28 +174,23 @@ extension EyeTrackingService: CameraSessionDelegate {
if let leftRatio = result.leftPupilRatio, if let leftRatio = result.leftPupilRatio,
let rightRatio = result.rightPupilRatio, let rightRatio = result.rightPupilRatio,
let faceWidth = result.faceWidthRatio { let faceWidth = result.faceWidthRatio {
Task { @MainActor in guard CalibratorService.shared.isCalibrating else { return }
guard CalibratorService.shared.isCalibrating else { return } CalibratorService.shared.submitSampleToBridge(
CalibratorService.shared.submitSampleToBridge( leftRatio: leftRatio,
leftRatio: leftRatio, rightRatio: rightRatio,
rightRatio: rightRatio, leftVertical: result.leftVerticalRatio,
leftVertical: result.leftVerticalRatio, rightVertical: result.rightVerticalRatio,
rightVertical: result.rightVerticalRatio, faceWidthRatio: faceWidth
faceWidthRatio: faceWidth )
)
}
} }
Task { @MainActor [weak self] in self.faceDetected = result.faceDetected
guard let self else { return } self.isEyesClosed = result.isEyesClosed
self.faceDetected = result.faceDetected self.userLookingAtScreen = result.userLookingAtScreen
self.isEyesClosed = result.isEyesClosed self.debugAdapter.update(from: result)
self.userLookingAtScreen = result.userLookingAtScreen self.debugAdapter.updateEyeImages(from: PupilDetector.self)
self.debugAdapter.update(from: result) self.syncDebugState()
self.debugAdapter.updateEyeImages(from: PupilDetector.self) self.updateGazeConfiguration()
self.syncDebugState()
self.updateGazeConfiguration()
}
} }
} }

View File

@@ -6,7 +6,7 @@
// //
import Foundation import Foundation
import Vision @preconcurrency import Vision
import simd import simd
struct EyeTrackingProcessingResult: Sendable { struct EyeTrackingProcessingResult: Sendable {
@@ -54,19 +54,19 @@ final class GazeDetector: @unchecked Sendable {
} }
private let lock = NSLock() private let lock = NSLock()
private var configuration: Configuration private nonisolated(unsafe) var configuration: Configuration
init(configuration: Configuration) { nonisolated init(configuration: Configuration) {
self.configuration = configuration self.configuration = configuration
} }
func updateConfiguration(_ configuration: Configuration) { nonisolated func updateConfiguration(_ configuration: Configuration) {
lock.lock() lock.lock()
self.configuration = configuration self.configuration = configuration
lock.unlock() lock.unlock()
} }
nonisolated func process( func process(
analysis: VisionPipeline.FaceAnalysis, analysis: VisionPipeline.FaceAnalysis,
pixelBuffer: CVPixelBuffer pixelBuffer: CVPixelBuffer
) -> EyeTrackingProcessingResult { ) -> EyeTrackingProcessingResult {
@@ -75,7 +75,7 @@ final class GazeDetector: @unchecked Sendable {
config = configuration config = configuration
lock.unlock() lock.unlock()
guard analysis.faceDetected, let face = analysis.face else { guard analysis.faceDetected, let face = analysis.face?.value else {
return EyeTrackingProcessingResult( return EyeTrackingProcessingResult(
faceDetected: false, faceDetected: false,
isEyesClosed: false, isEyesClosed: false,

View File

@@ -18,7 +18,7 @@ import Accelerate
import CoreImage import CoreImage
import ImageIO import ImageIO
import UniformTypeIdentifiers import UniformTypeIdentifiers
import Vision @preconcurrency import Vision
struct PupilPosition: Equatable, Sendable { struct PupilPosition: Equatable, Sendable {
let x: CGFloat let x: CGFloat

View File

@@ -6,17 +6,21 @@
// //
import Foundation import Foundation
import Vision @preconcurrency import Vision
final class VisionPipeline: @unchecked Sendable { final class VisionPipeline: @unchecked Sendable {
struct FaceAnalysis: Sendable { struct FaceAnalysis: Sendable {
let faceDetected: Bool let faceDetected: Bool
let face: VNFaceObservation? let face: NonSendableFaceObservation?
let imageSize: CGSize let imageSize: CGSize
let debugYaw: Double? let debugYaw: Double?
let debugPitch: Double? let debugPitch: Double?
} }
struct NonSendableFaceObservation: @unchecked Sendable {
nonisolated(unsafe) let value: VNFaceObservation
}
nonisolated func analyze( nonisolated func analyze(
pixelBuffer: CVPixelBuffer, pixelBuffer: CVPixelBuffer,
imageSize: CGSize imageSize: CGSize
@@ -46,7 +50,7 @@ final class VisionPipeline: @unchecked Sendable {
) )
} }
guard let face = (request.results as? [VNFaceObservation])?.first else { guard let face = request.results?.first else {
return FaceAnalysis( return FaceAnalysis(
faceDetected: false, faceDetected: false,
face: nil, face: nil,
@@ -58,7 +62,7 @@ final class VisionPipeline: @unchecked Sendable {
return FaceAnalysis( return FaceAnalysis(
faceDetected: true, faceDetected: true,
face: face, face: NonSendableFaceObservation(value: face),
imageSize: imageSize, imageSize: imageSize,
debugYaw: face.yaw?.doubleValue, debugYaw: face.yaw?.doubleValue,
debugPitch: face.pitch?.doubleValue debugPitch: face.pitch?.doubleValue

View File

@@ -76,7 +76,10 @@ final class MenuBarGuideOverlayPresenter {
private func startCheckTimer() { private func startCheckTimer() {
checkTimer?.invalidate() checkTimer?.invalidate()
checkTimer = Timer.scheduledTimer(withTimeInterval: 0.01, repeats: true) { [weak self] _ in checkTimer = Timer.scheduledTimer(withTimeInterval: 0.01, repeats: true) { [weak self] _ in
self?.checkWindowFrame() guard let self else { return }
Task { @MainActor in
self.checkWindowFrame()
}
} }
} }
@@ -129,7 +132,10 @@ final class MenuBarGuideOverlayPresenter {
// Set up KVO for window frame changes // Set up KVO for window frame changes
onboardingWindowObserver = onboardingWindow.observe(\.frame, options: [.new, .old]) { onboardingWindowObserver = onboardingWindow.observe(\.frame, options: [.new, .old]) {
[weak self] _, _ in [weak self] _, _ in
self?.checkWindowFrame() guard let self else { return }
Task { @MainActor in
self.checkWindowFrame()
}
} }
// Add observer for when the onboarding window is closed // Add observer for when the onboarding window is closed
@@ -145,7 +151,10 @@ final class MenuBarGuideOverlayPresenter {
} }
// Hide the overlay when onboarding window closes // Hide the overlay when onboarding window closes
self?.hide() guard let self else { return }
Task { @MainActor in
self.hide()
}
} }
} }
} }

View File

@@ -54,7 +54,6 @@ struct LookAwaySetupView: View {
private func previewLookAway() { private func previewLookAway() {
guard let screen = NSScreen.main else { return } guard let screen = NSScreen.main else { return }
let sizePercentage = settingsManager.settings.subtleReminderSize.percentage
let lookAwayIntervalMinutes = settingsManager.settings.lookAwayIntervalMinutes let lookAwayIntervalMinutes = settingsManager.settings.lookAwayIntervalMinutes
PreviewWindowHelper.showPreview(on: screen) { dismiss in PreviewWindowHelper.showPreview(on: screen) { dismiss in
LookAwayReminderView(countdownSeconds: lookAwayIntervalMinutes * 60, onDismiss: dismiss) LookAwayReminderView(countdownSeconds: lookAwayIntervalMinutes * 60, onDismiss: dismiss)