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 // Initialize MLT...
156 repository.reset(Mlt::Factory::init());
157
158#ifdef MLT_LOG_REDIRECTION
159 mlt_log_set_level(MLT_LOG_VERBOSE);
160 mlt_log_set_callback(&qt_redirection_callback);
161#endif /* MLT_LOG_REDIRECTION */
162
163 // Register our backend plugin
165
166 profile.reset(new Mlt::Profile());
167 profile->set_frame_rate(24, 1);
168
169 {
170 std::function<void (int)> callback(std::bind(&Private::pushAudio, this, std::placeholders::_1));
173 );
174 }
175
176 {
177 std::function<void (const double)> callback(std::bind(&KisPlaybackEngineMLT::throttledSetSpeed, m_self, std::placeholders::_1));
180 );
181 }
182
184 }
185
188 repository.reset();
189 Mlt::Factory::close();
190 }
191
192 void pushAudio(int frame) {
193
194 if (pushConsumer->is_stopped() || !m_self->activeCanvas()) {
195 return;
196 }
197
200 const int SCRUB_AUDIO_WINDOW = qMax(1, qRound(profile->frame_rate_num() * SCRUB_AUDIO_SECONDS));
201 activeProducer->seek(frame);
202 for (int i = 0; i < SCRUB_AUDIO_WINDOW; i++ ) {
203 Mlt::Frame* f = activeProducer->get_frame();
204 pushConsumer->push(*f);
205 delete f;
206 }
207
208 // It turns out that get_frame actually seeks to the frame too,
209 // Not having this last seek will cause unexpected "jumps" at
210 // the beginning of playback...
211 activeProducer->seek(frame);
212 }
213 }
214
216 pushConsumer.reset(new Mlt::PushConsumer(*profile, "sdl2_audio"));
217 pullConsumer.reset(new Mlt::Consumer(*profile, "sdl2_audio"));
218 pullConsumerConnection.reset(pullConsumer->listen("consumer-frame-show", m_self, (mlt_listener)mltOnConsumerFrameShow));
219 }
220
222 if (pullConsumer && !pullConsumer->is_stopped()) {
223 pullConsumer->stop();
224 }
225
226 if (pushConsumer && !pushConsumer->is_stopped()) {
227 pushConsumer->stop();
228 }
229
230 pullConsumer.reset();
231 pushConsumer.reset();
233 }
234
236 return m_self->activeCanvas();
237 }
238
244
250
251 bool dropFrames() const {
252 return m_self->dropFrames();
253 }
254
255private:
257
258public:
259 QScopedPointer<Mlt::Repository> repository;
260 QScopedPointer<Mlt::Profile> profile;
261
262 //MLT PUSH CONSUMER
263 QScopedPointer<Mlt::Consumer> pullConsumer;
264 QScopedPointer<Mlt::Event> pullConsumerConnection;
265
266 //MLT PULL CONSUMER
267 QScopedPointer<Mlt::PushConsumer> pushConsumer;
268
269 // Map of handles to Mlt producers..
270 QMap<KisCanvas2*, QSharedPointer<Mlt::Producer>> canvasProducers;
271
272 QScopedPointer<KisSignalCompressorWithParam<int>> sigPushAudioCompressor;
273 QScopedPointer<KisSignalCompressorWithParam<double>> sigSetPlaybackSpeed;
274
276 bool mute;
277
279 FrameRenderingStats frameStats;
280};
281
282//=====
283
290public:
291 explicit StopAndResume(KisPlaybackEngineMLT::Private* p_d, bool requireFullRestart = false)
292 : m_d(p_d)
293 {
294 KIS_ASSERT(p_d);
295
296
297 {
301 }
302
303 m_d->pushConsumer->stop();
304 m_d->pushConsumer->purge();
305 m_d->pullConsumer->stop();
306 m_d->pullConsumer->purge();
307 m_d->pullConsumer->disconnect_all_producers();
308
309 if (requireFullRestart) {
311 }
312 }
313
316 if (!m_d->pushConsumer || !m_d->pullConsumer) {
318 }
319
320 if (m_d->activeCanvas()) {
322 KIS_SAFE_ASSERT_RECOVER_RETURN(animationState);
323
324 {
328
330 }
331
332 m_d->frameStats.reset();
333
334 {
342 m_d->activeProducer()->set("start_frame", animInterface->activePlaybackRange().start());
343 m_d->activeProducer()->set("end_frame", animInterface->activePlaybackRange().end());
344 m_d->activeProducer()->set("speed", m_d->playbackSpeed);
345 const int shouldLimit = m_d->activePlaybackMode() == PLAYBACK_PUSH ? 0 : 1;
346 m_d->activeProducer()->set("limit_enabled", shouldLimit);
347 }
348
350 m_d->pushConsumer->set("volume", m_d->mute ? 0.0 : animationState->currentVolume());
351 m_d->pushConsumer->start();
352 } else {
353 m_d->pullConsumer->connect_producer(*m_d->activeProducer());
354 m_d->pullConsumer->set("volume", m_d->mute ? 0.0 : animationState->currentVolume());
355 m_d->pullConsumer->set("real_time", m_d->dropFrames() ? 1 : 0);
356 m_d->pullConsumer->start();
357 }
358 }
359 }
360
361private:
363};
364
365//=====
366
368 : KisPlaybackEngine(parent)
369 , m_d( new Private(this))
370{
372}
373
377
378void KisPlaybackEngineMLT::seek(int frameIndex, SeekOptionFlags flags)
379{
380 KIS_ASSERT(activeCanvas() && activeCanvas()->animationState());
382
383 if (m_d->activePlaybackMode() == PLAYBACK_PUSH) {
384 m_d->canvasProducers[activeCanvas()]->seek(frameIndex);
385
386 if (flags & SEEK_PUSH_AUDIO) {
387
388 m_d->sigPushAudioCompressor->start(frameIndex);
389 }
390
391 animationState->showFrame(frameIndex, (flags & SEEK_FINALIZE) > 0);
392 }
393}
394
395void KisPlaybackEngineMLT::setupProducer(boost::optional<QFileInfo> file)
396{
397 if (!m_d->canvasProducers.contains(activeCanvas())) {
398 connect(activeCanvas(), SIGNAL(destroyed(QObject*)), this, SLOT(canvasDestroyed(QObject*)));
399 }
400
401 //First, assign to "count" producer.
402 m_d->canvasProducers[activeCanvas()] = QSharedPointer<Mlt::Producer>(new Mlt::Producer(*m_d->profile, "krita_play_chunk", "count"));
403
404 //If we have a file and the file has a valid producer, use that. Otherwise, stick to our "default" producer.
405 if (file.has_value()) {
407
408#ifdef Q_OS_ANDROID
409 new Mlt::Producer(*m_d->profile,
410 "krita_play_chunk",
411 KisAndroidFileProxy::getFileFromContentUri(file->absoluteFilePath()).toUtf8().data()));
412#else
413 new Mlt::Producer(*m_d->profile, "krita_play_chunk", file->absoluteFilePath().toUtf8().data()));
414#endif
415 if (producer->is_valid()) {
416 m_d->canvasProducers[activeCanvas()] = producer;
417 } else {
418 // SANITY CHECK: Check that the MLT plugins and resources are where the program expects them to be.
419 // HINT -- Check krita/main.cc's mlt environment variable setup for appimage.
420 KIS_SAFE_ASSERT_RECOVER_NOOP(qEnvironmentVariableIsSet("MLT_REPOSITORY"));
421 KIS_SAFE_ASSERT_RECOVER_NOOP(qEnvironmentVariableIsSet("MLT_PROFILES_PATH"));
422 KIS_SAFE_ASSERT_RECOVER_NOOP(qEnvironmentVariableIsSet("MLT_PRESETS_PATH"));
423 qDebug() << "Warning: Invalid MLT producer for file: " << ppVar(file->absoluteFilePath()) << " Falling back to audio-less playback.";
424 }
425 }
426
428 QSharedPointer<Mlt::Producer> producer = m_d->canvasProducers[activeCanvas()];
429 KIS_ASSERT(producer->is_valid());
430 KIS_ASSERT(animInterface);
431
432 producer->set("start_frame", animInterface->documentPlaybackRange().start());
433 producer->set("end_frame", animInterface->documentPlaybackRange().end());
434 producer->set("limit_enabled", false);
435 producer->set("speed", m_d->playbackSpeed);
436}
437
439{
440 KisCanvas2* canvas = dynamic_cast<KisCanvas2*>(p_canvas);
441
442 if (activeCanvas() == canvas) {
443 return;
444 }
445
446 if (activeCanvas()) {
448
449 // Disconnect old canvas, prepare for new one..
450 if (animationState) {
451 this->disconnect(animationState);
452 animationState->disconnect(this);
453 }
454
455 // Disconnect old image, prepare for new one..
456 auto image = activeCanvas()->image();
457 if (image && image->animationInterface()) {
458 this->disconnect(image->animationInterface());
459 image->animationInterface()->disconnect(this);
460 }
461 }
462
463 StopAndResume stopResume(m_d.data(), true);
464
466
467 // Connect new canvas..
468 if (activeCanvas()) {
470 KIS_SAFE_ASSERT_RECOVER_RETURN(animationState);
471
472 connect(animationState, &KisCanvasAnimationState::sigPlaybackStateChanged, this, [this](PlaybackState state){
473 Q_UNUSED(state); // We don't need the state yet -- we just want to stop and resume playback according to new state info.
474 StopAndResume callbackStopResume(m_d.data());
475 });
476
477 connect(animationState, &KisCanvasAnimationState::sigPlaybackMediaChanged, this, [this](){
479 if (animationState) {
480 setupProducer(animationState->mediaInfo());
481 }
482 });
483
484 connect(animationState, &KisCanvasAnimationState::sigPlaybackSpeedChanged, this, [this](qreal value){
485 m_d->sigSetPlaybackSpeed->start(value);
486 });
487 m_d->playbackSpeed = animationState->playbackSpeed();
488
490
491 auto image = activeCanvas()->image();
493
494 // Connect new image..
495 connect(image->animationInterface(), &KisImageAnimationInterface::sigFramerateChanged, this, [this](){
496 StopAndResume callbackStopResume(m_d.data());
497 m_d->profile->set_frame_rate(activeCanvas()->image()->animationInterface()->framerate(), 1);
498
507 KisCanvasAnimationState* animationState = activeCanvas()->animationState();
508 if (animationState) {
509 setupProducer(animationState->mediaInfo());
510 }
511 });
512
513 // cold init the framerate
514 m_d->profile->set_frame_rate(activeCanvas()->image()->animationInterface()->framerate(), 1);
515
516 connect(image->animationInterface(), &KisImageAnimationInterface::sigPlaybackRangeChanged, this, [this](){
517 QSharedPointer<Mlt::Producer> producer = m_d->canvasProducers[activeCanvas()];
518 auto image = activeCanvas()->image();
519 KIS_SAFE_ASSERT_RECOVER_RETURN(image);
520 producer->set("start_frame", image->animationInterface()->activePlaybackRange().start());
521 producer->set("end_frame", image->animationInterface()->activePlaybackRange().end());
522 });
523
524 setupProducer(animationState->mediaInfo());
525 }
526
527}
528
532
534{
535 KIS_SAFE_ASSERT_RECOVER_RETURN(m_d->activeCanvas() != canvas);
536
541 for (auto it = m_d->canvasProducers.begin(); it != m_d->canvasProducers.end(); ++it) {
542 if (it.key() == canvas) {
543 m_d->canvasProducers.erase(it);
544 break;
545 }
546 }
547}
548
550{
551 if (activeCanvas() && activeCanvas()->animationState() &&
552 m_d->activePlaybackMode() == PLAYBACK_PULL ) {
553
554 if (m_d->frameStats.lastRenderedFrame < 0) {
555 m_d->frameStats.timeSinceLastFrame.start();
556 } else {
557 const int droppedFrames = qMax(0, frame - m_d->frameStats.lastRenderedFrame - 1);
558 m_d->frameStats.averageFrameDuration(m_d->frameStats.timeSinceLastFrame.restart());
559 m_d->frameStats.droppedFramesCount(droppedFrames);
560 }
561 m_d->frameStats.lastRenderedFrame = frame;
562
564 }
565
566 {
567 QMutexLocker l(&m_d->frameWaitingInterface.renderingControlMutex);
568 m_d->frameWaitingInterface.waitingForFrame = false;
569 m_d->frameWaitingInterface.renderingWaitCondition.wakeAll();
570 }
571}
572
574{
575 StopAndResume stopResume(m_d.data(), false);
576 m_d->playbackSpeed = speed;
577}
578
579void KisPlaybackEngineMLT::setAudioVolume(qreal volumeNormalized)
580{
581 if (m_d->mute) {
582 m_d->pullConsumer->set("volume", 0.0);
583 m_d->pushConsumer->set("volume", 0.0);
584 } else {
585 m_d->pullConsumer->set("volume", volumeNormalized);
586 m_d->pushConsumer->set("volume", volumeNormalized);
587 }
588}
589
594
596{
597 // restart playback if it was active
598 StopAndResume r(m_d.data(), false);
599
601}
602
604{
607
608 qreal currentVolume = animationState->currentVolume();
609 m_d->mute = val;
610 setAudioVolume(currentVolume);
611}
612
614{
615 return m_d->mute;
616}
617
619{
621
622 if (activeCanvas() && activeCanvas()->animationState() &&
623 m_d->activePlaybackMode() == PLAYBACK_PULL ) {
624
625 const int droppedFrames = m_d->frameStats.droppedFramesCount.rollingSum();
626 const int totalFrames =
627 m_d->frameStats.droppedFramesCount.rollingCount() +
628 droppedFrames;
629
630 stats.droppedFramesPortion = qreal(droppedFrames) / totalFrames;
631 stats.expectedFps = qreal(activeCanvas()->image()->animationInterface()->framerate()) * m_d->playbackSpeed;
632
633 const qreal avgTimePerFrame = m_d->frameStats.averageFrameDuration.rollingMeanSafe();
634 stats.realFps = !qFuzzyIsNull(avgTimePerFrame) ? 1000.0 / avgTimePerFrame : 0.0;
635
636 }
637
638 return stats;
639}
640
641
642
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
connect(this, SIGNAL(optionsChanged()), this, SLOT(saveOptions()))
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)