Vuoi condividere lo schermo di una scheda del browser in HTML5?

Negli ultimi due anni, ho aiutato diverse aziende a realizzare funzionalità simili alla condivisione dello schermo utilizzando solo tecnologie basate su browser. In base alla mia esperienza, l'implementazione di VNC esclusivamente nelle tecnologie delle piattaforme web (ovvero senza plug-in) è un problema difficile. Ci sono molti aspetti da considerare e molte sfide da superare. L'inoltro della posizione del puntatore del mouse, l'inoltro delle sequenze di tasti e il raggiungimento di colori a 24 bit a 60 f/s sono solo alcuni dei problemi.

Acquisizione dei contenuti della scheda

Se eliminiamo le complessità della tradizionale condivisione dello schermo e ci concentriamo sulla condivisione dei contenuti di una scheda del browser, il problema semplifica notevolmente a.) l'acquisizione della scheda visibile nel suo stato attuale e b) l'invio di quel "frame" attraverso il cavo. In sostanza, abbiamo bisogno di un modo per acquisire un'istantanea del DOM e condividerlo.

La parte della condivisione è facile. I WebSocket sono in grado di inviare dati in diversi formati (stringa, JSON, file binario). La parte relativa alla creazione di snapshot è un problema molto più difficile. Progetti come html2canvas hanno affrontato l'acquisizione dello schermo in HTML reimplementando 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. Saresti fortunato ad avere una velocità effettiva di 1 f/s, molto meno dell'ambita 60 f/s.

Questo articolo illustra alcune delle mie soluzioni proof of concept preferite per la "condivisione dello schermo" di una scheda.

Metodo 1: osservatori della mutazione + WebSocket

Un approccio per il mirroring di una scheda è stato dimostrato da +Rafael Weinstein all'inizio di quest'anno. La sua tecnica utilizza Osservatori di mutazione e un WebSocket.

Essenzialmente, la scheda condivisa dal presentatore rileva le modifiche apportate alla pagina e invia le differenze allo spettatore utilizzando un websocket. Mentre l'utente scorre la pagina o interagisce con la pagina, gli osservatori acquisiscono queste modifiche e le segnalano utilizzando la libreria di riepilogo delle mutazioni di Rafael. In questo modo, le prestazioni sono elevate. Non viene inviata l'intera pagina per ogni frame.

Come sottolinea Rafael nel video, si tratta semplicemente di una proof of concept. Tuttavia, penso che sia un modo carino per combinare una funzionalità della piattaforma più recente come Mutation Observationrs con una più vecchia come Websockets.

Metodo 2: BLOB da un documento HTMLDocument + WebSocket binario

Il prossimo metodo è uno che mi è appena venuto in mente. È simile all'approccio di Mutation Observationrs, ma invece di inviare differenze di riepilogo, crea un clone BLOB dell'intero HTMLDocument e lo invia attraverso un websocket binario. Ecco la procedura di configurazione per configurazione:

  1. Riscrivi tutti gli URL della pagina in modo che siano assoluti. In questo modo, le immagini statiche e gli asset CSS non contengono link inaccessibili.
  2. Clona l'elemento del documento della pagina: document.documentElement.cloneNode(true);
  3. Rendi il clone di sola lettura, non selezionabile e impedisci lo scorrimento utilizzando CSS pointer-events: 'none';user-select:'none';overflow:hidden;
  4. Acquisisci la posizione di scorrimento corrente della pagina e aggiungili come attributi data-* sul duplicato.
  5. Crea un oggetto new Blob() da .outerHTML del duplicato.

Il codice è simile al seguente (ho semplificato le cose dal 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. Ciò è necessario per evitare che immagini, CSS, caratteri e script vengano visualizzati nel contesto di un URL blob (ad esempio, da un'origine diversa).

Un'ultima modifica che ho apportato è stata l'aggiunta del supporto dello scorrimento. Quando il presentatore scorre la pagina, deve seguire. A questo scopo, ho memorizzato le posizioni scrollX e scrollY correnti come attributi data-* sul campo HTMLDocument duplicato. Prima che venga creato il BLOB finale, viene inserito un codice JS che si attiva 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);
    });

Contraffare lo scorrimento dà l'impressione di aver acquisito una parte della pagina originale, mentre in realtà l'abbiamo duplicato e semplicemente riposizionato. #clever

Demo

Tuttavia, per la condivisione delle schede, dobbiamo acquisire continuamente la scheda e inviarla agli spettatori. Per questo ho scritto un piccolo server websocket, un'app e un bookmarklet di Node che illustra il flusso. Se il codice non ti interessa, ecco un breve video che spiega come funziona:

Miglioramenti futuri

Un'ottimizzazione non è la duplicazione dell'intero documento in ogni frame. È uno spreco, qualcosa in cui l'esempio di Mutation Observationr fa bene. Un altro miglioramento è la gestione delle immagini di sfondo CSS relative in urlsToAbsolute(). Questo è un aspetto non preso in considerazione dallo script attuale.

Metodo 3: API Chrome Extension + Binary WebSocket

Alla conferenza Google I/O 2012, ho mostrato un altro approccio per la condivisione dello schermo dei contenuti di una scheda del browser. Tuttavia, questo è un imbrogli. Richiede un'API Chrome Extension: non la pura magia HTML5.

Anche la fonte di questo articolo è disponibile su GitHub, ma il concetto è:

  1. Acquisisci la scheda corrente come URL data .png. Le estensioni di Chrome dispongono di un'API per chrome.tabs.captureVisibleTab().
  2. Converti il dataURL in un Blob. Consulta la guida convertDataURIToBlob().
  3. Invia ogni BLOB (frame) al visualizzatore utilizzando un websocket binario impostando socket.responseType='blob'.

Esempio

Di seguito è riportato un codice per acquisire uno screenshot della scheda corrente come 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 fotogrammi è sorprendentemente buona, ma potrebbe essere ancora migliore. Un miglioramento potrebbe essere la rimozione dell'overhead associato alla conversione di dataURL in un BLOB. Purtroppo, chrome.tabs.captureVisibleTab() ci fornisce solo un dataURL. Se restituisse un BLOB o un array typed, potremmo inviarlo direttamente tramite websocket invece di eseguire noi stessi la conversione in un BLOB. Aggiungi a Speciali crbug.com/32498 per renderlo possibile.

Metodo 4: WebRTC, il vero futuro

Ultimo, ma non per importanza.

Il futuro della condivisione dello 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é questo tizio non sarà pronto, lasciamo con i metodi 1-3.

Conclusione

Con la tecnologia web odierno è quindi possibile condividere le schede del browser.

Ma... questa affermazione dovrebbe essere assunta con un pizzico di sale. Nonostante siano chiare, le tecniche riportate in questo articolo non consentono di condividere un'esperienza utente ottimale in un modo o nell'altro. Tutto cambierà con l'acquisizione di contenuti delle schede WebRTC, ma finché il processo non sarà disponibile, rimarranno disponibili plug-in del browser o soluzioni limitate come quelle illustrate qui.

Hai altre tecniche? Pubblica un commento.