Setting Up a Signalling Server: Client Video Streaming (Part 2 of 3)

Setting Up a Signalling Server: Client Video Streaming (Part 2 of 3)

Welcome back to our thrilling three-part series on setting up a peer-to-peer connection for client video streaming. In Part 1, we got our hands dirty by setting up the basics of a client-side WebRTC application. Now, it's time to tackle the server-side of things. Buckle up, because we're about to dive into the fascinating world of signalling servers and WebRTC peer information exchange!

Here's the link to the repo.

Previously on "Client Video Streaming"...

In the last episode, we set up our client-side application to establish a WebRTC connection, allowing video streaming between two clients. However, our two clients were lonely and couldn't find each other in the vast internet space. This is where our signalling server comes to the rescue. It will act as a matchmaker, helping the two clients find each other and exchange the necessary information to start their video streaming relationship.

A Signalling Server to the Rescue

Now that we've set the stage, let's get down to business. Our mission today is to set up a signalling server that will help users find one another and establish a WebRTC connection. As a reference, we'll be using the code snippet provided below. We'll be using TypeScript, Express, and Socket.IO to make this happen.

import express from 'express';
import { Server as HttpServer } from 'http';
import { Server as SocketIOServer, Socket } from 'socket.io';

const app = express();
const httpServer = new HttpServer(app);
const io = new SocketIOServer(httpServer);

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

io.on('connection', (socket: Socket) => {
  console.log('User connected:', socket.id);

  socket.on('join-room', (roomId: string) => {
    console.log('User joined room:', roomId);
    const roomClients = io.sockets.adapter.rooms.get(roomId);

    if (!roomClients || roomClients.size === 0) {
      socket.join(roomId);
    } else {
      socket.emit('room-full', 'Room is full');
      socket.disconnect();
    }
  });

  socket.on('offer', (data: { roomId: string; offer: RTCSessionDescriptionInit }) => {
    socket.to(data.roomId).emit('offer', data.offer);
  });

  socket.on('answer', (data: { roomId: string; answer: RTCSessionDescriptionInit }) => {
    socket.to(data.roomId).emit('answer', data.answer);
  });

  socket.on('icecandidate', (data: { roomId: string; candidate: RTCIceCandidateInit }) => {
    socket.to(data.roomId).emit('icecandidate', data.candidate);
  });

  socket.on('disconnect', () => {
    console.log('User disconnected:', socket.id);
  });
});

const PORT = process.env.PORT || 3000;
httpServer.listen(PORT, () => {
  console.log(`Server is running on port ${PORT}`);
});

Step 1: Setting up the server

First things first, let's set up our server using Express and Socket.IO. Our server will be listening on port 3000 (or any available port in the environment). Here's the code to get our server up and running:

import express from 'express';
import { Server as HttpServer } from 'http';
import { Server as SocketIOServer, Socket } from 'socket.io';

const app = express();
const httpServer = new HttpServer(app);
const io = new SocketIOServer(httpServer);

const PORT = process.env.PORT || 3000;
httpServer.listen(PORT, () => {
  console.log(`Server is running on port ${PORT}`);
});

Step 2: Serving static files

To serve our client-side files, we'll use the express.static middleware. This will enable our server to serve static files from the public folder:

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

Step 3: Handling Socket.IO connections

Now, it's time to handle the connections coming from our clients. We'll listen for the connection event on our Socket.IO server and print a message whenever a user connects:

io.on('connection', (socket: Socket) => {
  console.log('User connected:', socket.id);
});

Step 4: Room management and WebRTC signalling

Our server will be responsible for managing rooms and exchanging WebRTC peer information between the clients. To do this, we'll listen for various events, such as join-room, offer, answer, and icecandidate. We'll also handle the disconnect event to clean up after a user leaves:

socket.on('join-room', (roomId: string) => {
  // ...
});

socket.on('offer', (data: { roomId: string; offer: RTCSessionDescriptionInit }) => {
  // ...
});

socket.on('answer', (data: { roomId: string; answer: RTCSessionDescriptionInit }) => {
  // ...
});

socket.on('icecandidate', (data: { roomId: string; candidate: RTCIceCandidateInit }) => {
  // ...
});

socket.on('disconnect', () => {
  console.log('User disconnected:', socket.id);
});

Within each event handler, we'll manage the rooms and forward the WebRTC information to the appropriate clients. For example, when handling the join-room event, we'll check if the room is full and either let the user join or disconnect them:

socket.on('join-room', (roomId: string) => {
  console.log('User joined room:', roomId);
  const roomClients = io.sockets.adapter.rooms.get(roomId);

  if (!roomClients || roomClients.size === 0) {
    socket.join(roomId);
  } else {
    socket.emit('room-full', 'Room is full');
    socket.disconnect();
  }
});

And there you have it! With these event handlers in place, our signalling server is now ready to help clients exchange WebRTC peer information and establish video streaming connections.

Coming Soon: The Final Chapter

Phew! Our signalling server is up and running, and our clients are no longer lonely. They can now find each other and start streaming video, all thanks to our trusty server-side matchmaker. But our journey doesn't end here! In the next and final part of this series, we'll put everything together and make some final adjustments to our application, ensuring that our video streams seamlessly between clients.

See you in the last part of this series!