WebRTC Cheatsheet

Screen Sharing

Use this WebRTC reference while you build software engineering projects, review code for technical interview prep, or polish examples for a software engineer resume.

getDisplayMedia — Basic Usage

// Prompt user to pick a screen, window, or tab
const screenStream = await navigator.mediaDevices.getDisplayMedia({
  video: true,
  audio: false, // system audio (limited browser support)
});

const [screenTrack] = screenStream.getVideoTracks();
document.getElementById('preview').srcObject = screenStream;

getDisplayMedia must be called from a user gesture (click, keypress). It always shows the browser's native picker — you cannot bypass it.

Options and Constraints

const stream = await navigator.mediaDevices.getDisplayMedia({
  video: {
    displaySurface: 'monitor',   // 'browser' | 'window' | 'monitor'
    width:     { ideal: 1920, max: 3840 },
    height:    { ideal: 1080, max: 2160 },
    frameRate: { ideal: 30, max: 60 },
    cursor:    'always',         // 'always' | 'motion' | 'never' (deprecated)
  },
  audio: {
    echoCancellation: false,     // recommended for system audio
    noiseSuppression: false,
    sampleRate: 44100,
  },
  // Preferences (Chrome 107+)
  preferCurrentTab: false,       // hint to pre-select current tab
  selfBrowserSurface: 'exclude', // 'include' | 'exclude'
  systemAudio: 'include',        // 'include' | 'exclude'
  surfaceSwitching: 'include',   // show/hide the "Switch" button mid-share
  monitorTypeSurfaces: 'include',// show/hide monitor option
});

displaySurface Values

ValueWhat the user sees
"browser"Browser tabs only
"window"Application windows
"monitor"Entire screen/monitor

System Audio Capture (Chrome)

// Chrome only — capture tab audio when sharing a tab
const stream = await navigator.mediaDevices.getDisplayMedia({
  video: { displaySurface: 'browser' },
  audio: true, // user must check "Share tab audio" in the picker
});

const [audioTrack] = stream.getAudioTracks();
if (audioTrack) {
  console.log('System audio captured:', audioTrack.label);
}

Firefox and Safari do not support system audio capture via getDisplayMedia.

Sharing Screen via RTCPeerConnection

// Replace an existing video track (no renegotiation needed)
async function startScreenShare(pc) {
  const screenStream = await navigator.mediaDevices.getDisplayMedia({ video: true });
  const [screenTrack] = screenStream.getVideoTracks();

  const sender = pc.getSenders().find(s => s.track?.kind === 'video');
  await sender.replaceTrack(screenTrack);

  // Listen for the user stopping share via browser UI
  screenTrack.onended = () => stopScreenShare(pc);

  return screenTrack;
}

// Switch back to camera
async function stopScreenShare(pc, cameraStream) {
  const [cameraTrack] = cameraStream.getVideoTracks();
  const sender = pc.getSenders().find(s => s.track?.kind === 'video');
  await sender.replaceTrack(cameraTrack);
}

Detecting When the User Stops Sharing

const [track] = screenStream.getVideoTracks();

// Fires when user clicks "Stop Sharing" in the browser bar
track.onended = () => {
  console.log('User stopped screen sharing');
  cleanup();
};

// Also check readyState
if (track.readyState === 'ended') {
  // Already stopped
}

Screen Share + Camera Picture-in-Picture

// Send screen as primary video, camera as a canvas overlay
const screenStream = await navigator.mediaDevices.getDisplayMedia({ video: true });
const cameraStream = await navigator.mediaDevices.getUserMedia({ video: true });

const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width  = 1280;
canvas.height = 720;

const screenVideo = document.createElement('video');
screenVideo.srcObject = screenStream;
screenVideo.play();

const cameraVideo = document.createElement('video');
cameraVideo.srcObject = cameraStream;
cameraVideo.play();

function drawFrame() {
  ctx.drawImage(screenVideo, 0, 0, canvas.width, canvas.height);
  // Camera pip: bottom-right corner
  const pipW = 240, pipH = 135;
  ctx.drawImage(cameraVideo, canvas.width - pipW - 16, canvas.height - pipH - 16, pipW, pipH);
  requestAnimationFrame(drawFrame);
}

drawFrame();

const compositeStream = canvas.captureStream(30);
const sender = pc.getSenders().find(s => s.track?.kind === 'video');
await sender.replaceTrack(compositeStream.getVideoTracks()[0]);

Optimizing Screen Share Quality

// High-quality screen share: maximize resolution and sharpness
const stream = await navigator.mediaDevices.getDisplayMedia({
  video: {
    width:     { ideal: 2560 },
    height:    { ideal: 1440 },
    frameRate: { ideal: 60 },
  },
});

// Tune the sender for screen content (low motion, high detail)
const sender = pc.getSenders().find(s => s.track?.kind === 'video');
const params = sender.getParameters();
params.encodings[0].maxBitrate = 8_000_000;         // 8 Mbps for high detail
params.encodings[0].scaleResolutionDownBy = 1;
// contentHint helps the encoder choose settings
stream.getVideoTracks()[0].contentHint = 'detail';  // 'motion' | 'detail' | 'text'
await sender.setParameters(params);

contentHint Values

ValueBest forEncoder behavior
"" (empty)GeneralDefault
"motion"Video, gamesPrioritize frame rate
"detail"Slides, photosPrioritize resolution
"text"Text, codeSharpen edges, low motion

Screen Share with Audio Mix (Tab + Mic)

const screenStream = await navigator.mediaDevices.getDisplayMedia({
  video: true,
  audio: true, // tab audio
});
const micStream = await navigator.mediaDevices.getUserMedia({ audio: true });

const audioCtx = new AudioContext();
const dest = audioCtx.createMediaStreamDestination();

// Mix both audio sources
audioCtx.createMediaStreamSource(screenStream).connect(dest);
audioCtx.createMediaStreamSource(micStream).connect(dest);

const mixedStream = new MediaStream([
  ...screenStream.getVideoTracks(),
  ...dest.stream.getAudioTracks(),
]);

Reading Track Settings After Capture

const [track] = screenStream.getVideoTracks();
const settings = track.getSettings();

console.log(settings.displaySurface); // 'monitor' | 'window' | 'browser'
console.log(settings.width);
console.log(settings.height);
console.log(settings.frameRate);
console.log(settings.logicalSurface);  // boolean — is the full logical surface captured?
console.log(settings.cursor);          // 'always' | 'motion' | 'never'

Common Screen Sharing Gotchas

  • No gesture = InvalidStateErrorgetDisplayMedia throws if not triggered by a user event.
  • User can stop sharing at any time from the browser's "Stop sharing" bar — always handle track.onended.
  • Screen content may change resolution when the user resizes a window — the track settings update dynamically; listen for constraint changes.
  • Safari supports getDisplayMedia from Safari 13 (macOS) but not on iOS (as of 2024 iOS 18 remains restricted).
  • Firefox does not support system audio capture.
  • preferCurrentTab: true (Chrome 107+) pre-highlights the current tab in the picker but the user can still choose another surface.
  • High CPU on capture — screen capture at 60 fps is expensive; use frameRate: { max: 15 } for slide sharing, frameRate: { ideal: 60 } only for game/video streaming.
  • replaceTrack vs renegotiation — switching from camera to screen track with replaceTrack avoids a new offer/answer cycle, but both tracks must be the same kind (video).