playback: migrate to reactive model

Migrate the playback system to a reactive model where internalPlayer
is now the complete source of truth for the playback state.

This removes the observer pattern for positions and instead introduces
a new State datatype that allows consumers to reactively calculate
where the position probably is.

This is actually really great for efficiency and state coherency, and
is really what I was trying to aim for with previous (failed) reworks
to the playback system. There's probably some bugs, but way less than
the ground-up rewrites I tried before.

This also lays the groundwork for gapless playback, as the internal
player framework is now completely capable of having more functionality
borged into it.
This commit is contained in:
Alexander Capehart 2022-09-06 11:40:21 -06:00
parent a9bbdff25d
commit e5d7cdc340
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
9 changed files with 212 additions and 172 deletions

View file

@ -45,8 +45,6 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
* - The RecyclerView data for each fragment
* - The sorts for each type of data
* @author OxygenCobalt
*
* TODO: Unify how detail items are indicated [When playlists are implemented]
*/
class DetailViewModel(application: Application) :
AndroidViewModel(application), MusicStore.Callback {

View file

@ -189,11 +189,7 @@ class Task(context: Context, private val raw: Song.Raw) {
val id = tag.key.sanitize().uppercase()
val value = tag.value.sanitize()
if (value.isNotEmpty()) {
if (vorbisTags.containsKey(id)) {
vorbisTags[id]!!.add(value)
} else {
vorbisTags[id] = mutableListOf(value)
}
vorbisTags.getOrPut(id) { mutableListOf() }.add(value)
}
}
}

View file

@ -20,6 +20,8 @@ package org.oxycblt.auxio.playback
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
@ -78,6 +80,8 @@ class PlaybackViewModel(application: Application) :
val currentAudioSessionId: Int?
get() = playbackManager.currentAudioSessionId
private var lastPositionJob: Job? = null
init {
playbackManager.addCallback(this)
}
@ -209,7 +213,7 @@ class PlaybackViewModel(application: Application) :
/** Flip the playing status, e.g from playing to paused */
fun invertPlaying() {
playbackManager.isPlaying = !playbackManager.isPlaying
playbackManager.changePlaying(!playbackManager.playerState.isPlaying)
}
/** Flip the shuffle status, e.g from on to off. Will keep song by default. */
@ -268,12 +272,18 @@ class PlaybackViewModel(application: Application) :
_parent.value = playbackManager.parent
}
override fun onPositionChanged(positionMs: Long) {
_positionDs.value = positionMs.msToDs()
}
override fun onStateChanged(state: InternalPlayer.State) {
_isPlaying.value = state.isPlaying
override fun onPlayingChanged(isPlaying: Boolean) {
_isPlaying.value = isPlaying
// Start watching the position again
lastPositionJob?.cancel()
lastPositionJob =
viewModelScope.launch {
while (true) {
_positionDs.value = state.calculateElapsedPosition().msToDs()
delay(100)
}
}
}
override fun onShuffledChanged(isShuffled: Boolean) {

View file

@ -39,8 +39,6 @@ import org.oxycblt.auxio.util.logD
* Instead, we wrap it in a safe class that hopefully implements enough sanity checks to not crash
* the app or result in blatantly janky behavior. Mostly.
*
* TODO: Add smooth seeking
*
* @author OxygenCobalt
*/
class StyledSeekBar
@ -69,15 +67,19 @@ constructor(
var positionDs: Long
get() = binding.seekBarSlider.value.toLong()
set(value) {
// Sanity check 1: Ensure that no negative values are sneaking their way into
// this component.
val from = max(value, 0)
// Sanity check: Ensure that this value is within the duration and will not crash
// the app, and that the user is not currently seeking (which would cause the SeekBar
// to jump around).
if (value <= durationDs && !isActivated) {
binding.seekBarSlider.value = value.toFloat()
if (from <= durationDs && !isActivated) {
binding.seekBarSlider.value = from.toFloat()
// We would want to keep this in the callback, but the callback only fires when
// a value changes completely, and sometimes that does not happen with this view.
binding.seekBarPosition.text = value.formatDurationDs(true)
binding.seekBarPosition.text = from.formatDurationDs(true)
}
}

View file

@ -18,6 +18,8 @@
package org.oxycblt.auxio.playback.state
import android.net.Uri
import android.os.SystemClock
import android.support.v4.media.session.PlaybackStateCompat
import org.oxycblt.auxio.music.Song
/** Represents a class capable of managing the internal player. */
@ -28,14 +30,16 @@ interface InternalPlayer {
/** Whether the player should rewind instead of going to the previous song. */
val shouldRewindWithPrev: Boolean
val currentState: State
/** Called when a new song should be loaded into the player. */
fun loadSong(song: Song?)
fun loadSong(song: Song?, play: Boolean)
/** Seek to [positionMs] in the player. */
fun seekTo(positionMs: Long)
/** Called when the playing state is changed. */
fun onPlayingChanged(isPlaying: Boolean)
/** Called when the playing state needs to be changed. */
fun changePlaying(isPlaying: Boolean)
/**
* Called when [PlaybackStateManager] desires some [Action] to be completed. Returns true if the
@ -43,6 +47,77 @@ interface InternalPlayer {
*/
fun onAction(action: Action): Boolean
class State
private constructor(
/**
* Whether the user has actually chosen to play this audio. The player might not actually be
* playing at this time.
*/
val isPlaying: Boolean,
/** Whether the player is actually advancing through the audio. */
private val isAdvancing: Boolean,
/** The initial position at update time. */
private val initPositionMs: Long,
/** The time this instance was created. */
private val creationTime: Long
) {
/**
* Calculate the estimated position that the player is now at. If the player's position is
* not advancing, this will be the initial position. Otherwise, this will be the position
* plus the elapsed time since this state was uploaded.
*/
fun calculateElapsedPosition() =
if (isAdvancing) {
initPositionMs + (SystemClock.elapsedRealtime() - creationTime)
} else {
// Not advancing due to buffering or some unrelated pausing, such as
// a transient audio focus change.
initPositionMs
}
/** Load this state into the analogous [PlaybackStateCompat.Builder]. */
fun intoPlaybackState(builder: PlaybackStateCompat.Builder): PlaybackStateCompat.Builder =
builder.setState(
if (isPlaying) {
PlaybackStateCompat.STATE_PLAYING
} else {
PlaybackStateCompat.STATE_PAUSED
},
initPositionMs,
if (isAdvancing) {
1f
} else {
// Not advancing, so don't move the position.
0f
},
creationTime)
override fun equals(other: Any?) =
other is State &&
isPlaying == other.isPlaying &&
isAdvancing == other.isAdvancing &&
initPositionMs == other.initPositionMs
override fun hashCode(): Int {
var result = isPlaying.hashCode()
result = 31 * result + isAdvancing.hashCode()
result = 31 * result + initPositionMs.hashCode()
return result
}
companion object {
/** Create a new instance of this state. */
fun new(isPlaying: Boolean, isAdvancing: Boolean, positionMs: Long) =
State(
// Minor sanity check: Make sure that advancing can't occur if the
// main playing value is paused.
isPlaying,
isPlaying && isAdvancing,
positionMs,
SystemClock.elapsedRealtime())
}
}
sealed class Action {
object RestoreState : Action()
object ShuffleAll : Action()

View file

@ -52,6 +52,8 @@ import org.oxycblt.auxio.util.logW
*/
class PlaybackStateManager private constructor() {
private val musicStore = MusicStore.getInstance()
private val callbacks = mutableListOf<Callback>()
private var internalPlayer: InternalPlayer? = null
/** The currently playing song. Null if there isn't one */
val song
@ -67,14 +69,10 @@ class PlaybackStateManager private constructor() {
var index = -1
private set
/** Whether playback is playing or not */
var isPlaying = false
set(value) {
field = value
notifyPlayingChanged()
}
/** The current playback progress */
private var positionMs = 0L
/** The current state of the internal player. */
var playerState = InternalPlayer.State.new(isPlaying = false, isAdvancing = false, 0)
private set
/** The current [RepeatMode] */
var repeatMode = RepeatMode.NONE
set(value) {
@ -94,22 +92,16 @@ class PlaybackStateManager private constructor() {
get() = internalPlayer?.audioSessionId
/** An action that is awaiting the internal player instance to consume it. */
var pendingAction: InternalPlayer.Action? = null
// --- CALLBACKS ---
private val callbacks = mutableListOf<Callback>()
private var internalPlayer: InternalPlayer? = null
private var pendingAction: InternalPlayer.Action? = null
/** Add a callback to this instance. Make sure to remove it when done. */
@Synchronized
fun addCallback(callback: Callback) {
if (isInitialized) {
callback.onNewPlayback(index, queue, parent)
callback.onPositionChanged(positionMs)
callback.onPlayingChanged(isPlaying)
callback.onRepeatChanged(repeatMode)
callback.onShuffledChanged(isShuffled)
callback.onStateChanged(playerState)
}
callbacks.add(callback)
@ -130,10 +122,10 @@ class PlaybackStateManager private constructor() {
}
if (isInitialized) {
internalPlayer.loadSong(song)
internalPlayer.seekTo(positionMs)
internalPlayer.onPlayingChanged(isPlaying)
internalPlayer.loadSong(song, playerState.isPlaying)
internalPlayer.seekTo(playerState.calculateElapsedPosition())
requestAction(internalPlayer)
synchronizeState(internalPlayer)
}
this.internalPlayer = internalPlayer
@ -155,6 +147,7 @@ class PlaybackStateManager private constructor() {
/** Play a [song]. */
@Synchronized
fun play(song: Song, playbackMode: PlaybackMode, settings: Settings) {
val internalPlayer = internalPlayer ?: return
val library = musicStore.library ?: return
parent =
@ -169,33 +162,44 @@ class PlaybackStateManager private constructor() {
}
applyNewQueue(library, settings, settings.keepShuffle && isShuffled, song)
notifyNewPlayback()
notifyShuffledChanged()
isPlaying = true
internalPlayer.loadSong(song, true)
isInitialized = true
}
/** Play a [parent], such as an artist or album. */
@Synchronized
fun play(parent: MusicParent, shuffled: Boolean, settings: Settings) {
val internalPlayer = internalPlayer ?: return
val library = musicStore.library ?: return
this.parent = parent
applyNewQueue(library, settings, shuffled, null)
notifyNewPlayback()
notifyShuffledChanged()
isPlaying = true
internalPlayer.loadSong(song, true)
isInitialized = true
}
/** Shuffle all songs. */
@Synchronized
fun shuffleAll(settings: Settings) {
val internalPlayer = internalPlayer ?: return
val library = musicStore.library ?: return
parent = null
applyNewQueue(library, settings, true, null)
notifyNewPlayback()
notifyShuffledChanged()
isPlaying = true
internalPlayer.loadSong(song, true)
isInitialized = true
}
@ -204,36 +208,41 @@ class PlaybackStateManager private constructor() {
/** Go to the next song, along with doing all the checks that entails. */
@Synchronized
fun next() {
val internalPlayer = internalPlayer ?: return
// Increment the index, if it cannot be incremented any further, then
// repeat and pause/resume playback depending on the setting
if (index < _queue.lastIndex) {
gotoImpl(index + 1, true)
gotoImpl(internalPlayer, index + 1, true)
} else {
gotoImpl(0, repeatMode == RepeatMode.ALL)
gotoImpl(internalPlayer, 0, repeatMode == RepeatMode.ALL)
}
}
/** Go to the previous song, doing any checks that are needed. */
@Synchronized
fun prev() {
val internalPlayer = internalPlayer ?: return
// If enabled, rewind before skipping back if the position is past 3 seconds [3000ms]
if (internalPlayer?.shouldRewindWithPrev == true) {
if (internalPlayer.shouldRewindWithPrev) {
rewind()
isPlaying = true
changePlaying(true)
} else {
gotoImpl(max(index - 1, 0), true)
gotoImpl(internalPlayer, max(index - 1, 0), true)
}
}
@Synchronized
fun goto(index: Int) {
gotoImpl(index, true)
val internalPlayer = internalPlayer ?: return
gotoImpl(internalPlayer, index, true)
}
private fun gotoImpl(idx: Int, play: Boolean) {
private fun gotoImpl(internalPlayer: InternalPlayer, idx: Int, play: Boolean) {
index = idx
notifyIndexMoved()
isPlaying = play
internalPlayer.loadSong(song, play)
}
/** Add a [song] to the top of the queue. */
@ -330,19 +339,17 @@ class PlaybackStateManager private constructor() {
// --- INTERNAL PLAYER FUNCTIONS ---
/** Update the current [positionMs]. Only meant for use by [InternalPlayer] */
@Synchronized
fun synchronizePosition(internalPlayer: InternalPlayer, positionMs: Long) {
fun synchronizeState(internalPlayer: InternalPlayer) {
if (BuildConfig.DEBUG && this.internalPlayer !== internalPlayer) {
logW("Given internal player did not match current internal player")
return
}
// Don't accept any bugged positions that are over the duration of the song.
val maxDuration = song?.durationMs ?: -1
if (positionMs <= maxDuration) {
this.positionMs = positionMs
notifyPositionChanged()
val newState = internalPlayer.currentState
if (newState != playerState) {
playerState = newState
notifyStateChanged()
}
}
@ -350,7 +357,7 @@ class PlaybackStateManager private constructor() {
fun startAction(action: InternalPlayer.Action) {
val internalPlayer = internalPlayer
if (internalPlayer == null || !internalPlayer.onAction(action)) {
logD("Internal player not present or did not consume action, ignoring")
logD("Internal player not present or did not consume action, waiting")
pendingAction = action
}
}
@ -369,15 +376,18 @@ class PlaybackStateManager private constructor() {
}
}
/** Change the current playing state. */
fun changePlaying(isPlaying: Boolean) {
internalPlayer?.changePlaying(isPlaying)
}
/**
* **Seek** to a [positionMs].
* @param positionMs The position to seek to in millis.
*/
@Synchronized
fun seekTo(positionMs: Long) {
this.positionMs = positionMs
internalPlayer?.seekTo(positionMs)
notifyPositionChanged()
}
/** Rewind to the beginning of a song. */
@ -392,14 +402,13 @@ class PlaybackStateManager private constructor() {
}
val library = musicStore.library ?: return false
val internalPlayer = internalPlayer ?: return false
val state = withContext(Dispatchers.IO) { database.read(library) }
synchronized(this) {
if (state != null && (!isInitialized || force)) {
// Continuing playback while also possibly doing drastic state updates is
// a bad idea, so pause.
isPlaying = false
index = state.index
parent = state.parent
_queue = state.queue.toMutableList()
@ -407,10 +416,12 @@ class PlaybackStateManager private constructor() {
isShuffled = state.isShuffled
notifyNewPlayback()
seekTo(state.positionMs)
notifyRepeatModeChanged()
notifyShuffledChanged()
internalPlayer.loadSong(song, false)
internalPlayer.seekTo(state.positionMs)
isInitialized = true
return true
@ -440,13 +451,15 @@ class PlaybackStateManager private constructor() {
return
}
val internalPlayer = internalPlayer ?: return
logD("Sanitizing state")
// While we could just save and reload the state, we instead sanitize the state
// at runtime for better efficiency (and to sidestep a co-routine on behalf of the caller).
// at runtime for better performance (and to sidestep a co-routine on behalf of the caller).
val oldSongId = song?.id
val oldPosition = positionMs
val oldPosition = playerState.calculateElapsedPosition()
parent =
parent?.let {
@ -463,10 +476,11 @@ class PlaybackStateManager private constructor() {
index--
}
notifyNewPlayback()
// Continuing playback while also possibly doing drastic state updates is
// a bad idea, so pause.
isPlaying = false
notifyNewPlayback()
internalPlayer.loadSong(song, false)
if (index > -1) {
// Internal player may have reloaded the media item, re-seek to the previous position
@ -479,14 +493,13 @@ class PlaybackStateManager private constructor() {
index = index,
parent = parent,
queue = _queue,
positionMs = positionMs,
positionMs = playerState.calculateElapsedPosition(),
isShuffled = isShuffled,
repeatMode = repeatMode)
// --- CALLBACKS ---
private fun notifyIndexMoved() {
internalPlayer?.loadSong(song)
for (callback in callbacks) {
callback.onIndexMoved(index)
}
@ -505,22 +518,14 @@ class PlaybackStateManager private constructor() {
}
private fun notifyNewPlayback() {
internalPlayer?.loadSong(song)
for (callback in callbacks) {
callback.onNewPlayback(index, queue, parent)
}
}
private fun notifyPlayingChanged() {
internalPlayer?.onPlayingChanged(isPlaying)
private fun notifyStateChanged() {
for (callback in callbacks) {
callback.onPlayingChanged(isPlaying)
}
}
private fun notifyPositionChanged() {
for (callback in callbacks) {
callback.onPositionChanged(positionMs)
callback.onStateChanged(playerState)
}
}
@ -553,11 +558,8 @@ class PlaybackStateManager private constructor() {
/** Called when playback is changed completely, with a new index, queue, and parent. */
fun onNewPlayback(index: Int, queue: List<Song>, parent: MusicParent?) {}
/** Called when the playing state is changed. */
fun onPlayingChanged(isPlaying: Boolean) {}
/** Called when the position is re-synchronized by the internal player. */
fun onPositionChanged(positionMs: Long) {}
/** Called when the state of the internal player changes. */
fun onStateChanged(state: InternalPlayer.State) {}
/** Called when the repeat mode is changed. */
fun onRepeatChanged(repeatMode: RepeatMode) {}

View file

@ -22,18 +22,17 @@ import android.content.Intent
import android.graphics.Bitmap
import android.net.Uri
import android.os.Bundle
import android.os.SystemClock
import android.support.v4.media.MediaDescriptionCompat
import android.support.v4.media.MediaMetadataCompat
import android.support.v4.media.session.MediaSessionCompat
import android.support.v4.media.session.PlaybackStateCompat
import androidx.media.session.MediaButtonReceiver
import com.google.android.exoplayer2.Player
import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.R
import org.oxycblt.auxio.image.BitmapProvider
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.state.InternalPlayer
import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.playback.state.RepeatMode
import org.oxycblt.auxio.settings.Settings
@ -59,26 +58,16 @@ import org.oxycblt.auxio.util.logD
* Replace it. Please.
*
* @author OxygenCobalt
*
* TODO: Remove the player callback once smooth seeking is implemented
*/
class MediaSessionComponent(
private val context: Context,
private val player: Player,
private val callback: Callback
) :
Player.Listener,
MediaSessionCompat.Callback(),
PlaybackStateManager.Callback,
Settings.Callback {
class MediaSessionComponent(private val context: Context, private val callback: Callback) :
MediaSessionCompat.Callback(), PlaybackStateManager.Callback, Settings.Callback {
interface Callback {
fun onPostNotification(notification: NotificationComponent?, reason: PostingReason)
}
enum class PostingReason {
METADATA,
ACTIONS,
POSITION
ACTIONS
}
private val mediaSession =
@ -94,7 +83,6 @@ class MediaSessionComponent(
private val provider = BitmapProvider(context)
init {
player.addListener(this)
playbackManager.addCallback(this)
mediaSession.setCallback(this)
}
@ -106,7 +94,6 @@ class MediaSessionComponent(
fun release() {
provider.release()
settings.release()
player.removeListener(this)
playbackManager.removeCallback(this)
mediaSession.apply {
@ -225,13 +212,10 @@ class MediaSessionComponent(
mediaSession.setQueue(queueItems)
}
override fun onPlayingChanged(isPlaying: Boolean) {
override fun onStateChanged(state: InternalPlayer.State) {
invalidateSessionState()
notification.updatePlaying(playbackManager.isPlaying)
notification.updatePlaying(playbackManager.playerState.isPlaying)
if (!provider.isBusy) {
// Still probably want to start the notification though regardless of the version,
// as playback is starting.
callback.onPostNotification(notification, PostingReason.ACTIONS)
}
}
@ -269,25 +253,6 @@ class MediaSessionComponent(
}
}
// --- EXOPLAYER CALLBACKS ---
override fun onPositionDiscontinuity(
oldPosition: Player.PositionInfo,
newPosition: Player.PositionInfo,
reason: Int
) {
invalidateSessionState()
if (!playbackManager.isPlaying) {
// Hack around issue where the position won't update after a seek when paused.
// Apparently this can be fixed by re-posting the notification, but not always
// when we invalidate the state (that will cause us to be rate-limited), and also not
// always when we seek (that will also cause us to be rate-limited). Someone looked at
// this system and said it was well-designed.
callback.onPostNotification(notification, PostingReason.POSITION)
}
}
// --- MEDIASESSION CALLBACKS ---
override fun onPlayFromMediaId(mediaId: String?, extras: Bundle?) {
@ -316,11 +281,11 @@ class MediaSessionComponent(
}
override fun onPlay() {
playbackManager.isPlaying = true
playbackManager.changePlaying(true)
}
override fun onPause() {
playbackManager.isPlaying = false
playbackManager.changePlaying(false)
}
override fun onSkipToNext() {
@ -341,7 +306,7 @@ class MediaSessionComponent(
override fun onRewind() {
playbackManager.rewind()
playbackManager.isPlaying = true
playbackManager.changePlaying(true)
}
override fun onSetRepeatMode(repeatMode: Int) {
@ -391,17 +356,9 @@ class MediaSessionComponent(
val state =
PlaybackStateCompat.Builder()
.setActions(ACTIONS)
.setBufferedPosition(player.bufferedPosition)
.setActiveQueueItemId(playbackManager.index.toLong())
val playerState =
if (playbackManager.isPlaying) {
PlaybackStateCompat.STATE_PLAYING
} else {
PlaybackStateCompat.STATE_PAUSED
}
state.setState(playerState, player.currentPosition, 1.0f, SystemClock.elapsedRealtime())
playbackManager.playerState.intoPlaybackState(state)
// Android 13+ leverages custom actions in the notification.

View file

@ -39,10 +39,10 @@ import com.google.android.exoplayer2.ext.flac.LibflacAudioRenderer
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory
import com.google.android.exoplayer2.mediacodec.MediaCodecSelector
import com.google.android.exoplayer2.source.DefaultMediaSourceFactory
import kotlin.math.max
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.R
@ -102,7 +102,6 @@ class PlaybackService :
// Coroutines
private val serviceJob = Job()
private val positionScope = CoroutineScope(serviceJob + Dispatchers.Main)
private val restoreScope = CoroutineScope(serviceJob + Dispatchers.Main)
private val saveScope = CoroutineScope(serviceJob + Dispatchers.Main)
@ -149,15 +148,9 @@ class PlaybackService :
playbackManager.registerInternalPlayer(this)
musicStore.addCallback(this)
positionScope.launch {
while (true) {
playbackManager.synchronizePosition(this@PlaybackService, player.currentPosition)
delay(POS_POLL_INTERVAL)
}
}
widgetComponent = WidgetComponent(this)
mediaSessionComponent = MediaSessionComponent(this, player, this)
mediaSessionComponent = MediaSessionComponent(this, this)
IntentFilter().apply {
addAction(AudioManager.ACTION_AUDIO_BECOMING_NOISY)
@ -197,10 +190,10 @@ class PlaybackService :
foregroundManager.release()
// Pause just in case this destruction was unexpected.
playbackManager.isPlaying = false
playbackManager.changePlaying(false)
playbackManager.unregisterInternalPlayer(this)
musicStore.addCallback(this)
musicStore.removeCallback(this)
settings.release()
unregisterReceiver(systemReceiver)
serviceJob.cancel()
@ -220,15 +213,21 @@ class PlaybackService :
// --- PLAYER OVERRIDES ---
override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) {
super.onPlayWhenReadyChanged(playWhenReady, reason)
override fun onEvents(player: Player, events: Player.Events) {
super.onEvents(player, events)
if (playWhenReady) {
hasPlayed = true
var needToSynchronize =
events.containsAny(Player.EVENT_IS_PLAYING_CHANGED, Player.EVENT_POSITION_DISCONTINUITY)
if (events.contains(Player.EVENT_PLAY_WHEN_READY_CHANGED)) {
needToSynchronize = true
if (player.playWhenReady) {
hasPlayed = true
}
}
if (playbackManager.isPlaying != playWhenReady) {
playbackManager.isPlaying = playWhenReady
if (needToSynchronize) {
playbackManager.synchronizeState(this)
}
}
@ -237,7 +236,7 @@ class PlaybackService :
if (playbackManager.repeatMode == RepeatMode.TRACK) {
playbackManager.rewind()
if (settings.pauseOnRepeat) {
playbackManager.isPlaying = false
playbackManager.changePlaying(false)
}
} else {
playbackManager.next()
@ -251,14 +250,6 @@ class PlaybackService :
playbackManager.next()
}
override fun onPositionDiscontinuity(
oldPosition: Player.PositionInfo,
newPosition: Player.PositionInfo,
reason: Int
) {
playbackManager.synchronizePosition(this, player.currentPosition)
}
override fun onTracksChanged(tracks: Tracks) {
super.onTracksChanged(tracks)
@ -284,7 +275,12 @@ class PlaybackService :
override val shouldRewindWithPrev: Boolean
get() = settings.rewindWithPrev && player.currentPosition > REWIND_THRESHOLD
override fun loadSong(song: Song?) {
override val currentState: InternalPlayer.State
get() =
InternalPlayer.State.new(
player.playWhenReady, player.isPlaying, max(player.currentPosition, 0))
override fun loadSong(song: Song?, play: Boolean) {
if (song == null) {
// Stop the foreground state if there's nothing to play.
logD("Nothing playing, stopping playback")
@ -310,6 +306,8 @@ class PlaybackService :
broadcastAudioEffectAction(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION)
openAudioEffectSession = true
}
player.playWhenReady = play
}
private fun broadcastAudioEffectAction(event: String) {
@ -337,7 +335,7 @@ class PlaybackService :
player.seekTo(positionMs)
}
override fun onPlayingChanged(isPlaying: Boolean) {
override fun changePlaying(isPlaying: Boolean) {
player.playWhenReady = isPlaying
}
@ -437,7 +435,8 @@ class PlaybackService :
AudioManager.ACTION_AUDIO_BECOMING_NOISY -> pauseFromPlug()
// --- AUXIO EVENTS ---
ACTION_PLAY_PAUSE -> playbackManager.isPlaying = !playbackManager.isPlaying
ACTION_PLAY_PAUSE ->
playbackManager.changePlaying(!playbackManager.playerState.isPlaying)
ACTION_INC_REPEAT_MODE ->
playbackManager.repeatMode = playbackManager.repeatMode.increment()
ACTION_INVERT_SHUFFLE ->
@ -445,7 +444,7 @@ class PlaybackService :
ACTION_SKIP_PREV -> playbackManager.prev()
ACTION_SKIP_NEXT -> playbackManager.next()
ACTION_EXIT -> {
playbackManager.isPlaying = false
playbackManager.changePlaying(false)
stopAndSave()
}
WidgetProvider.ACTION_WIDGET_UPDATE -> widgetComponent.update()
@ -466,7 +465,7 @@ class PlaybackService :
settings.headsetAutoplay &&
initialHeadsetPlugEventHandled) {
logD("Device connected, resuming")
playbackManager.isPlaying = true
playbackManager.changePlaying(true)
}
}
@ -474,7 +473,7 @@ class PlaybackService :
private fun pauseFromPlug() {
if (playbackManager.song != null) {
logD("Device disconnected, pausing")
playbackManager.isPlaying = false
playbackManager.changePlaying(false)
}
}
}

View file

@ -28,6 +28,7 @@ import org.oxycblt.auxio.image.BitmapProvider
import org.oxycblt.auxio.image.SquareFrameTransform
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.state.InternalPlayer
import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.playback.state.RepeatMode
import org.oxycblt.auxio.settings.Settings
@ -74,7 +75,7 @@ class WidgetComponent(private val context: Context) :
}
// Note: Store these values here so they remain consistent once the bitmap is loaded.
val isPlaying = playbackManager.isPlaying
val isPlaying = playbackManager.playerState.isPlaying
val repeatMode = playbackManager.repeatMode
val isShuffled = playbackManager.isShuffled
@ -139,7 +140,7 @@ class WidgetComponent(private val context: Context) :
override fun onIndexMoved(index: Int) = update()
override fun onNewPlayback(index: Int, queue: List<Song>, parent: MusicParent?) = update()
override fun onPlayingChanged(isPlaying: Boolean) = update()
override fun onStateChanged(state: InternalPlayer.State) = update()
override fun onShuffledChanged(isShuffled: Boolean) = update()
override fun onRepeatChanged(repeatMode: RepeatMode) = update()
override fun onSettingChanged(key: String) {