File indexing completed on 2024-12-22 04:12:15

0001 /*
0002  *  SPDX-FileCopyrightText: 2021 Know Zero
0003  *  SPDX-FileCopyrightText: 2021 Eoin O'Neill <eoinoneill1991@gmail.com>
0004  *  SPDX-FileCopyrightText: 2021 Emmet O'Neill <emmetoneill.pdx@gmail.com>
0005  *  SPDX-FileCopyrightText: 2021 Wolthera van Hövell tot Westerflier <griffinvalley@gmail.com>
0006  *
0007  *  SPDX-License-Identifier: GPL-3.0-or-later
0008  */
0009 
0010 #include "KisDlgImportVideoAnimation.h"
0011 
0012 #include <QStandardPaths>
0013 #include <QRegExp>
0014 #include <QtMath>
0015 #include <QJsonObject>
0016 #include <QJsonArray>
0017 #include <QMessageBox>
0018 
0019 #include <KFormat>
0020 
0021 #include "KoFileDialog.h"
0022 
0023 #include <KisDocument.h>
0024 #include <KisMainWindow.h>
0025 #include <KisImportExportManager.h>
0026 #include <kis_image.h>
0027 #include <kis_image_animation_interface.h>
0028 #include <kis_memory_statistics_server.h>
0029 #include <kis_icon_utils.h>
0030 
0031 #include "KisFFMpegWrapper.h"
0032 
0033 KisDlgImportVideoAnimation::KisDlgImportVideoAnimation(KisMainWindow *mainWindow, KisView *activeView) :
0034     KoDialog(mainWindow),
0035     m_mainWindow(mainWindow),
0036     m_activeView(activeView)
0037 {
0038     setButtons(Ok | Cancel);
0039     setDefaultButton(Ok);
0040     setWindowTitle(i18nc("@title:window", "Import Video Animation"));
0041 
0042     QWidget *page = new QWidget(this);
0043     m_ui.setupUi(page);
0044     setMainWidget(page);
0045 
0046     toggleInputControls(false);
0047 
0048     KisPropertiesConfigurationSP config = loadLastUsedConfiguration("ANIMATION_EXPORT");
0049     QFileInfo ffmpegFileInfo(config->getPropertyLazy("ffmpeg_path",""));
0050     QFileInfo ffprobeFileInfo(config->getPropertyLazy("ffprobe_path",""));
0051 
0052     dbgFile << "Config data =" << "ffmpeg:" << ffmpegFileInfo.absoluteFilePath() << "ffprobe:" << ffprobeFileInfo.absoluteFilePath();
0053 
0054     QJsonObject ffmpegInfo = KisFFMpegWrapper::findFFMpeg(ffmpegFileInfo.absoluteFilePath());
0055 
0056     if (ffmpegInfo["enabled"].toBool()) {
0057         m_ui.cmbFFMpegLocation->addItem(ffmpegInfo["path"].toString(),ffmpegInfo);
0058 
0059         if (ffprobeFileInfo.filePath().isEmpty())
0060             ffprobeFileInfo.setFile(ffmpegFileInfo.absoluteDir().filePath("ffprobe"));
0061 
0062     } else {
0063         enableButtonOk(false);
0064         m_ui.tabGeneral->setEnabled(false);
0065         QMessageBox::warning(this, i18nc("@title:window", "Krita"), i18n("FFMpeg not found! Please add a path to FFMpeg in the \"Advanced\" tab"));
0066     }
0067 
0068     QJsonObject ffprobeInfo = KisFFMpegWrapper::findFFProbe(ffprobeFileInfo.absoluteFilePath());
0069 
0070     if (ffprobeInfo["enabled"].toBool())
0071         m_ui.cmbFFProbeLocation->addItem(ffprobeInfo["path"].toString(),ffprobeInfo);
0072 
0073     m_ui.cmbFFProbeLocation->addItem("[Disabled]",QJsonObject({{"path",""},{"enabled",false}}));
0074 
0075     m_ui.fileLocation->setMode(KoFileDialog::OpenFile);
0076     m_ui.fileLocation->setMimeTypeFilters(makeVideoMimeTypesList());
0077     m_ui.nextFrameButton->setIcon(KisIconUtils::loadIcon("arrow-right"));
0078     m_ui.prevFrameButton->setIcon(KisIconUtils::loadIcon("arrow-left"));
0079 
0080     m_ui.fpsSpinbox->setValue(24.0);
0081     m_ui.fpsSpinbox->setSuffix(i18nc("FPS as a unit following a value, like 60 FPS", " FPS"));
0082 
0083     m_ui.frameSkipSpinbox->setValue(1);
0084     m_ui.frameSkipSpinbox->setRange(1,20);
0085 
0086     m_ui.startExportingAtSpinbox->setValue(0.0);
0087     m_ui.startExportingAtSpinbox->setRange(0.0, 9999.0);
0088     m_ui.startExportingAtSpinbox->setSuffix(i18nc("Second as a unit following a value, like 60 s", " s"));
0089 
0090     m_ui.videoPreviewSlider->setTickInterval(1);
0091     m_ui.videoPreviewSlider->setValue(0);
0092 
0093     m_ui.exportDurationSpinbox->setValue(3.0);
0094     m_ui.exportDurationSpinbox->setSuffix(i18nc("Second as a unit following a value, like 60 s", " s"));
0095 
0096     m_ui.lblWarning->hide();
0097 
0098     connect(m_ui.cmbDocumentHandler, SIGNAL(currentIndexChanged(int)), SLOT(slotDocumentHandlerChanged(int)));
0099 
0100     m_ui.cmbDocumentHandler->addItem(i18nc("Import video to New Document", "New Document"), "0");
0101 
0102     if (m_activeView && m_activeView->document()) {
0103         m_ui.cmbDocumentHandler->addItem(i18nc("Import video to Current Document", "Current Document"), "1");
0104         m_ui.cmbDocumentHandler->setCurrentIndex(1);
0105         m_ui.fpsDocumentLabel->setText(i18nc("Video importer: fps of the document you're importing into"
0106                                              , "<small>Document:\n %1 FPS</small>"
0107                                              , QString::number(m_activeView->document()->image()->animationInterface()->framerate()))
0108                                        );
0109     }
0110 
0111     m_ui.documentWidthSpinbox->setValue(0);
0112     m_ui.documentHeightSpinbox->setValue(0);
0113     m_ui.documentWidthSpinbox->setRange(1,100000);
0114     m_ui.documentHeightSpinbox->setRange(1,100000);
0115 
0116     m_ui.videoWidthSpinbox->setValue(0);
0117     m_ui.videoHeightSpinbox->setValue(0);
0118     m_ui.videoWidthSpinbox->setRange(1,100000);
0119     m_ui.videoHeightSpinbox->setRange(1,100000);
0120 
0121     m_ui.sensitivitySpinbox->setValue(50.0f);
0122 
0123     m_ui.cmbVideoScaleFilter->addItem(i18n("Bicubic"), "bicubic");
0124     m_ui.cmbVideoScaleFilter->addItem(i18n("Bilinear"), "bilinear");
0125     m_ui.cmbVideoScaleFilter->addItem(i18n("Lanczos3"), "lanczos");
0126     m_ui.cmbVideoScaleFilter->addItem(i18n("Nearest Neighbor"), "neighbor");
0127     m_ui.cmbVideoScaleFilter->addItem(i18nc("An interpolation method", "Spline"), "spline");
0128 
0129     m_ui.tabWidget->setCurrentIndex(0);
0130 
0131     m_videoSliderTimer = new QTimer(this);
0132     m_videoSliderTimer->setSingleShot(true);
0133 
0134     connect(m_videoSliderTimer, SIGNAL(timeout()), SLOT(slotVideoTimerTimeout()));
0135 
0136     m_currentFrame = 0;
0137     CurrentFrameChanged(0);
0138 
0139     connect(m_ui.fileLocation, &KisFileNameRequester::fileSelected, this, &KisDlgImportVideoAnimation::loadVideoFile);
0140     connect(m_ui.nextFrameButton, SIGNAL(clicked()), SLOT(slotNextFrame()));
0141     connect(m_ui.prevFrameButton, SIGNAL(clicked()), SLOT(slotPrevFrame()));
0142     connect(m_ui.currentFrameNumberInput, SIGNAL(valueChanged(int)), SLOT(slotFrameNumberChanged(int)));
0143     connect(m_ui.videoPreviewSlider, SIGNAL(valueChanged(int)), SLOT(slotVideoSliderChanged()));
0144 
0145     connect(m_ui.ffprobePickerButton, SIGNAL(clicked()), SLOT(slotFFProbeFile()));
0146     connect(m_ui.ffmpegPickerButton, SIGNAL(clicked()), SLOT(slotFFMpegFile()));
0147 
0148     connect(m_ui.exportDurationSpinbox, SIGNAL(valueChanged(qreal)), SLOT(slotImportDurationChanged(qreal)));
0149 }
0150 
0151 KisPropertiesConfigurationSP KisDlgImportVideoAnimation::loadLastUsedConfiguration(QString configurationID) {
0152     KisConfig globalConfig(true);
0153     return globalConfig.exportConfiguration(configurationID);
0154 }
0155 
0156 void KisDlgImportVideoAnimation::saveLastUsedConfiguration(QString configurationID, KisPropertiesConfigurationSP config)
0157 {
0158     KisConfig globalConfig(false);
0159     globalConfig.setExportConfiguration(configurationID, config);
0160 }
0161 
0162 float rerange(float value, float oldMin, float oldMax, float newMin, float newMax) {
0163     return ((value - oldMin) / (oldMax - oldMin)) * (newMax - newMin) + newMin;
0164 }
0165 
0166 RenderedFrames KisDlgImportVideoAnimation::renderFrames(const QDir& directory)
0167 {
0168     RenderedFrames info;
0169     QStringList &frameFileList = info.renderedFrameFiles;
0170     QList<int> &frameTimeList = info.renderedFrameTargetTimes;
0171 
0172     if ( !directory.mkpath(".") ) {
0173         QMessageBox::warning(this, i18nc("@title:window", "Krita"), i18n("Failed to create a work directory, make sure you have write permission"));
0174         return info;
0175     }
0176 
0177     QStringList args;
0178     const float exportDuration = m_ui.exportDurationSpinbox->value();
0179     const float fps = m_ui.fpsSpinbox->value();
0180     const float nDuplicateSensitivity = m_ui.sensitivitySpinbox->value() / m_ui.sensitivitySpinbox->maximum();
0181 
0182     if (exportDuration / fps > 100.0) {
0183         if (QMessageBox::warning(this, i18nc("Title for a messagebox", "Krita"),
0184                              i18n("Warning: you are trying to import more than 100 frames into Krita.\n\n"
0185                                   "This means you might be overloading your system.\n"
0186                                   "If you want to edit a clip larger than 100 frames, consider using a real video editor, like Kdenlive (https://kdenlive.org)."),
0187                                  QMessageBox::Ok | QMessageBox::Cancel,
0188                                  QMessageBox::Cancel) == QMessageBox::Cancel) {
0189             return info;
0190         }
0191     }
0192 
0193 
0194     args << "-ss" << QString::number(m_ui.startExportingAtSpinbox->value())
0195          << "-i" << m_videoInfo.file;
0196 
0197     const float sceneFiltrationFilterThreshold = rerange( 1 - nDuplicateSensitivity, 0.0f, 1.0f, 0.0005f, 0.2f);
0198 
0199     if (m_ui.optionFilterDuplicates->isChecked()) {
0200          args << "-filter:v" << QString("select=gt(scene\\,%1)+eq(n\\,0)").arg(sceneFiltrationFilterThreshold)
0201               << "-vsync" << "0";
0202     }
0203 
0204     args << "-t" << QString::number(exportDuration)
0205          << "-r" << QString::number(fps);
0206 
0207     if ( m_videoInfo.width != m_ui.videoWidthSpinbox->value() || m_videoInfo.height != m_ui.videoHeightSpinbox->value() ) {
0208         args << "-vf" <<  QString("scale=w=")
0209                                     .append(QString::number(m_ui.videoWidthSpinbox->value()))
0210                                     .append(":h=")
0211                                     .append(QString::number(m_ui.videoHeightSpinbox->value()))
0212                                     .append(":flags=")
0213                                     .append(m_ui.cmbVideoScaleFilter->currentData().toString());
0214     }
0215 
0216     QJsonObject ffmpegInfo = m_ui.cmbFFMpegLocation->currentData().toJsonObject();
0217     QJsonObject ffprobeInfo = m_ui.cmbFFProbeLocation->currentData().toJsonObject();
0218 
0219     KisPropertiesConfigurationSP config = loadLastUsedConfiguration("ANIMATION_EXPORT");
0220 
0221     config->setProperty("ffmpeg_path", ffmpegInfo["path"].toString());
0222     config->setProperty("ffprobe_path", ffprobeInfo["path"].toString());
0223 
0224     saveLastUsedConfiguration("ANIMATION_EXPORT", config);
0225 
0226 
0227     {
0228         KisFFMpegWrapperSettings ffmpegSettings;
0229 
0230         ffmpegSettings.processPath = ffmpegInfo["path"].toString();
0231         ffmpegSettings.args = args;
0232         ffmpegSettings.outputFile = directory.filePath("output_%04d.png");
0233         ffmpegSettings.logPath = QDir::tempPath() + QDir::separator() + "krita" + QDir::separator() + "ffmpeg.log";
0234         ffmpegSettings.totalFrames = qCeil(exportDuration * fps);
0235         ffmpegSettings.progressMessage = i18nc("FFMPEG animated video import message. arg1: frame progress number. arg2: file suffix."
0236                                                , "Extracted %1 frames from %2 video.", "[progress]", "[suffix]");
0237 
0238         QScopedPointer<KisFFMpegWrapper> ffmpeg(new KisFFMpegWrapper(this));
0239         ffmpeg->startNonBlocking(ffmpegSettings);
0240         ffmpeg->waitForFinished();
0241 
0242         frameFileList = directory.entryList(QStringList() << "output_*.png",QDir::Files);
0243         frameFileList.replaceInStrings("output_", directory.absolutePath() + QDir::separator() + "output_");
0244 
0245         dbgFile << "Import frames list:" << frameFileList;
0246     }
0247 
0248     if (m_ui.optionFilterDuplicates->isChecked()){
0249         KisFFMpegWrapperSettings ffmpegSettings;
0250         ffmpegSettings.defaultPrependArgs.clear();
0251         ffmpegSettings.processPath = ffprobeInfo["path"].toString();
0252 
0253         QString filter = "movie=" + m_videoInfo.file + QString(",setpts=N+1,select=gt(scene\\,%1)").arg(sceneFiltrationFilterThreshold);
0254         ffmpegSettings.args = QStringList() << "-select_streams" << "v"
0255                                             << "-show_entries" << "frame=pkt_pts"
0256                                             << "-of" << "compact=p=0:nk=1"
0257                                             << "-f" << "lavfi" << filter;
0258 
0259 
0260         QScopedPointer<KisFFMpegWrapper> ffmpeg(new KisFFMpegWrapper(this));
0261         frameTimeList = {0}; // We always have a frame 0 here...
0262         connect(ffmpeg.data(), &KisFFMpegWrapper::sigReadSTDOUT, [&](QByteArray arr) {
0263             QString out = QString(arr);
0264 
0265 #if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
0266             QStringList integerOuts = out.split("\n", Qt::SkipEmptyParts);
0267 #else
0268             QStringList integerOuts = out.split("\n", QString::SkipEmptyParts);
0269 #endif
0270             Q_FOREACH(const QString& str, integerOuts){
0271                 bool ok = false;
0272                 const int value = str.toUInt(&ok);
0273                 if (ok) {
0274                     frameTimeList.push_back(value);
0275                 }
0276             }
0277         });
0278         ffmpeg->startNonBlocking(ffmpegSettings);
0279         ffmpeg->waitForFinished();
0280 
0281         dbgFile << "Assign to frames:" << ppVar(frameTimeList);
0282     }
0283 
0284     if ( frameFileList.isEmpty() ) {
0285          QMessageBox::critical(this, i18nc("@title:window", "Krita"), i18n("Failed to export frames from video"));
0286     }
0287 
0288     return info;
0289 }
0290 
0291 
0292 QStringList KisDlgImportVideoAnimation::documentInfo() {
0293     QStringList documentInfoList;
0294 
0295     // We're looking for a possible profile here, otherwise it gets generated. Then we get the name.
0296     QString profileColorSpace = RGBAColorModelID.id();
0297     QString profileName = KoColorSpaceRegistry::instance()->p709SRGBProfile()->name();
0298     if (m_videoInfo.colorTransfer != TRC_UNSPECIFIED && m_videoInfo.colorPrimaries != PRIMARIES_UNSPECIFIED) {
0299         const KoColorProfile *profile = KoColorSpaceRegistry::instance()->profileFor(QVector<double>(), m_videoInfo.colorPrimaries, m_videoInfo.colorTransfer);
0300         profileName = profile->name();
0301         profileColorSpace = profile->colorModelID();
0302     }
0303 
0304     documentInfoList << QString::number(m_ui.frameSkipSpinbox->value())
0305                      << QString::number(m_ui.fpsSpinbox->value())
0306                      << QString::number(qCeil(m_ui.fpsSpinbox->value() * m_ui.exportDurationSpinbox->value()))
0307                      << m_videoInfo.file;
0308 
0309     if ( m_ui.cmbDocumentHandler->currentIndex() == 0 ) {
0310         documentInfoList << "0"
0311                          << QString::number(m_ui.documentWidthSpinbox->value())
0312                          << QString::number(m_ui.documentHeightSpinbox->value())
0313                          << QString::number(72)
0314                          << profileColorSpace
0315                          << m_videoInfo.colorDepth
0316                          << profileName;
0317     } else {
0318         documentInfoList << "1";
0319     }
0320 
0321     return documentInfoList;
0322 }
0323 
0324 QStringList KisDlgImportVideoAnimation::makeVideoMimeTypesList()
0325 {
0326     QStringList supportedMimeTypes = QStringList();
0327     supportedMimeTypes << "video/x-matroska";
0328     supportedMimeTypes << "image/gif";
0329     supportedMimeTypes << "image/apng";
0330     supportedMimeTypes << "image/png";
0331     supportedMimeTypes << "video/quicktime"; // MOV
0332     supportedMimeTypes << "video/ogg";
0333     supportedMimeTypes << "video/mp4";
0334     supportedMimeTypes << "video/mpeg";
0335     supportedMimeTypes << "video/webm";
0336 
0337     // All files
0338     supportedMimeTypes << "application/octet-stream";
0339 
0340     return supportedMimeTypes;
0341 }
0342 
0343 QStringList KisDlgImportVideoAnimation::showOpenFileDialog()
0344 {
0345     KoFileDialog dialog(this, KoFileDialog::ImportFiles, "OpenDocument");
0346     dialog.setDefaultDir(QStandardPaths::writableLocation(QStandardPaths::PicturesLocation));
0347     dialog.setMimeTypeFilters( makeVideoMimeTypesList() );
0348     dialog.setCaption(i18n("Select your Video File"));
0349 
0350     return dialog.filenames();
0351 }
0352 
0353 
0354 void KisDlgImportVideoAnimation::toggleInputControls(bool toggleBool)
0355 {
0356     enableButtonOk(toggleBool);
0357     m_ui.videoPreviewSlider->setEnabled(toggleBool);
0358     m_ui.currentFrameNumberInput->setEnabled(toggleBool);
0359     m_ui.nextFrameButton->setEnabled(toggleBool);
0360     m_ui.prevFrameButton->setEnabled(toggleBool);
0361 }
0362 
0363 void KisDlgImportVideoAnimation::loadVideoFile(const QString &filename)
0364 {
0365     const QFileInfo resultFileInfo(filename);
0366     const QDir videoDir(resultFileInfo.absolutePath());
0367 
0368     m_videoInfo = loadVideoInfo(filename);
0369 
0370     if ( m_videoInfo.file.isEmpty() ) return;
0371 
0372     QStringList textInfo;
0373 
0374     textInfo.append(i18nc("video importer: video file statistics", "Width: %1 px", QString::number(m_videoInfo.width)));
0375     textInfo.append(i18nc("video importer: video file statistics", "Height: %1 px", QString::number(m_videoInfo.height)));
0376 
0377     if (m_videoInfo.colorPrimaries != PRIMARIES_UNSPECIFIED && m_videoInfo.colorTransfer != TRC_UNSPECIFIED) {
0378         textInfo.append(i18nc("video importer: video file statistics"
0379                               , "Color Primaries: %1"
0380                               , KoColorProfile::getColorPrimariesName(m_videoInfo.colorPrimaries)));
0381         textInfo.append(i18nc("video importer: video file statistics"
0382                               , "Color Transfer: %1"
0383                               , KoColorProfile::getTransferCharacteristicName(m_videoInfo.colorTransfer)));
0384     }
0385     textInfo.append(i18nc("video importer: video file statistics", "Duration: %1 s", QString::number(m_videoInfo.duration, 'f', 2)));
0386     textInfo.append(i18nc("video importer: video file statistics", "Frames: %1", QString::number(m_videoInfo.frames)));
0387     textInfo.append(i18nc("video importer: video file statistics", "FPS: %1", QString::number(m_videoInfo.fps)));
0388 
0389 
0390     if ( m_videoInfo.hasOverriddenFPS ) {
0391         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>"));
0392     }
0393 
0394     m_ui.fpsSpinbox->setValue( qCeil(m_videoInfo.fps) );
0395     m_ui.fileLoadedDetails->setText(textInfo.join("\n"));
0396 
0397 
0398     m_ui.videoPreviewSlider->setRange(0, m_videoInfo.frames);
0399     m_ui.currentFrameNumberInput->setRange(0, m_videoInfo.frames);
0400     m_ui.exportDurationSpinbox->setRange(0, 9999.0);
0401 
0402     if (m_ui.cmbDocumentHandler->currentIndex() == 0) {
0403         m_ui.documentWidthSpinbox->setValue(m_videoInfo.width);
0404         m_ui.documentHeightSpinbox->setValue(m_videoInfo.height);
0405     }
0406 
0407     m_ui.videoWidthSpinbox->setValue(m_videoInfo.width);
0408     m_ui.videoHeightSpinbox->setValue(m_videoInfo.height);
0409 
0410     m_ui.exportDurationSpinbox->setValue(m_videoInfo.duration);
0411 
0412     CurrentFrameChanged(0);
0413 
0414     if ( m_videoInfo.file.isEmpty() ) {
0415         toggleInputControls(false);
0416     } else {
0417         toggleInputControls(true);
0418         updateVideoPreview();
0419     }
0420 
0421 
0422 }
0423 
0424 
0425 void KisDlgImportVideoAnimation::updateVideoPreview()
0426 {
0427     float currentDuration = ( m_videoInfo.stream != -1 ) ?  (m_currentFrame / m_videoInfo.fps):0;
0428     QStringList args;
0429 
0430     args << "-ss" << QString::number(currentDuration)
0431          << "-i" << m_videoInfo.file
0432          << "-v" << "quiet"
0433          << "-vframes" << "1"
0434          << "-vcodec" << "mjpeg"
0435          << "-f" << "image2pipe"
0436          << "pipe:1";
0437 
0438     struct KisFFMpegWrapperSettings ffmpegSettings;
0439 
0440     QJsonObject ffmpegInfo = m_ui.cmbFFMpegLocation->currentData().toJsonObject();
0441     QByteArray byteImage = KisFFMpegWrapper::runProcessAndReturn(ffmpegInfo["path"].toString(), args, FFMPEG_TIMEOUT);
0442 
0443     if ( byteImage.isEmpty() ) {
0444         m_ui.thumbnailImageHolder->setText( m_videoInfo.frames == m_currentFrame ? "End of Video":"No Preview" );
0445     } else {
0446         QPixmap thumbnailPixmap;
0447         thumbnailPixmap.loadFromData(byteImage,"JFIF");
0448 
0449         m_ui.thumbnailImageHolder->clear();
0450         const QSize previewSize =
0451             m_ui.thumbnailImageHolder->contentsRect().size() * m_ui.thumbnailImageHolder->devicePixelRatioF();
0452         QPixmap img = thumbnailPixmap.scaled(previewSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
0453         img.setDevicePixelRatio(m_ui.thumbnailImageHolder->devicePixelRatioF());
0454         m_ui.thumbnailImageHolder->setPixmap(img);
0455     }
0456 }
0457 
0458 
0459 void KisDlgImportVideoAnimation::slotVideoTimerTimeout()
0460 {
0461     updateVideoPreview();
0462 }
0463 
0464 void KisDlgImportVideoAnimation::slotImportDurationChanged(qreal time)
0465 {
0466     const KisMemoryStatisticsServer::Statistics stats =
0467             KisMemoryStatisticsServer::instance()
0468             ->fetchMemoryStatistics(m_activeView ? m_activeView->image() : 0);
0469     const KFormat format;
0470 
0471     const int resolution = m_videoInfo.width * m_videoInfo.height;
0472     quint32 pixelSize = 4; //how do we even go about getting the bitdepth???
0473     if (m_activeView && m_ui.cmbDocumentHandler->currentIndex() > 0) {
0474         pixelSize = m_activeView->image()->colorSpace()->pixelSize() * 4;
0475     } else if (m_videoInfo.colorDepth == "U16"){
0476         pixelSize = 8;
0477     }
0478     const qint64 frames = std::lround(qreal(m_videoInfo.fps) * time + 2);
0479     // 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.
0480     // It's more efficient therefore to calculate the maximum amount of frames possible.
0481 
0482     const qint64 maxFrames = stats.totalMemoryLimit / resolution / pixelSize;
0483 
0484     QStringList warnings;
0485 
0486     const QString text_frames = i18nc("part of warning in video importer."
0487                                 , "<b>Warning:</b> you are trying to import %1 frames, the maximum amount you can import is %2."
0488                                 , frames
0489                                 , maxFrames);
0490 
0491     QString text_memory;
0492 
0493     const QString text_video_editor = i18nc("part of warning in video importer.",
0494                                       "Use a <a href=\"https://kdenlive.org\">video editor</a> instead!");
0495 
0496     if (maxFrames < frames) {
0497         warnings.append(text_frames);
0498         text_memory = i18nc("part of warning in video importer."
0499                             , "You do not have enough memory to load this many frames, the computer will be overloaded.");
0500         warnings.insert(0, "<span style=\"color:#ff692e;\">");
0501         warnings.append(text_memory);
0502         warnings.append(text_video_editor);
0503         m_ui.lblWarning->setVisible(true);
0504     } else if (maxFrames < frames * 2) {
0505         warnings.append(text_frames);
0506         text_memory = i18nc("part of warning in video importer."
0507                             , "This will take over half the available memory, editing will be difficult.");
0508         warnings.insert(0, "<span style=\"color:#ffee00;\">");
0509         warnings.append(text_memory);
0510         warnings.append(text_video_editor);
0511         m_ui.lblWarning->setVisible(true);
0512     } else if (m_videoInfo.colorTransfer == TRC_ITU_R_BT_2100_0_HLG
0513                || m_videoInfo.colorTransfer == TRC_SMPTE_ST_428_1) {
0514         warnings.append(text_frames);
0515         QString text_trc =  i18nc("part of warning in video importer."
0516                                   , "Krita does not support the video transfer curve (%1), it will be loaded as linear."
0517                                   , KoColorProfile::getTransferCharacteristicName(m_videoInfo.colorTransfer));
0518         warnings.append(text_trc);
0519     }
0520 
0521     if (warnings.isEmpty()) {
0522         m_ui.lblWarning->setVisible(false);
0523     } else {
0524         m_ui.lblWarning->setText(warnings.join(" "));
0525         m_ui.lblWarning->setPixmap(
0526             m_ui.lblWarning->style()->standardIcon(QStyle::SP_MessageBoxWarning).pixmap(QSize(32, 32)));
0527         m_ui.lblWarning->setVisible(true);
0528     }
0529 }
0530 
0531 void KisDlgImportVideoAnimation::slotNextFrame()
0532 {
0533     CurrentFrameChanged(m_currentFrame+1);
0534 }
0535 
0536 void KisDlgImportVideoAnimation::slotPrevFrame()
0537 {
0538     CurrentFrameChanged(m_currentFrame-1);
0539 }
0540 
0541 void KisDlgImportVideoAnimation::slotFrameNumberChanged(int frame)
0542 {
0543     CurrentFrameChanged(frame);
0544 }
0545 
0546 
0547 void KisDlgImportVideoAnimation::slotFFProbeFile()
0548 {
0549     KoFileDialog dialog(this, KoFileDialog::OpenFile, i18n("Open FFProbe"));
0550     dialog.setDefaultDir(QStandardPaths::standardLocations(QStandardPaths::ApplicationsLocation).last());
0551     dialog.setCaption(i18n("Open FFProbe"));
0552 
0553     QStringList filenames = dialog.filenames();
0554 
0555     if (!filenames.isEmpty()) {
0556         QJsonObject ffprobeInfo = KisFFMpegWrapper::findFFProbe(filenames[0]);
0557 
0558         if (ffprobeInfo["enabled"].toBool() && ffprobeInfo["custom"].toBool()) {
0559             m_ui.cmbFFProbeLocation->addItem(filenames[0],ffprobeInfo);
0560             m_ui.cmbFFProbeLocation->setCurrentText(filenames[0]);
0561             return;
0562         }
0563         QMessageBox::warning(this, i18nc("@title:window", "Krita"), i18n("FFProbe is invalid!"));
0564     }
0565 
0566 }
0567 
0568 void KisDlgImportVideoAnimation::slotFFMpegFile()
0569 {
0570     KoFileDialog dialog(this, KoFileDialog::OpenFile, i18n("Open FFMpeg"));
0571     dialog.setDefaultDir(QStandardPaths::standardLocations(QStandardPaths::ApplicationsLocation).last());
0572     dialog.setCaption(i18n("Open FFMpeg"));
0573 
0574     QStringList filenames = dialog.filenames();
0575 
0576     if (!filenames.isEmpty()) {
0577         QJsonObject ffmpegInfo = KisFFMpegWrapper::findFFMpeg(filenames[0]);
0578 
0579         if (ffmpegInfo["enabled"].toBool()) {
0580             if (ffmpegInfo["custom"].toBool()) {
0581                 m_ui.cmbFFMpegLocation->addItem(filenames[0],ffmpegInfo);
0582                 m_ui.cmbFFMpegLocation->setCurrentText(filenames[0]);
0583             } else {
0584                 QMessageBox::warning(this, i18nc("@title:window", "Krita"), i18n("FFMpeg is invalid!"));
0585             }
0586             m_ui.tabGeneral->setEnabled(true);
0587             return;
0588         }
0589 
0590         m_ui.tabGeneral->setEnabled(false);
0591         QMessageBox::critical(this, i18nc("@title:window", "Krita"), i18n("No FFMpeg found!"));
0592     }
0593 
0594 }
0595 
0596 void KisDlgImportVideoAnimation::slotDocumentHandlerChanged(int selectedIndex)
0597 {
0598     bool toggleDocumentOptions = selectedIndex == 0;
0599 
0600     if (toggleDocumentOptions) {
0601         m_ui.fpsDocumentLabel->setText(" ");
0602 
0603         if (m_videoInfo.stream != -1) {
0604             m_ui.documentWidthSpinbox->setValue(m_videoInfo.width);
0605             m_ui.documentHeightSpinbox->setValue(m_videoInfo.height);
0606         }
0607 
0608     } else if (m_activeView) {
0609         m_ui.fpsDocumentLabel->setText(i18nc("Video importer: fps of the document you're importing into"
0610                                              , "<small>Document:\n %1 FPS</small>"
0611                                              , QString::number(m_activeView->document()->image()->animationInterface()->framerate()))
0612                                        );
0613     }
0614 
0615     m_ui.optionsDocumentGroup->setEnabled(toggleDocumentOptions);
0616 
0617 }
0618 
0619 void KisDlgImportVideoAnimation::slotVideoSliderChanged()
0620 {
0621     CurrentFrameChanged(m_ui.videoPreviewSlider->value());
0622 
0623     if (!m_videoSliderTimer->isActive()) m_videoSliderTimer->start(300);
0624 
0625 }
0626 
0627 void KisDlgImportVideoAnimation::CurrentFrameChanged(int frame)
0628 {
0629     float currentSeconds = 0;
0630 
0631     // update frame and seconds model data if they have changed
0632     if (m_currentFrame != frame ) {
0633         dbgFile << "Frame change to:" << frame;
0634         m_currentFrame = frame;
0635         currentSeconds = m_currentFrame / m_videoInfo.fps;
0636     }
0637 
0638     // update UI components if they are out of sync
0639     if (m_currentFrame != m_ui.currentFrameNumberInput->value())
0640         m_ui.currentFrameNumberInput->setValue(m_currentFrame);
0641 
0642     if (m_currentFrame != m_ui.videoPreviewSlider->value())
0643         m_ui.videoPreviewSlider->setValue(m_currentFrame);
0644 
0645     m_ui.videoPreviewSliderValueLabel->setText( QString::number(currentSeconds, 'f', 2).append(i18nc("Second as a unit following a value, like 60 s", " s")) );
0646 }
0647 
0648 KisBasicVideoInfo KisDlgImportVideoAnimation::loadVideoInfo(const QString &inputFile)
0649 {
0650     QJsonObject ffmpegInfo = m_ui.cmbFFMpegLocation->currentData().toJsonObject();
0651     QJsonObject ffprobeInfo = m_ui.cmbFFProbeLocation->currentData().toJsonObject();
0652     struct KisBasicVideoInfo videoInfoData;
0653 
0654     KisFFMpegWrapper *ffprobe = new KisFFMpegWrapper(this);
0655     QJsonObject ffprobeJsonObj;
0656 
0657     std::function<void(void)> warnFFmpegFormatSupport = [this](){
0658         QMessageBox::warning(this, i18nc("@title:window", "Krita"), i18n("Your FFMpeg version does not support this format"));
0659     };
0660 
0661     if (ffprobeInfo["enabled"].toBool()) {
0662         ffprobeJsonObj = ffprobe->ffprobe(inputFile, ffprobeInfo["path"].toString());
0663     }
0664 
0665     // Attempt manual probing with ffmpeg itself if ffprobe is disable or if something went wrong with ffprobe.
0666     if ( !ffprobeInfo["enabled"].toBool() || ffprobeJsonObj["error"].toInt() == FFProbeErrorCodes::INVALID_JSON ) {
0667         KisFFMpegWrapper *ffmpeg = new KisFFMpegWrapper(this);
0668 
0669         ffprobeJsonObj = ffmpeg->ffmpegProbe(inputFile, ffmpegInfo["path"].toString(), false);
0670 
0671         dbgFile << "ffmpeg probe1" << ffprobeJsonObj;
0672 
0673         QJsonObject ffprobeProgress = ffprobeJsonObj["progress"].toObject();
0674 
0675         videoInfoData.frames = ffprobeProgress["frame"].toString().toInt();
0676     }
0677 
0678     if ( ffprobeJsonObj["error"].toInt() == FFProbeErrorCodes::UNSUPPORTED_CODEC) {
0679         // If, after all of that, we still don't determine a supported codec then we'll back out.
0680         warnFFmpegFormatSupport();
0681         return {};
0682     } else if ( ffprobeJsonObj["error"].toInt() == FFProbeErrorCodes::NONE ) {
0683 
0684         QJsonObject ffprobeFormat = ffprobeJsonObj["format"].toObject();
0685         QJsonArray ffprobeStreams = ffprobeJsonObj["streams"].toArray();
0686 
0687         videoInfoData.file = inputFile;
0688         for (const QJsonValueRef &streamItemRef : ffprobeStreams) {
0689             QJsonObject streamItemObj = streamItemRef.toObject();
0690 
0691             if ( streamItemObj["codec_type"].toString() == "video" ) {
0692                 videoInfoData.stream = streamItemObj["index"].toInt();
0693                 break;
0694             }
0695         }
0696 
0697         if ( videoInfoData.stream == -1 ) {
0698             QMessageBox::warning(this, i18nc("@title:window", "Krita"), i18n("No video stream could be found!"));
0699             return {};
0700         }
0701 
0702         const QJsonObject ffprobeSelectedStream = ffprobeStreams[videoInfoData.stream].toObject();
0703 
0704         const QJsonObject decoders = ffmpegInfo.value("codecs").toObject();
0705 
0706         const QString codecName = ffprobeSelectedStream["codec_name"].toString();
0707 
0708         const auto decoder = decoders.constFind(codecName);
0709 
0710         if (decoder == decoders.constEnd() ) {
0711             dbgFile << "Codec missing or unsupported:" << codecName;
0712             warnFFmpegFormatSupport();
0713             return {};
0714         } else if (!decoder->toObject().value("decoding").toBool()) {
0715             dbgFile << "Codec not supported for decoding:" << codecName;
0716             warnFFmpegFormatSupport();
0717             return {};
0718         }
0719 
0720         videoInfoData.width = ffprobeSelectedStream["width"].toInt();
0721         videoInfoData.height = ffprobeSelectedStream["height"].toInt();
0722         videoInfoData.encoding = ffprobeSelectedStream["codec_name"].toString();
0723         videoInfoData.colorPrimaries = KisFFMpegWrapper::colorPrimariesFromName(ffprobeSelectedStream["color_primaries"].toString());
0724         videoInfoData.colorTransfer = KisFFMpegWrapper::transferCharacteristicsFromName(ffprobeSelectedStream["color_transfer"].toString());
0725 
0726         // bits_per_raw_sample was introduced in 2014.
0727         if (ffprobeSelectedStream.value("bits_per_raw_sample").toInt() > 8) {
0728             videoInfoData.colorDepth = Integer16BitsColorDepthID.id();
0729         } else {
0730             videoInfoData.colorDepth = Integer8BitsColorDepthID.id();
0731         }
0732 
0733         // frame rate comes back in odd format...so we need to do a bit of work so it is more usable.
0734         // data will come back like "50/3"
0735         QStringList rawFrameRate = ffprobeSelectedStream["r_frame_rate"].toString().split('/');
0736 
0737         if (!rawFrameRate.isEmpty())
0738             videoInfoData.fps = qCeil(rawFrameRate[0].toFloat() / rawFrameRate[1].toFloat());
0739 
0740         if ( !ffprobeSelectedStream["nb_frames"].isNull() ) {
0741             videoInfoData.frames = ffprobeSelectedStream["nb_frames"].toString().toInt();
0742         }
0743 
0744         // Get duration from stream, if it doesn't exist such as on VP8 and VP9, try to get it out of format
0745         if ( !ffprobeSelectedStream["duration"].isNull() ) {
0746             videoInfoData.duration = ffprobeSelectedStream["duration"].toString().toFloat();
0747         } else if ( !ffprobeFormat["duration"].isNull() ) {
0748             videoInfoData.duration = ffprobeFormat["duration"].toString().toFloat();
0749         } else if ( videoInfoData.frames ) {
0750             videoInfoData.duration = videoInfoData.frames / videoInfoData.fps;
0751         }
0752 
0753         dbgFile << "Initial video info from probe: "
0754                 << "stream:" << videoInfoData.stream
0755                 << "frames:" << videoInfoData.frames
0756                 << "duration:" << videoInfoData.duration
0757                 << "fps:" << videoInfoData.fps
0758                 << "encoding:" << videoInfoData.encoding;
0759 
0760         if ( !videoInfoData.frames && !videoInfoData.duration ) {
0761             KisFFMpegWrapper *ffmpeg = new KisFFMpegWrapper(this);
0762 
0763             QJsonObject ffmpegJsonObj = ffmpeg->ffmpegProbe(inputFile, ffmpegInfo["path"].toString(), false);
0764 
0765             dbgFile << "ffmpeg probe2" << ffmpegJsonObj;
0766 
0767             if ( ffprobeJsonObj["error"].toInt() != FFProbeErrorCodes::NONE ) {
0768                 QMessageBox::warning(this, i18nc("@title:window", "Krita"), i18n("Failed to load video information"));
0769                 return {};
0770             }
0771 
0772             QJsonObject ffmpegProgressJsonObj = ffmpegJsonObj["progress"].toObject();
0773             float ffmpegFPS = ffmpegProgressJsonObj["ffmpeg_fps"].toString().toFloat();
0774 
0775             videoInfoData.frames = ffmpegProgressJsonObj["frame"].toString().toInt();
0776 
0777             if (ffmpegFPS > 0 && videoInfoData.frames) {
0778                 videoInfoData.duration = videoInfoData.frames / ffmpegFPS;
0779                 videoInfoData.fps = ffmpegFPS;
0780             } else {
0781                 videoInfoData.duration = ffmpegProgressJsonObj["out_time_ms"].toString().toFloat() / 1000000;
0782                 if (videoInfoData.frames) videoInfoData.fps =  videoInfoData.frames / videoInfoData.duration;
0783             }
0784 
0785         }
0786     }
0787 
0788     // if there is no data on frames but duration and fps exists like OGV, try to estimate it
0789     if ( videoInfoData.fps && videoInfoData.duration && !videoInfoData.frames ) {
0790         videoInfoData.frames = qCeil( videoInfoData.fps * videoInfoData.duration );
0791     }
0792 
0793     dbgFile << "Final video info from probe: "
0794             << "stream:" << videoInfoData.stream
0795             << "frames:" << videoInfoData.frames
0796             << "duration:" << videoInfoData.duration
0797             << "fps:" << videoInfoData.fps
0798             << "encoding:" << videoInfoData.encoding;
0799 
0800     const float calculatedFrameRateByDuration = videoInfoData.frames / videoInfoData.duration;
0801     const int frameRateDifference = qAbs(videoInfoData.fps - calculatedFrameRateByDuration);
0802 
0803     if (frameRateDifference > 1) {
0804         // something is not right with the frame rate with this file, so let's use our calculated value
0805         // to make  sure we get the whole duration
0806         videoInfoData.hasOverriddenFPS = true;
0807         videoInfoData.fps = calculatedFrameRateByDuration;
0808     } else {
0809         videoInfoData.hasOverriddenFPS = false;
0810     }
0811 
0812     return videoInfoData;
0813 }
0814 
0815