Skip to main content

WebRTC Introduction - Interactive Connectivity Establishment

Shenzhen, China

WIP

Interactive Connectivity Establishment (ICE)

Peer Connection

Use STUN servers to exchange your public address:

// List of STUN server to exchange public IPs
const iceServers = {
  iceServers: [
    { urls: "stun:stun.l.google.com:19302" },
    { urls: "stun:stun.nextcloud.com:443" }
  ]
}

The RTCPeerConnection interface represents a WebRTC connection between the local computer and a remote peer. It provides methods to connect to a remote peer, maintain and monitor the connection, and close the connection once it's no longer needed.

rtcPeerConnection is an interface that provides several functions that need to be defined. First we need to handle the local stream that we get from fetchLocalStream in form of the userStream object. This is done by addTrack():

// Room is ready event
socket.on('ready', function() {
  if (creator) {
    rtcPeerConnection = new RTCPeerConnection(iceServers)
    rtcPeerConnection.oniceccandidate = onIceCandidate()
    rtcPeerConnection.ontrack = onTrack()
    // get local audio stream from userStream object
    rtcPeerConnection.addTrack(userStream.getTracks()[0], userstream)
    // get local video stream from userStream object
    rtcPeerConnection.addTrack(userStream.getTracks()[0], userstream)
  }
})

The second one is onIceCandidate() returns a candidate for the connection that needs to be published through our websocket:

// If ICE candidate is returned emit over ws
function onIceCandidate(event) {
  if (event.candidate) {
    socket.emit("candidate", event.candidate, chatRoom.value);
  }
}

The third function we need to handle is onTrack() that handles the media stream that is provided by the remote candidate:

// If ICE candidate starts streaming media emit over ws
function onTrack(event) {
  peerVideo.srcObject = event.streams[0];
  peerVideo.onloadedmetadata = function (e) {
    peerVideo.play();
  };
}

Negotiate Media (SDP)

The creator of the room now needs to use the established connection to send an offer to the remote candidate. Since the answering remote candidate will have to send the same media information I will export those functions and recycle them.

When the room is created and ready get public address of the remote candidate and collect your local media information

function rtcPeering() {
  rtcPeerConnection = new RTCPeerConnection(iceServers)
  rtcPeerConnection.oniceccandidate = onIceCandidate()
  rtcPeerConnection.ontrack = onTrack()
  // get audio stream from userStream object
  rtcPeerConnection.addTrack(userStream.getTracks()[0], userstream)
  // get video stream from userStream object
  rtcPeerConnection.addTrack(userStream.getTracks()[0], userstream)
}

Offer

Send the offer to your remote candidate:

// Room is ready event
socket.on('ready', function() {
  console.log('INFO :: Room ready.')
  // Room creator generates offer for remote candidate to join
  if (creator) {
    rtcPeering()
    // create offer
    rtcPeerConnection
    .createOffer()
    .then((offer) => {
      rtcPeerConnection.setLocalDescription(offer);
      socket.emit("offer", offer, roomName);
    })

    .catch((error) => {
      console.log(error);
    });
  }
  console.log('INFO :: Offer send.')
})

Answer

When the remote candidate receives the offer it will provide the following answer containing the same information but it's local media:

// Triggered on receiving an offer from the person who created the room.
socket.on("offer", function (offer) {
  // If not creator send answer to offer
  if (!creator) {
    rtcPeering()
    rtcPeerConnection.setRemoteDescription(offer);
    rtcPeerConnection
      .createAnswer()
      .then((answer) => {
        rtcPeerConnection.setLocalDescription(answer);
        socket.emit("answer", answer, roomName);
      })
      .catch((error) => {
        console.log(error);
      });
  }
  console.log('INFO :: Answer send.')
});