Как эффективно управлять видеопотоком с веб-камеры в браузере

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Веб‑технологии, такие как Media Capture and Streams API (или просто MediaStream API), открывают большие возможности для работы с видеопотоком в браузере. Они позволяют легко захватывать видеопоток с веб‑камеры и использовать его для создания мощных и интерактивных веб‑приложений. Однако несмотря на широкую доступность этих API их эффективное использование остаётся непростой задачей.

Меня зовут Артем Шовкин, я RnD‑разработчик в СберТехе. В процессе изучения MediaStream API наша команда столкнулась с рядом интересных вопросов. Как эффективно управлять параметрами видеопотока в зависимости от возможностей устройства и сети? Какие подводные камни возникают при кроссбраузерной реализации? Как лучше всего обрабатывать ошибки при работе с видеопотоком?

Мы решили не просто разобраться в работе API, но и в деталях изучить спецификацию Media Capture and Streams, чтобы понять, как она используется в реальных приложениях. В статье мы также использовали код исходников реализации getUserMedia.

Материал пригодится разработчикам, которые встраивают в свои решения работу с камерой, особенно полезен фронтендерам, которые столкнулись с задачами по созданию функциональности работы с веб‑камерой и которым нужно добиться качества видео не хуже, чем в нативных приложениях.

Поехали!

Как видеоконтент появляется в браузере

Когда разработчики слышат про видеопоток в браузере, то часто в первую очередь думают о WebRTC. Но на самом деле спецификация, которая описывает инициализацию медиапотоков, — это Media Capture and Streams. Безусловно, как подмечено на ресурсе MDN, эти спецификации взаимосвязаны и активно друг на друга ссылаются, но реализуются обособленно. Основная их связь в том, что WebRTC — потребитель (или, в терминах спецификации, consumer) исходящих данных MediaStream.

Разберёмся, откуда вообще может появиться видеоконтент в браузере. Источника три:

  • получение при peer‑to‑peer (непосредственная передача данных между браузерами без прослоек в виде сервера);

  • видеофайл (например, в формате mp4 или webm);

  • веб‑камера.

Конечно, есть ещё протоколы HLS (используется в Twitch) и DASH (в YouTube), но это технологии загрузки видео пакетами, а не потоками. Мы де поговорим о получении видео с веб‑камеры.

Для получения видеопотока с устройства основополагающей является эта строчка кода:

await navigator.mediaDevices.getUserMedia({ video: true })

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

Описание того, как должен работать getUserMedia, в спецификации объёмное, с множеством ссылок на различные понятия. Суммарно оно состоит из 1507 слов (без референсов), а это четыре страницы текста. К примеру, описание работы стрелочных функций в EcmaScript умещается в 501 слово.

Описание работы getUserMedia сводится к трём основным аспектам, которые напрямую касаются практического применения:

  1. причины реджекта промиса getUserMedia;

  2. использование сonstraints;

  3. алгоритм SelectSettings.

Про них и поговорим далее.

Причины реджекта промиса getUserMedia

Разберём, почему getUserMedia может возвращать ошибку.

  1. Вызов getUserMedia без аргумента. Если запросить доступ к медиаустройствам пользователя (например, веб‑камере и микрофону) через API getUserMedia, но не передать никаких аргументов, то сразу получим reject:

    await navigator.mediaDevices.getUserMedia()

    Обязательно необходимо передавать объект, который включает запрашиваемые медиатипы. Возможны три варианта:

    • видео;

    • аудио;

    • и видео, и аудио.

    Если не передать аргументы хотя бы с одним из медиатипов, то getUserMedia вернёт ошибку.

  1. Not fully active document. Второй случай реджекта — ситуация, когда документ не fully active (не полностью активен). Вот как это состояние описывается согласно спецификации HTML:

    7.3.3 Fully active documents

    A Document d is said to be fully active when d is the active document of a navigable navigable, and either navigable is a top‑level traversable or navigable's container document is fully active.

    Взгляните на эти прекрасные формулировки: a navigable navigable, and either navigable. Перевести можно примерно так: «Документ d считается fully active, если d является активным документом навигатора navigable, и либо navigable является обходчиком верхнего уровня, либо контейнер документа navigable полностью активен». А означает это лишь одно: вкладка должна быть активной. Если вкладка неактивна, а видеопоток не в активном iframe, мы получаем ошибку «DOMException: InvalidStateError».

  1. Нет разрешения пользователя. Третий случай достаточно банальный: пользователь не дал разрешение на использование видеокамер. Тут есть важный момент: если взаимодействуете с периферийными устройствами в браузере, не забывайте работать в защищённом контексте, то есть под HTTPS. Иначе будете получать ошибку.

Что такое constraints и зачем они нужны

Пойдём немного глубже и поговорим о настройке видеопотока. Если вызывать видеопоток стандартно, без каких‑либо настроек: await navigator.mediaDevices.getUserMedia({ video: true }), то мы получим разрешение 640×480. Это базовое разрешение, рекомендованное спецификацией передачи по WebRTC. Если его растянуть на весь экран, изображение будет зернистым.

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

await navigator.mediaDevice.getUserMedia({ 
    video: {
        width:  1920,
        height: 1080,
        facingMode: 'environment'
    }
})

В примере выше словом constraint обозначаются все параметры — width, height, facingMode. А всё вместе в терминах спецификации — MediaTrackConstraintSet.

Каждый constraint может представлять собой:

  • диапазон значений, который настраивается полями со значениями min и max: width: { min: 1280, max: 1920 }

  • конкретное значение: height: 1080

  • значение из определённого списка enum: facingMode: 'environment'

И тут возникает вопрос: если у нас есть диапазон, то как браузер выбирает конкретное значение? В таком случае он может взять ширину и 1280, и 1440, и 1920 по верхней границе. Для решения такой задачи в спецификации предусмотрен алгоритм SelectSettings.

Описание SelectSettings

Как работает этот алгоритм? Для примера запросим в настройках ширину из диапазона 1280 на 1920. Высоту тоже установим не конкретную, а ограничим сверху: 1080.

await navigator.mediaDevice.getUserMedia({ 
    video: {
        width:  { min: 1280, max: 1920 },
        height: { max: 1080 },
        facingMode: 'environment'
    }
})

Согласно алгоритму SelectSettings, происходит следующее. На основе переданных constraints формируются все возможные настройки, которые будут удовлетворять условиям и не противоречить системным возможностям. Каждый набор таких настроек называется кандидатом, все вместе они представляют набор кандидатов.

Для примера напишем три кандидата (в реальности их получается гораздо больше):

const candidat_1 = {
    width:  1280,
    height: 480
}
const candidat_2 = {
    width:  1280,
    height: 720
}
const candidat_3 = {
    width:  1920,
    height: 1080
}

У браузера также есть настройки по умолчанию. Как правило, следующие:

const defaultVideoSettings = {
    width:  640,
    height: 480,
    aspectRatio: 1.3333333,
    resizeMode: 'none',
    echoCancelation: true
}

Такие настройки рекомендует спецификация как самые подходящие для WebRTC‑соединения.

Далее начинается самое интересное. Браузер рассчитывает fitness distance от настроек по умолчанию до каждого кандидата. Я бы перевёл это как «расстояние соответствия». Для целочисленных constraints оно считается по формуле:

( actual == ideal ) ? 0: | actual — ideal | / max( | actual |, | ideal | )

За actual берётся значение из кандидата, а за ideal — значение из настроек по умолчанию.

До нашего candidat_2 расстояние по ширине — 0, так как у нас диапазон значений ограничен минимальным значением — 1280. По высоте у нас будет расстояние (720 — 480) / 720 = 0.33.

По constraint aspectRatio также будет считаться расстояние по этой формуле, хоть параметр явно и не указан в кандидате. Браузер может установить его значение нехитрым способом: делит ширину и высоту. Таким образом, aspectRatio для candidat_2 равен 1.77, а fitness distance до этого значения будет вычислен следующим образом: (1,77 — 1,33) / 1,77 = 0,25.

Для constraints, значения которых выбираются из enum (все варианты перечисления определены спецификаций), fitness distance считается следующим образом:

( actual == ideal ) ? 0: 1

Constraint resizeMode для candidat_2 также определяется браузером в значении none. Таким образом, расстояние до него равно нулю.

А вот значение echoCancelation браузер не может подсчитать явно, поэтому если такого constraint в кандидате нет, то до него берётся расстояние 0.

Таким образом, по каждому constraint считается расстояние, а затем значения складываются. Аналогично вычисляется fitness distance до каждого кандидата, для candidat_2 он равен 0,58. До candidat_3 fitness distance равен 1.13:

const defaultVideoSettings = {
    width:  1280,            /* 0.33 до 1920 */
    height: 480,             /* 0.55 до 1080 */
    aspectRatio: 1.3333333,  /* 0.25 до 1.77 */
    resizeMode: 'none',      /* 0 */
    echoCancelation: true    /* 0 */
}

Что же с первым кандидатом? 

const defaultVideoSettings = {
    width:  1280,            /* 0   до 1280 */
    height: 480,             /* 0   до 480  */
    aspectRatio: 1.3333333,  /* 0.5 до 2.66 */
    resizeMode: 'none',      /* 0 */
    echoCancelation: true    /* 0 */
}

На первый взгляд — 0,5. Чтобы разобраться, действительно ли это так, запросим точное разрешение 1280×480 у браузера и получим следующую картинку:

Давайте сравним с нашими целевыми 1920x1080:

Как видите, чтобы браузер смог получить изображение 1280х480, ему приходится обрезать видеопоток снизу и сверху. А это значит, что для первого кандидата браузер выставляет constraint resizeMode в значении crop-and-scale:

const candidat_1 = {
    width:  1280,
    height: 480,
    resizeMode: 'crop-and-scale'
}

И получается, что fitness distance рассчитывается уже со следующими значениями:

const defaultVideoSettings = {
    width:  1280,            /* 0   до 1280 */
    height: 480,             /* 0   до 480  */
    aspectRatio: 1.3333333,  /* 0.5 до 2.66 */
    resizeMode: 'none',      /* 1   до crop-and-scale */
    echoCancelation: true    /* 0 */
}

Итоговое расстояние — 1,5! Резюмируя подсчёты: до первого кандидата расстояние — 1,5; до второго — 0,58; до третьего — 1,13.

Далее браузер выбирает кандидата с наименьшим fitness distance, в нашем случае candidat_2, и возвращает поток с настройками из этого кандидата. То есть при вызове getUserMedia с такими constraints:

await navigator.mediaDevice.getUserMedia({ 
    video: {
        width:  { min: 1280, max: 1920 },
        height: { max: 1080 },
        facingMode: 'environment'
    }
})

вернётся видеопоток со сторонами 1280x720.

Но и тут есть нюанс. Такие значения вернутся в Chrome, так как значение настроек по умолчанию он меняет по нижней границе указанного диапазона. А Safari, напротив, будет брать верхние. То есть в формуле

 ( actual == ideal ) ? 0 : | actual - ideal | / max( | actual |, | ideal | )

в значение ideal будет подставляться 1920. Следовательно, в Safari побеждает кандидат 3, и getUserMedia вернёт разрешение 1920×1080.

Подведём итоги

Мы рассмотрели с точки зрения спецификации, как браузеры возвращают видеопоток, что такое SelectSettings и как этот параметр помогает определиться с результирующим изображением, основывая свои вычисления на переданных constraints.

Напоследок важный момент: не забывайте про нюансы реализации спецификаций разными браузерами и всегда проверяйте свои проекты на всевозможных устройствах.

Надеюсь, материал был для вас полезным. Спасибо за внимание!

Источник: https://habr.com/ru/companies/sberbank/articles/868134/


Интересные статьи

Интересные статьи

1С-Битрикс официально присвоил Digital HR компания «Улей» максимальный уровень компетенции «Крупные корпоративные внедрения». Компетенция подтверждает профессиональные навыки компании в разработк...
После очередного обновления коробочного портала Битрикс 24 в марте 2023г, в бизнес процессах, перестала работать модификация типов при присвоении значения строковым переменным.Например, при присвоении...
В этой статье я хочу рассказать о новой, перспективной модели тактических наушников, выводящей позиционирование звука на новый уровень.
Для каких целей? На момент возникновения идеи было еще не совсем понятно. Но предпосылки развития идеи были
Приглашаем вас на вебинар, на котором Михаил Божнев, менеджер департамента информационной безопасности СУЭК, расскажет об опыте внедрения в компании решений Quest для аудита и отчетности по событиям и...