playback: move player into module
This commit is contained in:
parent
e32c687c61
commit
5d1111b12a
6 changed files with 208 additions and 159 deletions
|
@ -16,7 +16,7 @@
|
|||
* 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.exoplayer.source.ShuffleOrder
|
|
@ -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.MediaItem
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import org.oxycblt.auxio.playback.PlaybackSettings
|
||||
|
||||
class GaplessPlayerKernel(private val exoPlayer: ExoPlayer, private val playbackSettings: PlaybackSettings) : PlayerKernel, PlaybackSettings.Listener {
|
||||
init {
|
||||
playbackSettings.registerListener(this)
|
||||
class GaplessQueuer private constructor(private val exoPlayer: ExoPlayer) : Queuer {
|
||||
data object Factory : Queuer.Factory {
|
||||
override fun create(exoPlayer: ExoPlayer) = GaplessQueuer(exoPlayer)
|
||||
}
|
||||
|
||||
override val isPlaying: Boolean = exoPlayer.isPlaying
|
||||
override var playWhenReady: Boolean = exoPlayer.playWhenReady
|
||||
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 val currentMediaItem: MediaItem? = exoPlayer.currentMediaItem
|
||||
override val currentMediaItemIndex: Int = exoPlayer.currentMediaItemIndex
|
||||
override val shuffleModeEnabled: Boolean = exoPlayer.shuffleModeEnabled
|
||||
|
||||
override fun computeHeap(): List<MediaItem> {
|
||||
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() =
|
||||
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 seekToNext() = exoPlayer.seekToNext()
|
||||
|
@ -164,18 +137,9 @@ class GaplessPlayerKernel(private val exoPlayer: ExoPlayer, private val playback
|
|||
if (exoPlayer.shuffleModeEnabled) {
|
||||
// Have to manually refresh the shuffle seed and anchor it to the new current songs
|
||||
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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -16,23 +16,14 @@
|
|||
* 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.Intent
|
||||
import android.media.audiofx.AudioEffect
|
||||
import androidx.media3.common.AudioAttributes
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.PlaybackException
|
||||
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 javax.inject.Inject
|
||||
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.logE
|
||||
|
||||
class ExoPlaybackStateHolder(
|
||||
class PlayerStateHolder(
|
||||
private val context: Context,
|
||||
private val kernel: PlayerKernel,
|
||||
private val playerFactory: PlayerFactory,
|
||||
private val playbackManager: PlaybackStateManager,
|
||||
private val persistenceRepository: PersistenceRepository,
|
||||
private val playbackSettings: PlaybackSettings,
|
||||
|
@ -75,52 +66,24 @@ class ExoPlaybackStateHolder(
|
|||
PlaybackStateHolder,
|
||||
Player.Listener,
|
||||
MusicRepository.UpdateListener,
|
||||
ImageSettings.Listener {
|
||||
ImageSettings.Listener,
|
||||
PlaybackSettings.Listener {
|
||||
class Factory
|
||||
@Inject
|
||||
constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val playbackManager: PlaybackStateManager,
|
||||
private val persistenceRepository: PersistenceRepository,
|
||||
private val playbackSettings: PlaybackSettings,
|
||||
private val playerFactory: PlayerFactory,
|
||||
private val commandFactory: PlaybackCommand.Factory,
|
||||
private val mediaSourceFactory: MediaSource.Factory,
|
||||
private val replayGainProcessor: ReplayGainAudioProcessor,
|
||||
private val musicRepository: MusicRepository,
|
||||
private val imageSettings: ImageSettings,
|
||||
) {
|
||||
fun create(): ExoPlaybackStateHolder {
|
||||
// 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 ExoPlaybackStateHolder(
|
||||
fun create(context: Context): PlayerStateHolder {
|
||||
return PlayerStateHolder(
|
||||
context,
|
||||
GaplessPlayerKernel(exoPlayer, playbackSettings),
|
||||
playerFactory,
|
||||
playbackManager,
|
||||
persistenceRepository,
|
||||
playbackSettings,
|
||||
|
@ -136,25 +99,29 @@ class ExoPlaybackStateHolder(
|
|||
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)
|
||||
|
||||
var sessionOngoing = false
|
||||
private set
|
||||
|
||||
fun attach() {
|
||||
player.attach(this)
|
||||
playbackSettings.registerListener(this)
|
||||
imageSettings.registerListener(this)
|
||||
kernel.addListener(this)
|
||||
playbackManager.registerStateHolder(this)
|
||||
musicRepository.addUpdateListener(this)
|
||||
}
|
||||
|
||||
fun release() {
|
||||
saveJob.cancel()
|
||||
kernel.removeListener(this)
|
||||
player.release()
|
||||
playbackSettings.unregisterListener(this)
|
||||
playbackManager.unregisterStateHolder(this)
|
||||
musicRepository.removeUpdateListener(this)
|
||||
replayGainProcessor.release()
|
||||
imageSettings.unregisterListener(this)
|
||||
kernel.release()
|
||||
player.release()
|
||||
}
|
||||
|
||||
override var parent: MusicParent? = null
|
||||
|
@ -162,15 +129,15 @@ class ExoPlaybackStateHolder(
|
|||
|
||||
override val progression: Progression
|
||||
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 clampedPosition = kernel.currentPosition.coerceAtLeast(0).coerceAtMost(duration)
|
||||
return Progression.from(kernel.playWhenReady, kernel.isPlaying, clampedPosition)
|
||||
val clampedPosition = player.currentPosition.coerceAtLeast(0).coerceAtMost(duration)
|
||||
return Progression.from(player.playWhenReady, player.isPlaying, clampedPosition)
|
||||
}
|
||||
|
||||
override val repeatMode
|
||||
get() =
|
||||
when (val repeatMode = kernel.repeatMode) {
|
||||
when (val repeatMode = player.repeatMode) {
|
||||
Player.REPEAT_MODE_OFF -> RepeatMode.NONE
|
||||
Player.REPEAT_MODE_ONE -> RepeatMode.TRACK
|
||||
Player.REPEAT_MODE_ALL -> RepeatMode.ALL
|
||||
|
@ -178,12 +145,12 @@ class ExoPlaybackStateHolder(
|
|||
}
|
||||
|
||||
override val audioSessionId: Int
|
||||
get() = kernel.audioSessionId
|
||||
get() = player.audioSessionId
|
||||
|
||||
override fun resolveQueue(): RawQueue {
|
||||
val heap = kernel.computeHeap()
|
||||
val shuffledMapping = if (kernel.shuffleModeEnabled) kernel.computeMapping() else emptyList()
|
||||
return RawQueue(heap.mapNotNull { it.song }, shuffledMapping, kernel.currentMediaItemIndex)
|
||||
val heap = queuer.computeHeap()
|
||||
val shuffledMapping = if (queuer.shuffleModeEnabled) queuer.computeMapping() else emptyList()
|
||||
return RawQueue(heap.mapNotNull { it.song }, shuffledMapping, queuer.currentMediaItemIndex)
|
||||
}
|
||||
|
||||
override fun handleDeferred(action: DeferredPlayback): Boolean {
|
||||
|
@ -236,22 +203,23 @@ class ExoPlaybackStateHolder(
|
|||
}
|
||||
|
||||
override fun playing(playing: Boolean) {
|
||||
kernel.playWhenReady = playing
|
||||
player.playWhenReady = playing
|
||||
}
|
||||
|
||||
override fun seekTo(positionMs: Long) {
|
||||
kernel.seekTo(positionMs)
|
||||
player.seekTo(positionMs)
|
||||
deferSave()
|
||||
// Ack handled w/ExoPlayer events
|
||||
}
|
||||
|
||||
override fun repeatMode(repeatMode: RepeatMode) {
|
||||
kernel.repeatMode =
|
||||
player.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()
|
||||
}
|
||||
|
@ -263,14 +231,14 @@ class ExoPlaybackStateHolder(
|
|||
command.song
|
||||
?.let { command.queue.indexOf(it) }
|
||||
.also { check(it != -1) { "Start song not in queue" } }
|
||||
kernel.prepareNew(mediaItems, startIndex, command.shuffled)
|
||||
kernel.play()
|
||||
queuer.prepareNew(mediaItems, startIndex, command.shuffled)
|
||||
player.play()
|
||||
playbackManager.ack(this, StateAck.NewPlayback)
|
||||
deferSave()
|
||||
}
|
||||
|
||||
override fun shuffled(shuffled: Boolean) {
|
||||
kernel.shuffled(shuffled)
|
||||
queuer.shuffled(shuffled)
|
||||
playbackManager.ack(this, StateAck.QueueReordered)
|
||||
deferSave()
|
||||
}
|
||||
|
@ -279,17 +247,17 @@ class ExoPlaybackStateHolder(
|
|||
// 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 (kernel.repeatMode == Player.REPEAT_MODE_ALL || kernel.hasNextMediaItem()) {
|
||||
kernel.seekToNext()
|
||||
if (player.repeatMode == Player.REPEAT_MODE_ALL || queuer.hasNextMediaItem()) {
|
||||
queuer.seekToNext()
|
||||
if (!playbackSettings.rememberPause) {
|
||||
kernel.play()
|
||||
player.play()
|
||||
}
|
||||
} else {
|
||||
kernel.goto(kernel.computeFirstMediaItemIndex())
|
||||
queuer.goto(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) {
|
||||
kernel.pause()
|
||||
player.pause()
|
||||
}
|
||||
}
|
||||
playbackManager.ack(this, StateAck.IndexMoved)
|
||||
|
@ -298,47 +266,47 @@ class ExoPlaybackStateHolder(
|
|||
|
||||
override fun prev() {
|
||||
if (playbackSettings.rewindWithPrev) {
|
||||
kernel.seekToPrevious()
|
||||
} else if (kernel.hasPreviousMediaItem()) {
|
||||
kernel.seekToPreviousMediaItem()
|
||||
queuer.seekToPrevious()
|
||||
} else if (queuer.hasPreviousMediaItem()) {
|
||||
queuer.seekToPreviousMediaItem()
|
||||
} else {
|
||||
kernel.seekTo(0)
|
||||
player.seekTo(0)
|
||||
}
|
||||
if (!playbackSettings.rememberPause) {
|
||||
kernel.play()
|
||||
player.play()
|
||||
}
|
||||
playbackManager.ack(this, StateAck.IndexMoved)
|
||||
deferSave()
|
||||
}
|
||||
|
||||
override fun goto(index: Int) {
|
||||
val indices = kernel.computeMapping()
|
||||
val indices = queuer.computeMapping()
|
||||
if (indices.isEmpty()) {
|
||||
return
|
||||
}
|
||||
val trueIndex = indices[index]
|
||||
kernel.goto(trueIndex)
|
||||
queuer.goto(trueIndex)
|
||||
if (!playbackSettings.rememberPause) {
|
||||
kernel.play()
|
||||
player.play()
|
||||
}
|
||||
playbackManager.ack(this, StateAck.IndexMoved)
|
||||
deferSave()
|
||||
}
|
||||
|
||||
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)
|
||||
deferSave()
|
||||
}
|
||||
|
||||
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)
|
||||
deferSave()
|
||||
}
|
||||
|
||||
override fun move(from: Int, to: Int, ack: StateAck.Move) {
|
||||
val indices = kernel.computeMapping()
|
||||
val indices = queuer.computeMapping()
|
||||
if (indices.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
@ -346,22 +314,22 @@ class ExoPlaybackStateHolder(
|
|||
val trueFrom = indices[from]
|
||||
val trueTo = indices[to]
|
||||
|
||||
kernel.moveMediaItem(trueFrom, trueTo)
|
||||
queuer.moveMediaItem(trueFrom, trueTo)
|
||||
playbackManager.ack(this, ack)
|
||||
deferSave()
|
||||
}
|
||||
|
||||
override fun remove(at: Int, ack: StateAck.Remove) {
|
||||
val indices = kernel.computeMapping()
|
||||
val indices = queuer.computeMapping()
|
||||
if (indices.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
val trueIndex = indices[at]
|
||||
val songWillChange = kernel.currentMediaItemIndex == trueIndex
|
||||
kernel.removeMediaItem(trueIndex)
|
||||
val songWillChange = queuer.currentMediaItemIndex == trueIndex
|
||||
queuer.removeMediaItem(trueIndex)
|
||||
if (songWillChange && !playbackSettings.rememberPause) {
|
||||
kernel.play()
|
||||
player.play()
|
||||
}
|
||||
playbackManager.ack(this, ack)
|
||||
deferSave()
|
||||
|
@ -379,8 +347,8 @@ class ExoPlaybackStateHolder(
|
|||
sendEvent = true
|
||||
}
|
||||
if (rawQueue != resolveQueue()) {
|
||||
kernel.prepareSaved(rawQueue.heap.map { it.buildMediaItem() }, rawQueue.shuffledMapping, rawQueue.heapIndex, rawQueue.isShuffled)
|
||||
kernel.pause()
|
||||
queuer.prepareSaved(rawQueue.heap.map { it.buildMediaItem() }, rawQueue.shuffledMapping, rawQueue.heapIndex, rawQueue.isShuffled)
|
||||
player.pause()
|
||||
sendEvent = true
|
||||
}
|
||||
if (sendEvent) {
|
||||
|
@ -403,7 +371,7 @@ class ExoPlaybackStateHolder(
|
|||
}
|
||||
|
||||
override fun reset(ack: StateAck.NewPlayback) {
|
||||
kernel.discard()
|
||||
queuer.discard()
|
||||
playbackManager.ack(this, ack)
|
||||
deferSave()
|
||||
}
|
||||
|
@ -413,7 +381,7 @@ class ExoPlaybackStateHolder(
|
|||
override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) {
|
||||
super.onPlayWhenReadyChanged(playWhenReady, reason)
|
||||
|
||||
if (kernel.playWhenReady) {
|
||||
if (player.playWhenReady) {
|
||||
// Mark that we have started playing so that the notification can now be posted.
|
||||
logD("Player has started playing")
|
||||
sessionOngoing = true
|
||||
|
@ -435,9 +403,9 @@ class ExoPlaybackStateHolder(
|
|||
override fun onPlaybackStateChanged(playbackState: Int) {
|
||||
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)
|
||||
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) {
|
||||
saveJob {
|
||||
persistenceRepository.saveState(playbackManager.toSavedState())
|
|
@ -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.Player
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.playback.state.RawQueue
|
||||
import org.oxycblt.auxio.playback.state.RepeatMode
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
|
||||
interface PlayerKernel {
|
||||
// REPLICAS
|
||||
val isPlaying: Boolean
|
||||
var playWhenReady: Boolean
|
||||
val currentPosition: Long
|
||||
@get:Player.RepeatMode var repeatMode: Int
|
||||
val audioSessionId: Int
|
||||
interface Queuer {
|
||||
val currentMediaItem: MediaItem?
|
||||
val currentMediaItemIndex: Int
|
||||
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 seekToNext()
|
||||
|
@ -47,5 +31,8 @@ interface PlayerKernel {
|
|||
fun addTopMediaItems(mediaItems: List<MediaItem>)
|
||||
fun addBottomMediaItems(mediaItems: List<MediaItem>)
|
||||
fun shuffled(shuffled: Boolean)
|
||||
}
|
||||
|
||||
interface Factory {
|
||||
fun create(exoPlayer: ExoPlayer): Queuer
|
||||
}
|
||||
}
|
|
@ -25,6 +25,7 @@ import kotlinx.coroutines.Job
|
|||
import org.oxycblt.auxio.ForegroundListener
|
||||
import org.oxycblt.auxio.ForegroundServiceNotification
|
||||
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.PlaybackStateManager
|
||||
import org.oxycblt.auxio.util.logD
|
||||
|
@ -35,7 +36,7 @@ private constructor(
|
|||
private val context: Context,
|
||||
private val foregroundListener: ForegroundListener,
|
||||
private val playbackManager: PlaybackStateManager,
|
||||
exoHolderFactory: ExoPlaybackStateHolder.Factory,
|
||||
playerHolderFactory: PlayerStateHolder.Factory,
|
||||
sessionHolderFactory: MediaSessionHolder.Factory,
|
||||
widgetComponentFactory: WidgetComponent.Factory,
|
||||
systemReceiverFactory: SystemPlaybackReceiver.Factory,
|
||||
|
@ -44,7 +45,7 @@ private constructor(
|
|||
@Inject
|
||||
constructor(
|
||||
private val playbackManager: PlaybackStateManager,
|
||||
private val exoHolderFactory: ExoPlaybackStateHolder.Factory,
|
||||
private val exoHolderFactory: PlayerStateHolder.Factory,
|
||||
private val sessionHolderFactory: MediaSessionHolder.Factory,
|
||||
private val widgetComponentFactory: WidgetComponent.Factory,
|
||||
private val systemReceiverFactory: SystemPlaybackReceiver.Factory,
|
||||
|
@ -61,7 +62,7 @@ private constructor(
|
|||
}
|
||||
|
||||
private val waitJob = Job()
|
||||
private val exoHolder = exoHolderFactory.create()
|
||||
private val exoHolder = playerHolderFactory.create(context)
|
||||
private val sessionHolder = sessionHolderFactory.create(context, foregroundListener)
|
||||
private val widgetComponent = widgetComponentFactory.create(context)
|
||||
private val systemReceiver = systemReceiverFactory.create(context, widgetComponent)
|
||||
|
|
Loading…
Reference in a new issue