const TopLevelComponent = ({children}) => {
return (
<WithAudioPlayback allowConcurrentPlayback={false}>
{children}
</WithAudioPlayback>
);
};Audio Playback
As of the version stream-chat-react@13.12.0 it is possible to build audio player components with the following combo of components:
WithAudioPlaybackReact componentuseAudioPlayerhookuseActiveAudioPlayerhookuseAudioControllerhook (deprecated)
In this document we will focus on working with the non-deprecated components.
All the audio playback is controlled using the combination of WithAudioPlayback React component and useAudioPlayer hook. Component WithAudioPlayback is necessary to initiate and maintain a pool of AudioPlayer instances (that hold audio element reference). The pool is not publicly accessible. We access individual AudioPlayer instances using useAudioPlayer hook instead.
By default, the SDK renders WithAudioPlayback component inside the Channel component. All the Channel component’s children can thus reach to one shared pool of AudioPlayer instances using useAudioPlayer hook.
Audio Playback Mode
We can decide, whether we want to allow only a single or multiple audios to be played at a time.
Single-playback mode - only a single audio can be reproduced at a time (default in Channel component).
Concurrent-playback mode - multiple audios can be reproduced at the same time
It is possible to change the default (false), by setting allowConcurrentPlayback prop of WithAudioPlayback component.
Building Audio Components
If we wanted to build a React component separated from Channel and needed to play audio inside this component, we need to perform the following steps:
- Have a top-level component render
WithAudioPlayback
- Have a child component that renders the audio player UI
import type { AudioPlayer, AudioPlayerDescriptor, AudioPlayerState } from 'stream-chat-react';
import {
FileSizeIndicator,
PlayButton,
PlaybackRateButton,
useAudioPlayer,
useStateStore,
WaveProgressBar,
} from 'stream-chat-react';
/*
have a mechanism that provides a stable id so that the component gets hold of a stable instance of AudioPlayer
(e.g. generated as a combination of a message id and message parent_id or anything that makes sense for your business logic)
*/
import { useStableComponentId } from './useStableComponentId';
import {
FileIcon,
} from './yourComponents'
type AudioPlayerComponentProps = Omit<AudioPlayerDescriptor, 'id'> & {
playbackRates: number[];
};
const AudioPlayerComponent = (props: AudioPlayerComponentProps) => {
const componentID = useStableComponentId();
// retrieve the AudioPlayer instance
const audioPlayer = useAudioPlayer({
durationSeconds: props.durationSeconds,
fileSize: props.fileSize,
mimeType: props.mimeType,
playbackRates: props.playbackRates,
requester: componentID,
src: props.src,
title: props.title,
waveformData: props.waveformData,
});
return audioPlayer ? <AudioPlayerUI audioPlayer={audioPlayer}/> : null;
}
type AudioPlayerUIProps = {
audioPlayer: AudioPlayer;
}
// we need to select relevant data from AudioPlayer state
const audioPlayerStateSelector = (state: AudioPlayerState) => ({
canPlayRecord: state.canPlayRecord,
isPlaying: state.isPlaying,
playbackRate: state.currentPlaybackRate,
progress: state.progressPercent,
secondsElapsed: state.secondsElapsed,
});
const AudioPlayerUI = ({ audioPlayer }: AudioPlayerUIProps) => {
// we subscribe to AudioPlayer state changes
const { canPlayRecord, isPlaying, playbackRate, progress, secondsElapsed } =
useStateStore(audioPlayer.state, audioPlayerStateSelector);
const displayedDuration = secondsElapsed || audioPlayer.durationSeconds;
return (
<div className='audio-player-root'>
<PlayButton isPlaying={!!isPlaying} onClick={audioPlayer.togglePlay} />
<div className='audio-player-body'>
{audioPlayer.title &&
<div className='audio-player-title'>{audioPlayer.title}</div>
}
{typeof displayedDuration === 'number' &&
<div className='audio-player-duration'>{displayedDuration}</div>
}
{typeof audioPlayer.fileSize !== 'undefined' &&
<FileSizeIndicator
fileSize={audioPlayer.fileSize}
maximumFractionDigits={0}
/>
}
{!!audioPlayer.waveformData &&
<WaveProgressBar
progress={progress}
seek={audioPlayer.seek}
waveformData={audioPlayer.waveformData}
/>
}
{isPlaying ? (
<PlaybackRateButton
disabled={!canPlayRecord}
onClick={audioPlayer.increasePlaybackRate}
>
{playbackRate?.toFixed(1)}x
</PlaybackRateButton>
) : (
<FileIcon big={true} mimeType={audioPlayer.mimeType} size={40} />
)}
</div>
</div>
);
};AudioPlayer
All the control of the audio playback is executed through AudioPlayer instance retrieved using useAudioPlayerHook. The audio playback state changes are then reflected through the AudioPlayer state which we subscribe to.
AudioPlayer API
These are the AudioPlayer methods needed to create a audio player UI widget:
audioPlayer.play(params)
Starts playing the audio.
| Signature |
|---|
(params?: {currentPlaybackRate?: number; playbackRates?: number[];}) => Promise<void>; |
audioPlayer.pause()
Pauses the audio, keeps the current playback progress.
| Signature |
|---|
| () => void; |
audioPlayer.stop()
Pauses the audio and resets the playback state, including the progress.
| Signature |
|---|
| () => void; |
audioPlayer.togglePlay()
Starts playing if paused, pauses if playing
| Signature |
|---|
| () => void; |
audioPlayer.seek(params)
Navigates to the selected time in the track. The parameters:
clientX- X coordinate where in the progress bar has the user clickedcurrentTarget- progress bar root element
| Signature |
|---|
(params: { clientX: number; currentTarget: HTMLDivElement }) => Promise<void> |
AudioPlayer State
We can subscribe to AudioPlayer state changes as follows:
// determines which properties we want to observe
const audioPlayerStateSelector = (state: AudioPlayerState) => ({
canPlayRecord: state.canPlayRecord,
isPlaying: state.isPlaying,
playbackRate: state.currentPlaybackRate,
progress: state.progressPercent,
secondsElapsed: state.secondsElapsed,
// ...
});
const selectedState =
useStateStore(audioPlayer.state, audioPlayerStateSelector);The state has the following shape:
type AudioPlayerState = {
/** Signals whether the browser can play the record. */
canPlayRecord: boolean;
/** Current playback speed. Initiated with the first item of the playbackRates array. */
currentPlaybackRate: number;
/** The audio element ref */
elementRef: HTMLAudioElement | null;
/** Signals whether the playback is in progress. */
isPlaying: boolean;
/** Keeps the latest playback error reference. */
playbackError: Error | null;
/** An array of fractional numeric values of playback speed to override the defaults (1.0, 1.5, 2.0) */
playbackRates: number[];
/** Playback progress expressed in percent. */
progressPercent: number;
/** Playback progress expressed in seconds. */
secondsElapsed: number;
};Active AudioPlayer
When operating in single-playback mode it can make sense to have a global audio player UI that displays the playback of active audio player. For example if a user starts the playback from a message’s audio widget and scrolls away, we may want to show the global audio player UI from which the playback could still be controlled without having to return to the original message.
We can achieve this by using useActiveAudioPlayer hook. This hooks provides reactive value in form of AudioPlayer instance. The instance always corresponds to the currently active player.
import { useEffect, useState } from 'react';
import {
useActiveAudioPlayer,
} from 'stream-chat-react';
import { AudioPlayerUI } from './MyAudioPlayerUI';
const Player = () => {
const activePlayer = useActiveAudioPlayer();
const [open, setOpen] = useState<boolean>(!!activePlayer);
useEffect(() => {
setOpen(!!activePlayer);
if (!activePlayer) return;
const element = activePlayer?.elementRef;
if (!element) return;
const handleEnded = () => {
setOpen(false);
};
// optionally we can hide the player once the playback ended
element.addEventListener('ended', handleEnded);
return () => {
element.removeEventListener('ended', handleEnded);
};
}, [activePlayer]);
if (!activePlayer || !open) return null;
return (
<div>
<AudioPlayerUI audioPlayer={activePlayer} />
<button onClick={() => setOpen(false)}>Close</button>
</div>
);
};