playback: restructure repeat mode/listeners
This commit is contained in:
parent
b784250fed
commit
f245e33887
5 changed files with 163 additions and 113 deletions
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Reference in a new issue