Skip to content

Commit

Permalink
feat: useIsPlaying hook and isPlaying() method (#2040)
Browse files Browse the repository at this point in the history
* feat: useIsPlaying hook and isPlaying() method

* fix: empty line removed at end of file for prettier

* fix: added documentation and also updated API to reflect undefined possibilities

* fix: account for code review feedback

We now return undefined in all cases where the state is uncertain, and we also return both `playing` and `bufferingDuringPlay` from `isPlaying()`.
  • Loading branch information
fivecar authored Jun 28, 2023
1 parent 70f8364 commit 8331979
Show file tree
Hide file tree
Showing 5 changed files with 87 additions and 21 deletions.
24 changes: 24 additions & 0 deletions docs/docs/guides/play-button.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
sidebar_position: 4
---

# Play Buttons

UI often needs to display a Play button that changes between three states:

1. Play
2. Pause
3. Spinner (e.g. if playback is being attempted, but sound is paused due to buffering)

Implementing this correctly will take a bit of care. For instance, `usePlaybackState` can return `State.Buffering` even if playback is currently paused. `usePlayWhenReady` is one way to check if the player is attempting to play, but can return true even if `PlaybackState` is `State.Error` or `State.Ended`.

To determine how to render a Play button in its three states correctly, do the following:

* Render the button as a spinner if `playWhenReady` and `state === State.Loading || state === State.Buffering`
* Else render the button as being in the Playing state if `playWhenReady && !(state === State.Error || state === State.Buffering)`
* Otherwise render the button as being in the Paused state

To help with this logic, the API has two utilities:

1. The `useIsPlaying()` hook. This returns `{playing: boolean | undefined, bufferingDuringPlay: boolean | undefined}`, which you can consult to render your play button correctly. You should render a spinner if `bufferingDuringPlay === true`; otherwise render according to `playing`. Values are `undefined` if the player isn't yet in a state where they can be determined.
2. The `async isPlaying()` function, which returns the same result as `useIsPlaying()`, but can be used outside of React components (i.e. without hooks). Note that you can't easily just instead call `getPlaybackState()` to determine the same answer, unless you've accounted for the issues mentioned above.
26 changes: 6 additions & 20 deletions example/src/components/PlayPauseButton.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,19 @@
import React from 'react';
import { ActivityIndicator, StyleSheet, View } from 'react-native';
import TrackPlayer, {
State,
usePlayWhenReady,
} from 'react-native-track-player';
import { useDebouncedValue } from '../hooks';
import TrackPlayer, { useIsPlaying } from 'react-native-track-player';
import { Button } from './Button';

export const PlayPauseButton: React.FC<{
state: State | undefined;
}> = ({ state }) => {
const playWhenReady = usePlayWhenReady();
const isLoading = useDebouncedValue(
state === State.Loading || state === State.Buffering,
250
);
export const PlayPauseButton: React.FC = () => {
const { playing, bufferingDuringPlay } = useIsPlaying();

const isErrored = state === State.Error;
const isEnded = state === State.Ended;
const showPause = playWhenReady && !(isErrored || isEnded);
const showBuffering = playWhenReady && isLoading;
return showBuffering ? (
return bufferingDuringPlay ? (
<View style={styles.statusContainer}>
<ActivityIndicator />
</View>
) : (
<Button
title={showPause ? 'Pause' : 'Play'}
onPress={showPause ? TrackPlayer.pause : TrackPlayer.play}
title={playing ? 'Pause' : 'Play'}
onPress={playing ? TrackPlayer.pause : TrackPlayer.play}
type="primary"
style={styles.playPause}
/>
Expand Down
2 changes: 1 addition & 1 deletion example/src/components/PlayerControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export const PlayerControls: React.FC = () => {
<View style={styles.container}>
<View style={styles.row}>
<Button title="Prev" onPress={performSkipToPrevious} type="secondary" />
<PlayPauseButton state={playback.state} />
<PlayPauseButton />
<Button title="Next" onPress={performSkipToNext} type="secondary" />
</View>
<PlaybackError
Expand Down
1 change: 1 addition & 0 deletions src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './useActiveTrack';
export * from './useIsPlaying';
export * from './usePlayWhenReady';
export * from './usePlaybackState';
export * from './useProgress';
Expand Down
55 changes: 55 additions & 0 deletions src/hooks/useIsPlaying.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import TrackPlayer from '..';
import { State } from '../constants';
import { usePlayWhenReady } from './usePlayWhenReady';
import { usePlaybackState } from './usePlaybackState';

/**
* Tells whether the TrackPlayer is in a mode that most people would describe
* as "playing." Great for UI to decide whether to show a Play or Pause button.
* @returns playing - whether UI should likely show as Playing, or undefined
* if this isn't yet known.
* @returns bufferingDuringPlay - whether UI should show as Buffering, or
* undefined if this isn't yet known.
*/
export function useIsPlaying() {
const state = usePlaybackState().state;
const playWhenReady = usePlayWhenReady();

return determineIsPlaying(playWhenReady, state);
}

function determineIsPlaying(playWhenReady?: boolean, state?: State) {
if (playWhenReady === undefined || state === undefined) {
return { playing: undefined, bufferingDuringPlay: undefined };
}

const isLoading = state === State.Loading || state === State.Buffering;
const isErrored = state === State.Error;
const isEnded = state === State.Ended;

return {
playing: playWhenReady && !(isErrored || isEnded),
bufferingDuringPlay: playWhenReady && isLoading,
};
}

/**
* This exists if you need realtime status on whether the TrackPlayer is
* playing, whereas the hooks all have a delay because they depend on responding
* to events before their state is updated.
*
* It also exists whenever you need to know the play state outside of a React
* component, since hooks only work in components.
*
* @returns playing - whether UI should likely show as Playing, or undefined
* if this isn't yet known.
* @returns bufferingDuringPlay - whether UI should show as Buffering, or
* undefined if this isn't yet known.
*/
export async function isPlaying() {
const [playbackState, playWhenReady] = await Promise.all([
TrackPlayer.getPlaybackState(),
TrackPlayer.getPlayWhenReady(),
]);
return determineIsPlaying(playWhenReady, playbackState.state);
}

0 comments on commit 8331979

Please sign in to comment.