Casting to a UPnP renderer (Sonos) with the screen locked on battery would, after ~12 seconds, drop the renderer and fall back to a silent phone — while the Sonos was streaming fine the whole time.
Confirmed from logcat: shortly after screen-off the phone logged Unable to resolve host "minstrel.fabledsword.com" (its own DNS dying, not the server), the queue-extend aborted (1/58 appended), the 1 Hz poll then tripped DROP_THRESHOLD, and the local fallback came up active=null (no network either → silence).
Root cause
While casting, the wrapped ExoPlayer is paused, which releases its automatic WAKE_MODE_NETWORK WiFi/wake locks. Nothing else held the radio awake, so a locked, on-battery phone power-saved the WiFi within seconds and dozed the CPU — starving both the liveness poll to the renderer and the extend calls to the server. (USB charging masks this: no Doze while charging, which is why it only bit on battery.)
Fix
New CastNetworkLock: a high-perf/low-latency WifiManager.WifiLock + partial PowerManager.WakeLock, acquired when a UPnP route goes active and released on every teardown path (all funnel through holder.set(null) → onActiveChanged(null)). Idempotent + synchronized. WAKE_LOCK permission already declared; minSdk 26 handled via WIFI_MODE_FULL_LOW_LATENCY (API 29+) / FULL_HIGH_PERF fallback.
Testing
Needs a real device test on battery (USB masks the bug): install → cast to Sonos → unplug → lock screen → confirm playback survives past ~15s.
CI verified green on dev (run 986: ktlint / detekt / unit tests).
## Problem
Casting to a UPnP renderer (Sonos) with the screen locked **on battery** would, after ~12 seconds, drop the renderer and fall back to a silent phone — while the Sonos was streaming fine the whole time.
Confirmed from logcat: shortly after screen-off the phone logged `Unable to resolve host "minstrel.fabledsword.com"` (its *own* DNS dying, not the server), the queue-extend aborted (1/58 appended), the 1 Hz poll then tripped `DROP_THRESHOLD`, and the local fallback came up `active=null` (no network either → silence).
## Root cause
While casting, the wrapped ExoPlayer is paused, which **releases its automatic `WAKE_MODE_NETWORK` WiFi/wake locks**. Nothing else held the radio awake, so a locked, on-battery phone power-saved the WiFi within seconds and dozed the CPU — starving both the liveness poll to the renderer and the extend calls to the server. (USB charging masks this: no Doze while charging, which is why it only bit on battery.)
## Fix
New `CastNetworkLock`: a high-perf/low-latency `WifiManager.WifiLock` + partial `PowerManager.WakeLock`, acquired when a UPnP route goes active and released on every teardown path (all funnel through `holder.set(null)` → `onActiveChanged(null)`). Idempotent + synchronized. `WAKE_LOCK` permission already declared; minSdk 26 handled via `WIFI_MODE_FULL_LOW_LATENCY` (API 29+) / `FULL_HIGH_PERF` fallback.
## Testing
Needs a real device test **on battery** (USB masks the bug): install → cast to Sonos → unplug → lock screen → confirm playback survives past ~15s.
CI verified green on dev (run 986: ktlint / detekt / unit tests).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
While casting, the wrapped ExoPlayer is paused, releasing its
WAKE_MODE_NETWORK locks -- so nothing kept the phone's radio awake. On a
locked, on-battery phone the WiFi power-saves within seconds and the CPU
dozes, stalling the 1 Hz liveness poll to the renderer and the
queue-extend calls to the server. The poll then trips DROP_THRESHOLD and
playback falls back to a phone that also has no network: silence, while
the Sonos was streaming fine the whole time.
Diagnosed from logcat: ~12s after screen-off the phone logged
"Unable to resolve host minstrel.fabledsword.com" (its own DNS, not the
server), the extend aborted (1/58 appended), then the drop tripped and
the local fallback came up active=null. USB charging masks it (no Doze
while charging), which is why it only bit on battery.
Add CastNetworkLock: a high-perf/low-latency WifiLock + partial WakeLock
acquired when a UPnP route goes active and released on drop/switch-back
(every teardown path funnels through holder.set(null) -> onActiveChanged).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Problem
Casting to a UPnP renderer (Sonos) with the screen locked on battery would, after ~12 seconds, drop the renderer and fall back to a silent phone — while the Sonos was streaming fine the whole time.
Confirmed from logcat: shortly after screen-off the phone logged
Unable to resolve host "minstrel.fabledsword.com"(its own DNS dying, not the server), the queue-extend aborted (1/58 appended), the 1 Hz poll then trippedDROP_THRESHOLD, and the local fallback came upactive=null(no network either → silence).Root cause
While casting, the wrapped ExoPlayer is paused, which releases its automatic
WAKE_MODE_NETWORKWiFi/wake locks. Nothing else held the radio awake, so a locked, on-battery phone power-saved the WiFi within seconds and dozed the CPU — starving both the liveness poll to the renderer and the extend calls to the server. (USB charging masks this: no Doze while charging, which is why it only bit on battery.)Fix
New
CastNetworkLock: a high-perf/low-latencyWifiManager.WifiLock+ partialPowerManager.WakeLock, acquired when a UPnP route goes active and released on every teardown path (all funnel throughholder.set(null)→onActiveChanged(null)). Idempotent + synchronized.WAKE_LOCKpermission already declared; minSdk 26 handled viaWIFI_MODE_FULL_LOW_LATENCY(API 29+) /FULL_HIGH_PERFfallback.Testing
Needs a real device test on battery (USB masks the bug): install → cast to Sonos → unplug → lock screen → confirm playback survives past ~15s.
CI verified green on dev (run 986: ktlint / detekt / unit tests).
🤖 Generated with Claude Code