const TopLevelComponent = ({ children }) => {
return (
<WithAudioPlayback allowConcurrentPlayback={false}>
{children}
</WithAudioPlayback>
);
};Audio Playback
Starting in stream-chat-react@13.12.0, you can build audio players with:
WithAudioPlaybackReact componentuseAudioPlayerhookuseActiveAudioPlayerhookuseAudioControllerhook (deprecated)
Best Practices
- Prefer
useAudioPlayerover deprecateduseAudioController. - Use a stable player ID to prevent re-creating audio elements.
- Keep playback UI lightweight for virtualized lists.
- Choose single vs. concurrent playback based on your UX.
- Subscribe to only the state you need via
useStateStore.
This guide focuses on the non-deprecated APIs.
Audio playback is controlled through WithAudioPlayback and useAudioPlayer. WithAudioPlayback creates and maintains a private pool of AudioPlayer instances (each with its own audio element). You access players via useAudioPlayer.
By default, Channel renders WithAudioPlayback, so all children can access the shared pool via useAudioPlayer.
Audio Playback Mode
You can allow only one audio at a time or multiple concurrent audios.
Single-playback mode: only one audio plays at a time (default in Channel).
Concurrent-playback mode: multiple audios can play simultaneously.
Set allowConcurrentPlayback on WithAudioPlayback to switch modes (default: false).
Building Audio Components
If you’re building audio UI outside of Channel, follow these steps:
- Render
WithAudioPlaybackat the top of your subtree.
- Render the player UI in a child component and request an
AudioPlayer.
import type {
AudioPlayer,
AudioPlayerDescriptor,
AudioPlayerState,
} from "stream-chat-react";
import {
FileSizeIndicator,
PlayButton,
PlaybackRateButton,
useAudioPlayer,
useStateStore,
WaveProgressBar,
} from "stream-chat-react";
/*
Provide a stable ID so you get a stable AudioPlayer instance
(for example: message ID + parent ID or any other stable key).
*/
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;
};
// Select the data you need 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) => {
// 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 playback control lives on the AudioPlayer instance returned by useAudioPlayer. Playback state changes are exposed through audioPlayer.state, which you can subscribe to with useStateStore.
AudioPlayer API
These are the key AudioPlayer methods for a custom UI:
audioPlayer.play(params)
Starts playback.
| Signature |
|---|
(params?: {currentPlaybackRate?: number; playbackRates?: number[];}) => Promise<void>; |
audioPlayer.pause()
Pauses playback and keeps the current progress.
| Signature |
|---|
| () => void; |
audioPlayer.stop()
Pauses playback and resets progress.
| Signature |
|---|
| () => void; |
audioPlayer.togglePlay()
Plays if paused, pauses if playing.
| Signature |
|---|
| () => void; |
audioPlayer.seek(params)
Seeks to the selected time in the track. Parameters:
clientX- X coordinate of the click on the progress barcurrentTarget- progress bar root element
| Signature |
|---|
(params: { clientX: number; currentTarget: HTMLDivElement }) => Promise<void> |
AudioPlayer State
Subscribe to AudioPlayer state like this:
// 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
In single-playback mode, a global player UI can be useful. For example, if playback starts in a message and the user scrolls away, a global player lets them keep control.
Use useActiveAudioPlayer to get a reactive AudioPlayer instance for 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>
);
};