fix: thread-safe player position logging

This commit is contained in:
2026-05-24 10:41:51 +02:00
parent b5bc01fac2
commit eb53d1e0ad

View File

@@ -50,6 +50,7 @@ class PlaybackService : Service() {
private var player: ExoPlayer? = null private var player: ExoPlayer? = null
private var positionPollJob: Job? = null private var positionPollJob: Job? = null
private var positionCachePollJob: Job? = null
private val _uiState = MutableStateFlow(PlaybackUiState.initial()) private val _uiState = MutableStateFlow(PlaybackUiState.initial())
val uiState: StateFlow<PlaybackUiState> = _uiState.asStateFlow() val uiState: StateFlow<PlaybackUiState> = _uiState.asStateFlow()
@@ -64,6 +65,10 @@ class PlaybackService : Service() {
private var queue: List<TrackQueueItem> = emptyList() private var queue: List<TrackQueueItem> = emptyList()
private var index: Int = 0 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 = private val playerListener =
object : Player.Listener { object : Player.Listener {
override fun onPlaybackStateChanged(playbackState: Int) { 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() { private fun releasePlayer() {
positionPollJob?.cancel() positionPollJob?.cancel()
positionPollJob = null positionPollJob = null
positionCachePollJob?.cancel()
positionCachePollJob = null
player?.removeListener(playerListener) player?.removeListener(playerListener)
player?.release() player?.release()
player = null player = null
@@ -321,13 +336,24 @@ class PlaybackService : Service() {
return hint 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() { private fun updateProgressFromPlayer() {
val p = player ?: return val p = player ?: return
val durationMs = currentDurationMs().coerceAtLeast(1L) refreshCachedPlaybackPositionMs()
val positionMs = p.currentPosition.coerceIn(0L, durationMs)
if (queue.isEmpty()) { if (queue.isEmpty()) {
return return
} }
val durationMs = currentDurationMs().coerceAtLeast(1L)
val positionMs = cachedPlaybackPositionMs
val progress = (positionMs.toFloat() / durationMs.toFloat()).coerceIn(0f, 1f) val progress = (positionMs.toFloat() / durationMs.toFloat()).coerceIn(0f, 1f)
val durationSec = (durationMs / 1000L).toInt().coerceAtLeast(1) val durationSec = (durationMs / 1000L).toInt().coerceAtLeast(1)
_uiState.value = _uiState.value =
@@ -413,11 +439,9 @@ class PlaybackService : Service() {
/** /**
* Milliseconds from the start of the current track — same timebase as [ExoPlayer.getCurrentPosition]. * 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 { fun getPlaybackPositionMs(): Long = cachedPlaybackPositionMs
return player?.currentPosition ?: 0L
}
private fun refreshForegroundNotification() { private fun refreshForegroundNotification() {
if (queue.isEmpty()) return if (queue.isEmpty()) return
@@ -494,6 +518,7 @@ class PlaybackService : Service() {
override fun onDestroy() { override fun onDestroy() {
positionPollJob?.cancel() positionPollJob?.cancel()
positionCachePollJob?.cancel()
releasePlayer() releasePlayer()
mediaSession.run { mediaSession.run {
isActive = false isActive = false
@@ -538,6 +563,7 @@ class PlaybackService : Service() {
companion object { companion object {
private const val UPDATE_INTERVAL_MS = 250L private const val UPDATE_INTERVAL_MS = 250L
private const val POSITION_CACHE_INTERVAL_MS = 20L
private const val DEFAULT_DURATION_HINT_MS = 180_000 private const val DEFAULT_DURATION_HINT_MS = 180_000
const val ACTION_START_PLAYLIST = "at.lockstep.player.action.START_PLAYLIST" const val ACTION_START_PLAYLIST = "at.lockstep.player.action.START_PLAYLIST"