summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-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"