diff options
author | Artem Dyomin <artem.dyomin@qt.io> | 2023-03-02 14:09:15 +0100 |
---|---|---|
committer | Artem Dyomin <artem.dyomin@qt.io> | 2023-03-09 05:18:40 +0000 |
commit | 7306154c02f32c169b73ff69e6e61d31999f127a (patch) | |
tree | d1bc46b014f1c08722c998f586577721721c7fe5 | |
parent | ad21ea6e6e94a2ad9926408ca60c714ccca37184 (diff) |
Implement looping for ffmpeg mediaplayer backend
- Looping of ffmpeg mediaplayer has been added with auto tests
- Minor code cleanup (that left from previous implmntations)
Task-number: QTBUG-111209
Change-Id: I3839ee866bc2dc571919e41a18b093fb15165293
Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org>
Reviewed-by: Lars Knoll <lars@knoll.priv.no>
(cherry picked from commit 46394944487b189962c1ceef8a236b328e8e7736)
8 files changed, 224 insertions, 74 deletions
diff --git a/src/plugins/multimedia/ffmpeg/playbackengine/qffmpegmediadataholder.cpp b/src/plugins/multimedia/ffmpeg/playbackengine/qffmpegmediadataholder.cpp index 1a016acc8..eb4f35b7a 100644 --- a/src/plugins/multimedia/ffmpeg/playbackengine/qffmpegmediadataholder.cpp +++ b/src/plugins/multimedia/ffmpeg/playbackengine/qffmpegmediadataholder.cpp @@ -267,6 +267,14 @@ int MediaDataHolder::activeTrack(QPlatformMediaPlayer::TrackType type) const return type < QPlatformMediaPlayer::NTrackTypes ? m_requestedStreams[type] : -1; } +const QList<MediaDataHolder::StreamInfo> &MediaDataHolder::streamInfo( + QPlatformMediaPlayer::TrackType trackType) const +{ + Q_ASSERT(trackType < QPlatformMediaPlayer::NTrackTypes); + + return m_streamMap[trackType]; +} + } // namespace QFFmpeg QT_END_NAMESPACE diff --git a/src/plugins/multimedia/ffmpeg/playbackengine/qffmpegmediadataholder_p.h b/src/plugins/multimedia/ffmpeg/playbackengine/qffmpegmediadataholder_p.h index dc9f4c907..e1b6873a8 100644 --- a/src/plugins/multimedia/ffmpeg/playbackengine/qffmpegmediadataholder_p.h +++ b/src/plugins/multimedia/ffmpeg/playbackengine/qffmpegmediadataholder_p.h @@ -52,10 +52,16 @@ public: static QPlatformMediaPlayer::TrackType trackTypeFromMediaType(int mediaType); - bool setActiveTrack(QPlatformMediaPlayer::TrackType type, int streamNumber); - int activeTrack(QPlatformMediaPlayer::TrackType type) const; + const QList<StreamInfo> &streamInfo(QPlatformMediaPlayer::TrackType trackType) const; + + qint64 duration() const { return m_duration; } + + const QMediaMetaData &metaData() const { return m_metaData; } + + bool isSeekable() const { return m_isSeekable; } + protected: std::optional<ContextError> recreateAVFormatContext(const QUrl &media, QIODevice *stream); @@ -63,6 +69,8 @@ protected: void updateMetaData(); + bool setActiveTrack(QPlatformMediaPlayer::TrackType type, int streamNumber); + protected: std::unique_ptr<AVFormatContext, AVFormatContextDeleter> m_context; bool m_isSeekable = false; diff --git a/src/plugins/multimedia/ffmpeg/qffmpegaudiodecoder.cpp b/src/plugins/multimedia/ffmpeg/qffmpegaudiodecoder.cpp index 65e8d4148..c40826383 100644 --- a/src/plugins/multimedia/ffmpeg/qffmpegaudiodecoder.cpp +++ b/src/plugins/multimedia/ffmpeg/qffmpegaudiodecoder.cpp @@ -149,7 +149,7 @@ void QFFmpegAudioDecoder::start() if (!checkNoError()) return; - durationChanged(m_decoder->m_duration / 1000); + durationChanged(m_decoder->duration() / 1000); setIsDecoding(true); } diff --git a/src/plugins/multimedia/ffmpeg/qffmpegmediaplayer.cpp b/src/plugins/multimedia/ffmpeg/qffmpegmediaplayer.cpp index 0041c9a43..507550ab5 100644 --- a/src/plugins/multimedia/ffmpeg/qffmpegmediaplayer.cpp +++ b/src/plugins/multimedia/ffmpeg/qffmpegmediaplayer.cpp @@ -7,7 +7,8 @@ #include "qaudiooutput.h" #include "qffmpegplaybackengine_p.h" - +#include <qiodevice.h> +#include <qvideosink.h> #include <qtimer.h> #include <qloggingcategory.h> @@ -19,22 +20,22 @@ using namespace QFFmpeg; QFFmpegMediaPlayer::QFFmpegMediaPlayer(QMediaPlayer *player) : QPlatformMediaPlayer(player) { - positionUpdateTimer.setInterval(50); - positionUpdateTimer.setTimerType(Qt::PreciseTimer); - connect(&positionUpdateTimer, &QTimer::timeout, this, &QFFmpegMediaPlayer::updatePosition); + m_positionUpdateTimer.setInterval(50); + m_positionUpdateTimer.setTimerType(Qt::PreciseTimer); + connect(&m_positionUpdateTimer, &QTimer::timeout, this, &QFFmpegMediaPlayer::updatePosition); } QFFmpegMediaPlayer::~QFFmpegMediaPlayer() = default; qint64 QFFmpegMediaPlayer::duration() const { - return decoder ? decoder->m_duration / 1000 : 0; + return m_playbackEngine ? m_playbackEngine->duration() / 1000 : 0; } void QFFmpegMediaPlayer::setPosition(qint64 position) { - if (decoder) { - decoder->seek(position * 1000); + if (m_playbackEngine) { + m_playbackEngine->seek(position * 1000); updatePosition(); } if (state() == QMediaPlayer::StoppedState) @@ -43,15 +44,24 @@ void QFFmpegMediaPlayer::setPosition(qint64 position) void QFFmpegMediaPlayer::updatePosition() { - positionChanged(decoder ? decoder->currentPosition() / 1000 : 0); + positionChanged(m_playbackEngine ? m_playbackEngine->currentPosition() / 1000 : 0); } void QFFmpegMediaPlayer::endOfStream() { - positionUpdateTimer.stop(); + // start update timer and report end position anyway + m_positionUpdateTimer.stop(); positionChanged(duration()); - stateChanged(QMediaPlayer::StoppedState); - mediaStatusChanged(QMediaPlayer::EndOfMedia); + + if (doLoop()) { + m_playbackEngine->seek(0); + positionChanged(0); + + runPlayback(); + } else { + stateChanged(QMediaPlayer::StoppedState); + mediaStatusChanged(QMediaPlayer::EndOfMedia); + } } float QFFmpegMediaPlayer::bufferProgress() const @@ -74,8 +84,8 @@ void QFFmpegMediaPlayer::setPlaybackRate(qreal rate) if (m_playbackRate == rate) return; m_playbackRate = rate; - if (decoder) - decoder->setPlaybackRate(rate); + if (m_playbackEngine) + m_playbackEngine->setPlaybackRate(rate); } QUrl QFFmpegMediaPlayer::media() const @@ -92,7 +102,7 @@ void QFFmpegMediaPlayer::setMedia(const QUrl &media, QIODevice *stream) { m_url = media; m_device = stream; - decoder = nullptr; + m_playbackEngine = nullptr; positionChanged(0); @@ -110,66 +120,76 @@ void QFFmpegMediaPlayer::setMedia(const QUrl &media, QIODevice *stream) } mediaStatusChanged(QMediaPlayer::LoadingMedia); - decoder = std::make_unique<Decoder>(); - connect(decoder.get(), &Decoder::endOfStream, this, &QFFmpegMediaPlayer::endOfStream); - connect(decoder.get(), &Decoder::errorOccured, this, &QFFmpegMediaPlayer::error); - - if (!decoder->setMedia(media, stream)) { - decoder.reset(); + m_playbackEngine = std::make_unique<PlaybackEngine>(); + connect(m_playbackEngine.get(), &PlaybackEngine::endOfStream, this, + &QFFmpegMediaPlayer::endOfStream); + connect(m_playbackEngine.get(), &PlaybackEngine::errorOccured, this, + &QFFmpegMediaPlayer::error); + + if (!m_playbackEngine->setMedia(media, stream)) { + m_playbackEngine.reset(); handleIncorrectMedia(QMediaPlayer::InvalidMedia); return; } - decoder->setAudioSink(m_audioOutput); - decoder->setVideoSink(m_videoSink); + m_playbackEngine->setAudioSink(m_audioOutput); + m_playbackEngine->setVideoSink(m_videoSink); durationChanged(duration()); tracksChanged(); metaDataChanged(); - seekableChanged(decoder->isSeekable()); - - audioAvailableChanged(!decoder->m_streamMap[QPlatformMediaPlayer::AudioStream].isEmpty()); - videoAvailableChanged(!decoder->m_streamMap[QPlatformMediaPlayer::VideoStream].isEmpty()); + seekableChanged(m_playbackEngine->isSeekable()); + audioAvailableChanged( + !m_playbackEngine->streamInfo(QPlatformMediaPlayer::AudioStream).isEmpty()); + videoAvailableChanged( + !m_playbackEngine->streamInfo(QPlatformMediaPlayer::VideoStream).isEmpty()); QMetaObject::invokeMethod(this, "delayedLoadedStatus", Qt::QueuedConnection); } void QFFmpegMediaPlayer::play() { - if (!decoder) + if (!m_playbackEngine) return; if (mediaStatus() == QMediaPlayer::EndOfMedia && state() == QMediaPlayer::StoppedState) { - decoder->seek(0); + m_playbackEngine->seek(0); positionChanged(0); + resetCurrentLoop(); } - decoder->play(); - positionUpdateTimer.start(); + + runPlayback(); +} + +void QFFmpegMediaPlayer::runPlayback() +{ + m_playbackEngine->play(); + m_positionUpdateTimer.start(); stateChanged(QMediaPlayer::PlayingState); mediaStatusChanged(QMediaPlayer::BufferedMedia); } void QFFmpegMediaPlayer::pause() { - if (!decoder) + if (!m_playbackEngine) return; if (mediaStatus() == QMediaPlayer::EndOfMedia && state() == QMediaPlayer::StoppedState) { - decoder->seek(0); + m_playbackEngine->seek(0); positionChanged(0); } - decoder->pause(); - positionUpdateTimer.stop(); + m_playbackEngine->pause(); + m_positionUpdateTimer.stop(); stateChanged(QMediaPlayer::PausedState); mediaStatusChanged(QMediaPlayer::BufferedMedia); } void QFFmpegMediaPlayer::stop() { - if (!decoder) + if (!m_playbackEngine) return; - decoder->stop(); - positionUpdateTimer.stop(); + m_playbackEngine->stop(); + m_positionUpdateTimer.stop(); positionChanged(0); stateChanged(QMediaPlayer::StoppedState); mediaStatusChanged(QMediaPlayer::LoadedMedia); @@ -181,13 +201,13 @@ void QFFmpegMediaPlayer::setAudioOutput(QPlatformAudioOutput *output) return; m_audioOutput = output; - if (decoder) - decoder->setAudioSink(output); + if (m_playbackEngine) + m_playbackEngine->setAudioSink(output); } QMediaMetaData QFFmpegMediaPlayer::metaData() const { - return decoder ? decoder->m_metaData : QMediaMetaData{}; + return m_playbackEngine ? m_playbackEngine->metaData() : QMediaMetaData{}; } void QFFmpegMediaPlayer::setVideoSink(QVideoSink *sink) @@ -196,8 +216,8 @@ void QFFmpegMediaPlayer::setVideoSink(QVideoSink *sink) return; m_videoSink = sink; - if (decoder) - decoder->setVideoSink(sink); + if (m_playbackEngine) + m_playbackEngine->setVideoSink(sink); } QVideoSink *QFFmpegMediaPlayer::videoSink() const @@ -207,25 +227,26 @@ QVideoSink *QFFmpegMediaPlayer::videoSink() const int QFFmpegMediaPlayer::trackCount(TrackType type) { - return decoder ? decoder->m_streamMap[type].count() : 0; + return m_playbackEngine ? m_playbackEngine->streamInfo(type).count() : 0; } QMediaMetaData QFFmpegMediaPlayer::trackMetaData(TrackType type, int streamNumber) { - if (!decoder || streamNumber < 0 || streamNumber >= decoder->m_streamMap[type].count()) + if (!m_playbackEngine || streamNumber < 0 + || streamNumber >= m_playbackEngine->streamInfo(type).count()) return {}; - return decoder->m_streamMap[type].at(streamNumber).metaData; + return m_playbackEngine->streamInfo(type).at(streamNumber).metaData; } int QFFmpegMediaPlayer::activeTrack(TrackType type) { - return decoder ? decoder->activeTrack(type) : -1; + return m_playbackEngine ? m_playbackEngine->activeTrack(type) : -1; } void QFFmpegMediaPlayer::setActiveTrack(TrackType type, int streamNumber) { - if (decoder) - decoder->setActiveTrack(type, streamNumber); + if (m_playbackEngine) + m_playbackEngine->setActiveTrack(type, streamNumber); } QT_END_NAMESPACE diff --git a/src/plugins/multimedia/ffmpeg/qffmpegmediaplayer_p.h b/src/plugins/multimedia/ffmpeg/qffmpegmediaplayer_p.h index 75e9ce5f3..7e9126a22 100644 --- a/src/plugins/multimedia/ffmpeg/qffmpegmediaplayer_p.h +++ b/src/plugins/multimedia/ffmpeg/qffmpegmediaplayer_p.h @@ -18,12 +18,12 @@ #include <private/qplatformmediaplayer_p.h> #include <qmediametadata.h> #include <qtimer.h> +#include <qpointer.h> #include "qffmpeg_p.h" QT_BEGIN_NAMESPACE namespace QFFmpeg { -class Decoder; class PlaybackEngine; } class QPlatformAudioOutput; @@ -32,7 +32,6 @@ class QFFmpegMediaPlayer : public QObject, public QPlatformMediaPlayer { Q_OBJECT public: - using Decoder = QFFmpeg::PlaybackEngine; QFFmpegMediaPlayer(QMediaPlayer *player); ~QFFmpegMediaPlayer(); @@ -55,8 +54,6 @@ public: void pause() override; void stop() override; -// bool streamPlaybackSupported() const { return false; } - void setAudioOutput(QPlatformAudioOutput *) override; QMediaMetaData metaData() const override; @@ -71,6 +68,9 @@ public: Q_INVOKABLE void delayedLoadedStatus() { mediaStatusChanged(QMediaPlayer::LoadedMedia); } +private: + void runPlayback(); + private slots: void updatePosition(); void endOfStream(); @@ -80,16 +80,16 @@ private slots: } private: - friend class QFFmpeg::Decoder; + QTimer m_positionUpdateTimer; - QTimer positionUpdateTimer; + using PlaybackEngine = QFFmpeg::PlaybackEngine; - std::unique_ptr<Decoder> decoder; + std::unique_ptr<PlaybackEngine> m_playbackEngine; QPlatformAudioOutput *m_audioOutput = nullptr; - QVideoSink *m_videoSink = nullptr; + QPointer<QVideoSink> m_videoSink; QUrl m_url; - QIODevice *m_device = nullptr; + QPointer<QIODevice> m_device; float m_playbackRate = 1.; }; diff --git a/src/plugins/multimedia/ffmpeg/qffmpegplaybackengine.cpp b/src/plugins/multimedia/ffmpeg/qffmpegplaybackengine.cpp index ca646c5c8..19105499d 100644 --- a/src/plugins/multimedia/ffmpeg/qffmpegplaybackengine.cpp +++ b/src/plugins/multimedia/ffmpeg/qffmpegplaybackengine.cpp @@ -425,10 +425,6 @@ void PlaybackEngine::setAudioSink(QAudioOutput *output) forceUpdate(); } -bool PlaybackEngine::isSeekable() const { - return m_isSeekable; -} - qint64 PlaybackEngine::currentPosition() const { auto pos = std::numeric_limits<qint64>::max(); diff --git a/src/plugins/multimedia/ffmpeg/qffmpegplaybackengine_p.h b/src/plugins/multimedia/ffmpeg/qffmpegplaybackengine_p.h index 48e939f82..682d9093b 100644 --- a/src/plugins/multimedia/ffmpeg/qffmpegplaybackengine_p.h +++ b/src/plugins/multimedia/ffmpeg/qffmpegplaybackengine_p.h @@ -62,7 +62,7 @@ class QFFmpegMediaPlayer; namespace QFFmpeg { -class PlaybackEngine : public QObject, protected MediaDataHolder +class PlaybackEngine : public QObject, public MediaDataHolder { Q_OBJECT public: @@ -100,17 +100,8 @@ public: using MediaDataHolder::activeTrack; - bool isSeekable() const; - qint64 currentPosition() const; - // To be removed after aligning with Decoder - using MediaDataHolder::m_currentAVStreamIndex; - using MediaDataHolder::m_duration; - using MediaDataHolder::m_metaData; - using MediaDataHolder::m_requestedStreams; - using MediaDataHolder::m_streamMap; - signals: void endOfStream(); void errorOccured(int, const QString &); diff --git a/tests/auto/integration/qmediaplayerbackend/tst_qmediaplayerbackend.cpp b/tests/auto/integration/qmediaplayerbackend/tst_qmediaplayerbackend.cpp index 0786145da..6e1365808 100644 --- a/tests/auto/integration/qmediaplayerbackend/tst_qmediaplayerbackend.cpp +++ b/tests/auto/integration/qmediaplayerbackend/tst_qmediaplayerbackend.cpp @@ -65,6 +65,8 @@ private slots: void multipleSeekStressTest(); void playbackRateChanging(); void durationDetectionIssues(); + void finiteLoops(); + void infiteLoops(); private: QUrl selectVideoFile(const QStringList& mediaCandidates); @@ -1619,6 +1621,130 @@ void tst_QMediaPlayerBackend::durationDetectionIssues() QCOMPARE(videoTracks.front().value(QMediaMetaData::Duration), QVariant(qint64(400))); } +static std::vector<std::pair<qint64, qint64>> +positionChangingIntervals(const QSignalSpy &positionSpy) +{ + std::vector<std::pair<qint64, qint64>> result; + for (auto ¶ms : positionSpy) { + const auto pos = params.front().value<qint64>(); + + if (result.empty() || pos < result.back().second) + result.emplace_back(pos, pos); + else + result.back().second = pos; + } + + return result; +} + +void tst_QMediaPlayerBackend::finiteLoops() +{ + if (localVideoFile3ColorsWithSound.isEmpty()) + QSKIP("Video format is not supported"); + +#ifdef Q_OS_MACOS + if (qEnvironmentVariable("QTEST_ENVIRONMENT").toLower() == "ci") + QSKIP("The test accidently gets crashed on macOS CI, not reproduced locally. To be " + "investigated: QTBUG-111744"); +#endif + + TestVideoSink surface(false); + QMediaPlayer player; + + QSignalSpy positionSpy(&player, &QMediaPlayer::positionChanged); + + player.setVideoOutput(&surface); + + QCOMPARE(player.loops(), 1); + player.setLoops(3); + QCOMPARE(player.loops(), 3); + + player.setSource(localVideoFile3ColorsWithSound); + player.setPlaybackRate(5); + + player.play(); + surface.waitForFrame(); + + // check pause doesn't affect looping + { + QTest::qWait(static_cast<int>(player.duration() * 3 + * 0.6 /*relative pos*/ / player.playbackRate())); + player.pause(); + player.play(); + } + + QTRY_COMPARE(player.playbackState(), QMediaPlayer::StoppedState); + + auto intervals = positionChangingIntervals(positionSpy); + + QCOMPARE(intervals.size(), 3); + QCOMPARE_GT(intervals[0].first, 0); + QCOMPARE(intervals[0].second, player.duration()); + QCOMPARE(intervals[1].first, 0); + QCOMPARE(intervals[1].second, player.duration()); + QCOMPARE(intervals[2].first, 0); + QCOMPARE(intervals[2].second, player.duration()); + + QCOMPARE(player.mediaStatus(), QMediaPlayer::EndOfMedia); + + // be sure that counter is reset if repeat the same + { + positionSpy.clear(); + player.play(); + player.setPlaybackRate(10); + surface.waitForFrame(); + + QTRY_COMPARE(player.playbackState(), QMediaPlayer::StoppedState); + QCOMPARE(positionChangingIntervals(positionSpy).size(), 3); + QCOMPARE(player.mediaStatus(), QMediaPlayer::EndOfMedia); + } +} + +void tst_QMediaPlayerBackend::infiteLoops() +{ + if (localVideoFile2.isEmpty()) + QSKIP("Video format is not supported"); + +#ifdef Q_OS_MACOS + if (qEnvironmentVariable("QTEST_ENVIRONMENT").toLower() == "ci") + QSKIP("The test accidently gets crashed on macOS CI, not reproduced locally. To be " + "investigated: QTBUG-111744"); +#endif + + TestVideoSink surface(false); + QMediaPlayer player; + + player.setVideoOutput(&surface); + + QCOMPARE(player.loops(), 1); + player.setLoops(QMediaPlayer::Infinite); + QCOMPARE(player.loops(), QMediaPlayer::Infinite); + + // select some small file + player.setSource(localVideoFile2); + player.setPlaybackRate(20); + + player.play(); + surface.waitForFrame(); + + for (int i = 0; i < 2; ++i) { + QSignalSpy positionSpy(&player, &QMediaPlayer::positionChanged); + + QTest::qWait( + std::max(static_cast<int>(player.duration() / player.playbackRate() * 4), + 300 /*ensure some minimum waiting time to reduce threading flakiness*/)); + QCOMPARE(player.mediaStatus(), QMediaPlayer::BufferedMedia); + QCOMPARE(player.playbackState(), QMediaPlayer::PlayingState); + + const auto intervals = positionChangingIntervals(positionSpy); + QVERIFY(!intervals.empty()); + QCOMPARE(intervals.front().second, player.duration()); + } + + player.stop(); // QMediaPlayer::stop stops whether or not looping is infinite + QCOMPARE(player.playbackState(), QMediaPlayer::StoppedState); +} + QTEST_MAIN(tst_QMediaPlayerBackend) #include "tst_qmediaplayerbackend.moc" |