fix: restore tablet cursor after display sleep/wake - #43
Conversation
After the display idle-sleeps and wakes, ScreenCaptureKit delivers no frames for cursor-only movement over the static virtual display, so the 0.6.8 idle keepalive re-sends a stale, cursor-less frame indefinitely and the stall-based restart never fires. The tablet shows a live picture with no cursor until a manual restart. - Hold kIOPMAssertionTypePreventUserIdleDisplaySleep while streaming so the display (and the virtual display) never idle-sleeps. - On NSWorkspace.screensDidWake/didWake, rebuild the SCStream via hardRestart(reason:), guarded by an isStreaming flag + streamGeneration token so a wake restart cannot resurrect a stopped stream; debounced 2s. - Pass kCGDisplayStreamShowCursor to the CGDisplayStream fallback (was nil). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
@lhysilicon is attempting to deploy a commit to the tranvuongquocdat's projects Team on Vercel. A member of the Team first needs to authorize it. |
- kCGDisplayStreamShowCursor was obsoleted in the 26.x SDK; use the renamed CGDisplayStream.showCursor so the CGDisplayStream cursor fallback compiles. - restartStream() lacked the isStreaming + streamGeneration guard that hardRestart() has, so a stale stall-monitor restart could resurrect capture after stopStreaming(). Add the same generation-token guard at each async continuation point. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…activation - A Finder/open-launched app has no ~/.local/bin in PATH, so setupADBReverse() and adb probing failed to find adb and USB mode never tunnelled. Resolve adb via a single StatusDetector.adbExecutablePath() that also searches ~/.local/bin, Homebrew, /usr/local/bin and the Android SDK; reuse it in AppDelegate instead of a duplicate inline search. - Screen Recording grant was not re-checked after the user granted it, so Start stayed disabled until relaunch. Re-check passively on applicationDidBecomeActive (no system prompt); only the explicit permission button requests access. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Is this similar to #42? |
- Touch input needs Accessibility to inject CGEvents; the app never prompted for it (promptAccessibilityPermission had no caller), so enabling touch silently did nothing. Prompt for Accessibility when touch is enabled (launch + toggle) and reflect the grant in the UI. - Connection status showed a green 'Listening' even when stopped (it keyed off IP presence, not listener state). Show real states: Server stopped / Waiting for tablet / connected. - 'Gaming Boost' claimed '1000 Mbps' and '120 Hz' in the UI but the encoder forces 50 Mbps. Rename to 'Low-Latency Mode' and state honestly (50 Mbps target, speed prioritized, quality may drop). Normal mode now respects the user's chosen bitrate (dropped the hidden max(bitrate,60) floor). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Low-Latency Mode (ex-Gaming Boost): finish honest reporting — effectiveBitrate and the settings notices now reflect the real 50 Mbps target instead of 1000; refresh rate is the user's pick (not a forced 120); diagnostics/labels no longer say 'Gaming Boost'. - adb probe: single-flight + per-command timeout + generation check so slow/hung adb can't pile up overlapping detached probes; skip adb entirely in Wireless mode; cache the not-found result under the same TTL. - UI polish: real VoiceOver labels; no QR while the LAN address is empty (was 0.0.0.0), rebuild on change; Reset uses one shared Defaults source so a reset no longer flips 60Hz->120Hz; bitrate slider commits once on drag-end instead of rebuilding the encoder every 10 Mbps. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Stop previously drained frameQueue/receiveQueue synchronously on the main thread, freezing the settings window under backlog. Move teardown to a background queue with a lifecycle generation token; the UI drops to a 'stopping' state immediately and mode/resolution auto-restart happens once, after teardown. Compiles + no streaming/start regression; Stop-path smoothness not yet driven-tested via UI. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Port used TextField(value:format:) which only commits on Return/focus-loss, so clicking Start could read a stale port; port 0 was not rejected. Use a local string draft parsed live to 1...65535, commit before Start, disable Start when invalid. - adb status collapsed missing/unauthorized/offline/multiple-devices/command-error into one 'Not detected'. Return a small ADBStatus enum (parsed from stderr + exit code) and show the matching actionable hint in the status row. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Yes, it overlaps #42 a bit — the sleep-prevention part here is unique though, and it looks good. This PR just covers a lot of ground beyond the cursor fix, so would you mind splitting it up? Keep this one focused on the sleep/wake fix and open the rest separately — much easier for me to review and merge quickly that way. |
…rd stream restarts Extracted from PR #43 (adapted onto the #42 wake-restart path): - Hold kIOPMAssertionTypePreventUserIdleDisplaySleep for the whole streaming session so the virtual display never idle-sleeps — the sleep/wake transition is what strands the cursor in the first place. The #42 wake observers remain as the recovery path for what the assertion cannot prevent (manual/forced sleep, lid close, display reconnects). Released in stopStreaming. - isStreaming flag + streamGeneration token: every restart is invalidated by a stop or a newer restart at each await point, so the 2s-delayed wake restart (or a stalled-monitor restart) can never resurrect capture after the user hits Stop.
|
Update: I've pulled the two unique pieces of this branch into main — the display-sleep assertion and the generation-guarded restarts — with the commit credited to you as-is. The rest (ADB status overhaul, settings UI work, the Low-Latency change) is still very welcome as separate PRs where each can get a proper look. Thanks for the solid debugging on this one! |
Port the generation-token restart guard from upstream PR tranvuongquocdat#43 without the display-sleep assertion, preserving Ambient's sleep behavior. The design was authored by @lhysilicon with Claude and integrated into upstream main by @tranvuongquocdat in d7d334d. Co-authored-by: lhysilicon <247304446+lhysilicon@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Problem
When the Mac's display idle-sleeps (
pmset displaysleep) and then wakes, the tablet keeps showing a live picture but the mouse cursor is gone — and stays gone until the stream is manually restarted.Root cause
SCStreamConfiguration.showsCursor = truelets ScreenCaptureKit composite the cursor into captured frames. But cursor-only movement over an idle virtual display does not dirty the surface, so ScreenCaptureKit delivers no new frames. The idle keepalive added in 0.6.8 then re-sends the last cached buffer indefinitely and resetslastFrameTime, so the stall-based restart instartFrameMonitornever fires — the tablet is frozen on a stale, cursor-less frame. There was no display sleep/wake handling, so nothing rebuilt the capture.Confirmed empirically: with a static desktop, sweeping the real cursor across the virtual display's global coordinates produced zero new SCStream frames (the host stayed at the ~1 fps keepalive the whole time).
Fix
kIOPMAssertionTypePreventUserIdleDisplaySleepwhile streaming so the display (hence the virtual display) never idle-sleeps. Released instopStreaming.NSWorkspace.screensDidWakeNotification/didWakeNotification, rebuild the SCStream via a newScreenCapture.hardRestart(reason:). Guarded by anisStreamingflag + astreamGenerationtoken so a wake restart can never resurrect a stream that was stopped mid-flight, and debounced 2 s (both notifications can fire on a full-system wake).CGDisplayStreamfallback was created withproperties: nil, which omits the cursor; it now passes[kCGDisplayStreamShowCursor: true].Two files changed:
MacHost/Sources/ScreenCapture.swift,MacHost/Sources/AppDelegate.swift.Notes / testing
@Statemacro does not expand under CLT, which blocks the link (unrelated to this change). In whole-module compilation the two changed files produced no compiler errors — only the pre-existing SwiftUI/toolchain issue inSettingsWindow.swiftstopped the final binary. Please verify the build on a full-Xcode setup before merging.didChangeScreenParametersNotificationwas intentionally not hooked: it fires on benign arrangement/resolution changes and on every virtual-display create/destroy, which would cause spurious restarts;screensDidWakeis the precise signal for this bug.🤖 Generated with Claude Code