From f245e3388705d88857c7db11af12accea3d33c70 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Tue, 24 Sep 2024 18:40:18 -0600 Subject: [PATCH] playback: restructure repeat mode/listeners --- .../auxio/playback/player/GaplessQueuer.kt | 48 ++++++- .../{PlayerFactory.kt => PlayerKernel.kt} | 89 ++++++++----- .../auxio/playback/player/PlayerModule.kt | 2 +- .../playback/player/PlayerStateHolder.kt | 124 +++++++----------- .../oxycblt/auxio/playback/player/Queuer.kt | 13 +- 5 files changed, 163 insertions(+), 113 deletions(-) rename app/src/main/java/org/oxycblt/auxio/playback/player/{PlayerFactory.kt => PlayerKernel.kt} (53%) diff --git a/app/src/main/java/org/oxycblt/auxio/playback/player/GaplessQueuer.kt b/app/src/main/java/org/oxycblt/auxio/playback/player/GaplessQueuer.kt index d532ad2a7..8d5f41d1d 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/player/GaplessQueuer.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/player/GaplessQueuer.kt @@ -3,15 +3,39 @@ package org.oxycblt.auxio.playback.player import androidx.media3.common.C import androidx.media3.common.MediaItem import androidx.media3.common.Player +import androidx.media3.common.Player.RepeatMode import androidx.media3.exoplayer.ExoPlayer +import org.oxycblt.auxio.playback.PlaybackSettings +import javax.inject.Inject -class GaplessQueuer private constructor(private val exoPlayer: ExoPlayer) : Queuer { - data object Factory : Queuer.Factory { - override fun create(exoPlayer: ExoPlayer) = GaplessQueuer(exoPlayer) +/** + * + */ +class GaplessQueuer private constructor(private val exoPlayer: ExoPlayer, private val listener: Queuer.Listener, private val playbackSettings: PlaybackSettings) : Queuer, PlaybackSettings.Listener, Player.Listener { + class Factory @Inject constructor(private val playbackSettings: PlaybackSettings) : Queuer.Factory { + override fun create(exoPlayer: ExoPlayer, listener: Queuer.Listener) = GaplessQueuer(exoPlayer, listener, playbackSettings) } + override val currentMediaItem: MediaItem? = exoPlayer.currentMediaItem override val currentMediaItemIndex: Int = exoPlayer.currentMediaItemIndex override val shuffleModeEnabled: Boolean = exoPlayer.shuffleModeEnabled + @get:RepeatMode + override var repeatMode: Int = exoPlayer.repeatMode + set(value) { + field = value + exoPlayer.repeatMode = value + updatePauseOnRepeat() + } + + override fun attach() { + playbackSettings.registerListener(this) + exoPlayer.addListener(this) + } + + override fun release() { + playbackSettings.unregisterListener(this) + exoPlayer.removeListener(this) + } override fun computeHeap(): List { return (0 until exoPlayer.mediaItemCount).map { exoPlayer.getMediaItemAt(it) } @@ -142,4 +166,22 @@ class GaplessQueuer private constructor(private val exoPlayer: ExoPlayer) : Queu } } + override fun onPauseOnRepeatChanged() { + super.onPauseOnRepeatChanged() + updatePauseOnRepeat() + } + + private fun updatePauseOnRepeat() { + exoPlayer.pauseAtEndOfMediaItems = + exoPlayer.repeatMode == Player.REPEAT_MODE_ONE && playbackSettings.pauseOnRepeat + } + + override fun onPlaybackStateChanged(playbackState: Int) { + super.onPlaybackStateChanged(playbackState) + + if (playbackState == Player.STATE_ENDED && exoPlayer.repeatMode == Player.REPEAT_MODE_OFF) { + goto(0) + exoPlayer.pause() + } + } } \ No newline at end of file diff --git a/app/src/main/java/org/oxycblt/auxio/playback/player/PlayerFactory.kt b/app/src/main/java/org/oxycblt/auxio/playback/player/PlayerKernel.kt similarity index 53% rename from app/src/main/java/org/oxycblt/auxio/playback/player/PlayerFactory.kt rename to app/src/main/java/org/oxycblt/auxio/playback/player/PlayerKernel.kt index baecc5d4b..021cb1158 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/player/PlayerFactory.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/player/PlayerKernel.kt @@ -3,8 +3,8 @@ package org.oxycblt.auxio.playback.player import android.content.Context import androidx.media3.common.AudioAttributes import androidx.media3.common.C +import androidx.media3.common.PlaybackException import androidx.media3.common.Player -import androidx.media3.common.Player.RepeatMode import androidx.media3.decoder.ffmpeg.FfmpegAudioRenderer import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.RenderersFactory @@ -15,31 +15,36 @@ import androidx.media3.exoplayer.source.MediaSource import org.oxycblt.auxio.playback.replaygain.ReplayGainAudioProcessor import javax.inject.Inject -interface PlayerFactory { - fun create(context: Context): ThinPlayer - -} - -interface ThinPlayer { +interface PlayerKernel { val isPlaying: Boolean var playWhenReady: Boolean val currentPosition: Long - @get:RepeatMode var repeatMode: Int val audioSessionId: Int - var pauseAtEndOfMediaItems: Boolean + val queuer: Queuer - fun attach(listener: Player.Listener) + fun attach() fun release() fun play() fun pause() fun seekTo(positionMs: Long) - fun intoQueuer(queuerFactory: Queuer.Factory): Queuer + fun replaceQueuer(queuerFactory: Queuer.Factory) + + interface Listener { + fun onPlayWhenReadyChanged() + fun onIsPlayingChanged() + fun onPositionDiscontinuity() + fun onError(error: PlaybackException) + } + + interface Factory { + fun create(context: Context, playerListener: Listener, queuerFactory: Queuer.Factory, queuerListener: Queuer.Listener): PlayerKernel + } } -class PlayerFactoryImpl(@Inject private val mediaSourceFactory: MediaSource.Factory, @Inject private val replayGainProcessor: ReplayGainAudioProcessor) : PlayerFactory { - override fun create(context: Context): ThinPlayer { +class PlayerKernelFactoryImpl(@Inject private val mediaSourceFactory: MediaSource.Factory, @Inject private val replayGainProcessor: ReplayGainAudioProcessor) : PlayerKernel.Factory { + override fun create(context: Context, playerListener: PlayerKernel.Listener, queuerFactory: Queuer.Factory, queuerListener: Queuer.Listener): PlayerKernel { // Since Auxio is a music player, only specify an audio renderer to save // battery/apk size/cache size val audioRenderer = RenderersFactory { handler, _, audioListener, _, _ -> @@ -68,14 +73,18 @@ class PlayerFactoryImpl(@Inject private val mediaSourceFactory: MediaSource.Fact true) .build() - return ThinPlayerImpl(exoPlayer, replayGainProcessor) + return PlayerKernelImpl(exoPlayer, replayGainProcessor, playerListener, queuerListener, queuerFactory) } } -private class ThinPlayerImpl( +private class PlayerKernelImpl( private val exoPlayer: ExoPlayer, - private val replayGainProcessor: ReplayGainAudioProcessor -) : ThinPlayer { + private val replayGainProcessor: ReplayGainAudioProcessor, + private val playerListener: PlayerKernel.Listener, + private val queuerListener: Queuer.Listener, + queuerFactory: Queuer.Factory +) : PlayerKernel, Player.Listener { + override var queuer: Queuer = queuerFactory.create(exoPlayer, queuerListener) override val isPlaying: Boolean get() = exoPlayer.isPlaying override var playWhenReady: Boolean get() = exoPlayer.playWhenReady @@ -83,24 +92,16 @@ private class ThinPlayerImpl( exoPlayer.playWhenReady = value } override val currentPosition: Long get() = exoPlayer.currentPosition - override var repeatMode: Int - get() = exoPlayer.repeatMode - set(value) { - exoPlayer.repeatMode = value - } override val audioSessionId: Int get() = exoPlayer.audioSessionId - override var pauseAtEndOfMediaItems: Boolean - get() = exoPlayer.pauseAtEndOfMediaItems - set(value) { - exoPlayer.pauseAtEndOfMediaItems = value - } - override fun attach(listener: Player.Listener) { - exoPlayer.addListener(listener) + override fun attach() { + exoPlayer.addListener(this) replayGainProcessor.attach() + queuer.attach() } override fun release() { + queuer.release() replayGainProcessor.release() exoPlayer.release() } @@ -111,5 +112,33 @@ private class ThinPlayerImpl( override fun seekTo(positionMs: Long) = exoPlayer.seekTo(positionMs) - override fun intoQueuer(queuerFactory: Queuer.Factory) = queuerFactory.create(exoPlayer) + override fun replaceQueuer(queuerFactory: Queuer.Factory) { + queuer.release() + queuer = queuerFactory.create(exoPlayer, queuerListener) + queuer.attach() + } + + override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) { + super.onPlayWhenReadyChanged(playWhenReady, reason) + playerListener.onPlayWhenReadyChanged() + } + + override fun onIsPlayingChanged(isPlaying: Boolean) { + super.onIsPlayingChanged(isPlaying) + playerListener.onIsPlayingChanged() + } + + override fun onPositionDiscontinuity( + oldPosition: Player.PositionInfo, + newPosition: Player.PositionInfo, + reason: Int + ) { + super.onPositionDiscontinuity(oldPosition, newPosition, reason) + playerListener.onPositionDiscontinuity() + } + + override fun onPlayerError(error: PlaybackException) { + super.onPlayerError(error) + playerListener.onError(error) + } } \ No newline at end of file diff --git a/app/src/main/java/org/oxycblt/auxio/playback/player/PlayerModule.kt b/app/src/main/java/org/oxycblt/auxio/playback/player/PlayerModule.kt index 05b6160d2..d20c80bd0 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/player/PlayerModule.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/player/PlayerModule.kt @@ -42,7 +42,7 @@ import dagger.hilt.components.SingletonComponent @Module @InstallIn(SingletonComponent::class) interface PlayerModule { - @Binds fun playerFactory(factory: PlayerFactoryImpl): PlayerFactory + @Binds fun playerKernelFactory(factory: PlayerKernelFactoryImpl): PlayerKernel.Factory } @Module diff --git a/app/src/main/java/org/oxycblt/auxio/playback/player/PlayerStateHolder.kt b/app/src/main/java/org/oxycblt/auxio/playback/player/PlayerStateHolder.kt index 403bd079e..4e306107f 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/player/PlayerStateHolder.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/player/PlayerStateHolder.kt @@ -54,29 +54,31 @@ import org.oxycblt.auxio.util.logE class PlayerStateHolder( private val context: Context, - playerFactory: PlayerFactory, + playerKernelFactory: PlayerKernel.Factory, private val playbackManager: PlaybackStateManager, private val persistenceRepository: PersistenceRepository, private val playbackSettings: PlaybackSettings, private val commandFactory: PlaybackCommand.Factory, private val replayGainProcessor: ReplayGainAudioProcessor, + gaplessQueuerFactory: Queuer.Factory, private val musicRepository: MusicRepository, - private val imageSettings: ImageSettings, + private val imageSettings: ImageSettings ) : PlaybackStateHolder, - Player.Listener, + PlayerKernel.Listener, + Queuer.Listener, MusicRepository.UpdateListener, - ImageSettings.Listener, - PlaybackSettings.Listener { + ImageSettings.Listener { class Factory @Inject constructor( private val playbackManager: PlaybackStateManager, private val persistenceRepository: PersistenceRepository, private val playbackSettings: PlaybackSettings, - private val playerFactory: PlayerFactory, + private val playerFactory: PlayerKernel.Factory, private val commandFactory: PlaybackCommand.Factory, private val replayGainProcessor: ReplayGainAudioProcessor, + private val gaplessQueuerFactory: Queuer.Factory, private val musicRepository: MusicRepository, private val imageSettings: ImageSettings, ) { @@ -89,6 +91,7 @@ class PlayerStateHolder( playbackSettings, commandFactory, replayGainProcessor, + gaplessQueuerFactory, musicRepository, imageSettings) } @@ -99,15 +102,13 @@ class PlayerStateHolder( private val restoreScope = CoroutineScope(Dispatchers.IO + saveJob) private var currentSaveJob: Job? = null private var openAudioEffectSession = false - private val player = playerFactory.create(context) - private val queuer = player.intoQueuer(GaplessQueuer.Factory) + private val player = playerKernelFactory.create(context, this, gaplessQueuerFactory, this) var sessionOngoing = false private set fun attach() { - player.attach(this) - playbackSettings.registerListener(this) + player.attach() imageSettings.registerListener(this) playbackManager.registerStateHolder(this) musicRepository.addUpdateListener(this) @@ -116,7 +117,6 @@ class PlayerStateHolder( fun release() { saveJob.cancel() player.release() - playbackSettings.unregisterListener(this) playbackManager.unregisterStateHolder(this) musicRepository.removeUpdateListener(this) replayGainProcessor.release() @@ -129,7 +129,7 @@ class PlayerStateHolder( override val progression: Progression get() { - val mediaItem = queuer.currentMediaItem ?: return Progression.nil() + val mediaItem = player.queuer.currentMediaItem ?: return Progression.nil() val duration = mediaItem.mediaMetadata.extras?.getLong("durationMs") ?: Long.MAX_VALUE val clampedPosition = player.currentPosition.coerceAtLeast(0).coerceAtMost(duration) return Progression.from(player.playWhenReady, player.isPlaying, clampedPosition) @@ -137,7 +137,7 @@ class PlayerStateHolder( override val repeatMode get() = - when (val repeatMode = player.repeatMode) { + when (val repeatMode = player.queuer.repeatMode) { Player.REPEAT_MODE_OFF -> RepeatMode.NONE Player.REPEAT_MODE_ONE -> RepeatMode.TRACK Player.REPEAT_MODE_ALL -> RepeatMode.ALL @@ -148,9 +148,9 @@ class PlayerStateHolder( get() = player.audioSessionId override fun resolveQueue(): RawQueue { - val heap = queuer.computeHeap() - val shuffledMapping = if (queuer.shuffleModeEnabled) queuer.computeMapping() else emptyList() - return RawQueue(heap.mapNotNull { it.song }, shuffledMapping, queuer.currentMediaItemIndex) + val heap = player.queuer.computeHeap() + val shuffledMapping = if (player.queuer.shuffleModeEnabled) player.queuer.computeMapping() else emptyList() + return RawQueue(heap.mapNotNull { it.song }, shuffledMapping, player.queuer.currentMediaItemIndex) } override fun handleDeferred(action: DeferredPlayback): Boolean { @@ -213,13 +213,12 @@ class PlayerStateHolder( } override fun repeatMode(repeatMode: RepeatMode) { - player.repeatMode = + player.queuer.repeatMode = when (repeatMode) { RepeatMode.NONE -> Player.REPEAT_MODE_OFF RepeatMode.ALL -> Player.REPEAT_MODE_ALL RepeatMode.TRACK -> Player.REPEAT_MODE_ONE } - updatePauseOnRepeat() playbackManager.ack(this, StateAck.RepeatModeChanged) deferSave() } @@ -231,14 +230,14 @@ class PlayerStateHolder( command.song ?.let { command.queue.indexOf(it) } .also { check(it != -1) { "Start song not in queue" } } - queuer.prepareNew(mediaItems, startIndex, command.shuffled) + player.queuer.prepareNew(mediaItems, startIndex, command.shuffled) player.play() playbackManager.ack(this, StateAck.NewPlayback) deferSave() } override fun shuffled(shuffled: Boolean) { - queuer.shuffled(shuffled) + player.queuer.shuffled(shuffled) playbackManager.ack(this, StateAck.QueueReordered) deferSave() } @@ -247,13 +246,13 @@ class PlayerStateHolder( // Replicate the old pseudo-circular queue behavior when no repeat option is implemented. // Basically, you can't skip back and wrap around the queue, but you can skip forward and // wrap around the queue, albeit playback will be paused. - if (player.repeatMode == Player.REPEAT_MODE_ALL || queuer.hasNextMediaItem()) { - queuer.seekToNext() + if (player.queuer.repeatMode == Player.REPEAT_MODE_ALL || player.queuer.hasNextMediaItem()) { + player.queuer.seekToNext() if (!playbackSettings.rememberPause) { player.play() } } else { - queuer.goto(queuer.computeFirstMediaItemIndex()) + player.queuer.goto(player.queuer.computeFirstMediaItemIndex()) // TODO: Dislike the UX implications of this, I feel should I bite the bullet // and switch to dynamic skip enable/disable? if (!playbackSettings.rememberPause) { @@ -266,9 +265,9 @@ class PlayerStateHolder( override fun prev() { if (playbackSettings.rewindWithPrev) { - queuer.seekToPrevious() - } else if (queuer.hasPreviousMediaItem()) { - queuer.seekToPreviousMediaItem() + player.queuer.seekToPrevious() + } else if (player.queuer.hasPreviousMediaItem()) { + player.queuer.seekToPreviousMediaItem() } else { player.seekTo(0) } @@ -280,12 +279,12 @@ class PlayerStateHolder( } override fun goto(index: Int) { - val indices = queuer.computeMapping() + val indices = player.queuer.computeMapping() if (indices.isEmpty()) { return } val trueIndex = indices[index] - queuer.goto(trueIndex) + player.queuer.goto(trueIndex) if (!playbackSettings.rememberPause) { player.play() } @@ -294,19 +293,19 @@ class PlayerStateHolder( } override fun playNext(songs: List, ack: StateAck.PlayNext) { - queuer.addBottomMediaItems(songs.map { it.buildMediaItem() }) + player.queuer.addBottomMediaItems(songs.map { it.buildMediaItem() }) playbackManager.ack(this, ack) deferSave() } override fun addToQueue(songs: List, ack: StateAck.AddToQueue) { - queuer.addTopMediaItems(songs.map { it.buildMediaItem() }) + player.queuer.addTopMediaItems(songs.map { it.buildMediaItem() }) playbackManager.ack(this, ack) deferSave() } override fun move(from: Int, to: Int, ack: StateAck.Move) { - val indices = queuer.computeMapping() + val indices = player.queuer.computeMapping() if (indices.isEmpty()) { return } @@ -314,20 +313,20 @@ class PlayerStateHolder( val trueFrom = indices[from] val trueTo = indices[to] - queuer.moveMediaItem(trueFrom, trueTo) + player.queuer.moveMediaItem(trueFrom, trueTo) playbackManager.ack(this, ack) deferSave() } override fun remove(at: Int, ack: StateAck.Remove) { - val indices = queuer.computeMapping() + val indices = player.queuer.computeMapping() if (indices.isEmpty()) { return } val trueIndex = indices[at] - val songWillChange = queuer.currentMediaItemIndex == trueIndex - queuer.removeMediaItem(trueIndex) + val songWillChange = player.queuer.currentMediaItemIndex == trueIndex + player.queuer.removeMediaItem(trueIndex) if (songWillChange && !playbackSettings.rememberPause) { player.play() } @@ -347,7 +346,7 @@ class PlayerStateHolder( sendEvent = true } if (rawQueue != resolveQueue()) { - queuer.prepareSaved(rawQueue.heap.map { it.buildMediaItem() }, rawQueue.shuffledMapping, rawQueue.heapIndex, rawQueue.isShuffled) + player.queuer.prepareSaved(rawQueue.heap.map { it.buildMediaItem() }, rawQueue.shuffledMapping, rawQueue.heapIndex, rawQueue.isShuffled) player.pause() sendEvent = true } @@ -371,16 +370,14 @@ class PlayerStateHolder( } override fun reset(ack: StateAck.NewPlayback) { - queuer.discard() + player.queuer.discard() playbackManager.ack(this, ack) deferSave() } // --- PLAYER OVERRIDES --- - override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) { - super.onPlayWhenReadyChanged(playWhenReady, reason) - + override fun onPlayWhenReadyChanged() { if (player.playWhenReady) { // Mark that we have started playing so that the notification can now be posted. logD("Player has started playing") @@ -398,40 +395,23 @@ class PlayerStateHolder( broadcastAudioEffectAction(AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION) openAudioEffectSession = false } + + playbackManager.ack(this, StateAck.ProgressionChanged) } - override fun onPlaybackStateChanged(playbackState: Int) { - super.onPlaybackStateChanged(playbackState) - - if (playbackState == Player.STATE_ENDED && player.repeatMode == Player.REPEAT_MODE_OFF) { - goto(0) - player.pause() - } + override fun onIsPlayingChanged() { + playbackManager.ack(this, StateAck.ProgressionChanged) } - override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) { - super.onMediaItemTransition(mediaItem, reason) - - if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_AUTO) { - playbackManager.ack(this, StateAck.IndexMoved) - } + override fun onPositionDiscontinuity() { + playbackManager.ack(this, StateAck.ProgressionChanged) } - override fun onEvents(player: Player, events: Player.Events) { - super.onEvents(player, events) - - // So many actions trigger progression changes that it becomes easier just to handle it - // in an ExoPlayer callback anyway. This doesn't really cause issues anywhere. - if (events.containsAny( - Player.EVENT_PLAY_WHEN_READY_CHANGED, - Player.EVENT_IS_PLAYING_CHANGED, - Player.EVENT_POSITION_DISCONTINUITY)) { - logD("Player state changed, must synchronize state") - playbackManager.ack(this, StateAck.ProgressionChanged) - } + override fun onAutoTransition() { + playbackManager.ack(this, StateAck.IndexMoved) } - override fun onPlayerError(error: PlaybackException) { + override fun onError(error: PlaybackException) { // TODO: Replace with no skipping and a notification instead // If there's any issue, just go to the next song. logE("Player error occurred") @@ -458,18 +438,6 @@ class PlayerStateHolder( } } - // --- PLAYBACK SETTINGS METHODS --- - - override fun onPauseOnRepeatChanged() { - super.onPauseOnRepeatChanged() - updatePauseOnRepeat() - } - - private fun updatePauseOnRepeat() { - player.pauseAtEndOfMediaItems = - player.repeatMode == Player.REPEAT_MODE_ONE && playbackSettings.pauseOnRepeat - } - // --- OVERRIDES --- private fun save(cb: () -> Unit) { diff --git a/app/src/main/java/org/oxycblt/auxio/playback/player/Queuer.kt b/app/src/main/java/org/oxycblt/auxio/playback/player/Queuer.kt index 3274ba34b..6701eea76 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/player/Queuer.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/player/Queuer.kt @@ -1,6 +1,8 @@ package org.oxycblt.auxio.playback.player import androidx.media3.common.MediaItem +import androidx.media3.common.Player +import androidx.media3.common.Player.RepeatMode import androidx.media3.exoplayer.ExoPlayer interface Queuer { @@ -8,6 +10,11 @@ interface Queuer { val currentMediaItemIndex: Int val shuffleModeEnabled: Boolean + @get:RepeatMode var repeatMode: Int + + fun attach() + fun release() + fun goto(mediaItemIndex: Int) fun seekToNext() @@ -32,7 +39,11 @@ interface Queuer { fun addBottomMediaItems(mediaItems: List) fun shuffled(shuffled: Boolean) + interface Listener { + fun onAutoTransition() + } + interface Factory { - fun create(exoPlayer: ExoPlayer): Queuer + fun create(exoPlayer: ExoPlayer, listener: Listener): Queuer } } \ No newline at end of file