ПОШЛО НАХУЙ КТО ЭТИ ЗВОВНКИ ДЕЛАЛ
This commit is contained in:
@@ -0,0 +1,583 @@
|
||||
# Протокол звонков (ru.oneme.app 26.24.0, versionCode 6784)
|
||||
|
||||
Источник: реверс APK `ru.oneme.app.apk` (jadx). SDK звонков — `ru.ok.android.externcalls.sdk`
|
||||
(`calls-sdk`, sdkVersion `0.2.3`), медиаядро — `libjingle_peerconnection_so.so` (WebRTC).
|
||||
|
||||
## 0. Два независимых слоя
|
||||
|
||||
Звонок живёт в **двух** протоколах, и это ключ ко всему:
|
||||
|
||||
| Слой | Транспорт | Формат | За что отвечает |
|
||||
|------|-----------|--------|-----------------|
|
||||
| **MAX** | основной сокет приложения | MessagePack + Zstd, опкоды | создать/присоединиться к звонку, ссылки, история, пуш о входящем |
|
||||
| **Signaling** | отдельный WebSocket на `endpoint` из ответа MAX | **JSON текстом** | всё внутри звонка: медиа, SDP/ICE, админка, участники |
|
||||
|
||||
MAX-слой выдаёт `endpoint` + `token`, дальше клиент открывает второй сокет и всё
|
||||
управление звонком идёт уже там. Админка групповых звонков — целиком второй слой.
|
||||
|
||||
---
|
||||
|
||||
## 1. MAX-слой: опкоды звонков
|
||||
|
||||
Таблица опкодов вытащена из enum `p000/kzb.java` (`new kzb("ИМЯ", ordinal, (short) opcode, parser)`).
|
||||
|
||||
| Опкод | Имя | Назначение |
|
||||
|-------|-----|-----------|
|
||||
| 76 | `VIDEO_CHAT_START` | старт видеочата (в этой сборке ссылок на отправку нет — легаси) |
|
||||
| 78 | `VIDEO_CHAT_START_ACTIVE` | **создать / войти в звонок** |
|
||||
| 79 | `VIDEO_CHAT_HISTORY` | история видеочатов |
|
||||
| 84 | `VIDEO_CHAT_CREATE_JOIN_LINK` | **создать ссылку-приглашение** |
|
||||
| 89 | `LINK_INFO` | превью по ссылке (в т.ч. звонковой) |
|
||||
| 137 | `NOTIF_CALL_START` | пуш «входящий звонок» |
|
||||
| 163 | `CALL_HISTORY` | история звонков |
|
||||
| 164 | `CALL_HISTORY_CLEAR` | очистка истории |
|
||||
| 165 | `NOTIF_CALL_HISTORY` | пуш обновления истории |
|
||||
| 166 | `VIDEO_CHAT_JOIN` | **вход по ссылке** |
|
||||
| 195 | `VIDEO_CHAT_MEMBERS` | участники видеочата |
|
||||
|
||||
Полный дамп всех 175 опкодов — `calls-opcodes.txt` рядом с этим файлом.
|
||||
|
||||
### 1.0 Сверка с нашим `opcode_map.dart`
|
||||
|
||||
По звонкам расхождений в номерах нет, только в названиях (`164` мы зовём
|
||||
`videoChatDeleteHistory`, в APK это `CALL_HISTORY_CLEAR`; `166` — `videoChatJoinByLink`
|
||||
против `VIDEO_CHAT_JOIN`). Не хватает трёх звонковых опкодов:
|
||||
`163 CALL_HISTORY`, `165 NOTIF_CALL_HISTORY` (и `103 GET_INBOUND_CALLS` у нас есть).
|
||||
|
||||
Вне звонков сверка дала одно расхождение, которое стоит проверить отдельно:
|
||||
у нас `contactsGet = 8`, в APK опкод `8` — это `LOGIN2`. Остальные отличия — только имена
|
||||
(`91` GET_COMMENTS_UPDATES / commentsInfo, `202`, `209`, `210`, `293`, `302`).
|
||||
|
||||
### 1.1 Создание / вход в звонок — opcode 78 `VIDEO_CHAT_START_ACTIVE`
|
||||
|
||||
`p000/vjb.java` → `m30941c()`:
|
||||
|
||||
```jsonc
|
||||
// request payload
|
||||
{
|
||||
"conversationId": "<string>", // ApiProtocol.PARAM_CONVERSATION_ID
|
||||
"calleeIds": [<long>, ...], // только если массив непустой (p2p-звонок контактам)
|
||||
"chatId": <long>, // только если != null (звонок в чате/группе)
|
||||
"isVideo": <bool>,
|
||||
"internalParams": "<json-string>" // см. 1.4
|
||||
}
|
||||
```
|
||||
|
||||
`conversationId` генерируется клиентом (`one.me.calls.api.conversationid.ConversationIdGenerator`),
|
||||
а не сервером — это идемпотентный ключ звонка.
|
||||
|
||||
### 1.2 Ссылка на групповой звонок — opcode 84 `VIDEO_CHAT_CREATE_JOIN_LINK`
|
||||
|
||||
`p000/rg4.java:1664`:
|
||||
|
||||
```jsonc
|
||||
// request
|
||||
{ "conversationId": "<string>" }
|
||||
```
|
||||
|
||||
Вызывается уже **после** того, как звонок создан (лог `"start creating p2p join link"`,
|
||||
ошибки `"create p2p join link failed due to conversationId in null or empty"`,
|
||||
`"join link already exist"`, метрика провала `CREATE_LINK_FAILED`).
|
||||
То есть ссылка не создаёт звонок — она выдаётся к существующему `conversationId`.
|
||||
|
||||
Ответ — объект `VideoConference` (`p000/r6i.java`, msgpack-ключи):
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"conferenceId": "<string>",
|
||||
"conversationId": "<string>",
|
||||
"joinLink": "<string>",
|
||||
"chatId": <long>,
|
||||
"callName": "<string>",
|
||||
"participantsCount": <int>,
|
||||
"previewParticipantIds": [<long>, ...],
|
||||
"startAt": <long>,
|
||||
"type": <int>,
|
||||
"owner": <...>
|
||||
}
|
||||
```
|
||||
|
||||
Этот же объект приходит в превью ссылки (`LINK_INFO`, 89).
|
||||
|
||||
### 1.3 Вход по ссылке — opcode 166 `VIDEO_CHAT_JOIN`
|
||||
|
||||
`p000/vjb.java` → `m30940b()`:
|
||||
|
||||
```jsonc
|
||||
// request
|
||||
{
|
||||
"joinLink": "<string>", // ApiProtocol.PARAM_JOIN_LINK
|
||||
"isVideo": <bool>,
|
||||
"internalParams": "<json-string>"
|
||||
}
|
||||
```
|
||||
|
||||
Deep-link/роут разбирается в `p000/wg1.java` по ключам
|
||||
`call_link`, `is_link_call`, `call_chat_id`, `call_title`, `chat_id`.
|
||||
|
||||
### 1.4 `internalParams`
|
||||
|
||||
JSON-строка, сериализуется из `InternalParamsDto`
|
||||
(`ru/ok/android/externcalls/sdk/api/delegate/InternalParamsDto`), собирается в `p000/f98.java`:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"platform": "ANDROID", // UploadHelper.SDK_TYPE_STRING
|
||||
"sdkVersion": "0.2.3",
|
||||
"clientAppKey": "<string>",
|
||||
"deviceId": "<string>",
|
||||
"protocolVersion": 6, // 6 если multiple-devices включён, иначе 5
|
||||
"domainId": "<string|null>",
|
||||
"onlyAdminCanRecord": false,
|
||||
"isWaitForAdminEnabled": <bool>,
|
||||
"hexCapability": "1877f" // см. раздел 6
|
||||
}
|
||||
```
|
||||
|
||||
### 1.5 Ответ: параметры конференции
|
||||
|
||||
Разбирается `CallInfoParser` / `ConversationParams` / `CallInfo`, ключи из `ApiProtocol`:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"endpoint": "<wss url>", // сюда открываем signaling-сокет
|
||||
"wt_endpoint": "<wss url>", // watch-together
|
||||
"id": "<string>",
|
||||
"token": "<string>",
|
||||
"join_link": "<string>",
|
||||
"client_type": "<string>",
|
||||
"device_idx": <int>,
|
||||
"is_concurrent": <bool>,
|
||||
"p2p_forbidden": <bool>,
|
||||
"upload_url": "<string>",
|
||||
"stun_server": { "urls": [ ... ] },
|
||||
"turn_server": { "urls": [ ... ], "username": "...", "credential": "..." }
|
||||
}
|
||||
```
|
||||
|
||||
Плюс `wsIps` / `wtIps` — списки IP для обхода DNS.
|
||||
|
||||
---
|
||||
|
||||
## 2. Signaling: конверт сообщений
|
||||
|
||||
WebSocket на `endpoint`, полезная нагрузка — **JSON строкой** (не msgpack).
|
||||
|
||||
**Клиент → сервер** (`p000/r7l.java:m26483a`, `p000/tkf.java:117`):
|
||||
|
||||
```jsonc
|
||||
{ "command": "<имя>", ...поля..., "sequence": <long> }
|
||||
```
|
||||
|
||||
**Сервер → клиент**, три вида (`p000/tkf.java:m29114f`, `p000/r51.java:100`):
|
||||
|
||||
```jsonc
|
||||
{ "type": "response", "sequence": <long>, "response": "<имя команды>", ... }
|
||||
{ "type": "error", "sequence": <long>, "error": "<код>", "errorCode": <int>, "recoverable": <bool> }
|
||||
{ "notification": "<имя>", ... } // + "stamp": <long> для порядка
|
||||
```
|
||||
|
||||
Коды ошибок, которые встречаются: `service-unavailable`, `participants-limit-reached` (+`limit`),
|
||||
`conversation-ended`, `conversation-not-found`, `conversation-recording`, `closed-conversation`,
|
||||
`illegal-conversation-state`, `illegal-participant-state`, `invalid-request`, `invalid-token`,
|
||||
`no-call`, `call-unfeasible`, `command-not-delivered`, `command-discarded`,
|
||||
`command-can-not-be-postponed`, `movie-not-found`, `movie-limit-exceeded`.
|
||||
|
||||
### 2.1 Идентификатор участника
|
||||
|
||||
`participantId` в разных местах в двух формах:
|
||||
|
||||
- разложенный: `{"participantId": <long>, "participantType": "GROUP"|"USER", "deviceIdx": <int>}`
|
||||
- строкой (`mq1.m20652b()`): `u<id>` для юзера, `g<id>` для группы, с суффиксом устройства
|
||||
|
||||
Идентификаторы медиа-треков (`p000/r7l.java:m26478K`):
|
||||
|
||||
```
|
||||
<participantId>:sCAMERA // камера
|
||||
<participantId>:sSCREEN // ДЕМОНСТРАЦИЯ ЭКРАНА
|
||||
<participantId>:sMOVIE:m<id> // расшаренное видео
|
||||
<participantId>:sSTREAM
|
||||
<participantId>:sANIMOJI
|
||||
audio-<participantId> // аудио-трек
|
||||
video-<participantId> // видео-трек
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Медиа: камера / микрофон / экран
|
||||
|
||||
### 3.1 Своё состояние — `change-media-settings`
|
||||
|
||||
`p000/zkf.java` + `p000/r7l.java:m26497o`. Шлётся **целиком, не дельтой**, при любом переключении:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"command": "change-media-settings",
|
||||
"mediaSettings": {
|
||||
"isVideoEnabled": <bool>, // камера
|
||||
"isAudioEnabled": <bool>, // микрофон
|
||||
"isScreenSharingEnabled": <bool>, // ДЕМОНСТРАЦИЯ ЭКРАНА
|
||||
"isAnimojiEnabled": <bool>,
|
||||
"isFastScreenSharingEnabled": <bool>, // только если включён fastScreenShare
|
||||
"isAudioSharingEnabled": <bool> // только если включён audioShare
|
||||
},
|
||||
"sequence": <long>
|
||||
}
|
||||
```
|
||||
|
||||
Последние два поля **условные** — добавляются только когда соответствующая фича согласована
|
||||
при подключении. Если слать их всегда — есть риск, что старый сервер отвергнет команду.
|
||||
|
||||
### 3.2 Чужое состояние — нотификации
|
||||
|
||||
```jsonc
|
||||
{ "notification": "media-settings-changed",
|
||||
"participantId": <long>, "participantType": "...", "deviceIdx": <int>,
|
||||
"mediaSettings": { "isAudioEnabled": ..., "isVideoEnabled": ...,
|
||||
"isScreenSharingEnabled": ..., "isAnimojiEnabled": ... } }
|
||||
```
|
||||
|
||||
```jsonc
|
||||
// админ принудительно выключил медиа — клиент ОБЯЗАН применить и погасить у себя
|
||||
{ "notification": "force-media-settings-change", "mediaSettings": { ... } }
|
||||
```
|
||||
|
||||
```jsonc
|
||||
{ "notification": "audio-activity", "activeParticipants": ["u123", ...] }
|
||||
{ "notification": "speaker-changed", "speaker": "u123" }
|
||||
```
|
||||
|
||||
### 3.3 Демонстрация экрана — детали
|
||||
|
||||
Что нужно, чтобы экран реально поехал (`ScreenCaptureManagerImpl.setScreenCaptureEnabled`):
|
||||
|
||||
1. **Capability `SCREEN_TRACK_PRODUCER` (бит 0)** должен быть в маске (см. 6).
|
||||
Без него сервер не примет screen-трек. `SCREEN_TRACK_CONSUMER` (бит 4) — чтобы видеть чужой.
|
||||
2. Проверяется медиа-опция `screenshareState`: если админ поставил `MUTE_PERMANENT`
|
||||
на `SCREEN_SHARING` — включение молча игнорируется.
|
||||
3. `change-media-settings` с `isScreenSharingEnabled: true`.
|
||||
4. В PeerConnection добавляется **отдельный видео-трек** с id `<pid>:sSCREEN`
|
||||
(unified plan, вторая m-line) — камера при этом остаётся своим треком `<pid>:sCAMERA`.
|
||||
Это самое частое место поломки: если слать экран в тот же трек, что и камеру,
|
||||
удалённая сторона увидит либо камеру, либо ничего.
|
||||
5. Foreground-сервис `CallScreenShareService` с типом `mediaProjection` — андроид-специфика,
|
||||
но без него система убивает захват.
|
||||
|
||||
**Про 1:1 (topology `DIRECT`).** Топология бывает `DIRECT` (p2p) и `SERVER` (`p000/f9h.java`).
|
||||
Само видео экрана в `DIRECT` работает, а вот **звук демонстрации** (`setAudioCaptureEnabled`,
|
||||
`n61.m21001I`) жёстко требует `SERVER`:
|
||||
|
||||
```java
|
||||
if (m21020p() && this.f46846n0.m24163I(f9h.f22608c)) { // f22608c == SERVER
|
||||
```
|
||||
|
||||
Апгрейд топологии клиент запрашивает сам:
|
||||
|
||||
```jsonc
|
||||
{ "command": "switch-topology", "topology": "SERVER", "force": false }
|
||||
```
|
||||
|
||||
В стоковом клиенте это шлётся при развале p2p ICE (`onTopologyUpgradeProposed`), но команда
|
||||
доступна в любой момент. Сервер отвечает нотификацией:
|
||||
|
||||
```jsonc
|
||||
{ "notification": "topology-changed", "topology": "SERVER" | "DIRECT" }
|
||||
```
|
||||
|
||||
Практический вывод для нашего клиента: если экран в 1:1 не едет — проверять в порядке
|
||||
(1) бит capability, (2) отдельный `sSCREEN`-трек, (3) при необходимости `switch-topology → SERVER`.
|
||||
|
||||
### 3.4 Согласование фич при подключении
|
||||
|
||||
`p000/r7l.java:m26485c` — объект, который клиент отдаёт при инициализации сессии.
|
||||
Именно он включает `fastScreenShare` и остальное:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"maxH264Decoders": <int>,
|
||||
"estimatedPerformanceIndex": <int>, // опционально
|
||||
"producerNotificationDataChannelVersion": 7,
|
||||
"producerCommandDataChannelVersion": <int>,
|
||||
"audioMix": true,
|
||||
"consumerUpdate": <bool>,
|
||||
"onDemandTracks": <bool>,
|
||||
"singleSession": true,
|
||||
"unifiedPlan": true,
|
||||
"fastScreenShare": true,
|
||||
"producerScreenDataChannelVersion": 1, // условно
|
||||
"consumerScreenDataChannelVersion": 1, // условно
|
||||
"animojiDataChannelVersion": 2, // условно
|
||||
"animojiBackendRender": true, // условно
|
||||
"asrDataChannelVersion": 1, // условно
|
||||
"consumerFastScreenShare": true, // условно
|
||||
"consumerFastScreenShareQualityOnDemand": true,
|
||||
"audioShare": true, // условно
|
||||
"simulcast": true, // условно
|
||||
"simulcastNativeOrder": true, // условно
|
||||
"red": true,
|
||||
"videoTracksCount": <int>, // если > 0
|
||||
"csrcAccessible": true // если videoTracksCount > 0
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. SDP / ICE
|
||||
|
||||
Всё заворачивается в `transmit-data` (`p000/r7l.java:m26493k`, `m26502t`, `m26503u`):
|
||||
|
||||
```jsonc
|
||||
// offer / answer
|
||||
{ "command": "transmit-data",
|
||||
"participantId": <long>, "participantType": "...", "deviceIdx": <int>,
|
||||
"data": {
|
||||
"sdp": { "type": "offer"|"answer", "sdp": "<sdp>", "p2pRelay": "true" },
|
||||
"capabilities": "1877f", // hex, если != 0
|
||||
"label": "<string>" // опционально
|
||||
},
|
||||
"sequence": <long> }
|
||||
|
||||
// одиночный кандидат
|
||||
{ "command": "transmit-data", ..., "data": { "candidate": { "candidate": "...", "sdpMid": "...", "sdpMLineIndex": <int> } } }
|
||||
|
||||
// удалённые кандидаты
|
||||
{ "command": "transmit-data", ..., "data": { "candidates-removed": [ {...}, ... ] } }
|
||||
```
|
||||
|
||||
`p2pRelay: "true"` (строкой!) добавляется только в offer при включённом P2P-relay.
|
||||
|
||||
Топология SERVER добавляет отдельный набор команд (`p000/ooh.java`):
|
||||
`allocate-consumer`, `accept-producer`, `request-realloc` — аллокация консьюмеров/продюсеров
|
||||
на медиасервере. Плюс `change-simulcast`, `update-display-layout`, `report-network-stat`,
|
||||
`report-perf-stat` (`p000/ug8.java`).
|
||||
|
||||
---
|
||||
|
||||
## 5. Групповые звонки: администрирование
|
||||
|
||||
Роли (`p000/pq1.java`): **`CREATOR`**, **`ADMIN`**, **`SPEAKER`**.
|
||||
|
||||
### 5.1 Мьют участников
|
||||
|
||||
```jsonc
|
||||
// запросить включение медиа у участника (или у всех, если participantId=null)
|
||||
{ "command": "mute-participant",
|
||||
"participantId": "u123" | null,
|
||||
"requestedMedia": ["AUDIO"|"VIDEO"|"SCREEN_SHARING"|"MOVIE_SHARING", ...],
|
||||
"roomId": "<id>" // опционально, для session room
|
||||
}
|
||||
```
|
||||
|
||||
```jsonc
|
||||
// жёстко выставить состояния мьюта
|
||||
{ "command": "mute-participant",
|
||||
"participantId": "u123" | null,
|
||||
"muteStates": {
|
||||
"AUDIO": "UNMUTE"|"MUTE"|"MUTE_PERMANENT"|null,
|
||||
"VIDEO": ...,
|
||||
"SCREEN_SHARING": ...,
|
||||
"MOVIE_SHARING": ...
|
||||
},
|
||||
"roomId": "<id>" // опционально
|
||||
}
|
||||
```
|
||||
|
||||
`MUTE` — выключить сейчас (можно включить обратно), `MUTE_PERMANENT` — запретить.
|
||||
Внутренние состояния клиента: `UNMUTED`, `UNMUTED_BUT_MUTED_ONCE`, `MUTED_PERMANENT`,
|
||||
`MUTED_PERMANENT_BUT_UNMUTED_ONCE`.
|
||||
|
||||
Быстрый мьют микрофона (`ConversationImpl:2331`, `:3446`):
|
||||
|
||||
```jsonc
|
||||
{ "command": "switch-micro", "eId": "u123", "muteTarget": <bool> }
|
||||
{ "command": "switch-micro", "all": true, "muteTarget": true }
|
||||
```
|
||||
|
||||
Обратная нотификация: `{ "notification": "switch-micro", "mute": <bool> }`
|
||||
(если поля `mute` нет — клиент логирует `switch-micro without 'mute'` и игнорирует).
|
||||
|
||||
### 5.2 Роли и промоушен
|
||||
|
||||
```jsonc
|
||||
{ "command": "promote-participant", "participantId": "u123", "demote": <bool> }
|
||||
|
||||
{ "command": "grant-roles", "participantId": "u123",
|
||||
"roles": ["ADMIN", "SPEAKER", ...], "revoke": <bool> }
|
||||
|
||||
// стерео-режим / запрос слова
|
||||
{ "command": "request-promotion", ... } // + reject / unrequest
|
||||
{ "command": "accept-promotion", ... }
|
||||
{ "command": "get-hand-queue" }
|
||||
```
|
||||
|
||||
### 5.3 Участники
|
||||
|
||||
```jsonc
|
||||
// по внешним id
|
||||
{ "command": "add-participant",
|
||||
"externalIds": [ ... ],
|
||||
"unban": true, // опционально
|
||||
"payload": "{\"show_chat_history\":true}" } // опционально, СТРОКОЙ
|
||||
|
||||
// по ссылке/QR
|
||||
{ "command": "add-participant", "participantIdAsQRCodeLink": "<link>" }
|
||||
|
||||
{ "command": "remove-participant", "participantId": "u123" }
|
||||
|
||||
{ "command": "pin-participant", "participantId": "u123",
|
||||
"unpin": <bool>, "roomId": "<id>" }
|
||||
|
||||
{ "command": "get-participant-list-chunk",
|
||||
"count": <int>, "listType": "GRID"|"SIDE", "roomId": "<id>" }
|
||||
|
||||
{ "command": "get-waiting-hall", "count": <int>, "fromId": "...", "backward": <bool> }
|
||||
```
|
||||
|
||||
### 5.4 Состояния участника (рука, помощь)
|
||||
|
||||
```jsonc
|
||||
{ "command": "change-participant-state",
|
||||
"participantId": "u123", // отсутствует → своё состояние
|
||||
"participantState": { "state": { "hand": "1"|"0", "drat": "1"|"0" } } }
|
||||
|
||||
{ "command": "put-hands-down" } // опустить руки всем
|
||||
```
|
||||
|
||||
`hand` — поднятая рука, `drat` — запрошена помощь (`ASSISTANCE_REQUESTED`).
|
||||
Значения строковые `"1"` / `"0"`.
|
||||
|
||||
### 5.5 Опции конференции
|
||||
|
||||
```jsonc
|
||||
{ "command": "change-options",
|
||||
"options": { "WAITING_HALL": true, "AUDIENCE_MODE": false, ... } }
|
||||
```
|
||||
|
||||
Доступные опции (`p000/l61.java`): `REQUIRE_AUTH_TO_JOIN`, `WAITING_HALL`, `RECURRING`,
|
||||
`FEEDBACK`, `AUDIENCE_MODE`, `ASR`, `WAIT_FOR_ADMIN`, `ADMIN_IS_HERE`.
|
||||
|
||||
```jsonc
|
||||
{ "command": "enable-feature-for-roles",
|
||||
"feature": "ADD_PARTICIPANT"|"ADMIN"|"ASR"|"MOVIE_SHARE"|"RECORD"|"SPEAKER",
|
||||
"roles": ["ADMIN", "SPEAKER", ...] }
|
||||
```
|
||||
|
||||
### 5.6 Запись
|
||||
|
||||
```jsonc
|
||||
{ "command": "record-start",
|
||||
"movieId": <long>, "name": "...", "description": "...",
|
||||
"privacy": "...", "groupId": <long>, "albumId": "...",
|
||||
"streamMovie": <bool>, "roomId": "<id>" }
|
||||
|
||||
{ "command": "record-stop", "roomId": "<id>", "remove": <bool> }
|
||||
```
|
||||
|
||||
Ответ на `record-start`: `{"type":"response","sequence":N,"response":"record-start","recordMovieId":<id>}`.
|
||||
|
||||
### 5.7 Комнаты (session rooms)
|
||||
|
||||
```jsonc
|
||||
{ "command": "switch-room", "participantId": "u123", "toRoomId": "<id>" }
|
||||
{ "command": "update-rooms", "rooms": [ { "id": "...",
|
||||
"addParticipantIds": [...], "removeParticipantIds": [...] } ] }
|
||||
{ "command": "get-rooms" }
|
||||
{ "command": "room-query", ... }
|
||||
{ "command": "room-tx", ... }
|
||||
```
|
||||
|
||||
### 5.8 Совместный просмотр / шаринг ссылки
|
||||
|
||||
```jsonc
|
||||
{ "command": "add-movie", "movieId": <long>, ... }
|
||||
{ "command": "update-movie", "movieId": <long>, "pause": <bool> }
|
||||
{ "command": "remove-movie", "movieId": <long> }
|
||||
|
||||
{ "command": "start-url-sharing", "sharedUrl": "<url>" }
|
||||
{ "command": "stop-url-sharing" }
|
||||
```
|
||||
|
||||
### 5.9 Прочее
|
||||
|
||||
```jsonc
|
||||
{ "command": "hangup", "reason": "..." }
|
||||
{ "command": "accept-call", ... }
|
||||
{ "command": "custom-data", "participantId": <long>, "participantType": "...",
|
||||
"deviceIdx": <int>, "data": { ...произвольный JSON... } }
|
||||
{ "command": "request-asr", ... }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Capabilities
|
||||
|
||||
`ClientCapabilities` — битовая маска, передаётся **hex-строкой** (в `capabilities` внутри
|
||||
`transmit-data.data` и в `hexCapability` внутри `internalParams`).
|
||||
|
||||
| Бит | Имя | Что даёт |
|
||||
|-----|-----|----------|
|
||||
| 0 | `SCREEN_TRACK_PRODUCER` | **отдавать демонстрацию экрана** |
|
||||
| 1 | `VIDEO_TRACKS` | видео-треки |
|
||||
| 2 | `WAITING_HALL` | зал ожидания |
|
||||
| 3 | `FILTER_DEFAULTS` | |
|
||||
| 4 | `SCREEN_TRACK_CONSUMER` | **принимать чужую демонстрацию** |
|
||||
| 5 | `ADMIN_MUTE_NOTIFY` | нотификации админского мьюта |
|
||||
| 6 | `WATCH_MOVIE` | совместный просмотр |
|
||||
| 8 | `SESSION_ROOMS` | комнаты |
|
||||
| 9 | `VMOJI` | анимоджи |
|
||||
| 10 | `CALL_TO_CONTACTS` | |
|
||||
| 11 | `AUDIENCE_MODE` | режим зрителей |
|
||||
| 14 | `SESSION_STATE_UPDATES` | |
|
||||
| 15 | `ADD_PARTICIPANT` | добавление участников |
|
||||
| 16 | `USE_P2P_RELAY` | p2p-relay |
|
||||
| 17 | `WAIT_FOR_ADMIN` | ждать админа |
|
||||
| 18 | `HOLD` | удержание |
|
||||
|
||||
Дефолт стокового клиента — биты `0,1,2,3,4,5,6,8,9,10,15,16` = **`0x1877f`**.
|
||||
Обрати внимание: `AUDIENCE_MODE`(11), `SESSION_STATE_UPDATES`(14), `WAIT_FOR_ADMIN`(17),
|
||||
`HOLD`(18) в дефолт **не входят** и включаются по конфигу.
|
||||
|
||||
---
|
||||
|
||||
## 7. Нотификации сервер → клиент (полный список)
|
||||
|
||||
Из switch в `p000/r51.java` (ключ `"notification"`):
|
||||
|
||||
**Сессия и участники**
|
||||
`connection`, `registered-peer`, `participant-added`, `participant-joined`, `accepted-call`,
|
||||
`participants-state-changed`, `participant-state-changed`, `participant-animoji-changed`,
|
||||
`decorative-participant-id-changed`, `session-state`, `stalled-activity`, `hold`, `hungup`,
|
||||
`closed-conversation`, `multiparty-chat-created`
|
||||
|
||||
**Медиа**
|
||||
`media-settings-changed`, `force-media-settings-change`, `audio-activity`, `speaker-changed`,
|
||||
`switch-micro`, `mute-participant`, `topology-changed`, `realloc-con`, `transmitted-data`
|
||||
|
||||
**Админка**
|
||||
`roles-changed`, `promote-participant`, `promotion-approved`, `options-changed`,
|
||||
`feature-set-changed`, `features-per-role-changed`, `pin-participant`, `settings-update`
|
||||
|
||||
**Комнаты**
|
||||
`room-updated`, `rooms-updated`, `room-participants-updated`, `chat-room-updated`
|
||||
|
||||
**Контент**
|
||||
`record-started`, `record-stopped`, `movie-share-started`, `movie-share-stopped`,
|
||||
`url-sharing-info-updated`, `asr-started`, `asr-stopped`, `chat-message`, `custom-data`,
|
||||
`join-link-changed`, `feedback`, `rate-call-data`
|
||||
|
||||
---
|
||||
|
||||
## 8. Что из этого чинит наши баги
|
||||
|
||||
**Демонстрация экрана в 1:1** — раздел 3.3. Порядок проверки: бит 0 в capabilities →
|
||||
отдельный трек `<pid>:sSCREEN` → `change-media-settings` с полным объектом →
|
||||
при необходимости `switch-topology → SERVER` (обязательно, если нужен звук демонстрации).
|
||||
|
||||
**Групповые звонки** — скорее всего не хватает серверной топологии целиком:
|
||||
`allocate-consumer` / `accept-producer` / `request-realloc` (`p000/ooh.java`),
|
||||
плюс `get-participant-list-chunk` для подгрузки участников пачками и
|
||||
`update-display-layout` для раскладки. В `DIRECT` группа не работает by design.
|
||||
|
||||
**Условные поля.** `isFastScreenSharingEnabled` / `isAudioSharingEnabled` в `mediaSettings`
|
||||
и половина полей в объекте согласования фич добавляются **только** при включённой фиче.
|
||||
Слать их безусловно — рискованно.
|
||||
Reference in New Issue
Block a user