# Lobby Preview

This guide covers creating an effective lobby experience before joining calls. A well-designed lobby enhances user experience and ensures smooth transitions into video calls.

## Best Practices

- **Show video preview** - Let users verify camera setup before joining
- **Display call information** - Show who's already in the call
- **Enable media controls** - Allow audio/video toggle before joining
- **Request permissions early** - Handle camera/microphone permissions in the lobby
- **Provide clear join action** - Make the join button prominent and accessible

Lobby capabilities:

- **Video preview** - Users see themselves before joining
- **Call information** - Display call details and participants
- **Media preferences** - Set audio/video mute state before joining
- **Participant list** - Show who has already joined

>
> **Note:** Component visualization varies by application. This guide focuses on building principles and data source integration.
>

## The Call data

Display call information when users arrive at the lobby:

- **Call ID and type** - Retrieved via `useCall` hook
- **Joined participants** - Retrieved via `useCallSession` hook

>
> **Note:** Retrieve initial call information using the `get` or `getOrCreate` method of a Call instance. Register an effect where the call is created:
>
> ```tsx
> const call =
>   /* Created call */
>
>   useEffect(() => {
>     const getOrCreateCall = async () => {
>       try {
>         await call?.getOrCreate();
>       } catch (error) {
>         console.error("Failed to get or create call", error);
>       }
>     };
>
>     getOrCreateCall();
>   }, [call]);
> ```
>

These hooks provide real-time updates via [Stream's WebSocket events](https://getstream.io/video/docs/react-native/v2/guides/events/).

## Video Input Preview

Display a local camera preview before joining. Use `LobbyCameraPreview` from `@stream-io/video-react-native-sdk` — it renders the platform camera preview and handles camera permission, direction, target resolution, and mirror mode for you. Drive its visibility from `optimisticIsMute` on `useCameraState()` — `isMute` and `status` are not set until the call is joined.

![Local Participant Preview Off](https://getstream.io/docs-assets/images/74c3d4357771.png)

![Local Participant Preview On](https://getstream.io/docs-assets/images/ab37f0aa2070.png)

Example:

```tsx
import {
  Avatar,
  LobbyCameraPreview,
  StreamVideoParticipant,
  useConnectedUser,
  useCallStateHooks,
} from "@stream-io/video-react-native-sdk";
import React from "react";
import { StyleSheet, View, Text } from "react-native";

export const LocalVideoRenderer = () => {
  const connectedUser = useConnectedUser();
  const { useCameraState } = useCallStateHooks();
  const { optimisticIsMute: cameraIsMuted } = useCameraState();

  const connectedUserAsParticipant = {
    userId: connectedUser?.id,
    image: connectedUser?.image,
    name: connectedUser?.name,
  } as StreamVideoParticipant;

  return (
    <View style={styles.videoView}>
      <View style={styles.topView} />
      {!cameraIsMuted ? (
        <LobbyCameraPreview objectFit="cover" />
      ) : (
        <Avatar participant={connectedUserAsParticipant} />
      )}
      <ParticipantStatus />
    </View>
  );
};

const ParticipantStatus = () => {
  const connectedUser = useConnectedUser();
  const participantLabel = connectedUser?.name ?? connectedUser?.id;
  const { useMicrophoneState } = useCallStateHooks();
  const { optimisticIsMute: microphoneIsMuted } = useMicrophoneState();

  return (
    <View style={styles.status}>
      <Text style={styles.userNameLabel} numberOfLines={1}>
        {participantLabel}
      </Text>
      {microphoneIsMuted && (
        <View style={styles.svgContainerStyle}>
          <Text>(Mic off)</Text>
        </View>
      )}
    </View>
  );
};

const styles = StyleSheet.create({
  videoView: {
    backgroundColor: "gray",
    height: 280,
    width: "100%",
    justifyContent: "space-between",
    alignItems: "center",
    overflow: "hidden",
    marginVertical: 8,
  },
  topView: {},
  status: {
    alignSelf: "flex-start",
    flexDirection: "row",
    alignItems: "center",
    padding: 8,
    borderRadius: 4,
    backgroundColor: "#dddddd",
  },
  userNameLabel: {
    flexShrink: 1,
    color: "white",
  },
  svgContainerStyle: {
    marginLeft: 8,
  },
});
```

## Media Stream Management

To control audio or video mute status in the Lobby, use the `useCameraState` and `useMicrophoneState` hooks from `useCallStateHooks`. Buttons call `call.camera.toggle()` and `call.microphone.toggle()` as usual, and read `optimisticIsMute` from those hooks to render the button state — `isMute` is not populated until the call is joined.

>
> **Note:** When accessing camera and microphone, native permissions are required. Make sure to request permissions before attempting to access devices, and handle cases where permissions are denied. See the [Native Permissions guide](https://getstream.io/video/docs/react-native/v2/guides/native-permissions/) for details.
>

![Toggle Audio/Video Button On](https://getstream.io/docs-assets/images/0b6c6c10260c.png)

![Toggle Audio/Video Button Off](https://getstream.io/docs-assets/images/e00e41ea8e0d.png)

Example:

```tsx title="MediaStreamButtonGroup.tsx"
import React from "react";
import { Pressable, View, Text, StyleSheet } from "react-native";
import { useCall, useCallStateHooks } from "@stream-io/video-react-native-sdk";

const MUTED = "#080707dd";
const ACTIVE = "white";

export const MediaStreamButtonGroup = () => {
  const call = useCall();
  const { useMicrophoneState, useCameraState } = useCallStateHooks();
  const { optimisticIsMute: microphoneMuted } = useMicrophoneState();
  const { optimisticIsMute: cameraMuted } = useCameraState();

  return (
    <View style={styles.buttonGroup}>
      <Pressable
        onPress={() => call?.microphone.toggle()}
        style={[
          styles.button,
          { backgroundColor: microphoneMuted ? MUTED : ACTIVE },
        ]}
      >
        <Text
          style={[
            styles.mediaButtonText,
            { color: microphoneMuted ? ACTIVE : MUTED },
          ]}
        >
          {microphoneMuted ? "Audio off" : "Audio on"}
        </Text>
      </Pressable>
      <Pressable
        onPress={() => call?.camera.toggle()}
        style={[
          styles.button,
          { backgroundColor: cameraMuted ? MUTED : ACTIVE },
        ]}
      >
        <Text
          style={[
            styles.mediaButtonText,
            { color: cameraMuted ? ACTIVE : MUTED },
          ]}
        >
          {cameraMuted ? "Video off" : "Video on"}
        </Text>
      </Pressable>
    </View>
  );
};

const styles = StyleSheet.create({
  buttonGroup: {
    flexDirection: "row",
    justifyContent: "space-evenly",
  },
  button: {
    height: 80,
    width: 80,
    borderRadius: 40,
    justifyContent: "center",
  },
  mediaButtonText: {
    textAlign: "center",
  },
});
```

## Participants in a call

Retrieve joined participants via `session.participants`, maintained by the `useCallSession` hook.

![Preview of the already joined participants example](https://getstream.io/docs-assets/images/3345dac297f3.png)

```tsx title="LobbyParticipantsPreview.tsx"
import { Image, StyleSheet, Text, View } from "react-native";
import { useCallStateHooks } from "@stream-io/video-react-native-sdk";

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

  if (!(session?.participants && session?.participants.length)) {
    return null;
  }

  return (
    <View>
      <Text style={styles.infoText}>
        Already in call ({session.participants.length}):
      </Text>
      <View style={styles.userInfo}>
        {session.participants.map((participant) => (
          <View key={participant.user.id}>
            <Image
              source={{ uri: participant.user.image }}
              style={styles.avatar}
            />
            {participant.user.name && (
              <Text style={styles.title}>{participant.user.name}</Text>
            )}
          </View>
        ))}
      </View>
    </View>
  );
};

const styles = StyleSheet.create({
  infoText: {
    color: "white",
    textAlign: "center",
  },
  userInfo: {
    flexDirection: "row",
    justifyContent: "space-evenly",
    marginTop: 10,
  },
  avatar: {
    height: 100,
    width: 100,
    borderRadius: 50,
  },
  title: {
    fontSize: 16,
    color: "white",
    marginVertical: 10,
    textAlign: "center",
  },
});
```

## Joining the call button

Join calls using `call.join()`. See the [Joining & Creating Calls guide](https://getstream.io/video/docs/react-native/v2/guides/joining-and-creating-calls/).

```tsx title="JoinCallButton.tsx"
import React, { useCallback } from "react";
import { Pressable, StyleSheet, Text } from "react-native";
import { useCall } from "@stream-io/video-react-native-sdk";

export const JoinCallButton = () => {
  const call = useCall();

  const onCallJoinHandler = useCallback(async () => {
    try {
      await call?.join({ create: true });
    } catch (error) {
      if (error instanceof Error) {
        console.log("Error joining call:", error);
      }
    }
  }, [call]);

  return (
    <Pressable onPress={onCallJoinHandler} style={styles.joinButton}>
      <Text style={styles.joinButtonText}>Join Call</Text>
    </Pressable>
  );
};

const styles = StyleSheet.create({
  joinButton: {
    backgroundColor: "blue",
    paddingVertical: 10,
  },
  joinButtonText: {
    textAlign: "center",
    fontSize: 25,
    color: "white",
  },
});
```

## Assembling it all together

![Lobby Completed Camera and Mic off](https://getstream.io/docs-assets/images/e559fced844e.png)

![Lobby Completed Camera and Mic on](https://getstream.io/docs-assets/images/bf900e39c433.png)

```tsx
import { StyleSheet, View } from "react-native";

export const Lobby = () => {
  return (
    <View style={styles.container}>
      <LocalVideoRenderer />
      <MediaStreamButtonGroup />
      <LobbyParticipantsPreview />
      <JoinCallButton />
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#272A30",
    justifyContent: "space-evenly",
  },
});
```

---

For the most recent version of this documentation, visit [https://getstream.io/video/docs/react-native/v2/ui-cookbook/lobby-preview/](https://getstream.io/video/docs/react-native/v2/ui-cookbook/lobby-preview/).