Commit 3480b5af authored by Julius Künzel's avatar Julius Künzel
Browse files

[Render Widget] Refactoring and feature extension

The most important changes:
* Rename "Profiles" to "Render Presets"
* Separate presets view and model code better
* Extend preset editor to have all important options in the UI instead of needing to write the properties by hand
* Add a batch render mode using guides as separator
* Re-implement "Stem audio" export under new name "Separate file for each track"
* Other UI improvements and face lifting

Fixes #211
Related to #444

BUG: 415610
FIXED-IN: 22.04.0
parent fdc29aa4
Pipeline #150860 passed with stage
in 15 minutes and 16 seconds
......@@ -59,6 +59,7 @@ add_subdirectory(onlineresources)
add_subdirectory(profiles)
add_subdirectory(project)
add_subdirectory(pythoninterfaces)
add_subdirectory(renderpresets)
add_subdirectory(scopes)
add_subdirectory(simplekeyframes)
add_subdirectory(timeline2)
......
......@@ -446,12 +446,14 @@ CommentedTime MarkerListModel::getMarker(const GenTime &pos, bool *ok) const
return marker(pos);
}
QList<CommentedTime> MarkerListModel::getAllMarkers() const
QList<CommentedTime> MarkerListModel::getAllMarkers(int type) const
{
READ_LOCK();
QList<CommentedTime> markers;
for (const auto &marker : m_markerList) {
markers << marker.second;
if (type == -1 || marker.second.markerType() == type) {
markers << marker.second;
}
}
std::sort(markers.begin(), markers.end());
return markers;
......
......@@ -90,8 +90,8 @@ public:
/** @brief Returns a marker data at given pos */
CommentedTime getMarker(const GenTime &pos, bool *ok) const;
/** @brief Returns all markers in model */
QList<CommentedTime> getAllMarkers() const;
/** @brief Returns all markers in model or – if a type is given – all markers of the given type */
QList<CommentedTime> getAllMarkers(int type = -1) const;
/** @brief Returns all markers of model that are intersect with a given range.
* @param start is the position where start to search for markers
......
......@@ -22,7 +22,7 @@ const int MAXCLIPDURATION = 15000;
namespace Kdenlive {
enum MonitorId { NoMonitor = 0x01, ClipMonitor = 0x02, ProjectMonitor = 0x04, RecordMonitor = 0x08, StopMotionMonitor = 0x10 };
enum MonitorId { NoMonitor = 0x01, ClipMonitor = 0x02, ProjectMonitor = 0x04, RecordMonitor = 0x08, StopMotionMonitor = 0x10, RenderMonitor = 0x20 };
const int DefaultThumbHeight = 100;
} // namespace Kdenlive
......
......@@ -7,6 +7,7 @@ set(kdenlive_SRCS
dialogs/profilesdialog.cpp
dialogs/proxytest.cpp
dialogs/renderwidget.cpp
dialogs/renderpresetdialog.cpp
dialogs/speechdialog.cpp
dialogs/subtitleedit.cpp
dialogs/textbasededit.cpp
......
This diff is collapsed.
#ifndef RENDERPRESETDIALOG_H
#define RENDERPRESETDIALOG_H
#include "ui_editrenderpreset_ui.h"
class RenderPresetModel;
class Monitor;
class RenderPresetDialog : public QDialog, Ui::EditRenderPreset_UI
{
Q_OBJECT
public:
enum Mode {
New = 0,
Edit,
SaveAs
};
explicit RenderPresetDialog(QWidget *parent, RenderPresetModel *preset = nullptr, Mode mode = Mode::New);
/** @returns the name that was finally under which the preset has been saved */
~RenderPresetDialog();
QString saveName();
private:
QString m_saveName;
QStringList m_uiParams;
Monitor *m_monitor;
private slots:
void slotUpdateParams();
};
#endif // RENDERPRESETDIALOG_H
This diff is collapsed.
......@@ -20,6 +20,7 @@ class Menu;
#include "definitions.h"
#include "bin/model/markerlistmodel.hpp"
#include "ui_renderwidget_ui.h"
#include "renderpresets/tree/renderpresettreemodel.hpp"
class QDomElement;
class QKeyEvent;
......@@ -104,31 +105,31 @@ class RenderWidget : public QDialog
Q_OBJECT
public:
enum RenderError {
CompositeError = 0,
PresetError = 1,
ProxyWarning = 2,
PlaybackError = 3,
OptionsError = 4
};
explicit RenderWidget(bool enableProxy, QWidget *parent = nullptr);
~RenderWidget() override;
void saveConfig();
void loadConfig();
void setGuides(std::weak_ptr<MarkerListModel> guidesModel);
void focusFirstVisibleItem(const QString &profile = QString());
void setRenderJob(const QString &dest, int progress = 0, int frame = 0);
void focusItem(const QString &profile = QString());
void setRenderProgress(const QString &dest, int progress = 0, int frame = 0);
void setRenderStatus(const QString &dest, int status, const QString &error);
void reloadProfiles();
void setRenderProfile(const QMap<QString, QString> &props);
void saveRenderProfile();
void updateDocumentPath();
int waitingJobsCount() const;
int runningJobsCount() const;
QString getFreeScriptName(const QUrl &projectName = QUrl(), const QString &prefix = QString());
bool startWaitingRenderJobs();
/** @brief Returns true if the export audio checkbox is set to automatic. */
bool automaticAudioExport() const;
/** @brief Returns true if user wants audio export. */
bool selectedAudioExport() const;
/** @brief Show / hide proxy settings. */
void updateProxyConfig(bool enable);
/** @brief Should we render using proxy clips. */
bool proxyRendering();
/** @brief Returns true if the stem audio export checkbox is set. */
bool isStemAudioExportEnabled() const;
enum RenderError { CompositeError = 0, ProfileError = 1, ProxyWarning = 2, PlaybackError = 3 };
/** @brief Display warning message in render widget. */
void errorMessage(RenderError type, const QString &message);
......@@ -136,6 +137,8 @@ public:
protected:
QSize sizeHint() const override;
void keyPressEvent(QKeyEvent *e) override;
void parseProfile(QDomElement profile, QTreeWidgetItem *childitem, QString groupName,
QString extension = QString(), QString renderer = QStringLiteral("avformat"));
public slots:
void slotAbortCurrentJob();
......@@ -145,56 +148,60 @@ public slots:
/** @brief Adjust render file name to current project name. */
void resetRenderPath(const QString &path);
private slots:
/**
* Will be called when the user selects an output file via the file dialog.
* File extension will be added automatically.
*/
void slotUpdateButtons(const QUrl &url);
/**
* Will be called when the user changes the output file path in the text line.
* File extension must NOT be added, would make editing impossible!
*/
void slotUpdateButtons();
void refreshView();
void slotChangeSelection(const QModelIndex &current, const QModelIndex &previous);
/** @brief Updates available options when a new format has been selected. */
void loadProfile();
void refreshParams();
void slotSaveProfile();
void slotEditProfile();
void slotDeleteProfile(bool dontRefresh = false);
void slotUpdateGuideBox();
void slotSavePresetAs();
void slotNewPreset();
void slotEditPreset();
void slotRenderModeChanged();
void slotUpdateRescaleHeight(int);
void slotUpdateRescaleWidth(int);
void slotSwitchAspectRatio();
void slotCheckStartGuidePosition();
void slotCheckEndGuidePosition();
void showInfoPanel();
/** @brief Enable / disable the rescale options. */
void setRescaleEnabled(bool enable);
/** @brief Show updated command parameter in tooltip. */
void adjustSpeed(int videoQuality);
void slotStartScript();
void slotDeleteScript();
void slotGenerateScript();
void parseScriptFiles();
void slotCheckScript();
void slotCheckJob();
void slotEditItem(QTreeWidgetItem *item);
void slotCLeanUpJobs();
void slotCleanUpJobs();
void slotHideLog();
void slotPlayRendering(QTreeWidgetItem *item, int);
void slotStartCurrentJob();
void slotCopyToFavorites();
void slotDownloadNewRenderProfiles();
void slotUpdateEncodeThreads(int);
void slotUpdateRescaleHeight(int);
void slotUpdateRescaleWidth(int);
void slotSwitchAspectRatio();
/** @brief Update export audio label depending on current settings. */
void slotUpdateAudioLabel(int ix);
/** @brief Enable / disable the rescale options. */
void setRescaleEnabled(bool enable);
/** @brief Adjust video/audio quality spinboxes from quality slider. */
void adjustAVQualities(int quality);
/** @brief Adjust quality slider from video spinbox. */
void adjustQuality(int videoQuality);
/** @brief Show updated command parameter in tooltip. */
void adjustSpeed(int videoQuality);
/** @brief Display warning on proxy rendering. */
void slotProxyWarn(bool enableProxy);
/** @brief User shared a rendered file, give feedback. */
void slotShareActionFinished(const QJsonObject &output, int error, const QString &message);
/** @brief running jobs menu. */
void prepareMenu(const QPoint &pos);
void prepareJobContextMenu(const QPoint &pos);
private:
enum Tabs {
RenderTab = 0,
JobsTab,
ScriptsTab
};
Ui::RenderWidget_UI m_view;
QString m_projectFolder;
RenderViewDelegate *m_scriptsDelegate;
......@@ -204,32 +211,29 @@ private:
QMap<int, QString> m_errorMessages;
std::weak_ptr<MarkerListModel> m_guidesModel;
QStringList m_acodecsList;
QStringList m_vcodecsList;
QStringList m_supportedFormats;
std::shared_ptr<RenderPresetTreeModel> m_treeModel;
QString m_currentProfile;
#ifdef KF5_USE_PURPOSE
Purpose::Menu *m_shareMenu;
#endif
void parseMltPresets();
void parseProfiles(const QString &selectedProfile = QString());
void parseFile(const QString &exportFile, bool editable);
void updateButtons();
QUrl filenameWithExtension(QUrl url, const QString &extension);
/** @brief Check if a job needs to be started. */
void checkRenderStatus();
void startRendering(RenderJobItem *item);
bool saveProfile(QDomElement newprofile);
/** @brief Create a rendering profile from MLT preset. */
QTreeWidgetItem *loadFromMltPreset(const QString &groupName, const QString &path, const QString &profileName);
void checkCodecs();
void prepareRendering(bool delayedRendering, const QString &chapterFile);
void generateRenderFiles(QDomDocument doc, const QString &playlistPath, int in, int out, bool delayedRendering);
QTreeWidgetItem *loadFromMltPreset(const QString &groupName, const QString &path, QString profileName, bool codecInName = false);
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);
signals:
void abortProcess(const QString &url);
/** Send the info about rendering that will be saved in the document:
(profile destination, profile name and url of rendered file */
(profile destination, profile name and url of rendered file) */
void selectedRenderProfile(const QMap<QString, QString> &renderProps);
void shutdown();
};
......
......@@ -915,14 +915,14 @@
<default>false</default>
</entry>
<entry name="validated_luts" type="StringList">
<label>The paths of lut files that have been validated.</label>
<entry name="renderProfile" type="String">
<label>Default render profile.</label>
<default></default>
</entry>
<entry name="showrenderparams" type="Bool">
<label>Show avformat render parameters in rendering dialog.</label>
<default>false</default>
<entry name="validated_luts" type="StringList">
<label>The paths of lut files that have been validated.</label>
<default></default>
</entry>
<entry name="displayClipMonitorInfo" type="Int">
......
......@@ -2221,7 +2221,7 @@ void MainWindow::setRenderingProgress(const QString &url, int progress, int fram
{
emit setRenderProgress(progress);
if (m_renderWidget) {
m_renderWidget->setRenderJob(url, progress, frame);
m_renderWidget->setRenderProgress(url, progress, frame);
}
}
......
......@@ -73,8 +73,15 @@ std::shared_ptr<ProfileTreeModel> ProfileTreeModel::construct(QObject *parent)
std::unique_ptr<ProfileModel> &ptr = ProfileRepository::get()->getProfile(profile.second);
// we create the data list corresponding to this profile
QList<QVariant> data;
data << profile.first << profile.second << ptr->height() << ptr->width() << ptr->display_aspect_num() << ptr->display_aspect_den() << ptr->sar()
<< ptr->fps() << ProfileRepository::getColorspaceDescription(ptr->colorspace());
data << profile.first
<< profile.second
<< ptr->height()
<< ptr->width()
<< ptr->display_aspect_num()
<< ptr->display_aspect_den()
<< ptr->sar()
<< ptr->fps()
<< ProfileRepository::getColorspaceDescription(ptr->colorspace());
for (const auto &filter : filters) {
bool matching = true;
for (size_t i = 0; i < nbCrit && matching; ++i) {
......
set(kdenlive_SRCS
${kdenlive_SRCS}
renderpresets/renderpresetrepository.cpp
renderpresets/renderpresetmodel.cpp
renderpresets/tree/renderpresettreemodel.cpp
PARENT_SCOPE)
/*
SPDX-FileCopyrightText: 2017 Nicolas Carion
SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
This file is part of Kdenlive. See www.kdenlive.org.
*/
#include "renderpresetmodel.hpp"
#include "renderpresetrepository.hpp"
#include "core.h"
#include "kdenlive_debug.h"
#include "kdenlivesettings.h"
#include "profiles/profilerepository.hpp"
#include "profiles/profilemodel.hpp"
#include <KLocalizedString>
#include <KMessageWidget>
#include <QDir>
#include <QFile>
#include <QRegularExpression>
#include <memory>
RenderPresetModel::RenderPresetModel(QDomElement preset, const QString &presetFile, bool editable, const QString &groupName, const QString &renderer)
: m_presetFile(presetFile)
, m_editable(editable)
, m_name(preset.attribute(QStringLiteral("name")))
, m_note()
, m_standard(preset.attribute(QStringLiteral("standard")))
, m_params()
, m_groupName(preset.attribute(QStringLiteral("category"), groupName))
, m_renderer(renderer)
, m_url(preset.attribute(QStringLiteral("url")))
, m_speeds(preset.attribute(QStringLiteral("speeds")))
, m_topFieldFirst(preset.attribute(QStringLiteral("top_field_first")))
{
if (m_groupName.isEmpty()) {
m_groupName = i18nc("Category Name", "Custom");
}
QTextDocument docConvert;
docConvert.setHtml(preset.attribute(QStringLiteral("args")));
m_params = docConvert.toPlainText().simplified();
m_extension = preset.attribute(QStringLiteral("extension"));
if (getParam(QStringLiteral("f")).isEmpty() && !m_extension.isEmpty() && RenderPresetRepository::supportedFormats().contains(m_extension)) {
m_params.append(QStringLiteral(" f=%1").arg(m_extension));
}
m_vQualities = preset.attribute(QStringLiteral("qualities"));
m_defaultVQuality = preset.attribute(QStringLiteral("defaultquality"));
m_vBitrates = preset.attribute(QStringLiteral("bitrates"));
m_defaultVBitrate = preset.attribute(QStringLiteral("defaultbitrate"));
m_aQualities = preset.attribute(QStringLiteral("audioqualities"));
m_defaultAQuality = preset.attribute(QStringLiteral("defaultaudioquality"));
m_aBitrates = preset.attribute(QStringLiteral("audiobitrates"));
m_defaultABitrate = preset.attribute(QStringLiteral("defaultaudiobitrate"));
checkPreset();
}
RenderPresetModel::RenderPresetModel(const QString &groupName, const QString &path, QString presetName, const QString &params, bool codecInName)
: m_editable(false)
, m_params(params)
, m_groupName(groupName)
, m_renderer(QStringLiteral("avformat"))
{
KConfig config(path, KConfig::SimpleConfig);
KConfigGroup group = config.group(QByteArray());
QString vcodec = group.readEntry("vcodec");
QString acodec = group.readEntry("acodec");
m_extension = group.readEntry("meta.preset.extension");
if (getParam(QStringLiteral("f")).isEmpty() && !m_extension.isEmpty() && RenderPresetRepository::supportedFormats().contains(m_extension)) {
m_params.append(QStringLiteral(" f=%1").arg(m_extension));
}
m_note = group.readEntry("meta.preset.note");
if (codecInName && (!vcodec.isEmpty() || !acodec.isEmpty())) {
presetName.append(" (");
if (!vcodec.isEmpty()) {
presetName.append(vcodec);
if (!acodec.isEmpty()) {
presetName.append("+" + acodec);
}
} else if (!acodec.isEmpty()) {
presetName.append(acodec);
}
presetName.append(QLatin1Char(')'));
}
m_name = presetName;
checkPreset();
}
RenderPresetModel::RenderPresetModel(const QString &name, const QString &groupName, const QString &params, const QString &defaultVBitrate, const QString &defaultVQuality,
const QString &defaultABitrate, const QString &defaultAQuality, const QString &speeds)
: m_presetFile()
, m_editable()
, m_name(name)
, m_note()
, m_standard()
, m_params(params)
, m_extension()
, m_groupName(groupName)
, m_renderer(QStringLiteral("avformat"))
, m_url()
, m_speeds(speeds)
, m_topFieldFirst()
, m_vBitrates()
, m_defaultVBitrate(defaultVBitrate)
, m_vQualities()
, m_defaultVQuality(defaultVQuality)
, m_aBitrates()
, m_defaultABitrate(defaultABitrate)
, m_aQualities()
, m_defaultAQuality(defaultAQuality)
{
if (m_groupName.isEmpty()) {
m_groupName = i18nc("Category Name", "Custom");
}
checkPreset();
}
void RenderPresetModel::checkPreset() {
QStringList acodecs = RenderPresetRepository::acodecs();
QStringList vcodecs = RenderPresetRepository::vcodecs();
QStringList supportedFormats = RenderPresetRepository::supportedFormats();
bool replaceVorbisCodec = acodecs.contains(QStringLiteral("libvorbis"));
bool replaceLibfaacCodec = acodecs.contains(QStringLiteral("libfaac"));
if (replaceVorbisCodec && m_params.contains(QStringLiteral("acodec=vorbis"))) {
// replace vorbis with libvorbis
m_params = m_params.replace(QLatin1String("=vorbis"), QLatin1String("=libvorbis"));
}
if (replaceLibfaacCodec && m_params.contains(QStringLiteral("acodec=aac"))) {
// replace libfaac with aac
m_params = m_params.replace(QLatin1String("aac"), QLatin1String("libfaac"));
}
// We borrow a reference to the profile's pointer to query it more easily
std::unique_ptr<ProfileModel> &profile = pCore->getCurrentProfile();
double project_framerate = double(profile->frame_rate_num()) / profile->frame_rate_den();
if (m_standard.isEmpty() ||
(m_standard.contains(QStringLiteral("PAL"), Qt::CaseInsensitive) && profile->frame_rate_num() == 25 && profile->frame_rate_den() == 1) ||
(m_standard.contains(QStringLiteral("NTSC"), Qt::CaseInsensitive) && profile->frame_rate_num() == 30000 && profile->frame_rate_den() == 1001)) {
// Standard OK
} else {
m_errors = i18n("Standard (%1) not compatible with project profile (%2)", m_standard, project_framerate);
return;
}
if (m_params.contains(QStringLiteral("mlt_profile="))) {
QString profile_str = getParam(QStringLiteral("mlt_profile"));
std::unique_ptr<ProfileModel> &target_profile = ProfileRepository::get()->getProfile(profile_str);
if (target_profile->frame_rate_den() > 0) {
double profile_rate = double(target_profile->frame_rate_num()) / target_profile->frame_rate_den();
if (int(1000.0 * profile_rate) != int(1000.0 * project_framerate)) {
m_errors = i18n("Frame rate (%1) not compatible with project profile (%2)", profile_rate, project_framerate);
return;
}
}
}
// Make sure the selected preset uses an installed avformat codec / format
QString format;
format = getParam(QStringLiteral("f")).toLower();
if (!format.isEmpty() && !supportedFormats.contains(format)) {
m_errors = i18n("Unsupported video format: %1", format);
return;
}
// check for missing audio codecs
format = getParam(QStringLiteral("acodec")).toLower();
if (!format.isEmpty() && !acodecs.contains(format)) {
m_errors = i18n("Unsupported audio codec: %1", format);
return;
}
// check for missing video codecs
format = getParam(QStringLiteral("vcodec")).toLower();
if (!format.isEmpty() && !vcodecs.contains(format)) {
m_errors = i18n("Unsupported video codec: %1", format);
return;
}
if (hasParam(QStringLiteral("profile"))) {
m_warnings = i18n("This render preset uses a 'profile' parameter.<br />Unless you know what you are doing you will probably "
"have to change it to 'mlt_profile'.");
return;
}
}
QDomElement RenderPresetModel::toXml()
{
QDomDocument doc;
QDomElement profileElement = doc.createElement(QStringLiteral("profile"));
doc.appendChild(profileElement);
profileElement.setAttribute(QStringLiteral("name"), m_name);
profileElement.setAttribute(QStringLiteral("category"), m_groupName);
if (!m_extension.isEmpty()) {
profileElement.setAttribute(QStringLiteral("extension"), m_extension);
}
profileElement.setAttribute(QStringLiteral("args"), m_params);
if (!m_defaultVBitrate.isEmpty()) {
profileElement.setAttribute(QStringLiteral("defaultbitrate"), m_defaultVBitrate);
}
if (!m_vBitrates.isEmpty()) {
profileElement.setAttribute(QStringLiteral("bitrates"), m_vBitrates);
}
if (!m_defaultVQuality.isEmpty()) {
profileElement.setAttribute(QStringLiteral("defaultquality"), m_defaultVQuality);
}
if (!m_vQualities.isEmpty()) {
profileElement.setAttribute(QStringLiteral("qualities"), m_vQualities);
}
if (!m_defaultABitrate.isEmpty()) {
profileElement.setAttribute(QStringLiteral("defaultaudiobitrate"), m_defaultABitrate);
}
if (!m_aBitrates.isEmpty()) {
profileElement.setAttribute(QStringLiteral("audiobitrates"), m_aBitrates);
}
if (!m_defaultAQuality.isEmpty()) {
profileElement.setAttribute(QStringLiteral("defaultaudioquality"), m_defaultAQuality);
}
if (!m_aQualities.isEmpty()) {
profileElement.setAttribute(QStringLiteral("audioqualities"), m_aQualities);
}
if (m_speeds.isEmpty()) {
// profile has a variable speed
profileElement.setAttribute(QStringLiteral("speeds"), m_speeds);
}
return doc.documentElement();
}
QString RenderPresetModel::params(QStringList removeParams) const
{
QString params = m_params;
for (auto p : removeParams) {
params.remove(QRegularExpression(QStringLiteral("((^|\\s)%1=\\S*)").arg(p)));
}
return params.simplified();
}
QString RenderPresetModel::extension() const {
if (!m_extension.isEmpty()) {
return m_extension;
}
return getParam(QStringLiteral("f"));