File indexing completed on 2024-12-22 04:12:16
0001 /* 0002 * SPDX-FileCopyrightText: 2016 Dmitry Kazakov <dimula73@gmail.com> 0003 * 0004 * SPDX-License-Identifier: GPL-2.0-or-later 0005 */ 0006 0007 #include "KisVideoSaver.h" 0008 0009 #include <QDebug> 0010 #include <QFileInfo> 0011 #include <QFileSystemWatcher> 0012 #include <QProcess> 0013 #include <QProgressDialog> 0014 #include <QEventLoop> 0015 #include <QTemporaryFile> 0016 #include <QTemporaryDir> 0017 #include <QTime> 0018 0019 #include <KisDocument.h> 0020 #include <kis_image.h> 0021 #include <kis_image_animation_interface.h> 0022 #include <kis_time_span.h> 0023 #include <KoColorSpace.h> 0024 #include <KoColorSpaceRegistry.h> 0025 #include <KoColorModelStandardIds.h> 0026 #include <KoResourcePaths.h> 0027 #include "kis_config.h" 0028 #include "KisAnimationRenderingOptions.h" 0029 #include "animation/KisFFMpegWrapper.h" 0030 0031 #include "KisPart.h" 0032 0033 KisAnimationVideoSaver::KisAnimationVideoSaver(KisDocument *doc, bool batchMode) 0034 : m_image(doc->image()) 0035 , m_doc(doc) 0036 , m_batchMode(batchMode) 0037 { 0038 } 0039 0040 KisAnimationVideoSaver::~KisAnimationVideoSaver() 0041 { 0042 } 0043 0044 KisImageSP KisAnimationVideoSaver::image() 0045 { 0046 return m_image; 0047 } 0048 0049 KisImportExportErrorCode KisAnimationVideoSaver::encode(const QString &savedFilesMask, const KisAnimationRenderingOptions &options) 0050 { 0051 if (!QFileInfo(options.ffmpegPath).exists()) { 0052 m_doc->setErrorMessage(i18n("ffmpeg could not be found at %1", options.ffmpegPath)); 0053 return ImportExportCodes::Failure; 0054 } 0055 0056 KisImportExportErrorCode resultOuter = ImportExportCodes::OK; 0057 0058 KisImageAnimationInterface *animation = m_image->animationInterface(); 0059 0060 const int sequenceStart = options.sequenceStart; 0061 const KisTimeSpan clipRange = KisTimeSpan::fromTimeToTime(options.firstFrame, 0062 options.lastFrame); 0063 0064 // export dimensions could be off a little bit, so the last force option tweaks the pixels for the export to work 0065 const QString exportDimensions = 0066 QString("scale=w=") 0067 .append(QString::number(options.width)) 0068 .append(":h=") 0069 .append(QString::number(options.height)) 0070 .append(":flags=") 0071 .append(options.scaleFilter); 0072 //.append(":force_original_aspect_ratio=decrease"); HOTFIX for even:odd dimension images. 0073 0074 const QString resultFile = options.resolveAbsoluteVideoFilePath(); 0075 const QFileInfo resultFileInfo(resultFile); 0076 const QDir videoDir(resultFileInfo.absolutePath()); 0077 0078 const QString suffix = resultFileInfo.suffix().toLower(); 0079 const QString palettePath = videoDir.filePath("KritaTempPalettegen_\%06d.png"); 0080 0081 #if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) 0082 QStringList additionalOptionsList = options.customFFMpegOptions.split(' ', Qt::SkipEmptyParts); 0083 #else 0084 QStringList additionalOptionsList = options.customFFMpegOptions.split(' ', QString::SkipEmptyParts); 0085 #endif 0086 0087 QScopedPointer<KisFFMpegWrapper> ffmpegWrapper(new KisFFMpegWrapper(this)); 0088 0089 { 0090 0091 QStringList paletteArgs; 0092 QStringList simpleFilterArgs; 0093 QStringList complexFilterArgs; 0094 QStringList args; 0095 0096 args << "-y" // Auto Confirm... 0097 << "-r" << QString::number(options.frameRate) // Frame rate for video... 0098 << "-start_number" << QString::number(sequenceStart) << "-start_number_range" << "1" 0099 << "-i" << savedFilesMask; // Input frame(s) file mask.. 0100 0101 const int lavfiOptionsIndex = additionalOptionsList.indexOf("-lavfi"); 0102 0103 if ( lavfiOptionsIndex != -1 ) { 0104 complexFilterArgs << additionalOptionsList.takeAt(lavfiOptionsIndex + 1); 0105 0106 additionalOptionsList.removeAt( lavfiOptionsIndex ); 0107 } 0108 0109 if ( suffix == "gif" ) { 0110 paletteArgs << "-r" << QString::number(options.frameRate) 0111 << "-start_number" << QString::number(sequenceStart) << "-start_number_range" << "1" 0112 << "-i" << savedFilesMask; 0113 0114 const int paletteOptionsIndex = additionalOptionsList.indexOf("-palettegen"); 0115 QString palettegenString = "palettegen"; 0116 0117 if ( paletteOptionsIndex != -1 ) { 0118 palettegenString = additionalOptionsList.takeAt(paletteOptionsIndex + 1); 0119 0120 additionalOptionsList.removeAt( paletteOptionsIndex ); 0121 } 0122 0123 if (m_image->width() != options.width || m_image->height() != options.height) { 0124 paletteArgs << "-vf" << (exportDimensions + "," + palettegenString ); 0125 } else { 0126 paletteArgs << "-vf" << palettegenString; 0127 } 0128 0129 paletteArgs << "-y" << palettePath; 0130 0131 QStringList ffmpegArgs; 0132 ffmpegArgs << "-v" << "debug" 0133 << paletteArgs; 0134 0135 KisFFMpegWrapperSettings ffmpegSettings; 0136 ffmpegSettings.args = ffmpegArgs; 0137 ffmpegSettings.processPath = options.ffmpegPath; 0138 0139 ffmpegSettings.progressIndeterminate = true; 0140 ffmpegSettings.progressMessage = i18nc("Animation export dialog for palette exporting. arg1: file-suffix", 0141 "Creating palette for %1 file format.", "[suffix]"); 0142 ffmpegSettings.logPath = QDir::tempPath() + QDir::separator() + "krita" + QDir::separator() + "ffmpeg.log"; 0143 0144 KisImportExportErrorCode result = ffmpegWrapper->start(ffmpegSettings); 0145 0146 if (!result.isOk()) { 0147 return result; 0148 } 0149 0150 if (lavfiOptionsIndex == -1) { 0151 complexFilterArgs << "[0:v][1:v] paletteuse"; 0152 } 0153 0154 args << "-i" << palettePath; 0155 0156 // We need to kill the process so we can reuse it later down the chain. BUG:446320 0157 ffmpegWrapper->reset(); 0158 } 0159 0160 QVector<QFileInfo> audioFiles = m_doc->getAudioTracks(); 0161 if (options.includeAudio && audioFiles.count() > 0 && audioFiles.first().exists()) { 0162 QFileInfo audioFileInfo = audioFiles.first(); 0163 const int msecPerFrame = (1000 / animation->framerate()); 0164 const int msecStart = msecPerFrame * clipRange.start(); 0165 const int msecDuration = msecPerFrame * clipRange.duration(); 0166 0167 const QTime startTime = QTime::fromMSecsSinceStartOfDay(msecStart); 0168 const QTime durationTime = QTime::fromMSecsSinceStartOfDay(msecDuration); 0169 const QString ffmpegTimeFormat = QStringLiteral("H:m:s.zzz"); 0170 0171 args << "-ss" << QLocale::c().toString(startTime, ffmpegTimeFormat); 0172 args << "-t" << QLocale::c().toString(durationTime, ffmpegTimeFormat); 0173 args << "-i" << audioFileInfo.absoluteFilePath(); 0174 } 0175 0176 // if we are exporting out at a different image size, we apply scaling filter 0177 // export options HAVE to go after input options, so make sure this is after the audio import 0178 if (m_image->width() != options.width || m_image->height() != options.height) { 0179 simpleFilterArgs << exportDimensions; 0180 } 0181 0182 if ( !complexFilterArgs.isEmpty() ) { 0183 args << "-lavfi" << (!simpleFilterArgs.isEmpty() ? simpleFilterArgs.join(",").append("[0:v];"):"") + complexFilterArgs.join(";"); 0184 } else if ( !simpleFilterArgs.isEmpty() ) { 0185 args << "-vf" << simpleFilterArgs.join(","); 0186 } 0187 0188 args << additionalOptionsList; 0189 0190 dbgFile << "savedFilesMask" << savedFilesMask 0191 << "save files offset" << sequenceStart 0192 << "start" << QString::number(clipRange.start()) 0193 << "duration" << clipRange.duration(); 0194 0195 0196 KisFFMpegWrapperSettings ffmpegSettings; 0197 ffmpegSettings.processPath = options.ffmpegPath; 0198 ffmpegSettings.args = args; 0199 ffmpegSettings.outputFile = resultFile; 0200 ffmpegSettings.totalFrames = clipRange.duration(); 0201 ffmpegSettings.logPath = QDir::tempPath() + QDir::separator() + "krita" + QDir::separator() + "ffmpeg.log"; 0202 ffmpegSettings.progressMessage = i18nc("Animation export dialog for tracking ffmpeg progress. arg1: file-suffix, arg2: progress frame number, arg3: totalFrameCount.", 0203 "Creating desired %1 file: %2/%3 frames.", "[suffix]", "[progress]", "[framecount]"); 0204 0205 resultOuter = ffmpegWrapper->start(ffmpegSettings); 0206 } 0207 0208 0209 return resultOuter; 0210 } 0211 0212 KisImportExportErrorCode KisAnimationVideoSaver::convert(KisDocument *document, const QString &savedFilesMask, const KisAnimationRenderingOptions &options, bool batchMode) 0213 { 0214 KisAnimationVideoSaver videoSaver(document, batchMode); 0215 KisImportExportErrorCode res = videoSaver.encode(savedFilesMask, options); 0216 return res; 0217 }