Reproducción de videos web para dispositivos móviles

François Beaufort
François Beaufort

¿Cómo se crea la mejor experiencia de medios para dispositivos móviles en la Web? ¡Fácil! Todo depende de la participación del usuario y de la importancia que des a los medios 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, la experiencia del usuario tiene que ser envolvente y volver a atraer.

Reproducción de videos en la Web móvil

En este artículo, te muestro cómo mejorar de manera progresiva tu experiencia multimedia y hacer que sea más envolvente gracias a una plétora de APIs web. Por eso, crearemos una experiencia sencilla de reproductor para dispositivos móviles 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 de búsqueda para retroceder y avanzar, y algunos elementos de tiempo actual, duración y seguimiento del tiempo.

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

Lee metadatos de video

Primero, esperemos a que se carguen los metadatos del video para establecer la duración del video, 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í y que convierte una cantidad de segundos en una string en formato "hh:mm:ss", que es más adecuada 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: Reproductor multimedia que muestra metadatos de video

Reproducir/pausar video

Ahora que los metadatos de video están cargados, agreguemos el primer botón que permite al usuario reproducir y pausar un 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 los controles de video en el objeto de escucha de eventos click, usamos los eventos de video play y pause. Hacer que nuestros controles estén basados en eventos ayuda con 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 del 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 son visibles.

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

Cuando termina el video, simplemente cambiamos el estado del botón a "reproducir", vuelve a establecer el video currentTime en 0 y, por el momento, mostramos los controles de 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;
});

Avanzar y retroceder

Continuemos y agreguemos los botones "buscar hacia atrás" y "adelantar" 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 estilo de video en los objetos de escucha de eventos click de estos botones, usaremos los eventos de video activados seeking y seeked para ajustar el brillo del video. Mi clase seeking personalizada de CSS 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, te mostramos 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 interrupciones. Para ver cómo funciona, consulta la muestra.

Obviamente, no es necesario que los uses todos. Solo elige las que sean adecuadas para ti y combínalas para crear tu flujo personalizado.

Cómo evitar la pantalla completa automática

En iOS, los elementos video entran mágicamente en el modo de pantalla completa cuando comienza la reproducción de contenido multimedia. Como intentamos adaptar y controlar tanto como sea posible nuestra experiencia multimedia en navegadores para dispositivos móviles, te recomendamos que configures el atributo playsinline del elemento video para forzar la reproducción intercalada en iPhone y que no entre en 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 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 para el video con la API de pantalla completa. Cuando el usuario haga clic en el "botón de pantalla completa", salgamos del modo de pantalla completa con document.exitFullscreen() si el documento lo está usando. De lo contrario, solicita el modo 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 pantalla completa cuando cambia la orientación de la pantalla

A medida que el usuario rota el dispositivo en modo horizontal, se debe actuar de manera inteligente y solicitar automáticamente la pantalla completa para crear una experiencia envolvente. Para ello, necesitaremos la API de Screen Orientation, que aún no se admite en todas partes y aún tiene el prefijo en algunos navegadores en ese momento. Por lo tanto, esta será nuestra primera mejora progresiva.

¿Cómo funciona? En cuanto detectemos que cambia la orientación de la pantalla, solicitemos la pantalla completa si la ventana del navegador está en modo horizontal (es decir, si su ancho es mayor que su altura). De lo contrario, 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();
    }
  });
}

Pantalla de bloqueo horizontal cuando se hace clic en un botón

Dado que el video se puede ver mejor en modo horizontal, recomendamos bloquear la pantalla en ese modo cuando el usuario haga clic en el botón de pantalla completa. Combinaremos la API de Screen Orientation que se usaba anteriormente con algunas consultas de contenido multimedia para asegurarnos de que esta experiencia sea la mejor.

Bloquear la pantalla en orientación horizontal es tan fácil como llamar a screen.orientation.lock('landscape'). Sin embargo, solo debemos hacer esto cuando el dispositivo está en modo vertical con matchMedia('(orientation: portrait)') y se puede sostener en una mano con matchMedia('(max-device-width: 768px)'), ya que esto no sería una gran 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');
  }
}

Desbloquear la pantalla al cambiar la orientación del dispositivo

Es posible que hayas notado que la experiencia de 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 en la orientación del dispositivo, desbloquearemos la pantalla con screen.orientation.unlock() si el usuario sostiene el dispositivo en modo vertical 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 de pantalla completa fluida que buscábamos. Para ver cómo funciona, consulta la muestra.

Reproducción en segundo plano

Cuando detectes que una página web o un video ya no es visible, te recomendamos que actualices tus estadísticas para que reflejen el tiempo real. Esto también podría afectar la reproducción en el momento, como cuando se selecciona una pista diferente, se pausa o incluso se muestran botones personalizados al usuario.

Detener el video en el cambio de 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 de los cambios de visibilidad. El código debajo pausa el video cuando la página está oculta. Esto sucede cuando el bloqueo de pantalla está activo o cuando cambias de pestaña, por ejemplo.

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

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

Mostrar/ocultar el botón para silenciar durante el cambio de visibilidad del video

Si usas la nueva API de Intersection Observer, puedes obtener un nivel aún más detallado sin costo. Esta API te avisa cuando un elemento observado entra en el viewport del navegador o sale de él.

Ocultar o mostrar el botón de silencio según la visibilidad del video en la página Si se está reproduciendo un video, pero no es visible en ese momento, aparecerá un pequeño botón para silenciar en la esquina inferior derecha de la página a fin de que el usuario pueda controlar el sonido del video. El evento de video volumechange se usa para actualizar el estilo del botón de silencio.

<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);
});

Reproducir 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 otros automáticamente, de modo que el usuario no tenga que escuchar varias pistas de audio reproduciéndose simultáneamente.

// 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();
  });
}

Personalizar notificaciones multimedia

Con la API de Media Session, también puedes personalizar las notificaciones multimedia proporcionando metadatos del video que se está reproduciendo. También te permite controlar eventos relacionados con contenido multimedia, como la búsqueda o el cambio de seguimiento, 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, ya puedes ver una notificación multimedia en la bandeja de notificaciones. En Android, Chrome hace todo lo posible para mostrar la información apropiada mediante el título del documento y la imagen de ícono más grande que pueda encontrar.

Veamos cómo personalizar esta notificación multimedia configurando algunos metadatos de la sesión multimedia, como el título, el artista, el nombre del álbum y el material gráfico 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 finaliza la reproducción, no tienes 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 eso, debes actualizarlo para asegurarte de mostrar siempre información relevante en la notificación multimedia.

Si tu app web proporciona una lista de reproducción, es posible que quieras permitirle al usuario navegar por ella directamente desde la notificación 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 se conservarán los controladores de acciones multimedia. Es muy similar al patrón del objeto de escucha de eventos, con la excepción de que controlar un evento significa que el navegador deja de realizar cualquier comportamiento predeterminado y lo usa como indicador de que tu aplicación web admite la acción multimedia. Por lo tanto, los controles de acciones multimedia no se mostrarán, a menos que establezcas el controlador de acciones adecuado.

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

La API de Media Session te permite mostrar los íconos de notificaciones multimedia "Seek Backward" y "Seek Forward" si deseas 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 de "Reproducir/Pausar" siempre se muestra en la notificación 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 en el que los metadatos y los controles multimedia son visibles. La notificación multimedia se sincroniza automáticamente con cualquier dispositivo wearable vinculado. También aparece en las pantallas bloqueadas.

Comentarios