Nos últimos anos, ajudei algumas empresas a alcançar uma funcionalidade semelhante ao compartilhamento de tela usando apenas tecnologias de navegador. Na minha experiência, implementar o VNC apenas em tecnologias de plataforma da Web (ou seja, sem plug-ins) é um problema difícil. Há muitas coisas a considerar e muitos desafios a superar. A transmissão da posição do ponteiro do mouse, o encaminhamento de teclas e a pintura de cores de 24 bits a 60 fps são apenas alguns dos problemas.
Captura de conteúdo da guia
Se removermos as complexidades do compartilhamento de tela tradicional e nos concentrarmos no compartilhamento do conteúdo de uma guia do navegador, o problema será muito simplificado para a.) capturar a guia visível no estado atual e b.) enviar esse "frame" pela rede. Precisamos de uma maneira de fazer uma captura de tela do DOM e compartilhá-la.
A parte de compartilhamento é fácil. Os WebSockets são muito capazes de enviar dados em diferentes formatos (string, JSON, binário). A parte de criação de snapshots é um problema muito mais difícil. Projetos como o html2canvas abordaram a captura de tela HTML reimplementando o mecanismo de renderização do navegador… em JavaScript! Outro exemplo é o Google Feedback, que não é de código aberto. Esses tipos de projetos são muito legais, mas também são terrivelmente lentos. Você teria sorte se conseguisse uma taxa de transferência de 1 fps, muito menos que os cobiçados 60 fps.
Neste artigo, discutimos algumas das minhas soluções de prova de conceito favoritas para "compartilhar a tela" de uma guia.
Método 1: observadores de mutação + WebSocket
Uma abordagem para espelhar uma guia foi demonstrada por +Rafael Weinstein no início deste ano. A técnica dele usa Mutation Observers e um WebSocket.
Basicamente, a guia que o apresentador está compartilhando monitora as alterações na página e envia diferenças para o espectador usando um WebSocket. À medida que o usuário rola ou interage com a página, os observadores detectam essas mudanças e as informam ao espectador usando a biblioteca de resumo de mutação do Rafael. Isso mantém o desempenho. A página inteira não é enviada para cada frame.
Como Rafael aponta no vídeo, isso é apenas uma prova de conceito. Ainda assim, acho que é uma maneira legal de combinar um recurso mais recente da plataforma, como os Mutation Observers, com um mais antigo, como os Websockets.
Método 2: blob de um HTMLDocument + WebSocket binário
Esse próximo método é um que me ocorreu recentemente. É semelhante à abordagem dos observadores de mutação, mas, em vez de enviar diferenças de resumo, ele cria um clone de Blob de todo o HTMLDocument
e o envia por um websocket binário. Confira a configuração por configuração:
- Reescreva todos os URLs na página para que sejam absolutos. Isso evita que recursos de imagem estática e CSS tenham links quebrados.
- Clone o elemento do documento da página:
document.documentElement.cloneNode(true);
- Tornar o clone somente leitura, não selecionável, e impedir a rolagem usando CSS
pointer-events: 'none';user-select:'none';overflow:hidden;
- Capture a posição de rolagem atual da página e adicione-a como atributos
data-*
na cópia. - Crie uma
new Blob()
com base no.outerHTML
da cópia.
O código é mais ou menos assim (fiz simplificações na fonte completa):
function screenshotPage() {
// 1. Rewrite current doc's imgs, css, and script URLs to be absolute before
// we duplicate. This ensures no broken links when viewing the duplicate.
urlsToAbsolute(document.images);
urlsToAbsolute(document.querySelectorAll("link[rel='stylesheet']"));
urlsToAbsolute(document.scripts);
// 2. Duplicate entire document tree.
var screenshot = document.documentElement.cloneNode(true);
// 3. Screenshot should be readyonly, no scrolling, and no selections.
screenshot.style.pointerEvents = 'none';
screenshot.style.overflow = 'hidden';
screenshot.style.userSelect = 'none'; // Note: need vendor prefixes
// 4. … read on …
// 5. Create a new .html file from the cloned content.
var blob = new Blob([screenshot.outerHTML], {type: 'text/html'});
// Open a popup to new file by creating a blob URL.
window.open(window.URL.createObjectURL(blob));
}
urlsToAbsolute()
contém regexs simples para transformar URLs relativos/sem esquema em absolutos. Isso é necessário para que imagens, css, fontes e scripts não sejam interrompidos quando visualizados no contexto de um URL de blob (por exemplo, de uma origem diferente).
Um último ajuste que fiz foi adicionar suporte à rolagem. Quando o apresentador rola a página, o espectador precisa acompanhar. Para fazer isso, armazenei as posições atuais de scrollX
e scrollY
como atributos data-*
na cópia de HTMLDocument
. Antes que o blob final seja criado, um pouco de JS é injetado e acionado no carregamento da página:
// 4. Preserve current x,y scroll position of this page. See addOnPageLoad().
screenshot.dataset.scrollX = window.scrollX;
screenshot.dataset.scrollY = window.scrollY;
// 4.5. When screenshot loads (e.g. in blob URL), scroll it to the same location
// of this page. Do this by appending a window.onDOMContentLoaded listener
// which pulls out the screenshot (dupe's) saved scrollX/Y state on the DOM.
var script = document.createElement('script');
script.textContent = '(' + addOnPageLoad_.toString() + ')();'; // self calling.
screenshot.querySelector('body').appendChild(script);
// NOTE: Not to be invoked directly. When the screenshot loads, scroll it
// to the same x,y location of original page.
function addOnPageLoad() {
window.addEventListener('DOMContentLoaded', function(e) {
var scrollX = document.documentElement.dataset.scrollX || 0;
var scrollY = document.documentElement.dataset.scrollY || 0;
window.scrollTo(scrollX, scrollY);
});
Simular a rolagem dá a impressão de que capturamos uma parte da página original, quando na verdade duplicamos tudo e apenas reposicionamos. #clever
Demonstração
Mas, para compartilhar guias, precisamos capturar a guia continuamente e enviá-la aos espectadores. Para isso, escrevi um pequeno servidor Node websocket, um app e um bookmarklet que demonstram o fluxo. Se você não tiver interesse no código, confira um breve vídeo de como ele funciona:
Melhorias futuras
Uma otimização é não duplicar o documento inteiro em todos os frames. Isso é um desperdício e algo que o exemplo de Mutation Observer faz bem. Outra melhoria é
processar imagens de plano de fundo CSS relativas em urlsToAbsolute()
. Isso é algo
que o script atual não considera.
Método 3: API da extensão do Chrome + WebSocket binário
No Google I/O 2012, demonstrei outra abordagem para compartilhar a tela do conteúdo de uma guia do navegador. No entanto, este é um truque. Ele requer uma API de extensão do Chrome, não a magia pura do HTML5.
A fonte desse exemplo também está no GitHub, mas o objetivo é:
- Capture a guia atual como um dataURL .png. As extensões do Chrome têm uma API para isso
chrome.tabs.captureVisibleTab()
. - Converta o dataURL em um
Blob
. Consulte o auxiliarconvertDataURIToBlob()
. - Envie cada Blob (frame) para o espectador usando um websocket binário definindo
socket.responseType='blob'
.
Exemplo
Confira o código para fazer uma captura de tela da guia atual como PNG e enviar o frame por um WebSocket:
var IMG_MIMETYPE = 'images/jpeg'; // Update to image/webp when crbug.com/112957 is fixed.
var IMG_QUALITY = 80; // [0-100]
var SEND_INTERVAL = 250; // ms
var ws = new WebSocket('ws://…', 'dumby-protocol');
ws.binaryType = 'blob';
function captureAndSendTab() {
var opts = {format: IMG_MIMETYPE, quality: IMG_QUALITY};
chrome.tabs.captureVisibleTab(null, opts, function(dataUrl) {
// captureVisibleTab returns a dataURL. Decode it -> convert to blob -> send.
ws.send(convertDataURIToBlob(dataUrl, IMG_MIMETYPE));
});
}
var intervalId = setInterval(function() {
if (ws.bufferedAmount == 0) {
captureAndSendTab();
}
}, SEND_INTERVAL);
Melhorias futuras
A taxa de quadros é surpreendentemente boa para esse vídeo, mas poderia ser ainda melhor. Uma melhoria seria remover a sobrecarga da conversão do dataURL em um Blob. Infelizmente, chrome.tabs.captureVisibleTab()
só nos dá um dataURL. Se ele retornasse um blob ou uma matriz com tipos, poderíamos enviar diretamente pelo websocket, em vez de fazer a conversão para um blob. Dê estrela a crbug.com/32498 para que isso aconteça.
Método 4: WebRTC: o futuro real
Por último, mas não menos importante!
O futuro do compartilhamento de tela no navegador será realizado pelo WebRTC. Em 14 de agosto de 2012, a equipe propôs uma API WebRTC Tab Content Capture para compartilhar o conteúdo da guia:
Até que ele esteja pronto, vamos usar os métodos 1 a 3.
Conclusão
O compartilhamento de guias do navegador é possível com a tecnologia da Web atual.
Mas… essa declaração precisa ser considerada com cautela. Embora sejam interessantes, as técnicas deste artigo não são suficientes para compartilhar uma ótima experiência de compartilhamento. Isso vai mudar com a captura de conteúdo de guias do WebRTC, mas até que isso se torne realidade, teremos plugins de navegador ou soluções limitadas, como as abordadas aqui.
Tem mais técnicas? Postar um comentário