[Translation] WebRTC and Node.js: Building a real-time video chat application

[Translation] WebRTC and Node.js: Building a real-time video chat application

WebRTC and Node.js: Create a real-time video chat application

(Real-time) Time is money, then I'm straight to the point. In this article, I will take you to write a video chat application that supports video and voice communication between two users. No difficulty, nothing fancy, but a great trial of JavaScript - strictly speaking, WebRTC and Node.js.

What is WebRTC?

Web Real-Time Communication (WebRTC, abbreviated as WebRTC) is an HTML5 specification, which enables you to directly use the browser for real-time communication without relying on third-party plug-ins . WebRTC has many uses (even file sharing), but its main application is real-time point-to-point audio and video communication, which is the focus of this article.

The power of WebRTC is that it allows access to the device-you can call the microphone, camera, and even share the screen through WebRTC, all in real time! Therefore, WebRTC uses the simplest way

Make web voice and video chat possible.

WebRTC JavaScript API

WebRTC is a complex topic, which involves many technologies. The establishment of connection, communication, and data transmission are through a series of JavaScript APIs. The main APIs are:

  • RTCPeerConnection -create and navigate a point-to-point connection,
  • RTCSessionDescription -description of the (potential) connection endpoint and its configuration,
  • navigator.getUserMedia  -Get audio and video.

Why use Node.js?

If you want to establish a remote connection between two or more devices, you need a server. In this case, what you need is a server that can control real-time communication. You know that Node.js supports real-time scalable applications. To develop a two-way connection application that can freely exchange data, you may use WebSocket, which can open a communication session between the client and the server. The request sent by the client is processed into a loop-strictly speaking an event loop, which makes Node.js a good choice because it uses a "non-blocking" method to process requests, which can achieve low latency and High throughput.

Extended reading:  New features of Node.js will subvert more amazing fields such as AI and Internet of Things

Demonstration of ideas: What do we want to do?

We are going to make a very simple application that can push audio and video streams to connected devices-a basic video chat application. We will use:

  • Express library to provide static files such as user interface HTML files,
  • The socket.io library uses WebSocket to establish a connection between two devices,
  • WebRTC enables media devices (cameras and microphones) to push audio and video streams between connected devices.

Implement video chat

In the first step, we need to have an HTML file to be used as the user interface of the application. By npm initinitializing a new Node.js project. Then, run npm i -D typescript ts-node nodemon @types/express @types/socket.ioto install some dependencies develop, run npm i express socket.ioto install the production dependencies.

Now, we can package.jsonwrite a script file to run the project:

{
 "scripts": {
   "start": "ts-node src/index.ts",
   "dev": "nodemon --watch 'src/**/*.ts' --exec 'ts-node' src/index.ts"
 },
 "devDependencies": {
   "@types/express": "^4.17.2",
   "@types/socket.io": "^2.1.4",
   "nodemon": "^1.19.4",
   "ts-node": "^8.4.1",
   "typescript": "^3.7.2"
 },
 "dependencies": {
   "express": "^4.17.1",
   "socket.io": "^2.3.0"
 }
}
 

We run the npm run devcommand, Nodemon listens src folder for each .tschange file suffix. Now let's create a src folder. In src, create two TypeScript files: index.tsand server.ts.

In server.ts, we will create a Server class, and make it fit Express and socket.io:

import express, { Application } from "express";
import socketIO, { Server as SocketIOServer } from "socket.io";
import { createServer, Server as HTTPServer } from "http";
 
export class Server {
 private httpServer: HTTPServer;
 private app: Application;
 private io: SocketIOServer;
 
 private readonly DEFAULT_PORT = 5000;
 
 constructor() {
   this.initialize();
 
   this.handleRoutes();
   this.handleSocketConnection();
 }
 
 private initialize(): void {
   this.app = express();
   this.httpServer = createServer(this.app);
   this.io = socketIO(this.httpServer);
 }
 
 private handleRoutes(): void {
   this.app.get("/", (req, res) => {
     res.send(`<h1>Hello World</h1>`); 
   });
 }
 
 private handleSocketConnection(): void {
   this.io.on("connection", socket => {
     console.log("Socket connected.");
   });
 }
 
 public listen(callback: (port: number) => void): void {
   this.httpServer.listen(this.DEFAULT_PORT, () =>
     callback(this.DEFAULT_PORT)
   );
 }
}
 

We need to index.tscreate a new file in an Serverinstance of the class and call the listenmethod, so that we can start the server:

import { Server } from "./server";
 
const server = new Server();
 
server.listen(port => {
 console.log(`Server is listening on http://localhost:${port}`);
});
 

Now running npm run dev, we will see:

Open the browser and visit http://localhost:5000 , we will see the words "Hello World":

Now, we want to create a new HTML file public/index.html:

<!DOCTYPE html>
<html lang="en">
 <head>
   <meta charset="UTF-8"/>
   <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
   <meta http-equiv="X-UA-Compatible" content="ie=edge"/>
   <title>Dogeller</title>
   <link
     href="https://fonts.googleapis.com/css?family=Montserrat:300,400,500,700&display=swap"
     rel="stylesheet"
  />
   <link rel="stylesheet" href="./styles.css"/>
   <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.3.0/socket.io.js"></script>
 </head>
 <body>
   <div class="container">
     <header class="header">
       <div class="logo-container">
         <img src="./img/doge.png" alt="doge logo" class="logo-img"/>
         <h1 class="logo-text">
           Doge<span class="logo-highlight">ller</span>
         </h1>
       </div>
     </header>
     <div class="content-container">
       <div class="active-users-panel" id="active-user-container">
         <h3 class="panel-title">Active Users:</h3>
       </div>
       <div class="video-chat-container">
         <h2 class="talk-info" id="talking-with-info"> 
           Select active user on the left menu.
         </h2>
         <div class="video-container">
           <video autoplay class="remote-video" id="remote-video"></video>
           <video autoplay muted class="local-video" id="local-video"></video>
         </div>
       </div>
     </div>
   </div>
   <script src="./scripts/index.js"></script>
 </body>
</html>
 

In this file, we declare two video elements: one is used to present remote video connection, and the other is used to present local video. You may have noticed, we have introduced a local script file, so let's create a new folder - named scriptsand in which to create the index.jsfile. As for the style file, you can download it from the GitHub repository .

Now is the time to index.htmlpass the browser from the server. First you have to tell Express which static file you want to return. This requires us to Serverimplement a new method in the class:

private configureApp(): void {
   this.app.use(express.static(path.join(__dirname, "../public")));
 }
 

Do not forget to initializecall the methods configureAppmethod:

private initialize(): void {
   this.app = express();
   this.httpServer = createServer(this.app);
   this.io = socketIO(this.httpServer);
 
   this.configureApp();
   this.handleSocketConnection();
 }
 

At this point, when you open HTTP://localhost: 5000 , you will see the index.htmlfile already up and running:

Next step is to access the camera and microphone, and let the media stream in the show local-videoelement of. Open the  public/scripts/index.js file and add the following code:

navigator.getUserMedia(
 { video: true, audio: true },
 stream => {
   const localVideo = document.getElementById("local-video");
   if (localVideo) {
     localVideo.srcObject = stream;
   }
 },
 error => {
   console.warn(error.message);
 }
);
 

Go back to the browser and you will see a prompt box requesting access to the media device. After authorizing this request, you will see that your camera is woken up!

Extended reading: A simple guide: Node.js concurrency and some pitfalls

How to handle socket connections?

Now we will focus on how to handle socket connections-we need to connect the client and server, so socket.io is used. In public/scripts/index.jsadding:

this.io.on("connection", socket => {
     const existingSocket = this.activeSockets.find(
       existingSocket => existingSocket === socket.id
     );
 
     if (!existingSocket) {
       this.activeSockets.push(socket.id);
 
       socket.emit("update-user-list", {
         users: this.activeSockets.filter(
           existingSocket => existingSocket !== socket.id
         )
       });
 
       socket.broadcast.emit("update-user-list", {
         users: [socket.id]
       });
     }
   }
 

Refresh the page and you will see a message in the terminal: "Socket connected".

We return to this point server.tswill be stored into memory socket, it is easy to keep uniqueness of the connection. In other words, Serveradd a new private class field:

private activeSockets: string[] = [];
 

When connecting to the socket, check whether a socket already exists. If not, add a new socket to the memory and send the data to the connected user:

this.io.on("connection", socket => {
     const existingSocket = this.activeSockets.find(
       existingSocket => existingSocket === socket.id
     );
 
     if (!existingSocket) {
       this.activeSockets.push(socket.id);
 
       socket.emit("update-user-list", {
         users: this.activeSockets.filter(
           existingSocket => existingSocket !== socket.id
         )
       });
 
       socket.broadcast.emit("update-user-list", {
         users: [socket.id]
       });
     }
   }
 

It also needs to respond when the socket is disconnected, so add in the socket:

socket.on("disconnect", () => {
   this.activeSockets = this.activeSockets.filter(
     existingSocket => existingSocket !== socket.id
   );
   socket.broadcast.emit("remove-user", {
     socketId: socket.id
   });
 });
 

On the client side (that is  public/scripts/index.js), you need to perform corresponding operations on these messages:

socket.on("update-user-list", ({ users }) => {
 updateUserList(users);
});
 
socket.on("remove-user", ({ socketId }) => {
 const elToRemove = document.getElementById(socketId);
 
 if (elToRemove) {
   elToRemove.remove();
 }
});
 

This is the updateUserListfunction:

function updateUserList(socketIds) {
 const activeUserContainer = document.getElementById("active-user-container");
 
 socketIds.forEach(socketId => {
   const alreadyExistingUser = document.getElementById(socketId);
   if (!alreadyExistingUser) {
     const userContainerEl = createUserItemContainer(socketId);
     activeUserContainer.appendChild(userContainerEl);
   }
 });
}
 

There are createUserItemContainerfunctions:

function createUserItemContainer(socketId) {
 const userContainerEl = document.createElement("div");
 
 const usernameEl = document.createElement("p");
 
 userContainerEl.setAttribute("class", "active-user");
 userContainerEl.setAttribute("id", socketId);
 usernameEl.setAttribute("class", "username");
 usernameEl.innerHTML = `Socket: ${socketId}`;
 
 userContainerEl.appendChild(usernameEl);
 
 userContainerEl.addEventListener("click", () => {
   unselectUsersFromList();
   userContainerEl.setAttribute("class", "active-user active-user--selected");
   const talkingWithInfo = document.getElementById("talking-with-info");
   talkingWithInfo.innerHTML = `Talking with: "Socket: ${socketId}"`;
   callUser(socketId);
 }); 
 return userContainerEl;
}
 

Please note that we have added a click event listener, click on the user calls the container element callUserfunction - now, you can write to empty function. Now, when you run two browser windows (one of them as a local user window), you will find that there are two connected sockets in the application:

Click the list of online users, to call callUserthe function. However, before the implementation of the function, you need to windowdeclare two class object.

const { RTCPeerConnection, RTCSessionDescription } = window;
 

We will callUseruse them function:

async function callUser(socketId) {
 const offer = await peerConnection.createOffer();
 await peerConnection.setLocalDescription(new RTCSessionDescription(offer));
 
 socket.emit("call-user", {
   offer,
   to: socketId
 });
}
 

Here, we create a local connection request and send it to the selected user. The server listens on a named call-userevent, intercepting connection requests issued locally and sent to the selected user. In server.tsneed of such implementations:

socket.on("call-user", data => {
   socket.to(data.to).emit("call-made", {
     offer: data.offer,
     socket: socket.id
   });
 });
 

Now the client side, we need to call-maderespond to events:

socket.on("call-made", async data => {
 await peerConnection.setRemoteDescription(
   new RTCSessionDescription(data.offer)
 );
 const answer = await peerConnection.createAnswer();
 await peerConnection.setLocalDescription(new RTCSessionDescription(answer));
 
 socket.emit("make-answer", {
   answer,
   to: data.socket
 });
});
 

Then, set a remote description for the connection request received from the server, and create a response to the request. On the server side, you need to send the corresponding data to the selected user. In the server.tsmiddle, add an event listener:

socket.on("make-answer", data => {
   socket.to(data.to).emit("answer-made", {
     socket: socket.id,
     answer: data.answer
   });
 });
 

Accordingly, the client processing answer-madeevents:

socket.on("answer-made", async data => {
 await peerConnection.setRemoteDescription(
   new RTCSessionDescription(data.answer)
 );
 
 if (!isAlreadyCalling) {
   callUser(data.socket);
   isAlreadyCalling = true;
 }
});
 

We use a very useful flag - isAlreadyCalling - to ensure that the user is only called once.

Finally, just add local recordings-audio and video-to the connection so that audio and video can be shared with connected users. Then we need the  navigator.getMediaDevicecallback function, with the peerConnectionobject calls  addTrackthe function.

navigator.getUserMedia(
 { video: true, audio: true },
 stream => {
   const localVideo = document.getElementById("local-video");
   if (localVideo) {
     localVideo.srcObject = stream;
   }
 
   stream.getTracks().forEach(track => peerConnection.addTrack(track, stream));
 },
 error => {
   console.warn(error.message);
 }
);
 

As well as the ontrackprocessing function to add the corresponding event:

peerConnection.ontrack = function({ streams: [stream] }) {
 const remoteVideo = document.getElementById("remote-video");
 if (remoteVideo) {
   remoteVideo.srcObject = stream;
 }
};
 

As you can see, we have obtained from the incoming object to the media stream, and rewritten remote-videoin srcObjectorder to use the received media stream. So, now when you click on an online user, you can establish an audio and video connection, as follows:

Extended reading: Node.js and dependency injection-is it friend or foe?

Now you have brightened up your skills in developing video chat applications!

WebRTC is a huge topic-especially if you want to know the underlying principles. Fortunately, we have a simple and easy-to-use JavaScript API that allows us to make very simple applications such as video chat applications!

If you want to learn more about WebRTC, please see the official document of WebRTC . Personally recommend reading the MDN documentation .

If you find there is a translation error or other areas for improvement, welcome to Denver translation program to be modified and translations PR, also obtained the corresponding bonus points. The beginning of the article Permalink article is the MarkDown the links in this article on GitHub.


Nuggets Translation Project is a high-quality translation of technical articles Internet community, Source for the Nuggets English Share article on. Content covering Android , iOS , front-end , back-end , block chain , product , design , artificial intelligence field, etc., you want to see more high-quality translations, please continue to focus Nuggets translation program , the official micro-blog , we know almost columns .