A complete WebRTC stack for Node.js
A complete WebRTC stack for Node.js servers, with a browser-compatible API. Use the same RTCPeerConnection, RTCDataChannel, and RTCRtpTransceiver you know from the browser — running natively on the server, with first-class support for production deployments.
Build SFUs, MCUs, recording servers, WHIP/WHEP endpoints, SIP gateways, conference backends, and any other server-side WebRTC system on a single, complete stack.
- Features
- Install
- Quick start
- API
- Architecture
- ICE modes: lite vs full
- Codec support
- Loss resilience
- Use cases
- Debugging
- Interoperability
- RFC compliance
- Sponsors
- License
- W3C WebRTC API —
RTCPeerConnection,RTCRtpSender/Receiver/Transceiver,RTCDataChannel,RTCDtlsTransport,RTCIceTransport,RTCSctpTransport,RTCCertificate,RTCDTMFSender - Full media stack — VP8, VP9, H.264, H.265, AV1 video; Opus audio; with packetization, jitter buffer, and a three-layer loss-recovery stack (NACK/RTX + RED + FlexFEC)
- ICE — full and lite modes, trickle ICE, ICE restart, host/srflx/relay candidates
- NAT traversal beyond the browser — resolves browsers' mDNS
.localcandidates (draft-ietf-mmusic-mdns-ice-candidates) and gathers gateway-assisted candidates via UPnP-IGD/NAT-PMP/PCP — direct P2P connections that a browser alone can't make, often with no STUN/TURN at all - DTLS-SRTP — AEAD AES-128-GCM and AES-256-GCM (RFC 7714) with AES-128-CM fallback; full handshake with certificate generation and reuse; keys derived via the RFC 5764 DTLS exporter
- Loss resilience — NACK/RTX retransmission, RED redundant audio (RFC 2198), and FlexFEC forward error correction (flexfec-03), all negotiated and wired automatically
- DataChannel — SCTP over DTLS, ordered/unordered, reliable/unreliable, full DCEP (RFC 8832)
- Simulcast — RFC 8853 with RID and
a=ssrc-group:SIMfallback - Bandwidth estimation — REMB and transport-cc feedback, delay-based estimator
- DTMF — full
RTCDTMFSenderper W3C:insertDTMF,ontonechange, RFC 4733 events on the audio stream — ready for SIP gateways - Active speaker detection — RFC 6464 ssrc-audio-level in both directions, exposed via
getSynchronizationSources() - Statistics — full W3C
getStats()with all RTC stat types - Server-grade routing —
WebRTCRouterfor shared UDP port (RFC 9443), supporting many peers on a single port like 443 - Pure JavaScript — no native bindings; runs anywhere Node runs
npm install webrtc-serverRequires Node.js 18 or newer.
import { RTCPeerConnection } from 'webrtc-server';
const pc = new RTCPeerConnection();
pc.ondatachannel = (event) => {
const channel = event.channel;
channel.onmessage = (e) => channel.send('echo: ' + e.data);
};
// Wire up signaling — exchange offer/answer with your client over WebSocket,
// HTTP (WHIP), or whatever transport you prefer.
pc.onicecandidate = (e) => {
if (e.candidate) signaling.send({ candidate: e.candidate });
};
signaling.on('offer', async (offer) => {
await pc.setRemoteDescription(offer);
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
signaling.send({ answer });
});
signaling.on('candidate', async (candidate) => {
await pc.addIceCandidate(candidate);
});That's it. Signaling is your responsibility (always is, by spec); everything else mirrors browser behavior.
Receive the browser's webcam stream and forward it elsewhere via the media-processing companion library:
import { RTCPeerConnection } from 'webrtc-server';
import { MediaRecorder, MediaStream } from 'media-processing';
import fs from 'node:fs';
const pc = new RTCPeerConnection();
pc.ontrack = (event) => {
const track = event.track;
console.log('got', track.kind, 'track');
if (track.kind === 'video') {
// track is a real MediaStreamTrack — pipe it to a recorder, transcoder,
// file, or RTMP egress using media-processing's tools.
const recorder = new MediaRecorder(new MediaStream([track]));
recorder.ondataavailable = (e) => fs.appendFileSync('out.webm', e.data);
recorder.start();
}
};
// ...signaling...For production servers handling many peers, you typically want all WebRTC traffic on a single UDP port (often 443 to bypass restrictive firewalls). Use WebRTCRouter:
import { RTCPeerConnection, WebRTCRouter } from 'webrtc-server';
const router = new WebRTCRouter({
announcedAddresses: ['203.0.113.5'], // your server's public IP
});
await router.listen(443);
// Every PeerConnection shares the router's socket and is demultiplexed
// by 5-tuple (fast path) or STUN USERNAME (first packet of a new peer).
function handleNewClient(signaling) {
const pc = new RTCPeerConnection({ router });
// ...usual signaling glue...
}The router handles many concurrent peers on one port. See WebRTCRouter for the shared-port mode (coexistence with TURN/QUIC on 443 per RFC 9443).
The API follows the W3C WebRTC specification as implemented by browsers. If something works in Chrome/Firefox/Safari, it should work here. This section highlights what's specific to webrtc-server; for general WebRTC API documentation, MDN's RTCPeerConnection page is a great reference.
import { RTCPeerConnection } from 'webrtc-server';
const pc = new RTCPeerConnection(configuration);All standard methods are implemented: createOffer, createAnswer, setLocalDescription, setRemoteDescription, addIceCandidate, addTrack, removeTrack, addTransceiver, getSenders, getReceivers, getTransceivers, createDataChannel, getStats, getConfiguration, setConfiguration, restartIce, close.
All standard events are emitted: ontrack, ondatachannel, onicecandidate, onicecandidateerror, onnegotiationneeded, onsignalingstatechange, oniceconnectionstatechange, onicegatheringstatechange, onconnectionstatechange.
The static method RTCPeerConnection.generateCertificate(algorithm) produces an RTCCertificate you can pass via configuration.certificates to reuse a long-lived identity across connections — useful for servers where re-generating certificates per connection is wasteful.
Legacy createOffer options are honored for older codebases: offerToReceiveAudio / offerToReceiveVideo behave per JSEP §5.1 (true ensures a receive-capable transceiver exists; false strips the receive direction from existing ones).
RTCRtpSender.getCapabilities(kind) / RTCRtpReceiver.getCapabilities(kind) return the real codec set — including per-codec sdpFmtpLine sourced from the same tables used in negotiation, so capabilities and on-the-wire SDP can never drift apart. Session descriptions follow RFC 3264 §8: the o= session version increments on every generated offer/answer.
The constructor accepts the standard W3C RTCConfiguration plus a few server-oriented extensions:
const pc = new RTCPeerConnection({
// ── Standard W3C ──
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'turn:turn.example.com', username: 'u', credential: 'p' },
],
iceTransportPolicy: 'all', // or 'relay'
bundlePolicy: 'max-bundle', // 'balanced' | 'max-bundle' | 'max-compat'
rtcpMuxPolicy: 'require',
iceCandidatePoolSize: 0,
certificates: [cert], // RTCCertificate from generateCertificate
// ── Server-side extensions ──
router: webRTCRouter, // share a UDP port — see WebRTCRouter
socket: existingDgramSocket, // bring your own UDP socket
socket6: existingIpv6Socket,
announcedAddresses: ['1.2.3.4'], // public IPs to advertise as host candidates
mode: 'lite', // ICE mode — see below
mdns: true, // resolve .local candidates — auto by mode, see below
portMapping: { description: 'MyApp' }, // gateway-assisted gathering — auto by mode
cert: pemString, key: pemString, // raw cert/key (alternative to certificates)
});When router or any socket is supplied, the connection auto-defaults to ICE lite mode. When no socket is supplied, it acts as a full-ICE client.
Tracks come from the companion media-processing package, which provides MediaStreamTrack, MediaStream, getUserMedia, encoders, decoders, and file/network sources:
import { getUserMedia } from 'media-processing';
const stream = await getUserMedia({ video: true, audio: true });
for (const track of stream.getTracks()) {
pc.addTrack(track, stream);
}You can also build tracks programmatically (frame generators, file readers, RTMP/RTSP ingest) — see the media-processing docs.
Codec preferences via transceiver.setCodecPreferences() are supported. Simulcast is configured via the sendEncodings parameter to addTransceiver:
pc.addTransceiver('video', {
direction: 'sendonly',
sendEncodings: [
{ rid: 'high', maxBitrate: 2_000_000 },
{ rid: 'med', maxBitrate: 500_000, scaleResolutionDownBy: 2 },
{ rid: 'low', maxBitrate: 150_000, scaleResolutionDownBy: 4 },
],
});Standard W3C interface. Both negotiated and in-band channels:
const dc = pc.createDataChannel('chat', {
ordered: true,
maxRetransmits: 3,
});
dc.onopen = () => dc.send('hello');
dc.onmessage = (e) => console.log(e.data);Backed by a full SCTP implementation: ordered/unordered delivery, reliable/partial-reliable, configurable max message size up to 256 KiB by default.
A peer that restarts its SCTP association over the same DTLS connection (RFC 4960 §5.2.2 / §5.2.4) is followed: every channel of the old association ends with an error (errorDetail: 'sctp-failure') and close, as after a remote ABORT, while the connection itself stays up and new channels open on the new association.
bufferedAmountcounts bytes queued bysend()that have not yet gone out on the network (W3C §6.2): it falls as chunks are first transmitted (or dropped before that), so pacing onbufferedamountlowreally waits for the transport. Lower-level:sctp.on('chunkUnbuffered', { streamId, ppid, bytes }).send()accepts a string,ArrayBuffer,Bufferor anyArrayBufferView(a typed array orDataViewsends its own bytes). The data is copied beforesend()returns — the caller may reuse its buffer at once.- Received binary messages each own their
ArrayBuffer(safe to transfer to a worker). - Flow control covers messages waiting to be delivered to the application too: a sender outrunning the
messagehandler is slowed down by the receive window instead of growing a queue. - A channel that finished closing releases its stream id: the peer can open a new channel on it, and local ids are reused once the id space runs out.
sender.dtmf returns a live RTCDTMFSender on audio senders once telephone-event is negotiated (it's in the default audio codec set, so any standards-following peer will accept it):
const sender = pc.getSenders().find((s) => s.track && s.track.kind === 'audio');
if (sender.dtmf.canInsertDTMF) {
sender.dtmf.ontonechange = (e) =>
console.log(e.tone === '' ? 'done' : 'playing ' + e.tone);
sender.dtmf.insertDTMF('123#', 100, 70); // tones, duration ms, gap ms
}Semantics match the browser: tone characters 0-9 A-D * #, , inserts a 2-second pause, invalid characters throw InvalidCharacterError, duration is clamped to 40–6000 ms, and calling insertDTMF again replaces the pending buffer. On the wire the events ride the audio stream's own SSRC and sequence space per RFC 4733 — fixed event timestamp, growing duration field, triple-sent end packet — so SIP gateways and telephony peers interoperate cleanly.
Every outgoing audio packet carries the RFC 6464 ssrc-audio-level header extension (computed per frame before encoding), and incoming levels are cached per SSRC. Read them with the standard API:
setInterval(() => {
for (const receiver of pc.getReceivers()) {
if (receiver.track.kind !== 'audio') continue;
for (const src of receiver.getSynchronizationSources()) {
// audioLevel is the W3C linear scale: 0.0 (silence) .. 1.0 (0 dBov)
if (src.audioLevel > 0.1) console.log('speaking:', src.source);
}
}
}, 200);This is how "who is talking" indicators work without decoding any audio — which also makes it the right primitive for SFU-side speaker selection.
const report = await pc.getStats();
for (const [id, stat] of report) {
if (stat.type === 'inbound-rtp') console.log(stat.kind, stat.packetsLost);
}All standard stat types are emitted: inbound-rtp, outbound-rtp, remote-inbound-rtp, remote-outbound-rtp, media-source, media-playout, codec, transport, candidate-pair, local-candidate, remote-candidate, certificate, data-channel, peer-connection, sctp-transport.
Shared-port routing for multi-peer servers. Two modes:
Mode 1: Router owns the socket (simple case — no other protocols on the port)
import { WebRTCRouter, RTCPeerConnection } from 'webrtc-server';
const router = new WebRTCRouter({
announcedAddresses: ['203.0.113.5'],
});
await router.listen(443);
const pc = new RTCPeerConnection({ router });
// All peers created with { router } share the one UDP socket on :443.Mode 2: External socket (coexist with TURN/QUIC on the same port — RFC 9443)
import dgram from 'node:dgram';
import { WebRTCRouter, RTCPeerConnection } from 'webrtc-server';
const socket = dgram.createSocket('udp4');
socket.bind(443);
const router = new WebRTCRouter({
socket,
announcedAddresses: ['203.0.113.5'],
});
socket.on('message', (msg, rinfo) => {
if (router.dispatch(msg, rinfo)) return; // routed to a WebRTC peer
// ...else hand off to your TURN or QUIC handler...
});
const pc = new RTCPeerConnection({ router });Inbound packets are routed by 5-tuple (cached fast path) or by parsing the STUN USERNAME attribute on the first packet of a new peer (slow path). The router survives ICE restarts and cleans up automatically when peers close.
router.getPeerCount(); // active peer count
router.hasSession(rinfo); // is this 5-tuple a known peer?
router.close(); // closes router-owned sockets onlyICE over TCP (RFC 6544, passive) — for peers whose UDP is blocked. The router binds no TCP port; whoever accepts TCP connections asks once for a handler and passes it every connection that carries ICE (RFC 4571 framing — a 2-byte length before each packet):
import net from 'node:net';
const iceTcp = router.tcp(); // { port } — default: the UDP socket's port
net.createServer(iceTcp).listen(443);From that call on, every new peer connection also announces a tcp … typ host tcptype passive candidate per address, below its UDP candidate in priority, so a peer that can reach UDP still picks UDP. A connection is tied to the peer connection whose ufrag its first STUN check names; one that sends anything else first, or nothing within 5 seconds, is closed. On a port shared with TLS or TURN over TCP, tell them apart by the first bytes: TLS opens with 0x16, TURN with STUN (magic cookie at bytes 4–7), ICE-TCP with a length and then STUN (cookie at bytes 6–9).
A TURN server on the same process — when a browser can only reach this server through TURN, and that TURN server is turn-server in the same process, give it to the router:
import { createServer } from 'turn-server';
const turn = createServer({ socket, relay: { externalIp: '203.0.113.5' } });
const router = new WebRTCRouter({ socket, announcedAddresses: ['203.0.113.5'], turn });A relay whose peer is this router (one of announcedAddresses, on the router's UDP port) then hands its datagrams to the router in memory, and the router's answers go back into the relay the same way — instead of leaving for the network and coming back in on the shared port, which a home router often does not turn around. Nothing else changes: TURN's permissions, hooks and counters apply as before, and a relay to any other peer goes out to the network. router.close() detaches it. Without turn, the router behaves exactly as before.
The SDP layer the connection negotiates with is exported for inspection — what a remote description offers, as this library reads it:
import { SDP } from 'webrtc-server';
const offer = SDP.parseOffer(sdpString); // SDP.parseAnswer for an answer
offer.bundleMids; // ['0', '1']
for (const m of offer.media) {
console.log(m.mid, m.type, m.direction, m.codecs.map((c) => c.name));
}Besides parseOffer / parseAnswer: parseCandidate / buildCandidateString, extractCodecs, negotiateCodecs, getSimulcastStructure, the default codec and header-extension tables (DEFAULT_AUDIO_CODECS, DEFAULT_VIDEO_CODECS, CODEC_REGISTRY, …). This is an advanced API and isn't required for normal usage.
Changing SDP between negotiation steps (codec munging, header-extension filtering) is done on the text, with sdp-transform — the parser this library is built on — and the result passed to setRemoteDescription / setLocalDescription as usual:
import sdpTransform from 'sdp-transform';
const parsed = sdpTransform.parse(offer.sdp);
for (const m of parsed.media) {
// Drop PCMU: its rtpmap, fmtp and rtcp-fb lines, and its payload type.
const drop = new Set(m.rtp.filter((c) => c.codec === 'PCMU').map((c) => c.payload));
m.rtp = m.rtp.filter((c) => !drop.has(c.payload));
m.fmtp = (m.fmtp || []).filter((f) => !drop.has(f.payload));
m.rtcpFb = (m.rtcpFb || []).filter((f) => !drop.has(Number(f.payload)));
m.payloads = m.rtp.map((c) => c.payload).join(' ');
}
await pc.setRemoteDescription({ type: 'offer', sdp: sdpTransform.write(parsed) });For codec order alone, transceiver.setCodecPreferences() does it without touching SDP.
webrtc-server is built on a stack of focused libraries, each independently usable:
┌─────────────────────────────────────────────────┐
│ webrtc-server (this package) │
│ RTCPeerConnection, signaling, SDP, glue │
└─────────┬──────────────┬──────────────┬─────────┘
│ │ │
┌──────▼──────┐ ┌─────▼────┐ ┌───────▼──────┐
│ rtp-packet │ │ lemon-tls│ │ turn-server │
│ (RTP/RTCP, │ │ (DTLS) │ │ (ICE agent, │
│ SRTP) │ │ │ │ STUN/TURN) │
└──────┬──────┘ └──────────┘ └──────────────┘
│
┌──────▼─────────────┐
│ media-processing │
│ (codecs, tracks, │
│ getUserMedia) │
└────────────────────┘
Each lower-level library is published separately, so you can build narrower products (RTP parser, RTSP server, SIP softphone) without pulling the whole WebRTC stack.
WebRTC peers come in two flavors:
- Full ICE (the browser default) — gathers host/srflx/relay candidates, runs the full connectivity check matrix, sends keepalives. Right for clients behind NATs.
- ICE Lite (the standard server choice) — assumes a public IP, skips connectivity-check initiation, just responds to peer probes. Cheaper, simpler, and the right choice for most server deployments.
webrtc-server picks the right mode automatically based on configuration:
| Scenario | Resolved mode |
|---|---|
new RTCPeerConnection() (no socket/router) |
'full' |
new RTCPeerConnection({ router }) |
'lite' |
new RTCPeerConnection({ socket }) |
'lite' |
Explicit { mode: 'full' | 'lite' } |
as specified |
You can override by passing mode explicitly.
A browser has exactly three ways to reach a peer: host, STUN srflx, TURN relay. webrtc-server ships two more capabilities — enabled automatically where they make sense — that frequently turn "relay or fail" into a direct connection:
mDNS candidate resolution. Every modern browser conceals its LAN
addresses behind .local names (draft-ietf-mmusic-mdns-ice-candidates).
A Node peer that can't resolve them silently loses every direct LAN path —
same-office calls end up crossing the internet through a relay.
webrtc-server resolves them like a browser would (with the draft's safety
rules: UUID-only names, single-IP rule), and a client can also conceal its
own addresses with mdns: { register: true }.
Port-mapping assisted gathering. For full-ICE clients (Electron, CLI,
IoT — the P2P case), the stack asks the gateway for a UDP forwarding rule
via UPnP-IGD, NAT-PMP or PCP, and advertises the external address as an
srflx candidate. Unlike a STUN mapping, a forwarding rule is reachable by
any peer — it keeps working behind symmetric NAT, where classic srflx
fails and browsers fall back to TURN. It's the same NAT traversal
qBittorrent and Syncthing enable by default, and it means two home users
can often connect directly, with zero servers in either the media or
the discovery path. Exposure is no wider than ICE already implies: the
mapped port leads only to the ICE socket, which drops anything that isn't
credentialed STUN or the session's DTLS. Mappings carry finite auto-renewed
leases, are labeled in the router's UI (set portMapping: { description }),
and are removed on close. CGNAT is detected up front and reported via
onicecandidateerror instead of producing useless candidates.
Defaults follow the resolved ICE mode — zero configuration for the common cases, explicit config always wins:
| full (client) | lite (server with router/socket) |
|
|---|---|---|
mdns (resolve .local) |
on | off |
portMapping |
on | off |
mdns: { register } |
opt-in | — |
// Electron P2P client — everything above just works:
const pc = new RTCPeerConnection({ iceServers: [...] });
// Opt out, or customize:
const pc2 = new RTCPeerConnection({ portMapping: false });
const pc3 = new RTCPeerConnection({
mdns: { register: true }, // conceal our host IPs too
portMapping: { description: 'MyApp' }, // what the router UI shows
});Both capabilities live in turn-server's
ICE agent as lazy optional dependencies (mdns-local, port-mapper) —
an SFU deployment that never uses them never loads them.
| Codec | Direction | Notes |
|---|---|---|
| VP8 | send + recv | Full simulcast support |
| VP9 | send + recv | Including SVC layers |
| H.264 | send + recv | Constrained Baseline + Main profiles |
| H.265 | send + recv | RFC 7798; negotiated with Safari natively |
| AV1 | send + recv | With Dependency Descriptor |
| Opus | send + recv | Stereo, in-band FEC, DTX — negotiated fmtp is applied to the encoder |
| RED | send + recv | RFC 2198 redundant audio, offered by default (Chrome-compatible) |
| FlexFEC | send + recv | flexfec-03, own SSRC + a=ssrc-group:FEC-FR |
| DTMF | send + recv | RFC 4733 — full RTCDTMFSender on send, event parsing on receive |
Frame production and consumption is handled by the media-processing companion package — webrtc-server itself handles the wire format, encryption, and negotiation.
Three complementary recovery layers run automatically once negotiated — no configuration needed:
| Layer | Mechanism | Cost | Recovers |
|---|---|---|---|
| NACK / RTX (RFC 4585/4588) | Receiver requests retransmission; sender replays from a ring buffer on the RTX SSRC | One RTT | Any loss, if the packet is still buffered |
| RED (RFC 2198, audio) | Each packet carries the previous Opus frame as redundancy | +1 frame of bitrate | Single losses with zero added latency |
| FlexFEC (flexfec-03, video) | XOR repair packet per group on a dedicated SSRC, paired via a=ssrc-group:FEC-FR |
~1/groupSize bitrate overhead | One loss per group, zero RTT |
Recovered packets are re-injected through the normal receive path, so jitter buffering, NACK bookkeeping, and statistics all see them as regular media. FEC packets themselves are excluded from retransmission and congestion-control accounting, matching libwebrtc behavior. Interop verified against the formats Chrome ships (RED at PT 63 with fmtp <pt>/<pt>, flexfec-03 with repair-window).
- SFU and MCU media servers — full control over routing, simulcast layer selection, and per-peer policy
- Recording servers — server-side recording of WebRTC sessions to file, S3, or other storage
- WHIP/WHEP endpoints — HTTP-based WebRTC ingest and egress
- Live streaming ingest — receive browser streams and forward to RTMP, HLS, or other peers
- Conference backends — build conferencing logic on a WebRTC peer that speaks the same API as the participants
- SIP gateways — bridge WebRTC ↔ SIP/RTP, with DTMF support
- IoT and remote-camera servers — low-latency video from edge devices to browsers
- Browser automation testing — drive WebRTC stacks from Node test runners
Set WEBRTC_DEBUG=1 to enable diagnostic output covering signaling state transitions, RTP/RTCP routing decisions, ICE candidate gathering, DTLS handshake progress, SRTP profile selection (answering 0x0007 / SRTP session ready), FlexFEC encoder/decoder lifecycle, and SCTP send paths:
WEBRTC_DEBUG=1 node server.jsThis produces verbose output and is intended for development; leave it off in production.
Verified against every implementation in sipsorcery/webrtc-interop, in both the client and server roles.
Peer Connection Test
| client | server | |
|---|---|---|
| pion (Go) | ✔ | ✔ |
| aiortc (Python) | ✔ | ✔ |
| libdatachannel (C++) | ✔ | ✔ |
| SIPSorcery (C#) | ✔ | ✔ |
| webrtc-rs (Rust) | ✔ | ✔ |
| werift (TypeScript) | ✔ | ✔ |
| libwebrtc (Chromium m132) | ✔ | — |
| GStreamer | ✔ | — |
| Janus | ✔ | — |
Data Channel Echo Test
| client | server | |
|---|---|---|
| libdatachannel | ✔ | ✔ |
| SIPSorcery | ✔ | ✔ |
| werift | ✔ | ✔ |
Also verified against Chrome directly via the browser test page.
- RFC 2198 — RTP Payload for Redundant Audio Data (RED)
- RFC 3264 — Offer/Answer Model (including o= version discipline)
- RFC 3550 — RTP: A Transport Protocol for Real-Time Applications
- RFC 3711 — Secure Real-time Transport Protocol (SRTP)
- RFC 4585 — Extended RTP Profile for RTCP-Based Feedback (NACK, PLI)
- RFC 4588 — RTP Retransmission Payload Format (RTX)
- RFC 4733 — RTP Payload for DTMF Digits
- RFC 4566 — Session Description Protocol (SDP)
- RFC 5245 / RFC 8445 — Interactive Connectivity Establishment (ICE)
- draft-ietf-mmusic-mdns-ice-candidates — mDNS
.localcandidate resolution and concealment (the mechanism browsers ship; never published as an RFC) - RFC 5763 / RFC 5764 — DTLS-SRTP key exchange
- RFC 5761 — Multiplexing RTP and RTCP
- RFC 6184 — RTP Payload Format for H.264
- RFC 6347 — DTLS 1.2
- RFC 6464 — RTP Header Extension for Client-to-Mixer Audio Level
- RFC 7587 — RTP Payload Format for Opus
- RFC 7714 — AES-GCM for SRTP
- RFC 7741 — RTP Payload Format for VP8
- RFC 7798 — RTP Payload Format for H.265/HEVC
- RFC 8285 — RTP header extensions (two-byte format)
- RFC 8829 — JSEP (offer/answer model)
- RFC 8831 — WebRTC Data Channels
- RFC 8832 — DCEP (Data Channel Establishment Protocol)
- RFC 8841 — SCTP-Based Media Transport
- RFC 8852 — RID (RTP Stream Identifier)
- RFC 8853 — Simulcast (with
a=simulcastand RID) - RFC 9443 — Multiplexing scheme updates for shared UDP ports
- draft-ietf-payload-flexible-fec-scheme-03 — FlexFEC (the wire format Chrome ships)
- AOMedia AV1 RTP Specification
webrtc-server is an evenings-and-weekends project. Building a complete WebRTC stack in pure JavaScript is a significant undertaking — support development via GitHub Sponsors or by sharing the project.
Apache License 2.0
Copyright © 2026 colocohen
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.