Хабрахабр

[Перевод] Введение в Screen Capture API — Сканируем QR коды в браузере

Введение

Этот API появился на свет в 2014 году и новым его назвать сложно, однако поддержка браузерами все еще достаточно слабая. В этой статье мы, как вы уже догадались, поговорим про Screen Capture API. Тем не менее, его вполне можно использовать для персональных проектов или там где эта поддержка не так важна.

Немного ссылок для начала:

На случай если ссылка с демо отвалится (или если вам лень туда перейти) — вот так выглядит готовое демо:

Приступим.

Мотивация

И хотя они обычно удобны для передачи сложных данных или длинных ссылок в реальном мире, где на них можно направить телефон, на десктопе все несколько сложнее. Недавно у меня возникла идея веб-приложения, которое использует в своей работе QR-коды. Неудобно. Если QR код находится на экране того же устройства, на котором его нужно прочитать — нужно возиться с сервисами для распознавания, или распознать с телефона и переправить данные обратно на ПК.

При необходимости настроить аккаунт из QR-кода они открывают полупрозрачное окно, которое можно перетащить поверх изображения с кодом и он автоматически распознается. Некоторые продукты, такие как, например, 1Password — включают в себя интересное решение для этой ситуации. Вот как это выглядит:

Но, наверное, в браузере так не выйдет... Было бы идеально, если бы мы могли реализовать что-то подобное для нашего приложения.

Здесь нам и поможет Screen Capture API с его единственным методом getDisplayMedia. Ну почти. К сожалению, поддержка браузерами, как уже было сказано выше, далеко не такая распространенная как у доступа к камере. getDisplayMedia — это как getUserMedia, только для экрана устройства, вместо его камеры. Согласно MDN использовать его можно в Firefox, Chrome, Edge (правда там оно находится в неправильном месте — сразу в navigator, а не в navigator.mediaDevices) + Edge Mobile и… Opera for Android.

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

Оно работает также как и getUserMedia, но позволяет захватывать видеопоток с одной из определенных display surface: Само по себе API крайне простое.

  • c монитора (экран целиком)
  • с окна или всех окон определенного приложения
  • с браузера, или точнее с конкретного документа. В Chrome этим документом является отдельная вкладка, а в FF такая опция отсутствует.

Можно захватывать картинку из других окон и, например, в реальном времени распознавать и переводить текст, как Google Translate Camera. Браузерное API, которое позволяет заглянуть за пределы браузера… Звучит знакомо и обычно сулит одни неприятности, но в данном случае может быть достаточно удобно. Ну и, наверное, есть еще много интересных применений.

Собираем

Что дальше? Итак, с возможностями, которые нам дает API, разобрались.

Для этого мы используем элементы <video>, <canvas> и еще немного JS. А дальше нам нужно перегнать этот видеопоток в изображения, над которыми можно работать.

Крупным планом процесс выглядит примерно так:

  • Направить поток в <video>
  • С определенной частотой рисовать содержимое <video> в <canvas>
  • Забирать объект ImageData из <canvas> используя метод 2D контекста getImageData

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

Опуская все не относящееся к делу, чтобы запустить поток и вытащить из него фрейм нам понадобится примерно следующий код:

async function run() , audio: false } video.srcObject = await navigator.mediaDevices.getDisplayMedia(displayMediaOptions); const videoTrack = video.srcObject.getVideoTracks()[0]; const { height, width } = videoTrack.getSettings(); context.drawImage(video, 0, 0, width, height); return context.getImageData(0, 0, width, height);
} await run();

Как и говорилось выше — сначала мы создаем элементы <video> и <canvas>, и просим у канваса 2D контекст (CanvasRenderingContext2D).

В отличие от потоков с камеры, тут их немного. Затем мы определяем ограничения/условия потока. Хотя на момент написания этой статьи захват аудио все равно никем не поддерживается. Мы говорим, что не хотим видеть курсор, и что нам не нужно аудио.

Обратите внимание, что getDisplayMedia возвращает Promise. После этого мы цепляем полученный поток типа MediaStream к элементу <video>.

Наконец, из полученных данных о потоке мы запоминаем разрешение видео, чтобы правильно отрисовать его на канвас, отрисовываем фрейм и вытаскиваем из канваса объект ImageData.

Например, пока вы ждете что необходимое изображение появится в кадре. Для полноценного использования вам, скорее всего, захочется обрабатывать кадры в цикле, нежели один раз. И здесь нужно сказать пару слов.

Однако в нашем случае использовать его не выйдет. Когда речь заходит об "обработке чего-то в DOM в постоянном цикле" первое что приходит на ум — это, скорее всего, requestAnimationFrame. В нашем случае, зачастую именно в это время мы будем хотеть обрабатывать изображения. Все дело в том, что когда вкладка перестает быть активной — браузеры ставят обработку циклов rAF на паузу.

Но и с ним не так все гладко. В связи с этим вместо rAF мы будем использовать старый добрый setInterval. Тем не менее, нам этого достаточно. В неактивной вкладке интервал между срабатываниями коллбэка составляет минимум 1 секунду.

Для целей этого демо мы будем использовать библиотеку jsQR. Наконец, добравшись до кадров мы можем обрабатывать их как нам заблагорассудится. Если в полученном изображении есть QR-код — назад вы получите JS-объект с распознанными данными.
Давайте дополним наш предыдущий пример еще буквально парой строк кода: Она крайне проста: на вход принимает ImageData, ширину и высоту изображения.

const imageData = await run();
const code = jsQR(imageData.data, streamWidth, streamHeight);

Готово!

NPM

Библиотека очень проста, на данном этапе она всего лишь принимает коллбэк, на который будет отправляться ImageData и один дополнительный параметр — частоту отправки данных. Я подумал, что основной код за этим примером можно упаковать в npm-библиотеку и сэкономить в последующем использовании немного времени на первоначальную настройку. Я подумаю, есть ли смысл расширять ее функционал. Весь процессинг нужно приносить свой.

Библиотека называется stream-display: NPM | Github.

Ее использование сводится буквально к трем строчкам кода и коллбэку:

const callback = imageData => {...} // do whatever with those images
const capture = new StreamDisplay(callback); // specify where the ImageData will go
await capture.startCapture(); // when ready
capture.stopCapture(); // when done

Также есть CodePen версия для быстрых экспериментов. Демо можно посмотреть здесь. Оба примера используют упомянутый выше NPM-пакет.

Немного о тестировании

Совершенно не хотелось тянуть 50МБ безголового Chrome чтобы запускать в нем несколько маленьких тестов. Упаковывая этот код в библиотеку мне пришлось задуматься о том, как же ее тестировать. Вот что в итоге пришлось имитировать: И хотя идея писать заглушки для всех составных частей казалась слишком мучительной, в итоге я так и поступил.
В качестве тест-раннера был выбран tape.

  • объект document и DOM-элементы. Для этого я взял jsdom
  • некоторые методы jsdom, у которых отсутствует имплементация — HTMLMediaElement#play, HTMLCanvasElement#getContext and navigator.mediaDevices#getDisplayMedia
  • Время. Для этого я воспользовался useFakeTimers библиотеки sinon, которая под капотом зовет lolex. Она устанавливает свои замены setInterval, requestAnimationFrame и многим другим функциям, работающим со временем, а также позволяет управлять течением этого фальшивого времени. Но будьте осторожны: jsdom в одном месте своего процесса инициализации использует течение времени и если сначала включить sinon — все зависнет.

Остальное было реализовано пустыми JS-функциями. Также я использовал sinon для всех заглушек функций, за вызовами которых нужно было следить.

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

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

Заключение

Остается только надеяться, что когда браузеры научатся видеть сквозь свои окна — эти возможности будут строго подконтрольны нам. Решение получилось не столь элегантным, как упомянутое в начале статьи прозрачное окно, но, возможно, однажды веб дойдет и до этого. Так что не шарьте больше, чем нужно! А пока что помните, что когда вы шарите экран в Chrome — его могут парсить, записывать, и т.д.

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

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть