WebRTC

Created: Real-time Communication with WebRTC

Updated: 03 September 2023

From this CodeLab

Getting the Code

The code can be found here

git clone https://github.com/googlecodelabs/webrtc-web

Running the Website

Run the application in the ‘work’ directory using an HTTP server

Stream video from webcam

In the index.html file add a video element as well as links to the relevant css and js files

<!DOCTYPE html>
<html>
  <head>
    <title>Realtime communication with WebRTC</title>

    <link rel="stylesheet" href="css/main.css" />
  </head>

  <body>
    <h1>Realtime communication with WebRTC</h1>

    <video autoplay playsinline></video>

    <script src="js/main.js"></script>
  </body>
</html>

In the main.js file add the following

'use strict'

// On this codelab, you will be streaming only video (video: true).
const mediaStreamConstraints = {
  video: true,
}

// Video element where stream will be placed.
const localVideo = document.querySelector('video')

// Local stream that will be reproduced on the video.
let localStream

// Handles success by adding the MediaStream to the video element.
function gotLocalMediaStream(mediaStream) {
  localStream = mediaStream
  localVideo.srcObject = mediaStream
}

// Handles error by logging a message to the console with the error message.
function handleLocalMediaStreamError(error) {
  console.log('navigator.getUserMedia error: ', error)
}

// Initializes media stream.
navigator.mediaDevices
  .getUserMedia(mediaStreamConstraints)
  .then(gotLocalMediaStream)
  .catch(handleLocalMediaStreamError)

The getUserMedia function requests access to the mediaStreamConstraints object that it is given, and in turn (after requesting access from the user) will return a MediaStream object which can be used by a media object

Thereafter we use the gotLocalMediaStream and handleLocalMediaStreamError functions to set the video srcObject or respond to errors respectively

The constraints object can consist of different properties, such as:

const hdConstraints = {
  video: {
    width: {
      min: 1280,
    },
    height: {
      min: 720,
    },
  },
}

More specific information on that can be found here

While we’re playing around with the video element you can also simply flip the content on your webcam to mirror itself with the CSS property transform: rotateY(180deg) and just generally play around with the filter property

Also if you do not make use of autoplay on the video you will only see a single frame

More examples of constraints here

Streaming Video with RTCPeerConnection

We will now update the application to consist of a local and remote video in which the page will connect to itself

Add a second video element as well as some buttons for controlling the content to the HTML. Also add the adapter.js file which is a WebRTC shim for simpler compatability between browsers

index.html

<!DOCTYPE html>
<html>
  <head>
    <title>Realtime communication with WebRTC</title>
    <link rel="stylesheet" href="css/main.css" />
  </head>

  <body>
    <h1>Realtime communication with WebRTC</h1>

    <video id="localVideo" autoplay playsinline></video>
    <video id="remoteVideo" autoplay playsinline></video>

    <div>
      <button id="startButton">Start</button>
      <button id="callButton">Call</button>
      <button id="hangupButton">Hang Up</button>
    </div>

    <script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
    <script src="js/main.js"></script>
  </body>
</html>

main.js

'use strict'

// Set up media stream constant and parameters.

// In this codelab, you will be streaming video only: "video: true".
// Audio will not be streamed because it is set to "audio: false" by default.
const mediaStreamConstraints = {
  video: true,
}

// Set up to exchange only video.
const offerOptions = {
  offerToReceiveVideo: 1,
}

// Define initial start time of the call (defined as connection between peers).
let startTime = null

// Define peer connections, streams and video elements.
const localVideo = document.getElementById('localVideo')
const remoteVideo = document.getElementById('remoteVideo')

let localStream
let remoteStream

let localPeerConnection
let remotePeerConnection

// Define MediaStreams callbacks.

// Sets the MediaStream as the video element src.
function gotLocalMediaStream(mediaStream) {
  localVideo.srcObject = mediaStream
  localStream = mediaStream
  trace('Received local stream.')
  callButton.disabled = false // Enable call button.
}

// Handles error by logging a message to the console.
function handleLocalMediaStreamError(error) {
  trace(`navigator.getUserMedia error: ${error.toString()}.`)
}

// Handles remote MediaStream success by adding it as the remoteVideo src.
function gotRemoteMediaStream(event) {
  const mediaStream = event.stream
  remoteVideo.srcObject = mediaStream
  remoteStream = mediaStream
  trace('Remote peer connection received remote stream.')
}

// Add behavior for video streams.

// Logs a message with the id and size of a video element.
function logVideoLoaded(event) {
  const video = event.target
  trace(
    `${video.id} videoWidth: ${video.videoWidth}px, ` +
      `videoHeight: ${video.videoHeight}px.`
  )
}

// Logs a message with the id and size of a video element.
// This event is fired when video begins streaming.
function logResizedVideo(event) {
  logVideoLoaded(event)

  if (startTime) {
    const elapsedTime = window.performance.now() - startTime
    startTime = null
    trace(`Setup time: ${elapsedTime.toFixed(3)}ms.`)
  }
}

localVideo.addEventListener('loadedmetadata', logVideoLoaded)
remoteVideo.addEventListener('loadedmetadata', logVideoLoaded)
remoteVideo.addEventListener('onresize', logResizedVideo)

// Define RTC peer connection behavior.

// Connects with new peer candidate.
function handleConnection(event) {
  const peerConnection = event.target
  const iceCandidate = event.candidate

  if (iceCandidate) {
    const newIceCandidate = new RTCIceCandidate(iceCandidate)
    const otherPeer = getOtherPeer(peerConnection)

    otherPeer
      .addIceCandidate(newIceCandidate)
      .then(() => {
        handleConnectionSuccess(peerConnection)
      })
      .catch((error) => {
        handleConnectionFailure(peerConnection, error)
      })

    trace(
      `${getPeerName(peerConnection)} ICE candidate:\n` +
        `${event.candidate.candidate}.`
    )
  }
}

// Logs that the connection succeeded.
function handleConnectionSuccess(peerConnection) {
  trace(`${getPeerName(peerConnection)} addIceCandidate success.`)
}

// Logs that the connection failed.
function handleConnectionFailure(peerConnection, error) {
  trace(
    `${getPeerName(peerConnection)} failed to add ICE Candidate:\n` +
      `${error.toString()}.`
  )
}

// Logs changes to the connection state.
function handleConnectionChange(event) {
  const peerConnection = event.target
  console.log('ICE state change event: ', event)
  trace(
    `${getPeerName(peerConnection)} ICE state: ` +
      `${peerConnection.iceConnectionState}.`
  )
}

// Logs error when setting session description fails.
function setSessionDescriptionError(error) {
  trace(`Failed to create session description: ${error.toString()}.`)
}

// Logs success when setting session description.
function setDescriptionSuccess(peerConnection, functionName) {
  const peerName = getPeerName(peerConnection)
  trace(`${peerName} ${functionName} complete.`)
}

// Logs success when localDescription is set.
function setLocalDescriptionSuccess(peerConnection) {
  setDescriptionSuccess(peerConnection, 'setLocalDescription')
}

// Logs success when remoteDescription is set.
function setRemoteDescriptionSuccess(peerConnection) {
  setDescriptionSuccess(peerConnection, 'setRemoteDescription')
}

// Logs offer creation and sets peer connection session descriptions.
function createdOffer(description) {
  trace(`Offer from localPeerConnection:\n${description.sdp}`)

  trace('localPeerConnection setLocalDescription start.')
  localPeerConnection
    .setLocalDescription(description)
    .then(() => {
      setLocalDescriptionSuccess(localPeerConnection)
    })
    .catch(setSessionDescriptionError)

  trace('remotePeerConnection setRemoteDescription start.')
  remotePeerConnection
    .setRemoteDescription(description)
    .then(() => {
      setRemoteDescriptionSuccess(remotePeerConnection)
    })
    .catch(setSessionDescriptionError)

  trace('remotePeerConnection createAnswer start.')
  remotePeerConnection
    .createAnswer()
    .then(createdAnswer)
    .catch(setSessionDescriptionError)
}

// Logs answer to offer creation and sets peer connection session descriptions.
function createdAnswer(description) {
  trace(`Answer from remotePeerConnection:\n${description.sdp}.`)

  trace('remotePeerConnection setLocalDescription start.')
  remotePeerConnection
    .setLocalDescription(description)
    .then(() => {
      setLocalDescriptionSuccess(remotePeerConnection)
    })
    .catch(setSessionDescriptionError)

  trace('localPeerConnection setRemoteDescription start.')
  localPeerConnection
    .setRemoteDescription(description)
    .then(() => {
      setRemoteDescriptionSuccess(localPeerConnection)
    })
    .catch(setSessionDescriptionError)
}

// Define and add behavior to buttons.

// Define action buttons.
const startButton = document.getElementById('startButton')
const callButton = document.getElementById('callButton')
const hangupButton = document.getElementById('hangupButton')

// Set up initial action buttons status: disable call and hangup.
callButton.disabled = true
hangupButton.disabled = true

// Handles start button action: creates local MediaStream.
function startAction() {
  startButton.disabled = true
  navigator.mediaDevices
    .getUserMedia(mediaStreamConstraints)
    .then(gotLocalMediaStream)
    .catch(handleLocalMediaStreamError)
  trace('Requesting local stream.')
}

// Handles call button action: creates peer connection.
function callAction() {
  callButton.disabled = true
  hangupButton.disabled = false

  trace('Starting call.')
  startTime = window.performance.now()

  // Get local media stream tracks.
  const videoTracks = localStream.getVideoTracks()
  const audioTracks = localStream.getAudioTracks()
  if (videoTracks.length > 0) {
    trace(`Using video device: ${videoTracks[0].label}.`)
  }
  if (audioTracks.length > 0) {
    trace(`Using audio device: ${audioTracks[0].label}.`)
  }

  const servers = null // Allows for RTC server configuration.

  // Create peer connections and add behavior.
  localPeerConnection = new RTCPeerConnection(servers)
  trace('Created local peer connection object localPeerConnection.')

  localPeerConnection.addEventListener('icecandidate', handleConnection)
  localPeerConnection.addEventListener(
    'iceconnectionstatechange',
    handleConnectionChange
  )

  remotePeerConnection = new RTCPeerConnection(servers)
  trace('Created remote peer connection object remotePeerConnection.')

  remotePeerConnection.addEventListener('icecandidate', handleConnection)
  remotePeerConnection.addEventListener(
    'iceconnectionstatechange',
    handleConnectionChange
  )
  remotePeerConnection.addEventListener('addstream', gotRemoteMediaStream)

  // Add local stream to connection and create offer to connect.
  localPeerConnection.addStream(localStream)
  trace('Added local stream to localPeerConnection.')

  trace('localPeerConnection createOffer start.')
  localPeerConnection
    .createOffer(offerOptions)
    .then(createdOffer)
    .catch(setSessionDescriptionError)
}

// Handles hangup action: ends up call, closes connections and resets peers.
function hangupAction() {
  localPeerConnection.close()
  remotePeerConnection.close()
  localPeerConnection = null
  remotePeerConnection = null
  hangupButton.disabled = true
  callButton.disabled = false
  trace('Ending call.')
}

// Add click event handlers for buttons.
startButton.addEventListener('click', startAction)
callButton.addEventListener('click', callAction)
hangupButton.addEventListener('click', hangupAction)

// Define helper functions.

// Gets the "other" peer connection.
function getOtherPeer(peerConnection) {
  return peerConnection === localPeerConnection
    ? remotePeerConnection
    : localPeerConnection
}

// Gets the name of a certain peer connection.
function getPeerName(peerConnection) {
  return peerConnection === localPeerConnection
    ? 'localPeerConnection'
    : 'remotePeerConnection'
}

// Logs an action (text) and the time when it happened on the console.
function trace(text) {
  text = text.trim()
  const now = (window.performance.now() / 1000).toFixed(3)

  console.log(now, text)
}