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

François Beaufort
François Beaufort

O picture-in-picture (PiP) permite que os usuários assistam vídeos em uma janela flutuante (sempre por cima das outras janelas) para que possam acompanhar assistindo enquanto interage com outros sites ou aplicativos.

Com a API Picture-in-Picture Web, você pode iniciar e controlar Picture-in-picture de elementos de vídeo no seu site. Faça um teste no nosso amostra oficial do modo picture-in-picture (link em inglês).

Contexto

Em setembro de 2016, o Safari adicionou o suporte a picture-in-picture por meio de uma API WebKit. no macOS Sierra. Seis meses depois, o Chrome reproduziu automaticamente Vídeo picture-in-picture em dispositivos móveis com o lançamento do Android O usando um API nativa do Android. Seis meses depois, anunciamos nossa intenção de criar e uma API da Web, um recurso compatível com o Safari, que permitiria desenvolvedores a criar e controlar a experiência completa em torno do picture-in-picture. E aqui estamos nós!

Entrar no código

Entrar no modo picture-in-picture

Vamos começar com um elemento de vídeo e uma forma de interação do usuário a ele, como um elemento de botão.

<video id="videoElement" src="https://example.com/file.mp4"></video>
<button id="pipButtonElement"></button>

Solicite o picture-in-picture somente em resposta a um gesto do usuário, e nunca na promise retornada por videoElement.play(). Isso porque as promessas não mas propagam gestos do usuário. Em vez disso, chame requestPictureInPicture() em uma em pipButtonElement, conforme mostrado abaixo. Você é responsável para lidar com 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 se mover e posicionar sobre outras janelas.

Pronto. Muito bem! Você pode parar de ler e ir buscar o merecido férias. Infelizmente, isso nem sempre é assim. A promessa pode ser rejeitada para qualquer dos seguintes motivos:

  • O sistema não oferece suporte ao modo picture-in-picture.
  • O documento não tem permissão para usar o modo picture-in-picture devido a uma restrição política de permissões.
  • Os metadados do vídeo ainda não foram carregados (videoElement.readyState === 0).
  • O arquivo de vídeo contém 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ó será aplicável se não houver um elemento no Já está no modo picture-in-picture.

A seção Suporte a recursos abaixo mostra como ativar/desativar um botão com base no essas restrições.

Vamos adicionar um bloco try...catch para capturar esses possíveis erros e permitir para que os usuários saibam 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 em picture-in-picture ou não: os eventos são disparados e os métodos de chamada funcionam. Ela reflete as mudanças de estado a janela picture-in-picture (como reproduzir, pausar, procurar etc.) e também é possível alterar o estado de maneira programática em JavaScript.

Sair do modo picture-in-picture

Agora, vamos fazer nosso botão alternar entre a entrada e a saída do picture-in-picture. Qa primeiro precisa verificar se o objeto somente leitura document.pictureInPictureElement é o nosso elemento de vídeo. Se não estiver, enviaremos uma solicitação para entrar Use o modo picture-in-picture acima. Caso contrário, vamos pedir para sair ligando 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 picture-in-picture

Os sistemas operacionais geralmente restringem o picture-in-picture a uma janela, então A implementação do Chrome segue esse padrão. Isso significa que os usuários só podem jogar um vídeo picture-in-picture por vez. Você vai esperar que os usuários saiam Picture-in-picture, mesmo que você não tenha pedido.

Os novos manipuladores de eventos enterpictureinpicture e leavepictureinpicture permitem a personalizar a experiência para os usuários. Pode ser qualquer coisa, desde procurar um de vídeos, até exibir um chat com 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 é compatível com os botões de reproduzir/pausar, faixa anterior e próxima faixa no Janela picture-in-picture que você pode controlar usando a API Media Session.

Controles de mídia em uma janela picture-in-picture
Figura 1. Controles de mídia em uma janela picture-in-picture
.

Por padrão, um botão reproduzir/pausar é sempre mostrado no a menos que o vídeo esteja reproduzindo objetos MediaStream (por exemplo, getUserMedia(), getDisplayMedia(), canvas.captureStream()) ou o vídeo tem uma MediaSource. definida como +Infinity (por exemplo, feed ao vivo). Para garantir que um botão reproduzir/pausar estiver sempre visível, defina alguns manipuladores de ação de sessão de mídia para "Reproduzir" e "Pausar" eventos de mídia, conforme mostrado 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.
});

Mostrando "Faixa anterior" e "Próxima faixa" é semelhante. Ambiente Os gerenciadores de ação de sessão de mídia desses arquivos os mostrarão no modo picture-in-picture e você conseguirá lidar com 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).

Acessar o tamanho da janela picture-in-picture

Se você quiser ajustar a qualidade do vídeo quando ele entrar ou sair você precisa saber o tamanho da janela picture-in-picture e notificado se um usuário redimensionar a janela manualmente.

O exemplo abaixo mostra como encontrar a largura e a altura de Janela picture-in-picture quando é 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.
}

Eu sugiro não vincular diretamente ao evento de redimensionamento, pois cada pequena alteração feita ao tamanho da janela picture-in-picture acionará um evento separado que pode causar problemas de desempenho ao executar uma operação de alto custo em cada redimensionamento. Em em outras palavras, a operação de redimensionamento acionará os eventos repetidas vezes rapidamente. Recomendo o uso de técnicas comuns, como limitação e de devolvendo para resolver esse problema.

Suporte a recursos

A API Picture-in-Picture Web pode não ser suportada, então você precisa detectar isso para fornecer aprimoramento progressivo. Mesmo quando há suporte, pode ser desativada pelo usuário ou desativada por uma política de permissões. Felizmente, você pode 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.');
}

Aplicado a um elemento de botão específico de um vídeo, é assim que você poderá querer gerenciar a visibilidade do botão 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ídeos do MediaStream

Vídeo que reproduz objetos 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 é possível mostrar uma janela picture-in-picture que contém a webcam do usuário stream de vídeo, stream de vídeo de exibição ou mesmo um elemento de tela. Observe que o elemento de vídeo não precisa ser anexado ao DOM para inserir 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 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 elemento de 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();

Combine o canvas.captureStream() com a API Media Session para criar uma janela de playlist de áudio no Chrome 74. Confira o Amostra 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 nossa amostra oficial de picture-in-picture API Web.

Demonstrações e codelabs virão a seguir.

O que vem em seguida?

Primeiro, confira a página de status da implementação para saber quais partes Atualmente, a API é implementada no Chrome e em outros navegadores.

Saiba o que esperar em breve:

Suporte ao navegador

A API Picture-in-Picture Web é compatível com o Chrome, Edge, Opera e Safari. Consulte o MDN para ver detalhes.

Recursos

Agradecemos a Mounir Lamouri e Jennifer Apacible por seu trabalho no Picture-in-picture e ajuda com este artigo. Muito obrigada a todos envolvidos no esforço de padronização.