1. Introdução
Neste artigo, adivinhamos que falaremos sobre a API de captura de tela. Essa API nasceu em 2014 e é difícil chamá-la de nova, mas o suporte ao navegador ainda é bastante fraco. No entanto, pode ser usado para projetos pessoais ou onde esse apoio não é tão importante.
Alguns links para você começar:
Caso o link com a demonstração caia (ou se você estiver com preguiça de ir para lá) - é assim que a demonstração final será exibida:

Vamos começar.
Motivação
Recentemente, tive a idéia de um aplicativo da web que usa códigos QR em seu trabalho. E, embora geralmente sejam convenientes para transmitir, por exemplo, links longos no mundo real onde você pode apontar o telefone para eles, na área de trabalho é um pouco mais complicado. Se o código QR estiver na tela do mesmo dispositivo em que você precisa lê-lo, você precisará mexer nos serviços de reconhecimento ou reconhecimento do telefone e transferir os dados de volta para o PC. Inconvenientemente.
Alguns produtos, como o 1Password , incluem uma solução interessante para essa situação. Se você precisar configurar uma conta a partir de um código QR, eles abrirão uma janela translúcida que você pode arrastar sobre a imagem com o código e ela será reconhecida automaticamente. Aqui está o que parece:

Seria ideal se pudéssemos implementar algo semelhante para a nossa aplicação. Mas provavelmente não funcionará no navegador ...
Bem, quase. Aqui a API do Screen Capture com seu único método getDisplayMedia
. getDisplayMedia
é como getUserMedia
, apenas para a tela do dispositivo, em vez de sua câmera. Infelizmente, o suporte ao navegador, como mencionado acima, está longe de ser tão difundido quanto o acesso à câmera. Segundo a MDN, você pode usá-lo no Firefox, Chrome, Edge (embora esteja no lugar errado - imediatamente no navigator
, e não no navigator.mediaDevices
) + Edge Mobile e ... Opera para Android.
Uma seleção bastante curiosa de navegadores de celular ao lado dos esperados Big Two.
A API em si é extremamente simples. Funciona da mesma forma que getUserMedia
, mas permite capturar um fluxo de vídeo de uma das superfícies de exibição definidas:
- do monitor (tela inteira),
- de uma janela ou de todas as janelas de um determinado aplicativo,
- de um navegador , ou melhor, de um documento específico. No Chrome, este documento é uma guia separada, mas no FF não existe essa opção.
API do navegador, que permite que você olhe além do navegador ... Parece familiar e geralmente é um pouco de alguns problemas, mas nesse caso pode ser bastante conveniente. Você pode capturar uma imagem de outras janelas e, por exemplo, reconhecer e traduzir texto em tempo real, como a Câmera do Google Tradutor. Bem, e provavelmente há muitos usos mais interessantes.
Nós coletamos
Então, descobrimos os recursos que a API nos fornece. O que vem a seguir?
E então precisamos ultrapassar esse fluxo de vídeo em imagens nas quais possamos trabalhar. Para fazer isso, usamos os elementos <video>
, <canvas>
e um pouco mais de JS.
Um close do processo é mais ou menos assim:
- Fluxo direto para
<video>
; - Com uma certa frequência, desenhe o conteúdo do
<video>
em <canvas>
; - Colete um objeto ImageData em
<canvas>
usando o método de contexto 2D getImageData
.
Todo esse procedimento pode parecer um pouco estranho devido a um pipeline tão longo, mas esse método é bastante popular e foi usado para capturar dados de webcams no getUserMedia
.
Omitindo tudo que é irrelevante, para iniciar o fluxo e extrair o quadro dele, precisamos do código a seguir:
async function run() { const video = document.createElement('video'); const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); const displayMediaOptions = { video: { cursor: "never" }, audio: false } video.srcObject = await navigator.mediaDevices.getDisplayMedia(displayMediaOptions); const videoTrack = video.srcObject.getVideoTracks()[0]; const { height, width } = videoTrack.getSettings(); context.drawImage(video, 0, 0, width, height); return context.getImageData(0, 0, width, height); } await run();
Como mencionado acima: primeiro, criamos os elementos <video>
e <canvas>
e solicitamos à tela um contexto 2D ( CanvasRenderingContext2D
).
Em seguida, definimos restrições / condições de fluxo. Ao contrário dos fluxos da câmera, existem alguns deles. Dizemos que não queremos ver o cursor e que não precisamos de áudio. Embora no momento da redação deste artigo, a captura de áudio ainda não seja suportada por ninguém.
Depois disso, conectamos o fluxo recebido do tipo MediaStream
ao elemento <video>
. Observe que getDisplayMedia
retorna uma promessa.
Finalmente, a partir dos dados recebidos no fluxo, lembramos a resolução do vídeo para desenhá-lo corretamente na tela, desenhar o quadro e retirar o objeto ImageData da ImageData
.
Para uso total, você provavelmente desejará processar quadros em loop em vez de uma vez. Por exemplo, enquanto você espera quando a imagem desejada aparece no quadro. E aqui algumas palavras precisam ser ditas.
Quando se trata de "manipular algo no DOM em um loop constante", a primeira coisa que vem à mente é provavelmente requestAnimationFrame
. No entanto, no nosso caso, usá-lo não funcionará. O problema é que, quando a guia deixa de estar ativa - os navegadores pausam o processamento do loop rAF. No nosso caso, é nesse momento que queremos processar as imagens.
Nesse sentido, em vez de rAF, usaremos o bom e velho setInterval
. Mas as coisas não são tão fáceis com ele. Em uma guia inativa, o intervalo entre as operações de retorno de chamada é de pelo menos 1 segundo . No entanto, isso é suficiente para nós.
Finalmente, quando chegarmos aos quadros, podemos processá-los como quisermos. Para os fins desta demonstração, usaremos a biblioteca jsQR . É extremamente simples: a entrada aceita ImageData
, a largura e a altura da imagem. Se a imagem recebida tiver um código QR, você receberá um objeto JS com dados reconhecidos.
Vamos complementar nosso exemplo anterior com apenas mais algumas linhas de código:
const imageData = await run(); const code = jsQR(imageData.data, streamWidth, streamHeight);
Feito!
NPM
Eu pensei que o código principal por trás deste exemplo poderia ser empacotado em uma biblioteca npm e economizar algum tempo no uso inicial para uso posterior. A biblioteca é muito simples; nesse estágio, apenas aceita o retorno de chamada para o qual o ImageData
será enviado e um parâmetro adicional é a frequência do envio de dados. Todo o processamento que você precisa para trazer o seu. Vou pensar se faz sentido expandir sua funcionalidade.
A biblioteca é chamada stream-display
: NPM | Github .
Seu uso é reduzido a literalmente três linhas de código e um retorno de chamada:
const callback = imageData => {...}
A demo pode ser vista aqui . Há também uma versão do CodePen para experiências rápidas. Ambos os exemplos usam o pacote NPM acima.
Um pouco sobre o teste
Ao empacotar esse código na biblioteca, tive que pensar em como testá-lo. Eu absolutamente não queria arrastar 50 MB de Chrome sem cabeça para executar alguns pequenos testes nele. E embora a ideia de escrever stubs para todos os componentes parecesse muito dolorosa, no final eu o fiz.
Como corredor de teste, a tape
foi selecionada. Aqui está o que eu finalmente tive que simular:
- objeto de
document
e elementos DOM. Por isso, tomei jsdom ; - alguns métodos jsdom que não possuem implementação:
HTMLMediaElement#play
, HTMLCanvasElement#getContext
e navigator.mediaDevices#getDisplayMedia
; - hora. Para fazer isso, usei o
useFakeTimers
biblioteca useFakeTimers
, que sob o capô chama lolex
. Ele define suas substituições para setInterval
, requestAnimationFrame
e muitas outras funções que funcionam com o tempo e também permite controlar o fluxo desse tempo falso. Mas tenha cuidado: jsdom usa a passagem do tempo em um local do processo de inicialização e, se você ativar o sinon primeiro, tudo irá congelar.
Eu também usei sinon para todos os stubs de função que precisavam ser monitorados. O restante foi implementado por funções JS vazias.
Obviamente, você é livre para escolher as ferramentas com as quais você já está familiarizado. Espero que esta lista permita que você a prepare com antecedência, já que agora você sabe com o que precisa lidar.
O resultado final pode ser visto no repositório da biblioteca. Não parece muito bonito, mas funciona.
Conclusão
A solução acabou não sendo tão elegante quanto a janela transparente mencionada no início do artigo, mas talvez a Web chegue a isso algum dia. Só podemos esperar que, quando os navegadores aprenderem a ver através de suas janelas - esses recursos serão estritamente controlados por nós. Enquanto isso, lembre-se de que quando você mexe com a tela no Chrome - ela pode ser analisada, gravada etc. Portanto, não remexer mais do que o necessário!
Espero que alguém após este artigo tenha aprendido um novo truque para si. Se você tem idéias para o que mais isso pode ser usado, escreva nos comentários. E até breve.