diff options
Diffstat (limited to 'tests/auto/integration/qmediaplayerbackend')
29 files changed, 4085 insertions, 1015 deletions
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..883330513 --- /dev/null +++ b/tests/auto/integration/qmediaplayerbackend/fixture.h @@ -0,0 +1,95 @@ +// 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>>; + +struct TestSubtitleSink : QObject +{ + Q_OBJECT + +public Q_SLOTS: + void addSubtitle(QString string) + { + QMetaObject::invokeMethod(this, [this, string = std::move(string)]() mutable { + subtitles.append(std::move(string)); + }); + } + +public: + QStringList subtitles; +}; + +#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/15s.mkv b/tests/auto/integration/qmediaplayerbackend/testdata/15s.mkv Binary files differnew file mode 100644 index 000000000..80ee0f923 --- /dev/null +++ b/tests/auto/integration/qmediaplayerbackend/testdata/15s.mkv 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/multitrack-subtitle-start-at-zero.mkv b/tests/auto/integration/qmediaplayerbackend/testdata/multitrack-subtitle-start-at-zero.mkv Binary files differnew file mode 100644 index 000000000..1962f00c1 --- /dev/null +++ b/tests/auto/integration/qmediaplayerbackend/testdata/multitrack-subtitle-start-at-zero.mkv diff --git a/tests/auto/integration/qmediaplayerbackend/testdata/multitrack.mkv b/tests/auto/integration/qmediaplayerbackend/testdata/multitrack.mkv Binary files differnew file mode 100644 index 000000000..a3c2e9bb9 --- /dev/null +++ b/tests/auto/integration/qmediaplayerbackend/testdata/multitrack.mkv 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/testdata/subtitletest.mkv b/tests/auto/integration/qmediaplayerbackend/testdata/subtitletest.mkv Binary files differnew file mode 100644 index 000000000..2051e4df5 --- /dev/null +++ b/tests/auto/integration/qmediaplayerbackend/testdata/subtitletest.mkv diff --git a/tests/auto/integration/qmediaplayerbackend/tst_qmediaplayerbackend.cpp b/tests/auto/integration/qmediaplayerbackend/tst_qmediaplayerbackend.cpp index b0d84b836..b212b4b63 100644 --- a/tests/auto/integration/qmediaplayerbackend/tst_qmediaplayerbackend.cpp +++ b/tests/auto/integration/qmediaplayerbackend/tst_qmediaplayerbackend.cpp @@ -1,48 +1,73 @@ -/**************************************************************************** -** -** 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 <qaudiodevice.h> +#include <qvideosink.h> +#include <qvideoframe.h> +#include <qaudiooutput.h> +#include <qmediadevices.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 +79,1991 @@ 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_initializesExpectedDefaultState(); + void setSource_initializesExpectedDefaultState_data(); + 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_resetsState_whenCalledWithEmptyUrl_data(); + 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 pause_initializesExpectedDefaultState(); + void pause_initializesExpectedDefaultState_data(); + void pause_doesNotAdvancePosition(); + void pause_playback_resumesFromPausedPosition(); + + 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 play_succeedsFromSourceDevice(); + void play_succeedsFromSourceDevice_data(); + + void stop_entersStoppedState_whenPlayerWasPaused(); + void stop_entersStoppedState_whenPlayerWasPaused_data(); + void stop_setsPositionToZero_afterPlayingToEndOfMedia(); + + void playbackRate_returnsOne_byDefault(); + void setPlaybackRate_changesPlaybackRateAndEmitsSignal_data(); + void setPlaybackRate_changesPlaybackRateAndEmitsSignal(); + void setPlaybackRate_changesPlaybackDuration(); + void setPlaybackRate_changesPlaybackDuration_data(); + + 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(); + + void setVideoOutput_doesNotStopPlayback_data(); + void setVideoOutput_doesNotStopPlayback(); + void setAudioOutput_doesNotStopPlayback_data(); + void setAudioOutput_doesNotStopPlayback(); + void swapAudioDevice_doesNotStopPlayback_data(); + void swapAudioDevice_doesNotStopPlayback(); + + void play_readsSubtitle(); + void multiTrack_validateMetadata(); + void play_readsSubtitle_fromMultiTrack(); + void play_readsSubtitle_fromMultiTrack_data(); + + void setActiveSubtitleTrack_switchesSubtitles(); + void setActiveSubtitleTrack_switchesSubtitles_data(); + + void setActiveVideoTrack_switchesVideoTrack(); + + void disablingAllTracks_doesNotStopPlayback(); + void disablingAllTracks_beforeTracksChanged_doesNotStopPlayback(); private: - QMediaContent selectVideoFile(const QStringList& mediaCandidates); - bool isWavSupported(); - - //one second local wav file - QMediaContent localWavFile; - QMediaContent localWavFile2; - QMediaContent localVideoFile; - QMediaContent localCompressedSoundFile; - QMediaContent localFileWithMetadata; + QUrl selectVideoFile(const QStringList &mediaCandidates); - 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{}; + MaybeUrl m_15sVideo = QUnexpect{}; + MaybeUrl m_subtitleVideo = QUnexpect{}; + MaybeUrl m_multitrackVideo = QUnexpect{}; + MaybeUrl m_multitrackSubtitleStartsAtZeroVideo = 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(); + + return temporaryFile; +} -class ProbeDataHandler : public QObject +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(); -void tst_QMediaPlayerBackend::init() +#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 + + 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 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"); - return QMediaContent(); +#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"); + + 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"); + m_15sVideo = m_mediaSelector.select("qrc:/testdata/15s.mkv"); + m_subtitleVideo = m_mediaSelector.select("qrc:/testdata/subtitletest.mkv"); + m_multitrackVideo = m_mediaSelector.select("qrc:/testdata/multitrack.mkv"); + m_multitrackSubtitleStartsAtZeroVideo = + m_mediaSelector.select("qrc:/testdata/multitrack-subtitle-start-at-zero.mkv"); + + 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() { - QMediaPlayer player; - if (!player.isAvailable()) - QSKIP("Media player service is not available"); +#ifdef QT_FEATURE_network + UnResponsiveRtspServer server; + QVERIFY(server.listen()); - qRegisterMetaType<QMediaContent>(); + auto player = std::make_unique<QMediaPlayer>(); + player->setSource(server.address()); - localWavFile = MediaFileSelector::selectMediaFile(QStringList() << QFINDTESTDATA("testdata/test.wav")); - localWavFile2 = MediaFileSelector::selectMediaFile(QStringList() << QFINDTESTDATA("testdata/_test.wav"));; + QVERIFY(server.waitForConnection()); - QStringList mediaCandidates; - mediaCandidates << QFINDTESTDATA("testdata/colors.mp4"); -#ifndef SKIP_OGV_TEST - mediaCandidates << QFINDTESTDATA("testdata/colors.ogv"); + // 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 - localVideoFile = MediaFileSelector::selectMediaFile(mediaCandidates); +} - mediaCandidates.clear(); - mediaCandidates << QFINDTESTDATA("testdata/nokia-tune.mp3"); - mediaCandidates << QFINDTESTDATA("testdata/nokia-tune.mkv"); - localCompressedSoundFile = MediaFileSelector::selectMediaFile(mediaCandidates); +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; +} - localFileWithMetadata = MediaFileSelector::selectMediaFile(QStringList() << QFINDTESTDATA("testdata/nokia-tune.mp3")); +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; - qgetenv("QT_TEST_CI").toInt(&m_inCISystem,10); + 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::cleanup() +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::construction() +void tst_QMediaPlayerBackend::setSource_emitsError_whenCalledWithInvalidFile() { - QMediaPlayer player; - QTRY_VERIFY(player.isAvailable()); + 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::loadMedia() +void tst_QMediaPlayerBackend::setSource_emitsMediaStatusChange_whenCalledWithInvalidFile() { - if (!isWavSupported()) - QSKIP("Sound format is not supported"); + m_fixture->player.setSource({ "Some not existing media" }); + QTRY_COMPARE_EQ(m_fixture->player.error(), QMediaPlayer::ResourceError); - QMediaPlayer player; + QCOMPARE_EQ(m_fixture->mediaStatusChanged, + SignalList({ { QMediaPlayer::LoadingMedia }, { QMediaPlayer::InvalidMedia } })); +} - QCOMPARE(player.state(), QMediaPlayer::StoppedState); - QCOMPARE(player.mediaStatus(), QMediaPlayer::NoMedia); +void tst_QMediaPlayerBackend::setSource_doesNotEmitPlaybackStateChange_whenCalledWithInvalidFile() +{ + m_fixture->player.setSource({ "Some not existing media" }); + QTRY_COMPARE_EQ(m_fixture->player.error(), QMediaPlayer::ResourceError); - 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->playbackStateChanged.empty()); +} - player.setMedia(localWavFile); +void tst_QMediaPlayerBackend::setSource_setsSourceMediaStatusAndError_whenCalledWithInvalidFile() +{ + const QUrl invalidFile{ "Some not existing media" }; - QCOMPARE(player.state(), QMediaPlayer::StoppedState); + m_fixture->player.setSource(invalidFile); + QTRY_COMPARE_EQ(m_fixture->player.error(), QMediaPlayer::ResourceError); - QVERIFY(player.mediaStatus() != QMediaPlayer::NoMedia); - QVERIFY(player.mediaStatus() != QMediaPlayer::InvalidMedia); - QVERIFY(player.media() == localWavFile); - QVERIFY(player.currentMedia() == localWavFile); + MediaPlayerState expectedState = MediaPlayerState::defaultState(); + expectedState.source = invalidFile; + expectedState.mediaStatus = QMediaPlayer::InvalidMedia; + expectedState.error = QMediaPlayer::ResourceError; - 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); + const MediaPlayerState actualState{ m_fixture->player }; - QTRY_COMPARE(player.mediaStatus(), QMediaPlayer::LoadedMedia); + COMPARE_MEDIA_PLAYER_STATE_EQ(actualState, expectedState); +} + +void tst_QMediaPlayerBackend::setSource_initializesExpectedDefaultState() +{ + QFETCH(MaybeUrl, url); + CHECK_SELECTED_URL(url); + + QMediaPlayer &player = m_fixture->player; + player.setSource(*url); + + MediaPlayerState expectedState = MediaPlayerState::defaultState(); + expectedState.source = *url; + expectedState.mediaStatus = QMediaPlayer::LoadingMedia; + + if (isGStreamerPlatform()) { + // gstreamer initializes the tracks + expectedState.audioTracks = std::nullopt; + expectedState.videoTracks = std::nullopt; + expectedState.activeAudioTrack = std::nullopt; + expectedState.activeVideoTrack = std::nullopt; + expectedState.hasAudio = std::nullopt; + expectedState.hasVideo = std::nullopt; + + expectedState.isSeekable = true; + } - QVERIFY(player.isAudioAvailable()); - QVERIFY(!player.isVideoAvailable()); + const MediaPlayerState actualState{ m_fixture->player }; + COMPARE_MEDIA_PLAYER_STATE_EQ(actualState, expectedState); } -void tst_QMediaPlayerBackend::unloadMedia() +void tst_QMediaPlayerBackend::setSource_initializesExpectedDefaultState_data() { - if (!isWavSupported()) - QSKIP("Sound format is not supported"); + QTest::addColumn<MaybeUrl>("url"); - QMediaPlayer player; - player.setNotifyInterval(50); + QTest::addRow("with wave file") << m_localWavFile; + QTest::addRow("with video file") << m_localVideoFile; + QTest::addRow("with av1 file") << m_av1File; + QTest::addRow("with compressed sound file") << m_localCompressedSoundFile; +} + +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 +} + +void tst_QMediaPlayerBackend::setSource_changesSourceAndMediaStatus_whenCalledWithValidFile() +{ + CHECK_SELECTED_URL(m_localVideoFile); + + m_fixture->player.setSource(*m_localVideoFile); + + QCOMPARE_EQ(m_fixture->mediaStatusChanged, SignalList({ { QMediaPlayer::LoadingMedia } })); + + 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::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::setSource_stopsAndEntersErrorState_whenPlayerWasPlaying() +{ + 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")); - 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))); + QTRY_COMPARE(m_fixture->player.playbackState(), QMediaPlayer::StoppedState); + QTRY_COMPARE(m_fixture->player.mediaStatus(), QMediaPlayer::InvalidMedia); + QTRY_COMPARE(m_fixture->player.error(), QMediaPlayer::ResourceError); - player.setMedia(localWavFile); + QVERIFY(!m_fixture->surface.videoFrame().isValid()); + + QCOMPARE(m_fixture->errorOccurred.size(), 1); + + QTest::qWait(20); + QCOMPARE(m_fixture->framesCount, savedFramesCount); +} + +void tst_QMediaPlayerBackend::setSource_loadsAudioTrack_whenCalledWithValidWavFile() +{ + CHECK_SELECTED_URL(m_localWavFile); + + m_fixture->player.setSource(*m_localWavFile); + + QCOMPARE(m_fixture->player.playbackState(), QMediaPlayer::StoppedState); + + QVERIFY(m_fixture->player.mediaStatus() != QMediaPlayer::NoMedia); + QVERIFY(m_fixture->player.mediaStatus() != QMediaPlayer::InvalidMedia); + QVERIFY(m_fixture->player.source() == *m_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); + + QTRY_COMPARE(m_fixture->player.mediaStatus(), QMediaPlayer::LoadedMedia); + + QVERIFY(m_fixture->player.hasAudio()); + QVERIFY(!m_fixture->player.hasVideo()); +} + +void tst_QMediaPlayerBackend::setSource_resetsState_whenCalledWithEmptyUrl() +{ + QFETCH(MaybeUrl, url); + CHECK_SELECTED_URL(url); + + QMediaPlayer &player = m_fixture->player; + + // Load valid media and start playing + player.setSource(*url); QTRY_COMPARE(player.mediaStatus(), QMediaPlayer::LoadedMedia); - QVERIFY(player.position() == 0); - QVERIFY(player.duration() > 0); + QCOMPARE(player.position(), 0); + + if (isQNXPlatform()) + // QNX mm-renderer updates the duration when 'play' is triggered + QCOMPARE(player.duration(), 0); + else + QCOMPARE_GT(player.duration(), 0); player.play(); - QTRY_VERIFY(player.position() > 0); - QVERIFY(player.duration() > 0); + QTRY_COMPARE_GT(player.position(), 0); + if (isGStreamerPlatform()) + QTRY_COMPARE_GT(player.duration(), 0); // duration update is asynchronous + else + QCOMPARE_GT(player.duration(), 0); - stateSpy.clear(); - statusSpy.clear(); - mediaSpy.clear(); - currentMediaSpy.clear(); - positionSpy.clear(); - durationSpy.clear(); + // Set empty URL and verify that state is fully reset to default + m_fixture->clearSpies(); + + m_fixture->player.setSource(QUrl()); - player.setMedia(QMediaContent()); + QVERIFY(!m_fixture->mediaStatusChanged.isEmpty()); + QVERIFY(!m_fixture->sourceChanged.isEmpty()); - 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()); + MediaPlayerState expectedState = MediaPlayerState::defaultState(); + if (isGStreamerPlatform()) // QTBUG-124005: no buffer progress update + expectedState.bufferProgress = std::nullopt; + const MediaPlayerState actualState{ player }; - QVERIFY(!stateSpy.isEmpty()); - QVERIFY(!statusSpy.isEmpty()); - QVERIFY(!mediaSpy.isEmpty()); - QVERIFY(!currentMediaSpy.isEmpty()); - QVERIFY(!positionSpy.isEmpty()); + COMPARE_MEDIA_PLAYER_STATE_EQ(actualState, expectedState); } -void tst_QMediaPlayerBackend::loadMediaInLoadingState() +void tst_QMediaPlayerBackend::setSource_resetsState_whenCalledWithEmptyUrl_data() { - if (!isWavSupported()) - QSKIP("Sound format is not supported"); + QTest::addColumn<MaybeUrl>("url"); - 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); + QTest::addRow("with wave file") << m_localWavFile; + QTest::addRow("with video file") << m_localVideoFile; } -void tst_QMediaPlayerBackend::playPauseStop() +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 positionSpy(&player, SIGNAL(positionChanged(qint64))); - QSignalSpy errorSpy(&player, SIGNAL(error(QMediaPlayer::Error))); + // 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()); - // Check play() without a media - player.play(); + m_fixture->clearSpies(); - 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); + // Load an audio file, and verify that only audio track is loaded + m_fixture->player.setSource(*m_localWavFile2); - // Check pause() without a media - player.pause(); + QTRY_COMPARE_EQ(m_fixture->player.mediaStatus(), QMediaPlayer::MediaStatus::LoadedMedia); - 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); + 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()); - // The rest is with a valid media + m_fixture->player.play(); - player.setMedia(localWavFile); + // Load video only file, and verify that only video track is loaded + m_fixture->player.setSource(*m_localVideoFile2); - QCOMPARE(player.position(), qint64(0)); + QTRY_COMPARE_EQ(m_fixture->player.mediaStatus(), QMediaPlayer::MediaStatus::LoadedMedia); - player.play(); + QCOMPARE(m_fixture->player.playbackState(), QMediaPlayer::StoppedState); + QVERIFY(m_fixture->player.hasVideo()); + QVERIFY(!m_fixture->player.hasAudio()); + QCOMPARE(m_fixture->errorOccurred.size(), 0); +} - QCOMPARE(player.state(), QMediaPlayer::PlayingState); +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()); +} - QTRY_COMPARE(player.mediaStatus(), QMediaPlayer::BufferedMedia); +void tst_QMediaPlayerBackend::setSource_entersStoppedState_whenPlayerWasPlaying() +{ + 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); + QTRY_COMPARE(m_fixture->mediaStatusChanged, + SignalList({ + { QMediaPlayer::LoadedMedia }, + { QMediaPlayer::BufferingMedia }, + { QMediaPlayer::BufferedMedia }, + { QMediaPlayer::LoadedMedia }, + { QMediaPlayer::LoadingMedia }, + { 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); +} - 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); +void tst_QMediaPlayerBackend::setSource_emitsError_whenSdpFileIsLoaded() +{ +#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 - QTRY_VERIFY(player.position() > 100); - QVERIFY(player.duration() > 0); - QVERIFY(positionSpy.count() > 0); - QVERIFY(positionSpy.last()[0].value<qint64>() > 0); + if (!isFFMPEGPlatform()) + QSKIP("This test is only for FFmpeg backend"); - stateSpy.clear(); - statusSpy.clear(); - positionSpy.clear(); + // Create stream + if (!canCreateRtpStream()) + QSKIP("Rtp stream cannot be created"); + + // Make sure the default whitelist is used + qunsetenv("QT_FFMPEG_PROTOCOL_WHITELIST"); + + 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"); - qint64 positionBeforePause = player.position(); + auto processCloser = qScopeGuard([&process, &sdpUrl]() { + // End stream + process->close(); + + // Remove .sdp file created by VLC + QFile(sdpUrl.toLocalFile()).remove(); + }); + + m_fixture->player.setSource(sdpUrl); + QTRY_COMPARE_EQ(m_fixture->player.error(), QMediaPlayer::ResourceError); +#endif // QT_CONFIG(process) +} + +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; + QTest::addRow("video with subtitle") << m_subtitleVideo << 1 << 1 << 1; + QTest::addRow("video with multiple streams") << m_multitrackVideo << 2 << 2 << 2; +} + +void tst_QMediaPlayerBackend::setSource_updatesTrackProperties() +{ + QFETCH(MaybeUrl, url); + QFETCH(int, numberOfVideoTracks); + QFETCH(int, numberOfAudioTracks); + QFETCH(int, numberOfSubtitleTracks); + + QMediaPlayer &player = m_fixture->player; + + CHECK_SELECTED_URL(url); + + player.setSource(*url); + + QTRY_COMPARE(player.videoTracks().size(), numberOfVideoTracks); + QTRY_COMPARE(player.audioTracks().size(), numberOfAudioTracks); + QTRY_COMPARE(player.subtitleTracks().size(), numberOfSubtitleTracks); +} + +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; + QTest::addRow("video with subtitle") << m_subtitleVideo << 1 << 1 << 1; + QTest::addRow("video with multiple streams") << m_multitrackVideo << 2 << 2 << 2; +} + +void tst_QMediaPlayerBackend::setSource_emitsTracksChanged() +{ + QFETCH(MaybeUrl, url); + QFETCH(int, numberOfVideoTracks); + QFETCH(int, numberOfAudioTracks); + QFETCH(int, numberOfSubtitleTracks); + + QMediaPlayer &player = m_fixture->player; + + CHECK_SELECTED_URL(url); + + QSignalSpy tracksChanged(&player, &QMediaPlayer::tracksChanged); + player.setSource(*url); + + QVERIFY(tracksChanged.wait()); + + QCOMPARE(player.videoTracks().size(), numberOfVideoTracks); + QCOMPARE(player.audioTracks().size(), numberOfAudioTracks); + QCOMPARE(player.subtitleTracks().size(), numberOfSubtitleTracks); +} + +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); +} + +void tst_QMediaPlayerBackend:: + setSourceAndPlay_setCorrectVideoSize_whenVideoHasNonStandardPixelAspectRatio() +{ +#ifdef Q_OS_ANDROID + QSKIP("SKIP initTestCase on CI, because of QTBUG-126428"); +#endif + if (isGStreamerPlatform() && isCI()) + QSKIP("QTBUG-124005: Fails with gstreamer on CI"); + + QFETCH(MaybeUrl, url); + QFETCH(QSize, expectedVideoSize); + + CHECK_SELECTED_URL(url); + + 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(m_fixture->surface.videoSize(), expectedVideoSize); + + m_fixture->player.play(); + + 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)); + +#ifdef Q_OS_ANDROID + QSKIP("frame.toImage will return null image because of QTBUG-108446"); +#endif + + 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); +} + +void tst_QMediaPlayerBackend::pause_doesNotChangePlayerState_whenInvalidFileLoaded() +{ + m_fixture->player.setSource({ "Some not existing media" }); + QTRY_COMPARE_EQ(m_fixture->player.error(), QMediaPlayer::ResourceError); + + const MediaPlayerState expectedState{ m_fixture->player }; + + m_fixture->player.pause(); + + const MediaPlayerState actualState{ m_fixture->player }; + + COMPARE_MEDIA_PLAYER_STATE_EQ(actualState, expectedState); +} + +void tst_QMediaPlayerBackend::pause_doesNothing_whenMediaIsNotLoaded() +{ + m_fixture->player.pause(); + + 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::pause_entersPauseState_whenPlayerWasPlaying() +{ + CHECK_SELECTED_URL(m_localWavFile); + + // 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::pause_initializesExpectedDefaultState() +{ + QFETCH(MaybeUrl, url); + QFETCH(bool, hasVideo); + QFETCH(bool, hasAudio); + CHECK_SELECTED_URL(url); + + if (isFFMPEGPlatform() && url->path().contains("Av1")) + QSKIP("QTBUG-119711: ffmpeg's binaries on CI do not support av1"); + + QMediaPlayer &player = m_fixture->player; + player.setSource(*url); player.pause(); - QCOMPARE(player.state(), QMediaPlayer::PausedState); - QCOMPARE(player.mediaStatus(), QMediaPlayer::BufferedMedia); + QTRY_COMPARE(player.playbackState(), QMediaPlayer::PausedState); - QCOMPARE(stateSpy.count(), 1); - QCOMPARE(stateSpy.last()[0].value<QMediaPlayer::State>(), QMediaPlayer::PausedState); + MediaPlayerState expectedState = MediaPlayerState::defaultState(); + expectedState.source = *url; + expectedState.playbackState = QMediaPlayer::PausedState; + expectedState.isSeekable = true; - QTest::qWait(2000); + expectedState.mediaStatus = std::nullopt; + expectedState.duration = std::nullopt; + expectedState.bufferProgress = std::nullopt; - QVERIFY(qAbs(player.position() - positionBeforePause) < 150); - QCOMPARE(positionSpy.count(), 1); + expectedState.audioTracks = std::nullopt; + expectedState.videoTracks = std::nullopt; + expectedState.metaData = std::nullopt; - stateSpy.clear(); - statusSpy.clear(); + if (hasVideo) { + expectedState.activeVideoTrack = 0; + expectedState.hasVideo = std::nullopt; + } - player.stop(); + if (hasAudio) { + expectedState.activeAudioTrack = 0; + expectedState.hasAudio = std::nullopt; + } - QCOMPARE(player.state(), QMediaPlayer::StoppedState); - QTRY_COMPARE(player.mediaStatus(), QMediaPlayer::LoadedMedia); + const MediaPlayerState actualState{ player }; + COMPARE_MEDIA_PLAYER_STATE_EQ(actualState, expectedState); - 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); + QVERIFY(actualState.mediaStatus == QMediaPlayer::BufferingMedia + || actualState.mediaStatus == QMediaPlayer::BufferedMedia); - //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); + if (hasVideo) + QCOMPARE(actualState.videoTracks->size(), 1); + if (hasAudio) + QCOMPARE(actualState.audioTracks->size(), 1); - stateSpy.clear(); - statusSpy.clear(); - positionSpy.clear(); + QEXPECT_FAIL_GSTREAMER("", "GStreamer doesn't update bufferProgress while paused", Continue); - player.play(); + QTRY_COMPARE_GT(actualState.bufferProgress, 0); +} - 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); +void tst_QMediaPlayerBackend::pause_initializesExpectedDefaultState_data() +{ + QTest::addColumn<MaybeUrl>("url"); + QTest::addColumn<bool>("hasVideo"); + QTest::addColumn<bool>("hasAudio"); + + QTest::addRow("with wave file") << m_localWavFile << false << true; + QTest::addRow("with video file") << m_localVideoFile << true << true; + QTest::addRow("with av1 file") << m_av1File << true << false; + QTest::addRow("with compressed sound file") << m_localCompressedSoundFile << false << true; +} - player.stop(); - stateSpy.clear(); - statusSpy.clear(); - positionSpy.clear(); +void tst_QMediaPlayerBackend::pause_doesNotAdvancePosition() +{ + using namespace std::chrono_literals; - player.setMedia(localWavFile2); + CHECK_SELECTED_URL(m_localVideoFile); - 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); + QMediaPlayer &player = m_fixture->player; + player.setSource(*m_localVideoFile); + + player.pause(); + + QTest::qWait(1s); + + QTRY_COMPARE_EQ(player.position(), 0); +} + +void tst_QMediaPlayerBackend::pause_playback_resumesFromPausedPosition() +{ + using namespace std::chrono_literals; + + CHECK_SELECTED_URL(m_localVideoFile); + + QMediaPlayer &player = m_fixture->player; + player.setSource(*m_localVideoFile); player.play(); - QTRY_VERIFY(player.position() > 100); + QTRY_COMPARE_GT(player.position(), 100); - player.setMedia(localWavFile); + player.pause(); - 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); + qint64 pausePos = player.position(); + QTest::qWait(1s); - stateSpy.clear(); - statusSpy.clear(); - positionSpy.clear(); + QCOMPARE_EQ(player.position(), pausePos); player.play(); - QTRY_VERIFY(player.position() > 100); + // Make sure the media player does not make up for the lost time + m_fixture->positionChanged.wait(); + m_fixture->positionChanged.wait(); - player.setMedia(QMediaContent()); + QCOMPARE_LT(player.position(), pausePos + 500); +} - 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); +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::processEOS() +void tst_QMediaPlayerBackend::play_doesNothing_whenMediaIsNotLoaded() +{ + 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() { - if (!isWavSupported()) - QSKIP("Sound format is not supported"); + 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_VERIFY(m_fixture->player.mediaStatus() == QMediaPlayer::BufferedMedia + || m_fixture->player.mediaStatus() == QMediaPlayer::EndOfMedia); + + QCOMPARE(m_fixture->playbackStateChanged, SignalList({ { QMediaPlayer::PlayingState } })); + + auto expectedMediaStatus = SignalList{ + { QMediaPlayer::LoadingMedia }, + { QMediaPlayer::LoadedMedia }, + { QMediaPlayer::BufferingMedia }, + { QMediaPlayer::BufferedMedia }, + }; + + QTRY_COMPARE_EQ(m_fixture->mediaStatusChanged.first(4), expectedMediaStatus); + + 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 { + QTRY_COMPARE_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); + + QList allowedSignalSequences = { + SignalList{ + { QMediaPlayer::LoadedMedia }, + { QMediaPlayer::BufferingMedia }, + { QMediaPlayer::BufferedMedia }, + { QMediaPlayer::LoadedMedia }, + { QMediaPlayer::NoMedia }, + }, + SignalList{ + { QMediaPlayer::LoadedMedia }, + { QMediaPlayer::BufferingMedia }, + { QMediaPlayer::BufferedMedia }, + { QMediaPlayer::EndOfMedia }, // EndOfMedia can be reached before setSource({}) + { QMediaPlayer::LoadedMedia }, + { QMediaPlayer::NoMedia }, + }, + }; + + QTRY_VERIFY(allowedSignalSequences.contains(m_fixture->mediaStatusChanged)); + + 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.setMedia(localWavFile); + player.setVideoSink(&surface); + // Ignore audio output to check timings accuratelly + // player.setAudioOutput(&output); + + 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); + + auto frame1 = surface.waitForFrame(); + QVERIFY(frame1.isValid()); + QCOMPARE(frame1.size(), QSize(213, 120)); + + 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() +{ +#ifdef Q_OS_ANDROID + QSKIP("SKIP initTestCase on CI, because of QTBUG-126428"); +#endif + 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, 850); + 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::play_succeedsFromSourceDevice() +{ + QFETCH(const MaybeUrl, mediaUrl); + QFETCH(bool, streamOutlivesPlayer); + + CHECK_SELECTED_URL(mediaUrl); + + auto *stream = new QFile(u":"_s + mediaUrl->path()); + + QVERIFY(stream->open(QFile::ReadOnly)); + + QMediaPlayer &player = m_fixture->player; + + player.setSourceDevice(stream); player.play(); + QTRY_COMPARE_GT(player.position(), 100); - //position is reset to start - QTRY_VERIFY(player.position() < 100); - QTRY_VERIFY(positionSpy.count() > 0); - QCOMPARE(positionSpy.first()[0].value<qint64>(), 0); + if (streamOutlivesPlayer) + stream->setParent(&player); + else + delete stream; +} + +void tst_QMediaPlayerBackend::play_succeedsFromSourceDevice_data() +{ + QTest::addColumn<MaybeUrl>("mediaUrl"); + QTest::addColumn<bool>("streamOutlivesPlayer"); + + QTest::addRow("audio file") << m_localWavFile << true; + QTest::addRow("video file") << m_localVideoFile << true; + + // QMediaPlayer crashes when we delete the stream during playback + constexpr bool validateStreamDestructionDuringPlayback = false; + if constexpr (validateStreamDestructionDuringPlayback) { + QTest::addRow("audio file, stream destroyed during playback") << m_localWavFile << false; + QTest::addRow("video file, stream destroyed during playback") << m_localVideoFile << false; + } +} + +void tst_QMediaPlayerBackend::stop_entersStoppedState_whenPlayerWasPaused() +{ + QFETCH(const MaybeUrl, mediaUrl); + + CHECK_SELECTED_URL(mediaUrl); + QMediaPlayer &player = m_fixture->player; - QCOMPARE(player.state(), QMediaPlayer::PlayingState); + // Arrange + player.setSource(*mediaUrl); + player.play(); + QTRY_COMPARE_GT(player.position(), 100); + player.pause(); QTRY_COMPARE(player.mediaStatus(), QMediaPlayer::BufferedMedia); + m_fixture->clearSpies(); + + if (!isGStreamerPlatform()) // Gstreamer may see EOS already + QCOMPARE_GT(player.position(), 100); + + // Act + player.stop(); + + // Assert + QCOMPARE(player.playbackState(), QMediaPlayer::StoppedState); + QTRY_COMPARE(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() && *mediaUrl == *m_localWavFile) { + // QTBUG-124517: for some media types gstreamer does not emit buffer progress messages + } else { + QCOMPARE(m_fixture->bufferProgressChanged, SignalList({ { 0.f } })); + } + + QTRY_COMPARE(m_fixture->player.position(), qint64(0)); - 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); + QTRY_VERIFY(!m_fixture->positionChanged.empty()); + QCOMPARE(m_fixture->positionChanged.last()[0].value<qint64>(), qint64(0)); + QVERIFY(player.duration() > 0); +} + +void tst_QMediaPlayerBackend::stop_entersStoppedState_whenPlayerWasPaused_data() +{ + QTest::addColumn<MaybeUrl>("mediaUrl"); + + QTest::addRow("audio file") << m_localWavFile; + QTest::addRow("video file") << m_localVideoFile; +} + +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::setPlaybackRate_changesPlaybackDuration() +{ + using namespace std::chrono; + using namespace std::chrono_literals; + + CHECK_SELECTED_URL(m_15sVideo); + + // speeding up a 15s file by 3 should result in a duration of 5s + // auto minDuration = 3s; + // auto maxDuration = 7s; + // auto playbackRate = 3.0; + + // speeding up a 15s file by 5 should result in a duration of 3s + auto minDuration = 2s; + auto maxDuration = 4s; + auto playbackRate = 5.0; + + QFETCH(const QLatin1String, testMode); + + QMediaPlayer &player = m_fixture->player; + + if (testMode == "SetRateBeforeSetSource"_L1) + player.setPlaybackRate(playbackRate); + + player.setSource(*m_15sVideo); + + QTRY_COMPARE_EQ(player.mediaStatus(), QMediaPlayer::LoadedMedia); + + auto begin = steady_clock::now(); + + if (testMode == "SetRateBeforePlay"_L1) { + QSKIP_GSTREAMER("FIXME: SetRateBeforeSetSource is currently broken"); + player.setPlaybackRate(playbackRate); + } + + player.play(); + + if (testMode == "SetRateAfterPlay"_L1) + player.setPlaybackRate(playbackRate); + + if (testMode == "SetRateAfterPlaybackStarted"_L1) { + QTRY_COMPARE_GT(player.position(), 50); + player.setPlaybackRate(playbackRate); + } + + QCOMPARE(player.playbackRate(), playbackRate); + + QTRY_COMPARE_EQ_WITH_TIMEOUT(player.playbackState(), QMediaPlayer::StoppedState, 20s); + + auto end = steady_clock::now(); + auto duration = end - begin; + + if (false) + qDebug() << round<milliseconds>(duration); + + QCOMPARE_LT(duration, maxDuration); + QCOMPARE_GT(duration, minDuration); +} + +void tst_QMediaPlayerBackend::setPlaybackRate_changesPlaybackDuration_data() +{ + QTest::addColumn<QLatin1String>("testMode"); + + QTest::addRow("SetRateBeforeSetSource") << "SetRateBeforeSetSource"_L1; + QTest::addRow("SetRateBeforePlay") << "SetRateBeforePlay"_L1; + QTest::addRow("SetRateAfterPlay") << "SetRateAfterPlay"_L1; + QTest::addRow("SetRateAfterPlaybackStarted") << "SetRateAfterPlaybackStarted"_L1; +} + +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"); + + 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); + } + + 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()); - //position stays at the end of file - QCOMPARE(player.position(), player.duration()); - QVERIFY(positionSpy.count() > 0); - QCOMPARE(positionSpy.last()[0].value<qint64>(), 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(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 +2087,8 @@ private slots: void onMediaStatusChanged(QMediaPlayer::MediaStatus status) { if (status == QMediaPlayer::EndOfMedia) { - player-> deleteLater(); - player = 0; + player->deleteLater(); + player = nullptr; } } @@ -613,73 +2100,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 +2137,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 +2313,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 +2326,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 +2361,562 @@ 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); - QMediaPlayer *player = new QMediaPlayer; + QAudioOutput output; + TestVideoSink surface(false); + QMediaPlayer player; - TestVideoSurface *surface = new TestVideoSurface; - player->setVideoOutput(surface); + player.setVideoOutput(&surface); + player.setAudioOutput(&output); + player.setSource(*m_localVideoFile); - QVideoProbe *videoProbe = new QVideoProbe; - QAudioProbe *audioProbe = new QAudioProbe; + QCOMPARE(player.source(), *m_localVideoFile); + QTRY_COMPARE(player.mediaStatus(), QMediaPlayer::LoadedMedia); - 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())); + player.setPosition(0); + player.play(); - if (!videoProbe->setSource(player)) - QSKIP("QVideoProbe is not supported"); - audioProbe->setSource(player); + QCOMPARE(player.error(), QMediaPlayer::NoError); + QCOMPARE(player.playbackState(), QMediaPlayer::PlayingState); + QVERIFY(player.isSeekable()); + QTRY_VERIFY(player.position() > 0); + QCOMPARE(player.source(), *m_localVideoFile); - player->setMedia(localVideoFile); - QTRY_COMPARE(player->mediaStatus(), QMediaPlayer::LoadedMedia); + player.stop(); + + player.setSource(*m_localVideoFile2); - player->pause(); - QTRY_COMPARE(surface->m_frameList.size(), 1); - QVERIFY(!probeHandler.m_frameList.isEmpty()); - QTRY_VERIFY(!probeHandler.m_bufferList.isEmpty()); + QCOMPARE(player.source(), *m_localVideoFile2); + QTRY_COMPARE(player.mediaStatus(), QMediaPlayer::LoadedMedia); + QTRY_VERIFY(player.isSeekable()); - delete player; - QTRY_VERIFY(probeHandler.isVideoFlushCalled); - delete videoProbe; - delete audioProbe; + player.setPosition(0); + player.play(); + + QCOMPARE(player.error(), QMediaPlayer::NoError); + QCOMPARE(player.playbackState(), QMediaPlayer::PlayingState); + QTRY_VERIFY(player.position() > 0); + QCOMPARE(player.source(), *m_localVideoFile2); + + player.stop(); + + 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(); + using namespace std::chrono_literals; + using namespace std::chrono; - // <<< 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(); + constexpr milliseconds expectedVideoDuration = 3000ms; + constexpr milliseconds waitingInterval = 200ms; + constexpr milliseconds maxDuration = expectedVideoDuration + 2000ms; + constexpr milliseconds minDuration = expectedVideoDuration - 100ms; + constexpr milliseconds maxFrameDelay = 2000ms; - // <<< Invalid2 - 1st pass >>> - fileInfo.setFile(QFINDTESTDATA("/testdata/invalid_media2.m3u")); - player.setMedia(QUrl::fromLocalFile(fileInfo.absoluteFilePath())); + surface.m_elapsedTimer.start(); - 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(); + nanoseconds duration = 0ns; - // <<< 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(); + auto waitForPlaybackStateChange = [&]() { + QElapsedTimer timer; + timer.start(); - // <<< Recursive - 1st pass >>> - fileInfo.setFile(QFINDTESTDATA("testdata/recursive_master.m3u")); - player.setMedia(QUrl::fromLocalFile(fileInfo.absoluteFilePath())); + QScopeGuard addDuration([&]() { + duration += duration_cast<nanoseconds>(timer.durationElapsed() * player.playbackRate()); + }); + return spy.wait(waitingInterval); + }; - 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(); + for (int i = 0; !waitForPlaybackStateChange(); ++i) { + player.setPlaybackRate(0.5 * (i % 4 + 1)); - // <<< 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 + QCOMPARE_LE(duration, maxDuration); + + QVERIFY2(surface.m_elapsedTimer.durationElapsed() < maxFrameDelay, + "If the delay is more than 2s, we consider the video playing is hanging."); + + /* 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(); + }*/ + } + + 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"); + QSKIP_GSTREAMER("QTBUG-124005: timing issues"); - QList<QVideoFrame::PixelFormat> formatsRGB; - formatsRGB << QVideoFrame::Format_RGB32 - << QVideoFrame::Format_ARGB32 - << QVideoFrame::Format_RGB565 - << QVideoFrame::Format_BGRA32; + QFETCH(bool, withAudio); + QFETCH(int, positionDeviationMs); - 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; - - 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); - QTest::newRow("RGB & YUV formats") - << formatsRGB + formatsYUV; + 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(); + + 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()); - 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")); + // Act + const QMediaMetaData metadata = m_fixture->player.metaData(); + const QImage thumbnail = metadata.value(key).value<QImage>(); - metadataAvailableSpy.clear(); - metadataChangedSpy.clear(); + // Assert + QCOMPARE_EQ(!thumbnail.isNull(), hasThumbnail); + QCOMPARE_EQ(thumbnail.size(), expectedSize); - player.setMedia(QMediaContent()); + if (hasThumbnail) { + const QPoint center{ expectedSize.width() / 2, expectedSize.height() / 2 }; + const auto centerColor = thumbnail.pixelColor(center); - QVERIFY(!player.isMetaDataAvailable()); - QCOMPARE(metadataAvailableSpy.count(), 1); - QVERIFY(!metadataAvailableSpy.last()[0].toBool()); - QCOMPARE(metadataChangedSpy.count(), 1); - QVERIFY(player.availableMetaData().isEmpty()); + 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); + + if (!isFFMPEGPlatform() && !isDarwinPlatform()) + QSKIP("This test is only for FFmpeg and Darwin backends"); + + m_fixture->player.setSource(*mediaUrl); + QTRY_VERIFY(!m_fixture->metadataChanged.empty()); + + const QMediaMetaData metadata = m_fixture->player.videoTracks().front(); + const bool hdrContent = metadata.value(QMediaMetaData::HasHdrContent).value<bool>(); + + 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,89 +2925,1325 @@ 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"); + + CHECK_SELECTED_URL(m_localVideoFile); - TestVideoSurface surface(false); + 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); } } -bool TestVideoSurface::start(const QVideoSurfaceFormat &format) +void tst_QMediaPlayerBackend::isSeekable() { - if (!isFormatSupported(format)) { - setError(UnsupportedFormatError); - return false; + 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() +{ +#ifdef Q_OS_ANDROID + QSKIP("SKIP initTestCase on CI, because of QTBUG-126428"); +#endif + 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); } - return QAbstractVideoSurface::start(format); + 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); + } } -void TestVideoSurface::stop() +void tst_QMediaPlayerBackend::infiniteLoops() { - QAbstractVideoSurface::stop(); + 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()); + } + + 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); + + QCOMPARE(m_fixture->mediaStatusChanged, + SignalList({ { QMediaPlayer::LoadingMedia }, + { QMediaPlayer::LoadedMedia }, + { QMediaPlayer::BufferingMedia }, + { QMediaPlayer::BufferedMedia }, + { QMediaPlayer::LoadedMedia } })); } -bool TestVideoSurface::present(const QVideoFrame &frame) +void tst_QMediaPlayerBackend::seekOnLoops() { - if (m_storeFrames) - m_frameList.push_back(frame); - m_totalFrames++; - return true; + 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 tst_QMediaPlayerBackend::changeLoopsOnTheFly() +{ + QSKIP_GSTREAMER("QTBUG-123056(?): spuriously failures of the gstreamer backend"); -void ProbeDataHandler::processFrame(const QVideoFrame &frame) + 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); +} + +void tst_QMediaPlayerBackend::seekAfterLoopReset() +{ + 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"); + + 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() +{ + 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() +{ +#ifdef Q_OS_ANDROID + QSKIP("SKIP initTestCase on CI, because of QTBUG-126428"); +#endif + 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 tst_QMediaPlayerBackend::setMedia_setsVideoSinkSize_beforePlaying() +{ + 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); +} + +#if QT_CONFIG(process) +std::unique_ptr<QProcess> tst_QMediaPlayerBackend::createRtpStreamProcess(QString fileName, + QString sdpUrl) +{ + 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 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); +#ifdef Q_OS_ANDROID + QSKIP("frame.toImage will return null image because of QTBUG-108446"); +#endif + 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); +} + +void tst_QMediaPlayerBackend::setVideoOutput_doesNotStopPlayback() +{ + using namespace std::chrono_literals; + + CHECK_SELECTED_URL(m_15sVideo); + + QFETCH(QMediaPlayer::PlaybackState, playbackState); + + TestVideoSink surface(false); + QAudioOutput audioOut; + + QMediaPlayer player; + player.setAudioOutput(&audioOut); + player.setSource(*m_15sVideo); + + switch (playbackState) { + case QMediaPlayer::StoppedState: + break; + case QMediaPlayer::PausedState: + QSKIP_FFMPEG("QTBUG-126014: Test failure with the ffmpeg backend"); + player.pause(); + break; + case QMediaPlayer::PlayingState: + QSKIP_FFMPEG("QTBUG-126014: Test failure with the ffmpeg backend"); + QSKIP_GSTREAMER("QTBUG-124005: Test failure with the gstreamer backend"); + player.play(); + break; + } + + // set video output + QTest::qWait(1s); + player.setVideoOutput(&surface); + + if (playbackState == QMediaPlayer::PlayingState) { + QVideoFrame frame = surface.waitForFrame(); + QCOMPARE(frame.size(), QSize(20, 20)); + } + + // unset video output + QTest::qWait(1s); + player.setVideoOutput(nullptr); + + // wait for play until end + if (playbackState != QMediaPlayer::PlayingState) + player.play(); + + player.setPlaybackRate(5); + QTRY_COMPARE(player.playbackState(), QMediaPlayer::StoppedState); +} + +void tst_QMediaPlayerBackend::setVideoOutput_doesNotStopPlayback_data() { - m_frameList.append(frame); + QTest::addColumn<QMediaPlayer::PlaybackState>("playbackState"); + QTest::newRow("StoppedState") << QMediaPlayer::StoppedState; + QTest::newRow("PausedState") << QMediaPlayer::PausedState; + QTest::newRow("PlayingState") << QMediaPlayer::PlayingState; } -void ProbeDataHandler::processBuffer(const QAudioBuffer &buffer) +void tst_QMediaPlayerBackend::setAudioOutput_doesNotStopPlayback() { - m_bufferList.append(buffer); + QSKIP_FFMPEG("QTBUG-126014: Test failure with the ffmpeg backend"); + + using namespace std::chrono_literals; + + CHECK_SELECTED_URL(m_15sVideo); + QFETCH(QMediaPlayer::PlaybackState, playbackState); + + TestVideoSink surface(false); + QAudioOutput audioOut; + + QMediaPlayer player; + player.setVideoOutput(&surface); + player.setSource(*m_15sVideo); + + switch (playbackState) { + case QMediaPlayer::StoppedState: + break; + case QMediaPlayer::PausedState: + player.pause(); + break; + case QMediaPlayer::PlayingState: + player.play(); + break; + } + + // set audio output + QTest::qWait(1s); + player.setAudioOutput(&audioOut); + + // unset audio output + QTest::qWait(1s); + player.setAudioOutput(nullptr); + + // wait for play until end + if (playbackState != QMediaPlayer::PlayingState) + player.play(); + player.setPlaybackRate(5); + QTRY_COMPARE(player.playbackState(), QMediaPlayer::StoppedState); } -void ProbeDataHandler::flushVideo() +void tst_QMediaPlayerBackend::setAudioOutput_doesNotStopPlayback_data() { - isVideoFlushCalled = true; + QTest::addColumn<QMediaPlayer::PlaybackState>("playbackState"); + QTest::newRow("StoppedState") << QMediaPlayer::StoppedState; + QTest::newRow("PausedState") << QMediaPlayer::PausedState; + QTest::newRow("PlayingState") << QMediaPlayer::PlayingState; } -void ProbeDataHandler::flushAudio() +void tst_QMediaPlayerBackend::swapAudioDevice_doesNotStopPlayback() { + using namespace std::chrono_literals; + const QList<QAudioDevice> outputDevices = QMediaDevices::audioOutputs(); + + if (outputDevices.size() < 2) + QSKIP("swapAudioDevice_doesNotStopPlayback requires two audio output devices"); + + CHECK_SELECTED_URL(m_15sVideo); + QFETCH(QMediaPlayer::PlaybackState, playbackState); + + TestVideoSink surface(false); + QAudioOutput audioOut; + + QMediaPlayer player; + player.setVideoOutput(&surface); + player.setAudioOutput(&audioOut); + player.setSource(*m_15sVideo); + switch (playbackState) { + case QMediaPlayer::StoppedState: + break; + case QMediaPlayer::PausedState: + player.pause(); + break; + case QMediaPlayer::PlayingState: + player.play(); + break; + } + + // swap output device + QTest::qWait(1s); + audioOut.setDevice(outputDevices[0]); + + QTest::qWait(1s); + audioOut.setDevice(outputDevices[1]); + + QTest::qWait(1s); + audioOut.setDevice(outputDevices[0]); + + // wait for play until end + if (playbackState != QMediaPlayer::PlayingState) + player.play(); + player.setPlaybackRate(5); + QTRY_COMPARE(player.playbackState(), QMediaPlayer::StoppedState); +} + +void tst_QMediaPlayerBackend::swapAudioDevice_doesNotStopPlayback_data() +{ + QTest::addColumn<QMediaPlayer::PlaybackState>("playbackState"); + QTest::newRow("StoppedState") << QMediaPlayer::StoppedState; + QTest::newRow("PausedState") << QMediaPlayer::PausedState; + QTest::newRow("PlayingState") << QMediaPlayer::PlayingState; +} + +void tst_QMediaPlayerBackend::play_readsSubtitle() +{ + using namespace std::chrono_literals; + CHECK_SELECTED_URL(m_subtitleVideo); + + QVideoSink &sink = m_fixture->surface; + QMediaPlayer &player = m_fixture->player; + + TestSubtitleSink subtitleSink; + QObject::connect(&sink, &QVideoSink::subtitleTextChanged, &subtitleSink, + &TestSubtitleSink::addSubtitle); + + player.setSource(*m_subtitleVideo); + QTRY_COMPARE(player.subtitleTracks().size(), 1); + QCOMPARE_EQ(player.subtitleTracks()[0].value(QMediaMetaData::Duration), 3000); + + player.setActiveSubtitleTrack(0); + + if (!isGStreamerPlatform()) // FIXME: spurious deadlocks + player.setPlaybackRate(5.f); + + player.play(); + + QStringList expectedSubtitleList = { + u"Hello"_s, + u""_s, + u"World"_s, + u""_s, + }; + + QTRY_COMPARE(subtitleSink.subtitles, expectedSubtitleList); +} + +void tst_QMediaPlayerBackend::multiTrack_validateMetadata() +{ + CHECK_SELECTED_URL(m_multitrackVideo); + QMediaPlayer &player = m_fixture->player; + + player.setSource(*m_multitrackVideo); + + QTRY_COMPARE(player.videoTracks().size(), 2); + QTRY_COMPARE(player.audioTracks().size(), 2); + QTRY_COMPARE(player.subtitleTracks().size(), 2); + + QSKIP_GSTREAMER("GStreamer does not provide correct track order"); + + QCOMPARE(player.videoTracks()[0][QMediaMetaData::Title], u"One"_s); + QCOMPARE(player.videoTracks()[1][QMediaMetaData::Title], u"Two"_s); + + QCOMPARE(player.audioTracks()[0][QMediaMetaData::Language], QLocale::Language::English); + QCOMPARE(player.audioTracks()[1][QMediaMetaData::Language], QLocale::Language::Spanish); + QCOMPARE(player.subtitleTracks()[0][QMediaMetaData::Language], QLocale::Language::English); + QCOMPARE(player.subtitleTracks()[1][QMediaMetaData::Language], QLocale::Language::Spanish); +} + +void tst_QMediaPlayerBackend::play_readsSubtitle_fromMultiTrack() +{ + using namespace std::chrono_literals; + CHECK_SELECTED_URL(m_multitrackVideo); + + QFETCH(int, track); + QFETCH(const QStringList, expectedSubtitles); + + QVideoSink &sink = m_fixture->surface; + QMediaPlayer &player = m_fixture->player; + + TestSubtitleSink subtitleSink; + QObject::connect(&sink, &QVideoSink::subtitleTextChanged, &subtitleSink, + &TestSubtitleSink::addSubtitle); + + player.setSource(*m_multitrackVideo); + + QTRY_COMPARE(player.subtitleTracks().size(), 2); + + if (track != -1) { + if (isGStreamerPlatform()) + QCOMPARE(player.subtitleTracks()[0].value(QMediaMetaData::Duration), 4000); + if (isFFMPEGPlatform()) + QCOMPARE(player.subtitleTracks()[0].value(QMediaMetaData::Duration), 15046); + } + + if (isGStreamerPlatform()) { + bool swapTracks = + player.subtitleTracks()[0][QMediaMetaData::Language] == QLocale::Language::Spanish; + + if (swapTracks && track == 1) + track = 0; + if (swapTracks && track == 0) + track = 1; + } + + player.setActiveSubtitleTrack(track); + if (!isGStreamerPlatform()) + player.setPlaybackRate(5.f); + player.play(); + + if (expectedSubtitles.isEmpty()) + QTRY_COMPARE_GT(player.position(), 2000); + + QTRY_COMPARE(subtitleSink.subtitles, expectedSubtitles); +} + +void tst_QMediaPlayerBackend::play_readsSubtitle_fromMultiTrack_data() +{ + QSKIP_GSTREAMER("GStreamer does not provide consistent track order"); + + QTest::addColumn<int>("track"); + QTest::addColumn<QStringList>("expectedSubtitles"); + + QTest::addRow("track 0") << 0 + << QStringList{ + u"1s track 1"_s, + u""_s, + u"3s track 1"_s, + u""_s, + }; + QTest::addRow("track 1") << 1 + << QStringList{ + u"1s track 2"_s, + u""_s, + u"3s track 2"_s, + u""_s, + }; + + QTest::addRow("no subtitles") << -1 << QStringList{}; +} + +void tst_QMediaPlayerBackend::setActiveSubtitleTrack_switchesSubtitles() +{ + QVideoSink &sink = m_fixture->surface; + QMediaPlayer &player = m_fixture->player; + + QFETCH(const QUrl, media); + QFETCH(const int, positionToSwapTrack); + QFETCH(const QLatin1String, testMode); + QFETCH(const QStringList, expectedSubtitles); + + TestSubtitleSink subtitleSink; + QObject::connect(&sink, &QVideoSink::subtitleTextChanged, &subtitleSink, + &TestSubtitleSink::addSubtitle); + + player.setSource(media); + + QTRY_COMPARE(player.subtitleTracks().size(), 2); + + int track0 = 0; + int track1 = 1; + if (isGStreamerPlatform()) { + bool swapTracks = + player.subtitleTracks()[0][QMediaMetaData::Language] == QLocale::Language::Spanish; + + if (swapTracks) { + track1 = 0; + track0 = 1; + } + } + + player.setActiveSubtitleTrack(track0); + + player.play(); + QTRY_COMPARE_GT(player.position(), positionToSwapTrack); + + if (testMode == "setWhilePaused"_L1) { + player.pause(); + player.setActiveSubtitleTrack(track1); + player.play(); + } else if (testMode == "setWhilePlaying"_L1) { + player.setActiveSubtitleTrack(track1); + } else { + QFAIL("should not reach"); + } + + QTRY_COMPARE(subtitleSink.subtitles, expectedSubtitles); +} + +void tst_QMediaPlayerBackend::setActiveSubtitleTrack_switchesSubtitles_data() +{ + QSKIP_GSTREAMER("GStreamer does not provide consistent track order"); + + QTest::addColumn<QUrl>("media"); + QTest::addColumn<QLatin1String>("testMode"); + QTest::addColumn<int>("positionToSwapTrack"); + QTest::addColumn<QStringList>("expectedSubtitles"); + + QTest::addRow("while paused") << *m_multitrackVideo << "setWhilePaused"_L1 << 2100 + << QStringList{ + u"1s track 1"_s, + u""_s, + u"3s track 2"_s, + u""_s, + }; + QTest::addRow("while playing") << *m_multitrackVideo << "setWhilePlaying"_L1 << 2100 + << QStringList{ + u"1s track 1"_s, + u""_s, + u"3s track 2"_s, + u""_s, + }; + + QTest::addRow("while paused, subtitles start at zero") + << *m_multitrackSubtitleStartsAtZeroVideo << "setWhilePaused"_L1 << 1100 + << QStringList{ + u"0s track 1"_s, + u""_s, + u"2s track 2"_s, + u""_s, + }; + QTest::addRow("while playing, subtitles start at zero") + << *m_multitrackSubtitleStartsAtZeroVideo << "setWhilePlaying"_L1 << 1100 + << QStringList{ + u"0s track 1"_s, + u""_s, + u"2s track 2"_s, + u""_s, + }; +} + +void tst_QMediaPlayerBackend::setActiveVideoTrack_switchesVideoTrack() +{ + using namespace std::chrono_literals; + QSKIP_GSTREAMER("GStreamer does not provide consistent track order"); + + TestVideoSink &sink = m_fixture->surface; + sink.setStoreFrames(); + QMediaPlayer &player = m_fixture->player; + + player.setSource(*m_multitrackVideo); + + QTRY_COMPARE(player.videoTracks().size(), 2); + + int track0 = 0; + int track1 = 1; + if (isGStreamerPlatform()) { + bool swapTracks = player.subtitleTracks()[0][QMediaMetaData::Title] != u"One"_s; + + if (swapTracks) { + track0 = 1; + track1 = 0; + } + } + + player.setActiveVideoTrack(track0); + player.play(); + + sink.waitForFrame(); + + QTest::qWait(500ms); + sink.waitForFrame(); + QCOMPARE(sink.m_frameList.back().toImage().pixel(10, 10), QColor(0xff, 0x80, 0x7f).rgb()); + + player.setActiveVideoTrack(track1); + + QTest::qWait(500ms); + sink.waitForFrame(); + QCOMPARE(sink.m_frameList.back().toImage().pixel(10, 10), QColor(0x80, 0x80, 0xff).rgb()); +} + +void tst_QMediaPlayerBackend::disablingAllTracks_doesNotStopPlayback() +{ + QSKIP_GSTREAMER("position does not advance in GStreamer"); + + QMediaPlayer &player = m_fixture->player; + + player.setSource(*m_multitrackVideo); + + // CAVEAT: we cannot set active tracks before tracksChanged is emitted + QTRY_COMPARE(player.videoTracks().size(), 2); + + player.setActiveVideoTrack(-1); + player.setActiveAudioTrack(-1); + + player.play(); + QTRY_VERIFY(player.position() > 1000); + + QCOMPARE(m_fixture->surface.m_totalFrames, 0); +} + +void tst_QMediaPlayerBackend::disablingAllTracks_beforeTracksChanged_doesNotStopPlayback() +{ + QSKIP_GSTREAMER("position does not advance in GStreamer"); + QSKIP_FFMPEG("setActiveXXXTrack(-1) only works after tracksChanged"); + + QMediaPlayer &player = m_fixture->player; + + player.setSource(*m_multitrackVideo); + + player.setActiveVideoTrack(-1); + player.setActiveAudioTrack(-1); + + player.play(); + QTRY_VERIFY(player.position() > 1000); + + QCOMPARE(m_fixture->surface.m_totalFrames, 0); } QTEST_MAIN(tst_QMediaPlayerBackend) + #include "tst_qmediaplayerbackend.moc" |