v0.1·real-time media runtime·rooms · video · voice · stt

Медиа-рантайм для real-time звонков.WebRTC, voice DSP, N:1 STT.

Pion SFU, H.264 1080p @ 30fps, Opus 256 kbps c DSP в AudioWorklet, N:1 mixed transcription. SDK, HMAC-webhook'и, OpenAPI.

rooms
webrtc pion
sfu · rtp fanout · ice/turn
video
h.264 1080p30
single-layer · screen on dedicated pc
voice
opus 256 kbps
audioworklet dsp · fec · no dtx
transcription
n → 1
weighted mixer · <5 ms dsp
01Runtime surface
Rooms
Pion SFU
WebRTC-комнаты на Pion. RTP fanout, adaptive tier ladder для screen share, per-peer loudness нормализация.
Video
H.264 · 1080p @ 30fps
Single-layer камерный энкодинг, screen share на выделенной PeerConnection — до 25 Mbps @ 120 fps.
Voice
Opus 256 · AudioWorklet DSP
Opus 256 kbps stereo · AudioWorklet expander + slow loudness normalizer · пресеты studio / conversation / speakerphone.
Sessions
Lifecycle · identity · auth
Устойчивый reconnect через externalUserId. JWT join-токены, JWKS, internal secret для server-to-server.
Transcription
N:1 mixed STT
Weighted mixer → один provider-stream на комнату. Soniox · ElevenLabs · Gladia — смена провайдера конфигом.
Events
HMAC-signed webhooks
room.created / track.published / participant.left / transcript.final. Подпись SHA-256, retry, idempotency.
SDK
@relayrooms/js · OpenAPI · Go
JavaScript + React hooks, типизированный WebSocket протокол, Go / Python клиенты.
02Audio

Минимум деструктивной DSP на тракте.

AEC опционально, NS/AGC выключены там, где они портят тембр. Заменены на свой мягкий expander и медленный loudness normalizer в AudioWorklet. Opus 256 kbps VBR + FEC без DTX. SFU ничего не перекодирует.

Studio-grade audio

Звук, ради которого приходят из продакшна.

Opus 256 kbps stereo, VBR, FEC, без DTX. Никакого перекодирования на сервере — SFU форвардит RTP бит-в-бит. Браузерный DSP выключаем там, где это портит тембр, и заменяем собственным мягким expander'ом и медленным loudness normalizer'ом на AudioWorklet. На приёмной стороне каждый удалённый участник проходит ещё один normalizer — тихий собеседник не остаётся тихим.

256kbps
Opus VBR
48kHz
full-band
−18dBFS
target RMS
<5ms
worklet DSP
studiocapture spec
Scene
Подкаст, интервью, музыкальная сессия. Качественный микрофон + закрытые наушники.
Echo cancellation
Off
Noise suppression
Off
Auto gain control
Off — заменён нашим loudness normalizer
Channels
Stereo · 2 ch
Sample rate
48 000 Hz
Expander threshold
-58 dBFS · ratio 2:1
Loudness target
−18 dBFS RMS · 400 ms window
Loudness floor
-52 dBFS · ниже — удерживаем gain
Opus stereo fmtp
stereo=1 · sprop-stereo=1
sender.workletAudioWorklet · rr-audio-processor
0101 · raw capturegetUserMedia · no DSPexpander · ratio 2:1 · thr -58 dB0202 · soft downward expandertransparent above thresholdloudness · target −18 dBFS · 400 ms RMS0303 · slow loudness normalizer3/6 dB·s · cap +12/−6 dB · floor guard→ Opus encoder · sendonly transceiver
opus.sdpa=fmtp rewritten before setLocalDescription
a=rtpmap:111 opus/48000/2
a=fmtp:111 minptime=10;
            useinbandfec=1;
            usedtx=0;
            stereo=1;
            sprop-stereo=1;
            maxaveragebitrate=256000;
            maxplaybackrate=48000;
            cbr=0
a=sender.priority          high
a=sender.networkPriority   high
a=track.contentHint        music
minptime10
smallest ptime → low latency
useinbandfec1
recover short losses без NACK
usedtx0
без comfort noise на тишине
stereo1
2-канальный encode
sprop-stereo1
hint для decoder
maxaveragebitrate256000
Opus rate-controller ceiling
maxplaybackrate48000
full-band · 20 kHz
cbr0
VBR → лучшее качество на бит
receive.perPeerNormalizerslow makeup gain · 1 s window · 2/3 dB·s
problem · pre-normalize

У всех публикующих RMS идеален на отправке. У тебя в ухе — всё равно разный уровень. Причины: разные микрофоны, концил-параметры jitter-буфера, разное расстояние до капсюля. Решаем на клиенте — отдельный worklet на каждого удалённого peer'а.

  • window · 1000 ms
  • target · −18 dBFS
  • cap · +9 / −6 dB
  • attack · 2 dB·s · release · 3 dB·s
  • floor · −55 dBFS (не гонимся за шумом)
equalised
anna
marc
liz
yuri
raw
after normalizer
parameters.matrixsender worklet · per preset
param
studio
conversation
speakerphone
expanderThresholdDb
−58
−55
−50
expanderRatio
2.0:1
2.0:1
2.5:1
expanderAttackMs
5
5
5
expanderReleaseMs
200
200
250
loudnessWindowMs
400
400
400
loudnessTargetDbfs
−18
−18
−18
loudnessMaxGainDb
+12
+12
+12
loudnessMinGainDb
−6
−6
−6
loudnessAttack dB·s
3
3
3
loudnessRelease dB·s
6
6
6
loudnessFloorDbfs
−52
−50
−48
03Video

Один энкодер — один поток.

1080p @ 30 fps без simulcast'а: CPU тратит всё на единственный outbound layer. Screen share — на выделенной PC, с адаптивным тройным ladder'ом до 25 Mbps.

Studio-grade video

1080p камера. Выделенный PC под screen share.

Одна энкодинг-линия на камеру вместо трёх — CPU тратит все циклы на единственный поток, который реально уходит в сеть. H.264 в приоритете как самый стабильный real-time кодек. Screen share живёт на отдельной PeerConnection, чтобы не делить битрейт с камерой и не гасить LED-индикатор при остановке шеринга.

1080p30 fps
camera ideal
4Mbps
cam ceiling
25Mbps
screen · high
120fps
screen max
camera.profilesingle-layer · 1080p

3-слойный simulcast на consumer-железе заставляет CPU держать два лишних энкодера параллельно, и top-layer (f) проседает по битрейту независимо от maxBitrate — удалённые видят 240p-feel. Single-layer отдаёт все такты одной линии, которая реально уходит в сеть.

Resolution
1920 × 1080 · ideal · fallback 640+
Framerate
30 fps · max 30
Encoding
1 layer · no simulcast
maxBitrate
4 Mbps @ 1080p · 2.5 Mbps @ 720p
degradationPreference
maintain-resolution
Codec order
H.264 > VP8 > VP9
codec.preferencesetCodecPreferences · before SLD

H.264 — самый стабильный real-time видео-кодек в браузерах. VP8 остаётся доступен как mid-priority fallback, VP9 замыкает список — peer, который предлагает только VP9, всё равно подключится.

rank 1
H.264
preferred
rank 2
VP8
fallback
rank 3
VP9
last resort

Adaptive screen share · 3 tier ladder

ceiling from source fps · encoder-feedback driven
tier 1
high
120 fps
motion · maintain-framerate
max
25 Mbps
min
8 Mbps
content
motion
fps src
≥ 90

60 / 120 Hz source. Игра, анимация, демки с плавным движением. Уронит разрешение раньше, чем fps.

tier 2
balanced
60 fps
detail · maintain-resolution
max
10 Mbps
min
4 Mbps
content
detail
fps src
≥ 45

IDE, документы, дизайн-инструменты. Плавность 60 Hz, но fps просядет первым, чтобы текст не плыл.

tier 3
low
30 fps
data-saver
max
2.5 Mbps
min
800 kbps
content
detail
fps src
< 45

Узкий канал или слабый CPU. Ниже этого — ухудшается читаемость.

publisher.topologyone PC per purpose · Meet / LiveKit style

Камера и микрофон живут на общей publish PeerConnection — добавление камеры после join'а идёт renegotiation'ом, а не новым PC. Screen share — отдельная publish PC, так что старт/стоп шеринга не трогает камеру, LED-индикатор остаётся честным, и пропадает целый класс гонок с track.enabled хаками.

Publish PCmic + camera · single transceiver setScreen PCdedicated · adaptive tier ladderrr-worker SFUPion · RTP forward · no decodeopus + h264 · 1 m=audio · 1 m=videoh264 · content=motion / detailrenegotiateisolatedwebhook.track.published on first RTP · one publish flow per purpose
04STT input quality

Один моно-вход в провайдер STT.

N источников → один stream. До суммирования каждый канал проходит HPF, VAD, шумовой гейт и AGC; в микс попадают только активные кадры. Итог: ниже бил у провайдера и меньше misrecognition'ов — в API не уходит наложение из двух шумящих дорожек.

Per-channel DSP before sum

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

Стандартный путь — отдельный STT-стрим на каждого участника, провайдер борется с фоновым шумом на каждом. Здесь каналы сначала чистятся по отдельности (HPF, VAD, gate), взвешиваются по активности, суммируются в моно 16 kHz, проходят limiter — и только тогда уходят в API. В микс попадают только активные кадры говорящего; тишина остальных не суммируется.

  • · hypotheses cleaner → меньше misrecognition'ов
  • · fewer active frames → ниже билл у провайдера
  • · speaker diarization идёт из наших VAD-событий
N:1 mixer·STT-grade · live
48 → 16 kHz·HPF · VAD · gate · AGC·< 5 ms
alex
-∞
maria
-∞
dan
-∞
irina
-∞
sergey
-∞
Mixed · master
Transcript
awaiting signal…
05STT DSP pipeline

Десять стадий до первого токена.

~3000 строк production Go на rr-stt. End-to-end DSP ниже 5 мс. Каждая стадия направлена на то, чтобы провайдеру пришёл чистый, моно, 16 kHz PCM без шумов, без клиппинга, только с речью активных говорящих.

01

RTP decode

Opus / G.711 → PCM

Разбор RTP-пакетов. Sequence, timestamp, SSRC, payload. Декодирование opus во float-сэмплы на 48 кГц.

V=2PXCCMPT=111SequenceTimestampSSRCOpus payload . . .rtpdecodepcm48 kHz · s16le
02

Jitter buffer

adaptive · clock-driven

Адаптивный буфер компенсирует сетевые колебания. Неровный вход → ровный выход, пропущенные пакеты интерполируются.

inbufdepth · adaptiveout
03

Resample

48 kHz → 16 kHz · FIR

Polyphase FIR-децимация с anti-aliasing. 3:1 downsampling, Kaiser-window, фильтр в 128 tap.

48 kHz · 72 samples÷3 · FIR16 kHz · 24 samplesKaiser window · 128 tap
04

High-pass

85 Hz · 2nd order

Biquad HPF. Срез низкочастотного гула, шума кондиционера, рокота. Наклон −12 дБ/октаву ниже среза.

0-6-12-18-2420501002005001k2k5k10k20kfc = 85 Hzgain · dBfrequency · Hz · log
05

VAD

voice activity detection

Обрабатываем только фреймы с реальной речью. Тишина не идёт в STT — экономим API-вызовы и шум.

signalgateonoff
06

Noise gate

per-channel · hysteresis

Гейт на каждом канале с гистерезисом. Фоновый шум не суммируется от всех N участников.

opencloseinout
07

AGC

automatic gain control

Нормализация громкости по каждому участнику. Тихий и громкий приходят на микс с одинаковой энергией.

input · rmsP1P2P3P4P5gain · ×× 2.80× 0.82× 1.56× 1.08× 4.67output · normalizedP1P2P3P4P5
08

Weighted N:1 mixer

activity-driven weights

N каналов → моно-поток. Активный говорящий получает вес в миксе, пассивные участники затухают.

P137%P218%P322%P417%P56%Σ
09

Limiter

look-ahead · −1 dBFS

Look-ahead лимитер на мастере. Суммирование N каналов не уводит уровень в клиппинг.

−1 dBFStransfer · out vs ininputwaveform · raw vs limited
10

STT

soniox · gladia · elevenlabs

Один provider-stream на комнату вместо N. Смена провайдера — конфиг, не код.

melspectrogram · 40 ms hopstranscript · streaming
06Integration modes
Full stackmode
Client → RelayRooms SFU → DSP → STT → Transcript

Полный медиа-рантайм. Комнаты, участники, транскрипция в одной системе. Для новых голосовых продуктов.

const room = await rr.rooms.create({
  stt_provider: "soniox",
  webhook_url: "https://app/hook"
});
const client = new RelayRoom(room.join_url, room.token);
client.on("transcript", s => console.log(s.text));
await client.connect();
await client.enableMicrophone();
Ingestmode
Any SFU → RelayRooms Ingest → DSP → STT → Transcript

Для тех, у кого уже есть LiveKit / Daily / Twilio. Подключи аудио — получи транскрипт без переписывания медиа-стека.

const { ws_url } = await fetch("/v1/ingest", {
  method: "POST",
  headers: { Authorization: "Bearer rr_..." },
  body: JSON.stringify({ stt_provider: "soniox" })
}).then(r => r.json());

const ws = new WebSocket(ws_url);
onAudioFrame(({ participantId, pcm }) => {
  ws.send(framePacket(participantId, pcm));
});
07Developer experience
LiveKit path
487
LOC
5
files
~3d
time

Token server, Python agent, dispatch, deploy, client subscription, optional custom AudioMixer for N:1.

RelayRooms path
18
LOC
1
files
30m
time

One API call to create a room, one webhook for the final transcript. That's the whole integration.

08Экономика N:1
N:1 economics

Один поток вместо N.

Стандартный путь — отдельный STT на каждого участника. Мы миксуем N в один поток после DSP. Посчитай разницу на своих объёмах.

* индикативные тарифы провайдеров

Participants8 ppl
Session length30 min
Sessions / day50 sess
Provider
Monthly STT cost45,000 min / mo
Per-participant
$2124.00/ mo
8 streams
RelayRooms N:1
$265.50/ mo
1 stream
Savings× 8.0
−$1858.50/ mo
09Quickstart
Quickstart

Минимальная интеграция.

API-ключ в консоли, снипет ниже — комната, подключение, транскрипт в one webhook.

index.ts
@relayrooms/js
import { RelayRooms, RelayRoom } from "@relayrooms/js";

const rr = new RelayRooms({ apiKey: process.env.RR_KEY! });

const room = await rr.rooms.create({
  stt_provider: "soniox",
  audio_preset: "studio",   // "conversation" · "speakerphone"
});

const client = new RelayRoom(room.join_url, room.token);
client.on("transcript", s => console.log(s.text));

await client.connect();
await client.enableMicrophone();