Revision control
+ // Copyright (C) 2026 The Qt Company Ltd.
+ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+ #include <QtTest/qtest.h>
+
+ #include <QtCore/qdeadlinetimer.h>
+ #include <QtCore/qthread.h>
+
+ #include <QtFFmpegMediaPluginImpl/private/qffmpegencoderthread_p.h>
+ #include <QtFFmpegMediaPluginImpl/private/qffmpegrecordingengine_p.h>
+
+ #include <atomic>
+ #include <chrono>
+ #include <memory>
+ #include <new>
+ #include <thread>
+
+ using namespace QFFmpeg;
+ using namespace std::chrono_literals;
+
+ namespace {
+
+ class IdleEncoder : public EncoderThread
+ {
+ public:
+ using EncoderThread::EncoderThread;
+ using EncoderThread::setEndOfSourceStream;
+
+ protected:
+ bool checkIfCanPushFrame() const override { return true; }
+ bool init() override { return true; }
+ void cleanup() override { }
+ bool hasData() const override { return false; }
+ void processOne() override { }
+ };
+
+ // A real RecordingEngine asserts a completed finalization on destruction. The encoders here are
+ // never started, so the reference is never dereferenced.
+ RecordingEngine &unusedRecordingEngine()
+ {
+ alignas(RecordingEngine) static std::byte storage[sizeof(RecordingEngine)];
+ return *reinterpret_cast<RecordingEngine *>(storage);
+ }
+
+ } // namespace
+
+ class tst_QFFmpegEncoderThread : public QObject
+ {
+ Q_OBJECT
+
+ private slots:
+ void isEndOfSourceStream_isFalse_byDefault();
+ void isEndOfSourceStream_isVisibleToOtherThread_whenSetFromWorkerThread();
+ };
+
+ void tst_QFFmpegEncoderThread::isEndOfSourceStream_isFalse_byDefault()
+ {
+ ConsumerThreadUPtr<IdleEncoder> encoder(new IdleEncoder(unusedRecordingEngine()));
+ QVERIFY(!encoder->isEndOfSourceStream());
+ }
+
+ void tst_QFFmpegEncoderThread::
+ isEndOfSourceStream_isVisibleToOtherThread_whenSetFromWorkerThread()
+ {
+ constexpr int iterations = 500;
+
+ for (int i = 0; i < iterations; ++i) {
+ ConsumerThreadUPtr<IdleEncoder> audio(new IdleEncoder(unusedRecordingEngine()));
+ ConsumerThreadUPtr<IdleEncoder> video(new IdleEncoder(unusedRecordingEngine()));
+
+ std::atomic_bool go = false;
+ auto reportEos = [&](IdleEncoder *encoder) {
+ while (!go.load())
+ std::this_thread::yield();
+ encoder->setEndOfSourceStream();
+ };
+
+ std::thread audioSource(reportEos, audio.get());
+ std::thread videoSource(reportEos, video.get());
+
+ go = true;
+
+ QDeadlineTimer deadline(5s);
+ bool allEnded = false;
+ while (!deadline.hasExpired()) {
+ allEnded = audio->isEndOfSourceStream() && video->isEndOfSourceStream();
+ if (allEnded)
+ break;
+ std::this_thread::yield();
+ }
+
+ audioSource.join();
+ videoSource.join();
+
+ QVERIFY2(allEnded, "End of source stream was never observed by the polling thread");
+ }
+ }
+
+ QTEST_GUILESS_MAIN(tst_QFFmpegEncoderThread)
+
+ #include "tst_qffmpegencoderthread.moc"