File indexing completed on 2024-03-24 04:53:39
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 }