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 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.

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 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.

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 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:

Suporte ao navegador

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

Recursos

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.