Krita Source Code Documentation
Loading...
Searching...
No Matches
KisDlgImportVideoAnimation.cpp
Go to the documentation of this file.
1/*
2 * SPDX-FileCopyrightText: 2021 Know Zero
3 * SPDX-FileCopyrightText: 2021 Eoin O'Neill <eoinoneill1991@gmail.com>
4 * SPDX-FileCopyrightText: 2021 Emmet O'Neill <emmetoneill.pdx@gmail.com>
5 * SPDX-FileCopyrightText: 2021 Wolthera van Hövell tot Westerflier <griffinvalley@gmail.com>
6 *
7 * SPDX-License-Identifier: GPL-3.0-or-later
8 */
9
11
12#include <QStandardPaths>
13#include <QtMath>
14#include <QJsonObject>
15#include <QJsonArray>
16#include <QMessageBox>
17
18#include <KFormat>
19
20#include "KoFileDialog.h"
21
22#include <KisDocument.h>
23#include <KisMainWindow.h>
25#include <kis_image.h>
28#include <kis_icon_utils.h>
29
30#include "KisFFMpegWrapper.h"
31
33 KoDialog(mainWindow),
34 m_mainWindow(mainWindow),
35 m_activeView(activeView)
36{
39 setWindowTitle(i18nc("@title:window", "Import Video Animation"));
40
41 QWidget *page = new QWidget(this);
42 m_ui.setupUi(page);
43 setMainWidget(page);
44
46
48 QFileInfo ffmpegFileInfo(config->getPropertyLazy("ffmpeg_path",""));
49 QFileInfo ffprobeFileInfo(config->getPropertyLazy("ffprobe_path",""));
50
51 dbgFile << "Config data =" << "ffmpeg:" << ffmpegFileInfo.absoluteFilePath() << "ffprobe:" << ffprobeFileInfo.absoluteFilePath();
52
53 QJsonObject ffmpegInfo = KisFFMpegWrapper::findFFMpeg(ffmpegFileInfo.absoluteFilePath());
54
55 if (ffmpegInfo["enabled"].toBool()) {
56 m_ui.cmbFFMpegLocation->addItem(ffmpegInfo["path"].toString(),ffmpegInfo);
57
58 if (ffprobeFileInfo.filePath().isEmpty())
59 ffprobeFileInfo.setFile(ffmpegFileInfo.absoluteDir().filePath("ffprobe"));
60
61 } else {
62 enableButtonOk(false);
63 m_ui.tabGeneral->setEnabled(false);
64 QMessageBox::warning(this, i18nc("@title:window", "Krita"), i18n("FFMpeg not found! Please add a path to FFMpeg in the \"Advanced\" tab"));
65 }
66
67 QJsonObject ffprobeInfo = KisFFMpegWrapper::findFFProbe(ffprobeFileInfo.absoluteFilePath());
68
69 if (ffprobeInfo["enabled"].toBool())
70 m_ui.cmbFFProbeLocation->addItem(ffprobeInfo["path"].toString(),ffprobeInfo);
71
72 m_ui.cmbFFProbeLocation->addItem("[Disabled]",QJsonObject({{"path",""},{"enabled",false}}));
73
74 m_ui.fileLocation->setMode(KoFileDialog::OpenFile);
75 m_ui.fileLocation->setMimeTypeFilters(makeVideoMimeTypesList());
76 m_ui.nextFrameButton->setIcon(KisIconUtils::loadIcon("arrow-right"));
77 m_ui.prevFrameButton->setIcon(KisIconUtils::loadIcon("arrow-left"));
78
79 m_ui.fpsSpinbox->setValue(24.0);
80 m_ui.fpsSpinbox->setSuffix(i18nc("FPS as a unit following a value, like 60 FPS", " FPS"));
81
82 m_ui.frameSkipSpinbox->setValue(1);
83 m_ui.frameSkipSpinbox->setRange(1,20);
84
85 m_ui.startExportingAtSpinbox->setValue(0.0);
86 m_ui.startExportingAtSpinbox->setRange(0.0, 9999.0);
87 m_ui.startExportingAtSpinbox->setSuffix(i18nc("Second as a unit following a value, like 60 s", " s"));
88
89 m_ui.videoPreviewSlider->setTickInterval(1);
90 m_ui.videoPreviewSlider->setValue(0);
91
92 m_ui.exportDurationSpinbox->setValue(3.0);
93 m_ui.exportDurationSpinbox->setSuffix(i18nc("Second as a unit following a value, like 60 s", " s"));
94
95 m_ui.lblWarning->hide();
96
97 connect(m_ui.cmbDocumentHandler, SIGNAL(currentIndexChanged(int)), SLOT(slotDocumentHandlerChanged(int)));
98
99 m_ui.cmbDocumentHandler->addItem(i18nc("Import video to New Document", "New Document"), "0");
100
102 m_ui.cmbDocumentHandler->addItem(i18nc("Import video to Current Document", "Current Document"), "1");
103 m_ui.cmbDocumentHandler->setCurrentIndex(1);
104 m_ui.fpsDocumentLabel->setText(i18nc("Video importer: fps of the document you're importing into"
105 , "<small>Document:\n %1 FPS</small>"
106 , QString::number(m_activeView->document()->image()->animationInterface()->framerate()))
107 );
108 }
109
110 m_ui.documentWidthSpinbox->setValue(0);
111 m_ui.documentHeightSpinbox->setValue(0);
112 m_ui.documentWidthSpinbox->setRange(1,100000);
113 m_ui.documentHeightSpinbox->setRange(1,100000);
114
115 m_ui.videoWidthSpinbox->setValue(0);
116 m_ui.videoHeightSpinbox->setValue(0);
117 m_ui.videoWidthSpinbox->setRange(1,100000);
118 m_ui.videoHeightSpinbox->setRange(1,100000);
119
120 m_ui.sensitivitySpinbox->setValue(50.0f);
121
122 m_ui.cmbVideoScaleFilter->addItem(i18n("Bicubic"), "bicubic");
123 m_ui.cmbVideoScaleFilter->addItem(i18n("Bilinear"), "bilinear");
124 m_ui.cmbVideoScaleFilter->addItem(i18n("Lanczos3"), "lanczos");
125 m_ui.cmbVideoScaleFilter->addItem(i18n("Nearest Neighbor"), "neighbor");
126 m_ui.cmbVideoScaleFilter->addItem(i18nc("An interpolation method", "Spline"), "spline");
127
128 m_ui.tabWidget->setCurrentIndex(0);
129
130 m_videoSliderTimer = new QTimer(this);
131 m_videoSliderTimer->setSingleShot(true);
132
133 connect(m_videoSliderTimer, SIGNAL(timeout()), SLOT(slotVideoTimerTimeout()));
134
135 m_currentFrame = 0;
137
139 connect(m_ui.nextFrameButton, SIGNAL(clicked()), SLOT(slotNextFrame()));
140 connect(m_ui.prevFrameButton, SIGNAL(clicked()), SLOT(slotPrevFrame()));
141 connect(m_ui.currentFrameNumberInput, SIGNAL(valueChanged(int)), SLOT(slotFrameNumberChanged(int)));
142 connect(m_ui.videoPreviewSlider, SIGNAL(valueChanged(int)), SLOT(slotVideoSliderChanged()));
143
144 connect(m_ui.ffprobePickerButton, SIGNAL(clicked()), SLOT(slotFFProbeFile()));
145 connect(m_ui.ffmpegPickerButton, SIGNAL(clicked()), SLOT(slotFFMpegFile()));
146
147 connect(m_ui.exportDurationSpinbox, SIGNAL(valueChanged(qreal)), SLOT(slotImportDurationChanged(qreal)));
148}
149
151 KisConfig globalConfig(true);
152 return globalConfig.exportConfiguration(configurationID);
153}
154
156{
157 KisConfig globalConfig(false);
158 globalConfig.setExportConfiguration(configurationID, config);
159}
160
161float rerange(float value, float oldMin, float oldMax, float newMin, float newMax) {
162 return ((value - oldMin) / (oldMax - oldMin)) * (newMax - newMin) + newMin;
163}
164
166{
167 RenderedFrames frames;
168 QStringList &frameFileList = frames.renderedFrameFiles;
169 QList<int> &frameTimeList = frames.renderedFrameTargetTimes;
170
171 if ( !directory.mkpath(".") ) {
172 QMessageBox::warning(this, i18nc("@title:window", "Krita"), i18n("Failed to create a work directory, make sure you have write permission"));
173 return frames;
174 }
175
176 QStringList args;
177 const float exportDuration = m_ui.exportDurationSpinbox->value();
178 const float fps = m_ui.fpsSpinbox->value();
179 const float nDuplicateSensitivity = m_ui.sensitivitySpinbox->value() / m_ui.sensitivitySpinbox->maximum();
180
181 if (exportDuration / fps > 100.0) {
182 if (QMessageBox::warning(this, i18nc("Title for a messagebox", "Krita"),
183 i18n("Warning: you are trying to import more than 100 frames into Krita.\n\n"
184 "This means you might be overloading your system.\n"
185 "If you want to edit a clip larger than 100 frames, consider using a real video editor, like Kdenlive (https://kdenlive.org)."),
186 QMessageBox::Ok | QMessageBox::Cancel,
187 QMessageBox::Cancel) == QMessageBox::Cancel) {
188 return frames;
189 }
190 }
191
192 // Setup FFMpeg Command Args...
193 args << "-ss" << QString::number(m_ui.startExportingAtSpinbox->value())
194 << "-i" << m_videoInfo.file;
195
196 const float sceneFiltrationFilterThreshold = rerange( 1 - nDuplicateSensitivity, 0.0f, 1.0f, 0.0005f, 0.2f);
197
198 if (m_ui.optionFilterDuplicates->isChecked()) {
199 args << "-filter:v" << QString("select=gt(scene\\,%1)+eq(n\\,0)").arg(sceneFiltrationFilterThreshold)
200 << "-vsync" << "0";
201 }
202
203 args << "-t" << QString::number(exportDuration)
204 << "-r" << QString::number(fps);
205
206 if ( m_videoInfo.width != m_ui.videoWidthSpinbox->value() || m_videoInfo.height != m_ui.videoHeightSpinbox->value() ) {
207 args << "-vf" << QString("scale=w=")
208 .append(QString::number(m_ui.videoWidthSpinbox->value()))
209 .append(":h=")
210 .append(QString::number(m_ui.videoHeightSpinbox->value()))
211 .append(":flags=")
212 .append(m_ui.cmbVideoScaleFilter->currentData().toString());
213 }
214
215 QJsonObject ffmpegInfo = m_ui.cmbFFMpegLocation->currentData().toJsonObject();
216 QJsonObject ffprobeInfo = m_ui.cmbFFProbeLocation->currentData().toJsonObject();
217
218 KisPropertiesConfigurationSP config = loadLastUsedConfiguration("ANIMATION_EXPORT");
219
220 config->setProperty("ffmpeg_path", ffmpegInfo["path"].toString());
221 config->setProperty("ffprobe_path", ffprobeInfo["path"].toString());
222
223 saveLastUsedConfiguration("ANIMATION_EXPORT", config);
224
225 {
226 KisFFMpegWrapperSettings ffmpegSettings;
227
228 ffmpegSettings.processPath = ffmpegInfo["path"].toString();
229 ffmpegSettings.args = args;
230 ffmpegSettings.outputFile = directory.filePath("output_%04d.png");
231 ffmpegSettings.logPath = QDir::tempPath() + QDir::separator() + "krita" + QDir::separator() + "ffmpeg.log";
232 ffmpegSettings.totalFrames = qCeil(exportDuration * fps);
233 ffmpegSettings.progressMessage = i18nc("FFMPEG animated video import message. arg1: frame progress number. arg2: file suffix."
234 , "Extracted %1 frames from %2 video.", "[progress]", "[suffix]");
235
236 QScopedPointer<KisFFMpegWrapper> ffmpeg(new KisFFMpegWrapper(this));
237
238 ffmpeg->startNonBlocking(ffmpegSettings);
239
240 bool ffmpegSuccess = ffmpeg->waitForFinished();
241 if (!ffmpegSuccess) {
242 return RenderedFrames();
243 }
244
245 frameFileList = directory.entryList(QStringList() << "output_*.png",QDir::Files);
246 frameFileList.replaceInStrings("output_", directory.absolutePath() + QDir::separator() + "output_");
247
248 dbgFile << "Import frames list:" << frameFileList;
249 }
250
251 if (m_ui.optionFilterDuplicates->isChecked()){
252 KisFFMpegWrapperSettings ffmpegSettings;
253 ffmpegSettings.defaultPrependArgs.clear();
254 ffmpegSettings.processPath = ffprobeInfo["path"].toString();
255
256 QString filter = "movie=" + m_videoInfo.file + QString(",setpts=N+1,select=gt(scene\\,%1)").arg(sceneFiltrationFilterThreshold);
257 ffmpegSettings.args = QStringList() << "-select_streams" << "v"
258 << "-show_entries" << "frame=pkt_pts"
259 << "-of" << "compact=p=0:nk=1"
260 << "-f" << "lavfi" << filter;
261
262
263 QScopedPointer<KisFFMpegWrapper> ffmpeg(new KisFFMpegWrapper(this));
264 frameTimeList = {0}; // We always have a frame 0 here...
265 connect(ffmpeg.data(), &KisFFMpegWrapper::sigReadSTDOUT, [&](QByteArray arr) {
266 QString out = QString(arr);
267
268 QStringList integerOuts = out.split("\n", Qt::SkipEmptyParts);
269 Q_FOREACH(const QString& str, integerOuts){
270 bool ok = false;
271 const int value = str.toUInt(&ok);
272 if (ok) {
273 frameTimeList.push_back(value);
274 }
275 }
276 });
277
278 ffmpeg->startNonBlocking(ffmpegSettings);
279
280 bool ffmpegSuccess = ffmpeg->waitForFinished();
281 if (!ffmpegSuccess) {
282 return RenderedFrames();
283 }
284
285 dbgFile << "Assign to frames:" << ppVar(frameTimeList);
286 }
287
288 if ( frameFileList.isEmpty() ) {
289 QMessageBox::critical(this, i18nc("@title:window", "Krita"), i18n("Failed to export frames from video"));
290 }
291
292 return frames;
293}
294
295
297 QStringList documentInfoList;
298
299 // We're looking for a possible profile here, otherwise it gets generated. Then we get the name.
300 QString profileColorSpace = RGBAColorModelID.id();
301 QString profileName = KoColorSpaceRegistry::instance()->p709SRGBProfile()->name();
304 profileName = profile->name();
305 profileColorSpace = profile->colorModelID();
306 }
307
308 documentInfoList << QString::number(m_ui.frameSkipSpinbox->value())
309 << QString::number(m_ui.fpsSpinbox->value())
310 << QString::number(qCeil(m_ui.fpsSpinbox->value() * m_ui.exportDurationSpinbox->value()))
311 << m_videoInfo.file;
312
313 if ( m_ui.cmbDocumentHandler->currentIndex() == 0 ) {
314 documentInfoList << "0"
315 << QString::number(m_ui.documentWidthSpinbox->value())
316 << QString::number(m_ui.documentHeightSpinbox->value())
317 << QString::number(72)
318 << profileColorSpace
320 << profileName;
321 } else {
322 documentInfoList << "1";
323 }
324
325 return documentInfoList;
326}
327
329{
330 QStringList supportedMimeTypes = QStringList();
331 supportedMimeTypes << "video/x-matroska";
332 supportedMimeTypes << "image/gif";
333 supportedMimeTypes << "image/apng";
334 supportedMimeTypes << "image/png";
335 supportedMimeTypes << "video/quicktime"; // MOV
336 supportedMimeTypes << "video/ogg";
337 supportedMimeTypes << "video/mp4";
338 supportedMimeTypes << "video/mpeg";
339 supportedMimeTypes << "video/webm";
340
341 // All files
342 supportedMimeTypes << "application/octet-stream";
343
344 return supportedMimeTypes;
345}
346
348{
349 KoFileDialog dialog(this, KoFileDialog::ImportFiles, "OpenDocument");
350 dialog.setDefaultDir(QStandardPaths::writableLocation(QStandardPaths::PicturesLocation));
351 dialog.setMimeTypeFilters( makeVideoMimeTypesList() );
352 dialog.setCaption(i18n("Select your Video File"));
353
354 return dialog.filenames();
355}
356
357
359{
360 enableButtonOk(toggleBool);
361 m_ui.videoPreviewSlider->setEnabled(toggleBool);
362 m_ui.currentFrameNumberInput->setEnabled(toggleBool);
363 m_ui.nextFrameButton->setEnabled(toggleBool);
364 m_ui.prevFrameButton->setEnabled(toggleBool);
365}
366
367void KisDlgImportVideoAnimation::loadVideoFile(const QString &filename)
368{
369 const QFileInfo resultFileInfo(filename);
370 const QDir videoDir(resultFileInfo.absolutePath());
371
372 m_videoInfo = loadVideoInfo(filename);
373
374 if ( m_videoInfo.file.isEmpty() ) return;
375
376 QStringList textInfo;
377
378 textInfo.append(i18nc("video importer: video file statistics", "Width: %1 px", QString::number(m_videoInfo.width)));
379 textInfo.append(i18nc("video importer: video file statistics", "Height: %1 px", QString::number(m_videoInfo.height)));
380
382 textInfo.append(i18nc("video importer: video file statistics"
383 , "Color Primaries: %1"
385 textInfo.append(i18nc("video importer: video file statistics"
386 , "Color Transfer: %1"
388 }
389 textInfo.append(i18nc("video importer: video file statistics", "Duration: %1 s", QString::number(m_videoInfo.duration, 'f', 2)));
390 textInfo.append(i18nc("video importer: video file statistics", "Frames: %1", QString::number(m_videoInfo.frames)));
391 textInfo.append(i18nc("video importer: video file statistics", "FPS: %1", QString::number(m_videoInfo.fps)));
392
393
395 textInfo.append(i18nc("video importer: video file statistics", "*<font size='0.5em'><em>*FPS not right in file. Modified to see full duration</em></font>"));
396 }
397
398 m_ui.fpsSpinbox->setValue( qCeil(m_videoInfo.fps) );
399 m_ui.fileLoadedDetails->setText(textInfo.join("\n"));
400
401
402 m_ui.videoPreviewSlider->setRange(0, m_videoInfo.frames);
403 m_ui.currentFrameNumberInput->setRange(0, m_videoInfo.frames);
404 m_ui.exportDurationSpinbox->setRange(0, 9999.0);
405
406 if (m_ui.cmbDocumentHandler->currentIndex() == 0) {
407 m_ui.documentWidthSpinbox->setValue(m_videoInfo.width);
408 m_ui.documentHeightSpinbox->setValue(m_videoInfo.height);
409 }
410
411 m_ui.videoWidthSpinbox->setValue(m_videoInfo.width);
412 m_ui.videoHeightSpinbox->setValue(m_videoInfo.height);
413
414 m_ui.exportDurationSpinbox->setValue(m_videoInfo.duration);
415
417
418 if ( m_videoInfo.file.isEmpty() ) {
419 toggleInputControls(false);
420 } else {
423 }
424
425
426}
427
428
430{
431 float currentDuration = ( m_videoInfo.stream != -1 ) ? (m_currentFrame / m_videoInfo.fps):0;
432 QStringList args;
433
434 args << "-ss" << QString::number(currentDuration)
435 << "-i" << m_videoInfo.file
436 << "-v" << "quiet"
437 << "-vframes" << "1"
438 << "-vcodec" << "mjpeg"
439 << "-f" << "image2pipe"
440 << "pipe:1";
441
442 struct KisFFMpegWrapperSettings ffmpegSettings;
443
444 QJsonObject ffmpegInfo = m_ui.cmbFFMpegLocation->currentData().toJsonObject();
445 QByteArray byteImage = KisFFMpegWrapper::runProcessAndReturn(ffmpegInfo["path"].toString(), args, FFMPEG_TIMEOUT);
446
447 if ( byteImage.isEmpty() ) {
448 m_ui.thumbnailImageHolder->setText( m_videoInfo.frames == m_currentFrame ? "End of Video":"No Preview" );
449 } else {
450 QPixmap thumbnailPixmap;
451 thumbnailPixmap.loadFromData(byteImage,"JFIF");
452
453 m_ui.thumbnailImageHolder->clear();
454 const QSize previewSize =
455 m_ui.thumbnailImageHolder->contentsRect().size() * m_ui.thumbnailImageHolder->devicePixelRatioF();
456 QPixmap img = thumbnailPixmap.scaled(previewSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
457 img.setDevicePixelRatio(m_ui.thumbnailImageHolder->devicePixelRatioF());
458 m_ui.thumbnailImageHolder->setPixmap(img);
459 }
460}
461
462
467
469{
473 const KFormat format;
474
475 const int resolution = m_videoInfo.width * m_videoInfo.height;
476 quint32 pixelSize = 4; //how do we even go about getting the bitdepth???
477 if (m_activeView && m_ui.cmbDocumentHandler->currentIndex() > 0) {
478 pixelSize = m_activeView->image()->colorSpace()->pixelSize() * 4;
479 } else if (m_videoInfo.colorDepth == "U16"){
480 pixelSize = 8;
481 }
482 const qint64 frames = std::lround(qreal(m_videoInfo.fps) * time + 2);
483 // Sometimes, the potential size of the file is so big (a feature length film taking easily 970 gib), that we cannot put it into a number.
484 // It's more efficient therefore to calculate the maximum amount of frames possible.
485
486 const qint64 maxFrames = stats.totalMemoryLimit / resolution / pixelSize;
487
488 QStringList warnings;
489
490 const QString text_frames = i18nc("part of warning in video importer."
491 , "<b>Warning:</b> you are trying to import %1 frames, the maximum amount you can import is %2."
492 , frames
493 , maxFrames);
494
495 QString text_memory;
496
497 const QString text_video_editor = i18nc("part of warning in video importer.",
498 "Use a <a href=\"https://kdenlive.org\">video editor</a> instead!");
499
500 if (maxFrames < frames) {
501 warnings.append(text_frames);
502 text_memory = i18nc("part of warning in video importer."
503 , "You do not have enough memory to load this many frames, the computer will be overloaded.");
504 warnings.insert(0, "<span style=\"color:#ff692e;\">");
505 warnings.append(text_memory);
506 warnings.append(text_video_editor);
507 m_ui.lblWarning->setVisible(true);
508 } else if (maxFrames < frames * 2) {
509 warnings.append(text_frames);
510 text_memory = i18nc("part of warning in video importer."
511 , "This will take over half the available memory, editing will be difficult.");
512 warnings.insert(0, "<span style=\"color:#ffee00;\">");
513 warnings.append(text_memory);
514 warnings.append(text_video_editor);
515 m_ui.lblWarning->setVisible(true);
518 warnings.append(text_frames);
519 QString text_trc = i18nc("part of warning in video importer."
520 , "Krita does not support the video transfer curve (%1), it will be loaded as linear."
522 warnings.append(text_trc);
523 }
524
525 if (warnings.isEmpty()) {
526 m_ui.lblWarning->setVisible(false);
527 } else {
528 m_ui.lblWarning->setText(warnings.join(" "));
529 m_ui.lblWarning->setPixmap(
530 m_ui.lblWarning->style()->standardIcon(QStyle::SP_MessageBoxWarning).pixmap(QSize(32, 32)));
531 m_ui.lblWarning->setVisible(true);
532 }
533}
534
539
544
549
550
552{
553 KoFileDialog dialog(this, KoFileDialog::OpenFile, i18n("Open FFProbe"));
554 dialog.setDefaultDir(QStandardPaths::standardLocations(QStandardPaths::ApplicationsLocation).last());
555 dialog.setCaption(i18n("Open FFProbe"));
556
557 QStringList filenames = dialog.filenames();
558
559 if (!filenames.isEmpty()) {
560 QJsonObject ffprobeInfo = KisFFMpegWrapper::findFFProbe(filenames[0]);
561
562 if (ffprobeInfo["enabled"].toBool() && ffprobeInfo["custom"].toBool()) {
563 m_ui.cmbFFProbeLocation->addItem(filenames[0],ffprobeInfo);
564 m_ui.cmbFFProbeLocation->setCurrentText(filenames[0]);
565 return;
566 }
567 QMessageBox::warning(this, i18nc("@title:window", "Krita"), i18n("FFProbe is invalid!"));
568 }
569
570}
571
573{
574 KoFileDialog dialog(this, KoFileDialog::OpenFile, i18n("Open FFMpeg"));
575 dialog.setDefaultDir(QStandardPaths::standardLocations(QStandardPaths::ApplicationsLocation).last());
576 dialog.setCaption(i18n("Open FFMpeg"));
577
578 QStringList filenames = dialog.filenames();
579
580 if (!filenames.isEmpty()) {
581 QJsonObject ffmpegInfo = KisFFMpegWrapper::findFFMpeg(filenames[0]);
582
583 if (ffmpegInfo["enabled"].toBool()) {
584 if (ffmpegInfo["custom"].toBool()) {
585 m_ui.cmbFFMpegLocation->addItem(filenames[0],ffmpegInfo);
586 m_ui.cmbFFMpegLocation->setCurrentText(filenames[0]);
587 } else {
588 QMessageBox::warning(this, i18nc("@title:window", "Krita"), i18n("FFMpeg is invalid!"));
589 }
590 m_ui.tabGeneral->setEnabled(true);
591 return;
592 }
593
594 m_ui.tabGeneral->setEnabled(false);
595 QMessageBox::critical(this, i18nc("@title:window", "Krita"), i18n("No FFMpeg found!"));
596 }
597
598}
599
601{
602 bool toggleDocumentOptions = selectedIndex == 0;
603
604 if (toggleDocumentOptions) {
605 m_ui.fpsDocumentLabel->setText(" ");
606
607 if (m_videoInfo.stream != -1) {
608 m_ui.documentWidthSpinbox->setValue(m_videoInfo.width);
609 m_ui.documentHeightSpinbox->setValue(m_videoInfo.height);
610 }
611
612 } else if (m_activeView) {
613 m_ui.fpsDocumentLabel->setText(i18nc("Video importer: fps of the document you're importing into"
614 , "<small>Document:\n %1 FPS</small>"
615 , QString::number(m_activeView->document()->image()->animationInterface()->framerate()))
616 );
617 }
618
619 m_ui.optionsDocumentGroup->setEnabled(toggleDocumentOptions);
620
621}
622
624{
625 CurrentFrameChanged(m_ui.videoPreviewSlider->value());
626
627 if (!m_videoSliderTimer->isActive()) m_videoSliderTimer->start(300);
628
629}
630
632{
633 float currentSeconds = 0;
634
635 // update frame and seconds model data if they have changed
636 if (m_currentFrame != frame ) {
637 dbgFile << "Frame change to:" << frame;
638 m_currentFrame = frame;
639 currentSeconds = m_currentFrame / m_videoInfo.fps;
640 }
641
642 // update UI components if they are out of sync
643 if (m_currentFrame != m_ui.currentFrameNumberInput->value())
644 m_ui.currentFrameNumberInput->setValue(m_currentFrame);
645
646 if (m_currentFrame != m_ui.videoPreviewSlider->value())
647 m_ui.videoPreviewSlider->setValue(m_currentFrame);
648
649 m_ui.videoPreviewSliderValueLabel->setText( QString::number(currentSeconds, 'f', 2).append(i18nc("Second as a unit following a value, like 60 s", " s")) );
650}
651
653{
654 QJsonObject ffmpegInfo = m_ui.cmbFFMpegLocation->currentData().toJsonObject();
655 QJsonObject ffprobeInfo = m_ui.cmbFFProbeLocation->currentData().toJsonObject();
656 struct KisBasicVideoInfo videoInfoData;
657
658 KisFFMpegWrapper *ffprobe = new KisFFMpegWrapper(this);
659 QJsonObject ffprobeJsonObj;
660
661 std::function<void(void)> warnFFmpegFormatSupport = [this](){
662 QMessageBox::warning(this, i18nc("@title:window", "Krita"), i18n("Your FFMpeg version does not support this format"));
663 };
664
665 if (ffprobeInfo["enabled"].toBool()) {
666 ffprobeJsonObj = ffprobe->ffprobe(inputFile, ffprobeInfo["path"].toString());
667 }
668
669 // Attempt manual probing with ffmpeg itself if ffprobe is disable or if something went wrong with ffprobe.
670 if ( !ffprobeInfo["enabled"].toBool() || ffprobeJsonObj["error"].toInt() == FFProbeErrorCodes::INVALID_JSON ) {
671 KisFFMpegWrapper *ffmpeg = new KisFFMpegWrapper(this);
672
673 ffprobeJsonObj = ffmpeg->ffmpegProbe(inputFile, ffmpegInfo["path"].toString(), false);
674
675 dbgFile << "ffmpeg probe1" << ffprobeJsonObj;
676
677 QJsonObject ffprobeProgress = ffprobeJsonObj["progress"].toObject();
678
679 videoInfoData.frames = ffprobeProgress["frame"].toString().toInt();
680 }
681
682 if ( ffprobeJsonObj["error"].toInt() == FFProbeErrorCodes::UNSUPPORTED_CODEC) {
683 // If, after all of that, we still don't determine a supported codec then we'll back out.
684 warnFFmpegFormatSupport();
685 return {};
686 } else if ( ffprobeJsonObj["error"].toInt() == FFProbeErrorCodes::NONE ) {
687
688 QJsonObject ffprobeFormat = ffprobeJsonObj["format"].toObject();
689 QJsonArray ffprobeStreams = ffprobeJsonObj["streams"].toArray();
690
691 videoInfoData.file = inputFile;
692 for (const QJsonValueRef &streamItemRef : ffprobeStreams) {
693 QJsonObject streamItemObj = streamItemRef.toObject();
694
695 if ( streamItemObj["codec_type"].toString() == "video" ) {
696 videoInfoData.stream = streamItemObj["index"].toInt();
697 break;
698 }
699 }
700
701 if ( videoInfoData.stream == -1 ) {
702 QMessageBox::warning(this, i18nc("@title:window", "Krita"), i18n("No video stream could be found!"));
703 return {};
704 }
705
706 const QJsonObject ffprobeSelectedStream = ffprobeStreams[videoInfoData.stream].toObject();
707
708 const QJsonObject decoders = ffmpegInfo.value("codecs").toObject();
709
710 const QString codecName = ffprobeSelectedStream["codec_name"].toString();
711
712 const auto decoder = decoders.constFind(codecName);
713
714 if (decoder == decoders.constEnd() ) {
715 dbgFile << "Codec missing or unsupported:" << codecName;
716 warnFFmpegFormatSupport();
717 return {};
718 } else if (!decoder->toObject().value("decoding").toBool()) {
719 dbgFile << "Codec not supported for decoding:" << codecName;
720 warnFFmpegFormatSupport();
721 return {};
722 }
723
724 videoInfoData.width = ffprobeSelectedStream["width"].toInt();
725 videoInfoData.height = ffprobeSelectedStream["height"].toInt();
726 videoInfoData.encoding = ffprobeSelectedStream["codec_name"].toString();
727 videoInfoData.colorPrimaries = KisFFMpegWrapper::colorPrimariesFromName(ffprobeSelectedStream["color_primaries"].toString());
728 videoInfoData.colorTransfer = KisFFMpegWrapper::transferCharacteristicsFromName(ffprobeSelectedStream["color_transfer"].toString());
729
730 // bits_per_raw_sample was introduced in 2014.
731 if (ffprobeSelectedStream.value("bits_per_raw_sample").toInt() > 8) {
732 videoInfoData.colorDepth = Integer16BitsColorDepthID.id();
733 } else {
734 videoInfoData.colorDepth = Integer8BitsColorDepthID.id();
735 }
736
737 // frame rate comes back in odd format...so we need to do a bit of work so it is more usable.
738 // data will come back like "50/3"
739 QStringList rawFrameRate = ffprobeSelectedStream["r_frame_rate"].toString().split('/');
740
741 if (!rawFrameRate.isEmpty())
742 videoInfoData.fps = qCeil(rawFrameRate[0].toFloat() / rawFrameRate[1].toFloat());
743
744 if ( !ffprobeSelectedStream["nb_frames"].isNull() ) {
745 videoInfoData.frames = ffprobeSelectedStream["nb_frames"].toString().toInt();
746 }
747
748 // Get duration from stream, if it doesn't exist such as on VP8 and VP9, try to get it out of format
749 if ( !ffprobeSelectedStream["duration"].isNull() ) {
750 videoInfoData.duration = ffprobeSelectedStream["duration"].toString().toFloat();
751 } else if ( !ffprobeFormat["duration"].isNull() ) {
752 videoInfoData.duration = ffprobeFormat["duration"].toString().toFloat();
753 } else if ( videoInfoData.frames ) {
754 videoInfoData.duration = videoInfoData.frames / videoInfoData.fps;
755 }
756
757 dbgFile << "Initial video info from probe: "
758 << "stream:" << videoInfoData.stream
759 << "frames:" << videoInfoData.frames
760 << "duration:" << videoInfoData.duration
761 << "fps:" << videoInfoData.fps
762 << "encoding:" << videoInfoData.encoding;
763
764 if ( !videoInfoData.frames && !videoInfoData.duration ) {
765 KisFFMpegWrapper *ffmpeg = new KisFFMpegWrapper(this);
766
767 QJsonObject ffmpegJsonObj = ffmpeg->ffmpegProbe(inputFile, ffmpegInfo["path"].toString(), false);
768
769 dbgFile << "ffmpeg probe2" << ffmpegJsonObj;
770
771 if ( ffprobeJsonObj["error"].toInt() != FFProbeErrorCodes::NONE ) {
772 QMessageBox::warning(this, i18nc("@title:window", "Krita"), i18n("Failed to load video information"));
773 return {};
774 }
775
776 QJsonObject ffmpegProgressJsonObj = ffmpegJsonObj["progress"].toObject();
777 float ffmpegFPS = ffmpegProgressJsonObj["ffmpeg_fps"].toString().toFloat();
778
779 videoInfoData.frames = ffmpegProgressJsonObj["frame"].toString().toInt();
780
781 if (ffmpegFPS > 0 && videoInfoData.frames) {
782 videoInfoData.duration = videoInfoData.frames / ffmpegFPS;
783 videoInfoData.fps = ffmpegFPS;
784 } else {
785 videoInfoData.duration = ffmpegProgressJsonObj["out_time_ms"].toString().toFloat() / 1000000;
786 if (videoInfoData.frames) videoInfoData.fps = videoInfoData.frames / videoInfoData.duration;
787 }
788
789 }
790 }
791
792 // if there is no data on frames but duration and fps exists like OGV, try to estimate it
793 if ( videoInfoData.fps && videoInfoData.duration && !videoInfoData.frames ) {
794 videoInfoData.frames = qCeil( videoInfoData.fps * videoInfoData.duration );
795 }
796
797 dbgFile << "Final video info from probe: "
798 << "stream:" << videoInfoData.stream
799 << "frames:" << videoInfoData.frames
800 << "duration:" << videoInfoData.duration
801 << "fps:" << videoInfoData.fps
802 << "encoding:" << videoInfoData.encoding;
803
804 const float calculatedFrameRateByDuration = videoInfoData.frames / videoInfoData.duration;
805 const int frameRateDifference = qAbs(videoInfoData.fps - calculatedFrameRateByDuration);
806
807 if (frameRateDifference > 1) {
808 // something is not right with the frame rate with this file, so let's use our calculated value
809 // to make sure we get the whole duration
810 videoInfoData.hasOverriddenFPS = true;
811 videoInfoData.fps = calculatedFrameRateByDuration;
812 } else {
813 videoInfoData.hasOverriddenFPS = false;
814 }
815
816 return videoInfoData;
817}
818
819
float value(const T *src, size_t ch)
float rerange(float value, float oldMin, float oldMax, float newMin, float newMax)
@ INVALID_JSON
@ UNSUPPORTED_CODEC
@ NONE
const int FFMPEG_TIMEOUT
QList< QString > QStringList
const KoID Integer8BitsColorDepthID("U8", ki18n("8-bit integer/channel"))
const KoID Integer16BitsColorDepthID("U16", ki18n("16-bit integer/channel"))
const KoID RGBAColorModelID("RGBA", ki18n("RGB/Alpha"))
@ PRIMARIES_UNSPECIFIED
@ TRC_ITU_R_BT_2100_0_HLG
@ TRC_SMPTE_ST_428_1
KisPropertiesConfigurationSP exportConfiguration(const QString &filterId, bool defaultValue=false) const
void setExportConfiguration(const QString &filterId, KisPropertiesConfigurationSP properties) const
KisPropertiesConfigurationSP loadLastUsedConfiguration(QString configurationID)
KisDlgImportVideoAnimation(KisMainWindow *m_mainWindow, KisView *m_activeView)
void loadVideoFile(const QString &filename)
RenderedFrames renderFrames(const QDir &directory)
void saveLastUsedConfiguration(QString configurationID, KisPropertiesConfigurationSP config)
KisBasicVideoInfo loadVideoInfo(const QString &inputFile)
void slotDocumentHandlerChanged(int selectedIndex)
static TransferCharacteristics transferCharacteristicsFromName(QString name)
static QJsonObject findFFProbe(const QString &customLocation)
void sigReadSTDOUT(QByteArray stdoutBuffer)
QJsonObject ffmpegProbe(const QString &inputFile, const QString &ffmpegPath, bool batchMode)
static ColorPrimaries colorPrimariesFromName(QString name)
static QJsonObject findFFMpeg(const QString &customLocation)
static QByteArray runProcessAndReturn(const QString &processPath, const QStringList &args, int msecs=FFMPEG_TIMEOUT)
void fileSelected(const QString &fileName)
const KoColorSpace * colorSpace() const
Main window for Krita.
KisImageWSP image() const
Definition KisView.cpp:432
QPointer< KisDocument > document
Definition KisView.cpp:121
virtual quint32 pixelSize() const =0
A dialog base class with standard buttons and predefined layouts.
Definition KoDialog.h:116
void enableButtonOk(bool state)
Definition KoDialog.cpp:606
void setMainWidget(QWidget *widget)
Definition KoDialog.cpp:345
void setButtons(ButtonCodes buttonMask)
Definition KoDialog.cpp:195
void setDefaultButton(ButtonCode id)
Definition KoDialog.cpp:293
@ Ok
Show Ok button. (this button accept()s the dialog; result set to QDialog::Accepted)
Definition KoDialog.h:127
@ Cancel
Show Cancel-button. (this button reject()s the dialog; result set to QDialog::Rejected)
Definition KoDialog.h:130
QString id() const
Definition KoID.cpp:63
#define ppVar(var)
Definition kis_debug.h:155
#define dbgFile
Definition kis_debug.h:53
typedef void(QOPENGLF_APIENTRYP PFNGLINVALIDATEBUFFERDATAPROC)(GLuint buffer)
QIcon loadIcon(const QString &name)
TransferCharacteristics colorTransfer
Statistics fetchMemoryStatistics(KisImageSP image) const
static KisMemoryStatisticsServer * instance()
static QString getTransferCharacteristicName(TransferCharacteristics curve)
getTransferCharacteristicName
static QString getColorPrimariesName(ColorPrimaries primaries)
getColorPrimariesName
virtual QString colorModelID() const
const KoColorProfile * profileFor(const QVector< double > &colorants, ColorPrimaries colorPrimaries, TransferCharacteristics transferFunction) const
profileFor tries to find the profile that matches these characteristics, if no such profile is found,...
static KoColorSpaceRegistry * instance()
const KoColorProfile * p709SRGBProfile() const
QList< int > renderedFrameTargetTimes