diff --git a/src/call/call_audio.c b/src/call/call_audio.c index 5edfbd27..c85b9e08 100644 --- a/src/call/call_audio.c +++ b/src/call/call_audio.c @@ -48,6 +48,9 @@ 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_capture_epoch; +static int64_t g_capture_end = INT64_MIN; +static int g_codec_delay; static unsigned int g_tx_pending, g_tx_dropped, g_tx_reported; static unsigned int g_tx_reasons; static int g_tx_encode_error; @@ -59,6 +62,7 @@ struct call_audio_tx { struct UTUN_INSTANCE* inst; uint64_t call_id, generation, created_tb; int len; + struct call_capture_time timing; uint8_t opus[256]; }; @@ -73,7 +77,7 @@ static void call_audio_send(void* arg) { if (current) { ++g_tx_dropped; g_tx_reasons |= 16; } } pthread_mutex_unlock(&g_mtx); - if (send) call_send_media(tx->inst, tx->call_id, tx->opus, tx->len); + if (send) call_send_media(tx->inst, tx->call_id, tx->opus, tx->len, &tx->timing); } static const struct call_audio_ops g_ops = { @@ -113,12 +117,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) { + uint16_t seq, uint32_t ts_ms, const uint8_t* opus, int len, const struct call_capture_time* timing) { (void)inst; (void)seq; pthread_mutex_lock(&g_mtx); if (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)); + call_jitter_push(g_vj, opus, len, ts_ms, (int64_t)(get_time_tb() / 10), timing); } pthread_mutex_unlock(&g_mtx); } @@ -151,6 +155,13 @@ static int call_audio_start_impl(uint64_t call_id, int prepared) { return -1; } opus_codec_encoder_bitrate_set(enc, CALL_AUDIO_BITRATE); + int codec_delay = 0; + if (opus_codec_encoder_delay(enc, &codec_delay) < 0) { + opus_codec_encoder_destroy(enc); + pthread_mutex_unlock(&g_mtx); + DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: encoder delay unavailable", CALL_AUDIO_ID); + return -1; + } opus_codec_decoder_t* dec = opus_codec_decoder_create(CALL_AUDIO_SAMPLE_RATE, 1); if (!dec) { @@ -218,6 +229,7 @@ static int call_audio_start_impl(uint64_t call_id, int prepared) { g_call_id = call_id; g_active = 1; ++g_tx_generation; g_tx_dropped = g_tx_reported = 0; + g_capture_epoch = 1; g_capture_end = INT64_MIN; g_codec_delay = codec_delay; g_tx_reasons = 0; g_tx_encode_error = 0; g_ending = 0; g_ended_started = 0; @@ -300,6 +312,7 @@ void call_audio_reset_io(uint64_t call_id, int playback) { if (g_active && g_call_id == call_id) { if (g_aec) speex_aec_reset(g_aec); if (!playback && g_compressor) audio_compressor_reset(g_compressor); + if (!playback) { ++g_capture_epoch; g_capture_end = INT64_MIN; } if (playback) { call_jitter_reset(g_vj); g_gap_samples = 0; @@ -353,8 +366,10 @@ void call_audio_set_aec_delay_frames(int frames) { /* ── аудио-поток: TX / RX ── */ -static int call_audio_feed_impl(uint64_t call_id, const int16_t* pcm, int count, int muted, const uint8_t* mute_mask, int prepared) { - if (!pcm || count != CALL_AUDIO_FRAME_SAMPLES || (prepared && !mute_mask)) { +static int call_audio_feed_impl(uint64_t call_id, const int16_t* pcm, int count, int muted, const uint8_t* mute_mask, + int prepared, int64_t capture_pts_us) { + if (!pcm || count != CALL_AUDIO_FRAME_SAMPLES || (prepared && !mute_mask) + || (capture_pts_us != INT64_MIN && (capture_pts_us < 0 || capture_pts_us > INT64_MAX / 2))) { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: invalid capture frame count=%d prepared=%d", CALL_AUDIO_ID, count, prepared); return -1; } @@ -375,6 +390,14 @@ static int call_audio_feed_impl(uint64_t call_id, const int16_t* pcm, int count, DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: capture policy mismatch prepared=%d", CALL_AUDIO_ID, prepared); return -1; } + if (capture_pts_us != INT64_MIN) { + if (g_capture_end != INT64_MIN && (capture_pts_us < g_capture_end - 2000 || capture_pts_us > g_capture_end + 2000)) { + ++g_capture_epoch; + DEBUG_WARN(DEBUG_CATEGORY_CALL, "call capture discontinuity epoch=%llu expected=%lld actual=%lld", + (unsigned long long)g_capture_epoch, (long long)g_capture_end, (long long)capture_pts_us); + } + g_capture_end = capture_pts_us + 20000; + } else g_capture_end = INT64_MIN; const int16_t* tx = pcm; if (g_aec) { if (speex_aec_process_capture(g_aec, pcm, count, cleaned) == count) @@ -415,6 +438,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->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); task->task.callback = call_audio_send; task->task.arg = task; @@ -425,16 +449,16 @@ static int call_audio_feed_impl(uint64_t call_id, const int16_t* pcm, int count, } /* Маска соответствует границам mute в том же упорядоченном потоке, что и PCM. */ -int call_audio_feed_prepared_pcm(uint64_t call_id, const int16_t* pcm, const uint8_t* mute_mask) { - return call_audio_feed_impl(call_id, pcm, CALL_AUDIO_FRAME_SAMPLES, 0, mute_mask, 1); +int call_audio_feed_prepared_pcm(uint64_t call_id, const int16_t* pcm, const uint8_t* mute_mask, int64_t capture_pts_us) { + return call_audio_feed_impl(call_id, pcm, CALL_AUDIO_FRAME_SAMPLES, 0, mute_mask, 1, capture_pts_us); } -int call_audio_feed_pcm_muted(uint64_t call_id, const int16_t* pcm, int count, int muted) { - return call_audio_feed_impl(call_id, pcm, count, muted, NULL, 0); +int call_audio_feed_pcm_muted(uint64_t call_id, const int16_t* pcm, int count, int muted, int64_t capture_pts_us) { + return call_audio_feed_impl(call_id, pcm, count, muted, NULL, 0, capture_pts_us); } /* Незамьюченный захват для платформ без программного mute. */ -int call_audio_feed_pcm(uint64_t call_id, const int16_t* pcm, int count) { - return call_audio_feed_impl(call_id, pcm, count, 0, NULL, 0); +int call_audio_feed_pcm(uint64_t call_id, const int16_t* pcm, int count, int64_t capture_pts_us) { + return call_audio_feed_impl(call_id, pcm, count, 0, NULL, 0, capture_pts_us); } int call_audio_pull_pcm(uint64_t call_id, int16_t* out, int max_samples) { @@ -468,6 +492,7 @@ int call_audio_pull_pcm(uint64_t call_id, int16_t* out, int max_samples) { } if (call_tones_ended_pending(tones)) n = call_tones_render(tones, out, max_samples); + call_jitter_silence(j, (unsigned)n); } else { n = call_jitter_pull(j, out, max_samples); int previous_gap = g_gap_samples; @@ -477,6 +502,7 @@ int call_audio_pull_pcm(uint64_t call_id, int16_t* out, int max_samples) { g_gap_samples = 0; } int missing = max_samples - n; + call_jitter_silence(j, (unsigned)missing); /* Подготовка устройств и начальное накопление PCM не являются потерей звука. */ int quiet = g_playback_started ? CALL_STALL_SAMPLES - g_gap_samples : missing; if (quiet > missing) quiet = missing; @@ -512,6 +538,23 @@ int call_audio_pull_pcm(uint64_t call_id, int16_t* out, int max_samples) { return n; } +void call_audio_playback_progress(uint64_t call_id, uint64_t epoch, uint64_t position, + int64_t at_us, int64_t now_us, int measured) { + pthread_mutex_lock(&g_mtx); + if (g_active && call_id == g_call_id && g_vj) call_jitter_progress(g_vj, epoch, position, at_us, now_us, measured); + pthread_mutex_unlock(&g_mtx); +} + +int call_audio_get_clock(uint64_t call_id, int64_t now_us, struct call_clock_snapshot* out) { + if (!out) { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "call clock missing output"); return -1; } + memset(out, 0, sizeof(*out)); out->pts_us = -1; + pthread_mutex_lock(&g_mtx); + int present = g_active && call_id == g_call_id && g_vj; + if (present) call_jitter_clock(g_vj, now_us, out); + pthread_mutex_unlock(&g_mtx); + return present ? 0 : -1; +} + /* ── uasync-поток: метрики ── */ int call_audio_get_stats(uint64_t call_id, int* buffer_ms, int* tempo_x100, diff --git a/src/call/call_audio.h b/src/call/call_audio.h index a74fdc1d..fdf7e949 100644 --- a/src/call/call_audio.h +++ b/src/call/call_audio.h @@ -51,11 +51,11 @@ void call_audio_release(uint64_t call_id); void call_audio_stop(void); /* аудио-поток: закодировать PCM-кадр (ровно CALL_AUDIO_FRAME_SAMPLES) и отправить пиру. */ -int call_audio_feed_pcm(uint64_t call_id, const int16_t* pcm, int count); +int call_audio_feed_pcm(uint64_t call_id, const int16_t* pcm, int count, int64_t capture_pts_us); /* Mute после AEC/AGC: фильтр получает настоящий микрофон, Opus — нулевой PCM. */ /* Подготовленный PCM и маска mute (960 элементов); AEC уже выполнен до этого API. */ -int call_audio_feed_prepared_pcm(uint64_t call_id, const int16_t* pcm, const uint8_t* mute_mask); -int call_audio_feed_pcm_muted(uint64_t call_id, const int16_t* pcm, int count, int muted); +int call_audio_feed_prepared_pcm(uint64_t call_id, const int16_t* pcm, const uint8_t* mute_mask, int64_t capture_pts_us); +int call_audio_feed_pcm_muted(uint64_t call_id, const int16_t* pcm, int count, int muted, int64_t capture_pts_us); /* аудио-поток: выдать до max_samples финального PCM (jitter + stretch + тоны). * Возвращает число сэмплов (может быть < max — андерфлоу/конец тона). */ @@ -64,7 +64,14 @@ int call_audio_pull_pcm(uint64_t call_id, int16_t* out, int max_samples); /* uasync-поток (из call.c): принятый Opus-кадр — кладём в джиттер-буфер (call_jitter_push, * декодирование произойдёт на pull в аудио-потоке). */ 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); + uint16_t seq, uint32_t ts_ms, const uint8_t* opus, int len, const struct call_capture_time* timing); + +/* Playback worker: номер PCM-фрейма и время его вывода, epoch берётся из get_clock после start/reset. + * UI/video: get_clock возвращает 0 для существующего звонка; valid внутри снимка может быть 0. + * now_us/at_us — локальные монотонные часы, та же база, что и callback аудиоустройства. */ +void call_audio_playback_progress(uint64_t call_id, uint64_t epoch, uint64_t position, + int64_t at_us, int64_t now_us, int measured); +int call_audio_get_clock(uint64_t call_id, int64_t now_us, struct call_clock_snapshot* out); /* uasync-поток: метрики джиттер-буфера для CALL_STATS (dropped/underruns — необязательные). */ int call_audio_get_stats(uint64_t call_id, int* buffer_ms, int* tempo_x100,