From 913ae531ec2263d0be0da68decc3107ea8a4149b Mon Sep 17 00:00:00 2001 From: evgeny Date: Mon, 26 Oct 2026 15:51:35 +0100 Subject: [PATCH] Queue complete headless PCM frames and account for overflow drops --- src/call/call_headless.c | 45 +++++++++++++++++++++++++++++----------- src/call/call_headless.h | 4 ++-- 2 files changed, 35 insertions(+), 14 deletions(-) diff --git a/src/call/call_headless.c b/src/call/call_headless.c index a2c76673..235dd5ea 100644 --- a/src/call/call_headless.c +++ b/src/call/call_headless.c @@ -42,6 +42,8 @@ struct call_audio_client { uint8_t send_buf[AUDIO_SEND_BUF_SIZE]; int send_len; int send_offset; + uint32_t pcm_dropped; /* Целые исходящие PCM-кадры, отброшенные при переполнении. */ + uint64_t drop_log_tb; /* Ограничение частоты WARN: не чаще раза в секунду. */ int closing; struct call_audio* ca; struct call_audio_client* next; @@ -66,6 +68,7 @@ struct call_audio { uint32_t c_dec_frames; uint32_t c_enc_dropped; uint32_t c_dec_dropped; + uint32_t c_pcm_dropped; }; static struct call_audio* ca_of(struct UTUN_INSTANCE* inst) { @@ -76,12 +79,24 @@ static void ca_close_client(struct call_audio_client* cli); /* ── отправка фрейма клиенту (с буферизацией как в control-сокете) ── */ +/* Принимает целый ограниченный фрейм; short write сохраняет его остаток перед следующими кадрами. */ static void ca_send_raw(struct call_audio_client* cli, const uint8_t* data, size_t len) { - struct call_audio* ca = cli->ca; if (!cli || cli->closing || cli->fd == SOCKET_INVALID || !data || len == 0) return; + struct call_audio* ca = cli->ca; if (cli->send_len > 0) { size_t space = sizeof(cli->send_buf) - (size_t)cli->send_len; - if (len > space) return; + if (len > space) { + ++cli->pcm_dropped; ++ca->c_pcm_dropped; + uint64_t now = get_time_tb(); + if (cli->pcm_dropped == 1 || now - cli->drop_log_tb >= 10000) { + cli->drop_log_tb = now; + DEBUG_WARN(DEBUG_CATEGORY_CALL, + "%s: PCM frame dropped call=%016llx fd=%d frame=%zu queued=%d offset=%d capacity=%zu dropped=%u total=%u", + CH_ID, (unsigned long long)cli->call_id, (int)cli->fd, len, cli->send_len, cli->send_offset, + sizeof(cli->send_buf), cli->pcm_dropped, ca->c_pcm_dropped); + } + return; + } memcpy(cli->send_buf + cli->send_len, data, len); cli->send_len += (int)len; return; @@ -105,13 +120,18 @@ static void ca_send_raw(struct call_audio_client* cli, const uint8_t* data, size static void ca_write_frame(struct call_audio_client* cli, uint8_t type, uint64_t call_id, const uint8_t* data, uint16_t len) { - uint8_t hdr[CALL_AUDIO_HDR_SIZE]; - hdr[0] = type; - memcpy(hdr + 1, &call_id, 8); - hdr[9] = (uint8_t)(len >> 8); - hdr[10] = (uint8_t)(len & 0xFF); - ca_send_raw(cli, hdr, sizeof(hdr)); - if (len) ca_send_raw(cli, data, len); + if (len > CA_PCM_FRAME_BYTES || (len && !data)) { + DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: invalid output frame call=%016llx type=%u len=%u data=%p", + CH_ID, (unsigned long long)call_id, type, len, (const void*)data); + return; + } + uint8_t frame[CALL_AUDIO_HDR_SIZE + CA_PCM_FRAME_BYTES]; + frame[0] = type; + memcpy(frame + 1, &call_id, 8); + frame[9] = (uint8_t)(len >> 8); + frame[10] = (uint8_t)(len & 0xFF); + if (len) memcpy(frame + CALL_AUDIO_HDR_SIZE, data, len); + ca_send_raw(cli, frame, CALL_AUDIO_HDR_SIZE + len); } /* ── Opus-кодеки (одна активная звонка за раз) ── */ @@ -249,7 +269,8 @@ static void ca_close_client(struct call_audio_client* cli) { cli->closing = 1; if (ca && ca->ua && cli->socket_id) uasync_remove_socket_t(ca->ua, cli->fd); if (cli->fd != SOCKET_INVALID) { socket_close_wrapper(cli->fd); cli->fd = SOCKET_INVALID; } - DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: audio client closed call_id=%016llx", CH_ID, (unsigned long long)cli->call_id); + DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: audio client closed call_id=%016llx pcm_drop=%u", + CH_ID, (unsigned long long)cli->call_id, cli->pcm_dropped); } static void ca_read_callback(socket_t fd, void* arg) { @@ -381,7 +402,7 @@ void call_headless_destroy(struct UTUN_INSTANCE* inst) { c = n; } ca->clients = NULL; ca->client_count = 0; - DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: audio socket destroyed (enc=%u dec=%u enc_drop=%u dec_drop=%u)", - CH_ID, ca->c_enc_frames, ca->c_dec_frames, ca->c_enc_dropped, ca->c_dec_dropped); + DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: audio socket destroyed (enc=%u dec=%u enc_drop=%u dec_drop=%u pcm_drop=%u)", + CH_ID, ca->c_enc_frames, ca->c_dec_frames, ca->c_enc_dropped, ca->c_dec_dropped, ca->c_pcm_dropped); inst->call_audio = NULL; u_free(ca); } diff --git a/src/call/call_headless.h b/src/call/call_headless.h index 1fad4bdf..033ee63d 100644 --- a/src/call/call_headless.h +++ b/src/call/call_headless.h @@ -14,8 +14,8 @@ // call_id хранится в порядке байтов хоста, len — старший байт первым; PCM — native int16_t. // Это отдельный сервис с кодеками instance, без DSP/jitter движка call_audio.h. // Все вызовы и callbacks выполняются в потоке ua; клиент обеспечивает темп и воспроизведение. -// Известный дефект 26.10.2026: переполнение исходящей очереди может оставить заголовок -// без PCM и нарушить фрейминг. Аудит/план исправления — doc/call_audit_findings.txt,раздел11. +// Исходящая очередь принимает header+PCM целиком; при переполнении отбрасывается целый кадр с WARN/счётчиком. +// Частичная отправка продолжается перед следующим кадром. Размер очереди ограничен, темп чтения задаёт клиент. #ifndef CALL_HEADLESS_H #define CALL_HEADLESS_H