Merge pull request 'fix(android): hold WiFi+wake lock during UPnP cast (locked-screen poll starvation)' (#97) from dev into main
This commit was merged in pull request #97.
This commit is contained in:
@@ -0,0 +1,77 @@
|
|||||||
|
package com.fabledsword.minstrel.player
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.wifi.WifiManager
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.PowerManager
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holds a WiFi + CPU wake lock for the lifetime of an active UPnP cast.
|
||||||
|
*
|
||||||
|
* While casting, the wrapped ExoPlayer is paused, so its automatic
|
||||||
|
* `WAKE_MODE_NETWORK` locks are released and nothing keeps the phone's radio
|
||||||
|
* awake. On a locked, on-battery phone the WiFi enters power-save within
|
||||||
|
* seconds and the CPU dozes; that stalls the 1 Hz liveness poll to the
|
||||||
|
* renderer and the queue-extend calls to the server. The poll then trips the
|
||||||
|
* drop threshold and playback falls back to a phone that *also* has no
|
||||||
|
* network — silence, while the renderer was streaming fine the whole time.
|
||||||
|
*
|
||||||
|
* A high-performance / low-latency [WifiManager.WifiLock] keeps the radio out
|
||||||
|
* of power-save, and a partial [PowerManager.WakeLock] keeps the poll
|
||||||
|
* coroutine scheduled. Both are acquired when a route goes active and released
|
||||||
|
* the moment it drops or the user switches back to the phone.
|
||||||
|
*
|
||||||
|
* All methods are idempotent (`setReferenceCounted(false)` + held-checks) and
|
||||||
|
* synchronized, so they are safe to call from any thread.
|
||||||
|
*/
|
||||||
|
class CastNetworkLock(context: Context) {
|
||||||
|
|
||||||
|
private val appContext = context.applicationContext
|
||||||
|
|
||||||
|
private val wifiLock: WifiManager.WifiLock? =
|
||||||
|
(appContext.getSystemService(Context.WIFI_SERVICE) as? WifiManager)
|
||||||
|
?.createWifiLock(wifiLockMode(), LOCK_TAG)
|
||||||
|
?.apply { setReferenceCounted(false) }
|
||||||
|
|
||||||
|
private val wakeLock: PowerManager.WakeLock? =
|
||||||
|
(appContext.getSystemService(Context.POWER_SERVICE) as? PowerManager)
|
||||||
|
?.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, LOCK_TAG)
|
||||||
|
?.apply { setReferenceCounted(false) }
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun acquire() {
|
||||||
|
runCatching { wifiLock?.takeUnless { it.isHeld }?.acquire() }
|
||||||
|
.onFailure { Timber.w(it, "CastNetworkLock: wifi acquire failed") }
|
||||||
|
runCatching { wakeLock?.takeUnless { it.isHeld }?.acquire() }
|
||||||
|
.onFailure { Timber.w(it, "CastNetworkLock: wake acquire failed") }
|
||||||
|
Timber.w(
|
||||||
|
"CastNetworkLock acquired (wifi=%s wake=%s)",
|
||||||
|
wifiLock?.isHeld, wakeLock?.isHeld,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun release() {
|
||||||
|
runCatching { wifiLock?.takeIf { it.isHeld }?.release() }
|
||||||
|
.onFailure { Timber.w(it, "CastNetworkLock: wifi release failed") }
|
||||||
|
runCatching { wakeLock?.takeIf { it.isHeld }?.release() }
|
||||||
|
.onFailure { Timber.w(it, "CastNetworkLock: wake release failed") }
|
||||||
|
Timber.w("CastNetworkLock released")
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
const val LOCK_TAG = "minstrel:upnp-cast"
|
||||||
|
|
||||||
|
// WIFI_MODE_FULL_LOW_LATENCY (API 29+) both disables power-save and
|
||||||
|
// lowers latency; FULL_HIGH_PERF is the pre-Q equivalent for keeping
|
||||||
|
// the radio fully awake.
|
||||||
|
fun wifiLockMode(): Int =
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
WifiManager.WIFI_MODE_FULL_LOW_LATENCY
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
WifiManager.WIFI_MODE_FULL_HIGH_PERF
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -63,6 +63,7 @@ class MinstrelForwardingPlayer(
|
|||||||
private val delegate: Player,
|
private val delegate: Player,
|
||||||
private val holder: ActiveUpnpHolder,
|
private val holder: ActiveUpnpHolder,
|
||||||
private val remoteState: RemotePlayerState,
|
private val remoteState: RemotePlayerState,
|
||||||
|
private val castNetworkLock: CastNetworkLock,
|
||||||
private val onDrop: (routeName: String) -> Unit,
|
private val onDrop: (routeName: String) -> Unit,
|
||||||
) : ForwardingPlayer(delegate) {
|
) : ForwardingPlayer(delegate) {
|
||||||
|
|
||||||
@@ -438,6 +439,11 @@ class MinstrelForwardingPlayer(
|
|||||||
lastNotifiedTrackIdx = -1
|
lastNotifiedTrackIdx = -1
|
||||||
if (active != null) {
|
if (active != null) {
|
||||||
Timber.w("UPnP active: %s -- pollLoop starting", active.routeName)
|
Timber.w("UPnP active: %s -- pollLoop starting", active.routeName)
|
||||||
|
// Hold the WiFi + CPU awake for the cast: the wrapped ExoPlayer is
|
||||||
|
// about to be paused (releasing its WAKE_MODE_NETWORK locks), so
|
||||||
|
// without this the radio power-saves on a locked screen and the
|
||||||
|
// poll below starves -- see [CastNetworkLock].
|
||||||
|
castNetworkLock.acquire()
|
||||||
// Pause the wrapped ExoPlayer so we are not playing local audio
|
// Pause the wrapped ExoPlayer so we are not playing local audio
|
||||||
// simultaneously with the remote renderer. handler.post targets the
|
// simultaneously with the remote renderer. handler.post targets the
|
||||||
// application looper, so this runs on the same thread that processes
|
// application looper, so this runs on the same thread that processes
|
||||||
@@ -447,6 +453,7 @@ class MinstrelForwardingPlayer(
|
|||||||
handler.post { delegate.pause() }
|
handler.post { delegate.pause() }
|
||||||
pollJob = scope.launch { pollLoop(active) }
|
pollJob = scope.launch { pollLoop(active) }
|
||||||
} else {
|
} else {
|
||||||
|
castNetworkLock.release()
|
||||||
remoteState.reset()
|
remoteState.reset()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ class PlayerFactory @Inject constructor(
|
|||||||
delegate = exo,
|
delegate = exo,
|
||||||
holder = activeUpnpHolder,
|
holder = activeUpnpHolder,
|
||||||
remoteState = remoteState,
|
remoteState = remoteState,
|
||||||
|
castNetworkLock = CastNetworkLock(context),
|
||||||
onDrop = { name -> emitDrop(name) },
|
onDrop = { name -> emitDrop(name) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user