Lobby Preview

Build an inviting lobby page where users can see call info and configure their devices before joining.

Best Practices

  • Use VideoPreview component - it handles video states (starting, playing, no camera) automatically.
  • Show call session info (useCallSession) so users know who's already joined.
  • Let users test devices before joining with device selectors.
  • Handle browser permission states - users get one chance to grant camera/mic access.
This guide focuses on component principles and data sources, not styling (CSS).

The call data

We would like to show some basic information about the call, when users arrive to the lobby. For example:

  • call name
  • who is invited
  • who already joined

The initial call information can be retrieved by the get or getOrCreate method of a Call instance. Then we can use the following hooks:

  • useCallSession
  • useCallMembers

These hooks make sure that the information about call session or call members is updated in real time. The updates are made automatically in response to Stream's WebSocket events arriving from the backend.

Learn more about setting up the boilerplate of joining a call room in our Joining and Creating Calls guide.

Video input preview

The SDK comes with a pre-built VideoPreview component that handles video input stream preview. It also presents various UIs based on video playing state (starting, playing, unavailable video devices). In the example below, we are assembling a custom video preview using SDK's VideoPreview component and our custom UI components for each playing state.

import {
  useConnectedUser,
  DefaultVideoPlaceholder,
  type StreamVideoParticipant,
  VideoPreview,
} from "@stream-io/video-react-sdk";

import { CameraIcon, LoadingIndicator } from "../icons";

export const Lobby = () => {
  return (
    <div>
      {/* ... other components */}
      <VideoPreview
        DisabledVideoPreview={DisabledVideoPreview}
        NoCameraPreview={NoCameraPreview}
        StartingCameraPreview={StartingCameraPreview}
      />
      {/* ... other components */}
    </div>
  );
};

export const DisabledVideoPreview = () => {
  const connectedUser = useConnectedUser();
  if (!connectedUser) return null;

  return (
    <DefaultVideoPlaceholder
      participant={
        {
          image: connectedUser.image,
          name: connectedUser.name,
        } as StreamVideoParticipant
      }
    />
  );
};

const NoCameraPreview = () => (
  <div>
    <CameraIcon />
  </div>
);

const StartingCameraPreview = () => (
  <div>
    <LoadingIndicator />
  </div>
);

Audio input preview

Microphone configuration in the lobby may consist of checking whether our microphone works and deciding whether it will be enabled when we join the call.

Learn how to build a toggle button for call preview pages in Call controls tutorial.

We build our custom sound detector in the dedicated tutorial about Audio Volume Indicator.

Device selection

Switching devices is done through a set of utility methods or hooks. We speak a bit more about the function and the pre-built components in the media devices guide.

When accessing camera and microphone, be aware that browser permissions are critical. Users only get one chance to grant permissions—if denied, the browser won't prompt again. Make sure to handle permission states appropriately. See the Camera & Microphone guide for details.

Device selector example

The selectors can be thought of as dropdowns in our example.

import { useCallStateHooks, useDeviceList } from "@stream-io/video-react-sdk";

type DeviceSelectorProps = {
  devices: MediaDeviceInfo[];
  selectedDeviceId?: string;
  onSelect: (deviceId: string) => void;
};

export const DeviceSelector = ({
  devices,
  selectedDeviceId,
  onSelect,
}: DeviceSelectorProps) => {
  const { deviceList } = useDeviceList(devices, selectedDeviceId);
  return (
    <div className="selector">
      {deviceList.map((device) => (
        <div
          key={device.deviceId}
          className={`option ${device.isSelected ? "option--selected" : ""}`}
          onClick={() => {
            if (device.deviceId !== "default") {
              onSelect(device.deviceId);
            }
          }}
        >
          {device.label}
        </div>
      ))}
    </div>
  );
};

export const AudioInputDeviceSelector = () => {
  const { useMicrophoneState } = useCallStateHooks();
  const { microphone, devices, selectedDevice } = useMicrophoneState();
  return (
    <DeviceSelector
      devices={devices || []}
      selectedDeviceId={selectedDevice}
      onSelect={(deviceId) => microphone.select(deviceId)}
    />
  );
};

export const VideoInputDeviceSelector = () => {
  const { useCameraState } = useCallStateHooks();
  const { camera, devices, selectedDevice } = useCameraState();
  return (
    <DeviceSelector
      devices={devices || []}
      selectedDeviceId={selectedDevice}
      onSelect={(deviceId) => camera.select(deviceId)}
    />
  );
};

export const AudioOutputDeviceSelector = () => {
  const { useSpeakerState } = useCallStateHooks();
  const { speaker, devices, selectedDevice, isDeviceSelectionSupported } =
    useSpeakerState();

  if (!isDeviceSelectionSupported) return null;
  return (
    <DeviceSelector
      devices={devices || []}
      selectedDeviceId={selectedDevice}
      onSelect={(deviceId) => speaker.select(deviceId)}
    />
  );
};

Participants in a call

We can retrieve the list of members, that already joined the call (participants), by inspecting the call session object (session.participants). The object is provided and maintained up-to-date by useCallSession hook.

import { Avatar, useCallStateHooks } from "@stream-io/video-react-sdk";

export const ParticipantsPreview = () => {
  const { useCallSession } = useCallStateHooks();
  const session = useCallSession();

  if (session?.participants || session?.participants.length === 0) return null;

  return (
    <div>
      <div>Already in call ({session.participants.length}):</div>
      <div style={{ display: "flex" }}>
        {session.participants.map((participant) => (
          <div>
            <Avatar
              name={participant.user.name}
              imageSrc={participant.user.image}
            />
            {participant.user.name && <div>{participant.user.name}</div>}
          </div>
        ))}
      </div>
    </div>
  );
};

Joining the call button

Lastly, to join a call we simply invoke call.join(). Learn more about the topic in the dedicated Joining & Creating Calls guide.