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:

Server index.js file. Entry point of our server

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:

joinRoom server listener

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 created 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)

: 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

: when the user sends a message. The server emits back a 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

: 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

: when the user logs out. The server emits the new users list through the 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

: when the user changes the status. The server updates it and send back the new values using the same 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

: when the user (A) open a private chat with someone else (B). The server joins the room and emits back a 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 event leavePrivateRoom : when the user closes a private chat. The server emits back the same event to notify the other user

: 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:

Main chat components

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:

Join a main room. Example with new message emitter and listener

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

Text area to send messages. It emits an event to the parent every time the user sends a public message, what emits a 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:

Directive to display the public messages

<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.

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 socket event. When the user changes the status, it updates the state and emits the 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:

New events listener

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:

Callback function implementations for all the listeners

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:

Adding redis adator for socket.io

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:

Redis as a DB. Hash pattern

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: