Видеозвонок в браузере давно стал рутиной уровня электронной почты. Открыл вкладку, разрешил камеру и микрофон, и разговор с человеком за тысячи километров идёт как с соседом по кабинету. За этой простотой прячется стек протоколов, который за секунды договаривается о маршруте, шифровании, кодеке и битрейте, а потом непрерывно подстраивается под капризы сети. Пока цепочка работает, человек о технологии не вспоминает. Ломается одно звено, и картинка рассыпается в мозаику ровно на ключевом слайде, а голос превращается в робота, договаривающего фразу за миг до её растворения.

Знакомо? С этим сталкивался каждый, кто вёл вебинар или созванивался с коллегой из глубинки. Оптимизация браузерной видеосвязи это работа по всей цепочке: маршрут, кодеки, подстройка под канал, серверная топология и мониторинг. Звенья переплетены: дешёвый по процессору кодек освобождает ресурсы под симулкаст, география TURN сокращает круг задержки, точные метрики getStats подсказывают, где именно крутить гайки. Дальше цепочка разобрана по звеньям, с кодом и числами.

Маршрут пакетов через ICE STUN и TURN и цена каждого лишнего хопа

Медиа в WebRTC ездит по протоколу SRTP поверх UDP и по умолчанию напрямую между браузерами. Прямой путь экономит задержку, но между двумя домашними роутерами почти всегда стоит NAT, а иногда и корпоративный брандмауэр. Задачу решает ICE, фреймворк поиска работоспособного маршрута. Браузер собирает кандидатов: локальные адреса host, внешние адреса, подсвеченные через STUN, и ретрансляционные адреса на TURN-сервере, которые работают даже там, где остальные пути заблокированы. Описание сессии SDP уходит сигнальным каналом, обычно WebSocket поверх TLS, затем пары кандидатов проверяются STUN-запросами и выбирается лучшая. Рукопожатие DTLS выдаёт ключи для шифрования медиапотока: шифрование в WebRTC часть транспорта, а не опция.

Оптимизация начинается с географии. TURN это страховка, но плата за неё измеряется десятками миллисекунд: лишний хоп удлиняет путь, и TURN за океаном ломает перезапросы потерянных пакетов. Практичное правило: TURN и медиасервер держать в тех регионах, где живут пользователи, TURN по UDP предпочитать TURN по TCP, а вариант с TLS поверх TCP беречь для корпоративных сетей, где UDP задушен. При смене Wi-Fi на мобильную сеть соединение умирает, и здесь выручает мягкий перезапуск маршрута без пересоздания звонка:

const pc = new RTCPeerConnection({
  iceServers: [
    { urls: "stun:stun.example.net:3478" },
    {
      urls: "turn:turn-fra.example.net:3478?transport=udp",
      username: "conf",
      credential: "uW9xQp2m"
    }
  ],
  bundlePolicy: "max-bundle"
});

pc.addEventListener("connectionstatechange", () => {
  if (pc.connectionState === "failed") {
    pc.restartIce();
  }
});

Выбор кодеков VP8 VP9 AV1 H.264 и Opus под устройства и сеть

Стандарт требует от браузеров обязательный минимум: видео VP8 и H.264 в профиле Constrained Baseline, звук Opus и ветеран G.711. VP9 поддержан в Chrome с 48-й версии и экономит 30-40% битрейта при том же качестве, AV1 сжимает ещё сильнее и доступен в Chrome начиная с 113-го выпуска и в свежих Firefox, а HEVC в вебе только-только появляется. Safari опирался на аппаратный H.264 и постепенно подтянул VP8 и VP9. Отсюда вывод: телефону с аппаратным кодером H.264 не нужны программные экзерсисы, они сажают батарею, а десктоп с Chrome способен отдать AV1 и сэкономить канал. Узнать, что умеет браузер, можно через RTCRtpSender.getCapabilities, а приоритет кодеков задаётся вручную через setCodecPreferences на трансивере.

Ориентиры по битрейту для VP8 примерно такие: 180p на 150-200 кбит/с, 360p на 500-800, 720p на 1,5-2,5 Мбит/с, 1080p на 3-4,5 Мбит/с. VP9 и AV1 дают ту же картинку примерно на треть дешевле, но платой становится нагрузка на процессор, и на слабых машинах экономия канала оборачивается потерей кадров. Со звуком всё интереснее: Opus справляется с речью на 24-64 кбит/с и ценится дороже видео. Режим DTX перестаёт платить за тишину: микрофон молчит, и в канал уходит лишь лёгкая метка присутствия. Для плохих сетей у Opus есть резервирование RED, дублирующее звук в соседних пакетах, и там, где видеоряд уже разваливается, речь продолжает пробиваться. Когда канал тонет, первым за борт улетает разрешение, а не голос. Экранная трансляция имеет свой характер: подсказка contentHint со значением detail или text велит кодеру беречь резкость букв и мириться с низким FPS, а значение motion нужно живому видео со сцены.

Симулкаст и SVC когда участники с разным каналом смотрят одну встречу

Классическая ловушка конференции: у одного участника оптика на гигабит, у второго LTE на грани, у третьего отельный Wi-Fi. Отправитель не знает, у кого какой канал, и угодить всем одним потоком не может. Первое решение, симулкаст, заставляет браузер кодировать две-три версии одного видео: полную рамку на 2,5 Мбит/с, половинную на 700 кбит/с и четвертную на 160 кбит/с. Слои помечаются rid-идентификаторами, а сервер выбирает каждому участнику версию по его каналу. Плата: процессор кодирует три потока вместо одного, аплинк несёт сумму слоёв.

const tx = pc.addTransceiver(cameraTrack, {
  direction: "sendonly",
  sendEncodings: [
    { rid: "f", maxBitrate: 2500000, scaleResolutionDownBy: 1 },
    { rid: "h", maxBitrate: 700000, scaleResolutionDownBy: 2 },
    { rid: "q", maxBitrate: 160000, scaleResolutionDownBy: 4 }
  ]
});

Альтернатива с одним потоком называется SVC, масштабируемым кодированием. Кодер упаковывает видео в слои по разрешению и по частоте кадров, и сервер, не расшифровывая поток, отбрасывает верхние слои тем, кому они не по карману. Так умеют работать VP9 и AV1 в свежих Chrome, и такие потоки умеют пересылать серверные движки вроде mediasoup и LiveKit. Выигрыш по процессору заметен: один проход кодирования вместо трёх, а временные слои дополнительно сглаживают картинку при кратких провалах сети. Но экосистема SVC пока концентрируется вокруг Chrome, тогда как симулкаст работает во всех браузерах, включая мобильные Safari, поэтому симулкаст остаётся разумным умолчанием, а SVC подключают точечно, когда парк устройств известен. Отдельная головная боль больших комнат это ключевые кадры: новый участник входит, сервер запрашивает полный кадр, и просадка на секунду случается у всех. Лечится это регулировкой частоты таких запросов и политикой, по которой полный слой достаётся только активному спикеру.

Контроль перегрузки сети и восстановление потерь через GCC NACK FEC и джиттер-буфер

Канал не бывает ровным: Wi-Fi дышит, LTE мигрирует между вышками, сосед по роутеру качает обновление системы. WebRTC удерживает качество петлёй обратной связи: алгоритм GCC следит за ростом задержки в очередях и за потерями, собирает рапорты transport-cc от получателя и подаёт кодеру целевой битрейт. Кодер поднимает битрейт осторожно и роняет быстро, поэтому первые секунды встречи у всех "мыльные": это прогрев, а не поломка, и лечится заблаговременным соединением.

Потери собираются целым набором инструментов, и работают они сообща. NACK просит переотправить конкретный пакет, быстрый и дешёвый при малом круге задержки. FEC добавляет избыточные пакеты, из которых получатель восстанавливает утраченное без обратных вопросов. Аудио дополнительно страхуется резервированием RED, а джиттер-буфер амортизирует неровности прихода пакетов, удерживая 20-80 мс запаса и подстраивая глубину под поведение сети. Когда битрейт уже не спасает, кодеру нужен приоритет: параметр degradationPreference говорит, что беречь. Камере подходит balanced, экрану с текстом нужен maintain-resolution, живой сцене важнее maintain-framerate. Практичные пороги выглядят так: потери до 2% переживаются молча, от 2 до 5% включают усиленное резервирование и лёгкое снижение слоя, выше 5% честнее уронить разрешение и запустить поиск маршрута заново. Числа не догма, а ориентир под свои метрики.

Топология конференции полный меш SFU или MCU и что выбрать для масштаба

Раскладка первая, полный меш: браузеры соединяются каждый с каждым и отправляют соседям свой поток. Исходящий канал растёт линейно с числом участников: для троих терпимо, для пятерых домашний аплинк уже проседает. Раскладка вторая, SFU, selective forwarding unit: каждый отправляет на сервер один поток, а получает столько, сколько участников показывает, и сервер ничего не расшифровывает и не перекодирует, только сортирует пакеты. Раскладка третья, MCU, микширует всё в один составной кадр на сервере: дорого по процессору, добавляет задержку, в браузерных конференциях схема почти сошла со сцены.

Готовые SFU-движки покрывают почти любую задачу. mediasoup даёт Node.js API поверх производительного воркера на C++ и ценится за тонкую настройку симулкаста и слоёв. LiveKit написан на Go поверх библиотек Pion и поставляет серверы с SDK для всех платформ. Janus, надёжный сервер на C, держит комнаты в плагине videoroom. Jitsi Videobridge на Java входит в связку Jitsi целиком. Pion это библиотеки WebRTC на Go, из которых собирают серверы любой формы. Экономика масштаба считается просто: десять участников по 2,5 Мбит/с это четверть гигабита от одного сервера на комнату, и разумная топология не про мощное железо, а про меньше лишних миль и байт. Отсюда региональные пулы, каскадирование серверов между дата-центрами, TURN рядом с SFU, а в интерфейсе здоровый компромисс: полный слой активному спикеру, остальным плиткам достаточно 180p.

Мониторинг через getStats какие метрики выдают реальное качество звонка

getStats это слепок состояния соединения, и в нём есть почти всё. Тип outbound-rtp об исходящем видео: framesPerSecond, bytesSent, framesEncoded. Тип inbound-rtp о входящем: jitter, packetsLost, framesDropped, а в свежих редакциях стандарта и счётчик замираний freezeCount. Пара кандидатов candidate-pair держит currentRoundTripTime и оценку доступного исходящего битрейта. Отдельный подарок это qualityLimitationReason: кодер сам признаётся, почему снизил качество, из-за процессора или из-за канала, и одна метрика заменяет час споров в чате поддержки. Опрос раз в одну-две секунды, агрегация на бэкенде, и перед глазами дашборд с перцентилями: оптимизировать надо хвосты, страдает конкретный человек, а не статистика. Для разбора единичного случая незаменим отладчик Chrome по адресу chrome://webrtc-internals с полной историей соединения, графиками битрейта и потерь.

setInterval(async () => {
  const report = await pc.getStats();
  for (const s of report.values()) {
    if (s.type === "outbound-rtp" && s.kind === "video") {
      console.log(s.framesPerSecond, s.qualityLimitationReason);
    }
    if (s.type === "candidate-pair" && s.state === "succeeded") {
      console.log(s.currentRoundTripTime, s.availableOutgoingBitrate);
    }
  }
}, 2000);

Практические приёмы оптимизации от географии TURN до аппаратных кодеков и экранного contentHint

Мелочи, из которых складывается репутация "у них звонки работают": предварительное соединение до входа в комнату, активный спикер на полном слое, экранная трансляция с contentHint под характер содержимого, DTX и резервирование для звука, штатные шумодав, автогейн и подавление эха из getUserMedia, отключать их без причины не стоит. На мобильных отдельно живут радиомодули: встреча, переезжающая с LTE на Wi-Fi, это стресс-тест, и перезапуск маршрута должен отрабатывать сам, без просьбы перезайти. Проверять всё это нужно под плохим каналом, с эмуляцией потерь и задержек: на офисной гигабитной сети стабильно работает всё, даже сломанное. Весь путь оптимизации сходится в короткую последовательность шагов:

  1. собрать метрики getStats на реальных встречах и искать хвосты, а не средние;
  2. выбрать матрицу кодеков под парк устройств и отдать мобильным аппаратный H.264;
  3. включить симулкаст с тремя слоями и точечно опробовать SVC на Chrome;
  4. разместить TURN и SFU рядом с пользователями, а не за океаном;
  5. настроить NACK, FEC и резервирование звука с порогами деградации;
  6. построить мониторинг с алертами по RTT, потерям и замираниям;
  7. проверять каждое изменение A/B-сравнением на живом трафике.

Чудесной настройки в видеосвязи не существует. Кодек без налаженного маршрута слеп, маршрут без метрик одноразовый, метрики без продуманной топологии указывают не туда. Стабильно работающая вкладка с конференцией это цепочка, собранная целиком, от первого STUN-запроса до графика freezeCount, и именно собранность всей цепочки отличает продукт, в который не приходится перезаходить, от сервиса с вечным "плохо с интернетом".