WebRTC

From David's Wiki
\( \newcommand{\P}[]{\unicode{xB6}} \newcommand{\AA}[]{\unicode{x212B}} \newcommand{\empty}[]{\emptyset} \newcommand{\O}[]{\emptyset} \newcommand{\Alpha}[]{Α} \newcommand{\Beta}[]{Β} \newcommand{\Epsilon}[]{Ε} \newcommand{\Iota}[]{Ι} \newcommand{\Kappa}[]{Κ} \newcommand{\Rho}[]{Ρ} \newcommand{\Tau}[]{Τ} \newcommand{\Zeta}[]{Ζ} \newcommand{\Mu}[]{\unicode{x039C}} \newcommand{\Chi}[]{Χ} \newcommand{\Eta}[]{\unicode{x0397}} \newcommand{\Nu}[]{\unicode{x039D}} \newcommand{\Omicron}[]{\unicode{x039F}} \DeclareMathOperator{\sgn}{sgn} \def\oiint{\mathop{\vcenter{\mathchoice{\huge\unicode{x222F}\,}{\unicode{x222F}}{\unicode{x222F}}{\unicode{x222F}}}\,}\nolimits} \def\oiiint{\mathop{\vcenter{\mathchoice{\huge\unicode{x2230}\,}{\unicode{x2230}}{\unicode{x2230}}{\unicode{x2230}}}\,}\nolimits} \)

WebRTC (Web Real-Time Communication) is a standard for peer-to-peer real-time communication.
It supports audio streams, video streams, and data streams but has a complicated API and handshake which needs to be performed over an existing connection such as WebSockets to a server accessible from both peers.

Background (JavaScript)

To get started, first read this background section, then look into WebRTC examples in JavaScript.

WebRTC is a real-time P2P communications protocol which supports audio, video, and data.
For P2P applications to work, they need to first negotiation a connection using an intermediary server.
This signalling is typically done through a WebSocket but can also be done using AJAX via Socket.IO.

MediaStream

The purpose of MediaStream to to access the user's camera, microphone, or screen. This is done using MediaDevices.getUserMedia

RTCPeerConnection

The purpose of RTCPeerConnection is to negotiate a connection using the signalling channel (the Websocket forwarded through your nat).

When you make an RTCPeerConnection, you should pass in a list of iceServers:

const configuration = {iceServers: [{urls: 'stun.l.google.com:19302'}]};
const pc = new RTCPeerConnection(configuration);

There are two types of iceServers:

  • STUN servers are used to find the public IP and port of the client.
    • You can find a list of STUN servers here.
  • TURN servers are a fallback used to proxy data through the NAT.

For each STUN server, you need to send it to the other peer via your signalling channel:

const configuration = {iceServers: [{urls: 'stuns:stun.example.org'}]};

The other client will add these as follows:

remoteConnection.addIceCandidate(e.candidate)

Next the local and remote clients exchange descriptions:

const localOffer = await localConnection.createOffer();
localConnection.setLocalDescription(localOffer);
signallingChannel.send(localConnection.localDescription)

# On the remote
const localOffer = signalingChannel.receiveAnswer(); // Actually a callback irl
remoteConnection.setRemoteDescription(localOffer);
const answer = await remoteConnection.createAnswer();
remoteConnection.setLocalDescription(answer);
signallingChannel.send(remoteConnection.localDescription)

# On the local
const remoteDescription = signalingChannel.receiveAnswer();
localConnection.setRemoteDescription(remoteDescription);
  • Note that both clients can continue to update descriptions as the network updates.
    As long as the signalling is setup correctly, you don't need to worry about what RTCPeerConnection is doing under the hood.
RTCPeerConnection Example

Below is from HTML5Rocks.

// handles JSON.stringify/parse
const signaling = new SignalingChannel();
const constraints = {audio: true, video: true};
const configuration = {iceServers: [{urls: 'stuns:stun.example.org'}]};
const pc = new RTCPeerConnection(configuration);

// send any ice candidates to the other peer
pc.onicecandidate = ({candidate}) => signaling.send({candidate});

// let the "negotiationneeded" event trigger offer generation
pc.onnegotiationneeded = async () => {
  try {
    await pc.setLocalDescription(await pc.createOffer());
    // send the offer to the other peer
    signaling.send({desc: pc.localDescription});
  } catch (err) {
    console.error(err);
  }
};

// once remote track media arrives, show it in remote video element
pc.ontrack = (event) => {
  // don't set srcObject again if it is already set.
  if (remoteView.srcObject) return;
  remoteView.srcObject = event.streams[0];
};

// call start() to initiate
async function start() {
  try {
    // get local stream, show it in self-view and add it to be sent
    const stream =
      await navigator.mediaDevices.getUserMedia(constraints);
    stream.getTracks().forEach((track) =>
      pc.addTrack(track, stream));
    selfView.srcObject = stream;
  } catch (err) {
    console.error(err);
  }
}

signaling.onmessage = async ({desc, candidate}) => {
  try {
    if (desc) {
      // if we get an offer, we need to reply with an answer
      if (desc.type === 'offer') {
        await pc.setRemoteDescription(desc);
        const stream =
          await navigator.mediaDevices.getUserMedia(constraints);
        stream.getTracks().forEach((track) =>
          pc.addTrack(track, stream));
        await pc.setLocalDescription(await pc.createAnswer());
        signaling.send({desc: pc.localDescription});
      } else if (desc.type === 'answer') {
        await pc.setRemoteDescription(desc);
      } else {
        console.log('Unsupported SDP type.');
      }
    } else if (candidate) {
      await pc.addIceCandidate(candidate);
    }
  } catch (err) {
    console.error(err);
  }
};

RTCDataChannel

const localConnection = new RTCPeerConnection(servers);
const remoteConnection = new RTCPeerConnection(servers);
const sendChannel =
  localConnection.createDataChannel('sendDataChannel');

// ...

remoteConnection.ondatachannel = (event) => {
  receiveChannel = event.channel;
  receiveChannel.onmessage = onReceiveMessage;
  receiveChannel.onopen = onReceiveChannelStateChange;
  receiveChannel.onclose = onReceiveChannelStateChange;
};

function onReceiveMessage(event) {
  document.querySelector("textarea#send").value = event.data;
}

document.querySelector("button#send").onclick = () => {
  var data = document.querySelector("textarea#send").value;
  sendChannel.send(data);
};

JavaScript API

Multiple Clients

To handle multiple clients, you simply have one RTCPeerConnection per client.

Native APIs (C++)

Resources

References