With the recent launch of Stream Chat, the team here has been working with vendors to add real-time chat and messaging to various platforms. We’ve had several requests for voice and video chat, and after looking around at the market, it was a clear choice to partner up with Voxeet (recently acquired by Dolby). The proof is in the pudding, so we built out a fully functional proof of concept for a live video-conferencing application using Stream Chat and Voxeet
•Updated: Jun 9, 2021
•Published: Nov 1, 2019
With the recent launch of Stream Chat, the team here has been working with vendors to add real-time chat and messaging to various platforms. We’ve had several requests for voice and video chat, and after looking around at the market, it was a clear choice to partner up with Voxeet (recently acquired by Dolby). The proof is in the pudding, so we built out a fully functional proof of concept for a live video-conferencing application using Stream Chat and Voxeet as a web application with React.
The reason for our decision to use Voxeet over other competitors such as Twilio’s Programmable Video platform was primarily due to the following:
- Predictable and generous pricing model that flat out makes sense – it’s based on the number of voice and video minutes
- Their documentation is written for developers with an emphasis on how to integrate – including nice example tutorials in the beginning
- They provide a React integration that works flawlessly with the Stream Chat React components making it easy for developers to access the underlying Voxeet API and connect it with Stream Chat
Furthermore, our initial tests showed that Voxeet had substantially less latency and offered far more clarity when it came to real-time video (and their video conferencing API) compared to Twilio. With better documentation, we knew ahead of time that we would be able to execute without running into any hiccups.
For the rest of this post, we will outline how we went about building a competitor similar to Zoom (the video communication tool) in less than a week. We\'ll be using Voxeet for live video (via WebRTC as well as Stream Chat for real-time chat/messaging capabilities.
To fully follow along with this tutorial, you’ll want to make sure that you have a decent understanding of the following:
- Redux/Redux Saga
You will also want to ensure that you have the following installed on your machine:
And, you’ll need to have created free accounts with the following services:
- Heroku (for hosting the API – free tier)
- Stream (for real-time chat functionality – free 14-day trial)
- Voxeet (for video conferencing – free tier)
Note: We’ll be using the latest version of Chrome on macOS to test throughout this tutorial.
The build-out of this application is rather straightforward. It requires two primary elements – the frontend UI and the backend API. If you want to take it a step further, you can host the frontend CRA application on Netlify and the API on Heroku in just a few minutes.
Step 1: Frontend UI
- Stream Key
- Stream Secret
- Voxeet Key
- Voxeet Secret
npx create-react-app stream-voxeet in your terminal. Once complete, add the Stream API key and the Voxeet keys from the associated dashboards to a
.env file within your project\'s root directory for safe-keeping.
Note: With create-react-app, you must prepend all environment variables with REACT_APP.
You will need the following environment variables in your
Head back to your terminal and install the dependencies we need for the project:
Note: In our initial testing we realized that the Voxeet components currently throw an error with the latest version of react-redux. Make sure you enter firstname.lastname@example.org when you install the dependencies above.
Finally, create a
jsconfig.json file in the root of your application. Copy and paste the following:
Note: The jsconfig.json file will allow aliased imports based on your directory structure without having to use absolute paths to your modules and components. E.g. import Button from components/Button.
Now that our project is all set up let’s jump into our code editor and start building.
Step 2: Setup
First up, we’ll get our Redux store, middleware, and styled-components theme set up, as well as removing the defaults provided by the CRA boilerplate.
Next, navigate to the
src directory in your project and remove the
You’ll want to remove the imports for these assets in the
App.js and the
index.js files, as well as strip all boilerplate markup out of
App.js – leaving only the parent
Note: You can also remove the className prop as we will replace all of the CSS with styled-components. Doing so will provide a theme file where you can easily tweak aspects of the design of the app once it is fully built.
Then, create the following directories inside your
We’ll start by creating a couple of files inside of the
styles directory. First, create
styles/breakpoints.js respectively, and paste the following:
Next, we will import these variables in a moment to help populate our styled-components theme. Then, we’ll create a
colorUtils.js file and paste the following code inside:
Note: This file is adapted from v0.x of Material UI and offers handy utilities for handling colors – such as converting hex to RGB/RGBA and quickly altering the opacity, hue, and saturation of the color just by passing in a hex value. We often add this to our theme definition for easy access inside styled-components.
Now, we can bring this all together in
styles/theme.js – this theme will be available inside all of our styled-components and accessible through the
Last, we’ll create
styles/global.js. This file is effectively a drop-in replacement for
index.css that we removed earlier. Please note that we’ll also import two additional files at a later time to override styles for both stream-chat-react and voxeet.
We’ll get this all tied together in our
App.js shortly, but first, we’ll set up our Redux store.
Inside of your data directory, add a file called
createReducer.js with the following code inside of it:
The following code will hook the Voxeet reducer up to our Redux store and enable the
Conference components to work correctly. We will also create a
rootSaga.js file within the data directory; however, for now, we will leave it blank and come back to it later.
Next, let\'s create the Redux store itself in
data/createStore.js to tie it all together – we will add the thunk middleware which is required under the hood in the Voxeet components, as well as the redux-saga middleware which we will use a bit later on when we add simple authentication to the app as well as some other fun stuff.
Now that our theme and our Redux store are ready to go, we’ll wrap our app in
Provider and include our global styles and initial route definitions. To do this, open
App.js and make sure it looks like the following snippet:
We’re now ready to start including Voxeet’s components and later, Stream Chat too. However, you should now have two missing imports from the file above. One is the Conference screen which we will create in just a second. The other is
history in this way, we can pass it to the
Router in the file above and it will operate identically to react-router v4’s
BrowserRouter. We can also import it in our sagas (and anywhere in our code for that matter) to navigate programmatically without using
<Link /> or in areas where the
history prop is inaccessible.
Here is the code for the history util:
Note: We will still want to use the component in our JSX code – but this allows us, for example, to redirect a user when they log in or log out, from within the authentication saga itself.
Step 3: Initializing Voxeet
We’ll start by creating the Conference screen where the video calls themselves will take place. Create
Conference/Conference.js inside of your
screens directory, open it in your editor and paste in the following:
You should now be able to go to
http://localhost:3000/test to see a video call screen that is fully operational. The Voxeet components are fantastic. Out of the box, Voxeet provides full functionality to all of the features available by their SDK and APIs.
We are using the URL param
match.params.conferenceAlias as defined in
App.js to scope each conference to the URL – which also enables users to share the URL to invite other participants, much like Google Hangouts or Zoom. At this point, if you were to push your site up to Netlify, you can share the URL with a friend, and you will be able to conduct a full-featured video call! Note that you can use any random string after the
/ in the URL to set the conference id – but more on that later.
Right now we only have control over the styling of the Voxeet UI through overriding the default CSS and users will have randomly generated usernames and ids. So, let’s fix that.
We can achieve much higher customization of the UI by passing in some additional props to the
ConferenceRoom component, namely
actionsButtons. Both of these props should be passed either a function that returns a component, or the component itself. We can start by passing NOOP functions to these props as follows:
The components passed to these props will replace the chat drawer and the bottom actions bar, respectively. The
ConferenceRoom will pass down the necessary state from Redux as props, allowing us to use custom components that will alter the state of the call. Furthermore, it will aid us in providing a custom chat UI, which we\'ll use Stream Chat for later in the tutorial!
With the above changes, you should now see the main wrapper of the Conference screen will be full-height and full-width in the browser, and the actions bar along the bottom will be gone. We can now start to build our custom call UI.
Inside of the
screens/Conference directory, create a
components directory and inside create a new file called
For now, we can leave this file empty as we will need some other components first that we can then utilize within the ActionButtons bar itself.
src/components let’s create a new directory called Icons and an
Icon.js file inside with the following code:
This file creates a re-usable
Icon component that we can use to generate a set of consistent SVG icons. We can then use these icons just the same as any other react component. The
Icon component also offers additional functionality – such as the ability to control the icon height/width via the
size prop (while maintaining consistency if we set the
viewBox correctly), as well as the ability to change the color with the
Note: Thanks to the withTheme HOC, we can pass color names that map directly to our styled-components theme – i.e., purple, red, error, placeholder, text, etc. meaning we can add more color options by adding values to styles/colors.js.
For the sake of speed and ease, we used Material Icons and converting them for use inside of our new Icon component is a breeze.
Now that we have icons to use, we can create our
ActionButton component that we will use inside of the
ActionsButtons.js file we created earlier. Create a new
ActionButton.js file inside of
screens/Conference/components and paste in the following code:
And finally, we’re ready to create our custom actions bar!
screens/Conference/components/ActionsButtons.js we can now add the following:
Currently, we are setting the
unreadCount prop to fall back to
0 as we are not defining it anywhere just yet. We will pull this data off of our Redux store later once we have Stream Chat set up.
All other props are provided by the Voxeet
ConferenceRoom component and are used internally by the default
ActionsButtons UI to change the state of buttons conditionally – so we will do the same. This state depends on parameters such as whether or not the user\'s mic or camera are disabled, if the chat drawer is open, etc.
Note that we have left some of the default Voxeet views in the UI such as settings (for controlling the input/output devices and the
AttendeesList for viewing all the active participants).
These views are great out of the box, and small amounts of CSS can get them looking as we would want without building them out by hand. Currently, there is not a prop available to pass a custom settings view; however, should you wish to build the attendees list by hand, as we have with the
ActionsButtons, you can pass a component in the same way to the
attendeesList prop of the
Now all that’s left to do is import our shiny new
ActionsButtons into the
ConferenceRoom as below:
Now in your browser, you should see our custom call UI along the bottom of the screen! You can hang up the call, disable your camera or microphone, share your screen with the other participants, change your I/O settings, view the list of participants and, last but not least, toggle the chat drawer. This will be empty for now, but we’ll come back to this shortly.
We are also going to want to override some of Voxeets CSS to have the Conference screen match the rest of our application. For the sake of brevity, below is the complete CSS file from our finished version - feel free to tweak any of the values you see fit.
Go to your
src/styles folder and create a new directory called
css – then create a new file inside called
voxeet.js with the following styled-components CSS code:
Note: By creating our CSS overrides in this way, we can write our CSS as we usually would in a standard .css file, but with access to our theme.
All that’s left to do now is to jump into
styles/global.js and add the following two lines to import our overrides into the global CSS file:
Step 4: Setting up the Backend API & Authentication
Prior to setting up our Chat components, we will make things much easier for ourselves by first setting up the backend API and authentication for the app.
The backend for this project is simple, and Voxeet’s react components handle all of the Voxeet related backend functionality for you. All that we will need to do is pass in user data, which means we only need to generate a token for Stream Chat.
You can download the boilerplate here and follow along with the following section to get the backend and authentication flow working in your app. Alternatively, you can hit the Heroku button to immediately deploy the complete API code to Heroku or click here to view the finished API repo.
Note: You can click the Heroku Deploy button below to launch a Stream Chat trial using our prebuilt boilerplate API. Please take caution when using this, as the API does not enforce auth when hitting the /v1/token endpoint. If you would like to lock this down, we secure adding additional security measures to prevent authorized access.
Note: You can safely skip this next part and jump to building the authentication flow if you go the auto-deploy with Heroku route.
Let’s get to coding the backend!
Fortunately, a lot of the work has already been completed through the provided boilerplate API. We need to make a few small changes to get it working how we want.
Outside of your frontend repository, run the following command in your terminal from whichever directory you want to store the API code in.
cd stream-voxeet-api && yarn and open the
stream-chat-voxeet directory in your editor.
We’ll start by renaming
.env and inserting our Stream key and secret that we saved earlier into the correct variables. Once you\'re done, it should look something like this:
Now, let’s hop back into the terminal and run
yarn dev to spin up the development server. The repo uses nodemon to automatically refresh when you make a change whilst keeping the server alive.
Finally, we need to open
src/controllers/v1/token/token.action.js and do a quick “find all” so that we can change all references to
token.action.js file should now look like the following snippet:
Note: By changing data.email to data.username we are changing the default behavior of authenticating via an email address, to using a simple username that we will build a login form for in just a second.
Optionally, you will see on line 28 the repo uses robohash to generate an avatar for the user. In our final version, we used ui-avatars.com as the images it generates are better suited to our design, but you can drop in any avatar generation URL here that you see fit.
And that’s it for the backend!
You can test that this is all working properly with Postman (or your REST client of choice is) by leaving the server running, and firing a POST request to
http://localhost:8080/v1/token where the body is the following object:
If successful, you will see the following in the response body:
Awesome! We can now start to build our login form and authenticate users inside our app.
Leave your API server running for now, and head back to your frontend code in your editor.
First up, let’s add a new env variable (in
REACT_APP_API_ENDPOINT and set it’s value to
http://localhost:8080/v1 and then make sure we have rebooted the frontend by closing the dev server and running
yarn start once again.
We’ll also need this small wrapper utility around Axios to make a request to our backend, save it in your project file in
Building the Authentication Flow
Now that we’re all set up to generate tokens for our users, we’ll kick off the auth flow by creating a new directory inside of our
screens directory called
Login with a
Login.js file inside of that with the following code:
Login.js file, we are importing a few components that we don’t have in our project just yet. The biggest of which is the
LoginForm component which we will come back to in a second but first, here is the code for the
and Button` components we will need to build the form UI.
Now we have all the pieces we need to build our
LoginForm component using Formik and Yup which we installed at the start of the tutorial.
Formik is an awesome library that allows us to easily build complex forms with React, and Yup provides a nice and powerful way to validate our input values. We won’t be utilizing the full potential of Formik in this tutorial, but should you decide to build upon this project after your done with the tutorial, this will set you up with a strong foundation for building any kind of form quickly and painlessly.
First, we’ll create a new directory inside of
forms and inside of there create a file at this path
We start off by importing
<Formik /> component will be the root element in our render function and we pass in our
onSubmit handler from props as well as some initial values as defined in the getter function on line 24 and finally, our
this.renderForm function as the child of the component.
Note: The initial values ensure that our inputs are always ‘controlled’. If we don’t do this, the value will be undefined and react will throw an error to say we have switched the input from a non-controlled to a controlled state.
<Field /> component provided by Formik encapsulates all the functionality needed to pass the
onChange handler down to our custom input component, along with error messages (if there are any). It also ensures that the value is updated in the correct key of the forms value store using the name prop, which we will set to
name=”username” to match the key in the initial values.
We are also importing
validationSchema.js – this is where Yup comes in. The syntax is pretty easy to grasp and not only are the docs easy to follow but Yup also provides useful helpers for validating emails, phone numbers, etc as well as the option to pass custom regex validators and easily set the error messages that will be passed into the input component if the conditions are not met.
Create the validation file in your
LoginForm directory, next to the form component and paste in the following code:
Now, back inside of
LoginForm.js you’ll see that we are passing our Yup schema into Formik as the
validationSchema prop. At this point, our form will be fully operational, but isn’t yet rendered to the DOM anywhere and doesn’t send it’s value to the backend just yet either.
Let’s quickly jump into
App.js and update the file to render our login route.
Now, when you navigate to http://localhost:3000/ you should see our login page rendered to the screen, complete with the login form. You can also fill out the form, click the button below and open your console to see the output from the form.
We now need a way to send our form data to the backend, retrieve our token, and authenticate the user before sending them to the Conference Screen. Time for some Redux magic!
We’ll start out by creating our action creators and action types. You can find the relative snippets below, which will save inside of
data/auth in our project.
Note: You’ll notice we are passing conferenceAlias as the second argument to the loginRequest action. This will be used later on when a user receives a link to join a call so that we can route them to the correct place once they have logged in.
Now, inside of
auth/sagas/index.js add the following code:
Generators, used heavily in redux-saga, are a new feature of JS as of ES6 and work very similarly to async/await. They are initialized using the
function * () syntax and have a special keyword
yield that works very much like
await by “pausing” the function and waiting for the expression to resolve before continuing.
redux-saga also provides some helpers like
takeEvery (used in the above snippet) which will take an action type as the first argument and a saga as the second. Every time the action is fired, the saga will run.
So the next logical step is to build our
loginRequest saga. Inside of
auth/sagas create a new file,
loginRequest.js and paste in the following code.
Let’s quickly break down what’s going on here.
- We import the
puthelpers from redux-saga as well as
shortidfor generating the conference alias if we aren’t provided one by our login action.
- We then import
fetchfrom our utils directory.
- We also import our
LOGINaction type so we can fire a success action or error action later in the saga.
- Next, we define our saga and destructure its first and only argument, which will always be the action creator we fired (
loginRequest), meaning we can pull the values off of the action and use them in our saga, in this case, the optional conference alias and the form data.
- We then open a try/catch block and do some simple formatting on the username, to make sure it is all lowercase and contains no spaces (we’re replacing them with ‘_’ using regex) before running our fetch call.
- We use the
callhelper and pass in fetch as the first argument. All following arguments will be passed through to fetch itself.
- Axios (and therefore our fetch util) returns the response body from the server as the
datakey, so we also destructure that to pull out the token and user object from the response.
- Next, we store these values in localStorage so we can persist the user across sessions (but more on this later when we get to our reducer) and then check if a
conferenceAliaswas present in the action data. If it wasn’t, we generate one using
- And finally, we use the
allhelper to run two saga helpers in parallel. The first is
put, which will fire our success action for the reducer to consume and the second is
callin which we pass
history.pushto navigate the user to the conference screen.
- We also use
putin the catch block to fire the
LOGIN.ERRORaction and pass the message to the reducer.
Now that our login saga is ready to rock, we need to make sure we are actually running the saga so that it can listen for the
LOGIN.REQUEST action. We will do this by using another saga helper (
fork) in the root saga that we created when we initialized our Redux store.
data/rootSaga.js in your editor and amend it to look like the following snippet:
Next, create a
reducer.js file inside of
data/auth and add the following code to it:
The reducer will handle the
LOGIN.SUCCESS action and save the users token and profile data to the Redux store. We’re passing in some initial state with the defaults set to pull the values from
localStorage – if they aren’t present they will just return null and therefore leave the app in an unauthenticated state ready for login.
We can now use this data in our components to create a wrapper around the Conference screen route to protect it from unauthenticated users. In your
src/containers directory, create a new file called
AuthedRoute.js and past in the following snippet:
The above component handles protecting our route from unauthenticated users as well as showing a loading screen if the auth request is in progress. It will also pass the
conferenceAlias parameter to
location.state so that if a user tries to access a conference without logging in, it will store the id so we can redirect them by checking for the value in the location state and passing it through to the action creator that we set up previously.
For the LoadingScreen add the following file to
We are also using
reselect here to pull values out of the Redux store.
reselect memoizes the values from our store to reduce accidental or unnecessary re-renders of our components if the values haven’t changed. But, we haven’t defined these selectors yet. Back in
data/auth create a new file called
selectors.js and add the following:
Now to implement our
AuthedRoute, jump back into your
App.js file, import it and update the route definition for the conference screen to use
AuthedRoute instead of React Routers
Finally, let’s go back into our
screens/Login/Login.js file and make the following changes:
We added some getter functions for formatting the user\'s name and retrieving the
conferenceAlias from the location state. We also connected our component to the Redux store so that we can access our store as well as mapping our
loginRequest function to props so we can fire it in the
Lastly, we added a new method to our class,
renderWelcome which renders a slightly different UI that shows the uses avatar and a “Start Video Call” button to launch a call. We conditionally call either that or
renderLogin in our render function depending on whether the user is authenticated or not.
Now that our backend is in place and our Login screen is fully operational, we can finish up our app by building our chat implementation with Stream! 🎉
Step 5: Stream Chat
If you have clicked the chat button in our call UI we built earlier, you will have no doubt noticed that opening the drawer will move the videos over to the left, but the drawer itself will not be visible. So now is as good a time as any to start integrating Stream Chat and building the custom drawer that we can pass into our
First up, you will need to add the following
Portal util, built using Reacts internal Portal implementation. If you haven’t used Portals before, they essentially allow you to define a component anywhere in the tree but have it rendered to the DOM anywhere outside of its current parent hierarchy. We will use the Portal to render a chat drawer inside of the body – above the rest of the app.
We will also need a couple more action creators to handle toggling our drawer from outside of our
ActionsButtons component and setting the unread count in Redux. Create a new directory called
chat inside of your
src/data directory and place the following three files inside that contain our Action Types, Action Creators and Selectors for pulling the values out of Redux.
Last, we will need to create our
chat reducer and import it into our root reducer also. Below is the code for the reducer, followed by a snippet showing the root reducer.
Now, on to our chat drawer!
screens/Conference directory, let\'s make a new directory called
containers and create a new directory inside of that called
AttendeesChat. Inside, we’ll create
AttendeesChat.js and place the following code inside:
There is quite a lot going on here – so here is a breakdown.
First, in our constructor, we define our default state and also set
this.chatClient to contain our
StreamChat instance and pass it the
.env variables we set at the beginning of the tutorial. Then, in
componentDidMount we call
this.chatClient.setUser and pass in an object representing our
user which we are pulling into props using our Redux selectors.
We then initialize the chat channel using
this.chatClient.channel() and pass in the “messaging” config parameter, along with the
conferenceAlias from our URL parameter so that we can scope the chat channel to the current conference and finally, call
this.setState to store our channel in our components state for later.
componentDidUpdate we run a comparison check that will run when our channel is first initialized, by comparing the previous state value to the current one and checking our channel exists. If the condition is truthy, we run
this.init() and in turn, calls
await this.state.channel.watch() which subscribes the channel to future updates.
We also initialize a listener here, that fires on the
message.new event. Every time a new message is sent in the channel, the callback will run and updates our unread count using our
setUnreadCount action creator.
Next, in our render function, we render the
stream-chat-react components that will power our UI and handle a lot of the state management for us.
We wrap everything with the
Chat component and pass in
this.chatClient to the client prop. Inside of this, we render the
Channel component to which we pass our
this.state, followed by
Window which will wrap the visual aspects of our Chat UI. Then, Inside of our
Window we render the
Back inside of
ChatHeader.js and add the following snippet:
And that’s everything we need for a fully operational chat UI!
But we aren’t finished just yet...
We need to include the CSS file from
stream-chat-react. The default file exported from the library is, of course, responsive. However, this presents some issues due to the fact we are rendering the Chat components inside of a 375px wide wrapper div.
Because CSS media queries react to the size of the window and not the size of the parent div, properties such as
max-width will be incorrect – using the values it would for the full viewport regardless of the container size. To fix this, we’ll strip out the media queries so that we only use the mobile-sized definitions.
Place the above file in your
public directory, then in
public/index.html you can import the file like so:
We also need to jump back into our
ActionsButtons.js component we made for our custom call UI along the bottom of the screen and connect it to our redux store so we can retrieve the unread count and show the notification badge accordingly:
If you’re interested in adding chat to your application, the Stream Chat SDKs has you covered – with React Components (that we used in this tutorial), as well as SDKs and libraries for JS, iOS SDK, Android SDK, and most other popular programming languages.
Happy coding! ✌️