From cbe4cb1e33d8fa3a8ef32930f02ad8b4281d9c45 Mon Sep 17 00:00:00 2001 From: evgeny Date: Sun, 1 Nov 2026 22:48:19 +0100 Subject: [PATCH] Use video playback clocks when the source disables audio --- src/stream/stream.c | 10 ++++++-- src/stream/stream.h | 2 +- tools/chatgui/src/streamplayer_window.cpp | 11 +++++++-- tools/chatgui/tests/test_stream_view.cpp | 30 ++++++++++++++++++++--- 4 files changed, 44 insertions(+), 9 deletions(-) diff --git a/src/stream/stream.c b/src/stream/stream.c index 03021fa1f..51e0edb7a 100644 --- a/src/stream/stream.c +++ b/src/stream/stream.c @@ -553,7 +553,8 @@ static void member_changed(uint64_t node_id, const char *tags, const char *chann || state.source.block_size != s->descriptor.block_size)))) { s->invalidated = 1; s->reason = STREAM_CLOSE_SOURCE_CHANGED; } - if (!own && result == STREAM_OK) s->audio_enabled = state.published && state.audio_enabled; + // Снятие published предшествует FIN: сохраняем состояние последнего эфира до дочитывания его хвоста. + if (!own && result == STREAM_OK && state.published) s->audio_enabled = state.audio_enabled; session_radio_update(s); } if (!changed || service->reconcile) return; @@ -930,7 +931,12 @@ int stream_media_get_status(struct STREAM_HANDLE *h, struct stream_media_status DEBUG_WARN(DEBUG_CATEGORY_STREAM, "media status: detached/unconfigured reader"); return STREAM_ERR_STATE; } if (h->session->media_failed) return STREAM_ERR_FORMAT; - return stream_media_cursor_status(h->media_cursor, status); + int result = stream_media_cursor_status(h->media_cursor, status); + if (result == STREAM_OK && !h->session->audio_enabled) { + status->audio_enabled = 0; + if (status->initialized) status->ready_us = status->video_us; + } + return result; } int stream_media_resync(struct STREAM_HANDLE *h) { diff --git a/src/stream/stream.h b/src/stream/stream.h index ee143a505..4ff9ae32a 100644 --- a/src/stream/stream.h +++ b/src/stream/stream.h @@ -178,7 +178,7 @@ int stream_media_open(struct UTUN_INSTANCE *inst, uint64_t group_id, uint64_t so int stream_media_read(struct STREAM_HANDLE *handle, int64_t position_us, int want_video, int64_t audio_ahead_us, struct stream_media_packet *packet); -// Снимок курсора и доступных медиаданных; STREAM_OK или отрицательная ошибка. +// Снимок курсора с текущим звуком источника: при No audio audio_enabled=0, ready_us ограничен только видео. OK/ERR_*. int stream_media_get_status(struct STREAM_HANDLE *handle, struct stream_media_status *status); // Сбросить только свой медиакурсор для поиска новой точки KEY; STREAM_OK или STREAM_ERR_STATE. diff --git a/tools/chatgui/src/streamplayer_window.cpp b/tools/chatgui/src/streamplayer_window.cpp index 1307dba84..1998e5f62 100644 --- a/tools/chatgui/src/streamplayer_window.cpp +++ b/tools/chatgui/src/streamplayer_window.cpp @@ -438,7 +438,7 @@ void StreamPlayerWindow::decode() { p.videos.clear(); p.videoBytes = 0; p.started = p.presenting = p.videoReady = false; p.position = p.ready = -1; p.decoderLead = 0; p.framePeriod = 33333; p.lastInputPts = p.lastDecodedPts = p.lastShownPts = -1; - p.ended = p.videoEnded = p.audioEnded = p.resync = false; + p.ended = p.videoEnded = p.audioEnded = p.activeAudio = p.resync = false; p.audioDacAt = 0; p.audioFrameStart = 0; p.endFrames = UINT64_MAX; p.audioFedAt = 0; p.audioFedPts = -1; observedGaps = p.underruns; p.audioUnderrunning = false; } @@ -450,6 +450,13 @@ void StreamPlayerWindow::decode() { auto observe = [&] { stream_media_status status{}; if (!gui_stream_status(p.id, status)) return; + if (status.initialized && p.started && p.activeAudio != bool(status.audio_enabled && p.audioAvailable)) { + DEBUG_INFO(DEBUG_CATEGORY_STREAM, "desktop playback audio changed id=%llu previous=%d source_enabled=%d output=%d " + "position_us=%lld video_us=%lld audio_us=%lld; resync playback clock", + (unsigned long long)p.id, p.activeAudio, status.audio_enabled, int(p.audioAvailable.load()), + (long long)p.position, (long long)status.video_us, (long long)status.audio_us); + reset(true); return; + } if (status.initialized && !p.started) { bool audio = status.audio_enabled && p.audioAvailable; auto* sound = audio ? stream_playback_audio_create(status.start_us, 250) : nullptr; @@ -465,7 +472,7 @@ void StreamPlayerWindow::decode() { std::lock_guard guard(p.lock); p.media = status; if (status.initialized) - p.ready = p.activeAudio && !status.audio_ended ? status.audio_us : std::max(status.audio_us, status.video_us); + p.ready = !p.activeAudio ? status.video_us : !status.audio_ended ? status.audio_us : std::max(status.audio_us, status.video_us); }; if (!gui_stream_open(p.id, p.group, p.source, p.epoch, [](void* arg) { static_cast(arg)->notify(true); }, &p)) fail(text("Could not connect to the stream")); diff --git a/tools/chatgui/tests/test_stream_view.cpp b/tools/chatgui/tests/test_stream_view.cpp index 420ab240e..0032f6d77 100644 --- a/tools/chatgui/tests/test_stream_view.cpp +++ b/tools/chatgui/tests/test_stream_view.cpp @@ -779,10 +779,11 @@ private slots: QCOMPARE(shown.count(), 0); QCOMPARE(nonzeroSamples.load(), uint64_t(0)); #endif - QTimer sender; - connect(&sender, &QTimer::timeout, this, [this, video, &audioPackets, &sender] { + QTimer sender; bool audioEnabled = true; unsigned videoBatches = 0; + connect(&sender, &QTimer::timeout, this, [this, video, &audioPackets, &sender, &audioEnabled, &videoBatches] { + videoBatches++; #ifdef TEST_NULL_AUDIO - int64_t began = fixtureNowUs(); auto audio = audioPackets(); int64_t encoded = fixtureNowUs(); + int64_t began = fixtureNowUs(); auto audio = audioEnabled ? audioPackets() : QList(); int64_t encoded = fixtureNowUs(); publish(video, audio, 3); int64_t published = fixtureNowUs(); int64_t previous = audioSupplyAtUs.exchange(published); audioSupplyPtsUs = mediaPts + audio.size() * 20000; if (audioDiagnostics) DEBUG_INFO(DEBUG_CATEGORY_STREAM, @@ -791,7 +792,7 @@ private slots: (long long)(published - encoded), (unsigned long long)mediaPts, (long long)audioSupplyPtsUs.load(), (int)audio.size(), (int)sender.timerType()); #else - publish(video, audioPackets(), 3); + publish(video, audioEnabled ? audioPackets() : QList(), 3); #endif }); sender.start(100); @@ -875,7 +876,28 @@ private slots: (unsigned long long)fullVolumeEnergy, (unsigned long long)halfScaleEnergy); volume->setValue(0); QTest::qWait(50); uint64_t played = nonzeroSamples.load(); QTest::qWait(100); QCOMPARE(nonzeroSamples.load(), played); + volume->setValue(100); #endif + audioEnabled = false; QCOMPARE(stream_publish_set_audio_enabled(publisher, 0), STREAM_OK); + QTest::qWait(800); // Дать доиграть уже подготовленному PCM до проверки продвижения без аудиопакетов. + const int beforeSilent = shown.count(); const unsigned batchesBeforeSilent = videoBatches; + QTest::qWait(500); + DEBUG_INFO(DEBUG_CATEGORY_STREAM, "fixture no audio video_batches=%u displayed_frames=%d audio_packets=0 epoch=%llx", + videoBatches - batchesBeforeSilent, shown.count() - beforeSilent, (unsigned long long)epoch); + QVERIFY2(shown.count() > beforeSilent + 5, "Video must continue when the source stops sending audio"); + { + StreamPlayerWindow silent(group, inst->node_id, epoch, "Initially without audio"); silent.show(); + QSignalSpy silentShown(&silent, &StreamPlayerWindow::framePresented); + QTRY_VERIFY_WITH_TIMEOUT(silentShown.count() > 3, 3000); QVERIFY(!silent.failed()); + } + audioEnabled = true; QCOMPARE(stream_publish_set_audio_enabled(publisher, 1), STREAM_OK); +#ifdef TEST_NULL_AUDIO + const uint64_t beforeAudio = nonzeroSamples.load(); + QTRY_VERIFY_WITH_TIMEOUT(nonzeroSamples.load() > beforeAudio, 3000); +#endif + const int beforeResumed = shown.count(); QTRY_VERIFY_WITH_TIMEOUT(shown.count() > beforeResumed + 5, 3000); + stream_info continued{}; QCOMPARE(stream_get_info(publisher, &continued), STREAM_OK); + QCOMPARE(continued.descriptor.source_epoch, epoch); QVERIFY(!window.failed()); sender.stop(); window.close(); QTest::qWait(20); #ifdef TEST_NULL_AUDIO playbackDelayUs = 0;