← View all posts
May 16, 2024

Cross-browser support for choosing WebRTC codecs

Contributed by Jan-Ivar Bruaroey,

WebRTC enables video calls directly in browsers. Effectively managing the codecs that encode and decode media streams is a crucial component of delivering high-quality audio and video. The setCodecPreferences method allows developers to specify which codecs their applications prefer to receive.

With Firefox Nightly 128, setCodecPreferences is now available in all browsers! Let’s explore how it functions, its implications for web developers, and its role in harmonizing browser behaviors.

Firefox supporting this WebRTC 1.0 feature is a big deal. It helps webpages work the same across all browsers, eliminating browser-conditional code or having to resort to munging WebRTC’s signaling messages written in SDP, by hand.

But what made it seem blog-worthy was this question: do people know how it works? Looking at MDN and recent spec changes suggested not. Contrary to popular belief, it doesn’t control what you send. At least not directly.

How does setCodecPreferences work?

setCodecPreferences is a method on a WebRTC transceiver. It lets webpages sort and filter the decoders to be considered when negotiating with a peer over a connection. The following illustrates a baseline for usage e.g. on a video transceiver:

transceiver.setCodecPreferences(RTCRtpReceiver.getCapabilities("video").codecs)

This sets the browser’s whole array of available decoders unmodified. Perhaps surprisingly, this is not a no-op. It restricts the browser to consider all its codecs in this specific order. This technically differs from the default transceiver.setCodecPreferences([]) which doesn’t restrict the browser in this way. E.g. a browser may choose to leave out some codecs by default.

It’s also an imperfect API for other reasons. The codecs array contains a mix of real decoders (often repeated with different profile levels) along with special entries for features called RTX, RED and ULPFEC (thanks to JSEP).

As such, it’s not advisable to brute-force your own fixed array e.g. setCodecPreferences([h265, h264]), as this would inadvertently disable RTX, RED and ULPFEC. Instead, we’ll demonstrate best practice: carefully sort the array, using a sortByMimeTypes(codecs, preferredOrder) function below.

setCodecPreferences sets what encodings you prefer to receive

The important part to remember is that setCodecPreferences sets what media encodings you prefer to receive, not what you want to send. Take this example:

If you click the “Result” tab, you should see more or less the following across browsers (use Firefox 128+):

pc1 prefers video/H264,video/VP8,video/VP9,video/ulpfec,video/red,video/rtx
pc2 prefers video/VP9,video/VP8,video/H264,video/ulpfec,video/red,video/rtx
checking
connected
pc1 is sending video/VP9
pc2 is sending video/H264

You’ll see pc1 sending VP9 because that’s what pc2 prefers to receive, and pc2 sending H.264 because that’s what pc1 prefers to receive. In other words, setCodecPreferences sets preferences for reception.

Does setCodecPreferences configure a codec for sending?

It does that too sometimes, sort of, implicitly. To understand why, it helps to appreciate how setCodecPreferences is rooted in the offer/answer exchange, where what encoder a sender uses is the result of SDP negotiation with its peer.

The simplest way to see this is to consider what happens when only pc1 expresses a preference:

Now, if you click the “Result” tab, you’ll see pc1‘s preference controlling what both peers send:

pc1 prefers video/H264,video/VP8,video/VP9,video/ulpfec,video/red,video/rtx
pc2 has no preference
checking
connected
pc1 is sending video/H264
pc2 is sending video/H264

Essentially, the JSEP offer/answer exchange negotiates what to use with the peer. The devil is in the details, but if only the offerer (pc1) has a preference then it simplifies the negotiation quite a bit.

Especially when the transceiver’s direction is "sendonly", this makes it appear like pc1‘s preference is controlling what it sends, because it is, implicitly (provided the peer has no preference).

Since a transceiver’s direction can be renegotiated over time, having codec negotiation work (mostly) independently of direction is a nice invariant (even if JSEP sometimes gets in the way, more on that below).

Looking at setCodecPreferences as a sender-control leads to surprises

Unfortunately, the pc1 preference trick above to control both sides doesn’t work on pc2:

Click the “Result” tab, you’ll see pc2‘s preference only controls what it receives:

pc1 has no preference
pc2 prefers video/VP9,video/VP8,video/H264,video/ulpfec,video/red,video/rtx
checking
connected
pc1 is sending video/VP9
pc2 is sending video/VP8

This is because SDP negotiation is inherently asymmetric: pc1 is the offerer and pc2 is the answerer. By the time pc2’s call to setCodecPreferences is considered the offer has already happened.

This is a case where reversing roles would yield different negotiation results, so it’s good to be aware of it.

But this anomaly is a result of our expectation to control sending. If we shift our mental model to setCodecPreferences controlling reception, then it should hopefully yield fewer surprises.

Live updates and the negotiationneeded event

setCodecPreferences can also be used to make runtime changes mid-stream. Below we see pc1‘s preferences controlling both sides again, except now with a drop-down picker letting us make live changes:

Clicking the “Start!” button, you should be able to use the drop-down and switch codecs live. But behind the scenes, renegotiation needs to happen for these changes to take effect. It’s perhaps surprising that (as of this writing) calling setCodecPreferences() does not trigger the negotiationneeded event! I’ve raised an issue with the spec about this. Hopefully this can be fixed, but that’s going to take some time, so in the meantime the fiddle works around this by negotiating explicitly.

To instead change encoder without negotiation (choosing between already negotiated codecs), look for a future blog post on setting codec with setParameters.

When a browser can decode what it cannot encode

Some concerns were raised on the spec recently about decode-only codecs. For example, Chrome might initially only be able to decode H.265 but not encode it, whereas Safari might both encode and decode it.

It’s a limitation of JSEP that there is no way to indicate per-codec directions in SDP negotiation (where codecs apply to the whole m-line). As a result, some of the spec language, if taken literally, would have browsers exclude decode-only codecs from their sendonly and sendrecv transceivers.

But current thinking based on what Justin Uberti suggested is that this is too strict of a read. Instead, the leading idea is Chrome could offer (to receive) H.265 with H.264 as a fallback. This way Chrome can negotiate successfully with Safari to have Safari send it H.265 while it sends H.264 back.

This seems like a good solution that wouldn’t require any changes, and it aligns well with an understanding of setCodecPreferences as a means to control reception. In short, the offerer includes a fallback codec it can send with. This is another good reason to never truncate the array when using this API. It largely avoids this problem.

Conclusion

Hopefully, this post has helped clarify how setCodecPreferences works. While the method primarily dictates the codecs your application prefers to receive, it also indirectly influences what is sent during peer-to-peer exchanges. This dual role in the negotiation process is helpful to understand for effective use of the API. The method is one of only two on the RTCRtpTransceiver, an interface designed to simplify complex underpinnings. As you integrate this feature into your projects, you’ll likely appreciate both its capabilities and its challenges. I’d love to hear about your experiences. There’s no comments section here unfortunately, but feel free to reach out on X.

Tags