A WebRTC media stream is only as good as the codec carrying it, and the codec you actually get is the result of a negotiation you may never have looked at. Two browsers exchange lists of what they can encode and decode, intersect those lists, and settle on whatever survives — often a default you never chose. That default is usually fine for a quick prototype and frequently wrong for production, where battery life, CPU headroom on weak laptops, and bandwidth bills all hinge on the choice.
This article walks through the audio and video codecs that real browsers ship today, what each one costs
in encoder CPU and bitrate, where hardware acceleration kicks in, and how to take control of the
selection with the modern setCodecPreferences() API instead of the SDP-string surgery people
used to resort to. The goal is to let you match a codec to a use case deliberately rather than inherit one
by accident.
Audio: Opus does almost everything
Audio is the easy part, and that is mostly thanks to Opus. The WebRTC specification makes Opus mandatory-to-implement, so every conformant browser can both send and receive it. Opus is unusually flexible: it spans roughly 6 kbps to 510 kbps, covers narrowband speech up to fullband music, and adapts its internal mode on the fly. For a voice call you might sit around 24–32 kbps; for stereo music sharing you can push past 128 kbps and it still sounds clean.
Three Opus features matter more than the raw bitrate. In-band forward error correction
(useinbandfec=1) lets the encoder embed a low-bitrate copy of the previous packet inside the
next one, so a single lost packet can be reconstructed without a retransmit — a real win on lossy
networks. Discontinuous transmission (usedtx=1) stops sending packets during
silence, which slashes bandwidth and battery use on a muted or quiet participant. And stereo
(stereo=1) is off by default for calls but worth enabling for music or spatial audio. These
are set through the fmtp line in SDP or, more conveniently, through encoder parameters.
The fallback worth knowing is G.711, exposed as PCMU and PCMA (μ-law and A-law). It is ancient, uncompressed-ish 64 kbps narrowband audio, but it is the lingua franca of the PSTN. If your WebRTC app bridges to a SIP gateway or a phone network, G.711 is what keeps interop working when Opus is not available on the far side. You will almost never choose it for a browser-to-browser call.
On the experimental edge, Google's Lyra and other neural speech codecs can deliver intelligible voice at a few kbps using machine-learned models, and AI-based audio enhancement is creeping into stacks. None of this is a standard WebRTC negotiable codec in shipping browsers yet, so treat it as a research direction rather than something to deploy against today.
Video codecs, one at a time
Video is where the real trade-offs live. Newer codecs compress better but cost dramatically more CPU to encode, and hardware support is uneven. Here is how the field looks in practice.
VP8 — the universal baseline
VP8 is the safe default. It is royalty-free, supported by essentially every WebRTC browser, and its encoder is mature and predictable. It is software-encoded almost everywhere, which means it leans on the CPU but never fails to negotiate. VP8 supports temporal scalability, the foundation of simulcast: the encoder produces layered frames so a selective forwarding unit can drop higher temporal layers to send a lower frame rate to constrained receivers. If you need a stream that "just works" across an unknown fleet of devices, VP8 is it.
H.264 — hardware acceleration nearly everywhere
H.264 is the pragmatic choice on devices with weak CPUs because almost every phone, tablet, and laptop has
a dedicated H.264 encoder block in silicon. Offloading encoding to hardware keeps the CPU cool and the
battery alive, which is exactly what you want on mobile and what Safari has historically favored. The
catch is profile interop: WebRTC implementations agree on Constrained Baseline Profile,
and the profile-level-id in SDP must line up between peers, or two H.264-capable endpoints can
still fail to talk. Licensing is real but handled by the browser vendors, so as an application developer
you generally do not deal with patent fees directly.
VP9 — better compression, more CPU
VP9 delivers roughly 30–40% better compression than VP8 or H.264 at equivalent visual quality, meaning lower bitrate for the same picture or a sharper picture for the same bitrate. It also supports true spatial and temporal SVC (scalable video coding), letting one encoded stream carry multiple resolution and frame-rate layers an SFU can peel apart — more efficient than running multiple simulcast encoders. The price is higher encode CPU and patchier hardware encoding, so on a low-end device VP9 can cost more than it saves.
AV1 — best compression, heaviest encode
AV1 is the most efficient royalty-free codec available, beating VP9 again on bitrate-per-quality. Hardware decode is now common on recent GPUs and mobile SoCs, but hardware encode is still rare, and software AV1 encoding is expensive at high resolutions. The practical pattern today is to use AV1 where it shines without melting the CPU: low bitrates (think 2-person calls on constrained links) and screen sharing, where its excellent handling of flat regions and sharp text pays off. Real-time AV1 at 1080p60 from a software encoder is still a stretch on most hardware.
H.265 / HEVC — limited in WebRTC
HEVC compresses well and has broad hardware support in consumer devices, but its WebRTC story is thin. Patent licensing kept it out of the open ecosystem, and support is mostly confined to Safari and specific hardware paths. You can occasionally negotiate it in an Apple-centric deployment, but it is not something to build a cross-browser product around.
Side-by-side comparison
The table below summarizes the trade-offs. Treat the efficiency and CPU columns as relative ordering, not precise multipliers — actual numbers vary with content, resolution, and encoder build.
| Codec | Compression efficiency | Encode CPU cost | Hardware encode | Browser support | Simulcast / SVC | Royalty status |
|---|---|---|---|---|---|---|
| VP8 | Baseline | Low–moderate | Rare | Universal | Simulcast (temporal) | Royalty-free |
| H.264 | Similar to VP8 | Low (in hardware) | Very common | Universal | Simulcast | Licensed (vendor-paid) |
| VP9 | ~30–40% better than VP8 | High | Some | Chrome, Firefox, Safari | SVC + simulcast | Royalty-free |
| AV1 | Best (better than VP9) | Very high (software) | Decode common, encode rare | Chrome, Firefox, newer Safari | SVC | Royalty-free |
| H.265 / HEVC | Better than H.264 | Low (in hardware) | Common in devices | Mainly Safari / hardware | Limited | Licensed |
How negotiation actually happens in SDP
When you create an offer, the browser writes every codec it supports into the session description. The
m=video line lists payload type numbers, and each number is mapped to a codec by an
a=rtpmap attribute, with codec-specific options in a=fmtp. The answerer keeps
the codecs it shares and reorders them by its own preference. Here is a trimmed excerpt:
m=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 100
a=rtpmap:96 VP8/90000
a=rtpmap:97 rtx/90000 ; retransmission for PT 96
a=rtpmap:98 H264/90000
a=fmtp:98 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f
a=rtpmap:99 VP9/90000
a=rtpmap:100 AV1/90000
The order of payload types after m=video is the offerer's preference list. The
profile-level-id=42e01f on the H.264 line encodes Constrained Baseline at level 3.1 — if the
far end advertises an incompatible profile, that H.264 entry may not match even though both sides "support
H.264." This is why H.264 interop bugs are so common and so confusing.
Taking control with setCodecPreferences()
The clean, supported way to influence which codec wins is
RTCRtpTransceiver.setCodecPreferences(). You query the codecs the browser can handle, filter
and reorder them, and apply that order to the transceiver before creating the offer. The browser then
writes your preferred order into the SDP for you.
const pc = new RTCPeerConnection();
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
// Add the track and grab its transceiver
const transceiver = pc.addTransceiver(stream.getVideoTracks()[0], {
direction: 'sendrecv',
});
// What can this browser do?
const { codecs } = RTCRtpSender.getCapabilities('video');
// Prefer VP9, then VP8, then H.264; keep RTX/RED/FEC entries too
const preferred = [];
for (const name of ['video/VP9', 'video/VP8', 'video/H264']) {
preferred.push(...codecs.filter(c => c.mimeType === name));
}
// Always retain helper codecs so retransmission still works
preferred.push(
...codecs.filter(c => /rtx|red|ulpfec/i.test(c.mimeType))
);
if (transceiver.setCodecPreferences) {
transceiver.setCodecPreferences(preferred);
}
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
The legacy alternative was "SDP munging" — running a regular expression over the offer string to move a
codec's payload type to the front of the m= line. Avoid it. Munging is brittle against SDP
format changes, easy to get subtly wrong (forgetting the matching rtx entry breaks
retransmission), and unsupported in spirit by the standard. Reach for it only as a last resort on a
browser that lacks setCodecPreferences, which is rare in 2026.
Recommendations by scenario
There is no single best codec, only a best fit for a given workload. A few field-tested defaults:
- 1:1 mobile calls: prefer H.264. Hardware encoders are nearly universal on phones, so you keep the CPU idle and the battery happy. This is also the friendliest path for Safari and older iOS devices.
- Group calls through an SFU: VP8 or VP9 with simulcast as the workhorse so the server can forward different layers to different receivers; opt into AV1 for participants whose devices can encode it efficiently. See SFU vs MCU vs Mesh for how the routing layer shapes this choice.
-
Screen sharing: AV1 or VP9 win on text clarity and flat backgrounds. Set
track.contentHint = 'detail'(or'text') so the encoder optimizes for sharp edges over smooth motion. - Low-end devices: stick to VP8 or hardware H.264. VP9 and AV1 software encoding can starve a weak CPU and cause dropped frames, which looks worse than a lower-efficiency codec running smoothly. See WebRTC on Mobile Devices for the constraints that matter here.
- Recording pipelines: consistency beats efficiency. A widely decodable codec like H.264 makes downstream transcoding and playback far less painful than a cutting-edge one your tooling may not support.
Rough bandwidth expectations
For planning purposes, 720p30 video lands around 1.5–2.5 Mbps on H.264 or VP8 at good
quality. VP9 reaches comparable quality at roughly 30% less, and AV1 shaves off a bit more again. These are
approximations — content complexity (a static slide deck versus a fast-moving sports clip) moves them
substantially — but they are good enough to size a link or set an initial bitrate cap. Set targets with
RTCRtpSender.setParameters() on the encoding's maxBitrate, then let congestion
control adapt downward as conditions demand.
None of this matters if the connection never forms
All of the above assumes media is flowing. If ICE never completes — because a TURN server is unreachable, a firewall blocks UDP, or candidates never pair — then the best codec in the world encodes into the void. Codec selection is a quality optimization that sits on top of a working connection, not a substitute for one. Get connectivity solid first, then tune codecs. You can confirm your STUN/TURN servers are reachable and gathering candidates with the free ICE server tester before you spend time profiling encoders.
Check Your Own Servers
Before you fine-tune VP9 versus AV1, make sure media can actually traverse the network. Run your STUN and TURN servers through the tester to confirm candidate gathering, relay reachability, and round-trip latency under real conditions.
Open the Free ICE Server Tester →