Skip to content

Latest commit

 

History

History
662 lines (509 loc) · 17.7 KB

File metadata and controls

662 lines (509 loc) · 17.7 KB

import Tabs from "@theme/Tabs"; import TabItem from "@theme/TabItem"; import JellyfishArchitecture from "./_jellyfish-architecture.mdx"; import StartingJellyfishBackend from "./_starting-jellyfish-backend.mdx"; import StartingJellyfishDashboard from "./_starting-dashboard.mdx";

React Native / Expo Quickstart Guide

What you'll learn

This tutorial will guide you through creating your first React Native / Expo project which uses Jellyfish client. By the end of the tutorial, you'll have a working application that connects to the front-end dashboard using WebRTC technology and streams and receives camera tracks.

Finished app

You can check out the finished project here.

What do you need

Jellyfish architecture

Setup

Create React Native / Expo project

Firstly create a brand new project.

npx react-native@latest init JellyfishDashboard
npx create-expo-app --template bare-minimum jellyfish-dashboard
npx create-expo-app jellyfish-dashboard

Add dependencies

In order for this module to work you'll need to also add `expo` package. The expo package has a small footprint and it's necessary as Jellyfish Client package is built as Expo module.
npx install-expo-modules@latest
npm install @jellyfish-dev/react-native-client-sdk
expo install @jellyfish-dev/react-native-client-sdk
expo install @jellyfish-dev/react-native-client-sdk

Native configuration

In order for camera and audio to work you'll need to add some native configuration:

You need to at least set up camera permissions.

On Android add to your AndroidManifest.xml:

  <uses-permission android:name="android.permission.CAMERA"/>

For audio you'll need the RECORD_AUDIO permission:

  <uses-permission android:name="android.permission.RECORD_AUDIO"/>

On iOS you must set NSCameraUsageDescription in Info.plist file. You can edit this file in Xcode. This value is a description that is shown when iOS asks user for camera permission.

Similarly, for audio there is NSMicrophoneUsageDescription.

For screencast there is more configuration needed, it's described here.

We also suggest setting background mode to audio so that the app doesn't disconnect when it's in the background:

<key>UIBackgroundModes</key>
<array>
  <string>audio</string>
</array>

You have two options here. You can follow configuration instructions for React Native (Expo Bare workflow is a React Native project after all) or if you're using expo prebuild command to set up native code you can add our Expo plugin. Just add it to app.json:

{
  "expo": {
    "name": "example",
    //...
    "plugins": ["@jellyfish-dev/react-native-membrane-webrtc"]
  }
}

Jellyfish Client provides Expo plugin that should take care of native configuration for you. Just add it to app.json:

{
  "expo": {
    "name": "example",
    //...
    "plugins": ["@jellyfish-dev/react-native-membrane-webrtc"]
  }
}

Start the Jellyfish backend

Start the dashboard web front-end

(Optional) Add components library

For your convenience, we've prepared a library with nice-looking components useful for following this tutorial. Feel free to use standard React Native components or your own components though!

npx expo install @expo/vector-icons expo-barcode-scanner expo-font @expo-google-fonts/noto-sans @jellyfish-dev/react-native-jellyfish-components

You'll also need to install Reanimated library and React Navigation

Connecting to the server and joining the room

Our app will consist of two screens. The first one allows the user to type, paste or scan the peer token and connect to the room. The second screen shows room participants with their video tracks.

Screens

For managing screens I've used React Navigation library, you're free though to use another one. Our app will contain two screens: Connect screen and Room screen:

import React from "react";
import { NavigationContainer } from "@react-navigation/native";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import ConnectScreen from "./screens/Connect";
import RoomScreen from "./screens/Room";

const Stack = createNativeStackNavigator();

function App(): JSX.Element {
  return (
    <NavigationContainer>
      <Stack.Navigator>
        <Stack.Screen name="Connect" component={ConnectScreen} />
        <Stack.Screen name="Room" component={RoomScreen} />
      </Stack.Navigator>
    </NavigationContainer>
  );
}

export default App;

Connect screen

The UI of the Connect screen consists of a simple text input and a few buttons. The flow for this screen is simple: the user either copies the peer token from the dashboard or scans it with a QR code scanner and presses Connect button. The QR code scanner is provided by our components library and it's completely optional, just for convenience.

The code for the UI looks like this:

import React, { useState } from "react";
import { View, StyleSheet } from "react-native";
import {
  Button,
  TextInput,
  QRCodeScanner,
} from "@jellyfish-dev/react-native-jellyfish-components";

function ConnectScreen({ navigation }): JSX.Element {
  const [peerToken, setPeerToken] = useState<string>("");

  return (
    <View style={styles.container}>
      <TextInput
        placeholder="Enter peer token"
        value={peerToken}
        onChangeText={setPeerToken}
      />
      <Button
        onPress={() => {
          /* to be filled */
        }}
        title="Connect"
        disabled={!peerToken}
      />
      <QRCodeScanner onCodeScanned={setPeerToken} />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: "center",
    backgroundColor: "#BFE7F8",
    padding: 24,
    rowGap: 24,
  },
});

export default ConnectScreen;

Connecting to the server

Once the UI is ready, let's implement connecting to the server.

Firstly wrap your app with JelyfishContextProvider:

import React from "react";
// highlight-next-line
import { JellyfishContextProvider } from "@jellyfish-dev/react-native-client-sdk";
import { NavigationContainer } from "@react-navigation/native";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import ConnectScreen from "./screens/Connect";
import RoomScreen from "./screens/Room";

const Stack = createNativeStackNavigator();

function App(): JSX.Element {
  return (
    // highlight-next-line
    <JellyfishContextProvider>
      <NavigationContainer>
        <Stack.Navigator>
          <Stack.Screen name="Connect" component={ConnectScreen} />
          <Stack.Screen name="Room" component={RoomScreen} />
        </Stack.Navigator>
      </NavigationContainer>
      // highlight-next-line
    </JellyfishContextProvider>
  );
}

export default App;

Then in the ConnectScreen use the useJellyfishClient hook to connect to the server. Simply call the connect method with your Jellyfish server URL and the peer token. The connect function establishes a connection with the Jellyfish server via web socket and authenticates the peer.

// highlight-next-line
import { useJellyfishClient } from "@jellyfish-dev/react-native-client-sdk";

// This is the address of the Jellyfish backend. Change the local IP to yours. We
// strongly recommend setting this as an environment variable, we hardcoded it here
// for simplicity.
// highlight-next-line
const JELLYFISH_URL = "ws://192.168.81.152:4000/socket/peer/websocket";

function ConnectScreen({ navigation }): JSX.Element {
  const [peerToken, setPeerToken] = useState<string>("");

  // highlight-next-line
  const { connect } = useJellyfishClient();

  const connectToRoom = async () => {
    try {
      // highlight-next-line
      await connect(JELLYFISH_URL, peerToken.trim());
    } catch (e) {
      console.log("Error while connecting", e);
    }
  };

  return (
    <View style={styles.container}>
      <TextInput
        placeholder="Enter peer token"
        value={peerToken}
        onChangeText={setPeerToken}
      />
      <Button onPress={connectToRoom} title="Connect" disabled={!peerToken} />
      <QRCodeScanner onCodeScanned={setPeerToken} />
    </View>
  );
}

// ...

Camera permissions (Android only)

To start the camera we need to ask the user for permission first. We'll use a standard React Native module for this:

import { View, StyleSheet, Permission, PermissionsAndroid } from "react-native";

// ...

function ConnectScreen({ navigation }): JSX.Element {
  // ...

  // highlight-start
  const grantedCameraPermissions = async () => {
    const granted = await PermissionsAndroid.request(
      PermissionsAndroid.PERMISSIONS.CAMERA as Permission
    );
    if (granted !== PermissionsAndroid.RESULTS.GRANTED) {
      console.error("Camera permission denied");
      return false;
    }
    return true;
  };
  // highlight-end

  const connectToRoom = async () => {
    try {
      await connect(JELLYFISH_URL, peerToken.trim());

      // highlight-start
      if (!(await grantedCameraPermissions())) {
        return;
      }
      // highlight-end
    } catch (e) {
      console.log("Error while connecting", e);
    }
  };

  // ...
}

// ...

Starting the camera

Jellyfish Client provides a handy hook for managing the camera: useCamera. It can not only start a camera but also toggle, switch between multiple cameras, manage camera state and camera track simulcast settings and bandwidth. Also when starting the camera you can provide multiple different settings such as resolution, quality, and metadata. In this example though we'll simply turn it on to stream the camera to the dashboard with default settings:

import {
  useJellyfishClient,
  // highlight-next-line
  useCamera,
} from "@jellyfish-dev/react-native-client-sdk";

function ConnectScreen({ navigation }): JSX.Element {
  // ...

  // highlight-next-line
  const { startCamera } = useCamera();

  const connectToRoom = async () => {
    try {
      await connect(JELLYFISH_URL, peerToken.trim());

      if (!(await grantedCameraPermissions())) {
        return;
      }

      // highlight-next-line
      await startCamera();
    } catch (e) {
      console.log("Error while connecting", e);
    }
  };

  // ...
}

// ...

Joining the room

The last step of connecting to the room would be actually joining the room - so that your camera track is visible to other users. To do this simply use the join function from the useJellyfishClient hook.

You can also provide some user metadata when joining. You can put there whatever you want, it depends on your business logic. In our example, we provide a user name.

After joining the room we navigate to the next screen: Room screen.

// ...

function ConnectScreen({ navigation }): JSX.Element {
  // highlight-next-line
  const { connect, join } = useJellyfishClient();

  const connectToRoom = async () => {
    try {
      await connect(JELLYFISH_URL, peerToken.trim());

      if (!(await grantedCameraPermissions())) {
        return;
      }

      await startCamera();

      // highlight-next-line
      await join({ name: "Mobile RN Client" });
      // highlight-next-line
      navigation.navigate("Room");
    } catch (e) {
      console.log("Error while connecting", e);
    }
  };

  // ...
}

// ...

Now the app is ready for the first test. If everything went well you should see a video from your camera in the front-end dashboard. Now onto the second part: displaying the streams from other participants.

Displaying streams from other participants

Displaying video tracks

The Room screen displays the video from the camera on your device and also videos from other participants as well. It also allows the user to exit the call.

To get information about all participants (also the local one) in the room use usePeers() hook from Jellyfish Client. The hook returns all the participants with their ids, tracks and metadata. When a new participant joins or any participant leaves or anything else changes, the hook updates with the new information.

To display video tracks Jellyfish Client has a dedicated component for displaying a video track: <VideoRenderer>. It takes a track id as a prop (it may be a local or remote track) and you can style it just like an ordinary <View>. You can even animate it!

So, let's display all the participants in the simplest way possible:

import React from "react";
import { View, StyleSheet } from "react-native";
// highlight-start
import {
  usePeers,
  VideoRendererView,
} from "@jellyfish-dev/react-native-client-sdk";
// highlight-end

function RoomScreen({ navigation }): JSX.Element {
  // highlight-next-line
  const peers = usePeers();

  return (
    <View style={styles.container}>
      <View style={styles.videoContainer}>
        // highlight-start
        {peers.map((peer) =>
          peer.tracks[0] ? (
            <VideoRendererView
              trackId={peer.tracks[0].id}
              style={styles.video}
            />
          ) : null
        )}
        // highlight-end
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: "center",
    justifyContent: "space-between",
    backgroundColor: "#F1FAFE",
    padding: 24,
  },
  videoContainer: {
    flexDirection: "row",
    gap: 8,
    flexWrap: "wrap",
  },
  video: { width: 200, height: 200 },
});

export default RoomScreen;

You should now see your own camera on your mobile device. You can add another participant and their new track (displaying for example rotating frog) in the dashboard like this:

Adding new participant

It should show up in the Room screen automatically:

Room screen

For your convenience in our components library we provided a component to layout videos in a nice grid:

// highlight-next-line
import { VideosGrid } from "@jellyfish-dev/react-native-jellyfish-components";

function RoomScreen({ navigation }): JSX.Element {
  const peers = usePeers();

  return (
    <View style={styles.container}>
      // highlight-start
      <VideosGrid
        tracks={peers.map((peer) => peer.tracks[0]?.id).filter((t) => t)}
      />
      // highlight-end
    </View>
  );
}

Gracefully leaving the room

To leave a room we'll add a button for the user. When the user clicks it, we gracefully leave the room, close the server connection and go back to the Connect screen.

For leaving the room and closing the server connection you can use the cleanUp method from the useJellyfishClient() hook.

After leaving the room we go back to the Connect screen.

// ...
import {
  usePeers,
  VideoRendererView,
  // highlight-next-line
  useJellyfishClient,
} from "@jellyfish-dev/react-native-client-sdk";
// highlight-next-line
import { InCallButton } from "@jellyfish-dev/react-native-jellyfish-components";

function RoomScreen({ navigation }): JSX.Element {
  const peers = usePeers();
  // highlight-start
  const { cleanUp } = useJellyfishClient();

  const onDisconnectPress = () => {
    cleanUp();
    navigation.goBack();
  };
  // highlight-end

  return (
    <View style={styles.container}>
      <VideosGrid
        tracks={peers.map((peer) => peer.tracks[0]?.id).filter((t) => t)}
      />
      // highlight-start
      <InCallButton
        type="disconnect"
        iconName="phone-hangup"
        onPress={onDisconnectPress}
      />
      // highlight-end
    </View>
  );
}

// ...

Summary

Congrats on finishing your first Jellyfish mobile application! In this tutorial, you've learned how to make a basic Jellyfish client application that streams and receives video tracks with WebRTC technology.

But this was just the beginning. Jellyfish Client supports much more than just streaming camera: it can also stream audio, screencast your device's screen, configure your camera and audio devices, detect voice activity, control simulcast, bandwidth and encoding settings, show camera preview, display WebRTC stats and more to come. Check out our other tutorials to learn about those features.

You can also take a look at our fully featured Videoroom Demo example:

Videoroom Demo