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 &params)
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 &section : 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 }