diff --git a/electron/native-bridge/cursor/recording/macNativeCursorRecordingSession.ts b/electron/native-bridge/cursor/recording/macNativeCursorRecordingSession.ts index 7c799b9f..5e09e929 100644 --- a/electron/native-bridge/cursor/recording/macNativeCursorRecordingSession.ts +++ b/electron/native-bridge/cursor/recording/macNativeCursorRecordingSession.ts @@ -210,7 +210,8 @@ export class MacNativeCursorRecordingSession implements CursorRecordingSession { try { systemPreferences.isTrustedAccessibilityClient(true); } catch { - // Link cursor detection degrades to arrow when Accessibility is unavailable. + // Without Accessibility, text/pointer affordance detection is unavailable; + // cursor bitmaps are still captured natively via NSCursor. } const helperPath = findMacCursorHelperPath(); @@ -333,7 +334,7 @@ export class MacNativeCursorRecordingSession implements CursorRecordingSession { if (payload.type === "ready") { if (payload.accessibilityTrusted === false) { console.warn( - "[cursor-macos] Accessibility is not trusted; cursor shape detection will be arrow-only.", + "[cursor-macos] Accessibility is not trusted; text/pointer affordance detection disabled (bitmap capture still active).", ); } this.resolveReady(); diff --git a/electron/native/screencapturekit/Sources/OpenScreenMacOSCursorHelper/main.swift b/electron/native/screencapturekit/Sources/OpenScreenMacOSCursorHelper/main.swift index 0daaf2b5..ace6827c 100644 --- a/electron/native/screencapturekit/Sources/OpenScreenMacOSCursorHelper/main.swift +++ b/electron/native/screencapturekit/Sources/OpenScreenMacOSCursorHelper/main.swift @@ -314,36 +314,39 @@ emit([ "mouseTapReady": mouseTapReady, ]) -var lastEmittedAssetId: String? +// Process-wide set so each unique cursor shape is serialised at most once, +// even if the user alternates between shapes (e.g. arrow → text → arrow). +var emittedAssetIds = Set() while true { - mouseTracker.pump() - let mouseEvents = mouseTracker.consume() - let asset = currentCursorAsset() - // Only ship the (large) base64 payload the first time a cursor shape is seen; - // subsequent samples reference it by assetId so stdout stays small. - var assetPayload: [String: Any]? - if let asset, asset.id != lastEmittedAssetId { - lastEmittedAssetId = asset.id - assetPayload = [ - "id": asset.id, - "imageDataUrl": asset.imageDataUrl, - "width": asset.width, - "height": asset.height, - "hotspotX": asset.hotspotX, - "hotspotY": asset.hotspotY, - "scaleFactor": asset.scaleFactor, - ] + autoreleasepool { + mouseTracker.pump() + let mouseEvents = mouseTracker.consume() + let asset = currentCursorAsset() + // Only ship the (large) base64 payload the first time a cursor shape is seen; + // subsequent samples reference it by assetId so stdout stays small. + var assetPayload: [String: Any]? + if let asset, emittedAssetIds.insert(asset.id).inserted { + assetPayload = [ + "id": asset.id, + "imageDataUrl": asset.imageDataUrl, + "width": asset.width, + "height": asset.height, + "hotspotX": asset.hotspotX, + "hotspotY": asset.hotspotY, + "scaleFactor": asset.scaleFactor, + ] + } + emit([ + "type": "sample", + "timestampMs": timestampMs(), + "cursorType": currentCursorType(), + "assetId": asset?.id, + "asset": assetPayload, + "leftButtonDown": leftButtonDown(), + "leftButtonPressed": mouseEvents.leftDownCount > 0, + "leftButtonReleased": mouseEvents.leftUpCount > 0, + ]) + Thread.sleep(forTimeInterval: Double(intervalMs) / 1000.0) } - emit([ - "type": "sample", - "timestampMs": timestampMs(), - "cursorType": currentCursorType(), - "assetId": asset?.id, - "asset": assetPayload, - "leftButtonDown": leftButtonDown(), - "leftButtonPressed": mouseEvents.leftDownCount > 0, - "leftButtonReleased": mouseEvents.leftUpCount > 0, - ]) - Thread.sleep(forTimeInterval: Double(intervalMs) / 1000.0) }