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 usando uma API WebKit no macOS Sierra. Seis meses depois, o Chrome reproduziu 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. E aqui estamos!
Acessar o 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>
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 no pipButtonElement
, conforme mostrado abaixo. É sua responsabilidade
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 a uma pequena janela que o usuário pode mover e posicionar sobre outras janelas.
Pronto. Muito bem! Você pode parar de ler e tirar suas merecidas férias. Infelizmente, isso 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 só é aplicável se 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, esteja ele no modo picture-in-picture ou não: os eventos são acionados 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. Observe que esse método também retorna uma promessa.
...
try {
if (videoElement !== document.pictureInPictureElement) {
await videoElement.requestPictureInPicture();
} else {
await document.exitPictureInPicture();
}
}
...
Ouvir eventos de 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 assistir um vídeo picture-in-picture por vez. Os usuários devem sair do modo picture-in-picture mesmo que você não tenha solicitado.
Os novos manipuladores de eventos enterpictureinpicture
e leavepictureinpicture
permitem
que você personalize a experiência para os usuários. Pode ser qualquer coisa, desde a navegação em um
catálogo de vídeos até a exibição de 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 de tocar/pausar, faixa anterior e faixa seguinte na janela Picture-in-Picture, que podem ser controlados 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 da janela "Faixa anterior" e "Próxima faixa" é semelhante. A definição de gerenciadores de ações da Sessão de mídia para eles vai mostrá-los na janela picture-in-picture e você poderá processar 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 esse recurso em ação, teste o exemplo oficial de sessão de mídia (link em inglês).
Conferir 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.
}
Sugerimos não 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 executando uma operação cara em cada redimensionamento. Em outras palavras, a operação de redimensionamento acionará os eventos repetidamente e de maneira bastante rápida. 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 do MediaStream
Vídeos que reproduzem objetos do MediaStream (por exemplo, getUserMedia()
, getDisplayMedia()
,
canvas.captureStream()
) também são compatíveis com o 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 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 virão a seguir.
O que vem em seguida?
Primeiro, consulte 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 da 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 mais 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 Picture-in-Picture não oficial: https://github.com/gbentaieb/pip-polyfill/ (em inglês)
Agradecemos a Mounir Lamouri e Jennifer Apacible pelo trabalho com o Picture-in-Picture e pela ajuda com este artigo. Agradecemos a todos que participaram do esforço de padronização.