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 }