projectclip.cpp 91.8 KB
Newer Older
1
/*
Camille Moulin's avatar
Camille Moulin committed
2
3
SPDX-FileCopyrightText: 2012 Till Theato <root@ttill.de>
SPDX-FileCopyrightText: 2014 Jean-Baptiste Mardelle <jb@kdenlive.org>
4
5
This file is part of Kdenlive. See www.kdenlive.org.

Camille Moulin's avatar
Camille Moulin committed
6
SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
7
8
9
*/

#include "projectclip.h"
10
#include "bin.h"
11
#include "clipcreator.hpp"
Nicolas Carion's avatar
Nicolas Carion committed
12
#include "core.h"
13
14
#include "doc/docundostack.hpp"
#include "doc/kdenlivedoc.h"
15
#include "doc/kthumb.h"
Nicolas Carion's avatar
Nicolas Carion committed
16
#include "effects/effectstack/model/effectstackmodel.hpp"
17
#include "jobs/audiolevelstask.h"
18
#include "jobs/cachetask.h"
19
#include "jobs/cliploadtask.h"
20
#include "jobs/proxytask.h"
21
#include "kdenlivesettings.h"
22
#include "lib/audio/audioStreamInfo.h"
23
#include "macros.hpp"
24
#include "mltcontroller/clipcontroller.h"
25
#include "mltcontroller/clippropertiescontroller.h"
26
#include "model/markerlistmodel.hpp"
27
#include "profiles/profilemodel.hpp"
Nicolas Carion's avatar
Nicolas Carion committed
28
#include "project/projectcommands.h"
29
#include "project/projectmanager.h"
Nicolas Carion's avatar
Nicolas Carion committed
30
31
32
#include "projectfolder.h"
#include "projectitemmodel.h"
#include "projectsubclip.h"
33
#include "utils/timecode.h"
Nicolas Carion's avatar
style    
Nicolas Carion committed
34
#include "timeline2/model/snapmodel.hpp"
35

36
#include "utils/thumbnailcache.hpp"
37
#include "xml/xml.hpp"
38
#include <QPainter>
39
#include <kimagecache.h>
40

Laurent Montel's avatar
Laurent Montel committed
41
#include "kdenlive_debug.h"
42
#include <KLocalizedString>
43
#include <KMessageBox>
44
#include <QApplication>
Nicolas Carion's avatar
Nicolas Carion committed
45
46
47
48
#include <QCryptographicHash>
#include <QDir>
#include <QDomElement>
#include <QFile>
Nicolas Carion's avatar
Nicolas Carion committed
49
#include <memory>
50

Vincent Pinon's avatar
Vincent Pinon committed
51
52
#ifdef CRASH_AUTO_TEST
#include "logger.hpp"
Nicolas Carion's avatar
Nicolas Carion committed
53
54
55
56
57
58
59
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-parameter"
#pragma GCC diagnostic ignored "-Wsign-conversion"
#pragma GCC diagnostic ignored "-Wfloat-equal"
#pragma GCC diagnostic ignored "-Wshadow"
#pragma GCC diagnostic ignored "-Wpedantic"
#include <rttr/registration>
60

Nicolas Carion's avatar
Nicolas Carion committed
61
62
63
64
65
66
#pragma GCC diagnostic pop
RTTR_REGISTRATION
{
    using namespace rttr;
    registration::class_<ProjectClip>("ProjectClip");
}
Vincent Pinon's avatar
Vincent Pinon committed
67
68
#endif

Nicolas Carion's avatar
Nicolas Carion committed
69

Nicolas Carion's avatar
Nicolas Carion committed
70
ProjectClip::ProjectClip(const QString &id, const QIcon &thumb, const std::shared_ptr<ProjectItemModel> &model, std::shared_ptr<Mlt::Producer> producer)
71
    : AbstractProjectItem(AbstractProjectItem::ClipItem, id, model)
Julius Künzel's avatar
Julius Künzel committed
72
    , ClipController(id, producer)
73
    , m_resetTimelineOccurences(false)
74
    , m_audioCount(0)
75
    , m_uuid(QUuid::createUuid())
76
{
77
    m_markerModel = std::make_shared<MarkerListModel>(id, pCore->projectManager()->undoStack());
78
    if (producer->get_int("_placeholder") == 1) {
79
        m_clipStatus = FileStatus::StatusMissing;
80
    } else if (producer->get_int("_missingsource") == 1) {
81
82
83
        m_clipStatus = FileStatus::StatusProxyOnly;
    } else if (m_usesProxy) {
        m_clipStatus = FileStatus::StatusProxy;
84
    } else {
85
        m_clipStatus = FileStatus::StatusReady;
86
    }
87
88
    m_name = clipName();
    m_duration = getStringDuration();
89
    m_inPoint = 0;
90
    m_outPoint = 0;
91
92
    m_date = date;
    m_description = ClipController::description();
93
    if (m_clipType == ClipType::Audio) {
94
        m_thumbnail = QIcon::fromTheme(QStringLiteral("audio-x-generic"));
95
96
97
    } else {
        m_thumbnail = thumb;
    }
98
99
    // Make sure we have a hash for this clip
    hash();
100
101
    m_boundaryTimer.setSingleShot(true);
    m_boundaryTimer.setInterval(500);
102
103
104
    if (m_hasLimitedDuration) {
        connect(&m_boundaryTimer, &QTimer::timeout, this, &ProjectClip::refreshBounds);
    }
Vincent Pinon's avatar
Vincent Pinon committed
105
    connect(m_markerModel.get(), &MarkerListModel::modelChanged, this, [&]() {
106
107
        setProducerProperty(QStringLiteral("kdenlive:markers"), m_markerModel->toJson());
    });
108
109
    QString markers = getProducerProperty(QStringLiteral("kdenlive:markers"));
    if (!markers.isEmpty()) {
Vincent Pinon's avatar
Vincent Pinon committed
110
        QMetaObject::invokeMethod(m_markerModel.get(), "importFromJson", Qt::QueuedConnection, Q_ARG(QString, markers), Q_ARG(bool, true),
111
                                  Q_ARG(bool, false));
112
    }
113
    setTags(getProducerProperty(QStringLiteral("kdenlive:tags")));
114
    AbstractProjectItem::setRating(uint(getProducerIntProperty(QStringLiteral("kdenlive:rating"))));
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
115
    connectEffectStack();
116
    if (m_clipStatus == FileStatus::StatusProxy || m_clipStatus == FileStatus::StatusReady || m_clipStatus == FileStatus::StatusProxyOnly) {
117
        // Generate clip thumbnail
118
        ClipLoadTask::start({ObjectType::BinClip,m_binId.toInt()}, QDomElement(), true, -1, -1, this);
119
        // Generate audio thumbnail
120
        if (KdenliveSettings::audiothumbnails() && (m_clipType == ClipType::AV || m_clipType == ClipType::Audio || m_clipType == ClipType::Playlist || m_clipType == ClipType::Unknown)) {
121
122
            AudioLevelsTask::start({ObjectType::BinClip, m_binId.toInt()}, this, false);
        }
123
    }
124
125
}

126
// static
Nicolas Carion's avatar
Nicolas Carion committed
127
128
std::shared_ptr<ProjectClip> ProjectClip::construct(const QString &id, const QIcon &thumb, const std::shared_ptr<ProjectItemModel> &model,
                                                    const std::shared_ptr<Mlt::Producer> &producer)
129
{
130
    std::shared_ptr<ProjectClip> self(new ProjectClip(id, thumb, model, producer));
131
    baseFinishConstruct(self);
Vincent Pinon's avatar
Vincent Pinon committed
132
    QMetaObject::invokeMethod(model.get(), "loadSubClips", Qt::QueuedConnection, Q_ARG(QString, id), Q_ARG(QString, self->getProducerProperty(QStringLiteral("kdenlive:clipzones"))));
133
134
135
    return self;
}

136
void ProjectClip::importEffects(const std::shared_ptr<Mlt::Producer> &producer, const QString &originalDecimalPoint)
137
{
138
    m_effectStack->importEffects(producer, PlaylistState::Disabled, true, originalDecimalPoint);
139
140
}

Nicolas Carion's avatar
Nicolas Carion committed
141
ProjectClip::ProjectClip(const QString &id, const QDomElement &description, const QIcon &thumb, const std::shared_ptr<ProjectItemModel> &model)
142
    : AbstractProjectItem(AbstractProjectItem::ClipItem, id, model)
143
    , ClipController(id)
144
    , m_resetTimelineOccurences(false)
145
    , m_audioCount(0)
146
    , m_uuid(QUuid::createUuid())
147
{
148
    m_clipStatus = FileStatus::StatusWaiting;
149
    m_thumbnail = thumb;
150
    m_markerModel = std::make_shared<MarkerListModel>(m_binId, pCore->projectManager()->undoStack());
151
    if (description.hasAttribute(QStringLiteral("type"))) {
152
        m_clipType = ClipType::ProducerType(description.attribute(QStringLiteral("type")).toInt());
153
        if (m_clipType == ClipType::Audio) {
154
            m_thumbnail = QIcon::fromTheme(QStringLiteral("audio-x-generic"));
155
        }
156
    }
157
    m_temporaryUrl = getXmlProperty(description, QStringLiteral("resource"));
158
    QString clipName = getXmlProperty(description, QStringLiteral("kdenlive:clipname"));
159
160
    if (!clipName.isEmpty()) {
        m_name = clipName;
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
161
    } else if (!m_temporaryUrl.isEmpty()) {
162
        m_name = QFileInfo(m_temporaryUrl).fileName();
163
164
    } else {
        m_name = i18n("Untitled");
165
    }
166
167
    m_boundaryTimer.setSingleShot(true);
    m_boundaryTimer.setInterval(500);
Vincent Pinon's avatar
Vincent Pinon committed
168
    connect(m_markerModel.get(), &MarkerListModel::modelChanged, this, [&]() { setProducerProperty(QStringLiteral("kdenlive:markers"), m_markerModel->toJson()); });
169
170
}

171
172
std::shared_ptr<ProjectClip> ProjectClip::construct(const QString &id, const QDomElement &description, const QIcon &thumb,
                                                    std::shared_ptr<ProjectItemModel> model)
173
{
Nicolas Carion's avatar
Nicolas Carion committed
174
    std::shared_ptr<ProjectClip> self(new ProjectClip(id, description, thumb, std::move(model)));
175
176
177
178
    baseFinishConstruct(self);
    return self;
}

179
180
181
182
ProjectClip::~ProjectClip()
{
}

183
184
void ProjectClip::connectEffectStack()
{
Vincent Pinon's avatar
Vincent Pinon committed
185
    connect(m_effectStack.get(), &EffectStackModel::dataChanged, this, [&]() {
186
187
        if (auto ptr = m_model.lock()) {
            std::static_pointer_cast<ProjectItemModel>(ptr)->onItemUpdated(std::static_pointer_cast<ProjectClip>(shared_from_this()),
188
                                                                           {AbstractProjectItem::IconOverlay});
189
190
        }
    });
191
192
}

193
194
QString ProjectClip::getToolTip() const
{
195
196
197
198
    if (m_clipType == ClipType::Color && m_path.contains(QLatin1Char('/'))) {
        return m_path.section(QLatin1Char('/'), -1);
    }
    return m_path;
199
200
}

201
QString ProjectClip::getXmlProperty(const QDomElement &producer, const QString &propertyName, const QString &defaultValue)
202
{
203
    QString value = defaultValue;
204
    QDomNodeList props = producer.elementsByTagName(QStringLiteral("property"));
205
    for (int i = 0; i < props.count(); ++i) {
206
        if (props.at(i).toElement().attribute(QStringLiteral("name")) == propertyName) {
207
208
209
210
211
212
213
            value = props.at(i).firstChild().nodeValue();
            break;
        }
    }
    return value;
}

214
void ProjectClip::updateAudioThumbnail(bool cachedThumb)
215
{
Vincent Pinon's avatar
Vincent Pinon committed
216
    emit audioThumbReady();
217
218
    if (m_clipType == ClipType::Audio) {
        QImage thumb = ThumbnailCache::get()->getThumbnail(m_binId, 0);
219
        if (thumb.isNull() && !pCore->taskManager.hasPendingJob({ObjectType::BinClip, m_binId.toInt()}, AbstractTask::AUDIOTHUMBJOB)) {
220
221
            int iconHeight = int(QFontInfo(qApp->font()).pixelSize() * 3.5);
            QImage img(QSize(int(iconHeight * pCore->getCurrentDar()), iconHeight), QImage::Format_ARGB32);
222
223
224
225
226
227
228
229
230
231
232
233
234
235
            img.fill(Qt::darkGray);
            QMap <int, QString> streams = audioInfo()->streams();
            QMap <int, int> channelsList = audioInfo()->streamChannels();
            QPainter painter(&img);
            QPen pen = painter.pen();
            pen.setColor(Qt::white);
            painter.setPen(pen);
            int streamCount = 0;
            if (streams.count() > 0) {
                double streamHeight = iconHeight / streams.count();
                QMapIterator<int, QString> st(streams);
                while (st.hasNext()) {
                    st.next();
                    int channels = channelsList.value(st.key());
236
                    double channelHeight = double(streamHeight) / channels;
237
                    const QVector <uint8_t> audioLevels = audioFrameCache(st.key());
238
239
                    qreal indicesPrPixel = qreal(audioLevels.length()) / img.width();
                    int idx;
240
241
242
                    for (int channel = 0; channel < channels; channel++) {
                        double y = (streamHeight * streamCount) + (channel * channelHeight) + channelHeight / 2;
                        for (int i = 0; i <= img.width(); i++) {
243
                            idx = int(ceil(i * indicesPrPixel));
244
245
                            idx += idx % channels;
                            idx += channel;
246
247
                            if (idx >= audioLevels.length() || idx < 0) {
                                break;
248
                            }
249
                            double level = audioLevels.at(idx) * channelHeight / 510.; // divide height by 510 (2*255) to get height
250
                            painter.drawLine(i, int(y - level), i, int(y + level));
251
                        }
252
253
254
255
256
257
258
259
                    }
                    streamCount++;
                }
            }
            thumb = img;
            // Cache thumbnail
            ThumbnailCache::get()->storeThumbnail(m_binId, 0, thumb, true);
        }
260
261
262
        if (!thumb.isNull()) {
            setThumbnail(thumb, -1, -1);
        }
263
    }
264
265
266
    if (!KdenliveSettings::audiothumbnails()) {
        return;
    }
267
    m_audioThumbCreated = true;
268
269
270
271
    if (!cachedThumb) {
        // Audio was just created
        updateTimelineClips({TimelineModel::ReloadAudioThumbRole});
    }
272
273
274
275
}

bool ProjectClip::audioThumbCreated() const
{
276
    return (m_audioThumbCreated);
277
278
}

279
ClipType::ProducerType ProjectClip::clipType() const
280
{
281
    return m_clipType;
282
283
}

284
285
bool ProjectClip::hasParent(const QString &id) const
{
286
287
    std::shared_ptr<AbstractProjectItem> par = parent();
    while (par) {
288
289
290
291
        if (par->clipId() == id) {
            return true;
        }
        par = par->parent();
292
293
294
295
    }
    return false;
}

296
std::shared_ptr<ProjectClip> ProjectClip::clip(const QString &id)
297
{
Nicolas Carion's avatar
Nicolas Carion committed
298
    if (id == m_binId) {
299
        return std::static_pointer_cast<ProjectClip>(shared_from_this());
300
    }
301
    return std::shared_ptr<ProjectClip>();
302
303
}

304
std::shared_ptr<ProjectFolder> ProjectClip::folder(const QString &id)
305
{
306
    Q_UNUSED(id)
307
    return std::shared_ptr<ProjectFolder>();
308
309
}

310
std::shared_ptr<ProjectSubClip> ProjectClip::getSubClip(int in, int out)
311
{
312
    for (int i = 0; i < childCount(); ++i) {
313
        std::shared_ptr<ProjectSubClip> clip = std::static_pointer_cast<ProjectSubClip>(child(i))->subClip(in, out);
314
315
316
317
        if (clip) {
            return clip;
        }
    }
318
    return std::shared_ptr<ProjectSubClip>();
319
320
}

321
322
323
QStringList ProjectClip::subClipIds() const
{
    QStringList subIds;
324
    for (int i = 0; i < childCount(); ++i) {
325
        std::shared_ptr<AbstractProjectItem> clip = std::static_pointer_cast<AbstractProjectItem>(child(i));
326
327
328
329
330
331
332
        if (clip) {
            subIds << clip->clipId();
        }
    }
    return subIds;
}

333
std::shared_ptr<ProjectClip> ProjectClip::clipAt(int ix)
334
{
Nicolas Carion's avatar
Nicolas Carion committed
335
    if (ix == row()) {
336
        return std::static_pointer_cast<ProjectClip>(shared_from_this());
337
    }
338
    return std::shared_ptr<ProjectClip>();
339
340
}

341
/*bool ProjectClip::isValid() const
342
{
343
344
    return m_controller->isValid();
}*/
345

346
347
bool ProjectClip::hasUrl() const
{
348
    if ((m_clipType != ClipType::Color) && (m_clipType != ClipType::Unknown)) {
349
        return (!clipUrl().isEmpty());
350
    }
351
352
353
    return false;
}

354
const QString ProjectClip::url() const
355
{
356
    return clipUrl();
357
358
}

359
360
361
362
363
const QSize ProjectClip::frameSize() const
{
    return getFrameSize();
}

364
GenTime ProjectClip::duration() const
365
{
366
    return getPlaytime();
367
368
}

369
size_t ProjectClip::frameDuration() const
370
{
371
    return size_t(getFramePlaytime());
372
373
}

374
void ProjectClip::reloadProducer(bool refreshOnly, bool isProxy, bool forceAudioReload)
375
{
376
    // we find if there are some loading job on that clip
377
    QMutexLocker lock(&m_thumbMutex);
378
    if (refreshOnly) {
379
380
        // In that case, we only want a new thumbnail.
        // We thus set up a thumb job. We must make sure that there is no pending LOADJOB
381
        // Clear cache first
382
        ThumbnailCache::get()->invalidateThumbsForClip(m_binId);
383
        pCore->taskManager.discardJobs({ObjectType::BinClip, m_binId.toInt()}, AbstractTask::LOADJOB, true);
384
        pCore->taskManager.discardJobs({ObjectType::BinClip, m_binId.toInt()}, AbstractTask::CACHEJOB);
385
        m_thumbsProducer.reset();
386
387
388
        // Reset uuid to enforce reloading thumbnails from qml cache
        m_uuid = QUuid::createUuid();
        updateTimelineClips({TimelineModel::ClipThumbRole});
389
        ClipLoadTask::start({ObjectType::BinClip,m_binId.toInt()}, QDomElement(), true, -1, -1, this);
390
    } else {
391
        // If another load job is running?
392
393
        pCore->taskManager.discardJobs({ObjectType::BinClip, m_binId.toInt()}, AbstractTask::LOADJOB, true);
        pCore->taskManager.discardJobs({ObjectType::BinClip, m_binId.toInt()}, AbstractTask::CACHEJOB);
394
        if (QFile::exists(m_path) && (!isProxy && !hasProxy()) && m_properties) {
395
396
            clearBackupProperties();
        }
397
        QDomDocument doc;
398
        QDomElement xml;
399
400
401
402
        QString resource;
        if (m_properties) {
            resource = m_properties->get("resource");
        }
403
404
405
406
407
        if (m_service.isEmpty() && !resource.isEmpty()) {
            xml = ClipCreator::getXmlFromUrl(resource).documentElement();
        } else {
            xml = toXml(doc);
        }
408
        if (!xml.isNull()) {
409
            bool hashChanged = false;
410
            m_thumbsProducer.reset();
411
            ClipType::ProducerType type = clipType();
412
            if (type != ClipType::Color && type != ClipType::Image && type != ClipType::SlideShow) {
413
414
                xml.removeAttribute("out");
            }
415
416
417
418
419
420
421
422
423
424
            if (type == ClipType::Audio || type == ClipType::AV) {
                // Check if source file was changed and rebuild audio data if necessary
                QString clipHash = getProducerProperty(QStringLiteral("kdenlive:file_hash"));
                if (!clipHash.isEmpty()) {
                    if (clipHash != getFileHash()) {
                        // Source clip has changed, rebuild data
                        hashChanged = true;
                    }
                }
            }
425
            m_audioThumbCreated = false;
426
427
            // Reset uuid to enforce reloading thumbnails from qml cache
            m_uuid = QUuid::createUuid();
428
            if (forceAudioReload || (!isProxy && hashChanged)) {
429
430
                discardAudioThumb();
            }
431
            ThumbnailCache::get()->invalidateThumbsForClip(clipId());
432
            m_clipStatus = FileStatus::StatusWaiting;
433
            m_thumbsProducer.reset();
434
            ClipLoadTask::start({ObjectType::BinClip,m_binId.toInt()}, xml, false, -1, -1, this);
435
        }
436
    }
437
438
}

439
QDomElement ProjectClip::toXml(QDomDocument &document, bool includeMeta, bool includeProfile)
440
{
441
    getProducerXML(document, includeMeta, includeProfile);
442
443
444
445
446
447
    QDomElement prod;
    if (document.documentElement().tagName() == QLatin1String("producer")) {
        prod = document.documentElement();
    } else {
        prod = document.documentElement().firstChildElement(QStringLiteral("producer"));
    }
448
    if (m_clipType != ClipType::Unknown) {
449
        prod.setAttribute(QStringLiteral("type"), int(m_clipType));
450
    }
451
    return prod;
452
453
}

454
void ProjectClip::setThumbnail(const QImage &img, int in, int out, bool inCache)
455
{
456
457
458
    if (img.isNull()) {
        return;
    }
459
460
461
462
463
464
465
    if (in > -1) {
        std::shared_ptr<ProjectSubClip> sub = getSubClip(in, out);
        if (sub) {
            sub->setThumbnail(img);
        }
        return;
    }
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
466
467
    QPixmap thumb = roundedPixmap(QPixmap::fromImage(img));
    if (hasProxy() && !thumb.isNull()) {
468
        // Overlay proxy icon
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
469
        QPainter p(&thumb);
470
        QColor c(220, 220, 10, 200);
471
        QRect r(0, 0, int(thumb.height() / 2.5), int(thumb.height() / 2.5));
472
473
474
475
476
477
478
479
        p.fillRect(r, c);
        QFont font = p.font();
        font.setPixelSize(r.height());
        font.setBold(true);
        p.setFont(font);
        p.setPen(Qt::black);
        p.drawText(r, Qt::AlignCenter, i18nc("The first letter of Proxy, used as abbreviation", "P"));
    }
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
480
    m_thumbnail = QIcon(thumb);
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
481
    if (auto ptr = m_model.lock()) {
482
        std::static_pointer_cast<ProjectItemModel>(ptr)->onItemUpdated(std::static_pointer_cast<ProjectClip>(shared_from_this()),
483
                                                                       {AbstractProjectItem::DataThumbnail});
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
484
    }
485
486
    if (!inCache && (m_clipType == ClipType::Text || m_clipType == ClipType::TextTemplate)) {
        // Title clips always use the same thumb as bin, refresh
487
        updateTimelineClips({TimelineModel::ClipThumbRole});
488
    }
489
490
}

491
492
bool ProjectClip::hasAudioAndVideo() const
{
493
    return hasAudio() && hasVideo() && m_masterProducer->get_int("set.test_image") == 0 && m_masterProducer->get_int("set.test_audio") == 0;
494
495
496
497
498
}

bool ProjectClip::isCompatible(PlaylistState::ClipState state) const
{
    switch (state) {
499
500
501
502
503
504
    case PlaylistState::AudioOnly:
        return hasAudio() && (m_masterProducer->get_int("set.test_audio") == 0);
    case PlaylistState::VideoOnly:
        return hasVideo() && (m_masterProducer->get_int("set.test_image") == 0);
    default:
        return true;
505
506
507
    }
}

508
509
510
511
512
QPixmap ProjectClip::thumbnail(int width, int height)
{
    return m_thumbnail.pixmap(width, height);
}

513
bool ProjectClip::setProducer(std::shared_ptr<Mlt::Producer> producer, bool generateThumb)
514
{
515
    qDebug() << "################### ProjectClip::setproducer #################";
516
    QMutexLocker locker(&m_producerMutex);
517
    FileStatus::ClipStatus currentStatus = m_clipStatus;
518
    // Make sure we have a hash for this clip
Nicolas Carion's avatar
Nicolas Carion committed
519
    updateProducer(producer);
520
    getFileHash();
521
    emit producerChanged(m_binId, producer);
522
    m_thumbsProducer.reset();
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
523
    connectEffectStack();
524

525
526
527
    // Update info
    if (m_name.isEmpty()) {
        m_name = clipName();
528
    }
529
530
531
    m_date = date;
    m_description = ClipController::description();
    m_temporaryUrl.clear();
532
    if (m_clipType == ClipType::Audio) {
533
        m_thumbnail = QIcon::fromTheme(QStringLiteral("audio-x-generic"));
534
    } else if (m_clipType == ClipType::Image) {
535
        if (producer->get_int("meta.media.width") < 8 || producer->get_int("meta.media.height") < 8) {
536
537
            KMessageBox::information(QApplication::activeWindow(),
                                     i18n("Image dimension smaller than 8 pixels.\nThis is not correctly supported by our video framework."));
538
        }
539
    }
540
    m_duration = getStringDuration();
541
    m_clipStatus = m_usesProxy ? FileStatus::StatusProxy : FileStatus::StatusReady;
542
    QVector<int>updateRoles;
543
    if (m_clipStatus != currentStatus) {
544
        updateRoles = {AbstractProjectItem::ClipStatus, AbstractProjectItem::IconOverlay};
545
        updateTimelineClips({TimelineModel::StatusRole,TimelineModel::ClipThumbRole});
546
    }
547
    setTags(getProducerProperty(QStringLiteral("kdenlive:tags")));
548
    AbstractProjectItem::setRating(uint(getProducerIntProperty(QStringLiteral("kdenlive:rating"))));
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
549
    if (auto ptr = m_model.lock()) {
550
        updateRoles << AbstractProjectItem::DataDuration;
551
        std::static_pointer_cast<ProjectItemModel>(ptr)->onItemUpdated(std::static_pointer_cast<ProjectClip>(shared_from_this()),
552
                                                                       updateRoles);
553
        std::static_pointer_cast<ProjectItemModel>(ptr)->updateWatcher(std::static_pointer_cast<ProjectClip>(shared_from_this()));
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
554
    }
555
556
    // set parent again (some info need to be stored in producer)
    updateParent(parentItem().lock());
557
558
559
560
    if (generateThumb && m_clipType != ClipType::Audio) {
        // Generate video thumb
        ClipLoadTask::start({ObjectType::BinClip,m_binId.toInt()}, QDomElement(), true, -1, -1, this);
    }
561
    if (KdenliveSettings::audiothumbnails() && (m_clipType == ClipType::AV || m_clipType == ClipType::Audio || m_clipType == ClipType::Playlist || m_clipType == ClipType::Unknown)) {
562
563
        AudioLevelsTask::start({ObjectType::BinClip, m_binId.toInt()}, this, false);
    }
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
    pCore->bin()->reloadMonitorIfActive(clipId());
    for (auto &p : m_audioProducers) {
        m_effectStack->removeService(p.second);
    }
    for (auto &p : m_videoProducers) {
        m_effectStack->removeService(p.second);
    }
    for (auto &p : m_timewarpProducers) {
        m_effectStack->removeService(p.second);
    }
    // Release audio producers
    m_audioProducers.clear();
    m_videoProducers.clear();
    m_timewarpProducers.clear();
    emit refreshPropertiesPanel();
579
580
581
582
583
    if (m_hasLimitedDuration) {
        connect(&m_boundaryTimer, &QTimer::timeout, this, &ProjectClip::refreshBounds);
    } else {
        disconnect(&m_boundaryTimer, &QTimer::timeout, this, &ProjectClip::refreshBounds);
    }
584
585
586
587
    replaceInTimeline();
    updateTimelineClips({TimelineModel::IsProxyRole});
    bool generateProxy = false;
    QList<std::shared_ptr<ProjectClip>> clipList;
588
    if (pCore->currentDoc()->useProxy() && pCore->currentDoc()->getDocumentProperty(QStringLiteral("generateproxy")).toInt() == 1) {
589
        // automatic proxy generation enabled
590
        if (m_clipType == ClipType::Image && pCore->currentDoc()->getDocumentProperty(QStringLiteral("generateimageproxy")).toInt() == 1) {
591
            if (getProducerIntProperty(QStringLiteral("meta.media.width")) >= KdenliveSettings::proxyimageminsize() &&
Vincent Pinon's avatar
Vincent Pinon committed
592
                getProducerProperty(QStringLiteral("kdenlive:proxy")) == QLatin1String()) {
593
594
                clipList << std::static_pointer_cast<ProjectClip>(shared_from_this());
            }
595
        } else if (pCore->currentDoc()->getDocumentProperty(QStringLiteral("generateproxy")).toInt() == 1 &&
Vincent Pinon's avatar
Vincent Pinon committed
596
                   (m_clipType == ClipType::AV || m_clipType == ClipType::Video) && getProducerProperty(QStringLiteral("kdenlive:proxy")) == QLatin1String()) {
597
598
599
600
601
602
603
604
605
            bool skipProducer = false;
            if (pCore->currentDoc()->getDocumentProperty(QStringLiteral("enableexternalproxy")).toInt() == 1) {
                QStringList externalParams = pCore->currentDoc()->getDocumentProperty(QStringLiteral("externalproxyparams")).split(QLatin1Char(';'));
                // We have a camcorder profile, check if we have opened a proxy clip
                if (externalParams.count() >= 6) {
                    QFileInfo info(m_path);
                    QDir dir = info.absoluteDir();
                    dir.cd(externalParams.at(3));
                    QString fileName = info.fileName();
606
607
608
609
                    if (fileName.startsWith(externalParams.at(1))) {
                        fileName.remove(0, externalParams.at(1).size());
                        fileName.prepend(externalParams.at(4));
                    }
610
611
612
613
614
615
616
617
618
619
620
621
622
623
                    if (!externalParams.at(2).isEmpty()) {
                        fileName.chop(externalParams.at(2).size());
                    }
                    fileName.append(externalParams.at(5));
                    if (dir.exists(fileName)) {
                        setProducerProperty(QStringLiteral("kdenlive:proxy"), m_path);
                        m_path = dir.absoluteFilePath(fileName);
                        setProducerProperty(QStringLiteral("kdenlive:originalurl"), m_path);
                        getFileHash();
                        skipProducer = true;
                    }
                }
            }
            if (!skipProducer && getProducerIntProperty(QStringLiteral("meta.media.width")) >= KdenliveSettings::proxyminsize()) {
624
625
                clipList << std::static_pointer_cast<ProjectClip>(shared_from_this());
            }
626
627
628
        } else if (m_clipType == ClipType::Playlist && pCore->getCurrentFrameDisplaySize().width() >= KdenliveSettings::proxyminsize() &&
                getProducerProperty(QStringLiteral("kdenlive:proxy")) == QLatin1String()) {
            clipList << std::static_pointer_cast<ProjectClip>(shared_from_this());
629
        }
630
        if (!clipList.isEmpty()) {
631
            generateProxy = true;
632
633
        }
    }
634
    if (!generateProxy && KdenliveSettings::hoverPreview() && (m_clipType == ClipType::AV || m_clipType == ClipType::Video || m_clipType == ClipType::Playlist)) {
635
        QTimer::singleShot(1000, this, [this]() {
636
            CacheTask::start({ObjectType::BinClip,m_binId.toInt()}, 30, 0, 0, this);
637
638
        });
    }
639
    if (generateProxy) {
640
        QMetaObject::invokeMethod(pCore->currentDoc(), "slotProxyCurrentItem", Q_ARG(bool,true), Q_ARG(QList<std::shared_ptr<ProjectClip> >,clipList), Q_ARG(bool,false));
641
    }
642
    return true;
643
644
}

645
646
647
648
649
void ProjectClip::setThumbProducer(std::shared_ptr<Mlt::Producer>prod)
{
    m_thumbsProducer = std::move(prod);
}

650
std::shared_ptr<Mlt::Producer> ProjectClip::thumbProducer()
651
652
653
654
{
    if (m_thumbsProducer) {
        return m_thumbsProducer;
    }
655
    if (clipType() == ClipType::Unknown || m_masterProducer == nullptr || m_clipStatus == FileStatus::StatusWaiting) {
Laurent Montel's avatar
Laurent Montel committed
656
        return nullptr;
657
    }
658
    QMutexLocker lock(&m_thumbMutex);
659
    if (KdenliveSettings::gpu_accel()) {
660
        // TODO: when the original producer changes, we must reload this thumb producer
661
        m_thumbsProducer = softClone(ClipController::getPassPropertiesList());
662
    } else {
663
664
665
666
667
        QString mltService = m_masterProducer->get("mlt_service");
        const QString mltResource = m_masterProducer->get("resource");
        if (mltService == QLatin1String("avformat")) {
            mltService = QStringLiteral("avformat-novalidate");
        }
668
669
670
671
672
        Mlt::Profile *profile = pCore->thumbProfile();
        if (mltService.startsWith(QLatin1String("xml"))) {
            // Xml producers can corrupt the profile, so enforce width/height again after loading
            int profileWidth = profile->width();
            int profileHeight= profile->height();
673
            m_thumbsProducer.reset(new Mlt::Producer(*profile, "consumer", mltResource.toUtf8().constData()));
674
675
676
677
678
            profile->set_width(profileWidth);
            profile->set_height(profileHeight);
        } else {
            m_thumbsProducer.reset(new Mlt::Producer(*profile, mltService.toUtf8().constData(), mltResource.toUtf8().constData()));
        }
679
680
681
682
683
684
685
686
        if (m_thumbsProducer->is_valid()) {
            Mlt::Properties original(m_masterProducer->get_properties());
            Mlt::Properties cloneProps(m_thumbsProducer->get_properties());
            cloneProps.pass_list(original, ClipController::getPassPropertiesList());
            Mlt::Filter scaler(*pCore->thumbProfile(), "swscale");
            Mlt::Filter padder(*pCore->thumbProfile(), "resize");
            Mlt::Filter converter(*pCore->thumbProfile(), "avcolor_space");
            m_thumbsProducer->set("audio_index", -1);
687
688
            // Required to make get_playtime() return > 1
            m_thumbsProducer->set("out", m_thumbsProducer->get_length() -1);
689
690
691
692
            m_thumbsProducer->attach(scaler);
            m_thumbsProducer->attach(padder);
            m_thumbsProducer->attach(converter);
        }
693
694
695
696
    }
    return m_thumbsProducer;
}

Nicolas Carion's avatar
Nicolas Carion committed
697
698
699
void ProjectClip::createDisabledMasterProducer()
{
    if (!m_disabledProducer) {
700
        m_disabledProducer = cloneProducer();
Nicolas Carion's avatar
Nicolas Carion committed
701
702
        m_disabledProducer->set("set.test_audio", 1);
        m_disabledProducer->set("set.test_image", 1);
703
        m_effectStack->addService(m_disabledProducer);
Nicolas Carion's avatar
Nicolas Carion committed
704
705
    }
}
706

707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
int ProjectClip::getRecordTime()
{
    if (m_masterProducer && (m_clipType == ClipType::AV || m_clipType == ClipType::Video || m_clipType == ClipType::Audio)) {
        int recTime = m_masterProducer->get_int("kdenlive:record_date");
        if (recTime > 0) {
            return recTime;
        }
        if (recTime < 0) {
            // Cannot read record date on this clip, abort
            return 0;
        }
        // Try to get record date metadata
        if (KdenliveSettings::mediainfopath().isEmpty()) {
        }
        QProcess extractInfo;
        extractInfo.start(KdenliveSettings::mediainfopath(), {url(),QStringLiteral("--output=XML")});
        extractInfo.waitForFinished();
        if(extractInfo.exitStatus() != QProcess::NormalExit || extractInfo.exitCode() != 0) {
            KMessageBox::error(QApplication::activeWindow(), i18n("Cannot extract metadata from %1\n%2", url(),
                        QString(extractInfo.readAllStandardError())));
            return 0;
        }
        QDomDocument doc;
        doc.setContent(extractInfo.readAllStandardOutput());
731
732
733
734
735
736
        bool dateFormat = false;
        QDomNodeList nodes = doc.documentElement().elementsByTagName(QStringLiteral("TimeCode_FirstFrame"));
        if (nodes.isEmpty()) {
            nodes = doc.documentElement().elementsByTagName(QStringLiteral("Recorded_Date"));
            dateFormat = true;
        }
737
        if (!nodes.isEmpty()) {
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
            // Parse recorded time (HH:MM:SS)
            QString recInfo = nodes.at(0).toElement().text();
            if (!recInfo.isEmpty()) {
                if (dateFormat) {
                    if (recInfo.contains(QLatin1Char('+'))) {
                        recInfo = recInfo.section(QLatin1Char('+'), 0, 0);
                    } else if (recInfo.contains(QLatin1Char('-'))) {
                        recInfo = recInfo.section(QLatin1Char('-'), 0, 0);
                    }
                    QDateTime date = QDateTime::fromString(recInfo, "yyyy-MM-dd hh:mm:ss");
                    recTime = date.time().msecsSinceStartOfDay();
                } else {
                    // Timecode Format HH:MM:SS:FF
                    // Check if we have a different fps
                    double producerFps = m_masterProducer->get_double("meta.media.frame_rate_num") / m_masterProducer->get_double("meta.media.frame_rate_den");
                    if (!qFuzzyCompare(producerFps, pCore->getCurrentFps())) {
                        // Producer and project have a different fps
                        bool ok;
                        int frames = recInfo.section(QLatin1Char(':'), -1).toInt(&ok);
                        if (ok) {
Vincent Pinon's avatar
Vincent Pinon committed
758
                            frames *= int(pCore->getCurrentFps() / producerFps);
759
760
761
762
                            recInfo.chop(2);
                            recInfo.append(QString::number(frames).rightJustified(1, QChar('0')));
                        }
                    }
763
                    recTime = int(1000 * pCore->timecode().getFrameCount(recInfo) / pCore->getCurrentFps());
764
765
766
767
768
769
770
771
772
773
774
775
                }
                m_masterProducer->set("kdenlive:record_date", recTime);
                return recTime;
            }
        } else {
            m_masterProducer->set("kdenlive:record_date", -1);
            return 0;
        }
    }
    return 0;
}

776
std::shared_ptr<Mlt::Producer> ProjectClip::getTimelineProducer(int trackId, int clipId, PlaylistState::ClipState state, int audioStream, double speed, bool secondPlaylist, bool timeremap)
777
{
778
779
780
    if (!m_masterProducer) {
        return nullptr;
    }
781
    if (qFuzzyCompare(speed, 1.0) && !timeremap) {
782
        // we are requesting a normal speed producer
783
784
785
786
787
        bool byPassTrackProducer = false;
        if (trackId == -1 && (state != PlaylistState::AudioOnly || audioStream == m_masterProducer->get_int("audio_index"))) {
            byPassTrackProducer = true;
        }
        if (byPassTrackProducer ||
788
            (state == PlaylistState::VideoOnly && (m_clipType == ClipType::Color || m_clipType == ClipType::Image || m_clipType == ClipType::Text|| m_clipType == ClipType::TextTemplate || m_clipType == ClipType::Qml))) {
789
            // Temporary copy, return clone of master
790
            int duration = m_masterProducer->time_to_frames(m_masterProducer->get("kdenlive:duration"));
791
            return std::shared_ptr<Mlt::Producer>(m_masterProducer->cut(-1, duration > 0 ? duration - 1 : -1));
792
        }
793
794
795
796
        if (m_timewarpProducers.count(clipId) > 0) {
            m_effectStack->removeService(m_timewarpProducers[clipId]);
            m_timewarpProducers.erase(clipId);
        }
797
798
        if (state == PlaylistState::AudioOnly) {
            // We need to get an audio producer, if none exists
799
800
801
802
803
804
805
            if (audioStream > -1) {
                if (trackId >= 0) {
                    trackId += 100 * audioStream;
                } else {
                    trackId -= 100 * audioStream;
                }
            }
806
807
808
809
            // second playlist producers use negative trackId
            if (secondPlaylist) {
                trackId = -trackId;
            }
810
811
812
813
            if (m_audioProducers.count(trackId) == 0) {
                m_audioProducers[trackId] = cloneProducer(true);
                m_audioProducers[trackId]->set("set.test_audio", 0);
                m_audioProducers[trackId]->set("set.test_image", 1);
814
815
                if (m_streamEffects.contains(audioStream)) {
                    QStringList effects = m_streamEffects.value(audioStream);
Vincent Pinon's avatar
Vincent Pinon committed
816
                    for (const QString &effect : qAsConst(effects)) {
817
818
819
820
821
822
823
824
                        Mlt::Filter filt(*m_audioProducers[trackId]->profile(), effect.toUtf8().constData());
                        if (filt.is_valid()) {
                            // Add stream effect markup
                            filt.set("kdenlive:stream", 1);
                            m_audioProducers[trackId]->attach(filt);
                        }
                    }
                }
825
826
827
                if (audioStream > -1) {
                    m_audioProducers[trackId]->set("audio_index", audioStream);
                }