WebRTC Cheatsheet

Media Streams and Tracks

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

MediaStream

A MediaStream is a container of MediaStreamTrack objects (video and/or audio). Streams flow from sources (camera, mic, screen, canvas, audio element) through RTCPeerConnection to remote peers.

// Get stream from getUserMedia
const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });

// Inspect the stream
console.log(stream.id);           // unique UUID
console.log(stream.active);       // true if any tracks are live
console.log(stream.getTracks());  // all tracks (audio + video)
console.log(stream.getVideoTracks()); // [MediaStreamTrack, …]
console.log(stream.getAudioTracks()); // [MediaStreamTrack, …]

MediaStreamTrack

const [videoTrack] = stream.getVideoTracks();
const [audioTrack] = stream.getAudioTracks();

// Key properties
videoTrack.id;           // UUID
videoTrack.kind;         // 'video' | 'audio'
videoTrack.label;        // device label, e.g. 'Built-in FaceTime Camera'
videoTrack.enabled;      // mute/unmute without stopping
videoTrack.muted;        // true if track is muted by the source (read-only)
videoTrack.readyState;   // 'live' | 'ended'

// Mute (sends silence/black frame) — does NOT release hardware
audioTrack.enabled = false; // mute
audioTrack.enabled = true;  // unmute

// Stop — releases hardware, readyState → 'ended'
videoTrack.stop();

MediaStreamTrack Events

track.onmute   = () => console.log('track muted remotely');
track.onunmute = () => console.log('track unmuted');
track.onended  = () => console.log('track ended');

Building a Composite Stream

// Combine audio from one source with video from another
const videoStream = await navigator.mediaDevices.getUserMedia({ video: true });
const audioStream = await navigator.mediaDevices.getUserMedia({ audio: true });

const combined = new MediaStream([
  ...videoStream.getVideoTracks(),
  ...audioStream.getAudioTracks(),
]);

Track Constraints and Settings

const [track] = stream.getVideoTracks();

// What the browser actually delivered
const settings = track.getSettings();
// { width, height, frameRate, deviceId, facingMode, aspectRatio, … }

// What the browser can deliver (range)
const caps = track.getCapabilities();
// { width: { min, max }, height: { min, max }, frameRate: { min, max }, … }

// What constraints were applied
const constraints = track.getConstraints();

// Apply new constraints at runtime
await track.applyConstraints({ frameRate: { max: 15 } });
await track.applyConstraints({ width: 640, height: 480 });

Adding / Removing Tracks

// Add a track to the peer connection
const sender = pc.addTrack(videoTrack, stream);

// Remove (triggers renegotiation via onnegotiationneeded)
pc.removeTrack(sender);

// Swap track without renegotiation
await sender.replaceTrack(newTrack);
// Pass null to "black-hole" the sender without removing it
await sender.replaceTrack(null);

Attaching Streams to HTML Elements

// Video element
const videoEl = document.querySelector('video');
videoEl.srcObject = stream;
videoEl.autoplay = true;
videoEl.playsInline = true;  // required on iOS
videoEl.muted = true;        // muted autoplays; unmuted requires gesture

// Audio element
const audioEl = document.querySelector('audio');
audioEl.srcObject = audioStream;
audioEl.play();

// Mirror local camera (flip horizontally with CSS)
videoEl.style.transform = 'scaleX(-1)';

Streams from Non-Camera Sources

Canvas as a Video Source

const canvas = document.getElementById('myCanvas');
const canvasStream = canvas.captureStream(30); // 30 fps
const [canvasTrack] = canvasStream.getVideoTracks();
pc.addTrack(canvasTrack, canvasStream);

Audio Element as a Source

const audioCtx = new AudioContext();
const source = audioCtx.createMediaElementSource(document.getElementById('audio'));
const dest = audioCtx.createMediaStreamDestination();
source.connect(dest);
const [processedAudioTrack] = dest.stream.getAudioTracks();
pc.addTrack(processedAudioTrack, dest.stream);

Web Audio API Pipeline

const audioCtx = new AudioContext();
const micSource = audioCtx.createMediaStreamSource(stream);

// Add effects
const gainNode = audioCtx.createGain();
gainNode.gain.value = 1.5;

const dest = audioCtx.createMediaStreamDestination();
micSource.connect(gainNode).connect(dest);

const processedStream = dest.stream;

MediaStreamTrackGenerator (Insertable Streams)

// Transform video frames before sending
const { readable, writable } = new MediaStreamTrackProcessor({ track: videoTrack });
const { readable: outReadable, writable: outWritable } = new MediaStreamTrackGenerator({ kind: 'video' });

const transformer = new TransformStream({
  async transform(videoFrame, controller) {
    // Manipulate the frame
    controller.enqueue(videoFrame);
    videoFrame.close();
  }
});

readable.pipeThrough(transformer).pipeTo(outWritable);

// Send the processed track
pc.addTrack(outReadable.getReader().read().value, stream);

RTCRtpSender — Encoding Control

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

// Read current encoding parameters
const params = sender.getParameters();

// Adjust bitrate and framerate
params.encodings[0].maxBitrate = 1_000_000;    // 1 Mbps
params.encodings[0].maxFramerate = 24;
params.encodings[0].scaleResolutionDownBy = 1;  // 1 = full resolution, 2 = half

// Priority hint
params.encodings[0].networkPriority = 'high';  // 'very-low'|'low'|'medium'|'high'
params.encodings[0].priority = 'high';

await sender.setParameters(params);

RTCRtpReceiver — Received Track Access

pc.ontrack = ({ track, streams, receiver }) => {
  // track — the MediaStreamTrack
  // streams — associated MediaStream array
  // receiver — RTCRtpReceiver
  remoteVideo.srcObject = streams[0];

  // Read receiver stats
  receiver.getStats().then(stats => stats.forEach(r => console.log(r)));

  track.onunmute = () => {
    // Remote unmuted their track
    remoteVideo.play();
  };
};

Checking Track Quality / Stats

const statsReport = await pc.getStats();

statsReport.forEach(report => {
  if (report.type === 'inbound-rtp' && report.kind === 'video') {
    console.log({
      fps:           report.framesPerSecond,
      decoded:       report.framesDecoded,
      dropped:       report.framesDropped,
      width:         report.frameWidth,
      height:        report.frameHeight,
      packetsLost:   report.packetsLost,
      jitter:        report.jitter,
      freezeCount:   report.freezeCount,
      freezeDuration: report.totalFreezesDuration,
    });
  }
});

Common Media Stream Gotchas

  • track.enabled = false mutes by sending silence/black; track.stop() terminates the track — stopped tracks cannot be restarted.
  • stream.active is true as long as any track is live; once all tracks end, it becomes false.
  • Tracks survive streams — a track can be in multiple streams simultaneously; stopping the stream object does nothing — stop the tracks.
  • Remote tracks start muted until data arrives; wait for track.onunmute before relying on videoEl.videoWidth.
  • Canvas captureStream(0)0 fps means frames only on requestAnimationFrame; use a positive value for continuous output.
  • replaceTrack(null) keeps the sender but sends nothing; use it to temporarily pause without SDP renegotiation.
  • iOS Safari requires playsInline, muted, and autoplay on <video> for local preview, and a user tap to start audio on remote.