projectclip.cpp 31.3 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
/*
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
by the membership of KDE e.V.), which shall act as a proxy 
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"
#include "projectfolder.h"
25
#include "projectsubclip.h"
26
#include "bin.h"
27
#include "timecode.h"
28 29
#include "doc/kthumb.h"
#include "kdenlivesettings.h"
30
#include "project/projectcommands.h"
31
#include "mltcontroller/clipcontroller.h"
32
#include "lib/audio/audioStreamInfo.h"
33
#include "mltcontroller/clippropertiescontroller.h"
34 35 36

#include <QDomElement>
#include <QFile>
37
#include <QDir>
38 39
#include <QDebug>
#include <QCryptographicHash>
40
#include <QtConcurrent>
41
#include <KLocalizedString>
42
#include <KMessageBox>
43 44


Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
45
ProjectClip::ProjectClip(const QString &id, QIcon thumb, ClipController *controller, ProjectFolder* parent) :
46 47
    AbstractProjectItem(AbstractProjectItem::ClipItem, id, parent)
    , audioFrameCache()
Vincent Pinon's avatar
Vincent Pinon committed
48
    , m_controller(controller)
49
    , m_gpuProducer(NULL)
50
    , m_abortAudioThumb(false)
51
{
52
    m_clipStatus = StatusReady;
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
53
    m_thumbnail = thumb;
54 55
    m_name = m_controller->clipName();
    m_duration = m_controller->getStringDuration();
56
    m_date = m_controller->date;
57
    m_description = m_controller->description();
58
    m_type = m_controller->clipType();
59
    getFileHash();
60
    setParent(parent);
61
    bin()->loadSubClips(id, m_controller->getPropertiesFromPrefix("kdenlive:clipzone."));
62
    if (KdenliveSettings::audiothumbnails()) {
63
        m_audioThumbsThread = QtConcurrent::run(this, &ProjectClip::slotCreateAudioThumbs);
64
    }
65 66
}

67
ProjectClip::ProjectClip(const QDomElement& description, QIcon thumb, ProjectFolder* parent) :
68
    AbstractProjectItem(AbstractProjectItem::ClipItem, description, parent)
69
    , audioFrameCache()
Vincent Pinon's avatar
Vincent Pinon committed
70
    , m_controller(NULL)
71
    , m_gpuProducer(NULL)
72
    , m_abortAudioThumb(false)
73
    , m_type(Unknown)
74 75
{
    Q_ASSERT(description.hasAttribute("id"));
76
    m_clipStatus = StatusWaiting;
77
    m_thumbnail = thumb;
78 79 80
    if (description.hasAttribute("type")) {
        m_type = (ClipType) description.attribute("type").toInt();
    }
81
    m_temporaryUrl = QUrl::fromLocalFile(getXmlProperty(description, "resource"));
82
    QString clipName = getXmlProperty(description, "kdenlive:clipname");
83 84 85
    if (!clipName.isEmpty()) {
        m_name = clipName;
    }
86 87
    else if (m_temporaryUrl.isValid()) {
        m_name = m_temporaryUrl.fileName();
88
    }
89
    else m_name = i18n("Untitled");
90
    setParent(parent);
91 92 93 94 95
}


ProjectClip::~ProjectClip()
{
96
    // controller is deleted in bincontroller
97
    abortAudioThumbs();
98 99
}

100 101 102 103 104
QString ProjectClip::getToolTip() const
{
    return url().toLocalFile();
}

105
QString ProjectClip::getXmlProperty(const QDomElement &producer, const QString &propertyName, const QString &defaultValue)
106
{
107
    QString value = defaultValue;
108 109 110 111 112 113 114 115 116 117
    QDomNodeList props = producer.elementsByTagName("property");
    for (int i = 0; i < props.count(); ++i) {
        if (props.at(i).toElement().attribute("name") == propertyName) {
            value = props.at(i).firstChild().nodeValue();
            break;
        }
    }
    return value;
}

118
void ProjectClip::updateAudioThumbnail(QVariantList* audioLevels)
119 120
{
    ////qDebug() << "CLIPBASE RECIEDVED AUDIO DATA*********************************************";
121 122
    audioFrameCache = audioLevels;
    m_controller->audioThumbCreated = true;
123 124 125 126 127 128 129 130 131 132 133
    emit gotAudioData();
}

QList < CommentedTime > ProjectClip::commentedSnapMarkers() const
{
    if (m_controller) return m_controller->commentedSnapMarkers();
    return QList < CommentedTime > ();
}

bool ProjectClip::audioThumbCreated() const
{
134
    return (m_controller && m_controller->audioThumbCreated);
135 136
}

137 138
ClipType ProjectClip::clipType() const
{
139
    return m_type;
140 141 142 143 144 145 146 147 148 149
}

ProjectClip* ProjectClip::clip(const QString &id)
{
    if (id == m_id) {
        return this;
    }
    return NULL;
}

150 151 152 153 154
ProjectFolder* ProjectClip::folder(const QString &id)
{
    return NULL;
}

155 156 157
ProjectSubClip* ProjectClip::getSubClip(int in, int out)
{
    for (int i = 0; i < count(); ++i) {
Vincent Pinon's avatar
Vincent Pinon committed
158
        ProjectSubClip *clip = static_cast<ProjectSubClip *>(at(i))->subClip(in, out);
159 160 161 162 163 164 165
        if (clip) {
            return clip;
        }
    }
    return NULL;
}

166 167 168 169 170 171 172 173
ProjectClip* ProjectClip::clipAt(int ix)
{
    if (ix == index()) {
        return this;
    }
    return NULL;
}

174
/*bool ProjectClip::isValid() const
175
{
176 177
    return m_controller->isValid();
}*/
178 179 180

QUrl ProjectClip::url() const
{
181
    if (m_controller) return m_controller->clipUrl();
182
    return m_temporaryUrl;
183 184 185 186
}

bool ProjectClip::hasLimitedDuration() const
{
187 188 189 190
    if (m_controller) {
        return m_controller->hasLimitedDuration();
    }
    return true;
191 192
}

193
GenTime ProjectClip::duration() const
194
{
195 196
    if (m_controller) {
	return m_controller->getPlaytime();
197
    }
198
    return GenTime();
199 200
}

201
void ProjectClip::reloadProducer(bool thumbnailOnly)
202 203 204
{
    QDomDocument doc;
    QDomElement xml = toXml(doc);
205 206 207 208
    if (thumbnailOnly) {
        // set a special flag to request thumbnail only
        xml.setAttribute("thumbnailOnly", "1");
    }
209
    bin()->reloadProducer(m_id, xml);
210 211 212 213
}

void ProjectClip::setCurrent(bool current, bool notify)
{
214
    //AbstractProjectItem::setCurrent(current, notify);
215
    if (current && m_controller) {
Jean-Baptiste Mardelle's avatar
cleanup  
Jean-Baptiste Mardelle committed
216
        bin()->openProducer(m_controller);
217
        bin()->editMasterEffect(m_controller);
218 219 220 221 222
    }
}

QDomElement ProjectClip::toXml(QDomDocument& document)
{
223 224 225
    if (m_controller) {
        m_controller->getProducerXML(document);
        return document.documentElement().firstChildElement("producer");
226
    }
227
    return QDomElement();
228 229 230 231
}

void ProjectClip::setThumbnail(QImage img)
{
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
232 233
    QPixmap thumb = roundedPixmap(QPixmap::fromImage(img));
    if (hasProxy() && !thumb.isNull()) {
234
        // Overlay proxy icon
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
235
        QPainter p(&thumb);
236
        QColor c(220, 220, 10, 200);
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
237
        QRect r(0, 0, thumb.height() / 2.5, thumb.height() / 2.5);
238 239 240 241 242 243 244 245
        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
246
    m_thumbnail = QIcon(thumb);
247 248 249
    bin()->emitItemUpdated(this);
}

250 251 252 253 254
QPixmap ProjectClip::thumbnail(int width, int height)
{
    return m_thumbnail.pixmap(width, height);
}

255
void ProjectClip::setProducer(ClipController *controller, bool replaceProducer)
256
{
257
    if (!replaceProducer && m_controller) {
258 259 260
        qDebug()<<"// RECIEVED PRODUCER BUT WE ALREADY HAVE ONE\n----------";
        return;
    }
261 262
    if (m_controller) {
        // Replace clip for this controller
263
        //m_controller->updateProducer(m_id, &(controller->originalProducer()));
264
       //delete controller;
265
    }
266
    else if (controller) {
267 268 269 270
        // We did not yet have the controller, update info
        m_controller = controller;
        if (m_name.isEmpty()) m_name = m_controller->clipName();
        m_duration = m_controller->getStringDuration();
271
        m_date = m_controller->date;
272
        m_description = m_controller->description();
273
        m_temporaryUrl.clear();
274
        if (m_type == Unknown) m_type = m_controller->clipType();
275
    }
276
    m_clipStatus = StatusReady;
277 278
    bin()->emitItemUpdated(this);
    getFileHash();
279 280 281 282 283 284 285 286 287 288 289 290 291 292
    createAudioThumbs();
}

void ProjectClip::createAudioThumbs()
{
    if (KdenliveSettings::audiothumbnails() && !m_audioThumbsThread.isRunning()) {
        m_audioThumbsThread = QtConcurrent::run(this, &ProjectClip::slotCreateAudioThumbs);
    }
}

void ProjectClip::abortAudioThumbs()
{
    if (m_audioThumbsThread.isRunning()) {
        m_abortAudioThumb = true;
293
        m_audioThumbsThread.waitForFinished();
294
    }
295 296 297 298
}

Mlt::Producer *ProjectClip::producer()
{
299 300
    if (!m_controller) return NULL;
    return m_controller->masterProducer();
301 302
}

303 304 305 306 307
ClipController *ProjectClip::controller()
{
    return m_controller;
}

308
bool ProjectClip::isReady() const
309
{
310
    return m_controller != NULL && m_clipStatus == StatusReady;
311 312
}

313
/*void ProjectClip::setZone(const QPoint &zone)
314 315
{
    m_zone = zone;
316
}*/
317 318 319

QPoint ProjectClip::zone() const
{
320 321 322
    int x = getProducerIntProperty("kdenlive:zone_in");
    int y = getProducerIntProperty("kdenlive:zone_out");
    return QPoint(x, y);
323 324
}

Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
325 326 327 328 329 330 331 332
void ProjectClip::resetProducerProperty(const QString &name)
{
    if (m_controller) {
        m_controller->resetProperty(name);
    }
}

void ProjectClip::setProducerProperty(const QString &name, int data)
333
{
334 335
    if (m_controller) {
	m_controller->setProperty(name, data);
336 337 338
    }
}

Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
339
void ProjectClip::setProducerProperty(const QString &name, double data)
340
{
341 342
    if (m_controller) {
        m_controller->setProperty(name, data);
343 344 345
    }
}

Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
346
void ProjectClip::setProducerProperty(const QString &name, const QString &data)
347
{
348 349
    if (m_controller) {
        m_controller->setProperty(name, data);
350 351 352
    }
}

353 354 355 356 357 358 359 360 361 362 363 364 365 366
QMap <QString, QString> ProjectClip::currentProperties(const QMap <QString, QString> &props)
{
    QMap <QString, QString> currentProps;
    if (!m_controller) {
        return currentProps;
    }
    QMap<QString, QString>::const_iterator i = props.constBegin();
    while (i != props.constEnd()) {
        currentProps.insert(i.key(), m_controller->property(i.key()));
        ++i;
    }
    return currentProps;
}

367 368 369 370 371 372 373 374
QColor ProjectClip::getProducerColorProperty(const QString &key) const
{
    if (m_controller) {
        return m_controller->color_property(key);
    }
    return QColor();
}

375 376 377 378 379 380 381 382
int ProjectClip::getProducerIntProperty(const QString &key) const
{
    int value = 0;
    if (m_controller) {
        value = m_controller->int_property(key);
    }
    return value;
}
383

384
QString ProjectClip::getProducerProperty(const QString &key) const
385 386
{
    QString value;
387 388
    if (m_controller) {
	value = m_controller->property(key);
389 390 391 392 393 394
    }
    return value;
}

const QString ProjectClip::hash()
{
395 396
    if (m_controller) {
        QString clipHash = m_controller->property("kdenlive:file_hash");
397 398
        if (!clipHash.isEmpty()) {
            return clipHash;
399
        }
400
    }
401
    return getFileHash();
402 403
}

404
const QString ProjectClip::getFileHash() const
405
{
406 407 408 409 410 411 412 413
    QByteArray fileData;
    QByteArray fileHash;
    switch (m_type) {
      case SlideShow:
          fileData = m_controller ? m_controller->clipUrl().toLocalFile().toUtf8() : m_temporaryUrl.toLocalFile().toUtf8();
          fileHash = QCryptographicHash::hash(fileData, QCryptographicHash::Md5);
          break;
      case Text:
414 415 416
          fileData = m_controller ? m_controller->property("xmldata").toUtf8() : name().toUtf8();
          fileHash = QCryptographicHash::hash(fileData, QCryptographicHash::Md5);
          break;
417
      case Color:
418 419
          fileData = m_controller ? m_controller->property("resource").toUtf8() : name().toUtf8();
          fileHash = QCryptographicHash::hash(fileData, QCryptographicHash::Md5);
420 421 422 423 424
          break;
      default:
          QFile file(m_controller ? m_controller->clipUrl().toLocalFile() : m_temporaryUrl.toLocalFile());
          if (file.open(QIODevice::ReadOnly)) { // write size and hash only if resource points to a file
              /*
425 426 427
               * 1 MB = 1 second per 450 files (or faster)
               * 10 MB = 9 seconds per 450 files (or faster)
               */
428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443
            if (file.size() > 2000000) {
                fileData = file.read(1000000);
                if (file.seek(file.size() - 1000000))
                    fileData.append(file.readAll());
            } else
                fileData = file.readAll();
            file.close();
            if (m_controller) m_controller->setProperty("kdenlive:file_size", QString::number(file.size()));
            fileHash = QCryptographicHash::hash(fileData, QCryptographicHash::Md5);
          }
          break;
    }
    if (fileHash.isEmpty()) return QString();
    QString result = fileHash.toHex();
    if (m_controller) {
	m_controller->setProperty("kdenlive:file_hash", result);
444
    }
445
    return result;
446
}
447 448 449 450 451 452

double ProjectClip::getOriginalFps() const
{
    if (!m_controller) return 0;
    return m_controller->originalFps();
}
453 454 455

bool ProjectClip::hasProxy() const
{
456
    QString proxy = getProducerProperty("kdenlive:proxy");
457 458 459 460
    if (proxy.isEmpty() || proxy == "-") return false;
    return true;
}

461
void ProjectClip::setProperties(QMap <QString, QString> properties, bool refreshPanel)
462 463
{
    QMapIterator<QString, QString> i(properties);
464
    QMap <QString, QString> passProperties;
465
    bool refreshProducer = false;
466
    bool refreshAnalysis = false;
467 468 469
    bool reload = false;
    // Some properties also need to be passed to track producers
    QStringList timelineProperties;
470
    timelineProperties << "force_aspect_ratio" << "video_index" << "audio_index" << "set.force_full_luma"<< "full_luma" <<"threads" <<"force_colorspace"<<"force_tff"<<"force_progressive"<<"force_fps";
471 472 473 474
    QStringList keys;
    keys << "luma_duration" << "luma_file" << "fade" << "ttl" << "softness" << "crop" << "animation";
    while (i.hasNext()) {
        i.next();
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
475
        setProducerProperty(i.key(), i.value());
476 477 478
        if (m_type == SlideShow && keys.contains(i.key())) {
            reload = true;
        }
479
        if (i.key().startsWith("kdenlive:clipanalysis")) refreshAnalysis = true;
480 481 482
        if (timelineProperties.contains(i.key())) {
            passProperties.insert(i.key(), i.value());
        }
483
    }
484 485
    if (properties.contains("kdenlive:proxy")) {
        QString value = properties.value("kdenlive:proxy");
486 487 488 489 490 491
        // If value is "-", that means user manually disabled proxy on this clip
        if (value.isEmpty() || value == "-") {
            // reset proxy
            if (bin()->hasPendingJob(m_id, AbstractClipJob::PROXYJOB)) {
                bin()->discardJobs(m_id, AbstractClipJob::PROXYJOB);
            }
492 493 494
            else {
                reloadProducer();
            }
495 496
        }
        else {
497
            // A proxy was requested, make sure to keep original url
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
498
            setProducerProperty("kdenlive:originalurl", url().toLocalFile());
499
            bin()->startJob(m_id, AbstractClipJob::PROXYJOB);
500 501
        }
    }
502 503
    else if (properties.contains("resource")) {
        // Clip resource changed, update thumbnail
504
        if (m_type != Color) {
505
            reloadProducer();
506
        }
507
        reload = true;
508
    }
509
    if (properties.contains("xmldata") || !passProperties.isEmpty()) {
510 511
        refreshProducer = true;
    }
512
    if (refreshAnalysis) emit refreshAnalysisPanel();
513 514 515 516 517
    if (properties.contains("length")) {
        m_duration = m_controller->getStringDuration();
        bin()->emitItemUpdated(this);
    }

518 519 520 521
    if (properties.contains("kdenlive:clipname")) {
        m_name = properties.value("kdenlive:clipname");
        bin()->emitItemUpdated(this);
    }
522 523 524 525
    if (refreshPanel) {
        // Some of the clip properties have changed through a command, update properties panel
        emit refreshPropertiesPanel();
    }
526
    if (refreshProducer || reload) {
527
        // producer has changed, refresh monitor and thumbnail
528
        if (reload) reloadProducer(true);
529
        bin()->refreshClip(m_id);
530
    }
531 532 533
    if (!passProperties.isEmpty()) {
        bin()->updateTimelineProducers(m_id, passProperties);
    }
534 535 536 537 538 539
}

void ProjectClip::setJobStatus(AbstractClipJob::JOBTYPE jobType, ClipJobStatus status, int progress, const QString &statusMessage)
{
    m_jobType = jobType;
    if (progress > 0) {
540
        if (m_jobProgress == progress) return;
541 542 543 544 545 546
	m_jobProgress = progress;
    }
    else {
	m_jobProgress = status;
	if ((status == JobAborted || status == JobCrashed  || status == JobDone) || !statusMessage.isEmpty()) {
	    m_jobMessage = statusMessage;
547
            bin()->emitMessage(statusMessage, OperationCompletedMessage);
548 549 550 551
	}
    }
    bin()->emitItemUpdated(this);
}
552

553 554 555

ClipPropertiesController *ProjectClip::buildProperties(QWidget *parent)
{
556
    ClipPropertiesController *panel = new ClipPropertiesController(bin()->projectTimecode(), m_controller, parent);
557
    connect(this, SIGNAL(refreshPropertiesPanel()), panel, SLOT(slotReloadProperties()));
558
    connect(this, SIGNAL(refreshAnalysisPanel()), panel, SLOT(slotFillAnalysisData()));
559 560 561
    return panel;
}

562 563
void ProjectClip::updateParentInfo(const QString &folderid, const QString &foldername)
{
564
    m_controller->setProperty("kdenlive:folderid", folderid);
565 566
}

567 568 569 570 571 572
bool ProjectClip::matches(QString condition)
{
    //TODO
    return true;
}

573
const QString ProjectClip::codec(bool audioCodec) const
574
{
575 576
    if (!m_controller) return QString();
    return m_controller->codec(audioCodec);
577
}
578

579
bool ProjectClip::rename(const QString &name, int column)
580 581 582
{
    QMap <QString, QString> newProperites;
    QMap <QString, QString> oldProperites;
583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605
    bool edited = false;
    switch (column) {
      case 0:
        if (m_name == name) return false;
        // Rename clip
        oldProperites.insert("kdenlive:clipname", m_name);
        newProperites.insert("kdenlive:clipname", name);
        m_name = name;
        edited = true;
        break;
      case 2:
        if (m_description == name) return false;
        // Rename clip
        oldProperites.insert("kdenlive:description", m_description);
        newProperites.insert("kdenlive:description", name);
        m_description = name;
        edited = true;
        break;
    }
    if (edited) {
        bin()->slotEditClipCommand(m_id, oldProperites, newProperites);
    }
    return edited;
606 607
}

608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635
void ProjectClip::addClipMarker(QList <CommentedTime> newMarkers, QUndoCommand *groupCommand)
{
    if (!m_controller) return;
    QList <CommentedTime> oldMarkers;
    for (int i = 0; i < newMarkers.count(); ++i) {
        CommentedTime oldMarker = m_controller->markerAt(newMarkers.at(i).time());
        if (oldMarker == CommentedTime()) {
            oldMarker = newMarkers.at(i);
            oldMarker.setMarkerType(-1);
        }
        oldMarkers << oldMarker;
    }
    (void) new AddMarkerCommand(this, oldMarkers, newMarkers, groupCommand);
}

bool ProjectClip::deleteClipMarkers(QUndoCommand *command)
{
    QList <CommentedTime> markers = commentedSnapMarkers();
    if (markers.isEmpty()) {
        return false;
    }
    QList <CommentedTime> newMarkers;
    for (int i = 0; i < markers.size(); ++i) {
        CommentedTime marker = markers.at(i);
        marker.setMarkerType(-1);
        newMarkers << marker;
    }
    new AddMarkerCommand(this, markers, newMarkers, command);
636
    return true;
637 638 639 640 641 642 643 644 645 646 647 648 649 650
}

void ProjectClip::addMarkers(QList <CommentedTime> &markers)
{
    if (!m_controller) return;
    for (int i = 0; i < markers.count(); ++i) {
      if (markers.at(i).markerType() < 0) m_controller->deleteSnapMarker(markers.at(i).time());
      else m_controller->addSnapMarker(markers.at(i));
    }
    // refresh markers in clip monitor
    bin()->refreshClipMarkers(m_id);
    // refresh markers in timeline clips
    emit refreshClipDisplay();
}
651

Vincent Pinon's avatar
Vincent Pinon committed
652
void ProjectClip::addEffect(const ProfileInfo &pInfo, QDomElement &effect)
653
{
654
    m_controller->addEffect(pInfo, effect);
655
    bin()->editMasterEffect(m_controller);
656
    bin()->emitItemUpdated(this);
657 658
}

Vincent Pinon's avatar
Vincent Pinon committed
659
void ProjectClip::removeEffect(const ProfileInfo &pInfo, int ix)
660 661 662
{
    m_controller->removeEffect(pInfo, ix);
    bin()->editMasterEffect(m_controller);
663 664 665 666 667 668 669
    bin()->emitItemUpdated(this);
}

QVariant ProjectClip::data(DataType type) const
{
    switch (type) {
      case AbstractProjectItem::IconOverlay:
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
670
            return m_controller != NULL ? (m_controller->hasEffects() ? QVariant("kdenlive-track_has_effect") : QVariant()) : QVariant();
671 672 673 674 675
            break;
        default:
	    break;
    }
    return AbstractProjectItem::data(type);
676
}
677 678 679

void ProjectClip::slotExtractImage(QList <int> frames)
{
680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697
    Mlt::Producer *prod = producer();
    if (prod == NULL) return;
    // Check if we are using GPU accel, then we need to use alternate producer
    if (KdenliveSettings::gpu_accel()) {
	if (m_gpuProducer == NULL) {
            QString service = prod->get("mlt_service");
            m_gpuProducer = new Mlt::Producer(*prod->profile(), service.toUtf8().constData(), prod->get("resource"));
            Mlt::Filter scaler(*prod->profile(), "swscale");
	    Mlt::Filter converter(*prod->profile(), "avcolor_space");
	    m_gpuProducer->attach(scaler);
	    m_gpuProducer->attach(converter);
        }
	prod = m_gpuProducer;
    }
    int fullWidth = (int)((double) 150 * prod->profile()->dar() + 0.5);
    QDir thumbFolder(bin()->projectFolder().path() + "/thumbs/");
    for (int i = 0; i < frames.count(); i++) {
        int pos = frames.at(i);
698 699
        if (thumbFolder.exists(hash() + '#' + QString::number(pos) + ".png")) {
            emit thumbReady(pos, QImage(thumbFolder.absoluteFilePath(hash() + '#' + QString::number(pos) + ".png")));
700 701 702 703 704 705 706
            continue;
        }
	int max = prod->get_out();
	if (pos >= max) pos = max - 1;
	prod->seek(pos);
	Mlt::Frame *frame = prod->get_frame();
	if (frame && frame->is_valid()) {
707
            QImage img = KThumb::getFrame(frame, fullWidth, 150);
708 709 710 711
            emit thumbReady(frames.at(i), img);
        }
        delete frame;
    }
712
}
713

714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733
void ProjectClip::slotExtractSubImage(QList <int> frames)
{
    Mlt::Producer *prod = producer();
    if (prod == NULL) return;
    // Check if we are using GPU accel, then we need to use alternate producer
    if (KdenliveSettings::gpu_accel()) {
        if (m_gpuProducer == NULL) {
            QString service = prod->get("mlt_service");
            m_gpuProducer = new Mlt::Producer(*prod->profile(), service.toUtf8().constData(), prod->get("resource"));
            Mlt::Filter scaler(*prod->profile(), "swscale");
            Mlt::Filter converter(*prod->profile(), "avcolor_space");
            m_gpuProducer->attach(scaler);
            m_gpuProducer->attach(converter);
        }
        prod = m_gpuProducer;
    }
    int fullWidth = (int)((double) 150 * prod->profile()->dar() + 0.5);
    QDir thumbFolder(bin()->projectFolder().path() + "/thumbs/");
    for (int i = 0; i < frames.count(); i++) {
        int pos = frames.at(i);
734 735
        QString path = thumbFolder.absoluteFilePath(hash() + "#" + QString::number(pos) + ".png");
        QImage img(path);
736 737
        if (!img.isNull()) {
            for (int i = 0; i < count(); ++i) {
Vincent Pinon's avatar
Vincent Pinon committed
738
                ProjectSubClip *clip = static_cast<ProjectSubClip *>(at(i));
739 740 741 742 743 744 745 746
                if (clip && clip->zone().x() == pos) {
                    clip->setThumbnail(img);
                }
            }
            continue;
        }
        int max = prod->get_out();
        if (pos >= max) pos = max - 1;
747
        if (pos < 0) pos = 0;
748 749 750
        prod->seek(pos);
        Mlt::Frame *frame = prod->get_frame();
        if (frame && frame->is_valid()) {
751
            QImage img = KThumb::getFrame(frame, fullWidth, 150);
752
            if (!img.isNull()) {
753
                img.save(path);
754
                for (int i = 0; i < count(); ++i) {
Vincent Pinon's avatar
Vincent Pinon committed
755
                    ProjectSubClip *clip = static_cast<ProjectSubClip *>(at(i));
756 757 758 759 760 761 762 763 764 765
                    if (clip && clip->zone().x() == pos) {
                        clip->setThumbnail(img);
                    }
                }
            }
        }
        delete frame;
    }
}

766 767 768 769 770 771
int ProjectClip::audioChannels() const
{
    if (!m_controller || !m_controller->audioInfo()) return 0;
    return m_controller->audioInfo()->channels();
}

772 773 774 775 776 777 778
void ProjectClip::slotCreateAudioThumbs()
{
    Mlt::Producer *prod = producer();
    AudioStreamInfo *audioInfo = m_controller->audioInfo();
    if (audioInfo == NULL) return;
    QString clipHash = hash();
    if (clipHash.isEmpty()) return;
779
    QString audioPath = bin()->projectFolder().path() + "/thumbs/" + clipHash + "_audio.png";
780 781 782 783 784 785
    double lengthInFrames = prod->get_playtime();
    int frequency = audioInfo->samplingRate();
    if (frequency <= 0) frequency = 48000;
    int channels = audioInfo->channels();
    if (channels <= 0) channels = 2;
    double frame = 0.0;
786 787 788 789 790 791 792 793 794 795 796
    QVariantList* audioLevels = new QVariantList;
    QImage image(audioPath);
    if (!image.isNull()) {
        // convert cached image
        int n = image.width() * image.height();
        for (int i = 0; i < n; i++) {
            QRgb p = image.pixel(i / 2, i % channels);
            *audioLevels << qRed(p);
            *audioLevels << qGreen(p);
            *audioLevels << qBlue(p);
            *audioLevels << qAlpha(p);
797 798
        }
    }
799 800
    if (audioLevels->size() > 0) {
        updateAudioThumbnail(audioLevels);
801 802 803
        return;
    }
    QString service = prod->get("mlt_service");
804 805 806 807
    if (service == "avformat-novalidate")
        service = "avformat";
    else if (service.startsWith("xml"))
        service = "xml-nogl";
808 809 810 811 812
    Mlt::Producer *audioProducer = new Mlt::Producer(*prod->profile(), service.toUtf8().constData(), prod->get("resource"));
    if (!audioProducer->is_valid()) {
        delete audioProducer;
        return;
    }
813 814 815 816 817 818
    Mlt::Filter chans(*prod->profile(), "audiochannels");
    Mlt::Filter converter(*prod->profile(), "audioconvert");
    Mlt::Filter levels(*prod->profile(), "audiolevel");
    audioProducer->attach(chans);
    audioProducer->attach(converter);
    audioProducer->attach(levels);
819

820
    audioProducer->set("video_index", "-1");
821 822 823 824
    int last_val = 0;
    setJobStatus(AbstractClipJob::THUMBJOB, JobWaiting, 0, i18n("Creating audio thumbnails"));
    double framesPerSecond = audioProducer->get_fps();
    mlt_audio_format audioFormat = mlt_audio_s16;
825 826 827 828 829
    QStringList keys;
    for (int i = 0; i < channels; i++) {
        keys << "meta.media.audio_level." + QString::number(i);
    }
    for (int z = (int) frame; z < (int)(frame + lengthInFrames) && !m_abortAudioThumb; ++z) {
830 831 832 833 834 835
        int val = (int)((z - frame) / (frame + lengthInFrames) * 100.0);
        if (last_val != val && val > 1) {
            setJobStatus(AbstractClipJob::THUMBJOB, JobWorking, val);
            last_val = val;
        }
        Mlt::Frame *mlt_frame = audioProducer->get_frame();
836 837 838 839 840 841
        if (mlt_frame && mlt_frame->is_valid() && !mlt_frame->get_int("test_audio")) {
            int samples = mlt_sample_calculator(framesPerSecond, frequency, z);
            mlt_frame->get_audio(audioFormat, frequency, channels, samples);
            for (int channel = 0; channel < channels; ++channel) {
                double level = 256 * qMin(mlt_frame->get_double(keys.at(channel).toUtf8().constData()) * 0.9, 1.0);
                *audioLevels << level;
842
            }
843 844 845
        } else if (!audioLevels->isEmpty()) {
            for (int channel = 0; channel < channels; channel++)
                *audioLevels << audioLevels->last();
846 847
        }
        delete mlt_frame;
848
        if (m_abortAudioThumb) break;
849
    }
850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871

    if (!m_abortAudioThumb && audioLevels->size() > 0) {
        // Put into an image for caching.
        int count = audioLevels->size();
        QImage image((count + 3) / 4, channels, QImage::Format_ARGB32);
        int n = image.width() * image.height();
        for (int i = 0; i < n; i ++) {
            QRgb p; 
            if ((4*i + 3) < count) {
                p = qRgba(audioLevels->at(4*i).toInt(), audioLevels->at(4*i+1).toInt(), audioLevels->at(4*i+2).toInt(), audioLevels->at(4*i+3).toInt());
            } else {
                int last = audioLevels->last().toInt();
                int r = (4*i+0) < count? audioLevels->at(4*i+0).toInt() : last;
                int g = (4*i+1) < count? audioLevels->at(4*i+1).toInt() : last;
                int b = (4*i+2) < count? audioLevels->at(4*i+2).toInt() : last;
                int a = last;
                p = qRgba(r, g, b, a);
            }
            image.setPixel(i / 2, i % channels, p);
        }
        image.save(audioPath);
    }
872 873
    delete audioProducer;
    setJobStatus(AbstractClipJob::THUMBJOB, JobDone, 0, i18n("Audio thumbnails done"));
874 875
    if (!m_abortAudioThumb) {
        updateAudioThumbnail(audioLevels);
876
    }
877
    m_abortAudioThumb = false;
878
}
879 880 881 882 883 884 885

bool ProjectClip::isTransparent() const
{
    if (m_type == Text) return true;
    if (m_type == Image && m_controller->int_property("kdenlive:transparency") == 1) return true;
    return false;
}
886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952

QStringList ProjectClip::updatedAnalysisData(const QString &name, const QString &data, int offset)
{
    if (data.isEmpty()) {
        // Remove data
        return QStringList() << QString("kdenlive:clipanalysis." + name) << QString();
        //m_controller->resetProperty("kdenlive:clipanalysis." + name);
    }
    else {
        QString current = m_controller->property("kdenlive:clipanalysis." + name);
        if (!current.isEmpty()) {
            if (KMessageBox::questionYesNo(QApplication::activeWindow(), i18n("Clip already contains analysis data %1", name), QString(), KGuiItem(i18n("Merge")), KGuiItem(i18n("Add"))) == KMessageBox::Yes) {
                // Merge data
                Mlt::Profile *profile = m_controller->profile();
                Mlt::Geometry geometry(current.toUtf8().data(), duration().frames(profile->fps()), profile->width(), profile->height());
                Mlt::Geometry newGeometry(data.toUtf8().data(), duration().frames(profile->fps()), profile->width(), profile->height());
                Mlt::GeometryItem item;
                int pos = 0;
                while (!newGeometry.next_key(&item, pos)) {
                    pos = item.frame();
                    item.frame(pos + offset);
                    pos++;
                    geometry.insert(item);
                }
                return QStringList() << QString("kdenlive:clipanalysis." + name) << geometry.serialise();
                //m_controller->setProperty("kdenlive:clipanalysis." + name, geometry.serialise());
            }
            else {
                // Add data with another name
                int i = 1;
                QString data = m_controller->property("kdenlive:clipanalysis." + name + ' ' + QString::number(i));
                while (!data.isEmpty()) {
                    ++i;
                    data = m_controller->property("kdenlive:clipanalysis." + name + ' ' + QString::number(i));
                }
                return QStringList() << QString("kdenlive:clipanalysis." + name + ' ' + QString::number(i)) << geometryWithOffset(data, offset);
                //m_controller->setProperty("kdenlive:clipanalysis." + name + ' ' + QString::number(i), geometryWithOffset(data, offset));
            }
        }
        else {
            return QStringList() << QString("kdenlive:clipanalysis." + name) << geometryWithOffset(data, offset);
            //m_controller->setProperty("kdenlive:clipanalysis." + name, geometryWithOffset(data, offset));
        }
    }
}

QMap <QString, QString> ProjectClip::analysisData(bool withPrefix)
{
    return m_controller->getPropertiesFromPrefix("kdenlive:clipanalysis.", withPrefix);
}

const QString ProjectClip::geometryWithOffset(const QString &data, int offset)
{
    if (offset == 0) return data;
    Mlt::Profile *profile = m_controller->profile();
    Mlt::Geometry geometry(data.toUtf8().data(), duration().frames(profile->fps()), profile->width(), profile->height());
    Mlt::Geometry newgeometry(NULL, duration().frames(profile->fps()), profile->width(), profile->height());
    Mlt::GeometryItem item;
    int pos = 0;
    while (!geometry.next_key(&item, pos)) {
        pos = item.frame();
        item.frame(pos + offset);
        pos++;
        newgeometry.insert(item);
    }
    return newgeometry.serialise();
}