320 lines
13 KiB
C++
320 lines
13 KiB
C++
/*
|
|
* Copyright (c) 2012-2025 Meltytech, LLC
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include "encodejob.h"
|
|
|
|
#include "dialogs/listselectiondialog.h"
|
|
#include "docks/timelinedock.h"
|
|
#include "jobqueue.h"
|
|
#include "jobs/videoqualityjob.h"
|
|
#include "mainwindow.h"
|
|
#include "qmltypes/qmlapplication.h"
|
|
#include "qmltypes/qmlutilities.h"
|
|
#include "settings.h"
|
|
#include "spatialmedia/spatialmedia.h"
|
|
#include "util.h"
|
|
|
|
#include <QAction>
|
|
#include <QDesktopServices>
|
|
#include <QDir>
|
|
#include <QDomDocument>
|
|
#include <QFileDialog>
|
|
#include <QFileInfo>
|
|
#include <QJSEngine>
|
|
#include <QTemporaryFile>
|
|
#include <QTextStream>
|
|
#include <QUrl>
|
|
|
|
#include "Logger.h"
|
|
|
|
EncodeJob::EncodeJob(const QString &name,
|
|
const QString &xml,
|
|
int frameRateNum,
|
|
int frameRateDen,
|
|
QThread::Priority priority)
|
|
: MeltJob(name, xml, frameRateNum, frameRateDen, priority)
|
|
{
|
|
QAction *action = new QAction(tr("Open"), this);
|
|
action->setData("Open");
|
|
action->setToolTip(tr("Open the output file in the Shotcut player"));
|
|
connect(action, SIGNAL(triggered()), this, SLOT(onOpenTiggered()));
|
|
m_successActions << action;
|
|
|
|
action = new QAction(tr("Show In Files"), this);
|
|
action->setToolTip(tr("Show In Files"));
|
|
connect(action, SIGNAL(triggered()), this, SLOT(onShowInFilesTriggered()));
|
|
m_successActions << action;
|
|
|
|
action = new QAction(tr("Show In Folder"), this);
|
|
action->setToolTip(tr("Show In Folder"));
|
|
connect(action, SIGNAL(triggered()), this, SLOT(onShowFolderTriggered()));
|
|
m_successActions << action;
|
|
|
|
action = new QAction(tr("Measure Video Quality..."), this);
|
|
connect(action, SIGNAL(triggered()), this, SLOT(onVideoQualityTriggered()));
|
|
m_successActions << action;
|
|
|
|
action = new QAction(tr("Set Equirectangular..."), this);
|
|
connect(action, SIGNAL(triggered()), this, SLOT(onSpatialMediaTriggered()));
|
|
m_successActions << action;
|
|
|
|
action = new QAction(tr("Embed Markers as Chapters..."), this);
|
|
connect(action, SIGNAL(triggered()), this, SLOT(onEmbedChapters()));
|
|
m_successActions << action;
|
|
}
|
|
|
|
void EncodeJob::onVideoQualityTriggered()
|
|
{
|
|
// Get the location and file name for the report.
|
|
QString directory = Settings.encodePath();
|
|
QString caption = tr("Video Quality Report");
|
|
QString nameFilter = tr("Text Documents (*.txt);;All Files (*)");
|
|
QString reportPath = QFileDialog::getSaveFileName(&MAIN,
|
|
caption,
|
|
directory,
|
|
nameFilter,
|
|
nullptr,
|
|
Util::getFileDialogOptions());
|
|
if (!reportPath.isEmpty()) {
|
|
QFileInfo fi(reportPath);
|
|
if (fi.suffix().isEmpty())
|
|
reportPath += ".txt";
|
|
|
|
if (Util::warnIfNotWritable(reportPath, &MAIN, caption))
|
|
return;
|
|
|
|
// Get temp file for the new XML.
|
|
QScopedPointer<QTemporaryFile> tmp(Util::writableTemporaryFile(reportPath));
|
|
tmp->open();
|
|
|
|
// Generate the XML for the comparison.
|
|
Mlt::Tractor tractor(MLT.profile());
|
|
Mlt::Producer original(MLT.profile(), xmlPath().toUtf8().constData());
|
|
Mlt::Producer encoded(MLT.profile(), objectName().toUtf8().constData());
|
|
Mlt::Transition vqm(MLT.profile(), "vqm");
|
|
if (original.is_valid() && encoded.is_valid() && vqm.is_valid()) {
|
|
tractor.set_track(original, 0);
|
|
tractor.set_track(encoded, 1);
|
|
tractor.plant_transition(vqm);
|
|
vqm.set("render", 0);
|
|
MLT.saveXML(tmp->fileName(), &tractor, false /* without relative paths */, tmp.data());
|
|
tmp->close();
|
|
|
|
// Add consumer element to XML.
|
|
QFile f1(tmp->fileName());
|
|
f1.open(QIODevice::ReadOnly);
|
|
QDomDocument dom(tmp->fileName());
|
|
dom.setContent(&f1);
|
|
f1.close();
|
|
|
|
QDomElement consumerNode = dom.createElement("consumer");
|
|
QDomNodeList profiles = dom.elementsByTagName("profile");
|
|
if (profiles.isEmpty())
|
|
dom.documentElement().insertAfter(consumerNode, dom.documentElement());
|
|
else
|
|
dom.documentElement().insertAfter(consumerNode, profiles.at(profiles.length() - 1));
|
|
consumerNode.setAttribute("mlt_service", "null");
|
|
consumerNode.setAttribute("real_time", -1);
|
|
consumerNode.setAttribute("terminate_on_pause", 1);
|
|
|
|
// Create job and add it to the queue.
|
|
JOBS.add(new VideoQualityJob(objectName(),
|
|
dom.toString(2),
|
|
reportPath,
|
|
MLT.profile().frame_rate_num(),
|
|
MLT.profile().frame_rate_den()));
|
|
}
|
|
}
|
|
}
|
|
|
|
void EncodeJob::onSpatialMediaTriggered()
|
|
{
|
|
// Get the location and file name for the output file.
|
|
QString caption = tr("Set Equirectangular Projection");
|
|
QFileInfo info(objectName());
|
|
QString directory = QStringLiteral("%1/%2 - ERP.%3")
|
|
.arg(Settings.encodePath())
|
|
.arg(info.completeBaseName())
|
|
.arg(info.suffix());
|
|
QString filePath = QFileDialog::getSaveFileName(&MAIN,
|
|
caption,
|
|
directory,
|
|
QString(),
|
|
nullptr,
|
|
Util::getFileDialogOptions());
|
|
if (!filePath.isEmpty()) {
|
|
if (SpatialMedia::injectSpherical(objectName().toStdString(), filePath.toStdString())) {
|
|
MAIN.showStatusMessage(tr("Successfully wrote %1").arg(QFileInfo(filePath).fileName()));
|
|
} else {
|
|
MAIN.showStatusMessage(tr("An error occurred saving the projection."));
|
|
}
|
|
}
|
|
}
|
|
|
|
void EncodeJob::onEmbedChapters()
|
|
{
|
|
// Options dialog
|
|
auto uniqueColors = MAIN.timelineDock()->markersModel()->allColors();
|
|
if (uniqueColors.isEmpty()) {
|
|
return;
|
|
}
|
|
std::sort(uniqueColors.begin(), uniqueColors.end(), [=](const QColor &a, const QColor &b) {
|
|
if (a.hue() == b.hue()) {
|
|
if (a.saturation() == b.saturation()) {
|
|
return a.value() <= b.value();
|
|
}
|
|
return a.saturation() <= b.saturation();
|
|
}
|
|
return a.hue() <= b.hue();
|
|
});
|
|
QStringList colors;
|
|
for (auto &color : uniqueColors) {
|
|
colors << color.name();
|
|
}
|
|
const auto rangesOption = tr("Include ranges (Duration > 1 frame)?");
|
|
QStringList initialOptions;
|
|
for (auto &m : MAIN.timelineDock()->markersModel()->getMarkers()) {
|
|
if (m.end != m.start) {
|
|
initialOptions << rangesOption;
|
|
break;
|
|
}
|
|
}
|
|
|
|
ListSelectionDialog dialog(initialOptions, &MAIN);
|
|
dialog.setWindowModality(QmlApplication::dialogModality());
|
|
dialog.setWindowTitle(tr("Choose Markers"));
|
|
if (Settings.exportRangeMarkers()) {
|
|
dialog.setSelection({rangesOption});
|
|
}
|
|
dialog.setColors(colors);
|
|
if (dialog.exec() != QDialog::Accepted) {
|
|
return;
|
|
}
|
|
auto selection = dialog.selection();
|
|
Settings.setExportRangeMarkers(selection.contains(rangesOption));
|
|
|
|
// Get the location and file name for the output file.
|
|
QString caption = tr("Embed Chapters");
|
|
QFileInfo info(objectName());
|
|
QString directory = QStringLiteral("%1/%2 - Chapters.%3")
|
|
.arg(Settings.encodePath(), info.completeBaseName(), info.suffix());
|
|
QString filePath = QFileDialog::getSaveFileName(&MAIN,
|
|
caption,
|
|
directory,
|
|
QString(),
|
|
nullptr,
|
|
Util::getFileDialogOptions());
|
|
if (!filePath.isEmpty()) {
|
|
if (Util::warnIfNotWritable(filePath, &MAIN, caption))
|
|
return;
|
|
|
|
// Locate the JavaScript file in the filesystem.
|
|
QDir qmlDir = QmlUtilities::qmlDir();
|
|
qmlDir.cd("export-chapters");
|
|
auto jsFileName = qmlDir.absoluteFilePath("export-chapters.js");
|
|
QFile scriptFile(jsFileName);
|
|
if (scriptFile.open(QIODevice::ReadOnly)) {
|
|
// Read JavaScript into a string.
|
|
QTextStream stream(&scriptFile);
|
|
stream.setEncoding(QStringConverter::Utf8);
|
|
stream.setAutoDetectUnicode(true);
|
|
QString contents = stream.readAll();
|
|
scriptFile.close();
|
|
|
|
// Evaluate JavaScript.
|
|
QJSEngine jsEngine;
|
|
QJSValue result = jsEngine.evaluate(contents, jsFileName);
|
|
if (!result.isError()) {
|
|
// Call the JavaScript main function.
|
|
QJSValue options = jsEngine.newObject();
|
|
options.setProperty("ffmetadata", true);
|
|
if (selection.contains(rangesOption)) {
|
|
options.setProperty("includeRanges", true);
|
|
selection.removeOne(rangesOption);
|
|
}
|
|
QJSValue array = jsEngine.newArray(selection.size());
|
|
for (int i = 0; i < selection.size(); ++i)
|
|
array.setProperty(i, selection[i].toUpper());
|
|
options.setProperty("colors", array);
|
|
QJSValueList args;
|
|
args << MLT.XML(0, true, true) << options;
|
|
result = result.call(args);
|
|
if (!result.isError()) {
|
|
// Save the result with the export file name.
|
|
auto tempFile = Util::writableTemporaryFile(filePath, "shotcut-XXXXXX.txt");
|
|
tempFile->write(result.toString().toUtf8());
|
|
tempFile->close();
|
|
|
|
QStringList args;
|
|
args << "-i" << objectName() << "-i" << tempFile->fileName() << "-map_metadata"
|
|
<< "1"
|
|
<< "-c"
|
|
<< "copy"
|
|
<< "-y" << filePath;
|
|
auto job = new FfmpegJob(filePath, args, false);
|
|
job->setLabel(filePath);
|
|
tempFile->setParent(job);
|
|
JOBS.add(job);
|
|
}
|
|
} else {
|
|
LOG_ERROR() << "Uncaught exception at line" << result.property("lineNumber").toInt()
|
|
<< ":" << result.toString();
|
|
MAIN.showStatusMessage(tr("A JavaScript error occurred during export."));
|
|
}
|
|
} else {
|
|
MAIN.showStatusMessage(tr("Failed to open export-chapters.js"));
|
|
}
|
|
}
|
|
}
|
|
|
|
void EncodeJob::onFinished(int exitCode, QProcess::ExitStatus exitStatus)
|
|
{
|
|
if (exitStatus != QProcess::NormalExit && exitCode != 0 && !stopped()) {
|
|
LOG_INFO() << "job failed with" << exitCode;
|
|
appendToLog(QStringLiteral("Failed with exit code %1\n").arg(exitCode));
|
|
bool isParallel = false;
|
|
// Parse the XML.
|
|
m_xml->open();
|
|
QDomDocument dom(xmlPath());
|
|
dom.setContent(m_xml.data());
|
|
m_xml->close();
|
|
|
|
// Locate the consumer element.
|
|
QDomNodeList consumers = dom.elementsByTagName("consumer");
|
|
for (int i = 0; i < consumers.length(); i++) {
|
|
QDomElement consumer = consumers.at(i).toElement();
|
|
// If real_time is set for parallel.
|
|
if (consumer.attribute("real_time").toInt() < -1) {
|
|
isParallel = true;
|
|
consumer.setAttribute("real_time", "-1");
|
|
}
|
|
}
|
|
if (isParallel) {
|
|
QString message(tr("Export job failed; trying again without Parallel processing."));
|
|
MAIN.showStatusMessage(message);
|
|
appendToLog(message.append("\n"));
|
|
m_xml->open();
|
|
QTextStream textStream(m_xml.data());
|
|
dom.save(textStream, 2);
|
|
m_xml->close();
|
|
MeltJob::start();
|
|
return;
|
|
}
|
|
}
|
|
MeltJob::onFinished(exitCode, exitStatus);
|
|
}
|