File indexing completed on 2024-04-28 08:43:07

0001 /*
0002     SPDX-FileCopyrightText: 2007 Jean-Baptiste Mardelle <jb@kdenlive.org>
0003 
0004     SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0005 */
0006 
0007 #include "renderjob.h"
0008 
0009 #include <QStringList>
0010 #include <QThread>
0011 #ifndef NODBUS
0012 #include <QtDBus>
0013 #else
0014 #include <QJsonDocument>
0015 #include <QJsonObject>
0016 #endif
0017 #include <QDebug>
0018 #include <QDir>
0019 #include <QElapsedTimer>
0020 #include <QStandardPaths>
0021 #include <utility>
0022 // Can't believe I need to do this to sleep.
0023 class SleepThread : QThread
0024 {
0025 public:
0026     void run() override {}
0027     static void msleep(unsigned long msecs) { QThread::msleep(msecs); }
0028 };
0029 
0030 RenderJob::RenderJob(const QString &render, const QString &scenelist, const QString &target, int pid, int in, int out, const QString &subtitleFile,
0031                      QObject *parent)
0032     : QObject(parent)
0033     , m_scenelist(scenelist)
0034     , m_dest(target)
0035     , m_progress(0)
0036     , m_prog(render)
0037 #ifndef NODBUS
0038     , m_jobUiserver(nullptr)
0039     , m_kdenliveinterface(nullptr)
0040 #else
0041     , m_kdenlivesocket(new QLocalSocket(this))
0042 #endif
0043     , m_logfile(m_dest + QStringLiteral(".log"))
0044     , m_erase(scenelist.startsWith(QDir::tempPath()) || scenelist.startsWith(QString("xml:%2").arg(QDir::tempPath())))
0045     , m_seconds(0)
0046     , m_frame(in)
0047     , m_framein(in)
0048     , m_frameout(out)
0049     , m_pid(pid)
0050     , m_dualpass(false)
0051     , m_subtitleFile(subtitleFile)
0052 {
0053     m_renderProcess = new QProcess(&m_looper);
0054     m_renderProcess->setReadChannel(QProcess::StandardError);
0055     connect(m_renderProcess, &QProcess::stateChanged, this, &RenderJob::slotCheckProcess);
0056 
0057     // Disable VDPAU so that rendering will work even if there is a Kdenlive instance using VDPAU
0058     qputenv("MLT_NO_VDPAU", "1");
0059     m_args << "-progress" << scenelist;
0060 
0061     // Create a log of every render process.
0062     if (!m_logfile.open(QIODevice::WriteOnly | QIODevice::Text)) {
0063         qWarning() << "Unable to log to " << m_logfile.fileName();
0064     } else {
0065         m_logstream.setDevice(&m_logfile);
0066     }
0067 }
0068 
0069 RenderJob::~RenderJob()
0070 {
0071 #ifndef NODBUS
0072     delete m_jobUiserver;
0073     delete m_kdenliveinterface;
0074 #else
0075     if (m_kdenlivesocket->state() == QLocalSocket::ConnectedState) {
0076         m_kdenlivesocket->disconnectFromServer();
0077     }
0078     delete m_kdenlivesocket;
0079 #endif
0080     delete m_renderProcess;
0081     m_logfile.close();
0082 }
0083 
0084 void RenderJob::slotAbort(const QString &url)
0085 {
0086     if (m_dest == url) {
0087         slotAbort();
0088     }
0089 }
0090 
0091 void RenderJob::sendFinish(int status, const QString &error)
0092 {
0093 #ifndef NODBUS
0094     if (m_kdenliveinterface) {
0095         m_kdenliveinterface->callWithArgumentList(QDBus::NoBlock, QStringLiteral("setRenderingFinished"), {m_dest, status, error});
0096     }
0097     if (m_jobUiserver) {
0098         if (status > -3) {
0099             m_jobUiserver->call(QStringLiteral("setDescriptionField"), 1, tr("Rendered file"), m_dest);
0100         }
0101         m_jobUiserver->call(QStringLiteral("terminate"), QString());
0102     }
0103 #else
0104     if (m_kdenlivesocket->state() == QLocalSocket::ConnectedState) {
0105         QJsonObject method, args;
0106         args["url"] = m_dest;
0107         args["status"] = status;
0108         args["error"] = error;
0109         method["setRenderingFinished"] = args;
0110         m_kdenlivesocket->write(QJsonDocument(method).toJson());
0111         m_kdenlivesocket->flush();
0112     }
0113 #endif
0114     else {
0115         qDebug() << "Rendering to" << m_dest << "finished. Status:" << status << "Errors:" << error;
0116     }
0117 }
0118 
0119 void RenderJob::slotAbort()
0120 {
0121     m_renderProcess->kill();
0122     sendFinish(-3, QString());
0123     if (m_erase) {
0124         QFile(m_scenelist).remove();
0125     }
0126     QFile(m_dest).remove();
0127     m_logstream << "Job aborted by user"
0128                 << "\n";
0129     m_logstream.flush();
0130     m_logfile.close();
0131 #ifndef NODBUS
0132     qApp->quit();
0133 #endif
0134 }
0135 
0136 void RenderJob::receivedStderr()
0137 {
0138     QString result = QString::fromLocal8Bit(m_renderProcess->readAllStandardError()).simplified();
0139     if (!result.startsWith(QLatin1String("Current Frame"))) {
0140         m_errorMessage.append(result + QStringLiteral("<br>"));
0141         m_logstream << result;
0142     } else {
0143         int progress = result.section(QLatin1Char(' '), -1).toInt();
0144         int frame = result.section(QLatin1Char(','), 0, 0).section(QLatin1Char(' '), -1).toInt();
0145         if (progress <= m_progress || progress <= 0 || progress > 100) {
0146             return;
0147         }
0148         m_progress = progress;
0149         if (m_args.contains(QStringLiteral("pass=1"))) {
0150             m_progress /= 2;
0151         } else if (m_args.contains(QStringLiteral("pass=2"))) {
0152             m_progress = 50 + m_progress / 2;
0153         }
0154         qint64 elapsedTime = m_startTime.secsTo(QDateTime::currentDateTime());
0155         if (elapsedTime == m_seconds) {
0156             return;
0157         }
0158         int speed = (frame - m_frame) / (elapsedTime - m_seconds);
0159         m_seconds = elapsedTime;
0160         m_frame = frame;
0161         updateProgress(speed);
0162     }
0163 }
0164 
0165 void RenderJob::updateProgress(int speed)
0166 {
0167 #ifndef NODBUS
0168     if ((m_kdenliveinterface != nullptr) && m_kdenliveinterface->isValid()) {
0169         m_kdenliveinterface->callWithArgumentList(QDBus::NoBlock, QStringLiteral("setRenderingProgress"), {m_dest, m_progress, m_frame});
0170     }
0171     if (m_jobUiserver) {
0172         qint64 remaining = m_seconds * (100 - m_progress) / m_progress;
0173         int days = int(remaining / 86400);
0174         int remainingSecs = int(remaining % 86400);
0175         QTime when = QTime(0, 0, 0, 0).addSecs(remainingSecs);
0176         QString est = tr("Remaining time ");
0177         if (days > 0) {
0178             est.append(tr("%n day(s) ", "", days));
0179         }
0180         est.append(when.toString(QStringLiteral("hh:mm:ss")));
0181         m_jobUiserver->call(QStringLiteral("setPercent"), uint(m_progress));
0182         m_jobUiserver->call(QStringLiteral("setProcessedAmount"), qulonglong(m_frame - m_framein), tr("frames"));
0183         if (speed > -1) {
0184             m_jobUiserver->call(QStringLiteral("setSpeed"), qulonglong(speed));
0185         }
0186         m_jobUiserver->call(QStringLiteral("setDescriptionField"), 0, QString(), est);
0187     }
0188 #else
0189     if (m_kdenlivesocket->state() == QLocalSocket::ConnectedState) {
0190         QJsonObject method, args;
0191         args["url"] = m_dest;
0192         args["progress"] = m_progress;
0193         args["frame"] = m_frame;
0194         method["setRenderingProgress"] = args;
0195         m_kdenlivesocket->write(QJsonDocument(method).toJson());
0196         m_kdenlivesocket->flush();
0197     }
0198 #endif
0199     else {
0200         qDebug() << "Progress:" << m_progress << "%,"
0201                  << "frame" << m_frame;
0202     }
0203     m_logstream << QStringLiteral("%1\t%2\t%3\n").arg(m_seconds).arg(m_frame).arg(m_progress);
0204 }
0205 
0206 void RenderJob::start()
0207 {
0208     m_startTime = QDateTime::currentDateTime();
0209 #ifndef NODBUS
0210     QDBusConnectionInterface *interface = QDBusConnection::sessionBus().interface();
0211     if ((interface != nullptr)) {
0212         if (!interface->isServiceRegistered(QStringLiteral("org.kde.JobViewServer"))) {
0213             qWarning() << "No org.kde.JobViewServer registered, trying to start kuiserver";
0214             if (QProcess::startDetached(QStringLiteral("kuiserver"), QStringList())) {
0215                 // Give it a couple of seconds to start
0216                 QElapsedTimer t;
0217                 t.start();
0218                 while (!interface->isServiceRegistered(QStringLiteral("org.kde.JobViewServer")) && t.elapsed() < 3000) {
0219                     SleepThread::msleep(100); // Sleep 100 ms
0220                 }
0221             } else {
0222                 qWarning() << "Failed to start kuiserver";
0223             }
0224         }
0225 
0226         if (interface->isServiceRegistered(QStringLiteral("org.kde.JobViewServer"))) {
0227             QDBusInterface kuiserver(QStringLiteral("org.kde.JobViewServer"), QStringLiteral("/JobViewServer"), QStringLiteral("org.kde.JobViewServer"));
0228             QDBusReply<QDBusObjectPath> objectPath =
0229                 kuiserver.asyncCall(QStringLiteral("requestView"), QLatin1String("kdenlive"), QLatin1String("kdenlive"), 0x0001);
0230             QString reply = QDBusObjectPath(objectPath).path();
0231 
0232             // Use of the KDE JobViewServer is an ugly hack, it is not reliable
0233             QString dbusView = QStringLiteral("org.kde.JobViewV2");
0234             m_jobUiserver = new QDBusInterface(QStringLiteral("org.kde.JobViewServer"), reply, dbusView);
0235             if (m_jobUiserver->isValid()) {
0236                 if (!m_args.contains(QStringLiteral("pass=2"))) {
0237                     m_jobUiserver->call(QStringLiteral("setPercent"), 0);
0238                 }
0239 
0240                 m_jobUiserver->call(QStringLiteral("setInfoMessage"), tr("Rendering %1").arg(QFileInfo(m_dest).fileName()));
0241                 m_jobUiserver->call(QStringLiteral("setTotalAmount"), m_frameout);
0242                 QDBusConnection::sessionBus().connect(QStringLiteral("org.kde.JobViewServer"), reply, dbusView, QStringLiteral("cancelRequested"), this,
0243                                                       SLOT(slotAbort()));
0244             }
0245         }
0246     }
0247     if (m_pid > -1) {
0248         initKdenliveDbusInterface();
0249     }
0250 #else
0251     connect(m_kdenlivesocket, &QLocalSocket::connected, this, [this]() {
0252         m_kdenlivesocket->write(QJsonDocument({{"url", m_dest}}).toJson());
0253         m_kdenlivesocket->flush();
0254         QJsonObject method, args;
0255         args["url"] = m_dest;
0256         args["progress"] = 0;
0257         args["frame"] = 0;
0258         method["setRenderingProgress"] = args;
0259         m_kdenlivesocket->write(QJsonDocument(method).toJson());
0260         m_kdenlivesocket->flush();
0261     });
0262     connect(m_kdenlivesocket, &QLocalSocket::readyRead, this, [this]() {
0263         QByteArray msg = m_kdenlivesocket->readAll();
0264         if (msg == "abort") {
0265             slotAbort();
0266         }
0267     });
0268     if (m_pid > -1) {
0269         QString servername = QStringLiteral("org.kde.kdenlive-%1").arg(m_pid);
0270         m_kdenlivesocket->connectToServer(servername);
0271     }
0272 #endif
0273 
0274     // Because of the logging, we connect to stderr in all cases.
0275     connect(m_renderProcess, &QProcess::readyReadStandardError, this, &RenderJob::receivedStderr);
0276     m_renderProcess->start(m_prog, m_args);
0277     m_logstream << "Started render process: " << m_prog << ' ' << m_args.join(QLatin1Char(' ')) << "\n";
0278     m_logstream.flush();
0279     m_looper.exec();
0280 }
0281 
0282 #ifndef NODBUS
0283 void RenderJob::initKdenliveDbusInterface()
0284 {
0285     QString kdenliveId = QStringLiteral("org.kde.kdenlive-%1").arg(m_pid);
0286     QDBusConnection connection = QDBusConnection::sessionBus();
0287     QDBusConnectionInterface *ibus = connection.interface();
0288     if (!ibus->isServiceRegistered(kdenliveId)) {
0289         kdenliveId.clear();
0290         const QStringList services = ibus->registeredServiceNames();
0291         for (const QString &service : services) {
0292             if (!service.startsWith(QLatin1String("org.kde.kdenlive"))) {
0293                 continue;
0294             }
0295             kdenliveId = service;
0296             break;
0297         }
0298     }
0299     if (kdenliveId.isEmpty()) {
0300         return;
0301     }
0302     m_kdenliveinterface =
0303         new QDBusInterface(kdenliveId, QStringLiteral("/kdenlive/MainWindow_1"), QStringLiteral("org.kde.kdenlive.rendering"), connection, this);
0304 
0305     if (!m_args.contains(QStringLiteral("pass=2"))) {
0306         m_kdenliveinterface->callWithArgumentList(QDBus::NoBlock, QStringLiteral("setRenderingProgress"), {m_dest, 0, 0});
0307     }
0308     connect(m_kdenliveinterface, SIGNAL(abortRenderJob(QString)), this, SLOT(slotAbort(QString)));
0309 }
0310 #endif
0311 
0312 void RenderJob::slotCheckProcess(QProcess::ProcessState state)
0313 {
0314     if (state == QProcess::NotRunning) {
0315         slotIsOver(m_renderProcess->exitStatus());
0316     }
0317 }
0318 
0319 void RenderJob::slotIsOver(QProcess::ExitStatus status, bool isWritable)
0320 {
0321     if (!isWritable) {
0322         QString error = tr("Cannot write to %1, check permissions.").arg(m_dest);
0323         sendFinish(-2, error);
0324         // assumes kdialog installed!!
0325         QProcess::startDetached(QStringLiteral("kdialog"), {QStringLiteral("--error"), error});
0326         m_logstream << error << "\n";
0327         Q_EMIT renderingFinished();
0328         // qApp->quit();
0329     }
0330     if (m_erase) {
0331         QFile(m_scenelist).remove();
0332     }
0333     if (status == QProcess::CrashExit || m_renderProcess->error() != QProcess::UnknownError || m_renderProcess->exitCode() != 0) {
0334         // rendering crashed
0335         sendFinish(-2, m_errorMessage);
0336         QStringList args;
0337         QString error = tr("Rendering of %1 aborted, resulting video will probably be corrupted.").arg(m_dest);
0338         if (m_frame > 0) {
0339             error += QLatin1Char('\n') + tr("Frame: %1").arg(m_frame);
0340         }
0341         args << QStringLiteral("--error") << error;
0342         m_logstream << error << "\n";
0343         QProcess::startDetached(QStringLiteral("kdialog"), args);
0344         Q_EMIT renderingFinished();
0345     } else {
0346         m_logstream << "Rendering of " << m_dest << " finished"
0347                     << "\n";
0348         m_logstream.flush();
0349         if (m_dualpass) {
0350             deleteLater();
0351         } else {
0352             m_logfile.remove();
0353             if (!m_subtitleFile.isEmpty()) {
0354                 // Embed subtitles
0355                 QString ffmpegExe = QStandardPaths::findExecutable(QStringLiteral("ffmpeg"));
0356                 if (!ffmpegExe.isEmpty()) {
0357                     QFileInfo videoRender(m_dest);
0358                     m_temporaryRenderFile = QDir::temp().absoluteFilePath(videoRender.fileName());
0359                     QStringList args = {
0360                         "-y", "-v", "quiet", "-stats", "-i", m_dest, "-i", m_subtitleFile, "-c", "copy", "-f", "matroska", m_temporaryRenderFile};
0361                     qDebug() << "::: JOB ARGS: " << args;
0362                     m_progress = 0;
0363                     disconnect(m_renderProcess, &QProcess::stateChanged, this, &RenderJob::slotCheckProcess);
0364                     disconnect(m_renderProcess, &QProcess::readyReadStandardError, this, &RenderJob::receivedStderr);
0365                     m_subsProcess = new QProcess(&m_looper);
0366                     m_subsProcess->setProcessChannelMode(QProcess::MergedChannels);
0367                     connect(m_subsProcess, &QProcess::readyReadStandardOutput, this, &RenderJob::receivedSubtitleProgress);
0368                     m_subsProcess->start(ffmpegExe, args);
0369                     m_subsProcess->waitForStarted(-1);
0370                     m_subsProcess->waitForFinished(-1);
0371                     slotCheckSubtitleProcess(m_subsProcess->exitCode(), m_subsProcess->exitStatus());
0372                     return;
0373                 }
0374             }
0375             sendFinish(-1, QString());
0376         }
0377     }
0378     Q_EMIT renderingFinished();
0379     m_looper.quit();
0380 }
0381 
0382 void RenderJob::receivedSubtitleProgress()
0383 {
0384     QString outputData = QString::fromLocal8Bit(m_subsProcess->readAllStandardOutput()).simplified();
0385     if (outputData.isEmpty()) {
0386         return;
0387     }
0388     QStringList output = outputData.split(QLatin1Char(' '));
0389     m_errorMessage.append(outputData + QStringLiteral("<br>"));
0390     QString result = output.takeFirst();
0391     bool ok = false;
0392     int frame = -1;
0393     if (result == (QLatin1String("frame=")) && !output.isEmpty()) {
0394         // Frame number is the second parameter
0395         result = output.takeFirst();
0396         frame = result.toInt(&ok);
0397     } else if (result.startsWith(QLatin1String("frame="))) {
0398         frame = result.section(QLatin1Char('='), 1).toInt(&ok);
0399     }
0400     if (ok && frame > 0) {
0401         m_frame = frame;
0402         m_progress = 100 * frame / (m_frameout - m_framein);
0403         if (m_progress > 0) {
0404             updateProgress();
0405         }
0406     }
0407 }
0408 
0409 void RenderJob::slotCheckSubtitleProcess(int exitCode, QProcess::ExitStatus exitStatus)
0410 {
0411     if (exitStatus == QProcess::CrashExit || !QFile::exists(m_temporaryRenderFile)) {
0412         // rendering crashed
0413         qDebug() << ":::: FOUND ERROR IN SUBS: " << exitStatus << " / " << exitCode << ", FILE EXISTS: " << QFile::exists(m_temporaryRenderFile);
0414         QString error = tr("Rendering of %1 aborted when adding subtitles.").arg(m_dest);
0415         m_errorMessage.append(error);
0416         sendFinish(-2, m_errorMessage);
0417         QStringList args;
0418         if (m_frame > 0) {
0419             error += QLatin1Char('\n') + tr("Frame: %1").arg(m_frame);
0420         }
0421         args << QStringLiteral("--error") << error;
0422         m_logstream << error << "\n";
0423         QProcess::startDetached(QStringLiteral("kdialog"), args);
0424     } else {
0425         QFile::remove(m_dest);
0426         QFile::rename(m_temporaryRenderFile, m_dest);
0427         sendFinish(-1, QString());
0428     }
0429     QFile::remove(m_subtitleFile);
0430     Q_EMIT renderingFinished();
0431     m_looper.quit();
0432 }