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";
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.
You can check out the finished project here.
- a little bit of experience in creating apps with React Native and/or Expo - refer to the React Native Guide or Expo Guide to learn more
Firstly create a brand new project.
npx react-native@latest init JellyfishDashboardnpx create-expo-app --template bare-minimum jellyfish-dashboardnpx create-expo-app jellyfish-dashboardnpx install-expo-modules@latest
npm install @jellyfish-dev/react-native-client-sdkexpo install @jellyfish-dev/react-native-client-sdkexpo install @jellyfish-dev/react-native-client-sdkIn 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"]
}
}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-componentsYou'll also need to install Reanimated library and React Navigation
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.
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;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;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>
);
}
// ...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);
}
};
// ...
}
// ...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);
}
};
// ...
}
// ...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.
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:
It should show up in the Room screen automatically:
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>
);
}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>
);
}
// ...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:



