Bildschirmfreigabe für einen Browsertab in HTML5?

In den letzten Jahren habe ich mehreren Unternehmen geholfen, Funktionen wie den Bildschirmfreigabedienst mithilfe von Browsertechnologien zu implementieren. Aus meiner Erfahrung ist es schwierig, VNC ausschließlich in Webplattformtechnologien (d.h. ohne Plug-ins) zu implementieren. Es gibt viele Dinge zu beachten und viele Herausforderungen zu meistern. Die Position des Mauszeigers weitergeben, Tastenanschläge weiterleiten und eine vollständige 24-Bit-Farbwiedergabe mit 60 fps erzielen sind nur einige der Probleme.

Tab-Inhalte erfassen

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) Der sichtbare Tab wird in seinem aktuellen Zustand erfasst und b) dieser „Frame“ wird über die Leitung gesendet. Im Grunde benötigen wir eine Möglichkeit, ein Snapshot des DOM zu erstellen und freizugeben.

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. Projekte wie html2canvas haben sich dem Erstellen von Screenshots von HTML angenommen, indem sie das Rendering-Engine des Browsers in JavaScript neu implementiert haben. Ein weiteres Beispiel ist Google Feedback, das jedoch kein Open-Source-Tool ist. Diese Art von Projekten ist sehr cool, aber auch extrem langsam. Sie wären schon glücklich, wenn Sie eine Durchlaufrate von 1 fps erreichen würden, ganz zu schweigen von den begehrten 60 fps.

In diesem Artikel werden einige meiner bevorzugten Proof-of-Concept-Lösungen für den „Bildschirmfreigabe“ eines Tabs beschrieben.

Methode 1: Mutation Observers + WebSocket

Eine Möglichkeit zum Spiegeln eines Tabs hat +Rafael Weinstein Anfang des Jahres gezeigt. Dabei verwendet er Mutation Observers und ein WebSocket.

Der Tab, den der Vortragende freigibt, überwacht im Grunde Änderungen an der Seite und sendet Differenzen ü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 Mutation Summary Library von Rafael. So bleibt die Leistung hoch. Die gesamte Seite wird nicht für jeden Frame gesendet.

Wie Rafael im Video erwähnt, ist dies nur ein Proof of Concept. Ich denke aber, dass es eine gute Möglichkeit ist, eine neuere Plattformfunktion wie Mutation Observers mit einer älteren wie Websockets zu kombinieren.

Methode 2: Blob aus einem HTML-Dokument + binäres WebSocket

Diese nächste Methode ist mir vor Kurzem aufgefallen. Er ähnelt dem Ansatz von Mutation Observers, aber anstatt Zusammenfassungs-Diffs zu senden, wird ein Blob-Klon der gesamten HTMLDocument erstellt und über ein binäres WebSocket gesendet. So funktioniert die Einrichtung je nach Einrichtung:

  1. Erstellen Sie alle URLs auf der Seite als absolute URLs. So wird verhindert, dass statische Bilder 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 das Scrollen mit CSS verhindern pointer-events: 'none';user-select:'none';overflow:hidden;
  4. Erfassen Sie die aktuelle Scrollposition der Seite und fügen Sie sie dem Duplikat als data-*-Attribute hinzu.
  5. Erstellen Sie eine new Blob() aus der .outerHTML des Duplikats.

Der Code sieht in etwa so aus (ich habe die vollständige Quelle 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, mit denen relative/schemalose URLs in absolute URLs umgeschrieben werden. Das ist erforderlich, damit Bilder, CSS, Schriftarten und Scripts nicht beschädigt werden, wenn sie im Kontext einer Blob-URL angezeigt werden (z.B. von einem anderen Ursprung).

Als letzte Änderung habe ich die Scrollfunktion hinzugefügt. Wenn der Vortragende auf der Seite scrollt, sollten die Zuschauer mitlesen. Dazu speichere ich die aktuellen Positionen scrollX und scrollY als data-*-Attribute im Duplikat HTMLDocument. Bevor der endgültige Blob erstellt wird, wird ein JS-Code eingefügt, der beim Laden der Seite 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 gefälschte Scrollen entsteht der Eindruck, dass wir einen Screenshot eines Teils der ursprünglichen Seite gemacht haben, obwohl wir in Wirklichkeit die gesamte Seite dupliziert und nur neu positioniert haben. #clever

Demo

Bei der Freigabe von Tabs 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 sich nicht für den Code interessieren, sehen Sie sich dieses kurze Video an:

Zukünftige Verbesserungen

Eine Optimierung besteht darin, das gesamte Dokument nicht in jedem Frame zu duplizieren. Das ist verschwenderisch und das Beispiel mit dem Mutation Observer zeigt das gut. Eine weitere Verbesserung ist die Verarbeitung relativer CSS-Hintergrundbilder in urlsToAbsolute(). Das wird im aktuellen Script nicht berücksichtigt.

Methode 3: Chrome Extension API + Binary WebSocket

Auf der Google I/O 2012 habe ich einen anderen Ansatz für die Bildschirmfreigabe des Inhalts eines Browsertabs demonstriert. Dieser Trick ist jedoch ein Betrug. Es ist eine Chrome-Erweiterungs-API erforderlich, keine reine HTML5-Magie.

Der Quellcode für diesen Code ist ebenfalls auf GitHub verfügbar. Hier ist der Grundgedanke:

  1. Den aktuellen Tab als .png-Daten-URL erfassen Chrome-Erweiterungen haben dafür eine API chrome.tabs.captureVisibleTab().
  2. Konvertieren Sie die dataURL in eine Blob. Weitere Informationen finden Sie im Hilfeartikel zu convertDataURIToBlob().
  3. Wenn du jeden Blob (Frame) über ein binäres WebSocket an den Betrachter senden möchtest, setze socket.responseType='blob'.

Beispiel

Hier ist der Code, um einen Screenshot des aktuellen Tabs als PNG zu erstellen und den Frame über ein 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 für diesen Titel überraschend gut, könnte aber noch besser sein. Eine Verbesserung wäre es, den Overhead bei der Umwandlung der Daten-URL in einen Blob zu entfernen. Leider erhalten wir von chrome.tabs.captureVisibleTab() nur eine Daten-URL. Wenn ein Blob oder ein typisiertes Array zurückgegeben wird, können wir es direkt über das WebSocket senden, anstatt die Umwandlung in einen Blob selbst vorzunehmen. Bitte setzen Sie ein Häkchen bei crbug.com/32498, damit wir das umsetzen können.

Methode 4: WebRTC – die wahre Zukunft

Zu guter Letzt:

Die Zukunft der Bildschirmfreigabe im Browser wird durch WebRTC ermöglicht. Am 14. August 2012 schlug das Team eine WebRTC Tab Content Capture API für die Freigabe von Tabinhalten vor:

Bis dahin bleiben uns die Methoden 1–3.

Fazit

Mit der heutigen Webtechnologie ist das Teilen von Browsertabs also möglich.

Aber… diese Aussage sollte mit Vorsicht betrachtet werden. Die in diesem Artikel beschriebenen Techniken sind zwar praktisch, aber sie bieten in gewisser Weise keine optimale UX für die Freigabe. Das wird sich mit der WebRTC-Tab-Inhaltserfassung ändern, aber bis dahin bleiben uns nur Browser-Plug-ins oder eingeschränkte Lösungen wie die hier beschriebenen.

Haben Sie weitere Techniken? Schreib einen Kommentar!