From 402e67740750fc40bb5ef416affc6c1eebc41bd1 Mon Sep 17 00:00:00 2001 From: Brian Matherly Date: Thu, 17 Aug 2023 15:56:23 -0500 Subject: [PATCH] Resource view (#1456) * Add new resource dialog * Add "Convert" option to Resource Dialog * Show resource dialog when multiple files are added and need conversion. * Show resource dialog when multiple files are dropped on the timeline. The dialog only shows if any of the files need conversion. * Show resource dialog when multiple files are dropped on the playlist * Improve resource view table * Add alternating row background colors * Make the columns resizable --- src/CMakeLists.txt | 4 + src/dialogs/resourcedialog.cpp | 111 ++++++++ src/dialogs/resourcedialog.h | 50 ++++ src/docks/playlistdock.cpp | 22 +- src/docks/timelinedock.cpp | 174 +++++------ src/docks/timelinedock.h | 1 + src/mainwindow.cpp | 64 ++++- src/mainwindow.h | 6 +- src/mainwindow.ui | 5 + src/mltcontroller.cpp | 3 +- src/mltcontroller.h | 4 +- src/models/resourcemodel.cpp | 380 +++++++++++++++++++++++++ src/models/resourcemodel.h | 59 ++++ src/qml/views/timeline/Timeline.js | 5 +- src/transcoder.cpp | 296 +++++++++++++++++++ src/transcoder.h | 45 +++ src/util.cpp | 85 ++++++ src/util.h | 4 + src/widgets/avformatproducerwidget.cpp | 271 +----------------- src/widgets/avformatproducerwidget.h | 1 - src/widgets/resourcewidget.cpp | 118 ++++++++ src/widgets/resourcewidget.h | 48 ++++ 22 files changed, 1387 insertions(+), 369 deletions(-) create mode 100644 src/dialogs/resourcedialog.cpp create mode 100644 src/dialogs/resourcedialog.h create mode 100644 src/models/resourcemodel.cpp create mode 100644 src/models/resourcemodel.h create mode 100644 src/transcoder.cpp create mode 100644 src/transcoder.h create mode 100644 src/widgets/resourcewidget.cpp create mode 100644 src/widgets/resourcewidget.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 2676f9c745..1016684064 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -24,6 +24,7 @@ add_executable(shotcut WIN32 MACOSX_BUNDLE dialogs/listselectiondialog.ui dialogs/longuitask.cpp dialogs/longuitask.h dialogs/multifileexportdialog.cpp dialogs/multifileexportdialog.h + dialogs/resourcedialog.cpp dialogs/resourcedialog.h dialogs/saveimagedialog.cpp dialogs/saveimagedialog.h dialogs/slideshowgeneratordialog.cpp dialogs/slideshowgeneratordialog.h dialogs/systemsyncdialog.cpp dialogs/systemsyncdialog.h @@ -73,6 +74,7 @@ add_executable(shotcut WIN32 MACOSX_BUNDLE models/motiontrackermodel.h models/motiontrackermodel.cpp models/multitrackmodel.cpp models/multitrackmodel.h models/playlistmodel.cpp models/playlistmodel.h + models/resourcemodel.cpp models/resourcemodel.h openotherdialog.cpp openotherdialog.h openotherdialog.ui player.cpp player.h @@ -102,6 +104,7 @@ add_executable(shotcut WIN32 MACOSX_BUNDLE settings.cpp settings.h sharedframe.cpp sharedframe.h shotcut_mlt_properties.h + transcoder.cpp transcoder.h spatialmedia/box.cpp spatialmedia/box.h spatialmedia/container.cpp spatialmedia/container.h spatialmedia/mpeg4_container.cpp spatialmedia/mpeg4_container.h @@ -164,6 +167,7 @@ add_executable(shotcut WIN32 MACOSX_BUNDLE widgets/producerpreviewwidget.cpp widgets/producerpreviewwidget.h widgets/pulseaudiowidget.cpp widgets/pulseaudiowidget.h widgets/pulseaudiowidget.ui + widgets/resourcewidget.cpp widgets/resourcewidget.h widgets/scopes/audioloudnessscopewidget.cpp widgets/scopes/audioloudnessscopewidget.h widgets/scopes/audiopeakmeterscopewidget.cpp widgets/scopes/audiopeakmeterscopewidget.h widgets/scopes/audiospectrumscopewidget.cpp widgets/scopes/audiospectrumscopewidget.h diff --git a/src/dialogs/resourcedialog.cpp b/src/dialogs/resourcedialog.cpp new file mode 100644 index 0000000000..9dd78f642a --- /dev/null +++ b/src/dialogs/resourcedialog.cpp @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2023 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 . + */ + +#include "resourcedialog.h" + +#include "Logger.h" +#include "mltcontroller.h" +#include "qmltypes/qmlapplication.h" +#include "transcodedialog.h" +#include "transcoder.h" +#include "widgets/resourcewidget.h" + +#include +#include +#include +#include + +ResourceDialog::ResourceDialog(QWidget *parent) + : QDialog(parent) +{ + setWindowTitle(tr("Resources")); + setSizeGripEnabled(true) ; + + QVBoxLayout *vlayout = new QVBoxLayout(); + m_resourceWidget = new ResourceWidget(this); + vlayout->addWidget(m_resourceWidget); + + // Button Box + QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Close); + buttonBox->button(QDialogButtonBox::Close)->setAutoDefault(false); + connect(buttonBox, SIGNAL(rejected()), this, SLOT(reject())); + // Convert button + QPushButton *convertButton = buttonBox->addButton(tr("Convert Selected"), + QDialogButtonBox::ActionRole); + connect(convertButton, SIGNAL(pressed()), this, SLOT(convert())); + vlayout->addWidget(buttonBox); + + setLayout(vlayout); +} + +void ResourceDialog::search(Mlt::Producer *producer) +{ + m_resourceWidget->search(producer); +} + +void ResourceDialog::add(Mlt::Producer *producer) +{ + m_resourceWidget->add(producer); +} + +void ResourceDialog::selectTroubleClips() +{ + m_resourceWidget->selectTroubleClips(); +} + +bool ResourceDialog::hasTroubleClips() +{ + return m_resourceWidget->hasTroubleClips(); +} + +void ResourceDialog::convert() +{ + QList producers(m_resourceWidget->getSelected()); + + // Only convert avformat producers + QMutableListIterator i(producers); + while (i.hasNext()) { + Mlt::Producer producer = i.next(); + if (!QString(producer.get("mlt_service")).startsWith("avformat")) + i.remove(); + } + + if (producers.length() < 1) { + QMessageBox::warning(this, windowTitle(), tr("No resources to convert")); + return; + } + + TranscodeDialog dialog( + tr("Choose an edit-friendly format below and then click OK to choose a file name. " + "After choosing a file name, a job is created. " + "When it is done, double-click the job to open it.\n"), + MLT.profile().progressive(), this); + dialog.setWindowTitle(tr("Convert...")); + dialog.setWindowModality(QmlApplication::dialogModality()); + dialog.set709Convert(true); + Transcoder transcoder; + transcoder.setProducers(producers); + transcoder.convert(dialog); + accept(); +} + +void ResourceDialog::showEvent(QShowEvent *event) +{ + m_resourceWidget->updateSize(); + resize(m_resourceWidget->width() + 4, m_resourceWidget->height()); + QDialog::showEvent(event); +} diff --git a/src/dialogs/resourcedialog.h b/src/dialogs/resourcedialog.h new file mode 100644 index 0000000000..8621290739 --- /dev/null +++ b/src/dialogs/resourcedialog.h @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2023 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 . + */ + +#ifndef RESOURCEDIALOG_H +#define RESOURCEDIALOG_H + +#include + +class ResourceWidget; + +namespace Mlt { +class Producer; +} + +class ResourceDialog : public QDialog +{ + Q_OBJECT +public: + explicit ResourceDialog(QWidget *parent = 0); + + void search(Mlt::Producer *producer); + void add(Mlt::Producer *producer); + void selectTroubleClips(); + bool hasTroubleClips(); + +private slots: + void convert(); + +protected: + virtual void showEvent(QShowEvent *event) override; + +private: + ResourceWidget *m_resourceWidget; +}; + +#endif // RESOURCEDIALOG_H diff --git a/src/docks/playlistdock.cpp b/src/docks/playlistdock.cpp index 8df0dcb903..b2b68ae833 100644 --- a/src/docks/playlistdock.cpp +++ b/src/docks/playlistdock.cpp @@ -21,6 +21,7 @@ #include "dialogs/durationdialog.h" #include "dialogs/filedatedialog.h" #include "dialogs/longuitask.h" +#include "dialogs/resourcedialog.h" #include "dialogs/slideshowgeneratordialog.h" #include "mainwindow.h" #include "settings.h" @@ -1075,6 +1076,7 @@ void PlaylistDock::onDropped(const QMimeData *data, int row) { bool resetIndex = true; if (data && data->hasUrls()) { + ResourceDialog dialog(this); LongUiTask longTask(tr("Add Files")); int insertNextAt = row; bool first = true; @@ -1114,7 +1116,9 @@ void PlaylistDock::onDropped(const QMimeData *data, int row) if (first) { first = false; if (!MLT.producer() || !MLT.producer()->is_valid()) { - MAIN.open(path, nullptr, false); + Mlt::Properties properties; + properties.set(kShotcutSkipConvertProperty, 1); + MAIN.open(path, &properties, false); if (MLT.producer() && MLT.producer()->is_valid()) { producer = MLT.producer(); first = true; @@ -1122,6 +1126,7 @@ void PlaylistDock::onDropped(const QMimeData *data, int row) } } producer = MLT.setupNewProducer(producer); + producer->set(kShotcutSkipConvertProperty, true); if (!MLT.isLiveProducer(producer) || producer->get_int(kShotcutVirtualClip)) { ProxyManager::generateIfNotExists(*producer); if (row == -1) @@ -1130,10 +1135,10 @@ void PlaylistDock::onDropped(const QMimeData *data, int row) MAIN.undoStack()->push(new Playlist::InsertCommand(m_model, MLT.XML(producer), insertNextAt++)); } else { LongUiTask::cancel(); - DurationDialog dialog(this); - dialog.setDuration(MLT.profile().fps() * 5); - if (dialog.exec() == QDialog::Accepted) { - producer->set_in_and_out(0, dialog.duration() - 1); + DurationDialog durationDialog(this); + durationDialog.setDuration(MLT.profile().fps() * 5); + if (durationDialog.exec() == QDialog::Accepted) { + producer->set_in_and_out(0, durationDialog.duration() - 1); if (row == -1) MAIN.undoStack()->push(new Playlist::AppendCommand(m_model, MLT.XML(producer))); else @@ -1145,9 +1150,16 @@ void PlaylistDock::onDropped(const QMimeData *data, int row) setIndex(0); resetIndex = false; } + dialog.add(producer); delete producer; } } + if (dialog.hasTroubleClips()) { + dialog.selectTroubleClips(); + dialog.setWindowTitle(tr("Dropped Files")); + longTask.cancel(); + dialog.exec(); + } } else if (data && data->hasFormat(Mlt::XmlMimeType)) { if (MLT.producer() && MLT.producer()->is_valid()) { if (MLT.producer()->type() == mlt_service_playlist_type) { diff --git a/src/docks/timelinedock.cpp b/src/docks/timelinedock.cpp index afb14aab65..c235dbf597 100644 --- a/src/docks/timelinedock.cpp +++ b/src/docks/timelinedock.cpp @@ -33,6 +33,7 @@ #include "dialogs/alignaudiodialog.h" #include "dialogs/editmarkerdialog.h" #include "dialogs/longuitask.h" +#include "dialogs/resourcedialog.h" #include "widgets/docktoolbar.h" #include @@ -2409,6 +2410,88 @@ void TimelineDock::initLoad() load(false); } +void TimelineDock::handleDrop(int trackIndex, int position, QString xml) +{ + if (xml.startsWith(kFileUrlProtocol)) { + // Handle drop from file manager to empty project. + if (!MLT.producer() || !MLT.producer()->is_valid()) { + QUrl url = xml.split(kFilesUrlDelimiter).first(); + Mlt::Properties properties; + properties.set(kShotcutSkipConvertProperty, 1); + if (!MAIN.open(Util::removeFileScheme(url), &properties, false /* play */)) + MAIN.open(Util::removeFileScheme(url, false), &properties, false /* play */); + } + + LongUiTask longTask(QObject::tr("Drop Files")); + Mlt::Playlist playlist(MLT.profile()); + QList urls; + auto strings = xml.split(kFilesUrlDelimiter); + for (auto &s : strings) { +#ifdef Q_OS_WIN + if (!s.startsWith(kFileUrlProtocol)) { + s.prepend(kFileUrlProtocol); + } +#endif + urls << s; + } + int i = 0, count = urls.size(); + ResourceDialog dialog(this); + for (const auto &path : Util::sortedFileList(urls)) { + if (MAIN.isSourceClipMyProject(path, /* withDialog */ false)) continue; + if (MLT.checkFile(path)) { + MAIN.showStatusMessage(QObject::tr("Failed to open ").append(path)); + continue; + } + longTask.reportProgress(Util::baseName(path), i++, count); + Mlt::Producer p; + if (path.endsWith(".mlt") || path.endsWith(".xml")) { + if (Settings.playerGPU() && MLT.profile().is_explicit()) { + Mlt::Profile testProfile; + Mlt::Producer producer(testProfile, path.toUtf8().constData()); + if (testProfile.width() != MLT.profile().width() + || testProfile.height() != MLT.profile().height() + || Util::isFpsDifferent(MLT.profile().fps(), testProfile.fps())) { + MAIN.showStatusMessage(QObject::tr("Failed to open ").append(path)); + continue; + } + } + p = Mlt::Producer(MLT.profile(), path.toUtf8().constData()); + if (p.is_valid()) { + p.set(kShotcutVirtualClip, 1); + p.set("resource", path.toUtf8().constData()); + } + } else { + p = Mlt::Producer(MLT.profile(), path.toUtf8().constData()); + } + if (p.is_valid()) { + if (!qstrcmp(p.get("mlt_service"), "avformat") && !p.get_int("seekable")) { + MAIN.showStatusMessage(QObject::tr("Not adding non-seekable file: ") + Util::baseName(path)); + continue; + } + Mlt::Producer *producer = MLT.setupNewProducer(&p); + producer->set(kShotcutSkipConvertProperty, true); + ProxyManager::generateIfNotExists(*producer); + playlist.append(*producer); + dialog.add(producer); + delete producer; + } + } + xml = MLT.XML(&playlist); + if (dialog.hasTroubleClips()) { + dialog.selectTroubleClips(); + dialog.setWindowTitle(tr("Dropped Files")); + longTask.cancel(); + dialog.exec(); + } + } + + if (Settings.timelineRipple()) { + insert(trackIndex, position, xml, false); + } else { + overwrite(trackIndex, position, xml, false); + } +} + void TimelineDock::setTrackName(int trackIndex, const QString &value) { MAIN.undoStack()->push( @@ -2638,65 +2721,6 @@ bool TimelineDock::trimClipOut(int trackIndex, int clipIndex, int delta, bool ri return true; } -static QString convertUrlsToXML(const QString &xml) -{ - if (xml.startsWith(kFileUrlProtocol)) { - LongUiTask longTask(QObject::tr("Drop Files")); - Mlt::Playlist playlist(MLT.profile()); - QList urls; - auto strings = xml.split(kFilesUrlDelimiter); - for (auto &s : strings) { -#ifdef Q_OS_WIN - if (!s.startsWith(kFileUrlProtocol)) { - s.prepend(kFileUrlProtocol); - } -#endif - urls << s; - } - int i = 0, count = urls.size(); - for (const auto &path : Util::sortedFileList(urls)) { - if (MAIN.isSourceClipMyProject(path, /* withDialog */ false)) continue; - if (MLT.checkFile(path)) { - MAIN.showStatusMessage(QObject::tr("Failed to open ").append(path)); - continue; - } - longTask.reportProgress(Util::baseName(path), i++, count); - Mlt::Producer p; - if (path.endsWith(".mlt") || path.endsWith(".xml")) { - if (Settings.playerGPU() && MLT.profile().is_explicit()) { - Mlt::Profile testProfile; - Mlt::Producer producer(testProfile, path.toUtf8().constData()); - if (testProfile.width() != MLT.profile().width() - || testProfile.height() != MLT.profile().height() - || Util::isFpsDifferent(MLT.profile().fps(), testProfile.fps())) { - MAIN.showStatusMessage(QObject::tr("Failed to open ").append(path)); - continue; - } - } - p = Mlt::Producer(MLT.profile(), path.toUtf8().constData()); - if (p.is_valid()) { - p.set(kShotcutVirtualClip, 1); - p.set("resource", path.toUtf8().constData()); - } - } else { - p = Mlt::Producer(MLT.profile(), path.toUtf8().constData()); - } - if (p.is_valid()) { - if (!qstrcmp(p.get("mlt_service"), "avformat") && !p.get_int("seekable")) { - MAIN.showStatusMessage(QObject::tr("Not adding non-seekable file: ") + Util::baseName(path)); - continue; - } - Mlt::Producer *producer = MLT.setupNewProducer(&p); - ProxyManager::generateIfNotExists(*producer); - playlist.append(*producer); - delete producer; - } - } - return MLT.XML(&playlist); - } - return xml; -} - void TimelineDock::insert(int trackIndex, int position, const QString &xml, bool seek) { // Validations @@ -2708,15 +2732,6 @@ void TimelineDock::insert(int trackIndex, int position, const QString &xml, bool } if (xml.contains(MAIN.fileName()) && MAIN.isSourceClipMyProject()) return; - // Handle drop from file manager to empty project. - if ((!MLT.producer() || !MLT.producer()->is_valid()) && xml.startsWith(kFileUrlProtocol)) { - QUrl url = xml.split(kFilesUrlDelimiter).first(); - Mlt::Properties properties; - properties.set(kShotcutSkipConvertProperty, 1); - if (!MAIN.open(Util::removeFileScheme(url), &properties, false /* play */)) - MAIN.open(Util::removeFileScheme(url, false), &properties, false /* play */); - } - // Use MLT XML on the clipboard if it exists and is newer than source clip. QString xmlToUse = QGuiApplication::clipboard()->text(); if (isSystemClipboardValid(xmlToUse)) { @@ -2734,11 +2749,8 @@ void TimelineDock::insert(int trackIndex, int position, const QString &xml, bool ProxyManager::generateIfNotExists(producer); xmlToUse = MLT.XML(&producer); } else if (!xml.isEmpty()) { - // Convert a list of file URLs from the xml arg to MLT XML - xmlToUse = convertUrlsToXML(xml); - if (xml.startsWith(kFileUrlProtocol) && xml.split(kFilesUrlDelimiter).size() > 1) { - selectBlocker.reset(new TimelineSelectionBlocker(*this)); - } + xmlToUse = xml; + selectBlocker.reset(new TimelineSelectionBlocker(*this)); } if (xmlToUse.isEmpty()) { return; @@ -2835,15 +2847,6 @@ void TimelineDock::overwrite(int trackIndex, int position, const QString &xml, b } if (xml.contains(MAIN.fileName()) && MAIN.isSourceClipMyProject()) return; - // Handle drop from file manager to empty project. - if ((!MLT.producer() || !MLT.producer()->is_valid()) && xml.startsWith(kFileUrlProtocol)) { - QUrl url = xml.split(kFilesUrlDelimiter).first(); - Mlt::Properties properties; - properties.set(kShotcutSkipConvertProperty, 1); - if (!MAIN.open(Util::removeFileScheme(url), &properties, false /* play */)) - MAIN.open(Util::removeFileScheme(url, false), &properties, false /* play */); - } - // Use MLT XML on the clipboard if it exists and is newer than source clip. QString xmlToUse = QGuiApplication::clipboard()->text(); if (isSystemClipboardValid(xmlToUse)) { @@ -2861,10 +2864,8 @@ void TimelineDock::overwrite(int trackIndex, int position, const QString &xml, b ProxyManager::generateIfNotExists(producer); xmlToUse = MLT.XML(&producer); } else if (!xml.isEmpty()) { - xmlToUse = convertUrlsToXML(xml); - if (xml.startsWith(kFileUrlProtocol) && xml.split(kFilesUrlDelimiter).size() > 1) { - selectBlocker.reset(new TimelineSelectionBlocker(*this)); - } + xmlToUse = xml; + selectBlocker.reset(new TimelineSelectionBlocker(*this)); } if (position < 0) { position = qMax(m_position, 0); @@ -2912,7 +2913,6 @@ void TimelineDock::overwrite(int trackIndex, int position, const QString &xml, b position = 0; addVideoTrack(); } - MAIN.undoStack()->push( new Timeline::OverwriteCommand(m_model, trackIndex, position, xmlToUse, seek)); } diff --git a/src/docks/timelinedock.h b/src/docks/timelinedock.h index 125b4749b9..46d88c6fc1 100644 --- a/src/docks/timelinedock.h +++ b/src/docks/timelinedock.h @@ -206,6 +206,7 @@ public slots: void trimClipIn(bool ripple = false); void trimClipOut(bool ripple = false); void initLoad(); + void handleDrop(int trackIndex, int position, QString xml); protected: void dragEnterEvent(QDragEnterEvent *event); diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 60b72d8a09..27cdf3a305 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -52,6 +52,7 @@ #include "docks/filtersdock.h" #include "dialogs/actionsdialog.h" #include "dialogs/customprofiledialog.h" +#include "dialogs/resourcedialog.h" #include "dialogs/saveimagedialog.h" #include "settings.h" #include "database.h" @@ -80,6 +81,7 @@ #include "dialogs/longuitask.h" #include "dialogs/systemsyncdialog.h" #include "proxymanager.h" +#include "transcoder.h" #include "models/motiontrackermodel.h" #if defined(Q_OS_WIN) && (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) #include "windowstools.h" @@ -134,6 +136,7 @@ MainWindow::MainWindow() , m_exitCode(EXIT_SUCCESS) , m_upgradeUrl("https://www.shotcut.org/download/") , m_keyframesDock(0) + , m_multipleFilesLoading(false) { #if defined(Q_OS_UNIX) && !defined(Q_OS_MAC) QLibrary libJack("libjack.so.0"); @@ -288,6 +291,7 @@ void MainWindow::setupAndConnectPlayerWidget() connect(m_player, SIGNAL(outChanged(int)), this, SLOT(onCutModified())); connect(m_player, SIGNAL(tabIndexChanged(int)), SLOT(onPlayerTabIndexChanged(int))); connect(MLT.videoWidget(), SIGNAL(started()), SLOT(processMultipleFiles())); + connect(MLT.videoWidget(), SIGNAL(started()), SLOT(processSingleFile())); connect(MLT.videoWidget(), SIGNAL(paused()), m_player, SLOT(showPaused())); connect(MLT.videoWidget(), SIGNAL(playing()), m_player, SLOT(showPlaying())); connect(MLT.videoWidget(), SIGNAL(toggleZoom(bool)), m_player, SLOT(toggleZoom(bool))); @@ -661,6 +665,7 @@ void MainWindow::setupAndConnectDocks() void MainWindow::setupMenuView() { ui->menuView->addSeparator(); + ui->menuView->addAction(ui->actionResources); ui->menuView->addAction(ui->actionApplicationLog); } @@ -1495,7 +1500,7 @@ void MainWindow::onAutosaveTimeout() } } -bool MainWindow::open(QString url, const Mlt::Properties *properties, bool play) +bool MainWindow::open(QString url, const Mlt::Properties *properties, bool play, bool skipConvert) { // returns false when MLT is unable to open the file, possibly because it has percent sign in the path LOG_DEBUG() << url; @@ -1570,7 +1575,7 @@ bool MainWindow::open(QString url, const Mlt::Properties *properties, bool play) MLT.profile().set_explicit(false); } QString urlToOpen = checker.isUpdated() ? checker.tempFile().fileName() : url; - if (!MLT.open(QDir::fromNativeSeparators(urlToOpen), QDir::fromNativeSeparators(url)) + if (!MLT.open(QDir::fromNativeSeparators(urlToOpen), QDir::fromNativeSeparators(url), skipConvert) && MLT.producer() && MLT.producer()->is_valid()) { Mlt::Properties *props = const_cast(properties); if (props && props->is_valid()) @@ -1625,7 +1630,7 @@ void MainWindow::openMultiple(const QList &urls) { if (urls.size() > 1) { m_multipleFiles = Util::sortedFileList(Util::expandDirectories(urls)); - open(m_multipleFiles.first()); + open(m_multipleFiles.first(), nullptr, true, true); } else if (urls.size() > 0) { QUrl url = urls.first(); if (!open(Util::removeFileScheme(url))) @@ -1649,7 +1654,7 @@ void MainWindow::openVideo() activateWindow(); if (filenames.length() > 1) m_multipleFiles = filenames; - open(filenames.first()); + open(filenames.first(), nullptr, true, filenames.length() > 1); } else { // If file invalid, then on some platforms the dialog messes up SDL. MLT.onWindowResize(); @@ -3128,7 +3133,7 @@ QWidget *MainWindow::loadProducerWidget(Mlt::Producer *producer) } if (-1 != w->metaObject()->indexOfSlot("offerConvert(QString)")) { connect(m_filterController->attachedModel(), SIGNAL(requestConvert(QString, bool, bool)), w, - SLOT(offerConvert(QString, bool, bool)), Qt::QueuedConnection); + SLOT(offerConvert(QString, bool)), Qt::QueuedConnection); } scrollArea->setWidget(w); onProducerChanged(); @@ -3289,9 +3294,11 @@ void MainWindow::processMultipleFiles() m_multipleFiles.clear(); int count = multipleFiles.length(); if (count > 1) { + m_multipleFilesLoading = true; LongUiTask longTask(tr("Open Files")); m_playlistDock->show(); m_playlistDock->raise(); + ResourceDialog dialog(this); for (int i = 0; i < count; i++) { QString filename = multipleFiles.takeFirst(); LOG_DEBUG() << filename; @@ -3305,12 +3312,20 @@ void MainWindow::processMultipleFiles() Util::getHash(p); Mlt::Producer *producer = MLT.setupNewProducer(&p); ProxyManager::generateIfNotExists(*producer); + producer->set(kShotcutSkipConvertProperty, true); undoStack()->push(new Playlist::AppendCommand(*m_playlistDock->model(), MLT.XML(producer), false)); m_recentDock->add(filename.toUtf8().constData()); + dialog.add(producer); delete producer; } } emit m_playlistDock->model()->modified(); + if (dialog.hasTroubleClips()) { + dialog.selectTroubleClips(); + dialog.setWindowTitle(tr("Opened Files")); + dialog.exec(); + } + m_multipleFilesLoading = false; } if (m_isPlaylistLoaded && Settings.playerGPU()) { updateThumbnails(); @@ -3318,6 +3333,37 @@ void MainWindow::processMultipleFiles() } } +void MainWindow::processSingleFile() +{ + if (!m_multipleFilesLoading && Settings.showConvertClipDialog() + && !MLT.producer()->get_int(kShotcutSkipConvertProperty)) { + int trc = MLT.producer()->get_int("meta.media.color_trc"); + QString convertAdvice = Util::getConversionAdvice(MLT.producer()); + if (!convertAdvice.isEmpty()) { + MLT.producer()->set(kShotcutSkipConvertProperty, true); + LongUiTask::cancel(); + MLT.pause(); + TranscodeDialog dialog(convertAdvice.append( + tr(" Do you want to convert it to an edit-friendly format?\n\n" + "If yes, choose a format below and then click OK to choose a file name. " + "After choosing a file name, a job is created. " + "When it is done, it automatically replaces clips, or you can double-click the job to open it.\n")), + MLT.producer()->get_int("progressive"), this); + dialog.setWindowModality(QmlApplication::dialogModality()); + dialog.showCheckBox(); + dialog.set709Convert(!Util::trcIsCompatible(MLT.producer()->get_int("meta.media.color_trc"))); + dialog.showSubClipCheckBox(); + LOG_DEBUG() << "in" << MLT.producer()->get_in() << "out" << MLT.producer()->get_out() << "length" << + MLT.producer()->get_length() - 1; + dialog.setSubClipChecked(MLT.producer()->get_in() > 0 + || MLT.producer()->get_out() < MLT.producer()->get_length() - 1); + Transcoder transcoder; + transcoder.addProducer(MLT.producer()); + transcoder.convert(dialog); + } + } +} + void MainWindow::onLanguageTriggered(QAction *action) { Settings.setLanguage(action->data().toString()); @@ -3744,6 +3790,14 @@ void MainWindow::onDrawingMethodTriggered(QAction *action) } #endif +void MainWindow::on_actionResources_triggered() +{ + ResourceDialog dialog(this); + dialog.search(multitrack()); + dialog.search(playlist()); + dialog.exec(); +} + void MainWindow::on_actionApplicationLog_triggered() { TextViewerDialog dialog(this); diff --git a/src/mainwindow.h b/src/mainwindow.h index 13be1d1144..031a990516 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -204,6 +204,7 @@ class MainWindow : public QMainWindow QMenu *m_customProfileMenu; QMenu *m_keyerMenu; QStringList m_multipleFiles; + bool m_multipleFilesLoading; bool m_isPlaylistLoaded; QActionGroup *m_languagesGroup; QSharedPointer m_autosaveFile; @@ -223,7 +224,8 @@ class MainWindow : public QMainWindow public slots: bool isCompatibleWithGpuMode(MltXmlChecker &checker); bool isXmlRepaired(MltXmlChecker &checker, QString &fileName); - bool open(QString url, const Mlt::Properties * = nullptr, bool play = true); + bool open(QString url, const Mlt::Properties * = nullptr, bool play = true, + bool skipConvert = false); void openMultiple(const QStringList &paths); void openMultiple(const QList &urls); void openVideo(); @@ -303,6 +305,7 @@ private slots: void onProfileChanged(); void on_actionAddCustomProfile_triggered(); void processMultipleFiles(); + void processSingleFile(); void onLanguageTriggered(QAction *); void on_actionSystemTheme_triggered(); void on_actionFusionDark_triggered(); @@ -327,6 +330,7 @@ private slots: #if !defined(Q_OS_MAC) void onDrawingMethodTriggered(QAction *); #endif + void on_actionResources_triggered(); void on_actionApplicationLog_triggered(); void on_actionClose_triggered(); void onPlayerTabIndexChanged(int index); diff --git a/src/mainwindow.ui b/src/mainwindow.ui index e7ca187ee8..f3b2422429 100644 --- a/src/mainwindow.ui +++ b/src/mainwindow.ui @@ -839,6 +839,11 @@ Normal + + + Resources... + + Application Log... diff --git a/src/mltcontroller.cpp b/src/mltcontroller.cpp index 2cec90abd3..c4a2c163b4 100644 --- a/src/mltcontroller.cpp +++ b/src/mltcontroller.cpp @@ -117,7 +117,7 @@ int Controller::setProducer(Mlt::Producer *producer, bool) return error; } -int Controller::open(const QString &url, const QString &urlToSave) +int Controller::open(const QString &url, const QString &urlToSave, bool skipConvert) { int error = checkFile(url); if (error) { @@ -175,6 +175,7 @@ int Controller::open(const QString &url, const QString &urlToSave) m_url = urlToSave; } Producer *producer = setupNewProducer(newProducer); + producer->set(kShotcutSkipConvertProperty, skipConvert); delete newProducer; newProducer = producer; } else { diff --git a/src/mltcontroller.h b/src/mltcontroller.h index c350e134e3..736e03e275 100644 --- a/src/mltcontroller.h +++ b/src/mltcontroller.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011-2022 Meltytech, LLC + * Copyright (c) 2011-2023 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 @@ -67,7 +67,7 @@ class Controller virtual QObject *videoWidget() = 0; virtual int setProducer(Mlt::Producer *, bool isMulti = false); - virtual int open(const QString &url, const QString &urlToSave); + virtual int open(const QString &url, const QString &urlToSave, bool skipConvert = false); bool openXML(const QString &filename); virtual void close(); virtual int displayWidth() const = 0; diff --git a/src/models/resourcemodel.cpp b/src/models/resourcemodel.cpp new file mode 100644 index 0000000000..b7b7af85c4 --- /dev/null +++ b/src/models/resourcemodel.cpp @@ -0,0 +1,380 @@ +/* + * Copyright (c) 2023 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 . + */ + +#include "resourcemodel.h" + +#include "util.h" +#include "Logger.h" +#include "mainwindow.h" + +class ProducerFinder : public Mlt::Parser +{ +public: + ProducerFinder(ResourceModel *model) + : Mlt::Parser() + , m_model(model) + {} + + int on_start_producer(Mlt::Producer *producer) + { + m_model->add(producer); + return 0; + } + + int on_start_filter(Mlt::Filter *) + { + return 0; + } + int on_end_producer(Mlt::Producer *) + { + return 0; + } + int on_start_playlist(Mlt::Playlist *) + { + return 0; + } + int on_end_playlist(Mlt::Playlist *) + { + return 0; + } + int on_start_tractor(Mlt::Tractor *) + { + return 0; + } + int on_end_tractor(Mlt::Tractor *) + { + return 0; + } + int on_start_multitrack(Mlt::Multitrack *) + { + return 0; + } + int on_end_multitrack(Mlt::Multitrack *) + { + return 0; + } + int on_start_track() + { + return 0; + } + int on_end_track() + { + return 0; + } + int on_end_filter(Mlt::Filter *) + { + return 0; + } + int on_start_transition(Mlt::Transition *) + { + return 0; + } + int on_end_transition(Mlt::Transition *) + { + return 0; + } + int on_start_chain(Mlt::Chain *) + { + return 0; + } + int on_end_chain(Mlt::Chain *) + { + return 0; + } + int on_start_link(Mlt::Link *) + { + return 0; + } + int on_end_link(Mlt::Link *) + { + return 0; + } +private: + ResourceModel *m_model; +}; + +ResourceModel::ResourceModel(QObject *parent) + : QAbstractItemModel(parent) +{ +} + +ResourceModel::~ResourceModel() +{ +} + +void ResourceModel::search(Mlt::Producer *producer) +{ + if (!producer) { + return; + } + ProducerFinder parser(this); + parser.start(*producer); +} + +void ResourceModel::add(Mlt::Producer *producer) +{ + if (producer->is_blank()) { + // Do not add + } else if (producer->is_cut()) { + Mlt::Producer parent = producer->parent(); + QString hash = Util::getHash(parent); + if (!hash.isEmpty() && !exists(hash)) { + beginInsertRows(QModelIndex(), m_producers.size(), m_producers.size()); + m_producers.append(parent); + endInsertRows(); + } + } else { + QString hash = Util::getHash(*producer); + if (!hash.isEmpty() && !exists(hash)) { + beginInsertRows(QModelIndex(), m_producers.size(), m_producers.size()); + m_producers.append(*producer); + endInsertRows(); + } + } +} + +QList ResourceModel::getProducers(const QModelIndexList &indices) +{ + QList producers; + foreach (auto index, indices) { + int row = index.row(); + if (row >= 0 && row < m_producers.size()) { + producers << m_producers[row]; + } + } + return producers; +} + +bool ResourceModel::exists(const QString &hash) +{ + for ( int i = 0; i < m_producers.count(); ++i ) { + if (Util::getHash(m_producers[i]) == hash) { + return true; + } + } + return false; +} + +int ResourceModel::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + return m_producers.size(); +} + +int ResourceModel::columnCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + return COLUMN_COUNT; +} + +QVariant ResourceModel::data(const QModelIndex &index, int role) const +{ + QVariant result; + + switch (role) { + case Qt::StatusTipRole: + case Qt::FontRole: + case Qt::SizeHintRole: + case Qt::CheckStateRole: + case Qt::BackgroundRole: + case Qt::ForegroundRole: + return result; + } + + if (!index.isValid() || index.column() < 0 || index.column() >= COLUMN_COUNT || index.row() < 0 + || index.row() >= m_producers.size()) { + LOG_ERROR() << "Invalid Index: " << index.row() << index.column() << role; + return result; + } + + Mlt::Producer *producer = const_cast( &m_producers[index.row()] ); + switch (role) { + case Qt::DisplayRole: + switch (index.column()) { + case COLUMN_INFO: + break; + case COLUMN_NAME: { + QString path = Util::GetFilenameFromProducer(producer, true); + QFileInfo info(path); + result = info.fileName(); + break; + } + case COLUMN_SIZE: { + QString path = Util::GetFilenameFromProducer(producer, true); + QFileInfo info(path); + double size = (double)info.size() / (double)(1024 * 1024); + result = QString(tr("%1MB")).arg(QLocale().toString(size, 'f', 2)); + break; + } + case COLUMN_VID_DESCRIPTION: { + int width = producer->get_int("meta.media.width"); + int height = producer->get_int("meta.media.height"); + if (producer->get_int("video_index") >= 0) { + double frame_rate_num = producer->get_double("meta.media.frame_rate_num"); + double frame_rate_den = producer->get_double("meta.media.frame_rate_den"); + if ( width && height && frame_rate_num && frame_rate_den + && (frame_rate_num / frame_rate_den) < 1000) { + int index = producer->get_int("video_index"); + QString key = QString("meta.media.%1.codec.name").arg(index); + QString codec(producer->get(key.toLatin1().constData())); + double frame_rate = frame_rate_num / frame_rate_den; + result = QString(tr("%1 %2x%3 %4fps")) + .arg(codec) + .arg(width) + .arg(height) + .arg(QLocale().toString(frame_rate, 'f', 2)); + } + } + if (result.isNull() && width > 0 && height > 0 ) { + result = QString(QObject::tr("%1x%2")) + .arg(width) + .arg(height); + } + break; + } + case COLUMN_AUD_DESCRIPTION: { + if (producer->get_int("audio_index") >= 0) { + int index = producer->get_int("audio_index"); + QString key = QString("meta.media.%1.codec.name").arg(index); + QString codec(producer->get(key.toLatin1().constData())); + if (!codec.isEmpty()) { + key = QString("meta.media.%1.codec.channels").arg(index); + int channels(producer->get_int(key.toLatin1().constData())); + key = QString("meta.media.%1.codec.sample_rate").arg(index); + QString sampleRate(producer->get(key.toLatin1().constData())); + result = QString("%1 %2ch %3KHz") + .arg(codec) + .arg(channels) + .arg(sampleRate.toDouble() / 1000); + } + } + break; + } + default: + LOG_ERROR() << "Invalid DisplayRole Column" << index.row() << index.column() << roleNames()[role] << + role; + break; + } + break; + case Qt::ToolTipRole: + switch (index.column()) { + case COLUMN_INFO: + result = Util::getConversionAdvice(producer); + break; + case COLUMN_NAME: + case COLUMN_VID_DESCRIPTION: + case COLUMN_AUD_DESCRIPTION: + case COLUMN_SIZE: + result = Util::GetFilenameFromProducer(producer, true); + break; + default: + LOG_ERROR() << "Invalid ToolTipRole Column" << index.row() << index.column() << roleNames()[role] << + role; + break; + } + break; + case Qt::TextAlignmentRole: + switch (index.column()) { + case COLUMN_INFO: + case COLUMN_NAME: + case COLUMN_VID_DESCRIPTION: + case COLUMN_AUD_DESCRIPTION: + result = Qt::AlignLeft; + break; + case COLUMN_SIZE: + result = Qt::AlignRight; + break; + default: + LOG_ERROR() << "Invalid TextAlignmentRole Column" << index.row() << index.column() << + roleNames()[role] << role; + break; + } + break; + case Qt::DecorationRole: + switch (index.column()) { + case COLUMN_INFO: + if (!Util::getConversionAdvice(producer).isEmpty()) { + result = QIcon(":/icons/oxygen/32x32/status/task-attempt.png"); + } else { + result = QIcon(":/icons/oxygen/32x32/status/task-complete.png"); + } + break; + case COLUMN_NAME: + case COLUMN_VID_DESCRIPTION: + case COLUMN_AUD_DESCRIPTION: + case COLUMN_SIZE: + break; + default: + LOG_ERROR() << "Invalid DecorationRole Column" << index.row() << index.column() << roleNames()[role] + << role; + break; + } + break; + default: + LOG_ERROR() << "Invalid Role" << index.row() << index.column() << roleNames()[role] << role; + break; + } + return result; +} + +QVariant ResourceModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + switch (role) { + case Qt::DisplayRole: { + switch (section) { + case COLUMN_NAME: + return tr("Name"); + case COLUMN_SIZE: + return tr("Size"); + case COLUMN_VID_DESCRIPTION: + return tr("Video"); + case COLUMN_AUD_DESCRIPTION: + return tr("Audio"); + default: + break; + } + } + case Qt::TextAlignmentRole: + switch (section) { + case COLUMN_NAME: + return Qt::AlignLeft; + case COLUMN_VID_DESCRIPTION: + case COLUMN_AUD_DESCRIPTION: + case COLUMN_SIZE: + return Qt::AlignCenter; + default: + break; + } + break; + } + + return QVariant(); +} + +QModelIndex ResourceModel::index(int row, int column, const QModelIndex &parent) const +{ + Q_UNUSED(parent) + if (column < 0 || column >= COLUMN_COUNT || row < 0 || row >= m_producers.size()) + return QModelIndex(); + return createIndex(row, column, (int)0); +} + +QModelIndex ResourceModel::parent(const QModelIndex &index) const +{ + Q_UNUSED(index) + return QModelIndex(); +} diff --git a/src/models/resourcemodel.h b/src/models/resourcemodel.h new file mode 100644 index 0000000000..9fd6eec03c --- /dev/null +++ b/src/models/resourcemodel.h @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2023 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 . + */ + +#ifndef RESOURCEMODEL_H +#define RESOURCEMODEL_H + +#include + +#include + +class ResourceModel : public QAbstractItemModel +{ + Q_OBJECT + +public: + + enum Columns { + COLUMN_INFO = 0, + COLUMN_NAME, + COLUMN_SIZE, + COLUMN_VID_DESCRIPTION, + COLUMN_AUD_DESCRIPTION, + COLUMN_COUNT, + }; + + explicit ResourceModel(QObject *parent = 0); + virtual ~ResourceModel(); + void search(Mlt::Producer *producer); + void add(Mlt::Producer *producer); + QList getProducers(const QModelIndexList &indices); + bool exists(const QString &hash); + // Implement QAbstractItemModel + int rowCount(const QModelIndex &parent) const; + int columnCount(const QModelIndex &parent) const; + QVariant data(const QModelIndex &index, int role) const; + QVariant headerData(int section, Qt::Orientation orientation, int role) const; + QModelIndex index(int row, int column = 0, const QModelIndex &parent = QModelIndex()) const; + QModelIndex parent(const QModelIndex &index) const; + +private: + + QList m_producers; +}; + +#endif // RESOURCEMODEL_H diff --git a/src/qml/views/timeline/Timeline.js b/src/qml/views/timeline/Timeline.js index fd9d8fc0ff..6f5e5bf0e4 100644 --- a/src/qml/views/timeline/Timeline.js +++ b/src/qml/views/timeline/Timeline.js @@ -106,10 +106,7 @@ function dropped() { function acceptDrop(xml) { var position = Math.round((dropTarget.x + tracksFlickable.contentX - headerWidth) / multitrack.scaleFactor) - if (settings.timelineRipple) - timeline.insert(timeline.currentTrack, position, xml, false) - else - timeline.overwrite(timeline.currentTrack, position, xml, false) + timeline.handleDrop(timeline.currentTrack, position, xml) } function trackHeight(isAudio) { diff --git a/src/transcoder.cpp b/src/transcoder.cpp new file mode 100644 index 0000000000..7eb453a10b --- /dev/null +++ b/src/transcoder.cpp @@ -0,0 +1,296 @@ +/* + * Copyright (c) 2023 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 . + */ + +#include "transcoder.h" + +#include "jobqueue.h" +#include "jobs/ffmpegjob.h" +#include "mainwindow.h" +#include "util.h" +#include "settings.h" +#include "shotcut_mlt_properties.h" + +#include +#include + +static const auto kHandleSeconds = 15.0; + +void Transcoder::setProducers(QList &producers) +{ + m_producers = producers; +} + +void Transcoder::addProducer(Mlt::Producer &producer) +{ + m_producers.append(producer); +} + +void Transcoder::addProducer(Mlt::Producer *producer) +{ + m_producers.append(*producer); +} + +void Transcoder::convert(TranscodeDialog &dialog) +{ + int result = dialog.exec(); + if (dialog.isCheckBoxChecked()) { + Settings.setShowConvertClipDialog(false); + } + if (result != QDialog::Accepted) { + return; + } + + QString path = Settings.savePath(); + QString suffix = dialog.isSubClip() ? tr("Sub-clip") + ' ' : tr("Converted"); + QString filename; + QString nameFormat; + QString nameFilter; + + switch (dialog.format()) { + case 0: + nameFormat = "/%1 - %2.mp4"; + nameFilter = tr("MP4 (*.mp4);;All Files (*)"); + break; + case 1: + nameFormat = "/%1 - %2.mov"; + nameFilter = tr("MOV (*.mov);;All Files (*)"); + break; + case 2: + nameFormat = "/%1 - %2.mkv"; + nameFilter = tr("MKV (*.mkv);;All Files (*)"); + break; + } + + if (m_producers.length() == 1) { + QString resource = Util::GetFilenameFromProducer(&m_producers[0]); + QFileInfo fi(resource); + filename = path + nameFormat.arg(fi.completeBaseName(), suffix); + if (dialog.isSubClip()) { + filename = Util::getNextFile(path); + } + filename = QFileDialog::getSaveFileName(MAIN.centralWidget(), dialog.windowTitle(), filename, + nameFilter, + nullptr, Util::getFileDialogOptions()); + if (!filename.isEmpty()) { + if (filename == QDir::toNativeSeparators(resource)) { + QMessageBox::warning(MAIN.centralWidget(), dialog.windowTitle(), + QObject::tr("Unable to write file %1\n" + "Perhaps you do not have permission.\n" + "Try again with a different folder.") + .arg(fi.fileName())); + return; + } + if (Util::warnIfNotWritable(filename, MAIN.centralWidget(), dialog.windowTitle())) + return; + + if (Util::warnIfLowDiskSpace(filename)) { + MAIN.showStatusMessage(tr("Convert canceled")); + return; + } + } + convertProducer(&m_producers[0], dialog, filename); + } else if (m_producers.length() > 1) { + path = QFileDialog::getExistingDirectory(MAIN.centralWidget(), dialog.windowTitle(), path, + Util::getFileDialogOptions()); + if (path.isEmpty()) { + MAIN.showStatusMessage(tr("Convert canceled")); + return; + } + if (Util::warnIfNotWritable(path, MAIN.centralWidget(), dialog.windowTitle())) { + return; + } + if (Util::warnIfLowDiskSpace(path)) { + MAIN.showStatusMessage(tr("Convert canceled")); + return; + } + + for (auto &producer : m_producers) { + QString resource = Util::GetFilenameFromProducer(&producer); + QFileInfo fi(resource); + filename = path + nameFormat.arg(fi.completeBaseName(), suffix); + filename = Util::getNextFile(filename); + convertProducer(&producer, dialog, filename); + } + } + Settings.setSavePath(QFileInfo(filename).path()); +} + +void Transcoder::convertProducer(Mlt::Producer *producer, TranscodeDialog &dialog, QString filename) +{ + QString resource = Util::GetFilenameFromProducer(producer); + QStringList args; + int in = -1; + + args << "-loglevel" << "verbose"; + args << "-i" << resource; + args << "-max_muxing_queue_size" << "9999"; + + if (dialog.isSubClip()) { + if (Settings.proxyEnabled()) { + producer->Mlt::Properties::clear(kOriginalResourceProperty); + } + + // set trim options + if (producer->get(kFilterInProperty)) { + in = producer->get_int(kFilterInProperty); + int ss = qMax(0, in - qRound(producer->get_fps() * kHandleSeconds)); + auto s = QString::fromLatin1(producer->frames_to_time(ss, mlt_time_clock)); + args << "-ss" << s.replace(',', '.'); + in -= ss; + } else { + args << "-ss" << QString::fromLatin1(producer->get_time("in", mlt_time_clock)).replace(',', + '.').replace(',', '.'); + } + if (producer->get(kFilterOutProperty)) { + int out = producer->get_int(kFilterOutProperty); + int to = qMin(producer->get_playtime() - 1, out + qRound(producer->get_fps() * kHandleSeconds)); + auto s = QString::fromLatin1(producer->frames_to_time(to, mlt_time_clock)); + args << "-to" << s.replace(',', '.'); + } else { + args << "-to" << QString::fromLatin1(producer->get_time("out", mlt_time_clock)).replace(',', '.'); + } + } + + // transcode all streams except data, subtitles, and attachments + auto audioIndex = producer->property_exists(kDefaultAudioIndexProperty) ? producer->get_int( + kDefaultAudioIndexProperty) : producer->get_int("audio_index"); + if (producer->get_int("video_index") < audioIndex) { + args << "-map" << "0:V?" << "-map" << "0:a?"; + } else { + args << "-map" << "0:a?" << "-map" << "0:V?"; + } + args << "-map_metadata" << "0" << "-ignore_unknown"; + + // Set Sample rate if different than source + if ( !dialog.sampleRate().isEmpty() ) { + args << "-ar" << dialog.sampleRate(); + } + + // Set video filters + args << "-vf"; + QString filterString; + if (dialog.deinterlace()) { + QString deinterlaceFilter = QString("bwdif,"); + filterString = filterString + deinterlaceFilter; + } + + QString color_range; + if (producer->get("color_range")) { + if (producer->get_int("color_range") == 2) { + color_range = "full"; + } else { + color_range = "mpeg"; + } + } else if (producer->get("force_full_range")) { + if (producer->get_int("force_full_range")) { + color_range = "full"; + } else { + color_range = "mpeg"; + } + } else { + color_range = producer->get("meta.media.color_range"); + } + if (color_range != "full" && color_range != "mpeg") { + color_range = "mpeg"; + } + + if (dialog.get709Convert() && !Util::trcIsCompatible(producer->get_int("meta.media.color_trc"))) { + QString convertFilter = + QString("zscale=t=linear:npl=100,format=gbrpf32le,zscale=p=bt709,tonemap=tonemap=hable:desat=0,zscale=t=bt709:m=bt709:r=tv,format=yuv422p,"); + filterString = filterString + convertFilter; + } + filterString = filterString + + QString("scale=flags=accurate_rnd+full_chroma_inp+full_chroma_int:in_range=%1:out_range=%2").arg( + color_range).arg(color_range); + if (dialog.fpsOverride()) { + auto fps = QString("%1").arg(dialog.fps(), 0, 'f', 6); + int numerator, denominator; + Util::normalizeFrameRate(dialog.fps(), numerator, denominator); + if (denominator == 1001) { + fps = QString("%1/%2").arg(numerator).arg(denominator); + } + QString minterpFilter = + QString(",minterpolate='mi_mode=%1:mc_mode=aobmc:me_mode=bidir:vsbmc=1:fps=%2'").arg( + dialog.frc()).arg(fps); + filterString = filterString + minterpFilter; + } + args << filterString; + + // Specify color range + if (color_range == "full") { + args << "-color_range" << "2"; + } else { + args << "-color_range" << "1"; + } + + int progressive = producer->get_int("meta.media.progressive") + || producer->get_int("force_progressive"); + if (!dialog.deinterlace() && !progressive) { + int tff = producer->get_int("meta.media.top_field_first") || producer->get_int("force_tff"); + args << "-flags" << "+ildct+ilme" << "-top" << QString::number(tff); + } + + switch (dialog.format()) { + case 0: + args << "-f" << "mp4" << "-codec:a" << "ac3" << "-b:a" << "512k" << "-codec:v" << "libx264"; + args << "-preset" << "medium" << "-g" << "1" << "-crf" << "15"; + break; + case 1: + args << "-f" << "mov" << "-codec:a" << "pcm_f32le"; + if (dialog.deinterlace() || progressive) { + args << "-codec:v" << "dnxhd" << "-profile:v" << "dnxhr_hq" << "-pix_fmt" << "yuv422p"; + } else { // interlaced + args << "-codec:v" << "prores_ks" << "-profile:v" << "standard"; + } + break; + case 2: + args << "-f" << "matroska" << "-codec:a" << "pcm_f32le" << "-codec:v" << "utvideo"; + args << "-pix_fmt" << "yuv422p"; + break; + } + if (dialog.get709Convert()) { + args << "-colorspace" << "bt709" << "-color_primaries" << "bt709" << "-color_trc" << "bt709"; + } + + args << "-y" << filename; + producer->Mlt::Properties::clear(kOriginalResourceProperty); + + FfmpegJob *job = new FfmpegJob(filename, args, false); + job->setLabel(tr("Convert %1").arg(Util::baseName(filename))); + if (dialog.isSubClip()) { + if (producer->get(kMultitrackItemProperty)) { + QString s = QString::fromLatin1(producer->get(kMultitrackItemProperty)); + auto parts = s.split(':'); + if (parts.length() == 2) { + int clipIndex = parts[0].toInt(); + int trackIndex = parts[1].toInt(); + QUuid uuid = MAIN.timelineClipUuid(trackIndex, clipIndex); + if (!uuid.isNull()) { + job->setPostJobAction(new ReplaceOnePostJobAction(resource, filename, QString(), uuid, + in)); + JOBS.add(job); + } + } + } else { + job->setPostJobAction(new OpenPostJobAction(resource, filename, QString())); + JOBS.add(job); + } + return; + } + job->setPostJobAction(new ReplaceAllPostJobAction(resource, filename, Util::getHash(*producer))); + JOBS.add(job); +} diff --git a/src/transcoder.h b/src/transcoder.h new file mode 100644 index 0000000000..8d21261a47 --- /dev/null +++ b/src/transcoder.h @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2023 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 . + */ + +#ifndef TRANSCODER_H +#define TRANSCODER_H + +#include "dialogs/transcodedialog.h" + +#include + +#include +#include + +class Transcoder : public QObject +{ + Q_OBJECT + +public: + + explicit Transcoder() : QObject() {} + void setProducers(QList &producers); + void addProducer(Mlt::Producer &producer); + void addProducer(Mlt::Producer *producer); + void convert(TranscodeDialog &dialog); + +private: + void convertProducer(Mlt::Producer *producer, TranscodeDialog &dialog, QString filename); + QList m_producers; +}; + +#endif // TRANSCODER_H diff --git a/src/util.cpp b/src/util.cpp index 3bf1b8b9ec..9f4e85a963 100644 --- a/src/util.cpp +++ b/src/util.cpp @@ -737,3 +737,88 @@ bool Util::isFpsDifferent(double a, double b) { return qAbs(a - b) > 0.001; } + +QString Util::getNextFile(const QString &filePath) +{ + QFileInfo info(filePath); + QString basename = info.completeBaseName(); + QString extension = info.suffix(); + if (extension.isEmpty()) { + extension = basename; + basename = QString(); + } + for (unsigned i = 1; i < std::numeric_limits::max(); i++) { + QString filename = QString::fromLatin1("%1%2.%3").arg(basename).arg(i).arg(extension); + if (!info.dir().exists(filename)) + return info.dir().filePath(filename); + } + return filePath; +} + +QString Util::trcString(int trc) +{ + QString trcString = QObject::tr("unknown (%1)").arg(trc); + switch (trc) { + case 0: + trcString = QObject::tr("NA"); + break; + case 1: + trcString = "ITU-R BT.709"; + break; + case 6: + trcString = "ITU-R BT.601"; + break; + case 7: + trcString = "SMPTE ST240"; + break; + case 11: + trcString = "IEC 61966-2-4"; + break; + case 14: + trcString = "ITU-R BT.2020"; + break; + case 15: + trcString = "ITU-R BT.2020"; + break; + case 16: + trcString = "SMPTE ST2084 (PQ)"; + break; + case 17: + trcString = "SMPTE ST428"; + break; + case 18: + trcString = "ARIB B67 (HLG)"; + break; + } + return trcString; +} + +bool Util::trcIsCompatible(int trc) +{ + // Transfer characteristics > SMPTE240M Probably need conversion except IEC61966-2-4 is OK + return trc <= 7 || trc == 11 || trc == 18; +} + +QString Util::getConversionAdvice(Mlt::Producer *producer) +{ + QString advice; + producer->probe(); + QString resource = Util::GetFilenameFromProducer(producer); + int trc = producer->get_int("meta.media.color_trc"); + if (!Util::trcIsCompatible(trc)) { + QString trcString = Util::trcString(trc); + LOG_INFO() << resource << "Probable HDR" << trcString; + advice = QObject::tr("This file uses color transfer characteristics %1, which may result in incorrect colors or brightness in Shotcut.").arg( + trcString); + } else if (producer->get_int("meta.media.variable_frame_rate")) { + LOG_INFO() << resource << "is variable frame rate"; + advice = QObject::tr("This file is variable frame rate, which is not reliable for editing."); + } else if (QFile::exists(resource) && !MLT.isSeekable(producer)) { + LOG_INFO() << resource << "is not seekable"; + advice = QObject::tr("This file does not support seeking and cannot be used for editing."); + } else if (QFile::exists(resource) && resource.endsWith(".m2t")) { + LOG_INFO() << resource << "is HDV"; + advice = QObject::tr("This file format (HDV) is not reliable for editing."); + } + return advice; +} diff --git a/src/util.h b/src/util.h index 62981a1bfd..6b1e24b861 100644 --- a/src/util.h +++ b/src/util.h @@ -73,6 +73,10 @@ class Util static void passProducerProperties(Mlt::Producer *src, Mlt::Producer *dst); static bool warnIfLowDiskSpace(const QString &path); static bool isFpsDifferent(double a, double b); + static QString getNextFile(const QString &filePath); + static QString trcString(int trc); + static bool trcIsCompatible(int trc); + static QString getConversionAdvice(Mlt::Producer *producer); }; #endif // UTIL_H diff --git a/src/widgets/avformatproducerwidget.cpp b/src/widgets/avformatproducerwidget.cpp index 2f0c0cbd5f..312e2302d7 100644 --- a/src/widgets/avformatproducerwidget.cpp +++ b/src/widgets/avformatproducerwidget.cpp @@ -34,6 +34,7 @@ #include "proxymanager.h" #include "dialogs/longuitask.h" #include "spatialmedia/spatialmedia.h" +#include "transcoder.h" #include @@ -134,7 +135,9 @@ void AvformatProducerWidget::offerConvert(QString message, bool set709Convert, b m_producer->get_length() - 1; dialog.setSubClipChecked(setSubClip && (m_producer->get_in() > 0 || m_producer->get_out() < m_producer->get_length() - 1)); - convert(dialog); + Transcoder transcoder; + transcoder.addProducer(m_producer.data()); + transcoder.convert(dialog); } void AvformatProducerWidget::keyPressEvent(QKeyEvent *event) @@ -346,39 +349,7 @@ void AvformatProducerWidget::reloadProducerValues() ui->videoTableWidget->setItem(4, 1, new QTableWidgetItem(csString)); key = QString("meta.media.%1.codec.color_trc").arg(i); int trc = m_producer->get_int(key.toLatin1().constData()); - QString trcString = tr("unknown (%1)").arg(trc); - switch (trc) { - case 0: - trcString = tr("NA"); - break; - case 1: - trcString = "ITU-R BT.709"; - break; - case 6: - trcString = "ITU-R BT.601"; - break; - case 7: - trcString = "SMPTE ST240"; - break; - case 11: - trcString = "IEC 61966-2-4"; - break; - case 14: - trcString = "ITU-R BT.2020"; - break; - case 15: - trcString = "ITU-R BT.2020"; - break; - case 16: - trcString = "SMPTE ST2084 (PQ)"; - break; - case 17: - trcString = "SMPTE ST428"; - break; - case 18: - trcString = "ARIB B67 (HLG)"; - break; - } + QString trcString = Util::trcString(trc); QTableWidgetItem *trcItem = new QTableWidgetItem(trcString); trcItem->setData(Qt::UserRole, QVariant(trc)); ui->videoTableWidget->setItem(5, 1, trcItem); @@ -542,31 +513,6 @@ void AvformatProducerWidget::reloadProducerValues() } ui->syncSlider->setValue(qRound(m_producer->get_double("video_delay") * 1000.0)); setSyncVisibility(); - - if (Settings.showConvertClipDialog() && !m_producer->get_int(kShotcutSkipConvertProperty)) { - auto transferItem = ui->videoTableWidget->item(5, 1); - if (transferItem) LOG_INFO() << "color transfer" << transferItem->data( - Qt::UserRole).toInt() << "=" << transferItem->text(); - if (transferItem && transferItem->data(Qt::UserRole).toInt() > 7 - && transferItem->data(Qt::UserRole).toInt() != 11 - && transferItem->data(Qt::UserRole).toInt() != 18) { - // Transfer characteristics > SMPTE240M Probably need conversion except IEC61966-2-4 is OK - QString trcString = ui->videoTableWidget->item(5, 1)->text(); - LOG_INFO() << resource << "Probable HDR" << trcString; - offerConvert( - tr("This file uses color transfer characteristics %1, which may result in incorrect colors or brightness in Shotcut.").arg( - trcString), true); - } else if (isVariableFrameRate) { - LOG_INFO() << resource << "is variable frame rate"; - offerConvert(tr("This file is variable frame rate, which is not reliable for editing.")); - } else if (QFile::exists(resource) && !MLT.isSeekable(m_producer.data())) { - LOG_INFO() << resource << "is not seekable"; - offerConvert(tr("This file does not support seeking and cannot be used for editing.")); - } else if (QFile::exists(resource) && resource.endsWith(".m2t")) { - LOG_INFO() << resource << "is HDV"; - offerConvert(tr("This file format (HDV) is not reliable for editing.")); - } - } } void AvformatProducerWidget::on_videoTrackComboBox_activated(int index) @@ -758,210 +704,9 @@ void AvformatProducerWidget::on_actionFFmpegConvert_triggered() dialog.setWindowModality(QmlApplication::dialogModality()); dialog.set709Convert(ui->videoTableWidget->item(5, 1)->data(Qt::UserRole).toInt() > 7); dialog.showSubClipCheckBox(); - convert(dialog); -} - -static QString getNextFile(const QString &filePath) -{ - QFileInfo info(filePath); - QString basename = info.completeBaseName(); - QString extension = info.suffix(); - if (extension.isEmpty()) { - extension = basename; - basename = QString(); - } - for (unsigned i = 1; i < std::numeric_limits::max(); i++) { - QString filename = QString::fromLatin1("%1%2.%3").arg(basename).arg(i).arg(extension); - if (!info.dir().exists(filename)) - return info.dir().filePath(filename); - } - return filePath; -} - -void AvformatProducerWidget::convert(TranscodeDialog &dialog) -{ - int result = dialog.exec(); - if (dialog.isCheckBoxChecked()) { - Settings.setShowConvertClipDialog(false); - } - if (result == QDialog::Accepted) { - QString resource = Util::GetFilenameFromProducer(producer()); - QString path = Settings.savePath(); - QStringList args; - QString nameFilter; - int in = -1; - - args << "-loglevel" << "verbose"; - args << "-i" << resource; - args << "-max_muxing_queue_size" << "9999"; - - if (dialog.isSubClip()) { - if (Settings.proxyEnabled()) { - m_producer->Mlt::Properties::clear(kOriginalResourceProperty); - } - - // set trim options - if (m_producer->get(kFilterInProperty)) { - in = m_producer->get_int(kFilterInProperty); - int ss = qMax(0, in - qRound(m_producer->get_fps() * kHandleSeconds)); - auto s = QString::fromLatin1(m_producer->frames_to_time(ss, mlt_time_clock)); - args << "-ss" << s.replace(',', '.'); - in -= ss; - } else { - args << "-ss" << QString::fromLatin1(m_producer->get_time("in", mlt_time_clock)).replace(',', - '.').replace(',', '.'); - } - if (m_producer->get(kFilterOutProperty)) { - int out = m_producer->get_int(kFilterOutProperty); - int to = qMin(m_producer->get_playtime() - 1, out + qRound(m_producer->get_fps() * kHandleSeconds)); - auto s = QString::fromLatin1(m_producer->frames_to_time(to, mlt_time_clock)); - args << "-to" << s.replace(',', '.'); - } else { - args << "-to" << QString::fromLatin1(m_producer->get_time("out", mlt_time_clock)).replace(',', '.'); - } - } - - // transcode all streams except data, subtitles, and attachments - auto audioIndex = m_producer->property_exists(kDefaultAudioIndexProperty) ? m_producer->get_int( - kDefaultAudioIndexProperty) : m_producer->get_int("audio_index"); - if (m_producer->get_int("video_index") < audioIndex) { - args << "-map" << "0:V?" << "-map" << "0:a?"; - } else { - args << "-map" << "0:a?" << "-map" << "0:V?"; - } - args << "-map_metadata" << "0" << "-ignore_unknown"; - - // Set Sample rate if different than source - if ( !dialog.sampleRate().isEmpty() ) { - args << "-ar" << dialog.sampleRate(); - } - - // Set video filters - args << "-vf"; - QString filterString; - if (dialog.deinterlace()) { - QString deinterlaceFilter = QString("bwdif,"); - filterString = filterString + deinterlaceFilter; - } - QString range; - if (ui->rangeComboBox->currentIndex()) - range = "full"; - else - range = "mpeg"; - if (dialog.get709Convert()) { - QString convertFilter = - QString("zscale=t=linear:npl=100,format=gbrpf32le,zscale=p=bt709,tonemap=tonemap=hable:desat=0,zscale=t=bt709:m=bt709:r=tv,format=yuv422p,"); - filterString = filterString + convertFilter; - } - filterString = filterString + - QString("scale=flags=accurate_rnd+full_chroma_inp+full_chroma_int:in_range=%1:out_range=%2").arg( - range).arg(range); - if (dialog.fpsOverride()) { - auto fps = QString("%1").arg(dialog.fps(), 0, 'f', 6); - int numerator, denominator; - Util::normalizeFrameRate(dialog.fps(), numerator, denominator); - if (denominator == 1001) { - fps = QString("%1/%2").arg(numerator).arg(denominator); - } - QString minterpFilter = - QString(",minterpolate='mi_mode=%1:mc_mode=aobmc:me_mode=bidir:vsbmc=1:fps=%2'").arg( - dialog.frc()).arg(fps); - filterString = filterString + minterpFilter; - } - args << filterString; - - // Specify color range - if (ui->rangeComboBox->currentIndex()) - args << "-color_range" << "jpeg"; - else - args << "-color_range" << "mpeg"; - - if (!dialog.deinterlace() && !ui->scanComboBox->currentIndex()) - args << "-flags" << "+ildct+ilme" << "-top" << QString::number( - ui->fieldOrderComboBox->currentIndex()); - - switch (dialog.format()) { - case 0: - path.append("/%1 - %2.mp4"); - nameFilter = tr("MP4 (*.mp4);;All Files (*)"); - args << "-f" << "mp4" << "-codec:a" << "ac3" << "-b:a" << "512k" << "-codec:v" << "libx264"; - args << "-preset" << "medium" << "-g" << "1" << "-crf" << "15"; - break; - case 1: - args << "-f" << "mov" << "-codec:a" << "pcm_f32le"; - if (dialog.deinterlace() || ui->scanComboBox->currentIndex()) { // progressive - args << "-codec:v" << "dnxhd" << "-profile:v" << "dnxhr_hq" << "-pix_fmt" << "yuv422p"; - } else { // interlaced - args << "-codec:v" << "prores_ks" << "-profile:v" << "standard"; - } - path.append("/%1 - %2.mov"); - nameFilter = tr("MOV (*.mov);;All Files (*)"); - break; - case 2: - args << "-f" << "matroska" << "-codec:a" << "pcm_f32le" << "-codec:v" << "utvideo"; - args << "-pix_fmt" << "yuv422p"; - path.append("/%1 - %2.mkv"); - nameFilter = tr("MKV (*.mkv);;All Files (*)"); - break; - } - if (dialog.get709Convert()) { - args << "-colorspace" << "bt709" << "-color_primaries" << "bt709" << "-color_trc" << "bt709"; - } - QFileInfo fi(resource); - QString suffix = dialog.isSubClip() ? tr("Sub-clip") + ' ' : tr("Converted"); - path = path.arg(fi.completeBaseName(), suffix); - if (dialog.isSubClip()) { - path = getNextFile(path); - } - QString filename = QFileDialog::getSaveFileName(this, dialog.windowTitle(), path, nameFilter, - nullptr, Util::getFileDialogOptions()); - if (!filename.isEmpty()) { - if (filename == QDir::toNativeSeparators(resource)) { - QMessageBox::warning(this, dialog.windowTitle(), - QObject::tr("Unable to write file %1\n" - "Perhaps you do not have permission.\n" - "Try again with a different folder.") - .arg(fi.fileName())); - return; - } - if (Util::warnIfNotWritable(filename, this, dialog.windowTitle())) - return; - - if (Util::warnIfLowDiskSpace(filename)) { - MAIN.showStatusMessage(tr("Convert canceled")); - return; - } - - Settings.setSavePath(QFileInfo(filename).path()); - args << "-y" << filename; - m_producer->Mlt::Properties::clear(kOriginalResourceProperty); - - FfmpegJob *job = new FfmpegJob(filename, args, false); - job->setLabel(tr("Convert %1").arg(Util::baseName(filename))); - if (dialog.isSubClip()) { - if (m_producer->get(kMultitrackItemProperty)) { - QString s = QString::fromLatin1(m_producer->get(kMultitrackItemProperty)); - auto parts = s.split(':'); - if (parts.length() == 2) { - int clipIndex = parts[0].toInt(); - int trackIndex = parts[1].toInt(); - QUuid uuid = MAIN.timelineClipUuid(trackIndex, clipIndex); - if (!uuid.isNull()) { - job->setPostJobAction(new ReplaceOnePostJobAction(resource, filename, QString(), uuid, - in)); - JOBS.add(job); - } - } - } else { - job->setPostJobAction(new OpenPostJobAction(resource, filename, QString())); - JOBS.add(job); - } - return; - } - job->setPostJobAction(new ReplaceAllPostJobAction(resource, filename, Util::getHash(*m_producer))); - JOBS.add(job); - } - } + Transcoder transcoder; + transcoder.addProducer(m_producer.data()); + transcoder.convert(dialog); } bool AvformatProducerWidget::revertToOriginalResource() diff --git a/src/widgets/avformatproducerwidget.h b/src/widgets/avformatproducerwidget.h index 32ae012324..85331e1ce6 100644 --- a/src/widgets/avformatproducerwidget.h +++ b/src/widgets/avformatproducerwidget.h @@ -132,7 +132,6 @@ private slots: void reopen(Mlt::Producer *p); void recreateProducer(); - void convert(TranscodeDialog &dialog); bool revertToOriginalResource(); void setSyncVisibility(); void reloadProducerValues(); diff --git a/src/widgets/resourcewidget.cpp b/src/widgets/resourcewidget.cpp new file mode 100644 index 0000000000..d488fad16e --- /dev/null +++ b/src/widgets/resourcewidget.cpp @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2023 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 . + */ + +#include "resourcewidget.h" + +#include "Logger.h" +#include "models/resourcemodel.h" + +#include +#include +#include + +ResourceWidget::ResourceWidget(QWidget *parent) + : QWidget(parent) +{ + QVBoxLayout *vlayout = new QVBoxLayout(); + + m_model = new ResourceModel(this); + + m_table = new QTreeView(); + m_table->setSelectionMode(QAbstractItemView::ExtendedSelection); + m_table->setSelectionBehavior(QAbstractItemView::SelectRows); + m_table->setItemsExpandable(false); + m_table->setRootIsDecorated(false); + m_table->setUniformRowHeights(true); + m_table->setSortingEnabled(false); + m_table->setModel(m_model); + m_table->setWordWrap(false); + m_table->setAlternatingRowColors(true); + m_table->header()->setStretchLastSection(false); + qreal rowHeight = fontMetrics().height() * devicePixelRatioF(); + m_table->header()->setMinimumSectionSize(rowHeight); + m_table->header()->setSectionResizeMode(ResourceModel::COLUMN_INFO, QHeaderView::Fixed); + m_table->setColumnWidth(ResourceModel::COLUMN_INFO, rowHeight); + m_table->header()->setSectionResizeMode(ResourceModel::COLUMN_NAME, QHeaderView::Interactive); + m_table->header()->setSectionResizeMode(ResourceModel::COLUMN_SIZE, QHeaderView::Interactive); + m_table->header()->setSectionResizeMode(ResourceModel::COLUMN_VID_DESCRIPTION, + QHeaderView::Interactive); + m_table->header()->setSectionResizeMode(ResourceModel::COLUMN_AUD_DESCRIPTION, + QHeaderView::Interactive); + connect(m_table->selectionModel(), &QItemSelectionModel::currentChanged, this, [ = ]() { + m_table->selectionModel()->clearCurrentIndex(); + }); + vlayout->addWidget(m_table); + + setLayout(vlayout); +} + +ResourceWidget::~ResourceWidget() +{ +} + +void ResourceWidget::search(Mlt::Producer *producer) +{ + m_model->search(producer); +} + +void ResourceWidget::add(Mlt::Producer *producer) +{ + m_model->add(producer); +} + +void ResourceWidget::selectTroubleClips() +{ + m_table->selectionModel()->clearSelection(); + for (int i = 0; i < m_model->rowCount(QModelIndex()); i++) { + QModelIndex index = m_model->index(i, ResourceModel::COLUMN_INFO); + if (!m_model->data(index, Qt::ToolTipRole).toString().isEmpty()) { + m_table->selectionModel()->select(index, QItemSelectionModel::Select | QItemSelectionModel::Rows); + } + } +} + +bool ResourceWidget::hasTroubleClips() +{ + for (int i = 0; i < m_model->rowCount(QModelIndex()); i++) { + QModelIndex index = m_model->index(i, ResourceModel::COLUMN_INFO); + if (!m_model->data(index, Qt::ToolTipRole).toString().isEmpty()) { + return true; + } + } + return false; +} + +QList ResourceWidget::getSelected() +{ + return m_model->getProducers(m_table->selectionModel()->selectedRows()); +} + +void ResourceWidget::updateSize() +{ + static const int MAX_COLUMN_WIDTH = 300; + int tableWidth = 38 + m_table->columnWidth(ResourceModel::COLUMN_INFO); + for (int i = ResourceModel::COLUMN_NAME; i < m_table->model()->columnCount(); i++) { + m_table->resizeColumnToContents(i); + int columnWidth = m_table->columnWidth(i); + if (columnWidth > MAX_COLUMN_WIDTH) { + columnWidth = MAX_COLUMN_WIDTH; + m_table->setColumnWidth(i, columnWidth); + } + tableWidth += columnWidth; + } + resize(tableWidth, 400); +} diff --git a/src/widgets/resourcewidget.h b/src/widgets/resourcewidget.h new file mode 100644 index 0000000000..82084f3608 --- /dev/null +++ b/src/widgets/resourcewidget.h @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2023 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 . + */ + +#ifndef RESOURCEWIDGET_H +#define RESOURCEWIDGET_H + +#include + +#include + +class ResourceModel; +class QTreeView; + +class ResourceWidget : public QWidget +{ + Q_OBJECT + +public: + ResourceWidget(QWidget *parent); + virtual ~ResourceWidget(); + + void search(Mlt::Producer *producer); + void add(Mlt::Producer *producer); + void selectTroubleClips(); + bool hasTroubleClips(); + QList getSelected(); + void updateSize(); + +private: + ResourceModel *m_model; + QTreeView *m_table; +}; + +#endif // RESOURCEWIDGET_H