# Pre-call test recording

The loopback recording API lets you record a short self-test of the local participant's media as it travels through Stream's SFU. The recording stays in the browser as an in-memory `Blob` — nothing is written to disk and nothing is uploaded.

## When to use it

- **Pre-call device check** - verify the user's microphone, camera, and network path end-to-end before they join a real call.
- **Diagnostics builds** - capture a short reference clip when users report audio or video quality issues, so you have a reproducible artifact to inspect.

## How it works

The hook records your published media after it has made a full round trip through Stream's servers - capturing what other participants would actually see and hear, not just your local camera and microphone. Any problem along that path shows up in the recording.

The result is a `Blob`, and **the caller owns it**. To play it back, wrap it with `URL.createObjectURL()`, and revoke that URL when you are done with it - the hook keeps no reference and revokes nothing on your behalf.

## Requirements

The component must be rendered inside `<StreamCall>`, and the call must be joined with `allowOwnTracksLoopback: true`. The flag is fixed at join time, so a loopback recording cannot be started on a call that was joined without it.

<Admonition type="warning">

No other participants can be present. `startRecording()` resolves with `null` when the call already has remote participants, and an in-progress recording stops as soon as someone joins.

</Admonition>

## API reference

```tsx
import { useLoopbackRecording } from "@stream-io/video-react-sdk";

const {
  startRecording,
  stopRecording,
  recordingState,
  loopbackVideoStream,
  loopbackAudioStream,
} = useLoopbackRecording();
```

| Field                      | Description                                                                                                                                                                                                                                                                                                                                                                                                                           |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `startRecording(options?)` | Starts a recording. Returns a promise that resolves with `{ blob }` once recording finishes, or `null` if no recording was produced (no call, remote participants present, the wait was aborted, or no data was captured). Pass `{ includeVideo: false }` for an audio-only recording, or `{ maxDurationMs }` to override the default 10-second cap (clamped to a 5-second minimum and a 2-minute maximum). Rejects on a fatal error. |
| `stopRecording()`          | Stops the recording early. During `'awaiting-streams'` it aborts the pending wait; during `'recording'` it finalises the blob and resolves once it is ready.                                                                                                                                                                                                                                                                          |
| `recordingState`           | One of `'idle'`, `'awaiting-streams'`, or `'recording'`.                                                                                                                                                                                                                                                                                                                                                                              |
| `loopbackVideoStream`      | The video `MediaStream` echoed back by the SFU, when present. Useful as a "the round trip is live" indicator.                                                                                                                                                                                                                                                                                                                         |
| `loopbackAudioStream`      | The audio `MediaStream` echoed back by the SFU, when present.                                                                                                                                                                                                                                                                                                                                                                         |

Audio is always recorded — there is no video-only mode.

## Minimal example

```tsx
import { useCallback, useEffect, useState } from "react";
import {
  StreamCall,
  useCall,
  useLoopbackRecording,
  useStreamVideoClient,
  type Call,
} from "@stream-io/video-react-sdk";

export const PreCallTest = () => {
  const client = useStreamVideoClient();
  const [call, setCall] = useState<Call>();

  useEffect(() => {
    if (!client) return;
    const _call = client.call("default", `self-test-${crypto.randomUUID()}`);
    setCall(_call);
    _call.getOrCreate().catch(console.error);
    return () => {
      setCall(undefined);
      _call.leave().catch(console.error);
    };
  }, [client]);

  if (!call) return null;

  return (
    <StreamCall call={call}>
      <RecordButton />
    </StreamCall>
  );
};

const RecordButton = () => {
  const call = useCall();
  const { startRecording, recordingState } = useLoopbackRecording();

  const run = useCallback(async () => {
    if (!call) return;
    try {
      await call.join({ create: true, allowOwnTracksLoopback: true });
      const recording = await startRecording();
      if (recording) {
        // You now have an in-memory blob. Play it back or offer it as a download.
        console.log("Recording ready", recording.blob.type);
      }
    } finally {
      await call.leave();
    }
  }, [call, startRecording]);

  return (
    <button onClick={run} disabled={recordingState !== "idle"}>
      {recordingState === "idle" ? "Record" : recordingState}
    </button>
  );
};
```

## Lifecycle and limits

- Recordings **auto-stop after 10 seconds** by default. Override via `startRecording({ maxDurationMs })` - values outside the `[5 seconds, 2 minutes]` range snap to the nearest bound.
- `startRecording()` waits up to **10 seconds** for the SFU to echo the loopback tracks back. If the wait times out the promise rejects - the usual cause is a call that was joined without `allowOwnTracksLoopback: true`.
- The recording stops and finalises when the call is left, when a remote participant joins, or when the component unmounts - the blob produced so far is still returned.
- The output format is whatever the browser's `MediaRecorder` produces (typically WebM in Chromium and Firefox, MP4 in Safari). Read `blob.type` if you need to name a downloaded file.

## See also

- [Pre-call self-test cookbook](https://getstream.io/video/docs/react/ui-cookbook/pre-call-self-test/) - a complete UI recipe (preview → record → play back).
- [Call Stats Report](https://getstream.io/video/docs/react/advanced/stats/) - surface latency, jitter, and bitrate during the loopback test for richer diagnostics.


---

This page was last updated at 2026-08-22T17:30:00.460Z.

For the most recent version of this documentation, visit [https://getstream.io/video/docs/react/advanced/loopback-recording/](https://getstream.io/video/docs/react/advanced/loopback-recording/).