video_saver.cpp 9.21 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
             << specialArgs;

126 127
        qDebug() << "\t" << m_ffmpegPath << args.join(" ");

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

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

138 139 140 141
private:
    KisImageBuilder_Result waitForFFMpegProcess(const QString &message,
                                                QFile &progressFile,
                                                QProcess &ffmpegProcess,
142 143
                                                int totalFrames)
    {
144 145 146 147 148 149 150 151

        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);
152
        progress.setRange(0, 100);
153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173

        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;
        }
174

175
        return retval;
176 177
    }

178 179 180
private:
    QProcess m_process;
    bool m_cancelled;
181
    QString m_ffmpegPath;
182
};
183

184

185
VideoSaver::VideoSaver(KisDocument *doc, const QString &ffmpegPath, bool batchMode)
186 187 188
    : m_image(doc->image())
    , m_doc(doc)
    , m_batchMode(batchMode)
189 190
    , m_ffmpegPath(ffmpegPath)
    , m_runner(new KisFFMpegRunner(ffmpegPath))
191 192 193 194 195 196
{
}

VideoSaver::~VideoSaver()
{
}
197

198 199 200 201
KisImageSP VideoSaver::image()
{
    return m_image;
}
202

203 204 205 206 207
bool VideoSaver::hasFFMpeg() const
{
    return !m_ffmpegPath.isEmpty();
}

208
KisImageBuilder_Result VideoSaver::encode(const QString &filename, KisPropertiesConfigurationSP configuration)
209 210
{

211
    qDebug() << "ffmpeg" << m_ffmpegPath << "filename" << filename << "configuration" << configuration->toXML();
212

213 214 215 216
    if (m_ffmpegPath.isEmpty()) {
        m_ffmpegPath = configuration->getString("ffmpeg_path");
        if (!QFileInfo(m_ffmpegPath).exists()) {
            return KisImageBuilder_RESULT_FAILURE;
217 218 219
        }
    }

220
    KisImageBuilder_Result result = KisImageBuilder_RESULT_OK;
221

222
    KisImageAnimationInterface *animation = m_image->animationInterface();
223 224
    const KisTimeRange fullRange = animation->fullClipRange();
    const KisTimeRange clipRange(configuration->getInt("firstframe", fullRange.start()), configuration->getInt("lastFrame"), fullRange.end());
225
    const int frameRate = animation->framerate();
226

227
    const QDir framesDir(configuration->getString("directory"));
228

229 230 231 232 233 234 235
    QString resultFile;
    if (QFileInfo(filename).isAbsolute()) {
        resultFile = filename;
    }
    else {
        resultFile = framesDir.absolutePath() + "/" + filename;
    }
236 237
    const QFileInfo info(resultFile);
    const QString suffix = info.suffix().toLower();
238

239
    const QString palettePath = framesDir.filePath("palette.png");
240

241
    const QString savedFilesMask = configuration->getString("savedFilesMask");
242

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

245 246 247 248
    if (suffix == "gif") {
        {
            QStringList args;
            args << "-r" << QString::number(frameRate)
249
                 << "-i" << savedFilesMask
250 251 252 253 254
                 << "-vf" << "palettegen"
                 << "-y" << palettePath;

            KisImageBuilder_Result result =
                m_runner->runFFMpeg(args, i18n("Fetching palette..."),
255
                                    framesDir.filePath("log_generate_palette_gif.log"),
256
                                    clipRange.duration() + clipRange.start());
257

258
            if (result != KisImageBuilder_RESULT_OK) {
259
                return result;
260 261 262
            }
        }

263 264 265
        {
            QStringList args;
            args << "-r" << QString::number(frameRate)
266
                 << "-i" << savedFilesMask
267 268 269 270 271
                 << "-i" << palettePath
                 << "-lavfi" << "[0:v][1:v] paletteuse"
                 << additionalOptionsList
                 << "-y" << resultFile;

272 273
            dbgFile << "savedFilesMask" << savedFilesMask << "start" << clipRange.start() << "duration" << clipRange.duration();

274 275
            KisImageBuilder_Result result =
                m_runner->runFFMpeg(args, i18n("Encoding frames..."),
276
                                    framesDir.filePath("log_encode_gif.log"),
277
                                    clipRange.duration() + clipRange.start());
278

279
            if (result != KisImageBuilder_RESULT_OK) {
280 281 282 283 284 285
                return result;
            }
        }
    } else {
        QStringList args;
        args << "-r" << QString::number(frameRate)
286
             << "-i" << savedFilesMask
287 288 289
             << additionalOptionsList
             << "-y" << resultFile;

290 291
        result = m_runner->runFFMpeg(args, i18n("Encoding frames..."),
                                     framesDir.filePath("log_encode.log"),
292
                                     clipRange.duration() + clipRange.start());
293 294
    }

295
    return result;
296 297 298 299
}

void VideoSaver::cancel()
{
300
    m_runner->cancel();
301
}
302 303

#include "video_saver.moc"