Reproducción automática: las condiciones exactas y qué hacer cuando falla
Las reglas que aplican los navegadores para permitir o bloquear el autoplay, el índice de interacción con medios, por qué play() devuelve una promesa que hay que capturar, y el patrón correcto de degradación.
La reproducción automática lleva más de una década siendo un campo minado, y el estado actual es el resultado de una escalada: los sitios abusaron del vídeo con sonido, los navegadores respondieron bloqueando, los sitios buscaron rodeos, los navegadores endurecieron las reglas. Hoy las condiciones son razonablemente predecibles y están documentadas, pero incluyen un componente heurístico por usuario que hace imposible garantizar nada. Escribir código que asuma que la reproducción va a arrancar es escribir código que falla en producción.
- Enumerar las condiciones bajo las que cada motor permite reproducción automática.
- Explicar qué es el índice de interacción con medios y por qué rompe las pruebas locales.
- Manejar correctamente la promesa que devuelve
play()y todos sus modos de fallo. - Implementar la degradación a controles visibles sin desplazar la maquetación.
Las condiciones, motor por motor
La regla común a todos los navegadores modernos es una: el vídeo sin sonido se reproduce solo; el vídeo con sonido no, salvo que el usuario haya demostrado interés.
En términos de atributos, la combinación que funciona en todas partes es esta:
<video autoplay muted playsinline loop
poster="fondo-lqip.avif"
width="1920" height="1080"
aria-hidden="true" tabindex="-1">
<source src="fondo-av1.mp4" type='video/mp4; codecs="av01.0.05M.08"'>
<source src="fondo-h264.mp4" type='video/mp4; codecs="avc1.640028"'>
</video>
Los cuatro atributos hacen cosas distintas y todos son necesarios:
mutedes la condición de fondo. Sin él, el bloqueo es la norma.playsinlinees imprescindible en iOS. Sin él, Safari en iPhone abre el vídeo a pantalla completa en lugar de reproducirlo en su caja, lo cual arruina cualquier vídeo de fondo. En iPad y en escritorio no hace daño.autoplaydispara el intento.loopes opcional pero, en vídeos de fondo, evita que el elemento se quede congelado en el último fotograma.
Los atributos de accesibilidad de arriba no son decorativos: un vídeo decorativo en bucle no debe estar en el orden de tabulación ni ser anunciado por un lector de pantalla, porque no aporta información.
Sobre las diferencias entre motores, que son menos de las que la gente cree:
Chromium permite reproducción automática siempre que esté silenciado. Con sonido, la permite si el usuario ha interactuado con el dominio, si tiene el sitio en la pantalla de inicio en móvil, o si el índice de interacción con medios del usuario para ese sitio es alto. Ese índice se calcula por origen, en el perfil local, contando reproducciones significativas —más de siete segundos, con sonido, con la pestaña activa y el vídeo de tamaño suficiente— y decae con el tiempo.
Safari aplica una lógica muy parecida y añade un ajuste por sitio que el usuario controla desde la barra de direcciones. Silenciado y con playsinline funciona; con sonido depende del historial del usuario en ese origen y de las preferencias explícitas. Además, Safari en modo de ahorro de energía puede bloquear también el vídeo silenciado.
Firefox bloquea por defecto el audio audible y permite el silenciado, con un panel de permisos por sitio equivalente.
El índice de interacción rompe tus pruebas locales
Este es el punto que hace perder tardes enteras. Como el índice se calcula en el perfil local y sube cada vez que reproduces un vídeo en ese origen, tu navegador de desarrollo acaba teniendo permiso para todo en tu propio dominio. Pruebas el autoplay con sonido, funciona, lo subes, y en el navegador de cualquier usuario nuevo está bloqueado.
La forma de probar en condiciones honestas es abrir una ventana de incógnito, que arranca sin índice acumulado, o borrar los datos del sitio antes de cada prueba. En Chromium hay además una bandera de línea de comandos que fuerza la política estricta, útil para automatización:
chrome --autoplay-policy=document-user-activation-required
Y hay una comprobación que conviene tener en el propio código, porque el estado se puede consultar: el navegador expone si el documento tiene activación del usuario.
// true si el usuario ya ha interactuado alguna vez en esta página
console.log(navigator.userActivation.hasBeenActive);
// true si la interacción es lo bastante reciente para autorizar acciones
console.log(navigator.userActivation.isActive);
play() devuelve una promesa, y hay que capturarla
Este es el error de código más frecuente del tema. HTMLMediaElement.play() devuelve una promesa que se rechaza si el navegador bloquea la reproducción. Si no la capturas, tienes un rechazo no manejado en la consola de todos tus usuarios y, peor, tu interfaz se queda en un estado inconsistente creyendo que el vídeo está reproduciéndose.
El patrón completo, con degradación:
async function arrancarFondo(video) {
video.muted = true; // por si el HTML no lo trae
video.playsInline = true;
try {
await video.play();
return 'reproduciendo';
} catch (err) {
if (err.name === 'NotAllowedError') {
// Bloqueo por politica. Mostrar controles y dejar la decision al usuario.
video.controls = true;
video.setAttribute('aria-hidden', 'false');
video.removeAttribute('tabindex');
return 'bloqueado';
}
if (err.name === 'NotSupportedError') {
// Ninguna fuente reproducible. Caer a la imagen estatica.
video.hidden = true;
video.previousElementSibling?.removeAttribute('hidden');
return 'sin-soporte';
}
if (err.name === 'AbortError') {
// play() interrumpido por un pause() o por quitar el src. No es un fallo real.
return 'abortado';
}
throw err;
}
}
Los cuatro nombres de error son los que verás de verdad. AbortError merece una mención porque es el que más ruido genera sin ser un problema: ocurre cuando llamas a play() y antes de que la promesa se resuelva alguien llama a pause() o cambia el src. Es habitual en carruseles y en aplicaciones de una sola página al navegar. Tratarlo como fallo llena la telemetría de falsos positivos.
La reproducción automática se discute siempre en términos de permiso, y casi nunca en términos de coste. Los números merecen estar sobre la mesa.
Un vídeo de fondo de 1920 por 1080 a 30 fotogramas por segundo, en bucle infinito, obliga al decodificador a producir treinta fotogramas por segundo durante toda la visita, y al compositor a subirlos a la GPU y componerlos treinta veces por segundo. Con decodificación por hardware eso cuesta poco pero no cero. Sin decodificación por hardware —AV1 en un Mac con M1, por ejemplo, o cualquier códec en un Android barato— es un núcleo entero ocupado de forma permanente. El dispositivo se calienta, entra en limitación térmica al cabo de unos minutos, y a partir de ese momento cada interacción del usuario es más lenta, porque la frecuencia de la CPU ha bajado. El INP se degrada en una página que no tiene ningún problema de JavaScript.
Tres mitigaciones concretas, en orden de rentabilidad.
La primera y obligatoria: pausar el vídeo cuando no se ve. Un IntersectionObserver de tres líneas elimina el coste del vídeo de cabecera durante el noventa por ciento de la sesión.
const io = new IntersectionObserver(([e]) => {
if (e.isIntersecting) e.target.play().catch(() => {});
else e.target.pause();
}, { threshold: 0.1 });
io.observe(document.querySelector('.video-fondo'));La segunda: respetar la preferencia de movimiento reducido. Un vídeo de fondo en bucle es movimiento continuo, y hay usuarios para los que eso es un problema médico, no una preferencia estética. matchMedia('(prefers-reduced-motion: reduce)').matches te lo dice, y en ese caso lo correcto es mostrar el póster y no reproducir nada.
La tercera: bajar la resolución del vídeo de fondo mucho más de lo que crees que puedes. Un fondo desenfocado, tapado por un degradado y con texto encima, se ve idéntico a 960 píxeles de ancho y a 1920, y cuesta la cuarta parte de píxeles que decodificar. La comprobación es abrir los dos a pantalla completa y ver si distingues cuál es cuál. Casi nunca se distingue.
Degradar sin desplazar
Cuando la reproducción se bloquea, la interfaz cambia: aparecen controles, o aparece un botón de reproducir. Si ese cambio altera el tamaño de la caja, has generado desplazamiento de contenido en un caso que además es aleatorio por usuario, y por tanto imposible de reproducir cuando llegue el informe.
La regla es que el contenedor tenga su tamaño final desde el primer layout, con aspect-ratio o con width y height en el elemento, y que todo lo que aparezca —controles, botón, capa de aviso— vaya posicionado por encima con position: absolute, sin participar en el flujo.
.fondo-video { position: relative; aspect-ratio: 16 / 9; overflow: hidden; }
.fondo-video > video,
.fondo-video > .capa-reproducir { position: absolute; inset: 0; }
.fondo-video > video { inline-size: 100%; block-size: 100%; object-fit: cover; }
.capa-reproducir { display: grid; place-items: center; background: color-mix(in oklch, black 35%, transparent); }
Y un detalle de accesibilidad que también es de rendimiento percibido: el botón de reproducir que aparece tras un bloqueo debe ser un <button> real, enfocable y con texto accesible, no un <div> con un icono. Si el usuario llegó ahí por una política de bloqueo, es probable que sea un usuario que navega con teclado o con ajustes restrictivos.
Coge un vídeo de fondo de tu sitio. Mide en el panel de rendimiento, con la CPU estrangulada a 4x, el porcentaje de tiempo que el hilo del compositor está ocupado con el vídeo reproduciéndose y con el vídeo pausado. Después implementa el observador de intersección y vuelve a medir con la página desplazada más allá de la cabecera. Por último, abre la página en incógnito con la política estricta activada y comprueba que la degradación no mueve ni un píxel de la maquetación.