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 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 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 Zoom/Whatsapp style video calling experience 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 0 - Prepare your environment

For this tutorial, we'll need a few tools to be installed on our device. You can skip this step in case you already have them installed.

  • Node.js (version 18 or higher)
  • Yarn (version 1.22 or higher)

Step 1 - Set up an app and install React Video SDK

In this step, we will create a new React application using the Vite CLI, and install Stream's React Video SDK. We recommend using Vite because it is fast and easy to use.

Terminal (bash)
1
2
3
yarn create vite video-call --template react-ts cd video-call yarn add @stream-io/video-react-sdk

Step 2 - Create & Join a call

We open up src/App.tsx and replace it with this code:

src/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
35
36
37
38
39
40
41
42
43
44
45
import { CallingState, StreamCall, StreamVideo, StreamVideoClient, useCall, useCallStateHooks, User } from '@stream-io/video-react-sdk'; const apiKey = 'REPLACE_WITH_API_KEY'; const token = 'REPLACE_WITH_TOKEN'; const userId = 'REPLACE_WITH_USER_ID'; const callId = 'REPLACE_WITH_CALL_ID'; // set up the user object const user: User = { id: userId, name: 'Oliver', image: 'https://getstream.io/random_svg/?id=oliver&name=Oliver', }; const client = new StreamVideoClient({ apiKey, user, token }); const call = client.call('default', callId); await call.join({ create: true }); export default function App() { return ( <StreamVideo client={client}> <StreamCall call={call}> <MyUILayout /> </StreamCall> </StreamVideo> ); } export const MyUILayout = () => { const call = useCall(); const { useCallCallingState, useParticipantCount } = useCallStateHooks(); const callingState = useCallCallingState(); const participantCount = useParticipantCount(); if (callingState !== CallingState.JOINED) { return <div>Loading...</div>; } return ( <div> Call "{call?.id}" has {participantCount} participants </div> ); };

To actually run this sample, we need a valid user token. The user token is typically generated by our server side API. When a user logs in to our app we return the user token that gives them access to the call. To make this tutorial easier to follow, we've generated the credentails for you.

Now we are ready to run our app. In our terminal, we run:

bash
1
yarn dev

Once we open the browser and run the app, it will connect successfully to our systems. The text will say "Call ... has 1 participant" (yourself).

Let's review what we did in the above code.

User setup

First, we create a user object. Typically, these users are synced via a server side integration from our own backend. Alternatively, we can also use guest or anonymous users.

ts
1
2
3
4
5
6
7
import type { User } from '@stream-io/video-react-sdk'; const user: User = { id: userId, name: 'Oliver', image: 'https://getstream.io/random_svg/?id=oliver&name=Oliver', };

Client setup

Next, we initialize the client by passing the API Key, user and user token.

ts
1
2
3
import { StreamVideoClient } from '@stream-io/video-react-sdk'; const client = new StreamVideoClient({ apiKey, user, token });

Create and join call

After the user and client are created, we create a call like this:

ts
1
2
const call = client.call('default', callId); await call.join({ create: true });

As soon as we use call.join() the connection for video & audio is setup.

In production grade apps, we'd typically store the call instance in a state variable and take care of correctly disposing it. Read more in our Joining and Creating Calls guide.

Rendering the UI

Lastly, we render the UI by observing the call state through the call state hooks:

tsx
1
2
3
4
5
6
7
8
import { useCallStateHooks } from '@stream-io/video-react-sdk'; // all call state hooks are available in the useCallStateHooks() object // your IDE should help you to explore all of them const { useCallCallingState, useParticipantCount } = useCallStateHooks(); const callingState = useCallCallingState(); const participantCount = useParticipantCount();

We'll find all relevant state for the call in call.state - also exposed through a set of SDK provided hooks. The documentation on Call and Participant state explains this in further detail.

To render the UI, we can either use SDK provided components or build our own on top of the Core Components and Call State Hooks.

Step 3 - Joining from the web

To make this a little more interactive, let's join the call from your browser.

For testing you can join the call on our web-app:

In your browser, you'll see the text update to: Call <call-id> has 2 participants. Let's keep the browser tab open as you go through the tutorial.

Step 4 - Rendering Video

In this next step, we're going to render our local & remote participants video.

Before we start, we will delete the default contents of the index.css file to ensure it doesn't interfere with our setup.

Let's update our MyUILayout component to load the predefined SDK stylesheet, apply the default theme and render the participant's video and play their audio.

src/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
35
36
import { CallingState, StreamCall, StreamVideo, StreamVideoClient, useCallStateHooks, User, StreamTheme, } from '@stream-io/video-react-sdk'; import '@stream-io/video-react-sdk/dist/css/styles.css'; // ... rest of the App.tsx code export const MyUILayout = () => { const { useCallCallingState, useLocalParticipant, useRemoteParticipants, } = useCallStateHooks(); const callingState = useCallCallingState(); const localParticipant = useLocalParticipant(); const remoteParticipants = useRemoteParticipants(); if (callingState !== CallingState.JOINED) { return <div>Loading...</div>; } return ( <StreamTheme> <MyParticipantList participants={remoteParticipants} /> <MyFloatingLocalParticipant participant={localParticipant} /> </StreamTheme> ); };

We will now create a MyParticipantList component that will render the remote participants' video and play the participants' audio. We are going to use the SDK-provided ParticipantView component. We'll learn more about it later in this tutorial.

src/App.tsx (tsx)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { ParticipantView, StreamVideoParticipant } from '@stream-io/video-react-sdk'; // ... rest of the App.tsx code export const MyParticipantList = (props: { participants: StreamVideoParticipant[] }) => { const { participants } = props; return ( <div style={{ display: 'flex', flexDirection: 'row', gap: '8px' }}> {participants.map((participant) => ( <ParticipantView participant={participant} key={participant.sessionId} /> ))} </div> ); };

With this, we have a simple grid layout for the remote participants. Let's add a floating video (MyFloatingLocalParticipant) for the local participant:

src/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
// ... rest of the App.tsx code export const MyFloatingLocalParticipant = (props: { participant?: StreamVideoParticipant }) => { const { participant } = props; if (!participant) { return <p>Error: No local participant</p>; } return ( <div style={{ position: 'absolute', top: '15px', left: '15px', width: '240px', height: '135px', boxShadow: 'rgba(0, 0, 0, 0.1) 0px 0px 10px 3px', borderRadius: '12px', }} > <ParticipantView participant={participant} /> </div> ); };

Now when we run the app (yarn run dev), we'll see our local video in a floating video element and the video from our other browser tab.

The final result should look somewhat like this (the person on screen might differ):

Let's review the changes we made. ParticipantView is one of our primary low-level components.

tsx
1
2
3
import { ParticipantView } from '@stream-io/video-react-sdk'; <ParticipantView participant={participant} key={participant.sessionId} />;

It displays the video stream and comes with some default UI elements, such as participant's name. The video is lazily loaded, and only requested from the video infrastructure if we're actually displaying it. So if we have a video call with 200 participants, and we show only 10 of them, we'll only receive video for 10 participants. This is how software like Zoom and Google Meet make large calls work.

MyFloatingLocalParticipant renders a display of our own video.

tsx
1
2
3
4
const { useLocalParticipant } = useCallStateHooks(); const localParticipant = useLocalParticipant(); <MyFloatingLocalParticipant participant={useLocalParticipant} />;

MyParticipantList renders a list of remote participants.

tsx
1
2
3
4
const { useRemoteParticipants } = useCallStateHooks(); const remoteParticipants = useRemoteParticipants(); <MyParticipantList participants={remoteParticipants} />;

Step 5 - A Full Video Calling UI

The above example showed how to use the call state and React to build a basic video UI. For a production version app, we'd want a few more UI elements:

  • Indicators of when someone is speaking
  • Quality of their network connection
  • Layout support for more than two participants
  • Labels for the participant names
  • Call controls

Stream's React Video SDK ships with several React components to make this easy. We can customize the components with theming, arguments and swapping parts of them. This is convenient if we want to quickly build a production ready calling experience for our app (and if you need more flexibility, many customers use the above low level approach to build a UI from scratch).

To render a full calling UI, we'll leverage the SpeakerLayout component for arranging the video elements, and the CallControls component for rendering the call controls. Also, we are going to introduce a minimalistic CSS file to make the UI look a bit nicer.

src/index.css (css)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
body, html { height: 100%; width: 100%; margin: 0; font-family: sans-serif; } .str-video { background-color: #272a30; color: #ffffff; height: 100dvh; width: 100%; display: flex; flex-direction: column; min-width: 0; max-width: 100%; }

Then we're switching out the entire content of our App.tsx file with this:

src/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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import { CallControls, CallingState, SpeakerLayout, StreamCall, StreamTheme, StreamVideo, StreamVideoClient, useCallStateHooks, User, } from '@stream-io/video-react-sdk'; import '@stream-io/video-react-sdk/dist/css/styles.css'; import './index.css'; const apiKey = 'REPLACE_WITH_API_KEY'; const token = 'REPLACE_WITH_TOKEN'; const userId = 'REPLACE_WITH_USER_ID'; const callId = 'REPLACE_WITH_CALL_ID'; const user: User = { id: userId, name: 'Oliver', image: 'https://getstream.io/random_svg/?id=oliver&name=Oliver', }; const client = new StreamVideoClient({ apiKey, user, token }); const call = client.call('default', callId); call.join({ create: true }); export default function App() { return ( <StreamVideo client={client}> <StreamCall call={call}> <MyUILayout /> </StreamCall> </StreamVideo> ); } export const MyUILayout = () => { const { useCallCallingState } = useCallStateHooks(); const callingState = useCallCallingState(); if (callingState !== CallingState.JOINED) { return <div>Loading...</div>; } return ( <StreamTheme> <SpeakerLayout participantsBarPosition='bottom' /> <CallControls /> </StreamTheme> ); };

The final UI should look like this:

When we now run our app, we'll see a more polished video UI. It supports reactions, screensharing, active speaker detection, network quality indicators etc. The most commonly used UI components are:

  • ParticipantView: For rendering video and automatically requesting video tracks when needed. Most of the Video components are built on top of this.
  • DefaultParticipantViewUI: The participant's video + some UI elements for network quality, reactions, speaking etc. ParticipantView uses this UI by default.
  • PaginatedGridLayout: A grid of participants. Support pagination out of the box.
  • SpeakerLayout: A layout that shows the active speaker in a large video, and the rest of the participants a scrollable bar.
  • CallControls: A set of buttons for controlling your call, such as changing audio and video mute state, switching mic or a camera.

The full list of UI components is available in the docs.

Because of simplicity, in this tutorial, we are skipping some of the best practices for building a production ready app. Take a look at our sample app linked at the end of this tutorial for a more complete example.

Step 6 - Customizing the UI

We can customize the UI by:

  • Building our own UI components (the most flexibility, build anything).
  • Mixing and matching with Stream's UI Components (speeds up how quickly you can build custom video UIs).
  • Theming (basic customization of colors, fonts etc).

You can find many examples on how to build your own custom UI components in our UI Cookbook docs.

Step 7 - Enable Noise Cancellation

Background noise during a call session is never pleasant for the call participants.

Our SDK provides a plugin that helps to greatly reduce the unwanted noise caught by users' microphones. Read more on how to enable it here.

Recap

Please do let us know if you ran into any issues while building an video calling app with React Video SDK. Our team is also happy to review your UI designs and offer recommendations on how to achieve it with Stream.

Let's recap what we've learned:

  • To set up a call: const call = client.call('default', '123');.
  • The call type ('default' in the above case) controls which features are enabled and how permissions are setup.
  • When we join a call, realtime communication is setup for audio & video calling: call.join().
  • Call state call.state and helper state access hooks exposed through useCallStateHooks() make it easy to build our own UI
  • ParticipantView is the low level component that renders video, plays audio and by default, it utilizes DefaultParticipantViewUI that adds UI elements as participant name, network quality, etc...

We've used Stream's Video Calling API, which means calls run on a global edge network of video servers. By being closer to our users the latency and reliability of calls are better. The React SDK enables us to build in-app video calling, audio rooms and livestreaming in days.

The source code for the final app can be found in our GitHub repository.

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 video app with our React SDK component library. We also showed how easy it is to customize the behavior and the style of the React video app components with minimal code changes.

Both the video SDK for React 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.