Revision control

Copy as Markdown

// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#include <QtMultimediaQuick/private/qquickvideooutput_p.h>
#include <QtMultimediaTestLib/private/mediabackendutils_p.h>
#include <QtMultimediaTestLib/private/mediafileselector_p.h>
#include <QtMultimediaTestLib/private/qfileutil_p.h>
#include <QtMultimediaTestLib/private/qintegrationtestbase_p.h>
#include <QtMultimediaTestLib/private/qsequentialfileadaptor_p.h>
#include <QtTest/qtest.h>
#include <QtMultimedia/qaudiobuffer.h>
#include <QtMultimedia/qaudiodevice.h>
#include <QtMultimedia/qaudiooutput.h>
#include <QtMultimedia/qmediadevices.h>
#include <QtMultimedia/qmediametadata.h>
#include <QtMultimedia/qmediaplayer.h>
#include <QtMultimedia/qmediatimerange.h>
#include <QtMultimedia/qvideoframe.h>
#include <QtMultimedia/qvideosink.h>
#include <QtMultimedia/private/qplatformmediaintegration_p.h>
#include <QtMultimedia/private/qplatformvideosink_p.h>
#include <QtMultimedia/private/qtmultimedia-config_p.h>
#include <QtQuick/qquickitem.h>
#include <QtQuick/qquickview.h>
#include <QtQuick/private/qquickloader_p.h>
#include <QtQml/qqmlcomponent.h>
#include <QtQml/qqmlengine.h>
#include <QtQml/qqmlproperty.h>
#include <QtCore/qbuffer.h>
#include <QtCore/qdebug.h>
#include <QtCore/qoperatingsystemversion.h>
#include <QtCore/qrandom.h>
#include <QtCore/qtemporaryfile.h>
#include <QtCore/qtimer.h>
#include <QtCore/qurl.h>
#include <QtCore/private/qglobal_p.h>
#if QT_CONFIG(process)
# include <QtCore/qprocess.h>
#endif
#ifdef QT_FEATURE_network
# include <QtNetwork/qtcpserver.h>
#endif
#include "fake.h"
#include "fixture.h"
#include "mediaplayerstate.h"
#include "server.h"
#include <array>
#include <memory>
// NOLINTBEGIN(readability-convert-member-functions-to-static)
using namespace Qt::Literals;
using namespace std::chrono_literals;
namespace {
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));
}
} // namespace
/*
This is the backend conformance test.
Since it relies on platform media framework and sound hardware
it may be less stable.
*/
using namespace QtMultimediaTest;
class tst_QMediaPlayerBackend : public QIntegrationTestBase
{
Q_OBJECT
public slots:
void initTestCase();
void init() { m_fixture = std::make_unique<Fixture>(); }
void cleanup() { m_fixture = nullptr; }
private slots:
void testMediaFilesAreSupported();
void destructor_cancelsPreviousSetSource_whenServerDoesNotRespond();
void destructor_emitsOnlyQObjectDestroyedSignal_whenPlayerIsRunning();
void getters_returnExpectedValues_whenCalledWithDefaultConstructedPlayer_data() const;
void getters_returnExpectedValues_whenCalledWithDefaultConstructedPlayer() const;
void makeInvalidMediaCase();
void setSource_emitsSourceChanged_whenCalledWithInvalidMedia_data();
void setSource_emitsSourceChanged_whenCalledWithInvalidMedia();
void setSource_emitsError_whenCalledWithInvalidMedia_data();
void setSource_emitsError_whenCalledWithInvalidMedia();
void setSource_emitsMediaStatusChange_whenCalledWithInvalidMedia_data();
void setSource_emitsMediaStatusChange_whenCalledWithInvalidMedia();
void setSource_doesNotEmitPlaybackStateChange_whenCalledWithInvalidMedia_data();
void setSource_doesNotEmitPlaybackStateChange_whenCalledWithInvalidMedia();
void setSource_setsSourceMediaStatusAndError_whenCalledWithInvalidMedia_data();
void setSource_setsSourceMediaStatusAndError_whenCalledWithInvalidMedia();
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 setSource_doesNotCrash_whenCalledWithEmptyUrlWhileLoadingMedia();
void setSourceAndPlay_setCorrectVideoSize_whenVideoHasNonStandardPixelAspectRatio_data();
void setSourceAndPlay_setCorrectVideoSize_whenVideoHasNonStandardPixelAspectRatio();
void pause_doesNotChangePlayerState_whenInvalidFileLoaded();
void pause_doesNothing_whenMediaIsNotLoaded();
void pause_entersPauseState_whenPlayerWasPlaying();
void pause_doesNotAdvancePosition();
void pause_playback_resumesFromPausedPosition();
void pause_triggersTheFirstFrame_whenPlayerIsNotPlaying_data();
void pause_triggersTheFirstFrame_whenPlayerIsNotPlaying();
void play_doesNotResetErrorState_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 setPosition_doesNotStallInBufferingState_whenSourceIsSeekableDevice();
void play_playbackLastsForTheExpectedTime();
void play_playbackLastsForTheExpectedTime_data();
void play_threeMediaPlayers();
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 playToEOS_finishesWithEmptyFrame();
void volumeAcrossFiles_data();
void volumeAcrossFiles();
void initialVolume();
void seekPauseSeek();
void seekInStoppedState();
void subsequentPlayback();
void subsequentPlayback_playsForExpectedDuration();
void surfaceTest();
void metadata();
void metadata_returnsMetadataWithThumbnail_whenMediaHasThumbnail_data();
void metadata_returnsMetadataWithThumbnail_whenMediaHasThumbnail();
void metadata_returnsMetadataWithHasHdrContent_whenMediaHasHdrContent_data();
void metadata_returnsMetadataWithHasHdrContent_whenMediaHasHdrContent();
void metadata_returnsMetadataWithCorrectDate_data();
void metadata_returnsMetadataWithCorrectDate();
void playerStateAtEOS();
void playFromBuffer();
void playFromSequentialStream();
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 finiteLoops_data();
void infiniteLoops();
void seekOnLoops();
void changeLoopsOnTheFly();
void seekAfterLoopReset();
void cleanSinkAndNoMoreFramesAfterStop();
void lazyLoadVideo();
void videoSinkSignals();
void nonAsciiFileName();
void setMedia_setsVideoSinkSize_beforePlaying();
void play_playsTransformedVideoOutput_whenVideoFileHasTransformationMetadata_data();
void play_playsTransformedVideoOutput_whenVideoFileHasTransformationMetadata();
void setVideoOutput_doesNotStopPlayback_data();
void setVideoOutput_doesNotStopPlayback();
void setVideoOutput_whilePaused_updatesNewSink();
void setVideoOutput_whilePlaying_doesNotDropFrames();
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();
void play_finishes_whenPlayingFileWithPacketsAfterStreamEnd_data();
void play_finishes_whenPlayingFileWithPacketsAfterStreamEnd();
void play_finishes_whenPlayingFileIncludingPacketsWithUndefinedTimestamp();
void makeStressTestCases();
void stressTest_setupAndTeardown();
void stressTest_setupAndTeardown_data();
void stressTest_setupAndTeardown_keepAudioOutput();
void stressTest_setupAndTeardown_keepAudioOutput_data();
void stressTest_setupAndTeardown_keepVideoOutput();
void stressTest_setupAndTeardown_keepVideoOutput_data();
void destruction_doesNotDeadlock_afterMediaPlayerCall();
void destruction_doesNotDeadlock_afterMediaPlayerCall_data();
private:
QUrl selectVideoFile(const QStringList &mediaCandidates);
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{ q23::unexpect };
MaybeUrl m_localWavFile2{ q23::unexpect };
MaybeUrl m_localVideoFile{ q23::unexpect };
MaybeUrl m_localVideoFile2{ q23::unexpect };
MaybeUrl m_localVideoFile1Sec{ q23::unexpect };
MaybeUrl m_av1File{ q23::unexpect };
MaybeUrl m_videoDimensionTestFile{ q23::unexpect };
MaybeUrl m_localCompressedSoundFile{ q23::unexpect };
MaybeUrl m_localMp3FileWithMetadataAndEmbeddedThumbnail{ q23::unexpect };
MaybeUrl m_localVideoFile3ColorsWithSound{ q23::unexpect };
MaybeUrl m_videoFileWithJpegThumbnail{ q23::unexpect };
MaybeUrl m_videoFileWithPngThumbnail{ q23::unexpect };
MaybeUrl m_oneRedFrameVideo{ q23::unexpect };
MaybeUrl m_192x108_PAR_2_3_Video{ q23::unexpect };
MaybeUrl m_192x108_PAR_3_2_Video{ q23::unexpect };
MaybeUrl m_colorMatrixVideo{ q23::unexpect };
MaybeUrl m_colorMatrixMirroredVideo{ q23::unexpect };
MaybeUrl m_colorMatrix90degClockwiseVideo{ q23::unexpect };
MaybeUrl m_colorMatrix90degClockwiseMirroredVideo{ q23::unexpect };
MaybeUrl m_colorMatrix180degClockwiseVideo{ q23::unexpect };
MaybeUrl m_colorMatrix180degClockwiseMirroredVideo{ q23::unexpect };
MaybeUrl m_colorMatrix270degClockwiseVideo{ q23::unexpect };
MaybeUrl m_colorMatrix270degClockwiseMirroredVideo{ q23::unexpect };
MaybeUrl m_hdrVideo{ q23::unexpect };
MaybeUrl m_15sVideo{ q23::unexpect };
MaybeUrl m_subtitleVideo{ q23::unexpect };
MaybeUrl m_multitrackVideo{ q23::unexpect };
MaybeUrl m_multitrackSubtitleStartsAtZeroVideo{ q23::unexpect };
MaybeUrl m_oggEndingWithInvalidTiming{ q23::unexpect };
MaybeUrl m_withDate{ q23::unexpect };
MaybeUrl m_withCreationTime{ q23::unexpect };
MaybeUrl m_withDateAndCreationTime{ q23::unexpect };
MaybeUrl m_withQtDateAndCreationTime{ q23::unexpect };
MediaFileSelector m_mediaSelector;
const std::array<QRgb, 3> m_video3Colors = { { 0xFF0000, 0x00FF00, 0x0000FF } };
QString m_vlcCommand;
std::unique_ptr<Fixture> m_fixture;
};
static bool commandExists(const QString &command)
{
#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);
});
}
void tst_QMediaPlayerBackend::detectVlcCommand()
{
m_vlcCommand = qEnvironmentVariable("QT_VLC_COMMAND");
if (!m_vlcCommand.isEmpty())
return;
#if defined(Q_OS_WINDOWS)
m_vlcCommand = "vlc.exe";
#else
m_vlcCommand = "vlc";
#endif
if (commandExists(m_vlcCommand))
return;
m_vlcCommand.clear();
#if defined(Q_OS_MACOS)
m_vlcCommand = "/Applications/VLC.app/Contents/MacOS/VLC";
#elif defined(Q_OS_WINDOWS)
m_vlcCommand = "C:/Program Files/VideoLAN/VLC/vlc.exe";
#endif
if (!QFile::exists(m_vlcCommand))
m_vlcCommand.clear();
}
bool tst_QMediaPlayerBackend::canCreateRtpStream() const
{
return !m_vlcCommand.isEmpty();
}
void tst_QMediaPlayerBackend::initTestCase()
{
if (!initIntegrationTestCase())
return;
if (qEnvironmentVariable("COIN_PLATFORM_ID") == "macos-15-x86_64-tests")
QSKIP("Skipping test on macOS 15 x86_64, as it's flaky on CI");
QMediaPlayer player;
if (!player.isAvailable())
QSKIP("Media player service is not available");
qRegisterMetaType<MaybeUrl>();
m_localWavFile = m_mediaSelector.select("qrc:/testdata/test.wav");
m_localWavFile2 = m_mediaSelector.select("qrc:/testdata/_test.wav");
m_localVideoFile =
m_mediaSelector.select("qrc:/testdata/colors.mp4", "qrc:/testdata/colors.ogv");
m_localVideoFile3ColorsWithSound =
m_mediaSelector.select("qrc:/testdata/3colors_with_sound_1s.mp4");
m_videoFileWithJpegThumbnail =
m_mediaSelector.select("qrc:/testdata/audio_video_with_jpg_thumbnail.mp4");
m_videoFileWithPngThumbnail =
m_mediaSelector.select("qrc:/testdata/audio_video_with_png_thumbnail.mp4");
#ifndef Q_OS_MACOS // QTBUG-119711 Add support for AV1 decoding with the FFmpeg backend in online installer
m_av1File = m_mediaSelector.select("qrc:/testdata/busAv1.webm");
#endif
m_localVideoFile2 =
m_mediaSelector.select("qrc:/testdata/BigBuckBunny.mp4", "qrc:/testdata/busMpeg4.mp4");
m_localVideoFile1Sec = m_mediaSelector.select("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_localMp3FileWithMetadataAndEmbeddedThumbnail = 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_colorMatrixMirroredVideo = m_mediaSelector.select("qrc:/testdata/color_matrix_mirrored.mp4");
m_colorMatrix90degClockwiseVideo =
m_mediaSelector.select("qrc:/testdata/color_matrix_90_deg_clockwise.mp4");
m_colorMatrix90degClockwiseMirroredVideo =
m_mediaSelector.select("qrc:/testdata/color_matrix_90_deg_clockwise_mirrored.mp4");
m_colorMatrix180degClockwiseVideo =
m_mediaSelector.select("qrc:/testdata/color_matrix_180_deg_clockwise.mp4");
m_colorMatrix180degClockwiseMirroredVideo =
m_mediaSelector.select("qrc:/testdata/color_matrix_180_deg_clockwise_mirrored.mp4");
m_colorMatrix270degClockwiseVideo =
m_mediaSelector.select("qrc:/testdata/color_matrix_270_deg_clockwise.mp4");
m_colorMatrix270degClockwiseMirroredVideo =
m_mediaSelector.select("qrc:/testdata/color_matrix_270_deg_clockwise_mirrored.mp4");
// On Windows 10, IMFMediaEngine::Shutdown() hangs after loading this file.
if (!isWMFPlatform()
|| QOperatingSystemVersion::current() >= QOperatingSystemVersion::Windows11) {
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");
m_oggEndingWithInvalidTiming = m_mediaSelector.select("qrc:/testdata/corrupt_end.ogg");
m_withDate = m_mediaSelector.select("qrc:/testdata/with_date.mp4");
m_withCreationTime = m_mediaSelector.select("qrc:/testdata/with_creation_time.mp4");
m_withDateAndCreationTime = m_mediaSelector.select("qrc:/testdata/with_date_and_creation_time.mp4");
m_withQtDateAndCreationTime = m_mediaSelector.select("qrc:/testdata/with_qtdate_and_creation_time.mov");
detectVlcCommand();
}
void tst_QMediaPlayerBackend::testMediaFilesAreSupported()
{
QSKIP_DARWIN("AVFoundation does not support Matroska/OGG containers");
QSKIP_OHOS("OHOS demuxer rejects some H.264/HEVC/AV1 profiles used by the test media");
QSKIP_WMF("Media Foundation supports Ogg/Matroska only with optional Windows media "
"extensions, which may not be installed");
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::destructor_cancelsPreviousSetSource_whenServerDoesNotRespond()
{
#ifdef QT_FEATURE_network
QSKIP_DARWIN("AVFoundation does not immediately connect to RTSP server on setSource");
QSKIP_OHOS("OHOS app sandbox blocks listening sockets used by UnResponsiveRtspServer");
UnResponsiveRtspServer server;
QVERIFY(server.listen());
auto player = std::make_unique<QMediaPlayer>();
player->setSource(server.address());
QVERIFY(server.waitForConnection());
// Cancel connection (should be fast, but can't be reliably verified
// in a test. For now we just verify that we don't crash.
player = nullptr;
#else
QSKIP("Test requires network feature");
#endif
}
void tst_QMediaPlayerBackend::destructor_emitsOnlyQObjectDestroyedSignal_whenPlayerIsRunning()
{
CHECK_SELECTED_URL(m_localVideoFile3ColorsWithSound);
// Arrange
m_fixture->player.setSource(*m_localVideoFile3ColorsWithSound);
m_fixture->player.play();
// Wait for started
QTRY_VERIFY(m_fixture->player.mediaStatus() == QMediaPlayer::BufferedMedia
|| m_fixture->player.mediaStatus() == QMediaPlayer::EndOfMedia);
m_fixture->clearSpies();
// Act
m_fixture->player.~QMediaPlayer();
new (&m_fixture->player) QMediaPlayer;
// Assert
QCOMPARE(m_fixture->playbackStateChanged.size(), 0);
QCOMPARE(m_fixture->errorOccurred.size(), 0);
QCOMPARE(m_fixture->sourceChanged.size(), 0);
QCOMPARE(m_fixture->mediaStatusChanged.size(), 0);
QCOMPARE(m_fixture->positionChanged.size(), 0);
QCOMPARE(m_fixture->durationChanged.size(), 0);
QCOMPARE(m_fixture->metadataChanged.size(), 0);
QCOMPARE(m_fixture->volumeChanged.size(), 0);
QCOMPARE(m_fixture->mutedChanged.size(), 0);
QCOMPARE(m_fixture->bufferProgressChanged.size(), 0);
QCOMPARE(m_fixture->destroyed.size(), 1);
}
void tst_QMediaPlayerBackend::
getters_returnExpectedValues_whenCalledWithDefaultConstructedPlayer_data() const
{
QTest::addColumn<bool>("hasAudioOutput");
QTest::addColumn<bool>("hasVideoOutput");
QTest::addColumn<bool>("hasVideoSink");
QTest::newRow("noOutput") << false << false << false;
QTest::newRow("withAudioOutput") << true << false << false;
QTest::newRow("withVideoOutput") << false << true << false;
QTest::newRow("withVideoSink") << false << false << true;
QTest::newRow("withAllOutputs") << true << true << true;
}
void tst_QMediaPlayerBackend::getters_returnExpectedValues_whenCalledWithDefaultConstructedPlayer()
const
{
QFETCH(const bool, hasAudioOutput);
QFETCH(const bool, hasVideoOutput);
QFETCH(const bool, hasVideoSink);
QAudioOutput audioOutput;
TestVideoOutput videoOutput;
QMediaPlayer player;
if (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::makeInvalidMediaCase()
{
QTest::addColumn<QUrl>("invalidMedia");
QTest::newRow("invalidFile") << QUrl("some_not_existing_file.mp4");
QTest::newRow("invalidQrcResource") << QUrl("qrc:/some_not_existing_qrc_resource.mp4");
QTest::newRow("invalidHttpsAddress") << QUrl("https://qt.io/invalid_https_address.mp4");
}
void tst_QMediaPlayerBackend::setSource_emitsSourceChanged_whenCalledWithInvalidMedia_data()
{
makeInvalidMediaCase();
}
void tst_QMediaPlayerBackend::setSource_emitsSourceChanged_whenCalledWithInvalidMedia()
{
QFETCH(QUrl, invalidMedia);
m_fixture->player.setSource(invalidMedia);
QTRY_COMPARE_EQ_WITH_TIMEOUT(m_fixture->player.error(), QMediaPlayer::ResourceError, 30s);
QCOMPARE_EQ(m_fixture->sourceChanged, SignalList({ { invalidMedia } }));
}
void tst_QMediaPlayerBackend::setSource_emitsError_whenCalledWithInvalidMedia_data()
{
makeInvalidMediaCase();
}
void tst_QMediaPlayerBackend::setSource_emitsError_whenCalledWithInvalidMedia()
{
QFETCH(QUrl, invalidMedia);
m_fixture->player.setSource(invalidMedia);
QTRY_COMPARE_EQ_WITH_TIMEOUT(m_fixture->player.error(), QMediaPlayer::ResourceError, 30s);
QCOMPARE_EQ(m_fixture->errorOccurred[0][0], QMediaPlayer::ResourceError);
}
void tst_QMediaPlayerBackend::setSource_emitsMediaStatusChange_whenCalledWithInvalidMedia_data()
{
makeInvalidMediaCase();
}
void tst_QMediaPlayerBackend::setSource_emitsMediaStatusChange_whenCalledWithInvalidMedia()
{
QFETCH(QUrl, invalidMedia);
m_fixture->player.setSource(invalidMedia);
QTRY_COMPARE_EQ_WITH_TIMEOUT(m_fixture->player.error(), QMediaPlayer::ResourceError, 30s);
QCOMPARE_EQ(m_fixture->mediaStatusChanged,
SignalList({ { QMediaPlayer::LoadingMedia }, { QMediaPlayer::InvalidMedia } }));
}
void tst_QMediaPlayerBackend::setSource_doesNotEmitPlaybackStateChange_whenCalledWithInvalidMedia_data()
{
makeInvalidMediaCase();
}
void tst_QMediaPlayerBackend::setSource_doesNotEmitPlaybackStateChange_whenCalledWithInvalidMedia()
{
QFETCH(QUrl, invalidMedia);
m_fixture->player.setSource(invalidMedia);
QTRY_COMPARE_EQ_WITH_TIMEOUT(m_fixture->player.error(), QMediaPlayer::ResourceError, 30s);
QVERIFY(m_fixture->playbackStateChanged.empty());
}
void tst_QMediaPlayerBackend::setSource_setsSourceMediaStatusAndError_whenCalledWithInvalidMedia_data()
{
makeInvalidMediaCase();
}
void tst_QMediaPlayerBackend::setSource_setsSourceMediaStatusAndError_whenCalledWithInvalidMedia()
{
QFETCH(QUrl, invalidMedia);
m_fixture->player.setSource(invalidMedia);
QTRY_COMPARE_EQ_WITH_TIMEOUT(m_fixture->player.error(), QMediaPlayer::ResourceError, 30s);
MediaPlayerState expectedState = MediaPlayerState::defaultState();
expectedState.source = invalidMedia;
expectedState.mediaStatus = QMediaPlayer::InvalidMedia;
expectedState.error = QMediaPlayer::ResourceError;
const MediaPlayerState actualState{ m_fixture->player };
COMPARE_MEDIA_PLAYER_STATE_EQ(actualState, expectedState);
}
void tst_QMediaPlayerBackend::setSource_initializesExpectedDefaultState()
{
QSKIP_GSTREAMER("gst_play may fill some metadata");
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;
const MediaPlayerState actualState{ m_fixture->player };
COMPARE_MEDIA_PLAYER_STATE_EQ(actualState, expectedState);
}
void tst_QMediaPlayerBackend::setSource_initializesExpectedDefaultState_data()
{
QTest::addColumn<MaybeUrl>("url");
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()
{
QSKIP_DARWIN("AVFoundation does not immediately connect to RTSP server on setSource");
QSKIP_OHOS("OHOS app sandbox blocks listening sockets used by UnResponsiveRtspServer");
#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());
if (!isGStreamerPlatform()) {
// QTBUG-124005: gstreamer sees multiple loading/loaded transitions
// 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()
{
QSKIP_OHOS("OH_AVPlayer does not yet expose enough metadata to populate MediaPlayerState");
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 if (isWMFPlatform())
expectedState.duration = 15019;
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()
if (isWMFPlatform()) {
expectedState.bufferProgress = std::nullopt;
expectedState.bufferedTimeRange = std::nullopt;
}
MediaPlayerState actualState{ m_fixture->player };
COMPARE_MEDIA_PLAYER_STATE_EQ(actualState, expectedState);
}
void tst_QMediaPlayerBackend::setSource_stopsAndEntersErrorState_whenPlayerWasPlaying()
{
QSKIP_OHOS("OH_AVPlayer state machine fires extra status transitions on setSource error path");
CHECK_SELECTED_URL(m_localVideoFile3ColorsWithSound);
// Arrange
m_fixture->player.setSource(*m_localVideoFile3ColorsWithSound);
m_fixture->player.play();
QTRY_VERIFY(m_fixture->framesCount > 0);
QCOMPARE(m_fixture->errorOccurred.size(), 0);
// Act
m_fixture->player.setSource(QUrl("Some not existing media"));
// Assert
const int savedFramesCount = m_fixture->framesCount;
QCOMPARE(m_fixture->player.source(), QUrl("Some not existing media"));
QTRY_COMPARE(m_fixture->player.playbackState(), QMediaPlayer::StoppedState);
QTRY_COMPARE(m_fixture->player.mediaStatus(), QMediaPlayer::InvalidMedia);
QTRY_COMPARE(m_fixture->player.error(), QMediaPlayer::ResourceError);
QVERIFY(!m_fixture->surface.videoFrame().isValid());
QCOMPARE(m_fixture->errorOccurred.size(), 1);
QTest::qWait(20);
QCOMPARE(m_fixture->framesCount, savedFramesCount);
}
void tst_QMediaPlayerBackend::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()
{
QSKIP_OHOS("OH_AVPlayer leaves residual state after Reset() that the QPlatformMediaPlayer baseline does not match");
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);
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_COMPARE_GT(player.position(), 0);
if (isGStreamerPlatform())
QTRY_COMPARE_GT(player.duration(), 0); // duration update is asynchronous
else
QCOMPARE_GT(player.duration(), 0);
// Set empty URL and verify that state is fully reset to default
m_fixture->clearSpies();
m_fixture->player.setSource(QUrl());
QVERIFY(!m_fixture->mediaStatusChanged.isEmpty());
QVERIFY(!m_fixture->sourceChanged.isEmpty());
MediaPlayerState expectedState = MediaPlayerState::defaultState();
if (isGStreamerPlatform()) // QTBUG-124005: no buffer progress update
expectedState.bufferProgress = std::nullopt;
const MediaPlayerState actualState{ player };
COMPARE_MEDIA_PLAYER_STATE_EQ(actualState, expectedState);
}
void tst_QMediaPlayerBackend::setSource_resetsState_whenCalledWithEmptyUrl_data()
{
QTest::addColumn<MaybeUrl>("url");
QTest::addRow("with wave file") << m_localWavFile;
QTest::addRow("with video file") << m_localVideoFile;
}
void tst_QMediaPlayerBackend::setSource_loadsNewMedia_whenPreviousMediaWasFullyLoaded()
{
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);
}
void tst_QMediaPlayerBackend::setSource_loadsCorrectTracks_whenLoadingMediaInSequence()
{
QSKIP_OHOS("OH_AVPlayer retains preroll frame across Reset()+SetSource for a new audio-only file");
CHECK_SELECTED_URL(m_localVideoFile3ColorsWithSound);
CHECK_SELECTED_URL(m_localWavFile2);
CHECK_SELECTED_URL(m_localVideoFile2);
// 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());
m_fixture->clearSpies();
// Load an audio file, and verify that only audio track is loaded
m_fixture->player.setSource(*m_localWavFile2);
QTRY_COMPARE_EQ(m_fixture->player.mediaStatus(), QMediaPlayer::MediaStatus::LoadedMedia);
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());
m_fixture->player.play();
// Load video only file, and verify that only video track is loaded
m_fixture->player.setSource(*m_localVideoFile2);
QTRY_COMPARE_EQ(m_fixture->player.mediaStatus(), QMediaPlayer::MediaStatus::LoadedMedia);
QCOMPARE(m_fixture->player.playbackState(), QMediaPlayer::StoppedState);
QVERIFY(m_fixture->player.hasVideo());
QVERIFY(!m_fixture->player.hasAudio());
QCOMPARE(m_fixture->errorOccurred.size(), 0);
}
void tst_QMediaPlayerBackend::setSource_remainsInStoppedState_whenPlayerWasStopped()
{
QSKIP_OHOS("OH_AVPlayer emits a spurious playbackStateChanged when reloading source after stop()");
CHECK_SELECTED_URL(m_localWavFile);
CHECK_SELECTED_URL(m_localWavFile2);
// Arrange
m_fixture->player.setSource(*m_localWavFile);
m_fixture->player.play();
QTRY_VERIFY(m_fixture->player.position() > 100);
m_fixture->player.stop();
m_fixture->clearSpies();
// Act
m_fixture->player.setSource(*m_localWavFile2);
// Assert
QTRY_VERIFY(m_fixture->mediaStatusChanged.size() > 0);
QTRY_COMPARE(m_fixture->player.mediaStatus(), QMediaPlayer::LoadedMedia);
QCOMPARE_EQ(m_fixture->mediaStatusChanged,
SignalList({ { QMediaPlayer::LoadingMedia }, { QMediaPlayer::LoadedMedia } }));
QCOMPARE(m_fixture->player.playbackState(), QMediaPlayer::StoppedState);
QVERIFY(m_fixture->playbackStateChanged.empty());
}
void tst_QMediaPlayerBackend::setSource_entersStoppedState_whenPlayerWasPlaying()
{
QSKIP_OHOS("OH_AVPlayer emits an extra mediaStatusChanged transition when reloading source mid-playback");
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 } }));
QSKIP_DARWIN("AVFoundation may not have enough time to deliver positionChannged signals");
QTRY_VERIFY(!m_fixture->positionChanged.empty()
&& m_fixture->positionChanged.last()[0].value<qint64>() == 0);
QCOMPARE(m_fixture->player.position(), 0);
}
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
QSKIP_IF_NOT_FFMPEG("This test is only for FFmpeg backend");
// 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(QUrl(u"qrc:/testdata/colors.mp4"_s),
u"colors.XXXXXX.mp4"_s);
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");
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::FormatError);
#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()
{
QSKIP_OHOS("OH_AVPlayer reports a single audio+video track and cannot expose subtitle/multitrack info");
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()
{
QSKIP_GSTREAMER("gst_play does not parse tracks on setSource. Users will have to pause(). "
"Alternatively we could use gst_discover");
QSKIP_OHOS("OH_AVPlayer reports a single audio+video track and cannot expose subtitle/multitrack info");
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::setSource_doesNotCrash_whenCalledWithEmptyUrlWhileLoadingMedia()
{
// Added to prevent Q_ASSERT failures in QFFmpegMediaPlayer::setMediaAsync(), like QTBUG-128159
using namespace std::chrono_literals;
CHECK_SELECTED_URL(m_localVideoFile);
// Act
m_fixture->player.setSource(*m_localVideoFile); // Loads media in a separate thread
QTRY_VERIFY(m_fixture->player.mediaStatus() == QMediaPlayer::LoadingMedia);
m_fixture->player.setSource(QUrl("")); // Cancels loading before the thread returns
QTRY_VERIFY(m_fixture->player.mediaStatus() == QMediaPlayer::NoMedia);
// Assert
// At this point, due to a waitForFinished() in setMedia(), the loading thread has
// finished its invokeMethod() call to transition back to the calling thread.
// qWait() triggers the transition immediately, so 10 ms is sufficient.
QTest::qWait(10ms);
}
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()
{
if (isGStreamerPlatform() && isCI())
QSKIP("QTBUG-124005: Fails with gstreamer on CI");
QSKIP_DARWIN("No pixel aspect ratio scaling support");
QSKIP_OHOS("OH_AVPlayer reports the encoded video size and does not apply pixel-aspect-ratio scaling");
QSKIP_WMF("IMFMediaEngine applies PAR as horizontal scaling only, while the test expects "
"vertical scaling for PAR < 1");
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(QUrl{u"Some not existing media"_s});
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()
{
QSKIP_OHOS("OH_AVPlayer_Pause emits an extra position-update before the PausedState callback");
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_doesNotAdvancePosition()
{
using namespace std::chrono_literals;
CHECK_SELECTED_URL(m_localVideoFile);
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;
if (isDarwinPlatform() && isCI())
QSKIP("Flaky on CI: position may drift by 1ms after pause due to async event delivery");
QSKIP_WMF("IMFMediaEngine: position may drift by 30-40ms after resuming from pause due to "
"decode-priming latency");
CHECK_SELECTED_URL(m_localVideoFile);
QMediaPlayer &player = m_fixture->player;
player.setSource(*m_localVideoFile);
player.play();
QTRY_COMPARE_GT(player.position(), 100);
player.pause();
qint64 pausePos = player.position();
QTest::qWait(1s);
QCOMPARE_EQ(player.position(), pausePos);
player.play();
// Make sure the media player does not make up for the lost time
m_fixture->positionChanged.wait();
m_fixture->positionChanged.wait();
QCOMPARE_LT(player.position(), pausePos + 500);
}
void tst_QMediaPlayerBackend::pause_triggersTheFirstFrame_whenPlayerIsNotPlaying_data()
{
QTest::addColumn<float>("playbackRate");
QTest::addColumn<bool>("waitForLoad");
// provoking any RC by setting different playback rates
for (float rate : { 0.1f, 1.f, 10.f }) {
// invoking pause during or after loading may affect the flow
// because of differences in managing pending state
for (bool waitForLoad : { false, true })
QTest::addRow("playbackRate=%f, waitForLoad=%s", rate, waitForLoad ? "yes" : "no")
<< rate << waitForLoad;
}
}
void tst_QMediaPlayerBackend::pause_triggersTheFirstFrame_whenPlayerIsNotPlaying()
{
QSKIP_OHOS("OH_AVPlayer does not deliver a preroll frame to QVideoSink when paused without playing first");
QFETCH(const float, playbackRate);
QFETCH(const bool, waitForLoad);
using namespace std::chrono_literals;
CHECK_SELECTED_URL(m_localVideoFile);
// Arrange
QMediaPlayer &player = m_fixture->player;
player.setSource(*m_localVideoFile);
player.setPlaybackRate(playbackRate);
QTRY_VERIFY(!waitForLoad || player.mediaStatus() == QMediaPlayer::LoadedMedia);
// Act
player.pause();
// Assert
QVERIFY(!player.isPlaying());
QVideoFrame frame = m_fixture->surface.waitForFrame();
QVERIFY(frame.isValid());
QCOMPARE(frame.startTime(), 0);
QTRY_COMPARE(player.playbackState(), QMediaPlayer::PausedState);
QTRY_COMPARE(m_fixture->playbackStateChanged, SignalList({ { QMediaPlayer::PausedState } }));
// wait a bit to check that no more frames arrived
QTest::qWait(300ms);
QCOMPARE(m_fixture->surface.m_totalFrames, 1);
}
void tst_QMediaPlayerBackend::play_doesNotResetErrorState_whenCalledWithInvalidFile()
{
m_fixture->player.setSource(QUrl{u"Some not existing media"_s});
QTRY_COMPARE_EQ(m_fixture->player.error(), QMediaPlayer::ResourceError);
MediaPlayerState expectedState{ m_fixture->player };
m_fixture->player.play();
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);
m_fixture->surface.m_frameList.clear();
// Act
m_fixture->player.play();
// Assert
QTRY_VERIFY(m_fixture->framesCount > 0);
QTRY_VERIFY(m_fixture->player.mediaStatus() == QMediaPlayer::BufferedMedia
|| m_fixture->player.mediaStatus() == QMediaPlayer::EndOfMedia);
QCOMPARE_EQ(m_fixture->player.playbackState(), QMediaPlayer::PlayingState);
QCOMPARE(m_fixture->player.error(), QMediaPlayer::NoError);
}
void tst_QMediaPlayerBackend::play_doesNothing_whenMediaIsNotLoaded()
{
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()
{
QSKIP_OHOS("OH_AVPlayer emits a fatal error on this test media (video preroll race) — needs investigation");
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()
{
QSKIP_OHOS("OH_AVPlayer goes through INITIALIZED after Stop(), so play() requires a reprepare and emits LoadingMedia");
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() && !isWMFPlatform()) {
QCOMPARE_EQ(m_fixture->mediaStatusChanged,
SignalList({
{ QMediaPlayer::BufferingMedia },
{ QMediaPlayer::BufferedMedia },
}));
} else {
QTRY_COMPARE_EQ(m_fixture->mediaStatusChanged,
// gstreamer/windows: short files may reach EndOfMedia
SignalList({
{ QMediaPlayer::BufferingMedia },
{ QMediaPlayer::BufferedMedia },
{ QMediaPlayer::EndOfMedia },
}));
}
}
void tst_QMediaPlayerBackend::playAndSetSource_emitsExpectedSignalsAndStopsPlayback_whenSetSourceWasCalledWithEmptyUrl()
{
if (isDarwinPlatform() && isCI() && QSysInfo::currentCpuArchitecture() == "x86_64")
QSKIP("Flaky on CI: AVFoundation may not emit positionChanged on x86_64");
QSKIP_OHOS("OH_AVPlayer_Reset() during playback emits an extra mediaStatus sequence that does not match the cross-platform baseline");
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");
QSKIP_DARWIN("RTSP playback is not reliably supported with AVFoundation");
QSKIP_WMF("IMFMediaEngine does not support playing RTSP/RTP media streams directly");
auto temporaryFile = copyResourceToTemporaryFile(QUrl(u"qrc:/testdata/colors.mp4"_s),
u"colors.XXXXXX.mp4"_s);
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;
QSignalSpy errorSpy(&player, &QMediaPlayer::errorOccurred);
player.setVideoSink(&surface);
// Ignore audio output to check timings accuratelly
// player.setAudioOutput(&output);
player.setSource(QUrl{streamUrl});
player.play();
QTRY_COMPARE(player.playbackState(), QMediaPlayer::PlayingState);
const auto colors = { qRgb(0, 0, 0xFF), qRgb(0xFF, 0, 0), qRgb(0, 0xFE, 0) };
const auto colorInterval = 5000;
for (auto pos : { colorInterval / 2, colorInterval + 100 }) {
qDebug() << "Waiting for position:" << pos;
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()
{
QSKIP_GSTREAMER("test failure with gst_play");
QSKIP_OHOS("OH_AVPlayer does not hold the last video frame for its full presentation duration");
QSKIP_WMF("IMFMediaEngine fires ENDED synchronously with the last frame "
"for single-frame videos, so elapsed time is always ~0");
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()
{
QSKIP_OHOS("OH_AVPlayer does not always emit EndOfMedia for short test media within the test timeout");
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");
QSKIP_GSTREAMER("GStreamer backend playback fails when no output is connected");
}
if (videoConnected && !audioConnected)
QSKIP_GSTREAMER("GStreamer backend playback fails when no video 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
QSKIP_IF_NOT_FFMPEG("This test is only for FFmpeg backend");
// Create stream
if (!canCreateRtpStream())
QSKIP("Rtp stream cannot be created");
auto temporaryFile = copyResourceToTemporaryFile(QUrl(u"qrc:/testdata/colors.mp4"_s),
u"colors.XXXXXX.mp4"_s);
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()
{
QSKIP_OHOS("OH_AVPlayer_SetFDSource cannot read from a generic QIODevice; QMediaPlayer source-device path is not supported");
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);
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::setPosition_doesNotStallInBufferingState_whenSourceIsSeekableDevice()
{
// reproducer for QTBUG-147839
CHECK_SELECTED_URL(m_localWavFile);
// Load the audio file fully into a QBuffer — a fast, fully-seekable in-memory device.
QFile audioFile(u":"_s + m_localWavFile->path());
QVERIFY(audioFile.open(QFile::ReadOnly));
QByteArray audioData = audioFile.readAll();
QVERIFY(!audioData.isEmpty());
QBuffer buffer(&audioData);
QVERIFY(buffer.open(QBuffer::ReadOnly));
QMediaPlayer &player = m_fixture->player;
player.setSourceDevice(&buffer, *m_localWavFile);
QTRY_COMPARE(player.mediaStatus(), QMediaPlayer::LoadedMedia);
player.play();
QTRY_COMPARE(player.mediaStatus(), QMediaPlayer::BufferedMedia);
// Seek while paused. setPosition() resets the demuxer, which re-fills very quickly from
// the in-memory buffer. The queued packetsBuffered signal may arrive while mediaStatus()
// is still LoadedMedia — before the player enters BufferingMedia.
player.pause();
player.setPosition(100);
// Run the event loop so the queued packetsBuffered signal can be delivered
// while the player is still in LoadedMedia.
QTest::qWait(100ms);
player.play();
// The player must reach BufferedMedia; before the fix it got stuck in BufferingMedia forever.
QTRY_COMPARE_EQ(player.mediaStatus(), QMediaPlayer::BufferedMedia);
player.stop();
}
void tst_QMediaPlayerBackend::play_playbackLastsForTheExpectedTime()
{
using namespace std::chrono;
using namespace std::chrono_literals;
QFETCH(const QUrl, media);
QFETCH(const int, loops);
QFETCH(const float, rate);
QFETCH(const bool, pauseBeforePlay);
QSKIP_GSTREAMER("gst_play does not allow precise timing due to pipelining of state changes");
QSKIP_OHOS("OH_AVPlayer reports StoppedState immediately after play() for short test media; loop/rate timing is unreliable");
if (isFFMPEGPlatform() && loops != 1 && pauseBeforePlay)
QSKIP_FFMPEG(
"QTBUG-133652: if we pause before play, setLoops may not be applied correctly");
QMediaPlayer &player = m_fixture->player;
QSignalSpy mediaStatusSpy(&player, &QMediaPlayer::mediaStatusChanged);
player.setSource(media);
// wait for preroll
QTRY_COMPARE(player.mediaStatus(), QMediaPlayer::LoadedMedia);
if (pauseBeforePlay)
player.pause();
if (loops > 1)
player.setLoops(loops);
if (rate != 1.f)
player.setPlaybackRate(rate);
player.play();
auto timer = QElapsedTimer();
timer.start();
QTRY_COMPARE_EQ_WITH_TIMEOUT(player.playbackState(), QMediaPlayer::StoppedState, 10s);
nanoseconds duration{ timer.durationElapsed() };
nanoseconds expectedDuration = milliseconds{ int(loops / rate * 1000) };
QVERIFY2(abs(duration - expectedDuration) < 600ms,
qPrintable(u"expected duration: %1ms, actual duration: %2ms"_s
.arg(round<milliseconds>(expectedDuration).count())
.arg(round<milliseconds>(duration).count())));
QCOMPARE_EQ(player.mediaStatus(), QMediaPlayer::EndOfMedia);
QVERIFY(mediaStatusSpy.contains(QList{ QVariant::fromValue(QMediaPlayer::EndOfMedia) }));
}
void tst_QMediaPlayerBackend::play_playbackLastsForTheExpectedTime_data()
{
QTest::addColumn<QUrl>("media");
QTest::addColumn<int>("loops");
QTest::addColumn<float>("rate");
QTest::addColumn<bool>("pauseBeforePlay");
for (const MaybeUrl &maybeUrl : { m_localWavFile, m_localVideoFile1Sec }) {
if (!maybeUrl)
continue;
for (float rate : { 1.f, 2.f, 0.5f }) {
for (bool pauseBeforePlay : { false, true }) {
for (int loops : { 1, 2 }) {
auto name = QStringLiteral("file %1, loops %2, rate %3, pause before %4")
.arg(maybeUrl->toString())
.arg(loops)
.arg(rate)
.arg(pauseBeforePlay);
QTest::addRow("%s", name.toLatin1().constData())
<< *maybeUrl << loops << rate << pauseBeforePlay;
}
}
}
}
}
void tst_QMediaPlayerBackend::play_threeMediaPlayers()
{
CHECK_SELECTED_URL(m_localVideoFile);
CHECK_SELECTED_URL(m_localVideoFile3ColorsWithSound);
QMediaPlayer player2, player3;
QVideoSink sink2, sink3;
QSignalSpy spy2(&player2, &QMediaPlayer::playbackStateChanged);
QSignalSpy spy3(&player3, &QMediaPlayer::playbackStateChanged);
player2.setVideoOutput(&sink2);
player3.setVideoOutput(&sink3);
m_fixture->clearSpies();
m_fixture->player.setSource(*m_localVideoFile);
player2.setSource(*m_localVideoFile);
player3.setSource(*m_localVideoFile3ColorsWithSound);
m_fixture->player.play();
player2.play();
player3.play();
QTRY_VERIFY(m_fixture->playbackStateChanged.contains(
QList{ QVariant::fromValue(QMediaPlayer::PlayingState) }));
QTRY_VERIFY(spy2.contains(QList{ QVariant::fromValue(QMediaPlayer::PlayingState) }));
QTRY_VERIFY(spy3.contains(QList{ QVariant::fromValue(QMediaPlayer::PlayingState) }));
QCOMPARE(m_fixture->player.error(), QMediaPlayer::NoError);
QCOMPARE(player2.error(), QMediaPlayer::NoError);
QCOMPARE(player3.error(), QMediaPlayer::NoError);
}
void tst_QMediaPlayerBackend::stop_entersStoppedState_whenPlayerWasPaused()
{
QSKIP_OHOS("OH_AVPlayer_Stop() from PausedState emits an extra mediaStatus transition before LoadedMedia");
QFETCH(const MaybeUrl, mediaUrl);
CHECK_SELECTED_URL(mediaUrl);
QMediaPlayer &player = m_fixture->player;
// 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())
QCOMPARE(m_fixture->bufferProgressChanged, SignalList({ { 0.f } }));
QTRY_COMPARE(m_fixture->player.position(), qint64(0));
QSKIP_GSTREAMER("QTBUG-124005: spurious failures with gstreamer - possibly due to EOS?");
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()
{
QSKIP_OHOS("OH_AVPlayer does not always reset position to zero on Stop() after AV_COMPLETED");
CHECK_SELECTED_URL(m_localVideoFile3ColorsWithSound);
// 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 =
isWMFPlatform() || 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()
{
QSKIP_OHOS("OH_AVPlayer does not support negative or zero playback rates");
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 5 should result in a duration of 3s, but in CI,
// time measurements may be quite off. We can therefore only do basic
// sanity checking to make sure the playback has a duration greater
// than zero, and less than the video duration at normal playback rate
auto minDuration = 1s; // Reasonable approximation for zero
auto maxDuration = 14s; // Approximation for less than 15 seconds
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)
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;
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");
QSKIP_OHOS("OH_AVPlayer end-of-stream handling does not follow the cross-platform signal contract");
QMediaPlayer &player = m_fixture->player;
QSignalSpy &mediaStatusChanged = m_fixture->mediaStatusChanged;
QSignalSpy &playbackStateChanged = m_fixture->playbackStateChanged;
QSignalSpy &positionChanged = m_fixture->positionChanged;
QSignalSpy &bufferProgressChanged = m_fixture->bufferProgressChanged;
CHECK_SELECTED_URL(m_localWavFile);
player.setSource(*m_localWavFile);
player.play();
player.setPosition(900);
//wait up to 5 seconds for EOS
QTRY_COMPARE(player.mediaStatus(), QMediaPlayer::EndOfMedia);
QVERIFY(mediaStatusChanged.size() > 0);
QCOMPARE(mediaStatusChanged.last()[0].value<QMediaPlayer::MediaStatus>(),
QMediaPlayer::EndOfMedia);
QCOMPARE(player.playbackState(), QMediaPlayer::StoppedState);
QCOMPARE(playbackStateChanged.size(), 2);
QCOMPARE(playbackStateChanged.last()[0].value<QMediaPlayer::PlaybackState>(),
QMediaPlayer::StoppedState);
//at EOS the position stays at the end of file
QCOMPARE(player.position(), player.duration());
QTRY_VERIFY(positionChanged.size() > 0);
QTRY_COMPARE(positionChanged.last()[0].value<qint64>(), player.duration());
playbackStateChanged.clear();
mediaStatusChanged.clear();
positionChanged.clear();
player.play();
//position is reset to start
QTRY_COMPARE_LT(player.position(), 500);
QTRY_VERIFY(positionChanged.size() > 0);
QCOMPARE(positionChanged.first()[0].value<qint64>(), 0);
QCOMPARE(player.playbackState(), QMediaPlayer::PlayingState);
QTRY_VERIFY(player.mediaStatus() == QMediaPlayer::BufferedMedia
|| player.mediaStatus() == QMediaPlayer::EndOfMedia);
QCOMPARE(playbackStateChanged.size(), 1);
QCOMPARE(playbackStateChanged.last()[0].value<QMediaPlayer::PlaybackState>(),
QMediaPlayer::PlayingState);
QVERIFY(mediaStatusChanged.size() > 0);
QCOMPARE(mediaStatusChanged.last()[0].value<QMediaPlayer::MediaStatus>(),
QMediaPlayer::BufferedMedia);
positionChanged.clear();
QTRY_VERIFY(player.position() > 100);
QTRY_VERIFY(positionChanged.size() > 0 && positionChanged.last()[0].value<qint64>() > 100);
player.setPosition(900);
//wait up to 5 seconds for EOS
QTRY_COMPARE(player.mediaStatus(), QMediaPlayer::EndOfMedia);
QVERIFY(mediaStatusChanged.size() > 0);
QCOMPARE(mediaStatusChanged.last()[0].value<QMediaPlayer::MediaStatus>(),
QMediaPlayer::EndOfMedia);
QCOMPARE(player.playbackState(), QMediaPlayer::StoppedState);
QCOMPARE(playbackStateChanged.size(), 2);
QCOMPARE(playbackStateChanged.last()[0].value<QMediaPlayer::PlaybackState>(),
QMediaPlayer::StoppedState);
QCOMPARE_GT(bufferProgressChanged.size(), 1);
QCOMPARE(bufferProgressChanged.back().front(), 0.f);
// position stays at the end of file
QCOMPARE(player.position(), player.duration());
QTRY_VERIFY(positionChanged.size() > 0);
QTRY_COMPARE(positionChanged.last()[0].value<qint64>(), player.duration());
//after setPosition EndOfMedia status should be reset to Loaded
playbackStateChanged.clear();
mediaStatusChanged.clear();
player.setPosition(500);
//this transition can be async, so allow backend to perform it
QTRY_COMPARE(player.mediaStatus(), QMediaPlayer::LoadedMedia);
QCOMPARE(playbackStateChanged.size(), 0);
QTRY_VERIFY(mediaStatusChanged.size() > 0
&& mediaStatusChanged.last()[0].value<QMediaPlayer::MediaStatus>()
== QMediaPlayer::LoadedMedia);
player.play();
player.setPosition(900);
//wait up to 5 seconds for EOS
QTRY_COMPARE(player.mediaStatus(), QMediaPlayer::EndOfMedia);
QCOMPARE(player.playbackState(), QMediaPlayer::StoppedState);
QCOMPARE(player.position(), player.duration());
playbackStateChanged.clear();
mediaStatusChanged.clear();
positionChanged.clear();
// pause() should reset position to beginning and status to Buffered
player.pause();
QTRY_COMPARE(player.position(), 0);
QTRY_VERIFY(positionChanged.size() > 0);
QTRY_COMPARE(positionChanged.first()[0].value<qint64>(), 0);
QCOMPARE(player.playbackState(), QMediaPlayer::PausedState);
QTRY_COMPARE(player.mediaStatus(), QMediaPlayer::BufferedMedia);
QCOMPARE(playbackStateChanged.size(), 1);
QCOMPARE(playbackStateChanged.last()[0].value<QMediaPlayer::PlaybackState>(),
QMediaPlayer::PausedState);
QVERIFY(mediaStatusChanged.size() > 0);
QCOMPARE(m_fixture->mediaStatusChanged.last()[0].value<QMediaPlayer::MediaStatus>(), QMediaPlayer::BufferedMedia);
}
// Helper class for tst_QMediaPlayerBackend::deleteLaterAtEOS()
class DeleteLaterAtEos : public QObject
{
Q_OBJECT
public:
DeleteLaterAtEos(QMediaPlayer* p) : player(p)
{
}
public slots:
void play()
{
QVERIFY(connect(player, SIGNAL(mediaStatusChanged(QMediaPlayer::MediaStatus)),
this, SLOT(onMediaStatusChanged(QMediaPlayer::MediaStatus))));
player->play();
}
private slots:
void onMediaStatusChanged(QMediaPlayer::MediaStatus status)
{
if (status == QMediaPlayer::EndOfMedia) {
player->deleteLater();
player = nullptr;
}
}
private:
QMediaPlayer* player;
};
// Regression test for
// QTBUG-24927 - deleteLater() called to QMediaPlayer from its signal handler does not work as expected
void tst_QMediaPlayerBackend::deleteLaterAtEOS()
{
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->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, &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::playToEOS_finishesWithEmptyFrame()
{
QSKIP_OHOS("OH_AVPlayer does not flush the video sink with an empty frame on AV_COMPLETED");
CHECK_SELECTED_URL(m_localVideoFile3ColorsWithSound);
QVector<bool> frameValidState;
TestVideoSink surface(false);
QObject::connect(&surface, &QVideoSink::videoFrameChanged, &surface,
[&](const QVideoFrame &frame) {
frameValidState.append(frame.isValid());
});
QMediaPlayer player;
player.setVideoSink(&surface);
player.setSource(*m_localVideoFile3ColorsWithSound);
player.play();
// play to end
QTRY_COMPARE(player.mediaStatus(), QMediaPlayer::EndOfMedia);
QTRY_VERIFY(!frameValidState.isEmpty());
// ensure that we end with an empty frame
QTRY_VERIFY(!frameValidState.back());
// while all earlier frames are valid
frameValidState.pop_back();
QVERIFY(std::all_of(frameValidState.begin(), frameValidState.end(), q20::identity{}));
constexpr int framesInMedia = 77;
constexpr int expectedMaxFrameCount = framesInMedia + 1;
constexpr bool strictFrameCountValidation = false;
if constexpr (strictFrameCountValidation) {
QCOMPARE_EQ(surface.m_totalFrames, expectedMaxFrameCount);
} else {
// the backend may drop frames, so there's no reasonable lower limit
QCOMPARE_LE(surface.m_totalFrames, expectedMaxFrameCount);
}
}
void tst_QMediaPlayerBackend::volumeAcrossFiles_data()
{
QTest::addColumn<int>("volume");
QTest::addColumn<bool>("muted");
QTest::newRow("100 unmuted") << 100 << false;
QTest::newRow("50 unmuted") << 50 << false;
QTest::newRow("0 unmuted") << 0 << false;
QTest::newRow("100 muted") << 100 << true;
QTest::newRow("50 muted") << 50 << true;
QTest::newRow("0 muted") << 0 << true;
}
void tst_QMediaPlayerBackend::volumeAcrossFiles()
{
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(output.volume() > 0);
QVERIFY(!output.isMuted());
output.setVolume(vol);
output.setMuted(muted);
QTRY_COMPARE(output.volume(), vol);
QTRY_COMPARE(output.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(output.volume(), vol);
QCOMPARE(output.isMuted(), muted);
player.setSource(QUrl());
QTRY_COMPARE(output.volume(), vol);
QCOMPARE(output.isMuted(), muted);
player.setSource(*m_localWavFile);
player.pause();
QTRY_COMPARE(output.volume(), vol);
QCOMPARE(output.isMuted(), muted);
}
void tst_QMediaPlayerBackend::initialVolume()
{
CHECK_SELECTED_URL(m_localWavFile);
{
QAudioOutput output;
QMediaPlayer player;
player.setAudioOutput(&output);
output.setVolume(1);
player.setSource(*m_localWavFile);
QCOMPARE(output.volume(), 1);
player.play();
QTRY_COMPARE(player.mediaStatus(), QMediaPlayer::EndOfMedia);
QCOMPARE(output.volume(), 1);
}
{
QAudioOutput output;
QMediaPlayer player;
player.setAudioOutput(&output);
player.setSource(*m_localWavFile);
QCOMPARE(output.volume(), 1);
player.play();
QTRY_COMPARE(player.mediaStatus(), QMediaPlayer::EndOfMedia);
QCOMPARE(output.volume(), 1);
}
}
void tst_QMediaPlayerBackend::seekPauseSeek()
{
QSKIP_OHOS("OH_AVPlayer_Seek timing does not produce the expected frame sequence in PausedState");
#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;
player.setAudioOutput(&output);
QSignalSpy positionSpy(&player, &QMediaPlayer::positionChanged);
player.setVideoOutput(&surface);
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());
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
player.pause();
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)
QSKIP("No timestamp");
{
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());
// Darwin returns coded dimensions (160x120), others apply PAR (213x120)
if (!isDarwinPlatform())
QCOMPARE(frame.width(), 213);
QCOMPARE(frame.height(), 120);
// create QImage for QVideoFrame to verify RGB pixel colors
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);
}
surface.m_frameList.clear();
positionSpy.clear();
position = 12000;
player.setPosition(position);
QTRY_VERIFY(!positionSpy.isEmpty() && qAbs(player.position() - position) < (qint64)500);
QCOMPARE(player.playbackState(), QMediaPlayer::PausedState);
QTRY_VERIFY(!surface.m_frameList.isEmpty());
{
QVideoFrame frame = surface.m_frameList.back();
const qint64 elapsed = (frame.startTime() / 1000) - position;
QVERIFY2(qAbs(elapsed) < (qint64)500, QByteArray::number(elapsed).constData());
// Darwin returns coded dimensions (160x120), others apply PAR (213x120)
if (!isDarwinPlatform())
QCOMPARE(frame.width(), 213);
QCOMPARE(frame.height(), 120);
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);
}
}
void tst_QMediaPlayerBackend::seekInStoppedState()
{
QSKIP_GSTREAMER("QTBUG-124005: spurious failures with gstreamer");
QSKIP_OHOS("OH_AVPlayer_Seek is rejected while the player is in AV_STOPPED");
CHECK_SELECTED_URL(m_localVideoFile);
TestVideoSink surface(false);
QAudioOutput output;
QMediaPlayer player;
player.setAudioOutput(&output);
player.setVideoOutput(&surface);
QSignalSpy stateSpy(&player, &QMediaPlayer::playbackStateChanged);
QSignalSpy positionSpy(&player, &QMediaPlayer::positionChanged);
player.setSource(*m_localVideoFile);
QTRY_COMPARE(player.mediaStatus(), QMediaPlayer::LoadedMedia);
QCOMPARE(player.playbackState(), QMediaPlayer::StoppedState);
QCOMPARE(player.position(), 0);
QVERIFY(player.isSeekable());
stateSpy.clear();
positionSpy.clear();
qint64 position = 5000;
player.setPosition(position);
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();
QCOMPARE(player.playbackState(), QMediaPlayer::PlayingState);
QTRY_COMPARE_GT(player.position(), position);
QTest::qWait(100);
// Check that it never played from the beginning
QCOMPARE_GT(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.playbackState(), QMediaPlayer::StoppedState);
QTRY_COMPARE(player.mediaStatus(), QMediaPlayer::LoadedMedia);
QTRY_COMPARE(player.position(), 0);
stateSpy.clear();
positionSpy.clear();
player.setPosition(position);
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();
QCOMPARE(player.playbackState(), QMediaPlayer::PlayingState);
QVERIFY(qAbs(player.position() - position) < qint64(200));
QTest::qWait(500);
// Check that it never played from the beginning
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
QSKIP_WMF("IMFMediaEngine: rebuffering after seek from EndOfMedia "
"on byte stream sources exceeds the QTRY timeout");
player.setPosition(player.duration() - 100);
QTRY_COMPARE(player.mediaStatus(), QMediaPlayer::EndOfMedia);
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(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);
player.play();
QTRY_COMPARE(player.playbackState(), QMediaPlayer::PlayingState);
QTRY_VERIFY(player.mediaStatus() == QMediaPlayer::BufferedMedia
|| player.mediaStatus() == QMediaPlayer::EndOfMedia);
positionSpy.clear();
QTRY_COMPARE_GT(player.position(), (position - 200));
QTest::qWait(500);
// Check that it never played from the beginning
QCOMPARE_GT(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()
{
QSKIP_GSTREAMER("QTBUG-124005: spurious seek failures with gstreamer");
QSKIP_OHOS("OH_AVPlayer position reporting is unreliable across subsequent setSource calls");
if (isDarwinPlatform() && isCI() && QSysInfo::currentCpuArchitecture() == "x86_64")
QSKIP("AVFoundation playback unreliable on x86_64 CI machines");
CHECK_SELECTED_URL(m_localCompressedSoundFile);
QAudioOutput output;
QMediaPlayer player;
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.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);
QCOMPARE_GT(player.position(), 0);
player.play();
QTRY_COMPARE(player.playbackState(), QMediaPlayer::PlayingState);
QTRY_COMPARE_GT(player.position(), 1000);
player.pause();
QCOMPARE(player.playbackState(), QMediaPlayer::PausedState);
// make sure position does not "jump" closer to the end of the file
QCOMPARE_GT(player.position(), 1000);
// try to seek back to zero
player.setPosition(0);
QTRY_COMPARE(player.position(), qint64(0));
player.play();
QCOMPARE(player.playbackState(), QMediaPlayer::PlayingState);
QTRY_COMPARE_GT(player.position(), 1000);
player.pause();
QCOMPARE(player.playbackState(), QMediaPlayer::PausedState);
QCOMPARE_GT(player.position(), 1000);
}
void tst_QMediaPlayerBackend::subsequentPlayback_playsForExpectedDuration()
{
using namespace std::chrono_literals;
QSKIP_GSTREAMER("QTBUG-127346: subsequent playback finishes almost immediately");
QSKIP_OHOS("OH_AVPlayer playback duration is unreliable across subsequent setSource calls");
if (isDarwinPlatform() && isCI() && QSysInfo::currentCpuArchitecture() == "x86_64")
QSKIP("AVFoundation playback unreliable on x86_64 CI machines");
CHECK_SELECTED_URL(m_localCompressedSoundFile);
QMediaPlayer &player = m_fixture->player;
player.setSource(*m_localCompressedSoundFile);
QTRY_COMPARE(player.mediaStatus(), QMediaPlayer::LoadedMedia);
player.setPosition(5000);
player.play();
QVERIFY(player.position() >= 5000);
QTRY_COMPARE_WITH_TIMEOUT(player.mediaStatus(), QMediaPlayer::EndOfMedia, 10s);
QElapsedTimer timer;
timer.start();
// playback should take 7 seconds
player.play();
QCOMPARE_NE(player.mediaStatus(), QMediaPlayer::EndOfMedia);
QTRY_COMPARE_WITH_TIMEOUT(player.mediaStatus(), QMediaPlayer::EndOfMedia, 15s);
std::chrono::nanoseconds duration = timer.durationElapsed();
QCOMPARE_GE(duration + 100ms, std::chrono::milliseconds(player.duration()));
QCOMPARE_LT(duration, std::chrono::seconds(12));
}
void tst_QMediaPlayerBackend::multipleMediaPlayback()
{
QSKIP_OHOS("OH_AVPlayer playback duration / position contract differs from baseline");
CHECK_SELECTED_URL(m_localVideoFile);
CHECK_SELECTED_URL(m_localVideoFile2);
QAudioOutput output;
TestVideoSink surface(false);
QMediaPlayer player;
player.setVideoOutput(&surface);
player.setAudioOutput(&output);
player.setSource(*m_localVideoFile);
QCOMPARE(player.source(), *m_localVideoFile);
QTRY_COMPARE(player.mediaStatus(), QMediaPlayer::LoadedMedia);
player.setPosition(0);
player.play();
QCOMPARE(player.error(), QMediaPlayer::NoError);
QCOMPARE(player.playbackState(), QMediaPlayer::PlayingState);
QVERIFY(player.isSeekable());
QTRY_COMPARE_GT(player.position(), 0);
QCOMPARE(player.source(), *m_localVideoFile);
player.stop();
player.setSource(*m_localVideoFile2);
QCOMPARE(player.source(), *m_localVideoFile2);
QTRY_COMPARE(player.mediaStatus(), QMediaPlayer::LoadedMedia);
QTRY_VERIFY(player.isSeekable());
player.setPosition(0);
player.play();
QCOMPARE(player.error(), QMediaPlayer::NoError);
QCOMPARE(player.playbackState(), QMediaPlayer::PlayingState);
QTRY_COMPARE_GT(player.position(), 0);
QCOMPARE(player.source(), *m_localVideoFile2);
player.stop();
QTRY_COMPARE(player.playbackState(), QMediaPlayer::StoppedState);
}
void tst_QMediaPlayerBackend::multiplePlaybackRateChangingStressTest()
{
QSKIP_OHOS("OH_AVPlayer position does not advance reliably under rapid SetPlaybackRate stress");
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.");
}
QSKIP_GSTREAMER(
"playback rate changes are flushing the pipeline, so this test is not representative");
QSKIP_WMF("IMFMediaEngine: SetPlaybackRate is not instantaneous");
TestVideoSink surface(false);
QAudioOutput output;
QMediaPlayer player;
player.setAudioOutput(&output);
player.setVideoOutput(&surface);
player.setSource(*m_localVideoFile3ColorsWithSound);
player.play();
surface.waitForFrame();
QSignalSpy spy(&player, &QMediaPlayer::playbackStateChanged);
using namespace std::chrono_literals;
using namespace std::chrono;
constexpr milliseconds expectedVideoDuration = 3000ms;
constexpr milliseconds waitingInterval = 200ms;
constexpr milliseconds maxDuration = expectedVideoDuration + 2000ms;
constexpr milliseconds minDuration = expectedVideoDuration - 100ms;
constexpr milliseconds maxFrameDelay = 2000ms;
surface.m_elapsedTimer.start();
nanoseconds duration = 0ns;
auto waitForPlaybackStateChange = [&]() {
QElapsedTimer timer;
timer.start();
QScopeGuard addDuration([&]() {
duration += duration_cast<nanoseconds>(timer.durationElapsed() * player.playbackRate());
});
return spy.wait(waitingInterval);
};
for (int i = 0; !waitForPlaybackStateChange(); ++i) {
player.setPlaybackRate(0.5 * (i % 4 + 1));
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.
* 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::multipleSeekStressTest()
{
QSKIP_GSTREAMER("QTBUG-124005: spurious test failures with gstreamer");
QSKIP_DARWIN("hasNewPixelBufferForItemTime returns false after rapid seeks");
QSKIP_OHOS("OH_AVPlayer_Seek does not reliably deliver a frame on every seek under stress");
QSKIP_WMF("IMFMediaEngine: OnVideoStreamTick may not return a valid frame after rapid seeks");
#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;
player.setAudioOutput(&output);
player.setVideoOutput(&surface);
player.setSource(*m_localVideoFile3ColorsWithSound);
player.play();
auto waitAndCheckFrame = [&](qint64 pos, QString checkInfo) {
auto errorPrintingGuard = qScopeGuard([&]() {
qDebug() << "Error:" << checkInfo;
qDebug() << "Position:" << pos;
});
auto frame = surface.waitForFrame();
QVERIFY(frame.isValid());
const auto trackTime = pos * 1000;
// 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_GE(frame.startTime(), trackTime - 200'000);
QCOMPARE_LE(frame.endTime(), trackTime + 200'000);
auto frameImage = frame.toImage();
const auto actualColor = frameImage.pixel(1, 1);
const auto actualColorIndex = findSimilarColorIndex(m_video3Colors, actualColor);
const auto expectedColorIndex = pos / 1000;
QCOMPARE(actualColorIndex, expectedColorIndex);
errorPrintingGuard.dismiss();
};
auto seekAndCheck = [&](qint64 pos) {
QSignalSpy positionSpy(&player, &QMediaPlayer::positionChanged);
player.setPosition(pos);
QTRY_VERIFY(positionSpy.size() >= 1);
int setPosition = positionSpy.first().first().toInt();
constexpr int threshold = 160; // 160ms threshold for position deviation
QCOMPARE_GT(setPosition, pos - threshold);
QCOMPARE_LT(setPosition, pos + threshold);
};
constexpr qint64 posInterval = 10;
{
for (qint64 pos = posInterval; pos <= 2200; pos += posInterval)
seekAndCheck(pos);
waitAndCheckFrame(2200, "emulate fast moving of a seek slider forward");
QCOMPARE_NE(player.mediaStatus(), QMediaPlayer::EndOfMedia);
QCOMPARE(player.playbackState(), QMediaPlayer::PlayingState);
}
{
for (qint64 pos = 2100; pos >= 800; pos -= posInterval)
seekAndCheck(pos);
waitAndCheckFrame(800, "emulate fast moving of a seek slider backward");
QCOMPARE_NE(player.mediaStatus(), QMediaPlayer::EndOfMedia);
QCOMPARE(player.playbackState(), QMediaPlayer::PlayingState);
}
{
player.pause();
for (qint64 pos = 500; pos <= 1100; pos += posInterval)
seekAndCheck(pos);
waitAndCheckFrame(1100, "emulate fast moving of a seek slider forward on paused state");
QCOMPARE_NE(player.mediaStatus(), QMediaPlayer::EndOfMedia);
QCOMPARE(player.playbackState(), QMediaPlayer::PausedState);
}
}
void tst_QMediaPlayerBackend::setPlaybackRate_changesActualRateAndFramesRenderingTime_data()
{
QTest::addColumn<bool>("withAudio");
QTest::addColumn<int>("positionDeviationMs");
QTest::newRow("Without audio") << false << 170;
// set greater positionDeviationMs for case with audio due to possible synchronization.
QTest::newRow("With audio") << true << 200;
}
void tst_QMediaPlayerBackend::setPlaybackRate_changesActualRateAndFramesRenderingTime()
{
QSKIP_GSTREAMER("QTBUG-124005: timing issues");
QSKIP_OHOS("OH_AVPlayer playback rate is a discrete enum and frame timing differs from real-time scaling");
QSKIP_WMF("IMFMediaEngine: SetPlaybackRate is not instantaneous, causing position lag after "
"rate change");
QMediaPlayer &player = m_fixture->player;
QFETCH(bool, withAudio);
QFETCH(int, positionDeviationMs);
#ifdef Q_OS_ANDROID
QSKIP("frame.toImage will return null image because of QTBUG-108446");
#endif
CHECK_SELECTED_URL(m_localVideoFile3ColorsWithSound);
#ifdef Q_OS_MACOS
if (isCI())
QSKIP("SKIP on macOS CI since multiple fake drawing on macOS CI platform causes UB. To be "
"investigated: QTBUG-111744");
#endif
player.setAudioOutput(
withAudio ? &m_fixture->output
: nullptr); // TODO: mock audio output and check sound by frequency
player.setSource(*m_localVideoFile3ColorsWithSound);
auto checkColorAndPosition = [&](qint64 expectedPosition, QString errorTag) {
constexpr qint64 intervalTime = 1000;
const int colorIndex = expectedPosition / intervalTime;
const auto expectedColor = m_video3Colors[colorIndex];
const auto actualPosition = 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(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();
};
player.play();
m_fixture->surface.waitForFrame();
auto waitUntil = [&](qint64 targetPosition) {
const auto position = player.position();
const auto waitingIntervalMs =
static_cast<int>((targetPosition - position) / player.playbackRate());
if (targetPosition > position)
QTest::qWait(waitingIntervalMs);
qDebug() << "Test waiting:" << waitingIntervalMs << "ms, Position:" << position << "=>"
<< player.position() << "Expected target position:" << targetPosition
<< "playbackRate:" << player.playbackRate();
};
waitUntil(400);
checkColorAndPosition(400, "Check default playback rate");
player.setPlaybackRate(2.);
waitUntil(1400);
checkColorAndPosition(1400, "Check 2.0 playback rate");
player.setPlaybackRate(0.5);
waitUntil(1800);
checkColorAndPosition(1800, "Check 0.5 playback rate");
player.setPlaybackRate(0.321);
player.stop();
}
void tst_QMediaPlayerBackend::surfaceTest()
{
QSKIP_GSTREAMER("QTBUG-124005: spurious failure, probably asynchronous event delivery");
if (isDarwinPlatform() && isCI())
QSKIP("Flaky on CI: frame count depends on system load");
CHECK_SELECTED_URL(m_localVideoFile);
// 25 fps video file
QAudioOutput output;
TestVideoSink surface(false);
QMediaPlayer player;
player.setAudioOutput(&output);
player.setVideoOutput(&surface);
player.setSource(*m_localVideoFile);
player.play();
QTRY_VERIFY(player.position() >= 1000);
QVERIFY2(surface.m_totalFrames >= 25, qPrintable(QStringLiteral("Expected >= 25, got %1").arg(surface.m_totalFrames)));
}
void tst_QMediaPlayerBackend::metadata()
{
QSKIP_OHOS("OH_AVPlayer does not extract container metadata (Title/Author/CoverArt) from MP3");
QMediaMetaData::Key coverArtKey = QMediaMetaData::CoverArtImage;
CHECK_SELECTED_URL(m_localMp3FileWithMetadataAndEmbeddedThumbnail);
m_fixture->player.setSource(*m_localMp3FileWithMetadataAndEmbeddedThumbnail);
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"));
static QList allowedDurations{
QVariant(7680), // FFmpeg-8
QVariant(7704), // FFmpeg-7 and earlier, avfoundation
QVariant(7383), // IMFMediaEngine (Windows)
};
const QVariant durationVar = metadata.value(QMediaMetaData::Duration);
QVERIFY2(allowedDurations.contains(durationVar),
qPrintable(u"Unexpected duration: "_s + durationVar.toString()));
// macOS 15 and earlier: AVFoundation does not return ID3 artwork (APIC)
// for MP3 files through any metadata API
if (isDarwinPlatform()
&& QOperatingSystemVersion::current() <= QOperatingSystemVersion::MacOSSequoia) {
QEXPECT_FAIL("", "APIC metadata unavailable for MP3 on macOS <= 15", Continue);
}
QVERIFY(!metadata.value(coverArtKey).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_returnsMetadataWithThumbnail_whenMediaHasThumbnail_data()
{
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);
}
void tst_QMediaPlayerBackend::metadata_returnsMetadataWithThumbnail_whenMediaHasThumbnail()
{
QSKIP_OHOS("OH_AVPlayer does not expose embedded thumbnail/cover art from MP3/MP4 metadata");
QMediaMetaData::Key key = QMediaMetaData::CoverArtImage;
// Arrange
QFETCH(const MaybeUrl, mediaUrl);
QFETCH(const bool, hasThumbnail);
QFETCH(const QSize, expectedSize);
QFETCH(const QColor, expectedColor);
CHECK_SELECTED_URL(mediaUrl);
// MP4 artwork is only available via IPropertyStore which requires
// a file path — not supported for qrc:// byte stream sources.
if (isWMFPlatform() && hasThumbnail)
QSKIP_WMF("MP4 artwork requires a file path (IPropertyStore)");
m_fixture->player.setSource(*mediaUrl);
QTRY_VERIFY(!m_fixture->metadataChanged.empty());
// Act
const QMediaMetaData metadata = m_fixture->player.metaData();
const QImage thumbnail = metadata.value(key).value<QImage>();
// Assert
QCOMPARE_EQ(!thumbnail.isNull(), hasThumbnail);
QCOMPARE_EQ(thumbnail.size(), expectedSize);
if (hasThumbnail) {
const QPoint center{ expectedSize.width() / 2, expectedSize.height() / 2 };
const auto centerColor = thumbnail.pixelColor(center);
constexpr int maxChannelDiff = 5;
QCOMPARE_LT(std::abs(centerColor.red() - expectedColor.red()), maxChannelDiff);
QCOMPARE_LT(std::abs(centerColor.green() - expectedColor.green()), maxChannelDiff);
QCOMPARE_LT(std::abs(centerColor.blue() - expectedColor.blue()), maxChannelDiff);
}
}
void tst_QMediaPlayerBackend::metadata_returnsMetadataWithHasHdrContent_whenMediaHasHdrContent_data()
{
QTest::addColumn<MaybeUrl>("mediaUrl");
QTest::addColumn<bool>("hasHdrContent");
QTest::addRow("SDR Video") << m_localVideoFile << false;
QTest::addRow("HDR Video") << m_hdrVideo << true;
}
void tst_QMediaPlayerBackend::metadata_returnsMetadataWithHasHdrContent_whenMediaHasHdrContent()
{
QFETCH(const MaybeUrl, mediaUrl);
QFETCH(const bool, hasHdrContent);
if (!isFFMPEGPlatform() && !isDarwinPlatform())
QSKIP("This test is only for FFmpeg and Darwin backends");
CHECK_SELECTED_URL(mediaUrl);
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::metadata_returnsMetadataWithCorrectDate_data()
{
QTest::addColumn<MaybeUrl>("mediaUrl");
QTest::addRow("With Recorded date") << m_withDate;
QTest::addRow("With Encoded date") << m_withCreationTime;
QTest::addRow("With Recorded date and Encoded date") << m_withDateAndCreationTime;
QTest::addRow("With QuickTime creation date and Encoded date") << m_withQtDateAndCreationTime;
}
void tst_QMediaPlayerBackend::metadata_returnsMetadataWithCorrectDate()
{
QSKIP_OHOS("OH_AVPlayer does not extract container creation/encoded date from MP4 metadata");
QFETCH(const MaybeUrl, mediaUrl);
if (mediaUrl == m_withQtDateAndCreationTime)
QSKIP_GSTREAMER("GStreamer doesn't expose com.apple.quicktime.creationdate");
if (mediaUrl == m_withQtDateAndCreationTime)
QSKIP_DARWIN("Non-standard QuickTime metadata keys are not mapped");
// MF does not map MP4 date atoms to any metadata property
// accessible via IPropertyStore or IMFMetadataProvider.
QSKIP_WMF("MF does not expose date metadata from MP4 containers");
CHECK_SELECTED_URL(mediaUrl);
m_fixture->player.setSource(*mediaUrl);
QTRY_VERIFY(!m_fixture->metadataChanged.empty());
const QMediaMetaData metadata = m_fixture->player.metaData();
QCOMPARE_EQ(metadata.value(QMediaMetaData::Date).value<QDateTime>(),
QDateTime::fromString("1995-04-18T00:00:00Z", Qt::ISODate));
}
void tst_QMediaPlayerBackend::playerStateAtEOS()
{
CHECK_SELECTED_URL(m_localWavFile);
QAudioOutput output;
QMediaPlayer player;
player.setAudioOutput(&output);
bool endOfMediaReceived = false;
connect(&player, &QMediaPlayer::mediaStatusChanged,
this, [&](QMediaPlayer::MediaStatus status) {
if (status == QMediaPlayer::EndOfMedia) {
QTRY_COMPARE(player.playbackState(), QMediaPlayer::StoppedState);
endOfMediaReceived = true;
}
});
player.setSource(*m_localWavFile);
player.play();
QTRY_COMPARE(player.mediaStatus(), QMediaPlayer::EndOfMedia);
QVERIFY(endOfMediaReceived);
}
void tst_QMediaPlayerBackend::playFromBuffer()
{
QSKIP_GSTREAMER("QTBUG-124005: spurious failure, probably asynchronous event delivery");
if (isDarwinPlatform() && isCI())
QSKIP("Flaky on CI: frame count depends on system load");
CHECK_SELECTED_URL(m_localVideoFile);
TestVideoSink surface(false);
QMediaPlayer player;
player.setVideoOutput(&surface);
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);
QVERIFY2(surface.m_totalFrames >= 25, qPrintable(QStringLiteral("Expected >= 25, got %1").arg(surface.m_totalFrames)));
}
void tst_QMediaPlayerBackend::playFromSequentialStream()
{
QSKIP_OHOS("OH_AVPlayer_SetFDSource cannot consume a generic sequential QIODevice");
using namespace std::chrono_literals;
CHECK_SELECTED_URL(m_localVideoFile);
QSKIP_DARWIN("AVAssetResourceLoader delegate does not support sequential QIODevice");
TestVideoSink surface(false);
QMediaPlayer player;
player.setVideoOutput(&surface);
QSequentialFileAdaptor file(u":"_s + m_localVideoFile->toEncoded(QUrl::RemoveScheme));
QObject::connect(&player, &QMediaPlayer::errorOccurred, &player, [] {
QFAIL("playFromSequentialStream: error occurred");
});
QVERIFY(file.open(QIODevice::ReadOnly));
player.setSourceDevice(&file, *m_localVideoFile);
QCOMPARE(player.isSeekable(), false);
player.play();
QTRY_COMPARE_GE(player.position(), 1000);
QCOMPARE(player.isSeekable(), false);
player.setPosition(0);
QCOMPARE_GE(player.position(), 1000);
QTest::qWait(200ms);
QTRY_COMPARE_GE(player.position(), 1000);
}
void tst_QMediaPlayerBackend::audioVideoAvailable()
{
QSKIP_OHOS("OH_AVPlayer reports audio/video track availability slightly differently from baseline");
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);
}
void tst_QMediaPlayerBackend::audioVideoAvailable_updatedOnNewMedia()
{
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);
QCOMPARE(hasAudioSpy.size(), 0);
}
void tst_QMediaPlayerBackend::isSeekable()
{
CHECK_SELECTED_URL(m_localVideoFile);
TestVideoSink surface(false);
QMediaPlayer player;
player.setVideoOutput(&surface);
QVERIFY(!player.isSeekable());
player.setSource(*m_localVideoFile);
QTRY_VERIFY(player.isSeekable());
}
void tst_QMediaPlayerBackend::positionAfterSeek()
{
CHECK_SELECTED_URL(m_localVideoFile);
TestVideoSink surface(false);
QMediaPlayer player;
player.setVideoOutput(&surface);
QVERIFY(!player.isSeekable());
player.setSource(*m_localVideoFile);
QTRY_COMPARE(player.mediaStatus(), QMediaPlayer::LoadedMedia);
player.pause();
player.setPosition(500);
QTRY_COMPARE(player.position(), 500);
player.setPosition(700);
QVERIFY(player.position() != 0);
QTRY_COMPARE(player.position(), 700);
player.play();
QTRY_COMPARE_GT(player.position(), 700);
player.setPosition(200);
QVERIFY(player.position() != 0);
QTRY_COMPARE_LT(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()
{
QSKIP_OHOS("OH_AVPlayer does not deliver a paused-state preroll frame at the expected resolution");
QSKIP_GSTREAMER("gst_play may end up in error handling code");
QFETCH(const MaybeUrl, mediaFile);
QFETCH(const int, width);
QFETCH(const int, height);
CHECK_SELECTED_URL(mediaFile);
if (isWMFPlatform() && mediaFile == m_av1File)
QSKIP_WMF("IMFMediaEngine does not apply PAR for AV1/WebM files");
// Arrange
TestVideoSink surface(true);
QMediaPlayer player;
player.setVideoOutput(&surface);
QVERIFY(!player.isSeekable());
player.setSource(*mediaFile);
QTRY_COMPARE(player.mediaStatus(), QMediaPlayer::LoadedMedia);
// Act
player.pause();
#ifndef Q_OS_ANDROID
// isCI() does not work on Android (variable is set on host instead of device where tests are run)
if (isCI() && isFFMPEGPlatform())
#endif
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()
{
QSKIP_OHOS("OH_AVPlayer position is reported in coarser increments than the test expects");
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);
QCOMPARE_GT(player.position(), 950);
QCOMPARE_LT(player.position(), 1050);
QTRY_COMPARE_GT(player.position(), 1050);
player.pause();
player.setPosition(500);
QCOMPARE_GT(player.position(), 450);
QCOMPARE_LT(player.position(), 550);
QTest::qWait(200);
QCOMPARE_GT(player.position(), 450);
QCOMPARE_LT(player.position(), 550);
using namespace std::chrono;
using namespace std::chrono_literals;
// colors.mp4 is 25fps
// ffmpeg will round down the start time of the frame fo 480ms, while gstreamer will set a
// reduced frame time
const QVideoFrame &lastFrame = surface.m_frameList.back();
if (isGStreamerPlatform()) {
QCOMPARE_EQ(microseconds(lastFrame.startTime()), 500ms);
QCOMPARE_EQ(microseconds(lastFrame.endTime()), 520ms);
} else if (isWMFPlatform()) {
QCOMPARE_EQ(microseconds(lastFrame.startTime()), 500ms);
QCOMPARE_EQ(microseconds(lastFrame.endTime()), 540ms);
} else {
QCOMPARE_EQ(microseconds(lastFrame.startTime()), 480ms);
QCOMPARE_EQ(microseconds(lastFrame.endTime()), 520ms);
}
}
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
if (!isGStreamerPlatform()) {
QSKIP_GSTREAMER("QTBUG-124005: spurious failures with gstreamer");
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");
QSKIP_OHOS("OH_AVPlayer does not consult container-level metadata for stream duration");
QSKIP_DARWIN("AVFoundation does not support Matroska/WebM containers");
QFETCH(QString, mediaFile);
if (isWMFPlatform()) {
// Matroska/WebM support depends on optional Windows media extensions.
const MaybeUrl supported = m_mediaSelector.select(mediaFile);
CHECK_SELECTED_URL(supported);
}
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(QUrl{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
// IMFMediaEngine does not expose per-stream duration via GetStreamAttribute.
const auto videoTracks = player.videoTracks();
QCOMPARE(videoTracks.size(), expectedVideoTrackCount);
if (expectedVideoTrackCount != 0 && !isWMFPlatform())
QCOMPARE(videoTracks.front().value(QMediaMetaData::Duration), expectedVideoTrackDuration);
const auto audioTracks = player.audioTracks();
QCOMPARE(audioTracks.size(), expectedAudioTrackCount);
if (expectedAudioTrackCount != 0 && !isWMFPlatform())
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;
static constexpr bool dumpPositions = false;
static constexpr bool dumpLoops = false;
// Loops through all positions emitted by QMediaPlayer::positionChanged
for (auto &params : positionSpy) {
const auto pos = params.front().value<qint64>();
if constexpr (dumpPositions)
qDebug() << pos;
// 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;
}
}
if constexpr (dumpLoops)
for (auto &element : result)
qDebug() << element.startPos << element.endPos << element.posCount;
return result;
}
void tst_QMediaPlayerBackend::finiteLoops()
{
QSKIP_OHOS("OH_AVPlayer setLoops timing and frame production differ from baseline");
CHECK_SELECTED_URL(m_localVideoFile3ColorsWithSound);
QFETCH(bool, pauseDuringPlayback);
QFETCH(bool, rateChange);
#ifdef Q_OS_MACOS
if (isCI())
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);
if (rateChange)
m_fixture->player.setPlaybackRate(5);
QCOMPARE(m_fixture->player.loops(), 3);
m_fixture->player.play();
m_fixture->surface.waitForFrame();
if (pauseDuringPlayback) {
// 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_WITH_TIMEOUT(m_fixture->player.playbackState(), QMediaPlayer::StoppedState, 15s);
// Check for expected number of loop iterations and startPos, endPos and posCount per iteration
const std::vector<LoopIteration> iterations = loopIterations(m_fixture->positionChanged);
// expected loops boundaries:
// 0: (0 ... duration]. 0 is not emitted as it's not a change from the start state
// 1: [0 ... duration]
// 2: [0 ... duration]
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());
// In the FFmpeg backend an approximate interval of position change update is 50ms,
// so we estimate to get "duration / 50 / playbackState" intermediate updates and
// 2 boundary updates.
// Assuming timing inaccuaracies, let's consider the interval = 50 * 2
// to address the test flakeness.
// For other backends, let's check that we have at lest one intermediate position update.
const int expectedIntermediatePositionsCount = isFFMPEGPlatform()
? m_fixture->player.duration() / 100 / m_fixture->player.playbackRate()
: 1;
QCOMPARE_GE(iterations[0].posCount, expectedIntermediatePositionsCount + 1);
QCOMPARE_GE(iterations[1].posCount, expectedIntermediatePositionsCount + 2);
QCOMPARE_GE(iterations[2].posCount, expectedIntermediatePositionsCount + 2);
QCOMPARE(m_fixture->player.mediaStatus(), QMediaPlayer::EndOfMedia);
// Check that loop counter is reset when playback is restarted.
QSKIP_WMF("IMFMediaEngine: SetPlaybackRate(10) is not applied instantaneously "
"after play(), so 3 loops at rate=1 (~9.7s) may exceed the 8s timeout");
m_fixture->positionChanged.clear();
m_fixture->player.play();
m_fixture->player.setPlaybackRate(10);
m_fixture->surface.waitForFrame();
QTRY_COMPARE_WITH_TIMEOUT(m_fixture->player.playbackState(), QMediaPlayer::StoppedState, 8s);
QCOMPARE(loopIterations(m_fixture->positionChanged).size(), 3u);
QCOMPARE(m_fixture->player.mediaStatus(), QMediaPlayer::EndOfMedia);
}
void tst_QMediaPlayerBackend::finiteLoops_data()
{
QTest::addColumn<bool>("pauseDuringPlayback");
QTest::addColumn<bool>("rateChange");
QTest::newRow("No pause, default rate") << false << false;
QTest::newRow("No pause, fast rate") << false << true;
QTest::newRow("Pause, default rate") << true << false;
QTest::newRow("Pause, fast rate") << true << true;
}
void tst_QMediaPlayerBackend::infiniteLoops()
{
QSKIP_OHOS("OH_AVPlayer setLoops(Infinite) signals are not aligned with the cross-platform contract");
CHECK_SELECTED_URL(m_localVideoFile2);
#ifdef Q_OS_MACOS
if (isCI())
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);
if (!isGStreamerPlatform())
QCOMPARE(m_fixture->mediaStatusChanged,
SignalList({ { QMediaPlayer::LoadingMedia },
{ QMediaPlayer::LoadedMedia },
{ QMediaPlayer::BufferingMedia },
{ QMediaPlayer::BufferedMedia },
{ QMediaPlayer::LoadedMedia } }));
}
void tst_QMediaPlayerBackend::seekOnLoops()
{
CHECK_SELECTED_URL(m_localVideoFile3ColorsWithSound);
QSKIP_OHOS("OH_AVPlayer_Seek interaction with setLoops is unreliable");
#ifdef Q_OS_MACOS
if (isCI())
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()
{
CHECK_SELECTED_URL(m_localVideoFile3ColorsWithSound);
QSKIP_OHOS("OH_AVPlayer setLoops behavior mid-playback is not aligned with the cross-platform contract");
#ifdef Q_OS_MACOS
if (isCI())
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 (isCI())
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::setVideoOutput_whilePlaying_doesNotDropFrames()
{
QSKIP_GSTREAMER("QTBUG-124005: gstreamer will lose frames, possibly due to buffering");
QSKIP_OHOS("Reattaching the video surface mid-playback drops the first frame in the new sink");
QSKIP_DARWIN("DisplayLink-based rendering does not match video frame rate");
QSKIP_WMF("IMFMediaEngine: frame loss during video output switch with short files");
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_COMPARE(player.mediaStatus(), QMediaPlayer::EndOfMedia);
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_COMPARE(player.mediaStatus(), QMediaPlayer::EndOfMedia);
// check if no frames sent to old sinks
QCOMPARE(framesCount[1], savedFrameNumber1);
QCOMPARE(framesCount[2], savedFrameNumber2);
constexpr int videoOutputChanges = 2; // video frame goes from the previous sink.
// no frames lost
QCOMPARE(framesCount[1] + framesCount[2] + framesCount[3], framesCount[0] + videoOutputChanges);
}
void tst_QMediaPlayerBackend::cleanSinkAndNoMoreFramesAfterStop()
{
QSKIP_GSTREAMER(
"QTBUG-124005: spurious failures on gstreamer, probably due to asynchronous play()");
QSKIP_OHOS("OH_AVPlayer may deliver extra frames after Stop() before the sink is cleared");
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()
{
QSKIP_OHOS("OH_AVPlayer lazy-load through QML does not deliver the expected frame sequence");
QQmlEngine engine;
QQmlComponent component(&engine);
component.loadUrl(QUrl("qrc:/LazyLoad.qml"));
std::unique_ptr<QObject> root(component.create());
QQuickItem *rootItem = qobject_cast<QQuickItem *>(root.get());
QVERIFY(rootItem);
QQuickView view;
rootItem->setParentItem(view.contentItem());
view.resize(600, 800);
view.show();
QQuickLoader *loader = qobject_cast<QQuickLoader *>(rootItem->findChild<QQuickItem *>("loader"));
QVERIFY(loader);
QCOMPARE(QQmlProperty::read(loader, "active").toBool(), false);
loader->setProperty("active", true);
QCOMPARE(QQmlProperty::read(loader, "active").toBool(), true);
QQuickItem *videoPlayer = qobject_cast<QQuickItem *>(loader->findChild<QQuickItem *>("videoPlayer"));
QVERIFY(videoPlayer);
QTRY_COMPARE_EQ(QQmlProperty::read(videoPlayer, "playbackState").value<QMediaPlayer::PlaybackState>(), QMediaPlayer::PlayingState);
QCOMPARE(QQmlProperty::read(videoPlayer, "error").value<QMediaPlayer::Error>(), QMediaPlayer::NoError);
QVideoSink *videoSink = QQmlProperty::read(videoPlayer, "videoSink").value<QVideoSink *>();
QVERIFY(videoSink);
QSignalSpy spy(videoSink, &QVideoSink::videoFrameChanged);
QVERIFY(spy.wait());
QVideoFrame frame = spy.at(0).at(0).value<QVideoFrame>();
QVERIFY(frame.isValid());
}
void tst_QMediaPlayerBackend::videoSinkSignals()
{
std::atomic<int> videoFrameCounter = 0;
std::atomic<int> videoSizeCounter = 0;
// TODO: come up with custom frames source,
// create the test target tst_QVideoSinkBackend,
// and move the test there
CHECK_SELECTED_URL(m_localVideoFile2);
QVideoSink sink;
QMediaPlayer player;
player.setVideoSink(&sink);
player.setSource(*m_localVideoFile2);
QTRY_COMPARE(player.mediaStatus(), QMediaPlayer::MediaStatus::LoadedMedia);
sink.platformVideoSink()->setNativeSize({}); // reset size to be able to check the size update
connect(&sink, &QVideoSink::videoFrameChanged, this, [&](const QVideoFrame &frame) {
QCOMPARE(sink.videoFrame(), frame);
QCOMPARE(sink.videoSize(), frame.size());
++videoFrameCounter;
}, Qt::DirectConnection);
connect(&sink, &QVideoSink::videoSizeChanged, this, [&]() {
QCOMPARE(sink.videoSize(), sink.videoFrame().size());
if (sink.videoSize().isValid()) // filter end frame
++videoSizeCounter;
}, Qt::DirectConnection);
player.play();
QTRY_COMPARE_GE(videoFrameCounter, 2);
QCOMPARE(videoSizeCounter, 1);
}
void tst_QMediaPlayerBackend::nonAsciiFileName()
{
CHECK_SELECTED_URL(m_localWavFile);
auto temporaryFile = copyResourceToTemporaryFile(QUrl(u"qrc:/testdata/test.wav"_s),
u"äöüØøÆ中文.XXXXXX.wav"_s);
QVERIFY(temporaryFile);
QString path = temporaryFile->fileName();
if (isWMFPlatform()) {
// QTemporaryFile on Windows uses FILE_FLAG_DELETE_ON_CLOSE, which
// marks the file as "pending delete". While Qt can still open such
// files (it uses FILE_SHARE_DELETE), IMFMediaEngine cannot because
// it does not use FILE_SHARE_DELETE when opening the file.
temporaryFile->setAutoRemove(false);
temporaryFile.reset();
}
auto cleanup = qScopeGuard([&] {
if (!temporaryFile)
QFile::remove(path);
});
m_fixture->player.setSource(QUrl::fromLocalFile(path));
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);
QSKIP_OHOS("Video sink size is published only after the surface attaches, not on setSource");
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)
{
QTEST_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 started with some delay after the vlc process starts.
// Ideally, we should wait for open connections, it requires some extra work + QNetwork
// dependency.
QTest::qWait(2000);
return process;
}
#endif //QT_CONFIG(process)
void tst_QMediaPlayerBackend::
play_playsTransformedVideoOutput_whenVideoFileHasTransformationMetadata_data()
{
QTest::addColumn<MaybeUrl>("fileURL");
QTest::addColumn<QRgb>("expectedColor");
QTest::addColumn<QtVideo::Rotation>("expectedRotation");
QTest::addColumn<bool>("expectedMirrored");
QTest::addColumn<QSize>("videoSize");
// clang-format off
// useful command for creating testdata:
// ffmpeg -display_hflip -display_rotation 90 -i color_matrix.mp4 -codec copy color_matrix_90_deg_clockwise_mirrored.mp4
QTest::addRow("without transformation")
<< m_colorMatrixVideo
<< QRgb(0xFF0000)
<< QtVideo::Rotation::None
<< false
<< QSize(960, 540);
QTest::addRow("mirrored")
<< m_colorMatrixMirroredVideo
<< QRgb(0x00FF00)
<< QtVideo::Rotation::None
<< true
<< QSize(960, 540);
QTest::addRow("90 deg clockwise")
<< m_colorMatrix90degClockwiseVideo
<< QRgb(0x0000FF)
<< QtVideo::Rotation::Clockwise90
<< false
<< QSize(540, 960);
QTest::addRow("90 deg clockwise mirrored")
<< m_colorMatrix90degClockwiseMirroredVideo
<< QRgb(0xFF0000)
<< QtVideo::Rotation::Clockwise90
<< true
<< QSize(540, 960);
QTest::addRow("180 deg clockwise")
<< m_colorMatrix180degClockwiseVideo
<< QRgb(0xFFFF00)
<< QtVideo::Rotation::Clockwise180
<< false
<< QSize(960, 540);
QTest::addRow("180 deg clockwise mirrored")
<< m_colorMatrix180degClockwiseMirroredVideo
<< QRgb(0x0000FF)
<< QtVideo::Rotation::Clockwise180
<< true
<< QSize(960, 540);
QTest::addRow("270 deg clockwise")
<< m_colorMatrix270degClockwiseVideo
<< QRgb(0x00FF00)
<< QtVideo::Rotation::Clockwise270
<< false
<< QSize(540, 960);
QTest::addRow("270 deg clockwise mirrored")
<< m_colorMatrix270degClockwiseMirroredVideo
<< QRgb(0xFFFF00)
<< QtVideo::Rotation::Clockwise270
<< true
<< QSize(540, 960);
// clang-format on
}
void tst_QMediaPlayerBackend::
play_playsTransformedVideoOutput_whenVideoFileHasTransformationMetadata()
{
QSKIP_OHOS("OH_AVPlayer does not surface MP4 rotation/mirror metadata to QVideoFrame transformations");
// 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(QtVideo::Rotation, expectedRotation);
QFETCH(const bool, expectedMirrored);
QFETCH(const QSize, videoSize);
if (expectedMirrored) {
QSKIP_GSTREAMER("gst_play - 'mirrored' property not taken into account");
QSKIP_WMF("MF_MT_VIDEO_ROTATION does not support mirroring");
}
CHECK_SELECTED_URL(fileURL);
// IMFMediaEngine applies rotation to decoded pixels directly and does not
// expose rotation through metadata or surfaceFormat.
if (isWMFPlatform())
expectedRotation = QtVideo::Rotation::None;
// 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
if (!isWMFPlatform()) // IMFMediaEngine: nativeSize available after FORMATCHANGE
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, expectedRotation);
// 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, expectedRotation);
// 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.surfaceFormat().rotation(), expectedRotation);
QCOMPARE(videoFrame.surfaceFormat().isMirrored(), expectedMirrored);
QCOMPARE(videoFrame.rotation(), QtVideo::Rotation::None);
QVERIFY(!videoFrame.mirrored());
#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);
const qreal maxColorDifference = isRhiRenderingSupported()
? 0.03 // tolerance for 1 luma / 3 chroma LSB, required for e.g. paravirtualized VideoToolbox
: 0.06 // QTBUG-127784: cpu conversion has quite some rounding errors
;
QCOMPARE_LT(colorDifference(upperLeftColor, expectedColor), maxColorDifference);
// 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:
#ifdef Q_OS_ANDROID
QSKIP("Goldfish MediaCodec decoder on the Android emulator hangs when resuming playback after pause");
#endif
player.pause();
break;
case QMediaPlayer::PlayingState:
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(64, 64));
}
// 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()
{
QTest::addColumn<QMediaPlayer::PlaybackState>("playbackState");
QTest::newRow("StoppedState") << QMediaPlayer::StoppedState;
QTest::newRow("PausedState") << QMediaPlayer::PausedState;
QTest::newRow("PlayingState") << QMediaPlayer::PlayingState;
}
void tst_QMediaPlayerBackend::setVideoOutput_whilePaused_updatesNewSink()
{
using namespace std::chrono_literals;
CHECK_SELECTED_URL(m_15sVideo);
TestVideoSink surface1(/*storeFrames=*/false);
TestVideoSink surface2(/*storeFrames=*/false);
QAudioOutput audioOut;
QMediaPlayer player;
player.setAudioOutput(&audioOut);
player.setVideoOutput(&surface1);
player.setSource(*m_15sVideo);
QTRY_COMPARE(player.mediaStatus(), QMediaPlayer::LoadedMedia);
player.play();
QTRY_COMPARE_GT(player.position(), 100);
#ifndef Q_OS_ANDROID // fails with android/ffmpeg
QCOMPARE(surface1.videoFrame().toImage().pixelColor(10, 10), QColorConstants::White);
#endif
player.pause();
QTest::qWait(100ms); // pause() may be asynchronous
player.setVideoOutput(&surface2);
// new frame is delivered asynchronously
QTRY_COMPARE_EQ(surface2.m_totalFrames, 1);
#ifndef Q_OS_ANDROID // fails with android/ffmpeg
QCOMPARE(surface2.videoFrame().toImage().pixelColor(10, 10), QColorConstants::White);
#endif
if (isFFMPEGPlatform())
QCOMPARE(surface1.videoFrame(), surface2.videoFrame());
else if (isGStreamerPlatform())
// timestamps differ, so we only compare pixels
QCOMPARE(surface1.videoFrame().toImage(), surface2.videoFrame().toImage());
}
void tst_QMediaPlayerBackend::setAudioOutput_doesNotStopPlayback()
{
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 tst_QMediaPlayerBackend::setAudioOutput_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::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");
QSKIP_GSTREAMER("QTBUG-124005: spurious failures with gstreamer");
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()
{
QSKIP_OHOS("OH_AVPlayer does not expose subtitle tracks");
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);
if (!isGStreamerPlatform() && !isWMFPlatform()) {
// QTBUG-124005: spurious failures with gstreamer
// IMFTimedText does not expose per-track duration
QCOMPARE_EQ(player.subtitleTracks()[0].value(QMediaMetaData::Duration), 3000);
}
player.setActiveSubtitleTrack(0);
if (!isGStreamerPlatform()) // FIXME: spurious deadlocks
player.setPlaybackRate(5.f);
player.play();
QSKIP_GSTREAMER("QTBUG-124005: gstreamer sometimes reports more than 4 subtitle events");
QSKIP_WMF("IMFMediaEngine: calling SetPlaybackRate before play() causes the first timed-text "
"cue to be delivered twice");
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.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);
QSKIP_WMF("IMFMediaEngine: MKV track titles are not exposed through stream attributes or "
"IPropertyStore");
QCOMPARE(player.videoTracks()[0][QMediaMetaData::Title], u"One"_s);
QCOMPARE(player.videoTracks()[1][QMediaMetaData::Title], u"Two"_s);
}
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");
CHECK_SELECTED_URL(m_multitrackVideo);
CHECK_SELECTED_URL(m_multitrackSubtitleStartsAtZeroVideo);
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");
QSKIP_WMF("IMFMediaEngine does not deliver video frames for multitrack MKV via byte stream");
CHECK_SELECTED_URL(m_multitrackVideo);
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();
if (!isRhiRenderingSupported())
QEXPECT_FAIL("", "QTBUG-127784: Inaccurate color handling when no RHI backend is available", Abort);
QCOMPARE(QColor{ sink.m_frameList.back().toImage().pixel(10, 10) }, QColor(0xff, 0x80, 0x7f));
player.setActiveVideoTrack(track1);
QTest::qWait(500ms);
sink.waitForFrame();
if (!isRhiRenderingSupported())
QEXPECT_FAIL("", "QTBUG-127784: Inaccurate color handling when no RHI backend is available", Abort);
QCOMPARE(QColor{ sink.m_frameList.back().toImage().pixel(10, 10) }, QColor(0x80, 0x80, 0xff));
}
void tst_QMediaPlayerBackend::disablingAllTracks_doesNotStopPlayback()
{
QSKIP_GSTREAMER("position does not advance in GStreamer");
CHECK_SELECTED_URL(m_multitrackVideo);
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_COMPARE_GT(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");
CHECK_SELECTED_URL(m_multitrackVideo);
QMediaPlayer &player = m_fixture->player;
player.setSource(*m_multitrackVideo);
player.setActiveVideoTrack(-1);
player.setActiveAudioTrack(-1);
player.play();
QTRY_COMPARE_GT(player.position(), 1000);
QCOMPARE(m_fixture->surface.m_totalFrames, 0);
}
void tst_QMediaPlayerBackend::play_finishes_whenPlayingFileWithPacketsAfterStreamEnd_data()
{
QTest::addColumn<int>("loops");
QTest::addColumn<qreal>("rate");
QTest::newRow("Played once") << 1 << 1.0;
QTest::newRow("Played thrice") << 3 << 3.0;
}
void tst_QMediaPlayerBackend::play_finishes_whenPlayingFileWithPacketsAfterStreamEnd()
{
QSKIP_OHOS("OH_AVPlayer rejects OGG with invalid trailing packets and does not finalize playback");
CHECK_SELECTED_URL(m_oggEndingWithInvalidTiming);
QFETCH(int, loops);
QFETCH(qreal, rate);
// Arrange
m_fixture->player.setSource(*m_oggEndingWithInvalidTiming);
m_fixture->player.setLoops(loops);
m_fixture->player.setPlaybackRate(rate);
// Act
m_fixture->player.play();
QTRY_VERIFY(m_fixture->player.isPlaying());
// Assert
QTRY_COMPARE_WITH_TIMEOUT(m_fixture->player.playbackState(), QMediaPlayer::StoppedState, 15s);
QSKIP_DARWIN("AVFoundation has flaky test failures on CI");
QCOMPARE(loopIterations(m_fixture->positionChanged).size(), unsigned(loops));
}
void tst_QMediaPlayerBackend::play_finishes_whenPlayingFileIncludingPacketsWithUndefinedTimestamp()
{
// When FFmpeg demuxes MP3 files with embedded thumbnails, the PTS/DTS of the first packet
// is AV_NOPTS_VALUE, aka "Undefined timestamp value". This test makes sure it is played to the
// end by checking for position changes before reaching StoppedState.
if (isDarwinPlatform() && isCI())
QSKIP("Flaky on CI: AVFoundation may not emit enough positionChanged signals for short media at high playback rate");
CHECK_SELECTED_URL(m_localMp3FileWithMetadataAndEmbeddedThumbnail);
// Arrange
m_fixture->player.setSource(*m_localMp3FileWithMetadataAndEmbeddedThumbnail);
m_fixture->player.setPlaybackRate(10);
// Act
m_fixture->player.play();
QTRY_VERIFY(m_fixture->player.isPlaying());
// Assert
QTRY_COMPARE_WITH_TIMEOUT(m_fixture->player.playbackState(), QMediaPlayer::StoppedState, 10s);
QCOMPARE_GE(m_fixture->positionChanged.size(), 5);
}
void tst_QMediaPlayerBackend::makeStressTestCases()
{
QTest::addColumn<MaybeUrl>("media");
QTest::addColumn<bool>("play");
QTest::newRow("no media") << MaybeUrl{ q23::unexpect } << false;
QTest::newRow("audio, not playing") << m_localWavFile << false;
QTest::newRow("audio, playing") << m_localWavFile << true;
QTest::newRow("video, not playing") << m_localVideoFile << false;
QTest::newRow("video, playing") << m_localVideoFile << true;
}
void tst_QMediaPlayerBackend::stressTest_setupAndTeardown()
{
QSKIP_GSTREAMER("race condition in gst_play");
QFETCH(MaybeUrl, media);
QFETCH(bool, play);
QRandomGenerator rng;
for (int i = 0; i < 50; i++) {
QMediaPlayer player;
QAudioOutput output;
TestVideoSink videoSink;
player.setAudioOutput(&output);
player.setVideoOutput(&videoSink);
if (media) {
player.setSource(*media);
if (play) {
player.play();
QTRY_COMPARE_GT(player.position(), 10);
}
}
QTest::qWait(rng.bounded(200));
}
}
void tst_QMediaPlayerBackend::stressTest_setupAndTeardown_data()
{
makeStressTestCases();
}
void tst_QMediaPlayerBackend::stressTest_setupAndTeardown_keepAudioOutput()
{
QSKIP_GSTREAMER("race condition in gst_play");
QFETCH(MaybeUrl, media);
QFETCH(bool, play);
QRandomGenerator rng;
QAudioOutput output;
for (int i = 0; i < 50; i++) {
QMediaPlayer player;
TestVideoSink videoSink;
player.setAudioOutput(&output);
player.setVideoOutput(&videoSink);
if (media) {
player.setSource(*media);
if (play) {
player.play();
QTRY_COMPARE_GT(player.position(), 10);
}
}
QTest::qWait(rng.bounded(200));
}
}
void tst_QMediaPlayerBackend::stressTest_setupAndTeardown_keepAudioOutput_data()
{
makeStressTestCases();
}
void tst_QMediaPlayerBackend::stressTest_setupAndTeardown_keepVideoOutput()
{
QSKIP_GSTREAMER("race condition in gst_play");
QFETCH(MaybeUrl, media);
QFETCH(bool, play);
QRandomGenerator rng;
TestVideoSink videoSink;
for (int i = 0; i < 50; i++) {
QMediaPlayer player;
QAudioOutput output;
player.setAudioOutput(&output);
player.setVideoOutput(&videoSink);
if (media) {
player.setSource(*media);
if (play) {
player.play();
QTRY_COMPARE_GT(player.position(), 10);
}
}
QTest::qWait(rng.bounded(200));
}
}
void tst_QMediaPlayerBackend::stressTest_setupAndTeardown_keepVideoOutput_data()
{
makeStressTestCases();
}
enum DestructionOrder { AVM, AMV, MAV, MVA, VMA, VAM };
enum MediaPlayerCall { Pause, Stop, SetSource, SetSourceNull, SetVideoSink, SetAudioOutput
, SetPlaybackRate, SetPosition, SetPosition_SetPosition, SetAudioOutput_Stop
, SetVideoSink_Stop, Stop_Play, None };
void tst_QMediaPlayerBackend::destruction_doesNotDeadlock_afterMediaPlayerCall()
{
QSKIP_GSTREAMER("Triggers various deadlocks between gstPlay threads and Qt main thread");
// Example test executable command line argument to specify calls and destruction order, including handy flags:
// destruction_doesNotDeadlock_afterMediaPlayerCall:MVA_stop_play -maxwarnings 100000 -repeat 100
QFETCH(DestructionOrder, destructionOrder);
QFETCH(MediaPlayerCall, mediaPlayerCall);
CHECK_SELECTED_URL(m_localVideoFile3ColorsWithSound);
CHECK_SELECTED_URL(m_localVideoFile);
// setup
auto mediaPlayer = std::make_unique<QMediaPlayer>();
auto audioOutput = std::make_unique<QAudioOutput>();
auto videoSink = std::make_unique<QVideoSink>();
mediaPlayer->setVideoSink(videoSink.get());
mediaPlayer->setAudioOutput(audioOutput.get());
mediaPlayer->setSource(*m_localVideoFile3ColorsWithSound);
mediaPlayer->play();
QTRY_COMPARE_GT(mediaPlayer->position(), 0);
// act
switch (mediaPlayerCall) {
case MediaPlayerCall::Pause: qDebug() << "...pause..."; mediaPlayer->pause(); break;
case MediaPlayerCall::Stop: qDebug() << "...stop..."; mediaPlayer->stop(); break;
case MediaPlayerCall::SetSource: qDebug() << "...setSource(*m_localVideoFile)..."; mediaPlayer->setSource(*m_localVideoFile); break;
case MediaPlayerCall::SetSourceNull: qDebug() << "...setSource(QUrl())..."; mediaPlayer->setSource(QUrl()); break;
case MediaPlayerCall::SetVideoSink: qDebug() << "...setVideoSink(nullptr)..."; mediaPlayer->setVideoSink(nullptr); break;
case MediaPlayerCall::SetAudioOutput: qDebug() << "...setAudioOutput(nullptr)..."; mediaPlayer->setAudioOutput(nullptr); break;
case MediaPlayerCall::SetPlaybackRate: qDebug() << "...setPlaybackRate(2)..."; mediaPlayer->setPlaybackRate(2); break;
case MediaPlayerCall::SetPosition: qDebug() << "...setPosition(1000)..."; mediaPlayer->setPosition(1000); break;
case MediaPlayerCall::SetPosition_SetPosition: qDebug() << "...setPosition(1000)..."; mediaPlayer->setPosition(1000); qDebug() << "...setPosition(500)..."; mediaPlayer->setPosition(500); break;
case MediaPlayerCall::SetAudioOutput_Stop: qDebug() << "...setAudioOutput(nullptr)..."; mediaPlayer->setAudioOutput(nullptr); qDebug() << "...stop..."; mediaPlayer->stop(); break;
case MediaPlayerCall::SetVideoSink_Stop: qDebug() << "...setVideoSink(nullptr)..."; mediaPlayer->setVideoSink(nullptr); qDebug() << "...stop..."; mediaPlayer->stop(); break;
case MediaPlayerCall::Stop_Play: qDebug() << "...stop..."; mediaPlayer->stop(); qDebug() << "...play..."; mediaPlayer->play();; break;
case MediaPlayerCall::None: break;
}
qDebug() << "...destruction...";
switch (destructionOrder) {
case DestructionOrder::AVM: audioOutput.reset(); videoSink.reset(); mediaPlayer.reset(); break;
case DestructionOrder::AMV: audioOutput.reset(); mediaPlayer.reset(); videoSink.reset(); break;
case DestructionOrder::MAV: mediaPlayer.reset(); audioOutput.reset(); videoSink.reset(); break;
case DestructionOrder::MVA: mediaPlayer.reset(); videoSink.reset(); audioOutput.reset(); break;
case DestructionOrder::VMA: videoSink.reset(); mediaPlayer.reset(); audioOutput.reset(); break;
case DestructionOrder::VAM: videoSink.reset(); audioOutput.reset(); mediaPlayer.reset(); break;
}
}
void tst_QMediaPlayerBackend::destruction_doesNotDeadlock_afterMediaPlayerCall_data()
{
QTest::addColumn<DestructionOrder>("destructionOrder");
QTest::addColumn<MediaPlayerCall>("mediaPlayerCall");
static const std::pair<const char *, DestructionOrder> destructionOrders[] = {
{"AVM", DestructionOrder::AVM},
{"AMV", DestructionOrder::AMV},
{"MAV", DestructionOrder::MAV},
{"MVA", DestructionOrder::MVA},
{"VMA", DestructionOrder::VMA},
{"VAM", DestructionOrder::VAM},
};
static const std::pair<const char *, MediaPlayerCall> call[] = {
{"none", MediaPlayerCall::None},
{"pause", MediaPlayerCall::Pause},
{"stop", MediaPlayerCall::Stop},
{"setSource", MediaPlayerCall::SetSource},
{"setSourceNull", MediaPlayerCall::SetSourceNull},
{"setVideoSink", MediaPlayerCall::SetVideoSink},
{"setAudioOutput", MediaPlayerCall::SetAudioOutput},
{"setPlaybackRate", MediaPlayerCall::SetPlaybackRate},
{"setPosition", MediaPlayerCall::SetPosition},
{"setPosition_setPosition", MediaPlayerCall::SetPosition_SetPosition},
{"setAudioOutput_stop", MediaPlayerCall::SetAudioOutput_Stop},
{"setVideoSink_stop", MediaPlayerCall::SetVideoSink_Stop},
{"stop_play", MediaPlayerCall::Stop_Play},
};
for (auto &[orderName, order] : destructionOrders) {
for (auto &[callName, call] : call) {
QTest::addRow("%s_%s", orderName, callName) << order << call;
}
}
}
QTEST_MAIN(tst_QMediaPlayerBackend)
#include "tst_qmediaplayerbackend.moc"