Assistir ao vídeo usando o picture-in-picture

François Beaufort
François Beaufort

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.

Controles de reprodução de mídia em uma janela picture-in-picture
Figura 1. Controles de reprodução de mídia em uma janela picture-in-picture

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.

Playlist de áudio em uma janela picture-in-picture
Figura 2. Playlist de áudio em uma janela picture-in-picture

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:

Suporte ao navegador

A API Web Picture-in-Picture tem suporte no Chrome, Edge, Opera e Safari. Consulte o MDN para ver detalhes.

Recursos

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.