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