mirror of
https://github.com/bbara04/Purefin.git
synced 2026-07-23 19:26:50 +00:00
Derive player state from Media3 events
This commit is contained in:
@@ -6,7 +6,6 @@ import androidx.media3.common.MediaItem
|
|||||||
import androidx.media3.common.PlaybackException
|
import androidx.media3.common.PlaybackException
|
||||||
import androidx.media3.common.Player
|
import androidx.media3.common.Player
|
||||||
import androidx.media3.common.TrackSelectionOverride
|
import androidx.media3.common.TrackSelectionOverride
|
||||||
import androidx.media3.common.Tracks
|
|
||||||
import androidx.media3.common.util.UnstableApi
|
import androidx.media3.common.util.UnstableApi
|
||||||
import dagger.hilt.android.scopes.ViewModelScoped
|
import dagger.hilt.android.scopes.ViewModelScoped
|
||||||
import hu.bbara.purefin.core.data.PlaybackReportContext
|
import hu.bbara.purefin.core.data.PlaybackReportContext
|
||||||
@@ -71,40 +70,6 @@ class PlayerManager @Inject constructor(
|
|||||||
val queue: StateFlow<List<QueueItemUi>> = _queue.asStateFlow()
|
val queue: StateFlow<List<QueueItemUi>> = _queue.asStateFlow()
|
||||||
|
|
||||||
private val listener = object : Player.Listener {
|
private val listener = object : Player.Listener {
|
||||||
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
|
||||||
_playbackState.update { it.copy(isPlaying = isPlaying, isBuffering = false, isEnded = false) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPlaybackStateChanged(playbackState: Int) {
|
|
||||||
val buffering = playbackState == Player.STATE_BUFFERING
|
|
||||||
val ended = playbackState == Player.STATE_ENDED
|
|
||||||
_playbackState.update { state ->
|
|
||||||
state.copy(
|
|
||||||
isBuffering = buffering,
|
|
||||||
isEnded = ended,
|
|
||||||
error = if (playbackState == Player.STATE_IDLE) state.error else null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (ended) player.pause()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPlayerError(error: PlaybackException) {
|
|
||||||
_playbackState.update { it.copy(error = error.errorCodeName ?: error.localizedMessage ?: "Playback error") }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onTracksChanged(tracks: Tracks) {
|
|
||||||
refreshTracks(tracks)
|
|
||||||
scope.launch {
|
|
||||||
applyTrackPreferences()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
|
|
||||||
clearPendingSeek()
|
|
||||||
refreshMetadata(mediaItem)
|
|
||||||
refreshQueue()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPositionDiscontinuity(
|
override fun onPositionDiscontinuity(
|
||||||
oldPosition: Player.PositionInfo,
|
oldPosition: Player.PositionInfo,
|
||||||
newPosition: Player.PositionInfo,
|
newPosition: Player.PositionInfo,
|
||||||
@@ -112,13 +77,29 @@ class PlayerManager @Inject constructor(
|
|||||||
) {
|
) {
|
||||||
syncPendingSeek(newPosition.positionMs)
|
syncPendingSeek(newPosition.positionMs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onPlayerError(error: PlaybackException) {
|
||||||
|
_playbackState.update {
|
||||||
|
it.copy(error = mapPlayerError(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onEvents(player: Player, events: Player.Events) {
|
||||||
|
if (events.contains(Player.EVENT_MEDIA_ITEM_TRANSITION)) {
|
||||||
|
clearPendingSeek()
|
||||||
|
}
|
||||||
|
updateFromPlayer(player)
|
||||||
|
if (events.contains(Player.EVENT_TRACKS_CHANGED)) {
|
||||||
|
scope.launch {
|
||||||
|
applyTrackPreferences()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
player.addListener(listener)
|
player.addListener(listener)
|
||||||
refreshMetadata(player.currentMediaItem)
|
updateFromPlayer(player)
|
||||||
refreshTracks(player.currentTracks)
|
|
||||||
refreshQueue()
|
|
||||||
startProgressLoop()
|
startProgressLoop()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,14 +109,10 @@ class PlayerManager @Inject constructor(
|
|||||||
player.setMediaItem(mediaItem)
|
player.setMediaItem(mediaItem)
|
||||||
player.prepare()
|
player.prepare()
|
||||||
player.playWhenReady = true
|
player.playWhenReady = true
|
||||||
refreshMetadata(mediaItem)
|
|
||||||
refreshQueue()
|
|
||||||
_playbackState.update { it.copy(isEnded = false, error = null) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addToQueue(mediaItem: MediaItem) {
|
fun addToQueue(mediaItem: MediaItem) {
|
||||||
player.addMediaItem(mediaItem)
|
player.addMediaItem(mediaItem)
|
||||||
refreshQueue()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun togglePlayPause() {
|
fun togglePlayPause() {
|
||||||
@@ -229,7 +206,6 @@ class PlayerManager @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
player.trackSelectionParameters = builder.build()
|
player.trackSelectionParameters = builder.build()
|
||||||
refreshTracks(player.currentTracks)
|
|
||||||
|
|
||||||
// Save track preference if media context is available
|
// Save track preference if media context is available
|
||||||
currentMediaContext?.let { context ->
|
currentMediaContext?.let { context ->
|
||||||
@@ -256,7 +232,6 @@ class PlayerManager @Inject constructor(
|
|||||||
clearPendingSeek()
|
clearPendingSeek()
|
||||||
player.seekToDefaultPosition(targetIndex)
|
player.seekToDefaultPosition(targetIndex)
|
||||||
player.playWhenReady = true
|
player.playWhenReady = true
|
||||||
refreshQueue()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -341,7 +316,30 @@ class PlayerManager @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun refreshQueue() {
|
private fun updateFromPlayer(player: Player) {
|
||||||
|
val currentMediaItem = player.currentMediaItem
|
||||||
|
val playbackState = player.playbackState
|
||||||
|
val playbackReportContext = currentMediaItem?.localConfiguration?.tag as? PlaybackReportContext
|
||||||
|
val currentMetadata = player.mediaMetadata
|
||||||
|
val playerError = player.playerError
|
||||||
|
|
||||||
|
_playbackState.value = PlaybackStateSnapshot(
|
||||||
|
isPlaying = player.isPlaying,
|
||||||
|
isBuffering = playbackState == Player.STATE_BUFFERING,
|
||||||
|
isEnded = playbackState == Player.STATE_ENDED,
|
||||||
|
error = mapPlayerError(playerError)
|
||||||
|
)
|
||||||
|
_metadata.value = MetadataState(
|
||||||
|
mediaId = currentMediaItem?.mediaId,
|
||||||
|
title = currentMetadata.title?.toString(),
|
||||||
|
subtitle = currentMetadata.subtitle?.toString(),
|
||||||
|
playbackReportContext = playbackReportContext,
|
||||||
|
)
|
||||||
|
_tracks.value = trackMapper.map(player.currentTracks)
|
||||||
|
_queue.value = buildQueueSnapshot(player)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildQueueSnapshot(player: Player): List<QueueItemUi> {
|
||||||
val items = mutableListOf<QueueItemUi>()
|
val items = mutableListOf<QueueItemUi>()
|
||||||
for (i in 0 until player.mediaItemCount) {
|
for (i in 0 until player.mediaItemCount) {
|
||||||
val mediaItem = player.getMediaItemAt(i)
|
val mediaItem = player.getMediaItemAt(i)
|
||||||
@@ -355,21 +353,7 @@ class PlayerManager @Inject constructor(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
_queue.value = items
|
return items
|
||||||
}
|
|
||||||
|
|
||||||
private fun refreshMetadata(mediaItem: MediaItem?) {
|
|
||||||
val playbackReportContext = mediaItem?.localConfiguration?.tag as? PlaybackReportContext
|
|
||||||
_metadata.value = MetadataState(
|
|
||||||
mediaId = mediaItem?.mediaId,
|
|
||||||
title = mediaItem?.mediaMetadata?.title?.toString(),
|
|
||||||
subtitle = mediaItem?.mediaMetadata?.subtitle?.toString(),
|
|
||||||
playbackReportContext = playbackReportContext,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun refreshTracks(tracks: Tracks) {
|
|
||||||
_tracks.value = trackMapper.map(tracks)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun clampSeekPosition(positionMs: Long): Long {
|
private fun clampSeekPosition(positionMs: Long): Long {
|
||||||
@@ -389,6 +373,10 @@ class PlayerManager @Inject constructor(
|
|||||||
pendingSeekPositionMs = null
|
pendingSeekPositionMs = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun mapPlayerError(error: PlaybackException?): String? {
|
||||||
|
return error?.errorCodeName ?: error?.localizedMessage ?: error?.message
|
||||||
|
}
|
||||||
|
|
||||||
private fun syncPendingSeek(positionMs: Long) {
|
private fun syncPendingSeek(positionMs: Long) {
|
||||||
val pendingPosition = pendingSeekPositionMs ?: return
|
val pendingPosition = pendingSeekPositionMs ?: return
|
||||||
if (abs(positionMs - pendingPosition) <= SEEK_SETTLE_TOLERANCE_MS) {
|
if (abs(positionMs - pendingPosition) <= SEEK_SETTLE_TOLERANCE_MS) {
|
||||||
@@ -396,4 +384,3 @@ class PlayerManager @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user