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 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"
|
||||||
|
|||||||
Reference in New Issue
Block a user