cutclipjob.cpp 18.6 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
/***************************************************************************
 *                                                                         *
 *   Copyright (C) 2011 by Jean-Baptiste Mardelle (jb@kdenlive.org)        *
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 *   This program is distributed in the hope that it will be useful,       *
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
 *   GNU General Public License for more details.                          *
 *                                                                         *
 *   You should have received a copy of the GNU General Public License     *
 *   along with this program; if not, write to the                         *
 *   Free Software Foundation, Inc.,                                       *
 *   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA          *
 ***************************************************************************/

#include "cutclipjob.h"
#include "kdenlivesettings.h"
23
#include "bin/projectclip.h"
24
#include "doc/kdenlivedoc.h"
25

26 27 28
#include "ui_cutjobdialog_ui.h"

#include <KMessageBox>
29 30
#include <klocalizedstring.h>

31
#include <QApplication>
Laurent Montel's avatar
Laurent Montel committed
32
#include "kdenlive_debug.h"
33 34
#include <QDialog>
#include <QPointer>
35 36
#include <QJsonObject>
#include <QJsonArray>
Laurent Montel's avatar
Laurent Montel committed
37
#include <QJsonDocument>
38

39
CutClipJob::CutClipJob(ClipType cType, const QString &id, const QStringList &parameters) : AbstractClipJob(CUTJOB, cType, id)
40
{
41
    m_jobStatus = JobWaiting;
42 43 44 45
    jobType = (AbstractClipJob::JOBTYPE) parameters.at(0).toInt();
    m_dest = parameters.at(1);
    m_src = parameters.at(2);
    switch (jobType) {
Laurent Montel's avatar
Laurent Montel committed
46 47 48 49 50 51 52 53 54 55
    case AbstractClipJob::TRANSCODEJOB:
        description = i18n("Transcode clip");
        break;
    case AbstractClipJob::CUTJOB:
        description = i18n("Cut clip");
        break;
    case AbstractClipJob::ANALYSECLIPJOB:
    default:
        description = i18n("Analyse clip");
        break;
56
    }
57
    replaceClip = false;
58 59 60 61 62
    if (jobType != AbstractClipJob::ANALYSECLIPJOB) {
        m_start = parameters.at(3);
        m_end = parameters.at(4);
        m_jobDuration = parameters.at(5).toInt();
        m_addClipToProject = parameters.at(6).toInt();
Laurent Montel's avatar
Laurent Montel committed
63 64 65 66
        if (parameters.count() == 8) {
            m_cutExtraParams = parameters.at(7).simplified();
        }
    } else {
67 68
        m_jobDuration = parameters.at(3).toInt();
    }
69 70
}

71
void CutClipJob::startJob()
72 73
{
    // Special case: playlist clips (.mlt or .kdenlive project files)
74
    if (clipType == AV || clipType == Audio || clipType == Video) {
75 76 77 78 79 80
        if (KdenliveSettings::ffmpegpath().isEmpty()) {
            //FFmpeg not detected, cannot process the Job
            m_errorMessage = i18n("Cannot process job. FFmpeg not found, please set path in Kdenlive's settings Environment");
            setStatus(JobCrashed);
            return;
        }
81
        QStringList parameters;
82 83 84
        QString exec;
        if (jobType == AbstractClipJob::ANALYSECLIPJOB) {
            // TODO: don't hardcode params
Laurent Montel's avatar
Laurent Montel committed
85
            parameters << QStringLiteral("-select_streams") << QStringLiteral("v") << QStringLiteral("-show_frames") << QStringLiteral("-hide_banner") << QStringLiteral("-of") << QStringLiteral("json=c=1") << m_src;
86 87
            exec = KdenliveSettings::ffprobepath();
        } else {
88 89 90
            if (!m_cutExtraParams.contains(QLatin1String("-i "))) {
                parameters << QStringLiteral("-i") << m_src;
            }
Laurent Montel's avatar
Laurent Montel committed
91 92 93
            if (!m_start.isEmpty()) {
                parameters << QStringLiteral("-ss") << m_start << QStringLiteral("-t") << m_end;
            }
94
            if (!m_cutExtraParams.isEmpty()) {
Laurent Montel's avatar
Laurent Montel committed
95
                foreach (const QString &s, m_cutExtraParams.split(QLatin1Char(' '))) {
96
                    parameters << s;
97 98 99
                    if (s == QLatin1String("-i")) {
                        parameters << m_src;
                    }
Laurent Montel's avatar
Laurent Montel committed
100
                }
101
            }
102

103
            // Make sure we don't block when proxy file already exists
104
            parameters << QStringLiteral("-y");
105 106 107
            parameters << m_dest;
            exec = KdenliveSettings::ffmpegpath();
        }
108
        m_jobProcess = new QProcess;
109 110 111 112
        if (jobType != AbstractClipJob::ANALYSECLIPJOB) {
            m_jobProcess->setProcessChannelMode(QProcess::MergedChannels);
        }
        m_jobProcess->start(exec, parameters);
113
        m_jobProcess->waitForStarted();
114
        while (m_jobProcess->state() != QProcess::NotRunning) {
115 116
            if (jobType == AbstractClipJob::ANALYSECLIPJOB) {
                analyseLogInfo();
Laurent Montel's avatar
Laurent Montel committed
117
            } else {
118 119
                processLogInfo();
            }
120
            if (m_jobStatus == JobAborted) {
121 122
                m_jobProcess->close();
                m_jobProcess->waitForFinished();
Laurent Montel's avatar
Laurent Montel committed
123 124 125
                if (!m_dest.isEmpty()) {
                    QFile::remove(m_dest);
                }
126 127 128
            }
            m_jobProcess->waitForFinished(400);
        }
129

130
        if (m_jobStatus != JobAborted) {
131 132
            int result = m_jobProcess->exitStatus();
            if (result == QProcess::NormalExit) {
133 134 135
                if (jobType == AbstractClipJob::ANALYSECLIPJOB) {
                    analyseLogInfo();
                    processAnalyseLog();
136
                    setStatus(JobDone);
137 138 139 140 141 142 143 144 145
                } else {
                    if (QFileInfo(m_dest).size() == 0) {
                        // File was not created
                        processLogInfo();
                        m_errorMessage.append(i18n("Failed to create file."));
                        setStatus(JobCrashed);
                    } else {
                        setStatus(JobDone);
                    }
146
                }
147
            } else if (result == QProcess::CrashExit) {
148
                // Proxy process crashed
Laurent Montel's avatar
Laurent Montel committed
149 150 151
                if (!m_dest.isEmpty()) {
                    QFile::remove(m_dest);
                }
152
                setStatus(JobCrashed);
153 154
            }
        }
155
        delete m_jobProcess;
156
        return;
157 158
    } else {
        m_errorMessage = i18n("Cannot process this clip type.");
159
    }
160
    setStatus(JobCrashed);
161
    return;
162 163
}

164
void CutClipJob::processLogInfo()
165
{
Laurent Montel's avatar
Laurent Montel committed
166 167 168
    if (!m_jobProcess || m_jobDuration == 0 || m_jobStatus == JobAborted) {
        return;
    }
Laurent Montel's avatar
Laurent Montel committed
169
    QString log = QString::fromUtf8(m_jobProcess->readAll());
Laurent Montel's avatar
Laurent Montel committed
170 171 172
    if (!log.isEmpty()) {
        m_logDetails.append(log + QLatin1Char('\n'));
    }
173 174
    int progress;
    // Parse FFmpeg output
175
    //TODO: parsing progress info works with FFmpeg but not with libav
Laurent Montel's avatar
Laurent Montel committed
176
    if (log.contains(QLatin1String("frame="))) {
177
        progress = log.section(QStringLiteral("frame="), 1, 1).simplified().section(QLatin1Char(' '), 0, 0).toInt();
Laurent Montel's avatar
Laurent Montel committed
178 179
        emit jobProgress(m_clipId, (int)(100.0 * progress / m_jobDuration), jobType);
    } else if (log.contains(QLatin1String("time="))) {
180
        QString time = log.section(QStringLiteral("time="), 1, 1).simplified().section(QLatin1Char(' '), 0, 0);
Laurent Montel's avatar
Laurent Montel committed
181 182
        if (time.contains(QLatin1Char(':'))) {
            QStringList numbers = time.split(QLatin1Char(':'));
183
            progress = numbers.at(0).toInt() * 3600 + numbers.at(1).toInt() * 60 + numbers.at(2).toDouble();
Laurent Montel's avatar
Laurent Montel committed
184 185
        } else {
            progress = (int) time.toDouble();
186
        }
Laurent Montel's avatar
Laurent Montel committed
187
        emit jobProgress(m_clipId, (int)(100.0 * progress / m_jobDuration), jobType);
188 189 190
    }
}

191 192
void CutClipJob::analyseLogInfo()
{
Laurent Montel's avatar
Laurent Montel committed
193 194 195
    if (!m_jobProcess || m_jobStatus == JobAborted) {
        return;
    }
196 197
    QString log = QString::fromUtf8(m_jobProcess->readAll());
    m_logDetails.append(log);
198
    int pos = log.indexOf(QStringLiteral("coded_picture_number"), 0);
199 200
    if (pos > -1) {
        log.remove(0, pos);
Laurent Montel's avatar
Laurent Montel committed
201
        int frame = log.section(QLatin1Char(','), 0, 0).section(QLatin1Char(':'), 1).toInt();
Laurent Montel's avatar
Laurent Montel committed
202 203 204
        if (frame > 0 && m_jobDuration > 0) {
            emit jobProgress(m_clipId, (int)(100.0 * frame / m_jobDuration), jobType);
        }
205 206 207
    }
}

208 209 210 211 212 213 214 215 216 217 218
CutClipJob::~CutClipJob()
{
}

const QString CutClipJob::destination() const
{
    return m_dest;
}

stringMap CutClipJob::cancelProperties()
{
Laurent Montel's avatar
Laurent Montel committed
219
    QMap<QString, QString> props;
220 221 222
    return props;
}

223
const QString CutClipJob::statusMessage()
224 225
{
    QString statusInfo;
226
    switch (m_jobStatus) {
Laurent Montel's avatar
Laurent Montel committed
227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246
    case JobWorking:
        if (jobType == AbstractClipJob::TRANSCODEJOB) {
            statusInfo = i18n("Transcoding clip");
        } else if (jobType == AbstractClipJob::CUTJOB) {
            statusInfo = i18n("Extracting clip cut");
        } else {
            statusInfo = i18n("Analysing clip");
        }
        break;
    case JobWaiting:
        if (jobType == AbstractClipJob::TRANSCODEJOB) {
            statusInfo = i18n("Waiting - transcode clip");
        } else if (jobType == AbstractClipJob::CUTJOB) {
            statusInfo = i18n("Waiting - cut clip");
        } else {
            statusInfo = i18n("Waiting - analyse clip");
        }
        break;
    default:
        break;
247 248 249
    }
    return statusInfo;
}
250

251 252 253 254 255
bool CutClipJob::isExclusive()
{
    return false;
}

Laurent Montel's avatar
Laurent Montel committed
256
// static
Laurent Montel's avatar
Laurent Montel committed
257
QList<ProjectClip *> CutClipJob::filterClips(const QList<ProjectClip *> &clips, const QStringList &params)
258 259 260 261 262 263
{
    QString condition;
    if (params.count() > 3) {
        // there is a condition for this job, for example operate only on vcodec=mpeg1video
        condition = params.at(3);
    }
Laurent Montel's avatar
Laurent Montel committed
264
    QList<ProjectClip *> result;
265 266 267 268 269 270 271 272 273 274 275 276 277 278 279
    for (int i = 0; i < clips.count(); i++) {
        ProjectClip *clip = clips.at(i);
        ClipType type = clip->clipType();
        if (type != AV && type != Audio && type != Video) {
            // Clip will not be processed by this job
            continue;
        }
        if (!condition.isEmpty() && !clip->matches(condition)) {
            // Clip does not match requested condition, do not process
            continue;
        }
        result << clip;
    }
    return result;
}
Laurent Montel's avatar
Laurent Montel committed
280

Laurent Montel's avatar
Laurent Montel committed
281
// static
282

Laurent Montel's avatar
Laurent Montel committed
283
QHash<ProjectClip *, AbstractClipJob *> CutClipJob::prepareCutClipJob(double fps, double originalFps, ProjectClip *clip)
284
{
Laurent Montel's avatar
Laurent Montel committed
285
    QHash<ProjectClip *, AbstractClipJob *> jobs;
Laurent Montel's avatar
Laurent Montel committed
286 287 288
    if (!clip) {
        return jobs;
    }
289
    QString source = clip->url();
290
    QPoint zone = clip->zone();
291
    QString ext = source.section(QLatin1Char('.'), -1);
Laurent Montel's avatar
Laurent Montel committed
292
    QString dest = source.section(QLatin1Char('.'), 0, -2) + QLatin1Char('_') + QString::number(zone.x()) + QLatin1Char('.') + ext;
293

294
    if (originalFps < 1e-6) {
Laurent Montel's avatar
Laurent Montel committed
295 296
        originalFps = fps;
    }
297 298 299
    // if clip and project have different frame rate, adjust in and out
    int in = zone.x();
    int out = zone.y();
Vincent Pinon's avatar
Vincent Pinon committed
300
    int max = clip->duration().frames(originalFps);
301 302
    int duration = out - in - 1;
    // Locale conversion does not seem necessary here...
Vincent Pinon's avatar
Vincent Pinon committed
303 304
    QString timeIn = QString::number(GenTime(in, originalFps).ms() / 1000);
    QString timeOut = QString::number(GenTime(duration, originalFps).ms() / 1000);
305

306 307 308 309 310 311 312 313
    QPointer<QDialog> d = new QDialog(QApplication::activeWindow());
    Ui::CutJobDialog_UI ui;
    ui.setupUi(d);
    ui.extra_params->setVisible(false);
    ui.add_clip->setChecked(KdenliveSettings::add_new_clip());
    ui.file_url->setMode(KFile::File);
    ui.extra_params->setMaximumHeight(QFontMetrics(QApplication::font()).lineSpacing() * 5);
    ui.file_url->setUrl(QUrl(dest));
314 315
    ui.button_more->setIcon(QIcon::fromTheme(QStringLiteral("configure")));
    ui.extra_params->setPlainText(QStringLiteral("-acodec copy -vcodec copy"));
316
    QString mess = i18n("Extracting %1 out of %2", Timecode::getStringTimecode(duration, fps, true), Timecode::getStringTimecode(max, fps, true));
317 318 319 320 321
    ui.info_label->setText(mess);
    if (d->exec() != QDialog::Accepted) {
        delete d;
        return jobs;
    }
322
    dest = ui.file_url->url().toLocalFile();
323 324 325 326 327 328 329
    bool acceptPath = dest != source;
    if (acceptPath && QFileInfo(dest).size() > 0) {
        // destination file olready exists, overwrite?
        acceptPath = false;
    }
    while (!acceptPath) {
        // Do not allow to save over original clip
Laurent Montel's avatar
Laurent Montel committed
330
        if (dest == source) {
Laurent Montel's avatar
Laurent Montel committed
331
            ui.info_label->setText(QStringLiteral("<b>") + i18n("You cannot overwrite original clip.") + QStringLiteral("</b><br>") + mess);
Laurent Montel's avatar
Laurent Montel committed
332 333 334
        } else if (KMessageBox::questionYesNo(QApplication::activeWindow(), i18n("Overwrite file %1", dest)) == KMessageBox::Yes) {
            break;
        }
335 336 337 338
        if (d->exec() != QDialog::Accepted) {
            delete d;
            return jobs;
        }
339
        dest = ui.file_url->url().toLocalFile();
340 341 342 343 344 345 346 347 348 349
        acceptPath = dest != source;
        if (acceptPath && QFileInfo(dest).size() > 0) {
            acceptPath = false;
        }
    }
    QString extraParams = ui.extra_params->toPlainText().simplified();
    KdenliveSettings::setAdd_new_clip(ui.add_clip->isChecked());
    delete d;

    QStringList jobParams;
350
    jobParams << QString::number((int) AbstractClipJob::CUTJOB);
351 352 353
    jobParams << dest << source << timeIn << timeOut << QString::number(duration);
    // parent folder, or -100 if we don't want to add clip to project
    jobParams << (KdenliveSettings::add_new_clip() ? clip->parent()->clipId() : QString::number(-100));
Laurent Montel's avatar
Laurent Montel committed
354 355 356
    if (!extraParams.isEmpty()) {
        jobParams << extraParams;
    }
357 358 359 360 361
    CutClipJob *job = new CutClipJob(clip->clipType(), clip->clipId(), jobParams);
    jobs.insert(clip, job);
    return jobs;
}

Laurent Montel's avatar
Laurent Montel committed
362
// static
Laurent Montel's avatar
Laurent Montel committed
363
QHash<ProjectClip *, AbstractClipJob *> CutClipJob::prepareTranscodeJob(double fps, const QList<ProjectClip *> &clips, const QStringList &parameters)
364
{
Laurent Montel's avatar
Laurent Montel committed
365
    QHash<ProjectClip *, AbstractClipJob *> jobs;
366 367
    QString params = parameters.at(0);
    QString desc;
Laurent Montel's avatar
Laurent Montel committed
368
    if (parameters.count() > 1) {
369
        desc = parameters.at(1);
Laurent Montel's avatar
Laurent Montel committed
370
    }
371

372 373 374
    QStringList existingFiles;
    QStringList sources;
    QStringList destinations;
Laurent Montel's avatar
Laurent Montel committed
375 376
    destinations.reserve(clips.count());
    sources.reserve(clips.count());
377
    for (int i = 0; i < clips.count(); i++) {
378
        QString source = clips.at(i)->url();
379
        sources << source;
380
        QString newFile = params.section(QLatin1Char(' '), -1).replace(QLatin1String("%1"), source);
381
        destinations << newFile;
Laurent Montel's avatar
Laurent Montel committed
382 383 384
        if (QFile::exists(newFile)) {
            existingFiles << newFile;
        }
385 386
    }
    if (!existingFiles.isEmpty()) {
Laurent Montel's avatar
Laurent Montel committed
387 388 389
        if (KMessageBox::warningContinueCancelList(QApplication::activeWindow(), i18n("The transcoding job will overwrite the following files:"), existingFiles) ==  KMessageBox::Cancel) {
            return jobs;
        }
390
    }
391

392 393 394 395 396 397
    QPointer<QDialog> d = new QDialog(QApplication::activeWindow());
    Ui::CutJobDialog_UI ui;
    ui.setupUi(d);
    d->setWindowTitle(i18n("Transcoding"));
    ui.extra_params->setMaximumHeight(QFontMetrics(qApp->font()).lineSpacing() * 5);
    if (clips.count() == 1) {
Laurent Montel's avatar
Laurent Montel committed
398
        ui.file_url->setUrl(QUrl(destinations.constFirst()));
Laurent Montel's avatar
Laurent Montel committed
399
    } else {
400 401 402 403 404
        ui.destination_label->setVisible(false);
        ui.file_url->setVisible(false);
    }
    ui.extra_params->setVisible(false);
    d->adjustSize();
405
    ui.button_more->setIcon(QIcon::fromTheme(QStringLiteral("configure")));
Laurent Montel's avatar
Laurent Montel committed
406
    connect(ui.button_more, &QAbstractButton::toggled, ui.extra_params, &QWidget::setVisible);
407
    ui.add_clip->setChecked(KdenliveSettings::add_new_clip());
408
    ui.extra_params->setPlainText(params.simplified().section(QLatin1Char(' '), 0, -2));
409
    QString mess = desc;
Laurent Montel's avatar
Laurent Montel committed
410
    mess.append(QLatin1Char(' ') + i18np("(%1 clip)", "(%1 clips)", clips.count()));
411 412 413 414 415 416 417 418 419 420 421 422 423
    ui.info_label->setText(mess);
    if (d->exec() != QDialog::Accepted) {
        delete d;
        return jobs;
    }
    params = ui.extra_params->toPlainText().simplified();
    KdenliveSettings::setAdd_new_clip(ui.add_clip->isChecked());
    for (int i = 0; i < clips.count(); i++) {
        ProjectClip *item = clips.at(i);
        QString src = sources.at(i);
        QString dest;
        if (clips.count() > 1) {
            dest = destinations.at(i);
Laurent Montel's avatar
Laurent Montel committed
424
        } else {
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
425
            dest = ui.file_url->url().toLocalFile();
426 427
        }
        QStringList jobParams;
428
        jobParams << QString::number((int) AbstractClipJob::TRANSCODEJOB);
429 430
        jobParams << dest << src << QString() << QString();
        jobParams << QString::number((int) item->duration().frames(fps));
431 432
        // parent folder, or -100 if we don't want to add clip to project
        jobParams << (KdenliveSettings::add_new_clip() ? item->parent()->clipId() : QString::number(-100));
433 434 435 436 437 438 439
        jobParams << params;
        CutClipJob *job = new CutClipJob(item->clipType(), item->clipId(), jobParams);
        jobs.insert(item, job);
    }
    delete d;
    return jobs;
}
440

Laurent Montel's avatar
Laurent Montel committed
441
// static
Laurent Montel's avatar
Laurent Montel committed
442
QHash<ProjectClip *, AbstractClipJob *> CutClipJob::prepareAnalyseJob(double fps, const QList<ProjectClip *> &clips, const QStringList &parameters)
443 444 445 446
{
    // Might be useful some day
    Q_UNUSED(parameters);

Laurent Montel's avatar
Laurent Montel committed
447
    QHash<ProjectClip *, AbstractClipJob *> jobs;
Laurent Montel's avatar
Laurent Montel committed
448
    for (ProjectClip *clip : clips) {
449
        QString source = clip->url();
450 451 452 453 454 455 456 457 458 459 460 461 462
        QStringList jobParams;
        int duration = clip->duration().frames(fps) * clip->getOriginalFps() / fps;
        jobParams << QString::number((int) AbstractClipJob::ANALYSECLIPJOB) << QString() << source << QString::number(duration);
        CutClipJob *job = new CutClipJob(clip->clipType(), clip->clipId(), jobParams);
        jobs.insert(clip, job);
    }
    return jobs;
}

void CutClipJob::processAnalyseLog()
{
    QJsonDocument doc = QJsonDocument::fromJson(m_logDetails.toUtf8());
    if (doc.isEmpty()) {
Laurent Montel's avatar
Laurent Montel committed
463
        qCDebug(KDENLIVE_LOG) << "+ + + + +CORRUPTED JSON DOC";
464 465
    }
    QJsonObject jsonObject = doc.object();
Laurent Montel's avatar
Laurent Montel committed
466
    const QJsonArray jsonArray = jsonObject[QStringLiteral("frames")].toArray();
Laurent Montel's avatar
Laurent Montel committed
467
    QList<int> frames;
Laurent Montel's avatar
Laurent Montel committed
468
    for (const QJsonValue &value : jsonArray) {
469
        QJsonObject obj = value.toObject();
Laurent Montel's avatar
Laurent Montel committed
470 471 472
        if (obj[QStringLiteral("pict_type")].toString() != QLatin1String("I")) {
            continue;
        }
473
        frames << obj[QStringLiteral("coded_picture_number")].toInt();
474 475
    }
    qSort(frames);
Laurent Montel's avatar
Laurent Montel committed
476
    QMap<QString, QString> jobResults;
477
    QStringList sortedFrames;
Laurent Montel's avatar
Laurent Montel committed
478
    foreach (int frm, frames) {
479 480
        sortedFrames << QString::number(frm);
    }
481
    jobResults.insert(QStringLiteral("i-frame"), sortedFrames.join(QLatin1Char(';')));
Laurent Montel's avatar
Laurent Montel committed
482
    QMap<QString, QString> extraInfo;
483 484 485 486 487
    extraInfo.insert(QStringLiteral("addmarkers"), QStringLiteral("3"));
    extraInfo.insert(QStringLiteral("key"), QStringLiteral("i-frame"));
    extraInfo.insert(QStringLiteral("simplelist"), QStringLiteral("1"));
    extraInfo.insert(QStringLiteral("label"), i18n("I-Frame "));
    extraInfo.insert(QStringLiteral("resultmessage"), i18n("Found %count I-Frames"));
488 489
    emit gotFilterJobResults(m_clipId, 0, 0, jobResults, extraInfo);
}