diff options
Diffstat (limited to 'tests/auto/integration')
148 files changed, 11793 insertions, 5799 deletions
diff --git a/tests/auto/integration/CMakeLists.txt b/tests/auto/integration/CMakeLists.txt new file mode 100644 index 000000000..9be80db63 --- /dev/null +++ b/tests/auto/integration/CMakeLists.txt @@ -0,0 +1,28 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +# Generated from integration.pro. + +# special case begin +add_subdirectory(qaudiodecoderbackend) +add_subdirectory(qaudiodevice) +add_subdirectory(qaudiosource) +add_subdirectory(qaudiosink) +add_subdirectory(qmediaplayerbackend) +add_subdirectory(qmediaplayerformatsupport) +add_subdirectory(qsoundeffect) +add_subdirectory(qvideoframebackend) +add_subdirectory(backends) +add_subdirectory(multiapp) +add_subdirectory(qmediaframeinputsbackend) +if(TARGET Qt::Widgets) + add_subdirectory(qmediacapturesession) + add_subdirectory(qcamerabackend) + add_subdirectory(qscreencapturebackend) + add_subdirectory(qwindowcapturebackend) +endif() +if(TARGET Qt::Quick) + add_subdirectory(qquickvideooutput) + add_subdirectory(qquickvideooutput_window) +endif() +# special case end diff --git a/tests/auto/integration/backends/CMakeLists.txt b/tests/auto/integration/backends/CMakeLists.txt new file mode 100644 index 000000000..b65293b5e --- /dev/null +++ b/tests/auto/integration/backends/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright (C) 2024 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +qt_internal_add_test(tst_backends + SOURCES + tst_backends.cpp + LIBRARIES + Qt::MultimediaPrivate +) diff --git a/tests/auto/integration/backends/tst_backends.cpp b/tests/auto/integration/backends/tst_backends.cpp new file mode 100644 index 000000000..2cc1df256 --- /dev/null +++ b/tests/auto/integration/backends/tst_backends.cpp @@ -0,0 +1,60 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include <QtTest/QtTest> +#include <QDebug> +#include <QtCore/qsysinfo.h> +#include <private/qplatformmediaintegration_p.h> + +QT_USE_NAMESPACE + +class tst_backends : public QObject +{ + Q_OBJECT + +public slots: + void initTestCase() + { + // Log operating system name and currently supported backends + qDebug() << QSysInfo::prettyProductName() << "supports backends" + << QPlatformMediaIntegration::availableBackends().join(", "); + } + +private slots: + void availableBackends_returns_expectedBackends_data() + { + QTest::addColumn<QStringList>("expectedBackends"); + QStringList backends; + +#if defined(Q_OS_WIN) + backends << "windows"; + if (QSysInfo::currentCpuArchitecture() == "x86_64") + backends << "ffmpeg"; +#elif defined(Q_OS_ANDROID) + backends << "android" << "ffmpeg"; +#elif defined(Q_OS_DARWIN) + backends << "darwin" << "ffmpeg"; +#elif defined(Q_OS_WASM) + backends << "wasm"; +#elif defined(Q_OS_QNX) + backends << "qnx"; +#else + backends << "ffmpeg" << "gstreamer"; +#endif + + QTest::addRow("backends") << backends; + } + + void availableBackends_returns_expectedBackends() + { + QFETCH(QStringList, expectedBackends); + QStringList actualBackends = QPlatformMediaIntegration::availableBackends(); + for (const auto &expectedBackend : expectedBackends) { + QVERIFY(actualBackends.contains(expectedBackend)); + } + } +}; + +QTEST_MAIN(tst_backends) + +#include "tst_backends.moc" diff --git a/tests/auto/integration/integration.pro b/tests/auto/integration/integration.pro deleted file mode 100644 index c61fbd4ee..000000000 --- a/tests/auto/integration/integration.pro +++ /dev/null @@ -1,3 +0,0 @@ -TEMPLATE = subdirs - -SUBDIRS += multimedia.pro 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..793a56e9d --- /dev/null +++ b/tests/auto/integration/multiapp/tst_multiapp.cpp @@ -0,0 +1,161 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#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_whenRecreatingApplication() + { + QVERIFY(executeTestOutOfProcess( + "mediaDevices_doesNotCrash_whenRecreatingApplication_impl"_L1)); + } + + bool mediaDevices_doesNotCrash_whenRecreatingApplication_impl(int argc, char ** argv) + { + { + QCoreApplication app{ argc, argv }; + QMediaDevices::defaultAudioOutput(); + } + { + QCoreApplication app{ argc, argv }; + QMediaDevices::defaultAudioOutput(); + } + + 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" diff --git a/tests/auto/integration/multimedia.pro b/tests/auto/integration/multimedia.pro deleted file mode 100644 index 88960ec03..000000000 --- a/tests/auto/integration/multimedia.pro +++ /dev/null @@ -1,19 +0,0 @@ - -TEMPLATE = subdirs -SUBDIRS += \ - qaudiodecoderbackend \ - qaudiodeviceinfo \ - qaudioinput \ - qaudiooutput \ - qmediaplayerbackend \ - qcamerabackend \ - qsoundeffect \ - qsound - -qtHaveModule(quick) { - SUBDIRS += \ - qdeclarativevideooutput \ - qdeclarativevideooutput_window -} - -!qtHaveModule(widgets): SUBDIRS -= qcamerabackend diff --git a/tests/auto/integration/qaudiodecoderbackend/BLACKLIST b/tests/auto/integration/qaudiodecoderbackend/BLACKLIST deleted file mode 100644 index 316c5a083..000000000 --- a/tests/auto/integration/qaudiodecoderbackend/BLACKLIST +++ /dev/null @@ -1,2 +0,0 @@ -# QTBUG-56796 -windows diff --git a/tests/auto/integration/qaudiodecoderbackend/CMakeLists.txt b/tests/auto/integration/qaudiodecoderbackend/CMakeLists.txt new file mode 100644 index 000000000..d2206182f --- /dev/null +++ b/tests/auto/integration/qaudiodecoderbackend/CMakeLists.txt @@ -0,0 +1,29 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +# Generated from qaudiodecoderbackend.pro. + +##################################################################### +## tst_qaudiodecoderbackend Test: +##################################################################### + +# Collect test data +file(GLOB_RECURSE test_data_glob + RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} + testdata/*) +list(APPEND test_data ${test_data_glob}) + +qt_internal_add_test(tst_qaudiodecoderbackend + SOURCES + tst_qaudiodecoderbackend.cpp + ../shared/mediafileselector.h + ../shared/mediabackendutils.h + INCLUDE_DIRECTORIES + ../shared/ + ../../../../src/multimedia/audio + LIBRARIES + Qt::Gui + Qt::Multimedia + Qt::MultimediaPrivate + TESTDATA ${test_data} +) diff --git a/tests/auto/integration/qaudiodecoderbackend/qaudiodecoderbackend.pro b/tests/auto/integration/qaudiodecoderbackend/qaudiodecoderbackend.pro deleted file mode 100644 index 672bcfa6a..000000000 --- a/tests/auto/integration/qaudiodecoderbackend/qaudiodecoderbackend.pro +++ /dev/null @@ -1,21 +0,0 @@ -TARGET = tst_qaudiodecoderbackend - -QT += multimedia multimedia-private testlib - -# This is more of a system test -CONFIG += testcase -TESTDATA += testdata/* - -INCLUDEPATH += \ - ../../../../src/multimedia/audio - -HEADERS += \ - ../shared/mediafileselector.h - -SOURCES += \ - tst_qaudiodecoderbackend.cpp - -boot2qt: { - # Yocto sysroot does not have gstreamer/wav - QMAKE_CXXFLAGS += -DWAV_SUPPORT_NOT_FORCED -} diff --git a/tests/auto/integration/qaudiodecoderbackend/testdata/test-no-audio-track.mp4 b/tests/auto/integration/qaudiodecoderbackend/testdata/test-no-audio-track.mp4 Binary files differnew file mode 100644 index 000000000..6b67a3433 --- /dev/null +++ b/tests/auto/integration/qaudiodecoderbackend/testdata/test-no-audio-track.mp4 diff --git a/tests/auto/integration/qaudiodecoderbackend/tst_qaudiodecoderbackend.cpp b/tests/auto/integration/qaudiodecoderbackend/tst_qaudiodecoderbackend.cpp index 1e582d14b..5a48b4457 100644 --- a/tests/auto/integration/qaudiodecoderbackend/tst_qaudiodecoderbackend.cpp +++ b/tests/auto/integration/qaudiodecoderbackend/tst_qaudiodecoderbackend.cpp @@ -1,40 +1,30 @@ -/**************************************************************************** -** -** Copyright (C) 2016 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:GPL-EXCEPT$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 3 as published by the Free Software -** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #include <QtTest/QtTest> #include <QDebug> #include "qaudiodecoder.h" -#include "../shared/mediafileselector.h" +#include "mediafileselector.h" +#include "mediabackendutils.h" -#define TEST_FILE_NAME "testdata/test.wav" -#define TEST_UNSUPPORTED_FILE_NAME "testdata/test-unsupported.avi" -#define TEST_CORRUPTED_FILE_NAME "testdata/test-corrupted.wav" +constexpr char TEST_FILE_NAME[] = "testdata/test.wav"; +constexpr char TEST_UNSUPPORTED_FILE_NAME[] = "testdata/test-unsupported.avi"; +constexpr char TEST_CORRUPTED_FILE_NAME[] = "testdata/test-corrupted.wav"; +constexpr char TEST_INVALID_SOURCE[] = "invalid"; +constexpr char TEST_NO_AUDIO_TRACK[] = "testdata/test-no-audio-track.mp4"; + +constexpr int testFileSampleCount = 44094; +constexpr int testFileSampleRate = 44100; + +constexpr std::chrono::microseconds testFileDuration = [] { + using namespace std::chrono; + using namespace std::chrono_literals; + auto duration = nanoseconds(1s) * testFileSampleCount / testFileSampleRate; + return round<microseconds>(duration); +}(); + +constexpr qint64 testFileDurationUs = qint64(testFileDuration.count()); QT_USE_NAMESPACE @@ -54,13 +44,29 @@ public slots: void initTestCase(); private slots: + void testMediaFilesAreSupported(); + void directBruteForceReading(); + void indirectReadingByBufferReadySignal(); + void indirectReadingByBufferAvailableSignal(); + void stopOnBufferReady(); + void restartOnBufferReady(); + void restartOnFinish(); void fileTest(); void unsupportedFileTest(); void corruptedFileTest(); + void invalidSource(); void deviceTest(); + void play_emitsFormatError_whenMediaHasNoAudioTrack(); private: - bool isWavSupported(); + QUrl testFileUrl(const QString filePath); + void checkNoMoreChanges(QAudioDecoder &decoder); +#ifdef Q_OS_ANDROID + QTemporaryFile *temporaryFile = nullptr; +#endif + + MediaFileSelector m_mediaSelector; + MaybeUrl m_wavFile = QUnexpect{}; }; void tst_QAudioDecoderBackend::init() @@ -70,81 +76,345 @@ void tst_QAudioDecoderBackend::init() void tst_QAudioDecoderBackend::initTestCase() { QAudioDecoder d; - if (!d.isAvailable()) + if (!d.isSupported()) QSKIP("Audio decoder service is not available"); - qRegisterMetaType<QMediaContent>(); + m_wavFile = m_mediaSelector.select(QFINDTESTDATA(TEST_FILE_NAME)); } void tst_QAudioDecoderBackend::cleanup() { +#ifdef Q_OS_ANDROID + if (temporaryFile) { + delete temporaryFile; + temporaryFile = nullptr; + } +#endif } -bool tst_QAudioDecoderBackend::isWavSupported() +QUrl tst_QAudioDecoderBackend::testFileUrl(const QString filePath) { -#ifdef WAV_SUPPORT_NOT_FORCED - return !MediaFileSelector::selectMediaFile(QStringList() << QFINDTESTDATA(TEST_FILE_NAME)).isNull(); + QUrl url; +#ifndef Q_OS_ANDROID + QFileInfo fileInfo(QFINDTESTDATA(filePath)); + url = QUrl::fromLocalFile(fileInfo.absoluteFilePath()); #else - return true; + QFile file(":/" + filePath); + if (temporaryFile) { + delete temporaryFile; + temporaryFile = nullptr; + } + if (file.open(QIODevice::ReadOnly)) { + temporaryFile = QTemporaryFile::createNativeFile(file); + url = QUrl(temporaryFile->fileName()); + } #endif + return url; +} + +void tst_QAudioDecoderBackend::checkNoMoreChanges(QAudioDecoder &decoder) +{ + QSignalSpy finishedSpy(&decoder, &QAudioDecoder::finished); + QSignalSpy bufferReadySpy(&decoder, &QAudioDecoder::bufferReady); + QSignalSpy bufferAvailableSpy(&decoder, &QAudioDecoder::bufferAvailableChanged); + + QTest::qWait(50); // wait a bit to check nothing happened after finish + + QCOMPARE(finishedSpy.size(), 0); + QCOMPARE(bufferReadySpy.size(), 0); + QCOMPARE(bufferAvailableSpy.size(), 0); +} + +void tst_QAudioDecoderBackend::testMediaFilesAreSupported() +{ + QCOMPARE(m_mediaSelector.dumpErrors(), ""); +} + +void tst_QAudioDecoderBackend::directBruteForceReading() +{ + CHECK_SELECTED_URL(m_wavFile); + + QAudioDecoder decoder; + if (decoder.error() == QAudioDecoder::NotSupportedError) + QSKIP("There is no audio decoding support on this platform."); + + int sampleCount = 0; + + decoder.setSource(*m_wavFile); + QVERIFY(!decoder.isDecoding()); + QVERIFY(!decoder.bufferAvailable()); + + decoder.start(); + QTRY_VERIFY(decoder.isDecoding()); + + auto waitAndCheck = [](auto &&predicate) { QVERIFY(QTest::qWaitFor(predicate)); }; + + auto waitForBufferAvailable = [&]() { + waitAndCheck([&]() { return !decoder.isDecoding() || decoder.bufferAvailable(); }); + + return decoder.bufferAvailable(); + }; + + while (waitForBufferAvailable()) { + auto buffer = decoder.read(); + QVERIFY(buffer.isValid()); + + sampleCount += buffer.sampleCount(); + } + + checkNoMoreChanges(decoder); + + QCOMPARE(sampleCount, testFileSampleCount); +} + +void tst_QAudioDecoderBackend::indirectReadingByBufferReadySignal() +{ + CHECK_SELECTED_URL(m_wavFile); + + QAudioDecoder decoder; + if (decoder.error() == QAudioDecoder::NotSupportedError) + QSKIP("There is no audio decoding support on this platform."); + + int sampleCount = 0; + + connect(&decoder, &QAudioDecoder::bufferReady, this, [&]() { + QVERIFY(decoder.bufferAvailable()); + + auto buffer = decoder.read(); + QVERIFY(buffer.isValid()); + QVERIFY(!decoder.bufferAvailable()); + + sampleCount += buffer.sampleCount(); + }); + + QSignalSpy decodingSpy(&decoder, &QAudioDecoder::isDecodingChanged); + QSignalSpy finishSpy(&decoder, &QAudioDecoder::finished); + + decoder.setSource(*m_wavFile); + QVERIFY(!decoder.isDecoding()); + QVERIFY(!decoder.bufferAvailable()); + + decoder.start(); + QTRY_VERIFY(decodingSpy.size() >= 1); + + QTRY_VERIFY(finishSpy.size() == 1); + QVERIFY(!decoder.isDecoding()); + + checkNoMoreChanges(decoder); + + QCOMPARE(sampleCount, testFileSampleCount); + QCOMPARE(finishSpy.size(), 1); +} + +void tst_QAudioDecoderBackend::indirectReadingByBufferAvailableSignal() { + CHECK_SELECTED_URL(m_wavFile); + + QAudioDecoder decoder; + if (decoder.error() == QAudioDecoder::NotSupportedError) + QSKIP("There is no audio decoding support on this platform."); + + int sampleCount = 0; + + connect(&decoder, &QAudioDecoder::bufferAvailableChanged, this, [&](bool available) { + QCOMPARE(decoder.bufferAvailable(), available); + + if (!available) + return; + + while (decoder.bufferAvailable()) { + auto buffer = decoder.read(); + QVERIFY(buffer.isValid()); + + sampleCount += buffer.sampleCount(); + } + }); + + QSignalSpy decodingSpy(&decoder, &QAudioDecoder::isDecodingChanged); + QSignalSpy finishSpy(&decoder, &QAudioDecoder::finished); + + decoder.setSource(*m_wavFile); + QVERIFY(!decoder.isDecoding()); + QVERIFY(!decoder.bufferAvailable()); + + decoder.start(); + QTRY_VERIFY(decodingSpy.size() >= 1); + + QTRY_VERIFY(finishSpy.size() == 1); + QVERIFY(!decoder.isDecoding()); + + checkNoMoreChanges(decoder); + + QCOMPARE(sampleCount, testFileSampleCount); + QCOMPARE(finishSpy.size(), 1); +} + +void tst_QAudioDecoderBackend::stopOnBufferReady() +{ + CHECK_SELECTED_URL(m_wavFile); + + QAudioDecoder decoder; + if (decoder.error() == QAudioDecoder::NotSupportedError) + QSKIP("There is no audio decoding support on this platform."); + + connect(&decoder, &QAudioDecoder::bufferReady, this, [&]() { + decoder.read(); // run next reading + decoder.stop(); + }); + + QSignalSpy finishSpy(&decoder, &QAudioDecoder::finished); + QSignalSpy bufferReadySpy(&decoder, &QAudioDecoder::bufferReady); + + decoder.setSource(*m_wavFile); + decoder.start(); + + bufferReadySpy.wait(); + QVERIFY(!decoder.isDecoding()); + + checkNoMoreChanges(decoder); + + QCOMPARE(bufferReadySpy.size(), 1); +} + +void tst_QAudioDecoderBackend::restartOnBufferReady() +{ + QSKIP_GSTREAMER("QTBUG-124005: failures on gstreamer"); + + CHECK_SELECTED_URL(m_wavFile); + + QAudioDecoder decoder; + if (decoder.error() == QAudioDecoder::NotSupportedError) + QSKIP("There is no audio decoding support on this platform."); + + int sampleCount = 0; + + std::once_flag restartOnce; + connect(&decoder, &QAudioDecoder::bufferReady, this, [&]() { + QVERIFY(decoder.bufferAvailable()); + + auto buffer = decoder.read(); + QVERIFY(buffer.isValid()); + QVERIFY(!decoder.bufferAvailable()); + + sampleCount += buffer.sampleCount(); + + std::call_once(restartOnce, [&]() { + sampleCount = 0; + decoder.stop(); + decoder.start(); + }); + }); + + QSignalSpy finishSpy(&decoder, &QAudioDecoder::finished); + + decoder.setSource(*m_wavFile); + decoder.start(); + + QTRY_VERIFY2(finishSpy.size() == 2, "Wait for signals after restart and after finishing"); + QVERIFY(!decoder.isDecoding()); + + checkNoMoreChanges(decoder); + + QCOMPARE(sampleCount, testFileSampleCount); +} + +void tst_QAudioDecoderBackend::restartOnFinish() +{ + CHECK_SELECTED_URL(m_wavFile); + + QAudioDecoder decoder; + if (decoder.error() == QAudioDecoder::NotSupportedError) + QSKIP("There is no audio decoding support on this platform."); + + int sampleCount = 0; + + connect(&decoder, &QAudioDecoder::bufferReady, this, [&]() { + auto buffer = decoder.read(); + QVERIFY(buffer.isValid()); + + sampleCount += buffer.sampleCount(); + }); + + QSignalSpy finishSpy(&decoder, &QAudioDecoder::finished); + + std::once_flag restartOnce; + connect(&decoder, &QAudioDecoder::finished, this, [&]() { + QVERIFY(!decoder.bufferAvailable()); + QVERIFY(!decoder.isDecoding()); + + std::call_once(restartOnce, [&]() { + sampleCount = 0; + decoder.start(); + }); + }); + + decoder.setSource(*m_wavFile); + decoder.start(); + + QTRY_VERIFY(finishSpy.size() == 2); + + QVERIFY(!decoder.isDecoding()); + + checkNoMoreChanges(decoder); + QCOMPARE(sampleCount, testFileSampleCount); } void tst_QAudioDecoderBackend::fileTest() { - if (!isWavSupported()) - QSKIP("Sound format is not supported"); + CHECK_SELECTED_URL(m_wavFile); QAudioDecoder d; - if (d.error() == QAudioDecoder::ServiceMissingError) + if (d.error() == QAudioDecoder::NotSupportedError) QSKIP("There is no audio decoding support on this platform."); QAudioBuffer buffer; quint64 duration = 0; int byteCount = 0; int sampleCount = 0; - QVERIFY(d.state() == QAudioDecoder::StoppedState); + QVERIFY(!d.isDecoding()); QVERIFY(d.bufferAvailable() == false); - QCOMPARE(d.sourceFilename(), QString("")); + QCOMPARE(d.source(), QStringLiteral("")); QVERIFY(d.audioFormat() == QAudioFormat()); // Test local file - QFileInfo fileInfo(QFINDTESTDATA(TEST_FILE_NAME)); - d.setSourceFilename(fileInfo.absoluteFilePath()); - QVERIFY(d.state() == QAudioDecoder::StoppedState); + + d.setSource(*m_wavFile); + QVERIFY(!d.isDecoding()); QVERIFY(!d.bufferAvailable()); - QCOMPARE(d.sourceFilename(), fileInfo.absoluteFilePath()); + QCOMPARE(d.source(), *m_wavFile); - QSignalSpy readySpy(&d, SIGNAL(bufferReady())); - QSignalSpy bufferChangedSpy(&d, SIGNAL(bufferAvailableChanged(bool))); + QSignalSpy readySpy(&d, &QAudioDecoder::bufferReady); + QSignalSpy bufferChangedSpy(&d, &QAudioDecoder::bufferAvailableChanged); QSignalSpy errorSpy(&d, SIGNAL(error(QAudioDecoder::Error))); - QSignalSpy stateSpy(&d, SIGNAL(stateChanged(QAudioDecoder::State))); - QSignalSpy durationSpy(&d, SIGNAL(durationChanged(qint64))); - QSignalSpy finishedSpy(&d, SIGNAL(finished())); - QSignalSpy positionSpy(&d, SIGNAL(positionChanged(qint64))); + QSignalSpy isDecodingSpy(&d, &QAudioDecoder::isDecodingChanged); + QSignalSpy durationSpy(&d, &QAudioDecoder::durationChanged); + QSignalSpy finishedSpy(&d, &QAudioDecoder::finished); + QSignalSpy positionSpy(&d, &QAudioDecoder::positionChanged); d.start(); - QTRY_VERIFY(d.state() == QAudioDecoder::DecodingState); - QTRY_VERIFY(!stateSpy.isEmpty()); + + QTRY_VERIFY(!isDecodingSpy.isEmpty()); QTRY_VERIFY(!readySpy.isEmpty()); QTRY_VERIFY(!bufferChangedSpy.isEmpty()); QVERIFY(d.bufferAvailable()); QTRY_VERIFY(!durationSpy.isEmpty()); - QVERIFY(qAbs(d.duration() - 1000) < 20); + + QVERIFY(qAbs(durationSpy.front().front().value<qint64>() - 1000) < 20); + if (finishedSpy.empty()) + QVERIFY(qAbs(d.duration() - 1000) < 20); + else + QCOMPARE(d.duration(), -1); buffer = d.read(); QVERIFY(buffer.isValid()); // Test file is 44.1K 16bit mono, 44094 samples QCOMPARE(buffer.format().channelCount(), 1); - QCOMPARE(buffer.format().sampleRate(), 44100); - QCOMPARE(buffer.format().sampleSize(), 16); - QCOMPARE(buffer.format().sampleType(), QAudioFormat::SignedInt); - QCOMPARE(buffer.format().codec(), QString("audio/pcm")); + QCOMPARE(buffer.format().sampleRate(), testFileSampleRate); + QCOMPARE(buffer.format().sampleFormat(), QAudioFormat::Int16); QCOMPARE(buffer.byteCount(), buffer.sampleCount() * 2); // 16bit mono // The decoder should still have no format set QVERIFY(d.audioFormat() == QAudioFormat()); - QVERIFY(errorSpy.isEmpty()); duration += buffer.duration(); @@ -152,53 +422,61 @@ void tst_QAudioDecoderBackend::fileTest() byteCount += buffer.byteCount(); // Now drain the decoder - if (sampleCount < 44094) { + if (sampleCount < testFileSampleCount) { QTRY_COMPARE(d.bufferAvailable(), true); } + auto durationToMs = [](uint64_t dur) { + if (isGStreamerPlatform()) + return std::round(dur / 1000.0); + else + return dur / 1000.0; + }; + while (d.bufferAvailable()) { buffer = d.read(); QVERIFY(buffer.isValid()); QTRY_VERIFY(!positionSpy.isEmpty()); - QVERIFY(positionSpy.takeLast().at(0).toLongLong() == qint64(duration / 1000)); + QCOMPARE(positionSpy.takeLast().at(0).toLongLong(), qint64(durationToMs(duration))); duration += buffer.duration(); sampleCount += buffer.sampleCount(); byteCount += buffer.byteCount(); - if (sampleCount < 44094) { + if (sampleCount < testFileSampleCount) { QTRY_COMPARE(d.bufferAvailable(), true); } } // Make sure the duration is roughly correct (+/- 20ms) - QCOMPARE(sampleCount, 44094); - QCOMPARE(byteCount, 44094 * 2); + QCOMPARE(sampleCount, testFileSampleCount); + QCOMPARE(byteCount, testFileSampleCount * 2); QVERIFY(qAbs(qint64(duration) - 1000000) < 20000); QVERIFY(qAbs((d.position() + (buffer.duration() / 1000)) - 1000) < 20); - QTRY_COMPARE(finishedSpy.count(), 1); + QTRY_COMPARE(finishedSpy.size(), 1); QVERIFY(!d.bufferAvailable()); - QTRY_COMPARE(d.state(), QAudioDecoder::StoppedState); + QTRY_VERIFY(!d.isDecoding()); d.stop(); - QTRY_COMPARE(d.state(), QAudioDecoder::StoppedState); - QTRY_COMPARE(durationSpy.count(), 2); + QTRY_VERIFY(!d.isDecoding()); + QTRY_COMPARE(durationSpy.size(), 2); QCOMPARE(d.duration(), qint64(-1)); QVERIFY(!d.bufferAvailable()); readySpy.clear(); bufferChangedSpy.clear(); - stateSpy.clear(); + isDecodingSpy.clear(); durationSpy.clear(); finishedSpy.clear(); positionSpy.clear(); +#ifdef Q_OS_ANDROID + QSKIP("Setting a desired audio format is not yet supported on Android", QTest::SkipSingle); +#endif // change output audio format QAudioFormat format; format.setChannelCount(2); - format.setSampleSize(8); format.setSampleRate(11050); - format.setCodec("audio/pcm"); - format.setSampleType(QAudioFormat::SignedInt); + format.setSampleFormat(QAudioFormat::UInt8); d.setAudioFormat(format); @@ -213,13 +491,16 @@ void tst_QAudioDecoderBackend::fileTest() byteCount = 0; d.start(); - QTRY_VERIFY(d.state() == QAudioDecoder::DecodingState); - QTRY_VERIFY(!stateSpy.isEmpty()); + QTRY_VERIFY(!isDecodingSpy.isEmpty()); QTRY_VERIFY(!readySpy.isEmpty()); QTRY_VERIFY(!bufferChangedSpy.isEmpty()); QVERIFY(d.bufferAvailable()); QTRY_VERIFY(!durationSpy.isEmpty()); - QVERIFY(qAbs(d.duration() - 1000) < 20); + QVERIFY(qAbs(durationSpy.front().front().value<qint64>() - 1000) < 20); + if (finishedSpy.empty()) + QVERIFY(qAbs(d.duration() - 1000) < 20); + else + QCOMPARE(d.duration(), -1); buffer = d.read(); QVERIFY(buffer.isValid()); @@ -235,40 +516,35 @@ void tst_QAudioDecoderBackend::fileTest() sampleCount += buffer.sampleCount(); byteCount += buffer.byteCount(); - // Now drain the decoder - if (duration < 998000) { - QTRY_COMPARE(d.bufferAvailable(), true); - } + while (finishedSpy.isEmpty() || d.bufferAvailable()) { + if (!d.bufferAvailable()) { + QTest::qWait(std::chrono::milliseconds(10)); + continue; + } - while (d.bufferAvailable()) { buffer = d.read(); QVERIFY(buffer.isValid()); QTRY_VERIFY(!positionSpy.isEmpty()); - QVERIFY(positionSpy.takeLast().at(0).toLongLong() == qint64(duration / 1000)); - QVERIFY(d.position() - (duration / 1000) < 20); + QCOMPARE(positionSpy.takeLast().at(0).toLongLong(), qlonglong(durationToMs(duration))); + QCOMPARE_LT(d.position() - durationToMs(duration), 20u); duration += buffer.duration(); sampleCount += buffer.sampleCount(); byteCount += buffer.byteCount(); - - if (duration < 998000) { - QTRY_COMPARE(d.bufferAvailable(), true); - } } // Resampling might end up with fewer or more samples // so be a bit sloppy - QVERIFY(qAbs(sampleCount - 22047) < 100); - QVERIFY(qAbs(byteCount - 22047) < 100); - QVERIFY(qAbs(qint64(duration) - 1000000) < 20000); - QVERIFY(qAbs((d.position() + (buffer.duration() / 1000)) - 1000) < 20); - QTRY_COMPARE(finishedSpy.count(), 1); + QCOMPARE_LT(qAbs(sampleCount - 22047), 100); + QCOMPARE_LT(qAbs(byteCount - 22047), 100); + QCOMPARE_LT(qAbs(qint64(duration) - testFileDurationUs), 20000); + QCOMPARE_LT(qAbs((d.position() + (buffer.duration() / 1000)) - 1000), 20); QVERIFY(!d.bufferAvailable()); - QTRY_COMPARE(d.state(), QAudioDecoder::StoppedState); + QVERIFY(!d.isDecoding()); d.stop(); - QTRY_COMPARE(d.state(), QAudioDecoder::StoppedState); - QTRY_COMPARE(durationSpy.count(), 2); + QTRY_VERIFY(!d.isDecoding()); + QTRY_COMPARE(durationSpy.size(), 2); QCOMPARE(d.duration(), qint64(-1)); QVERIFY(!d.bufferAvailable()); } @@ -279,32 +555,32 @@ void tst_QAudioDecoderBackend::fileTest() void tst_QAudioDecoderBackend::unsupportedFileTest() { QAudioDecoder d; - if (d.error() == QAudioDecoder::ServiceMissingError) + if (d.error() == QAudioDecoder::NotSupportedError) QSKIP("There is no audio decoding support on this platform."); QAudioBuffer buffer; - QVERIFY(d.state() == QAudioDecoder::StoppedState); + QVERIFY(!d.isDecoding()); QVERIFY(d.bufferAvailable() == false); - QCOMPARE(d.sourceFilename(), QString("")); + QCOMPARE(d.source(), QStringLiteral("")); QVERIFY(d.audioFormat() == QAudioFormat()); // Test local file - QFileInfo fileInfo(QFINDTESTDATA(TEST_UNSUPPORTED_FILE_NAME)); - d.setSourceFilename(fileInfo.absoluteFilePath()); - QVERIFY(d.state() == QAudioDecoder::StoppedState); + QUrl url = testFileUrl(TEST_UNSUPPORTED_FILE_NAME); + d.setSource(url); + QVERIFY(!d.isDecoding()); QVERIFY(!d.bufferAvailable()); - QCOMPARE(d.sourceFilename(), fileInfo.absoluteFilePath()); + QCOMPARE(d.source(), url); - QSignalSpy readySpy(&d, SIGNAL(bufferReady())); - QSignalSpy bufferChangedSpy(&d, SIGNAL(bufferAvailableChanged(bool))); + QSignalSpy readySpy(&d, &QAudioDecoder::bufferReady); + QSignalSpy bufferChangedSpy(&d, &QAudioDecoder::bufferAvailableChanged); QSignalSpy errorSpy(&d, SIGNAL(error(QAudioDecoder::Error))); - QSignalSpy stateSpy(&d, SIGNAL(stateChanged(QAudioDecoder::State))); - QSignalSpy durationSpy(&d, SIGNAL(durationChanged(qint64))); - QSignalSpy finishedSpy(&d, SIGNAL(finished())); - QSignalSpy positionSpy(&d, SIGNAL(positionChanged(qint64))); + QSignalSpy isDecodingSpy(&d, &QAudioDecoder::isDecodingChanged); + QSignalSpy durationSpy(&d, &QAudioDecoder::durationChanged); + QSignalSpy finishedSpy(&d, &QAudioDecoder::finished); + QSignalSpy positionSpy(&d, &QAudioDecoder::positionChanged); d.start(); - QTRY_VERIFY(d.state() == QAudioDecoder::StoppedState); + QTRY_VERIFY(!d.isDecoding()); QVERIFY(!d.bufferAvailable()); QCOMPARE(d.audioFormat(), QAudioFormat()); QCOMPARE(d.duration(), qint64(-1)); @@ -321,16 +597,17 @@ void tst_QAudioDecoderBackend::unsupportedFileTest() // Check all other spies. QVERIFY(readySpy.isEmpty()); QVERIFY(bufferChangedSpy.isEmpty()); - QVERIFY(stateSpy.isEmpty()); + QVERIFY(isDecodingSpy.isEmpty()); QVERIFY(finishedSpy.isEmpty()); QVERIFY(positionSpy.isEmpty()); - QVERIFY(durationSpy.isEmpty()); + // Either reject the file directly, or set the duration to 5secs on setUrl() and back to -1 on start() + QVERIFY(durationSpy.isEmpty() || durationSpy.size() == 2); errorSpy.clear(); // Try read even if the file is not supported to test robustness. buffer = d.read(); - QTRY_VERIFY(d.state() == QAudioDecoder::StoppedState); + QTRY_VERIFY(!d.isDecoding()); QVERIFY(!buffer.isValid()); QVERIFY(!d.bufferAvailable()); QCOMPARE(d.position(), qint64(-1)); @@ -338,14 +615,14 @@ void tst_QAudioDecoderBackend::unsupportedFileTest() QVERIFY(errorSpy.isEmpty()); QVERIFY(readySpy.isEmpty()); QVERIFY(bufferChangedSpy.isEmpty()); - QVERIFY(stateSpy.isEmpty()); + QVERIFY(isDecodingSpy.isEmpty()); QVERIFY(finishedSpy.isEmpty()); QVERIFY(positionSpy.isEmpty()); - QVERIFY(durationSpy.isEmpty()); + QVERIFY(durationSpy.isEmpty() || durationSpy.size() == 2); d.stop(); - QTRY_COMPARE(d.state(), QAudioDecoder::StoppedState); + QTRY_VERIFY(!d.isDecoding()); QCOMPARE(d.duration(), qint64(-1)); QVERIFY(!d.bufferAvailable()); } @@ -357,32 +634,32 @@ void tst_QAudioDecoderBackend::unsupportedFileTest() void tst_QAudioDecoderBackend::corruptedFileTest() { QAudioDecoder d; - if (d.error() == QAudioDecoder::ServiceMissingError) + if (d.error() == QAudioDecoder::NotSupportedError) QSKIP("There is no audio decoding support on this platform."); QAudioBuffer buffer; - QVERIFY(d.state() == QAudioDecoder::StoppedState); + QVERIFY(!d.isDecoding()); QVERIFY(d.bufferAvailable() == false); - QCOMPARE(d.sourceFilename(), QString("")); + QCOMPARE(d.source(), QUrl()); QVERIFY(d.audioFormat() == QAudioFormat()); // Test local file - QFileInfo fileInfo(QFINDTESTDATA(TEST_CORRUPTED_FILE_NAME)); - d.setSourceFilename(fileInfo.absoluteFilePath()); - QVERIFY(d.state() == QAudioDecoder::StoppedState); + QUrl url = testFileUrl(TEST_CORRUPTED_FILE_NAME); + d.setSource(url); + QVERIFY(!d.isDecoding()); QVERIFY(!d.bufferAvailable()); - QCOMPARE(d.sourceFilename(), fileInfo.absoluteFilePath()); + QCOMPARE(d.source(), url); - QSignalSpy readySpy(&d, SIGNAL(bufferReady())); - QSignalSpy bufferChangedSpy(&d, SIGNAL(bufferAvailableChanged(bool))); + QSignalSpy readySpy(&d, &QAudioDecoder::bufferReady); + QSignalSpy bufferChangedSpy(&d, &QAudioDecoder::bufferAvailableChanged); QSignalSpy errorSpy(&d, SIGNAL(error(QAudioDecoder::Error))); - QSignalSpy stateSpy(&d, SIGNAL(stateChanged(QAudioDecoder::State))); - QSignalSpy durationSpy(&d, SIGNAL(durationChanged(qint64))); - QSignalSpy finishedSpy(&d, SIGNAL(finished())); - QSignalSpy positionSpy(&d, SIGNAL(positionChanged(qint64))); + QSignalSpy isDecodingSpy(&d, &QAudioDecoder::isDecodingChanged); + QSignalSpy durationSpy(&d, &QAudioDecoder::durationChanged); + QSignalSpy finishedSpy(&d, &QAudioDecoder::finished); + QSignalSpy positionSpy(&d, &QAudioDecoder::positionChanged); d.start(); - QTRY_VERIFY(d.state() == QAudioDecoder::StoppedState); + QTRY_VERIFY(!d.isDecoding()); QVERIFY(!d.bufferAvailable()); QCOMPARE(d.audioFormat(), QAudioFormat()); QCOMPARE(d.duration(), qint64(-1)); @@ -399,7 +676,7 @@ void tst_QAudioDecoderBackend::corruptedFileTest() // Check all other spies. QVERIFY(readySpy.isEmpty()); QVERIFY(bufferChangedSpy.isEmpty()); - QVERIFY(stateSpy.isEmpty()); + QVERIFY(isDecodingSpy.isEmpty()); QVERIFY(finishedSpy.isEmpty()); QVERIFY(positionSpy.isEmpty()); QVERIFY(durationSpy.isEmpty()); @@ -408,7 +685,7 @@ void tst_QAudioDecoderBackend::corruptedFileTest() // Try read even if the file is corrupted to test the robustness. buffer = d.read(); - QTRY_VERIFY(d.state() == QAudioDecoder::StoppedState); + QTRY_VERIFY(!d.isDecoding()); QVERIFY(!buffer.isValid()); QVERIFY(!d.bufferAvailable()); QCOMPARE(d.position(), qint64(-1)); @@ -416,72 +693,161 @@ void tst_QAudioDecoderBackend::corruptedFileTest() QVERIFY(errorSpy.isEmpty()); QVERIFY(readySpy.isEmpty()); QVERIFY(bufferChangedSpy.isEmpty()); - QVERIFY(stateSpy.isEmpty()); + QVERIFY(isDecodingSpy.isEmpty()); QVERIFY(finishedSpy.isEmpty()); QVERIFY(positionSpy.isEmpty()); QVERIFY(durationSpy.isEmpty()); + d.stop(); + QTRY_VERIFY(!d.isDecoding()); + QCOMPARE(d.duration(), qint64(-1)); + QVERIFY(!d.bufferAvailable()); +} + +void tst_QAudioDecoderBackend::invalidSource() +{ + QAudioDecoder d; + if (d.error() == QAudioDecoder::NotSupportedError) + QSKIP("There is no audio decoding support on this platform."); + QAudioBuffer buffer; + + QVERIFY(!d.isDecoding()); + QVERIFY(d.bufferAvailable() == false); + QCOMPARE(d.source(), QUrl()); + QVERIFY(d.audioFormat() == QAudioFormat()); + + // Test invalid file source + QFileInfo fileInfo(TEST_INVALID_SOURCE); + QUrl url = QUrl::fromLocalFile(fileInfo.absoluteFilePath()); + d.setSource(url); + QVERIFY(!d.isDecoding()); + QVERIFY(!d.bufferAvailable()); + QCOMPARE(d.source(), url); + + QSignalSpy readySpy(&d, &QAudioDecoder::bufferReady); + QSignalSpy bufferChangedSpy(&d, &QAudioDecoder::bufferAvailableChanged); + QSignalSpy errorSpy(&d, SIGNAL(error(QAudioDecoder::Error))); + QSignalSpy isDecodingSpy(&d, &QAudioDecoder::isDecodingChanged); + QSignalSpy durationSpy(&d, &QAudioDecoder::durationChanged); + QSignalSpy finishedSpy(&d, &QAudioDecoder::finished); + QSignalSpy positionSpy(&d, &QAudioDecoder::positionChanged); + + d.start(); + QTRY_VERIFY(!d.isDecoding()); + QVERIFY(!d.bufferAvailable()); + QCOMPARE(d.audioFormat(), QAudioFormat()); + QCOMPARE(d.duration(), qint64(-1)); + QCOMPARE(d.position(), qint64(-1)); + + // Check the error code. + QTRY_VERIFY(!errorSpy.isEmpty()); + + // Have to use qvariant_cast, toInt will return 0 because unrecognized type; + QAudioDecoder::Error errorCode = qvariant_cast<QAudioDecoder::Error>(errorSpy.takeLast().at(0)); + QCOMPARE(errorCode, QAudioDecoder::ResourceError); + QCOMPARE(d.error(), QAudioDecoder::ResourceError); + + // Check all other spies. + QVERIFY(readySpy.isEmpty()); + QVERIFY(bufferChangedSpy.isEmpty()); + QVERIFY(isDecodingSpy.isEmpty()); + QVERIFY(finishedSpy.isEmpty()); + QVERIFY(positionSpy.isEmpty()); + QVERIFY(durationSpy.isEmpty()); + + errorSpy.clear(); + + d.stop(); + QTRY_VERIFY(!d.isDecoding()); + QCOMPARE(d.duration(), qint64(-1)); + QVERIFY(!d.bufferAvailable()); + + QFile file; + file.setFileName(TEST_INVALID_SOURCE); + file.open(QIODevice::ReadOnly); + d.setSourceDevice(&file); + + d.start(); + QTRY_VERIFY(!d.isDecoding()); + QVERIFY(!d.bufferAvailable()); + QCOMPARE(d.audioFormat(), QAudioFormat()); + QCOMPARE(d.duration(), qint64(-1)); + QCOMPARE(d.position(), qint64(-1)); + + // Check the error code. + QTRY_VERIFY(!errorSpy.isEmpty()); + errorCode = qvariant_cast<QAudioDecoder::Error>(errorSpy.takeLast().at(0)); + QCOMPARE(errorCode, QAudioDecoder::ResourceError); + QCOMPARE(d.error(), QAudioDecoder::ResourceError); + // Check all other spies. + QVERIFY(readySpy.isEmpty()); + QVERIFY(bufferChangedSpy.isEmpty()); + QVERIFY(isDecodingSpy.isEmpty()); + QVERIFY(finishedSpy.isEmpty()); + QVERIFY(positionSpy.isEmpty()); + QVERIFY(durationSpy.isEmpty()); + + errorSpy.clear(); d.stop(); - QTRY_COMPARE(d.state(), QAudioDecoder::StoppedState); + QTRY_VERIFY(!d.isDecoding()); QCOMPARE(d.duration(), qint64(-1)); QVERIFY(!d.bufferAvailable()); } void tst_QAudioDecoderBackend::deviceTest() { - if (!isWavSupported()) - QSKIP("Sound format is not supported"); + using namespace std::chrono; + CHECK_SELECTED_URL(m_wavFile); QAudioDecoder d; - if (d.error() == QAudioDecoder::ServiceMissingError) + if (d.error() == QAudioDecoder::NotSupportedError) QSKIP("There is no audio decoding support on this platform."); QAudioBuffer buffer; quint64 duration = 0; int sampleCount = 0; - QSignalSpy readySpy(&d, SIGNAL(bufferReady())); - QSignalSpy bufferChangedSpy(&d, SIGNAL(bufferAvailableChanged(bool))); + QSignalSpy readySpy(&d, &QAudioDecoder::bufferReady); + QSignalSpy bufferChangedSpy(&d, &QAudioDecoder::bufferAvailableChanged); QSignalSpy errorSpy(&d, SIGNAL(error(QAudioDecoder::Error))); - QSignalSpy stateSpy(&d, SIGNAL(stateChanged(QAudioDecoder::State))); - QSignalSpy durationSpy(&d, SIGNAL(durationChanged(qint64))); - QSignalSpy finishedSpy(&d, SIGNAL(finished())); - QSignalSpy positionSpy(&d, SIGNAL(positionChanged(qint64))); + QSignalSpy isDecodingSpy(&d, &QAudioDecoder::isDecodingChanged); + QSignalSpy durationSpy(&d, &QAudioDecoder::durationChanged); + QSignalSpy finishedSpy(&d, &QAudioDecoder::finished); + QSignalSpy positionSpy(&d, &QAudioDecoder::positionChanged); - QVERIFY(d.state() == QAudioDecoder::StoppedState); + QVERIFY(!d.isDecoding()); QVERIFY(d.bufferAvailable() == false); - QCOMPARE(d.sourceFilename(), QString("")); + QCOMPARE(d.source(), QStringLiteral("")); QVERIFY(d.audioFormat() == QAudioFormat()); - - QFileInfo fileInfo(QFINDTESTDATA(TEST_FILE_NAME)); - QFile file(fileInfo.absoluteFilePath()); + QFile file(m_wavFile->toString()); QVERIFY(file.open(QIODevice::ReadOnly)); d.setSourceDevice(&file); QVERIFY(d.sourceDevice() == &file); - QVERIFY(d.sourceFilename().isEmpty()); + QVERIFY(d.source().isEmpty()); // We haven't set the format yet QVERIFY(d.audioFormat() == QAudioFormat()); d.start(); - QTRY_VERIFY(d.state() == QAudioDecoder::DecodingState); - QTRY_VERIFY(!stateSpy.isEmpty()); + + QTRY_VERIFY(!isDecodingSpy.isEmpty()); QTRY_VERIFY(!readySpy.isEmpty()); QTRY_VERIFY(!bufferChangedSpy.isEmpty()); QVERIFY(d.bufferAvailable()); QTRY_VERIFY(!durationSpy.isEmpty()); - QVERIFY(qAbs(d.duration() - 1000) < 20); + if (finishedSpy.empty()) + QVERIFY(qAbs(d.duration() - 1000) < 20); + else + QCOMPARE(d.duration(), -1); buffer = d.read(); QVERIFY(buffer.isValid()); // Test file is 44.1K 16bit mono QCOMPARE(buffer.format().channelCount(), 1); - QCOMPARE(buffer.format().sampleRate(), 44100); - QCOMPARE(buffer.format().sampleSize(), 16); - QCOMPARE(buffer.format().sampleType(), QAudioFormat::SignedInt); - QCOMPARE(buffer.format().codec(), QString("audio/pcm")); + QCOMPARE(buffer.format().sampleRate(), testFileSampleRate); + QCOMPARE(buffer.format().sampleFormat(), QAudioFormat::Int16); QVERIFY(errorSpy.isEmpty()); @@ -489,7 +855,7 @@ void tst_QAudioDecoderBackend::deviceTest() sampleCount += buffer.sampleCount(); // Now drain the decoder - if (sampleCount < 44094) { + if (sampleCount < testFileSampleCount) { QTRY_COMPARE(d.bufferAvailable(), true); } @@ -497,43 +863,50 @@ void tst_QAudioDecoderBackend::deviceTest() buffer = d.read(); QVERIFY(buffer.isValid()); QTRY_VERIFY(!positionSpy.isEmpty()); - QVERIFY(positionSpy.takeLast().at(0).toLongLong() == qint64(duration / 1000)); + if (isGStreamerPlatform()) + QCOMPARE_EQ(positionSpy.takeLast().at(0).toLongLong(), + round<milliseconds>(microseconds{ duration }).count()); + else + QCOMPARE_EQ(positionSpy.takeLast().at(0).toLongLong(), + floor<milliseconds>(microseconds{ duration }).count()); + QVERIFY(d.position() - (duration / 1000) < 20); duration += buffer.duration(); sampleCount += buffer.sampleCount(); - if (sampleCount < 44094) { + if (sampleCount < testFileSampleCount) { QTRY_COMPARE(d.bufferAvailable(), true); } } // Make sure the duration is roughly correct (+/- 20ms) - QCOMPARE(sampleCount, 44094); + QCOMPARE(sampleCount, testFileSampleCount); QVERIFY(qAbs(qint64(duration) - 1000000) < 20000); QVERIFY(qAbs((d.position() + (buffer.duration() / 1000)) - 1000) < 20); - QTRY_COMPARE(finishedSpy.count(), 1); + QTRY_COMPARE(finishedSpy.size(), 1); QVERIFY(!d.bufferAvailable()); - QTRY_COMPARE(d.state(), QAudioDecoder::StoppedState); + QTRY_VERIFY(!d.isDecoding()); d.stop(); - QTRY_COMPARE(d.state(), QAudioDecoder::StoppedState); + QTRY_VERIFY(!d.isDecoding()); QVERIFY(!d.bufferAvailable()); - QTRY_COMPARE(durationSpy.count(), 2); + QTRY_COMPARE(durationSpy.size(), 2); QCOMPARE(d.duration(), qint64(-1)); readySpy.clear(); bufferChangedSpy.clear(); - stateSpy.clear(); + isDecodingSpy.clear(); durationSpy.clear(); finishedSpy.clear(); positionSpy.clear(); +#ifdef Q_OS_ANDROID + QSKIP("Setting a desired audio format is not yet supported on Android", QTest::SkipSingle); +#endif // Now try changing formats QAudioFormat format; format.setChannelCount(2); - format.setSampleSize(8); format.setSampleRate(8000); - format.setCodec("audio/pcm"); - format.setSampleType(QAudioFormat::SignedInt); + format.setSampleFormat(QAudioFormat::UInt8); d.setAudioFormat(format); @@ -541,13 +914,18 @@ void tst_QAudioDecoderBackend::deviceTest() QVERIFY(d.audioFormat() == format); d.start(); - QTRY_VERIFY(d.state() == QAudioDecoder::DecodingState); - QTRY_VERIFY(!stateSpy.isEmpty()); + QVERIFY(d.error() == QAudioDecoder::NoError); + QTRY_VERIFY(!isDecodingSpy.isEmpty()); QTRY_VERIFY(!readySpy.isEmpty()); QTRY_VERIFY(!bufferChangedSpy.isEmpty()); QVERIFY(d.bufferAvailable()); QTRY_VERIFY(!durationSpy.isEmpty()); - QVERIFY(qAbs(d.duration() - 1000) < 20); + + QVERIFY(qAbs(durationSpy.front().front().value<qint64>() - 1000) < 20); + if (finishedSpy.empty()) + QVERIFY(qAbs(d.duration() - 1000) < 20); + else + QCOMPARE(d.duration(), -1); buffer = d.read(); QVERIFY(buffer.isValid()); @@ -560,12 +938,28 @@ void tst_QAudioDecoderBackend::deviceTest() QVERIFY(errorSpy.isEmpty()); d.stop(); - QTRY_COMPARE(d.state(), QAudioDecoder::StoppedState); + QTRY_VERIFY(!d.isDecoding()); QVERIFY(!d.bufferAvailable()); - QTRY_COMPARE(durationSpy.count(), 2); + QTRY_COMPARE(durationSpy.size(), 2); QCOMPARE(d.duration(), qint64(-1)); } +void tst_QAudioDecoderBackend::play_emitsFormatError_whenMediaHasNoAudioTrack() +{ + QSKIP_GSTREAMER("QTBUG-124206: gstreamer does not emit errors"); + + QAudioDecoder decoder; + + QSignalSpy errors{ &decoder, qOverload<QAudioDecoder::Error>(&QAudioDecoder::error) }; + + decoder.setSource(testFileUrl(TEST_NO_AUDIO_TRACK)); + decoder.start(); + + QTRY_VERIFY(!errors.empty()); + + QCOMPARE_EQ(decoder.error(), QAudioDecoder::Error::FormatError); +} + QTEST_MAIN(tst_QAudioDecoderBackend) #include "tst_qaudiodecoderbackend.moc" diff --git a/tests/auto/integration/qaudiodeviceinfo/BLACKLIST b/tests/auto/integration/qaudiodevice/BLACKLIST index 40dc63a09..40dc63a09 100644 --- a/tests/auto/integration/qaudiodeviceinfo/BLACKLIST +++ b/tests/auto/integration/qaudiodevice/BLACKLIST diff --git a/tests/auto/integration/qaudiodevice/CMakeLists.txt b/tests/auto/integration/qaudiodevice/CMakeLists.txt new file mode 100644 index 000000000..93f0d49dd --- /dev/null +++ b/tests/auto/integration/qaudiodevice/CMakeLists.txt @@ -0,0 +1,16 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +# Generated from qaudiodevice.pro. + +##################################################################### +## tst_qaudiodevice Test: +##################################################################### + +qt_internal_add_test(tst_qaudiodevice + SOURCES + tst_qaudiodevice.cpp + LIBRARIES + Qt::Gui + Qt::MultimediaPrivate +) diff --git a/tests/auto/integration/qaudiodevice/tst_qaudiodevice.cpp b/tests/auto/integration/qaudiodevice/tst_qaudiodevice.cpp new file mode 100644 index 000000000..cd686bd08 --- /dev/null +++ b/tests/auto/integration/qaudiodevice/tst_qaudiodevice.cpp @@ -0,0 +1,164 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + + +#include <QtTest/QtTest> +#include <QtCore/qlocale.h> +#include <qaudiodevice.h> + +#include <QStringList> +#include <QList> +#include <QMediaDevices> + +class tst_QAudioDevice : public QObject +{ + Q_OBJECT +public: + tst_QAudioDevice(QObject* parent=nullptr) : QObject(parent) {} + +private slots: + void initTestCase(); + void checkAvailableDefaultInput(); + void checkAvailableDefaultOutput(); + void channels(); + void sampleFormat(); + void sampleRates(); + void isFormatSupported(); + void preferred(); + void assignOperator(); + void id(); + void defaultConstructor(); + void equalityOperator(); + +private: + std::unique_ptr<QAudioDevice> device; +}; + +void tst_QAudioDevice::initTestCase() +{ + // Only perform tests if audio output device exists! + QList<QAudioDevice> devices = QMediaDevices::audioOutputs(); + if (devices.size() == 0) { + QSKIP("NOTE: no audio output device found, no tests will be performed"); + } else { + device = std::make_unique<QAudioDevice>(devices.at(0)); + } +} + +void tst_QAudioDevice::checkAvailableDefaultInput() +{ + // Only perform tests if audio input device exists! + QList<QAudioDevice> devices = QMediaDevices::audioInputs(); + if (devices.size() > 0) { + auto defaultInput = QMediaDevices::defaultAudioInput(); + QVERIFY(!defaultInput.isNull()); + QCOMPARE(std::count(devices.begin(), devices.end(), defaultInput), 1); + } +} + +void tst_QAudioDevice::checkAvailableDefaultOutput() +{ + // Only perform tests if audio input device exists! + QList<QAudioDevice> devices = QMediaDevices::audioOutputs(); + if (devices.size() > 0) { + auto defaultOutput = QMediaDevices::defaultAudioOutput(); + QVERIFY(!defaultOutput.isNull()); + QCOMPARE(std::count(devices.begin(), devices.end(), defaultOutput), 1); + } +} + +void tst_QAudioDevice::channels() +{ + QVERIFY(device->minimumChannelCount() > 0); + QVERIFY(device->maximumChannelCount() >= device->minimumChannelCount()); +} + +void tst_QAudioDevice::sampleFormat() +{ + QList<QAudioFormat::SampleFormat> avail = device->supportedSampleFormats(); + QVERIFY(avail.size() > 0); +} + +void tst_QAudioDevice::sampleRates() +{ + QVERIFY(device->minimumSampleRate() > 0); + QVERIFY(device->maximumSampleRate() >= device->minimumSampleRate()); +} + +void tst_QAudioDevice::isFormatSupported() +{ + QAudioFormat format; + format.setSampleRate(44100); + format.setChannelCount(2); + format.setSampleFormat(QAudioFormat::Int16); + + // Should always be true for these format + QVERIFY(device->isFormatSupported(format)); +} + +void tst_QAudioDevice::preferred() +{ + QAudioFormat format = device->preferredFormat(); + QVERIFY(format.isValid()); + QVERIFY(device->isFormatSupported(format)); +} + +// QAudioDevice's assignOperator method +void tst_QAudioDevice::assignOperator() +{ + QAudioDevice dev; + QVERIFY(dev.id().isNull()); + QVERIFY(dev.isNull() == true); + + QList<QAudioDevice> devices = QMediaDevices::audioOutputs(); + QVERIFY(devices.size() > 0); + QAudioDevice dev1(devices.at(0)); + dev = dev1; + QVERIFY(dev.isNull() == false); + QVERIFY(dev.id() == dev1.id()); +} + +void tst_QAudioDevice::id() +{ + QVERIFY(!device->id().isNull()); + QVERIFY(device->id() == QMediaDevices::audioOutputs().at(0).id()); +} + +// QAudioDevice's defaultConstructor method +void tst_QAudioDevice::defaultConstructor() +{ + QAudioDevice dev; + QVERIFY(dev.isNull() == true); + QVERIFY(dev.id().isNull()); +} + +void tst_QAudioDevice::equalityOperator() +{ + // Get some default device infos + QAudioDevice dev1; + QAudioDevice dev2; + + QVERIFY(dev1 == dev2); + QVERIFY(!(dev1 != dev2)); + + // Make sure each available device is not equal to null + const auto infos = QMediaDevices::audioOutputs(); + for (const QAudioDevice &info : infos) { + QVERIFY(dev1 != info); + QVERIFY(!(dev1 == info)); + + dev2 = info; + + QVERIFY(dev2 == info); + QVERIFY(!(dev2 != info)); + + QVERIFY(dev1 != dev2); + QVERIFY(!(dev1 == dev2)); + } + + // XXX Perhaps each available device should not be equal to any other +} + +QTEST_MAIN(tst_QAudioDevice) + +#include "tst_qaudiodevice.moc" diff --git a/tests/auto/integration/qaudiodeviceinfo/qaudiodeviceinfo.pro b/tests/auto/integration/qaudiodeviceinfo/qaudiodeviceinfo.pro deleted file mode 100644 index 3eb0905c7..000000000 --- a/tests/auto/integration/qaudiodeviceinfo/qaudiodeviceinfo.pro +++ /dev/null @@ -1,9 +0,0 @@ -TARGET = tst_qaudiodeviceinfo - -QT += core multimedia-private testlib - -# This is more of a system test -CONFIG += testcase - -SOURCES += tst_qaudiodeviceinfo.cpp - diff --git a/tests/auto/integration/qaudiodeviceinfo/tst_qaudiodeviceinfo.cpp b/tests/auto/integration/qaudiodeviceinfo/tst_qaudiodeviceinfo.cpp deleted file mode 100644 index c946c0894..000000000 --- a/tests/auto/integration/qaudiodeviceinfo/tst_qaudiodeviceinfo.cpp +++ /dev/null @@ -1,248 +0,0 @@ -/**************************************************************************** -** -** Copyright (C) 2016 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the test suite of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:GPL-EXCEPT$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 3 as published by the Free Software -** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ - - -#include <QtTest/QtTest> -#include <QtCore/qlocale.h> -#include <qaudiodeviceinfo.h> - -#include <QStringList> -#include <QList> - -//TESTED_COMPONENT=src/multimedia - -class tst_QAudioDeviceInfo : public QObject -{ - Q_OBJECT -public: - tst_QAudioDeviceInfo(QObject* parent=0) : QObject(parent) {} - -private slots: - void initTestCase(); - void checkAvailableDefaultInput(); - void checkAvailableDefaultOutput(); - void codecs(); - void channels(); - void sampleSizes(); - void byteOrders(); - void sampleTypes(); - void sampleRates(); - void isFormatSupported(); - void preferred(); - void nearest(); - void supportedChannelCounts(); - void supportedSampleRates(); - void assignOperator(); - void deviceName(); - void defaultConstructor(); - void equalityOperator(); - -private: - QAudioDeviceInfo* device; -}; - -void tst_QAudioDeviceInfo::initTestCase() -{ - // Only perform tests if audio output device exists! - QList<QAudioDeviceInfo> devices = QAudioDeviceInfo::availableDevices(QAudio::AudioOutput); - if (devices.size() == 0) { - QSKIP("NOTE: no audio output device found, no tests will be performed"); - } else { - device = new QAudioDeviceInfo(devices.at(0)); - } -} - -void tst_QAudioDeviceInfo::checkAvailableDefaultInput() -{ - // Only perform tests if audio input device exists! - QList<QAudioDeviceInfo> devices = QAudioDeviceInfo::availableDevices(QAudio::AudioInput); - if (devices.size() > 0) { - QVERIFY(!QAudioDeviceInfo::defaultInputDevice().isNull()); - } -} - -void tst_QAudioDeviceInfo::checkAvailableDefaultOutput() -{ - QVERIFY(!QAudioDeviceInfo::defaultOutputDevice().isNull()); -} - -void tst_QAudioDeviceInfo::codecs() -{ - QStringList avail = device->supportedCodecs(); - QVERIFY(avail.size() > 0); -} - -void tst_QAudioDeviceInfo::channels() -{ - QList<int> avail = device->supportedChannelCounts(); - QVERIFY(avail.size() > 0); -} - -void tst_QAudioDeviceInfo::sampleSizes() -{ - QList<int> avail = device->supportedSampleSizes(); - QVERIFY(avail.size() > 0); -} - -void tst_QAudioDeviceInfo::byteOrders() -{ - QList<QAudioFormat::Endian> avail = device->supportedByteOrders(); - QVERIFY(avail.size() > 0); -} - -void tst_QAudioDeviceInfo::sampleTypes() -{ - QList<QAudioFormat::SampleType> avail = device->supportedSampleTypes(); - QVERIFY(avail.size() > 0); -} - -void tst_QAudioDeviceInfo::sampleRates() -{ - QList<int> avail = device->supportedSampleRates(); - QVERIFY(avail.size() > 0); -} - -void tst_QAudioDeviceInfo::isFormatSupported() -{ - QAudioFormat format; - format.setSampleRate(44100); - format.setChannelCount(2); - format.setSampleType(QAudioFormat::SignedInt); - format.setByteOrder(QAudioFormat::LittleEndian); - format.setSampleSize(16); - format.setCodec("audio/pcm"); - - // Should always be true for these format - QVERIFY(device->isFormatSupported(format)); -} - -void tst_QAudioDeviceInfo::preferred() -{ - QAudioFormat format = device->preferredFormat(); - QVERIFY(format.isValid()); - QVERIFY(device->isFormatSupported(format)); - QVERIFY(device->nearestFormat(format) == format); -} - -// Returns closest QAudioFormat to settings that system audio supports. -void tst_QAudioDeviceInfo::nearest() -{ - /* - QAudioFormat format1, format2; - format1.setSampleRate(8000); - format2 = device->nearestFormat(format1); - QVERIFY(format2.sampleRate() == 44100); - */ - QAudioFormat format; - format.setSampleRate(44100); - format.setChannelCount(2); - format.setSampleType(QAudioFormat::SignedInt); - format.setByteOrder(QAudioFormat::LittleEndian); - format.setSampleSize(16); - format.setCodec("audio/pcm"); - - QAudioFormat format2 = device->nearestFormat(format); - - // This is definitely dependent on platform support (but isFormatSupported tests that above) - QVERIFY(format2.sampleRate() == 44100); -} - -// Returns a list of supported channel counts. -void tst_QAudioDeviceInfo::supportedChannelCounts() -{ - QList<int> avail = device->supportedChannelCounts(); - QVERIFY(avail.size() > 0); -} - -// Returns a list of supported sample rates. -void tst_QAudioDeviceInfo::supportedSampleRates() -{ - QList<int> avail = device->supportedSampleRates(); - QVERIFY(avail.size() > 0); -} - -// QAudioDeviceInfo's assignOperator method -void tst_QAudioDeviceInfo::assignOperator() -{ - QAudioDeviceInfo dev; - QVERIFY(dev.deviceName().isNull()); - QVERIFY(dev.isNull() == true); - - QList<QAudioDeviceInfo> devices = QAudioDeviceInfo::availableDevices(QAudio::AudioOutput); - QVERIFY(devices.size() > 0); - QAudioDeviceInfo dev1(devices.at(0)); - dev = dev1; - QVERIFY(dev.isNull() == false); - QVERIFY(dev.deviceName() == dev1.deviceName()); -} - -// Returns human readable name of audio device -void tst_QAudioDeviceInfo::deviceName() -{ - QVERIFY(!device->deviceName().isNull()); - QVERIFY(device->deviceName() == QAudioDeviceInfo::availableDevices(QAudio::AudioOutput).at(0).deviceName()); -} - -// QAudioDeviceInfo's defaultConstructor method -void tst_QAudioDeviceInfo::defaultConstructor() -{ - QAudioDeviceInfo dev; - QVERIFY(dev.isNull() == true); - QVERIFY(dev.deviceName().isNull()); -} - -void tst_QAudioDeviceInfo::equalityOperator() -{ - // Get some default device infos - QAudioDeviceInfo dev1; - QAudioDeviceInfo dev2; - - QVERIFY(dev1 == dev2); - QVERIFY(!(dev1 != dev2)); - - // Make sure each available device is not equal to null - const auto infos = QAudioDeviceInfo::availableDevices(QAudio::AudioOutput); - for (const QAudioDeviceInfo info : infos) { - QVERIFY(dev1 != info); - QVERIFY(!(dev1 == info)); - - dev2 = info; - - QVERIFY(dev2 == info); - QVERIFY(!(dev2 != info)); - - QVERIFY(dev1 != dev2); - QVERIFY(!(dev1 == dev2)); - } - - // XXX Perhaps each available device should not be equal to any other -} - -QTEST_MAIN(tst_QAudioDeviceInfo) - -#include "tst_qaudiodeviceinfo.moc" diff --git a/tests/auto/integration/qaudioinput/BLACKLIST b/tests/auto/integration/qaudioinput/BLACKLIST deleted file mode 100644 index b7b86283b..000000000 --- a/tests/auto/integration/qaudioinput/BLACKLIST +++ /dev/null @@ -1,7 +0,0 @@ -#QTBUG-49736 -[pushSuspendResume] -linux -[pull] -linux -[pullSuspendResume] -linux diff --git a/tests/auto/integration/qaudioinput/qaudioinput.pro b/tests/auto/integration/qaudioinput/qaudioinput.pro deleted file mode 100644 index 31de98eb0..000000000 --- a/tests/auto/integration/qaudioinput/qaudioinput.pro +++ /dev/null @@ -1,9 +0,0 @@ -TARGET = tst_qaudioinput - -QT += core multimedia-private testlib - -# This is more of a system test -CONFIG += testcase - -HEADERS += wavheader.h -SOURCES += wavheader.cpp tst_qaudioinput.cpp diff --git a/tests/auto/integration/qaudioinput/tst_qaudioinput.cpp b/tests/auto/integration/qaudioinput/tst_qaudioinput.cpp deleted file mode 100644 index bcc50f78a..000000000 --- a/tests/auto/integration/qaudioinput/tst_qaudioinput.cpp +++ /dev/null @@ -1,901 +0,0 @@ -/**************************************************************************** -** -** Copyright (C) 2016 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the test suite of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:GPL-EXCEPT$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 3 as published by the Free Software -** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ - -#include <QtTest/QtTest> -#include <QtCore/qlocale.h> -#include <QtCore/QTemporaryDir> -#include <QtCore/QSharedPointer> -#include <QtCore/QScopedPointer> - -#include <qaudioinput.h> -#include <qaudiodeviceinfo.h> -#include <qaudioformat.h> -#include <qaudio.h> - -#include "wavheader.h" - -//TESTED_COMPONENT=src/multimedia - -#define AUDIO_BUFFER 192000 -#define RANGE_ERR 0.5 - -template<typename T> inline bool qTolerantCompare(T value, T expected) -{ - return qAbs(value - expected) < (RANGE_ERR * expected); -} - -#ifndef QTRY_VERIFY2 -#define QTRY_VERIFY2(__expr,__msg) \ - do { \ - const int __step = 50; \ - const int __timeout = 5000; \ - if (!(__expr)) { \ - QTest::qWait(0); \ - } \ - for (int __i = 0; __i < __timeout && !(__expr); __i+=__step) { \ - QTest::qWait(__step); \ - } \ - QVERIFY2(__expr,__msg); \ - } while(0) -#endif - -class tst_QAudioInput : public QObject -{ - Q_OBJECT -public: - tst_QAudioInput(QObject* parent=0) : QObject(parent) {} - -private slots: - void initTestCase(); - - void format(); - void invalidFormat_data(); - void invalidFormat(); - - void bufferSize(); - void notifyInterval(); - void disableNotifyInterval(); - - void stopWhileStopped(); - void suspendWhileStopped(); - void resumeWhileStopped(); - - void pull_data(){generate_audiofile_testrows();} - void pull(); - - void pullSuspendResume_data(){generate_audiofile_testrows();} - void pullSuspendResume(); - - void push_data(){generate_audiofile_testrows();} - void push(); - - void pushSuspendResume_data(){generate_audiofile_testrows();} - void pushSuspendResume(); - - void reset_data(){generate_audiofile_testrows();} - void reset(); - - void volume_data(){generate_audiofile_testrows();} - void volume(); - -private: - typedef QSharedPointer<QFile> FilePtr; - - QString formatToFileName(const QAudioFormat &format); - - void generate_audiofile_testrows(); - - QAudioDeviceInfo audioDevice; - QList<QAudioFormat> testFormats; - QList<FilePtr> audioFiles; - QScopedPointer<QTemporaryDir> m_temporaryDir; - - QScopedPointer<QByteArray> m_byteArray; - QScopedPointer<QBuffer> m_buffer; - - bool m_inCISystem; -}; - -void tst_QAudioInput::generate_audiofile_testrows() -{ - QTest::addColumn<FilePtr>("audioFile"); - QTest::addColumn<QAudioFormat>("audioFormat"); - - for (int i=0; i<audioFiles.count(); i++) { - QTest::newRow(QString("Audio File %1").arg(i).toLocal8Bit().constData()) - << audioFiles.at(i) << testFormats.at(i); - - // Only run first format in CI system to reduce test times - if (m_inCISystem) - break; - } -} - -QString tst_QAudioInput::formatToFileName(const QAudioFormat &format) -{ - const QString formatEndian = (format.byteOrder() == QAudioFormat::LittleEndian) - ? QString("LE") : QString("BE"); - - const QString formatSigned = (format.sampleType() == QAudioFormat::SignedInt) - ? QString("signed") : QString("unsigned"); - - return QString("%1_%2_%3_%4_%5") - .arg(format.sampleRate()) - .arg(format.sampleSize()) - .arg(formatSigned) - .arg(formatEndian) - .arg(format.channelCount()); -} - -void tst_QAudioInput::initTestCase() -{ - qRegisterMetaType<QAudioFormat>(); - - // Only perform tests if audio output device exists - const QList<QAudioDeviceInfo> devices = - QAudioDeviceInfo::availableDevices(QAudio::AudioInput); - - if (devices.size() <= 0) - QSKIP("No audio backend"); - - audioDevice = QAudioDeviceInfo::defaultInputDevice(); - - - QAudioFormat format; - - format.setCodec("audio/pcm"); - - if (audioDevice.isFormatSupported(audioDevice.preferredFormat())) - testFormats.append(audioDevice.preferredFormat()); - - // PCM 8000 mono S8 - format.setSampleRate(8000); - format.setSampleSize(8); - format.setSampleType(QAudioFormat::SignedInt); - format.setByteOrder(QAudioFormat::LittleEndian); - format.setChannelCount(1); - if (audioDevice.isFormatSupported(format)) - testFormats.append(format); - - // PCM 11025 mono S16LE - format.setSampleRate(11025); - format.setSampleSize(16); - if (audioDevice.isFormatSupported(format)) - testFormats.append(format); - - // PCM 22050 mono S16LE - format.setSampleRate(22050); - if (audioDevice.isFormatSupported(format)) - testFormats.append(format); - - // PCM 22050 stereo S16LE - format.setChannelCount(2); - if (audioDevice.isFormatSupported(format)) - testFormats.append(format); - - // PCM 44100 stereo S16LE - format.setSampleRate(44100); - if (audioDevice.isFormatSupported(format)) - testFormats.append(format); - - // PCM 48000 stereo S16LE - format.setSampleRate(48000); - if (audioDevice.isFormatSupported(format)) - testFormats.append(format); - - QVERIFY(testFormats.size()); - - const QChar slash = QLatin1Char('/'); - QString temporaryPattern = QDir::tempPath(); - if (!temporaryPattern.endsWith(slash)) - temporaryPattern += slash; - temporaryPattern += "tst_qaudioinputXXXXXX"; - m_temporaryDir.reset(new QTemporaryDir(temporaryPattern)); - m_temporaryDir->setAutoRemove(true); - QVERIFY(m_temporaryDir->isValid()); - - const QString temporaryAudioPath = m_temporaryDir->path() + slash; - for (const QAudioFormat &format : qAsConst(testFormats)) { - const QString fileName = temporaryAudioPath + formatToFileName(format) + QStringLiteral(".wav"); - audioFiles.append(FilePtr::create(fileName)); - } - qgetenv("QT_TEST_CI").toInt(&m_inCISystem,10); -} - -void tst_QAudioInput::format() -{ - QAudioInput audioInput(audioDevice.preferredFormat(), this); - - QAudioFormat requested = audioDevice.preferredFormat(); - QAudioFormat actual = audioInput.format(); - - QVERIFY2((requested.channelCount() == actual.channelCount()), - QString("channels: requested=%1, actual=%2").arg(requested.channelCount()).arg(actual.channelCount()).toLocal8Bit().constData()); - QVERIFY2((requested.sampleRate() == actual.sampleRate()), - QString("sampleRate: requested=%1, actual=%2").arg(requested.sampleRate()).arg(actual.sampleRate()).toLocal8Bit().constData()); - QVERIFY2((requested.sampleSize() == actual.sampleSize()), - QString("sampleSize: requested=%1, actual=%2").arg(requested.sampleSize()).arg(actual.sampleSize()).toLocal8Bit().constData()); - QVERIFY2((requested.codec() == actual.codec()), - QString("codec: requested=%1, actual=%2").arg(requested.codec()).arg(actual.codec()).toLocal8Bit().constData()); - QVERIFY2((requested.byteOrder() == actual.byteOrder()), - QString("byteOrder: requested=%1, actual=%2").arg(requested.byteOrder()).arg(actual.byteOrder()).toLocal8Bit().constData()); - QVERIFY2((requested.sampleType() == actual.sampleType()), - QString("sampleType: requested=%1, actual=%2").arg(requested.sampleType()).arg(actual.sampleType()).toLocal8Bit().constData()); -} - -void tst_QAudioInput::invalidFormat_data() -{ - QTest::addColumn<QAudioFormat>("invalidFormat"); - - QAudioFormat format; - - QTest::newRow("Null Format") - << format; - - format = audioDevice.preferredFormat(); - format.setChannelCount(0); - QTest::newRow("Channel count 0") - << format; - - format = audioDevice.preferredFormat(); - format.setSampleRate(0); - QTest::newRow("Sample rate 0") - << format; - - format = audioDevice.preferredFormat(); - format.setSampleSize(0); - QTest::newRow("Sample size 0") - << format; -} - -void tst_QAudioInput::invalidFormat() -{ - QFETCH(QAudioFormat, invalidFormat); - - QVERIFY2(!audioDevice.isFormatSupported(invalidFormat), - "isFormatSupported() is returning true on an invalid format"); - - QAudioInput audioInput(invalidFormat, this); - - // Check that we are in the default state before calling start - QVERIFY2((audioInput.state() == QAudio::StoppedState), "state() was not set to StoppedState before start()"); - QVERIFY2((audioInput.error() == QAudio::NoError), "error() was not set to QAudio::NoError before start()"); - - audioInput.start(); - - // Check that error is raised - QTRY_VERIFY2((audioInput.error() == QAudio::OpenError),"error() was not set to QAudio::OpenError after start()"); -} - -void tst_QAudioInput::bufferSize() -{ - QAudioInput audioInput(audioDevice.preferredFormat(), this); - - QVERIFY2((audioInput.error() == QAudio::NoError), "error() was not set to QAudio::NoError on creation"); - - audioInput.setBufferSize(512); - QVERIFY2((audioInput.error() == QAudio::NoError), "error() is not QAudio::NoError after setBufferSize(512)"); - QVERIFY2((audioInput.bufferSize() == 512), - QString("bufferSize: requested=512, actual=%2").arg(audioInput.bufferSize()).toLocal8Bit().constData()); - - audioInput.setBufferSize(4096); - QVERIFY2((audioInput.error() == QAudio::NoError), "error() is not QAudio::NoError after setBufferSize(4096)"); - QVERIFY2((audioInput.bufferSize() == 4096), - QString("bufferSize: requested=4096, actual=%2").arg(audioInput.bufferSize()).toLocal8Bit().constData()); - - audioInput.setBufferSize(8192); - QVERIFY2((audioInput.error() == QAudio::NoError), "error() is not QAudio::NoError after setBufferSize(8192)"); - QVERIFY2((audioInput.bufferSize() == 8192), - QString("bufferSize: requested=8192, actual=%2").arg(audioInput.bufferSize()).toLocal8Bit().constData()); -} - -void tst_QAudioInput::notifyInterval() -{ - QAudioInput audioInput(audioDevice.preferredFormat(), this); - - QVERIFY2((audioInput.error() == QAudio::NoError), "error() was not set to QAudio::NoError on creation"); - - audioInput.setNotifyInterval(50); - QVERIFY2((audioInput.error() == QAudio::NoError), "error() is not QAudio::NoError after setNotifyInterval(50)"); - QVERIFY2((audioInput.notifyInterval() == 50), - QString("notifyInterval: requested=50, actual=%2").arg(audioInput.notifyInterval()).toLocal8Bit().constData()); - - audioInput.setNotifyInterval(100); - QVERIFY2((audioInput.error() == QAudio::NoError), "error() is not QAudio::NoError after setNotifyInterval(100)"); - QVERIFY2((audioInput.notifyInterval() == 100), - QString("notifyInterval: requested=100, actual=%2").arg(audioInput.notifyInterval()).toLocal8Bit().constData()); - - audioInput.setNotifyInterval(250); - QVERIFY2((audioInput.error() == QAudio::NoError), "error() is not QAudio::NoError after setNotifyInterval(250)"); - QVERIFY2((audioInput.notifyInterval() == 250), - QString("notifyInterval: requested=250, actual=%2").arg(audioInput.notifyInterval()).toLocal8Bit().constData()); - - audioInput.setNotifyInterval(1000); - QVERIFY2((audioInput.error() == QAudio::NoError), "error() is not QAudio::NoError after setNotifyInterval(1000)"); - QVERIFY2((audioInput.notifyInterval() == 1000), - QString("notifyInterval: requested=1000, actual=%2").arg(audioInput.notifyInterval()).toLocal8Bit().constData()); -} - -void tst_QAudioInput::disableNotifyInterval() -{ - // Sets an invalid notification interval (QAudioInput::setNotifyInterval(0)) - // Checks that - // - No error is raised (QAudioInput::error() returns QAudio::NoError) - // - if <= 0, set to zero and disable notify signal - - QAudioInput audioInput(audioDevice.preferredFormat(), this); - - QVERIFY2((audioInput.error() == QAudio::NoError), "error() was not set to QAudio::NoError on creation"); - - audioInput.setNotifyInterval(0); - QVERIFY2((audioInput.error() == QAudio::NoError), "error() is not QAudio::NoError after setNotifyInterval(0)"); - QVERIFY2((audioInput.notifyInterval() == 0), - "notifyInterval() is not zero after setNotifyInterval(0)"); - - audioInput.setNotifyInterval(-1); - QVERIFY2((audioInput.error() == QAudio::NoError), "error() is not QAudio::NoError after setNotifyInterval(-1)"); - QVERIFY2((audioInput.notifyInterval() == 0), - "notifyInterval() is not zero after setNotifyInterval(-1)"); - - //start and run to check if notify() is emitted - if (audioFiles.size() > 0) { - QAudioInput audioInputCheck(testFormats.at(0), this); - audioInputCheck.setNotifyInterval(0); - QSignalSpy notifySignal(&audioInputCheck, SIGNAL(notify())); - QFile *audioFile = audioFiles.at(0).data(); - audioFile->open(QIODevice::WriteOnly); - audioInputCheck.start(audioFile); - QTest::qWait(3000); // 3 seconds should be plenty - audioInputCheck.stop(); - QVERIFY2((notifySignal.count() == 0), - QString("didn't disable notify interval: shouldn't have got any but got %1").arg(notifySignal.count()).toLocal8Bit().constData()); - audioFile->close(); - } -} - -void tst_QAudioInput::stopWhileStopped() -{ - // Calls QAudioInput::stop() when object is already in StoppedState - // Checks that - // - No state change occurs - // - No error is raised (QAudioInput::error() returns QAudio::NoError) - - QAudioInput audioInput(audioDevice.preferredFormat(), this); - - QVERIFY2((audioInput.state() == QAudio::StoppedState), "state() was not set to StoppedState before start()"); - QVERIFY2((audioInput.error() == QAudio::NoError), "error() was not set to QAudio::NoError before start()"); - - QSignalSpy stateSignal(&audioInput, SIGNAL(stateChanged(QAudio::State))); - audioInput.stop(); - - // Check that no state transition occurred - QVERIFY2((stateSignal.count() == 0), "stop() while stopped is emitting a signal and it shouldn't"); - QVERIFY2((audioInput.error() == QAudio::NoError), "error() was not set to QAudio::NoError after stop()"); -} - -void tst_QAudioInput::suspendWhileStopped() -{ - // Calls QAudioInput::suspend() when object is already in StoppedState - // Checks that - // - No state change occurs - // - No error is raised (QAudioInput::error() returns QAudio::NoError) - - QAudioInput audioInput(audioDevice.preferredFormat(), this); - - QVERIFY2((audioInput.state() == QAudio::StoppedState), "state() was not set to StoppedState before start()"); - QVERIFY2((audioInput.error() == QAudio::NoError), "error() was not set to QAudio::NoError before start()"); - - QSignalSpy stateSignal(&audioInput, SIGNAL(stateChanged(QAudio::State))); - audioInput.suspend(); - - // Check that no state transition occurred - QVERIFY2((stateSignal.count() == 0), "stop() while suspended is emitting a signal and it shouldn't"); - QVERIFY2((audioInput.error() == QAudio::NoError), "error() was not set to QAudio::NoError after stop()"); -} - -void tst_QAudioInput::resumeWhileStopped() -{ - // Calls QAudioInput::resume() when object is already in StoppedState - // Checks that - // - No state change occurs - // - No error is raised (QAudioInput::error() returns QAudio::NoError) - - QAudioInput audioInput(audioDevice.preferredFormat(), this); - - QVERIFY2((audioInput.state() == QAudio::StoppedState), "state() was not set to StoppedState before start()"); - QVERIFY2((audioInput.error() == QAudio::NoError), "error() was not set to QAudio::NoError before start()"); - - QSignalSpy stateSignal(&audioInput, SIGNAL(stateChanged(QAudio::State))); - audioInput.resume(); - - // Check that no state transition occurred - QVERIFY2((stateSignal.count() == 0), "resume() while stopped is emitting a signal and it shouldn't"); - QVERIFY2((audioInput.error() == QAudio::NoError), "error() was not set to QAudio::NoError after resume()"); -} - -void tst_QAudioInput::pull() -{ - QFETCH(FilePtr, audioFile); - QFETCH(QAudioFormat, audioFormat); - - QAudioInput audioInput(audioFormat, this); - - audioInput.setNotifyInterval(100); - - QSignalSpy notifySignal(&audioInput, SIGNAL(notify())); - QSignalSpy stateSignal(&audioInput, SIGNAL(stateChanged(QAudio::State))); - - // Check that we are in the default state before calling start - QVERIFY2((audioInput.state() == QAudio::StoppedState), "state() was not set to StoppedState before start()"); - QVERIFY2((audioInput.error() == QAudio::NoError), "error() was not set to QAudio::NoError before start()"); - QVERIFY2((audioInput.elapsedUSecs() == qint64(0)),"elapsedUSecs() not zero on creation"); - - audioFile->close(); - audioFile->open(QIODevice::WriteOnly); - WavHeader wavHeader(audioFormat); - QVERIFY(wavHeader.write(*audioFile)); - - audioInput.start(audioFile.data()); - - // Check that QAudioInput immediately transitions to ActiveState or IdleState - QTRY_VERIFY2((stateSignal.count() > 0),"didn't emit signals on start()"); - QVERIFY2((audioInput.state() == QAudio::ActiveState || audioInput.state() == QAudio::IdleState), - "didn't transition to ActiveState or IdleState after start()"); - QVERIFY2((audioInput.error() == QAudio::NoError), "error state is not equal to QAudio::NoError after start()"); - QVERIFY(audioInput.periodSize() > 0); - stateSignal.clear(); - - // Check that 'elapsed' increases - QTest::qWait(40); - QVERIFY2((audioInput.elapsedUSecs() > 0), "elapsedUSecs() is still zero after start()"); - - // Allow some recording to happen - QTest::qWait(3000); // 3 seconds should be plenty - - stateSignal.clear(); - - qint64 processedUs = audioInput.processedUSecs(); - - audioInput.stop(); - QTest::qWait(40); - QTRY_VERIFY2((stateSignal.count() == 1), - QString("didn't emit StoppedState signal after stop(), got %1 signals instead").arg(stateSignal.count()).toLocal8Bit().constData()); - QVERIFY2((audioInput.state() == QAudio::StoppedState), "didn't transitions to StoppedState after stop()"); - - QVERIFY2(qTolerantCompare(processedUs, 3040000LL), - QString("processedUSecs() doesn't fall in acceptable range, should be 3040000 (%1)").arg(processedUs).toLocal8Bit().constData()); - QVERIFY2((audioInput.error() == QAudio::NoError), "error() is not QAudio::NoError after stop()"); - QVERIFY2((audioInput.elapsedUSecs() == (qint64)0), "elapsedUSecs() not equal to zero in StoppedState"); - QVERIFY2(notifySignal.count() > 0, "not emitting notify() signal"); - - WavHeader::writeDataLength(*audioFile, audioFile->pos() - WavHeader::headerLength()); - audioFile->close(); - -} - -void tst_QAudioInput::pullSuspendResume() -{ -#ifdef Q_OS_LINUX - if (m_inCISystem) - QSKIP("QTBUG-26504 Fails 20% of time with pulseaudio backend"); -#endif - QFETCH(FilePtr, audioFile); - QFETCH(QAudioFormat, audioFormat); - - QAudioInput audioInput(audioFormat, this); - - audioInput.setNotifyInterval(100); - - QSignalSpy notifySignal(&audioInput, SIGNAL(notify())); - QSignalSpy stateSignal(&audioInput, SIGNAL(stateChanged(QAudio::State))); - - // Check that we are in the default state before calling start - QVERIFY2((audioInput.state() == QAudio::StoppedState), "state() was not set to StoppedState before start()"); - QVERIFY2((audioInput.error() == QAudio::NoError), "error() was not set to QAudio::NoError before start()"); - QVERIFY2((audioInput.elapsedUSecs() == qint64(0)),"elapsedUSecs() not zero on creation"); - - audioFile->close(); - audioFile->open(QIODevice::WriteOnly); - WavHeader wavHeader(audioFormat); - QVERIFY(wavHeader.write(*audioFile)); - - audioInput.start(audioFile.data()); - - // Check that QAudioInput immediately transitions to ActiveState or IdleState - QTRY_VERIFY2((stateSignal.count() > 0),"didn't emit signals on start()"); - QVERIFY2((audioInput.state() == QAudio::ActiveState || audioInput.state() == QAudio::IdleState), - "didn't transition to ActiveState or IdleState after start()"); - QVERIFY2((audioInput.error() == QAudio::NoError), "error state is not equal to QAudio::NoError after start()"); - QVERIFY(audioInput.periodSize() > 0); - stateSignal.clear(); - - // Check that 'elapsed' increases - QTest::qWait(40); - QVERIFY2((audioInput.elapsedUSecs() > 0), "elapsedUSecs() is still zero after start()"); - - // Allow some recording to happen - QTest::qWait(3000); // 3 seconds should be plenty - - QVERIFY2((audioInput.state() == QAudio::ActiveState), - "didn't transition to ActiveState after some recording"); - QVERIFY2((audioInput.error() == QAudio::NoError), "error state is not equal to QAudio::NoError after some recording"); - - stateSignal.clear(); - - audioInput.suspend(); - - // Give backends running in separate threads a chance to suspend. - QTest::qWait(100); - - QVERIFY2((stateSignal.count() == 1), - QString("didn't emit SuspendedState signal after suspend(), got %1 signals instead").arg(stateSignal.count()).toLocal8Bit().constData()); - QVERIFY2((audioInput.state() == QAudio::SuspendedState), "didn't transitions to SuspendedState after stop()"); - QVERIFY2((audioInput.error() == QAudio::NoError), "error() is not QAudio::NoError after stop()"); - stateSignal.clear(); - - // Check that only 'elapsed', and not 'processed' increases while suspended - qint64 elapsedUs = audioInput.elapsedUSecs(); - qint64 processedUs = audioInput.processedUSecs(); - QTest::qWait(1000); - QVERIFY(audioInput.elapsedUSecs() > elapsedUs); - QVERIFY(audioInput.processedUSecs() == processedUs); - - audioInput.resume(); - - // Give backends running in separate threads a chance to resume. - QTest::qWait(100); - - // Check that QAudioInput immediately transitions to ActiveState - QVERIFY2((stateSignal.count() == 1), - QString("didn't emit signal after resume(), got %1 signals instead").arg(stateSignal.count()).toLocal8Bit().constData()); - QVERIFY2((audioInput.state() == QAudio::ActiveState), "didn't transition to ActiveState after resume()"); - QVERIFY2((audioInput.error() == QAudio::NoError), "error state is not equal to QAudio::NoError after resume()"); - stateSignal.clear(); - - processedUs = audioInput.processedUSecs(); - - audioInput.stop(); - QTest::qWait(40); - QTRY_VERIFY2((stateSignal.count() == 1), - QString("didn't emit StoppedState signal after stop(), got %1 signals instead").arg(stateSignal.count()).toLocal8Bit().constData()); - QVERIFY2((audioInput.state() == QAudio::StoppedState), "didn't transitions to StoppedState after stop()"); - - QVERIFY2(qTolerantCompare(processedUs, 3040000LL), - QString("processedUSecs() doesn't fall in acceptable range, should be 3040000 (%1)").arg(processedUs).toLocal8Bit().constData()); - QVERIFY2((audioInput.error() == QAudio::NoError), "error() is not QAudio::NoError after stop()"); - QVERIFY2((audioInput.elapsedUSecs() == (qint64)0), "elapsedUSecs() not equal to zero in StoppedState"); - QVERIFY2(notifySignal.count() > 0, "not emitting notify() signal"); - - WavHeader::writeDataLength(*audioFile,audioFile->pos()-WavHeader::headerLength()); - audioFile->close(); -} - -void tst_QAudioInput::push() -{ - QFETCH(FilePtr, audioFile); - QFETCH(QAudioFormat, audioFormat); - - QAudioInput audioInput(audioFormat, this); - - audioInput.setNotifyInterval(100); - - QSignalSpy notifySignal(&audioInput, SIGNAL(notify())); - QSignalSpy stateSignal(&audioInput, SIGNAL(stateChanged(QAudio::State))); - - // Check that we are in the default state before calling start - QVERIFY2((audioInput.state() == QAudio::StoppedState), "state() was not set to StoppedState before start()"); - QVERIFY2((audioInput.error() == QAudio::NoError), "error() was not set to QAudio::NoError before start()"); - QVERIFY2((audioInput.elapsedUSecs() == qint64(0)),"elapsedUSecs() not zero on creation"); - - audioFile->close(); - audioFile->open(QIODevice::WriteOnly); - WavHeader wavHeader(audioFormat); - QVERIFY(wavHeader.write(*audioFile)); - - // Set a large buffer to avoid underruns during QTest::qWaits - audioInput.setBufferSize(audioFormat.bytesForDuration(1000000)); - - QIODevice* feed = audioInput.start(); - - // Check that QAudioInput immediately transitions to IdleState - QTRY_VERIFY2((stateSignal.count() == 1),"didn't emit IdleState signal on start()"); - QVERIFY2((audioInput.state() == QAudio::IdleState), - "didn't transition to IdleState after start()"); - QVERIFY2((audioInput.error() == QAudio::NoError), "error state is not equal to QAudio::NoError after start()"); - QVERIFY(audioInput.periodSize() > 0); - stateSignal.clear(); - - // Check that 'elapsed' increases - QTest::qWait(40); - QVERIFY2((audioInput.elapsedUSecs() > 0), "elapsedUSecs() is still zero after start()"); - - qint64 totalBytesRead = 0; - bool firstBuffer = true; - QByteArray buffer(AUDIO_BUFFER, 0); - qint64 len = (audioFormat.sampleRate()*audioFormat.channelCount()*(audioFormat.sampleSize()/8)*2); // 2 seconds - while (totalBytesRead < len) { - QTRY_VERIFY_WITH_TIMEOUT(audioInput.bytesReady() >= audioInput.periodSize(), 10000); - qint64 bytesRead = feed->read(buffer.data(), audioInput.periodSize()); - audioFile->write(buffer.constData(),bytesRead); - totalBytesRead+=bytesRead; - if (firstBuffer && bytesRead) { - // Check for transition to ActiveState when data is provided - QTRY_VERIFY2((stateSignal.count() == 1),"didn't emit ActiveState signal on data"); - QVERIFY2((audioInput.state() == QAudio::ActiveState), - "didn't transition to ActiveState after data"); - QVERIFY2((audioInput.error() == QAudio::NoError), "error state is not equal to QAudio::NoError after start()"); - firstBuffer = false; - } - } - - QTest::qWait(1000); - - stateSignal.clear(); - - qint64 processedUs = audioInput.processedUSecs(); - - audioInput.stop(); - QTest::qWait(40); - QTRY_VERIFY2((stateSignal.count() == 1), - QString("didn't emit StoppedState signal after stop(), got %1 signals instead").arg(stateSignal.count()).toLocal8Bit().constData()); - QVERIFY2((audioInput.state() == QAudio::StoppedState), "didn't transitions to StoppedState after stop()"); - - QVERIFY2(qTolerantCompare(processedUs, 2040000LL), - QString("processedUSecs() doesn't fall in acceptable range, should be 2040000 (%1)").arg(processedUs).toLocal8Bit().constData()); - QVERIFY2((audioInput.error() == QAudio::NoError), "error() is not QAudio::NoError after stop()"); - QVERIFY2((audioInput.elapsedUSecs() == (qint64)0), "elapsedUSecs() not equal to zero in StoppedState"); - QVERIFY2(notifySignal.count() > 0, "not emitting notify() signal"); - - WavHeader::writeDataLength(*audioFile,audioFile->pos()-WavHeader::headerLength()); - audioFile->close(); -} - -void tst_QAudioInput::pushSuspendResume() -{ -#ifdef Q_OS_LINUX - if (m_inCISystem) - QSKIP("QTBUG-26504 Fails 20% of time with pulseaudio backend"); -#endif - QFETCH(FilePtr, audioFile); - QFETCH(QAudioFormat, audioFormat); - QAudioInput audioInput(audioFormat, this); - - audioInput.setNotifyInterval(100); - audioInput.setBufferSize(audioFormat.bytesForDuration(1000000)); - - QSignalSpy notifySignal(&audioInput, SIGNAL(notify())); - QSignalSpy stateSignal(&audioInput, SIGNAL(stateChanged(QAudio::State))); - - // Check that we are in the default state before calling start - QVERIFY2((audioInput.state() == QAudio::StoppedState), "state() was not set to StoppedState before start()"); - QVERIFY2((audioInput.error() == QAudio::NoError), "error() was not set to QAudio::NoError before start()"); - QVERIFY2((audioInput.elapsedUSecs() == qint64(0)),"elapsedUSecs() not zero on creation"); - - audioFile->close(); - audioFile->open(QIODevice::WriteOnly); - WavHeader wavHeader(audioFormat); - QVERIFY(wavHeader.write(*audioFile)); - - QIODevice* feed = audioInput.start(); - - // Check that QAudioInput immediately transitions to IdleState - QTRY_VERIFY2((stateSignal.count() == 1),"didn't emit IdleState signal on start()"); - QVERIFY2((audioInput.state() == QAudio::IdleState), - "didn't transition to IdleState after start()"); - QVERIFY2((audioInput.error() == QAudio::NoError), "error state is not equal to QAudio::NoError after start()"); - QVERIFY(audioInput.periodSize() > 0); - stateSignal.clear(); - - // Check that 'elapsed' increases - QTest::qWait(40); - QTRY_VERIFY2((audioInput.elapsedUSecs() > 0), "elapsedUSecs() is still zero after start()"); - - qint64 totalBytesRead = 0; - bool firstBuffer = true; - QByteArray buffer(AUDIO_BUFFER, 0); - qint64 len = (audioFormat.sampleRate()*audioFormat.channelCount()*(audioFormat.sampleSize()/8)); // 1 seconds - while (totalBytesRead < len) { - QTRY_VERIFY_WITH_TIMEOUT(audioInput.bytesReady() >= audioInput.periodSize(), 10000); - qint64 bytesRead = feed->read(buffer.data(), audioInput.periodSize()); - audioFile->write(buffer.constData(),bytesRead); - totalBytesRead+=bytesRead; - if (firstBuffer && bytesRead) { - // Check for transition to ActiveState when data is provided - QTRY_VERIFY2((stateSignal.count() == 1),"didn't emit ActiveState signal on data"); - QVERIFY2((audioInput.state() == QAudio::ActiveState), - "didn't transition to ActiveState after data"); - QVERIFY2((audioInput.error() == QAudio::NoError), "error state is not equal to QAudio::NoError after start()"); - firstBuffer = false; - } - } - stateSignal.clear(); - - audioInput.suspend(); - - // Give backends running in separate threads a chance to suspend - QTest::qWait(100); - - QVERIFY2((stateSignal.count() == 1), - QString("didn't emit SuspendedState signal after suspend(), got %1 signals instead").arg(stateSignal.count()).toLocal8Bit().constData()); - QVERIFY2((audioInput.state() == QAudio::SuspendedState), "didn't transitions to SuspendedState after stop()"); - QVERIFY2((audioInput.error() == QAudio::NoError), "error() is not QAudio::NoError after stop()"); - stateSignal.clear(); - - // Check that only 'elapsed', and not 'processed' increases while suspended - qint64 elapsedUs = audioInput.elapsedUSecs(); - qint64 processedUs = audioInput.processedUSecs(); - QTest::qWait(1000); - QVERIFY(audioInput.elapsedUSecs() > elapsedUs); - QVERIFY(audioInput.processedUSecs() == processedUs); - - // Drain any data, in case we run out of space when resuming - const int reads = audioInput.bytesReady() / audioInput.periodSize(); - for (int r = 0; r < reads; ++r) - feed->read(buffer.data(), audioInput.periodSize()); - - audioInput.resume(); - - // Check that QAudioInput immediately transitions to Active or IdleState - QTRY_VERIFY2((stateSignal.count() > 0),"didn't emit signals on resume()"); - QVERIFY2((audioInput.state() == QAudio::ActiveState || audioInput.state() == QAudio::IdleState), - "didn't transition to ActiveState or IdleState after resume()"); - QVERIFY2((audioInput.error() == QAudio::NoError), "error state is not equal to QAudio::NoError after resume()"); - QVERIFY(audioInput.periodSize() > 0); - - // Let it play out what is in buffer and go to Idle before continue - QTest::qWait(1000); - stateSignal.clear(); - - // Read another seconds worth - totalBytesRead = 0; - firstBuffer = true; - while (totalBytesRead < len && audioInput.state() != QAudio::StoppedState) { - QTRY_VERIFY_WITH_TIMEOUT(audioInput.bytesReady() >= audioInput.periodSize(), 10000); - qint64 bytesRead = feed->read(buffer.data(), audioInput.periodSize()); - audioFile->write(buffer.constData(),bytesRead); - totalBytesRead+=bytesRead; - } - stateSignal.clear(); - - processedUs = audioInput.processedUSecs(); - - audioInput.stop(); - QTest::qWait(40); - QVERIFY2((stateSignal.count() == 1), - QString("didn't emit StoppedState signal after stop(), got %1 signals instead").arg(stateSignal.count()).toLocal8Bit().constData()); - QVERIFY2((audioInput.state() == QAudio::StoppedState), "didn't transitions to StoppedState after stop()"); - - QVERIFY2(qTolerantCompare(processedUs, 2040000LL), - QString("processedUSecs() doesn't fall in acceptable range, should be 2040000 (%1)").arg(processedUs).toLocal8Bit().constData()); - QVERIFY2((audioInput.elapsedUSecs() == (qint64)0), "elapsedUSecs() not equal to zero in StoppedState"); - - WavHeader::writeDataLength(*audioFile,audioFile->pos()-WavHeader::headerLength()); - audioFile->close(); -} - -void tst_QAudioInput::reset() -{ - QFETCH(QAudioFormat, audioFormat); - - // Try both push/pull.. the vagaries of Active vs Idle are tested elsewhere - { - QAudioInput audioInput(audioFormat, this); - - audioInput.setNotifyInterval(100); - - QSignalSpy notifySignal(&audioInput, SIGNAL(notify())); - QSignalSpy stateSignal(&audioInput, SIGNAL(stateChanged(QAudio::State))); - - // Check that we are in the default state before calling start - QVERIFY2((audioInput.state() == QAudio::StoppedState), "state() was not set to StoppedState before start()"); - QVERIFY2((audioInput.error() == QAudio::NoError), "error() was not set to QAudio::NoError before start()"); - QVERIFY2((audioInput.elapsedUSecs() == qint64(0)),"elapsedUSecs() not zero on creation"); - - QIODevice* device = audioInput.start(); - // Check that QAudioInput immediately transitions to IdleState - QTRY_VERIFY2((stateSignal.count() == 1),"didn't emit IdleState signal on start()"); - QVERIFY2((audioInput.state() == QAudio::IdleState), "didn't transition to IdleState after start()"); - QVERIFY2((audioInput.error() == QAudio::NoError), "error state is not equal to QAudio::NoError after start()"); - QVERIFY(audioInput.periodSize() > 0); - QTRY_VERIFY2_WITH_TIMEOUT((audioInput.bytesReady() > audioInput.periodSize()), "no bytes available after starting", 10000); - - // Trigger a read - QByteArray data = device->read(audioInput.periodSize()); - QVERIFY2((audioInput.error() == QAudio::NoError), "error state is not equal to QAudio::NoError after start()"); - stateSignal.clear(); - - audioInput.reset(); - QTRY_VERIFY2((stateSignal.count() == 1),"didn't emit StoppedState signal after reset()"); - QVERIFY2((audioInput.state() == QAudio::StoppedState), "didn't transitions to StoppedState after reset()"); - QVERIFY2((audioInput.bytesReady() == 0), "buffer not cleared after reset()"); - } - - { - QAudioInput audioInput(audioFormat, this); - QBuffer buffer; - - audioInput.setNotifyInterval(100); - - QSignalSpy notifySignal(&audioInput, SIGNAL(notify())); - QSignalSpy stateSignal(&audioInput, SIGNAL(stateChanged(QAudio::State))); - - // Check that we are in the default state before calling start - QVERIFY2((audioInput.state() == QAudio::StoppedState), "state() was not set to StoppedState before start()"); - QVERIFY2((audioInput.error() == QAudio::NoError), "error() was not set to QAudio::NoError before start()"); - QVERIFY2((audioInput.elapsedUSecs() == qint64(0)),"elapsedUSecs() not zero on creation"); - - audioInput.start(&buffer); - - // Check that QAudioInput immediately transitions to ActiveState - QTRY_VERIFY2((stateSignal.count() >= 1),"didn't emit state changed signal on start()"); - QTRY_VERIFY2((audioInput.state() == QAudio::ActiveState), "didn't transition to ActiveState after start()"); - QVERIFY2((audioInput.error() == QAudio::NoError), "error state is not equal to QAudio::NoError after start()"); - QVERIFY(audioInput.periodSize() > 0); - stateSignal.clear(); - - audioInput.reset(); - QTRY_VERIFY2((stateSignal.count() >= 1),"didn't emit StoppedState signal after reset()"); - QVERIFY2((audioInput.state() == QAudio::StoppedState), "didn't transitions to StoppedState after reset()"); - QVERIFY2((audioInput.bytesReady() == 0), "buffer not cleared after reset()"); - } -} - -void tst_QAudioInput::volume() -{ - QFETCH(QAudioFormat, audioFormat); - - const qreal half(0.5f); - const qreal one(1.0f); - - QAudioInput audioInput(audioFormat, this); - - qreal volume = audioInput.volume(); - audioInput.setVolume(half); - QTRY_VERIFY(qRound(audioInput.volume()*10.0f) == 5); - // Wait a while to see if this changes - QTest::qWait(500); - QTRY_VERIFY(qRound(audioInput.volume()*10.0f) == 5); - - audioInput.setVolume(one); - QTRY_VERIFY(qRound(audioInput.volume()*10.0f) == 10); - // Wait a while to see if this changes - QTest::qWait(500); - QTRY_VERIFY(qRound(audioInput.volume()*10.0f) == 10); - - audioInput.setVolume(half); - audioInput.start(); - QTRY_VERIFY(qRound(audioInput.volume()*10.0f) == 5); - audioInput.setVolume(one); - QTRY_VERIFY(qRound(audioInput.volume()*10.0f) == 10); - - audioInput.setVolume(volume); -} - -QTEST_MAIN(tst_QAudioInput) - -#include "tst_qaudioinput.moc" diff --git a/tests/auto/integration/qaudioinput/wavheader.cpp b/tests/auto/integration/qaudioinput/wavheader.cpp deleted file mode 100644 index 869d74d5b..000000000 --- a/tests/auto/integration/qaudioinput/wavheader.cpp +++ /dev/null @@ -1,192 +0,0 @@ -/**************************************************************************** -** -** Copyright (C) 2016 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the test suite of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:GPL-EXCEPT$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 3 as published by the Free Software -** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ - -#include <QtCore/qendian.h> -#include "wavheader.h" - - -struct chunk -{ - char id[4]; - quint32 size; -}; - -struct RIFFHeader -{ - chunk descriptor; // "RIFF" - char type[4]; // "WAVE" -}; - -struct WAVEHeader -{ - chunk descriptor; - quint16 audioFormat; - quint16 numChannels; - quint32 sampleRate; - quint32 byteRate; - quint16 blockAlign; - quint16 bitsPerSample; -}; - -struct DATAHeader -{ - chunk descriptor; -}; - -struct CombinedHeader -{ - RIFFHeader riff; - WAVEHeader wave; - DATAHeader data; -}; - -static const int HeaderLength = sizeof(CombinedHeader); - - -WavHeader::WavHeader(const QAudioFormat &format, qint64 dataLength) - : m_format(format) - , m_dataLength(dataLength) -{ - -} - -bool WavHeader::read(QIODevice &device) -{ - bool result = true; - - if (!device.isSequential()) - result = device.seek(0); - // else, assume that current position is the start of the header - - if (result) { - CombinedHeader header; - result = (device.read(reinterpret_cast<char *>(&header), HeaderLength) == HeaderLength); - if (result) { - if ((memcmp(&header.riff.descriptor.id, "RIFF", 4) == 0 - || memcmp(&header.riff.descriptor.id, "RIFX", 4) == 0) - && memcmp(&header.riff.type, "WAVE", 4) == 0 - && memcmp(&header.wave.descriptor.id, "fmt ", 4) == 0 - && header.wave.audioFormat == 1 // PCM - ) { - if (memcmp(&header.riff.descriptor.id, "RIFF", 4) == 0) - m_format.setByteOrder(QAudioFormat::LittleEndian); - else - m_format.setByteOrder(QAudioFormat::BigEndian); - - m_format.setChannelCount(qFromLittleEndian<quint16>(header.wave.numChannels)); - m_format.setCodec("audio/pcm"); - m_format.setSampleRate(qFromLittleEndian<quint32>(header.wave.sampleRate)); - m_format.setSampleSize(qFromLittleEndian<quint16>(header.wave.bitsPerSample)); - - switch(header.wave.bitsPerSample) { - case 8: - m_format.setSampleType(QAudioFormat::UnSignedInt); - break; - case 16: - m_format.setSampleType(QAudioFormat::SignedInt); - break; - default: - result = false; - } - - m_dataLength = device.size() - HeaderLength; - } else { - result = false; - } - } - } - - return result; -} - -bool WavHeader::write(QIODevice &device) -{ - CombinedHeader header; - - memset(&header, 0, HeaderLength); - - // RIFF header - if (m_format.byteOrder() == QAudioFormat::LittleEndian) - memcpy(header.riff.descriptor.id,"RIFF",4); - else - memcpy(header.riff.descriptor.id,"RIFX",4); - qToLittleEndian<quint32>(quint32(m_dataLength + HeaderLength - 8), - reinterpret_cast<unsigned char*>(&header.riff.descriptor.size)); - memcpy(header.riff.type, "WAVE",4); - - // WAVE header - memcpy(header.wave.descriptor.id,"fmt ",4); - qToLittleEndian<quint32>(quint32(16), - reinterpret_cast<unsigned char*>(&header.wave.descriptor.size)); - qToLittleEndian<quint16>(quint16(1), - reinterpret_cast<unsigned char*>(&header.wave.audioFormat)); - qToLittleEndian<quint16>(quint16(m_format.channelCount()), - reinterpret_cast<unsigned char*>(&header.wave.numChannels)); - qToLittleEndian<quint32>(quint32(m_format.sampleRate()), - reinterpret_cast<unsigned char*>(&header.wave.sampleRate)); - qToLittleEndian<quint32>(quint32(m_format.sampleRate() * m_format.channelCount() * m_format.sampleSize() / 8), - reinterpret_cast<unsigned char*>(&header.wave.byteRate)); - qToLittleEndian<quint16>(quint16(m_format.channelCount() * m_format.sampleSize() / 8), - reinterpret_cast<unsigned char*>(&header.wave.blockAlign)); - qToLittleEndian<quint16>(quint16(m_format.sampleSize()), - reinterpret_cast<unsigned char*>(&header.wave.bitsPerSample)); - - // DATA header - memcpy(header.data.descriptor.id,"data",4); - qToLittleEndian<quint32>(quint32(m_dataLength), - reinterpret_cast<unsigned char*>(&header.data.descriptor.size)); - - return (device.write(reinterpret_cast<const char *>(&header), HeaderLength) == HeaderLength); -} - -const QAudioFormat& WavHeader::format() const -{ - return m_format; -} - -qint64 WavHeader::dataLength() const -{ - return m_dataLength; -} - -qint64 WavHeader::headerLength() -{ - return HeaderLength; -} - -bool WavHeader::writeDataLength(QIODevice &device, qint64 dataLength) -{ - bool result = false; - if (!device.isSequential()) { - device.seek(40); - unsigned char dataLengthLE[4]; - qToLittleEndian<quint32>(quint32(dataLength), dataLengthLE); - result = (device.write(reinterpret_cast<const char *>(dataLengthLE), 4) == 4); - } - return result; -} diff --git a/tests/auto/integration/qaudioinput/wavheader.h b/tests/auto/integration/qaudioinput/wavheader.h deleted file mode 100644 index b9595cffc..000000000 --- a/tests/auto/integration/qaudioinput/wavheader.h +++ /dev/null @@ -1,67 +0,0 @@ -/**************************************************************************** -** -** Copyright (C) 2016 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the test suite of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:GPL-EXCEPT$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 3 as published by the Free Software -** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ - - -#ifndef WAVHEADER_H -#define WAVHEADER_H - -#include <QtCore/qobject.h> -#include <QtCore/qfile.h> -#include <qaudioformat.h> - -/** - * Helper class for parsing WAV file headers. - * - * See https://ccrma.stanford.edu/courses/422/projects/WaveFormat/ - */ -class WavHeader -{ -public: - WavHeader(const QAudioFormat &format = QAudioFormat(), - qint64 dataLength = 0); - - // Reads WAV header and seeks to start of data - bool read(QIODevice &device); - - // Writes WAV header - bool write(QIODevice &device); - - const QAudioFormat& format() const; - qint64 dataLength() const; - - static qint64 headerLength(); - - static bool writeDataLength(QIODevice &device, qint64 dataLength); - -private: - QAudioFormat m_format; - qint64 m_dataLength; -}; - -#endif - diff --git a/tests/auto/integration/qaudiooutput/BLACKLIST b/tests/auto/integration/qaudiooutput/BLACKLIST deleted file mode 100644 index 966b48af6..000000000 --- a/tests/auto/integration/qaudiooutput/BLACKLIST +++ /dev/null @@ -1 +0,0 @@ -linux ci diff --git a/tests/auto/integration/qaudiooutput/qaudiooutput.pro b/tests/auto/integration/qaudiooutput/qaudiooutput.pro deleted file mode 100644 index dfaebe36a..000000000 --- a/tests/auto/integration/qaudiooutput/qaudiooutput.pro +++ /dev/null @@ -1,9 +0,0 @@ -TARGET = tst_qaudiooutput - -QT += core multimedia-private testlib - -# This is more of a system test -CONFIG += testcase - -HEADERS += wavheader.h -SOURCES += wavheader.cpp tst_qaudiooutput.cpp diff --git a/tests/auto/integration/qaudiooutput/tst_qaudiooutput.cpp b/tests/auto/integration/qaudiooutput/tst_qaudiooutput.cpp deleted file mode 100644 index a81706ec1..000000000 --- a/tests/auto/integration/qaudiooutput/tst_qaudiooutput.cpp +++ /dev/null @@ -1,988 +0,0 @@ -/**************************************************************************** -** -** Copyright (C) 2016 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the test suite of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:GPL-EXCEPT$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 3 as published by the Free Software -** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ - -//TESTED_COMPONENT=src/multimedia - -#include <QtTest/QtTest> -#include <QtCore/qlocale.h> -#include <QtCore/QTemporaryDir> -#include <QtCore/QSharedPointer> -#include <QtCore/QScopedPointer> - -#include <qaudiooutput.h> -#include <qaudiodeviceinfo.h> -#include <qaudioformat.h> -#include <qaudio.h> - -#include "wavheader.h" - -#define AUDIO_BUFFER 192000 - -#ifndef QTRY_VERIFY2 -#define QTRY_VERIFY2(__expr,__msg) \ - do { \ - const int __step = 50; \ - const int __timeout = 5000; \ - if (!(__expr)) { \ - QTest::qWait(0); \ - } \ - for (int __i = 0; __i < __timeout && !(__expr); __i+=__step) { \ - QTest::qWait(__step); \ - } \ - QVERIFY2(__expr,__msg); \ - } while (0) -#endif - -class tst_QAudioOutput : public QObject -{ - Q_OBJECT -public: - tst_QAudioOutput(QObject* parent=0) : QObject(parent) {} - -private slots: - void initTestCase(); - - void format(); - void invalidFormat_data(); - void invalidFormat(); - - void bufferSize_data(); - void bufferSize(); - - void notifyInterval_data(); - void notifyInterval(); - - void disableNotifyInterval(); - - void stopWhileStopped(); - void suspendWhileStopped(); - void resumeWhileStopped(); - - void pull_data(){generate_audiofile_testrows();} - void pull(); - - void pullSuspendResume_data(){generate_audiofile_testrows();} - void pullSuspendResume(); - - void push_data(){generate_audiofile_testrows();} - void push(); - - void pushSuspendResume_data(){generate_audiofile_testrows();} - void pushSuspendResume(); - - void pushUnderrun_data(){generate_audiofile_testrows();} - void pushUnderrun(); - - void volume_data(); - void volume(); - -private: - typedef QSharedPointer<QFile> FilePtr; - - QString formatToFileName(const QAudioFormat &format); - void createSineWaveData(const QAudioFormat &format, qint64 length, int sampleRate = 440); - - void generate_audiofile_testrows(); - - QAudioDeviceInfo audioDevice; - QList<QAudioFormat> testFormats; - QList<FilePtr> audioFiles; - QScopedPointer<QTemporaryDir> m_temporaryDir; - - QScopedPointer<QByteArray> m_byteArray; - QScopedPointer<QBuffer> m_buffer; -}; - -QString tst_QAudioOutput::formatToFileName(const QAudioFormat &format) -{ - const QString formatEndian = (format.byteOrder() == QAudioFormat::LittleEndian) - ? QString("LE") : QString("BE"); - - const QString formatSigned = (format.sampleType() == QAudioFormat::SignedInt) - ? QString("signed") : QString("unsigned"); - - return QString("%1_%2_%3_%4_%5") - .arg(format.sampleRate()) - .arg(format.sampleSize()) - .arg(formatSigned) - .arg(formatEndian) - .arg(format.channelCount()); -} - -void tst_QAudioOutput::createSineWaveData(const QAudioFormat &format, qint64 length, int sampleRate) -{ - const int channelBytes = format.sampleSize() / 8; - const int sampleBytes = format.channelCount() * channelBytes; - - Q_ASSERT(length % sampleBytes == 0); - Q_UNUSED(sampleBytes); // suppress warning in release builds - - m_byteArray.reset(new QByteArray(length, 0)); - unsigned char *ptr = reinterpret_cast<unsigned char *>(m_byteArray->data()); - int sampleIndex = 0; - - while (length) { - const qreal x = qSin(2 * M_PI * sampleRate * qreal(sampleIndex % format.sampleRate()) / format.sampleRate()); - for (int i=0; i<format.channelCount(); ++i) { - if (format.sampleSize() == 8 && format.sampleType() == QAudioFormat::UnSignedInt) { - const quint8 value = static_cast<quint8>((1.0 + x) / 2 * 255); - *reinterpret_cast<quint8*>(ptr) = value; - } else if (format.sampleSize() == 8 && format.sampleType() == QAudioFormat::SignedInt) { - const qint8 value = static_cast<qint8>(x * 127); - *reinterpret_cast<quint8*>(ptr) = value; - } else if (format.sampleSize() == 16 && format.sampleType() == QAudioFormat::UnSignedInt) { - quint16 value = static_cast<quint16>((1.0 + x) / 2 * 65535); - if (format.byteOrder() == QAudioFormat::LittleEndian) - qToLittleEndian<quint16>(value, ptr); - else - qToBigEndian<quint16>(value, ptr); - } else if (format.sampleSize() == 16 && format.sampleType() == QAudioFormat::SignedInt) { - qint16 value = static_cast<qint16>(x * 32767); - if (format.byteOrder() == QAudioFormat::LittleEndian) - qToLittleEndian<qint16>(value, ptr); - else - qToBigEndian<qint16>(value, ptr); - } - - ptr += channelBytes; - length -= channelBytes; - } - ++sampleIndex; - } - - m_buffer.reset(new QBuffer(m_byteArray.data(), this)); - Q_ASSERT(m_buffer->open(QIODevice::ReadOnly)); -} - -void tst_QAudioOutput::generate_audiofile_testrows() -{ - QTest::addColumn<FilePtr>("audioFile"); - QTest::addColumn<QAudioFormat>("audioFormat"); - - for (int i=0; i<audioFiles.count(); i++) { - QTest::newRow(QString("Audio File %1").arg(i).toLocal8Bit().constData()) - << audioFiles.at(i) << testFormats.at(i); - - } -} - -void tst_QAudioOutput::initTestCase() -{ - qRegisterMetaType<QAudioFormat>(); - - // Only perform tests if audio output device exists - const QList<QAudioDeviceInfo> devices = - QAudioDeviceInfo::availableDevices(QAudio::AudioOutput); - - if (devices.size() <= 0) - QSKIP("No audio backend"); - - audioDevice = QAudioDeviceInfo::defaultOutputDevice(); - - - QAudioFormat format; - - format.setCodec("audio/pcm"); - - if (audioDevice.isFormatSupported(audioDevice.preferredFormat())) - testFormats.append(audioDevice.preferredFormat()); - - // PCM 8000 mono S8 - format.setSampleRate(8000); - format.setSampleSize(8); - format.setSampleType(QAudioFormat::SignedInt); - format.setByteOrder(QAudioFormat::LittleEndian); - format.setChannelCount(1); - if (audioDevice.isFormatSupported(format)) - testFormats.append(format); - - // PCM 11025 mono S16LE - format.setSampleRate(11025); - format.setSampleSize(16); - if (audioDevice.isFormatSupported(format)) - testFormats.append(format); - - // PCM 22050 mono S16LE - format.setSampleRate(22050); - if (audioDevice.isFormatSupported(format)) - testFormats.append(format); - - // PCM 22050 stereo S16LE - format.setChannelCount(2); - if (audioDevice.isFormatSupported(format)) - testFormats.append(format); - - // PCM 44100 stereo S16LE - format.setSampleRate(44100); - if (audioDevice.isFormatSupported(format)) - testFormats.append(format); - - // PCM 48000 stereo S16LE - format.setSampleRate(48000); - if (audioDevice.isFormatSupported(format)) - testFormats.append(format); - - QVERIFY(testFormats.size()); - - const QChar slash = QLatin1Char('/'); - QString temporaryPattern = QDir::tempPath(); - if (!temporaryPattern.endsWith(slash)) - temporaryPattern += slash; - temporaryPattern += "tst_qaudiooutputXXXXXX"; - m_temporaryDir.reset(new QTemporaryDir(temporaryPattern)); - m_temporaryDir->setAutoRemove(true); - QVERIFY(m_temporaryDir->isValid()); - - const QString temporaryAudioPath = m_temporaryDir->path() + slash; - for (const QAudioFormat &format : qAsConst(testFormats)) { - qint64 len = (format.sampleRate()*format.channelCount()*(format.sampleSize()/8)*2); // 2 seconds - createSineWaveData(format, len); - // Write generate sine wave data to file - const QString fileName = temporaryAudioPath + QStringLiteral("generated") - + formatToFileName(format) + QStringLiteral(".wav"); - FilePtr file(new QFile(fileName)); - QVERIFY2(file->open(QIODevice::WriteOnly), qPrintable(file->errorString())); - WavHeader wavHeader(format, len); - wavHeader.write(*file.data()); - file->write(m_byteArray->data(), len); - file->close(); - audioFiles.append(file); - } -} - -void tst_QAudioOutput::format() -{ - QAudioOutput audioOutput(audioDevice.preferredFormat(), this); - - QAudioFormat requested = audioDevice.preferredFormat(); - QAudioFormat actual = audioOutput.format(); - - QVERIFY2((requested.channelCount() == actual.channelCount()), - QString("channels: requested=%1, actual=%2").arg(requested.channelCount()).arg(actual.channelCount()).toLocal8Bit().constData()); - QVERIFY2((requested.sampleRate() == actual.sampleRate()), - QString("sampleRate: requested=%1, actual=%2").arg(requested.sampleRate()).arg(actual.sampleRate()).toLocal8Bit().constData()); - QVERIFY2((requested.sampleSize() == actual.sampleSize()), - QString("sampleSize: requested=%1, actual=%2").arg(requested.sampleSize()).arg(actual.sampleSize()).toLocal8Bit().constData()); - QVERIFY2((requested.codec() == actual.codec()), - QString("codec: requested=%1, actual=%2").arg(requested.codec()).arg(actual.codec()).toLocal8Bit().constData()); - QVERIFY2((requested.byteOrder() == actual.byteOrder()), - QString("byteOrder: requested=%1, actual=%2").arg(requested.byteOrder()).arg(actual.byteOrder()).toLocal8Bit().constData()); - QVERIFY2((requested.sampleType() == actual.sampleType()), - QString("sampleType: requested=%1, actual=%2").arg(requested.sampleType()).arg(actual.sampleType()).toLocal8Bit().constData()); -} - -void tst_QAudioOutput::invalidFormat_data() -{ - QTest::addColumn<QAudioFormat>("invalidFormat"); - - QAudioFormat format; - - QTest::newRow("Null Format") - << format; - - format = audioDevice.preferredFormat(); - format.setChannelCount(0); - QTest::newRow("Channel count 0") - << format; - - format = audioDevice.preferredFormat(); - format.setSampleRate(0); - QTest::newRow("Sample rate 0") - << format; - - format = audioDevice.preferredFormat(); - format.setSampleSize(0); - QTest::newRow("Sample size 0") - << format; -} - -void tst_QAudioOutput::invalidFormat() -{ - QFETCH(QAudioFormat, invalidFormat); - - QVERIFY2(!audioDevice.isFormatSupported(invalidFormat), - "isFormatSupported() is returning true on an invalid format"); - - QAudioOutput audioOutput(invalidFormat, this); - - // Check that we are in the default state before calling start - QVERIFY2((audioOutput.state() == QAudio::StoppedState), "state() was not set to StoppedState before start()"); - QVERIFY2((audioOutput.error() == QAudio::NoError), "error() was not set to QAudio::NoError before start()"); - - audioOutput.start(); - // Check that error is raised - QTRY_VERIFY2((audioOutput.error() == QAudio::OpenError),"error() was not set to QAudio::OpenError after start()"); -} - -void tst_QAudioOutput::bufferSize_data() -{ - QTest::addColumn<int>("bufferSize"); - QTest::newRow("Buffer size 512") << 512; - QTest::newRow("Buffer size 4096") << 4096; - QTest::newRow("Buffer size 8192") << 8192; -} - -void tst_QAudioOutput::bufferSize() -{ - QFETCH(int, bufferSize); - QAudioOutput audioOutput(audioDevice.preferredFormat(), this); - - QVERIFY2((audioOutput.error() == QAudio::NoError), QString("error() was not set to QAudio::NoError on creation(%1)").arg(bufferSize).toLocal8Bit().constData()); - - audioOutput.setBufferSize(bufferSize); - QVERIFY2((audioOutput.error() == QAudio::NoError), "error() is not QAudio::NoError after setBufferSize"); - QVERIFY2((audioOutput.bufferSize() == bufferSize), - QString("bufferSize: requested=%1, actual=%2").arg(bufferSize).arg(audioOutput.bufferSize()).toLocal8Bit().constData()); -} - -void tst_QAudioOutput::notifyInterval_data() -{ - QTest::addColumn<int>("interval"); - QTest::newRow("Notify interval 50") << 50; - QTest::newRow("Notify interval 100") << 100; - QTest::newRow("Notify interval 250") << 250; - QTest::newRow("Notify interval 1000") << 1000; -} - -void tst_QAudioOutput::notifyInterval() -{ - QFETCH(int, interval); - QAudioOutput audioOutput(audioDevice.preferredFormat(), this); - - QVERIFY2((audioOutput.error() == QAudio::NoError), "error() was not set to QAudio::NoError on creation"); - - audioOutput.setNotifyInterval(interval); - QVERIFY2((audioOutput.error() == QAudio::NoError), QString("error() is not QAudio::NoError after setNotifyInterval(%1)").arg(interval).toLocal8Bit().constData()); - QVERIFY2((audioOutput.notifyInterval() == interval), - QString("notifyInterval: requested=%1, actual=%2").arg(interval).arg(audioOutput.notifyInterval()).toLocal8Bit().constData()); -} - -void tst_QAudioOutput::disableNotifyInterval() -{ - // Sets an invalid notification interval (QAudioOutput::setNotifyInterval(0)) - // Checks that - // - No error is raised (QAudioOutput::error() returns QAudio::NoError) - // - if <= 0, set to zero and disable notify signal - - QAudioOutput audioOutput(audioDevice.preferredFormat(), this); - - QVERIFY2((audioOutput.error() == QAudio::NoError), "error() was not set to QAudio::NoError on creation"); - - audioOutput.setNotifyInterval(0); - QVERIFY2((audioOutput.error() == QAudio::NoError), "error() is not QAudio::NoError after setNotifyInterval(0)"); - QVERIFY2((audioOutput.notifyInterval() == 0), - "notifyInterval() is not zero after setNotifyInterval(0)"); - - audioOutput.setNotifyInterval(-1); - QVERIFY2((audioOutput.error() == QAudio::NoError), "error() is not QAudio::NoError after setNotifyInterval(-1)"); - QVERIFY2((audioOutput.notifyInterval() == 0), - "notifyInterval() is not zero after setNotifyInterval(-1)"); - - //start and run to check if notify() is emitted - if (audioFiles.size() > 0) { - QAudioOutput audioOutputCheck(testFormats.at(0), this); - audioOutputCheck.setNotifyInterval(0); - audioOutputCheck.setVolume(0.1f); - - QSignalSpy notifySignal(&audioOutputCheck, SIGNAL(notify())); - QFile *audioFile = audioFiles.at(0).data(); - audioFile->open(QIODevice::ReadOnly); - audioOutputCheck.start(audioFile); - QTest::qWait(3000); // 3 seconds should be plenty - audioOutputCheck.stop(); - QVERIFY2((notifySignal.count() == 0), - QString("didn't disable notify interval: shouldn't have got any but got %1").arg(notifySignal.count()).toLocal8Bit().constData()); - audioFile->close(); - } -} - -void tst_QAudioOutput::stopWhileStopped() -{ - // Calls QAudioOutput::stop() when object is already in StoppedState - // Checks that - // - No state change occurs - // - No error is raised (QAudioOutput::error() returns QAudio::NoError) - - QAudioOutput audioOutput(audioDevice.preferredFormat(), this); - - QVERIFY2((audioOutput.state() == QAudio::StoppedState), "state() was not set to StoppedState before start()"); - QVERIFY2((audioOutput.error() == QAudio::NoError), "error() was not set to QAudio::NoError before start()"); - - QSignalSpy stateSignal(&audioOutput, SIGNAL(stateChanged(QAudio::State))); - audioOutput.stop(); - - // Check that no state transition occurred - QVERIFY2((stateSignal.count() == 0), "stop() while stopped is emitting a signal and it shouldn't"); - QVERIFY2((audioOutput.error() == QAudio::NoError), "error() was not set to QAudio::NoError after stop()"); -} - -void tst_QAudioOutput::suspendWhileStopped() -{ - // Calls QAudioOutput::suspend() when object is already in StoppedState - // Checks that - // - No state change occurs - // - No error is raised (QAudioOutput::error() returns QAudio::NoError) - - QAudioOutput audioOutput(audioDevice.preferredFormat(), this); - - QVERIFY2((audioOutput.state() == QAudio::StoppedState), "state() was not set to StoppedState before start()"); - QVERIFY2((audioOutput.error() == QAudio::NoError), "error() was not set to QAudio::NoError before start()"); - - QSignalSpy stateSignal(&audioOutput, SIGNAL(stateChanged(QAudio::State))); - audioOutput.suspend(); - - // Check that no state transition occurred - QVERIFY2((stateSignal.count() == 0), "stop() while suspended is emitting a signal and it shouldn't"); - QVERIFY2((audioOutput.error() == QAudio::NoError), "error() was not set to QAudio::NoError after stop()"); -} - -void tst_QAudioOutput::resumeWhileStopped() -{ - // Calls QAudioOutput::resume() when object is already in StoppedState - // Checks that - // - No state change occurs - // - No error is raised (QAudioOutput::error() returns QAudio::NoError) - - QAudioOutput audioOutput(audioDevice.preferredFormat(), this); - - QVERIFY2((audioOutput.state() == QAudio::StoppedState), "state() was not set to StoppedState before start()"); - QVERIFY2((audioOutput.error() == QAudio::NoError), "error() was not set to QAudio::NoError before start()"); - - QSignalSpy stateSignal(&audioOutput, SIGNAL(stateChanged(QAudio::State))); - audioOutput.resume(); - - // Check that no state transition occurred - QVERIFY2((stateSignal.count() == 0), "resume() while stopped is emitting a signal and it shouldn't"); - QVERIFY2((audioOutput.error() == QAudio::NoError), "error() was not set to QAudio::NoError after resume()"); -} - -void tst_QAudioOutput::pull() -{ - QFETCH(FilePtr, audioFile); - QFETCH(QAudioFormat, audioFormat); - - QAudioOutput audioOutput(audioFormat, this); - - audioOutput.setNotifyInterval(100); - audioOutput.setVolume(0.1f); - - QSignalSpy notifySignal(&audioOutput, SIGNAL(notify())); - QSignalSpy stateSignal(&audioOutput, SIGNAL(stateChanged(QAudio::State))); - - // Check that we are in the default state before calling start - QVERIFY2((audioOutput.state() == QAudio::StoppedState), "state() was not set to StoppedState before start()"); - QVERIFY2((audioOutput.error() == QAudio::NoError), "error() was not set to QAudio::NoError before start()"); - QVERIFY2((audioOutput.elapsedUSecs() == qint64(0)),"elapsedUSecs() not zero on creation"); - - audioFile->close(); - audioFile->open(QIODevice::ReadOnly); - audioFile->seek(WavHeader::headerLength()); - - audioOutput.start(audioFile.data()); - - // Check that QAudioOutput immediately transitions to ActiveState - QTRY_VERIFY2((stateSignal.count() == 1), - QString("didn't emit signal on start(), got %1 signals instead").arg(stateSignal.count()).toLocal8Bit().constData()); - QVERIFY2((audioOutput.state() == QAudio::ActiveState), "didn't transition to ActiveState after start()"); - QVERIFY2((audioOutput.error() == QAudio::NoError), "error state is not equal to QAudio::NoError after start()"); - QVERIFY(audioOutput.periodSize() > 0); - stateSignal.clear(); - - // Check that 'elapsed' increases - QTest::qWait(40); - QVERIFY2((audioOutput.elapsedUSecs() > 0), "elapsedUSecs() is still zero after start()"); - - // Wait until playback finishes - QTRY_VERIFY2(audioFile->atEnd(), "didn't play to EOF"); - QTRY_VERIFY(stateSignal.count() > 0); - QCOMPARE(qvariant_cast<QAudio::State>(stateSignal.last().at(0)), QAudio::IdleState); - QVERIFY2((audioOutput.state() == QAudio::IdleState), "didn't transitions to IdleState when at EOF"); - stateSignal.clear(); - - qint64 processedUs = audioOutput.processedUSecs(); - - audioOutput.stop(); - QTest::qWait(40); - QVERIFY2((stateSignal.count() == 1), - QString("didn't emit StoppedState signal after stop(), got %1 signals instead").arg(stateSignal.count()).toLocal8Bit().constData()); - QVERIFY2((audioOutput.state() == QAudio::StoppedState), "didn't transitions to StoppedState after stop()"); - - QVERIFY2((processedUs == 2000000), - QString("processedUSecs() doesn't equal file duration in us (%1)").arg(processedUs).toLocal8Bit().constData()); - QVERIFY2((audioOutput.error() == QAudio::NoError), "error() is not QAudio::NoError after stop()"); - QVERIFY2((audioOutput.elapsedUSecs() == (qint64)0), "elapsedUSecs() not equal to zero in StoppedState"); - QVERIFY2(notifySignal.count() > 0, "not emitting notify() signal"); - - audioFile->close(); -} - -void tst_QAudioOutput::pullSuspendResume() -{ - QFETCH(FilePtr, audioFile); - QFETCH(QAudioFormat, audioFormat); - QAudioOutput audioOutput(audioFormat, this); - - audioOutput.setNotifyInterval(100); - audioOutput.setVolume(0.1f); - - QSignalSpy notifySignal(&audioOutput, SIGNAL(notify())); - QSignalSpy stateSignal(&audioOutput, SIGNAL(stateChanged(QAudio::State))); - - // Check that we are in the default state before calling start - QVERIFY2((audioOutput.state() == QAudio::StoppedState), "state() was not set to StoppedState before start()"); - QVERIFY2((audioOutput.error() == QAudio::NoError), "error() was not set to QAudio::NoError before start()"); - QVERIFY2((audioOutput.elapsedUSecs() == qint64(0)),"elapsedUSecs() not zero on creation"); - - audioFile->close(); - audioFile->open(QIODevice::ReadOnly); - audioFile->seek(WavHeader::headerLength()); - - audioOutput.start(audioFile.data()); - // Check that QAudioOutput immediately transitions to ActiveState - QTRY_VERIFY2((stateSignal.count() == 1), - QString("didn't emit signal on start(), got %1 signals instead").arg(stateSignal.count()).toLocal8Bit().constData()); - QVERIFY2((audioOutput.state() == QAudio::ActiveState), "didn't transition to ActiveState after start()"); - QVERIFY2((audioOutput.error() == QAudio::NoError), "error state is not equal to QAudio::NoError after start()"); - QVERIFY(audioOutput.periodSize() > 0); - stateSignal.clear(); - - // Wait for half of clip to play - QTest::qWait(1000); - - audioOutput.suspend(); - - // Give backends running in separate threads a chance to suspend. - QTest::qWait(100); - - QVERIFY2((stateSignal.count() == 1), - QString("didn't emit SuspendedState signal after suspend(), got %1 signals instead") - .arg(stateSignal.count()).toLocal8Bit().constData()); - QVERIFY2((audioOutput.state() == QAudio::SuspendedState), "didn't transition to SuspendedState after suspend()"); - QVERIFY2((audioOutput.error() == QAudio::NoError), "error state is not equal to QAudio::NoError after suspend()"); - stateSignal.clear(); - - // Check that only 'elapsed', and not 'processed' increases while suspended - qint64 elapsedUs = audioOutput.elapsedUSecs(); - qint64 processedUs = audioOutput.processedUSecs(); - QTest::qWait(1000); - QVERIFY(audioOutput.elapsedUSecs() > elapsedUs); - QVERIFY(audioOutput.processedUSecs() == processedUs); - - audioOutput.resume(); - - // Check that QAudioOutput immediately transitions to ActiveState - QVERIFY2((stateSignal.count() == 1), - QString("didn't emit signal after resume(), got %1 signals instead").arg(stateSignal.count()).toLocal8Bit().constData()); - QVERIFY2((audioOutput.state() == QAudio::ActiveState), "didn't transition to ActiveState after resume()"); - QVERIFY2((audioOutput.error() == QAudio::NoError), "error state is not equal to QAudio::NoError after resume()"); - stateSignal.clear(); - - // Wait until playback finishes - QTest::qWait(3000); // 3 seconds should be plenty - - QVERIFY2(audioFile->atEnd(), "didn't play to EOF"); - QVERIFY(stateSignal.count() > 0); - QCOMPARE(qvariant_cast<QAudio::State>(stateSignal.last().at(0)), QAudio::IdleState); - QVERIFY2((audioOutput.state() == QAudio::IdleState), "didn't transitions to IdleState when at EOF"); - stateSignal.clear(); - - processedUs = audioOutput.processedUSecs(); - - audioOutput.stop(); - QTest::qWait(40); - QVERIFY2((stateSignal.count() == 1), - QString("didn't emit StoppedState signal after stop(), got %1 signals instead").arg(stateSignal.count()).toLocal8Bit().constData()); - QVERIFY2((audioOutput.state() == QAudio::StoppedState), "didn't transitions to StoppedState after stop()"); - - QVERIFY2((processedUs == 2000000), - QString("processedUSecs() doesn't equal file duration in us (%1)").arg(processedUs).toLocal8Bit().constData()); - QVERIFY2((audioOutput.error() == QAudio::NoError), "error() is not QAudio::NoError after stop()"); - QVERIFY2((audioOutput.elapsedUSecs() == (qint64)0), "elapsedUSecs() not equal to zero in StoppedState"); - - audioFile->close(); -} - -void tst_QAudioOutput::push() -{ - QFETCH(FilePtr, audioFile); - QFETCH(QAudioFormat, audioFormat); - - QAudioOutput audioOutput(audioFormat, this); - - audioOutput.setNotifyInterval(100); - audioOutput.setVolume(0.1f); - - QSignalSpy notifySignal(&audioOutput, SIGNAL(notify())); - QSignalSpy stateSignal(&audioOutput, SIGNAL(stateChanged(QAudio::State))); - - // Check that we are in the default state before calling start - QVERIFY2((audioOutput.state() == QAudio::StoppedState), "state() was not set to StoppedState before start()"); - QVERIFY2((audioOutput.error() == QAudio::NoError), "error() was not set to QAudio::NoError before start()"); - QVERIFY2((audioOutput.elapsedUSecs() == qint64(0)),"elapsedUSecs() not zero on creation"); - - audioFile->close(); - audioFile->open(QIODevice::ReadOnly); - audioFile->seek(WavHeader::headerLength()); - - QIODevice* feed = audioOutput.start(); - - // Check that QAudioOutput immediately transitions to IdleState - QTRY_VERIFY2((stateSignal.count() == 1), - QString("didn't emit signal on start(), got %1 signals instead").arg(stateSignal.count()).toLocal8Bit().constData()); - QVERIFY2((audioOutput.state() == QAudio::IdleState), "didn't transition to IdleState after start()"); - QVERIFY2((audioOutput.error() == QAudio::NoError), "error state is not equal to QAudio::NoError after start()"); - QVERIFY(audioOutput.periodSize() > 0); - stateSignal.clear(); - - // Check that 'elapsed' increases - QTest::qWait(40); - QVERIFY2((audioOutput.elapsedUSecs() > 0), "elapsedUSecs() is still zero after start()"); - QVERIFY2((audioOutput.processedUSecs() == qint64(0)), "processedUSecs() is not zero after start()"); - - qint64 written = 0; - bool firstBuffer = true; - QByteArray buffer(AUDIO_BUFFER, 0); - - while (written < audioFile->size()-WavHeader::headerLength()) { - - if (audioOutput.bytesFree() >= audioOutput.periodSize()) { - qint64 len = audioFile->read(buffer.data(),audioOutput.periodSize()); - written += feed->write(buffer.constData(), len); - - if (firstBuffer) { - // Check for transition to ActiveState when data is provided - QVERIFY2((stateSignal.count() == 1), - QString("didn't emit signal after receiving data, got %1 signals instead") - .arg(stateSignal.count()).toLocal8Bit().constData()); - QVERIFY2((audioOutput.state() == QAudio::ActiveState), "didn't transition to ActiveState after receiving data"); - QVERIFY2((audioOutput.error() == QAudio::NoError), "error state is not equal to QAudio::NoError after receiving data"); - firstBuffer = false; - stateSignal.clear(); - } - } else - QTest::qWait(20); - } - - // Wait until playback finishes - QTest::qWait(3000); // 3 seconds should be plenty - - QVERIFY2(audioFile->atEnd(), "didn't play to EOF"); - QVERIFY(stateSignal.count() > 0); - QCOMPARE(qvariant_cast<QAudio::State>(stateSignal.last().at(0)), QAudio::IdleState); - QVERIFY2((audioOutput.state() == QAudio::IdleState), "didn't transitions to IdleState when at EOF"); - stateSignal.clear(); - - qint64 processedUs = audioOutput.processedUSecs(); - - audioOutput.stop(); - QTest::qWait(40); - QVERIFY2((stateSignal.count() == 1), - QString("didn't emit StoppedState signal after stop(), got %1 signals instead").arg(stateSignal.count()).toLocal8Bit().constData()); - QVERIFY2((audioOutput.state() == QAudio::StoppedState), "didn't transitions to StoppedState after stop()"); - - QVERIFY2((processedUs == 2000000), - QString("processedUSecs() doesn't equal file duration in us (%1)").arg(processedUs).toLocal8Bit().constData()); - QVERIFY2((audioOutput.error() == QAudio::NoError), "error() is not QAudio::NoError after stop()"); - QVERIFY2((audioOutput.elapsedUSecs() == (qint64)0), "elapsedUSecs() not equal to zero in StoppedState"); - QVERIFY2(notifySignal.count() > 0, "not emitting notify signal"); - - audioFile->close(); -} - -void tst_QAudioOutput::pushSuspendResume() -{ - QFETCH(FilePtr, audioFile); - QFETCH(QAudioFormat, audioFormat); - - QAudioOutput audioOutput(audioFormat, this); - - audioOutput.setNotifyInterval(100); - audioOutput.setVolume(0.1f); - - QSignalSpy notifySignal(&audioOutput, SIGNAL(notify())); - QSignalSpy stateSignal(&audioOutput, SIGNAL(stateChanged(QAudio::State))); - - // Check that we are in the default state before calling start - QVERIFY2((audioOutput.state() == QAudio::StoppedState), "state() was not set to StoppedState before start()"); - QVERIFY2((audioOutput.error() == QAudio::NoError), "error() was not set to QAudio::NoError before start()"); - QVERIFY2((audioOutput.elapsedUSecs() == qint64(0)),"elapsedUSecs() not zero on creation"); - - audioFile->close(); - audioFile->open(QIODevice::ReadOnly); - audioFile->seek(WavHeader::headerLength()); - - QIODevice* feed = audioOutput.start(); - - // Check that QAudioOutput immediately transitions to IdleState - QTRY_VERIFY2((stateSignal.count() == 1), - QString("didn't emit signal on start(), got %1 signals instead").arg(stateSignal.count()).toLocal8Bit().constData()); - QVERIFY2((audioOutput.state() == QAudio::IdleState), "didn't transition to IdleState after start()"); - QVERIFY2((audioOutput.error() == QAudio::NoError), "error state is not equal to QAudio::NoError after start()"); - QVERIFY(audioOutput.periodSize() > 0); - stateSignal.clear(); - - // Check that 'elapsed' increases - QTest::qWait(40); - QVERIFY2((audioOutput.elapsedUSecs() > 0), "elapsedUSecs() is still zero after start()"); - QVERIFY2((audioOutput.processedUSecs() == qint64(0)), "processedUSecs() is not zero after start()"); - - qint64 written = 0; - bool firstBuffer = true; - QByteArray buffer(AUDIO_BUFFER, 0); - - // Play half of the clip - while (written < (audioFile->size()-WavHeader::headerLength())/2) { - - if (audioOutput.bytesFree() >= audioOutput.periodSize()) { - qint64 len = audioFile->read(buffer.data(),audioOutput.periodSize()); - written += feed->write(buffer.constData(), len); - - if (firstBuffer) { - // Check for transition to ActiveState when data is provided - QVERIFY2((stateSignal.count() == 1), - QString("didn't emit signal after receiving data, got %1 signals instead") - .arg(stateSignal.count()).toLocal8Bit().constData()); - QVERIFY2((audioOutput.state() == QAudio::ActiveState), "didn't transition to ActiveState after receiving data"); - QVERIFY2((audioOutput.error() == QAudio::NoError), "error state is not equal to QAudio::NoError after receiving data"); - firstBuffer = false; - } - } else - QTest::qWait(20); - } - stateSignal.clear(); - - audioOutput.suspend(); - - // Give backends running in separate threads a chance to suspend. - QTest::qWait(100); - - QVERIFY2((stateSignal.count() == 1), - QString("didn't emit SuspendedState signal after suspend(), got %1 signals instead") - .arg(stateSignal.count()).toLocal8Bit().constData()); - QVERIFY2((audioOutput.state() == QAudio::SuspendedState), "didn't transition to SuspendedState after suspend()"); - QVERIFY2((audioOutput.error() == QAudio::NoError), "error state is not equal to QAudio::NoError after suspend()"); - stateSignal.clear(); - - // Check that only 'elapsed', and not 'processed' increases while suspended - qint64 elapsedUs = audioOutput.elapsedUSecs(); - qint64 processedUs = audioOutput.processedUSecs(); - QTest::qWait(1000); - QVERIFY(audioOutput.elapsedUSecs() > elapsedUs); - QVERIFY(audioOutput.processedUSecs() == processedUs); - - audioOutput.resume(); - - // Give backends running in separate threads a chance to resume - // but not too much or the rest of the file may be processed - QTest::qWait(20); - - // Check that QAudioOutput immediately transitions to IdleState - QVERIFY2((stateSignal.count() == 1), - QString("didn't emit signal after resume(), got %1 signals instead").arg(stateSignal.count()).toLocal8Bit().constData()); - QVERIFY2((audioOutput.state() == QAudio::IdleState), "didn't transition to IdleState after resume()"); - QVERIFY2((audioOutput.error() == QAudio::NoError), "error state is not equal to QAudio::NoError after resume()"); - stateSignal.clear(); - - // Play rest of the clip - while (!audioFile->atEnd()) { - if (audioOutput.bytesFree() >= audioOutput.periodSize()) { - qint64 len = audioFile->read(buffer.data(),audioOutput.periodSize()); - written += feed->write(buffer.constData(), len); - QVERIFY2((audioOutput.state() == QAudio::ActiveState), "didn't transition to ActiveState after writing audio data"); - } else - QTest::qWait(20); - } - stateSignal.clear(); - - // Wait until playback finishes - QTest::qWait(1000); // 1 seconds should be plenty - - QVERIFY2(audioFile->atEnd(), "didn't play to EOF"); - QVERIFY(stateSignal.count() > 0); - QCOMPARE(qvariant_cast<QAudio::State>(stateSignal.last().at(0)), QAudio::IdleState); - QVERIFY2((audioOutput.state() == QAudio::IdleState), "didn't transitions to IdleState when at EOF"); - stateSignal.clear(); - - processedUs = audioOutput.processedUSecs(); - - audioOutput.stop(); - QTest::qWait(40); - QVERIFY2((stateSignal.count() == 1), - QString("didn't emit StoppedState signal after stop(), got %1 signals instead").arg(stateSignal.count()).toLocal8Bit().constData()); - QVERIFY2((audioOutput.state() == QAudio::StoppedState), "didn't transitions to StoppedState after stop()"); - - QVERIFY2((processedUs == 2000000), - QString("processedUSecs() doesn't equal file duration in us (%1)").arg(processedUs).toLocal8Bit().constData()); - QVERIFY2((audioOutput.error() == QAudio::NoError), "error() is not QAudio::NoError after stop()"); - QVERIFY2((audioOutput.elapsedUSecs() == (qint64)0), "elapsedUSecs() not equal to zero in StoppedState"); - - audioFile->close(); -} - -void tst_QAudioOutput::pushUnderrun() -{ - QFETCH(FilePtr, audioFile); - QFETCH(QAudioFormat, audioFormat); - - QAudioOutput audioOutput(audioFormat, this); - - audioOutput.setNotifyInterval(100); - audioOutput.setVolume(0.1f); - - QSignalSpy notifySignal(&audioOutput, SIGNAL(notify())); - QSignalSpy stateSignal(&audioOutput, SIGNAL(stateChanged(QAudio::State))); - - // Check that we are in the default state before calling start - QVERIFY2((audioOutput.state() == QAudio::StoppedState), "state() was not set to StoppedState before start()"); - QVERIFY2((audioOutput.error() == QAudio::NoError), "error() was not set to QAudio::NoError before start()"); - QVERIFY2((audioOutput.elapsedUSecs() == qint64(0)),"elapsedUSecs() not zero on creation"); - - audioFile->close(); - audioFile->open(QIODevice::ReadOnly); - audioFile->seek(WavHeader::headerLength()); - - QIODevice* feed = audioOutput.start(); - - // Check that QAudioOutput immediately transitions to IdleState - QTRY_VERIFY2((stateSignal.count() == 1), - QString("didn't emit signal on start(), got %1 signals instead").arg(stateSignal.count()).toLocal8Bit().constData()); - QVERIFY2((audioOutput.state() == QAudio::IdleState), "didn't transition to IdleState after start()"); - QVERIFY2((audioOutput.error() == QAudio::NoError), "error state is not equal to QAudio::NoError after start()"); - QVERIFY(audioOutput.periodSize() > 0); - stateSignal.clear(); - - // Check that 'elapsed' increases - QTest::qWait(40); - QVERIFY2((audioOutput.elapsedUSecs() > 0), "elapsedUSecs() is still zero after start()"); - QVERIFY2((audioOutput.processedUSecs() == qint64(0)), "processedUSecs() is not zero after start()"); - - qint64 written = 0; - bool firstBuffer = true; - QByteArray buffer(AUDIO_BUFFER, 0); - - // Play half of the clip - while (written < (audioFile->size()-WavHeader::headerLength())/2) { - - if (audioOutput.bytesFree() >= audioOutput.periodSize()) { - qint64 len = audioFile->read(buffer.data(),audioOutput.periodSize()); - written += feed->write(buffer.constData(), len); - - if (firstBuffer) { - // Check for transition to ActiveState when data is provided - QVERIFY2((stateSignal.count() == 1), - QString("didn't emit signal after receiving data, got %1 signals instead") - .arg(stateSignal.count()).toLocal8Bit().constData()); - QVERIFY2((audioOutput.state() == QAudio::ActiveState), "didn't transition to ActiveState after receiving data"); - QVERIFY2((audioOutput.error() == QAudio::NoError), "error state is not equal to QAudio::NoError after receiving data"); - firstBuffer = false; - } - } else - QTest::qWait(20); - } - stateSignal.clear(); - - // Wait for data to be played - QTest::qWait(1000); - - QVERIFY2((stateSignal.count() == 1), - QString("didn't emit IdleState signal after suspend(), got %1 signals instead") - .arg(stateSignal.count()).toLocal8Bit().constData()); - QVERIFY2((audioOutput.state() == QAudio::IdleState), "didn't transition to IdleState, no data"); - QVERIFY2((audioOutput.error() == QAudio::UnderrunError), "error state is not equal to QAudio::UnderrunError, no data"); - stateSignal.clear(); - - firstBuffer = true; - // Play rest of the clip - while (!audioFile->atEnd()) { - if (audioOutput.bytesFree() >= audioOutput.periodSize()) { - qint64 len = audioFile->read(buffer.data(),audioOutput.periodSize()); - written += feed->write(buffer.constData(), len); - if (firstBuffer) { - // Check for transition to ActiveState when data is provided - QVERIFY2((stateSignal.count() == 1), - QString("didn't emit signal after receiving data, got %1 signals instead") - .arg(stateSignal.count()).toLocal8Bit().constData()); - QVERIFY2((audioOutput.state() == QAudio::ActiveState), "didn't transition to ActiveState after receiving data"); - QVERIFY2((audioOutput.error() == QAudio::NoError), "error state is not equal to QAudio::NoError after receiving data"); - firstBuffer = false; - } - } else - QTest::qWait(20); - } - stateSignal.clear(); - - // Wait until playback finishes - QTest::qWait(1000); // 1 seconds should be plenty - - QVERIFY2(audioFile->atEnd(), "didn't play to EOF"); - QVERIFY2((stateSignal.count() == 1), - QString("didn't emit IdleState signal when at EOF, got %1 signals instead").arg(stateSignal.count()).toLocal8Bit().constData()); - QVERIFY2((audioOutput.state() == QAudio::IdleState), "didn't transitions to IdleState when at EOF"); - stateSignal.clear(); - - qint64 processedUs = audioOutput.processedUSecs(); - - audioOutput.stop(); - QTest::qWait(40); - QVERIFY2((stateSignal.count() == 1), - QString("didn't emit StoppedState signal after stop(), got %1 signals instead").arg(stateSignal.count()).toLocal8Bit().constData()); - QVERIFY2((audioOutput.state() == QAudio::StoppedState), "didn't transitions to StoppedState after stop()"); - - QVERIFY2((processedUs == 2000000), - QString("processedUSecs() doesn't equal file duration in us (%1)").arg(processedUs).toLocal8Bit().constData()); - QVERIFY2((audioOutput.error() == QAudio::NoError), "error() is not QAudio::NoError after stop()"); - QVERIFY2((audioOutput.elapsedUSecs() == (qint64)0), "elapsedUSecs() not equal to zero in StoppedState"); - - audioFile->close(); -} - -void tst_QAudioOutput::volume_data() -{ - QTest::addColumn<float>("actualFloat"); - QTest::addColumn<int>("expectedInt"); - QTest::newRow("Volume 0.3") << 0.3f << 3; - QTest::newRow("Volume 0.6") << 0.6f << 6; - QTest::newRow("Volume 0.9") << 0.9f << 9; -} - -void tst_QAudioOutput::volume() -{ - QFETCH(float, actualFloat); - QFETCH(int, expectedInt); - QAudioOutput audioOutput(audioDevice.preferredFormat(), this); - - audioOutput.setVolume(actualFloat); - QTRY_VERIFY(qRound(audioOutput.volume()*10.0f) == expectedInt); - // Wait a while to see if this changes - QTest::qWait(500); - QTRY_VERIFY(qRound(audioOutput.volume()*10.0f) == expectedInt); -} - -QTEST_MAIN(tst_QAudioOutput) - -#include "tst_qaudiooutput.moc" diff --git a/tests/auto/integration/qaudiooutput/wavheader.cpp b/tests/auto/integration/qaudiooutput/wavheader.cpp deleted file mode 100644 index 869d74d5b..000000000 --- a/tests/auto/integration/qaudiooutput/wavheader.cpp +++ /dev/null @@ -1,192 +0,0 @@ -/**************************************************************************** -** -** Copyright (C) 2016 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the test suite of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:GPL-EXCEPT$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 3 as published by the Free Software -** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ - -#include <QtCore/qendian.h> -#include "wavheader.h" - - -struct chunk -{ - char id[4]; - quint32 size; -}; - -struct RIFFHeader -{ - chunk descriptor; // "RIFF" - char type[4]; // "WAVE" -}; - -struct WAVEHeader -{ - chunk descriptor; - quint16 audioFormat; - quint16 numChannels; - quint32 sampleRate; - quint32 byteRate; - quint16 blockAlign; - quint16 bitsPerSample; -}; - -struct DATAHeader -{ - chunk descriptor; -}; - -struct CombinedHeader -{ - RIFFHeader riff; - WAVEHeader wave; - DATAHeader data; -}; - -static const int HeaderLength = sizeof(CombinedHeader); - - -WavHeader::WavHeader(const QAudioFormat &format, qint64 dataLength) - : m_format(format) - , m_dataLength(dataLength) -{ - -} - -bool WavHeader::read(QIODevice &device) -{ - bool result = true; - - if (!device.isSequential()) - result = device.seek(0); - // else, assume that current position is the start of the header - - if (result) { - CombinedHeader header; - result = (device.read(reinterpret_cast<char *>(&header), HeaderLength) == HeaderLength); - if (result) { - if ((memcmp(&header.riff.descriptor.id, "RIFF", 4) == 0 - || memcmp(&header.riff.descriptor.id, "RIFX", 4) == 0) - && memcmp(&header.riff.type, "WAVE", 4) == 0 - && memcmp(&header.wave.descriptor.id, "fmt ", 4) == 0 - && header.wave.audioFormat == 1 // PCM - ) { - if (memcmp(&header.riff.descriptor.id, "RIFF", 4) == 0) - m_format.setByteOrder(QAudioFormat::LittleEndian); - else - m_format.setByteOrder(QAudioFormat::BigEndian); - - m_format.setChannelCount(qFromLittleEndian<quint16>(header.wave.numChannels)); - m_format.setCodec("audio/pcm"); - m_format.setSampleRate(qFromLittleEndian<quint32>(header.wave.sampleRate)); - m_format.setSampleSize(qFromLittleEndian<quint16>(header.wave.bitsPerSample)); - - switch(header.wave.bitsPerSample) { - case 8: - m_format.setSampleType(QAudioFormat::UnSignedInt); - break; - case 16: - m_format.setSampleType(QAudioFormat::SignedInt); - break; - default: - result = false; - } - - m_dataLength = device.size() - HeaderLength; - } else { - result = false; - } - } - } - - return result; -} - -bool WavHeader::write(QIODevice &device) -{ - CombinedHeader header; - - memset(&header, 0, HeaderLength); - - // RIFF header - if (m_format.byteOrder() == QAudioFormat::LittleEndian) - memcpy(header.riff.descriptor.id,"RIFF",4); - else - memcpy(header.riff.descriptor.id,"RIFX",4); - qToLittleEndian<quint32>(quint32(m_dataLength + HeaderLength - 8), - reinterpret_cast<unsigned char*>(&header.riff.descriptor.size)); - memcpy(header.riff.type, "WAVE",4); - - // WAVE header - memcpy(header.wave.descriptor.id,"fmt ",4); - qToLittleEndian<quint32>(quint32(16), - reinterpret_cast<unsigned char*>(&header.wave.descriptor.size)); - qToLittleEndian<quint16>(quint16(1), - reinterpret_cast<unsigned char*>(&header.wave.audioFormat)); - qToLittleEndian<quint16>(quint16(m_format.channelCount()), - reinterpret_cast<unsigned char*>(&header.wave.numChannels)); - qToLittleEndian<quint32>(quint32(m_format.sampleRate()), - reinterpret_cast<unsigned char*>(&header.wave.sampleRate)); - qToLittleEndian<quint32>(quint32(m_format.sampleRate() * m_format.channelCount() * m_format.sampleSize() / 8), - reinterpret_cast<unsigned char*>(&header.wave.byteRate)); - qToLittleEndian<quint16>(quint16(m_format.channelCount() * m_format.sampleSize() / 8), - reinterpret_cast<unsigned char*>(&header.wave.blockAlign)); - qToLittleEndian<quint16>(quint16(m_format.sampleSize()), - reinterpret_cast<unsigned char*>(&header.wave.bitsPerSample)); - - // DATA header - memcpy(header.data.descriptor.id,"data",4); - qToLittleEndian<quint32>(quint32(m_dataLength), - reinterpret_cast<unsigned char*>(&header.data.descriptor.size)); - - return (device.write(reinterpret_cast<const char *>(&header), HeaderLength) == HeaderLength); -} - -const QAudioFormat& WavHeader::format() const -{ - return m_format; -} - -qint64 WavHeader::dataLength() const -{ - return m_dataLength; -} - -qint64 WavHeader::headerLength() -{ - return HeaderLength; -} - -bool WavHeader::writeDataLength(QIODevice &device, qint64 dataLength) -{ - bool result = false; - if (!device.isSequential()) { - device.seek(40); - unsigned char dataLengthLE[4]; - qToLittleEndian<quint32>(quint32(dataLength), dataLengthLE); - result = (device.write(reinterpret_cast<const char *>(dataLengthLE), 4) == 4); - } - return result; -} diff --git a/tests/auto/integration/qaudiooutput/wavheader.h b/tests/auto/integration/qaudiooutput/wavheader.h deleted file mode 100644 index b9595cffc..000000000 --- a/tests/auto/integration/qaudiooutput/wavheader.h +++ /dev/null @@ -1,67 +0,0 @@ -/**************************************************************************** -** -** Copyright (C) 2016 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the test suite of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:GPL-EXCEPT$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 3 as published by the Free Software -** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ - - -#ifndef WAVHEADER_H -#define WAVHEADER_H - -#include <QtCore/qobject.h> -#include <QtCore/qfile.h> -#include <qaudioformat.h> - -/** - * Helper class for parsing WAV file headers. - * - * See https://ccrma.stanford.edu/courses/422/projects/WaveFormat/ - */ -class WavHeader -{ -public: - WavHeader(const QAudioFormat &format = QAudioFormat(), - qint64 dataLength = 0); - - // Reads WAV header and seeks to start of data - bool read(QIODevice &device); - - // Writes WAV header - bool write(QIODevice &device); - - const QAudioFormat& format() const; - qint64 dataLength() const; - - static qint64 headerLength(); - - static bool writeDataLength(QIODevice &device, qint64 dataLength); - -private: - QAudioFormat m_format; - qint64 m_dataLength; -}; - -#endif - diff --git a/tests/auto/integration/qaudiosink/BLACKLIST b/tests/auto/integration/qaudiosink/BLACKLIST new file mode 100644 index 000000000..0b8789267 --- /dev/null +++ b/tests/auto/integration/qaudiosink/BLACKLIST @@ -0,0 +1,11 @@ +#QTBUG-113194 +[pullSuspendResume] +macos ci + +#QTBUG-113194 +[pushSuspendResume] +macos ci + +#QTBUG-122309 +[pullResumeFromUnderrun] +rhel-9.2 diff --git a/tests/auto/integration/qaudiosink/CMakeLists.txt b/tests/auto/integration/qaudiosink/CMakeLists.txt new file mode 100644 index 000000000..902326dcc --- /dev/null +++ b/tests/auto/integration/qaudiosink/CMakeLists.txt @@ -0,0 +1,14 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +##################################################################### +## tst_qaudiosink Test: +##################################################################### + +qt_internal_add_test(tst_qaudiosink + SOURCES + tst_qaudiosink.cpp + LIBRARIES + Qt::Gui + Qt::MultimediaPrivate +) diff --git a/tests/auto/integration/qaudiosink/tst_qaudiosink.cpp b/tests/auto/integration/qaudiosink/tst_qaudiosink.cpp new file mode 100644 index 000000000..6fdfe8221 --- /dev/null +++ b/tests/auto/integration/qaudiosink/tst_qaudiosink.cpp @@ -0,0 +1,1053 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include <QtTest/QtTest> +#include <QtCore/qlocale.h> +#include <QtCore/QTemporaryDir> +#include <QtCore/QSharedPointer> +#include <QtCore/QScopedPointer> + +#include <qaudiosink.h> +#include <qaudiodevice.h> +#include <qaudioformat.h> +#include <qaudio.h> +#include <qmediadevices.h> +#include <qwavedecoder.h> + +#define AUDIO_BUFFER 192000 + +class tst_QAudioSink : public QObject +{ + Q_OBJECT +public: + tst_QAudioSink(QObject* parent=nullptr) : QObject(parent) {} + +private slots: + void initTestCase(); + void format(); + void invalidFormat_data(); + void invalidFormat(); + + void bufferSize_data(); + void bufferSize(); + + void stopWhileStopped(); + void suspendWhileStopped(); + void resumeWhileStopped(); + + void pull_data(){generate_audiofile_testrows();} + void pull(); + + void pullSuspendResume_data(){generate_audiofile_testrows();} + void pullSuspendResume(); + void pullResumeFromUnderrun(); + + void push_data(){generate_audiofile_testrows();} + void push(); + + void pushSuspendResume_data(){generate_audiofile_testrows();} + void pushSuspendResume(); + + void pushResetResume(); + + void pushUnderrun_data(){generate_audiofile_testrows();} + void pushUnderrun(); + + void volume_data(); + void volume(); + +private: + using FilePtr = QSharedPointer<QFile>; + + static QString formatToFileName(const QAudioFormat &format); + void createSineWaveData(const QAudioFormat &format, qint64 length, int sampleRate = 440); + static QString dumpStateSignalSpy(const QSignalSpy &stateSignalSpy); + + static qint64 wavDataSize(QIODevice &input); + + template<typename Checker> + static void pushDataToAudioSink(QAudioSink &sink, QIODevice &input, QIODevice &feed, + qint64 &allWritten, qint64 writtenLimit, Checker &&checker, + bool checkOnlyFirst = false); + + void generate_audiofile_testrows(); + + QAudioDevice audioDevice; + QList<QAudioFormat> testFormats; + QList<FilePtr> audioFiles; + QScopedPointer<QTemporaryDir> m_temporaryDir; + + QScopedPointer<QByteArray> m_byteArray; + QScopedPointer<QBuffer> m_buffer; +}; + +QString tst_QAudioSink::formatToFileName(const QAudioFormat &format) +{ + return QStringLiteral("%1_%2_%3") + .arg(format.sampleRate()) + .arg(format.bytesPerSample()) + .arg(format.channelCount()); +} + +void tst_QAudioSink::createSineWaveData(const QAudioFormat &format, qint64 length, int sampleRate) +{ + const int channelBytes = format.bytesPerSample(); + const int sampleBytes = format.bytesPerFrame(); + + Q_ASSERT(length % sampleBytes == 0); + Q_UNUSED(sampleBytes); // suppress warning in release builds + + m_byteArray.reset(new QByteArray(length, 0)); + unsigned char *ptr = reinterpret_cast<unsigned char *>(m_byteArray->data()); + int sampleIndex = 0; + + while (length) { + const qreal x = qSin(2 * M_PI * sampleRate * qreal(sampleIndex % format.sampleRate()) / format.sampleRate()); + for (int i = 0; i < format.channelCount(); ++i) { + switch (format.sampleFormat()) { + case QAudioFormat::UInt8: { + const quint8 value = static_cast<quint8>((1.0 + x) / 2 * 255); + *reinterpret_cast<quint8 *>(ptr) = value; + break; + } + case QAudioFormat::Int16: { + qint16 value = static_cast<qint16>(x * 32767); + *reinterpret_cast<qint16 *>(ptr) = value; + break; + } + case QAudioFormat::Int32: { + quint32 value = static_cast<quint32>(x) * std::numeric_limits<qint32>::max(); + *reinterpret_cast<qint32 *>(ptr) = value; + break; + } + case QAudioFormat::Float: + *reinterpret_cast<float *>(ptr) = x; + break; + case QAudioFormat::Unknown: + case QAudioFormat::NSampleFormats: + break; + } + + ptr += channelBytes; + length -= channelBytes; + } + ++sampleIndex; + } + + m_buffer.reset(new QBuffer(m_byteArray.data(), this)); + Q_ASSERT(m_buffer->open(QIODevice::ReadOnly)); +} + +QString tst_QAudioSink::dumpStateSignalSpy(const QSignalSpy& stateSignalSpy) { + QString result = "["; + bool first = true; + for (auto& params : stateSignalSpy) + { + if (!std::exchange(first, false)) + result += ','; + result += QString::number(params.front().value<QAudio::State>()); + } + result.append(']'); + return result; +} + +qint64 tst_QAudioSink::wavDataSize(QIODevice &input) +{ + return input.size() - QWaveDecoder::headerLength(); +} + +template<typename Checker> +void tst_QAudioSink::pushDataToAudioSink(QAudioSink &sink, QIODevice &input, QIODevice &feed, + qint64 &allWritten, qint64 writtenLimit, Checker &&checker, + bool checkOnlyFirst) +{ + bool firstBuffer = true; + qint64 offset = 0; + QByteArray buffer; + + while ((allWritten < writtenLimit || writtenLimit < 0) && !input.atEnd() + && !QTest::currentTestFailed()) { + if (sink.bytesFree() > 0) { + if (buffer.isNull()) + buffer = input.read(sink.bytesFree()); + + const auto written = feed.write(buffer); + allWritten += written; + offset += written; + + if (offset >= buffer.size()) { + offset = 0; + buffer.clear(); + } + + if (!checkOnlyFirst || firstBuffer) + checker(); + + firstBuffer = false; + } else { + // wait a bit to ensure some the sink has consumed some data + // The delay getting might need some improvements + const auto delay = qMin(10, sink.format().durationForBytes(sink.bufferSize()) / 1000 / 2); + QTest::qWait(delay); + } + } +} + +void tst_QAudioSink::generate_audiofile_testrows() +{ + QTest::addColumn<FilePtr>("audioFile"); + QTest::addColumn<QAudioFormat>("audioFormat"); + + for (int i=0; i<audioFiles.size(); i++) { + QTest::newRow(QStringLiteral("Audio File %1").arg(i).toUtf8().constData()) + << audioFiles.at(i) << testFormats.at(i); + } +} + +void tst_QAudioSink::initTestCase() +{ + // Only perform tests if audio output device exists + const QList<QAudioDevice> devices = QMediaDevices::audioOutputs(); + + if (devices.size() <= 0) + QSKIP("No audio backend"); + + audioDevice = QMediaDevices::defaultAudioOutput(); + + + QAudioFormat format; + + if (audioDevice.isFormatSupported(audioDevice.preferredFormat())) { + if (format.sampleFormat() == QAudioFormat::Int16) + testFormats.append(audioDevice.preferredFormat()); + } + + // PCM 11025 mono S16LE + format.setChannelCount(1); + format.setSampleRate(11025); + format.setSampleFormat(QAudioFormat::Int16); + if (audioDevice.isFormatSupported(format)) + testFormats.append(format); + + // PCM 22050 mono S16LE + format.setSampleRate(22050); + if (audioDevice.isFormatSupported(format)) + testFormats.append(format); + + // PCM 22050 stereo S16LE + format.setChannelCount(2); + if (audioDevice.isFormatSupported(format)) + testFormats.append(format); + + // PCM 44100 stereo S16LE + format.setSampleRate(44100); + if (audioDevice.isFormatSupported(format)) +#ifdef Q_OS_ANDROID + // Testset crash on emulator x86 with API 23 (Android 6) for 44,1 MHz. + // It is not happen on x86 with API 24. What is more, there is no crash when + // tested sample rate is 44,999 or any other value. Seems like problem on + // emulator side. Let's turn off this frequency for API 23 + if (QNativeInterface::QAndroidApplication::sdkVersion() > __ANDROID_API_M__) +#endif + testFormats.append(format); + + // PCM 48000 stereo S16LE + format.setSampleRate(48000); + if (audioDevice.isFormatSupported(format)) + testFormats.append(format); + + QVERIFY(testFormats.size()); + + const QChar slash = QLatin1Char('/'); + QString temporaryPattern = QDir::tempPath(); + if (!temporaryPattern.endsWith(slash)) + temporaryPattern += slash; + temporaryPattern += "tst_qaudiooutputXXXXXX"; + m_temporaryDir.reset(new QTemporaryDir(temporaryPattern)); + m_temporaryDir->setAutoRemove(true); + QVERIFY(m_temporaryDir->isValid()); + + const QString temporaryAudioPath = m_temporaryDir->path() + slash; + for (const QAudioFormat &format : std::as_const(testFormats)) { + qint64 len = format.sampleRate()*format.bytesPerFrame(); // 1 second + createSineWaveData(format, len); + // Write generate sine wave data to file + const QString fileName = temporaryAudioPath + QStringLiteral("generated") + + formatToFileName(format) + QStringLiteral(".wav"); + FilePtr file(new QFile(fileName)); + QVERIFY2(file->open(QIODevice::WriteOnly), qPrintable(file->errorString())); + QWaveDecoder waveDecoder(file.data(), format); + if (waveDecoder.open(QIODevice::WriteOnly)) { + waveDecoder.write(m_byteArray->data(), len); + waveDecoder.close(); + } + file->close(); + audioFiles.append(file); + } +} + +void tst_QAudioSink::format() +{ + QAudioSink audioOutput(audioDevice.preferredFormat(), this); + + QAudioFormat requested = audioDevice.preferredFormat(); + QAudioFormat actual = audioOutput.format(); + + QVERIFY2((requested.channelCount() == actual.channelCount()), + QStringLiteral("channels: requested=%1, actual=%2") + .arg(requested.channelCount()) + .arg(actual.channelCount()) + .toUtf8() + .constData()); + QVERIFY2((requested.sampleRate() == actual.sampleRate()), + QStringLiteral("sampleRate: requested=%1, actual=%2") + .arg(requested.sampleRate()) + .arg(actual.sampleRate()) + .toUtf8() + .constData()); + QVERIFY2((requested.sampleFormat() == actual.sampleFormat()), + QStringLiteral("sampleFormat: requested=%1, actual=%2") + .arg((ushort)requested.sampleFormat()) + .arg((ushort)actual.sampleFormat()) + .toUtf8() + .constData()); + QVERIFY(requested == actual); +} + +void tst_QAudioSink::invalidFormat_data() +{ + QTest::addColumn<QAudioFormat>("invalidFormat"); + + QAudioFormat format; + + QTest::newRow("Null Format") + << format; + + format = audioDevice.preferredFormat(); + format.setChannelCount(0); + QTest::newRow("Channel count 0") + << format; + + format = audioDevice.preferredFormat(); + format.setSampleRate(0); + QTest::newRow("Sample rate 0") + << format; + + format = audioDevice.preferredFormat(); + format.setSampleFormat(QAudioFormat::Unknown); + QTest::newRow("Sample size 0") + << format; +} + +void tst_QAudioSink::invalidFormat() +{ + QFETCH(QAudioFormat, invalidFormat); + + QVERIFY2(!audioDevice.isFormatSupported(invalidFormat), + "isFormatSupported() is returning true on an invalid format"); + + QAudioSink audioOutput(invalidFormat, this); + + // Check that we are in the default state before calling start + QVERIFY2((audioOutput.state() == QAudio::StoppedState), "state() was not set to StoppedState before start()"); + QVERIFY2((audioOutput.error() == QAudio::NoError), "error() was not set to QAudio::NoError before start()"); + + audioOutput.start(); + // Check that error is raised + QTRY_VERIFY2((audioOutput.error() == QAudio::OpenError),"error() was not set to QAudio::OpenError after start()"); +} + +void tst_QAudioSink::bufferSize_data() +{ + QTest::addColumn<int>("bufferSize"); + QTest::newRow("Buffer size 512") << 512; + QTest::newRow("Buffer size 4096") << 4096; + QTest::newRow("Buffer size 8192") << 8192; +} + +void tst_QAudioSink::bufferSize() +{ + QFETCH(int, bufferSize); + QAudioSink audioOutput(audioDevice.preferredFormat(), this); + + QVERIFY2((audioOutput.error() == QAudio::NoError), + QStringLiteral("error() was not set to QAudio::NoError on creation(%1)") + .arg(bufferSize) + .toUtf8() + .constData()); + + audioOutput.setBufferSize(bufferSize); + QVERIFY2((audioOutput.error() == QAudio::NoError), "error() is not QAudio::NoError after setBufferSize"); + QVERIFY2((audioOutput.bufferSize() == bufferSize), + QStringLiteral("bufferSize: requested=%1, actual=%2") + .arg(bufferSize) + .arg(audioOutput.bufferSize()) + .toUtf8() + .constData()); +} + +void tst_QAudioSink::stopWhileStopped() +{ + // Calls QAudioSink::stop() when object is already in StoppedState + // Checks that + // - No state change occurs + // - No error is raised (QAudioSink::error() returns QAudio::NoError) + + QAudioSink audioOutput(audioDevice.preferredFormat(), this); + + QVERIFY2((audioOutput.state() == QAudio::StoppedState), "state() was not set to StoppedState before start()"); + QVERIFY2((audioOutput.error() == QAudio::NoError), "error() was not set to QAudio::NoError before start()"); + + QSignalSpy stateSignal(&audioOutput, &QAudioSink::stateChanged); + audioOutput.stop(); + + // Check that no state transition occurred + QVERIFY2((stateSignal.size() == 0), "stop() while stopped is emitting a signal and it shouldn't"); + QVERIFY2((audioOutput.error() == QAudio::NoError), "error() was not set to QAudio::NoError after stop()"); +} + +void tst_QAudioSink::suspendWhileStopped() +{ + // Calls QAudioSink::suspend() when object is already in StoppedState + // Checks that + // - No state change occurs + // - No error is raised (QAudioSink::error() returns QAudio::NoError) + + QAudioSink audioOutput(audioDevice.preferredFormat(), this); + + QVERIFY2((audioOutput.state() == QAudio::StoppedState), "state() was not set to StoppedState before start()"); + QVERIFY2((audioOutput.error() == QAudio::NoError), "error() was not set to QAudio::NoError before start()"); + + QSignalSpy stateSignal(&audioOutput, &QAudioSink::stateChanged); + audioOutput.suspend(); + + // Check that no state transition occurred + QVERIFY2((stateSignal.size() == 0), "stop() while suspended is emitting a signal and it shouldn't"); + QVERIFY2((audioOutput.error() == QAudio::NoError), "error() was not set to QAudio::NoError after stop()"); +} + +void tst_QAudioSink::resumeWhileStopped() +{ + // Calls QAudioSink::resume() when object is already in StoppedState + // Checks that + // - No state change occurs + // - No error is raised (QAudioSink::error() returns QAudio::NoError) + + QAudioSink audioOutput(audioDevice.preferredFormat(), this); + + QVERIFY2((audioOutput.state() == QAudio::StoppedState), "state() was not set to StoppedState before start()"); + QVERIFY2((audioOutput.error() == QAudio::NoError), "error() was not set to QAudio::NoError before start()"); + + QSignalSpy stateSignal(&audioOutput, &QAudioSink::stateChanged); + audioOutput.resume(); + + // Check that no state transition occurred + QVERIFY2((stateSignal.size() == 0), "resume() while stopped is emitting a signal and it shouldn't"); + QVERIFY2((audioOutput.error() == QAudio::NoError), "error() was not set to QAudio::NoError after resume()"); +} + +void tst_QAudioSink::pull() +{ + QFETCH(FilePtr, audioFile); + QFETCH(QAudioFormat, audioFormat); + + QAudioSink audioOutput(audioFormat, this); + + audioOutput.setVolume(0.1f); + + QSignalSpy stateSignal(&audioOutput, &QAudioSink::stateChanged); + + // Check that we are in the default state before calling start + QVERIFY2((audioOutput.state() == QAudio::StoppedState), "state() was not set to StoppedState before start()"); + QVERIFY2((audioOutput.error() == QAudio::NoError), "error() was not set to QAudio::NoError before start()"); + QVERIFY2((audioOutput.elapsedUSecs() == qint64(0)),"elapsedUSecs() not zero on creation"); + + audioFile->close(); + audioFile->open(QIODevice::ReadOnly); + audioFile->seek(QWaveDecoder::headerLength()); + + audioOutput.start(audioFile.data()); + + // Check that QAudioSink immediately transitions to ActiveState + QTRY_VERIFY2((stateSignal.size() == 1), + QStringLiteral("didn't emit signal on start(), got %1 signals instead") + .arg(dumpStateSignalSpy(stateSignal)) + .toUtf8() + .constData()); + QVERIFY2((audioOutput.state() == QAudio::ActiveState), "didn't transition to ActiveState after start()"); + QVERIFY2((audioOutput.error() == QAudio::NoError), "error state is not equal to QAudio::NoError after start()"); + stateSignal.clear(); + + // Check that 'elapsed' increases + QTest::qWait(40); + QVERIFY2((audioOutput.elapsedUSecs() > 0), "elapsedUSecs() is still zero after start()"); + + // Wait until playback finishes + QTRY_VERIFY2(audioFile->atEnd(), "didn't play to EOF"); + QTRY_VERIFY(stateSignal.size() > 0); + QTRY_COMPARE(qvariant_cast<QAudio::State>(stateSignal.last().at(0)), QAudio::IdleState); + QVERIFY2((audioOutput.state() == QAudio::IdleState), "didn't transitions to IdleState when at EOF"); + stateSignal.clear(); + + QTRY_COMPARE(audioOutput.processedUSecs(), 1000000); + + audioOutput.stop(); + QTest::qWait(40); + QVERIFY2((stateSignal.size() == 1), + QStringLiteral("didn't emit StoppedState signal after stop(), got %1 signals instead") + .arg(dumpStateSignalSpy(stateSignal)) + .toUtf8() + .constData()); + QVERIFY2((audioOutput.state() == QAudio::StoppedState), "didn't transitions to StoppedState after stop()"); + + QVERIFY2((audioOutput.error() == QAudio::NoError), "error() is not QAudio::NoError after stop()"); + QVERIFY2((audioOutput.elapsedUSecs() == (qint64)0), "elapsedUSecs() not equal to zero in StoppedState"); + + audioFile->close(); +} + +void tst_QAudioSink::pullSuspendResume() +{ + QFETCH(FilePtr, audioFile); + QFETCH(QAudioFormat, audioFormat); + QAudioSink audioOutput(audioFormat, this); + + audioOutput.setVolume(0.1f); + + QSignalSpy stateSignal(&audioOutput, &QAudioSink::stateChanged); + + // Check that we are in the default state before calling start + QVERIFY2((audioOutput.state() == QAudio::StoppedState), "state() was not set to StoppedState before start()"); + QVERIFY2((audioOutput.error() == QAudio::NoError), "error() was not set to QAudio::NoError before start()"); + QVERIFY2((audioOutput.elapsedUSecs() == qint64(0)),"elapsedUSecs() not zero on creation"); + + audioFile->close(); + audioFile->open(QIODevice::ReadOnly); + audioFile->seek(QWaveDecoder::headerLength()); + + audioOutput.start(audioFile.data()); + // Check that QAudioSink immediately transitions to ActiveState + QTRY_VERIFY2((stateSignal.size() == 1), + QStringLiteral("didn't emit signal on start(), got %1 signals instead") + .arg(dumpStateSignalSpy(stateSignal)) + .toUtf8() + .constData()); + QVERIFY2((audioOutput.state() == QAudio::ActiveState), "didn't transition to ActiveState after start()"); + QVERIFY2((audioOutput.error() == QAudio::NoError), + "error state is not equal to QAudio::NoError after start()"); + + stateSignal.clear(); + // Wait for half of clip to play + QTest::qWait(500); + + audioOutput.suspend(); + QTest::qWait(100); + + QTRY_VERIFY2( + (stateSignal.size() == 1), + QStringLiteral( + "didn't emit SuspendedState signal after suspend(), got %1 signals instead") + .arg(dumpStateSignalSpy(stateSignal)) + .toUtf8() + .constData()); + QVERIFY2((audioOutput.state() == QAudio::SuspendedState), "didn't transition to SuspendedState after suspend()"); + QVERIFY2((audioOutput.error() == QAudio::NoError), "error state is not equal to QAudio::NoError after suspend()"); + stateSignal.clear(); + + // Check that only 'elapsed', and not 'processed' increases while suspended + qint64 elapsedUs = audioOutput.elapsedUSecs(); + qint64 processedUs = audioOutput.processedUSecs(); + QTest::qWait(100); + QVERIFY(audioOutput.elapsedUSecs() > elapsedUs); + QVERIFY(audioOutput.processedUSecs() == processedUs); + + audioOutput.resume(); + + // Check that QAudioSink immediately transitions to ActiveState + QVERIFY2((stateSignal.size() == 1), + QStringLiteral("didn't emit signal after resume(), got %1 signals instead") + .arg(dumpStateSignalSpy(stateSignal)) + .toUtf8() + .constData()); + QVERIFY2((audioOutput.state() == QAudio::ActiveState), "didn't transition to ActiveState after resume()"); + QVERIFY2((audioOutput.error() == QAudio::NoError), "error state is not equal to QAudio::NoError after resume()"); + stateSignal.clear(); + + // Wait until playback finishes + QTRY_VERIFY2(audioFile->atEnd(), "didn't play to EOF"); + QTRY_VERIFY(stateSignal.size() > 0); + QTRY_COMPARE(qvariant_cast<QAudio::State>(stateSignal.last().at(0)), QAudio::IdleState); + QVERIFY2((audioOutput.state() == QAudio::IdleState), "didn't transitions to IdleState when at EOF"); + stateSignal.clear(); + + QTRY_COMPARE(audioOutput.processedUSecs(), 1000000); + + audioOutput.stop(); + QTest::qWait(40); + QVERIFY2((stateSignal.size() == 1), + QStringLiteral("didn't emit StoppedState signal after stop(), got %1 signals instead") + .arg(dumpStateSignalSpy(stateSignal)) + .toUtf8() + .constData()); + QVERIFY2((audioOutput.state() == QAudio::StoppedState), "didn't transitions to StoppedState after stop()"); + + QVERIFY2((audioOutput.error() == QAudio::NoError), "error() is not QAudio::NoError after stop()"); + QVERIFY2((audioOutput.elapsedUSecs() == (qint64)0), "elapsedUSecs() not equal to zero in StoppedState"); + + audioFile->close(); +} + +void tst_QAudioSink::pullResumeFromUnderrun() +{ + class AudioPullSource : public QIODevice + { + public: + qint64 readData(char *data, qint64 len) override { + qint64 read = qMin(len, available); + available -= read; + memset(data, 0, read); + return read; + } + qint64 writeData(const char *, qint64) override { return 0; } + bool isSequential() const override { return true; } + + qint64 bytesAvailable() const override { return available; } + bool atEnd() const override { return signalEnd && available == 0; } + + qint64 available = 0; + bool signalEnd = false; + }; + + constexpr int chunkSize = 128; + + QAudioFormat format; + format.setChannelCount(1); + format.setSampleFormat(QAudioFormat::UInt8); + format.setSampleRate(8000); + + AudioPullSource audioSource; + QAudioSink audioOutput(format, this); + QSignalSpy stateSignal(&audioOutput, &QAudioSink::stateChanged); + + audioSource.open(QIODeviceBase::ReadOnly); + audioSource.available = chunkSize; + QCOMPARE(audioOutput.state(), QAudio::StoppedState); + audioOutput.start(&audioSource); + + QTRY_COMPARE(stateSignal.size(), 1); + QCOMPARE(audioOutput.state(), QAudio::ActiveState); + QCOMPARE(audioOutput.error(), QAudio::NoError); + stateSignal.clear(); + + QTRY_COMPARE(stateSignal.size(), 1); + QCOMPARE(audioOutput.state(), QAudio::IdleState); + QCOMPARE(audioOutput.error(), QAudio::UnderrunError); + stateSignal.clear(); + + QTest::qWait(300); + audioSource.available = chunkSize; + audioSource.signalEnd = true; + + // Resume pull + emit audioSource.readyRead(); + + QTRY_COMPARE(stateSignal.size(), 2); + QCOMPARE(stateSignal.at(0).front().value<QAudio::State>(), QAudio::ActiveState); + QCOMPARE(stateSignal.at(1).front().value<QAudio::State>(), QAudio::IdleState); + + QCOMPARE(audioOutput.error(), QAudio::NoError); + QCOMPARE(audioOutput.state(), QAudio::IdleState); + + // we played two chunks, sample rate is per second + const int expectedUSecs = (double(chunkSize) / double(format.sampleRate())) + * 2 * 1000 * 1000; + QTRY_COMPARE(audioOutput.processedUSecs(), expectedUSecs); +} + +void tst_QAudioSink::push() +{ + QFETCH(FilePtr, audioFile); + QFETCH(QAudioFormat, audioFormat); + + QAudioSink audioOutput(audioFormat, this); + + audioOutput.setVolume(0.1f); + + QSignalSpy stateSignal(&audioOutput, &QAudioSink::stateChanged); + + // Check that we are in the default state before calling start + QVERIFY2((audioOutput.state() == QAudio::StoppedState), "state() was not set to StoppedState before start()"); + QVERIFY2((audioOutput.error() == QAudio::NoError), "error() was not set to QAudio::NoError before start()"); + QVERIFY2((audioOutput.elapsedUSecs() == qint64(0)),"elapsedUSecs() not zero on creation"); + + audioFile->close(); + audioFile->open(QIODevice::ReadOnly); + audioFile->seek(QWaveDecoder::headerLength()); + + QIODevice* feed = audioOutput.start(); + + // Check that QAudioSink immediately transitions to IdleState + QTRY_VERIFY2((stateSignal.size() == 1), + QStringLiteral("didn't emit signal on start(), got %1 signals instead") + .arg(dumpStateSignalSpy(stateSignal)) + .toUtf8() + .constData()); + QVERIFY2((audioOutput.state() == QAudio::IdleState), "didn't transition to IdleState after start()"); + QVERIFY2((audioOutput.error() == QAudio::NoError), "error state is not equal to QAudio::NoError after start()"); + stateSignal.clear(); + + // Check that 'elapsed' increases + QTest::qWait(40); + QVERIFY2((audioOutput.elapsedUSecs() > 0), "elapsedUSecs() is still zero after start()"); + QVERIFY2((audioOutput.processedUSecs() == qint64(0)), "processedUSecs() is not zero after start()"); + + qint64 written = 0; + + auto checker = [&]() { + // Check for transition to ActiveState when data is provided + QVERIFY2((stateSignal.size() == 1), + QStringLiteral("didn't emit signal after receiving data, got %1 signals instead") + .arg(dumpStateSignalSpy(stateSignal)) + .toUtf8() + .constData()); + QVERIFY2((audioOutput.state() == QAudio::ActiveState), + "didn't transition to ActiveState after receiving data"); + QVERIFY2((audioOutput.error() == QAudio::NoError), + "error state is not equal to QAudio::NoError after receiving data"); + stateSignal.clear(); + }; + + pushDataToAudioSink(audioOutput, *audioFile, *feed, written, wavDataSize(*audioFile), checker, + true); + + // Wait until playback finishes + QVERIFY2(audioFile->atEnd(), "didn't play to EOF"); + QTRY_VERIFY(audioOutput.state() == QAudio::IdleState); + QTRY_VERIFY(stateSignal.size() > 0); + QTRY_COMPARE(qvariant_cast<QAudio::State>(stateSignal.last().at(0)), QAudio::IdleState); + QVERIFY2((audioOutput.state() == QAudio::IdleState), "didn't transitions to IdleState when at EOF"); + stateSignal.clear(); + + QTRY_COMPARE(audioOutput.processedUSecs(), 1000000); + + audioOutput.stop(); + QTest::qWait(40); + QVERIFY2((stateSignal.size() == 1), + QStringLiteral("didn't emit StoppedState signal after stop(), got %1 signals instead") + .arg(dumpStateSignalSpy(stateSignal)) + .toUtf8() + .constData()); + QVERIFY2((audioOutput.state() == QAudio::StoppedState), "didn't transitions to StoppedState after stop()"); + + QVERIFY2((audioOutput.error() == QAudio::NoError), "error() is not QAudio::NoError after stop()"); + QVERIFY2((audioOutput.elapsedUSecs() == (qint64)0), "elapsedUSecs() not equal to zero in StoppedState"); + + audioFile->close(); +} + +void tst_QAudioSink::pushSuspendResume() +{ + QFETCH(FilePtr, audioFile); + QFETCH(QAudioFormat, audioFormat); + + QAudioSink audioOutput(audioFormat, this); + + audioOutput.setVolume(0.1f); + + QSignalSpy stateSignal(&audioOutput, &QAudioSink::stateChanged); + + // Check that we are in the default state before calling start + QVERIFY2((audioOutput.state() == QAudio::StoppedState), "state() was not set to StoppedState before start()"); + QVERIFY2((audioOutput.error() == QAudio::NoError), "error() was not set to QAudio::NoError before start()"); + QVERIFY2((audioOutput.elapsedUSecs() == qint64(0)),"elapsedUSecs() not zero on creation"); + + audioFile->close(); + audioFile->open(QIODevice::ReadOnly); + audioFile->seek(QWaveDecoder::headerLength()); + + QIODevice* feed = audioOutput.start(); + + // Check that QAudioSink immediately transitions to IdleState + QTRY_VERIFY2((stateSignal.size() == 1), + QStringLiteral("didn't emit signal on start(), got %1 signals instead") + .arg(dumpStateSignalSpy(stateSignal)) + .toUtf8() + .constData()); + QVERIFY2((audioOutput.state() == QAudio::IdleState), "didn't transition to IdleState after start()"); + QVERIFY2((audioOutput.error() == QAudio::NoError), "error state is not equal to QAudio::NoError after start()"); + stateSignal.clear(); + + // Check that 'elapsed' increases + QTest::qWait(40); + QVERIFY2((audioOutput.elapsedUSecs() > 0), "elapsedUSecs() is still zero after start()"); + QVERIFY2((audioOutput.processedUSecs() == qint64(0)), "processedUSecs() is not zero after start()"); + + auto firstHalfChecker = [&]() { + QVERIFY2((stateSignal.size() == 1), + QStringLiteral("didn't emit signal after receiving data, got %1 signals instead") + .arg(dumpStateSignalSpy(stateSignal)) + .toUtf8() + .constData()); + QVERIFY2((audioOutput.state() == QAudio::ActiveState), + "didn't transition to ActiveState after receiving data"); + QVERIFY2((audioOutput.error() == QAudio::NoError), + "error state is not equal to QAudio::NoError after receiving data"); + }; + + qint64 written = 0; + // Play half of the clip + pushDataToAudioSink(audioOutput, *audioFile, *feed, written, wavDataSize(*audioFile) / 2, + firstHalfChecker, true); + + stateSignal.clear(); + + const auto suspendedInState = audioOutput.state(); + audioOutput.suspend(); + + QTRY_VERIFY2( + (stateSignal.size() == 1), + QStringLiteral( + "didn't emit SuspendedState signal after suspend(), got %1 signals instead") + .arg(dumpStateSignalSpy(stateSignal)) + .toUtf8() + .constData()); + QVERIFY2((audioOutput.state() == QAudio::SuspendedState), "didn't transition to SuspendedState after suspend()"); + QVERIFY2((audioOutput.error() == QAudio::NoError), "error state is not equal to QAudio::NoError after suspend()"); + stateSignal.clear(); + + // Check that only 'elapsed', and not 'processed' increases while suspended + qint64 elapsedUs = audioOutput.elapsedUSecs(); + qint64 processedUs = audioOutput.processedUSecs(); + QTest::qWait(100); + QVERIFY(audioOutput.elapsedUSecs() > elapsedUs); + QVERIFY(audioOutput.processedUSecs() == processedUs); + + audioOutput.resume(); + + // Give backends running in separate threads a chance to resume + // but not too much or the rest of the file may be processed + QTest::qWait(20); + + // Check that QAudioSink immediately transitions to IdleState + QVERIFY2((stateSignal.size() == 1), + QStringLiteral("didn't emit signal after resume(), got %1 signals instead") + .arg(dumpStateSignalSpy(stateSignal)) + .toUtf8() + .constData()); + QVERIFY2((audioOutput.state() == suspendedInState), "resume() didn't transition to state before suspend()"); + QVERIFY2((audioOutput.error() == QAudio::NoError), "error state is not equal to QAudio::NoError after resume()"); + stateSignal.clear(); + + // Play rest of the clip + + auto restChecker = [&]() { + QVERIFY2((audioOutput.state() == QAudio::ActiveState), + "didn't transition to ActiveState after writing audio data"); + }; + + pushDataToAudioSink(audioOutput, *audioFile, *feed, written, -1, restChecker); + + QVERIFY(audioOutput.state() != QAudio::IdleState); + stateSignal.clear(); + + QVERIFY2(audioFile->atEnd(), "didn't play to EOF"); + QTRY_VERIFY(stateSignal.size() > 0); + QTRY_COMPARE(qvariant_cast<QAudio::State>(stateSignal.last().at(0)), QAudio::IdleState); + QVERIFY2((audioOutput.state() == QAudio::IdleState), "didn't transitions to IdleState when at EOF"); + stateSignal.clear(); + + QTRY_COMPARE(audioOutput.processedUSecs(), 1000000); + + audioOutput.stop(); + QTest::qWait(40); + QVERIFY2((stateSignal.size() == 1), + QStringLiteral("didn't emit StoppedState signal after stop(), got %1 signals instead") + .arg(dumpStateSignalSpy(stateSignal)) + .toUtf8() + .constData()); + QVERIFY2((audioOutput.state() == QAudio::StoppedState), "didn't transitions to StoppedState after stop()"); + + QVERIFY2((audioOutput.error() == QAudio::NoError), "error() is not QAudio::NoError after stop()"); + QVERIFY2((audioOutput.elapsedUSecs() == (qint64)0), "elapsedUSecs() not equal to zero in StoppedState"); + + audioFile->close(); +} + +void tst_QAudioSink::pushResetResume() +{ + auto audioFile = audioFiles.at(0); + auto audioFormat = testFormats.at(0); + + QAudioSink audioOutput(audioFormat, this); + + audioOutput.setBufferSize(8192); + audioOutput.setVolume(0.1f); + + audioFile->close(); + audioFile->open(QIODevice::ReadOnly); + audioFile->seek(QWaveDecoder::headerLength()); + + QPointer<QIODevice> feed = audioOutput.start(); + + QTest::qWait(20); + + auto buffer = audioFile->read(audioOutput.bytesFree()); + feed->write(buffer); + + QTest::qWait(20); + QTRY_COMPARE(audioOutput.state(), QAudio::ActiveState); + + audioOutput.reset(); + QCOMPARE(audioOutput.state(), QAudio::StoppedState); + QCOMPARE(audioOutput.error(), QAudio::NoError); + + const auto processedUSecs = audioOutput.processedUSecs(); + + audioOutput.resume(); + QTest::qWait(40); + + // Nothing changed if resume after reset + QCOMPARE(audioOutput.state(), QAudio::StoppedState); + QCOMPARE(audioOutput.error(), QAudio::NoError); + + QCOMPARE(audioOutput.processedUSecs(), processedUSecs); +} + +void tst_QAudioSink::pushUnderrun() +{ + QFETCH(FilePtr, audioFile); + QFETCH(QAudioFormat, audioFormat); + + QAudioSink audioOutput(audioFormat, this); + + audioOutput.setVolume(0.1f); + + QSignalSpy stateSignal(&audioOutput, &QAudioSink::stateChanged); + + // Check that we are in the default state before calling start + QVERIFY2((audioOutput.state() == QAudio::StoppedState), "state() was not set to StoppedState before start()"); + QVERIFY2((audioOutput.error() == QAudio::NoError), "error() was not set to QAudio::NoError before start()"); + QVERIFY2((audioOutput.elapsedUSecs() == qint64(0)),"elapsedUSecs() not zero on creation"); + + audioFile->close(); + audioFile->open(QIODevice::ReadOnly); + audioFile->seek(QWaveDecoder::headerLength()); + + QIODevice* feed = audioOutput.start(); + + // Check that QAudioSink immediately transitions to IdleState + QTRY_VERIFY2((stateSignal.size() == 1), + QStringLiteral("didn't emit signal on start(), got %1 signals instead") + .arg(dumpStateSignalSpy(stateSignal)) + .toUtf8() + .constData()); + QVERIFY2((audioOutput.state() == QAudio::IdleState), "didn't transition to IdleState after start()"); + QVERIFY2((audioOutput.error() == QAudio::NoError), "error state is not equal to QAudio::NoError after start()"); + stateSignal.clear(); + + // Check that 'elapsed' increases + QTest::qWait(40); + QVERIFY2((audioOutput.elapsedUSecs() > 0), "elapsedUSecs() is still zero after start()"); + QVERIFY2((audioOutput.processedUSecs() == qint64(0)), "processedUSecs() is not zero after start()"); + + qint64 written = 0; + + // Play half of the clip + + auto firstHalfChecker = [&]() { + QVERIFY2((stateSignal.size() == 1), + QStringLiteral("didn't emit signal after receiving data, got %1 signals instead") + .arg(dumpStateSignalSpy(stateSignal)) + .toUtf8() + .constData()); + QVERIFY2((audioOutput.state() == QAudio::ActiveState), + "didn't transition to ActiveState after receiving data"); + QVERIFY2((audioOutput.error() == QAudio::NoError), + "error state is not equal to QAudio::NoError after receiving data"); + }; + + pushDataToAudioSink(audioOutput, *audioFile, *feed, written, wavDataSize(*audioFile) / 2, + firstHalfChecker, true); + + stateSignal.clear(); + + // Wait for data to be played + QTest::qWait(700); + + QVERIFY2((stateSignal.size() == 1), + QStringLiteral("didn't emit IdleState signal after suspend(), got %1 signals instead") + .arg(dumpStateSignalSpy(stateSignal)) + .toUtf8() + .constData()); + QVERIFY2((audioOutput.state() == QAudio::IdleState), "didn't transition to IdleState, no data"); + QVERIFY2((audioOutput.error() == QAudio::UnderrunError), "error state is not equal to QAudio::UnderrunError, no data"); + stateSignal.clear(); + + // Play rest of the clip + auto restChecker = [&]() { + QVERIFY2((stateSignal.size() == 1), + QStringLiteral("didn't emit signal after receiving data, got %1 signals instead") + .arg(dumpStateSignalSpy(stateSignal)) + .toUtf8() + .constData()); + QVERIFY2((audioOutput.state() == QAudio::ActiveState), + "didn't transition to ActiveState after receiving data"); + QVERIFY2((audioOutput.error() == QAudio::NoError), + "error state is not equal to QAudio::NoError after receiving data"); + }; + pushDataToAudioSink(audioOutput, *audioFile, *feed, written, -1, restChecker, true); + + stateSignal.clear(); + + // Wait until playback finishes + QVERIFY2(audioFile->atEnd(), "didn't play to EOF"); + QTRY_VERIFY2((stateSignal.size() == 1), + QStringLiteral("didn't emit IdleState signal when at EOF, got %1 signals instead") + .arg(dumpStateSignalSpy(stateSignal)) + .toUtf8() + .constData()); + QVERIFY2((audioOutput.state() == QAudio::IdleState), "didn't transitions to IdleState when at EOF"); + stateSignal.clear(); + + QTRY_COMPARE(audioOutput.processedUSecs(), 1000000); + + audioOutput.stop(); + QTest::qWait(40); + QVERIFY2((stateSignal.size() == 1), + QStringLiteral("didn't emit StoppedState signal after stop(), got %1 signals instead") + .arg(dumpStateSignalSpy(stateSignal)) + .toUtf8() + .constData()); + QVERIFY2((audioOutput.state() == QAudio::StoppedState), "didn't transitions to StoppedState after stop()"); + + QVERIFY2((audioOutput.error() == QAudio::NoError), "error() is not QAudio::NoError after stop()"); + QVERIFY2((audioOutput.elapsedUSecs() == (qint64)0), "elapsedUSecs() not equal to zero in StoppedState"); + + audioFile->close(); +} + +void tst_QAudioSink::volume_data() +{ + QTest::addColumn<float>("actualFloat"); + QTest::addColumn<int>("expectedInt"); + QTest::newRow("Volume 0.3") << 0.3f << 3; + QTest::newRow("Volume 0.6") << 0.6f << 6; + QTest::newRow("Volume 0.9") << 0.9f << 9; +} + +void tst_QAudioSink::volume() +{ + QFETCH(float, actualFloat); + QFETCH(int, expectedInt); + QAudioSink audioOutput(audioDevice.preferredFormat(), this); + + audioOutput.setVolume(actualFloat); + QTRY_VERIFY(qRound(audioOutput.volume()*10.0f) == expectedInt); + // Wait a while to see if this changes + QTest::qWait(500); + QTRY_VERIFY(qRound(audioOutput.volume()*10.0f) == expectedInt); +} + +QTEST_MAIN(tst_QAudioSink) + +#include "tst_qaudiosink.moc" diff --git a/tests/auto/integration/qaudiosource/CMakeLists.txt b/tests/auto/integration/qaudiosource/CMakeLists.txt new file mode 100644 index 000000000..0e572e804 --- /dev/null +++ b/tests/auto/integration/qaudiosource/CMakeLists.txt @@ -0,0 +1,14 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +##################################################################### +## tst_qaudiosource Test: +##################################################################### + +qt_internal_add_test(tst_qaudiosource + SOURCES + tst_qaudiosource.cpp + LIBRARIES + Qt::Gui + Qt::MultimediaPrivate +) diff --git a/tests/auto/integration/qaudiosource/tst_qaudiosource.cpp b/tests/auto/integration/qaudiosource/tst_qaudiosource.cpp new file mode 100644 index 000000000..ae100a08b --- /dev/null +++ b/tests/auto/integration/qaudiosource/tst_qaudiosource.cpp @@ -0,0 +1,817 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include <QtTest/QtTest> +#include <QtCore/qlocale.h> +#include <QtCore/QTemporaryDir> +#include <QtCore/QSharedPointer> +#include <QtCore/QScopedPointer> + +#include <qaudiosource.h> +#include <qaudiodevice.h> +#include <qaudioformat.h> +#include <qaudio.h> +#include <qmediadevices.h> + +#include <qwavedecoder.h> + +#define RANGE_ERR 0.5 + +template<typename T> inline bool qTolerantCompare(T value, T expected) +{ + return qAbs(value - expected) < (RANGE_ERR * expected); +} + +class tst_QAudioSource : public QObject +{ + Q_OBJECT +public: + tst_QAudioSource(QObject* parent=nullptr) : QObject(parent) {} + +private slots: + void initTestCase(); + + void format(); + void invalidFormat_data(); + void invalidFormat(); + + void bufferSize(); + + void stopWhileStopped(); + void suspendWhileStopped(); + void resumeWhileStopped(); + + void pull_data(){generate_audiofile_testrows();} + void pull(); + + void pullSuspendResume_data(){generate_audiofile_testrows();} + void pullSuspendResume(); + + void push_data(){generate_audiofile_testrows();} + void push(); + + void pushSuspendResume_data(){generate_audiofile_testrows();} + void pushSuspendResume(); + + void reset_data(){generate_audiofile_testrows();} + void reset(); + + void volume_data(){generate_audiofile_testrows();} + void volume(); + +private: + using FilePtr = QSharedPointer<QFile>; + + QString formatToFileName(const QAudioFormat &format); + + void generate_audiofile_testrows(); + + QAudioDevice audioDevice; + QList<QAudioFormat> testFormats; + QList<FilePtr> audioFiles; + QScopedPointer<QTemporaryDir> m_temporaryDir; + + QScopedPointer<QByteArray> m_byteArray; + QScopedPointer<QBuffer> m_buffer; + + bool m_inCISystem = false; +}; + +void tst_QAudioSource::generate_audiofile_testrows() +{ + QTest::addColumn<FilePtr>("audioFile"); + QTest::addColumn<QAudioFormat>("audioFormat"); + + for (int i=0; i<audioFiles.size(); i++) { + QTest::newRow(QStringLiteral("%1").arg(i).toUtf8().constData()) + << audioFiles.at(i) << testFormats.at(i); + + // Only run first format in CI system to reduce test times + if (m_inCISystem) + break; + } +} + +QString tst_QAudioSource::formatToFileName(const QAudioFormat &format) +{ + return QStringLiteral("%1_%2_%3") + .arg(format.sampleRate()) + .arg(format.bytesPerSample()) + .arg(format.channelCount()); +} + +void tst_QAudioSource::initTestCase() +{ +#ifdef Q_OS_ANDROID + // The test might fail because libOpenSLES cannot create AudioRecorder for that emulator. The + // Android documentation states that the emulator doesn't support this at all all + // https://developer.android.com/media/platform/mediarecorder. However, in practice this test + // fails only prior to Android 10. + if (QNativeInterface::QAndroidApplication::sdkVersion() < __ANDROID_API_Q__) + QSKIP("Emulated Android version doesn't support audio recording"); +#endif + + m_inCISystem = qEnvironmentVariable("QTEST_ENVIRONMENT").toLower() == "ci"; + + if (m_inCISystem) + QSKIP("SKIP initTestCase on CI. To be fixed"); + + // Only perform tests if audio input device exists + const QList<QAudioDevice> devices = QMediaDevices::audioInputs(); + + if (devices.size() <= 0) + QSKIP("No audio backend"); + + audioDevice = QMediaDevices::defaultAudioInput(); + + + QAudioFormat format; + format.setChannelCount(1); + + if (audioDevice.isFormatSupported(audioDevice.preferredFormat())) { + if (format.sampleFormat() == QAudioFormat::Int16) + testFormats.append(audioDevice.preferredFormat()); + } + + // PCM 11025 mono S16LE + format.setSampleRate(11025); + format.setSampleFormat(QAudioFormat::Int16); + if (audioDevice.isFormatSupported(format)) + testFormats.append(format); + + // PCM 22050 mono S16LE + format.setSampleRate(22050); + if (audioDevice.isFormatSupported(format)) + testFormats.append(format); + + // PCM 22050 stereo S16LE + format.setChannelCount(2); + if (audioDevice.isFormatSupported(format)) + testFormats.append(format); + + // PCM 44100 stereo S16LE + format.setSampleRate(44100); + if (audioDevice.isFormatSupported(format)) + testFormats.append(format); + + // PCM 48000 stereo S16LE + format.setSampleRate(48000); + if (audioDevice.isFormatSupported(format)) + testFormats.append(format); + + QVERIFY(testFormats.size()); + + const QChar slash = QLatin1Char('/'); + QString temporaryPattern = QDir::tempPath(); + if (!temporaryPattern.endsWith(slash)) + temporaryPattern += slash; + temporaryPattern += "tst_qaudioinputXXXXXX"; + m_temporaryDir.reset(new QTemporaryDir(temporaryPattern)); + m_temporaryDir->setAutoRemove(true); + QVERIFY(m_temporaryDir->isValid()); + + const QString temporaryAudioPath = m_temporaryDir->path() + slash; + for (const QAudioFormat &format : std::as_const(testFormats)) { + const QString fileName = temporaryAudioPath + formatToFileName(format) + QStringLiteral(".wav"); + audioFiles.append(FilePtr::create(fileName)); + } +} + +void tst_QAudioSource::format() +{ + QAudioSource audioInput(audioDevice.preferredFormat(), this); + + QAudioFormat requested = audioDevice.preferredFormat(); + QAudioFormat actual = audioInput.format(); + + QVERIFY2((requested.channelCount() == actual.channelCount()), + QStringLiteral("channels: requested=%1, actual=%2") + .arg(requested.channelCount()) + .arg(actual.channelCount()) + .toUtf8() + .constData()); + QVERIFY2((requested.sampleRate() == actual.sampleRate()), + QStringLiteral("sampleRate: requested=%1, actual=%2") + .arg(requested.sampleRate()) + .arg(actual.sampleRate()) + .toUtf8() + .constData()); + QVERIFY2((requested.sampleFormat() == actual.sampleFormat()), + QStringLiteral("sampleFormat: requested=%1, actual=%2") + .arg((ushort)requested.sampleFormat()) + .arg((ushort)actual.sampleFormat()) + .toUtf8() + .constData()); + QCOMPARE(actual, requested); +} + +void tst_QAudioSource::invalidFormat_data() +{ + QTest::addColumn<QAudioFormat>("invalidFormat"); + + QAudioFormat format; + + QTest::newRow("Null Format") + << format; + + format = audioDevice.preferredFormat(); + format.setChannelCount(0); + QTest::newRow("Channel count 0") + << format; + + format = audioDevice.preferredFormat(); + format.setSampleRate(0); + QTest::newRow("Sample rate 0") + << format; + + format = audioDevice.preferredFormat(); + format.setSampleFormat(QAudioFormat::Unknown); + QTest::newRow("Sample size 0") + << format; +} + +void tst_QAudioSource::invalidFormat() +{ + QFETCH(QAudioFormat, invalidFormat); + + QVERIFY2(!audioDevice.isFormatSupported(invalidFormat), + "isFormatSupported() is returning true on an invalid format"); + + QAudioSource audioInput(invalidFormat, this); + + // Check that we are in the default state before calling start + QVERIFY2((audioInput.state() == QAudio::StoppedState), "state() was not set to StoppedState before start()"); + QVERIFY2((audioInput.error() == QAudio::NoError), "error() was not set to QAudio::NoError before start()"); + + audioInput.start(); + + // Check that error is raised + QTRY_VERIFY2((audioInput.error() == QAudio::OpenError),"error() was not set to QAudio::OpenError after start()"); +} + +void tst_QAudioSource::bufferSize() +{ + QAudioSource audioInput(audioDevice.preferredFormat(), this); + + QVERIFY2((audioInput.error() == QAudio::NoError), "error() was not set to QAudio::NoError on creation"); + + audioInput.setBufferSize(512); + QVERIFY2((audioInput.error() == QAudio::NoError), "error() is not QAudio::NoError after setBufferSize(512)"); + QVERIFY2((audioInput.bufferSize() == 512), + QStringLiteral("bufferSize: requested=512, actual=%2") + .arg(audioInput.bufferSize()) + .toUtf8() + .constData()); + + audioInput.setBufferSize(4096); + QVERIFY2((audioInput.error() == QAudio::NoError), "error() is not QAudio::NoError after setBufferSize(4096)"); + QVERIFY2((audioInput.bufferSize() == 4096), + QStringLiteral("bufferSize: requested=4096, actual=%2") + .arg(audioInput.bufferSize()) + .toUtf8() + .constData()); + + audioInput.setBufferSize(8192); + QVERIFY2((audioInput.error() == QAudio::NoError), "error() is not QAudio::NoError after setBufferSize(8192)"); + QVERIFY2((audioInput.bufferSize() == 8192), + QStringLiteral("bufferSize: requested=8192, actual=%2") + .arg(audioInput.bufferSize()) + .toUtf8() + .constData()); +} + +void tst_QAudioSource::stopWhileStopped() +{ + // Calls QAudioSource::stop() when object is already in StoppedState + // Checks that + // - No state change occurs + // - No error is raised (QAudioSource::error() returns QAudio::NoError) + + QAudioSource audioInput(audioDevice.preferredFormat(), this); + + QVERIFY2((audioInput.state() == QAudio::StoppedState), "state() was not set to StoppedState before start()"); + QVERIFY2((audioInput.error() == QAudio::NoError), "error() was not set to QAudio::NoError before start()"); + + QSignalSpy stateSignal(&audioInput, &QAudioSource::stateChanged); + audioInput.stop(); + + // Check that no state transition occurred + QVERIFY2((stateSignal.size() == 0), "stop() while stopped is emitting a signal and it shouldn't"); + QVERIFY2((audioInput.error() == QAudio::NoError), "error() was not set to QAudio::NoError after stop()"); +} + +void tst_QAudioSource::suspendWhileStopped() +{ + // Calls QAudioSource::suspend() when object is already in StoppedState + // Checks that + // - No state change occurs + // - No error is raised (QAudioSource::error() returns QAudio::NoError) + + QAudioSource audioInput(audioDevice.preferredFormat(), this); + + QVERIFY2((audioInput.state() == QAudio::StoppedState), "state() was not set to StoppedState before start()"); + QVERIFY2((audioInput.error() == QAudio::NoError), "error() was not set to QAudio::NoError before start()"); + + QSignalSpy stateSignal(&audioInput, &QAudioSource::stateChanged); + audioInput.suspend(); + + // Check that no state transition occurred + QVERIFY2((stateSignal.size() == 0), "stop() while suspended is emitting a signal and it shouldn't"); + QVERIFY2((audioInput.error() == QAudio::NoError), "error() was not set to QAudio::NoError after stop()"); +} + +void tst_QAudioSource::resumeWhileStopped() +{ + // Calls QAudioSource::resume() when object is already in StoppedState + // Checks that + // - No state change occurs + // - No error is raised (QAudioSource::error() returns QAudio::NoError) + + QAudioSource audioInput(audioDevice.preferredFormat(), this); + + QVERIFY2((audioInput.state() == QAudio::StoppedState), "state() was not set to StoppedState before start()"); + QVERIFY2((audioInput.error() == QAudio::NoError), "error() was not set to QAudio::NoError before start()"); + + QSignalSpy stateSignal(&audioInput, &QAudioSource::stateChanged); + audioInput.resume(); + + // Check that no state transition occurred + QVERIFY2((stateSignal.size() == 0), "resume() while stopped is emitting a signal and it shouldn't"); + QVERIFY2((audioInput.error() == QAudio::NoError), "error() was not set to QAudio::NoError after resume()"); +} + +void tst_QAudioSource::pull() +{ + QFETCH(FilePtr, audioFile); + QFETCH(QAudioFormat, audioFormat); + + QAudioSource audioInput(audioFormat, this); + + QSignalSpy stateSignal(&audioInput, &QAudioSource::stateChanged); + + // Check that we are in the default state before calling start + QVERIFY2((audioInput.state() == QAudio::StoppedState), "state() was not set to StoppedState before start()"); + QVERIFY2((audioInput.error() == QAudio::NoError), "error() was not set to QAudio::NoError before start()"); + QVERIFY2((audioInput.elapsedUSecs() == qint64(0)),"elapsedUSecs() not zero on creation"); + + audioFile->close(); + audioFile->open(QIODevice::WriteOnly); + QWaveDecoder waveDecoder(audioFile.data(), audioFormat); + if (!waveDecoder.open(QIODevice::WriteOnly)) { + waveDecoder.close(); + audioFile->close(); + QSKIP("Audio format not supported for writing to WAV file."); + } + QCOMPARE(waveDecoder.size(), QWaveDecoder::headerLength()); + + audioInput.start(audioFile.data()); + + // Check that QAudioSource immediately transitions to ActiveState or IdleState + QTRY_VERIFY2((stateSignal.size() > 0),"didn't emit signals on start()"); + QVERIFY2((audioInput.state() == QAudio::ActiveState || audioInput.state() == QAudio::IdleState), + "didn't transition to ActiveState or IdleState after start()"); + QVERIFY2((audioInput.error() == QAudio::NoError), "error state is not equal to QAudio::NoError after start()"); + stateSignal.clear(); + + // Check that 'elapsed' increases + QTRY_VERIFY2((audioInput.elapsedUSecs() > 0), "elapsedUSecs() is still zero after start()"); + QTRY_VERIFY2((audioInput.processedUSecs() > 0), "elapsedUSecs() is still zero after start()"); + + // Allow some recording to happen + QTest::qWait(3000); // 3 seconds should be plenty + + stateSignal.clear(); + + qint64 processedUs = audioInput.processedUSecs(); + QVERIFY2(qTolerantCompare(processedUs, 3000000LL), + QStringLiteral( + "processedUSecs() doesn't fall in acceptable range, should be 3000000 (%1)") + .arg(processedUs) + .toUtf8() + .constData()); + + audioInput.stop(); + QTRY_VERIFY2( + (stateSignal.size() == 1), + QStringLiteral("didn't emit StoppedState signal after stop(), got %1 signals instead") + .arg(stateSignal.size()) + .toUtf8() + .constData()); + QVERIFY2((audioInput.state() == QAudio::StoppedState), "didn't transitions to StoppedState after stop()"); + + QVERIFY2((audioInput.error() == QAudio::NoError), "error() is not QAudio::NoError after stop()"); + QVERIFY2((audioInput.elapsedUSecs() == (qint64)0), "elapsedUSecs() not equal to zero in StoppedState"); + + //QWaveHeader::writeDataLength(*audioFile, audioFile->pos() - WavHeader::headerLength()); + //waveDecoder.writeDataLength(); + waveDecoder.close(); + audioFile->close(); + +} + +void tst_QAudioSource::pullSuspendResume() +{ + QFETCH(FilePtr, audioFile); + QFETCH(QAudioFormat, audioFormat); + + QAudioSource audioInput(audioFormat, this); + + QSignalSpy stateSignal(&audioInput, &QAudioSource::stateChanged); + + // Check that we are in the default state before calling start + QVERIFY2((audioInput.state() == QAudio::StoppedState), "state() was not set to StoppedState before start()"); + QVERIFY2((audioInput.error() == QAudio::NoError), "error() was not set to QAudio::NoError before start()"); + QVERIFY2((audioInput.elapsedUSecs() == qint64(0)),"elapsedUSecs() not zero on creation"); + + audioFile->close(); + audioFile->open(QIODevice::WriteOnly); + QWaveDecoder waveDecoder(audioFile.get(), audioFormat); + if (!waveDecoder.open(QIODevice::WriteOnly)) { + waveDecoder.close(); + audioFile->close(); + QSKIP("Audio format not supported for writing to WAV file."); + } + QCOMPARE(waveDecoder.size(), QWaveDecoder::headerLength()); + + audioInput.start(audioFile.data()); + + // Check that QAudioSource immediately transitions to ActiveState or IdleState + QTRY_VERIFY2((stateSignal.size() > 0),"didn't emit signals on start()"); + QVERIFY2((audioInput.state() == QAudio::ActiveState || audioInput.state() == QAudio::IdleState), + "didn't transition to ActiveState or IdleState after start()"); + QVERIFY2((audioInput.error() == QAudio::NoError), "error state is not equal to QAudio::NoError after start()"); + stateSignal.clear(); + + // Check that 'elapsed' increases + QTRY_VERIFY2((audioInput.elapsedUSecs() > 0), "elapsedUSecs() is still zero after start()"); + QTRY_VERIFY2((audioInput.processedUSecs() > 0), "elapsedUSecs() is still zero after start()"); + + // Allow some recording to happen + QTest::qWait(3000); // 3 seconds should be plenty + + QVERIFY2((audioInput.state() == QAudio::ActiveState), + "didn't transition to ActiveState after some recording"); + QVERIFY2((audioInput.error() == QAudio::NoError), "error state is not equal to QAudio::NoError after some recording"); + + stateSignal.clear(); + + audioInput.suspend(); + + QTRY_VERIFY2( + (stateSignal.size() == 1), + QStringLiteral( + "didn't emit SuspendedState signal after suspend(), got %1 signals instead") + .arg(stateSignal.size()) + .toUtf8() + .constData()); + QVERIFY2((audioInput.state() == QAudio::SuspendedState), "didn't transitions to SuspendedState after stop()"); + QVERIFY2((audioInput.error() == QAudio::NoError), "error() is not QAudio::NoError after stop()"); + stateSignal.clear(); + + // Check that only 'elapsed', and not 'processed' increases while suspended + qint64 elapsedUs = audioInput.elapsedUSecs(); + qint64 processedUs = audioInput.processedUSecs(); + QVERIFY2(qTolerantCompare(processedUs, 3000000LL), + QStringLiteral( + "processedUSecs() doesn't fall in acceptable range, should be 3000000 (%1)") + .arg(processedUs) + .toUtf8() + .constData()); + QTRY_VERIFY(audioInput.elapsedUSecs() > elapsedUs); + QVERIFY(audioInput.processedUSecs() == processedUs); + + audioInput.resume(); + + // Check that QAudioSource immediately transitions to ActiveState + QTRY_VERIFY2((stateSignal.size() == 1), + QStringLiteral("didn't emit signal after resume(), got %1 signals instead") + .arg(stateSignal.size()) + .toUtf8() + .constData()); + QVERIFY2((audioInput.state() == QAudio::ActiveState), "didn't transition to ActiveState after resume()"); + QVERIFY2((audioInput.error() == QAudio::NoError), "error state is not equal to QAudio::NoError after resume()"); + stateSignal.clear(); + + audioInput.stop(); + QTest::qWait(40); + QTRY_VERIFY2( + (stateSignal.size() == 1), + QStringLiteral("didn't emit StoppedState signal after stop(), got %1 signals instead") + .arg(stateSignal.size()) + .toUtf8() + .constData()); + QVERIFY2((audioInput.state() == QAudio::StoppedState), "didn't transitions to StoppedState after stop()"); + + QVERIFY2((audioInput.error() == QAudio::NoError), "error() is not QAudio::NoError after stop()"); + QVERIFY2((audioInput.elapsedUSecs() == (qint64)0), "elapsedUSecs() not equal to zero in StoppedState"); + + //WavHeader::writeDataLength(*audioFile,audioFile->pos()-WavHeader::headerLength()); + //waveDecoder.writeDataLength(); + waveDecoder.close(); + audioFile->close(); +} + +void tst_QAudioSource::push() +{ + QFETCH(FilePtr, audioFile); + QFETCH(QAudioFormat, audioFormat); + + QAudioSource audioInput(audioFormat, this); + + QSignalSpy stateSignal(&audioInput, &QAudioSource::stateChanged); + + // Check that we are in the default state before calling start + QVERIFY2((audioInput.state() == QAudio::StoppedState), "state() was not set to StoppedState before start()"); + QVERIFY2((audioInput.error() == QAudio::NoError), "error() was not set to QAudio::NoError before start()"); + QVERIFY2((audioInput.elapsedUSecs() == qint64(0)),"elapsedUSecs() not zero on creation"); + + audioFile->close(); + audioFile->open(QIODevice::WriteOnly); + QWaveDecoder waveDecoder(audioFile.get(), audioFormat); + if (!waveDecoder.open(QIODevice::WriteOnly)) { + waveDecoder.close(); + audioFile->close(); + QSKIP("Audio format not supported for writing to WAV file."); + } + QCOMPARE(waveDecoder.size(), QWaveDecoder::headerLength()); + + // Set a large buffer to avoid underruns during QTest::qWaits + audioInput.setBufferSize(audioFormat.bytesForDuration(100000)); + + QIODevice* feed = audioInput.start(); + + // Check that QAudioSource immediately transitions to IdleState + QTRY_VERIFY2((stateSignal.size() == 1),"didn't emit IdleState signal on start()"); + QVERIFY2((audioInput.state() == QAudio::IdleState), + "didn't transition to IdleState after start()"); + QVERIFY2((audioInput.error() == QAudio::NoError), "error state is not equal to QAudio::NoError after start()"); + stateSignal.clear(); + + // Check that 'elapsed' increases + QTest::qWait(40); + QVERIFY2((audioInput.elapsedUSecs() > 0), "elapsedUSecs() is still zero after start()"); + + qint64 totalBytesRead = 0; + bool firstBuffer = true; + qint64 len = audioFormat.sampleRate()*audioFormat.bytesPerFrame()/2; // .5 seconds + while (totalBytesRead < len) { + QTRY_VERIFY_WITH_TIMEOUT(audioInput.bytesAvailable() > 0, 1000); + QByteArray buffer = feed->readAll(); + audioFile->write(buffer); + totalBytesRead += buffer.size(); + if (firstBuffer && buffer.size()) { + // Check for transition to ActiveState when data is provided + QTRY_VERIFY2((stateSignal.size() == 1),"didn't emit ActiveState signal on data"); + QVERIFY2((audioInput.state() == QAudio::ActiveState), + "didn't transition to ActiveState after data"); + QVERIFY2((audioInput.error() == QAudio::NoError), "error state is not equal to QAudio::NoError after start()"); + firstBuffer = false; + } + } + + stateSignal.clear(); + + qint64 processedUs = audioInput.processedUSecs(); + + audioInput.stop(); + QTRY_VERIFY2( + (stateSignal.size() == 1), + QStringLiteral("didn't emit StoppedState signal after stop(), got %1 signals instead") + .arg(stateSignal.size()) + .toUtf8() + .constData()); + QVERIFY2((audioInput.state() == QAudio::StoppedState), "didn't transitions to StoppedState after stop()"); + + QVERIFY2(qTolerantCompare(processedUs, 500000LL), + QStringLiteral( + "processedUSecs() doesn't fall in acceptable range, should be 500000 (%1)") + .arg(processedUs) + .toUtf8() + .constData()); + QVERIFY2((audioInput.error() == QAudio::NoError), "error() is not QAudio::NoError after stop()"); + QVERIFY2((audioInput.elapsedUSecs() == (qint64)0), "elapsedUSecs() not equal to zero in StoppedState"); + + //WavHeader::writeDataLength(*audioFile,audioFile->pos()-WavHeader::headerLength()); + //waveDecoder.writeDataLength(); + waveDecoder.close(); + audioFile->close(); +} + +void tst_QAudioSource::pushSuspendResume() +{ +#ifdef Q_OS_LINUX + if (m_inCISystem) + QSKIP("QTBUG-26504 Fails 20% of time with pulseaudio backend"); +#endif + QFETCH(FilePtr, audioFile); + QFETCH(QAudioFormat, audioFormat); + QAudioSource audioInput(audioFormat, this); + + audioInput.setBufferSize(audioFormat.bytesForDuration(100000)); + + QSignalSpy stateSignal(&audioInput, &QAudioSource::stateChanged); + + // Check that we are in the default state before calling start + QVERIFY2((audioInput.state() == QAudio::StoppedState), "state() was not set to StoppedState before start()"); + QVERIFY2((audioInput.error() == QAudio::NoError), "error() was not set to QAudio::NoError before start()"); + QVERIFY2((audioInput.elapsedUSecs() == qint64(0)),"elapsedUSecs() not zero on creation"); + + audioFile->close(); + audioFile->open(QIODevice::WriteOnly); + QWaveDecoder waveDecoder(audioFile.get(), audioFormat); + if (!waveDecoder.open(QIODevice::WriteOnly)) { + waveDecoder.close(); + audioFile->close(); + QSKIP("Audio format not supported for writing to WAV file."); + } + QCOMPARE(waveDecoder.size(), QWaveDecoder::headerLength()); + + QIODevice* feed = audioInput.start(); + + // Check that QAudioSource immediately transitions to IdleState + QTRY_VERIFY2((stateSignal.size() == 1),"didn't emit IdleState signal on start()"); + QVERIFY2((audioInput.state() == QAudio::IdleState), + "didn't transition to IdleState after start()"); + QVERIFY2((audioInput.error() == QAudio::NoError), "error state is not equal to QAudio::NoError after start()"); + stateSignal.clear(); + + // Check that 'elapsed' increases + QTRY_VERIFY2((audioInput.elapsedUSecs() > 0), "elapsedUSecs() is still zero after start()"); + + qint64 totalBytesRead = 0; + bool firstBuffer = true; + qint64 len = audioFormat.sampleRate() * audioFormat.bytesPerFrame() / 2; // .5 seconds + while (totalBytesRead < len) { + QTRY_VERIFY_WITH_TIMEOUT(audioInput.bytesAvailable() > 0, 1000); + auto buffer = feed->readAll(); + audioFile->write(buffer); + totalBytesRead += buffer.size(); + if (firstBuffer && buffer.size()) { + // Check for transition to ActiveState when data is provided + QTRY_VERIFY2((stateSignal.size() == 1),"didn't emit ActiveState signal on data"); + QVERIFY2((audioInput.state() == QAudio::ActiveState), + "didn't transition to ActiveState after data"); + QVERIFY2((audioInput.error() == QAudio::NoError), "error state is not equal to QAudio::NoError after start()"); + firstBuffer = false; + } + } + stateSignal.clear(); + + audioInput.suspend(); + + QTRY_VERIFY2( + (stateSignal.size() == 1), + QStringLiteral( + "didn't emit SuspendedState signal after suspend(), got %1 signals instead") + .arg(stateSignal.size()) + .toUtf8() + .constData()); + QVERIFY2((audioInput.state() == QAudio::SuspendedState), "didn't transitions to SuspendedState after stop()"); + QVERIFY2((audioInput.error() == QAudio::NoError), "error() is not QAudio::NoError after stop()"); + stateSignal.clear(); + + // Check that only 'elapsed', and not 'processed' increases while suspended + qint64 elapsedUs = audioInput.elapsedUSecs(); + qint64 processedUs = audioInput.processedUSecs(); + QTRY_VERIFY(audioInput.elapsedUSecs() > elapsedUs); + QVERIFY(audioInput.processedUSecs() == processedUs); + + // Drain any data, in case we run out of space when resuming + while (feed->readAll().size() > 0) + ; + QCOMPARE(audioInput.bytesAvailable(), 0); + + audioInput.resume(); + + // Check that QAudioSource immediately transitions to Active or IdleState + QTRY_VERIFY2((stateSignal.size() > 0),"didn't emit signals on resume()"); + QVERIFY2((audioInput.state() == QAudio::ActiveState || audioInput.state() == QAudio::IdleState), + "didn't transition to ActiveState or IdleState after resume()"); + QVERIFY2((audioInput.error() == QAudio::NoError), "error state is not equal to QAudio::NoError after resume()"); + + stateSignal.clear(); + + // Read another seconds worth + totalBytesRead = 0; + firstBuffer = true; + while (totalBytesRead < len && audioInput.state() != QAudio::StoppedState) { + QTRY_VERIFY(audioInput.bytesAvailable() > 0); + auto buffer = feed->readAll(); + audioFile->write(buffer); + totalBytesRead += buffer.size(); + } + stateSignal.clear(); + + processedUs = audioInput.processedUSecs(); + + audioInput.stop(); + QTRY_VERIFY2( + (stateSignal.size() == 1), + QStringLiteral("didn't emit StoppedState signal after stop(), got %1 signals instead") + .arg(stateSignal.size()) + .toUtf8() + .constData()); + QVERIFY2((audioInput.state() == QAudio::StoppedState), "didn't transitions to StoppedState after stop()"); + + QVERIFY2(qTolerantCompare(processedUs, 1000000LL), + QStringLiteral( + "processedUSecs() doesn't fall in acceptable range, should be 2040000 (%1)") + .arg(processedUs) + .toUtf8() + .constData()); + QVERIFY2((audioInput.elapsedUSecs() == (qint64)0), "elapsedUSecs() not equal to zero in StoppedState"); + + //WavHeader::writeDataLength(*audioFile,audioFile->pos()-WavHeader::headerLength()); + //waveDecoder.writeDataLength(); + waveDecoder.close(); + audioFile->close(); +} + +void tst_QAudioSource::reset() +{ + QFETCH(QAudioFormat, audioFormat); + + // Try both push/pull.. the vagaries of Active vs Idle are tested elsewhere + { + QAudioSource audioInput(audioFormat, this); + + QSignalSpy stateSignal(&audioInput, &QAudioSource::stateChanged); + + // Check that we are in the default state before calling start + QVERIFY2((audioInput.state() == QAudio::StoppedState), "state() was not set to StoppedState before start()"); + QVERIFY2((audioInput.error() == QAudio::NoError), "error() was not set to QAudio::NoError before start()"); + QVERIFY2((audioInput.elapsedUSecs() == qint64(0)),"elapsedUSecs() not zero on creation"); + + QIODevice* device = audioInput.start(); + // Check that QAudioSource immediately transitions to IdleState + QTRY_VERIFY2((stateSignal.size() == 1),"didn't emit IdleState signal on start()"); + QVERIFY2((audioInput.state() == QAudio::IdleState), "didn't transition to IdleState after start()"); + QVERIFY2((audioInput.error() == QAudio::NoError), "error state is not equal to QAudio::NoError after start()"); + QTRY_VERIFY2_WITH_TIMEOUT((audioInput.bytesAvailable() > 0), "no bytes available after starting", 10000); + + // Trigger a read + QByteArray data = device->readAll(); + QVERIFY2((audioInput.error() == QAudio::NoError), "error state is not equal to QAudio::NoError after start()"); + stateSignal.clear(); + + audioInput.reset(); + QTRY_VERIFY2((stateSignal.size() == 1),"didn't emit StoppedState signal after reset()"); + QVERIFY2((audioInput.state() == QAudio::StoppedState), "didn't transitions to StoppedState after reset()"); + QVERIFY2((audioInput.bytesAvailable() == 0), "buffer not cleared after reset()"); + } + + { + QAudioSource audioInput(audioFormat, this); + QBuffer buffer; + buffer.open(QIODevice::WriteOnly); + + QSignalSpy stateSignal(&audioInput, &QAudioSource::stateChanged); + + // Check that we are in the default state before calling start + QVERIFY2((audioInput.state() == QAudio::StoppedState), "state() was not set to StoppedState before start()"); + QVERIFY2((audioInput.error() == QAudio::NoError), "error() was not set to QAudio::NoError before start()"); + QVERIFY2((audioInput.elapsedUSecs() == qint64(0)),"elapsedUSecs() not zero on creation"); + + audioInput.start(&buffer); + + // Check that QAudioSource immediately transitions to ActiveState + QTRY_VERIFY2((stateSignal.size() >= 1),"didn't emit state changed signal on start()"); + QTRY_VERIFY2((audioInput.state() == QAudio::ActiveState), "didn't transition to ActiveState after start()"); + QVERIFY2((audioInput.error() == QAudio::NoError), "error state is not equal to QAudio::NoError after start()"); + stateSignal.clear(); + + audioInput.reset(); + QTRY_VERIFY2((stateSignal.size() >= 1),"didn't emit StoppedState signal after reset()"); + QVERIFY2((audioInput.state() == QAudio::StoppedState), "didn't transitions to StoppedState after reset()"); + QVERIFY2((audioInput.bytesAvailable() == 0), "buffer not cleared after reset()"); + } +} + +void tst_QAudioSource::volume() +{ + QFETCH(QAudioFormat, audioFormat); + + const qreal half(0.5f); + const qreal one(1.0f); + + QAudioSource audioInput(audioFormat, this); + + qreal volume = audioInput.volume(); + audioInput.setVolume(half); + QTRY_VERIFY(qRound(audioInput.volume()*10.0f) == 5); + + audioInput.setVolume(one); + QTRY_VERIFY(qRound(audioInput.volume()*10.0f) == 10); + + audioInput.setVolume(half); + audioInput.start(); + QTRY_VERIFY(qRound(audioInput.volume()*10.0f) == 5); + audioInput.setVolume(one); + QTRY_VERIFY(qRound(audioInput.volume()*10.0f) == 10); + + audioInput.setVolume(volume); +} + +QTEST_MAIN(tst_QAudioSource) + +#include "tst_qaudiosource.moc" diff --git a/tests/auto/integration/qcamerabackend/BLACKLIST b/tests/auto/integration/qcamerabackend/BLACKLIST new file mode 100644 index 000000000..e958c2103 --- /dev/null +++ b/tests/auto/integration/qcamerabackend/BLACKLIST @@ -0,0 +1,12 @@ +ci + +[testCameraCaptureMetadata] +osx +ios + +[testCameraStartParallel] +ios + +[testCameraFormat] +osx +ios diff --git a/tests/auto/integration/qcamerabackend/CMakeLists.txt b/tests/auto/integration/qcamerabackend/CMakeLists.txt new file mode 100644 index 000000000..07a6a4cae --- /dev/null +++ b/tests/auto/integration/qcamerabackend/CMakeLists.txt @@ -0,0 +1,27 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +# Generated from qcamerabackend.pro. + +##################################################################### +## tst_qcamerabackend Test: +##################################################################### + +qt_internal_add_test(tst_qcamerabackend + SOURCES + tst_qcamerabackend.cpp + ../shared/mediabackendutils.h + INCLUDE_DIRECTORIES + ../shared/ + LIBRARIES + Qt::Gui + Qt::MultimediaPrivate + Qt::Widgets +) + +# special case begin +if(APPLE) + set_property(TARGET tst_qcamerabackend PROPERTY MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_SOURCE_DIR}/Info.plist") + set_property(TARGET tst_qcamerabackend PROPERTY PROPERTY MACOSX_BUNDLE TRUE) +endif() +# special case end diff --git a/tests/auto/integration/qcamerabackend/Info.plist b/tests/auto/integration/qcamerabackend/Info.plist new file mode 100644 index 000000000..590080a1f --- /dev/null +++ b/tests/auto/integration/qcamerabackend/Info.plist @@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>CFBundleInfoDictionaryVersion</key> + <string>6.0</string> + <key>CFBundlePackageType</key> + <string>APPL</string> + + <key>CFBundleName</key> + <string>${MACOSX_BUNDLE_BUNDLE_NAME}</string> + <key>CFBundleIdentifier</key> + <string>${MACOSX_BUNDLE_GUI_IDENTIFIER}</string> + <key>CFBundleExecutable</key> + <string>${MACOSX_BUNDLE_EXECUTABLE_NAME}</string> + + <key>CFBundleVersion</key> + <string>${MACOSX_BUNDLE_BUNDLE_VERSION}</string> + <key>CFBundleShortVersionString</key> + <string>${MACOSX_BUNDLE_SHORT_VERSION_STRING}</string> + <key>CFBundleLongVersionString</key> + <string>${MACOSX_BUNDLE_LONG_VERSION_STRING}</string> + + <key>LSMinimumSystemVersion</key> + <string>${CMAKE_OSX_DEPLOYMENT_TARGET}</string> + + <key>CFBundleGetInfoString</key> + <string>${MACOSX_BUNDLE_INFO_STRING}</string> + <key>NSHumanReadableCopyright</key> + <string>${MACOSX_BUNDLE_COPYRIGHT}</string> + + <key>CFBundleIconFile</key> + <string>${MACOSX_BUNDLE_ICON_FILE}</string> + + <key>CFBundleDevelopmentRegion</key> + <string>English</string> + + <key>NSCameraUsageDescription</key> + <string>Qt Multimedia Test</string> + <key>NSMicrophoneUsageDescription</key> + <string>Qt Multimedia Test</string> + + <key>NSSupportsAutomaticGraphicsSwitching</key> + <true/> +</dict> +</plist> diff --git a/tests/auto/integration/qcamerabackend/qcamerabackend.pro b/tests/auto/integration/qcamerabackend/qcamerabackend.pro deleted file mode 100644 index b5f079008..000000000 --- a/tests/auto/integration/qcamerabackend/qcamerabackend.pro +++ /dev/null @@ -1,10 +0,0 @@ -TARGET = tst_qcamerabackend - -# DirectShow plugin requires widgets. -QT += multimedia-private widgets testlib - -# This is more of a system test -CONFIG += testcase - -SOURCES += tst_qcamerabackend.cpp - diff --git a/tests/auto/integration/qcamerabackend/tst_qcamerabackend.cpp b/tests/auto/integration/qcamerabackend/tst_qcamerabackend.cpp index c49236f43..fd6d819eb 100644 --- a/tests/auto/integration/qcamerabackend/tst_qcamerabackend.cpp +++ b/tests/auto/integration/qcamerabackend/tst_qcamerabackend.cpp @@ -1,54 +1,32 @@ -/**************************************************************************** -** -** Copyright (C) 2016 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:GPL-EXCEPT$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 3 as published by the Free Software -** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ - -//TESTED_COMPONENT=src/multimedia +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #include <QtTest/QtTest> #include <QtGui/QImageReader> +#include <QtCore/qurl.h> +#include <QtCore/qlocale.h> #include <QDebug> +#include <QVideoSink> +#include <QMediaPlayer> -#include <qabstractvideosurface.h> -#include <qcameracontrol.h> -#include <qcameralockscontrol.h> -#include <qcameraexposurecontrol.h> -#include <qcameraflashcontrol.h> -#include <qcamerafocuscontrol.h> -#include <qcameraimagecapturecontrol.h> -#include <qimageencodercontrol.h> -#include <qcameraimageprocessingcontrol.h> -#include <qcameracapturebufferformatcontrol.h> -#include <qcameracapturedestinationcontrol.h> -#include <qmediaservice.h> +#include <private/qplatformcamera_p.h> +#include <private/qplatformimagecapture_p.h> +#include <private/qplatformmediaintegration_p.h> #include <qcamera.h> -#include <qcamerainfo.h> -#include <qcameraimagecapture.h> -#include <qvideorenderercontrol.h> -#include <private/qmediaserviceprovider_p.h> +#include <qcameradevice.h> +#include <qimagecapture.h> +#include <qmediacapturesession.h> +#include <qobject.h> +#include <qmediadevices.h> +#include <qmediarecorder.h> +#include <qmediaplayer.h> +#include <qaudiooutput.h> + +#ifdef Q_OS_DARWIN +#include <QtCore/private/qcore_mac_p.h> +#endif + +#include <mediabackendutils.h> QT_USE_NAMESPACE @@ -68,14 +46,13 @@ public slots: void cleanupTestCase(); private slots: - void testCameraInfo(); - void testCtorWithDevice(); - void testCtorWithCameraInfo(); + void testCameraDevice(); + void testCtorWithCameraDevice(); void testCtorWithPosition(); - void testCameraStates(); - void testCameraStartError(); - void testCaptureMode(); + void testCameraActive(); + void testCameraStartParallel(); + void testCameraFormat(); void testCameraCapture(); void testCaptureToBuffer(); void testCameraCaptureMetadata(); @@ -84,285 +61,318 @@ private slots: void testVideoRecording_data(); void testVideoRecording(); + + void testNativeMetadata(); + + void multipleCameraSet(); + private: + bool noCamera = false; +}; + +class TestVideoFormat : public QVideoSink +{ + Q_OBJECT +public: + explicit TestVideoFormat(const QCameraFormat &format) + : formatMismatch(0), + cameraFormat(format) + { + connect(this, &QVideoSink::videoFrameChanged, this, &TestVideoFormat::checkVideoFrameFormat); + } + + void setCameraFormatToTest(const QCameraFormat &format) + { + formatMismatch = -1; + cameraFormat = format; + } + + int formatMismatch = -1; + +private: + QCameraFormat cameraFormat; + +public Q_SLOTS: + void checkVideoFrameFormat(const QVideoFrame &frame) + { + QVideoFrameFormat surfaceFormat = frame.surfaceFormat(); + if (surfaceFormat.pixelFormat() == cameraFormat.pixelFormat() + && surfaceFormat.frameSize() == cameraFormat.resolution()) { + formatMismatch = 0; +#ifdef Q_OS_ANDROID + } else if ((surfaceFormat.pixelFormat() == QVideoFrameFormat::Format_YUV420P + || surfaceFormat.pixelFormat() == QVideoFrameFormat::Format_NV12) + && (cameraFormat.pixelFormat() == QVideoFrameFormat::Format_YUV420P + || cameraFormat.pixelFormat() == QVideoFrameFormat::Format_NV12) + && surfaceFormat.frameSize() == cameraFormat.resolution()) { + formatMismatch = 0; +#endif + } else { + formatMismatch = 1; + } + } }; void tst_QCameraBackend::initTestCase() { +#ifdef Q_OS_ANDROID + QSKIP("SKIP initTestCase on CI, because of QTBUG-118571"); +#endif QCamera camera; - if (!camera.isAvailable()) - QSKIP("Camera is not available"); + noCamera = !camera.isAvailable(); } void tst_QCameraBackend::cleanupTestCase() { } -void tst_QCameraBackend::testCameraInfo() +void tst_QCameraBackend::testCameraDevice() { - int deviceCount = QMediaServiceProvider::defaultServiceProvider()->devices(QByteArray(Q_MEDIASERVICE_CAMERA)).count(); - const QList<QCameraInfo> cameras = QCameraInfo::availableCameras(); - QCOMPARE(cameras.count(), deviceCount); + const QList<QCameraDevice> cameras = QMediaDevices::videoInputs(); if (cameras.isEmpty()) { - QVERIFY(QCameraInfo::defaultCamera().isNull()); + QVERIFY(noCamera); + QVERIFY(QMediaDevices::defaultVideoInput().isNull()); QSKIP("Camera selection is not supported"); } + QVERIFY(!noCamera); - for (const QCameraInfo &info : cameras) { - QVERIFY(!info.deviceName().isEmpty()); + for (const QCameraDevice &info : cameras) { + QVERIFY(!info.id().isEmpty()); QVERIFY(!info.description().isEmpty()); - QVERIFY(info.orientation() % 90 == 0); } } -void tst_QCameraBackend::testCtorWithDevice() +void tst_QCameraBackend::testCtorWithCameraDevice() { - const auto availableCameras = QCameraInfo::availableCameras(); - if (availableCameras.isEmpty()) - QSKIP("Camera selection not supported"); - - QCamera *camera = new QCamera(availableCameras.first().deviceName().toLatin1()); - QCOMPARE(camera->error(), QCamera::NoError); - delete camera; + if (noCamera) { + // only verify that we get an error trying to create a camera + QCamera camera; + QCOMPARE(camera.error(), QCamera::CameraError); + QVERIFY(camera.cameraDevice().isNull()); - //loading non existing camera should fail - camera = new QCamera(QUuid::createUuid().toByteArray()); - QCOMPARE(camera->error(), QCamera::ServiceMissingError); - - delete camera; -} + QSKIP("No camera available"); + } -void tst_QCameraBackend::testCtorWithCameraInfo() -{ - if (QCameraInfo::availableCameras().isEmpty()) - QSKIP("Camera selection not supported"); + QCameraDevice defaultCamera = QMediaDevices::defaultVideoInput(); { - QCameraInfo info = QCameraInfo::defaultCamera(); - QCamera camera(info); + // should use default camera + QCamera camera; QCOMPARE(camera.error(), QCamera::NoError); - QCOMPARE(QCameraInfo(camera), info); + QVERIFY(!camera.cameraDevice().isNull()); + QCOMPARE(camera.cameraDevice(), defaultCamera); } + { - QCameraInfo info = QCameraInfo::availableCameras().first(); - QCamera camera(info); + // should use default camera + QCamera camera(QCameraDevice{}); QCOMPARE(camera.error(), QCamera::NoError); - QCOMPARE(QCameraInfo(camera), info); + QVERIFY(!camera.cameraDevice().isNull()); + QCOMPARE(camera.cameraDevice(), defaultCamera); } + { - // loading an invalid CameraInfo should fail - QCamera *camera = new QCamera(QCameraInfo()); - QCOMPARE(camera->error(), QCamera::ServiceMissingError); - QVERIFY(QCameraInfo(*camera).isNull()); - delete camera; + QCamera camera(defaultCamera); + QCOMPARE(camera.error(), QCamera::NoError); + QCOMPARE(camera.cameraDevice(), defaultCamera); } { - // loading non existing camera should fail - QCamera camera(QCameraInfo(QUuid::createUuid().toByteArray())); - QCOMPARE(camera.error(), QCamera::ServiceMissingError); - QVERIFY(QCameraInfo(camera).isNull()); + QCameraDevice info = QMediaDevices::videoInputs().first(); + QCamera camera(info); + QCOMPARE(camera.error(), QCamera::NoError); + QCOMPARE(camera.cameraDevice(), info); } } void tst_QCameraBackend::testCtorWithPosition() { + if (noCamera) + QSKIP("No camera available"); + { - QCamera camera(QCamera::UnspecifiedPosition); + QCamera camera(QCameraDevice::UnspecifiedPosition); QCOMPARE(camera.error(), QCamera::NoError); } { - QCamera camera(QCamera::FrontFace); + QCamera camera(QCameraDevice::FrontFace); // even if no camera is available at this position, it should not fail // and load the default camera QCOMPARE(camera.error(), QCamera::NoError); } { - QCamera camera(QCamera::BackFace); + QCamera camera(QCameraDevice::BackFace); // even if no camera is available at this position, it should not fail // and load the default camera QCOMPARE(camera.error(), QCamera::NoError); } } -void tst_QCameraBackend::testCameraStates() +void tst_QCameraBackend::testCameraActive() { + QMediaCaptureSession session; QCamera camera; - QCameraImageCapture imageCapture(&camera); - - QSignalSpy errorSignal(&camera, SIGNAL(errorOccurred(QCamera::Error))); - QSignalSpy stateChangedSignal(&camera, SIGNAL(stateChanged(QCamera::State))); - QSignalSpy statusChangedSignal(&camera, SIGNAL(statusChanged(QCamera::Status))); + camera.setCameraDevice(QCameraDevice()); + QImageCapture imageCapture; + session.setCamera(&camera); + session.setImageCapture(&imageCapture); - QCOMPARE(camera.state(), QCamera::UnloadedState); - QCOMPARE(camera.status(), QCamera::UnloadedStatus); + QSignalSpy errorSignal(&camera, &QCamera::errorOccurred); + QSignalSpy activeChangedSignal(&camera, &QCamera::activeChanged); - camera.load(); - QCOMPARE(camera.state(), QCamera::LoadedState); - QCOMPARE(stateChangedSignal.count(), 1); - QCOMPARE(stateChangedSignal.last().first().value<QCamera::State>(), QCamera::LoadedState); - QVERIFY(stateChangedSignal.count() > 0); + QCOMPARE(camera.isActive(), false); - QTRY_COMPARE(camera.status(), QCamera::LoadedStatus); - QCOMPARE(statusChangedSignal.last().first().value<QCamera::Status>(), QCamera::LoadedStatus); - - camera.unload(); - QCOMPARE(camera.state(), QCamera::UnloadedState); - QCOMPARE(stateChangedSignal.last().first().value<QCamera::State>(), QCamera::UnloadedState); - QTRY_COMPARE(camera.status(), QCamera::UnloadedStatus); - QCOMPARE(statusChangedSignal.last().first().value<QCamera::Status>(), QCamera::UnloadedStatus); + if (noCamera) + QSKIP("No camera available"); + camera.setCameraDevice(QMediaDevices::defaultVideoInput()); + QCOMPARE(camera.error(), QCamera::NoError); camera.start(); - QCOMPARE(camera.state(), QCamera::ActiveState); - QCOMPARE(stateChangedSignal.last().first().value<QCamera::State>(), QCamera::ActiveState); - QTRY_COMPARE(camera.status(), QCamera::ActiveStatus); - QCOMPARE(statusChangedSignal.last().first().value<QCamera::Status>(), QCamera::ActiveStatus); + QTRY_COMPARE(camera.isActive(), true); + QTRY_COMPARE(activeChangedSignal.size(), 1); + QCOMPARE(activeChangedSignal.last().first().value<bool>(), true); camera.stop(); - QCOMPARE(camera.state(), QCamera::LoadedState); - QCOMPARE(stateChangedSignal.last().first().value<QCamera::State>(), QCamera::LoadedState); - QTRY_COMPARE(camera.status(), QCamera::LoadedStatus); - QCOMPARE(statusChangedSignal.last().first().value<QCamera::Status>(), QCamera::LoadedStatus); - - camera.unload(); - QCOMPARE(camera.state(), QCamera::UnloadedState); - QCOMPARE(stateChangedSignal.last().first().value<QCamera::State>(), QCamera::UnloadedState); - QTRY_COMPARE(camera.status(), QCamera::UnloadedStatus); - QCOMPARE(statusChangedSignal.last().first().value<QCamera::Status>(), QCamera::UnloadedStatus); + QCOMPARE(camera.isActive(), false); + QCOMPARE(activeChangedSignal.last().first().value<bool>(), false); QCOMPARE(camera.errorString(), QString()); - QCOMPARE(errorSignal.count(), 0); } -void tst_QCameraBackend::testCameraStartError() +void tst_QCameraBackend::testCameraStartParallel() { - QCamera camera1(QCameraInfo::defaultCamera()); - QCamera camera2(QCameraInfo::defaultCamera()); +#ifdef Q_OS_ANDROID + QSKIP("Multi-camera feature is currently not supported on Android. " + "Cannot open same device twice."); +#endif +#ifdef Q_OS_LINUX + QSKIP("Multi-camera feature is currently not supported on Linux. " + "Cannot open same device twice."); +#endif + if (noCamera) + QSKIP("No camera available"); + + QMediaCaptureSession session1; + QMediaCaptureSession session2; + QCamera camera1(QMediaDevices::defaultVideoInput()); + QCamera camera2(QMediaDevices::defaultVideoInput()); + session1.setCamera(&camera1); + session2.setCamera(&camera2); QSignalSpy errorSpy1(&camera1, &QCamera::errorOccurred); QSignalSpy errorSpy2(&camera2, &QCamera::errorOccurred); camera1.start(); camera2.start(); - QCOMPARE(camera1.state(), QCamera::ActiveState); - QTRY_COMPARE(camera1.status(), QCamera::ActiveStatus); + QCOMPARE(camera1.isActive(), true); QCOMPARE(camera1.error(), QCamera::NoError); - QCOMPARE(camera2.state(), QCamera::UnloadedState); - QCOMPARE(camera2.status(), QCamera::UnloadedStatus); - QCOMPARE(camera2.error(), QCamera::CameraError); + QCOMPARE(camera2.isActive(), true); + QCOMPARE(camera2.error(), QCamera::NoError); - QCOMPARE(errorSpy1.count(), 0); - QCOMPARE(errorSpy2.count(), 1); + QCOMPARE(errorSpy1.size(), 0); + QCOMPARE(errorSpy2.size(), 0); } -void tst_QCameraBackend::testCaptureMode() +void tst_QCameraBackend::testCameraFormat() { QCamera camera; + QCameraDevice device = camera.cameraDevice(); + auto videoFormats = device.videoFormats(); + if (videoFormats.isEmpty()) + QSKIP("No Camera available, skipping test."); + QCameraFormat cameraFormat = videoFormats.first(); + QSignalSpy spy(&camera, &QCamera::cameraFormatChanged); + QVERIFY(spy.size() == 0); + + QMediaCaptureSession session; + session.setCamera(&camera); + QVERIFY(videoFormats.size()); + camera.setCameraFormat(cameraFormat); + QCOMPARE(camera.cameraFormat(), cameraFormat); + QVERIFY(spy.size() == 1); + + TestVideoFormat videoFormatTester(cameraFormat); + session.setVideoOutput(&videoFormatTester); + camera.start(); + QTRY_VERIFY(videoFormatTester.formatMismatch == 0); - QSignalSpy errorSignal(&camera, SIGNAL(errorOccurred(QCamera::Error))); - QSignalSpy stateChangedSignal(&camera, SIGNAL(stateChanged(QCamera::State))); - QSignalSpy captureModeSignal(&camera, SIGNAL(captureModeChanged(QCamera::CaptureModes))); - - QCOMPARE(camera.captureMode(), QCamera::CaptureStillImage); - - if (!camera.isCaptureModeSupported(QCamera::CaptureVideo)) { - camera.setCaptureMode(QCamera::CaptureVideo); - QCOMPARE(camera.captureMode(), QCamera::CaptureStillImage); - QSKIP("Video capture not supported"); + spy.clear(); + camera.stop(); + // Change camera format + if (videoFormats.size() > 1) { + QCameraFormat secondFormat = videoFormats.at(1); + camera.setCameraFormat(secondFormat); + QCOMPARE(camera.cameraFormat(), secondFormat); + QCOMPARE(spy.size(), 1); + QCOMPARE(camera.cameraFormat(), secondFormat); + videoFormatTester.setCameraFormatToTest(secondFormat); + camera.start(); + QTRY_VERIFY(videoFormatTester.formatMismatch == 0); + + // check that frame format is not same as previous camera format + videoFormatTester.setCameraFormatToTest(cameraFormat); + QTRY_VERIFY(videoFormatTester.formatMismatch == 1); } - camera.setCaptureMode(QCamera::CaptureVideo); - QCOMPARE(camera.captureMode(), QCamera::CaptureVideo); - QTRY_COMPARE(captureModeSignal.size(), 1); - QCOMPARE(captureModeSignal.last().first().value<QCamera::CaptureModes>(), QCamera::CaptureVideo); - captureModeSignal.clear(); - - camera.load(); - QTRY_COMPARE(camera.status(), QCamera::LoadedStatus); - //capture mode should still be video - QCOMPARE(camera.captureMode(), QCamera::CaptureVideo); - - //it should be possible to switch capture mode in Loaded state - camera.setCaptureMode(QCamera::CaptureStillImage); - QTRY_COMPARE(captureModeSignal.size(), 1); - QCOMPARE(captureModeSignal.last().first().value<QCamera::CaptureModes>(), QCamera::CaptureStillImage); - captureModeSignal.clear(); - - camera.setCaptureMode(QCamera::CaptureVideo); - QTRY_COMPARE(captureModeSignal.size(), 1); - QCOMPARE(captureModeSignal.last().first().value<QCamera::CaptureModes>(), QCamera::CaptureVideo); - captureModeSignal.clear(); - + // Set null format + spy.clear(); + camera.stop(); + camera.setCameraFormat({}); + QCOMPARE(spy.size(), 1); + videoFormatTester.setCameraFormatToTest({}); camera.start(); - QTRY_COMPARE(camera.status(), QCamera::ActiveStatus); - //capture mode should still be video - QCOMPARE(camera.captureMode(), QCamera::CaptureVideo); - - stateChangedSignal.clear(); - //it should be possible to switch capture mode in Active state - camera.setCaptureMode(QCamera::CaptureStillImage); - //camera may leave Active status, but should return to Active - QTest::qWait(10); //camera may leave Active status async - QTRY_COMPARE(camera.status(), QCamera::ActiveStatus); - QCOMPARE(camera.captureMode(), QCamera::CaptureStillImage); - QVERIFY2(stateChangedSignal.isEmpty(), "camera should not change the state during capture mode changes"); - - QCOMPARE(captureModeSignal.size(), 1); - QCOMPARE(captureModeSignal.last().first().value<QCamera::CaptureModes>(), QCamera::CaptureStillImage); - captureModeSignal.clear(); - - camera.setCaptureMode(QCamera::CaptureVideo); - //camera may leave Active status, but should return to Active - QTest::qWait(10); //camera may leave Active status async - QTRY_COMPARE(camera.status(), QCamera::ActiveStatus); - QCOMPARE(camera.captureMode(), QCamera::CaptureVideo); - - QVERIFY2(stateChangedSignal.isEmpty(), "camera should not change the state during capture mode changes"); - - QCOMPARE(captureModeSignal.size(), 1); - QCOMPARE(captureModeSignal.last().first().value<QCamera::CaptureModes>(), QCamera::CaptureVideo); - captureModeSignal.clear(); - + // In case of a null format, the backend should have picked + // a decent format to render frames + QTRY_VERIFY(videoFormatTester.formatMismatch == 1); camera.stop(); - QCOMPARE(camera.captureMode(), QCamera::CaptureVideo); - camera.unload(); - QCOMPARE(camera.captureMode(), QCamera::CaptureVideo); - QVERIFY2(errorSignal.isEmpty(), QString("Camera error: %1").arg(camera.errorString()).toLocal8Bit()); + spy.clear(); + // Shouldn't change anything as it's the same device + camera.setCameraDevice(device); + QCOMPARE(spy.size(), 0); } void tst_QCameraBackend::testCameraCapture() { + QMediaCaptureSession session; QCamera camera; - QCameraImageCapture imageCapture(&camera); + QImageCapture imageCapture; + session.setCamera(&camera); + session.setImageCapture(&imageCapture); + //prevents camera to flash during the test - camera.exposure()->setFlashMode(QCameraExposure::FlashOff); + camera.setFlashMode(QCamera::FlashOff); QVERIFY(!imageCapture.isReadyForCapture()); - QSignalSpy capturedSignal(&imageCapture, SIGNAL(imageCaptured(int,QImage))); - QSignalSpy savedSignal(&imageCapture, SIGNAL(imageSaved(int,QString))); - QSignalSpy errorSignal(&imageCapture, SIGNAL(error(int,QCameraImageCapture::Error,QString))); + QSignalSpy capturedSignal(&imageCapture, &QImageCapture::imageCaptured); + QSignalSpy savedSignal(&imageCapture, &QImageCapture::imageSaved); + QSignalSpy errorSignal(&imageCapture, &QImageCapture::errorOccurred); - imageCapture.capture(); + imageCapture.captureToFile(); QTRY_COMPARE(errorSignal.size(), 1); - QCOMPARE(imageCapture.error(), QCameraImageCapture::NotReadyError); + QCOMPARE(imageCapture.error(), QImageCapture::NotReadyError); QCOMPARE(capturedSignal.size(), 0); errorSignal.clear(); + if (noCamera) + QSKIP("No camera available"); + + QVideoSink sink; + session.setVideoOutput(&sink); camera.start(); QTRY_VERIFY(imageCapture.isReadyForCapture()); - QCOMPARE(camera.status(), QCamera::ActiveStatus); + QVERIFY(camera.isActive()); QCOMPARE(errorSignal.size(), 0); - int id = imageCapture.capture(); + int id = imageCapture.captureToFile(); - QTRY_VERIFY(!savedSignal.isEmpty()); + QTRY_VERIFY_WITH_TIMEOUT(!savedSignal.isEmpty(), 8s); QTRY_COMPARE(capturedSignal.size(), 1); QCOMPARE(capturedSignal.last().first().toInt(), id); QCOMPARE(errorSignal.size(), 0); - QCOMPARE(imageCapture.error(), QCameraImageCapture::NoError); + QCOMPARE(imageCapture.error(), QImageCapture::NoError); QCOMPARE(savedSignal.last().first().toInt(), id); QString location = savedSignal.last().last().toString(); @@ -378,35 +388,25 @@ void tst_QCameraBackend::testCameraCapture() void tst_QCameraBackend::testCaptureToBuffer() { - QCamera camera; - QCameraImageCapture imageCapture(&camera); - camera.exposure()->setFlashMode(QCameraExposure::FlashOff); - - camera.load(); + if (noCamera) + QSKIP("No camera available"); - if (!imageCapture.isCaptureDestinationSupported(QCameraImageCapture::CaptureToBuffer)) - QSKIP("Buffer capture not supported"); - - QTRY_COMPARE(camera.status(), QCamera::LoadedStatus); + QMediaCaptureSession session; + QCamera camera; + QImageCapture imageCapture; + session.setCamera(&camera); + session.setImageCapture(&imageCapture); - QVERIFY(imageCapture.isCaptureDestinationSupported(QCameraImageCapture::CaptureToFile)); - QVERIFY(imageCapture.isCaptureDestinationSupported(QCameraImageCapture::CaptureToBuffer)); - QVERIFY(imageCapture.isCaptureDestinationSupported( - QCameraImageCapture::CaptureToBuffer | QCameraImageCapture::CaptureToFile)); + camera.setFlashMode(QCamera::FlashOff); - QSignalSpy destinationChangedSignal(&imageCapture, SIGNAL(captureDestinationChanged(QCameraImageCapture::CaptureDestinations))); + camera.setActive(true); - QCOMPARE(imageCapture.captureDestination(), QCameraImageCapture::CaptureToFile); - imageCapture.setCaptureDestination(QCameraImageCapture::CaptureToBuffer); - QCOMPARE(imageCapture.captureDestination(), QCameraImageCapture::CaptureToBuffer); - QCOMPARE(destinationChangedSignal.size(), 1); - QCOMPARE(destinationChangedSignal.first().first().value<QCameraImageCapture::CaptureDestinations>(), - QCameraImageCapture::CaptureToBuffer); + QTRY_VERIFY(camera.isActive()); - QSignalSpy capturedSignal(&imageCapture, SIGNAL(imageCaptured(int,QImage))); - QSignalSpy imageAvailableSignal(&imageCapture, SIGNAL(imageAvailable(int,QVideoFrame))); - QSignalSpy savedSignal(&imageCapture, SIGNAL(imageSaved(int,QString))); - QSignalSpy errorSignal(&imageCapture, SIGNAL(error(int,QCameraImageCapture::Error,QString))); + QSignalSpy capturedSignal(&imageCapture, &QImageCapture::imageCaptured); + QSignalSpy imageAvailableSignal(&imageCapture, &QImageCapture::imageAvailable); + QSignalSpy savedSignal(&imageCapture, &QImageCapture::imageSaved); + QSignalSpy errorSignal(&imageCapture, &QImageCapture::errorOccurred); camera.start(); QTRY_VERIFY(imageCapture.isReadyForCapture()); @@ -425,241 +425,340 @@ void tst_QCameraBackend::testCaptureToBuffer() QCOMPARE(imageAvailableSignal.first().first().toInt(), id); QVideoFrame frame = imageAvailableSignal.first().last().value<QVideoFrame>(); - QVERIFY(!frame.image().isNull()); + QVERIFY(!frame.toImage().isNull()); frame = QVideoFrame(); capturedSignal.clear(); imageAvailableSignal.clear(); savedSignal.clear(); - if (imageCapture.supportedBufferFormats().contains(QVideoFrame::Format_UYVY)) { - imageCapture.setBufferFormat(QVideoFrame::Format_UYVY); - QCOMPARE(imageCapture.bufferFormat(), QVideoFrame::Format_UYVY); - - id = imageCapture.capture(); - QTRY_VERIFY(!imageAvailableSignal.isEmpty()); - - QVERIFY(errorSignal.isEmpty()); - QVERIFY(!capturedSignal.isEmpty()); - QVERIFY(!imageAvailableSignal.isEmpty()); - QVERIFY(savedSignal.isEmpty()); - - QTest::qWait(2000); - QVERIFY(savedSignal.isEmpty()); - - frame = imageAvailableSignal.first().last().value<QVideoFrame>(); - QVERIFY(frame.isValid()); - - qDebug() << frame.pixelFormat(); - QCOMPARE(frame.pixelFormat(), QVideoFrame::Format_UYVY); - QVERIFY(!frame.size().isEmpty()); - frame = QVideoFrame(); - - capturedSignal.clear(); - imageAvailableSignal.clear(); - savedSignal.clear(); - - imageCapture.setBufferFormat(QVideoFrame::Format_Jpeg); - QCOMPARE(imageCapture.bufferFormat(), QVideoFrame::Format_Jpeg); - } - QTRY_VERIFY(imageCapture.isReadyForCapture()); - - //Try to capture to both buffer and file - if (imageCapture.isCaptureDestinationSupported(QCameraImageCapture::CaptureToBuffer | QCameraImageCapture::CaptureToFile)) { - imageCapture.setCaptureDestination(QCameraImageCapture::CaptureToBuffer | QCameraImageCapture::CaptureToFile); - - int oldId = id; - id = imageCapture.capture(); - QVERIFY(id != oldId); - QTRY_VERIFY(!savedSignal.isEmpty()); - - QVERIFY(errorSignal.isEmpty()); - QVERIFY(!capturedSignal.isEmpty()); - QVERIFY(!imageAvailableSignal.isEmpty()); - QVERIFY(!savedSignal.isEmpty()); - - QCOMPARE(capturedSignal.first().first().toInt(), id); - QCOMPARE(imageAvailableSignal.first().first().toInt(), id); - - frame = imageAvailableSignal.first().last().value<QVideoFrame>(); - QVERIFY(!frame.image().isNull()); - - QString fileName = savedSignal.first().last().toString(); - QVERIFY(QFileInfo(fileName).exists()); - } } void tst_QCameraBackend::testCameraCaptureMetadata() { - QSKIP("Capture metadata is supported only on harmattan"); + if (noCamera) + QSKIP("No camera available"); + QMediaCaptureSession session; QCamera camera; - QCameraImageCapture imageCapture(&camera); - camera.exposure()->setFlashMode(QCameraExposure::FlashOff); + QImageCapture imageCapture; + session.setCamera(&camera); + session.setImageCapture(&imageCapture); + + camera.setFlashMode(QCamera::FlashOff); + + QMediaMetaData referenceMetaData; + referenceMetaData.insert(QMediaMetaData::Title, QStringLiteral("Title")); + referenceMetaData.insert(QMediaMetaData::Language, QVariant::fromValue(QLocale::German)); + referenceMetaData.insert(QMediaMetaData::Description, QStringLiteral("Description")); + imageCapture.setMetaData(referenceMetaData); - QSignalSpy metadataSignal(&imageCapture, SIGNAL(imageMetadataAvailable(int,QString,QVariant))); - QSignalSpy savedSignal(&imageCapture, SIGNAL(imageSaved(int,QString))); + QSignalSpy metadataSignal(&imageCapture, &QImageCapture::imageMetadataAvailable); + QSignalSpy savedSignal(&imageCapture, &QImageCapture::imageSaved); camera.start(); QTRY_VERIFY(imageCapture.isReadyForCapture()); - int id = imageCapture.capture(QString::fromLatin1("/dev/null")); + QTemporaryDir dir; + auto tmpFile = dir.filePath("testImage"); + int id = imageCapture.captureToFile(tmpFile); QTRY_VERIFY(!savedSignal.isEmpty()); QVERIFY(!metadataSignal.isEmpty()); + QCOMPARE(metadataSignal.first().first().toInt(), id); + QMediaMetaData receivedMetaData = metadataSignal.first()[1].value<QMediaMetaData>(); + + if (isGStreamerPlatform()) { + for (auto key : { + QMediaMetaData::Title, + QMediaMetaData::Language, + QMediaMetaData::Description, + }) + QCOMPARE(receivedMetaData[key], referenceMetaData[key]); + QVERIFY(receivedMetaData[QMediaMetaData::Resolution].isValid()); + } } void tst_QCameraBackend::testExposureCompensation() { - QSKIP("Capture exposure parameters are supported only on mobile platforms"); + if (noCamera) + QSKIP("No camera available"); + QMediaCaptureSession session; QCamera camera; - QCameraExposure *exposure = camera.exposure(); + session.setCamera(&camera); + + QSignalSpy exposureCompensationSignal(&camera, &QCamera::exposureCompensationChanged); - QSignalSpy exposureCompensationSignal(exposure, SIGNAL(exposureCompensationChanged(qreal))); + // it should be possible to set exposure parameters in Unloaded state + QCOMPARE(camera.exposureCompensation(), 0.); + if (!(camera.supportedFeatures() & QCamera::Feature::ExposureCompensation)) + return; - //it should be possible to set exposure parameters in Unloaded state - QCOMPARE(exposure->exposureCompensation()+1.0, 1.0); - exposure->setExposureCompensation(1.0); - QCOMPARE(exposure->exposureCompensation(), 1.0); - QTRY_COMPARE(exposureCompensationSignal.count(), 1); + camera.setExposureCompensation(1.0); + QCOMPARE(camera.exposureCompensation(), 1.0); + QTRY_COMPARE(exposureCompensationSignal.size(), 1); QCOMPARE(exposureCompensationSignal.last().first().toReal(), 1.0); //exposureCompensationChanged should not be emitted when value is not changed - exposure->setExposureCompensation(1.0); + camera.setExposureCompensation(1.0); QTest::qWait(50); - QCOMPARE(exposureCompensationSignal.count(), 1); - - //exposure compensation should be preserved during load/start - camera.load(); - QTRY_COMPARE(camera.status(), QCamera::LoadedStatus); - - QCOMPARE(exposure->exposureCompensation(), 1.0); - - exposureCompensationSignal.clear(); - exposure->setExposureCompensation(-1.0); - QCOMPARE(exposure->exposureCompensation(), -1.0); - QTRY_COMPARE(exposureCompensationSignal.count(), 1); - QCOMPARE(exposureCompensationSignal.last().first().toReal(), -1.0); + QCOMPARE(exposureCompensationSignal.size(), 1); + //exposure compensation should be preserved during start camera.start(); - QTRY_COMPARE(camera.status(), QCamera::ActiveStatus); + QTRY_VERIFY(camera.isActive()); - QCOMPARE(exposure->exposureCompensation(), -1.0); + QCOMPARE(camera.exposureCompensation(), 1.0); exposureCompensationSignal.clear(); - exposure->setExposureCompensation(1.0); - QCOMPARE(exposure->exposureCompensation(), 1.0); - QTRY_COMPARE(exposureCompensationSignal.count(), 1); - QCOMPARE(exposureCompensationSignal.last().first().toReal(), 1.0); + camera.setExposureCompensation(-1.0); + QCOMPARE(camera.exposureCompensation(), -1.0); + QTRY_COMPARE(exposureCompensationSignal.size(), 1); + QCOMPARE(exposureCompensationSignal.last().first().toReal(), -1.0); } void tst_QCameraBackend::testExposureMode() { - QSKIP("Capture exposure parameters are supported only on mobile platforms"); + if (noCamera) + QSKIP("No camera available"); QCamera camera; - QCameraExposure *exposure = camera.exposure(); - QCOMPARE(exposure->exposureMode(), QCameraExposure::ExposureAuto); + QCOMPARE(camera.exposureMode(), QCamera::ExposureAuto); // Night - exposure->setExposureMode(QCameraExposure::ExposureNight); - QCOMPARE(exposure->exposureMode(), QCameraExposure::ExposureNight); - camera.start(); - QTRY_COMPARE(camera.status(), QCamera::ActiveStatus); - QCOMPARE(exposure->exposureMode(), QCameraExposure::ExposureNight); + if (camera.isExposureModeSupported(QCamera::ExposureNight)) { + camera.setExposureMode(QCamera::ExposureNight); + QCOMPARE(camera.exposureMode(), QCamera::ExposureNight); + camera.start(); + QVERIFY(camera.isActive()); + QCOMPARE(camera.exposureMode(), QCamera::ExposureNight); + } - camera.unload(); - QTRY_COMPARE(camera.status(), QCamera::UnloadedStatus); + camera.stop(); + QTRY_VERIFY(!camera.isActive()); // Auto - exposure->setExposureMode(QCameraExposure::ExposureAuto); - QCOMPARE(exposure->exposureMode(), QCameraExposure::ExposureAuto); + camera.setExposureMode(QCamera::ExposureAuto); + QCOMPARE(camera.exposureMode(), QCamera::ExposureAuto); camera.start(); - QTRY_COMPARE(camera.status(), QCamera::ActiveStatus); - QCOMPARE(exposure->exposureMode(), QCameraExposure::ExposureAuto); + QTRY_VERIFY(camera.isActive()); + QCOMPARE(camera.exposureMode(), QCamera::ExposureAuto); + + // Manual + if (camera.isExposureModeSupported(QCamera::ExposureManual)) { + camera.setExposureMode(QCamera::ExposureManual); + QCOMPARE(camera.exposureMode(), QCamera::ExposureManual); + camera.start(); + QVERIFY(camera.isActive()); + QCOMPARE(camera.exposureMode(), QCamera::ExposureManual); + + camera.setManualExposureTime(.02f); // ~20ms should be supported by most cameras + QVERIFY(camera.manualExposureTime() > .01 && camera.manualExposureTime() < .04); + } + + camera.setExposureMode(QCamera::ExposureAuto); } void tst_QCameraBackend::testVideoRecording_data() { - QTest::addColumn<QByteArray>("device"); + QTest::addColumn<QCameraDevice>("device"); - const auto devices = QCameraInfo::availableCameras(); + const auto devices = QMediaDevices::videoInputs(); - for (const auto &device : devices) { - QTest::newRow(device.description().toUtf8()) - << device.deviceName().toLatin1(); - } + for (const auto &device : devices) + QTest::newRow(device.description().toUtf8()) << device; if (devices.isEmpty()) - QTest::newRow("Default device") << QByteArray(); + QTest::newRow("Null device") << QCameraDevice(); } void tst_QCameraBackend::testVideoRecording() { - QFETCH(QByteArray, device); + if (noCamera) + QSKIP("No camera available"); + QFETCH(QCameraDevice, device); - QScopedPointer<QCamera> camera(device.isEmpty() ? new QCamera : new QCamera(device)); + QMediaCaptureSession session; + QScopedPointer<QCamera> camera(new QCamera(device)); + session.setCamera(camera.data()); - QMediaRecorder recorder(camera.data()); + QMediaRecorder recorder; + session.setRecorder(&recorder); - QSignalSpy errorSignal(camera.data(), SIGNAL(errorOccurred(QCamera::Error))); - QSignalSpy recorderErrorSignal(&recorder, SIGNAL(error(QMediaRecorder::Error))); - QSignalSpy recorderStatusSignal(&recorder, SIGNAL(statusChanged(QMediaRecorder::Status))); + QSignalSpy errorSignal(camera.data(), &QCamera::errorOccurred); + QSignalSpy recorderErrorSignal(&recorder, &QMediaRecorder::errorOccurred); + QSignalSpy recorderStateChanged(&recorder, &QMediaRecorder::recorderStateChanged); + QSignalSpy durationChanged(&recorder, &QMediaRecorder::durationChanged); - if (!camera->isCaptureModeSupported(QCamera::CaptureVideo)) { - QSKIP("Video capture not supported"); - } + recorder.setVideoResolution(320, 240); - camera->setCaptureMode(QCamera::CaptureVideo); + // Insert metadata + QMediaMetaData metaData; + metaData.insert(QMediaMetaData::Author, QStringLiteral("Author")); + metaData.insert(QMediaMetaData::Date, QDateTime::currentDateTime()); + recorder.setMetaData(metaData); - QVideoEncoderSettings videoSettings; - videoSettings.setResolution(320, 240); - recorder.setVideoSettings(videoSettings); + camera->start(); + if (noCamera || device.isNull()) { + QVERIFY(!camera->isActive()); + return; + } + QTRY_VERIFY(camera->isActive()); - QCOMPARE(recorder.status(), QMediaRecorder::UnloadedStatus); + QTRY_VERIFY(camera->isActive()); - camera->start(); - QVERIFY(recorder.status() == QMediaRecorder::LoadingStatus || - recorder.status() == QMediaRecorder::LoadedStatus); - QCOMPARE(recorderStatusSignal.last().first().value<QMediaRecorder::Status>(), recorder.status()); - QTRY_COMPARE(camera->status(), QCamera::ActiveStatus); - QTRY_COMPARE(recorder.status(), QMediaRecorder::LoadedStatus); - QCOMPARE(recorderStatusSignal.last().first().value<QMediaRecorder::Status>(), recorder.status()); - - //record 5 seconds clip recorder.record(); - QTRY_COMPARE(recorder.status(), QMediaRecorder::RecordingStatus); - QCOMPARE(recorderStatusSignal.last().first().value<QMediaRecorder::Status>(), recorder.status()); - QTest::qWait(5000); - recorderStatusSignal.clear(); - recorder.stop(); - bool foundFinalizingStatus = false; - for (auto &list : recorderStatusSignal) { - if (list.contains(QVariant(QMediaRecorder::FinalizingStatus))) { - foundFinalizingStatus = true; - break; - } + if (!recorderErrorSignal.empty() || recorderErrorSignal.wait(550)) { + QEXPECT_FAIL_GSTREAMER("", "QTBUG-124148: GStreamer might return ResourceError", Continue); + + QCOMPARE(recorderErrorSignal.last().first().toInt(), QMediaRecorder::FormatError); + return; } - QVERIFY(foundFinalizingStatus); - QTRY_COMPARE(recorder.status(), QMediaRecorder::LoadedStatus); - QCOMPARE(recorderStatusSignal.last().first().value<QMediaRecorder::Status>(), recorder.status()); + + QTRY_VERIFY(durationChanged.size()); + + QCOMPARE(recorder.metaData(), metaData); + + recorderStateChanged.clear(); + recorder.stop(); + QTRY_VERIFY(recorderStateChanged.size() > 0); + QVERIFY(recorder.recorderState() == QMediaRecorder::StoppedState); QVERIFY(errorSignal.isEmpty()); QVERIFY(recorderErrorSignal.isEmpty()); QString fileName = recorder.actualLocation().toLocalFile(); QVERIFY(!fileName.isEmpty()); + QVERIFY(QFileInfo(fileName).size() > 0); + + QMediaPlayer player; + player.setSource(fileName); + + QTRY_COMPARE_WITH_TIMEOUT(player.mediaStatus(), QMediaPlayer::LoadedMedia, 8s); + QCOMPARE_EQ(player.metaData().value(QMediaMetaData::Resolution).toSize(), QSize(320, 240)); + QCOMPARE_GT(player.duration(), 350); + QCOMPARE_LT(player.duration(), 650); + + // TODO: integrate with a virtual camera and check mediaplayer output + + QFile(fileName).remove(); +} + +void tst_QCameraBackend::testNativeMetadata() +{ + if (noCamera) + QSKIP("No camera available"); + + QMediaCaptureSession session; + QCameraDevice device = QMediaDevices::defaultVideoInput(); + QCamera camera(device); + session.setCamera(&camera); + QMediaRecorder recorder; + session.setRecorder(&recorder); + + QSignalSpy errorSignal(&camera, &QCamera::errorOccurred); + QSignalSpy recorderErrorSignal(&recorder, &QMediaRecorder::errorOccurred); + QSignalSpy recorderStateChanged(&recorder, &QMediaRecorder::recorderStateChanged); + QSignalSpy durationChanged(&recorder, &QMediaRecorder::durationChanged); + + camera.start(); + if (device.isNull()) { + QVERIFY(!camera.isActive()); + return; + } + + QTRY_VERIFY(camera.isActive()); + + // Insert common metadata supported on all platforms + // Don't use Date, as some backends set that on their own + QMediaMetaData metaData; + metaData.insert(QMediaMetaData::Title, QStringLiteral("Title")); + metaData.insert(QMediaMetaData::Language, QVariant::fromValue(QLocale::German)); + metaData.insert(QMediaMetaData::Description, QStringLiteral("Description")); + + recorder.setMetaData(metaData); + + recorder.record(); + QTRY_VERIFY(durationChanged.size()); + QTRY_VERIFY(recorder.recorderState() == QMediaRecorder::RecorderState::RecordingState); + + QCOMPARE(recorder.metaData(), metaData); + + recorderStateChanged.clear(); + recorder.stop(); + + QTRY_VERIFY(recorderStateChanged.size() > 0); + QTRY_VERIFY(recorder.recorderState() == QMediaRecorder::RecorderState::StoppedState); + + QVERIFY(errorSignal.isEmpty()); + if (!isGStreamerPlatform()) { + // https://bugreports.qt.io/browse/QTBUG-124183 + QVERIFY(recorderErrorSignal.isEmpty()); + } + + QString fileName = recorder.actualLocation().toLocalFile(); + QVERIFY(!fileName.isEmpty()); QVERIFY(QFileInfo(fileName).size() > 0); + + // QMediaRecorder::metaData() can only test that QMediaMetaData is set properly on the recorder. + // Use QMediaPlayer to test that the native metadata is properly set on the track + QAudioOutput output; + QMediaPlayer player; + player.setAudioOutput(&output); + + QSignalSpy metadataChangedSpy(&player, &QMediaPlayer::metaDataChanged); + + player.setSource(QUrl::fromLocalFile(fileName)); + player.play(); + + int metadataChangedRequiredCount = isGStreamerPlatform() ? 2 : 1; + + QTRY_VERIFY(metadataChangedSpy.size() >= metadataChangedRequiredCount); + + QCOMPARE(player.metaData().value(QMediaMetaData::Title).toString(), + metaData.value(QMediaMetaData::Title).toString()); + auto lang = player.metaData().value(QMediaMetaData::Language).value<QLocale::Language>(); + if (lang != QLocale::AnyLanguage) + QCOMPARE(lang, metaData.value(QMediaMetaData::Language).value<QLocale::Language>()); + QCOMPARE(player.metaData().value(QMediaMetaData::Description).toString(), metaData.value(QMediaMetaData::Description).toString()); + QVERIFY(player.metaData().value(QMediaMetaData::Resolution).isValid()); + + if (isGStreamerPlatform()) + QVERIFY(player.metaData().value(QMediaMetaData::Date).isValid()); + + player.stop(); + player.setSource({}); QFile(fileName).remove(); +} + +void tst_QCameraBackend::multipleCameraSet() +{ + if (noCamera) + QSKIP("No camera available"); + + QMediaCaptureSession session; + QCameraDevice device = QMediaDevices::defaultVideoInput(); + + QMediaRecorder recorder; + session.setRecorder(&recorder); - camera->setCaptureMode(QCamera::CaptureStillImage); - QTRY_COMPARE(recorder.status(), QMediaRecorder::UnloadedStatus); - QCOMPARE(recorderStatusSignal.last().first().value<QMediaRecorder::Status>(), recorder.status()); + for (int i = 0; i < 5; ++i) { +#ifdef Q_OS_DARWIN + QMacAutoReleasePool releasePool; +#endif + + QCamera camera(device); + session.setCamera(&camera); + + camera.start(); + + QTest::qWait(100); + } } QTEST_MAIN(tst_QCameraBackend) diff --git a/tests/auto/integration/qdeclarativevideooutput/qdeclarativevideooutput.pro b/tests/auto/integration/qdeclarativevideooutput/qdeclarativevideooutput.pro deleted file mode 100644 index c4221232a..000000000 --- a/tests/auto/integration/qdeclarativevideooutput/qdeclarativevideooutput.pro +++ /dev/null @@ -1,11 +0,0 @@ -TARGET = tst_qdeclarativevideooutput - -QT += multimedia-private qml testlib quick qtmultimediaquicktools-private -CONFIG += testcase - -RESOURCES += qml.qrc - -SOURCES += \ - tst_qdeclarativevideooutput.cpp - -INCLUDEPATH += ../../../../src/imports/multimedia diff --git a/tests/auto/integration/qdeclarativevideooutput/qml.qrc b/tests/auto/integration/qdeclarativevideooutput/qml.qrc deleted file mode 100644 index 5f6483ac3..000000000 --- a/tests/auto/integration/qdeclarativevideooutput/qml.qrc +++ /dev/null @@ -1,5 +0,0 @@ -<RCC> - <qresource prefix="/"> - <file>main.qml</file> - </qresource> -</RCC> diff --git a/tests/auto/integration/qdeclarativevideooutput/tst_qdeclarativevideooutput.cpp b/tests/auto/integration/qdeclarativevideooutput/tst_qdeclarativevideooutput.cpp deleted file mode 100644 index b31947738..000000000 --- a/tests/auto/integration/qdeclarativevideooutput/tst_qdeclarativevideooutput.cpp +++ /dev/null @@ -1,740 +0,0 @@ -/**************************************************************************** -** -** Copyright (C) 2016 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:GPL-EXCEPT$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 3 as published by the Free Software -** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ - -//TESTED_COMPONENT=plugins/declarative/multimedia - -#include <QtTest/QtTest> - -#include <QtQml/qqmlengine.h> -#include <QtQml/qqmlcomponent.h> -#include <QQuickView> - -#include "private/qdeclarativevideooutput_p.h" - -#include <qabstractvideosurface.h> -#include <qvideorenderercontrol.h> -#include <qvideosurfaceformat.h> - -#include <qmediaobject.h> - -class SurfaceHolder : public QObject -{ - Q_OBJECT - Q_PROPERTY(QAbstractVideoSurface *videoSurface READ videoSurface WRITE setVideoSurface) -public: - SurfaceHolder(QObject *parent) - : QObject(parent) - , m_surface(0) - { - } - - QAbstractVideoSurface *videoSurface() const - { - return m_surface; - } - void setVideoSurface(QAbstractVideoSurface *surface) - { - if (m_surface != surface && m_surface && m_surface->isActive()) { - m_surface->stop(); - } - m_surface = surface; - } - - void presentDummyFrame(const QSize &size); - -private: - QAbstractVideoSurface *m_surface; - -}; - -// Starts the surface and puts a frame -void SurfaceHolder::presentDummyFrame(const QSize &size) -{ - if (m_surface && m_surface->supportedPixelFormats().count() > 0) { - QVideoFrame::PixelFormat pixelFormat = m_surface->supportedPixelFormats().value(0); - QVideoSurfaceFormat format(size, pixelFormat); - QVideoFrame frame(size.width() * size.height() * 4, size, size.width() * 4, pixelFormat); - - if (!m_surface->isActive()) - m_surface->start(format); - m_surface->present(frame); - - // Have to spin an event loop or two for the surfaceFormatChanged() signal - qApp->processEvents(); - } -} - -class tst_QDeclarativeVideoOutput : public QObject -{ - Q_OBJECT -public: - tst_QDeclarativeVideoOutput(); - - ~tst_QDeclarativeVideoOutput() - { - delete m_mappingOutput; - delete m_mappingSurface; - delete m_mappingComponent; - } - -public slots: - void initTestCase(); - -private slots: - void fillMode(); - void flushMode(); - void orientation(); - void surfaceSource(); - void paintSurface(); - void sourceRect(); - - void contentRect(); - void contentRect_data(); - - void mappingPoint(); - void mappingPoint_data(); - void mappingRect(); - void mappingRect_data(); - - // XXX May be worth adding tests that the surface activeChanged signals are sent appropriately - // to holder? - -private: - QQmlEngine m_engine; - - // Variables used for the mapping test - QQmlComponent *m_mappingComponent; - QObject *m_mappingOutput; - SurfaceHolder *m_mappingSurface; - - void updateOutputGeometry(QObject *output); - - QRectF invokeR2R(QObject *object, const char *signature, const QRectF &rect); - QPointF invokeP2P(QObject *object, const char *signature, const QPointF &point); -}; - -void tst_QDeclarativeVideoOutput::initTestCase() -{ - // We initialize the mapping vars here - m_mappingComponent = new QQmlComponent(&m_engine); - m_mappingComponent->loadUrl(QUrl("qrc:/main.qml")); - m_mappingSurface = new SurfaceHolder(this); - - m_mappingOutput = m_mappingComponent->create(); - QVERIFY(m_mappingOutput != 0); - - m_mappingOutput->setProperty("source", QVariant::fromValue(static_cast<QObject*>(m_mappingSurface))); - - m_mappingSurface->presentDummyFrame(QSize(200,100)); // this should start m_surface - updateOutputGeometry(m_mappingOutput); -} - -Q_DECLARE_METATYPE(QDeclarativeVideoOutput::FillMode) -Q_DECLARE_METATYPE(QDeclarativeVideoOutput::FlushMode) - -tst_QDeclarativeVideoOutput::tst_QDeclarativeVideoOutput() - : m_mappingComponent(0) - , m_mappingOutput(0) - , m_mappingSurface(0) -{ - qRegisterMetaType<QDeclarativeVideoOutput::FillMode>(); -} - -void tst_QDeclarativeVideoOutput::fillMode() -{ - QQmlComponent component(&m_engine); - component.loadUrl(QUrl("qrc:/main.qml")); - - QObject *videoOutput = component.create(); - QVERIFY(videoOutput != 0); - - QSignalSpy propSpy(videoOutput, SIGNAL(fillModeChanged(QDeclarativeVideoOutput::FillMode))); - - // Default is preserveaspectfit - QCOMPARE(videoOutput->property("fillMode").value<QDeclarativeVideoOutput::FillMode>(), QDeclarativeVideoOutput::PreserveAspectFit); - QCOMPARE(propSpy.count(), 0); - - videoOutput->setProperty("fillMode", QVariant(int(QDeclarativeVideoOutput::PreserveAspectCrop))); - QCOMPARE(videoOutput->property("fillMode").value<QDeclarativeVideoOutput::FillMode>(), QDeclarativeVideoOutput::PreserveAspectCrop); - QCOMPARE(propSpy.count(), 1); - - videoOutput->setProperty("fillMode", QVariant(int(QDeclarativeVideoOutput::Stretch))); - QCOMPARE(videoOutput->property("fillMode").value<QDeclarativeVideoOutput::FillMode>(), QDeclarativeVideoOutput::Stretch); - QCOMPARE(propSpy.count(), 2); - - videoOutput->setProperty("fillMode", QVariant(int(QDeclarativeVideoOutput::Stretch))); - QCOMPARE(videoOutput->property("fillMode").value<QDeclarativeVideoOutput::FillMode>(), QDeclarativeVideoOutput::Stretch); - QCOMPARE(propSpy.count(), 2); - - delete videoOutput; -} - -void tst_QDeclarativeVideoOutput::flushMode() -{ - QQmlComponent component(&m_engine); - component.loadUrl(QUrl("qrc:/main.qml")); - - QObject *videoOutput = component.create(); - QVERIFY(videoOutput != 0); - - QSignalSpy propSpy(videoOutput, SIGNAL(flushModeChanged())); - - QCOMPARE(videoOutput->property("flushMode").value<QDeclarativeVideoOutput::FlushMode>(), QDeclarativeVideoOutput::EmptyFrame); - QCOMPARE(propSpy.count(), 0); - - videoOutput->setProperty("flushMode", QVariant(int(QDeclarativeVideoOutput::FirstFrame))); - QCOMPARE(videoOutput->property("fillMode").value<QDeclarativeVideoOutput::FlushMode>(), QDeclarativeVideoOutput::FirstFrame); - QCOMPARE(propSpy.count(), 1); -} - -void tst_QDeclarativeVideoOutput::orientation() -{ - QQmlComponent component(&m_engine); - component.loadUrl(QUrl("qrc:/main.qml")); - - QObject *videoOutput = component.create(); - QVERIFY(videoOutput != 0); - - QSignalSpy propSpy(videoOutput, SIGNAL(orientationChanged())); - - // Default orientation is 0 - QCOMPARE(videoOutput->property("orientation").toInt(), 0); - QCOMPARE(propSpy.count(), 0); - - videoOutput->setProperty("orientation", QVariant(90)); - QCOMPARE(videoOutput->property("orientation").toInt(), 90); - QCOMPARE(propSpy.count(), 1); - - videoOutput->setProperty("orientation", QVariant(180)); - QCOMPARE(videoOutput->property("orientation").toInt(), 180); - QCOMPARE(propSpy.count(), 2); - - videoOutput->setProperty("orientation", QVariant(270)); - QCOMPARE(videoOutput->property("orientation").toInt(), 270); - QCOMPARE(propSpy.count(), 3); - - videoOutput->setProperty("orientation", QVariant(360)); - QCOMPARE(videoOutput->property("orientation").toInt(), 360); - QCOMPARE(propSpy.count(), 4); - - // More than 360 should be fine - videoOutput->setProperty("orientation", QVariant(540)); - QCOMPARE(videoOutput->property("orientation").toInt(), 540); - QCOMPARE(propSpy.count(), 5); - - // Negative should be fine - videoOutput->setProperty("orientation", QVariant(-180)); - QCOMPARE(videoOutput->property("orientation").toInt(), -180); - QCOMPARE(propSpy.count(), 6); - - // Same value should not reemit - videoOutput->setProperty("orientation", QVariant(-180)); - QCOMPARE(videoOutput->property("orientation").toInt(), -180); - QCOMPARE(propSpy.count(), 6); - - // Non multiples of 90 should not work - videoOutput->setProperty("orientation", QVariant(-1)); - QCOMPARE(videoOutput->property("orientation").toInt(), -180); - QCOMPARE(propSpy.count(), 6); - - delete videoOutput; -} - -void tst_QDeclarativeVideoOutput::surfaceSource() -{ - QQmlComponent component(&m_engine); - component.loadUrl(QUrl("qrc:/main.qml")); - - QObject *videoOutput = component.create(); - QVERIFY(videoOutput != 0); - - SurfaceHolder holder(this); - - QCOMPARE(holder.videoSurface(), static_cast<QAbstractVideoSurface*>(0)); - - videoOutput->setProperty("source", QVariant::fromValue(static_cast<QObject*>(&holder))); - - QVERIFY(holder.videoSurface() != 0); - - // Now we could do things with the surface.. - const QList<QVideoFrame::PixelFormat> formats = holder.videoSurface()->supportedPixelFormats(); - QVERIFY(formats.count() > 0); - - // See if we can start and stop each pixel format (..) - for (QVideoFrame::PixelFormat format : formats) { - QVideoSurfaceFormat surfaceFormat(QSize(200,100), format); - QVERIFY(holder.videoSurface()->isFormatSupported(surfaceFormat)); // This does kind of depend on node factories - - QVERIFY(holder.videoSurface()->start(surfaceFormat)); - QVERIFY(holder.videoSurface()->surfaceFormat() == surfaceFormat); - QVERIFY(holder.videoSurface()->isActive()); - - holder.videoSurface()->stop(); - - QVERIFY(!holder.videoSurface()->isActive()); - } - - delete videoOutput; - - // This should clear the surface - QCOMPARE(holder.videoSurface(), static_cast<QAbstractVideoSurface*>(0)); - - // Also, creating two sources, setting them in order, and destroying the first - // should not zero holder.videoSurface() - videoOutput = component.create(); - videoOutput->setProperty("source", QVariant::fromValue(static_cast<QObject*>(&holder))); - - QAbstractVideoSurface *surface = holder.videoSurface(); - QVERIFY(holder.videoSurface()); - - QObject *videoOutput2 = component.create(); - QVERIFY(videoOutput2); - videoOutput2->setProperty("source", QVariant::fromValue(static_cast<QObject*>(&holder))); - QVERIFY(holder.videoSurface()); - QVERIFY(holder.videoSurface() != surface); // Surface should have changed - surface = holder.videoSurface(); - - // Now delete first one - delete videoOutput; - QVERIFY(holder.videoSurface()); - QVERIFY(holder.videoSurface() == surface); // Should not have changed surface - - // Now create a second surface and assign it as the source - // The old surface holder should be zeroed - SurfaceHolder holder2(this); - videoOutput2->setProperty("source", QVariant::fromValue(static_cast<QObject*>(&holder2))); - - QCOMPARE(holder.videoSurface(), static_cast<QAbstractVideoSurface*>(0)); - QVERIFY(holder2.videoSurface() != 0); - - // Finally a combination - set the same source to two things, then assign a new source - // to the first output - should not reset the first source - videoOutput = component.create(); - videoOutput->setProperty("source", QVariant::fromValue(static_cast<QObject*>(&holder2))); - - // Both vo and vo2 were pointed to holder2 - setting vo2 should not clear holder2 - QVERIFY(holder2.videoSurface() != 0); - QVERIFY(holder.videoSurface() == 0); - videoOutput2->setProperty("source", QVariant::fromValue(static_cast<QObject*>(&holder))); - QVERIFY(holder2.videoSurface() != 0); - QVERIFY(holder.videoSurface() != 0); - - // They should also be independent - QVERIFY(holder.videoSurface() != holder2.videoSurface()); - - delete videoOutput; - delete videoOutput2; -} - -static const uchar rgb32ImageData[] = -{// B G R A - 0x00, 0x01, 0x02, 0xff, 0x03, 0x04, 0x05, 0xff, - 0x06, 0x07, 0x08, 0xff, 0x09, 0x0a, 0x0b, 0xff -}; - -void tst_QDeclarativeVideoOutput::paintSurface() -{ - QQuickView window; - window.setSource(QUrl("qrc:/main.qml")); - window.show(); - QVERIFY(QTest::qWaitForWindowExposed(&window)); - - auto videoOutput = qobject_cast<QDeclarativeVideoOutput *>(window.rootObject()); - QVERIFY(videoOutput); - - auto surface = videoOutput->property("videoSurface").value<QAbstractVideoSurface *>(); - QVERIFY(surface); - QVERIFY(!surface->isActive()); - videoOutput->setSize(QSize(2, 2)); - QVideoSurfaceFormat format(QSize(2, 2), QVideoFrame::Format_RGB32); - QVERIFY(surface->isFormatSupported(format)); - QVERIFY(surface->start(format)); - QVERIFY(surface->isActive()); - - QImage img(rgb32ImageData, 2, 2, 8, QImage::Format_RGB32); - QVERIFY(surface->present(img)); -} - -void tst_QDeclarativeVideoOutput::sourceRect() -{ - QQmlComponent component(&m_engine); - component.loadUrl(QUrl("qrc:/main.qml")); - - QObject *videoOutput = component.create(); - QVERIFY(videoOutput != 0); - - SurfaceHolder holder(this); - - QSignalSpy propSpy(videoOutput, SIGNAL(sourceRectChanged())); - - videoOutput->setProperty("source", QVariant::fromValue(static_cast<QObject*>(&holder))); - - QRectF invalid(0,0,-1,-1); - - QCOMPARE(videoOutput->property("sourceRect").toRectF(), invalid); - - holder.presentDummyFrame(QSize(200,100)); - - QCOMPARE(videoOutput->property("sourceRect").toRectF(), QRectF(0, 0, 200, 100)); - QCOMPARE(propSpy.count(), 1); - - // Another frame shouldn't cause a source rect change - holder.presentDummyFrame(QSize(200,100)); - QCOMPARE(propSpy.count(), 1); - QCOMPARE(videoOutput->property("sourceRect").toRectF(), QRectF(0, 0, 200, 100)); - - // Changing orientation and stretch modes should not affect this - videoOutput->setProperty("orientation", QVariant(90)); - updateOutputGeometry(videoOutput); - QCOMPARE(videoOutput->property("sourceRect").toRectF(), QRectF(0, 0, 200, 100)); - - videoOutput->setProperty("orientation", QVariant(180)); - updateOutputGeometry(videoOutput); - QCOMPARE(videoOutput->property("sourceRect").toRectF(), QRectF(0, 0, 200, 100)); - - videoOutput->setProperty("orientation", QVariant(270)); - updateOutputGeometry(videoOutput); - QCOMPARE(videoOutput->property("sourceRect").toRectF(), QRectF(0, 0, 200, 100)); - - videoOutput->setProperty("orientation", QVariant(-90)); - updateOutputGeometry(videoOutput); - QCOMPARE(videoOutput->property("sourceRect").toRectF(), QRectF(0, 0, 200, 100)); - - videoOutput->setProperty("fillMode", QVariant(int(QDeclarativeVideoOutput::PreserveAspectCrop))); - updateOutputGeometry(videoOutput); - QCOMPARE(videoOutput->property("sourceRect").toRectF(), QRectF(0, 0, 200, 100)); - - videoOutput->setProperty("fillMode", QVariant(int(QDeclarativeVideoOutput::Stretch))); - updateOutputGeometry(videoOutput); - QCOMPARE(videoOutput->property("sourceRect").toRectF(), QRectF(0, 0, 200, 100)); - - videoOutput->setProperty("fillMode", QVariant(int(QDeclarativeVideoOutput::Stretch))); - updateOutputGeometry(videoOutput); - QCOMPARE(videoOutput->property("sourceRect").toRectF(), QRectF(0, 0, 200, 100)); - - delete videoOutput; -} - -void tst_QDeclarativeVideoOutput::mappingPoint() -{ - QFETCH(QPointF, point); - QFETCH(int, orientation); - QFETCH(QDeclarativeVideoOutput::FillMode, fillMode); - QFETCH(QPointF, expected); - - QVERIFY(m_mappingOutput); - m_mappingOutput->setProperty("orientation", QVariant(orientation)); - m_mappingOutput->setProperty("fillMode", QVariant::fromValue(fillMode)); - - updateOutputGeometry(m_mappingOutput); - - QPointF output = invokeP2P(m_mappingOutput, "mapPointToItem", point); - QPointF reverse = invokeP2P(m_mappingOutput, "mapPointToSource", output); - - QCOMPARE(output, expected); - QCOMPARE(reverse, point); - - // Now the normalized versions - // Source rectangle is 200x100 - QPointF normal(point.x() / 200, point.y() / 100); - - output = invokeP2P(m_mappingOutput, "mapNormalizedPointToItem", normal); - reverse = invokeP2P(m_mappingOutput, "mapPointToSourceNormalized", output); - - QCOMPARE(output, expected); - QCOMPARE(reverse, normal); -} - -void tst_QDeclarativeVideoOutput::mappingPoint_data() -{ - QTest::addColumn<QPointF>("point"); - QTest::addColumn<int>("orientation"); - QTest::addColumn<QDeclarativeVideoOutput::FillMode>("fillMode"); - QTest::addColumn<QPointF>("expected"); - - QDeclarativeVideoOutput::FillMode stretch = QDeclarativeVideoOutput::Stretch; - QDeclarativeVideoOutput::FillMode fit = QDeclarativeVideoOutput::PreserveAspectFit; - QDeclarativeVideoOutput::FillMode crop = QDeclarativeVideoOutput::PreserveAspectCrop; - - // First make sure the component has processed the frame - QCOMPARE(m_mappingOutput->property("sourceRect").toRectF(), QRectF(0,0,200,100)); - - // 200x100 -> 150,100 stretch, 150x75 fit @ 12.5f, 200x100 @-25,0 crop - - // Corners, then the center, then a point in the middle somewhere - QTest::newRow("s0-0") << QPointF(0,0) << 0 << stretch << QPointF(0,0); - QTest::newRow("s1-0") << QPointF(200,0) << 0 << stretch << QPointF(150,0); - QTest::newRow("s2-0") << QPointF(0,100) << 0 << stretch << QPointF(0,100); - QTest::newRow("s3-0") << QPointF(200,100) << 0 << stretch << QPointF(150,100); - QTest::newRow("s4-0") << QPointF(100,50) << 0 << stretch << QPointF(75,50); - QTest::newRow("s5-0") << QPointF(40,80) << 0 << stretch << QPointF(30,80); - - QTest::newRow("f0-0") << QPointF(0,0) << 0 << fit << QPointF(0,12.5f); - QTest::newRow("f1-0") << QPointF(200,0) << 0 << fit << QPointF(150,12.5f); - QTest::newRow("f2-0") << QPointF(0,100) << 0 << fit << QPointF(0,87.5f); - QTest::newRow("f3-0") << QPointF(200,100) << 0 << fit << QPointF(150,87.5f); - QTest::newRow("f4-0") << QPointF(100,50) << 0 << stretch << QPointF(75,50); - QTest::newRow("f5-0") << QPointF(40,80) << 0 << stretch << QPointF(30,80); - - QTest::newRow("c0-0") << QPointF(0,0) << 0 << crop << QPointF(-25,0); - QTest::newRow("c1-0") << QPointF(200,0) << 0 << crop << QPointF(175,0); - QTest::newRow("c2-0") << QPointF(0,100) << 0 << crop << QPointF(-25,100); - QTest::newRow("c3-0") << QPointF(200,100) << 0 << crop << QPointF(175,100); - QTest::newRow("c4-0") << QPointF(100,50) << 0 << stretch << QPointF(75,50); - QTest::newRow("c5-0") << QPointF(40,80) << 0 << stretch << QPointF(30,80); - - // 90 degrees (anti clockwise) - QTest::newRow("s0-90") << QPointF(0,0) << 90 << stretch << QPointF(0,100); - QTest::newRow("s1-90") << QPointF(200,0) << 90 << stretch << QPointF(0,0); - QTest::newRow("s2-90") << QPointF(0,100) << 90 << stretch << QPointF(150,100); - QTest::newRow("s3-90") << QPointF(200,100) << 90 << stretch << QPointF(150,0); - QTest::newRow("s4-90") << QPointF(100,50) << 90 << stretch << QPointF(75,50); - QTest::newRow("s5-90") << QPointF(40,80) << 90 << stretch << QPointF(120,80); - - QTest::newRow("f0-90") << QPointF(0,0) << 90 << fit << QPointF(50,100); - QTest::newRow("f1-90") << QPointF(200,0) << 90 << fit << QPointF(50,0); - QTest::newRow("f2-90") << QPointF(0,100) << 90 << fit << QPointF(100,100); - QTest::newRow("f3-90") << QPointF(200,100) << 90 << fit << QPointF(100,0); - QTest::newRow("f4-90") << QPointF(100,50) << 90 << fit << QPointF(75,50); - QTest::newRow("f5-90") << QPointF(40,80) << 90 << fit << QPointF(90,80); - - QTest::newRow("c0-90") << QPointF(0,0) << 90 << crop << QPointF(0,200); - QTest::newRow("c1-90") << QPointF(200,0) << 90 << crop << QPointF(0,-100); - QTest::newRow("c2-90") << QPointF(0,100) << 90 << crop << QPointF(150,200); - QTest::newRow("c3-90") << QPointF(200,100) << 90 << crop << QPointF(150,-100); - QTest::newRow("c4-90") << QPointF(100,50) << 90 << crop << QPointF(75,50); - QTest::newRow("c5-90") << QPointF(40,80) << 90 << crop << QPointF(120,140); - - // 180 - QTest::newRow("s0-180") << QPointF(0,0) << 180 << stretch << QPointF(150,100); - QTest::newRow("s1-180") << QPointF(200,0) << 180 << stretch << QPointF(0,100); - QTest::newRow("s2-180") << QPointF(0,100) << 180 << stretch << QPointF(150,0); - QTest::newRow("s3-180") << QPointF(200,100) << 180 << stretch << QPointF(0,0); - QTest::newRow("s4-180") << QPointF(100,50) << 180 << stretch << QPointF(75,50); - QTest::newRow("s5-180") << QPointF(40,80) << 180 << stretch << QPointF(120,20); - - QTest::newRow("f0-180") << QPointF(0,0) << 180 << fit << QPointF(150,87.5f); - QTest::newRow("f1-180") << QPointF(200,0) << 180 << fit << QPointF(0,87.5f); - QTest::newRow("f2-180") << QPointF(0,100) << 180 << fit << QPointF(150,12.5f); - QTest::newRow("f3-180") << QPointF(200,100) << 180 << fit << QPointF(0,12.5f); - QTest::newRow("f4-180") << QPointF(100,50) << 180 << fit << QPointF(75,50); - QTest::newRow("f5-180") << QPointF(40,80) << 180 << fit << QPointF(120,27.5f); - - QTest::newRow("c0-180") << QPointF(0,0) << 180 << crop << QPointF(175,100); - QTest::newRow("c1-180") << QPointF(200,0) << 180 << crop << QPointF(-25,100); - QTest::newRow("c2-180") << QPointF(0,100) << 180 << crop << QPointF(175,0); - QTest::newRow("c3-180") << QPointF(200,100) << 180 << crop << QPointF(-25,0); - QTest::newRow("c4-180") << QPointF(100,50) << 180 << crop << QPointF(75,50); - QTest::newRow("c5-180") << QPointF(40,80) << 180 << crop << QPointF(135,20); - - // 270 - QTest::newRow("s0-270") << QPointF(0,0) << 270 << stretch << QPointF(150,0); - QTest::newRow("s1-270") << QPointF(200,0) << 270 << stretch << QPointF(150,100); - QTest::newRow("s2-270") << QPointF(0,100) << 270 << stretch << QPointF(0,0); - QTest::newRow("s3-270") << QPointF(200,100) << 270 << stretch << QPointF(0,100); - QTest::newRow("s4-270") << QPointF(100,50) << 270 << stretch << QPointF(75,50); - QTest::newRow("s5-270") << QPointF(40,80) << 270 << stretch << QPointF(30,20); - - QTest::newRow("f0-270") << QPointF(0,0) << 270 << fit << QPointF(100,0); - QTest::newRow("f1-270") << QPointF(200,0) << 270 << fit << QPointF(100,100); - QTest::newRow("f2-270") << QPointF(0,100) << 270 << fit << QPointF(50,0); - QTest::newRow("f3-270") << QPointF(200,100) << 270 << fit << QPointF(50,100); - QTest::newRow("f4-270") << QPointF(100,50) << 270 << fit << QPointF(75,50); - QTest::newRow("f5-270") << QPointF(40,80) << 270 << fit << QPointF(60,20); - - QTest::newRow("c0-270") << QPointF(0,0) << 270 << crop << QPointF(150,-100); - QTest::newRow("c1-270") << QPointF(200,0) << 270 << crop << QPointF(150,200); - QTest::newRow("c2-270") << QPointF(0,100) << 270 << crop << QPointF(0,-100); - QTest::newRow("c3-270") << QPointF(200,100) << 270 << crop << QPointF(0,200); - QTest::newRow("c4-270") << QPointF(100,50) << 270 << crop << QPointF(75,50); - QTest::newRow("c5-270") << QPointF(40,80) << 270 << crop << QPointF(30,-40); -} - -/* Test all rectangle mapping */ -void tst_QDeclarativeVideoOutput::mappingRect() -{ - QFETCH(QRectF, rect); - QFETCH(int, orientation); - QFETCH(QDeclarativeVideoOutput::FillMode, fillMode); - QFETCH(QRectF, expected); - - QVERIFY(m_mappingOutput); - m_mappingOutput->setProperty("orientation", QVariant(orientation)); - m_mappingOutput->setProperty("fillMode", QVariant::fromValue(fillMode)); - - updateOutputGeometry(m_mappingOutput); - - QRectF output = invokeR2R(m_mappingOutput, "mapRectToItem", rect); - QRectF reverse = invokeR2R(m_mappingOutput, "mapRectToSource", output); - - QCOMPARE(output, expected); - QCOMPARE(reverse, rect); - - // Now the normalized versions - // Source rectangle is 200x100 - QRectF normal(rect.x() / 200, rect.y() / 100, rect.width() / 200, rect.height() / 100); - - output = invokeR2R(m_mappingOutput, "mapNormalizedRectToItem", normal); - reverse = invokeR2R(m_mappingOutput, "mapRectToSourceNormalized", output); - - QCOMPARE(output, expected); - QCOMPARE(reverse, normal); -} - -void tst_QDeclarativeVideoOutput::mappingRect_data() -{ - QTest::addColumn<QRectF>("rect"); - QTest::addColumn<int>("orientation"); - QTest::addColumn<QDeclarativeVideoOutput::FillMode>("fillMode"); - QTest::addColumn<QRectF>("expected"); - - // First make sure the component has processed the frame - QCOMPARE(m_mappingOutput->property("sourceRect").toRectF(), QRectF(0,0,200,100)); - - QDeclarativeVideoOutput::FillMode stretch = QDeclarativeVideoOutput::Stretch; - QDeclarativeVideoOutput::FillMode fit = QDeclarativeVideoOutput::PreserveAspectFit; - QDeclarativeVideoOutput::FillMode crop = QDeclarativeVideoOutput::PreserveAspectCrop; - - // Full rectangle mapping - // Stretch - QTest::newRow("s0") << QRectF(0,0, 200, 100) << 0 << stretch << QRectF(0,0,150,100); - QTest::newRow("s90") << QRectF(0,0, 200, 100) << 90 << stretch << QRectF(0,0,150,100); - QTest::newRow("s180") << QRectF(0,0, 200, 100) << 180 << stretch << QRectF(0,0,150,100); - QTest::newRow("s270") << QRectF(0,0, 200, 100) << 270 << stretch << QRectF(0,0,150,100); - - // Fit - QTest::newRow("f0") << QRectF(0,0, 200, 100) << 0 << fit << QRectF(0,12.5f,150,75); - QTest::newRow("f90") << QRectF(0,0, 200, 100) << 90 << fit << QRectF(50,0,50,100); - QTest::newRow("f180") << QRectF(0,0, 200, 100) << 180 << fit << QRectF(0,12.5f,150,75); - QTest::newRow("f270") << QRectF(0,0, 200, 100) << 270 << fit << QRectF(50,0,50,100); - - // Crop - QTest::newRow("c0") << QRectF(0,0, 200, 100) << 0 << crop << QRectF(-25,0,200,100); - QTest::newRow("c90") << QRectF(0,0, 200, 100) << 90 << crop << QRectF(0,-100,150,300); - QTest::newRow("c180") << QRectF(0,0, 200, 100) << 180 << crop << QRectF(-25,0,200,100); - QTest::newRow("c270") << QRectF(0,0, 200, 100) << 270 << crop << QRectF(0,-100,150,300); - - // Partial rectangle mapping - // Stretch - // 50-130 in x (0.25 - 0.65), 25-50 (0.25 - 0.5) in y (out of 200, 100) -> 150x100 - QTest::newRow("p-s0") << QRectF(50, 25, 80, 25) << 0 << stretch << QRectF(37.5f,25,60,25); - QTest::newRow("p-s90") << QRectF(50, 25, 80, 25) << 90 << stretch << QRectF(37.5f,35,37.5f,40); - QTest::newRow("p-s180") << QRectF(50, 25, 80, 25) << 180 << stretch << QRectF(52.5f,50,60,25); - QTest::newRow("p-s270") << QRectF(50, 25, 80, 25) << 270 << stretch << QRectF(75,25,37.5f,40); - - // Fit - QTest::newRow("p-f0") << QRectF(50, 25, 80, 25) << 0 << fit << QRectF(37.5f,31.25f,60,18.75f); - QTest::newRow("p-f90") << QRectF(50, 25, 80, 25) << 90 << fit << QRectF(62.5f,35,12.5f,40); - QTest::newRow("p-f180") << QRectF(50, 25, 80, 25) << 180 << fit << QRectF(52.5f,50,60,18.75f); - QTest::newRow("p-f270") << QRectF(50, 25, 80, 25) << 270 << fit << QRectF(75,25,12.5f,40); - - // Crop - QTest::newRow("p-c0") << QRectF(50, 25, 80, 25) << 0 << crop << QRectF(25,25,80,25); - QTest::newRow("p-c90") << QRectF(50, 25, 80, 25) << 90 << crop << QRectF(37.5f,5,37.5f,120); - QTest::newRow("p-c180") << QRectF(50, 25, 80, 25) << 180 << crop << QRectF(45,50,80,25); - QTest::newRow("p-c270") << QRectF(50, 25, 80, 25) << 270 << crop << QRectF(75,-25,37.5f,120); -} - -void tst_QDeclarativeVideoOutput::updateOutputGeometry(QObject *output) -{ - // Since the object isn't visible, update() doesn't do anything - // so we manually force this - QMetaObject::invokeMethod(output, "_q_updateGeometry"); -} - -void tst_QDeclarativeVideoOutput::contentRect() -{ - QFETCH(int, orientation); - QFETCH(QDeclarativeVideoOutput::FillMode, fillMode); - QFETCH(QRectF, expected); - - QVERIFY(m_mappingOutput); - m_mappingOutput->setProperty("orientation", QVariant(orientation)); - m_mappingOutput->setProperty("fillMode", QVariant::fromValue(fillMode)); - - updateOutputGeometry(m_mappingOutput); - - QRectF output = m_mappingOutput->property("contentRect").toRectF(); - QCOMPARE(output, expected); -} - -void tst_QDeclarativeVideoOutput::contentRect_data() -{ - QTest::addColumn<int>("orientation"); - QTest::addColumn<QDeclarativeVideoOutput::FillMode>("fillMode"); - QTest::addColumn<QRectF>("expected"); - - // First make sure the component has processed the frame - QCOMPARE(m_mappingOutput->property("sourceRect").toRectF(), QRectF(0,0,200,100)); - - QDeclarativeVideoOutput::FillMode stretch = QDeclarativeVideoOutput::Stretch; - QDeclarativeVideoOutput::FillMode fit = QDeclarativeVideoOutput::PreserveAspectFit; - QDeclarativeVideoOutput::FillMode crop = QDeclarativeVideoOutput::PreserveAspectCrop; - - // Stretch just keeps the full render rect regardless of orientation - QTest::newRow("s0") << 0 << stretch << QRectF(0,0,150,100); - QTest::newRow("s90") << 90 << stretch << QRectF(0,0,150,100); - QTest::newRow("s180") << 180 << stretch << QRectF(0,0,150,100); - QTest::newRow("s270") << 270 << stretch << QRectF(0,0,150,100); - - // Fit depends on orientation - // Source is 200x100, fitting in 150x100 -> 150x75 - // or 100x200 -> 50x100 - QTest::newRow("f0") << 0 << fit << QRectF(0,12.5f,150,75); - QTest::newRow("f90") << 90 << fit << QRectF(50,0,50,100); - QTest::newRow("f180") << 180 << fit << QRectF(0,12.5,150,75); - QTest::newRow("f270") << 270 << fit << QRectF(50,0,50,100); - - // Crop also depends on orientation, may go outside render rect - // 200x100 -> -25,0 200x100 - // 100x200 -> 0,-100 150x300 - QTest::newRow("c0") << 0 << crop << QRectF(-25,0,200,100); - QTest::newRow("c90") << 90 << crop << QRectF(0,-100,150,300); - QTest::newRow("c180") << 180 << crop << QRectF(-25,0,200,100); - QTest::newRow("c270") << 270 << crop << QRectF(0,-100,150,300); -} - - -QRectF tst_QDeclarativeVideoOutput::invokeR2R(QObject *object, const char *signature, const QRectF &rect) -{ - QRectF r; - QMetaObject::invokeMethod(object, signature, Q_RETURN_ARG(QRectF, r), Q_ARG(QRectF, rect)); - return r; -} - -QPointF tst_QDeclarativeVideoOutput::invokeP2P(QObject *object, const char *signature, const QPointF &point) -{ - QPointF p; - QMetaObject::invokeMethod(object, signature, Q_RETURN_ARG(QPointF, p), Q_ARG(QPointF, point)); - return p; -} - - -QTEST_MAIN(tst_QDeclarativeVideoOutput) - -#include "tst_qdeclarativevideooutput.moc" diff --git a/tests/auto/integration/qdeclarativevideooutput_window/qdeclarativevideooutput_window.pro b/tests/auto/integration/qdeclarativevideooutput_window/qdeclarativevideooutput_window.pro deleted file mode 100644 index eeb1c0135..000000000 --- a/tests/auto/integration/qdeclarativevideooutput_window/qdeclarativevideooutput_window.pro +++ /dev/null @@ -1,11 +0,0 @@ -TARGET = tst_qdeclarativevideooutput_window - -QT += multimedia-private qml testlib quick qtmultimediaquicktools-private -CONFIG += testcase - -RESOURCES += qml.qrc - -SOURCES += \ - tst_qdeclarativevideooutput_window.cpp - -INCLUDEPATH += ../../../../src/imports/multimedia diff --git a/tests/auto/integration/qdeclarativevideooutput_window/qml.qrc b/tests/auto/integration/qdeclarativevideooutput_window/qml.qrc deleted file mode 100644 index 5f6483ac3..000000000 --- a/tests/auto/integration/qdeclarativevideooutput_window/qml.qrc +++ /dev/null @@ -1,5 +0,0 @@ -<RCC> - <qresource prefix="/"> - <file>main.qml</file> - </qresource> -</RCC> diff --git a/tests/auto/integration/qdeclarativevideooutput_window/tst_qdeclarativevideooutput_window.cpp b/tests/auto/integration/qdeclarativevideooutput_window/tst_qdeclarativevideooutput_window.cpp deleted file mode 100644 index 645b5d3c6..000000000 --- a/tests/auto/integration/qdeclarativevideooutput_window/tst_qdeclarativevideooutput_window.cpp +++ /dev/null @@ -1,271 +0,0 @@ -/**************************************************************************** -** -** Copyright (C) 2016 The Qt Company Ltd. -** Copyright (C) 2016 Research In Motion -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:GPL-EXCEPT$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 3 as published by the Free Software -** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ - -//TESTED_COMPONENT=plugins/declarative/multimedia - -#include "private/qdeclarativevideooutput_p.h" -#include <QtCore/qobject.h> -#include <QtTest/qtest.h> -#include <QtQml/qqmlengine.h> -#include <QtQml/qqmlcomponent.h> -#include <QtQuick/qquickitem.h> -#include <QtQuick/qquickview.h> -#include <QtMultimedia/qmediaobject.h> -#include <QtMultimedia/qmediaservice.h> -#include <QtMultimedia/qvideowindowcontrol.h> - -Q_DECLARE_METATYPE(QDeclarativeVideoOutput::FillMode) - -class SourceObject : public QObject -{ - Q_OBJECT - Q_PROPERTY(QObject *mediaObject READ mediaObject CONSTANT) -public: - explicit SourceObject(QMediaObject *mediaObject, QObject *parent = 0) - : QObject(parent), m_mediaObject(mediaObject) - {} - - QObject *mediaObject() const - { return m_mediaObject; } - -private: - QMediaObject *m_mediaObject; -}; - -class QtTestWindowControl : public QVideoWindowControl -{ -public: - QtTestWindowControl() - : m_winId(0) - , m_repaintCount(0) - , m_brightness(0) - , m_contrast(0) - , m_hue(0) - , m_saturation(0) - , m_aspectRatioMode(Qt::KeepAspectRatio) - , m_fullScreen(0) - { - } - - WId winId() const { return m_winId; } - void setWinId(WId id) { m_winId = id; } - - QRect displayRect() const { return m_displayRect; } - void setDisplayRect(const QRect &rect) { m_displayRect = rect; } - - bool isFullScreen() const { return m_fullScreen; } - void setFullScreen(bool fullScreen) { emit fullScreenChanged(m_fullScreen = fullScreen); } - - int repaintCount() const { return m_repaintCount; } - void setRepaintCount(int count) { m_repaintCount = count; } - void repaint() { ++m_repaintCount; } - - QSize nativeSize() const { return m_nativeSize; } - void setNativeSize(const QSize &size) { m_nativeSize = size; emit nativeSizeChanged(); } - - Qt::AspectRatioMode aspectRatioMode() const { return m_aspectRatioMode; } - void setAspectRatioMode(Qt::AspectRatioMode mode) { m_aspectRatioMode = mode; } - - int brightness() const { return m_brightness; } - void setBrightness(int brightness) { emit brightnessChanged(m_brightness = brightness); } - - int contrast() const { return m_contrast; } - void setContrast(int contrast) { emit contrastChanged(m_contrast = contrast); } - - int hue() const { return m_hue; } - void setHue(int hue) { emit hueChanged(m_hue = hue); } - - int saturation() const { return m_saturation; } - void setSaturation(int saturation) { emit saturationChanged(m_saturation = saturation); } - -private: - WId m_winId; - int m_repaintCount; - int m_brightness; - int m_contrast; - int m_hue; - int m_saturation; - Qt::AspectRatioMode m_aspectRatioMode; - QRect m_displayRect; - QSize m_nativeSize; - bool m_fullScreen; -}; - -class QtTestVideoService : public QMediaService -{ - Q_OBJECT -public: - QtTestVideoService(QtTestWindowControl *window) - : QMediaService(0) - , windowControl(window) - {} - - QMediaControl *requestControl(const char *name) - { - if (qstrcmp(name, QVideoWindowControl_iid) == 0) - return windowControl; - return 0; - } - - void releaseControl(QMediaControl *control) - { - Q_ASSERT(control); - } - - QtTestWindowControl *windowControl; -}; - -class QtTestVideoObject : public QMediaObject -{ - Q_OBJECT -public: - explicit QtTestVideoObject(QtTestVideoService *service): - QMediaObject(0, service) - { - } -}; - -class tst_QDeclarativeVideoOutputWindow : public QObject -{ - Q_OBJECT -public: - tst_QDeclarativeVideoOutputWindow() - : QObject(0) - , m_service(new QtTestVideoService(&m_windowControl)) - , m_videoObject(m_service) - , m_sourceObject(&m_videoObject) - { - } - - ~tst_QDeclarativeVideoOutputWindow() - { - } - -public slots: - void initTestCase(); - void cleanupTestCase(); - -private slots: - void winId(); - void nativeSize(); - void aspectRatio(); - void geometryChange(); - void resetCanvas(); - -private: - QQmlEngine m_engine; - QQuickItem *m_videoItem; - QScopedPointer<QQuickItem> m_rootItem; - QtTestWindowControl m_windowControl; - QtTestVideoService *m_service; - QtTestVideoObject m_videoObject; - SourceObject m_sourceObject; - QQuickView m_view; -}; - -void tst_QDeclarativeVideoOutputWindow::initTestCase() -{ - qRegisterMetaType<QDeclarativeVideoOutput::FillMode>(); - - QQmlComponent component(&m_engine); - component.loadUrl(QUrl("qrc:/main.qml")); - - m_rootItem.reset(qobject_cast<QQuickItem *>(component.create())); - m_videoItem = m_rootItem->findChild<QQuickItem *>("videoOutput"); - QVERIFY(m_videoItem); - m_rootItem->setParentItem(m_view.contentItem()); - m_videoItem->setProperty("source", QVariant::fromValue<QObject *>(&m_sourceObject)); - - m_windowControl.setNativeSize(QSize(400, 200)); - m_view.resize(200, 200); - m_view.show(); -} - -void tst_QDeclarativeVideoOutputWindow::cleanupTestCase() -{ - // Make sure that QDeclarativeVideoOutput doesn't segfault when it is being destroyed after - // the service is already gone - delete m_service; - m_service = 0; - m_view.setSource(QUrl()); - m_rootItem.reset(); -} - -void tst_QDeclarativeVideoOutputWindow::winId() -{ - QCOMPARE(m_windowControl.winId(), m_view.winId()); -} - -void tst_QDeclarativeVideoOutputWindow::nativeSize() -{ - QCOMPARE(m_videoItem->implicitWidth(), qreal(400.0f)); - QCOMPARE(m_videoItem->implicitHeight(), qreal(200.0f)); -} - -void tst_QDeclarativeVideoOutputWindow::aspectRatio() -{ - const QRect expectedDisplayRect(25, 50, 150, 100); - int oldRepaintCount = m_windowControl.repaintCount(); - m_videoItem->setProperty("fillMode", QDeclarativeVideoOutput::Stretch); - QTRY_COMPARE(m_windowControl.aspectRatioMode(), Qt::IgnoreAspectRatio); - QCOMPARE(m_windowControl.displayRect(), expectedDisplayRect); - QVERIFY(m_windowControl.repaintCount() > oldRepaintCount); - - oldRepaintCount = m_windowControl.repaintCount(); - m_videoItem->setProperty("fillMode", QDeclarativeVideoOutput::PreserveAspectFit); - QTRY_COMPARE(m_windowControl.aspectRatioMode(), Qt::KeepAspectRatio); - QCOMPARE(m_windowControl.displayRect(), expectedDisplayRect); - QVERIFY(m_windowControl.repaintCount() > oldRepaintCount); - - oldRepaintCount = m_windowControl.repaintCount(); - m_videoItem->setProperty("fillMode", QDeclarativeVideoOutput::PreserveAspectCrop); - QTRY_COMPARE(m_windowControl.aspectRatioMode(), Qt::KeepAspectRatioByExpanding); - QCOMPARE(m_windowControl.displayRect(), expectedDisplayRect); - QVERIFY(m_windowControl.repaintCount() > oldRepaintCount); -} - -void tst_QDeclarativeVideoOutputWindow::geometryChange() -{ - m_videoItem->setWidth(50); - QTRY_COMPARE(m_windowControl.displayRect(), QRect(25, 50, 50, 100)); - - m_videoItem->setX(30); - QTRY_COMPARE(m_windowControl.displayRect(), QRect(30, 50, 50, 100)); -} - -void tst_QDeclarativeVideoOutputWindow::resetCanvas() -{ - m_rootItem->setParentItem(0); - QCOMPARE((int)m_windowControl.winId(), 0); -} - - -QTEST_MAIN(tst_QDeclarativeVideoOutputWindow) - -#include "tst_qdeclarativevideooutput_window.moc" diff --git a/tests/auto/integration/qmediacapturesession/BLACKLIST b/tests/auto/integration/qmediacapturesession/BLACKLIST new file mode 100644 index 000000000..550ecdd6f --- /dev/null +++ b/tests/auto/integration/qmediacapturesession/BLACKLIST @@ -0,0 +1 @@ +ci diff --git a/tests/auto/integration/qmediacapturesession/CMakeLists.txt b/tests/auto/integration/qmediacapturesession/CMakeLists.txt new file mode 100644 index 000000000..1aec26493 --- /dev/null +++ b/tests/auto/integration/qmediacapturesession/CMakeLists.txt @@ -0,0 +1,26 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +# Generated from qcamerabackend.pro. + +##################################################################### +## tst_qcamerabackend Test: +##################################################################### + +qt_internal_add_test(tst_qmediacapturesession + SOURCES + tst_qmediacapturesession.cpp + ../shared/mediabackendutils.h + INCLUDE_DIRECTORIES + ../shared/ + LIBRARIES + Qt::Gui + Qt::MultimediaPrivate + Qt::MultimediaWidgets +) + +if(QT_FEATURE_gstreamer) + set_tests_properties(tst_qmediacapturesession + PROPERTIES ENVIRONMENT "G_DEBUG=fatal_criticals" + ) +endif() diff --git a/tests/auto/integration/qmediacapturesession/tst_qmediacapturesession.cpp b/tests/auto/integration/qmediacapturesession/tst_qmediacapturesession.cpp new file mode 100644 index 000000000..4de5b7239 --- /dev/null +++ b/tests/auto/integration/qmediacapturesession/tst_qmediacapturesession.cpp @@ -0,0 +1,1278 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include <QtTest/QtTest> +#include <QtGui/QImageReader> +#include <QtCore/qurl.h> +#include <QDebug> +#include <QVideoSink> +#include <QVideoWidget> +#include <QSysInfo> + +#include <qcamera.h> +#include <qcameradevice.h> +#include <qimagecapture.h> +#include <qmediacapturesession.h> +#include <qmediaplayer.h> +#include <qmediadevices.h> +#include <qmediarecorder.h> +#include <qaudiooutput.h> +#include <qaudioinput.h> +#include <qaudiodevice.h> +#include <qaudiodecoder.h> +#include <qaudiobuffer.h> +#include <qscreencapture.h> +#include <qwindowcapture.h> +#include <qaudiobufferinput.h> +#include <qvideoframeinput.h> + +#include <qcamera.h> +#include <QMediaFormat> +#include <QtMultimediaWidgets/QVideoWidget> + +#include <mediabackendutils.h> + +QT_USE_NAMESPACE + +/* + This is the backend conformance test. + + Since it relies on platform media framework and sound hardware + it may be less stable. +*/ + +class tst_QMediaCaptureSession: public QObject +{ + Q_OBJECT + +private slots: + + void initTestCase() + { + if (qEnvironmentVariable("QTEST_ENVIRONMENT").toLower() == "ci") { +#ifdef Q_OS_ANDROID + QSKIP("SKIP initTestCase on CI, because of QTBUG-118571"); +#endif + } + } + + void testAudioMute(); + void stress_test_setup_and_teardown(); + void stress_test_setup_and_teardown_keep_session(); + void stress_test_setup_and_teardown_keep_recorder(); + void stress_test_setup_and_teardown_keep_camera(); + void stress_test_setup_and_teardown_keep_audioinput(); + void stress_test_setup_and_teardown_keep_audiooutput(); + void stress_test_setup_and_teardown_keep_video(); + + void record_video_without_preview(); + + void can_add_and_remove_AudioInput_with_and_without_AudioOutput_attached(); + void can_change_AudioDevices_on_attached_AudioInput(); + void can_change_AudioInput_during_recording(); + void disconnects_deleted_AudioInput(); + void can_move_AudioInput_between_sessions(); + + void disconnects_deleted_AudioOutput(); + void can_move_AudioOutput_between_sessions_and_player(); + + void disconnects_deleted_AudioBufferInput(); + void can_move_AudioBufferInput_between_sessions(); + + void disconnects_deleted_VideoFrameInput(); + void can_move_VideoFrameInput_between_sessions(); + + void can_add_and_remove_Camera(); + void can_move_Camera_between_sessions(); + void can_disconnect_Camera_when_recording(); + void can_add_and_remove_different_Cameras(); + void can_change_CameraDevice_on_attached_Camera(); + + void can_change_VideoOutput_with_and_without_camera(); + void can_change_VideoOutput_when_recording(); + + void can_add_and_remove_recorders(); + void can_move_Recorder_between_sessions(); + void cannot_record_without_Camera_and_AudioInput(); + void can_record_AudioInput_with_null_AudioDevice(); + void can_record_Camera_with_null_CameraDevice(); + void recording_stops_when_recorder_removed(); + + void can_add_and_remove_ImageCapture(); + void can_move_ImageCapture_between_sessions(); + void capture_is_not_available_when_Camera_is_null(); + void can_add_ImageCapture_and_capture_during_recording(); + + void can_reset_audio_input_output(); + +private: + void recordOk(QMediaCaptureSession &session); + void recordFail(QMediaCaptureSession &session); +}; + + +void tst_QMediaCaptureSession::recordOk(QMediaCaptureSession &session) +{ + QMediaRecorder recorder; + session.setRecorder(&recorder); + + QSignalSpy recorderErrorSignal(&recorder, &QMediaRecorder::errorOccurred); + QSignalSpy durationChanged(&recorder, &QMediaRecorder::durationChanged); + + recorder.record(); + QTRY_VERIFY_WITH_TIMEOUT(recorder.recorderState() == QMediaRecorder::RecordingState, 2000); + QVERIFY(durationChanged.wait(2000)); + recorder.stop(); + + QTRY_VERIFY_WITH_TIMEOUT(recorder.recorderState() == QMediaRecorder::StoppedState, 2000); + QVERIFY(recorderErrorSignal.isEmpty()); + + QString fileName = recorder.actualLocation().toLocalFile(); + QVERIFY(!fileName.isEmpty()); + QTRY_VERIFY(QFileInfo(fileName).size() > 0); + QFile(fileName).remove(); +} + +void tst_QMediaCaptureSession::recordFail(QMediaCaptureSession &session) +{ + QMediaRecorder recorder; + QSignalSpy recorderErrorSignal(&recorder, &QMediaRecorder::errorOccurred); + + session.setRecorder(&recorder); + recorder.record(); + + QTRY_VERIFY_WITH_TIMEOUT(recorderErrorSignal.size() == 1, 2000); + QTRY_VERIFY_WITH_TIMEOUT(recorder.recorderState() == QMediaRecorder::StoppedState, 2000); +} + +void tst_QMediaCaptureSession::stress_test_setup_and_teardown() +{ + for (int i = 0; i < 50; i++) { + QMediaCaptureSession session; + QMediaRecorder recorder; + QCamera camera; + QAudioInput input; + QAudioOutput output; + QVideoWidget video; + + session.setAudioInput(&input); + session.setAudioOutput(&output); + session.setRecorder(&recorder); + session.setCamera(&camera); + session.setVideoOutput(&video); + + QRandomGenerator rng; + QTest::qWait(rng.bounded(200)); + } +} + +void tst_QMediaCaptureSession::stress_test_setup_and_teardown_keep_session() +{ + QMediaCaptureSession session; + for (int i = 0; i < 50; i++) { + QMediaRecorder recorder; + QCamera camera; + QAudioInput input; + QAudioOutput output; + QVideoWidget video; + + session.setAudioInput(&input); + session.setAudioOutput(&output); + session.setRecorder(&recorder); + session.setCamera(&camera); + session.setVideoOutput(&video); + + QRandomGenerator rng; + QTest::qWait(rng.bounded(200)); + } +} + +void tst_QMediaCaptureSession::stress_test_setup_and_teardown_keep_recorder() +{ + QMediaCaptureSession session; + QMediaRecorder recorder; + for (int i = 0; i < 50; i++) { + QCamera camera; + QAudioInput input; + QAudioOutput output; + QVideoWidget video; + + session.setAudioInput(&input); + session.setAudioOutput(&output); + session.setRecorder(&recorder); + session.setCamera(&camera); + session.setVideoOutput(&video); + + QRandomGenerator rng; + QTest::qWait(rng.bounded(200)); + } +} + +void tst_QMediaCaptureSession::stress_test_setup_and_teardown_keep_camera() +{ + QCamera camera; + for (int i = 0; i < 50; i++) { + QMediaCaptureSession session; + QMediaRecorder recorder; + QAudioInput input; + QAudioOutput output; + QVideoWidget video; + + session.setAudioInput(&input); + session.setAudioOutput(&output); + session.setRecorder(&recorder); + session.setCamera(&camera); + session.setVideoOutput(&video); + + QRandomGenerator rng; + QTest::qWait(rng.bounded(200)); + } +} + +void tst_QMediaCaptureSession::stress_test_setup_and_teardown_keep_audioinput() +{ + QAudioInput input; + for (int i = 0; i < 50; i++) { + QMediaCaptureSession session; + QMediaRecorder recorder; + QCamera camera; + QAudioOutput output; + QVideoWidget video; + + session.setAudioInput(&input); + session.setAudioOutput(&output); + session.setRecorder(&recorder); + session.setCamera(&camera); + session.setVideoOutput(&video); + + QRandomGenerator rng; + QTest::qWait(rng.bounded(200)); + } +} + +void tst_QMediaCaptureSession::stress_test_setup_and_teardown_keep_audiooutput() +{ + QAudioOutput output; + for (int i = 0; i < 50; i++) { + QMediaCaptureSession session; + QMediaRecorder recorder; + QCamera camera; + QAudioInput input; + QVideoWidget video; + + session.setAudioInput(&input); + session.setAudioOutput(&output); + session.setRecorder(&recorder); + session.setCamera(&camera); + session.setVideoOutput(&video); + + QRandomGenerator rng; + QTest::qWait(rng.bounded(200)); + } +} + +void tst_QMediaCaptureSession::stress_test_setup_and_teardown_keep_video() +{ + QVideoWidget video; + for (int i = 0; i < 50; i++) { + QMediaCaptureSession session; + QMediaRecorder recorder; + QCamera camera; + QAudioInput input; + QAudioOutput output; + + session.setAudioInput(&input); + session.setAudioOutput(&output); + session.setRecorder(&recorder); + session.setCamera(&camera); + session.setVideoOutput(&video); + + QRandomGenerator rng; + QTest::qWait(rng.bounded(200)); + } +} + +void tst_QMediaCaptureSession::record_video_without_preview() +{ + QCamera camera; + + if (!camera.isAvailable()) + QSKIP("No video input is available"); + + QMediaRecorder recorder; + QMediaCaptureSession session; + + session.setRecorder(&recorder); + + QSignalSpy cameraChanged(&session, &QMediaCaptureSession::cameraChanged); + + session.setCamera(&camera); + camera.setActive(true); + QTRY_COMPARE(cameraChanged.size(), 1); + QTRY_COMPARE(camera.isActive(), true); + + recordOk(session); + QVERIFY(!QTest::currentTestFailed()); + + session.setCamera(nullptr); + QTRY_COMPARE(cameraChanged.size(), 2); + + // can't record without audio and video + recordFail(session); + QVERIFY(!QTest::currentTestFailed()); +} + +void tst_QMediaCaptureSession::can_add_and_remove_AudioInput_with_and_without_AudioOutput_attached() +{ + QAudioInput input; + if (input.device().isNull()) + QSKIP("No audio input available"); + + QMediaCaptureSession session; + QSignalSpy audioInputChanged(&session, &QMediaCaptureSession::audioInputChanged); + QSignalSpy audioOutputChanged(&session, &QMediaCaptureSession::audioOutputChanged); + + session.setAudioInput(&input); + QTRY_COMPARE(audioInputChanged.size(), 1); + session.setAudioInput(nullptr); + QTRY_COMPARE(audioInputChanged.size(), 2); + + QAudioOutput output; + if (output.device().isNull()) + return; + + session.setAudioOutput(&output); + QTRY_COMPARE(audioOutputChanged.size(), 1); + + session.setAudioInput(&input); + QTRY_COMPARE(audioInputChanged.size(), 3); + + session.setAudioOutput(nullptr); + QTRY_COMPARE(audioOutputChanged.size(), 2); + + session.setAudioInput(nullptr); + QTRY_COMPARE(audioInputChanged.size(), 4); +} + +void tst_QMediaCaptureSession::can_change_AudioDevices_on_attached_AudioInput() +{ + auto audioInputs = QMediaDevices::audioInputs(); + if (audioInputs.size() < 2) + QSKIP("Two audio inputs are not available"); + + QAudioInput input(audioInputs[0]); + QSignalSpy deviceChanged(&input, &QAudioInput::deviceChanged); + + QMediaCaptureSession session; + QSignalSpy audioInputChanged(&session, &QMediaCaptureSession::audioInputChanged); + + session.setAudioInput(&input); + QTRY_COMPARE(audioInputChanged.size(), 1); + + recordOk(session); + QVERIFY(!QTest::currentTestFailed()); + + input.setDevice(audioInputs[1]); + QTRY_COMPARE(deviceChanged.size(), 1); + + recordOk(session); + QVERIFY(!QTest::currentTestFailed()); + + input.setDevice(audioInputs[0]); + QTRY_COMPARE(deviceChanged.size(), 2); + + recordOk(session); + QVERIFY(!QTest::currentTestFailed()); +} + +void tst_QMediaCaptureSession::can_change_AudioInput_during_recording() +{ + QAudioInput input; + if (input.device().isNull()) + QSKIP("No audio input available"); + + QMediaRecorder recorder; + QMediaCaptureSession session; + + session.setRecorder(&recorder); + + QSignalSpy audioInputChanged(&session, &QMediaCaptureSession::audioInputChanged); + QSignalSpy recorderErrorSignal(&recorder, &QMediaRecorder::errorOccurred); + QSignalSpy durationChanged(&recorder, &QMediaRecorder::durationChanged); + + session.setAudioInput(&input); + QTRY_COMPARE(audioInputChanged.size(), 1); + + recorder.record(); + QTRY_VERIFY(recorder.recorderState() == QMediaRecorder::RecordingState); + QVERIFY(durationChanged.wait(2000)); + session.setAudioInput(nullptr); + QTRY_COMPARE(audioInputChanged.size(), 2); + session.setAudioInput(&input); + QTRY_COMPARE(audioInputChanged.size(), 3); + recorder.stop(); + + QTRY_VERIFY(recorder.recorderState() == QMediaRecorder::StoppedState); + QVERIFY(recorderErrorSignal.isEmpty()); + + session.setAudioInput(nullptr); + QTRY_COMPARE(audioInputChanged.size(), 4); + + QString fileName = recorder.actualLocation().toLocalFile(); + QVERIFY(!fileName.isEmpty()); + QTRY_VERIFY(QFileInfo(fileName).size() > 0); + QFile(fileName).remove(); +} + +void tst_QMediaCaptureSession::disconnects_deleted_AudioInput() +{ + if (QMediaDevices::audioInputs().isEmpty()) + QSKIP("No audio input available"); + + QMediaCaptureSession session; + QSignalSpy audioInputChanged(&session, &QMediaCaptureSession::audioInputChanged); + { + QAudioInput input; + session.setAudioInput(&input); + QTRY_COMPARE(audioInputChanged.size(), 1); + } + QVERIFY(session.audioInput() == nullptr); + QTRY_COMPARE(audioInputChanged.size(), 2); +} + +void tst_QMediaCaptureSession::can_move_AudioInput_between_sessions() +{ + if (QMediaDevices::audioInputs().isEmpty()) + QSKIP("No audio input available"); + + QMediaCaptureSession session0; + QMediaCaptureSession session1; + QSignalSpy audioInputChanged0(&session0, &QMediaCaptureSession::audioInputChanged); + QSignalSpy audioInputChanged1(&session1, &QMediaCaptureSession::audioInputChanged); + + QAudioInput input; + { + QMediaCaptureSession session2; + QSignalSpy audioInputChanged2(&session2, &QMediaCaptureSession::audioInputChanged); + session2.setAudioInput(&input); + QTRY_COMPARE(audioInputChanged2.size(), 1); + } + session0.setAudioInput(&input); + QTRY_COMPARE(audioInputChanged0.size(), 1); + QVERIFY(session0.audioInput() != nullptr); + + session1.setAudioInput(&input); + QTRY_COMPARE(audioInputChanged0.size(), 2); + QVERIFY(session0.audioInput() == nullptr); + QTRY_COMPARE(audioInputChanged1.size(), 1); + QVERIFY(session1.audioInput() != nullptr); +} + +void tst_QMediaCaptureSession::disconnects_deleted_AudioOutput() +{ + if (QMediaDevices::audioOutputs().isEmpty()) + QSKIP("No audio output available"); + + QMediaCaptureSession session; + QSignalSpy audioOutputChanged(&session, &QMediaCaptureSession::audioOutputChanged); + { + QAudioOutput output; + session.setAudioOutput(&output); + QTRY_COMPARE(audioOutputChanged.size(), 1); + } + QVERIFY(session.audioOutput() == nullptr); + QTRY_COMPARE(audioOutputChanged.size(), 2); +} + +void tst_QMediaCaptureSession::can_move_AudioOutput_between_sessions_and_player() +{ + if (QMediaDevices::audioOutputs().isEmpty()) + QSKIP("No audio output available"); + + QAudioOutput output; + + QMediaCaptureSession session0; + QMediaCaptureSession session1; + QMediaPlayer player; + QSignalSpy audioOutputChanged0(&session0, &QMediaCaptureSession::audioOutputChanged); + QSignalSpy audioOutputChanged1(&session1, &QMediaCaptureSession::audioOutputChanged); + QSignalSpy audioOutputChangedPlayer(&player, &QMediaPlayer::audioOutputChanged); + + { + QMediaCaptureSession session2; + QSignalSpy audioOutputChanged2(&session2, &QMediaCaptureSession::audioOutputChanged); + session2.setAudioOutput(&output); + QTRY_COMPARE(audioOutputChanged2.size(), 1); + } + + session0.setAudioOutput(&output); + QTRY_COMPARE(audioOutputChanged0.size(), 1); + QVERIFY(session0.audioOutput() != nullptr); + + session1.setAudioOutput(&output); + QTRY_COMPARE(audioOutputChanged0.size(), 2); + QVERIFY(session0.audioOutput() == nullptr); + QTRY_COMPARE(audioOutputChanged1.size(), 1); + QVERIFY(session1.audioOutput() != nullptr); + + player.setAudioOutput(&output); + QTRY_COMPARE(audioOutputChanged0.size(), 2); + QVERIFY(session0.audioOutput() == nullptr); + QTRY_COMPARE(audioOutputChanged1.size(), 2); + QVERIFY(session1.audioOutput() == nullptr); + QTRY_COMPARE(audioOutputChangedPlayer.size(), 1); + QVERIFY(player.audioOutput() != nullptr); + + session0.setAudioOutput(&output); + QTRY_COMPARE(audioOutputChanged0.size(), 3); + QVERIFY(session0.audioOutput() != nullptr); + QTRY_COMPARE(audioOutputChangedPlayer.size(), 2); + QVERIFY(player.audioOutput() == nullptr); +} + +void tst_QMediaCaptureSession::disconnects_deleted_AudioBufferInput() +{ + QMediaCaptureSession session; + QSignalSpy audioBufferInputChanged(&session, &QMediaCaptureSession::audioBufferInputChanged); + { + QAudioBufferInput input; + session.setAudioBufferInput(&input); + QTRY_COMPARE(audioBufferInputChanged.size(), 1); + } + QCOMPARE(session.audioBufferInput(), nullptr); + QCOMPARE(audioBufferInputChanged.size(), 2); +} + +void tst_QMediaCaptureSession::can_move_AudioBufferInput_between_sessions() +{ + QMediaCaptureSession session0; + QMediaCaptureSession session1; + QSignalSpy audioBufferInputChanged0(&session0, &QMediaCaptureSession::audioBufferInputChanged); + QSignalSpy audioBufferInputChanged1(&session1, &QMediaCaptureSession::audioBufferInputChanged); + + QAudioBufferInput input; + { + QMediaCaptureSession session2; + QSignalSpy audioBufferInputChanged2(&session2, + &QMediaCaptureSession::audioBufferInputChanged); + session2.setAudioBufferInput(&input); + QCOMPARE(audioBufferInputChanged2.size(), 1); + } + session0.setAudioBufferInput(&input); + QCOMPARE(audioBufferInputChanged0.size(), 1); + QCOMPARE(session0.audioBufferInput(), &input); + QCOMPARE(input.captureSession(), &session0); + + session1.setAudioBufferInput(&input); + + QCOMPARE(audioBufferInputChanged0.size(), 2); + QCOMPARE(session0.audioBufferInput(), nullptr); + QCOMPARE(audioBufferInputChanged1.size(), 1); + QCOMPARE(session1.audioBufferInput(), &input); + QCOMPARE(input.captureSession(), &session1); +} + +void tst_QMediaCaptureSession::disconnects_deleted_VideoFrameInput() +{ + QMediaCaptureSession session; + QSignalSpy videoFrameInputChanged(&session, &QMediaCaptureSession::videoFrameInputChanged); + { + QVideoFrameInput input; + session.setVideoFrameInput(&input); + QTRY_COMPARE(videoFrameInputChanged.size(), 1); + } + QCOMPARE(session.videoFrameInput(), nullptr); + QCOMPARE(videoFrameInputChanged.size(), 2); +} + +void tst_QMediaCaptureSession::can_move_VideoFrameInput_between_sessions() +{ + QMediaCaptureSession session0; + QMediaCaptureSession session1; + QSignalSpy videoFrameInputChanged0(&session0, &QMediaCaptureSession::videoFrameInputChanged); + QSignalSpy videoFrameInputChanged1(&session1, &QMediaCaptureSession::videoFrameInputChanged); + + QVideoFrameInput input; + { + QMediaCaptureSession session2; + QSignalSpy videoFrameInputChanged2(&session2, + &QMediaCaptureSession::videoFrameInputChanged); + session2.setVideoFrameInput(&input); + QCOMPARE(videoFrameInputChanged2.size(), 1); + } + session0.setVideoFrameInput(&input); + QCOMPARE(videoFrameInputChanged0.size(), 1); + QCOMPARE(session0.videoFrameInput(), &input); + QCOMPARE(input.captureSession(), &session0); + + session1.setVideoFrameInput(&input); + + QCOMPARE(videoFrameInputChanged0.size(), 2); + QCOMPARE(session0.videoFrameInput(), nullptr); + QCOMPARE(videoFrameInputChanged1.size(), 1); + QCOMPARE(session1.videoFrameInput(), &input); + QCOMPARE(input.captureSession(), &session1); +} + +void tst_QMediaCaptureSession::can_add_and_remove_Camera() +{ + QCamera camera; + + if (!camera.isAvailable()) + QSKIP("No video input is available"); + + QMediaRecorder recorder; + QMediaCaptureSession session; + + session.setRecorder(&recorder); + + QSignalSpy cameraChanged(&session, &QMediaCaptureSession::cameraChanged); + + session.setCamera(&camera); + camera.setActive(true); + QTRY_COMPARE(cameraChanged.size(), 1); + QTRY_COMPARE(camera.isActive(), true); + + session.setCamera(nullptr); + QTRY_COMPARE(cameraChanged.size(), 2); + + session.setCamera(&camera); + QTRY_COMPARE(cameraChanged.size(), 3); + + recordOk(session); + QVERIFY(!QTest::currentTestFailed()); +} + +void tst_QMediaCaptureSession::can_move_Camera_between_sessions() +{ + QMediaCaptureSession session0; + QMediaCaptureSession session1; + QSignalSpy cameraChanged0(&session0, &QMediaCaptureSession::cameraChanged); + QSignalSpy cameraChanged1(&session1, &QMediaCaptureSession::cameraChanged); + { + QCamera camera; + { + QMediaCaptureSession session2; + QSignalSpy cameraChanged2(&session2, &QMediaCaptureSession::cameraChanged); + session2.setCamera(&camera); + QTRY_COMPARE(cameraChanged2.size(), 1); + } + QVERIFY(camera.captureSession() == nullptr); + + session0.setCamera(&camera); + QTRY_COMPARE(cameraChanged0.size(), 1); + QVERIFY(session0.camera() == &camera); + QVERIFY(camera.captureSession() == &session0); + + session1.setCamera(&camera); + QTRY_COMPARE(cameraChanged0.size(), 2); + QVERIFY(session0.camera() == nullptr); + QTRY_COMPARE(cameraChanged1.size(), 1); + QVERIFY(session1.camera() == &camera); + QVERIFY(camera.captureSession() == &session1); + } + QTRY_COMPARE(cameraChanged1.size(), 2); + QVERIFY(session1.camera() == nullptr); +} + +void tst_QMediaCaptureSession::can_disconnect_Camera_when_recording() +{ + QCamera camera; + + if (!camera.isAvailable()) + QSKIP("No video input is available"); + + QMediaRecorder recorder; + QMediaCaptureSession session; + + session.setRecorder(&recorder); + + QSignalSpy cameraChanged(&session, &QMediaCaptureSession::cameraChanged); + QSignalSpy recorderErrorSignal(&recorder, &QMediaRecorder::errorOccurred); + QSignalSpy durationChanged(&recorder, &QMediaRecorder::durationChanged); + + camera.setActive(true); + session.setCamera(&camera); + QTRY_COMPARE(cameraChanged.size(), 1); + QTRY_COMPARE(camera.isActive(), true); + + durationChanged.clear(); + recorder.record(); + QTRY_VERIFY(recorder.recorderState() == QMediaRecorder::RecordingState); + QTRY_VERIFY(durationChanged.size() > 0); + + session.setCamera(nullptr); + QTRY_COMPARE(cameraChanged.size(), 2); + + recorder.stop(); + QTRY_VERIFY(recorder.recorderState() == QMediaRecorder::StoppedState); + QVERIFY(recorderErrorSignal.isEmpty()); + + QString fileName = recorder.actualLocation().toLocalFile(); + QVERIFY(!fileName.isEmpty()); + QTRY_VERIFY(QFileInfo(fileName).size() > 0); + QFile(fileName).remove(); +} + +void tst_QMediaCaptureSession::can_add_and_remove_different_Cameras() +{ + auto cameraDevices = QMediaDevices().videoInputs(); + + if (cameraDevices.size() < 2) + QSKIP("Two video input are not available"); + + QCamera camera(cameraDevices[0]); + QCamera camera2(cameraDevices[1]); + + QMediaRecorder recorder; + QMediaCaptureSession session; + + session.setRecorder(&recorder); + + QSignalSpy cameraChanged(&session, &QMediaCaptureSession::cameraChanged); + + camera.setActive(true); + session.setCamera(&camera); + QTRY_COMPARE(cameraChanged.size(), 1); + QTRY_COMPARE(camera.isActive(), true); + + session.setCamera(nullptr); + QTRY_COMPARE(cameraChanged.size(), 2); + + session.setCamera(&camera2); + camera2.setActive(true); + QTRY_COMPARE(cameraChanged.size(), 3); + QTRY_COMPARE(camera2.isActive(), true); + + recordOk(session); + QVERIFY(!QTest::currentTestFailed()); +} + +void tst_QMediaCaptureSession::can_change_CameraDevice_on_attached_Camera() +{ + auto cameraDevices = QMediaDevices().videoInputs(); + + if (cameraDevices.size() < 2) + QSKIP("Two video input are not available"); + + QCamera camera(cameraDevices[0]); + + QMediaRecorder recorder; + QMediaCaptureSession session; + + session.setRecorder(&recorder); + + QSignalSpy cameraDeviceChanged(&camera, &QCamera::cameraDeviceChanged); + QSignalSpy cameraChanged(&session, &QMediaCaptureSession::cameraChanged); + + session.setCamera(&camera); + QTRY_COMPARE(cameraChanged.size(), 1); + + recordFail(session); + QVERIFY(!QTest::currentTestFailed()); + + camera.setActive(true); + QTRY_COMPARE(camera.isActive(), true); + + recordOk(session); + QVERIFY(!QTest::currentTestFailed()); + + camera.setCameraDevice(cameraDevices[1]); + camera.setActive(true); + QTRY_COMPARE(cameraDeviceChanged.size(), 1); + QTRY_COMPARE(camera.isActive(), true); + + recordOk(session); + QVERIFY(!QTest::currentTestFailed()); +} + +void tst_QMediaCaptureSession::can_change_VideoOutput_with_and_without_camera() +{ + QCamera camera; + if (!camera.isAvailable()) + QSKIP("No video input is available"); + + QVideoWidget videoOutput; + QVideoWidget videoOutput2; + videoOutput.show(); + videoOutput2.show(); + + QMediaCaptureSession session; + + QSignalSpy videoOutputChanged(&session, &QMediaCaptureSession::videoOutputChanged); + QSignalSpy cameraChanged(&session, &QMediaCaptureSession::cameraChanged); + + session.setCamera(&camera); + QTRY_COMPARE(cameraChanged.size(), 1); + + session.setVideoOutput(&videoOutput); + QTRY_COMPARE(videoOutputChanged.size(), 1); + + session.setVideoOutput(nullptr); + QTRY_COMPARE(videoOutputChanged.size(), 2); + + session.setVideoOutput(&videoOutput2); + QTRY_COMPARE(videoOutputChanged.size(), 3); + + session.setCamera(nullptr); + QTRY_COMPARE(cameraChanged.size(), 2); + + session.setVideoOutput(nullptr); + QTRY_COMPARE(videoOutputChanged.size(), 4); +} + +void tst_QMediaCaptureSession::can_change_VideoOutput_when_recording() +{ + QCamera camera; + if (!camera.isAvailable()) + QSKIP("No video input is available"); + + QVideoWidget videoOutput; + videoOutput.show(); + + QMediaCaptureSession session; + QMediaRecorder recorder; + + session.setRecorder(&recorder); + + QSignalSpy cameraChanged(&session, &QMediaCaptureSession::cameraChanged); + QSignalSpy recorderErrorSignal(&recorder, &QMediaRecorder::errorOccurred); + QSignalSpy durationChanged(&recorder, &QMediaRecorder::durationChanged); + QSignalSpy videoOutputChanged(&session, &QMediaCaptureSession::videoOutputChanged); + + camera.setActive(true); + session.setCamera(&camera); + QTRY_COMPARE(cameraChanged.size(), 1); + QTRY_COMPARE(camera.isActive(), true); + + recorder.record(); + QTRY_VERIFY(recorder.recorderState() == QMediaRecorder::RecordingState); + QVERIFY(durationChanged.wait(2000)); + + session.setVideoOutput(&videoOutput); + QTRY_COMPARE(videoOutputChanged.size(), 1); + + session.setVideoOutput(nullptr); + QTRY_COMPARE(videoOutputChanged.size(), 2); + + session.setVideoOutput(&videoOutput); + QTRY_COMPARE(videoOutputChanged.size(), 3); + + recorder.stop(); + QTRY_VERIFY(recorder.recorderState() == QMediaRecorder::StoppedState); + QVERIFY(recorderErrorSignal.isEmpty()); + + QString fileName = recorder.actualLocation().toLocalFile(); + QVERIFY(!fileName.isEmpty()); + QTRY_VERIFY(QFileInfo(fileName).size() > 0); + QFile(fileName).remove(); +} + +void tst_QMediaCaptureSession::can_add_and_remove_recorders() +{ + QAudioInput input; + if (input.device().isNull()) + QSKIP("Recording source not available"); + + QMediaRecorder recorder; + QMediaRecorder recorder2; + QMediaCaptureSession session; + + QSignalSpy audioInputChanged(&session, &QMediaCaptureSession::audioInputChanged); + QSignalSpy recorderChanged(&session, &QMediaCaptureSession::recorderChanged); + + session.setAudioInput(&input); + QTRY_COMPARE(audioInputChanged.size(), 1); + + session.setRecorder(&recorder); + QTRY_COMPARE(recorderChanged.size(), 1); + + session.setRecorder(&recorder2); + QTRY_COMPARE(recorderChanged.size(), 2); + + session.setRecorder(&recorder); + QTRY_COMPARE(recorderChanged.size(), 3); + + recordOk(session); + QVERIFY(!QTest::currentTestFailed()); +} + +void tst_QMediaCaptureSession::can_move_Recorder_between_sessions() +{ + QMediaCaptureSession session0; + QMediaCaptureSession session1; + QSignalSpy recorderChanged0(&session0, &QMediaCaptureSession::recorderChanged); + QSignalSpy recorderChanged1(&session1, &QMediaCaptureSession::recorderChanged); + { + QMediaRecorder recorder; + { + QMediaCaptureSession session2; + QSignalSpy recorderChanged2(&session2, &QMediaCaptureSession::recorderChanged); + session2.setRecorder(&recorder); + QTRY_COMPARE(recorderChanged2.size(), 1); + } + QVERIFY(recorder.captureSession() == nullptr); + + session0.setRecorder(&recorder); + QTRY_COMPARE(recorderChanged0.size(), 1); + QVERIFY(session0.recorder() == &recorder); + QVERIFY(recorder.captureSession() == &session0); + + session1.setRecorder(&recorder); + QTRY_COMPARE(recorderChanged0.size(), 2); + QVERIFY(session0.recorder() == nullptr); + QTRY_COMPARE(recorderChanged1.size(), 1); + QVERIFY(session1.recorder() == &recorder); + QVERIFY(recorder.captureSession() == &session1); + } + QTRY_COMPARE(recorderChanged1.size(), 2); + QVERIFY(session1.recorder() == nullptr); +} + +void tst_QMediaCaptureSession::cannot_record_without_Camera_and_AudioInput() +{ + QMediaCaptureSession session; + recordFail(session); +} + +void tst_QMediaCaptureSession::can_record_AudioInput_with_null_AudioDevice() +{ + if (QMediaDevices().audioInputs().size() == 0) + QSKIP("No audio input is not available"); + + QAudioDevice nullDevice; + QAudioInput input(nullDevice); + + QMediaCaptureSession session; + QSignalSpy audioInputChanged(&session, &QMediaCaptureSession::audioInputChanged); + + session.setAudioInput(&input); + QTRY_COMPARE(audioInputChanged.size(), 1); + + recordOk(session); + QVERIFY(!QTest::currentTestFailed()); +} + +void tst_QMediaCaptureSession::can_record_Camera_with_null_CameraDevice() +{ + if (QMediaDevices().videoInputs().size() == 0) + QSKIP("No video input is not available"); + + QCameraDevice nullDevice; + QCamera camera(nullDevice); + + QMediaCaptureSession session; + QSignalSpy cameraChanged(&session, &QMediaCaptureSession::cameraChanged); + + session.setCamera(&camera); + QTRY_COMPARE(cameraChanged.size(), 1); + + camera.setActive(true); + QTRY_COMPARE(camera.isActive(), true); + + recordOk(session); + QVERIFY(!QTest::currentTestFailed()); +} + +void tst_QMediaCaptureSession::recording_stops_when_recorder_removed() +{ + QAudioInput input; + if (input.device().isNull()) + QSKIP("Recording source not available"); + + QMediaRecorder recorder; + QMediaCaptureSession session; + + QSignalSpy audioInputChanged(&session, &QMediaCaptureSession::audioInputChanged); + QSignalSpy recorderChanged(&session, &QMediaCaptureSession::recorderChanged); + QSignalSpy recorderErrorSignal(&recorder, &QMediaRecorder::errorOccurred); + QSignalSpy durationChanged(&recorder, &QMediaRecorder::durationChanged); + + session.setAudioInput(&input); + QTRY_COMPARE(audioInputChanged.size(), 1); + + session.setRecorder(&recorder); + QTRY_COMPARE(recorderChanged.size(), 1); + + recorder.record(); + QTRY_VERIFY(recorder.recorderState() == QMediaRecorder::RecordingState); + QVERIFY(durationChanged.wait(2000)); + + session.setRecorder(nullptr); + QTRY_COMPARE(recorderChanged.size(), 2); + + QTRY_VERIFY(recorder.recorderState() == QMediaRecorder::StoppedState); + QVERIFY(recorderErrorSignal.isEmpty()); + + QString fileName = recorder.actualLocation().toLocalFile(); + QVERIFY(!fileName.isEmpty()); + QTRY_VERIFY(QFileInfo(fileName).size() > 0); + QFile(fileName).remove(); +} + +void tst_QMediaCaptureSession::can_add_and_remove_ImageCapture() +{ + QCamera camera; + + if (!camera.isAvailable()) + QSKIP("No video input available"); + + QImageCapture capture; + QMediaCaptureSession session; + + QSignalSpy cameraChanged(&session, &QMediaCaptureSession::cameraChanged); + QSignalSpy imageCaptureChanged(&session, &QMediaCaptureSession::imageCaptureChanged); + QSignalSpy readyForCaptureChanged(&capture, &QImageCapture::readyForCaptureChanged); + + QVERIFY(!capture.isAvailable()); + QVERIFY(!capture.isReadyForCapture()); + + session.setImageCapture(&capture); + QTRY_COMPARE(imageCaptureChanged.size(), 1); + QVERIFY(!capture.isAvailable()); + QVERIFY(!capture.isReadyForCapture()); + + session.setCamera(&camera); + QTRY_COMPARE(cameraChanged.size(), 1); + QVERIFY(capture.isAvailable()); + + QVERIFY(!capture.isReadyForCapture()); + + camera.setActive(true); + QTRY_COMPARE(camera.isActive(), true); + + QTRY_COMPARE(readyForCaptureChanged.size(), 1); + QVERIFY(capture.isReadyForCapture()); + + session.setImageCapture(nullptr); + QTRY_COMPARE(imageCaptureChanged.size(), 2); + QTRY_COMPARE(readyForCaptureChanged.size(), 2); + + QVERIFY(!capture.isAvailable()); + QVERIFY(!capture.isReadyForCapture()); + + session.setImageCapture(&capture); + QTRY_COMPARE(imageCaptureChanged.size(), 3); + QTRY_COMPARE(readyForCaptureChanged.size(), 3); + QVERIFY(capture.isAvailable()); + QVERIFY(capture.isReadyForCapture()); +} + +void tst_QMediaCaptureSession::can_move_ImageCapture_between_sessions() +{ + QSKIP_GSTREAMER("QTBUG-124005: Spurious failure on CI"); + + QMediaCaptureSession session0; + QMediaCaptureSession session1; + QSignalSpy imageCaptureChanged0(&session0, &QMediaCaptureSession::imageCaptureChanged); + QSignalSpy imageCaptureChanged1(&session1, &QMediaCaptureSession::imageCaptureChanged); + { + QImageCapture imageCapture; + { + QMediaCaptureSession session2; + QSignalSpy imageCaptureChanged2(&session2, &QMediaCaptureSession::imageCaptureChanged); + session2.setImageCapture(&imageCapture); + QTRY_COMPARE(imageCaptureChanged2.size(), 1); + } + QVERIFY(imageCapture.captureSession() == nullptr); + + session0.setImageCapture(&imageCapture); + QTRY_COMPARE(imageCaptureChanged0.size(), 1); + QVERIFY(session0.imageCapture() == &imageCapture); + QVERIFY(imageCapture.captureSession() == &session0); + + session1.setImageCapture(&imageCapture); + QTRY_COMPARE(imageCaptureChanged0.size(), 2); + QVERIFY(session0.imageCapture() == nullptr); + QTRY_COMPARE(imageCaptureChanged1.size(), 1); + QVERIFY(session1.imageCapture() == &imageCapture); + QVERIFY(imageCapture.captureSession() == &session1); + } + QTRY_COMPARE(imageCaptureChanged1.size(), 2); + QVERIFY(session1.imageCapture() == nullptr); +} + +void tst_QMediaCaptureSession::capture_is_not_available_when_Camera_is_null() +{ + QCamera camera; + + if (!camera.isAvailable()) + QSKIP("No video input available"); + + QImageCapture capture; + QMediaCaptureSession session; + + QSignalSpy cameraChanged(&session, &QMediaCaptureSession::cameraChanged); + QSignalSpy capturedSignal(&capture, &QImageCapture::imageCaptured); + QSignalSpy readyForCaptureChanged(&capture, &QImageCapture::readyForCaptureChanged); + + session.setImageCapture(&capture); + session.setCamera(&camera); + camera.setActive(true); + QTRY_COMPARE(camera.isActive(), true); + + QTRY_COMPARE(readyForCaptureChanged.size(), 1); + QVERIFY(capture.isReadyForCapture()); + + QVERIFY(capture.capture() >= 0); + QTRY_COMPARE(capturedSignal.size(), 1); + + QVERIFY(capture.isReadyForCapture()); + int readyCount = readyForCaptureChanged.size(); + + session.setCamera(nullptr); + + QTRY_COMPARE(readyForCaptureChanged.size(), readyCount + 1); + QVERIFY(!capture.isReadyForCapture()); + QVERIFY(!capture.isAvailable()); + QVERIFY(capture.capture() < 0); +} + +void tst_QMediaCaptureSession::can_add_ImageCapture_and_capture_during_recording() +{ + QCamera camera; + + if (!camera.isAvailable()) + QSKIP("No video input available"); + + QImageCapture capture; + QMediaCaptureSession session; + QMediaRecorder recorder; + + QSignalSpy recorderChanged(&session, &QMediaCaptureSession::recorderChanged); + QSignalSpy recorderErrorSignal(&recorder, &QMediaRecorder::errorOccurred); + QSignalSpy durationChanged(&recorder, &QMediaRecorder::durationChanged); + QSignalSpy imageCaptureChanged(&session, &QMediaCaptureSession::imageCaptureChanged); + QSignalSpy readyForCaptureChanged(&capture, &QImageCapture::readyForCaptureChanged); + QSignalSpy capturedSignal(&capture, &QImageCapture::imageCaptured); + + session.setCamera(&camera); + camera.setActive(true); + QTRY_COMPARE(camera.isActive(), true); + + session.setRecorder(&recorder); + QTRY_COMPARE(recorderChanged.size(), 1); + + recorder.record(); + QTRY_VERIFY(recorder.recorderState() == QMediaRecorder::RecordingState); + QVERIFY(durationChanged.wait(2000)); + + session.setImageCapture(&capture); + QTRY_COMPARE(imageCaptureChanged.size(), 1); + QTRY_COMPARE(readyForCaptureChanged.size(), 1); + QVERIFY(capture.isReadyForCapture()); + + QVERIFY(capture.capture() >= 0); + QTRY_COMPARE(capturedSignal.size(), 1); + + session.setImageCapture(nullptr); + QVERIFY(readyForCaptureChanged.size() >= 2); + QVERIFY(!capture.isReadyForCapture()); + + recorder.stop(); + QTRY_VERIFY(recorder.recorderState() == QMediaRecorder::StoppedState); + QVERIFY(recorderErrorSignal.isEmpty()); + + QString fileName = recorder.actualLocation().toLocalFile(); + QVERIFY(!fileName.isEmpty()); + QTRY_VERIFY(QFileInfo(fileName).size() > 0); + QFile(fileName).remove(); +} + +void tst_QMediaCaptureSession::testAudioMute() +{ + QAudioInput audioInput; + if (audioInput.device().isNull()) + QSKIP("No audio input available"); + + QMediaRecorder recorder; + QMediaCaptureSession session; + + session.setRecorder(&recorder); + + session.setAudioInput(&audioInput); + + session.setCamera(nullptr); + recorder.setOutputLocation(QStringLiteral("test")); + + QSignalSpy spy(&audioInput, &QAudioInput::mutedChanged); + QSignalSpy durationChanged(&recorder, &QMediaRecorder::durationChanged); + + QMediaFormat format; + format.setAudioCodec(QMediaFormat::AudioCodec::Wave); + recorder.setMediaFormat(format); + + audioInput.setMuted(true); + + recorder.record(); + + QCOMPARE(spy.size(), 1); + QCOMPARE(spy.last()[0], true); + + QTRY_VERIFY_WITH_TIMEOUT(recorder.recorderState() == QMediaRecorder::RecordingState, 2000); + QVERIFY(durationChanged.wait(2000)); + + recorder.stop(); + + QTRY_COMPARE(recorder.recorderState(), QMediaRecorder::StoppedState); + + QString actualLocation = recorder.actualLocation().toLocalFile(); + + QVERIFY2(!actualLocation.isEmpty(), "Recorder did not save a file"); + QTRY_VERIFY2(QFileInfo(actualLocation).size() > 0, "Recorded file is empty (zero bytes)"); + + QAudioDecoder decoder; + QAudioBuffer buffer; + decoder.setSource(QUrl::fromLocalFile(actualLocation)); + + decoder.start(); + + // Wait a while + QTRY_VERIFY(decoder.bufferAvailable()); + + while (decoder.bufferAvailable()) { + buffer = decoder.read(); + QVERIFY(buffer.isValid()); + + const void *data = buffer.constData<void *>(); + QVERIFY(data != nullptr); + + const unsigned int *idata = reinterpret_cast<const unsigned int *>(data); + QCOMPARE(*idata, 0U); + } + + decoder.stop(); + + QFile(actualLocation).remove(); + + audioInput.setMuted(false); + + QCOMPARE(spy.size(), 2); + QCOMPARE(spy.last()[0], false); +} + +void tst_QMediaCaptureSession::can_reset_audio_input_output() +{ + QAudioInput in1; + QMediaCaptureSession session; + session.setAudioInput(&in1); + QVERIFY(session.audioInput() != nullptr); + QAudioInput in2; + QSignalSpy changeSpy1(&session, &QMediaCaptureSession::audioInputChanged); + session.setAudioInput(&in2); + QVERIFY(session.audioInput() != nullptr); + QCOMPARE(changeSpy1.count(), 1); + + QAudioOutput out1; + session.setAudioOutput(&out1); + QVERIFY(session.audioOutput() != nullptr); + QSignalSpy changeSpy2(&session, &QMediaCaptureSession::audioOutputChanged); + QAudioOutput out2; + session.setAudioOutput(&out2); + QVERIFY(session.audioOutput() != nullptr); + QCOMPARE(changeSpy2.count(), 1); +} + +QTEST_MAIN(tst_QMediaCaptureSession) + +#include "tst_qmediacapturesession.moc" diff --git a/tests/auto/integration/qmediaframeinputsbackend/CMakeLists.txt b/tests/auto/integration/qmediaframeinputsbackend/CMakeLists.txt new file mode 100644 index 000000000..8d35b1de0 --- /dev/null +++ b/tests/auto/integration/qmediaframeinputsbackend/CMakeLists.txt @@ -0,0 +1,22 @@ +# Copyright (C) 2024 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +##################################################################### +## tst_qmediaframeinputsbackend Test: +##################################################################### + +qt_internal_add_test(tst_qmediaframeinputsbackend + SOURCES + tst_qmediaframeinputsbackend.cpp tst_qmediaframeinputsbackend.h + capturesessionfixture.cpp capturesessionfixture.h + mediainfo.h + framegenerator.cpp framegenerator.h + ../shared/testvideosink.h + LIBRARIES + Qt::Multimedia + Qt::MultimediaPrivate + Qt::Gui + Qt::Widgets +) + + diff --git a/tests/auto/integration/qmediaframeinputsbackend/capturesessionfixture.cpp b/tests/auto/integration/qmediaframeinputsbackend/capturesessionfixture.cpp new file mode 100644 index 000000000..aae03df60 --- /dev/null +++ b/tests/auto/integration/qmediaframeinputsbackend/capturesessionfixture.cpp @@ -0,0 +1,88 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include "capturesessionfixture.h" +#include <QtTest/qtest.h> + +QT_BEGIN_NAMESPACE + +using namespace std::chrono; + +CaptureSessionFixture::CaptureSessionFixture(StreamType streamType, AutoStop autoStop) + : m_streamType{ streamType } +{ + m_recorder.setQuality(QMediaRecorder::VeryHighQuality); + m_session.setRecorder(&m_recorder); + + if (hasVideo()) { + m_session.setVideoFrameInput(&m_videoInput); + + QObject::connect(&m_videoGenerator, &VideoGenerator::frameCreated, // + &m_videoInput, &QVideoFrameInput::sendVideoFrame); + + if (autoStop == AutoStop::EmitEmpty) { + m_recorder.setAutoStop(true); + m_videoGenerator.emitEmptyFrameOnStop(); + } + } + + if (hasAudio()) { + m_session.setAudioBufferInput(&m_audioInput); + + QObject::connect(&m_audioGenerator, &AudioGenerator::audioBufferCreated, // + &m_audioInput, &QAudioBufferInput::sendAudioBuffer); + + if (autoStop == AutoStop::EmitEmpty) { + m_recorder.setAutoStop(true); + m_audioGenerator.emitEmptyBufferOnStop(); + } + } + + m_tempFile.open(); + m_recorder.setOutputLocation(m_tempFile.fileName()); +} + +CaptureSessionFixture::~CaptureSessionFixture() +{ + QFile::remove(m_recorder.actualLocation().toLocalFile()); +} + +void CaptureSessionFixture::connectPullMode() +{ + if (hasVideo()) + QObject::connect(&m_videoInput, &QVideoFrameInput::readyToSendVideoFrame, // + &m_videoGenerator, &VideoGenerator::nextFrame); + + if (hasAudio()) + QObject::connect(&m_audioInput, &QAudioBufferInput::readyToSendAudioBuffer, // + &m_audioGenerator, &AudioGenerator::nextBuffer); +} + +bool CaptureSessionFixture::waitForRecorderStopped(milliseconds duration) +{ + // StoppedState is emitted when media is finalized. + const bool stopped = QTest::qWaitFor( + [&] { // + return recorderStateChanged.contains( + QList<QVariant>{ QMediaRecorder::RecorderState::StoppedState }); + }, + duration); + + if (!stopped) + return false; + + return m_recorder.recorderState() == QMediaRecorder::StoppedState + && m_recorder.error() == QMediaRecorder::NoError; +} + +bool CaptureSessionFixture::hasAudio() const +{ + return m_streamType == StreamType::Audio || m_streamType == StreamType::AudioAndVideo; +} + +bool CaptureSessionFixture::hasVideo() const +{ + return m_streamType == StreamType::Video || m_streamType == StreamType::AudioAndVideo; +} + +QT_END_NAMESPACE diff --git a/tests/auto/integration/qmediaframeinputsbackend/capturesessionfixture.h b/tests/auto/integration/qmediaframeinputsbackend/capturesessionfixture.h new file mode 100644 index 000000000..f7aa27a65 --- /dev/null +++ b/tests/auto/integration/qmediaframeinputsbackend/capturesessionfixture.h @@ -0,0 +1,49 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#ifndef CAPTURESESSIONFIXTURE_H +#define CAPTURESESSIONFIXTURE_H + +#include "framegenerator.h" +#include <QtMultimedia/qvideoframeinput.h> +#include <QtMultimedia/qaudioinput.h> +#include <QtMultimedia/qmediacapturesession.h> +#include <QtMultimedia/qmediarecorder.h> +#include <QtMultimedia/qaudiobufferinput.h> +#include <QtCore/qtemporaryfile.h> + +#include <../shared/testvideosink.h> +#include <QtTest/qsignalspy.h> + +QT_BEGIN_NAMESPACE + +enum class StreamType { Audio, Video, AudioAndVideo }; +enum class AutoStop { EmitEmpty, No }; + +struct CaptureSessionFixture +{ + explicit CaptureSessionFixture(StreamType streamType, AutoStop autoStop); + ~CaptureSessionFixture(); + + void connectPullMode(); + bool waitForRecorderStopped(milliseconds duration); + bool hasAudio() const; + bool hasVideo() const; + + VideoGenerator m_videoGenerator; + AudioGenerator m_audioGenerator; + QVideoFrameInput m_videoInput; + QAudioBufferInput m_audioInput; + QMediaCaptureSession m_session; + QMediaRecorder m_recorder; + QTemporaryFile m_tempFile; + StreamType m_streamType = StreamType::Video; + + QSignalSpy readyToSendVideoFrame{ &m_videoInput, &QVideoFrameInput::readyToSendVideoFrame }; + QSignalSpy readyToSendAudioBuffer{ &m_audioInput, &QAudioBufferInput::readyToSendAudioBuffer }; + QSignalSpy recorderStateChanged{ &m_recorder, &QMediaRecorder::recorderStateChanged }; +}; + +QT_END_NAMESPACE + +#endif diff --git a/tests/auto/integration/qmediaframeinputsbackend/framegenerator.cpp b/tests/auto/integration/qmediaframeinputsbackend/framegenerator.cpp new file mode 100644 index 000000000..c84ee9078 --- /dev/null +++ b/tests/auto/integration/qmediaframeinputsbackend/framegenerator.cpp @@ -0,0 +1,119 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include "framegenerator.h" +#include <QtCore/qdebug.h> + +QT_BEGIN_NAMESPACE + +void VideoGenerator::setFrameCount(int count) +{ + m_maxFrameCount = count; +} + +void VideoGenerator::setSize(QSize size) +{ + m_size = size; +} + +void VideoGenerator::setFrameRate(double rate) +{ + m_frameRate = rate; +} + +void VideoGenerator::setPeriod(milliseconds period) +{ + m_period = period; +} + +void VideoGenerator::emitEmptyFrameOnStop() +{ + m_emitEmptyFrameOnStop = true; +} + +QVideoFrame VideoGenerator::createFrame() +{ + QImage image(m_size, QImage::Format_ARGB32); + image.fill(colors[m_frameIndex % colors.size()]); + QVideoFrame frame(image); + + if (m_frameRate) + frame.setStreamFrameRate(*m_frameRate); + + if (m_period) { + frame.setStartTime(duration_cast<microseconds>(*m_period).count() * m_frameIndex); + frame.setEndTime(duration_cast<microseconds>(*m_period).count() * (m_frameIndex + 1)); + } + + return frame; +} + +void VideoGenerator::nextFrame() +{ + if (m_frameIndex == m_maxFrameCount) { + emit done(); + if (m_emitEmptyFrameOnStop) + emit frameCreated({}); + return; + } + + const QVideoFrame frame = createFrame(); + emit frameCreated(frame); + ++m_frameIndex; +} + +AudioGenerator::AudioGenerator() +{ + m_format.setSampleFormat(QAudioFormat::UInt8); + m_format.setSampleRate(8000); + m_format.setChannelConfig(QAudioFormat::ChannelConfigMono); +} + +void AudioGenerator::setFormat(const QAudioFormat &format) +{ + m_format = format; +} + +void AudioGenerator::setBufferCount(int count) +{ + m_maxBufferCount = count; +} + +void AudioGenerator::setDuration(microseconds duration) +{ + m_duration = duration; +} + +void AudioGenerator::emitEmptyBufferOnStop() +{ + m_emitEmptyBufferOnStop = true; +} + +QAudioBuffer AudioGenerator::createAudioBuffer() +{ + const microseconds bufferDuration = m_duration / m_maxBufferCount.value_or(1); + const qint32 byteCount = m_format.bytesForDuration(bufferDuration.count()); + const QByteArray data(byteCount, '\0'); + + QAudioBuffer buffer(data, m_format); + return buffer; +} + +void AudioGenerator::nextBuffer() +{ + if (m_bufferIndex == m_maxBufferCount) { + emit done(); + if (m_emitEmptyBufferOnStop) + emit audioBufferCreated({}); + return; + } + + const QAudioBuffer buffer = createAudioBuffer(); + + emit audioBufferCreated(buffer); + ++m_bufferIndex; +} + +QT_END_NAMESPACE + +#include "moc_framegenerator.cpp" diff --git a/tests/auto/integration/qmediaframeinputsbackend/framegenerator.h b/tests/auto/integration/qmediaframeinputsbackend/framegenerator.h new file mode 100644 index 000000000..866707685 --- /dev/null +++ b/tests/auto/integration/qmediaframeinputsbackend/framegenerator.h @@ -0,0 +1,74 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#ifndef FRAMEGENERATOR_H +#define FRAMEGENERATOR_H + +#include <QtCore/qobject.h> +#include <QtCore/qlist.h> +#include <QtMultimedia/qvideoframe.h> +#include <QtMultimedia/qaudiobuffer.h> +#include <functional> +#include <chrono> + +QT_BEGIN_NAMESPACE + +using namespace std::chrono; + +class VideoGenerator : public QObject +{ + Q_OBJECT +public: + void setFrameCount(int count); + void setSize(QSize size); + void setFrameRate(double rate); + void setPeriod(milliseconds period); + void emitEmptyFrameOnStop(); + QVideoFrame createFrame(); + +signals: + void done(); + void frameCreated(const QVideoFrame &frame); + +public slots: + void nextFrame(); + +private: + QList<QColor> colors = { Qt::red, Qt::green, Qt::blue, Qt::black, Qt::white }; + QSize m_size{ 640, 480 }; + std::optional<int> m_maxFrameCount; + int m_frameIndex = 0; + std::optional<double> m_frameRate; + std::optional<milliseconds> m_period; + bool m_emitEmptyFrameOnStop = false; +}; + +class AudioGenerator : public QObject +{ + Q_OBJECT +public: + AudioGenerator(); + void setFormat(const QAudioFormat &format); + void setBufferCount(int count); + void setDuration(microseconds duration); + void emitEmptyBufferOnStop(); + QAudioBuffer createAudioBuffer(); + +signals: + void done(); + void audioBufferCreated(const QAudioBuffer &buffer); + +public slots: + void nextBuffer(); + +private: + std::optional<int> m_maxBufferCount; + microseconds m_duration = 1s; + int m_bufferIndex = 0; + QAudioFormat m_format; + bool m_emitEmptyBufferOnStop = false; +}; + +QT_END_NAMESPACE + +#endif diff --git a/tests/auto/integration/qmediaframeinputsbackend/mediainfo.h b/tests/auto/integration/qmediaframeinputsbackend/mediainfo.h new file mode 100644 index 000000000..e7525080d --- /dev/null +++ b/tests/auto/integration/qmediaframeinputsbackend/mediainfo.h @@ -0,0 +1,71 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#ifndef MEDIAINFO_H +#define MEDIAINFO_H + +#include <QtTest/QTest> +#include <QtMultimedia/qmediaplayer.h> +#include <QtMultimedia/qmediametadata.h> +#include <QtMultimedia/qaudiooutput.h> +#include "../shared/testvideosink.h" +#include <chrono> + +QT_USE_NAMESPACE + +using namespace std::chrono; + +// Extracts media metadata from a input media file +struct MediaInfo +{ + static std::optional<MediaInfo> create(const QUrl &fileLocation) + { + QMediaPlayer player; + const QSignalSpy mediaStatusChanged{ &player, &QMediaPlayer::mediaStatusChanged }; + + QAudioOutput audioOutput; + player.setAudioOutput(&audioOutput); + + TestVideoSink sink; + player.setVideoSink(&sink); + + player.setSource(fileLocation); + + // Loop through all frames to be able to count them + player.setPlaybackRate(50); // let's speed it up + player.play(); + + const bool endReached = QTest::qWaitFor( + [&] { + return mediaStatusChanged.contains(QList<QVariant>{ QMediaPlayer::EndOfMedia }) + || mediaStatusChanged.contains( + QList<QVariant>{ QMediaPlayer::InvalidMedia }); + }, + 10min); + + if (!endReached) + return {}; + + MediaInfo info{}; + info.m_frameRate = player.metaData().value(QMediaMetaData::VideoFrameRate).toReal(); + info.m_size = player.metaData().value(QMediaMetaData::Resolution).toSize(); + + info.m_duration = milliseconds{ player.duration() }; + info.m_frameCount = sink.m_totalFrames - 1; + info.m_frameTimes = sink.m_frameTimes; + info.m_hasVideo = player.hasVideo(); + info.m_hasAudio = player.hasAudio(); + return info; + } + + int m_frameCount = 0; + qreal m_frameRate = 0.0f; + QSize m_size; + milliseconds m_duration; + bool m_hasVideo = false; + bool m_hasAudio = false; + + std::vector<TestVideoSink::TimePoint> m_frameTimes; +}; + +#endif diff --git a/tests/auto/integration/qmediaframeinputsbackend/tst_qmediaframeinputsbackend.cpp b/tests/auto/integration/qmediaframeinputsbackend/tst_qmediaframeinputsbackend.cpp new file mode 100644 index 000000000..ee4b78606 --- /dev/null +++ b/tests/auto/integration/qmediaframeinputsbackend/tst_qmediaframeinputsbackend.cpp @@ -0,0 +1,302 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include "capturesessionfixture.h" +#include "tst_qmediaframeinputsbackend.h" + +#include "mediainfo.h" +#include <QtTest/QtTest> +#include <qvideoframeinput.h> +#include <qaudiobufferinput.h> +#include <qsignalspy.h> +#include <qmediarecorder.h> +#include <qmediaplayer.h> +#include <../shared/testvideosink.h> +#include <../shared/mediabackendutils.h> + +QT_BEGIN_NAMESPACE + +void tst_QMediaFrameInputsBackend::initTestCase() +{ + QSKIP_GSTREAMER("Not implemented in the gstreamer backend"); +} + +void tst_QMediaFrameInputsBackend::mediaRecorderWritesAudio_whenAudioFramesInputSends_data() +{ + QTest::addColumn<int>("bufferCount"); + QTest::addColumn<QAudioFormat::SampleFormat>("sampleFormat"); + QTest::addColumn<QAudioFormat::ChannelConfig>("channelConfig"); + QTest::addColumn<int>("sampleRate"); + QTest::addColumn<milliseconds>("duration"); + +#ifndef Q_OS_WINDOWS // sample rate 8000 is not supported. TODO: investigate. + QTest::addRow("bufferCount: 20; sampleFormat: Int16; channelConfig: Mono; sampleRate: 8000; " + "duration: 1000") + << 20 << QAudioFormat::Int16 << QAudioFormat::ChannelConfigMono << 8000 << 1000ms; +#endif + QTest::addRow("bufferCount: 30; sampleFormat: Int32; channelConfig: Stereo; sampleRate: " + "12000; duration: 2000") + << 30 << QAudioFormat::Int32 << QAudioFormat::ChannelConfigStereo << 12000 << 2000ms; + + // TODO: investigate fails of channels configuration + // QTest::addRow("bufferCount: 10; sampleFormat: UInt8; channelConfig: 2Dot1; sampleRate: + // 40000; duration: 1500") + // << 10 << QAudioFormat::UInt8 << QAudioFormat::ChannelConfig2Dot1 << 40000 << 1500; + // QTest::addRow("bufferCount: 10; sampleFormat: Float; channelConfig: 3Dot0; sampleRate: + // 50000; duration: 2500") + // << 40 << QAudioFormat::Float << QAudioFormat::ChannelConfig3Dot0 << 50000 << 2500; +} + +void tst_QMediaFrameInputsBackend::mediaRecorderWritesAudio_whenAudioFramesInputSends() +{ + QFETCH(const int, bufferCount); + QFETCH(const QAudioFormat::SampleFormat, sampleFormat); + QFETCH(const QAudioFormat::ChannelConfig, channelConfig); + QFETCH(const int, sampleRate); + QFETCH(const milliseconds, duration); + + CaptureSessionFixture f{ StreamType::Audio, AutoStop::EmitEmpty }; + f.connectPullMode(); + + QAudioFormat format; + format.setSampleFormat(sampleFormat); + format.setSampleRate(sampleRate); + format.setChannelConfig(channelConfig); + + f.m_audioGenerator.setFormat(format); + f.m_audioGenerator.setBufferCount(bufferCount); + f.m_audioGenerator.setDuration(duration); + + f.m_recorder.record(); + + QVERIFY(f.waitForRecorderStopped(60s)); + + auto info = MediaInfo::create(f.m_recorder.actualLocation()); + + QVERIFY(info->m_hasAudio); + QCOMPARE_GE(info->m_duration, duration - 50ms); + QCOMPARE_LE(info->m_duration, duration + 50ms); +} + +void tst_QMediaFrameInputsBackend::mediaRecorderWritesVideo_whenVideoFramesInputSendsFrames_data() +{ + QTest::addColumn<int>("framesNumber"); + QTest::addColumn<milliseconds>("frameDuration"); + QTest::addColumn<QSize>("resolution"); + QTest::addColumn<bool>("setTimeStamp"); + + QTest::addRow("framesNumber: 5; frameRate: 2; resolution: 50x80; with time stamps") + << 5 << 500ms << QSize(50, 80) << true; + QTest::addRow("framesNumber: 20; frameRate: 1; resolution: 200x100; with time stamps") + << 20 << 1000ms << QSize(200, 100) << true; + + QTest::addRow("framesNumber: 20; frameRate: 30; resolution: 200x100; with frame rate") + << 20 << 250ms << QSize(200, 100) << false; + QTest::addRow("framesNumber: 60; frameRate: 4; resolution: 200x100; with frame rate") + << 60 << 24ms << QSize(200, 100) << false; +} + +void tst_QMediaFrameInputsBackend::mediaRecorderWritesVideo_whenVideoFramesInputSendsFrames() +{ + QFETCH(const int, framesNumber); + QFETCH(const milliseconds, frameDuration); + QFETCH(const QSize, resolution); + QFETCH(const bool, setTimeStamp); + + CaptureSessionFixture f{ StreamType::Video, AutoStop::EmitEmpty }; + f.connectPullMode(); + f.m_videoGenerator.setFrameCount(framesNumber); + f.m_videoGenerator.setSize(resolution); + + const qreal frameRate = 1e6 / duration_cast<microseconds>(frameDuration).count(); + if (setTimeStamp) + f.m_videoGenerator.setPeriod(frameDuration); + else + f.m_videoGenerator.setFrameRate(frameRate); + + f.m_recorder.record(); + + QVERIFY(f.waitForRecorderStopped(60s)); + + auto info = MediaInfo::create(f.m_recorder.actualLocation()); + + QCOMPARE_LT(info->m_frameRate, frameRate * 1.001); + QCOMPARE_GT(info->m_frameRate, frameRate * 0.999); + + QCOMPARE_LT(info->m_duration, frameDuration * framesNumber * 1.001); + QCOMPARE_GE(info->m_duration, frameDuration * framesNumber * 0.999); + + QCOMPARE(info->m_size, resolution); + QCOMPARE_EQ(info->m_frameCount, framesNumber); +} + +void tst_QMediaFrameInputsBackend::mediaRecorderWritesVideo_withSingleFrame() +{ + CaptureSessionFixture f{ StreamType::Video, AutoStop::EmitEmpty }; + f.connectPullMode(); + f.m_videoGenerator.setFrameCount(1); + f.m_videoGenerator.setSize({ 640, 480 }); + f.m_videoGenerator.setPeriod(1s); + f.m_recorder.record(); + QVERIFY(f.waitForRecorderStopped(60s)); + auto info = MediaInfo::create(f.m_recorder.actualLocation()); + + QCOMPARE_EQ(info->m_frameCount, 1); + QCOMPARE_EQ(info->m_duration, 1s); +} + +void tst_QMediaFrameInputsBackend::mediaRecorderStopsRecording_whenInputsReportedEndOfStream_data() +{ + QTest::addColumn<bool>("audioStopsFirst"); + + QTest::addRow("audio stops first") << true; + QTest::addRow("video stops first") << true; +} + +void tst_QMediaFrameInputsBackend::mediaRecorderStopsRecording_whenInputsReportedEndOfStream() +{ + QFETCH(const bool, audioStopsFirst); + + CaptureSessionFixture f{ StreamType::AudioAndVideo, AutoStop::No }; + f.m_recorder.setAutoStop(true); + f.connectPullMode(); + + f.m_audioGenerator.setBufferCount(30); + f.m_videoGenerator.setFrameCount(30); + + QSignalSpy audioDone{ &f.m_audioGenerator, &AudioGenerator::done }; + QSignalSpy videoDone{ &f.m_videoGenerator, &VideoGenerator::done }; + + f.m_recorder.record(); + + audioDone.wait(); + videoDone.wait(); + + if (audioStopsFirst) { + f.m_audioInput.sendAudioBuffer({}); + QVERIFY(!f.waitForRecorderStopped(300ms)); // Should not stop until both streams stopped + f.m_videoInput.sendVideoFrame({}); + } else { + f.m_videoInput.sendVideoFrame({}); + QVERIFY(!f.waitForRecorderStopped(300ms)); // Should not stop until both streams stopped + f.m_audioInput.sendAudioBuffer({}); + } + + QVERIFY(f.waitForRecorderStopped(60s)); + + // check if the file has been written + + const std::optional<MediaInfo> mediaInfo = MediaInfo::create(f.m_recorder.actualLocation()); + + QVERIFY(mediaInfo); + QVERIFY(mediaInfo->m_hasVideo); + QVERIFY(mediaInfo->m_hasAudio); +} + +void tst_QMediaFrameInputsBackend::readyToSend_isEmitted_whenRecordingStarts_data() +{ + QTest::addColumn<StreamType>("streamType"); + QTest::addRow("audio") << StreamType::Audio; + QTest::addRow("video") << StreamType::Video; + QTest::addRow("audioAndVideo") << StreamType::AudioAndVideo; +} + +void tst_QMediaFrameInputsBackend::readyToSend_isEmitted_whenRecordingStarts() +{ + QFETCH(StreamType, streamType); + + CaptureSessionFixture f{ streamType, AutoStop::No }; + + f.m_recorder.record(); + + if (f.hasAudio()) + QTRY_COMPARE_EQ(f.readyToSendAudioBuffer.size(), 1); + + if (f.hasVideo()) + QTRY_COMPARE_EQ(f.readyToSendVideoFrame.size(), 1); +} + +void tst_QMediaFrameInputsBackend::readyToSendVideoFrame_isEmitted_whenSendVideoFrameIsCalled() +{ + CaptureSessionFixture f{ StreamType::Video, AutoStop::No }; + + f.m_recorder.record(); + QVERIFY(f.readyToSendVideoFrame.wait()); + + f.m_videoInput.sendVideoFrame(f.m_videoGenerator.createFrame()); + QVERIFY(f.readyToSendVideoFrame.wait()); + + f.m_videoInput.sendVideoFrame(f.m_videoGenerator.createFrame()); + QVERIFY(f.readyToSendVideoFrame.wait()); +} + +void tst_QMediaFrameInputsBackend::readyToSendAudioBuffer_isEmitted_whenSendAudioBufferIsCalled() +{ + CaptureSessionFixture f{ StreamType::Audio, AutoStop::No }; + + f.m_recorder.record(); + QVERIFY(f.readyToSendAudioBuffer.wait()); + + f.m_audioInput.sendAudioBuffer(f.m_audioGenerator.createAudioBuffer()); + QVERIFY(f.readyToSendAudioBuffer.wait()); + + f.m_audioInput.sendAudioBuffer(f.m_audioGenerator.createAudioBuffer()); + QVERIFY(f.readyToSendAudioBuffer.wait()); +} + +void tst_QMediaFrameInputsBackend::readyToSendVideoFrame_isEmittedRepeatedly_whenPullModeIsEnabled() +{ + CaptureSessionFixture f{ StreamType::Video, AutoStop::EmitEmpty }; + f.connectPullMode(); + + constexpr int expectedSignalCount = 4; + f.m_videoGenerator.setFrameCount(expectedSignalCount - 1); + + f.m_recorder.record(); + + f.waitForRecorderStopped(60s); + + QCOMPARE_EQ(f.readyToSendVideoFrame.size(), expectedSignalCount); +} + +void tst_QMediaFrameInputsBackend:: + readyToSendAudioBuffer_isEmittedRepeatedly_whenPullModeIsEnabled() +{ + CaptureSessionFixture f{ StreamType::Audio, AutoStop::EmitEmpty }; + f.connectPullMode(); + + constexpr int expectedSignalCount = 4; + f.m_audioGenerator.setBufferCount(expectedSignalCount - 1); + + f.m_recorder.record(); + + f.waitForRecorderStopped(60s); + + QCOMPARE_EQ(f.readyToSendAudioBuffer.size(), expectedSignalCount); +} + +void tst_QMediaFrameInputsBackend:: + readyToSendAudioBufferAndVideoFrame_isEmittedRepeatedly_whenPullModeIsEnabled() +{ + CaptureSessionFixture f{ StreamType::AudioAndVideo, AutoStop::EmitEmpty }; + f.connectPullMode(); + + constexpr int expectedSignalCount = 4; + f.m_audioGenerator.setBufferCount(expectedSignalCount - 1); + f.m_videoGenerator.setFrameCount(expectedSignalCount - 1); + + f.m_recorder.record(); + + f.waitForRecorderStopped(60s); + + QCOMPARE_EQ(f.readyToSendAudioBuffer.size(), expectedSignalCount); + QCOMPARE_EQ(f.readyToSendVideoFrame.size(), expectedSignalCount); +} + +QT_END_NAMESPACE + +QT_USE_NAMESPACE + +QTEST_MAIN(tst_QMediaFrameInputsBackend) + +#include "moc_tst_qmediaframeinputsbackend.cpp" diff --git a/tests/auto/integration/qmediaframeinputsbackend/tst_qmediaframeinputsbackend.h b/tests/auto/integration/qmediaframeinputsbackend/tst_qmediaframeinputsbackend.h new file mode 100644 index 000000000..61e4d3118 --- /dev/null +++ b/tests/auto/integration/qmediaframeinputsbackend/tst_qmediaframeinputsbackend.h @@ -0,0 +1,42 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#ifndef TST_QMEDIAFRAMEINPUTSBACKEND_H +#define TST_QMEDIAFRAMEINPUTSBACKEND_H + +#include <QObject> + +QT_BEGIN_NAMESPACE + +class tst_QMediaFrameInputsBackend : public QObject +{ + Q_OBJECT + +private slots: + void initTestCase(); + + void mediaRecorderWritesAudio_whenAudioFramesInputSends_data(); + void mediaRecorderWritesAudio_whenAudioFramesInputSends(); + + void mediaRecorderWritesVideo_whenVideoFramesInputSendsFrames_data(); + void mediaRecorderWritesVideo_whenVideoFramesInputSendsFrames(); + + void mediaRecorderWritesVideo_withSingleFrame(); + + void mediaRecorderStopsRecording_whenInputsReportedEndOfStream_data(); + void mediaRecorderStopsRecording_whenInputsReportedEndOfStream(); + + void readyToSend_isEmitted_whenRecordingStarts_data(); + void readyToSend_isEmitted_whenRecordingStarts(); + + void readyToSendVideoFrame_isEmitted_whenSendVideoFrameIsCalled(); + void readyToSendAudioBuffer_isEmitted_whenSendAudioBufferIsCalled(); + + void readyToSendVideoFrame_isEmittedRepeatedly_whenPullModeIsEnabled(); + void readyToSendAudioBuffer_isEmittedRepeatedly_whenPullModeIsEnabled(); + void readyToSendAudioBufferAndVideoFrame_isEmittedRepeatedly_whenPullModeIsEnabled(); +}; + +QT_END_NAMESPACE + +#endif diff --git a/tests/auto/integration/qmediaplayerbackend/BLACKLIST b/tests/auto/integration/qmediaplayerbackend/BLACKLIST index e91f47755..1f24e07a1 100644 --- a/tests/auto/integration/qmediaplayerbackend/BLACKLIST +++ b/tests/auto/integration/qmediaplayerbackend/BLACKLIST @@ -1,32 +1,6 @@ -# QTBUG-46368 - -osx -windows-7 -windows-7sp1 -windows-10 msvc-2015 -windows-10 msvc-2017 -windows-10 msvc-2019 # Media player plugin not built at the moment on this platform opensuse-13.1 64bit -[loadMedia] -windows 64bit developer-build - -[unloadMedia] -windows 64bit developer-build - -[playPauseStop] -windows 64bit developer-build - -[processEOS] -windows 64bit developer-build - -[deleteLaterAtEOS] -windows 64bit developer-build - -[initialVolume] -windows 64bit developer-build - [playlist] redhatenterpriselinuxworkstation-6.6 diff --git a/tests/auto/integration/qmediaplayerbackend/CMakeLists.txt b/tests/auto/integration/qmediaplayerbackend/CMakeLists.txt new file mode 100644 index 000000000..3a9d25926 --- /dev/null +++ b/tests/auto/integration/qmediaplayerbackend/CMakeLists.txt @@ -0,0 +1,39 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +# Generated from qmediaplayerbackend.pro. + +##################################################################### +## tst_qmediaplayerbackend Test: +##################################################################### + +# Collect test data +file(GLOB_RECURSE test_data_glob + RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} + testdata/*) +list(APPEND testdata_resource_files ${test_data_glob}) + +qt_internal_add_test(tst_qmediaplayerbackend + SOURCES + ../shared/mediafileselector.h + ../shared/mediabackendutils.h + ../shared/testvideosink.h + mediaplayerstate.h + fake.h + fixture.h + server.h + tst_qmediaplayerbackend.cpp + LIBRARIES + Qt::Gui + Qt::MultimediaPrivate + Qt::MultimediaQuickPrivate + Qt::Qml + Qt::Quick + Qt::QuickPrivate + BUILTIN_TESTDATA + TESTDATA + ${testdata_resource_files} + "LazyLoad.qml" + INCLUDE_DIRECTORIES + ../shared/ +) diff --git a/tests/auto/integration/qmediaplayerbackend/LazyLoad.qml b/tests/auto/integration/qmediaplayerbackend/LazyLoad.qml new file mode 100644 index 000000000..04af13186 --- /dev/null +++ b/tests/auto/integration/qmediaplayerbackend/LazyLoad.qml @@ -0,0 +1,52 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +import QtQuick +import QtMultimedia + +Rectangle { + id: root + width: 600 + height: 800 + color: "black" + + Component { + id: videoOutputComponent + + Item { + objectName: "videoPlayer" + property alias mediaPlayer: mediaPlayer + property alias videoOutput: videoOutput + property alias videoSink: videoOutput.videoSink + + property alias playbackState: mediaPlayer.playbackState + property alias error: mediaPlayer.error + + + MediaPlayer { + id: mediaPlayer + objectName: "mediaPlayer" + source: "qrc:/testdata/colors.mp4" + } + VideoOutput { + id: videoOutput + objectName: "videoOutput" + anchors.fill: parent + } + } + } + + Loader { + id: loader + objectName: "loader" + sourceComponent: videoOutputComponent + anchors.fill: parent + active: false + onActiveChanged: { + if (active) { + loader.item.mediaPlayer.videoOutput = loader.item.videoOutput + loader.item.mediaPlayer.play() + } + } + } +} diff --git a/tests/auto/integration/qmediaplayerbackend/fake.h b/tests/auto/integration/qmediaplayerbackend/fake.h new file mode 100644 index 000000000..9a68741d0 --- /dev/null +++ b/tests/auto/integration/qmediaplayerbackend/fake.h @@ -0,0 +1,29 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#ifndef FAKE_H +#define FAKE_H + +#include <testvideosink.h> + +QT_USE_NAMESPACE + +class TestVideoOutput : public QObject +{ + Q_OBJECT +public: + TestVideoOutput() = default; + + Q_INVOKABLE QVideoSink *videoSink() { return &m_sink; } + + TestVideoSink m_sink; +}; + +inline void setVideoSinkAsyncFramesCounter(QVideoSink &sink, std::atomic_int &counter) +{ + QObject::connect( + &sink, &QVideoSink::videoFrameChanged, &sink, [&counter]() { ++counter; }, + Qt::DirectConnection); +} + +#endif // FAKE_H diff --git a/tests/auto/integration/qmediaplayerbackend/fixture.h b/tests/auto/integration/qmediaplayerbackend/fixture.h new file mode 100644 index 000000000..bf539d7ff --- /dev/null +++ b/tests/auto/integration/qmediaplayerbackend/fixture.h @@ -0,0 +1,79 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#ifndef FIXTURE_H +#define FIXTURE_H + +#include <qmediaplayer.h> +#include <qaudiooutput.h> +#include <qtest.h> +#include <qsignalspy.h> + +#include "fake.h" +#include "testvideosink.h" + +QT_USE_NAMESPACE + +struct Fixture : QObject +{ + Q_OBJECT +public: + Fixture() + : playbackStateChanged(&player, &QMediaPlayer::playbackStateChanged), + errorOccurred(&player, &QMediaPlayer::errorOccurred), + sourceChanged(&player, &QMediaPlayer::sourceChanged), + mediaStatusChanged(&player, &QMediaPlayer::mediaStatusChanged), + positionChanged(&player, &QMediaPlayer::positionChanged), + durationChanged(&player, &QMediaPlayer::durationChanged), + playbackRateChanged(&player, &QMediaPlayer::playbackRateChanged), + metadataChanged(&player, &QMediaPlayer::metaDataChanged), + volumeChanged(&output, &QAudioOutput::volumeChanged), + mutedChanged(&output, &QAudioOutput::mutedChanged), + bufferProgressChanged(&player, &QMediaPlayer::bufferProgressChanged), + destroyed(&player, &QObject::destroyed) + { + setVideoSinkAsyncFramesCounter(surface, framesCount); + + player.setAudioOutput(&output); + player.setVideoOutput(&surface); + } + + void clearSpies() + { + playbackStateChanged.clear(); + errorOccurred.clear(); + sourceChanged.clear(); + mediaStatusChanged.clear(); + positionChanged.clear(); + durationChanged.clear(); + playbackRateChanged.clear(); + metadataChanged.clear(); + volumeChanged.clear(); + mutedChanged.clear(); + bufferProgressChanged.clear(); + destroyed.clear(); + } + + QMediaPlayer player; + QAudioOutput output; + TestVideoSink surface; + std::atomic_int framesCount = 0; + + QSignalSpy playbackStateChanged; + QSignalSpy errorOccurred; + QSignalSpy sourceChanged; + QSignalSpy mediaStatusChanged; + QSignalSpy positionChanged; + QSignalSpy durationChanged; + QSignalSpy playbackRateChanged; + QSignalSpy metadataChanged; + QSignalSpy volumeChanged; + QSignalSpy mutedChanged; + QSignalSpy bufferProgressChanged; + QSignalSpy destroyed; +}; + +// Helper to create an object that is comparable to a QSignalSpy +using SignalList = QList<QList<QVariant>>; + +#endif // FIXTURE_H diff --git a/tests/auto/integration/qmediaplayerbackend/mediaplayerstate.h b/tests/auto/integration/qmediaplayerbackend/mediaplayerstate.h new file mode 100644 index 000000000..d9f2cc875 --- /dev/null +++ b/tests/auto/integration/qmediaplayerbackend/mediaplayerstate.h @@ -0,0 +1,167 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#ifndef MEDIAPLAYERSTATE_H +#define MEDIAPLAYERSTATE_H + +#include <QtCore/qlist.h> +#include <QtCore/qurl.h> +#include <QtMultimedia/qaudiooutput.h> +#include <QtMultimedia/qmediametadata.h> +#include <QtMultimedia/qmediaplayer.h> +#include <QtMultimedia/qmediatimerange.h> +#include <QtTest/qtestcase.h> + +#include <optional> + +QT_USE_NAMESPACE + +/*! + * Helper class that simplifies testing the state of + * a media player against an expected state. + * + * Use the COMPARE_MEDIA_PLAYER_STATE_EQ macro to compare + * the media player state against the expected state. + * + * Individual properties can be ignored by comparison by + * assigning the std::nullopt value to the property of the + * expected state. + */ +struct MediaPlayerState +{ + std::optional<QList<QMediaMetaData>> audioTracks; + std::optional<QList<QMediaMetaData>> videoTracks; + std::optional<QList<QMediaMetaData>> subtitleTracks; + std::optional<int> activeAudioTrack; + std::optional<int> activeVideoTrack; + std::optional<int> activeSubtitleTrack; + std::optional<QAudioOutput*> audioOutput; + std::optional<QObject*> videoOutput; + std::optional<QVideoSink*> videoSink; + std::optional<QUrl> source; + std::optional<QIODevice const*> sourceDevice; + std::optional<QMediaPlayer::PlaybackState> playbackState; + std::optional<QMediaPlayer::MediaStatus> mediaStatus; + std::optional<qint64> duration; + std::optional<qint64> position; + std::optional<bool> hasAudio; + std::optional<bool> hasVideo; + std::optional<float> bufferProgress; + std::optional<QMediaTimeRange> bufferedTimeRange; + std::optional<bool> isSeekable; + std::optional<qreal> playbackRate; + std::optional<bool> isPlaying; + std::optional<int> loops; + std::optional<QMediaPlayer::Error> error; + std::optional<bool> isAvailable; + std::optional<QMediaMetaData> metaData; + + /*! + * Read the state from an existing media player + */ + explicit MediaPlayerState(const QMediaPlayer &player) + : audioTracks{ player.audioTracks() }, + videoTracks{ player.videoTracks() }, + subtitleTracks{ player.subtitleTracks() }, + activeAudioTrack{ player.activeAudioTrack() }, + activeVideoTrack{ player.activeVideoTrack() }, + activeSubtitleTrack{ player.activeSubtitleTrack() }, + audioOutput{ player.audioOutput() }, + videoOutput{ player.videoOutput() }, + videoSink{ player.videoSink() }, + source{ player.source() }, + sourceDevice{ player.sourceDevice() }, + playbackState{ player.playbackState() }, + mediaStatus{ player.mediaStatus() }, + duration{ player.duration() }, + position{ player.position() }, + hasAudio{ player.hasAudio() }, + hasVideo{ player.hasVideo() }, + bufferProgress{ player.bufferProgress() }, + bufferedTimeRange{ player.bufferedTimeRange() }, + isSeekable{ player.isSeekable() }, + playbackRate{ player.playbackRate() }, + isPlaying{ player.isPlaying() }, + loops{ player.loops() }, + error{ player.error() }, + isAvailable{ player.isAvailable() }, + metaData{ player.metaData() } + { + } + + /*! + * Creates the default state of a media player. The default state + * is the state the player should have when it is default constructed. + */ + static MediaPlayerState defaultState() + { + MediaPlayerState state{}; + state.audioTracks = QList<QMediaMetaData>{}; + state.videoTracks = QList<QMediaMetaData>{}; + state.subtitleTracks = QList<QMediaMetaData>{}; + state.activeAudioTrack = -1; + state.activeVideoTrack = -1; + state.activeSubtitleTrack = -1; + state.audioOutput = nullptr; + state.videoOutput = nullptr; + state.videoSink = nullptr; + state.source = QUrl{}; + state.sourceDevice = nullptr; + state.playbackState = QMediaPlayer::StoppedState; + state.mediaStatus = QMediaPlayer::NoMedia; + state.duration = 0; + state.position = 0; + state.hasAudio = false; + state.hasVideo = false; + state.bufferProgress = 0.0f; + state.bufferedTimeRange = QMediaTimeRange{}; + state.isSeekable = false; + state.playbackRate = static_cast<qreal>(1); + state.isPlaying = false; + state.loops = 1; + state.error = QMediaPlayer::NoError; + state.isAvailable = true; + state.metaData = QMediaMetaData{}; + return state; + } + +private: + MediaPlayerState() = default; + +}; + +#define COMPARE_EQ_IGNORE_OPTIONAL(actual, expected) \ + do { \ + if ((expected).has_value()) { \ + QCOMPARE_EQ(actual, expected); \ + } \ + } while (false) + +#define COMPARE_MEDIA_PLAYER_STATE_EQ(actual, expected) \ + do { \ + COMPARE_EQ_IGNORE_OPTIONAL((actual).audioTracks, (expected).audioTracks); \ + COMPARE_EQ_IGNORE_OPTIONAL((actual).videoTracks, (expected).videoTracks); \ + COMPARE_EQ_IGNORE_OPTIONAL((actual).subtitleTracks, (expected).subtitleTracks); \ + COMPARE_EQ_IGNORE_OPTIONAL((actual).activeAudioTrack, (expected).activeAudioTrack); \ + COMPARE_EQ_IGNORE_OPTIONAL((actual).activeVideoTrack, (expected).activeVideoTrack); \ + COMPARE_EQ_IGNORE_OPTIONAL((actual).activeSubtitleTrack, (expected).activeSubtitleTrack); \ + COMPARE_EQ_IGNORE_OPTIONAL((actual).source, (expected).source); \ + COMPARE_EQ_IGNORE_OPTIONAL((actual).sourceDevice, (expected).sourceDevice); \ + COMPARE_EQ_IGNORE_OPTIONAL((actual).playbackState, (expected).playbackState); \ + COMPARE_EQ_IGNORE_OPTIONAL((actual).mediaStatus, (expected).mediaStatus); \ + COMPARE_EQ_IGNORE_OPTIONAL((actual).duration, (expected).duration); \ + COMPARE_EQ_IGNORE_OPTIONAL((actual).position, (expected).position); \ + COMPARE_EQ_IGNORE_OPTIONAL((actual).hasAudio, (expected).hasAudio); \ + COMPARE_EQ_IGNORE_OPTIONAL((actual).hasVideo, (expected).hasVideo); \ + COMPARE_EQ_IGNORE_OPTIONAL((actual).bufferProgress, (expected).bufferProgress); \ + COMPARE_EQ_IGNORE_OPTIONAL((actual).bufferedTimeRange, (expected).bufferedTimeRange); \ + COMPARE_EQ_IGNORE_OPTIONAL((actual).isSeekable, (expected).isSeekable); \ + COMPARE_EQ_IGNORE_OPTIONAL((actual).playbackRate, (expected).playbackRate); \ + COMPARE_EQ_IGNORE_OPTIONAL((actual).isPlaying, (expected).isPlaying); \ + COMPARE_EQ_IGNORE_OPTIONAL((actual).loops, (expected).loops); \ + COMPARE_EQ_IGNORE_OPTIONAL((actual).error, (expected).error); \ + COMPARE_EQ_IGNORE_OPTIONAL((actual).isAvailable, (expected).isAvailable); \ + COMPARE_EQ_IGNORE_OPTIONAL((actual).metaData, (expected).metaData); \ + } while (false) + +#endif // MEDIAPLAYERSTATE_H diff --git a/tests/auto/integration/qmediaplayerbackend/qmediaplayerbackend.pro b/tests/auto/integration/qmediaplayerbackend/qmediaplayerbackend.pro deleted file mode 100644 index 6dd1e8d62..000000000 --- a/tests/auto/integration/qmediaplayerbackend/qmediaplayerbackend.pro +++ /dev/null @@ -1,20 +0,0 @@ -TARGET = tst_qmediaplayerbackend - -QT += multimedia-private testlib - -# This is more of a system test -CONFIG += testcase - - -SOURCES += \ - tst_qmediaplayerbackend.cpp - -HEADERS += \ - ../shared/mediafileselector.h - -TESTDATA += testdata/* - -boot2qt: { - # OGV testing is unstable with qemu - QMAKE_CXXFLAGS += -DSKIP_OGV_TEST -} diff --git a/tests/auto/integration/qmediaplayerbackend/server.h b/tests/auto/integration/qmediaplayerbackend/server.h new file mode 100644 index 000000000..a6fde1a7a --- /dev/null +++ b/tests/auto/integration/qmediaplayerbackend/server.h @@ -0,0 +1,48 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#ifndef SERVER_H +#define SERVER_H + +#include <private/qglobal_p.h> + +#ifdef QT_FEATURE_network + +#include <qstring.h> +#include <qtcpserver.h> +#include <qtest.h> +#include <qurl.h> + +QT_USE_NAMESPACE + +class UnResponsiveRtspServer : public QObject +{ + Q_OBJECT +public: + UnResponsiveRtspServer() : m_server{ new QTcpServer{ this } } + { + connect(m_server, &QTcpServer::newConnection, this, [&] { m_connected = true; }); + } + + bool listen() { return m_server->listen(QHostAddress::LocalHost); } + + bool waitForConnection() + { + return QTest::qWaitFor([this] { return m_connected; }); + } + + QUrl address() const + { + return QUrl{ QString{ "rtsp://%1:%2" } + .arg(m_server->serverAddress().toString()) + .arg(m_server->serverPort()) }; + } + +private: + QTcpServer *m_server; + bool m_connected = false; +}; + +#endif // QT_FEATURE_network + +#endif // SERVER_H diff --git a/tests/auto/integration/qmediaplayerbackend/testdata/3colors_with_sound_1s.mp4 b/tests/auto/integration/qmediaplayerbackend/testdata/3colors_with_sound_1s.mp4 Binary files differnew file mode 100644 index 000000000..96ae2e4e3 --- /dev/null +++ b/tests/auto/integration/qmediaplayerbackend/testdata/3colors_with_sound_1s.mp4 diff --git a/tests/auto/integration/qmediaplayerbackend/testdata/BigBuckBunny.mp4 b/tests/auto/integration/qmediaplayerbackend/testdata/BigBuckBunny.mp4 Binary files differnew file mode 100644 index 000000000..6cf08011c --- /dev/null +++ b/tests/auto/integration/qmediaplayerbackend/testdata/BigBuckBunny.mp4 diff --git a/tests/auto/integration/qmediaplayerbackend/testdata/audio_video_with_jpg_thumbnail.mp4 b/tests/auto/integration/qmediaplayerbackend/testdata/audio_video_with_jpg_thumbnail.mp4 Binary files differnew file mode 100644 index 000000000..dfeec1546 --- /dev/null +++ b/tests/auto/integration/qmediaplayerbackend/testdata/audio_video_with_jpg_thumbnail.mp4 diff --git a/tests/auto/integration/qmediaplayerbackend/testdata/audio_video_with_png_thumbnail.mp4 b/tests/auto/integration/qmediaplayerbackend/testdata/audio_video_with_png_thumbnail.mp4 Binary files differnew file mode 100644 index 000000000..147adf777 --- /dev/null +++ b/tests/auto/integration/qmediaplayerbackend/testdata/audio_video_with_png_thumbnail.mp4 diff --git a/tests/auto/integration/qmediaplayerbackend/testdata/busAv1.webm b/tests/auto/integration/qmediaplayerbackend/testdata/busAv1.webm Binary files differnew file mode 100644 index 000000000..048d02e8a --- /dev/null +++ b/tests/auto/integration/qmediaplayerbackend/testdata/busAv1.webm diff --git a/tests/auto/integration/qmediaplayerbackend/testdata/busMpeg4.mp4 b/tests/auto/integration/qmediaplayerbackend/testdata/busMpeg4.mp4 Binary files differnew file mode 100644 index 000000000..8824fe8c9 --- /dev/null +++ b/tests/auto/integration/qmediaplayerbackend/testdata/busMpeg4.mp4 diff --git a/tests/auto/integration/qmediaplayerbackend/testdata/color_matrix.mp4 b/tests/auto/integration/qmediaplayerbackend/testdata/color_matrix.mp4 Binary files differnew file mode 100644 index 000000000..a3661b9d2 --- /dev/null +++ b/tests/auto/integration/qmediaplayerbackend/testdata/color_matrix.mp4 diff --git a/tests/auto/integration/qmediaplayerbackend/testdata/color_matrix_180_deg_clockwise.mp4 b/tests/auto/integration/qmediaplayerbackend/testdata/color_matrix_180_deg_clockwise.mp4 Binary files differnew file mode 100644 index 000000000..9a60850d6 --- /dev/null +++ b/tests/auto/integration/qmediaplayerbackend/testdata/color_matrix_180_deg_clockwise.mp4 diff --git a/tests/auto/integration/qmediaplayerbackend/testdata/color_matrix_270_deg_clockwise.mp4 b/tests/auto/integration/qmediaplayerbackend/testdata/color_matrix_270_deg_clockwise.mp4 Binary files differnew file mode 100644 index 000000000..b3ecd486d --- /dev/null +++ b/tests/auto/integration/qmediaplayerbackend/testdata/color_matrix_270_deg_clockwise.mp4 diff --git a/tests/auto/integration/qmediaplayerbackend/testdata/color_matrix_90_deg_clockwise.mp4 b/tests/auto/integration/qmediaplayerbackend/testdata/color_matrix_90_deg_clockwise.mp4 Binary files differnew file mode 100644 index 000000000..dc620d05f --- /dev/null +++ b/tests/auto/integration/qmediaplayerbackend/testdata/color_matrix_90_deg_clockwise.mp4 diff --git a/tests/auto/integration/qmediaplayerbackend/testdata/duration_issues.webm b/tests/auto/integration/qmediaplayerbackend/testdata/duration_issues.webm Binary files differnew file mode 100644 index 000000000..87b737949 --- /dev/null +++ b/tests/auto/integration/qmediaplayerbackend/testdata/duration_issues.webm diff --git a/tests/auto/integration/qmediaplayerbackend/testdata/h264_avc1_yuv420p10le_tv_bt2020.mov b/tests/auto/integration/qmediaplayerbackend/testdata/h264_avc1_yuv420p10le_tv_bt2020.mov Binary files differnew file mode 100644 index 000000000..c5a508a1f --- /dev/null +++ b/tests/auto/integration/qmediaplayerbackend/testdata/h264_avc1_yuv420p10le_tv_bt2020.mov diff --git a/tests/auto/integration/qmediaplayerbackend/testdata/nokia-tune.mp3 b/tests/auto/integration/qmediaplayerbackend/testdata/nokia-tune.mp3 Binary files differindex 2435f65b8..892c2a89e 100644 --- a/tests/auto/integration/qmediaplayerbackend/testdata/nokia-tune.mp3 +++ b/tests/auto/integration/qmediaplayerbackend/testdata/nokia-tune.mp3 diff --git a/tests/auto/integration/qmediaplayerbackend/testdata/one_red_frame.mp4 b/tests/auto/integration/qmediaplayerbackend/testdata/one_red_frame.mp4 Binary files differnew file mode 100644 index 000000000..6b67a3433 --- /dev/null +++ b/tests/auto/integration/qmediaplayerbackend/testdata/one_red_frame.mp4 diff --git a/tests/auto/integration/qmediaplayerbackend/testdata/par_2_3.mp4 b/tests/auto/integration/qmediaplayerbackend/testdata/par_2_3.mp4 Binary files differnew file mode 100644 index 000000000..b0d9b3593 --- /dev/null +++ b/tests/auto/integration/qmediaplayerbackend/testdata/par_2_3.mp4 diff --git a/tests/auto/integration/qmediaplayerbackend/testdata/par_3_2.mp4 b/tests/auto/integration/qmediaplayerbackend/testdata/par_3_2.mp4 Binary files differnew file mode 100644 index 000000000..55baed13e --- /dev/null +++ b/tests/auto/integration/qmediaplayerbackend/testdata/par_3_2.mp4 diff --git a/tests/auto/integration/qmediaplayerbackend/tst_qmediaplayerbackend.cpp b/tests/auto/integration/qmediaplayerbackend/tst_qmediaplayerbackend.cpp index b0d84b836..209295e78 100644 --- a/tests/auto/integration/qmediaplayerbackend/tst_qmediaplayerbackend.cpp +++ b/tests/auto/integration/qmediaplayerbackend/tst_qmediaplayerbackend.cpp @@ -1,48 +1,71 @@ -/**************************************************************************** -** -** Copyright (C) 2016 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:GPL-EXCEPT$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 3 as published by the Free Software -** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #include <QtTest/QtTest> #include <QDebug> -#include <qabstractvideosurface.h> -#include "qmediaservice.h" #include "qmediaplayer.h" -#include "qaudioprobe.h" -#include "qvideoprobe.h" -#include <qmediaplaylist.h> +#include "mediaplayerstate.h" +#include "fake.h" +#include "fixture.h" +#include "server.h" #include <qmediametadata.h> - -#include "../shared/mediafileselector.h" -//TESTED_COMPONENT=src/multimedia - +#include <qaudiobuffer.h> +#include <qvideosink.h> +#include <qvideoframe.h> +#include <qaudiooutput.h> +#if QT_CONFIG(process) +#include <qprocess.h> +#endif +#include <private/qglobal_p.h> +#ifdef QT_FEATURE_network +#include <qtcpserver.h> +#endif +#include <qmediatimerange.h> +#include <private/qplatformvideosink_p.h> + +#include <QtQml/qqmlengine.h> +#include <QtQml/qqmlcomponent.h> +#include <QtQml/qqmlproperty.h> +#include <QtQuick/qquickitem.h> +#include <QtQuick/qquickview.h> +#include <QtQuick/private/qquickloader_p.h> + +#include "mediafileselector.h" +#include "mediabackendutils.h" #include <QtMultimedia/private/qtmultimedia-config_p.h> +#include "private/qquickvideooutput_p.h" + +#include <array> QT_USE_NAMESPACE +using namespace Qt::Literals; + +namespace { +static qreal colorDifference(QRgb first, QRgb second) +{ + const auto diffVector = QVector3D(qRed(first), qGreen(first), qBlue(first)) + - QVector3D(qRed(second), qGreen(second), qBlue(second)); + static const auto normalizationFactor = 1. / (255 * qSqrt(3.)); + return diffVector.length() * normalizationFactor; +} + +template <typename It> +It findSimilarColor(It it, It end, QRgb color) +{ + return std::min_element(it, end, [color](QRgb first, QRgb second) { + return colorDifference(first, color) < colorDifference(second, color); + }); +} + +template <typename Colors> +auto findSimilarColorIndex(const Colors &colors, QRgb color) +{ + return std::distance(std::begin(colors), + findSimilarColor(std::begin(colors), std::end(colors), color)); +} +} + /* This is the backend conformance test. @@ -54,529 +77,1645 @@ class tst_QMediaPlayerBackend : public QObject { Q_OBJECT public slots: - void init(); - void cleanup(); void initTestCase(); + void init() { m_fixture = std::make_unique<Fixture>(); } + void cleanup() { m_fixture = nullptr; } private slots: - void construction(); - void loadMedia(); - void unloadMedia(); - void loadMediaInLoadingState(); - void playPauseStop(); + void testMediaFilesAreSupported(); + void destructor_cancelsPreviousSetSource_whenServerDoesNotRespond(); + void destructor_emitsOnlyQObjectDestroyedSignal_whenPlayerIsRunning(); + + void getters_returnExpectedValues_whenCalledWithDefaultConstructedPlayer_data() const; + void getters_returnExpectedValues_whenCalledWithDefaultConstructedPlayer() const; + + void setSource_emitsSourceChanged_whenCalledWithInvalidFile(); + void setSource_emitsError_whenCalledWithInvalidFile(); + void setSource_emitsMediaStatusChange_whenCalledWithInvalidFile(); + void setSource_doesNotEmitPlaybackStateChange_whenCalledWithInvalidFile(); + void setSource_setsSourceMediaStatusAndError_whenCalledWithInvalidFile(); + void setSource_silentlyCancelsPreviousCall_whenServerDoesNotRespond(); + void setSource_changesSourceAndMediaStatus_whenCalledWithValidFile(); + void setSource_updatesExpectedAttributes_whenMediaHasLoaded(); + void setSource_stopsAndEntersErrorState_whenPlayerWasPlaying(); + void setSource_loadsAudioTrack_whenCalledWithValidWavFile(); + void setSource_resetsState_whenCalledWithEmptyUrl(); + void setSource_loadsNewMedia_whenPreviousMediaWasFullyLoaded(); + void setSource_loadsCorrectTracks_whenLoadingMediaInSequence(); + void setSource_remainsInStoppedState_whenPlayerWasStopped(); + void setSource_entersStoppedState_whenPlayerWasPlaying(); + void setSource_emitsError_whenSdpFileIsLoaded(); + void setSource_updatesTrackProperties_data(); + void setSource_updatesTrackProperties(); + void setSource_emitsTracksChanged_data(); + void setSource_emitsTracksChanged(); + + void setSourceAndPlay_setCorrectVideoSize_whenVideoHasNonStandardPixelAspectRatio_data(); + void setSourceAndPlay_setCorrectVideoSize_whenVideoHasNonStandardPixelAspectRatio(); + + void pause_doesNotChangePlayerState_whenInvalidFileLoaded(); + void pause_doesNothing_whenMediaIsNotLoaded(); + void pause_entersPauseState_whenPlayerWasPlaying(); + + void play_resetsErrorState_whenCalledWithInvalidFile(); + void play_resumesPlaying_whenValidMediaIsProvidedAfterInvalidMedia(); + void play_doesNothing_whenMediaIsNotLoaded(); + void play_setsPlaybackStateAndMediaStatus_whenValidFileIsLoaded(); + void play_startsPlaybackAndChangesPosition_whenValidFileIsLoaded(); + void play_doesNotEnterMediaLoadingState_whenResumingPlayingAfterStop(); + void playAndSetSource_emitsExpectedSignalsAndStopsPlayback_whenSetSourceWasCalledWithEmptyUrl(); + void play_createsFramesWithExpectedContentAndIncreasingFrameTime_whenPlayingRtspMediaStream(); + void play_waitsForLastFrameEnd_whenPlayingVideoWithLongFrames(); + void play_startsPlayback_withAndWithoutOutputsConnected(); + void play_startsPlayback_withAndWithoutOutputsConnected_data(); + void play_playsRtpStream_whenSdpFileIsLoaded(); + + void stop_entersStoppedState_whenPlayerWasPaused(); + void stop_setsPositionToZero_afterPlayingToEndOfMedia(); + + void playbackRate_returnsOne_byDefault(); + void setPlaybackRate_changesPlaybackRateAndEmitsSignal_data(); + void setPlaybackRate_changesPlaybackRateAndEmitsSignal(); + + void setVolume_changesVolume_whenVolumeIsInRange(); + void setVolume_clampsToRange_whenVolumeIsOutsideRange(); + void setVolume_doesNotChangeMutedState(); + + void setMuted_changesMutedState_whenMutedStateChanged(); + void setMuted_doesNotChangeVolume(); + void processEOS(); void deleteLaterAtEOS(); - void volumeAndMuted(); + void volumeAcrossFiles_data(); void volumeAcrossFiles(); void initialVolume(); void seekPauseSeek(); void seekInStoppedState(); void subsequentPlayback(); - void probes(); - void playlist(); - void playlistObject(); - void surfaceTest_data(); void surfaceTest(); - void multipleSurfaces(); void metadata(); + void metadata_returnsMetadataWithThumbnail_whenMediaHasThumbnail_data(); + void metadata_returnsMetadataWithThumbnail_whenMediaHasThumbnail(); + void metadata_returnsMetadataWithHasHdrContent_whenMediaHasHdrContent_data(); + void metadata_returnsMetadataWithHasHdrContent_whenMediaHasHdrContent(); void playerStateAtEOS(); void playFromBuffer(); + void audioVideoAvailable(); + void audioVideoAvailable_updatedOnNewMedia(); + void isSeekable(); + void positionAfterSeek(); + void pause_rendersVideoAtCorrectResolution_data(); + void pause_rendersVideoAtCorrectResolution(); + void position(); + void multipleMediaPlayback(); + void multiplePlaybackRateChangingStressTest(); + void multipleSeekStressTest(); + void setPlaybackRate_changesActualRateAndFramesRenderingTime_data(); + void setPlaybackRate_changesActualRateAndFramesRenderingTime(); + void durationDetectionIssues_data(); + void durationDetectionIssues(); + void finiteLoops(); + void infiniteLoops(); + void seekOnLoops(); + void changeLoopsOnTheFly(); + void seekAfterLoopReset(); + void changeVideoOutputNoFramesLost(); + void cleanSinkAndNoMoreFramesAfterStop(); + void lazyLoadVideo(); + void videoSinkSignals(); + void nonAsciiFileName(); + void setMedia_setsVideoSinkSize_beforePlaying(); + void play_playsRotatedVideoOutput_whenVideoFileHasOrientationMetadata_data(); + void play_playsRotatedVideoOutput_whenVideoFileHasOrientationMetadata(); private: - QMediaContent selectVideoFile(const QStringList& mediaCandidates); - bool isWavSupported(); + QUrl selectVideoFile(const QStringList &mediaCandidates); - //one second local wav file - QMediaContent localWavFile; - QMediaContent localWavFile2; - QMediaContent localVideoFile; - QMediaContent localCompressedSoundFile; - QMediaContent localFileWithMetadata; - - bool m_inCISystem; + bool canCreateRtpStream() const; +#if QT_CONFIG(process) + std::unique_ptr<QProcess> createRtpStreamProcess(QString fileName, QString sdpUrl); +#endif + void detectVlcCommand(); + + // one second local wav file + MaybeUrl m_localWavFile = QUnexpect{}; + MaybeUrl m_localWavFile2 = QUnexpect{}; + MaybeUrl m_localVideoFile = QUnexpect{}; + MaybeUrl m_localVideoFile2 = QUnexpect{}; + MaybeUrl m_av1File = QUnexpect{}; + MaybeUrl m_videoDimensionTestFile = QUnexpect{}; + MaybeUrl m_localCompressedSoundFile = QUnexpect{}; + MaybeUrl m_localFileWithMetadata = QUnexpect{}; + MaybeUrl m_localVideoFile3ColorsWithSound = QUnexpect{}; + MaybeUrl m_videoFileWithJpegThumbnail = QUnexpect{}; + MaybeUrl m_videoFileWithPngThumbnail = QUnexpect{}; + MaybeUrl m_oneRedFrameVideo = QUnexpect{}; + MaybeUrl m_192x108_PAR_2_3_Video = QUnexpect{}; + MaybeUrl m_192x108_PAR_3_2_Video = QUnexpect{}; + MaybeUrl m_colorMatrixVideo = QUnexpect{}; + MaybeUrl m_colorMatrix90degClockwiseVideo = QUnexpect{}; + MaybeUrl m_colorMatrix180degClockwiseVideo = QUnexpect{}; + MaybeUrl m_colorMatrix270degClockwiseVideo = QUnexpect{}; + MaybeUrl m_hdrVideo = QUnexpect{}; + + MediaFileSelector m_mediaSelector; + + const std::array<QRgb, 3> m_video3Colors = { { 0xFF0000, 0x00FF00, 0x0000FF } }; + QString m_vlcCommand; + + std::unique_ptr<Fixture> m_fixture; }; -/* - This is a simple video surface which records all presented frames. -*/ -class TestVideoSurface : public QAbstractVideoSurface +static bool commandExists(const QString &command) { - Q_OBJECT -public: - explicit TestVideoSurface(bool storeFrames = true); - void setSupportedFormats(const QList<QVideoFrame::PixelFormat>& formats) { m_supported = formats; } +#if defined(Q_OS_WINDOWS) + static constexpr QChar separator = ';'; +#else + static constexpr QChar separator = ':'; +#endif + static const QStringList pathDirs = qEnvironmentVariable("PATH").split(separator); + return std::any_of(pathDirs.cbegin(), pathDirs.cend(), [&command](const QString &dir) { + QString fullPath = QDir(dir).filePath(command); + return QFile::exists(fullPath); + }); +} - //video surface - QList<QVideoFrame::PixelFormat> supportedPixelFormats( - QAbstractVideoBuffer::HandleType handleType = QAbstractVideoBuffer::NoHandle) const; +static std::unique_ptr<QTemporaryFile> copyResourceToTemporaryFile(QString resource, + QString filePattern) +{ + QFile resourceFile(resource); + if (!resourceFile.open(QIODeviceBase::ReadOnly)) + return nullptr; - bool start(const QVideoSurfaceFormat &format); - void stop(); - bool present(const QVideoFrame &frame); + auto temporaryFile = std::make_unique<QTemporaryFile>(filePattern); + if (!temporaryFile->open()) + return nullptr; - QList<QVideoFrame> m_frameList; - int m_totalFrames; // used instead of the list when frames are not stored + QByteArray bytes = resourceFile.readAll(); + QDataStream stream(temporaryFile.get()); + stream.writeRawData(bytes.data(), bytes.length()); -private: - bool m_storeFrames; - QList<QVideoFrame::PixelFormat> m_supported; -}; + temporaryFile->close(); -class ProbeDataHandler : public QObject + return temporaryFile; +} + +void tst_QMediaPlayerBackend::detectVlcCommand() { - Q_OBJECT + m_vlcCommand = qEnvironmentVariable("QT_VLC_COMMAND"); -public: - ProbeDataHandler() : isVideoFlushCalled(false) { } + if (!m_vlcCommand.isEmpty()) + return; - QList<QVideoFrame> m_frameList; - QList<QAudioBuffer> m_bufferList; - bool isVideoFlushCalled; +#if defined(Q_OS_WINDOWS) + m_vlcCommand = "vlc.exe"; +#else + m_vlcCommand = "vlc"; +#endif + if (commandExists(m_vlcCommand)) + return; -public slots: - void processFrame(const QVideoFrame&); - void processBuffer(const QAudioBuffer&); - void flushVideo(); - void flushAudio(); -}; + m_vlcCommand.clear(); + +#if defined(Q_OS_MACOS) + m_vlcCommand = "/Applications/VLC.app/Contents/MacOS/VLC"; +#elif defined(Q_OS_WINDOWS) + m_vlcCommand = "C:/Program Files/VideoLAN/VLC/vlc.exe"; +#endif -void tst_QMediaPlayerBackend::init() + if (!QFile::exists(m_vlcCommand)) + m_vlcCommand.clear(); +} + +bool tst_QMediaPlayerBackend::canCreateRtpStream() const { + return !m_vlcCommand.isEmpty(); } -QMediaContent tst_QMediaPlayerBackend::selectVideoFile(const QStringList& mediaCandidates) +void tst_QMediaPlayerBackend::initTestCase() { - // select supported video format +#ifdef Q_OS_ANDROID + QSKIP("SKIP initTestCase on CI, because of QTBUG-118571"); +#endif + QMediaPlayer player; - TestVideoSurface *surface = new TestVideoSurface; - player.setVideoOutput(surface); + if (!player.isAvailable()) + QSKIP("Media player service is not available"); - QSignalSpy errorSpy(&player, SIGNAL(error(QMediaPlayer::Error))); + qRegisterMetaType<MaybeUrl>(); - for (const QString &s : mediaCandidates) { - QFileInfo videoFile(s); - if (!videoFile.exists()) - continue; - QMediaContent media = QMediaContent(QUrl::fromLocalFile(videoFile.absoluteFilePath())); - player.setMedia(media); - player.pause(); + m_localWavFile = m_mediaSelector.select("qrc:/testdata/test.wav"); + m_localWavFile2 = m_mediaSelector.select("qrc:/testdata/_test.wav"); - for (int i = 0; i < 2000 && surface->m_frameList.isEmpty() && errorSpy.isEmpty(); i+=50) { - QTest::qWait(50); - } + m_localVideoFile = + m_mediaSelector.select("qrc:/testdata/colors.mp4", "qrc:/testdata/colors.ogv"); - if (!surface->m_frameList.isEmpty() && errorSpy.isEmpty()) { - return media; - } - errorSpy.clear(); - } + m_localVideoFile3ColorsWithSound = + m_mediaSelector.select("qrc:/testdata/3colors_with_sound_1s.mp4"); + + m_videoFileWithJpegThumbnail = + m_mediaSelector.select("qrc:/testdata/audio_video_with_jpg_thumbnail.mp4"); + + m_videoFileWithPngThumbnail = + m_mediaSelector.select("qrc:/testdata/audio_video_with_png_thumbnail.mp4"); + +#ifndef Q_OS_MACOS // QTBUG-119711 Add support for AV1 decoding with the FFmpeg backend in online installer + m_av1File = m_mediaSelector.select("qrc:/testdata/busAv1.webm"); +#endif + + m_localVideoFile2 = + m_mediaSelector.select("qrc:/testdata/BigBuckBunny.mp4", "qrc:/testdata/busMpeg4.mp4"); + + m_videoDimensionTestFile = m_mediaSelector.select("qrc:/testdata/BigBuckBunny.mp4"); + + m_localCompressedSoundFile = + m_mediaSelector.select("qrc:/testdata/nokia-tune.mp3", "qrc:/testdata/nokia-tune.mkv"); + + m_localFileWithMetadata = m_mediaSelector.select("qrc:/testdata/nokia-tune.mp3"); + + m_oneRedFrameVideo = m_mediaSelector.select("qrc:/testdata/one_red_frame.mp4"); + + m_192x108_PAR_2_3_Video = m_mediaSelector.select("qrc:/testdata/par_2_3.mp4"); + m_192x108_PAR_3_2_Video = m_mediaSelector.select("qrc:/testdata/par_3_2.mp4"); - return QMediaContent(); + m_colorMatrixVideo = m_mediaSelector.select("qrc:/testdata/color_matrix.mp4"); + m_colorMatrix90degClockwiseVideo = + m_mediaSelector.select("qrc:/testdata/color_matrix_90_deg_clockwise.mp4"); + m_colorMatrix180degClockwiseVideo = + m_mediaSelector.select("qrc:/testdata/color_matrix_180_deg_clockwise.mp4"); + m_colorMatrix270degClockwiseVideo = + m_mediaSelector.select("qrc:/testdata/color_matrix_270_deg_clockwise.mp4"); + + m_hdrVideo = m_mediaSelector.select("qrc:/testdata/h264_avc1_yuv420p10le_tv_bt2020.mov"); + + detectVlcCommand(); } -bool tst_QMediaPlayerBackend::isWavSupported() +void tst_QMediaPlayerBackend::testMediaFilesAreSupported() { - return !localWavFile.isNull(); + const auto mediaSelectionErrors = m_mediaSelector.dumpErrors(); + if (!mediaSelectionErrors.isEmpty()) + qDebug().noquote() << "Dump media selection errors:\n" << mediaSelectionErrors; + + // TODO: probalbly, we should check errors anyway; TBD. + QCOMPARE(m_mediaSelector.failedSelectionsCount(), 0); } -void tst_QMediaPlayerBackend::initTestCase() +void tst_QMediaPlayerBackend::destructor_cancelsPreviousSetSource_whenServerDoesNotRespond() +{ +#ifdef QT_FEATURE_network + UnResponsiveRtspServer server; + QVERIFY(server.listen()); + + auto player = std::make_unique<QMediaPlayer>(); + player->setSource(server.address()); + + QVERIFY(server.waitForConnection()); + + // Cancel connection (should be fast, but can't be reliably verified + // in a test. For now we just verify that we don't crash. + player = nullptr; +#else + QSKIP("Test requires network feature"); +#endif +} + +void tst_QMediaPlayerBackend::destructor_emitsOnlyQObjectDestroyedSignal_whenPlayerIsRunning() +{ + CHECK_SELECTED_URL(m_localVideoFile3ColorsWithSound); + + // Arrange + m_fixture->player.setSource(*m_localVideoFile3ColorsWithSound); + m_fixture->player.play(); + + // Wait for started + QTRY_VERIFY(m_fixture->player.mediaStatus() == QMediaPlayer::BufferedMedia + || m_fixture->player.mediaStatus() == QMediaPlayer::EndOfMedia); + + m_fixture->clearSpies(); + + // Act + m_fixture->player.~QMediaPlayer(); + new (&m_fixture->player) QMediaPlayer; + + // Assert + QCOMPARE(m_fixture->playbackStateChanged.size(), 0); + QCOMPARE(m_fixture->errorOccurred.size(), 0); + QCOMPARE(m_fixture->sourceChanged.size(), 0); + QCOMPARE(m_fixture->mediaStatusChanged.size(), 0); + QCOMPARE(m_fixture->positionChanged.size(), 0); + QCOMPARE(m_fixture->durationChanged.size(), 0); + QCOMPARE(m_fixture->metadataChanged.size(), 0); + QCOMPARE(m_fixture->volumeChanged.size(), 0); + QCOMPARE(m_fixture->mutedChanged.size(), 0); + QCOMPARE(m_fixture->bufferProgressChanged.size(), 0); + QCOMPARE(m_fixture->destroyed.size(), 1); +} + +void tst_QMediaPlayerBackend:: + getters_returnExpectedValues_whenCalledWithDefaultConstructedPlayer_data() const +{ + QTest::addColumn<bool>("hasAudioOutput"); + QTest::addColumn<bool>("hasVideoOutput"); + QTest::addColumn<bool>("hasVideoSink"); + + QTest::newRow("noOutput") << false << false << false; + QTest::newRow("withAudioOutput") << true << false << false; + QTest::newRow("withVideoOutput") << false << true << false; + QTest::newRow("withVideoSink") << false << false << true; + QTest::newRow("withAllOutputs") << true << true << true; +} + +void tst_QMediaPlayerBackend::getters_returnExpectedValues_whenCalledWithDefaultConstructedPlayer() + const { + QFETCH(const bool, hasAudioOutput); + QFETCH(const bool, hasVideoOutput); + QFETCH(const bool, hasVideoSink); + + QAudioOutput audioOutput; + TestVideoOutput videoOutput; + QMediaPlayer player; - if (!player.isAvailable()) - QSKIP("Media player service is not available"); - qRegisterMetaType<QMediaContent>(); + if (hasAudioOutput) + player.setAudioOutput(&audioOutput); + + if (hasVideoOutput) + player.setVideoOutput(&videoOutput); + + if (hasVideoSink) + player.setVideoSink(videoOutput.videoSink()); + + MediaPlayerState expectedState = MediaPlayerState::defaultState(); + expectedState.audioOutput = hasAudioOutput ? &audioOutput : nullptr; + expectedState.videoOutput = (hasVideoOutput && !hasVideoSink) ? &videoOutput : nullptr; + expectedState.videoSink = (hasVideoSink || hasVideoOutput) ? videoOutput.videoSink() : nullptr; + + const MediaPlayerState actualState{ player }; + COMPARE_MEDIA_PLAYER_STATE_EQ(actualState, expectedState); +} + +void tst_QMediaPlayerBackend::setSource_emitsSourceChanged_whenCalledWithInvalidFile() +{ + m_fixture->player.setSource({ "Some not existing media" }); + QTRY_COMPARE_EQ(m_fixture->player.error(), QMediaPlayer::ResourceError); + + QCOMPARE_EQ(m_fixture->sourceChanged, SignalList({ { QUrl("Some not existing media") } })); +} + +void tst_QMediaPlayerBackend::setSource_emitsError_whenCalledWithInvalidFile() +{ + m_fixture->player.setSource({ "Some not existing media" }); + QTRY_COMPARE_EQ(m_fixture->player.error(), QMediaPlayer::ResourceError); + + QCOMPARE_EQ(m_fixture->errorOccurred[0][0], QMediaPlayer::ResourceError); +} + +void tst_QMediaPlayerBackend::setSource_emitsMediaStatusChange_whenCalledWithInvalidFile() +{ + m_fixture->player.setSource({ "Some not existing media" }); + QTRY_COMPARE_EQ(m_fixture->player.error(), QMediaPlayer::ResourceError); + + QCOMPARE_EQ(m_fixture->mediaStatusChanged, + SignalList({ { QMediaPlayer::LoadingMedia }, { QMediaPlayer::InvalidMedia } })); +} + +void tst_QMediaPlayerBackend::setSource_doesNotEmitPlaybackStateChange_whenCalledWithInvalidFile() +{ + m_fixture->player.setSource({ "Some not existing media" }); + QTRY_COMPARE_EQ(m_fixture->player.error(), QMediaPlayer::ResourceError); - localWavFile = MediaFileSelector::selectMediaFile(QStringList() << QFINDTESTDATA("testdata/test.wav")); - localWavFile2 = MediaFileSelector::selectMediaFile(QStringList() << QFINDTESTDATA("testdata/_test.wav"));; + QVERIFY(m_fixture->playbackStateChanged.empty()); +} + +void tst_QMediaPlayerBackend::setSource_setsSourceMediaStatusAndError_whenCalledWithInvalidFile() +{ + const QUrl invalidFile{ "Some not existing media" }; - QStringList mediaCandidates; - mediaCandidates << QFINDTESTDATA("testdata/colors.mp4"); -#ifndef SKIP_OGV_TEST - mediaCandidates << QFINDTESTDATA("testdata/colors.ogv"); + m_fixture->player.setSource(invalidFile); + QTRY_COMPARE_EQ(m_fixture->player.error(), QMediaPlayer::ResourceError); + + MediaPlayerState expectedState = MediaPlayerState::defaultState(); + expectedState.source = invalidFile; + expectedState.mediaStatus = QMediaPlayer::InvalidMedia; + expectedState.error = QMediaPlayer::ResourceError; + + const MediaPlayerState actualState{ m_fixture->player }; + + COMPARE_MEDIA_PLAYER_STATE_EQ(actualState, expectedState); +} + +void tst_QMediaPlayerBackend::setSource_silentlyCancelsPreviousCall_whenServerDoesNotRespond() +{ +#ifdef QT_FEATURE_network + CHECK_SELECTED_URL(m_localVideoFile); + + UnResponsiveRtspServer server; + + QVERIFY(server.listen()); + + m_fixture->player.setSource(server.address()); + QVERIFY(server.waitForConnection()); + + m_fixture->player.setSource(*m_localVideoFile); + + // Cancellation can not be reliably verified due to relatively short timeout, + // but we can verify that the player is in the correct state. + QTRY_COMPARE_EQ(m_fixture->player.mediaStatus(), QMediaPlayer::LoadedMedia); + + // Cancellation is silent + QVERIFY(m_fixture->errorOccurred.empty()); + + // Media status is emitted as if only one file was loaded + const SignalList expectedMediaStatus = { { QMediaPlayer::LoadingMedia }, + { QMediaPlayer::LoadedMedia } }; + QCOMPARE_EQ(m_fixture->mediaStatusChanged, expectedMediaStatus); + + // Two media source changed signals should be emitted still + const SignalList expectedSource = { { server.address() }, { *m_localVideoFile } }; + QCOMPARE_EQ(m_fixture->sourceChanged, expectedSource); + +#else + QSKIP("Test requires network feature"); #endif - localVideoFile = MediaFileSelector::selectMediaFile(mediaCandidates); +} + +void tst_QMediaPlayerBackend::setSource_changesSourceAndMediaStatus_whenCalledWithValidFile() +{ + CHECK_SELECTED_URL(m_localVideoFile); - mediaCandidates.clear(); - mediaCandidates << QFINDTESTDATA("testdata/nokia-tune.mp3"); - mediaCandidates << QFINDTESTDATA("testdata/nokia-tune.mkv"); - localCompressedSoundFile = MediaFileSelector::selectMediaFile(mediaCandidates); + m_fixture->player.setSource(*m_localVideoFile); - localFileWithMetadata = MediaFileSelector::selectMediaFile(QStringList() << QFINDTESTDATA("testdata/nokia-tune.mp3")); + QCOMPARE_EQ(m_fixture->mediaStatusChanged, SignalList({ { QMediaPlayer::LoadingMedia } })); - qgetenv("QT_TEST_CI").toInt(&m_inCISystem,10); + MediaPlayerState expectedState = MediaPlayerState::defaultState(); + expectedState.source = *m_localVideoFile; + expectedState.mediaStatus = QMediaPlayer::LoadingMedia; + + if (isGStreamerPlatform()) // gstreamer synchronously identifies file streams as seekable + expectedState.isSeekable = true; + + MediaPlayerState actualState{ m_fixture->player }; + + QSKIP_GSTREAMER("QTBUG-124005: spurious failures"); + COMPARE_MEDIA_PLAYER_STATE_EQ(actualState, expectedState); } -void tst_QMediaPlayerBackend::cleanup() +void tst_QMediaPlayerBackend::setSource_updatesExpectedAttributes_whenMediaHasLoaded() { + CHECK_SELECTED_URL(m_localVideoFile); + + m_fixture->player.setSource(*m_localVideoFile); + + QTRY_COMPARE_EQ(m_fixture->player.mediaStatus(), QMediaPlayer::LoadedMedia); + + MediaPlayerState expectedState = MediaPlayerState::defaultState(); + + // Modify all attributes that are supposed to change with this media file + // All other state variables are verified to be unchanged. + expectedState.source = *m_localVideoFile; + expectedState.mediaStatus = QMediaPlayer::LoadedMedia; + expectedState.audioTracks = std::nullopt; // Don't compare + expectedState.videoTracks = std::nullopt; // Don't compare + expectedState.activeAudioTrack = 0; + expectedState.activeVideoTrack = 0; + + if (isGStreamerPlatform()) + expectedState.duration = 15019; + else if (isDarwinPlatform()) + expectedState.duration = 15000; + else + expectedState.duration = 15018; + expectedState.hasAudio = true; + expectedState.hasVideo = true; + expectedState.isSeekable = true; + expectedState.metaData = std::nullopt; // Don't compare + + if (isGStreamerPlatform()) + expectedState.bufferProgress = std::nullopt; // QTBUG-124633: can change before play() + + MediaPlayerState actualState{ m_fixture->player }; + + COMPARE_MEDIA_PLAYER_STATE_EQ(actualState, expectedState); } -void tst_QMediaPlayerBackend::construction() +void tst_QMediaPlayerBackend::setSource_stopsAndEntersErrorState_whenPlayerWasPlaying() { - QMediaPlayer player; - QTRY_VERIFY(player.isAvailable()); + CHECK_SELECTED_URL(m_localVideoFile3ColorsWithSound); + + // Arrange + m_fixture->player.setSource(*m_localVideoFile3ColorsWithSound); + m_fixture->player.play(); + QTRY_VERIFY(m_fixture->framesCount > 0); + QCOMPARE(m_fixture->errorOccurred.size(), 0); + + // Act + m_fixture->player.setSource(QUrl("Some not existing media")); + + // Assert + const int savedFramesCount = m_fixture->framesCount; + + QCOMPARE(m_fixture->player.source(), QUrl("Some not existing media")); + + QTRY_COMPARE(m_fixture->player.playbackState(), QMediaPlayer::StoppedState); + QTRY_COMPARE(m_fixture->player.mediaStatus(), QMediaPlayer::InvalidMedia); + QTRY_COMPARE(m_fixture->player.error(), QMediaPlayer::ResourceError); + + QVERIFY(!m_fixture->surface.videoFrame().isValid()); + + QCOMPARE(m_fixture->errorOccurred.size(), 1); + + QTest::qWait(20); + QCOMPARE(m_fixture->framesCount, savedFramesCount); } -void tst_QMediaPlayerBackend::loadMedia() +void tst_QMediaPlayerBackend::setSource_loadsAudioTrack_whenCalledWithValidWavFile() { - if (!isWavSupported()) - QSKIP("Sound format is not supported"); + CHECK_SELECTED_URL(m_localWavFile); - QMediaPlayer player; + m_fixture->player.setSource(*m_localWavFile); - QCOMPARE(player.state(), QMediaPlayer::StoppedState); - QCOMPARE(player.mediaStatus(), QMediaPlayer::NoMedia); + QCOMPARE(m_fixture->player.playbackState(), QMediaPlayer::StoppedState); - QSignalSpy stateSpy(&player, SIGNAL(stateChanged(QMediaPlayer::State))); - QSignalSpy statusSpy(&player, SIGNAL(mediaStatusChanged(QMediaPlayer::MediaStatus))); - QSignalSpy mediaSpy(&player, SIGNAL(mediaChanged(QMediaContent))); - QSignalSpy currentMediaSpy(&player, SIGNAL(currentMediaChanged(QMediaContent))); + QVERIFY(m_fixture->player.mediaStatus() != QMediaPlayer::NoMedia); + QVERIFY(m_fixture->player.mediaStatus() != QMediaPlayer::InvalidMedia); + QVERIFY(m_fixture->player.source() == *m_localWavFile); - player.setMedia(localWavFile); + QCOMPARE(m_fixture->playbackStateChanged.size(), 0); + QVERIFY(m_fixture->mediaStatusChanged.size() > 0); + QCOMPARE(m_fixture->sourceChanged.size(), 1); + QCOMPARE(m_fixture->sourceChanged.last()[0].value<QUrl>(), *m_localWavFile); - QCOMPARE(player.state(), QMediaPlayer::StoppedState); + QTRY_COMPARE(m_fixture->player.mediaStatus(), QMediaPlayer::LoadedMedia); - QVERIFY(player.mediaStatus() != QMediaPlayer::NoMedia); - QVERIFY(player.mediaStatus() != QMediaPlayer::InvalidMedia); - QVERIFY(player.media() == localWavFile); - QVERIFY(player.currentMedia() == localWavFile); + QVERIFY(m_fixture->player.hasAudio()); + QVERIFY(!m_fixture->player.hasVideo()); +} - QCOMPARE(stateSpy.count(), 0); - QVERIFY(statusSpy.count() > 0); - QCOMPARE(mediaSpy.count(), 1); - QCOMPARE(mediaSpy.last()[0].value<QMediaContent>(), localWavFile); - QCOMPARE(currentMediaSpy.last()[0].value<QMediaContent>(), localWavFile); +void tst_QMediaPlayerBackend::setSource_resetsState_whenCalledWithEmptyUrl() +{ + CHECK_SELECTED_URL(m_localWavFile); - QTRY_COMPARE(player.mediaStatus(), QMediaPlayer::LoadedMedia); + // Load valid media and start playing + m_fixture->player.setSource(*m_localWavFile); - QVERIFY(player.isAudioAvailable()); - QVERIFY(!player.isVideoAvailable()); + QTRY_COMPARE(m_fixture->player.mediaStatus(), QMediaPlayer::LoadedMedia); + + QVERIFY(m_fixture->player.position() == 0); +#ifdef Q_OS_QNX + // QNX mm-renderer only updates the duration when 'play' is triggered + QVERIFY(m_fixture->player.duration() == 0); +#else + QVERIFY(m_fixture->player.duration() > 0); +#endif + + m_fixture->player.play(); + + QTRY_VERIFY(m_fixture->player.position() > 0); + QVERIFY(m_fixture->player.duration() > 0); + + // Set empty URL and verify that state is fully reset to default + m_fixture->clearSpies(); + + m_fixture->player.setSource(QUrl()); + + QVERIFY(!m_fixture->mediaStatusChanged.isEmpty()); + QVERIFY(!m_fixture->sourceChanged.isEmpty()); + + const MediaPlayerState expectedState = MediaPlayerState::defaultState(); + const MediaPlayerState actualState{ m_fixture->player }; + + COMPARE_MEDIA_PLAYER_STATE_EQ(actualState, expectedState); } -void tst_QMediaPlayerBackend::unloadMedia() +void tst_QMediaPlayerBackend::setSource_loadsNewMedia_whenPreviousMediaWasFullyLoaded() { - if (!isWavSupported()) - QSKIP("Sound format is not supported"); + CHECK_SELECTED_URL(m_localWavFile); + CHECK_SELECTED_URL(m_localWavFile2); + + // Load media and wait for it to completely load + m_fixture->player.setSource(*m_localWavFile2); + QCOMPARE(m_fixture->player.mediaStatus(), QMediaPlayer::LoadingMedia); + QTRY_COMPARE(m_fixture->player.mediaStatus(), QMediaPlayer::LoadedMedia); + + // Load another media file, play it, and wait for it to enter playing state + m_fixture->player.setSource(*m_localWavFile); + QCOMPARE(m_fixture->player.mediaStatus(), QMediaPlayer::LoadingMedia); + QTRY_COMPARE(m_fixture->player.mediaStatus(), QMediaPlayer::LoadedMedia); + m_fixture->player.play(); + QTRY_VERIFY(m_fixture->player.mediaStatus() == QMediaPlayer::BufferedMedia + || m_fixture->player.mediaStatus() == QMediaPlayer::EndOfMedia); + + // Load first file again, and wait for it to start loading + m_fixture->player.setSource(*m_localWavFile2); + QCOMPARE(m_fixture->player.mediaStatus(), QMediaPlayer::LoadingMedia); +} - QMediaPlayer player; - player.setNotifyInterval(50); +void tst_QMediaPlayerBackend::setSource_loadsCorrectTracks_whenLoadingMediaInSequence() +{ + CHECK_SELECTED_URL(m_localVideoFile3ColorsWithSound); + CHECK_SELECTED_URL(m_localWavFile2); - QSignalSpy stateSpy(&player, SIGNAL(stateChanged(QMediaPlayer::State))); - QSignalSpy statusSpy(&player, SIGNAL(mediaStatusChanged(QMediaPlayer::MediaStatus))); - QSignalSpy mediaSpy(&player, SIGNAL(mediaChanged(QMediaContent))); - QSignalSpy currentMediaSpy(&player, SIGNAL(currentMediaChanged(QMediaContent))); - QSignalSpy positionSpy(&player, SIGNAL(positionChanged(qint64))); - QSignalSpy durationSpy(&player, SIGNAL(positionChanged(qint64))); + // Load audio/video file, play it, and verify that both tracks are loaded + m_fixture->player.setSource(*m_localVideoFile3ColorsWithSound); + m_fixture->player.play(); + QTRY_COMPARE_EQ(m_fixture->player.playbackState(), QMediaPlayer::PlayingState); + QVERIFY(m_fixture->surface.waitForFrame().isValid()); + QVERIFY(m_fixture->player.hasAudio()); + QVERIFY(m_fixture->player.hasVideo()); - player.setMedia(localWavFile); + m_fixture->clearSpies(); - QTRY_COMPARE(player.mediaStatus(), QMediaPlayer::LoadedMedia); + // Load an audio file, and verify that only audio track is loaded + m_fixture->player.setSource(*m_localWavFile2); - QVERIFY(player.position() == 0); - QVERIFY(player.duration() > 0); + QTRY_COMPARE_EQ(m_fixture->player.mediaStatus(), QMediaPlayer::MediaStatus::LoadedMedia); - player.play(); + QCOMPARE(m_fixture->player.source(), *m_localWavFile2); + QCOMPARE(m_fixture->player.playbackState(), QMediaPlayer::StoppedState); + QCOMPARE(m_fixture->playbackStateChanged.size(), 1); + QCOMPARE(m_fixture->errorOccurred.size(), 0); + QVERIFY(m_fixture->player.hasAudio()); + QVERIFY(!m_fixture->player.hasVideo()); + QVERIFY(!m_fixture->surface.videoFrame().isValid()); - QTRY_VERIFY(player.position() > 0); - QVERIFY(player.duration() > 0); + m_fixture->player.play(); - stateSpy.clear(); - statusSpy.clear(); - mediaSpy.clear(); - currentMediaSpy.clear(); - positionSpy.clear(); - durationSpy.clear(); + // Load video only file, and verify that only video track is loaded + m_fixture->player.setSource(*m_localVideoFile2); - player.setMedia(QMediaContent()); + QTRY_COMPARE_EQ(m_fixture->player.mediaStatus(), QMediaPlayer::MediaStatus::LoadedMedia); - QVERIFY(player.position() <= 0); - QVERIFY(player.duration() <= 0); - QCOMPARE(player.state(), QMediaPlayer::StoppedState); - QCOMPARE(player.mediaStatus(), QMediaPlayer::NoMedia); - QCOMPARE(player.media(), QMediaContent()); - QCOMPARE(player.currentMedia(), QMediaContent()); + QCOMPARE(m_fixture->player.playbackState(), QMediaPlayer::StoppedState); + QVERIFY(m_fixture->player.hasVideo()); + QVERIFY(!m_fixture->player.hasAudio()); + QCOMPARE(m_fixture->errorOccurred.size(), 0); +} - QVERIFY(!stateSpy.isEmpty()); - QVERIFY(!statusSpy.isEmpty()); - QVERIFY(!mediaSpy.isEmpty()); - QVERIFY(!currentMediaSpy.isEmpty()); - QVERIFY(!positionSpy.isEmpty()); +void tst_QMediaPlayerBackend::setSource_remainsInStoppedState_whenPlayerWasStopped() +{ + CHECK_SELECTED_URL(m_localWavFile); + CHECK_SELECTED_URL(m_localWavFile2); + + // Arrange + m_fixture->player.setSource(*m_localWavFile); + m_fixture->player.play(); + QTRY_VERIFY(m_fixture->player.position() > 100); + m_fixture->player.stop(); + m_fixture->clearSpies(); + + // Act + m_fixture->player.setSource(*m_localWavFile2); + + // Assert + QTRY_VERIFY(m_fixture->mediaStatusChanged.size() > 0); + QTRY_COMPARE(m_fixture->player.mediaStatus(), QMediaPlayer::LoadedMedia); + QCOMPARE_EQ(m_fixture->mediaStatusChanged, + SignalList({ { QMediaPlayer::LoadingMedia }, { QMediaPlayer::LoadedMedia } })); + QCOMPARE(m_fixture->player.playbackState(), QMediaPlayer::StoppedState); + QVERIFY(m_fixture->playbackStateChanged.empty()); } -void tst_QMediaPlayerBackend::loadMediaInLoadingState() +void tst_QMediaPlayerBackend::setSource_entersStoppedState_whenPlayerWasPlaying() { - if (!isWavSupported()) - QSKIP("Sound format is not supported"); + CHECK_SELECTED_URL(m_localWavFile); + CHECK_SELECTED_URL(m_localWavFile2); + + // Arrange + m_fixture->player.setSource(*m_localWavFile2); + m_fixture->clearSpies(); + m_fixture->player.play(); + QTRY_VERIFY(m_fixture->player.position() > 100); + + // Act + m_fixture->player.setSource(*m_localWavFile); + + // Assert + QTRY_COMPARE(m_fixture->player.mediaStatus(), QMediaPlayer::LoadedMedia); + if (!isGStreamerPlatform()) { + // QTBUG-124005: GStreamer has lots of state changes + QTRY_COMPARE(m_fixture->mediaStatusChanged, + SignalList({ { QMediaPlayer::LoadedMedia }, + { QMediaPlayer::BufferingMedia }, + { QMediaPlayer::BufferedMedia }, + { QMediaPlayer::LoadedMedia }, + { QMediaPlayer::LoadingMedia }, + { QMediaPlayer::LoadedMedia } })); + } - QMediaPlayer player; - player.setMedia(localWavFile); - player.play(); - QCOMPARE(player.mediaStatus(), QMediaPlayer::LoadingMedia); - // Sets new media while old has not been finished. - player.setMedia(localWavFile); - QCOMPARE(player.mediaStatus(), QMediaPlayer::LoadingMedia); - QTRY_COMPARE(player.mediaStatus(), QMediaPlayer::LoadedMedia); + QCOMPARE(m_fixture->player.playbackState(), QMediaPlayer::StoppedState); + QTRY_COMPARE(m_fixture->playbackStateChanged, + SignalList({ { QMediaPlayer::PlayingState }, { QMediaPlayer::StoppedState } })); + + QTRY_VERIFY(!m_fixture->positionChanged.empty() + && m_fixture->positionChanged.last()[0].value<qint64>() == 0); + + QCOMPARE(m_fixture->player.position(), 0); } -void tst_QMediaPlayerBackend::playPauseStop() +void tst_QMediaPlayerBackend::setSource_emitsError_whenSdpFileIsLoaded() { - if (!isWavSupported()) - QSKIP("Sound format is not supported"); +#if !QT_CONFIG(process) + QSKIP("This test requires QProcess support"); +#else + // NOTE: This test checks that playing rtp streams using local .sdp file as a source is blocked + // by default. For when the user wants to override these defaults, see + // play_playsRtpStream_whenSdpFileIsLoaded - QMediaPlayer player; - player.setNotifyInterval(50); + if (!isFFMPEGPlatform()) + QSKIP("This test is only for FFmpeg backend"); - QSignalSpy stateSpy(&player, SIGNAL(stateChanged(QMediaPlayer::State))); - QSignalSpy statusSpy(&player, SIGNAL(mediaStatusChanged(QMediaPlayer::MediaStatus))); - QSignalSpy positionSpy(&player, SIGNAL(positionChanged(qint64))); - QSignalSpy errorSpy(&player, SIGNAL(error(QMediaPlayer::Error))); + // Create stream + if (!canCreateRtpStream()) + QSKIP("Rtp stream cannot be created"); - // Check play() without a media - player.play(); + // Make sure the default whitelist is used + qunsetenv("QT_FFMPEG_PROTOCOL_WHITELIST"); - QCOMPARE(player.state(), QMediaPlayer::StoppedState); - QCOMPARE(player.mediaStatus(), QMediaPlayer::NoMedia); - QCOMPARE(player.error(), QMediaPlayer::NoError); - QCOMPARE(player.position(), 0); - QCOMPARE(stateSpy.count(), 0); - QCOMPARE(statusSpy.count(), 0); - QCOMPARE(positionSpy.count(), 0); - QCOMPARE(errorSpy.count(), 0); + auto temporaryFile = copyResourceToTemporaryFile(":/testdata/colors.mp4", "colors.XXXXXX.mp4"); + QVERIFY(temporaryFile); - // Check pause() without a media - player.pause(); + // Pass a "file:" URL to VLC in order to generate an .sdp file + const QUrl sdpUrl = QUrl::fromLocalFile(QFileInfo("test.sdp").absoluteFilePath()); - QCOMPARE(player.state(), QMediaPlayer::StoppedState); - QCOMPARE(player.mediaStatus(), QMediaPlayer::NoMedia); - QCOMPARE(player.error(), QMediaPlayer::NoError); - QCOMPARE(player.position(), 0); - QCOMPARE(stateSpy.count(), 0); - QCOMPARE(statusSpy.count(), 0); - QCOMPARE(positionSpy.count(), 0); - QCOMPARE(errorSpy.count(), 0); + auto process = createRtpStreamProcess(temporaryFile->fileName(), sdpUrl.toString()); + QVERIFY2(process, "Cannot start rtp process"); - // The rest is with a valid media + auto processCloser = qScopeGuard([&process, &sdpUrl]() { + // End stream + process->close(); - player.setMedia(localWavFile); + // Remove .sdp file created by VLC + QFile(sdpUrl.toLocalFile()).remove(); + }); - QCOMPARE(player.position(), qint64(0)); + m_fixture->player.setSource(sdpUrl); + QTRY_COMPARE_EQ(m_fixture->player.error(), QMediaPlayer::ResourceError); +#endif // QT_CONFIG(process) +} - player.play(); +void tst_QMediaPlayerBackend::setSource_updatesTrackProperties_data() +{ + QTest::addColumn<MaybeUrl>("url"); + QTest::addColumn<int>("numberOfVideoTracks"); + QTest::addColumn<int>("numberOfAudioTracks"); + QTest::addColumn<int>("numberOfSubtitleTracks"); + + QTest::addRow("video file with audio") << m_localVideoFile3ColorsWithSound << 1 << 1 << 0; + QTest::addRow("video file without audio") << m_colorMatrixVideo << 1 << 0 << 0; + QTest::addRow("uncompressed audio file") << m_localWavFile << 0 << 1 << 0; + QTest::addRow("compressed audio file") << m_localCompressedSoundFile << 0 << 1 << 0; +} - QCOMPARE(player.state(), QMediaPlayer::PlayingState); +void tst_QMediaPlayerBackend::setSource_updatesTrackProperties() +{ + QFETCH(MaybeUrl, url); + QFETCH(int, numberOfVideoTracks); + QFETCH(int, numberOfAudioTracks); + QFETCH(int, numberOfSubtitleTracks); - QTRY_COMPARE(player.mediaStatus(), QMediaPlayer::BufferedMedia); + QMediaPlayer &player = m_fixture->player; - QCOMPARE(stateSpy.count(), 1); - QCOMPARE(stateSpy.last()[0].value<QMediaPlayer::State>(), QMediaPlayer::PlayingState); - QTRY_VERIFY(statusSpy.count() > 0 && - statusSpy.last()[0].value<QMediaPlayer::MediaStatus>() == QMediaPlayer::BufferedMedia); + CHECK_SELECTED_URL(url); - QTRY_VERIFY(player.position() > 100); - QVERIFY(player.duration() > 0); - QVERIFY(positionSpy.count() > 0); - QVERIFY(positionSpy.last()[0].value<qint64>() > 0); + player.setSource(*url); - stateSpy.clear(); - statusSpy.clear(); - positionSpy.clear(); + QTRY_COMPARE(player.videoTracks().size(), numberOfVideoTracks); + QTRY_COMPARE(player.audioTracks().size(), numberOfAudioTracks); + QTRY_COMPARE(player.subtitleTracks().size(), numberOfSubtitleTracks); +} - qint64 positionBeforePause = player.position(); - player.pause(); +void tst_QMediaPlayerBackend::setSource_emitsTracksChanged_data() +{ + QTest::addColumn<MaybeUrl>("url"); + QTest::addColumn<int>("numberOfVideoTracks"); + QTest::addColumn<int>("numberOfAudioTracks"); + QTest::addColumn<int>("numberOfSubtitleTracks"); + + QTest::addRow("video file with audio") << m_localVideoFile3ColorsWithSound << 1 << 1 << 0; + QTest::addRow("video file without audio") << m_colorMatrixVideo << 1 << 0 << 0; + QTest::addRow("uncompressed audio file") << m_localWavFile << 0 << 1 << 0; + QTest::addRow("compressed audio file") << m_localCompressedSoundFile << 0 << 1 << 0; +} - QCOMPARE(player.state(), QMediaPlayer::PausedState); - QCOMPARE(player.mediaStatus(), QMediaPlayer::BufferedMedia); +void tst_QMediaPlayerBackend::setSource_emitsTracksChanged() +{ + QFETCH(MaybeUrl, url); + QFETCH(int, numberOfVideoTracks); + QFETCH(int, numberOfAudioTracks); + QFETCH(int, numberOfSubtitleTracks); - QCOMPARE(stateSpy.count(), 1); - QCOMPARE(stateSpy.last()[0].value<QMediaPlayer::State>(), QMediaPlayer::PausedState); + QMediaPlayer &player = m_fixture->player; - QTest::qWait(2000); + CHECK_SELECTED_URL(url); - QVERIFY(qAbs(player.position() - positionBeforePause) < 150); - QCOMPARE(positionSpy.count(), 1); + QSignalSpy tracksChanged(&player, &QMediaPlayer::tracksChanged); + player.setSource(*url); - stateSpy.clear(); - statusSpy.clear(); + QVERIFY(tracksChanged.wait()); - player.stop(); + QCOMPARE(player.videoTracks().size(), numberOfVideoTracks); + QCOMPARE(player.audioTracks().size(), numberOfAudioTracks); + QCOMPARE(player.subtitleTracks().size(), numberOfSubtitleTracks); +} - QCOMPARE(player.state(), QMediaPlayer::StoppedState); - QTRY_COMPARE(player.mediaStatus(), QMediaPlayer::LoadedMedia); +void tst_QMediaPlayerBackend:: + setSourceAndPlay_setCorrectVideoSize_whenVideoHasNonStandardPixelAspectRatio_data() +{ + QTest::addColumn<MaybeUrl>("url"); + QTest::addColumn<QSize>("expectedVideoSize"); + + QTest::addRow("Horizontal expanding (par=3/2)") + << m_192x108_PAR_3_2_Video << QSize(192 * 3 / 2, 108); + + if (isGStreamerPlatform()) + // QTBUG-125249: gstreamer tries "to keep the input height (because of interlacing)" + QTest::addRow("Horizontal shrinking (par=2/3)") + << m_192x108_PAR_2_3_Video << QSize(192 * 2 / 3, 108); + else + QTest::addRow("Vertical expanding (par=2/3)") + << m_192x108_PAR_2_3_Video << QSize(192, 108 * 3 / 2); +} - QCOMPARE(stateSpy.count(), 1); - QCOMPARE(stateSpy.last()[0].value<QMediaPlayer::State>(), QMediaPlayer::StoppedState); - //it's allowed to emit statusChanged() signal async - QTRY_COMPARE(statusSpy.count(), 1); - QCOMPARE(statusSpy.last()[0].value<QMediaPlayer::MediaStatus>(), QMediaPlayer::LoadedMedia); +void tst_QMediaPlayerBackend:: + setSourceAndPlay_setCorrectVideoSize_whenVideoHasNonStandardPixelAspectRatio() +{ + if (isGStreamerPlatform() && isCI()) + QSKIP("QTBUG-124005: Fails with gstreamer on CI"); - //ensure the position is reset to 0 at stop and positionChanged(0) is emitted - QCOMPARE(player.position(), qint64(0)); - QCOMPARE(positionSpy.last()[0].value<qint64>(), qint64(0)); - QVERIFY(player.duration() > 0); + QFETCH(MaybeUrl, url); + QFETCH(QSize, expectedVideoSize); - stateSpy.clear(); - statusSpy.clear(); - positionSpy.clear(); + CHECK_SELECTED_URL(url); - player.play(); + m_fixture->player.setSource(*url); + QTRY_COMPARE(m_fixture->player.mediaStatus(), QMediaPlayer::LoadedMedia); + QCOMPARE(m_fixture->player.metaData().value(QMediaMetaData::Resolution), QSize(192, 108)); - QCOMPARE(player.state(), QMediaPlayer::PlayingState); - QTRY_COMPARE(player.mediaStatus(), QMediaPlayer::BufferedMedia); - QCOMPARE(stateSpy.count(), 1); - QCOMPARE(stateSpy.last()[0].value<QMediaPlayer::State>(), QMediaPlayer::PlayingState); - QCOMPARE(statusSpy.count(), 1); // Should not go through Loading again when play -> stop -> play - QCOMPARE(statusSpy.last()[0].value<QMediaPlayer::MediaStatus>(), QMediaPlayer::BufferedMedia); + QCOMPARE(m_fixture->surface.videoSize(), expectedVideoSize); - player.stop(); - stateSpy.clear(); - statusSpy.clear(); - positionSpy.clear(); + m_fixture->player.play(); - player.setMedia(localWavFile2); + auto frame = m_fixture->surface.waitForFrame(); + QVERIFY(frame.isValid()); + QCOMPARE(frame.size(), expectedVideoSize); + QCOMPARE(frame.surfaceFormat().frameSize(), expectedVideoSize); + QCOMPARE(frame.surfaceFormat().viewport(), QRect(QPoint(), expectedVideoSize)); - QTRY_VERIFY(statusSpy.count() > 0); - QTRY_COMPARE(player.mediaStatus(), QMediaPlayer::LoadedMedia); - QCOMPARE(statusSpy.last()[0].value<QMediaPlayer::MediaStatus>(), QMediaPlayer::LoadedMedia); - QCOMPARE(player.state(), QMediaPlayer::StoppedState); - QCOMPARE(stateSpy.count(), 0); +#ifdef Q_OS_ANDROID + QSKIP("frame.toImage will return null image because of QTBUG-108446"); +#endif - player.play(); + auto image = frame.toImage(); + QCOMPARE(frame.size(), expectedVideoSize); + + // clang-format off + + // Video schema: + // + // 192 + // *---------------------* + // | White | | + // | | | + // |----------/ | 108 + // | Red | + // | | + // *---------------------* + + // clang-format on + + // check the proper scaling + const std::vector<QRgb> colors = { 0xFFFFFF, 0xFF0000, 0xFF00, 0xFF, 0x0 }; + + const auto pixelsOffset = 4; + const auto halfSize = expectedVideoSize / 2; + + QCOMPARE(findSimilarColorIndex(colors, image.pixel(0, 0)), 0); + QCOMPARE(findSimilarColorIndex(colors, image.pixel(halfSize.width() - pixelsOffset, 0)), 0); + QCOMPARE(findSimilarColorIndex(colors, image.pixel(0, halfSize.height() - pixelsOffset)), 0); + QCOMPARE(findSimilarColorIndex(colors, + image.pixel(halfSize.width() - pixelsOffset, + halfSize.height() - pixelsOffset)), + 0); + + QCOMPARE(findSimilarColorIndex(colors, image.pixel(halfSize.width() + pixelsOffset, 0)), 1); + QCOMPARE(findSimilarColorIndex(colors, image.pixel(0, halfSize.height() + pixelsOffset)), 1); + QCOMPARE(findSimilarColorIndex(colors, + image.pixel(halfSize.width() + pixelsOffset, + halfSize.height() + pixelsOffset)), + 1); +} - QTRY_VERIFY(player.position() > 100); +void tst_QMediaPlayerBackend::pause_doesNotChangePlayerState_whenInvalidFileLoaded() +{ + m_fixture->player.setSource({ "Some not existing media" }); + QTRY_COMPARE_EQ(m_fixture->player.error(), QMediaPlayer::ResourceError); - player.setMedia(localWavFile); + const MediaPlayerState expectedState{ m_fixture->player }; - QTRY_COMPARE(player.mediaStatus(), QMediaPlayer::LoadedMedia); - QCOMPARE(statusSpy.last()[0].value<QMediaPlayer::MediaStatus>(), QMediaPlayer::LoadedMedia); - QCOMPARE(player.state(), QMediaPlayer::StoppedState); - QCOMPARE(stateSpy.last()[0].value<QMediaPlayer::State>(), QMediaPlayer::StoppedState); - QCOMPARE(player.position(), 0); - QCOMPARE(positionSpy.last()[0].value<qint64>(), 0); + m_fixture->player.pause(); - stateSpy.clear(); - statusSpy.clear(); - positionSpy.clear(); + const MediaPlayerState actualState{ m_fixture->player }; - player.play(); + COMPARE_MEDIA_PLAYER_STATE_EQ(actualState, expectedState); +} + +void tst_QMediaPlayerBackend::pause_doesNothing_whenMediaIsNotLoaded() +{ + m_fixture->player.pause(); - QTRY_VERIFY(player.position() > 100); + const MediaPlayerState expectedState = MediaPlayerState::defaultState(); + const MediaPlayerState actualState{ m_fixture->player }; - player.setMedia(QMediaContent()); + COMPARE_MEDIA_PLAYER_STATE_EQ(actualState, expectedState); - QTRY_COMPARE(player.mediaStatus(), QMediaPlayer::NoMedia); - QCOMPARE(statusSpy.last()[0].value<QMediaPlayer::MediaStatus>(), QMediaPlayer::NoMedia); - QCOMPARE(player.state(), QMediaPlayer::StoppedState); - QCOMPARE(stateSpy.last()[0].value<QMediaPlayer::State>(), QMediaPlayer::StoppedState); - QCOMPARE(player.position(), 0); - QCOMPARE(positionSpy.last()[0].value<qint64>(), 0); - QCOMPARE(player.duration(), 0); + QVERIFY(m_fixture->playbackStateChanged.empty()); + QVERIFY(m_fixture->mediaStatusChanged.empty()); + QVERIFY(m_fixture->positionChanged.empty()); + QVERIFY(m_fixture->errorOccurred.empty()); } +void tst_QMediaPlayerBackend::pause_entersPauseState_whenPlayerWasPlaying() +{ + CHECK_SELECTED_URL(m_localWavFile); -void tst_QMediaPlayerBackend::processEOS() + // Arrange + m_fixture->player.setSource(*m_localWavFile); + m_fixture->player.play(); + QTRY_COMPARE_GT(m_fixture->player.position(), 100); + m_fixture->clearSpies(); + const qint64 positionBeforePause = m_fixture->player.position(); + + // Act + m_fixture->player.pause(); + + // Assert + QCOMPARE(m_fixture->player.playbackState(), QMediaPlayer::PausedState); + QCOMPARE_EQ(m_fixture->playbackStateChanged, SignalList({ { QMediaPlayer::PausedState } })); + QTRY_COMPARE(m_fixture->player.mediaStatus(), QMediaPlayer::BufferedMedia); + + QTRY_COMPARE_LT(qAbs(m_fixture->player.position() - positionBeforePause), 200); + + QTest::qWait(500); + + QTRY_COMPARE_LT(qAbs(m_fixture->player.position() - positionBeforePause), 200); +} + +void tst_QMediaPlayerBackend::play_resetsErrorState_whenCalledWithInvalidFile() +{ + m_fixture->player.setSource({ "Some not existing media" }); + QTRY_COMPARE_EQ(m_fixture->player.error(), QMediaPlayer::ResourceError); + + MediaPlayerState expectedState{ m_fixture->player }; + + m_fixture->player.play(); + + expectedState.error = QMediaPlayer::NoError; + COMPARE_MEDIA_PLAYER_STATE_EQ(MediaPlayerState{ m_fixture->player }, expectedState); + + QTest::qWait(150); // wait a bit and check position is not changed + + COMPARE_MEDIA_PLAYER_STATE_EQ(MediaPlayerState{ m_fixture->player }, expectedState); + QCOMPARE(m_fixture->surface.m_totalFrames, 0); +} + +void tst_QMediaPlayerBackend::play_resumesPlaying_whenValidMediaIsProvidedAfterInvalidMedia() +{ + CHECK_SELECTED_URL(m_localVideoFile3ColorsWithSound); + + // Arrange + m_fixture->player.setSource(*m_localVideoFile3ColorsWithSound); + m_fixture->player.play(); + QTRY_VERIFY(m_fixture->framesCount > 0); + m_fixture->player.setSource(QUrl("Some not existing media")); + QTRY_COMPARE(m_fixture->player.error(), QMediaPlayer::ResourceError); + m_fixture->player.setSource(*m_localVideoFile3ColorsWithSound); + + // Act + m_fixture->player.play(); + + // Assert + QTRY_VERIFY(m_fixture->framesCount > 0); + QTRY_VERIFY(m_fixture->player.mediaStatus() == QMediaPlayer::BufferedMedia + || m_fixture->player.mediaStatus() == QMediaPlayer::EndOfMedia); + QCOMPARE_EQ(m_fixture->player.playbackState(), QMediaPlayer::PlayingState); + QCOMPARE(m_fixture->player.error(), QMediaPlayer::NoError); +} + +void tst_QMediaPlayerBackend::play_doesNothing_whenMediaIsNotLoaded() { - if (!isWavSupported()) - QSKIP("Sound format is not supported"); + m_fixture->player.play(); + + const MediaPlayerState expectedState = MediaPlayerState::defaultState(); + const MediaPlayerState actualState{ m_fixture->player }; + + COMPARE_MEDIA_PLAYER_STATE_EQ(actualState, expectedState); + QVERIFY(m_fixture->playbackStateChanged.empty()); + QVERIFY(m_fixture->mediaStatusChanged.empty()); + QVERIFY(m_fixture->positionChanged.empty()); + QVERIFY(m_fixture->errorOccurred.empty()); +} + +void tst_QMediaPlayerBackend::play_setsPlaybackStateAndMediaStatus_whenValidFileIsLoaded() +{ + CHECK_SELECTED_URL(m_localVideoFile); + + m_fixture->player.setSource(*m_localVideoFile); + m_fixture->player.play(); + + QTRY_COMPARE_EQ(m_fixture->player.playbackState(), QMediaPlayer::PlayingState); + QTRY_COMPARE_EQ(m_fixture->player.mediaStatus(), QMediaPlayer::BufferedMedia); + + QCOMPARE(m_fixture->playbackStateChanged, SignalList({ { QMediaPlayer::PlayingState } })); + + if (!isGStreamerPlatform()) { + // QTBUG-124005: GStreamer has lots of state changes + QTRY_COMPARE_EQ(m_fixture->mediaStatusChanged, + SignalList({ { QMediaPlayer::LoadingMedia }, + { QMediaPlayer::LoadedMedia }, + { QMediaPlayer::BufferingMedia }, + { QMediaPlayer::BufferedMedia } })); + } + + QTRY_COMPARE_GT(m_fixture->bufferProgressChanged.size(), 0); + QTRY_COMPARE_NE(m_fixture->bufferProgressChanged.front().front(), 0.f); + QTRY_COMPARE(m_fixture->bufferProgressChanged.back().front(), 1.f); +} + +void tst_QMediaPlayerBackend::play_startsPlaybackAndChangesPosition_whenValidFileIsLoaded() +{ + CHECK_SELECTED_URL(m_localVideoFile); + + m_fixture->player.setSource(*m_localVideoFile); + m_fixture->player.play(); + + QTRY_VERIFY(m_fixture->player.position() > 100); + QTRY_VERIFY(!m_fixture->durationChanged.empty()); + QTRY_VERIFY(!m_fixture->positionChanged.empty()); + QTRY_VERIFY(m_fixture->positionChanged.last()[0].value<qint64>() > 100); +} + +void tst_QMediaPlayerBackend::play_doesNotEnterMediaLoadingState_whenResumingPlayingAfterStop() +{ + CHECK_SELECTED_URL(m_localWavFile); + + // Arrange: go through a play->pause->stop sequence + m_fixture->player.setSource(*m_localWavFile); + m_fixture->player.play(); + QTRY_VERIFY(m_fixture->player.position() > 100); + m_fixture->player.pause(); + QTRY_COMPARE(m_fixture->player.mediaStatus(), QMediaPlayer::BufferedMedia); + m_fixture->player.stop(); + m_fixture->clearSpies(); + + // Act + m_fixture->player.play(); + + // Assert + QCOMPARE(m_fixture->player.playbackState(), QMediaPlayer::PlayingState); + QTRY_VERIFY(m_fixture->player.mediaStatus() == QMediaPlayer::BufferedMedia + || m_fixture->player.mediaStatus() == QMediaPlayer::EndOfMedia); + QTRY_VERIFY(m_fixture->playbackStateChanged.contains({ QMediaPlayer::PlayingState })); + + // Note: Should not go through Loading again when play -> stop -> play + if (!isGStreamerPlatform()) { + QCOMPARE_EQ(m_fixture->mediaStatusChanged, + SignalList({ + { QMediaPlayer::BufferingMedia }, + { QMediaPlayer::BufferedMedia }, + })); + } else { + QCOMPARE_EQ(m_fixture->mediaStatusChanged, + // gstreamer may see EndOfMedia + SignalList({ + { QMediaPlayer::BufferingMedia }, + { QMediaPlayer::BufferedMedia }, + { QMediaPlayer::EndOfMedia }, + })); + } +} + +void tst_QMediaPlayerBackend::playAndSetSource_emitsExpectedSignalsAndStopsPlayback_whenSetSourceWasCalledWithEmptyUrl() +{ + CHECK_SELECTED_URL(m_localWavFile2); + + // Arrange + m_fixture->player.setSource(*m_localWavFile2); + m_fixture->clearSpies(); + + // Act + m_fixture->player.play(); + QTRY_VERIFY(m_fixture->player.position() > 100); + m_fixture->player.setSource(QUrl()); + + // Assert + const MediaPlayerState expectedState = MediaPlayerState::defaultState(); + const MediaPlayerState actualState{ m_fixture->player }; + COMPARE_MEDIA_PLAYER_STATE_EQ(actualState, expectedState); + + if (!isGStreamerPlatform()) { + // QTBUG-124005: GStreamer may see QMediaPlayer::EndOfMedia + QTRY_COMPARE_EQ(m_fixture->mediaStatusChanged, + SignalList({ { QMediaPlayer::LoadedMedia }, + { QMediaPlayer::BufferingMedia }, + { QMediaPlayer::BufferedMedia }, + { QMediaPlayer::LoadedMedia }, + { QMediaPlayer::NoMedia } })); + } + + QTRY_COMPARE_EQ(m_fixture->playbackStateChanged, + SignalList({ { QMediaPlayer::PlayingState }, { QMediaPlayer::StoppedState } })); + + QTRY_VERIFY(m_fixture->positionChanged.size() > 0); + QCOMPARE(m_fixture->positionChanged.last()[0].value<qint64>(), 0); +} + +void tst_QMediaPlayerBackend:: + play_createsFramesWithExpectedContentAndIncreasingFrameTime_whenPlayingRtspMediaStream() +{ +#if !QT_CONFIG(process) + QSKIP("This test requires QProcess support"); +#else + if (!canCreateRtpStream()) + QSKIP("Rtsp stream cannot be created"); + + QSKIP_GSTREAMER("GStreamer tests fail"); + + auto temporaryFile = copyResourceToTemporaryFile(":/testdata/colors.mp4", "colors.XXXXXX.mp4"); + QVERIFY(temporaryFile); + + const QString streamUrl = "rtsp://localhost:8083/stream"; + + auto process = createRtpStreamProcess(temporaryFile->fileName(), streamUrl); + QVERIFY2(process, "Cannot start rtsp process"); + + auto processCloser = qScopeGuard([&process]() { process->close(); }); + + TestVideoSink surface(false); QMediaPlayer player; - player.setNotifyInterval(50); - QSignalSpy stateSpy(&player, SIGNAL(stateChanged(QMediaPlayer::State))); - QSignalSpy statusSpy(&player, SIGNAL(mediaStatusChanged(QMediaPlayer::MediaStatus))); - QSignalSpy positionSpy(&player, SIGNAL(positionChanged(qint64))); + QSignalSpy errorSpy(&player, &QMediaPlayer::errorOccurred); + + player.setVideoSink(&surface); + // Ignore audio output to check timings accuratelly + // player.setAudioOutput(&output); - player.setMedia(localWavFile); + player.setSource(streamUrl); player.play(); - player.setPosition(900); - //wait up to 5 seconds for EOS - QTRY_COMPARE(player.mediaStatus(), QMediaPlayer::EndOfMedia); + QTRY_COMPARE(player.playbackState(), QMediaPlayer::PlayingState); - QVERIFY(statusSpy.count() > 0); - QCOMPARE(statusSpy.last()[0].value<QMediaPlayer::MediaStatus>(), QMediaPlayer::EndOfMedia); - QCOMPARE(player.state(), QMediaPlayer::StoppedState); - QCOMPARE(stateSpy.count(), 2); - QCOMPARE(stateSpy.last()[0].value<QMediaPlayer::State>(), QMediaPlayer::StoppedState); + const auto colors = { qRgb(0, 0, 0xFF), qRgb(0xFF, 0, 0), qRgb(0, 0xFE, 0) }; + const auto colorInterval = 5000; - //at EOS the position stays at the end of file - QCOMPARE(player.position(), player.duration()); - QVERIFY(positionSpy.count() > 0); - QCOMPARE(positionSpy.last()[0].value<qint64>(), player.duration()); + for (auto pos : { colorInterval / 2, colorInterval + 100 }) { + qDebug() << "Waiting for position:" << pos; - stateSpy.clear(); - statusSpy.clear(); - positionSpy.clear(); + QTRY_COMPARE_GT(player.position(), pos); - player.play(); + auto frame1 = surface.waitForFrame(); + QVERIFY(frame1.isValid()); + QCOMPARE(frame1.size(), QSize(213, 120)); - //position is reset to start - QTRY_VERIFY(player.position() < 100); - QTRY_VERIFY(positionSpy.count() > 0); - QCOMPARE(positionSpy.first()[0].value<qint64>(), 0); + QCOMPARE_GT(frame1.startTime(), pos * 1000); + + auto frameTime = frame1.startTime(); + const auto coloIndex = frameTime / (colorInterval * 1000); + QCOMPARE_LT(coloIndex, 2); + + const auto image1 = frame1.toImage(); + QVERIFY(!image1.isNull()); + QCOMPARE(findSimilarColorIndex(colors, image1.pixel(1, 1)), coloIndex); + QCOMPARE(findSimilarColorIndex(colors, image1.pixel(100, 100)), coloIndex); + + auto frame2 = surface.waitForFrame(); + QVERIFY(frame2.isValid()); + QCOMPARE_GT(frame2.startTime(), frame1.startTime()); + } + + player.stop(); + + QCOMPARE(player.playbackState(), QMediaPlayer::StoppedState); + QCOMPARE(errorSpy.size(), 0); +#endif //QT_CONFIG(process) +} + +void tst_QMediaPlayerBackend::play_waitsForLastFrameEnd_whenPlayingVideoWithLongFrames() +{ + if (isCI() && isGStreamerPlatform()) + QSKIP_GSTREAMER("QTBUG-124005: spurious failures with gstreamer"); + + CHECK_SELECTED_URL(m_oneRedFrameVideo); + + m_fixture->surface.setStoreFrames(true); + + m_fixture->player.setSource(*m_oneRedFrameVideo); + m_fixture->player.play(); + + QTRY_COMPARE_GT(m_fixture->surface.m_totalFrames, 0); + QVERIFY(m_fixture->surface.m_frameList.front().isValid()); + + QElapsedTimer timer; + timer.start(); + + QTRY_COMPARE_GT(m_fixture->surface.m_totalFrames, 1); + const auto elapsed = timer.elapsed(); + + if (!isGStreamerPlatform()) { + // QTBUG-124005: GStreamer timing seems to be off + + // 1000 is expected + QCOMPARE_GT(elapsed, 900); + QCOMPARE_LT(elapsed, 1400); + } + + QTRY_COMPARE(m_fixture->player.mediaStatus(), QMediaPlayer::EndOfMedia); + QCOMPARE(m_fixture->surface.m_totalFrames, 2); + QVERIFY(!m_fixture->surface.m_frameList.back().isValid()); +} + +void tst_QMediaPlayerBackend::play_startsPlayback_withAndWithoutOutputsConnected() +{ + QFETCH(const bool, audioConnected); + QFETCH(const bool, videoConnected); + + CHECK_SELECTED_URL(m_localVideoFile3ColorsWithSound); + + if (!videoConnected && !audioConnected) + QSKIP_FFMPEG("FFMPEG backend playback fails when no output is connected"); + + // Arrange + m_fixture->player.setSource(*m_localVideoFile3ColorsWithSound); + if (!audioConnected) + m_fixture->player.setAudioOutput(nullptr); + + if (!videoConnected) + m_fixture->player.setVideoOutput(nullptr); + + m_fixture->clearSpies(); + + // Act + m_fixture->player.play(); + + // Assert + QTRY_VERIFY(!m_fixture->mediaStatusChanged.empty() + && m_fixture->mediaStatusChanged.back() + == QList<QVariant>{ QMediaPlayer::EndOfMedia }); + + QTRY_COMPARE_EQ(m_fixture->playbackStateChanged, + SignalList({ + { QMediaPlayer::PlayingState }, + { QMediaPlayer::StoppedState }, + })); +} + +void tst_QMediaPlayerBackend::play_startsPlayback_withAndWithoutOutputsConnected_data() +{ + QTest::addColumn<bool>("videoConnected"); + QTest::addColumn<bool>("audioConnected"); + + QTest::addRow("all connected") << true << true; + QTest::addRow("video connected") << true << false; + QTest::addRow("audio connected") << false << true; + QTest::addRow("no output connected") << false << false; +} + +void tst_QMediaPlayerBackend::play_playsRtpStream_whenSdpFileIsLoaded() +{ +#if !QT_CONFIG(process) + QSKIP("This test requires QProcess support"); +#else + if (!isFFMPEGPlatform()) + QSKIP("This test is only for FFmpeg backend"); + + // Create stream + if (!canCreateRtpStream()) + QSKIP("Rtp stream cannot be created"); + + auto temporaryFile = copyResourceToTemporaryFile(":/testdata/colors.mp4", "colors.XXXXXX.mp4"); + QVERIFY(temporaryFile); + + // Pass a "file:" URL to VLC in order to generate an .sdp file + const QUrl sdpUrl = QUrl::fromLocalFile(QFileInfo("test.sdp").absoluteFilePath()); + + auto process = createRtpStreamProcess(temporaryFile->fileName(), sdpUrl.toString()); + QVERIFY2(process, "Cannot start rtp process"); + + // Set reasonable protocol whitelist that includes rtp and udp + qputenv("QT_FFMPEG_PROTOCOL_WHITELIST", "file,crypto,data,rtp,udp"); + + auto processCloser = qScopeGuard([&process, &sdpUrl]() { + // End stream + process->close(); + + // Remove .sdp file created by VLC + QFile(sdpUrl.toLocalFile()).remove(); + + // Unset environment variable + qunsetenv("QT_FFMPEG_PROTOCOL_WHITELIST"); + }); + + m_fixture->player.setSource(sdpUrl); + + // Play + m_fixture->player.play(); + QTRY_COMPARE(m_fixture->player.playbackState(), QMediaPlayer::PlayingState); +#endif // QT_CONFIG(process) +} + +void tst_QMediaPlayerBackend::stop_entersStoppedState_whenPlayerWasPaused() +{ + CHECK_SELECTED_URL(m_localWavFile); + + // Arrange + m_fixture->player.setSource(*m_localWavFile); + m_fixture->player.play(); + QTRY_VERIFY(m_fixture->player.position() > 100); + m_fixture->player.pause(); + QTRY_COMPARE(m_fixture->player.mediaStatus(), QMediaPlayer::BufferedMedia); + m_fixture->clearSpies(); + + // Act + m_fixture->player.stop(); + + // Assert + QCOMPARE(m_fixture->player.playbackState(), QMediaPlayer::StoppedState); + QTRY_COMPARE(m_fixture->player.mediaStatus(), QMediaPlayer::LoadedMedia); + + QCOMPARE(m_fixture->playbackStateChanged, SignalList({ { QMediaPlayer::StoppedState } })); + // it's allowed to emit statusChanged() signal async + QTRY_COMPARE(m_fixture->mediaStatusChanged, SignalList({ { QMediaPlayer::LoadedMedia } })); + + if (!isGStreamerPlatform()) + // QTBUG-124517: for some media types gstreamer does not emit buffer progress messages + QCOMPARE(m_fixture->bufferProgressChanged, SignalList({ { 0.f } })); + + QTRY_COMPARE(m_fixture->player.position(), qint64(0)); + if (isGStreamerPlatform()) + QSKIP_GSTREAMER("QTBUG-124005: spurious failures with gstreamer "); + + QTRY_VERIFY(!m_fixture->positionChanged.empty()); + QCOMPARE(m_fixture->positionChanged.last()[0].value<qint64>(), qint64(0)); + QVERIFY(m_fixture->player.duration() > 0); +} + +void tst_QMediaPlayerBackend::stop_setsPositionToZero_afterPlayingToEndOfMedia() +{ + // Arrange + m_fixture->player.setSource(*m_localVideoFile3ColorsWithSound); + m_fixture->player.play(); + QTRY_COMPARE(m_fixture->player.mediaStatus(), QMediaPlayer::EndOfMedia); + QCOMPARE(m_fixture->player.playbackState(), QMediaPlayer::StoppedState); + + // Act + m_fixture->player.stop(); + + // Assert + QCOMPARE(m_fixture->player.position(), qint64(0)); + QTRY_COMPARE(m_fixture->player.mediaStatus(), QMediaPlayer::LoadedMedia); + QCOMPARE(m_fixture->player.playbackState(), QMediaPlayer::StoppedState); + + m_fixture->player.play(); + + if (isGStreamerPlatform()) + QSKIP_GSTREAMER("QTBUG-124005: spurious failures with gstreamer"); + + QVERIFY(m_fixture->surface.waitForFrame().isValid()); +} + + +void tst_QMediaPlayerBackend::playbackRate_returnsOne_byDefault() +{ + QCOMPARE_EQ(m_fixture->player.playbackRate(), static_cast<qreal>(1.0f)); +} + +void tst_QMediaPlayerBackend::setPlaybackRate_changesPlaybackRateAndEmitsSignal_data() +{ + QTest::addColumn<float>("initialPlaybackRate"); + QTest::addColumn<float>("targetPlaybackRate"); + QTest::addColumn<float>("expectedPlaybackRate"); + QTest::addColumn<bool>("signalExpected"); + + QTest::addRow("Increase") << 1.0f << 2.0f << 2.0f << true; + QTest::addRow("Decrease") << 1.0f << 0.5f << 0.5f << true; + QTest::addRow("Keep") << 0.5f << 0.5f << 0.5f << false; + + bool backendSupportsNegativePlayback = + isWindowsPlatform() || isDarwinPlatform() || isGStreamerPlatform(); + + if (backendSupportsNegativePlayback) { + QTest::addRow("DecreaseBelowZero") << 0.5f << -0.5f << -0.5f << true; + QTest::addRow("KeepDecreasingBelowZero") << -0.5f << -0.6f << -0.6f << true; + } else { + QTest::addRow("DecreaseBelowZero") << 0.5f << -0.5f << 0.0f << true; + QTest::addRow("KeepDecreasingBelowZero") << -0.5f << -0.6f << 0.0f << false; + } +} + +void tst_QMediaPlayerBackend::setPlaybackRate_changesPlaybackRateAndEmitsSignal() +{ + QFETCH(const float, initialPlaybackRate); + QFETCH(const float, targetPlaybackRate); + QFETCH(const float, expectedPlaybackRate); + QFETCH(const bool, signalExpected); + + // Arrange + m_fixture->player.setPlaybackRate(initialPlaybackRate); + m_fixture->clearSpies(); + + // Act + m_fixture->player.setPlaybackRate(targetPlaybackRate); + + // Assert + if (signalExpected) + QCOMPARE_EQ(m_fixture->playbackRateChanged, SignalList({ { expectedPlaybackRate } })); + else + QVERIFY(m_fixture->playbackRateChanged.empty()); + + QCOMPARE_EQ(m_fixture->player.playbackRate(), expectedPlaybackRate); +} + +void tst_QMediaPlayerBackend::setVolume_changesVolume_whenVolumeIsInRange() +{ + m_fixture->output.setVolume(0.0f); + QCOMPARE_EQ(m_fixture->output.volume(), 0.0f); + QCOMPARE(m_fixture->volumeChanged, SignalList({ { 0.0f } })); + + m_fixture->output.setVolume(0.5f); + QCOMPARE_EQ(m_fixture->output.volume(), 0.5f); + QCOMPARE(m_fixture->volumeChanged, SignalList({ { 0.0f }, { 0.5f } })); + + m_fixture->output.setVolume(1.0f); + QCOMPARE_EQ(m_fixture->output.volume(), 1.0f); + QCOMPARE(m_fixture->volumeChanged, SignalList({ { 0.0f }, { 0.5f }, { 1.0f } })); +} + +void tst_QMediaPlayerBackend::setVolume_clampsToRange_whenVolumeIsOutsideRange() +{ + m_fixture->output.setVolume(-0.1f); + QCOMPARE_EQ(m_fixture->output.volume(), 0.0f); + QCOMPARE(m_fixture->volumeChanged, SignalList({ { 0.0f } })); + + m_fixture->output.setVolume(1.1f); + QCOMPARE_EQ(m_fixture->output.volume(), 1.0f); + QCOMPARE(m_fixture->volumeChanged, SignalList({ { 0.0f }, { 1.0f } })); +} + +void tst_QMediaPlayerBackend::setVolume_doesNotChangeMutedState() +{ + m_fixture->output.setMuted(true); + m_fixture->output.setVolume(0.5f); + QVERIFY(m_fixture->output.isMuted()); + + m_fixture->output.setMuted(false); + m_fixture->output.setVolume(0.0f); + QVERIFY(!m_fixture->output.isMuted()); +} + +void tst_QMediaPlayerBackend::setMuted_changesMutedState_whenMutedStateChanged() +{ + m_fixture->output.setMuted(true); + QVERIFY(m_fixture->output.isMuted()); + QCOMPARE(m_fixture->mutedChanged, SignalList({ { true } })); + + // No new events emitted when muted state did not change + m_fixture->output.setMuted(true); + QCOMPARE(m_fixture->mutedChanged, SignalList({ { true } })); + + m_fixture->output.setMuted(false); + QVERIFY(!m_fixture->output.isMuted()); + QCOMPARE(m_fixture->mutedChanged, SignalList({ { true }, { false } })); + + // No new events emitted when muted state did not change + m_fixture->output.setMuted(false); + QCOMPARE(m_fixture->mutedChanged, SignalList({ { true }, { false } })); +} + +void tst_QMediaPlayerBackend::setMuted_doesNotChangeVolume() +{ + m_fixture->output.setVolume(0.5f); + + m_fixture->output.setMuted(true); + QCOMPARE_EQ(m_fixture->output.volume(), 0.5f); + + m_fixture->output.setMuted(false); + QCOMPARE_EQ(m_fixture->output.volume(), 0.5f); +} + +void tst_QMediaPlayerBackend::processEOS() +{ + QSKIP_GSTREAMER("QTBUG-124005: spurious failure with gstreamer"); - QCOMPARE(player.state(), QMediaPlayer::PlayingState); - QTRY_COMPARE(player.mediaStatus(), QMediaPlayer::BufferedMedia); + if (!isGStreamerPlatform()) { + // QTBUG-124517: for some media types, including wav files, gstreamer does not emit buffer + // progress messages + CHECK_SELECTED_URL(m_localWavFile); + m_fixture->player.setSource(*m_localWavFile); + } else { + CHECK_SELECTED_URL(m_localVideoFile3ColorsWithSound); + m_fixture->player.setSource(*m_localVideoFile3ColorsWithSound); + } - QCOMPARE(stateSpy.count(), 1); - QCOMPARE(stateSpy.last()[0].value<QMediaPlayer::State>(), QMediaPlayer::PlayingState); - QVERIFY(statusSpy.count() > 0); - QCOMPARE(statusSpy.last()[0].value<QMediaPlayer::MediaStatus>(), QMediaPlayer::BufferedMedia); + m_fixture->player.play(); + m_fixture->player.setPosition(900); - player.setPosition(900); //wait up to 5 seconds for EOS - QTRY_COMPARE(player.mediaStatus(), QMediaPlayer::EndOfMedia); - QVERIFY(statusSpy.count() > 0); - QCOMPARE(statusSpy.last()[0].value<QMediaPlayer::MediaStatus>(), QMediaPlayer::EndOfMedia); - QCOMPARE(player.state(), QMediaPlayer::StoppedState); - QCOMPARE(stateSpy.count(), 2); - QCOMPARE(stateSpy.last()[0].value<QMediaPlayer::State>(), QMediaPlayer::StoppedState); + QTRY_COMPARE(m_fixture->player.mediaStatus(), QMediaPlayer::EndOfMedia); + + QVERIFY(m_fixture->mediaStatusChanged.size() > 0); + QCOMPARE(m_fixture->mediaStatusChanged.last()[0].value<QMediaPlayer::MediaStatus>(), QMediaPlayer::EndOfMedia); + QCOMPARE(m_fixture->player.playbackState(), QMediaPlayer::StoppedState); + QCOMPARE(m_fixture->playbackStateChanged.size(), 2); + QCOMPARE(m_fixture->playbackStateChanged.last()[0].value<QMediaPlayer::PlaybackState>(), QMediaPlayer::StoppedState); + + //at EOS the position stays at the end of file + QCOMPARE(m_fixture->player.position(), m_fixture->player.duration()); + QTRY_VERIFY(m_fixture->positionChanged.size() > 0); + QTRY_COMPARE(m_fixture->positionChanged.last()[0].value<qint64>(), m_fixture->player.duration()); + + m_fixture->playbackStateChanged.clear(); + m_fixture->mediaStatusChanged.clear(); + m_fixture->positionChanged.clear(); + + m_fixture->player.play(); + + //position is reset to start + QTRY_COMPARE_LT(m_fixture->player.position(), 500); + QTRY_VERIFY(m_fixture->positionChanged.size() > 0); + QCOMPARE(m_fixture->positionChanged.first()[0].value<qint64>(), 0); + + QCOMPARE(m_fixture->player.playbackState(), QMediaPlayer::PlayingState); + QTRY_VERIFY(m_fixture->player.mediaStatus() == QMediaPlayer::BufferedMedia + || m_fixture->player.mediaStatus() == QMediaPlayer::EndOfMedia); + + QCOMPARE(m_fixture->playbackStateChanged.size(), 1); + QCOMPARE(m_fixture->playbackStateChanged.last()[0].value<QMediaPlayer::PlaybackState>(), QMediaPlayer::PlayingState); + QVERIFY(m_fixture->mediaStatusChanged.size() > 0); + QCOMPARE(m_fixture->mediaStatusChanged.last()[0].value<QMediaPlayer::MediaStatus>(), QMediaPlayer::BufferedMedia); + + m_fixture->positionChanged.clear(); + QTRY_VERIFY(m_fixture->player.position() > 100); + QTRY_VERIFY(m_fixture->positionChanged.size() > 0 && m_fixture->positionChanged.last()[0].value<qint64>() > 100); + m_fixture->player.setPosition(900); + //wait up to 5 seconds for EOS + QTRY_COMPARE(m_fixture->player.mediaStatus(), QMediaPlayer::EndOfMedia); + QVERIFY(m_fixture->mediaStatusChanged.size() > 0); + QCOMPARE(m_fixture->mediaStatusChanged.last()[0].value<QMediaPlayer::MediaStatus>(), QMediaPlayer::EndOfMedia); + QCOMPARE(m_fixture->player.playbackState(), QMediaPlayer::StoppedState); + QCOMPARE(m_fixture->playbackStateChanged.size(), 2); + QCOMPARE(m_fixture->playbackStateChanged.last()[0].value<QMediaPlayer::PlaybackState>(), QMediaPlayer::StoppedState); + + QCOMPARE_GT(m_fixture->bufferProgressChanged.size(), 1); + QCOMPARE(m_fixture->bufferProgressChanged.back().front(), 0.f); - //position stays at the end of file - QCOMPARE(player.position(), player.duration()); - QVERIFY(positionSpy.count() > 0); - QCOMPARE(positionSpy.last()[0].value<qint64>(), player.duration()); + // position stays at the end of file + QCOMPARE(m_fixture->player.position(), m_fixture->player.duration()); + QTRY_VERIFY(m_fixture->positionChanged.size() > 0); + QTRY_COMPARE(m_fixture->positionChanged.last()[0].value<qint64>(), m_fixture->player.duration()); //after setPosition EndOfMedia status should be reset to Loaded - stateSpy.clear(); - statusSpy.clear(); - player.setPosition(500); + m_fixture->playbackStateChanged.clear(); + m_fixture->mediaStatusChanged.clear(); + m_fixture->player.setPosition(500); //this transition can be async, so allow backend to perform it - QTRY_COMPARE(player.mediaStatus(), QMediaPlayer::LoadedMedia); + QTRY_COMPARE(m_fixture->player.mediaStatus(), QMediaPlayer::LoadedMedia); - QCOMPARE(stateSpy.count(), 0); - QTRY_VERIFY(statusSpy.count() > 0 && - statusSpy.last()[0].value<QMediaPlayer::MediaStatus>() == QMediaPlayer::LoadedMedia); + QCOMPARE(m_fixture->playbackStateChanged.size(), 0); + QTRY_VERIFY(m_fixture->mediaStatusChanged.size() > 0 && + m_fixture->mediaStatusChanged.last()[0].value<QMediaPlayer::MediaStatus>() == QMediaPlayer::LoadedMedia); - player.play(); - player.setPosition(900); + m_fixture->player.play(); + m_fixture->player.setPosition(900); //wait up to 5 seconds for EOS - QTRY_COMPARE(player.mediaStatus(), QMediaPlayer::EndOfMedia); - QCOMPARE(player.state(), QMediaPlayer::StoppedState); - QCOMPARE(player.position(), player.duration()); + QTRY_COMPARE(m_fixture->player.mediaStatus(), QMediaPlayer::EndOfMedia); + QCOMPARE(m_fixture->player.playbackState(), QMediaPlayer::StoppedState); + QCOMPARE(m_fixture->player.position(), m_fixture->player.duration()); - stateSpy.clear(); - statusSpy.clear(); - positionSpy.clear(); + m_fixture->playbackStateChanged.clear(); + m_fixture->mediaStatusChanged.clear(); + m_fixture->positionChanged.clear(); // pause() should reset position to beginning and status to Buffered - player.pause(); + m_fixture->player.pause(); - QTRY_COMPARE(player.position(), 0); - QTRY_VERIFY(positionSpy.count() > 0); - QCOMPARE(positionSpy.first()[0].value<qint64>(), 0); + QTRY_COMPARE(m_fixture->player.position(), 0); + QTRY_VERIFY(m_fixture->positionChanged.size() > 0); + QTRY_COMPARE(m_fixture->positionChanged.first()[0].value<qint64>(), 0); - QCOMPARE(player.state(), QMediaPlayer::PausedState); - QTRY_COMPARE(player.mediaStatus(), QMediaPlayer::BufferedMedia); + QCOMPARE(m_fixture->player.playbackState(), QMediaPlayer::PausedState); + QTRY_COMPARE(m_fixture->player.mediaStatus(), QMediaPlayer::BufferedMedia); - QCOMPARE(stateSpy.count(), 1); - QCOMPARE(stateSpy.last()[0].value<QMediaPlayer::State>(), QMediaPlayer::PausedState); - QVERIFY(statusSpy.count() > 0); - QCOMPARE(statusSpy.last()[0].value<QMediaPlayer::MediaStatus>(), QMediaPlayer::BufferedMedia); + QCOMPARE(m_fixture->playbackStateChanged.size(), 1); + QCOMPARE(m_fixture->playbackStateChanged.last()[0].value<QMediaPlayer::PlaybackState>(), QMediaPlayer::PausedState); + QVERIFY(m_fixture->mediaStatusChanged.size() > 0); + QCOMPARE(m_fixture->mediaStatusChanged.last()[0].value<QMediaPlayer::MediaStatus>(), QMediaPlayer::BufferedMedia); } // Helper class for tst_QMediaPlayerBackend::deleteLaterAtEOS() @@ -600,8 +1739,8 @@ private slots: void onMediaStatusChanged(QMediaPlayer::MediaStatus status) { if (status == QMediaPlayer::EndOfMedia) { - player-> deleteLater(); - player = 0; + player->deleteLater(); + player = nullptr; } } @@ -613,73 +1752,28 @@ private: // QTBUG-24927 - deleteLater() called to QMediaPlayer from its signal handler does not work as expected void tst_QMediaPlayerBackend::deleteLaterAtEOS() { - if (!isWavSupported()) - QSKIP("Sound format is not supported"); + CHECK_SELECTED_URL(m_localWavFile); QPointer<QMediaPlayer> player(new QMediaPlayer); + QAudioOutput output; + player->setAudioOutput(&output); + player->setPosition(800); // don't wait as long for EOS DeleteLaterAtEos deleter(player); - player->setMedia(localWavFile); + player->setSource(*m_localWavFile); // Create an event loop for verifying deleteLater behavior instead of using // QTRY_VERIFY or QTest::qWait. QTest::qWait makes extra effort to process // DeferredDelete events during the wait, which interferes with this test. QEventLoop loop; - QTimer::singleShot(0, &deleter, SLOT(play())); - QTimer::singleShot(5000, &loop, SLOT(quit())); - connect(player.data(), SIGNAL(destroyed()), &loop, SLOT(quit())); + QTimer::singleShot(0, &deleter, &DeleteLaterAtEos::play); + QTimer::singleShot(5000, &loop, &QEventLoop::quit); + connect(player.data(), &QObject::destroyed, &loop, &QEventLoop::quit); loop.exec(); // Verify that the player was destroyed within the event loop. // This check will fail without the fix for QTBUG-24927. QVERIFY(player.isNull()); } -void tst_QMediaPlayerBackend::volumeAndMuted() -{ - //volume and muted properties should be independent - QMediaPlayer player; - QVERIFY(player.volume() > 0); - QVERIFY(!player.isMuted()); - - player.setMedia(localWavFile); - player.pause(); - - QVERIFY(player.volume() > 0); - QVERIFY(!player.isMuted()); - - QSignalSpy volumeSpy(&player, SIGNAL(volumeChanged(int))); - QSignalSpy mutedSpy(&player, SIGNAL(mutedChanged(bool))); - - //setting volume to 0 should not trigger muted - player.setVolume(0); - QTRY_COMPARE(player.volume(), 0); - QVERIFY(!player.isMuted()); - QCOMPARE(volumeSpy.count(), 1); - QCOMPARE(volumeSpy.last()[0].toInt(), player.volume()); - QCOMPARE(mutedSpy.count(), 0); - - player.setVolume(50); - QTRY_COMPARE(player.volume(), 50); - QVERIFY(!player.isMuted()); - QCOMPARE(volumeSpy.count(), 2); - QCOMPARE(volumeSpy.last()[0].toInt(), player.volume()); - QCOMPARE(mutedSpy.count(), 0); - - player.setMuted(true); - QTRY_VERIFY(player.isMuted()); - QVERIFY(player.volume() > 0); - QCOMPARE(volumeSpy.count(), 2); - QCOMPARE(mutedSpy.count(), 1); - QCOMPARE(mutedSpy.last()[0].toBool(), player.isMuted()); - - player.setMuted(false); - QTRY_VERIFY(!player.isMuted()); - QVERIFY(player.volume() > 0); - QCOMPARE(volumeSpy.count(), 2); - QCOMPARE(mutedSpy.count(), 2); - QCOMPARE(mutedSpy.last()[0].toBool(), player.isMuted()); - -} - void tst_QMediaPlayerBackend::volumeAcrossFiles_data() { QTest::addColumn<int>("volume"); @@ -695,166 +1789,173 @@ void tst_QMediaPlayerBackend::volumeAcrossFiles_data() void tst_QMediaPlayerBackend::volumeAcrossFiles() { -#ifdef Q_OS_LINUX - if (m_inCISystem) - QSKIP("QTBUG-26577 Fails with gstreamer backend on ubuntu 10.4"); -#endif + CHECK_SELECTED_URL(m_localWavFile); QFETCH(int, volume); QFETCH(bool, muted); + float vol = volume/100.; + QAudioOutput output; QMediaPlayer player; + player.setAudioOutput(&output); + //volume and muted should not be preserved between player instances - QVERIFY(player.volume() > 0); - QVERIFY(!player.isMuted()); + QVERIFY(output.volume() > 0); + QVERIFY(!output.isMuted()); - player.setVolume(volume); - player.setMuted(muted); + output.setVolume(vol); + output.setMuted(muted); - QTRY_COMPARE(player.volume(), volume); - QTRY_COMPARE(player.isMuted(), muted); + QTRY_COMPARE(output.volume(), vol); + QTRY_COMPARE(output.isMuted(), muted); - player.setMedia(localWavFile); - QCOMPARE(player.volume(), volume); - QCOMPARE(player.isMuted(), muted); + player.setSource(*m_localWavFile); + QCOMPARE(output.volume(), vol); + QCOMPARE(output.isMuted(), muted); player.pause(); //to ensure the backend doesn't change volume/muted //async during file loading. - QTRY_COMPARE(player.volume(), volume); - QCOMPARE(player.isMuted(), muted); + QTRY_COMPARE(output.volume(), vol); + QCOMPARE(output.isMuted(), muted); - player.setMedia(QMediaContent()); - QTRY_COMPARE(player.volume(), volume); - QCOMPARE(player.isMuted(), muted); + player.setSource(QUrl()); + QTRY_COMPARE(output.volume(), vol); + QCOMPARE(output.isMuted(), muted); - player.setMedia(localWavFile); + player.setSource(*m_localWavFile); player.pause(); - QTRY_COMPARE(player.volume(), volume); - QCOMPARE(player.isMuted(), muted); + QTRY_COMPARE(output.volume(), vol); + QCOMPARE(output.isMuted(), muted); } void tst_QMediaPlayerBackend::initialVolume() { - if (!isWavSupported()) - QSKIP("Sound format is not supported"); + CHECK_SELECTED_URL(m_localWavFile); { + QAudioOutput output; QMediaPlayer player; - player.setVolume(1); - player.setMedia(localWavFile); - QCOMPARE(player.volume(), 1); + player.setAudioOutput(&output); + output.setVolume(1); + player.setSource(*m_localWavFile); + QCOMPARE(output.volume(), 1); player.play(); QTRY_COMPARE(player.mediaStatus(), QMediaPlayer::EndOfMedia); - QCOMPARE(player.volume(), 1); + QCOMPARE(output.volume(), 1); } { + QAudioOutput output; QMediaPlayer player; - player.setMedia(localWavFile); - QCOMPARE(player.volume(), 100); + player.setAudioOutput(&output); + player.setSource(*m_localWavFile); + QCOMPARE(output.volume(), 1); player.play(); QTRY_COMPARE(player.mediaStatus(), QMediaPlayer::EndOfMedia); - QCOMPARE(player.volume(), 100); + QCOMPARE(output.volume(), 1); } } void tst_QMediaPlayerBackend::seekPauseSeek() { - if (localVideoFile.isNull()) - QSKIP("No supported video file"); +#ifdef Q_OS_ANDROID + QSKIP("frame.toImage will return null image because of QTBUG-108446"); +#endif + CHECK_SELECTED_URL(m_localVideoFile); + TestVideoSink surface(true); + QAudioOutput output; QMediaPlayer player; - QSignalSpy positionSpy(&player, SIGNAL(positionChanged(qint64))); + player.setAudioOutput(&output); + + QSignalSpy positionSpy(&player, &QMediaPlayer::positionChanged); - TestVideoSurface *surface = new TestVideoSurface; - player.setVideoOutput(surface); + player.setVideoOutput(&surface); - player.setMedia(localVideoFile); - QCOMPARE(player.state(), QMediaPlayer::StoppedState); - QVERIFY(surface->m_frameList.isEmpty()); // frame must not appear until we call pause() or play() + player.setSource(*m_localVideoFile); + QCOMPARE(player.playbackState(), QMediaPlayer::StoppedState); + QTRY_COMPARE(player.mediaStatus(), QMediaPlayer::LoadedMedia); + QVERIFY(surface.m_frameList.isEmpty()); // frame must not appear until we call pause() or play() positionSpy.clear(); qint64 position = 7000; player.setPosition(position); - QTRY_VERIFY(!positionSpy.isEmpty() && qAbs(player.position() - position) < (qint64)500); - QCOMPARE(player.state(), QMediaPlayer::StoppedState); + QTRY_VERIFY(!positionSpy.isEmpty()); + QTRY_COMPARE(player.position(), position); + QCOMPARE(player.playbackState(), QMediaPlayer::StoppedState); QTest::qWait(250); // wait a bit to ensure the frame is not rendered - QVERIFY(surface->m_frameList.isEmpty()); // still no frame, we must call pause() or play() to see a frame + QVERIFY(surface.m_frameList + .isEmpty()); // still no frame, we must call pause() or play() to see a frame player.pause(); - QTRY_COMPARE(player.state(), QMediaPlayer::PausedState); // it might take some time for the operation to be completed - QTRY_VERIFY_WITH_TIMEOUT(!surface->m_frameList.isEmpty(), 10000); // we must see a frame at position 7000 here + QTRY_COMPARE(player.playbackState(), QMediaPlayer::PausedState); // it might take some time for the operation to be completed + QTRY_VERIFY(!surface.m_frameList.isEmpty()); // we must see a frame at position 7000 here // Make sure that the frame has a timestamp before testing - not all backends provides this - if (!surface->m_frameList.back().isValid() || surface->m_frameList.back().startTime() < 0) + if (!surface.m_frameList.back().isValid() || surface.m_frameList.back().startTime() < 0) QSKIP("No timestamp"); { - QVideoFrame frame = surface->m_frameList.back(); -#if !QT_CONFIG(directshow) + QVideoFrame frame = surface.m_frameList.back(); const qint64 elapsed = (frame.startTime() / 1000) - position; // frame.startTime() is microsecond, position is milliseconds. QVERIFY2(qAbs(elapsed) < (qint64)500, QByteArray::number(elapsed).constData()); -#endif - QCOMPARE(frame.width(), 160); + QCOMPARE(frame.width(), 213); QCOMPARE(frame.height(), 120); // create QImage for QVideoFrame to verify RGB pixel colors - QVERIFY(frame.map(QAbstractVideoBuffer::ReadOnly)); - QImage image(frame.bits(), frame.width(), frame.height(), QVideoFrame::imageFormatFromPixelFormat(frame.pixelFormat())); + QImage image = frame.toImage(); QVERIFY(!image.isNull()); QVERIFY(qRed(image.pixel(0, 0)) >= 230); // conversion from YUV => RGB, that's why it's not 255 QVERIFY(qGreen(image.pixel(0, 0)) < 20); QVERIFY(qBlue(image.pixel(0, 0)) < 20); - frame.unmap(); } - surface->m_frameList.clear(); + surface.m_frameList.clear(); positionSpy.clear(); position = 12000; player.setPosition(position); QTRY_VERIFY(!positionSpy.isEmpty() && qAbs(player.position() - position) < (qint64)500); - QCOMPARE(player.state(), QMediaPlayer::PausedState); - QVERIFY(!surface->m_frameList.isEmpty()); + QCOMPARE(player.playbackState(), QMediaPlayer::PausedState); + QTRY_VERIFY(!surface.m_frameList.isEmpty()); { - QVideoFrame frame = surface->m_frameList.back(); -#if !QT_CONFIG(directshow) + QVideoFrame frame = surface.m_frameList.back(); const qint64 elapsed = (frame.startTime() / 1000) - position; QVERIFY2(qAbs(elapsed) < (qint64)500, QByteArray::number(elapsed).constData()); -#endif - QCOMPARE(frame.width(), 160); + QCOMPARE(frame.width(), 213); QCOMPARE(frame.height(), 120); - QVERIFY(frame.map(QAbstractVideoBuffer::ReadOnly)); - QImage image(frame.bits(), frame.width(), frame.height(), QVideoFrame::imageFormatFromPixelFormat(frame.pixelFormat())); + QImage image = frame.toImage(); QVERIFY(!image.isNull()); QVERIFY(qRed(image.pixel(0, 0)) < 20); QVERIFY(qGreen(image.pixel(0, 0)) >= 230); QVERIFY(qBlue(image.pixel(0, 0)) < 20); - frame.unmap(); } } void tst_QMediaPlayerBackend::seekInStoppedState() { - if (localVideoFile.isNull()) - QSKIP("No supported video file"); + CHECK_SELECTED_URL(m_localVideoFile); + TestVideoSink surface(false); + QAudioOutput output; QMediaPlayer player; - player.setNotifyInterval(500); - QSignalSpy stateSpy(&player, SIGNAL(stateChanged(QMediaPlayer::State))); - QSignalSpy positionSpy(&player, SIGNAL(positionChanged(qint64))); + player.setAudioOutput(&output); + player.setVideoOutput(&surface); + + QSignalSpy stateSpy(&player, &QMediaPlayer::playbackStateChanged); + QSignalSpy positionSpy(&player, &QMediaPlayer::positionChanged); - player.setMedia(localVideoFile); + player.setSource(*m_localVideoFile); QTRY_COMPARE(player.mediaStatus(), QMediaPlayer::LoadedMedia); - QCOMPARE(player.state(), QMediaPlayer::StoppedState); + QCOMPARE(player.playbackState(), QMediaPlayer::StoppedState); QCOMPARE(player.position(), 0); QVERIFY(player.isSeekable()); @@ -864,12 +1965,12 @@ void tst_QMediaPlayerBackend::seekInStoppedState() qint64 position = 5000; player.setPosition(position); - QTRY_VERIFY(qAbs(player.position() - position) < qint64(500)); - QCOMPARE(positionSpy.count(), 1); - QVERIFY(qAbs(positionSpy.last()[0].value<qint64>() - position) < qint64(500)); + QTRY_VERIFY(qAbs(player.position() - position) < qint64(200)); + QTRY_VERIFY(positionSpy.size() > 0); + QVERIFY(qAbs(positionSpy.last()[0].value<qint64>() - position) < qint64(200)); - QCOMPARE(player.state(), QMediaPlayer::StoppedState); - QCOMPARE(stateSpy.count(), 0); + QCOMPARE(player.playbackState(), QMediaPlayer::StoppedState); + QCOMPARE(stateSpy.size(), 0); QCOMPARE(player.mediaStatus(), QMediaPlayer::LoadedMedia); @@ -877,35 +1978,34 @@ void tst_QMediaPlayerBackend::seekInStoppedState() player.play(); - QCOMPARE(player.state(), QMediaPlayer::PlayingState); - QTRY_COMPARE(player.mediaStatus(), QMediaPlayer::BufferedMedia); - QVERIFY(qAbs(player.position() - position) < qint64(500)); + QCOMPARE(player.playbackState(), QMediaPlayer::PlayingState); + QTRY_VERIFY(player.position() > position); - QTest::qWait(2000); + QTest::qWait(100); // Check that it never played from the beginning - QVERIFY(player.position() > (position - 500)); - for (int i = 0; i < positionSpy.count(); ++i) - QVERIFY(positionSpy.at(i)[0].value<qint64>() > (position - 500)); + QVERIFY(player.position() > position); + for (int i = 0; i < positionSpy.size(); ++i) + QVERIFY(positionSpy.at(i)[0].value<qint64>() > (position - 200)); // ------ // Same tests but after play() --> stop() player.stop(); - QCOMPARE(player.state(), QMediaPlayer::StoppedState); + QCOMPARE(player.playbackState(), QMediaPlayer::StoppedState); QTRY_COMPARE(player.mediaStatus(), QMediaPlayer::LoadedMedia); - QCOMPARE(player.position(), 0); + QTRY_COMPARE(player.position(), 0); stateSpy.clear(); positionSpy.clear(); player.setPosition(position); - QTRY_VERIFY(qAbs(player.position() - position) < qint64(500)); - QCOMPARE(positionSpy.count(), 1); - QVERIFY(qAbs(positionSpy.last()[0].value<qint64>() - position) < qint64(500)); + QTRY_VERIFY(qAbs(player.position() - position) < qint64(200)); + QTRY_VERIFY(positionSpy.size() > 0); + QVERIFY(qAbs(positionSpy.last()[0].value<qint64>() - position) < qint64(200)); - QCOMPARE(player.state(), QMediaPlayer::StoppedState); - QCOMPARE(stateSpy.count(), 0); + QCOMPARE(player.playbackState(), QMediaPlayer::StoppedState); + QCOMPARE(stateSpy.size(), 0); QCOMPARE(player.mediaStatus(), QMediaPlayer::LoadedMedia); @@ -913,560 +2013,552 @@ void tst_QMediaPlayerBackend::seekInStoppedState() player.play(); - QCOMPARE(player.state(), QMediaPlayer::PlayingState); - QTRY_COMPARE(player.mediaStatus(), QMediaPlayer::BufferedMedia); - QVERIFY(qAbs(player.position() - position) < qint64(500)); + QCOMPARE(player.playbackState(), QMediaPlayer::PlayingState); + QVERIFY(qAbs(player.position() - position) < qint64(200)); - QTest::qWait(2000); + QTest::qWait(500); // Check that it never played from the beginning - QVERIFY(player.position() > (position - 500)); - for (int i = 0; i < positionSpy.count(); ++i) - QVERIFY(positionSpy.at(i)[0].value<qint64>() > (position - 500)); + QVERIFY(player.position() > (position - 200)); + for (int i = 0; i < positionSpy.size(); ++i) + QVERIFY(positionSpy.at(i)[0].value<qint64>() > (position - 200)); // ------ // Same tests but after reaching the end of the media - player.setPosition(player.duration() - 500); + player.setPosition(player.duration() - 100); QTRY_COMPARE(player.mediaStatus(), QMediaPlayer::EndOfMedia); - QCOMPARE(player.state(), QMediaPlayer::StoppedState); - QCOMPARE(player.position(), player.duration()); + QCOMPARE(player.playbackState(), QMediaPlayer::StoppedState); + QVERIFY(qAbs(player.position() - player.duration()) < 10); stateSpy.clear(); positionSpy.clear(); player.setPosition(position); - QTRY_VERIFY(qAbs(player.position() - position) < qint64(500)); - QCOMPARE(positionSpy.count(), 1); - QVERIFY(qAbs(positionSpy.last()[0].value<qint64>() - position) < qint64(500)); - - QCOMPARE(player.state(), QMediaPlayer::StoppedState); - QCOMPARE(stateSpy.count(), 0); + QTRY_VERIFY(qAbs(player.position() - position) < qint64(200)); + QTRY_VERIFY(positionSpy.size() > 0); + QVERIFY(qAbs(positionSpy.last()[0].value<qint64>() - position) < qint64(200)); + QCOMPARE(player.playbackState(), QMediaPlayer::StoppedState); + QCOMPARE(stateSpy.size(), 0); QCOMPARE(player.mediaStatus(), QMediaPlayer::LoadedMedia); - positionSpy.clear(); - player.play(); + QTRY_COMPARE(player.playbackState(), QMediaPlayer::PlayingState); + QTRY_VERIFY(player.mediaStatus() == QMediaPlayer::BufferedMedia + || player.mediaStatus() == QMediaPlayer::EndOfMedia); - QCOMPARE(player.state(), QMediaPlayer::PlayingState); - QTRY_COMPARE(player.mediaStatus(), QMediaPlayer::BufferedMedia); - QVERIFY(qAbs(player.position() - position) < qint64(500)); + positionSpy.clear(); + QTRY_VERIFY(player.position() > (position - 200)); - QTest::qWait(2000); + QTest::qWait(500); // Check that it never played from the beginning - QVERIFY(player.position() > (position - 500)); - for (int i = 0; i < positionSpy.count(); ++i) - QVERIFY(positionSpy.at(i)[0].value<qint64>() > (position - 500)); + QVERIFY(player.position() > (position - 200)); + for (int i = 0; i < positionSpy.size(); ++i) + QVERIFY(positionSpy.at(i)[0].value<qint64>() > (position - 200)); } void tst_QMediaPlayerBackend::subsequentPlayback() { -#ifdef Q_OS_LINUX - if (m_inCISystem) - QSKIP("QTBUG-26769 Fails with gstreamer backend on ubuntu 10.4, setPosition(0)"); -#endif + QSKIP_GSTREAMER("QTBUG-124005: spurious seek failures with gstreamer"); - if (localCompressedSoundFile.isNull()) - QSKIP("Sound format is not supported"); + CHECK_SELECTED_URL(m_localCompressedSoundFile); + QAudioOutput output; QMediaPlayer player; - player.setMedia(localCompressedSoundFile); + player.setAudioOutput(&output); + player.setSource(*m_localCompressedSoundFile); + QTRY_COMPARE(player.mediaStatus(), QMediaPlayer::LoadedMedia); + QTRY_VERIFY(player.isSeekable()); + player.setPosition(5000); player.play(); QCOMPARE(player.error(), QMediaPlayer::NoError); - QTRY_COMPARE(player.state(), QMediaPlayer::PlayingState); - QTRY_COMPARE_WITH_TIMEOUT(player.mediaStatus(), QMediaPlayer::EndOfMedia, 15000); - QCOMPARE(player.state(), QMediaPlayer::StoppedState); + QTRY_COMPARE(player.playbackState(), QMediaPlayer::PlayingState); + QTRY_COMPARE_WITH_TIMEOUT(player.mediaStatus(), QMediaPlayer::EndOfMedia, 10s); + QCOMPARE(player.playbackState(), QMediaPlayer::StoppedState); // Could differ by up to 1 compressed frame length QVERIFY(qAbs(player.position() - player.duration()) < 100); QVERIFY(player.position() > 0); player.play(); - QTRY_COMPARE(player.state(), QMediaPlayer::PlayingState); - QTRY_VERIFY_WITH_TIMEOUT(player.position() > 2000 && player.position() < 5000, 10000); + QTRY_COMPARE(player.playbackState(), QMediaPlayer::PlayingState); + QTRY_COMPARE_GT(player.position(), 1000); player.pause(); - QCOMPARE(player.state(), QMediaPlayer::PausedState); + QCOMPARE(player.playbackState(), QMediaPlayer::PausedState); // make sure position does not "jump" closer to the end of the file - QVERIFY(player.position() > 2000 && player.position() < 5000); + QVERIFY(player.position() > 1000); // try to seek back to zero player.setPosition(0); QTRY_COMPARE(player.position(), qint64(0)); player.play(); - QCOMPARE(player.state(), QMediaPlayer::PlayingState); - QTRY_VERIFY_WITH_TIMEOUT(player.position() > 2000 && player.position() < 5000, 10000); + QCOMPARE(player.playbackState(), QMediaPlayer::PlayingState); + QTRY_COMPARE_GT(player.position(), 1000); player.pause(); - QCOMPARE(player.state(), QMediaPlayer::PausedState); - QVERIFY(player.position() > 2000 && player.position() < 5000); + QCOMPARE(player.playbackState(), QMediaPlayer::PausedState); + QCOMPARE_GT(player.position(), 1000); } -void tst_QMediaPlayerBackend::probes() +void tst_QMediaPlayerBackend::multipleMediaPlayback() { - if (localVideoFile.isNull()) - QSKIP("No supported video file"); + CHECK_SELECTED_URL(m_localVideoFile); + CHECK_SELECTED_URL(m_localVideoFile2); + + QAudioOutput output; + TestVideoSink surface(false); + QMediaPlayer player; + + player.setVideoOutput(&surface); + player.setAudioOutput(&output); + player.setSource(*m_localVideoFile); + + QCOMPARE(player.source(), *m_localVideoFile); + QTRY_COMPARE(player.mediaStatus(), QMediaPlayer::LoadedMedia); + + player.setPosition(0); + player.play(); - QMediaPlayer *player = new QMediaPlayer; + QCOMPARE(player.error(), QMediaPlayer::NoError); + QCOMPARE(player.playbackState(), QMediaPlayer::PlayingState); + QVERIFY(player.isSeekable()); + QTRY_VERIFY(player.position() > 0); + QCOMPARE(player.source(), *m_localVideoFile); - TestVideoSurface *surface = new TestVideoSurface; - player->setVideoOutput(surface); + player.stop(); - QVideoProbe *videoProbe = new QVideoProbe; - QAudioProbe *audioProbe = new QAudioProbe; + player.setSource(*m_localVideoFile2); - ProbeDataHandler probeHandler; - connect(videoProbe, SIGNAL(videoFrameProbed(QVideoFrame)), &probeHandler, SLOT(processFrame(QVideoFrame))); - connect(videoProbe, SIGNAL(flush()), &probeHandler, SLOT(flushVideo())); - connect(audioProbe, SIGNAL(audioBufferProbed(QAudioBuffer)), &probeHandler, SLOT(processBuffer(QAudioBuffer))); - connect(audioProbe, SIGNAL(flush()), &probeHandler, SLOT(flushAudio())); + QCOMPARE(player.source(), *m_localVideoFile2); + QTRY_COMPARE(player.mediaStatus(), QMediaPlayer::LoadedMedia); + QTRY_VERIFY(player.isSeekable()); - if (!videoProbe->setSource(player)) - QSKIP("QVideoProbe is not supported"); - audioProbe->setSource(player); + player.setPosition(0); + player.play(); - player->setMedia(localVideoFile); - QTRY_COMPARE(player->mediaStatus(), QMediaPlayer::LoadedMedia); + QCOMPARE(player.error(), QMediaPlayer::NoError); + QCOMPARE(player.playbackState(), QMediaPlayer::PlayingState); + QTRY_VERIFY(player.position() > 0); + QCOMPARE(player.source(), *m_localVideoFile2); - player->pause(); - QTRY_COMPARE(surface->m_frameList.size(), 1); - QVERIFY(!probeHandler.m_frameList.isEmpty()); - QTRY_VERIFY(!probeHandler.m_bufferList.isEmpty()); + player.stop(); - delete player; - QTRY_VERIFY(probeHandler.isVideoFlushCalled); - delete videoProbe; - delete audioProbe; + QTRY_COMPARE(player.playbackState(), QMediaPlayer::StoppedState); } -void tst_QMediaPlayerBackend::playlist() +void tst_QMediaPlayerBackend::multiplePlaybackRateChangingStressTest() { + CHECK_SELECTED_URL(m_localVideoFile3ColorsWithSound); + + if (isCI()) { + if (isDarwinPlatform()) + QSKIP("SKIP on macOS CI since multiple fake drawing on macOS CI platform causes UB. To " + "be investigated."); + + if (isGStreamerPlatform()) + QSKIP_GSTREAMER("QTBUG-124005: spurious failures with gstreamer"); + } + + TestVideoSink surface(false); + QAudioOutput output; QMediaPlayer player; - QSignalSpy mediaSpy(&player, SIGNAL(mediaChanged(QMediaContent))); - QSignalSpy currentMediaSpy(&player, SIGNAL(currentMediaChanged(QMediaContent))); - QSignalSpy stateSpy(&player, SIGNAL(stateChanged(QMediaPlayer::State))); - QSignalSpy mediaStatusSpy(&player, SIGNAL(mediaStatusChanged(QMediaPlayer::MediaStatus))); - QSignalSpy errorSpy(&player, SIGNAL(error(QMediaPlayer::Error))); + player.setAudioOutput(&output); + player.setVideoOutput(&surface); - QFileInfo fileInfo(QFINDTESTDATA("testdata/sample.m3u")); - player.setMedia(QUrl::fromLocalFile(fileInfo.absoluteFilePath())); + player.setSource(*m_localVideoFile3ColorsWithSound); player.play(); - QTRY_COMPARE_WITH_TIMEOUT(player.state(), QMediaPlayer::StoppedState, 10000); - - if (player.mediaStatus() == QMediaPlayer::InvalidMedia || mediaSpy.count() == 1) - QSKIP("QMediaPlayer does not support loading M3U playlists as QMediaPlaylist"); - - QCOMPARE(mediaSpy.count(), 2); - // sample.m3u -> sample.m3u resolved -> test.wav -> - // nested1.m3u -> nested1.m3u resolved -> test.wav -> - // nested2.m3u -> nested2.m3u resolved -> - // test.wav -> _test.wav - // currentMediaChanged signals not emmitted for - // nested1.m3u\_test.wav and nested2.m3u\_test.wav - // because current media stays the same - QCOMPARE(currentMediaSpy.count(), 11); - QCOMPARE(stateSpy.count(), 2); - QCOMPARE(errorSpy.count(), 0); - QCOMPARE(mediaStatusSpy.count(), 19); // 6 x (LoadingMedia -> BufferedMedia -> EndOfMedia) + NoMedia - - mediaSpy.clear(); - currentMediaSpy.clear(); - stateSpy.clear(); - mediaStatusSpy.clear(); - errorSpy.clear(); - player.play(); - QTRY_COMPARE_WITH_TIMEOUT(player.state(), QMediaPlayer::StoppedState, 10000); - QCOMPARE(mediaSpy.count(), 0); - QCOMPARE(currentMediaSpy.count(), 8); - QCOMPARE(stateSpy.count(), 2); - QCOMPARE(errorSpy.count(), 0); - QCOMPARE(mediaStatusSpy.count(), 19); // 6 x (LoadingMedia -> BufferedMedia -> EndOfMedia) + NoMedia - - mediaSpy.clear(); - currentMediaSpy.clear(); - stateSpy.clear(); - mediaStatusSpy.clear(); - errorSpy.clear(); + surface.waitForFrame(); - // <<< Invalid - 1st pass >>> - fileInfo.setFile(QFINDTESTDATA("testdata/invalid_media.m3u")); - player.setMedia(QUrl::fromLocalFile(fileInfo.absoluteFilePath())); + QSignalSpy spy(&player, &QMediaPlayer::playbackStateChanged); - player.play(); - QTRY_COMPARE(player.state(), QMediaPlayer::StoppedState); - // playlist -> resolved playlist - QCOMPARE(mediaSpy.count(), 2); - // playlist -> resolved playlist -> invalid -> "" - QCOMPARE(currentMediaSpy.count(), 4); - QCOMPARE(stateSpy.count(), 2); - QCOMPARE(errorSpy.count(), 1); - QCOMPARE(mediaStatusSpy.count(), 3); // LoadingMedia -> InvalidMedia -> NoMedia - - mediaSpy.clear(); - currentMediaSpy.clear(); - stateSpy.clear(); - mediaStatusSpy.clear(); - errorSpy.clear(); + constexpr qint64 expectedVideoDuration = 3000; + constexpr int waitingInterval = 200; + constexpr qint64 maxDuration = expectedVideoDuration + 2000; + constexpr qint64 minDuration = expectedVideoDuration - 100; + constexpr qint64 maxFrameDelay = 2000; - // <<< Invalid - 2nd pass >>> - player.play(); - QTRY_COMPARE(player.state(), QMediaPlayer::StoppedState); - // media is not changed - QCOMPARE(mediaSpy.count(), 0); - // resolved playlist -> invalid -> "" - QCOMPARE(currentMediaSpy.count(), 3); - QCOMPARE(stateSpy.count(), 2); - QCOMPARE(errorSpy.count(), 1); - QCOMPARE(mediaStatusSpy.count(), 3); // LoadingMedia -> InvalidMedia -> NoMedia - - mediaSpy.clear(); - currentMediaSpy.clear(); - stateSpy.clear(); - mediaStatusSpy.clear(); - errorSpy.clear(); + surface.m_elapsedTimer.start(); - // <<< Invalid2 - 1st pass >>> - fileInfo.setFile(QFINDTESTDATA("/testdata/invalid_media2.m3u")); - player.setMedia(QUrl::fromLocalFile(fileInfo.absoluteFilePath())); + qint64 duration = 0; - player.play(); - QTRY_COMPARE_WITH_TIMEOUT(player.state(), QMediaPlayer::StoppedState, 20000); - // playlist -> resolved playlist - QCOMPARE(mediaSpy.count(), 2); - // playlist -> resolved playlist -> test.wav -> invalid -> test.wav -> "" - QCOMPARE(currentMediaSpy.count(), 6); - QCOMPARE(stateSpy.count(), 2); - QCOMPARE(errorSpy.count(), 1); - QCOMPARE(mediaStatusSpy.count(), 9); // 3 x LoadingMedia + 2 x (BufferedMedia -> EndOfMedia) + InvalidMedia + NoMedia (not in this order) - - mediaSpy.clear(); - currentMediaSpy.clear(); - stateSpy.clear(); - mediaStatusSpy.clear(); - errorSpy.clear(); + for (int i = 0; !spy.wait(waitingInterval); ++i) { + duration += waitingInterval * player.playbackRate(); - // <<< Invalid2 - 2nd pass >>> - player.play(); - QTRY_COMPARE_WITH_TIMEOUT(player.state(), QMediaPlayer::StoppedState, 20000); - // playlist -> resolved playlist - QCOMPARE(mediaSpy.count(), 0); - // playlist -> test.wav -> invalid -> test.wav -> "" - QCOMPARE(currentMediaSpy.count(), 5); - QCOMPARE(stateSpy.count(), 2); - QCOMPARE(errorSpy.count(), 1); - QCOMPARE(mediaStatusSpy.count(), 9); // 3 x LoadingMedia + 2 x (BufferedMedia -> EndOfMedia) + InvalidMedia + NoMedia (not in this order) - - mediaSpy.clear(); - currentMediaSpy.clear(); - stateSpy.clear(); - mediaStatusSpy.clear(); - errorSpy.clear(); + player.setPlaybackRate(0.5 * (i % 4 + 1)); - // <<< Recursive - 1st pass >>> - fileInfo.setFile(QFINDTESTDATA("testdata/recursive_master.m3u")); - player.setMedia(QUrl::fromLocalFile(fileInfo.absoluteFilePath())); + QCOMPARE_LE(duration, maxDuration); - player.play(); - QTRY_COMPARE_WITH_TIMEOUT(player.state(), QMediaPlayer::StoppedState, 20000); - // master playlist -> resolved master playlist - QCOMPARE(mediaSpy.count(), 2); - // master playlist -> resolved master playlist -> - // recursive playlist -> resolved recursive playlist -> - // recursive playlist (this URL is already in the chain of playlists, so the playlist is not resolved) -> - // invalid -> test.wav -> "" - QCOMPARE(currentMediaSpy.count(), 8); - QCOMPARE(stateSpy.count(), 2); - // there is one invalid media in the master playlist - QCOMPARE(errorSpy.count(), 1); - QCOMPARE(mediaStatusSpy.count(), 6); // LoadingMedia -> InvalidMedia -> LoadingMedia -> BufferedMedia - // -> EndOfMedia -> NoMedia - - mediaSpy.clear(); - currentMediaSpy.clear(); - stateSpy.clear(); - mediaStatusSpy.clear(); - errorSpy.clear(); + QVERIFY2(surface.m_elapsedTimer.elapsed() < maxFrameDelay, + "If the delay is more than 2s, we consider the video playing is hanging."); - // <<< Recursive - 2nd pass >>> - player.play(); - QTRY_COMPARE_WITH_TIMEOUT(player.state(), QMediaPlayer::StoppedState, 20000); - QCOMPARE(mediaSpy.count(), 0); - // resolved master playlist -> - // resolved recursive playlist -> - // recursive playlist (this URL is already in the chain of playlists, so the playlist is not resolved) -> - // invalid -> test.wav -> "" - QCOMPARE(currentMediaSpy.count(), 6); - QCOMPARE(stateSpy.count(), 2); - // there is one invalid media in the master playlist - QCOMPARE(errorSpy.count(), 1); - QCOMPARE(mediaStatusSpy.count(), 6); // LoadingMedia -> InvalidMedia -> LoadingMedia -> BufferedMedia - // -> EndOfMedia -> NoMedia + /* Some debug code for windows. Use the code instead of the check above to debug the bug. + * https://bugreports.qt.io/browse/QTBUG-105940. + * TODO: fix hanging on windows and remove. + if ( surface.m_elapsedTimer.elapsed() > maxFrameDelay ) { + qDebug() << "pause/play"; + player.pause(); + player.play(); + surface.m_elapsedTimer.restart(); + spy.clear(); + }*/ + } + + duration += waitingInterval * player.playbackRate(); + + QCOMPARE_GT(duration, minDuration); + + QCOMPARE(spy.size(), 1); + QCOMPARE(spy.at(0).size(), 1); + QCOMPARE(spy.at(0).at(0).value<QMediaPlayer::PlaybackState>(), QMediaPlayer::StoppedState); + + QCOMPARE(player.playbackState(), QMediaPlayer::StoppedState); + QCOMPARE(player.mediaStatus(), QMediaPlayer::EndOfMedia); } -void tst_QMediaPlayerBackend::playlistObject() +void tst_QMediaPlayerBackend::multipleSeekStressTest() { - if (!isWavSupported()) - QSKIP("Sound format is not supported"); + QSKIP_GSTREAMER("QTBUG-124005: spurious test failures with gstreamer"); +#ifdef Q_OS_ANDROID + QSKIP("frame.toImage will return null image because of QTBUG-108446"); +#endif + CHECK_SELECTED_URL(m_localVideoFile3ColorsWithSound); + + TestVideoSink surface(false); + QAudioOutput output; QMediaPlayer player; - QSignalSpy mediaSpy(&player, SIGNAL(mediaChanged(QMediaContent))); - QSignalSpy currentMediaSpy(&player, SIGNAL(currentMediaChanged(QMediaContent))); - QSignalSpy stateSpy(&player, SIGNAL(stateChanged(QMediaPlayer::State))); - QSignalSpy mediaStatusSpy(&player, SIGNAL(mediaStatusChanged(QMediaPlayer::MediaStatus))); - QSignalSpy errorSpy(&player, SIGNAL(error(QMediaPlayer::Error))); + player.setAudioOutput(&output); + player.setVideoOutput(&surface); - // --- empty playlist - QMediaPlaylist emptyPlaylist; - player.setPlaylist(&emptyPlaylist); + player.setSource(*m_localVideoFile3ColorsWithSound); player.play(); - QTRY_COMPARE_WITH_TIMEOUT(player.state(), QMediaPlayer::StoppedState, 10000); - QCOMPARE(mediaSpy.count(), 1); - QCOMPARE(currentMediaSpy.count(), 1); // Empty media - QCOMPARE(stateSpy.count(), 0); - QCOMPARE(errorSpy.count(), 0); - QCOMPARE(mediaStatusSpy.count(), 0); + auto waitAndCheckFrame = [&](qint64 pos, QString checkInfo) { + auto errorPrintingGuard = qScopeGuard([&]() { + qDebug() << "Error:" << checkInfo; + qDebug() << "Position:" << pos; + }); - mediaSpy.clear(); - currentMediaSpy.clear(); - stateSpy.clear(); - mediaStatusSpy.clear(); - errorSpy.clear(); + auto frame = surface.waitForFrame(); + QVERIFY(frame.isValid()); - // --- Valid playlist - QMediaPlaylist playlist; - playlist.addMedia(QUrl::fromLocalFile(QFileInfo(QFINDTESTDATA("testdata/test.wav")).absoluteFilePath())); - playlist.addMedia(QUrl::fromLocalFile(QFileInfo(QFINDTESTDATA("testdata/_test.wav")).absoluteFilePath())); - player.setPlaylist(&playlist); + const auto trackTime = pos * 1000; - player.play(); - QTRY_COMPARE_WITH_TIMEOUT(player.state(), QMediaPlayer::StoppedState, 10000); + // in theory, previous frame might be received, in this case we wait for a new one that is + // expected to be relevant + if (frame.endTime() < trackTime || frame.startTime() > trackTime) { + frame = surface.waitForFrame(); + QVERIFY(frame.isValid()); + } - QCOMPARE(mediaSpy.count(), 1); - QCOMPARE(currentMediaSpy.count(), 3); // test.wav -> _test.wav -> NoMedia - QCOMPARE(stateSpy.count(), 2); - QCOMPARE(errorSpy.count(), 0); - QCOMPARE(mediaStatusSpy.count(), 7); // 2 x (LoadingMedia -> BufferedMedia -> EndOfMedia) + NoMedia + QCOMPARE_GE(frame.startTime(), trackTime - 200'000); + QCOMPARE_LE(frame.endTime(), trackTime + 200'000); - mediaSpy.clear(); - currentMediaSpy.clear(); - stateSpy.clear(); - mediaStatusSpy.clear(); - errorSpy.clear(); + auto frameImage = frame.toImage(); + const auto actualColor = frameImage.pixel(1, 1); - player.play(); - QTRY_COMPARE_WITH_TIMEOUT(player.state(), QMediaPlayer::StoppedState, 10000); + const auto actualColorIndex = findSimilarColorIndex(m_video3Colors, actualColor); - QCOMPARE(mediaSpy.count(), 0); - QCOMPARE(currentMediaSpy.count(), 4); // playlist -> test.wav -> _test.wav -> NoMedia - QCOMPARE(stateSpy.count(), 2); - QCOMPARE(errorSpy.count(), 0); - QCOMPARE(mediaStatusSpy.count(), 7); // 2 x (LoadingMedia -> BufferedMedia -> EndOfMedia) + NoMedia + const auto expectedColorIndex = pos / 1000; - player.setPlaylist(nullptr); + QCOMPARE(actualColorIndex, expectedColorIndex); - mediaSpy.clear(); - currentMediaSpy.clear(); - stateSpy.clear(); - mediaStatusSpy.clear(); - errorSpy.clear(); + errorPrintingGuard.dismiss(); + }; - // --- Nested playlist - QMediaPlaylist nestedPlaylist; - nestedPlaylist.addMedia(QUrl::fromLocalFile(QFileInfo(QFINDTESTDATA("testdata/_test.wav")).absoluteFilePath())); - nestedPlaylist.addMedia(QUrl::fromLocalFile(QFileInfo(QFINDTESTDATA("testdata/test.wav")).absoluteFilePath())); - nestedPlaylist.addMedia(&playlist); - player.setPlaylist(&nestedPlaylist); + auto seekAndCheck = [&](qint64 pos) { + QSignalSpy positionSpy(&player, &QMediaPlayer::positionChanged); + player.setPosition(pos); - player.play(); - QTRY_COMPARE_WITH_TIMEOUT(player.state(), QMediaPlayer::StoppedState, 10000); + QTRY_VERIFY(positionSpy.size() >= 1); + int setPosition = positionSpy.first().first().toInt(); + QCOMPARE_GT(setPosition, pos - 100); + QCOMPARE_LT(setPosition, pos + 100); + }; - QCOMPARE(mediaSpy.count(), 1); - QCOMPARE(currentMediaSpy.count(), 6); // _test.wav -> test.wav -> nested playlist - // -> test.wav -> _test.wav -> NoMedia - QCOMPARE(stateSpy.count(), 2); - QCOMPARE(errorSpy.count(), 0); - QCOMPARE(mediaStatusSpy.count(), 13); // 4 x (LoadingMedia -> BufferedMedia -> EndOfMedia) + NoMedia + constexpr qint64 posInterval = 10; - player.setPlaylist(nullptr); + { + for (qint64 pos = posInterval; pos <= 2200; pos += posInterval) + seekAndCheck(pos); - mediaSpy.clear(); - currentMediaSpy.clear(); - stateSpy.clear(); - mediaStatusSpy.clear(); - errorSpy.clear(); + waitAndCheckFrame(2200, "emulate fast moving of a seek slider forward"); - // --- playlist with invalid media - QMediaPlaylist invalidPlaylist; - invalidPlaylist.addMedia(QUrl("invalid")); - invalidPlaylist.addMedia(QUrl::fromLocalFile(QFileInfo(QFINDTESTDATA("testdata/test.wav")).absoluteFilePath())); + QCOMPARE_NE(player.mediaStatus(), QMediaPlayer::EndOfMedia); + QCOMPARE(player.playbackState(), QMediaPlayer::PlayingState); + } - player.setPlaylist(&invalidPlaylist); + { + for (qint64 pos = 2100; pos >= 800; pos -= posInterval) + seekAndCheck(pos); - player.play(); - QTRY_COMPARE_WITH_TIMEOUT(player.state(), QMediaPlayer::StoppedState, 10000); + waitAndCheckFrame(800, "emulate fast moving of a seek slider backward"); - QCOMPARE(mediaSpy.count(), 1); - QCOMPARE(currentMediaSpy.count(), 3); // invalid -> test.wav -> NoMedia - QCOMPARE(stateSpy.count(), 2); - QCOMPARE(errorSpy.count(), 1); - QCOMPARE(mediaStatusSpy.count(), 6); // Loading -> Invalid -> Loading -> Buffered -> EndOfMedia -> NoMedia + QCOMPARE_NE(player.mediaStatus(), QMediaPlayer::EndOfMedia); + QCOMPARE(player.playbackState(), QMediaPlayer::PlayingState); + } - player.setPlaylist(nullptr); + { + player.pause(); - mediaSpy.clear(); - currentMediaSpy.clear(); - stateSpy.clear(); - mediaStatusSpy.clear(); - errorSpy.clear(); + for (qint64 pos = 500; pos <= 1100; pos += posInterval) + seekAndCheck(pos); - // --- playlist with only invalid media - QMediaPlaylist invalidPlaylist2; - invalidPlaylist2.addMedia(QUrl("invalid")); - invalidPlaylist2.addMedia(QUrl("invalid2")); + waitAndCheckFrame(1100, "emulate fast moving of a seek slider forward on paused state"); - player.setPlaylist(&invalidPlaylist2); + QCOMPARE_NE(player.mediaStatus(), QMediaPlayer::EndOfMedia); + QCOMPARE(player.playbackState(), QMediaPlayer::PausedState); + } +} - player.play(); - QTRY_COMPARE_WITH_TIMEOUT(player.state(), QMediaPlayer::StoppedState, 10000); +void tst_QMediaPlayerBackend::setPlaybackRate_changesActualRateAndFramesRenderingTime_data() +{ + QTest::addColumn<bool>("withAudio"); + QTest::addColumn<int>("positionDeviationMs"); + + QTest::newRow("Without audio") << false << 170; - QCOMPARE(mediaSpy.count(), 1); - QCOMPARE(currentMediaSpy.count(), 3); // invalid -> invalid2 -> NoMedia - QCOMPARE(stateSpy.count(), 2); - QCOMPARE(errorSpy.count(), 2); - QCOMPARE(mediaStatusSpy.count(), 5); // Loading -> Invalid -> Loading -> Invalid -> NoMedia + // set greater positionDeviationMs for case with audio due to possible synchronization. + QTest::newRow("With audio") << true << 200; } -void tst_QMediaPlayerBackend::surfaceTest_data() +void tst_QMediaPlayerBackend::setPlaybackRate_changesActualRateAndFramesRenderingTime() { - QTest::addColumn< QList<QVideoFrame::PixelFormat> >("formatsList"); - - QList<QVideoFrame::PixelFormat> formatsRGB; - formatsRGB << QVideoFrame::Format_RGB32 - << QVideoFrame::Format_ARGB32 - << QVideoFrame::Format_RGB565 - << QVideoFrame::Format_BGRA32; + QSKIP_GSTREAMER("QTBUG-124005: timing issues"); - QList<QVideoFrame::PixelFormat> formatsYUV; - formatsYUV << QVideoFrame::Format_YUV420P - << QVideoFrame::Format_YUV422P - << QVideoFrame::Format_YV12 - << QVideoFrame::Format_UYVY - << QVideoFrame::Format_YUYV - << QVideoFrame::Format_NV12 - << QVideoFrame::Format_NV21; + QFETCH(bool, withAudio); + QFETCH(int, positionDeviationMs); - QTest::newRow("RGB formats") - << formatsRGB; +#ifdef Q_OS_ANDROID + QSKIP("frame.toImage will return null image because of QTBUG-108446"); +#endif + CHECK_SELECTED_URL(m_localVideoFile3ColorsWithSound); -#if !QT_CONFIG(directshow) - QTest::newRow("YVU formats") - << formatsYUV; +#ifdef Q_OS_MACOS + if (qEnvironmentVariable("QTEST_ENVIRONMENT").toLower() == "ci") + QSKIP("SKIP on macOS CI since multiple fake drawing on macOS CI platform causes UB. To be " + "investigated: QTBUG-111744"); #endif + m_fixture->player.setAudioOutput( + withAudio ? &m_fixture->output + : nullptr); // TODO: mock audio output and check sound by frequency + m_fixture->player.setSource(*m_localVideoFile3ColorsWithSound); + + auto checkColorAndPosition = [&](qint64 expectedPosition, QString errorTag) { + constexpr qint64 intervalTime = 1000; + + const int colorIndex = expectedPosition / intervalTime; + const auto expectedColor = m_video3Colors[colorIndex]; + const auto actualPosition = m_fixture->player.position(); + + auto frame = m_fixture->surface.videoFrame(); + auto image = frame.toImage(); + QVERIFY(!image.isNull()); + + const auto actualColor = image.pixel(1, 1); + + auto errorPrintingGuard = qScopeGuard([&]() { + qDebug() << "Error Tag:" << errorTag; + qDebug() << " Actual Color:" << QColor(actualColor) + << " Expected Color:" << QColor(expectedColor); + qDebug() << " Most probable actual color index:" + << findSimilarColorIndex(m_video3Colors, actualColor) + << "Expected color index:" << colorIndex; + qDebug() << " Actual position:" << actualPosition; + qDebug() << " Frame start time:" << frame.startTime(); + }); + + // TODO: investigate why frames sometimes are not delivered in time on windows + constexpr qreal maxColorDifference = 0.18; + QVERIFY(m_fixture->player.isPlaying()); + QCOMPARE_LE(colorDifference(actualColor, expectedColor), maxColorDifference); + QCOMPARE_GT(actualPosition, expectedPosition - positionDeviationMs); + QCOMPARE_LT(actualPosition, expectedPosition + positionDeviationMs); + + const auto framePosition = frame.startTime() / 1000; + + QCOMPARE_GT(framePosition, expectedPosition - positionDeviationMs); + QCOMPARE_LT(framePosition, expectedPosition + positionDeviationMs); + QCOMPARE_LT(qAbs(framePosition - actualPosition), positionDeviationMs); + + errorPrintingGuard.dismiss(); + }; + + m_fixture->player.play(); - QTest::newRow("RGB & YUV formats") - << formatsRGB + formatsYUV; + m_fixture->surface.waitForFrame(); + + auto waitUntil = [&](qint64 targetPosition) { + const auto position = m_fixture->player.position(); + + const auto waitingIntervalMs = + static_cast<int>((targetPosition - position) / m_fixture->player.playbackRate()); + + if (targetPosition > position) + QTest::qWait(waitingIntervalMs); + + qDebug() << "Test waiting:" << waitingIntervalMs << "ms, Position:" << position << "=>" + << m_fixture->player.position() << "Expected target position:" << targetPosition + << "playbackRate:" << m_fixture->player.playbackRate(); + }; + + waitUntil(400); + checkColorAndPosition(400, "Check default playback rate"); + + m_fixture->player.setPlaybackRate(2.); + + waitUntil(1400); + checkColorAndPosition(1400, "Check 2.0 playback rate"); + + m_fixture->player.setPlaybackRate(0.5); + + waitUntil(1800); + checkColorAndPosition(1800, "Check 0.5 playback rate"); + + m_fixture->player.setPlaybackRate(0.321); + + m_fixture->player.stop(); } void tst_QMediaPlayerBackend::surfaceTest() { - // 25 fps video file - if (localVideoFile.isNull()) - QSKIP("No supported video file"); + QSKIP_GSTREAMER("QTBUG-124005: spurious failure, probably asynchronous event delivery"); - QFETCH(QList<QVideoFrame::PixelFormat>, formatsList); + CHECK_SELECTED_URL(m_localVideoFile); + // 25 fps video file - TestVideoSurface surface(false); - surface.setSupportedFormats(formatsList); + QAudioOutput output; + TestVideoSink surface(false); QMediaPlayer player; + player.setAudioOutput(&output); player.setVideoOutput(&surface); - player.setMedia(localVideoFile); + player.setSource(*m_localVideoFile); player.play(); QTRY_VERIFY(player.position() >= 1000); - if (surface.error() == QAbstractVideoSurface::UnsupportedFormatError) - QSKIP("None of the pixel formats is supported by the backend"); - QVERIFY2(surface.m_totalFrames >= 25, qPrintable(QString("Expected >= 25, got %1").arg(surface.m_totalFrames))); + QVERIFY2(surface.m_totalFrames >= 25, qPrintable(QStringLiteral("Expected >= 25, got %1").arg(surface.m_totalFrames))); } -void tst_QMediaPlayerBackend::multipleSurfaces() +void tst_QMediaPlayerBackend::metadata() { - if (localVideoFile.isNull()) - QSKIP("No supported video file"); + // QTBUG-124380: gstreamer reports CoverArtImage instead of ThumbnailImage + QMediaMetaData::Key thumbnailKey = + isGStreamerPlatform() ? QMediaMetaData::CoverArtImage : QMediaMetaData::ThumbnailImage; - QList<QVideoFrame::PixelFormat> formats1; - formats1 << QVideoFrame::Format_RGB32 - << QVideoFrame::Format_ARGB32; - QList<QVideoFrame::PixelFormat> formats2; - formats2 << QVideoFrame::Format_YUV420P - << QVideoFrame::Format_RGB32; + CHECK_SELECTED_URL(m_localFileWithMetadata); - TestVideoSurface surface1(false); - surface1.setSupportedFormats(formats1); - TestVideoSurface surface2(false); - surface2.setSupportedFormats(formats2); + m_fixture->player.setSource(*m_localFileWithMetadata); - QMediaPlayer player; - player.setVideoOutput(QList<QAbstractVideoSurface *>() << &surface1 << &surface2); - player.setMedia(localVideoFile); - player.play(); - QTRY_VERIFY(player.position() >= 1000); - QVERIFY2(surface1.m_totalFrames >= 25, qPrintable(QString("Expected >= 25, got %1").arg(surface1.m_totalFrames))); - QVERIFY2(surface2.m_totalFrames >= 25, qPrintable(QString("Expected >= 25, got %1").arg(surface2.m_totalFrames))); - QCOMPARE(surface1.m_totalFrames, surface2.m_totalFrames); + QTRY_VERIFY(m_fixture->metadataChanged.size() > 0); + + const QMediaMetaData metadata = m_fixture->player.metaData(); + QCOMPARE(metadata.value(QMediaMetaData::Title).toString(), QStringLiteral("Nokia Tune")); + QCOMPARE(metadata.value(QMediaMetaData::ContributingArtist).toString(), QStringLiteral("TestArtist")); + QCOMPARE(metadata.value(QMediaMetaData::AlbumTitle).toString(), QStringLiteral("TestAlbum")); + QCOMPARE(metadata.value(QMediaMetaData::Duration), QVariant(7704)); + QVERIFY(!metadata.value(thumbnailKey).value<QImage>().isNull()); + m_fixture->clearSpies(); + + m_fixture->player.setSource(QUrl()); + + QCOMPARE(m_fixture->metadataChanged.size(), 1); + QVERIFY(m_fixture->player.metaData().isEmpty()); } -void tst_QMediaPlayerBackend::metadata() +void tst_QMediaPlayerBackend::metadata_returnsMetadataWithThumbnail_whenMediaHasThumbnail_data() { - if (localFileWithMetadata.isNull()) - QSKIP("No supported media file"); + QTest::addColumn<MaybeUrl>("mediaUrl"); + QTest::addColumn<bool>("hasThumbnail"); + QTest::addColumn<QSize>("expectedSize"); + QTest::addColumn<QColor>("expectedColor"); + + QTest::addRow("jpeg thumbnail") << m_videoFileWithJpegThumbnail << true << QSize{ 20, 28 } << QColor(35, 177, 77); + QTest::addRow("png thumbnail") << m_videoFileWithPngThumbnail << true << QSize{ 20, 28 } << QColor(35, 177, 77); + QTest::addRow("no thumbnail") << m_localVideoFile3ColorsWithSound << false << QSize{ 0, 0 } << QColor(0, 0, 0); +} - QMediaPlayer player; +void tst_QMediaPlayerBackend::metadata_returnsMetadataWithThumbnail_whenMediaHasThumbnail() +{ + // QTBUG-124380: gstreamer reports CoverArtImage instead of ThumbnailImage + QMediaMetaData::Key key = + isGStreamerPlatform() ? QMediaMetaData::CoverArtImage : QMediaMetaData::ThumbnailImage; - QSignalSpy metadataAvailableSpy(&player, SIGNAL(metaDataAvailableChanged(bool))); - QSignalSpy metadataChangedSpy(&player, SIGNAL(metaDataChanged())); + // Arrange + QFETCH(const MaybeUrl, mediaUrl); + QFETCH(const bool, hasThumbnail); + QFETCH(const QSize, expectedSize); + QFETCH(const QColor, expectedColor); - player.setMedia(localFileWithMetadata); + CHECK_SELECTED_URL(mediaUrl); - QTRY_VERIFY(player.isMetaDataAvailable()); - QCOMPARE(metadataAvailableSpy.count(), 1); - QVERIFY(metadataAvailableSpy.last()[0].toBool()); - QVERIFY(metadataChangedSpy.count() > 0); + m_fixture->player.setSource(*mediaUrl); + QTRY_VERIFY(!m_fixture->metadataChanged.empty()); + + // Act + const QMediaMetaData metadata = m_fixture->player.metaData(); + const QImage thumbnail = metadata.value(key).value<QImage>(); + + // Assert + QCOMPARE_EQ(!thumbnail.isNull(), hasThumbnail); + QCOMPARE_EQ(thumbnail.size(), expectedSize); + + if (hasThumbnail) { + const QPoint center{ expectedSize.width() / 2, expectedSize.height() / 2 }; + const auto centerColor = thumbnail.pixelColor(center); + + constexpr int maxChannelDiff = 5; + QCOMPARE_LT(std::abs(centerColor.red() - expectedColor.red()), maxChannelDiff); + QCOMPARE_LT(std::abs(centerColor.green() - expectedColor.green()), maxChannelDiff); + QCOMPARE_LT(std::abs(centerColor.blue() - expectedColor.blue()), maxChannelDiff); + } +} + +void tst_QMediaPlayerBackend::metadata_returnsMetadataWithHasHdrContent_whenMediaHasHdrContent_data() +{ + QTest::addColumn<MaybeUrl>("mediaUrl"); + QTest::addColumn<bool>("hasHdrContent"); + + QTest::addRow("SDR Video") << m_localVideoFile << false; + QTest::addRow("HDR Video") << m_hdrVideo << true; +} + +void tst_QMediaPlayerBackend::metadata_returnsMetadataWithHasHdrContent_whenMediaHasHdrContent() +{ + QFETCH(const MaybeUrl, mediaUrl); + QFETCH(const bool, hasHdrContent); - QCOMPARE(player.metaData(QMediaMetaData::Title).toString(), QStringLiteral("Nokia Tune")); - QCOMPARE(player.metaData(QMediaMetaData::ContributingArtist).toString(), QStringLiteral("TestArtist")); - QCOMPARE(player.metaData(QMediaMetaData::AlbumTitle).toString(), QStringLiteral("TestAlbum")); + QSKIP_IF_NOT_FFMPEG(); - metadataAvailableSpy.clear(); - metadataChangedSpy.clear(); + m_fixture->player.setSource(*mediaUrl); + QTRY_VERIFY(!m_fixture->metadataChanged.empty()); - player.setMedia(QMediaContent()); + const QMediaMetaData metadata = m_fixture->player.videoTracks().front(); + const bool hdrContent = metadata.value(QMediaMetaData::HasHdrContent).value<bool>(); - QVERIFY(!player.isMetaDataAvailable()); - QCOMPARE(metadataAvailableSpy.count(), 1); - QVERIFY(!metadataAvailableSpy.last()[0].toBool()); - QCOMPARE(metadataChangedSpy.count(), 1); - QVERIFY(player.availableMetaData().isEmpty()); + QCOMPARE_EQ(hasHdrContent, hdrContent); } void tst_QMediaPlayerBackend::playerStateAtEOS() { - if (!isWavSupported()) - QSKIP("Sound format is not supported"); + CHECK_SELECTED_URL(m_localWavFile); + QAudioOutput output; QMediaPlayer player; + player.setAudioOutput(&output); bool endOfMediaReceived = false; - connect(&player, &QMediaPlayer::mediaStatusChanged, [&](QMediaPlayer::MediaStatus status) { + connect(&player, &QMediaPlayer::mediaStatusChanged, + this, [&](QMediaPlayer::MediaStatus status) { if (status == QMediaPlayer::EndOfMedia) { - QCOMPARE(player.state(), QMediaPlayer::StoppedState); + QCOMPARE(player.playbackState(), QMediaPlayer::StoppedState); endOfMediaReceived = true; } }); - player.setMedia(localWavFile); + player.setSource(*m_localWavFile); player.play(); QTRY_COMPARE(player.mediaStatus(), QMediaPlayer::EndOfMedia); @@ -1475,87 +2567,857 @@ void tst_QMediaPlayerBackend::playerStateAtEOS() void tst_QMediaPlayerBackend::playFromBuffer() { - if (localVideoFile.isNull()) - QSKIP("No supported video file"); + QSKIP_GSTREAMER("QTBUG-124005: spurious failure, probably asynchronous event delivery"); - TestVideoSurface surface(false); + CHECK_SELECTED_URL(m_localVideoFile); + + TestVideoSink surface(false); QMediaPlayer player; player.setVideoOutput(&surface); - QFile file(localVideoFile.request().url().toLocalFile()); - if (!file.open(QIODevice::ReadOnly)) - QSKIP("Could not open file"); - player.setMedia(localVideoFile, &file); + QFile file(u":"_s + m_localVideoFile->toEncoded(QUrl::RemoveScheme)); + QVERIFY(file.open(QIODevice::ReadOnly)); + + player.setSourceDevice(&file, *m_localVideoFile); player.play(); QTRY_VERIFY(player.position() >= 1000); - if (surface.error() == QAbstractVideoSurface::UnsupportedFormatError) - QSKIP("None of the pixel formats is supported by the backend"); - QVERIFY2(surface.m_totalFrames >= 25, qPrintable(QString("Expected >= 25, got %1").arg(surface.m_totalFrames))); + QVERIFY2(surface.m_totalFrames >= 25, qPrintable(QStringLiteral("Expected >= 25, got %1").arg(surface.m_totalFrames))); } -TestVideoSurface::TestVideoSurface(bool storeFrames): - m_totalFrames(0), - m_storeFrames(storeFrames) +void tst_QMediaPlayerBackend::audioVideoAvailable() { - // set default formats - m_supported << QVideoFrame::Format_RGB32 - << QVideoFrame::Format_ARGB32 - << QVideoFrame::Format_ARGB32_Premultiplied - << QVideoFrame::Format_RGB565 - << QVideoFrame::Format_RGB555; + CHECK_SELECTED_URL(m_localVideoFile); + + TestVideoSink surface(false); + QAudioOutput output; + QMediaPlayer player; + QSignalSpy hasVideoSpy(&player, &QMediaPlayer::hasVideoChanged); + QSignalSpy hasAudioSpy(&player, &QMediaPlayer::hasAudioChanged); + player.setVideoOutput(&surface); + player.setAudioOutput(&output); + player.setSource(*m_localVideoFile); + QTRY_VERIFY(player.hasVideo()); + QTRY_VERIFY(player.hasAudio()); + QCOMPARE(hasVideoSpy.size(), 1); + QCOMPARE(hasAudioSpy.size(), 1); + player.setSource(QUrl()); + QTRY_VERIFY(!player.hasVideo()); + QTRY_VERIFY(!player.hasAudio()); + QCOMPARE(hasVideoSpy.size(), 2); + QCOMPARE(hasAudioSpy.size(), 2); } -QList<QVideoFrame::PixelFormat> TestVideoSurface::supportedPixelFormats( - QAbstractVideoBuffer::HandleType handleType) const +void tst_QMediaPlayerBackend::audioVideoAvailable_updatedOnNewMedia() { - if (handleType == QAbstractVideoBuffer::NoHandle) { - return m_supported; + CHECK_SELECTED_URL(m_localVideoFile); + CHECK_SELECTED_URL(m_localWavFile); + + TestVideoSink surface(false); + QAudioOutput output; + QMediaPlayer player; + QSignalSpy hasVideoSpy(&player, &QMediaPlayer::hasVideoChanged); + QSignalSpy hasAudioSpy(&player, &QMediaPlayer::hasAudioChanged); + player.setVideoOutput(&surface); + player.setAudioOutput(&output); + player.setSource(*m_localVideoFile); + QTRY_VERIFY(player.hasVideo()); + QTRY_VERIFY(player.hasAudio()); + QCOMPARE(hasVideoSpy.size(), 1); + QCOMPARE(hasAudioSpy.size(), 1); + + hasVideoSpy.clear(); + hasAudioSpy.clear(); + + player.setSource(*m_localWavFile); + + auto expectedHasVideoSignals = SignalList{ + { false }, + }; + QTRY_COMPARE(hasVideoSpy, expectedHasVideoSignals); + + if (isGStreamerPlatform()) { + // GStreamer unsets hasAudio/hasVideo on new URIs + auto expectedHasAudioSignals = SignalList{ + { false }, + { true }, + }; + QTRY_COMPARE(hasAudioSpy, expectedHasAudioSignals); } else { - return QList<QVideoFrame::PixelFormat>(); + QCOMPARE(hasAudioSpy.size(), 0); + } +} + +void tst_QMediaPlayerBackend::isSeekable() +{ + CHECK_SELECTED_URL(m_localVideoFile); + + TestVideoSink surface(false); + QMediaPlayer player; + player.setVideoOutput(&surface); + QVERIFY(!player.isSeekable()); + player.setSource(*m_localVideoFile); + QTRY_VERIFY(player.isSeekable()); +} + +void tst_QMediaPlayerBackend::positionAfterSeek() +{ + CHECK_SELECTED_URL(m_localVideoFile); + + TestVideoSink surface(false); + QMediaPlayer player; + player.setVideoOutput(&surface); + QVERIFY(!player.isSeekable()); + player.setSource(*m_localVideoFile); + QTRY_COMPARE(player.mediaStatus(), QMediaPlayer::LoadedMedia); + player.pause(); + player.setPosition(500); + QTRY_VERIFY(player.position() == 500); + player.setPosition(700); + QVERIFY(player.position() != 0); + QTRY_VERIFY(player.position() == 700); + player.play(); + QTRY_VERIFY(player.position() > 700); + player.setPosition(200); + QVERIFY(player.position() != 0); + QTRY_VERIFY(player.position() < 700); +} + +void tst_QMediaPlayerBackend::pause_rendersVideoAtCorrectResolution_data() +{ + QTest::addColumn<MaybeUrl>("mediaFile"); + QTest::addColumn<int>("width"); + QTest::addColumn<int>("height"); + + QTest::addRow("mp4") << m_videoDimensionTestFile << 540 << 320; + QTest::addRow("av1") << m_av1File << 160 * 143 / 80 << 160; +} + +void tst_QMediaPlayerBackend::pause_rendersVideoAtCorrectResolution() +{ + QFETCH(const MaybeUrl, mediaFile); + QFETCH(const int, width); + QFETCH(const int, height); + CHECK_SELECTED_URL(mediaFile); + + // Arrange + TestVideoSink surface(true); + QMediaPlayer player; + player.setVideoOutput(&surface); + QVERIFY(!player.isSeekable()); + player.setSource(*mediaFile); + QTRY_COMPARE(player.mediaStatus(), QMediaPlayer::LoadedMedia); + + // Act + player.pause(); + + if (isCI() && isFFMPEGPlatform()) + QEXPECT_FAIL("av1", "QTBUG-119711: AV1 decoding requires HW support in the FFMPEG backend", + Abort); + + QTRY_COMPARE(surface.m_totalFrames, 1); + + // Assert + QCOMPARE(surface.m_frameList.last().width(), width); + QCOMPARE(surface.videoSize().width(), width); + QCOMPARE(surface.m_frameList.last().height(), height); + QCOMPARE(surface.videoSize().height(), height); +} + +void tst_QMediaPlayerBackend::position() +{ + CHECK_SELECTED_URL(m_localVideoFile); + + TestVideoSink surface(true); + QMediaPlayer player; + player.setVideoOutput(&surface); + QVERIFY(!player.isSeekable()); + player.setSource(*m_localVideoFile); + QTRY_VERIFY(player.isSeekable()); + + player.play(); + player.setPosition(1000); + QVERIFY(player.position() > 950); + QVERIFY(player.position() < 1050); + QTRY_VERIFY(player.position() > 1050); + + player.pause(); + player.setPosition(500); + QVERIFY(player.position() > 450); + QVERIFY(player.position() < 550); + QTest::qWait(200); + QVERIFY(player.position() > 450); + QVERIFY(player.position() < 550); +} + +void tst_QMediaPlayerBackend::durationDetectionIssues_data() +{ + QTest::addColumn<QString>("mediaFile"); + QTest::addColumn<qint64>("expectedDuration"); + QTest::addColumn<int>("expectedVideoTrackCount"); + QTest::addColumn<qint64>("expectedVideoTrackDuration"); + QTest::addColumn<int>("expectedAudioTrackCount"); + QTest::addColumn<QVariant>("expectedAudioTrackDuration"); + + // clang-format off + + QTest::newRow("stream-duration-in-metadata") + << QString{ "qrc:/testdata/duration_issues.webm" } + << 400ll // Total media duration + << 1 // Number of video tracks in file + << 400ll // Video stream duration + << 0 // Number of audio tracks in file + << QVariant{}; // Audio stream duration (unused) + + QTest::newRow("no-stream-duration-in-metadata") + << QString{ "qrc:/testdata/nokia-tune.mkv" } + << 7531ll // Total media duration + << 0 // Number of video tracks in file + << 0ll // Video stream duration (unused) + << 1 // Number of audio tracks in file + << QVariant{}; // Audio stream duration (not present on file) + + // clang-format on +} + +void tst_QMediaPlayerBackend::durationDetectionIssues() +{ + if (isGStreamerPlatform() && isCI()) + QSKIP("QTBUG-124005: Fails with gstreamer on CI"); + + QFETCH(QString, mediaFile); + QFETCH(qint64, expectedDuration); + QFETCH(int, expectedVideoTrackCount); + QFETCH(qint64, expectedVideoTrackDuration); + QFETCH(int, expectedAudioTrackCount); + QFETCH(QVariant, expectedAudioTrackDuration); + + // ffmpeg detects stream an incorrect stream duration, so we take + // the correct duration from the metadata + + TestVideoSink surface(false); + QAudioOutput output; + QMediaPlayer player; + + QSignalSpy durationSpy(&player, &QMediaPlayer::durationChanged); + + player.setVideoOutput(&surface); + player.setAudioOutput(&output); + player.setSource(mediaFile); + + QTRY_COMPARE_EQ(player.mediaStatus(), QMediaPlayer::LoadedMedia); + + // Duration event received + QCOMPARE(durationSpy.size(), 1); + QCOMPARE(durationSpy.front().front(), expectedDuration); + + // Duration property + QCOMPARE(player.duration(), expectedDuration); + QCOMPARE(player.metaData().value(QMediaMetaData::Duration), expectedDuration); + + // Track duration properties + const auto videoTracks = player.videoTracks(); + QCOMPARE(videoTracks.size(), expectedVideoTrackCount); + + if (expectedVideoTrackCount != 0) + QCOMPARE(videoTracks.front().value(QMediaMetaData::Duration), expectedVideoTrackDuration); + + const auto audioTracks = player.audioTracks(); + QCOMPARE(audioTracks.size(), expectedAudioTrackCount); + + if (expectedAudioTrackCount != 0) + QCOMPARE(audioTracks.front().value(QMediaMetaData::Duration), expectedAudioTrackDuration); +} + +struct LoopIteration { + qint64 startPos; + qint64 endPos; + qint64 posCount; +}; +// Creates a vector of LoopIterations, containing start- and end position +// and the number of position changes per video loop iteration. +static std::vector<LoopIteration> loopIterations(const QSignalSpy &positionSpy) +{ + std::vector<LoopIteration> result; + // Loops through all positions emitted by QMediaPlayer::positionChanged + for (auto ¶ms : positionSpy) { + const auto pos = params.front().value<qint64>(); + + // Adds new LoopIteration struct to result if position is lower than previous position + if (result.empty() || pos < result.back().endPos) { + result.push_back(LoopIteration{pos, pos, 1}); + } + // Updates end position of the current LoopIteration if position is higher than previous position + else { + result.back().posCount++; + result.back().endPos = pos; + } + } + return result; +} + +void tst_QMediaPlayerBackend::finiteLoops() +{ + QSKIP_GSTREAMER("QTBUG-123056(?): spuriously failures of the gstreamer backend"); + + CHECK_SELECTED_URL(m_localVideoFile3ColorsWithSound); + +#ifdef Q_OS_MACOS + if (qEnvironmentVariable("QTEST_ENVIRONMENT").toLower() == "ci") + QSKIP("The test accidently gets crashed on macOS CI, not reproduced locally. To be " + "investigated: QTBUG-111744"); +#endif + + QCOMPARE(m_fixture->player.loops(), 1); + m_fixture->player.setLoops(3); + QCOMPARE(m_fixture->player.loops(), 3); + + m_fixture->player.setSource(*m_localVideoFile3ColorsWithSound); + m_fixture->player.setPlaybackRate(5); + QCOMPARE(m_fixture->player.loops(), 3); + + m_fixture->player.play(); + m_fixture->surface.waitForFrame(); + + // check pause doesn't affect looping + { + QTest::qWait(static_cast<int>(m_fixture->player.duration() * 3 + * 0.6 /*relative pos*/ / m_fixture->player.playbackRate())); + m_fixture->player.pause(); + m_fixture->player.play(); + } + + QTRY_COMPARE(m_fixture->player.playbackState(), QMediaPlayer::StoppedState); + + // Check for expected number of loop iterations and startPos, endPos and posCount per iteration + std::vector<LoopIteration> iterations = loopIterations(m_fixture->positionChanged); + QCOMPARE(iterations.size(), 3u); + QCOMPARE_GT(iterations[0].startPos, 0); + QCOMPARE(iterations[0].endPos, m_fixture->player.duration()); + QCOMPARE(iterations[1].startPos, 0); + QCOMPARE(iterations[1].endPos, m_fixture->player.duration()); + QCOMPARE(iterations[2].startPos, 0); + QCOMPARE(iterations[2].endPos, m_fixture->player.duration()); + if (isFFMPEGPlatform()) { + QCOMPARE_GT(iterations[0].posCount, 10); + QCOMPARE_GT(iterations[1].posCount, 10); + QCOMPARE_GT(iterations[2].posCount, 10); + } + + QCOMPARE(m_fixture->player.mediaStatus(), QMediaPlayer::EndOfMedia); + + // Check that loop counter is reset when playback is restarted. + { + m_fixture->positionChanged.clear(); + m_fixture->player.play(); + m_fixture->player.setPlaybackRate(10); + m_fixture->surface.waitForFrame(); + + QTRY_COMPARE(m_fixture->player.playbackState(), QMediaPlayer::StoppedState); + QCOMPARE(loopIterations(m_fixture->positionChanged).size(), 3u); + QCOMPARE(m_fixture->player.mediaStatus(), QMediaPlayer::EndOfMedia); } } -bool TestVideoSurface::start(const QVideoSurfaceFormat &format) +void tst_QMediaPlayerBackend::infiniteLoops() { - if (!isFormatSupported(format)) { - setError(UnsupportedFormatError); - return false; + QSKIP_GSTREAMER("QTBUG-123056(?): spuriously failures of the gstreamer backend"); + + CHECK_SELECTED_URL(m_localVideoFile2); + +#ifdef Q_OS_MACOS + if (qEnvironmentVariable("QTEST_ENVIRONMENT").toLower() == "ci") + QSKIP("The test accidently gets crashed on macOS CI, not reproduced locally. To be " + "investigated: QTBUG-111744"); +#endif + + m_fixture->player.setLoops(QMediaPlayer::Infinite); + QCOMPARE(m_fixture->player.loops(), QMediaPlayer::Infinite); + + // select some small file + m_fixture->player.setSource(*m_localVideoFile2); + m_fixture->player.setPlaybackRate(20); + + m_fixture->player.play(); + m_fixture->surface.waitForFrame(); + + for (int i = 0; i < 2; ++i) { + m_fixture->positionChanged.clear(); + + QTest::qWait( + std::max(static_cast<int>(m_fixture->player.duration() + / m_fixture->player.playbackRate() * 4), + 300 /*ensure some minimum waiting time to reduce threading flakiness*/)); + QVERIFY(m_fixture->player.mediaStatus() == QMediaPlayer::BufferingMedia + || m_fixture->player.mediaStatus() == QMediaPlayer::BufferedMedia); + QCOMPARE(m_fixture->player.playbackState(), QMediaPlayer::PlayingState); + + const auto iterations = loopIterations(m_fixture->positionChanged); + QVERIFY(!iterations.empty()); + QCOMPARE(iterations.front().endPos, m_fixture->player.duration()); } - return QAbstractVideoSurface::start(format); + QTRY_VERIFY(m_fixture->player.mediaStatus() == QMediaPlayer::BufferedMedia + || m_fixture->player.mediaStatus() == QMediaPlayer::EndOfMedia); + + m_fixture->player.stop(); // QMediaPlayer::stop stops whether or not looping is infinite + QCOMPARE(m_fixture->player.playbackState(), QMediaPlayer::StoppedState); + + if (isGStreamerPlatform()) + return; // QTBUG-124005: many StalledMedia/BufferingMedia/BufferedMedia signals are emitted + QCOMPARE(m_fixture->mediaStatusChanged, + SignalList({ { QMediaPlayer::LoadingMedia }, + { QMediaPlayer::LoadedMedia }, + { QMediaPlayer::BufferingMedia }, + { QMediaPlayer::BufferedMedia }, + { QMediaPlayer::LoadedMedia } })); +} + +void tst_QMediaPlayerBackend::seekOnLoops() +{ + QSKIP_GSTREAMER("QTBUG-123056(?): spuriously failures of the gstreamer backend"); + + CHECK_SELECTED_URL(m_localVideoFile3ColorsWithSound); + +#ifdef Q_OS_MACOS + if (qEnvironmentVariable("QTEST_ENVIRONMENT").toLower() == "ci") + QSKIP("The test accidently gets crashed on macOS CI, not reproduced locally. To be " + "investigated: QTBUG-111744"); +#endif + + m_fixture->player.setLoops(3); + m_fixture->player.setPlaybackRate(2); + + m_fixture->player.setSource(*m_localVideoFile3ColorsWithSound); + + m_fixture->player.play(); + m_fixture->surface.waitForFrame(); + + // seek in the 1st loop + m_fixture->player.setPosition(m_fixture->player.duration() * 4 / 5); + + // wait for the 2nd loop and seek + m_fixture->surface.waitForFrame(); + QTRY_VERIFY(m_fixture->player.position() < m_fixture->player.duration() / 2); + m_fixture->player.setPosition(m_fixture->player.duration() * 8 / 9); + + // wait for the 3rd loop and seek + m_fixture->surface.waitForFrame(); + QTRY_VERIFY(m_fixture->player.position() < m_fixture->player.duration() / 2); + m_fixture->player.setPosition(m_fixture->player.duration() * 4 / 5); + + QTRY_COMPARE(m_fixture->player.playbackState(), QMediaPlayer::StoppedState); + + auto iterations = loopIterations(m_fixture->positionChanged); + + QCOMPARE(iterations.size(), 3u); + QCOMPARE_GT(iterations[0].startPos, 0); + QCOMPARE(iterations[0].endPos, m_fixture->player.duration()); + QCOMPARE_GT(iterations[0].posCount, 2); + QCOMPARE(iterations[1].startPos, 0); + QCOMPARE(iterations[1].endPos, m_fixture->player.duration()); + QCOMPARE_GT(iterations[1].posCount, 2); + QCOMPARE(iterations[2].startPos, 0); + QCOMPARE(iterations[2].endPos, m_fixture->player.duration()); + QCOMPARE_GT(iterations[2].posCount, 2); + + QCOMPARE(m_fixture->player.mediaStatus(), QMediaPlayer::EndOfMedia); } -void TestVideoSurface::stop() +void tst_QMediaPlayerBackend::changeLoopsOnTheFly() { - QAbstractVideoSurface::stop(); + QSKIP_GSTREAMER("QTBUG-123056(?): spuriously failures of the gstreamer backend"); + + CHECK_SELECTED_URL(m_localVideoFile3ColorsWithSound); + +#ifdef Q_OS_MACOS + if (qEnvironmentVariable("QTEST_ENVIRONMENT").toLower() == "ci") + QSKIP("The test accidently gets crashed on macOS CI, not reproduced locally. To be " + "investigated: QTBUG-111744"); +#endif + + m_fixture->player.setLoops(4); + m_fixture->player.setPlaybackRate(5); + + m_fixture->player.setSource(*m_localVideoFile3ColorsWithSound); + + m_fixture->player.play(); + m_fixture->surface.waitForFrame(); + + m_fixture->player.setPosition(m_fixture->player.duration() * 4 / 5); + + // wait for the 2nd loop + m_fixture->surface.waitForFrame(); + QTRY_VERIFY(m_fixture->player.position() < m_fixture->player.duration() / 2); + m_fixture->player.setPosition(m_fixture->player.duration() * 8 / 9); + + m_fixture->player.setLoops(1); + + QTRY_COMPARE(m_fixture->player.playbackState(), QMediaPlayer::StoppedState); + QCOMPARE(m_fixture->player.mediaStatus(), QMediaPlayer::EndOfMedia); + + auto iterations = loopIterations(m_fixture->positionChanged); + QCOMPARE(iterations.size(), 2u); + + QCOMPARE(iterations[1].startPos, 0); + QCOMPARE(iterations[1].endPos, m_fixture->player.duration()); + QCOMPARE_GT(iterations[1].posCount, 2); } -bool TestVideoSurface::present(const QVideoFrame &frame) +void tst_QMediaPlayerBackend::seekAfterLoopReset() { - if (m_storeFrames) - m_frameList.push_back(frame); - m_totalFrames++; - return true; + CHECK_SELECTED_URL(m_localVideoFile3ColorsWithSound); + +#ifdef Q_OS_MACOS + if (qEnvironmentVariable("QTEST_ENVIRONMENT").toLower() == "ci") + QSKIP("The test accidently gets crashed on macOS CI, not reproduced locally. To be " + "investigated: QTBUG-111744"); +#endif + + m_fixture->surface.setStoreFrames(false); + + m_fixture->player.setLoops(QMediaPlayer::Infinite); + m_fixture->player.setPlaybackRate(2); + + m_fixture->player.setSource(*m_localVideoFile3ColorsWithSound); + + m_fixture->player.play(); + m_fixture->surface.waitForFrame(); + + // seek in the 1st loop + m_fixture->player.setPosition(m_fixture->player.duration() * 4 / 5); + + // wait for the 2nd loop + m_fixture->surface.waitForFrame(); + QTRY_VERIFY(m_fixture->player.position() < m_fixture->player.duration() / 2); + + // reset loops and seek + m_fixture->player.setLoops(1); + m_fixture->player.setPosition(m_fixture->player.duration() * 8 / 9); + + QTRY_COMPARE(m_fixture->player.playbackState(), QMediaPlayer::StoppedState); + QCOMPARE(m_fixture->player.mediaStatus(), QMediaPlayer::EndOfMedia); } +void tst_QMediaPlayerBackend::changeVideoOutputNoFramesLost() +{ + QSKIP_GSTREAMER("QTBUG-124005: gstreamer will lose frames, possibly due to buffering"); -void ProbeDataHandler::processFrame(const QVideoFrame &frame) + CHECK_SELECTED_URL(m_localVideoFile3ColorsWithSound); + + QVideoSink sinks[4]; + std::atomic_int framesCount[4] = { + 0, + }; + for (int i = 0; i < 4; ++i) + setVideoSinkAsyncFramesCounter(sinks[i], framesCount[i]); + + QMediaPlayer player; + + player.setPlaybackRate(10); + + player.setVideoOutput(&sinks[0]); + player.setSource(*m_localVideoFile3ColorsWithSound); + player.play(); + QTRY_VERIFY(!player.isPlaying()); + + player.setPlaybackRate(4); + player.setVideoOutput(&sinks[1]); + player.play(); + + QTRY_COMPARE_GE(framesCount[1], framesCount[0] / 4); + player.setVideoOutput(&sinks[2]); + const int savedFrameNumber1 = framesCount[1]; + + QTRY_COMPARE_GE(framesCount[2], (framesCount[0] - savedFrameNumber1) / 2); + player.setVideoOutput(&sinks[3]); + const int savedFrameNumber2 = framesCount[2]; + + QTRY_VERIFY(!player.isPlaying()); + + // check if no frames sent to old sinks + QCOMPARE(framesCount[1], savedFrameNumber1); + QCOMPARE(framesCount[2], savedFrameNumber2); + + // no frames lost + QCOMPARE(framesCount[1] + framesCount[2] + framesCount[3], framesCount[0]); +} + +void tst_QMediaPlayerBackend::cleanSinkAndNoMoreFramesAfterStop() { - m_frameList.append(frame); + QSKIP_GSTREAMER( + "QTBUG-124005: spurious failures on gstreamer, probably due to asynchronous play()"); + + CHECK_SELECTED_URL(m_localVideoFile3ColorsWithSound); + + QVideoSink sink; + std::atomic_int framesCount = 0; + setVideoSinkAsyncFramesCounter(sink, framesCount); + QMediaPlayer player; + + player.setPlaybackRate(10); + player.setVideoOutput(&sink); + + player.setSource(*m_localVideoFile3ColorsWithSound); + + // Run a few time to have more chances to detect race conditions + for (int i = 0; i < 8; ++i) { + player.play(); + QTRY_VERIFY(framesCount > 0); + QVERIFY(sink.videoFrame().isValid()); + + player.stop(); + + if (isGStreamerPlatform()) + // QTBUG-124005: stop() is asynchronous in gstreamer + QTRY_VERIFY(!sink.videoFrame().isValid()); + else + QVERIFY(!sink.videoFrame().isValid()); + + QCOMPARE_NE(framesCount, 0); + framesCount = 0; + + QTest::qWait(30); + + if (isGStreamerPlatform()) + continue; // QTBUG-124005: stop() is asynchronous in gstreamer + + // check if nothing changed after short waiting + QCOMPARE(framesCount, 0); + } +} + +void tst_QMediaPlayerBackend::lazyLoadVideo() +{ + QQmlEngine engine; + QQmlComponent component(&engine); + component.loadUrl(QUrl("qrc:/LazyLoad.qml")); + QScopedPointer<QObject> root(component.create()); + QQuickItem *rootItem = qobject_cast<QQuickItem *>(root.get()); + QVERIFY(rootItem); + + QQuickView view; + rootItem->setParentItem(view.contentItem()); + view.resize(600, 800); + view.show(); + + QQuickLoader *loader = qobject_cast<QQuickLoader *>(rootItem->findChild<QQuickItem *>("loader")); + QVERIFY(loader); + QCOMPARE(QQmlProperty::read(loader, "active").toBool(), false); + loader->setProperty("active", true); + QCOMPARE(QQmlProperty::read(loader, "active").toBool(), true); + + QQuickItem *videoPlayer = qobject_cast<QQuickItem *>(loader->findChild<QQuickItem *>("videoPlayer")); + QVERIFY(videoPlayer); + + QTRY_COMPARE_EQ(QQmlProperty::read(videoPlayer, "playbackState").value<QMediaPlayer::PlaybackState>(), QMediaPlayer::PlayingState); + QCOMPARE(QQmlProperty::read(videoPlayer, "error").value<QMediaPlayer::Error>(), QMediaPlayer::NoError); + + QVideoSink *videoSink = QQmlProperty::read(videoPlayer, "videoSink").value<QVideoSink *>(); + QVERIFY(videoSink); + + QSignalSpy spy(videoSink, &QVideoSink::videoFrameChanged); + QVERIFY(spy.wait()); + + QVideoFrame frame = spy.at(0).at(0).value<QVideoFrame>(); + QVERIFY(frame.isValid()); +} + +void tst_QMediaPlayerBackend::videoSinkSignals() +{ + std::atomic<int> videoFrameCounter = 0; + std::atomic<int> videoSizeCounter = 0; + + // TODO: come up with custom frames source, + // create the test target tst_QVideoSinkBackend, + // and move the test there + + CHECK_SELECTED_URL(m_localVideoFile2); + + QVideoSink sink; + QMediaPlayer player; + player.setVideoSink(&sink); + + player.setSource(*m_localVideoFile2); + + QTRY_COMPARE(player.mediaStatus(), QMediaPlayer::MediaStatus::LoadedMedia); + + sink.platformVideoSink()->setNativeSize({}); // reset size to be able to check the size update + + connect(&sink, &QVideoSink::videoFrameChanged, this, [&](const QVideoFrame &frame) { + QCOMPARE(sink.videoFrame(), frame); + QCOMPARE(sink.videoSize(), frame.size()); + ++videoFrameCounter; + }, Qt::DirectConnection); + + connect(&sink, &QVideoSink::videoSizeChanged, this, [&]() { + QCOMPARE(sink.videoSize(), sink.videoFrame().size()); + if (sink.videoSize().isValid()) // filter end frame + ++videoSizeCounter; + }, Qt::DirectConnection); + + player.play(); + + QTRY_COMPARE_GE(videoFrameCounter, 2); + QCOMPARE(videoSizeCounter, 1); +} + +void tst_QMediaPlayerBackend::nonAsciiFileName() +{ + CHECK_SELECTED_URL(m_localWavFile); + + auto temporaryFile = + copyResourceToTemporaryFile(":/testdata/test.wav", "äöüØøÆä¸æ–‡.XXXXXX.wav"); + QVERIFY(temporaryFile); + + m_fixture->player.setSource(temporaryFile->fileName()); + m_fixture->player.play(); + + QTRY_VERIFY(m_fixture->player.mediaStatus() == QMediaPlayer::BufferedMedia + || m_fixture->player.mediaStatus() == QMediaPlayer::EndOfMedia); + + QCOMPARE(m_fixture->errorOccurred.size(), 0); } -void ProbeDataHandler::processBuffer(const QAudioBuffer &buffer) +void tst_QMediaPlayerBackend::setMedia_setsVideoSinkSize_beforePlaying() { - m_bufferList.append(buffer); + CHECK_SELECTED_URL(m_localVideoFile3ColorsWithSound); + + QVideoSink sink1; + QVideoSink sink2; + QMediaPlayer player; + + QSignalSpy spy1(&sink1, &QVideoSink::videoSizeChanged); + QSignalSpy spy2(&sink2, &QVideoSink::videoSizeChanged); + + player.setVideoOutput(&sink1); + QCOMPARE(sink1.videoSize(), QSize()); + + player.setSource(*m_localVideoFile3ColorsWithSound); + + QTRY_COMPARE(player.mediaStatus(), QMediaPlayer::MediaStatus::LoadedMedia); + + QCOMPARE(sink1.videoSize(), QSize(684, 384)); + + player.setVideoOutput(&sink2); + QCOMPARE(sink2.videoSize(), QSize(684, 384)); + + QCOMPARE(spy1.size(), 1); + QCOMPARE(spy2.size(), 1); } -void ProbeDataHandler::flushVideo() +#if QT_CONFIG(process) +std::unique_ptr<QProcess> tst_QMediaPlayerBackend::createRtpStreamProcess(QString fileName, + QString sdpUrl) { - isVideoFlushCalled = true; + Q_ASSERT(!m_vlcCommand.isEmpty()); + + auto process = std::make_unique<QProcess>(); +#if defined(Q_OS_WINDOWS) + fileName.replace('/', '\\'); +#endif + + QStringList vlcParams = { "-vvv", fileName, + "--sout", QStringLiteral("#rtp{dst=localhost,sdp=%1}").arg(sdpUrl), + "--intf", "dummy" }; + + process->start(m_vlcCommand, vlcParams); + if (!process->waitForStarted()) + return nullptr; + + // rtp stream might be with started some delay after the vlc process starts. + // Ideally, we should wait for open connections, it requires some extra work + QNetwork + // dependency. + int timeout = 500; +#ifdef Q_OS_MACOS + timeout = 2000; +#endif + QTest::qWait(timeout); + + return process; } +#endif //QT_CONFIG(process) -void ProbeDataHandler::flushAudio() +void tst_QMediaPlayerBackend::play_playsRotatedVideoOutput_whenVideoFileHasOrientationMetadata_data() { + QTest::addColumn<MaybeUrl>("fileURL"); + QTest::addColumn<QRgb>("expectedColor"); + QTest::addColumn<QtVideo::Rotation>("expectedRotationAngle"); + QTest::addColumn<QSize>("videoSize"); + + // clang-format off + QTest::addRow("without rotation") << m_colorMatrixVideo + << QRgb(0xff0000) + << QtVideo::Rotation::None + << QSize(960, 540); + + QTest::addRow("90 deg clockwise") << m_colorMatrix90degClockwiseVideo + << QRgb(0x0000FF) + << QtVideo::Rotation::Clockwise90 + << QSize(540, 960); + + QTest::addRow("180 deg clockwise") << m_colorMatrix180degClockwiseVideo + << QRgb(0xFFFF00) + << QtVideo::Rotation::Clockwise180 + << QSize(960, 540); + + QTest::addRow("270 deg clockwise") << m_colorMatrix270degClockwiseVideo + << QRgb(0x00FF00) + << QtVideo::Rotation::Clockwise270 + << QSize(540, 960); + // clang-format on +} +void tst_QMediaPlayerBackend::play_playsRotatedVideoOutput_whenVideoFileHasOrientationMetadata() +{ + if (isGStreamerPlatform() && isCI()) + QSKIP("QTBUG-124005: Fails with gstreamer on CI"); + + // This test uses 4 video files with a 2x2 color matrix consisting of + // red (upper left), blue (lower left), yellow (lower right) and green (upper right). + // The files are identical, except that three of them contain + // orientation (rotation) metadata specifying that they should be + // viewed with a 90, 180 and 270 degree clockwise rotation respectively. + + // Fetch path and expected color of upper left area of each file + QFETCH(const MaybeUrl, fileURL); + QFETCH(const QRgb, expectedColor); + QFETCH(const QtVideo::Rotation, expectedRotationAngle); + QFETCH(const QSize, videoSize); + + CHECK_SELECTED_URL(fileURL); + + // Load video file + m_fixture->player.setSource(*fileURL); + QTRY_COMPARE(m_fixture->player.mediaStatus(), QMediaPlayer::LoadedMedia); + + // Compare videoSize of the output video sink with the expected value before starting playing + QCOMPARE(m_fixture->surface.videoSize(), videoSize); + + // Compare orientation metadata of QMediaPlayer with expected value + const auto metaData = m_fixture->player.metaData(); + const auto playerOrientation = metaData.value(QMediaMetaData::Orientation).value<QtVideo::Rotation>(); + QCOMPARE(playerOrientation, expectedRotationAngle); + + // Compare orientation metadata of active video stream with expected value + const int activeVideoTrack = m_fixture->player.activeVideoTrack(); + const auto videoTrackMetaData = m_fixture->player.videoTracks().at(activeVideoTrack); + const auto videoTrackOrientation = videoTrackMetaData.value(QMediaMetaData::Orientation).value<QtVideo::Rotation>(); + QCOMPARE(videoTrackOrientation, expectedRotationAngle); + + // Play video file, sample upper left area, compare with expected color + m_fixture->player.play(); + QTRY_COMPARE(m_fixture->player.playbackState(), QMediaPlayer::PlayingState); + QVideoFrame videoFrame = m_fixture->surface.waitForFrame(); + QVERIFY(videoFrame.isValid()); + QCOMPARE(videoFrame.rotation(), expectedRotationAngle); + QImage image = videoFrame.toImage(); + QVERIFY(!image.isNull()); + QRgb upperLeftColor = image.pixel(5, 5); + QCOMPARE_LT(colorDifference(upperLeftColor, expectedColor), 0.004); + + QSKIP_GSTREAMER("QTBUG-124005: surface.videoSize() not updated with rotation"); + + // Compare videoSize of the output video sink with the expected value after getting a frame + QCOMPARE(m_fixture->surface.videoSize(), videoSize); } QTEST_MAIN(tst_QMediaPlayerBackend) diff --git a/tests/auto/integration/qmediaplayerformatsupport/CMakeLists.txt b/tests/auto/integration/qmediaplayerformatsupport/CMakeLists.txt new file mode 100644 index 000000000..b4dd19f75 --- /dev/null +++ b/tests/auto/integration/qmediaplayerformatsupport/CMakeLists.txt @@ -0,0 +1,30 @@ +# Copyright (C) 2024 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +file(GLOB_RECURSE test_data_glob + RELATIVE + ${CMAKE_CURRENT_SOURCE_DIR} + testdata/** +) + +list(APPEND testdata_resource_files ${test_data_glob}) + +qt_internal_add_test(tst_qmediaplayerformatsupport + SOURCES + tst_qmediaplayerformatsupport.cpp + ../shared/mediabackendutils.h + INCLUDE_DIRECTORIES + ../shared/ + LIBRARIES + Qt::Core + Qt::MultimediaPrivate + TESTDATA + ${testdata_resource_files} +) + +qt_internal_add_resource(tst_qmediaplayerformatsupport "testdata" + PREFIX + "/" + FILES + ${testdata_resource_files} +) diff --git a/tests/auto/integration/qmediaplayerformatsupport/testdata/README.md b/tests/auto/integration/qmediaplayerformatsupport/testdata/README.md new file mode 100644 index 000000000..f52108bcc --- /dev/null +++ b/tests/auto/integration/qmediaplayerformatsupport/testdata/README.md @@ -0,0 +1,35 @@ +Sample video files are created using FFmpeg, for example: + +:: Supported container formats +ffmpeg -ss 2 -i flipable.gif -t 1 -r 5 containers/supported/container.avi +ffmpeg -ss 2 -i flipable.gif -t 1 -r 5 containers/supported/container.mkv +ffmpeg -ss 2 -i flipable.gif -t 1 -r 5 containers/supported/container.mp4 +ffmpeg -ss 2 -i flipable.gif -t 1 -r 25 containers/supported/container.mpeg +ffmpeg -ss 2 -i flipable.gif -t 1 -r 25 containers/supported/container.wmv + +:: Unsupported container formats +ffmpeg -ss 2 -i flipable.gif -t 1 -r 5 containers/unsupported/container.webm + +:: Supported pixel formats h264 +ffmpeg -i flipable.gif -pix_fmt yuv420p -colorspace bt709 -color_primaries bt709 -color_trc bt709 -color_range tv -vcodec libx264 -r 5 pixel_formats/supported/h264_yuv420p.mp4 +ffmpeg -i flipable.gif -pix_fmt yuv420p10 -colorspace bt709 -color_primaries bt709 -color_trc bt709 -color_range tv -vcodec libx264 -r 5 pixel_formats/supported/h264_yuv420p10.mp4 +ffmpeg -i flipable.gif -pix_fmt yuv422p -colorspace bt709 -color_primaries bt709 -color_trc bt709 -color_range tv -vcodec libx264 -r 5 pixel_formats/supported/h264_yuv422p.mp4 +ffmpeg -i flipable.gif -pix_fmt yuv422p10 -colorspace bt709 -color_primaries bt709 -color_trc bt709 -color_range tv -vcodec libx264 -r 5 pixel_formats/supported/h264_yuv422p10.mp4 +ffmpeg -i flipable.gif -pix_fmt yuv444p -colorspace bt709 -color_primaries bt709 -color_trc bt709 -color_range tv -vcodec libx264 -r 5 pixel_formats/supported/h264_yuv444p.mp4 +ffmpeg -i flipable.gif -pix_fmt yuvj420p -colorspace bt709 -color_primaries bt709 -color_trc bt709 -color_range tv -vcodec libx264 -r 5 pixel_formats/supported/h264_yuvj420p.mp4 +ffmpeg -i flipable.gif -pix_fmt yuvj422p -colorspace bt709 -color_primaries bt709 -color_trc bt709 -color_range tv -vcodec libx264 -r 5 pixel_formats/supported/h264_yuvj422p.mp4 +ffmpeg -i flipable.gif -pix_fmt yuvj444p -colorspace bt709 -color_primaries bt709 -color_trc bt709 -color_range tv -vcodec libx264 -r 5 pixel_formats/supported/h264_yuvj444p.mp4 +ffmpeg -i flipable.gif -pix_fmt nv12 -colorspace bt709 -color_primaries bt709 -color_trc bt709 -color_range tv -vcodec libx264 -r 5 pixel_formats/supported/h264_nv12.mp4 +ffmpeg -i flipable.gif -pix_fmt nv16 -colorspace bt709 -color_primaries bt709 -color_trc bt709 -color_range tv -vcodec libx264 -r 5 pixel_formats/supported/h264_nv16.mp4 +ffmpeg -i flipable.gif -pix_fmt nv21 -colorspace bt709 -color_primaries bt709 -color_trc bt709 -color_range tv -vcodec libx264 -r 5 pixel_formats/supported/h264_nv21.mp4 +ffmpeg -i flipable.gif -pix_fmt yuv420p10le -colorspace bt709 -color_primaries bt709 -color_trc bt709 -color_range tv -vcodec libx264 -r 5 pixel_formats/supported/h264_yuv420p10le.mp4 +ffmpeg -i flipable.gif -pix_fmt yuv444p10 -colorspace bt709 -color_primaries bt709 -color_trc bt709 -color_range tv -vcodec libx264 -r 5 pixel_formats/supported/h264_yuv444p10.mp4 +ffmpeg -i flipable.gif -pix_fmt yuv422p10le -colorspace bt709 -color_primaries bt709 -color_trc bt709 -color_range tv -vcodec libx264 -r 5 pixel_formats/supported/h264_yuv422p10le.mp4 +ffmpeg -i flipable.gif -pix_fmt gray -colorspace bt709 -color_primaries bt709 -color_trc bt709 -color_range tv -vcodec libx264 -r 5 pixel_formats/supported/h264_gray.mp4 +ffmpeg -i flipable.gif -pix_fmt gray10le -colorspace bt709 -color_primaries bt709 -color_trc bt709 -color_range tv -vcodec libx264 -r 5 pixel_formats/supported/h264_gray10le.mp4 + +ffmpeg -i flipable.gif -pix_fmt bgr0 -colorspace bt709 -color_primaries bt709 -color_trc bt709 -color_range tv -vcodec libx264rgb -r 5 pixel_formats/supported/h264_bgr0.mp4 +ffmpeg -i flipable.gif -pix_fmt bgr24 -colorspace bt709 -color_primaries bt709 -color_trc bt709 -color_range tv -vcodec libx264rgb -r 5 pixel_formats/supported/h264_bgr24.mp4 +ffmpeg -i flipable.gif -pix_fmt rgb24 -colorspace bt709 -color_primaries bt709 -color_trc bt709 -color_range tv -vcodec libx264rgb -r 5 pixel_formats/supported/h264_rgb24.mp4 + + diff --git a/tests/auto/integration/qmediaplayerformatsupport/testdata/containers/supported/container.avi b/tests/auto/integration/qmediaplayerformatsupport/testdata/containers/supported/container.avi Binary files differnew file mode 100644 index 000000000..a48028550 --- /dev/null +++ b/tests/auto/integration/qmediaplayerformatsupport/testdata/containers/supported/container.avi diff --git a/tests/auto/integration/qmediaplayerformatsupport/testdata/containers/supported/container.mkv b/tests/auto/integration/qmediaplayerformatsupport/testdata/containers/supported/container.mkv Binary files differnew file mode 100644 index 000000000..2e362d7ca --- /dev/null +++ b/tests/auto/integration/qmediaplayerformatsupport/testdata/containers/supported/container.mkv diff --git a/tests/auto/integration/qmediaplayerformatsupport/testdata/containers/supported/container.mp4 b/tests/auto/integration/qmediaplayerformatsupport/testdata/containers/supported/container.mp4 Binary files differnew file mode 100644 index 000000000..bff40278d --- /dev/null +++ b/tests/auto/integration/qmediaplayerformatsupport/testdata/containers/supported/container.mp4 diff --git a/tests/auto/integration/qmediaplayerformatsupport/testdata/containers/supported/container.mpeg b/tests/auto/integration/qmediaplayerformatsupport/testdata/containers/supported/container.mpeg Binary files differnew file mode 100644 index 000000000..b94a47f0a --- /dev/null +++ b/tests/auto/integration/qmediaplayerformatsupport/testdata/containers/supported/container.mpeg diff --git a/tests/auto/integration/qmediaplayerformatsupport/testdata/containers/supported/container.wmv b/tests/auto/integration/qmediaplayerformatsupport/testdata/containers/supported/container.wmv Binary files differnew file mode 100644 index 000000000..1a7577e3f --- /dev/null +++ b/tests/auto/integration/qmediaplayerformatsupport/testdata/containers/supported/container.wmv diff --git a/tests/auto/integration/qmediaplayerformatsupport/testdata/containers/unsupported/container.webp b/tests/auto/integration/qmediaplayerformatsupport/testdata/containers/unsupported/container.webp Binary files differnew file mode 100644 index 000000000..ed8b6f631 --- /dev/null +++ b/tests/auto/integration/qmediaplayerformatsupport/testdata/containers/unsupported/container.webp diff --git a/tests/auto/integration/qmediaplayerformatsupport/testdata/flipable.gif b/tests/auto/integration/qmediaplayerformatsupport/testdata/flipable.gif Binary files differnew file mode 100644 index 000000000..fd187906b --- /dev/null +++ b/tests/auto/integration/qmediaplayerformatsupport/testdata/flipable.gif diff --git a/tests/auto/integration/qmediaplayerformatsupport/testdata/pixel_formats/supported/h264_bgr0.mp4 b/tests/auto/integration/qmediaplayerformatsupport/testdata/pixel_formats/supported/h264_bgr0.mp4 Binary files differnew file mode 100644 index 000000000..ff9eacb65 --- /dev/null +++ b/tests/auto/integration/qmediaplayerformatsupport/testdata/pixel_formats/supported/h264_bgr0.mp4 diff --git a/tests/auto/integration/qmediaplayerformatsupport/testdata/pixel_formats/supported/h264_bgr24.mp4 b/tests/auto/integration/qmediaplayerformatsupport/testdata/pixel_formats/supported/h264_bgr24.mp4 Binary files differnew file mode 100644 index 000000000..ff9eacb65 --- /dev/null +++ b/tests/auto/integration/qmediaplayerformatsupport/testdata/pixel_formats/supported/h264_bgr24.mp4 diff --git a/tests/auto/integration/qmediaplayerformatsupport/testdata/pixel_formats/supported/h264_gray.mp4 b/tests/auto/integration/qmediaplayerformatsupport/testdata/pixel_formats/supported/h264_gray.mp4 Binary files differnew file mode 100644 index 000000000..c69357d2b --- /dev/null +++ b/tests/auto/integration/qmediaplayerformatsupport/testdata/pixel_formats/supported/h264_gray.mp4 diff --git a/tests/auto/integration/qmediaplayerformatsupport/testdata/pixel_formats/supported/h264_gray10le.mp4 b/tests/auto/integration/qmediaplayerformatsupport/testdata/pixel_formats/supported/h264_gray10le.mp4 Binary files differnew file mode 100644 index 000000000..339912757 --- /dev/null +++ b/tests/auto/integration/qmediaplayerformatsupport/testdata/pixel_formats/supported/h264_gray10le.mp4 diff --git a/tests/auto/integration/qmediaplayerformatsupport/testdata/pixel_formats/supported/h264_nv12.mp4 b/tests/auto/integration/qmediaplayerformatsupport/testdata/pixel_formats/supported/h264_nv12.mp4 Binary files differnew file mode 100644 index 000000000..20b51609a --- /dev/null +++ b/tests/auto/integration/qmediaplayerformatsupport/testdata/pixel_formats/supported/h264_nv12.mp4 diff --git a/tests/auto/integration/qmediaplayerformatsupport/testdata/pixel_formats/supported/h264_nv16.mp4 b/tests/auto/integration/qmediaplayerformatsupport/testdata/pixel_formats/supported/h264_nv16.mp4 Binary files differnew file mode 100644 index 000000000..165819b22 --- /dev/null +++ b/tests/auto/integration/qmediaplayerformatsupport/testdata/pixel_formats/supported/h264_nv16.mp4 diff --git a/tests/auto/integration/qmediaplayerformatsupport/testdata/pixel_formats/supported/h264_nv21.mp4 b/tests/auto/integration/qmediaplayerformatsupport/testdata/pixel_formats/supported/h264_nv21.mp4 Binary files differnew file mode 100644 index 000000000..20b51609a --- /dev/null +++ b/tests/auto/integration/qmediaplayerformatsupport/testdata/pixel_formats/supported/h264_nv21.mp4 diff --git a/tests/auto/integration/qmediaplayerformatsupport/testdata/pixel_formats/supported/h264_rgb24.mp4 b/tests/auto/integration/qmediaplayerformatsupport/testdata/pixel_formats/supported/h264_rgb24.mp4 Binary files differnew file mode 100644 index 000000000..ff9eacb65 --- /dev/null +++ b/tests/auto/integration/qmediaplayerformatsupport/testdata/pixel_formats/supported/h264_rgb24.mp4 diff --git a/tests/auto/integration/qmediaplayerformatsupport/testdata/pixel_formats/supported/h264_yuv420p.mp4 b/tests/auto/integration/qmediaplayerformatsupport/testdata/pixel_formats/supported/h264_yuv420p.mp4 Binary files differnew file mode 100644 index 000000000..53ef62b45 --- /dev/null +++ b/tests/auto/integration/qmediaplayerformatsupport/testdata/pixel_formats/supported/h264_yuv420p.mp4 diff --git a/tests/auto/integration/qmediaplayerformatsupport/testdata/pixel_formats/supported/h264_yuv420p10.mp4 b/tests/auto/integration/qmediaplayerformatsupport/testdata/pixel_formats/supported/h264_yuv420p10.mp4 Binary files differnew file mode 100644 index 000000000..bb45d344c --- /dev/null +++ b/tests/auto/integration/qmediaplayerformatsupport/testdata/pixel_formats/supported/h264_yuv420p10.mp4 diff --git a/tests/auto/integration/qmediaplayerformatsupport/testdata/pixel_formats/supported/h264_yuv420p10le.mp4 b/tests/auto/integration/qmediaplayerformatsupport/testdata/pixel_formats/supported/h264_yuv420p10le.mp4 Binary files differnew file mode 100644 index 000000000..bb45d344c --- /dev/null +++ b/tests/auto/integration/qmediaplayerformatsupport/testdata/pixel_formats/supported/h264_yuv420p10le.mp4 diff --git a/tests/auto/integration/qmediaplayerformatsupport/testdata/pixel_formats/supported/h264_yuv422p.mp4 b/tests/auto/integration/qmediaplayerformatsupport/testdata/pixel_formats/supported/h264_yuv422p.mp4 Binary files differnew file mode 100644 index 000000000..165819b22 --- /dev/null +++ b/tests/auto/integration/qmediaplayerformatsupport/testdata/pixel_formats/supported/h264_yuv422p.mp4 diff --git a/tests/auto/integration/qmediaplayerformatsupport/testdata/pixel_formats/supported/h264_yuv422p10.mp4 b/tests/auto/integration/qmediaplayerformatsupport/testdata/pixel_formats/supported/h264_yuv422p10.mp4 Binary files differnew file mode 100644 index 000000000..563fce6da --- /dev/null +++ b/tests/auto/integration/qmediaplayerformatsupport/testdata/pixel_formats/supported/h264_yuv422p10.mp4 diff --git a/tests/auto/integration/qmediaplayerformatsupport/testdata/pixel_formats/supported/h264_yuv422p10le.mp4 b/tests/auto/integration/qmediaplayerformatsupport/testdata/pixel_formats/supported/h264_yuv422p10le.mp4 Binary files differnew file mode 100644 index 000000000..563fce6da --- /dev/null +++ b/tests/auto/integration/qmediaplayerformatsupport/testdata/pixel_formats/supported/h264_yuv422p10le.mp4 diff --git a/tests/auto/integration/qmediaplayerformatsupport/testdata/pixel_formats/supported/h264_yuv444p.mp4 b/tests/auto/integration/qmediaplayerformatsupport/testdata/pixel_formats/supported/h264_yuv444p.mp4 Binary files differnew file mode 100644 index 000000000..953d014ec --- /dev/null +++ b/tests/auto/integration/qmediaplayerformatsupport/testdata/pixel_formats/supported/h264_yuv444p.mp4 diff --git a/tests/auto/integration/qmediaplayerformatsupport/testdata/pixel_formats/supported/h264_yuv444p10.mp4 b/tests/auto/integration/qmediaplayerformatsupport/testdata/pixel_formats/supported/h264_yuv444p10.mp4 Binary files differnew file mode 100644 index 000000000..c27683ed7 --- /dev/null +++ b/tests/auto/integration/qmediaplayerformatsupport/testdata/pixel_formats/supported/h264_yuv444p10.mp4 diff --git a/tests/auto/integration/qmediaplayerformatsupport/testdata/pixel_formats/supported/h264_yuvj420p.mp4 b/tests/auto/integration/qmediaplayerformatsupport/testdata/pixel_formats/supported/h264_yuvj420p.mp4 Binary files differnew file mode 100644 index 000000000..3906325e1 --- /dev/null +++ b/tests/auto/integration/qmediaplayerformatsupport/testdata/pixel_formats/supported/h264_yuvj420p.mp4 diff --git a/tests/auto/integration/qmediaplayerformatsupport/testdata/pixel_formats/supported/h264_yuvj422p.mp4 b/tests/auto/integration/qmediaplayerformatsupport/testdata/pixel_formats/supported/h264_yuvj422p.mp4 Binary files differnew file mode 100644 index 000000000..6b2d43fd9 --- /dev/null +++ b/tests/auto/integration/qmediaplayerformatsupport/testdata/pixel_formats/supported/h264_yuvj422p.mp4 diff --git a/tests/auto/integration/qmediaplayerformatsupport/testdata/pixel_formats/supported/h264_yuvj444p.mp4 b/tests/auto/integration/qmediaplayerformatsupport/testdata/pixel_formats/supported/h264_yuvj444p.mp4 Binary files differnew file mode 100644 index 000000000..75008f2aa --- /dev/null +++ b/tests/auto/integration/qmediaplayerformatsupport/testdata/pixel_formats/supported/h264_yuvj444p.mp4 diff --git a/tests/auto/integration/qmediaplayerformatsupport/tst_qmediaplayerformatsupport.cpp b/tests/auto/integration/qmediaplayerformatsupport/tst_qmediaplayerformatsupport.cpp new file mode 100644 index 000000000..8cabb28c8 --- /dev/null +++ b/tests/auto/integration/qmediaplayerformatsupport/tst_qmediaplayerformatsupport.cpp @@ -0,0 +1,124 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include "mediabackendutils.h" +#include <QtTest/QtTest> +#include <QDebug> +#include <QtMultimedia/qmediaplayer.h> +#include <QtMultimedia/QVideoSink> + +using namespace Qt::StringLiterals; + +QT_USE_NAMESPACE + +struct Fixture +{ + Fixture() { player.setVideoOutput(&videoOutput); } + + QVideoSink videoOutput; + QMediaPlayer player; + QSignalSpy errorOccurred{ &player, &QMediaPlayer::errorOccurred }; + QSignalSpy playbackStateChanged{ &player, &QMediaPlayer::playbackStateChanged }; + + bool startedPlaying() const + { + return playbackStateChanged.contains(QList<QVariant>{ QMediaPlayer::PlayingState }); + } +}; + +void addTestData(QLatin1StringView dir) +{ + QDirIterator it(dir); + while (it.hasNext()) { + QString v = it.next(); + QTest::addRow("%s", v.toLatin1().data()) << QUrl{ "qrc" + v }; + } +} + +class tst_qmediaplayerformatsupport : public QObject +{ + Q_OBJECT + +public slots: + void initTestCase() + { + if (!isFFMPEGPlatform()) + QSKIP("Test is only intended for FFmpeg backend"); + } + +private slots: + void play_succeeds_withSupportedContainer_data() + { + QTest::addColumn<QUrl>("url"); + addTestData(":testdata/containers/supported"_L1); + } + + void play_succeeds_withSupportedContainer() + { + QFETCH(const QUrl, url); + + Fixture f; + f.player.setSource(url); + f.player.play(); + + QTRY_VERIFY(f.startedPlaying()); + + // Log to understand failures in CI + for (const QList<QVariant> &err : f.errorOccurred) + qCritical() << "Unexpected failure detected:" << err[0] << "," << err[1]; + +#ifdef Q_OS_ANDROID + QSKIP("QTBUG-125613 Limited format support on Android 14"); +#endif + + QVERIFY(f.errorOccurred.empty()); + } + + void play_succeeds_withSupportedPixelFormats_data() + { + QTest::addColumn<QUrl>("url"); + addTestData(":testdata/pixel_formats/supported"_L1); + } + + void play_succeeds_withSupportedPixelFormats() + { + QFETCH(const QUrl, url); + + Fixture f; + f.player.setSource(url); + f.player.play(); + + QTRY_VERIFY(f.startedPlaying()); + + // Log to understand failures in CI + for (const QList<QVariant> &err : f.errorOccurred) + qCritical() << "Unexpected failure detected:" << err[0] << "," << err[1]; + +#ifdef Q_OS_ANDROID + QSKIP("QTBUG-125613 Limited format support on Android 14"); +#endif + + QVERIFY(f.errorOccurred.empty()); + } + + void play_fails_withUnsupportedContainer_data() + { + QTest::addColumn<QUrl>("url"); + addTestData(":testdata/containers/unsupported"_L1); + } + + void play_fails_withUnsupportedContainer() + { + QFETCH(const QUrl, url); + + Fixture f; + f.player.setSource(url); + f.player.play(); + + QTRY_COMPARE_NE(f.player.error(), QMediaPlayer::NoError); + } +}; + +QTEST_MAIN(tst_qmediaplayerformatsupport) + +#include "tst_qmediaplayerformatsupport.moc" diff --git a/tests/auto/integration/qml/CMakeLists.txt b/tests/auto/integration/qml/CMakeLists.txt new file mode 100644 index 000000000..1b7a1f686 --- /dev/null +++ b/tests/auto/integration/qml/CMakeLists.txt @@ -0,0 +1,23 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +# Generated from qml.pro. + +##################################################################### +## tst_qml Test: +##################################################################### + +qt_internal_add_test(tst_qml + GUI + QMLTEST + SOURCES + tst_qml.cpp + LIBRARIES + Qt::Gui +) + +#### Keys ignored in scope 1:.:.:qml.pro:<TRUE>: +# DEPLOYMENT = "importFiles" +# TEMPLATE = "app" +# importFiles.files = "soundeffect" +# importFiles.path = "." diff --git a/tests/auto/integration/qml/qml.pro b/tests/auto/integration/qml/qml.pro deleted file mode 100644 index c0015d9bd..000000000 --- a/tests/auto/integration/qml/qml.pro +++ /dev/null @@ -1,10 +0,0 @@ -TEMPLATE=app -TARGET=tst_qml -CONFIG += qmltestcase -SOURCES += tst_qml.cpp - - -importFiles.files = soundeffect - -importFiles.path = . -DEPLOYMENT += importFiles diff --git a/tests/auto/integration/qml/soundeffect/tst_soundeffect.qml b/tests/auto/integration/qml/soundeffect/tst_soundeffect.qml index 9100cd806..0b5867bce 100644 --- a/tests/auto/integration/qml/soundeffect/tst_soundeffect.qml +++ b/tests/auto/integration/qml/soundeffect/tst_soundeffect.qml @@ -1,33 +1,8 @@ -/**************************************************************************** -** -** Copyright (C) 2016 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the test suite of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:GPL-EXCEPT$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 3 as published by the Free Software -** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ - -import QtQuick 2.0 -import QtMultimedia 5.0 +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +import QtQuick +import QtMultimedia import QtTest 1.0 /* diff --git a/tests/auto/integration/qml/tst_qml.cpp b/tests/auto/integration/qml/tst_qml.cpp index 668285a31..2bdca1037 100644 --- a/tests/auto/integration/qml/tst_qml.cpp +++ b/tests/auto/integration/qml/tst_qml.cpp @@ -1,30 +1,5 @@ -/**************************************************************************** -** -** Copyright (C) 2016 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the test suite of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:GPL-EXCEPT$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 3 as published by the Free Software -** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #include <QtQuickTest/quicktest.h> QUICK_TEST_MAIN(qml) diff --git a/tests/auto/integration/qquickvideooutput/CMakeLists.txt b/tests/auto/integration/qquickvideooutput/CMakeLists.txt new file mode 100644 index 000000000..909416140 --- /dev/null +++ b/tests/auto/integration/qquickvideooutput/CMakeLists.txt @@ -0,0 +1,34 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +# Generated from qdeclarativevideooutput.pro. + +##################################################################### +## tst_qdeclarativevideooutput Test: +##################################################################### + +qt_internal_add_test(tst_qquickvideooutput + SOURCES + tst_qquickvideooutput.cpp + INCLUDE_DIRECTORIES + ../../../../src/imports/multimedia + LIBRARIES + Qt::Gui + Qt::MultimediaPrivate + Qt::MultimediaQuickPrivate + Qt::Qml + Qt::Quick +) + +# Resources: +set(qml_resource_files + "main.qml" +) + +qt_internal_add_resource(tst_qquickvideooutput "qml" + PREFIX + "/" + FILES + ${qml_resource_files} +) + diff --git a/tests/auto/integration/qdeclarativevideooutput/main.qml b/tests/auto/integration/qquickvideooutput/main.qml index e456adf6c..6893a6e9f 100644 --- a/tests/auto/integration/qdeclarativevideooutput/main.qml +++ b/tests/auto/integration/qquickvideooutput/main.qml @@ -1,5 +1,5 @@ -import QtQuick 2.0 -import QtMultimedia 5.0 +import QtQuick +import QtMultimedia VideoOutput { width: 150 diff --git a/tests/auto/integration/qquickvideooutput/tst_qquickvideooutput.cpp b/tests/auto/integration/qquickvideooutput/tst_qquickvideooutput.cpp new file mode 100644 index 000000000..85a30bbb7 --- /dev/null +++ b/tests/auto/integration/qquickvideooutput/tst_qquickvideooutput.cpp @@ -0,0 +1,392 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +//TESTED_COMPONENT=plugins/declarative/multimedia + +#include <QtTest/QtTest> + +#include <QtQml/qqmlengine.h> +#include <QtQml/qqmlcomponent.h> +#include <QQuickView> +#include <QVideoSink> +#include <QMediaPlayer> + +#include "private/qquickvideooutput_p.h" + +#include <qobject.h> +#include <qvideoframeformat.h> +#include <qvideoframe.h> + +void presentDummyFrame(QVideoSink *sink, const QSize &size) +{ + if (sink) { + QVideoFrameFormat format(size, QVideoFrameFormat::Format_ARGB8888_Premultiplied); + QVideoFrame frame(format); + + sink->setVideoFrame(frame); + + // Have to spin an event loop or two for the surfaceFormatChanged() signal + qApp->processEvents(); + } +} + +class tst_QQuickVideoOutput : public QObject +{ + Q_OBJECT +public: + tst_QQuickVideoOutput(); + + ~tst_QQuickVideoOutput() override + { + delete m_mappingOutput; + delete m_mappingComponent; + } + +public slots: + void initTestCase(); + +private slots: + void fillMode(); + void orientation(); + void surfaceSource(); + void paintSurface(); + void sourceRect(); + + void contentRect(); + void contentRect_data(); + +private: + QQmlEngine m_engine; + + // Variables used for the mapping test + QQmlComponent *m_mappingComponent = nullptr; + QQuickVideoOutput *m_mappingOutput = nullptr; + + void updateOutputGeometry(QObject *output); +}; + +void tst_QQuickVideoOutput::initTestCase() +{ + // We initialize the mapping vars here + m_mappingComponent = new QQmlComponent(&m_engine); + m_mappingComponent->loadUrl(QUrl("qrc:/main.qml")); + + auto *component = m_mappingComponent->create(); + QVERIFY(component != nullptr); + + m_mappingOutput = qobject_cast<QQuickVideoOutput *>(component); + QVERIFY(m_mappingOutput); + + presentDummyFrame(m_mappingOutput->videoSink(), QSize(200,100)); + updateOutputGeometry(m_mappingOutput); + // First make sure the component has processed the frame + QCOMPARE(m_mappingOutput->sourceRect(), QRectF(0, 0, 200,100)); +} + +tst_QQuickVideoOutput::tst_QQuickVideoOutput() +{ +} + +void tst_QQuickVideoOutput::fillMode() +{ + QQmlComponent component(&m_engine); + component.loadUrl(QUrl("qrc:/main.qml")); + + QObject *videoOutput = component.create(); + QVERIFY(videoOutput != nullptr); + + QSignalSpy propSpy(videoOutput, SIGNAL(fillModeChanged(QQuickVideoOutput::FillMode))); + + // Default is preserveaspectfit + QCOMPARE(videoOutput->property("fillMode").value<QQuickVideoOutput::FillMode>(), QQuickVideoOutput::PreserveAspectFit); + QCOMPARE(propSpy.size(), 0); + + videoOutput->setProperty("fillMode", QVariant(int(QQuickVideoOutput::PreserveAspectCrop))); + QCOMPARE(videoOutput->property("fillMode").value<QQuickVideoOutput::FillMode>(), QQuickVideoOutput::PreserveAspectCrop); + QCOMPARE(propSpy.size(), 1); + + videoOutput->setProperty("fillMode", QVariant(int(QQuickVideoOutput::Stretch))); + QCOMPARE(videoOutput->property("fillMode").value<QQuickVideoOutput::FillMode>(), QQuickVideoOutput::Stretch); + QCOMPARE(propSpy.size(), 2); + + videoOutput->setProperty("fillMode", QVariant(int(QQuickVideoOutput::Stretch))); + QCOMPARE(videoOutput->property("fillMode").value<QQuickVideoOutput::FillMode>(), QQuickVideoOutput::Stretch); + QCOMPARE(propSpy.size(), 2); + + delete videoOutput; +} + +void tst_QQuickVideoOutput::orientation() +{ + QQmlComponent component(&m_engine); + component.loadUrl(QUrl("qrc:/main.qml")); + + QObject *videoOutput = component.create(); + QVERIFY(videoOutput != nullptr); + + QSignalSpy propSpy(videoOutput, SIGNAL(orientationChanged())); + + // Default orientation is 0 + QCOMPARE(videoOutput->property("orientation").toInt(), 0); + QCOMPARE(propSpy.size(), 0); + + videoOutput->setProperty("orientation", QVariant(90)); + QCOMPARE(videoOutput->property("orientation").toInt(), 90); + QCOMPARE(propSpy.size(), 1); + + videoOutput->setProperty("orientation", QVariant(180)); + QCOMPARE(videoOutput->property("orientation").toInt(), 180); + QCOMPARE(propSpy.size(), 2); + + videoOutput->setProperty("orientation", QVariant(270)); + QCOMPARE(videoOutput->property("orientation").toInt(), 270); + QCOMPARE(propSpy.size(), 3); + + videoOutput->setProperty("orientation", QVariant(360)); + QCOMPARE(videoOutput->property("orientation").toInt(), 360); + QCOMPARE(propSpy.size(), 4); + + // More than 360 should be fine + videoOutput->setProperty("orientation", QVariant(540)); + QCOMPARE(videoOutput->property("orientation").toInt(), 540); + QCOMPARE(propSpy.size(), 5); + + // Negative should be fine + videoOutput->setProperty("orientation", QVariant(-180)); + QCOMPARE(videoOutput->property("orientation").toInt(), -180); + QCOMPARE(propSpy.size(), 6); + + // Same value should not reemit + videoOutput->setProperty("orientation", QVariant(-180)); + QCOMPARE(videoOutput->property("orientation").toInt(), -180); + QCOMPARE(propSpy.size(), 6); + + // Non multiples of 90 should not work + videoOutput->setProperty("orientation", QVariant(-1)); + QCOMPARE(videoOutput->property("orientation").toInt(), -180); + QCOMPARE(propSpy.size(), 6); + + delete videoOutput; +} + +void tst_QQuickVideoOutput::surfaceSource() +{ + QQmlComponent component(&m_engine); + component.loadUrl(QUrl("qrc:/main.qml")); + + QObject *videoOutput = component.create(); + QVERIFY(videoOutput != nullptr); + + QMediaPlayer holder(this); + + QCOMPARE(holder.videoOutput(), nullptr); + + holder.setVideoOutput(videoOutput); + + QVERIFY(holder.videoOutput() != nullptr); + QVERIFY(holder.videoSink() != nullptr); + + delete videoOutput; + + // This should clear the surface + QVERIFY(holder.videoOutput() == nullptr); + QVERIFY(holder.videoSink() == nullptr); + + // Also, creating two sources, setting them in order, and destroying the first + // should not zero holder.videoSink() + videoOutput = component.create(); + holder.setVideoOutput(videoOutput); + + QObject *surface = holder.videoOutput(); + QVERIFY(surface != nullptr); + + QObject *videoOutput2 = component.create(); + QVERIFY(videoOutput2); + holder.setVideoOutput(videoOutput2); + QVERIFY(holder.videoOutput() != nullptr); + QVERIFY(holder.videoOutput() != surface); // Surface should have changed + surface = holder.videoOutput(); + QVERIFY(surface == videoOutput2); + + // Now delete first one + delete videoOutput; + QVERIFY(holder.videoOutput() == surface); // Should not have changed surface + + // Now create a second surface and assign it as the source + // The old surface holder should be zeroed + QMediaPlayer holder2(this); + holder2.setVideoOutput(videoOutput2); + + QVERIFY(holder.videoOutput() == nullptr); + QVERIFY(holder2.videoOutput() != nullptr); + + // Finally a combination - set the same source to two things, then assign a new source + // to the first output - should not reset the first source + videoOutput = component.create(); + holder2.setVideoOutput(videoOutput); + + // Both vo and vo2 were pointed to holder2 - setting vo2 should not clear holder2 + QVERIFY(holder2.videoOutput() != nullptr); + QVERIFY(holder.videoOutput() == nullptr); + holder.setVideoOutput(videoOutput2); + QVERIFY(holder2.videoOutput() != nullptr); + QVERIFY(holder.videoOutput() != nullptr); + + // They should also be independent + QVERIFY(holder.videoOutput() != holder2.videoOutput()); + + delete videoOutput; + delete videoOutput2; +} + +static const uchar rgb32ImageData[] = +{// B G R A + 0x00, 0x01, 0x02, 0xff, 0x03, 0x04, 0x05, 0xff, + 0x06, 0x07, 0x08, 0xff, 0x09, 0x0a, 0x0b, 0xff, + 0x00, 0x01, 0x02, 0xff, 0x03, 0x04, 0x05, 0xff, + 0x06, 0x07, 0x08, 0xff, 0x09, 0x0a, 0x0b, 0xff, + 0x00, 0x01, 0x02, 0xff, 0x03, 0x04, 0x05, 0xff, + 0x06, 0x07, 0x08, 0xff, 0x09, 0x0a, 0x0b, 0xff, + 0x00, 0x01, 0x02, 0xff, 0x03, 0x04, 0x05, 0xff, + 0x06, 0x07, 0x08, 0xff, 0x09, 0x0a, 0x0b, 0xff +}; + +void tst_QQuickVideoOutput::paintSurface() +{ + QQuickView window; + window.setSource(QUrl("qrc:/main.qml")); + window.show(); + QVERIFY(QTest::qWaitForWindowExposed(&window)); + + auto videoOutput = qobject_cast<QQuickVideoOutput *>(window.rootObject()); + QVERIFY(videoOutput); + + auto surface = videoOutput->videoSink(); + QVERIFY(surface); + videoOutput->setSize(QSize(2, 2)); + + QVideoFrame frame(QVideoFrameFormat(QSize(4, 4), QVideoFrameFormat::Format_ARGB8888)); + frame.map(QtVideo::MapMode::ReadWrite); + QCOMPARE(frame.mappedBytes(0), 64); + memcpy(frame.bits(0), rgb32ImageData, 64); + frame.unmap(); + surface->setVideoFrame(frame); +} + +void tst_QQuickVideoOutput::sourceRect() +{ + QQmlComponent component(&m_engine); + component.loadUrl(QUrl("qrc:/main.qml")); + + QObject *videoOutput = component.create(); + QVERIFY(videoOutput != nullptr); + + QMediaPlayer holder(this); + + QSignalSpy propSpy(videoOutput, SIGNAL(sourceRectChanged())); + + holder.setVideoOutput(videoOutput); + + QCOMPARE(videoOutput->property("sourceRect").toRectF(), QRectF()); + + presentDummyFrame(holder.videoSink(), QSize(200,100)); + + QCOMPARE(videoOutput->property("sourceRect").toRectF(), QRectF(0, 0, 200, 100)); + QCOMPARE(propSpy.size(), 1); + + // Another frame shouldn't cause a source rect change + presentDummyFrame(holder.videoSink(), QSize(200,100)); + QCOMPARE(propSpy.size(), 1); + QCOMPARE(videoOutput->property("sourceRect").toRectF(), QRectF(0, 0, 200, 100)); + + // Changing orientation and stretch modes should not affect this + videoOutput->setProperty("orientation", QVariant(90)); + updateOutputGeometry(videoOutput); + QCOMPARE(videoOutput->property("sourceRect").toRectF(), QRectF(0, 0, 200, 100)); + + videoOutput->setProperty("orientation", QVariant(180)); + updateOutputGeometry(videoOutput); + QCOMPARE(videoOutput->property("sourceRect").toRectF(), QRectF(0, 0, 200, 100)); + + videoOutput->setProperty("orientation", QVariant(270)); + updateOutputGeometry(videoOutput); + QCOMPARE(videoOutput->property("sourceRect").toRectF(), QRectF(0, 0, 200, 100)); + + videoOutput->setProperty("orientation", QVariant(-90)); + updateOutputGeometry(videoOutput); + QCOMPARE(videoOutput->property("sourceRect").toRectF(), QRectF(0, 0, 200, 100)); + + videoOutput->setProperty("fillMode", QVariant(int(QQuickVideoOutput::PreserveAspectCrop))); + updateOutputGeometry(videoOutput); + QCOMPARE(videoOutput->property("sourceRect").toRectF(), QRectF(0, 0, 200, 100)); + + videoOutput->setProperty("fillMode", QVariant(int(QQuickVideoOutput::Stretch))); + updateOutputGeometry(videoOutput); + QCOMPARE(videoOutput->property("sourceRect").toRectF(), QRectF(0, 0, 200, 100)); + + videoOutput->setProperty("fillMode", QVariant(int(QQuickVideoOutput::Stretch))); + updateOutputGeometry(videoOutput); + QCOMPARE(videoOutput->property("sourceRect").toRectF(), QRectF(0, 0, 200, 100)); + + delete videoOutput; +} + +void tst_QQuickVideoOutput::updateOutputGeometry(QObject *output) +{ + // Since the object isn't visible, update() doesn't do anything + // so we manually force this + QMetaObject::invokeMethod(output, "_q_updateGeometry"); +} + +void tst_QQuickVideoOutput::contentRect() +{ + QFETCH(int, orientation); + QFETCH(QQuickVideoOutput::FillMode, fillMode); + QFETCH(QRectF, expected); + + QVERIFY(m_mappingOutput); + m_mappingOutput->setProperty("orientation", QVariant(orientation)); + m_mappingOutput->setProperty("fillMode", QVariant::fromValue(fillMode)); + + updateOutputGeometry(m_mappingOutput); + + QRectF output = m_mappingOutput->property("contentRect").toRectF(); + QCOMPARE(output, expected); +} + +void tst_QQuickVideoOutput::contentRect_data() +{ + QTest::addColumn<int>("orientation"); + QTest::addColumn<QQuickVideoOutput::FillMode>("fillMode"); + QTest::addColumn<QRectF>("expected"); + + QQuickVideoOutput::FillMode stretch = QQuickVideoOutput::Stretch; + QQuickVideoOutput::FillMode fit = QQuickVideoOutput::PreserveAspectFit; + QQuickVideoOutput::FillMode crop = QQuickVideoOutput::PreserveAspectCrop; + + // Stretch just keeps the full render rect regardless of orientation + QTest::newRow("s0") << 0 << stretch << QRectF(0,0,150,100); + QTest::newRow("s90") << 90 << stretch << QRectF(0,0,150,100); + QTest::newRow("s180") << 180 << stretch << QRectF(0,0,150,100); + QTest::newRow("s270") << 270 << stretch << QRectF(0,0,150,100); + + // Fit depends on orientation + // Source is 200x100, fitting in 150x100 -> 150x75 + // or 100x200 -> 50x100 + QTest::newRow("f0") << 0 << fit << QRectF(0,12.5f,150,75); + QTest::newRow("f90") << 90 << fit << QRectF(50,0,50,100); + QTest::newRow("f180") << 180 << fit << QRectF(0,12.5,150,75); + QTest::newRow("f270") << 270 << fit << QRectF(50,0,50,100); + + // Crop also depends on orientation, may go outside render rect + // 200x100 -> -25,0 200x100 + // 100x200 -> 0,-100 150x300 + QTest::newRow("c0") << 0 << crop << QRectF(-25,0,200,100); + QTest::newRow("c90") << 90 << crop << QRectF(0,-100,150,300); + QTest::newRow("c180") << 180 << crop << QRectF(-25,0,200,100); + QTest::newRow("c270") << 270 << crop << QRectF(0,-100,150,300); +} + +QTEST_MAIN(tst_QQuickVideoOutput) + +#include "tst_qquickvideooutput.moc" diff --git a/tests/auto/integration/qquickvideooutput_window/CMakeLists.txt b/tests/auto/integration/qquickvideooutput_window/CMakeLists.txt new file mode 100644 index 000000000..e7b624c70 --- /dev/null +++ b/tests/auto/integration/qquickvideooutput_window/CMakeLists.txt @@ -0,0 +1,34 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +# Generated from qdeclarativevideooutput_window.pro. + +##################################################################### +## tst_qdeclarativevideooutput_window Test: +##################################################################### + +qt_internal_add_test(tst_qquickvideooutput_window + SOURCES + tst_qquickvideooutput_window.cpp + INCLUDE_DIRECTORIES + ../../../../src/imports/multimedia + LIBRARIES + Qt::Gui + Qt::MultimediaPrivate + Qt::MultimediaQuickPrivate + Qt::Qml + Qt::Quick +) + +# Resources: +set(qml_resource_files + "main.qml" +) + +qt_internal_add_resource(tst_qquickvideooutput_window "qml" + PREFIX + "/" + FILES + ${qml_resource_files} +) + diff --git a/tests/auto/integration/qdeclarativevideooutput_window/main.qml b/tests/auto/integration/qquickvideooutput_window/main.qml index 8866be147..40cecc762 100644 --- a/tests/auto/integration/qdeclarativevideooutput_window/main.qml +++ b/tests/auto/integration/qquickvideooutput_window/main.qml @@ -1,5 +1,5 @@ -import QtQuick 2.0 -import QtMultimedia 5.0 +import QtQuick +import QtMultimedia Item { width: 200 diff --git a/tests/auto/integration/qquickvideooutput_window/tst_qquickvideooutput_window.cpp b/tests/auto/integration/qquickvideooutput_window/tst_qquickvideooutput_window.cpp new file mode 100644 index 000000000..68f1771f9 --- /dev/null +++ b/tests/auto/integration/qquickvideooutput_window/tst_qquickvideooutput_window.cpp @@ -0,0 +1,96 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// Copyright (C) 2016 Research In Motion +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +//TESTED_COMPONENT=plugins/declarative/multimedia + +#include "private/qquickvideooutput_p.h" +#include <QtCore/qobject.h> +#include <QtTest/qtest.h> +#include <QtQml/qqmlengine.h> +#include <QtQml/qqmlcomponent.h> +#include <QtQuick/qquickitem.h> +#include <QtQuick/qquickview.h> +#include <private/qplatformvideosink_p.h> +#include <qmediaplayer.h> + +class QtTestVideoObject : public QObject +{ + Q_OBJECT +public: + explicit QtTestVideoObject() + : QObject(nullptr) + { + } +}; + +class tst_QQuickVideoOutputWindow : public QObject +{ + Q_OBJECT +public: + tst_QQuickVideoOutputWindow() + : QObject(nullptr) + , m_sourceObject(&m_videoObject) + { + } + + ~tst_QQuickVideoOutputWindow() override + = default; + +public slots: + void initTestCase(); + void cleanupTestCase(); + +private slots: + void aspectRatio(); + +private: + QQmlEngine m_engine; + QQuickVideoOutput *m_videoItem; + QScopedPointer<QQuickItem> m_rootItem; + QtTestVideoObject m_videoObject; + QMediaPlayer m_sourceObject; + QQuickView m_view; + QVideoSink *m_sink; +}; + +void tst_QQuickVideoOutputWindow::initTestCase() +{ + QQmlComponent component(&m_engine); + component.loadUrl(QUrl("qrc:/main.qml")); + + m_rootItem.reset(qobject_cast<QQuickItem *>(component.create())); + QVERIFY(m_rootItem != nullptr); + m_videoItem = qobject_cast<QQuickVideoOutput *>(m_rootItem->findChild<QQuickItem *>("videoOutput")); + QVERIFY(m_videoItem); + m_sink = m_videoItem->videoSink(); + m_rootItem->setParentItem(m_view.contentItem()); + m_sourceObject.setVideoOutput(m_videoItem); + + m_view.resize(200, 200); + m_view.show(); +} + +void tst_QQuickVideoOutputWindow::cleanupTestCase() +{ + // Make sure that QQuickVideoOutput doesn't segfault when it is being destroyed after + // the service is already gone + m_view.setSource(QUrl()); + m_rootItem.reset(); +} + +void tst_QQuickVideoOutputWindow::aspectRatio() +{ + m_videoItem->setProperty("fillMode", QQuickVideoOutput::Stretch); + QTRY_COMPARE(m_videoItem->fillMode(), QQuickVideoOutput::Stretch); + + m_videoItem->setProperty("fillMode", QQuickVideoOutput::PreserveAspectFit); + QTRY_COMPARE(m_videoItem->fillMode(), QQuickVideoOutput::PreserveAspectFit); + + m_videoItem->setProperty("fillMode", QQuickVideoOutput::PreserveAspectCrop); + QTRY_COMPARE(m_videoItem->fillMode(), QQuickVideoOutput::PreserveAspectCrop); +} + +QTEST_MAIN(tst_QQuickVideoOutputWindow) + +#include "tst_qquickvideooutput_window.moc" diff --git a/tests/auto/integration/qscreencapturebackend/BLACKLIST b/tests/auto/integration/qscreencapturebackend/BLACKLIST new file mode 100644 index 000000000..bd6bb0e01 --- /dev/null +++ b/tests/auto/integration/qscreencapturebackend/BLACKLIST @@ -0,0 +1,11 @@ +macos ci +windows ci + +#QTBUG-122577 +opensuse-15.5 ci + +#QTBUG-112827 on Android +#QTBUG-111190, v4l2m2m issues +[capture_capturesToFile_whenConnectedToMediaRecorder] +linux ci +android ci diff --git a/tests/auto/integration/qscreencapturebackend/CMakeLists.txt b/tests/auto/integration/qscreencapturebackend/CMakeLists.txt new file mode 100644 index 000000000..9b40642a0 --- /dev/null +++ b/tests/auto/integration/qscreencapturebackend/CMakeLists.txt @@ -0,0 +1,17 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +##################################################################### +## tst_qscreencapturebackend Test: +##################################################################### + +qt_internal_add_test(tst_qscreencapturebackend + SOURCES + tst_qscreencapturebackend.cpp + LIBRARIES + Qt::Multimedia + Qt::Gui + Qt::Widgets +) + + diff --git a/tests/auto/integration/qscreencapturebackend/tst_qscreencapturebackend.cpp b/tests/auto/integration/qscreencapturebackend/tst_qscreencapturebackend.cpp new file mode 100644 index 000000000..522d9bcdd --- /dev/null +++ b/tests/auto/integration/qscreencapturebackend/tst_qscreencapturebackend.cpp @@ -0,0 +1,505 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include <QtTest/QtTest> + +#include <qvideosink.h> +#include <qvideoframe.h> +#include <qmediacapturesession.h> +#include <qpainter.h> +#include <qscreencapture.h> +#include <qsignalspy.h> +#include <qmediarecorder.h> +#include <qmediaplayer.h> + +#include <vector> + +QT_USE_NAMESPACE + +/* + This is the backend conformance test. + + Since it relies on platform media framework it may be less stable. + Note, some of screen capture backend is not implemented or has bugs. + That's why some of the tests could get failed. + TODO: fix and platform implementations and make it stable. +*/ + +class QTestWidget : public QWidget +{ +public: + QTestWidget(QColor firstColor, QColor secondColor) + : m_firstColor(firstColor), m_secondColor(secondColor) + { + } + + static std::unique_ptr<QTestWidget> createAndShow(Qt::WindowFlags flags, const QRect &geometry, + QScreen *screen = nullptr, + QColor firstColor = QColor(0xFF, 0, 0), + QColor secondColor = QColor(0, 0, 0xFF)) + { + auto widget = std::make_unique<QTestWidget>(firstColor, secondColor); + + widget->setWindowTitle("Test QScreenCapture"); + widget->setScreen(screen ? screen : QApplication::primaryScreen()); + widget->setWindowFlags(flags); + widget->setGeometry(geometry); + widget->show(); + + return widget; + } + + void setColors(QColor firstColor, QColor secondColor) + { + m_firstColor = firstColor; + m_secondColor = secondColor; + this->repaint(); + } + +protected: + void paintEvent(QPaintEvent * /*event*/) override + { + QPainter p(this); + p.setPen(Qt::NoPen); + + p.setBrush(m_firstColor); + auto rect = this->rect(); + p.drawRect(rect); + + if (m_firstColor != m_secondColor) { + rect.adjust(40, 50, -60, -70); + p.setBrush(m_secondColor); + p.drawRect(rect); + } + } + +private: + QColor m_firstColor; + QColor m_secondColor; +}; + +class TestVideoSink : public QVideoSink +{ + Q_OBJECT +public: + TestVideoSink() + { + connect(this, &QVideoSink::videoFrameChanged, this, &TestVideoSink::videoFrameChangedSync); + } + + void setStoreImagesEnabled(bool storeImages = true) { + if (storeImages) + connect(this, &QVideoSink::videoFrameChanged, this, &TestVideoSink::storeImage, Qt::UniqueConnection); + else + disconnect(this, &QVideoSink::videoFrameChanged, this, &TestVideoSink::storeImage); + } + + const std::vector<QImage> &images() const { return m_images; } + + QVideoFrame waitForFrame() + { + QSignalSpy spy(this, &TestVideoSink::videoFrameChangedSync); + return spy.wait() ? spy.at(0).at(0).value<QVideoFrame>() : QVideoFrame{}; + } + +signals: + void videoFrameChangedSync(QVideoFrame frame); + +private: + void storeImage(const QVideoFrame &frame) { + auto image = frame.toImage(); + image.detach(); + m_images.push_back(std::move(image)); + } + +private: + std::vector<QImage> m_images; +}; + +class tst_QScreenCaptureBackend : public QObject +{ + Q_OBJECT + + void removeWhileCapture(std::function<void(QScreenCapture &)> scModifier, + std::function<void()> deleter); + + void capture(QTestWidget &widget, const QPoint &drawingOffset, const QSize &expectedSize, + std::function<void(QScreenCapture &)> scModifier); + +private slots: + void initTestCase(); + void setActive_startsAndStopsCapture(); + void setScreen_selectsScreen_whenCalledWithWidgetsScreen(); + void constructor_selectsPrimaryScreenAsDefault(); + void setScreen_selectsSecondaryScreen_whenCalledWithSecondaryScreen(); + + void capture_capturesToFile_whenConnectedToMediaRecorder(); + void removeScreenWhileCapture(); // Keep the test last defined. TODO: find a way to restore + // application screens. +}; + +void tst_QScreenCaptureBackend::setActive_startsAndStopsCapture() +{ +#ifdef Q_OS_ANDROID + // Should be removed after fixing QTBUG-112855 + auto widget = QTestWidget::createAndShow(Qt::Window | Qt::FramelessWindowHint, + QRect{ 200, 100, 430, 351 }); + QVERIFY(QTest::qWaitForWindowExposed(widget.get())); + QTest::qWait(100); +#endif + TestVideoSink sink; + QScreenCapture sc; + + QSignalSpy errorsSpy(&sc, &QScreenCapture::errorOccurred); + QSignalSpy activeStateSpy(&sc, &QScreenCapture::activeChanged); + + QMediaCaptureSession session; + + session.setScreenCapture(&sc); + session.setVideoSink(&sink); + + QCOMPARE(activeStateSpy.size(), 0); + QVERIFY(!sc.isActive()); + + // set active true + { + sc.setActive(true); + + QVERIFY(sc.isActive()); + QCOMPARE(activeStateSpy.size(), 1); + QCOMPARE(activeStateSpy.front().front().toBool(), true); + QCOMPARE(errorsSpy.size(), 0); + } + + // wait a bit + { + activeStateSpy.clear(); + QTest::qWait(50); + + QCOMPARE(activeStateSpy.size(), 0); + } + + // set active false + { + sc.setActive(false); + + sink.setStoreImagesEnabled(true); + + QVERIFY(!sc.isActive()); + QCOMPARE(sink.images().size(), 0u); + QCOMPARE(activeStateSpy.size(), 1); + QCOMPARE(activeStateSpy.front().front().toBool(), false); + QCOMPARE(errorsSpy.size(), 0); + } + + // set active false again + { + activeStateSpy.clear(); + + sc.setActive(false); + + QVERIFY(!sc.isActive()); + QCOMPARE(activeStateSpy.size(), 0); + QCOMPARE(errorsSpy.size(), 0); + } +} + +void tst_QScreenCaptureBackend::capture(QTestWidget &widget, const QPoint &drawingOffset, + const QSize &expectedSize, + std::function<void(QScreenCapture &)> scModifier) +{ + TestVideoSink sink; + QScreenCapture sc; + + QSignalSpy errorsSpy(&sc, &QScreenCapture::errorOccurred); + + if (scModifier) + scModifier(sc); + + QMediaCaptureSession session; + + session.setScreenCapture(&sc); + session.setVideoSink(&sink); + + const auto pixelRatio = widget.devicePixelRatio(); + + sc.setActive(true); + + QVERIFY(sc.isActive()); + + // In some cases, on Linux the window seems to be of a wrong color after appearance, + // the delay helps. + // TODO: remove the delay + QTest::qWait(300); + + // Let's wait for the first frame to address a potential initialization delay. + // In practice, the delay varies between the platform and may randomly get increased. + { + const auto firstFrame = sink.waitForFrame(); + QVERIFY(firstFrame.isValid()); + } + + sink.setStoreImagesEnabled(); + + const int delay = 200; + + QTest::qWait(delay); + const auto expectedFramesCount = + delay / static_cast<int>(1000 / std::min(widget.screen()->refreshRate(), 60.)); + const int framesCount = static_cast<int>(sink.images().size()); + QCOMPARE_LE(framesCount, expectedFramesCount + 2); + QCOMPARE_GE(framesCount, 1); + + for (const auto &image : sink.images()) { + auto pixelColor = [&drawingOffset, pixelRatio, &image](int x, int y) { + return image.pixelColor((QPoint(x, y) + drawingOffset) * pixelRatio).toRgb(); + }; + const int capturedWidth = qRound(image.size().width() / pixelRatio); + const int capturedHeight = qRound(image.size().height() / pixelRatio); + QCOMPARE(QSize(capturedWidth, capturedHeight), expectedSize); + QCOMPARE(pixelColor(0, 0), QColor(0xFF, 0, 0)); + + QCOMPARE(pixelColor(39, 50), QColor(0xFF, 0, 0)); + QCOMPARE(pixelColor(40, 49), QColor(0xFF, 0, 0)); + + QCOMPARE(pixelColor(40, 50), QColor(0, 0, 0xFF)); + } + + QCOMPARE(errorsSpy.size(), 0); +} + +void tst_QScreenCaptureBackend::removeWhileCapture( + std::function<void(QScreenCapture &)> scModifier, std::function<void()> deleter) +{ + QVideoSink sink; + QScreenCapture sc; + + QSignalSpy errorsSpy(&sc, &QScreenCapture::errorOccurred); + + QMediaCaptureSession session; + + if (scModifier) + scModifier(sc); + + session.setScreenCapture(&sc); + session.setVideoSink(&sink); + + sc.setActive(true); + + QTest::qWait(300); + + QCOMPARE(errorsSpy.size(), 0); + + if (deleter) + deleter(); + + QTest::qWait(100); + + QSignalSpy framesSpy(&sink, &QVideoSink::videoFrameChanged); + + QTest::qWait(100); + + QCOMPARE(errorsSpy.size(), 1); + QCOMPARE(errorsSpy.front().front().value<QScreenCapture::Error>(), + QScreenCapture::CaptureFailed); + QVERIFY2(!errorsSpy.front().back().value<QString>().isEmpty(), + "Expected not empty error description"); + + QVERIFY2(framesSpy.empty(), "No frames expected after screen removal"); +} + +void tst_QScreenCaptureBackend::initTestCase() +{ +#ifdef Q_OS_ANDROID + QSKIP("grabWindow() no longer supported on Android adding child windows support: QTBUG-118849"); +#endif +#if defined(Q_OS_LINUX) + if (qEnvironmentVariable("QTEST_ENVIRONMENT").toLower() == "ci" && + qEnvironmentVariable("XDG_SESSION_TYPE").toLower() != "x11") + QSKIP("Skip on wayland; to be fixed"); +#endif + + if (!QApplication::primaryScreen()) + QSKIP("No screens found"); + + QScreenCapture sc; + if (sc.error() == QScreenCapture::CapturingNotSupported) + QSKIP("Screen capturing not supported"); +} + +void tst_QScreenCaptureBackend::setScreen_selectsScreen_whenCalledWithWidgetsScreen() +{ + auto widget = QTestWidget::createAndShow(Qt::Window | Qt::FramelessWindowHint + | Qt::WindowStaysOnTopHint +#ifdef Q_OS_ANDROID + | Qt::Popup +#endif + , + QRect{ 200, 100, 430, 351 }); + QVERIFY(QTest::qWaitForWindowExposed(widget.get())); + + capture(*widget, { 200, 100 }, widget->screen()->size(), + [&widget](QScreenCapture &sc) { sc.setScreen(widget->screen()); }); +} + +void tst_QScreenCaptureBackend::constructor_selectsPrimaryScreenAsDefault() +{ + auto widget = QTestWidget::createAndShow(Qt::Window | Qt::FramelessWindowHint + | Qt::WindowStaysOnTopHint +#ifdef Q_OS_ANDROID + | Qt::Popup +#endif + , + QRect{ 200, 100, 430, 351 }); + QVERIFY(QTest::qWaitForWindowExposed(widget.get())); + + capture(*widget, { 200, 100 }, QApplication::primaryScreen()->size(), nullptr); +} + +void tst_QScreenCaptureBackend::setScreen_selectsSecondaryScreen_whenCalledWithSecondaryScreen() +{ + const auto screens = QApplication::screens(); + if (screens.size() < 2) + QSKIP("2 or more screens required"); + + auto topLeft = screens.back()->geometry().topLeft().x(); + + auto widgetOnSecondaryScreen = QTestWidget::createAndShow( + Qt::Window | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint, + QRect{ topLeft + 200, 100, 430, 351 }, screens.back()); + QVERIFY(QTest::qWaitForWindowExposed(widgetOnSecondaryScreen.get())); + + auto widgetOnPrimaryScreen = QTestWidget::createAndShow( + Qt::Window | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint, + QRect{ 200, 100, 430, 351 }, screens.front(), QColor(0, 0, 0), QColor(0, 0, 0)); + QVERIFY(QTest::qWaitForWindowExposed(widgetOnPrimaryScreen.get())); + capture(*widgetOnSecondaryScreen, { 200, 100 }, screens.back()->size(), + [&screens](QScreenCapture &sc) { sc.setScreen(screens.back()); }); +} + +void tst_QScreenCaptureBackend::capture_capturesToFile_whenConnectedToMediaRecorder() +{ +#ifdef Q_OS_LINUX + if (qEnvironmentVariable("QTEST_ENVIRONMENT").toLower() == "ci") + QSKIP("QTBUG-116671: SKIP on linux CI to avoid crashes in ffmpeg. To be fixed."); +#endif + + // Create widget with blue color + auto widget = QTestWidget::createAndShow(Qt::Window | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint, + QRect{ 200, 100, 430, 351 }); + widget->setColors(QColor(0, 0, 0xFF), QColor(0, 0, 0xFF)); + + QScreenCapture sc; + QSignalSpy errorsSpy(&sc, &QScreenCapture::errorOccurred); + QMediaCaptureSession session; + QMediaRecorder recorder; + session.setScreenCapture(&sc); + session.setRecorder(&recorder); + auto screen = QApplication::primaryScreen(); + QSize screenSize = screen->geometry().size(); + QSize videoResolution = QSize(1920, 1080); + recorder.setVideoResolution(videoResolution); + recorder.setQuality(QMediaRecorder::VeryHighQuality); + + // Insert metadata + QMediaMetaData metaData; + metaData.insert(QMediaMetaData::Author, QStringLiteral("Author")); + metaData.insert(QMediaMetaData::Date, QDateTime::currentDateTime()); + recorder.setMetaData(metaData); + + sc.setActive(true); + + QTest::qWait(1000); // wait a bit for SC threading activating + + { + QSignalSpy recorderStateChanged(&recorder, &QMediaRecorder::recorderStateChanged); + + recorder.record(); + + QTRY_VERIFY(!recorderStateChanged.empty()); + QCOMPARE(recorder.recorderState(), QMediaRecorder::RecordingState); + } + + QTest::qWait(1000); + widget->setColors(QColor(0, 0xFF, 0), QColor(0, 0xFF, 0)); // Change widget color + QTest::qWait(1000); + + { + QSignalSpy recorderStateChanged(&recorder, &QMediaRecorder::recorderStateChanged); + + recorder.stop(); + + QTRY_VERIFY(!recorderStateChanged.empty()); + QCOMPARE(recorder.recorderState(), QMediaRecorder::StoppedState); + } + + QString fileName = recorder.actualLocation().toLocalFile(); + QVERIFY(!fileName.isEmpty()); + QVERIFY(QFileInfo(fileName).size() > 0); + + TestVideoSink sink; + QMediaPlayer player; + player.setSource(fileName); + QTRY_COMPARE(player.mediaStatus(), QMediaPlayer::LoadedMedia); + QCOMPARE_EQ(player.metaData().value(QMediaMetaData::Resolution).toSize(), + QSize(videoResolution)); + QCOMPARE_GT(player.duration(), 350); + QCOMPARE_LT(player.duration(), 3000); + + // Convert video frames to QImages + player.setVideoSink(&sink); + sink.setStoreImagesEnabled(); + player.setPlaybackRate(10); + player.play(); + QTRY_COMPARE(player.mediaStatus(), QMediaPlayer::EndOfMedia); + const size_t framesCount = sink.images().size(); + + // Find pixel point at center of widget + int x = 415 * videoResolution.width() / screenSize.width(); + int y = 275 * videoResolution.height() / screenSize.height(); + auto point = QPoint(x, y); + + // Verify color of first fourth of the video frames + for (size_t i = 0; i <= static_cast<size_t>(framesCount * 0.25); i++) { + QImage image = sink.images().at(i); + QVERIFY(!image.isNull()); + QRgb rgb = image.pixel(point); +// qDebug() << QStringLiteral("RGB: %1, %2, %3").arg(qRed(rgb)).arg(qGreen(rgb)).arg(qBlue(rgb)); + + // RGB values should be 0, 0, 255. Compensating for inaccurate video encoding. + QVERIFY(qRed(rgb) <= 60); + QVERIFY(qGreen(rgb) <= 60); + QVERIFY(qBlue(rgb) >= 200); + } + + // Verify color of last fourth of the video frames + for (size_t i = static_cast<size_t>(framesCount * 0.75); i < framesCount - 1; i++) { + QImage image = sink.images().at(i); + QVERIFY(!image.isNull()); + QRgb rgb = image.pixel(point); +// qDebug() << QStringLiteral("RGB: %1, %2, %3").arg(qRed(rgb)).arg(qGreen(rgb)).arg(qBlue(rgb)); + + // RGB values should be 0, 255, 0. Compensating for inaccurate video encoding. + QVERIFY(qRed(rgb) <= 60); + QVERIFY(qGreen(rgb) >= 200); + QVERIFY(qBlue(rgb) <= 60); + } + + QFile(fileName).remove(); +} + +void tst_QScreenCaptureBackend::removeScreenWhileCapture() +{ + QSKIP("TODO: find a reliable way to emulate it"); + + removeWhileCapture([](QScreenCapture &sc) { sc.setScreen(QApplication::primaryScreen()); }, + []() { + // It's something that doesn't look safe but it performs required flow + // and allows to test the corener case. + delete QApplication::primaryScreen(); + }); +} + +QTEST_MAIN(tst_QScreenCaptureBackend) + +#include "tst_qscreencapturebackend.moc" diff --git a/tests/auto/integration/qsound/BLACKLIST b/tests/auto/integration/qsound/BLACKLIST deleted file mode 100644 index ccb68f541..000000000 --- a/tests/auto/integration/qsound/BLACKLIST +++ /dev/null @@ -1,3 +0,0 @@ -[testLooping] -opensuse-42.3 - diff --git a/tests/auto/integration/qsound/qsound.pro b/tests/auto/integration/qsound/qsound.pro deleted file mode 100644 index 1b552c60e..000000000 --- a/tests/auto/integration/qsound/qsound.pro +++ /dev/null @@ -1,13 +0,0 @@ -TARGET = tst_qsound - -QT += core multimedia-private testlib - -# This is more of a system test -CONFIG += testcase - -SOURCES += tst_qsound.cpp - -TESTDATA += test.wav - -RESOURCES += \ - resources.qrc diff --git a/tests/auto/integration/qsound/resources.qrc b/tests/auto/integration/qsound/resources.qrc deleted file mode 100644 index b54c65040..000000000 --- a/tests/auto/integration/qsound/resources.qrc +++ /dev/null @@ -1,5 +0,0 @@ -<RCC> - <qresource prefix="/"> - <file>test.wav</file> - </qresource> -</RCC> diff --git a/tests/auto/integration/qsound/test.wav b/tests/auto/integration/qsound/test.wav Binary files differdeleted file mode 100644 index e4088a973..000000000 --- a/tests/auto/integration/qsound/test.wav +++ /dev/null diff --git a/tests/auto/integration/qsound/tst_qsound.cpp b/tests/auto/integration/qsound/tst_qsound.cpp deleted file mode 100644 index dbf75f2e3..000000000 --- a/tests/auto/integration/qsound/tst_qsound.cpp +++ /dev/null @@ -1,148 +0,0 @@ -/**************************************************************************** -** -** Copyright (C) 2016 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the test suite of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:GPL-EXCEPT$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 3 as published by the Free Software -** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ - - -#include <QtTest/QtTest> -#include <QtCore/QString> -#include <QSound> -#include <QSoundEffect> - -class tst_QSound : public QObject -{ - Q_OBJECT - -public: - tst_QSound( QObject* parent=0) : QObject(parent) {} - -private slots: - void initTestCase(); - void cleanupTestCase(); - void testLooping(); - void testPlay(); - void testStop(); - - void testPlayResource_data(); - void testPlayResource(); - - void testStaticPlay(); - -private: - QSound* sound; -}; - - -void tst_QSound::initTestCase() -{ - sound = 0; - // Only perform tests if audio device exists - QStringList mimeTypes = QSoundEffect::supportedMimeTypes(); - if (mimeTypes.empty()) - QSKIP("No audio devices available"); - - const QString testFileName = QStringLiteral("test.wav"); - const QString fullPath = QFINDTESTDATA(testFileName); - QVERIFY2(!fullPath.isEmpty(), qPrintable(QStringLiteral("Unable to locate ") + testFileName)); - sound = new QSound(fullPath, this); - - QVERIFY(!sound->fileName().isEmpty()); - QCOMPARE(sound->loops(),1); -} - -void tst_QSound::cleanupTestCase() -{ - if (sound) - { - delete sound; - sound = NULL; - } -} - -void tst_QSound::testLooping() -{ - sound->setLoops(5); - QCOMPARE(sound->loops(),5); - - sound->play(); - QVERIFY(!sound->isFinished()); - - // test.wav is about 200ms, wait until it has finished playing 5 times - QTRY_VERIFY(sound->isFinished()); - QCOMPARE(sound->loopsRemaining(),0); -} - -void tst_QSound::testPlay() -{ - sound->setLoops(1); - sound->play(); - QVERIFY(!sound->isFinished()); - QTRY_VERIFY(sound->isFinished()); -} - -void tst_QSound::testStop() -{ - sound->setLoops(10); - sound->play(); - QVERIFY(!sound->isFinished()); - QTest::qWait(1000); - sound->stop(); - QTRY_VERIFY(sound->isFinished()); -} - -void tst_QSound::testPlayResource_data() -{ - QTest::addColumn<QString>("filePath"); - - QTest::newRow("prefix :/") << ":/test.wav"; - QTest::newRow("prefix qrc:") << "qrc:test.wav"; - QTest::newRow("prefix qrc:///") << "qrc:///test.wav"; -} - -void tst_QSound::testPlayResource() -{ - QFETCH(QString, filePath); - - QSound snd(filePath); - snd.play(); - QVERIFY(!snd.isFinished()); - QTRY_VERIFY(snd.isFinished()); -} - -void tst_QSound::testStaticPlay() -{ - // Check that you hear sound with static play also. - const QString testFileName = QStringLiteral("test.wav"); - const QString fullPath = QFINDTESTDATA(testFileName); - QVERIFY2(!fullPath.isEmpty(), qPrintable(QStringLiteral("Unable to locate ") + testFileName)); - - QSound::play(fullPath); - - QTest::qWait(1000); -} - -QTEST_MAIN(tst_QSound); -#include "tst_qsound.moc" diff --git a/tests/auto/integration/qsoundeffect/CMakeLists.txt b/tests/auto/integration/qsoundeffect/CMakeLists.txt new file mode 100644 index 000000000..d403d9514 --- /dev/null +++ b/tests/auto/integration/qsoundeffect/CMakeLists.txt @@ -0,0 +1,20 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +# Generated from qsoundeffect.pro. + +##################################################################### +## tst_qsoundeffect Test: +##################################################################### + +qt_internal_add_test(tst_qsoundeffect + SOURCES + tst_qsoundeffect.cpp + LIBRARIES + Qt::Gui + Qt::MultimediaPrivate + TESTDATA + "test.wav" + "test_corrupted.wav" + "test_tone.wav" +) diff --git a/tests/auto/integration/qsoundeffect/qsoundeffect.pro b/tests/auto/integration/qsoundeffect/qsoundeffect.pro deleted file mode 100644 index 868346a2e..000000000 --- a/tests/auto/integration/qsoundeffect/qsoundeffect.pro +++ /dev/null @@ -1,19 +0,0 @@ -TARGET = tst_qsoundeffect - -QT += core multimedia-private testlib - -# This is more of a system test -CONFIG += testcase - -SOURCES += tst_qsoundeffect.cpp - -unix:!mac { - !qtConfig(pulseaudio) { - DEFINES += QT_MULTIMEDIA_QMEDIAPLAYER - } -} - -TESTDATA += test.wav - -RESOURCES += \ - resources.qrc diff --git a/tests/auto/integration/qsoundeffect/resources.qrc b/tests/auto/integration/qsoundeffect/resources.qrc deleted file mode 100644 index 24700560d..000000000 --- a/tests/auto/integration/qsoundeffect/resources.qrc +++ /dev/null @@ -1,8 +0,0 @@ -<RCC> - <qresource prefix="/"> - <file>test.wav</file> - <file>test_corrupted.wav</file> - <file>test_tone.wav</file> - <file>test24.wav</file> - </qresource> -</RCC> diff --git a/tests/auto/integration/qsoundeffect/test24.wav b/tests/auto/integration/qsoundeffect/test24.wav Binary files differdeleted file mode 100644 index 9575aaaee..000000000 --- a/tests/auto/integration/qsoundeffect/test24.wav +++ /dev/null diff --git a/tests/auto/integration/qsoundeffect/tst_qsoundeffect.cpp b/tests/auto/integration/qsoundeffect/tst_qsoundeffect.cpp index 7cbd57007..0d3d9f8b3 100644 --- a/tests/auto/integration/qsoundeffect/tst_qsoundeffect.cpp +++ b/tests/auto/integration/qsoundeffect/tst_qsoundeffect.cpp @@ -1,45 +1,18 @@ -/**************************************************************************** -** -** Copyright (C) 2016 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the test suite of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:GPL-EXCEPT$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 3 as published by the Free Software -** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ - -//TESTED_COMPONENT=src/multimedia +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #include <QtTest/QtTest> #include <QtCore/qlocale.h> -#include <qaudiooutput.h> -#include <qaudiodeviceinfo.h> +#include <qaudiodevice.h> #include <qaudio.h> #include "qsoundeffect.h" +#include "qmediadevices.h" class tst_QSoundEffect : public QObject { Q_OBJECT public: - tst_QSoundEffect(QObject* parent=0) : QObject(parent) {} + tst_QSoundEffect(QObject* parent=nullptr) : QObject(parent) {} public slots: void init(); @@ -63,14 +36,12 @@ private slots: void testSupportedMimeTypes_data(); void testSupportedMimeTypes(); void testCorruptFile(); - void testPlaying24Bits(); private: QSoundEffect* sound; QUrl url; // test.wav: pcm_s16le, 48000 Hz, stereo, s16 QUrl url2; // test_tone.wav: pcm_s16le, 44100 Hz, mono QUrl urlCorrupted; // test_corrupted.wav: corrupted - QUrl url24Bits; // test24.wav pcm_s24le, 44100 Hz, mono }; void tst_QSoundEffect::init() @@ -108,11 +79,6 @@ void tst_QSoundEffect::initTestCase() QVERIFY2(!fullPath.isEmpty(), qPrintable(QStringLiteral("Unable to locate ") + testFileName)); urlCorrupted = QUrl::fromLocalFile(fullPath); - testFileName = QStringLiteral("test24.wav"); - fullPath = QFINDTESTDATA(testFileName); - QVERIFY2(!fullPath.isEmpty(), qPrintable(QStringLiteral("Unable to locate ") + testFileName)); - url24Bits = QUrl::fromLocalFile(fullPath); - sound = new QSoundEffect(this); QVERIFY(sound->source().isEmpty()); @@ -123,18 +89,18 @@ void tst_QSoundEffect::initTestCase() void tst_QSoundEffect::testSource() { - QSignalSpy readSignal(sound, SIGNAL(sourceChanged())); + QSignalSpy readSignal(sound, &QSoundEffect::sourceChanged); sound->setSource(url); sound->setVolume(0.1f); QCOMPARE(sound->source(),url); - QCOMPARE(readSignal.count(),1); + QCOMPARE(readSignal.size(),1); QTestEventLoop::instance().enterLoop(1); sound->play(); - - QTest::qWait(3000); + QTRY_COMPARE(sound->isPlaying(), false); + QCOMPARE(sound->loopsRemaining(), 0); } void tst_QSoundEffect::testLooping() @@ -142,24 +108,24 @@ void tst_QSoundEffect::testLooping() sound->setSource(url); QTRY_COMPARE(sound->status(), QSoundEffect::Ready); - QSignalSpy readSignal_Count(sound, SIGNAL(loopCountChanged())); - QSignalSpy readSignal_Remaining(sound, SIGNAL(loopsRemainingChanged())); + QSignalSpy readSignal_Count(sound, &QSoundEffect::loopCountChanged); + QSignalSpy readSignal_Remaining(sound, &QSoundEffect::loopsRemainingChanged); - sound->setLoopCount(5); + sound->setLoopCount(3); sound->setVolume(0.1f); - QCOMPARE(sound->loopCount(), 5); - QCOMPARE(readSignal_Count.count(), 1); + QCOMPARE(sound->loopCount(), 3); + QCOMPARE(readSignal_Count.size(), 1); QCOMPARE(sound->loopsRemaining(), 0); - QCOMPARE(readSignal_Remaining.count(), 0); + QCOMPARE(readSignal_Remaining.size(), 0); sound->play(); - QVERIFY(readSignal_Remaining.count() > 0); + QVERIFY(readSignal_Remaining.size() > 0); - // test.wav is about 200ms, wait until it has finished playing 5 times + // test.wav is about 200ms, wait until it has finished playing 3 times QTestEventLoop::instance().enterLoop(3); QTRY_COMPARE(sound->loopsRemaining(), 0); - QVERIFY(readSignal_Remaining.count() >= 6); + QVERIFY(readSignal_Remaining.size() == 4); QTRY_VERIFY(!sound->isPlaying()); // QTBUG-36643 (setting the loop count while playing should work) @@ -167,31 +133,31 @@ void tst_QSoundEffect::testLooping() readSignal_Count.clear(); readSignal_Remaining.clear(); - sound->setLoopCount(30); - QCOMPARE(sound->loopCount(), 30); - QCOMPARE(readSignal_Count.count(), 1); + sound->setLoopCount(10); + QCOMPARE(sound->loopCount(), 10); + QCOMPARE(readSignal_Count.size(), 1); QCOMPARE(sound->loopsRemaining(), 0); - QCOMPARE(readSignal_Remaining.count(), 0); + QCOMPARE(readSignal_Remaining.size(), 0); sound->play(); - QVERIFY(readSignal_Remaining.count() > 0); + QVERIFY(readSignal_Remaining.size() > 0); // wait for the sound to be played several times - QTRY_VERIFY(sound->loopsRemaining() <= 20); - QVERIFY(readSignal_Remaining.count() >= 10); + QTRY_VERIFY(sound->loopsRemaining() <= 7); + QVERIFY(readSignal_Remaining.size() >= 3); readSignal_Count.clear(); readSignal_Remaining.clear(); // change the loop count while playing - sound->setLoopCount(5); - QCOMPARE(sound->loopCount(), 5); - QCOMPARE(readSignal_Count.count(), 1); - QCOMPARE(sound->loopsRemaining(), 5); - QCOMPARE(readSignal_Remaining.count(), 1); + sound->setLoopCount(3); + QCOMPARE(sound->loopCount(), 3); + QCOMPARE(readSignal_Count.size(), 1); + QCOMPARE(sound->loopsRemaining(), 3); + QCOMPARE(readSignal_Remaining.size(), 1); // wait for all the loops to be completed QTRY_COMPARE(sound->loopsRemaining(), 0); - QTRY_VERIFY(readSignal_Remaining.count() >= 6); + QTRY_VERIFY(readSignal_Remaining.size() == 4); QTRY_VERIFY(!sound->isPlaying()); } @@ -201,15 +167,15 @@ void tst_QSoundEffect::testLooping() sound->setLoopCount(QSoundEffect::Infinite); QCOMPARE(sound->loopCount(), int(QSoundEffect::Infinite)); - QCOMPARE(readSignal_Count.count(), 1); + QCOMPARE(readSignal_Count.size(), 1); QCOMPARE(sound->loopsRemaining(), 0); - QCOMPARE(readSignal_Remaining.count(), 0); + QCOMPARE(readSignal_Remaining.size(), 0); sound->play(); QTRY_COMPARE(sound->loopsRemaining(), int(QSoundEffect::Infinite)); - QCOMPARE(readSignal_Remaining.count(), 1); + QCOMPARE(readSignal_Remaining.size(), 1); - QTest::qWait(1500); + QTest::qWait(500); QVERIFY(sound->isPlaying()); readSignal_Count.clear(); readSignal_Remaining.clear(); @@ -217,36 +183,34 @@ void tst_QSoundEffect::testLooping() // Setting the loop count to 0 should play it one last time sound->setLoopCount(0); QCOMPARE(sound->loopCount(), 1); - QCOMPARE(readSignal_Count.count(), 1); + QCOMPARE(readSignal_Count.size(), 1); QCOMPARE(sound->loopsRemaining(), 1); - QCOMPARE(readSignal_Remaining.count(), 1); + QCOMPARE(readSignal_Remaining.size(), 1); QTRY_COMPARE(sound->loopsRemaining(), 0); - QTRY_VERIFY(readSignal_Remaining.count() >= 2); + QTRY_VERIFY(readSignal_Remaining.size() >= 2); QTRY_VERIFY(!sound->isPlaying()); } } void tst_QSoundEffect::testVolume() { - QSignalSpy readSignal(sound, SIGNAL(volumeChanged())); + QSignalSpy readSignal(sound, &QSoundEffect::volumeChanged); sound->setVolume(0.5); QCOMPARE(sound->volume(),0.5); - QTest::qWait(20); - QCOMPARE(readSignal.count(),1); + QTRY_COMPARE(readSignal.size(),1); } void tst_QSoundEffect::testMuting() { - QSignalSpy readSignal(sound, SIGNAL(mutedChanged())); + QSignalSpy readSignal(sound, &QSoundEffect::mutedChanged); sound->setMuted(true); QCOMPARE(sound->isMuted(),true); - QTest::qWait(20); - QCOMPARE(readSignal.count(),1); + QTRY_COMPARE(readSignal.size(),1); } void tst_QSoundEffect::testPlaying() @@ -309,7 +273,7 @@ void tst_QSoundEffect::testDestroyWhilePlaying() instance->setVolume(0.1f); QTestEventLoop::instance().enterLoop(1); instance->play(); - QTest::qWait(500); + QTest::qWait(100); delete instance; QTestEventLoop::instance().enterLoop(1); } @@ -321,7 +285,7 @@ void tst_QSoundEffect::testDestroyWhileRestartPlaying() instance->setVolume(0.1f); QTestEventLoop::instance().enterLoop(1); instance->play(); - QTest::qWait(1000); + QTRY_COMPARE(instance->isPlaying(), false); //restart playing instance->play(); delete instance; @@ -330,7 +294,7 @@ void tst_QSoundEffect::testDestroyWhileRestartPlaying() void tst_QSoundEffect::testSetSourceWhileLoading() { - for (int i = 0; i < 10; i++) { + for (int i = 0; i < 2; i++) { sound->setSource(url); QVERIFY(sound->status() == QSoundEffect::Loading || sound->status() == QSoundEffect::Ready); sound->setSource(url); // set same source again @@ -358,7 +322,7 @@ void tst_QSoundEffect::testSetSourceWhileLoading() void tst_QSoundEffect::testSetSourceWhilePlaying() { - for (int i = 0; i < 10; i++) { + for (int i = 0; i < 2; i++) { sound->setSource(url); QTRY_COMPARE(sound->status(), QSoundEffect::Ready); sound->play(); @@ -392,9 +356,9 @@ void tst_QSoundEffect::testSupportedMimeTypes_data() { // Verify also passing of audio device info as parameter QTest::addColumn<QSoundEffect*>("instance"); - QTest::newRow("without QAudioDeviceInfo") << sound; - QAudioDeviceInfo deviceInfo(QAudioDeviceInfo::defaultOutputDevice()); - QTest::newRow("with QAudioDeviceInfo") << new QSoundEffect(deviceInfo, this); + QTest::newRow("without QAudioDevice") << sound; + QAudioDevice deviceInfo(QMediaDevices::defaultAudioOutput()); + QTest::newRow("with QAudioDevice") << new QSoundEffect(deviceInfo, this); } void tst_QSoundEffect::testSupportedMimeTypes() @@ -410,13 +374,19 @@ void tst_QSoundEffect::testSupportedMimeTypes() void tst_QSoundEffect::testCorruptFile() { + using namespace Qt::Literals; + auto expectedMessagePattern = + QRegularExpression(uR"(^QSoundEffect\(qaudio\): Error decoding source .*$)"_s); + for (int i = 0; i < 10; i++) { - QSignalSpy statusSpy(sound, SIGNAL(statusChanged())); + QSignalSpy statusSpy(sound, &QSoundEffect::statusChanged); + QTest::ignoreMessage(QtMsgType::QtWarningMsg, expectedMessagePattern); + sound->setSource(urlCorrupted); QVERIFY(!sound->isPlaying()); QVERIFY(sound->status() == QSoundEffect::Loading || sound->status() == QSoundEffect::Error); QTRY_COMPARE(sound->status(), QSoundEffect::Error); - QCOMPARE(statusSpy.count(), 2); + QCOMPARE(statusSpy.size(), 2); sound->play(); QVERIFY(!sound->isPlaying()); @@ -427,26 +397,6 @@ void tst_QSoundEffect::testCorruptFile() } } -void tst_QSoundEffect::testPlaying24Bits() -{ - sound->setLoopCount(QSoundEffect::Infinite); - sound->setSource(url24Bits); - QTestEventLoop::instance().enterLoop(1); - sound->play(); - QTestEventLoop::instance().enterLoop(1); - QTRY_COMPARE(sound->isPlaying(), true); - sound->stop(); - - QSignalSpy readSignal(sound, SIGNAL(volumeChanged())); - sound->setVolume(0.5); - QCOMPARE(sound->volume(), 0.5); - sound->play(); - QTestEventLoop::instance().enterLoop(1); - QTRY_COMPARE(sound->isPlaying(), true); - QCOMPARE(readSignal.count(), 1); - sound->stop(); -} - QTEST_MAIN(tst_QSoundEffect) #include "tst_qsoundeffect.moc" diff --git a/tests/auto/integration/qvideoframebackend/CMakeLists.txt b/tests/auto/integration/qvideoframebackend/CMakeLists.txt new file mode 100644 index 000000000..e4fa79201 --- /dev/null +++ b/tests/auto/integration/qvideoframebackend/CMakeLists.txt @@ -0,0 +1,26 @@ +# Copyright (C) 2024 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +##################################################################### +## tst_qvideoframebackend Test: +##################################################################### + +# Collect test data +file(GLOB_RECURSE test_data_glob + RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} + testdata/*) +list(APPEND testdata_resource_files ${test_data_glob}) + +qt_internal_add_test(tst_qvideoframebackend + SOURCES + ../shared/mediafileselector.h + ../shared/testvideosink.h + tst_qvideoframebackend.cpp + LIBRARIES + Qt::Gui + Qt::MultimediaPrivate + BUILTIN_TESTDATA + TESTDATA ${testdata_resource_files} + INCLUDE_DIRECTORIES + ../shared/ +) diff --git a/tests/auto/integration/qvideoframebackend/testdata/colors.mp4 b/tests/auto/integration/qvideoframebackend/testdata/colors.mp4 Binary files differnew file mode 100644 index 000000000..30ddda8b0 --- /dev/null +++ b/tests/auto/integration/qvideoframebackend/testdata/colors.mp4 diff --git a/tests/auto/integration/qvideoframebackend/testdata/one_red_frame.mp4 b/tests/auto/integration/qvideoframebackend/testdata/one_red_frame.mp4 Binary files differnew file mode 100644 index 000000000..6b67a3433 --- /dev/null +++ b/tests/auto/integration/qvideoframebackend/testdata/one_red_frame.mp4 diff --git a/tests/auto/integration/qvideoframebackend/tst_qvideoframebackend.cpp b/tests/auto/integration/qvideoframebackend/tst_qvideoframebackend.cpp new file mode 100644 index 000000000..590f57160 --- /dev/null +++ b/tests/auto/integration/qvideoframebackend/tst_qvideoframebackend.cpp @@ -0,0 +1,258 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include <QtTest/QtTest> +#include <qmediaplayer.h> +#include <qvideoframe.h> +#include <qdebug.h> + +#include "mediafileselector.h" +#include "testvideosink.h" +#include "private/qvideotexturehelper_p.h" +#include "private/qvideowindow_p.h" +#include <thread> + + +QT_USE_NAMESPACE + +class tst_QVideoFrameBackend : public QObject +{ + Q_OBJECT + +public slots: + void initTestCase(); + void init() { } + void cleanup() { } + +private slots: + void testMediaFilesAreSupported(); + + void toImage_retainsThePreviousMappedState_data(); + void toImage_retainsThePreviousMappedState(); + + void toImage_rendersUpdatedFrame_afterMappingInWriteModeAndModifying_data(); + void toImage_rendersUpdatedFrame_afterMappingInWriteModeAndModifying(); + + void toImage_returnsImage_whenCalledFromSeparateThreadAndWhileRenderingToWindow(); + +private: + QVideoFrame createDefaultFrame() const; + + QVideoFrame createMediaPlayerFrame() const; + + using FrameCreator = decltype(&tst_QVideoFrameBackend::createDefaultFrame); + + template <typename F> + void addMediaPlayerFrameTestData(F &&f); + +private: + MaybeUrl m_oneRedFrameVideo = QUnexpect{}; + MaybeUrl m_colorsVideo = QUnexpect{}; + MediaFileSelector m_mediaSelector; +}; + +QVideoFrame tst_QVideoFrameBackend::createDefaultFrame() const +{ + return QVideoFrame(QVideoFrameFormat(QSize(10, 20), QVideoFrameFormat::Format_ARGB8888)); +} + +QVideoFrame tst_QVideoFrameBackend::createMediaPlayerFrame() const +{ + if (!m_oneRedFrameVideo) + return {}; + + TestVideoSink sink; + QMediaPlayer player; + + player.setVideoOutput(&sink); + player.setSource(*m_oneRedFrameVideo); + + player.play(); + + return sink.waitForFrame(); +} + +template <typename F> +void tst_QVideoFrameBackend::addMediaPlayerFrameTestData(F &&f) +{ + if (!m_oneRedFrameVideo) { + qWarning() << "Skipping test data with mediaplayer as the source cannot be open." + "\nSee the test case 'testMediaFilesAreSupported' for details"; + return; + } + + f(); +} + +void tst_QVideoFrameBackend::initTestCase() +{ +#ifdef Q_OS_ANDROID + qWarning() << "Skip media selection, QTBUG-118571"; + return; +#endif + + m_oneRedFrameVideo = m_mediaSelector.select("qrc:/testdata/one_red_frame.mp4"); + m_colorsVideo = m_mediaSelector.select("qrc:/testdata/colors.mp4"); +} + +void tst_QVideoFrameBackend::testMediaFilesAreSupported() +{ +#ifdef Q_OS_ANDROID + QSKIP("Skip test cases with mediaPlayerFrame on Android CI, because of QTBUG-118571"); +#endif + + QCOMPARE(m_mediaSelector.dumpErrors(), ""); +} + +void tst_QVideoFrameBackend::toImage_retainsThePreviousMappedState_data() +{ + QTest::addColumn<FrameCreator>("frameCreator"); + QTest::addColumn<QtVideo::MapMode>("initialMapMode"); + + // clang-format off + QTest::addRow("defaulFrame.notMapped") << &tst_QVideoFrameBackend::createDefaultFrame + << QtVideo::MapMode::NotMapped; + QTest::addRow("defaulFrame.readOnly") << &tst_QVideoFrameBackend::createDefaultFrame + << QtVideo::MapMode::ReadOnly; + + addMediaPlayerFrameTestData([]() + { + QTest::addRow("mediaPlayerFrame.notMapped") + << &tst_QVideoFrameBackend::createMediaPlayerFrame + << QtVideo::MapMode::NotMapped; + QTest::addRow("mediaPlayerFrame.readOnly") + << &tst_QVideoFrameBackend::createMediaPlayerFrame + << QtVideo::MapMode::ReadOnly; + }); + + // clang-format on +} + +void tst_QVideoFrameBackend::toImage_retainsThePreviousMappedState() +{ + QFETCH(const FrameCreator, frameCreator); + QFETCH(const QtVideo::MapMode, initialMapMode); + const bool initiallyMapped = initialMapMode != QtVideo::MapMode::NotMapped; + + QVideoFrame frame = std::invoke(frameCreator, this); + QVERIFY(frame.isValid()); + + frame.map(initialMapMode); + QCOMPARE(static_cast<QtVideo::MapMode>(frame.mapMode()), initialMapMode); + + QImage image = frame.toImage(); + QVERIFY(!image.isNull()); + + QCOMPARE(static_cast<QtVideo::MapMode>(frame.mapMode()), initialMapMode); + QCOMPARE(frame.isMapped(), initiallyMapped); +} + +void tst_QVideoFrameBackend::toImage_rendersUpdatedFrame_afterMappingInWriteModeAndModifying_data() +{ + QTest::addColumn<FrameCreator>("frameCreator"); + QTest::addColumn<QtVideo::MapMode>("mapMode"); + + // clang-format off + QTest::addRow("defaulFrame.writeOnly") << &tst_QVideoFrameBackend::createDefaultFrame + << QtVideo::MapMode::WriteOnly; + QTest::addRow("defaulFrame.readWrite") << &tst_QVideoFrameBackend::createDefaultFrame + << QtVideo::MapMode::ReadWrite; + + addMediaPlayerFrameTestData([]() + { + QTest::addRow("mediaPlayerFrame.writeOnly") + << &tst_QVideoFrameBackend::createMediaPlayerFrame + << QtVideo::MapMode::WriteOnly; + QTest::addRow("mediaPlayerFrame.readWrite") + << &tst_QVideoFrameBackend::createMediaPlayerFrame + << QtVideo::MapMode::ReadWrite; + }); + // clang-format on +} + +void tst_QVideoFrameBackend::toImage_rendersUpdatedFrame_afterMappingInWriteModeAndModifying() +{ + QFETCH(const FrameCreator, frameCreator); + QFETCH(const QtVideo::MapMode, mapMode); + + // Arrange + + QVideoFrame frame = std::invoke(frameCreator, this); + QVERIFY(frame.isValid()); + + QImage originalImage = frame.toImage(); + QVERIFY(!originalImage.isNull()); + + // Act: map the frame in write mode and change the top level pixel + frame.map(mapMode); + QVERIFY(frame.isWritable()); + + QCOMPARE_NE(frame.pixelFormat(), QVideoFrameFormat::Format_Invalid); + + const QVideoTextureHelper::TextureDescription *textureDescription = + QVideoTextureHelper::textureDescription(frame.pixelFormat()); + QVERIFY(textureDescription); + + uchar *firstPlaneBits = frame.bits(0); + QVERIFY(firstPlaneBits); + + for (int i = 0; i < textureDescription->strideFactor; ++i) + firstPlaneBits[i] = ~firstPlaneBits[i]; + + frame.unmap(); + + // get an image from modified frame + QImage modifiedImage = frame.toImage(); + + // Assert + + QVERIFY(!frame.isMapped()); + QCOMPARE_NE(originalImage.pixel(0, 0), modifiedImage.pixel(0, 0)); + QCOMPARE(originalImage.pixel(1, 0), modifiedImage.pixel(1, 0)); + QCOMPARE(originalImage.pixel(1, 1), modifiedImage.pixel(1, 1)); +} + +void tst_QVideoFrameBackend::toImage_returnsImage_whenCalledFromSeparateThreadAndWhileRenderingToWindow() +{ + if (qEnvironmentVariable("QTEST_ENVIRONMENT").toLower() == "ci") { +#ifdef Q_OS_MACOS + QSKIP("SKIP on macOS because of crash and error \"Failed to create QWindow::MetalSurface. Metal is not supported by any of the GPUs in this system.\""); +#elif defined(Q_OS_ANDROID) + QSKIP("SKIP initTestCase on CI, because of QTBUG-118571"); +#endif + } + // Arrange + QVideoWindow window; + window.show(); + + QVERIFY(QTest::qWaitForWindowExposed(&window)); + + QMediaPlayer player; + player.setVideoOutput(&window); + + const QVideoSink *sink = window.videoSink(); + std::vector<QImage> images; + + // act + connect(sink, &QVideoSink::videoFrameChanged, sink, [&](const QVideoFrame &frame) { + + // Run toImage on separate thread to exercise special code path + QImage image; + auto t = std::thread([&] { image = frame.toImage(); }); + t.join(); + + if (!image.isNull()) + images.push_back(image); + }); + + // Arrange some more + player.setSource(*m_colorsVideo); + player.setLoops(10); + player.play(); + + // assert + QTRY_COMPARE_GE_WITH_TIMEOUT(images.size(), 10u, std::chrono::seconds(60) ); +} + +QTEST_MAIN(tst_QVideoFrameBackend) +#include "tst_qvideoframebackend.moc" diff --git a/tests/auto/integration/qwindowcapturebackend/BLACKLIST b/tests/auto/integration/qwindowcapturebackend/BLACKLIST new file mode 100644 index 000000000..bc176cf98 --- /dev/null +++ b/tests/auto/integration/qwindowcapturebackend/BLACKLIST @@ -0,0 +1,13 @@ +macos ci + +#QTBUG-112827 on Android +#QTBUG-111190, v4l2m2m issues +[recorder_encodesFrames_toValidMediaFile] +linux ci +android ci + +#QTBUG-112827 on Android +#QTBUG-111190, v4l2m2m issues +[recorder_encodesFrames_toValidMediaFile_whenWindowResizes] +linux ci +android ci diff --git a/tests/auto/integration/qwindowcapturebackend/CMakeLists.txt b/tests/auto/integration/qwindowcapturebackend/CMakeLists.txt new file mode 100644 index 000000000..8f633a1da --- /dev/null +++ b/tests/auto/integration/qwindowcapturebackend/CMakeLists.txt @@ -0,0 +1,20 @@ +# Copyright (C) 2023 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +qt_internal_add_test(tst_qwindowcapturebackend + SOURCES + tst_qwindowcapturebackend.cpp + widget.h + widget.cpp + grabber.h + grabber.cpp + fixture.h + fixture.cpp + LIBRARIES + Qt::Multimedia + Qt::Gui + Qt::Widgets + Qt::MultimediaWidgets +) + + diff --git a/tests/auto/integration/qwindowcapturebackend/fixture.cpp b/tests/auto/integration/qwindowcapturebackend/fixture.cpp new file mode 100644 index 000000000..ee130e294 --- /dev/null +++ b/tests/auto/integration/qwindowcapturebackend/fixture.cpp @@ -0,0 +1,234 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include "fixture.h" + +#include <qmediaplayer.h> +#include <qvideowidget.h> +#include <qsystemsemaphore.h> +#include <quuid.h> + +DisableCursor::DisableCursor() +{ + QCursor cursor(Qt::BlankCursor); + QApplication::setOverrideCursor(cursor); +} + +DisableCursor::~DisableCursor() +{ + QGuiApplication::restoreOverrideCursor(); +} + +WindowCaptureFixture::WindowCaptureFixture() +{ + m_session.setWindowCapture(&m_capture); + m_session.setVideoSink(&m_grabber); +} + +QString WindowCaptureFixture::getResultsPath(const QString &fileName) +{ + const QString sep = QStringLiteral("--"); + + QString stem = QCoreApplication::applicationName(); + if (const char *currentTest = QTest::currentTestFunction()) + stem += sep + QString::fromLatin1(currentTest); + + if (const char *currentTag = QTest::currentDataTag()) + stem += sep + QString::fromLatin1(currentTag); + + stem += sep + fileName; + + const QDir resultsDir = qEnvironmentVariable("COIN_CTEST_RESULTSDIR", QDir::tempPath()); + + return resultsDir.filePath(stem); +} + +bool WindowCaptureFixture::compareImages(QImage actual, const QImage &expected, + const QString &fileSuffix) +{ + // Convert to same format so that we can compare images + actual = actual.convertToFormat(expected.format()); + + if (actual == expected) + return true; + + qWarning() << "Image comparison failed."; + qWarning() << "Actual image:"; + qWarning() << actual; + qWarning() << "Expected image:"; + qWarning() << expected; + + const QString actualName = getResultsPath(QStringLiteral("actual%1.png").arg(fileSuffix)); + if (!actual.save(actualName)) + qWarning() << "Failed to save actual file to " << actualName; + + const QString expectedName = getResultsPath(QStringLiteral("expected%1.png").arg(fileSuffix)); + if (!expected.save(expectedName)) + qWarning() << "Failed to save expected file to " << expectedName; + + return false; +} + +bool WindowCaptureWithWidgetFixture::start(QSize size) +{ + // In case of window capture failure, signal the grabber so we can stop + // waiting for frames that will never come. + connect(&m_capture, &QWindowCapture::errorOccurred, &m_grabber, &FrameGrabber::stop); + + m_widget.setSize(size); + + m_widget.show(); + + // Make sure window is in a state that allows it to be found by QWindowCapture. + // Not necessary on Windows, but seems to be necessary on some platforms. + if (!QTest::qWaitForWindowExposed(&m_widget, static_cast<int>(s_testTimeout.count()))) { + qWarning() << "Failed to display widget within timeout"; + return false; + } + + m_captureWindow = findCaptureWindow(m_widget.windowTitle()); + + if (!m_captureWindow.isValid()) + return false; + + m_capture.setWindow(m_captureWindow); + m_capture.setActive(true); + + return true; +} + +QVideoFrame WindowCaptureWithWidgetFixture::waitForFrame(qint64 noOlderThanTime) +{ + const std::vector<QVideoFrame> frames = m_grabber.waitAndTakeFrames(1u, noOlderThanTime); + if (frames.empty()) + return QVideoFrame{}; + + return frames.back(); +} + +QCapturableWindow WindowCaptureWithWidgetFixture::findCaptureWindow(const QString &windowTitle) +{ + QList<QCapturableWindow> allWindows = QWindowCapture::capturableWindows(); + + const auto window = std::find_if(allWindows.begin(), allWindows.end(), + [windowTitle](const QCapturableWindow &win) { + return win.description() == windowTitle; + }); + + // Extra debug output to help understanding if test widget window could not be found + if (window == allWindows.end()) { + qDebug() << "Could not find window" << windowTitle << ". Existing capturable windows:"; + std::for_each(allWindows.begin(), allWindows.end(), [](const QCapturableWindow &win) { + qDebug() << " " << win.description(); + }); + return QCapturableWindow{}; + } + + return *window; +} + +void WindowCaptureWithWidgetAndRecorderFixture::start(QSize size, bool togglePattern) +{ + if (togglePattern) { + // Drive animation + connect(&m_grabber, &FrameGrabber::videoFrameChanged, &m_widget, + &TestWidget::togglePattern); + } + + connect(&m_recorder, &QMediaRecorder::recorderStateChanged, this, + &WindowCaptureWithWidgetAndRecorderFixture::recorderStateChanged); + + m_session.setRecorder(&m_recorder); + m_recorder.setQuality(QMediaRecorder::HighQuality); + m_recorder.setOutputLocation(QUrl::fromLocalFile(m_mediaFile)); + m_recorder.setVideoResolution(size); + + WindowCaptureWithWidgetFixture::start(size); + + m_recorder.record(); +} + +bool WindowCaptureWithWidgetAndRecorderFixture::stop() +{ + m_recorder.stop(); + + const auto recorderStopped = [this] { return m_recorderState == QMediaRecorder::StoppedState; }; + + return QTest::qWaitFor(recorderStopped, s_testTimeout); +} + +bool WindowCaptureWithWidgetAndRecorderFixture::testVideoFilePlayback(const QString &fileName) +{ + QVideoWidget widget; + + QMediaPlayer player; + + bool playing = true; + connect(&player, &QMediaPlayer::playbackStateChanged, this, + [&](QMediaPlayer::PlaybackState state) { + if (state == QMediaPlayer::StoppedState) + playing = false; + }); + + QMediaPlayer::Error error = QMediaPlayer::NoError; + connect(&player, &QMediaPlayer::errorOccurred, this, + [&](QMediaPlayer::Error e, const QString &errorString) { + error = e; + qWarning() << errorString; + }); + + player.setSource(QUrl{ fileName }); + player.setVideoOutput(&widget); + widget.show(); + player.play(); + + const bool completed = QTest::qWaitFor( + [&] { return !playing || error != QMediaPlayer::NoError; }, s_testTimeout); + + return completed && error == QMediaPlayer::NoError; +} + +void WindowCaptureWithWidgetAndRecorderFixture::recorderStateChanged( + QMediaRecorder::RecorderState state) +{ + m_recorderState = state; +} + +bool WindowCaptureWithWidgetInOtherProcessFixture::start() +{ + // In case of window capture failure, signal the grabber so we can stop + // waiting for frames that will never come. + connect(&m_capture, &QWindowCapture::errorOccurred, &m_grabber, &FrameGrabber::stop); + + // Create a new window title that is also used as a semaphore key with less than 30 characters + const QString windowTitle = QString::number(qHash(QUuid::createUuid().toString())); + + QSystemSemaphore windowVisible{ QNativeIpcKey{ windowTitle } }; + + // Start another instance of the test executable and ask it to show a + // its test widget. + m_windowProcess.setArguments({ "--show", windowTitle }); + m_windowProcess.setProgram(QApplication::applicationFilePath()); + m_windowProcess.start(); + + // Make sure window is in a state that allows it to be found by QWindowCapture. + // We do this by waiting for the process to release the semaphore once its window is visible + windowVisible.acquire(); + + m_captureWindow = findCaptureWindow(windowTitle); + + if (!m_captureWindow.isValid()) + return false; + + // Start capturing the out-of-process window + m_capture.setWindow(m_captureWindow); + m_capture.setActive(true); + + // Show in-process widget used to create a reference image + m_widget.show(); + + return true; +} + + +#include "moc_fixture.cpp" diff --git a/tests/auto/integration/qwindowcapturebackend/fixture.h b/tests/auto/integration/qwindowcapturebackend/fixture.h new file mode 100644 index 000000000..2f72c3468 --- /dev/null +++ b/tests/auto/integration/qwindowcapturebackend/fixture.h @@ -0,0 +1,147 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#ifndef WINDOW_CAPTURE_FIXTURE_H +#define WINDOW_CAPTURE_FIXTURE_H + +#include "grabber.h" +#include "widget.h" + +#include <chrono> +#include <qmediacapturesession.h> +#include <qmediarecorder.h> +#include <qobject.h> +#include <qsignalspy.h> +#include <qtest.h> +#include <qvideoframe.h> +#include <qwindowcapture.h> +#include <qprocess.h> + +QT_USE_NAMESPACE + +constexpr inline std::chrono::milliseconds s_testTimeout = std::chrono::seconds(60); + +/*! + Utility used to hide application cursor for image comparison tests. + On Windows, the mouse cursor is captured as part of the window capture. + This and can cause differences when comparing captured images with images + from QWindow::grab() which is used as a reference. +*/ +struct DisableCursor final +{ + DisableCursor(); + ~DisableCursor(); + + DisableCursor(const DisableCursor &) = delete; + DisableCursor &operator=(const DisableCursor &) = delete; +}; + +/*! + Fixture class that orchestrates setup/teardown of window capturing +*/ +class WindowCaptureFixture : public QObject +{ + Q_OBJECT + +public: + WindowCaptureFixture(); + + /*! + Compare two images, ignoring format. + If images differ, diagnostic output is logged and images are saved to file. + */ + static bool compareImages(QImage actual, const QImage &expected, + const QString &fileSuffix = ""); + + QMediaCaptureSession m_session; + QWindowCapture m_capture; + FrameGrabber m_grabber; + + QSignalSpy m_errors{ &m_capture, &QWindowCapture::errorOccurred }; + QSignalSpy m_activations{ &m_capture, &QWindowCapture::activeChanged }; + +private: + /*! + Calculate a result path based upon a single filename. + On CI, the file will be located in COIN_CTEST_RESULTSDIR, and on developer + computers, the file will be located in TEMP. + + The file name is on the form "testCase_testFunction_[dataTag_]fileName" + */ + static QString getResultsPath(const QString &fileName); +}; + +/*! + Fixture class that extends window capture fixture with a capturable widget +*/ +class WindowCaptureWithWidgetFixture : public WindowCaptureFixture +{ + Q_OBJECT + +public: + /*! + Starts capturing and returns true if successful. + + Two phase initialization is used to be able to detect + failure to find widget window as a capturable window. + */ + bool start(QSize size = { 60, 40 }); + + /*! + Waits until the a captured frame is received and returns it + */ + QVideoFrame waitForFrame(qint64 noOlderThanTime = 0); + + DisableCursor m_cursorDisabled; // Avoid mouse cursor causing image differences + TestWidget m_widget; + QCapturableWindow m_captureWindow; + +protected: + static QCapturableWindow findCaptureWindow(const QString &windowTitle); +}; + +class WindowCaptureWithWidgetInOtherProcessFixture : public WindowCaptureWithWidgetFixture +{ + Q_OBJECT + +public: + ~WindowCaptureWithWidgetInOtherProcessFixture() { m_windowProcess.close(); } + + /*! + Create widget in separate process and start capturing its content + */ + bool start(); + + QProcess m_windowProcess; +}; + +class WindowCaptureWithWidgetAndRecorderFixture : public WindowCaptureWithWidgetFixture +{ + Q_OBJECT + +public: + void start(QSize size = { 60, 40 }, bool togglePattern = true); + + /*! + Stop recording. + + Since recorder finalizes the file asynchronously, even after destructors are called, + we need to explicitly wait for the stopped state before ending the test. If we don't + do this, the media file can not be deleted by the QTemporaryDir at destruction. + */ + bool stop(); + + bool testVideoFilePlayback(const QString& fileName); + +public slots: + void recorderStateChanged(QMediaRecorder::RecorderState state); + +public: + QTemporaryDir m_tempDir; + const QString m_mediaFile = m_tempDir.filePath("test.mp4"); + QMediaRecorder m_recorder; + QMediaRecorder::RecorderState m_recorderState = QMediaRecorder::StoppedState; + QSignalSpy m_recorderErrors{ &m_recorder, &QMediaRecorder::errorOccurred }; +}; + +#endif diff --git a/tests/auto/integration/qwindowcapturebackend/grabber.cpp b/tests/auto/integration/qwindowcapturebackend/grabber.cpp new file mode 100644 index 000000000..a7b72aeef --- /dev/null +++ b/tests/auto/integration/qwindowcapturebackend/grabber.cpp @@ -0,0 +1,62 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include "grabber.h" +#include "fixture.h" + +#include <qtest.h> +#include <qvideoframe.h> + +FrameGrabber::FrameGrabber() +{ + const auto copyFrame = [this](const QVideoFrame &frame) { m_frames.push_back(frame); }; + + connect(this, &QVideoSink::videoFrameChanged, this, copyFrame, Qt::DirectConnection); +} + +const std::vector<QVideoFrame> &FrameGrabber::getFrames() const +{ + return m_frames; +} + +std::vector<QVideoFrame> FrameGrabber::waitAndTakeFrames(size_t minCount, qint64 noOlderThanTime) +{ + m_frames.clear(); + + const auto enoughFramesOrStopped = [this, minCount, noOlderThanTime]() -> bool { + if (m_stopped) + return true; // Stop waiting + + if (noOlderThanTime > 0) { + // Reject frames older than noOlderThanTime + const auto newEnd = std::remove_if(m_frames.begin(), m_frames.end(), + [noOlderThanTime](const QVideoFrame &frame) { + return frame.startTime() <= noOlderThanTime; + }); + m_frames.erase(newEnd, m_frames.end()); + } + + return m_frames.size() >= minCount; + }; + + if (!QTest::qWaitFor(enoughFramesOrStopped, s_testTimeout)) + return {}; + + if (m_stopped) + return {}; + + return std::exchange(m_frames, {}); +} + +bool FrameGrabber::isStopped() const +{ + return m_stopped; +} + +void FrameGrabber::stop() +{ + qWarning() << "Stopping grabber"; + m_stopped = true; +} + +#include "moc_grabber.cpp" diff --git a/tests/auto/integration/qwindowcapturebackend/grabber.h b/tests/auto/integration/qwindowcapturebackend/grabber.h new file mode 100644 index 000000000..e997ff954 --- /dev/null +++ b/tests/auto/integration/qwindowcapturebackend/grabber.h @@ -0,0 +1,43 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#ifndef WINDOW_CAPTURE_GRABBER_H +#define WINDOW_CAPTURE_GRABBER_H + +#include <qvideosink.h> +#include <vector> + +QT_USE_NAMESPACE + +/*! + The FrameGrabber stores frames that arrive from the window capture, + and is used to inspect captured frames in the tests. +*/ +class FrameGrabber : public QVideoSink +{ + Q_OBJECT + +public: + FrameGrabber(); + + const std::vector<QVideoFrame> &getFrames() const; + + /*! + Wait for at least \a minCount frames that are no older than noOlderThanTime. + + Returns empty if not enough frames arrived, or if grabber was stopped before global timeout + elapsed. + */ + std::vector<QVideoFrame> waitAndTakeFrames(size_t minCount, qint64 noOlderThanTime = 0); + + bool isStopped() const; + +public slots: + void stop(); + +private: + std::vector<QVideoFrame> m_frames; + bool m_stopped = false; +}; + +#endif diff --git a/tests/auto/integration/qwindowcapturebackend/tst_qwindowcapturebackend.cpp b/tests/auto/integration/qwindowcapturebackend/tst_qwindowcapturebackend.cpp new file mode 100644 index 000000000..6809f81a8 --- /dev/null +++ b/tests/auto/integration/qwindowcapturebackend/tst_qwindowcapturebackend.cpp @@ -0,0 +1,278 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +// TESTED_COMPONENT=src/multimedia + +#include "fixture.h" +#include "widget.h" + +#include <qmediarecorder.h> +#include <qpainter.h> +#include <qsignalspy.h> +#include <qtest.h> +#include <qwindowcapture.h> +#include <qcommandlineparser.h> + +#include <chrono> +#include <vector> + +using std::chrono::duration_cast; +using std::chrono::high_resolution_clock; +using std::chrono::microseconds; + +QT_USE_NAMESPACE + +class tst_QWindowCaptureBackend : public QObject +{ + Q_OBJECT + +private slots: + static void initTestCase() + { +#ifdef Q_OS_ANDROID + QSKIP("Feature does not work on Android"); +#endif +#if defined(Q_OS_LINUX) + if (qEnvironmentVariable("QTEST_ENVIRONMENT").toLower() == "ci" + && qEnvironmentVariable("XDG_SESSION_TYPE").toLower() != "x11") + QSKIP("Skip on wayland; to be fixed"); +#elif defined(Q_OS_MACOS) + if (qEnvironmentVariable("QTEST_ENVIRONMENT").toLower() == "ci") + QSKIP("QTBUG-116285: Skip on macOS CI because of permissions issues"); +#endif + + const QWindowCapture capture; + if (capture.error() == QWindowCapture::CapturingNotSupported) + QSKIP("Screen capturing not supported"); + } + + void isActive_returnsFalse_whenNotStarted() + { + const WindowCaptureFixture fixture; + QVERIFY(!fixture.m_capture.isActive()); + } + + void setActive_failsAndEmitEerrorOccurred_whenNoWindowSelected() + { + WindowCaptureFixture fixture; + + fixture.m_capture.setActive(true); + + QVERIFY(!fixture.m_capture.isActive()); + QVERIFY(!fixture.m_errors.empty()); + } + + void setActive_startsWindowCapture_whenCalledWithTrue() + { + WindowCaptureWithWidgetFixture fixture; + QVERIFY(fixture.start()); + + // Ensure that we have received a frame + QVERIFY(fixture.waitForFrame().isValid()); + + QCOMPARE(fixture.m_activations.size(), 1); + QVERIFY(fixture.m_errors.empty()); + } + + void capturedImage_equals_imageFromGrab_data() + { + QTest::addColumn<QSize>("windowSize"); + QTest::newRow("single-pixel-window") << QSize{1, 1}; + QTest::newRow("small-window") << QSize{60, 40}; + QTest::newRow("odd-width-window") << QSize{ 61, 40 }; + QTest::newRow("odd-height-window") << QSize{ 60, 41 }; + QTest::newRow("big-window") << QApplication::primaryScreen()->size(); + } + + void capturedImage_equals_imageFromGrab() + { + QFETCH(QSize, windowSize); + + WindowCaptureWithWidgetFixture fixture; + QVERIFY(fixture.start(windowSize)); + + const QImage expected = fixture.m_widget.grabImage(); + const QImage actual = fixture.waitForFrame().toImage(); + + QVERIFY(fixture.compareImages(actual, expected)); + } + + void capturedImage_changes_whenWindowContentChanges() + { + WindowCaptureWithWidgetFixture fixture; + QVERIFY(fixture.start()); + + const auto startTime = high_resolution_clock::now(); + + const QVideoFrame colorFrame = fixture.waitForFrame(); + QVERIFY(colorFrame.isValid()); + + fixture.m_widget.setDisplayPattern(TestWidget::Grid); + + // Ignore all frames that were grabbed since the colored frame, + // to ensure that we get a frame after we changed display pattern + const high_resolution_clock::duration delay = high_resolution_clock::now() - startTime; + const QVideoFrame gridFrame = fixture.waitForFrame( + colorFrame.endTime() + duration_cast<microseconds>(delay).count()); + + QVERIFY(gridFrame.isValid()); + + // Make sure that the gridFrame has a different content than the colorFrame + QCOMPARE(gridFrame.size(), colorFrame.size()); + QCOMPARE_NE(gridFrame.toImage(), colorFrame.toImage()); + + const QImage actualGridImage = fixture.m_widget.grabImage(); + QVERIFY(fixture.compareImages(gridFrame.toImage(), actualGridImage)); + } + + void sequenceOfCapturedImages_compareEqual_whenWindowContentIsUnchanged() + { + WindowCaptureWithWidgetFixture fixture; + QVERIFY(fixture.start()); + + const std::vector<QVideoFrame> frames = fixture.m_grabber.waitAndTakeFrames(10); + QVERIFY(!frames.empty()); + + QImage firstFrame = frames.front().toImage(); + QVERIFY(!firstFrame.isNull()); + + qsizetype index = 0; + for (const auto &frame : std::as_const(frames)){ + QVERIFY(fixture.compareImages(frame.toImage(), firstFrame, QString::number(index))); + ++index; + } + } + + void recorder_encodesFrames_toValidMediaFile_data() + { + QTest::addColumn<QSize>("windowSize"); + //QTest::newRow("empty-window") << QSize{ 0, 0 }; TODO: Crash + //QTest::newRow("single-pixel-window") << QSize{ 1, 1 }; TODO: Crash + QTest::newRow("small-window") << QSize{ 60, 40 }; + QTest::newRow("odd-width-window") << QSize{ 61, 40 }; + QTest::newRow("odd-height-window") << QSize{ 60, 41 }; + QTest::newRow("big-window") << QSize{ 800, 600 }; + } + + void recorder_encodesFrames_toValidMediaFile() + { +#ifdef Q_OS_LINUX + if (qEnvironmentVariable("QTEST_ENVIRONMENT").toLower() == "ci") + QSKIP("QTBUG-116671: SKIP on linux CI to avoid crashes in ffmpeg. To be fixed."); +#endif + QFETCH(QSize, windowSize); + + WindowCaptureWithWidgetAndRecorderFixture fixture; + fixture.start(windowSize); + + // Wait on grabber to ensure that video recorder also get some frames + fixture.m_grabber.waitAndTakeFrames(60); + + // Wait for recorder finalization + fixture.stop(); + + QVERIFY(fixture.m_recorderErrors.empty()); + QVERIFY(QFile{ fixture.m_mediaFile }.exists()); + QVERIFY(fixture.testVideoFilePlayback(fixture.m_mediaFile)); + } + + void recorder_encodesFrames_toValidMediaFile_whenWindowResizes_data() + { + QTest::addColumn<int>("increment"); + QTest::newRow("shrink") << -1; + QTest::newRow("grow") << 1; + } + + void recorder_encodesFrames_toValidMediaFile_whenWindowResizes() + { +#ifdef Q_OS_LINUX + if (qEnvironmentVariable("QTEST_ENVIRONMENT").toLower() == "ci") + QSKIP("QTBUG-116671: SKIP on linux CI to avoid crashes in ffmpeg. To be fixed."); +#endif + QFETCH(int, increment); + + QSize windowSize = { 200, 150 }; + WindowCaptureWithWidgetAndRecorderFixture fixture; + fixture.start(windowSize, /*toggle pattern*/ false); + + for (qsizetype i = 0; i < 20; ++i) { + windowSize.setWidth(windowSize.width() + increment); + windowSize.setHeight(windowSize.height() + increment); + fixture.m_widget.setSize(windowSize); + + // Wait on grabber to ensure that video recorder also get some frames + fixture.m_grabber.waitAndTakeFrames(1); + } + + // Wait for recorder finalization + fixture.stop(); + + QVERIFY(fixture.m_recorderErrors.empty()); + QVERIFY(QFile{ fixture.m_mediaFile }.exists()); + QVERIFY(fixture.testVideoFilePlayback(fixture.m_mediaFile)); + } + + void windowCapture_capturesWindowsInOtherProcesses() + { + WindowCaptureWithWidgetInOtherProcessFixture fixture; + QVERIFY(fixture.start()); + + // Get reference image from our in-process widget + const QImage expected = fixture.m_widget.grabImage(); + + // Get actual image grabbed from out-of-process widget + const QImage actual = fixture.waitForFrame().toImage(); + + QVERIFY(fixture.compareImages(actual, expected)); + } + + /* + This test is not a requirement per se, but we want all platforms + to behave the same. A reasonable alternative could have been to + treat closed window as a regular 'Stop' capture (not an error). + */ + void windowCapture_stopsWithError_whenProcessCloses() + { + WindowCaptureWithWidgetInOtherProcessFixture fixture; + QVERIFY(fixture.start()); + + // Get capturing started + fixture.m_grabber.waitAndTakeFrames(3); + + // Closing the process waits for it to exit + fixture.m_windowProcess.close(); + + const bool captureFailed = + QTest::qWaitFor([&] { return !fixture.m_errors.empty(); }, s_testTimeout); + + QVERIFY(captureFailed); + } +}; + +int main(int argc, char *argv[]) +{ + QCommandLineParser cmd; + const QCommandLineOption showTestWidget{ QStringList{ "show" }, + "Creates a test widget with given title", + "windowTitle" }; + cmd.addOption(showTestWidget); + cmd.parse({ argv, argv + argc }); + + if (cmd.isSet(showTestWidget)) { + QApplication app{ argc, argv }; + const QString windowTitle = cmd.value(showTestWidget); + const bool result = showCaptureWindow(windowTitle); + return result ? 0 : 1; + } + + // If no special arguments are set, enter the regular QTest main routine + TESTLIB_SELFCOVERAGE_START("tst_QWindowCaptureatioBackend") + QT_PREPEND_NAMESPACE(QTest::Internal::callInitMain)<tst_QWindowCaptureBackend>(); + QApplication app(argc, argv); + app.setAttribute(Qt::AA_Use96Dpi, true); + tst_QWindowCaptureBackend tc; + QTEST_SET_MAIN_SOURCE_PATH return QTest::qExec(&tc, argc, argv); + +} + +#include "tst_qwindowcapturebackend.moc" diff --git a/tests/auto/integration/qwindowcapturebackend/widget.cpp b/tests/auto/integration/qwindowcapturebackend/widget.cpp new file mode 100644 index 000000000..b17487149 --- /dev/null +++ b/tests/auto/integration/qwindowcapturebackend/widget.cpp @@ -0,0 +1,125 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include "widget.h" +#include "fixture.h" + +#include <qapplication.h> +#include <qsystemsemaphore.h> +#include <qtest.h> + + +TestWidget::TestWidget(const QString &uuid, QScreen *screen) +{ + // Give each window a unique title so that we can uniquely identify it + setWindowTitle(uuid); + + setScreen(screen ? screen : QApplication::primaryScreen()); + + // Use frameless hint because on Windows UWP platform, the window titlebar is captured, + // but the reference image acquired by 'QWindow::grab()' does not include titlebar. + // This allows us to do pixel-perfect matching of captured content. + setWindowFlags(Qt::Window | Qt::FramelessWindowHint); + setFixedSize(60, 40); +} + +void TestWidget::setDisplayPattern(Pattern p) +{ + m_pattern = p; + repaint(); +} + +void TestWidget::setSize(QSize size) +{ + if (size == QApplication::primaryScreen()->size()) + setWindowState(Qt::WindowMaximized); + else + setFixedSize(size); +} + +QImage TestWidget::grabImage() +{ + return grab().toImage(); +} + +void TestWidget::togglePattern() +{ + Pattern p = m_pattern == ColoredSquares ? Grid : ColoredSquares; + setDisplayPattern(p); +} + +void TestWidget::paintEvent(QPaintEvent *) +{ + QPainter p(this); + p.setPen(Qt::NoPen); + p.setBrush(Qt::black); + p.drawRect(rect()); + + if (m_pattern == ColoredSquares) + drawColoredSquares(p); + else + drawGrid(p); + + p.end(); +} + +void TestWidget::drawColoredSquares(QPainter &p) +{ + const std::vector<std::vector<Qt::GlobalColor>> colors = { { Qt::red, Qt::green, Qt::blue }, + { Qt::white, Qt::white, Qt::white }, + { Qt::blue, Qt::green, Qt::red } }; + + const QSize squareSize = size() / 3; + QRect rect{ QPoint{ 0, 0 }, squareSize }; + + for (const auto &row : colors) { + for (const auto &color : row) { + p.setBrush(color); + p.drawRect(rect); + rect.moveLeft(rect.left() + rect.width()); + } + rect.moveTo({ 0, rect.bottom() }); + } +} + +void TestWidget::drawGrid(QPainter &p) const +{ + const QSize winSize = size(); + + p.setPen(Qt::white); + + QLine vertical{ QPoint{ 5, 0 }, QPoint{ 5, winSize.height() } }; + while (vertical.x1() < winSize.width()) { + p.drawLine(vertical); + vertical.translate(10, 0); + } + QLine horizontal{ QPoint{ 0, 5 }, QPoint{ winSize.width(), 5 } }; + while (horizontal.y1() < winSize.height()) { + p.drawLine(horizontal); + horizontal.translate(0, 10); + } +} + +bool showCaptureWindow(const QString &windowTitle) +{ + const QNativeIpcKey key{ windowTitle }; + QSystemSemaphore windowVisible(key); + + TestWidget widget{ windowTitle }; + widget.show(); + + // Wait for window to be visible and suitable for window capturing + const bool result = QTest::qWaitForWindowExposed(&widget, s_testTimeout.count()); + if (!result) + qDebug() << "Failed to show window"; + + // Signal to host process that the window is visible + windowVisible.release(); + + // Keep window visible until a termination signal is received + QApplication::exec(); + + return result; +} + +#include "moc_widget.cpp" diff --git a/tests/auto/integration/qwindowcapturebackend/widget.h b/tests/auto/integration/qwindowcapturebackend/widget.h new file mode 100644 index 000000000..56427a566 --- /dev/null +++ b/tests/auto/integration/qwindowcapturebackend/widget.h @@ -0,0 +1,43 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#ifndef WINDOW_CAPTURE_WIDGET_H +#define WINDOW_CAPTURE_WIDGET_H + +#include <qwidget.h> +#include <qscreen.h> +#include <qpainter.h> +#include <quuid.h> + +/*! + Window capable of drawing test patterns used for capture tests + */ +class TestWidget : public QWidget +{ + Q_OBJECT + +public: + enum Pattern { ColoredSquares, Grid }; + + TestWidget(const QString &uuid = QUuid::createUuid().toString(), QScreen *screen = nullptr); + + void setDisplayPattern(Pattern p); + void setSize(QSize size); + QImage grabImage(); + +public slots: + void togglePattern(); + +protected: + void paintEvent(QPaintEvent *) override; + +private: + void drawColoredSquares(QPainter &p); + void drawGrid(QPainter &p) const; + + Pattern m_pattern = ColoredSquares; +}; + +bool showCaptureWindow(const QString &windowTitle); + +#endif diff --git a/tests/auto/integration/shared/mediabackendutils.h b/tests/auto/integration/shared/mediabackendutils.h new file mode 100644 index 000000000..142c01c01 --- /dev/null +++ b/tests/auto/integration/shared/mediabackendutils.h @@ -0,0 +1,64 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#ifndef MEDIABACKENDUTILS_H +#define MEDIABACKENDUTILS_H + +#include <QtTest/qtestcase.h> +#include <private/qplatformmediaintegration_p.h> + +inline bool isGStreamerPlatform() +{ + return QPlatformMediaIntegration::instance()->name() == "gstreamer"; +} + +inline bool isDarwinPlatform() +{ + return QPlatformMediaIntegration::instance()->name() == "darwin"; +} + +inline bool isAndroidPlatform() +{ + return QPlatformMediaIntegration::instance()->name() == "android"; +} + +inline bool isFFMPEGPlatform() +{ + return QPlatformMediaIntegration::instance()->name() == "ffmpeg"; +} + +inline bool isWindowsPlatform() +{ + return QPlatformMediaIntegration::instance()->name() == "windows"; +} + +inline bool isCI() +{ + return qEnvironmentVariable("QTEST_ENVIRONMENT").toLower() == "ci"; +} + +#define QSKIP_GSTREAMER(message) \ + do { \ + if (isGStreamerPlatform()) \ + QSKIP(message); \ + } while (0) + +#define QSKIP_IF_NOT_FFMPEG() \ + do { \ + if (!isFFMPEGPlatform()) \ + QSKIP("Feature is only supported on FFmpeg"); \ + } while (0) + +#define QSKIP_FFMPEG(message) \ + do { \ + if (isFFMPEGPlatform()) \ + QSKIP(message); \ + } while (0) + +#define QEXPECT_FAIL_GSTREAMER(dataIndex, comment, mode) \ + do { \ + if (isGStreamerPlatform()) \ + QEXPECT_FAIL(dataIndex, comment, mode); \ + } while (0) + +#endif // MEDIABACKENDUTILS_H diff --git a/tests/auto/integration/shared/mediafileselector.h b/tests/auto/integration/shared/mediafileselector.h index 984da6e2b..aa192f3e9 100644 --- a/tests/auto/integration/shared/mediafileselector.h +++ b/tests/auto/integration/shared/mediafileselector.h @@ -1,71 +1,179 @@ -/**************************************************************************** -** -** Copyright (C) 2017 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the test suite of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:GPL-EXCEPT$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 3 as published by the Free Software -** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ +// Copyright (C) 2017 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #ifndef MEDIAFILESELECTOR_H #define MEDIAFILESELECTOR_H -#include <QMediaContent> -#include <QMediaPlayer> +#include <QUrl> +#include <qmediaplayer.h> +#include <qaudiooutput.h> +#include <qvideosink.h> +#include <qsignalspy.h> +#include <qfileinfo.h> +#include <qtest.h> +#include <private/qmultimediautils_p.h> + +#include <unordered_map> QT_BEGIN_NAMESPACE -namespace MediaFileSelector { +using MaybeUrl = QMaybe<QUrl, QString>; + +#define CHECK_SELECTED_URL(maybeUrl) \ + if (!maybeUrl) \ + QSKIP((QLatin1String("\nUnable to select none of the media candidates:\n") + maybeUrl.error()) \ + .toLocal8Bit() \ + .data()) -static QMediaContent selectMediaFile(const QStringList& mediaCandidates) +class MediaFileSelector { - QMediaPlayer player; +public: + int failedSelectionsCount() const { return m_failedSelectionsCount; } - QSignalSpy errorSpy(&player, SIGNAL(error(QMediaPlayer::Error))); + QString dumpErrors() const + { + QStringList failedMedias; + for (const auto &mediaToError : m_mediaToErrors) + if (!mediaToError.second.isEmpty()) + failedMedias.emplace_back(mediaToError.first); - for (const QString &s : mediaCandidates) { - QFileInfo mediaFile(s); - if (!mediaFile.exists()) - continue; - QMediaContent media = QMediaContent(QUrl::fromLocalFile(mediaFile.absoluteFilePath())); - player.setMedia(media); - player.play(); + failedMedias.sort(); + return dumpErrors(failedMedias); + } - for (int i = 0; i < 2000 && player.mediaStatus() != QMediaPlayer::BufferedMedia && errorSpy.isEmpty(); i+=50) { - QTest::qWait(50); + template <typename... Media> + MaybeUrl select(Media... media) + { + return select({ std::move(nativeFileName(media))... }); + } + + MaybeUrl select(const QStringList &candidates) + { + QUrl foundUrl; + for (const auto &media : candidates) { + auto emplaceRes = m_mediaToErrors.try_emplace(media, QString()); + if (emplaceRes.second) { + auto maybeUrl = selectMediaFile(media); + if (!maybeUrl) { + Q_ASSERT(!maybeUrl.error().isEmpty()); + emplaceRes.first->second = maybeUrl.error(); + } + } + + if (foundUrl.isEmpty() && emplaceRes.first->second.isEmpty()) + foundUrl = media; } - if (player.mediaStatus() == QMediaPlayer::BufferedMedia && errorSpy.isEmpty()) { - return media; + if (!foundUrl.isEmpty()) + return foundUrl; + + ++m_failedSelectionsCount; + return { QUnexpect{}, dumpErrors(candidates) }; + } + +private: + QString dumpErrors(const QStringList &medias) const + { + using namespace Qt::StringLiterals; + QString result; + + for (const auto &media : medias) { + auto it = m_mediaToErrors.find(media); + if (it != m_mediaToErrors.end() && !it->second.isEmpty()) + result.append("\t"_L1) + .append(it->first) + .append(": "_L1) + .append(it->second) + .append("\n"_L1); } - errorSpy.clear(); + + return result; + } + + static MaybeUrl selectMediaFile(QString media) + { + if (qEnvironmentVariableIsSet("QTEST_SKIP_MEDIA_VALIDATION")) + return QUrl(media); + + using namespace Qt::StringLiterals; + + QAudioOutput audioOutput; + QVideoSink videoOutput; + QMediaPlayer player; + player.setAudioOutput(&audioOutput); + player.setVideoOutput(&videoOutput); + + player.setSource(media); + player.play(); + + const auto waitingFinished = QTest::qWaitFor([&]() { + if (player.error() != QMediaPlayer::NoError) + return true; + + switch (player.mediaStatus()) { + case QMediaPlayer::BufferingMedia: + case QMediaPlayer::BufferedMedia: + case QMediaPlayer::EndOfMedia: + case QMediaPlayer::InvalidMedia: + return true; + + default: + return false; + } + }); + + auto enumValueToString = [](auto enumValue) { + return QString(QMetaEnum::fromType<decltype(enumValue)>().valueToKey(enumValue)); + }; + + if (!waitingFinished) + return { QUnexpect{}, + "The media got stuck in the status "_L1 + + enumValueToString(player.mediaStatus()) }; + + if (player.mediaStatus() == QMediaPlayer::InvalidMedia) + return { QUnexpect{}, + "Unable to load the media. Error ["_L1 + enumValueToString(player.error()) + + " "_L1 + player.errorString() + "]"_L1 }; + + if (player.error() != QMediaPlayer::NoError) + return { QUnexpect{}, + "Unable to start playing the media, codecs issues. Error ["_L1 + + enumValueToString(player.error()) + " "_L1 + player.errorString() + + "]"_L1 }; + + return QUrl(media); } - return QMediaContent(); -} + QString nativeFileName(const QString &media) + { +#ifdef Q_OS_ANDROID + auto it = m_nativeFiles.find(media); + if (it != m_nativeFiles.end()) + return it->second->fileName(); + + QFile file(media); + if (file.open(QIODevice::ReadOnly)) { + m_nativeFiles.insert({ media, std::unique_ptr<QTemporaryFile>(QTemporaryFile::createNativeFile(file))}); + return m_nativeFiles[media]->fileName(); + } + qWarning() << "Failed to create temporary file"; +#endif // Q_OS_ANDROID + + return media; + } -} // MediaFileSelector namespace +private: +#ifdef Q_OS_ANDROID + std::unordered_map<QString, std::unique_ptr<QTemporaryFile>> m_nativeFiles; +#endif + std::unordered_map<QString, QString> m_mediaToErrors; + int m_failedSelectionsCount = 0; +}; QT_END_NAMESPACE +Q_DECLARE_METATYPE(MaybeUrl) + #endif diff --git a/tests/auto/integration/shared/testvideosink.h b/tests/auto/integration/shared/testvideosink.h new file mode 100644 index 000000000..b14c819c5 --- /dev/null +++ b/tests/auto/integration/shared/testvideosink.h @@ -0,0 +1,69 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#ifndef TESTVIDEOSINK_H +#define TESTVIDEOSINK_H + +#include <qvideosink.h> +#include <qvideoframe.h> +#include <qelapsedtimer.h> +#include <qsignalspy.h> +#include <chrono> + +QT_BEGIN_NAMESPACE + +/* + This is a simple video surface which records all presented frames. +*/ +class TestVideoSink : public QVideoSink +{ + Q_OBJECT +public: + explicit TestVideoSink(bool storeFrames = false) : m_storeFrames(storeFrames) + { + connect(this, &QVideoSink::videoFrameChanged, this, &TestVideoSink::addVideoFrame); + connect(this, &QVideoSink::videoFrameChanged, this, &TestVideoSink::videoFrameChangedSync); + } + + QVideoFrame waitForFrame() + { + QSignalSpy spy(this, &TestVideoSink::videoFrameChangedSync); + return spy.wait() ? spy.at(0).at(0).value<QVideoFrame>() : QVideoFrame{}; + } + + void setStoreFrames(bool storeFrames = true) { m_storeFrames = storeFrames; } + +private Q_SLOTS: + void addVideoFrame(const QVideoFrame &frame) + { + if (!m_elapsedTimer.isValid()) + m_elapsedTimer.start(); + else + m_elapsedTimer.restart(); + + if (m_storeFrames) + m_frameList.append(frame); + + if (frame.isValid()) + m_frameTimes.emplace_back(std::chrono::microseconds(frame.startTime())); + + ++m_totalFrames; + } + +signals: + void videoFrameChangedSync(const QVideoFrame &frame); + +public: + QList<QVideoFrame> m_frameList; + int m_totalFrames = 0; // used instead of the list when frames are not stored + QElapsedTimer m_elapsedTimer; + using TimePoint = std::chrono::time_point<std::chrono::high_resolution_clock>; + std::vector<TimePoint> m_frameTimes; + +private: + bool m_storeFrames; +}; + +QT_END_NAMESPACE + +#endif // TESTVIDEOSINK_H |