You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

14 KiB

P2P аудио-звонок пользователю — архитектура

Статус: спроектировано, ждёт реализации.

1. Область

Голос 1:1. Звонок можно инициировать из CHAT-группы или из p2p-чата (DM) — модуль не привязан к типу группы: он оперирует group_id + peer_node_id (для DM group_id — DM-группа, маршрутизация через etcp_router идентична). Протокол расширяем под видео (track-модель), реализуется только аудио. Работает desktop↔desktop и desktop↔android (обе стороны компилируют общий стек etcp_router / topo_group (BGP) / conn_mgr).

Полный цикл: проверка онлайна (BGP) → INVITE → звонок → ACCEPT/DECLINE → установка соединения (conn_mgr) → аудиопоток → HANGUP.

2. Разбиение файлов (отдельный каталог src/call/)

Файл Язык Сборки Назначение
src/call/call_proto.h C везде wire-формат, subcmd, svc_id ETCP_RT_ID_CALL 0x34
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/audio_jitter.h/cpp C++ только GUI-сборки SoundTouch джиттер-буфер, C API

Autotools-сборка демона остаётся C-only (.cpp не компилируется); сигналинг call.c присутствует и в демоне, аудиопроцессинг — только там, где есть GUI.

Новая категория логирования: DEBUG_CATEGORY_CALL = 31 (добавить в debug_config.h, DEBUG_CATEGORY_COUNT → 32).

3. Протокол (svc_id 0x34, первый байт payload = subcmd)

Формат приёма (как media_delivery, одноуровневый): entry->dgram[0]=svc_id, entry->dgram[1]=subcmd, entry->dgram[2..]=payload.

CALL_INVITE  0x01 {call_id:8, group_id:8, tracks:[{type:AUDIO, codec:OPUS, sr:4, ch:1, bitrate:4}]}
CALL_RINGING 0x02 {call_id:8}
CALL_ACCEPT  0x03 {call_id:8, agreed_tracks}
CALL_DECLINE 0x04 {call_id:8, reason:1}
CALL_BUSY    0x05 {call_id:8}
CALL_CANCEL  0x06 {call_id:8}
CALL_HANGUP  0x07 {call_id:8, reason:1}
CALL_MEDIA   0x10 {call_id:8, track_id:1, seq:2, ts_ms:4, payload}
  • call_id — случайный uint64, генерит вызывающий; идентифицирует сессию на обеих сторонах.
  • group_id — CHAT-группа или DM-группа (общая маршрутизация).
  • Сигналинг: etcp_route_send(..., ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT).
  • Медиа: без подписи (уже внутри шифрованного канала etcp_router).
  • CALL_MEDIA — общий контейнер; track_id оставляет место видео (type=VIDEO).
  • Аудио: Opus 48kHz mono, 20ms (960 сэмплов), ~24–32 kbps. ~50 кадров/с.

4. Машина состояний

Caller: IDLE → OUTGOING(INVITE) → RINGING(RINGING) → CONNECTING(ACCEPT, conn_mgr_open) → ACTIVE → ENDED
Callee: IDLE → INCOMING(INVITE) → RINGING(sent RINGING) → ACCEPTING(accept, conn_mgr_open) → ACTIVE → ENDED

ENDED reasons: LOCAL_HANGUP / REMOTE_HANGUP / DECLINE / BUSY / CANCEL / NO_TRAFFIC / RING_TIMEOUT / CONNECT_FAIL.

Таймауты:

  • кодограмма INVITE 300мс — вызывающий шлёт её весь период вызова (OUTGOING + RINGING) как «сердцебиение»: покрывает потерю INVITE/RINGING и держит маршрут живым;
  • watchdog 2с у вызываемого — нет кодограммы 2с → вызов считается сорванным (отмена потерялась), сессия закрывается локально (CALL_REASON_CANCEL);
  • ring-timeout 45с (вызывающий, нет ответа → CANCEL);
  • connect: conn_mgr CONN_EVENT_TIMEOUT → fail;
  • NO_TRAFFIC 20с — нет входящих медиа-кадров → авто HANGUP.

Завершающие кодограммы (HANGUP/CANCEL/DECLINE) шлются без ретрансмиссий — потеря компенсируется watchdog'ом 2с у вызываемого. Запоздалые кодограммы вызова при идущем разговоре (CONNECTING/ACTIVE) игнорируются.

Онлайн-проверка: topo_node_find_by_id(group, peer) == NULL → «не в сети» (event).

Соединение: на ACCEPT обе стороны conn_mgr_open(inst, group_id, peer, cb, ...); CONN_EVENT_UP → ACTIVE. Медиа идёт etcp_route_send (после подъёма прямого линка роутер идёт напрямую — RTT минимален).

5. Отправка медиа — backpressure через локальный буфер

Требование: не дропать аудио при переполнении роутера — держим в локальном буфере отправки, дренируем при появлении места.

Механика (etcp_router уже предоставляет):

  • etcp_route_send(force=0) возвращает -1, когда send_q роутера полон (count >= ROUTER_MAX_SEND_Q_PACKETS).
  • etcp_router_send_q_has_room(...) — есть ли место (порог ROUTER_MAX_SEND_Q_PACKETS-1).
  • etcp_router_on_send_ready(..., &waiter, drain_cb, arg) — одноразовый waiter: колбэк вызывается, когда send_q->count <= threshold (освободилось место).

В сессии:

  • tx_q — локальная FIFO очередь закодированных кадров (ll_entry с dgram), ёмкостью ~1с (≈50 кадров). При переполнении — дроп старейшего + счётчик c_tx_dropped.
  • Алгоритм (call_tx_flush): пока tx_q не пуст:
    • если send_q_has_room → queue_data_get + etcp_route_send(force=0);
    • иначе — если waiter не зарегистрирован → etcp_router_on_send_ready(... drain_cb) и выход.
  • drain_cb(q, arg) → снова call_tx_flush(session) (re-arm при необходимости).
  • Весь код — в uasync-потоке (single-threaded, реентерабельность как в media_delivery.c:stream_send_chunk_cb).

6. Thread-модель (single-writer)

Объект Владелец
call_session (состояние, encoder, decoder, tx_q, таймеры) uasync-поток
Opus encoder (send) аудио-поток (encode)
Opus decoder (recv) uasync-поток (decode → ajb_push)
ajb push uasync-поток
ajb pull + SoundTouch аудио-поток
call_tones аудио-поток

Поток данных:

  • TX: аудио-поток кодирует → call_send_encoded(call_id, opus, len) → uasync_post → uasync: tx_q + call_tx_flush (backpressure).
  • RX: uasync принимает CALL_MEDIA → decode → ajb_push; аудио-поток call_pull_pcm → ajb_pull (+ тоны).

ajb = mutex-кольцо PCM (гранулярность 20ms/50 ops в сек — мьютекс бесплатен); SoundTouch инстанс заперт на pull-стороне.

Контракт жизненного цикла: GUI стартует аудио-устройство по CALL_ACCEPTED, останавливает по CALL_ENDED и только после остановки (join аудио-потока) вызывает call_release(call_id) (post в uasync) → uasync освобождает session/encoder/decoder/jb. Пока устройство работает — call_pull_pcm/call_send_encoded не гоняются с освобождением.

7. Адаптивный джиттер-буфер (SoundTouch)

Буфер ≤ 1000 мс, target playout ~60 мс; при росте буфера плавно tempo → 1.6x. SoundTouch: USE_QUICKSEEK=1, SEQUENCE_MS=20, SEEKWINDOW_MS=10, OVERLAP_MS=4. API: ajb_create / ajb_push / ajb_pull / ajb_stats / ajb_destroy.

8. Тоны и диагностика

  • Исходящий звонок — две фазы с разными гудками (у вызывающего):
    • фаза 1 «пытаемся связаться» (OUTGOING: INVITE отправлен/ретранслируется, маршрут устанавливается) — гудок «соединение» (короткие бипы);
    • фаза 2 «звоним» (RINGING: получен RINGING, у пира идёт вызов) — длинные гудки (ringback). Гудки генерируются платформенно-нативно: desktop — цикличные MP3 через SoundManager (call_connecting/call_ringing), Android — ToneGenerator (TONE_SUP_DIAL/TONE_SUP_RINGTONE). Переключение — по CHAT_EVT_CALL_RINGING, остановка — по CALL_ACCEPTED/CALL_ENDED/CALL_ERROR/hangup.
  • Сталл >500мс (нет входящих медиа) → в call_pull_pcm микшируется CALL_TONE_GLITCH: пи-пи (2×150мс, gap 100мс) … тишина до 1.0с … пи(200мс) пауза(200мс) пи(200мс), 1000 Гц, цикл пока длится сталл.
  • Завершение звонка → всегда CALL_TONE_ENDED: 3 нисходящих 700/500/400 Гц по 150 мс.
  • Счётчики: tx/rx кадры, c_tx_dropped/c_rx_dropped, дропы буфера, сталлы, tempo-события, причины завершения. Логи DEBUG_CATEGORY_CALL (+стата раз в 1с).

9. GUI-события (extend chat_event.h + gui_bridge.h + android)

CHAT_EVT_CALL_INCOMING   32  [ch_id_len][ch_id][caller_node_id:8][call_id:8]
CHAT_EVT_CALL_RINGING    33  [call_id:8]
CHAT_EVT_CALL_ACCEPTED   34  [call_id:8]
CHAT_EVT_CALL_ENDED      35  [call_id:8][reason:1]
CHAT_EVT_CALL_DECLINED   36  [call_id:8][reason:1]
CHAT_EVT_CALL_STATS      37  [call_id:8][rtt_ms:2][buffer_ms:2][tempo_x100:2]
CHAT_EVT_CALL_ERROR      38  [call_id:8][err:1][text:var]
CHAT_EVT_CALL_AUDIO_ROUTE 39 (Android: текущий маршрут/наличие гарнитуры)

10. Публичный API (call.h)

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_init / call_destroy — вызываются из chat_core_init / chat_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)
    • события в gui_bridge_impl.cpp.
  • Android: JNI nativeCall* + Kotlin AudioRecord/AudioTrack. Переключение источника: AudioManager (API31+ setCommunicationDevice + availableCommunicationDevices; fallback setSpeakerphoneOn + startBluetoothSco), детект гарнитуры (ACTION_HEADSET_PLUG, BT-профиль, ACTION_SCO_AUDIO_STATE_UPDATED), кнопка-цикл трубка/громкая/гарнитура, авто-выбор гарнитуры. nativeCallSetAudioRoute(route).

12. Сборка

  • src/Makefile.am + tests/Makefile.am: call.c (C, без .cpp). src/call/ в include path.
  • tools/chatgui/libutun/CMakeLists.txt: +call_audio.c, call_tones.c, audio_jitter.cpp, soundtouch sources (AAFilter, FIFOSampleBuffer, FIRFilter, InterpolateCubic/Linear/Shannon, RateTransposer, SoundTouch, TDStretch, cpu_detect_x86, 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.
  • lib/debug_config.h: DEBUG_CATEGORY_CALL 31, DEBUG_CATEGORY_COUNT → 32.

13. Порядок задач

  1. call_proto.h + svc_id + DEBUG_CATEGORY_CALL.
  2. call.c (машина состояний, backpressure-media, watchdog, события, API) + chat_core хуки.
  3. audio_jitter.cpp (SoundTouch) + test_audio_jitter.c.
  4. call_tones.c + call_audio.c (Opus + фрейминг + тон-обвязка).
  5. Desktop: call_audio_engine.cpp + UI + события.
  6. Android: JNI + Kotlin AudioRecord/Track + переключение источника + UI.
  7. Сборка (Makefile.am / CMake обе).
  8. test_call.c (два инстанса), интеграция desktop↔desktop и desktop↔android.