fix: thread-safe player position logging
This commit is contained in:
@@ -50,6 +50,7 @@ class PlaybackService : Service() {
|
||||
|
||||
private var player: ExoPlayer? = null
|
||||
private var positionPollJob: Job? = null
|
||||
private var positionCachePollJob: Job? = null
|
||||
|
||||
private val _uiState = MutableStateFlow(PlaybackUiState.initial())
|
||||
val uiState: StateFlow<PlaybackUiState> = _uiState.asStateFlow()
|
||||
@@ -64,6 +65,10 @@ class PlaybackService : Service() {
|
||||
private var queue: List<TrackQueueItem> = emptyList()
|
||||
private var index: Int = 0
|
||||
|
||||
/** Updated on the main thread whenever progress is read from ExoPlayer — safe for sensor threads. */
|
||||
@Volatile
|
||||
private var cachedPlaybackPositionMs: Long = 0L
|
||||
|
||||
private val playerListener =
|
||||
object : Player.Listener {
|
||||
override fun onPlaybackStateChanged(playbackState: Int) {
|
||||
@@ -215,11 +220,21 @@ class PlaybackService : Service() {
|
||||
}
|
||||
}
|
||||
}
|
||||
positionCachePollJob?.cancel()
|
||||
positionCachePollJob =
|
||||
scope.launch {
|
||||
while (isActive) {
|
||||
delay(POSITION_CACHE_INTERVAL_MS)
|
||||
refreshCachedPlaybackPositionMs()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun releasePlayer() {
|
||||
positionPollJob?.cancel()
|
||||
positionPollJob = null
|
||||
positionCachePollJob?.cancel()
|
||||
positionCachePollJob = null
|
||||
player?.removeListener(playerListener)
|
||||
player?.release()
|
||||
player = null
|
||||
@@ -321,13 +336,24 @@ class PlaybackService : Service() {
|
||||
return hint
|
||||
}
|
||||
|
||||
private fun refreshCachedPlaybackPositionMs() {
|
||||
val p = player ?: return
|
||||
if (queue.isEmpty()) {
|
||||
cachedPlaybackPositionMs = 0L
|
||||
return
|
||||
}
|
||||
val durationMs = currentDurationMs().coerceAtLeast(1L)
|
||||
cachedPlaybackPositionMs = p.currentPosition.coerceIn(0L, durationMs)
|
||||
}
|
||||
|
||||
private fun updateProgressFromPlayer() {
|
||||
val p = player ?: return
|
||||
val durationMs = currentDurationMs().coerceAtLeast(1L)
|
||||
val positionMs = p.currentPosition.coerceIn(0L, durationMs)
|
||||
refreshCachedPlaybackPositionMs()
|
||||
if (queue.isEmpty()) {
|
||||
return
|
||||
}
|
||||
val durationMs = currentDurationMs().coerceAtLeast(1L)
|
||||
val positionMs = cachedPlaybackPositionMs
|
||||
val progress = (positionMs.toFloat() / durationMs.toFloat()).coerceIn(0f, 1f)
|
||||
val durationSec = (durationMs / 1000L).toInt().coerceAtLeast(1)
|
||||
_uiState.value =
|
||||
@@ -413,11 +439,9 @@ class PlaybackService : Service() {
|
||||
|
||||
/**
|
||||
* Milliseconds from the start of the current track — same timebase as [ExoPlayer.getCurrentPosition].
|
||||
* May be called from any thread per Media3 [Player] contract.
|
||||
* Reads a main-thread cache so callers on background threads (e.g. run-data sensor collection) stay safe.
|
||||
*/
|
||||
fun getPlaybackPositionMs(): Long {
|
||||
return player?.currentPosition ?: 0L
|
||||
}
|
||||
fun getPlaybackPositionMs(): Long = cachedPlaybackPositionMs
|
||||
|
||||
private fun refreshForegroundNotification() {
|
||||
if (queue.isEmpty()) return
|
||||
@@ -494,6 +518,7 @@ class PlaybackService : Service() {
|
||||
|
||||
override fun onDestroy() {
|
||||
positionPollJob?.cancel()
|
||||
positionCachePollJob?.cancel()
|
||||
releasePlayer()
|
||||
mediaSession.run {
|
||||
isActive = false
|
||||
@@ -538,6 +563,7 @@ class PlaybackService : Service() {
|
||||
|
||||
companion object {
|
||||
private const val UPDATE_INTERVAL_MS = 250L
|
||||
private const val POSITION_CACHE_INTERVAL_MS = 20L
|
||||
private const val DEFAULT_DURATION_HINT_MS = 180_000
|
||||
|
||||
const val ACTION_START_PLAYLIST = "at.lockstep.player.action.START_PLAYLIST"
|
||||
|
||||
Reference in New Issue
Block a user