Overview
Having previously established the signaling server and successfully run it on your local computer in the preceding lesson, the next step involves crafting your own signaling clients for the creation of peer-to-peer video calls.
In the preceding lesson, we harnessed socket.io to establish the signaling server, facilitating the exchange of SDP messages. Now, on the client side, we will employ socket.io to establish communication with the server.
To start, create a new file named client.js
. You can import and establish a connection with the signaling server using the provided code snippets as shown below:
import { io } from "https://cdn.socket.io/4.4.1/socket.io.esm.min.js";
const socket = io('http://localhost:3000', {
transports: ['websocket', 'polling', 'flashsocket'],
cors: {
origin: "http://localhost:3000",
credentials: true
},
withCredentials: true
});
There are various methods to import socket.io on your client side. For detailed information, you can refer to the socket.io's Client API documentation. For the purpose of this lesson, we have imported socket.io using a CDN URL.
Create a Peer Connection
Since this involves a 1:1 peer-to-peer connection, we will establish a direct peer connection to hold the remote peer information for ongoing communication. As previously discussed in earlier lessons, setting up an RTCPeerConnection is straightforward, as demonstrated below:
const pc_config = {
iceServers: [
{
urls: "stun:stun.l.google.com:19302",
},
],
};
const peerConnection = new RTCPeerConnection(pc_config);
Now let’s communicate with the signaling server to exchange SDP messages.
Listening to the Connection With the Signaling Server
Let's proceed to listen to socket messages from the signaling server step by step. To begin, you can capture the connection message by implementing the following method:
socket.on('connect', () => {
console.log('Hello, successfully connected to the signaling server!');
});
socket.on("room_users", (data) => {
console.log("join:" + data);
});
If you execute the following command to run the client, you'll observe the log message in your developer console on your Chrome.
node client
Communicate With The Signaling Server
Let's review the process of exchanging SDP (Session Description Protocol) messages that we discussed in Lesson 2: Create a Peer Connection (2): SDP Messages.
The SDP contains vital details necessary for establishing a peer connection, including Codec information, source address, audio and video media types, and other relevant properties. You can observe these components within the SDP message example below:
You will be able to exchange SDP messages through the following sequence:
- Generate an SDP Offer
- Generate an SDP Answer
- Exchange ICE Candidates
- Establish local and remote peer connections
Generate an SDP Offer
Let's start by crafting an offer that needs to be transmitted to the intended peer through the signaling server. You can generate an offer using the following example:
socket.on("room_users", (data) => {
console.log("join:" + data);
createOffer()
});
const createOffer = () => {
console.log("create offer");
peerConnection
.createOffer({offerToReceiveAudio: true, offerToReceiveVideo: true})
.then(sdp => {
peerConnection.setLocalDescription(sdp);
socket.emit("offer", sdp);
})
.catch(error => {
console.log(error);
});
};
Upon receiving the room_users
packet from the signaling server, you will generate an SDK offer message and send it to the signaling server by using the socket.emit(offet, sdp)
method. The signaling server will then facilitate the transmission of this message to the other peer.
Conversely, when the signaling server receives the offer message from a client (Client A), it will relay the message to the other client (Client B), excluding the sender (Client A). Subsequently, you should manage the received offer message from the receiving client (Client B), as demonstrated in the following example:
socket.on("getOffer", (sdp) => {
console.log("get offer:" + sdp);
createAnswer(sdp);
});
Generate an SDP Answer
Now, the receiving client (Client B) should acknowledge the signaling server that it has successfully received the offer message. To achieve this, you need to generate an SDP Answer message and transmit it to the signaling server, as shown in the following example:
const createAnswer = (sdp) => {
peerConnection.setRemoteDescription(sdp).then(() => {
console.log("answer set remote description success");
peerConnection
.createAnswer({
offerToReceiveVideo: true,
offerToReceiveAudio: true,
})
.then(sdp1 => {
console.log("create answer");
peerConnection.setLocalDescription(sdp1);
socket.emit("answer", sdp1);
})
.catch(error => {
console.log(error);
});
});
};
As depicted in the aforementioned example, the receiving client (Client B) proceeds to establish the received SDP messages as a remote description. Subsequently, it generates an SDP Answer using the socket.emit(answer, sdp1)
method to transmit the response to the signaling server.
Upon receiving an SDP Answer message from the signaling client, the receiving client (Client A) is required to incorporate the SDP message as a remote description, as illustrated in the provided example:
socket.on("getAnswer", (sdp) => {
console.log("get answer:" + sdp);
peerConnection.setRemoteDescription(sdp);
});
Exchange ICE Candidates
Finally, each peer must exchange ICE candidates through the signaling server. You can detect changes in ICE candidates using the onicecandidate
method, as shown in the example below:
peerConnection.onicecandidate = e => {
if (e.candidate) {
console.log("onicecandidate");
socket.emit("candidate", e.candidate);
}
};
peerConnection.oniceconnectionstatechange = e => {
console.log(e);
};
Once the peer connection instance generates ICE candidate information, the sending client (Client A) will emit the ICE candidate to the signaling server, which will then broadcast the ICE candidate to the receiving client (Client B).
On the receiving client side (Client B), the client can observe the new ICE candidates from the signaling server by adding the following socket protocol:
socket.on("getCandidate", (candidate) => {
peerConnection.addIceCandidate(new RTCIceCandidate(candidate)).then(() => {
console.log("candidate add success");
});
});
You've addressed the key aspects of implementing the signaling client for SDP message exchange via the signaling server. Now, let's integrate the PeerConnection
and MediaStream
components to effectively display both local and remote video streams.
Combine MediaStreams and PeerConnections
Next, we are ready to implement the process of joining a room by integrating MediaStream
and PeerConnection
s. As we've discussed in Creating a RTCPeerConnection, you should include media tracks to the PeerConnection
s, as shown in the example below:
navigator.mediaDevices
.getUserMedia({
video: true,
audio: true,
})
.then(stream => {
if (localVideo.current) localVideo.current.srcObject = stream;
stream.getTracks().forEach(track => {
peerConnection.addTrack(track, stream);
});
})
Next, it's important to monitor the changes in ICE connection to detect the arrival of new ICE candidates. Upon receiving a new ICE candidate, you should ensure its transmission to the signaling server for eventual delivery to the intended destination peer.
peerConnection.onicecandidate = e => {
if (e.candidate) {
console.log("onicecandidate");
socket.emit("candidate", e.candidate);
}
};
peerConnection.oniceconnectionstatechange = e => {
console.log(e);
};
peerConnection.ontrack = ev => {
console.log("add remotetrack success");
if (remoteVideo.current)
remoteVideo.current.srcObject = ev.streams[0];
};
As illustrated in the provided code snippet, you can effectively monitor the ICE candidate event of the remote peer connection using the peerConnection.onicecandidate function. To facilitate the process, you should transmit the newly generated candidate to the signaling server using the socket.emit(candidate, e.candidate);
command.
Finally, to signify your intent to join, it's essential to inform the signaling server by transmitting a \'join\' message, as demonstrated in the following code snippet:
socket.emit("join", {
room: 1234,
name: skydoves@getstream.io,
});
If you put these all steps within a single function named init
, the code will resemble:
async function init(e) {
console.log("render videos");
try {
navigator.mediaDevices
.getUserMedia({
video: true,
audio: true,
})
.then(stream => {
if (localVideo.current) localVideo.current.srcObject = stream;
stream.getTracks().forEach(track => {
peerConnection.addTrack(track, stream);
});
peerConnection.onicecandidate = e => {
if (e.candidate) {
console.log("onicecandidate");
socket.emit("candidate", e.candidate);
}
};
peerConnection.oniceconnectionstatechange = e => {
console.log(e);
};
peerConnection.ontrack = ev => {
console.log("add remotetrack success");
if (remoteVideo.current)
remoteVideo.current.srcObject = ev.streams[0];
};
socket.emit("join", {
room: "1234",
name: "skydoves@getstream.io",
});
})
.catch(error => {
console.log(`getUserMedia error: ${error}`);
});
} catch (e) {
console.log(e);
}
}
const localVideo = document.getElementById('localVideo');
const remoteVideo = document.getElementById('remoteVideo');
document.querySelector('#join').addEventListener('click', e => init(e));
Conclusion
In this lesson, you've acquired the skills to construct the 1:1 peer-to-peer signaling client. Now, let's proceed to render it within a web browser and simulate its behavior using the signaling server.