Демонстрация экрана вкладки браузера в HTML5?

За последние пару лет я помог нескольким компаниям реализовать функциональность, подобную совместному использованию экрана, используя только технологии браузера. По моему опыту, реализация VNC исключительно в технологиях веб-платформы (т. е. без плагинов) является сложной проблемой. Есть много вещей, о которых нужно подумать, и много проблем, которые нужно преодолеть. Передача положения указателя мыши, перенаправление нажатий клавиш и полная перерисовка 24-битного цвета со скоростью 60 кадров в секунду — это лишь некоторые из проблем.

Захват содержимого вкладки

Если мы удалим сложности традиционного совместного использования экрана и сосредоточимся на совместном использовании содержимого вкладки браузера, проблема значительно упростится до а.) захвата видимой вкладки в ее текущем состоянии и б.) отправки этого «кадра» по сети. По сути, нам нужен способ сделать снимок DOM и поделиться им.

Часть обмена проста. Веб-сокеты способны отправлять данные в разных форматах (строка, JSON, двоичный). Часть моментальных снимков представляет собой гораздо более сложную задачу. Такие проекты, как html2canvas, решили захват экрана HTML путем повторной реализации механизма рендеринга браузера… на JavaScript! Другой пример — Google Feedback , хотя его исходный код не является открытым. Подобные проекты очень крутые, но они также ужасно медленные. Вам повезет, если вы получите пропускную способность 1 кадр в секунду, а тем более желанные 60 кадров в секунду.

В этой статье обсуждаются некоторые из моих любимых экспериментальных решений для «совместного использования экрана» вкладки.

Метод 1: наблюдатели мутаций + WebSocket

Один из подходов к зеркалированию вкладки был продемонстрирован Рафаэлем Вайнштейном ранее в этом году. Его техника использует наблюдатели мутаций и WebSocket.

По сути, вкладка, которой делится ведущий, отслеживает изменения на странице и отправляет различия зрителю с помощью веб-сокета. Когда пользователь прокручивает страницу или взаимодействует с ней, наблюдатели улавливают эти изменения и сообщают о них зрителю, используя библиотеку сводки мутаций Rafael. Это сохраняет производительность. Вся страница не отправляется для каждого кадра.

Как отмечает Рафаэль в видео, это всего лишь доказательство концепции. Тем не менее, я думаю, что это отличный способ объединить новую функцию платформы, такую ​​​​как Mutation Observers, со старой, такой как Websockets.

Способ 2: большой двоичный объект из HTMLDocument + двоичный WebSocket

Следующий метод пришел ко мне недавно. Он похож на подход Mutation Observers, но вместо отправки сводных различий он создает клон Blob всего HTMLDocument и отправляет его через двоичный веб-сокет. Вот настройка за установкой:

  1. Перепишите все URL-адреса на странице, чтобы они были абсолютными. Это предотвращает появление неработающих ссылок в статических изображениях и CSS-ресурсах.
  2. Клонируйте элемент документа страницы: document.documentElement.cloneNode(true);
  3. Сделайте клон доступным только для чтения, недоступным для выбора и запретите прокрутку с помощью pointer-events: 'none';user-select:'none';overflow:hidden;
  4. Зафиксируйте текущую позицию прокрутки страницы и добавьте ее в качестве атрибутов data-* в дубликат.
  5. Создайте new Blob() из .outerHTML дубликата.

Код выглядит примерно так (я сделал упрощения из полного исходника):

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() содержит простые регулярные выражения для перезаписи относительных/бессхемных URL-адресов в абсолютные. Это необходимо, чтобы изображения, CSS, шрифты и скрипты не ломались при просмотре в контексте URL-адреса большого двоичного объекта (например, из другого источника).

Последним изменением, которое я сделал, было добавление поддержки прокрутки. Когда ведущий прокручивает страницу, зритель должен следовать за ней. Для этого я сохраняю текущие позиции scrollX и scrollY в качестве атрибутов data-* в дубликате HTMLDocument . Прежде чем будет создан окончательный Blob, вводится немного JS, который срабатывает при загрузке страницы:

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

Имитация прокрутки создает впечатление, что мы сделали скриншот части исходной страницы, хотя на самом деле мы продублировали ее целиком и просто изменили ее положение. #умный

Демо

Но для совместного использования вкладок нам необходимо постоянно захватывать вкладку и отправлять ее зрителям. Для этого я написал небольшой веб-сервер Node, приложение и букмарклет, демонстрирующий весь процесс. Если вам не интересен код , вот небольшое видео, как все в действии:

Будущие улучшения

Одна из оптимизаций заключается в том, чтобы не дублировать весь документ в каждом кадре. Это расточительно, и с этим хорошо справляется пример Mutation Observer. Еще одним улучшением является обработка относительных фоновых изображений CSS в urlsToAbsolute() . Это то, что текущий сценарий не учитывает.

Способ 3: API расширений Chrome + двоичный WebSocket

На Google I/O 2012 я продемонстрировал другой подход к демонстрации содержимого вкладки браузера. Однако это обман. Для этого требуется API расширений Chrome, а не чистая магия HTML5.

Исходный код также находится на Github, но суть такова:

  1. Запишите текущую вкладку в виде URL-адреса данных в формате PNG. Расширения Chrome имеют API для этого chrome.tabs.captureVisibleTab() .
  2. Преобразуйте dataURL в Blob . См. помощник convertDataURIToBlob() .
  3. Отправьте каждый большой двоичный объект (кадр) средству просмотра с помощью двоичного веб-сокета, установив socket.responseType='blob' .

Пример

Вот код, позволяющий сделать снимок экрана текущей вкладки в формате PNG и отправить кадр через веб-сокет:

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

Будущие улучшения

Частота кадров на удивление хороша для этого, но могла бы быть еще лучше. Одним из улучшений было бы устранение накладных расходов на преобразование dataURL в Blob. К сожалению, chrome.tabs.captureVisibleTab() дает нам только URL-адрес данных. Если бы он возвращал Blob или типизированный массив, мы могли бы отправить его напрямую через веб-сокет, а не выполнять преобразование в Blob самостоятельно. Пожалуйста, поставьте звездочку crbug.com/32498 , чтобы это произошло!

Способ 4: WebRTC — истинное будущее

Последний, но тем не менее важный!

Будущее совместного использования экрана в браузере будет реализовано за счет WebRTC . 14 августа 2012 года команда предложила API захвата содержимого вкладок WebRTC для обмена содержимым вкладок:

Пока этот парень не готов, нам остаются методы 1-3.

Заключение

Таким образом, с помощью современных веб-технологий стало возможным совместное использование вкладок браузера!

Но... к этому заявлению следует относиться с недоверием. Несмотря на свою аккуратность, методы, описанные в этой статье, так или иначе не могут обеспечить отличный UX для совместного использования. Все изменится с появлением технологии захвата содержимого вкладок WebRTC, но пока это не станет реальностью, нам останутся плагины для браузера или ограниченные решения, подобные описанным здесь.

Есть еще техники? Оставить комментарий!