Using Whereby's Browser SDK with React Hooks for a fully custom UI
What are you building?
In this tutorial you will build a fully functional video-conferencing app using the Whereby browser SDK with React hooks. This sample app will connect two participants in a video chat with custom UI, which is the most common scenario in telehealth video solutions. The app will allow the participants to control their camera and microphone, share their screen and send chat messages.
Here is a preview of what you will achieve:
Click Open Sandbox to run the code and see the result. (You will need to add your own room URL). The final code can also be found on Github.
Setup for the tutorial
Create a Whereby room
You will need a Whereby Embedded account with a Whereby room to follow this tutorial. Sign up for a free Embedded account here or login to your existing account. Then create the room using a wizard available on the homepage of your Customer Portal or with the API request. Learn more how to programatically create a Whereby room.
For this tutorial make sure that your room is unlocked, so that we don't have to implement the knock functionality.
Copy the participant link to the room. In its most basic form, the room link has the following structure:
This will be your room URL, which you will need to later paste into the code.
Initiate the CodeSandbox
In this tutorial we're using CodeSandbox - an online web development environment, which lets you write the code in your browser and immediately preview how your users will see the app that you’ve created. In the live code editor below, click Open Sandbox in the bottom-right corner to open the editor in a new tab. You will see some text and the starter code of the project.
Note
You can also follow this tutorial using your local development environment. To do this, you need to:
Install Node.js
Unzip the archive, then open a terminal and
cd
to the directory you unzippedChange the ROOM_URL constant in
App.tsx
to your own Whereby room URL.Install the dependencies with
npm install
oryarn install
Run
npm start
oryarn start
to start the development server
Overview
Now that you are set up, let's see how the Whereby browser SDK works.
Inspecting the starter code
The starter project does not include any code from the Whereby browser SDK. It's a React and TypeScript example project, but it has the SDK already installed as visible in the package.json
file. We also added some CSS classes in the styles.css
file and an IconButton
component. These will be used throughout the tutorial. These have been added, so we can focus on the functionality of the SDK instead of styling. Take some time to look around and get familiar with the starter code before proceeding with the tutorial.
The App.tsx
file is the main component in our project and is where we will be building our app.
Connecting to the room
The first natural step is to connect to the room. When we connect to a room using the browser SDK, it is essentially the same as connecting to a standard Whereby room via a direct URL. Therefore, it is possible to run a session where some participants join the room from an app using the SDK client, while others use the regular Whereby room URL directly in the browser or embedded in another platform.
Initially, there is some disposable text in the App.tsx
component. Let's remove it and add the App
CSS class to the top div to achieve the following state:
The browser SDK exposes a useRoomConnection
React hook to connect to any Whereby room. Let's import it to our app:
The useRoomConnection
hook accepts two parameters: a Whereby room URL and a set of media constraints.
For the Whereby room URL you need to provide the link to the unlocked Whereby room that you created initially. Declare it as a constant named ROOM_URL
.
You can then use localMediaOptions
parameter to control the access to the camera and microphone of the meeting participant. Here, you can set the video and audio properties to true
to allow the app access to both devices:
Try saving the file. If your room URL is valid, you should see this prompt:
If you do, then congrats! Click "Allow", and you are now connected to your room. You can verify that you are connected by opening the room URL in a different tab. You should see a "Guest" user, which is the participant joining from the SDK.
Rendering your own video cell
Once we are connected to the room, we can begin rendering the video UI. A logical first step is to render our own video cell. The useRoomConnection
hook consists of three main properties: actions
, components
, and state
.
actions
actions
Actions consist of a set of functions that you can trigger inside a room, such as toggleCamera
, toggleMicrophone
, startScreenshare
, stopScreenshare
, sendChatMessage
, and more. A full list of available actions can be found in our docs.
components
components
Components expose a VideoView
component that can be used to render video cells. This is essentially a standard <video />
element with additional properties to enhance the user experience.
state
state
This is the state of the provided room. Here you have access to your own video stream through the localParticipant
object, as well as a list of remote participants in the remoteParticipants
array. This is also includes features such as screenshare and cloud recording status.
Now let's use the functionality of the useRoomConnection
hook to render our own video cell. We need the VideoView
from components
and the localParticipant
from state
.
In this tutorial we use destructuring syntax, but you can also access the properties in the following way:
const components = roomConnection.components
Both of these approaches achieve the same result. The only difference is that with the latter, you have the flexibility to name the variable as you wish. However, for this tutorial we prefer destructuring as it makes the code more concise, especially when dealing with multiple properties.
First, capture the components
property from roomConnection
and store it in a variable. Next, create another variable to hold the VideoView
. Then, integrate VideoView
into the JSX element returned by our App
component:
This won't actually render anything yet, as we don't have a stream to attach to the VideoView
. This is where we need to pull in our state
object and access the localParticipant
property:
If you run this code, you'll get the following error:
This happens because we are trying to access the stream
property of the localParticipant
before it is mounted. To resolve this, you can use conditional rendering of the VideoView,
depending on the availability of localParticipant.stream
. We should also mute the local audio, otherwise we will hear ourselves in the audio feedback loop.
Now, you should be able to see your own video stream in your app. However, the video currently takes up the entire screen. To fix this, let's add some CSS.
In the project, there are already CSS classes available for use. First, we will add a wrapper <div />
around the video view with a class named left-section
. This wrapper will allow us to add more elements later and divide up the screen. Additionally, we will add another <div />
around the video with a class called self-view-wrapper
:
Now your video cell appears as an oval in the bottom left corner. Well done!
At this point, your code should look something like this:
Rendering the remote participant(s)
It's cool that we can see our own video, but we can't have a meeting by ourselves. Let's render the videos of other participant(s) in the video call.
As mentioned before, we can access the remote participants through the remoteParticipants
array on the state
object. This array provides a list of all participants in the call (excluding ourselves) and allows us to access their display names and media streams, among other things.
The initial step is to render a video cell for each participant:
You can now see all the other participants in the room! Join the room from a separate browser, and you will see yourself twice - one small circle in the bottom left corner (The SDK self-view), and one big video. The big video represents the remote participant. If there are more people in the call, they will be displayed side-by-side.
As this tutorial is focused on a one-on-one telehealth meeting, we are only interested in one remote participant. Therefore, we will modify our code to only render the first participant.
The following process is not recommended for a real app, as there may still be other participants connected to the room and they will be hidden with this implementation.
However, for the purpose of this tutorial, we will assume that there will only ever be two participants in the meeting.
Let's update our code to only render one participant and add some classes. We will place the remote participant in a <div />
that will have a class named video-stage
and the <VideoView />
will be wrapped in another <div />
with a class named remote-view-wrapper
:
Great! Now, the remote participant is visible in the center of the screen within an oval video cell.
Adding the display name
To identify the participants during a conversation, we can add a label on the video cell displaying their names. We can achieve this by implementing a helper function that takes a participant's id
as input. This function will return the participant's display name, or the string "Guest" if the display name is blank.
For the local participant, there is no need to have a display name, so we can simply use the string "You". We will add a <p />
tag with a class of self-name
:
For the remote participant, we will add a <p />
tag with the class remote-name
, and use our newly created helper function getDisplayName
:
Now we can see the name of the participants in the call!
Adding in-room actions
Let's add some in-room actions. First, we'll add buttons to control the camera and microphone. To simplify the styling (and to keep this tutorial as short as possible!), we have already included a component called <IconButton />
that can render the buttons. It has 3 variants: camera | microphone | share
. Let's begin with the camera.
The SDK provides a function called toggleCamera
which accepts a single parameter enabled: boolean
. We can use this function to control the state of our camera. To display a different button variant depending on whether the camera is enabled or disabled, we will use React's useState
function to store this information in our local component's state:
We set the default value to true
because we join the room with the camera turned on by default. With this local state, we can now make use of the toggleCamera
function on the roomConnection
's action object:
Let's render the button. As mentioned earlier, we will use the <IconButton />
component. In this case, we'll set the variant
prop to camera
. The <IconButton />
component also has a propererty called isActive
, which is a boolean value that determines whether to show the "on" or "off" version of the button. We can pass our newly created isCameraActive
variable to this prop.
The last prop is the onClick
handler. Here, we need to do two things: change the value of our isCameraActive
state variable to the opposite of its current value, and call the toggleCamera
function to actually turn the camera on or off.
To style this section, we'll add two divs. The first div will have a class name of controls-wrapper
and will act as the outer wrapper. The second div, with the class name buttons
, will hold our buttons. We'll place this code after the video stage (the remote video container):
Follow the same pattern for the microphone:
We now have two functional action buttons! Cool.
At this point, your code should look something like this:
Screen sharing
We can now extend the functionality of the app with the ability to share the screen.
Remote screenshare
In this step we will display the screen shared by the remote participant.
The SDK exposes the screenshares
array within the state
object of the useRoomConnection
hook. This array includes the screenshare stream of both the local and remote participant. They can be distinguished using the isLocal: boolean
prop. First, we need to destructure the screenshares
prop from the state
object. Then, render the first screenshare in the array, similar to how we did it for the remote participant video.
We will wrap Add a class with the name of screenshare-view-wrapper
in a div wrapping the VideoView
.
Wrap the VideoView
component in a <div />
element with a class name of screenshare-view-wrapper
:
To test the screen sharing functionality, open the room URL in a new browser tab, start a screenshare and return to your app. Currently, the screenshare appears directly below the remote participant's video. Our goal is to replace the remote video with the screenshare and move the remote video to the top-right corner of the shared screen. We have added CSS classes to achieve this, but we need to apply them conditionally.
Here is how we modify the rendering of the remote video view: if there are any active screenshares (screenshares.length > 0
), add the class remote-view-small
to the wrapper div. If there are no active screenshares, keep the existing remote-view-wrapper
class.
Similarly, for the remote display name add the class screenshare-remote-name
if there are any active screenshares, and keep the remote-name
class if there are none.
As a result, the screen shared by the remote participant is centered and their video cell appears in the top-right corner:
Local screenshare
In this step we will enable local screen sharing from our app. The SDK provides two actions for this: startScreenshare
and stopScreenshare
. The implementation for local screen sharing will be very similar to the camera and microphone actions. We will keep track of the screenshare state by using the useState
local component, with a default value of false
. We will also desctructure the startScreenshare
and stopScreenshare
actions from the actions
object, and then add an IconButton
with a variant of share
.
As a result we have a new action button, and we can start and stop sharing the screen from our app.
Chat
The last functionality that we will add to the app is the ability to exchange messages through a chat.
Receiving chat messages
In the first step we will display the incoming messages in the app.
Similarly to other updates that take place in the room, the SDK exposes an array of chat messages within the state
object. To begin, let's destructure this array and loop through the messages in the start of the left-section
that we created earlier. Additionally, we will wrap all the messages with a <div />
element using the chat-wrapper
class.
We can also utilize the previously defined helper function to retrieve the display name of the participant who sent the chat message. Each chat object includes a senderId
field, which represents the participant's id. If the message is from ourselves, we can simply display "You" as the sender's name.
Now we can see incoming chat messages. To test it, open the room URL in a new browser tab, and type some messages as the remote participant.
Sending chat messages
To enable sending chat messages from our app, let's add an input field. The SDK provides an action called sendChatMessage
, which takes a string as its only parameter. We will use this action to send chat messages to the room. To implement this, let's create a form and render it after the local participant in our HTML markup. We also need to keep track of the input values, so let's create a new local state called text
:
This enables us to send chat messages. However, you might notice a bug: whenever we type in the input field, the whole component re-renders, causing a flash on the video feeds. This happens because all the state is contained within the same component, causing it to re-render whenever a value changes. We can fix that easily by extracting the chat input in to a separate component. Let's create a new file called ChatInput.tsx
and move the code there:
Now we can import this component into our app, and pass the sendChatMessage
function as a prop. Remove all the code that we just added to App.tsx
, and replace it with this:
Well done! We can now type chat messages without triggering re-renders. However, there are still re-renders whenever a new chat message is received or when the camera and microphone are turned on or off. These issues can be fixed using the same above approach.
Conclusion
Congrats! You have created a fully custom video app that:
Connects to a Whereby room
Acquires media permissions
Renders the local video stream
Renders the remote participant's video stream
Renders screenshares
Allows users to toggle their camera and microphone on and off
Allows users to share their screen
Shows chat messages as they come are received
Allows users to send chat messages to other participants in the room
Great job! We hope this tutorial has given you a better understanding of how the React hooks in Whereby browser SDK work, and that you will continue to build with it.
The final code can be viewed here:
Next steps
If you would like to continue working on this app, a good next step would be to separate things into components. This will help eliminate the re-rendering problems that we discussed.
For example, you can create a <Controls />
component to handle the buttons. This way, the app won't re-render when any of the buttons are clicked. Similarly, you can create a separate component for the chat, so that the app doesn't re-render whenever a new chat message comes in.
lastly, it would be a good exercise to handle multiple remote participants or multiple screenshares.
Last updated