Nos últimos anos, ajudei algumas empresas diferentes a alcançar funcionalidades do tipo compartilhamento de tela usando apenas tecnologias de navegador. Por experiência própria, implementar 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. Redirecionar a posição do ponteiro do mouse, encaminhar os pressionamentos de tecla e realizar repinturas de cores completas de 24 bits a 60 fps são apenas alguns dos problemas.
Captura do 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á bastante simplificado para: a.) capturar a guia visível no estado atual e b.) enviar esse "quadro" pela rede. Essencialmente, precisamos de uma maneira de criar um snapshot e compartilhar o DOM.
O compartilhamento é fácil. Os Websockets são muito capazes de enviar dados em diferentes formatos (string, JSON, binário). A parte da criação de snapshots é um problema muito mais difícil. Projetos como o html2canvas enfrentaram a captura de tela em HTML implementando novamente 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 de ter 1 fps, muito menos que os 60 fps.
Este artigo discute algumas das minhas soluções de prova de conceito favoritas para o compartilhamento de 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.
Essencialmente, a guia compartilhada pelo apresentador observa as mudanças na página e envia diferenças para o visualizador usando um websocket. À medida que o usuário rola ou interage com a página, os observadores captam essas mudanças e as informam de volta usando a biblioteca de resumo de mutações de Rafael. Isso mantém o desempenho funcionando. A página inteira não é enviada para todos os frames.
Como Rafael destaca no vídeo, isso é meramente uma prova de conceito. Ainda assim, acho uma boa maneira de combinar um recurso de plataforma mais recente, como o Mutation Observers, com um mais antigo, como o Websockets.
Método 2: Blob de um HTMLDocument + WebSocket binário
Este próximo método é algo que despertou para mim. Ele é semelhante à abordagem de Mutation Observers, mas, em vez de enviar diffs de resumo, ele cria um clone Blob de todo o HTMLDocument
e o envia por um websocket binário. Veja a definição por configuração:
- Reescreva todos os URLs da página para serem absolutos. Isso impede que imagens estáticas e recursos CSS contenham links corrompidos.
- Clonar 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-*
à cópia. - Crie um
new Blob()
com base no.outerHTML
da cópia.
O código é mais ou menos assim (Fiz simplificações a partir 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 regex simples para reescrever URLs relativos/sem esquema para 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).
Um último ajuste que fiz foi adicionar suporte à rolagem. Quando o apresentador rola a página, o espectador precisa acompanhar. Para fazer isso, guardo as posições scrollX
e scrollY
atuais como atributos data-*
no HTMLDocument
duplicado. Antes da criação do Blob final, é injetado um pouco de JS que é 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 fizemos uma captura de tela de uma parte da página original, quando, na verdade, duplicamos tudo e apenas o reposicionamos. #clever
Demonstração
No entanto, para o compartilhamento de guias, precisamos capturar a guia continuamente e enviá-la aos espectadores. Para isso, criei um pequeno servidor websocket, um app e um marcador de posição Node que demonstra o fluxo. Caso não tenha interesse no código, assista a este vídeo curto de como funciona:
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 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 de tela. No entanto, essa é uma trapaça. Ela exige uma API de extensão do Google Chrome, não magia pura em HTML5.
A fonte deste arquivo também está no GitHub, mas a essência é:
- Captura a guia atual como um dataURL .png. As extensões do Chrome têm uma API para essa
chrome.tabs.captureVisibleTab()
. - Converta o dataURL em um
Blob
. Consulte o assistente doconvertDataURIToBlob()
. - Envie cada Blob (frame) ao visualizador usando um websocket binário definindo
socket.responseType='blob'
.
Exemplo
Confira este 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
O frame rate é surpreendentemente bom para esse caso, mas poderia ser ainda melhor. Uma melhoria seria remover a sobrecarga de conversão do dataURL em um Blob. Infelizmente, o método chrome.tabs.captureVisibleTab()
fornece apenas um dataURL. Se ele retornasse um Blob ou Typed Array, poderíamos enviá-lo diretamente pelo websocket em vez de fazer a conversão para um Blob nós mesmos. 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á concretizado pela WebRTC. Em 14 de agosto de 2012, a equipe propôs uma API WebRTC Tab Content Capture para o compartilhamento de conteúdo de guias:
Até que esse cara esteja pronto, vamos continuar com os métodos 1 a 3.
Conclusão
Portanto, o compartilhamento de guias do navegador é possível com a tecnologia da web de hoje.
Mas... essa afirmação deve ser interpretada com muito sal. Embora sejam legais, as técnicas deste artigo não são suficientes para compartilhar a experiência do usuário de uma forma ou de outra. Tudo isso mudará com o esforço de captura de conteúdo de guias WebRTC, mas até que isso se concretize, vamos continuar com plug-ins do navegador ou soluções limitadas como as que são abordadas aqui.
Tem mais técnicas? Poste um comentário!