Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: useIsPlaying hook and isPlaying() method #2040

Merged
merged 4 commits into from
Jun 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
}