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 ¶m : 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 }