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.
 
 
 
 
 
 

39 KiB

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

Статус: реализовано. Звонок использует роутер, CM подготавливает оптимальный путь.

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 → аудиопоток через роутер → HANGUP. CM инициируется вызывающим при старте, вызываемым при принятии вызова.

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

Файл Язык Сборки Назначение
src/call/call_proto.h C везде wire-формат, subcmd, svc_id ETCP_RT_ID_CALL 0x36
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

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

Диагностика: категория call (DEBUG_CATEGORY_CALL); транспортная доставка — etcp_route.

3. Протокол (сигналинг svc_id 0x36, первый байт payload = subcmd)

Формат приёма: entry->dgram[0]=svc_id, служебный заголовок роутера до ROUTER_SVC_PAYLOAD_OFF=26, затем subcmd и тело команды.

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}
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_END_ACK 0x08 {call_id:8}
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-группа (общая маршрутизация).
  • Сигналинг: 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 кадров/с.
  • Заголовок медиа — 36 байт с subcmd. ts_ms — время отправки для оценки сетевого джиттера; capture_pts_us — время первого входного сэмпла в монотонной шкале отправителя, INT64_MIN при отсутствии метки. capture_epoch различает разрывы захвата; codec_delay_samples передаёт Opus lookahead. Старый формат не поддерживается; медиа принимается только от пира текущего звонка.

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

Caller: IDLE → OUTGOING(INVITE) → RINGING(RINGING) → ACTIVE(ACCEPT) → ENDED
Callee: IDLE → INCOMING(INVITE) → RINGING(sent RINGING) → ACTIVE(accept) → 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);
  • CONN_EVENT_DOWN/TIMEOUT CM не завершают звонок: роутер использует доступные пути;
  • NO_TRAFFIC 20с — нет входящих медиа-кадров → авто HANGUP.

Завершающие кодограммы HANGUP/CANCEL/DECLINE повторяются каждые 300мс до END_ACK, не дольше 20с. Это необходимо при смене эпохи роутера: старые очереди удаляются, даже если отправитель ранее получил успешный результат постановки команды в очередь. Медиа и их локальные очереди освобождаются сразу, ENDED публикуется один раз. В реестре до ACK/таймаута остаётся только завершённая сессия с CM handle и таймером. Получатель подтверждает также повторные команды после удаления сессии, без повторного ENDED. Команда и ACK проверяются по подписи, шифрованию, размеру, call_id, пиру и группе существующей сессии. ACK не подтверждается. Новый звонок имеет другой call_id и не затрагивается поздними командами старого. При одновременном завершении стороны независимо подтверждают команды друг друга. После ACK сессия удаляется на ближайшем tick завершения; при удалённом завершении CM удерживается ещё 500мс для отправки ACK. Остановка ядра отменяет таймер без повторных отправок. Запоздалые кодограммы вызова при идущем разговоре (ACTIVE) игнорируются.

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

CM handle инициирует оптимальное подключение и удерживается до завершения звонка. Медиа всегда идёт через etcp_route_send: роутер сам выбирает лучший путь и fallback. Звонок не хранит ETCP_CONN и не завершается при потере отдельного транспорта. Очистка записи CM отсоединяет handles и отменяет отложенные UP; handle закрывает владелец. Закрытие CM handle не посылает DISCONNECT удалённым владельцам. Механизм NCD не меняется.

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), ёмкостью 15с (750 кадров). При переполнении — дроп старейшего + счётчик c_tx_dropped.
  • Алгоритм (call_tx_flush): пока tx_q не пуст:
    • если send_q_has_room → queue_data_get + etcp_route_send(force=1) (после проверки места);
    • иначе — если 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. Потоки и жизненный цикл

call_session, CM handle, очередь TX и таймеры принадлежат uasync-потоку. Аудиопоток кодирует PCM и постит call_send_media; приём в uasync кладёт Opus в call_jitter. Декодер, SoundTouch и генератор тонов принадлежат аудиопотоку. Кольцо кодированных кадров и снимок статистики защищены мьютексом; статистика не читает SoundTouch из другого потока. CallTimeline хранит фактическое соответствие входных участков SoundTouch выходному PCM. Платформа публикует позицию воспроизведения с локальной монотонной меткой; call_audio_get_clock возвращает исходный PTS слышимого участка. Локальная тишина и тоны удерживают PTS. Невалидная/устаревшая отметка устройства не разрешает продвижение видео. Контракт и ограничения — call_video.txt.

GUI запускает аудиоустройство по CALL_ACCEPTED. При CALL_ENDED начинается финальный тон; устройство останавливается перед call_audio_release. Явное завершение звонка прекращает воспроизведение накопленной речи и включает тон завершения — на обеих платформах. Android останавливает гудки до настройки звонкового аудиотракта. После выбора маршрута capture/play потоки независимо создают и запускают AudioRecord/AudioTrack; каждый сразу обслуживает своё направление. start() подтверждает запуск подготовки, ошибки устройств передаются владельцу на main через onFailure. Ошибка любого направления завершает звонок. Общий stop() отменяет подготовку, разблокирует I/O и ждёт оба потока до освобождения Android-устройств и native-аудиостека. Отдельные locks защищают подготовку/остановку каждого устройства. Замер AEC планируется через 300мс после готовности обоих направлений и не задерживает воспроизведение. При наличии Telecom Connection режимом и выходом звонка управляет только Telecom. ACTIVE запрашивается до создания аудиоустройств; AudioManager/прямой preferred device не дублируют выбор. На API 34+ маршрут запрашивается через доступный CallEndpoint, на старых — через Connection.setAudioRoute. Подтверждённый маршрут приходит из callback системы, отдельно от желания пользователя; ошибка запроса логируется. Если Connection ещё нет, используется AudioManager. Поздняя регистрация передаёт управление Telecom, снимая заявку приложения на режим и прямые устройства. Рация сохраняет отдельный AudioManager-путь. Реальное время запуска/переключения зависит от Android и требует проверки на устройстве. Маршрутизатор использует один снимок входных/выходных устройств; обновляет его при старте, событиях устройств/BT-профиля и явной переоценке. По завершении снимок освобождается. INFO-логи Android показывают длительности настройки режима, регистрации callback, BT-профиля, выбора маршрута, создания/старта устройств, первого чтения микрофона и первой записи в AudioTrack, а также готовность другого направления. Общий call_audio логирует время ожидания первого PCM; запись в AudioTrack может содержать начальную тишину. Эффективный рабочий буфер AudioTrack на всех поддерживаемых API при старте запрашивается размером два блока PCM (40мс); на API 31+ стартовый порог следует фактически принятому размеру. Ёмкость выделенной памяти может быть больше. Ограничение повторяется по callback фактической смены выхода на main, под trackLock; callback старого AudioTrack игнорируется и снимается при stop. Подачу PCM задаёт свободное место в AudioTrack, без дополнительного таймера 20мс. Частичная запись удерживает остаток одного блока до полной передачи; следующий pull ждёт. Сетевой запас остаётся в адаптивном JB, его рост и SoundTouch-догон не изменены. Лог playback buffering показывает причину настройки, запрошенный/принятый размер, порог и ёмкость; ежесекундный audio summary — queueUs, текущий порог и underruns; новый episode underrun имеет уровень WARN. Это очередь AudioTrack, а не полная сквозная задержка и не сетевой jitter reserve. Ранее на Samsung startFrames=1920 был принят, но без ограничения рабочего буфера written-played оставался 120–160мс; одного стартового порога недостаточно. Замер Samsung подтвердил постоянную очередь 40мс. При прежнем параллельном управлении маршрутом AudioTrack пересоздавался, очередь временно достигала 160мс, а ожидание свободного места — 328–336мс. При старте AudioTrack.play занимал 1080мс. После перехода к единственному владельцу Telecom в проверенном звонке старт занял 62мс, очередь оставалась не более 40мс и при смене выхода. Переключение всё ещё останавливало потребление на 293–328мс; этот всплеск принимает сетевой JB.

На desktop AudioDiag: capture clock при call=debug выводит секундную сводку исходных backend-блоков: число блоков/сэмплов, накопленную последовательность, диапазон ошибки относительно конца предыдущего блока, latency PulseAudio, число скачков >2мс, hole-сэмплы и блоки без достоверного измерения. Первый скачок за секунду содержит обе latency/поправки конвертера, expected/actual PTS и монотонное время. Backend публикует только числовой снимок в ограниченное кольцо; форматирование выполняет reporter. Исходный backend-снимок остаётся наблюдением. В capture worker позиция PCM задаёт непрерывные PTS, а backend медленно корректирует их привязку: постоянная времени 2с, предел 0.5% (100мкс на 20мс PCM). Скорректированное время используется и для Opus, и для выбора AEC-референса. Сам PCM не изменяется. Разрыв sequence, смена route и скачок исходных меток >20мс по-прежнему сбрасывают capture/AEC и эпоху; порог 2мс в call_audio не увеличивается. Секундный voice-io: capture clock показывает phase/error/adjustment. Тесты проверяют десять минут дрейфа +/-300ppm, скачки оценки до 14мс, разбиение чанков 25мс на Opus 20мс, сохранность PCM, явную потерю, смену устройства и большой разрыв исходных часов. Счётчик sequence описывает полученные backend-сэмплы; отсутствие потерь очереди само по себе не доказывает непрерывность физического захвата. Проверять вместе с hole/overflow и route-событиями.

Для захвата звонка desktop явно запрашивает период 20мс вместо дефолта PulseAudio 25мс; фактический период сообщает audio: call-capture initialized. Сборщик по-прежнему принимает произвольные чанки. Секундные замеры задержки при call=debug:

  • voice-io: capture latency: возраст первого сэмпла перед native feed (включает сам блок 20мс), подготовка/AEC в worker и вызов native feed; диапазоны min/avg/max в микросекундах.
  • voice-io: playback latency: приблизительный остаток PCM после mix, включая частичный блок, длительность pull и недостающие сэмплы. Это очередь приложения, отдельно от backend.
  • AudioDiag: playback clock: latency backend, задержка конвертера/cache и максимальный размер backend-порции в микросекундах. При measured=0 нулевые границы не означают нулевую задержку.
  • call audio TX timing: ожидание g_mtx, обработка/кодирование под ним, ожидание posted task; post включает ожидание lock при выполнении задачи, его разрешение 100мкс. На Android этот же замер обработки включает программный AEC, на desktop AEC уже выполнен в capture worker.
  • call audio TX queue: число ожидающих кадров, возраст старейшего и avg/max ожидания кадров, переданных роутеру за интервал. Отправка роутеру не является подтверждением доставки пиру.
  • Android audio capture latency: возраст первого сэмпла перед native feed и число измеренных привязок; дополнительные поля старта устройств разделяют min-buffer query, constructor, routing и buffer setup. Возраст PCM содержит часть измеренной обработки: эти числа нельзя механически складывать. PTS/возраст используют локальный MONOTONIC, интервалы внутри native — локальный get_time_us/get_time_tb; эти временные базы и часы разных устройств напрямую не вычитаются. Все сводки формируются вне device callback. Период захвата 20мс и новые измерения требуют контрольного звонка; дополнительная desktop PCM-очередь, жизненный цикл подготовки Android и таймер показа видео этим изменением не менялись.

На Android CallPlaybackBuffer подбирает рабочую очередь отдельно от JB/SoundTouch: после 30с наблюдаемого продвижения playback head без underrun пробует уменьшить её на один блок PCM (960 сэмплов, 20мс), до одного блока или фактического минимума устройства. При росте AudioTrack.underrunCount добавляет один блок в пределах выделенной ёмкости. Интервал следующей пробы удваивается (60/120/240/300с), чтобы не повторять частые неудачные уменьшения. После старта, запроса/подтверждения маршрута и увеличения даются 2с на стабилизацию; после уменьшения первый underrun учитывается немедленно. Отсутствие продвижения head или пропуск наблюдений >1с обнуляет стабильный участок. Сброс системного счётчика учитывается отдельно. Фактическая смена выхода начинает подбор с 40мс заново. Непринятая меньшая заявка фиксирует нижнюю границу до следующего выхода; ошибки API выключают подбор с WARN/ERROR, сохраняя воспроизведение. Reporter управляет размером под trackLock независимо от фильтра debug; playback не берёт этот lock. Нет flush/recreate, частичная запись PCM сохраняется. После смены размера/выхода через секунду повторно измеряется задержка для AEC. Ожидание полного буфера и недостача сети не увеличивают его размер. Лог настройки содержит reason, previous/requested/actualFrames и прирост underruns; достижение ёмкости предупреждает не чаще раза в 10с. Принятие и стабильность 20мс ещё требуют проверки на Samsung; буферы HAL и Bluetooth могут добавлять собственную задержку. На desktop адаптация устройства ещё не реализована: выход принадлежит SoundManager, изменение backend-буфера должно выполняться его владельцем, а не отдельным звонком.

Диагностика Android при call=debug: playback сохраняет один числовой снимок на блок (20мс) в кольце 512 элементов, без форматирования/вывода. Отдельный reporter раз в 200мс забирает записи; раз в секунду выводит audio summary с min/avg/max очередей, задержек pull/write и опоздания потока, целевым запасом/разбросом доставки JB, темпом, недостающим PCM и отдельными счётчиками JB/AudioTrack. Снимок JB не сбрасывает события для CALL_STATS. encodedMs — очередь Opus после pull, processingMs — оставшийся PCM SoundTouch в длительности исходной речи; reserveMs зависит только от доставки. Reporter опрашивает AudioTimestamp, routedDevice, underrunCount и размеры буфера не чаще раза в 200мс. Playback читает только опубликованный неизменяемый снимок. Его begin/end и возраст deviceAgeUs отличают задержку опроса от задержки аудио; stampQueryUs/routeQueryUs/propertiesQueryUs показывают стоимость запросов. Android API может удерживать общие внутренние locks даже при вызове из reporter: перенос убирает прямое ожидание диагностического запроса в playback, но сам по себе не доказывает отсутствие влияния на write/head. frame/time аппаратного timestamp и время начала опроса печатаются отдельно; при недоступности стоит stampOk=0. Времена trace — MONOTONIC; очередь устройства — written-played. Эти показатели нельзя складывать с возрастом AudioTimestamp как независимые задержки. В playback отдельно измеряются writeCallUs (только вызовы write), headCallUs (чтение позиции), progressCallUs (публикация позиции ядру), jbSnapshotUs (нативный снимок), writeSpanUs (весь цикл записи) и blockUs (pull до формирования строки). При частичных записях длительности вызовов суммируются за блок. pullGapExcessUs — превышение интервала между началами соседних pull над 20мс; включает ожидание устройства, а не только задержку планировщика (прежнее поле wakeLateUs удалено). Старое поле writeUs включало чтение позиции и прогресс: его нельзя трактовать как длительность одного AudioTrack.write. Stop дожидается reporter, включая непрерываемый Android-запрос, до освобождения AudioTrack.

audio detail сохраняет 2с до и 3с после старта, фактической смены routedDevice или новой аномалии: очередь устройства >100мс в течение секунды, отсутствие продвижения head 100мс при наличии данных, либо новый episode underrun (затухает через 3с без повторений). Порог очереди — диагностический, не настройка JB и не изменение воспроизведения. События внутри окна объединяются, между выгрузками не менее 10с; продолжающаяся аномалия не создаёт повторных дампов. Восстановление логируется INFO. Подробные строки содержат по четыре снимка и заголовок с порядком колонок; при stop окно может быть неполным. Переполнение кольца при задержке reporter имеет отдельный WARN lost=N. Аудиоданные не сохраняются. Выключение call=debug прекращает сбор/выгрузку; буфер и reporter освобождаются с playback worker. Сравнивать сводки следует по call ID и monoUs; рост сетевого запаса сам по себе не является ошибкой.

7. Буфер звонка и восстановление

call_jitter независим от radio_jitter: рация сохраняет свои правила задержки и сброса. Буфер звонка хранит до 750 кодированных кадров (15с): 10с перебоя и 5с запаса. Целевой запас — clamp(max-min, 60, max_reserve_ms) мс по изменению задержки доставки: на входящем кадре накапливается разность интервалов приёма и отправки, десять секундных интервалов хранят её экстремумы. Отправитель уже передаёт timestamp в CALL_MEDIA; смещение часов пиров исключается, переполнение uint32 учитывается. Подготовка местного аудиоустройства и ускоренное воспроизведение очереди не изменяют оценку доставки. Минимум/максимум глубины очереди для GUI собираются отдельно в аудиопотоке за те же десять секунд. Верхний предел задаётся call_jitter_max_reserve_ms: 500–2000 мс, по умолчанию 1000 мс. Настройка есть в аудиопараметрах desktop и Android и применяется к следующему звонку. Начальное и повторное накопление ждёт этот запас; увеличение запаса не прерывает текущую речь. Ускорение зависит от превышения запаса. При отсутствии входящих кадров обычное воспроизведение может исчерпать запас: это не жёсткий запрет воспроизведения ниже цели. Статистика раз в секунду отображается в desktop-окне и Android-экране звонка (под кнопками): глубина, запас, минимум/максимум, темп, RTT, удаления при переполнении и недостачи PCM. Диагностика буфера — категория call=debug. После восстановления кадры последовательно декодируются; темп плавно растёт до 1.6x без изменения высоты голоса: линейный предел роста 1.0x→1.6x за 500мс воспроизведения. При уменьшении накопления целевой темп снижается к 1.0x; скорость снижения ограничена тем же пределом. Пауза аудиоустройства не сбрасывает буфер. Переполнение удаляет старейший кадр с WARN и счётчиком.

Роутер сохраняет последовательность и неподтверждённые данные при смене пути. Для сервиса звонка срок без прогресса ACK увеличен примерно до 20с; прочие сервисы сохраняют прежний срок. Перезапуск удалённого процесса/роутерной сессии не является кратковременным перебоем пути и может сбросить данные. SoundTouch: USE_QUICKSEEK=1, SEQUENCE_MS=20, SEEKWINDOW_MS=10, OVERLAP_MS=4.

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

  • Исходящий звонок — две фазы с разными гудками (у вызывающего):
    • фаза 1 «пытаемся связаться» (OUTGOING: INVITE отправлен/ретранслируется, маршрут устанавливается) — гудок «соединение» (короткие бипы);
    • фаза 2 «звоним» (RINGING: получен RINGING, у пира идёт вызов) — длинные гудки (ringback). Гудки генерируются платформенно-нативно: desktop — цикличные MP3 через SoundManager (call_connecting/call_ringing), Android — цикличные MP3 через CallRingback/MediaPlayer. Переключение — по CHAT_EVT_CALL_RINGING, остановка — по CALL_ACCEPTED/CALL_ENDED/CALL_ERROR/hangup.
  • Начальное ожидание первого PCM → тишина без сигнала потери звука. Подготовка удалённого микрофона и начальное накопление jitter не считаются перебоем. Существующий таймаут 20с без медиа действует с ACTIVE.
  • Недобор PCM ≥500мс после начала воспроизведения → в call_audio_pull_pcm воспроизводится CALL_TONE_GLITCH: пи-пи (2×150мс, gap 100мс) … тишина до 1.0с … пи(200мс) пауза(200мс) пи(200мс), 1000 Гц, цикл пока длится сталл. До порога — тишина при нехватке PCM. При возобновлении PCM сигнал останавливается; одного входящего пакета без достаточного запаса недостаточно.
  • Завершение звонка → всегда 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      39  [call_id:8][rtt:2][buffer:2][tempo:2][dropped:2][underruns:2][min:2][max:2][reserve: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.