One-on-one Video Call Example in One HTML Page

One-on-one Video Call Example in One HTML Page

·

3 min read

In this article, I will show you how to use WebRTC API in browser by implementing a one-on-one video call example in one html page.

Process

Before the code starts, let's go through the basic process first.

Say there are user A and user B.

Each user should create a RTCPeerConnection object, and this will be used to transmit audio and video. We can create the object in js like this:

const pc1 = new RTCPeerConnection(null);

Then each user should capture local audio and video tracks from media devices and this can be done by this:

const stream = await navigator.mediaDevices.getUserMedia({ video: true });

Now we have the RTCPeerConnection object and local tracks, and the following is media negotiation.

We can think media negotiation is a process of two computers to exchange their media parameters and try to reach a consensus. For example, two users exchange the list of encoders supported by each, find the commonly supported parts and then select one of them as the final encoder to be used.

The media negotiation process is done by a offer-answser model. We can see the actual steps in below code.

const offer = await pc1.createOffer();
await pc1.setLocalDescription(offer);

// now offer will be sent to pc2 using a signaling server

await pc2.setRemoteDescription(offer);
const answer = await pc2.createAnswer();
await pc2.setLocalDescription(answer);

// now answer will be sent to pc1 using a signaling server

await pc1.setRemoteDescription(answer);

After two users come to a consensus then we need to establish a p2p connection between them.

This is done by the candidate exchange model. A candidate actually is an object containing ip/port and other address information. Each user normally have more than one candidate. These candidates will be sent to each other by a signaling server. Then each user will try to establish a p2p connection using the remote candidate information they received. This process can be done by below code.

pc1.onicecandidate = async e => {
  if (!e.candidate) {
    return;
  }
  try {
    await pc2.addIceCandidate(e.candidate);
  } catch (error) {
    console.log("pc2 add pc1 candidate error", error);
  }
}

There is a callback in RTCPeerConnection object to monitor p2p connection.

pc1.onconnectionstatechange = () => {
  console.log(pc1.connectionState)
}

Finally, after p2p connection is established, we can get remote tracks from a callback ontrack.

pc2.ontrack = (e) => {
  remoteVideo.srcObject = new MediaStream([e.track]);
}

Code

<!DOCTYPE html>
<html lang="en">

<head>
  <style>
    video {
      width: 400px;
    }
  </style>
</head>

<body>
  <button id="start">start</button>

  <p>local video</p>
  <video autoplay id="local-video"></video>

  <p>remote video</p>
  <video autoplay id="remote-video"></video>

  <script>
    const localVideo = document.getElementById("local-video")
    const remoteVideo = document.getElementById("remote-video")

    // pc1 act as local pc
    const pc1 = new RTCPeerConnection(null);

    // pc2 act as remote pc
    const pc2 = new RTCPeerConnection(null);

    start.onclick = async () => {
      try {
        const stream = await navigator.mediaDevices.getUserMedia({ video: true });
        localVideo.srcObject = stream;

        const videoTrack = stream.getVideoTracks()[0];
        pc1.addTrack(videoTrack);
      } catch (error) {
        console.log("get local stream error", error);
      }
    }

    pc1.onnegotiationneeded = async () => {
      try {
        const offer = await pc1.createOffer();
        await pc1.setLocalDescription(offer);

        // now offer will be sent to pc2 using a signaling server

        await pc2.setRemoteDescription(offer);
        const answer = await pc2.createAnswer();
        await pc2.setLocalDescription(answer);

        // now answer will be sent to pc1 using a signaling server

        await pc1.setRemoteDescription(answer);

      } catch (error) {
        console.log("offer answer error", error);
      }
    }

    pc1.onconnectionstatechange = () => {
      console.log(pc1.connectionState)
    }

    pc1.onicecandidate = async e => {
      if (!e.candidate) {
        return;
      }
      try {
        await pc2.addIceCandidate(e.candidate);
      } catch (error) {
        console.log("pc2 add pc1 candidate error", error);
      }
    }

    pc2.onicecandidate = async e => {
      if (!e.candidate) {
        return;
      }
      try {
        await pc1.addIceCandidate(e.candidate);
      } catch (error) {
        console.log("pc1 add pc2 candidate error", error);
      }
    }

    pc2.ontrack = (e) => {
      remoteVideo.srcObject = new MediaStream([e.track]);
    }

  </script>
</body>

</html>