Reproducción de videos web para dispositivos móviles

François Beaufort
François Beaufort

¿Cómo puedes crear la mejor experiencia multimedia para dispositivos móviles en la Web? ¡Fácil! Todo depende de la participación del usuario 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 del usuario, la experiencia del usuario debe ser envolvente y atractiva.

reproducción de video web móvil

En este artículo, te mostraré 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 para dispositivos móviles simple 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 analizaremos más adelante incluyen: un botón de reproducción y pausa, un botón de pantalla completa, botones de avance y retroceso, 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>

Cómo leer metadatos de video

Primero, esperemos a que se carguen los metadatos del video para configurar la duración del video y la hora actual, e inicializar la barra de progreso. Ten en cuenta que la función secondsToTimeCode() es una función de utilidad personalizada que escribí que convierte una cantidad de segundos en una cadena 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 del video están cargados, agreguemos nuestro primer botón que le permite 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. Hacer que nuestros controles se basen 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 “pausa” y ocultamos los controles del video. Cuando se pausa el video, solo 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 cambia la hora indicada por el atributo currentTime del video 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 termina el video, simplemente cambiamos el estado del botón a "reproducir", restablecemos currentTime del video a 0 y mostramos los controles de video por el momento. Ten en cuenta que también podríamos 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 fácilmente parte del contenido.

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

Al igual que 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 seeking personalizada 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 próxima 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 fluida. Para ver cómo funciona, consulta el ejemplo.

Desde luego, no es necesario que las uses a todas. Solo elige los que te resulten útiles y combínalos para crear tu flujo personalizado.

Cómo impedir el modo de pantalla completa automático

En iOS, los elementos video ingresan automáticamente al modo de pantalla completa automáticamente cuando comienza la reproducción de contenido multimedia. Como intentamos adaptar y controlar nuestra experiencia multimedia en los navegadores para dispositivos móviles tanto como sea posible, te recomendamos que configures el atributo playsinline del elemento video para forzar que se reproduzca intercalado en iPhone y no entre en el 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 con un clic en el botón

Ahora que evitamos el modo de 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”, salgamos del modo de pantalla completa con document.exitFullscreen() si el documento está usando ese modo. De lo contrario, solicita el modo de 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

Cuando el usuario rota el dispositivo en modo horizontal, debemos ser inteligentes y solicitar automáticamente el modo de 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 aún tiene un prefijo en algunos navegadores en ese momento. Por lo tanto, esta será nuestra primera mejora progresiva.

¿Cómo funciona? En cuanto detectemos los cambios de 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();
    }
  });
}

Bloquear pantalla en orientación horizontal cuando se hace clic en un botón

Dado que los videos se pueden ver mejor en modo horizontal, es posible que queramos bloquear la pantalla en este modo cuando el usuario hace clic en el "botón de pantalla completa". Combinaremos la API de Screen Orientation que usamos anteriormente y algunas consultas multimedia 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, debemos hacerlo solo cuando el dispositivo esté en modo vertical con matchMedia('(orientation: portrait)') y se pueda sostener con una mano con matchMedia('(max-device-width: 768px)'), ya que 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');
  }
}

Desbloquea la pantalla cuando cambia la orientación del dispositivo

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 detectemos un cambio de orientación del dispositivo, desbloqueemos 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 sin interrupciones que buscábamos. Para ver esto en acción, consulta el ejemplo.

Reproducción en segundo plano

Cuando detectes que una página web o un video en ella ya no son visibles, te recomendamos que actualices tus estadísticas para que reflejen esta situación. Esto también podría afectar la reproducción actual, como elegir una pista diferente, pausarla o incluso mostrarle botones personalizados al usuario.

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

Como la mayoría de los navegadores para dispositivos móviles ahora ofrecen controles fuera del navegador que permiten reanudar un video pausado, te recomiendo 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();
  }
});

Ocultar o mostrar el botón para silenciar cuando se cambia la visibilidad del video

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

Mostremos o ocultemos un botón para silenciar según la visibilidad del video en la página. Si el video se está reproduciendo, pero no está visible actualmente, aparecerá un botón mini para silenciar en la esquina inferior derecha de la página para que el usuario controle 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 sugiero que solo reproduzcas uno y pauses los demás automáticamente para que el usuario no tenga que escuchar varias pistas de audio reproduciéndose 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 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 contenido multimedia, como la búsqueda o el cambio de seguimiento, que pueden provenir de notificaciones o teclas multimedia. Para ver cómo funciona, consulta el ejemplo.

Cuando tu app web reproduce audio o video, ya 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 del ícono más grande que pueda encontrar.

Veamos cómo personalizar esta notificación multimedia mediante la configuración de 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 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 comience cualquier reproducción. Por este motivo, debes actualizarla para asegurarte de mostrar siempre información relevante en la notificación multimedia.

Si tu app web proporciona una playlist, es posible que quieras permitir que el usuario navegue 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 los controladores de acciones 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 lo usa como una señal de que tu app web admite la acción multimedia. Por lo tanto, no se mostrarán los controles de acción multimedia, a menos que configures 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 notificación multimedia "Retroceso" y "Avance rápido" 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 "Reproducir" y "Pausar".

Lo interesante 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 multimedia. La notificación multimedia se sincroniza automáticamente con cualquier dispositivo wearable vinculado. También aparece en las pantallas de bloqueo.

Comentarios