Всем привет! Давно я ничего не писал на Хабр про WebRTC. Наверное как‑то не было повода, да и WebRTC давно понятен и прост в общих чертах. Пара строк кода с одной стороны, пара строк кода с другой — вот и готово. Наверное. Там дальше есть несколько тонкостей. На самом деле — целое море тонкостей и обстоятельств, которые надо понимать и уметь с ними работать, но такая уж наша инженерная доля — криво неидеально читать и писать стандарты.
Но сегодня я хочу рассказать не столько про WebRTC как таковой, сколько про велосипеды в его использовании в продакшн‑среде и о том, как тихо без помпы растёт новый стандарт для его сигналинга. На написание этой серии статей меня натолкнула активность Sean Dubois, создателя и мейнтейнера Pion — отличной WebRTC‑библиотеки для Golang.
Для тех, кто совсем не знает, что такое WebRTC, я хочу коротко пояснить — это протокол удалённой передачи аудио и видео в реальном времени, сконструированный специально, чтобы работать в ненадёжных сетях без специального оборудования или ПО, прямо из браузера. Если бы не он, человечество не смогло бы так хорошо пережить пандемию COVID-19 пять лет назад. Он помог быстро и массово проводить 8–9 часов созвонов в день из дома, при этом не разрушив интернет. Именно в годы пандемии WebRTC получил максимальное развитие.
Этот цикл на несколько статей про стандарты WHEP/WHIP. Сегодня я вам расскажу о том, как они работают и зачем они нужны, а дальше будет несколько статей о том, как ими можно заменить старые‑добрые протоколы вроде RTMP, RTSP, SIP в их привычном применении. Меня как фронтенд‑разработчика в прошлом немного напрягает, когда есть какой‑то новый стандарт или фреймворк, а люди вокруг используют старые для своих новых проектов. Да и не то чтобы вы найдёте много материалов на русском, а иногда даже на английском языке по теме. Так что, надеюсь, будет интересно.
Из чего состоит WebRTC-сессия
Давайте не будем начинать с основ. На Хабре можно найти статьи о том, как сделать любое WebRTC‑решение: от простой 1–1-звонилки до конференций и сложных промышленных решений. В них описано много про работу с самим WebRTC: шумоподавление, работа с нестабильным интернетом и прочее интересное. Но в основном авторы концентрируются на самих аудио и видео, не очень много посвящая внимания архитектуре решения целиком, — именно про это стандарт WHIP, который мы обсудим сегодня.
В WebRTC, чтобы соединить двух пользователей, нужно обменяться несколькими текстовыми файлами, потом ещё небольшим количеством сообщений — и вот у вас уже есть звук и видео, а иногда ещё и данные. Давайте дальше называть такой звонок сессией. Эту сессию можно архитектурно разделить на две составляющие: обмен метаинформацией для установления и поддержания соединения и обмен медиаданными с полезной нагрузкой. Для простоты назовём их сигнальным потоком и медиапотоком.
В этой статье мы будем только упоминать медиапоток в разрезе его особенностей. Он очень хорошо описан в разных RFC, и наверное столько материала у нас есть из‑за его сложности. Там бегает огромное количество бинарных данных, нужны классные и сложные алгоритмы компенсации потерь, механизмы подстройки под канал — медиапоток олицетворяет всё то, что инженеры так любят и ценят в своей работе. Для упрощения, в этой статье можно считать его абсолютно самодостаточным и самонастраивающимся. Допустим, за него отвечает идеальный медиасервер.
У медиапотока в WebRTC есть несколько особенностей, которые стоит знать: в основном он работает по UDP, умеет восстанавливать передачу без вмешательства разработчика приложения и умеет самостоятельно подстраиваться под канал. Не нужно запоминать эти особенности прямо сейчас, я буду рад вам напомнить о них, когда придёт время.
Вроде бы все вводные про WebRTC я вам дал, давайте про архитектуру сигнального потока.

Что мы имеем:
Нам нужно передать первичные SDP между двумя участниками.
Соединение должно легко устанавливаться, проходя через NAT.
Нам нужно передавать сигнальные данные в процессе, чтобы поддерживать соединение.
В общем‑то это самый минимум того, что нужно, чтобы соединить участников друг с другом или с сервером.
Если вы хотя бы пару лет в веб‑разработке, то вы сразу представили себе, как WebSocket полностью удовлетворяет все потребности для сигнального потока. Наверное, вы будете правы, но как всегда есть НО. Нет никакого стандарта, как именно и в каком формате будут передаваться все нужные сообщения. В результате мы имеем очень хорошую стратегию для маркетинга, но ад для инженеров — нельзя просто так взять и позвонить из одного решения для видеоконференций в другой. И проблема кроется не в совместимости железа или медиапотока, а в несовместимости того, как именно передаются те или иные стандартные данные в сигнальном потоке.

Чтобы сделать разные сервисы совместимыми друг с другом, сейчас нужно делать адаптеры. Как один из примеров такого адаптера можно взять go2rtc. Это обычно сложные и крутые решения, но мы, инженеры, знаем только одно решение для таких проблем — создать новый стандарт.
Один из ключевых инженеров в мире WebRTC Sergio Garcia Murillo начал разрабатывать два стандарта: WHIP и WHEP. Изначально они позиционировались скорее для замены RTMP и как унифицированный стандарт для плееров на основе WebRTC. WHIP — для того чтобы отправлять сигнал, а WHEP — для того чтобы принимать сигнал. Для каждого завели свои пропоузалы. До полноценного стандарта дошёл только WHIP, и на самом деле, я не уверен что Sergio думал о том, какие глубокие проблемы решает этот стандарт. Но об этом мы поговорим в следующей статье.
Давайте по пунктам разберём, как это работает
Как работает WHIP
Итак, создадим соединение согласно стандарту и увидим работу на практике!
Если говорить по-простому…
Первое, что здесь нужно сказать: стандарт никак не описывает, как именно вы будете проводить авторизацию ваших пользователей. Вы можете использовать любую авторизацию на заголовках, но я бы рекомендовал использовать JWT или любой способ, который максимально ускорит процесс проверки (без того, чтобы ходить в базу, но при этом с горизонтальным масштабированием). Мне нравится эфемерная авторизация, потому что в этом случае нам не придётся дополнительно ходить за авторизацией TURN. Если у вас не такая авторизация, то вы можете или получить конфигурацию от своего бэкенда при авторизации, или воспользоваться тем методом, что даёт нам стандарт WHIP.
По стандарту, WHIP endpoint может отдавать конфигурацию TURN в ответ на каждый запрос в заголовке Link
. Если ваш клиент не умеет менять конфигурацию TURN, после того как был создан первый offer, или вы не хотите поддерживать Trickle ICE, то вы можете сходить на WHIP endpoint методом OPTIONS чтобы получить список TURN и авторизацию к ним. После этого инициализируйте свой RTCPeerconnection
с этими настройками и затем перейдём к отправке offer.

Дальше нужно просто отправить свой offer на WHIP endpoint в POST‑запросе, получить ответ с кодом 201 и answer в теле и ETag в заголовке. А когда сессия закончится, отправить DELETE‑запрос на WHIP endpoint. И вы великолепны: один HTTP(s) endpoint, пара запросов — и вот вы уже видите/слышите другое устройство.

… и если добавить нюансов
Но есть серия тонкостей, которые лучше знать заранее, перед тем как вы захотите реализовать WHIP у себя. Особенно эти тонкости расцветают, если ваша бизнес‑логика уже написана и надо её портировать.
Я уже упомянул: поддержка Trickle ICE опциональная (как и ice‑restart), и сам стандарт не предусматривает способа узнать, поддерживает ли тот или иной WHIP endpoint Trickle ICE. На данный момент правилом хорошего тона считается отправка всех набранных кандидатов так рано вместе с offer. Для libwebrtc, например, для этого придётся сделать три действия: сгенерировать offer, установить его локально, и сгенерировать offer ещё раз после того как iceGathering закончится (ну или придёт пустой кандидат, если делать по‑старинке). Сервер в WHIP‑схеме никогда не поддерживает отправку дополнительных кандидатов (как правило, у сервера кандидаты известны почти сразу, так как у него обычно белый IP), а должен отправить своих кандидатов сразу в SDP. Кстати, если вы используете только STUN, то, возможно, вам вообще не нужны кандидаты, но об этом я расскажу как‑нибудь в другой статье.
Если и сервер поддерживает Trickle ICE, то во время сессии можно засылать ему при помощи отправки PATCH на тот же WHIP endpoint примерно вот так:
PATCH /session/id HTTP/1.1
Host: whip.example.com
If-Match: "xyzzy"
Content-Type: application/trickle-ice-sdpfrag
Content-Length: 576
a=group:BUNDLE 0 1
m=audio 9 UDP/TLS/RTP/SAVPF 111
a=mid:0
a=ice-ufrag:EsAw
a=ice-pwd:P2uYro0UCOQ4zxjKXaWCBui1
a=candidate:1387637174 1 udp 2122260223 192.0.2.1 61764 typ host
generation 0 ufrag EsAw network-id 1
a=candidate:3471623853 1 udp 2122194687 198.51.100.2 61765 typ host
generation 0 ufrag EsAw network-id 2
a=candidate:473322822 1 tcp 1518280447 192.0.2.1 9 typ host tcptype
active generation 0 ufrag EsAw network-id 1
a=candidate:2154773085 1 tcp 1518214911 198.51.100.2 9 typ host
tcptype active generation 0 ufrag EsAw network-id 2
a=end-of-candidates
HTTP/1.1 204 No Content
Зная инженеров, я готов поспорить, что у вас сразу возник вопрос: «А как же сервер состыкует два абсолютно не связанных запроса? Наверно надо что‑то своё прикручивать!» — и мозг тут же потянулся придумывать схемку узнавания клиента.
Так вот, помните, что вам вернули ETag с ответом 201? Предполагается, что с этого момента сервер может идентифицировать сессию по значению из ETag, отправленному в If‑Match заголовке, в том числе для PATCH и DELETE. Если клиент потерял или прислал неверный ETag, то можно отдать ему код 428, чтобы он шёл делать новую сессию. А если вы считаете, что ETag, отправленный по зашифрованному каналу, это не безопасно (да, все заголовки в HTTPS по умолчанию шифруются), то вы можете добавить проверку IP отправителя запроса. Однако при этом вы безнадёжно потеряете часть мобильных клиентов, но кому важны клиенты, когда есть немного здоровой паранойи, правда же?!
А вот если вы захотите использовать ICE Restart, то тут придётся уже строить свою собственную систему узнавания клиента, так как по стандарту ETag привязывается именно к ICE сессии, и для инициализации ICE Restart клиент обязан прислать If-Match: "*"
и в ответ получить новый ETag.
Ещё серия проблем для готовых WebRTC серверов — WHIP не поддерживает:
работу без бандла,
SDP Plan B,
смену DTLS‑ролей,
частичную отправку SDP.
Но это то, что вы должны были и так поддерживать, если следуете современным RFC. Если по каким‑то причинам вам очень нужно что‑то из этого, то для поддержки WHIP вы можете сделать себе лёгкий бордер на Pion и использовать его как прокси, который состыкует вас, или немного отойти от стандарта (кто вас осудит, это же просто RFC, Cisco можно, и вам тоже).
И последняя проблема, от которой сложно смотреть на WHIP в практической плоскости: WHIP не поддерживает Renegotiation (ReInvite в переводе на SIP). Вот так, совсем. Зато WHIP поддерживает написание своих расширений, которые позволяют решить эту и многие другие проблемы.
Но об этом я напишу немного позже в этом же цикле.
topright007
А в каком кейсе предлагают заменить RTMP, Игорь? врядли чтобы в браузер, мобильные или телеки трансляции транслировать.
А если нужно просто over udp - то чем webrtc лучше SRT?
irbisadm Автор
Вообще собирался в следующей статье написать про это, но скорее через SRT камерой не покрутишь.