Krita Source Code Documentation
Loading...
Searching...
No Matches
recorder_export.cpp
Go to the documentation of this file.
1/*
2 * SPDX-FileCopyrightText: 2020 Dmitrii Utkin <loentar@gmail.com>
3 *
4 * SPDX-License-Identifier: LGPL-2.1-only
5 */
6
7#include "recorder_export.h"
9#include "ui_recorder_export.h"
13
14#include <klocalizedstring.h>
15#include <kis_icon_utils.h>
16#include "kis_config.h"
17#include "KoFileDialog.h"
18#include "KisMimeDatabase.h"
19
20#include <QAction>
21#include <QDesktopServices>
22#include <QDir>
23#include <QDirIterator>
24#include <QUrl>
25#include <QDebug>
26#include <QCloseEvent>
27#include <QMessageBox>
28#include <QJsonObject>
29#include <QJsonArray>
30#include <QImageReader>
31#include <QElapsedTimer>
32
33#include "kis_debug.h"
34
35#ifdef Q_OS_ANDROID
38#include <KisAndroidUtils.h>
39#else
42#endif
43
44
45namespace
46{
47enum ExportPageIndex
48{
49 PageSettings = 0,
50 PageProgress = 1,
51 PageDone = 2
52};
53
54#ifdef Q_OS_ANDROID
55using Exporter = KisMediaEncoderWrapper;
56#else
57using Exporter = KisFFMpegWrapper;
58#endif
59}
60
61
63{
64public:
65 static constexpr int DIMENSION_LIMIT = 1920;
66
68 QScopedPointer<Ui::RecorderExport> ui;
70
71 QScopedPointer<RecorderExportPreprocessor> preprocessor;
72 QScopedPointer<Exporter> exporter;
74
75 QElapsedTimer elapsedTimer;
76
79
81 : q(q_ptr)
82 , ui(new Ui::RecorderExport)
83 , settings(q_ptr->settings)
84 , preprocessor(new RecorderExportPreprocessor(settings->inputDirectory, settings->format))
85 {
86 }
87
88#ifndef Q_OS_ANDROID
90 {
91 const QJsonObject ffmpegJson = KisFFMpegWrapper::findFFMpeg(settings->ffmpegPath);
92 const bool success = ffmpegJson["enabled"].toBool();
93 const QIcon &icon = KisIconUtils::loadIcon(success ? "dialog-ok" : "window-close");
94 const QList<QAction *> &actions = ui->editFfmpegPath->actions();
95 QAction *action;
96
97 if (!actions.isEmpty()) {
98 action = actions.first();
99 action->setIcon(icon);
100 } else {
101 action = ui->editFfmpegPath->addAction(icon, QLineEdit::TrailingPosition);
102 }
103 if (success) {
104 const QJsonArray h264Encoders = ffmpegJson["codecs"].toObject()["h264"].toObject()["encoders"].toArray();
105 settings->ffmpegPath = ffmpegJson["path"].toString();
106 settings->h264Encoder = h264Encoders.contains("libopenh264") ? "libopenh264" : "libx264";
107 ui->editFfmpegPath->setText(settings->ffmpegPath);
108 action->setToolTip("Version: "+ffmpegJson["version"].toString()
109 +(ffmpegJson["codecs"].toObject()["h264"].toObject()["encoding"].toBool() ? "":" (MP4/MKV UNSUPPORTED)")
110 );
111 } else {
112 ui->editFfmpegPath->setText(i18nc("This text is displayed instead of path to external tool in case of external tool is not found", "[NOT FOUND]"));
113 action->setToolTip(i18n("FFmpeg executable location couldn't be detected, please install it or select its location manually"));
114 }
115 ui->buttonBox->button(QDialogButtonBox::Save)->setEnabled(success);
116 }
117#endif
118
120 {
121 int indexToSelect;
122 {
123 QSignalBlocker blocker(ui->comboProfile);
124 ui->comboProfile->clear();
125#ifdef Q_OS_ANDROID
127 int count = formats.size();
128 if (count == 0) {
129 return;
130 }
131
132 indexToSelect = -1;
133 for (int i = 0, count = formats.size(); i < count; ++i) {
134 QString key = formats[i]->key();
135 ui->comboProfile->addItem(formats[i]->title(), QVariant(key));
136 if (key == settings->selectedFormat) {
137 indexToSelect = i;
138 }
139 }
140
141 if (indexToSelect == -1) {
142 indexToSelect = 0;
143 settings->selectedFormat = formats[0]->key();
144 }
145#else
146 for (const RecorderProfile &profile : settings->profiles) {
147 ui->comboProfile->addItem(profile.name);
148 }
149 indexToSelect = settings->profileIndex;
150#endif
151 }
152 ui->comboProfile->setCurrentIndex(indexToSelect);
153 }
154
156 {
158 QDir::Name, QDir::Files | QDir::NoDotAndDotDot);
159 QStringList frames = dir.entryList(); // dir.count() calls entryList().count() internally
160 settings->framesCount = frames.count();
161 if (settings->framesCount != 0) {
162 const QString &fileName = settings->inputDirectory % QDir::separator() % frames.last();
163 settings->imageSize = QImageReader(fileName).size();
164 settings->imageSize.rwidth() &= ~1;
165 settings->imageSize.rheight() &= ~1;
166 }
167#ifdef Q_OS_ANDROID
168 // QDir::entryList is mind-bogglingly slow on Android, so we only load
169 // this once and cache the result. Not like these are supposed to change
170 // while this modal dialog is up anyway.
171 settings->inputFilePaths.clear();
172 settings->inputFilePaths.reserve(settings->framesCount);
173 for (const QString &frame : frames) {
174 settings->inputFilePaths.append(dir.filePath(frame));
175 }
176#endif
177 }
178
179#ifndef Q_OS_ANDROID
181 {
182 if (settings->videoDirectory.isEmpty())
184
186 % QDir::separator()
188 % "."
189 % settings->profiles[settings->profileIndex].extension;
190 QSignalBlocker blocker(ui->editVideoFilePath);
191 ui->editVideoFilePath->setText(settings->videoFilePath);
192 }
193#endif
194
195 void updateRatio(bool widthToHeight)
196 {
197 const float ratio = static_cast<float>(settings->imageSize.width()) / static_cast<float>(settings->imageSize.height());
198 if (widthToHeight) {
199 settings->size.setHeight(static_cast<int>(settings->size.width() / ratio));
200 } else {
201 settings->size.setWidth(static_cast<int>(settings->size.height() * ratio));
202 }
203 // make width and height even
204 settings->size.rwidth() &= ~1;
205 settings->size.rheight() &= ~1;
206 QSignalBlocker blockerWidth(ui->spinScaleHeight);
207 QSignalBlocker blockerHeight(ui->spinScaleWidth);
208 ui->spinScaleHeight->setValue(settings->size.height());
209 ui->spinScaleWidth->setValue(settings->size.width());
210 }
211
212 void updateFps(RecorderExportConfig &config, bool takeFromInputFps = false)
213 {
214 if (!settings->lockFps)
215 return;
216
217 if (takeFromInputFps) {
219 config.setFps(settings->fps);
220 ui->spinFps->setValue(settings->fps);
221 } else {
224 ui->spinInputFps->setValue(settings->inputFps);
225 }
227 }
228
230 {
231 if (!exporter)
232 return true;
233
234 if (QMessageBox::question(q, q->windowTitle(), i18n("Abort encoding the timelapse video?"))
235 == QMessageBox::Yes) {
237 return true;
238 }
239
240 return false;
241 }
242
243#ifndef Q_OS_ANDROID
244 QStringList splitCommand(const QString &command)
245 {
246 QStringList args;
247 QString tmp;
248 int quoteCount = 0;
249 bool inQuote = false;
250
251 // handle quoting. tokens can be surrounded by double quotes
252 // "hello world". three consecutive double quotes represent
253 // the quote character itself.
254 for (int i = 0; i < command.size(); ++i) {
255 if (command.at(i) == QLatin1Char('"')) {
256 ++quoteCount;
257 if (quoteCount == 3) {
258 // third consecutive quote
259 quoteCount = 0;
260 tmp += command.at(i);
261 }
262 continue;
263 }
264 if (quoteCount) {
265 if (quoteCount == 1)
266 inQuote = !inQuote;
267 quoteCount = 0;
268 }
269 if (!inQuote && command.at(i).isSpace()) {
270 if (!tmp.isEmpty()) {
271 args += tmp;
272 tmp.clear();
273 }
274 } else {
275 tmp += command.at(i);
276 }
277 }
278 if (!tmp.isEmpty())
279 args += tmp;
280
281 return args;
282 }
283#endif
284
286 {
287 Q_ASSERT(exporter == nullptr);
288
289#ifndef Q_OS_ANDROID
290 // The android export system is robust enough to not need this
292 preprocessor->doPreprocessing();
293
294 // We don't do this again on Android, it's mind-bogglingly slow.
296#endif
297
298 exporter.reset(new Exporter(q));
299 QObject::connect(exporter.data(), SIGNAL(sigStarted()), q, SLOT(onExporterStarted()));
300 QObject::connect(exporter.data(), SIGNAL(sigFinished()), q, SLOT(onExporterFinished()));
301 QObject::connect(exporter.data(), SIGNAL(sigFinishedWithError(QString)), q, SLOT(onExporterFinishedWithError(QString)));
302 QObject::connect(exporter.data(), SIGNAL(sigProgressUpdated(int)), q, SLOT(onExporterProgressUpdated(int)));
303
304#ifdef Q_OS_ANDROID
305 KisMediaEncoderWrapperSettings exporterSettings = {
307 settings->inputFilePaths,
308 QString(),
310 settings->formatPreferences.value(settings->selectedFormat).toMap(),
311 QString(),
314 settings->fps,
317 0,
318 };
319#else
321 KisFFMpegWrapperSettings exporterSettings;
322 exporterSettings.processPath = settings->ffmpegPath;
323 exporterSettings.args = splitCommand(applyVariables(profile.arguments));
324 exporterSettings.outputFile = settings->videoFilePath;
325 exporterSettings.batchMode = true; //TODO: Consider renaming to 'silent' mode, meaning no window for extra window handling...
326#endif
327
328 ui->labelStatus->setText(i18nc("Status for the export of the video record", "Starting exporter..."));
329 ui->buttonCancelExport->setEnabled(false);
330 ui->progressExport->setValue(0);
331 elapsedTimer.start();
332
333 // Do this last, it may immediately emit a failure.
334 exporter->startNonBlocking(exporterSettings);
335 }
336
338 {
339 if (exporter) {
340 exporter->reset();
341 exporter.reset();
342 }
343 }
344
345#ifndef Q_OS_ANDROID
346 QString applyVariables(const QString &templateArguments)
347 {
348 const QSize &outSize = settings->resize ? settings->size : settings->imageSize;
349 const int previewLength = settings->resultPreview ? settings->firstFrameSec : 0;
350 const int resultLength = settings->extendResult ? settings->lastFrameSec : 0;
351 const float transitionLength = settings->resultPreview ? 0.7 : 0;
352 return QString(templateArguments)
353 .replace("$IN_FPS", QString::number(settings->inputFps))
354 .replace("$OUT_FPS", QString::number(settings->fps))
355 .replace("$WIDTH", QString::number(outSize.width()))
356 .replace("$HEIGHT", QString::number(outSize.height()))
357 .replace("$FRAMES", QString::number(settings->framesCount))
358 .replace("$INPUT_DIR", settings->inputDirectory)
359 .replace("$FIRST_FRAME_SEC", QString::number(previewLength))
360 .replace("$TRANSITION_LENGTH", QString::number(transitionLength))
361 .replace("$H264_ENCODER", settings->h264Encoder)
362 .replace("$LAST_FRAME_SEC", QString::number(resultLength))
364 }
365#endif
366
368 {
369 long ms = (settings->framesCount * 1000L / (settings->inputFps ? settings->inputFps : 30));
370
371 if (settings->resultPreview) {
372 ms += (settings->firstFrameSec * 1000L);
373 }
374
375 if (settings->extendResult) {
376 ms += (settings->lastFrameSec * 1000L);
377 }
378
379 ui->labelVideoDuration->setText(formatDuration(ms));
380 }
381
382 QString formatDuration(long durationMs)
383 {
384 QString result;
385 const long ms = (durationMs % 1000) / 10;
386
387 result += QString(".%1").arg(ms, 2, 10, QLatin1Char('0'));
388
389 long duration = durationMs / 1000;
390 const long seconds = duration % 60;
391 result = QString("%1%2").arg(seconds, 2, 10, QLatin1Char('0')).arg(result);
392
393 duration = duration / 60;
394 const long minutes = duration % 60;
395 if (minutes != 0) {
396 result = QString("%1:%2").arg(minutes, 2, 10, QLatin1Char('0')).arg(result);
397
398 duration = duration / 60;
399 if (duration != 0)
400 result = QString("%1:%2").arg(duration, 2, 10, QLatin1Char('0')).arg(result);
401 }
402
403 return result;
404 }
405
407 {
408 // Video with dimensions above 1920 pixels isn't widely supported.
409 // They often fail to encode with mysterious errors, can't be played
410 // back properly and/or are rejected by websites where users attempt to
411 // upload the videos. This caps the dimensions to avoid that trap.
412 settings->resize = true;
413 settings->lockRatio = true;
414 if (settings->imageSize.isEmpty()) {
415 settings->size = QSize(1024, 1024);
416 } else {
417 int iw = settings->imageSize.width();
418 int ih = settings->imageSize.height();
419 if (iw > DIMENSION_LIMIT || ih > DIMENSION_LIMIT) {
420 int ow, oh;
421 if (iw >= ih) {
422 ow = DIMENSION_LIMIT;
423 oh = qRound(qreal(DIMENSION_LIMIT) / qreal(iw) * qreal(ih));
424 } else {
425 ow = qRound(qreal(DIMENSION_LIMIT) / qreal(ih) * qreal(iw));
426 oh = DIMENSION_LIMIT;
427 }
428 ow &= ~1;
429 oh &= ~1;
430 settings->size = QSize(ow, oh);
431 } else {
433 }
434 }
435 }
436
438 {
439 ui->wdgWarnFps->setVisible(settings->fps > 30);
440 QSize size = settings->resize ? settings->size : settings->imageSize;
441 ui->wdgWarnSize->setVisible(size.width() > DIMENSION_LIMIT || size.height() > DIMENSION_LIMIT);
442 }
443
444 QString requestFile(const QString &extension, const QString &defaultDir = QString())
445 {
446 KoFileDialog dialog(q, KoFileDialog::SaveFile, "ExportTimelapse");
447 dialog.setCaption(i18n("Export Timelapse Video As"));
448 if (!defaultDir.isEmpty()) {
449 dialog.setDefaultDir(defaultDir);
450 }
451 dialog.setMimeTypeFilters(QStringList(KisMimeDatabase::mimeTypeForSuffix(extension)));
452 return dialog.filename();
453 }
454
455 static void desktopServicesOpenPath(const QString &path)
456 {
457#ifdef Q_OS_ANDROID
458 // QDesktopServices doesn't clear exceptions
459 KisAndroidUtils::clearJniException(QStringLiteral("before opening ") + path);
460 QDesktopServices::openUrl(QUrl(path));
461 KisAndroidUtils::clearJniException(QStringLiteral("after opening ") + path);
462#else
463 QDesktopServices::openUrl(QUrl::fromLocalFile(path));
464#endif
465 }
466};
467
468
470 : QDialog(parent)
471 , settings(s)
472 , d(new Private(this))
473{
474 d->ui->setupUi(this);
475
476#ifdef Q_OS_ANDROID
477 d->ui->labelFfmpegLocation->hide();
478 d->ui->editFfmpegPath->hide();
479 d->ui->buttonBrowseFfmpeg->hide();
480 d->ui->labelExportTo->hide();
481 d->ui->editVideoFilePath->hide();
482 d->ui->buttonBrowseExport->hide();
483 d->ui->buttonShowInFolder->hide();
484#else
485 d->ui->buttonBrowseFfmpeg->setIcon(KisIconUtils::loadIcon("folder"));
486 d->ui->buttonBrowseExport->setIcon(KisIconUtils::loadIcon("folder"));
487 d->ui->buttonShowInFolder->setIcon(KisIconUtils::loadIcon("folder"));
488#endif
489
490 d->spinInputFPSMaxValue = d->ui->spinInputFps->minimum();
491 d->spinInputFPSMaxValue = d->ui->spinInputFps->maximum();
492 d->ui->buttonBrowseDirectory->setIcon(KisIconUtils::loadIcon("view-preview"));
493 d->ui->buttonEditProfile->setIcon(KisIconUtils::loadIcon("document-edit"));
494 d->ui->buttonLockRatio->setIcon(settings->lockRatio ? KisIconUtils::loadIcon("locked") : KisIconUtils::loadIcon("unlocked"));
495 d->ui->buttonLockFps->setIcon(settings->lockFps ? KisIconUtils::loadIcon("locked") : KisIconUtils::loadIcon("unlocked"));
496 d->ui->buttonWatchIt->setIcon(KisIconUtils::loadIcon("media-playback-start"));
497 d->ui->buttonRemoveSnapshots->setIcon(KisIconUtils::loadIcon("edit-delete"));
498 d->ui->stackedWidget->setCurrentIndex(ExportPageIndex::PageSettings);
499 d->ui->spinLastFrameSec->setEnabled(d->ui->extendResultCheckBox->isChecked());
500 d->ui->spinFirstFrameSec->setEnabled(d->ui->resultPreviewCheckBox->isChecked());
501 d->ui->lblWarnFpsIcon->setPixmap(KisIconUtils::loadIcon("dialog-warning").pixmap(48, 48));
502 d->ui->lblWarnSizeIcon->setPixmap(KisIconUtils::loadIcon("dialog-warning").pixmap(48, 48));
503 d->ui->wdgWarnFps->hide();
504 d->ui->wdgWarnSize->hide();
505
506 connect(d->ui->buttonBrowseDirectory, SIGNAL(clicked()), SLOT(onButtonBrowseDirectoryClicked()));
507 connect(d->ui->spinInputFps, SIGNAL(valueChanged(int)), SLOT(onSpinInputFpsValueChanged(int)));
508 connect(d->ui->spinFps, SIGNAL(valueChanged(int)), SLOT(onSpinFpsValueChanged(int)));
509 connect(d->ui->resultPreviewCheckBox, SIGNAL(toggled(bool)), SLOT(onCheckResultPreviewToggled(bool)));
510 connect(d->ui->spinFirstFrameSec, SIGNAL(valueChanged(int)), SLOT(onFirstFrameSecValueChanged(int)));
511 connect(d->ui->extendResultCheckBox, SIGNAL(toggled(bool)), SLOT(onCheckExtendResultToggled(bool)));
512 connect(d->ui->spinLastFrameSec, SIGNAL(valueChanged(int)), SLOT(onLastFrameSecValueChanged(int)));
513 connect(d->ui->checkResize, SIGNAL(toggled(bool)), SLOT(onCheckResizeToggled(bool)));
514 connect(d->ui->spinScaleWidth, SIGNAL(valueChanged(int)), SLOT(onSpinScaleWidthValueChanged(int)));
515 connect(d->ui->spinScaleHeight, SIGNAL(valueChanged(int)), SLOT(onSpinScaleHeightValueChanged(int)));
516 connect(d->ui->buttonLockRatio, SIGNAL(toggled(bool)), SLOT(onButtonLockRatioToggled(bool)));
517 connect(d->ui->buttonLockFps, SIGNAL(toggled(bool)), SLOT(onButtonLockFpsToggled(bool)));
518 connect(d->ui->comboProfile, SIGNAL(currentIndexChanged(int)), SLOT(onComboProfileIndexChanged(int)));
519 connect(d->ui->buttonEditProfile, SIGNAL(clicked()), SLOT(onButtonEditProfileClicked()));
520 connect(d->ui->buttonBox->button(QDialogButtonBox::Save), SIGNAL(clicked()), this, SLOT(onButtonExportClicked()));
521 connect(d->ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
522 connect(d->ui->buttonCancelExport, SIGNAL(clicked()), SLOT(onButtonCancelClicked()));
523 connect(d->ui->buttonWatchIt, SIGNAL(clicked()), SLOT(onButtonWatchItClicked()));
524 connect(d->ui->buttonRemoveSnapshots, SIGNAL(clicked()), SLOT(onButtonRemoveSnapshotsClicked()));
525 connect(d->ui->buttonRestart, SIGNAL(clicked()), SLOT(onButtonRestartClicked()));
526 connect(d->ui->resultPreviewCheckBox, SIGNAL(toggled(bool)), d->ui->spinFirstFrameSec, SLOT(setEnabled(bool)));
527 connect(d->ui->extendResultCheckBox, SIGNAL(toggled(bool)), d->ui->spinLastFrameSec, SLOT(setEnabled(bool)));
528
529#ifndef Q_OS_ANDROID
530 connect(d->ui->buttonBrowseFfmpeg, SIGNAL(clicked()), SLOT(onButtonBrowseFfmpegClicked()));
531 connect(d->ui->buttonBrowseExport, SIGNAL(clicked()), SLOT(onButtonBrowseExportClicked()));
532 connect(d->ui->editVideoFilePath, SIGNAL(textChanged(QString)), SLOT(onEditVideoPathChanged(QString)));
533 connect(d->ui->buttonShowInFolder, SIGNAL(clicked()), SLOT(onButtonShowInFolderClicked()));
534 d->ui->editVideoFilePath->installEventFilter(this);
535#endif
536
537 d->ui->buttonBox->button(QDialogButtonBox::Save)->setText(i18n("Export"));
538}
539
543
545{
546 RecorderExportConfig config(true);
547 d->updateFps(config);
548 d->updateFrameInfo();
549
550 if (settings->framesCount == 0) {
551 d->ui->labelRecordInfo->setText(i18nc("Can't export recording because nothing to export", "No frames to export"));
552 d->ui->buttonBox->button(QDialogButtonBox::Save)->setEnabled(false);
553 } else {
554 d->ui->labelRecordInfo->setText(QString("%1: %2x%3 %4, %5 %6")
555 .arg(i18nc("General information about recording", "Recording info"))
556 .arg(settings->imageSize.width())
557 .arg(settings->imageSize.height())
558 .arg(i18nc("Pixel dimension suffix", "px"))
559 .arg(settings->framesCount)
560 .arg(i18ncp("The suffix after number of frames", "frame", "frames", settings->framesCount))
561 );
562 }
563
564
565 // Don't load lockFps flag from config, if liveCaptureMode was just set by the user
568
569 // Video dimensions are much more restrictive than image dimensions.
570 // Clobber them with sensible defaults instead of letting the user run into
571 // the trap of trying to export video well beyond what most encoders,
572 // devices and websites support.
573 d->initDimensions();
574
575 d->ui->spinInputFps->setValue(settings->inputFps);
576 d->ui->spinFps->setValue(settings->fps);
577 d->ui->resultPreviewCheckBox->setChecked(settings->resultPreview);
578 d->ui->spinFirstFrameSec->setValue(settings->firstFrameSec);
579 d->ui->extendResultCheckBox->setChecked(settings->extendResult);
580 d->ui->spinLastFrameSec->setValue(settings->lastFrameSec);
581 d->ui->checkResize->setChecked(settings->resize);
582 {
583 // Need to block signals or else a locked ratio will mess these up.
584 QSignalBlocker spinScaleWidthBlocker(d->ui->spinScaleWidth);
585 QSignalBlocker spinScaleHeightBlocker(d->ui->spinScaleHeight);
586 QSignalBlocker buttonLockRatioBlocker(d->ui->buttonLockRatio);
587 d->ui->spinScaleWidth->setValue(settings->size.width());
588 d->ui->spinScaleHeight->setValue(settings->size.height());
589 d->ui->buttonLockRatio->setChecked(settings->lockRatio);
590 }
591 d->ui->buttonLockRatio->setIcon(settings->lockRatio ? KisIconUtils::loadIcon("locked") : KisIconUtils::loadIcon("unlocked"));
592 d->ui->labelRealTimeCaptureNotion->setVisible(settings->realTimeCaptureMode);
593 d->ui->buttonLockFps->setChecked(settings->lockFps);
594 d->ui->buttonLockFps->setIcon(settings->lockFps ? KisIconUtils::loadIcon("locked") : KisIconUtils::loadIcon("unlocked"));
595 d->fillComboProfiles();
596#ifndef Q_OS_ANDROID
597 d->checkExporter();
598 d->updateVideoFilePath();
599#endif
600 d->updateVideoDuration();
601}
602
603void RecorderExport::closeEvent(QCloseEvent *event)
604{
605 if (!d->tryAbortExport())
606 event->ignore();
607}
608
610{
611 if (d->tryAbortExport())
612 QDialog::reject();
613}
614
616{
617 if (settings->framesCount != 0) {
619 } else {
620 QMessageBox::warning(this, windowTitle(), i18nc("Can't browse frames of recording because no frames have been recorded", "No frames to browse."));
621 return;
622 }
623}
624
626{
628 RecorderExportConfig config(false);
629 config.setInputFps(value);
630 d->updateFps(config, true);
631 d->updateVideoDuration();
632}
633
635{
636 settings->fps = value;
637 RecorderExportConfig config(false);
638 config.setFps(value);
639 d->updateFps(config, false);
640 d->updateVideoDuration();
641 d->updateWarningVisibility();
642}
643
645{
646 settings->resultPreview = checked;
648 d->updateVideoDuration();
649}
650
657
659{
660 settings->extendResult = checked;
662 d->updateVideoDuration();
663}
664
666{
669 d->updateVideoDuration();
670}
671
673{
674 settings->resize = checked;
675 RecorderExportConfig(false).setResize(checked);
676 d->updateWarningVisibility();
677}
678
680{
681 settings->size.setWidth(value);
682 if (settings->lockRatio)
683 d->updateRatio(true);
685 d->updateWarningVisibility();
686}
687
689{
690 settings->size.setHeight(value);
691 if (settings->lockRatio)
692 d->updateRatio(false);
694 d->updateWarningVisibility();
695}
696
698{
699 settings->lockRatio = checked;
700 RecorderExportConfig config(false);
701 config.setLockRatio(checked);
702 if (settings->lockRatio) {
703 d->updateRatio(true);
704 config.setSize(settings->size);
705 }
706 d->ui->buttonLockRatio->setIcon(settings->lockRatio ? KisIconUtils::loadIcon("locked") : KisIconUtils::loadIcon("unlocked"));
707 d->updateWarningVisibility();
708}
709
711{
712 settings->lockFps = checked;
713 RecorderExportConfig config(false);
714 config.setLockFps(checked);
715 d->updateFps(config);
716 if (settings->lockFps) {
717 d->ui->buttonLockFps->setIcon(KisIconUtils::loadIcon("locked"));
718 d->ui->spinInputFps->setMinimum(d->ui->spinFps->minimum());
719 d->ui->spinInputFps->setMaximum(d->ui->spinFps->maximum());
720 } else {
721 d->ui->buttonLockFps->setIcon(KisIconUtils::loadIcon("unlocked"));
722 d->ui->spinInputFps->setMinimum(d->spinInputFPSMinValue);
723 d->ui->spinInputFps->setMaximum(d->spinInputFPSMaxValue);
724 }
725 d->updateWarningVisibility();
726}
727
728#ifndef Q_OS_ANDROID
730{
731 KoFileDialog dialog(this, KoFileDialog::OpenFile, "SelectFFmpeg");
732 dialog.setCaption(i18n("Select FFmpeg Executable File"));
733 dialog.setDefaultDir(settings->ffmpegPath);
734 QString file = dialog.filename();
735 if (!file.isEmpty()) {
736 settings->ffmpegPath = file;
738 d->checkExporter();
739 }
740}
741#endif
742
744{
745#ifdef Q_OS_ANDROID
746 QString format = d->ui->comboProfile->itemData(index).toString();
747 d->settings->selectedFormat = format;
748 RecorderExportConfig(false).setSelectedFormat(format);
749#else
750 settings->profileIndex = index;
751 d->updateVideoFilePath();
753#endif
754}
755
757{
758#ifdef Q_OS_ANDROID
759 QString key = settings->selectedFormat;
762
763 KisMediaEncoderPreferencesDialog dlg(format, settings->formatPreferences.value(key).toMap(), this);
764 if (dlg.exec() == QDialog::Accepted) {
765 settings->formatPreferences.insert(key, dlg.preferences());
766 RecorderExportConfig(false).setFormatPreferences(settings->formatPreferences);
767 }
768#else
769 RecorderProfileSettings settingsDialog(this);
770
771 connect(&settingsDialog, &RecorderProfileSettings::requestPreview, [&](const QString & arguments) {
772 settingsDialog.setPreview(settings->ffmpegPath % " -y " % d->applyVariables(arguments).replace("\n", " ")
773 % " \"" % settings->videoFilePath % "\"");
774 });
775
776 if (settingsDialog.editProfile(
778 d->fillComboProfiles();
779 d->updateVideoFilePath();
781 }
782#endif
783}
784
785#ifndef Q_OS_ANDROID
786void RecorderExport::onEditVideoPathChanged(const QString &videoFilePath)
787{
788 QFileInfo fileInfo(videoFilePath);
789 if (!fileInfo.isRelative())
790 settings->videoDirectory = fileInfo.absolutePath();
791 settings->videoFileName = fileInfo.completeBaseName();
792}
793#endif
794
795#ifndef Q_OS_ANDROID
797{
798 QString videoFileName = d->requestFile(settings->profiles[settings->profileIndex].extension, settings->videoDirectory);
799 if (!videoFileName.isEmpty()) {
800 QFileInfo fileInfo(videoFileName);
801 settings->videoDirectory = fileInfo.absolutePath();
802 settings->videoFileName = fileInfo.completeBaseName();
804 d->updateVideoFilePath();
805 }
806}
807#endif
808
810{
811 if (settings->framesCount == 0) {
812 QMessageBox::warning(this, windowTitle(), i18n("No frames to export."));
813 return;
814 }
815
816#ifdef Q_OS_ANDROID
819 settings->videoFilePath = d->requestFile(format->extension());
820 if (settings->videoFilePath.isEmpty()) {
821 return;
822 }
823#else
824 if (QFile::exists(settings->videoFilePath)) {
825 if (QMessageBox::question(this, windowTitle(),
826 i18n("The video file already exists. Do you wish to overwrite it?"))
827 != QMessageBox::Yes) {
828 return;
829 }
830 }
831#endif
832
833 d->ui->stackedWidget->setCurrentIndex(ExportPageIndex::PageProgress);
834 d->startExport();
835}
836
838{
839 if (d->cleaner) {
840 d->cleaner->stop();
841 d->cleaner->deleteLater();
842 d->cleaner = nullptr;
843 return;
844 }
845
846 if (d->tryAbortExport())
847 d->ui->stackedWidget->setCurrentIndex(ExportPageIndex::PageSettings);
848}
849
850
852{
853 d->ui->buttonCancelExport->setEnabled(true);
854 d->ui->labelStatus->setText(i18n("The timelapse video is being encoded..."));
855}
856
858{
859 quint64 elapsed = d->elapsedTimer.elapsed();
860 d->ui->labelRenderTime->setText(d->formatDuration(elapsed));
861 d->ui->stackedWidget->setCurrentIndex(ExportPageIndex::PageDone);
862 d->ui->labelVideoPathDone->setText(settings->videoFilePath);
863 d->cleanupExporter();
864}
865
867{
868 d->ui->stackedWidget->setCurrentIndex(ExportPageIndex::PageSettings);
869 QMessageBox::critical(this, windowTitle(), i18n("Export failed. Error message:") % "\n\n" % error);
870 d->cleanupExporter();
871}
872
874{
875 d->ui->progressExport->setValue(frameNo * 100 / (settings->framesCount * settings->fps / static_cast<float>(settings->inputFps)));
876}
877
882
883#ifndef Q_OS_ANDROID
888#endif
889
891{
892 const QString confirmation(i18n("The recordings for this document will be deleted"
893 " and you will not be able to export a timelapse for it again"
894 ". Note that already exported timelapses will still be preserved."
895 "\n\nDo you wish to continue?"));
896 if (QMessageBox::question(this, windowTitle(), confirmation) != QMessageBox::Yes)
897 return;
898
899 d->ui->labelStatus->setText(i18nc("Label title, Snapshot directory deleting is in progress", "Cleaning up..."));
900 d->ui->stackedWidget->setCurrentIndex(ExportPageIndex::PageProgress);
901
902 Q_ASSERT(d->cleaner == nullptr);
903 d->cleaner = new RecorderDirectoryCleaner({d->settings->inputDirectory});
904 connect(d->cleaner, SIGNAL(finished()), this, SLOT(onCleanUpFinished()));
905 d->cleaner->start();
906}
907
909{
910 d->ui->stackedWidget->setCurrentIndex(ExportPageIndex::PageSettings);
911}
912
914{
915 d->cleaner->deleteLater();
916 d->cleaner = nullptr;
917
918 d->ui->stackedWidget->setCurrentIndex(ExportPageIndex::PageDone);
919 d->ui->buttonRestart->hide();
920 d->ui->buttonRemoveSnapshots->hide();
921}
922
923#ifndef Q_OS_ANDROID
924bool RecorderExport::eventFilter(QObject *obj, QEvent *event)
925{
926 if (obj == d->ui->editVideoFilePath && event->type() == QEvent::FocusOut)
927 d->updateVideoFilePath();
928
929 return QDialog::eventFilter(obj, event);
930}
931#endif
float value(const T *src, size_t ch)
QList< QString > QStringList
static QJsonObject findFFMpeg(const QString &customLocation)
virtual QString extension() const =0
static KisMediaEncoderFormat * getFormatByKey(const QString &key)
static const QVector< KisMediaEncoderFormat * > & getSupportedFormats()
static QString mimeTypeForSuffix(const QString &suffix)
Find the mimetype for a given extension. The extension may have the form "*.xxx" or "xxx".
void setProfiles(const QList< RecorderProfile > &value)
void setVideoDirectory(const QString &value)
void loadConfiguration(RecorderExportSettings *settings, bool loadLockFps=true) const
void setFfmpegPath(const QString &value)
void setSize(const QSize &value)
QString requestFile(const QString &extension, const QString &defaultDir=QString())
void updateFps(RecorderExportConfig &config, bool takeFromInputFps=false)
QScopedPointer< RecorderExportPreprocessor > preprocessor
RecorderDirectoryCleaner * cleaner
QString applyVariables(const QString &templateArguments)
QScopedPointer< Ui::RecorderExport > ui
RecorderExportSettings * settings
QString formatDuration(long durationMs)
static void desktopServicesOpenPath(const QString &path)
Private(RecorderExport *q_ptr)
QScopedPointer< Exporter > exporter
static constexpr int DIMENSION_LIMIT
QStringList splitCommand(const QString &command)
void updateRatio(bool widthToHeight)
void onButtonBrowseFfmpegClicked()
void onCheckResultPreviewToggled(bool checked)
void onSpinInputFpsValueChanged(int value)
void onLastFrameSecValueChanged(int value)
void onButtonShowInFolderClicked()
void closeEvent(QCloseEvent *event) override
void onEditVideoPathChanged(const QString &videoFilePath)
RecorderExportSettings * settings
RecorderExport(RecorderExportSettings *s, QWidget *parent=nullptr)
void onSpinScaleHeightValueChanged(int value)
void onButtonEditProfileClicked()
void onExporterFinishedWithError(QString error)
void onFirstFrameSecValueChanged(int value)
void onButtonRemoveSnapshotsClicked()
QScopedPointer< Private > d
void reject() override
void onCheckExtendResultToggled(bool checked)
bool eventFilter(QObject *obj, QEvent *event) override
void onCheckResizeToggled(bool checked)
void onButtonBrowseDirectoryClicked()
void onButtonLockFpsToggled(bool checked)
void onComboProfileIndexChanged(int index)
void onSpinFpsValueChanged(int value)
void onButtonLockRatioToggled(bool checked)
void onExporterProgressUpdated(int frameNo)
void onSpinScaleWidthValueChanged(int value)
void onButtonBrowseExportClicked()
bool editProfile(RecorderProfile *profile, const RecorderProfile &defaultProfile)
void setPreview(const QString &preview)
void requestPreview(QString arguments)
#define KIS_SAFE_ASSERT_RECOVER_RETURN(cond)
Definition kis_assert.h:128
void clearJniException(const QString &location)
QIcon loadIcon(const QString &name)
QLatin1String fileExtension(RecorderFormat format)
QList< RecorderProfile > profiles
QList< RecorderProfile > defaultProfiles