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

François Beaufort
François Beaufort

Com o recurso picture-in-picture (PiP), os usuários podem assistir vídeos em uma janela flutuante (sempre por cima de outras janelas) para 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 nossa amostra oficial de picture-in-picture.

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 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, recurso compatível com o Safari, que permitiria que desenvolvedores da Web criassem e controlassem a experiência completa do Picture-in-Picture. Essa é a situação.

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 em 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 janela pequena 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, 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, um clique no 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. 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. 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 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, transmissão ao vivo). Para garantir que um botão de reprodução/pausa esteja sempre visível, defina alguns manipuladores de ação da sessão de mídia para eventos de mídia "Play" e "Pause", 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.
});

Mostrar os controles de janela "Faixa anterior" e "Faixa seguinte" é 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 conferir isso em ação, teste o exemplo oficial da sessão de mídia.

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 receber notificações 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 de forma muito rápida. Recomendo o uso de 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

Exemplos, 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 mais 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 envolvidos no esforço de padronização.