summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJøger Hansegård <joger.hansegard@qt.io>2024-03-10 18:43:54 +0100
committerQt Cherry-pick Bot <cherrypick_bot@qt-project.org>2024-03-28 17:38:22 +0000
commit2f4af94229dadf454fedb92f754a5b9b88a1bf4b (patch)
tree1051fefa13cc97c9e64a680075600ea11ec8c93c
parent73c87b5e6c8ed2b8bcfc65264a80f4921423540f (diff)
Tie Qt Multimedia backend lifetime to Qt Application lifetime
This patch uses a specialized application static variable to tie backend lifetime to the Qt application lifetime if an application is present. This ensures that system resources are released before static destruction, and prevents abrupt termination on Windows during static destruction. This is according to Windows Media Foundation's documentation that states that WMF classes shall not be released during static destruction. If Qt Multimedia features are used without a Qt Application object, the lifetime of the multimedia backend remains as before, and destruction takes place as part of static destruction. As a side effect, this change fixes a crash if Qt Multimedia features are used after recreating the Qt Application. Fixes: QTBUG-120198 Pick-to: 6.5 Change-Id: I570743d6462e27630d8f29dc60cfa414c8cbc17d Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org> Reviewed-by: Artem Dyomin <artem.dyomin@qt.io> (cherry picked from commit 7efc9f9b6d058b6e7e6c768b94abf602719b31b4) Reviewed-by: Qt Cherry-pick Bot <cherrypick_bot@qt-project.org> (cherry picked from commit f187f750734497f27d934b54293d3f513d0dfbca)
-rw-r--r--src/multimedia/audio/qsoundeffect.cpp9
-rw-r--r--src/multimedia/platform/qplatformmediaintegration.cpp61
-rw-r--r--tests/auto/integration/CMakeLists.txt1
-rw-r--r--tests/auto/integration/multiapp/CMakeLists.txt21
-rw-r--r--tests/auto/integration/multiapp/double-drop.wavbin0 -> 20626 bytes
-rw-r--r--tests/auto/integration/multiapp/tst_multiapp.cpp175
6 files changed, 256 insertions, 11 deletions
diff --git a/src/multimedia/audio/qsoundeffect.cpp b/src/multimedia/audio/qsoundeffect.cpp
index 4685b4671..fda704c97 100644
--- a/src/multimedia/audio/qsoundeffect.cpp
+++ b/src/multimedia/audio/qsoundeffect.cpp
@@ -114,6 +114,14 @@ void QSoundEffectPrivate::sampleReady()
if (!m_audioSink) {
const auto audioDevice =
m_audioDevice.isNull() ? QMediaDevices::defaultAudioOutput() : m_audioDevice;
+
+ if (audioDevice.isNull()) {
+ // We are likely on a virtual machine, for example in CI
+ qCCritical(qLcSoundEffect) << "Failed to play sound. No audio devices present.";
+ setStatus(QSoundEffect::Error);
+ return;
+ }
+
const auto &sampleFormat = m_sample->format();
const auto sampleChannelConfig =
sampleFormat.channelConfig() == QAudioFormat::ChannelConfigUnknown
@@ -141,6 +149,7 @@ void QSoundEffectPrivate::sampleReady()
m_audioBuffer = QAudioBuffer(m_sample->data(), m_sample->format());
m_audioSink.reset(new QAudioSink(audioDevice, m_audioBuffer.format()));
+
connect(m_audioSink.get(), &QAudioSink::stateChanged, this, &QSoundEffectPrivate::stateChanged);
if (!m_muted)
m_audioSink->setVolume(m_volume);
diff --git a/src/multimedia/platform/qplatformmediaintegration.cpp b/src/multimedia/platform/qplatformmediaintegration.cpp
index dfa248c56..2a563417a 100644
--- a/src/multimedia/platform/qplatformmediaintegration.cpp
+++ b/src/multimedia/platform/qplatformmediaintegration.cpp
@@ -13,6 +13,7 @@
#include <qcameradevice.h>
#include <qloggingcategory.h>
#include <QtCore/qcoreapplication.h>
+#include <QtCore/qapplicationstatic.h>
#include "qplatformcapturablewindows_p.h"
#include "qplatformmediadevices_p.h"
@@ -74,7 +75,7 @@ struct InstanceHolder
if (backend.isEmpty() && !backends.isEmpty())
backend = defaultBackend(backends);
- qCDebug(qLcMediaPlugin) << "loading backend" << backend;
+ qCDebug(qLcMediaPlugin) << "Loading media backend" << backend;
instance.reset(
qLoadPlugin<QPlatformMediaIntegration, QPlatformMediaPlugin>(loader(), backend));
@@ -84,10 +85,54 @@ struct InstanceHolder
}
}
+ ~InstanceHolder()
+ {
+ instance.reset();
+ qCDebug(qLcMediaPlugin) << "Released media backend";
+ }
+
+ // Play nice with QtGlobalStatic::ApplicationHolder
+ using QAS_Type = InstanceHolder;
+ static void innerFunction(void *pointer)
+ {
+ new (pointer) InstanceHolder();
+ }
+
std::unique_ptr<QPlatformMediaIntegration> instance;
};
-Q_GLOBAL_STATIC(InstanceHolder, instanceHolder);
+// Specialized implementation of Q_APPLICATION_STATIC which behaves as
+// an application static if a Qt application is present, otherwise as a Q_GLOBAL_STATIC.
+// By doing this, and we have a Qt application, all system resources allocated by the
+// backend is released when application lifetime ends. This is important on Windows,
+// where Windows Media Foundation instances should not be released during static destruction.
+//
+// If we don't have a Qt application available when instantiating the instance holder,
+// it will be created once, and not destroyed until static destruction. This can cause
+// abrupt termination of Windows applications during static destruction. This is not a
+// supported use case, but we keep this as a fallback to keep old applications functional.
+// See also QTBUG-120198
+struct ApplicationHolder : QtGlobalStatic::ApplicationHolder<InstanceHolder>
+{
+ // Replace QtGlobalStatic::ApplicationHolder::pointer to prevent crash if
+ // no application is present
+ static InstanceHolder* pointer()
+ {
+ if (guard.loadAcquire() == QtGlobalStatic::Initialized)
+ return realPointer();
+
+ QMutexLocker locker(&mutex);
+ if (guard.loadRelaxed() == QtGlobalStatic::Uninitialized) {
+ InstanceHolder::innerFunction(&storage);
+
+ if (const QCoreApplication *app = QCoreApplication::instance())
+ QObject::connect(app, &QObject::destroyed, app, reset, Qt::DirectConnection);
+
+ guard.storeRelease(QtGlobalStatic::Initialized);
+ }
+ return realPointer();
+ }
+};
} // namespace
@@ -95,7 +140,8 @@ QT_BEGIN_NAMESPACE
QPlatformMediaIntegration *QPlatformMediaIntegration::instance()
{
- return instanceHolder->instance.get();
+ static QGlobalStatic<ApplicationHolder> s_instanceHolder;
+ return s_instanceHolder->instance.get();
}
QList<QCameraDevice> QPlatformMediaIntegration::videoInputs()
@@ -146,19 +192,12 @@ QPlatformMediaFormatInfo *QPlatformMediaIntegration::createFormatInfo()
return new QPlatformMediaFormatInfo;
}
-// clang-format off
std::unique_ptr<QPlatformMediaDevices> QPlatformMediaIntegration::createMediaDevices()
{
- // Avoid releasing WMF resources and uninitializing WMF during static
- // destruction, QTBUG-120198
- if (QCoreApplication::instance())
- connect(qApp, &QObject::destroyed, this, [this] {
- m_mediaDevices = nullptr;
- });
-
return QPlatformMediaDevices::create();
}
+// clang-format off
QPlatformVideoDevices *QPlatformMediaIntegration::videoDevices()
{
std::call_once(m_videoDevicesOnceFlag,
diff --git a/tests/auto/integration/CMakeLists.txt b/tests/auto/integration/CMakeLists.txt
index 87baccfcc..cbacf0c1a 100644
--- a/tests/auto/integration/CMakeLists.txt
+++ b/tests/auto/integration/CMakeLists.txt
@@ -12,6 +12,7 @@ add_subdirectory(qmediaplayerbackend)
add_subdirectory(qsoundeffect)
add_subdirectory(qvideoframebackend)
add_subdirectory(backends)
+add_subdirectory(multiapp)
if(TARGET Qt::Widgets)
add_subdirectory(qmediacapturesession)
add_subdirectory(qcamerabackend)
diff --git a/tests/auto/integration/multiapp/CMakeLists.txt b/tests/auto/integration/multiapp/CMakeLists.txt
new file mode 100644
index 000000000..8a297cafc
--- /dev/null
+++ b/tests/auto/integration/multiapp/CMakeLists.txt
@@ -0,0 +1,21 @@
+# Copyright (C) 2024 The Qt Company Ltd.
+# SPDX-License-Identifier: BSD-3-Clause
+
+qt_internal_add_test(tst_multiapp
+ SOURCES
+ tst_multiapp.cpp
+ LIBRARIES
+ Qt::Core
+ Qt::MultimediaPrivate
+)
+
+set(resources_resource_files
+ "double-drop.wav"
+)
+
+qt_add_resources(tst_multiapp "resources"
+ PREFIX
+ "/"
+ FILES
+ ${resources_resource_files}
+)
diff --git a/tests/auto/integration/multiapp/double-drop.wav b/tests/auto/integration/multiapp/double-drop.wav
new file mode 100644
index 000000000..bd9a507c7
--- /dev/null
+++ b/tests/auto/integration/multiapp/double-drop.wav
Binary files differ
diff --git a/tests/auto/integration/multiapp/tst_multiapp.cpp b/tests/auto/integration/multiapp/tst_multiapp.cpp
new file mode 100644
index 000000000..a19ae44b3
--- /dev/null
+++ b/tests/auto/integration/multiapp/tst_multiapp.cpp
@@ -0,0 +1,175 @@
+// Copyright (C) 2024 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
+
+#include <QtTest/QtTest>
+#include <QtCore/qdebug.h>
+#include <QtCore/qprocess.h>
+#include <QtCore/qcoreapplication.h>
+#include <QtCore/qstring.h>
+#include <QtCore/qmetaobject.h>
+#include <QtMultimedia/qsoundeffect.h>
+#include <QtMultimedia/qmediadevices.h>
+#include <QtMultimedia/qaudiodevice.h>
+
+using namespace Qt::StringLiterals;
+
+QT_USE_NAMESPACE
+
+namespace {
+bool executeTestOutOfProcess(const QString &testName);
+void playSound();
+} // namespace
+
+class tst_multiapp : public QObject
+{
+ Q_OBJECT
+
+public slots:
+ void initTestCase()
+ {
+#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
+ QSKIP("Out-of-process testing does not behave correctly on mobile OS");
+#endif
+ }
+
+private slots:
+ void mediaDevices_doesNotCrash_whenCalledWithoutApplication()
+ {
+ QVERIFY(executeTestOutOfProcess(
+ "mediaDevices_doesNotCrash_whenCalledWithoutApplication_impl"_L1));
+ }
+
+ bool mediaDevices_doesNotCrash_whenCalledWithoutApplication_impl(int argc, char **argv)
+ {
+ Q_ASSERT(!qApp);
+
+ QMediaDevices::defaultAudioOutput(); // Just verify that we don't crash
+ return true;
+ }
+
+ void mediaDevices_doesNotCrash_whenCalledAfterApplicationExit()
+ {
+ QVERIFY(executeTestOutOfProcess(
+ "mediaDevices_doesNotCrash_whenCalledAfterApplicationExit_impl"_L1));
+ }
+
+ bool mediaDevices_doesNotCrash_whenCalledAfterApplicationExit_impl(int argc, char **argv)
+ {
+ Q_ASSERT(!qApp);
+
+ {
+ QCoreApplication app{ argc, argv };
+ // Create the backend bound to the lifetime of the app
+ QMediaDevices::defaultAudioOutput();
+ }
+
+ QMediaDevices::defaultAudioOutput(); // Just verify that we don't crash
+ return true;
+ }
+
+ void soundEffect_doesNotCrash_whenRecreatingApplication()
+ {
+ QVERIFY(executeTestOutOfProcess(
+ "soundEffect_doesNotCrash_whenRecreatingApplication_impl"_L1));
+ }
+
+ bool soundEffect_doesNotCrash_whenRecreatingApplication_impl(int argc, char **argv)
+ {
+ Q_ASSERT(!qApp);
+
+ // Play a sound twice under two different application objects
+ // This verifies that QSoundEffect works in use cases where
+ // client application recreates Qt application instances,
+ // for example when the client application loads plugins
+ // implemented using Qt.
+ {
+ QCoreApplication app{ argc, argv };
+ playSound();
+ }
+ {
+ QCoreApplication app{ argc, argv };
+ playSound();
+ }
+
+ return true;
+ }
+
+};
+
+namespace {
+
+void playSound()
+{
+ const QUrl url{ "qrc:double-drop.wav"_L1 };
+
+ QSoundEffect effect;
+ effect.setSource(url);
+ effect.play();
+
+ QObject::connect(&effect, &QSoundEffect::playingChanged, qApp, [&]() {
+ if (!effect.isPlaying())
+ qApp->quit();
+ });
+
+ // In some CI configurations, we do not have any audio devices. We must therefore
+ // close the qApp on error signal instead of on playingChanged.
+ QObject::connect(&effect, &QSoundEffect::statusChanged, qApp, [&]() {
+ if (effect.status() == QSoundEffect::Status::Error) {
+ qDebug() << "Failed to play sound effect";
+ qApp->quit();
+ }
+ });
+
+ qApp->exec();
+}
+
+bool executeTestOutOfProcess(const QString &testName)
+{
+ const QStringList args{ "--run-test"_L1, testName };
+ const QString processName = QCoreApplication::applicationFilePath();
+ const int status = QProcess::execute(processName, args);
+ return status == 0;
+}
+
+} // namespace
+
+// This main function executes tests like normal qTest, and adds support
+// for executing specific test functions when called out of process. In this
+// case we don't create a QApplication, because the intent is to test how features
+// behave when no QApplication exists.
+int main(int argc, char *argv[])
+{
+ QCommandLineParser cmd;
+ const QCommandLineOption runTest{ QStringList{ "run-test" }, "Executes a named test",
+ "runTest" };
+ cmd.addOption(runTest);
+ cmd.parse({ argv, argv + argc });
+
+ if (cmd.isSet(runTest)) {
+ // We are requested to run a test case in a separate process without a Qt application
+ const QString testName = cmd.value(runTest);
+
+ bool returnValue = false;
+ tst_multiapp tc;
+
+ // Call the requested function on the test class
+ const bool invokeResult =
+ QMetaObject::invokeMethod(&tc, testName.toLatin1(), Qt::DirectConnection,
+ qReturnArg(returnValue), argc, argv);
+
+ return (invokeResult && returnValue) ? 0 : 1;
+ }
+
+ // If no special arguments are set, enter the regular QTest main routine
+ // The below lines are the same that QTEST_GUILESS_MAIN would stamp out,
+ // except the `int main(...)`
+ TESTLIB_SELFCOVERAGE_START("tst_multiapp")
+ QT_PREPEND_NAMESPACE(QTest::Internal::callInitMain)<tst_multiapp>();
+ QCoreApplication app(argc, argv);
+ app.setAttribute(Qt::AA_Use96Dpi, true);
+ tst_multiapp tc;
+ QTEST_SET_MAIN_SOURCE_PATH
+ return QTest::qExec(&tc, argc, argv);
+}
+
+#include "tst_multiapp.moc"