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