Bildschirmfreigabe für einen Browsertab in HTML5?

In den letzten Jahren habe ich einigen Unternehmen dabei geholfen, eine Funktion ähnlich der Bildschirmfreigabe nur mit Browsertechnologien zu implementieren. Meiner Erfahrung nach ist die Implementierung von VNC ausschließlich mit Webplattformtechnologien (also ohne Plug-ins) ein schwieriges Problem. Es gibt viele Dinge zu beachten und viele Herausforderungen zu bewältigen. Die Weitergabe der Mauszeigerposition, die Weiterleitung von Tastenanschlägen und die vollständige 24-Bit-Farbwiedergabe bei 60 fps sind nur einige der Probleme.

Tab-Inhalt wird aufgenommen

Wenn wir die Komplexität der herkömmlichen Bildschirmfreigabe entfernen und uns auf die Freigabe des Inhalts eines Browsertabs konzentrieren, vereinfacht sich das Problem erheblich: a) Erfassen des sichtbaren Tabs in seinem aktuellen Zustand und b) Senden dieses „Frames“ über die Leitung. Im Grunde brauchen wir eine Möglichkeit, einen Snapshot des DOM zu erstellen und ihn zu teilen.

Das Teilen ist ganz einfach. WebSockets sind sehr gut in der Lage, Daten in verschiedenen Formaten (String, JSON, Binär) zu senden. Der Snapshot-Teil ist ein viel schwierigeres Problem. Bei Projekten wie html2canvas wurde das Problem der HTML-Bildschirmaufnahme gelöst, indem die Rendering-Engine des Browsers in JavaScript neu implementiert wurde. Ein weiteres Beispiel ist Google Feedback, das jedoch nicht Open Source ist. Diese Art von Projekten ist sehr cool, aber auch furchtbar langsam. Sie können sich glücklich schätzen, wenn Sie einen Durchsatz von 1 fps erreichen, geschweige denn die begehrten 60 fps.

In diesem Artikel werden einige meiner bevorzugten Proof-of-Concept-Lösungen für die Bildschirmfreigabe eines Tabs erläutert.

Methode 1: MutationObserver + WebSocket

Ein Ansatz zum Spiegeln eines Tabs wurde Anfang des Jahres von +Rafael Weinstein vorgestellt. Seine Technik verwendet MutationObserver und einen WebSocket.

Im Wesentlichen wird der Tab, den der Vortragende teilt, auf Änderungen an der Seite überwacht und Unterschiede werden über einen WebSocket an den Betrachter gesendet. Wenn der Nutzer scrollt oder mit der Seite interagiert, werden diese Änderungen von den Beobachtern erfasst und mithilfe der Mutation Summary-Bibliothek von Rafael an den Betrachter zurückgemeldet. So bleibt die Leistung hoch. Es wird nicht für jeden Frame die gesamte Seite gesendet.

Wie Rafael im Video erwähnt, ist dies nur ein Proof of Concept. Trotzdem finde ich es eine gute Möglichkeit, eine neuere Plattformfunktion wie Mutation Observer mit einer älteren wie Websockets zu kombinieren.

Methode 2: Blob aus einem HTMLDocument + binärer WebSocket

Die nächste Methode ist mir erst vor Kurzem eingefallen. Dieser Ansatz ähnelt dem mit Mutation Observers, aber anstatt Zusammenfassungen der Unterschiede zu senden, wird ein Blob-Klon des gesamten HTMLDocument erstellt und über einen binären WebSocket gesendet. Hier ist die Anleitung:

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

Der Code sieht in etwa so aus (ich habe den vollständigen Quellcode vereinfacht):

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 URLs 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) angezeigt werden.

Als letzte Anpassung habe ich die Unterstützung für das Scrollen hinzugefügt. Wenn der Vortragende auf der Seite scrollt, sollte der Zuschauer folgen. Dazu speichere ich die aktuellen scrollX- und scrollY-Positionen als data-*-Attribute im Duplikat HTMLDocument. Bevor das endgültige Blob erstellt wird, wird ein wenig JS eingefügt, 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);
    });

Durch das Vortäuschen des Scrollens entsteht der Eindruck, dass wir einen Screenshot eines Teils der Originalseite erstellt haben. Tatsächlich haben wir jedoch die gesamte Seite dupliziert und lediglich neu positioniert. #clever

Demo

Für die Tabfreigabe müssen wir den Tab jedoch kontinuierlich erfassen und an die Zuschauer senden. Dazu habe ich einen kleinen Node-WebSocket-Server, eine App und ein Bookmarklet geschrieben, die den Ablauf veranschaulichen. Wenn Sie nicht am Code interessiert sind, finden Sie hier ein kurzes Video, in dem die Funktionen gezeigt werden:

Zukünftige Verbesserungen

Eine Optimierung besteht darin, nicht das gesamte Dokument in jedem Frame zu duplizieren. Das ist ineffizient und wird im Mutation Observer-Beispiel besser gelöst. Eine weitere Verbesserung ist die Verarbeitung relativer CSS-Hintergrundbilder in urlsToAbsolute(). Das wird im aktuellen Skript nicht berücksichtigt.

Methode 3: Chrome-Erweiterung API + Binary WebSocket

Auf der Google I/O 2012 habe ich einen anderen Ansatz für die Bildschirmfreigabe des Inhalts eines Browsertabs vorgestellt. Hier wird jedoch geschummelt. Dafür ist eine Chrome-Erweiterungs-API erforderlich, nicht nur HTML5.

Die Quelle für dieses Beispiel ist ebenfalls auf GitHub verfügbar. Hier ist die Zusammenfassung:

  1. Aktuellen Tab als PNG-dataURL erfassen. Chrome-Erweiterungen haben dafür eine API: chrome.tabs.captureVisibleTab().
  2. Konvertieren Sie die dataURL in ein Blob. Siehe convertDataURIToBlob()-Hilfsprogramm.
  3. Senden Sie jedes Blob (Frame) mit einem binären WebSocket an den Betrachter, indem Sie socket.responseType='blob' festlegen.

Beispiel

Hier ist der Code, um einen Screenshot des aktuellen Tabs als PNG-Datei zu erstellen und den Frame über einen WebSocket zu 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 überraschend gut, könnte aber noch besser sein. Eine Verbesserung wäre, den Aufwand für die Konvertierung der dataURL in ein Blob zu entfernen. Leider liefert chrome.tabs.captureVisibleTab() nur eine dataURL. Wenn ein Blob oder ein typisiertes Array zurückgegeben würde, könnten wir es direkt über den WebSocket senden, anstatt die Konvertierung in einen Blob selbst vorzunehmen. Bitte markieren Sie crbug.com/32498 mit einem Stern, damit wir das umsetzen können.

Methode 4: WebRTC – die Zukunft

Und zu guter Letzt:

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

Bis diese Methode verfügbar ist, müssen wir uns mit den Methoden 1 bis 3 begnügen.

Fazit

Das Teilen von Browsertabs ist also mit der heutigen Webtechnologie möglich.

Aber… diese Aussage sollte mit Vorsicht genossen werden. Die in diesem Artikel beschriebenen Techniken sind zwar gut, aber sie bieten keine optimale UX für das Teilen. Das wird sich mit der WebRTC-Tab-Inhaltserfassung ändern, aber bis dahin müssen wir uns mit Browser-Plug-ins oder eingeschränkten Lösungen wie den hier beschriebenen begnügen.

Haben Sie weitere Techniken? Kommentar posten