Nos últimos anos, ajudei algumas empresas a implementar uma funcionalidade semelhante ao compartilhamento de tela usando apenas tecnologias de navegador. Pela minha experiência, implementar o VNC apenas em tecnologias de plataforma da Web (ou seja, sem plug-ins) é um problema difícil. Há muitos fatores a considerar e desafios a superar. Transmitir a posição do ponteiro do mouse, encaminhar as teclas digitadas e alcançar repinturas de cores de 24 bits a 60 fps são apenas alguns dos problemas.
Capturando o conteúdo da guia
Se removermos as complexidades do compartilhamento de tela tradicional e nos concentrarmos em compartilhar o conteúdo de uma guia do navegador, o problema será simplificado para: a) capturar a guia visível no estado atual e b) enviar esse "frame" pela rede. Basicamente, precisamos de uma maneira de capturar o DOM e compartilhá-lo.
A parte do compartilhamento é fácil. Os websockets são muito capazes de enviar dados em diferentes formatos (string, JSON, binário). A parte do snapshot é um problema muito mais difícil. Projetos como o html2canvas (link em inglês) tentaram resolver o problema de captura de tela de HTML reimplementando o mecanismo de renderização do navegador… em JavaScript! Outro exemplo é o Google Feedback, embora não seja 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 capacidade de processamento de 1 fps, muito menos 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: Mutation Observers + 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 mudanças na página e envia as diferenças para o espectador usando um WebSocket. Conforme o usuário rola ou interage com a página, os observadores detectam essas mudanças e as informam ao visualizador usando a biblioteca de resumo de mutações do Rafael. Isso mantém o desempenho. A página inteira não é enviada para cada frame.
Como Rafael destaca no vídeo, essa é apenas uma prova de conceito. Ainda assim, acho que é uma maneira legal de combinar um recurso de plataforma mais recente, como os Mutation Observers, com um mais antigo, como os Websockets.
Método 2: blob de um HTMLDocument + WebSocket binário
Este 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 as etapas:
- Reescreva todos os URLs na página para que sejam absolutos. Isso evita que os recursos estáticos de imagem e CSS contenham links corrompidos.
- Clonar o elemento do documento da página:
document.documentElement.cloneNode(true); - Torne o clone somente leitura, não selecionável e impeça a rolagem usando o 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 um
new Blob()com base no.outerHTMLda cópia.
O código é semelhante a este (fiz simplificações da 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 reescrever URLs relativos/sem esquema em URLs absolutos. Isso é necessário para que imagens, CSS, fontes e scripts não sejam corrompidos quando visualizados no contexto de um URL de blob (por exemplo, de uma origem diferente).
O último ajuste que fiz foi adicionar suporte à rolagem. Quando o apresentador rolar a página, o espectador deve acompanhar. Para fazer isso, armazeno as posições atuais de scrollX e scrollY como atributos data-* no HTMLDocument duplicado. Antes de criar o Blob final, um pouco de JS é injetado e disparado no carregamento de 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 fizemos uma captura de tela de uma parte da página original, quando, na verdade, duplicamos tudo e apenas reposicionamos. #clever
Demonstração
Mas, para compartilhar guias, precisamos capturar continuamente a guia e enviá-la aos espectadores. Para isso, escrevi um pequeno servidor WebSocket do Node, um app e um bookmarklet que demonstram o fluxo. Se você não tiver interesse no código, confira este vídeo curto que mostra o funcionamento:
Melhorias futuras
Uma otimização é não duplicar o documento inteiro em cada frame. Isso é um desperdício, e o exemplo do Mutation Observer faz isso 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 de extensão do Chrome + WebSocket binário
No Google I/O 2012, demonstrei outra abordagem para compartilhar o conteúdo de uma guia do navegador. No entanto, essa é uma trapaça. Ele requer uma API de extensão do Chrome, não apenas HTML5.
A origem (link em inglês) também está no GitHub, mas o resumo é:
- Captura 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) ao espectador usando um WebSocket binário ao definir
socket.responseType='blob'.
Exemplo
Este é o código para fazer uma captura de tela da guia atual como um 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, mas poderia ser ainda melhor. Uma melhoria seria remover a sobrecarga de converter o dataURL em um blob. Infelizmente, chrome.tabs.captureVisibleTab() só fornece um dataURL. Se ele retornasse um Blob ou uma matriz tipada, poderíamos enviá-lo diretamente pelo WebSocket em vez de fazer a conversão para um Blob. Marque crbug.com/32498 com estrela para que isso aconteça.
Método 4: WebRTC, o verdadeiro futuro
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 de guias:
Até que ele esteja pronto, temos os métodos 1 a 3.
Conclusão
Portanto, o compartilhamento de guias do navegador é possível com a tecnologia da Web atual.
Mas… essa afirmação deve ser vista com cautela. Embora sejam interessantes, as técnicas deste artigo não oferecem uma ótima UX de compartilhamento de uma forma ou de outra. Isso vai mudar com o esforço de captura de conteúdo de guias do WebRTC, mas, até que isso seja uma realidade, ficamos com plug-ins de navegador ou soluções limitadas, como as abordadas aqui.
Tem mais técnicas? Poste um comentário!