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

Negli ultimi due anni, ho aiutato alcune aziende a ottenere funzionalità simili alla condivisione schermo utilizzando solo tecnologie del browser. In base alla mia esperienza, l'implementazione di VNC esclusivamente nelle tecnologie della piattaforma web (ovvero senza plug-in) è un problema difficile. Ci sono molti aspetti da considerare e molte sfide da superare. La trasmissione della posizione del puntatore del mouse, l'inoltro delle pressioni dei tasti e l'ottenimento di ricolorazioni complete a 24 bit a 60 fps sono solo alcuni dei problemi.

Acquisizione dei contenuti della scheda in corso…

Se eliminiamo le complessità della condivisione schermo tradizionale e ci concentriamo sulla condivisione dei contenuti di una scheda del browser, il problema si semplifica notevolmente: a) acquisizione della scheda visibile nel suo stato attuale e b) invio di quel "frame" tramite la rete. In sostanza, abbiamo bisogno di un modo per acquisire uno snapshot del DOM e condividerlo.

La condivisione è semplice. I WebSocket sono in grado di inviare dati in diversi formati (stringa, JSON, binario). La parte relativa agli snapshot è un problema molto più difficile. Progetti come html2canvas hanno affrontato l'acquisizione di schermate 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 anche terribilmente lenti. Saresti fortunato a ottenere un throughput di 1 fps, figuriamoci i tanto ambiti 60 fps.

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

Metodo 1: MutationObserver + WebSocket

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

In sostanza, la scheda condivisa dal presentatore monitora le modifiche alla pagina e invia le differenze al visualizzatore utilizzando un websocket. Mentre l'utente scorre o interagisce con la pagina, gli osservatori rilevano queste modifiche e le segnalano al visualizzatore utilizzando la libreria di riepilogo delle mutazioni di Rafael. In questo modo, le prestazioni rimangono elevate. L'intera pagina non viene inviata per ogni frame.

Come sottolinea 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 MutationObserver con una meno recente come WebSocket.

Metodo 2: blob da un documento HTML + WebSocket binario

Questo metodo è uno di quelli che mi sono venuti in mente di recente. È simile all'approccio MutationObserver, ma anziché inviare differenze di riepilogo, crea un clone Blob dell'intero HTMLDocument e lo invia tramite un websocket binario. Ecco la procedura di configurazione:

  1. Riscrivi tutti gli URL della pagina in modo che siano assoluti. In questo modo, gli asset immagine e CSS statici non conterranno 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 aggiungila come attributi data-* nel duplicato.
  5. Crea un new Blob() dal .outerHTML del duplicato.

Il codice è simile al seguente (ho apportato delle semplificazioni rispetto 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 espressioni regolari semplici per riscrivere gli URL relativi/senza schema in URL assoluti. Questo è necessario per evitare che immagini, CSS, caratteri e script si interrompano se visualizzati nel contesto di un URL blob (ad esempio da un'origine diversa).

Un'ultima modifica che ho apportato è stata l'aggiunta del supporto per lo scorrimento. Quando il presentatore scorre la pagina, il visualizzatore deve seguirlo. Per farlo, memorizzo le posizioni scrollX e scrollY correnti come attributi data-* sul duplicato HTMLDocument. Prima della creazione del blob finale, viene inserito un po' di codice JS che viene attivato al caricamento 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);
    });

La simulazione dello scorrimento dà l'impressione di aver 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 delle schede, dobbiamo acquisire continuamente la scheda e inviarla agli spettatori. Per questo ho scritto un piccolo server WebSocket Node, un'app e un bookmarklet che mostrano il flusso. Se non ti interessa il codice, ecco un breve video che mostra il tutto in azione:

Miglioramenti futuri

Un'ottimizzazione consiste nel non duplicare l'intero documento in ogni frame. Questo è uno spreco e l'esempio di Mutation Observer lo fa bene. Un altro miglioramento è la gestione delle immagini di sfondo CSS relative in urlsToAbsolute(). Questo è un aspetto che lo script attuale non prende in considerazione.

Metodo 3: API delle estensioni di Chrome + WebSocket binario

In occasione di Google I/O 2012, ho mostrato un altro approccio per la condivisione schermo dei contenuti di una scheda del browser. Tuttavia, questo è un imbroglio. Richiede un'API delle estensioni di Chrome: non è pura magia HTML5.

Anche il codice sorgente di questo esempio è disponibile su GitHub, ma il concetto è:

  1. Acquisisce la scheda corrente come dataURL .png. Le estensioni di Chrome hanno un'API per questo chrome.tabs.captureVisibleTab().
  2. Converti il dataURL in un Blob. Consulta l'helper convertDataURIToBlob().
  3. Invia ogni blob (frame) al visualizzatore utilizzando un WebSocket binario impostando socket.responseType='blob'.

Esempio

Ecco il 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 per questo gioco, ma potrebbe essere ancora migliore. Un miglioramento sarebbe rimuovere l'overhead della conversione del dataURL in un blob. Purtroppo, chrome.tabs.captureVisibleTab() ci fornisce solo un dataURL. Se restituisse un blob o un array tipizzato, potremmo inviarlo direttamente tramite il websocket anziché eseguire noi la conversione in un blob. Per farlo, aggiungi crbug.com/32498 a Speciali.

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 la condivisione dei contenuti delle schede:

Finché non sarà pronto, ci restano 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 pinze. Sebbene siano interessanti, le tecniche descritte in questo articolo non offrono un'esperienza utente di condivisione ottimale in un modo o nell'altro. Tutto questo cambierà con l'iniziativa WebRTC Tab Content Capture, ma finché non diventerà realtà, dovremo accontentarci di plug-in del browser o di soluzioni limitate come quelle descritte qui.

Hai altre tecniche? Pubblica un commento.