fix(android): periodic position polling for smooth seek bar
PlayerController only ran the UI-state copy on Media3 Player.Events callbacks (track change, pause/play, buffer state) - none of which fire on normal position advancement during playback. The seek bar appeared to update every 20-30 seconds, whenever a buffer event happened to fire, instead of ticking smoothly. Adds a startPositionPolling() coroutine launched on Dispatchers.Main.immediate after the controller is connected. Samples controller.currentPosition + bufferedPosition every 500ms while isPlaying; skips state updates while paused (positionMs stays at the last value, which is the correct paused-state behavior). The poll patches only the position fields via .copy() so stale event-driven snapshots can't clobber the latest state. Cadence reference: Flutter via just_audio positionStream runs at ~200ms (audio_handler.dart:75); Spotify / Apple Music sit around 250-500ms in-app. 500ms is the battery-friendly middle ground that still reads as smooth on a slider. Lock-screen / notification surfaces are driven by Media3's MediaSession separately and don't need this poll.
This commit is contained in:
@@ -13,12 +13,15 @@ import com.fabledsword.minstrel.models.TrackRef
|
||||
import com.fabledsword.minstrel.shared.resolveServerUrl
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import javax.inject.Inject
|
||||
@@ -193,6 +196,7 @@ class PlayerController @Inject constructor(
|
||||
return
|
||||
}
|
||||
mediaController = controller
|
||||
startPositionPolling(controller)
|
||||
controller.addListener(
|
||||
object : Player.Listener {
|
||||
override fun onPlayerError(error: androidx.media3.common.PlaybackException) {
|
||||
@@ -235,6 +239,33 @@ class PlayerController @Inject constructor(
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Periodic `currentPosition` / `bufferedPosition` poller. Media3's
|
||||
* `Player.Events` callbacks only fire at boundaries (track change,
|
||||
* pause/play, buffer state), never on normal position advancement,
|
||||
* so the seek bar would otherwise sit static between events. Polls
|
||||
* at [POSITION_POLL_INTERVAL_MS]; matches the in-app cadence of
|
||||
* Spotify / Apple Music / Flutter (just_audio `positionStream` runs
|
||||
* at ~200ms; this 500ms is a battery-friendly middle ground that
|
||||
* still reads as smooth on a slider).
|
||||
*
|
||||
* The poll only patches position fields via `.copy()` so a stale
|
||||
* `onEvents` snapshot can't clobber the latest state. MediaController
|
||||
* is touched only from the main dispatcher.
|
||||
*/
|
||||
private fun startPositionPolling(controller: MediaController) {
|
||||
scope.launch(Dispatchers.Main.immediate) {
|
||||
while (isActive) {
|
||||
delay(POSITION_POLL_INTERVAL_MS)
|
||||
if (!controller.isPlaying) continue
|
||||
uiStateInternal.value = uiStateInternal.value.copy(
|
||||
positionMs = controller.currentPosition.coerceAtLeast(0),
|
||||
bufferedPositionMs = controller.bufferedPosition.coerceAtLeast(0),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bridges Media3's `ListenableFuture<MediaController>.buildAsync()`
|
||||
* to a suspend function without pulling in `kotlinx-coroutines-guava`
|
||||
@@ -296,3 +327,10 @@ class PlayerController @Inject constructor(
|
||||
const val MINSTREL_SOURCE_KEY: String = "minstrel_source"
|
||||
}
|
||||
}
|
||||
|
||||
// 500ms: smooth-enough on a slider without being battery-hostile.
|
||||
// Flutter (just_audio positionStream) ticks at ~200ms; Spotify / Apple
|
||||
// Music sit around 250-500ms in-app. Lock-screen / notification
|
||||
// surfaces are driven by Media3's MediaSession callbacks separately;
|
||||
// they don't need this poll.
|
||||
private const val POSITION_POLL_INTERVAL_MS = 500L
|
||||
|
||||
Reference in New Issue
Block a user