# 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`.

<admonition type="info">
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.
</admonition>

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.

```tsx
const TopLevelComponent = ({ children }) => {
  return (
    <WithAudioPlayback allowConcurrentPlayback={false}>
      {children}
    </WithAudioPlayback>
  );
};
```

2. Render the player UI in a child component and request an `AudioPlayer`.

```tsx
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:

```ts
// 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:

```ts
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.

```tsx
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>
  );
};
```


---

This page was last updated at 2026-04-21T09:53:42.542Z.

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/sdk/react/v13/guides/audio-playback/](https://getstream.io/chat/docs/sdk/react/v13/guides/audio-playback/).