WebRTC Codec Selection: Opus, VP8, VP9, H.264, and AV1 Compared

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

Video codecs: compression efficiency versus support Plotting the main WebRTC video codecs: AV1 and VP9 offer the highest compression efficiency but have narrower hardware and browser support; H.264 and VP8 compress less efficiently but are supported almost everywhere with hardware acceleration. broader browser & hardware support → higher compression efficiency → AV1best efficiency, newest VP9strong efficiency VP8universal, older H.264HW everywhere Audio: Opus covers nearly every case — you rarely need to change it
Figure 1 — The efficiency-versus-support trade-off for video. Newer codecs (top-left) save bandwidth but aren't hardware-accelerated everywhere; H.264/VP8 (bottom-right) are safe defaults for reach.

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 →