• WebRTC and Node.js: Development of a Real-Time Video Chat App
  • Mikołaj Wargowski
  • The Nuggets translation Project
  • Permanent link to this article: github.com/xitu/gold-m…
  • Translator: 👊 Badd
  • Proofreader: RubyJy, CyZ980908

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

(Real-time) Time is money, so I’ll get straight to the point. In this article, I will take you through writing a video chat application that supports video and voice communication between two users. Nothing difficult, nothing fancy, but JavaScript — technically WebRTC andNode.js– the perfect test.

What is a WebRTC?

Web Real-Time Communication (abbreviated WebRTC) is an HTML5 specification that enables you to communicate in Real Time directly from your browser, 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 devices — you can call microphones, cameras, and even shared screens from WebRTC, all in real time! Therefore, WebRTC takes the simplest approach

Make web voice and video chat possible.

WebRTC JavaScript API

WebRTC is a complex topic, with a lot of technology involved. The connection, communication, and data transfer are done through a series of JavaScript apis. The main apis are:

  • RTCPeerConnection — Create and navigate point-to-point connections,
  • RTCSessionDescription — Describes (potential) connection endpoints and their configuration,
  • Navigator. GetUserMedia — Get audio and video.

Why node.js?

To establish remote connections between two or more devices, you need a server. In this case, you need a server that can handle real-time communications. As you know, Node.js supports real-time extensible applications. To develop a two-way connection application that can freely exchange data, you might use WebSocket, which opens a communication session between the client and server. Requests made by the client are processed in a loop — strictly an event loop — which makes Node.js a good choice because it uses a “non-blocking” approach to handle requests, resulting in low latency and high throughput.

Read more: New Node.js features will disrupt AI, the Internet of Things, and many more amazing fields

What are we going to build?

We’re going to make a very simple app that pushes audio and video streams to connected devices — a basic video chat app. We will use:

  • Express library to provide static files such as USER interface HTML files,
  • Socket. IO library, using WebSocket to establish a connection between two devices,
  • WebRTC enables media devices (cameras and microphones) to push audio and video streams between connected devices.

Implementing video chat

The first step is to have an HTML file that will be used as the user interface for your application. Initialize a new Node.js project with NPM init. Then, run NPM i-d typescript ts-node nodemon@types/Express@types /socket. IO to install some development dependencies, Run NPM I Express socket. IO to install production dependencies.

Now we can write a script in package.json 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." "}}Copy the code

After we run NPM run dev, Nodemon will listen for changes to every.ts file in the SRC folder. Now create a SRC folder. Inside SRC, create two TypeScript files: index.ts and server.ts.

In server.ts, we create a server class and make it work with 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, (a)= >
     callback(this.DEFAULT_PORT) ); }}Copy the code

We need to create a new instance of the Server class in the index.ts file and call listen to start the Server:

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

Now run NPM run dev and we should see:

Open your browser and visit http://localhost:5000 and we will see the words “Hello World” :

Now we will create a new HTML file public/index.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, 400500700 & 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>
Copy the code

In this file, we declare two video elements: one to render the remote video connection and one to render the local video. As you may have noticed, we’ve also introduced local script files, so let’s create a new folder — name it scripts and create the index.js file in it. For style files, you can download them from the GitHub repository.

Now it’s time to pass the index.html from the server to the browser. First you tell Express which static file you want to return. This requires us to implement a new method in the Server class:

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

Don’t forget to call the configureApp method in the Initialize method:

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

At this point, when you open http://localhost:5000, you should see the index.html file up and running:

The next step is to access the camera and microphone and have the media stream displayed in the local-video element. 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); });Copy the code

Back in the browser, you’ll see a request to access the media device, authorize the request, and you’ll see your camera wake up!

Read More: A quick guide to Node.js concurrency and some pitfalls

How do I handle socket connections?

Now we’ll focus on how to handle socket connections — we need to connect to the client and server, so we use socket.io. Add to public/scripts/index.js:

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] }); }}Copy the code

When you refresh the page, a message “Socket Connected” is displayed on the terminal.

server.ts
Server

private activeSockets: string[] = [];
Copy the code

Check whether a socket already exists when connecting to the socket. If not, add a new socket to memory and send 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] }); }}Copy the code

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

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

On the client side (i.e., public/scripts/index.js), you need to perform the 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(); }});Copy the code

Here’s the updateUserList function:

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

There is also the createUserItemContainer function:

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;
}
Copy the code

Notice that we’ve added a click event listener on the user container element, which calls the callUser function — for now, you can write the null function first. Now, when you run two browser Windows (one as the local user window), you will see that there are two sockets in the application connection:

After clicking the online user in the list, the callUser function is called. But before you can implement this function, you need to declare two classes in the Window object.

const { RTCPeerConnection, RTCSessionDescription } = window;
Copy the code

We’ll use them in the callUser function:

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

Here, we create a local connection request and send it to the selected user. The server listens for an event called call-user that intercepts local connection requests and sends them to the selected user. In server.ts we need to implement this:

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

Now on the client side, we need to respond to the Call-made event:

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
 });
});
Copy the code

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

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

Accordingly, the answer-made event is handled on the client side:

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

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

Finally, you simply add local records — audio and video — to the connection so that you can share audio and video with connected users. This requires us to call addTrack with the peerConnection object in the navigator. GetMediaDevice callback.

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); });Copy the code

And add the corresponding handler for the ontrack event:

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

As you can see, we took the media stream from the incoming object and overwrote srcObject in remote-Video to use the received media stream. So now, when you click on an online user, you can create an audio and video connection like this:

Read More: Node.js and Dependency Injection — Friend or foe?

Now you have the ability to develop video chat apps.

WebRTC is a huge topic — especially if you want to understand the underlying principles. Fortunately, we have an easy-to-use JavaScript API that allows us to make really neat applications like video chat apps!

If you want to learn more about WebRTC, see the official WebRTC documentation. I recommend reading the MDN documentation.

If you find any mistakes in your translation or other areas that need to be improved, you are welcome to the Nuggets Translation Program to revise and PR your translation, and you can also get the corresponding reward points. The permanent link to this article at the beginning of this article is the MarkDown link to this article on GitHub.


The Nuggets Translation Project is a community that translates quality Internet technical articles from English sharing articles on nuggets. The content covers Android, iOS, front-end, back-end, blockchain, products, design, artificial intelligence and other fields. If you want to see more high-quality translation, please continue to pay attention to the Translation plan of Digging Gold, the official Weibo, Zhihu column.