Skip to content

fix(recording): restore correct display selection after screen unlock#308

Open
pagarsky wants to merge 1 commit into
JerryZLiu:mainfrom
pagarsky:fix/screen-unlock-display-race
Open

fix(recording): restore correct display selection after screen unlock#308
pagarsky wants to merge 1 commit into
JerryZLiu:mainfrom
pagarsky:fix/screen-unlock-display-race

Conversation

@pagarsky

@pagarsky pagarsky commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #277.

On dual-monitor Macs, when an external display sleeps during screen lock, SCShareableContent.displays transiently reports only the built-in display for a couple of seconds after unlock. ScreenRecorder.setupCapture runs inside that window (via resumeRecording(after: 0.5)) and its fallback to content.displays.first selects the built-in, even though the tracker's activeDisplayID still points at the external. Once cachedDisplay = built-in, the tracker's .removeDuplicates() swallows any republish of the same id, so handleActiveDisplayChange and refreshDisplay never re-run — capture stays on the wrong display until the cursor crosses screens for a genuine tracker id change (in the affected user's real workflow, that meant 22 minutes of misclassified capture).

Fix

Three linked changes in ScreenRecorder.swift:

  1. setupCapture: when a preferred display id exists but isn't in the current snapshot, don't fall back to content.displays.first. Persist the wanted id as requestedDisplayID and leave cachedDisplay = nil (captureScreenshot already handles nil).
  2. refreshDisplay: same principle — if the target id isn't in the snapshot, keep cachedDisplay untouched rather than picking a different display.
  3. New observer on NSApplication.didChangeScreenParametersNotification in registerForSleepAndLock() calls refreshDisplay() when screens change. This is the recovery path: when the external monitor finishes waking up, refreshDisplay finds the persisted requestedDisplayID in the fresh snapshot and restores capture to it.

Also removes currentDisplayID != nil from handleActiveDisplayChange's guard, since after (1) .capturing no longer implies a resolved display.

Test plan

  • Builds (Debug, macOS arm64)
  • Maintainer manual repro: dual-monitor setup, work on external, lock (Ctrl+Cmd+Q), wait 60 s so the external sleeps, unlock, keep working. Resulting timeline card should describe external-monitor content; without the fix it labels as whatever was on the built-in.

When an external display sleeps between lock and unlock, SCShareableContent transiently reports only the built-in display. setupCapture, running at unlock+0.5s, would fall back to content.displays.first (built-in) even though the tracker's activeDisplayID pointed to the (currently-invisible) external — silently misclassifying capture as coming from the wrong screen until the tracker happened to publish a different id (which never happens if the cursor doesn't cross screens).

Three linked changes:

1. setupCapture no longer falls back to content.displays.first when a preferred display id exists but isn't in the snapshot. Instead it persists the wanted id as requestedDisplayID and leaves cachedDisplay nil — captureScreenshot already handles that.
2. refreshDisplay likewise keeps cachedDisplay untouched when the target id is missing from the snapshot, rather than falling back to whichever display happens to be first.
3. ScreenRecorder now subscribes to NSApplication.didChangeScreenParametersNotification and calls refreshDisplay when it fires — which is what recovers the deferred selection once the external monitor finishes waking up.

Also removes 'currentDisplayID != nil' from the handleActiveDisplayChange guard, since '.capturing' no longer implies a resolved display.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Dual-monitor: Dayflow records the wrong display, mislabelling work as Personal / Idle / Music

1 participant