# СОБЫТИЕ ·18 — TURN-слой и большие залы (v1.13)

## Что это

·18 умеет соединять устройства напрямую (WebRTC audio-mesh). Сигналинг — публичные
Nostr-ретрансляторы; голос летит P2P. Но P2P пробивает не всякую сеть: за
**симметричным NAT** (часть мобильных операторов, корпоративные сети) прямое
соединение не устанавливается. Решение — TURN-релей: сервер, через который
голос пересылается, когда напрямую нельзя. Сервис встроен как **надстройка**:
ядро ·18 не переписывалось, при выключенном TURN всё работает как раньше.

## TURN-слой (v1.13)

- Панель «⌇ TURN-релей» в меню «СОЗДАТЬ ЗАЛ» и в меню «ВОЙТИ В ЗАЛ».
- Пользователь вводит **свой** сервер: адрес(а) `turn:host:port`, логин, пароль.
- Реквизиты хранятся только в localStorage устройства (`s18.turn.v1`), никуда
  не отправляются, в коммиты не попадают.
- Инъекция в ICE происходит перехватом `RTCPeerConnection` на уровне браузера:
  конфиги с уже заданными ICE-серверами (между устройствами) получают TURN;
  путь «этот браузер» (два окна, `iceServers: []`) не трогается.
- Статус связи честный: «соединений: N · P2P напрямую / через релей: M ·
  TURN: задан/нет». Считается по реальному `getStats()` каждой связи.

## Как поднять свой coturn за одну команду

```bash
docker run -d --network host --name coturn coturn/coturn -n \
  --lt-cred-mech --user singulyar:МОЙ_ПАРОЛЬ --realm singulyar.local
```

Проверка: `turnutils_uclient -u singulyar -w МОЙ_ПАРОЛЬ IP_СЕРВЕРА` (из пакета
coturn) или любой STUN/TURN-чекер. Порт 3478/udp (и диапазон 49152-65535/udp
для relay-портов) должен быть открыт.

Затем в ·18: «⌇ TURN-релей» → включить → `turn:IP_СЕРВЕРА:3478?transport=udp`,
логин `singulyar`, пароль `МОЙ_ПАРОЛЬ` → сохранить. То же самое сделать на
устройствах гостей (реквизиты одинаковые).

## Политика «>5 участников» (аудио-меш, честно)

Аудио-меш: каждый участник держит N-1 прямых соединений. Это дёшево по трафику
(голос ~24-51 кбит/с на связь), но растёт квадратично по числу связей.

| участников | соединений у каждого | статус |
|---|---|---|
| 2-5 | 1-4 | штатно |
| 6-8 | 5-7 | в интерфейсе появляется предупреждение о бюджете |
| >8 | 8+ | щадящее кодирование Opus (maxBitrate 24 кбит/с) автоматически |

- Предупреждение показывается всем участникам в строке статуса связи.
- Никто не блокируется: жёсткого лимита нет, решение остаётся за людьми.

## SFU для больших залов — ЧЕСТНЫЙ СТАТУС: UNKNOWN (план)

Для залов >10-15 человек правильная архитектура — SFU (Selective Forwarding
Unit): каждый шлёт один поток на сервер, сервер разворачивает его всем.
Кандидат — [Galène](https://galene.org/) (Go, один бинарь, WebRTC из коробки).

**Почему не сделано сейчас:** сборка Galène требует Go-тулчейна; в песочнице
сборки его нет, готовых бинарей проект не публикует. Врать «сделано» не будем.

**План внедрения (когда будет сервер):**
1. Поднять Galène (`go build`), включить TURN в его же конфиге.
2. В ·18 добавить транспорт «SFU»: группа `sfu` → WebSocket к Galène →
   публикация микрофона на группу вместо mesh-соединений.
3. Транспорт-фабрика `сделатьТранспорт` получает третий вид `sfu`; остальной
   код ·18 не меняется (те же колбэки cbData/cbMedia/cbPeers).
4. Реальный тест: зал из 12 вкладок через SFU, замер CPU/RAM сервера.

## Что проверено (CLAIM → TEST → RESULT)

- TURN-инъекция: конфиг со STUN получает turn-сервер из настроек — TEST ✓ (Playwright)
- Путь «этот браузер» не затронут (iceServers остаётся пустым) — TEST ✓
- Две панели (ведущий/гость) без дублей id — TEST ✓
- Мониторинг связи: статус-строка обновляется, PC отслеживаются обёрткой — TEST ✓
- Зал ведущий+гость через BroadcastChannel работает после вшивания слоя — TEST ✓ (4/4)
- Реальный звонок через поднятый coturn — UNKNOWN (нужен внешний сервер/хост,
  в песочнице нет публичного TURN и двух сетей за симметричным NAT)
- SFU Galène — UNKNOWN (нет Go-тулчейна в песочнице; план выше)
