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.
<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.
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:
- Picture-in-Picture não é suportada pelo sistema.
- O documento não pode usar o Picture-in-Picture devido a uma política restritiva de recursos.
- Os metadados do vídeo ainda não foram carregados (
videoElement.readyState === 0
). - O arquivo de vídeo é apenas áudio.
- O novo atributo
disablePictureInPicture
está presente no elementovideo
. - A chamada não foi feita em um manipulador de eventos de gestos do usuário (por exemplo, um clique no botão). A partir do Chrome 74, isso é aplicável apenas se ainda não houver um elemento no Picture-in-Picture.
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.
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.
...
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.
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.
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.
// 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.
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.
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.
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.
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
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');
// 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.
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:
- Picture-in-Picture suportada no Android O.
- Desenvolvedores poderão personalizar os controles de PiP.
Recursos
- Chrome Feature Status: https://www.chromestatus.com/feature/5729206566649856
- Chrome Implementation Bugs: https://crbug.com/?q=component:Blink>Media>PictureInPicture
- Picture-in-Picture Web API Spec: https://wicg.github.io/picture-in-picture
- Spec Issues: https://github.com/WICG/picture-in-picture/issues
- Sample: https://googlechrome.github.io/samples/picture-in-picture/
- Unofficial Picture-in-Picture polyfill: https://github.com/gbentaieb/pip-polyfill/
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.