Assistir ao vídeo usando o picture-in-picture

François Beaufort
François Beaufort

O picture-in-picture (PiP) permite que os usuários assistam vídeos em uma janela flutuante (sempre em cima de outras janelas) para acompanhar o que estão assistindo enquanto interagem com outros sites ou apps.

Com a API Picture-in-Picture Web, você pode iniciar e controlar elementos de vídeo no seu site. Faça um teste no nosso exemplo oficial do picture-in-picture (link em inglês).

Contexto

Em setembro de 2016, o Safari adicionou suporte ao modo picture-in-picture usando uma API WebKit no macOS Sierra. Seis meses depois, o Chrome reproduziu automaticamente o vídeo picture-in-picture em dispositivos móveis com o lançamento do Android O usando uma API nativa do Android. Seis meses depois, anunciamos nossa intenção de criar e padronizar uma API da Web, recurso compatível com o Safari, que permitiria aos desenvolvedores da Web criar e controlar a experiência completa do modo picture-in-picture. E aqui estamos!

Entrar no código

Entrar no modo picture-in-picture

Vamos começar com um elemento de vídeo e uma maneira de o usuário interagir com ele, como um elemento de botão.

<video id="videoElement" src="https://example.com/file.mp4"></video>
<button id="pipButtonElement"></button>

Solicite o picture-in-picture apenas em resposta a um gesto do usuário, e nunca na promessa retornada por videoElement.play(). Isso ocorre porque as promessas não ainda propagam gestos do usuário. Em vez disso, chame requestPictureInPicture() em um gerenciador de cliques em pipButtonElement, conforme mostrado abaixo. É sua responsabilidade lidar com o que acontece se um usuário clicar duas vezes.

pipButtonElement.addEventListener('click', async function () {
  pipButtonElement.disabled = true;

  await videoElement.requestPictureInPicture();

  pipButtonElement.disabled = false;
});

Quando a promessa é resolvida, o Chrome reduz o vídeo em uma pequena janela que o usuário pode mover e posicionar sobre outras janelas.

Pronto. Muito bem! Pare a leitura e aproveite suas merecidas férias. Infelizmente, esse nem sempre é o caso. A promessa pode ser rejeitada por qualquer um dos seguintes motivos:

  • O recurso picture-in-picture não é compatível com o sistema.
  • O documento não tem permissão para usar o picture-in-picture devido a uma política de permissões restritiva.
  • Os metadados do vídeo ainda não foram carregados (videoElement.readyState === 0).
  • O arquivo de vídeo é somente de áudio.
  • O novo atributo disablePictureInPicture está presente no elemento de vídeo.
  • A chamada não foi feita em um manipulador de eventos de gesto do usuário (por exemplo, um clique de botão). A partir do Chrome 74, isso é aplicável somente se ainda não houver um elemento no modo picture-in-picture.

A seção Suporte a recursos abaixo mostra como ativar/desativar um botão com base nessas restrições.

Vamos adicionar um bloco try...catch para capturar esses possíveis erros e informar ao usuário o que está acontecendo.

pipButtonElement.addEventListener('click', async function () {
  pipButtonElement.disabled = true;

  try {
    await videoElement.requestPictureInPicture();
  } catch (error) {
    // TODO: Show error message to user.
  } finally {
    pipButtonElement.disabled = false;
  }
});

O elemento de vídeo se comporta da mesma forma, seja no modo picture-in-picture ou não: os eventos são disparados e os métodos de chamada funcionam. Ela reflete as mudanças de estado na janela picture-in-picture (como reproduzir, pausar, procurar etc.) e também é possível mudar o estado de forma programática em JavaScript.

Sair do modo picture-in-picture

Agora, vamos fazer com que o botão alterne para entrar e sair do modo picture-in-picture. Primeiro, precisamos verificar se o objeto somente leitura document.pictureInPictureElement é o elemento de vídeo. Se não estiver, enviaremos uma solicitação para entrar no modo picture-in-picture acima. Caso contrário, pedimos para sair chamando document.exitPictureInPicture(), o que significa que o vídeo vai aparecer de volta na guia original. Esse método também retorna uma promessa.

    ...
    try {
      if (videoElement !== document.pictureInPictureElement) {
        await videoElement.requestPictureInPicture();
      } else {
        await document.exitPictureInPicture();
      }
    }
    ...

Ouvir eventos picture-in-picture

Os sistemas operacionais geralmente restringem o picture-in-picture a uma janela, por isso, a implementação do Chrome segue esse padrão. Isso significa que os usuários só podem reproduzir um vídeo picture-in-picture por vez. É esperado que os usuários saiam do modo picture-in-picture mesmo que você não tenha solicitado.

Os novos manipuladores de eventos enterpictureinpicture e leavepictureinpicture permitem personalizar a experiência dos usuários. Pode ser qualquer coisa, desde navegar em um catálogo de vídeos até mostrar um chat com transmissão ao vivo.

videoElement.addEventListener('enterpictureinpicture', function (event) {
  // Video entered Picture-in-Picture.
});

videoElement.addEventListener('leavepictureinpicture', function (event) {
  // Video left Picture-in-Picture.
  // User may have played a Picture-in-Picture video from a different page.
});

Personalizar a janela picture-in-picture

O Chrome 74 oferece suporte aos botões "Reproduzir/pausar", "Faixa anterior" e "Próxima faixa" na janela picture-in-picture que você pode controlar usando a API Media Session.

Controles de reprodução de mídia em uma janela picture-in-picture
Figura 1. Controles de reprodução de mídia em uma janela picture-in-picture

Por padrão, um botão "Reproduzir/pausar" é sempre mostrado na janela picture-in-picture, a menos que o vídeo esteja reproduzindo objetos MediaStream (por exemplo, getUserMedia(), getDisplayMedia(), canvas.captureStream()) ou tenha uma duração do MediaSource definida como +Infinity (por exemplo, feed ao vivo). Para garantir que um botão de reproduzir/pausar esteja sempre visível, defina alguns gerenciadores de ações de sessão de mídia para os eventos de mídia "Reproduzir" e "Pausar", conforme mostrado abaixo.

// Show a play/pause button in the Picture-in-Picture window
navigator.mediaSession.setActionHandler('play', function () {
  // User clicked "Play" button.
});
navigator.mediaSession.setActionHandler('pause', function () {
  // User clicked "Pause" button.
});

A exibição dos controles da janela "Faixa anterior" e "Próxima faixa" é semelhante. Se você definir gerenciadores de ação de sessão de mídia, eles serão exibidos na janela picture-in-picture, e você poderá lidar com essas ações.

navigator.mediaSession.setActionHandler('previoustrack', function () {
  // User clicked "Previous Track" button.
});

navigator.mediaSession.setActionHandler('nexttrack', function () {
  // User clicked "Next Track" button.
});

Para ver isso em ação, teste o exemplo oficial de sessão de mídia.

Consultar o tamanho da janela picture-in-picture

Se você quiser ajustar a qualidade quando o vídeo entra e sai do modo picture-in-picture, é necessário saber o tamanho da janela no modo picture-in-picture e receber uma notificação se um usuário redimensionar a janela manualmente.

O exemplo abaixo mostra como conferir a largura e a altura da janela picture-in-picture quando ela é criada ou redimensionada.

let pipWindow;

videoElement.addEventListener('enterpictureinpicture', function (event) {
  pipWindow = event.pictureInPictureWindow;
  console.log(`> Window size is ${pipWindow.width}x${pipWindow.height}`);
  pipWindow.addEventListener('resize', onPipWindowResize);
});

videoElement.addEventListener('leavepictureinpicture', function (event) {
  pipWindow.removeEventListener('resize', onPipWindowResize);
});

function onPipWindowResize(event) {
  console.log(
    `> Window size changed to ${pipWindow.width}x${pipWindow.height}`
  );
  // TODO: Change video quality based on Picture-in-Picture window size.
}

Sugiro não se vincular diretamente ao evento de redimensionamento, já que cada pequena mudança feita no tamanho da janela picture-in-picture vai disparar um evento separado que pode causar problemas de desempenho se você estiver fazendo uma operação cara em cada redimensionamento. Em outras palavras, a operação de redimensionamento acionará os eventos repetidamente, de forma muito rápida. É recomendável usar técnicas comuns, como limitação e deturpação, para resolver esse problema.

Suporte a recursos

A API Picture-in-Picture Web pode não ter suporte, então é necessário detectar isso para oferecer melhorias progressivas. Mesmo quando há suporte, ele pode ser desativado pelo usuário ou desativado por uma política de permissões. Felizmente, é possível usar o novo booleano document.pictureInPictureEnabled para determinar isso.

if (!('pictureInPictureEnabled' in document)) {
  console.log('The Picture-in-Picture Web API is not available.');
} else if (!document.pictureInPictureEnabled) {
  console.log('The Picture-in-Picture Web API is disabled.');
}

Aplicado a um elemento de botão específico de um vídeo, é assim que você pode processar a visibilidade do botão Picture-in-Picture.

if ('pictureInPictureEnabled' in document) {
  // Set button ability depending on whether Picture-in-Picture can be used.
  setPipButton();
  videoElement.addEventListener('loadedmetadata', setPipButton);
  videoElement.addEventListener('emptied', setPipButton);
} else {
  // Hide button if Picture-in-Picture is not supported.
  pipButtonElement.hidden = true;
}

function setPipButton() {
  pipButtonElement.disabled =
    videoElement.readyState === 0 ||
    !document.pictureInPictureEnabled ||
    videoElement.disablePictureInPicture;
}

Compatibilidade com vídeos do MediaStream

Objetos do MediaStream em reprodução de vídeo (por exemplo, getUserMedia(), getDisplayMedia(), canvas.captureStream()) também oferecem suporte ao modo picture-in-picture no Chrome 71. Isso significa que você pode mostrar uma janela picture-in-picture que contém o stream de vídeo da webcam do usuário, o stream de vídeo exibido ou até mesmo um elemento da tela. Não é preciso anexar o elemento de vídeo ao DOM para entrar no picture-in-picture, como mostrado abaixo.

Mostrar a webcam do usuário na janela picture-in-picture

const video = document.createElement('video');
video.muted = true;
video.srcObject = await navigator.mediaDevices.getUserMedia({video: true});
video.play();

// Later on, video.requestPictureInPicture();

Mostrar exibição na janela picture-in-picture

const video = document.createElement('video');
video.muted = true;
video.srcObject = await navigator.mediaDevices.getDisplayMedia({video: true});
video.play();

// Later on, video.requestPictureInPicture();

Mostrar elemento de tela na janela picture-in-picture

const canvas = document.createElement('canvas');
// Draw something to canvas.
canvas.getContext('2d').fillRect(0, 0, canvas.width, canvas.height);

const video = document.createElement('video');
video.muted = true;
video.srcObject = canvas.captureStream();
video.play();

// Later on, video.requestPictureInPicture();

Combinar canvas.captureStream() com a API Media Session permite, por exemplo, criar uma janela de playlist de áudio no Chrome 74. Confira a amostra de playlist de áudio oficial.

Playlist de áudio em uma janela picture-in-picture
Figura 2. Playlist de áudio em uma janela picture-in-picture

Amostras, demonstrações e codelabs

Confira nosso exemplo oficial da API Picture-in-Picture para testar a API Picture-in-Picture.

Em seguida, você terá demonstrações e codelabs.

O que vem em seguida?

Primeiro, confira a página de status da implementação para saber quais partes da API estão implementadas no Chrome e em outros navegadores.

Veja o que esperar em breve:

Suporte ao navegador

A API Picture-in-Picture Web é compatível com Chrome, Edge, Opera e Safari. Consulte o MDN para ver detalhes.

Recursos

Agradecemos a Mounir Lamouri e Jennifer Apacible pelo trabalho de picture-in-picture e pela ajuda com este artigo. Agradecemos imensamente a todos envolvidos no esforço de padronização.