Improve BLE call batching, startup buffering and diagnostics
This commit is contained in:
1 parent
0afccfb624
commit
049c8ef155
44 files changed
+1406
-245
No files matched your search
+7
-1
@@ -1,6 +1,12 @@
|
||||
BLE GATT → ETCP (chatgui desktop + Android)
|
||||
|
||||
Текущее состояние (29.10.2026):
|
||||
Обновление30.10.2026: Android предпочитает PHY1M. Common BLE ограничивает число
|
||||
целых DATA до ACK последнего фрагмента (config.tx_window); очередь tx_limit
|
||||
и защитный предел12500байт учитываются дополнительно. Формат DATA/ACK прежний.
|
||||
Выбор окна и актуальный контракт — doc/ble_transport.txt. Новые аппаратные
|
||||
измерения выполнены отдельным стендом драйвера без ETCP.
|
||||
|
||||
Историческое состояние (29.10.2026):
|
||||
Write Without Response/Notify, HIGH и PHY2M включены. BLE version3 добавляет
|
||||
16-bit ID каждого DATA GATT-value, накопительный ACK по отдельной характеристике
|
||||
не чаще15ms и окно12500байт на направление (фиксированные2Мбит/с ×50ms).
|
||||
|
||||
+34
-10
@@ -61,13 +61,17 @@ RX при заполнении отбрасывает целый новый ка
|
||||
если он присутствует. Это не Ethernet payload MTU 1500 (которому нужно >=1514).
|
||||
На link не больше одной незавершённой GATT-передачи. Она хранит свой fragment
|
||||
до завершения ОС. Очередь ядра/Qt не используется как неограниченный TX-буфер.
|
||||
DATA дополнительно ограничен неподтверждёнными GATT-values: фиксированные2Мбит/с
|
||||
×50ms/8 =12500байт на направление, включая flow/framing headers. Локальный sent
|
||||
не возвращает этот кредит: его освобождает накопительный ACK удалённого драйвера.
|
||||
DATA дополнительно ограничен config.tx_window: 0 означает BLE_TX_WINDOW_FRAMES=3,
|
||||
явное значение1..64. В окно входят все принятые tx_put кадры: очередь, текущая
|
||||
фрагментация и ожидание удалённого ACK последнего фрагмента. Локальный sent
|
||||
и ACK середины кадра не освобождают слот. Это учёт существующих flow ACK,
|
||||
без новой wire-команды и без гарантии принятия кадра приложением.
|
||||
Независимый защитный предел GATT-values —12500байт на направление, включая
|
||||
flow/framing headers; байтовый кредит освобождается накопительным удалённым ACK.
|
||||
При заполнении ble_tx_ready=false; ACK будит writable после появления места.
|
||||
Текущий кадр может остановиться посреди фрагментации, INVITE и ACK продолжают работать.
|
||||
Предел12500 — расчётный размер по фиксированной скорости, не гарантия фактических50ms
|
||||
при медленном радио. Сверх окна остаётся только ограниченная собственная TX-очередь.
|
||||
Предел12500 не задаёт скорость или время доставки. Собственная TX-очередь входит
|
||||
в окно целых кадров; предел config.tx_limit дополнительно ограничивает её размер.
|
||||
Переподключение не переносит старые кадры и не повторяет неоднозначные отправки.
|
||||
|
||||
Wire protocol
|
||||
@@ -152,7 +156,7 @@ Value/Confirm без адреса клиента для серверной пе
|
||||
Выходящий клиент: DATA/ACK WriteValue(type=command), INVITE WriteValue(type=request),
|
||||
StartNotify для TX, INVITE и ACK; открытие после всех трёх подписок.
|
||||
Публичный Device1 D-Bus API не задаёт per-link PHY/connection interval: согласованием
|
||||
управляют ОС и peer; Android запрашивает 2M также в серверной роли.
|
||||
управляют ОС и peer; Android запрашивает 1M также в серверной роли.
|
||||
Не вызывается глобальный Device1.Disconnect: ACL может обслуживать другие сервисы.
|
||||
Проверено с BlueZ 5.85; требуется серверный AcquireNotify с indication completion.
|
||||
|
||||
@@ -400,15 +404,35 @@ Instrumentation временно включает echo, возвращает с
|
||||
Профиль скорости Android
|
||||
------------------------
|
||||
После подключения client просит CONNECTION_PRIORITY_HIGH и ATT MTU 517.
|
||||
Обе роли запрашивают PHY_LE_2M для TX/RX при поддержке локального контроллера.
|
||||
Обе роли запрашивают PHY_LE_1M для TX/RX: аппаратные сравнения A52/Linux показали
|
||||
падение полезной скорости на2M при занятой очереди контроллера.
|
||||
Запрос не гарантирует смену PHY: onPhyUpdate/onPhyRead логируют фактические TX/RX.
|
||||
При отсутствии поддержки остаётся согласованный PHY. S2/S8 — параметры только
|
||||
LE Coded PHY (дальность/скорость), с выбранным 2M применяется NO_PREFERRED.
|
||||
S2/S8 — параметры только LE Coded PHY; для1M применяется NO_PREFERRED.
|
||||
У Android BluetoothGattServer нет public requestConnectionPriority; входящий link
|
||||
использует дополнительный BluetoothGatt handle того же ACL только для HIGH.
|
||||
Запрос/результат логируются, handle закрывается при teardown; нового ACL для настройки нет.
|
||||
Сам UUID/advertising остаётся legacy
|
||||
1M для обнаружения; PHY 2M относится к установленному соединению.
|
||||
1M для обнаружения; предпочтение PHY отдельно задаётся установленному соединению.
|
||||
|
||||
Подбор окна 2026-10-30
|
||||
---------------------
|
||||
Отдельный стенд без ETCP: A52 Android-central ↔ Linux/BlueZ, PHY1M, MTU517,
|
||||
interval15ms, DLE251/time2120us. Каждая проба:5s туда,5s обратно,5s дуплекс,
|
||||
побайтовая проверка1500B кадров. Окно квитанций стенда64, ограничивает драйвер.
|
||||
Скорости полезной нагрузки, кбит/с: окно1 —178/222, дуплекс241; окно2 (два
|
||||
прохода) —374/399..406, дуплекс341..343; окно3 (два прохода) —378..385/413..416,
|
||||
дуплекс390..395; окно4 —385/418, дуплекс418. Max RTT до квитанции соответственно
|
||||
137/155/196/245ms. Окно3 выбрано как минимальное близкое к насыщению канала
|
||||
с RTT<=200ms в этих пробах; окно2 снижает дуплексную скорость примерно на13%.
|
||||
Окно драйвера освобождается flow ACK, поэтому не равно окну квитанций стенда.
|
||||
С активной BLE-мышью DLE time падал до340us, наблюдались RTT>200ms даже при
|
||||
малом окне. Фиксированное окно не гарантирует200ms при произвольной нагрузке
|
||||
радио. Эти короткие замеры не являются долговременным SLA и не включают ETCP.
|
||||
Счётчики frames/window_frames/peak_frames и frames_released показывают
|
||||
ограничение и освобождение кадров; PHY callbacks показывают фактический PHY.
|
||||
Контроль после финальной сборки со штатными defaults:381/423кбит/с, дуплекс388,
|
||||
maxRTT177.018ms, обе стороны PASS. При Linux-central окно3 также прошло:
|
||||
378/420кбит/с, дуплекс381, maxRTT181.328ms. Все три BLE unit/mock-теста PASS.
|
||||
|
||||
Проверка 2026-10-29
|
||||
------------------
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
Адаптивная упаковка Opus в звонке
|
||||
================================
|
||||
|
||||
Сервис CALL_AUDIO использует одну Router-кодограмму для 1..10 Opus-кадров.
|
||||
Кодек и длительность кадра (20мс) не меняются. Новый формат должен быть
|
||||
на обеих сторонах; старый одиночный CALL_MEDIA не поддерживается.
|
||||
|
||||
Wire, packed, порядок байтов хоста как у остального CALL:
|
||||
Общий header, 27 байт:
|
||||
subcmd:u8, call_id:u64, track_id:u8, capture_epoch:u64,
|
||||
codec_delay_samples:u32, sent_ms:u32, count:u8.
|
||||
Далее count раз:
|
||||
seq:u16, capture_pts_us:i64, length:u16, opus[length].
|
||||
Накладные расходы: 27 + 12*N байт CALL на N кадров, плюс один Router.
|
||||
|
||||
count=1..10, весь CALL_MEDIA <=1024 байт. Пачка может быть меньше target из-за
|
||||
размера, срока отправки или смены capture epoch/codec delay. PTS/seq каждого
|
||||
кадра сохраняются; PTS может быть INT64_MIN. Парсер проверяет всю кодограмму
|
||||
до выдачи первого кадра, отвергает хвостовые байты и незавершённые записи.
|
||||
Headless callback по-прежнему получает отдельные Opus-кадры.
|
||||
|
||||
Пример для Opus по 37 байт, прямой Router с SIGN|ENCRYPT (+163 байта):
|
||||
N кадров CALL payload Router wire байт Router на кадр
|
||||
1 76 239 239
|
||||
2 125 288 144
|
||||
3 174 337 112,33
|
||||
5 272 435 87
|
||||
10 517 680 68
|
||||
Обрамление ETCP/BLE и ACK не включены. Normalizer может объединять/разбивать
|
||||
кодограммы. Старый расход: 236 байт Router на каждый такой Opus-кадр;
|
||||
историческое разложение — call_audio_packet_overhead.txt.
|
||||
|
||||
Источник обратной связи
|
||||
-----------------------
|
||||
etcp_router_tx_stats(group_id, peer_node_id, ETCP_RT_ID_CALL_AUDIO).
|
||||
Это конкретный логический аудиоканал, не общая сумма очередей ядра.
|
||||
Интерфейс уже используется видеотреком звонка; stream_publisher использует
|
||||
другой, общий снимок ETCP и для данного регулятора не подходит.
|
||||
|
||||
Router сохраняет исходное монотонное enqueued_us в send_q и переносит его
|
||||
в inflight. Ретрансмит не меняет эту метку. При первом подтверждении:
|
||||
sample = ACK time - enqueued_us;
|
||||
delivery_rtt_us = EWMA sample с весом 1/8 (первый sample без сглаживания).
|
||||
Снимок также отдаёт queued_age_us/inflight_age_us головы от исходного enqueue
|
||||
и last_ack_us. Возраст пустой очереди равен нулю. При reset история очищается.
|
||||
Прежние rtt_us/baseline_rtt_us и алгоритм видеорегулятора не меняются.
|
||||
|
||||
Полный delivery_rtt_us включает исходную очередь Router, нижние очереди,
|
||||
путь до приёмника и возврат ACK. Базовый RTT НЕ вычитается. Это не время
|
||||
доставки в динамик и не измерение односторонней задержки.
|
||||
|
||||
Регулятор
|
||||
---------
|
||||
Снимок каждые50мс в uasync; управление целиком в call_session.
|
||||
D = max(delivery_rtt_us, queued_age_us, inflight_age_us).
|
||||
Собственное намеренное накопление пачки в D не добавляется.
|
||||
|
||||
Начальное target=4, deadline60мс от первого кадра: это время сбора ещё трёх
|
||||
кадров по20мс. При смене local/peer Router epoch или исчезновении канала
|
||||
target/история сбрасываются. Первый пригодный ACK сразу переводит deadline
|
||||
на измеренную задержку, даже до заполнения окна статистики500мс; на быстром
|
||||
канале ожидание сразу выключается. Для изменения target нужно полное окно.
|
||||
До первого ACK сохраняется стартовое ожидание60мс; при no_route, receive_busy
|
||||
и minRTT probe ожидание и рост выключены.
|
||||
|
||||
При D>100мс непрерывно200мс target увеличивается на1 до10. Между изменениями
|
||||
не менее max(500мс, 2*delivery_rtt_us). Высокий обычный RTT без очереди тоже
|
||||
может включить упаковку — это намеренное поведение.
|
||||
При D<40мс и пустом Router send_q в течение2с target уменьшается на1.
|
||||
Промежуточные значения сбрасывают отсчёты high/low, сохраняя target.
|
||||
Если ACK не продвигался дольше max(2с, 2*delivery_rtt_us, 500мс), рост
|
||||
и намеренное ожидание выключаются; это состояние не считается свободной полосой.
|
||||
|
||||
Предельное время накопления:
|
||||
budget = clamp((D - 40мс) / 4, 0, 40мс);
|
||||
hold = min((target - 1)*20мс, budget).
|
||||
На быстром канале hold сразу равен0, даже пока target постепенно уменьшается.
|
||||
При150мс полного RTT допустим hold до27,5мс, при200мс и более — до40мс.
|
||||
|
||||
Отправка и приём
|
||||
---------------
|
||||
Пачка собирается из собственной FIFO до передачи Router. Переданные Router
|
||||
кодограммы не изменяются и не изымаются из inflight. Подпись и оба уровня
|
||||
шифрования сохраняются. При отсутствии места остаётся прежний отменяемый
|
||||
waiter Router; новые кадры/наблюдение/освобождение очереди продолжают drain.
|
||||
|
||||
Отправляем при target кадров, достижении лимита байт, несовместимости эпохи
|
||||
следующего кадра или deadline головы. Новый кадр не продлевает deadline.
|
||||
40мс — предел адаптивного ожидания (до первого ACK60мс), а не предел
|
||||
backpressure или полного RTT.
|
||||
10 кадров — максимальный размер готовой пачки; ожидания180мс ради её наполнения нет.
|
||||
Прежняя защитная FIFO750 и её удаление старейшего при overflow сохраняются;
|
||||
данное изменение не обещает жёсткую задержку200мс при остановившемся канале.
|
||||
|
||||
sent_ms ставится перед подачей пачки Router. Приёмник передаёт его всем кадрам,
|
||||
но обновляет оценку сетевого jitter только по первому. Capture PTS остаётся
|
||||
отдельной временной шкалой каждого кадра; учитываются реальные сетевые паузы,
|
||||
а собственное накопление пачки не создаёт N фиктивных измерений jitter.
|
||||
После полной проверки кадры идут в прежние decoder/jitter/headless callbacks.
|
||||
|
||||
Первое воспроизведение ждёт накопления не менее200мс аудио в jitter buffer
|
||||
(10 кадров по20мс), либо сетевого запаса плюс PCM-блок, если это больше.
|
||||
Захват и отправка начинаются сразу. До первой выдачи PCM нет сигнала перебоя.
|
||||
После старта действуют прежние адаптивный запас и плавное ускорение излишка;
|
||||
200мс не закрепляются как постоянная глубина. Повторное накопление после
|
||||
недостачи и сброс устройства после начала речи используют обычный запас.
|
||||
Это порог начального заполнения, не ограничение полной сквозной задержки.
|
||||
|
||||
cleanup/ENDED/destroy отменяют оба новых таймера и прежний Router waiter до
|
||||
освобождения FIFO. Доступ к ядру только в uasync; audio capture worker API
|
||||
не меняется. Счётчики tx/rx кадров и пачек разделены.
|
||||
|
||||
Диагностика
|
||||
-----------
|
||||
call=info: стартовые target/hold, изменение target с причиной, D, полным RTT,
|
||||
возрастами и hold; порог JB и фактическая глубина при первом воспроизведении.
|
||||
call=debug: flow раз в секунду и TX/RX каждой пачки (count, bytes, seq, age).
|
||||
etcp_route=debug: подтверждение с enqueue->ACK, EWMA и числом попыток.
|
||||
Ошибки формата/аллокации/таймеров/отправки логируются. Нужны обе новые сборки.
|
||||
|
||||
Проверки: test_call_audio_batch (формат и регулятор), test_call (реальная
|
||||
доставка 10 кадров/неполной пачки и teardown), test_etcp_router (ACK-метрики
|
||||
после backpressure), test_call_jitter (один arrival на пачку, поздний пакет,
|
||||
отсутствие декодирования при180мс и старт при200мс),
|
||||
headless/admission и прежние проверки аудио. Аппаратный эффект проверяется
|
||||
отдельным звонком и сравнением логов, не выводится из одних unit-тестов.
|
||||
@@ -0,0 +1,124 @@
|
||||
Аудиозвонок через BLE: размер пакета по слоям
|
||||
==========================================
|
||||
|
||||
ИСТОРИЧЕСКИЙ СРЕЗ ДО ВВЕДЕНИЯ ПАЧЕК. Текущий формат и размеры описаны
|
||||
в call_audio_batch.txt. Таблица ниже сохранена для сравнения с исходным звонком.
|
||||
|
||||
Срез реализации и измерение: 30.10.2026, Linux -> Samsung A52, кодек 16 кбит/с.
|
||||
Один Opus-кадр представляет 20 мс звука. В 2021 аудиопакете, отправленном Router
|
||||
за ACTIVE-интервал 40,571045 с: средний размер Opus 36,939 байта, медиана 37,
|
||||
минимум 21, максимум 57, p95 44 байта. При постоянных 16 кбит/с за 20 мс было
|
||||
бы 40 байт; фактический размер закодированного кадра меняется.
|
||||
|
||||
Пример ниже: Opus 37 байт, прямой маршрут, одна аудиокодограмма в одном
|
||||
ETCP DATA-пакете, один BLE-фрагмент. Базовая ETCP ACK-секция содержит count=0;
|
||||
дополнительных timestamp/measurement/filler/ACK_REQUEST секций нет.
|
||||
Это расчёт конкретной структуры пакета, не средний расход всех кадров звонка.
|
||||
|
||||
Слой / поле Добавлено, байт Всего, байт
|
||||
-----------------------------------------------------------------------------
|
||||
Opus 37 37
|
||||
CALL_MEDIA header 36 73
|
||||
Router header 57 130
|
||||
Router E2E: nonce 13 + CRC32 4 + AES-CCM tag 16 33 163
|
||||
Router: Ed25519 signature 64 227
|
||||
Router path: node_id 8 + count 1 9 236
|
||||
Normalizer: длина кодограммы 2 238
|
||||
ETCP: timestamp 2 + flags 1 3 241
|
||||
ETCP session: type 1 + sender epoch 8 + peer epoch 8 17 258
|
||||
ETCP: базовая ACK-секция 8 266
|
||||
ETCP DATA: type 1 + seq 4 + attempt 2 7 273
|
||||
ETCP link encryption: nonce 13 + CRC32 4 + tag 16 33 306
|
||||
BLE flow: номер фрагмента 2 308
|
||||
BLE frame fragment: type 1 + seq 2 + total 2 + off 2 7 315
|
||||
ATT: opcode 1 + characteristic handle 2 3 318
|
||||
L2CAP: length 2 + channel ID 2 4 322
|
||||
|
||||
Границы счётчиков:
|
||||
- 236 байт: Router wire-кодограмма.
|
||||
- 238 байт: данные normalizer, до обрамления ETCP.
|
||||
- 306 байт: зашифрованный ETCP-пакет; целый кадр BLE wire TX/RX в журнале.
|
||||
- 315 байт: значение характеристики GATT; BLE DATA TX/RX в журнале.
|
||||
- 322 байта: L2CAP PDU, до фрагментации и обрамления Link Layer.
|
||||
|
||||
IP/UDP в этом BLE-пути нет. Адаптер ble_etcp передаёт готовый ETCP-пакет
|
||||
в ble_tx_put без дополнительного собственного заголовка.
|
||||
Заголовки Link Layer, MIC при включённом шифровании BLE, служебные пакеты,
|
||||
межпакетные интервалы и радиоповторы в таблицу не включены. Один GATT value
|
||||
может занимать несколько Link Layer пакетов. HCI не является счётчиком эфира.
|
||||
|
||||
Состав CALL_MEDIA header, 36 байт
|
||||
--------------------------------
|
||||
subcmd 1 + call_id 8 + track_id 1 + seq 2 + ts_ms 4 + capture_pts_us 8
|
||||
+ capture_epoch 8 + codec_delay_samples 4 = 36.
|
||||
|
||||
Состав Router header, 57 байт
|
||||
----------------------------
|
||||
cmd 1 + group_id 8 + dst_node_id 8 + src_node_id 8 + seq 4 + svc_id 1
|
||||
+ flags 1 + timestamp 2 + reset_id 8 + peer_reset_id 8 + challenge 8 = 57.
|
||||
|
||||
Router с защитой и прямым путём добавляет 57 + 33 + 64 + 9 = 163 байта.
|
||||
CALL и Router вместе добавляют 36 + 163 = 199 байт на каждый Opus-кадр
|
||||
в текущем формате. Шифрование Router и шифрование ETCP — разные слои:
|
||||
сквозная защита сервисного содержимого и защита соединения соответственно.
|
||||
|
||||
Переменные расходы и подтверждения
|
||||
----------------------------------
|
||||
1. ETCP ACK: базовые 8 байт уже включены в таблицу. При N>0 дополнительно
|
||||
10*N байт записей (seq 4 + attempt 2 + receiver_delay_us 4) и 16 байт точки
|
||||
приёма (накопленные DATA bytes 8 + receive time 8). Итого ACK 24 + 10*N;
|
||||
при N=0 — 8 байт. ACK может идти вместе с DATA или отдельным ETCP-пакетом.
|
||||
|
||||
2. Дополнительный ETCP timestamp: 5 байт. ACK_REQUEST: 1 байт.
|
||||
При измерительной серии возможны MEAS_TS 9, MEAS_RESP 13 и FILLER 3 + N
|
||||
байт. Это условные секции, их нельзя приписывать каждому DATA-пакету.
|
||||
Для примера таблицы: одна ACK-запись и дополнительный timestamp добавляют
|
||||
10 + 16 + 5 = 31 байт; кадр BLE становится 337, GATT value — 346 байт.
|
||||
|
||||
3. Router ACK обратного аудиопотока: 57 + 9 = 66 байт на прямом пути до
|
||||
normalizer/ETCP/BLE. Это самостоятельная Router-кодограмма; подтверждения
|
||||
накопительные, поэтому нет обязательного соотношения один ACK на кадр.
|
||||
|
||||
4. BLE ACK: отдельное двухбайтовое GATT value, дополнительно ATT 3 и L2CAP 4.
|
||||
Обрамление BLE DATA 9 байт к нему не добавляется. ACK накопительный;
|
||||
возможны повторы. Его расход нельзя добавлять ровно один раз на аудиокадр.
|
||||
|
||||
5. Каждый дополнительный Router-hop добавляет 8 байт к списку пути.
|
||||
После разбиения кадра BLE каждый DATA-фрагмент получает свои 9 байт
|
||||
BLE-обрамления и собственные ATT/L2CAP-заголовки.
|
||||
|
||||
6. Normalizer может объединять кодограммы и разбивать поток на фрагменты:
|
||||
2 байта длины приходятся на каждую Router-кодограмму; обрамление ETCP —
|
||||
на каждый передаваемый ETCP-пакет. CALL/Router-заголовки при этом сохраняются.
|
||||
Поэтому средние размеры всего трафика нельзя получать простым умножением
|
||||
одного примера на число Opus-кадров. Повторы также считаются отдельно.
|
||||
|
||||
Проверка по последнему звонку
|
||||
----------------------------
|
||||
Исходящие Linux audio DATA во время ACTIVE:
|
||||
Opus 74654 байта = 14,721 кбит/с
|
||||
CALL header 72756 байт = 14,346 кбит/с
|
||||
Router/crypto/path 329423 байта = 64,957 кбит/с
|
||||
Всего audio Router DATA 476833 байта = 94,024 кбит/с
|
||||
Router ACK обратного аудио 109428 байт = 21,578 кбит/с
|
||||
Все скорости десятичные. Это разные статьи трафика, ACK не входит в DATA.
|
||||
|
||||
Полный поток кадров BLE с ETCP и другим трафиком: Linux -> A52 182,179 кбит/с,
|
||||
A52 -> Linux 170,851 кбит/с. Это не скорость одного аудиопотока и не физическая
|
||||
скорость PHY. Локальный исходный материал (не включён в репозиторий):
|
||||
tools/ble-bench/call-diagnosis/gates-20261030/{linux.log,throughput.json}.
|
||||
|
||||
Основания в коде
|
||||
----------------
|
||||
src/call/call_proto.h: call_pkt_media, CALL_MEDIA_HDR_SIZE, CALL_OPUS_FRAME_MS.
|
||||
src/call/call.c: call_send_media, call_tx_flush.
|
||||
src/routing_layer/etcp_router.h: SVC_ROUTE_HDR.
|
||||
src/routing_layer/etcp_router.c: router_path_append, router_source_send.
|
||||
src/routing_layer/route_crypto.c: route_crypto_encode.
|
||||
src/transport_layer/pkt_normalizer.c: etcp_input_ready_cb (длина кодограммы).
|
||||
src/transport_layer/etcp_connections.h: ETCP_*_SIZE, SC_ENCRYPT_OVERHEAD.
|
||||
src/transport_layer/etcp_connections.c: etcp_encode_plain, etcp_datagram_send.
|
||||
src/transport_layer/etcp.c: сборка секций в etcp_request_pkt.
|
||||
src/transport_layer/secure_channel.h: SC_NONCE_SIZE, SC_CRC32_SIZE, SC_TAG_SIZE, SC_SIGN_SIZE.
|
||||
tools/cgathui_common/ble_etcp.c: send_packet.
|
||||
lib/ble/ble_transport.c: pump, HEADER, FLOW_HEADER.
|
||||
+11
-5
@@ -170,7 +170,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_MEDIA 0x10 {call_id:8, track_id:1, capture_epoch:8, codec_delay_samples:4, sent_ms:4, count:1, frames[count]}
|
||||
frame {seq:2, capture_pts_us:8, length:2, opus[length]}
|
||||
```
|
||||
|
||||
- `call_id` — случайный uint64, генерит вызывающий; идентифицирует сессию на обеих сторонах.
|
||||
@@ -186,11 +187,14 @@ CALL_MEDIA 0x10 {call_id:8, track_id:1, seq:2, ts_ms:4, capture_pts_us:8, capt
|
||||
Надбавка к сервисным данным: nonce(13) + CRC(4) + CCM tag(16) + Ed25519(64) = 97 байт.
|
||||
- `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 сэмплов), целевой bitrate32кбит/с, ~50 кадров/с.
|
||||
- Заголовок медиа — 36 байт с subcmd. `ts_ms` — время отправки для оценки сетевого джиттера;
|
||||
- Аудио: Opus 48kHz mono, 20ms (960 сэмплов), bitrate16/32/64кбит/с по настройке, ~50 кадров/с.
|
||||
- Общий заголовок пачки — 27 байт с subcmd, на кадр — ещё12 байт. count=1..10, вся пачка <=1024 байт.
|
||||
`sent_ms` — время подачи пачки в Router; сетевой jitter измеряется один раз на пачку;
|
||||
`capture_pts_us` — время первого входного сэмпла в монотонной шкале отправителя, `INT64_MIN` при отсутствии метки.
|
||||
`capture_epoch` различает разрывы захвата; `codec_delay_samples` передаёт Opus lookahead.
|
||||
Старый формат не поддерживается; медиа принимается только от пира текущего звонка.
|
||||
Размер пачки регулируется по полному времени Router enqueue→ACK и возрасту очередей;
|
||||
быстрый канал не ждёт накопления. Алгоритм и ограничения: [call_audio_batch.txt](call_audio_batch.txt).
|
||||
|
||||
## 4. Машина состояний
|
||||
|
||||
@@ -447,8 +451,10 @@ Stop дожидается reporter, включая непрерываемый An
|
||||
Минимум/максимум глубины очереди для GUI собираются отдельно в аудиопотоке за те же десять секунд.
|
||||
Верхний предел задаётся `call_jitter_max_reserve_ms`: 500–2000 мс, по умолчанию 1000 мс.
|
||||
Настройка есть в аудиопараметрах desktop и Android и применяется к следующему звонку.
|
||||
Начальное и повторное накопление ждёт этот запас плюс размер запрошенного PCM-блока;
|
||||
после первой полной выдачи остаётся целевой запас. Увеличение запаса не прерывает текущую речь.
|
||||
Первое накопление ждёт минимум200мс аудио либо этот запас плюс размер запрошенного PCM-блока, если это больше.
|
||||
Захват и отправка идут сразу; до первой выдачи PCM сигнал перебоя не включается.
|
||||
После старта излишек плавно сокращается обычным регулятором. Повторное накопление ждёт
|
||||
сетевой запас плюс PCM-блок. Увеличение запаса не прерывает текущую речь.
|
||||
Ускорение зависит от превышения запаса. При отсутствии входящих кадров обычное воспроизведение
|
||||
может исчерпать запас: это не жёсткий запрет воспроизведения ниже цели.
|
||||
Статистика раз в секунду отображается в desktop-окне и Android-экране звонка (под кнопками): глубина, запас, минимум/максимум,
|
||||
|
||||
@@ -4,6 +4,80 @@
|
||||
|
||||
## Текущие задачи
|
||||
|
||||
[+] **Звонок: адаптивные пачки Opus 1..10 по полному времени Router enqueue→ACK** — 30.10.2026.
|
||||
`call_audio_batch` объединяет кадры в одну подписанную/зашифрованную Router-кодограмму; общий CALL header27,
|
||||
описание кадра12 байт. seq/PTS сохраняются, capture epoch/codec delay общие; count1..10, пакет до1024 байт.
|
||||
Router хранит исходный enqueue timestamp через send_q/inflight/ретрансмит и отдаёт полный RTT EWMA,
|
||||
возраст головы очередей и время последнего ACK через существующий etcp_router_tx_stats.
|
||||
Полный RTT не вычитается: высокий обычный RTT тоже включает упаковку. Быстрый канал сразу прекращает
|
||||
намеренное накопление. Рост при >100мс в течение200мс; уменьшение при <40мс без send_q в течение2с.
|
||||
Старт с target4/deadline60мс до первого пригодного ACK; далее deadline зависит от задержки, максимум40мс.
|
||||
Десять кадров не требуют ожидания180мс. Первый вывод PCM ждёт минимум200мс в JB; захват/отправка идут сразу.
|
||||
Сетевой jitter измеряется один раз на пачку. cleanup отменяет оба таймера; счётчики кадров/пачек разделены.
|
||||
Формат требует обновления обоих участников. Ядро/desktop/Android APK собраны; unit, Router, звонок,
|
||||
headless/admission и23 voice-проверки PASS. ./check.sh:231/231, proxy/burst/load PASS.
|
||||
В первом BLE-звонке глитчи были в начале; в логе совпадают с ростом Router RTT/возраста inflight.
|
||||
Версия со стартом4/JB200 установлена на A52 и Linux GUI, BLE восстановлен; эффект старта требует нового звонка.
|
||||
Алгоритм/формат: [call_audio_batch.txt](call_audio_batch.txt); прежние размеры: [call_audio_packet_overhead.txt](call_audio_packet_overhead.txt).
|
||||
|
||||
[ ] **BLE-звонок 16 кбит/с: задержки Router/ETCP, RTO/pacing и окно по байтам** — 30.10.2026.
|
||||
В звонке cc621497ae36edfa (40,571 с) прослежены все 2024 исходящих аудиопакета Linux:
|
||||
Router enqueue → первая успешная передача драйверу BLE, медиана 387,245 мс, p95 676,071 мс, max 895,534 мс.
|
||||
Средние по стадиям: Router 124,332 мс, normalizer 12,414 мс, ETCP input 28,444 мс, send_q 240,316 мс.
|
||||
Это задержка до радио, не до воспроизведения. Подтверждено минимум 8,863 с ожидания pacing при готовом BLE;
|
||||
16,703 с ожидания BLE, включая 3,873 с одновременным pacing; 15,005 с оставлены неклассифицированными.
|
||||
169 RTO: в 135 случаях ACK пришёл до повтора, в 34 после повтора. BBR получает loss уже при RTO.
|
||||
Исходящих BGP/Merkle и повторов аудио Router в ACTIVE нет; присутствуют DM/media и ACK.
|
||||
Добавлена диагностика границ очередей и причин ожидания; алгоритмы RTO/BBR и окно в этом прогоне не менялись.
|
||||
Следующий шаг: отдельная оценка полного времени ACK для RTO без изменения смысла RTT для BBR, затем A/B;
|
||||
после этого подбор окна по байтам на пакетах звонка. Проверить обе стороны и влияние подробных логов.
|
||||
Сборка ядра/GUI, ETCP windows, normalizer, Router, BLE ETCP и ETCP packet link PASS.
|
||||
Локальный отчёт: `tools/ble-bench/call-diagnosis/gates-20261030/RESULTS.txt` (стенд и логи исключены из Git).
|
||||
|
||||
[+] **BLE: проверить пробуждение отправителя при освобождении окна** — 30.10.2026.
|
||||
Цепочка `wake_writer → ble_etcp.writable → packet_link.state_changed → etcp_conn_wake_sender` проверена;
|
||||
потерянных пробуждений в проверенных сценариях не обнаружено, рабочий код транспорта не менялся.
|
||||
Добавлены регрессии MTU 23/517: ACK до/после completion, повтор ACK, повторное заполнение из writable,
|
||||
tx_limit=1/window=3, чередование 7/1500 байт. Следующий кадр подаётся только callback, без polling/отказа put.
|
||||
В test_ble_etcp убрана маскирующая пробная отправка при busy; ожидание совместной доставки приглашения и DATA
|
||||
исправлено с 1000 poll(0) на ожидание результата с deadline: лог подтвердил завершение после ACK через ~15 мс.
|
||||
BLE transport, BlueZ private-bus, BLE ETCP, packet_link и ETCP packet adapter PASS; проверены счётчики памяти.
|
||||
В реальном Linux-логе звонка 16 кбит/с: 3985 интервалов от восстановленной готовности до следующего принятого
|
||||
кадра, медиана 1,201 мс, p95 17,316 мс, max 47,731 мс. Это включает ETCP/BBR, не чистое время callback.
|
||||
Задержки >200 мс на этом участке не найдены; результат не исключает другие причины глитчей или редкие сбои.
|
||||
Локальные материалы: `tools/ble-bench/call-diagnosis/call4-resume-audit.json`, `REPORT.txt`, `resume-*.log`.
|
||||
|
||||
[ ] **Desktop: завершение GUI после остановки ядра по SIGTERM** — 30.10.2026.
|
||||
Два наблюдения на Linux: PID 744782 после BLE-звонка и PID 747101 при обновлении desktop ChatGUI.
|
||||
Повтор 15:00:56–15:01:18: активного звонка не было; при teardown лог `call: destroyed (started=0 ended=0)`.
|
||||
Значит, завершённый звонок не является обязательным условием; зависимость от BLE пока не установлена.
|
||||
Воспроизведение наблюдавшегося сценария: запустить обычный `tools/chatgui/build/vibechat`, дождаться
|
||||
соединений с A52 по BLE и colo2 по сети, открыть монитор соединений, отправить SIGTERM только этому PID.
|
||||
Ожидается сохранение UI-state, однократная остановка служб/worker и выход процесса без SIGKILL.
|
||||
Фактически процесс остаётся жив более 22 секунд; в 15:01:07 `ps` показывал 36 потоков, состояние `Ssl`.
|
||||
Подтверждённая последовательность второго случая:
|
||||
— 15:00:56.192: BLE закрыт с `reason=transport stopped`; `utun_node: poll stopped ... running=0 requested=1`.
|
||||
— 15:00:56.196–.199: освобождаются call_audio, radio, chat и транспорт; есть `etcp_router destroyed`.
|
||||
— с 15:00:56.389 каждые 200 мс повторяется `desktop shutdown queue unavailable; stopping directly`.
|
||||
— каждый цикл также пытается сохранить `window_geometry`, `window_maximized`, `window_splitter`:
|
||||
`GUI setting rejected: ... core unavailable`, `UI setting was not queued`, `GUI command rejected`.
|
||||
GUI-таймер продолжает исполняться; мост к ядру уже отключён. Полный возврат из destroy и завершение
|
||||
worker не подтверждены: нельзя считать доказанным, что ядро полностью освободилось, а зависло только окно.
|
||||
Проверить `MainWindow::onShutdownCheck/requestShutdown` (`tools/chatgui/src/mainwindow.cpp`):
|
||||
сигнал вызывает повторный shutdown через таймер 200 мс, проверка выхода зависит от `m_node->isRunning()`.
|
||||
Проследить `UtunNode::runLoop`, `requestStop`, `isRunning`, доставку `stopped` и join worker
|
||||
(`tools/chatgui/transport/utun_node.cpp`), включая ожидание GUI readers и остаток `utun_instance_destroy`.
|
||||
Это направления проверки, точная точка блокировки пока неизвестна; backtrace не получен
|
||||
(в первом случае attach gdb был недоступен). Добавить контрольные логи до/после teardown и join,
|
||||
с generation, идентификатором потока и состояниями stop/running/GUI-detached; проверить фильтры sys/general.
|
||||
В обоих случаях после ожидания пришлось применить SIGKILL к своему зависшему процессу и запустить GUI снова;
|
||||
после второго случая новый PID 749434, BLE восстановлен. Это обход, исправления ещё нет.
|
||||
Лог повтора сохранён локально: `tools/ble-bench/call-diagnosis/gui-shutdown-20261030-150056.log`
|
||||
(300000 байт, исходный хвост `/tmp/utun-monitor-shutdown-tail.log`; каталог стенда исключён из Git).
|
||||
Проверка исправления: SIGTERM и «Выход» завершают процесс с открытым/закрытым монитором, после звонка
|
||||
и без звонка, с BLE и без него; повторный stop не сохраняет UI-state заново и не запускает второй teardown.
|
||||
Логи должны подтверждать завершение worker и GUI; без зависания, потери ожидающих записей UI и SIGKILL.
|
||||
|
||||
[+] **BLE/ETCP: ограничить очередь удалёнными ACK GATT-фрагментов** — 29.10.2026.
|
||||
BLE version3: циклический ID16bit каждого DATA GATT-value, отдельная ACK Write/Indicate характеристика.
|
||||
Окно12500байт на направление = фиксированные2Мбит/с ×50ms; локальный completion не возвращает кредит.
|
||||
|
||||
+31
-14
@@ -15,7 +15,7 @@
|
||||
#define STATS_US 5000000ULL
|
||||
struct ble_stats {
|
||||
uint64_t since, tx_bytes, rx_bytes, wait_us, max_wait_us;
|
||||
unsigned tx_frames, rx_frames, fragments, dropped, missing, ack_tx, ack_rx, max_flight;
|
||||
unsigned tx_frames, rx_frames, fragments, dropped, missing, ack_tx, ack_rx, max_flight, max_frames;
|
||||
};
|
||||
struct ble_channel_state {
|
||||
struct ll_queue *tx, *rx;
|
||||
@@ -34,8 +34,10 @@ struct ble_connection {
|
||||
struct ble_channel_state channels[BLE_CHANNEL_COUNT];
|
||||
unsigned int capacity;
|
||||
uint16_t pending_bytes;
|
||||
/* ID монотонен по модулю 65536; кольцо хранит только размеры, payload остаётся у ETCP. */
|
||||
uint16_t tx_next, tx_acked, rx_last, ack_sent, flight_head, flight_count, flight_bytes, flight_sizes[FLOW_SLOTS];
|
||||
/* ID монотонен по модулю 65536; кольцо хранит размеры и границы кадров, собственных ретрансляций нет. */
|
||||
uint16_t tx_next, tx_acked, rx_last, ack_sent, flight_head, flight_count, flight_bytes;
|
||||
struct { uint16_t size; uint8_t frame_end; } flight[FLOW_SLOTS]; /* Последний фрагмент освобождает слот кадра. */
|
||||
unsigned tx_frames_pending; /* Принятые DATA, включая очередь и текущий кадр, до удалённого ACK конца. */
|
||||
uint64_t flight_deadline, ack_due, ack_completed;
|
||||
void* ack_timer;
|
||||
int rx_flow_seen, ack_dirty, pending_ack;
|
||||
@@ -88,10 +90,11 @@ static void log_stats(struct ble_connection* c, uint64_t now) {
|
||||
queue_entry_count(c->channels[BLE_CHANNEL_INVITE].tx) + !!c->channels[BLE_CHANNEL_INVITE].current,
|
||||
(unsigned long long)(c->pending ? (now - c->submitted) / 1000 : 0));
|
||||
DEBUG_DEBUG(CAT, "BLE flow link=%llu flight=%u/%u bytes=%u/%u peak_bytes=%u tx_next=%u acked=%u "
|
||||
"rx_last=%u ACK_tx=%u ACK_rx=%u ack_dirty=%d ack_pending=%d",
|
||||
"rx_last=%u ACK_tx=%u ACK_rx=%u ack_dirty=%d ack_pending=%d frames=%u/%u peak_frames=%u",
|
||||
(unsigned long long)c->id, c->flight_count, (unsigned)FLOW_SLOTS, c->flight_bytes,
|
||||
BLE_TX_WINDOW_BYTES, s->max_flight, c->tx_next, c->tx_acked, c->rx_last,
|
||||
s->ack_tx, s->ack_rx, c->ack_dirty, c->pending_ack && c->pending);
|
||||
s->ack_tx, s->ack_rx, c->ack_dirty, c->pending_ack && c->pending,
|
||||
c->tx_frames_pending, c->owner->config.tx_window, s->max_frames);
|
||||
}
|
||||
memset(s, 0, sizeof(*s)); s->since = now;
|
||||
}
|
||||
@@ -228,7 +231,10 @@ static void pump(struct ble_transport* t, struct ble_connection* c) {
|
||||
c->ack_sent = c->rx_last;
|
||||
} else if (wire == BLE_WIRE_DATA) {
|
||||
if (!c->flight_count) c->flight_deadline = c->submitted + DEADLINE_US;
|
||||
c->flight_sizes[(c->flight_head + c->flight_count++) % FLOW_SLOTS] = len;
|
||||
unsigned slot = (c->flight_head + c->flight_count++) % FLOW_SLOTS;
|
||||
struct ble_channel_state* ch = &c->channels[BLE_CHANNEL_DATA];
|
||||
c->flight[slot].size = len;
|
||||
c->flight[slot].frame_end = c->pending_bytes && ch->offset + c->pending_bytes == ch->current->len;
|
||||
c->flight_bytes += len;
|
||||
if (c->flight_bytes > c->stats.max_flight) c->stats.max_flight = c->flight_bytes;
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "BLE DATA TX link=%llu seq=%u type=%u bytes=%zu flight=%u/%u",
|
||||
@@ -271,7 +277,7 @@ static void tick(void* arg) {
|
||||
if (!t->timer) { DEBUG_ERROR(CAT, "BLE timer allocation failed"); ble_backend_status(t, 0, "timer allocation failed"); }
|
||||
}
|
||||
struct ble_transport* ble_create(struct UASYNC* ua, const struct ble_config* config) {
|
||||
if (!ua || !config || !config->node_id || config->tx_limit > BLE_QUEUE_MAX ||
|
||||
if (!ua || !config || !config->node_id || config->tx_limit > BLE_QUEUE_MAX || config->tx_window > BLE_QUEUE_MAX ||
|
||||
(config->adapter && strlen(config->adapter) >= 64)) {
|
||||
DEBUG_ERROR(CAT, "BLE create invalid configuration"); return NULL;
|
||||
}
|
||||
@@ -279,6 +285,7 @@ struct ble_transport* ble_create(struct UASYNC* ua, const struct ble_config* con
|
||||
if (!t) { DEBUG_ERROR(CAT, "BLE create allocation failed"); return NULL; }
|
||||
t->ua = ua; t->config = *config;
|
||||
if (!t->config.tx_limit) t->config.tx_limit = BLE_QUEUE_MAX;
|
||||
if (!t->config.tx_window) t->config.tx_window = BLE_TX_WINDOW_FRAMES;
|
||||
if (config->adapter) { strcpy(t->adapter, config->adapter); t->config.adapter = t->adapter; }
|
||||
return t;
|
||||
}
|
||||
@@ -331,7 +338,8 @@ int ble_channel_tx_ready(struct ble_transport* t, ble_link_id id, enum ble_chann
|
||||
struct ble_connection* c = lookup(t, id);
|
||||
struct ble_channel_state* ch = c ? &c->channels[channel] : NULL;
|
||||
return c && c->ready && (channel != BLE_CHANNEL_DATA ||
|
||||
(BLE_TX_WINDOW_BYTES - c->flight_bytes >= FLOW_MIN_VALUE && c->flight_count < FLOW_SLOTS)) &&
|
||||
(c->tx_frames_pending < t->config.tx_window &&
|
||||
BLE_TX_WINDOW_BYTES - c->flight_bytes >= FLOW_MIN_VALUE && c->flight_count < FLOW_SLOTS)) &&
|
||||
(unsigned)queue_entry_count(ch->tx) + !!ch->current < (channel == BLE_CHANNEL_DATA ? t->config.tx_limit : 1);
|
||||
}
|
||||
int ble_channel_tx_put(struct ble_transport* t, ble_link_id id, enum ble_channel channel, struct ll_entry* e) {
|
||||
@@ -349,6 +357,12 @@ int ble_channel_tx_put(struct ble_transport* t, ble_link_id id, enum ble_channel
|
||||
}
|
||||
/* Без лимита/индекса/consumer put не имеет отказа после проверок и не вызывает чужой callback. */
|
||||
if (queue_data_put(ch->tx, e) != 0) { DEBUG_ERROR(CAT, "BLE TX queue invariant failed"); return BLE_ERROR; }
|
||||
if (channel == BLE_CHANNEL_DATA) {
|
||||
c->tx_frames_pending++;
|
||||
if (c->tx_frames_pending > c->stats.max_frames) c->stats.max_frames = c->tx_frames_pending;
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "BLE frame queued link=%llu frames=%u/%u bytes=%u",
|
||||
(unsigned long long)id, c->tx_frames_pending, t->config.tx_window, e->len);
|
||||
}
|
||||
if (!ble_channel_tx_ready(t, id, channel)) ch->blocked = 1;
|
||||
pump(t, c); return BLE_OK;
|
||||
}
|
||||
@@ -389,9 +403,9 @@ void ble_backend_opened(struct ble_transport* t, ble_link_id id, unsigned int mt
|
||||
c->opened = 1;
|
||||
DEBUG_INFO(CAT, "BLE GATT open link=%llu mtu=%u fragment_payload=%u outgoing=%d DATA=without_response/notify INVITE=request/indicate tx_limit=%u",
|
||||
(unsigned long long)id, mtu, c->capacity - HEADER - FLOW_HEADER, c->outgoing, t->config.tx_limit);
|
||||
DEBUG_INFO(CAT, "BLE flow configured link=%llu rate_bits=%u window_ms=%u window_bytes=%u "
|
||||
DEBUG_INFO(CAT, "BLE flow configured link=%llu window_frames=%u window_bytes=%u "
|
||||
"ACK=without_response/notify ACK_interval_us=%u ACK_repeat_us=%u id_bits=16",
|
||||
(unsigned long long)id, BLE_TX_RATE_BITS, BLE_TX_WINDOW_MS, BLE_TX_WINDOW_BYTES, BLE_ACK_INTERVAL_US, BLE_ACK_REPEAT_US);
|
||||
(unsigned long long)id, t->config.tx_window, BLE_TX_WINDOW_BYTES, BLE_ACK_INTERVAL_US, BLE_ACK_REPEAT_US);
|
||||
if (c->outgoing) {
|
||||
c->control[0] = HELLO; write64(c->control + 1, t->config.node_id); write64(c->control + 9, c->peer);
|
||||
c->control_pending = 1;
|
||||
@@ -424,14 +438,17 @@ void ble_backend_received(struct ble_transport* t, ble_link_id id, enum ble_wire
|
||||
(unsigned long long)id, ack, c->tx_acked, c->tx_next, c->flight_count);
|
||||
close_link(t, c, "ACK outside flight"); return;
|
||||
}
|
||||
unsigned bytes = 0;
|
||||
unsigned bytes = 0, frames = 0;
|
||||
for (unsigned i = 0; i < advance; i++) {
|
||||
bytes += c->flight_sizes[c->flight_head]; c->flight_head = (c->flight_head + 1) % FLOW_SLOTS;
|
||||
bytes += c->flight[c->flight_head].size; frames += c->flight[c->flight_head].frame_end;
|
||||
c->flight_head = (c->flight_head + 1) % FLOW_SLOTS;
|
||||
}
|
||||
c->tx_frames_pending -= frames;
|
||||
c->flight_count -= advance; c->flight_bytes -= bytes; c->tx_acked = ack; c->stats.ack_rx++;
|
||||
c->flight_deadline = c->flight_count ? get_time_us() + DEADLINE_US : 0;
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "BLE ACK RX link=%llu id=%u released=%u/%u flight=%u/%u", (unsigned long long)id,
|
||||
ack, advance, bytes, c->flight_count, c->flight_bytes);
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "BLE ACK RX link=%llu id=%u released=%u/%u flight=%u/%u frames_released=%u frames=%u/%u",
|
||||
(unsigned long long)id, ack, advance, bytes, c->flight_count, c->flight_bytes,
|
||||
frames, c->tx_frames_pending, t->config.tx_window);
|
||||
wake_writer(t, c, BLE_CHANNEL_DATA); pump(t, c); return;
|
||||
}
|
||||
enum ble_channel channel = wire == BLE_WIRE_DATA ? BLE_CHANNEL_DATA : BLE_CHANNEL_INVITE;
|
||||
|
||||
@@ -20,10 +20,8 @@ extern "C" {
|
||||
#define BLE_ENDPOINT_MAX 256
|
||||
#define BLE_LINKS_MAX 16
|
||||
#define BLE_QUEUE_MAX 64
|
||||
/* Расчётная скорость фиксирована; окно включает DATA GATT-values с заголовками. */
|
||||
#define BLE_TX_RATE_BITS 2000000u
|
||||
#define BLE_TX_WINDOW_MS 50u
|
||||
#define BLE_TX_WINDOW_BYTES (BLE_TX_RATE_BITS / 8u * BLE_TX_WINDOW_MS / 1000u)
|
||||
#define BLE_TX_WINDOW_FRAMES 3u /* Целые DATA от tx_put до ACK последнего фрагмента; баланс скорости/RTT на A52/BlueZ. */
|
||||
#define BLE_TX_WINDOW_BYTES 12500u /* Дополнительный предел GATT-байтов; не оценка скорости/задержки. */
|
||||
#define BLE_ACK_INTERVAL_US 15000u
|
||||
#define BLE_ACK_REPEAT_US 100000u /* Повтор последнего ACK при отсутствии нового DATA. */
|
||||
struct ble_transport;
|
||||
@@ -52,6 +50,7 @@ struct ble_config {
|
||||
struct ble_callbacks callbacks;
|
||||
void* arg; /* Заимствованный контекст callbacks, живёт до destroy. */
|
||||
unsigned tx_limit; /* Целых TX-пакетов, включая фрагментируемый: 0=64, 1..64. */
|
||||
unsigned tx_window; /* DATA в очереди/отправке/ожидании удалённого ACK: 0=BLE_TX_WINDOW_FRAMES, 1..64. */
|
||||
};
|
||||
/* Создать остановленный transport. ua/config заимствованы только ua; возвращает handle или NULL с логом. */
|
||||
struct ble_transport* ble_create(struct UASYNC* ua, const struct ble_config* config);
|
||||
@@ -74,7 +73,7 @@ void ble_disconnect(struct ble_transport* transport, ble_link_id link);
|
||||
/* Принять целый ll_entry в TX. При 0 владение передано; при любом отказе entry остаётся вызывающему.
|
||||
* entry должен быть вне очереди, dgram!=NULL, len=1..1500. Заполнение: WOULD_BLOCK, затем writable. */
|
||||
int ble_tx_put(struct ble_transport* transport, ble_link_id link, struct ll_entry* entry);
|
||||
/* Снимок готовности принять пакет; заполнение учитывает текущую фрагментацию.
|
||||
/* Снимок готовности принять пакет; учитывает очередь и кадры до ACK последнего фрагмента.
|
||||
* После заполнения writable вызывается при освобождении места, без обязательного отказа tx_put. */
|
||||
int ble_tx_ready(struct ble_transport* transport, ble_link_id link);
|
||||
/* Извлечь целый кадр из RX или NULL; вызывающий освобождает queue_dgram_free + queue_entry_free. */
|
||||
|
||||
@@ -137,9 +137,10 @@ static int acquire(const char* device, int channel) {
|
||||
"AcquireNotify", replied, &reply, "a{sv}", 2, "device", "o", device, "mtu", "q", 23) >= 0);
|
||||
poll_all(); CHECK(reply.done && !reply.failed && reply.fd >= 0); sd_bus_slot_unref(reply.slot); return reply.fd;
|
||||
}
|
||||
static void write_value(const char* device, const uint8_t* data, size_t len, int invitation) {
|
||||
static void write_value(const char* device, const uint8_t* data, size_t len, int channel) {
|
||||
struct reply reply = {0}; sd_bus_message* m = NULL;
|
||||
CHECK(sd_bus_message_new_method_call(bus, &m, application, invitation ? ROOT "/service0/invite" : ROOT "/service0/rx", "org.bluez.GattCharacteristic1", "WriteValue") >= 0);
|
||||
const char* path = channel == 2 ? ROOT "/service0/ack" : channel ? ROOT "/service0/invite" : ROOT "/service0/rx";
|
||||
CHECK(sd_bus_message_new_method_call(bus, &m, application, path, "org.bluez.GattCharacteristic1", "WriteValue") >= 0);
|
||||
CHECK(sd_bus_message_append_array(m, 'y', data, len) >= 0);
|
||||
CHECK(sd_bus_message_append(m, "a{sv}", 2, "device", "o", device, "mtu", "q", 23) >= 0);
|
||||
CHECK(sd_bus_call_async(bus, &reply.slot, m, replied, &reply, 1000000) >= 0); sd_bus_message_unref(m);
|
||||
@@ -219,6 +220,8 @@ int main(void) {
|
||||
CHECK(recv(fd1, data, sizeof(data), MSG_DONTWAIT) == (part == 22 ? 18 : 20));
|
||||
CHECK(data[8] == part && data[9] == 0xd4);
|
||||
}
|
||||
/* Flow ACK последнего DATA-фрагмента, независимо от ATT confirmation канала INVITE. */
|
||||
write_value(ADAPTER "/dev_A", data, 2, 2);
|
||||
CHECK(send(invite2, "\1", 1, MSG_NOSIGNAL) == 1); poll_all();
|
||||
}
|
||||
CHECK(ack_values >= 2);
|
||||
|
||||
@@ -110,6 +110,78 @@ static struct ll_entry* frame(unsigned int size, uint8_t value) {
|
||||
struct ll_entry* e = ll_alloc_lldgram(size); CHECK(e); e->len = size; memset(e->dgram, value, size); return e;
|
||||
}
|
||||
static void release(struct ll_entry* e) { queue_dgram_free(e); queue_entry_free(e); }
|
||||
struct resume_writer { struct ble_transport* radio; unsigned sent, wakes, goal; int fragmented; };
|
||||
/* Следующий кадр подаёт только writable: ни polling tx_ready, ни пробный отказ не маскируют потерянное пробуждение. */
|
||||
static void resume_write(void* arg, ble_link_id id) {
|
||||
struct resume_writer* w = arg; w->wakes++;
|
||||
CHECK(ble_tx_ready(w->radio, id));
|
||||
if (w->sent == w->goal) return;
|
||||
unsigned size = w->fragmented && w->sent % 2 ? 1500 : 7;
|
||||
CHECK(!ble_tx_put(w->radio, id, frame(size, w->sent))); w->sent++;
|
||||
}
|
||||
/* ACK до completion не освобождает занятую локальную очередь; обратный порядок ждёт ACK окна из одного кадра. */
|
||||
static void exercise_resume_order(unsigned test_mtu, int ack_first) {
|
||||
mtu = test_mtu; hold = hold_ack = drop = reject_peer = 0;
|
||||
struct UASYNC* ua = uasync_create(); CHECK(ua);
|
||||
struct resume_writer writer = {0};
|
||||
struct ble_config cfg = {.node_id = 1, .tx_limit = 1, .tx_window = 1, .arg = &writer};
|
||||
cfg.callbacks.writable = resume_write;
|
||||
struct ble_transport* a = ble_create(ua, &cfg); writer.radio = a;
|
||||
cfg.node_id = 2; cfg.arg = NULL; cfg.callbacks.writable = NULL;
|
||||
struct ble_transport* b = ble_create(ua, &cfg); CHECK(a && b && !ble_start(a) && !ble_start(b)); drain(ua);
|
||||
struct ble_endpoint ep = {"1"}; ble_link_id id = ble_connect(a, &ep, 2); CHECK(id); drain(ua);
|
||||
ble_link_id other = incoming_ids[1]; hold = hold_ack = 1;
|
||||
writer = (struct resume_writer){.radio = a, .goal = 8};
|
||||
CHECK(!ble_tx_put(a, id, frame(7, 0))); writer.sent = 1;
|
||||
for (unsigned i = 0; i < writer.goal; i++) {
|
||||
struct fake_link* f = slot(backends[0], id); CHECK(f && f->pending && f->channel == BLE_WIRE_DATA);
|
||||
uint8_t ack[2] = {f->data[0], f->data[1]};
|
||||
CHECK(!ble_tx_ready(a, id) && writer.wakes == i);
|
||||
ble_backend_received(b, other, f->channel, f->data, f->len);
|
||||
if (ack_first) ble_backend_received(a, id, BLE_WIRE_ACK, ack, sizeof(ack));
|
||||
else { f->pending = 0; ble_backend_sent(a, id, 1); }
|
||||
CHECK(!ble_tx_ready(a, id) && writer.wakes == i);
|
||||
if (ack_first) { f->pending = 0; ble_backend_sent(a, id, 1); }
|
||||
else ble_backend_received(a, id, BLE_WIRE_ACK, ack, sizeof(ack));
|
||||
CHECK(writer.wakes == i + 1);
|
||||
ble_backend_received(a, id, BLE_WIRE_ACK, ack, sizeof(ack)); CHECK(writer.wakes == i + 1);
|
||||
struct ll_entry* e = ble_rx_get(b, other); CHECK(e && e->len == 7 && e->dgram[0] == i); release(e);
|
||||
}
|
||||
CHECK(writer.sent == writer.goal && ble_tx_ready(a, id));
|
||||
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "PASS BLE resume order: MTU=%u ACK_first=%d wakes=%u frames=%u; no refused put/polling",
|
||||
test_mtu, ack_first, writer.wakes, writer.sent);
|
||||
ble_destroy(a); ble_destroy(b); uasync_destroy(ua, 0); hold = hold_ack = 0;
|
||||
}
|
||||
/* Production tx_limit=1/window=3: только callback подаёт следующие короткие и фрагментированные кадры. */
|
||||
static void exercise_resume_stream(unsigned test_mtu) {
|
||||
mtu = test_mtu; hold = hold_ack = drop = reject_peer = 0;
|
||||
struct UASYNC* ua = uasync_create(); CHECK(ua);
|
||||
struct resume_writer writer = {0};
|
||||
struct ble_config cfg = {.node_id = 1, .tx_limit = 1, .tx_window = 3, .arg = &writer};
|
||||
cfg.callbacks.writable = resume_write;
|
||||
struct ble_transport* a = ble_create(ua, &cfg); writer.radio = a;
|
||||
cfg.node_id = 2; cfg.arg = NULL; cfg.callbacks.writable = NULL;
|
||||
struct ble_transport* b = ble_create(ua, &cfg); CHECK(a && b && !ble_start(a) && !ble_start(b)); drain(ua);
|
||||
struct ble_endpoint ep = {"1"}; ble_link_id id = ble_connect(a, &ep, 2); CHECK(id); drain(ua);
|
||||
ble_link_id other = incoming_ids[1];
|
||||
writer = (struct resume_writer){.radio = a, .goal = 24, .fragmented = 1};
|
||||
CHECK(!ble_tx_put(a, id, frame(7, 0))); writer.sent = 1;
|
||||
uint64_t started = get_time_us(); unsigned received = 0;
|
||||
while ((received < writer.goal || writer.wakes < writer.goal) && get_time_us() - started < 3000000) {
|
||||
uasync_poll(ua, 10);
|
||||
struct ll_entry* e;
|
||||
while ((e = ble_rx_get(b, other))) {
|
||||
CHECK(e->len == (received % 2 ? 1500 : 7));
|
||||
for (unsigned i = 0; i < e->len; i++) CHECK(e->dgram[i] == received);
|
||||
received++; release(e);
|
||||
}
|
||||
}
|
||||
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "BLE resume stream: MTU=%u TX=%u RX=%u wakes=%u elapsed_us=%llu",
|
||||
test_mtu, writer.sent, received, writer.wakes, (unsigned long long)(get_time_us() - started));
|
||||
CHECK(writer.sent == writer.goal && received == writer.goal && writer.wakes == writer.goal);
|
||||
CHECK(ble_tx_ready(a, id));
|
||||
ble_destroy(a); ble_destroy(b); uasync_destroy(ua, 0);
|
||||
}
|
||||
/* Непрерывный RX при ACK completion >15ms не должен вытеснять встречный DATA. */
|
||||
static void exercise_ack_fairness(void) {
|
||||
mtu = 517; hold = drop = reject_peer = hold_ack = 0; ack_delay_us = 0;
|
||||
@@ -141,7 +213,7 @@ static void exercise(unsigned int test_mtu) {
|
||||
struct UASYNC* ua = uasync_create(); CHECK(ua);
|
||||
struct ble_transport* t[3];
|
||||
for (int i = 0; i < 3; i++) {
|
||||
struct ble_config cfg = {0}; cfg.node_id = i + 1; cfg.company_id = 0xffff;
|
||||
struct ble_config cfg = {.tx_window = BLE_QUEUE_MAX}; cfg.node_id = i + 1; cfg.company_id = 0xffff;
|
||||
cfg.callbacks.validate = validate; cfg.callbacks.connection = connection; cfg.callbacks.writable = writable;
|
||||
t[i] = ble_create(ua, &cfg); CHECK(t[i] && !ble_start(t[i]));
|
||||
}
|
||||
@@ -208,7 +280,8 @@ static void exercise(unsigned int test_mtu) {
|
||||
static void exercise_flow(void) {
|
||||
mtu = 517; hold = drop = reject_peer = hold_ack = 0;
|
||||
struct UASYNC* ua = uasync_create(); CHECK(ua);
|
||||
struct ble_config cfg = {.node_id = 1}; cfg.callbacks.writable = writable; cfg.callbacks.connection = connection;
|
||||
struct ble_config cfg = {.node_id = 1, .tx_window = BLE_QUEUE_MAX};
|
||||
cfg.callbacks.writable = writable; cfg.callbacks.connection = connection;
|
||||
struct ble_transport* a = ble_create(ua, &cfg); cfg.node_id = 2;
|
||||
struct ble_transport* b = ble_create(ua, &cfg); CHECK(a && b && !ble_start(a) && !ble_start(b)); drain(ua);
|
||||
struct ble_endpoint ep = {"1"}; ble_link_id id = ble_connect(a, &ep, 2); CHECK(id); drain(ua);
|
||||
@@ -254,7 +327,7 @@ static void exercise_flow(void) {
|
||||
(unsigned long long)(get_time_us() - recovery_started));
|
||||
drain(ua);
|
||||
/* Больше полного оборота wire ID; новые данные действительно приходят, старые ACK не открывают окно. */
|
||||
unsigned sent = 0, delivered = 0; deadline = get_time_us() + 10000000;
|
||||
unsigned sent = 0, delivered = 0; deadline = get_time_us() + 30000000;
|
||||
while (delivered < 70000 && get_time_us() < deadline) {
|
||||
while (sent < 70000 && ble_tx_ready(a, id)) { CHECK(!ble_tx_put(a, id, frame(1, sent))); sent++; }
|
||||
uasync_poll(ua, 0);
|
||||
@@ -273,11 +346,52 @@ static void exercise_flow(void) {
|
||||
ble_destroy(a); ble_destroy(b); uasync_destroy(ua, 0);
|
||||
puts("BLE flow PASS (12500-byte duplex bound, ACK >=15ms, duplicate ACK, resume, 70000 IDs, invalid ACK, timeout, teardown)");
|
||||
}
|
||||
/* Локальное завершение и ACK середины кадра не освобождают окно; ACK конца освобождает ровно один слот. */
|
||||
static void exercise_frame_window(unsigned test_mtu) {
|
||||
mtu = test_mtu; hold = drop = reject_peer = hold_ack = 0;
|
||||
struct UASYNC* ua = uasync_create(); CHECK(ua);
|
||||
struct ble_config cfg = {.node_id = 1, .tx_window = 2}; cfg.callbacks.writable = writable;
|
||||
struct ble_transport* a = ble_create(ua, &cfg); cfg.node_id = 2;
|
||||
struct ble_transport* b = ble_create(ua, &cfg); CHECK(a && b && !ble_start(a) && !ble_start(b)); drain(ua);
|
||||
struct ble_endpoint ep = {"1"}; ble_link_id id = ble_connect(a, &ep, 2); CHECK(id); drain(ua);
|
||||
ble_link_id other = incoming_ids[1]; hold_ack = 1;
|
||||
CHECK(!ble_tx_put(a, id, frame(1500, 0x41))); drain(ua);
|
||||
struct fake_link* receiver = slot(backends[1], other); CHECK(receiver && receiver->ack_held);
|
||||
uint16_t first_end = receiver->ack_id, partial = first_end - 1;
|
||||
uint8_t ack[2] = {partial >> 8, partial};
|
||||
ble_backend_received(a, id, BLE_WIRE_ACK, ack, sizeof(ack));
|
||||
CHECK(!ble_tx_put(a, id, frame(17, 0x42))); drain(ua);
|
||||
CHECK(!ble_tx_ready(a, id));
|
||||
struct ll_entry* e = frame(7, 0x43);
|
||||
CHECK(ble_tx_put(a, id, e) == BLE_WOULD_BLOCK && !e->owner);
|
||||
unsigned before_writable = writable_count;
|
||||
ack[0] = first_end >> 8; ack[1] = first_end;
|
||||
ble_backend_received(a, id, BLE_WIRE_ACK, ack, sizeof(ack));
|
||||
CHECK(ble_tx_ready(a, id) && writable_count > before_writable);
|
||||
CHECK(!ble_tx_put(a, id, e) && !ble_tx_ready(a, id));
|
||||
ble_backend_received(a, id, BLE_WIRE_ACK, ack, sizeof(ack)); CHECK(!ble_tx_ready(a, id));
|
||||
CHECK(!ble_channel_tx_put(a, id, BLE_CHANNEL_INVITE, frame(3, 0x44))); drain(ua);
|
||||
e = ble_channel_rx_get(b, other, BLE_CHANNEL_INVITE); CHECK(e && e->len == 3); release(e);
|
||||
hold_ack = 0; schedule(backends[1]); drain(ua);
|
||||
unsigned sizes[] = {1500, 17, 7};
|
||||
for (unsigned i = 0; i < 3; i++) {
|
||||
e = ble_rx_get(b, other); CHECK(e && e->len == sizes[i] && e->dgram[0] == 0x41 + i); release(e);
|
||||
}
|
||||
CHECK(!ble_rx_get(b, other) && ble_tx_ready(a, id));
|
||||
ble_destroy(a); ble_destroy(b); uasync_destroy(ua, 0);
|
||||
printf("BLE frame window PASS (MTU=%u, two frames, partial/final/duplicate ACK, writable, independent INVITE)\n", test_mtu);
|
||||
}
|
||||
int main(void) {
|
||||
debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN);
|
||||
debug_set_category_level(DEBUG_CATEGORY_DEBUG, DEBUG_LEVEL_INFO);
|
||||
size_t before = u_get_allocated_count();
|
||||
for (unsigned i = 0; i < 2; i++) {
|
||||
exercise_resume_order(i ? 517 : 23, 0); exercise_resume_order(i ? 517 : 23, 1);
|
||||
exercise_resume_stream(i ? 517 : 23); CHECK(u_get_allocated_count() == before);
|
||||
}
|
||||
exercise_ack_fairness(); CHECK(u_get_allocated_count() == before);
|
||||
exercise(23); exercise(247); exercise(517);
|
||||
exercise_frame_window(23); exercise_frame_window(517);
|
||||
exercise_flow();
|
||||
CHECK(u_get_allocated_count() == before); puts("BLE transport tests PASS (MTU 23/247/517, duplex, loss, backpressure, peers, teardown)"); return 0;
|
||||
}
|
||||
@@ -208,6 +208,8 @@ utun_CORE_SOURCES = \
|
||||
dm/dm_mailbox_media.c \
|
||||
dm/dm_mailbox_media.h \
|
||||
call/call.c \
|
||||
call/call_audio_batch.c \
|
||||
call/call_audio_batch.h \
|
||||
call/call_video_rate.c \
|
||||
call/call_video_rate.h \
|
||||
call/call_video.c \
|
||||
@@ -402,6 +404,8 @@ libutun_a_SOURCES = \
|
||||
dm/dm_mailbox_media.c \
|
||||
dm/dm_mailbox_media.h \
|
||||
call/call.c \
|
||||
call/call_audio_batch.c \
|
||||
call/call_audio_batch.h \
|
||||
call/call_video_rate.c \
|
||||
call/call_video_rate.h \
|
||||
call/call_video.c \
|
||||
|
||||
+142
-90
@@ -40,6 +40,16 @@
|
||||
|
||||
#define CALL_ID "call"
|
||||
|
||||
/* Локальный кадр до упаковки: время создания не зависит от сетевого wire timestamp пачки. */
|
||||
struct call_audio_pending {
|
||||
uint64_t created_us;
|
||||
struct call_capture_time timing;
|
||||
uint16_t seq;
|
||||
};
|
||||
static void call_tx_flush(struct call_session* s);
|
||||
static void call_audio_batch_tick(void* arg);
|
||||
|
||||
|
||||
/* ───────────────────────────────────────────────────────────────────────── */
|
||||
|
||||
static struct call_ctx* call_of(struct UTUN_INSTANCE* inst) {
|
||||
@@ -140,6 +150,7 @@ static struct call_session* call_session_new(struct call_ctx* ctx, uint64_t call
|
||||
s->state = CALL_ST_IDLE;
|
||||
s->started_tb = get_time_tb();
|
||||
s->ctx = ctx;
|
||||
call_audio_batch_init(&s->audio_batch, get_time_us());
|
||||
s->tx_q = queue_new(ctx->inst->ua, 0, 0, 0, "call_txq");
|
||||
if (!s->tx_q) { queue_entry_free(qe); return NULL; }
|
||||
// При провале queue_data_put_with_index сам освобождает qe (вместе с s), поэтому
|
||||
@@ -161,6 +172,9 @@ static void call_session_free_cb(void* arg) {
|
||||
* дренировать tx_q (без отправки). Безопасно вызывать из uasync-потока. */
|
||||
static void call_session_cleanup(struct call_ctx* ctx, struct call_session* s) {
|
||||
call_video_transport_stop(s);
|
||||
if (s->audio_batch_timer) uasync_cancel_timeout(ctx->inst->ua, s->audio_batch_timer);
|
||||
if (s->audio_flush_timer) uasync_cancel_timeout(ctx->inst->ua, s->audio_flush_timer);
|
||||
s->audio_batch_timer = s->audio_flush_timer = NULL;
|
||||
etcp_router_cancel_send_ready(ctx->inst, s->group_id, s->peer_node_id, ETCP_RT_ID_CALL_AUDIO, &s->send_waiter);
|
||||
if (s->tx_q) {
|
||||
struct ll_entry* e;
|
||||
@@ -417,6 +431,9 @@ static void call_activate(struct call_session* s) {
|
||||
if (s->state == CALL_ST_ACTIVE || s->state == CALL_ST_ENDED) return;
|
||||
struct call_ctx* ctx = s->ctx;
|
||||
s->state = CALL_ST_ACTIVE;
|
||||
call_audio_batch_tick(s);
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "audio batch startup call=%llx target=%u hold_us=%u",
|
||||
(unsigned long long)s->call_id, s->audio_batch.target, s->audio_batch.hold_us);
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: accepted id=%016llx -> ACTIVE (router delivery)", CALL_ID,
|
||||
(unsigned long long)s->call_id);
|
||||
call_event_id(ctx, CHAT_EVT_CALL_ACCEPTED, s->call_id, 0);
|
||||
@@ -513,8 +530,8 @@ static void call_post_stats(struct call_session* s) {
|
||||
uint16_t rtt = rtt_tb / 10; /* ETCP хранит RTT в 0.1мс; CALL_STATS передаёт миллисекунды. */
|
||||
uint32_t oldest_ms = 0;
|
||||
if (s->tx_q && s->tx_q->head) {
|
||||
struct call_pkt_media oldest; memcpy(&oldest, s->tx_q->head->dgram + 1, sizeof(oldest));
|
||||
oldest_ms = (uint32_t)(get_time_tb() / 10) - oldest.ts_ms;
|
||||
struct call_audio_pending *oldest = (struct call_audio_pending*)s->tx_q->head->data;
|
||||
oldest_ms = (uint32_t)((get_time_us() - oldest->created_us) / 1000);
|
||||
}
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "call audio TX queue id=%016llx drained=%u queued=%zu oldest_ms=%u wait_avg_max_ms=%llu/%u",
|
||||
(unsigned long long)s->call_id, s->tx_wait_frames, s->tx_q ? (size_t)queue_entry_count(s->tx_q) : 0, oldest_ms,
|
||||
@@ -756,46 +773,33 @@ static void call_handle_hangup(struct call_ctx* ctx, uint64_t from, const uint8_
|
||||
}
|
||||
|
||||
static void call_handle_media(struct call_ctx* ctx, uint64_t from, const uint8_t* b, size_t n) {
|
||||
if (n <= CALL_MEDIA_HDR_SIZE) { DEBUG_WARN(DEBUG_CATEGORY_CALL, "call media truncated bytes=%zu", n); return; }
|
||||
uint64_t call_id; memcpy(&call_id, b + 1, 8);
|
||||
struct call_session* s = call_session_find(ctx, call_id);
|
||||
if (!s || s->state != CALL_ST_ACTIVE) {
|
||||
/* медиа вне ACTIVE — игнорируем (гонка с завершением) */
|
||||
return;
|
||||
struct call_audio_batch_view batch;
|
||||
if (call_audio_batch_parse(b, n, &batch)) return;
|
||||
struct call_pkt_media *h = &batch.header;
|
||||
struct call_session* s = call_session_find(ctx, h->call_id);
|
||||
if (!s || s->state != CALL_ST_ACTIVE) return;
|
||||
if (from != s->peer_node_id) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CALL, "audio batch wrong peer=%llx expected=%llx",
|
||||
(unsigned long long)from, (unsigned long long)s->peer_node_id); return;
|
||||
}
|
||||
struct call_pkt_media packet;
|
||||
memcpy(&packet, b, sizeof(packet));
|
||||
if (from != s->peer_node_id || packet.track_id != CALL_TRACK_AUDIO || packet.codec_delay_samples > CALL_OPUS_SAMPLE_RATE
|
||||
|| (packet.capture_pts_us != INT64_MIN && (packet.capture_pts_us < 0 || packet.capture_pts_us > INT64_MAX / 2
|
||||
|| !packet.capture_epoch))) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CALL, "call media invalid peer=%016llx expected=%016llx track=%u pts=%lld epoch=%llu delay=%u",
|
||||
(unsigned long long)from, (unsigned long long)s->peer_node_id, packet.track_id,
|
||||
(long long)packet.capture_pts_us, (unsigned long long)packet.capture_epoch, packet.codec_delay_samples);
|
||||
return;
|
||||
}
|
||||
struct call_capture_time timing = {packet.capture_pts_us, packet.capture_epoch, packet.codec_delay_samples};
|
||||
s->c_rx_frames++;
|
||||
++s->rx_batches;
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "audio batch RX call=%llx frames=%u bytes=%zu first_seq=%u sent_ms=%u",
|
||||
(unsigned long long)s->call_id, h->count, n, batch.frames[0].header.seq, h->sent_ms);
|
||||
s->last_media_tb = get_time_tb();
|
||||
|
||||
uint16_t seq = 0;
|
||||
uint32_t ts_ms = 0;
|
||||
memcpy(&seq, b + 10, 2);
|
||||
memcpy(&ts_ms, b + 12, 4);
|
||||
|
||||
/* аудио-движок (декод + джиттер-буфер) — если зарегистрирован */
|
||||
const struct call_audio_ops* audio = ctx->inst->call_audio_ops;
|
||||
if (audio && audio->on_media)
|
||||
audio->on_media(ctx->inst, call_id, seq, ts_ms, b + CALL_MEDIA_HDR_SIZE, (int)(n - CALL_MEDIA_HDR_SIZE), &timing);
|
||||
|
||||
/* релей Opus-кадра потребителю (headless-аудио-сокет) */
|
||||
if (ctx->media_recv_fn)
|
||||
ctx->media_recv_fn(ctx->inst, call_id, b + CALL_MEDIA_HDR_SIZE,
|
||||
(int)(n - CALL_MEDIA_HDR_SIZE), ctx->media_recv_arg);
|
||||
|
||||
/* watchdog: пере-взводим таймер no-traffic на каждый принятый кадр */
|
||||
if (s->traffic_timer) { uasync_cancel_timeout(ctx->inst->ua, s->traffic_timer); s->traffic_timer = NULL; }
|
||||
for (unsigned i = 0; i < h->count && s->state == CALL_ST_ACTIVE; i++) {
|
||||
const struct call_audio_frame_view *f = &batch.frames[i];
|
||||
struct call_capture_time timing = {f->header.capture_pts_us, h->capture_epoch, h->codec_delay_samples};
|
||||
s->c_rx_frames++;
|
||||
const struct call_audio_ops* audio = ctx->inst->call_audio_ops;
|
||||
if (audio && audio->on_media)
|
||||
audio->on_media(ctx->inst, s->call_id, f->header.seq, h->sent_ms, f->opus, f->header.length, &timing, i == 0);
|
||||
if (ctx->media_recv_fn && s->state == CALL_ST_ACTIVE)
|
||||
ctx->media_recv_fn(ctx->inst, s->call_id, f->opus, f->header.length, ctx->media_recv_arg);
|
||||
}
|
||||
if (s->state != CALL_ST_ACTIVE) return;
|
||||
if (s->traffic_timer) uasync_cancel_timeout(ctx->inst->ua, s->traffic_timer);
|
||||
s->traffic_timer = uasync_set_timeout(ctx->inst->ua, CALL_NO_TRAFFIC_TB, s, call_traffic_timeout_cb, "call_traffic");
|
||||
(void)from;
|
||||
if (!s->traffic_timer) DEBUG_ERROR(DEBUG_CATEGORY_CALL, "audio RX watchdog allocation failed");
|
||||
}
|
||||
|
||||
/* ─────────────────── приём (etcp_router) ─────────────────── */
|
||||
@@ -1001,77 +1005,125 @@ static void call_tx_drain_cb(struct ll_queue* q, void* arg) {
|
||||
call_tx_flush(s);
|
||||
}
|
||||
|
||||
/* Дренируем tx_q, пока роутер готов; иначе — одноразовый waiter (re-arm в drain_cb). */
|
||||
/* Deadline относится к возрасту головы; новые кадры не отодвигают срок отправки. */
|
||||
static void call_audio_flush_timer(void* arg) {
|
||||
struct call_session *s = arg; s->audio_flush_timer = NULL; call_tx_flush(s);
|
||||
}
|
||||
static void call_audio_batch_tick(void* arg) {
|
||||
struct call_session *s = arg; s->audio_batch_timer = NULL;
|
||||
if (s->state != CALL_ST_ACTIVE) return;
|
||||
uint64_t now = get_time_us();
|
||||
struct ETCP_ROUTER_TX_STATS net = {0};
|
||||
int found = etcp_router_tx_stats(s->ctx->inst, s->group_id, s->peer_node_id, ETCP_RT_ID_CALL_AUDIO, now, &net) == 0;
|
||||
if (call_audio_batch_step(&s->audio_batch, now, found ? &net : NULL)) s->audio_batch.hold_us = 0;
|
||||
if (now - s->audio_report_us >= 1000000) {
|
||||
s->audio_report_us = now;
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_CALL,
|
||||
"audio batch flow call=%llx target=%u hold_us=%u router_rtt_us=%llu queued=%u/%llu inflight=%u/%llu "
|
||||
"ack_bytes_500ms=%llu local_frames=%d tx_frames=%u tx_batches=%u rx_frames=%u rx_batches=%u",
|
||||
(unsigned long long)s->call_id, s->audio_batch.target, s->audio_batch.hold_us,
|
||||
(unsigned long long)net.delivery_rtt_us, net.queued_packets, (unsigned long long)net.queued_age_us,
|
||||
net.inflight_packets, (unsigned long long)net.inflight_age_us, (unsigned long long)net.window.bytes,
|
||||
s->tx_q ? queue_entry_count(s->tx_q) : 0, s->c_tx_frames, s->tx_batches, s->c_rx_frames, s->rx_batches);
|
||||
}
|
||||
call_tx_flush(s);
|
||||
if (s->state != CALL_ST_ACTIVE) return;
|
||||
s->audio_batch_timer = uasync_set_timeout(s->ctx->inst->ua, 500, s, call_audio_batch_tick, "audio_batch");
|
||||
if (!s->audio_batch_timer) {
|
||||
s->audio_batch.hold_us = 0;
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "audio batch timer allocation failed; immediate TX");
|
||||
}
|
||||
}
|
||||
|
||||
/* Пачка формируется до Router: принятые им кодограммы не изменяются. */
|
||||
static void call_tx_flush(struct call_session* s) {
|
||||
struct call_ctx* ctx = s->ctx;
|
||||
if (s->state == CALL_ST_ENDED) return;
|
||||
/* гарантируем router conn — иначе has_room/on_send_ready не видят его */
|
||||
etcp_router_conn_get(ctx->inst, s->group_id, s->peer_node_id, ETCP_RT_ID_CALL_AUDIO);
|
||||
while (s->tx_q && s->tx_q->head) {
|
||||
if (s->state != CALL_ST_ACTIVE || !s->tx_q || !s->tx_q->head) return;
|
||||
if (s->audio_flush_timer) { uasync_cancel_timeout(ctx->inst->ua, s->audio_flush_timer); s->audio_flush_timer = NULL; }
|
||||
if (!etcp_router_conn_get(ctx->inst, s->group_id, s->peer_node_id, ETCP_RT_ID_CALL_AUDIO)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "audio router channel unavailable; retaining frames"); return;
|
||||
}
|
||||
while (s->state == CALL_ST_ACTIVE && s->tx_q && s->tx_q->head) {
|
||||
if (!etcp_router_send_q_has_room(ctx->inst, s->group_id, s->peer_node_id, ETCP_RT_ID_CALL_AUDIO)) {
|
||||
etcp_router_on_send_ready(ctx->inst, s->group_id, s->peer_node_id, ETCP_RT_ID_CALL_AUDIO,
|
||||
&s->send_waiter, call_tx_drain_cb, s);
|
||||
return;
|
||||
}
|
||||
struct ll_entry* e = queue_data_get(s->tx_q);
|
||||
struct call_pkt_media packet; memcpy(&packet, e->dgram + 1, sizeof(packet));
|
||||
uint32_t wait_ms = (uint32_t)(get_time_tb() / 10) - packet.ts_ms;
|
||||
++s->tx_wait_frames; s->tx_wait_total_ms += wait_ms;
|
||||
if (wait_ms > s->tx_wait_max_ms) s->tx_wait_max_ms = wait_ms;
|
||||
if (etcp_route_send(ctx->inst, s->group_id, s->peer_node_id, e, 1, ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT) == 0) {
|
||||
s->c_tx_frames++;
|
||||
if (packet.capture_pts_us != INT64_MIN) call_video_audio_sent(s, packet.capture_pts_us + 20000);
|
||||
} else DEBUG_WARN(DEBUG_CATEGORY_CALL, "%s: media enqueue failed id=%016llx", CALL_ID, (unsigned long long)s->call_id);
|
||||
struct call_audio_pending *first = (struct call_audio_pending*)s->tx_q->head->data;
|
||||
struct call_audio_batch_view batch = {.header = {.subcmd = CALL_SUBCMD_MEDIA, .call_id = s->call_id,
|
||||
.track_id = CALL_TRACK_AUDIO, .capture_epoch = first->timing.epoch,
|
||||
.codec_delay_samples = first->timing.delay_samples, .sent_ms = (uint32_t)(get_time_tb() / 10)}};
|
||||
size_t size = CALL_MEDIA_HDR_SIZE;
|
||||
struct ll_entry *next = s->tx_q->head;
|
||||
while (next && batch.header.count < s->audio_batch.target) {
|
||||
struct call_audio_pending *p = (struct call_audio_pending*)next->data;
|
||||
if (p->timing.epoch != first->timing.epoch || p->timing.delay_samples != first->timing.delay_samples
|
||||
|| size + CALL_AUDIO_FRAME_HDR_SIZE + next->len > CALL_MEDIA_MAX_BYTES) break;
|
||||
batch.frames[batch.header.count++] = (struct call_audio_frame_view){
|
||||
.header = {p->seq, p->timing.pts_us, next->len}, .opus = next->dgram};
|
||||
size += CALL_AUDIO_FRAME_HDR_SIZE + next->len; next = next->next;
|
||||
}
|
||||
uint64_t now = get_time_us(), age = now - first->created_us;
|
||||
if (!next && batch.header.count < s->audio_batch.target && age < s->audio_batch.hold_us) {
|
||||
s->audio_flush_timer = uasync_set_timeout(ctx->inst->ua, (s->audio_batch.hold_us - age + 199) / 100,
|
||||
s, call_audio_flush_timer, "audio_batch_flush");
|
||||
if (s->audio_flush_timer) return;
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "audio flush timer allocation failed; sending incomplete batch");
|
||||
}
|
||||
struct ll_entry *wire = ll_alloc_lldgram(1 + size);
|
||||
if (!wire) { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "audio batch allocation failed bytes=%zu", size); return; }
|
||||
wire->dgram[0] = ETCP_RT_ID_CALL_AUDIO;
|
||||
size_t encoded = 0;
|
||||
if (call_audio_batch_encode(&batch, wire->dgram + 1, size, &encoded)) {
|
||||
queue_dgram_free(wire); queue_entry_free(wire); return;
|
||||
}
|
||||
wire->len = 1 + encoded;
|
||||
unsigned count = batch.header.count;
|
||||
int64_t last_pts = batch.frames[count - 1].header.capture_pts_us;
|
||||
/* Снять исходные кадры до синхронных callbacks Router, которые могут завершить звонок. */
|
||||
for (unsigned i = 0; i < count; i++) {
|
||||
struct ll_entry *e = queue_data_get(s->tx_q);
|
||||
uint32_t wait_ms = (uint32_t)((now - ((struct call_audio_pending*)e->data)->created_us) / 1000);
|
||||
s->tx_wait_frames++; s->tx_wait_total_ms += wait_ms;
|
||||
if (wait_ms > s->tx_wait_max_ms) s->tx_wait_max_ms = wait_ms;
|
||||
queue_dgram_free(e); queue_entry_free(e);
|
||||
}
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "audio batch TX call=%llx frames=%u target=%u bytes=%zu oldest_us=%llu first_seq=%u",
|
||||
(unsigned long long)s->call_id, count, s->audio_batch.target, encoded,
|
||||
(unsigned long long)age, batch.frames[0].header.seq);
|
||||
if (etcp_route_send(ctx->inst, s->group_id, s->peer_node_id, wire, 1, ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT) == 0) {
|
||||
s->c_tx_frames += count; s->tx_batches++;
|
||||
if (s->state == CALL_ST_ACTIVE && last_pts != INT64_MIN) call_video_audio_sent(s, last_pts + 20000);
|
||||
} else {
|
||||
s->c_tx_dropped += count;
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CALL, "audio batch enqueue failed call=%llx frames=%u", (unsigned long long)s->call_id, count);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int call_send_media(struct UTUN_INSTANCE* inst, uint64_t call_id, const uint8_t* opus, int len,
|
||||
const struct call_capture_time* timing) {
|
||||
struct call_ctx* ctx = call_of(inst);
|
||||
if (!ctx || !ctx->initialized || !opus || len <= 0) return -1;
|
||||
if (!ctx || !ctx->initialized) return -1;
|
||||
struct call_session* s = call_session_find(ctx, call_id);
|
||||
if (!s || s->state != CALL_ST_ACTIVE) return -1;
|
||||
if (CALL_MEDIA_HDR_SIZE + (size_t)len > 1024) return -1;
|
||||
if (timing && (timing->delay_samples > CALL_OPUS_SAMPLE_RATE || (timing->pts_us != INT64_MIN
|
||||
&& (timing->pts_us < 0 || timing->pts_us > INT64_MAX / 2 || !timing->epoch)))) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "call media invalid capture metadata pts=%lld epoch=%llu delay=%u",
|
||||
(long long)timing->pts_us, (unsigned long long)timing->epoch, timing->delay_samples);
|
||||
return -1;
|
||||
if (!opus || len <= 0 || (size_t)len > CALL_AUDIO_MAX_OPUS || (timing && (timing->delay_samples > CALL_OPUS_SAMPLE_RATE
|
||||
|| (timing->pts_us != INT64_MIN && (timing->pts_us < 0 || timing->pts_us > INT64_MAX / 2 || !timing->epoch))))) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CALL, "audio frame invalid length/timing len=%d", len); return -1;
|
||||
}
|
||||
|
||||
uint8_t buf[1024];
|
||||
struct call_pkt_media* m = (struct call_pkt_media*)buf;
|
||||
m->subcmd = CALL_SUBCMD_MEDIA;
|
||||
m->call_id = call_id;
|
||||
m->track_id = CALL_TRACK_AUDIO;
|
||||
m->seq = s->tx_media_seq++;
|
||||
m->ts_ms = (uint32_t)(get_time_tb() / 10);
|
||||
m->capture_pts_us = timing ? timing->pts_us : INT64_MIN;
|
||||
m->capture_epoch = timing ? timing->epoch : 0;
|
||||
m->codec_delay_samples = timing ? timing->delay_samples : 0;
|
||||
memcpy(buf + CALL_MEDIA_HDR_SIZE, opus, (size_t)len);
|
||||
size_t mlen = CALL_MEDIA_HDR_SIZE + (size_t)len;
|
||||
|
||||
/* entry: [svc_id][media-пакет] — кладём в tx_q (переполнение → дроп старейшего) */
|
||||
struct ll_entry* e = queue_entry_new(0);
|
||||
if (!e) return -1;
|
||||
e->dgram = u_malloc(1 + mlen);
|
||||
if (!e->dgram) { queue_entry_free(e); return -1; }
|
||||
e->dgram[0] = ETCP_RT_ID_CALL_AUDIO;
|
||||
memcpy(e->dgram + 1, buf, mlen);
|
||||
e->len = (uint16_t)(1 + mlen);
|
||||
|
||||
struct ll_entry* e = queue_entry_new(sizeof(struct call_audio_pending));
|
||||
if (!e) { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "audio frame metadata allocation failed"); return -1; }
|
||||
e->dgram = u_malloc(len);
|
||||
if (!e->dgram) { queue_entry_free(e); DEBUG_ERROR(DEBUG_CATEGORY_CALL, "audio frame allocation failed len=%d", len); return -1; }
|
||||
*(struct call_audio_pending*)e->data = (struct call_audio_pending){get_time_us(),
|
||||
timing ? *timing : (struct call_capture_time){INT64_MIN, 0, 0}, s->tx_media_seq++};
|
||||
memcpy(e->dgram, opus, len); e->len = len;
|
||||
if (queue_entry_count(s->tx_q) >= CALL_MAX_TX_FRAMES) {
|
||||
struct ll_entry* old = queue_data_get(s->tx_q);
|
||||
if (old) {
|
||||
queue_dgram_free(old); queue_entry_free(old); s->c_tx_dropped++;
|
||||
if (s->c_tx_dropped == 1 || s->c_tx_dropped % 50 == 0)
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CALL, "%s: TX overflow id=%016llx queued=%zu dropped=%u", CALL_ID,
|
||||
(unsigned long long)call_id, (size_t)queue_entry_count(s->tx_q), s->c_tx_dropped);
|
||||
}
|
||||
queue_dgram_free(old); queue_entry_free(old); s->c_tx_dropped++;
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CALL, "audio TX overflow call=%llx dropped=%u", (unsigned long long)call_id, s->c_tx_dropped);
|
||||
}
|
||||
queue_data_put(s->tx_q, e);
|
||||
|
||||
call_tx_flush(s);
|
||||
return 0;
|
||||
}
|
||||
|
||||
+7
-4
@@ -74,7 +74,7 @@ uint8_t call_phone_status(struct UTUN_INSTANCE* inst);
|
||||
// ── медиа (релей Opus-кадров, uasync-поток) ──
|
||||
|
||||
/* Отправить закодированный Opus-кадр пиру (оборачивает в CALL_MEDIA).
|
||||
* Только ACTIVE: len=1..988, opus/timing копируются до возврата; timing=NULL — без capture PTS.
|
||||
* Только ACTIVE: len=1..985, opus/timing копируются до возврата; timing=NULL — без capture PTS.
|
||||
* seq/ts выставляются автоматически. 0 = принят в локальную FIFO, -1 = неверные данные/нет ACTIVE/ошибка памяти.
|
||||
* Результат не подтверждает доставку: FIFO750 удаляет старейший кадр при заполнении, роутер может отказать при drain. */
|
||||
int call_send_media(struct UTUN_INSTANCE* inst, uint64_t call_id, const uint8_t* opus, int len,
|
||||
@@ -91,17 +91,20 @@ void call_set_media_recv_cb(struct UTUN_INSTANCE* inst, call_media_recv_fn fn, v
|
||||
|
||||
/* ── аудио-движок (опционально, GUI-сборки) ──
|
||||
* Сервисный аудио-контур (codec + джиттер-буфер + тоны) регистрируется через
|
||||
* call_set_audio_ops. call.c зовёт on_media на каждый принятый медиа-кадр (с seq/ts)
|
||||
* call_set_audio_ops. call.c зовёт on_media на каждый принятый медиа-кадр (с seq и временем подачи пачки в Router)
|
||||
* и get_stats для события CALL_STATS. Демон без аудио (headless) — ops не ставит. */
|
||||
|
||||
struct call_audio_ops {
|
||||
// uasync-поток: принятый Opus-кадр; opus/timing заимствованы до возврата. Декодирование определяет frontend.
|
||||
// uasync: Opus/PTS заимствованы; ts_ms — время пачки, packet_start=1 только у её первого кадра.
|
||||
void (*on_media)(struct UTUN_INSTANCE* inst, uint64_t call_id,
|
||||
uint16_t seq, uint32_t ts_ms, const uint8_t* opus, int len, const struct call_capture_time* timing);
|
||||
uint16_t seq, uint32_t ts_ms, const uint8_t* opus, int len, const struct call_capture_time* timing, int packet_start);
|
||||
/* uasync-поток: метрики (buffer_ms, tempo_x100=100 для 1x, накопительные dropped/underruns, range).
|
||||
* Выходы необязательны. 0 = снимок получен, -1 = нет активного DSP; реализация может потреблять события диагностики. */
|
||||
int (*get_stats)(uint64_t call_id, int* buffer_ms, int* tempo_x100,
|
||||
uint32_t* dropped, uint32_t* underruns, struct call_jitter_range* range);
|
||||
/* uasync: применить opus_codec_preset (0/1/2 = 16/32/64 кбит/с) к текущему и следующим звонкам.
|
||||
* Необязательный callback; 0 = применено, -1 = ошибка, прежний битрейт сохранён. */
|
||||
int (*set_codec_preset)(struct UTUN_INSTANCE* inst, int preset);
|
||||
};
|
||||
/* Только uasync: сохранить заимствованный ops у ядра (NULL снимает регистрацию), допустимо до запуска чата.
|
||||
* Объект живёт до снятия/уничтожения ядра. Stop/start чата сохраняет регистрацию, состояние звонка — нет. */
|
||||
|
||||
+36
-7
@@ -24,7 +24,6 @@
|
||||
#include <string.h>
|
||||
|
||||
#define CALL_AUDIO_ID "call_audio"
|
||||
#define CALL_AUDIO_BITRATE 32000
|
||||
#define CALL_STALL_SAMPLES (CALL_AUDIO_SAMPLE_RATE / 2) /* 500мс без PCM из jitter → глитч-тон */
|
||||
#define CALL_AEC_FILTER_SAMPLES 14400 /* хвост эха 300мс @48кГц */
|
||||
#define CALL_AEC_DELAY_FRAMES 2 /* desktop: минимальная задержка duplex */
|
||||
@@ -39,6 +38,7 @@ static int g_ended_started = 0; /* только аудио-
|
||||
static uint64_t g_call_id = 0;
|
||||
static int g_gap_samples = 0; /* только аудиопоток; насыщение на пороге */
|
||||
static opus_codec_encoder_t* g_encoder = NULL;
|
||||
static int g_codec_preset = 1; /* Снимок настройки из uasync; защищён g_mtx. */
|
||||
static opus_codec_decoder_t* g_decoder = NULL;
|
||||
static struct call_jitter* g_vj = NULL;
|
||||
static struct call_tones* g_tones = NULL;
|
||||
@@ -102,9 +102,32 @@ static void call_audio_send(void* arg) {
|
||||
if (send) call_send_media(tx->inst, tx->call_id, tx->opus, tx->len, &tx->timing);
|
||||
}
|
||||
|
||||
static int call_audio_set_codec_preset(struct UTUN_INSTANCE* inst, int preset) {
|
||||
if (preset < 0 || preset > 2) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: invalid codec preset=%d", CALL_AUDIO_ID, preset); return -1;
|
||||
}
|
||||
int bitrate = 16000 << preset;
|
||||
pthread_mutex_lock(&g_mtx);
|
||||
if (g_inst != inst) {
|
||||
pthread_mutex_unlock(&g_mtx);
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: codec setting for detached instance", CALL_AUDIO_ID); return -1;
|
||||
}
|
||||
if (g_encoder && opus_codec_encoder_bitrate_set(g_encoder, bitrate) < 0) {
|
||||
pthread_mutex_unlock(&g_mtx);
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: codec bitrate update failed preset=%d bitrate=%d", CALL_AUDIO_ID, preset, bitrate);
|
||||
return -1;
|
||||
}
|
||||
g_codec_preset = preset;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: codec preset=%d bitrate=%d active=%d call=%016llx", CALL_AUDIO_ID,
|
||||
preset, bitrate, g_active, (unsigned long long)g_call_id);
|
||||
pthread_mutex_unlock(&g_mtx);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static const struct call_audio_ops g_ops = {
|
||||
.on_media = call_audio_on_media,
|
||||
.get_stats = call_audio_get_stats,
|
||||
.set_codec_preset = call_audio_set_codec_preset,
|
||||
};
|
||||
|
||||
/* Создать AEC с текущей задержкой (объявление; определение ниже). */
|
||||
@@ -128,7 +151,7 @@ int call_audio_init(struct UTUN_INSTANCE* inst) {
|
||||
}
|
||||
call_set_audio_ops(inst, &g_ops);
|
||||
pthread_mutex_unlock(&g_mtx);
|
||||
return 0;
|
||||
return call_audio_set_codec_preset(inst, chat_setting_get_int(inst, "opus_codec_preset", 1));
|
||||
}
|
||||
|
||||
void call_audio_destroy(struct UTUN_INSTANCE* inst) {
|
||||
@@ -147,12 +170,12 @@ void call_audio_destroy(struct UTUN_INSTANCE* inst) {
|
||||
/* ── uasync-поток: приём медиа (кладём кодированный кадр в jitter-буфер) ── */
|
||||
|
||||
void call_audio_on_media(struct UTUN_INSTANCE* inst, uint64_t call_id,
|
||||
uint16_t seq, uint32_t ts_ms, const uint8_t* opus, int len, const struct call_capture_time* timing) {
|
||||
uint16_t seq, uint32_t ts_ms, const uint8_t* opus, int len, const struct call_capture_time* timing, int packet_start) {
|
||||
(void)seq;
|
||||
|
||||
pthread_mutex_lock(&g_mtx);
|
||||
if (inst == g_inst && g_active && call_id == g_call_id && g_vj && opus && len > 0) {
|
||||
call_jitter_push(g_vj, opus, len, ts_ms, (int64_t)(get_time_tb() / 10), timing);
|
||||
call_jitter_push(g_vj, opus, len, ts_ms, (int64_t)(get_time_tb() / 10), timing, packet_start);
|
||||
}
|
||||
pthread_mutex_unlock(&g_mtx);
|
||||
}
|
||||
@@ -190,7 +213,13 @@ static int call_audio_start_impl(uint64_t call_id, int prepared) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: encoder create failed", CALL_AUDIO_ID);
|
||||
return -1;
|
||||
}
|
||||
opus_codec_encoder_bitrate_set(enc, CALL_AUDIO_BITRATE);
|
||||
int preset = g_codec_preset, bitrate = 16000 << preset;
|
||||
if (opus_codec_encoder_bitrate_set(enc, bitrate) < 0) {
|
||||
opus_codec_encoder_destroy(enc);
|
||||
pthread_mutex_unlock(&g_mtx);
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: encoder bitrate failed preset=%d bitrate=%d", CALL_AUDIO_ID, preset, bitrate);
|
||||
return -1;
|
||||
}
|
||||
int codec_delay = 0;
|
||||
if (opus_codec_encoder_delay(enc, &codec_delay) < 0) {
|
||||
opus_codec_encoder_destroy(enc);
|
||||
@@ -277,8 +306,8 @@ static int call_audio_start_impl(uint64_t call_id, int prepared) {
|
||||
g_aec_reported_over = g_aec_reported_under = g_pcm_events = 0;
|
||||
pthread_mutex_unlock(&g_mtx);
|
||||
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: started call=0x%016llx aec=%d delay=%d compressor=%s gain=%d", CALL_AUDIO_ID,
|
||||
(unsigned long long)call_id, g_aec ? 1 : 0, g_aec_delay_frames,
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: started call=0x%016llx preset=%d bitrate=%d aec=%d delay=%d compressor=%s gain=%d", CALL_AUDIO_ID,
|
||||
(unsigned long long)call_id, preset, bitrate, g_aec ? 1 : 0, g_aec_delay_frames,
|
||||
ac ? (audio_compressor_is_enabled(ac) ? "on" : "off") : "unavailable",
|
||||
compressor_gain);
|
||||
return 0;
|
||||
|
||||
@@ -45,7 +45,8 @@ int call_audio_init(struct UTUN_INSTANCE* inst);
|
||||
* Поздний detach прежнего inst не затрагивает нового владельца. Codec/jitter освобождаются отдельно через stop/release. */
|
||||
void call_audio_destroy(struct UTUN_INSTANCE* inst);
|
||||
|
||||
/* GUI-поток: создать codec + jitter + tones. Старый API принимает raw PCM и использует core AEC.
|
||||
/* GUI-поток: создать codec + jitter + tones. Битрейт берётся из opus_codec_preset; runtime-set обновляет его без restart.
|
||||
* Старый API принимает raw PCM и использует core AEC.
|
||||
* Нужен привязанный через init живой inst; без него -1. Для desktop нужен start_prepared:
|
||||
* AEC и stereo reference принадлежат VoiceAudioIo. Повтор активного ID сохраняет DSP;
|
||||
* смена raw/prepared для него отклоняется. До смены ID остановить оба I/O-направления. 0 = успех, -1 = ошибка. */
|
||||
@@ -80,9 +81,9 @@ int call_audio_feed_pcm_muted(uint64_t call_id, const int16_t* pcm, int count, i
|
||||
int call_audio_pull_pcm(uint64_t call_id, int16_t* out, int max_samples);
|
||||
|
||||
/* uasync-поток (из call.c): кадр текущего inst/активного DSP передаётся в call_jitter_push;
|
||||
* данные копируются, декодирование произойдёт на pull в аудио-потоке. Чужой inst/ID или отсутствие DSP — no-op. */
|
||||
* данные копируются, декодирование на pull. ts_ms — время пачки; packet_start=1 только на первом кадре. Чужой inst/ID или отсутствие DSP — no-op. */
|
||||
void call_audio_on_media(struct UTUN_INSTANCE* inst, uint64_t call_id,
|
||||
uint16_t seq, uint32_t ts_ms, const uint8_t* opus, int len, const struct call_capture_time* timing);
|
||||
uint16_t seq, uint32_t ts_ms, const uint8_t* opus, int len, const struct call_capture_time* timing, int packet_start);
|
||||
|
||||
/* Playback worker: номер PCM-фрейма и время его вывода, epoch берётся из get_clock после start/reset.
|
||||
* UI/video: get_clock возвращает 0 для существующего звонка; valid внутри снимка может быть 0.
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
#include "call_audio_batch.h"
|
||||
#include "../routing_layer/etcp_router.h"
|
||||
#include "../../lib/debug_config.h"
|
||||
#include <string.h>
|
||||
|
||||
static int valid_header(const struct call_pkt_media *h) {
|
||||
return h->subcmd == CALL_SUBCMD_MEDIA && h->call_id && h->track_id == CALL_TRACK_AUDIO
|
||||
&& h->count && h->count <= CALL_AUDIO_BATCH_MAX && h->codec_delay_samples <= CALL_OPUS_SAMPLE_RATE;
|
||||
}
|
||||
static int valid_frame(const struct call_pkt_media *h, const struct call_pkt_audio_frame *f) {
|
||||
return f->length && f->length <= CALL_AUDIO_MAX_OPUS && (f->capture_pts_us == INT64_MIN
|
||||
|| (f->capture_pts_us >= 0 && f->capture_pts_us <= INT64_MAX / 2 && h->capture_epoch));
|
||||
}
|
||||
int call_audio_batch_encode(const struct call_audio_batch_view *batch, uint8_t *out, size_t capacity, size_t *length) {
|
||||
if (!batch || !out || !length || !valid_header(&batch->header)) goto invalid;
|
||||
size_t total = CALL_MEDIA_HDR_SIZE;
|
||||
for (unsigned i = 0; i < batch->header.count; i++) {
|
||||
if (!batch->frames[i].opus || !valid_frame(&batch->header, &batch->frames[i].header)) goto invalid;
|
||||
total += CALL_AUDIO_FRAME_HDR_SIZE + batch->frames[i].header.length;
|
||||
}
|
||||
if (total > capacity || total > CALL_MEDIA_MAX_BYTES) goto invalid;
|
||||
memcpy(out, &batch->header, CALL_MEDIA_HDR_SIZE);
|
||||
size_t offset = CALL_MEDIA_HDR_SIZE;
|
||||
for (unsigned i = 0; i < batch->header.count; i++) {
|
||||
const struct call_audio_frame_view *f = &batch->frames[i];
|
||||
memcpy(out + offset, &f->header, CALL_AUDIO_FRAME_HDR_SIZE); offset += CALL_AUDIO_FRAME_HDR_SIZE;
|
||||
memcpy(out + offset, f->opus, f->header.length); offset += f->header.length;
|
||||
}
|
||||
*length = total;
|
||||
return 0;
|
||||
invalid:
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CALL, "audio batch encode invalid fields/capacity=%zu", capacity); return -1;
|
||||
}
|
||||
int call_audio_batch_parse(const uint8_t *wire, size_t length, struct call_audio_batch_view *out) {
|
||||
if (!wire || !out || length < CALL_MEDIA_HDR_SIZE || length > CALL_MEDIA_MAX_BYTES) goto invalid;
|
||||
struct call_audio_batch_view result = {0};
|
||||
memcpy(&result.header, wire, CALL_MEDIA_HDR_SIZE);
|
||||
if (!valid_header(&result.header)) goto invalid;
|
||||
size_t offset = CALL_MEDIA_HDR_SIZE;
|
||||
for (unsigned i = 0; i < result.header.count; i++) {
|
||||
struct call_audio_frame_view *f = &result.frames[i];
|
||||
if (length - offset < CALL_AUDIO_FRAME_HDR_SIZE) goto invalid;
|
||||
memcpy(&f->header, wire + offset, CALL_AUDIO_FRAME_HDR_SIZE); offset += CALL_AUDIO_FRAME_HDR_SIZE;
|
||||
if (!valid_frame(&result.header, &f->header) || f->header.length > length - offset) goto invalid;
|
||||
f->opus = wire + offset; offset += f->header.length;
|
||||
}
|
||||
if (offset != length) goto invalid;
|
||||
*out = result;
|
||||
return 0;
|
||||
invalid:
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CALL, "audio batch RX malformed bytes=%zu", length); return -1;
|
||||
}
|
||||
|
||||
void call_audio_batch_init(struct call_audio_batch_control *c, uint64_t now) {
|
||||
if (!c) { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "audio batch reset without state"); return; }
|
||||
*c = (struct call_audio_batch_control){.target = 4, .hold_us = 60000, .last_us = now};
|
||||
}
|
||||
int call_audio_batch_step(struct call_audio_batch_control *c, uint64_t now, const struct ETCP_ROUTER_TX_STATS *net) {
|
||||
if (!c || !c->target || now < c->last_us || (net && net->last_ack_us > now)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "audio batch invalid control sample time=%llu", (unsigned long long)now); return -1;
|
||||
}
|
||||
unsigned previous = c->target;
|
||||
if ((!net && (c->local_epoch || c->peer_epoch || c->feedback_received))
|
||||
|| (net && (net->local_epoch != c->local_epoch || net->peer_epoch != c->peer_epoch))) {
|
||||
call_audio_batch_init(c, now);
|
||||
if (net) { c->local_epoch = net->local_epoch; c->peer_epoch = net->peer_epoch; }
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "audio batch reset router epoch=%llx/%llx previous=%u",
|
||||
(unsigned long long)c->local_epoch, (unsigned long long)c->peer_epoch, previous);
|
||||
}
|
||||
c->last_us = now;
|
||||
c->delay_us = net ? net->delivery_rtt_us : 0;
|
||||
if (net && net->queued_age_us > c->delay_us) c->delay_us = net->queued_age_us;
|
||||
if (net && net->inflight_age_us > c->delay_us) c->delay_us = net->inflight_age_us;
|
||||
uint64_t interval = net && net->delivery_rtt_us > 250000 ? net->delivery_rtt_us * 2 : 500000;
|
||||
uint64_t fresh_us = interval > 2000000 ? interval : 2000000;
|
||||
int measured = net && net->ready && !net->no_route && !net->receive_busy && !net->probing
|
||||
&& net->acked_packets && now - net->last_ack_us <= fresh_us;
|
||||
if (measured) c->feedback_received = 1;
|
||||
int usable = measured && net->window.valid;
|
||||
const char *reason = "hold";
|
||||
if (!usable) { c->high = c->low = 0; reason = "unavailable"; }
|
||||
else if (c->delay_us > 100000) {
|
||||
c->low = 0;
|
||||
if (!c->high) { c->high = 1; c->high_since_us = now; }
|
||||
if (c->target < CALL_AUDIO_BATCH_MAX && now >= c->next_change_us && now - c->high_since_us >= 200000) {
|
||||
c->target++; c->next_change_us = now + interval; reason = "enqueue_ack_delay";
|
||||
}
|
||||
} else if (c->delay_us < 40000 && !net->queued_packets) {
|
||||
c->high = 0;
|
||||
if (!c->low) { c->low = 1; c->low_since_us = now; }
|
||||
if (c->target > 1 && now >= c->next_change_us && now - c->low_since_us >= 2000000) {
|
||||
c->target--; c->next_change_us = now + interval; c->low_since_us = now; reason = "recovered";
|
||||
}
|
||||
} else c->high = c->low = 0;
|
||||
/* Даже при медленном снижении target быстрый канал сразу перестаёт ждать накопления. */
|
||||
unsigned budget = c->delay_us > 40000 ? (unsigned)((c->delay_us - 40000) / 4 > 40000 ? 40000 : (c->delay_us - 40000) / 4) : 0;
|
||||
unsigned target_wait = (c->target - 1) * 20000;
|
||||
int starting = !c->feedback_received && (!net || (!net->no_route && !net->receive_busy && !net->probing));
|
||||
c->hold_us = measured ? (target_wait < budget ? target_wait : budget) : (starting ? 60000 : 0);
|
||||
if (c->target != previous) DEBUG_INFO(DEBUG_CATEGORY_CALL,
|
||||
"audio batch target=%u->%u reason=%s delay_us=%llu router_rtt_us=%llu queued_us=%llu flight_us=%llu hold_us=%u",
|
||||
previous, c->target, reason, (unsigned long long)c->delay_us, (unsigned long long)(net ? net->delivery_rtt_us : 0),
|
||||
(unsigned long long)(net ? net->queued_age_us : 0), (unsigned long long)(net ? net->inflight_age_us : 0), c->hold_us);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/* Упаковка Opus и регулятор пачек звонка. Только uasync; wire views заимствуют входной буфер.
|
||||
* Контроллер использует полный Router enqueue->ACK, не вычитает базовый RTT.
|
||||
* Владелец снимает статистику каждые 50мс, ограничивает ожидание пачки hold_us и отменяет свои таймеры. */
|
||||
#ifndef CALL_AUDIO_BATCH_H
|
||||
#define CALL_AUDIO_BATCH_H
|
||||
#include "call_proto.h"
|
||||
#include <stddef.h>
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
struct ETCP_ROUTER_TX_STATS;
|
||||
struct call_audio_frame_view {
|
||||
struct call_pkt_audio_frame header; /* Исходные seq/PTS/length; payload не копируется. */
|
||||
const uint8_t *opus; /* Не менее length байт до конца encode/потребления view. */
|
||||
};
|
||||
struct call_audio_batch_view {
|
||||
struct call_pkt_media header; /* Общие поля; count определяет число frames. */
|
||||
struct call_audio_frame_view frames[CALL_AUDIO_BATCH_MAX];
|
||||
};
|
||||
struct call_audio_batch_control {
|
||||
uint64_t local_epoch, peer_epoch, last_us, high_since_us, low_since_us, next_change_us;
|
||||
uint64_t delay_us; /* max полного RTT и возраста Router-очередей; собственное накопление сюда не входит. */
|
||||
unsigned target, hold_us; /* 1..10 кадров; старт4/60000мкс, после измерения 0..40000мкс. */
|
||||
int feedback_received; /* Есть первый пригодный ACK в текущей Router epoch. */
|
||||
int high, low;
|
||||
};
|
||||
/* Encode/parse: 0/-1. Проверяют весь пакет до изменения out/length/view; ошибки логируются.
|
||||
* Encode не допускает перекрытие output с Opus; capacity >= вычисленного размера, максимум 1024. */
|
||||
int call_audio_batch_encode(const struct call_audio_batch_view *batch, uint8_t *out, size_t capacity, size_t *length);
|
||||
int call_audio_batch_parse(const uint8_t *wire, size_t length, struct call_audio_batch_view *out);
|
||||
/* Начать с четырёх кадров и deadline60мс до первого измерения ACK. Reset нужен на каждый новый звонок. */
|
||||
void call_audio_batch_init(struct call_audio_batch_control *control, uint64_t now_us);
|
||||
/* Снимок NULL — канал пока отсутствует. Смена эпохи сбрасывает историю; 0/-1.
|
||||
* BLOCKED/нет свежих ACK: рост запрещён. Значения порогов и причин смены пишет DEBUG_CATEGORY_CALL. */
|
||||
int call_audio_batch_step(struct call_audio_batch_control *control, uint64_t now_us, const struct ETCP_ROUTER_TX_STATS *net);
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
@@ -38,7 +38,7 @@ struct call_jitter {
|
||||
CallDelay delay{jitter_decode, this, [](void* context, const soundtouch::SourceSpan& span) {
|
||||
static_cast<call_jitter*>(context)->timeline.source(span);
|
||||
}, this};
|
||||
bool primed = false;
|
||||
bool primed = false, playback_started = false;
|
||||
int budget = 0, attempts = 0, error_result = 0, error_len = 0;
|
||||
uint32_t decode_errors = 0;
|
||||
};
|
||||
@@ -100,8 +100,8 @@ struct call_jitter* call_jitter_create(call_jitter_decode_fn decode, void* arg,
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "call_jitter: delay processor unavailable");
|
||||
j->~call_jitter(); u_free(j); return nullptr;
|
||||
}
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "call_jitter: created capacity=%dms reserve=%d..%dms adaptive_pcm=1",
|
||||
CALL_JITTER_MAX_FRAMES * CALL_JITTER_FRAME_MS, CALL_JITTER_MIN_RESERVE_MS, max_reserve_ms);
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "call_jitter: created capacity=%dms reserve=%d..%dms startup=%dms adaptive_pcm=1",
|
||||
CALL_JITTER_MAX_FRAMES * CALL_JITTER_FRAME_MS, CALL_JITTER_MIN_RESERVE_MS, max_reserve_ms, CALL_JITTER_STARTUP_MS);
|
||||
return j;
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@ void call_jitter_destroy(struct call_jitter* j) {
|
||||
}
|
||||
|
||||
void call_jitter_push(struct call_jitter* j, const uint8_t* enc, int len, uint32_t sent_ms, int64_t received_ms,
|
||||
const struct call_capture_time* timing) {
|
||||
const struct call_capture_time* timing, int packet_start) {
|
||||
if (!j || !enc || len <= 0 || len > kMaxEncoded || received_ms < 0) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CALL, "call_jitter: invalid frame len=%d received=%lldms", len, (long long)received_ms);
|
||||
return;
|
||||
@@ -122,7 +122,7 @@ void call_jitter_push(struct call_jitter* j, const uint8_t* enc, int len, uint32
|
||||
(long long)j->arrival_window.last_received_ms, (long long)received_ms);
|
||||
return;
|
||||
}
|
||||
j->arrival_window.observe_arrival(received_ms, sent_ms);
|
||||
if (packet_start) j->arrival_window.observe_arrival(received_ms, sent_ms);
|
||||
j->range.reserve_ms = j->arrival_window.reserve;
|
||||
if (j->count == CALL_JITTER_MAX_FRAMES) {
|
||||
j->head = (j->head + 1) % CALL_JITTER_MAX_FRAMES; j->count--; j->dropped++;
|
||||
@@ -148,7 +148,9 @@ int call_jitter_pull(struct call_jitter* j, int16_t* out, int max_samples) {
|
||||
}
|
||||
if (!j->primed) {
|
||||
// После первого полного блока остаётся сетевой запас, не скрытый запас DSP.
|
||||
if (depth_samples < reserve * 48 + max_samples) {
|
||||
int required = reserve * 48 + max_samples;
|
||||
if (!j->playback_started) required = std::max(required, CALL_JITTER_STARTUP_MS * 48);
|
||||
if (depth_samples < required) {
|
||||
std::lock_guard<std::mutex> lock(j->mtx);
|
||||
j->diagnostics.depth_ms = (int)depth; j->diagnostics.reserve_ms = reserve;
|
||||
j->diagnostics.tempo_x100 = j->tempo_x100; j->diagnostics.waiting = 1;
|
||||
@@ -162,6 +164,11 @@ int call_jitter_pull(struct call_jitter* j, int16_t* out, int max_samples) {
|
||||
int n = (int)j->delay.pull(out, (unsigned)max_samples, count * kFrameSamples, reserve * 48);
|
||||
auto state = j->delay.stats();
|
||||
j->primed = n == max_samples;
|
||||
if (n > 0 && !j->playback_started) {
|
||||
j->playback_started = true;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "call_jitter: startup complete buffered_ms=%.1f target_ms=%d reserve_ms=%d pcm=%d",
|
||||
depth, CALL_JITTER_STARTUP_MS, reserve, n);
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(j->mtx);
|
||||
j->timeline.read((unsigned)n);
|
||||
|
||||
@@ -10,12 +10,13 @@ extern "C" {
|
||||
|
||||
/* Звонок: FIFO Opus, запас 20мс..max_reserve_ms по изменению задержки доставки за 10 секунд и догон после паузы устройства.
|
||||
* push — сетевой поток, pull/decode/CallDelay — единственный аудиопоток.
|
||||
* Старт/восстановление ждут запас плюс запрошенный блок; при выдаче PCM запас не резервируется повторно.
|
||||
* Первый старт ждёт не менее200мс; восстановление — сетевой запас плюс блок; при выдаче PCM запас не резервируется повторно.
|
||||
* destroy — после остановки всех обращений. get_stats/diagnostics/clock — любой поток,
|
||||
* внутренний mutex защищает их снимки. Все операции playback (включая silence/reset)
|
||||
* сериализует владелец; mutex не делает допустимыми два одновременных pull. */
|
||||
#define CALL_JITTER_FRAME_MS 20
|
||||
#define CALL_JITTER_MIN_RESERVE_MS 20
|
||||
#define CALL_JITTER_STARTUP_MS 200 /* Минимальная накопленная речь перед первым декодированием/выводом. */
|
||||
#define CALL_JITTER_MAX_FRAMES 750 /* 10с перебоя + 5с запаса на накопление и восстановление */
|
||||
/* Заимствованный context, encoded/len и выход/capacity (сэмплы, до 960); вернуть 1..capacity или <=0 при ошибке.
|
||||
* Callback вызывается синхронно на pull без сетевого mutex; encoded действителен только до возврата. */
|
||||
@@ -44,13 +45,15 @@ void call_jitter_destroy(struct call_jitter* j);
|
||||
* Абсолютное смещение часов пиров не влияет на оценку джиттера. enc (1..512 байт) и timing копируются;
|
||||
* timing=NULL означает отсутствие PTS. FIFO сохраняет порядок push, сортировки/дедупликации нет.
|
||||
* При заполнении удаляется старейший кадр; неверные аргументы/регрессия received_ms отклоняются с WARN. */
|
||||
/* packet_start=1: учитывать arrival/sent_ms как один сетевой пакет; остальные кадры пачки не меняют jitter. */
|
||||
void call_jitter_push(struct call_jitter* j, const uint8_t* enc, int len, uint32_t sent_ms, int64_t received_ms,
|
||||
const struct call_capture_time* timing);
|
||||
const struct call_capture_time* timing, int packet_start);
|
||||
/* Только после остановки playback-операций: удалить PCM/DSP, увеличить локальную epoch,
|
||||
* оставить последние ceil(reserve/20) Opus-кадров. Оценка доставки и суммарные dropped/underruns сохраняются. */
|
||||
void call_jitter_reset(struct call_jitter* j);
|
||||
/* out — max_samples mono int16 @48кГц; вернуть 0..max_samples, без дополнения тишиной.
|
||||
* Старт/восстановление ждут reserve плюс размер запроса. Decode ограничен min(750, 8+max_samples/480) попытками.
|
||||
* Первый старт ждёт max(200мс, reserve плюс размер запроса); восстановление — reserve плюс размер запроса.
|
||||
* Decode ограничен min(750, 8+max_samples/480) попытками.
|
||||
* NULL/неположительный размер дают 0. Вызывающий учитывает дополненную локальную тишину через silence. */
|
||||
int call_jitter_pull(struct call_jitter* j, int16_t* out, int max_samples);
|
||||
/* Playback worker: учесть только дополненные после pull локальные сэмплы; их PTS удерживается. */
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#ifndef CALL_PRIV_H
|
||||
#define CALL_PRIV_H
|
||||
#include "call.h"
|
||||
#include "call_audio_batch.h"
|
||||
#include "../../lib/ll_queue.h"
|
||||
|
||||
/* IDLE — только создание; RINGING бывает у обеих ролей, WAIT_DECISION — ожидание force
|
||||
@@ -33,7 +34,11 @@ struct call_session {
|
||||
uint8_t connection_reported, connection_type; /* Было событие CONNECTION / последний router path.type (0..3). */
|
||||
uint64_t connection_next_hop, last_media_tb; /* Кэш next hop и время последнего RX; watchdog использует traffic_timer. */
|
||||
uint16_t tx_media_seq; /* Следующий sequence аудиопакета с wrap uint16; расходуется до выделения TX entry. */
|
||||
struct ll_queue *tx_q; /* Собственная FIFO готовых audio packets [svc][media]; до 750, переполнение удаляет голову. */
|
||||
struct call_audio_batch_control audio_batch; /* Только uasync; reset при смене Router epoch. */
|
||||
void *audio_batch_timer, *audio_flush_timer; /* Наблюдение 50мс и deadline неполной пачки. */
|
||||
uint64_t audio_report_us;
|
||||
uint32_t tx_batches, rx_batches; /* Число Router-кодограмм, отдельно от аудиокадров. */
|
||||
struct ll_queue *tx_q; /* FIFO Opus с локальными seq/PTS/created_us в entry->data; до 750, overflow удаляет голову. */
|
||||
struct queue_waiter_handle send_waiter; /* Отменяемое одноразовое ожидание места в router audio send_q. */
|
||||
uint32_t c_tx_frames, c_rx_frames, c_tx_dropped, c_rx_dropped; /* TX принят router / валидный RX / TX overflow / RX не ведётся. */
|
||||
uint64_t tx_wait_total_ms; /* Сумма времени audio enqueue->drain, включая отказ router; сброс при отчёте stats. */
|
||||
|
||||
+19
-10
@@ -76,21 +76,30 @@ struct call_pkt_cancel { uint8_t subcmd; uint64_t call_id; };
|
||||
struct call_pkt_hangup { uint8_t subcmd; uint64_t call_id; uint8_t reason; };
|
||||
struct call_pkt_end_ack { uint8_t subcmd; uint64_t call_id; };
|
||||
|
||||
/* Одна Router-кодограмма содержит 1..10 Opus-кадров одной capture epoch.
|
||||
* После header: повторяются [call_pkt_audio_frame][opus length байт]. Без старого формата. */
|
||||
struct call_pkt_media {
|
||||
uint8_t subcmd; /* CALL_SUBCMD_MEDIA */
|
||||
uint64_t call_id; /* ID существующего ACTIVE-звонка; peer/group проверяет call.c. */
|
||||
uint8_t track_id; /* CALL_TRACK_AUDIO. */
|
||||
uint16_t seq; /* Счётчик отправителя с переполнением; call_audio FIFO не сортирует по seq. */
|
||||
uint32_t ts_ms; /* Монотонное время постановки в TX FIFO, миллисекунды modulo 2^32. */
|
||||
int64_t capture_pts_us; /* Монотонный PTS первого входного сэмпла; INT64_MIN — часы неизвестны. */
|
||||
uint64_t capture_epoch; /* Ненулевая эпоха capture при известном PTS; смена означает разрыв. */
|
||||
uint32_t codec_delay_samples; /* Opus lookahead при48кГц, не более48000. */
|
||||
/* Далее непустой Opus payload; call_send_media ограничивает общий media-пакет1024 байтами. */
|
||||
uint8_t subcmd; /* CALL_SUBCMD_MEDIA */
|
||||
uint64_t call_id; /* ACTIVE-звонок; peer/group проверяет call.c. */
|
||||
uint8_t track_id; /* CALL_TRACK_AUDIO */
|
||||
uint64_t capture_epoch; /* Общая эпоха источника; с известным PTS ненулевая. */
|
||||
uint32_t codec_delay_samples; /* Opus lookahead при 48кГц. */
|
||||
uint32_t sent_ms; /* Монотонное время подачи пачки в Router; одно измерение jitter на пачку. */
|
||||
uint8_t count; /* 1..CALL_AUDIO_BATCH_MAX */
|
||||
};
|
||||
struct call_pkt_audio_frame {
|
||||
uint16_t seq; /* Исходный номер кадра, допускает пропуски/переполнение. */
|
||||
int64_t capture_pts_us; /* PTS первого сэмпла; INT64_MIN при отсутствии. */
|
||||
uint16_t length; /* Непустой Opus, полностью помещается в кодограмме. */
|
||||
};
|
||||
|
||||
#pragma pack(pop)
|
||||
|
||||
#define CALL_MEDIA_HDR_SIZE sizeof(struct call_pkt_media) /* 36 */
|
||||
#define CALL_MEDIA_HDR_SIZE sizeof(struct call_pkt_media) /* 27 */
|
||||
#define CALL_AUDIO_FRAME_HDR_SIZE sizeof(struct call_pkt_audio_frame) /* 12 */
|
||||
#define CALL_AUDIO_BATCH_MAX 10u
|
||||
#define CALL_MEDIA_MAX_BYTES 1024u
|
||||
#define CALL_AUDIO_MAX_OPUS (CALL_MEDIA_MAX_BYTES - CALL_MEDIA_HDR_SIZE - CALL_AUDIO_FRAME_HDR_SIZE)
|
||||
|
||||
/* ── параметры аудио (Opus 48kHz mono, 20ms) ── */
|
||||
#define CALL_OPUS_SAMPLE_RATE 48000
|
||||
|
||||
@@ -307,7 +307,8 @@ struct chat_node_arg { struct UTUN_INSTANCE* inst; uint64_t node_id; };
|
||||
|
||||
/* Полные метрики одного соединения, вызывается в uasync. arg — u_malloc struct chat_node_arg,
|
||||
* trampoline освобождает arg; inst остаётся заимствованным. CHAT_EVT_CONN_METRICS содержит UTF-8
|
||||
* без завершающего NUL в длине; буфер растёт по числу линков, при OOM нет частичного события.
|
||||
* без завершающего NUL в длине; включает очереди Router для пира/TX/RX-пути и transit, очереди normalizer.
|
||||
* Буфер растёт по числу линков и router-сессий, при OOM нет частичного события.
|
||||
* Данные ответа заимствованы только на время синхронного event callback. */
|
||||
void chat_core_collect_conn_metrics_trampoline(void* arg);
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ static const struct chat_setting_def g_setting_defs[] = {
|
||||
{"stream_subblocks", CHAT_SETTING_INT, 8, 1, 255},
|
||||
{"stream_rs_redundant", CHAT_SETTING_INT, 0, 0, 254},
|
||||
{"storage_autoload", CHAT_SETTING_BOOL, 1, 0, 1},
|
||||
{"opus_codec_preset", CHAT_SETTING_INT, 1, 0, 4},
|
||||
{"opus_codec_preset", CHAT_SETTING_INT, 1, 0, 2},
|
||||
{"radio_opus_mode", CHAT_SETTING_INT, 0, 0, 1},
|
||||
{"radio_opus_bitrate", CHAT_SETTING_INT, 32, 16, 128},
|
||||
{"radio_capture_stereo", CHAT_SETTING_INT, 0, 0, 1},
|
||||
@@ -210,6 +210,10 @@ int chat_setting_set(struct UTUN_INSTANCE* inst, const char* name, const char* v
|
||||
struct chat_setting_state* s = cs_state(inst);
|
||||
if (!s) { DEBUG_ERROR(CS_ID, "chat_setting: instance is NULL"); return -2; }
|
||||
int result = chat_setting_set_state(s, name, value);
|
||||
if (!result && !strcmp(name, "opus_codec_preset") && inst->call_audio_ops && inst->call_audio_ops->set_codec_preset &&
|
||||
inst->call_audio_ops->set_codec_preset(inst, chat_setting_get_int(inst, name, 1))) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "chat_setting: '%s' stored but codec apply failed; retry setting to reapply", name); return -3;
|
||||
}
|
||||
if (!result && !strcmp(name, "incoming_call_policy"))
|
||||
call_set_phone_state(inst, inst->phone_state, (unsigned)chat_setting_get_int(inst, name, CALL_POLICY_PUBLISH_PHONE));
|
||||
if (!result && (!strcmp(name, "local_invites_enabled") || !strcmp(name, "local_broadcasts_enabled")) &&
|
||||
|
||||
@@ -52,8 +52,8 @@ int chat_setting_get_int_state(const struct chat_setting_state* s, const char*
|
||||
/* ─── instance-уровень (runtime) ─── */
|
||||
|
||||
/* Те же коды; NULL inst — -2. Большинство потребителей читают значение при следующем обращении/старте.
|
||||
* incoming_call_policy сразу обновляет телефонную политику, local_* — LAN permissions.
|
||||
* -3 — значение сохранено, применение LAN не удалось; повтор set того же значения повторяет apply. */
|
||||
* incoming_call_policy сразу обновляет телефонную политику, local_* — LAN permissions, opus_codec_preset — битрейт звонка.
|
||||
* -3 — значение сохранено, применение не удалось; повтор set того же значения повторяет apply. */
|
||||
int chat_setting_set(struct UTUN_INSTANCE* inst, const char* name, const char* value);
|
||||
/* Instance-варианты чтения с теми же lifetime/default; NULL inst даёт NULL/def. */
|
||||
const char* chat_setting_get(struct UTUN_INSTANCE* inst, const char* name);
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#include "../routing_layer/topo_node.h"
|
||||
#include "../routing_layer/topo_node_sqlite.h"
|
||||
#include "../routing_layer/topo_group.h"
|
||||
#include "../routing_layer/etcp_router.h"
|
||||
#include <string.h>
|
||||
|
||||
struct address_profile_text { char text[80]; };
|
||||
@@ -220,6 +221,65 @@ static void collect_conn_list(struct UTUN_INSTANCE* inst) {
|
||||
}
|
||||
|
||||
/* ── Полный текстовый дамп одного соединения ── */
|
||||
/* Читаемые имена основных сервисов; числовой svc выводится для каждого, включая неизвестные. */
|
||||
static const char* router_service_name(uint8_t svc) {
|
||||
switch (svc) {
|
||||
case ETCP_RT_ID_CALL_AUDIO: return "call audio";
|
||||
case ETCP_RT_ID_CALL: return "call signaling";
|
||||
case ETCP_RT_ID_CALL_VIDEO: return "call video";
|
||||
case ETCP_RT_ID_CALL_VIDEO_CONTROL: return "video control";
|
||||
case ETCP_RT_ID_DM: return "DM";
|
||||
case ETCP_RT_ID_DM_MEDIA: return "DM media";
|
||||
case ETCP_RT_ID_FILE_TRANSFER: return "file transfer";
|
||||
case ETCP_RT_ID_MEMBER_FILES: return "member files";
|
||||
case ETCP_RT_ID_CONN_MGR: return "connection manager";
|
||||
default: return "service";
|
||||
}
|
||||
}
|
||||
|
||||
/* Только чтение в uasync: сессии выбранного пира, текущего TX-пути или последнего RX-пути и его transit FIFO.
|
||||
* Байты — счётчики очередей, не wire size. Ошибка форматирования возвращается сборщику для общего ERROR. */
|
||||
static int append_router_metrics(struct strbuf* text, struct UTUN_INSTANCE* inst, struct ETCP_CONN* conn) {
|
||||
if (strbuf_addf(text, "\n--- Router queues ---\n"
|
||||
"Scope: peer / current TX hop / last RX hop\n"
|
||||
"send_q: before ETCP; wait_ack: service ACK pending (may still be queued in ETCP)\n") < 0) return -1;
|
||||
unsigned sessions = 0;
|
||||
for (struct ll_entry* e = inst->router_conns ? inst->router_conns->head : NULL; e; e = e->next) {
|
||||
struct ETCP_ROUTER_CONN* rc = (struct ETCP_ROUTER_CONN*)e;
|
||||
struct ETCP_ROUTER_PATH path = etcp_router_get_path(inst, rc->group_id, rc->remote_node_id, rc->svc_id);
|
||||
if (rc->remote_node_id != conn->peer_node_id && rc->recv_conn != conn && path.next_hop_node_id != conn->peer_node_id) continue;
|
||||
sessions++;
|
||||
if (strbuf_addf(text, "\ngroup=%016llX peer=%016llX svc=%u (%s)\n"
|
||||
"ready=%u no_route=%u send_blocked=%u peer_busy=%u recv_blocked=%u transport_wait=%u\n"
|
||||
"RTT=%.1fms inflight_limit=%u retrans=%u\n",
|
||||
(unsigned long long)rc->group_id, (unsigned long long)rc->remote_node_id, rc->svc_id, router_service_name(rc->svc_id),
|
||||
rc->peer_reset_id != 0, rc->no_route, rc->send_blocked, rc->peer_receive_blocked, rc->receive_blocked,
|
||||
rc->send_waiter_q != NULL, rc->rtt / 10.0, rc->inflight_limit, rc->c_retrans_done) < 0) return -1;
|
||||
struct ll_queue* queues[] = {rc->send_q, rc->inflight_q, rc->incoming_q, rc->recv_q};
|
||||
const char* names[] = {"send_q", "wait_ack", "incoming_q", "recv_q"};
|
||||
for (unsigned i = 0; i < 4; i++) {
|
||||
if (strbuf_addf(text, "%s: %zuB/%dp%s", names[i], queues[i] ? queue_total_bytes(queues[i]) : 0,
|
||||
queue_entry_count(queues[i]), i % 2 ? "\n" : " ") < 0) return -1;
|
||||
}
|
||||
}
|
||||
if (strbuf_addf(text, "sessions: %u\n", sessions) < 0) return -1;
|
||||
size_t transit_bytes = 0;
|
||||
unsigned transit_packets = 0;
|
||||
for (struct ll_entry* e = conn->transit_queues ? conn->transit_queues->head : NULL; e; e = e->next) {
|
||||
struct TRANSIT_QUEUE* tq = (struct TRANSIT_QUEUE*)e;
|
||||
size_t bytes = tq->q ? queue_total_bytes(tq->q) : 0;
|
||||
unsigned packets = queue_entry_count(tq->q);
|
||||
transit_bytes += bytes; transit_packets += packets;
|
||||
if (strbuf_addf(text, "transit group=%016llX src=%016llX dst=%016llX: %zuB/%up\n",
|
||||
(unsigned long long)tq->group_id, (unsigned long long)tq->src_node_id,
|
||||
(unsigned long long)tq->dst_node_id, bytes, packets) < 0) return -1;
|
||||
}
|
||||
if (strbuf_addf(text, "transit total: %zuB/%up\n", transit_bytes, transit_packets) < 0) return -1;
|
||||
DEBUG_TRACE(DEBUG_CATEGORY_CHAT, "connection router metrics: peer=%016llX sessions=%u transit=%zuB/%up",
|
||||
(unsigned long long)conn->peer_node_id, sessions, transit_bytes, transit_packets);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void collect_conn_metrics(struct UTUN_INSTANCE* inst, uint64_t peer_node_id) {
|
||||
struct strbuf text = strbuf_new();
|
||||
|
||||
@@ -294,6 +354,8 @@ static void collect_conn_metrics(struct UTUN_INSTANCE* inst, uint64_t peer_node_
|
||||
if (conn->output_queue && strbuf_addf(&text, "out_q: %uB/%dp\n",
|
||||
(unsigned)queue_total_bytes(conn->output_queue), queue_entry_count(conn->output_queue)) < 0) goto failed;
|
||||
|
||||
if (append_router_metrics(&text, inst, conn) < 0) goto failed;
|
||||
|
||||
if (strbuf_addf(&text, "\n--- ACK Debug ---\n"
|
||||
"hit_inf=%u hit_sndq=%u miss=%u link_wait=%u\n",
|
||||
conn->cnt_ack_hit_inf, conn->cnt_ack_hit_sndq, conn->cnt_ack_miss, conn->cnt_link_wait) < 0) goto failed;
|
||||
@@ -303,8 +365,11 @@ static void collect_conn_metrics(struct UTUN_INSTANCE* inst, uint64_t peer_node_
|
||||
struct PKTNORM* pn = conn->normalizer;
|
||||
if (strbuf_addf(&text,
|
||||
"\n--- Normalizer ---\n"
|
||||
"input_q: %zuB/%dp output_q: %zuB/%dp\n"
|
||||
"in: %llu pkts / %llu B out: %llu pkts / %llu B\n"
|
||||
"frag_size=%u data_ptr=%u data_size=%u alloc_err=%u logic_err=%u\n",
|
||||
pn->input ? queue_total_bytes(pn->input) : 0, queue_entry_count(pn->input),
|
||||
pn->output ? queue_total_bytes(pn->output) : 0, queue_entry_count(pn->output),
|
||||
(unsigned long long)pn->in_total_pkts, (unsigned long long)pn->in_total_bytes,
|
||||
(unsigned long long)pn->out_total_pkts, (unsigned long long)pn->out_total_bytes,
|
||||
pn->frag_size, pn->data_ptr, pn->data_size, pn->alloc_errors, pn->logic_errors) < 0) goto failed;
|
||||
|
||||
@@ -85,6 +85,11 @@ static int router_path_append(struct ll_entry* e, uint64_t node_id, uint8_t coun
|
||||
|
||||
static int router_source_send(struct ETCP_CONN* conn, struct ll_entry* e) {
|
||||
if (router_path_append(e, conn->instance->node_id, 0) < 0) return -1;
|
||||
const struct SVC_ROUTE_HDR* h = (const struct SVC_ROUTE_HDR*)e->dgram;
|
||||
DEBUG_TRACE(DEBUG_CATEGORY_TRAFFIC, "route TX conn=%s svc=%u flags=%02x seq=%u len=%u src=%016llx dst=%016llx group=%016llx epoch=%016llx",
|
||||
conn->log_name, h->svc_id, h->flags, h->seq, e->len,
|
||||
(unsigned long long)h->src_node_id, (unsigned long long)h->dst_node_id,
|
||||
(unsigned long long)h->group_id, (unsigned long long)h->reset_id);
|
||||
return etcp_send(conn, e);
|
||||
}
|
||||
|
||||
@@ -334,6 +339,9 @@ struct ETCP_ROUTER_PATH etcp_router_get_path(struct UTUN_INSTANCE* inst, uint64_
|
||||
return path;
|
||||
}
|
||||
|
||||
/* Локальные метаданные pending; время enqueue переживает переход в inflight и повторы. */
|
||||
struct router_pending { uint64_t enqueued_us; uint32_t seq; uint8_t mode; };
|
||||
|
||||
// Собрать и закодировать (подпись/шифрование) пакет в начале отправки.
|
||||
// Возвращает финальный wire-пакет [SVC_ROUTE_HDR][payload][sig?] (u_malloc).
|
||||
static int router_build_packet(struct ETCP_ROUTER_CONN* rconn, struct ll_entry* pending,
|
||||
@@ -346,13 +354,13 @@ static int router_build_packet(struct ETCP_ROUTER_CONN* rconn, struct ll_entry*
|
||||
hdr->group_id = rconn->group_id;
|
||||
hdr->dst_node_id = rconn->remote_node_id;
|
||||
hdr->src_node_id = rconn->inst->node_id;
|
||||
memcpy(&hdr->seq, pending->data, 4);
|
||||
hdr->seq = ((struct router_pending*)pending->data)->seq;
|
||||
hdr->svc_id = rconn->svc_id;
|
||||
hdr->reset_id = rconn->reset_id;
|
||||
hdr->peer_reset_id = rconn->peer_reset_id;
|
||||
hdr->timestamp = get_current_timestamp();
|
||||
memcpy(base + SVC_ROUTE_HDR_SIZE, pending->dgram, pending->len);
|
||||
int ret = route_crypto_encode(rconn->inst, rconn->remote_node_id, base, len, pending->data[4], out, out_len);
|
||||
int ret = route_crypto_encode(rconn->inst, rconn->remote_node_id, base, len, ((struct router_pending*)pending->data)->mode, out, out_len);
|
||||
u_free(base);
|
||||
return ret;
|
||||
}
|
||||
@@ -528,7 +536,7 @@ static int router_conn_reset(struct ETCP_ROUTER_CONN* rconn) {
|
||||
rconn->peer_receive_blocked = 0;
|
||||
rconn->recv_conn = NULL;
|
||||
|
||||
rconn->rtt = 0; rconn->rtt_jitter = 0;
|
||||
rconn->rtt = 0; rconn->rtt_jitter = 0; rconn->delivery_rtt_us = 0;
|
||||
rconn->max_inflight = ROUTER_MAX_INFLIGHT;
|
||||
rconn->inflight_limit = ROUTER_MAX_INFLIGHT;
|
||||
rconn->minrtt = MINRTT_DEFAULT_TB;
|
||||
@@ -646,7 +654,8 @@ static void router_send_drain_cb(struct ll_queue* q, void* arg) {
|
||||
free_entry(e); u_free(inf->dgram); u_free(inf); return;
|
||||
}
|
||||
memcpy(e->dgram, inf->dgram, len); e->len = len;
|
||||
memcpy(&inf->seq, pending->data, 4); inf->ll.size = 4; inf->ll.len = pending->len;
|
||||
inf->seq = ((struct router_pending*)pending->data)->seq; inf->ll.size = 4; inf->ll.len = pending->len;
|
||||
inf->enqueued_us = ((struct router_pending*)pending->data)->enqueued_us;
|
||||
inf->dgram_len = len; inf->last_sent_tb = get_time_tb(); inf->send_count = 1;
|
||||
uint32_t seq = inf->seq;
|
||||
queue_data_put_with_index(rconn->inflight_q, &inf->ll);
|
||||
@@ -814,12 +823,18 @@ static int router_enqueue_send(struct ETCP_ROUTER_CONN* rconn, const uint8_t* pa
|
||||
return -1;
|
||||
}
|
||||
if (!force && queue_entry_count(rconn->send_q) >= ROUTER_MAX_SEND_Q_PACKETS) return -1;
|
||||
struct ll_entry* e = queue_entry_new(5);
|
||||
struct ll_entry* e = queue_entry_new(sizeof(struct router_pending));
|
||||
if (!e) { DEBUG_ERROR(DEBUG_CATEGORY_ETCPROUTE, "router enqueue: allocation failed"); return -1; }
|
||||
e->dgram = u_malloc(len);
|
||||
if (!e->dgram) { DEBUG_ERROR(DEBUG_CATEGORY_ETCPROUTE, "router enqueue: payload allocation failed"); queue_entry_free(e); return -1; }
|
||||
memcpy(e->data, &rconn->tx_seq, 4); e->data[4] = mode;
|
||||
*(struct router_pending*)e->data = (struct router_pending){get_time_us(), rconn->tx_seq, (uint8_t)mode};
|
||||
memcpy(e->dgram, payload, len); e->len = len;
|
||||
DEBUG_TRACE(DEBUG_CATEGORY_TRAFFIC, "route enqueue peer=%016llx group=%016llx epoch=%016llx svc=%u seq=%u bytes=%zu "
|
||||
"queued=%d inflight=%d limit=%u ready=%u no_route=%u peer_busy=%u waiter=%u",
|
||||
(unsigned long long)rconn->remote_node_id, (unsigned long long)rconn->group_id,
|
||||
(unsigned long long)rconn->reset_id, rconn->svc_id, rconn->tx_seq, len,
|
||||
queue_entry_count(rconn->send_q), queue_entry_count(rconn->inflight_q), rconn->inflight_limit,
|
||||
rconn->peer_reset_id != 0, rconn->no_route, rconn->peer_receive_blocked, rconn->send_waiter_q != NULL);
|
||||
rconn->tx_seq++;
|
||||
queue_data_put(rconn->send_q, e);
|
||||
router_send_kick(rconn);
|
||||
@@ -1205,7 +1220,12 @@ static void router_handle_ack(struct UTUN_INSTANCE* inst, struct ETCP_ROUTER_CON
|
||||
while (rconn->inflight_q->head) {
|
||||
struct ROUTER_INFLIGHT* inf = (struct ROUTER_INFLIGHT*)rconn->inflight_q->head;
|
||||
if ((int32_t)(inf->seq - seq) >= 0) break;
|
||||
route_tx_meter_ack(&rconn->delivery, get_time_us(), inf->ll.len);
|
||||
uint64_t now_us = get_time_us(), elapsed = now_us - inf->enqueued_us;
|
||||
rconn->delivery_rtt_us = rconn->delivery.packets ? rconn->delivery_rtt_us - rconn->delivery_rtt_us / 8 + elapsed / 8 : elapsed;
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_ETCPROUTE, "router delivery svc=%u seq=%u enqueue_ack_us=%llu average_us=%llu attempts=%u",
|
||||
rconn->svc_id, inf->seq, (unsigned long long)elapsed,
|
||||
(unsigned long long)rconn->delivery_rtt_us, inf->send_count);
|
||||
route_tx_meter_ack(&rconn->delivery, now_us, inf->ll.len);
|
||||
queue_remove_data(rconn->inflight_q, &inf->ll);
|
||||
u_free(inf->dgram); u_free(inf);
|
||||
}
|
||||
@@ -1341,6 +1361,9 @@ static void etcp_router_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry)
|
||||
struct SVC_ROUTE_HDR* h = (struct SVC_ROUTE_HDR*)entry->dgram;
|
||||
size_t body = router_path_check(conn, entry);
|
||||
if (!body) { free_entry(entry); return; }
|
||||
DEBUG_TRACE(DEBUG_CATEGORY_TRAFFIC, "route RX conn=%s svc=%u flags=%02x seq=%u len=%u src=%016llx dst=%016llx",
|
||||
conn->log_name, h->svc_id, h->flags, h->seq, entry->len,
|
||||
(unsigned long long)h->src_node_id, (unsigned long long)h->dst_node_id);
|
||||
if (h->dst_node_id != inst->node_id) {
|
||||
if (router_path_append(entry, inst->node_id, entry->dgram[entry->len - 1]) < 0) {
|
||||
inst->router_path_errors++; free_entry(entry); return;
|
||||
@@ -1572,12 +1595,17 @@ int etcp_router_tx_stats(struct UTUN_INSTANCE *inst, uint64_t group, uint64_t pe
|
||||
.queued_bytes = rconn->send_q->total_bytes, .inflight_bytes = rconn->inflight_q->total_bytes,
|
||||
.queued_packets = rconn->send_q->count, .inflight_packets = rconn->inflight_q->count,
|
||||
.acked_bytes = rconn->delivery.bytes, .acked_packets = rconn->delivery.packets,
|
||||
.delivery_rtt_us = rconn->delivery_rtt_us, .last_ack_us = rconn->delivery.packets ? rconn->delivery.last_us : 0,
|
||||
.rtt_us = (uint32_t)rconn->rtt * 100,
|
||||
.baseline_rtt_us = rconn->minrtt_window_count ? (uint32_t)rconn->minrtt * 100 : 0,
|
||||
.ready = rconn->peer_reset_id != 0, .no_route = rconn->no_route,
|
||||
.receive_busy = rconn->peer_receive_blocked, .probing = rconn->minrtt_probe
|
||||
};
|
||||
if (route_tx_meter_window(&rconn->delivery, now_us, &result.window) < 0) return -1;
|
||||
if (rconn->send_q->head)
|
||||
result.queued_age_us = now_us - ((struct router_pending*)rconn->send_q->head->data)->enqueued_us;
|
||||
if (rconn->inflight_q->head)
|
||||
result.inflight_age_us = now_us - ((struct ROUTER_INFLIGHT*)rconn->inflight_q->head)->enqueued_us;
|
||||
*out = result;
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -179,6 +179,7 @@ struct ETCP_ROUTER_CONN {
|
||||
uint32_t c_oob_dropped; // дропнуто out-of-bounds seq
|
||||
uint32_t c_stale_ack; // ACK вне диапазона или без продвижения seq, включая допустимый повтор RECEIVE_BUSY
|
||||
uint32_t c_sign_fail; // signature verification failures
|
||||
uint64_t delivery_rtt_us; // EWMA enqueue->ACK (1/8), включает send_q/нижние очереди/обратный путь.
|
||||
struct route_tx_meter delivery; // первые подтверждения исходного payload, без wire overhead
|
||||
};
|
||||
|
||||
@@ -187,6 +188,8 @@ struct ETCP_ROUTER_CONN {
|
||||
struct ETCP_ROUTER_TX_STATS {
|
||||
uint64_t local_epoch, peer_epoch, queued_bytes, inflight_bytes, acked_bytes, acked_packets;
|
||||
uint32_t queued_packets, inflight_packets;
|
||||
uint64_t delivery_rtt_us, last_ack_us; // Полный RTT enqueue->ACK и монотонное время последнего продвижения ACK.
|
||||
uint64_t queued_age_us, inflight_age_us; // Возраст головы от исходного enqueue; 0 при пустой очереди.
|
||||
struct route_tx_window window;
|
||||
uint32_t rtt_us, baseline_rtt_us;
|
||||
int ready, no_route, receive_busy, probing;
|
||||
@@ -225,6 +228,7 @@ int etcp_router_tx_stats(struct UTUN_INSTANCE *inst, uint64_t group, uint64_t pe
|
||||
struct ROUTER_INFLIGHT {
|
||||
struct ll_entry ll; // индекс по seq (4 байта, offset 0)
|
||||
uint32_t seq;
|
||||
uint64_t enqueued_us; // Исходное время постановки в send_q, неизменно при повторах.
|
||||
uint64_t last_sent_tb; // время последней отправки (0.1ms)
|
||||
uint8_t send_count; // успешные отправки, включая первую; диагностический циклический счётчик
|
||||
uint8_t* dgram; // неизменяемое тело [SVC_ROUTE_HDR][payload][sig?], без visited_ids/count
|
||||
|
||||
@@ -932,7 +932,8 @@ static void input_queue_cb(struct ll_queue* q, void* arg) {
|
||||
// memory_pool_free(etcp->io_pool, in_pkt);// перемещаем из io_pool в inflight_pool
|
||||
queue_entry_free(&in_pkt->ll);
|
||||
|
||||
DEBUG_TRACE(DEBUG_CATEGORY_ETCP, "[%s] TX input -> inflight (seq=%u, len=%u); Qlen: in=%d snd=%d wait=%d", etcp->log_name, p->seq, p->ll.len, etcp->input_queue->count, etcp->input_send_q->count, etcp->input_wait_ack->count);
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "[%s] TX input -> inflight seq=%u len=%u input=%d send=%d wait=%d",
|
||||
etcp->log_name, p->seq, p->ll.len, etcp->input_queue->count, etcp->input_send_q->count, etcp->input_wait_ack->count);
|
||||
int len=p->ll.len;// сохраним len
|
||||
|
||||
// Add to send queue
|
||||
@@ -1076,6 +1077,38 @@ static void etcp_link_ready_callback(struct ETCP_CONN* etcp) {
|
||||
etcp_conn_wake_sender(etcp);
|
||||
}
|
||||
|
||||
/* Снимок после прохода отправителя: причины могут пересекаться, full относится к голове очереди.
|
||||
* Только BLE и включённая debug-категория; не вызывает selection/BBR и не меняет состояние очередей. */
|
||||
static void trace_ble_tx_wait(struct ETCP_CONN* conn) {
|
||||
if (!debug_should_output(DEBUG_LEVEL_DEBUG, DEBUG_CATEGORY_DEBUG) || conn->close_requested || conn->state == 2) return;
|
||||
struct PKTNORM* pn = conn->normalizer;
|
||||
if (!conn->input_send_q->count && !conn->input_queue->count &&
|
||||
(!pn || (!queue_entry_count(pn->input) && !pn->pending && !pn->data_ptr))) return;
|
||||
struct INFLIGHT_PACKET* head = (struct INFLIGHT_PACKET*)conn->input_send_q->head;
|
||||
unsigned input_len = conn->input_queue->head ? conn->input_queue->head->len : 0;
|
||||
uint64_t now = get_time_tb();
|
||||
for (struct ETCP_LINK* link = conn->links; link; link = link->next) {
|
||||
struct packet_link* channel = etcp_link_io_get(link);
|
||||
if (!channel || strcmp(packet_link_name(channel), "BLE")) continue;
|
||||
unsigned bytes = head ? head->ll.len : input_len;
|
||||
int cwnd_full = bytes && (bytes > link->inflight_lim_bytes || link->inflight_bytes > link->inflight_lim_bytes - bytes);
|
||||
int attempts_full = conn->active_attempts >= 2 * MAX_INFLIGHT_SIZE || queue_entry_count(link->attempts) >= MAX_INFLIGHT_SIZE;
|
||||
int input_full = input_len && (input_len > conn->optimal_inflight || conn->unacked_bytes > conn->optimal_inflight - input_len);
|
||||
uint64_t pacing_tb = link->shaper_load_time_tb >= now + SHAPER_BURST_DELAY_TB ?
|
||||
link->shaper_load_time_tb - now - SHAPER_BURST_DELAY_TB + 1 : 0;
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "[%s] TX gate path=%u head_seq=%u bytes=%u retry=%u io=%d up=%u init=%u "
|
||||
"cwnd_full=%d attempts_full=%d pacing=%d pacing_us=%llu rate_Bps=%u flight=%u/%u "
|
||||
"input_full=%d unacked=%u/%u peer_full=%d input=%d send=%d norm=%d pending=%u buffered=%u token=%d",
|
||||
conn->log_name, link->local_link_id, head ? head->seq : 0, bytes, head && head->last_attempt_id != 0,
|
||||
etcp_link_io_ready(link), link->link_status, link->initialized, cwnd_full, attempts_full,
|
||||
link->shaper_timer != NULL, (unsigned long long)(pacing_tb * 100), link->bbr_pacing_rate,
|
||||
link->inflight_bytes, link->inflight_lim_bytes, input_full, conn->unacked_bytes, conn->optimal_inflight,
|
||||
(uint32_t)(conn->next_tx_id - conn->rx_ack_till) > MAX_INFLIGHT_SIZE,
|
||||
conn->input_queue->count, conn->input_send_q->count, pn ? queue_entry_count(pn->input) : 0,
|
||||
pn && pn->pending ? pn->pending->len - pn->pending_in_ptr : 0, pn ? pn->data_ptr : 0, conn->tx_token != NULL);
|
||||
}
|
||||
}
|
||||
|
||||
// Process packets in send queue and transmit them
|
||||
static void etcp_conn_process_send_queue(struct ETCP_CONN* etcp) {// вызываем когда есть элемент в send_q или надо отправить ack
|
||||
if (etcp->tx_running || etcp->close_requested || etcp->state == 2) return;
|
||||
@@ -1088,6 +1121,7 @@ static void etcp_conn_process_send_queue(struct ETCP_CONN* etcp) {// вызыв
|
||||
}
|
||||
etcp->callbacks_running--;
|
||||
etcp->tx_running = 0;
|
||||
trace_ble_tx_wait(etcp);
|
||||
}
|
||||
|
||||
/* Один запуск обслуживает накопленные события; заблокированный DATA ждёт ACK/RTO/shaper, а не следующий poll. */
|
||||
|
||||
@@ -245,6 +245,10 @@ int etcp_send(struct ETCP_CONN* conn, struct ll_entry* entry) {
|
||||
(void*)conn, q->count, q->size_limit, q->hash_size);
|
||||
return -1;
|
||||
}
|
||||
if (entry->dgram && entry->len) {
|
||||
DEBUG_TRACE(DEBUG_CATEGORY_TRAFFIC, "enqueue id=0x%02x conn=%s len=%u pending=%d",
|
||||
entry->dgram[0], conn->log_name, entry->len, q->count);
|
||||
}
|
||||
return queue_data_put(conn->send_input_q, entry);
|
||||
}
|
||||
|
||||
|
||||
@@ -159,7 +159,12 @@ static void changed(struct packet_link* channel, void* arg) {
|
||||
goto done;
|
||||
}
|
||||
}
|
||||
if (state.tx_ready) etcp_conn_wake_sender(conn);
|
||||
if (state.tx_ready) {
|
||||
if (!io->native) DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "[%s] carrier ready path=%u driver=%s input=%d send=%d token=%d",
|
||||
conn->log_name, owner->local_link_id, packet_link_name(channel), queue_entry_count(conn->input_queue),
|
||||
queue_entry_count(conn->input_send_q), conn->tx_token != NULL);
|
||||
etcp_conn_wake_sender(conn);
|
||||
}
|
||||
done:
|
||||
etcp_link_release(owner); conn->callbacks_running--;
|
||||
}
|
||||
|
||||
@@ -217,7 +217,8 @@ static int pn_send_to_etcp(struct PKTNORM* pn) {
|
||||
pn->data = NULL;
|
||||
pn->data_ptr = 0;
|
||||
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_ETCP, "pn->etcp: size=%d memlen=%d frag_size=%d", frag->ll.len, frag->ll.memlen, pn->frag_size);
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "[%s] normalizer TX fragment bytes=%u capacity=%u frag_size=%u input=%d",
|
||||
pn->etcp->log_name, frag->ll.len, frag->ll.memlen, pn->frag_size, queue_entry_count(pn->input));
|
||||
if (debug_should_output(DEBUG_LEVEL_DEBUG, DEBUG_CATEGORY_DUMP)) log_dump(DEBUG_LEVEL_DEBUG, DEBUG_CATEGORY_DUMP, "NORM->ETCP", frag->ll.dgram, frag->ll.len);
|
||||
|
||||
if (queue_data_put(pn->etcp->input_queue, (struct ll_entry*)frag) < 0) {
|
||||
|
||||
@@ -13,6 +13,7 @@ check_PROGRAMS = \
|
||||
test_video_no_ffmpeg \
|
||||
test_video_plane \
|
||||
test_route_tx_stats \
|
||||
test_call_audio_batch \
|
||||
test_call_video_rate \
|
||||
test_call_video_playout \
|
||||
test_call_video_port \
|
||||
@@ -1070,6 +1071,9 @@ test_call_SOURCES = test_call.c
|
||||
test_route_tx_stats_SOURCES = test_route_tx_stats.c
|
||||
test_route_tx_stats_LDADD = $(LIBUTUN) $(COMMON_LIBS)
|
||||
|
||||
test_call_audio_batch_SOURCES = test_call_audio_batch.c
|
||||
test_call_audio_batch_LDADD = $(LIBUTUN) $(COMMON_LIBS)
|
||||
|
||||
test_call_video_rate_SOURCES = test_call_video_rate.c
|
||||
test_call_video_rate_LDADD = $(LIBUTUN) $(COMMON_LIBS)
|
||||
|
||||
|
||||
+55
-4
@@ -69,6 +69,7 @@ enum call_phase {
|
||||
P_WAIT_ACCEPTED, /* обе ACTIVE */
|
||||
P_SEND_MEDIA, /* A: call_send_media (тестовый Opus-кадр) */
|
||||
P_WAIT_MEDIA, /* B: media_recv_cb получил кадр */
|
||||
P_BATCH_SEND, P_BATCH_WAIT, P_PARTIAL_WAIT,
|
||||
P_VIDEO_CONFIG, P_VIDEO_SEND, P_VIDEO_HOLD, P_VIDEO_BUSY, P_VIDEO_RESUME, P_VIDEO_RANGE_WAIT, P_VIDEO_SWITCH, P_VIDEO_SWITCH_WAIT,
|
||||
P_VIDEO_SHRINK_WAIT, P_VIDEO_PORT_OPEN, P_VIDEO_PORT_RECEIVE, P_VIDEO_PORT_SEND, P_VIDEO_PORT_ACK,
|
||||
P_VIDEO_DECODE_OK, P_VIDEO_DECODE_REJECT,
|
||||
@@ -134,12 +135,14 @@ struct media_evt {
|
||||
};
|
||||
static struct media_evt g_media[2];
|
||||
static struct call_capture_time g_capture_timing;
|
||||
static unsigned g_audio_packets;
|
||||
static const struct call_capture_time sent_timing = {1234567890123ll, 19, 312};
|
||||
|
||||
static void audio_recv_cb(struct UTUN_INSTANCE* inst, uint64_t call_id, uint16_t seq, uint32_t ts,
|
||||
const uint8_t* opus, int len, const struct call_capture_time* timing) {
|
||||
(void)inst; (void)call_id; (void)seq; (void)ts; (void)opus; (void)len;
|
||||
const uint8_t* opus, int len, const struct call_capture_time* timing, int packet_start) {
|
||||
(void)inst; (void)call_id; (void)seq; (void)ts; (void)opus; (void)len; (void)packet_start;
|
||||
g_capture_timing = *timing;
|
||||
g_audio_packets += packet_start != 0;
|
||||
}
|
||||
static const struct call_audio_ops audio_ops = {.on_media = audio_recv_cb};
|
||||
|
||||
@@ -586,6 +589,48 @@ static void call_tick(void* arg) {
|
||||
|| g_capture_timing.epoch != sent_timing.epoch || g_capture_timing.delay_samples != sent_timing.delay_samples) {
|
||||
fprintf(stderr, "B: capture timing changed in transit\n"); t->result = 2; break;
|
||||
}
|
||||
t->phase = P_BATCH_SEND;
|
||||
}
|
||||
break;
|
||||
case P_BATCH_SEND: {
|
||||
struct call_session *cs = call_session_find(A->call, t->call_id);
|
||||
if (cs->audio_batch_timer) uasync_cancel_timeout(t->ua, cs->audio_batch_timer);
|
||||
cs->audio_batch_timer = NULL; cs->audio_batch.target = 10; cs->audio_batch.hold_us = 40000;
|
||||
uint8_t opus[16] = {0};
|
||||
for (unsigned i = 1; i <= 10; i++) {
|
||||
struct call_capture_time timing = sent_timing; timing.pts_us += i * 20000;
|
||||
if (call_send_media(A, t->call_id, opus, sizeof(opus), &timing)) { t->result = 2; break; }
|
||||
}
|
||||
if (cs->tx_batches != 2 || cs->c_tx_frames != 11 || cs->audio_flush_timer) {
|
||||
fprintf(stderr, "full batch was not sent once/cancelled timer\n"); t->result = 2;
|
||||
}
|
||||
t->phase = P_BATCH_WAIT; break;
|
||||
}
|
||||
case P_BATCH_WAIT:
|
||||
if (g_media[IDX_B].frames >= 11) {
|
||||
if (g_media[IDX_B].frames != 11 || g_audio_packets != 2 || g_capture_timing.pts_us != sent_timing.pts_us + 200000) {
|
||||
fprintf(stderr, "batch frames/timing not preserved\n"); t->result = 2; break;
|
||||
}
|
||||
struct call_session *cs = call_session_find(A->call, t->call_id);
|
||||
uint8_t opus[16] = {0};
|
||||
for (unsigned i = 11; i <= 13; i++) {
|
||||
struct call_capture_time timing = sent_timing; timing.pts_us += i * 20000;
|
||||
if (call_send_media(A, t->call_id, opus, sizeof(opus), &timing)) { t->result = 2; break; }
|
||||
}
|
||||
if (cs->tx_batches != 2 || !cs->audio_flush_timer || cs->tx_q->count != 3) {
|
||||
fprintf(stderr, "partial batch did not wait for deadline\n"); t->result = 2;
|
||||
}
|
||||
t->phase = P_PARTIAL_WAIT;
|
||||
}
|
||||
break;
|
||||
case P_PARTIAL_WAIT:
|
||||
if (g_media[IDX_B].frames >= 14) {
|
||||
struct call_session *cs = call_session_find(A->call, t->call_id);
|
||||
if (g_media[IDX_B].frames != 14 || g_audio_packets != 3 || cs->tx_batches != 3 || cs->audio_flush_timer
|
||||
|| g_capture_timing.pts_us != sent_timing.pts_us + 260000) {
|
||||
fprintf(stderr, "partial batch deadline delivery failed\n"); t->result = 2; break;
|
||||
}
|
||||
cs->audio_batch.target = 1; cs->audio_batch.hold_us = 0;
|
||||
t->phase = P_VIDEO_CONFIG;
|
||||
}
|
||||
break;
|
||||
@@ -642,7 +687,7 @@ static void call_tick(void* arg) {
|
||||
}
|
||||
case P_VIDEO_BUSY:
|
||||
if (g_video_frames) { fprintf(stderr, "video bypassed receive backpressure\n"); t->result = 2; break; }
|
||||
if (++t->phase_ticks < 10 || g_media[IDX_B].frames != 2) break;
|
||||
if (++t->phase_ticks < 10 || g_media[IDX_B].frames != 15) break;
|
||||
g_video_ready = 1; call_video_resume_receive(B, t->call_id); t->phase = P_VIDEO_RESUME; break;
|
||||
case P_VIDEO_RESUME:
|
||||
if (!g_video_frames || g_video_preroll != 6 || !g_rate_commands) break;
|
||||
@@ -779,9 +824,15 @@ static void call_tick(void* arg) {
|
||||
call_video_port_request_key(t->call_id);
|
||||
t->previous_call_id = t->call_id;
|
||||
g_drop_acks[IDX_B] = 2;
|
||||
struct call_session *pending_audio = call_session_find(B->call, t->call_id);
|
||||
pending_audio->audio_batch.target = 10; pending_audio->audio_batch.hold_us = 40000;
|
||||
uint8_t unfinished_opus[2] = {1, 2};
|
||||
if (call_send_media(B, t->call_id, unfinished_opus, sizeof(unfinished_opus), &sent_timing)
|
||||
|| !pending_audio->audio_flush_timer) { t->result = 2; break; }
|
||||
call_hangup(B, t->call_id);
|
||||
struct call_session* ended = call_session_find(B->call, t->call_id);
|
||||
if (!ended || ended->tx_q || ended->video || !ended->end_subcmd || !ended->teardown_timer) {
|
||||
if (!ended || ended->tx_q || ended->video || ended->audio_flush_timer || ended->audio_batch_timer
|
||||
|| !ended->end_subcmd || !ended->teardown_timer) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "ended call retained media or lost final command"); t->result = 2; break;
|
||||
}
|
||||
t->phase = P_VIDEO_PORT_CANCELLED; break;
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
/* Wire: границы/атомарный отказ. Регулятор: полный RTT, очередь без ACK, гистерезис и смена эпохи. */
|
||||
#include "../src/call/call_audio_batch.h"
|
||||
#include "../src/routing_layer/etcp_router.h"
|
||||
#include "../lib/debug_config.h"
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#define CHECK(x) do { if (!(x)) { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "audio batch test line=%d: %s", __LINE__, #x); exit(1); } } while (0)
|
||||
|
||||
static void wire_test(void) {
|
||||
uint8_t opus[57], wire[CALL_MEDIA_MAX_BYTES], saved[CALL_MEDIA_MAX_BYTES];
|
||||
memset(opus, 0xa5, sizeof(opus));
|
||||
struct call_audio_batch_view b = {.header = {.subcmd = CALL_SUBCMD_MEDIA, .call_id = 1, .track_id = CALL_TRACK_AUDIO,
|
||||
.capture_epoch = 17, .codec_delay_samples = 312, .sent_ms = UINT32_MAX}};
|
||||
size_t length = 0;
|
||||
for (unsigned n = 1; n <= 10; n++) {
|
||||
b.header.count = n;
|
||||
b.frames[n - 1] = (struct call_audio_frame_view){.header = {(uint16_t)(65532 + n), 1000000 + n * 20000, 37}, .opus = opus};
|
||||
CHECK(call_audio_batch_encode(&b, wire, sizeof(wire), &length) == 0 && length == 27 + n * 49);
|
||||
struct call_audio_batch_view out;
|
||||
CHECK(call_audio_batch_parse(wire, length, &out) == 0 && out.header.count == n && out.header.sent_ms == UINT32_MAX);
|
||||
for (unsigned i = 0; i < n; i++) CHECK(out.frames[i].header.seq == b.frames[i].header.seq
|
||||
&& out.frames[i].header.capture_pts_us == b.frames[i].header.capture_pts_us
|
||||
&& out.frames[i].header.length == 37 && !memcmp(out.frames[i].opus, opus, 37));
|
||||
}
|
||||
struct call_audio_batch_view out, before;
|
||||
memset(&out, 0x77, sizeof(out)); before = out;
|
||||
for (size_t n = 0; n < length; n++) {
|
||||
CHECK(call_audio_batch_parse(wire, n, &out) == -1 && !memcmp(&out, &before, sizeof(out)));
|
||||
}
|
||||
CHECK(call_audio_batch_parse(wire, length + 1, &out) == -1);
|
||||
memcpy(saved, wire, sizeof(wire));
|
||||
wire[CALL_MEDIA_HDR_SIZE - 1] = 11;
|
||||
CHECK(call_audio_batch_parse(wire, length, &out) == -1);
|
||||
memcpy(wire, saved, sizeof(wire));
|
||||
struct call_pkt_audio_frame invalid = {1, 0, UINT16_MAX};
|
||||
memcpy(wire + CALL_MEDIA_HDR_SIZE, &invalid, sizeof(invalid));
|
||||
CHECK(call_audio_batch_parse(wire, length, &out) == -1);
|
||||
memset(wire, 0x55, sizeof(wire)); memcpy(saved, wire, sizeof(wire));
|
||||
CHECK(call_audio_batch_encode(&b, wire, length - 1, &length) == -1 && !memcmp(wire, saved, sizeof(wire)));
|
||||
b.header.count = 1; b.header.capture_epoch = 0;
|
||||
CHECK(call_audio_batch_encode(&b, wire, sizeof(wire), &length) == -1);
|
||||
b.frames[0].header.capture_pts_us = INT64_MIN;
|
||||
CHECK(call_audio_batch_encode(&b, wire, sizeof(wire), &length) == 0);
|
||||
CHECK(call_audio_batch_parse(wire, length, &out) == 0 && out.frames[0].header.capture_pts_us == INT64_MIN);
|
||||
}
|
||||
|
||||
static void step(struct call_audio_batch_control *c, struct ETCP_ROUTER_TX_STATS *n, uint64_t t) {
|
||||
n->last_ack_us = t; CHECK(call_audio_batch_step(c, t, n) == 0);
|
||||
}
|
||||
static void control_test(void) {
|
||||
struct call_audio_batch_control c;
|
||||
struct ETCP_ROUTER_TX_STATS n = {.local_epoch = 1, .peer_epoch = 2, .ready = 1, .acked_packets = 1,
|
||||
.delivery_rtt_us = 20000, .window = {.valid = 1}};
|
||||
call_audio_batch_init(&c, 0);
|
||||
CHECK(c.target == 4 && c.hold_us == 60000);
|
||||
CHECK(call_audio_batch_step(&c, 0, NULL) == 0 && c.target == 4 && c.hold_us == 60000);
|
||||
n.ready = 0; n.acked_packets = 0; n.window.valid = 0;
|
||||
step(&c, &n, 50000); CHECK(c.target == 4 && c.hold_us == 60000);
|
||||
n.ready = 1; n.acked_packets = 1;
|
||||
step(&c, &n, 100000); CHECK(c.target == 4 && c.hold_us == 0); // Первый ACK, не ждём окна500мс.
|
||||
n.window.valid = 1;
|
||||
for (unsigned t = 150000; t < 2000000; t += 50000) step(&c, &n, t);
|
||||
CHECK(c.target == 4 && c.hold_us == 0);
|
||||
/* Изолированная задержка не включает упаковку. */
|
||||
n.delivery_rtt_us = 150000; step(&c, &n, 2000000); step(&c, &n, 2050000);
|
||||
n.delivery_rtt_us = 20000; step(&c, &n, 2100000); CHECK(c.target == 4);
|
||||
/* Даже без queued/inflight высокий обычный RTT включает пачки. */
|
||||
n.delivery_rtt_us = 150000;
|
||||
for (unsigned t = 2150000; t <= 2350000; t += 50000) step(&c, &n, t);
|
||||
CHECK(c.target == 5 && c.hold_us == 27500);
|
||||
for (unsigned t = 2400000; t <= 2850000; t += 50000) step(&c, &n, t);
|
||||
CHECK(c.target == 6 && c.hold_us == 27500);
|
||||
n.delivery_rtt_us = 300000;
|
||||
for (unsigned t = 2900000; t <= 9000000; t += 50000) step(&c, &n, t);
|
||||
CHECK(c.target == 10 && c.hold_us == 40000);
|
||||
n.delivery_rtt_us = 10000; step(&c, &n, 9050000);
|
||||
CHECK(c.target == 10 && c.hold_us == 0);
|
||||
for (unsigned t = 9100000; t <= 11050000; t += 50000) step(&c, &n, t);
|
||||
CHECK(c.target == 9);
|
||||
n.peer_epoch = 3; step(&c, &n, 11100000); CHECK(c.target == 4);
|
||||
n.inflight_age_us = 150000;
|
||||
for (unsigned t = 11150000; t <= 11350000; t += 50000) step(&c, &n, t);
|
||||
CHECK(c.target == 5);
|
||||
n.receive_busy = 1;
|
||||
for (unsigned t = 11400000; t <= 13000000; t += 50000) step(&c, &n, t);
|
||||
CHECK(c.target == 5 && c.hold_us == 0);
|
||||
n.receive_busy = 0; CHECK(call_audio_batch_step(&c, 20000000, &n) == 0 && c.target == 5 && c.hold_us == 0);
|
||||
CHECK(call_audio_batch_step(&c, 19999999, &n) == -1);
|
||||
CHECK(call_audio_batch_step(&c, 20000001, NULL) == 0 && c.target == 4 && c.hold_us == 60000);
|
||||
n.inflight_age_us = 0;
|
||||
for (unsigned t = 20100000; t <= 27000000; t += 50000) step(&c, &n, t);
|
||||
CHECK(c.target == 1 && c.hold_us == 0);
|
||||
}
|
||||
int main(void) {
|
||||
debug_config_init(); debug_set_level(DEBUG_LEVEL_ERROR);
|
||||
wire_test(); control_test();
|
||||
debug_set_level(DEBUG_LEVEL_INFO);
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "PASS audio batch: 1..10, partial/trailing/invalid wire, full RTT, hysteresis, deadline budget, epochs/stall");
|
||||
return 0;
|
||||
}
|
||||
@@ -4,6 +4,8 @@
|
||||
#include "../src/routing_layer/topo_node_sqlite.h"
|
||||
#include "../src/transport_layer/etcp.h"
|
||||
#include "../src/transport_layer/etcp_connections.h"
|
||||
#include "../src/transport_layer/pkt_normalizer.h"
|
||||
#include "../src/routing_layer/etcp_router.h"
|
||||
#include "../lib/strbuf.h"
|
||||
#include <assert.h>
|
||||
|
||||
@@ -20,6 +22,7 @@ static uint8_t detail[CHAT_MEMBER_DETAIL_MAX_SIZE];
|
||||
static size_t detail_size;
|
||||
static unsigned text_events;
|
||||
static int expect_large_text;
|
||||
static int expect_router_metrics;
|
||||
|
||||
static int matches(const char* sql) { return target && !strncmp(sql, target, strlen(target)); }
|
||||
static int take(enum fault point, const char* sql, int index) {
|
||||
@@ -86,6 +89,16 @@ static void event(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int
|
||||
if (type == CHAT_EVT_STATUS_REFRESH || type == CHAT_EVT_CONN_METRICS) {
|
||||
assert(size > 0);
|
||||
if (expect_large_text) assert(size > 8192 && memmem(data, (size_t)size, "LINK#299", 8));
|
||||
if (type == CHAT_EVT_CONN_METRICS && expect_router_metrics) {
|
||||
const char* expected[] = {"--- Router queues ---", "peer=0000000000000009 svc=63 (call audio)",
|
||||
"peer=000000000000000B svc=52 (DM)", "sessions: 2\n", "send_q: 202B/2p wait_ack: 303B/1p",
|
||||
"incoming_q: 44B/1p recv_q: 55B/1p", "send_q: 0B/0p wait_ack: 0B/0p",
|
||||
"transit total: 666B/2p", "input_q: 777B/1p output_q: 0B/0p"};
|
||||
for (unsigned i = 0; i < sizeof(expected) / sizeof(expected[0]); i++)
|
||||
assert(memmem(data, (size_t)size, expected[i], strlen(expected[i])));
|
||||
const char* excluded = "peer=000000000000000C svc=";
|
||||
assert(!memmem(data, (size_t)size, excluded, strlen(excluded)));
|
||||
}
|
||||
text_events++; return;
|
||||
}
|
||||
if (type == CHAT_EVT_MEMBER_DETAIL) {
|
||||
@@ -232,6 +245,16 @@ static void node_names(struct UTUN_INSTANCE* inst) {
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "PASS node names:4 SQL faults/retries,cleared failure output and zero-size guard");
|
||||
}
|
||||
|
||||
/* Очереди без callbacks: снимок обязан сохранить элементы и байтовые счётчики. */
|
||||
static struct ll_queue* metrics_queue(struct UTUN_INSTANCE* inst, unsigned count, uint16_t bytes) {
|
||||
struct ll_queue* q = queue_new(inst->ua, 0, 0, 0, "metrics_test"); assert(q);
|
||||
for (unsigned i = 0; i < count; i++) {
|
||||
struct ll_entry* e = ll_alloc_lldgram(bytes); assert(e); e->len = bytes;
|
||||
assert(!queue_data_put(q, e));
|
||||
}
|
||||
return q;
|
||||
}
|
||||
|
||||
static void diagnostics(struct UTUN_INSTANCE* inst) {
|
||||
struct ETCP_CONN connection = {.instance = inst, .peer_node_id = 9, .unacked_bytes = UINT32_MAX};
|
||||
struct ETCP_LINK* links = u_calloc(300, sizeof(*links)); assert(links); connection.links = links;
|
||||
@@ -242,17 +265,50 @@ static void diagnostics(struct UTUN_INSTANCE* inst) {
|
||||
struct ll_entry* entry = queue_entry_new(sizeof(struct conn_queue_entry)); assert(entry);
|
||||
struct conn_queue_entry* item = (struct conn_queue_entry*)entry->data; item->peer_node_id = 9; item->conn = &connection;
|
||||
assert(!queue_data_put_with_index(inst->connections, entry)); expect_large_text = 1; unsigned before = text_events;
|
||||
struct ll_queue* saved_router = inst->router_conns;
|
||||
inst->router_conns = metrics_queue(inst, 0, 0);
|
||||
struct ETCP_ROUTER_CONN* sessions[3];
|
||||
for (unsigned i = 0; i < 3; i++) {
|
||||
sessions[i] = (struct ETCP_ROUTER_CONN*)queue_entry_new(sizeof(*sessions[i]) - sizeof(struct ll_entry)); assert(sessions[i]);
|
||||
sessions[i]->inst = inst; sessions[i]->remote_node_id = i ? 10 + i : 9;
|
||||
sessions[i]->svc_id = i ? ETCP_RT_ID_DM : ETCP_RT_ID_CALL_AUDIO;
|
||||
assert(!queue_data_put(inst->router_conns, &sessions[i]->ll));
|
||||
}
|
||||
sessions[1]->recv_conn = &connection;
|
||||
struct ll_queue* queues[] = {metrics_queue(inst, 2, 101), metrics_queue(inst, 1, 303), metrics_queue(inst, 1, 44),
|
||||
metrics_queue(inst, 1, 55), metrics_queue(inst, 2, 333), metrics_queue(inst, 1, 777)};
|
||||
sessions[0]->send_q = queues[0]; sessions[0]->inflight_q = queues[1];
|
||||
sessions[0]->incoming_q = queues[2]; sessions[0]->recv_q = queues[3];
|
||||
struct TRANSIT_QUEUE* transit = (struct TRANSIT_QUEUE*)queue_entry_new(sizeof(*transit) - sizeof(struct ll_entry)); assert(transit);
|
||||
transit->q = queues[4]; transit->conn = &connection; transit->src_node_id = 11; transit->dst_node_id = 12;
|
||||
connection.transit_queues = metrics_queue(inst, 0, 0); assert(!queue_data_put(connection.transit_queues, &transit->ll));
|
||||
struct PKTNORM normalizer = {.input = queues[5]}; connection.normalizer = &normalizer;
|
||||
expect_router_metrics = 1;
|
||||
chat_core_collect_status_trampoline(inst);
|
||||
struct chat_node_arg* req = u_malloc(sizeof(*req)); assert(req); *req = (struct chat_node_arg){inst, 9}; chat_core_collect_conn_metrics_trampoline(req);
|
||||
assert(text_events == before + 2);
|
||||
target = "strbuf.c"; fault = F_REALLOCATE; chat_core_collect_status_trampoline(inst); assert(fault == F_NONE && text_events == before + 2);
|
||||
chat_core_collect_status_trampoline(inst); assert(text_events == before + 3); expect_large_text = 0;
|
||||
req = u_malloc(sizeof(*req)); assert(req); *req = (struct chat_node_arg){inst, 9};
|
||||
fault = F_REALLOCATE; chat_core_collect_conn_metrics_trampoline(req); assert(fault == F_NONE && text_events == before + 3);
|
||||
req = u_malloc(sizeof(*req)); assert(req); *req = (struct chat_node_arg){inst, 9}; chat_core_collect_conn_metrics_trampoline(req);
|
||||
assert(text_events == before + 4); expect_router_metrics = 0;
|
||||
const unsigned counts[] = {2, 1, 1, 1, 2, 1};
|
||||
const size_t bytes[] = {202, 303, 44, 55, 666, 777};
|
||||
for (unsigned i = 0; i < sizeof(queues) / sizeof(queues[0]); i++) {
|
||||
assert(queue_entry_count(queues[i]) == (int)counts[i] && queue_total_bytes(queues[i]) == bytes[i]);
|
||||
assert(!queue_check_consistency(queues[i])); queue_clear(queues[i]); queue_free(queues[i]);
|
||||
}
|
||||
queue_clear(connection.transit_queues); queue_free(connection.transit_queues); connection.transit_queues = NULL;
|
||||
queue_clear(inst->router_conns); queue_free(inst->router_conns); inst->router_conns = saved_router;
|
||||
connection.normalizer = NULL;
|
||||
queue_remove_data(inst->connections, entry); queue_entry_free(entry); u_free(links); queue_free(acks);
|
||||
if (SIZE_MAX > UINT32_MAX) {
|
||||
size_t baseline = u_get_allocated_count(); struct strbuf text; strbuf_init(&text, (size_t)UINT32_MAX + 1);
|
||||
assert(!text.buf && !text.cap && !text.len && u_get_allocated_count() == baseline);
|
||||
}
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "PASS diagnostics:300 links,full status/metrics beyond8KiB,OOM suppresses partial event,retry,allocator capacity boundary");
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "PASS router metrics:peer/RX scope,unrelated peer excluded,queue bytes/packets,transit,normalizer,null queues,read-only");
|
||||
}
|
||||
|
||||
static void member_limit(struct UTUN_INSTANCE* inst) {
|
||||
|
||||
@@ -543,6 +543,12 @@ static void monitor(void* arg) {
|
||||
uint32_t cycle_target = g_cycle_start_seq + g_cycle_sent;
|
||||
struct ETCP_ROUTER_CONN* rc = etcp_router_conn_get(cli, TOPO_GROUP_UTUN, server_node_id, TEST_SVC_ID);
|
||||
if (g_total_rcvd == cycle_target && rc->tx_acked == rc->tx_seq && !rc->send_q->count && !rc->inflight_q->count) {
|
||||
struct ETCP_ROUTER_TX_STATS stats;
|
||||
if (etcp_router_tx_stats(cli, TOPO_GROUP_UTUN, server_node_id, TEST_SVC_ID, get_time_us(), &stats)
|
||||
|| !stats.delivery_rtt_us || !stats.last_ack_us || stats.queued_age_us || stats.inflight_age_us
|
||||
|| stats.acked_packets != cycle_target) {
|
||||
printf("[FAIL] Router delivery timing/counters after drain\n"); g_done = -1; return;
|
||||
}
|
||||
t_cycle_drain_ms[g_cycle] = toc_ms(&t_phase_start);
|
||||
printf(" cycle %d drain: %7.1f ms rcvd=%u/%u\n",
|
||||
g_cycle + 1, t_cycle_drain_ms[g_cycle], g_total_rcvd, cycle_target);
|
||||
|
||||
@@ -252,6 +252,19 @@ static void invitations(void) {
|
||||
assert(!a->send(a->ctx, endpoints[1].inst.node_id, 2, link, strlen(link)));
|
||||
assert(!b->send(b->ctx, endpoints[0].inst.node_id, 3, link, strlen(link)));
|
||||
exchange(4); drain();
|
||||
/* 1000 poll(0) не ждут 15ms BLE ACK: ждём конкретную доставку, а не скорость CPU. */
|
||||
uint64_t started = get_time_us(), deadline = started + 5000000;
|
||||
for (int i = 0; i < 2; i++) {
|
||||
struct ETCP_CONN* c = endpoints[i].conn;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "BLE ETCP invitation overlap: side=%d received=%u target=5 input=%d send=%d ready=%d token=%d",
|
||||
i, endpoints[i].received, queue_entry_count(c->input_queue), queue_entry_count(c->input_send_q),
|
||||
etcp_link_io_ready(c->links), c->tx_token != NULL);
|
||||
}
|
||||
while ((endpoints[0].received < 5 || endpoints[1].received < 5 || invites_delivered[0] < 2 || invites_delivered[1] < 1) &&
|
||||
get_time_us() < deadline) uasync_poll(ua, 10);
|
||||
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "BLE ETCP invitation overlap completed: RX=%u/%u invitations=%u/%u elapsed_us=%llu",
|
||||
endpoints[0].received, endpoints[1].received, invites_delivered[0], invites_delivered[1],
|
||||
(unsigned long long)(get_time_us() - started));
|
||||
assert(invites_received[0] == 1 && invites_received[1] == 1);
|
||||
assert(invites_delivered[0] == 2 && invites_delivered[1] == 1);
|
||||
assert(delivery_status[0] == LOCAL_DELIVERY_RECEIVED && delivery_status[1] == LOCAL_DELIVERY_RECEIVED);
|
||||
@@ -266,7 +279,7 @@ static void invitations(void) {
|
||||
drop_invite_ack = 1;
|
||||
assert(!a->send(a->ctx, endpoints[1].inst.node_id, 5, link, strlen(link))); drain();
|
||||
assert(invites_received[1] == 2 && invites_delivered[0] == 4);
|
||||
uint64_t deadline = get_time_us() + 11000000ULL;
|
||||
deadline = get_time_us() + 11000000ULL;
|
||||
while (invites_delivered[0] == 4 && get_time_us() < deadline) uasync_poll(ua, 100);
|
||||
assert(invites_delivered[0] == 5 && delivery_status[0] == LOCAL_DELIVERY_RECEIVED && invites_received[1] == 2);
|
||||
hold = 1;
|
||||
@@ -307,6 +320,7 @@ static void invitation_without_etcp(void) {
|
||||
}
|
||||
int main(void) {
|
||||
debug_config_init(); debug_set_level(getenv("UTUN_TEST_DEBUG") ? DEBUG_LEVEL_DEBUG : DEBUG_LEVEL_WARN);
|
||||
debug_set_category_level(DEBUG_CATEGORY_DEBUG, DEBUG_LEVEL_INFO);
|
||||
size_t baseline = u_get_allocated_count(); setup();
|
||||
for (int i = 0; i < 2; i++) {
|
||||
struct ble_config config = {.node_id = endpoints[i].inst.node_id, .company_id = 0xffff};
|
||||
@@ -357,9 +371,7 @@ int main(void) {
|
||||
for (int i = 0; i < 2; i++) {
|
||||
ble_link_id id = backends[i]->links[0].id;
|
||||
assert(!ble_tx_ready(backends[i]->owner, id));
|
||||
struct ll_entry* refused = ll_alloc_lldgram(1); assert(refused); refused->len = 1;
|
||||
assert(ble_tx_put(backends[i]->owner, id, refused) == BLE_WOULD_BLOCK && !refused->owner);
|
||||
queue_dgram_free(refused); queue_entry_free(refused);
|
||||
assert(!etcp_link_io_ready(endpoints[i].conn->links)); /* Без отказа put, который мог бы замаскировать потерю wake. */
|
||||
}
|
||||
int before = fragments; drain(); assert(before == fragments); /* Нет busy loop, когда GATT занят. */
|
||||
if (n == 1) drop = 2; /* Локальный send успешен, но DATA-фрагмент теряется: восстанавливает ETCP. */
|
||||
|
||||
@@ -315,16 +315,14 @@ object BleRadio {
|
||||
BluetoothDevice.PHY_LE_CODED -> "CODED"; else -> "unknown($phy)" }
|
||||
log(if (status == 0) "INFO" else "WARN", "$role PHY address=$address tx=${name(txPhy)} rx=${name(rxPhy)} status=$status")
|
||||
}
|
||||
// 2M — запрос предпочтения. S2/S8 относятся только к CODED PHY, здесь они не применяются.
|
||||
// 1M: на проверенной Android/Linux паре 2M замедляет очередь контроллера. Результат проверяется callbacks.
|
||||
private fun preferFastPhy(device: BluetoothDevice, gatt: BluetoothGatt?) {
|
||||
try {
|
||||
if (adapter?.isLe2MPhySupported == true) {
|
||||
if (gatt != null) gatt.setPreferredPhy(BluetoothDevice.PHY_LE_2M_MASK,
|
||||
BluetoothDevice.PHY_LE_2M_MASK, BluetoothDevice.PHY_OPTION_NO_PREFERRED)
|
||||
else checkNotNull(server).setPreferredPhy(device, BluetoothDevice.PHY_LE_2M_MASK,
|
||||
BluetoothDevice.PHY_LE_2M_MASK, BluetoothDevice.PHY_OPTION_NO_PREFERRED)
|
||||
log("INFO", "PHY 2M requested address=${device.address} outgoing=${gatt != null}")
|
||||
} else log("INFO", "Local controller has no PHY 2M; retaining negotiated PHY address=${device.address}")
|
||||
if (gatt != null) gatt.setPreferredPhy(BluetoothDevice.PHY_LE_1M_MASK,
|
||||
BluetoothDevice.PHY_LE_1M_MASK, BluetoothDevice.PHY_OPTION_NO_PREFERRED)
|
||||
else checkNotNull(server).setPreferredPhy(device, BluetoothDevice.PHY_LE_1M_MASK,
|
||||
BluetoothDevice.PHY_LE_1M_MASK, BluetoothDevice.PHY_OPTION_NO_PREFERRED)
|
||||
log("INFO", "PHY 1M requested address=${device.address} outgoing=${gatt != null}")
|
||||
if (gatt != null) gatt.readPhy() else checkNotNull(server).readPhy(device)
|
||||
} catch (e: RuntimeException) { log("WARN", "PHY tuning unavailable address=${device.address}: ${e.message}") }
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
#include "bluetoothsettingspage.h"
|
||||
#include "databasesettingspage.h"
|
||||
#include "statuspage.h"
|
||||
#include "nodespage.h"
|
||||
#include "soundsettingspage.h"
|
||||
#include "audiodevicesettingspage.h"
|
||||
#include "storagesettingspage.h"
|
||||
@@ -13,7 +12,6 @@
|
||||
#include "avatars/avatarmanager.h"
|
||||
#include "avatars/avatarcrop.h"
|
||||
#include "ptt_hotkey_manager.h"
|
||||
#include "connmonitorwindow.h"
|
||||
#include "../transport/gui_bridge.h"
|
||||
#include "../db/db_manager.h"
|
||||
#include "../transport/utun_node.h"
|
||||
@@ -69,10 +67,8 @@ SettingsDialog::SettingsDialog(const QString& configPath, DbManager* db, UtunNod
|
||||
m_categoryList->addItem(QCoreApplication::translate("ChatGui", "Profile"));
|
||||
m_categoryList->addItem(QCoreApplication::translate("ChatGui", "Storage"));
|
||||
m_categoryList->addItem(QCoreApplication::translate("ChatGui", "Network"));
|
||||
m_categoryList->addItem(QCoreApplication::translate("ChatGui", "Connections"));
|
||||
m_categoryList->addItem(QCoreApplication::translate("ChatGui", "Database"));
|
||||
m_categoryList->addItem(QCoreApplication::translate("ChatGui", "Status"));
|
||||
m_categoryList->addItem(QCoreApplication::translate("ChatGui", "Nodes"));
|
||||
m_categoryList->addItem(QCoreApplication::translate("ChatGui", "Sound"));
|
||||
m_categoryList->addItem(QCoreApplication::translate("ChatGui", "Audio"));
|
||||
m_categoryList->addItem(QCoreApplication::translate("ChatGui", "Radio"));
|
||||
@@ -203,22 +199,6 @@ SettingsDialog::SettingsDialog(const QString& configPath, DbManager* db, UtunNod
|
||||
m_networkPage = new NetworkSettingsPage(m_pages);
|
||||
m_pages->addWidget(m_networkPage);
|
||||
|
||||
// Connections page — button to open standalone window
|
||||
{
|
||||
auto* connPage = new QWidget(m_pages);
|
||||
auto* connLayout = new QVBoxLayout(connPage);
|
||||
connLayout->setAlignment(Qt::AlignCenter);
|
||||
auto* hintLabel = new QLabel(QCoreApplication::translate("ChatGui", "Live ETCP connection monitor\nwith per-link BBR statistics"), connPage);
|
||||
hintLabel->setAlignment(Qt::AlignCenter);
|
||||
hintLabel->setStyleSheet("font-size: 12px; color: palette(text); padding: 20px;");
|
||||
connLayout->addWidget(hintLabel);
|
||||
auto* openBtn = new QPushButton(QCoreApplication::translate("ChatGui", "Open Connection Monitor"), connPage);
|
||||
openBtn->setStyleSheet("QPushButton { padding: 10px 20px; font-size: 13px; }");
|
||||
connect(openBtn, &QPushButton::clicked, this, [] { ConnMonitorWindow::showMonitor(); });
|
||||
connLayout->addWidget(openBtn);
|
||||
m_pages->addWidget(connPage);
|
||||
}
|
||||
|
||||
// Database page
|
||||
m_databasePage = new DatabaseSettingsPage(db, m_pages);
|
||||
m_pages->addWidget(m_databasePage);
|
||||
@@ -227,10 +207,6 @@ SettingsDialog::SettingsDialog(const QString& configPath, DbManager* db, UtunNod
|
||||
m_statusPage = new StatusPage(node, m_pages);
|
||||
m_pages->addWidget(m_statusPage);
|
||||
|
||||
// Nodes page
|
||||
m_nodesPage = new NodesPage(db, m_pages);
|
||||
m_pages->addWidget(m_nodesPage);
|
||||
|
||||
m_soundPage = new SoundSettingsPage(db, m_pages);
|
||||
m_pages->addWidget(m_soundPage);
|
||||
|
||||
@@ -340,9 +316,8 @@ void SettingsDialog::onCategoryChanged(int row) {
|
||||
m_pages->setCurrentIndex(row);
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_CONFIG, "Settings: selected page=%d; draft retained", row);
|
||||
// При переключении обновляем только диагностические страницы.
|
||||
if (row == 4) m_databasePage->refreshStatus();
|
||||
if (row == 5) m_statusPage->refreshStatus();
|
||||
if (row == 6) m_nodesPage->refreshNodes();
|
||||
if (m_pages->currentWidget() == m_databasePage) m_databasePage->refreshStatus();
|
||||
if (m_pages->currentWidget() == m_statusPage) m_statusPage->refreshStatus();
|
||||
}
|
||||
|
||||
void SettingsDialog::onSave() {
|
||||
|
||||
@@ -18,7 +18,6 @@ class BluetoothSettingsPage;
|
||||
class NetworkSettingsPage;
|
||||
class DatabaseSettingsPage;
|
||||
class StatusPage;
|
||||
class NodesPage;
|
||||
class SoundSettingsPage;
|
||||
class AudioDeviceSettingsPage;
|
||||
class StorageSettingsPage;
|
||||
@@ -36,7 +35,7 @@ public:
|
||||
AudioRecorder* recorder, QWidget* parent = nullptr);
|
||||
|
||||
private slots:
|
||||
void onCategoryChanged(int row); // Выбрать страницу; DB/status/nodes обновляются, редактируемый черновик сохраняется.
|
||||
void onCategoryChanged(int row); // Выбрать страницу; DB/status обновляются, редактируемый черновик сохраняется.
|
||||
/* Пройти запись/применение страниц и вызвать accept при завершении UI-шагов.
|
||||
* Команды владеют снимками значений; accept не подтверждает commit SQLite/применение ядром. Общей транзакции нет. */
|
||||
void onSave();
|
||||
@@ -62,7 +61,6 @@ private:
|
||||
NetworkSettingsPage* m_networkPage; // Черновик listen-сокетов и LAN-флагов.
|
||||
DatabaseSettingsPage* m_databasePage; // Сведения/обслуживание SQLite, обновление при выборе категории.
|
||||
StatusPage* m_statusPage; // Диагностика ядра/времени, обновление при выборе категории.
|
||||
NodesPage* m_nodesPage; // Снимок известных узлов БД, обновление при выборе категории.
|
||||
SoundSettingsPage* m_soundPage; // Черновик звуков событий.
|
||||
AudioDeviceSettingsPage* m_audioDevicePage; // Выбор устройств/аудиопараметров и тесты микрофона/динамика.
|
||||
StorageSettingsPage* m_storagePage; // Черновик автозагрузки и лимитов media-хранилища.
|
||||
|
||||
@@ -112,7 +112,7 @@ int main(int argc, char** argv) {
|
||||
auto incoming = [&](uint32_t timestamp) {
|
||||
for (auto& sample : remote) { rng = rng * 1664525u + 1013904223u; sample = int(rng >> 18) - 8192; }
|
||||
uint8_t opus[256]; int len = opus_codec_encode(remote_encoder, remote.data(), Samples, opus, sizeof(opus)); CHECK(len > 0);
|
||||
call_audio_on_media(&inst, 42, seq++, timestamp, opus, len, nullptr);
|
||||
call_audio_on_media(&inst, 42, seq++, timestamp, opus, len, nullptr, 1);
|
||||
};
|
||||
incoming(0); incoming(0); // Настоящий стартовый запас FIFO, приватное состояние не подменяется.
|
||||
for (unsigned frame = 0; frame < Total; ++frame) {
|
||||
|
||||
@@ -64,10 +64,10 @@ int main(int argc, char** argv) {
|
||||
int16_t pcm[960] = {0}; uint8_t packet[256];
|
||||
int length = opus_codec_encode(encoder, pcm, 960, packet, sizeof(packet)); CHECK_AUDIO(length > 0);
|
||||
struct UTUN_INSTANCE foreign = {0};
|
||||
call_audio_on_media(&foreign, 43, 0, 0, packet, length, NULL);
|
||||
call_audio_on_media(&foreign, 43, 0, 0, packet, length, NULL, 1);
|
||||
struct call_jitter_diagnostics diagnostic = {0};
|
||||
CHECK_AUDIO(!call_audio_get_diagnostics(43, &diagnostic) && diagnostic.encoded_ms == 0);
|
||||
for (unsigned i = 0; i < 3; ++i) provider->on_media(inst, 43, i, i * 20, packet, length, NULL);
|
||||
for (unsigned i = 0; i < 3; ++i) provider->on_media(inst, 43, i, i * 20, packet, length, NULL, 1);
|
||||
CHECK_AUDIO(!call_audio_get_diagnostics(43, &diagnostic) && diagnostic.encoded_ms == 60);
|
||||
opus_codec_encoder_destroy(encoder);
|
||||
call_audio_stop(); call_audio_destroy(inst);
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
extern "C" {
|
||||
#include "utun_instance.h"
|
||||
#include "call/call_audio.h"
|
||||
#include "chat/chat_setting.h"
|
||||
#include "opus_codec.h"
|
||||
#include "debug_config.h"
|
||||
#include "u_async.h"
|
||||
#include "mem.h"
|
||||
@@ -24,15 +26,19 @@ static std::array<uint8_t, CALL_AUDIO_FRAME_SAMPLES> mask{};
|
||||
static std::atomic<uint64_t> test_time{100000};
|
||||
static unsigned sent_new, sent_old, extra_new, expected_extra, extra_first = 8;
|
||||
static std::vector<std::string> messages;
|
||||
static std::vector<uint8_t> last_packet;
|
||||
|
||||
extern "C" uint64_t __wrap_get_time_tb() { return test_time.load(); }
|
||||
extern "C" int __wrap_chat_setting_get_int(UTUN_INSTANCE*, const char* key, int fallback) {
|
||||
return !strcmp(key, "call_compressor_enabled") || !strcmp(key, "aec_enabled") ? 0 : fallback;
|
||||
extern "C" int __real_chat_setting_get_int(UTUN_INSTANCE*, const char*, int);
|
||||
extern "C" int __wrap_chat_setting_get_int(UTUN_INSTANCE* inst, const char* key, int fallback) {
|
||||
return !strcmp(key, "call_compressor_enabled") || !strcmp(key, "aec_enabled") ? 0
|
||||
: __real_chat_setting_get_int(inst, key, fallback);
|
||||
}
|
||||
extern "C" int __wrap_call_send_media(UTUN_INSTANCE*, uint64_t id, const uint8_t* data, int len,
|
||||
const call_capture_time* timing) {
|
||||
REQUIRE(data && len > 0 && timing);
|
||||
REQUIRE(id == old_id || id == new_id);
|
||||
last_packet.assign(data, data + len);
|
||||
if (id == new_id) ++sent_new; else ++sent_old;
|
||||
return 0;
|
||||
}
|
||||
@@ -69,6 +75,40 @@ static void drain(UASYNC* ua) {
|
||||
uasync_poll(ua, 0); /* Tasks, опубликованные из extra, исполняются следующим проходом. */
|
||||
}
|
||||
|
||||
/* Сравнение настоящих Opus-пакетов с независимым encoder: сохранённая настройка,
|
||||
* изменение во время звонка и новый звонок без повторного init frontend. */
|
||||
static void check_codec_setting(UTUN_INSTANCE& inst) {
|
||||
REQUIRE(chat_setting_set(&inst, "opus_codec_preset", "0") == 0);
|
||||
begin(inst, new_id);
|
||||
auto* reference = opus_codec_encoder_create(48000, 1); REQUIRE(reference);
|
||||
unsigned frame = 0; uint32_t noise = 1;
|
||||
for (int preset : {0, 2, 1, 0}) {
|
||||
if (frame) REQUIRE(chat_setting_set(&inst, "opus_codec_preset", std::to_string(preset).c_str()) == 0);
|
||||
REQUIRE(opus_codec_encoder_bitrate_set(reference, 16000 << preset) == 0);
|
||||
REQUIRE(chat_setting_set(&inst, "opus_codec_preset", "3") == -2);
|
||||
for (unsigned i = 0; i < 12; ++i, ++frame) {
|
||||
for (auto& sample : pcm) { noise = noise * 1664525u + 1013904223u; sample = int16_t(noise >> 16) / 4; }
|
||||
uint8_t expected[256];
|
||||
int size = opus_codec_encode(reference, pcm.data(), pcm.size(), expected, sizeof(expected)); REQUIRE(size > 0);
|
||||
REQUIRE(feed(new_id, frame, 1) == 1); drain(inst.ua);
|
||||
REQUIRE(last_packet.size() == size && !memcmp(last_packet.data(), expected, size));
|
||||
}
|
||||
}
|
||||
opus_codec_encoder_destroy(reference); call_audio_stop();
|
||||
REQUIRE(chat_setting_set(&inst, "opus_codec_preset", "2") == 0);
|
||||
REQUIRE(call_audio_start_prepared(new_id) == 0);
|
||||
reference = opus_codec_encoder_create(48000, 1); REQUIRE(reference);
|
||||
REQUIRE(opus_codec_encoder_bitrate_set(reference, 64000) == 0);
|
||||
uint8_t expected[256];
|
||||
int size = opus_codec_encode(reference, pcm.data(), pcm.size(), expected, sizeof(expected)); REQUIRE(size > 0);
|
||||
REQUIRE(feed(new_id, 0, 1) == 1); drain(inst.ua);
|
||||
REQUIRE(last_packet.size() == size && !memcmp(last_packet.data(), expected, size));
|
||||
opus_codec_encoder_destroy(reference); unbind(inst);
|
||||
REQUIRE(chat_setting_set(&inst, "opus_codec_preset", "1") == 0);
|
||||
pcm.fill(0); sent_new = sent_old = 0;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "call-audio-tx: saved/live/restarted codec 16/32/64 kbit/s packets match reference PASS");
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
debug_config_init();
|
||||
debug_set_level(DEBUG_LEVEL_ERROR);
|
||||
@@ -102,6 +142,7 @@ int main(int argc, char** argv) {
|
||||
|
||||
UASYNC* ua = uasync_create(); REQUIRE(ua);
|
||||
inst.ua = other.ua = ua;
|
||||
if (!strcmp(mode, "fresh")) check_codec_setting(inst);
|
||||
unsigned initial_new = 0, expected_initial = 8;
|
||||
if (!strcmp(mode, "fresh") || !strcmp(mode, "idempotent")) {
|
||||
uasync_post(ua, extra, nullptr);
|
||||
|
||||
@@ -28,7 +28,7 @@ static int decode_bad(void* arg, const uint8_t*, int, int16_t*, int) {
|
||||
return -1;
|
||||
}
|
||||
static void push(call_jitter* j, int id, int64_t received_ms = -1) {
|
||||
call_jitter_push(j, (const uint8_t*)&id, sizeof(id), (uint32_t)(id * 20), received_ms < 0 ? id * 20 : received_ms, nullptr);
|
||||
call_jitter_push(j, (const uint8_t*)&id, sizeof(id), (uint32_t)(id * 20), received_ms < 0 ? id * 20 : received_ms, nullptr, 1);
|
||||
}
|
||||
|
||||
static void test_network_recovery() {
|
||||
@@ -52,7 +52,7 @@ static void test_network_recovery() {
|
||||
}
|
||||
CHECK(largest_reserve >= 60 && state.reserve_ms == 20);
|
||||
CHECK(state.delay_mode == 0 && state.phase_samples == 0 && state.tempo_ppm == 1000000);
|
||||
CHECK(state.underruns > 0 && state.underruns <= 2);
|
||||
CHECK(state.underruns <= 2);
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "recovery block=%d max_reserve=%dms final=%dms phase=%u underruns=%u",
|
||||
block, largest_reserve, state.reserve_ms, state.phase_samples, state.underruns);
|
||||
call_jitter_destroy(j);
|
||||
@@ -79,7 +79,7 @@ static void test_audio_clock() {
|
||||
for (int s = 0; s < 960; s++) voice[s] = (int16_t)(8000 * std::sin((sent * 960ll + s) * 0.09));
|
||||
int len = opus_codec_encode(encoder, voice, 960, packet, sizeof(packet)); CHECK(len > 0);
|
||||
call_capture_time timing{1000000 + sent * 20000ll, 9, (uint32_t)delay};
|
||||
call_audio_on_media(&audio_owner, id, (uint16_t)sent, 0, packet, len, &timing);
|
||||
call_audio_on_media(&audio_owner, id, (uint16_t)sent, 0, packet, len, &timing, 1);
|
||||
}
|
||||
CHECK(call_audio_pull_pcm(id, output, 480) == 480); written += 480;
|
||||
int64_t now = tick * 10000ll;
|
||||
@@ -115,7 +115,26 @@ static void test_audio_clock() {
|
||||
CHECK(call_audio_try_clock(id, 43000000, &clock) == -1 && !clock.valid);
|
||||
std::printf("audio clock: real Opus, burst/tempo, device progress, local tones and reset checked\n");
|
||||
}
|
||||
static void test_packet_arrival_sampling() {
|
||||
decoder d; auto *j = call_jitter_create(decode, &d, 1000);
|
||||
for (unsigned packet = 0; packet < 4; packet++) {
|
||||
for (int frame = 0; frame < 3; frame++) {
|
||||
int id = packet * 3 + frame;
|
||||
call_jitter_push(j, (const uint8_t*)&id, sizeof(id), packet * 60,
|
||||
1000 + packet * 60 + frame, nullptr, frame == 0);
|
||||
}
|
||||
}
|
||||
call_jitter_diagnostics diag{}; call_jitter_get_diagnostics(j, &diag, 0);
|
||||
CHECK(diag.arrival_spread_ms == 0);
|
||||
int id = 12;
|
||||
call_jitter_push(j, (const uint8_t*)&id, sizeof(id), 240, 1400, nullptr, 1);
|
||||
call_jitter_get_diagnostics(j, &diag, 0);
|
||||
CHECK(diag.arrival_spread_ms == 160);
|
||||
call_jitter_destroy(j);
|
||||
}
|
||||
|
||||
int main() {
|
||||
test_packet_arrival_sampling();
|
||||
CHECK(call_audio_init(&audio_owner) == 0);
|
||||
{
|
||||
call_jitter_window w;
|
||||
@@ -160,10 +179,15 @@ int main() {
|
||||
decoder d;
|
||||
auto* j = call_jitter_create(decode, &d, 1000);
|
||||
CHECK(j);
|
||||
push(j, 0); // Сохраняем запас 20мс после первого блока, а не расходуем его при старте.
|
||||
CHECK(call_jitter_pull(j, pcm, 480) == 0);
|
||||
push(j, 1);
|
||||
CHECK(call_jitter_pull(j, pcm, 480) > 0);
|
||||
for (int i = 0; i < 9; i++) {
|
||||
push(j, i);
|
||||
CHECK(call_jitter_pull(j, pcm, 480) == 0);
|
||||
CHECK(d.seen.empty()); // До200мс не декодируем и не расходуем накопленный звук.
|
||||
}
|
||||
call_jitter_get_stats(j, &depth, nullptr, nullptr, &under, nullptr);
|
||||
CHECK(depth == 180 && under == 0);
|
||||
push(j, 9);
|
||||
CHECK(call_jitter_pull(j, pcm, 480) == 480);
|
||||
call_jitter_diagnostics diagnostic{};
|
||||
call_jitter_get_diagnostics(j, &diagnostic, 0);
|
||||
CHECK(diagnostic.resumed == 1 && diagnostic.pcm_samples == 480 && diagnostic.reserve_ms == 20);
|
||||
@@ -175,13 +199,13 @@ int main() {
|
||||
call_jitter_get_diagnostics(j, &diagnostic, 0);
|
||||
CHECK(diagnostic.resumed == 0);
|
||||
call_jitter_range range;
|
||||
push(j, 2);
|
||||
for (int i = 3; i < 503; i++) push(j, i, 10060); // 10с сетевой задержки, затем пачка
|
||||
push(j, 10);
|
||||
for (int i = 11; i < 511; i++) push(j, i, 10220); // 10с сетевой задержки, затем пачка
|
||||
call_jitter_get_stats(j, nullptr, nullptr, nullptr, nullptr, &range);
|
||||
CHECK(range.reserve_ms == 1000);
|
||||
int next = 503, highest = 100;
|
||||
int next = 511, highest = 100;
|
||||
for (int tick = 0; tick < 6000; tick++) {
|
||||
if (tick % 2 == 0) push(j, next++, 10060 + tick * 10); // непрерывный live
|
||||
if (tick % 2 == 0) push(j, next++, 10220 + tick * 10); // непрерывный live
|
||||
call_jitter_pull(j, pcm, 480);
|
||||
call_jitter_get_stats(j, &depth, &tempo, &dropped, &under, nullptr);
|
||||
if (tempo > highest) highest = tempo;
|
||||
@@ -298,10 +322,10 @@ int main() {
|
||||
for (auto value : pcm) CHECK(value == 0);
|
||||
}
|
||||
// Первый пакет ещё не заполнил запас PCM: продолжается тихое ожидание.
|
||||
call_audio_on_media(&audio_owner, id, 0, 0, encoded, len, nullptr);
|
||||
call_audio_on_media(&audio_owner, id, 0, 0, encoded, len, nullptr, 1);
|
||||
CHECK(call_audio_pull_pcm(id, pcm, 480) == 480);
|
||||
for (auto value : pcm) CHECK(value == 0);
|
||||
for (int i = 1; i < 6; i++) call_audio_on_media(&audio_owner, id, i, 0, encoded, len, nullptr);
|
||||
for (int i = 1; i < 10; i++) call_audio_on_media(&audio_owner, id, i, 0, encoded, len, nullptr, 1);
|
||||
CHECK(call_audio_pull_pcm(id, pcm, 480) == 480);
|
||||
bool audible = false;
|
||||
for (auto value : pcm) if (value) audible = true;
|
||||
@@ -314,14 +338,14 @@ int main() {
|
||||
}
|
||||
CHECK(audible);
|
||||
// Малого запаса недостаточно для восстановления: сигнал продолжает звучать.
|
||||
call_audio_on_media(&audio_owner, id, 6, 0, encoded, len, nullptr);
|
||||
call_audio_on_media(&audio_owner, id, 10, 0, encoded, len, nullptr, 1);
|
||||
audible = false;
|
||||
for (int tick = 0; tick < 100; tick++) {
|
||||
CHECK(call_audio_pull_pcm(id, pcm, 480) == 480);
|
||||
for (auto value : pcm) if (std::abs((int)value) > 1000) audible = true;
|
||||
}
|
||||
CHECK(audible);
|
||||
for (int i = 7; i < 30; i++) call_audio_on_media(&audio_owner, id, i, 0, encoded, len, nullptr);
|
||||
for (int i = 11; i < 34; i++) call_audio_on_media(&audio_owner, id, i, 0, encoded, len, nullptr, 1);
|
||||
CHECK(call_audio_pull_pcm(id, pcm, 480) == 480);
|
||||
call_audio_begin_end(id); // явное завершение прекращает догон и включает финальный тон
|
||||
CHECK(call_audio_pull_pcm(id, pcm, 480) > 0);
|
||||
@@ -337,7 +361,7 @@ int main() {
|
||||
uint8_t encoded[512];
|
||||
int len = opus_codec_encode(encoder, silence, 960, encoded, sizeof(encoded));
|
||||
CHECK(len > 0);
|
||||
for (int i = 0; i < 6; i++) call_audio_on_media(&audio_owner, 124, i, 0, encoded, len, nullptr);
|
||||
for (int i = 0; i < 10; i++) call_audio_on_media(&audio_owner, 124, i, 0, encoded, len, nullptr, 1);
|
||||
std::vector<int16_t> output(777);
|
||||
under = 0;
|
||||
for (int tick = 0; tick < 30 && !under; tick++) {
|
||||
|
||||
Reference in new issue
Block a user