Krita Source Code Documentation
Loading...
Searching...
No Matches
KisVideoSaver.cpp
Go to the documentation of this file.
1/*
2 * SPDX-FileCopyrightText: 2016 Dmitry Kazakov <dimula73@gmail.com>
3 *
4 * SPDX-License-Identifier: GPL-2.0-or-later
5 */
6
7#include "KisVideoSaver.h"
8
9#include <QDebug>
10#include <QFileInfo>
11#include <QFileSystemWatcher>
12#include <QProcess>
13#include <QProgressDialog>
14#include <QEventLoop>
15#include <QTemporaryFile>
16#include <QTemporaryDir>
17#include <QTime>
18
19#include <KisDocument.h>
20#include <kis_image.h>
22#include <kis_time_span.h>
23#include <KoColorSpace.h>
26#include <KoResourcePaths.h>
27#include "kis_config.h"
29
30#include "KisPart.h"
31
32#ifdef Q_OS_ANDROID
34#include <KisAndroidUtils.h>
35#include <QJsonDocument>
36#include <QTemporaryFile>
37#else
39#endif
40
42 : m_image(doc->image())
43 , m_doc(doc)
44 , m_batchMode(batchMode)
45{
46}
47
51
56
58 const QString &savedFilesMask,
59 const QStringList &savedFiles,
60 const KisAnimationRenderingOptions &options)
61{
62#ifdef Q_OS_ANDROID
63 Q_UNUSED(savedFilesMask);
64
65 KisMediaEncoderFormat *format = KisMediaEncoderWrapper::getFormatByKey(options.videoFormatKey);
67
68 QVariantMap formatPreferences;
69 if (!options.videoFormatPreferencesJson.isEmpty()) {
70 formatPreferences = QJsonDocument::fromJson(options.videoFormatPreferencesJson.toUtf8()).toVariant().toMap();
71 }
72
74 options.videoFileName,
76 QString(),
77 format,
78 formatPreferences,
79 options.scaleFilter,
80 QSize(options.width, options.height),
81 options.frameRate,
82 options.frameRate,
83 0,
84 0,
85 0,
86 };
87
88 QString inputDir = framesDirectory;
89 if (!inputDir.endsWith(QStringLiteral("/"))) {
90 inputDir.append(QStringLiteral("/"));
91 }
92
93 settings.inputFiles.reserve(savedFiles.size());
94 for (const QString &savedFile : savedFiles) {
95 settings.inputFiles.append(inputDir + savedFile);
96 }
97
98 // MLT can't read from Android content URIs, so we have to copy the file
99 // into temporary storage and get a real path for it.
100 QTemporaryFile audioFile;
101 if (format->supportsAudio() && options.includeAudio) {
103 if (!audioFiles.isEmpty()) {
104 QString audioPath = audioFiles.first().filePath();
105 QString errorMessage;
106 if (KisAndroidUtils::copyFileToTemporary(audioPath, audioFile, &errorMessage)) {
107 settings.audioFile = audioFile.fileName();
108 settings.audioSeekFrame = options.firstFrame;
109 } else {
110 return KisImportExportErrorCode(i18n("Error preparing audio file: %1", errorMessage));
111 }
112 }
113 }
114
115 // This whole batchMode business is either a vestige or was never actually
116 // implemented to completion. It's *supposed* to not show dialogs if Krita
117 // is run in batch mode via the command line, but it's not actually wired up
118 // properly. KisAnimationRender always sets it to false with a TODO to fetch
119 // it correctly and the ffmpeg code here never sets it either, meaning it'll
120 // always be false. For consistency, I've made KisMediaEncoderWrapper behave
121 // the same, even if it's kind of pointless. But if you're working on this
122 // in the future, you should be able to do the same thing for this code path
123 // as you're doing for the ffmpeg one.
124 bool batchMode = false;
125 return KisMediaEncoderWrapper().start(settings, batchMode);
126#else
127 Q_UNUSED(framesDirectory);
128 Q_UNUSED(savedFiles);
129
130 if (!QFileInfo(options.ffmpegPath).exists()) {
131 m_doc->setErrorMessage(i18n("ffmpeg could not be found at %1", options.ffmpegPath));
133 }
134
136
138
139 const int sequenceStart = options.sequenceStart;
140 const KisTimeSpan clipRange = KisTimeSpan::fromTimeToTime(options.firstFrame,
141 options.lastFrame);
142
143 // export dimensions could be off a little bit, so the last force option tweaks the pixels for the export to work
144 const QString exportDimensions =
145 QString("scale=w=")
146 .append(QString::number(options.width))
147 .append(":h=")
148 .append(QString::number(options.height))
149 .append(":flags=")
150 .append(options.scaleFilter);
151 //.append(":force_original_aspect_ratio=decrease"); HOTFIX for even:odd dimension images.
152
153 const QString resultFile = options.resolveAbsoluteVideoFilePath();
154 const QFileInfo resultFileInfo(resultFile);
155 const QDir videoDir(resultFileInfo.absolutePath());
156
157 const QString suffix = resultFileInfo.suffix().toLower();
158 const QString palettePath = videoDir.filePath("KritaTempPalettegen_\%06d.png");
159
160 QStringList additionalOptionsList = options.customFFMpegOptions.split(' ', Qt::SkipEmptyParts);
161
162 QScopedPointer<KisFFMpegWrapper> ffmpegWrapper(new KisFFMpegWrapper(this));
163
164 {
165 QStringList paletteArgs;
166 QStringList simpleFilterArgs;
167 QStringList complexFilterArgs;
168 QStringList args;
169
170 args << "-y" // Auto Confirm...
171 << "-r" << QString::number(options.frameRate) // Frame rate for video...
172 << "-start_number" << QString::number(sequenceStart) << "-start_number_range" << "1"
173 << "-i" << savedFilesMask; // Input frame(s) file mask..
174
175 const int lavfiOptionsIndex = additionalOptionsList.indexOf("-lavfi");
176
177 if ( lavfiOptionsIndex != -1 ) {
178 complexFilterArgs << additionalOptionsList.takeAt(lavfiOptionsIndex + 1);
179
180 additionalOptionsList.removeAt( lavfiOptionsIndex );
181 }
182
183 if ( options.videoMimeType == "image/gif" ) {
184 paletteArgs << "-r" << QString::number(options.frameRate)
185 << "-start_number" << QString::number(sequenceStart) << "-start_number_range" << "1"
186 << "-i" << savedFilesMask;
187
188 const int paletteOptionsIndex = additionalOptionsList.indexOf("-palettegen");
189 QString palettegenString = "palettegen";
190
191 if ( paletteOptionsIndex != -1 ) {
192 palettegenString = additionalOptionsList.takeAt(paletteOptionsIndex + 1);
193
194 additionalOptionsList.removeAt( paletteOptionsIndex );
195 }
196
197 if (m_image->width() != options.width || m_image->height() != options.height) {
198 paletteArgs << "-vf" << (exportDimensions + "," + palettegenString );
199 } else {
200 paletteArgs << "-vf" << palettegenString;
201 }
202
203 paletteArgs << "-y" << palettePath;
204
205 QStringList ffmpegArgs;
206 ffmpegArgs << "-v" << "debug"
207 << paletteArgs;
208
209 KisFFMpegWrapperSettings ffmpegSettings;
210 ffmpegSettings.args = ffmpegArgs;
211 ffmpegSettings.processPath = options.ffmpegPath;
212
213 ffmpegSettings.progressIndeterminate = true;
214 ffmpegSettings.progressMessage = i18nc("Animation export dialog for palette exporting. arg1: file-suffix",
215 "Creating palette for %1 file format.", "[suffix]");
216 ffmpegSettings.logPath = QDir::tempPath() + QDir::separator() + "krita" + QDir::separator() + "ffmpeg.log";
217
218 KisImportExportErrorCode result = ffmpegWrapper->start(ffmpegSettings);
219
220 if (!result.isOk()) {
221 return result;
222 }
223
224 if (lavfiOptionsIndex == -1) {
225 complexFilterArgs << "[0:v][1:v] paletteuse";
226 }
227
228 args << "-i" << palettePath;
229
230 // We need to kill the process so we can reuse it later down the chain. BUG:446320
231 ffmpegWrapper->reset();
232 }
233
235 if (options.includeAudio && audioFiles.count() > 0 && audioFiles.first().exists()) {
236 QFileInfo audioFileInfo = audioFiles.first();
237 const int msecPerFrame = (1000 / animation->framerate());
238 const int msecStart = msecPerFrame * clipRange.start();
239 const int msecDuration = msecPerFrame * clipRange.duration();
240
241 const QTime startTime = QTime::fromMSecsSinceStartOfDay(msecStart);
242 const QTime durationTime = QTime::fromMSecsSinceStartOfDay(msecDuration);
243 const QString ffmpegTimeFormat = QStringLiteral("H:m:s.zzz");
244
245 args << "-ss" << QLocale::c().toString(startTime, ffmpegTimeFormat);
246 args << "-t" << QLocale::c().toString(durationTime, ffmpegTimeFormat);
247 args << "-i" << audioFileInfo.absoluteFilePath();
248 }
249
250 // if we are exporting out at a different image size, we apply scaling filter
251 // export options HAVE to go after input options, so make sure this is after the audio import
252 if (m_image->width() != options.width || m_image->height() != options.height) {
253 simpleFilterArgs << exportDimensions;
254 }
255
256 if ( !complexFilterArgs.isEmpty() ) {
257 args << "-lavfi" << (!simpleFilterArgs.isEmpty() ? simpleFilterArgs.join(",").append("[0:v];"):"") + complexFilterArgs.join(";");
258 } else if ( !simpleFilterArgs.isEmpty() ) {
259 args << "-vf" << simpleFilterArgs.join(",");
260 }
261
262 args << additionalOptionsList;
263
264 dbgFile << "savedFilesMask" << savedFilesMask
265 << "save files offset" << sequenceStart
266 << "start" << QString::number(clipRange.start())
267 << "duration" << clipRange.duration();
268
269
270 KisFFMpegWrapperSettings ffmpegSettings;
271 ffmpegSettings.processPath = options.ffmpegPath;
272 ffmpegSettings.args = args;
273 ffmpegSettings.outputFile = resultFile;
274 ffmpegSettings.totalFrames = clipRange.duration();
275 ffmpegSettings.logPath = QDir::tempPath() + QDir::separator() + "krita" + QDir::separator() + "ffmpeg.log";
276 ffmpegSettings.progressMessage = i18nc("Animation export dialog for tracking ffmpeg progress. arg1: file-suffix, arg2: progress frame number, arg3: totalFrameCount.",
277 "Creating desired %1 file: %2/%3 frames.", "[suffix]", "[progress]", "[framecount]");
278
279 resultOuter = ffmpegWrapper->start(ffmpegSettings);
280 }
281
282
283 return resultOuter;
284#endif
285}
286
288 const QString &framesDirectory,
289 const QString &savedFilesMask,
290 const QStringList &savedFiles,
291 const KisAnimationRenderingOptions &options,
292 bool batchMode)
293{
294 KisAnimationVideoSaver videoSaver(document, batchMode);
295 KisImportExportErrorCode res = videoSaver.encode(framesDirectory, savedFilesMask, savedFiles, options);
296 return res;
297}
QList< QString > QStringList
QString resolveAbsoluteVideoFilePath(const QString &documentPath) const
KisAnimationVideoSaver(KisDocument *doc, bool batchMode)
KisAnimationVideoSaver This is the object that takes an animation document and config and tells ffmpe...
~KisAnimationVideoSaver() override
static KisImportExportErrorCode convert(KisDocument *document, const QString &framesDirectory, const QString &savedFilesMask, const QStringList &savedFiles, const KisAnimationRenderingOptions &options, bool batchMode)
KisImportExportErrorCode encode(const QString &framesDirectory, const QString &savedFilesMask, const QStringList &savedFiles, const KisAnimationRenderingOptions &options)
KisImageSP image()
image
void setErrorMessage(const QString &errMsg)
QVector< QFileInfo > getAudioTracks() const
KisImageAnimationInterface * animationInterface() const
qint32 width() const
qint32 height() const
virtual bool supportsAudio() const =0
static KisMediaEncoderFormat * getFormatByKey(const QString &key)
KisImportExportErrorCode start(const KisMediaEncoderWrapperSettings &settings, bool batchMode)
int start() const
int duration() const
static KisTimeSpan fromTimeToTime(int start, int end)
#define KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(cond, val)
Definition kis_assert.h:129
#define dbgFile
Definition kis_debug.h:56
bool copyFileToTemporary(const QString &inputPath, QTemporaryFile &outputFile, QString *outErrorMessage)