Reproducción de videos en la Web móvil

François Beaufort
François Beaufort

¿Cómo se crea la mejor experiencia de contenido multimedia para dispositivos móviles en la Web? ¡Fácil! Todo depende de la participación de los usuarios y de la importancia que le des al contenido multimedia en una página web. Creo que todos estamos de acuerdo en que, si el video es EL motivo de la visita de un usuario, su experiencia debe ser envolvente y atractiva.

reproducción de videos en la Web para dispositivos móviles

En este artículo, te mostramos cómo mejorar de forma progresiva tu experiencia multimedia y hacerla más envolvente gracias a una gran cantidad de APIs web. Por eso, crearemos una experiencia de reproductor móvil sencilla con controles personalizados, pantalla completa y reproducción en segundo plano. Puedes probar la muestra ahora y encontrar el código en nuestro repositorio de GitHub.

Controles personalizados

Diseño HTML
Figura 1.Diseño HTML

Como puedes ver, el diseño HTML que usaremos para nuestro reproductor multimedia es bastante simple: un elemento raíz <div> contiene un elemento multimedia <video> y un elemento secundario <div> dedicado a los controles de video.

Los controles de video que abordaremos más adelante incluyen un botón de reproducción/pausa, un botón de pantalla completa, botones para retroceder y avanzar, y algunos elementos para la hora actual, la duración y el seguimiento del tiempo.

<div id="videoContainer">
  <video id="video" src="file.mp4"></video>
  <div id="videoControls"></div>
</div>

Lee los metadatos de los videos

Primero, esperemos a que se carguen los metadatos del video para establecer su duración, la hora actual y, luego, inicializar la barra de progreso. Ten en cuenta que la función secondsToTimeCode() es una función de utilidad personalizada que escribí para convertir una cantidad de segundos en una cadena con el formato "hh:mm:ss", que es más adecuado en nuestro caso.

<div id="videoContainer">
  <video id="video" src="file.mp4"></video>
  <div id="videoControls">
    <strong>
      <div id="videoCurrentTime"></div>
      <div id="videoDuration"></div>
      <div id="videoProgressBar"></div>
    </strong>
  </div>
</div>
video.addEventListener('loadedmetadata', function () {
  videoDuration.textContent = secondsToTimeCode(video.duration);
  videoCurrentTime.textContent = secondsToTimeCode(video.currentTime);
  videoProgressBar.style.transform = `scaleX(${
    video.currentTime / video.duration
  })`;
});
solo metadatos de video
Figura 2. Media Player con metadatos de video

Reproducir/pausar video

Ahora que se cargaron los metadatos del video, agreguemos nuestro primer botón que permita al usuario reproducir y pausar el video con video.play() y video.pause() según su estado de reproducción.

<div id="videoContainer">
  <video id="video" src="file.mp4"></video>
  <div id="videoControls">
    <strong><button id="playPauseButton"></button></strong>
    <div id="videoCurrentTime"></div>
    <div id="videoDuration"></div>
    <div id="videoProgressBar"></div>
  </div>
</div>
playPauseButton.addEventListener('click', function (event) {
  event.stopPropagation();
  if (video.paused) {
    video.play();
  } else {
    video.pause();
  }
});

En lugar de ajustar nuestros controles de video en el objeto de escucha de eventos click, usamos los eventos de video play y pause. Basar nuestros controles en eventos ayuda con la flexibilidad (como veremos más adelante con la API de Media Session) y nos permitirá mantener nuestros controles sincronizados si el navegador interviene en la reproducción. Cuando el video comienza a reproducirse, cambiamos el estado del botón a "pausar" y ocultamos los controles de video. Cuando se pausa el video, simplemente cambiamos el estado del botón a "reproducir" y mostramos los controles de video.

video.addEventListener('play', function () {
  playPauseButton.classList.add('playing');
});

video.addEventListener('pause', function () {
  playPauseButton.classList.remove('playing');
});

Cuando el tiempo indicado por el atributo currentTime del video cambia a través del evento de video timeupdate, también actualizamos nuestros controles personalizados si están visibles.

video.addEventListener('timeupdate', function () {
  if (videoControls.classList.contains('visible')) {
    videoCurrentTime.textContent = secondsToTimeCode(video.currentTime);
    videoProgressBar.style.transform = `scaleX(${
      video.currentTime / video.duration
    })`;
  }
});

Cuando finaliza el video, simplemente cambiamos el estado del botón a "reproducir", volvemos a establecer el valor de currentTime del video en 0 y, por ahora, mostramos los controles del video. Ten en cuenta que también podríamos elegir cargar automáticamente otro video si el usuario habilitó algún tipo de función de "Reproducción automática".

video.addEventListener('ended', function () {
  playPauseButton.classList.remove('playing');
  video.currentTime = 0;
});

Retroceder y avanzar

Continuemos y agreguemos los botones "retroceder" y "avanzar" para que el usuario pueda omitir contenido con facilidad.

<div id="videoContainer">
  <video id="video" src="file.mp4"></video>
  <div id="videoControls">
    <button id="playPauseButton"></button>
    <strong
      ><button id="seekForwardButton"></button>
      <button id="seekBackwardButton"></button
    ></strong>
    <div id="videoCurrentTime"></div>
    <div id="videoDuration"></div>
    <div id="videoProgressBar"></div>
  </div>
</div>
var skipTime = 10; // Time to skip in seconds

seekForwardButton.addEventListener('click', function (event) {
  event.stopPropagation();
  video.currentTime = Math.min(video.currentTime + skipTime, video.duration);
});

seekBackwardButton.addEventListener('click', function (event) {
  event.stopPropagation();
  video.currentTime = Math.max(video.currentTime - skipTime, 0);
});

Como antes, en lugar de ajustar el diseño del video en los objetos de escucha de eventos click de estos botones, usaremos los eventos de video seeking y seeked activados para ajustar el brillo del video. Mi clase de CSS personalizada seeking es tan simple como filter: brightness(0);.

video.addEventListener('seeking', function () {
  video.classList.add('seeking');
});

video.addEventListener('seeked', function () {
  video.classList.remove('seeking');
});

A continuación, se muestra lo que creamos hasta ahora. En la siguiente sección, implementaremos el botón de pantalla completa.

Pantalla completa

Aquí, aprovecharemos varias APIs web para crear una experiencia de pantalla completa perfecta y sin inconvenientes. Para ver cómo funciona, consulta el ejemplo.

Obviamente, no tienes que usarlos todos. Solo elige las que te parezcan adecuadas y combínalas para crear tu flujo personalizado.

Cómo impedir la pantalla completa automática

En iOS, los elementos video ingresan automáticamente al modo de pantalla completa cuando comienza la reproducción de contenido multimedia. Como intentamos adaptar y controlar lo más posible nuestra experiencia multimedia en los navegadores para dispositivos móviles, te recomiendo que configures el atributo playsinline del elemento video para forzarlo a reproducirse intercalado en iPhone y no ingresar al modo de pantalla completa cuando comience la reproducción. Ten en cuenta que esto no tiene efectos secundarios en otros navegadores.

<div id="videoContainer"></div>
  <video id="video" src="file.mp4"></video><strong>playsinline</strong></video>
  <div id="videoControls">...</div>
</div>

Activar o desactivar la pantalla completa cuando se hace clic en el botón

Ahora que evitamos la pantalla completa automática, debemos controlar el modo de pantalla completa del video con la API de pantalla completa. Cuando el usuario haga clic en el "botón de pantalla completa", sal del modo de pantalla completa con document.exitFullscreen() si el documento está usando el modo de pantalla completa en ese momento. De lo contrario, solicita la pantalla completa en el contenedor de video con el método requestFullscreen() si está disponible o recurre a webkitEnterFullscreen() en el elemento de video solo en iOS.

<div id="videoContainer">
  <video id="video" src="file.mp4"></video>
  <div id="videoControls">
    <button id="playPauseButton"></button>
    <button id="seekForwardButton"></button>
    <button id="seekBackwardButton"></button>
    <strong><button id="fullscreenButton"></button></strong>
    <div id="videoCurrentTime"></div>
    <div id="videoDuration"></div>
    <div id="videoProgressBar"></div>
  </div>
</div>
fullscreenButton.addEventListener('click', function (event) {
  event.stopPropagation();
  if (document.fullscreenElement) {
    document.exitFullscreen();
  } else {
    requestFullscreenVideo();
  }
});

function requestFullscreenVideo() {
  if (videoContainer.requestFullscreen) {
    videoContainer.requestFullscreen();
  } else {
    video.webkitEnterFullscreen();
  }
}

document.addEventListener('fullscreenchange', function () {
  fullscreenButton.classList.toggle('active', document.fullscreenElement);
});

Activar o desactivar la pantalla completa cuando cambia la orientación de la pantalla

A medida que el usuario rota el dispositivo en modo horizontal, seamos inteligentes y solicitemos automáticamente la pantalla completa para crear una experiencia envolvente. Para ello, necesitaremos la API de Screen Orientation, que aún no es compatible en todas partes y que, en ese momento, todavía tiene prefijos en algunos navegadores. Por lo tanto, esta será nuestra primera mejora progresiva.

¿Cómo funciona? En cuanto detectemos que cambia la orientación de la pantalla, solicitaremos la pantalla completa si la ventana del navegador está en modo horizontal (es decir, su ancho es mayor que su altura). Si no es así, salgamos de la pantalla completa. Eso es todo.

if ('orientation' in screen) {
  screen.orientation.addEventListener('change', function () {
    // Let's request fullscreen if user switches device in landscape mode.
    if (screen.orientation.type.startsWith('landscape')) {
      requestFullscreenVideo();
    } else if (document.fullscreenElement) {
      document.exitFullscreen();
    }
  });
}

Bloquear la pantalla en modo horizontal cuando se hace clic en el botón

Como los videos se ven mejor en modo horizontal, es posible que quieras bloquear la pantalla en modo horizontal cuando el usuario haga clic en el botón de pantalla completa. Combinaremos la API de Screen Orientation que usamos antes y algunas consultas de medios para asegurarnos de que esta experiencia sea la mejor.

Bloquear la pantalla en modo horizontal es tan fácil como llamar a screen.orientation.lock('landscape'). Sin embargo, deberíamos hacerlo solo cuando el dispositivo esté en modo Retrato con matchMedia('(orientation: portrait)') y se pueda sostener con una mano con matchMedia('(max-device-width: 768px)'), ya que esta no sería una buena experiencia para los usuarios de tablets.

fullscreenButton.addEventListener('click', function (event) {
  event.stopPropagation();
  if (document.fullscreenElement) {
    document.exitFullscreen();
  } else {
    requestFullscreenVideo();
    <strong>lockScreenInLandscape();</strong>;
  }
});
function lockScreenInLandscape() {
  if (!('orientation' in screen)) {
    return;
  }
  // Let's force landscape mode only if device is in portrait mode and can be held in one hand.
  if (
    matchMedia('(orientation: portrait) and (max-device-width: 768px)').matches
  ) {
    screen.orientation.lock('landscape');
  }
}

Desbloquea la pantalla cuando cambia la orientación del dispositivo

Sin embargo, es posible que hayas notado que la experiencia de la pantalla de bloqueo que acabamos de crear no es perfecta, ya que no recibimos cambios de orientación de la pantalla cuando está bloqueada.

Para solucionar este problema, usemos la API de Device Orientation si está disponible. Esta API proporciona información del hardware que mide la posición y el movimiento de un dispositivo en el espacio: el giroscopio y la brújula digital para su orientación, y el acelerómetro para su velocidad. Cuando detectamos un cambio de orientación del dispositivo, desbloqueamos la pantalla con screen.orientation.unlock() si el usuario sostiene el dispositivo en modo Retrato y la pantalla está bloqueada en modo horizontal.

function lockScreenInLandscape() {
  if (!('orientation' in screen)) {
    return;
  }
  // Let's force landscape mode only if device is in portrait mode and can be held in one hand.
  if (matchMedia('(orientation: portrait) and (max-device-width: 768px)').matches) {
    screen.orientation.lock('landscape')
    <strong>.then(function() {
      listenToDeviceOrientationChanges();
    })</strong>;
  }
}
function listenToDeviceOrientationChanges() {
  if (!('DeviceOrientationEvent' in window)) {
    return;
  }
  var previousDeviceOrientation, currentDeviceOrientation;
  window.addEventListener(
    'deviceorientation',
    function onDeviceOrientationChange(event) {
      // event.beta represents a front to back motion of the device and
      // event.gamma a left to right motion.
      if (Math.abs(event.gamma) > 10 || Math.abs(event.beta) < 10) {
        previousDeviceOrientation = currentDeviceOrientation;
        currentDeviceOrientation = 'landscape';
        return;
      }
      if (Math.abs(event.gamma) < 10 || Math.abs(event.beta) > 10) {
        previousDeviceOrientation = currentDeviceOrientation;
        // When device is rotated back to portrait, let's unlock screen orientation.
        if (previousDeviceOrientation == 'landscape') {
          screen.orientation.unlock();
          window.removeEventListener(
            'deviceorientation',
            onDeviceOrientationChange,
          );
        }
      }
    },
  );
}

Como puedes ver, esta es la experiencia fluida de pantalla completa que estábamos buscando. Para ver esto en acción, consulta la muestra.

Reproducción en segundo plano

Cuando detectes que una página web o un video de la página web ya no están visibles, es posible que quieras actualizar tus estadísticas para reflejar este cambio. Esto también podría afectar la reproducción actual, como elegir una pista diferente, pausarla o incluso mostrar botones personalizados al usuario, por ejemplo.

Pausar el video cuando cambie la visibilidad de la página

Con la API de visibilidad de páginas, podemos determinar la visibilidad actual de una página y recibir notificaciones sobre los cambios de visibilidad. El código que aparece a continuación pausa el video cuando la página está oculta. Esto sucede, por ejemplo, cuando el bloqueo de pantalla está activo o cuando cambias de pestaña.

Como la mayoría de los navegadores para dispositivos móviles ahora ofrecen controles fuera del navegador que permiten reanudar un video pausado, te recomendamos que configures este comportamiento solo si el usuario puede reproducir contenido en segundo plano.

document.addEventListener('visibilitychange', function () {
  // Pause video when page is hidden.
  if (document.hidden) {
    video.pause();
  }
});

Muestra u oculta el botón para silenciar cuando cambia la visibilidad del video

Si usas la nueva API de Intersection Observer, puedes obtener un nivel de detalle aún mayor sin costo. Esta API te permite saber cuándo un elemento observado entra o sale del viewport del navegador.

Mostremos u ocultemos un botón para silenciar según la visibilidad del video en la página. Si el video se está reproduciendo, pero no se ve, se mostrará un botón para silenciar pequeño en la esquina inferior derecha de la página para que el usuario pueda controlar el sonido del video. El evento de video volumechange se usa para actualizar el estilo del botón para silenciar.

<button id="muteButton"></button>
if ('IntersectionObserver' in window) {
  // Show/hide mute button based on video visibility in the page.
  function onIntersection(entries) {
    entries.forEach(function (entry) {
      muteButton.hidden = video.paused || entry.isIntersecting;
    });
  }
  var observer = new IntersectionObserver(onIntersection);
  observer.observe(video);
}

muteButton.addEventListener('click', function () {
  // Mute/unmute video on button click.
  video.muted = !video.muted;
});

video.addEventListener('volumechange', function () {
  muteButton.classList.toggle('active', video.muted);
});

Reproduce solo un video a la vez

Si hay más de un video en una página, te sugerimos que solo reproduzcas uno y pauses los demás automáticamente para que el usuario no tenga que escuchar varias pistas de audio que se reproducen de forma simultánea.

// This array should be initialized once all videos have been added.
var videos = Array.from(document.querySelectorAll('video'));

videos.forEach(function (video) {
  video.addEventListener('play', pauseOtherVideosPlaying);
});

function pauseOtherVideosPlaying(event) {
  var videosToPause = videos.filter(function (video) {
    return !video.paused && video != event.target;
  });
  // Pause all other videos currently playing.
  videosToPause.forEach(function (video) {
    video.pause();
  });
}

Cómo personalizar las notificaciones de contenido multimedia

Con la API de Media Session, también puedes personalizar las notificaciones de contenido multimedia proporcionando metadatos para el video que se está reproduciendo. También te permite controlar eventos relacionados con el contenido multimedia, como la búsqueda o el cambio de pistas, que pueden provenir de notificaciones o teclas multimedia. Para ver esto en acción, consulta la muestra.

Cuando tu app web reproduce audio o video, puedes ver una notificación de contenido multimedia en la bandeja de notificaciones. En Android, Chrome hace todo lo posible para mostrar la información adecuada con el título del documento y la imagen de ícono más grande que pueda encontrar.

Veamos cómo personalizar esta notificación de contenido multimedia configurando algunos metadatos de la sesión multimedia, como el título, el artista, el nombre del álbum y la carátula con la API de Media Session.

playPauseButton.addEventListener('click', function(event) {
  event.stopPropagation();
  if (video.paused) {
    video.play()
    <strong>.then(function() {
      setMediaSession();
    });</strong>
  } else {
    video.pause();
  }
});
function setMediaSession() {
  if (!('mediaSession' in navigator)) {
    return;
  }
  navigator.mediaSession.metadata = new MediaMetadata({
    title: 'Never Gonna Give You Up',
    artist: 'Rick Astley',
    album: 'Whenever You Need Somebody',
    artwork: [
      {src: 'https://dummyimage.com/96x96', sizes: '96x96', type: 'image/png'},
      {
        src: 'https://dummyimage.com/128x128',
        sizes: '128x128',
        type: 'image/png',
      },
      {
        src: 'https://dummyimage.com/192x192',
        sizes: '192x192',
        type: 'image/png',
      },
      {
        src: 'https://dummyimage.com/256x256',
        sizes: '256x256',
        type: 'image/png',
      },
      {
        src: 'https://dummyimage.com/384x384',
        sizes: '384x384',
        type: 'image/png',
      },
      {
        src: 'https://dummyimage.com/512x512',
        sizes: '512x512',
        type: 'image/png',
      },
    ],
  });
}

Una vez que finalice la reproducción, no tendrás que "liberar" la sesión multimedia, ya que la notificación desaparecerá automáticamente. Ten en cuenta que se usará el navigator.mediaSession.metadata actual cuando se inicie cualquier reproducción. Por este motivo, debes actualizarla para asegurarte de mostrar siempre información relevante en la notificación de contenido multimedia.

Si tu app web proporciona una playlist, es posible que quieras permitir que el usuario navegue por ella directamente desde la notificación de contenido multimedia con algunos íconos de "Pista anterior" y "Pista siguiente".

if ('mediaSession' in navigator) {
  navigator.mediaSession.setActionHandler('previoustrack', function () {
    // User clicked "Previous Track" media notification icon.
    playPreviousVideo(); // load and play previous video
  });
  navigator.mediaSession.setActionHandler('nexttrack', function () {
    // User clicked "Next Track" media notification icon.
    playNextVideo(); // load and play next video
  });
}

Ten en cuenta que los controladores de acciones de contenido multimedia persistirán. Esto es muy similar al patrón de objeto de escucha de eventos, excepto que controlar un evento significa que el navegador deja de realizar cualquier comportamiento predeterminado y usa esto como una señal de que tu app web admite la acción de contenido multimedia. Por lo tanto, no se mostrarán los controles de acciones de contenido multimedia, a menos que configures el controlador de acciones adecuado.

Por cierto, anular la configuración de un controlador de acciones de contenido multimedia es tan fácil como asignarlo a null.

La API de Media Session te permite mostrar los íconos de notificación de contenido multimedia "Retroceder" y "Avanzar" si quieres controlar la cantidad de tiempo que se omite.

if ('mediaSession' in navigator) {
  let skipTime = 10; // Time to skip in seconds

  navigator.mediaSession.setActionHandler('seekbackward', function () {
    // User clicked "Seek Backward" media notification icon.
    video.currentTime = Math.max(video.currentTime - skipTime, 0);
  });
  navigator.mediaSession.setActionHandler('seekforward', function () {
    // User clicked "Seek Forward" media notification icon.
    video.currentTime = Math.min(video.currentTime + skipTime, video.duration);
  });
}

El ícono "Reproducir/Pausar" siempre se muestra en la notificación de contenido multimedia, y el navegador controla automáticamente los eventos relacionados. Si, por algún motivo, el comportamiento predeterminado no funciona, puedes controlar los eventos multimedia de "Reproducir" y "Pausar".

Lo bueno de la API de Media Session es que la bandeja de notificaciones no es el único lugar donde se pueden ver los metadatos y los controles de contenido multimedia. La notificación de contenido multimedia se sincroniza automáticamente con cualquier dispositivo wearable vinculado. Y también aparece en las pantallas de bloqueo.

Comentarios