File indexing completed on 2024-05-19 04:29:18

0001 /* This file is part of the KDE project
0002    SPDX-FileCopyrightText: 2022 Emmet O'Neill <emmetoneill.pdx@gmail.com>
0003    SPDX-FileCopyrightText: 2022 Eoin O'Neill <eoinoneill1991@gmail.com>
0004 
0005    SPDX-License-Identifier: LGPL-2.0-or-later
0006 */
0007 
0008 #include "KisPlaybackEngineMLT.h"
0009 
0010 #include <QMap>
0011 
0012 #include <QElapsedTimer>
0013 #include <QWaitCondition>
0014 
0015 #include "kis_canvas2.h"
0016 #include "KisCanvasAnimationState.h"
0017 #include "kis_image_animation_interface.h"
0018 #include "kis_raster_keyframe_channel.h"
0019 #include "kis_signal_compressor_with_param.h"
0020 #include "animation/KisFrameDisplayProxy.h"
0021 #include "KisViewManager.h"
0022 #include "kis_onion_skin_compositor.h"
0023 
0024 #include <mlt++/Mlt.h>
0025 #include <mlt++/MltConsumer.h>
0026 #include <mlt++/MltFrame.h>
0027 #include <mlt++/MltFilter.h>
0028 #include <mlt-7/framework/mlt_service.h>
0029 
0030 #include "KisRollingMeanAccumulatorWrapper.h"
0031 #include "KisRollingSumAccumulatorWrapper.h"
0032 
0033 #ifdef Q_OS_ANDROID
0034 #include <KisAndroidFileProxy.h>
0035 #endif
0036 
0037 #include "kis_debug.h"
0038 
0039 #include "KisMLTProducerKrita.h"
0040 
0041 
0042 const float SCRUB_AUDIO_SECONDS = 0.128f;
0043 
0044 struct KisPlaybackEngineMLT::FrameWaitingInterface {
0045     bool renderingAllowed {false};
0046     bool waitingForFrame {false};
0047     QMutex renderingControlMutex;
0048     QWaitCondition renderingWaitCondition;
0049 };
0050 
0051 namespace {
0052 
0053 struct FrameRenderingStats
0054 {
0055     static constexpr int frameStatsWindow = 50;
0056 
0057     KisRollingMeanAccumulatorWrapper averageFrameDuration {frameStatsWindow};
0058     KisRollingSumAccumulatorWrapper droppedFramesCount {frameStatsWindow};
0059     int lastRenderedFrame {-1};
0060     QElapsedTimer timeSinceLastFrame;
0061 
0062     void reset() {
0063         averageFrameDuration.reset(frameStatsWindow);
0064         droppedFramesCount.reset(frameStatsWindow);
0065         lastRenderedFrame = -1;
0066     }
0067 };
0068 
0069 }
0070 /**
0071  *  This static funciton responds to MLT consumer requests for frames. This may
0072  *  continue to be called even when playback is stopped due to it running
0073  *  simultaneously in a separate thread.
0074  */
0075 static void mltOnConsumerFrameShow(mlt_consumer c, void* p_self, mlt_frame p_frame) {
0076     KisPlaybackEngineMLT* self = static_cast<KisPlaybackEngineMLT*>(p_self);
0077     Mlt::Frame frame(p_frame);
0078     Mlt::Consumer consumer(c);
0079     const int position = frame.get_position();
0080 
0081     KisPlaybackEngineMLT::FrameWaitingInterface *iface = self->frameWaitingInterface();
0082 
0083     /**
0084      * This function is called from the non-gui thread owned by MLT,
0085      * so we should wait until the frame would be really rendered.
0086      * This way MLT will have information about frame rendering speed
0087      * and will be able to drop frames accordingly.
0088      *
0089      * NOTE: we cannot use BlockingQueuedConnection here because it
0090      * would deadlock on any stream property change, when the the GUI
0091      * thread would call consumer->stop().
0092      *
0093      */
0094     QMutexLocker l(&iface->renderingControlMutex);
0095 
0096     if (!iface->renderingAllowed) return;
0097 
0098     KIS_SAFE_ASSERT_RECOVER_RETURN(!iface->waitingForFrame);
0099     iface->waitingForFrame = true;
0100 
0101     emit self->sigChangeActiveCanvasFrame(position);
0102 
0103     while (iface->renderingAllowed && iface->waitingForFrame) {
0104         iface->renderingWaitCondition.wait(&iface->renderingControlMutex);
0105     }
0106 }
0107 
0108 //=====
0109 
0110 struct KisPlaybackEngineMLT::Private {
0111 
0112     Private(KisPlaybackEngineMLT* p_self)
0113         : m_self(p_self)
0114         , playbackSpeed(1.0)
0115         , mute(false)
0116     {
0117         // Initialize MLT...
0118         repository.reset(Mlt::Factory::init());
0119 
0120         // Register our backend plugin
0121         registerKritaMLTProducer(repository.data());
0122 
0123         profile.reset(new Mlt::Profile());
0124         profile->set_frame_rate(24, 1);
0125 
0126         {
0127             std::function<void (int)> callback(std::bind(&Private::pushAudio, this, std::placeholders::_1));
0128             sigPushAudioCompressor.reset(
0129                         new KisSignalCompressorWithParam<int>(1000 * SCRUB_AUDIO_SECONDS, callback, KisSignalCompressor::FIRST_ACTIVE)
0130                         );
0131         }
0132 
0133         {
0134             std::function<void (const double)> callback(std::bind(&KisPlaybackEngineMLT::throttledSetSpeed, m_self, std::placeholders::_1));
0135             sigSetPlaybackSpeed.reset(
0136                         new KisSignalCompressorWithParam<double>(100, callback, KisSignalCompressor::POSTPONE)
0137                         );
0138         }
0139 
0140         initializeConsumers();
0141     }
0142 
0143     ~Private() {
0144         cleanupConsumers();
0145         repository.reset();
0146         Mlt::Factory::close();
0147     }
0148 
0149     void pushAudio(int frame) {
0150 
0151         if (pushConsumer->is_stopped() || !m_self->activeCanvas()) {
0152             return;
0153         }
0154 
0155         QSharedPointer<Mlt::Producer> activeProducer = canvasProducers[m_self->activeCanvas()];
0156         if (activePlaybackMode() == PLAYBACK_PUSH && activeProducer) {
0157             const int SCRUB_AUDIO_WINDOW = profile->frame_rate_num() * SCRUB_AUDIO_SECONDS;
0158             for (int i = 0; i < SCRUB_AUDIO_WINDOW; i++ ) {
0159                 Mlt::Frame* f = activeProducer->get_frame(frame + i );
0160                 pushConsumer->push(*f);
0161                 delete f;
0162             }
0163 
0164             // It turns out that get_frame actually seeks to the frame too,
0165             // Not having this last seek will cause unexpected "jumps" at
0166             // the beginning of playback...
0167             activeProducer->seek(frame);
0168         }
0169     }
0170 
0171     void initializeConsumers() {
0172         pushConsumer.reset(new Mlt::PushConsumer(*profile, "sdl2_audio"));
0173         pullConsumer.reset(new Mlt::Consumer(*profile, "sdl2_audio"));
0174         pullConsumerConnection.reset(pullConsumer->listen("consumer-frame-show", m_self, (mlt_listener)mltOnConsumerFrameShow));
0175     }
0176 
0177     void cleanupConsumers() {
0178         if (pullConsumer && !pullConsumer->is_stopped()) {
0179             pullConsumer->stop();
0180         }
0181 
0182         if (pushConsumer && !pushConsumer->is_stopped()) {
0183             pushConsumer->stop();
0184         }
0185 
0186         pullConsumer.reset();
0187         pushConsumer.reset();
0188     }
0189 
0190     KisCanvas2* activeCanvas() {
0191         return m_self->activeCanvas();
0192     }
0193 
0194     PlaybackMode activePlaybackMode() {
0195         KIS_ASSERT_RECOVER_RETURN_VALUE(activeCanvas(), PLAYBACK_PUSH);
0196         KIS_ASSERT_RECOVER_RETURN_VALUE(activeCanvas()->animationState(), PLAYBACK_PUSH);
0197         return activeCanvas()->animationState()->playbackState() == PlaybackState::PLAYING ? PLAYBACK_PULL : PLAYBACK_PUSH;
0198     }
0199 
0200     QSharedPointer<Mlt::Producer> activeProducer() {
0201         KIS_ASSERT_RECOVER_RETURN_VALUE(activeCanvas(), nullptr);
0202         KIS_ASSERT_RECOVER_RETURN_VALUE(canvasProducers.contains(activeCanvas()), nullptr);
0203         return canvasProducers[activeCanvas()];
0204     }
0205 
0206     bool dropFrames() const {
0207         return m_self->dropFrames();
0208     }
0209 
0210 private:
0211     KisPlaybackEngineMLT* m_self;
0212 
0213 public:
0214     QScopedPointer<Mlt::Repository> repository;
0215     QScopedPointer<Mlt::Profile> profile;
0216 
0217     //MLT PUSH CONSUMER
0218     QScopedPointer<Mlt::Consumer> pullConsumer;
0219     QScopedPointer<Mlt::Event> pullConsumerConnection;
0220 
0221     //MLT PULL CONSUMER
0222     QScopedPointer<Mlt::PushConsumer> pushConsumer;
0223 
0224     // Map of handles to Mlt producers..
0225     QMap<KisCanvas2*, QSharedPointer<Mlt::Producer>> canvasProducers;
0226 
0227     QScopedPointer<KisSignalCompressorWithParam<int>> sigPushAudioCompressor;
0228     QScopedPointer<KisSignalCompressorWithParam<double>> sigSetPlaybackSpeed;
0229 
0230     double playbackSpeed;
0231     bool mute;
0232 
0233     FrameWaitingInterface frameWaitingInterface;
0234     FrameRenderingStats frameStats;
0235 };
0236 
0237 //=====
0238 
0239 /**
0240  * @brief The StopAndResumeConsumer struct is used to encapsulate optional
0241  * stop-and-then-resume behavior of a consumer. Using RAII, we can stop
0242  * a consumer at construction and simply resume it when it exits scope.
0243  */
0244 struct KisPlaybackEngineMLT::StopAndResume {
0245 public:
0246     explicit StopAndResume(KisPlaybackEngineMLT::Private* p_d, bool requireFullRestart = false)
0247         : m_d(p_d)
0248     {
0249         KIS_ASSERT(p_d);
0250 
0251 
0252         {
0253             QMutexLocker l(&m_d->frameWaitingInterface.renderingControlMutex);
0254             m_d->frameWaitingInterface.renderingAllowed = false;
0255             m_d->frameWaitingInterface.renderingWaitCondition.wakeAll();
0256         }
0257 
0258         m_d->pushConsumer->stop();
0259         m_d->pushConsumer->purge();
0260         m_d->pullConsumer->stop();
0261         m_d->pullConsumer->purge();
0262         m_d->pullConsumer->disconnect_all_producers();
0263 
0264         if (requireFullRestart) {
0265             m_d->cleanupConsumers();
0266         }
0267     }
0268 
0269     ~StopAndResume() {
0270         KIS_ASSERT(m_d);
0271         if (!m_d->pushConsumer || !m_d->pullConsumer) {
0272             m_d->initializeConsumers();
0273         }
0274 
0275         if (m_d->activeCanvas()) {
0276             KisCanvasAnimationState* animationState = m_d->activeCanvas()->animationState();
0277             KIS_SAFE_ASSERT_RECOVER_RETURN(animationState);
0278 
0279             {
0280                 QMutexLocker l(&m_d->frameWaitingInterface.renderingControlMutex);
0281                 m_d->frameWaitingInterface.renderingAllowed = true;
0282                 m_d->frameWaitingInterface.waitingForFrame = false;
0283 
0284                 m_d->frameWaitingInterface.renderingWaitCondition.wakeAll();
0285             }
0286 
0287             m_d->frameStats.reset();
0288 
0289             {
0290                 /**
0291                  * Make sure that **all** producer properties are initialized **before**
0292                  * the consumers start pulling stuff from the producer. Otherwise we
0293                  * can get a race condition with the consumer's read-ahead thread.
0294                  */
0295 
0296                 KisImageAnimationInterface* animInterface = m_d->activeCanvas()->image()->animationInterface();
0297                 m_d->activeProducer()->set("start_frame", animInterface->activePlaybackRange().start());
0298                 m_d->activeProducer()->set("end_frame", animInterface->activePlaybackRange().end());
0299                 m_d->activeProducer()->set("speed", m_d->playbackSpeed);
0300                 const int shouldLimit = m_d->activePlaybackMode() == PLAYBACK_PUSH ? 0 : 1;
0301                 m_d->activeProducer()->set("limit_enabled", shouldLimit);
0302             }
0303 
0304             if (m_d->activePlaybackMode() == PLAYBACK_PUSH) {
0305                 m_d->pushConsumer->set("volume", m_d->mute ? 0.0 : animationState->currentVolume());
0306                 m_d->pushConsumer->start();
0307             } else {
0308                 m_d->pullConsumer->connect_producer(*m_d->activeProducer());
0309                 m_d->pullConsumer->set("volume", m_d->mute ? 0.0 : animationState->currentVolume());
0310                 m_d->pullConsumer->set("real_time", m_d->dropFrames() ? 1 : 0);
0311                 m_d->pullConsumer->start();
0312             }
0313         }
0314     }
0315 
0316 private:
0317     Private* m_d;
0318 };
0319 
0320 //=====
0321 
0322 KisPlaybackEngineMLT::KisPlaybackEngineMLT(QObject *parent)
0323     : KisPlaybackEngine(parent)
0324     , m_d( new Private(this))
0325 {
0326     connect(this, &KisPlaybackEngineMLT::sigChangeActiveCanvasFrame, this, &KisPlaybackEngineMLT::throttledShowFrame, Qt::UniqueConnection);
0327 }
0328 
0329 KisPlaybackEngineMLT::~KisPlaybackEngineMLT()
0330 {
0331 }
0332 
0333 void KisPlaybackEngineMLT::seek(int frameIndex, SeekOptionFlags flags)
0334 {
0335     KIS_ASSERT(activeCanvas() && activeCanvas()->animationState());
0336     KisCanvasAnimationState* animationState = activeCanvas()->animationState();
0337 
0338     if (m_d->activePlaybackMode() == PLAYBACK_PUSH) {
0339         m_d->canvasProducers[activeCanvas()]->seek(frameIndex);
0340 
0341         if (flags & SEEK_PUSH_AUDIO) {
0342 
0343             m_d->sigPushAudioCompressor->start(frameIndex);
0344         }
0345 
0346         animationState->showFrame(frameIndex, (flags & SEEK_FINALIZE) > 0);
0347     }
0348 }
0349 
0350 void KisPlaybackEngineMLT::setupProducer(boost::optional<QFileInfo> file)
0351 {
0352     if (!m_d->canvasProducers.contains(activeCanvas())) {
0353         connect(activeCanvas(), SIGNAL(destroyed(QObject*)), this, SLOT(canvasDestroyed(QObject*)));
0354     }
0355 
0356     //First, assign to "count" producer.
0357     m_d->canvasProducers[activeCanvas()] = QSharedPointer<Mlt::Producer>(new Mlt::Producer(*m_d->profile, "krita_play_chunk", "count"));
0358 
0359     //If we have a file and the file has a valid producer, use that. Otherwise, stick to our "default" producer.
0360     if (file.has_value()) {
0361         QSharedPointer<Mlt::Producer> producer(
0362 
0363 #ifdef Q_OS_ANDROID
0364             new Mlt::Producer(*m_d->profile,
0365                               "krita_play_chunk",
0366                               KisAndroidFileProxy::getFileFromContentUri(file->absoluteFilePath()).toUtf8().data()));
0367 #else
0368         new Mlt::Producer(*m_d->profile, "krita_play_chunk", file->absoluteFilePath().toUtf8().data()));
0369 #endif
0370         if (producer->is_valid()) {
0371             m_d->canvasProducers[activeCanvas()] = producer;
0372         } else {
0373             // SANITY CHECK: Check that the MLT plugins and resources are where the program expects them to be.
0374             // HINT -- Check krita/main.cc's mlt environment variable setup for appimage.
0375             KIS_SAFE_ASSERT_RECOVER_NOOP(qEnvironmentVariableIsSet("MLT_REPOSITORY"));
0376             KIS_SAFE_ASSERT_RECOVER_NOOP(qEnvironmentVariableIsSet("MLT_PROFILES_PATH"));
0377             KIS_SAFE_ASSERT_RECOVER_NOOP(qEnvironmentVariableIsSet("MLT_PRESETS_PATH"));
0378             qDebug() << "Warning: Invalid MLT producer for file: " << ppVar(file->absoluteFilePath()) << " Falling back to audio-less playback.";
0379         }
0380     }
0381 
0382     KisImageAnimationInterface *animInterface = activeCanvas()->image()->animationInterface();
0383     QSharedPointer<Mlt::Producer> producer = m_d->canvasProducers[activeCanvas()];
0384     KIS_ASSERT(producer->is_valid());
0385     KIS_ASSERT(animInterface);
0386 
0387     producer->set("start_frame", animInterface->documentPlaybackRange().start());
0388     producer->set("end_frame", animInterface->documentPlaybackRange().end());
0389     producer->set("limit_enabled", false);
0390     producer->set("speed", m_d->playbackSpeed);
0391 }
0392 
0393 void KisPlaybackEngineMLT::setCanvas(KoCanvasBase *p_canvas)
0394 {
0395     KisCanvas2* canvas = dynamic_cast<KisCanvas2*>(p_canvas);
0396 
0397     if (activeCanvas() == canvas) {
0398         return;
0399     }
0400 
0401     if (activeCanvas()) {
0402         KisCanvasAnimationState* animationState = activeCanvas()->animationState();
0403 
0404         // Disconnect old canvas, prepare for new one..
0405         if (animationState) {
0406             this->disconnect(animationState);
0407             animationState->disconnect(this);
0408         }
0409 
0410         // Disconnect old image, prepare for new one..
0411         auto image = activeCanvas()->image();
0412         if (image && image->animationInterface()) {
0413             this->disconnect(image->animationInterface());
0414             image->animationInterface()->disconnect(this);
0415         }
0416     }
0417 
0418     StopAndResume stopResume(m_d.data(), true);
0419 
0420     KisPlaybackEngine::setCanvas(p_canvas);
0421 
0422     // Connect new canvas..
0423     if (activeCanvas()) {
0424         KisCanvasAnimationState* animationState = activeCanvas()->animationState();
0425         KIS_SAFE_ASSERT_RECOVER_RETURN(animationState);
0426 
0427         connect(animationState, &KisCanvasAnimationState::sigPlaybackStateChanged, this, [this](PlaybackState state){
0428             Q_UNUSED(state); // We don't need the state yet -- we just want to stop and resume playback according to new state info.
0429             QSharedPointer<Mlt::Producer> activeProducer = m_d->canvasProducers[activeCanvas()];
0430             StopAndResume callbackStopResume(m_d.data());
0431         });
0432 
0433         connect(animationState, &KisCanvasAnimationState::sigPlaybackMediaChanged, this, [this](){
0434             KisCanvasAnimationState* animationState = activeCanvas()->animationState();
0435             if (animationState) {
0436                 setupProducer(animationState->mediaInfo());
0437             }
0438         });
0439 
0440         connect(animationState, &KisCanvasAnimationState::sigPlaybackSpeedChanged, this, [this](qreal value){
0441             m_d->sigSetPlaybackSpeed->start(value);
0442         });
0443         m_d->playbackSpeed = animationState->playbackSpeed();
0444 
0445         connect(animationState, &KisCanvasAnimationState::sigAudioLevelChanged, this, &KisPlaybackEngineMLT::setAudioVolume);
0446 
0447         auto image = activeCanvas()->image();
0448         KIS_SAFE_ASSERT_RECOVER_RETURN(image);
0449 
0450         // Connect new image..
0451         connect(image->animationInterface(), &KisImageAnimationInterface::sigFramerateChanged, this, [this](){
0452             StopAndResume callbackStopResume(m_d.data());
0453             m_d->profile->set_frame_rate(activeCanvas()->image()->animationInterface()->framerate(), 1);
0454         });
0455 
0456         connect(image->animationInterface(), &KisImageAnimationInterface::sigPlaybackRangeChanged, this, [this](){
0457             QSharedPointer<Mlt::Producer> producer = m_d->canvasProducers[activeCanvas()];
0458             auto image = activeCanvas()->image();
0459             KIS_SAFE_ASSERT_RECOVER_RETURN(image);
0460             producer->set("start_frame", image->animationInterface()->activePlaybackRange().start());
0461             producer->set("end_frame", image->animationInterface()->activePlaybackRange().end());
0462         });
0463 
0464         setupProducer(animationState->mediaInfo());
0465     }
0466 
0467 }
0468 
0469 void KisPlaybackEngineMLT::unsetCanvas() {
0470     setCanvas(nullptr);
0471 }
0472 
0473 void KisPlaybackEngineMLT::canvasDestroyed(QObject *canvas)
0474 {
0475     KIS_SAFE_ASSERT_RECOVER_RETURN(m_d->activeCanvas() != canvas);
0476 
0477     /**
0478      * We cannot use QMap::remove here, because the `canvas` is already
0479      * half-destroyed and we cannot up-cast to KisCanvas2 anymore
0480      */
0481     for (auto it = m_d->canvasProducers.begin(); it != m_d->canvasProducers.end(); ++it) {
0482         if (it.key() == canvas) {
0483             m_d->canvasProducers.erase(it);
0484             break;
0485         }
0486     }
0487 }
0488 
0489 void KisPlaybackEngineMLT::throttledShowFrame(const int frame)
0490 {
0491     if (activeCanvas() && activeCanvas()->animationState() &&
0492             m_d->activePlaybackMode() == PLAYBACK_PULL ) {
0493 
0494         if (m_d->frameStats.lastRenderedFrame < 0) {
0495             m_d->frameStats.timeSinceLastFrame.start();
0496         } else {
0497             const int droppedFrames = qMax(0, frame - m_d->frameStats.lastRenderedFrame - 1);
0498             m_d->frameStats.averageFrameDuration(m_d->frameStats.timeSinceLastFrame.restart());
0499             m_d->frameStats.droppedFramesCount(droppedFrames);
0500         }
0501         m_d->frameStats.lastRenderedFrame = frame;
0502 
0503         activeCanvas()->animationState()->showFrame(frame);
0504     }
0505 
0506     {
0507         QMutexLocker l(&m_d->frameWaitingInterface.renderingControlMutex);
0508         m_d->frameWaitingInterface.waitingForFrame = false;
0509         m_d->frameWaitingInterface.renderingWaitCondition.wakeAll();
0510     }
0511 }
0512 
0513 void KisPlaybackEngineMLT::throttledSetSpeed(const double speed)
0514 {
0515     StopAndResume stopResume(m_d.data(), false);
0516     m_d->playbackSpeed = speed;
0517 }
0518 
0519 void KisPlaybackEngineMLT::setAudioVolume(qreal volumeNormalized)
0520 {
0521     if (m_d->mute) {
0522         m_d->pullConsumer->set("volume", 0.0);
0523         m_d->pushConsumer->set("volume", 0.0);
0524     } else {
0525         m_d->pullConsumer->set("volume", volumeNormalized);
0526         m_d->pushConsumer->set("volume", volumeNormalized);
0527     }
0528 }
0529 
0530 KisPlaybackEngineMLT::FrameWaitingInterface *KisPlaybackEngineMLT::frameWaitingInterface()
0531 {
0532     return &m_d->frameWaitingInterface;
0533 }
0534 
0535 void KisPlaybackEngineMLT::setDropFramesMode(bool value)
0536 {
0537     // restart playback if it was active
0538     StopAndResume r(m_d.data(), false);
0539 
0540     KisPlaybackEngine::setDropFramesMode(value);
0541 }
0542 
0543 void KisPlaybackEngineMLT::setMute(bool val)
0544 {
0545     KIS_SAFE_ASSERT_RECOVER_RETURN(activeCanvas() && activeCanvas()->animationState());
0546     KisCanvasAnimationState* animationState = activeCanvas()->animationState();
0547 
0548     qreal currentVolume = animationState->currentVolume();
0549     m_d->mute = val;
0550     setAudioVolume(currentVolume);
0551 }
0552 
0553 bool KisPlaybackEngineMLT::isMute()
0554 {
0555     return m_d->mute;
0556 }
0557 
0558 KisPlaybackEngine::PlaybackStats KisPlaybackEngineMLT::playbackStatistics() const
0559 {
0560     KisPlaybackEngine::PlaybackStats stats;
0561 
0562     if (activeCanvas() && activeCanvas()->animationState() &&
0563         m_d->activePlaybackMode() == PLAYBACK_PULL ) {
0564 
0565         const int droppedFrames = m_d->frameStats.droppedFramesCount.rollingSum();
0566         const int totalFrames =
0567             m_d->frameStats.droppedFramesCount.rollingCount() +
0568             droppedFrames;
0569 
0570         stats.droppedFramesPortion = qreal(droppedFrames) / totalFrames;
0571         stats.expectedFps = qreal(activeCanvas()->image()->animationInterface()->framerate()) * m_d->playbackSpeed;
0572 
0573         const qreal avgTimePerFrame = m_d->frameStats.averageFrameDuration.rollingMeanSafe();
0574         stats.realFps = !qFuzzyIsNull(avgTimePerFrame) ? 1000.0 / avgTimePerFrame : 0.0;
0575 
0576     }
0577 
0578     return stats;
0579 }
0580 
0581 
0582