diff options
-rw-r--r-- | src/multimedia/audio/qsoundeffect.cpp | 9 | ||||
-rw-r--r-- | src/multimedia/platform/qplatformmediaintegration.cpp | 61 | ||||
-rw-r--r-- | tests/auto/integration/CMakeLists.txt | 1 | ||||
-rw-r--r-- | tests/auto/integration/multiapp/CMakeLists.txt | 21 | ||||
-rw-r--r-- | tests/auto/integration/multiapp/double-drop.wav | bin | 0 -> 20626 bytes | |||
-rw-r--r-- | tests/auto/integration/multiapp/tst_multiapp.cpp | 175 |
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 Binary files differnew file mode 100644 index 000000000..bd9a507c7 --- /dev/null +++ b/tests/auto/integration/multiapp/double-drop.wav 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" |