building
This commit is contained in:
@@ -10,16 +10,20 @@ import Combine
|
||||
import Foundation
|
||||
|
||||
protocol CameraSessionDelegate: AnyObject {
|
||||
nonisolated func cameraSession(
|
||||
@MainActor func cameraSession(
|
||||
_ manager: CameraSessionManager,
|
||||
didOutput pixelBuffer: CVPixelBuffer,
|
||||
imageSize: CGSize
|
||||
)
|
||||
}
|
||||
|
||||
private struct PixelBufferBox: @unchecked Sendable {
|
||||
let buffer: CVPixelBuffer
|
||||
}
|
||||
|
||||
final class CameraSessionManager: NSObject, ObservableObject {
|
||||
@Published private(set) var isRunning = false
|
||||
weak var delegate: CameraSessionDelegate?
|
||||
nonisolated(unsafe) weak var delegate: CameraSessionDelegate?
|
||||
|
||||
private var captureSession: AVCaptureSession?
|
||||
private var videoOutput: AVCaptureVideoDataOutput?
|
||||
@@ -116,6 +120,11 @@ extension CameraSessionManager: AVCaptureVideoDataOutputSampleBufferDelegate {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,7 +161,7 @@ class EyeTrackingService: NSObject, ObservableObject {
|
||||
}
|
||||
|
||||
extension EyeTrackingService: CameraSessionDelegate {
|
||||
nonisolated func cameraSession(
|
||||
@MainActor func cameraSession(
|
||||
_ manager: CameraSessionManager,
|
||||
didOutput pixelBuffer: CVPixelBuffer,
|
||||
imageSize: CGSize
|
||||
@@ -174,7 +174,6 @@ extension EyeTrackingService: CameraSessionDelegate {
|
||||
if let leftRatio = result.leftPupilRatio,
|
||||
let rightRatio = result.rightPupilRatio,
|
||||
let faceWidth = result.faceWidthRatio {
|
||||
Task { @MainActor in
|
||||
guard CalibratorService.shared.isCalibrating else { return }
|
||||
CalibratorService.shared.submitSampleToBridge(
|
||||
leftRatio: leftRatio,
|
||||
@@ -184,10 +183,7 @@ extension EyeTrackingService: CameraSessionDelegate {
|
||||
faceWidthRatio: faceWidth
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Task { @MainActor [weak self] in
|
||||
guard let self else { return }
|
||||
self.faceDetected = result.faceDetected
|
||||
self.isEyesClosed = result.isEyesClosed
|
||||
self.userLookingAtScreen = result.userLookingAtScreen
|
||||
@@ -196,7 +192,6 @@ extension EyeTrackingService: CameraSessionDelegate {
|
||||
self.syncDebugState()
|
||||
self.updateGazeConfiguration()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Error Handling
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Vision
|
||||
@preconcurrency import Vision
|
||||
import simd
|
||||
|
||||
struct EyeTrackingProcessingResult: Sendable {
|
||||
@@ -54,19 +54,19 @@ final class GazeDetector: @unchecked Sendable {
|
||||
}
|
||||
|
||||
private let lock = NSLock()
|
||||
private var configuration: Configuration
|
||||
private nonisolated(unsafe) var configuration: Configuration
|
||||
|
||||
init(configuration: Configuration) {
|
||||
nonisolated init(configuration: Configuration) {
|
||||
self.configuration = configuration
|
||||
}
|
||||
|
||||
func updateConfiguration(_ configuration: Configuration) {
|
||||
nonisolated func updateConfiguration(_ configuration: Configuration) {
|
||||
lock.lock()
|
||||
self.configuration = configuration
|
||||
lock.unlock()
|
||||
}
|
||||
|
||||
nonisolated func process(
|
||||
func process(
|
||||
analysis: VisionPipeline.FaceAnalysis,
|
||||
pixelBuffer: CVPixelBuffer
|
||||
) -> EyeTrackingProcessingResult {
|
||||
@@ -75,7 +75,7 @@ final class GazeDetector: @unchecked Sendable {
|
||||
config = configuration
|
||||
lock.unlock()
|
||||
|
||||
guard analysis.faceDetected, let face = analysis.face else {
|
||||
guard analysis.faceDetected, let face = analysis.face?.value else {
|
||||
return EyeTrackingProcessingResult(
|
||||
faceDetected: false,
|
||||
isEyesClosed: false,
|
||||
|
||||
@@ -18,7 +18,7 @@ import Accelerate
|
||||
import CoreImage
|
||||
import ImageIO
|
||||
import UniformTypeIdentifiers
|
||||
import Vision
|
||||
@preconcurrency import Vision
|
||||
|
||||
struct PupilPosition: Equatable, Sendable {
|
||||
let x: CGFloat
|
||||
|
||||
@@ -6,17 +6,21 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Vision
|
||||
@preconcurrency import Vision
|
||||
|
||||
final class VisionPipeline: @unchecked Sendable {
|
||||
struct FaceAnalysis: Sendable {
|
||||
let faceDetected: Bool
|
||||
let face: VNFaceObservation?
|
||||
let face: NonSendableFaceObservation?
|
||||
let imageSize: CGSize
|
||||
let debugYaw: Double?
|
||||
let debugPitch: Double?
|
||||
}
|
||||
|
||||
struct NonSendableFaceObservation: @unchecked Sendable {
|
||||
nonisolated(unsafe) let value: VNFaceObservation
|
||||
}
|
||||
|
||||
nonisolated func analyze(
|
||||
pixelBuffer: CVPixelBuffer,
|
||||
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(
|
||||
faceDetected: false,
|
||||
face: nil,
|
||||
@@ -58,7 +62,7 @@ final class VisionPipeline: @unchecked Sendable {
|
||||
|
||||
return FaceAnalysis(
|
||||
faceDetected: true,
|
||||
face: face,
|
||||
face: NonSendableFaceObservation(value: face),
|
||||
imageSize: imageSize,
|
||||
debugYaw: face.yaw?.doubleValue,
|
||||
debugPitch: face.pitch?.doubleValue
|
||||
|
||||
@@ -76,7 +76,10 @@ final class MenuBarGuideOverlayPresenter {
|
||||
private func startCheckTimer() {
|
||||
checkTimer?.invalidate()
|
||||
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
|
||||
onboardingWindowObserver = onboardingWindow.observe(\.frame, options: [.new, .old]) {
|
||||
[weak self] _, _ in
|
||||
self?.checkWindowFrame()
|
||||
guard let self else { return }
|
||||
Task { @MainActor in
|
||||
self.checkWindowFrame()
|
||||
}
|
||||
}
|
||||
|
||||
// Add observer for when the onboarding window is closed
|
||||
@@ -145,7 +151,10 @@ final class MenuBarGuideOverlayPresenter {
|
||||
}
|
||||
|
||||
// Hide the overlay when onboarding window closes
|
||||
self?.hide()
|
||||
guard let self else { return }
|
||||
Task { @MainActor in
|
||||
self.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +54,6 @@ struct LookAwaySetupView: View {
|
||||
|
||||
private func previewLookAway() {
|
||||
guard let screen = NSScreen.main else { return }
|
||||
let sizePercentage = settingsManager.settings.subtleReminderSize.percentage
|
||||
let lookAwayIntervalMinutes = settingsManager.settings.lookAwayIntervalMinutes
|
||||
PreviewWindowHelper.showPreview(on: screen) { dismiss in
|
||||
LookAwayReminderView(countdownSeconds: lookAwayIntervalMinutes * 60, onDismiss: dismiss)
|
||||
|
||||
Reference in New Issue
Block a user