WebRTC and Socket.io applications using Javascript/Node.js

WebRTC and Socket.io applications using Javascript/Node.js

Steven Rugg

Written by Steven Rugg /

Building a WebRTC Application with Socket.IO as a Signaling Solution

WebRTC (Web Real-Time Communication) is a powerful technology that enables peer-to-peer (P2P) communication for audio, video, and data sharing directly between browsers or devices. It removes the need for intermediary servers for media transmission, making communication faster and more private. In this article, we'll explore how WebRTC works, its use cases, and how to implement a file-sharing solution using a data channel, with Socket.IO as the signaling mechanism.

What is WebRTC?

WebRTC is a set of APIs and protocols that allows web applications and sites to capture media from devices, establish peer-to-peer connections, and transfer data with low latency. WebRTC is primarily used for:

  • Video/Audio Chat: Platforms like Google Meet and Zoom use WebRTC for video/audio conferencing.
  • File Transfer: Secure peer-to-peer file sharing without relying on servers.
  • Gaming: Real-time data transmission for multiplayer games.

WebRTC Components

  • MediaStream API: Captures media (audio/video) from cameras or microphones.
  • RTCPeerConnection: Establishes and manages the P2P connection.
  • RTCDataChannel: Allows direct communication for exchanging arbitrary data (e.g., file transfers, game states).

However, WebRTC needs signaling to facilitate the connection. This is where Socket.IO steps in.

Role of Signaling in WebRTC

Before peers can communicate, they need to exchange connection information (SDP—Session Description Protocol and ICE candidates). Signaling is responsible for this exchange. WebRTC doesn’t specify how signaling should happen; it’s up to developers to implement it using any transport mechanism like HTTP, WebSockets, or Socket.IO.

Why Use Socket.IO for Signaling?

Socket.IO is a real-time engine that makes it easy to establish bidirectional communication between the server and the client. It is perfect for WebRTC signaling because of its simplicity and event-driven nature. In our WebRTC setup, Socket.IO will handle signaling messages for:

  • Exchanging SDP: To describe multimedia information (media codec, encryption, etc.).
  • Exchanging ICE candidates: To share network details (IP addresses and ports) needed for the P2P connection.
  • Setting Up WebRTC with Socket.IO Signaling
  • Let's walk through how you can set up a WebRTC connection with Socket.IO for signaling and establish a data channel for file sharing.

1. Setting Up the Environment

We'll start by setting up a basic server using Node.js and Socket.IO for signaling. On the client side, we'll use vanilla JavaScript to implement WebRTC and Socket.IO.

Install Dependencies

First, install the necessary packages:

JAVASCRIPT
npm install express socket.io

Create server.js for Signaling

Here’s how the signaling server works:

  • It listens for connection events from clients.
  • When a client sends an SDP offer, it forwards it to the peer.
  • The same happens with ICE candidates.

This is a simple Node.js server that uses Socket.IO to facilitate signaling between two peers in a WebRTC session. Signaling is the process where WebRTC peers exchange necessary connection information (SDP offers, answers, and ICE candidates) before establishing a peer-to-peer connection

Simple express server and socket.io socket

JAVASCRIPT
const express = require('express');
const http = require('http');
const { Server } = require('socket.io');

const app = express();
const server = http.createServer(app);
const io = new Server(server);

app.use(express.static('public'));

io.on('connection', (socket) => {
    console.log('A user connected');

    // Relay offer/answer SDP
    socket.on('offer', (data) => {
        socket.broadcast.emit('offer', data);
    });

    socket.on('answer', (data) => {
        socket.broadcast.emit('answer', data);
    });

    // Relay ICE candidates
    socket.on('ice-candidate', (candidate) => {
        socket.broadcast.emit('ice-candidate', candidate);
    });

    socket.on('disconnect', () => {
        console.log('A user disconnected');
    });
});

server.listen(3000, () => {
    console.log('Server is listening on port 3000');
});

Explanation:

Express.js serves static files from the public folder (e.g., your client-side code). Socket.IO is used to create real-time communication between clients.

Socket.IO Events:

  • offer: Receives an SDP offer from one peer and broadcasts it to the other peer.
  • answer: Receives an SDP answer and broadcasts it to the other peer.
  • ice-candidate: Receives ICE candidates (network information) and forwards them to the other peer.

Client-Side WebRTC and Signaling

The client-side code handles establishing a WebRTC peer connection, sending SDP offers/answers, and exchanging ICE candidates through Socket.IO.

WebRTC and Signaling Code (Client-Side)

JAVASCRIPT
const socket = io();

// Creating a new RTCPeerConnection instance
const peerConnection = new RTCPeerConnection({
    iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]  // Using a public STUN server for NAT traversal
});

// Creating a Data Channel
let dataChannel = peerConnection.createDataChannel('fileChannel'); // 'fileChannel' is the label of the channel

// Handle ICE candidate event and send to signaling server
peerConnection.onicecandidate = (event) => {
    if (event.candidate) {
        socket.emit('ice-candidate', event.candidate);  // Send ICE candidate via signaling (Socket.IO)
    }
};

// Handling the data channel when opened by a peer
peerConnection.ondatachannel = (event) => {
    const receiveChannel = event.channel;  // Event fired when the other peer creates a data channel
    receiveChannel.onmessage = (e) => {
        console.log('Received file:', e.data);  // Handle received data (e.g., file)
    };
};

// Sending an SDP offer to the peer
async function makeOffer() {
    const offer = await peerConnection.createOffer();
    await peerConnection.setLocalDescription(offer);  // Set the local SDP description (the offer)
    socket.emit('offer', offer);  // Send offer to the other peer via signaling (Socket.IO)
}

// Receiving an SDP offer, setting it as the remote description, and creating an answer
socket.on('offer', async (offer) => {
    await peerConnection.setRemoteDescription(offer);  // Set the remote description to the received offer
    const answer = await peerConnection.createAnswer();
    await peerConnection.setLocalDescription(answer);  // Set the local SDP description (the answer)
    socket.emit('answer', answer);  // Send answer to the other peer via signaling
});

// Handling SDP answer
socket.on('answer', async (answer) => {
    await peerConnection.setRemoteDescription(answer);  // Set the remote SDP description to the received answer
});

// Handling ICE candidates from the signaling server
socket.on('ice-candidate', async (candidate) => {
    try {
        await peerConnection.addIceCandidate(candidate);  // Add the received ICE candidate to the peer connection
    } catch (e) {
        console.error('Error adding received ice candidate', e);
    }
});

// Sharing a file via the data channel
function sendFile(file) {
    dataChannel.send(file);  // Sending the file directly via the data channel
}

peerConnection.onnegotiationneeded = async () => {
    try {
        const offer = await peerConnection.createOffer();  // Create a new SDP offer when renegotiation is needed
        await peerConnection.setLocalDescription(offer);   // Set the local description with the new offer
        socket.emit('offer', offer);                       // Send the offer to the signaling server (Socket.IO)
    } catch (err) {
        console.error('Error during negotiation', err);    // Catch negotiation errors
    }
};

Key Elements Breakdown:

  1. Peer Connection (RTCPeerConnection):

This is the core WebRTC API that manages the connection between two peers. The iceServers configuration is where you define how to handle NAT traversal. Here, a public STUN server (stun:stun.l.google.com:19302) is used. Data Channel:

  1. A data channel (dataChannel) is established using peerConnection.createDataChannel(). This allows sending arbitrary data, such as files, directly between peers without using a server for the actual transfer. ICE Candidate Handling:

  2. Each peer gathers network information (ICE candidates) to establish a direct P2P connection. These candidates are exchanged through Socket.IO. SDP (Session Description Protocol):

  3. SDP Offer/Answer: These messages describe the media and connection capabilities of the peers. The SDP is exchanged via the signaling server (Socket.IO), which then allows the peers to agree on the media and connection settings. The makeOffer() function creates an SDP offer and sends it to the signaling server. The peer receiving the offer creates an SDP answer, which is also relayed via the signaling server. Sending Files via the Data Channel:

  4. The sendFile() function sends the file (or chunks of data) through the WebRTC data channel. Files are transmitted peer-to-peer directly through the data channel, allowing fast and secure file transfer without needing a middle server. Socket.IO for Signaling:

Socket.IO is used here for real-time signaling between clients. It forwards offers, answers, and ICE candidates. Once signaling is completed, WebRTC takes over for P2P communication. Summary:

  • WebRTC is used to establish a peer-to-peer connection between two users, allowing the direct transfer of media and data.
  • Socket.IO handles the signaling process, facilitating the exchange of connection details like SDP offers/answers and ICE candidates.
  • RTCDataChannel enables real-time data transfer between peers, such as sharing files or other custom data.

6. Triggered Automatically:

The onnegotiationneeded event is automatically fired when the browser detects that the peer connection needs renegotiation, like when you add a media stream or data channel. Renegotiation Flow: When this event fires, it creates a new offer (createOffer()), sets it as the local description (setLocalDescription()), and sends the offer to the peer via the signaling server (socket.emit('offer', offer)). Prevents Connection Breaks: Without proper negotiation, adding new streams or channels could break or fail to sync across peers.

This setup creates a flexible, low-latency solution for video/audio conferencing, file sharing, or other real-time communication scenarios.