File indexing completed on 2024-04-21 04:50:52

0001 /*
0002     SPDX-FileCopyrightText: 2008 Jean-Baptiste Mardelle <jb@kdenlive.org>
0003 
0004     SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0005 */
0006 
0007 #include "../src/lib/localeHandling.h"
0008 #include "mlt++/Mlt.h"
0009 #include "renderjob.h"
0010 #include <../config-kdenlive.h>
0011 #include <QApplication>
0012 #include <QCommandLineParser>
0013 #include <QDebug>
0014 #include <QDir>
0015 #include <QDomDocument>
0016 #include <QTemporaryFile>
0017 #include <QtGlobal>
0018 
0019 int main(int argc, char **argv)
0020 {
0021     // kdenlive_render needs to be a full QApplication since some MLT modules
0022     // like kdenlive_title require a full App to launch a QGraphicsView.
0023     // If you need to render on a headless server, you must start rendering using a
0024     // virtual server, like xvfb-run...
0025     QApplication app(argc, argv);
0026     QCoreApplication::setApplicationName("kdenlive_render");
0027     QCoreApplication::setApplicationVersion(KDENLIVE_VERSION);
0028 
0029     QCommandLineParser parser;
0030     parser.setApplicationDescription("Kdenlive video renderer for MLT");
0031     parser.addHelpOption();
0032     parser.addVersionOption();
0033 
0034     parser.addPositionalArgument("mode", "Render mode. Either \"delivery\" or \"preview-chunks\".");
0035     parser.parse(QCoreApplication::arguments());
0036     QStringList args = parser.positionalArguments();
0037     const QString mode = args.isEmpty() ? QString() : args.first();
0038 
0039     if (mode == "preview-chunks") {
0040 
0041         parser.clearPositionalArguments();
0042         parser.addPositionalArgument("preview-chunks", "Mode: Render splited in to multiple files for timeline preview.");
0043         parser.addPositionalArgument("source", "Source file (usually MLT XML).");
0044         parser.addPositionalArgument("destination", "Destination directory.");
0045         parser.addPositionalArgument("chunks", "Chunks to render.");
0046         parser.addPositionalArgument("chunk_size", "Chunks to render.");
0047         parser.addPositionalArgument("profile_path", "Path to profile.");
0048         parser.addPositionalArgument("file_extension", "Rendered file extension.");
0049         parser.addPositionalArgument("args", "Space separated libavformat arguments.", "[arg1 arg2 ...]");
0050 
0051         parser.process(app);
0052         args = parser.positionalArguments();
0053         if (args.count() < 7) {
0054             qCritical() << "Error: not enough arguments specified\n";
0055             parser.showHelp(1);
0056             // the command above will quit the app with return 1;
0057         }
0058         // After initialising the MLT factory, set the locale back from user default to C
0059         // to ensure numbers are always serialised with . as decimal point.
0060         Mlt::Factory::init();
0061         LocaleHandling::resetAllLocale();
0062 
0063         // mode
0064         args.removeFirst();
0065         // Source playlist path
0066         QString playlist = args.takeFirst();
0067         // destination - where to save result
0068         QDir baseFolder(args.takeFirst());
0069         // chunks to render
0070         QStringList chunks = args.takeFirst().split(QLatin1Char(','), Qt::SkipEmptyParts);
0071         // chunk size in frames
0072         int chunkSize = args.takeFirst().toInt();
0073         // path to profile
0074         Mlt::Profile profile(args.takeFirst().toUtf8().constData());
0075         // rendered file extension
0076         QString extension = args.takeFirst();
0077         // avformat consumer params
0078         QStringList consumerParams = args.takeFirst().split(QLatin1Char(' '), Qt::SkipEmptyParts);
0079 
0080         profile.set_explicit(1);
0081         Mlt::Producer prod(profile, nullptr, playlist.toUtf8().constData());
0082         if (!prod.is_valid()) {
0083             fprintf(stderr, "INVALID playlist: %s \n", playlist.toUtf8().constData());
0084             return 1;
0085         }
0086         const char *localename = prod.get_lcnumeric();
0087         QLocale::setDefault(QLocale(localename));
0088 
0089         int currentFrame = 0;
0090         int rangeStart = 0;
0091         int rangeEnd = 0;
0092         QString frame;
0093         while (!chunks.isEmpty()) {
0094             if (rangeEnd == 0) {
0095                 // We are not processing a range
0096                 frame = chunks.first();
0097             }
0098             if (rangeEnd > 0) {
0099                 // We are processing a range
0100                 currentFrame += chunkSize + 1;
0101                 frame = QString::number(currentFrame);
0102                 if (currentFrame >= rangeEnd) {
0103                     // End of range
0104                     rangeStart = 0;
0105                     rangeEnd = 0;
0106                     // Range is processed, remove from stack
0107                     chunks.removeFirst();
0108                 }
0109             } else if (frame.contains(QLatin1Char('-'))) {
0110                 rangeStart = frame.section(QLatin1Char('-'), 0, 0).toInt();
0111                 rangeEnd = frame.section(QLatin1Char('-'), 1, 1).toInt();
0112                 currentFrame = rangeStart;
0113                 frame = QString::number(currentFrame);
0114             } else {
0115                 // Frame will be processed, remove from stack
0116                 chunks.removeFirst();
0117             }
0118             fprintf(stderr, "START:%d \n", frame.toInt());
0119             QString fileName = QStringLiteral("%1.%2").arg(frame, extension);
0120             if (baseFolder.exists(fileName)) {
0121                 // Don't overwrite an existing file
0122                 fprintf(stderr, "DONE:%d \n", frame.toInt());
0123                 continue;
0124             }
0125             QScopedPointer<Mlt::Producer> playlst(prod.cut(frame.toInt(), frame.toInt() + chunkSize));
0126             QScopedPointer<Mlt::Consumer> cons(
0127                 new Mlt::Consumer(profile, QString("avformat:%1").arg(baseFolder.absoluteFilePath(fileName)).toUtf8().constData()));
0128             for (const QString &param : qAsConst(consumerParams)) {
0129                 if (param.contains(QLatin1Char('='))) {
0130                     cons->set(param.section(QLatin1Char('='), 0, 0).toUtf8().constData(), param.section(QLatin1Char('='), 1).toUtf8().constData());
0131                 }
0132             }
0133             if (!cons->is_valid()) {
0134                 fprintf(stderr, " = =  = INVALID CONSUMER\n\n");
0135                 return 1;
0136             }
0137             cons->set("terminate_on_pause", 1);
0138             cons->connect(*playlst);
0139             playlst.reset();
0140             cons->run();
0141             cons->stop();
0142             cons->purge();
0143             fprintf(stderr, "DONE:%d \n", frame.toInt());
0144         }
0145         // Mlt::Factory::close();
0146         fprintf(stderr, "+ + + RENDERING FINISHED + + + \n");
0147         return 0;
0148     }
0149 
0150     if (mode == "delivery") {
0151         parser.clearPositionalArguments();
0152         parser.addPositionalArgument("delivery", "Mode: Render to a final output file.");
0153         parser.addPositionalArgument("renderer", "Path to MLT melt renderer.");
0154         parser.addPositionalArgument("source", "Source file (usually MLT XML).");
0155 
0156         QCommandLineOption outputOption({"o", "output"},
0157                                         "The destination file, optional. If no set the destination will be retrieved from the \"target\" property of the "
0158                                         "consumer in the source file. If set it overrides the consumers \"taget\" property.",
0159                                         "file");
0160         parser.addOption(outputOption);
0161 
0162         QCommandLineOption pidOption("pid", "Process ID to send back progress.", "pid", QString::number(-1));
0163         parser.addOption(pidOption);
0164 
0165         QCommandLineOption subtitleOption("subtitle", "Subtitle file.", "file");
0166         parser.addOption(subtitleOption);
0167 
0168         parser.process(app);
0169         args = parser.positionalArguments();
0170 
0171         if (args.count() != 3) {
0172             qCritical() << "Error: wrong number of arguments specified\n";
0173             parser.showHelp(1);
0174             // the command above will quit the app with return 1;
0175         }
0176 
0177         // mode
0178         args.removeFirst();
0179         // renderer path (melt)
0180         QString render = args.takeFirst();
0181         // Source playlist path
0182         QString playlist = args.takeFirst();
0183 
0184         LocaleHandling::resetAllLocale();
0185         QFile f(playlist);
0186         QDomDocument doc;
0187         if (!f.open(QIODevice::ReadOnly)) {
0188             qWarning() << "Failed to open file" << f.fileName() << "for reading";
0189             return 1;
0190         }
0191         if (!doc.setContent(&f, false)) {
0192             qWarning() << "Failed to parse file" << f.fileName() << "to QDomDocument";
0193             f.close();
0194             return 1;
0195         }
0196         f.close();
0197         QDomElement consumer = doc.documentElement().firstChildElement(QStringLiteral("consumer"));
0198         int in = -1;
0199         int out = -1;
0200         QString target;
0201         // get in and out point, we need them to calculate the progress in some cases
0202         if (!consumer.isNull()) {
0203             in = consumer.attribute(QStringLiteral("in"), QString::number(-1)).toInt();
0204             out = consumer.attribute(QStringLiteral("out"), QString::number(-1)).toInt();
0205             target = consumer.attribute(QStringLiteral("target"));
0206             QString output = parser.value(outputOption);
0207             if (!output.isEmpty()) {
0208                 // A custom output target was set.
0209                 // To apply it we store a copy of the source file with the modified target
0210                 // in a temporary file and use this file instead of the original source file.
0211                 consumer.setAttribute(QStringLiteral("target"), output);
0212                 QTemporaryFile tmp(QDir::temp().absoluteFilePath(QStringLiteral("kdenlive-XXXXXX.mlt")));
0213                 tmp.setAutoRemove(false);
0214                 if (tmp.open()) {
0215                     tmp.close();
0216                     QFile file(tmp.fileName());
0217                     if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
0218                         qDebug() << "Failed to set custom output destination, falling back to target set in source file: " << target;
0219                     } else {
0220                         playlist = tmp.fileName();
0221                         target = output;
0222                         QTextStream outStream(&file);
0223 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0224                         outStream.setCodec("UTF-8");
0225 #endif
0226                         outStream << doc.toString();
0227                     }
0228                     file.close();
0229                 } else {
0230                     qDebug() << "Failed to set custom output destination, falling back to target set in source file: " << target;
0231                 }
0232                 tmp.close();
0233             }
0234         }
0235         int pid = parser.value(pidOption).toInt();
0236         QString subtitleFile = parser.value(subtitleOption);
0237 
0238         auto *rJob = new RenderJob(render, playlist, target, pid, in, out, subtitleFile, &app);
0239         QObject::connect(rJob, &RenderJob::renderingFinished, rJob, [&]() {
0240             rJob->deleteLater();
0241             app.quit();
0242         });
0243         // app.setQuitOnLastWindowClosed(false);
0244         QMetaObject::invokeMethod(rJob, "start", Qt::QueuedConnection);
0245         return app.exec();
0246     }
0247 
0248     qCritical() << "Error: unknown mode" << mode << "\n";
0249     parser.showHelp(1);
0250     // the command above will quit the app with return 1;
0251 }