WebRTC (Web Real-Time Communication) adalah API browser untuk koneksi langsung antara dua browser (peer-to-peer), untuk audio dan video, atau data apa pun lewat data channel. Semua yang lewat dienkripsi. Bayangin Bu Rini di meja kasir dan Mas Eko di mesin cuci mau saling lempar catatan langsung. Sebelum bisa, mereka perlu bertukar "alamat dan cara bicara" lewat kantor (server). Begitu masing-masing tahu cara menjangkau yang lain, kantornya gak ikut campur lagi.
🧪 Tebakan cepat: Tablet Bu Rini dan tablet Mas Eko sudah tersambung lewat data channel WebRTC, dengan server Socket.IO laundry sebagai perantara awalnya. Lalu koneksi Socket.IO Bu Rini diputus. Sampaikah pesan berikutnya dari Bu Rini ke tablet Mas Eko? Jawabannya ada di bagian "Signaling lewat Socket.IO yang sudah ada".
Dua peer dan satu data channel
Setiap ujung punya satu RTCPeerConnection. Yang memulai (Bu Rini) membuat data channel, membuat offer, dan yang menerima (Mas Eko) membalas answer. Offer dan answer berbentuk SDP (Session Description Protocol): teks yang menjelaskan apa yang mau dikirim dan kunci enkripsinya.
const rini = new RTCPeerConnection()
const eko = new RTCPeerConnection()
// di halaman yang sama, "kirim ke peer lain" cukup panggil fungsinya langsung
rini.onicecandidate = (e) => e.candidate && eko.addIceCandidate(e.candidate)
eko.onicecandidate = (e) => e.candidate && rini.addIceCandidate(e.candidate)
const channel = rini.createDataChannel('papan')
channel.onopen = () => channel.send(JSON.stringify({ id: 12, status: 'Siap diambil' }))
eko.ondatachannel = (e) => { e.channel.onmessage = (m) => console.log('eko dapat', m.data) }
rini.createOffer()
.then((offer) => rini.setLocalDescription(offer).then(() => eko.setRemoteDescription(offer)))
.then(() => eko.createAnswer())
.then((answer) => eko.setLocalDescription(answer).then(() => rini.setRemoteDescription(answer)))
Dijalankan di Chromium 147 (dan juga di Firefox 146 serta WebKit 26.4):
24 ms offer type offer sdp bytes 458
29 ms rini candidate candidate:4100380318 1 udp 2113937151 54fed8ec-...-7dc9ef9b0b62.local 63524 typ host ...
80 ms rini connectionState connected
81 ms rini channel open, readyState open maxMessageSize 262144
82 ms eko got {"id":12,"status":"Siap diambil"}
- Isi offer-nya antara lain
m=application 9 UDP/DTLS/SCTP webrtc-datachannel(data channel di atas SCTP yang dienkripsi DTLS),a=fingerprint:sha-256 ...(sidik kunci enkripsi), dana=max-message-size:262144(256 KB per pesan di Chromium dan WebKit). - Urutannya wajib.
createAnswer()sebelum menerima offer melemparInvalidStateError: ... PeerConnection cannot create an answer in a state other than have-remote-offer or have-local-pranswer.channel.send()sebelumopenjuga melemparInvalidStateError.
ICE, NAT, STUN, dan TURN
ICE candidate adalah satu alamat yang mungkin bisa dipakai peer lain untuk menjangkaumu (IP, port, protokol). ICE mencoba pasangan-pasangan kandidat sampai ada yang tembus.
- Host candidate: alamat di jaringan lokal. Browser menyembunyikan IP lokalnya di balik nama acak
...local(mDNS), seperti di output tadi. Di Wi-Fi laundry yang sama, ini sudah cukup. - NAT: router rumah atau operator yang membuat banyak perangkat berbagi satu IP publik. Perangkat di belakang NAT gak tahu alamat publiknya sendiri.
- STUN: server kecil yang menjawab "dari luar, kamu terlihat sebagai IP:port ini". Hasilnya kandidat
srflx. Murah, karena cuma tanya-jawab sekali. - TURN: server yang meneruskan setiap byte kalau koneksi langsung gak tembus (NAT yang ketat, firewall kantor). Karena semua lalu lintas lewat sana, TURN bayar bandwidth. Contoh harga Oktober 2026: Cloudflare Realtime TURN $0,05 per GB keluar (gratis kalau dipakai bersama SFU mereka).
const pc = new RTCPeerConnection({
iceServers: [
{ urls: 'stun:stun.example.com:3478' },
{ urls: 'turn:turn.example.com:3478', username: 'laundry', credential: 'rahasia-sementara' },
],
})
Signaling lewat Socket.IO yang sudah ada
WebRTC sengaja gak menentukan cara mengantar offer, answer, dan kandidat. Itu disebut signaling, dan kamu yang membuatnya. Server Socket.IO laundry sudah ada, jadi cukup diteruskan:
// server: cuma meneruskan, gak membaca isinya
io.on('connection', (socket) => {
socket.join('rtc:papan')
socket.to('rtc:papan').emit('peer-siap')
socket.on('signal', (data) => socket.to('rtc:papan').emit('signal', data))
})
// kedua tablet
pc.onicecandidate = (e) => { if (e.candidate) socket.emit('signal', { candidate: e.candidate }) }
socket.on('signal', async (d) => {
if (d.sdp) {
await pc.setRemoteDescription(d.sdp)
if (d.sdp.type === 'offer') {
await pc.setLocalDescription(await pc.createAnswer())
socket.emit('signal', { sdp: pc.localDescription })
}
} else if (d.candidate) await pc.addIceCandidate(d.candidate)
})
// tablet Bu Rini, saat menerima 'peer-siap':
// await pc.setLocalDescription(await pc.createOffer()); socket.emit('signal', { sdp: pc.localDescription })
Dicoba dengan dua halaman (Chromium 147 dengan Chromium 147, juga Chromium 147 dengan Firefox 146) dan server socket.io 4.8.4: server meneruskan 6 sinyal (offer, answer, 2 + 2 kandidat), total 1.942 byte JSON. Lalu socket.disconnect() di tablet Bu Rini, dan pesan berikutnya lewat data channel tetap sampai di tablet Mas Eko. Itu jawaban tebakannya: sampai. Server cuma dibutuhkan untuk berkenalan.
Kamera dan mikrofon
navigator.mediaDevices.getUserMedia({ video: true, audio: true }) meminta izin kamera dan mikrofon, lalu track-nya dipasang dengan pc.addTrack(). Fitur ini cuma ada di secure context (HTTPS atau localhost). Dicoba di ketiga browser lewat http://192.168.x.x: navigator.mediaDevices bernilai undefined, sedangkan RTCPeerConnection tetap ada. Video call dengan pelanggan di luar cakupan pelajaran ini.
Kapan WebRTC alat yang salah
- Server perlu tahu datanya. Status order harus tersimpan di database, jadi tetap lewat server (WebSocket atau SSE). Kabar status laundry gak butuh WebRTC.
- Satu ke banyak. Mengirim ke 10.000 HP berarti 10.000 koneksi dari satu tablet. Siaran butuh server (atau SFU, server yang meneruskan media).
- Jaringan yang memblokir UDP. Semua lewat TURN, dan kamu membayar bandwidth-nya.
WebRTC cocok untuk yang langsung antar dua orang dan berat atau sensitif latensi: panggilan suara dan video, berbagi layar, atau mengirim file besar antar tablet di Wi-Fi yang sama tanpa membebani server.
⚠️ Jebakan yang sering ketemu
- Kandidat gak diteruskan, atau datang sebelum offer.
addIceCandidatesebelumsetRemoteDescriptionmelemparInvalidStateError: ... The remote description was null. - Cuma diuji di jaringan yang sama. Di lapangan, NAT dan firewall menggagalkannya tanpa TURN.
getUserMediadi HTTP biasa.navigator.mediaDevicesgak ada di luar secure context.- Mengira signaling bagian dari WebRTC. Kamu yang membangunnya, dan sebaiknya di belakang autentikasi seperti event lain.
TL;DR: WebRTC menyambungkan dua browser langsung untuk media atau data channel, terenkripsi. RTCPeerConnection di tiap ujung, createDataChannel lalu offer dan answer berupa SDP, dan ICE candidate dari dua arah. Pengantar offer, answer, dan kandidat (signaling) kamu buat sendiri, misalnya lewat room Socket.IO; setelah tersambung, data gak lewat server lagi. Di jaringan lokal cukup host candidate; lewat internet butuh STUN untuk alamat publik, dan TURN (berbayar per GB) kalau jalur langsung gak tembus. Kamera dan mikrofon butuh secure context dan izin. Untuk kabar status yang harus tersimpan dan disiarkan ke banyak HP, tetap pakai server.