playback: redocument/refactor gapless playback
Should complete this feature, save regression fixes. Resolves #110.
This commit is contained in:
parent
48ab83f6de
commit
b2d9b244e5
4 changed files with 277 additions and 206 deletions
|
@ -25,73 +25,206 @@ import org.oxycblt.auxio.list.adapter.UpdateInstructions
|
|||
import org.oxycblt.auxio.music.MusicParent
|
||||
import org.oxycblt.auxio.music.Song
|
||||
|
||||
/**
|
||||
* The designated "source of truth" for the current playback state. Should only be used by
|
||||
* [PlaybackStateManager], which mirrors a more refined version of the state held here.
|
||||
*
|
||||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
interface PlaybackStateHolder {
|
||||
/** The current [Progression] state of the audio player. */
|
||||
val progression: Progression
|
||||
|
||||
/** The current [RepeatMode] of the audio player. */
|
||||
val repeatMode: RepeatMode
|
||||
|
||||
/** The current [MusicParent] being played from. Null if playing from all songs. */
|
||||
val parent: MusicParent?
|
||||
|
||||
/**
|
||||
* Resolve the current queue state as a [RawQueue].
|
||||
*
|
||||
* @return The current queue state.
|
||||
*/
|
||||
fun resolveQueue(): RawQueue
|
||||
|
||||
/** The current audio session ID of the audio player. */
|
||||
val audioSessionId: Int
|
||||
|
||||
/**
|
||||
* Applies a completely new playback state to the holder.
|
||||
*
|
||||
* @param queue The new queue to use.
|
||||
* @param start The song to start playback from. Should be in the queue.
|
||||
* @param parent The parent to play from.
|
||||
* @param shuffled Whether the queue should be shuffled.
|
||||
*/
|
||||
fun newPlayback(queue: List<Song>, start: Song?, parent: MusicParent?, shuffled: Boolean)
|
||||
|
||||
/**
|
||||
* Update the playing state of the audio player.
|
||||
*
|
||||
* @param playing Whether the player should be playing audio.
|
||||
*/
|
||||
fun playing(playing: Boolean)
|
||||
|
||||
/**
|
||||
* Seek to a position in the current song.
|
||||
*
|
||||
* @param positionMs The position to seek to, in milliseconds.
|
||||
*/
|
||||
fun seekTo(positionMs: Long)
|
||||
|
||||
/**
|
||||
* Update the repeat mode of the audio player.
|
||||
*
|
||||
* @param repeatMode The new repeat mode.
|
||||
*/
|
||||
fun repeatMode(repeatMode: RepeatMode)
|
||||
|
||||
/** Go to the next song in the queue. */
|
||||
fun next()
|
||||
|
||||
/** Go to the previous song in the queue. */
|
||||
fun prev()
|
||||
|
||||
/**
|
||||
* Go to a specific index in the queue.
|
||||
*
|
||||
* @param index The index to go to. Should be in the queue.
|
||||
*/
|
||||
fun goto(index: Int)
|
||||
|
||||
/**
|
||||
* Add songs to the currently playing item in the queue.
|
||||
*
|
||||
* @param songs The songs to add.
|
||||
* @param ack The [StateAck] to return to [PlaybackStateManager].
|
||||
*/
|
||||
fun playNext(songs: List<Song>, ack: StateAck.PlayNext)
|
||||
|
||||
/**
|
||||
* Add songs to the end of the queue.
|
||||
*
|
||||
* @param songs The songs to add.
|
||||
* @param ack The [StateAck] to return to [PlaybackStateManager].
|
||||
*/
|
||||
fun addToQueue(songs: List<Song>, ack: StateAck.AddToQueue)
|
||||
|
||||
/**
|
||||
* Move a song in the queue to a new position.
|
||||
*
|
||||
* @param from The index of the song to move.
|
||||
* @param to The index to move the song to.
|
||||
* @param ack The [StateAck] to return to [PlaybackStateManager].
|
||||
*/
|
||||
fun move(from: Int, to: Int, ack: StateAck.Move)
|
||||
|
||||
/**
|
||||
* Remove a song from the queue.
|
||||
*
|
||||
* @param at The index of the song to remove.
|
||||
* @param ack The [StateAck] to return to [PlaybackStateManager].
|
||||
* @return The [Song] that was removed.
|
||||
*/
|
||||
fun remove(at: Int, ack: StateAck.Remove)
|
||||
|
||||
/**
|
||||
* Reorder the queue.
|
||||
*
|
||||
* @param shuffled Whether the queue should be shuffled.
|
||||
*/
|
||||
fun shuffled(shuffled: Boolean)
|
||||
|
||||
/**
|
||||
* Handle a deferred playback action.
|
||||
*
|
||||
* @param action The action to handle.
|
||||
* @return Whether the action could be handled, or if it should be deferred for later.
|
||||
*/
|
||||
fun handleDeferred(action: DeferredPlayback): Boolean
|
||||
|
||||
/**
|
||||
* Override the current held state with a saved state.
|
||||
*
|
||||
* @param parent The parent to play from.
|
||||
* @param rawQueue The queue to use.
|
||||
* @param ack The [StateAck] to return to [PlaybackStateManager]. If null, do not return any
|
||||
* ack.
|
||||
*/
|
||||
fun applySavedState(parent: MusicParent?, rawQueue: RawQueue, ack: StateAck.NewPlayback?)
|
||||
}
|
||||
|
||||
/**
|
||||
* An acknowledgement that the state of the [PlaybackStateHolder] has changed. This is sent back to
|
||||
* [PlaybackStateManager] once an operation in [PlaybackStateHolder] has completed so that the new
|
||||
* state can be mirrored to the rest of the application.
|
||||
*
|
||||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
sealed interface StateAck {
|
||||
/**
|
||||
* @see PlaybackStateHolder.next
|
||||
* @see PlaybackStateHolder.prev
|
||||
* @see PlaybackStateHolder.goto
|
||||
*/
|
||||
data object IndexMoved : StateAck
|
||||
|
||||
/** @see PlaybackStateHolder.playNext */
|
||||
data class PlayNext(val at: Int, val size: Int) : StateAck
|
||||
|
||||
/** @see PlaybackStateHolder.addToQueue */
|
||||
data class AddToQueue(val at: Int, val size: Int) : StateAck
|
||||
|
||||
/** @see PlaybackStateHolder.move */
|
||||
data class Move(val from: Int, val to: Int) : StateAck
|
||||
|
||||
/** @see PlaybackStateHolder.remove */
|
||||
data class Remove(val index: Int) : StateAck
|
||||
|
||||
/** @see PlaybackStateHolder.shuffled */
|
||||
data object QueueReordered : StateAck
|
||||
|
||||
/**
|
||||
* @see PlaybackStateHolder.newPlayback
|
||||
* @see PlaybackStateHolder.applySavedState
|
||||
*/
|
||||
data object NewPlayback : StateAck
|
||||
|
||||
/**
|
||||
* @see PlaybackStateHolder.playing
|
||||
* @see PlaybackStateHolder.seekTo
|
||||
*/
|
||||
data object ProgressionChanged : StateAck
|
||||
|
||||
/** @see PlaybackStateHolder.repeatMode */
|
||||
data object RepeatModeChanged : StateAck
|
||||
}
|
||||
|
||||
/**
|
||||
* The queue as it is represented in the audio player held by [PlaybackStateHolder]. This should not
|
||||
* be used as anything but a container. Use the provided fields to obtain saner queue information.
|
||||
*
|
||||
* @param heap The ordered list of all [Song]s in the queue.
|
||||
* @param shuffledMapping A list of indices that remap the songs in [heap] into a shuffled queue.
|
||||
* Empty if the queue is not shuffled.
|
||||
* @param heapIndex The index of the current song in [heap]. Note that if shuffled, this will be a
|
||||
* nonsensical value that cannot be used to obtain next and last songs without first resolving the
|
||||
* queue.
|
||||
*/
|
||||
data class RawQueue(
|
||||
val heap: List<Song>,
|
||||
val shuffledMapping: List<Int>,
|
||||
val heapIndex: Int,
|
||||
) {
|
||||
/** Whether the queue is currently shuffled. */
|
||||
val isShuffled = shuffledMapping.isNotEmpty()
|
||||
|
||||
/**
|
||||
* Resolve and return the exact [Song] sequence in the queue.
|
||||
*
|
||||
* @return The [Song]s in the queue, in order.
|
||||
*/
|
||||
fun resolveSongs() =
|
||||
if (isShuffled) {
|
||||
shuffledMapping.map { heap[it] }
|
||||
|
@ -99,6 +232,11 @@ data class RawQueue(
|
|||
heap
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve and return the current index of the queue.
|
||||
*
|
||||
* @return The current index of the queue.
|
||||
*/
|
||||
fun resolveIndex() =
|
||||
if (isShuffled) {
|
||||
shuffledMapping.indexOf(heapIndex)
|
||||
|
@ -107,6 +245,7 @@ data class RawQueue(
|
|||
}
|
||||
|
||||
companion object {
|
||||
/** Create a blank instance. */
|
||||
fun nil() = RawQueue(emptyList(), emptyList(), -1)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -45,19 +45,25 @@ import org.oxycblt.auxio.util.logW
|
|||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
interface PlaybackStateManager {
|
||||
/** The current [Progression] state. */
|
||||
/** The current [Progression] of the audio player */
|
||||
val progression: Progression
|
||||
|
||||
/** The current [RepeatMode]. */
|
||||
val repeatMode: RepeatMode
|
||||
|
||||
/** The current [MusicParent] being played from */
|
||||
val parent: MusicParent?
|
||||
|
||||
/** The current [Song] being played. Null if nothing is playing. */
|
||||
val currentSong: Song?
|
||||
|
||||
/** The current queue of [Song]s. */
|
||||
val queue: List<Song>
|
||||
|
||||
/** The index of the currently playing [Song] in the queue. */
|
||||
val index: Int
|
||||
|
||||
/** Whether the queue is shuffled or not. */
|
||||
val isShuffled: Boolean
|
||||
|
||||
/** The audio session ID of the internal player. Null if no internal player exists. */
|
||||
|
|
|
@ -1,176 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2024 Auxio Project
|
||||
* ExoPlayerExt.kt is part of Auxio.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.playback.system
|
||||
|
||||
import androidx.media3.common.C
|
||||
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.util.logD
|
||||
|
||||
val ExoPlayer.song
|
||||
get() = currentMediaItem?.song
|
||||
|
||||
fun ExoPlayer.resolveQueue(): RawQueue {
|
||||
val heap = (0 until mediaItemCount).map { getMediaItemAt(it).song }
|
||||
val shuffledMapping = if (shuffleModeEnabled) unscrambleQueueIndices() else emptyList()
|
||||
return RawQueue(heap, shuffledMapping, currentMediaItemIndex)
|
||||
}
|
||||
|
||||
fun ExoPlayer.orderedQueue(queue: Collection<Song>, start: Song?) {
|
||||
clearMediaItems()
|
||||
shuffleModeEnabled = false
|
||||
setMediaItems(queue.map { it.toMediaItem() })
|
||||
if (start != null) {
|
||||
val startIndex = queue.indexOf(start)
|
||||
if (startIndex != -1) {
|
||||
seekTo(startIndex, C.TIME_UNSET)
|
||||
} else {
|
||||
throw IllegalArgumentException("Start song not in queue")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun ExoPlayer.shuffledQueue(queue: Collection<Song>, start: Song?) {
|
||||
setMediaItems(queue.map { it.toMediaItem() })
|
||||
shuffleModeEnabled = true
|
||||
val startIndex =
|
||||
if (start != null) {
|
||||
queue.indexOf(start).also { check(it != -1) { "Start song not in queue" } }
|
||||
} else {
|
||||
-1
|
||||
}
|
||||
setShuffleOrder(BetterShuffleOrder(queue.size, startIndex))
|
||||
seekTo(currentTimeline.getFirstWindowIndex(shuffleModeEnabled), C.TIME_UNSET)
|
||||
}
|
||||
|
||||
fun ExoPlayer.applyQueue(rawQueue: RawQueue) {
|
||||
setMediaItems(rawQueue.heap.map { it.toMediaItem() })
|
||||
if (rawQueue.isShuffled) {
|
||||
shuffleModeEnabled = true
|
||||
setShuffleOrder(BetterShuffleOrder(rawQueue.shuffledMapping.toIntArray()))
|
||||
} else {
|
||||
shuffleModeEnabled = false
|
||||
}
|
||||
seekTo(rawQueue.heapIndex, C.TIME_UNSET)
|
||||
}
|
||||
|
||||
fun ExoPlayer.shuffled(shuffled: Boolean) {
|
||||
logD("Reordering queue to $shuffled")
|
||||
shuffleModeEnabled = shuffled
|
||||
if (shuffled) {
|
||||
// Have to manually refresh the shuffle seed and anchor it to the new current songs
|
||||
setShuffleOrder(BetterShuffleOrder(mediaItemCount, currentMediaItemIndex))
|
||||
}
|
||||
}
|
||||
|
||||
fun ExoPlayer.playNext(songs: List<Song>) {
|
||||
addMediaItems(nextMediaItemIndex, songs.map { it.toMediaItem() })
|
||||
}
|
||||
|
||||
fun ExoPlayer.addToQueue(songs: List<Song>) {
|
||||
addMediaItems(songs.map { it.toMediaItem() })
|
||||
}
|
||||
|
||||
fun ExoPlayer.goto(index: Int) {
|
||||
val indices = unscrambleQueueIndices()
|
||||
if (indices.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
val trueIndex = indices[index]
|
||||
seekTo(trueIndex, C.TIME_UNSET)
|
||||
}
|
||||
|
||||
fun ExoPlayer.move(from: Int, to: Int) {
|
||||
val indices = unscrambleQueueIndices()
|
||||
if (indices.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
val trueFrom = indices[from]
|
||||
val trueTo = indices[to]
|
||||
|
||||
when {
|
||||
trueFrom > trueTo -> {
|
||||
moveMediaItem(trueFrom, trueTo)
|
||||
moveMediaItem(trueTo + 1, trueFrom)
|
||||
}
|
||||
trueTo > trueFrom -> {
|
||||
moveMediaItem(trueFrom, trueTo)
|
||||
moveMediaItem(trueTo - 1, trueFrom)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun ExoPlayer.remove(at: Int) {
|
||||
val indices = unscrambleQueueIndices()
|
||||
if (indices.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
val trueIndex = indices[at]
|
||||
removeMediaItem(trueIndex)
|
||||
}
|
||||
|
||||
fun ExoPlayer.unscrambleQueueIndices(): List<Int> {
|
||||
val timeline = currentTimeline
|
||||
if (timeline.isEmpty()) {
|
||||
return emptyList()
|
||||
}
|
||||
val queue = mutableListOf<Int>()
|
||||
|
||||
// Add the active queue item.
|
||||
val currentMediaItemIndex = currentMediaItemIndex
|
||||
queue.add(currentMediaItemIndex)
|
||||
|
||||
// Fill queue alternating with next and/or previous queue items.
|
||||
var firstMediaItemIndex = currentMediaItemIndex
|
||||
var lastMediaItemIndex = currentMediaItemIndex
|
||||
val shuffleModeEnabled = shuffleModeEnabled
|
||||
while ((firstMediaItemIndex != C.INDEX_UNSET || lastMediaItemIndex != C.INDEX_UNSET)) {
|
||||
// Begin with next to have a longer tail than head if an even sized queue needs to be
|
||||
// trimmed.
|
||||
if (lastMediaItemIndex != C.INDEX_UNSET) {
|
||||
lastMediaItemIndex =
|
||||
timeline.getNextWindowIndex(
|
||||
lastMediaItemIndex, Player.REPEAT_MODE_OFF, shuffleModeEnabled)
|
||||
if (lastMediaItemIndex != C.INDEX_UNSET) {
|
||||
queue.add(lastMediaItemIndex)
|
||||
}
|
||||
}
|
||||
if (firstMediaItemIndex != C.INDEX_UNSET) {
|
||||
firstMediaItemIndex =
|
||||
timeline.getPreviousWindowIndex(
|
||||
firstMediaItemIndex, Player.REPEAT_MODE_OFF, shuffleModeEnabled)
|
||||
if (firstMediaItemIndex != C.INDEX_UNSET) {
|
||||
queue.add(0, firstMediaItemIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return queue
|
||||
}
|
||||
|
||||
fun Song.toMediaItem() = MediaItem.Builder().setUri(uri).setTag(this).build()
|
||||
|
||||
private val MediaItem.song: Song
|
||||
get() = requireNotNull(localConfiguration).tag as Song
|
|
@ -222,13 +222,14 @@ class PlaybackService :
|
|||
|
||||
override val progression: Progression
|
||||
get() =
|
||||
player.song?.let {
|
||||
player.currentMediaItem?.let {
|
||||
Progression.from(
|
||||
player.playWhenReady,
|
||||
player.isPlaying,
|
||||
// The position value can be below zero or past the expected duration, make
|
||||
// sure we handle that.
|
||||
player.currentPosition.coerceAtLeast(0).coerceAtMost(it.durationMs))
|
||||
player.currentPosition.coerceAtLeast(0)
|
||||
.coerceAtMost(it.song.durationMs))
|
||||
}
|
||||
?: Progression.nil()
|
||||
|
||||
|
@ -243,7 +244,15 @@ class PlaybackService :
|
|||
|
||||
override var parent: MusicParent? = null
|
||||
|
||||
override fun resolveQueue() = player.resolveQueue()
|
||||
override fun resolveQueue(): RawQueue {
|
||||
val heap = (0 until player.mediaItemCount).map { player.getMediaItemAt(it).song }
|
||||
val shuffledMapping = if (player.shuffleModeEnabled) {
|
||||
player.unscrambleQueueIndices()
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
return RawQueue(heap, shuffledMapping, player.currentMediaItemIndex)
|
||||
}
|
||||
|
||||
override val audioSessionId: Int
|
||||
get() = player.audioSessionId
|
||||
|
@ -255,11 +264,18 @@ class PlaybackService :
|
|||
shuffled: Boolean
|
||||
) {
|
||||
this.parent = parent
|
||||
player.shuffleModeEnabled = shuffled
|
||||
player.setMediaItems(queue.map { it.toMediaItem() })
|
||||
val startIndex =
|
||||
start
|
||||
?.let { queue.indexOf(start) }
|
||||
.also { check(it != -1) { "Start song not in queue" } }
|
||||
if (shuffled) {
|
||||
player.shuffledQueue(queue, start)
|
||||
} else {
|
||||
player.orderedQueue(queue, start)
|
||||
player.setShuffleOrder(BetterShuffleOrder(queue.size, startIndex ?: -1))
|
||||
}
|
||||
val target =
|
||||
startIndex ?: player.currentTimeline.getFirstWindowIndex(player.shuffleModeEnabled)
|
||||
player.seekTo(target, C.TIME_UNSET)
|
||||
player.prepare()
|
||||
player.play()
|
||||
playbackManager.ack(this, StateAck.NewPlayback)
|
||||
|
@ -300,32 +316,67 @@ class PlaybackService :
|
|||
}
|
||||
|
||||
override fun goto(index: Int) {
|
||||
player.goto(index)
|
||||
val indices = player.unscrambleQueueIndices()
|
||||
if (indices.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
val trueIndex = indices[index]
|
||||
player.seekTo(trueIndex, C.TIME_UNSET)
|
||||
playbackManager.ack(this, StateAck.IndexMoved)
|
||||
}
|
||||
|
||||
override fun shuffled(shuffled: Boolean) {
|
||||
player.shuffled(shuffled)
|
||||
logD("Reordering queue to $shuffled")
|
||||
player.shuffleModeEnabled = shuffled
|
||||
if (shuffled) {
|
||||
// Have to manually refresh the shuffle seed and anchor it to the new current songs
|
||||
player.setShuffleOrder(
|
||||
BetterShuffleOrder(player.mediaItemCount, player.currentMediaItemIndex))
|
||||
}
|
||||
playbackManager.ack(this, StateAck.QueueReordered)
|
||||
}
|
||||
|
||||
override fun playNext(songs: List<Song>, ack: StateAck.PlayNext) {
|
||||
player.playNext(songs)
|
||||
player.addMediaItems(player.nextMediaItemIndex, songs.map { it.toMediaItem() })
|
||||
playbackManager.ack(this, ack)
|
||||
}
|
||||
|
||||
override fun addToQueue(songs: List<Song>, ack: StateAck.AddToQueue) {
|
||||
player.addToQueue(songs)
|
||||
player.addMediaItems(songs.map { it.toMediaItem() })
|
||||
playbackManager.ack(this, ack)
|
||||
}
|
||||
|
||||
override fun move(from: Int, to: Int, ack: StateAck.Move) {
|
||||
player.move(from, to)
|
||||
val indices = player.unscrambleQueueIndices()
|
||||
if (indices.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
val trueFrom = indices[from]
|
||||
val trueTo = indices[to]
|
||||
|
||||
when {
|
||||
trueFrom > trueTo -> {
|
||||
player.moveMediaItem(trueFrom, trueTo)
|
||||
player.moveMediaItem(trueTo + 1, trueFrom)
|
||||
}
|
||||
trueTo > trueFrom -> {
|
||||
player.moveMediaItem(trueFrom, trueTo)
|
||||
player.moveMediaItem(trueTo - 1, trueFrom)
|
||||
}
|
||||
}
|
||||
playbackManager.ack(this, ack)
|
||||
}
|
||||
|
||||
override fun remove(at: Int, ack: StateAck.Remove) {
|
||||
player.remove(at)
|
||||
val indices = player.unscrambleQueueIndices()
|
||||
if (indices.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
val trueIndex = indices[at]
|
||||
player.removeMediaItem(trueIndex)
|
||||
playbackManager.ack(this, ack)
|
||||
}
|
||||
|
||||
|
@ -375,7 +426,14 @@ class PlaybackService :
|
|||
ack: StateAck.NewPlayback?
|
||||
) {
|
||||
this.parent = parent
|
||||
player.applyQueue(rawQueue)
|
||||
player.setMediaItems(rawQueue.heap.map { it.toMediaItem() })
|
||||
if (rawQueue.isShuffled) {
|
||||
player.shuffleModeEnabled = true
|
||||
player.setShuffleOrder(BetterShuffleOrder(rawQueue.shuffledMapping.toIntArray()))
|
||||
} else {
|
||||
player.shuffleModeEnabled = false
|
||||
}
|
||||
player.seekTo(rawQueue.heapIndex, C.TIME_UNSET)
|
||||
player.prepare()
|
||||
ack?.let { playbackManager.ack(this, it) }
|
||||
}
|
||||
|
@ -456,13 +514,72 @@ class PlaybackService :
|
|||
}
|
||||
}
|
||||
|
||||
// --- OTHER FUNCTIONS ---
|
||||
override fun onPostNotification(notification: NotificationComponent) {
|
||||
// Do not post the notification if playback hasn't started yet. This prevents errors
|
||||
// where changing a setting would cause the notification to appear in an unfriendly
|
||||
// manner.
|
||||
if (hasPlayed) {
|
||||
logD("Played before, starting foreground state")
|
||||
if (!foregroundManager.tryStartForeground(notification)) {
|
||||
logD("Notification changed, re-posting")
|
||||
notification.post()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- PLAYER MANAGEMENT ---
|
||||
|
||||
private fun updatePauseOnRepeat() {
|
||||
player.pauseAtEndOfMediaItems =
|
||||
playbackManager.repeatMode == RepeatMode.TRACK && playbackSettings.pauseOnRepeat
|
||||
}
|
||||
|
||||
private fun ExoPlayer.unscrambleQueueIndices(): List<Int> {
|
||||
val timeline = currentTimeline
|
||||
if (timeline.isEmpty()) {
|
||||
return emptyList()
|
||||
}
|
||||
val queue = mutableListOf<Int>()
|
||||
|
||||
// Add the active queue item.
|
||||
val currentMediaItemIndex = currentMediaItemIndex
|
||||
queue.add(currentMediaItemIndex)
|
||||
|
||||
// Fill queue alternating with next and/or previous queue items.
|
||||
var firstMediaItemIndex = currentMediaItemIndex
|
||||
var lastMediaItemIndex = currentMediaItemIndex
|
||||
val shuffleModeEnabled = shuffleModeEnabled
|
||||
while ((firstMediaItemIndex != C.INDEX_UNSET || lastMediaItemIndex != C.INDEX_UNSET)) {
|
||||
// Begin with next to have a longer tail than head if an even sized queue needs to be
|
||||
// trimmed.
|
||||
if (lastMediaItemIndex != C.INDEX_UNSET) {
|
||||
lastMediaItemIndex =
|
||||
timeline.getNextWindowIndex(
|
||||
lastMediaItemIndex, Player.REPEAT_MODE_OFF, shuffleModeEnabled)
|
||||
if (lastMediaItemIndex != C.INDEX_UNSET) {
|
||||
queue.add(lastMediaItemIndex)
|
||||
}
|
||||
}
|
||||
if (firstMediaItemIndex != C.INDEX_UNSET) {
|
||||
firstMediaItemIndex =
|
||||
timeline.getPreviousWindowIndex(
|
||||
firstMediaItemIndex, Player.REPEAT_MODE_OFF, shuffleModeEnabled)
|
||||
if (firstMediaItemIndex != C.INDEX_UNSET) {
|
||||
queue.add(0, firstMediaItemIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return queue
|
||||
}
|
||||
|
||||
private fun Song.toMediaItem() = MediaItem.Builder().setUri(uri).setTag(this).build()
|
||||
|
||||
private val MediaItem.song: Song
|
||||
get() = requireNotNull(localConfiguration).tag as Song
|
||||
|
||||
// --- OTHER FUNCTIONS ---
|
||||
|
||||
private fun broadcastAudioEffectAction(event: String) {
|
||||
logD("Broadcasting AudioEffect event: $event")
|
||||
sendBroadcast(
|
||||
|
@ -484,21 +601,6 @@ class PlaybackService :
|
|||
}
|
||||
}
|
||||
|
||||
// --- MEDIASESSIONCOMPONENT OVERRIDES ---
|
||||
|
||||
override fun onPostNotification(notification: NotificationComponent) {
|
||||
// Do not post the notification if playback hasn't started yet. This prevents errors
|
||||
// where changing a setting would cause the notification to appear in an unfriendly
|
||||
// manner.
|
||||
if (hasPlayed) {
|
||||
logD("Played before, starting foreground state")
|
||||
if (!foregroundManager.tryStartForeground(notification)) {
|
||||
logD("Notification changed, re-posting")
|
||||
notification.post()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A [BroadcastReceiver] for receiving playback-specific [Intent]s from the system that require
|
||||
* an active [IntentFilter] to be registered.
|
||||
|
|
Loading…
Reference in a new issue