rumb
← logbook

Making YouTube play

One site as a test suite

Picking a single site to make work is a cheap way to find out what an engine is missing, because a large site uses everything. YouTube turned out to need Media Source Extensions, a GStreamer backend that could accept them, a fix to how WebM timestamps are read, a DOM property from 1997, an SVG that redraws when its contents change, and an IntersectionObserver that can be reconnected. None of those six are about video except the first two.

Media Source Extensions

MediaSource, SourceBuffer and SourceBufferList are implemented, and URL.createObjectURL now takes a MediaSource. A media element whose source is such a URL attaches to the MediaSource and plays what is appended to its source buffers.

That attachment changes where the element's own state comes from. With a MediaSource attached, the element takes its metadata and duration from the source rather than from the player, and derives readyState from the buffered ranges around the playback position — which is the only definition that makes sense when the bytes arrive in pieces and the piece under the playhead is the one that decides whether playback can continue.

A pipeline per source buffer

The GStreamer backend takes source buffers through a player created with StreamType::MediaSource. Each source buffer demuxes what is appended to it with a pipeline of its own and keeps the coded frames per track; those stored frames are where the buffered ranges come from, so the ranges script reads are a fact about what has been parsed rather than a count of bytes received.

Playback is fed from those frames through one appsrc per track, inside a new servomediasourcesrc element. After a seek it starts at the keyframe before the requested position, because starting at the position itself would hand the decoder a frame that depends on one it never saw. Fragmented MP4 and WebM are both accepted.

A NEW MEDIASOURCE, APPENDED FROM THE PLAYBACK POSITION 0s playback position where the append starts stored at zero first cluster → t=0 matroskademux rebases the segment nothing is buffered around the position, so the element never plays The timestamps of WebM frames are now taken as they come, and changeType to another container is no longer refused.

The quality change that stopped everything

Changing quality on YouTube makes it attach a new MediaSource and append from the current playback position. Two separate things made that fail.

The first is in matroskademux, which maps the first cluster it is given to time zero in its segment. Fed a WebM fragment that starts eleven minutes into the media, it stored those frames as though they started at the beginning. The buffered ranges then said the data was at zero, nothing was buffered around the playhead, and the element waited forever for bytes it had already been given. WebM frame timestamps are now taken as they come.

The second is simpler: changeType to another container was refused outright. A site that switches from WebM to fragmented MP4 mid-stream — which is what a quality change across codecs is — had nowhere to go.

preload, finally obeyed

A media element fetched its whole resource as soon as it had a source, whatever preload said, and playbin kept a second copy in a temporary file while it did. On a page with a dozen videos that is a dozen full downloads nobody asked for, twice over.

preload now decides, until something actually asks for playback. With none, nothing is fetched and no player is made at all. With metadata, which is the default the specification suggests, the fetch stops once the first frame is there, networkState becomes NETWORK_IDLE and suspend fires. A call to play() overrides all of it, which is the point: the hint describes what to do before the user has decided, not a cap on what happens after.

Four bugs that were not about media

document.all is implemented, which in 2026 reads like a joke and is not. YouTube's polymer-resin sanitiser compares values against document.all, and with the property undefined every undefined binding matched and was replaced with the string zClosurez. HTMLAllCollection emulates undefined — JSCLASS_EMULATES_UNDEFINED on its proxy class, so typeof gives "undefined" and the object is falsy — which keeps the old IE sniffs working while giving the comparison a real object to find.

An <svg> element's image is rasterized from its subtree's markup and kept until the element itself or its child list changed. A change further down — the d attribute of a path, say — left the old image on screen. That is why the play button never turned into a pause button, long after playback itself worked.

An IntersectionObserver is taken off its document when it is disconnected or loses its last target, but it stayed marked as connected, so observing a target afterwards never put it back and it silently reported nothing from then on. YouTube disconnects and reconnects the observer it loads images with.

And an inline <svg> was rasterized with a different scale on each axis to fill its box, the way an image is. Its box is a viewport: the drawing is now scaled uniformly and centred, which is what preserveAspectRatio defaults to. Other values of the attribute are still not looked at.

And the layout fixes underneath

A page like YouTube exercises enough CSS to find a different class of bug, the kind that is nobody's feature and everybody's problem.

mask is now built as clips when it can be. A mask layer whose image is opaque everywhere shows the element inside the box its mask-clip names and hides it outside, so compositing such layers is plain geometry: a rounded rectangle, or one with a smaller rectangle cut out with a ClipOut clip. That covers the usual way of drawing a gradient border, where two solid-colour gradients are composited to punch a hole.

backdrop-filter was clipped to a rectangle only, because WebRender clips one that way, so a box with a border-radius filtered its backdrop out to the square corners. The radii now get a clip of their own on top of the inherited chain.

Three more, each a one-liner with a long tail. The walk up the flat tree looking for a containing block did not step past an ancestor with no style and no box — it asked for the same node's parent again and never ended. Incremental layout rebuilt only the box when a restyle made an in-flow box absolutely positioned or floated, so the old box kept its place in the flow; the parent is rebuilt instead. And an element removed from the tree while hovered stayed the hover target, so the next mouse move walked up from a detached node, never reached the ancestors still in the tree, and left them with :hover and no mouseleave.

Two about :has() and <svg>, which found each other. Stylo works out which elements a relative selector depends on, but that is only wired up for Gecko, so a style that came from :has() stayed as it was after the first pass. A change to a node's children, or to an attribute other than style, now restyles the ancestors of where it happened — coarser than Stylo could be and still cheap, because an element whose style comes out the same stops the traversal. And serialising an <svg> element read its inherited colour with Element::style, which asks for a reflow; running inside Window::reflow while it handled the results of one, that nested reflow found the same element pending and recursed until the stack overflowed.

What still does not play

Codec support is whatever the GStreamer plugins on the machine provide, which means a build can satisfy a canPlayType check and then fail on the actual stream. Encrypted media is absent entirely — no EME, so anything behind DRM is not a question of bugs.

preserveAspectRatio is read as its default and nothing else. And none of this makes the site fast: the frames go through a demux pipeline per source buffer, which is correct and is not the same as efficient.

SCRIPT GSTREAMER BACKEND SourceBuffer, video appendBuffer() SourceBuffer, audio appendBuffer() demux pipeline one per buffer demux pipeline one per buffer coded frames, per track the buffered ranges are read off these appsrc / track servomediasourcesrc feeds playback from those frames, starting at the keyframe before the position after a seek. Fragmented MP4 and WebM are taken. readyState comes from the buffered ranges around the playback position, and metadata and duration from the MediaSource rather than the player
One demux pipeline per source buffer, coded frames kept per track, and one appsrc per track feeding playback.