Did you know? All Video & Audio API plans include a $100 free usage credit each month so you can build and test risk-free. View Plans ->

React Native Video Calling Tutorial

The following tutorial shows you how to quickly build a Video Calling app leveraging Stream's Video API and the Stream Video React Native components. The underlying API is very flexible and allows you to build nearly any type of video experience.

example of react video and audio sdk

This tutorial teaches you how to build a Zoom/Whatsapp-style video calling app.

  • All calls run on Stream's global edge network for optimal latency & reliability.
  • Configuring permissions gives you fine-grained control over who can do what.
  • Video quality and codecs are automatically optimized.
  • The calling support is 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":

Terminal (bash)
1
2
npx @react-native-community/cli@latest init VideoCallExample cd VideoCallExample

If you are having trouble with iOS, try to reinstall the dependencies by running:

  1. cd ios to navigate to the ios folder
  2. bundle install to install Bundler
  3. bundle exec pod install to install the iOS dependencies managed by CocoaPods

Step 2 - Install the SDK and Declare Permissions

To install the Stream Video React Native SDK, run the following command in your terminal of choice:

Terminal (bash)
1
yarn 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:

Terminal (bash)
1
2
3
4
5
6
7
yarn 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

The video calling app we built in this tutorial requires permission to access the users' camera, microphone, and network state.

Android
iOS

In AndroidManifest.xml add the following permissions before the application section.

xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<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>

In android/build.gradle add the following inside the buildscript section:

java
1
2
3
4
5
6
7
buildscript { ext { ... minSdkVersion = 24 } ... }

Run the App

Due to the emulator's limitations in audio and video support, we highly recommend running the app on a physical device to ensure the best possible experience. For guidance on running the app on a physical device, refer to the React Native documentation.

Step 3 - Understand the Basics

Before we write code, you should be familiar with two concepts: StreamVideoClient and Call.

  • StreamVideoClient is the SDK's low-level JavaScript client communicating with the Stream Video service. It provides all the necessary methods to connect the user Stream service, query calls, create calls, etc.
  • 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.
tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 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 your dashboard
  • user is the user object { id: "john_smith", "name": "John Smith" }
  • token is the user token generated by your server-side API. You can use the token generated by the Token Generator for development. You can read more information about client authentication on the Client & Authentication guide.

The client and call instances are made accessible to all the video components from the SDK via the StreamVideo and StreamCall components, respectively. In your app, you must wrap your component tree with the StreamVideo component and provide the client instance as a prop. The client creation typically occurs during the application's sign-in stage. Similarly, you must wrap your call-specific UI components with the StreamCall component and provide the call instance as a prop.

The StreamVideo and StreamCall components are basically context providers, enabling you to consume hooks provided by the SDK. These hooks do all the heavy lifting around call management and provide the necessary state and methods to build your UI.

tsx
1
2
3
4
5
6
7
8
9
<StreamVideo client={client}> ... <StreamCall call={call}> <View> <Text>Video Call UI</Text> </View> </StreamCall> ... </StreamVideo>

Step 4 - Setup a Starter UI

Let's begin by creating a basic UI for our video calling app. Usually, you would use a navigation library like React Navigation to navigate between screens. But we'll keep this tutorial 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 (takes callId as a prop)

Now copy the following content into the respective files (as mentioned in the header):

App.tsx
src/CallScreen.tsx
src/HomeScreen.tsx
App.tsx (tsx)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import 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, }, });

In App.tsx, we have hard-coded the placeholders of apiKey, userId, token and callId for the sake of simplicity in the tutorial. To 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 the Video Client

Within this configuration, we will establish a StreamVideoClient instance and facilitate the user's connection to the Stream Video service. In real applications, the client creation should be encapsulated within a useEffect hook and during unmount you should call client.disconnectUser() to avoid creating multiple websockets.

The client instance needs to be provided to the StreamVideo component, which will then provide the client instance to all the child components using React Context. It needs to be at the top of the component tree.

App.tsx (tsx)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
... 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 will not see any change in the UI at this point since we have yet to join the call.

Step 6 - Create & Join a Call

In this step, we will create and join a call. The call will be stored in a state variable call, which must be provided to the StreamCall component. As explained earlier, the SDK provides the StreamCall component, which provides all the necessary hooks for configuring the video calling app's UI. We will explore these hooks later in the tutorial.

Open src/CallScreen.tsx and replace its content with this code:

src/CallScreen.tsx (tsx)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
... 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 the Understand the Basics section, a call can be created or accessed using the client.call(...) method. Thus, we need access to the client inside the CallScreen component. We will use the useStreamVideoContext hook to get access to the client instance.

We will put the joining logic inside the useEffect hook, so we automatically join the call when a user goes to the CallScreen.

src/CallScreen.tsx (tsx)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import 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> ); };

The SDK's provided hooks, facilitated by the StreamCall component, allow access to all essential call information. As demonstrated above, the 'useCallStateHooks' hook will enable you to conveniently access a range of hooks related to the call's state. You can confidently depend on the most up-to-date state information by utilizing these hooks.

To enhance the interactivity of this tutorial moving forward, kindly follow these steps:

  • Refresh the app, then tap the "Join Video Call" button within your mobile app.
  • Access the web version of the video call in your browser by clicking the "Join Call" link to join the call.
For testing you can join the call on our web-app:

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 - Render the Video Calling UI

In this step, we will add the participant's view, which displays the participants' video and audio stream and other related information. It will also add buttons allowing users to control their audio and video streaming settings.

The CallContent adds the following things to the UI automatically:

  • Indicators to visualize when someone is speaking.
  • The quality of call participants' network.
  • Layout support for multiple participants.
  • Labels for the participants' names and the media stream's on/off status.
  • A floating local video view.
  • Buttons to toggle audio/video and to flip the camera.
  • A button to end/terminate the call.

Update the CallScreen component as follows:

src/CallScreen.tsx (tsx)
1
2
3
4
5
6
7
8
9
10
11
12
-
13
-
14
-
15
+
16
+
17
+
18
19
20
21
22
23
... import { ... CallContent, } from '@stream-io/video-react-native-sdk'; ... export const CallScreen = ({ goToHomeScreen, callId }: Props) => { ... 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 /> <CallContent onHangupCallHandler={goToHomeScreen} /> </View> </StreamCall> ); }; ...

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 result should look like this:

Step 8 - Customize the Calling UI

You can customize the UI by:

  • Building own UI components. This option provides the most flexibility to build any video calling experience you could imagine.
  • Mixing and matching with Stream's UI Components. This option speeds you up to build common video UIs.
  • Theming. The theming option provides basic customization of colors, fonts, icons, etc.

You can provide a custom component as prop to the CallContent component to customize the UI. The example below shows how you can customize the UI.

src/CallScreen.tsx (tsx)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
... 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.
src/CallScreen.tsx (tsx)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
import { ... 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.

App.tsx (tsx)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import 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 let us know if you encounter issues building a video calling app with our React Native SDK. Our team is also happy to review your UI designs and advise you on how to integrate them with Stream.

To recap what we've learned about Stream's video calling service:

  • 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 set.
  • 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 UIs.
  • The CallContent component renders audio and video and adds buttons to control streaming options.

Using the Stream's Video Calling API, all calls run on a global edge network of video servers closer to all users. Being closer to your users improves call latency and reliability. The React Native SDK enables you to build in-app video calling, audio rooms, and live streaming in days.

We hope you've enjoyed this tutorial. Please feel free to reach out if you have any suggestions or questions.

Final Thoughts

In this video calling app tutorial we built a fully functioning React Native video 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.

Give us feedback!

Did you find this tutorial helpful in getting you up and running with your project? Either good or bad, we're looking for your honest feedback so we can improve.

Start coding for free

No credit card required.
If you're interested in a custom plan or have any questions, please contact us.