Commit 34b1d26d authored by Jean-Baptiste Mardelle's avatar Jean-Baptiste Mardelle
Browse files

Add option to embed subtitles instead of burning them (mkv only)

parent d6de3ea9
......@@ -9,6 +9,11 @@
audiobitrates="256,64" defaultaudiobitrate="160"
args="f=mp4 movflags=+faststart vcodec=libx264 crf=%quality g=15 acodec=aac ab=%audiobitrate+'k'"
defaultspeedindex="6" speeds="preset=veryslow;preset=slower;preset=slow;preset=medium;preset=fast;preset=faster;preset=veryfast;preset=superfast;preset=ultrafast"/>
<profile name="Matroska-H264/AAC" extension="mkv"
qualities="15,45" defaultquality="23"
audiobitrates="256,64" defaultaudiobitrate="160"
args="f=matroska movflags=+faststart vcodec=libx264 crf=%quality g=15 acodec=aac ab=%audiobitrate+'k'"
defaultspeedindex="6" speeds="preset=veryslow;preset=slower;preset=slow;preset=medium;preset=fast;preset=faster;preset=veryfast;preset=superfast;preset=ultrafast"/>
<profile name="MPEG-2" extension="mpg"
qualities="3,15" defaultquality="5"
audioqualities="3,7" defaultaudioquality="3"
......
......@@ -16,6 +16,7 @@ int main(int argc, char **argv)
QApplication app(argc, argv);
QStringList args = app.arguments();
QStringList preargs;
QString subtitleFile;
if (args.count() >= 4) {
// Remove program name
args.removeFirst();
......@@ -137,6 +138,9 @@ int main(int argc, char **argv)
// Mlt::Factory::close();
fprintf(stderr, "+ + + RENDERING FINISHED + + + \n");
return 0;
} else if (args.count() > 1 && args.at(0) == QLatin1String("-subtitle")) {
args.removeFirst();
subtitleFile = args.takeFirst();
}
// older MLT version, does not support embedded consumer in/out in xml, and current
......@@ -154,7 +158,7 @@ int main(int argc, char **argv)
playlist.append(QStringLiteral("?multi=1"));
}
}
auto *rJob = new RenderJob(render, playlist, target, pid, in, out, qApp);
auto *rJob = new RenderJob(render, playlist, target, pid, in, out, subtitleFile, qApp);
rJob->start();
QObject::connect(rJob, &RenderJob::renderingFinished, rJob, [&]() {
rJob->deleteLater();
......
......@@ -26,7 +26,8 @@ public:
static void msleep(unsigned long msecs) { QThread::msleep(msecs); }
};
RenderJob::RenderJob(const QString &render, const QString &scenelist, const QString &target, int pid, int in, int out, QObject *parent)
RenderJob::RenderJob(const QString &render, const QString &scenelist, const QString &target, int pid, int in, int out, const QString &subtitleFile,
QObject *parent)
: QObject(parent)
, m_scenelist(scenelist)
, m_dest(target)
......@@ -48,6 +49,7 @@ RenderJob::RenderJob(const QString &render, const QString &scenelist, const QStr
, m_frameout(out)
, m_pid(pid)
, m_dualpass(false)
, m_subtitleFile(subtitleFile)
{
m_renderProcess = new QProcess;
m_renderProcess->setReadChannel(QProcess::StandardError);
......@@ -157,38 +159,44 @@ void RenderJob::receivedStderr()
}
int speed = (frame - m_frame) / (elapsedTime - m_seconds);
m_seconds = elapsedTime;
m_frame = frame;
updateProgress(speed);
}
}
void RenderJob::updateProgress(int speed)
{
#ifndef NODBUS
if ((m_kdenliveinterface != nullptr) && m_kdenliveinterface->isValid()) {
m_kdenliveinterface->callWithArgumentList(QDBus::NoBlock, QStringLiteral("setRenderingProgress"), {m_dest, m_progress, frame});
if ((m_kdenliveinterface != nullptr) && m_kdenliveinterface->isValid()) {
m_kdenliveinterface->callWithArgumentList(QDBus::NoBlock, QStringLiteral("setRenderingProgress"), {m_dest, m_progress, m_frame});
}
if (m_jobUiserver) {
qint64 remaining = m_seconds * (100 - m_progress) / m_progress;
int days = int(remaining / 86400);
int remainingSecs = int(remaining % 86400);
QTime when = QTime(0, 0, 0, 0).addSecs(remainingSecs);
QString est = tr("Remaining time ");
if (days > 0) {
est.append(tr("%n day(s) ", "", days));
}
if (m_jobUiserver) {
qint64 remaining = elapsedTime * (100 - progress) / progress;
int days = int(remaining / 86400);
int remainingSecs = int(remaining % 86400);
QTime when = QTime(0, 0, 0, 0).addSecs(remainingSecs);
QString est = tr("Remaining time ");
if (days > 0) {
est.append(tr("%n day(s) ", "", days));
}
est.append(when.toString(QStringLiteral("hh:mm:ss")));
m_jobUiserver->call(QStringLiteral("setPercent"), uint(m_progress));
m_jobUiserver->call(QStringLiteral("setProcessedAmount"), qulonglong(frame - m_framein), tr("frames"));
est.append(when.toString(QStringLiteral("hh:mm:ss")));
m_jobUiserver->call(QStringLiteral("setPercent"), uint(m_progress));
m_jobUiserver->call(QStringLiteral("setProcessedAmount"), qulonglong(m_frame - m_framein), tr("frames"));
if (speed > -1) {
m_jobUiserver->call(QStringLiteral("setSpeed"), qulonglong(speed));
m_jobUiserver->call(QStringLiteral("setDescriptionField"), 0, QString(), est);
}
m_jobUiserver->call(QStringLiteral("setDescriptionField"), 0, QString(), est);
}
#else
QJsonObject method, args;
args["url"] = m_dest;
args["progress"] = m_progress;
args["frame"] = frame;
method["setRenderingProgress"] = args;
m_kdenlivesocket->write(QJsonDocument(method).toJson());
m_kdenlivesocket->flush();
QJsonObject method, args;
args["url"] = m_dest;
args["progress"] = m_progress;
args["frame"] = m_frame;
method["setRenderingProgress"] = args;
m_kdenlivesocket->write(QJsonDocument(method).toJson());
m_kdenlivesocket->flush();
#endif
m_frame = frame;
m_logstream << QStringLiteral("%1\t%2\t%3\n").arg(m_seconds).arg(m_frame).arg(m_progress);
}
m_logstream << QStringLiteral("%1\t%2\t%3\n").arg(m_seconds).arg(m_frame).arg(m_progress);
}
void RenderJob::start()
......@@ -354,7 +362,76 @@ void RenderJob::slotIsOver(QProcess::ExitStatus status, bool isWritable)
deleteLater();
} else {
m_logfile.remove();
if (!m_subtitleFile.isEmpty()) {
// Embed subtitles
QString ffmpegExe = QStandardPaths::findExecutable(QStringLiteral("ffmpeg"));
if (!ffmpegExe.isEmpty()) {
QFileInfo videoRender(m_dest);
m_temporaryRenderFile = QDir::temp().absoluteFilePath(videoRender.fileName());
QStringList args = {
"-y", "-v", "quiet", "-stats", "-i", m_dest, "-i", m_subtitleFile, "-c", "copy", "-f", "matroska", m_temporaryRenderFile};
qDebug() << "::: JOB ARGS: " << args;
m_progress = 0;
delete m_renderProcess;
m_renderProcess = new QProcess;
/*disconnect(m_renderProcess, &QProcess::stateChanged, this, &RenderJob::slotCheckProcess);
disconnect(m_renderProcess, &QProcess::readyReadStandardError, this, &RenderJob::receivedStderr);*/
connect(m_renderProcess, &QProcess::readyReadStandardError, this, &RenderJob::receivedSubtitleProgress);
connect(m_renderProcess, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished), this, &RenderJob::slotCheckSubtitleProcess);
m_renderProcess->start(ffmpegExe, args);
m_renderProcess->waitForFinished();
return;
}
}
}
}
emit renderingFinished();
}
void RenderJob::receivedSubtitleProgress()
{
QString outputData = QString::fromLocal8Bit(m_renderProcess->readAllStandardError()).simplified();
if (outputData.isEmpty()) {
return;
}
QStringList output = outputData.split(QLatin1Char(' '));
m_errorMessage.append(outputData + QStringLiteral("<br>"));
QString result = output.takeFirst();
bool ok = false;
int frame = -1;
if (result == (QLatin1String("frame="))) {
// Frame number is the second parameter
result = output.takeFirst();
frame = result.toInt(&ok);
} else if (result.startsWith(QLatin1String("frame="))) {
frame = result.section(QLatin1Char('='), 1).toInt(&ok);
}
if (ok && frame > 0) {
m_frame = frame;
m_progress = 100 * frame / (m_frameout - m_framein);
updateProgress();
}
}
void RenderJob::slotCheckSubtitleProcess(int exitCode, QProcess::ExitStatus exitStatus)
{
if (exitStatus == QProcess::CrashExit || !QFile::exists(m_temporaryRenderFile)) {
// rendering crashed
qDebug() << ":::: FOUND ERROR IN SUBS: " << m_renderProcess->exitStatus() << " / " << exitCode
<< ", FILE ESISTS: " << QFile::exists(m_temporaryRenderFile);
QString error = tr("Rendering of %1 aborted when adding subtitles.").arg(m_dest);
m_errorMessage.append(error);
sendFinish(-2, m_errorMessage);
QStringList args;
if (m_frame > 0) {
error += QLatin1Char('\n') + tr("Frame: %1").arg(m_frame);
}
args << QStringLiteral("--error") << error;
m_logstream << error << "\n";
QProcess::startDetached(QStringLiteral("kdialog"), args);
} else {
QFile::remove(m_dest);
QFile::rename(m_temporaryRenderFile, m_dest);
}
emit renderingFinished();
}
......@@ -23,7 +23,8 @@ class RenderJob : public QObject
Q_OBJECT
public:
RenderJob(const QString &render, const QString &scenelist, const QString &target, int pid = -1, int in = -1, int out = -1, QObject *parent = nullptr);
RenderJob(const QString &render, const QString &scenelist, const QString &target, int pid = -1, int in = -1, int out = -1,
const QString &subtitleFile = QString(), QObject *parent = nullptr);
~RenderJob() override;
public slots:
......@@ -35,6 +36,8 @@ private slots:
void slotAbort();
void slotAbort(const QString &url);
void slotCheckProcess(QProcess::ProcessState state);
void slotCheckSubtitleProcess(int exitCode, QProcess::ExitStatus exitStatus);
void receivedSubtitleProgress();
private:
QString m_scenelist;
......@@ -59,6 +62,8 @@ private:
/** @brief The process id of the Kdenlive instance, used to get the dbus service. */
int m_pid;
bool m_dualpass;
QString m_subtitleFile;
QString m_temporaryRenderFile;
QProcess *m_renderProcess;
QString m_errorMessage;
QList<QVariant> m_dbusargs;
......@@ -72,6 +77,7 @@ private:
void initKdenliveDbusInterface();
#endif
void sendFinish(int status, const QString &error);
void updateProgress(int speed = -1);
void sendProgress();
signals:
......
......@@ -7,6 +7,7 @@
#include "bin/bin.h"
#include "core.h"
#include "doc/kdenlivedoc.h"
#include "kdenlivesettings.h"
#include "macros.hpp"
#include "profiles/profilemodel.hpp"
#include "project/projectmanager.h"
......
......@@ -277,7 +277,7 @@ RenderWidget::RenderWidget(bool enableProxy, QWidget *parent)
header->setSectionResizeMode(0, QHeaderView::Fixed);
header->resizeSection(0, size + 4);
// Find path for Kdenlive renderer
// Find path for Kdenlive renderer
#ifdef Q_OS_WIN
m_renderer = QCoreApplication::applicationDirPath() + QStringLiteral("/kdenlive_render.exe");
#else
......@@ -310,6 +310,7 @@ RenderWidget::RenderWidget(bool enableProxy, QWidget *parent)
refreshView();
focusItem();
adjustSize();
m_view.embed_subtitles->setToolTip(i18n("Only works for the matroska (mkv) format"));
}
void RenderWidget::slotShareActionFinished(const QJsonObject &output, int error, const QString &message)
......@@ -643,16 +644,6 @@ void RenderWidget::prepareRendering(bool delayedRendering)
// Set playlist audio volume to 100%
QDomDocument doc;
doc.setContent(playlistContent);
QDomElement tractor = doc.documentElement().firstChildElement(QStringLiteral("tractor"));
if (!tractor.isNull()) {
QDomNodeList props = tractor.elementsByTagName(QStringLiteral("property"));
for (int i = 0; i < props.count(); ++i) {
if (props.at(i).toElement().attribute(QStringLiteral("name")) == QLatin1String("meta.volume")) {
props.at(i).firstChild().setNodeValue(QStringLiteral("1"));
break;
}
}
}
// Add autoclose to playlists.
QDomNodeList playlists = doc.elementsByTagName(QStringLiteral("playlist"));
......@@ -672,17 +663,41 @@ void RenderWidget::prepareRendering(bool delayedRendering)
int out = pCore->projectDuration() - 2;
Monitor *pMon = pCore->getMonitor(Kdenlive::ProjectMonitor);
double fps = pCore->getCurrentProfile()->fps();
QString subtitleFile;
if (m_view.embed_subtitles->isEnabled() && m_view.embed_subtitles->isChecked() && project->hasSubtitles()) {
QTemporaryFile src(QDir::temp().absoluteFilePath(QString("XXXXXX.srt")));
if (!src.open()) {
// Something went wrong
KMessageBox::sorry(this, i18n("Could not create temporary subtitle file"));
return;
}
subtitleFile = src.fileName();
src.setAutoRemove(false);
// disable subtitle filter(s) as they will be embeded in a second step of rendering
QDomNodeList filters = doc.elementsByTagName(QStringLiteral("filter"));
for (int i = 0; i < filters.length(); ++i) {
if (Xml::getXmlProperty(filters.item(i).toElement(), QStringLiteral("mlt_service")) == QLatin1String("avfilter.subtitles")) {
Xml::setXmlProperty(filters.item(i).toElement(), QStringLiteral("disable"), QStringLiteral("1"));
}
}
}
if (m_view.render_zone->isChecked()) {
in = pMon->getZoneStart();
out = pMon->getZoneEnd() - 1;
generateRenderFiles(doc, in, out, outputFile, delayedRendering);
if (!subtitleFile.isEmpty()) {
project->generateRenderSubtitleFile(in, out, subtitleFile);
}
generateRenderFiles(doc, in, out, outputFile, delayedRendering, subtitleFile);
} else if (m_view.render_guide->isChecked()) {
double guideStart = m_view.guide_start->itemData(m_view.guide_start->currentIndex()).toDouble();
double guideEnd = m_view.guide_end->itemData(m_view.guide_end->currentIndex()).toDouble();
in = int(GenTime(guideStart).frames(fps));
// End rendering at frame before last guide
out = int(GenTime(guideEnd).frames(fps)) - 1;
generateRenderFiles(doc, in, out, outputFile, delayedRendering);
if (!subtitleFile.isEmpty()) {
project->generateRenderSubtitleFile(in, out, subtitleFile);
}
generateRenderFiles(doc, in, out, outputFile, delayedRendering, subtitleFile);
} else if (m_view.render_multi->isChecked()) {
if (auto ptr = m_guidesModel.lock()) {
int category = m_view.guideCategoryCombo->currentData().toInt();
......@@ -720,13 +735,29 @@ void RenderWidget::prepareRendering(bool delayedRendering)
QString filename =
outputFile.section(QLatin1Char('.'), 0, -2) + QStringLiteral("-%1.").arg(name) + outputFile.section(QLatin1Char('.'), -1);
QDomDocument docCopy = doc.cloneNode(true).toDocument();
generateRenderFiles(docCopy, in, out, filename, false);
if (!subtitleFile.isEmpty()) {
project->generateRenderSubtitleFile(in, out, subtitleFile);
}
generateRenderFiles(docCopy, in, out, filename, false, subtitleFile);
if (!subtitleFile.isEmpty() && i < markers.count() - 1) {
QTemporaryFile src(QDir::temp().absoluteFilePath(QString("XXXXXX.srt")));
if (!src.open()) {
// Something went wrong
KMessageBox::sorry(this, i18n("Could not create temporary subtitle file"));
return;
}
subtitleFile = src.fileName();
src.setAutoRemove(false);
}
}
}
}
}
} else {
generateRenderFiles(doc, in, out, outputFile, delayedRendering);
if (!subtitleFile.isEmpty()) {
project->generateRenderSubtitleFile(in, out, subtitleFile);
}
generateRenderFiles(doc, in, out, outputFile, delayedRendering, subtitleFile);
}
}
......@@ -777,7 +808,7 @@ QString RenderWidget::generatePlaylistFile(bool delayedRendering)
return tmp.fileName();
}
void RenderWidget::generateRenderFiles(QDomDocument doc, int in, int out, QString outputFile, bool delayedRendering)
void RenderWidget::generateRenderFiles(QDomDocument doc, int in, int out, QString outputFile, bool delayedRendering, const QString &subtitleFile)
{
QString playlistPath = generatePlaylistFile(delayedRendering);
QString extension = outputFile.section(QLatin1Char('.'), -1);
......@@ -785,7 +816,6 @@ void RenderWidget::generateRenderFiles(QDomDocument doc, int in, int out, QStrin
if (playlistPath.isEmpty()) {
return;
}
QString renderArgs = m_view.advanced_params->toPlainText().simplified();
QDomElement consumer = doc.createElement(QStringLiteral("consumer"));
consumer.setAttribute(QStringLiteral("in"), in);
......@@ -954,7 +984,7 @@ void RenderWidget::generateRenderFiles(QDomDocument doc, int in, int out, QStrin
QList<RenderJobItem *> jobList;
QMap<QString, QString>::const_iterator i = renderFiles.constBegin();
while (i != renderFiles.constEnd()) {
RenderJobItem *renderItem = createRenderJob(i.key(), i.value(), in, out);
RenderJobItem *renderItem = createRenderJob(i.key(), i.value(), in, out, subtitleFile);
if (renderItem != nullptr) {
jobList << renderItem;
}
......@@ -968,7 +998,7 @@ void RenderWidget::generateRenderFiles(QDomDocument doc, int in, int out, QStrin
checkRenderStatus();
}
RenderJobItem *RenderWidget::createRenderJob(const QString &playlist, const QString &outputFile, int in, int out)
RenderJobItem *RenderWidget::createRenderJob(const QString &playlist, const QString &outputFile, int in, int out, const QString &subtitleFile)
{
QList<QTreeWidgetItem *> existing = m_view.running_jobs->findItems(outputFile, Qt::MatchExactly, 1);
RenderJobItem *renderItem = nullptr;
......@@ -999,6 +1029,9 @@ RenderJobItem *RenderWidget::createRenderJob(const QString &playlist, const QStr
QStringLiteral("-pid:%1").arg(QCoreApplication::applicationPid()),
QStringLiteral("-out"),
QString::number(out)};
if (!subtitleFile.isEmpty()) {
argsJob << QStringLiteral("-subtitle") << subtitleFile;
}
renderItem->setData(1, ParametersRole, argsJob);
qDebug() << "* CREATED JOB WITH ARGS: " << argsJob;
renderItem->setData(1, OpenBrowserRole, m_view.open_browser->isChecked());
......@@ -1215,6 +1248,7 @@ void RenderWidget::loadProfile()
m_view.checkTwoPass->setChecked(passes && params.contains(QStringLiteral("passes=2")));
m_view.encoder_threads->setEnabled(!profile->hasParam(QStringLiteral("threads")));
m_view.embed_subtitles->setEnabled(profile->extension() == QLatin1String("mkv") || profile->extension() == QLatin1String("matroska"));
m_view.video_box->setChecked(profile->getParam(QStringLiteral("vn")) != QStringLiteral("1"));
m_view.audio_box->setChecked(profile->getParam(QStringLiteral("an")) != QStringLiteral("1"));
......
......@@ -226,8 +226,8 @@ private:
void prepareRendering(bool delayedRendering);
/** @brief Create a new empty playlist (*.mlt) file and @returns the filename of the created file */
QString generatePlaylistFile(bool delayedRendering);
void generateRenderFiles(QDomDocument doc, int in, int out, QString outputFile, bool delayedRendering);
RenderJobItem *createRenderJob(const QString &playlist, const QString &outputFile, int in, int out);
void generateRenderFiles(QDomDocument doc, int in, int out, QString outputFile, bool delayedRendering, const QString &subtitleFile = QString());
RenderJobItem *createRenderJob(const QString &playlist, const QString &outputFile, int in, int out, const QString &subtitleFile = QString());
signals:
void abortProcess(const QString &url);
......
......@@ -1882,6 +1882,18 @@ void KdenliveDoc::initializeSubtitles(std::shared_ptr<SubtitleModel> m_subtitle)
m_subtitleModel = m_subtitle;
}
bool KdenliveDoc::hasSubtitles() const
{
return (m_subtitleModel.lock() != nullptr);
}
void KdenliveDoc::generateRenderSubtitleFile(int in, int out, const QString &subtitleFile)
{
if (auto ptr = m_subtitleModel.lock()) {
ptr->subtitleFileFromZone(in, out, subtitleFile);
}
}
void KdenliveDoc::useOriginals(QDomDocument &doc)
{
QString root = doc.documentElement().attribute(QStringLiteral("root"));
......
......@@ -214,6 +214,10 @@ public:
/** @brief Replace proxy clips with originals for rendering. */
void useOriginals(QDomDocument &doc);
void processProxyNodes(QDomNodeList producers, const QString &root, const QMap<QString, QString> &proxies);
/** @brief Returns true if this project has subtitles. */
bool hasSubtitles() const;
/** @brief Generate a temporary subtitle file for a zone. */
void generateRenderSubtitleFile(int in, int out, const QString &subtitleFile);
private:
/** @brief Create a new KdenliveDoc using the provided QDomDocument (an
......
......@@ -686,10 +686,10 @@
</item>
<item>
<widget class="KMessageWidget" name="overrideParamsWarning">
<property name="wordWrap" stdset="0">
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="closeButtonVisible" stdset="0">
<property name="closeButtonVisible">
<bool>false</bool>
</property>
</widget>
......
......@@ -445,7 +445,7 @@
<x>0</x>
<y>0</y>
<width>313</width>
<height>924</height>
<height>952</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_4">
......@@ -783,10 +783,20 @@
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="embed_subtitles">
<property name="toolTip">
<string extracomment="Only works for the matroska (mkv) format"/>
</property>
<property name="text">
<string>Embed subtitles instead of burning them</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="open_browser">
<property name="text">
<string>Open browser window after export</string>
<string>Open folder after export</string>
</property>
</widget>
</item>
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment