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, apps de videoconferência geralmente transmitem esse vídeo para usuários remotos e também o renderizam para um HTMLVideoElement
local, para que o usuário local tenha uma prévia constante do que está sendo compartilhado.
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.
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, ele precisa mudar para essa plataforma, saindo 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 controle de superfície capturada?
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 escolher compartilhar uma janela ou uma tela, isso não será possível por enquanto. Mas se ele escolher compartilhar uma guia, poderemos continuar.
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 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.
Um gesto do usuário é necessário para mostrar uma solicitação de permissão. 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 de zoom in ou zoom out no app da Web, esse gesto será considerado como dado. No entanto, se o app quiser oferecer o controle de rolagem primeiro, os desenvolvedores precisam lembrar que a rolagem 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 abaixo:
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 encaminhar eventos de roda 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
ey
: as coordenadas em que o evento da roda será enviado.wheelDeltaX
ewheelDeltaY
: 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
etrackSettings.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 em relação ao elemento <video>
em coordenadas dentro do espaço de coordenadas da própria 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 para qualquer valor inteiro presente emgetSupportedZoomLevels()
e retorna uma promessa quando é bem-sucedido. O nível de zoom não é redefinido no final 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 "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:
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 o 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ões do "captured-surface-control"
permite gerenciar como o app de captura e os iframes incorporados de terceiros têm acesso ao controle de superfície capturada. 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 anteriores:
- A permissão, se concedida, é limitada à sessão de captura associada a esse
CaptureController
, e não à origem da captura.
- A permissão, se concedida, é limitada à sessão de captura associada a esse
- 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 comsetInterval()
.
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.
Descreva 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 alguma dúvida 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.