W ciągu ostatnich kilku lat pomogłem kilku firmom w wdrożeniu funkcji udostępniania ekranu przy użyciu wyłącznie technologii przeglądarki. Z moich doświadczeń wynika, że implementacja VNC wyłącznie w ramach technologii platformy internetowej (czyli bez wtyczek) jest trudnym zadaniem. Trzeba wziąć pod uwagę wiele kwestii i pokonać wiele wyzwań. Przekazywanie pozycji kursora myszy, przesyłanie naciśnięć klawiszy i uzyskiwanie pełnego odświeżania kolorów 24-bitowych z częstotliwością 60 FPS to tylko niektóre z problemów.
Rejestrowanie zawartości karty
Jeśli pominiemy złożoność tradycyjnego udostępniania ekranu i skupimy się na udostępnianiu zawartości karty przeglądarki, problem znacznie się uprości. Sprowadza się on do: a) przechwycenia widocznej karty w jej bieżącym stanie i b) przesłania tego „kadru” przez sieć. Potrzebujemy sposobu na zrobienie migawki DOM i udostępnienie jej.
Udostępnianie jest proste. Interfejs WebSockets umożliwia wysyłanie danych w różnych formatach (ciąg znaków, JSON, binarny). Część dotycząca tworzenia migawek jest znacznie trudniejsza. Projekty takie jak html2canvas rozwiązały problem z zapisywaniem ekranu HTML, ponownie implementując mechanizm renderowania przeglądarki... w JavaScript! Innym przykładem jest Google Feedback, który nie jest oparty na kodzie open source. Tego typu projekty są bardzo fajne, ale też strasznie powolne. Nawet 1 fps to dużo, a 60 fps to już szczyt marzeń.
W tym artykule omawiam kilka moich ulubionych rozwiązań typu proof-of-concept do „udostępniania ekranu” karty.
Metoda 1. Obserwatory zmian + WebSocket
W tym roku Rafael Weinstein zaprezentował jedną z metod dublowania karty. Ta technika korzysta z obserwatorów mutacji i WebSocket.
Zasadniczo karta, którą udostępnia prezenter, sprawdza zmiany na stronie i wysyła różnice do widza za pomocą WebSocket. Gdy użytkownik przewija stronę lub wchodzi z nią w interakcję, obserwatorzy rejestrują te zmiany i przekazują je widzowi za pomocą biblioteki podsumowania mutacji Rafaela. Dzięki temu wszystko działa sprawnie. Cała strona nie jest wysyłana w przypadku każdego kadru.
Jak zaznacza Rafael 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ą, takiej jak Websockets.
Metoda 2. Blob z HTMLDocument + binarne WebSocket
Ta metoda została niedawno przeze mnie odkryta. Jest to podobne do podejścia stosowanego przez obserwatorów zmian, ale zamiast wysyłać podsumowanie różnic tworzy ono klona Bloba całego HTMLDocument
i wysyła go przez binarną WebSocket. Oto konfiguracja:
- Zmień wszystkie adresy URL na stronie na bezwzględne. Zapobiega to występowaniu uszkodzonych linków w plikach z obrazami statycznymi i z kodem CSS.
- Skopiuj element dokumentu na stronie:
document.documentElement.cloneNode(true);
- Ustaw klona jako tylko do odczytu, niewybieralny i nieprzewijalny za pomocą CSS
pointer-events: 'none';user-select:'none';overflow:hidden;
- Zapisz bieżącą pozycję przewijania strony i dodaj ją jako atrybuty
data-*
w duplikacie. - Utwórz
new Blob()
z.outerHTML
duplikatu.
Kod wygląda mniej więcej tak (w porównaniu z pełnym kodem źródłowym jest uproszczony):
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 zamieniają względne lub bezschematowe adresy URL na bezwzględne. Jest to konieczne, aby obrazy, czcionki, skrypty i pliki CSS nie były uszkodzone podczas wyświetlania w kontekście adresu URL bloba (np. z innego źródła).
Ostatnią zmianą, jaką wprowadziłem, było dodanie obsługi przewijania. Gdy prezenter przewija stronę, widz powinien to robić razem z nim. W tym celu zapisuję bieżące pozycje scrollX
i scrollY
jako atrybuty data-*
w duplikacie HTMLDocument
. Przed utworzeniem ostatecznego pliku Blob wstrzykiwany jest fragment kodu JS, który jest uruchamiany przy wczytywaniu 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);
});
Udając przewijanie, sprawiamy wrażenie, że zrobiliśmy zrzut ekranu części oryginalnej strony, podczas gdy w rzeczywistości zduplikowaliśmy całą stronę i tylko ją przemieściliśmy. #clever
Prezentacja
W przypadku udostępniania karty musimy jednak ciągle rejestrować kartę i wysyłać ją do widzów. W tym celu napisałem mały serwer WebSocket Node, aplikację i bookmarklet, które demonstrują przepływ danych. Jeśli nie interesuje Cię kod, obejrzyj krótki film pokazujący, jak to działa:
Przyszłe ulepszenia
Jednym z elementów optymalizacji jest nieduplikowanie całego dokumentu w każdej klatce. To nieefektywne i właśnie w tym przykładowy kod Mutation Observer sprawdza się najlepiej. Kolejną poprawką jest obsługa względnych obrazów tła CSS w urlsToAbsolute()
. Obecny skrypt nie uwzględnia tego.
Metoda 3. Interfejs API rozszerzeń do Chrome + WebSocket binarny
Na konferencji Google I/O 2012 zaprezentowałem inny sposób udostępniania zawartości karty przeglądarki. Ten jednak jest oszukańczy. Wymaga interfejsu API rozszerzenia Chrome, a nie czystej magii HTML5.
Kod źródłowy tego modelu jest również dostępny na GitHubie, ale najważniejsze informacje to:
- Skopiuj bieżącą kartę jako adres URL danych w formacie .png. Rozszerzenia Chrome mają interfejs API do tego celu
chrome.tabs.captureVisibleTab()
. - Przekształć adres URL danych na
Blob
. Użyj narzędziaconvertDataURIToBlob()
. - Wysyłaj każdy Blob (klatkę) do odtwarzacza za pomocą binarnego interfejsu WebSocket, ustawiając wartość
socket.responseType='blob'
.
Przykład
Oto kod, który umożliwia zrobienie zrzutu ekranu bieżącej karty w formacie PNG i wysłanie ramki 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);
Przyszłe ulepszenia
W tym przypadku liczba klatek na sekundę jest zaskakująco dobra, ale mogłaby być jeszcze lepsza. Jednym z ulepszeń byłoby wyeliminowanie narzutu związanego z konwersją adresu URL danych na obiekt Blob. Niestety chrome.tabs.captureVisibleTab()
zawiera tylko adres URL danych. Jeśli zwróci on Bloba lub tablicę typów, możemy wysłać go bezpośrednio przez WebSocket, zamiast samodzielnie przekształcać go w Bloba. Aby to umożliwić, oznaczysz gwiazdką zgłoszenie crbug.com/32498.
Metoda 4. WebRTC – prawdziwa przyszłość
I na koniec
W przyszłości udostępnianie ekranu w przeglądarce będzie realizowane za pomocą WebRTC. 14 sierpnia 2012 r. zespół zaproponował interfejs API WebRTC Tab Content Capture do udostępniania treści kart:
Dopóki to nie nastąpi, pozostają nam metody 1–3.
Podsumowanie
Dzięki obecnym technologiom internetowym można udostępniać karty przeglądarki.
Ale… to stwierdzenie należy traktować z przymrużeniem oka. Chociaż są one przydatne, nie zapewniają w pełni dobrego interfejsu użytkownika do udostępniania. Wszystko się zmieni, gdy wprowadzimy rejestrowanie treści w karcie WebRTC, ale do tego czasu musimy korzystać z wtyczek do przeglądarki lub ograniczonych rozwiązań, takich jak te opisane w tym artykule.
Masz więcej technik? Dodaj komentarz.