# AGIL Calls — технический спайк: SFU/LiveKit, мульти-нода, мобильные SDK

> **Дата:** 13.11.2026. **Статус:** план внедрения, **без кода** (спайк/design).
> **Цель:** перейти от mesh ≤8 к SFU (50–100+ участников), получить облачную
> запись/RTMP/HLS, мульти-нода signaling и мобильные SDK — без потери текущего
> API/embedding и с mesh-fallback.
>
> **База:** [`docs/reverse-engineering.md`](./reverse-engineering.md) §3 (LiveKit),
> §4 (медиасерверы, лицензии), §9 (берём/делаем/покупаем), §13 (запись/STT);
> [`docs/estimate.md`](./estimate.md) (B12/B13/B14/B15/B31, C1);
> [`docs/scaling.md`](./scaling.md) (контракт `scale`, мульти-нода design);
> [`docs/backlog.md`](./backlog.md) (B1–B10, D1); текущий код
> (`packages/server`, `packages/widget`, `packages/sdk`, `docker-compose.yml`).
>
> **Что уже есть в коде (точка старта):** `TenantConfig.scale` (zod) + admin API
> `/v1/admin/scale`, `/v1/admin/cluster/status`, `roomMediaMode()` и
> `RoomInfo.mediaMode`; TURN-пул подставляется в `welcome.iceServers`;
> клиент — только `WebRtcEngine` (mesh, `MAX_MESH_PEERS=8`); SDK — браузерный
> (`@agil/calls-sdk` 0.1.0: iframe/global, WebRTC через браузер). SFU-оркестрации,
> egress, Redis-шины и мобильных SDK **нет**.

---

## 1. Целевая архитектура

```
                        ┌──────────── TLS/L7 LB (Caddy/Nginx/HAProxy) ────────────┐
 browser/mobile ────────┤  https://calls.example  (app 8787, WS signaling)        │
                        │  wss://sfu.example      (LiveKit 7880)                  │
                        └───────┬──────────────────────────────┬─────────────────┘
                                │                              │
                    ┌───────────▼───────────┐        ┌─────────▼──────────┐
                    │ AGIL server (Node)    │        │ LiveKit SFU (Go)   │
                    │ - API/rooms/audit     │        │ - rooms/tracks     │
                    │ - signaling WS (чат,  │◄──────►│ - webhooks         │
                    │   presence, state)    │  REST  │ - embedded TURN    │
                    │ - JWT для LiveKit     │        └─────────┬──────────┘
                    │ - mediaMode routing   │                  │
                    └───────┬───────────────┘        ┌─────────▼──────────┐
                            │                        │ Redis (LiveKit +  │
                    ┌───────▼────────┐               │ AGIL pub/sub)      │
                    │ coturn (mesh)  │               └─────────┬──────────┘
                    └────────────────┘                         │
                                                    ┌──────────▼──────────┐
                                                    │ LiveKit Egress      │
                                                    │ MP4 → S3, RTMP/HLS  │
                                                    └─────────────────────┘
```

**Принципы:**

1. **Медиаплейн — чужой (LiveKit, Apache-2.0), продуктовый слой — наш.** Наш
   signaling остаётся источником правды для чата, presence, состояния комнаты,
   аудита, конфига и API. LiveKit отвечает только за медиа (публикация/подписки).
2. **Комната = `tenantId_roomId`** в LiveKit (без PII в имени, как рекомендует
   LiveKit). Соответствие хранится в `RoomInfo` (уже есть `mediaMode`).
3. **Токены.** Наш `POST /v1/rooms/:id/join` уже выдаёт JWT комнаты — в ответе
   добавляется блок `media: { mode, url, token, room, canPublish }`, где `token` —
   **отдельный LiveKit-JWT** (HS256, `roomJoin`, `room`, `canPublish`,
   `canSubscribe`, TTL ≤ TTL нашего токена, `identity` = participantId, без PII).
   Секреты — только env (`AGIL_SFU_API_KEY/AGIL_SFU_API_SECRET`, как в
   `scale.sfu.apiKeyEnv/apiSecretEnv`).
4. **`mediaMode` уже в контракте.** `scale.mediaMode` = `mesh|sfu|broadcast`;
   `roomMediaMode(scale, mode)` вычисляет эффективный режим; `RoomInfo.mediaMode`
   уже отдаётся в API. Меняется только реализация: при `sfu/broadcast` клиент
   поднимает `SfuEngine` вместо `WebRtcEngine`.
5. **Mesh-fallback.** Если `mediaMode=mesh` (или SFU недоступен, а участников ≤8) —
   текущий `WebRtcEngine` без изменений. Если SFU недоступен при >8 — баннер
   «медиасервер недоступен» + запрет входа (честно, без тихой деградации).
6. **`broadcast`.** `canPublish=false` у зрителей, UI скрывает mic/cam/share;
   спикеры публикуют. Модерация чата — на нашем signaling (уже есть роли/права).
7. **Чат/presence/анализ — по-прежнему наш WS**, не через LiveKit data channels.
   Это сохраняет fallback чата при деградации медиа (наше преимущество).
8. **Вебхуки LiveKit** (`room_started`, `participant_joined/left`, `track_published`,
   `room_finished`, `egress_*`) → наш `POST /v1/sfu/webhook` (проверка подписи
   LiveKit) → обновление `RoomInfo`, аудит, метрики, автостоп egress.
9. **TURN.** Сохраняем coturn для mesh-пути; LiveKit включает **встроенный TURN**
   (TLS 5349/443) — это покрывает корпоративные сети для SFU. В `scale.turn`
   можно оставить пул для mesh, а для SFU — полагаться на LiveKit TURN.
10. **Мульти-нода.** Два независимых уровня:
    - **signaling-ноды AGIL:** sticky LB (cookie `agil_node`/hash по roomId),
      Redis pub/sub для чата/presence/config, lease владельца комнаты; медиа не
      идёт через signaling;
    - **SFU-ноды LiveKit:** собственный Redis LiveKit + комнаты закреплены за
      узлом; клиент получает `url` нужного узла в `media.url` (роутинг по
      `scale.nodes`/health).
11. **Egress.** Отдельный сервис `livekit/egress` (не в основном контейнере):
    композиция комнаты в MP4 → S3/MinIO, RTMP в YouTube/VK, HLS-сегменты → CDN.
    Запись стартует нашим admin API (`POST /v1/admin/rooms/:id/recording`) после
    `guard`/consent; egress подключается как участник `kind=EGRESS`.

### Поток входа (SFU-режим)

1. Браузер → `POST /v1/rooms/:id/join` (наш API) → проверки (пароль/лок/гео/лимит)
   → JWT комнаты + `media: { mode:'sfu', url, token, room, canPublish }`.
2. Виджет: если `mode=sfu` → `SfuEngine.connect(media)` (LiveKit `Room.connect`);
   чат/состояние — по нашему WS (как сейчас).
3. LiveKit webhook → сервер обновляет участников комнаты/аудит.
4. Выход: `room.disconnect()` + наш WS `leave`; egress останавливается по
   `room_finished`/таймауту.

### Что переиспользуем, что пишем

| Переиспользуем | Пишем/меняем |
|---|---|
| `TenantConfig.scale` (zod), admin API scale/cluster, `roomMediaMode` | `SfuEngine` в `packages/widget/src/call/` (интерфейс как у `WebRtcEngine`) |
| JWT-инфраструктуру (HS256, claims, TTL) | LiveKit-JWT (grant mapping), эндпоинт выдачи в join/welcome |
| Наш signaling WS (чат/presence/state/аудит) | `media`-блок в join/welcome, `scale.sfu` health-check |
| TURN-пул для mesh, coturn | LiveKit config (keys/redis/rtc/turn), webhook-эндпоинт + подпись |
| Dashboard/editor/MCP | Карточки статуса SFU/egress, кнопки записи/стрима |
| Client recording (MediaRecorder) | Server-side egress (MP4/S3), RTMP/HLS |

### 1.1 Почему LiveKit, а не альтернативы

| Вариант | Лицензия | Что даёт | Почему да/нет |
|---------|----------|----------|----------------|
| **LiveKit self-host** | Apache-2.0 | SFU (Go), SDK на 10+ платформ, egress/ingress, Redis multi-node, TURN | **Да:** один бинарник, готовый egress, зрелые SDK (в т.ч. RN/iOS/Android), экономия ≈1 144 ч против «с нуля» (`estimate.md` §C) |
| LiveKit Cloud | коммерческая | managed SFU/egress | **Нет для 152-ФЗ:** медиа/записи вне РФ; только как референс/пилот |
| mediasoup | ISC | SFU-библиотека | **Резерв:** дешевле лицензионно, но сигналинг/запись/мультирегион писать самим (+800–1200 ч, `reverse-engineering.md` §4.2) |
| Janus | GPL-3.0 | плагины SIP/VideoRoom | **Нет:** GPL-риск для closed-source |
| Jitsi-форк | Apache-2.0 | готовый продукт | **Нет:** XMPP/Lua/Kotlin, слабая кастомизация, «ещё один Jitsi» (C3 ≈3 400 ч) |
| Свой SFU | свой код | полный контроль | **Нет:** 8 568 ч / 52 нед, риск качества связи |

---

## 2. Пошаговый план интеграции

Оценки — **человеко-дни одного senior-инженера** (1 д = 8 ч); в скобках — часы.

| Этап | Что делаем | Результат | Оценка |
|------|------------|-----------|--------|
| **S0. Спайк/лаб (1 нед)** | Поднять LiveKit+Redis+egress в Docker на тест-стенде; проверить 2–8 участников, токены, запись, RTMP; замерить CPU/трафик/стоимость | отчёт: capacity-числа, выбранные версии, порты, риски | 5 дн (40 ч) |
| **S1. SFU-media ядро** | Сервер: выдача LiveKit-JWT в join/welcome, `media`-блок, health SFU, feature-flag по `scale.mediaMode`; клиент: `SfuEngine` (livekit-client), переключение engine, mesh-fallback, UI «режим SFU» | комната 50 участников работает SFU; mesh не сломан | 25–30 дн (200–240 ч) |
| **S2. Webhooks/состояние/модерация** | `/v1/sfu/webhook` (подпись), синхронизация участников/треков с `RoomInfo`, аудит, kick/mute через LiveKit API, проверка прав | паритет состояния с mesh | 8–10 дн (64–80 ч) |
| **S3. Egress: запись** | `livekit/egress` в compose; admin API старт/стоп записи; S3/MinIO; consent/индикатор; retention; webhook `egress_*`; автостоп | облачная запись MP4 в S3, доступ через `/v1/recordings/:id` | 12–15 дн (96–120 ч) |
| **S4. RTMP/HLS-стриминг** | Egress → RTMP URL (YouTube/VK) и HLS-сегменты; статус/ошибки; `scale.streaming`; UI-кнопка; CDN-хук | стрим наружу + HLS-плейлист | 10–12 дн (80–96 ч) |
| **S5. Мульти-нода signaling** | Redis pub/sub (чат/presence/config/analysis), room ownership/lease, sticky LB, WS-редирект на владельца, health-регистрация | 2+ signaling-ноды, комната на одной ноде | 25–30 дн (200–240 ч) |
| **S6. TURN/сеть/наблюдаемость** | LiveKit embedded TURN (TLS 5349/443), firewall-порты, health-check SFU/TURN, Prometheus/Grafana, алерты, capacity-дашборд | воспроизводимый деплой + алерты | 8–10 дн (64–80 ч) |
| **S7. Нагрузка/приёмка** | k6/webrtc-perf: 50/100/500 участников; потери/задержка/CPU; failover ноды; чек-лист | отчёт capacity + bugfix | 8–10 дн (64–80 ч) |
| **S8. Документация/панель** | Гайд «включить SFU», статус в dashboard/editor, MCP-инструменты, миграция mesh→sfu | оператор может включить сам | 4–5 дн (32–40 ч) |

Итого SFU+egress+multi-node (S0–S8): **≈105–127 дн** (840–1 016 ч) одного
senior; на команде 5,5 FTE — календарно ≈6–8 недель с учётом QA/приёмки.

**Минимальный P0-срез для «50+ участников»:** S0+S1+S2+S6 (≈46–55 дн).
**Запись/стримы:** +S3+S4 (≈22–27 дн). **Мульти-нода:** +S5 (≈25–30 дн).

### Риски и что проверить в спайке

| Риск | Влияние | Митигация |
|------|---------|-----------|
| **Лицензия LiveKit Apache-2.0** | нет (можно закрывать); обязателен NOTICE, нельзя использовать бренд LiveKit | сохранить NOTICE, не брендировать чужое, юр-ревью перед релизом |
| **Egress стоимость** | CPU/Chrome на каждую запись; трафик в S3; RTMP-исходящий | замер в S0 на 1/5/20 записей; лимит записей на тенант; retention 7–30 дн |
| **CDN/полоса** | HLS на тысячи зрителей; регион CDN | CDN только после S4, contract+регион; LL-HLS для интерактива |
| **152-ФЗ / self-host** | медиа и записи должны быть в РФ; LiveKit Cloud нельзя | только self-host в РФ; S3/MinIO в РФ; лог-политика; DPA |
| **Порты/firewall** | UDP 50000–60000, TCP 7881, TURN TLS 443 | открыть на стенде до S0; проверить из мобильной сети/VPN |
| **Мульти-нода** | split-brain, потеря комнаты при падении ноды | lease+TTL, редирект, health, тесты failover (S7) |
| **iOS/Safari** | WebRTC/кодеки, автоплей, TURN | матрица браузеров; TURN/TLS 443; тест на iPhone в S1–S7 |
| **Версии LiveKit** | частые релизы | пиннить тег `livekit/livekit-server:v1.x`/`livekit/egress:v1.x`, обновление по чек-листу |
| **Совместимость API** | сломать mesh-клиентов | `media` — опциональный блок; `mediaMode=mesh` по умолчанию; VERSION-политика моста |

### Сравнение с `docs/estimate.md`

| Блок estimate | Часы | Дни | Покрытие в этом плане |
|---------------|------|-----|------------------------|
| B12 SFU-интеграция (LiveKit) | 320 | 40 | S0+S1+S2 (≈330–360 ч; близко) |
| B13 масштабирование/инфраструктура | 240 | 30 | S5+S6+S7 (≈330–400 ч; план подробнее) |
| B15 запись/стриминг базово | 168 | 21 | S3+S4 (≈180–216 ч) |
| B31 запись v2 (watermark/retention) | 176 | 22 | S3 partial; остальное — отдельно |
| B14 мобильные SDK | 360 | 45 | §4 (см. ниже) |
| **C1 (LiveKit с 1 дня), всего** | **5 424** | **678** | ориентир полного продукта (≈33 нед на 5,5 FTE) |
| **B (mesh→SFU), всего** | **6 568** | **821** | альтернативный путь (≈40 нед) |

Вывод: план укладывается в C1/B12–B15; отдельно нужно заложить **+150–250 ч**
на нагрузочные тесты (B19/риск D1) и **+100–150 ч** на миграцию/поддержку двух
режимов. Свой SFU с нуля не рассматриваем: 8 568 ч / 52 нед (estimate §C).

---

## 3. Мобильные SDK/приложения

**Что даёт текущий SDK:** `@agil/calls-sdk` 0.1.0 — **браузерный** JS SDK
(iframe/global, `createEmbed/mount/resume/openCall`, темы), внутри — `WebRtcEngine`
(browser `RTCPeerConnection`). Нативных модулей, CallKit/Push, фонового аудио нет.

| Вариант | Плюсы | Минусы | Оценка MVP iOS+Android |
|---------|-------|--------|------------------------|
| **A. WebView-обёртка** (Capacitor/RN WebView на наш `embed.html`) | 1–2 недели; один код; сразу весь UI/темы | нет CallKit/Push/фонового аудио; iOS WKWebView WebRTC ограничен; «не приложение» для сторов | **10–15 дн** (80–120 ч) |
| **B. React Native + `@livekit/react-native` + наш API/WS** | нативные модули, CallKit/Push, один код на 2 платформы; LiveKit RN SDK зрелый | нужен SFU (S1); UI писать на RN (виджет не переиспользуется как есть); 2 платформы | **30–40 дн** (240–320 ч) |
| **C. Flutter + `livekit_client`** | один код, хороший WebRTC | меньше экспертизы в команде; дублирование UI; LiveKit Flutter SDK | **30–40 дн** (240–320 ч) |
| **D. Нативно Swift/Kotlin + LiveKit iOS/Android SDK** | максимум качества/интеграции (CallKit, VoIP push, Bluetooth) | дорого, два кода, долго | **45–60 дн** (360–480 ч; B14=360 ч) |

**Рекомендация:** после S1 (SFU) идти **B — React Native + LiveKit RN SDK**:
один код, нативные вызовы, CallKit/Android ConnectionService, push; наш сервер
выдаёт `media`-токен, чат/комнаты — по нашему API/WS (тот же контракт).
Для быстрых продаж — **A** как demo-shell (10–15 дн), но не как продуктовое
приложение. Нативные D — только при требовании «входящие звонки как в Zoom» и
готовности платить 45–60 дн.

**Неизвестное:** версии/совместимость LiveKit RN SDK с RN-версией команды;
политика сторов (фоновое аудио/VoIP push), сертификаты push (APNs/FCM), 152-ФЗ
для мобильных метаданных, iOS CallKit-ограничения — уточнить на S0.

---

## 4. Конкретные команды/инфраструктура для стенда

> Секреты — только в `.env`/config на сервере; в git не коммитить.
> Версии ниже — ориентир, **пиннить актуальные теги** на дату внедрения
> (`livekit/livekit-server:v1.x`, `livekit/egress:v1.x`).

### 4.1 Сервисы и порты

| Сервис | Образ | Порты | Назначение |
|--------|-------|-------|------------|
| app | `agil-calls:0.1.0` | `127.0.0.1:8787` | API/WS/статика (уже есть) |
| LiveKit SFU | `livekit/livekit-server:v1.x` | **7880/tcp** (HTTP/WS), **7881/tcp** (RTC fallback), **50000–60000/udp** (RTC), 6789 (Prometheus, internal) | медиаплейн |
| Redis | `redis:7-alpine` | 6379 (internal only) | LiveKit multi-node + AGIL pub/sub |
| Egress | `livekit/egress:v1.x` | исходящие: LiveKit 7880, S3, RTMP; health 8080 (internal) | запись/RTMP/HLS |
| coturn | `coturn/coturn:4.6` (уже есть) | 3478/udp, 5349/tcp (TLS), relay 49200–49250/udp | mesh/TURN; для SFU — embedded TURN LiveKit (5349/443) |
| Ingress (опция) | `livekit/ingress:v1.x` | RTMP/SRT вход | внешние камеры/OBS — позже |

**Firewall:** открыть `7880/tcp`, `7881/tcp`, `50000–60000/udp`, `5349/tcp`
(TURN/TLS), `443/udp` (TURN/UDP) — на публичном IP SFU.**Только для стенда:**
`7880` можно временно без TLS, но браузер требует `wss://` → ставим Caddy/Nginx
перед LiveKit с TLS.

### 4.2 `docker-compose.yml` — добавить рядом с app (фрагмент)

```yaml
  livekit:
    image: livekit/livekit-server:v1.x        # PIN!
    restart: unless-stopped
    network_mode: host                        # рекомендовано LiveKit для медиа
    command: ["--config", "/etc/livekit.yaml"]
    volumes:
      - ./infra/livekit.yaml:/etc/livekit.yaml:ro
    depends_on: [redis]
    # healthcheck: зависит от образа (wget/curl может отсутствовать) —\n    # проверяйте живучесть внешним probe /v1/health или curl с хоста

  redis:
    image: redis:7-alpine
    restart: unless-stopped
    command: ["redis-server", "--appendonly", "yes"]
    volumes: ["redis-data:/data"]
    # порт наружу не публиковать; доступ только из compose-сети

  egress:
    image: livekit/egress:v1.x                # PIN! нужен Chrome/GStreamer
    restart: unless-stopped
    network_mode: host
    environment:
      EGRESS_CONFIG_FILE: /etc/egress.yaml
      # секреты S3/RTMP — в egress.yaml через env-подстановку или env:
      # AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY / AWS_REGION
    volumes:
      - ./infra/egress.yaml:/etc/egress.yaml:ro
    depends_on: [livekit, redis]

volumes:
  redis-data:
```

### 4.3 `infra/livekit.yaml` (эскиз)

```yaml
port: 7880
rtc:
  tcp_port: 7881
  port_range_start: 50000
  port_range_end: 60000
  use_external_ip: true           # в облаке; на стенде — по факту NAT
redis:
  address: redis:6379
keys:
  agil_sfu: CHANGE_ME             # api key: secret (совпадает с AGIL_SFU_API_KEY/SECRET)
turn:
  enabled: true
  domain: turn.calls.example      # отдельный сертификат
  tls_port: 5349
webhook:
  api_key: agil_sfu
  urls:
    - https://calls.example.com/v1/sfu/webhook
prometheus_port: 6789
logging:
  level: info
```

### 4.4 `.env` (добавить к существующему)

```dotenv
# SFU / LiveKit
AGIL_SFU_API_KEY=agil_sfu
AGIL_SFU_API_SECRET=CHANGE_ME_32_BYTES
AGIL_SFU_URL=wss://sfu.calls.example.com
# для server→LiveKit REST (kick/mute/egress)
LIVEKIT_URL=https://sfu.calls.example.com
LIVEKIT_API_KEY=agil_sfu
LIVEKIT_API_SECRET=CHANGE_ME_32_BYTES
# webhook
AGIL_SFU_WEBHOOK_ENABLED=1
# egress → S3/MinIO (в РФ)
EGRESS_S3_ENDPOINT=https://s3.example.ru
EGRESS_S3_BUCKET=agil-recordings
EGRESS_S3_REGION=ru-msk
EGRESS_S3_ACCESS_KEY=CHANGE_ME
EGRESS_S3_SECRET_KEY=CHANGE_ME
```
В `TenantConfig.scale.sfu` выставить `provider:'livekit'`, `url`, `region`,
`apiKeyEnv:'AGIL_SFU_API_KEY'`, `apiSecretEnv:'AGIL_SFU_API_SECRET'`.

### 4.5 Проверка стенда (smoke)

```bash
# 1) поднять
docker compose up -d redis livekit egress

# 2) health LiveKit (без TLS локально)
curl -s http://127.0.0.1:7880 | head

# 3) метрики
curl -s http://127.0.0.1:6789/metrics | grep -E 'livekit_(rooms|participants|tracks)'

# 4) порты (снаружи)
nc -vz sfu.calls.example.com 7880
nc -vz sfu.calls.example.com 7881
# UDP проверить через webrtc-internals (ICE selected pair → relay/host)

# 5) egress: старт записи — через livekit-cli (синтаксис/тег уточнить в актуальной версии)
docker run --rm --network host livekit/livekit-cli:latest egress start \
  --url http://127.0.0.1:7880 --api-key "$LIVEKIT_API_KEY" --api-secret "$LIVEKIT_API_SECRET" \
  --room test --track composite --file /out/test.mp4
```

**TLS-прокси:** Caddy/Nginx должен проксировать `wss://sfu.example` → `http://127.0.0.1:7880`
с `Upgrade/Connection`; для egress S3/RTMP — только исходящие соединения.
**Capacity-ориентиры** (`docs/scaling.md` §7): SFU ≈1–2 тыс. подписок на ноду
(CPU-bound), signaling ≈5–10 тыс. WS на узел, TURN ≈1–3 Гбит/с relay; для
10 000 зрителей — HLS+CDN, не SFU.

---

## 5. Что нужно решить до старта (unknowns)

1. **Где ставим SFU:** тот же хост, что app (стенд/пилот) или отдельный узел с
   публичным IP и 10Gbps? Для прода — отдельный.
2. **Домен/сертификаты:** `sfu.*` и `turn.*` (TLS) — wildcard или отдельные.
3. **S3/MinIO в РФ** для записей: провайдер, регион, retention, шифрование.
4. **Стоимость egress** (CPU-часы + трафик + S3) — замерить в S0, утвердить
   тариф/лимиты записей.
5. **Продуктовая политика `broadcast`:** кто зритель, как входит (guest), чат
   moderated, лимит viewerLimit — согласовать с продажами.
6. **Миграция клиентов:** включать `scale.mediaMode=sfu` по умолчанию для новых
   тенантов; старым — поэтапно, с фича-флагом и баннером.
7. **Мобильный scope:** RN MVP (B) vs WebView demo (A) — влияет на сроки/бюджет.
8. **Версии LiveKit/egress** и политика обновлений (пиннинг + чек-лист).
9. **152-ФЗ:** self-host в РФ, DPA, сроки хранения записей/транскриптов — юрист.

---

## 6. Рекомендация (коротко)

1. **Медиаплейн — LiveKit self-host (Apache-2.0), не свой SFU и не Jitsi-форк**
   (экономия ≈1 144 ч против «с нуля», `docs/estimate.md` §C).
2. **Сначала P0-срез:** S0→S1→S2 (+S6) — **≈46–55 дн**: комнаты 50+, токены,
   webhooks, fallback, TURN/порты. Это закрывает главный gap «50+ участников».
3. **Затем запись/стримы:** S3→S4 — **≈22–27 дн** (MP4→S3, RTMP/HLS, CDN-хук).
4. **Мульти-нода:** S5 — **≈25–30 дн**, после стабильного SFU (не раньше).
5. **Мобильные:** после S1 — **RN + LiveKit RN SDK, 30–40 дн** на iOS+Android MVP;
   WebView-shell 10–15 дн для демо; нативные 45–60 дн — позже.
6. **Суммарно до SFU+egress+multi-node:** ≈105–127 дн одного senior (840–1 016 ч);
   календарно ≈6–8 недель на команде 5,5 FTE + QA/нагрузка. Полный
   Production-ready по `estimate.md` — 5 424 ч / ≈33 нед (C1).
7. **Старт:** поднять compose-фрагмент из §4 на тест-стенде, прогнать smoke,
   замерить egress/CPU/трафик — после этого подтверждать бюджет и даты.

