Audio Playback

Starting in stream-chat-react@13.12.0, you can build audio players with:

  • WithAudioPlayback React component
  • useAudioPlayer hook
  • useActiveAudioPlayer hook
  • useAudioController hook (deprecated)

Best Practices

  • Prefer useAudioPlayer over deprecated useAudioController.
  • 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.
Before 13.12.0, audio playback relied on `useAudioController` and audio elements lived inside the UI component. In virtualized lists, scrolling could unmount the audio element and stop playback. `useAudioPlayer` avoids this by using a shared pool of audio elements.

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:

  1. Render WithAudioPlayback at the top of your subtree.
const TopLevelComponent = ({ children }) => {
  return (
    <WithAudioPlayback allowConcurrentPlayback={false}>
      {children}
    </WithAudioPlayback>
  );
};
  1. 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 bar
  • currentTarget - 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>
  );
};