proxyclipjob.cpp 17.5 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/***************************************************************************
 *                                                                         *
 *   Copyright (C) 2011 by Jean-Baptiste Mardelle (jb@kdenlive.org)        *
 *                                                                         *
 *   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 "proxyclipjob.h"
22
#include "bin/bin.h"
Nicolas Carion's avatar
Nicolas Carion committed
23
#include "bin/projectclip.h"
24
#include "bin/projectitemmodel.h"
Nicolas Carion's avatar
style    
Nicolas Carion committed
25
#include "core.h"
Nicolas Carion's avatar
Nicolas Carion committed
26
27
28
#include "doc/kdenlivedoc.h"
#include "kdenlive_debug.h"
#include "kdenlivesettings.h"
29
30
#include "macros.hpp"

31
#include <QProcess>
32
#include <QTemporaryFile>
33
#include <QThread>
34

35
#include <klocalizedstring.h>
36

37
ProxyJob::ProxyJob(const QString &binId)
38
    : AbstractClipJob(PROXYJOB, binId, {ObjectType::BinClip, binId.toInt()})
39
40
    , m_jobDuration(0)
    , m_isFfmpegJob(true)
41
    , m_jobProcess(nullptr)
42
    , m_done(false)
43
44
45
{
}

46
const QString ProxyJob::getDescription() const
47
{
48
49
50
51
52
53
54
    return i18n("Creating proxy %1", m_clipId);
}

bool ProxyJob::startJob()
{
    auto binClip = pCore->projectItemModel()->getClipByBinID(m_clipId);
    const QString dest = binClip->getProducerProperty(QStringLiteral("kdenlive:proxy"));
55
56
    QFileInfo fInfo(dest);
    if (binClip->getProducerIntProperty(QStringLiteral("_overwriteproxy")) == 0 && fInfo.exists() && fInfo.size() > 0) {
57
58
59
60
        // Proxy clip already created
        m_done = true;
        return true;
    }
61
    ClipType::ProducerType type = binClip->clipType();
62
63
64
65
    bool result;
    QString source = binClip->getProducerProperty(QStringLiteral("kdenlive:originalurl"));
    int exif = binClip->getProducerIntProperty(QStringLiteral("_exif_orientation"));
    if (type == ClipType::Playlist || type == ClipType::SlideShow) {
66
        // change FFmpeg params to MLT format
67
        m_isFfmpegJob = false;
68
        QStringList mltParameters;
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
        QTemporaryFile *playlist = nullptr;
        // set clip origin
        if (type == ClipType::Playlist) {
            // Special case: playlists use the special 'consumer' producer to support resizing
            source.prepend(QStringLiteral("consumer:"));
        } else {
            // create temporary playlist to generate proxy
            // we save a temporary .mlt clip for rendering
            QDomDocument doc;
            QDomElement xml = binClip->toXml(doc, false);
            playlist = new QTemporaryFile();
            playlist->setFileTemplate(playlist->fileTemplate() + QStringLiteral(".mlt"));
            if (playlist->open()) {
                source = playlist->fileName();
                QTextStream out(playlist);
                out << doc.toString();
                playlist->close();
            }
        }
        mltParameters << source;
        // set destination
        mltParameters << QStringLiteral("-consumer") << QStringLiteral("avformat:") + dest;
        QString parameter = pCore->currentDoc()->getDocumentProperty(QStringLiteral("proxyparams")).simplified();
92
93
94
95
96
97
98
99
100
101
102
        if (parameter.isEmpty()) {
            // Automatic setting, decide based on hw support
            parameter = pCore->currentDoc()->getAutoProxyProfile();
            bool nvenc = parameter.contains(QStringLiteral("%nvcodec"));
            if (nvenc) {
                parameter = parameter.section(QStringLiteral("-i"), 1);
                parameter.replace(QStringLiteral("scale_cuda"), QStringLiteral("scale"));
                parameter.replace(QStringLiteral("scale_npp"), QStringLiteral("scale"));
                parameter.prepend(QStringLiteral("-pix_fmt yuv420p"));
            }
        }
Laurent Montel's avatar
Laurent Montel committed
103
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
104
        QStringList params = parameter.split(QLatin1Char('-'), QString::SkipEmptyParts);
Laurent Montel's avatar
Laurent Montel committed
105
106
107
#else
        QStringList params = parameter.split(QLatin1Char('-'), Qt::SkipEmptyParts);
#endif
108
        double display_ratio;
109
110
        if (source.startsWith(QLatin1String("consumer:"))) {
            display_ratio = KdenliveDoc::getDisplayRatio(source.section(QLatin1Char(':'), 1));
Laurent Montel's avatar
Laurent Montel committed
111
        } else {
112
            display_ratio = KdenliveDoc::getDisplayRatio(source);
Laurent Montel's avatar
Laurent Montel committed
113
        }
114
        if (display_ratio < 1e-6) {
115
            display_ratio = 1;
Laurent Montel's avatar
Laurent Montel committed
116
        }
117

118
        bool skipNext = false;
Vincent Pinon's avatar
Vincent Pinon committed
119
        for (const QString &s : qAsConst(params)) {
120
            QString t = s.simplified();
121
122
123
124
            if (skipNext) {
                skipNext = false;
                continue;
            }
Laurent Montel's avatar
Laurent Montel committed
125
126
            if (t.count(QLatin1Char(' ')) == 0) {
                t.append(QLatin1String("=1"));
Laurent Montel's avatar
Laurent Montel committed
127
            } else if (t.startsWith(QLatin1String("vf "))) {
128
129
130
                skipNext = true;
                bool ok = false;
                int width = t.section(QLatin1Char('='), 1, 1).section(QLatin1Char(':'), 0, 0).toInt(&ok);
Laurent Montel's avatar
Laurent Montel committed
131
132
133
                if (!ok) {
                    width = 640;
                }
134
                int height = width / display_ratio;
135
136
                // Make sure we get an even height
                height += height % 2;
Laurent Montel's avatar
Laurent Montel committed
137
                mltParameters << QStringLiteral("s=%1x%2").arg(width).arg(height);
138
139
140
                if (t.contains(QStringLiteral("yadif"))) {
                    mltParameters << QStringLiteral("progressive=1");
                }
141
                continue;
Laurent Montel's avatar
Laurent Montel committed
142
143
            } else {
                t.replace(QLatin1Char(' '), QLatin1String("="));
144
145
146
147
                if (t == QLatin1String("acodec=copy") && type == ClipType::Playlist) {
                    // drop this for playlists, otherwise we have no sound in proxies
                    continue;
                }
Laurent Montel's avatar
Laurent Montel committed
148
            }
149
            mltParameters << t;
150
        }
151
152
153
154
155
156
157
158
        int threadCount = QThread::idealThreadCount();
        if (threadCount > 2) {
            threadCount = qMin(threadCount - 1, 4);
        } else {
            threadCount = 1;
        }
        mltParameters.append(QStringLiteral("real_time=-%1").arg(threadCount));
        mltParameters.append(QStringLiteral("threads=%1").arg(threadCount));
159
        mltParameters.append(QStringLiteral("terminate_on_pause=1"));
160

161
        // TODO: currently, when rendering an xml file through melt, the display ration is lost, so we enforce it manually
162
        mltParameters << QStringLiteral("aspect=") + QString::number(display_ratio, 'f');
163

164
        // Ask for progress reporting
165
        mltParameters << QStringLiteral("progress=1");
166

167
        m_jobProcess = new QProcess;
168
        // m_jobProcess->setProcessChannelMode(QProcess::MergedChannels);
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
169
        connect(this, &ProxyJob::jobCanceled, m_jobProcess, &QProcess::kill, Qt::DirectConnection);
170
        connect(m_jobProcess, &QProcess::readyReadStandardError, this, &ProxyJob::processLogInfo);
171
        m_jobProcess->start(KdenliveSettings::rendererpath(), mltParameters);
172
173
174
175
        m_jobProcess->waitForFinished(-1);
        result = m_jobProcess->exitStatus() == QProcess::NormalExit;
        delete playlist;
    } else if (type == ClipType::Image) {
176
        m_isFfmpegJob = false;
177
        // Image proxy
178
        QImage i(source);
179
        if (i.isNull()) {
180
181
182
            m_done = false;
            m_errorMessage.append(i18n("Cannot load image %1.", source));
            return false;
183
184
        }

185
        QImage proxy;
186
        // Images are scaled to profile size.
Nicolas Carion's avatar
Nicolas Carion committed
187
        // TODO: Make it be configurable?
Laurent Montel's avatar
Laurent Montel committed
188
        if (i.width() > i.height()) {
189
            proxy = i.scaledToWidth(KdenliveSettings::proxyimagesize());
Laurent Montel's avatar
Laurent Montel committed
190
        } else {
191
            proxy = i.scaledToHeight(KdenliveSettings::proxyimagesize());
Laurent Montel's avatar
Laurent Montel committed
192
        }
193
        if (exif > 1) {
194
195
            // Rotate image according to exif data
            QImage processed;
196
            QTransform matrix;
197

198
            switch (exif) {
199
            case 2:
Laurent Montel's avatar
Laurent Montel committed
200
                matrix.scale(-1, 1);
201
202
                break;
            case 3:
Laurent Montel's avatar
Laurent Montel committed
203
                matrix.rotate(180);
204
205
                break;
            case 4:
Laurent Montel's avatar
Laurent Montel committed
206
                matrix.scale(1, -1);
207
208
                break;
            case 5:
Laurent Montel's avatar
Laurent Montel committed
209
210
                matrix.rotate(270);
                matrix.scale(-1, 1);
211
212
                break;
            case 6:
Laurent Montel's avatar
Laurent Montel committed
213
                matrix.rotate(90);
214
215
                break;
            case 7:
Laurent Montel's avatar
Laurent Montel committed
216
217
                matrix.rotate(90);
                matrix.scale(-1, 1);
218
219
                break;
            case 8:
Laurent Montel's avatar
Laurent Montel committed
220
                matrix.rotate(270);
221
                break;
222
            }
Laurent Montel's avatar
Laurent Montel committed
223
            processed = proxy.transformed(matrix);
224
            processed.save(dest);
225
        } else {
226
            proxy.save(dest);
227
        }
228
229
        m_done = true;
        return true;
230
    } else {
231
        m_isFfmpegJob = true;
Vincent Pinon's avatar
Vincent Pinon committed
232
        if (!QFileInfo(KdenliveSettings::ffmpegpath()).isFile()) {
Nicolas Carion's avatar
Nicolas Carion committed
233
            // FFmpeg not detected, cannot process the Job
234
            m_errorMessage.prepend(i18n("Failed to create proxy. FFmpeg not found, please set path in Kdenlive's settings Environment"));
235
236
            m_done = true;
            return false;
237
        }
238
        // Only output error data, make sure we don't block when proxy file already exists
239
240
        QStringList parameters = {QStringLiteral("-hide_banner"), QStringLiteral("-y"), QStringLiteral("-stats"), QStringLiteral("-v"), QStringLiteral("error")};
        m_jobDuration = (int)binClip->duration().seconds();
241
        QString proxyParams = pCore->currentDoc()->getDocumentProperty(QStringLiteral("proxyparams")).simplified();
242
243
244
245
        if (proxyParams.isEmpty()) {
            // Automatic setting, decide based on hw support
            proxyParams = pCore->currentDoc()->getAutoProxyProfile();
        }
246
247
248
249
        bool nvenc = proxyParams.contains(QStringLiteral("%nvcodec"));
        if (nvenc) {
            QString pix_fmt = binClip->videoCodecProperty(QStringLiteral("pix_fmt"));
            QString codec = binClip->videoCodecProperty(QStringLiteral("name"));
250
251
252
253
254
255
            QStringList supportedCodecs{QStringLiteral("hevc"),  QStringLiteral("h264"),  QStringLiteral("mjpeg"),
                                        QStringLiteral("mpeg1"), QStringLiteral("mpeg2"), QStringLiteral("mpeg4"),
                                        QStringLiteral("vc1"),   QStringLiteral("vp8"),   QStringLiteral("vp9")};
            QStringList supportedPixFmts{QStringLiteral("yuv420p"), QStringLiteral("yuyv422"), QStringLiteral("rgb24"),
                                         QStringLiteral("bgr24"),   QStringLiteral("yuv422p"), QStringLiteral("yuv444p"),
                                         QStringLiteral("rgb32"),   QStringLiteral("yuv410p"), QStringLiteral("yuv411p")};
256
            bool supported = KdenliveSettings::nvScalingEnabled() && supportedCodecs.contains(codec) && supportedPixFmts.contains(pix_fmt);
257
258
259
260
261
262
263
            if (supported) {
                // Full hardware decoding supported
                codec.append(QStringLiteral("_cuvid"));
                proxyParams.replace(QStringLiteral("%nvcodec"), codec);
            } else {
                proxyParams = proxyParams.section(QStringLiteral("-i"), 1);
                proxyParams.replace(QStringLiteral("scale_cuda"), QStringLiteral("scale"));
264
                proxyParams.replace(QStringLiteral("scale_npp"), QStringLiteral("scale"));
265
266
267
268
269
                if (!supportedPixFmts.contains(pix_fmt)) {
                    proxyParams.prepend(QStringLiteral("-pix_fmt yuv420p"));
                }
            }
        }
270
271
        bool disableAutorotate = binClip->getProducerProperty(QStringLiteral("autorotate")) == QLatin1String("0");
        if (disableAutorotate || proxyParams.contains(QStringLiteral("-noautorotate"))) {
272
273
274
            // The noautorotate flag must be passed before input source
            parameters << QStringLiteral("-noautorotate");
        }
275
        if (proxyParams.contains(QLatin1String("-i "))) {
276
277
            // we have some pre-filename parameters, filename will be inserted later
        } else {
278
            parameters << QStringLiteral("-i") << source;
279
        }
280
        QString params = proxyParams;
Laurent Montel's avatar
Laurent Montel committed
281
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
282
        for (const QString &s : params.split(QLatin1Char(' '), QString::SkipEmptyParts)) {
Laurent Montel's avatar
Laurent Montel committed
283
284
285
#else
        for (const QString &s : params.split(QLatin1Char(' '), Qt::SkipEmptyParts)) {
#endif
286
287
288
289
            QString t = s.simplified();
            if (t != QLatin1String("-noautorotate")) {
                parameters << t;
                if (t == QLatin1String("-i")) {
290
                    parameters << source;
291
                }
292
293
            }
        }
294

295
        // Make sure we keep the stream order
296
        parameters << QStringLiteral("-sn") << QStringLiteral("-dn") << QStringLiteral("-map") << QStringLiteral("0");
297
        parameters << dest;
298
        qDebug()<<"/// FULL PROXY PARAMS:\n"<<parameters<<"\n------";
299
        m_jobProcess = new QProcess;
300
        // m_jobProcess->setProcessChannelMode(QProcess::MergedChannels);
301
        connect(m_jobProcess, &QProcess::readyReadStandardError, this, &ProxyJob::processLogInfo);
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
302
        connect(this, &ProxyJob::jobCanceled, m_jobProcess, &QProcess::kill, Qt::DirectConnection);
303
        m_jobProcess->start(KdenliveSettings::ffmpegpath(), parameters, QIODevice::ReadOnly);
304
305
        m_jobProcess->waitForFinished(-1);
        result = m_jobProcess->exitStatus() == QProcess::NormalExit;
306
    }
307
    // remove temporary playlist if it exists
308
309
    if (result) {
        if (QFileInfo(dest).size() == 0) {
310
            QFile::remove(dest);
311
312
313
314
315
            // File was not created
            m_done = false;
            m_errorMessage.append(i18n("Failed to create proxy clip."));
        } else {
            m_done = true;
316
        }
317
318
319
320
321
    } else {
        // Proxy process crashed
        QFile::remove(dest);
        m_done = false;
        m_errorMessage.append(QString::fromUtf8(m_jobProcess->readAll()));
322
    }
323
    m_jobProcess->deleteLater();
324
    return result;
325
326
}

327
void ProxyJob::processLogInfo()
328
{
329
330
    const QString buffer = QString::fromUtf8(m_jobProcess->readAllStandardError());
    m_logDetails.append(buffer);
331
    int progress = 0;
332
333
334
    if (m_isFfmpegJob) {
        // Parse FFmpeg output
        if (m_jobDuration == 0) {
335
336
337
338
339
340
341
342
343
            if (buffer.contains(QLatin1String("Duration:"))) {
                QString data = buffer.section(QStringLiteral("Duration:"), 1, 1).section(QLatin1Char(','), 0, 0).simplified();
                if (!data.isEmpty()) {
                    QStringList numbers = data.split(QLatin1Char(':'));
                    if (numbers.size() < 3) {
                        return;
                    }
                    m_jobDuration = (int)(numbers.at(0).toInt() * 3600 + numbers.at(1).toInt() * 60 + numbers.at(2).toDouble());
                }
344
            }
345
346
347
        } else if (buffer.contains(QLatin1String("time="))) {
            QString time = buffer.section(QStringLiteral("time="), 1, 1).simplified().section(QLatin1Char(' '), 0, 0);
            if (!time.isEmpty()) {
Laurent Montel's avatar
Laurent Montel committed
348
                QStringList numbers = time.split(QLatin1Char(':'));
349
350
351
352
353
354
355
356
                if (numbers.size() < 3) {
                    progress = (int)time.toDouble();
                    if (progress == 0) {
                        return;
                    }
                } else {
                    progress = numbers.at(0).toInt() * 3600 + numbers.at(1).toInt() * 60 + numbers.at(2).toDouble();
                }
357
            }
358
            emit jobProgress((int)(100.0 * progress / m_jobDuration));
359
        }
Laurent Montel's avatar
Laurent Montel committed
360
    } else {
361
        // Parse MLT output
362
363
        if (buffer.contains(QLatin1String("percentage:"))) {
            progress = buffer.section(QStringLiteral("percentage:"), 1).simplified().section(QLatin1Char(' '), 0, 0).toInt();
364
            emit jobProgress(progress);
365
366
367
368
        }
    }
}

369
bool ProxyJob::commitResult(Fun &undo, Fun &redo)
370
{
371
372
373
374
375
376
    Q_ASSERT(!m_resultConsumed);
    if (!m_done) {
        qDebug() << "ERROR: Trying to consume invalid results";
        auto binClip = pCore->projectItemModel()->getClipByBinID(m_clipId);
        binClip->setProducerProperty(QStringLiteral("kdenlive:proxy"), QStringLiteral("-"));
        return false;
377
    }
378
    m_resultConsumed = true;
379
    auto operation = [clipId = m_clipId]() {
380
        auto binClip = pCore->projectItemModel()->getClipByBinID(clipId);
381
        binClip->setProducerProperty(QStringLiteral("_overwriteproxy"), QString());
382
383
        const QString dest = binClip->getProducerProperty(QStringLiteral("kdenlive:proxy"));
        binClip->setProducerProperty(QStringLiteral("resource"), dest);
384
        pCore->bin()->reloadClip(clipId);
385
386
        return true;
    };
387
    auto reverse = [clipId = m_clipId]() {
388
389
390
        auto binClip = pCore->projectItemModel()->getClipByBinID(clipId);
        const QString dest = binClip->getProducerProperty(QStringLiteral("kdenlive:originalurl"));
        binClip->setProducerProperty(QStringLiteral("resource"), dest);
391
        pCore->bin()->reloadClip(clipId);
392
393
394
395
396
        return true;
    };
    bool ok = operation();
    if (ok) {
        UPDATE_UNDO_REDO_NOLOCK(operation, reverse, undo, redo);
397
    }
398
399
    return ok;
    return true;
400
}