WebRTC media transport
WebRTC - основной media path для браузерного клиента. WSMedia остается резервным транспортом для первичного подключения в сетях, где UDP/WebRTC/SRTP недоступен, и как forced diagnostic mode. Если WebRTC endpoint уже был успешно поднят, последующие stale/reconnect/stall восстанавливаются через WebRTC restart/backoff, а не через переключение в WSM.
Клиент не подключается напрямую к Translator. Он создает WebRTC endpoint на выбранной RTC route (rtc_node_id) через CommandLoop signaling, а RTC node / WebRTC Gateway связывает этот endpoint с существующим Translator и conference lifecycle.
Эта страница фиксирует transport contract: что считается источником правды, какие binding-поля обязательны, где проходит граница между conference lifecycle и WebRTC transport, и какие fallback/recovery решения допустимы.
Текущий codec contract:
- audio: Opus через WebRTC;
- video: H.264 для текущего browser WebRTC path;
- WSMedia browser fallback тоже переносит H.264 RTP и декодирует его через WebCodecs;
- VP8 остается в legacy native/recorder/fileplayer path и как совместимость для старых сервисов.
Почему нельзя подключить браузер прямо к Translator
Серверный media core VideoGrace исторически работает по plain UDP/RTP:
TranslatorPoolвыделяет один из UDPTranslator-портов для устройства.Translatorразводит RTP поauthor_ssrcиreceiver_ssrc.- Native-клиенты и
WSMServerотправляют в Translator обычные RTP/RTCP пакеты. WSMServerсейчас является мостомWebSocket binary frame -> UDP RTP.
Браузерный RTCPeerConnection не умеет отправлять такой plain RTP напрямую в UDP-порт Translator. WebRTC поверх UDP всегда несет свой transport stack:
- ICE для выбора сетевого пути и consent checks;
- DTLS handshake;
- SRTP/SRTCP вместо plain RTP/RTCP;
- SDP offer/answer и ICE candidates;
- RTP/RTCP mux и обычно BUNDLE.
Значит, нужен WebRTC Gateway. Он терминирует ICE/DTLS/SRTP, получает обычные RTP/RTCP пакеты и связывает WebRTC tracks с уже существующей моделью device_id, author_ssrc, receiver_ssrc и Translator port.
Текущая схема
flowchart TB
subgraph BrowserSide[Browser client]
Browser["RTCPeerConnection"]
ControlWS["ControlWS"]
end
subgraph ServerSide[VideoGrace Server]
CommandLoop["CommandLoop"]
Processor["Processor\nconference lifecycle"]
Translator["TranslatorPool / Translator"]
Gateway["RTC Gateway"]
Legacy["Native / WSM / Recorder"]
end
ControlWS -->|join/device/signaling| CommandLoop
CommandLoop --> Processor
Processor -->|device_id / SSRC / port| CommandLoop
CommandLoop -->|webrtc_offer / ICE| Gateway
Browser -->|ICE + DTLS + SRTP| Gateway
Gateway <-->|plain RTP/RTCP| Translator
Translator <-->|plain RTP/RTCP| Legacy
CommandLoop остается каналом управления. Processor остается владельцем conference state. Gateway - только media adapter рядом с WSMServer, а не новая конференционная подсистема.
RTC route selection
Перед созданием WebRTC endpoint клиент запрашивает доступные RTC routes. Route - это логический handle WebRTC-ноды, а не сетевой адрес. Реальный ICE address приходит позже в SDP/ICE от выбранной ноды.
{
"webrtc_routes_request": {}
}
Ответ:
{
"webrtc_routes": {
"routes": [
{
"rtc_node_id": "rtc-eu-1",
"role": "rtc-translator",
"version": "3.0.0",
"capabilities": ["rtc", "webrtc", "rtp_bridge"],
"messages_in": 128,
"events_in": 64
}
]
}
}
Сервер возвращает только online CAN-ноды с role=rtc-translator и capability rtc. Порядок routes намеренно перемешивается, чтобы клиенты распределялись между нодами.
flowchart TD
A["Need WebRTC endpoint"] --> B["webrtc_routes_request"]
B --> C{"routes[] есть?"}
C -->|yes| D["try route by order"]
D --> E{"connected?"}
E -->|yes| OK["use WebRTC"]
E -->|no| F{"next route?"}
F -->|yes| D
F -->|no| G{"primary connect + auto mode?"}
C -->|no| H{"in-process gateway available?"}
H -->|yes| OK
H -->|no| G
G -->|yes| WSM["fallback to WSMedia"]
G -->|no| Retry["WebRTC restart/backoff"]
Правила:
- если route не подключился из-за ICE timeout или connect timeout, клиент пробует следующий
rtc_node_id; - если все routes не сработали при первичном подключении в режиме
auto, допускаетсяWSMediafallback; - если routes нет, сервер может использовать in-process gateway, если он собран и включен;
- если WebRTC endpoint уже был поднят, route failure лечится WebRTC restart/backoff, а не WSM fallback.
Модель реализации
WebRTC в VideoGrace - транспортный адаптер вокруг существующей серверной media-модели. Он не создает участников, не решает права, не управляет видимостью и не заменяет Translator.
| Компонент | Ответственность |
|---|---|
web-client |
Создает RTCPeerConnection, формирует SDP offer, отправляет signaling через ControlWS, следит за ICE/health/recovery. |
ControlWS / CommandLoop |
Переносит webrtc_routes_request, webrtc_offer, webrtc_answer, webrtc_ice_candidate; сохраняет порядок control events. |
Processor / conference lifecycle |
Создает участников и устройства, выдает device_id, author_ssrc, receiver_ssrc, port, применяет права и видимость. |
RtcTranslator / in-process WebRTC Gateway |
Терминирует ICE/DTLS/SRTP, сопоставляет WebRTC track с media binding, мостит RTP/RTCP в Translator. |
TranslatorPool / Translator |
Остается основным RTP router по author_ssrc и receiver_ssrc. |
WSMedia |
Резервный WSS media transport и compatibility path, но не часть успешного WebRTC endpoint. |
Главный инвариант: device_connect является источником правды для media device. WebRTC callbacks (ontrack, ICE connected, track open) не создают domain state. Они только подтверждают, что transport для уже созданного устройства начал работать.
Путь подключения всегда двухконтурный:
- Control contour создает/обнаруживает устройство через обычные команды конференции.
- Media contour поднимает WebRTC endpoint для уже существующего
device_idи SSRC binding.
flowchart TB
subgraph Control[Control lifecycle]
Join[connect_to_conference_request]
DeviceParams[device_params]
DeviceConnect[device_connect / device_disconnect]
Grants[conference grants and visibility]
end
subgraph WebRTC[WebRTC endpoint lifecycle]
Routes[webrtc_routes_request]
Offer[webrtc_offer]
Answer[webrtc_answer]
Ice[webrtc_ice_candidate]
Health[ICE / RTP health / recovery]
end
subgraph MediaCore[Media core]
Gateway[RTC Gateway]
Translator[TranslatorPool / Translator]
end
Join --> DeviceParams --> DeviceConnect --> Offer
Grants --> DeviceConnect
Routes --> Offer --> Gateway
Gateway --> Answer
Ice <--> Gateway
Gateway <--> Translator
Health --> Offer
Endpoint identity
Endpoint - одно WebRTC-подключение для одного направления одного media device. При restart создается новый RTCPeerConnection, но domain binding остается прежним: тот же device_id, SSRC и Translator port.
Каждая publish/subscribe попытка должна иметь стабильный endpoint_id, уникальный в рамках клиентской сессии и scope. Клиент передает его в webrtc_offer и webrtc_ice_candidate, а сервер возвращает тот же endpoint_id в webrtc_answer и ICE events.
Рекомендуемый формат:
<scope>-<uuid>
Примеры:
| Scope | Endpoint |
|---|---|
audio |
audio-publish-8b7c... |
video |
video-publish-8b7c... |
audio-subscribe |
audio-subscribe-8b7c... |
video-subscribe |
video-subscribe-8b7c... |
peer_id нужен для диагностики и сопоставления endpoint'ов, например local-mic-1005 или remote-video-1006-228800002.
stateDiagram-v2
[*] --> Planned: device_connect or self device ready
Planned --> Offering: create RTCPeerConnection + local offer
Offering --> Answered: webrtc_answer
Answered --> Checking: ICE candidates
Checking --> Connected: ICE connected/completed
Connected --> Ok: RTP progress
Ok --> Stalled: no RTP/progress or disconnected
Stalled --> Restarting: restart/backoff
Restarting --> Offering: same binding, new endpoint attempt
Ok --> Stopped: device_disconnect / leave / toggle off
Stalled --> Error: restart budget exceeded
Error --> Restarting: explicit user/reconnect kick
Stopped --> [*]
Для диагностики endpoint нужно логировать не только peer_id, но и полный binding: conference_tag, scope, rtc_node_id, endpoint_id, device_id, author_ssrc, receiver_ssrc, port.
Binding по scope
scope определяет направление, набор обязательных полей и семантику SSRC. Без полного binding SDP/ICE может технически завершиться, но media endpoint не будет рабочим: Gateway не знает, с каким Translator port и SSRC его связывать.
| Scope | Direction | Создается после | Обязательный binding | Что делает Gateway |
|---|---|---|---|---|
audio |
Browser -> server | self device_connect my=1 микрофона |
device_id, author_ssrc, port |
Принимает WebRTC Opus RTP, нормализует SSRC/PT и отправляет plain RTP в Translator. |
video |
Browser -> server | self device_connect my=1 камеры/демонстрации |
device_id, author_ssrc, port |
Принимает WebRTC H.264 RTP, нормализует SSRC/PT и отправляет plain RTP в Translator. |
audio-subscribe |
Server -> browser | remote device_connect my=0 микрофона |
device_id, author_ssrc, receiver_ssrc, port |
Регистрирует receiver на Translator, получает plain RTP автора, отправляет WebRTC audio track браузеру. |
video-subscribe |
Server -> browser | remote device_connect my=0 камеры/демонстрации |
device_id, author_ssrc, receiver_ssrc, port |
Регистрирует receiver, получает RTP, отправляет WebRTC video track и управляет keyframe/PLI. |
Media flows
Все четыре media flow одинаковы по форме: сначала control lifecycle создает device/binding, затем WebRTC signaling поднимает transport для этого binding.
flowchart LR
Device["device lifecycle\nid / SSRC / port"]
Offer["webrtc_offer\nscope + binding"]
Gateway["RTC Gateway"]
Translator["Translator"]
Browser["Browser track"]
Device --> Offer --> Gateway
Browser <-->|WebRTC track| Gateway
Gateway <-->|plain RTP/RTCP| Translator
Publish microphone
Локальный микрофон публикуется после device_params и self device_connect. Gateway принимает Opus RTP из WebRTC, снимает SRTP, нормализует SSRC/PT под внутренний media core и отправляет plain RTP в Translator с author_ssrc.
sequenceDiagram
participant C as Client
participant S as CommandLoop
participant G as RTC Gateway
participant T as Translator
C->>S: device_params(audio)
S-->>C: device_connect(my=1, device_id, author_ssrc, port)
C->>C: getUserMedia(audio) + RTCPeerConnection
C->>S: webrtc_offer(scope=audio, binding)
S->>G: start endpoint
G->>T: bind author_ssrc/port
G-->>S: webrtc_answer
C-->>G: Opus SRTP
G->>T: plain RTP(author_ssrc)
Publish camera / screen
Камера и демонстрация используют тот же publish contract, но video codec сейчас H.264-first. Gateway принимает RTP от браузера, нормализует SSRC в author_ssrc и отправляет plain RTP в Translator. Это сохраняет совместимость с native-клиентами, recorder и WSMServer.
Для browser-to-browser video Gateway может оптимизировать forwarding между WebRTC tracks, если источник тоже WebRTC. Эта оптимизация не отменяет Translator path: recorder, WSM, native и legacy clients по-прежнему завязаны на Translator.
Subscribe remote microphone
Remote audio subscribe создается из чужого device_connect. Клиент передает device_id, author_ssrc, receiver_ssrc и port в webrtc_offer(scope=audio-subscribe). Gateway регистрирует receiver на Translator, получает RTP автора и отправляет браузеру WebRTC audio track.
Важный browser-инвариант: входящий звук должен идти через нативный WebRTC/audio output. JS Opus decoder, ScriptProcessor и WebAudio graph не являются основным путем воспроизведения.
Subscribe remote camera / screen
Remote video subscribe тоже создается из чужого device_connect. Если источник native/WSM/recorder, Gateway получает RTP из Translator и отдает его в browser WebRTC track. Если источник WebRTC publisher, Gateway может использовать более короткий forwarding path, но обязан сохранить совместимость с Translator.
Keyframe requests идут через Gateway к publisher и должны быть throttled. Иначе decoder errors на одном клиенте могут превратиться в PLI/FIR flood для всей конференции.
Audio-first адаптация WebRTC
В конференции входящий и исходящий каналы участника могут быть сильно асимметричными. Типичный проблемный сценарий: несколько участников уже публикуют камеры и демонстрацию в хорошем качестве, затем в горячую конференцию заходит участник с узким входящим каналом. Его uplink может быть нормальным, поэтому остальные его видят и слышат, но его downlink забивается чужими видео до того, как отправители успеют снизить bitrate по max_input_bitrate.
Цель этой механики - сохранить звук и управляемо отложить входящее видео, а не пытаться чинить любой RTP stall одинаково.
Основные инварианты:
- audio важнее remote video;
- screen share обычно важнее камер, но при первичном входе слабого клиента может быть временно отложен;
- один stalled remote audio stream не доказывает перегрузку локального downlink: это может быть sender-side проблема, route glitch или перезапуск конкретного publish;
max_input_bitrate- это hint для отправителей, а не мгновенная гарантия, что уже идущие потоки снизились;- WebRTC fallback в WSM не используется для уже поднятых RTC endpoint'ов.
Общая петля адаптации
flowchart LR
Weak["Weak receiver\nтонкий downlink"] -->|speed test + Browser Network API| Control["ControlWS\nset_max_bitrate"]
Control --> MemberList["member.max_input_bitrate"]
MemberList --> Senders["Remote publishers\nupdate sender maxBitrate"]
Senders -->|ниже RTP bitrate| Gateway["RTC Gateway / Translator"]
Gateway --> Weak
Weak -->|локальный downlink budget| Subs["Remote video subscriptions"]
Subs -->|bandwidth_limited| UI["UI\nВидео отключено для звука"]
Адаптация состоит из двух независимых контуров:
| Контур | Где выполняется | Что регулирует |
|---|---|---|
| Sender-side cap | У отправителей | Максимальный bitrate исходящей камеры/демонстрации с учетом max_input_bitrate участников. |
| Receiver-side budget | У слабого получателя | Сколько входящих remote video subscriptions можно держать одновременно, чтобы не убить audio. |
max_input_bitrate и отправители
Клиент оценивает входящий канал через speed test и Browser Network API, затем отправляет set_max_bitrate. Сервер распространяет это значение как max_input_bitrate в member state. Отправители используют минимальный входящий лимит участников конференции как ограничение для своих RTCRtpSender параметров.
sequenceDiagram
participant Weak as Weak client
participant Control as ControlWS
participant Server
participant Sender as Existing sender
participant RTC as RTC Gateway
Weak->>Weak: speed test / Network API
Weak->>Control: set_max_bitrate(input_kbps)
Control->>Server: update member max_input_bitrate
Server-->>Sender: member state update
Sender->>Sender: recompute sender maxBitrate
Sender->>RTC: lower outgoing RTP bitrate
RTC-->>Weak: lower incoming media bitrate
Это не мгновенная защита. Между входом слабого клиента и применением новых sender caps есть окно, в котором старые камеры еще могут отдавать высокий bitrate. Поэтому нужен receiver-side join-settle.
Sender-side ramp-up
Даже если у отправителя еще нет точных данных о слабом получателе, новый publish не должен стартовать сразу на полном bitrate. Локальная камера и демонстрация стартуют с пониженного maxBitrate, затем плавно доходят до целевого значения.
flowchart LR
T0["0 ms\ncamera 35%\nscreen 45%"] --> T1["2.2 s\n70% target"] --> T2["6 s\n100% target"]
Ramp применяется к обычному WebRTC publish и к сценарию, где локальная WSM capture session уже есть, но publish переводится в WebRTC. Все отложенные шаги ramp планируются через общую mediaRecoveryQueue, чтобы leave/stale/stop могли снять jobs по ключу устройства.
Join-settle для слабого получателя
Если клиент входит в конференцию и его входящий канал оценивается как слабый, он временно ограничивает собственные remote video subscriptions. Это защищает первые секунды входа, пока отправители получают max_input_bitrate и снижают свои outgoing sender caps.
sequenceDiagram
participant Weak as Weak client
participant Control as ControlWS
participant Server
participant Sender as Existing senders
participant RTC as RTC Gateway
Weak->>Control: set_max_bitrate(input_kbps)
Weak->>Control: connect_to_conference_request
Control-->>Weak: connect_to_conference_response
Weak->>Weak: arm join-settle 4s
Server-->>Sender: member max_input_bitrate update
Sender->>Sender: apply sender cap / ramp
Server-->>Weak: device_connect existing cameras/screen
Weak->>Weak: hold remote video for 1.2s
Weak->>RTC: subscribe max 1 video during settle
Weak->>RTC: subscribe by budget after settle
Правила join-settle:
- включается после успешного
connect_to_conference_response, если входящий канал оценивается не выше 8 Mbps; - первые 1.2 секунды remote video subscriptions не стартуют;
- до конца 4-секундного окна разрешается максимум одно входящее video subscription;
- в это окно входит и screen share, потому что 2K демонстрация может забить канал сильнее нескольких камер;
- после окна screen share переоткрывается первым, затем камеры добираются по downlink budget.
Receiver-side downlink budget
Клиент локально считает, сколько входящих камер можно держать. Оценка downlink берется как максимум из speed test и Browser Network API, потому что любой из источников может быть устаревшим или консервативным.
Из бюджета вычитается audio reserve:
audio_reserve = 160 kbps + 48 kbps * remote_audio_streams
Оставшийся бюджет делится на условную стоимость одной remote camera, сейчас около 800 kbps. Приоритет получает active speaker, затем уже поднятые/стабильные video sessions, затем остальные камеры. Отключенные бюджетом remote video получают mediaStatus=bandwidth_limited, чтобы UI показывал не ошибку, а понятное состояние "Видео отключено для звука".
stateDiagram-v2
[*] --> Candidate
Candidate --> BandwidthLimited: budget или join-settle держит video
BandwidthLimited --> Subscribing: budget освободился
Candidate --> Subscribing: budget позволяет
Subscribing --> Ok: ICE + RTP progress
Ok --> BandwidthLimited: audio-first protection
BandwidthLimited --> [*]: device_disconnect / leave
Audio-stall protection
Remote audio stall сам по себе не всегда означает, что локальный downlink забит. Поэтому защита downlink включается только в двух случаях:
- локальный downlink budget уже очень низкий, меньше 1200 kbps;
- за короткое окно 8 секунд застопорились минимум два разных remote audio streams.
flowchart TD
A["Remote audio stalled"] --> B{"Budget < 1200 kbps?"}
B -->|yes| Protect["Enable audio-first protection 15s"]
B -->|no| C{"Stalls from >=2 streams\nwithin 8s?"}
C -->|yes| Protect
C -->|no| Restart["Restart only this audio subscribe"]
Protect --> Limit["Limit remote cameras"]
Limit --> KeepAudio["Keep audio reserve first"]
При active protection remote cameras ограничиваются агрессивнее:
| Downlink budget | Разрешенные remote cameras |
|---|---|
| нет оценки | 1 |
< 900 kbps |
0 |
< 3500 kbps |
1 |
>= 3500 kbps |
2 |
Screen share при обычной protection не режется как камера. Исключение - join-settle при входе слабого клиента, где screen share тоже может быть отложен на первые секунды.
Очередь отложенных действий
Адаптация не должна плодить независимые setTimeout, которые переживают leave, stale cleanup или смену device id. Все delayed jobs идут через общую media recovery queue:
| Job key | Назначение |
|---|---|
remote-video-downlink-budget:<reason> |
Пересчитать и применить remote video budget. |
local-publish-ramp:<deviceId>:mid |
Поднять sender bitrate до промежуточного ramp уровня. |
local-publish-ramp:<deviceId>:final |
Поднять sender bitrate до целевого уровня. |
При выходе из конференции, hard cleanup, stopAllRemoteMediaSessions и остановке local publish эти jobs снимаются по prefix. Это важно для native и mobile-клиентов: старый timer не должен воскресить video subscription или sender bitrate после того, как пользователь уже вышел.
Настраиваемые параметры
| Параметр | Значение | Смысл |
|---|---|---|
REMOTE_VIDEO_JOIN_SETTLE_BUDGET_THRESHOLD_KBPS |
8000 |
Включать join-settle для входа слабого клиента. |
REMOTE_VIDEO_JOIN_SETTLE_MS |
4000 |
Общая длительность join-settle окна. |
REMOTE_VIDEO_JOIN_SETTLE_HOLD_MS |
1200 |
Начальная пауза перед remote video subscriptions. |
REMOTE_AUDIO_DOWNLINK_PROTECT_MS |
15000 |
Длительность audio-first protection после подтвержденной downlink проблемы. |
REMOTE_AUDIO_DOWNLINK_STALL_WINDOW_MS |
8000 |
Окно, в котором считаются stalls разных audio streams. |
REMOTE_CAMERA_VIDEO_TARGET_KBPS |
800 |
Условная стоимость одной remote camera для downlink budget. |
LOCAL_VIDEO_PUBLISH_RAMP_MID_MS |
2200 |
Момент перехода sender ramp к 70%. |
LOCAL_VIDEO_PUBLISH_RAMP_MS |
6000 |
Момент перехода sender ramp к 100%. |
Signaling API
Минимальный набор команд поверх CommandLoop.
webrtc_routes_request / webrtc_routes
{
"webrtc_routes_request": {}
}
{
"webrtc_routes": {
"routes": [
{
"rtc_node_id": "rtc-eu-1",
"role": "rtc-translator",
"version": "3.0.0",
"capabilities": ["rtc", "webrtc", "rtp_bridge"]
}
]
}
}
webrtc_offer
{
"webrtc_offer": {
"peer_id": "local-mic-1005",
"conference_tag": "test",
"scope": "audio",
"rtc_node_id": "rtc-eu-1",
"endpoint_id": "audio-publish-8b7c",
"sdp": "v=0...",
"device_id": 1059,
"author_ssrc": 1214,
"port": 5063
}
}
Поля:
| Поле | Назначение |
|---|---|
peer_id |
Диагностический id endpoint'а на клиенте. |
conference_tag |
Конференция, к которой привязан media endpoint. |
scope |
audio, video, audio-subscribe, video-subscribe. |
rtc_node_id |
Выбранная route из webrtc_routes. Пустое значение разрешает серверный fallback, если он доступен. |
endpoint_id |
Клиентский id endpoint'а; нужен для корреляции answer/ICE. |
sdp |
Local SDP offer. |
device_id |
Device lifecycle id. |
author_ssrc |
SSRC автора/source. |
receiver_ssrc |
SSRC получателя для subscribe path. |
port |
Translator port из device_connect. |
device_id, author_ssrc, receiver_ssrc и port являются media binding. Клиент берет их из обычного device lifecycle: device_params выдает device_id/author_ssrc, а device_connect CreatedDevice или remote device_connect возвращает Translator port и receiver_ssrc. Если binding не передан, сервер может завершить SDP/ICE handshake, но endpoint не будет корректно связан с Translator.
webrtc_answer
{
"webrtc_answer": {
"peer_id": "local-mic-1005",
"conference_tag": "test",
"scope": "audio",
"rtc_node_id": "rtc-eu-1",
"endpoint_id": "audio-publish-8b7c",
"sdp": "v=0..."
}
}
webrtc_ice_candidate
{
"webrtc_ice_candidate": {
"peer_id": "local-mic-1005",
"conference_tag": "test",
"scope": "audio",
"rtc_node_id": "rtc-eu-1",
"endpoint_id": "audio-publish-8b7c",
"candidate": {
"candidate": "candidate:...",
"sdpMid": "0",
"sdpMLineIndex": 0
}
}
}
scope определяет назначение binding:
audio- publish microphone;video- publish camera/screen;audio-subscribe- receive remote microphone;video-subscribe- receive remote camera/screen.
Корреляция с текущей моделью устройств идет через device_id, author_ssrc, receiver_ssrc и port, которые уже приходят в device_connect.
ICE candidates можно отправлять как объект candidate, совместимый с RTCIceCandidate.toJSON(). Сервер также принимает flat-поля candidate, sdpMid, sdpMLineIndex.
Connect sequence
Эти sequence diagrams нужны для проверки реализации. В них видно главное: device_* команды идут раньше WebRTC signaling, а webrtc_offer всегда несет binding уже созданного device.
Publish
sequenceDiagram
participant C as Client
participant S as CommandLoop
participant R as RTC node
participant T as Translator
C->>S: device_params
S-->>C: device_connect(my=1, device_id, author_ssrc, port)
C->>S: webrtc_routes_request
S-->>C: webrtc_routes(routes[])
C->>C: create RTCPeerConnection + local track
C->>S: webrtc_offer(scope=audio/video, rtc_node_id, endpoint_id, binding)
S->>R: CAN rtc.endpoint.start
R->>T: bind translator port/ssrc
R-->>S: webrtc_answer
S-->>C: webrtc_answer
C-->>S: webrtc_ice_candidate
S-->>R: CAN rtc.endpoint.ice
Если publish restart происходит после stale/reconnect, новый device_params не отправляется. Клиент переиспользует существующий sourceDevice и поднимает новый WebRTC endpoint с тем же device_id/author_ssrc/port.
Subscribe
sequenceDiagram
participant C as Client
participant S as CommandLoop
participant R as RTC node
participant T as Translator
S-->>C: device_connect(my=0, device_id, author_ssrc, receiver_ssrc, port)
C->>S: webrtc_routes_request
S-->>C: webrtc_routes(routes[])
C->>C: create recvonly RTCPeerConnection
C->>S: webrtc_offer(scope=audio-subscribe/video-subscribe, rtc_node_id, endpoint_id, binding)
S->>R: CAN rtc.endpoint.start
R->>T: register receiver_ssrc / adjust packet
R-->>S: webrtc_answer
S-->>C: webrtc_answer
R-->>C: media track
Subscribe restart также не создает новое устройство. Он останавливает старый RTCPeerConnection и повторяет webrtc_offer для того же remote device_connect binding.
Инварианты
Эти правила важнее конкретной реализации Gateway:
Processorвладеет conference membership, permissions, visibility/hearability и lifecycle устройств.Translatorостается RTP router; WebRTC Gateway не принимает решений о правах доступа.- WebRTC path не создает WSMedia sessions для media payload.
- WSMedia остается fallback только для первичного подключения или forced diagnostic mode.
- Успешный WebRTC endpoint после stale/reconnect восстанавливается через WebRTC restart/backoff.
- Browser audio path не должен опираться на JS Opus decode/encode и
ScriptProcessorкак основной pipeline. - WebRTC SRTP-порты и Translator-порты
5060-5063- разные transport layers. - Browser-to-browser forwarding допустим внутри Gateway, но не должен ломать Translator path для native, WSMedia, recorder и services.
Failover и recovery
WebRTC - предпочтительный media path, но не единственный. На первичном подключении UDP/SRTP может быть заблокирован, поэтому auto mode умеет уйти в WSMedia. После успешного WebRTC connect такое переключение запрещено: иначе reconnect/stale превращается в смену transport model и может создать дубликаты media sessions.
Режим клиента:
auto: сначала пробовать WebRTC; если endpoint изначально не поднялся, перейти на WSMedia.webrtc: использовать только WebRTC для диагностики нового транспорта.wsm: использовать только текущий WSMedia mux для диагностики и совместимости.
Условия перехода auto -> wsm:
- первичный ICE не вышел в
connected/completedза ограниченный таймаут; - DTLS/SRTP handshake завершился ошибкой;
- первичный
webrtc_offerне получилwebrtc_answer; - браузер или платформа не поддерживает нужный WebRTC/audio path.
Условия, при которых auto -> wsm запрещен:
- WebRTC endpoint уже был в состоянии
connected/ok; - recovery выполняется после
restore_stale_session; - health-check показал
stalledилиno inbound RTP progressу уже активной WebRTC session; - local publish получил
disconnected/failedпосле успешного publish.
В этих случаях клиент перезапускает WebRTC endpoint с тем же device_id/sourceDevice, а не создает WSM-сессию. Это защищает device lifecycle: Processor, membership, device_id, author_ssrc, receiver_ssrc и TranslatorPool не должны зависеть от выбранного транспорта.
Во время активной конференции автоматический возврат wsm -> webrtc не обязателен. Его можно добавлять только если переключение будет безопасным и без заметного разрыва media.
Server-owned ICE
VideoGrace не должен зависеть от внешних STUN/TURN-сервисов для базового server-relay сценария. WebRTC Gateway работает как публичная UDP-точка самого VideoGrace Server или внешней RTC-ноды. Браузер получает ICE candidate через ControlWS и отправляет WebRTC/UDP на этот адрес.
Если сервер имеет публичный IP прямо на интерфейсе, дополнительных ICE-сервисов не требуется. Если сервер находится за NAT или libdatachannel видит только приватный адрес вроде 192.168.x.x, нужно явно указать адрес, который браузеры должны использовать.
В AdvertiseAddress должен быть указан именно внешний IP или DNS-имя, которое резолвится в этот IP:
export VG_WEBRTC_ADVERTISE_ADDRESS=core.videograce.ru
Та же настройка может жить в серверном конфиге:
[WebRTC]
AdvertiseAddress=core.videograce.ru
Если WebRTC.AdvertiseAddress не задан, сервер использует Network.Address / VG_ADDR.
Для эксплуатации лучше ограничить UDP-диапазон и открыть его на firewall/NAT. Эти порты отдельные от Translator-портов:
export VG_WEBRTC_PORT_RANGE_BEGIN=43000
export VG_WEBRTC_PORT_RANGE_END=43100
[WebRTC]
PortRangeBegin=43000
PortRangeEnd=43100
Если нужно привязать libdatachannel к конкретному локальному интерфейсу:
export VG_WEBRTC_BIND_ADDRESS=0.0.0.0
[WebRTC]
BindAddress=0.0.0.0
Инвариант: VG_WEBRTC_ADVERTISE_ADDRESS должен быть достижим с клиентской сети, а UDP-порты из диапазона должны приходить на VideoGrace Server. Эти порты отдельные от Translator-портов. Если UDP недоступен при первичном подключении, web-клиент в режиме auto может перейти на WSMedia.
Для внешнего RtcTranslator через CAN действует тот же ICE-инвариант, но адрес относится уже к RTC-ноде, а не к головному серверу. Core отдает клиенту только rtc_node_id; браузер получает реальный адрес RTC-ноды из SDP/ICE, которые возвращает сам worker. Поэтому на каждой внешней RTC-ноде нужно задавать свой VG_WEBRTC_ADVERTISE_ADDRESS и открывать ее UDP range.
Codec direction
Audio path использует Opus. Это обязательный WebRTC codec, он поддерживается браузерами и мобильными WebRTC stack'ами без дополнительных JS codecs.
Browser video path сейчас H.264-first. Web-клиент выставляет H.264 выше остальных codecs в WebRTC offer/answer. WSMedia fallback тоже использует H.264 через WebCodecs encoder/decoder и H.264 RTP packetizer/depacketizer.
Долгосрочный codec target:
- iOS WebRTC не должен рассматриваться как VP8-capable target для основного path;
- mobile native clients должны публиковать и принимать H.264;
- native desktop может использовать OpenH264 как software fallback;
- аппаратное ускорение нужно выносить в отдельные backend implementations: VideoToolbox на Apple, Media Foundation/NVENC/QSV на Windows, VAAPI/NVENC на Linux;
- VP8 оставить как legacy/fallback для старых desktop/services path, но не как основной browser/mobile codec.
Gateway не должен становиться video transcoder по умолчанию. Его задача - WebRTC termination, RTP routing, keyframe/RTCP control и совместимость transport layers. Кодирование/декодирование должно жить на клиенте или в специализированных media services.
SDP, codec и RTP payload type
SDP answer обязан выбирать codec и RTP payload type из browser offer. Нельзя хардкодить browser-side dynamic payload type на стороне Gateway только потому, что внутренний media core использует фиксированное значение.
Текущие инварианты:
- внутренний media core использует
RTPPayloadType::ptOpus = 111для Opus; - Firefox может предлагать Opus как
109, Chrome часто предлагает111; - H.264 payload type тоже dynamic и выбирается из offer;
- Gateway обязан разделять browser-side payload type и internal media-core payload type.
Правильная модель:
flowchart LR
BrowserOffer["Browser SDP offer\nm=audio ... 109\na=rtpmap:109 opus/48000/2"]
Gateway["Gateway\nselect_opus_candidate -> browser PT 109"]
Answer["Gateway SDP answer\nm=audio ... 109"]
Internal["Translator RTP\ninternal Opus PT 111"]
BrowserRtp["WebRTC RTP to browser\nbrowser Opus PT 109"]
BrowserOffer --> Gateway --> Answer
Internal --> Gateway --> BrowserRtp
Gateway всегда разделяет два мира:
- browser-side RTP должен использовать payload type, выбранный из browser SDP offer;
- internal RTP в Translator должен использовать payload type, который ожидает media core.
Для publish path Gateway принимает browser RTP с browser payload type и перед отправкой в Translator нормализует:
- RTP header extension layout приводится к compact VG RTP layout;
- SSRC заменяется на
author_ssrcизdevice_connect; - audio payload type заменяется на внутренний
111; - video payload type заменяется на внутренний codec/PT, который ожидает media core.
Для subscribe path Gateway делает обратное:
- получает plain RTP из Translator с внутренним payload type;
- сохраняет/нормализует SSRC автора;
- перед отправкой в browser WebRTC track выставляет payload type, выбранный из browser offer;
- для video дополнительно держит выбранный H.264 payload type и keyframe/PLI state.
Если answer выбирает payload type, которого не было в offer, разные браузеры ведут себя по-разному. Chrome может терпеть часть несовместимостей. Firefox часто поднимает ICE/DTLS и отдает track, но аудио остается немым или RTP не декодируется.
Поэтому при любом инциденте "ICE connected, track есть, звука нет" первым делом проверяется offer-summary-json и answer-summary-json, а не autoplay/output device.
Минимальная диагностика SDP:
[WebRTCPublish] offer-summary-json
[WebRTCPublish] answer-summary-json
[WebRTCSubscribe] offer-summary-json
[WebRTCSubscribe] answer-summary-json
Для аудио в Firefox ожидаем:
offer: m=audio ... 109 ...
answer: m=audio ... 109
Не ожидаем:
offer: m=audio ... 109 ...
answer: m=audio ... 111
Это ошибка Gateway negotiation, а не autoplay/output bug.
Production Gateway implementation
WebRTC Gateway - production-компонент сервера. Есть два runtime placement, но для клиента и CommandLoop это один и тот же signaling contract.
| Режим | Где выполняется | Когда используется |
|---|---|---|
| In-process gateway | VideoGraceServer, Server/WebRTC/LibDataChannelGateway.cpp + Engine/RTC/LibDataChannelBridge.cpp |
Путь по умолчанию для простого deployment без отдельной RTC-ноды. |
| External RTC node | Services/RtcTranslator/RtcTranslator.cpp через CAN jobs rtc.endpoint.* |
Масштабирование и вынос WebRTC/SRTP CPU, ICE state и UDP-портов на отдельные узлы. |
Оба режима используют libdatachannel как WebRTC termination layer:
- ICE и candidate negotiation;
- DTLS handshake;
- SRTP/SRTCP;
- SDP offer/answer;
- WebRTC media tracks;
- SCTP/datachannel support, который сейчас не является целью audio/video path.
Кодеки, микширование, конференционная модель, права доступа, TranslatorPool и device/SSRC lifecycle остаются в VideoGrace. Gateway должен получить из WebRTC обычные RTP/RTCP packets и передать их в существующий UDP media core.
Code map
| Код | Назначение |
|---|---|
Engine/Proto/CmdWebRTC.* |
Команды webrtc_routes_request, webrtc_offer, webrtc_answer, webrtc_ice_candidate. |
Server/WebRTC/IWebRTCGateway.h |
Backend-neutral интерфейс Gateway. |
Server/WebRTC/WebRTCServer.* |
Facade на уровне Processor, аналогично WSMServer для WebRTC signaling. |
Server/WebRTC/WebRTCGateway.cpp |
Factory: выбирает in-process backend, external CAN wrapper или unavailable stub. |
Server/WebRTC/LibDataChannelGateway.cpp |
In-process adapter между CommandLoop offer/ICE и Engine/RTC bridge. |
Server/WebRTC/CanRtcGateway.cpp |
Adapter для внешних RTC CAN workers. |
Engine/RTC/LibDataChannelBridge.* |
Общий WebRTC/RTP bridge: PeerConnection, SDP, ICE, RTP/PT/SSRC mapping, Translator UDP binding. |
Services/RtcTranslator/RtcTranslator.cpp |
Внешняя RTC-нода, принимающая CAN jobs rtc.endpoint.start/ice/stop/list. |
Новые клиенты не должны зависеть от того, выбран in-process или external gateway. Для клиента это всегда один contract: webrtc_routes_request, webrtc_offer, webrtc_answer, webrtc_ice_candidate и media binding из device_connect.
Runtime selection
По умолчанию сервер собирается с WebRTC gateway:
cmake -S . -B build
cmake --build build --target VideoGraceServer
Флаги сборки:
| Флаг | Значение |
|---|---|
VG_ENABLE_LIBDATACHANNEL=ON |
Включает libdatachannel backend. Это default. |
VG_WITHOUT_WEBRTC=ON |
Принудительно выключает WebRTC/libdatachannel gateway и оставляет только stub. |
Если gateway выключен, сервер может принять signaling-команды, но WebRTC media endpoint не поднимется. Такой режим нужен только для diagnostic/build fallback, не для production.
External RTC nodes включаются runtime-настройкой:
export VG_WEBRTC_EXTERNAL_RTC_ENABLED=1
или конфигом:
[WebRTC]
ExternalRtcEnabled=1
Когда external RTC включен и CAN-ноды role=rtc-translator доступны, сервер отдает их в webrtc_routes. Если внешняя нода недоступна или route не поднялся, клиент пробует следующий route или in-process fallback по обычным правилам route selection.
Сборка libdatachannel binaries
Исходники libdatachannel лежат в thirdparty/libdatachannel, headers - в Lib/libdatachannel/include. Бинарные артефакты не собираются основным CMake автоматически и не должны становиться частью обычного исходного diff. Для каждой платформы они собираются отдельно, публикуются как пакет Lib/libdatachannel, а в рабочую копию подтягиваются через Lib/upd_*_libs.py по той же модели, что и остальные bundled-библиотеки.
Для macOS arm64:
thirdparty/libdatachannel/build_vg_macos.sh
Для Linux x86_64:
thirdparty/libdatachannel/build_vg_linux.sh
Для Windows x64 из Developer PowerShell for Visual Studio:
powershell -ExecutionPolicy Bypass -File thirdparty/libdatachannel/build_vg_windows.ps1 -Platform win_x64
Для Windows Win32:
powershell -ExecutionPolicy Bypass -File thirdparty/libdatachannel/build_vg_windows.ps1 -Platform win32
Скрипт:
- подтягивает build-only зависимости в
thirdparty/libdatachannel/deps; - фиксирует
libsrtpнаv2.7.0, потому чтоlibdatachannel 0.24.2ожидает targetsrtp2; - собирает static
datachannel-static; - на Windows собирает
libdatachannelи его зависимости с MSVC runtime/MT, как основнойServer.vcxproj; - на Windows после CMake configure дополнительно патчит generated
.vcxproj, чтобы исключить случайный/MDв dependency targets; - использует OpenSSL из
Lib/OpenSSL/lib/x64дляwin_x64иLib/OpenSSL/lib/win32дляwin32; - копирует результат в платформенную папку
Lib/libdatachannel/lib/*.
Ожидаемые артефакты:
Lib/libdatachannel/lib/mac_arm64/libdatachannel-static.a
Lib/libdatachannel/lib/mac_arm64/libjuice-static.a
Lib/libdatachannel/lib/mac_arm64/libsrtp2.a
Lib/libdatachannel/lib/mac_arm64/libusrsctp.a
Lib/libdatachannel/lib/lin_x64/libdatachannel-static.a
Lib/libdatachannel/lib/lin_x64/libjuice-static.a
Lib/libdatachannel/lib/lin_x64/libsrtp2.a
Lib/libdatachannel/lib/lin_x64/libusrsctp.a
Lib/libdatachannel/lib/win_x64/datachannel-static.lib
Lib/libdatachannel/lib/win_x64/juice-static.lib
Lib/libdatachannel/lib/win_x64/srtp2.lib
Lib/libdatachannel/lib/win_x64/usrsctp.lib
Lib/libdatachannel/lib/win32/datachannel-static.lib
Lib/libdatachannel/lib/win32/juice-static.lib
Lib/libdatachannel/lib/win32/srtp2.lib
Lib/libdatachannel/lib/win32/usrsctp.lib
После публикации пакета для платформы сервер проверяется так:
cmake -S . -B /tmp/videograce-cmake-webrtc-check -DVG_ENABLE_LIBDATACHANNEL=ON
cmake --build /tmp/videograce-cmake-webrtc-check --target VideoGraceServer
Production contract
Gateway считается production-ready только если выполняет весь contract:
- signaling
webrtc_offer/answer/ice_candidateидет черезControlWS; - publish microphone: browser mic track -> Gateway -> Translator;
- subscribe microphone: Translator -> Gateway -> browser remote audio track;
- publish camera/screen: browser video track -> Gateway -> Translator;
- subscribe camera/screen: Translator или Gateway forwarding -> browser remote video track;
- WSMedia fallback не регрессит для сетей, где UDP/WebRTC/SRTP недоступен;
- native/WSM/recorder compatibility сохраняется через Translator path;
- keyframe requests throttled, чтобы browser RTCP feedback не создавал бесконечный PLI/FIR flood;
- SDP answer выбирает payload type из offer, а internal RTP нормализуется отдельно.
Если какой-то пункт ломается, это regression в WebRTC transport или Gateway bridge, а не отсутствие feature.
Дальше
Следующие доработки не меняют базовый WebRTC contract:
- Довести H.264-first до native/mobile/media services, сохранив VP8 только как legacy fallback.
- Улучшить RTP inspection для H.264 keyframe detection по IDR/STAP-A/FU-A.
- Добавить OpenH264 software fallback для native desktop.
- Для KMP/mobile clients использовать WebRTC как первый transport contract, без встраивания C++ media core.
- Hardware acceleration оформлять отдельными backend implementations, не смешивая с базовой H.264-интеграцией.