From b3116173ca3849925daed7d1dd8ef4f523957b56 Mon Sep 17 00:00:00 2001 From: evgeny Date: Tue, 20 Oct 2026 17:12:16 +0200 Subject: [PATCH] Verify shared stream mixing and avoid retrying terminal closures --- tools/chatgui/src/streamplayer_window.cpp | 6 +-- tools/chatgui/tests/test_stream_view.cpp | 54 +++++++++++++++++++++++ 2 files changed, 57 insertions(+), 3 deletions(-) diff --git a/tools/chatgui/src/streamplayer_window.cpp b/tools/chatgui/src/streamplayer_window.cpp index 6273718e..c30a7e4a 100644 --- a/tools/chatgui/src/streamplayer_window.cpp +++ b/tools/chatgui/src/streamplayer_window.cpp @@ -242,8 +242,8 @@ void StreamPlayerWindow::decode() { if (request && !gui_stream_resync(p.id)) DEBUG_WARN(DEBUG_CATEGORY_STREAM, "desktop playback resync refused"); deadline = nowUs() + 10000000; emit statusChanged(text("Waiting for a key frame…")); }; - auto fail = [&](const QString& reason) { - p.failed = p.complete = true; + auto fail = [&](const QString& reason, bool retry = true) { + p.failed = retry; p.complete = true; DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "desktop viewer failed id=%llu: %s", (unsigned long long)p.id, qPrintable(reason)); emit statusChanged(reason); }; @@ -389,7 +389,7 @@ void StreamPlayerWindow::decode() { : record.reason == STREAM_CLOSE_MEMBER ? "The source is no longer a channel member" : record.reason == STREAM_CLOSE_SERVICE ? "Chat service is stopped" : record.reason == STREAM_CLOSE_GROUP ? "The channel is closed" : "Could not continue the stream"; - fail(text(reason)); break; + fail(text(reason), record.reason == STREAM_CLOSE_FAILED); break; } if (record.kind == StreamRecordKind::End) { std::lock_guard guard(p.lock); p.ended = true; p.holdAudio = false; diff --git a/tools/chatgui/tests/test_stream_view.cpp b/tools/chatgui/tests/test_stream_view.cpp index ed099787..564569c1 100644 --- a/tools/chatgui/tests/test_stream_view.cpp +++ b/tools/chatgui/tests/test_stream_view.cpp @@ -116,6 +116,8 @@ static std::atomic silenceGaps{0}; static unsigned silentRun = 0; // Только callback; счётчик промежутков >=1мс между тестовыми тонами. static std::atomic leftEnergy{0}, rightEnergy{0}; static std::atomic playbackDelayUs{0}; +static std::atomic inspectMixedOutput{false}; +static std::atomic mixedMean{0}, mixedPeak{0}; extern "C" int64_t __real_audio_stream_frame_time(ma_device*, unsigned int, int*); extern "C" int64_t __wrap_audio_stream_frame_time(ma_device* device, unsigned int frames, int* valid) { if (!playbackDelayUs) return __real_audio_stream_frame_time(device, frames, valid); @@ -140,6 +142,11 @@ static void observeAudio(ma_device* device, void* out, const void* in, ma_uint32 else if (++silentRun == 48) silenceGaps.fetch_add(1); } leftEnergy.fetch_add(left); rightEnergy.fetch_add(right); nonzeroSamples.fetch_add(nonzero); + if (inspectMixedOutput) { + float mean = 0, peak = 0; + for (unsigned i = 0; i < frames * 2; ++i) { mean += pcm[i]; peak = std::max(peak, std::abs(pcm[i])); } + mixedMean = mean / (frames * 2); mixedPeak = peak; + } } extern "C" ma_result __wrap_ma_device_init(ma_context* context, const ma_device_config* config, ma_device* device) { ma_device_config copy = *config; @@ -349,6 +356,46 @@ private slots: stream_info info{}; QCOMPARE(stream_get_info(publisher, &info), STREAM_OK); QCOMPARE(info.owners, 1u); } + void sharedOutputMixesWithoutRenormalizing() { +#ifdef TEST_NULL_AUDIO + auto* sound = SoundManager::instance(); QVERIFY(sound->init()); + std::atomic firstFrames{0}, secondFrames{0}; + struct Cleanup { + SoundManager* sound; + ~Cleanup() { + sound->detachStream(90001); sound->detachStream(90002); sound->detachStream(90003); + inspectMixedOutput = false; + } + } cleanup{sound}; + inspectMixedOutput = true; + QVERIFY(sound->attachStream(90001, [&](float* out, unsigned frames, int64_t, uint64_t) { + for (unsigned i = 0; i < frames * 2; ++i) out[i] += 0.125f; + firstFrames += frames; + })); + QTRY_VERIFY(std::abs(mixedMean.load() - 0.125f) < 0.001f); + QVERIFY(sound->attachStream(90002, [&](float* out, unsigned frames, int64_t, uint64_t) { + for (unsigned i = 0; i < frames * 2; ++i) out[i] += 0.25f; + secondFrames += frames; + })); + QTRY_VERIFY(std::abs(mixedMean.load() - 0.375f) < 0.001f); + QVERIFY(sound->attachStream(90003, [](float*, unsigned, int64_t, uint64_t) {})); + QTest::qWait(30); QVERIFY(std::abs(mixedMean.load() - 0.375f) < 0.001f); + sound->detachStream(90001); + unsigned stopped = firstFrames, continuing = secondFrames; + QTRY_VERIFY(std::abs(mixedMean.load() - 0.25f) < 0.001f); + QTRY_VERIFY(secondFrames.load() > continuing); QCOMPARE(firstFrames.load(), stopped); + sound->detachStream(90003); + QVERIFY(sound->attachStream(90003, [](float* out, unsigned frames, int64_t, uint64_t) { + for (unsigned i = 0; i < frames * 2; ++i) out[i] += 0.9f; + })); + QTRY_VERIFY(mixedMean.load() > 0.95f); QVERIFY(mixedPeak.load() <= 1.0f); + sound->detachStream(90003); + QTRY_VERIFY(std::abs(mixedMean.load() - 0.25f) < 0.001f); +#else + QSKIP("Shared output fixture requires null audio"); +#endif + } + void independentReaders() { QVERIFY(gui_stream_open(201, group, inst->node_id, epoch)); QVERIFY(gui_stream_open(202, group, inst->node_id, epoch)); @@ -651,8 +698,15 @@ private slots: stream_info info{}; QCOMPARE(stream_get_info(publisher, &info), STREAM_OK); QVERIFY(gui_stream_open(105, group, inst->node_id, info.descriptor.source_epoch)); StreamRecord event; QVERIFY(receive(105, event)); QCOMPARE(event.kind, StreamRecordKind::Opened); +#ifdef HAVE_FFMPEG + StreamPlayerWindow window(group, inst->node_id, info.descriptor.source_epoch, "Closing channel"); + QTRY_VERIFY(stream_get_info(publisher, &info) == STREAM_OK && info.owners == 3); +#endif QByteArray channel = QByteArray::number(group); chat_core_delete_channel(inst, channel.constData()); QVERIFY(receive(105, event)); QCOMPARE(event.kind, StreamRecordKind::Closed); QCOMPARE(event.reason, STREAM_CLOSE_GROUP); +#ifdef HAVE_FFMPEG + QTRY_VERIFY(window.finished()); QVERIFY(!window.failed()); // Закрытый канал не запускает бесконечный retry. +#endif QTRY_COMPARE(snapshots.last().mid(9), QByteArray("[]")); QVERIFY(!stream_handle_active(publisher)); QCOMPARE(stream_handle_reason(publisher), STREAM_CLOSE_GROUP); }