Revision control
Copy as Markdown
// 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"