Звонки: WebPush incoming/stop, actions и TTL; обновлена логика

This commit is contained in:
AidarKC
2026-05-02 18:25:44 +03:00
parent 310863faec
commit c0c29b74ab
10 changed files with 711 additions and 208 deletions
@@ -1,113 +1,44 @@
# Логика установки соединения через сервер
# Логика установки звонка через сервер (актуальная)
Ниже описан фактический flow звонка в текущей реализации SHiNE с опорой на сообщения, которые проходят через сервер.
## 1) Ключевые API
- `CallInviteBroadcast` — широковещательный старт входящего звонка (`type=100`) по пользователю.
- `CallSignalToSession` — точечный сигнал в конкретную сессию (`RINGING/ACCEPT/DECLINE/TIMEOUT/HANGUP/OFFER/ANSWER/ICE`).
## 1) Основные операции и типы сообщений
## 2) Базовый поток звонка
1. Инициатор отправляет `CallInviteBroadcast(toLogin, callId, type=100)`.
2. Сервер отправляет онлайн-сессиям callee событие `IncomingCallInvite` по WS.
3. Офлайн-сессиям callee сервер отправляет WebPush `incoming_call` (с TTL).
4. Любая сессия callee, получив invite, может отправить `RINGING`.
5. Инициатор после первого `RINGING` показывает «Вызываем…».
6. При `ACCEPT` выбирается одна целевая сессия, остальные получают `HANGUP/stop_call` и закрывают экран входящего.
7. Далее только выбранная пара сессий обменивается `OFFER/ANSWER/ICE`.
Клиентские WS-операции:
- `CallInviteBroadcast` — широковещательный входящий вызов пользователю.
- `CallSignalToSession` — точечный сигнал в конкретную сессию.
## 3) WebPush по звонкам
### `incoming_call`
- Используется как fallback для офлайн-сессий.
- Поля: `kind`, `callId`, `fromLogin`, `fromSessionId`, `toLogin`, `sentAtMs`, `expiresAtMs`.
- TTL сейчас: **10 секунд**.
- Если push пришёл после `expiresAtMs`, уведомление не показывается.
Серверные события клиенту:
- `IncomingCallInvite` — уведомление о входящем вызове.
- `IncomingCallSignal` — сигнал по активному `callId`.
### `stop_call`
- Отправляется при завершении/отмене/принятии на другом устройстве.
- Поля: `kind`, `callId`, `reason`, `fromLogin`, `fromSessionId`, `toLogin`, `sentAtMs`.
- Service Worker закрывает уведомление по `tag=callId`.
Коды `type` в `IncomingCallSignal`:
- `100``INVITE`
- `110``RINGING`
- `120``ACCEPT`
- `130``DECLINE_BUSY`
- `140``TIMEOUT`
- `150``HANGUP`
- `200``OFFER`
- `210``ANSWER`
- `220``ICE`
## 4) Кнопки в push-уведомлении
- Для `incoming_call` есть actions:
- `accept` — «Ответить»
- `decline` — «Сбросить»
- По нажатию action:
- Service Worker шлёт событие в открытые вкладки,
- и открывает/фокусит UI с параметрами действия.
- UI пытается выполнить действие сразу (принять/отклонить), если сессия уже авторизована.
---
## 5) Важные ограничения
- Источник истины по звонку — серверный сигналинг (WS + серверные сигналы), push только вспомогательный канал.
- Push может прийти с задержкой и не по порядку, поэтому клиент фильтрует устаревшие события (`expiresAtMs` + `callId` + `stop_call`).
- Полноценный «автоответ в фоне без открытия UI» в web/PWA не гарантируется платформой.
## 2) Старт исходящего звонка
1. Инициатор создаёт `callId` и отправляет:
- `CallInviteBroadcast(toLogin, callId, type=100)`.
2. Сервер находит все активные WS-сессии целевого логина и шлёт им `IncomingCallInvite`.
3. На устройствах callee появляется экран входящего вызова.
---
## 3) Ранние статусы до поднятия трубки
Каждое устройство callee может отправить инициатору:
- `RINGING (110)` — «звонок идёт».
Для исходящего звонка инициатор фиксирует выбранную `remoteSessionId`:
- первый валидный `RINGING`/`ACCEPT` выбирает сессию,
- сигналы с тем же `callId`, но от других сессий этого же пользователя, игнорируются.
Это защищает от гонок мультидевайса.
---
## 4) Принятие звонка на одном устройстве callee
1. На выбранном устройстве callee пользователь нажимает «Поднять»:
- отправляется `ACCEPT (120)` в выбранную сессию инициатора.
2. Сервер дополнительно рассылает на **другие** сессии этого же callee:
- `HANGUP (150)` с `data=accepted_on_other_device`.
3. Остальные устройства callee закрывают экран входящего вызова.
Итог: активным остаётся один путь «инициатор ↔ выбранная сессия callee».
---
## 5) Обмен SDP (OFFER/ANSWER)
После `ACCEPT`:
1. Инициатор формирует `RTCPeerConnection`, `createOffer()`, отправляет:
- `OFFER (200)`.
2. Calee применяет `setRemoteDescription(offer)`, делает `createAnswer()`, отправляет:
- `ANSWER (210)`.
3. Инициатор применяет `setRemoteDescription(answer)`.
Защиты:
- повторный `ACCEPT`/повторный старт `offer` игнорируется;
- `ANSWER` обрабатывается только для исходящего звонка;
- `ANSWER` без локального `offer` или дубликат в `stable` игнорируется.
---
## 6) Обмен ICE-кандидатами
`ICE (220)` может приходить раньше SDP. Поэтому:
- если `pc` ещё не создан — ICE кладётся в очередь;
- если `pc` есть, но `remoteDescription` ещё нет — ICE тоже в очередь;
- после установки `remoteDescription` очередь применяется (`addIceCandidate`).
Это устраняет race «remote description was null».
---
## 7) Завершение и ошибки
Нормальное завершение:
- `HANGUP (150)` от одной стороны → вторая завершает звонок.
Неуспех установки:
- сторона, у которой setup не удался, шлёт `HANGUP (150)` с `data=setup_failed:...`,
- вторая сторона сразу закрывает экран ожидания.
Также пишутся `CallDeliveryReport`:
- `call_connected`, `incoming_failed`, `outgoing_failed`, `unknown_error` и расширенная диагностика ICE/SDP.
---
## 8) Почему схема устойчива сейчас
Текущая устойчивость обеспечивается тремя правилами:
1. **Одна выбранная сессия callee** для исходящего звонка.
2. **Принятие на одном устройстве закрывает входящий на остальных** через сервер.
3. **ICE буферизуется до готовности SDP/PC**, а не ломает handshake.
Именно комбинация этих трёх пунктов закрывает основные причины «иногда дозванивается, иногда нет» при мультидевайсе.
## 6) Ограничение текущей архитектуры
- Сейчас звонки надёжно работают в рамках одного сигнального контура (когда обе стороны подключены к совместимому серверу/кластеру).
- Межсерверный сценарий «A и B на полностью разных серверах» пока не завершён и вынесен в TODO.