fix(android): force poll on ON_RESUME + cap interpolation drift at 5s
android / Build + lint + test (push) Has been cancelled
android / Build + lint + test (push) Has been cancelled
This commit is contained in:
+36
-2
@@ -4,6 +4,9 @@ package com.fabledsword.minstrel.player
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.SystemClock
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import androidx.media3.common.ForwardingPlayer
|
||||
import androidx.media3.common.Player
|
||||
import com.fabledsword.minstrel.player.output.ActiveUpnp
|
||||
@@ -14,9 +17,12 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.selects.onTimeout
|
||||
import kotlinx.coroutines.selects.select
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
@@ -67,10 +73,29 @@ class MinstrelForwardingPlayer(
|
||||
// visibly jumps backwards immediately after a drag, then forwards again.
|
||||
@Volatile private var lastSeekIssuedAtMs: Long = 0L
|
||||
|
||||
// Wake channel for the poll loop. requestImmediatePoll() trySend's a Unit;
|
||||
// pollLoop's select{} races the delay against this channel so the next
|
||||
// pollOnce can fire immediately instead of waiting up to POLL_INTERVAL_MS.
|
||||
// Used on activity resume (ProcessLifecycleOwner.ON_RESUME) so the UI
|
||||
// catches up to Sonos within RTT rather than the full poll cadence.
|
||||
// CONFLATED so repeated trySend's between polls don't queue up.
|
||||
private val pollTrigger = Channel<Unit>(Channel.CONFLATED)
|
||||
|
||||
private val lifecycleObserver = object : DefaultLifecycleObserver {
|
||||
override fun onResume(owner: LifecycleOwner) {
|
||||
pollTrigger.trySend(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
scope.launch {
|
||||
holder.active.collect { active -> onActiveChanged(active) }
|
||||
}
|
||||
// Process lifecycle is observed on the main thread; ProcessLifecycleOwner's
|
||||
// addObserver requires it. The observer just trySend's to the channel.
|
||||
handler.post {
|
||||
ProcessLifecycleOwner.get().lifecycle.addObserver(lifecycleObserver)
|
||||
}
|
||||
}
|
||||
|
||||
private fun isRemote(): Boolean = holder.active.value != null
|
||||
@@ -238,6 +263,9 @@ class MinstrelForwardingPlayer(
|
||||
override fun release() {
|
||||
pollJob?.cancel()
|
||||
scope.cancel()
|
||||
handler.post {
|
||||
ProcessLifecycleOwner.get().lifecycle.removeObserver(lifecycleObserver)
|
||||
}
|
||||
super.release()
|
||||
}
|
||||
|
||||
@@ -266,6 +294,7 @@ class MinstrelForwardingPlayer(
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class) // onTimeout / select.onReceive
|
||||
private suspend fun pollLoop(active: ActiveUpnp) {
|
||||
while (scope.isActive && holder.active.value?.routeId == active.routeId) {
|
||||
val outcome = runCatching { pollOnce(active) }
|
||||
@@ -276,7 +305,12 @@ class MinstrelForwardingPlayer(
|
||||
handler.post { onDrop(active.routeName) }
|
||||
return
|
||||
}
|
||||
delay(POLL_INTERVAL_MS)
|
||||
// Race the normal cadence against any external wake (activity
|
||||
// resume). Whichever wins continues to the next pollOnce.
|
||||
select<Unit> {
|
||||
onTimeout(POLL_INTERVAL_MS) {}
|
||||
pollTrigger.onReceive {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -561,11 +561,13 @@ class PlayerController @Inject constructor(
|
||||
positionAnchorMs = raw
|
||||
positionAnchorAtRealtimeMs = now
|
||||
}
|
||||
return if (isPlaying) {
|
||||
positionAnchorMs + (now - positionAnchorAtRealtimeMs)
|
||||
} else {
|
||||
positionAnchorMs
|
||||
}
|
||||
if (!isPlaying) return positionAnchorMs
|
||||
// Cap how far past the last anchor we extrapolate. After
|
||||
// MAX_INTERPOLATION_DRIFT_MS without a poll update, freeze the
|
||||
// displayed position at anchor + cap rather than projecting wildly.
|
||||
// The next successful poll re-anchors and motion resumes.
|
||||
val delta = (now - positionAnchorAtRealtimeMs).coerceAtMost(MAX_INTERPOLATION_DRIFT_MS)
|
||||
return positionAnchorMs + delta
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -647,6 +649,7 @@ class PlayerController @Inject constructor(
|
||||
companion object {
|
||||
const val MINSTREL_SOURCE_KEY: String = "minstrel_source"
|
||||
private const val MS_PER_SECOND = 1_000L
|
||||
private const val MAX_INTERPOLATION_DRIFT_MS = 5_000L
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user