API in Capabilities
WebRTC API
Published Updated
WebRTC carries audio, video, and arbitrary data directly between two browsers (or a browser and a media server) over encrypted UDP: RTCPeerConnection negotiates codecs and transports through an SDP offer/answer exchange, ICE finds a network path, addTrack() attaches MediaStreamTracks captured with getUserMedia(), and RTCDataChannel carries application messages over SCTP. The exchange of offers, answers, and ICE candidates (signalling) is left to the application; any channel the page already has, usually a WebSocket, works.
The unprefixed RTCPeerConnection constructor has been available in Chrome 56, Edge 15, Firefox 44, Safari 11, Samsung Internet 6, and Android WebView 56, which is why Baseline lists it as widely available since 2017-09. getUserMedia() is [SecureContext], so capture needs https:// or localhost; the peer connection itself does not, but a page that cannot capture rarely has anything to send.
Syntax
Section titled “Syntax”new RTCPeerConnection()new RTCPeerConnection(configuration)
pc.createOffer()pc.createOffer(options)pc.createAnswer()pc.setLocalDescription()pc.setLocalDescription(description)pc.setRemoteDescription(description)pc.addIceCandidate(candidate)
pc.addTrack(track)pc.addTrack(track, ...streams)pc.createDataChannel(label)pc.createDataChannel(label, options)pc.close()
navigator.mediaDevices.getUserMedia(constraints)createOffer() and createAnswer() resolve with an RTCSessionDescriptionInit ({ type, sdp }); the set-description methods and addIceCandidate() resolve with undefined and run on the connection’s operations chain, so they execute in call order. Calling setLocalDescription() with no argument creates the offer or answer the current signalingState calls for. addTrack() returns an RTCRtpSender synchronously, createDataChannel() returns an RTCDataChannel synchronously, and getUserMedia() resolves with a MediaStream.
Parameters
Section titled “Parameters”The constructor’s RTCConfiguration, createDataChannel()’s RTCDataChannelInit, and getUserMedia()’s MediaStreamConstraints are the dictionaries a typical call touches (see RTCConfiguration members (w3.org)).
| Member | Type | Required | Description |
|---|---|---|---|
configuration.iceServers |
sequence<RTCIceServer> |
No | Default []. Each entry has urls (one or more stun:/turn:/turns: URLs) plus username and credential, which turn: and turns: entries must carry. Without a TURN server, peers behind symmetric NATs cannot connect. |
configuration.iceTransportPolicy |
RTCIceTransportPolicy |
No | "all" (default) or "relay"; "relay" hides the user’s IP by forcing TURN candidates only. |
configuration.bundlePolicy |
RTCBundlePolicy |
No | "balanced" (default), "max-compat", or "max-bundle"; controls how many transports are negotiated for multiple media sections. |
configuration.rtcpMuxPolicy |
RTCRtcpMuxPolicy |
No | Only "require" remains in the spec; a remote description that does not mux RTCP is rejected. |
configuration.certificates |
sequence<RTCCertificate> |
No | Default []. Certificates from RTCPeerConnection.generateCertificate() for a stable DTLS fingerprint across sessions. |
configuration.iceCandidatePoolSize |
octet |
No | Default 0. Candidates to pre-gather before setLocalDescription(). |
options.ordered (data channel) |
boolean |
No | Default true. false delivers messages as they arrive. |
options.maxPacketLifeTime / options.maxRetransmits |
unsigned short |
No | Mutually exclusive; either turns the channel partially reliable. |
options.negotiated / options.id |
boolean / unsigned short |
No | negotiated: true with an explicit id (0 to 65534) skips in-band channel announcement; both sides must create the channel. |
constraints.audio / constraints.video |
boolean or MediaTrackConstraints |
No | At least one must be present and truthy. Object form accepts width, height, frameRate, facingMode, deviceId, each as a bare value (preference) or { exact } / { ideal }. |
Exceptions
Section titled “Exceptions”The names below are those webrtc-pc and mediacapture-main define; RTCPeerConnection methods reject their promise except where noted as thrown (see RTCError (w3.org) for the SDP and transport failures that carry an errorDetail).
| Exception | Condition |
|---|---|
InvalidStateError |
createOffer(), createAnswer(), both set-description methods, addIceCandidate(), addTrack() (thrown), and createDataChannel() (thrown) once signalingState is "closed"; addIceCandidate() while remoteDescription is null; createAnswer() when the state is not "have-remote-offer" or "have-local-pranswer". |
InvalidModificationError |
setLocalDescription() is given an offer or answer whose sdp differs from the one createOffer()/createAnswer() last produced. |
InvalidAccessError |
The constructor is given an expired or cross-origin certificate, or a turn: server without username and credential (thrown); a description fails RFC 9429 semantics validation, does not mux RTCP under rtcpMuxPolicy: "require", or renegotiates RIDs; addTrack() is called twice with the same track (thrown). |
SyntaxError |
An iceServers URL does not parse, has a path, or has a query other than transport=udp or transport=tcp (thrown by the constructor and setConfiguration()). |
NotSupportedError |
An iceServers URL uses a scheme the browser does not implement (thrown). |
TypeError |
addIceCandidate() receives a non-empty candidate with both sdpMid and sdpMLineIndex null; createDataChannel() sets both maxPacketLifeTime and maxRetransmits, uses negotiated: true without an id, or uses id: 65535 (thrown). |
OperationError |
addIceCandidate()’s sdpMid matches no media section, sdpMLineIndex is out of range, or usernameFragment matches no applied remote description; createDataChannel() finds the id in use or exhausted (thrown); applying a description fails for a reason not covered above; createOffer() cannot inspect the system’s codecs and resources. |
RTCError |
A DOMException subclass whose errorDetail names the failure: "sdp-syntax-error" (with sdpLineNumber) from the set-description methods, "dtls-failure", "fingerprint-failure", "sctp-failure", or "data-channel-failure" from the transport, delivered through the error event on an RTCDataChannel. |
NotAllowedError (getUserMedia) |
The user denied the permission, the page is not allowed by the camera/microphone Permissions Policy, or the call lacks the activation the browser requires. |
NotFoundError (getUserMedia) |
No device of the requested kind exists. |
NotReadableError (getUserMedia) |
The OS or another application holds the device. |
OverconstrainedError (getUserMedia) |
An exact constraint is not satisfiable; error.constraint names it. |
TypeError / SecurityError (getUserMedia) |
constraints has neither audio nor video set; the document is not fully active or the context is insecure. |
Examples
Section titled “Examples”The three examples form one call: capture, negotiate, and exchange data, each with the branch taken when support or permission is missing.
Capturing local media with a constraint fallback
Section titled “Capturing local media with a constraint fallback”Feature-detect RTCPeerConnection and navigator.mediaDevices together; a page served over http:// has navigator.mediaDevices undefined. Ask for the ideal resolution first and retry with the bare video: true request when the camera cannot meet an exact constraint.
async function capture() { if (!("RTCPeerConnection" in window) || !navigator.mediaDevices?.getUserMedia) { return null; // show an upload form instead of a live call } try { return await navigator.mediaDevices.getUserMedia({ audio: true, video: { width: { ideal: 1280 }, height: { ideal: 720 }, facingMode: { exact: "user" } }, }); } catch (err) { if (err.name === "OverconstrainedError") { console.warn(`constraint ${err.constraint} not satisfiable; retrying without it`); return navigator.mediaDevices.getUserMedia({ audio: true, video: true }); } if (err.name === "NotAllowedError") return null; // permission denied: audio-free fallback throw err; }}facingMode: { exact: "user" } is the usual trigger on desktops, where webcams report no facing mode at all.
Driving offer, answer, and ICE from browser events
Section titled “Driving offer, answer, and ICE from browser events”Let negotiationneeded create the offer so that tracks added later renegotiate automatically, forward each icecandidate over the signalling channel, and feed the remote side’s candidates to addIceCandidate() only after setRemoteDescription() has run.
function connect(stream, signaling, iceServers) { const pc = new RTCPeerConnection({ iceServers }); const controller = new AbortController(); const { signal } = controller; for (const track of stream.getTracks()) pc.addTrack(track, stream);
pc.addEventListener("negotiationneeded", async () => { await pc.setLocalDescription(); // creates the offer for the current state signaling.send({ description: pc.localDescription }); }, { signal }); pc.addEventListener("icecandidate", ({ candidate }) => { if (candidate) signaling.send({ candidate }); }, { signal }); pc.addEventListener("track", ({ streams }) => { document.querySelector("#remote").srcObject = streams[0]; }, { signal });
signaling.onmessage = async ({ description, candidate }) => { if (description) { await pc.setRemoteDescription(description); if (description.type === "offer") { await pc.setLocalDescription(); signaling.send({ description: pc.localDescription }); } } else if (candidate && pc.remoteDescription) { await pc.addIceCandidate(candidate); } };
return () => { controller.abort(); for (const track of stream.getTracks()) track.stop(); pc.close(); };}The returned function is the hang-up: aborting the signal removes every listener in one call, stopping tracks releases the camera light, and close() ends ICE. The signal option on addEventListener() needs Chrome 90, Firefox 86, or Safari 15; below those, keep named handlers and call removeEventListener().
Sending messages over a data channel with a WebSocket fallback
Section titled “Sending messages over a data channel with a WebSocket fallback”Data channels do not need any media. Create the channel before the first offer so it is announced in-band, and read readyState before each send(); a channel is usable only while it is "open".
function openChat(pc, signalingSocket, onMessage) { if (!pc || typeof pc.createDataChannel !== "function") { signalingSocket.addEventListener("message", (e) => onMessage(e.data)); return (text) => signalingSocket.send(text); // relayed through the server } const channel = pc.createDataChannel("chat", { ordered: false, maxRetransmits: 0 }); channel.binaryType = "arraybuffer"; channel.addEventListener("message", (e) => onMessage(e.data)); channel.addEventListener("error", (e) => console.error(e.error.errorDetail, e.error.message));
pc.addEventListener("datachannel", ({ channel: incoming }) => { incoming.addEventListener("message", (e) => onMessage(e.data)); });
return (text) => { if (channel.readyState === "open") channel.send(text); else signalingSocket.send(text); };}ordered: false with maxRetransmits: 0 gives UDP-like delivery for presence or cursor updates; omit both options for chat text that must arrive complete and in order.
See also
Section titled “See also”- Screen Capture API, the
getDisplayMedia()source for screen-sharing tracks - WebCodecs, for custom encoding of frames before they enter a peer connection
- WebTransport API, the client-server alternative to
RTCDataChannel - Document Picture-in-Picture, for keeping the remote video visible in an always-on-top window
- WebRTC: RTCPeerConnection constructor (w3.org)
- WebRTC: addIceCandidate() (w3.org)
- Media Capture and Streams: getUserMedia() (w3.org)
- RFC 8445: Interactive Connectivity Establishment (ICE) (rfc-editor.org)
- WebRTC samples (webrtc.github.io)
Specifications
| Specification | Status |
|---|---|
| WebRTC (RTCPeerConnection) | W3C draft |
| WebRTC: RTCPeerConnection constructor | W3C draft |
| WebRTC: setLocalDescription() | W3C draft |
| WebRTC: addIceCandidate() | W3C draft |
| WebRTC: createDataChannel() | W3C draft |
| Media Capture and Streams: getUserMedia() | W3C draft |
- Legend
- Yes
- Partial
- Flag
- No
- Unknown
| Browser / Platform | Support | Versions | Confidence | Source | Notes |
|---|---|---|---|---|---|
| Chrome (Desktop) | Yes | 56 | high | source | — |
| Chrome (Android) | Yes | 56 | high | source | 1 |
| Edge (Desktop) | Yes | 15 | high | source | — |
| Firefox (Desktop) | Yes | 44 | high | source | — |
| Firefox (Android) | Yes | 44 | high | source | — |
| Safari (macOS) | Yes | 11 | high | source | — |
| Safari (iOS) | Yes | 11 | high | source | 2 |
| Samsung Internet | Yes | 6.0 | high | source | 3 |
| WebView (Android) | Yes | 56 | high | source | 4 |
- Derived by browser-compat-data mirroring from Chrome.
- Derived by browser-compat-data mirroring from Safari.
- Derived by browser-compat-data mirroring from Chrome Android.
- Derived by browser-compat-data mirroring from Chrome Android.