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.MusicParent
|
||||||
import org.oxycblt.auxio.music.Song
|
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 {
|
interface PlaybackStateHolder {
|
||||||
|
/** The current [Progression] state of the audio player. */
|
||||||
val progression: Progression
|
val progression: Progression
|
||||||
|
|
||||||
|
/** The current [RepeatMode] of the audio player. */
|
||||||
val repeatMode: RepeatMode
|
val repeatMode: RepeatMode
|
||||||
|
|
||||||
|
/** The current [MusicParent] being played from. Null if playing from all songs. */
|
||||||
val parent: MusicParent?
|
val parent: MusicParent?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve the current queue state as a [RawQueue].
|
||||||
|
*
|
||||||
|
* @return The current queue state.
|
||||||
|
*/
|
||||||
fun resolveQueue(): RawQueue
|
fun resolveQueue(): RawQueue
|
||||||
|
|
||||||
|
/** The current audio session ID of the audio player. */
|
||||||
val audioSessionId: Int
|
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)
|
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)
|
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)
|
fun seekTo(positionMs: Long)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the repeat mode of the audio player.
|
||||||
|
*
|
||||||
|
* @param repeatMode The new repeat mode.
|
||||||
|
*/
|
||||||
fun repeatMode(repeatMode: RepeatMode)
|
fun repeatMode(repeatMode: RepeatMode)
|
||||||
|
|
||||||
|
/** Go to the next song in the queue. */
|
||||||
fun next()
|
fun next()
|
||||||
|
|
||||||
|
/** Go to the previous song in the queue. */
|
||||||
fun prev()
|
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)
|
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)
|
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)
|
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)
|
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)
|
fun remove(at: Int, ack: StateAck.Remove)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reorder the queue.
|
||||||
|
*
|
||||||
|
* @param shuffled Whether the queue should be shuffled.
|
||||||
|
*/
|
||||||
fun shuffled(shuffled: Boolean)
|
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
|
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?)
|
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 {
|
sealed interface StateAck {
|
||||||
|
/**
|
||||||
|
* @see PlaybackStateHolder.next
|
||||||
|
* @see PlaybackStateHolder.prev
|
||||||
|
* @see PlaybackStateHolder.goto
|
||||||
|
*/
|
||||||
data object IndexMoved : StateAck
|
data object IndexMoved : StateAck
|
||||||
|
|
||||||
|
/** @see PlaybackStateHolder.playNext */
|
||||||
data class PlayNext(val at: Int, val size: Int) : StateAck
|
data class PlayNext(val at: Int, val size: Int) : StateAck
|
||||||
|
|
||||||
|
/** @see PlaybackStateHolder.addToQueue */
|
||||||
data class AddToQueue(val at: Int, val size: Int) : StateAck
|
data class AddToQueue(val at: Int, val size: Int) : StateAck
|
||||||
|
|
||||||
|
/** @see PlaybackStateHolder.move */
|
||||||
data class Move(val from: Int, val to: Int) : StateAck
|
data class Move(val from: Int, val to: Int) : StateAck
|
||||||
|
|
||||||
|
/** @see PlaybackStateHolder.remove */
|
||||||
data class Remove(val index: Int) : StateAck
|
data class Remove(val index: Int) : StateAck
|
||||||
|
|
||||||
|
/** @see PlaybackStateHolder.shuffled */
|
||||||
data object QueueReordered : StateAck
|
data object QueueReordered : StateAck
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see PlaybackStateHolder.newPlayback
|
||||||
|
* @see PlaybackStateHolder.applySavedState
|
||||||
|
*/
|
||||||
data object NewPlayback : StateAck
|
data object NewPlayback : StateAck
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see PlaybackStateHolder.playing
|
||||||
|
* @see PlaybackStateHolder.seekTo
|
||||||
|
*/
|
||||||
data object ProgressionChanged : StateAck
|
data object ProgressionChanged : StateAck
|
||||||
|
|
||||||
|
/** @see PlaybackStateHolder.repeatMode */
|
||||||
data object RepeatModeChanged : StateAck
|
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(
|
data class RawQueue(
|
||||||
val heap: List<Song>,
|
val heap: List<Song>,
|
||||||
val shuffledMapping: List<Int>,
|
val shuffledMapping: List<Int>,
|
||||||
val heapIndex: Int,
|
val heapIndex: Int,
|
||||||
) {
|
) {
|
||||||
|
/** Whether the queue is currently shuffled. */
|
||||||
val isShuffled = shuffledMapping.isNotEmpty()
|
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() =
|
fun resolveSongs() =
|
||||||
if (isShuffled) {
|
if (isShuffled) {
|
||||||
shuffledMapping.map { heap[it] }
|
shuffledMapping.map { heap[it] }
|
||||||
|
@ -99,6 +232,11 @@ data class RawQueue(
|
||||||
heap
|
heap
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve and return the current index of the queue.
|
||||||
|
*
|
||||||
|
* @return The current index of the queue.
|
||||||
|
*/
|
||||||
fun resolveIndex() =
|
fun resolveIndex() =
|
||||||
if (isShuffled) {
|
if (isShuffled) {
|
||||||
shuffledMapping.indexOf(heapIndex)
|
shuffledMapping.indexOf(heapIndex)
|
||||||
|
@ -107,6 +245,7 @@ data class RawQueue(
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
/** Create a blank instance. */
|
||||||
fun nil() = RawQueue(emptyList(), emptyList(), -1)
|
fun nil() = RawQueue(emptyList(), emptyList(), -1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,19 +45,25 @@ import org.oxycblt.auxio.util.logW
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
interface PlaybackStateManager {
|
interface PlaybackStateManager {
|
||||||
/** The current [Progression] state. */
|
/** The current [Progression] of the audio player */
|
||||||
val progression: Progression
|
val progression: Progression
|
||||||
|
|
||||||
|
/** The current [RepeatMode]. */
|
||||||
val repeatMode: RepeatMode
|
val repeatMode: RepeatMode
|
||||||
|
|
||||||
|
/** The current [MusicParent] being played from */
|
||||||
val parent: MusicParent?
|
val parent: MusicParent?
|
||||||
|
|
||||||
|
/** The current [Song] being played. Null if nothing is playing. */
|
||||||
val currentSong: Song?
|
val currentSong: Song?
|
||||||
|
|
||||||
|
/** The current queue of [Song]s. */
|
||||||
val queue: List<Song>
|
val queue: List<Song>
|
||||||
|
|
||||||
|
/** The index of the currently playing [Song] in the queue. */
|
||||||
val index: Int
|
val index: Int
|
||||||
|
|
||||||
|
/** Whether the queue is shuffled or not. */
|
||||||
val isShuffled: Boolean
|
val isShuffled: Boolean
|
||||||
|
|
||||||
/** The audio session ID of the internal player. Null if no internal player exists. */
|
/** 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
|
override val progression: Progression
|
||||||
get() =
|
get() =
|
||||||
player.song?.let {
|
player.currentMediaItem?.let {
|
||||||
Progression.from(
|
Progression.from(
|
||||||
player.playWhenReady,
|
player.playWhenReady,
|
||||||
player.isPlaying,
|
player.isPlaying,
|
||||||
// The position value can be below zero or past the expected duration, make
|
// The position value can be below zero or past the expected duration, make
|
||||||
// sure we handle that.
|
// sure we handle that.
|
||||||
player.currentPosition.coerceAtLeast(0).coerceAtMost(it.durationMs))
|
player.currentPosition.coerceAtLeast(0)
|
||||||
|
.coerceAtMost(it.song.durationMs))
|
||||||
}
|
}
|
||||||
?: Progression.nil()
|
?: Progression.nil()
|
||||||
|
|
||||||
|
@ -243,7 +244,15 @@ class PlaybackService :
|
||||||
|
|
||||||
override var parent: MusicParent? = null
|
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
|
override val audioSessionId: Int
|
||||||
get() = player.audioSessionId
|
get() = player.audioSessionId
|
||||||
|
@ -255,11 +264,18 @@ class PlaybackService :
|
||||||
shuffled: Boolean
|
shuffled: Boolean
|
||||||
) {
|
) {
|
||||||
this.parent = parent
|
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) {
|
if (shuffled) {
|
||||||
player.shuffledQueue(queue, start)
|
player.setShuffleOrder(BetterShuffleOrder(queue.size, startIndex ?: -1))
|
||||||
} else {
|
|
||||||
player.orderedQueue(queue, start)
|
|
||||||
}
|
}
|
||||||
|
val target =
|
||||||
|
startIndex ?: player.currentTimeline.getFirstWindowIndex(player.shuffleModeEnabled)
|
||||||
|
player.seekTo(target, C.TIME_UNSET)
|
||||||
player.prepare()
|
player.prepare()
|
||||||
player.play()
|
player.play()
|
||||||
playbackManager.ack(this, StateAck.NewPlayback)
|
playbackManager.ack(this, StateAck.NewPlayback)
|
||||||
|
@ -300,32 +316,67 @@ class PlaybackService :
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun goto(index: Int) {
|
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)
|
playbackManager.ack(this, StateAck.IndexMoved)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun shuffled(shuffled: Boolean) {
|
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)
|
playbackManager.ack(this, StateAck.QueueReordered)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun playNext(songs: List<Song>, ack: StateAck.PlayNext) {
|
override fun playNext(songs: List<Song>, ack: StateAck.PlayNext) {
|
||||||
player.playNext(songs)
|
player.addMediaItems(player.nextMediaItemIndex, songs.map { it.toMediaItem() })
|
||||||
playbackManager.ack(this, ack)
|
playbackManager.ack(this, ack)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun addToQueue(songs: List<Song>, ack: StateAck.AddToQueue) {
|
override fun addToQueue(songs: List<Song>, ack: StateAck.AddToQueue) {
|
||||||
player.addToQueue(songs)
|
player.addMediaItems(songs.map { it.toMediaItem() })
|
||||||
playbackManager.ack(this, ack)
|
playbackManager.ack(this, ack)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun move(from: Int, to: Int, ack: StateAck.Move) {
|
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)
|
playbackManager.ack(this, ack)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun remove(at: Int, ack: StateAck.Remove) {
|
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)
|
playbackManager.ack(this, ack)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -375,7 +426,14 @@ class PlaybackService :
|
||||||
ack: StateAck.NewPlayback?
|
ack: StateAck.NewPlayback?
|
||||||
) {
|
) {
|
||||||
this.parent = parent
|
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()
|
player.prepare()
|
||||||
ack?.let { playbackManager.ack(this, it) }
|
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() {
|
private fun updatePauseOnRepeat() {
|
||||||
player.pauseAtEndOfMediaItems =
|
player.pauseAtEndOfMediaItems =
|
||||||
playbackManager.repeatMode == RepeatMode.TRACK && playbackSettings.pauseOnRepeat
|
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) {
|
private fun broadcastAudioEffectAction(event: String) {
|
||||||
logD("Broadcasting AudioEffect event: $event")
|
logD("Broadcasting AudioEffect event: $event")
|
||||||
sendBroadcast(
|
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
|
* A [BroadcastReceiver] for receiving playback-specific [Intent]s from the system that require
|
||||||
* an active [IntentFilter] to be registered.
|
* an active [IntentFilter] to be registered.
|
||||||
|
|
Loading…
Reference in a new issue