Rolar e aplicar zoom a uma guia capturada

François Beaufort
François Beaufort

O compartilhamento de guias, janelas e telas já é possível na plataforma da Web com a API Screen Capture. Quando um app da Web chamar getDisplayMedia(), o Chrome solicitará que o usuário compartilhe uma guia, janela ou tela com o app como um vídeo MediaStreamTrack.

Muitos apps da Web que usam o getDisplayMedia() mostram ao usuário uma prévia em vídeo da superfície capturada. Por exemplo, os apps de videoconferência geralmente transmitem esse vídeo para usuários remotos e o renderizam para uma HTMLVideoElement local. Assim, o usuário local tem sempre uma prévia do que está compartilhando.

Esta documentação apresenta a nova API Captured Surface Control (link em inglês) no Chrome, que permite que o app da Web role uma guia capturada, além de ler e gravar o nível de zoom de uma guia capturada.

Um usuário rola e aplica zoom a uma guia capturada (demonstração).

Por que usar o Captured Surface Control?

Todos os apps de videoconferência têm a mesma desvantagem: se o usuário quiser interagir com uma guia ou janela capturada, precisará alternar para essa plataforma, removendo-o do app de videoconferência. Isso apresenta alguns desafios:

  • O usuário não poderá ver o app capturado e os vídeos de usuários remotos ao mesmo tempo, a menos que use o modo Picture-in-picture ou janelas lado a lado separadas para a guia de videoconferência e a guia compartilhada. Em uma tela menor, isso pode ser difícil.
  • O usuário fica sobrecarregado pela necessidade de alternar entre o app de videoconferência e a plataforma capturada.
  • o usuário perder o acesso aos controles expostos pelo app de videoconferência enquanto estiver fora dele; por exemplo, um app de chat incorporado, reações com emojis, notificações sobre pedidos de participação de usuários, controles multimídia e de layout e outros recursos úteis de videoconferência.
  • O apresentador não pode delegar o controle aos participantes remotos. Isso nos leva a uma situação muito familiar, em que usuários remotos pedem para o apresentador mudar o slide, rolar um pouco para cima e para baixo ou ajustar o nível de zoom.

A API Captured Surface Control resolve esses problemas.

Como usar o Captured Surface Control?

O uso do Controle de superfície capturada requer algumas etapas, como capturar explicitamente uma guia do navegador e receber permissão do usuário antes de rolar e aplicar zoom à guia capturada.

Capturar uma guia do navegador

Comece solicitando que o usuário escolha uma plataforma para compartilhar usando getDisplayMedia() e, no processo, associe um objeto CaptureController à sessão de captura. Vamos usar esse objeto para controlar a superfície capturada em breve.

const controller = new CaptureController();
const stream = await navigator.mediaDevices.getDisplayMedia({ controller });

Em seguida, produza uma visualização local da superfície capturada na forma de um elemento <video>:

const previewTile = document.querySelector('video');
previewTile.srcObject = stream;

Se o usuário optar por compartilhar uma janela ou tela, isso está fora do escopo no momento. No entanto, se ele optar por compartilhar uma guia, poderemos prosseguir.

const [track] = stream.getVideoTracks();

if (track.getSettings().displaySurface !== 'browser') {
  // Bail out early if the user didn't pick a tab.
  return;
}

Comando de permissão

A primeira invocação de sendWheel() ou setZoomLevel() em um determinado objeto CaptureController produz uma solicitação de permissão. Se o usuário conceder permissão, outras invocações desses métodos no objeto CaptureController serão permitidas. Se o usuário negar a permissão, a promessa retornada vai ser rejeitada.

Os objetos CaptureController são associados exclusivamente a uma capture-session específica, não podem ser associados a outra sessão de captura e não sobrevivem à navegação da página em que são definidos. No entanto, as sessões de captura sobrevivem à navegação da página capturada.

É necessário fazer um gesto para mostrar uma solicitação de permissão ao usuário. Apenas as chamadas sendWheel() e setZoomLevel() exigem um gesto do usuário e somente se a solicitação precisar ser mostrada. Se o usuário clicar em um botão para aumentar ou diminuir o zoom no app da Web, esse gesto do usuário será determinado. No entanto, se o app quiser oferecer controle de rolagem primeiro, os desenvolvedores precisam ter em mente que rolar não constitui um gesto do usuário. Uma possibilidade é oferecer primeiro ao usuário uma opção para "começar a rolar" , conforme o exemplo a seguir:

const startScrollingButton = document.querySelector('button');

startScrollingButton.addEventListener('click', async () => {
  try {
    const noOpWheelAction = {};

    await controller.sendWheel(noOpWheelAction);
    // The user approved the permission prompt.
    // You can now scroll and zoom the captured tab as shown later in the article.
  } catch (error) {
    return; // Permission denied. Bail.
  }
});

Rolagem

Usando sendWheel(), um app de captura pode exibir eventos de roda com a magnitude escolhida nas coordenadas escolhidas na janela de visualização de uma guia. O evento é indistinguível do app capturado da interação direta do usuário.

Supondo que o app de captura use um elemento <video> chamado "previewTile", o código abaixo mostra como redirecionar eventos de roda de envio para a guia capturada:

const previewTile = document.querySelector('video');

previewTile.addEventListener('wheel', async (event) => {
  // Translate the offsets into coordinates which sendWheel() can understand.
  // The implementation of this translation is explained further below.
  const [x, y] = translateCoordinates(event.offsetX, event.offsetY);
  const [wheelDeltaX, wheelDeltaY] = [-event.deltaX, -event.deltaY];

  try {
    // Relay the user's action to the captured tab.
    await controller.sendWheel({ x, y, wheelDeltaX, wheelDeltaY });
  } catch (error) {
    // Inspect the error.
    // ...
  }
});

O método sendWheel() usa um dicionário com dois conjuntos de valores:

  • x e y: as coordenadas em que o evento da roda será entregue.
  • wheelDeltaX e wheelDeltaY: as magnitudes dos rolamentos, em pixels, para rolagens horizontais e verticais, respectivamente. Esses valores são invertidos em comparação com o evento original da roda.

Uma possível implementação de translateCoordinates() é:

function translateCoordinates(offsetX, offsetY) {
  const previewDimensions = previewTile.getBoundingClientRect();
  const trackSettings = previewTile.srcObject.getVideoTracks()[0].getSettings();

  const x = trackSettings.width * offsetX / previewDimensions.width;
  const y = trackSettings.height * offsetY / previewDimensions.height;

  return [Math.floor(x), Math.floor(y)];
}

Observe que há três tamanhos diferentes em jogo no código anterior:

  • O tamanho do elemento <video>.
  • O tamanho dos frames capturados (representados aqui como trackSettings.width e trackSettings.height).
  • O tamanho da guia.

O tamanho do elemento <video> está totalmente dentro do domínio do app de captura e é desconhecido para o navegador. O tamanho da guia está totalmente dentro do domínio do navegador e é desconhecido para o aplicativo da web.

O app da Web usa translateCoordinates() para traduzir os deslocamentos relativos ao elemento <video> em coordenadas dentro do espaço de coordenadas da faixa de vídeo. Da mesma forma, o navegador fará a conversão entre o tamanho dos frames capturados e o tamanho da guia e fornecerá o evento de rolagem em um deslocamento correspondente à expectativa do app da Web.

A promessa retornada por sendWheel() pode ser rejeitada nos seguintes casos:

  • Se a sessão de captura ainda não tiver sido iniciada ou já tiver sido interrompida, incluindo a interrupção assíncrona enquanto a ação sendWheel() é processada pelo navegador.
  • Se o usuário não concedeu permissão ao app para usar sendWheel().
  • Se o app de captura tentar entregar um evento de rolagem em coordenadas que estejam fora de [trackSettings.width, trackSettings.height]. Esses valores podem mudar de forma assíncrona, por isso é uma boa ideia capturar o erro e ignorá-lo. Normalmente, o 0, 0 não estaria fora dos limites, então é seguro usá-lo para solicitar a permissão do usuário.

Zoom

A interação com o nível de zoom da guia capturada é feita pelas seguintes plataformas do CaptureController:

  • getSupportedZoomLevels() retorna uma lista de níveis de zoom compatíveis com o navegador, representada como porcentagens do "nível de zoom padrão", definido como 100%. Essa lista está aumentando monotonicamente e contém o valor 100.
  • getZoomLevel() retorna o nível de zoom atual da guia.
  • setZoomLevel() define o nível de zoom da guia como qualquer valor inteiro presente em getSupportedZoomLevels() e retorna uma promessa quando é concluído. O nível de zoom não é redefinido ao final da sessão de captura.
  • O oncapturedzoomlevelchange permite que você ouça as mudanças no nível de zoom de uma guia capturada, já que os usuários podem mudar o nível pelo app de captura ou pela interação direta com a guia capturada.

As chamadas para setZoomLevel() são controladas por permissão. chamadas para os outros métodos de zoom somente leitura são "sem custo financeiro", assim como a escuta de eventos.

O exemplo a seguir mostra como aumentar o nível de zoom de uma guia capturada em uma sessão de captura atual:

const zoomIncreaseButton = document.getElementById('zoomInButton');

zoomIncreaseButton.addEventListener('click', async (event) => {
  const levels = CaptureController.getSupportedZoomLevels();
  const index = levels.indexOf(controller.getZoomLevel());
  const newZoomLevel = levels[Math.min(index + 1, levels.length - 1)];

  try {
    await controller.setZoomLevel(newZoomLevel);
  } catch (error) {
    // Inspect the error.
    // ...
  }
});

O exemplo a seguir mostra como reagir às mudanças de nível de zoom de uma guia capturada:

controller.addEventListener('capturedzoomlevelchange', (event) => {
  const zoomLevel = controller.getZoomLevel();
  document.querySelector('#zoomLevelLabel').textContent = `${zoomLevel}%`;
});

Detecção de recursos

Para verificar se há suporte para o envio de eventos da roda, use:

if (!!window.CaptureController?.prototype.sendWheel) {
  // CaptureController sendWheel() is supported.
}

Para verificar se o controle de zoom é compatível, use:

if (!!window.CaptureController?.prototype.setZoomLevel) {
  // CaptureController setZoomLevel() is supported.
}

Ativar controle de superfície capturada

A API Captured Surface Control está disponível no Chrome para computadores com a flag "Captured Surface Control" e pode ser ativada em chrome://flags/#captured-surface-control.

Esse recurso também está iniciando um teste de origem com o Chrome 122 para computadores. Com ele, os desenvolvedores podem ativar o recurso para que os visitantes dos sites coletem dados de usuários reais. Consulte Introdução aos testes de origem para mais informações sobre eles e como eles funcionam.

Segurança e privacidade

A política de permissão "captured-surface-control" permite gerenciar como o app de captura e os iframes incorporados de terceiros têm acesso ao controle da superfície capturada. Para entender os prós e contras da segurança, consulte a seção Considerações sobre privacidade e segurança na explicação sobre o Captured Surface Control.

Demonstração

Você pode testar o Captured Surface Control executando a demonstração no Glitch. Não se esqueça de conferir o código-fonte.

Mudanças em relação às versões anteriores do Chrome

Confira algumas das principais diferenças comportamentais do controle de superfície capturada que você precisa conhecer:

  • No Chrome 124 e versões anteriores:
    • A permissão, se concedida, tem o escopo definido para a sessão de captura associada a essa CaptureController, não para a origem da captura.
  • No Chrome 122:
    • getZoomLevel() retorna uma promessa com o nível de zoom atual da guia.
    • sendWheel() vai retornar uma promessa rejeitada com a mensagem de erro "No permission." se o usuário não tiver concedido permissão de uso ao app. O tipo de erro é "NotAllowedError" no Chrome 123 e versões mais recentes.
    • oncapturedzoomlevelchange não está disponível. É possível preencher esse recurso com o polyfill usando setInterval().

Feedback

A equipe do Chrome e a comunidade de padrões da Web querem saber sobre suas experiências com o Captured Surface Control.

Conte-nos sobre o design

Alguma coisa na Captura de superfície capturada não funciona como esperado? Ou faltam métodos ou propriedades que você precisa para implementar sua ideia? Tem uma pergunta ou comentário sobre o modelo de segurança? Registre um problema de especificação no repositório do GitHub (link em inglês) ou adicione sua opinião a um problema.

Problemas com a implementação?

Você encontrou um bug na implementação do Chrome? Ou a implementação é diferente das especificações? Registre um bug em https://new.crbug.com. Certifique-se de incluir o máximo de detalhes possível, bem como instruções para reproduzi-los. O Glitch (link em inglês) funciona muito bem para compartilhar bugs reproduzíveis.