import { useLoopbackRecording } from "@stream-io/video-react-sdk";
const {
startRecording,
stopRecording,
recordingState,
loopbackVideoStream,
loopbackAudioStream,
} = useLoopbackRecording();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
| 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
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 withoutallowOwnTracksLoopback: 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
MediaRecorderproduces (typically WebM in Chromium and Firefox, MP4 in Safari). Readblob.typeif you need to name a downloaded file.
See also
- Pre-call self-test cookbook - a complete UI recipe (preview → record → play back).
- Call Stats Report - surface latency, jitter, and bitrate during the loopback test for richer diagnostics.