Picture in Picture con JavaScript

blog tutorial javascript

Una de las cosas que más disfruto como desarrollador Frontend es aprender nuevas y futuras características. Ya sea un nuevo estándar o una nueva API del navegador. Por ello hoy quisiera hablarte de PIP (Picture-in-Picture). Quédate para que conozcas que es y cómo funciona.

Picture-in-Picture

PIP básicamente permite a los usuarios ver videos en una ventana separada del navegador y siempre por encima de otras ventanas. Esto permite poder ver contenido multimedia mientras se realizan otras actividades.

¿No es fascinante? Y todo esto tan solo con JavaScript ♥️.

Un poco de historia

Todo surge cuando Safari agregó soporte a PIP a través de una API WebKit. Después se replicó este comportamiento en Android, utilizando una API nativa de Android. Posteriormente Google (Chrome) anuncia que quiere estandarizar una API que permita a los desarrolladores controlar esta característica.

Uso de picture-in-Picture

Bien, comencemos con lo primero. Vamos a crear un elemento de video y un botón para poder activar el evento.

<video id="video" src="video.mp4" width="720" controls></video>
<button id="button-active">Activar PIP</button>

Ahora vamos a llamar al método requestPictureInPicture() después de hacer click en nuestro botón.

requestPictureInPicture() devuelve una promesa. Cuando la promesa sea resuelta, el navegador creara una nueva ventana solo para ese video.

// Guardamos nuestros elementos HTML.
const video = document.getElementById("video"),
	  buttonActive = document.getElementById("button-active");

// Creamos un nuevo evento y le pasamos una función async que active PIP.
buttonActive.addEventListener("click", async () => {
	buttonActive.disabled = true;
	await video.requestPictureInPicture();
})

¡Cool!, eso es todo, ya tenemos un video con la funcionalidad de PIP. Bueno, casi, tal vez ya te diste cuenta de que podemos acceder a esta característica, pero, también debería de haber una forma de salir.

Salida de Picture-in-Picture

Para este segundo ejemplo necesitamos un nuevo botón que nos permita salir del modo Picture-in-Picture.

<video id="video" src="video.mp4" width="720" controls></video>
<button id="button-active">Activar PIP</button>
<button id="button-disabled">Desactivar PIP</button>

Vamos a llamar al método exitPictureInPicture() al hacer click en el segundo botón.

const video = document.getElementById("video"),
  buttonActive = document.getElementById("button-active"),
  buttonDisabled = document.getElementById("button-disabled");

buttonActive.addEventListener("click", async () => {
  buttonActive.disabled = true;
  buttonDisabled.disabled = false;
  await video.requestPictureInPicture();
});

// Agegamos nuestro nuevo evento:
buttonDisabled.addEventListener("click", async () => {
  buttonDisabled.disabled = true;
  buttonActive.disabled = false;
  await document.exitPictureInPicture();
});

Advertencia

Cómo puedes ver en el código, para salir de PIP, se utiliza document.exitPictureInPicture() y no el elemento video como pasa en requestPictureInPicture(). 👀 Ojo con eso.

Esto funciona bien, pero, tal vez no sea tan práctico tener dos botones, vamos a refactorizar el código haciendo que solo haya un botón.

<video id="video" src="video.mp4" width="720" controls></video>
<button id="button">Activar PIP</button>
const video = document.getElementById("video"),
      button = document.getElementById("button");

button.addEventListener("click", async () => {
  button.disabled = true;
  if(document.pictureInPictureElement === video) {
    await document.exitPictureInPicture();
    button.innerText = "Activar PIP";
  } else {
    await video.requestPictureInPicture();
    button.innerText = "Desactivar PIP";
  }
  button.disabled = false;
});

Nota

document.pictureInPictureElement regresa null si no está activo el PIP, de lo contrario devuelve el elemento video que tiene activo el PIP.

Manejo de errores

No siempre vamos a correr con la suerte de que la promesa se cumpla y todo vaya bien, puede que la promesa sé rechazarse por cualquiera de los siguientes motivos:

  • PIP no es compatible con el sistema.
  • La activación o llamada de PIP no se ejecutó por un evento del usuario.
  • Los metadatos de video aún no se han cargado (videoElement.readyState === 0).
  • El archivo de video es solo de audio.
  • El atributo disablePictureInPicture está activo en el video.
  • El documento no puede usar PIP debido a una política de características.

Para manejar todos estos posibles errores podemos usar try...catch y reaccionar a ese error o que al menos el usuario sepa lo que está pasando.

button.addEventListener("click", async () => {
   button.disabled = true;
   
  try {
    if(document.pictureInPictureElement === video) {
      await document.exitPictureInPicture();
      button.innerText = "Activar PIP";
    } else {
      await video.requestPictureInPicture();
      button.innerText = "Desactivar PIP";
    }
  } catch(error) {
    console.log(`Ocurrión un error: ${error}`)
  } finally { // Habilitamos el botón después del evento.
    button.disabled = false;
  }
});

Escuchando los eventos de PIP

Los eventos enterpictureinpicture y leavepictureinpicture nos permiten saber el comportamiento de la ventana del PIP y así poder adaptar la experiencia de los usuarios. Podríamos agregar controles multimedia, crear un reproductor de videos, incluso actualizar la resolución del video dependiendo de las dimensiones de la ventana.

// Cuando entremos en PIP
video.addEventListener("enterpictureinpicture", function(event) { ... });

// Cuando salgamos de PIP
video.addEventListener("leavepictureinpicture", function(event) { ... });

Cuando monitoreamos estos eventos, tenemos acceso a objeto event (se puede llamar cómo tú gustes) el cual contiene información sobre nuestra ventana (PIP).

video.addEventListener("enterpictureinpicture", function(event) { 
  console.log(event)
});
Resultado de event

Eres libre de ver cada característica del evento.

Soporte

Finalmente, hay que asegurarnos de que el navegador del usuario sea compatible con PIP, permitiendo que actuemos en el caso de que no sea así.

Para verificar el soporte podemos usar pictureInPictureEnabled que devuelve un valor booleano.

if ("pictureInPictureEnabled" in document) {
  // Tenemos compatiblidad 
} else {
  // No hay compatibilidad
}

¡Genial, has aprendido a usar Picture-in-Picture! Ahora ya puedes usar esta increíble API en tu aplicación, logrando un comportamiento más nativo con el sistema operativo y mejorando la experiencia del usuario.

No olvides dejar lo que piensas en los comentarios. ¿Qué te parece esta característica?, ¿Ya la conocías?, ¿Piensas implementarla en algún sitio? Cualquier opinión es bienvenida.

Referencias