belajarkoding Platform belajar web development Indonesia. Artikel, cheat sheets, roadmap, dan code challenges untuk developer Indonesia.
© 2026 BelajarKoding. All rights reserved.
Bagian dari ekosistem Galih Pratama
WebRTC Cheat Sheet Referensi cepat WebRTC API. getUserMedia, RTCPeerConnection, signaling, ICE, DataChannel, dan STUN/TURN. Perfect buat developer yang bangun real-time P2P apps.
JavaScript 10 min read 1.818 kata
Silakan
login atau
daftar untuk membaca cheat sheet ini.
Baca Cheat Sheet Lengkap Login atau daftar akun gratis untuk membaca cheat sheet ini.
WebRTC Cheat Sheet - BelajarKoding | BelajarKoding
# Konsep Dasar
# Arsitektur WebRTC
Browser A Browser B
| |
|--- Signaling (WebSocket) ------|
| (SDP offer/answer, ICE) |
| |
|=== Direct P2P (Media/Data) ====|
| (setelah connection established) |
Komponen utama:
Media Capture : getUserMedia (kamera, mikrofon, screen)
RTCPeerConnection : Koneksi P2P antara browser
RTCDataChannel : Kirim data arbitrer P2P (text, binary)
: Pertukaran metadata (bukan bagian WebRTC, harus implementasi sendiri)
Signaling
Akses kamera dan mikrofon browser.
// Basic: kamera + mikrofon
const stream = await navigator.mediaDevices. getUserMedia ({
video: true ,
audio: true ,
});
// Spesifik: HD video
const stream
# getDisplayMedia (Screen Sharing)// Share seluruh screen
const displayStream = await navigator.mediaDevices. getDisplayMedia ({
video: {
cursor: 'always' , // 'always' | 'motion' | 'hidden'
displaySurface: 'monitor' , // 'monitor' | 'window' | 'browser' | 'application'
const stream = await navigator.mediaDevices. getUserMedia ({ video: true , audio: true });
// Get individual tracks
const videoTrack = stream.
// List semua media devices
const devices = await navigator.mediaDevices. enumerateDevices ();
devices. forEach ( device => {
console. log ( `${ device . kind }: ${ device .
# Basic Connection (Caller Side)const configuration = {
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{
urls: 'turn:turn.example.com:3478'
const pc = new RTCPeerConnection (configuration);
// Add local tracks
const stream = await navigator.mediaDevices. getUserMedia ({ video: true , audio: true });
// Di caller side, setelah kirim offer
signaling. onmessage = async ( msg ) => {
if (msg.type === 'answer' ) {
await pc. setRemoteDescription (msg.sdp);
}
if (msg.type === 'ice' ) {
# ICE (Interactive Connectivity Establishment)pc. onicecandidate = ( event ) => {
if (event.candidate) {
const c = event.candidate.candidate;
// host: direct local address
// srflx: STUN server reflexive (public address via NAT)
// Caller creates channel
const pc = new RTCPeerConnection (configuration);
const channel = pc. createDataChannel ( 'chat' , {
ordered: true , // reliable, in-order delivery (TCP-like)
# DataChannel Send/Receive// Send text
channel. send ( 'Hello World' );
// Send JSON
channel. send ( JSON . stringify
# Perfect Negotiation PatternPattern yang mencegah glare (kedua peer offer bersamaan).
let makingOffer = false ;
let ignoreOffer = false ;
const polite = true ; // satu side polite, lainnya impolite
# Renegotiation (Add/Remove Tracks)// Add a track to existing connection
async function addScreenShare () {
const screenStream = await navigator.mediaDevices. getDisplayMedia ({ video: true });
// Add to existing peer connection
screenStream.
// Get connection statistics
const stats = await pc. getStats ();
stats. forEach ( report => {
if (report.type === 'inbound-rtp'
// 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' },
],
};
# Self-hosted TURN (coturn)# 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
WebRTC P2P terbatas pada beberapa participant. Buat group calls, pakai SFU (Selective Forwarding Unit).
// 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 // Feature detection
if ( ! navigator.mediaDevices?.getUserMedia) {
alert ( 'Browser tidak mendukung WebRTC' );
}
// Adapter.js untuk cross-browser compat
import adapter from 'webrtc-adapter' ;
// Polyfill check
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.
=
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 ,
});
},
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' );
});
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' ));
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 } },
});
,
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 });
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);
}
};
await
pc.
addIceCandidate
(msg.candidate);
}
};
// 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 ();
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!' );
};
({ 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 );
}
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);
}
};
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!
}
) {
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 );
// 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)
};
cert = /etc/ssl/certs/turn_cert.pem
pkey = /etc/ssl/private/turn_key.pem
no-cli
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