video_saver.cpp 9.58 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/*
 *  Copyright (c) 2016 Dmitry Kazakov <dimula73@gmail.com>
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA 02110-1301, USA.
 */

#include "video_saver.h"

#include <QDebug>

24
#include <QFileInfo>
25
26

#include <KisDocument.h>
27
28
29
#include <KoColorSpace.h>
#include <KoColorSpaceRegistry.h>
#include <KoColorModelStandardIds.h>
30
31
#include <KoResourcePaths.h>

32
33

#include <kis_image.h>
34
35
36
#include <kis_image_animation_interface.h>
#include <kis_time_range.h>

37
#include "kis_config.h"
38
#include "kis_animation_exporter.h"
39

40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#include <QFileSystemWatcher>
#include <QProcess>
#include <QProgressDialog>
#include <QEventLoop>
#include <QTemporaryFile>
#include <QTemporaryDir>

#include "KisPart.h"

class KisFFMpegProgressWatcher : public QObject {
    Q_OBJECT
public:
    KisFFMpegProgressWatcher(QFile &progressFile, int totalFrames)
        : m_progressFile(progressFile),
          m_totalFrames(totalFrames)
        {
            connect(&m_progressWatcher, SIGNAL(fileChanged(QString)), SLOT(slotFileChanged()));
57
58


59
60
            m_progressWatcher.addPath(m_progressFile.fileName());
        }
61

62
63
64
65
private Q_SLOTS:
    void slotFileChanged() {
        int currentFrame = -1;
        bool isEnded = false;
66

67
68
69
        while(!m_progressFile.atEnd()) {
            QString line = QString(m_progressFile.readLine()).remove(QChar('\n'));
            QStringList var = line.split("=");
70

71
72
73
74
75
76
            if (var[0] == "frame") {
                currentFrame = var[1].toInt();
            } else if (var[0] == "progress") {
                isEnded = var[1] == "end";
            }
        }
77

78
79
80
81
        if (isEnded) {
            emit sigProgressChanged(100);
            emit sigProcessingFinished();
        } else {
82

83
84
            emit sigProgressChanged(100 * currentFrame / m_totalFrames);
        }
85
86
    }

87
88
89
90
91
92
93
94
Q_SIGNALS:
    void sigProgressChanged(int percent);
    void sigProcessingFinished();

private:
    QFileSystemWatcher m_progressWatcher;
    QFile &m_progressFile;
    int m_totalFrames;
95
96
};

97
98

class KisFFMpegRunner
99
{
100
public:
101
102
103
    KisFFMpegRunner(const QString &ffmpegPath)
        : m_cancelled(false),
          m_ffmpegPath(ffmpegPath) {}
104
105
106
107
public:
    KisImageBuilder_Result runFFMpeg(const QStringList &specialArgs,
                                     const QString &actionName,
                                     const QString &logPath,
108
109
                                     int totalFrames)
    {
110
111
112
113
        dbgFile << "runFFMpeg: specialArgs" << specialArgs
                << "actionName" << actionName
                << "logPath" << logPath
                << "totalFrames" << totalFrames;
114
115
116
117
118
119
120
121
122

        QTemporaryFile progressFile("KritaFFmpegProgress.XXXXXX");
        progressFile.open();

        m_process.setStandardOutputFile(logPath);
        m_process.setProcessChannelMode(QProcess::MergedChannels);
        QStringList args;
        args << "-v" << "debug"
             << "-nostdin"
Dmitry Kazakov's avatar
Dmitry Kazakov committed
123
             << "-progress" << progressFile.fileName()
124
125
126
             << specialArgs;

        m_cancelled = false;
127
        m_process.start(m_ffmpegPath, args);
128
        return waitForFFMpegProcess(actionName, progressFile, m_process, totalFrames);
129
130
    }

131
132
133
    void cancel() {
        m_cancelled = true;
        m_process.kill();
134
135
    }

136
137
138
139
private:
    KisImageBuilder_Result waitForFFMpegProcess(const QString &message,
                                                QFile &progressFile,
                                                QProcess &ffmpegProcess,
140
141
                                                int totalFrames)
    {
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171

        KisFFMpegProgressWatcher watcher(progressFile, totalFrames);

        QProgressDialog progress(message, "", 0, 0, KisPart::instance()->currentMainwindow());
        progress.setWindowModality(Qt::ApplicationModal);
        progress.setCancelButton(0);
        progress.setMinimumDuration(0);
        progress.setValue(0);
        progress.setRange(0,100);

        QEventLoop loop;
        loop.connect(&watcher, SIGNAL(sigProcessingFinished()), SLOT(quit()));
        loop.connect(&ffmpegProcess, SIGNAL(finished(int, QProcess::ExitStatus)), SLOT(quit()));
        loop.connect(&watcher, SIGNAL(sigProgressChanged(int)), &progress, SLOT(setValue(int)));
        loop.exec();

        // wait for some errorneous case
        ffmpegProcess.waitForFinished(5000);

        KisImageBuilder_Result retval = KisImageBuilder_RESULT_OK;

        if (ffmpegProcess.state() != QProcess::NotRunning) {
            // sorry...
            ffmpegProcess.kill();
            retval = KisImageBuilder_RESULT_FAILURE;
        } else if (m_cancelled) {
            retval = KisImageBuilder_RESULT_CANCEL;
        } else if (ffmpegProcess.exitCode()) {
            retval = KisImageBuilder_RESULT_FAILURE;
        }
172

173
        return retval;
174
175
    }

176
177
178
private:
    QProcess m_process;
    bool m_cancelled;
179
    QString m_ffmpegPath;
180
};
181

182

183
184
185
186
VideoSaver::VideoSaver(KisDocument *doc, bool batchMode)
    : m_image(doc->image())
    , m_doc(doc)
    , m_batchMode(batchMode)
187
188
    , m_ffmpegPath(findFFMpeg())
    , m_runner(new KisFFMpegRunner(m_ffmpegPath))
189
190
191
192
193
194
{
}

VideoSaver::~VideoSaver()
{
}
195

196
197
198
199
KisImageSP VideoSaver::image()
{
    return m_image;
}
200

201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
bool VideoSaver::hasFFMpeg() const
{
    return !m_ffmpegPath.isEmpty();
}

QString VideoSaver::findFFMpeg()
{
    QString result;

    QStringList proposedPaths;

    QString customPath = KisConfig().customFFMpegPath();
    proposedPaths << customPath;
    proposedPaths << customPath + QDir::separator() + "ffmpeg";

    proposedPaths << "ffmpeg";
    proposedPaths << KoResourcePaths::getApplicationRoot() +
        QDir::separator() + "bin" + QDir::separator() + "ffmpeg";

    Q_FOREACH (const QString &path, proposedPaths) {
        if (path.isEmpty()) continue;

        QProcess testProcess;
        testProcess.start(path, QStringList() << "-version");
        testProcess.waitForFinished(1000);

        const bool successfulStart =
            testProcess.state() == QProcess::NotRunning &&
            testProcess.error() == QProcess::UnknownError;

        if (successfulStart) {
            result = path;
            break;
        }
    }

    return result;
}

240
KisImageBuilder_Result VideoSaver::encode(const QString &filename, KisPropertiesConfigurationSP configuration)
241
{
242

243
    //qDebug() << "ffmpeg" << m_ffmpegPath << "filename" << filename << "configuration" << configuration->toXML();
244

245
246
    if (m_ffmpegPath.isEmpty()) return KisImageBuilder_RESULT_FAILURE;

247
    KisImageBuilder_Result result = KisImageBuilder_RESULT_OK;
248

249
    KisImageAnimationInterface *animation = m_image->animationInterface();
250
251
    const KisTimeRange fullRange = animation->fullClipRange();
    const KisTimeRange clipRange(configuration->getInt("firstframe", fullRange.start()), configuration->getInt("lastFrame"), fullRange.end());
252
    const int frameRate = animation->framerate();
253

254
    const QDir framesDir(configuration->getString("directory"));
255

256
    const QString resultFile = framesDir.absolutePath() + "/" + filename;
257
258
    const QFileInfo info(resultFile);
    const QString suffix = info.suffix().toLower();
259

260
    const QString palettePath = framesDir.filePath("palette.png");
261

262
    const QString savedFilesMask = configuration->getString("savedFilesMask");
263

264
    const QStringList additionalOptionsList = configuration->getString("customUserOptions").split(' ', QString::SkipEmptyParts);
265

266
267
268
269
    if (suffix == "gif") {
        {
            QStringList args;
            args << "-r" << QString::number(frameRate)
270
                 << "-i" << savedFilesMask
271
272
273
274
275
                 << "-vf" << "palettegen"
                 << "-y" << palettePath;

            KisImageBuilder_Result result =
                m_runner->runFFMpeg(args, i18n("Fetching palette..."),
276
                                    framesDir.filePath("log_generate_palette_gif.log"),
277
                                    clipRange.duration() + clipRange.start());
278
279
280

            if (result) {
                return result;
281
282
283
            }
        }

284
285
286
        {
            QStringList args;
            args << "-r" << QString::number(frameRate)
287
                 << "-i" << savedFilesMask
288
289
290
291
292
293
294
                 << "-i" << palettePath
                 << "-lavfi" << "[0:v][1:v] paletteuse"
                 << additionalOptionsList
                 << "-y" << resultFile;

            KisImageBuilder_Result result =
                m_runner->runFFMpeg(args, i18n("Encoding frames..."),
295
                                    framesDir.filePath("log_encode_gif.log"),
296
                                    clipRange.duration() + clipRange.start());
297
298
299
300
301
302
303
304

            if (result) {
                return result;
            }
        }
    } else {
        QStringList args;
        args << "-r" << QString::number(frameRate)
305
             << "-i" << savedFilesMask
306
307
308
             << additionalOptionsList
             << "-y" << resultFile;

309
310
        result = m_runner->runFFMpeg(args, i18n("Encoding frames..."),
                                     framesDir.filePath("log_encode.log"),
311
                                     clipRange.duration() + clipRange.start());
312
313
    }

314
    return result;
315
316
317
318
}

void VideoSaver::cancel()
{
319
    m_runner->cancel();
320
}
321
322

#include "video_saver.moc"