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.

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.

API reference

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

const {
  startRecording,
  stopRecording,
  recordingState,
  loopbackVideoStream,
  loopbackAudioStream,
} = useLoopbackRecording();
FieldDescription
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.
recordingStateOne of 'idle', 'awaiting-streams', or 'recording'.
loopbackVideoStreamThe video MediaStream echoed back by the SFU, when present. Useful as a "the round trip is live" indicator.
loopbackAudioStreamThe audio MediaStream echoed back by the SFU, when present.

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

Minimal example

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