playback: extract gapless playback impl
I need to make a setting to switch between gapless and single-item playback to accomodate extremely large queues, so extract the crazy hacky queue stuff into a new PlayerKernel construct. Single-item will be added at a later point.
This commit is contained in:
parent
34f7bc4886
commit
e32c687c61
3 changed files with 335 additions and 210 deletions
|
@ -63,20 +63,74 @@ import org.oxycblt.auxio.util.logE
|
||||||
|
|
||||||
class ExoPlaybackStateHolder(
|
class ExoPlaybackStateHolder(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val player: ExoPlayer,
|
private val kernel: PlayerKernel,
|
||||||
private val playbackManager: PlaybackStateManager,
|
private val playbackManager: PlaybackStateManager,
|
||||||
private val persistenceRepository: PersistenceRepository,
|
private val persistenceRepository: PersistenceRepository,
|
||||||
private val playbackSettings: PlaybackSettings,
|
private val playbackSettings: PlaybackSettings,
|
||||||
private val commandFactory: PlaybackCommand.Factory,
|
private val commandFactory: PlaybackCommand.Factory,
|
||||||
private val replayGainProcessor: ReplayGainAudioProcessor,
|
private val replayGainProcessor: ReplayGainAudioProcessor,
|
||||||
private val musicRepository: MusicRepository,
|
private val musicRepository: MusicRepository,
|
||||||
private val imageSettings: ImageSettings
|
private val imageSettings: ImageSettings,
|
||||||
) :
|
) :
|
||||||
PlaybackStateHolder,
|
PlaybackStateHolder,
|
||||||
Player.Listener,
|
Player.Listener,
|
||||||
MusicRepository.UpdateListener,
|
MusicRepository.UpdateListener,
|
||||||
PlaybackSettings.Listener,
|
|
||||||
ImageSettings.Listener {
|
ImageSettings.Listener {
|
||||||
|
class Factory
|
||||||
|
@Inject
|
||||||
|
constructor(
|
||||||
|
@ApplicationContext private val context: Context,
|
||||||
|
private val playbackManager: PlaybackStateManager,
|
||||||
|
private val persistenceRepository: PersistenceRepository,
|
||||||
|
private val playbackSettings: PlaybackSettings,
|
||||||
|
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(
|
||||||
|
context,
|
||||||
|
GaplessPlayerKernel(exoPlayer, playbackSettings),
|
||||||
|
playbackManager,
|
||||||
|
persistenceRepository,
|
||||||
|
playbackSettings,
|
||||||
|
commandFactory,
|
||||||
|
replayGainProcessor,
|
||||||
|
musicRepository,
|
||||||
|
imageSettings)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private val saveJob = Job()
|
private val saveJob = Job()
|
||||||
private val saveScope = CoroutineScope(Dispatchers.IO + saveJob)
|
private val saveScope = CoroutineScope(Dispatchers.IO + saveJob)
|
||||||
private val restoreScope = CoroutineScope(Dispatchers.IO + saveJob)
|
private val restoreScope = CoroutineScope(Dispatchers.IO + saveJob)
|
||||||
|
@ -88,20 +142,19 @@ class ExoPlaybackStateHolder(
|
||||||
|
|
||||||
fun attach() {
|
fun attach() {
|
||||||
imageSettings.registerListener(this)
|
imageSettings.registerListener(this)
|
||||||
player.addListener(this)
|
kernel.addListener(this)
|
||||||
playbackManager.registerStateHolder(this)
|
playbackManager.registerStateHolder(this)
|
||||||
playbackSettings.registerListener(this)
|
|
||||||
musicRepository.addUpdateListener(this)
|
musicRepository.addUpdateListener(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun release() {
|
fun release() {
|
||||||
saveJob.cancel()
|
saveJob.cancel()
|
||||||
player.removeListener(this)
|
kernel.removeListener(this)
|
||||||
playbackManager.unregisterStateHolder(this)
|
playbackManager.unregisterStateHolder(this)
|
||||||
musicRepository.removeUpdateListener(this)
|
musicRepository.removeUpdateListener(this)
|
||||||
replayGainProcessor.release()
|
replayGainProcessor.release()
|
||||||
imageSettings.unregisterListener(this)
|
imageSettings.unregisterListener(this)
|
||||||
player.release()
|
kernel.release()
|
||||||
}
|
}
|
||||||
|
|
||||||
override var parent: MusicParent? = null
|
override var parent: MusicParent? = null
|
||||||
|
@ -109,15 +162,15 @@ class ExoPlaybackStateHolder(
|
||||||
|
|
||||||
override val progression: Progression
|
override val progression: Progression
|
||||||
get() {
|
get() {
|
||||||
val mediaItem = player.currentMediaItem ?: return Progression.nil()
|
val mediaItem = kernel.currentMediaItem ?: return Progression.nil()
|
||||||
val duration = mediaItem.mediaMetadata.extras?.getLong("durationMs") ?: Long.MAX_VALUE
|
val duration = mediaItem.mediaMetadata.extras?.getLong("durationMs") ?: Long.MAX_VALUE
|
||||||
val clampedPosition = player.currentPosition.coerceAtLeast(0).coerceAtMost(duration)
|
val clampedPosition = kernel.currentPosition.coerceAtLeast(0).coerceAtMost(duration)
|
||||||
return Progression.from(player.playWhenReady, player.isPlaying, clampedPosition)
|
return Progression.from(kernel.playWhenReady, kernel.isPlaying, clampedPosition)
|
||||||
}
|
}
|
||||||
|
|
||||||
override val repeatMode
|
override val repeatMode
|
||||||
get() =
|
get() =
|
||||||
when (val repeatMode = player.repeatMode) {
|
when (val repeatMode = kernel.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
|
||||||
|
@ -125,21 +178,12 @@ class ExoPlaybackStateHolder(
|
||||||
}
|
}
|
||||||
|
|
||||||
override val audioSessionId: Int
|
override val audioSessionId: Int
|
||||||
get() = player.audioSessionId
|
get() = kernel.audioSessionId
|
||||||
|
|
||||||
override fun resolveQueue(): RawQueue {
|
override fun resolveQueue(): RawQueue {
|
||||||
val deviceLibrary =
|
val heap = kernel.computeHeap()
|
||||||
musicRepository.deviceLibrary
|
val shuffledMapping = if (kernel.shuffleModeEnabled) kernel.computeMapping() else emptyList()
|
||||||
// No library, cannot do anything.
|
return RawQueue(heap.mapNotNull { it.song }, shuffledMapping, kernel.currentMediaItemIndex)
|
||||||
?: return RawQueue(emptyList(), emptyList(), 0)
|
|
||||||
val heap = (0 until player.mediaItemCount).map { player.getMediaItemAt(it) }
|
|
||||||
val shuffledMapping =
|
|
||||||
if (player.shuffleModeEnabled) {
|
|
||||||
player.unscrambleQueueIndices()
|
|
||||||
} else {
|
|
||||||
emptyList()
|
|
||||||
}
|
|
||||||
return RawQueue(heap.mapNotNull { it.song }, shuffledMapping, player.currentMediaItemIndex)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handleDeferred(action: DeferredPlayback): Boolean {
|
override fun handleDeferred(action: DeferredPlayback): Boolean {
|
||||||
|
@ -192,53 +236,41 @@ class ExoPlaybackStateHolder(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun playing(playing: Boolean) {
|
override fun playing(playing: Boolean) {
|
||||||
player.playWhenReady = playing
|
kernel.playWhenReady = playing
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun seekTo(positionMs: Long) {
|
override fun seekTo(positionMs: Long) {
|
||||||
player.seekTo(positionMs)
|
kernel.seekTo(positionMs)
|
||||||
deferSave()
|
deferSave()
|
||||||
// Ack handled w/ExoPlayer events
|
// Ack handled w/ExoPlayer events
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun repeatMode(repeatMode: RepeatMode) {
|
override fun repeatMode(repeatMode: RepeatMode) {
|
||||||
player.repeatMode =
|
kernel.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()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun newPlayback(command: PlaybackCommand) {
|
override fun newPlayback(command: PlaybackCommand) {
|
||||||
parent = command.parent
|
parent = command.parent
|
||||||
player.shuffleModeEnabled = command.shuffled
|
val mediaItems = command.queue.map { it.buildMediaItem() }
|
||||||
player.setMediaItems(command.queue.map { it.buildMediaItem() })
|
|
||||||
val startIndex =
|
val startIndex =
|
||||||
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" } }
|
||||||
if (command.shuffled) {
|
kernel.prepareNew(mediaItems, startIndex, command.shuffled)
|
||||||
player.setShuffleOrder(BetterShuffleOrder(command.queue.size, startIndex ?: -1))
|
kernel.play()
|
||||||
}
|
|
||||||
val target = startIndex ?: player.currentTimeline.getFirstWindowIndex(command.shuffled)
|
|
||||||
player.seekTo(target, C.TIME_UNSET)
|
|
||||||
player.prepare()
|
|
||||||
player.play()
|
|
||||||
playbackManager.ack(this, StateAck.NewPlayback)
|
playbackManager.ack(this, StateAck.NewPlayback)
|
||||||
deferSave()
|
deferSave()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun shuffled(shuffled: Boolean) {
|
override fun shuffled(shuffled: Boolean) {
|
||||||
player.setShuffleModeEnabled(shuffled)
|
kernel.shuffled(shuffled)
|
||||||
if (player.shuffleModeEnabled) {
|
|
||||||
// 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)
|
||||||
deferSave()
|
deferSave()
|
||||||
}
|
}
|
||||||
|
@ -247,18 +279,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 (player.repeatMode == Player.REPEAT_MODE_ALL || player.hasNextMediaItem()) {
|
if (kernel.repeatMode == Player.REPEAT_MODE_ALL || kernel.hasNextMediaItem()) {
|
||||||
player.seekToNext()
|
kernel.seekToNext()
|
||||||
if (!playbackSettings.rememberPause) {
|
if (!playbackSettings.rememberPause) {
|
||||||
player.play()
|
kernel.play()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
player.seekTo(
|
kernel.goto(kernel.computeFirstMediaItemIndex())
|
||||||
player.currentTimeline.getFirstWindowIndex(player.shuffleModeEnabled), C.TIME_UNSET)
|
|
||||||
// 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) {
|
||||||
player.pause()
|
kernel.pause()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
playbackManager.ack(this, StateAck.IndexMoved)
|
playbackManager.ack(this, StateAck.IndexMoved)
|
||||||
|
@ -267,94 +298,70 @@ class ExoPlaybackStateHolder(
|
||||||
|
|
||||||
override fun prev() {
|
override fun prev() {
|
||||||
if (playbackSettings.rewindWithPrev) {
|
if (playbackSettings.rewindWithPrev) {
|
||||||
player.seekToPrevious()
|
kernel.seekToPrevious()
|
||||||
} else if (player.hasPreviousMediaItem()) {
|
} else if (kernel.hasPreviousMediaItem()) {
|
||||||
player.seekToPreviousMediaItem()
|
kernel.seekToPreviousMediaItem()
|
||||||
} else {
|
} else {
|
||||||
player.seekTo(0)
|
kernel.seekTo(0)
|
||||||
}
|
}
|
||||||
if (!playbackSettings.rememberPause) {
|
if (!playbackSettings.rememberPause) {
|
||||||
player.play()
|
kernel.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 = player.unscrambleQueueIndices()
|
val indices = kernel.computeMapping()
|
||||||
if (indices.isEmpty()) {
|
if (indices.isEmpty()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val trueIndex = indices[index]
|
val trueIndex = indices[index]
|
||||||
player.seekTo(trueIndex, C.TIME_UNSET) // Handles remaining custom logic
|
kernel.goto(trueIndex)
|
||||||
if (!playbackSettings.rememberPause) {
|
if (!playbackSettings.rememberPause) {
|
||||||
player.play()
|
kernel.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) {
|
||||||
val currTimeline = player.currentTimeline
|
kernel.addBottomMediaItems(songs.map { it.buildMediaItem() })
|
||||||
val nextIndex =
|
|
||||||
if (currTimeline.isEmpty) {
|
|
||||||
C.INDEX_UNSET
|
|
||||||
} else {
|
|
||||||
currTimeline.getNextWindowIndex(
|
|
||||||
player.currentMediaItemIndex, Player.REPEAT_MODE_OFF, player.shuffleModeEnabled)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextIndex == C.INDEX_UNSET) {
|
|
||||||
player.addMediaItems(songs.map { it.buildMediaItem() })
|
|
||||||
} else {
|
|
||||||
player.addMediaItems(nextIndex, 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) {
|
||||||
player.addMediaItems(songs.map { it.buildMediaItem() })
|
kernel.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 = player.unscrambleQueueIndices()
|
val indices = kernel.computeMapping()
|
||||||
if (indices.isEmpty()) {
|
if (indices.isEmpty()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val trueFrom = indices[from]
|
val trueFrom = indices[from]
|
||||||
val trueTo = indices[to]
|
val trueTo = indices[to]
|
||||||
// ExoPlayer does not actually update it's ShuffleOrder when moving items. Retain a
|
|
||||||
// semblance of "normalcy" by doing a weird no-op swap that actually moves the item.
|
kernel.moveMediaItem(trueFrom, trueTo)
|
||||||
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)
|
||||||
deferSave()
|
deferSave()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun remove(at: Int, ack: StateAck.Remove) {
|
override fun remove(at: Int, ack: StateAck.Remove) {
|
||||||
val indices = player.unscrambleQueueIndices()
|
val indices = kernel.computeMapping()
|
||||||
if (indices.isEmpty()) {
|
if (indices.isEmpty()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val trueIndex = indices[at]
|
val trueIndex = indices[at]
|
||||||
val songWillChange = player.currentMediaItemIndex == trueIndex
|
val songWillChange = kernel.currentMediaItemIndex == trueIndex
|
||||||
player.removeMediaItem(trueIndex)
|
kernel.removeMediaItem(trueIndex)
|
||||||
if (songWillChange && !playbackSettings.rememberPause) {
|
if (songWillChange && !playbackSettings.rememberPause) {
|
||||||
player.play()
|
kernel.play()
|
||||||
}
|
}
|
||||||
playbackManager.ack(this, ack)
|
playbackManager.ack(this, ack)
|
||||||
deferSave()
|
deferSave()
|
||||||
|
@ -372,16 +379,8 @@ class ExoPlaybackStateHolder(
|
||||||
sendEvent = true
|
sendEvent = true
|
||||||
}
|
}
|
||||||
if (rawQueue != resolveQueue()) {
|
if (rawQueue != resolveQueue()) {
|
||||||
player.setMediaItems(rawQueue.heap.map { it.buildMediaItem() })
|
kernel.prepareSaved(rawQueue.heap.map { it.buildMediaItem() }, rawQueue.shuffledMapping, rawQueue.heapIndex, rawQueue.isShuffled)
|
||||||
if (rawQueue.isShuffled) {
|
kernel.pause()
|
||||||
player.shuffleModeEnabled = true
|
|
||||||
player.setShuffleOrder(BetterShuffleOrder(rawQueue.shuffledMapping.toIntArray()))
|
|
||||||
} else {
|
|
||||||
player.shuffleModeEnabled = false
|
|
||||||
}
|
|
||||||
player.seekTo(rawQueue.heapIndex, C.TIME_UNSET)
|
|
||||||
player.prepare()
|
|
||||||
player.pause()
|
|
||||||
sendEvent = true
|
sendEvent = true
|
||||||
}
|
}
|
||||||
if (sendEvent) {
|
if (sendEvent) {
|
||||||
|
@ -404,7 +403,7 @@ class ExoPlaybackStateHolder(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun reset(ack: StateAck.NewPlayback) {
|
override fun reset(ack: StateAck.NewPlayback) {
|
||||||
player.setMediaItems(listOf())
|
kernel.discard()
|
||||||
playbackManager.ack(this, ack)
|
playbackManager.ack(this, ack)
|
||||||
deferSave()
|
deferSave()
|
||||||
}
|
}
|
||||||
|
@ -414,7 +413,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 (player.playWhenReady) {
|
if (kernel.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
|
||||||
|
@ -436,9 +435,9 @@ class ExoPlaybackStateHolder(
|
||||||
override fun onPlaybackStateChanged(playbackState: Int) {
|
override fun onPlaybackStateChanged(playbackState: Int) {
|
||||||
super.onPlaybackStateChanged(playbackState)
|
super.onPlaybackStateChanged(playbackState)
|
||||||
|
|
||||||
if (playbackState == Player.STATE_ENDED && player.repeatMode == Player.REPEAT_MODE_OFF) {
|
if (playbackState == Player.STATE_ENDED && kernel.repeatMode == Player.REPEAT_MODE_OFF) {
|
||||||
goto(0)
|
goto(0)
|
||||||
player.pause()
|
kernel.pause()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -491,18 +490,6 @@ class ExoPlaybackStateHolder(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- PLAYBACKSETTINGS OVERRIDES ---
|
|
||||||
|
|
||||||
override fun onPauseOnRepeatChanged() {
|
|
||||||
super.onPauseOnRepeatChanged()
|
|
||||||
updatePauseOnRepeat()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updatePauseOnRepeat() {
|
|
||||||
player.pauseAtEndOfMediaItems =
|
|
||||||
player.repeatMode == Player.REPEAT_MODE_ONE && playbackSettings.pauseOnRepeat
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun save(cb: () -> Unit) {
|
private fun save(cb: () -> Unit) {
|
||||||
saveJob {
|
saveJob {
|
||||||
persistenceRepository.saveState(playbackManager.toSavedState())
|
persistenceRepository.saveState(playbackManager.toSavedState())
|
||||||
|
@ -533,100 +520,6 @@ class ExoPlaybackStateHolder(
|
||||||
private val MediaItem.song: Song?
|
private val MediaItem.song: Song?
|
||||||
get() = this.localConfiguration?.tag as? Song?
|
get() = this.localConfiguration?.tag as? Song?
|
||||||
|
|
||||||
private fun Player.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
|
|
||||||
}
|
|
||||||
|
|
||||||
class Factory
|
|
||||||
@Inject
|
|
||||||
constructor(
|
|
||||||
@ApplicationContext private val context: Context,
|
|
||||||
private val playbackManager: PlaybackStateManager,
|
|
||||||
private val persistenceRepository: PersistenceRepository,
|
|
||||||
private val playbackSettings: PlaybackSettings,
|
|
||||||
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(
|
|
||||||
context,
|
|
||||||
exoPlayer,
|
|
||||||
playbackManager,
|
|
||||||
persistenceRepository,
|
|
||||||
playbackSettings,
|
|
||||||
commandFactory,
|
|
||||||
replayGainProcessor,
|
|
||||||
musicRepository,
|
|
||||||
imageSettings)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
const val SAVE_BUFFER = 5000L
|
const val SAVE_BUFFER = 5000L
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,181 @@
|
||||||
|
package org.oxycblt.auxio.playback.service
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 fun computeHeap(): List<MediaItem> {
|
||||||
|
return (0 until exoPlayer.mediaItemCount).map { exoPlayer.getMediaItemAt(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun computeMapping(): List<Int> {
|
||||||
|
val timeline = exoPlayer.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 = exoPlayer.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
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
override fun hasNextMediaItem() = exoPlayer.hasNextMediaItem()
|
||||||
|
override fun seekToPrevious() = exoPlayer.seekToPrevious()
|
||||||
|
override fun seekToPreviousMediaItem() = exoPlayer.seekToPreviousMediaItem()
|
||||||
|
override fun hasPreviousMediaItem() = exoPlayer.hasPreviousMediaItem()
|
||||||
|
|
||||||
|
override fun prepareNew(mediaItems: List<MediaItem>, startIndex: Int?, shuffled: Boolean) {
|
||||||
|
exoPlayer.shuffleModeEnabled = shuffled
|
||||||
|
exoPlayer.setMediaItems(mediaItems)
|
||||||
|
if (shuffled) {
|
||||||
|
exoPlayer.setShuffleOrder(BetterShuffleOrder(mediaItems.size, startIndex ?: -1))
|
||||||
|
}
|
||||||
|
val target = startIndex ?: exoPlayer.currentTimeline.getFirstWindowIndex(shuffled)
|
||||||
|
exoPlayer.seekTo(target, C.TIME_UNSET)
|
||||||
|
exoPlayer.prepare()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun prepareSaved(mediaItems: List<MediaItem>, mapping: List<Int>, index: Int, shuffled: Boolean) {
|
||||||
|
exoPlayer.setMediaItems(mediaItems)
|
||||||
|
if (shuffled) {
|
||||||
|
exoPlayer.shuffleModeEnabled = true
|
||||||
|
exoPlayer.setShuffleOrder(BetterShuffleOrder(mapping.toIntArray()))
|
||||||
|
} else {
|
||||||
|
exoPlayer.shuffleModeEnabled = false
|
||||||
|
}
|
||||||
|
exoPlayer.seekTo(index, C.TIME_UNSET)
|
||||||
|
exoPlayer.prepare()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun discard() {
|
||||||
|
exoPlayer.setMediaItems(emptyList())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun addTopMediaItems(mediaItems: List<MediaItem>) {
|
||||||
|
val currTimeline = exoPlayer.currentTimeline
|
||||||
|
val nextIndex =
|
||||||
|
if (currTimeline.isEmpty) {
|
||||||
|
C.INDEX_UNSET
|
||||||
|
} else {
|
||||||
|
currTimeline.getNextWindowIndex(
|
||||||
|
exoPlayer.currentMediaItemIndex, Player.REPEAT_MODE_OFF, exoPlayer.shuffleModeEnabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextIndex == C.INDEX_UNSET) {
|
||||||
|
exoPlayer.addMediaItems(mediaItems)
|
||||||
|
} else {
|
||||||
|
exoPlayer.addMediaItems(nextIndex, mediaItems)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun addBottomMediaItems(mediaItems: List<MediaItem>) {
|
||||||
|
exoPlayer.addMediaItems(mediaItems)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun moveMediaItem(fromIndex: Int, toIndex: Int) {
|
||||||
|
// ExoPlayer does not actually update it's ShuffleOrder when moving items. Retain a
|
||||||
|
// semblance of "normalcy" by doing a weird no-op swap that actually moves the item.
|
||||||
|
when {
|
||||||
|
fromIndex > toIndex -> {
|
||||||
|
exoPlayer.moveMediaItem(fromIndex, toIndex)
|
||||||
|
exoPlayer.moveMediaItem(toIndex + 1, fromIndex)
|
||||||
|
}
|
||||||
|
toIndex > fromIndex -> {
|
||||||
|
exoPlayer.moveMediaItem(fromIndex, toIndex)
|
||||||
|
exoPlayer.moveMediaItem(toIndex - 1, fromIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun removeMediaItem(index: Int) = exoPlayer.removeMediaItem(index)
|
||||||
|
|
||||||
|
override fun shuffled(shuffled: Boolean) {
|
||||||
|
exoPlayer.setShuffleModeEnabled(shuffled)
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun onPauseOnRepeatChanged() {
|
||||||
|
super.onPauseOnRepeatChanged()
|
||||||
|
updatePauseOnRepeat()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updatePauseOnRepeat() {
|
||||||
|
exoPlayer.pauseAtEndOfMediaItems =
|
||||||
|
exoPlayer.repeatMode == Player.REPEAT_MODE_ONE && playbackSettings.pauseOnRepeat
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
package org.oxycblt.auxio.playback.service
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
interface PlayerKernel {
|
||||||
|
// REPLICAS
|
||||||
|
val isPlaying: Boolean
|
||||||
|
var playWhenReady: Boolean
|
||||||
|
val currentPosition: Long
|
||||||
|
@get:Player.RepeatMode var repeatMode: Int
|
||||||
|
val audioSessionId: Int
|
||||||
|
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()
|
||||||
|
fun hasNextMediaItem(): Boolean
|
||||||
|
fun seekToPrevious()
|
||||||
|
fun seekToPreviousMediaItem()
|
||||||
|
fun hasPreviousMediaItem(): Boolean
|
||||||
|
|
||||||
|
fun moveMediaItem(fromIndex: Int, toIndex: Int)
|
||||||
|
fun removeMediaItem(index: Int)
|
||||||
|
|
||||||
|
// EXTENSIONS
|
||||||
|
fun computeHeap(): List<MediaItem>
|
||||||
|
fun computeMapping(): List<Int>
|
||||||
|
fun computeFirstMediaItemIndex(): Int
|
||||||
|
|
||||||
|
fun prepareNew(mediaItems: List<MediaItem>, startIndex: Int?, shuffled: Boolean)
|
||||||
|
fun prepareSaved(mediaItems: List<MediaItem>, mapping: List<Int>, index: Int, shuffled: Boolean)
|
||||||
|
fun discard()
|
||||||
|
|
||||||
|
fun addTopMediaItems(mediaItems: List<MediaItem>)
|
||||||
|
fun addBottomMediaItems(mediaItems: List<MediaItem>)
|
||||||
|
fun shuffled(shuffled: Boolean)
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue