# Voice recordings and audio playback

> **Info:** This is release candidate documentation for **Stream Chat React Native SDK v10**. For the latest stable version, see the [latest version (v9)](https://getstream.io/chat/docs/sdk/react-native/).

A voice recording is a message the user records in the composer. This page covers recording, how a recording is turned into an attachment and sent, and how the SDK plays voice recordings and audio files.

## What users get

The user's composer is empty, so a microphone button sits where the send button would be. They hold it, and the first time the device asks for the microphone. While they hold it, the composer shows the recording time and "Slide to Cancel". Sliding left cancels the recording. Sliding up locks it, so they can let go: the composer then shows a live waveform, with buttons to delete the recording, stop it and send it. After they stop it, they can listen to it before they send or delete it.

Releasing the button without locking it finishes the recording. By default the recording then waits above the input with the text and files they add, and goes out when they send. A tap on the microphone instead of a hold shows "Hold to record. Release to save."

In the message list, the recording plays in a player with a play button, the waveform, the time left and a speed button that cycles x1.0, x1.5 and x2.0. Dragging along the waveform seeks. An audio file plays in the same player, with a progress bar in place of the waveform. In the channel list and in a reply that quotes it, a recording shows as "Voice message (0:12)".

Starting a recording or an audio file pauses the one that was playing in the channel, so only one plays at a time. Starting to record pauses it too. Closing the channel screen stops its audio.

## How it works across the layers

- **State.** A voice recording is an attachment of type `voiceRecording`. The composer uploads and sends it like any other attachment, so the upload settings and a custom upload function apply to it. See [Attachments and uploads](https://getstream.io/chat/docs/sdk/react-native/v10/features/rich-content/attachments-and-uploads/).
- **UI.** `MessageComposer` records through the audio package your app installs, and turns the recording into an attachment. `AudioAttachment` plays it in the message list. Playback runs through a pool of audio players that `Channel` and `Thread` each provide.
- **Layout.** Not involved, except that a channel and a thread each scope their own playback.

### The attachment

A voice recording carries these fields. The metadata is under `custom`, like every attachment's own data:

| Field                  | Holds                                                                                          |
| ---------------------- | ---------------------------------------------------------------------------------------------- |
| `type`                 | always `"voiceRecording"`                                                                      |
| `asset_url`            | the uploaded file                                                                              |
| `title`                | the file name, such as `audio_recording_2026-10-25T10_12_03_120Z.aac`                          |
| `custom.duration`      | the length in seconds                                                                          |
| `custom.file_size`     | the size in bytes. The SDK's own recordings carry `0`.                                         |
| `custom.mime_type`     | the file type, `audio/aac` for the SDK's recordings                                            |
| `custom.waveform_data` | up to 100 amplitudes between 0 and 1, drawn as the waveform. Without it, a progress bar shows. |

### Recording

1. With `audioRecordingEnabled` on `Channel` or `Thread`, the composer shows the record button, `StartAudioRecordingButton`, in place of the send button while it has no text and no attachments. A message being edited, an active command and a slow-mode cooldown each show their own button instead.
2. A press of at least `asyncMessagesMinimumPressDuration` (500 ms) starts recording. The audio package asks for the microphone the first time. When access was denied, a notification asks the user to allow it in the settings, with a button that opens them.
3. While the user holds the button, `AudioRecorder` takes the input's place, and `AudioRecordingLockIndicator` shows above it. Releasing the button after sliding it to the left by more than half of `asyncMessagesSlideToCancelDistance` (75 points) cancels the recording. Releasing it after sliding it up by more than half of `asyncMessagesLockDistance` (50 points) locks it. A locked recording shows `AudioRecordingInProgress` with its waveform, and after it's stopped, `AudioRecordingPreview`, which plays it.
4. The recording is AAC. While it runs, the audio level is sampled for the waveform, and when it completes, the samples are reduced to at most 100.
5. Releasing the button, or the send button of a locked recording, completes it. A recording shorter than 300 ms is thrown away. The SDK turns the recording into a `voiceRecording` attachment and uploads it with the composer's attachment manager. The recording waits in the composer, shown by `AudioAttachmentUploadPreview`, and goes out with the text and files when the user sends.
6. With `audioRecordingSendOnComplete`, the recording is sent as a message of its own as soon as it completes, without waiting in the composer.

`useMessageInputContext()` exposes the recorder to your own components: `audioRecorderManager`, whose `state` store holds `status`, `duration`, `waveformData` and `micLocked`, and the `startVoiceRecording()`, `stopVoiceRecording()`, `deleteVoiceRecording()` and `uploadVoiceRecording(sendOnComplete)` functions.

### Playback

`Channel` and `Thread` each provide an audio player pool through `AudioPlayerProvider`. A component asks the pool for a player with `useAudioPlayer({ uri, requester, type, duration, ... })`. The player's id is built from `requester`, `uri` and `id`, and the same id gives back the same player, so a message scrolled out of the list and back keeps its progress.

The player's state is a store, `audioPlayer.state`, and its methods control playback: `AudioPlayer.play()`, `AudioPlayer.pause()`, `AudioPlayer.toggle()`, `AudioPlayer.stop()`, `AudioPlayer.seek(positionInSeconds)` and `AudioPlayer.changePlaybackRate()`, which moves to the next of its `playbackRates`. The state holds `isPlaying`, `duration` and `position` in milliseconds, `progress` from 0 to 1, `currentPlaybackRate` and `playbackRates`. The `duration` you pass to `useAudioPlayer` is in seconds. When a file can't be played or seeked in, the player publishes an error notification.

By default the pool plays one sound at a time. A player that starts pauses the one that was playing. `useActiveAudioPlayer()` returns the player that is playing, and `null` once it pauses. When the channel or thread screen unmounts, the pool stops and discards its players. A thread has a pool of its own, so a recording playing in the thread doesn't pause one playing in the channel.

## Turning it on or off

Recording is off by default. Install an audio package, as [Installation](https://getstream.io/chat/docs/sdk/react-native/v10/getting-started/installation/#voice-recording-and-audio-playback) shows, with the microphone permission, and turn it on for each `Channel` and `Thread` you render:

```tsx
import type { Channel as StreamChannel } from "stream-chat";
import {
  Channel,
  MessageComposer,
  MessageList,
} from "stream-chat-react-native";

export const ChannelScreen = ({ channel }: { channel: StreamChannel }) => (
  <Channel audioRecordingEnabled channel={channel}>
    <MessageList />
    <MessageComposer />
  </Channel>
);
```

A thread's `Thread` needs the prop too. Recordings are uploaded, so uploads must be allowed in the channel. See [Turning it on or off](https://getstream.io/chat/docs/sdk/react-native/v10/features/rich-content/attachments-and-uploads/#turning-it-on-or-off) on Attachments and uploads.

>
> **Warning:** The record button checks neither of these. Without an audio package, it still shows, and holding it shows the notification about the microphone permission. When uploads aren't allowed, it still records, and the finished recording isn't added to the composer. Turn `audioRecordingEnabled` on only where both hold, for example where `channel.messageComposer.attachmentManager.isUploadEnabled` is `true`.
>

`audioRecordingSendOnComplete` sends the recording before its upload has finished, so it relies on [sending with pending uploads](https://getstream.io/chat/docs/sdk/react-native/v10/features/messaging/sending-with-pending-uploads/). Turn `messageComposer.attachments.pendingUploadsEnabled` on with it, or the recording doesn't reach the message.

Playback needs only the audio package, because `Channel` and `Thread` provide it. Without an audio package, recordings and audio files render as file rows, which open the file's URL.

### Playing several at once

`allowConcurrentAudioPlayback` on `Channel` and `Thread` lets several recordings play together. `useActiveAudioPlayer()` then always returns `null`.

## Customizing

### The recorder

Six override keys replace the recording UI: `StartAudioRecordingButton`, `AudioRecorder`, `AudioRecordingInProgress`, `AudioRecordingLockIndicator`, `AudioRecordingPreview` and `AudioRecordingWaveform`. `AudioAttachmentUploadPreview` replaces the recording's preview in the composer. A recorder of your own reads `audioRecorderManager` and calls the context's functions:

```tsx
import { Pressable, StyleSheet, Text, View } from "react-native";
import {
  useMessageInputContext,
  useStateStore,
} from "stream-chat-react-native";
import type { ComponentOverrides } from "stream-chat-react-native";

const recorderSelector = ({
  duration,
  status,
}: {
  duration: number;
  status: string;
}) => ({
  duration,
  status,
});

const SimpleAudioRecorder = () => {
  const {
    audioRecorderManager,
    audioRecordingSendOnComplete,
    deleteVoiceRecording,
    stopVoiceRecording,
    uploadVoiceRecording,
  } = useMessageInputContext();
  const { duration, status } = useStateStore(
    audioRecorderManager.state,
    recorderSelector,
  );

  return (
    <View style={styles.row}>
      <Text>{Math.floor(duration / 1000)}s</Text>
      <Pressable onPress={deleteVoiceRecording}>
        <Text>Delete</Text>
      </Pressable>
      {status === "recording" ? (
        <Pressable onPress={stopVoiceRecording}>
          <Text>Stop</Text>
        </Pressable>
      ) : null}
      <Pressable
        onPress={() => uploadVoiceRecording(audioRecordingSendOnComplete)}
      >
        <Text>Send</Text>
      </Pressable>
    </View>
  );
};

// Pass to `WithComponents`, declared once at module scope.
export const overrides: ComponentOverrides = {
  AudioRecorder: SimpleAudioRecorder,
};

const styles = StyleSheet.create({
  row: { alignItems: "center", flexDirection: "row", gap: 16, padding: 8 },
});
```

`AudioRecorder` renders while a recording runs. Before the recording is locked, the user's finger is still on the record button, so most apps keep the default and change only the locked state's parts. [Message composer UI](https://getstream.io/chat/docs/sdk/react-native/v10/ui/components/message-composer-ui/#voice-recording) lists the composer's other override keys.

The recording settings come from the audio package. It records AAC, and samples the audio level for the waveform. To change the settings, pass `overrideAudioRecordingConfiguration` to `registerNativeHandlers()`. See [Entry points and native handlers](https://getstream.io/chat/docs/sdk/react-native/v10/reference/entry-points-and-native-handlers/).

### A player of your own

`AudioAttachment` is the override key for the player in the message list, for voice recordings and audio files alike. It receives the attachment as `item`, and the `message`. This one renders a single button with the seconds played, and adds a 3x speed:

```tsx
import { Pressable, Text } from "react-native";
import { useAudioPlayer, useStateStore } from "stream-chat-react-native";
import type {
  AudioAttachmentProps,
  AudioPlayerState,
  ComponentOverrides,
} from "stream-chat-react-native";

const playbackRates = [1, 1.5, 2, 3];

const playerSelector = ({
  currentPlaybackRate,
  isPlaying,
  position,
}: AudioPlayerState) => ({
  currentPlaybackRate,
  isPlaying,
  position,
});

const CompactAudioAttachment = ({ item, message }: AudioAttachmentProps) => {
  const audioPlayer = useAudioPlayer({
    duration: item.custom?.duration ?? 0,
    mimeType: item.custom?.mime_type ?? "",
    playbackRates,
    requester: message?.id,
    type: item.type,
    uri: item.asset_url ?? "",
  });
  const { currentPlaybackRate, isPlaying, position } = useStateStore(
    audioPlayer.state,
    playerSelector,
  );

  return (
    <Pressable
      onLongPress={() => audioPlayer.changePlaybackRate()}
      onPress={() => audioPlayer.toggle()}
    >
      <Text>
        {isPlaying ? "Pause" : "Play"} {Math.round(position / 1000)}s x
        {currentPlaybackRate}
      </Text>
    </Pressable>
  );
};

// Pass to `WithComponents`, declared once at module scope.
export const overrides: ComponentOverrides = {
  AudioAttachment: CompactAudioAttachment,
};
```

A player keeps the `playbackRates` it was created with, so give the same rates wherever the same player is used. `PlayPauseButton`, `WaveProgressBar`, `ProgressControl` and `SpeedSettingsButton` are the SDK's own parts, if you want to build from them.

`useAudioPlayer` works under `Channel`, `Thread` or `AudioPlayerProvider`. Outside a channel screen, wrap your audio UI in `<AudioPlayerProvider value={{ allowConcurrentAudioPlayback: false }}>`. Changing `allowConcurrentAudioPlayback` creates a new pool and stops what was playing.

### A player that stays visible

`useActiveAudioPlayer()` returns the player that is playing, so a bar at the top of the channel can show it after the user scrolls away from the message. Call it under `Channel`, `Thread` or `AudioPlayerProvider`.

```tsx
import { Pressable, StyleSheet, Text, View } from "react-native";
import { useActiveAudioPlayer, useStateStore } from "stream-chat-react-native";
import type { AudioPlayer, AudioPlayerState } from "stream-chat-react-native";

const positionSelector = ({ duration, position }: AudioPlayerState) => ({
  duration,
  position,
});

const NowPlayingControls = ({ audioPlayer }: { audioPlayer: AudioPlayer }) => {
  const { duration, position } = useStateStore(
    audioPlayer.state,
    positionSelector,
  );

  return (
    <View style={styles.bar}>
      <Text>
        {Math.round(position / 1000)}s / {Math.round(duration / 1000)}s
      </Text>
      <Pressable onPress={() => audioPlayer.pause()}>
        <Text>Pause</Text>
      </Pressable>
    </View>
  );
};

export const NowPlaying = () => {
  const audioPlayer = useActiveAudioPlayer();
  return audioPlayer ? <NowPlayingControls audioPlayer={audioPlayer} /> : null;
};

const styles = StyleSheet.create({
  bar: { flexDirection: "row", gap: 12, justifyContent: "center", padding: 8 },
});
```

Render `NowPlaying` inside `Channel`, for example above the `MessageList`. It disappears when the player pauses, because the hook returns `null` then.

### Text and translations

The quoted reply's and the channel preview's `Voice message ({{duration}})` is `messagePreview.voiceMessage.label`, and the notifications above have keys under `messageInput.audioRecorder` and `audioPlayer`. See [Localization](https://getstream.io/chat/docs/sdk/react-native/v10/ui/customizing/localization/).

## Reference

- [Audio recorder](https://getstream.io/chat/docs/sdk/react-native/v10/component-reference/message-composer/audio-recorder/): the recorder's components and props.
- [Attachments](https://getstream.io/chat/docs/sdk/react-native/v10/component-reference/message/attachments/): `AudioAttachment` and the other renderers.
- [Channel](https://getstream.io/chat/docs/sdk/react-native/v10/component-reference/channel/channel/): `audioRecordingEnabled`, `audioRecordingSendOnComplete`, `allowConcurrentAudioPlayback` and the `asyncMessages*` props.
- [Composer hooks](https://getstream.io/chat/docs/sdk/react-native/v10/reference/hooks/composer-hooks/): `useMessageInputContext` and `useAttachmentManagerState`.
- [Installation](https://getstream.io/chat/docs/sdk/react-native/v10/getting-started/installation/#voice-recording-and-audio-playback): the audio packages and the microphone permission.

---

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/sdk/react-native/v10/features/rich-content/voice-recordings-and-audio-playback/](https://getstream.io/chat/docs/sdk/react-native/v10/features/rich-content/voice-recordings-and-audio-playback/).