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.
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.
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:
- Os desenvolvedores da Web poderão adicionar controles picture-in-picture personalizados.
- Uma nova API Web será fornecida para exibir objetos
HTMLElement
arbitrários em uma janela flutuante.
Suporte ao navegador
A API Picture-in-Picture Web é compatível com Chrome, Edge, Opera e Safari. Consulte o MDN para ver detalhes.
Recursos
- Status do recurso do Chrome: https://www.chromestatus.com/feature/5729206566649856
- Bugs de implementação do Chrome: https://crbug.com/?q=component:Blink>Media>PictureInPicture
- Especificações da API Picture-in-Picture Web: https://wicg.github.io/picture-in-picture (em inglês)
- Problemas de especificação: https://github.com/WICG/picture-in-picture/issues
- Exemplo: https://googlechrome.github.io/samples/picture-in-picture/
- polyfill de picture-in-picture não oficial: https://github.com/gbentaieb/pip-polyfill/ (em inglês)
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.