building
This commit is contained in:
@@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,7 +174,6 @@ 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,
|
||||||
@@ -184,10 +183,7 @@ extension EyeTrackingService: CameraSessionDelegate {
|
|||||||
faceWidthRatio: faceWidth
|
faceWidthRatio: faceWidth
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Task { @MainActor [weak self] in
|
|
||||||
guard let self else { return }
|
|
||||||
self.faceDetected = result.faceDetected
|
self.faceDetected = result.faceDetected
|
||||||
self.isEyesClosed = result.isEyesClosed
|
self.isEyesClosed = result.isEyesClosed
|
||||||
self.userLookingAtScreen = result.userLookingAtScreen
|
self.userLookingAtScreen = result.userLookingAtScreen
|
||||||
@@ -197,7 +193,6 @@ extension EyeTrackingService: CameraSessionDelegate {
|
|||||||
self.updateGazeConfiguration()
|
self.updateGazeConfiguration()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Error Handling
|
// MARK: - Error Handling
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user