From 97566b754b32bf52fc248ce225e0584eb41e8bef Mon Sep 17 00:00:00 2001 From: Hariharan Mudaliar Date: Sat, 4 Oct 2025 19:23:51 +0530 Subject: [PATCH] Working: Optimized - No polling --- DynamicIsland/DynamicIslandApp.swift | 3 + .../components/Settings/SettingsView.swift | 2 + .../managers/ScreenRecordingManager.swift | 232 +++++++----------- DynamicIsland/models/Constants.swift | 2 + 4 files changed, 91 insertions(+), 148 deletions(-) diff --git a/DynamicIsland/DynamicIslandApp.swift b/DynamicIsland/DynamicIslandApp.swift index ad27d3c..9660c89 100644 --- a/DynamicIsland/DynamicIslandApp.swift +++ b/DynamicIsland/DynamicIslandApp.swift @@ -252,6 +252,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { window.orderFrontRegardless() NotchSpaceManager.shared.notchSpace.windows.insert(window) + return window } @@ -430,6 +431,8 @@ class AppDelegate: NSObject, NSApplicationDelegate { ScreenRecordingManager.shared.stopMonitoring() } }.store(in: &cancellables) + + // Note: Polling setting removed - now uses event-driven private API detection only NotificationCenter.default.addObserver( self, diff --git a/DynamicIsland/components/Settings/SettingsView.swift b/DynamicIsland/components/Settings/SettingsView.swift index 518b32c..524612e 100644 --- a/DynamicIsland/components/Settings/SettingsView.swift +++ b/DynamicIsland/components/Settings/SettingsView.swift @@ -202,6 +202,8 @@ struct GeneralSettings: View { Defaults.Toggle("Show Recording Indicator", key: .showRecordingIndicator) .disabled(!enableScreenRecordingDetection) + // Note: Polling removed - now uses event-driven private API detection + if recordingManager.isMonitoring { HStack { Text("Detection Status") diff --git a/DynamicIsland/managers/ScreenRecordingManager.swift b/DynamicIsland/managers/ScreenRecordingManager.swift index bac90ba..22b5fb9 100644 --- a/DynamicIsland/managers/ScreenRecordingManager.swift +++ b/DynamicIsland/managers/ScreenRecordingManager.swift @@ -4,14 +4,40 @@ // // Created for screen recording detection feature // Monitors system for active screen recording and provides real-time status updates +// Uses private CoreGraphics APIs for accurate, event-driven screen capture detection import Foundation import Combine -import ScreenCaptureKit import AppKit import Defaults import SwiftUI +// MARK: - Private API Declarations +// These private APIs provide direct screen capture detection +// Use at your own risk - may break in future macOS versions + +@_silgen_name("CGSIsScreenWatcherPresent") +func CGSIsScreenWatcherPresent() -> Bool + +@_silgen_name("CGSRegisterNotifyProc") +func CGSRegisterNotifyProc( + _ callback: (@convention(c) (Int32, Int32, Int32, UnsafeMutableRawPointer?) -> Void)?, + _ event: Int32, + _ context: UnsafeMutableRawPointer? +) -> Bool + +// MARK: - Global Callback Function +// C function pointer cannot capture context, so we need a global function +private func screenCaptureEventCallback(eventType: Int32, _: Int32, _: Int32, context: UnsafeMutableRawPointer?) { + guard let context = context else { return } + let manager = Unmanaged.fromOpaque(context).takeUnretainedValue() + + DispatchQueue.main.async { + print("ScreenRecordingManager: 📢 Screen capture event received (type: \(eventType))") + manager.checkRecordingStatus() + } +} + @MainActor class ScreenRecordingManager: ObservableObject { static let shared = ScreenRecordingManager() @@ -27,27 +53,24 @@ class ScreenRecordingManager: ObservableObject { @Published var lastUpdated: Date = .distantPast // MARK: - Private Properties - private var monitoringTimer: Timer? - private var lastScreensHaveSeparateSpaces: Bool = false private var cancellables = Set() private var recordingStartTime: Date? private var durationTimer: Timer? private var debounceIdleTask: Task? // MARK: - Configuration - private let monitoringInterval: TimeInterval = 1.0 // Check every second - private let debounceDelay: TimeInterval = 0.5 // Debounce rapid changes + private let debounceDelay: TimeInterval = 0.2 // Debounce rapid changes // MARK: - Initialization private init() { - setupInitialState() + // No initial setup needed } deinit { - Task { @MainActor in - stopMonitoring() - debounceIdleTask?.cancel() - } + // Clean up monitoring state + // Note: We can't call async methods in deinit, so we just clean up local state + debounceIdleTask?.cancel() + durationTimer?.invalidate() } // MARK: - Public Methods @@ -60,19 +83,16 @@ class ScreenRecordingManager: ObservableObject { } isMonitoring = true - lastScreensHaveSeparateSpaces = NSScreen.screensHaveSeparateSpaces - print("ScreenRecordingManager: 🟢 Starting monitoring...") - print("ScreenRecordingManager: Initial screensHaveSeparateSpaces = \(lastScreensHaveSeparateSpaces)") + print("ScreenRecordingManager: 🟢 Starting screen capture monitoring (Private API)...") - // Start timer-based monitoring - monitoringTimer = Timer.scheduledTimer(withTimeInterval: monitoringInterval, repeats: true) { [weak self] _ in - Task { @MainActor in - await self?.checkRecordingStatus() - } - } + // Setup event-driven capture detection using private CoreGraphics APIs + setupPrivateAPINotifications() - print("ScreenRecordingManager: ✅ Started monitoring with \(monitoringInterval)s interval") + // Check initial state + checkRecordingStatus() + + print("ScreenRecordingManager: ✅ Started monitoring (event-driven, no polling)") } /// Stop monitoring for screen recording activity @@ -85,8 +105,9 @@ class ScreenRecordingManager: ObservableObject { print("ScreenRecordingManager: 🛑 Stopping monitoring...") isMonitoring = false - monitoringTimer?.invalidate() - monitoringTimer = nil + + // Note: We don't unregister the callback as there's no CGSUnregisterNotifyProc API + // The callback will simply not be processed when isMonitoring is false // Stop duration tracking stopDurationTracking() @@ -111,140 +132,60 @@ class ScreenRecordingManager: ObservableObject { // MARK: - Private Methods - private func setupInitialState() { - lastScreensHaveSeparateSpaces = NSScreen.screensHaveSeparateSpaces + /// Setup private API notifications for screen capture events + private func setupPrivateAPINotifications() { + // Pass self as context to the global callback function + let context = Unmanaged.passUnretained(self).toOpaque() + + // Register for remote session events (screen capture start/stop) + // kCGSessionRemoteConnect - fires when screen sharing/recording starts + let registered1 = CGSRegisterNotifyProc(screenCaptureEventCallback, 1502, context) + + // kCGSessionRemoteDisconnect - fires when screen sharing/recording stops + let registered2 = CGSRegisterNotifyProc(screenCaptureEventCallback, 1503, context) + + if registered1 && registered2 { + print("ScreenRecordingManager: ✅ Private API notifications registered") + } else { + print("ScreenRecordingManager: ⚠️ Failed to register private API notifications") + } } - /// Check current recording status using multiple detection methods - private func checkRecordingStatus() async { - let currentRecordingState = await detectScreenRecording() + /// Check current recording status using private API + func checkRecordingStatus() { + let currentRecordingState = CGSIsScreenWatcherPresent() // Debug: Always log current check print("ScreenRecordingManager: 🔍 Checking... current=\(isRecording), detected=\(currentRecordingState)") // Debounce changes to avoid flickering if currentRecordingState != isRecording { - print("ScreenRecordingManager: 🔄 State change detected (\(isRecording) -> \(currentRecordingState)), debouncing...") + print("ScreenRecordingManager: 🔄 State change detected (\(isRecording) -> \(currentRecordingState))") - // Add a small delay to confirm the state change - try? await Task.sleep(nanoseconds: UInt64(debounceDelay * 1_000_000_000)) - - // Double-check the state after debounce - let confirmedState = await detectScreenRecording() - print("ScreenRecordingManager: 🔍 After debounce: confirmed=\(confirmedState), original=\(currentRecordingState)") - - if confirmedState == currentRecordingState { - if confirmedState && !isRecording { - // Started recording - lastUpdated = Date() - startDurationTracking() - updateIdleState(recording: true) - // Trigger expanding view like music activity - coordinator.toggleExpandingView(status: true, type: .recording) - withAnimation(.smooth) { - isRecording = confirmedState - } - } else if !confirmedState && isRecording { - // Stopped recording - let expanding view auto-collapse naturally (like music) - lastUpdated = Date() - stopDurationTracking() - updateIdleState(recording: false) - withAnimation(.smooth) { - isRecording = confirmedState - } - } else { - isRecording = confirmedState + if currentRecordingState && !isRecording { + // Started recording + lastUpdated = Date() + startDurationTracking() + updateIdleState(recording: true) + // Trigger expanding view like music activity + coordinator.toggleExpandingView(status: true, type: .recording) + withAnimation(.smooth) { + isRecording = currentRecordingState } - - print("ScreenRecordingManager: ✅ Recording state changed to \(confirmedState)") - } else { - print("ScreenRecordingManager: ❌ State not confirmed, keeping \(isRecording)") + print("ScreenRecordingManager: 🔴 Screen recording STARTED") + } else if !currentRecordingState && isRecording { + // Stopped recording - let expanding view auto-collapse naturally (like music) + lastUpdated = Date() + stopDurationTracking() + updateIdleState(recording: false) + withAnimation(.smooth) { + isRecording = currentRecordingState + } + print("ScreenRecordingManager: ⚪ Screen recording STOPPED") } } } - /// Detect screen recording using hybrid approach - private func detectScreenRecording() async -> Bool { - print("ScreenRecordingManager: 🔎 Starting detection...") - - // Method 1: NSScreen.screensHaveSeparateSpaces detection - let screensHaveSeparateSpaces = NSScreen.screensHaveSeparateSpaces - let screenSpacesChanged = screensHaveSeparateSpaces != lastScreensHaveSeparateSpaces - - print("ScreenRecordingManager: Method 1 - screensHaveSeparateSpaces: \(screensHaveSeparateSpaces), last: \(lastScreensHaveSeparateSpaces), changed: \(screenSpacesChanged)") - - // Update last known state - lastScreensHaveSeparateSpaces = screensHaveSeparateSpaces - - // Method 2: ScreenCaptureKit detection (if available) - let screencaptureKitDetection = await detectWithScreenCaptureKit() - print("ScreenRecordingManager: Method 2 - ScreenCaptureKit: \(screencaptureKitDetection)") - - // Method 3: Process-based detection for common recording apps - let processBasedDetection = detectRecordingProcesses() - print("ScreenRecordingManager: Method 3 - Process detection: \(processBasedDetection)") - - // Combine detection methods (any positive result indicates recording) - let isCurrentlyRecording = screenSpacesChanged || screencaptureKitDetection || processBasedDetection - - print("ScreenRecordingManager: 🎯 Final result: \(isCurrentlyRecording) (spaces:\(screenSpacesChanged) || kit:\(screencaptureKitDetection) || process:\(processBasedDetection))") - - return isCurrentlyRecording - } - - /// Detect recording using ScreenCaptureKit (requires macOS 12.3+) - private func detectWithScreenCaptureKit() async -> Bool { - guard #available(macOS 12.3, *) else { return false } - - do { - // Check if there are any active capture sessions - let content = try await SCShareableContent.excludingDesktopWindows(false, onScreenWindowsOnly: true) - - // If we can successfully get content and there are running applications with capture capabilities, - // it might indicate active screen capture - let runningApps = content.applications.filter { $0.applicationName.lowercased().contains("screen") || - $0.applicationName.lowercased().contains("record") || - $0.applicationName.lowercased().contains("capture") } - - return !runningApps.isEmpty - } catch { - // If ScreenCaptureKit fails, fall back to other detection methods - return false - } - } - - /// Detect recording by checking for known recording processes - private func detectRecordingProcesses() -> Bool { - let recordingProcessNames = [ - "QuickTime Player", - "ScreenSearch", - "OBS", - "Camtasia", - "ScreenFlow", - "CleanMyMac", - "Screenshot", - "screencapture" - ] - - let runningApps = NSWorkspace.shared.runningApplications - - print("ScreenRecordingManager: Checking \(runningApps.count) running apps for recording processes...") - - for app in runningApps { - if let appName = app.localizedName { - for processName in recordingProcessNames { - if appName.lowercased().contains(processName.lowercased()) { - print("ScreenRecordingManager: 🎬 Found recording app: \(appName)") - return true - } - } - } - } - - print("ScreenRecordingManager: No recording processes found") - return false - } - /// Start tracking recording duration private func startDurationTracking() { recordingStartTime = Date() @@ -311,12 +252,7 @@ extension ScreenRecordingManager { /// Check if monitoring is available (for settings UI) var isMonitoringAvailable: Bool { - return true // Basic monitoring is always available - } - - /// Get current monitoring interval for debugging - var currentMonitoringInterval: TimeInterval { - return monitoringInterval + return true // Window-based monitoring is always available } /// Get formatted recording duration string diff --git a/DynamicIsland/models/Constants.swift b/DynamicIsland/models/Constants.swift index b4481f1..57d060b 100644 --- a/DynamicIsland/models/Constants.swift +++ b/DynamicIsland/models/Constants.swift @@ -346,6 +346,8 @@ extension Defaults.Keys { // MARK: Screen Recording Detection Feature static let enableScreenRecordingDetection = Key("enableScreenRecordingDetection", default: true) static let showRecordingIndicator = Key("showRecordingIndicator", default: true) + // Polling removed - now uses event-driven private API detection (CGSIsScreenWatcherPresent) + // static let enableScreenRecordingPolling = Key("enableScreenRecordingPolling", default: false) // Helper to determine the default media controller based on macOS version static var defaultMediaController: MediaControllerType {