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