playback: move player into module

This commit is contained in:
Alexander Capehart 2024-09-23 15:15:06 -06:00
parent e32c687c61
commit 5d1111b12a
6 changed files with 208 additions and 159 deletions

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.playback.service package org.oxycblt.auxio.playback.player
import androidx.media3.common.C import androidx.media3.common.C
import androidx.media3.exoplayer.source.ShuffleOrder import androidx.media3.exoplayer.source.ShuffleOrder

View file

@ -1,30 +1,17 @@
package org.oxycblt.auxio.playback.service 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.exoplayer.ExoPlayer import androidx.media3.exoplayer.ExoPlayer
import org.oxycblt.auxio.playback.PlaybackSettings
class GaplessPlayerKernel(private val exoPlayer: ExoPlayer, private val playbackSettings: PlaybackSettings) : PlayerKernel, PlaybackSettings.Listener { class GaplessQueuer private constructor(private val exoPlayer: ExoPlayer) : Queuer {
init { data object Factory : Queuer.Factory {
playbackSettings.registerListener(this) override fun create(exoPlayer: ExoPlayer) = GaplessQueuer(exoPlayer)
} }
override val currentMediaItem: MediaItem? = exoPlayer.currentMediaItem
override val isPlaying: Boolean = exoPlayer.isPlaying override val currentMediaItemIndex: Int = exoPlayer.currentMediaItemIndex
override var playWhenReady: Boolean = exoPlayer.playWhenReady override val shuffleModeEnabled: Boolean = exoPlayer.shuffleModeEnabled
set(value) {
field = value
exoPlayer.playWhenReady = value
}
override val currentPosition: Long = exoPlayer.currentPosition
@get:Player.RepeatMode override var repeatMode: Int = exoPlayer.repeatMode
set(value) {
field = value
exoPlayer.repeatMode = value
updatePauseOnRepeat()
}
override val audioSessionId: Int = exoPlayer.audioSessionId
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) }
@ -72,20 +59,6 @@ class GaplessPlayerKernel(private val exoPlayer: ExoPlayer, private val playback
override fun computeFirstMediaItemIndex() = override fun computeFirstMediaItemIndex() =
exoPlayer.currentTimeline.getFirstWindowIndex(exoPlayer.shuffleModeEnabled) exoPlayer.currentTimeline.getFirstWindowIndex(exoPlayer.shuffleModeEnabled)
override fun addListener(player: Player.Listener) = exoPlayer.addListener(player)
override fun removeListener(player: Player.Listener) = exoPlayer.removeListener(player)
override fun release() {
exoPlayer.release()
playbackSettings.unregisterListener(this)
}
override val currentMediaItem: MediaItem? = exoPlayer.currentMediaItem
override val currentMediaItemIndex: Int = exoPlayer.currentMediaItemIndex
override val shuffleModeEnabled: Boolean = exoPlayer.shuffleModeEnabled
override fun play() = exoPlayer.play()
override fun pause() = exoPlayer.pause()
override fun seekTo(positionMs: Long) = exoPlayer.seekTo(positionMs)
override fun goto(mediaItemIndex: Int) = exoPlayer.seekTo(mediaItemIndex, C.TIME_UNSET) override fun goto(mediaItemIndex: Int) = exoPlayer.seekTo(mediaItemIndex, C.TIME_UNSET)
override fun seekToNext() = exoPlayer.seekToNext() override fun seekToNext() = exoPlayer.seekToNext()
@ -164,18 +137,9 @@ class GaplessPlayerKernel(private val exoPlayer: ExoPlayer, private val playback
if (exoPlayer.shuffleModeEnabled) { if (exoPlayer.shuffleModeEnabled) {
// Have to manually refresh the shuffle seed and anchor it to the new current songs // Have to manually refresh the shuffle seed and anchor it to the new current songs
exoPlayer.setShuffleOrder( exoPlayer.setShuffleOrder(
BetterShuffleOrder(exoPlayer.mediaItemCount, exoPlayer.currentMediaItemIndex)) BetterShuffleOrder(exoPlayer.mediaItemCount, exoPlayer.currentMediaItemIndex)
)
} }
} }
override fun onPauseOnRepeatChanged() {
super.onPauseOnRepeatChanged()
updatePauseOnRepeat()
}
private fun updatePauseOnRepeat() {
exoPlayer.pauseAtEndOfMediaItems =
exoPlayer.repeatMode == Player.REPEAT_MODE_ONE && playbackSettings.pauseOnRepeat
}
} }

View file

@ -0,0 +1,115 @@
package org.oxycblt.auxio.playback.player
import android.content.Context
import androidx.media3.common.AudioAttributes
import androidx.media3.common.C
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
import androidx.media3.exoplayer.audio.AudioCapabilities
import androidx.media3.exoplayer.audio.MediaCodecAudioRenderer
import androidx.media3.exoplayer.mediacodec.MediaCodecSelector
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 {
val isPlaying: Boolean
var playWhenReady: Boolean
val currentPosition: Long
@get:RepeatMode var repeatMode: Int
val audioSessionId: Int
var pauseAtEndOfMediaItems: Boolean
fun attach(listener: Player.Listener)
fun release()
fun play()
fun pause()
fun seekTo(positionMs: Long)
fun intoQueuer(queuerFactory: Queuer.Factory): Queuer
}
class PlayerFactoryImpl(@Inject private val mediaSourceFactory: MediaSource.Factory, @Inject private val replayGainProcessor: ReplayGainAudioProcessor) : PlayerFactory {
override fun create(context: Context): ThinPlayer {
// Since Auxio is a music player, only specify an audio renderer to save
// battery/apk size/cache size
val audioRenderer = RenderersFactory { handler, _, audioListener, _, _ ->
arrayOf(
FfmpegAudioRenderer(handler, audioListener, replayGainProcessor),
MediaCodecAudioRenderer(
context,
MediaCodecSelector.DEFAULT,
handler,
audioListener,
AudioCapabilities.DEFAULT_AUDIO_CAPABILITIES,
replayGainProcessor))
}
val exoPlayer =
ExoPlayer.Builder(context, audioRenderer)
.setMediaSourceFactory(mediaSourceFactory)
// Enable automatic WakeLock support
.setWakeMode(C.WAKE_MODE_LOCAL)
.setAudioAttributes(
// Signal that we are a music player.
AudioAttributes.Builder()
.setUsage(C.USAGE_MEDIA)
.setContentType(C.AUDIO_CONTENT_TYPE_MUSIC)
.build(),
true)
.build()
return ThinPlayerImpl(exoPlayer, replayGainProcessor)
}
}
private class ThinPlayerImpl(
private val exoPlayer: ExoPlayer,
private val replayGainProcessor: ReplayGainAudioProcessor
) : ThinPlayer {
override val isPlaying: Boolean get() = exoPlayer.isPlaying
override var playWhenReady: Boolean
get() = exoPlayer.playWhenReady
set(value) {
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)
replayGainProcessor.attach()
}
override fun release() {
replayGainProcessor.release()
exoPlayer.release()
}
override fun play() = exoPlayer.play()
override fun pause() = exoPlayer.pause()
override fun seekTo(positionMs: Long) = exoPlayer.seekTo(positionMs)
override fun intoQueuer(queuerFactory: Queuer.Factory) = queuerFactory.create(exoPlayer)
}

View file

@ -16,23 +16,14 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.playback.service package org.oxycblt.auxio.playback.player
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.media.audiofx.AudioEffect import android.media.audiofx.AudioEffect
import androidx.media3.common.AudioAttributes
import androidx.media3.common.C
import androidx.media3.common.MediaItem 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.decoder.ffmpeg.FfmpegAudioRenderer
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.RenderersFactory
import androidx.media3.exoplayer.audio.AudioCapabilities
import androidx.media3.exoplayer.audio.MediaCodecAudioRenderer
import androidx.media3.exoplayer.mediacodec.MediaCodecSelector
import androidx.media3.exoplayer.source.MediaSource
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -61,9 +52,9 @@ import org.oxycblt.auxio.playback.state.StateAck
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logE import org.oxycblt.auxio.util.logE
class ExoPlaybackStateHolder( class PlayerStateHolder(
private val context: Context, private val context: Context,
private val kernel: PlayerKernel, private val playerFactory: PlayerFactory,
private val playbackManager: PlaybackStateManager, private val playbackManager: PlaybackStateManager,
private val persistenceRepository: PersistenceRepository, private val persistenceRepository: PersistenceRepository,
private val playbackSettings: PlaybackSettings, private val playbackSettings: PlaybackSettings,
@ -75,52 +66,24 @@ class ExoPlaybackStateHolder(
PlaybackStateHolder, PlaybackStateHolder,
Player.Listener, Player.Listener,
MusicRepository.UpdateListener, MusicRepository.UpdateListener,
ImageSettings.Listener { ImageSettings.Listener,
PlaybackSettings.Listener {
class Factory class Factory
@Inject @Inject
constructor( constructor(
@ApplicationContext private val context: Context,
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 commandFactory: PlaybackCommand.Factory, private val commandFactory: PlaybackCommand.Factory,
private val mediaSourceFactory: MediaSource.Factory,
private val replayGainProcessor: ReplayGainAudioProcessor, private val replayGainProcessor: ReplayGainAudioProcessor,
private val musicRepository: MusicRepository, private val musicRepository: MusicRepository,
private val imageSettings: ImageSettings, private val imageSettings: ImageSettings,
) { ) {
fun create(): ExoPlaybackStateHolder { fun create(context: Context): PlayerStateHolder {
// Since Auxio is a music player, only specify an audio renderer to save return PlayerStateHolder(
// battery/apk size/cache size
val audioRenderer = RenderersFactory { handler, _, audioListener, _, _ ->
arrayOf(
FfmpegAudioRenderer(handler, audioListener, replayGainProcessor),
MediaCodecAudioRenderer(
context,
MediaCodecSelector.DEFAULT,
handler,
audioListener,
AudioCapabilities.DEFAULT_AUDIO_CAPABILITIES,
replayGainProcessor))
}
val exoPlayer =
ExoPlayer.Builder(context, audioRenderer)
.setMediaSourceFactory(mediaSourceFactory)
// Enable automatic WakeLock support
.setWakeMode(C.WAKE_MODE_LOCAL)
.setAudioAttributes(
// Signal that we are a music player.
AudioAttributes.Builder()
.setUsage(C.USAGE_MEDIA)
.setContentType(C.AUDIO_CONTENT_TYPE_MUSIC)
.build(),
true)
.build()
return ExoPlaybackStateHolder(
context, context,
GaplessPlayerKernel(exoPlayer, playbackSettings), playerFactory,
playbackManager, playbackManager,
persistenceRepository, persistenceRepository,
playbackSettings, playbackSettings,
@ -136,25 +99,29 @@ class ExoPlaybackStateHolder(
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 queuer = player.intoQueuer(GaplessQueuer.Factory)
var sessionOngoing = false var sessionOngoing = false
private set private set
fun attach() { fun attach() {
player.attach(this)
playbackSettings.registerListener(this)
imageSettings.registerListener(this) imageSettings.registerListener(this)
kernel.addListener(this)
playbackManager.registerStateHolder(this) playbackManager.registerStateHolder(this)
musicRepository.addUpdateListener(this) musicRepository.addUpdateListener(this)
} }
fun release() { fun release() {
saveJob.cancel() saveJob.cancel()
kernel.removeListener(this) player.release()
playbackSettings.unregisterListener(this)
playbackManager.unregisterStateHolder(this) playbackManager.unregisterStateHolder(this)
musicRepository.removeUpdateListener(this) musicRepository.removeUpdateListener(this)
replayGainProcessor.release() replayGainProcessor.release()
imageSettings.unregisterListener(this) imageSettings.unregisterListener(this)
kernel.release() player.release()
} }
override var parent: MusicParent? = null override var parent: MusicParent? = null
@ -162,15 +129,15 @@ class ExoPlaybackStateHolder(
override val progression: Progression override val progression: Progression
get() { get() {
val mediaItem = kernel.currentMediaItem ?: return Progression.nil() val mediaItem = 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 = kernel.currentPosition.coerceAtLeast(0).coerceAtMost(duration) val clampedPosition = player.currentPosition.coerceAtLeast(0).coerceAtMost(duration)
return Progression.from(kernel.playWhenReady, kernel.isPlaying, clampedPosition) return Progression.from(player.playWhenReady, player.isPlaying, clampedPosition)
} }
override val repeatMode override val repeatMode
get() = get() =
when (val repeatMode = kernel.repeatMode) { when (val repeatMode = player.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
@ -178,12 +145,12 @@ class ExoPlaybackStateHolder(
} }
override val audioSessionId: Int override val audioSessionId: Int
get() = kernel.audioSessionId get() = player.audioSessionId
override fun resolveQueue(): RawQueue { override fun resolveQueue(): RawQueue {
val heap = kernel.computeHeap() val heap = queuer.computeHeap()
val shuffledMapping = if (kernel.shuffleModeEnabled) kernel.computeMapping() else emptyList() val shuffledMapping = if (queuer.shuffleModeEnabled) queuer.computeMapping() else emptyList()
return RawQueue(heap.mapNotNull { it.song }, shuffledMapping, kernel.currentMediaItemIndex) return RawQueue(heap.mapNotNull { it.song }, shuffledMapping, queuer.currentMediaItemIndex)
} }
override fun handleDeferred(action: DeferredPlayback): Boolean { override fun handleDeferred(action: DeferredPlayback): Boolean {
@ -236,22 +203,23 @@ class ExoPlaybackStateHolder(
} }
override fun playing(playing: Boolean) { override fun playing(playing: Boolean) {
kernel.playWhenReady = playing player.playWhenReady = playing
} }
override fun seekTo(positionMs: Long) { override fun seekTo(positionMs: Long) {
kernel.seekTo(positionMs) player.seekTo(positionMs)
deferSave() deferSave()
// Ack handled w/ExoPlayer events // Ack handled w/ExoPlayer events
} }
override fun repeatMode(repeatMode: RepeatMode) { override fun repeatMode(repeatMode: RepeatMode) {
kernel.repeatMode = player.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()
} }
@ -263,14 +231,14 @@ class ExoPlaybackStateHolder(
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" } }
kernel.prepareNew(mediaItems, startIndex, command.shuffled) queuer.prepareNew(mediaItems, startIndex, command.shuffled)
kernel.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) {
kernel.shuffled(shuffled) queuer.shuffled(shuffled)
playbackManager.ack(this, StateAck.QueueReordered) playbackManager.ack(this, StateAck.QueueReordered)
deferSave() deferSave()
} }
@ -279,17 +247,17 @@ class ExoPlaybackStateHolder(
// 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 (kernel.repeatMode == Player.REPEAT_MODE_ALL || kernel.hasNextMediaItem()) { if (player.repeatMode == Player.REPEAT_MODE_ALL || queuer.hasNextMediaItem()) {
kernel.seekToNext() queuer.seekToNext()
if (!playbackSettings.rememberPause) { if (!playbackSettings.rememberPause) {
kernel.play() player.play()
} }
} else { } else {
kernel.goto(kernel.computeFirstMediaItemIndex()) queuer.goto(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) {
kernel.pause() player.pause()
} }
} }
playbackManager.ack(this, StateAck.IndexMoved) playbackManager.ack(this, StateAck.IndexMoved)
@ -298,47 +266,47 @@ class ExoPlaybackStateHolder(
override fun prev() { override fun prev() {
if (playbackSettings.rewindWithPrev) { if (playbackSettings.rewindWithPrev) {
kernel.seekToPrevious() queuer.seekToPrevious()
} else if (kernel.hasPreviousMediaItem()) { } else if (queuer.hasPreviousMediaItem()) {
kernel.seekToPreviousMediaItem() queuer.seekToPreviousMediaItem()
} else { } else {
kernel.seekTo(0) player.seekTo(0)
} }
if (!playbackSettings.rememberPause) { if (!playbackSettings.rememberPause) {
kernel.play() player.play()
} }
playbackManager.ack(this, StateAck.IndexMoved) playbackManager.ack(this, StateAck.IndexMoved)
deferSave() deferSave()
} }
override fun goto(index: Int) { override fun goto(index: Int) {
val indices = kernel.computeMapping() val indices = queuer.computeMapping()
if (indices.isEmpty()) { if (indices.isEmpty()) {
return return
} }
val trueIndex = indices[index] val trueIndex = indices[index]
kernel.goto(trueIndex) queuer.goto(trueIndex)
if (!playbackSettings.rememberPause) { if (!playbackSettings.rememberPause) {
kernel.play() player.play()
} }
playbackManager.ack(this, StateAck.IndexMoved) playbackManager.ack(this, StateAck.IndexMoved)
deferSave() deferSave()
} }
override fun playNext(songs: List<Song>, ack: StateAck.PlayNext) { override fun playNext(songs: List<Song>, ack: StateAck.PlayNext) {
kernel.addBottomMediaItems(songs.map { it.buildMediaItem() }) 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) {
kernel.addTopMediaItems(songs.map { it.buildMediaItem() }) 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 = kernel.computeMapping() val indices = queuer.computeMapping()
if (indices.isEmpty()) { if (indices.isEmpty()) {
return return
} }
@ -346,22 +314,22 @@ class ExoPlaybackStateHolder(
val trueFrom = indices[from] val trueFrom = indices[from]
val trueTo = indices[to] val trueTo = indices[to]
kernel.moveMediaItem(trueFrom, trueTo) 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 = kernel.computeMapping() val indices = queuer.computeMapping()
if (indices.isEmpty()) { if (indices.isEmpty()) {
return return
} }
val trueIndex = indices[at] val trueIndex = indices[at]
val songWillChange = kernel.currentMediaItemIndex == trueIndex val songWillChange = queuer.currentMediaItemIndex == trueIndex
kernel.removeMediaItem(trueIndex) queuer.removeMediaItem(trueIndex)
if (songWillChange && !playbackSettings.rememberPause) { if (songWillChange && !playbackSettings.rememberPause) {
kernel.play() player.play()
} }
playbackManager.ack(this, ack) playbackManager.ack(this, ack)
deferSave() deferSave()
@ -379,8 +347,8 @@ class ExoPlaybackStateHolder(
sendEvent = true sendEvent = true
} }
if (rawQueue != resolveQueue()) { if (rawQueue != resolveQueue()) {
kernel.prepareSaved(rawQueue.heap.map { it.buildMediaItem() }, rawQueue.shuffledMapping, rawQueue.heapIndex, rawQueue.isShuffled) queuer.prepareSaved(rawQueue.heap.map { it.buildMediaItem() }, rawQueue.shuffledMapping, rawQueue.heapIndex, rawQueue.isShuffled)
kernel.pause() player.pause()
sendEvent = true sendEvent = true
} }
if (sendEvent) { if (sendEvent) {
@ -403,7 +371,7 @@ class ExoPlaybackStateHolder(
} }
override fun reset(ack: StateAck.NewPlayback) { override fun reset(ack: StateAck.NewPlayback) {
kernel.discard() queuer.discard()
playbackManager.ack(this, ack) playbackManager.ack(this, ack)
deferSave() deferSave()
} }
@ -413,7 +381,7 @@ class ExoPlaybackStateHolder(
override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) { override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) {
super.onPlayWhenReadyChanged(playWhenReady, reason) super.onPlayWhenReadyChanged(playWhenReady, reason)
if (kernel.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")
sessionOngoing = true sessionOngoing = true
@ -435,9 +403,9 @@ class ExoPlaybackStateHolder(
override fun onPlaybackStateChanged(playbackState: Int) { override fun onPlaybackStateChanged(playbackState: Int) {
super.onPlaybackStateChanged(playbackState) super.onPlaybackStateChanged(playbackState)
if (playbackState == Player.STATE_ENDED && kernel.repeatMode == Player.REPEAT_MODE_OFF) { if (playbackState == Player.STATE_ENDED && player.repeatMode == Player.REPEAT_MODE_OFF) {
goto(0) goto(0)
kernel.pause() player.pause()
} }
} }
@ -490,6 +458,20 @@ class ExoPlaybackStateHolder(
} }
} }
// --- 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) { private fun save(cb: () -> Unit) {
saveJob { saveJob {
persistenceRepository.saveState(playbackManager.toSavedState()) persistenceRepository.saveState(playbackManager.toSavedState())

View file

@ -1,29 +1,13 @@
package org.oxycblt.auxio.playback.service package org.oxycblt.auxio.playback.player
import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem
import androidx.media3.common.Player import androidx.media3.exoplayer.ExoPlayer
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.state.RawQueue
import org.oxycblt.auxio.playback.state.RepeatMode
interface PlayerKernel { interface Queuer {
// REPLICAS
val isPlaying: Boolean
var playWhenReady: Boolean
val currentPosition: Long
@get:Player.RepeatMode var repeatMode: Int
val audioSessionId: Int
val currentMediaItem: MediaItem? val currentMediaItem: MediaItem?
val currentMediaItemIndex: Int val currentMediaItemIndex: Int
val shuffleModeEnabled: Boolean val shuffleModeEnabled: Boolean
fun addListener(player: Player.Listener)
fun removeListener(player: Player.Listener)
fun release()
fun play()
fun pause()
fun seekTo(positionMs: Long)
fun goto(mediaItemIndex: Int) fun goto(mediaItemIndex: Int)
fun seekToNext() fun seekToNext()
@ -47,5 +31,8 @@ interface PlayerKernel {
fun addTopMediaItems(mediaItems: List<MediaItem>) fun addTopMediaItems(mediaItems: List<MediaItem>)
fun addBottomMediaItems(mediaItems: List<MediaItem>) fun addBottomMediaItems(mediaItems: List<MediaItem>)
fun shuffled(shuffled: Boolean) fun shuffled(shuffled: Boolean)
}
interface Factory {
fun create(exoPlayer: ExoPlayer): Queuer
}
}

View file

@ -25,6 +25,7 @@ import kotlinx.coroutines.Job
import org.oxycblt.auxio.ForegroundListener import org.oxycblt.auxio.ForegroundListener
import org.oxycblt.auxio.ForegroundServiceNotification import org.oxycblt.auxio.ForegroundServiceNotification
import org.oxycblt.auxio.IntegerTable import org.oxycblt.auxio.IntegerTable
import org.oxycblt.auxio.playback.player.PlayerStateHolder
import org.oxycblt.auxio.playback.state.DeferredPlayback import org.oxycblt.auxio.playback.state.DeferredPlayback
import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
@ -35,7 +36,7 @@ private constructor(
private val context: Context, private val context: Context,
private val foregroundListener: ForegroundListener, private val foregroundListener: ForegroundListener,
private val playbackManager: PlaybackStateManager, private val playbackManager: PlaybackStateManager,
exoHolderFactory: ExoPlaybackStateHolder.Factory, playerHolderFactory: PlayerStateHolder.Factory,
sessionHolderFactory: MediaSessionHolder.Factory, sessionHolderFactory: MediaSessionHolder.Factory,
widgetComponentFactory: WidgetComponent.Factory, widgetComponentFactory: WidgetComponent.Factory,
systemReceiverFactory: SystemPlaybackReceiver.Factory, systemReceiverFactory: SystemPlaybackReceiver.Factory,
@ -44,7 +45,7 @@ private constructor(
@Inject @Inject
constructor( constructor(
private val playbackManager: PlaybackStateManager, private val playbackManager: PlaybackStateManager,
private val exoHolderFactory: ExoPlaybackStateHolder.Factory, private val exoHolderFactory: PlayerStateHolder.Factory,
private val sessionHolderFactory: MediaSessionHolder.Factory, private val sessionHolderFactory: MediaSessionHolder.Factory,
private val widgetComponentFactory: WidgetComponent.Factory, private val widgetComponentFactory: WidgetComponent.Factory,
private val systemReceiverFactory: SystemPlaybackReceiver.Factory, private val systemReceiverFactory: SystemPlaybackReceiver.Factory,
@ -61,7 +62,7 @@ private constructor(
} }
private val waitJob = Job() private val waitJob = Job()
private val exoHolder = exoHolderFactory.create() private val exoHolder = playerHolderFactory.create(context)
private val sessionHolder = sessionHolderFactory.create(context, foregroundListener) private val sessionHolder = sessionHolderFactory.create(context, foregroundListener)
private val widgetComponent = widgetComponentFactory.create(context) private val widgetComponent = widgetComponentFactory.create(context)
private val systemReceiver = systemReceiverFactory.create(context, widgetComponent) private val systemReceiver = systemReceiverFactory.create(context, widgetComponent)