This tutorial teaches you how to build Zoom/Whatsapp style video calling for your app.
- Calls run on Stream's global edge network for optimal latency & reliability.
- Permissions give you fine-grained control over who can do what.
- Video quality and codecs are automatically optimized.
- Powered by Stream's Video Calling API.
Step 1 - Create a new React Native app
To create a new React Native app, you'll first need to set up your environment. Once you're set up, continue with the steps below to create an application and start developing.
You can use React Native Community CLI to generate a new project. Let's create a new React Native project called "VideoCallExample":
12npx @react-native-community/cli@latest init VideoCallExample cd VideoCallExample
If you are having trouble with iOS, try to reinstall the dependencies by running:
cd ios
to navigate to theios
folderbundle install
to install Bundlerbundle exec pod install
to install the iOS dependencies managed by CocoaPods
Step 2 - Install the SDK and declare permissions
In order to install the Stream Video React Native SDK, run the following command in your terminal of choice:
1yarn add @stream-io/video-react-native-sdk @stream-io/react-native-webrtc
The SDK requires installing some peer dependencies. You can run the following command to install them:
1234567yarn add react-native-incall-manager yarn add react-native-svg yarn add @react-native-community/netinfo yarn add @notifee/react-native # Install pods for iOS npx pod-install
Declare permissions
Application needs permissions to access camera, microphone and network state.
In AndroidManifest.xml
add the following permissions before the application
section.
123456789101112131415161718<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <uses-feature android:name="android.hardware.camera" /> <uses-feature android:name="android.hardware.camera.autofocus" /> <uses-feature android:name="android.hardware.audio.output" /> <uses-feature android:name="android.hardware.microphone" /> <uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.RECORD_AUDIO" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" /> <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" /> <uses-permission android:name="android.permission.INTERNET" /> ... <application ... /> </application> </manifest>
Add the following keys and values to Info.plist
file, under dict
tag.
123456789101112<plist version="1.0"> <dict> ... <key>CFBundleName</key> <string>$(PRODUCT_NAME)</string> <key>NSCameraUsageDescription</key> <string>$(PRODUCT_NAME) would like to use your camera</string> <key>NSMicrophoneUsageDescription</key> <string>$(PRODUCT_NAME) would like to use your microphone</string> ... </dict> </plist>
Android Specific installation
In android/build.gradle
add the following inside the buildscript
section:
1234567buildscript { ext { ... minSdkVersion = 24 } ... }
Run the app
To ensure the best possible experience, we highly recommend running the app on a physical device. This is due to the limitations in audio and video device support on emulators. You can refer to the React Native documentation for guidance on running the app on a physical device.
Step 3 - Understand the basics
Before we dive deep into writing code, there are two concepts you should be familiar with - StreamVideoClient
and Call
.
StreamVideoClient
is the low level JavaScript client used by the SDK to communicate with the Stream Video service.
It provides all the necessary methods to connect user Stream service, query calls, create call etc.
And a call
refers to an instance of the Call
class and is utilized for performing call-specific actions, such as joining a call, muting participants, leaving a call, and more.
1234567891011121314151617181920// create client instance const client = new StreamVideoClient({ apiKey, user, token }); // Alternatively you can also choose to separate client creation and user connection: // const client = new StreamVideoClient({ apiKey }); // await client.connectUser(user, token); // create a call instance const call = client.call('default', callId); call.join({ create: true, // create the call if it doesn't exist data: { members: [{ user_id: 'john_smith' }, { user_id: 'jane_doe' }], custom: { // custom data set on call title: 'React Native test', description: 'Conducting a test of React Native video calls', }, }, });
In this example
apiKey
is the API key of your Stream Video application available on dashboarduser
is the user object{ id: "john_smith", "name": "John Smith" }
- and
token
is the user token generated by your server-side API. For development purpose, you can use the token generated by the Token Generator. You can read more information about client authentication on the Client & Authentication guide.
Client instance and call instance are made accessible to all the video components from SDK via StreamVideo
component and StreamCall
component respectively.
In your app you need to wrap your component tree with StreamVideo
component and provide the client
instance to it as a prop.
client creation would normally take place during sign-in stage of the application.
And similarly you need to wrap your call specific UI components with StreamCall
component and provide the call
instance to it as a prop.
StreamVideo
and StreamCall
components are basically context providers, and they enable you to consume hooks provided by the SDK.
These hooks do all the heavy lifting around call management and provide you with the necessary state and methods to build your UI.
123456789<StreamVideo client={client}> ... <StreamCall call={call}> <View> <Text>Video Call UI</Text> </View> </StreamCall> ... </StreamVideo>
Step 4 - Setup Starter UI
Lets begin by creating a basic UI for our audio room. Normally you would use a navigation library like React Navigation to navigate between screens.
But for this tutorial we'll keep it simple and mock the navigation using a state variable - activeScreen
.
Within your tutorial app, create a folder named src
and create the following files within it:
src/HomeScreen.tsx
src/CallScreen.tsx
(takescallId
as a prop)
Now copy the following content into the respective files (as mentioned in header):
12345678910111213141516171819202122232425262728293031import React, { useState } from 'react'; import { SafeAreaView, StyleSheet } from 'react-native'; import { HomeScreen } from './src/HomeScreen'; import { CallScreen } from './src/CallScreen'; const apiKey = 'REPLACE_WITH_API_KEY'; const token = 'REPLACE_WITH_TOKEN'; const userId = 'REPLACE_WITH_USER_ID'; const callId = 'REPLACE_WITH_CALL_ID'; export default function App() { const [activeScreen, setActiveScreen] = useState('home'); const goToCallScreen = () => setActiveScreen('call-screen'); const goToHomeScreen = () => setActiveScreen('home'); return ( <SafeAreaView style={styles.container}> {activeScreen === 'call-screen' ? ( <CallScreen goToHomeScreen={goToHomeScreen} callId={callId} /> ) : ( <HomeScreen goToCallScreen={goToCallScreen} /> )} </SafeAreaView> ); } const styles = StyleSheet.create({ container: { flex: 1, }, });
12345678910111213141516171819202122232425262728import React from 'react'; import { Button, StyleSheet, Text, View } from 'react-native'; type Props = { goToHomeScreen: () => void; callId: string }; export const CallScreen = ({ goToHomeScreen, callId }: Props) => { return ( <View style={styles.container}> <Text style={styles.text}>Here we will add Video Calling UI</Text> <Button title='Go back' onPress={goToHomeScreen} /> </View> ); }; const styles = StyleSheet.create({ container: { flex: 1, justifyContent: 'center', backgroundColor: 'white', }, text: { color: 'black', fontSize: 20, fontWeight: 'bold', marginBottom: 20, textAlign: 'center', }, });
123456789101112131415161718192021222324252627282930import React from 'react'; import {Text, Button, StyleSheet, View} from 'react-native'; type Props = { goToCallScreen: () => void; }; export const HomeScreen = ({goToCallScreen}: Props) => { return ( <View style={styles.container}> <Text style={styles.text}>Welcome to Video Calling Tutorial</Text> <Button title="Join Video Call" onPress={goToCallScreen} /> </View> ); }; const styles = StyleSheet.create({ container: { flex: 1, justifyContent: 'center', backgroundColor: 'white', }, text: { color: 'black', fontSize: 20, fontWeight: 'bold', marginBottom: 20, textAlign: 'center', }, });
In App.tsx
we have hardcoded the placeholders of apiKey
, userId
, token
and callId
for simplicity of tutorial.
To actually run this sample we need a valid user token.
The user token is typically generated by your server side API.
When a user logs in to your app you return the user token that gives them access to the call.
To make this tutorial easier to follow we'll generate a user token for you.
You should see on your device the following UI.
Preview of the Home Screen
Preview of empty video UI
Step 5 - Setup Video Client
Within this configuration, we will establish a StreamVideoClient
instance and facilitate the user's connection to the Stream Video service.
In real application, client creation should be encapsulated within a useEffect
hook and during unmount you should call client.disconnectUser()
to avoid creating multiple websockets.
Client instance needs to be provided to StreamVideo
component and it will provide the client instance to all the child components using React Context.
It needs to go at the top of the component tree.
1234567891011121314151617181920212223... import { StreamVideo, StreamVideoClient, } from '@stream-io/video-react-native-sdk'; ... const user = { id: userId, name: 'John Malkovich', image: 'https://robohash.org/John', }; const client = new StreamVideoClient({ apiKey, user, token }); export default function App() { ... return ( <StreamVideo client={client}> <SafeAreaView style={styles.container}> ... </SafeAreaView> </StreamVideo> ); }
You wouldn't see any change in the UI at this point, since we haven't joined the call yet.
Step 6 - Create & Join a call
In this step we will create and join a call. Call will be stored in a state variable call
and it needs to be
provided to StreamCall
component. As explained earlier, StreamCall
component is provided by the SDK and it provides all the necessary hooks for configuring UI around audio room.
We will explore these hooks later in the tutorial.
Open up src/CallScreen.tsx
and replace it with this code:
1234567891011121314151617181920212223... import {Call, StreamCall} from '@stream-io/video-react-native-sdk'; ... export const CallScreen = ({goToHomeScreen, callId}: Props) => { const [call, setCall] = React.useState<Call | null>(null); if (!call) { return ( <View style={styles.container}> <Text style={styles.text}>Joining call...</Text> </View> ); } return ( <StreamCall call={call}> <View style={styles.container}> <Text style={styles.text}>Here we will add Video Calling UI</Text> <Button title="Go back" onPress={goToHomeScreen} /> </View> </StreamCall> ); };
Also as explained earlier in Understand the Basics section, call can be created or accessed using client.call(...)
method.
Thus we need access to client
inside CallScreen
component. We will use the useStreamVideoContext
hook to get access to the client instance.
We will put the joining logic inside useEffect hook, so we automatically join the call when user goes to CallScreen
.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253import React, {useEffect} from 'react'; import { ... useStreamVideoClient, useCallStateHooks, CallingState, } from '@stream-io/video-react-native-sdk'; export const CallScreen = ({goToHomeScreen, callId}: Props) => { const [call, setCall] = React.useState<Call | null>(null); const client = useStreamVideoClient(); useEffect(() => { const _call = client?.call('default', callId); _call?.join({ create: true }) .then(() => setCall(_call)); }, [client, callId]); useEffect(() => { return () => { // cleanup the call on unmount if the call was not left already if (call?.state.callingState !== CallingState.LEFT) { call?.leave(); } }; }, [call]); if (!call) { return ( <View style={styles.container}> <Text style={styles.text}>Joining call...</Text> </View> ); } return ( <StreamCall call={call}> <View style={styles.container}> <Text style={styles.text}>Here we will add Video Calling UI</Text> <Button title="Go back" onPress={goToHomeScreen} /> <ParticipantCountText /> </View> </StreamCall> ); }; const ParticipantCountText = () => { const {useParticipantCount} = useCallStateHooks(); const participantCount = useParticipantCount(); return ( <Text style={styles.text}>Call has {participantCount} participants</Text> ); };
All essential call information can be accessed through the SDK's provided hooks, facilitated by the StreamCall
component.
You can conveniently access a range of hooks related to the call's state by utilizing the useCallStateHooks
hook as demonstrated above.
By utilizing these hooks, you can confidently depend on the most up-to-date state information.
To enhance the interactivity of this tutorial moving forward, kindly follow these steps:
- Give the app a refresh, then tap the "Join Video Call" button within your mobile app.
- Access the web version of the video call on your browser by clicking the "Join Call" link provided below, and subsequently joining the call.
Now after joining in the browser, you'll see the text update to Call has 2 participants
in your mobile app.
Let's keep the browser tab open as you go through the tutorial.
Step 7 - Rendering Video UI
In this step, we are going to add the participant's view which displays the participants' video and audio stream and other related info. It will also add buttons that allow the user to control their streaming settings of audio and video.
The CallContent
adds the following things to the UI automatically:
- Indicators of when someone is speaking.
- Quality of their network.
- Layout support for multiple participants.
- Labels for the participant names, media stream on/off status.
- A floating local video view.
- Buttons to toggle audio/video and to flip the camera.
- Button to hang up the call.
Update the CallScreen
component as below:
123456789101112131415161718192021222324252627... import { ... CallContent, } from '@stream-io/video-react-native-sdk'; ... export const CallScreen = ({goToHomeScreen, callId}: Props) => { ... return ( <StreamCall call={call}> <View style={styles.container}> // removed-block-start <Text style={styles.text}>Here we will add Video Calling UI</Text> <Button title="Go back" onPress={goToHomeScreen} /> <ParticipantCountText /> // removed-block-end // added-block-start <CallContent onHangupCallHandler={goToHomeScreen} /> // added-block-end </View> </StreamCall> ); }; ...
Now when you run the app, you'll see your local video in a floating video element and the video from your other browser tab. You should also see the buttons to control the streaming settings. The end result should look like this:
Step 8 - Customizing the UI
You can customize the UI by:
- Building your own UI components (the most flexibility, build anything).
- Mixing and matching with Stream's UI Components (speeds up how quickly you can build common video UIs).
- Theming (basic customization of colors, fonts etc).
You can provide custom component as prop to CallContent
to customize the UI.
Example below shows how you can
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556... import { ... StreamCall, CallControlProps, HangUpCallButton, ToggleAudioPublishingButton as ToggleMic, ToggleVideoPublishingButton as ToggleCamera, useCall, useStreamVideoClient, } from '@stream-io/video-react-native-sdk'; import {callId} from '../config'; const CustomCallControls = (props: CallControlProps) => { const call = useCall(); return ( <View style={styles.customCallControlsContainer}> <ToggleMic onPressHandler={call?.microphone.toggle} /> <ToggleCamera onPressHandler={call?.camera.toggle} /> <HangUpCallButton onHangupCallHandler={props.onHangupCallHandler} /> </View> ); }; ... export const CallScreen = ({goToHomeScreen, callId}: Props) => { ... return ( <StreamCall call={call}> <View style={styles.container}> <CallContent onHangupCallHandler={goToHomeScreen} CallControls={CustomCallControls} /> </View> </StreamCall> ); }; const styles = StyleSheet.create({ ... customCallControlsContainer: { position: 'absolute', bottom: 40, paddingVertical: 10, width: '80%', marginHorizontal: 20, flexDirection: 'row', alignSelf: 'center', justifyContent: 'space-around', backgroundColor: 'orange', borderRadius: 10, borderColor: 'black', borderWidth: 5, zIndex: 5, }, });
In the following example we will customize the top bar of the CallContent to display
- current participants in the call
- name of the dominant speaker
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657import { ... useStreamVideoClient, useCallStateHooks, } from '@stream-io/video-react-native-sdk'; ... const CustomTopView = () => { const {useParticipants, useDominantSpeaker} = useCallStateHooks(); const participants = useParticipants(); const dominantSpeaker = useDominantSpeaker(); return ( <View style={styles.topContainer}> <Text ellipsizeMode="tail" numberOfLines={1} style={styles.topText}> Video Call between {participants.map(p => p.name).join(', ')} </Text> {dominantSpeaker?.name && ( <Text style={styles.topText}> Dominant Speaker: {dominantSpeaker?.name} </Text> )} </View> ); }; export const CallScreen = ({goToHomeScreen, callId}: Props) => { ... return ( <StreamCall call={call}> <View style={styles.container}> <CallContent onHangupCallHandler={goToHomeScreen} CallControls={CustomCallControls} CallTopView={CustomTopView} /> </View> </StreamCall> ); }; const styles = StyleSheet.create({ ... topContainer: { width: '100%', height: 50, backgroundColor: 'black', justifyContent: 'center', alignItems: 'center', }, topText: { color: 'white', }, });
You can also adjust the style of the underlying UI using the style prop on StreamVideo
component.
The complete array of properties that can be themed is available within the theme file.
123456789101112131415161718192021222324252627282930313233import React, {useMemo, useState} from 'react'; ... export default function App() { ... // Avoid passing inline styles to the component, as it will cause unnecessary re-renders const theme = useMemo( () => ({ callControlsButton: { container: { borderRadius: 10, }, }, hangupCallButton: { container: { backgroundColor: 'blue', }, }, toggleAudioPublishingButton: { container: { backgroundColor: 'green', }, }, }), [], ); ... return ( <StreamVideo client={client} style={theme}> ... </StreamVideo> ); ...
Recap
Please do let us know if you ran into any issues while building an video calling app with React Native. Our team is also happy to review your UI designs and offer recommendations on how to achieve it with Stream.
To recap what we've learned about Stream video calling:
- You set up a call: (
const call = client.call("default", "your-call-id")
) - The call type ("default" in the above case) controls which features are enabled and how permissions are setup
- When you join a call, real-time communication is set up for audio & video calling: (
call.join()
) - State-related hooks such as
useCallCallingState
make it easy to build your own UI CallContent
is the component that renders audio and video and adds buttons to control streaming options
We've used Stream's Video Calling API, which means calls run on a global edge network of video servers. By being closer to your users the latency and reliability of calls are better. The React Native SDK enables you to build in-app video calling, audio rooms and livestreaming in days.
We hope you've enjoyed this tutorial and please do feel free to reach out if you have any suggestions or questions.
Final Thoughts
In this video app tutorial we built a fully functioning React Native messaging app with our React Native SDK component library. We also showed how easy it is to customize the behavior and the style of the React Native video app components with minimal code changes.
Both the video SDK for React Native and the API have plenty more features available to support more advanced use-cases.