O recurso picture-in-picture (PiP) permite que os usuários assistam vídeos em uma janela flutuante (sempre por cima de outras janelas) para que possam acompanhar o que estão assistindo enquanto interagem com outros sites ou aplicativos.
Com a API Web Picture-in-Picture, você pode iniciar e controlar o Picture-in-Picture para elementos de vídeo no seu site. Teste esse recurso na nossa amostra oficial do picture-in-picture (link em inglês).
Contexto
Em setembro de 2016, o Safari adicionou suporte a picture-in-picture por meio de uma API WebKit no macOS Sierra. Seis meses depois, o Chrome passou a reproduzir automaticamente vídeos 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, um recurso compatível com o Safari, que permitiria que os desenvolvedores da Web criassem e controlassem a experiência completa do picture-in-picture. Essa é a situação.
Acessar o código
Entrar no 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>
Só solicite Picture-in-Picture em resposta a um gesto do usuário e nunca na
promessa retornada por videoElement.play()
. Isso ocorre porque as promessas ainda não
propagam gestos do usuário. Em vez disso, chame requestPictureInPicture()
em um
gerenciador de cliques em pipButtonElement
, conforme mostrado abaixo. Você é responsável por processar 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! Você pode parar de ler e ir tirar suas merecidas férias. Infelizmente, nem sempre é assim. A promessa pode ser rejeitada por um destes motivos:
- O sistema não oferece suporte ao recurso Picture-in-Picture.
- 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 á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, o clique de um botão). A partir do Chrome 74, isso será aplicável apenas se ainda não houver um elemento no 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. Ele reflete mudanças de estado na janela Picture-in-Picture (como reproduzir, pausar, procurar etc.), e também é possível mudar o estado programaticamente no JavaScript.
Sair do picture-in-picture
Agora, vamos fazer com que o botão alterne a entrada e a saída do picture-in-picture. Primeiro,
temos que verificar se o objeto somente leitura document.pictureInPictureElement
é o elemento de vídeo. Caso contrário, enviaremos uma solicitação para entrar
no modo Picture-in-Picture, conforme mostrado acima. Caso contrário, vamos pedir para sair chamando
document.exitPictureInPicture()
, o que significa que o vídeo vai aparecer novamente
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. Portanto, a implementação do Chrome segue esse padrão. Isso significa que os usuários só podem assistir um vídeo Picture-in-Picture por vez. Os usuários podem sair do Picture-in-Picture mesmo quando você não pediu.
Os novos manipuladores de eventos enterpictureinpicture
e leavepictureinpicture
permitem personalizar a experiência para os usuários. Pode ser qualquer coisa, desde navegar em um catálogo
de vídeos até encontrar um chat de 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 "Tocar/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 de reprodução/pausa é sempre mostrado na janela Picture-in-Picture,
a menos que o vídeo esteja reproduzindo objetos MediaStream (por exemplo, getUserMedia()
,
getDisplayMedia()
, canvas.captureStream()
) ou que o vídeo 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 manipuladores de ação da sessão de mídia para eventos de mídia "Reproduzir" e
"Pausar", conforme 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 de janela "Pista anterior" e "Próxima pista" é semelhante. A configuração de gerenciadores de ação da sessão de mídia para eles vai mostrar essas ações na janela Picture-in-Picture, e você poderá processá-las.
navigator.mediaSession.setActionHandler('previoustrack', function () {
// User clicked "Previous Track" button.
});
navigator.mediaSession.setActionHandler('nexttrack', function () {
// User clicked "Next Track" button.
});
Para ver esse recurso em ação, teste o exemplo oficial de sessão de mídia (link em inglês).
Acessar o tamanho da janela picture-in-picture
Se você quiser ajustar a qualidade do vídeo quando ele entrar e sair do modo picture-in-picture, é necessário saber o tamanho da janela do picture-in-picture e ser notificado se um usuário redimensionar a janela manualmente.
O exemplo abaixo mostra como receber 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 vincular diretamente ao evento de redimensionamento, já que cada pequena mudança feita no tamanho da janela de imagem em tela vai acionar 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 vai disparar os eventos repetidamente muito rapidamente. Recomendo usar técnicas comuns, como throttling e debouncing, para resolver esse problema.
Suporte a recursos
A API Web Picture-in-Picture pode não ter suporte. Portanto, você precisa detectar isso
para oferecer o aprimoramento progressivo. Mesmo com suporte, o recurso 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.');
}
Aplicada a um elemento de botão específico para um vídeo, esta é a forma de gerenciar a visibilidade do botão de 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;
}
Suporte a vídeo MediaStream
Vídeos que reproduzem objetos MediaStream (por exemplo, getUserMedia()
, getDisplayMedia()
,
canvas.captureStream()
) também oferecem suporte ao 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 de exibição ou até mesmo um elemento de tela. O
elemento de vídeo não precisa ser anexado ao DOM para entrar
no modo picture-in-picture, conforme 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 a tela 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 o elemento da 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();
Ao combinar canvas.captureStream()
com a API Media Session, você pode, por
exemplo, criar uma janela de playlist de áudio no Chrome 74. Confira o exemplo oficial
de playlist de áudio.
Amostras, demonstrações e codelabs
Confira nosso exemplo oficial de picture-in-picture para testar a API Web picture-in-picture.
Demonstrações e codelabs serão publicados em seguida.
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.
Confira o que você pode esperar em breve:
- Os desenvolvedores da Web poderão adicionar controles personalizados de Picture-in-Picture.
- Uma nova API Web será fornecida para mostrar objetos
HTMLElement
arbitrários em uma janela flutuante.
Suporte ao navegador
A API Web Picture-in-Picture tem suporte no 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ção da API Web Picture-in-Picture: https://wicg.github.io/picture-in-picture
- Problemas de especificação: https://github.com/WICG/picture-in-picture/issues
- Exemplo: https://googlechrome.github.io/samples/picture-in-picture/
- Polyfill não oficial de picture-in-picture: https://github.com/gbentaieb/pip-polyfill/
Agradecemos a Mounir Lamouri e Jennifer Apacible pelo trabalho com o Picture-in-Picture e pela ajuda com este artigo. Agradecemos muito a todos os envolvidos no esforço de padronização.