diff --git a/src/call/call_audio.c b/src/call/call_audio.c index 991425b4..ccd4af45 100644 --- a/src/call/call_audio.c +++ b/src/call/call_audio.c @@ -48,6 +48,7 @@ static int g_prepared_capture = 0; static int g_aec_enabled = 1; /* по умолчанию: настройка aec_enabled */ static struct audio_compressor* g_compressor = NULL; static uint64_t g_tx_generation; +static uint64_t g_instance_generation; /* Эпоха привязки; смена звонка её не меняет. */ static uint64_t g_capture_epoch; static int64_t g_capture_end = INT64_MIN; static int g_codec_delay; @@ -69,6 +70,7 @@ struct call_audio_tx { struct posted_task task; struct UTUN_INSTANCE* inst; uint64_t call_id, generation, created_tb; + uint64_t instance_generation; int len; struct call_capture_time timing; uint8_t opus[256]; @@ -77,9 +79,20 @@ struct call_audio_tx { static void call_audio_send(void* arg) { struct call_audio_tx* tx = arg; pthread_mutex_lock(&g_mtx); - int current = tx->generation == g_tx_generation; + int owner = tx->instance_generation == g_instance_generation && g_inst == tx->inst; + int current = owner && tx->generation == g_tx_generation; if (current) call_audio_duration_add(&g_post_time, (get_time_tb() - tx->created_tb) * 100); - if (g_tx_pending) --g_tx_pending; + /* Старый звонок той же привязки всё ещё занимает свой слот очереди. */ + if (owner) { + if (g_tx_pending) --g_tx_pending; + else DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: TX pending underflow call=%llu instance_epoch=%llu", CALL_AUDIO_ID, + (unsigned long long)tx->call_id, (unsigned long long)tx->instance_generation); + } + if (!current) DEBUG_TRACE(DEBUG_CATEGORY_CALL, + "%s: stale TX call=%llu owner=%d instance_epoch=%llu/%llu call_epoch=%llu/%llu pending=%u", CALL_AUDIO_ID, + (unsigned long long)tx->call_id, owner, (unsigned long long)tx->instance_generation, + (unsigned long long)g_instance_generation, (unsigned long long)tx->generation, + (unsigned long long)g_tx_generation, g_tx_pending); int send = current && g_active && !g_ending && g_inst == tx->inst && g_call_id == tx->call_id; if (get_time_tb() - tx->created_tb > 2000) { send = 0; @@ -104,13 +117,16 @@ static int call_audio_decode_cb(void* arg, const uint8_t* enc, int len, int16_t* } int call_audio_init(struct UTUN_INSTANCE* inst) { - if (!inst) return -1; + if (!inst) { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: init missing instance", CALL_AUDIO_ID); return -1; } pthread_mutex_lock(&g_mtx); if (g_inst == inst) { pthread_mutex_unlock(&g_mtx); return 0; } + unsigned int previous_pending = g_tx_pending; g_inst = inst; g_tx_pending = 0; + ++g_instance_generation; + DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: initialized inst=%p instance_epoch=%llu previous_pending=%u", CALL_AUDIO_ID, + (void*)inst, (unsigned long long)g_instance_generation, previous_pending); pthread_mutex_unlock(&g_mtx); call_set_audio_ops(inst, &g_ops); - DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: initialized", CALL_AUDIO_ID); return 0; } @@ -119,8 +135,10 @@ void call_audio_destroy(struct UTUN_INSTANCE* inst) { call_set_audio_ops(inst, NULL); pthread_mutex_lock(&g_mtx); g_inst = NULL; + ++g_instance_generation; + DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: destroyed inst=%p instance_epoch=%llu pending=%u", CALL_AUDIO_ID, + (void*)inst, (unsigned long long)g_instance_generation, g_tx_pending); pthread_mutex_unlock(&g_mtx); - DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: destroyed", CALL_AUDIO_ID); } /* ── uasync-поток: приём медиа (кладём кодированный кадр в jitter-буфер) ── */ @@ -452,6 +470,7 @@ static int call_audio_feed_impl(uint64_t call_id, const int16_t* pcm, int count, } task->inst = inst; task->call_id = call_id; task->generation = g_tx_generation; task->created_tb = get_time_tb(); + task->instance_generation = g_instance_generation; task->timing = (struct call_capture_time){capture_pts_us, g_capture_epoch, (uint32_t)g_codec_delay}; task->len = len; memcpy(task->opus, opus, (size_t)len); diff --git a/src/call/call_audio.h b/src/call/call_audio.h index 1b8e7504..0540a1b2 100644 --- a/src/call/call_audio.h +++ b/src/call/call_audio.h @@ -32,9 +32,11 @@ extern "C" { #define CALL_AUDIO_FRAME_SAMPLES (CALL_AUDIO_SAMPLE_RATE * CALL_AUDIO_FRAME_MS / 1000) /* 960 */ /* Зарегистрировать общий для процесса аудио-движок в call.c; повтор с тем же inst идемпотентен. - * Известный дефект 26.10.2026: destroy/init на живом uasync со старыми TX-задачами нарушает - * учёт лимита8 новых задач. Подробности и план изоляции эпох привязки — doc/call_audit_findings.txt,раздел6. */ + * init/destroy сериализованы lifecycle ядра; producers остановлены перед освобождением inst/ua. + * Лимит8 TX-задач относится к текущей привязке. Старые задачи не отправляют медиа и не меняют + * её счётчик/позднюю статистику; uasync освобождает их после callback либо при destroy. 0 = успех, -1 = нет inst. */ int call_audio_init(struct UTUN_INSTANCE* inst); +/* Снять регистрацию и инвалидировать TX-привязку; codec/jitter освобождаются отдельно через stop/release. */ void call_audio_destroy(struct UTUN_INSTANCE* inst); /* GUI-поток: создать codec + jitter + tones. Старый API принимает raw PCM и использует core AEC.