Оценка пропускной способности и контроль перегрузки в WebRTC

Автор: Николай СапуновОбновлено: август 202624 мин чтения
Содержание статьи +

Кратко (TL;DR)

Каждый вызов WebRTC незаметно для пользователя гоняет петлю обратной связи: десять-двадцать раз в секунду она спрашивает у сети «сколько ты сейчас способна провезти?» – и подгоняет целевой битрейт энкодера под ответ. То, что стандарты называют контролем перегрузки (congestion control), а реализации – оценкой пропускной способности (bandwidth estimation), и есть эта петля. В libwebrtc (тот самый C++-движок внутри Chrome, Edge, большинства серверных SFU и почти любого нативного мобильного SDK) петлю выполняет Google Congestion Control – сокращённо GCC: задержно-ориентированный (delay-based) оценщик читает времена прихода пакетов через Transport-Wide CC Feedback, а потерь-ориентированный (loss-based) оценщик читает долю потерь; финальный битрейт – минимум из двух. У IETF есть два альтернативных алгоритма – SCReAM (RFC 8298) и NADA (RFC 8698), – но в продакшен-браузерах они почти не встречаются; основное движение в 2026 году идёт в новых форматах фидбэка (RFC 8888 CCFB, ECN/L4S). Эта статья проходит петлю целиком: какие пакеты несут сигнал, какие RTCP-сообщения возвращают обратную связь, как из таймстампов получается битрейт, как энкодеру говорят «притормози», и где петля ломается в реальных сетях.

Зачем это знать

Если вы строите, эксплуатируете или покупаете WebRTC-продукт – видеоконференцию, телемедицину, e-learning, контакт-центр с видео, live-шоппинг, инструмент удалённой совместной работы или голосового ИИ-агента, – пользовательский опыт продукта по сути задан тем, насколько хорошо BWE работает на самых плохих сетях вашей аудитории. Когда оценщик перебарщивает с битрейтом, кадры замораживаются, звук трещит, и дашборды качества звонка краснеют. Когда оценщик недодаёт, картинка остаётся мыльной, скриншер размыт, и комната ощущается как «дешёвая», хотя на самом деле сети хватало с запасом. Инженеры тюнят эту петлю годами, а продакт-менеджеры эксплуатируют результат десятилетиями. Цель статьи – дать продакт-менеджеру и архитектору ту же ментальную модель, которая есть у WebRTC-инженера: что измеряет петля, что она решает, где осторожничает, где ошибается, и какие ручки можно повернуть.

Петля управления в одном абзаце

Представьте, что вы льёте воду в шланг, а противоположный конец не видите. Начинаете осторожно. Спустя мгновение друг на дальнем конце кричит, как доходит вода – вовремя, с задержкой или совсем не доходит. Если ответы чистые – наращиваете напор; если чуть запоздали – тут же сбрасываете. WebRTC-отправитель делает ровно это десять-двадцать раз в секунду, только вместо воды – пакеты, а вместо друга – RTCP-сообщение обратной связи. Отправитель пишет порядковый номер в каждый исходящий RTP-пакет, получатель отвечает компактным RTCP-отчётом с этими номерами и таймстампами прихода, а контроллер перегрузки на стороне отправителя сравнивает времена прихода со временами отправки. Если разрыв между отправкой и приходом растёт – сеть наполняется, надо сбросить. Если разрыв ровный и потерь нет – сеть свободна, можно аккуратно поднять. Эта петля и есть вся игра. Всё остальное в статье – детали того, как игра играется в условиях реальных потерь, джиттера мобильного радио, событий NAT-rebinding, кросс-трафика и десятитысячестрочного кодовой базы внутри каждого билда Chromium. (Транспортный контекст шире раскрыт в TCP и UDP в стриминге и Контроль перегрузки простыми словами: BBR, CUBIC, Copa.)

Рисунок 1. Петля контроля перегрузки в WebRTC. Отправитель пишет transport-wide порядковый номер на каждый исходящий пакет; получатель возвращает таймстампы прихода; отправитель оценивает доступную полосу по временному ряду и говорит энкодеру, на какой битрейт целиться. Петля работает непрерывно всю длительность звонка.

Почему медиа-вызову нужен собственный контроллер перегрузки

Веб-страница, выкачивающая файл по HTTP, использует TCP и наследует TCP-овский контроль перегрузки – обычно CUBIC, иногда BBR, иногда Reno (см. широкий контекст в Контроль перегрузки простыми словами: BBR, CUBIC, Copa). WebRTC-вызов сознательно делает иначе. Причина в том, что TCP реагирует на потерю пакета ретрансмитом по порядку, что порождает head-of-line blocking: новые байты ждут, пока придут старые. Для скачивания файла это правильный размен: испорченная таблица хуже, чем медленная. Для real-time видео – неправильный: 200-мс задержка из-за TCP-ретрансмита делает звук слышимо рваным и видео заметно дёрганым, а секундная задержка вообще убивает разговор. Поэтому WebRTC-медиа едет на UDP, упаковано в RTP с SRTP-шифрованием (см. DTLS, SRTP, TLS, mTLS – шифрование медиа), и движок WebRTC обязан делать контроль перегрузки самостоятельно, а не отдавать его TCP. Задача контроллера – решать, как быстро приложению можно слать медиа, а приложение этому подчиняется: снижает целевой битрейт энкодера, дропает simulcast-слои или замедляет темп пакетов в пейсере.

Размен у такого контроллера другой, чем у TCP. TCP оптимизирует пропускную способность: пропихнуть как можно больше байт. WebRTC-контроллер оптимизирует низкую сквозную задержку при приемлемом качестве – даже ценой того, что свободная ёмкость остаётся неиспользованной. Именно поэтому стандартный WebRTC-стек не давит против узкого места так сильно, как параллельная закачка файла: цели у звонка другие.

Сигнал: что несёт каждый пакет

Чтобы петля работала, отправителю нужно, чтобы получатель умел идентифицировать, какой пакет когда пришёл. Порядковые номера RTP существуют (16 бит, per-SSRC, определены в RFC 3550) и их хватает для детекции потерь, но они не дают получателю отчитаться единой временной шкалой по нескольким медиапотокам в одном транспорте. В типичном звонке отправитель шлёт аудио-поток и видео-поток – иногда simulcast в трёх разрешениях, иногда трек скриншера, иногда слои SVC, – а контроллер хочет рассуждать обо всех вместе, потому что у них один сетевой bottleneck. Это и есть задача RTP-расширения заголовка Transport-Wide Congestion Control.

Transport-wide порядковый номер

Отправитель навешивает на каждый исходящий пакет соединения RTP header extension под именем transport-wide-cc-02. Расширение несёт 16-битный номер, который увеличивается на единицу на каждый отправленный пакет транспорта – независимо от SSRC. Формат задокументирован на сайте проекта WebRTC: transport-wide-cc-02: два байта ID и длины расширения, два байта transport sequence number, итого четыре байта на пакет. Поскольку нумерация – транспортная, не на поток, получатель пишет единый ряд времён прихода по аудио, видео, simulcast-слоям и любому другому SSRC, делящему транспорт, – а отправитель потом рассуждает об одном сетевом пути, а не о пути на SSRC.

Предшествующее расширение abs-send-time (abs-send-time на сайте WebRTC) несёт время передачи отправителем в 24-битном разрешении и используется REMB-фидбэком (описан ниже). Современные отправители почти всегда согласовывают оба: abs-send-time для совместимости с REMB-приёмниками и transport-wide-cc-02 для современного пути фидбэка. Оба согласуются через SDP; если получатель не сигналит поддержку transport-wide-cc-02, отправитель откатывается на старый REMB-путь.

Обратная связь от получателя

Получатель шлёт обратно RTCP-пакет, который перечисляет для пачки transport-номеров: когда каждый пакет пришёл (относительно базового времени) и пришёл ли вообще. Формат – Transport-Wide CC Feedback: изобретение Google, не оформленное как RFC, но описанное в draft-holmer-rmcat-transport-wide-cc-extensions-01 и реализованное в каждой имплементации, наследующей libwebrtc. Частота фидбэка настраиваема, но обычно ~50 мс на видео, батчуя transport-номера за интервал. На выходе – высокочастотный временной ряд, по которому отправитель может действовать.

Второе, более старое сообщение фидбэка – Receiver Estimated Maximum Bitrate, REMB (определён в draft-alvestrand-rmcat-remb-03) – до сих пор используется некоторыми приёмниками. В мире REMB получатель сам гоняет delay-based оценщик и пишет обратно одно число: «не шли больше X бит в секунду на этот поток». Отправитель повинуется. В мире Transport-Wide CC получатель шлёт только таймстампы, а оценщик гоняет отправитель. Современный дефолт – последнее: у отправителя есть информация, которой нет у получателя (что энкодер пытался отправить, был ли в полёте probe-пакет), и оценка получается точнее.

Стандартная альтернатива IETF: RFC 8888 CCFB

В 2020 году IETF опубликовал RFC 8888RTP Control Protocol (RTCP) Feedback for Congestion Control. Концептуально это то же самое, что Transport-Wide CC Feedback: компактное RTCP-сообщение, возвращающее отправителю поэлементную информацию о прибытии. Различия в битах: RFC 8888 отчитывается per-SSRC, а не транспортно, явно содержит поле для ECN-CE-меток (Explicit Congestion Notification), что критично для истории L4S (см. ниже), и за ним стоит опубликованный RFC. Исходник WebRTC прямо документирует: transport-wide-cc extension не может использоваться одновременно с CCFB-фидбэком RFC 8888. На 2026 год поддержка RFC 8888 пришла в некоторые реализации, но дефолтом libwebrtc пока не стала; миграция идёт постепенно.

Математика: как времена прихода превращаются в битрейт

К этому моменту у отправителя примерно каждые 50 мс есть список transport-номеров, которые он отправил, и времена, когда каждый из них пришёл. Параллельно на этом списке работают два оценщика. Их выходы комбинируются; финальный битрейт энкодеру – минимум.

Оценщик 1 – delay-based

Delay-based оценщик – сердце Google Congestion Control. Канонический референс – (уже истёкший) Internet-Draft draft-ietf-rmcat-gcc-02 (Holmer, Lundin, Carlucci, De Cicco, Mascolo, июль 2016); алгоритм в libwebrtc эволюционировал, но структура та же, что в драфте.

Идея проста. Если сеть не перегружена, разрыв между двумя пакетами на приёмнике равен разрыву на отправителе – пакет A ушёл в t, пакет B в t + 10 мс, оба прошли пустую очередь, оба пришли с разницей 10 мс. Если сеть начинает забиваться, пакет B приходит позже ожидаемых 10 мс, потому что ждал в очереди. Оценщик меряет разницу межпакетных интервалов – одностороннюю вариацию задержки – и кладёт её в фильтр Калмана, который оценивает медленно меняющийся сигнал: куда движется тренд очередной задержки? Адаптивный порог (контроллер сам подкручивает порог по мере изменений сети) классифицирует тренд в одно из трёх состояний: under-using (разрыв сжимается – наращивать), normal (ровно – держать), over-using (растёт – сбрасывать). Состояние крутит rate controller, который мультипликативно наращивает битрейт в under-using, держит в normal и роняет (мультипликативно, около текущего receive-rate, скажем в 0.85 раза) в over-using.

Пример в замедленной съёмке. Скажем, отправитель шлёт на 1200 kbps. Он шлёт 1250-байтный видеопакет во время отправителя 100 000 µs и ещё один в 110 000 µs (разрыв 10 мс). Получатель фиксирует их в 100 300 µs и 110 330 µs (10,03-мс разрыв на его часах; смещение 300 µs контроллеру не интересно). Дельта – 10,03 − 10 = 30 µs, практически ничто. Фильтрованный тренд держится около нуля, порог говорит «normal», контроллер держит. Включается кросс-трафик и очередь bottleneck'а начинает расти. Следующая пара имеет send-разрыв 10 мс и arrival-разрыв 13 мс – дельта 3 мс. После нескольких таких сэмплов подряд оценка фильтра пересекает порог, состояние меняется на over-using, контроллер умножает целевой битрейт на коэффициент backoff (типично 0,85 от текущего receive-rate, ~1000 kbps в нашем примере) и говорит энкодеру. Энкодер задирает QP или дропает кадр, отправка падает до 1000 kbps, очередь рассасывается, следующие сэмплы возвращают дельту к нулю, состояние возвращается в normal, и через несколько секунд normal контроллер снова начинает расти – мультипликативно, обычно ×1.05–1.08 за RTT – в поисках нового потолка.

Вся петля проворачивается раз на каждый фидбэк-пакет (~50 мс). За минуту звонка контроллер принимает 1200 отдельных решений по целевому битрейту. Энкодер получает новый таргет через интерфейс libwebrtc Call::OnBitrateAllocationChanged (или его аналог в другом стеке); энкодер либо перекодирует следующий кадр под новый таргет, либо в simulcast меняет, какой слой передаёт (что выглядит как переключение слоя со стороны SFU, см. Simulcast и SVC в SFU).

Оценщик 2 – loss-based

Delay-based хорош, но слеп к одному режиму отказа: к каналу с потерями, где пакеты дропаются ещё до того, как очередь начала расти. Радиоканал со слабым сигналом, Wi-Fi с коллизиями, спутник в плохую погоду – всё это часто имеет низкую базовую задержку (очереди маленькие), но теряет 2–10% пакетов случайно. Delay-based сам по себе будет радостно наращивать, потому что задержка чистая.

Loss-based ловит этот случай. Он читает тот же RTCP-фидбэк (какие пакеты пришли и не пришли), считает долю потерь в скользящем окне и применяет пороги: ниже 2% – оценщик доволен и отдаёт высокий «безопасный» битрейт; выше 10% – сбрасывает примерно вдвое; между 2 и 10% – линейно интерполирует. Финальная оценка, которую видит энкодер, – min(delay-based, loss-based). Loss-based – страховка для случая lossy-without-congestion.

Сам loss-based прошёл три поколения внутри libwebrtc, как разбирает webrtcHacks-обзор Густаво Гарсии (Loss-based bandwidth estimation in WebRTC): оригинальные «пороги и ступеньки» 2011 года, «сглаженные пороги и тренды» v1 примерно 2019-го и «максимум правдоподобия из набора кандидатов» v2 с 2023-го. Именно v2 шипает в продакшен-стеках 2026 года; он формулирует loss-rate-as-a-function-of-sending-rate вероятностно и выбирает sending-rate, максимизирующий совместное правдоподобие наблюдаемых потерь и подтверждённых rate'ов. Разница больше всего бросается в глаза на flaky-сотовых, где старые версии заметно перешуты и недошуты.

Рисунок 2. Два оценщика внутри Google Congestion Control в libwebrtc. Delay-based ловит перегрузку до появления потерь; loss-based – потери без перегрузки. Финальный таргет – минимум: оба должны согласиться, чтобы битрейт мог вырасти.

Probing: третий приём

У контроллера, реагирующего только на то, что энкодер уже шлёт, есть проблема: он не отличает «энкодер шлёт 800 kbps и сеть в порядке» от «энкодер шлёт 800 kbps, но сеть провезёт 8000 kbps». Сигнал rate ограничен сверху выходом энкодера. Чтобы найти потолок, контроллер должен иногда слать больше, чем производит энкодер, посмотреть, доходит ли это «больше» вовремя, и в случае успеха наращивать энкодер.

Это и есть bandwidth probing. Контроллер впрыскивает короткий всплеск padding-пакетов – обычно burst до 100 мс на заведомо более высоком rate, чем текущий таргет, – и наблюдает фидбэк. Если пакеты burst'а приходят с той же околонулевой вариацией задержки, что обычный трафик – у сети есть запас, таргет можно поднять. Если пакеты burst'а толпятся в очереди – запаса нет, оставить таргет. Содержимое probe берётся из собственных padding-байт SFU или, в некоторых стеках, из дубликата существующего RTP-потока. Точная история и поведение описаны на webrtcHacks в материале Probing WebRTC Bandwidth Probing и в исходниках libwebrtc под modules/pacing/bitrate_prober.cc.

Probing – то, что делает старт быстрым. Без него звонок рос бы от стартового битрейта (обычно 300 kbps на видео) до потолка только за счёт ожидания, пока энкодер сам произведёт больше байт – а он не произведёт, пока контроллер не скажет. С probing контроллер за пару сотен миллисекунд после старта гонит burst на условные 2000 kbps, фидбэк подтверждает чистоту пути, энкодеру говорят сразу выходить на 2000. Видимый эффект: картинка резкая в первую секунду, а не на десятой.

Альтернативы: SCReAM и NADA

Рабочая группа IETF Real-time Multimedia Application coordination (RMCAT) в 2017–2019 опубликовала три алгоритма: Google Congestion Control как Internet-Draft, SCReAM как RFC 8298 и NADA как RFC 8698. Все три бьют по одной задаче и используют одни сигналы RTCP. Различаются формой алгоритма и тем, что оптимизируют.

SCReAM (Self-Clocked Rate Adaptation for Multimedia) – алгоритм Ericsson, спроектированный под мобильное радио. Он window-based (трекает «байты в полёте» против congestion window, как TCP), а не чисто rate-based, и гибрид loss-and-delay. Window-based делает его «self-clocked»: sending rate неявно следует из размера окна и RTT, явная цель пейсинга не нужна. SCReAM шипает в некоторых RTC-стеках в продакшене (особенно у самого Ericsson; публичная референс-реализация – EricssonResearch/scream), и в 2026 году идёт v2-апдейт draft-johansson-ccwg-rfc8298bis-screamv2-03.

NADA (Network-Assisted Dynamic Adaptation) – алгоритм Cisco. Rate-based, построен на использовании ECN-меток, если они есть, с откатом на задержку и потери, если ECN нет. ECN позволяет сетевому устройству ставить пометку в IP-заголовке вместо дропа пакета, когда очередь начинает расти; получатель эхом возвращает метку, и отправитель сбрасывает rate, не теряя данные. NADA лучше всего ложится на историю L4S (Low Latency, Low Loss, Scalable Throughput) – семейство RFC 9330–9332, – потому что L4S опирается на ECN. Без ECN NADA откатывается на задержку и потери, как и остальные.

Что в сухом остатке: какой шипает в браузерах? В 2026 году ответ не изменился: Chrome, Edge, Safari и Firefox шипают Google Congestion Control, потому что libwebrtc шипает GCC. Стандартизованные алгоритмы (SCReAM, NADA) встречаются в немногих специальных стеках и в академических работах. Это важно, потому что вся экосистема browser-to-browser, browser-to-SFU, SFU-to-browser наследует поведение GCC. Если ваш продукт где-то проходит через браузер – особенности GCC становятся вашими особенностями.

Сравнительная таблица

СвойствоGoogle Congestion Control (GCC)SCReAM (RFC 8298)NADA (RFC 8698)
Форма алгоритмаRate-based, delay + loss, min(delay, loss)Window-based, гибрид delay + lossRate-based, delay + loss + ECN
Статус стандартаИстёкший IETF draft (draft-ietf-rmcat-gcc-02, 2016); de-facto в libwebrtcIETF RFC 8298 (фев 2018); v2 в работе в 2026IETF RFC 8698 (фев 2020)
Сообщение фидбэкаTransport-Wide CC Feedback (Google) или REMB legacyRFC 8888 CCFBRFC 8888 CCFB
Реагирует на ECNНет (планируется через L4S)ОпциональноДа (по дизайну)
Где в продакшенеChrome / Edge / Safari / Firefox / SDK на libwebrtc / все крупные SFUСтек Ericsson; немного специальных стековИсследовательский и специальный
Поведение на радиоВ v2 loss-based – хорошоСпроектирован под этоСпроектирован под это
Где использовать в 2026Любой браузерный WebRTC-звонокЧасть Ericsson-стеков; исследованияL4S-сети; исследования

Если ваш продукт живёт в браузерах, GCC – ваш алгоритм, хотите вы того или нет. SCReAM и NADA становятся интересны, только если ваши эндпоинты не браузеры – встраиваемые устройства, кастомные RTC-стеки, broadcast-contribution-железо, – и даже тогда приходится взвесить операционную цену алгоритма, который не использует остальной WebRTC-интернет. (Где WebRTC-доставка вписывается в семейство стриминговых протоколов вообще – см. Дерево семейства протоколов доставки.)

Рисунок 3. Выбор RTC-алгоритма контроля перегрузки в 2026 году. Для подавляющего большинства продуктов, где-либо касающихся браузера, выбор сделан за вас силами libwebrtc.

Что энкодер реально делает с этим числом

Контроллер, выдающий таргетный битрейт, – только половина истории. Энкодер, менеджер simulcast/SVC и пейсер обязаны конвертировать это число в реальное поведение «на проводе».

Энкодеру сообщают новый таргет, и он реагирует на следующем кадре. Для одинокого WebRTC-видео – один паблишер, одно разрешение, один битрейт – энкодер просто переустанавливает rate: libvpx-VP8 или libvpx-VP9 с обновлённым rc_target_bitrate сделает следующий кадр на новом rate, обычно поправив параметр квантования (QP). Качество гладко падает с битрейтом до минимума, где энкодер меняет стратегию и начинает дропать кадры вместо дальнейшего роста QP – типично около 150–250 kbps для 720p.

Для simulcast контроллер не сообщает энкодеру одно число; он сообщает менеджеру simulcast-слоёв общий бюджет, и менеджер выбирает, какая комбинация в него уместится. Типичный libwebrtc-клиент паблишит три слоя: 720p на 1500 kbps, 360p на 500 kbps и 180p на 150 kbps. Если общий таргет 1500 kbps – паблишатся все три. Если упало до 700 kbps – менеджер выкидывает 720p и паблишит 360p + 180p (итого 650 kbps, влезаем). Если упало до 200 kbps – паблишится только 180p. На каждом шаге дроп резкий, поэтому качество simulcast при ухудшении сети выглядит «ступеньками», а не плавно. (Подробный разбор simulcast – в Simulcast и SVC в SFU.)

В SFU-разворотке сам SFU гоняет per-subscriber BWE на даунлинке к каждому подписчику и выбирает, какой simulcast-слой переслать тому или иному. mediasoup прямо документирует: оцененный outgoing bitrate (из REMB или Transport-Wide CC) распределяется между consumer'ами с приоритетом более важных треков. Архитектура LiveKit описывает тот же паттерн: клиент по умолчанию паблишит три simulcast-слоя, SFU выбирает слой подписчику исходя из REMB- и TWCC-сигналов на каждом даунлинке (LiveKit architecture deep dive). Получается, что петля работает дважды – раз между паблишером и SFU, раз между SFU и каждым подписчиком, – и кривой BWE-конфиг на даунлинке SFU способен убить опыт даже при здоровом аплинке.

Пейсер, наконец, превращает таргетный битрейт в реальные пакеты, распределённые по времени. Без пейсера энкодер 30 fps на 1 Mbps отправил бы все байты кадра одним burst'ом во время кадра, потом простоял 33 мс, потом снова burst. Такой burst-паттерн вреден для контроллера: он искусственно раздувает queueing delay и сбивает delay-based оценщик. Пейсер libwebrtc (в modules/pacing/) сглаживает исходящий rate, держа пакеты в очереди и выпуская их со скоростью таргета, – сеть видит ровный поток, а не всплески. В пейсере же сидит и логика probing – когда контроллер хочет пробить, именно пейсер впрыскивает дополнительные пакеты.

Что можно прочитать из `getStats()`

Каждая современная реализация WebRTC раскрывает внутренности петли через W3C-API RTCPeerConnection.getStats(). Релевантные поля – по W3C webrtc-stats Candidate Recommendation – лежат в репортах outbound-rtp и remote-inbound-rtp:

  • outbound-rtp.targetBitrate – текущая просьба контроллера к энкодеру. Это самое близкое к «оценке полосы» в одном числе.
  • outbound-rtp.totalEncodedBytesTarget – куммулятив таргета за звонок; скорость изменения – недавний таргет.
  • remote-inbound-rtp.fractionLost – доля потерь, которую получатель возвращает за последний интервал. Кормит loss-based оценщик.
  • remote-inbound-rtp.roundTripTime – сэмплы RTT по RTCP. Полезно для проверки реактивности контроллера.
  • candidate-pair.availableOutgoingBitrate – что контроллер готов отправлять прямо сейчас, включая запас на probing.

Если вы дебажите «почему картинка такая мыльная» – тащите targetBitrate и fractionLost из getStats() раз в секунду, постройте график, и ответ обычно проявляется в первые 30 секунд звонка. Таргет, повисший на 250 kbps на чистой сети, – обычно где-то слишком низкий min-bitrate. Таргет, прыгающий 2000 ↔ 400 kbps каждые пять секунд, – обычно loss-based реагирует на транзиентные всплески потерь. Таргет, плавно поднимающийся 300 → 1500 kbps за первые десять секунд и держащий – петля работает правильно.

Где это ломается в реальных деплоях

Петля надёжна в лаборатории и хрупка в полях. Самые частые продакшен-режимы отказа:

  1. Медленный старт. Стартовый таргет консервативен (libwebrtc по умолчанию 300 kbps на видео). Без эффективного probing – а оно требует чистого пути и нескольких RTT измерений – rate растёт медленно. Звонок, который должен ехать на 2 Mbps, через 30 секунд ползёт на 600 kbps. Решение: включить агрессивный probing в публикующем SDK и поднять min-bitrate, если это оправдано продуктом. (Фича LiveKit dynacast решает родственную проблему, паузя слои, на которые никто не подписан, и наращивая, только когда появляется подписчик, см. Bringing Zoom's end-to-end optimizations to WebRTC.)
  2. Переход Wi-Fi → сотовая сеть. Свойства активного пути меняются резко: latency удваивается, доступная полоса падает на порядок, bottleneck переезжает на радио. Реакция петли отстаёт на RTT плюс интервал фидбэка – обычно 100–200 мс, – за это время энкодер сильно переборщил. Итог – фриз на одну-две секунды. Митигация: ловить смену пути на уровне ICE (см. NAT, STUN, TURN, ICE в WebRTC) и превентивно опускать таргет, не дожидаясь фидбэка.
  3. Bufferbloat на бытовом роутере. Потребительский Wi-Fi-роутер с 500-мс FIFO-очередью способен поглотить много перешутов, прежде чем начнёт дропать пакеты – он маскирует перегрузку от loss-based, надувая одностороннюю задержку чудовищно. Delay-based ловит, но только когда очередь уже глубока – а как только она глубока, аудио трещит, потому что аудио-пакеты застряли позади видео-байт. Митигации сетевого уровня: AQM типа CoDel, или L4S/ECN сквозь весь путь. На уровне приложения – держать запас контроллера консервативно на сетях, про которые приложение знает, что они домашние.
  4. Кросс-трафик от параллельной TCP-закачки. TCP-поток (Netflix, YouTube, фоновое обновление) по тому же аплинку будет давить изо всех сил, пока WebRTC-контроллер сознательно отступает. WebRTC-rate падает, потому что GCC видит рост очереди; TCP-поток не видит ничего и продолжает. Результат: качество звонка обваливается, а закачка идёт. Это хорошо документированная проблема «real-time vs bulk fairness» и решения на протокольном уровне нет; требуется либо сетевое решение (та же AQM-история с L4S), либо продуктовое (предупредить пользователя поставить закачки на паузу).
  5. Асимметричные пути и не та сторона фидбэка. Фидбэк-пакеты идут путём, возможно отличным от прямого. Если фидбэк-путь забит – обратная связь приходит с задержкой, контроллер действует на устаревших данных. На практике редко, но случается на спутниковых аплинках и некоторых конфигурациях TURN-релея. Диагностика: растёт roundTripTime со стороны фидбэка, а availableOutgoingBitrate в прямом направлении выглядит нормально.
  6. Узкое место – даунлинк SFU, а паблишер ничем не помогает. Паблишер с толстым аплинком шлёт три simulcast-слоя на 2500 kbps в SFU. Подписчик на медленном даунлинке тянет только 700 kbps. Per-subscriber оценщик SFU роняет ему до нижнего слоя simulcast (180p на 150 kbps), и подписчик видит мыльный 180p, хотя в принципе 360p тоже паблишится. Решение: либо SFU c SVC вместо simulcast (любой подписчик может получить байты любого слоя), либо SDK паблишера, который пере-rate'ится под худшего подписчика. (Команды mediasoup, LiveKit, Janus и Jitsi Videobridge писали о вариантах этого размена; см. Сравнение SFU: mediasoup, Janus, LiveKit, Jitsi, Pion.)

Распространённая ошибка: тюнить maxBitrate энкодера, не тюня потолки контроллера

Частая мисконфигурация в WebRTC-продуктах – выставлять RTCRtpEncodingParameters.maxBitrate в высокое значение (5 Mbps для 720p simulcast-слоя), надеясь «дать звонку использовать больше полосы на хорошей сети». Контроллер не будет уважать этот потолок, пока сам не оценит, что сеть его потянет, так что на большинстве сетей энкодер всё равно режется оценкой контроллера, а не maxBitrate. Проблема всплывает на редкой сети, где оценка контроллера убегает вперёд реального bottleneck'а – и тогда энкодер ломится 5 Mbps в путь, который их не везёт, очередь распухает, аудио трещит, звонок разваливается. Лекарство – выставить maxBitrate в значение, соответствующее цели звонка (1500–2500 kbps для типичного 720p-конференца), и не пускать контроллер выше этого потолка. Поднимать потолок выше реальной потребности – напрашиваться на проблемы.

Фронтир 2026: машинное обучение, L4S и ИИ-агенты

Три изменения выкатываются в продакшен в этом и следующем году – по убыванию зрелости.

ML-augmented оценщики. Meta опубликовала Optimizing RTC bandwidth estimation with machine learning в начале 2024 года: описанный обученный контроллер развёрнут в видеозвонках Messenger и WhatsApp. Модель выдаёт тот же сигнал target-bitrate, что и GCC, но обучена на per-call телеметрии, а не вручную выставленных порогах. Команда сообщает о заметных приростах качества на длинном хвосте плохих сетей. Несколько вендорских стеков (LiveKit, Daily и др.) экспериментируют с похожими моделями; open-source libwebrtc пока на GCC. Стоит наблюдать.

L4S и ECN сквозь весь путь. L4S (RFC 9330–9332, январь 2023) – сетевое изменение, которое при развёртывании сквозь весь путь способно схлопнуть очередную задержку до почти нуля, не теряя ёмкости. Идея: queue-aware AQM на bottleneck'е маркирует ECN-CE на пакетах, когда очередь начинает расти, а не дропает; отправитель замедляется по метке, очередь не растёт. WebRTC-стеки начинают читать ECN-метки через фидбэк RFC 8888. Свежая ACM-статья Lag-Busting WebRTC: L4S-Enabled Adaptive Video Streaming демонстрирует L4S-enabled WebRTC-видео с заметно лучшим балансом задержки и качества. Подвох – L4S помогает, только если каждый хоп пути его поддерживает, а это медленный рост экосистемы.

ИИ-агенты и нагрузка «разговор от имени модели». Новая нагрузка, пришедшая массой в 2025-м, – голосовой LLM-агент: real-time WebRTC-сессия между человеком и server-side моделью, с low-latency аудио в обе стороны, часто и видеотреком. Профиль трафика агента отличается от человеческой конференции: всплески речи генерируются моделью и часто ровнее, паузы предсказуемые, а пути нередко идут между colocated дата-центром и резидентским потребителем. Несколько стеков начинают специализировать контроллер под этот workload; команда LiveKit писала об этом в анонсе LiveKit agents. Последствия для BWE в стандартах пока не выкристаллизовались, но в практике этот workload уже становится заметным драйвером новой работы по BWE.

Где Фора Софт в этом

Фора Софт делает WebRTC с 2010 года – когда WebRTC ещё был флагом в Chrome. В проектах по видеоконференцсвязи, телемедицине, e-learning, live-шопингу и AR/VR наши команды тюнили петлю BWE и контроля перегрузки на всех сетях, которые видели в продакшене: клиническом Wi-Fi за пятилетним роутером, мобильном LTE в регионах, где доминирующий тир ещё 3G, спутниковых каналах с сотнями миллисекунд базы, корпоративных сетях с замком, где только TURN-релейный путь переживает ICE. Паттерны выше – итог этих проектов. Когда качество звонка не то, обычно не то с петлёй; когда не то с петлёй, лекарство – один из шести режимов отказа. Относитесь к контроллеру как к первоклассному элементу продукта, а не как к унаследованному чёрному ящику.

Ключевые выводы

  • WebRTC гоняет собственный контроль перегрузки поверх UDP, потому что поведение TCP неправильно для real-time медиа.
  • Сигнал контроллера – время прихода пакета, возвращаемое в Transport-Wide CC Feedback (или legacy REMB, или RFC 8888 CCFB).
  • Google Congestion Control объединяет delay-based и loss-based; финальный таргет – минимум.
  • Probing находит потолок; без probing старт медленный и звонок не находит запаса.
  • SCReAM и NADA существуют; если ваши эндпоинты – браузеры, у вас всё равно GCC.
  • Энкодер, менеджер simulcast/SVC и пейсер превращают таргет в реальное поведение «на проводе».
  • RTCPeerConnection.getStats() раскрывает состояние петли – targetBitrate и fractionLost – основные поля для дебага.
  • Петля ломается на bufferbloat, Wi-Fi → cellular, кросс-трафике и асимметричных путях; митигация – продуктовая, не протокольная.

Что читать дальше

Строите такую систему?

Подберём параметры кодирования под ваш контент и посчитаем стоимость доставки до старта разработки.