playback: restructure repeat mode/listeners

This commit is contained in:
Alexander Capehart 2024-09-24 18:40:18 -06:00
parent b784250fed
commit f245e33887
5 changed files with 163 additions and 113 deletions

View file

@ -3,15 +3,39 @@ package org.oxycblt.auxio.playback.player
import androidx.media3.common.C import androidx.media3.common.C
import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem
import androidx.media3.common.Player import androidx.media3.common.Player
import androidx.media3.common.Player.RepeatMode
import androidx.media3.exoplayer.ExoPlayer 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 currentMediaItem: MediaItem? = exoPlayer.currentMediaItem
override val currentMediaItemIndex: Int = exoPlayer.currentMediaItemIndex override val currentMediaItemIndex: Int = exoPlayer.currentMediaItemIndex
override val shuffleModeEnabled: Boolean = exoPlayer.shuffleModeEnabled 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<MediaItem> { override fun computeHeap(): List<MediaItem> {
return (0 until exoPlayer.mediaItemCount).map { exoPlayer.getMediaItemAt(it) } 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()
}
}
} }

View file

@ -3,8 +3,8 @@ package org.oxycblt.auxio.playback.player
import android.content.Context import android.content.Context
import androidx.media3.common.AudioAttributes import androidx.media3.common.AudioAttributes
import androidx.media3.common.C import androidx.media3.common.C
import androidx.media3.common.PlaybackException
import androidx.media3.common.Player import androidx.media3.common.Player
import androidx.media3.common.Player.RepeatMode
import androidx.media3.decoder.ffmpeg.FfmpegAudioRenderer import androidx.media3.decoder.ffmpeg.FfmpegAudioRenderer
import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.RenderersFactory import androidx.media3.exoplayer.RenderersFactory
@ -15,31 +15,36 @@ import androidx.media3.exoplayer.source.MediaSource
import org.oxycblt.auxio.playback.replaygain.ReplayGainAudioProcessor import org.oxycblt.auxio.playback.replaygain.ReplayGainAudioProcessor
import javax.inject.Inject import javax.inject.Inject
interface PlayerFactory { interface PlayerKernel {
fun create(context: Context): ThinPlayer
}
interface ThinPlayer {
val isPlaying: Boolean val isPlaying: Boolean
var playWhenReady: Boolean var playWhenReady: Boolean
val currentPosition: Long val currentPosition: Long
@get:RepeatMode var repeatMode: Int
val audioSessionId: Int val audioSessionId: Int
var pauseAtEndOfMediaItems: Boolean val queuer: Queuer
fun attach(listener: Player.Listener) fun attach()
fun release() fun release()
fun play() fun play()
fun pause() fun pause()
fun seekTo(positionMs: Long) 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 { class PlayerKernelFactoryImpl(@Inject private val mediaSourceFactory: MediaSource.Factory, @Inject private val replayGainProcessor: ReplayGainAudioProcessor) : PlayerKernel.Factory {
override fun create(context: Context): ThinPlayer { 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 // Since Auxio is a music player, only specify an audio renderer to save
// battery/apk size/cache size // battery/apk size/cache size
val audioRenderer = RenderersFactory { handler, _, audioListener, _, _ -> val audioRenderer = RenderersFactory { handler, _, audioListener, _, _ ->
@ -68,14 +73,18 @@ class PlayerFactoryImpl(@Inject private val mediaSourceFactory: MediaSource.Fact
true) true)
.build() .build()
return ThinPlayerImpl(exoPlayer, replayGainProcessor) return PlayerKernelImpl(exoPlayer, replayGainProcessor, playerListener, queuerListener, queuerFactory)
} }
} }
private class ThinPlayerImpl( private class PlayerKernelImpl(
private val exoPlayer: ExoPlayer, private val exoPlayer: ExoPlayer,
private val replayGainProcessor: ReplayGainAudioProcessor private val replayGainProcessor: ReplayGainAudioProcessor,
) : ThinPlayer { 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 val isPlaying: Boolean get() = exoPlayer.isPlaying
override var playWhenReady: Boolean override var playWhenReady: Boolean
get() = exoPlayer.playWhenReady get() = exoPlayer.playWhenReady
@ -83,24 +92,16 @@ private class ThinPlayerImpl(
exoPlayer.playWhenReady = value exoPlayer.playWhenReady = value
} }
override val currentPosition: Long get() = exoPlayer.currentPosition 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 val audioSessionId: Int get() = exoPlayer.audioSessionId
override var pauseAtEndOfMediaItems: Boolean
get() = exoPlayer.pauseAtEndOfMediaItems
set(value) {
exoPlayer.pauseAtEndOfMediaItems = value
}
override fun attach(listener: Player.Listener) { override fun attach() {
exoPlayer.addListener(listener) exoPlayer.addListener(this)
replayGainProcessor.attach() replayGainProcessor.attach()
queuer.attach()
} }
override fun release() { override fun release() {
queuer.release()
replayGainProcessor.release() replayGainProcessor.release()
exoPlayer.release() exoPlayer.release()
} }
@ -111,5 +112,33 @@ private class ThinPlayerImpl(
override fun seekTo(positionMs: Long) = exoPlayer.seekTo(positionMs) 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)
}
} }

View file

@ -42,7 +42,7 @@ import dagger.hilt.components.SingletonComponent
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
interface PlayerModule { interface PlayerModule {
@Binds fun playerFactory(factory: PlayerFactoryImpl): PlayerFactory @Binds fun playerKernelFactory(factory: PlayerKernelFactoryImpl): PlayerKernel.Factory
} }
@Module @Module

View file

@ -54,29 +54,31 @@ import org.oxycblt.auxio.util.logE
class PlayerStateHolder( class PlayerStateHolder(
private val context: Context, private val context: Context,
playerFactory: PlayerFactory, playerKernelFactory: PlayerKernel.Factory,
private val playbackManager: PlaybackStateManager, private val playbackManager: PlaybackStateManager,
private val persistenceRepository: PersistenceRepository, private val persistenceRepository: PersistenceRepository,
private val playbackSettings: PlaybackSettings, private val playbackSettings: PlaybackSettings,
private val commandFactory: PlaybackCommand.Factory, private val commandFactory: PlaybackCommand.Factory,
private val replayGainProcessor: ReplayGainAudioProcessor, private val replayGainProcessor: ReplayGainAudioProcessor,
gaplessQueuerFactory: Queuer.Factory,
private val musicRepository: MusicRepository, private val musicRepository: MusicRepository,
private val imageSettings: ImageSettings, private val imageSettings: ImageSettings
) : ) :
PlaybackStateHolder, PlaybackStateHolder,
Player.Listener, PlayerKernel.Listener,
Queuer.Listener,
MusicRepository.UpdateListener, MusicRepository.UpdateListener,
ImageSettings.Listener, ImageSettings.Listener {
PlaybackSettings.Listener {
class Factory class Factory
@Inject @Inject
constructor( constructor(
private val playbackManager: PlaybackStateManager, private val playbackManager: PlaybackStateManager,
private val persistenceRepository: PersistenceRepository, private val persistenceRepository: PersistenceRepository,
private val playbackSettings: PlaybackSettings, private val playbackSettings: PlaybackSettings,
private val playerFactory: PlayerFactory, private val playerFactory: PlayerKernel.Factory,
private val commandFactory: PlaybackCommand.Factory, private val commandFactory: PlaybackCommand.Factory,
private val replayGainProcessor: ReplayGainAudioProcessor, private val replayGainProcessor: ReplayGainAudioProcessor,
private val gaplessQueuerFactory: Queuer.Factory,
private val musicRepository: MusicRepository, private val musicRepository: MusicRepository,
private val imageSettings: ImageSettings, private val imageSettings: ImageSettings,
) { ) {
@ -89,6 +91,7 @@ class PlayerStateHolder(
playbackSettings, playbackSettings,
commandFactory, commandFactory,
replayGainProcessor, replayGainProcessor,
gaplessQueuerFactory,
musicRepository, musicRepository,
imageSettings) imageSettings)
} }
@ -99,15 +102,13 @@ class PlayerStateHolder(
private val restoreScope = CoroutineScope(Dispatchers.IO + saveJob) private val restoreScope = CoroutineScope(Dispatchers.IO + saveJob)
private var currentSaveJob: Job? = null private var currentSaveJob: Job? = null
private var openAudioEffectSession = false private var openAudioEffectSession = false
private val player = playerFactory.create(context) private val player = playerKernelFactory.create(context, this, gaplessQueuerFactory, this)
private val queuer = player.intoQueuer(GaplessQueuer.Factory)
var sessionOngoing = false var sessionOngoing = false
private set private set
fun attach() { fun attach() {
player.attach(this) player.attach()
playbackSettings.registerListener(this)
imageSettings.registerListener(this) imageSettings.registerListener(this)
playbackManager.registerStateHolder(this) playbackManager.registerStateHolder(this)
musicRepository.addUpdateListener(this) musicRepository.addUpdateListener(this)
@ -116,7 +117,6 @@ class PlayerStateHolder(
fun release() { fun release() {
saveJob.cancel() saveJob.cancel()
player.release() player.release()
playbackSettings.unregisterListener(this)
playbackManager.unregisterStateHolder(this) playbackManager.unregisterStateHolder(this)
musicRepository.removeUpdateListener(this) musicRepository.removeUpdateListener(this)
replayGainProcessor.release() replayGainProcessor.release()
@ -129,7 +129,7 @@ class PlayerStateHolder(
override val progression: Progression override val progression: Progression
get() { 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 duration = mediaItem.mediaMetadata.extras?.getLong("durationMs") ?: Long.MAX_VALUE
val clampedPosition = player.currentPosition.coerceAtLeast(0).coerceAtMost(duration) val clampedPosition = player.currentPosition.coerceAtLeast(0).coerceAtMost(duration)
return Progression.from(player.playWhenReady, player.isPlaying, clampedPosition) return Progression.from(player.playWhenReady, player.isPlaying, clampedPosition)
@ -137,7 +137,7 @@ class PlayerStateHolder(
override val repeatMode override val repeatMode
get() = get() =
when (val repeatMode = player.repeatMode) { when (val repeatMode = player.queuer.repeatMode) {
Player.REPEAT_MODE_OFF -> RepeatMode.NONE Player.REPEAT_MODE_OFF -> RepeatMode.NONE
Player.REPEAT_MODE_ONE -> RepeatMode.TRACK Player.REPEAT_MODE_ONE -> RepeatMode.TRACK
Player.REPEAT_MODE_ALL -> RepeatMode.ALL Player.REPEAT_MODE_ALL -> RepeatMode.ALL
@ -148,9 +148,9 @@ class PlayerStateHolder(
get() = player.audioSessionId get() = player.audioSessionId
override fun resolveQueue(): RawQueue { override fun resolveQueue(): RawQueue {
val heap = queuer.computeHeap() val heap = player.queuer.computeHeap()
val shuffledMapping = if (queuer.shuffleModeEnabled) queuer.computeMapping() else emptyList() val shuffledMapping = if (player.queuer.shuffleModeEnabled) player.queuer.computeMapping() else emptyList()
return RawQueue(heap.mapNotNull { it.song }, shuffledMapping, queuer.currentMediaItemIndex) return RawQueue(heap.mapNotNull { it.song }, shuffledMapping, player.queuer.currentMediaItemIndex)
} }
override fun handleDeferred(action: DeferredPlayback): Boolean { override fun handleDeferred(action: DeferredPlayback): Boolean {
@ -213,13 +213,12 @@ class PlayerStateHolder(
} }
override fun repeatMode(repeatMode: RepeatMode) { override fun repeatMode(repeatMode: RepeatMode) {
player.repeatMode = player.queuer.repeatMode =
when (repeatMode) { when (repeatMode) {
RepeatMode.NONE -> Player.REPEAT_MODE_OFF RepeatMode.NONE -> Player.REPEAT_MODE_OFF
RepeatMode.ALL -> Player.REPEAT_MODE_ALL RepeatMode.ALL -> Player.REPEAT_MODE_ALL
RepeatMode.TRACK -> Player.REPEAT_MODE_ONE RepeatMode.TRACK -> Player.REPEAT_MODE_ONE
} }
updatePauseOnRepeat()
playbackManager.ack(this, StateAck.RepeatModeChanged) playbackManager.ack(this, StateAck.RepeatModeChanged)
deferSave() deferSave()
} }
@ -231,14 +230,14 @@ class PlayerStateHolder(
command.song command.song
?.let { command.queue.indexOf(it) } ?.let { command.queue.indexOf(it) }
.also { check(it != -1) { "Start song not in queue" } } .also { check(it != -1) { "Start song not in queue" } }
queuer.prepareNew(mediaItems, startIndex, command.shuffled) player.queuer.prepareNew(mediaItems, startIndex, command.shuffled)
player.play() player.play()
playbackManager.ack(this, StateAck.NewPlayback) playbackManager.ack(this, StateAck.NewPlayback)
deferSave() deferSave()
} }
override fun shuffled(shuffled: Boolean) { override fun shuffled(shuffled: Boolean) {
queuer.shuffled(shuffled) player.queuer.shuffled(shuffled)
playbackManager.ack(this, StateAck.QueueReordered) playbackManager.ack(this, StateAck.QueueReordered)
deferSave() deferSave()
} }
@ -247,13 +246,13 @@ class PlayerStateHolder(
// Replicate the old pseudo-circular queue behavior when no repeat option is implemented. // 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 // 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. // wrap around the queue, albeit playback will be paused.
if (player.repeatMode == Player.REPEAT_MODE_ALL || queuer.hasNextMediaItem()) { if (player.queuer.repeatMode == Player.REPEAT_MODE_ALL || player.queuer.hasNextMediaItem()) {
queuer.seekToNext() player.queuer.seekToNext()
if (!playbackSettings.rememberPause) { if (!playbackSettings.rememberPause) {
player.play() player.play()
} }
} else { } 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 // TODO: Dislike the UX implications of this, I feel should I bite the bullet
// and switch to dynamic skip enable/disable? // and switch to dynamic skip enable/disable?
if (!playbackSettings.rememberPause) { if (!playbackSettings.rememberPause) {
@ -266,9 +265,9 @@ class PlayerStateHolder(
override fun prev() { override fun prev() {
if (playbackSettings.rewindWithPrev) { if (playbackSettings.rewindWithPrev) {
queuer.seekToPrevious() player.queuer.seekToPrevious()
} else if (queuer.hasPreviousMediaItem()) { } else if (player.queuer.hasPreviousMediaItem()) {
queuer.seekToPreviousMediaItem() player.queuer.seekToPreviousMediaItem()
} else { } else {
player.seekTo(0) player.seekTo(0)
} }
@ -280,12 +279,12 @@ class PlayerStateHolder(
} }
override fun goto(index: Int) { override fun goto(index: Int) {
val indices = queuer.computeMapping() val indices = player.queuer.computeMapping()
if (indices.isEmpty()) { if (indices.isEmpty()) {
return return
} }
val trueIndex = indices[index] val trueIndex = indices[index]
queuer.goto(trueIndex) player.queuer.goto(trueIndex)
if (!playbackSettings.rememberPause) { if (!playbackSettings.rememberPause) {
player.play() player.play()
} }
@ -294,19 +293,19 @@ class PlayerStateHolder(
} }
override fun playNext(songs: List<Song>, ack: StateAck.PlayNext) { override fun playNext(songs: List<Song>, ack: StateAck.PlayNext) {
queuer.addBottomMediaItems(songs.map { it.buildMediaItem() }) player.queuer.addBottomMediaItems(songs.map { it.buildMediaItem() })
playbackManager.ack(this, ack) playbackManager.ack(this, ack)
deferSave() deferSave()
} }
override fun addToQueue(songs: List<Song>, ack: StateAck.AddToQueue) { override fun addToQueue(songs: List<Song>, ack: StateAck.AddToQueue) {
queuer.addTopMediaItems(songs.map { it.buildMediaItem() }) player.queuer.addTopMediaItems(songs.map { it.buildMediaItem() })
playbackManager.ack(this, ack) playbackManager.ack(this, ack)
deferSave() deferSave()
} }
override fun move(from: Int, to: Int, ack: StateAck.Move) { override fun move(from: Int, to: Int, ack: StateAck.Move) {
val indices = queuer.computeMapping() val indices = player.queuer.computeMapping()
if (indices.isEmpty()) { if (indices.isEmpty()) {
return return
} }
@ -314,20 +313,20 @@ class PlayerStateHolder(
val trueFrom = indices[from] val trueFrom = indices[from]
val trueTo = indices[to] val trueTo = indices[to]
queuer.moveMediaItem(trueFrom, trueTo) player.queuer.moveMediaItem(trueFrom, trueTo)
playbackManager.ack(this, ack) playbackManager.ack(this, ack)
deferSave() deferSave()
} }
override fun remove(at: Int, ack: StateAck.Remove) { override fun remove(at: Int, ack: StateAck.Remove) {
val indices = queuer.computeMapping() val indices = player.queuer.computeMapping()
if (indices.isEmpty()) { if (indices.isEmpty()) {
return return
} }
val trueIndex = indices[at] val trueIndex = indices[at]
val songWillChange = queuer.currentMediaItemIndex == trueIndex val songWillChange = player.queuer.currentMediaItemIndex == trueIndex
queuer.removeMediaItem(trueIndex) player.queuer.removeMediaItem(trueIndex)
if (songWillChange && !playbackSettings.rememberPause) { if (songWillChange && !playbackSettings.rememberPause) {
player.play() player.play()
} }
@ -347,7 +346,7 @@ class PlayerStateHolder(
sendEvent = true sendEvent = true
} }
if (rawQueue != resolveQueue()) { 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() player.pause()
sendEvent = true sendEvent = true
} }
@ -371,16 +370,14 @@ class PlayerStateHolder(
} }
override fun reset(ack: StateAck.NewPlayback) { override fun reset(ack: StateAck.NewPlayback) {
queuer.discard() player.queuer.discard()
playbackManager.ack(this, ack) playbackManager.ack(this, ack)
deferSave() deferSave()
} }
// --- PLAYER OVERRIDES --- // --- PLAYER OVERRIDES ---
override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) { override fun onPlayWhenReadyChanged() {
super.onPlayWhenReadyChanged(playWhenReady, reason)
if (player.playWhenReady) { if (player.playWhenReady) {
// Mark that we have started playing so that the notification can now be posted. // Mark that we have started playing so that the notification can now be posted.
logD("Player has started playing") logD("Player has started playing")
@ -398,40 +395,23 @@ class PlayerStateHolder(
broadcastAudioEffectAction(AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION) broadcastAudioEffectAction(AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION)
openAudioEffectSession = false openAudioEffectSession = false
} }
playbackManager.ack(this, StateAck.ProgressionChanged)
} }
override fun onPlaybackStateChanged(playbackState: Int) { override fun onIsPlayingChanged() {
super.onPlaybackStateChanged(playbackState) playbackManager.ack(this, StateAck.ProgressionChanged)
if (playbackState == Player.STATE_ENDED && player.repeatMode == Player.REPEAT_MODE_OFF) {
goto(0)
player.pause()
}
} }
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) { override fun onPositionDiscontinuity() {
super.onMediaItemTransition(mediaItem, reason) playbackManager.ack(this, StateAck.ProgressionChanged)
if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_AUTO) {
playbackManager.ack(this, StateAck.IndexMoved)
}
} }
override fun onEvents(player: Player, events: Player.Events) { override fun onAutoTransition() {
super.onEvents(player, events) playbackManager.ack(this, StateAck.IndexMoved)
// 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 onPlayerError(error: PlaybackException) { override fun onError(error: PlaybackException) {
// TODO: Replace with no skipping and a notification instead // TODO: Replace with no skipping and a notification instead
// If there's any issue, just go to the next song. // If there's any issue, just go to the next song.
logE("Player error occurred") 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 --- // --- OVERRIDES ---
private fun save(cb: () -> Unit) { private fun save(cb: () -> Unit) {

View file

@ -1,6 +1,8 @@
package org.oxycblt.auxio.playback.player package org.oxycblt.auxio.playback.player
import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem
import androidx.media3.common.Player
import androidx.media3.common.Player.RepeatMode
import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.ExoPlayer
interface Queuer { interface Queuer {
@ -8,6 +10,11 @@ interface Queuer {
val currentMediaItemIndex: Int val currentMediaItemIndex: Int
val shuffleModeEnabled: Boolean val shuffleModeEnabled: Boolean
@get:RepeatMode var repeatMode: Int
fun attach()
fun release()
fun goto(mediaItemIndex: Int) fun goto(mediaItemIndex: Int)
fun seekToNext() fun seekToNext()
@ -32,7 +39,11 @@ interface Queuer {
fun addBottomMediaItems(mediaItems: List<MediaItem>) fun addBottomMediaItems(mediaItems: List<MediaItem>)
fun shuffled(shuffled: Boolean) fun shuffled(shuffled: Boolean)
interface Listener {
fun onAutoTransition()
}
interface Factory { interface Factory {
fun create(exoPlayer: ExoPlayer): Queuer fun create(exoPlayer: ExoPlayer, listener: Listener): Queuer
} }
} }