File indexing completed on 2024-04-28 04:52:19
0001 /* 0002 SPDX-FileCopyrightText: 2023 Julius Künzel <jk.kdedev@smartlab.uber.space> 0003 SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL 0004 */ 0005 0006 #include "renderrequest.h" 0007 #include "bin/projectitemmodel.h" 0008 #include "core.h" 0009 #include "doc/kdenlivedoc.h" 0010 #include "kdenlivesettings.h" 0011 #include "project/projectmanager.h" 0012 #include "renderpresets/renderpresetrepository.hpp" 0013 #include "utils/qstringutils.h" 0014 #include "xml/xml.hpp" 0015 0016 #include "utils/KMessageBox_KdenliveCompat.h" 0017 0018 #include <QTemporaryFile> 0019 0020 // TODO: remove, see generatePlaylistFile() 0021 #include <KMessageBox> 0022 #include <QInputDialog> 0023 0024 // TODO: 0025 #include "doc/docundostack.hpp" 0026 #include <QUndoGroup> 0027 0028 QStringList RenderRequest::argsByJob(const RenderJob &job) 0029 { 0030 QStringList args = {QStringLiteral("delivery"), KdenliveSettings::meltpath(), job.playlistPath, QStringLiteral("--pid"), 0031 QString::number(QCoreApplication::applicationPid())}; 0032 if (!job.subtitlePath.isEmpty()) { 0033 args << QStringLiteral("--subtitle") << job.subtitlePath; 0034 } 0035 return args; 0036 } 0037 0038 RenderRequest::RenderRequest() 0039 { 0040 setBounds(-1, -1); 0041 } 0042 0043 void RenderRequest::setBounds(int in, int out) 0044 { 0045 m_boundingIn = qMax(0, in); 0046 if (out < 0 || out > pCore->projectDuration() - 1) { 0047 // Remove last black frame 0048 out = pCore->projectDuration() - 1; 0049 } 0050 m_boundingOut = out; 0051 } 0052 0053 void RenderRequest::setOutputFile(const QString &filename) 0054 { 0055 m_outputFile = filename; 0056 } 0057 0058 void RenderRequest::setPresetParams(const RenderPresetParams ¶ms) 0059 { 0060 m_presetParams = params; 0061 } 0062 0063 void RenderRequest::loadPresetParams(const QString &profileName) 0064 { 0065 std::unique_ptr<RenderPresetModel> &profile = RenderPresetRepository::get()->getPreset(profileName); 0066 m_presetParams = profile->params(); 0067 m_presetParams.refreshX265Params(); 0068 QStringList presetDefaults = profile->defaultValues(); 0069 0070 // Replace placeholders by default values 0071 m_presetParams.replacePlaceholder(QLatin1String("%quality"), presetDefaults.at(4)); 0072 m_presetParams.replacePlaceholder(QLatin1String("%audioquality"), presetDefaults.at(2)); 0073 m_presetParams.replacePlaceholder(QLatin1String("%audiobitrate+'k'"), QStringLiteral("%1k").arg(presetDefaults.at(1))); 0074 m_presetParams.replacePlaceholder(QLatin1String("%audiobitrate"), presetDefaults.at(1)); 0075 m_presetParams.replacePlaceholder(QLatin1String("%bitrate+'k'"), QStringLiteral("%1k").arg(presetDefaults.at(3))); 0076 m_presetParams.replacePlaceholder(QLatin1String("%bitrate"), presetDefaults.at(3)); 0077 0078 // Insert parameters of default speed 0079 if (!presetDefaults.first().isEmpty() && presetDefaults.first().contains(QLatin1Char('='))) { 0080 m_presetParams.insertFromString(presetDefaults.first(), false); 0081 } 0082 } 0083 0084 void RenderRequest::setDelayedRendering(bool enabled) 0085 { 0086 m_delayedRendering = enabled; 0087 } 0088 0089 void RenderRequest::setProxyRendering(bool enabled) 0090 { 0091 m_proxyRendering = enabled; 0092 } 0093 0094 void RenderRequest::setEmbedSubtitles(bool enabled) 0095 { 0096 m_embedSubtitles = enabled; 0097 } 0098 0099 void RenderRequest::setTwoPass(bool enabled) 0100 { 0101 m_twoPass = enabled; 0102 } 0103 0104 void RenderRequest::setAudioFilePerTrack(bool enabled) 0105 { 0106 m_audioFilePerTrack = enabled; 0107 } 0108 0109 void RenderRequest::setGuideParams(std::weak_ptr<MarkerListModel> model, bool enableMultiExport, int filterCategory) 0110 { 0111 m_guidesModel = std::move(model); 0112 m_guideMultiExport = enableMultiExport; 0113 m_guideCategory = filterCategory; 0114 } 0115 0116 void RenderRequest::setOverlayData(const QString &data) 0117 { 0118 m_overlayData = data; 0119 } 0120 0121 std::vector<RenderRequest::RenderJob> RenderRequest::process() 0122 { 0123 m_errors.clear(); 0124 0125 QString playlistPath = generatePlaylistFile(); 0126 if (playlistPath.isEmpty()) { 0127 return {}; 0128 } 0129 0130 KdenliveDoc *project = pCore->currentDoc(); 0131 0132 // On delayed rendering, make a copy of all assets 0133 if (m_delayedRendering) { 0134 QDir dir = QFileInfo(playlistPath).absoluteDir(); 0135 if (!dir.mkpath(QFileInfo(playlistPath).baseName())) { 0136 addErrorMessage(i18n("Could not create assets folder:\n %1", dir.absoluteFilePath(QFileInfo(playlistPath).baseName()))); 0137 return {}; 0138 } 0139 dir.cd(QFileInfo(playlistPath).baseName()); 0140 project->prepareRenderAssets(dir); 0141 } 0142 0143 QString playlistContent = 0144 pCore->projectManager()->projectSceneList(project->url().adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).toLocalFile(), m_overlayData); 0145 0146 QDomDocument doc; 0147 doc.setContent(playlistContent); 0148 0149 if (m_delayedRendering) { 0150 project->restoreRenderAssets(); 0151 } 0152 0153 // Add autoclose to playlists 0154 KdenliveDoc::setAutoclosePlaylists(doc, pCore->currentTimelineId().toString()); 0155 0156 // Do we want proxy rendering 0157 if (!m_proxyRendering && project->useProxy()) { 0158 KdenliveDoc::useOriginals(doc); 0159 } 0160 0161 if (m_embedSubtitles && project->hasSubtitles()) { 0162 // disable subtitle filter(s) as they will be embedded in a second step of rendering 0163 KdenliveDoc::disableSubtitles(doc); 0164 } 0165 0166 // If we use a pix_fmt with alpha channel (ie. transparent), 0167 // we need to remove the black background track 0168 if (m_presetParams.hasAlpha()) { 0169 KdenliveDoc::makeBackgroundTrackTransparent(doc); 0170 } 0171 0172 std::vector<RenderSection> sections; 0173 0174 if (m_guideMultiExport) { 0175 sections = getGuideSections(); 0176 } 0177 0178 if (sections.empty()) { 0179 RenderSection section; 0180 section.in = m_boundingIn; 0181 section.out = m_boundingOut; 0182 sections.push_back(section); 0183 } 0184 0185 const QUuid currentUuid = pCore->currentTimelineId(); 0186 0187 int i = 0; 0188 0189 std::vector<RenderJob> jobs; 0190 for (const auto §ion : sections) { 0191 i++; 0192 QString outputPath = m_outputFile; 0193 if (!section.name.isEmpty()) { 0194 outputPath = QStringUtils::appendToFilename(outputPath, QStringLiteral("-%1").arg(section.name)); 0195 } 0196 QDomDocument sectionDoc = doc.cloneNode(true).toDocument(); 0197 0198 QString subtitleFile; 0199 if (m_embedSubtitles) { 0200 subtitleFile = createEmptyTempFile(QStringLiteral("srt")); 0201 if (subtitleFile.isEmpty()) { 0202 addErrorMessage(i18n("Could not create temporary subtitle file")); 0203 return {}; 0204 } 0205 project->generateRenderSubtitleFile(currentUuid, section.in, section.out, subtitleFile); 0206 } 0207 0208 QString newPlaylistPath = playlistPath; 0209 newPlaylistPath = newPlaylistPath.replace(QStringLiteral(".mlt"), QString("-%1.mlt").arg(i)); 0210 // QString newPlaylistPath = createEmptyTempFile(QStringLiteral("mlt")); // !!! This does not take the delayed rendering logic of generatePlaylistFile() 0211 // in to account 0212 0213 // QFile::copy(job.playlistPath, newJob.playlistPath); // TODO: if we have a single item in sections this is unnesessary and produces just unused files 0214 0215 // set parameters 0216 setDocGeneralParams(sectionDoc, section.in, section.out); 0217 0218 createRenderJobs(jobs, sectionDoc, newPlaylistPath, outputPath, subtitleFile, currentUuid); 0219 } 0220 0221 return jobs; 0222 } 0223 0224 void RenderRequest::createRenderJobs(std::vector<RenderJob> &jobs, const QDomDocument &doc, const QString &playlistPath, QString outputPath, 0225 const QString &subtitlePath, const QUuid &uuid) 0226 { 0227 if (m_audioFilePerTrack) { 0228 if (m_delayedRendering) { 0229 addErrorMessage(i18n("Script rendering and multi track audio export can not be used together. Script will be saved without multi track export.")); 0230 } else { 0231 prepareMultiAudioFiles(jobs, doc, playlistPath, outputPath, uuid); 0232 } 0233 } 0234 0235 if (m_presetParams.isImageSequence()) { 0236 // Image sequence, ensure we have a %0xd (format string for counter) at output filename end 0237 static const QRegularExpression rx(QRegularExpression::anchoredPattern(QStringLiteral(".*%[0-9]*d.*"))); 0238 if (!rx.match(outputPath).hasMatch()) { 0239 outputPath = QStringUtils::appendToFilename(outputPath, QStringLiteral("_%05d")); 0240 } 0241 } 0242 0243 int passes = m_twoPass ? 2 : 1; 0244 0245 for (int i = 0; i < passes; i++) { 0246 // clone the dom if this is not the first iteration (happens with two pass) 0247 QDomDocument final = i > 0 ? doc.cloneNode(true).toDocument() : doc; 0248 0249 int pass = m_twoPass ? i + 1 : 0; 0250 RenderJob job; 0251 job.playlistPath = playlistPath; 0252 job.outputPath = outputPath; 0253 job.subtitlePath = subtitlePath; 0254 if (pass == 2) { 0255 job.playlistPath = QStringUtils::appendToFilename(job.playlistPath, QStringLiteral("-pass%1").arg(2)); 0256 } 0257 jobs.push_back(job); 0258 0259 // get the consumer element 0260 QDomNodeList consumers = final.elementsByTagName(QStringLiteral("consumer")); 0261 QDomElement consumer = consumers.at(0).toElement(); 0262 0263 consumer.setAttribute(QStringLiteral("target"), job.outputPath); 0264 0265 // Set two pass parameters. In case pass is 0 the function does nothing. 0266 setDocTwoPassParams(pass, final, job.outputPath); 0267 0268 if (!Xml::docContentToFile(final, job.playlistPath)) { 0269 addErrorMessage(i18n("Cannot write to file %1", job.playlistPath)); 0270 return; 0271 } 0272 } 0273 } 0274 0275 QString RenderRequest::createEmptyTempFile(const QString &extension) 0276 { 0277 QTemporaryFile tmp(QDir::temp().absoluteFilePath(QString("kdenlive-XXXXXX.%1").arg(extension))); 0278 if (!tmp.open()) { 0279 // Something went wrong 0280 qDebug() << "Could not create temporary file"; 0281 // TODO: some kind of warning to the UI? 0282 return {}; 0283 } 0284 tmp.setAutoRemove(false); 0285 tmp.close(); 0286 0287 return tmp.fileName(); 0288 } 0289 0290 QString RenderRequest::generatePlaylistFile() 0291 { 0292 if (!m_delayedRendering) { 0293 // No delayed rendering, we can use a temp file 0294 return createEmptyTempFile(QStringLiteral("mlt")); 0295 } 0296 0297 // TODO: this is the only code part of this class that still uses UI components 0298 bool ok; 0299 QString filename = QFileInfo(pCore->currentDoc()->url().toLocalFile()).fileName(); 0300 const QString fileExtension = QStringLiteral(".mlt"); 0301 if (filename.isEmpty()) { 0302 filename = i18n("export"); 0303 } else { 0304 filename = filename.section(QLatin1Char('.'), 0, -2); 0305 } 0306 0307 QDir projectFolder(pCore->currentDoc()->projectDataFolder()); 0308 projectFolder.mkpath(QStringLiteral("kdenlive-renderqueue")); 0309 projectFolder.cd(QStringLiteral("kdenlive-renderqueue")); 0310 int ix = 1; 0311 QString newFilename = filename; 0312 // if name already exist, add a suffix 0313 while (projectFolder.exists(newFilename + fileExtension)) { 0314 newFilename = QStringLiteral("%1-%2").arg(filename).arg(ix); 0315 ix++; 0316 } 0317 filename = QInputDialog::getText(nullptr, i18nc("@title:window", "Delayed Rendering"), i18n("Select a name for this rendering."), QLineEdit::Normal, 0318 newFilename, &ok); 0319 if (!ok) { 0320 return {}; 0321 } 0322 if (!filename.endsWith(fileExtension)) { 0323 filename.append(fileExtension); 0324 } 0325 if (projectFolder.exists(newFilename)) { 0326 if (KMessageBox::questionTwoActions(nullptr, i18n("File %1 already exists.\nDo you want to overwrite it?", filename), {}, KStandardGuiItem::overwrite(), 0327 KStandardGuiItem::cancel()) == KMessageBox::PrimaryAction) { 0328 return {}; 0329 } 0330 } 0331 return projectFolder.absoluteFilePath(filename); 0332 } 0333 0334 void RenderRequest::setDocGeneralParams(QDomDocument doc, int in, int out) 0335 { 0336 QDomElement consumer = doc.createElement(QStringLiteral("consumer")); 0337 consumer.setAttribute(QStringLiteral("in"), in); 0338 consumer.setAttribute(QStringLiteral("out"), out); 0339 consumer.setAttribute(QStringLiteral("mlt_service"), QStringLiteral("avformat")); 0340 consumer.setAttribute(QStringLiteral("rescale"), KdenliveSettings::renderInterp().toUtf8().constData()); 0341 consumer.setAttribute(QStringLiteral("deinterlacer"), KdenliveSettings::renderDeinterlacer().toUtf8().constData()); 0342 0343 QMapIterator<QString, QString> it(m_presetParams); 0344 0345 while (it.hasNext()) { 0346 it.next(); 0347 // insert params from preset 0348 consumer.setAttribute(it.key(), it.value()); 0349 } 0350 0351 // Insert consumer to document, after the profiles (if they exist) 0352 QDomNodeList profiles = doc.elementsByTagName(QStringLiteral("profile")); 0353 if (profiles.isEmpty()) { 0354 doc.documentElement().insertAfter(consumer, doc.documentElement()); 0355 } else { 0356 doc.documentElement().insertAfter(consumer, profiles.at(profiles.length() - 1)); 0357 } 0358 } 0359 0360 void RenderRequest::setDocTwoPassParams(int pass, QDomDocument &doc, const QString &outputFile) 0361 { 0362 if (pass != 1 && pass != 2) { 0363 return; 0364 } 0365 0366 QDomNodeList consumers = doc.elementsByTagName(QStringLiteral("consumer")); 0367 QDomElement consumer = consumers.at(0).toElement(); 0368 0369 QString logFile = QStringLiteral("%1_2pass.log").arg(outputFile); 0370 0371 if (m_presetParams.isX265()) { 0372 // The x265 codec is special 0373 QString x265params = consumer.attribute(QStringLiteral("x265-params")); 0374 x265params = QString("pass=%1:stats=%2:%3").arg(pass).arg(logFile.replace(":", "\\:"), x265params); 0375 consumer.setAttribute(QStringLiteral("x265-params"), x265params); 0376 } else { 0377 consumer.setAttribute(QStringLiteral("pass"), pass); 0378 consumer.setAttribute(QStringLiteral("passlogfile"), logFile); 0379 0380 if (pass == 1) { 0381 consumer.setAttribute(QStringLiteral("fastfirstpass"), 1); 0382 consumer.setAttribute(QStringLiteral("an"), 1); 0383 0384 consumer.removeAttribute(QStringLiteral("acodec")); 0385 } else { // pass == 2 0386 consumer.removeAttribute(QStringLiteral("fastfirstpass")); 0387 } 0388 } 0389 } 0390 0391 std::vector<RenderRequest::RenderSection> RenderRequest::getGuideSections() 0392 { 0393 std::vector<RenderSection> sections; 0394 if (auto ptr = m_guidesModel.lock()) { 0395 QList<CommentedTime> markers; 0396 double fps = pCore->getCurrentFps(); 0397 0398 // keep only markers that are within our bounds 0399 for (const auto &marker : ptr->getAllMarkers(m_guideCategory)) { 0400 0401 int pos = marker.time().frames(fps); 0402 if (pos < m_boundingIn || (m_boundingOut > 0 && pos > m_boundingOut)) continue; 0403 markers << marker; 0404 } 0405 0406 // if there are markers left, create sections based on them 0407 if (!markers.isEmpty()) { 0408 bool beginParsed = false; 0409 QStringList names; 0410 for (int i = 0; i < markers.count(); i++) { 0411 RenderSection section; 0412 if (!beginParsed && i == 0 && markers.at(i).time().frames(fps) != m_boundingIn) { 0413 i -= 1; 0414 beginParsed = true; 0415 section.name = i18n("begin"); 0416 } 0417 0418 // in point and name of section 0419 section.in = m_boundingIn; 0420 if (i >= 0) { 0421 section.name = markers.at(i).comment(); 0422 section.in = markers.at(i).time().frames(fps); 0423 } 0424 section.name.replace(QLatin1Char('/'), QLatin1Char('_')); 0425 section.name.replace(QLatin1Char('\\'), QLatin1Char('_')); 0426 section.name = QStringUtils::getUniqueName(names, section.name); 0427 names << section.name; 0428 0429 // out point of section 0430 section.out = m_boundingOut; 0431 if (i + 1 < markers.count()) { 0432 section.out = qMin(markers.at(i + 1).time().frames(fps) - 1, m_boundingOut); 0433 } 0434 0435 sections.push_back(section); 0436 } 0437 } 0438 } 0439 return sections; 0440 } 0441 0442 void RenderRequest::prepareMultiAudioFiles(std::vector<RenderJob> &jobs, const QDomDocument &doc, const QString &playlistFile, const QString &targetFile, 0443 const QUuid &uuid) 0444 { 0445 int audioCount = 0; 0446 QDomNodeList orginalTractors = doc.elementsByTagName(QStringLiteral("tractor")); 0447 // process in reversed order to make file naming fit to UI 0448 QStringList trackIds; 0449 for (int i = orginalTractors.size(); i >= 0; i--) { 0450 auto originalTracktor = orginalTractors.at(i).toElement(); 0451 const QUuid tractorUuid(Xml::getXmlProperty(originalTracktor, QStringLiteral("kdenlive:uuid"))); 0452 if (tractorUuid == uuid) { 0453 // We found the current timeline tractor, list its tracks 0454 QDomNodeList childTracks = originalTracktor.elementsByTagName(QStringLiteral("track")); 0455 for (int j = childTracks.size(); j >= 0; j--) { 0456 trackIds << childTracks.at(j).toElement().attribute(QStringLiteral("producer")); 0457 } 0458 break; 0459 } 0460 } 0461 0462 for (int i = orginalTractors.size(); i >= 0; i--) { 0463 // Create a render job for each track, muting others 0464 auto originalTracktor = orginalTractors.at(i).toElement(); 0465 const QString processedTrackId = originalTracktor.attribute(QStringLiteral("id")); 0466 if (!trackIds.contains(processedTrackId)) { 0467 continue; 0468 } 0469 QString trackName = Xml::getXmlProperty(originalTracktor, QStringLiteral("kdenlive:track_name")); 0470 bool originalIsAudio = Xml::getXmlProperty(originalTracktor, QStringLiteral("kdenlive:audio_track")).toInt() == 1; 0471 if (!originalIsAudio) { 0472 // Not an audio track, nothing to do 0473 continue; 0474 } 0475 0476 // setup filenames 0477 QString appendix = QString("_Audio_%1%2%3") 0478 .arg(audioCount + 1) 0479 .arg(trackName.isEmpty() ? QString() : QStringLiteral("-")) 0480 .arg(trackName.replace(QStringLiteral(" "), QStringLiteral("_"))); 0481 RenderJob job; 0482 job.playlistPath = QStringUtils::appendToFilename(playlistFile, appendix); 0483 job.outputPath = QStringUtils::appendToFilename(targetFile, appendix); 0484 jobs.push_back(job); 0485 0486 // init doc copy 0487 QDomDocument docCopy = doc.cloneNode(true).toDocument(); 0488 QDomElement consumer = docCopy.elementsByTagName(QStringLiteral("consumer")).at(0).toElement(); 0489 consumer.setAttribute(QStringLiteral("target"), job.outputPath); 0490 0491 QDomNodeList tracktors = docCopy.elementsByTagName(QStringLiteral("tractor")); 0492 Q_ASSERT(tracktors.size() == orginalTractors.size()); 0493 QDomNodeList mainTracks = docCopy.elementsByTagName(QStringLiteral("tractor")); 0494 for (int j = 0; j < tracktors.size(); j++) { 0495 auto tractor = tracktors.at(j).toElement(); 0496 const QString currentId = tractor.attribute(QStringLiteral("id")); 0497 if (!trackIds.contains(currentId)) { 0498 // Not a track in current timeline 0499 continue; 0500 } 0501 if (processedTrackId == currentId) { 0502 // This is the track we want 0503 continue; 0504 } 0505 bool copyIsAudio = Xml::getXmlProperty(tractor, QStringLiteral("kdenlive:audio_track")).toInt() == 1; 0506 if (!copyIsAudio) { 0507 // Not an audio track, leave as is 0508 continue; 0509 } 0510 QDomNodeList tracks = tractor.elementsByTagName(QStringLiteral("track")); 0511 for (int l = 0; l < tracks.size(); l++) { 0512 if (i != j) { 0513 tracks.at(l).toElement().setAttribute(QStringLiteral("hide"), QStringLiteral("both")); 0514 } 0515 } 0516 } 0517 Xml::docContentToFile(docCopy, job.playlistPath); 0518 audioCount++; 0519 } 0520 } 0521 0522 void RenderRequest::addErrorMessage(const QString &error) 0523 { 0524 m_errors.append(error); 0525 } 0526 0527 QStringList RenderRequest::errorMessages() 0528 { 0529 return m_errors; 0530 }