This is beta documentation for Stream Chat React SDK v14. For the latest stable version, see the latest version (v13) .

Audio Playback

With stream-chat-react, you can build audio players with:

  • WithAudioPlayback React component
  • useAudioPlayer hook
  • useActiveAudioPlayer hook

Best Practices

  • Use useAudioPlayer for custom playback UI.
  • 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.

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.
import { WithAudioPlayback } from "stream-chat-react";

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 "stream-chat-react";

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
            className="audio-player-file-icon"
            fileName={audioPlayer.title}
            mimeType={audioPlayer.mimeType}
          />
        )}
      </div>
    </div>
  );
};

Style the icon size through CSS:

.audio-player-file-icon {
  inline-size: 40px;
  block-size: 40px;
}

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:

MethodDescriptionSignature
audioPlayer.pause()Pauses playback and keeps the current progress.() => void
audioPlayer.play(params)Starts playback. Optionally accepts currentPlaybackRate and playbackRates.(params?: { currentPlaybackRate?: number; playbackRates?: number[] }) => Promise<void>
audioPlayer.seek(params)Seeks to the selected time in the track. clientX is the X coordinate of the click on the progress bar and currentTarget is the progress bar root element.(params: { clientX: number; currentTarget: HTMLDivElement }) => Promise<void>
audioPlayer.stop()Pauses playback and resets progress.() => void
audioPlayer.togglePlay()Plays if paused and pauses if playing.() => 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>
  );
};