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

W ciągu ostatnich kilku lat udało mi się pomóc kilku różnym firmom osiągnąć funkcję udostępniania ekranu, korzystając wyłącznie z technologii przeglądarek. Z mojego doświadczenia wynika, że implementacja VNC wyłącznie w technologii platform internetowych (tj. bez wtyczek) to trudny problem. Jest wiele kwestii, które trzeba przemyśleć, i wiele wyzwań do przezwyciężenia. Na przykład przekazywanie pozycji wskaźnika myszy, naciśnięcia klawiszy do przekazywania dalej i pełne 24-bitowe ponowne renderowanie 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: a.) przechwycenie widocznej karty w jej obecnym stanie i b) przesłanie tej „ramki” przez sieć. Zasadniczo potrzebujemy sposobu na zapisanie DOM i udostępnienie go.

Etap udostępniania jest prosty. Protokół Websockets może wysyłać dane w różnych formatach (ciąg znaków, JSON, plik binarny). Znacznie trudniej jest zrobić zdjęcia. Projekty takie jak html2canvas poradziły sobie z przechwytywaniem ekranu HTML przez zastosowanie mechanizmu renderowania w przeglądarce (...) w języku JavaScript. Innym przykładem jest Google Feedback, choć nie jest to oprogramowanie typu open source. Projekty tego typu są bardzo fajne, ale za to strasznie powolne. Prawdopodobnie uzyskasz przepustowość 1 kl./s, a to znacznie mniej, niż w przypadku wartości 60 kl./s.

W tym artykule omawiam kilka moich ulubionych rozwiązań koncepcyjnych dotyczących „udostępniania ekranu” kart.

Metoda 1. Obserwatorzy mutacji + WebSocket

Jednym ze sposobów na odbicie lustrzane karty jest +Rafael Weinstein w tym roku. W swojej metodzie stosuje obserwacje zmian i WebSocket.

Zasadniczo karta udostępniana przez prowadzącego monitoruje zmiany na stronie i wysyła różnice do widza za pomocą czytnika internetowego. Gdy użytkownik przewija stronę lub wchodzi z nią w interakcję, obserwatorzy wychwytują te zmiany i przesyłają je z powrotem do użytkownika, używając biblioteki podsumowania mutacji Rafaela. Zapewnia to dużą wydajność. W przypadku każdej klatki nie jest wysyłana cała strona.

Jak zaznacza Rafael w filmie, jest to jedynie dowód koncepcyjny. Myślę jednak, że to świetny sposób na połączenie nowszej funkcji platformy, takiej jak Obserwatorzy mutacji, ze starszą, taką jak Websockets.

Metoda 2. Blob z dokumentu HTML + Binary WebSocket

To kolejna metoda, którą zauważyłam niedawno. Ta metoda jest podobna do metody Obserwatorzy mutacji, ale zamiast wysyłania różnic podsumowujących tworzy klon całego obiektu blob (HTMLDocument) i wysyła go na binarny websocket. Oto sposób konfiguracji w zależności od konfiguracji:

  1. Przepisz wszystkie adresy URL na stronie na bezwzględne. Dzięki temu obrazy statyczne i zasoby CSS nie będą zawierać uszkodzonych linków.
  2. Skopiuj element dokumentu strony: document.documentElement.cloneNode(true);
  3. Ustaw klonowanie w trybie tylko do odczytu, nie wybieraj opcji wyboru i zapobiegaj przewijaniu za pomocą CSS pointer-events: 'none';user-select:'none';overflow:hidden;
  4. Przechwyć obecną 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 (w pełnym kodzie zostały wprowadzone uproszczenia):

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 pozwalają zastępować względne/bezschematowe adresy URL na bezwzględne. Jest to konieczne, aby obrazy, pliki CSS, czcionki i skrypty nie działały w kontekście adresu URL obiektu blob (np. z innego źródła).

Ostatnia poprawka to dodanie obsługi przewijania. Gdy prowadzący przewija stronę, widz powinien podążać za nim. Aby to zrobić, zapisuję bieżące pozycje scrollX i scrollY jako atrybuty data-* w duplikacie HTMLDocument. Przed utworzeniem ostatniego obiektu blob wstrzyknięty jest fragment kodu JS, który uruchamia się 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);
    });

Fałszywe przewijanie daje wrażenie, że zrobiliśmy zrzut ekranu tylko dla części oryginalnej strony, podczas gdy w rzeczywistości zduplikowaliśmy cały element i zmieniliśmy jego położenie. #clever

Pokaz

Jednak aby udostępniać kartę, musimy ją stale zarejestrować i wysyłać do widzów. W tym celu stworzyłem mały serwer WWW, aplikację i zakładki do zakładek, które ilustrują ten przepływ. Jeśli nie interesuje Cię kod, obejrzyj krótki film pokazujący działanie:

Planowane ulepszenia

Jedną z optymalizacji jest nie duplikowanie całego dokumentu w każdej klatce. To marnotrawstwo, a przykład Mutation Observer sprawdza się. Kolejną ulepszoną funkcją jest obsługa względnych obrazów tła CSS w komponencie urlsToAbsolute(). Tego nie uwzględnia obecny skrypt.

Metoda 3. Chrome Extension API + Binary WebSocket

Na Google I/O 2012 zaprezentowałem inny sposób udostępniania ekranu zawartości karty przeglądarki. To jednak oszustwo. Wymaga interfejsu API rozszerzeń Chrome, a nie samej magii HTML5.

Źródło tego artykułu jest też dostępne w GitHubie, ale sedno jest:

  1. Przechwyć bieżącą kartę jako adres URL danych w formacie .png. Rozszerzenia do Chrome mają interfejs API odpowiadający temu chrome.tabs.captureVisibleTab().
  2. Przekonwertuj parametr dataURL na typ Blob. Zobacz pomocnik convertDataURIToBlob().
  3. Wyślij każdy obiekt blob (ramkę) do przeglądarki za pomocą binarnego webhooka, ustawiając wartość socket.responseType='blob'.

Przykład

Oto kod umożliwiający zrzut ekranu z bieżącą kartą w postaci PNG i wysłanie ramki w programie 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);

Planowane ulepszenia

Liczba klatek na sekundę jest zaskakująca dobra, ale może być jeszcze lepsza. Jednym z poprawek jest ograniczenie nakładu pracy związanego z konwertowaniem obiektu dataURL na obiekt Blob. chrome.tabs.captureVisibleTab() podaje tylko adres URL. Jeśli zwraca on tablicę blob lub typową, można wysłać ją bezpośrednio z websocketu, zamiast samodzielnie konwertować te obiekty. Aby tak się stało, oznacz to gwiazdką crbug.com/32498.

Metoda 4. WebRTC – rzeczywista przyszłość

Na koniec jeszcze jedna uwaga.

Przyszłość udostępniania ekranu w przeglądarce urzeczywistni się dzięki WebRTC. 14 sierpnia 2012 roku zespół zaproponował interfejs API WebRTC Tab Content Capture do udostępniania zawartości kart:

Dopóki ten gość nie będzie gotowy, pozostajemy z metodami 1-3.

Podsumowanie

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

Jednak... to twierdzenie należy podchodzić z ziarnem ostrożności. Techniki omówione w tym artykule są utrzymane w porządku, jednak w jakiś sposób nie dają dobrych wrażeń w związku z udostępnianiem. Wszystko to się zmieni dzięki zastosowaniu narzędzia WebRTC Tab Content Capture, ale dopóki nie urzeczywistnimy się to w Google, będziemy pozostawiać wtyczki do przeglądarki lub ograniczone rozwiązania, takie jak te omówione w tym artykule.

Masz więcej metod? Opublikuj komentarz