diff --git a/doc/call_video.txt b/doc/call_video.txt index 6966c590..08e105ff 100644 --- a/doc/call_video.txt +++ b/doc/call_video.txt @@ -598,6 +598,18 @@ QCamera и восстанавливает его после остановки; не мешает работе камеры. Лог video local preview отдельно показывает частоту и время обновления миниатюры. Общий check.sh: 154/154, proxy 9/9, burst/load PASS; Qt codec/clock и Android 81 unit PASS. +Диагностика задержки desktop -> SM: локальный preview быстрый, но capture_age_us=-180000..-216000, +TX queue=6–8, queue_age_us=184000..248000, tail_ahead_us около 200000. +Первый запоздавший callback камеры привязывал относительный PTS к слишком позднему now; +следующие кадры оказывались в будущем и корректно ожидали audio barrier. +CallVideoCaptureClock теперь ограничивает mapping временем прихода каждого кадра и корректирует +offset только назад; поздний callback не увеличивает offset. Регрессия исходного PTS начинает новую привязку. +Тест моделирует 220мс задержки первого callback, её постепенное исчезновение, поздний callback и перезапуск PTS. +capture_age_us/clock_correction_us показывают привязку; queue_age_us использует только BOOTTIME, +tail_ahead_us сравнивает media PTS в MONOTONIC. Разность BOOTTIME и MONOTONIC не является задержкой. +Qt encoder/clock PASS; изменение требует повторного живого звонка после перезапуска ChatGUI. +Лог исходного дефекта: /tmp/call-latency-new-sample.log. + Подтверждённая причина фризов раз в 1–2с: коррекция capture timestamp меняла audio source_epoch, а обе платформы сбрасывали video decoder и ждали KEY. Теперь source_epoch обновляет аудиопривязку без потери video references, готовых кадров и последнего показанного PTS. diff --git a/doc/chat_call_arch.md b/doc/chat_call_arch.md index 55b8c411..ada60166 100644 --- a/doc/chat_call_arch.md +++ b/doc/chat_call_arch.md @@ -134,6 +134,12 @@ capture/play потоки независимо создают и запуска INFO-логи Android показывают длительности настройки режима, регистрации callback, BT-профиля, выбора маршрута, создания/старта устройств, первого чтения микрофона и первой записи в AudioTrack, а также готовность другого направления. Общий `call_audio` логирует время ожидания первого PCM; запись в AudioTrack может содержать начальную тишину. +Порог запуска AudioTrack — два блока PCM (40мс), отдельно от ёмкости буфера на API 31+; +на API 26–30 запрашивается такой эффективный размер буфера, с учётом ограничений устройства. +Прежнее заполнение всей ёмкости на Samsung оставляло около 140мс между written и played +(на desktop 60–80мс). Лог `playback buffering` показывает принятый порог и ёмкость; +ежесекундный `clock` — queuedUs, текущий порог и underruns, прирост underruns имеет уровень WARN. +Это очередь AudioTrack, а не полная сквозная задержка и не сетевой jitter reserve. ## 7. Буфер звонка и восстановление diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallAudioEngine.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallAudioEngine.kt index 33b1b952..ebe3cae7 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallAudioEngine.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallAudioEngine.kt @@ -9,6 +9,7 @@ import android.media.AudioRecord import android.media.AudioTimestamp import android.media.AudioTrack import android.media.MediaRecorder +import android.os.Build import android.os.Handler import android.os.Looper import android.os.SystemClock @@ -175,6 +176,14 @@ class CallAudioEngine { return@runWorker } applyTrackOutputDevice(router.currentOutputDevice) + // Ёмкость устройства не должна становиться постоянной задержкой звонка. + // Два блока оставляют запас на пробуждение playback-потока каждые 20 мс. + val startFrames = FRAME_SAMPLES * 2 + val actualStart = if (Build.VERSION.SDK_INT >= 31) track.setStartThresholdInFrames(startFrames) + else track.setBufferSizeInFrames(startFrames) + LogManager.addLog(if (actualStart > 0) "INFO" else "WARN", "CallAudio", + "playback buffering id=$id requestedStart=$startFrames actualStart=$actualStart " + + "bufferFrames=${track.bufferSizeInFrames} capacityFrames=${track.bufferCapacityInFrames}") val trackCreateMs = SystemClock.elapsedRealtime() - createdAt val playedAt = SystemClock.elapsedRealtime() track.play() @@ -431,6 +440,7 @@ class CallAudioEngine { val head = CallPlaybackHead() var clockFailed = false var logAtUs = 0L + var lastUnderruns = 0 while (active) { val track = audioTrack ?: return val n = NativeLib.callAudioPull(callId, buf) @@ -474,9 +484,14 @@ class CallAudioEngine { } if (nowUs - logAtUs >= 1_000_000 && NativeLib.callAudioClock(callId, nowUs, snapshot)) { logAtUs = nowUs - LogManager.addLog("DEBUG", "CallAudio", + val underruns = track.underrunCount + val threshold = if (Build.VERSION.SDK_INT >= 31) track.startThresholdInFrames else track.bufferSizeInFrames + val queuedUs = (snapshot[3] - snapshot[2]).coerceAtLeast(0) * 1_000_000 / SAMPLE_RATE + LogManager.addLog(if (underruns > lastUnderruns) "WARN" else "DEBUG", "CallAudio", "clock epoch=${snapshot[0]} source=${snapshot[1]} played=${snapshot[2]} written=${snapshot[3]} " + - "ptsUs=${snapshot[4]} valid=${snapshot[5]} measured=${snapshot[6]} heldUs=${snapshot[7]}") + "ptsUs=${snapshot[4]} valid=${snapshot[5]} measured=${snapshot[6]} heldUs=${snapshot[7]} " + + "queuedUs=$queuedUs bufferFrames=${track.bufferSizeInFrames} startFrames=$threshold underruns=$underruns") + lastUnderruns = underruns } }