Build Your Own Video Chat with Vue, WebRTC, SocketIO, Node & Redis
source link: https://www.tuicool.com/articles/JzuuIrr
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Building a multi-room video chat with horizontal scaling
Aug 1 ·12min read
Nowadays, there are plenty of free applications out there in the market providing chat and video conference functionality. In almost one click we all are able to communicate with anyone in any part of the world but, why don’t we try to build our own app to make it even more real? Let’s do it!
The main features of our app will be:
- Several available rooms and the possibility to jump across them
- User status (online/absent/unavailable)
- Be able to open a private chat with anyone else in the same room (just one private chat at the same time) and get notified whether that user is already talking with someone else or has closed the chat
- Be able to start a video conference within the same private chat
Private chat and video example
To build it we’ll use the following tech:
- Vue.js as a frontend framework. Vue is becoming more popular lately, so it’s a good chance to catch up with it. We’ll also use Vuex to store the user and chat information
- The backend will be implemented in NodeJS with Express
- Socket.io and WebRTC for the real-time engine and communications
- Redis as a database in memory
- Docker & Docker Compose
Talking about that tech (and Sockets and WebRTC specially) we could go as far as we wanted, diving deeper in more complex aspects, but that’s not the purpose. We want to build something simple to understand the basic points of each part but fully working though, so let’s get started.
App skeleton
First of all, let’s install and use @vue/cli
to easily create the main skeleton:
npm install -g @vue/cli vue create video-chat
After that, you’ll be prompted to pick a preset. In our case we manually select Babel , Router , Vuex , CSS Pre-processor and Linter support
To speed up the process we’ll use vue-material
as a style framework (it’s still on beta version even though they claim the API will not change)
npm install vue-material --save
Regarding HTTP and WebSocket communications, we’ll use vue-resource
and vue-socket.io
as custom implementations for Vue.js
npm install vue-resource
vue-socket.io
--save
Once all installed, we can configure them in our main.js file:
With actionPrefix and mutationPrefix in the vuex configuration we could trigger server side vuex actions and mutations respectively. In our example we will not use them as we’ll dispatch the actions in the client after listening the socket server events
Regarding the store attached to the VueSocketIO and Vue instances, we can configure them in the store.js file with the following state:
Every time the user triggers an action, we’ll dispatch it to the store generating a mutation execution and ending up with a new state.
Normally, the state management pattern is quite similar regardless the framework you choose. Check out the Vuex implementation in that case for more details
As we can see, the socket configuration is expecting a connection url, so before keeping going with the login page, let’s build the main basics of our server
Server
First of all, we need to install all the main packages to set up our server basics
npm install express http body-parser path cors socket.io --save
Secondly, within a /server
folder in the project root we create the index.js and app.js files as our main server entry points:
We define all the server configuration in the config.js file. That will help us in the future to configure several instances of our application easily
With the previous configuration we’ve mainly achieved:
- Create and configure both http and express servers
- Define the REST APIs for the login and the rooms (for both APIs will store the information in memory for simplicity)
- Create the static server that will serve all the static files of our frontend
- Create the websocket namespace and configure its server events
But what do you exactly mean with the namespace and which are those server events?
The namespace is essentially the endpoint or path of our WS connection. By default is always /
and it’s the one socket.io clients connect to by default. In our case we’ve set it up to /video-chat
That’s the reason the socket client connects to ${url}/video-chat
Regarding the events, for now we’ve just defined the basic one to join a room. Under the /chat_namespace
folder, we create the index.js file:
In the connection callback we are listening for the joinRoom
event. Once it gets triggered, we join the room, we add the user into that room and we emit back all the user within that room through the newUser
event. So our front will emit joinRoom event and it will listen for the newUser one
You can check out all the available server events in the socket.io emit cheatsheet
At this point we’d be ready to start building our frontend
Frontend
We’ll have two main pages: the login and the main chat page.
We won’t use any authentication mechanism, so for the first one we just need the user and the room to join. The user will work as a primary key in the system, so the username has to be unique. Besides the main rooms, the idea is to use the username as the room value for private conversations
If we wanted to allow more than one private chat at the same time, we could create for instance like a unique constrain name with the two usernames involved in the private conversation
Within a new /views
folder we create the file Home.vue as follows:
Now we just need to fetch the rooms and submit the user information:
Within the create d lifecycle method we fetch the rooms and save them in our store. Same when submitting the form, as soon as the user sends the right information, we save the room and navigate to the main chat page.
We change the state dispatching events to the store along with the appropriate payload this.$store.dispatch(<name>, payload)
For the main chat page, and to get some help with the structure, we’ll mainly use the material app component with the following parts:
General public room
- Change room select
- Header (room name and logout button)
- Users list area (with their status)
- Messages area
- Text area to send messages
For the private chat, we’ll use the material dialog component.
Even though we use material components, we’ll still need some tuning :)
For that, I’ve used style encapsulation in all the child components but the two parent pages (login and chat). For those, I use a global scope due to their self-reliance character and for simplicity when overwriting some material styles. You can checkout here an explanation of scoped CSS in Vue.js
At this point we can distinguish the following events:
- joinRoom : to join a main room (just explained above)
- publicMessage : when the user sends a message. The server emits back a newMessage event with the message to all the users within the same room
- leaveRoom : when the user changes the room. The server leaves the room and sends back the new users list for the room left. After that, the client will join the new room following the joinRoom event
- leaveChat : when the user logs out. The server emits the new users list through the leaveChat event and leaves the socket room
- changeStatus : when the user changes the status. The server updates it and send back the new values using the same newUser event than before
- joinPrivateRoom : when the user (A) open a private chat with someone else (B). The server joins the room and emits back a privateChat event to notify the other user (B). If the final user (B) is already talking, the server notifies the user (A) and force him to leave the private room with the leavePrivateRoom event
- leavePrivateRoom : when the user closes a private chat. The server emits back the same event to notify the other user
- privateMessage : when the user sends a private message. The server emits back the message with the privateMessage event to both users
Now, under the same /views
folder we create the Chat.vue file with the major components for our main chat page:
That’s our parent component and it will be the responsible to listen to all the sockets events emitted by the server. For that, we just need to create a socket object within our Vue component and create a listener method per server event :
In that example we emit the publicMessage event when the user sends a public message and we listen to the newMessage server event to get the message back.
Bear in mind that all the users will run the same client code, so we need to build a generic way to handle all the logic in any case
Going through all the details for each component would take too long, so we’ll explain the main functionality. They just basically get input data after the socket listeners get triggered and emit events to the parent under user actions:
- <MessageArea> Text area to send messages. It emits an event to the parent every time the user sends a public message, what emits a publicMessage socket event to the server
- <ChatArea> It displays all the public messages within a room using a directive to format the message based on the user:
- <UserList> It displays the current user status and the list of users with their status. When the user wants to open a private chat, it emits an event to the parent to open the private chat modal and emits the joinPrivateRoom socket event. When the user changes the status, it updates the state and emits the changeStatus socket event.
- <ChatDialog> Private chat. For each message it emits a privateMessage socket event. When closing the conversation it emits an event to the parent what emits the leavePrivateRoom socket event. It also contains the <VideoArea> component with all the video functionality
After that, it’s time to add the new listeners to our previous server file index.js within the /chat_namespace
folder:
As we are getting more events, we‘ve changed slightly that file and created a new events.js file under the same /chat_namespace
folder with all the callback functions:
Hold on, but what would it happen if we had more than one instance running our server?
During the whole process we’ve stored all the information in memory. That approach would work for simple cases, but as soon as we needed to scale, it just would not work properly because each server instance would have its own copy of the users. And not only that, the users might be connected to different instances, so there would not be a way to communicate both socket connections.
Horizontal scaling with 2 instances
Adding Redis
Implementing the Redis adaptor in our server solves the problem.
npm install socket.io-redis redis --save
In the server entry point index.js we add the following lines:
Besides, we’ll use Redis as a database to store all the users connected. For that we create an index.js file within a new /redis
folder in our server:
In our case we’ve implemented the hash pattern to store the data as an example, but there are more data-types and abstractions that we could’ve used based on the search requirements. Besides, there are also some node.js
redis clients that provide extra functionality with a layer of abstraction
After that we would just need to update all the references to the users in memory and change them to use our redis implementation.
And what about the video? Did you forget it?
WebRTC
WebRTC is a free and open project that provides web and mobile applications with Real-Time Communications (RTC) capabilities via simple APIs.
JSEP (Javascript Session Establishment Protocol) architecture
WebRTC enables peer to peer communications even tough it still needs a server for the signaling process. Signaling is the process of coordinating communication between the two clients to exchange certain metadata needed to establish the communication (session control and error messages, media metadata, etc). However, WebRTC does not specify any method and protocol for that, so it’s up to the application to implement the appropriate mechanism.
In our case we’ll use the private room as the signaling mechanism between the two users
To secure a WebRTC app in production is mandatory to use TLS (Transport Layer Security) for the signaling mechanism
For that, we’ll add a new server listener to our server configuration:
The mechanism to establish the communication between A (caller) and B (callee) would be the following:
- A creates a RTCPeerConnection object with the ICE servers configuration
- A creates an offer (SDP session description) with the RTCPeerConnection createOffer method
- A calls setLocalDescription(offer) method with the offer
- A uses the signaling mechanism ( privateMessagePCSignaling ) to send the offer to B
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK