Hacer que YouTube reproduzca
Un solo sitio como suite de pruebas
Elegir un único sitio y hacer que funcione es una manera barata de averiguar qué le falta a un motor, porque un sitio grande lo usa todo. YouTube resultó necesitar Media Source Extensions, un backend de GStreamer capaz de aceptarlas, un arreglo en cómo se leen los timestamps de WebM, una propiedad del DOM de 1997, un SVG que se redibuje cuando cambia su contenido, y un IntersectionObserver que se pueda reconectar. Ninguna de esas seis cosas va de vídeo salvo las dos primeras.
Media Source Extensions
MediaSource, SourceBuffer y SourceBufferList están implementados, y URL.createObjectURL ya acepta un MediaSource. Un elemento de medios cuya fuente es una URL así se engancha al MediaSource y reproduce lo que se apenda a sus source buffers.
Ese enganche cambia de dónde sale el estado del propio elemento. Con un MediaSource enganchado, el elemento coge sus metadatos y su duración de la fuente y no del reproductor, y deriva readyState de los rangos buffered alrededor de la posición de reproducción — que es la única definición que tiene sentido cuando los bytes llegan a trozos y el trozo que está bajo el cabezal es el que decide si se puede seguir.
Un pipeline por source buffer
El backend de GStreamer acepta source buffers mediante un reproductor creado con StreamType::MediaSource. Cada source buffer hace demux de lo que se le apenda con un pipeline propio y guarda los frames codificados por pista; esos frames guardados son de donde salen los rangos buffered, así que los rangos que lee el script son un hecho sobre lo que se ha parseado y no un recuento de bytes recibidos.
La reproducción se alimenta de esos frames por un appsrc por pista, dentro de un elemento nuevo, servomediasourcesrc. Después de un seek empieza en el keyframe anterior a la posición pedida, porque empezar en la posición misma le daría al decodificador un frame que depende de otro que no ha visto. Se aceptan MP4 fragmentado y WebM.
El cambio de calidad que lo paraba todo
Cambiar de calidad en YouTube le hace engancharse a un MediaSource nuevo y apendar desde la posición de reproducción actual. Dos cosas distintas hacían que eso fallara.
La primera está en matroskademux, que mapea el primer cluster que recibe al tiempo cero de su segmento. Alimentado con un fragmento de WebM que empieza once minutos dentro del medio, guardaba esos frames como si empezaran al principio. Los rangos buffered decían entonces que los datos estaban en cero, no había nada buffered alrededor del cabezal, y el elemento esperaba para siempre unos bytes que ya le habían dado. Los timestamps de los frames de WebM se toman ahora tal como vienen.
La segunda es más simple: changeType a otro contenedor se rechazaba de plano. Un sitio que cambia de WebM a MP4 fragmentado a media reproducción — que es lo que es un cambio de calidad entre códecs — no tenía a dónde ir.
preload, por fin obedecido
Un elemento de medios se descargaba el recurso entero en cuanto tenía una fuente, dijera lo que dijera preload, y mientras lo hacía playbin guardaba una segunda copia en un fichero temporal. En una página con una docena de vídeos eso son doce descargas completas que nadie ha pedido, por duplicado.
preload decide ahora, hasta que algo pida reproducir de verdad. Con none, no se descarga nada y no se crea ningún reproductor. Con metadata, que es el valor por defecto que sugiere la especificación, la descarga se para en cuanto está el primer frame, networkState pasa a NETWORK_IDLE y salta suspend. Una llamada a play() lo anula todo, que es justo el punto: la pista describe qué hacer antes de que el usuario decida, no un tope de lo que pasa después.
Cuatro bugs que no iban de medios
document.all está implementado, lo que en 2026 suena a broma y no lo es. El sanitizador polymer-resin de YouTube compara valores contra document.all, y con la propiedad indefinida cada binding indefinido coincidía y se reemplazaba por la cadena zClosurez. HTMLAllCollection emula undefined — JSCLASS_EMULATES_UNDEFINED en su clase de proxy, así que typeof da "undefined" y el objeto es falsy — lo que mantiene funcionando los sniffs de IE antiguo y a la vez le da a la comparación un objeto real que encontrar.
La imagen de un elemento <svg> se rasteriza del marcado de su subárbol y se guarda hasta que cambia el elemento mismo o su lista de hijos. Un cambio más abajo — el atributo d de un path, por ejemplo — dejaba la imagen vieja en pantalla. Por eso el botón de play nunca pasaba a pause, mucho después de que la reproducción ya funcionara.
Un IntersectionObserver se quita de su documento cuando se desconecta o pierde su último objetivo, pero se quedaba marcado como conectado, así que observar un objetivo después no lo volvía a poner y no informaba de nada desde ese momento, sin decirlo. YouTube desconecta y reconecta el observer con el que carga imágenes.
Y un <svg> en línea se rasterizaba con una escala distinta en cada eje para rellenar su caja, como se hace con una imagen. Su caja es un viewport: el dibujo se escala ahora de forma uniforme y se centra, que es lo que hace preserveAspectRatio por defecto. Los otros valores del atributo siguen sin mirarse.
Y los arreglos de layout que hay debajo
Una página como YouTube ejercita suficiente CSS para encontrar otra clase de bug, de los que no son la feature de nadie y el problema de todos.
mask se construye ahora como clips cuando se puede. Una capa de máscara cuya imagen es opaca en todas partes muestra el elemento dentro de la caja que nombra su mask-clip y lo oculta fuera, así que componer capas así es pura geometría: un rectángulo redondeado, o uno con otro más pequeño recortado con un clip ClipOut. Eso cubre la manera habitual de dibujar un borde con degradado, donde se componen dos degradados de color plano para abrir un hueco.
backdrop-filter se recortaba solo a un rectángulo, porque WebRender recorta uno así, de modo que una caja con border-radius filtraba su fondo hasta las esquinas cuadradas. Los radios reciben ahora un clip propio encima de la cadena heredada.
Tres más, cada uno de una línea y con cola larga. El recorrido hacia arriba por el árbol plano buscando un bloque contenedor no pasaba de un ancestro sin estilo y sin caja — pedía otra vez el padre del mismo nodo y no terminaba nunca. El layout incremental reconstruía solo la caja cuando un restyle hacía que una caja en flujo pasara a posicionada absolutamente o flotada, así que la caja vieja mantenía su sitio en el flujo; ahora se reconstruye el padre. Y un elemento quitado del árbol mientras estaba en hover se quedaba como objetivo del hover, así que el siguiente movimiento de ratón subía desde un nodo desconectado, no llegaba nunca a los ancestros que seguían en el árbol, y los dejaba con :hover y sin mouseleave.
Dos sobre :has() y <svg>, que se encontraron entre ellos. Stylo calcula de qué elementos depende un selector relativo, pero eso solo está cableado para Gecko, así que un estilo que venía de :has() se quedaba como estaba después de la primera pasada. Un cambio en los hijos de un nodo, o en un atributo que no sea style, reestiliza ahora los ancestros de donde ha pasado — más grueso de lo que Stylo podría y todavía barato, porque un elemento cuyo estilo sale igual detiene el recorrido. Y serializar un elemento <svg> leía el color que hereda con Element::style, que pide un reflow; ejecutándose dentro de Window::reflow mientras este atendía los resultados de uno, ese reflow anidado encontraba el mismo elemento pendiente y recursaba hasta desbordar la pila.
Lo que sigue sin reproducirse
El soporte de códecs es el que den los plugins de GStreamer de la máquina, lo que significa que un build puede pasar un canPlayType y luego fallar con el stream de verdad. Los medios cifrados no están en absoluto — no hay EME, así que lo que esté detrás de DRM no es cuestión de bugs.
preserveAspectRatio se lee como su valor por defecto y nada más. Y nada de esto hace rápido el sitio: los frames pasan por un pipeline de demux por source buffer, que es correcto y no es lo mismo que eficiente.