Rolar e aplicar zoom a uma guia capturada

François Beaufort
François Beaufort

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

Muitos apps da Web que usam 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 no Chrome, que permite que seu app da Web role uma guia capturada e leia e grave o nível de zoom de uma guia capturada.

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

Por que usar o controle de superfície capturada?

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 consegue ver o app capturado e os vídeos dos usuários remotos ao mesmo tempo, a menos que use o recurso Picture-in-Picture ou separe janelas lado a lado para a guia de videoconferência e a guia compartilhada. Em uma tela menor, isso pode ser difícil.
  • O usuário é sobrecarregado pela necessidade de alternar entre o app de videoconferência e a superfície capturada.
  • O usuário perde o acesso aos controles expostos pelo app de videoconferência enquanto está ausente dele. Por exemplo, um app de chat incorporado, reações com emojis, notificações sobre usuários que pedem para entrar na chamada, controles de multimídia e layout e outros recursos úteis de videoconferência.
  • O apresentador não pode delegar o controle a participantes remotos. Isso leva ao cenário muito conhecido em que os usuários remotos pedem ao apresentador para 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 exige algumas etapas, como capturar explicitamente uma guia do navegador e receber permissão do usuário antes de rolar e aplicar zoom na guia capturada.

Capturar uma guia do navegador

Comece pedindo ao usuário para escolher uma superfície para compartilhar usando getDisplayMedia() e, no processo, associe um objeto CaptureController à sessão de captura. Em breve, vamos usar esse objeto para controlar a superfície capturada.

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;
}

Solicitação de permissão

A primeira invocação de sendWheel() ou setZoomLevel() em um determinado objeto CaptureController gera 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 sessão de captura 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. Somente as chamadas sendWheel() e setZoomLevel() exigem um gesto do usuário e somente se o comando precisar ser mostrado. Se o usuário clicar em um botão para aumentar ou diminuir o zoom no app da Web, esse gesto é 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 um botão "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 enviar eventos de roda da magnitude escolhida sobre coordenadas de escolha dentro da viewport de uma guia. O evento não pode ser distinguido pelo app capturado da interação direta do usuário.

Supondo que o app de captura use um elemento <video> chamado "previewTile", o código a seguir 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() recebe 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. Observe que 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 app 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. O navegador também vai traduzir entre o tamanho dos frames capturados e o tamanho da guia e vai enviar o evento de rolagem com 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 começado ou já tiver parado, incluindo a interrupção assíncrona enquanto a ação sendWheel() é processada pelo navegador.
  • Se o usuário não concedeu a permissão do app para usar sendWheel().
  • Se o app de captura tentar enviar um evento de rolagem em coordenadas fora de [trackSettings.width, trackSettings.height]. Esses valores podem mudar de forma assíncrona. Por isso, é recomendável detectar e ignorar o erro. 0, 0 normalmente não está fora dos limites, então é seguro usá-lo para solicitar permissão ao usuário.

Zoom

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

  • getSupportedZoomLevels() retorna uma lista de níveis de zoom compatíveis com o navegador, representados como porcentagens do "nível de zoom padrão", que é definido como 100%. Essa lista é monotonicamente crescente 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 no fim da sessão de captura.
  • oncapturedzoomlevelchange permite detectar mudanças no nível de zoom de uma guia capturada, já que os usuários podem mudar o nível de zoom pelo app de captura ou pela interação direta com a guia capturada.

As chamadas para setZoomLevel() são bloqueadas por permissão. As chamadas para os outros métodos de zoom somente leitura são "gratuitas", 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:

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 o envio de eventos de roda é aceito, 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 computador atrás da flag Captured Surface Control e pode ser ativada em chrome://flags/#captured-surface-control.

Esse recurso também está entrando em um teste de origem a partir do Chrome 122 para computador, que permite que os desenvolvedores ativem o recurso para visitantes dos sites para coletar dados de usuários reais. Consulte Começar a usar os testes de origem para mais informações sobre 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 "Captured Surface Control". Para entender as compensações de segurança, consulte a seção Considerações sobre privacidade e segurança do texto explicativo sobre o controle de superfície capturada.

Demonstração

Você pode testar o controle de superfície capturada executando a demonstração no Glitch. Confira o código-fonte.

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

Confira algumas diferenças comportamentais importantes sobre o controle de superfície capturada:

  • 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() retorna uma promessa rejeitada com a mensagem de erro "No permission." se o usuário não conceder a permissão de uso ao app. O tipo de erro é "NotAllowedError" no Chrome 123 e versões mais recentes.
    • oncapturedzoomlevelchange está indisponível. É possível usar o polifill desse recurso com setInterval().

Feedback

A equipe do Chrome e a comunidade de padrões da Web querem saber sobre sua experiência com o controle de superfície capturada.

Conte-nos sobre o design

Há algo sobre a captura de superfície que não funciona como você esperava? Ou há métodos ou propriedades que faltam para implementar sua ideia? Tem uma pergunta ou comentário sobre o modelo de segurança? Envie um problema de especificação no repositório do GitHub ou adicione sua opinião a um problema existente.

Problemas com a implementação?

Você encontrou um bug na implementação do Chrome? Ou a implementação é diferente da especificação? Registre um bug em https://new.crbug.com. Inclua o máximo de detalhes possível, além de instruções para reproduzir o problema. O Glitch é ótimo para compartilhar bugs reproduzíveis.