BelajarKoding Logobelajarkoding

Platform belajar web development Indonesia. Artikel, cheat sheets, roadmap, dan code challenges untuk developer Indonesia.

Navigasi

  • Artikel
  • Cheat Sheets
  • Roadmap
  • Challenges
  • Pricing
  • Search

Produk Lain

  • JagoHermes
  • KelasClaude
  • KilatKoding
  • BelajarVibeCoding
  • JualanKoding

Support

  • Privacy Policy
  • Terms of Service
  • Email

© 2026 BelajarKoding. All rights reserved.

Galih PratamaBagian dari ekosistem Galih Pratama
belajarkoding LogobyGalih Pratama
RoadmapArtikelCheat SheetsChallengesUpgrade
belajarkoding LogobyGalih Pratama
RoadmapArtikelCheat SheetsChallengesUpgrade
belajarkoding LogobyGalih Pratama
RoadmapArtikelCheat SheetsChallengesUpgrade

Daftar Isi

Konsep DasarArsitektur WebRTCMedia CapturegetUserMediagetDisplayMedia (Screen Sharing)Track ManagementenumerateDevicesRTCPeerConnectionBasic Connection (Caller Side)Answerer SideCaller: Receive AnswerICE (Interactive Connectivity Establishment)ICE Candidate TypesDataChannelCreate DataChannelDataChannel Send/ReceivePerfect Negotiation PatternRenegotiation (Add/Remove Tracks)Stats MonitoringSTUN / TURN ServersSelf-hosted TURN (coturn)Multi-party (SFU)Browser CompatibilityGlossary
WebRTCReal-timeP2PWeb API

WebRTC Cheat Sheet

Referensi cepat WebRTC API. getUserMedia, RTCPeerConnection, signaling, ICE, DataChannel, dan STUN/TURN. Perfect buat developer yang bangun real-time P2P apps.

JavaScript10 min read1.818 kata
Silakan login atau daftar untuk membaca cheat sheet ini.

#Konsep Dasar

#Arsitektur WebRTC

text
Browser A                       Browser B
   |                                |
   |--- Signaling (WebSocket) ------|
   |    (SDP offer/answer, ICE)     |
   |                                |
   |=== Direct P2P (Media/Data) ====|
   |    (setelah connection established)  |

Komponen utama:

  1. Media Capture: getUserMedia (kamera, mikrofon, screen)
  2. RTCPeerConnection: Koneksi P2P antara browser
  3. RTCDataChannel: Kirim data arbitrer P2P (text, binary)
  4. Signaling: Pertukaran metadata (bukan bagian WebRTC, harus implementasi sendiri)

#Media Capture

#getUserMedia

Akses kamera dan mikrofon browser.

javascript
// Basic: kamera + mikrofon
const stream = await navigator.mediaDevices.getUserMedia({
  video: true,
  audio: true,
});
 
// Spesifik: HD video
const stream = await navigator.mediaDevices.getUserMedia({
  video: {
    width: { ideal: 1280 },
    height: { ideal: 720 },
    facingMode: 'user',        // 'user' (front) atau 'environment' (back)
    frameRate: { ideal: 30, max: 60 },
  },
  audio: {
    echoCancellation: true,
    noiseSuppression: true,
    autoGainControl: true,
  },
});
 
// Tampilkan di video element
const video = document.querySelector('video');
video.srcObject = stream;
await video.play();
 
// Audio only (voice call)
const audioStream = await navigator.mediaDevices.getUserMedia({
  audio: true,
  video: false,
});

#getDisplayMedia (Screen Sharing)

javascript
// Share seluruh screen
const displayStream = await navigator.mediaDevices.getDisplayMedia({
  video: {
    cursor: 'always',           // 'always' | 'motion' | 'hidden'
    displaySurface: 'monitor',  // 'monitor' | 'window' | 'browser' | 'application'
  },
  audio: true,                  // system audio
});
 
// Share dengan preferensi
const displayStream = await navigator.mediaDevices.getDisplayMedia({
  video: {
    width: { max: 1920 },
    height: { max: 1080 },
    frameRate: { max: 30 },
  },
  audio: false,
  preferCurrentTab: true,       // Chrome: prefer tab saat ini
  selfBrowserSurface: 'include', // 'include' | 'exclude'
});
 
// Handle stop sharing
displayStream.getVideoTracks()[0].addEventListener('ended', () => {
  console.log('User stopped sharing');
});

#Track Management

javascript
const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
 
// Get individual tracks
const videoTrack = stream.getVideoTracks()[0];
const audioTrack = stream.getAudioTracks()[0];
 
// Track properties
console.log(videoTrack.kind);        // 'video'
console.log(videoTrack.label);       // 'Default - Camera'
console.log(videoTrack.enabled);     // true
console.log(videoTrack.muted);       // false
console.log(videoTrack.readyState);  // 'live' | 'ended'
 
// Toggle mute
videoTrack.enabled = false;  // black screen, track tetap aktif
 
// Stop track (release camera)
videoTrack.stop();
 
// Track constraints (apply runtime)
await videoTrack.applyConstraints({
  width: 640,
  height: 480,
  frameRate: 15,
});
 
// Listen untuk track events
videoTrack.addEventListener('ended', () => console.log('Camera stopped'));
videoTrack.addEventListener('mute', () => console.log('Track muted'));
videoTrack.addEventListener('unmute', () => console.log('Track unmuted'));

#enumerateDevices

javascript
// List semua media devices
const devices = await navigator.mediaDevices.enumerateDevices();
 
devices.forEach(device => {
  console.log(`${device.kind}: ${device.label} [${device.deviceId}]`);
});
// videoinput: Front Camera [abc123]
// audioinput: Default Microphone [def456]
// audiooutput: Speakers [ghi789]
 
// Switch camera
const newStream = await navigator.mediaDevices.getUserMedia({
  video: { deviceId: { exact: targetDeviceId } },
});

#RTCPeerConnection

#Basic Connection (Caller Side)

javascript
const configuration = {
  iceServers: [
    { urls: 'stun:stun.l.google.com:19302' },
    {
      urls: 'turn:turn.example.com:3478',
      username: 'user',
      credential: 'pass',
    },
  ],
  iceCandidatePoolSize: 10,
};
 
const pc = new RTCPeerConnection(configuration);
 
// Add local tracks
const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
stream.getTracks().forEach(track => {
  pc.addTrack(track, stream);
});
 
// Handle ICE candidates
pc.onicecandidate = (event) => {
  if (event.candidate) {
    // Kirim candidate ke remote peer via signaling server
    signaling.send({ type: 'ice', candidate: event.candidate });
  }
};
 
// Handle remote tracks
pc.ontrack = (event) => {
  const remoteVideo = document.querySelector('#remote');
  remoteVideo.srcObject = event.streams[0];
};
 
// Handle connection state
pc.onconnectionstatechange = () => {
  console.log('Connection state:', pc.connectionState);
  // 'new' | 'connecting' | 'connected' | 'disconnected' | 'failed' | 'closed'
};
 
// Create offer
const offer = await pc.createOffer({
  offerToReceiveVideo: true,
  offerToReceiveAudio: true,
});
await pc.setLocalDescription(offer);
 
// Kirim offer ke remote peer via signaling
signaling.send({ type: 'offer', sdp: offer });

#Answerer Side

javascript
const pc = new RTCPeerConnection(configuration);
 
// Add local tracks
const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
stream.getTracks().forEach(track => pc.addTrack(track, stream));
 
// Handle remote tracks
pc.ontrack = (event) => {
  remoteVideo.srcObject = event.streams[0];
};
 
pc.onicecandidate = (event) => {
  if (event.candidate) {
    signaling.send({ type: 'ice', candidate: event.candidate });
  }
};
 
// Receive offer from signaling
signaling.onmessage = async (msg) => {
  if (msg.type === 'offer') {
    await pc.setRemoteDescription(msg.sdp);
 
    const answer = await pc.createAnswer();
    await pc.setLocalDescription(answer);
 
    signaling.send({ type: 'answer', sdp: answer });
  }
 
  if (msg.type === 'ice') {
    await pc.addIceCandidate(msg.candidate);
  }
};

#Caller: Receive Answer

javascript
// Di caller side, setelah kirim offer
signaling.onmessage = async (msg) => {
  if (msg.type === 'answer') {
    await pc.setRemoteDescription(msg.sdp);
  }
 
  if (msg.type === 'ice') {
    await pc.addIceCandidate(msg.candidate);
  }
};

#ICE (Interactive Connectivity Establishment)

#ICE Candidate Types

javascript
pc.onicecandidate = (event) => {
  if (event.candidate) {
    const c = event.candidate.candidate;
    // host: direct local address
    // srflx: STUN server reflexive (public address via NAT)
    // relay: TURN relay (traffic through TURN server)
    console.log(`ICE: ${c}`);
  }
};
 
// ICE gathering state
pc.onicegatheringstatechange = () => {
  console.log('ICE gathering:', pc.iceGatheringState);
  // 'new' | 'gathering' | 'complete'
};
 
// ICE connection state
pc.oniceconnectionstatechange = () => {
  console.log('ICE connection:', pc.iceConnectionState);
  // 'new' | 'checking' | 'connected' | 'completed' | 'failed' | 'disconnected' | 'closed'
};
 
// Trickle ICE: kirim candidate begitu tersedia (lebih cepat)
// Non-trickle: tunggu semua candidate terkumpul, baru kirim SDP
 
// Force ICE restart
await pc.restartIce();

#DataChannel

#Create DataChannel

javascript
// Caller creates channel
const pc = new RTCPeerConnection(configuration);
const channel = pc.createDataChannel('chat', {
  ordered: true,              // reliable, in-order delivery (TCP-like)
  maxRetransmits: 3,          // atau maxPacketLifeTime: 3000 (UDP-like)
});
 
channel.onopen = () => {
  console.log('DataChannel open');
  channel.send('Hello via DataChannel!');
};
 
channel.onmessage = (event) => {
  console.log('Received:', event.data);
};
 
channel.onclose = () => console.log('DataChannel closed');
channel.onerror = (err) => console.error('DataChannel error:', err);
 
// Answerer receives channel
pc.ondatachannel = (event) => {
  const channel = event.channel;
  channel.onmessage = (e) => console.log('Received:', e.data);
  channel.onopen = () => channel.send('Hi back!');
};

#DataChannel Send/Receive

javascript
// Send text
channel.send('Hello World');
 
// Send JSON
channel.send(JSON.stringify({ type: 'message', text: 'hi' }));
 
// Send binary (ArrayBuffer)
const buffer = new ArrayBuffer(4);
const view = new DataView(buffer);
view.setInt32(0, 42);
channel.send(buffer);
 
// Send Blob (large files)
channel.send(blob);
 
// Check channel state
console.log(channel.readyState);  // 'connecting' | 'open' | 'closing' | 'closed'
console.log(channel.bufferedAmount); // bytes queued, belum terkirim
 
// Flow control (jangan overflow buffer)
function sendChunk(data) {
  if (channel.bufferedAmount > 1024 * 1024) { // 1MB limit
    setTimeout(() => sendChunk(data), 100);
    return;
  }
  channel.send(data);
}
 
// File transfer dengan chunking
const CHUNK_SIZE = 16384; // 16KB
function sendFile(file) {
  let offset = 0;
  const reader = new FileReader();
 
  reader.onload = (e) => {
    channel.send(e.target.result);
    offset += e.target.result.byteLength;
 
    if (offset < file.size) {
      readSlice(offset);
    }
  };
 
  function readSlice(o) {
    const slice = file.slice(o, o + CHUNK_SIZE);
    reader.readAsArrayBuffer(slice);
  }
 
  readSlice(0);
}

#Perfect Negotiation Pattern

Pattern yang mencegah glare (kedua peer offer bersamaan).

javascript
let makingOffer = false;
let ignoreOffer = false;
const polite = true; // satu side polite, lainnya impolite
 
pc.onnegotiationneeded = async () => {
  try {
    makingOffer = true;
    await pc.setLocalDescription();
    signaling.send({ description: pc.localDescription });
  } catch (err) {
    console.error(err);
  } finally {
    makingOffer = false;
  }
};
 
signaling.onmessage = async ({ description, candidate }) => {
  try {
    if (description) {
      const offerCollision = description.type === 'offer' &&
        (makingOffer || pc.signalingState !== 'stable');
 
      ignoreOffer = !polite && offerCollision;
      if (ignoreOffer) return;
 
      await pc.setRemoteDescription(description);
      if (description.type === 'offer') {
        await pc.setLocalDescription();
        signaling.send({ description: pc.localDescription });
      }
    } else if (candidate) {
      try {
        await pc.addIceCandidate(candidate);
      } catch (err) {
        if (!ignoreOffer) throw err;
      }
    }
  } catch (err) {
    console.error(err);
  }
};

#Renegotiation (Add/Remove Tracks)

javascript
// Add a track to existing connection
async function addScreenShare() {
  const screenStream = await navigator.mediaDevices.getDisplayMedia({ video: true });
 
  // Add to existing peer connection
  screenStream.getTracks().forEach(track => {
    const sender = pc.addTrack(track, screenStream);
    // Track sender untuk remove nanti
    screenSender = sender;
  });
 
  // Renegotiation otomatis dipicu oleh onnegotiationneeded
}
 
// Remove a track
function stopScreenShare() {
  if (screenSender) {
    pc.removeTrack(screenSender);
    // onnegotiationneeded akan trigger lagi
  }
}
 
// Replace track (switch camera tanpa renegotiation)
async function switchCamera(newStream) {
  const newVideoTrack = newStream.getVideoTracks()[0];
  const sender = pc.getSenders().find(s => s.track.kind === 'video');
  await sender.replaceTrack(newVideoTrack);
  // Tidak butuh renegotiation!
}

#Stats Monitoring

javascript
// Get connection statistics
const stats = await pc.getStats();
 
stats.forEach(report => {
  if (report.type === 'inbound-rtp') {
    console.log('Received:', {
      packetsLost: report.packetsLost,
      jitter: report.jitter,
      bytesReceived: report.bytesReceived,
    });
  }
 
  if (report.type === 'outbound-rtp') {
    console.log('Sent:', {
      packetsSent: report.packetsSent,
      bytesSent: report.bytesSent,
      targetBitrate: report.targetBitrate,
    });
  }
 
  if (report.type === 'candidate-pair' && report.state === 'succeeded') {
    console.log('Connection:', {
      currentRoundTripTime: report.currentRoundTripTime,
      availableOutgoingBitrate: report.availableOutgoingBitrate,
    });
  }
});
 
// Monitor periodically
setInterval(async () => {
  const stats = await pc.getStats();
  // Process stats...
}, 1000);

#STUN / TURN Servers

javascript
// STUN: discovery public IP address (gratis)
const config = {
  iceServers: [
    { urls: 'stun:stun.l.google.com:19302' },
    { urls: 'stun:stun1.l.google.com:19302' },
    { urls: 'stun:stun2.l.google.com:19302' },
  ],
};
 
// TURN: relay server untuk NAT traversal (berbayar, self-hosted)
const config = {
  iceServers: [
    { urls: 'stun:stun.l.google.com:19302' },
    {
      urls: [
        'turn:turn.example.com:3478?transport=udp',
        'turn:turn.example.com:3478?transport=tcp',
        'turns:turn.example.com:5349?transport=tcp', // TLS
      ],
      username: 'username',
      credential: 'password',
    },
  ],
  iceTransportPolicy: 'relay', // force TURN only (testing)
};

#Self-hosted TURN (coturn)

bash
# Install coturn
sudo apt install coturn
 
# /etc/turnserver.conf
listening-port=3478
tls-listening-port=5349
realm=example.com
server-name=turn.example.com
user=username:password
lt-cred-mech
fingerprint
cert=/etc/ssl/certs/turn_cert.pem
pkey=/etc/ssl/private/turn_key.pem
no-cli

#Multi-party (SFU)

WebRTC P2P terbatas pada beberapa participant. Buat group calls, pakai SFU (Selective Forwarding Unit).

javascript
// Pattern: setiap participant punya RTCPeerConnection ke SFU
// SFU (mediasoup, Janus, LiveKit, Twilio) relay tracks antar participant
 
// Client connects ke SFU
const sfuSocket = new WebSocket('wss://sfu.example.com');
 
// SFU memberiin offer, client jawab dengan answer
// SFU handle routing tracks antar participant
 
// Library populer: LiveKit, mediasoup, Janus, Jitsi

#Browser Compatibility

javascript
// Feature detection
if (!navigator.mediaDevices?.getUserMedia) {
  alert('Browser tidak mendukung WebRTC');
}
 
// Adapter.js untuk cross-browser compat
import adapter from 'webrtc-adapter';
 
// Polyfill check
if (!window.RTCPeerConnection) {
  console.error('WebRTC not supported');
}
 
// Safari quirks
// - video element harus inline (playsinline attribute)
// <video playsinline autoplay></video>
 
// Firefox-specific
// - Simulcast perlu SDP munging
// - DataChannel: binaryType harus 'arraybuffer'
 
// Mobile
// - iOS requires user gesture untuk getUserMedia
// - Background tab: tracks mungkin pause
// - Multiple cameras: facingMode untuk switch

#Glossary

  • SDP (Session Description Protocol): Deskripsi media capabilities (codec, resolution, dll). Ditukar via signaling.
  • ICE Candidate: Alamat jaringan yang bisa dipake buat connect. Host (local), srflx (STUN), relay (TURN).
  • STUN: Server yang bantu browser nemuin public IP address. Gratis.
  • TURN: Relay server yang forward traffic kalau P2P gagal (NAT/firewall). Berbayar atau self-hosted.
  • Trickle ICE: Kirim ICE candidates begitu tersedia, bukan tunggu semua terkumpul. Lebih cepat.
  • Signaling: Proses pertukaran metadata (SDP, ICE candidates). Bukan bagian WebRTC, pakai WebSocket/HTTP.
  • SFU: Selective Forwarding Unit. Server yang relay media streams untuk multi-party calls.
  • DataChannel: Channel P2P untuk kirim data arbitrer (text, binary) dengan kecepatan seperti WebSocket tapi tanpa server.
  • NAT Traversal: Teknik buat connect melalui NAT/firewall. ICE, STUN, TURN adalah solusinya.
  • Perfect Negotiation: Pattern yang handle race condition saat kedua peer kirim offer bersamaan.
  • Simulcast: Kirim multiple quality levels dari stream yang sama. SFU pilih quality yang sesuai per receiver.
  • Renegotiation: Update koneksi P2P yang udah ada (add/remove tracks) tanpa rebuild connection.

Baca Cheat Sheet Lengkap

Login atau daftar akun gratis untuk membaca cheat sheet ini.

LoginDaftar Gratis
Share: