Krita Source Code Documentation
Loading...
Searching...
No Matches
KisPlaybackEngineMLT.cpp
Go to the documentation of this file.
1/* This file is part of the KDE project
2 SPDX-FileCopyrightText: 2022 Emmet O'Neill <emmetoneill.pdx@gmail.com>
3 SPDX-FileCopyrightText: 2022 Eoin O'Neill <eoinoneill1991@gmail.com>
4
5 SPDX-License-Identifier: LGPL-2.0-or-later
6*/
7
9
10#include <QMap>
11
12#include <QMutex>
13#include <QMutexLocker>
14#include <QElapsedTimer>
15#include <QWaitCondition>
16
17#include "kis_canvas2.h"
23#include "KisViewManager.h"
25
26#include <mlt++/Mlt.h>
27#include <mlt++/MltConsumer.h>
28#include <mlt++/MltFrame.h>
29#include <mlt++/MltFilter.h>
30#include <mlt-7/framework/mlt_service.h>
31
34
35#ifdef Q_OS_ANDROID
36#include <KisAndroidFileProxy.h>
37#endif
38
39#include "kis_debug.h"
40
41#include "KisMLTProducerKrita.h"
42
43
44//#define MLT_LOG_REDIRECTION
45
46#ifdef MLT_LOG_REDIRECTION
47void qt_redirection_callback(void *ptr, int level, const char *fmt, va_list vl)
48{
49 static int print_prefix = 1;
50 mlt_properties properties = ptr ? MLT_SERVICE_PROPERTIES((mlt_service) ptr) : NULL;
51
52 if (level > mlt_log_get_level())
53 return;
54
55 static const int prefix_size = 200;
56 char prefix[prefix_size] = "";
57
58 if (print_prefix && properties) {
59 char *mlt_type = mlt_properties_get(properties, "mlt_type");
60 char *mlt_service = mlt_properties_get(properties, "mlt_service");
61 char *resource = mlt_properties_get(properties, "resource");
62
63 if (!(resource && *resource && resource[0] == '<' && resource[strlen(resource) - 1] == '>'))
64 mlt_type = mlt_properties_get(properties, "mlt_type");
65 if (mlt_service)
66 snprintf(prefix, prefix_size, "[%s %s] ", mlt_type, mlt_service);
67 else
68 snprintf(prefix, prefix_size, "[%s %p] ", mlt_type, ptr);
69 if (resource)
70 snprintf(prefix, prefix_size, "%s\n ", resource);
71 qDebug().nospace() << qPrintable(prefix);
72 }
73 print_prefix = strstr(fmt, "\n") != NULL;
74 vsnprintf(prefix, prefix_size, fmt, vl);
75 qDebug().nospace() << qPrintable(prefix);
76}
77#endif
78
79
80const float SCRUB_AUDIO_SECONDS = 0.128f;
81
88
89namespace {
90
91struct FrameRenderingStats
92{
93 static constexpr int frameStatsWindow = 50;
94
95 KisRollingMeanAccumulatorWrapper averageFrameDuration {frameStatsWindow};
96 KisRollingSumAccumulatorWrapper droppedFramesCount {frameStatsWindow};
97 int lastRenderedFrame {-1};
98 QElapsedTimer timeSinceLastFrame;
99
100 void reset() {
101 averageFrameDuration.reset(frameStatsWindow);
102 droppedFramesCount.reset(frameStatsWindow);
103 lastRenderedFrame = -1;
104 }
105};
106
107}
113static void mltOnConsumerFrameShow(mlt_consumer c, void* p_self, mlt_frame p_frame) {
114 KisPlaybackEngineMLT* self = static_cast<KisPlaybackEngineMLT*>(p_self);
115 Mlt::Frame frame(p_frame);
116 Mlt::Consumer consumer(c);
117 const int position = frame.get_position();
118
120
132 QMutexLocker l(&iface->renderingControlMutex);
133
134 if (!iface->renderingAllowed) return;
135
137 iface->waitingForFrame = true;
138
139 Q_EMIT self->sigChangeActiveCanvasFrame(position);
140
141 while (iface->renderingAllowed && iface->waitingForFrame) {
143 }
144}
145
146//=====
147
149
151 : m_self(p_self)
152 , playbackSpeed(1.0)
153 , mute(false)
154 {
155 qDebug() << "Initializing MLT Animation Playback Engine. MLT ver: " << QString(mlt_version_get_string());
156
157 // Initialize MLT...
158 repository.reset(Mlt::Factory::init());
159
160#ifdef MLT_LOG_REDIRECTION
161 mlt_log_set_level(MLT_LOG_VERBOSE);
162 mlt_log_set_callback(&qt_redirection_callback);
163#endif /* MLT_LOG_REDIRECTION */
164
165 // Register our backend plugin
167
168 profile.reset(new Mlt::Profile());
169 profile->set_frame_rate(24, 1);
170
171 {
172 std::function<void (int)> callback(std::bind(&Private::pushAudio, this, std::placeholders::_1));
175 );
176 }
177
178 {
179 std::function<void (const double)> callback(std::bind(&KisPlaybackEngineMLT::throttledSetSpeed, m_self, std::placeholders::_1));
182 );
183 }
184
186 }
187
190 repository.reset();
191 Mlt::Factory::close();
192 }
193
194 void pushAudio(int frame) {
195
196 if (pushConsumer->is_stopped() || !m_self->activeCanvas()) {
197 return;
198 }
199
202 const int SCRUB_AUDIO_WINDOW = qMax(1, qRound(profile->frame_rate_num() * SCRUB_AUDIO_SECONDS));
203 activeProducer->seek(frame);
204 for (int i = 0; i < SCRUB_AUDIO_WINDOW; i++ ) {
205 Mlt::Frame* f = activeProducer->get_frame();
206 pushConsumer->push(*f);
207 delete f;
208 }
209
210 // It turns out that get_frame actually seeks to the frame too,
211 // Not having this last seek will cause unexpected "jumps" at
212 // the beginning of playback...
213 activeProducer->seek(frame);
214 }
215 }
216
218 pushConsumer.reset(new Mlt::PushConsumer(*profile, "sdl2_audio"));
219 pullConsumer.reset(new Mlt::Consumer(*profile, "sdl2_audio"));
220 pullConsumerConnection.reset(pullConsumer->listen("consumer-frame-show", m_self, (mlt_listener)mltOnConsumerFrameShow));
221 }
222
224 if (pullConsumer && !pullConsumer->is_stopped()) {
225 pullConsumer->stop();
226 }
227
228 if (pushConsumer && !pushConsumer->is_stopped()) {
229 pushConsumer->stop();
230 }
231
232 pullConsumer.reset();
233 pushConsumer.reset();
235 }
236
238 return m_self->activeCanvas();
239 }
240
246
252
253 bool dropFrames() const {
254 return m_self->dropFrames();
255 }
256
257private:
259
260public:
261 QScopedPointer<Mlt::Repository> repository;
262 QScopedPointer<Mlt::Profile> profile;
263
264 //MLT PUSH CONSUMER
265 QScopedPointer<Mlt::Consumer> pullConsumer;
266 QScopedPointer<Mlt::Event> pullConsumerConnection;
267
268 //MLT PULL CONSUMER
269 QScopedPointer<Mlt::PushConsumer> pushConsumer;
270
271 // Map of handles to Mlt producers..
272 QMap<KisCanvas2*, QSharedPointer<Mlt::Producer>> canvasProducers;
273
274 QScopedPointer<KisSignalCompressorWithParam<int>> sigPushAudioCompressor;
275 QScopedPointer<KisSignalCompressorWithParam<double>> sigSetPlaybackSpeed;
276
278 bool mute;
279
281 FrameRenderingStats frameStats;
282};
283
284//=====
285
292public:
293 explicit StopAndResume(KisPlaybackEngineMLT::Private* p_d, bool requireFullRestart = false)
294 : m_d(p_d)
295 {
296 KIS_ASSERT(p_d);
297
298
299 {
303 }
304
305 m_d->pushConsumer->stop();
306 m_d->pushConsumer->purge();
307 m_d->pullConsumer->stop();
308 m_d->pullConsumer->purge();
309 m_d->pullConsumer->disconnect_all_producers();
310
311 if (requireFullRestart) {
313 }
314 }
315
318 if (!m_d->pushConsumer || !m_d->pullConsumer) {
320 }
321
322 if (m_d->activeCanvas()) {
324 KIS_SAFE_ASSERT_RECOVER_RETURN(animationState);
325
326 {
330
332 }
333
334 m_d->frameStats.reset();
335
336 {
344 m_d->activeProducer()->set("start_frame", animInterface->activePlaybackRange().start());
345 m_d->activeProducer()->set("end_frame", animInterface->activePlaybackRange().end());
346 m_d->activeProducer()->set("speed", m_d->playbackSpeed);
347 const int shouldLimit = m_d->activePlaybackMode() == PLAYBACK_PUSH ? 0 : 1;
348 m_d->activeProducer()->set("limit_enabled", shouldLimit);
349 }
350
352 m_d->pushConsumer->set("volume", m_d->mute ? 0.0 : animationState->currentVolume());
353 m_d->pushConsumer->start();
354 } else {
355 m_d->pullConsumer->connect_producer(*m_d->activeProducer());
356 m_d->pullConsumer->set("volume", m_d->mute ? 0.0 : animationState->currentVolume());
357 m_d->pullConsumer->set("real_time", m_d->dropFrames() ? 1 : 0);
358 m_d->pullConsumer->start();
359 }
360 }
361 }
362
363private:
365};
366
367//=====
368
370 : KisPlaybackEngine(parent)
371 , m_d( new Private(this))
372{
374}
375
379
380void KisPlaybackEngineMLT::seek(int frameIndex, SeekOptionFlags flags)
381{
382 KIS_ASSERT(activeCanvas() && activeCanvas()->animationState());
384
385 if (m_d->activePlaybackMode() == PLAYBACK_PUSH) {
386 m_d->canvasProducers[activeCanvas()]->seek(frameIndex);
387
388 if (flags & SEEK_PUSH_AUDIO) {
389
390 m_d->sigPushAudioCompressor->start(frameIndex);
391 }
392
393 animationState->showFrame(frameIndex, (flags & SEEK_FINALIZE) > 0);
394 }
395}
396
397void KisPlaybackEngineMLT::setupProducer(boost::optional<QFileInfo> file)
398{
399 if (!m_d->canvasProducers.contains(activeCanvas())) {
400 connect(activeCanvas(), SIGNAL(destroyed(QObject*)), this, SLOT(canvasDestroyed(QObject*)));
401 }
402
403 //First, assign to "count" producer.
404 m_d->canvasProducers[activeCanvas()] = QSharedPointer<Mlt::Producer>(new Mlt::Producer(*m_d->profile, "krita_play_chunk", "count"));
405
406 //If we have a file and the file has a valid producer, use that. Otherwise, stick to our "default" producer.
407 if (file.has_value()) {
409
410#ifdef Q_OS_ANDROID
411 new Mlt::Producer(*m_d->profile,
412 "krita_play_chunk",
413 KisAndroidFileProxy::getFileFromContentUri(file->absoluteFilePath()).toUtf8().data()));
414#else
415 new Mlt::Producer(*m_d->profile, "krita_play_chunk", file->absoluteFilePath().toUtf8().data()));
416#endif
417 if (producer->is_valid()) {
418 m_d->canvasProducers[activeCanvas()] = producer;
419 } else {
420 // SANITY CHECK: Check that the MLT plugins and resources are where the program expects them to be.
421 // HINT -- Check krita/main.cc's mlt environment variable setup for appimage.
422 KIS_SAFE_ASSERT_RECOVER_NOOP(qEnvironmentVariableIsSet("MLT_REPOSITORY"));
423 KIS_SAFE_ASSERT_RECOVER_NOOP(qEnvironmentVariableIsSet("MLT_PROFILES_PATH"));
424 KIS_SAFE_ASSERT_RECOVER_NOOP(qEnvironmentVariableIsSet("MLT_PRESETS_PATH"));
425 qDebug() << "Warning: Invalid MLT producer for file: " << ppVar(file->absoluteFilePath()) << " Falling back to audio-less playback.";
426 }
427 }
428
430 QSharedPointer<Mlt::Producer> producer = m_d->canvasProducers[activeCanvas()];
431 KIS_ASSERT(producer->is_valid());
432 KIS_ASSERT(animInterface);
433
434 producer->set("start_frame", animInterface->documentPlaybackRange().start());
435 producer->set("end_frame", animInterface->documentPlaybackRange().end());
436 producer->set("limit_enabled", false);
437 producer->set("speed", m_d->playbackSpeed);
438}
439
441{
442 KisCanvas2* canvas = dynamic_cast<KisCanvas2*>(p_canvas);
443
444 if (activeCanvas() == canvas) {
445 return;
446 }
447
448 if (activeCanvas()) {
450
451 // Disconnect old canvas, prepare for new one..
452 if (animationState) {
453 this->disconnect(animationState);
454 animationState->disconnect(this);
455 }
456
457 // Disconnect old image, prepare for new one..
458 auto image = activeCanvas()->image();
459 if (image && image->animationInterface()) {
460 this->disconnect(image->animationInterface());
461 image->animationInterface()->disconnect(this);
462 }
463 }
464
465 StopAndResume stopResume(m_d.data(), true);
466
468
469 // Connect new canvas..
470 if (activeCanvas()) {
472 KIS_SAFE_ASSERT_RECOVER_RETURN(animationState);
473
474 connect(animationState, &KisCanvasAnimationState::sigPlaybackStateChanged, this, [this](PlaybackState state){
475 Q_UNUSED(state); // We don't need the state yet -- we just want to stop and resume playback according to new state info.
476 StopAndResume callbackStopResume(m_d.data());
477 });
478
479 connect(animationState, &KisCanvasAnimationState::sigPlaybackMediaChanged, this, [this](){
481 if (animationState) {
482 setupProducer(animationState->mediaInfo());
483 }
484 });
485
486 connect(animationState, &KisCanvasAnimationState::sigPlaybackSpeedChanged, this, [this](qreal value){
487 m_d->sigSetPlaybackSpeed->start(value);
488 });
489 m_d->playbackSpeed = animationState->playbackSpeed();
490
492
493 auto image = activeCanvas()->image();
495
496 // Connect new image..
497 connect(image->animationInterface(), &KisImageAnimationInterface::sigFramerateChanged, this, [this](){
498 StopAndResume callbackStopResume(m_d.data());
499 m_d->profile->set_frame_rate(activeCanvas()->image()->animationInterface()->framerate(), 1);
500
509 KisCanvasAnimationState* animationState = activeCanvas()->animationState();
510 if (animationState) {
511 setupProducer(animationState->mediaInfo());
512 }
513 });
514
515 // cold init the framerate
516 m_d->profile->set_frame_rate(activeCanvas()->image()->animationInterface()->framerate(), 1);
517
518 connect(image->animationInterface(), &KisImageAnimationInterface::sigPlaybackRangeChanged, this, [this](){
519 QSharedPointer<Mlt::Producer> producer = m_d->canvasProducers[activeCanvas()];
520 auto image = activeCanvas()->image();
521 KIS_SAFE_ASSERT_RECOVER_RETURN(image);
522 producer->set("start_frame", image->animationInterface()->activePlaybackRange().start());
523 producer->set("end_frame", image->animationInterface()->activePlaybackRange().end());
524 });
525
526 setupProducer(animationState->mediaInfo());
527 }
528
529}
530
534
536{
537 KIS_SAFE_ASSERT_RECOVER_RETURN(m_d->activeCanvas() != canvas);
538
543 for (auto it = m_d->canvasProducers.begin(); it != m_d->canvasProducers.end(); ++it) {
544 if (it.key() == canvas) {
545 m_d->canvasProducers.erase(it);
546 break;
547 }
548 }
549}
550
552{
553 if (activeCanvas() && activeCanvas()->animationState() &&
554 m_d->activePlaybackMode() == PLAYBACK_PULL ) {
555
556 if (m_d->frameStats.lastRenderedFrame < 0) {
557 m_d->frameStats.timeSinceLastFrame.start();
558 } else {
559 const int droppedFrames = qMax(0, frame - m_d->frameStats.lastRenderedFrame - 1);
560 m_d->frameStats.averageFrameDuration(m_d->frameStats.timeSinceLastFrame.restart());
561 m_d->frameStats.droppedFramesCount(droppedFrames);
562 }
563 m_d->frameStats.lastRenderedFrame = frame;
564
566 }
567
568 {
569 QMutexLocker l(&m_d->frameWaitingInterface.renderingControlMutex);
570 m_d->frameWaitingInterface.waitingForFrame = false;
571 m_d->frameWaitingInterface.renderingWaitCondition.wakeAll();
572 }
573}
574
576{
577 StopAndResume stopResume(m_d.data(), false);
578 m_d->playbackSpeed = speed;
579}
580
581void KisPlaybackEngineMLT::setAudioVolume(qreal volumeNormalized)
582{
583 if (m_d->mute) {
584 m_d->pullConsumer->set("volume", 0.0);
585 m_d->pushConsumer->set("volume", 0.0);
586 } else {
587 m_d->pullConsumer->set("volume", volumeNormalized);
588 m_d->pushConsumer->set("volume", volumeNormalized);
589 }
590}
591
596
598{
599 // restart playback if it was active
600 StopAndResume r(m_d.data(), false);
601
603}
604
606{
609
610 qreal currentVolume = animationState->currentVolume();
611 m_d->mute = val;
612 setAudioVolume(currentVolume);
613}
614
616{
617 return m_d->mute;
618}
619
621{
623
624 if (activeCanvas() && activeCanvas()->animationState() &&
625 m_d->activePlaybackMode() == PLAYBACK_PULL ) {
626
627 const int droppedFrames = m_d->frameStats.droppedFramesCount.rollingSum();
628 const int totalFrames =
629 m_d->frameStats.droppedFramesCount.rollingCount() +
630 droppedFrames;
631
632 stats.droppedFramesPortion = qreal(droppedFrames) / totalFrames;
633 stats.expectedFps = qreal(activeCanvas()->image()->animationInterface()->framerate()) * m_d->playbackSpeed;
634
635 const qreal avgTimePerFrame = m_d->frameStats.averageFrameDuration.rollingMeanSafe();
636 stats.realFps = !qFuzzyIsNull(avgTimePerFrame) ? 1000.0 / avgTimePerFrame : 0.0;
637
638 }
639
640 return stats;
641}
642
643
644
float value(const T *src, size_t ch)
void registerKritaMLTProducer(Mlt::Repository *repository)
static void mltOnConsumerFrameShow(mlt_consumer c, void *p_self, mlt_frame p_frame)
const float SCRUB_AUDIO_SECONDS
@ PLAYBACK_PUSH
@ PLAYBACK_PULL
@ SEEK_PUSH_AUDIO
@ SEEK_FINALIZE
static QString getFileFromContentUri(QString contentUri)
KisCanvasAnimationState * animationState() const
KisImageWSP image() const
The KisCanvasAnimationState class stores all of the canvas-specific animation state.
void sigAudioLevelChanged(qreal value)
void sigPlaybackStateChanged(PlaybackState state)
boost::optional< QFileInfo > mediaInfo()
Get the media file info associated with this canvas, if available.
void showFrame(int frame, bool finalize=false)
void sigPlaybackSpeedChanged(qreal value)
const KisTimeSpan & activePlaybackRange() const
activePlaybackRange
const KisTimeSpan & documentPlaybackRange() const
documentPlaybackRange
KisImageAnimationInterface * animationInterface() const
The KisPlaybackEngineMLT class is an implementation of KisPlaybackEngine that uses MLT (Media Lovin' ...
void sigChangeActiveCanvasFrame(int p_frame)
void setCanvas(KoCanvasBase *canvas) override
PlaybackStats playbackStatistics() const override
QScopedPointer< Private > m_d
void setMute(bool val) override
void throttledShowFrame(const int frame)
throttledShowFrame
void throttledSetSpeed(const double speed)
throttledSetSpeed
void canvasDestroyed(QObject *canvas)
void seek(int frameIndex, SeekOptionFlags flags=SEEK_FINALIZE|SEEK_PUSH_AUDIO) override
void setupProducer(boost::optional< QFileInfo > file)
Sets up an MLT::Producer object in response to audio being added to a Krita document or when canvas c...
void setDropFramesMode(bool value) override
void setAudioVolume(qreal volumeNormalized)
setAudioVolume
FrameWaitingInterface * frameWaitingInterface()
KisPlaybackEngineMLT(QObject *parent=nullptr)
Krita's base animation playback engine for producing image frame changes and associated audio.
virtual void setDropFramesMode(bool value)
class KisCanvas2 * activeCanvas() const
virtual void setCanvas(KoCanvasBase *p_canvas) override
A simple wrapper class that hides boost includes from QtCreator preventing it from crashing when one ...
A simple wrapper class that hides boost includes from QtCreator preventing it from crashing when one ...
int start() const
int end() const
static bool qFuzzyIsNull(half h)
#define KIS_ASSERT_RECOVER_RETURN_VALUE(cond, val)
Definition kis_assert.h:85
#define KIS_SAFE_ASSERT_RECOVER_RETURN(cond)
Definition kis_assert.h:128
#define KIS_SAFE_ASSERT_RECOVER_NOOP(cond)
Definition kis_assert.h:130
#define KIS_ASSERT(cond)
Definition kis_assert.h:33
#define ppVar(var)
Definition kis_debug.h:155
typedef void(QOPENGLF_APIENTRYP PFNGLINVALIDATEBUFFERDATAPROC)(GLuint buffer)
QScopedPointer< Mlt::Event > pullConsumerConnection
QScopedPointer< KisSignalCompressorWithParam< double > > sigSetPlaybackSpeed
QScopedPointer< KisSignalCompressorWithParam< int > > sigPushAudioCompressor
FrameWaitingInterface frameWaitingInterface
QMap< KisCanvas2 *, QSharedPointer< Mlt::Producer > > canvasProducers
QScopedPointer< Mlt::PushConsumer > pushConsumer
QScopedPointer< Mlt::Profile > profile
Private(KisPlaybackEngineMLT *p_self)
QScopedPointer< Mlt::Consumer > pullConsumer
QSharedPointer< Mlt::Producer > activeProducer()
QScopedPointer< Mlt::Repository > repository
The StopAndResumeConsumer struct is used to encapsulate optional stop-and-then-resume behavior of a c...
StopAndResume(KisPlaybackEngineMLT::Private *p_d, bool requireFullRestart=false)