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>
);Lobby Preview
Build an inviting lobby page where users can see call info and configure their devices before joining.
Best Practices
- Use
VideoPreviewcomponent - 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.
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:
useCallSessionuseCallMembers
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.
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.