A stream is not user activity to the OS, and controller input does not feed
the HID idle timer on any Apple platform — so a gamepad-only session reliably
idles the panel out from under the user mid-play. A keyboard/mouse capture
session masks the bug because those events are real local HID. The Android
client has held FLAG_KEEP_SCREEN_ON while streaming all along; the Apple
clients held nothing.
DisplaySleepGuard is acquired in beginStreaming and released at the top of
disconnect, so it is scoped to the session — every teardown path (user quit,
sessionEnded, the backgrounded keep-alive timeout) funnels through disconnect.
iOS/iPadOS/tvOS: UIApplication.isIdleTimerDisabled. App-wide and ignored
while backgrounded, which is what the audio-only keep-alive wants.
macOS: ProcessInfo.beginActivity(.userInitiated, .idleDisplaySleepDisabled),
the Foundation wrapper over IOKit power assertions. That defers display
sleep but NOT the screen saver, which runs off the HID idle timer
independently — so a 30 s IOPMAssertionDeclareUserActivity heartbeat runs
alongside it. Intended side effect: an idle-lock that follows the screen
saver is deferred for the session too.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>