Did you know? All Video & Audio API plans include a $100 free usage credit each month so you can build and test risk-free. View Plans ->

WebRTC For The Brave

Introduction to Signaling Servers

In this lesson, you'll learn how the signaling server works and configure to build your signaling server to resolve and establish connections between peers to connect them while minimizing the exposure of private information.

Overview

In the previous lessons, we delved into the fundamental concepts required to comprehend WebRTC, including SDP messages, ICE candidates, and peer connections. Now, in this lesson, we will focus on creating a signaling server using Socket.IO and Node.js to facilitate the WebRTC communication process.

What’s the Signaling Server?

In Lesson 2, we covered the process of establishing WebRTC calls between multiple peers on a local device. However, in real-world scenarios, the complexity of connecting multiple peers is significantly heightened.

Peers face challenges in determining where to send and receive stream data, especially considering various barriers like firewalls that can obstruct communication paths. These complexities make implementing WebRTC communication beyond the local environment a more intricate task. So we can resolve this technical problems by using the signaling server.

The signaling server plays a crucial role in resolving and establishing connections between peers, enabling them to connect with each other while minimizing the exposure of private information, as depicted in the figure below:

Indeed, building a signaling server and designing signaling protocols are essential steps to exchange connectivity information between peers. However, unlike traditional server-based communication, after the initial exchange of connectivity information, peers can communicate directly with each other in real-time, bypassing the need to involve a server.

This peer-to-peer communication approach offers significant advantages, such as reduced latency and lower costs compared to server-based communication methods. By leveraging this direct communication, you can achieve faster and more cost-effective data transmission.

Setting Up Prerequisites

Before building your own signaling server, you need to set up prerequisites below to build your signaling server:

1. Setting Up a Node.js On macOS

To set up a Node.js environment on macOS, you'll need to install two main components: Node.js and npm (Node Package Manager). Node.js is a runtime environment that allows you to run JavaScript code outside the browser, while npm serves as a package manager specifically designed for Node.js. You can download Node.js on the website or easily install using Homebrew with the command line below:

bash
            brew install node
        

2. Installing Socket.io

Next, you should install Socket.IO with an npm command line below:

bash
            npm install socket.io
        

If you installed socket.io successfully, you will see the message below on your terminal and be ready to build your own socket server.

Now, let’s build your own signaling server.

Build Your Own Signaling Server

As elucidated in the preceding sections, it's important to note that even with peer-to-peer connections, a signaling server remains imperative for orchestrating communication, facilitating destination exchange, and transmitting control messages. It's essential to understand that the signaling process is not a part of the RTCPeerConnection API, so we should build our own signaling server.

1. Create an HTTTP server and Socket

First and foremost, your initial step involves crafting an HTTP server using Express.
Express is a popular web application framework for Node.js, designed to facilitate the development of web applications and APIs. It's known for being minimal, flexible, and providing a robust set of features for both web and mobile applications.

You can effortlessly establish an HTTP server that awaits incoming requests on port 3000 using the listen() function, as demonstrated in the example below:

jsx
            const express = require('express');
const app = express();
app.use(cors());
app.get('/', (req, res) => {
    res.send('hello, word!');
})

const server = app.listen(3000, () => {
    console.log('server is running on http://localhost:3000')
})
        

Next, your task involves the creation of a socket.io instance, which you will seamlessly integrate with the previously established HTTP server.

jsx
            const express = require('express');
const socketIo = require('socket.io')

const io = socketIo(server);
io.on("connection", socket => {
  ..
}
        

Now, you’re able to communicate with clients with this io , such as managing connection and disconnection of clients, and exchanging SDP messages.

2. Manage a New User Joining the Room

Now’ let’s implement the handling of joining requests from clients. We’re going to use an “room” approach, which contains user information and allows you to manage the connection and disconnection of users. Each distinct room is identified by its dedicated room id, ensuring clear differentiation among multiple rooms.

jsx
            let rooms = {};
let socketToRoom = {};

io.on("connection", socket => {
    socket.on("join", data => {
        // let a new user join to the room
        const roomId = data.room
        socket.join(roomId);
        socketToRoom[socket.id] = roomId;

        // persist the new user in the room
        if (rooms[roomId]) {
            rooms[roomId].push({id: socket.id, name: data.name});
        } else {
            rooms[roomId] = [{id: socket.id, name: data.name}];
        }

        // sends a list of joined users to a new user
        const users = rooms[data.room].filter(user => user.id !== socket.id);
        io.sockets.to(socket.id).emit("room_users", users);
        console.log("[joined] room:" + data.room + " name: " + data.name);
    });
});
        

We’ve created some properties below:

  • rooms: This collection comprises several distinct rooms, which contains user information independently.
  • socketToRoom: This mapping pairs a socket id with a corresponding room id. This mechanism enables us to discern the specific room associated with a disconnected client. As a result, we can effectively manage and clear user information within that room.

In the provided code snippet, the process of handling a join request from clients with data is delineated through the following three steps:

  1. The socket establishes a connection for a new user within the designated room. This can be accomplished using the socket.join method.
  2. The user's details are consistently maintained within the room.
  3. An inventory of previously joined users is dispatched to the new user, thereby furnishing them with a roster of existing room participants. This facilitates awareness about those already present in the room. You can send the existing room participants to only the new user using the io.sockets.to(socket.id).emit method.

3. Handle SDP Messages

Now it’s time to handle SDP messages. The signaling server assumes the role of orchestrating SDP messages among peers, encompassing crucial components like offer, answer, and ice candidate. You can seamlessly facilitate these interactions by devising signaling protocols, much like the exemplar below:

jsx
            io.on("connection", socket => {
    socket.on("offer", sdp => {
        socket.broadcast.emit("getOffer", sdp);
        console.log("offer: " + socket.id);
    });

    socket.on("answer", sdp => {
        socket.broadcast.emit("getAnswer", sdp);
        console.log("answer: " + socket.id);
    });

    socket.on("candidate", candidate => {
        socket.broadcast.emit("getCandidate", candidate);
        console.log("candidate: " + socket.id);
    });
});
        

As evident in the provided code snippet, you have the capability to disseminate SDP messages to other participants, excluding the message sender. This can be achieved through the utilization of the socket.broadcast.emit method.

4. Handle Disconnecting

Lastly, it's essential to handle the disconnection of users to ensure the removal of user information from the room. By establishing a mapping between the socket id and room id when a new user joins a room, we can effectively determine which participants within a specific room should be notified when a user exits.

jsx
            io.on("connection", socket => {
    socket.on("disconnect", () => {
        const roomId = socketToRoom[socket.id];
        let room = rooms[roomId];
        if (room) {
            room = room.filter(user => user.id !== socket.id);
            rooms[roomId] = room;
        }
        socket.broadcast.to(room).emit("user_exit", {id: socket.id});
        console.log(`[${socketToRoom[socket.id]}]: ${socket.id} exit`);
    });
});
        

As illustrated in the following example, you can discern the room id to which a user has joined. This enables you to pinpoint the particular room and subsequently inform the remaining room participants about the departure of the user.

Run The Signaling Server

Now you can run the signaling server very easily with the command lines below:

jsx
            node signaling-server
        

After running the command line you’ll see the signaling server is operated with the port number 3000.

Conclusion

This concludes how to build the signaling server based on peer-to-peer. You can check all source of the lesson 5 on the GitHub repository. In the next lesson, you will learn how to build the signaling client.