Buffer stream PCM before playback and recover underruns without resets
This commit is contained in:
1 parent
677f79881c
commit
5b5221189c
7 files changed
+152
-16
No files matched your search
+9
-3
@@ -859,7 +859,11 @@ Android:
|
||||
и отсоединения своего mixer input; detach ядра будит signal, не освобождая его.
|
||||
При обычной нехватке данных decoder references сохраняются; GAP/вытеснение требуют нового KEY.
|
||||
Просроченный кадр при нормальной последовательности декодируется без показа.
|
||||
- Фиксированного prebuffer нет. Начало clock/audio output разрешено после первого декодированного кадра.
|
||||
- Первый декодированный кадр разрешает audio output; общий PCM-модуль desktop/Android перед первым звуком
|
||||
и после underrun накапливает 250ms готового PCM. Это порог готового звука, без аппаратной задержки и lookahead DSP.
|
||||
Во время накопления backend выдаёт silence с неподвижным исходным PTS; decoder references сохраняются.
|
||||
Уже переданный устройству PCM продолжает воспроизводиться. До возобновления регулятор держит темп 1x;
|
||||
затем избыток сокращается по обычной истории минимального запаса. EOF снимает порог и выпускает весь короткий хвост.
|
||||
AudioTrack задаёт время показа видео по реально услышанному PCM;
|
||||
без аудио используются монотонные часы с остановкой на границе готовых кадров.
|
||||
Android StreamAudioOutput принадлежит StreamAudioMixer и хранит один недописанный общий PCM (до 480 stereo frames).
|
||||
@@ -1081,7 +1085,8 @@ Desktop:
|
||||
Ожидающий кадр не теряется при заполненной очереди изображений. Отсутствие прогресса при наличии работы логируется как ошибка.
|
||||
- При начальном подключении desktop/Android прогревают Opus из кольца до первого KEY (до 500ms истории).
|
||||
Старый звук обрезается по PTS видео, включая начало stereo PCM-пакета; декодер не сбрасывается из-за preroll.
|
||||
Фиксированного prebuffer нет: показ следует физическим audio callback timestamps;
|
||||
Звук начинается после накопления 250ms готового PCM; после underrun применяется тот же порог без сброса decoder/PTS.
|
||||
EOF выпускает хвост любой длины. Показ следует физическим audio callback timestamps;
|
||||
без звука — монотонным часам с ограничением готовым PTS. END до KEY завершает пустой reader.
|
||||
Короткие пропуски аудио заполняются Opus PLC; большой разрыв/pressure сбрасывает timeline.
|
||||
Закрытие окна снимает только reader, собственная публикация сохраняется.
|
||||
@@ -1194,7 +1199,8 @@ test_stream_publish проверяет реальные encoder/decoder H.264/H.
|
||||
- AVAILABLE сообщает готовность индекса, DATA/GAP/END callbacks описывают live ingest.
|
||||
media_read выдаёт END после обоих треков конкретного reader. MORE продолжает ограниченную сборку
|
||||
отдельным callback без ожидания новых пакетов; WAIT ожидает новые данные или изменение позиции.
|
||||
- Декодированный PCM пополняется до 250ms при наличии данных; фиксированного ожидания перед стартом нет.
|
||||
- Декодированный PCM пополняется до 250ms при наличии данных; перед стартом и после underrun consumer ждёт этот порог.
|
||||
При EOF остаток выпускается без порога. Во время накопления PTS удерживается silence span; Opus/SoundTouch не сбрасываются.
|
||||
Это запас отдельного audio worker, размер аппаратного буфера не увеличивается. Audio lookahead — device lead + 500ms,
|
||||
достаточный для 250ms PCM даже при x1.6. SPSC имеет 128 слотов по 960 stereo frames с собственными PTS spans.
|
||||
Mixer/callback не вызывает Opus/SoundTouch; Android переиспользует PCM-массив каждого источника.
|
||||
|
||||
+13
-3
@@ -689,7 +689,7 @@
|
||||
Получатель принял приглашение и дважды отправил ACK; до проверки в логе отправителя нет приёма этого ACK. Отдельный повтор прошёл.
|
||||
Логи: /tmp/utun-stream-preview-local-transport-failure.log, /tmp/utun-stream-preview-local-transport-retry.log.
|
||||
|
||||
[ ] **Разобрать нестабильную проверку аудиопауз desktop-трансляции** — 17–20.10.2026:
|
||||
[x] **Разобрать нестабильную проверку аудиопауз desktop-трансляции** — исправлено 25.10.2026:
|
||||
test_stream_view::realH264RenderingAndClose нестабильно получает silenceGaps=3–4 вместо 2.
|
||||
Зафиксированы pcm_short с got=96/480 и 288/480; видео продолжало выводиться.
|
||||
Сбой повторялся с общей нагрузкой и после её завершения; изолированные повторы проходили.
|
||||
@@ -707,13 +707,23 @@
|
||||
minimum=233мс при guard=250мс лишь отключает ускорение, не восстанавливает недостающий запас.
|
||||
Это реальная нехватка PCM на границе пакетной подачи и асинхронной подготовки, а не ложный silenceGaps
|
||||
или доказательство блокировки аудио видео-декодером. Проверка после фиксированных 400мс зависит от фазы таймеров.
|
||||
Нужна политика накопления запаса PCM при старте/underrun с согласованной аудио/видеошкалой;
|
||||
отдельно сделать проверку video-stall независимой от джиттера тестового источника. Исправление ещё не внесено.
|
||||
Добавлены снимки PCM/темпа/guard/времени callback и отдельные эпизоды underrun;
|
||||
UTUN_TEST_AUDIO_DIAG=1 включает замеры публикации/кодирования источника и тишины тестового устройства.
|
||||
Воспроизведение: /tmp/utun2-audio-diag-repro.log, /tmp/utun2-audio-episodes-repro.log (повтор 2 из 5).
|
||||
Отдельный TRACE-прогон video-stall прошёл: 500мс задержки видео, 63352 ненулевых sample, новых пауз нет.
|
||||
Лог /tmp/utun2-audio-causal-trace.log; это подтверждает отдельность audio worker, но не устраняет PCM starvation.
|
||||
Исправление: общий PCM consumer при старте/underrun ждёт уже заданные 250мс готового PCM.
|
||||
Накопление сохраняет decoder и PTS; desktop/Android держат темп 1x до возобновления.
|
||||
EOF снимает ожидание порога, ограничение pull по PTS не считается нехваткой PCM.
|
||||
Счётчик preparedFrames публикуется после готового SPSC-слота, чтобы порог не учитывал ещё недоступный PCM.
|
||||
Детерминированная регрессия без таймеров падает до фикса и проходит после: 33мс после KEY,
|
||||
пачки по 100мс с джиттером 95–106мс + 20мс до feed, 767 полных callback без провала после старта.
|
||||
Отдельно проверены 600мс паузы, дробное поступление, возобновление/PTS, ограничение PTS и короткий EOF 20мс.
|
||||
Старые проверки точной длительности/темпа, stereo, конкурентного producer/consumer и позднего аудио проходят.
|
||||
GUI test_stream_view прошёл 8 повторов подряд; итоговый desktop stream-набор 4/4,
|
||||
Android stream unit tests 55/55, APK и основная C-сборка успешны; часы и Android bridge также проверены.
|
||||
Логи: /tmp/utun2-audio-regression-before.log, /tmp/utun2-audio-recovery-test-after.log,
|
||||
/tmp/utun2-audio-fix-view-repeat.log, /tmp/utun2-audio-fix-stream-tests.log, /tmp/utun2-audio-fix-android.log.
|
||||
Логи: /tmp/stream-zoom-full-view-audio-gap.log, /tmp/stream-zoom-full-view.log,
|
||||
/tmp/multiview-review/desktop-review.log, /tmp/multiview-review/audio-original.log.
|
||||
|
||||
|
||||
@@ -46,6 +46,8 @@ struct stream_playback_audio {
|
||||
double expectedOutput = 0, producedOutput = 0, lastPts = 0;
|
||||
uint64_t written = 0, lastAudioFrame = 0; // Consumer: граница последнего PCM до возможной тишины.
|
||||
std::atomic<bool> finished{false}, failed{false}, preparedEnd{false};
|
||||
std::atomic<bool> buffering{true}; // Consumer ждёт порог PCM при старте и после фактической нехватки.
|
||||
bool reportedBuffering = true; // Только producer: переходы логируются вне hardware callback.
|
||||
int16_t pcm[framesPerPacket * 2];
|
||||
float samples[maxFrames * 2];
|
||||
};
|
||||
@@ -114,7 +116,7 @@ struct stream_playback_audio* stream_playback_audio_create(int64_t start, unsign
|
||||
a->stretch.setSetting(SETTING_SEQUENCE_MS, 20); a->stretch.setSetting(SETTING_SEEKWINDOW_MS, 10);
|
||||
a->stretch.setSetting(SETTING_OVERLAP_MS, 4); a->stretch.setSetting(SETTING_USE_AA_FILTER, 0);
|
||||
DEBUG_INFO(DEBUG_CATEGORY_STREAM,
|
||||
"playback audio created start=%lld stereo=1 tempo=1.0..1.6 PCM_buffer=%ums/%u_frames DSP_latency_us=%lld output=SPSC",
|
||||
"playback audio created start=%lld stereo=1 tempo=1.0..1.6 PCM_prefill_resume=%ums/%u_frames DSP_latency_us=%lld output=SPSC",
|
||||
(long long)start, bufferMs, a->bufferFrames, (long long)stream_playback_audio_latency_us(a));
|
||||
return a;
|
||||
}
|
||||
@@ -169,6 +171,10 @@ unsigned stream_playback_audio_buffered(const struct stream_playback_audio* a) {
|
||||
return (unsigned)(prepared > consumed ? prepared - consumed : 0);
|
||||
}
|
||||
|
||||
int stream_playback_audio_buffering(const struct stream_playback_audio* a) {
|
||||
return a && !a->finished.load(std::memory_order_acquire) && a->buffering.load(std::memory_order_acquire);
|
||||
}
|
||||
|
||||
int64_t stream_playback_audio_latency_us(struct stream_playback_audio* a) {
|
||||
return a ? ((int64_t)a->stretch.getSetting(SETTING_INITIAL_LATENCY) * 1000000 + rate - 1) / rate : 0;
|
||||
}
|
||||
@@ -176,6 +182,13 @@ int64_t stream_playback_audio_latency_us(struct stream_playback_audio* a) {
|
||||
/* DSP принадлежит producer. Каждый слот содержит PCM и его исходные PTS, включая смены темпа. */
|
||||
int stream_playback_audio_prepare(struct stream_playback_audio* a) {
|
||||
if (!a || a->failed) { DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "playback PCM prepare invalid state"); return -1; }
|
||||
bool buffering = stream_playback_audio_buffering(a);
|
||||
if (buffering != a->reportedBuffering) {
|
||||
DEBUG_INFO(DEBUG_CATEGORY_STREAM, "playback PCM state=%s start=%lld current_frames=%u prefill_frames=%u tempo=%d finished=%d",
|
||||
buffering ? "buffering" : "playing", (long long)a->startPts, stream_playback_audio_buffered(a),
|
||||
a->bufferFrames, a->tempo, (int)a->finished.load());
|
||||
a->reportedBuffering = buffering;
|
||||
}
|
||||
uint64_t write = a->readyWrite.load(std::memory_order_relaxed);
|
||||
while (write - a->readyRead.load(std::memory_order_acquire) < pcmCapacity) {
|
||||
double end = a->finished ? std::round(a->expectedOutput) : std::floor(a->expectedOutput);
|
||||
@@ -209,8 +222,8 @@ int stream_playback_audio_prepare(struct stream_playback_audio* a) {
|
||||
a->failed = true; return -1;
|
||||
}
|
||||
a->preparedOutput = until;
|
||||
a->preparedFrames.store((uint64_t)until, std::memory_order_release);
|
||||
a->readyWrite.store(++write, std::memory_order_release);
|
||||
a->preparedFrames.store((uint64_t)until, std::memory_order_release);
|
||||
}
|
||||
if (a->finished && a->preparedOutput >= std::round(a->expectedOutput)) a->preparedEnd.store(true, std::memory_order_release);
|
||||
return 0;
|
||||
@@ -221,6 +234,11 @@ int stream_playback_audio_pull(struct stream_playback_audio* a, int16_t* pcm, un
|
||||
if (!a || !pcm || frames > maxFrames || a->failed || limit < 0) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "playback audio invalid pull frames=%u limit=%lld", frames, (long long)limit); return -1;
|
||||
}
|
||||
if (!frames) return 0;
|
||||
if (a->buffering.load(std::memory_order_relaxed)) {
|
||||
if (!a->finished.load(std::memory_order_acquire) && stream_playback_audio_buffered(a) < a->bufferFrames) return 0;
|
||||
a->buffering.store(false, std::memory_order_release);
|
||||
}
|
||||
unsigned done = 0;
|
||||
uint64_t read = a->readyRead.load(std::memory_order_relaxed);
|
||||
double consumedGain = a->consumedGain.load(std::memory_order_relaxed);
|
||||
@@ -254,6 +272,8 @@ int stream_playback_audio_pull(struct stream_playback_audio* a, int16_t* pcm, un
|
||||
}
|
||||
a->consumedGain.store(consumedGain, std::memory_order_release);
|
||||
a->consumedFrames.store((uint64_t)a->producedOutput, std::memory_order_release);
|
||||
if (done < frames && read == a->readyWrite.load(std::memory_order_acquire) && !a->finished.load(std::memory_order_acquire))
|
||||
a->buffering.store(true, std::memory_order_release);
|
||||
if (stream_playback_audio_empty(a)) a->lastPts = (double)a->endPts;
|
||||
return (int)done;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
* feed прогревает Opus перед start_pts, затем подаёт PCM с темпом 1.0..1.6 без изменения высоты звука.
|
||||
* pull записывает соответствие кадров устройства исходному PTS; silence продлевает hardware time, но не media time.
|
||||
* position/pending_gain используют реально проигранный frame counter, поэтому уже поставленный PCM сохраняет свой темп.
|
||||
* Последний Opus-пакет/EOF не теряется: finish выталкивает остаток SoundTouch. */
|
||||
* При старте/нехватке consumer ждёт buffer_ms готового PCM, сохраняя PTS и decoder; backend вставляет silence.
|
||||
* Последний Opus-пакет/EOF не теряется: finish выталкивает остаток SoundTouch и снимает ожидание порога. */
|
||||
#ifndef STREAM_PLAYBACK_AUDIO_H
|
||||
#define STREAM_PLAYBACK_AUDIO_H
|
||||
|
||||
@@ -17,8 +18,8 @@ extern "C" {
|
||||
#endif
|
||||
|
||||
struct stream_playback_audio;
|
||||
/* buffer_ms (1..1000) — порог пополнения готового PCM, без обязательного ожидания перед стартом.
|
||||
* Это запас декодера, без аппаратной задержки и сетевого джиттер-буфера. */
|
||||
/* buffer_ms (1..1000) — порог пополнения, старта и возобновления готового PCM.
|
||||
* Считается только готовый PCM: аппаратная очередь и закодированные пакеты в него не входят. */
|
||||
struct stream_playback_audio *stream_playback_audio_create(int64_t start_pts_us, unsigned buffer_ms);
|
||||
void stream_playback_audio_destroy(struct stream_playback_audio *audio);
|
||||
int stream_playback_audio_feed(struct stream_playback_audio *audio, int64_t pts_us, const uint8_t *packet, size_t length);
|
||||
@@ -26,12 +27,16 @@ int stream_playback_audio_feed(struct stream_playback_audio *audio, int64_t pts_
|
||||
int stream_playback_audio_prepare(struct stream_playback_audio *audio);
|
||||
/* Потокобезопасный снимок готового PCM, в stereo frames (48000 frames/s). */
|
||||
unsigned stream_playback_audio_buffered(const struct stream_playback_audio *audio);
|
||||
/* Потокобезопасно: consumer накапливает PCM перед стартом/возобновлением; после finish всегда 0.
|
||||
* При этом уже поставленный устройству звук продолжает идти по прежней шкале PTS. */
|
||||
int stream_playback_audio_buffering(const struct stream_playback_audio *audio);
|
||||
/* Producer: внутренний lookahead DSP при текущем темпе; добавляется к guard устройства, без PCM target. */
|
||||
int64_t stream_playback_audio_latency_us(struct stream_playback_audio *audio);
|
||||
void stream_playback_audio_set_tempo(struct stream_playback_audio *audio, int tempo_milli);
|
||||
int stream_playback_audio_needs_input(const struct stream_playback_audio *audio);
|
||||
/* frames <=8192; limit_pts_us ограничивает исходный PTS, INT64_MAX разрешает весь подготовленный звук.
|
||||
* Возвращает число stereo frames; оставшиеся места backend заполняет тишиной и вызывает silence. */
|
||||
* Возвращает число stereo frames; при накоплении порога — 0. После finish порог не применяется.
|
||||
* Оставшиеся места backend заполняет тишиной и вызывает silence; ограничение PTS не запускает новое накопление. */
|
||||
int stream_playback_audio_pull(struct stream_playback_audio *audio, int16_t *pcm, unsigned frames, int64_t limit_pts_us);
|
||||
void stream_playback_audio_silence(struct stream_playback_audio *audio, unsigned frames);
|
||||
int64_t stream_playback_audio_position(struct stream_playback_audio *audio, uint64_t played_frames);
|
||||
|
||||
@@ -221,6 +221,7 @@ JNIEXPORT jlongArray JNICALL Java_com_utun_chat_data_NativeLib_nativeStreamPlayb
|
||||
if (proposed > p->position) p->position = proposed;
|
||||
}
|
||||
p->time_us = now;
|
||||
if (stream_playback_audio_buffering(p->audio)) starved = JNI_TRUE;
|
||||
if (p->audio) guard += stream_playback_audio_latency_us(p->audio);
|
||||
if (stream_playback_clock_update(&p->clock, now, p->position, ready, guard, pending, starved, &p->state) < 0) return NULL;
|
||||
jlong values[] = {p->position, p->state.reserve_us, p->state.minimum_us, p->state.tempo_milli, p->state.waiting,
|
||||
|
||||
@@ -160,12 +160,14 @@ void StreamPlayerWindow::attachAudio() {
|
||||
if (!p.sound || !p.started || p.resync) return;
|
||||
p.audioFrameStart = stream_playback_audio_written(p.sound); p.audioDacAt = firstTimeUs;
|
||||
unsigned pcmBefore = stream_playback_audio_buffered(p.sound);
|
||||
bool buffering = stream_playback_audio_buffering(p.sound);
|
||||
int count = !p.presenting ? 0 : stream_playback_audio_pull(p.sound, pcm, frames, INT64_MAX);
|
||||
if (count < 0) { p.resync = true; count = 0; }
|
||||
if ((unsigned)count < frames) {
|
||||
stream_playback_audio_silence(p.sound, frames - (unsigned)count);
|
||||
if (p.presenting && !p.audioEnded) {
|
||||
p.underruns++; p.audioGap.count++; p.audioGap.silentFrames += frames - (unsigned)count;
|
||||
if (p.presenting && !p.audioEnded) p.audioGap.silentFrames += frames - (unsigned)count;
|
||||
if (p.presenting && !p.audioEnded && !buffering) {
|
||||
p.underruns++; p.audioGap.count++;
|
||||
if (!p.audioUnderrunning) { p.audioGap.episodes++; p.audioUnderrunning = true; }
|
||||
p.audioGap.at = now;
|
||||
p.audioGap.requested = frames; p.audioGap.received = (unsigned)count;
|
||||
@@ -305,7 +307,7 @@ void StreamPlayerWindow::decode() {
|
||||
p.clockAt = nowUs(); p.started = true;
|
||||
stream_playback_clock_reset(&p.adaptation, p.id);
|
||||
DEBUG_INFO(DEBUG_CATEGORY_STREAM,
|
||||
"desktop playback starts id=%llu pts=%lld fixed_prefill=0 elapsed_us=%lld audio=%d PCM_target_ms=250 separate_video=1",
|
||||
"desktop playback starts id=%llu pts=%lld elapsed_us=%lld audio=%d PCM_prefill_resume_ms=250 separate_video=1",
|
||||
(unsigned long long)p.id, (long long)p.position, (long long)(nowUs() - p.createdAt), audio);
|
||||
}
|
||||
std::lock_guard<std::mutex> guard(p.lock);
|
||||
@@ -352,6 +354,7 @@ void StreamPlayerWindow::decode() {
|
||||
if (p.presenting && p.ready >= 0) {
|
||||
uint64_t played = p.played(now);
|
||||
bool starved = p.underruns != observedGaps; observedGaps = p.underruns;
|
||||
if (p.sound && stream_playback_audio_buffering(p.sound)) starved = true;
|
||||
if (!p.activeAudio && p.position + (now - p.clockAt) * p.adaptation.tempo_milli / 1000 > p.ready) starved = true;
|
||||
int64_t guardUs = p.activeAudio ? std::max<int64_t>(40000, p.deviceLead + 20000) : p.framePeriod;
|
||||
if (p.sound) guardUs += stream_playback_audio_latency_us(p.sound);
|
||||
@@ -386,7 +389,7 @@ void StreamPlayerWindow::decode() {
|
||||
"TIME{position_ms=%s ready_ms=%s reserve_ms=%s minimum_ms=%s guard_ms=%s tempo=%d reason=%s "
|
||||
"correction_ms=%s pending_gain_ms=%s} "
|
||||
"VIDEO{shown=%llu drop=%llu pts_ms=%s av_ms=%s queue=%zu input=%zu decoder_lead_ms=%s wait_key=%d} "
|
||||
"AUDIO{enabled=%d pcm_ms=%.1f device_ms=%s underruns=%llu silence_frames=%llu} waiting=%d ended=%d",
|
||||
"AUDIO{enabled=%d pcm_ms=%.1f buffering=%d device_ms=%s underruns=%llu silence_frames=%llu} waiting=%d ended=%d",
|
||||
(unsigned long long)p.group, (unsigned long long)p.source, (unsigned long long)p.epoch,
|
||||
(unsigned long long)p.id, (long long)((began - playReportAt) / 1000), (unsigned long long)p.media.generation,
|
||||
ms(p.position, p.started).constData(), ms(p.ready, p.ready >= 0).constData(),
|
||||
@@ -399,6 +402,7 @@ void StreamPlayerWindow::decode() {
|
||||
ms(p.lastShownPts - position, p.lastShownPts >= 0 && position >= 0).constData(),
|
||||
p.videos.size(), p.videoInput.size(), ms(p.decoderLead, p.videoReady).constData(), !p.videoReady,
|
||||
p.activeAudio, p.sound ? stream_playback_audio_buffered(p.sound) / 48.0 : 0,
|
||||
stream_playback_audio_buffering(p.sound),
|
||||
ms(p.deviceLead, p.activeAudio && p.audioDacAt).constData(),
|
||||
(unsigned long long)(p.underruns - underrunBase),
|
||||
(unsigned long long)(p.audioGap.silentFrames - silenceBase), timing && p.timing.waiting, p.ended);
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <cstdlib>
|
||||
#include <vector>
|
||||
#include <thread>
|
||||
#include <deque>
|
||||
|
||||
extern "C" {
|
||||
#include "../../../lib/debug_config.h"
|
||||
@@ -147,6 +148,94 @@ static void test_pcm_concurrent() {
|
||||
DEBUG_INFO(DEBUG_CATEGORY_STREAM, "[PASS] independent PCM producer/consumer: stereo x1.6, 777-frame callbacks, exact EOF/PTS");
|
||||
}
|
||||
|
||||
/* Детерминированные часы: 33мс после KEY, пачки по 100мс, джиттер 95..106мс и ещё 20мс до feed.
|
||||
* Это исходный desktop-флак без QTimer/sleep: после первого звука ни один callback не теряет PCM. */
|
||||
static void test_bursty_pcm_delivery() {
|
||||
auto* encoder = opus_codec_encoder_create(48000, 2); CHECK(encoder);
|
||||
auto* audio = stream_playback_audio_create(966666, 250); CHECK(audio);
|
||||
int16_t pcm[1920] = {}, output[960]; uint8_t packet[1275];
|
||||
int length = opus_codec_encode(encoder, pcm, 960, packet, sizeof(packet)); CHECK(length > 0);
|
||||
CHECK(stream_playback_audio_feed(audio, 960000, packet, length) == 0);
|
||||
CHECK(stream_playback_audio_feed(audio, 980000, packet, length) == 0);
|
||||
struct Batch { int64_t at, pts; };
|
||||
std::deque<Batch> pending;
|
||||
const int jitter[] = {0, 6000, 1000, 6000, 1000};
|
||||
for (unsigned i = 0; i < 80; ++i) pending.push_back({100000 + i * 100000 + jitter[i % 5] + 20000, 1000000 + i * 100000});
|
||||
bool started = false; unsigned callbacks = 0, silent = 0;
|
||||
for (int64_t now = 0; now < 8000000; now += 10000) {
|
||||
while (!pending.empty() && pending.front().at <= now) {
|
||||
for (unsigned i = 0; i < 5; ++i)
|
||||
CHECK(stream_playback_audio_feed(audio, pending.front().pts + i * 20000, packet, length) == 0);
|
||||
pending.pop_front();
|
||||
}
|
||||
int count = stream_playback_audio_pull(audio, output, 480, INT64_MAX); CHECK(count >= 0);
|
||||
if (started || count) { CHECK(count == 480); started = true; ++callbacks; }
|
||||
else { ++silent; CHECK(stream_playback_audio_position(audio, stream_playback_audio_written(audio)) == 966666); }
|
||||
if (count < 480) stream_playback_audio_silence(audio, 480 - count);
|
||||
CHECK(stream_playback_audio_position(audio, stream_playback_audio_written(audio)) >= 966666);
|
||||
}
|
||||
CHECK(started && callbacks > 700 && silent < 40);
|
||||
stream_playback_audio_destroy(audio); opus_codec_encoder_destroy(encoder);
|
||||
DEBUG_INFO(DEBUG_CATEGORY_STREAM, "[PASS] 100ms bursts + 95..106ms jitter + 20ms worker delay: callbacks=%u no PCM gaps", callbacks);
|
||||
}
|
||||
|
||||
/* Длинная пауза не дробит звук на отдельные пришедшие пакеты; EOF выпускает короткий остаток без ожидания. */
|
||||
static void test_pcm_recovery_and_short_eof() {
|
||||
auto* encoder = opus_codec_encoder_create(48000, 2); CHECK(encoder);
|
||||
int16_t pcm[1920] = {}, output[960]; uint8_t packet[1275];
|
||||
int length = opus_codec_encode(encoder, pcm, 960, packet, sizeof(packet)); CHECK(length > 0);
|
||||
auto* audio = stream_playback_audio_create(1000000, 250); CHECK(audio);
|
||||
int64_t pts = 1000000;
|
||||
while (stream_playback_audio_needs_input(audio)) {
|
||||
CHECK(stream_playback_audio_feed(audio, pts, packet, length) == 0); pts += 20000;
|
||||
}
|
||||
unsigned prepared = stream_playback_audio_buffered(audio), consumed = 0;
|
||||
for (unsigned callback = 0; callback < 60; ++callback) {
|
||||
int count = stream_playback_audio_pull(audio, output, 480, INT64_MAX); CHECK(count >= 0);
|
||||
consumed += count;
|
||||
if (count < 480) stream_playback_audio_silence(audio, 480 - count);
|
||||
}
|
||||
CHECK(consumed == prepared);
|
||||
CHECK(stream_playback_audio_buffering(audio));
|
||||
int64_t held = stream_playback_audio_position(audio, stream_playback_audio_written(audio));
|
||||
for (unsigned i = 0; i < 5; ++i) {
|
||||
CHECK(stream_playback_audio_feed(audio, pts, packet, length) == 0); pts += 20000;
|
||||
CHECK(stream_playback_audio_pull(audio, output, 480, INT64_MAX) == 0);
|
||||
stream_playback_audio_silence(audio, 480);
|
||||
CHECK(stream_playback_audio_position(audio, stream_playback_audio_written(audio)) == held);
|
||||
}
|
||||
while (stream_playback_audio_needs_input(audio)) {
|
||||
CHECK(stream_playback_audio_feed(audio, pts, packet, length) == 0); pts += 20000;
|
||||
}
|
||||
CHECK(stream_playback_audio_pull(audio, output, 480, INT64_MAX) == 480); consumed += 480;
|
||||
CHECK(!stream_playback_audio_buffering(audio));
|
||||
int64_t resumed = stream_playback_audio_position(audio, stream_playback_audio_written(audio)); CHECK(resumed > held);
|
||||
int limited = stream_playback_audio_pull(audio, output, 480, resumed + 1000);
|
||||
CHECK(limited > 0 && limited < 480 && !stream_playback_audio_buffering(audio)); consumed += limited;
|
||||
for (unsigned i = 0; i < 100; ++i) {
|
||||
int count = stream_playback_audio_pull(audio, output, 480, INT64_MAX); CHECK(count >= 0); consumed += count;
|
||||
if (count < 480) { stream_playback_audio_silence(audio, 480 - count); break; }
|
||||
}
|
||||
CHECK(stream_playback_audio_buffering(audio));
|
||||
CHECK(stream_playback_audio_feed(audio, pts, packet, length) == 0); pts += 20000;
|
||||
CHECK(stream_playback_audio_pull(audio, output, 480, INT64_MAX) == 0);
|
||||
stream_playback_audio_finish(audio);
|
||||
for (unsigned i = 0; !stream_playback_audio_empty(audio) && i < 100; ++i) {
|
||||
int count = stream_playback_audio_pull(audio, output, 480, INT64_MAX); CHECK(count > 0); consumed += count;
|
||||
}
|
||||
CHECK(stream_playback_audio_empty(audio) && consumed == (pts - 1000000) * 48000 / 1000000);
|
||||
CHECK(stream_playback_audio_position(audio, stream_playback_audio_written(audio)) == pts);
|
||||
stream_playback_audio_destroy(audio);
|
||||
audio = stream_playback_audio_create(0, 250); CHECK(audio);
|
||||
CHECK(stream_playback_audio_feed(audio, 0, packet, length) == 0); stream_playback_audio_finish(audio);
|
||||
CHECK(stream_playback_audio_pull(audio, output, 480, INT64_MAX) == 480);
|
||||
CHECK(stream_playback_audio_pull(audio, output, 480, INT64_MAX) == 480);
|
||||
CHECK(stream_playback_audio_empty(audio) && stream_playback_audio_position(audio, 960) == 20000);
|
||||
stream_playback_audio_destroy(audio); opus_codec_encoder_destroy(encoder);
|
||||
DEBUG_INFO(DEBUG_CATEGORY_STREAM,
|
||||
"[PASS] 600ms gap: trickle waits, PTS freezes/resumes, PTS limit keeps state; recovery tail and 20ms EOF drain exactly");
|
||||
}
|
||||
|
||||
/* Новый темп не меняет PTS уже приготовленного PCM; дробный конец не теряется при сменах темпа. */
|
||||
static void test_prepared_tempo_changes() {
|
||||
auto* encoder = opus_codec_encoder_create(48000, 2); CHECK(encoder);
|
||||
@@ -244,6 +333,7 @@ static void test_late_audio_start() {
|
||||
int main() {
|
||||
debug_config_init(); debug_set_level(DEBUG_LEVEL_ERROR); debug_set_category_level(DEBUG_CATEGORY_STREAM, DEBUG_LEVEL_INFO);
|
||||
size_t allocations = u_get_allocated_count();
|
||||
test_bursty_pcm_delivery(); test_pcm_recovery_and_short_eof();
|
||||
test_stereo_tempo(); test_silence_clock(); test_pcm_buffer(); test_pcm_concurrent(); test_prepared_tempo_changes(); test_late_audio_start();
|
||||
CHECK(u_get_allocated_count() == allocations);
|
||||
return 0;
|
||||
|
||||
Reference in new issue
Block a user