diff --git a/doc/chat_call_arch.md b/doc/chat_call_arch.md index 0101a64e..b7c88e6c 100644 --- a/doc/chat_call_arch.md +++ b/doc/chat_call_arch.md @@ -1,17 +1,18 @@ -# P2P аудио-звонок пользователю — архитектура +# P2P аудио- и видеозвонок пользователю — архитектура Статус: реализовано. Звонок использует роутер, CM подготавливает оптимальный путь. ## 1. Область -Голос 1:1. Звонок можно инициировать **из CHAT-группы или из p2p-чата (DM)** — модуль -не привязан к типу группы: он оперирует `group_id` + `peer_node_id` (для DM `group_id` — -DM-группа, маршрутизация через `etcp_router` идентична). Протокол расширяем под видео -(track-модель), реализуется только аудио. Работает desktop↔desktop и desktop↔android +Аудио и видео 1:1. Звонок можно инициировать **из CHAT-группы или из p2p-чата (DM)**. +Модуль оперирует `group_id` + `peer_node_id`. Для `dm:` функция `dm_call_route` +выбирает общую CHAT-группу с живым BGP-маршрутом к собеседнику; отдельная DM-группа не создаётся. +Аудио и видео используют отдельные каналы `etcp_router`; возможности видеокодеков +согласуются в активном звонке (см. [call_video.txt](call_video.txt)). Работает desktop↔desktop и desktop↔android (обе стороны компилируют общий стек `etcp_router` / `topo_group` (BGP) / `conn_mgr`). Полный цикл: проверка онлайна (BGP) → INVITE → звонок → ACCEPT/DECLINE → -аудиопоток через роутер → HANGUP. CM инициируется вызывающим при старте, вызываемым при принятии вызова. +аудиопоток и опциональное видео через роутер → HANGUP. CM инициируется вызывающим при старте, вызываемым при принятии вызова. ### Входящие вызовы и телефонная занятость @@ -42,11 +43,12 @@ Android обычного звонка показывает своё уведом | Файл | Язык | Сборки | Назначение | |---|---|---|---| -| `src/call/call_proto.h` | C | везде | wire-формат, subcmd, svc_id `ETCP_RT_ID_CALL 0x36` | +| `src/call/call_proto.h` | C | везде | wire-формат сигналинга (`0x36`) и аудио (`0x3F`), subcmd | | `src/call/call.h` / `call.c` | C | везде (autotools + GUI) | сигналинг, реестр сессий, таймеры, watchdog (20с по приёму), релей медиа с backpressure, счётчики | | `src/call/call_audio.h/c` | C | только GUI-сборки | Opus encode/decode, фрейминг, pull-обвязка с тонами; регистрация через `call_audio_ops` | | `src/call/call_tones.h/c` | C | только GUI-сборки | генератор тонов (glitch/ended), синус | | `src/call/call_jitter.h/cpp` | C++ | только GUI-сборки | SoundTouch джиттер-буфер, C API | +| `src/call/call_video.h/c`, `call_video_transport.h/c` | C | везде | формат видеотрека, согласование возможностей и передача кадров отдельными каналами | Autotools-сборка демона остаётся C-only (`.cpp` не компилируется); сигналинг `call.c` присутствует и в демоне, аудиопроцессинг — только там, где есть GUI. @@ -60,11 +62,22 @@ Desktop направляет сообщения FFmpeg и Qt в общий `debu Отсутствующая desktop-запись приложения создаётся в XDG data applications для регистрации в portal. -## 3. Протокол (сигналинг svc_id 0x36, первый байт payload = subcmd) +## 3. Протокол -Формат приёма: `entry->dgram[0]=svc_id`, служебный заголовок роутера до +Каналы роутера независимы: + +| svc_id | Константа | Назначение | +|---|---|---| +| `0x36` | `ETCP_RT_ID_CALL` | сигналинг звонка | +| `0x3D` | `ETCP_RT_ID_CALL_VIDEO_CONTROL` | согласование и восстановление видеотрека | +| `0x3E` | `ETCP_RT_ID_CALL_VIDEO` | видеокадры | +| `0x3F` | `ETCP_RT_ID_CALL_AUDIO` | аудиокадры `CALL_MEDIA` | + +Формат приёма сигналинга и аудио: `entry->dgram[0]=svc_id`, служебный заголовок роутера до `ROUTER_SVC_PAYLOAD_OFF=26`, затем subcmd и тело команды. +Сигналинг (`0x36`): + ``` CALL_INVITE 0x01 {call_id:8, group_id:8, num_tracks:1, force:1, expires_us:8, tracks:[{type:AUDIO, codec:OPUS, sr:4, ch:1, bitrate:4}]} CALL_RINGING 0x02 {call_id:8} @@ -75,11 +88,16 @@ CALL_CANCEL 0x06 {call_id:8} CALL_HANGUP 0x07 {call_id:8, reason:1} CALL_END_ACK 0x08 {call_id:8} CALL_PHONE_BUSY 0x09 {call_id:8} +``` + +Аудио (`0x3F`): + +``` CALL_MEDIA 0x10 {call_id:8, track_id:1, seq:2, ts_ms:4, capture_pts_us:8, capture_epoch:8, codec_delay_samples:4, payload} ``` - `call_id` — случайный uint64, генерит вызывающий; идентифицирует сессию на обеих сторонах. -- `group_id` — CHAT-группа или DM-группа (общая маршрутизация). +- `group_id` — CHAT-группа; для DM-звонка выбирается общая группа участников с живым BGP-маршрутом. - Сигналинг: `etcp_route_send(..., ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT)`. - Сигналинг, аудио и видео: обязательные `ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT` между конечными узлами. - AES-128-CCM шифрует тело, Ed25519 подписывает заголовок роутера и шифртекст. Посредники не имеют ключа для расшифровки. @@ -89,7 +107,8 @@ CALL_MEDIA 0x10 {call_id:8, track_id:1, seq:2, ts_ms:4, capture_pts_us:8, capt Пустые служебные пакеты роутера не являются медиаданными. Перехода на открытое медиа при ошибках нет. - Каждый пакет получает случайный nonce(13) от OpenSSL CSPRNG; ошибка RNG прекращает шифрование. Надбавка к сервисным данным: nonce(13) + CRC(4) + CCM tag(16) + Ed25519(64) = 97 байт. -- `CALL_MEDIA` — общий контейнер; `track_id` оставляет место видео (`type=VIDEO`). +- `CALL_MEDIA` переносит только аудио (`track_id=CALL_TRACK_AUDIO`); остальные значения приёмник отклоняет. + Видео имеет отдельный формат и транспорт: `call_video.h`, `call_video_transport.h`, [call_video.txt](call_video.txt). - Аудио: Opus 48kHz mono, 20ms (960 сэмплов), ~24–32 kbps. ~50 кадров/с. - Заголовок медиа — 36 байт с subcmd. `ts_ms` — время отправки для оценки сетевого джиттера; `capture_pts_us` — время первого входного сэмпла в монотонной шкале отправителя, `INT64_MIN` при отсутствии метки. @@ -375,27 +394,45 @@ CHAT_EVT_CALL_ERROR 38 [call_id:8][err:1][text:var] CHAT_EVT_CALL_AUDIO_ROUTE 39 (Android: текущий маршрут/наличие гарнитуры) ``` -## 10. Публичный API (call.h) +## 10. Основные функции публичного API -``` -int call_start(ch_id, peer_node_id, out_call_id); // caller (post в uasync) -void call_accept(call_id); // callee -void call_decline(call_id); -void call_hangup(call_id); -int call_is_online(ch_id, peer_node_id); // BGP-проверка для UI -// аудио-поток (общий процессинг в C-ядре): -int call_encode_pcm(call_id, pcm, n, opus_out, cap); // single-writer: audio thread -int call_send_encoded(call_id, opus, len); // post → uasync → tx_q + flush -int call_pull_pcm(call_id, pcm_out, max); // single-writer: audio thread (ajb+tones) -void call_release(call_id); // post → uasync: освободить аудио-объекты +`call.h`: сигналинг и отправка готового Opus выполняются в uasync-потоке. +Из других потоков команды передаются через `uasync_post`/мост GUI и соответствующие `call_*_trampoline`. +`ch_id` — идентификатор CHAT-группы либо `dm:`; `out_call_id` — значение, заранее сгенерированное вызывающим. + +```c +void call_start(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t peer_node_id, uint64_t out_call_id); +void call_accept(struct UTUN_INSTANCE* inst, uint64_t call_id); +void call_decline(struct UTUN_INSTANCE* inst, uint64_t call_id); +void call_hangup(struct UTUN_INSTANCE* inst, uint64_t call_id); +int call_is_online(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t peer_node_id); +int call_send_media(struct UTUN_INSTANCE* inst, uint64_t call_id, const uint8_t* opus, int len, + const struct call_capture_time* timing); ``` -`call_init / call_destroy` — вызываются из `chat_core_init / chat_core_destroy`. +`call_audio.h`: движок сам кодирует PCM и ставит отправку Opus в uasync. +Захват и воспроизведение используют по одному аудиопотоку на направление. + +```c +int call_audio_feed_pcm(uint64_t call_id, const int16_t* pcm, int count, int64_t capture_pts_us); +int call_audio_feed_prepared_pcm(uint64_t call_id, const int16_t* pcm, const uint8_t* mute_mask, int64_t capture_pts_us); +int call_audio_pull_pcm(uint64_t call_id, int16_t* out, int max_samples); +void call_audio_release(uint64_t call_id); +``` + +`feed_pcm` принимает ровно 960 отсчётов; `feed_prepared_pcm` — 960 отсчётов после платформенного AEC +и маску mute. `call_audio_release` вызывается в GUI-потоке после остановки обоих направлений I/O. +Создание движка — `call_audio_start`, для подготовленного PCM — `call_audio_start_prepared`. +Видео настраивается через `call_video_configure`, кадры отправляются через `call_video_send_frame` +в uasync-потоке; полный контракт — в `call_video_transport.h`. + +`call_init / call_destroy` вызываются из `chat_service_start / chat_service_stop` в `utun_instance.c`: +инициализация после `dm_core_init`, освобождение перед `dm_core_destroy`. ## 11. Платформенный I/O - **Desktop:** `tools/chatgui/src/call_audio_engine.cpp` — miniaudio full-duplex - (capture → `call_encode_pcm`, `call_pull_pcm` → playback) + окно звонка (UI) + (capture через VoiceAudioIo → `call_audio_feed_prepared_pcm`, `call_audio_pull_pcm` → playback) + окно звонка (UI) + события в `gui_bridge_impl.cpp`. - **Android:** JNI `nativeCall*` + Kotlin `AudioRecord`/`AudioTrack`. **Переключение источника:** `AudioManager` (API31+ `setCommunicationDevice` + @@ -412,7 +449,8 @@ void call_release(call_id); // post → uasync: о mmx_optimized, sse_optimized) как C++. `call.c` попадёт через GLOB `src/*.c`. - `tools/chatgui-android/libutun_lite/CMakeLists.txt` + `utun_sources.cmake`: `LANGUAGES C CXX`, те же файлы. -- `src/transport_layer/etcp_api.h`: `#define ETCP_RT_ID_CALL 0x34`. +- `src/transport_layer/etcp_api.h`: `ETCP_RT_ID_CALL=0x36`, `ETCP_RT_ID_CALL_VIDEO_CONTROL=0x3D`, + `ETCP_RT_ID_CALL_VIDEO=0x3E`, `ETCP_RT_ID_CALL_AUDIO=0x3F`. - `lib/debug_config.h`: `DEBUG_CATEGORY_CALL 31`, `DEBUG_CATEGORY_COUNT` → 32. ## 13. Порядок задач diff --git a/src/call/call_proto.h b/src/call/call_proto.h index 8ce7fc28..2772c860 100644 --- a/src/call/call_proto.h +++ b/src/call/call_proto.h @@ -1,4 +1,6 @@ -// call_proto.h — wire-формат и константы P2P аудио-звонка (svc_id ETCP_RT_ID_CALL 0x36) +// call_proto.h — wire-формат сигналинга (ETCP_RT_ID_CALL 0x36) и аудио (ETCP_RT_ID_CALL_AUDIO 0x3F). +// CALL_MEDIA несёт только CALL_TRACK_AUDIO; видео использует call_video.h и отдельные каналы +// ETCP_RT_ID_CALL_VIDEO_CONTROL 0x3D / ETCP_RT_ID_CALL_VIDEO 0x3E (call_video_transport.h). // // Приём/отправка через etcp_router. На проводе первый байт payload = subcmd. // Сигналинг и медиа требуют ROUTE_CRYPTO_SIGN|ROUTE_CRYPTO_ENCRYPT: шифрование тела