Pro Features

WebRTC

Нагрузочное тестирование WebRTC через настоящий медиа-путь — ICE/DTLS-SRTP рукопожатия и RTP-потоки, WHIP/WHEP, кастомная сигнализация, P2P-звонки между VU

Обзор

Инструменты для HTTP/WS/gRPC останавливаются на уровне сигнализации — самая дорогая часть WebRTC-системы (DTLS-криптография, RTP-форвардинг, джиттер- буферы на вашем SFU или медиасервере) остаётся ненагруженной. perfscale гоняет полный медиа-путь: каждый виртуальный пользователь выполняет настоящие ICE/DTLS-SRTP рукопожатия и реальные RTP-потоки — против цели или против другого VU. Движок владеет медиа-плоскостью; сигнализация — либо встроенный WHIP/WHEP-клиент, либо кастомный протокол, делегированный wasm-библиотеке, — движок не обрастает кодом под каждого вендора.

WebRTC — платная возможность (тарифы Scale и Enterprise). Тесты с действиями pro/webrtc-* отклоняются при создании для тарифа Starter; в OSS-движке блок webrtc: или шаг pro/webrtc-* падает при загрузке с понятной ошибкой «требуется pro-модуль». Дизайн: RFC 007.

Три формы использования, комбинируемые в одном сценарии:

  • WHIP/WHEP — стандартизованные точки ingest/playback: публикуйте синтетическое или файловое AV в WHIP-эндпоинт, смотрите WHEP-поток и измеряйте его.
  • Кастомная сигнализация — LiveKit, mediasoup, Janus, проприетарные протоколы: wasm-библиотека (signal: library) получает SDP-оффер и возвращает ответ, поэтому вендорская специфика живёт в версионируемых библиотеках, а не в движке.
  • P2P-звонки между парами VU — один композитный шаг, pro/webrtc-call@v1, выполняет весь звонок: парность → connect → медиа в обе стороны → hold → stats → close.

Конфигурация

Опциональный верхнеуровневый блок webrtc: в config.yaml:

webrtc:
  ice_servers:                # STUN/TURN; по умолчанию: stun:stun.l.google.com:19302
    - urls: ["turn:turn.example.com:3478"]
      username: ${TURN_USER}
      credential: ${TURN_PASS}
  max_peer_connections: 500   # опциональный предохранитель; без ключа — без лимита

Периодичность сбора статистики (фоновый getStats против итоговых агрегатов) наследуется от конфигурации метрик запуска — модуль не добавляет своих настроек.

Действия

ДействиеНазначение
pro/webrtc-connect@v1Создать peer connection и завершить сигнализацию (signal: whip / whep / library); tracks: объявляет отправную раскладку (по умолчанию 1 аудио + 1 видео). Возвращает хендл для остальных шагов
pro/webrtc-publish@v1Прикрепить треки и начать отправку — несколько треков на одном соединении, Opus/VP8/H.264/AV1: source: synthetic (AV1 честно кодируется с запрошенным разрешением/битрейтом; VP8/H.264 проигрывают ассет 320x240) или source: file (IVF, Opus-in-Ogg, Annex-B H.264, по кругу); видеотреки могут объявлять simulcast-layers:
pro/webrtc-subscribe@v1Принимать удалённые треки; sink: measure считает кадры/пакеты, sink: record дополнительно сохраняет каждый трек per-VU (.ogg / .ivf / .h264); jitter_buffer_ms: применяет реальную задержку воспроизведения
pro/webrtc-stats@v1Мгновенный снимок getStats в outputs шага для проверок check:
pro/webrtc-close@v1Корректное закрытие с финальным сбросом статистики
pro/webrtc-call@v1Композитный P2P-звонок между парами VU (рандеву через общие переменные, сигнальный сервер не нужен)

Пример: P2P-звонки

steps:
  - name: p2p call
    use: pro/webrtc-call@v1
    with:
      pairing:
        driver: redis          # memory (по умолчанию) для одного агента; redis — между инстансами
        key: webrtc-room-1
        strategy: adjacent     # vu 2k-1 звонит vu 2k; 'custom' — своё правило
      media: bidirectional
      hold: 30s

Пример: WHIP ingest

steps:
  - name: publish camera
    use: pro/webrtc-connect@v1
    with:
      signal: whip
      url: https://stream.example.com/whip/cam-${vu}
      bearer: ${WHIP_TOKEN}
    outputs: cam
  - name: send media
    use: pro/webrtc-publish@v1
    with:
      id: ${cam.id}
      tracks:
        - kind: video
          codec: av1
          source: synthetic
          bitrate: 1500kbps
          resolution: 1280x720
        - kind: audio
          codec: opus
          source: synthetic
          bitrate: 64kbps

Справочник шагов

pro/webrtc-connect@v1

Создаёт peer connection и завершает сигналинг. Возвращает хендл соединения (rtc-1, …) через outputs: — модель живых соединений, как у std/ws-*.

ПараметрТипПо умолчаниюОписание
signalstring—whip | whep | library
urlstring—WHIP/WHEP-эндпоинт (только signal: whip|whep; токены ${…} раскрываются, напр. cam-${vu})
bearerstring—Bearer-токен эндпоинта (только signal: whip|whep)
library_callstring—Только signal: library: один токен ${alias.fn(args)}, указывающий функцию библиотеки, которая отвечает на SDP-оффер
trickleboolfalseДотекание ICE-кандидатов через WHIP PATCH (только WHIP/WHEP — library-сигналинг всегда non-trickle, кандидаты встроены в оффер); по умолчанию non-trickle (детерминированные метрики сетапа)
on_disconnectstringfail_fastfail_fast завершает последующие шаги при ICE disconnect; restart выполняет настоящий перезапуск ICE (см. ниже)
timeoutms10000Таймаут сигналинга и подключения (включая вызов библиотеки)
trackslist1 аудио + 1 видеоРаскладка отправляемых m-линий (только WHIP/library): [{kind, layers?}] — по одной записи на каждый трек, который прикрепит publish, с предобъявленными simulcast-rid для слоистых треков. См. «Мультитрек и simulcast» ниже

signal: library

Кастомный сигналинг (LiveKit, mediasoup, Janus, …) делегируется wasm-библиотеке: шаг сам строит SDP-оффер (трансиверы объявляются как send+receive, так что library-соединение умеет и публиковать, и подписываться), встраивает собранные ICE-кандидаты в него и вызывает функцию из library_call, передавая SDP-оффер первым аргументом, дальше — объявленные в токене аргументы (действует контракт отображения текст→JSON библиотек; токены ${…} внутри аргументов раскрываются первыми — работает identity=vu-${vu}). Библиотека возвращает SDP-ответ JSON-строкой {"sdp": "…", "type": "answer"}; кривой ответ завершает шаг ошибкой с именем вызова. library_call требует signal: library и наоборот; url/bearer/trickle с library отклоняются (всё необходимое библиотеке передавайте через аргументы вызова).

on_disconnect: restart

При ICE disconnect соединение выполняет настоящий перезапуск ICE вместо смерти: свежие ICE-креды (restart_ice), новый раунд сбора кандидатов и повторная сигнализация кредов — WHIP/WHEP отправляет PATCH на session-ресурс с ICE-restart-фрагментом application/trickle-ice-sdpfrag (форма перезапуска из драфта; если сервер не прислал Location при connect, session-ресурса нет и перезапуск невозможен), signal: library повторно вызывает функцию библиотеки с новым SDP-оффером. Трансиверы и треки переживают перезапуск, поэтому publish/subscribe продолжаются на новой ICE-сессии. На соединение выделяется максимум 3 попытки перезапуска (с паузой 500 мс, каждая ограничена timeout шага); если цель окончательно мертва, бюджет исчерпывается и последующие шаги падают ровно как при fail_fast. Успешные перезапуски учитываются в webrtc_ice_restarts_total и поле снапшота ice_restarts, а также пишутся в лог. pro/webrtc-call@v1 всегда работает в режиме fail_fast.

libraries:
  - use: ./livekit-signaling.wasm
    capabilities: []

steps:
  - name: join room
    use: pro/webrtc-connect@v1
    with:
      signal: library
      library_call: ${livekit.offer(room=loadtest, identity=vu-${vu})}
    outputs: room

pro/webrtc-publish@v1

Прикрепляет треки к соединению и начинает отправку.

ПараметрТипПо умолчаниюОписание
idstring—Хендл из connect (rtc-N)
trackslist—kind: audio|video, codec: opus|vp8|h264|av1, source: synthetic|file, bitrate, resolution (видео), интервал ключевых кадров

Синтетическое аудио — тон Opus с дизерингом амплитуды (кодер не уходит в DTX); синтетическое видео — движущийся тестовый паттерн с меткой времени в кадре. Что дают resolution:/bitrate:, зависит от кодека — крейт полностью на Rust, а чисто-Rust энкодер есть только для AV1:

Кодекsource: syntheticresolution: учитывается?
Opusвстроенный тонн/д
VP8, H.264встроенный тестовый паттерн, фиксированный 320x240 @ 15 fpsнет — значение проверяется и отражается в выводе, но ассет проигрывается как есть; при несовпадении пишется предупреждение (asset-bound 320x240 — resolution ignored)
AV1реально кодируется rav1e (чистый Rust) в запрошенном разрешении/битрейте, по умолчанию 320x240 @ 15 fpsда

Кодирование — честная цена AV1: каждый кодируемый синтетический трек стоит примерно одно ядро CPU, пока идёт медиа (пресет скорости 10, low-latency; на современном ноутбучном ядре это держит 15 fps примерно до 640x480 и проседает до ~6 fps на 720p — темп отправки в этом случае деградирует плавно, без всплесков).

С source: file трек зацикливает файл-образец (path:, относительно рабочей директории): IVF (VP8 или AV1 — FourCC AV01 принимается) и Annex-B H.264 (.h264, темп по fps:, по умолчанию 30) для видео, Opus-in-Ogg (.ogg) для аудио; кодек должен соответствовать контейнеру.

Мультитрек-публикация и simulcast layers:

Шаг publish может прикреплять несколько треков одного типа (две камеры плюс демонстрацию экрана, несколько микрофонов). WHIP/library-соединения предобъявляют отправляемые m-линии в оффере (в RFC 9727 нет перенегосиации), поэтому шаг connect принимает опциональный параметр tracks: — раскладку отправки: по одной записи {kind, layers?} на каждый трек, который прикрепит publish:

  - name: publish connect
    use: pro/webrtc-connect@v1
    with:
      signal: whip
      url: https://stream.example.com/whip/studio-${vu}
      tracks:                       # раскладка отправки; по умолчанию 1 аудио + 1 видео
        - { kind: audio }
        - { kind: video }           # камера
        - kind: video               # демонстрация экрана, simulcast
          layers: [ { rid: f }, { rid: h }, { rid: q } ]
    outputs: studio

Каждый трек публикации занимает один объявленный трансивер (совпадение по типу и набору rid); публикация сверх объявленной раскладки падает с ошибкой, указывающей на недостающее объявление. Вывод шага publish перечисляет все прикреплённые треки в tracks — {index, kind, codec, source, layers?} — рядом с человекочитаемыми строками published. На стороне подписчика каждый трек приходит отдельным remote-треком, и sink: record пишет по файлу на трек, как обычно.

Видеотрек может объявить layers: — simulcast: независимые кодирования одного источника на одной m-линии, различаемые по RID, — та же форма, что отправляет браузер через sendEncodings (оффер несёт строки a=rid:<rid> send + a=simulcast:send …, extmap SDES mid/rtp-stream-id и a=ssrc на каждый слой; пакеты в проводе помечены rid-расширением):

Ключ слояТипОписание
ridstringrid-id по RFC 8851 (1–16 букв/цифр), уникален в треке, слоёв ≥ 2
bitratebps/kbpsЦелевой битрейт слоя
resolutionWxHРазрешение слоя

Что дают bitrate/resolution слоя — та же чисто-Rust правда, что и на уровне трека: синтетические AV1-слои реально кодируются по слоям (по энкодеру rav1e на слой, в разрешении/битрейте слоя — ядро на слой); ассетные и файловые слои перекодировать нельзя, поэтому их содержимое — фиксированный ассет, а bitrate масштабирует темп (те же кадры, масштабированная частота кадров), resolution игнорируется с предупреждением. Это относится и к VP8, и к H.264, и к AV1 source: file.

Настоящий AV1 SVC (пространственные слои внутри ОДНОГО потока, режимы масштабируемости LxTy) не поддерживается: у rav1e — единственного чисто-Rust энкодера в стеке — нет API пространственных слоёв. Запросы scalability_mode:/svc: отклоняются с адресной ошибкой, указывающей на simulcast-слои, — без молчаливой подмены.

Особенность приёма (webrtc-rs 0.20.5): все слои simulcast m-линии приходят на один remote-трек — стек доставляет пакеты всех слоёв на трек m-линии и не предоставляет послойного демультиплексирования на приёме, поэтому выбор слоя на подписчике недоступен, а запись simulcast m-линии перемежает слои в одном файле. Потоки слоёв различимы по SSRC в getStats, а послойные метрики отправки точны (см. ниже).

pro/webrtc-subscribe@v1

Принимает удалённые треки и измеряет их.

ПараметрТипПо умолчаниюОписание
idstring—Хендл из connect
sinkstringmeasuremeasure считает кадры/пакеты; record дополнительно пишет каждый принятый трек на диск
record.dirstring—Каталог для sink: record (файлы: <шаг>-vu<vu>-track<idx>-<тип>.<ext>; Opus → .ogg, VP8/AV1 → .ivf, H.264 → .h264)
jitter_buffer_msmsвыкл. (пакеты доставляются по прибытии)Jitter-буфер на приёме: пакеты удерживаются до момента воспроизведения по расписанию RTP-таймстампов со смещением на эту задержку и отдаются в порядке sequence-номеров. Цена — рост TTFF/задержки на ту же величину (TTFF включает задержку). Применяется со следующего пакета, на всё соединение

pro/webrtc-stats@v1

Снимок getStats в outputs шага (совместим с проверками std/check@v1) и запуск фонового сэмплирования метрик качества медиа.

pro/webrtc-close@v1

Корректное закрытие с финальным сбросом статистики. Припаркованные соединения также закрываются в конце итерации VU.

pro/webrtc-call@v1

Композитный P2P-звонок — парность → connect → медиа в обе стороны → hold → stats → close, одним шагом. Пары VU находят друг друга через эфемерные ключи общих переменных (объявление shared_variables: не нужно); с driver: redis тот же тест масштабируется на несколько инстансов движка.

ПараметрТипПо умолчаниюОписание
pairing.driverstringmemoryДрайвер общих переменных для рандеву (memory — в пределах одного движка, redis — между инстансами)
pairing.keystring—Пространство имён рандеву; офферы/ансверы живут под <key>:offer|answer:<pair_id> с TTL
pairing.strategystringadjacentadjacent: vu 2k-1 звонит vu 2k; custom: вы вычисляете оба параметра сами
pairing.pair_idstring—только custom — id пары; ${vu} раскрывается
pairing.rolestring—только custom — offer или answer
mediastringbidirectionalПока только bidirectional — для односторонних звонков комбинируйте connect/publish/subscribe
holdduration—Длительность звонка перед stats + close (например, 30s)
timeoutms10000Дедлайн ожидания на рандеву (та же семантика, что у subscribe в std/pubsub@v1)
trackslistсинтетика: Opus-аудио + VP8-видеоПереопределение треков, тот же формат, что у pro/webrtc-publish@v1

Ошибки несут тег стадии (ice, dtls, signaling, publish, subscribe, hold) в outputs шага и в счётчиках webrtc_call_errors_<stage>. При нечётном числе VU старший нечётный VU остаётся без пары: он отправляет оффер и отваливается по таймауту — учитываемая ошибка стадии signaling, никаких молчаливых пропусков.

Метрики

Метрики сетапа и звонков, эмитируемые шагами (сворачиваются в сводку прогона):

  • webrtc_ice_duration_ms / webrtc_dtls_duration_ms / webrtc_setup_ms — HDR-гистограммы в мс (перцентили в сводке): согласование ICE, DTLS-рукопожатие и полное время подключения. Один сэмпл на успешное подключение, эмитируются pro/webrtc-connect@v1 и каждым плечом pro/webrtc-call@v1; упавшее подключение сэмпла не записывает (оно учитывается в webrtc_connect_errors).
  • webrtc_connect_total / webrtc_connect_errors — счётчики, всегда эмитируются как 1/0 или 1/1 на попытку подключения (при успехе и при ошибке), поэтому гейты webrtc_connect_errors: ["count==0"] разрешаются и на здоровых прогонах.
  • webrtc_ttff_ms — HDR-гистограмма в мс; время до первого кадра на подписывающей стороне, один сэмпл на успешную подписку (pro/webrtc-subscribe@v1 и композитные звонки). При заданном jitter_buffer_ms задержка буфера включена в сэмпл — это честная цена поглощения джиттера.
  • webrtc_tracks_published_total / webrtc_subscriptions_total / webrtc_connections_closed_total — шаговые счётчики от шагов publish / subscribe / close (прикреплённые треки, начатые подписки, закрытые соединения).
  • webrtc_packets_sent_total / webrtc_packets_received_total / webrtc_ice_restarts_total — счётчики итогов соединения, эмитируемые шагами stats и close (финальный сброс при закрытии, так что припаркованные соединения тоже отчитываются); webrtc_ice_restarts_total также сэмплируется вживую медиа-сэмплером ниже.

Качество медиа (фоновый getStats-сэмплер, раз в stats-интервал, пока соединение открыто — не по шагам):

  • webrtc_{audio,video}_rtt_ms / webrtc_{audio,video}_jitter_ms / webrtc_{audio,video}_bitrate_bps — сэмплы HDR-гистограмм на каждый тик (битрейты вычисляются из приращений байтов за тик).
  • webrtc_{audio,video}_packets_lost_total / webrtc_frames_decoded_total / webrtc_ice_restarts_total — счётчики, пополняемые приращениями за тик.

Поскольку сэмплер — не вызов шага, производных *_failed у этих серий нет — у качества медиа нет «вызова», который мог бы упасть. А вот шаговые гистограммы выше их имеют: webrtc_setup_ms_failed, webrtc_ttff_ms_failed, webrtc_call_duration_ms_failed (один сэмпл 0/1 на вызов, эмитировавший сэмпл — см. metrics).

Композитные P2P-звонки:

  • webrtc_calls_total — счётчик, по единице на шаг pro/webrtc-call@v1, успех или ошибка.
  • webrtc_call_duration_ms — HDR-гистограмма в мс; полная длина звонка (оффер → hold → teardown), только успешные звонки.
  • webrtc_call_errors_{ice,dtls,signaling,publish,subscribe,hold} — счётчики по стадиям; упавший звонок увеличивает ровно ту стадию, на которой погиб. Это счётчики причин ошибок — другой сигнал, чем производные *_failed: счётчики говорят, какая стадия сломалась (и существуют, только когда эта стадия уже падала), а rate вроде webrtc_setup_ms_failed измеряет, как часто падают вызовы, и именно его оценивают rate-гейты std/thresholds@v1. Ограничивайтесь по обоим: webrtc_setup_ms_failed: ["rate<0.05"] для SLO, webrtc_call_errors_ice: ["count==0"], чтобы привязать регрессию к стадии.

Потрековые серии отправки (мультитрек и simulcast):

  • webrtc_<тип><порядковый>_* — опубликованный трек получает порядковый номер публикации внутри типа: webrtc_video0_bitrate_bps, webrtc_video1_bitrate_bps, webrtc_audio0_bitrate_bps, плюс …_packets_sent_total. Эмитируются фоновым сэмплером (сэмплы гистограмм / приращения счётчиков за тик). Агрегатные серии по типу трека выше неизменны и смешивают все треки типа.
  • webrtc_<тип><порядковый>_<rid>_* — послойные серии simulcast-трека добавляют rid: webrtc_video1_f_bitrate_bps, webrtc_video1_h_packets_sent_total. По серии на объявленный слой.
  • Вывод шагов stats/close несёт ту же разбивку в send_tracks ({name, layers: [{rid, packets_sent, bytes_sent}]}) для check:-проверок.

Дальше