Projete vídeos usando a API PiP

Projetar vídeos com uso da API Picture-in-Picture (PiP) permite que os usuários assistam a vídeos em uma janela flutuante (sempre em cima de outras janelas) enquanto interagem com outros sites ou aplicativos.

Com a nova API Picture-in-Picture, você pode inicializar e controlar os elementos video em seu site. Experimente no nosso exemplo oficial Picture-in-Picture.

Nota do tradutor: Criei uma página de exemplo baseado nos códigos da página oficial conforme citada.
Consulte o o exemplo criado pelo Maujor (abre em nova janela)

Histórico

Em setembro de 2016, o Safari adicionou suporte à Picture-in-Picture valendo-se de uma API WebKit no macOS Sierra. Seis meses depois, com o lançamento do Android , o Chrome implementou o Picture-in-Picture no celular usando uma API Nativa do Android. Seis meses depois, anunciamos nossa intenção de criar e padronizar uma API Web tendo recurso compatível com o Safari, que permitiria que os desenvolvedores da Web criassem e controlassem a experiência completa do Picture-in-Picture. E aqui estamos!

"Bora codar!"

Entrar em PiP

Vamos começar com um elemento video e um elemento button com o qual o usuário irá interagir com o vídeo.

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

A requisição para a API Picture-in-Picture deverá ser feita em reposta a uma ação do usuário e nunca na promessa retornada por videoElement.play(). Isso porque as promessas ainda não propagam gestos do usuário. Em vez disso, chame requestPictureInPicture() em um manipulador de cliques em pipButtonElement como mostrado a seguir. É de sua responsabilidade lidar com o que acontece se um usuário clicar duas vezes.

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

  await videoElement.requestPictureInPicture(); 

  pipButtonElement.disabled = false;

});

Quando a promessa é resolvida, o Chrome redimensiona o vídeo para uma pequena janela a qual o usuário pode arrastar e posicionar na tela e sobre outras janelas. Você terminou. Bom trabalho! Você pode parar de ler e tirar suas merecidas férias. Infelizmente, esse nem sempre é o caso. A promessa pode ser rejeitada por qualquer um dos seguintes motivos:

Na seção Suporte à API adiante nessa matéria mostramos como ativar/desativar um botão com base nessas restrições.

Vamos adicionar um bloco try...catch para capturar esseserros em potencial e informar ao usuário o que está acontecendo.

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

  try { 
    await videoElement.requestPictureInPicture(); 
  } 

  catch(error) { 
    // TODO: Mostrar mensagens de erro ao usuário. 
  } finally { 
    pipButtonElement.disabled = false; 
  } 
})

O elemento video se comporta da mesma forma, seja na Picture-in-Picture ou não: os eventos são disparados e os métodos de chamada funcionam. Ele reflete alterações de estado na janela Picture-in-Picture (como reproduzir, pausar, procurar etc.) e também é possível alterar o estado programaticamente com JavaScript.

Sair de PiP

Agora, vamos fazer o nosso botão alternar entre entrar e sair do Picture-in-Picture. Primeiro, precisamos verificar se o objeto somente leitura document.pictureInPictureElement é o nosso elemento de vídeo. Se não, enviamos uma solicitação para inserir Picture-in-Picture como mostrado anteriormente. Se sim, pedimos para sair chamando document.exitPictureInPicture(), o que significa que o vídeo aparecerá na guia original. Observe que esse método também retorna uma promessa.

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

Nota: É recomendável que o vídeo deixe o Picture-in-Picture automaticamente quando entra em modo de tela cheia.

Ouvir eventos Picture-in-Picture

Os sistemas operacionais geralmente restringem o Picture-in-Picture a uma janela, assim, a implementação no Chrome segue esse padrão. Isso significa que os usuários podem reproduzir apenas um vídeo Picture-in-Picture de cada vez. Você deve esperar que os usuários saiam do Picture-in-Picture, mesmo quando você não solicitou.

Aviso: Ouça os eventos Picture-in-Picture em vez de esperar promessas de atualização dos controles do seu media player. É possível que o vídeo entre e saia do Picture-in-Picture a qualquer momento (por exemplo, o usuário clica em algum menu de contexto do navegador ou o Picture-in-Picture é acionado automaticamente).

Os novos manipuladores de eventos leavepictureinpicture e leavepictureinpicture permitem adaptar a experiência para os usuários. Pode ser qualquer coisa, desde navegar em um catálogo de vídeos até surgir em um bate-papo de transmissão ao vivo.

JavaScript
videoElement.addEventListener('enterpictureinpicture', function(event) {
  // Vídeo entra em Picture-in-Picture. 
});

videoElement.addEventListener('leavepictureinpicture', function(event) {
  // Vídeo sai de Picture-in-Picture. 
  // O usuário reproduziu um vídeo Picture-in-Picture em outra página. 
});

Personalização da janela PiP

O Chrome 74 oferece suporte aos botões reproduzir/pausar, faixa anterior e faixa seguinte na janela e você pode controlar esses botões usando a API Media Session.

Controles de reprodução de mídia em uma janela
Figure 1. Controles de reprodução de mídia em uma janela

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 o vídeo tenha uma duração MediaSource definida para +Infinity (por exemplo, feed ao vivo). Para garantir que um botão reproduzir/pausar esteja sempre visível, configure alguns manipuladores de ação da sessão de mídia para os eventos de mídia "Reproduzir" e "Pausar", conforme mostrado a seguir.

JavaScript
// Show a play/pause button in the Picture-in-Picture window
navigator.mediaSession.setActionHandler('play', function() { 
  // Usuário clicou o botão "Reproduzir". 
});

navigator.mediaSession.setActionHandler('pause', function() { 
  // Usuário clicou o botão "Pausa". 
});

Mostrar os controles "Faixa anterior" e "Próxima faixa" é semelhante. A configuração dos manipuladores de ação da sessão de mídia para eles será exibida na janela Picture-in-Picture e você poderá lidar com essas ações.

JavaScript
navigator.mediaSession.setActionHandler('previoustrack', function() { 
  // Usuário clicou o botão "Faixa anterior". 
});

navigator.mediaSession.setActionHandler('nexttrack', function() { 
  // Usuário clicou o botão "Próxima faixa". 
});

Para ver isso em ação, visite uma página contendo um exemplo oficial de Media Session.

Obter o tamanho da janela

Se você deseja ajustar a qualidade do vídeo quando o vídeo entra e sai do Picture-in-Picture, é necessário conhecer o tamanho da janela do Picture-in-Picture e ser notificado se um usuário redimensionar a janela manualmente. O exemplo abaixo mostra como obter a largura e a altura da janela Picture-in-Picture quando ela é criada ou redimensionada.

JavaScript
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: Alterar a qualidade do vídeo Picture-in-Picture de acordo com o tamanho da janela. 
}

Sugiro não atrelar diretamente ao evento de redimensionamento, pois cada pequena alteração feita no tamanho da janela Picture-in-Picture dispara um evento separado que pode causar problemas de desempenho se você estiver executando uma operação cara a cada redimensionamento. Em outras palavras, a operação de redimensionamento disparará os eventos repetidamente muito rapidamente. Eu recomendo o uso de técnicas comuns, tais como throttling e debouncing para solucionar esse problema.

Suporte à API

A API Picture-in-Picture pode não ser suportada, portanto, você deve detectá-la para fornecer aprimoramento progressivo. Mesmo quando suportada, ela pode ser desativada pelo usuário ou por uma política restritiva para recursos. Felizmente, você pode usar o novo booleano document. pictureInPictureEnabled para determinar isso.

JavaScript
if (!('pictureInPictureEnabled' in document)) {
  console.log('API PiP não disponível.'); 
} else if (!document.pictureInPictureEnabled) { 
  console.log('API PiP desabilitada.'); 
}

Observe a seguir o código para lidar com a visibilidade de um elemento button específico de um vídeo.

JavaScript
if ('pictureInPictureEnabled' in document) { 
  // Configurar o botão de acordo com disponibilidade de PiP.
  setPipButton(); 
  videoElement.addEventListener('loadedmetadata', setPipButton); 
  videoElement.addEventListener('emptied', setPipButton); 
} else { 
  // Esconde o botão se PiP não estiver disponível.
      pipButtonElement.hidden = true; 
} 

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

Suporte de vídeo MediaStream

O vídeo que reproduz objetos MediaStream (por exemplo, getUserMedia(), getDisplayMedia(), canvas.captureStream() ) também suporta Picture-in-Picture no Chrome 71. Isso significa que você pode mostrar uma janela Picture-in-Picture que contém o fluxo de vídeo da webcam do usuário, exibir fluxo de vídeo ou até mesmo um elemento de tela. Observe que o elemento de vídeo não precisa ser anexado ao DOM para inserir Picture-in-Picture, como mostrado a seguir.

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

JavaScript
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

JavaScript
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

JavaScript
const canvas = document.createElement('canvas'); 
// Desenha algo no 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();

Combinando canvas.captureStream() com a API Media Session você pode, por exemplo, criar uma janela da lista de reprodução de áudio no Chrome 74. Confira um exemplo oficial da lista de reprodução de áudio.

Lista de reprodução de audio em uma janela
Figure 2. Lista de reprodução de áudio em uma janela

Picture-in-Picture amostras, demos e codelabs

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

Em breve demonstrações e codelabs.

O que vem por aí

Primeiro, confira a página de status da implementação o para saber quais partes da API estão atualmente implementadas no Chrome e em outros navegadores. Aqui está o que você pode esperar ver em um futuro próximo:

Recursos

Muito obrigado a Mounir Lamouri e Jennifer Apacible por seu trabalho na Picture-in-Picture e por ajudar neste artigo. E um enorme agradecimento a todos os envolvidos no esforço de padronização.

Conheça os livros do Maujor®

Ir para a página de entrada nos sites dos livros.