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-*.
| Параметр | Тип | По умолчанию | Описание |
|---|---|---|---|
signal | string | — | whip | whep | library |
url | string | — | WHIP/WHEP-эндпоинт (только signal: whip|whep; токены ${…} раскрываются, напр. cam-${vu}) |
bearer | string | — | Bearer-токен эндпоинта (только signal: whip|whep) |
library_call | string | — | Только signal: library: один токен ${alias.fn(args)}, указывающий функцию библиотеки, которая отвечает на SDP-оффер |
trickle | bool | false | Дотекание ICE-кандидатов через WHIP PATCH (только WHIP/WHEP — library-сигналинг всегда non-trickle, кандидаты встроены в оффер); по умолчанию non-trickle (детерминированные метрики сетапа) |
on_disconnect | string | fail_fast | fail_fast завершает последующие шаги при ICE disconnect; restart выполняет настоящий перезапуск ICE (см. ниже) |
timeout | ms | 10000 | Таймаут сигналинга и подключения (включая вызов библиотеки) |
tracks | list | 1 аудио + 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
Прикрепляет треки к соединению и начинает отправку.
| Параметр | Тип | По умолчанию | Описание |
|---|---|---|---|
id | string | — | Хендл из connect (rtc-N) |
tracks | list | — | kind: audio|video, codec: opus|vp8|h264|av1, source: synthetic|file, bitrate, resolution (видео), интервал ключевых кадров |
Синтетическое аудио — тон Opus с дизерингом амплитуды (кодер не уходит в
DTX); синтетическое видео — движущийся тестовый паттерн с меткой времени
в кадре. Что дают resolution:/bitrate:, зависит от кодека — крейт
полностью на Rust, а чисто-Rust энкодер есть только для AV1:
| Кодек | source: synthetic | resolution: учитывается? |
|---|---|---|
| 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-расширением):
| Ключ слоя | Тип | Описание |
|---|---|---|
rid | string | rid-id по RFC 8851 (1–16 букв/цифр), уникален в треке, слоёв ≥ 2 |
bitrate | bps/kbps | Целевой битрейт слоя |
resolution | WxH | Разрешение слоя |
Что дают 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
Принимает удалённые треки и измеряет их.
| Параметр | Тип | По умолчанию | Описание |
|---|---|---|---|
id | string | — | Хендл из connect |
sink | string | measure | measure считает кадры/пакеты; record дополнительно пишет каждый принятый трек на диск |
record.dir | string | — | Каталог для sink: record (файлы: <шаг>-vu<vu>-track<idx>-<тип>.<ext>; Opus → .ogg, VP8/AV1 → .ivf, H.264 → .h264) |
jitter_buffer_ms | ms | выкл. (пакеты доставляются по прибытии) | 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.driver | string | memory | Драйвер общих переменных для рандеву (memory — в пределах одного движка, redis — между инстансами) |
pairing.key | string | — | Пространство имён рандеву; офферы/ансверы живут под <key>:offer|answer:<pair_id> с TTL |
pairing.strategy | string | adjacent | adjacent: vu 2k-1 звонит vu 2k; custom: вы вычисляете оба параметра сами |
pairing.pair_id | string | — | только custom — id пары; ${vu} раскрывается |
pairing.role | string | — | только custom — offer или answer |
media | string | bidirectional | Пока только bidirectional — для односторонних звонков комбинируйте connect/publish/subscribe |
hold | duration | — | Длительность звонка перед stats + close (например, 30s) |
timeout | ms | 10000 | Дедлайн ожидания на рандеву (та же семантика, что у subscribe в std/pubsub@v1) |
tracks | list | синтетика: 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:-проверок.
Дальше
- WebRTC из CLI — первый запуск по шагам, от установки до сводки метрик
- Нагрузочное тестирование WebRTC, шаг за шагом — полный туториал: P2P-звонки, simulcast, резиленс, CI-гейты
- Библиотеки — SDK wasm-библиотек за
signal: library - Общие переменные — рандеву за
pro/webrtc-call@v1, включая Redis-драйвер