Test publisher shutdown with GUI-dependent thread-local cleanup

This commit is contained in:
evgeny committed 2026-10-16 17:27:15 +02:00
1 parent 85101765eb
commit 0f5a5580c5
1 file changed
+49
@@ -59,9 +59,23 @@ static std::atomic<unsigned> captureOpens{0}, audioEncodes{0};
#ifdef HAVE_FFMPEG
static std::atomic<unsigned> softwareOpens{0}, hardwareOpens{0};
static std::atomic<bool> failHardware{false};
static std::atomic<bool> requireGuiAtThreadExit{false};
static std::atomic<unsigned> 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<bool>("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<StreamPublishEngine>();
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<bool>("allow");
QTest::newRow("decline") << false;