Udostępniasz ekran w HTML5 na karcie przeglądarki?

W ciągu ostatnich kilku lat pomogłem kilku firmom osiągnąć funkcjonalność podobną do udostępniania ekranu przy użyciu wyłącznie technologii przeglądarki. Z mojego doświadczenia wynika, że wdrożenie VNC wyłącznie w technologiach platformy internetowej (czyli bez wtyczek) jest trudnym zadaniem. Jest wiele kwestii do rozważenia i wiele wyzwań do pokonania. Przekazywanie pozycji wskaźnika myszy, przekazywanie naciśnięć klawiszy i osiąganie pełnego 24-bitowego odświeżania kolorów przy 60 kl./s to tylko niektóre z problemów.

Przechwytywanie zawartości karty

Jeśli usuniemy złożoność tradycyjnego udostępniania ekranu i skupimy się na udostępnianiu zawartości karty przeglądarki, problem znacznie się uprości do: a) przechwytywania widocznej karty w jej bieżącym stanie i b) wysyłania tej „klatki” przez sieć. Zasadniczo potrzebujemy sposobu na utworzenie zrzutu DOM i udostępnienie go.

Udostępnianie jest proste. Websockety mogą wysyłać dane w różnych formatach (ciąg tekstowy, JSON, binarny). Zrobienie zdjęcia jest znacznie trudniejsze. W projektach takich jak html2canvas podjęto próbę przechwytywania ekranu HTML przez ponowne wdrożenie silnika renderowania przeglądarki… w języku JavaScript. Innym przykładem jest Google Feedback, choć nie jest to narzędzie open source. Tego typu projekty są bardzo fajne, ale też strasznie powolne. Jeśli uda się uzyskać przepustowość 1 kl./s, to już będzie sukces, a co dopiero 60 kl./s.

W tym artykule omawiam kilka moich ulubionych rozwiązań typu proof-of-concept do „udostępniania ekranu” karty.

Metoda 1. Obserwatory mutacji + WebSocket

Jedną z metod odzwierciedlania karty zaprezentował wcześniej w tym roku +Rafael Weinstein. Jego technika wykorzystuje Mutation Observers i WebSocket.

Zasadniczo karta udostępniana przez prezentera obserwuje zmiany na stronie i wysyła różnice do przeglądarki za pomocą protokołu WebSocket. Gdy użytkownik przewija stronę lub wchodzi z nią w interakcję, obserwatorzy wykrywają te zmiany i zgłaszają je do przeglądarki, korzystając z biblioteki podsumowania mutacji Rafaela. Dzięki temu wszystko działa wydajnie. Nie cała strona jest wysyłana w każdej klatce.

Jak Rafael zauważa w filmie, jest to tylko model koncepcyjny. Mimo to uważam, że to świetny sposób na połączenie nowszej funkcji platformy, takiej jak Mutation Observers, ze starszą, taką jak Websockets.

Metoda 2. Obiekt blob z HTMLDocument + binarny WebSocket

Następna metoda to coś, co niedawno przyszło mi do głowy. Jest to podobne do podejścia Mutation Observers, ale zamiast wysyłać podsumowanie różnic, tworzy klon Blob całego elementu HTMLDocument i wysyła go przez binarny protokół WebSocket. Oto instrukcje konfiguracji:

  1. Przepisz wszystkie adresy URL na stronie, aby były bezwzględne. Dzięki temu statyczne obrazy i zasoby CSS nie będą zawierać uszkodzonych linków.
  2. Sklonuj element dokumentu na stronie: document.documentElement.cloneNode(true);
  3. Ustaw klon jako tylko do odczytu, uniemożliw zaznaczanie i przewijanie za pomocą CSS pointer-events: 'none';user-select:'none';overflow:hidden;
  4. Zapisz bieżącą pozycję przewijania strony i dodaj ją jako atrybuty data-* w duplikacie.
  5. Utwórz new Blob() z .outerHTML duplikatu.

Kod wygląda mniej więcej tak (uproszczona wersja pełnego kodu źródłowego):

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() zawiera proste wyrażenia regularne, które przekształcają względne adresy URL i adresy URL bez schematu w bezwzględne adresy URL. Jest to konieczne, aby obrazy, CSS, czcionki i skrypty nie ulegały uszkodzeniu, gdy są wyświetlane w kontekście adresu URL obiektu blob (np. z innego źródła).

Ostatnią zmianą, którą wprowadziłem, było dodanie obsługi przewijania. Gdy prezenter przewija stronę, widz powinien podążać za nim. W tym celu zapisuję bieżące pozycje scrollX i scrollY jako atrybuty data-* w duplikacie HTMLDocument. Przed utworzeniem ostatecznego obiektu Blob wstrzykiwany jest fragment kodu JS, który jest uruchamiany podczas wczytywania strony:

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

Symulowanie przewijania sprawia wrażenie, że zrobiliśmy zrzut ekranu części oryginalnej strony, podczas gdy w rzeczywistości skopiowaliśmy całą stronę i tylko zmieniliśmy jej położenie. #clever

Prezentacja

Jednak w przypadku udostępniania karty musimy stale przechwytywać kartę i wysyłać ją do widzów. W tym celu napisałem mały serwer WebSocket Node, aplikację i skryptozakładkę, które pokazują, jak to działa. Jeśli nie interesuje Cię kod, tutaj znajdziesz krótki film pokazujący, jak to działa:

Ulepszenia w przyszłości

Jedną z optymalizacji jest niepowielanie całego dokumentu w każdej ramce. To marnotrawstwo, którego można uniknąć, korzystając z Mutation Observer. Kolejnym ulepszeniem jest obsługa względnych obrazów tła CSS w elemencie urlsToAbsolute(). Obecny skrypt tego nie uwzględnia.

Metoda 3. Interfejs API rozszerzenia do Chrome + binarny protokół WebSocket

Na konferencji Google I/O 2012 zademonstrowałem inne podejście do udostępniania zawartości karty przeglądarki. Jednak ten jest oszustwem. Wymaga interfejsu API rozszerzenia do Chrome, a nie tylko HTML5.

Kod źródłowy tego przykładu jest również dostępny na GitHubie, ale najważniejsze jest to:

  1. Przechwyć bieżącą kartę jako adres URL danych .png. Rozszerzenia do Chrome mają do tego interfejs API chrome.tabs.captureVisibleTab().
  2. Przekonwertuj dataURL na Blob. Zobacz pomocnik convertDataURIToBlob().
  3. Wyślij każdy obiekt blob (klatkę) do przeglądarki za pomocą binarnego gniazda internetowego, ustawiając socket.responseType='blob'.

Przykład

Oto kod, który pozwala zrobić zrzut ekranu bieżącej karty w formacie PNG i wysłać klatkę przez 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);

Ulepszenia w przyszłości

Liczba klatek na sekundę jest zaskakująco dobra, ale mogłaby być jeszcze lepsza. Jednym z ulepszeń byłoby usunięcie narzutu związanego z konwersją dataURL na obiekt Blob. Niestety chrome.tabs.captureVisibleTab() podaje nam tylko adres URL danych. Jeśli zwróci obiekt Blob lub Typed Array, możemy wysłać go bezpośrednio przez WebSocket, zamiast samodzielnie konwertować go na obiekt Blob. Aby to umożliwić, oznacz gwiazdką crbug.com/32498.

Metoda 4. WebRTC – prawdziwa przyszłość

I ostatnia, ale nie mniej ważna kwestia.

Przyszłość udostępniania ekranu w przeglądarce będzie realizowana przez WebRTC. 14 sierpnia 2012 r. zespół zaproponował interfejs API WebRTC Tab Content Capture do udostępniania zawartości kart:

Dopóki nie będzie gotowy, musimy korzystać z metod 1–3.

Podsumowanie

Dzięki dzisiejszej technologii internetowej udostępnianie kart przeglądarki jest możliwe.

Ale… to stwierdzenie należy traktować z przymrużeniem oka. Techniki opisane w tym artykule są wprawdzie przydatne, ale nie zapewniają doskonałej wygody udostępniania. To wszystko zmieni się wraz z wprowadzeniem funkcji przechwytywania zawartości karty WebRTC, ale dopóki nie stanie się to rzeczywistością, musimy korzystać z wtyczek do przeglądarki lub ograniczonych rozwiązań, takich jak te opisane tutaj.

Znasz więcej technik? Dodaj komentarz.