Bildschirmfreigabe für einen Browsertab in HTML5?

In den letzten Jahren habe ich einigen Unternehmen geholfen, Funktionen für eine Bildschirmfreigabe zu entwickeln, die auf Browsertechnologien basieren. Nach meiner Erfahrung ist die Implementierung von VNC ausschließlich in Webplattformtechnologien (d.h. ohne Plug-ins) ein schwieriges Problem. Es gibt eine Menge zu beachten und viele Herausforderungen zu meistern. Zu den Problemen zählen beispielsweise die Übertragung der Mauszeigerposition, die Weiterleitung von Tastenanschlägen und die vollständige 24-Bit-Farbdarstellung mit 60 fps.

Tab-Inhalt aufnehmen

Wenn wir die Komplexität der herkömmlichen Bildschirmfreigabe beseitigen und uns auf die gemeinsame Nutzung der Inhalte eines Browsertabs konzentrieren, vereinfacht sich das Problem erheblich, da a.) die sichtbare Registerkarte in ihrem aktuellen Zustand erfasst wird und b.) dieser "Frame" über die Leitung gesendet wird. Im Wesentlichen brauchen wir eine Möglichkeit, um einen Snapshot des DOMs zu erstellen und freizugeben.

Das Teilen ist ganz einfach. WebSockets sind sehr in der Lage, Daten in verschiedenen Formaten (String, JSON, Binär) zu senden. Das Erstellen von Schnappschüssen ist ein viel schwierigeres Problem. Bei Projekten wie html2canvas konnte HTML mithilfe des Rendering-Moduls des Browsers wieder in JavaScript implementiert werden. Ein weiteres Beispiel ist Google Feedback, obwohl es kein Open-Source-Programm ist. Solche Projekte sind sehr cool, aber auch extrem langsam. Mit etwas Glück hättest du einen Durchsatz von 1 fps – viel weniger als begehrt von 60 fps.

In diesem Artikel werden einige meiner bevorzugten Proof-of-Concept-Lösungen für die Bildschirmfreigabe auf einem Tab beschrieben.

Methode 1: Mutation Observers + WebSocket

Rafael Weinstein hat Anfang des Jahres einen Ansatz zum Spiegeln eines Tabs gezeigt. Für sein Verfahren werden Mutation Observers und ein WebSocket verwendet.

Im Wesentlichen überwacht der Tab, den die präsentierende Person teilt, auf Änderungen an der Seite und sendet Unterschiede über ein WebSocket an den Betrachter. Wenn der Nutzer scrollt oder mit der Seite interagiert, erfassen die Beobachter diese Änderungen und melden sie dem Betrachter mithilfe der Mutationsübersichtsbibliothek von Rafael. So bleibt die Leistung aufrechterhalten. Nicht für jeden Frame wird die gesamte Seite gesendet.

Wie Rafael im Video betont, ist dies lediglich ein Proof of Concept. Dennoch denke ich, dass es eine tolle Möglichkeit ist, eine neuere Plattformfunktion wie Mutation Observers mit einer älteren Funktion wie WebSockets zu kombinieren.

Methode 2: Blob aus einem HTMLDocument + Binary WebSocket

Die nächste Methode kam mir erst vor Kurzem in den Sinn. Er ähnelt dem Mutation Observers-Ansatz, erstellt jedoch einen Blob-Klon der gesamten HTMLDocument und sendet ihn über einen binären WebSocket, statt zusammenfassende Unterschiede zu senden. So funktioniert die Einrichtung:

  1. Alle URLs auf der Seite in absolute URLs ändern So wird verhindert, dass statische Bild- und CSS-Assets fehlerhafte Links enthalten.
  2. Klonen Sie das Dokumentelement der Seite: document.documentElement.cloneNode(true);
  3. Den Klon schreibgeschützt und nicht auswählbar machen und Scrollen mit CSS pointer-events: 'none';user-select:'none';overflow:hidden; verhindern
  4. Erfassen Sie die aktuelle Scrollposition der Seite und fügen Sie sie dem Duplikat als data-*-Attribute hinzu.
  5. Erstellt ein new Blob() aus den .outerHTML des Duplikats.

Der Code sieht in etwa so aus (ich habe vereinfachte Informationen aus der vollständigen Quelle vorgenommen):

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() enthält einfache reguläre Ausdrücke, um relative/schemalose URLs in absolute Werte umzuschreiben. Das ist notwendig, damit Bilder, CSS, Schriftarten und Skripts nicht beschädigt werden, wenn sie im Kontext einer Blob-URL (z.B. von einem anderen Ursprung) betrachtet werden.

Eine letzte Optimierung war die Unterstützung für das Scrollen. Wenn die vortragende Person auf der Seite scrollt, sollten die Zuschauer mitlesen. Dazu speichere ich die aktuellen scrollX- und scrollY-Positionen als data-*-Attribute auf dem doppelten HTMLDocument. Bevor der endgültige Blob erstellt wird, wird etwas JS eingeschleust, das beim Seitenaufbau ausgelöst wird:

// 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);
    });

Das Fälschen des Scrollens vermittelt den Eindruck, dass wir einen Screenshot eines Teils der Originalseite erstellt haben, obwohl wir in Wirklichkeit das ganze Element dupliziert und nur nur neu positioniert haben. #clever

Demo

Aber für die Tab-Freigabe müssen wir den Tab kontinuierlich erfassen und an die Zuschauer senden. Dafür habe ich einen kleinen Node.js-WebSocket-Server, eine App und ein Lesezeichen geschrieben, die den Ablauf demonstriert. Falls Sie kein Interesse am Code haben, finden Sie hier ein kurzes Video, in dem die ersten Schritte veranschaulicht werden:

Zukünftige Verbesserungen

Eine Optimierung besteht darin, nicht das gesamte Dokument in jedem Frame zu duplizieren. Das ist verschwenderisch und das Mutation Observer-Beispiel funktioniert gut. Eine weitere Verbesserung besteht darin, relative CSS-Hintergrundbilder in urlsToAbsolute() zu verarbeiten. Das wird im aktuellen Skript nicht berücksichtigt.

Methode 3: Chrome Extension API + Binary WebSocket

Auf der Google I/O 2012 habe ich einen weiteren Ansatz für die Bildschirmfreigabe des Inhalts eines Browsertabs gezeigt. Diese Frage ist jedoch betrogen. Es ist eine Chrome Extension API erforderlich, keine HTML5-Magie.

Die Quelle dafür ist ebenfalls auf GitHub, aber das Wesentliche ist:

  1. Erfassen Sie den aktuellen Tab als .png dataURL. Chrome-Erweiterungen haben dafür eine API chrome.tabs.captureVisibleTab().
  2. Konvertieren Sie die dataURL in Blob. Weitere Informationen finden Sie im convertDataURIToBlob()-Hilfsprogramm.
  3. Senden Sie jeden Blob (Frame) mithilfe eines binären WebSocket an den Viewer, indem Sie socket.responseType='blob' festlegen.

Beispiel

Mit dem folgenden Code können Sie einen Screenshot des aktuellen Tabs im PNG-Format erstellen und den Frame über einen WebSocket senden:

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);

Zukünftige Verbesserungen

Die Framerate ist für dieses Video überraschend gut, könnte aber noch besser sein. Eine Verbesserung besteht darin, den Aufwand beim Konvertieren der dataURL in einen Blob zu entfallen. Leider gibt chrome.tabs.captureVisibleTab() nur eine Daten-URL an. Würde es ein Blob oder ein typisiertes Array zurückgeben, könnten wir dies direkt über den WebSocket senden, anstatt die Konvertierung selbst in ein Blob durchzuführen. Markieren Sie dazu bitte crbug.com/32498.

Methode 4: WebRTC – die wahre Zukunft

Zu guter Letzt:

Die Zukunft der Bildschirmfreigabe über Browser wird durch WebRTC realisiert. Am 14. August 2012 schlug das Team eine WebRTC Tab Content Capture API zum Teilen von Tab-Inhalten vor:

Bis er bereit ist, bleiben wir mit den Methoden 1 bis 3.

Fazit

Mit der heutigen Webtechnologie ist die Freigabe von Browsertabs also möglich!

Aber ... diese Aussage sollte mit einem Körnchen Salz verstanden werden. Auch wenn die Techniken in diesem Artikel übersichtlich sind, lassen sich manche UX-Designs auf die eine oder andere Weise nicht optimal teilen. All das ändert sich mit der Bemühungen, die WebRTC-Tab-Inhaltserfassung zu ändern. Bis es so weit ist, bleiben uns Browser-Plug-ins oder eingeschränkte Lösungen wie die hier beschriebenen.

Hast du noch weitere Techniken? Kommentar posten