Revision control
++ // Copyright (C) 2021 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
++
++ #include "qffmpegmediaplayer_p.h"
++ #include "private/qplatformaudiooutput_p.h"
++ #include "qvideosink.h"
++ #include "qaudiooutput.h"
++ #include "qaudiobufferoutput.h"
++ #include "qffmpegplaybackengine_p.h"
++ #include <qiodevice.h>
++ #include <qvideosink.h>
++ #include <QtMultimedia/qplaybackoptions.h>
++ #include <qtimer.h>
++ #include <QtConcurrent/QtConcurrent>
++
++ #include <qloggingcategory.h>
++
++ QT_BEGIN_NAMESPACE
++
++ namespace QFFmpeg {
++
++ class CancelToken : public ICancelToken
++ {
++ public:
++
++ bool isCancelled() const override { return m_cancelled.load(std::memory_order_acquire); }
++
++ void cancel() { m_cancelled.store(true, std::memory_order_release); }
++
++ private:
++ std::atomic_bool m_cancelled = false;
++ };
++
++ } // namespace QFFmpeg
++
++ using namespace QFFmpeg;
++
++ QFFmpegMediaPlayer::QFFmpegMediaPlayer(QMediaPlayer *player)
++ : QPlatformMediaPlayer(player)
++ {
++ m_positionUpdateTimer.setInterval(50);
++ m_positionUpdateTimer.setTimerType(Qt::PreciseTimer);
++ connect(&m_positionUpdateTimer, &QTimer::timeout, this, &QFFmpegMediaPlayer::updatePosition);
++ }
++
++ QFFmpegMediaPlayer::~QFFmpegMediaPlayer()
++ {
++ if (m_cancelToken)
++ m_cancelToken->cancel();
++
++ m_loadMedia.waitForFinished();
++ };
++
++ qint64 QFFmpegMediaPlayer::duration() const
++ {
++ return m_playbackEngine ? toUserDuration(m_playbackEngine->duration()).get() : 0;
++ }
++
++ void QFFmpegMediaPlayer::setPosition(qint64 position)
++ {
++ if (mediaStatus() == QMediaPlayer::LoadingMedia)
++ return;
++
++ if (m_playbackEngine) {
++ m_demuxerBuffered = false;
++ m_playbackEngine->seek(toTrackPosition(UserTrackPosition(position)));
++ updatePosition();
++ }
++
++ mediaStatusChanged(QMediaPlayer::LoadedMedia);
++ }
++
++ void QFFmpegMediaPlayer::updatePosition()
++ {
++ positionChanged(m_playbackEngine ? toUserPosition(m_playbackEngine->currentPosition()).get()
++ : 0);
++ }
++
++ void QFFmpegMediaPlayer::endOfStream()
++ {
++ // stop update timer and report end position anyway
++ m_positionUpdateTimer.stop();
++ QPointer currentPlaybackEngine(m_playbackEngine.get());
++ positionChanged(duration());
++
++ // skip changing state and mediaStatus if playbackEngine has been recreated,
++ // e.g. if new media has been loaded as a response to positionChanged signal
++ if (currentPlaybackEngine)
++ stateChanged(QMediaPlayer::StoppedState);
++ if (currentPlaybackEngine)
++ mediaStatusChanged(QMediaPlayer::EndOfMedia);
++ }
++
++ void QFFmpegMediaPlayer::onLoopChanged()
++ {
++ // report about finish and start
++ // reporting both signals is a bit contraversial
++ // but it eshures the idea of notifications about
++ // imporatant position points.
++ // Also, it ensures more predictable flow for testing.
++ positionChanged(duration());
++ positionChanged(0);
++ m_positionUpdateTimer.stop();
++ m_positionUpdateTimer.start();
++ }
++
++ void QFFmpegMediaPlayer::onBuffered()
++ {
++ m_demuxerBuffered = true;
++ if (mediaStatus() == QMediaPlayer::BufferingMedia)
++ mediaStatusChanged(QMediaPlayer::BufferedMedia);
++ }
++
++ float QFFmpegMediaPlayer::bufferProgress() const
++ {
++ return m_bufferProgress;
++ }
++
++ void QFFmpegMediaPlayer::mediaStatusChanged(QMediaPlayer::MediaStatus status)
++ {
++ if (mediaStatus() == status)
++ return;
++
++ const auto newBufferProgress = status == QMediaPlayer::BufferingMedia ? 0.25f // to be improved
++ : status == QMediaPlayer::BufferedMedia ? 1.f
++ : 0.f;
++
++ if (!QtPrivate::fuzzyCompare(newBufferProgress, m_bufferProgress)) {
++ m_bufferProgress = newBufferProgress;
++ bufferProgressChanged(newBufferProgress);
++ }
++
++ QPlatformMediaPlayer::mediaStatusChanged(status);
++ }
++
++ QMediaTimeRange QFFmpegMediaPlayer::availablePlaybackRanges() const
++ {
++ return {};
++ }
++
++ qreal QFFmpegMediaPlayer::playbackRate() const
++ {
++ return m_playbackRate;
++ }
++
++ void QFFmpegMediaPlayer::setPlaybackRate(qreal rate)
++ {
++ const float effectiveRate = std::max(static_cast<float>(rate), 0.0f);
++
++ if (QtPrivate::fuzzyCompare(m_playbackRate, effectiveRate))
++ return;
++
++ m_playbackRate = effectiveRate;
++
++ if (m_playbackEngine)
++ m_playbackEngine->setPlaybackRate(effectiveRate);
++
++ playbackRateChanged(effectiveRate);
++ }
++
++ QUrl QFFmpegMediaPlayer::media() const
++ {
++ return m_url;
++ }
++
++ const QIODevice *QFFmpegMediaPlayer::mediaStream() const
++ {
++ return m_device;
++ }
++
++ void QFFmpegMediaPlayer::handleIncorrectMedia(QMediaPlayer::MediaStatus status)
++ {
++ seekableChanged(false);
++ audioAvailableChanged(false);
++ videoAvailableChanged(false);
++ metaDataChanged();
++ mediaStatusChanged(status);
++ m_playbackEngine = nullptr;
++ };
++
++ void QFFmpegMediaPlayer::setMedia(const QUrl &media, QIODevice *stream)
++ {
++ // Wait for previous unfinished load attempts.
++ if (m_cancelToken)
++ m_cancelToken->cancel();
++
++ m_loadMedia.waitForFinished();
++
++ m_url = media;
++ m_device = stream;
++ m_playbackEngine = nullptr;
++ m_demuxerBuffered = false;
++
++ if (media.isEmpty() && !stream) {
++ handleIncorrectMedia(QMediaPlayer::NoMedia);
++ return;
++ }
++
++ mediaStatusChanged(QMediaPlayer::LoadingMedia);
++
++ m_requestedStatus = QMediaPlayer::StoppedState;
++
++ m_cancelToken = std::make_shared<CancelToken>();
++
++ // Load media asynchronously to keep GUI thread responsive while loading media
++ m_loadMedia = QtConcurrent::run([this, media, stream, cancelToken = m_cancelToken] {
++ // On worker thread
++ const MediaDataHolder::Maybe mediaHolder =
++ MediaDataHolder::create(media, stream, playbackOptions(), cancelToken);
++
++ // Transition back to calling thread using invokeMethod because
++ // QFuture continuations back on calling thread may deadlock (QTBUG-117918)
++ QMetaObject::invokeMethod(this, [this, mediaHolder, cancelToken] {
++ setMediaAsync(mediaHolder, cancelToken);
++ });
++ });
++ }
++
++ void QFFmpegMediaPlayer::setMediaAsync(QFFmpeg::MediaDataHolder::Maybe mediaDataHolder,
++ const std::shared_ptr<QFFmpeg::CancelToken> &cancelToken)
++ {
++ // If loading was cancelled, we do not emit any signals about failing
++ // to load media (or any other events). The rationale is that cancellation
++ // either happens during destruction, where the signals are no longer
++ // of interest, or it happens as a response to user requesting to load
++ // another media file. In the latter case, we don't want to risk popping
++ // up error dialogs or similar.
++ if (cancelToken->isCancelled()) {
++ return;
++ }
++
++ Q_ASSERT(mediaStatus() == QMediaPlayer::LoadingMedia);
++
++ if (!mediaDataHolder) {
++ const auto [code, description] = mediaDataHolder.error();
++ error(code, description);
++ handleIncorrectMedia(QMediaPlayer::MediaStatus::InvalidMedia);
++ return;
++ }
++
++ m_playbackEngine = std::make_unique<PlaybackEngine>(playbackOptions());
++
++ connect(m_playbackEngine.get(), &PlaybackEngine::endOfStream, this,
++ &QFFmpegMediaPlayer::endOfStream);
++ connect(m_playbackEngine.get(), &PlaybackEngine::errorOccured, this,
++ &QFFmpegMediaPlayer::error);
++ connect(m_playbackEngine.get(), &PlaybackEngine::loopChanged, this,
++ &QFFmpegMediaPlayer::onLoopChanged);
++ connect(m_playbackEngine.get(), &PlaybackEngine::buffered, this,
++ &QFFmpegMediaPlayer::onBuffered);
++
++ m_playbackEngine->setMedia(std::move(*mediaDataHolder.value()));
++
++ m_playbackEngine->setAudioBufferOutput(m_audioBufferOutput);
++ m_playbackEngine->setAudioSink(m_audioOutput);
++ m_playbackEngine->setVideoSink(m_videoSink);
++
++ m_playbackEngine->setLoops(loops());
++ m_playbackEngine->setPlaybackRate(m_playbackRate);
++ m_playbackEngine->setPitchCompensation(m_pitchCompensation);
++
++ durationChanged(duration());
++ tracksChanged();
++ metaDataChanged();
++ seekableChanged(m_playbackEngine->isSeekable());
++
++ audioAvailableChanged(
++ !m_playbackEngine->streamInfo(TrackType::AudioStream).isEmpty());
++ videoAvailableChanged(
++ !m_playbackEngine->streamInfo(TrackType::VideoStream).isEmpty());
++
++ mediaStatusChanged(QMediaPlayer::LoadedMedia);
++
++ if (m_requestedStatus != QMediaPlayer::StoppedState) {
++ if (m_requestedStatus == QMediaPlayer::PlayingState)
++ play();
++ else if (m_requestedStatus == QMediaPlayer::PausedState)
++ pause();
++ }
++ }
++
++ void QFFmpegMediaPlayer::play()
++ {
++ if (mediaStatus() == QMediaPlayer::LoadingMedia) {
++ m_requestedStatus = QMediaPlayer::PlayingState;
++ return;
++ }
++
++ if (!m_playbackEngine)
++ return;
++
++ if (mediaStatus() == QMediaPlayer::EndOfMedia && state() == QMediaPlayer::StoppedState) {
++ m_playbackEngine->seek(TrackPosition(0));
++ positionChanged(0);
++ }
++
++ runPlayback();
++ }
++
++ void QFFmpegMediaPlayer::runPlayback()
++ {
++ m_playbackEngine->play();
++ m_positionUpdateTimer.start();
++ stateChanged(QMediaPlayer::PlayingState);
++
++ if (mediaStatus() == QMediaPlayer::LoadedMedia || mediaStatus() == QMediaPlayer::EndOfMedia) {
++ mediaStatusChanged(m_demuxerBuffered ? QMediaPlayer::BufferedMedia
++ : QMediaPlayer::BufferingMedia);
++ }
++ }
++
++ void QFFmpegMediaPlayer::pause()
++ {
++ if (mediaStatus() == QMediaPlayer::LoadingMedia) {
++ m_requestedStatus = QMediaPlayer::PausedState;
++ return;
++ }
++
++ if (!m_playbackEngine)
++ return;
++
++ if (mediaStatus() == QMediaPlayer::EndOfMedia && state() == QMediaPlayer::StoppedState) {
++ m_playbackEngine->seek(TrackPosition(0));
++ positionChanged(0);
++ }
++ m_playbackEngine->pause();
++ m_positionUpdateTimer.stop();
++ stateChanged(QMediaPlayer::PausedState);
++
++ if (mediaStatus() == QMediaPlayer::LoadedMedia || mediaStatus() == QMediaPlayer::EndOfMedia) {
++ mediaStatusChanged(m_demuxerBuffered ? QMediaPlayer::BufferedMedia
++ : QMediaPlayer::BufferingMedia);
++ }
++ }
++
++ void QFFmpegMediaPlayer::stop()
++ {
++ if (mediaStatus() == QMediaPlayer::LoadingMedia) {
++ m_requestedStatus = QMediaPlayer::StoppedState;
++ return;
++ }
++
++ if (!m_playbackEngine)
++ return;
++
++ m_demuxerBuffered = false;
++ m_playbackEngine->stop();
++ m_positionUpdateTimer.stop();
++ m_playbackEngine->seek(TrackPosition(0));
++ positionChanged(0);
++ stateChanged(QMediaPlayer::StoppedState);
++ mediaStatusChanged(QMediaPlayer::LoadedMedia);
++ }
++
++ void QFFmpegMediaPlayer::setAudioOutput(QPlatformAudioOutput *output)
++ {
++ m_audioOutput = output;
++ if (m_playbackEngine)
++ m_playbackEngine->setAudioSink(output);
++ }
++
++ void QFFmpegMediaPlayer::setAudioBufferOutput(QAudioBufferOutput *output) {
++ m_audioBufferOutput = output;
++ if (m_playbackEngine)
++ m_playbackEngine->setAudioBufferOutput(output);
++ }
++
++ QMediaMetaData QFFmpegMediaPlayer::metaData() const
++ {
++ return m_playbackEngine ? m_playbackEngine->metaData() : QMediaMetaData{};
++ }
++
++ void QFFmpegMediaPlayer::setVideoSink(QVideoSink *sink)
++ {
++ m_videoSink = sink;
++ if (m_playbackEngine)
++ m_playbackEngine->setVideoSink(sink);
++ }
++
++ QVideoSink *QFFmpegMediaPlayer::videoSink() const
++ {
++ return m_videoSink;
++ }
++
++ int QFFmpegMediaPlayer::trackCount(TrackType type)
++ {
++ return m_playbackEngine ? m_playbackEngine->streamInfo(type).count() : 0;
++ }
++
++ QMediaMetaData QFFmpegMediaPlayer::trackMetaData(TrackType type, int streamNumber)
++ {
++ if (!m_playbackEngine || streamNumber < 0
++ || streamNumber >= m_playbackEngine->streamInfo(type).count())
++ return {};
++ return m_playbackEngine->streamInfo(type).at(streamNumber).metaData;
++ }
++
++ int QFFmpegMediaPlayer::activeTrack(TrackType type)
++ {
++ return m_playbackEngine ? m_playbackEngine->activeTrack(type) : -1;
++ }
++
++ void QFFmpegMediaPlayer::setActiveTrack(TrackType type, int streamNumber)
++ {
++ if (m_playbackEngine)
++ m_playbackEngine->setActiveTrack(type, streamNumber);
++ else
++ qWarning() << "Cannot set active track without open source";
++ }
++
++ void QFFmpegMediaPlayer::setLoops(int loops)
++ {
++ if (m_playbackEngine)
++ m_playbackEngine->setLoops(loops);
++
++ QPlatformMediaPlayer::setLoops(loops);
++ }
++
++ void QFFmpegMediaPlayer::setPitchCompensation(bool enabled)
++ {
++ if (enabled == m_pitchCompensation)
++ return;
++
++ m_pitchCompensation = enabled;
++ if (m_playbackEngine)
++ m_playbackEngine->setPitchCompensation(enabled);
++ QPlatformMediaPlayer::pitchCompensationChanged(enabled);
++ }
++
++ bool QFFmpegMediaPlayer::pitchCompensation() const
++ {
++ return m_pitchCompensation;
++ }
++
++ QPlatformMediaPlayer::PitchCompensationAvailability
++ QFFmpegMediaPlayer::pitchCompensationAvailability() const
++ {
++ return PitchCompensationAvailability::Available;
++ }
++
++ QT_END_NAMESPACE
++
++ #include "moc_qffmpegmediaplayer_p.cpp"