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