Centricular

Expertise, Straight from the Source



« Back

Devlog

Posts tagged with #webtransport

Getting low-latency video from a media pipeline into a web browser has historically meant reaching for WebRTC. WebRTC works, but it has a lot of pieces: SDP negotiation, ICE connectivity checks, and a stack of protocols designed for peer-to-peer calling. WebRTC is built for two-way media between peers that may sit behind NAT, and it needs a separate signalling server.

For a client-to-server setup where the server just pushes video to the browser, a simpler solution might be possible, albeit with some trade-offs discussed later.

WebTransport is a newer browser API designed for this kind of use case and as an improvement on WebSockets. But to appreciate what WebTransport brings, it helps to first understand the layer beneath it, which is QUIC.

Why QUIC

Most web traffic today runs on TCP, the protocol that carries HTTP/1.1 and HTTP/2. TCP guarantees ordered, reliable delivery of a single byte stream. The whole connection is one ordered sequence, and TCP stalls the connection if any packets are lost, even if later packets arrived intact. This is called head-of-line blocking, and for web pages and file downloads it's required. Lost packets are re-sent, and a download just completes a little later.

Real-time media is different. Frames arrive continuously, and one lost packet holds up every frame behind it while the network retransmits. A video player would rather skip damaged content than freeze, but TCP gives it no way to do that.

QUIC is a transport protocol originally designed by Google and later standardized by the IETF. It runs on top of UDP and brings several things that can make it a better fit for streaming.

First, QUIC avoids head-of-line blocking between streams. QUIC separates transmission order from delivery order: packet numbers record what was sent, while stream offsets record what was received. Data from multiple streams is interleaved into QUIC packets on the wire, which keeps transmission efficient. When a packet is lost, only the streams that carried data in that packet stall. All other streams keep making progress (RFC 9000 Section 13). Put each media frame on its own stream, and a dropped packet can't block unrelated frames.

Streams aren't the only option. QUIC also has an optional datagram extension where media can travel as datagrams instead of using a stream. A datagram is a self-contained message that QUIC sends at most once. If it's lost, it's lost. There is no retransmission and no ordering, so the receiver hands each datagram to the application as it arrives, and lost ones leave gaps between the messages that did arrive. A datagram must fit inside a single QUIC packet, and what goes into it, one frame or several, is up to the application.

Second, QUIC has TLS 1.3 encryption built in. All traffic is encrypted, and there is no plain-text mode. Connection setup is faster because the transport and TLS handshakes happen in a single round trip, instead of setting up TCP first and then doing TLS on top. It still has TLS overhead, and more overhead than plain TCP, but it trades latency for security.

Third, QUIC supports connection migration (keeping a session alive when you switch from Wi-Fi to cellular).

QUIC becomes the base protocol, and HTTP/3 maps HTTP traffic onto QUIC. WebTransport builds on top of HTTP/3, and that gives browsers a way to open a QUIC connection to a server and send data as either reliable streams or unreliable datagrams, all from JavaScript.

Why WebTransport instead of WebRTC or WebSockets

WebRTC has been the default way to get video and audio into the browser. It works, and it's supported everywhere. The problem is that WebRTC is built for two-way media between peers that may sit anywhere on the internet, behind NAT, on unknown networks. Even when your two "peers" are just your own server and your own browser, you still go through the offer/answer dance with SDP to negotiate media properties, gather ICE candidates, and often deploy a TURN server for NAT traversal. On top of that, you need a signalling server just to exchange the SDP and ICE candidates in the first place. All of that infrastructure solves problems that don't exist when the server is a known host running in a data center.

WebTransport sidesteps all of that complexity. It's client to server by design, and there is no SDP, no ICE, no offer/answer exchange, and no signalling server. The browser opens a QUIC connection and starts sending and receiving data. For media delivery, you get streams and optional datagrams. An application can send video frames over either.

One trade-off mentioned in the introduction is that skipping SDP means there is no built-in media negotiation. Both ends must agree on the format out of band, which for this demo means hard-coding the codec, resolution, and bitrate in the two applications. Depending on the application, this might or might not be an acceptable trade-off, and you may have to implement format negotiation yourself.

WebTransport can also serve as an alternative to WebSockets, which browser applications commonly use for live data. A WebSocket provides one reliable, ordered, bidirectional message channel. WebTransport, running over QUIC, provides multiple independent reliable streams plus optional unreliable datagrams within a single encrypted connection. With a conventional TCP-based WebSocket, packet loss can cause head-of-line blocking, delaying later messages on the connection. WebTransport avoids head-of-line blocking between streams, while its datagrams can be delivered without waiting for retransmission, at the cost of possible loss and reordering.

WebTransport still requires TLS certificates, but that's the same work a WebRTC deployment already does. WebRTC needs TLS for its signalling server whenever it isn't localhost, while the certificates on its peer-to-peer DTLS connection are usually self-signed, auto-generated ones, validated only by comparing fingerprints exchanged over SDP. WebTransport validates the server certificate on the QUIC connection itself, so there is no additional certificate setup beyond what the deployment already has.

Another trade-off compared to WebRTC is that WebRTC has built-in congestion control, bandwidth estimation, and forward error correction. That allows it to adapt to the network while playing, in a way that WebTransport doesn't out of the box. If the transmission bitrate is too high for the path, a stream backs up and latency grows just like TCP, while datagrams simply lose packets. Efforts like RTP over QUIC and Media over QUIC can address this bandwidth adaptation limitation, however.

WebTransport demo

The demo shows how a GStreamer pipeline can stream H.264 video directly into a browser using WebTransport. The setup has two parts: a Rust server that runs the GStreamer pipeline, and a React web app that receives and displays the video.

GStreamer

The server runs a pipeline that looks like this:

    videotestsrc ! videorate ! videoscale ! video/x-raw,width=640,height=360,framerate=15/1 ! \
    queue ! x264enc bitrate=600 speed-preset=ultrafast tune=zerolatency key-int-max=15 ! \
    h264parse config-interval=-1 ! video/x-h264,stream-format=byte-stream,alignment=au,profile=constrained-baseline ! \
    quinnwtsink

videotestsrc generates a test pattern, which gets scaled to 640x360 at 15 fps. x264enc encodes it to H.264 constrained baseline (a profile that all major browsers can decode) at a bitrate of 600 kbit/sec. The encoded frames then go into quinnwtsink, the GStreamer element that acts as a WebTransport server. The connection must be fast enough to carry this stream.

quinnwtsink is part of the gst-plugin-quinn plugin in gst-plugins-rs. Under the hood it uses the Rust quinn crate for the QUIC transport and web-transport-quinn for the WebTransport protocol layer. It listens for incoming QUIC connections and, once a client connects, sends the H.264 encoded data over a bidirectional stream.

QUIC can work with HTTP-based proxies when combined with MASQUE. The QUIC DATAGRAM extension provides a building block for carrying UDP payloads through a QUIC connection, and MASQUE's CONNECT-UDP protocol uses HTTP/3 to build UDP proxying on top of it. Cloudflare's post on QUIC proxying covers the details. For a media pipeline, this means an end-to-end QUIC connection, and therefore its streams, can traverse a compatible proxy without being converted to TCP, preserving QUIC's transport properties.

One detail worth mentioning: the browser's WebCodecs API needs the frame type (keyframe or delta) and a timestamp for each encoded video chunk. WebCodecs is the browser's API for decoding media natively. The chunks go through the browser's media stack, which may be a hardware or a software decoder. The demo prepends a small 6-byte header to each frame with the frame type and the payload size before handing it off to quinnwtsink. The payload size is there because QUIC streams don't preserve frame boundaries. A read from the stream can return half a frame, a whole frame, or pieces of several frames, so the application buffers incoming bytes and uses the size field to cut out complete frames.

Browser

The browser part is a React app that connects to the GStreamer server using the WebTransport API. WebTransport is supported on all major browsers. For details, see browser compatibility on MDN. Browser-specific differences may affect behaviour; this demo has been tested only with Chromium.

When the user hits connect, the browser opens a WebTransport session to https://localhost:4433 and creates a bidirectional stream. As data arrives, the app reassembles complete frames, then feeds each frame to the browser's WebCodecs VideoDecoder. The decoder outputs raw frames, which get drawn onto a <canvas> element.

The whole pipeline, from GStreamer's test pattern generator to pixels on the canvas, runs with a single WebTransport connection.

Running the demo

The demo is under net/quinn/examples/ in gst-plugins-rs. It has two pieces:

  • webtransport_webcodec.rs: the Rust server running the GStreamer pipeline
  • webtransport-webcodec-browser/: the React front-end

Since WebTransport requires TLS, you need a certificate. For local testing, a self-signed certificate works if you launch Chrome or Chromium with the right flags (--origin-to-force-quic-on and --ignore-certificate-errors-spki-list). The README in the demo directory walks through the exact steps.

Once the server is running, start the React dev server (pnpm run dev), open localhost:3001 in a Chromium-based browser, and click connect. You should see the GStreamer test pattern appear on the canvas.

Conclusion

In the presence of network latency, a WebTransport stream behaves like TCP. There are two sources of latency under packet loss. First, a lost packet stalls the stream until retransmission, and nothing in the transport tells the media pipeline to skip the damaged frame. Second, QUIC's congestion control reduces the sending rate under loss, and a fixed-bitrate pipeline can fall behind real-time. This is in comparison to WebRTC, which handles both with media-aware congestion control and forward error correction, so a video call can survive loss gracefully.

The datagram API avoids the first part of the problem: a lost datagram is dropped immediately, and the receiver just sees a gap. Congestion control still applies, however, so under sustained loss the sender slows down. If the application doesn't adapt its bitrate to match, the network continues dropping data. With WebTransport, your application gets to choose how to handle this, but it does have to handle it.

When latency matters, WebTransport datagrams are one option. The RTP over QUIC draft maps RTP and RTCP packets onto QUIC streams, datagrams, or a mixture of both, and leaves the choice to the application. Media over QUIC can be another option.

This demo is a starting point. With quinnwtsink acting as the server and the browser handling decoding and rendering, you can replace videotestsrc with a real video source (a camera, a file, a network stream) and stream it into a web app without WebRTC. Support for multiple streams is also provided by the quinnquicmux and quinnquicdemux elements, and an example is included upstream.

There are rough edges. WebTransport is still relatively new, and the API surface in quinnwtsink is minimal. QUIC has mandatory transport-level congestion control, but quinnwtsink doesn't expose it as of this writing, and the demo does no application-level congestion control or bitrate adaptation either.

The plugin also supports RTP over QUIC upstream, and work on Media over QUIC is in progress.

If you have ever wrestled with WebRTC when all you needed was a simple server-to-browser push, approaches utilising QUIC might serve your use case.