React-native-track-player: Clarify/Unify the states

Created on 16 May 2020  路  6Comments  路  Source: react-native-kit/react-native-track-player

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 idle
  • PLAYING: Self-explanatory / outputting audio
  • PAUSED: Self-explanatory
  • BUFFERING: The player was playing but it's waiting to buffer
  • CONNECTING: The player is loading the media but not in the play state
  • STOPPED: The player has ended

The 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 BUFFERING
  • isPaused(state) whether the state is PAUSED or CONNECTING
  • isBuffering(state) whether the state is BUFFERING or CONNECTING

Notice 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.

Docs Enhancement

Most helpful comment

@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.Playing

    • ExoPlayer: STATE_READY && playWhenReady

    • SwiftAudio: playing

    • UWP: Playing

    • HTML5 Audio: !paused && !ended && !buffering && playWhenReady

  • State.Paused

    • ExoPlayer: STATE_READY && !playWhenReady

    • SwiftAudio: paused

    • UWP: Paused, HTML5 Audio paused && !ended

  • State.Buffering

    • ExoPlayer: STATE_BUFFERING

    • SwiftAudio: buffering || loading

    • UWP: Opening || Buffering

    • HTML5 Audio: seeking || buffering

  • State.Ended

    • ExoPlayer: STATE_ENDED

    • SwiftAudio: idle && ended (ended would be a boolean property that is set as soon as the queue ends)

    • UWP: None && ended

    • HTML5 Audio: ended

  • State.None

    • ExoPlayer: STATE_IDLE

    • SwiftAudio: idle && !ended

    • UWP: None && !ended

    • HTML5 Audio: !loaded

  • getPlayWhenReady()

    • ExoPlayer: getPlayWhenReady()

    • SwiftAudio: Either a manually controlled boolean property or the playing || buffering states

    • UWP: Either a manually controlled boolean property or the Playing || Buffering states

    • HTML5 Audio: !paused

All 6 comments

+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.

State Mapping

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.

Unused

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._

Not Defined

  • CONNECTING: NOT defined in the iOS module.

Here are some other thoughts that may contribute to this thread.

States and UI

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.

Inconsistent State Transitions

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:

  • PLAYING: The player is outputting audio
  • PAUSED: The player is paused
  • BUFFERING: The player has paused to load data
  • ENDED: The queue has ended
  • NONE: The queue is empty (the player is idle)

The ones that I would remove:

  • CONNECTING: with the play property in place, we don't need another BUFFERING state when the track is paused.
  • READY: I still don't see a reason for this, I think we could skip this one and go directly to the equivalent state instead, like PAUSED
  • STOPPED: I don't see an use case for when stop() is called, I replaced it with ENDED for when the player finishes playing

With 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.Playing

    • ExoPlayer: STATE_READY && playWhenReady

    • SwiftAudio: playing

    • UWP: Playing

    • HTML5 Audio: !paused && !ended && !buffering && playWhenReady

  • State.Paused

    • ExoPlayer: STATE_READY && !playWhenReady

    • SwiftAudio: paused

    • UWP: Paused, HTML5 Audio paused && !ended

  • State.Buffering

    • ExoPlayer: STATE_BUFFERING

    • SwiftAudio: buffering || loading

    • UWP: Opening || Buffering

    • HTML5 Audio: seeking || buffering

  • State.Ended

    • ExoPlayer: STATE_ENDED

    • SwiftAudio: idle && ended (ended would be a boolean property that is set as soon as the queue ends)

    • UWP: None && ended

    • HTML5 Audio: ended

  • State.None

    • ExoPlayer: STATE_IDLE

    • SwiftAudio: idle && !ended

    • UWP: None && !ended

    • HTML5 Audio: !loaded

  • getPlayWhenReady()

    • ExoPlayer: getPlayWhenReady()

    • SwiftAudio: Either a manually controlled boolean property or the playing || buffering states

    • UWP: Either a manually controlled boolean property or the Playing || Buffering states

    • HTML5 Audio: !paused

hello everyone, any update for this issuse?

Was this page helpful?
0 / 5 - 0 ratings

Related issues

EhteshamAnwar picture EhteshamAnwar  路  3Comments

tarahiw picture tarahiw  路  3Comments

amed picture amed  路  4Comments

RiccardoNL picture RiccardoNL  路  3Comments

toooldmohammad picture toooldmohammad  路  3Comments