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

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

Запись контента вкладки

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

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

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

Способ 1. MutationObserver + WebSocket

Один из способов дублирования вкладки был продемонстрирован Рафаэлем Вайнштейном (Rafael Weinstein) ранее в этом году. В его методе используются MutationObserver и WebSocket.

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

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

Способ 2. Blob из HTMLDocument + двоичный WebSocket

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

  1. Перепишите все URL на странице, чтобы они были абсолютными. Это позволяет избежать неработающих ссылок в статических изображениях и объектах CSS.
  2. Клонирование элемента документа страницы: document.documentElement.cloneNode(true);
  3. Сделайте копию доступной только для чтения, запретите выделение и прокрутку с помощью CSS 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 и URL без протокола в абсолютные. Это необходимо, чтобы изображения, таблицы стилей, шрифты и скрипты не ломались при просмотре в контексте URL blob (например, из другого источника).

Последнее, что я сделал, – добавил поддержку прокрутки. Когда докладчик прокручивает страницу, зритель должен следовать за ним. Для этого я сохраняю текущие позиции 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);
    });

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

Демоверсии

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

Возможности для улучшения

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

Способ 3. API расширения Chrome + Binary WebSocket

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

Исходный код этого примера также доступен на GitHub. Вот его суть:

  1. Захватывает текущую вкладку в виде dataURL в формате PNG. Для этого у расширений Chrome есть API chrome.tabs.captureVisibleTab().
  2. Преобразуйте dataURL в Blob. См. вспомогательный класс convertDataURIToBlob().
  3. Отправьте каждый объект Blob (кадр) в средство просмотра, используя двоичный веб-сокет, задав 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 или Typed Array, мы могли бы отправлять их напрямую через веб-сокет, а не преобразовывать в Blob самостоятельно. Чтобы это произошло, поставьте звездочку на странице crbug.com/32498.

Способ 4. WebRTC – будущее уже здесь

И последнее, но не менее важное.

В будущем демонстрация экрана в браузере будет осуществляться с помощью WebRTC. 14 августа 2012 года команда предложила API WebRTC Tab Content Capture для демонстрации содержимого вкладок.

Пока он не готов, мы можем использовать только методы 1–3.

Заключение

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

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

Знаете другие способы? Оставьте комментарий