diff --git a/tools/chatgui/tests/test_stream_publish.cpp b/tools/chatgui/tests/test_stream_publish.cpp index 4373ef70..5c16786e 100644 --- a/tools/chatgui/tests/test_stream_publish.cpp +++ b/tools/chatgui/tests/test_stream_publish.cpp @@ -59,9 +59,23 @@ static std::atomic captureOpens{0}, audioEncodes{0}; #ifdef HAVE_FFMPEG static std::atomic softwareOpens{0}, hardwareOpens{0}; static std::atomic failHardware{false}; +static std::atomic requireGuiAtThreadExit{false}; +static std::atomic threadExitArmed{0}, threadExitCompleted{0}; +struct GuiThreadExit { + bool armed = false; + ~GuiThreadExit() { + if (!armed) return; + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + QMetaObject::invokeMethod(qApp, [] { threadExitCompleted++; }, Qt::BlockingQueuedConnection); + } +}; extern "C" int __real_avcodec_open2(AVCodecContext*, const AVCodec*, AVDictionary**); extern "C" int __wrap_avcodec_open2(AVCodecContext* context, const AVCodec* codec, AVDictionary** options) { if (av_codec_is_encoder(codec)) { + if (requireGuiAtThreadExit && QThread::currentThread() != qApp->thread()) { + thread_local GuiThreadExit cleanup; + if (!cleanup.armed) { cleanup.armed = true; threadExitArmed++; } + } QByteArray name(codec->name); bool hardware = name.endsWith("_nvenc") || name.endsWith("_qsv") || name.endsWith("_amf") || name.endsWith("_vaapi"); if (hardware) { hardwareOpens++; if (failHardware) return AVERROR(ENODEV); } @@ -514,6 +528,41 @@ private slots: QCOMPARE(status.total_bytes, status.video_bytes + status.audio_bytes + status.stream_bytes + status.transport_bytes + status.inflight_bytes); gui_stream_publish_stop(id); QTRY_VERIFY_WITH_TIMEOUT(ended(id), 4000); } + void stopWithGuiThreadCleanup_data() { + QTest::addColumn("destroy"); + QTest::newRow("stop") << false; + QTest::newRow("destructor") << true; + } + void stopWithGuiThreadCleanup() { +#if defined(HAVE_STREAM_CAPTURE) && defined(TEST_NULL_AUDIO) && defined(HAVE_FFMPEG) + if (!qEnvironmentVariableIsSet("UTUN_TEST_SCREEN_CAPTURE")) QSKIP("Run under isolated Xvfb"); + QFETCH(bool, destroy); + requireGuiAtThreadExit = true; failHardware = true; threadExitArmed = threadExitCompleted = 0; + auto reset = qScopeGuard([] { requireGuiAtThreadExit = false; failHardware = false; }); + auto engine = std::make_unique(); + connect(engine.get(), &StreamPublishEngine::softwareEncodingRequested, engine.get(), + [&](uint64_t generation, const QString&, const QString&) { engine->answerSoftwareEncoding(generation, true); }); + StreamPublishConfig config; config.audio = false; config.video.width = 320; config.video.height = 180; config.video.fps = 15; + for (const auto& source : engine->sources()) if (source.kind == StreamVideoSource::Screen) { config.source = source; break; } + QVERIFY(!config.source.id.isEmpty()); QVERIFY(engine->start(group, config)); + QTRY_COMPARE_WITH_TIMEOUT(threadExitArmed.load(), 1u, 4000); + unsigned heartbeats = 0; QTimer heartbeat; + connect(&heartbeat, &QTimer::timeout, this, [&] { heartbeats++; }); heartbeat.start(10); + QElapsedTimer elapsed; elapsed.start(); + if (destroy) { + engine.reset(); QVERIFY(elapsed.elapsed() < 3000); + } else { + engine->stop(); QVERIFY(elapsed.elapsed() < 300); QVERIFY(engine->active()); + QCOMPARE(threadExitCompleted.load(), 0u); + QTRY_VERIFY_WITH_TIMEOUT(!engine->active(), 4000); + } + QCOMPARE(threadExitCompleted.load(), 1u); QVERIFY(heartbeats >= 5); + DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "[PASS] publisher stop destroy=%d GUI-dependent TLS cleanup completed heartbeat=%u elapsed_ms=%lld", + destroy, heartbeats, (long long)elapsed.elapsed()); +#else + QSKIP("Capture test requires FFmpeg and null audio wrappers"); +#endif + } void cpuConsent_data() { QTest::addColumn("allow"); QTest::newRow("decline") << false;