feat(android): RemotePlayerState container for UPnP-synthesized player state
android / Build + lint + test (push) Failing after 1m28s
android / Build + lint + test (push) Failing after 1m28s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
package com.fabledsword.minstrel.player
|
||||
|
||||
/**
|
||||
* Synthesized state for the UPnP route -- what the ForwardingPlayer
|
||||
* exposes via Player.getCurrentPosition / isPlaying / etc. when the
|
||||
* remote leg is active. Not a Player; a container.
|
||||
*
|
||||
* Updates flow in from:
|
||||
* - 1Hz GetPositionInfo poll -> applyPositionInfo()
|
||||
* - Transport SOAP calls landing 200 OK -> applyTransport{Playing,Paused,Stopped}()
|
||||
* - Error paths -> applyError() (drop fallback) or recordPollFailure()
|
||||
*
|
||||
* The poll-failure counter implements the rolling-3 drop heuristic: 3
|
||||
* consecutive poll failures = remote considered dropped (returns true
|
||||
* from recordPollFailure for the caller to surface). Success resets it.
|
||||
*/
|
||||
class RemotePlayerState {
|
||||
|
||||
@Volatile var positionMs: Long = 0L; private set
|
||||
@Volatile var durationMs: Long = 0L; private set
|
||||
@Volatile var isPlaying: Boolean = false; private set
|
||||
@Volatile var currentTrackUri: String = ""; private set
|
||||
@Volatile var lastError: Throwable? = null; private set
|
||||
|
||||
private var consecutivePollFailures: Int = 0
|
||||
|
||||
fun applyPositionInfo(positionMs: Long, durationMs: Long, trackUri: String) {
|
||||
this.positionMs = positionMs
|
||||
this.durationMs = durationMs
|
||||
this.currentTrackUri = trackUri
|
||||
}
|
||||
|
||||
fun applyTransportPlaying() { isPlaying = true }
|
||||
fun applyTransportPaused() { isPlaying = false }
|
||||
fun applyTransportStopped() {
|
||||
isPlaying = false
|
||||
positionMs = 0L
|
||||
}
|
||||
|
||||
fun applyError(t: Throwable) {
|
||||
isPlaying = false
|
||||
lastError = t
|
||||
}
|
||||
|
||||
/** Returns true when the rolling threshold trips this call. */
|
||||
fun recordPollFailure(): Boolean {
|
||||
consecutivePollFailures += 1
|
||||
return consecutivePollFailures >= DROP_THRESHOLD
|
||||
}
|
||||
|
||||
fun recordPollSuccess() { consecutivePollFailures = 0 }
|
||||
|
||||
fun reset() {
|
||||
positionMs = 0L
|
||||
durationMs = 0L
|
||||
isPlaying = false
|
||||
currentTrackUri = ""
|
||||
lastError = null
|
||||
consecutivePollFailures = 0
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val DROP_THRESHOLD = 3
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.fabledsword.minstrel.player
|
||||
|
||||
import org.junit.jupiter.api.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class RemotePlayerStateTest {
|
||||
|
||||
@Test
|
||||
fun `starts in idle state`() {
|
||||
val state = RemotePlayerState()
|
||||
assertFalse(state.isPlaying)
|
||||
assertEquals(0L, state.positionMs)
|
||||
assertEquals(0L, state.durationMs)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `applyPositionInfo updates position and duration`() {
|
||||
val state = RemotePlayerState()
|
||||
state.applyPositionInfo(positionMs = 65_000L, durationMs = 210_000L, trackUri = "x")
|
||||
assertEquals(65_000L, state.positionMs)
|
||||
assertEquals(210_000L, state.durationMs)
|
||||
assertEquals("x", state.currentTrackUri)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `applyTransportPlaying flips isPlaying true`() {
|
||||
val state = RemotePlayerState()
|
||||
state.applyTransportPlaying()
|
||||
assertTrue(state.isPlaying)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `applyTransportPaused flips isPlaying false`() {
|
||||
val state = RemotePlayerState().apply { applyTransportPlaying() }
|
||||
state.applyTransportPaused()
|
||||
assertFalse(state.isPlaying)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `applyError resets to idle and records error`() {
|
||||
val state = RemotePlayerState().apply { applyTransportPlaying() }
|
||||
val ex = RuntimeException("disconnected")
|
||||
state.applyError(ex)
|
||||
assertFalse(state.isPlaying)
|
||||
assertEquals(ex, state.lastError)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `recordPollFailure trips after threshold`() {
|
||||
val state = RemotePlayerState()
|
||||
assertFalse(state.recordPollFailure())
|
||||
assertFalse(state.recordPollFailure())
|
||||
assertTrue(state.recordPollFailure())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `recordPollSuccess clears the failure counter`() {
|
||||
val state = RemotePlayerState()
|
||||
state.recordPollFailure()
|
||||
state.recordPollFailure()
|
||||
state.recordPollSuccess()
|
||||
assertFalse(state.recordPollFailure())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user