Не смотря на то, что технологии WebRTC уже больше 10 лет, сейчас она очень активно развивается. За последний год, использование WebRTC в браузерах выросло в 100 раз, апишка стала IETF-стандартом и научилась делать новые интересные трюки.
Я — Полина Гуртовая, занимаюсь фронтенд-разработкой и изобретаю велосипеды. В статье расскажу об RTC (Real-time communication) и быстрой доставке данных. Если мы говорим об RTC в Web, то мы говорим о WebRTC. Когда я только начинала работать с WebRTC, мне встретилось множество таинственных сокращений: TCP, NAT, SCTP, DTLS, ICE, STUN, TURN, RTP, UDP, AVC…
Хорошие новости: разобраться в них оказалось совсем не сложно. Сейчас мы сделаем это вместе. WebRTC crash course. Поехали!
WebRTC - challenge?
WebRTC приходится удовлетворять весьма противоречивым требованиям. Нужно как можно быстрее пересылать данные — видео, аудио-файлики, скриншары в условиях лагающего интернета, когда злые админы настроили firewall так, что ни один байтик не пролетит! А вокруг ходят зомби… При этом обмен данными происходит между большим количеством участников с разными приоритетами.
Например, есть панельная дискуссия и её модератор. Ему нужен специальный канал, чтобы вовремя передавать слово другому участнику, задавать вопросы из зала и разруливать всякие форс-мажорные обстоятельства. Важно, чтобы участники WebRTC сессии были идеально синхронизированы друг с другом. Здесь есть проблемы — задачи, которые технология пока не может решить. Например, несколько музыкантов скорее всего не смогут сыграть концерт используя WebRTC. Хотя брать музыкальные уроки, скорее всего получится.
Идея WebRTC — делать все быстро и безопасно, но не обязательно надёжно. В самой последней версии спецификации — WebRTC-NV добавился новый челлендж — пририсовать участникам видеоконференции смешные шапочки. И раз это настолько важно, посмотрим, как это осуществить на следующем примере:
Мы хотим стримить видео танцующего котика в шапочке. Чтобы это видео увидели наши друзья нужно:
Взять сырые фреймы камеры и превратить их в классный компактный формат. Этим занимается программа-кодек.
Сжатые стрим поделить на кусочки и как-то доставить нашим друзьям.
Мы хотим, чтобы доставка пошла самым прямым способом (на картинке — зелёная линия).
Здесь есть две истории про encoding и про транспорт. Начнём со второй.
Транспорт: медленно и надёжно или быстро и опасно?
Когда-то были стандартизованы два протокола для передачи данных: TCP и UDP.
TCP-протокол — это то, что использует в качестве транспорта HTTP протокол (версии 1 и 2, но не 3) и WebSockets. Этот классный протокол:
следит за состоянием;
гарантирует правильный порядок доставки;
ничего не теряет.
Здесь нам приходится обменивать скорость на надежность. Для этого храним состояние соединения (именно поэтому TCP header может весить до 60 байт) и поддерживаем Slow-Start. Вы сначала посылаете несколько пакетов, потом больше и больше, до тех пор, пока они не начнут теряться. Если бы TCP не поддерживал Slow-start, интернет бы сломался от слишком большого количества данных :)
Протокол UDP устроен проще чем TCP. Грубо говоря, UDP просто посылает пакеты по нужному адресу и не предоставляет никаких гарантий доставки. Именно поэтому высокоскоростные транспорты часто реализуют поверх UDP протокола WebRTC peer-to-peer over UDP.
В идеальном случае WebRTC тоже использует UDP протокол для установки peer-to-peer соединений. Однако WebRTC может работать поверх TCP и использовать peer-to-peer (вместо этого соединение устанавливается через специальный сервер)
Представим что, есть 2 участника, которые хотят установить WebRTC соединение.
Один на своей стороне создает экземпляр RTCPeerConnection. Он собирает информацию об окружении: какой кодек можно использовать, где находится и что происходит вокруг. Эту информацию надо отправить второй стороне. Но как это сделать? Соединение еще не установлено, вторая сторона неизвестно где. Спецификация WebRTC говорит: «Я не знаю, как вы будете обмениваться начальными данными, но сделайте это, пожалуйста". Так что вы можете использовать почтовых единорогов или голубей, но лучше попробуйте WebSockets. Данные об окружении, которые вам предстоит отправить называются оффер (offer). Вторая сторона, получив его, радостно крафтит у себя свой собственный экземпляр RTC-соединения. И аттачит полученный оффер к этому соединению. Дальше упражнение повторяется — вторая сторона генерирует ответный оффер, прикрепляет его к RTC соединению и отправляет назад.
Если две стороны смогут договориться, то соединение будет установлено, все счастливы. Но, часто этого не происходит, проблема в том, что кто-то из участников сидит за NAT.
NAT - это такой костыль, придуманный в древние времена, чтобы решить проблему недостатка IP-адресов версии 4. Этих IP-адресов очень мало. Можно попробовать посчитать сколько: 4 октета по 8 байт — примерно 4 биллиона уникальных адресов. Проблему решает протокол IPv6 , но не везде он поддерживается.
Вообще, NAT еще обеспечивает приватность и делает всякие хорошие штуки. Когда вы сидите в своей маленькой уютной приватной сети, у вас есть IP-адрес. Он никому не виден, по нему нельзя до вас достучаться.
Что нужно сделать, чтобы до вас достучались извне?
Пройти через этот самый NAT. NAT поддерживает специальную таблицу адресов, в которой хранится соответствие вашего внутреннего IP-адреса и порта внешнему порту. Это соответствие (NAT binding) записывается в таблицу, только когда трафик проходит от вас во внешний мир. При этом вы знаете только свой внутренний адрес, но не знаете публичного.
Информация о нашем внешнем адресе необходима для установки WebRTC - соединения. И тут нам на помощь приходит STUN сервер. Ему вы посылаете пакет, он в ответ присылает ваш публичный IP. Его вы уже передаете второй стороне. Соединение установлено, все счастливы и довольны.
Использовать эту схему получается не всегда. Первый вариант ваши админы очень заботятся о безопасности и поэтому просто вырубили весь UDP трафик. Другой случай — ваш NAT устроен так, что запись в таблице зависит от IP-адреса сервера, которому вы посылаете трафик.
Для таких случаев используется TURN. Вы заворачиваете весь трафик на TURN-сервер, и вся коммуникация идет через него.
Это может быть весьма накладно, TURN-серверу понадобится широкий канал, если будет много людей, желающих установить через него соединение. При установке WebRTC-соединения вы сначала пробуете TURN, потом STUN. Это надежнее, и вы всегда сможете перейти с TURN на STUN, если это будет возможно. Чтобы все это проделать во время установки WebRTC-соединения используется протокол ICE. Его единственная задача — обойти NAT.
При создании WebRTC сессии запускается ICE-агент. Его задача — собрать возможные STUN и TURN сервера, через которые может быть установлено соединение. И затем попробовать установить соединение через подходящую пару кандидатов.
Сбор ICE кандидатов — это долгий процесс. Поэтому используется trickle ICE. Как только ICE-агент находит каких-то кандидатов, он сразу начинает делать проверку. Пока проверка идёт, вы досылаете второй стороне ещё кандидатов, и так до бесконечности.
Если интересно, как это происходит, можете заглянуть в devtools любого браузера.
Расшифровываем картинку: fingerprinting – позволяет шифровать WebRTC соединение. Дальше идет информация о том, что используется trickle ICE и список ICE-кандидатов.
С этим разобрались, давайте теперь поговорим о безопасности.
DTLS: TLS для UDP
Когда мы устанавливаем WebRTC соединение, нужно сделать его максимально безопасным и шифровать весь трафик. Обычно эту задачу решает протокол TLS. Но мы хотим, чтобы WebRTC работал поверх UDP, который не гарантирует порядок доставки. Он также не гарантирует, что все пакеты будут доставлены. Поэтому TLS, который как раз полагается на такие гарантии, мы использовать не можем. Чтобы решить эту проблему, используют лайтовую версию TLS, умеющую работать поверх UDP. Она называется DTLS.
Перед WebRTC-соединением происходит DTLS handshake, а дальше вы засылаете трафик, используя один из двух вариантов:
Media: связка SRTP + SRTCP
Буква S в названии протоколов означает secure, то есть всё шифруется. Протокол RTP используется для отправки media-трафика (видяшек и звука). RTCP — предоставляет вам Quality of Service. Он собирает информацию о вашем соединении: сколько пакетов потеряно, сколько переслали и т.д.
Data: SCTP
Можно использовать WebRTC, чтобы пересылать любые бинарные данные. Например, если есть игра в реальном времени, где прекрасная эльфийка гоняется за орками с мечом, то информацию о том, как она бегает, можно пересылать, используя WebRTC data channels. При этом весь трафик также будет шифроваться. Напомню, что WebRTC data channels работают поверх UDP. Это более легковесный протокол, чем TCP, и скорее всего используя его, вы получите выигрыш по скорости. SCTP протокол можно очень гибко настраивать. Например, вы можете сделать так, чтобы не терять пакеты или мультиплексировать соединение. Возникает вопрос, а чем это лучше Websocket? Ответ: WebSockets работает поверх TCP, поэтому может быть медленнее.
Итого
Чтобы использовать WebRTC из коробки, нужно:
Придумать signaling (например используя WebSocket) чтобы участники могли обменяться офферами;
STUN и/или TURN.
Можно обойтись без STUN и TURN на этапе разработки, но на реальном приложении, они точно понадобятся. Напомню, что STUN — это простая штука: вы присылаете ей трафик, она сообщает IP, а также иногда пингует ваш NAT, чтобы сохранять NAT-binding (запись в табличке). Twilio и Google предоставляют STUN бесплатно. А вот за TURN уже придётся заплатить.
Все остальное: DTLS, SCTP, SRTP, ICE gathering сделает за нас browser.
Optimal encoding: latency vs quality
Теперь настало время поговорить про encoding. Это история про компромисс (trade-off) между скоростью и качеством.
Вся индустрия работает на то, чтобы видео и аудио эффективно сжималось и оставалось качественным. Поэтому крупные и мелкие компании объединяются, чтобы создавать эффективные кодеки. Давайте посмотрим на них поближе.
Под пунктирной линией на изображении — кодеки royalty-free. Используйте их, и все будут счастливы и довольны, а вам не придется за это платить.
В IETF-стандарте для WebRTC прописано, какие кодеки обязательно использовать и поддерживать, чтобы вас все любили. Их всего два:
baseline profile H264/AVC.
Проблема этого кодека в том, что его не всегда можно использовать бесплатно (non-royalty-free).
VP8 — классный (хоть уже немного устаревший) кодек от Google.
Если посмотреть на таймлайн на картинке выше, то видно, что оба этих кодека технологии примерно 2007 года. Индустрия уже давно шагнула вперед и появились более эффективные кодеки. Один из них — это AV1, о котором расскажу позже.
Теперь давайте посмотрим, как работают кодеки.
Compression: intra & inter
Допустим, вам нужно эффективно сжать видео с танцующей котейкой.
Чтобы сжать это видео, вам нужно сделать две вещи эффективно:
сжать каждый фрейм видео (картинка с пикселями) (intraframe compression);
сжать последовательность фреймов эффективно (interframe compression).
Intraframe: вы берете изображение, режете его на кусочки и сжимаете. Ключевая идея для эффективной intraframe-компрессии использовать пространственную корреляцию изображения (близкие пиксели скорее всего как-то связаны друг с другом). Для эффективной interframe компрессии нужно учитывать временную корреляцию. Если есть два фрейма, то с высокой степенью вероятности, они похожи друг на друга. Вам не нужно кодить каждый отдельно, как это делается в гифках. Достаточно закодить один ключевой фрейм, а остальные сделать зависимыми от него.
Зависимые фреймы содержат в себе motion vectors. Это векторы, которые показывают куда блоки нашего фрейма сдвинулись относительно ключевого. Так работали все кодеки в 2001 работали, в 2021 общие принципы работы не поменялись :) Но есть нюанс.
Intraframe compression (AV1)
Современные видеокодеки стали намного умнее. Появились новые более хитрые способы разбивать картинку на блоки и использовать пространственную корреляцию. Например, в AV1, есть 10 миллионов способов закодировать один блок — представьте себе вычислительную сложность этой истории!
Какие кодеки поддерживает ваш браузер?
Подсказать ответ на этот вопрос может простая команда в консоли, не нужно даже устанавливать WebRTC соединение.
Для случая на картинке, у вас будет поддерживаться H264, VP8, VP9 и AV1AV1. Навскидку примерно 90% трафика в интернете — это видео. Не удивительно, что все хотят его эффективно сжимать. Уже давно большие и маленькие компании трудятся над созданием своих кодеков. У Cisco был Thor, у Google серия кодеков VP, у Mozilla — Daala. В один момент они объединились в организацию — Alliance for Open Media и создали видеокодек AV1 на основе своих предыдущих наработок.
Что ещё можно сделать, чтобы пересылать видео эффективно?
Simulcast – несколько потоков разного качества
Simulcast — вы берёте видеопоток и вместо одного посылаете несколько с разными качествами. Решать какой поток куда посылать должен специальный сервер. О нем я расскажу позднее. Есть еще одна похожая техника: SVC.
SVC: как Simulcast, только лучше
Scalable Video Coding — это технология, которая поддерживается на уровне кодека и работает примерно следующим образом.
Видео нарезается на слои. Можно дропнуть верхние и оставить нижние. При этом на нижних слоях будет картинка и звук качеством похуже, а если добавить верхние, то получше.
Отбрасывая верхние слои, мы получаем видео и картинку качеством пониже.
Есть два типа SVC. В первом случае вы уменьшаете качество фреймов, во втором их количество.
Экзотические кодеки и всякий ML
Давайте обсудим еще два интересных кодека: Lyra от Google и Satin от Microsoft. Они вышли примерно в одно время, и идея в них очень прикольная. Чтобы хорошо передавать речь или музыку используются генеративные ML-модели. Вы передаете голос с низким качеством и битрейтом. Декодер на стороне принимающего запускает свою ML-магию и генерирует красивый, чистый голос.
Мы разобрались с кодеками. Остался вопрос: что должно произойти, если у нас есть несколько участников WebRTC-сессии? Как их соединить вместе?
Для этого рассмотрим несколько архитектур для WebRTC коммуникации:
Fully connected — соединить каждого участника с каждым.
Проблема в том, что количество соединения растет квадратично по отношению к количеству участников. Уже для 10-ти участников вам понадобится 45 соединений.
MCU – Multipoint Control Unit
Это популярный вариант древних времён. Каждый участник WebRTC соединения берёт свои видео и аудио, потом засылает их в одно определённое место. Здесь осуществляется транскодинг видеопотока, что-то смешивается, парсятся байтики и результат рассылается всем остальным.
MCU достаточно дорогое удовольствие, поэтому сейчас популярен более легковесный аналог.
SFU – Selective Forwarding Unit
Тут всё примерно то же самое, но SFU может:
Взять пакет и размножить на несколько участников.
Роутить пакеты от одного участника к другому;
Разруливать штуки, связанные с SVC, например, дропнуть некоторое количество ненужных слоев.
Но SFU не занимается транскодингом. Можно рассматривать эту архитектуру, как легковесный роутер пакетов.
Тут есть интересный вопрос, связанный с безопасностью. Кто в такой модели должен знать ваши секретные ключи, чтобы расшифровать трафик?
То же самое + 1000 зрителей
Рассмотрим полуискусственную историю из жизни. Представим, что есть решение на основе SFU для 5-ти участников сессии, всё работает хорошо. К вам приходит клиент и говорит, что они забыли один маленький нюанс — им нужно сделать так, чтобы эту штуку можно было стримить на 1000 зрителей.
Здесь можно придумать миллион решений, но пока давайте посмотрим как это устроено на клиенте.
У каждого участника сессии есть свой видосик (html видео), из него можно вытащить трек, который вы пересылаете по WebRTC.
Сложности начинаются когда меняется layout странички с участниками. Например, когда кто-то говорит или выключает микрофон. Как правильно менять layout выполнить для тысячи зрителей? Есть достаточно веселое решение.
Создаёте фейкового пользователя, которого никто не видит. Для этого пользователя вы создаете отдельный layout и меняете его так как это должны были видеть тысяча пользователей. Дальше вы захватываете экран этого пользователя и засылаете его в стриминговый сервис, который уже рассылает ваш стрим на 1000 зрителей.
Как скрафтить такого фейкового пользователя?
Для этого используем все самые любимые технологии:
берём браузер Firefox (можно Chrome, но Firefox веселее);
пакуем Firefox в Docker;
дальше экран Firefox стримим в X-framebuffer;
заводим PulseAudio — это всё в Docker-контейнере;
FFmpeg это подхватывает, получается микро транскодер;
дальше можно забрасывать это в стриминговый сервис.
Готовые решения
Если вы будете использовать WebRTC, то советую взять какое-нибудь готовое решение. Их очень много.
Jitsi, Kurento или более низкоуровневые штуки, типа mediaSoup.
Есть платформы, которые предоставляют сразу всё — STUN, TURN и архитектуру. Одна из них — Agora. У неё очень интересная история. Чтобы передавать данные максимально быстро, они поддерживают свою собственную сеть, которая состоит из SFU-роутеров. Там работает специальный ML-механизм, который рассчитывает самый эффективный маршрут для WebRTC трафика, например, из России в Новую Зеландию.
Теперь покажу, как выглядит отладка WebRTC системы.
Отладка
В Chrome:
В Firefox:
Ничего непонятно, но после часа чтения документации обещаю, что станет полегче:) Чего-то не хватает…
Давайте вспомним с чего мы начинали.
Внимательно присмотревшись, вы можете обнаружить нарушение SRP (принцип единой ответственности). В WebRTC смешана история про encoding и про транспорт. Хорошо бы разделить эти две части. Как это сделать? Есть три технологии.
WebCodecs
Он есть в последней версии Chrome (на 2021 год последней). Вы посылаете в WebRTC видео, которое захватывает камера и как-то его энкодите. Нельзя программно влезть в этот процесс. WebCodecs всё меняет, он позволяет взять сырые фреймы видео и что-нибудь с ними сделать. У вас есть доступ к ним.
Когда не было WebCodecs, для этого использовались обычные решения, основанные на WebAssembly. Брали кодек, написанный на C или на Rust, компилили это, запихивали в браузер и всё работало. WebCodecs предоставляет нативное решение, поэтому всё должно быть быстрее, эффективней и надёжней.
WebTransport
Эта технология позволяет послать датаграммы по протоколу HTTP/3 QUIC.
Insertable-streams
С этой штуковиной можно влезть внутрь трека. Его можно изменить, трансформировать и переслать получившийся результат.
Вот пример кода (предположу, что неработающего), но тут неплохо видно, как это устроено.
Вы создаёте трек-процессор, который возьмёт трек и превратит его в набор фреймов. Трек-генератор, набор фреймов превратит обратно в трек. Функция transformer берёт фрейм и превращает его в новый фрейм. Чтобы все это проиграть, нужен контроллер.
Получился следующий пайплайн — вы взяли трек, достали из него фреймы, запроцессили и отправили назад. Разобравшись со всем этим, настало время написать немного JS-кода для рисования смешных шапочек. Ведь мы здесь для этого, правда?
Как добавить смешную шапочку к вашему WebRTC-видео?
Первое, что нужно — это моделька, которая будет детектировать, куда прицеплять шапочку. Решений много вот парочка:
Использовать модель, конвертированную в формат TensorFlowJS;
Сконвертировать свою модель в формат onnxjs.
Загрузив модель в браузер, нам нужно закидывать в нее фреймы видео, а она на выходе будет выдавать нам координату внутри по фрейму для прицепления шапочки.
Рисуем фрейм и прицепленную шапочку на Canvas. Затем берем конструктор видео-фрейма и передаем ему нашу Canvas. Это не самый эффективный способ, но мы же тут экспериментируем.
Новый фрейм готов, он отправляется в новый поток, а старый фрейм мы закрываем, чтобы его собрал garbage collector.
Через неделю, 24 и 25 октября 2022 года в Москве в Start Hub начнется FrontendConf. Как всегда будет много всего интересного! Настоящее живое общение с ведущими экспертами, более 40 лучших докладчиков в своей сфере, круглые столы и нетворкинг. Погружение в самые современные подходы и лучшие практики, разбор наиболее актуальных технологий. Вы еще успеваете познакомиться с программой, подогнать расписание и заказать билеты на официальном сайте конференции