The playback states are either confusing, incomplete or not equivalent on all platforms.
Here's how they work on Android:
NONE: No track is loaded / the player is idlePLAYING: Self-explanatory / outputting audioPAUSED: Self-explanatoryBUFFERING: The player was playing but it's waiting to bufferCONNECTING: The player is loading the media but not in the play stateSTOPPED: The player has endedThe READY state is unused on Android. I'm pretty sure it is supposed to be set when the track is loaded, which seems somewhat useless, since the add/skip promises should be resolved when the track is ready.
These states can become confusing if you're looking for the primary states (playing, paused or buffering). For instance, if you need to check whether the player is paused, you'll have to look for the state PAUSED or CONNECTING.
One idea is to add utility functions for each state that could be implemented in pure JS:
isPlaying(state): whether the state is PLAYING or BUFFERINGisPaused(state) whether the state is PAUSED or CONNECTINGisBuffering(state) whether the state is BUFFERING or CONNECTINGNotice that those functions have the state as an argument, the state is not directly retrieved from the module and that's by design: not only you need to retrieve the state once for multiple calls to the functions, but you can also store the state in a single variable and use that later.
@curiousdustin I'd like to know more implementation details on iOS and open discussion about things we should or not change.
+1
Moreover the player can be playing or paused and buffering at the same time. I currently wrap the player to be able to check whether the player is paused or playing, even when it is buffering.
I explain: by "playing", I mean in a playing state. I noticed that if the player ends buffering, it will plays automatically. You can pause the player before it ends buffering to prevent that.
So I would rather like an event and a getter for each "state" (actually, the notion of "state" is not well defined and the definition of it in RNTP is not consistent to me).
On iOS the state is directly mapped to the states provided by SwiftAudio.
Here are the descriptions provided by the author of SwiftAudio:
NONE -> idle: No item loaded, the player is stopped.READY -> ready: The current item is loaded, and the player is ready to start playing.PLAYING -> playing: The player is playing.PAUSED -> paused: The player is paused.BUFFERING -> loading: An asset is being loaded for playback.STOPPED -> idle: No item loaded, the player is stopped.SwiftAudio has a buffering state that is not mapped to RNTP. This state is described as: _The current item is playing, but are currently buffering._
CONNECTING: NOT defined in the iOS module.Here are some other thoughts that may contribute to this thread.
One thing I struggled with when using RNTP on my project is making the UI feel responsive.
For instance if you have a button that toggles between play / pause icons. You want it to feel like that button has done its job, and changes UI state immediately, even though the player may not immediately be performing the desired action. If you press a play button to start a track playing, the player will have to go through several other states as it loads before it is actually playing.
The UI almost needs to represent intention rather than actual state.
So, anything we can do to make this easier to deal with would be good.
As many others have mentioned in various tickets, the transitions between various states as the result of various actions, is not consistent. I don't know if this can actually be solved, but it seems to be a pitfall for many.
A completely made up example just to illustrate the point, this may not be an actual behavior:
Calling add(), then play() on iOS may result in these states
NONE -> BUFFERING -> READY -> PLAYING
While on Android it may result in these:
NONE -> BUFFERING -> PAUSED -> PLAYING
It's clear now that we need some kind of way to differentiate the actual state from the controlled state, I think we can add a boolean property to reflect the controlled play state:
getPlayWhenReady(): true as soon as play() is called, false when stop(), pause() or reset() are called. (or it's forced to pause by an interruption or by an error)Here are a few "actual state" changes that I think would help:
The ones that I would remove:
BUFFERING state when the track is paused.PAUSEDstop() is called, I replaced it with ENDED for when the player finishes playingWith the changes said above, the following code would result in these states:
await TrackPlayer.add(...)
TrackPlayer.play()
| Property | 1 | 2 | 3 | 4 | ... | 5 |
| --------- | :-: | :-: | :-: | :-: | :-: | :-: |
| State | NONE | BUFFERING | PAUSED | PLAYING | ... | ENDED |
| Play when ready | false | false | false | true | true | false |
| Triggered by | | add() | | play() | ... | |
Another example:
TrackPlayer.play()
TrackPlayer.add(...)
| Property | 1 | 2 | 3 | 4 | ... | 5 |
| --------- | :-: | :-: | :-: | :-: | :-: | :-: |
| State | NONE | NONE | BUFFERING | PLAYING | ... | ENDED |
| Play when ready | false | true | true | true | true | false |
| Triggered by | | play() | add() | | ... | |
@curiousdustin I think we should go for it. Simple states and a getter for playWhenReady should be enough to fix most logic and UI issues.
We can also send the playWhenReady value in the payload from the remote-state event. I'm not sure about the playWhenReady name though.
TrackPlayer.getPlayWhenReady()
TrackPlayer.addEventListener('remote-state', (data) => {
// data.state
// data.playWhenReady
});
I've looked it up and here is how we can implement it on each platform:
STATE_READY && playWhenReadyplayingPlaying!paused && !ended && !buffering && playWhenReadySTATE_READY && !playWhenReadypausedPaused, HTML5 Audio paused && !endedSTATE_BUFFERINGbuffering || loadingOpening || Bufferingseeking || bufferingSTATE_ENDEDidle && ended (ended would be a boolean property that is set as soon as the queue ends)None && endedendedSTATE_IDLEidle && !endedNone && !ended!loadedgetPlayWhenReady()playing || buffering statesPlaying || Buffering states!pausedhello everyone, any update for this issuse?
Most helpful comment
@curiousdustin I think we should go for it. Simple states and a getter for
playWhenReadyshould be enough to fix most logic and UI issues.We can also send the
playWhenReadyvalue in the payload from theremote-stateevent. I'm not sure about theplayWhenReadyname though.I've looked it up and here is how we can implement it on each platform:
STATE_READY && playWhenReadyplayingPlaying!paused && !ended && !buffering && playWhenReadySTATE_READY && !playWhenReadypausedPaused, HTML5 Audiopaused && !endedSTATE_BUFFERINGbuffering || loadingOpening || Bufferingseeking || bufferingSTATE_ENDEDidle && ended(ended would be a boolean property that is set as soon as the queue ends)None && endedendedSTATE_IDLEidle && !endedNone && !ended!loadedgetPlayWhenReady()playing || bufferingstatesPlaying || Bufferingstates!paused