Negli ultimi due anni ho aiutato diverse aziende a ottenere funzionalità simili alla condivisione schermo utilizzando solo tecnologie browser. Dalla mia esperienza, l'implementazione di VNC solo nelle tecnologie della piattaforma web (ovvero senza plug-in) è un problema difficile. Ci sono molte cose da considerare e molte sfide da superare. Il trasferimento della posizione del cursore del mouse, l'inoltro delle sequenze di tasti e la realizzazione di ridisegni completi a colori a 24 bit a 60 fps sono solo alcuni dei problemi.
Acquisizione dei contenuti della scheda
Se rimuoviamo le complessità della condivisione schermo tradizionale e ci concentriamo sulla condivisione dei contenuti di una scheda del browser, il problema si semplifica notevolmente in a.) acquisire la scheda visibile nel suo stato corrente e b.) inviare questo "frame" tramite la rete. In sostanza, abbiamo bisogno di un modo per acquisire uno snapshot del DOM e condividerlo.
La parte di condivisione è facile. I WebSocket sono molto efficaci nell'invio di dati in diversi formati (stringa, JSON, binario). La parte relativa agli snapshot è un problema molto più difficile. Progetti come html2canvas hanno affrontato il problema della cattura di screenshot HTML riimplementando il motore di rendering del browser… in JavaScript. Un altro esempio è Google Feedback, anche se non è open source. Questi tipi di progetti sono molto interessanti, ma sono anche terribilmente lenti. Avresti fortuna a ottenere una velocità in bit di 1 FPS, per non parlare dei 60 FPS ambiti.
Questo articolo illustra alcune delle mie soluzioni di proof-of-concept preferite per la "condivisione schermo" di una scheda.
Metodo 1: Mutation Observers + WebSocket
Un approccio per il mirroring di una scheda è stato dimostrato da +Rafael Weinstein all'inizio di quest'anno. La sua tecnica utilizza gli osservatori delle mutazioni e un WebSocket.
In sostanza, la scheda che il presentatore condivide controlla le modifiche alla pagina e invia le differenze allo spettatore utilizzando un websocket. Quando l'utente scorre o interagisce con la pagina, gli osservatori rilevano queste modifiche e le segnalano allo spettatore utilizzando la libreria di riepilogo delle mutazioni di Rafael. In questo modo, il rendimento rimane invariato. L'intera pagina non viene inviata per ogni frame.
Come sottolineato da Rafael nel video, si tratta solo di una proof of concept. Tuttavia, penso che sia un ottimo modo per combinare una funzionalità della piattaforma più recente come gli osservatori delle mutazioni con una precedente come i WebSocket.
Metodo 2: blob da un HTMLDocument + WebSocket binario
Questo metodo è stato scoperto di recente. È simile all'approccio degli osservatori delle mutazioni, ma anziché inviare le differenze di riepilogo, crea un clone Blob dell'intero HTMLDocument
e lo invia tramite un websocket binario. Ecco la configurazione per ogni tipo di installazione:
- Riscrivi tutti gli URL della pagina in modo che siano assoluti. In questo modo, gli asset CSS e immagine statici non contengono link non funzionanti.
- Clona l'elemento del documento della pagina:
document.documentElement.cloneNode(true);
- Imposta il clone come di sola lettura, non selezionabile e impedisci lo scorrimento utilizzando CSS
pointer-events: 'none';user-select:'none';overflow:hidden;
- Acquisisci la posizione di scorrimento corrente della pagina e aggiungila come attributi
data-*
alla pagina duplicata. - Crea un
new Blob()
dal.outerHTML
del duplicato.
Il codice ha il seguente aspetto (ho apportato semplificazioni al codice sorgente completo):
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()
contiene semplici espressioni regolari per riscrivere gli URL relativi/senza schema in URL assoluti. Questo è necessario per evitare che immagini, CSS, caratteri e script vengano visualizzati in modo non corretto nel contesto di un URL blob (ad es. da un'origine diversa).
Un'ultima modifica che ho apportato è stata l'aggiunta del supporto dello scorrimento. Quando il presentatore scorre la pagina, lo spettatore deve seguire. Per farlo, nascondo le posizioni attuali scrollX
e scrollY
come attributi data-*
nel HTMLDocument
duplicato. Prima della creazione del blob finale, viene inserito un po' di codice JS che viene attivato al caricamento della pagina:
// 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);
});
Simulare lo scorrimento dà l'impressione che abbiamo acquisito uno screenshot di una parte della pagina originale, quando in realtà abbiamo duplicato l'intera pagina e l'abbiamo semplicemente riposizionata. #clever
Demo
Tuttavia, per la condivisione della scheda, dobbiamo acquisire continuamente la scheda e inviarla agli spettatori. Per questo ho scritto un piccolo server websocket Node, un'app e un bookmarklet che dimostrano il flusso. Se non ti interessa il codice, ecco un breve video che mostra il funzionamento:
Miglioramenti futuri
Un'ottimizzazione è non duplicare l'intero documento in ogni frame. Questo è uno spreco e l'esempio di Mutation Observer è molto efficace in questo senso. Un altro miglioramento è la gestione delle immagini di sfondo CSS relative in urlsToAbsolute()
. È un aspetto
che lo script attuale non prende in considerazione.
Metodo 3: API di estensioni di Chrome + WebSocket binario
Alla Google I/O 2012, ho dimostrato un altro approccio per la condivisione schermo dei contenuti di una scheda del browser. Tuttavia, questa è una scorciatoia. Richiede un'API di estensione di Chrome: non è pura magia HTML5.
Il codice sorgente di questo modello è disponibile anche su GitHub, ma il succo è:
- Acquisisci la scheda corrente come dataURL .png. Le estensioni di Chrome hanno un'API per questo
chrome.tabs.captureVisibleTab()
. - Converti il dataURL in un
Blob
. Consulta l'helperconvertDataURIToBlob()
. - Invia ogni blob (frame) allo spettatore utilizzando un websocket binario impostando
socket.responseType='blob'
.
Esempio
Ecco il codice per acquisire uno screenshot della scheda corrente come file PNG e inviare il frame tramite un 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);
Miglioramenti futuri
La frequenza dei fotogrammi è sorprendentemente buona per questo video, ma potrebbe essere ancora migliore. Un miglioramento potrebbe essere la rimozione del sovraccarico della conversione di dataURL in un blob. Purtroppo, chrome.tabs.captureVisibleTab()
ci fornisce solo un dataURL. Se restituiva un Blob o un array di tipo, potremmo inviarlo direttamente tramite il websocket anziché eseguire la conversione in un Blob. Aggiungi crbug.com/32498 a Speciali per contribuire a realizzare questo obiettivo.
Metodo 4: WebRTC, il vero futuro
Ultimo, ma non meno importante.
Il futuro della condivisione schermo nel browser sarà realizzato da WebRTC. Il 14 agosto 2012, il team ha proposto un'API WebRTC Tab Content Capture per condividere i contenuti delle schede:
Finché non sarà pronto, rimarranno i metodi 1-3.
Conclusione
Quindi, la condivisione delle schede del browser è possibile con la tecnologia web di oggi.
Tuttavia, questa affermazione va presa con le dovute cautele. Sebbene interessanti, le tecniche descritte in questo articolo non offrono un'esperienza utente di condivisione ottimale in un modo o nell'altro. Tutto cambierà con la funzionalità di acquisizione dei contenuti delle schede WebRTC, ma finché non sarà realtà, dovremo utilizzare i plug-in del browser o soluzioni limitate come quelle descritte qui.
Hai altre tecniche? Pubblica un commento.