projectclip.cpp 42.5 KB
Newer Older
1 2 3 4 5 6 7 8 9 10
/*
Copyright (C) 2012  Till Theato <root@ttill.de>
Copyright (C) 2014  Jean-Baptiste Mardelle <jb@kdenlive.org>
This file is part of Kdenlive. See www.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) version 3 or any later version
accepted by the membership of KDE e.V. (or its successor approved
11
by the membership of KDE e.V.), which shall act as a proxy
12 13 14 15 16 17 18 19 20 21 22 23
defined in Section 14 of version 3 of the license.

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, see <http://www.gnu.org/licenses/>.
*/

#include "projectclip.h"
24
#include "bin.h"
Nicolas Carion's avatar
Nicolas Carion committed
25
#include "core.h"
26 27
#include "doc/docundostack.hpp"
#include "doc/kdenlivedoc.h"
28
#include "doc/kthumb.h"
Nicolas Carion's avatar
Nicolas Carion committed
29
#include "effects/effectstack/model/effectstackmodel.hpp"
30
#include "jobs/jobmanager.h"
31
#include "jobs/loadjob.hpp"
32
#include "jobs/thumbjob.hpp"
33
#include "kdenlivesettings.h"
34
#include "lib/audio/audioStreamInfo.h"
35
#include "mltcontroller/clip.h"
36
#include "mltcontroller/clipcontroller.h"
37
#include "mltcontroller/clippropertiescontroller.h"
38
#include "model/markerlistmodel.hpp"
39
#include "profiles/profilemodel.hpp"
Nicolas Carion's avatar
Nicolas Carion committed
40
#include "project/projectcommands.h"
41
#include "project/projectmanager.h"
Nicolas Carion's avatar
Nicolas Carion committed
42 43 44 45
#include "projectfolder.h"
#include "projectitemmodel.h"
#include "projectsubclip.h"
#include "timecode.h"
Nicolas Carion's avatar
style  
Nicolas Carion committed
46
#include "timeline2/model/snapmodel.hpp"
Nicolas Carion's avatar
Nicolas Carion committed
47
#include "utils/KoIconUtils.h"
48
#include "utils/thumbnailcache.hpp"
49
#include "xml/xml.hpp"
50
#include <QPainter>
51
#include <jobs/proxyclipjob.h>
52
#include <kimagecache.h>
53

Laurent Montel's avatar
Laurent Montel committed
54
#include "kdenlive_debug.h"
55
#include <KLocalizedString>
56
#include <KMessageBox>
Nicolas Carion's avatar
Nicolas Carion committed
57 58 59 60 61
#include <QCryptographicHash>
#include <QDir>
#include <QDomElement>
#include <QFile>
#include <QtConcurrent>
Nicolas Carion's avatar
Nicolas Carion committed
62
#include <utility>
63

64 65
ProjectClip::ProjectClip(const QString &id, const QIcon &thumb, std::shared_ptr<ProjectItemModel> model, std::shared_ptr<Mlt::Producer> producer)
    : AbstractProjectItem(AbstractProjectItem::ClipItem, id, model)
66
    , ClipController(id, producer)
67
    , m_thumbsProducer(nullptr)
68
{
69
    m_markerModel = std::make_shared<MarkerListModel>(id, pCore->projectManager()->undoStack());
70
    m_clipStatus = StatusReady;
71 72 73 74
    m_name = clipName();
    m_duration = getStringDuration();
    m_date = date;
    m_description = ClipController::description();
75
    if (m_clipType == ClipType::Audio) {
76
        m_thumbnail = KoIconUtils::themedIcon(QStringLiteral("audio-x-generic"));
77 78 79
    } else {
        m_thumbnail = thumb;
    }
80 81
    if (m_clipType == ClipType::AV || m_clipType == ClipType::Audio || m_clipType == ClipType::Image || m_clipType == ClipType::Video ||
        m_clipType == ClipType::Playlist || m_clipType == ClipType::TextTemplate) {
82 83
        pCore->bin()->addWatchFile(id, clipUrl());
    }
84 85
    // Make sure we have a hash for this clip
    hash();
86
    connect(m_markerModel.get(), &MarkerListModel::modelChanged, [&]() { setProducerProperty(QStringLiteral("kdenlive:markers"), m_markerModel->toJson()); });
87 88
    QString markers = getProducerProperty(QStringLiteral("kdenlive:markers"));
    if (!markers.isEmpty()) {
89 90
        QMetaObject::invokeMethod(m_markerModel.get(), "importFromJson", Qt::QueuedConnection, Q_ARG(const QString &, markers), Q_ARG(bool, true),
                                  Q_ARG(bool, false));
91
    }
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
92
    connectEffectStack();
93 94
}

95 96
// static
std::shared_ptr<ProjectClip> ProjectClip::construct(const QString &id, const QIcon &thumb, std::shared_ptr<ProjectItemModel> model,
97
                                                    std::shared_ptr<Mlt::Producer> producer)
98
{
99
    std::shared_ptr<ProjectClip> self(new ProjectClip(id, thumb, model, producer));
100
    baseFinishConstruct(self);
101
    model->loadSubClips(id, self->getPropertiesFromPrefix(QStringLiteral("kdenlive:clipzone.")));
102 103 104
    return self;
}

105
ProjectClip::ProjectClip(const QString &id, const QDomElement &description, const QIcon &thumb, std::shared_ptr<ProjectItemModel> model)
106
    : AbstractProjectItem(AbstractProjectItem::ClipItem, id, model)
107
    , ClipController(id)
108
    , m_thumbsProducer(nullptr)
109
{
110
    m_clipStatus = StatusWaiting;
111
    m_thumbnail = thumb;
112
    m_markerModel = std::make_shared<MarkerListModel>(m_binId, pCore->projectManager()->undoStack());
113
    if (description.hasAttribute(QStringLiteral("type"))) {
114
        m_clipType = (ClipType)description.attribute(QStringLiteral("type")).toInt();
115
        if (m_clipType == ClipType::Audio) {
116
            m_thumbnail = KoIconUtils::themedIcon(QStringLiteral("audio-x-generic"));
117
        }
118
    }
119
    m_temporaryUrl = getXmlProperty(description, QStringLiteral("resource"));
120
    QString clipName = getXmlProperty(description, QStringLiteral("kdenlive:clipname"));
121 122
    if (!clipName.isEmpty()) {
        m_name = clipName;
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
123
    } else if (!m_temporaryUrl.isEmpty()) {
124
        m_name = QFileInfo(m_temporaryUrl).fileName();
125 126
    } else {
        m_name = i18n("Untitled");
127
    }
128
    connect(m_markerModel.get(), &MarkerListModel::modelChanged, [&]() { setProducerProperty(QStringLiteral("kdenlive:markers"), m_markerModel->toJson()); });
129 130
}

131 132
std::shared_ptr<ProjectClip> ProjectClip::construct(const QString &id, const QDomElement &description, const QIcon &thumb,
                                                    std::shared_ptr<ProjectItemModel> model)
133
{
134
    std::shared_ptr<ProjectClip> self(new ProjectClip(id, description, thumb, model));
135 136 137 138
    baseFinishConstruct(self);
    return self;
}

139 140
ProjectClip::~ProjectClip()
{
141
    // controller is deleted in bincontroller
142 143
    if (m_clipType == ClipType::AV || m_clipType == ClipType::Audio || m_clipType == ClipType::Image || m_clipType == ClipType::Video ||
        m_clipType == ClipType::Playlist || m_clipType == ClipType::TextTemplate) {
144 145
        pCore->bin()->removeWatchFile(clipId(), clipUrl());
    }
146
    m_thumbMutex.lock();
147
    m_requestedThumbs.clear();
148
    m_thumbMutex.unlock();
149
    m_thumbThread.waitForFinished();
150
    audioFrameCache.clear();
151 152
}

153 154
void ProjectClip::connectEffectStack()
{
155 156 157 158
    connect(m_effectStack.get(), &EffectStackModel::modelChanged, this, &ProjectClip::updateChildProducers);
    connect(m_effectStack.get(), &EffectStackModel::dataChanged, this, &ProjectClip::updateChildProducers);
    /*connect(m_effectStack.get(), &EffectStackModel::modelChanged, [&](){
        qDebug()<<"/ / / STACK CHANGED";
159
        updateChildProducers();
160
    });*/
161 162
}

163 164
QString ProjectClip::getToolTip() const
{
165
    return url();
166 167
}

168
QString ProjectClip::getXmlProperty(const QDomElement &producer, const QString &propertyName, const QString &defaultValue)
169
{
170
    QString value = defaultValue;
171
    QDomNodeList props = producer.elementsByTagName(QStringLiteral("property"));
172
    for (int i = 0; i < props.count(); ++i) {
173
        if (props.at(i).toElement().attribute(QStringLiteral("name")) == propertyName) {
174 175 176 177 178 179 180
            value = props.at(i).firstChild().nodeValue();
            break;
        }
    }
    return value;
}

181
void ProjectClip::updateAudioThumbnail(QVariantList audioLevels)
182
{
183
    std::swap(audioFrameCache, audioLevels); // avoid second copy
184
    m_audioThumbCreated = true;
185 186 187 188
    if (auto ptr = m_model.lock()) {
        emit std::static_pointer_cast<ProjectItemModel>(ptr)->refreshAudioThumbs(m_binId);
    }
    updateTimelineClips({TimelineModel::AudioLevelsRole});
189 190 191 192
}

bool ProjectClip::audioThumbCreated() const
{
193
    return (m_audioThumbCreated);
194 195
}

196 197
ClipType ProjectClip::clipType() const
{
198
    return m_clipType;
199 200
}

201 202
bool ProjectClip::hasParent(const QString &id) const
{
203 204
    std::shared_ptr<AbstractProjectItem> par = parent();
    while (par) {
205 206 207 208
        if (par->clipId() == id) {
            return true;
        }
        par = par->parent();
209 210 211 212
    }
    return false;
}

213
std::shared_ptr<ProjectClip> ProjectClip::clip(const QString &id)
214
{
Nicolas Carion's avatar
Nicolas Carion committed
215
    if (id == m_binId) {
216
        return std::static_pointer_cast<ProjectClip>(shared_from_this());
217
    }
218
    return std::shared_ptr<ProjectClip>();
219 220
}

221
std::shared_ptr<ProjectFolder> ProjectClip::folder(const QString &id)
222
{
223
    Q_UNUSED(id)
224
    return std::shared_ptr<ProjectFolder>();
225 226
}

227
std::shared_ptr<ProjectSubClip> ProjectClip::getSubClip(int in, int out)
228
{
229
    for (int i = 0; i < childCount(); ++i) {
230
        std::shared_ptr<ProjectSubClip> clip = std::static_pointer_cast<ProjectSubClip>(child(i))->subClip(in, out);
231 232 233 234
        if (clip) {
            return clip;
        }
    }
235
    return std::shared_ptr<ProjectSubClip>();
236 237
}

238 239 240
QStringList ProjectClip::subClipIds() const
{
    QStringList subIds;
241
    for (int i = 0; i < childCount(); ++i) {
242
        std::shared_ptr<AbstractProjectItem> clip = std::static_pointer_cast<AbstractProjectItem>(child(i));
243 244 245 246 247 248 249
        if (clip) {
            subIds << clip->clipId();
        }
    }
    return subIds;
}

250
std::shared_ptr<ProjectClip> ProjectClip::clipAt(int ix)
251
{
Nicolas Carion's avatar
Nicolas Carion committed
252
    if (ix == row()) {
253
        return std::static_pointer_cast<ProjectClip>(shared_from_this());
254
    }
255
    return std::shared_ptr<ProjectClip>();
256 257
}

258
/*bool ProjectClip::isValid() const
259
{
260 261
    return m_controller->isValid();
}*/
262

263 264
bool ProjectClip::hasUrl() const
{
265
    if ((m_clipType != ClipType::Color) && (m_clipType != ClipType::Unknown)) {
266
        return (!clipUrl().isEmpty());
267
    }
268 269 270
    return false;
}

271
const QString ProjectClip::url() const
272
{
273
    return clipUrl();
274 275
}

276
GenTime ProjectClip::duration() const
277
{
278
    return getPlaytime();
279 280
}

281 282 283
int ProjectClip::frameDuration() const
{
    GenTime d = duration();
284
    return d.frames(pCore->getCurrentFps());
285 286
}

287
void ProjectClip::reloadProducer(bool refreshOnly)
288
{
289 290 291
    // we find if there are some loading job on that clip
    int loadjobId = -1;
    pCore->jobManager()->hasPendingJob(clipId(), AbstractClipJob::LOADJOB, &loadjobId);
292
    if (refreshOnly) {
293 294
        // 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
295
        // Clear cache first
296
        m_thumbsProducer.reset();
297 298
        ThumbnailCache::get()->invalidateThumbsForClip(clipId());
        pCore->jobManager()->startJob<ThumbJob>({clipId()}, loadjobId, QString(), 150, -1, true, true);
299 300

    } else {
301
        // TODO: check if another load job is running?
302 303 304
        QDomDocument doc;
        QDomElement xml = toXml(doc);
        if (!xml.isNull()) {
305
            m_thumbsProducer.reset();
306
            ThumbnailCache::get()->invalidateThumbsForClip(clipId());
307
            int loadJob = pCore->jobManager()->startJob<LoadJob>({clipId()}, loadjobId, QString(), xml);
308
            pCore->jobManager()->startJob<ThumbJob>({clipId()}, loadJob, QString(), 150, -1, true, true);
309
        }
310
    }
311 312
}

313
QDomElement ProjectClip::toXml(QDomDocument &document, bool includeMeta)
314
{
315 316
    getProducerXML(document, includeMeta);
    QDomElement prod = document.documentElement().firstChildElement(QStringLiteral("producer"));
317
    if (m_clipType != ClipType::Unknown) {
318
        prod.setAttribute(QStringLiteral("type"), (int)m_clipType);
319
    }
320
    return prod;
321 322
}

Laurent Montel's avatar
Laurent Montel committed
323
void ProjectClip::setThumbnail(const QImage &img)
324
{
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
325 326
    QPixmap thumb = roundedPixmap(QPixmap::fromImage(img));
    if (hasProxy() && !thumb.isNull()) {
327
        // Overlay proxy icon
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
328
        QPainter p(&thumb);
329
        QColor c(220, 220, 10, 200);
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
330
        QRect r(0, 0, thumb.height() / 2.5, thumb.height() / 2.5);
331 332 333 334 335 336 337 338
        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
339
    m_thumbnail = QIcon(thumb);
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
340
    if (auto ptr = m_model.lock()) {
341 342
        std::static_pointer_cast<ProjectItemModel>(ptr)->onItemUpdated(std::static_pointer_cast<ProjectClip>(shared_from_this()),
                                                                       AbstractProjectItem::DataThumbnail);
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
343
    }
344 345
}

346 347 348 349 350
QPixmap ProjectClip::thumbnail(int width, int height)
{
    return m_thumbnail.pixmap(width, height);
}

351
bool ProjectClip::setProducer(std::shared_ptr<Mlt::Producer> producer, bool replaceProducer)
352
{
Vincent Pinon's avatar
Vincent Pinon committed
353
    Q_UNUSED(replaceProducer)
354
    qDebug() << "################### ProjectClip::setproducer";
355
    QMutexLocker locker(&m_producerMutex);
Nicolas Carion's avatar
Nicolas Carion committed
356
    updateProducer(std::move(producer));
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
357
    connectEffectStack();
358

359 360 361
    // Update info
    if (m_name.isEmpty()) {
        m_name = clipName();
362
    }
363 364 365
    m_date = date;
    m_description = ClipController::description();
    m_temporaryUrl.clear();
366
    if (m_clipType == ClipType::Audio) {
367
        m_thumbnail = KoIconUtils::themedIcon(QStringLiteral("audio-x-generic"));
368
    } else if (m_clipType == ClipType::Image) {
369
        if (getProducerIntProperty(QStringLiteral("meta.media.width")) < 8 || getProducerIntProperty(QStringLiteral("meta.media.height")) < 8) {
370 371
            KMessageBox::information(QApplication::activeWindow(),
                                     i18n("Image dimension smaller than 8 pixels.\nThis is not correctly supported by our video framework."));
372
        }
373
    }
374
    m_duration = getStringDuration();
375
    m_clipStatus = StatusReady;
376
    if (!hasProxy()) {
Nicolas Carion's avatar
Nicolas Carion committed
377
        if (auto ptr = m_model.lock()) emit std::static_pointer_cast<ProjectItemModel>(ptr)->refreshPanel(m_binId);
378
    }
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
379
    if (auto ptr = m_model.lock()) {
380 381
        std::static_pointer_cast<ProjectItemModel>(ptr)->onItemUpdated(std::static_pointer_cast<ProjectClip>(shared_from_this()),
                                                                       AbstractProjectItem::DataDuration);
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
382
    }
383
    // Make sure we have a hash for this clip
384
    getFileHash();
385 386
    if (m_clipType == ClipType::AV || m_clipType == ClipType::Audio || m_clipType == ClipType::Image || m_clipType == ClipType::Video ||
        m_clipType == ClipType::Playlist || m_clipType == ClipType::TextTemplate) {
387 388
        pCore->bin()->addWatchFile(clipId(), clipUrl());
    }
389 390
    // set parent again (some info need to be stored in producer)
    updateParent(parentItem().lock());
391
    return true;
392 393
}

394
std::shared_ptr<Mlt::Producer> ProjectClip::thumbProducer()
395 396 397 398 399
{
    QMutexLocker locker(&m_producerMutex);
    if (m_thumbsProducer) {
        return m_thumbsProducer;
    }
400
    if (clipType() == ClipType::Unknown) {
Laurent Montel's avatar
Laurent Montel committed
401
        return nullptr;
402
    }
403 404
    std::shared_ptr<Mlt::Producer> prod = originalProducer();
    if (!prod->is_valid()) {
Laurent Montel's avatar
Laurent Montel committed
405
        return nullptr;
406
    }
407
    if (KdenliveSettings::gpu_accel()) {
408
        // TODO: when the original producer changes, we must reload this thumb producer
409
        Clip clip(*prod.get());
410
        m_thumbsProducer = std::make_shared<Mlt::Producer>(clip.softClone(ClipController::getPassPropertiesList()));
411 412
        Mlt::Filter scaler(*prod->profile(), "swscale");
        Mlt::Filter converter(*prod->profile(), "avcolor_space");
413 414
        m_thumbsProducer->attach(scaler);
        m_thumbsProducer->attach(converter);
415
    } else {
416
        m_thumbsProducer = cloneProducer(pCore->thumbProfile());
417 418 419 420
    }
    return m_thumbsProducer;
}

421 422 423 424 425 426 427 428 429
void ProjectClip::createVideoMasterProducer()
{
    if (!m_videoProducer) {
        m_videoProducer = cloneProducer(&pCore->getCurrentProfile()->profile());
        // disable audio but activate video
        m_videoProducer->set("set.test_audio", 1);
        m_videoProducer->set("set.test_image", 0);
    }
}
430
std::shared_ptr<Mlt::Producer> ProjectClip::getTimelineProducer(int clipId, PlaylistState::ClipState state, double speed)
431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478
{
    if (qFuzzyCompare(speed, 1.0)) {
        // we are requesting a normal speed producer
        // We can first cleen the speed producers we have for the current id
        m_timewarpProducers.erase(clipId);
        if (state == PlaylistState::AudioOnly) {
            // We need to get an audio producer, if none exists
            if (m_audioProducers.count(clipId) == 0) {
                m_audioProducers[clipId] = cloneProducer(&pCore->getCurrentProfile()->profile());
                m_audioProducers[clipId]->set("set.test_audio", 0);
                m_audioProducers[clipId]->set("set.test_image", 1);
            }
            return std::shared_ptr<Mlt::Producer>(m_audioProducers[clipId]->cut());
        }
        // we return the video producer
        m_audioProducers.erase(clipId);
        createVideoMasterProducer();
        return std::shared_ptr<Mlt::Producer>(m_videoProducer->cut());
    }

    // in that case, we need to create a warp producer, if we don't have one
    m_audioProducers.erase(clipId);

    std::shared_ptr<Mlt::Producer> warpProducer;
    if (m_timewarpProducers.count(clipId) > 0) {
        if (qFuzzyCompare(m_timewarpProducers[clipId]->get_double("warp_speed"), speed)) {
            // the producer we have is good, use it !
            warpProducer = m_timewarpProducers[clipId];
        }
    }
    if (!warpProducer) {
        QString resource = QString("timewarp:%1:%2").arg(speed).arg(originalProducer()->get("resource"));
        warpProducer.reset(new Mlt::Producer(*originalProducer()->profile(), resource.toUtf8().constData()));
    }

    warpProducer->set("set.test_audio", 1);
    warpProducer->set("set.test_image", 1);
    if (state == PlaylistState::AudioOnly) {
        warpProducer->set("set.test_audio", 0);
    }
    if (state == PlaylistState::VideoOnly) {
        warpProducer->set("set.test_image", 0);
    }
    m_timewarpProducers[clipId] = warpProducer;
    return warpProducer;
}

std::pair<std::shared_ptr<Mlt::Producer>, bool> ProjectClip::giveMasterAndGetTimelineProducer(int clipId, std::shared_ptr<Mlt::Producer> master,
479
                                                                                              PlaylistState::ClipState state)
480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541
{
    int in = master->get_in();
    int out = master->get_out();
    if (master->parent().is_valid()) {
        // in that case, we have a cut
        // check whether it's a timewarp
        double speed = 1.0;
        double timeWarp = false;
        if (QString::fromUtf8(master->parent().get("mlt_service")) == QLatin1String("timewarp")) {
            speed = master->parent().get_double("warp_speed");
            timeWarp = true;
        }
        if (master->parent().get_int("loaded") == 1) {
            // we already have a clip that shares the same master

            if (state == PlaylistState::AudioOnly || timeWarp) {
                // In that case, we must create copies
                return {getTimelineProducer(clipId, state, speed), false};
            }
            // if it's a video or disabled clip, we must make sure that its master clip matches our video master
            if (!m_videoProducer) {
                qDebug() << "Warning: weird, we found a video clip whose master is already loaded but we don't have any yet";
                createVideoMasterProducer();
                return {std::shared_ptr<Mlt::Producer>(m_videoProducer->cut(in, out)), false};
            }
            if (QString::fromUtf8(m_videoProducer->get("id")) != QString::fromUtf8(master->parent().get("id"))) {
                qDebug() << "Warning: weird, we found a video clip whose master is already loaded but doesn't match ours";
                return {std::shared_ptr<Mlt::Producer>(m_videoProducer->cut(in, out)), false};
            }
            // We have a good id, this clip can be used
            return {master, true};
        } else {
            master->parent().set("loaded", 1);
            if (state == PlaylistState::AudioOnly) {
                m_audioProducers[clipId] = std::shared_ptr<Mlt::Producer>(&master->parent());
                return {master, true};
            }
            if (timeWarp) {
                m_timewarpProducers[clipId] = std::shared_ptr<Mlt::Producer>(&master->parent());
                return {master, true};
            }
            if (!m_videoProducer) {
                // good, we found a master video producer, and we didn't have any
                m_videoProducer.reset(&master->parent());
                return {master, true};
            }
            qDebug() << "Warning: weird, we found a video clip whose master is not loaded but we already have a master";
            return {std::shared_ptr<Mlt::Producer>(m_videoProducer->cut(in, out)), false};
        }
    } else if (master->is_valid()) {
        // in that case, we have a master
        qDebug() << "Warning: weird, we received a master clip in lieue of a cut";
        double speed = 1.0;
        if (QString::fromUtf8(master->get("mlt_service")) == QLatin1String("timewarp")) {
            speed = master->get_double("warp_speed");
        }
        return {getTimelineProducer(clipId, state, speed), false};
    }
    // we have a problem
    return {std::shared_ptr<Mlt::Producer>(ClipController::mediaUnavailable->cut()), false};
}
/*
542 543 544
std::shared_ptr<Mlt::Producer> ProjectClip::timelineProducer(PlaylistState::ClipState state, int track)
{
    if (!m_service.startsWith(QLatin1String("avformat"))) {
545
        std::shared_ptr<Mlt::Producer> prod(originalProducer()->cut());
546 547 548 549 550
        int length = getProducerIntProperty(QStringLiteral("kdenlive:duration"));
        if (length > 0) {
            prod->set_in_and_out(0, length);
        }
        return prod;
551 552 553
    }
    if (state == PlaylistState::VideoOnly) {
        if (m_timelineProducers.count(0) > 0) {
554
            return std::shared_ptr<Mlt::Producer>(m_timelineProducers.find(0)->second->cut());
555 556
        }
        std::shared_ptr<Mlt::Producer> videoProd = cloneProducer();
557
        videoProd->set("audio_index", -1);
558
        m_timelineProducers[0] = videoProd;
559
        return std::shared_ptr<Mlt::Producer>(videoProd->cut());
560 561 562
    }
    if (state == PlaylistState::AudioOnly) {
        if (m_timelineProducers.count(-track) > 0) {
563
            return std::shared_ptr<Mlt::Producer>(m_timelineProducers.find(-track)->second->cut());
564 565
        }
        std::shared_ptr<Mlt::Producer> audioProd = cloneProducer();
566
        audioProd->set("video_index", -1);
567
        m_timelineProducers[-track] = audioProd;
568
        return std::shared_ptr<Mlt::Producer>(audioProd->cut());
569 570
    }
    if (m_timelineProducers.count(track) > 0) {
571
        return std::shared_ptr<Mlt::Producer>(m_timelineProducers.find(track)->second->cut());
572 573 574
    }
    std::shared_ptr<Mlt::Producer> normalProd = cloneProducer();
    m_timelineProducers[track] = normalProd;
575
    return std::shared_ptr<Mlt::Producer>(normalProd->cut());
576
}*/
577

578
std::shared_ptr<Mlt::Producer> ProjectClip::cloneProducer(Mlt::Profile *destProfile)
579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597
{
    Mlt::Consumer c(*m_masterProducer->profile(), "xml", "string");
    Mlt::Service s(m_masterProducer->get_service());
    int ignore = s.get_int("ignore_points");
    if (ignore) {
        s.set("ignore_points", 0);
    }
    c.connect(s);
    c.set("time_format", "frames");
    c.set("no_meta", 1);
    c.set("no_root", 1);
    c.set("no_profile", 1);
    c.set("root", "/");
    c.set("store", "kdenlive");
    c.start();
    if (ignore) {
        s.set("ignore_points", ignore);
    }
    const QByteArray clipXml = c.get("string");
598
    std::shared_ptr<Mlt::Producer> prod(new Mlt::Producer(destProfile ? *destProfile : *m_masterProducer->profile(), "xml-string", clipXml.constData()));
599 600 601
    if (strcmp(prod->get("mlt_service"), "avformat") == 0) {
        prod->set("mlt_service", "avformat-novalidate");
    }
602
    return prod;
603 604
}

605
bool ProjectClip::isReady() const
606
{
607
    return m_clipStatus == StatusReady;
608 609
}

610
/*void ProjectClip::setZone(const QPoint &zone)
611 612
{
    m_zone = zone;
613
}*/
614 615 616

QPoint ProjectClip::zone() const
{
617 618
    int x = getProducerIntProperty(QStringLiteral("kdenlive:zone_in"));
    int y = getProducerIntProperty(QStringLiteral("kdenlive:zone_out"));
619 620 621
    if (y <= x) {
        y = getFramePlaytime() - 1;
    }
622
    return QPoint(x, y);
623 624 625 626
}

const QString ProjectClip::hash()
{
627
    QString clipHash = getProducerProperty(QStringLiteral("kdenlive:file_hash"));
628 629
    if (!clipHash.isEmpty()) {
        return clipHash;
630
    }
631
    return getFileHash();
632 633
}

634
const QString ProjectClip::getFileHash()
635
{
636 637
    QByteArray fileData;
    QByteArray fileHash;
638
    switch (m_clipType) {
639
    case ClipType::SlideShow:
640
        fileData = clipUrl().toUtf8();
641 642
        fileHash = QCryptographicHash::hash(fileData, QCryptographicHash::Md5);
        break;
643
    case ClipType::Text:
644
    case ClipType::TextTemplate:
645
        fileData = getProducerProperty(QStringLiteral("xmldata")).toUtf8();
646 647
        fileHash = QCryptographicHash::hash(fileData, QCryptographicHash::Md5);
        break;
648
    case ClipType::QText:
649
        fileData = getProducerProperty(QStringLiteral("text")).toUtf8();
650 651
        fileHash = QCryptographicHash::hash(fileData, QCryptographicHash::Md5);
        break;
652
    case ClipType::Color:
653
        fileData = getProducerProperty(QStringLiteral("resource")).toUtf8();
654 655 656
        fileHash = QCryptographicHash::hash(fileData, QCryptographicHash::Md5);
        break;
    default:
657
        QFile file(clipUrl());
658 659 660 661 662
        if (file.open(QIODevice::ReadOnly)) { // write size and hash only if resource points to a file
            /*
             * 1 MB = 1 second per 450 files (or faster)
             * 10 MB = 9 seconds per 450 files (or faster)
             */
663 664
            if (file.size() > 2000000) {
                fileData = file.read(1000000);
665
                if (file.seek(file.size() - 1000000)) {
666
                    fileData.append(file.readAll());
667 668
                }
            } else {
669
                fileData = file.readAll();
670
            }
671
            file.close();
Nicolas Carion's avatar
Nicolas Carion committed
672
            ClipController::setProducerProperty(QStringLiteral("kdenlive:file_size"), QString::number(file.size()));
673
            fileHash = QCryptographicHash::hash(fileData, QCryptographicHash::Md5);
674 675 676 677
        }
        break;
    }
    if (fileHash.isEmpty()) {
678
        qDebug() << "// WARNING EMPTY CLIP HASH: ";
679
        return QString();
680 681
    }
    QString result = fileHash.toHex();
Nicolas Carion's avatar
Nicolas Carion committed
682
    ClipController::setProducerProperty(QStringLiteral("kdenlive:file_hash"), result);
683
    return result;
684
}
685 686 687

double ProjectClip::getOriginalFps() const
{
688
    return originalFps();
689
}
690 691 692

bool ProjectClip::hasProxy() const
{
693
    QString proxy = getProducerProperty(QStringLiteral("kdenlive:proxy"));
694
    return proxy.size() > 2;
695 696
}

Laurent Montel's avatar
Laurent Montel committed
697
void ProjectClip::setProperties(const QMap<QString, QString> &properties, bool refreshPanel)
698 699
{
    QMapIterator<QString, QString> i(properties);
Laurent Montel's avatar
Laurent Montel committed
700
    QMap<QString, QString> passProperties;
701
    bool refreshAnalysis = false;
702
    bool reload = false;
703
    bool refreshOnly = true;
704 705
    // Some properties also need to be passed to track producers
    QStringList timelineProperties;
Laurent Montel's avatar
Laurent Montel committed
706 707
    if (properties.contains(QStringLiteral("templatetext"))) {
        m_description = properties.value(QStringLiteral("templatetext"));
708
        if (auto ptr = m_model.lock())
709 710
            std::static_pointer_cast<ProjectItemModel>(ptr)->onItemUpdated(std::static_pointer_cast<ProjectClip>(shared_from_this()),
                                                                           AbstractProjectItem::ClipStatus);
711 712
        refreshPanel = true;
    }
Nicolas Carion's avatar
Nicolas Carion committed
713 714 715 716
    timelineProperties << QStringLiteral("force_aspect_ratio") << QStringLiteral("video_index") << QStringLiteral("audio_index")
                       << QStringLiteral("set.force_full_luma") << QStringLiteral("full_luma") << QStringLiteral("threads")
                       << QStringLiteral("force_colorspace") << QStringLiteral("force_tff") << QStringLiteral("force_progressive")
                       << QStringLiteral("force_fps");
717
    QStringList keys;
Nicolas Carion's avatar
Nicolas Carion committed
718 719
    keys << QStringLiteral("luma_duration") << QStringLiteral("luma_file") << QStringLiteral("fade") << QStringLiteral("ttl") << QStringLiteral("softness")
         << QStringLiteral("crop") << QStringLiteral("animation");
720 721
    while (i.hasNext()) {
        i.next();
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
722
        setProducerProperty(i.key(), i.value());
723
        if (m_clipType == ClipType::SlideShow && keys.contains(i.key())) {
724
            reload = true;
725
            refreshOnly = false;
726
        }
727 728 729
        if (i.key().startsWith(QLatin1String("kdenlive:clipanalysis"))) {
            refreshAnalysis = true;
        }
730 731 732
        if (timelineProperties.contains(i.key())) {
            passProperties.insert(i.key(), i.value());
        }
733
    }
734 735
    if (properties.contains(QStringLiteral("kdenlive:proxy"))) {
        QString value = properties.value(QStringLiteral("kdenlive:proxy"));
736
        // If value is "-", that means user manually disabled proxy on this clip
737
        if (value.isEmpty() || value == QLatin1String("-")) {
738
            // reset proxy
739 740
            pCore->jobManager()->discardJobs(clipId(), AbstractClipJob::PROXYJOB);
            reloadProducer();
741
        } else {
742
            // A proxy was requested, make sure to keep original url
743
            setProducerProperty(QStringLiteral("kdenlive:originalurl"), url());
744
            pCore->jobManager()->startJob<ProxyJob>({clipId()}, -1, QString());
745
        }
Nicolas Carion's avatar
Nicolas Carion committed
746 747
    } else if (properties.contains(QStringLiteral("resource")) || properties.contains(QStringLiteral("templatetext")) ||
               properties.contains(QStringLiteral("autorotate"))) {
748
        // Clip resource changed, update thumbnail
749
        if (m_clipType != ClipType::Color) {
750
            reloadProducer();