Pre-call self-test

Introduction

Build a pre-call self-test screen where users record a short loopback of their own microphone, camera, and network path before joining a real call. Once recorded, they can watch the result back and optionally share the local video file.

This recipe uses the loopback recording API. The result is a two-screen flow: a recording screen and a playback / share screen.

Implementation

Consider using a dedicated call type (for example pre-call-test) and a recognisable call ID (for example prefixed with test_recording_) for the loopback call. That keeps self-tests cleanly separated from real call sessions in logs, analytics, and the Stream dashboard, and lets you scope permissions and defaults independently.

Step 1: Build the recording UI

Inside the recording screen, grab the active call with useCall() and drive the UI from useLoopbackRecording, using recordingState and the call's connecting state to reflect progress through join and record. When startRecording() resolves with a URI, you have a local video file you can play back or share via the OS share sheet; see the next step.

You can also render the live loopbackVideoStream via RTCView so the user sees the SFU echo immediately.

import React, { useCallback, useMemo } from "react";
import { Button, View } from "react-native";
import { RTCView } from "@stream-io/react-native-webrtc";
import {
  CallingState,
  useCall,
  useCallStateHooks,
  useLoopbackRecording,
} from "@stream-io/video-react-native-sdk";

const PreCallTest = () => {
  const call = useCall();
  const { useCallCallingState } = useCallStateHooks();
  const callingState = useCallCallingState();
  const { startRecording, stopRecording, recordingState, loopbackVideoStream } =
    useLoopbackRecording();

  const onStart = useCallback(async () => {
    if (!call) return;
    await call.join({ create: true, allowOwnTracksLoopback: true });
    const uri = await startRecording();
    call.leave();
    if (uri) {
      // `uri` is a `file://` URI of a local video on the device.
      // Use it to play the recording back or to share it via the OS share sheet.
    }
  }, [call, startRecording]);

  const isConnecting =
    (callingState === CallingState.JOINING ||
      callingState === CallingState.JOINED) &&
    recordingState !== "recording";

  const label = useMemo(() => {
    if (recordingState === "recording") return "Stop recording";
    if (isConnecting) return "Connecting…";
    return "Start recording";
  }, [recordingState, isConnecting]);

  const onPress =
    recordingState === "recording" ? () => stopRecording() : onStart;

  return (
    <View style={{ flex: 1 }}>
      {loopbackVideoStream ? (
        <RTCView
          streamURL={loopbackVideoStream.toURL()}
          objectFit="cover"
          style={{ flex: 1 }}
        />
      ) : null}
      <Button title={label} onPress={onPress} disabled={isConnecting} />
    </View>
  );
};

Step 2: Build the playback / share screen

The results screen takes the file URI as a prop and plays it back with react-native-video. Add a Share button to let users export the file via react-native-share.

import React, { useRef, useState } from "react";
import { Button, Pressable, StyleSheet, View } from "react-native";
import Video, { VideoRef } from "react-native-video";
import Share from "react-native-share";

export const TestRecordingResults = ({ uri }: { uri: string }) => {
  const videoRef = useRef<VideoRef>(null);
  const [paused, setPaused] = useState(true);

  const handleShare = () => {
    Share.open({
      url: uri,
      type: "video/mp4",
      failOnCancel: false,
    }).catch(() => {});
  };

  return (
    <View style={{ flex: 1 }}>
      <View style={{ flex: 1 }}>
        <Video
          ref={videoRef}
          source={{ uri }}
          paused={paused}
          style={StyleSheet.absoluteFillObject}
          resizeMode="cover"
        />
        <Pressable
          onPress={() => setPaused((p) => !p)}
          style={StyleSheet.absoluteFillObject}
        />
      </View>
      <Button title="Share" onPress={handleShare} />
    </View>
  );
};

Surfacing stats during the test

useCallStatsReport() works inside the loopback call too. Render a small overlay of latency, jitter, and bitrate while the user is recording so connectivity issues surface immediately. See the Call Stats Report guide.