File indexing completed on 2024-12-01 04:28:36

0001 /*
0002     SPDX-FileCopyrightText: 2021 Jean-Baptiste Mardelle <jb@kdenlive.org>
0003 
0004 SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0005 */
0006 
0007 #include "cuttask.h"
0008 #include "bin/bin.h"
0009 #include "bin/projectclip.h"
0010 #include "bin/projectfolder.h"
0011 #include "bin/projectitemmodel.h"
0012 #include "core.h"
0013 #include "kdenlive_debug.h"
0014 #include "kdenlivesettings.h"
0015 #include "macros.hpp"
0016 #include "mainwindow.h"
0017 #include "profiles/profilemodel.hpp"
0018 #include "ui_cutjobdialog_ui.h"
0019 #include "utils/qstringutils.h"
0020 #include "xml/xml.hpp"
0021 
0022 #include <KIO/RenameDialog>
0023 #include <KLineEdit>
0024 #include <KLocalizedString>
0025 #include <KMessageBox>
0026 #include <QComboBox>
0027 #include <QObject>
0028 #include <QProcess>
0029 #include <QThread>
0030 
0031 CutTask::CutTask(const ObjectId &owner, const QString &destination, const QStringList &encodingParams, int in, int out, bool addToProject, QObject *object)
0032     : AbstractTask(owner, AbstractTask::CUTJOB, object)
0033     , m_inPoint(GenTime(in, pCore->getCurrentFps()))
0034     , m_outPoint(GenTime(out, pCore->getCurrentFps()))
0035     , m_destination(destination)
0036     , m_encodingParams(encodingParams)
0037     , m_jobDuration(0)
0038     , m_addToProject(addToProject)
0039 {
0040     m_description = i18n("Extracting zone");
0041 }
0042 
0043 void CutTask::start(const ObjectId &owner, int in, int out, QObject *object, bool force)
0044 {
0045     auto binClip = pCore->projectItemModel()->getClipByBinID(QString::number(owner.itemId));
0046     ClipType::ProducerType type = binClip->clipType();
0047     if (type != ClipType::AV && type != ClipType::Audio && type != ClipType::Video) {
0048         // m_errorMessage.prepend(i18n("Cannot extract zone for this clip type."));
0049         return;
0050     }
0051     const QString source = binClip->url();
0052     QString videoCodec = binClip->codec(false);
0053     QString audioCodec = binClip->codec(true);
0054     // Check if the audio/video codecs are supported for encoding (required for the codec copy feature)
0055     QProcess checkProcess;
0056     QStringList params = {QStringLiteral("-codecs")};
0057     checkProcess.start(KdenliveSettings::ffmpegpath(), params);
0058     checkProcess.waitForFinished(); // sets current thread to sleep and waits for pingProcess end
0059     QString output(checkProcess.readAllStandardOutput());
0060     QString line;
0061     QTextStream stream(&output);
0062     bool videoOk = videoCodec.isEmpty();
0063     bool audioOk = audioCodec.isEmpty();
0064     while (stream.readLineInto(&line)) {
0065         if (!videoOk && line.contains(videoCodec)) {
0066             if (line.simplified().section(QLatin1Char(' '), 0, 0).contains(QLatin1Char('E'))) {
0067                 videoOk = true;
0068             }
0069         } else if (!audioOk && line.contains(audioCodec)) {
0070             if (line.simplified().section(QLatin1Char(' '), 0, 0).contains(QLatin1Char('E'))) {
0071                 audioOk = true;
0072             }
0073         }
0074         if (audioOk && videoOk) {
0075             break;
0076         }
0077     }
0078     QString warnMessage;
0079     if (!videoOk) {
0080         warnMessage = i18n("Cannot copy video codec %1, will re-encode.", videoCodec);
0081     }
0082     if (!audioOk) {
0083         if (!videoOk) {
0084             warnMessage.append(QLatin1Char('\n'));
0085         }
0086         warnMessage.append(i18n("Cannot copy audio codec %1, will re-encode.", audioCodec));
0087     }
0088 
0089     QFileInfo finfo(source);
0090     QDir dir = finfo.absoluteDir();
0091     QString inString = QString::number(int(GenTime(in, pCore->getCurrentFps()).seconds()));
0092     QString outString = QString::number(int(GenTime(out, pCore->getCurrentFps()).seconds()));
0093     QString fileName = QStringUtils::appendToFilename(finfo.fileName(), QString("-%1-%2").arg(inString, outString));
0094     QString path = dir.absoluteFilePath(fileName);
0095 
0096     QPointer<QDialog> d = new QDialog(QApplication::activeWindow());
0097     Ui::CutJobDialog_UI ui;
0098     ui.setupUi(d);
0099     ui.extra_params->setVisible(false);
0100     ui.message->setText(warnMessage);
0101     ui.message->setVisible(!warnMessage.isEmpty());
0102     if (videoCodec.isEmpty()) {
0103         ui.video_codec->setText(i18n("none"));
0104         ui.vcodec->setEnabled(false);
0105     } else {
0106         ui.video_codec->setText(videoCodec);
0107         ui.vcodec->addItem(i18n("Copy stream"), QStringLiteral("copy"));
0108         ui.vcodec->addItem(i18n("X264 encoding"), QStringLiteral("libx264"));
0109         ui.vcodec->addItem(i18n("Disable stream"));
0110     }
0111     if (audioCodec.isEmpty()) {
0112         ui.audio_codec->setText(i18n("none"));
0113         ui.acodec->setEnabled(false);
0114     } else {
0115         ui.audio_codec->setText(audioCodec);
0116         ui.acodec->addItem(i18n("Copy stream"), QStringLiteral("copy"));
0117         ui.acodec->addItem(i18n("PCM encoding"), QStringLiteral("pcm_s24le"));
0118         ui.acodec->addItem(i18n("AAC encoding"), QStringLiteral("aac"));
0119         ui.acodec->addItem(i18n("Disable stream"));
0120     }
0121     ui.audio_codec->setText(audioCodec);
0122     ui.add_clip->setChecked(KdenliveSettings::add_new_clip());
0123     ui.file_url->setMode(KFile::File);
0124     ui.extra_params->setMaximumHeight(QFontMetrics(QApplication::font()).lineSpacing() * 5);
0125     ui.file_url->setUrl(QUrl::fromLocalFile(path));
0126 
0127     QString transcoderExt = QLatin1Char('.') + finfo.suffix();
0128 
0129     std::function<void()> callBack = [&ui, transcoderExt]() {
0130         if (ui.acodec->currentData().isNull()) {
0131             // Video only
0132             ui.extra_params->setPlainText(QString("-an -c:v %1").arg(ui.vcodec->currentData().toString()));
0133         } else if (ui.vcodec->currentData().isNull()) {
0134             // Audio only
0135             ui.extra_params->setPlainText(QString("-vn -c:a %1").arg(ui.acodec->currentData().toString()));
0136         } else {
0137             ui.extra_params->setPlainText(QString("-c:a %1 -c:v %2").arg(ui.acodec->currentData().toString(), ui.vcodec->currentData().toString()));
0138         }
0139         QString path = ui.file_url->url().toLocalFile();
0140         QString fileName = path.section(QLatin1Char('.'), 0, -2);
0141         if (ui.acodec->currentData() == QLatin1String("copy") && ui.vcodec->currentData() == QLatin1String("copy")) {
0142             fileName.append(transcoderExt);
0143         } else {
0144             fileName.append(QStringLiteral(".mov"));
0145         }
0146         ui.file_url->setUrl(QUrl::fromLocalFile(fileName));
0147     };
0148 
0149     QObject::connect(ui.acodec, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), d.data(), [callBack]() { callBack(); });
0150 
0151     QObject::connect(ui.vcodec, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), d.data(), [callBack]() { callBack(); });
0152     QFontMetrics fm = ui.file_url->lineEdit()->fontMetrics();
0153     ui.file_url->setMinimumWidth(int(fm.boundingRect(ui.file_url->text().left(50)).width() * 1.4));
0154     callBack();
0155     QString mess = i18n("Extracting %1 out of %2", Timecode::getStringTimecode(out - in, pCore->getCurrentFps(), true), binClip->getStringDuration());
0156     ui.info_label->setText(mess);
0157     if (!videoOk) {
0158         ui.vcodec->setCurrentIndex(1);
0159     }
0160     if (!audioOk) {
0161         ui.acodec->setCurrentIndex(1);
0162     }
0163     if (d->exec() != QDialog::Accepted) {
0164         delete d;
0165         return;
0166     }
0167     path = ui.file_url->url().toLocalFile();
0168     QStringList encodingParams = ui.extra_params->toPlainText().split(QLatin1Char(' '), Qt::SkipEmptyParts);
0169     KdenliveSettings::setAdd_new_clip(ui.add_clip->isChecked());
0170     delete d;
0171 
0172     if (QFile::exists(path)) {
0173         KIO::RenameDialog renameDialog(qApp->activeWindow(), i18n("File already exists"), QUrl::fromLocalFile(path), QUrl::fromLocalFile(path),
0174                                        KIO::RenameDialog_Option::RenameDialog_Overwrite);
0175         if (renameDialog.exec() != QDialog::Rejected) {
0176             QUrl final = renameDialog.newDestUrl();
0177             if (final.isValid()) {
0178                 path = final.toLocalFile();
0179             }
0180         } else {
0181             return;
0182         }
0183     }
0184     CutTask *task = new CutTask(owner, path, encodingParams, in, out, KdenliveSettings::add_new_clip(), object);
0185     // Otherwise, start a filter thread.
0186     task->m_isForce = force;
0187     pCore->taskManager.startTask(owner.itemId, task);
0188 }
0189 
0190 void CutTask::run()
0191 {
0192     AbstractTaskDone whenFinished(m_owner.itemId, this);
0193     if (m_isCanceled || pCore->taskManager.isBlocked()) {
0194         return;
0195     }
0196     QMutexLocker lock(&m_runMutex);
0197     m_running = true;
0198     qDebug() << " + + + + + + + + STARTING STAB TASK";
0199 
0200     QString url;
0201     auto binClip = pCore->projectItemModel()->getClipByBinID(QString::number(m_owner.itemId));
0202     if (binClip) {
0203         // Filter applied on a timeline or bin clip
0204         url = binClip->url();
0205         QString folder = QStringLiteral("-1");
0206         auto containingFolder = std::static_pointer_cast<ProjectFolder>(binClip->parent());
0207         if (containingFolder) {
0208             folder = containingFolder->clipId();
0209         }
0210         if (url.isEmpty()) {
0211             QMetaObject::invokeMethod(pCore.get(), "displayBinMessage", Qt::QueuedConnection, Q_ARG(QString, i18n("No producer for this clip.")),
0212                                       Q_ARG(int, int(KMessageWidget::Warning)));
0213             m_errorMessage.append(i18n("No producer for this clip."));
0214             return;
0215         }
0216         if (QFileInfo(m_destination).absoluteFilePath() == QFileInfo(url).absoluteFilePath()) {
0217             QMetaObject::invokeMethod(pCore.get(), "displayBinMessage", Qt::QueuedConnection, Q_ARG(QString, i18n("You cannot overwrite original clip.")),
0218                                       Q_ARG(int, int(KMessageWidget::Warning)));
0219             m_errorMessage.append(i18n("You cannot overwrite original clip."));
0220             return;
0221         }
0222         QStringList params = {QStringLiteral("-y"),
0223                               QStringLiteral("-stats"),
0224                               QStringLiteral("-v"),
0225                               QStringLiteral("error"),
0226                               QStringLiteral("-noaccurate_seek"),
0227                               QStringLiteral("-ss"),
0228                               QString::number(m_inPoint.seconds()),
0229                               QStringLiteral("-i"),
0230                               url,
0231                               QStringLiteral("-t"),
0232                               QString::number((m_outPoint - m_inPoint).seconds()),
0233                               QStringLiteral("-avoid_negative_ts"),
0234                               QStringLiteral("make_zero"),
0235                               QStringLiteral("-sn"),
0236                               QStringLiteral("-dn"),
0237                               QStringLiteral("-map"),
0238                               QStringLiteral("0")};
0239         params << m_encodingParams << m_destination;
0240         m_jobProcess = std::make_unique<QProcess>(new QProcess);
0241         connect(m_jobProcess.get(), &QProcess::readyReadStandardError, this, &CutTask::processLogInfo);
0242         connect(this, &CutTask::jobCanceled, m_jobProcess.get(), &QProcess::kill, Qt::DirectConnection);
0243         qDebug() << "=== STARTING CUT JOB: " << params;
0244         m_jobProcess->start(KdenliveSettings::ffmpegpath(), params, QIODevice::ReadOnly);
0245         m_jobProcess->waitForFinished(-1);
0246         bool result = m_jobProcess->exitStatus() == QProcess::NormalExit;
0247         // remove temporary playlist if it exists
0248         if (result && !m_isCanceled) {
0249             if (QFileInfo(m_destination).size() == 0) {
0250                 QFile::remove(m_destination);
0251                 // File was not created
0252                 QMetaObject::invokeMethod(pCore.get(), "displayBinLogMessage", Qt::QueuedConnection, Q_ARG(QString, i18n("Failed to create file.")),
0253                                           Q_ARG(int, int(KMessageWidget::Warning)), Q_ARG(QString, m_logDetails));
0254             } else {
0255                 // all ok, add clip
0256                 if (m_addToProject) {
0257                     QMetaObject::invokeMethod(pCore->window(), "addProjectClip", Qt::QueuedConnection, Q_ARG(QString, m_destination), Q_ARG(QString, folder));
0258                 }
0259             }
0260         } else {
0261             // transcode task crashed
0262             QFile::remove(m_destination);
0263             QMetaObject::invokeMethod(pCore.get(), "displayBinLogMessage", Qt::QueuedConnection, Q_ARG(QString, i18n("Cut job failed.")),
0264                                       Q_ARG(int, int(KMessageWidget::Warning)), Q_ARG(QString, m_logDetails));
0265         }
0266     }
0267 }
0268 
0269 void CutTask::processLogInfo()
0270 {
0271     const QString buffer = QString::fromUtf8(m_jobProcess->readAllStandardError());
0272     m_logDetails.append(buffer);
0273     int progress = 0;
0274     // Parse FFmpeg output
0275     if (m_jobDuration == 0) {
0276         if (buffer.contains(QLatin1String("Duration:"))) {
0277             QString data = buffer.section(QStringLiteral("Duration:"), 1, 1).section(QLatin1Char(','), 0, 0).simplified();
0278             if (!data.isEmpty()) {
0279                 QStringList numbers = data.split(QLatin1Char(':'));
0280                 if (numbers.size() < 3) {
0281                     return;
0282                 }
0283                 m_jobDuration = numbers.at(0).toInt() * 3600 + numbers.at(1).toInt() * 60 + numbers.at(2).toInt();
0284             }
0285         }
0286     } else if (buffer.contains(QLatin1String("time="))) {
0287         QString time = buffer.section(QStringLiteral("time="), 1, 1).simplified().section(QLatin1Char(' '), 0, 0);
0288         if (!time.isEmpty()) {
0289             QStringList numbers = time.split(QLatin1Char(':'));
0290             if (numbers.size() < 3) {
0291                 progress = time.toInt();
0292                 if (progress == 0) {
0293                     return;
0294                 }
0295             } else {
0296                 progress = numbers.at(0).toInt() * 3600 + numbers.at(1).toInt() * 60 + qRound(numbers.at(2).toDouble());
0297             }
0298         }
0299         m_progress = 100 * progress / m_jobDuration;
0300         QMetaObject::invokeMethod(m_object, "updateJobProgress");
0301     }
0302 }