From bc60c745620c2b927ff039367c125e7394ff8f49 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Fri, 23 Oct 2020 01:26:17 +1100 Subject: [PATCH] finalized very, very basic OTIO support Ironed out various project and footage loading issues, and a single cache invalidation issue. --- app/codec/CMakeLists.txt | 3 - app/codec/decoder.cpp | 30 ++- app/codec/decoder.h | 4 +- app/codec/ffmpeg/ffmpegdecoder.cpp | 4 +- app/codec/ffmpeg/ffmpegdecoder.h | 2 +- app/codec/oiio/oiiodecoder.cpp | 2 +- app/codec/oiio/oiiodecoder.h | 2 +- app/codec/otio/otiodecoder.cpp | 123 ---------- app/core.cpp | 105 ++++++--- app/core.h | 9 +- app/panel/footageviewer/footageviewer.cpp | 2 +- app/project/item/footage/footage.cpp | 2 +- app/project/item/sequence/sequence.cpp | 2 +- app/render/backend/renderbackend.cpp | 11 +- app/render/backend/renderbackend.h | 4 + app/render/playbackcache.h | 17 +- app/task/project/CMakeLists.txt | 3 +- app/task/project/import/import.cpp | 25 +-- app/task/project/load/CMakeLists.txt | 2 + app/task/project/load/load.cpp | 9 +- app/task/project/load/load.h | 34 +-- app/task/project/load/loadbasetask.cpp | 32 +++ app/task/project/load/loadbasetask.h | 74 ++++++ .../project/loadotio}/CMakeLists.txt | 4 +- app/task/project/loadotio/loadotio.cpp | 212 ++++++++++++++++++ .../project/loadotio/loadotio.h} | 15 +- .../{exportotio => saveotio}/CMakeLists.txt | 4 +- .../saveotio.cpp} | 19 +- .../exportotiotask.h => saveotio/saveotio.h} | 6 +- app/widget/viewer/viewer.cpp | 3 + app/window/mainwindow/mainmenu.cpp | 7 - app/window/mainwindow/mainmenu.h | 3 - 32 files changed, 473 insertions(+), 301 deletions(-) delete mode 100644 app/codec/otio/otiodecoder.cpp create mode 100644 app/task/project/load/loadbasetask.cpp create mode 100644 app/task/project/load/loadbasetask.h rename app/{codec/otio => task/project/loadotio}/CMakeLists.txt (91%) create mode 100644 app/task/project/loadotio/loadotio.cpp rename app/{codec/otio/otiodecoder.h => task/project/loadotio/loadotio.h} (74%) rename app/task/project/{exportotio => saveotio}/CMakeLists.txt (89%) rename app/task/project/{exportotio/exportotiotask.cpp => saveotio/saveotio.cpp} (90%) rename app/task/project/{exportotio/exportotiotask.h => saveotio/saveotio.h} (91%) diff --git a/app/codec/CMakeLists.txt b/app/codec/CMakeLists.txt index 7f88f6c53..bce758bfd 100644 --- a/app/codec/CMakeLists.txt +++ b/app/codec/CMakeLists.txt @@ -16,9 +16,6 @@ add_subdirectory(ffmpeg) add_subdirectory(oiio) -if(OpenTimelineIO_FOUND) - add_subdirectory(otio) -endif() set(OLIVE_SOURCES ${OLIVE_SOURCES} diff --git a/app/codec/decoder.cpp b/app/codec/decoder.cpp index 2bbdd1963..d4dc7108b 100644 --- a/app/codec/decoder.cpp +++ b/app/codec/decoder.cpp @@ -27,13 +27,13 @@ #include "codec/ffmpeg/ffmpegcommon.h" #include "codec/ffmpeg/ffmpegdecoder.h" #include "codec/oiio/oiiodecoder.h" -#ifdef USE_OTIO -#include "codec/otio/otiodecoder.h" -#endif #include "codec/waveinput.h" #include "codec/waveoutput.h" #include "common/filefunctions.h" #include "common/timecodefunctions.h" +#ifdef USE_OTIO +#include "task/project/loadotio/loadotio.h" +#endif #include "task/taskmanager.h" #include "project/project.h" @@ -92,16 +92,13 @@ QVector ReceiveListOfAllDecoders() { // The order in which these decoders are added is their priority when probing. Hence FFmpeg should usually be last, // since it supports so many formats and we presumably want to override those formats with a more specific decoder. -#ifdef USE_OTIO - decoders.append(std::make_shared()); -#endif decoders.append(std::make_shared()); decoders.append(std::make_shared()); return decoders; } -ItemPtr Decoder::ProbeMedia(const QString &filename, const QAtomicInt* cancelled) +FootagePtr Decoder::ProbeMedia(Project* project, const QString &filename, const QAtomicInt* cancelled) { // Check for a valid filename if (filename.isEmpty()) { @@ -127,21 +124,22 @@ ItemPtr Decoder::ProbeMedia(const QString &filename, const QAtomicInt* cancelled DecoderPtr decoder = decoder_list.at(i); - ItemPtr item = decoder->Probe(filename, cancelled); + FootagePtr footage = decoder->Probe(filename, cancelled); - if (item) { + if (footage) { + QFileInfo file_info(filename); + footage->set_name(file_info.fileName()); + footage->set_filename(filename); - if (item->type() == Item::kFootage) { - // Attach the successful Decoder to this Footage object - FootagePtr footage = std::static_pointer_cast(item); - footage->set_decoder(decoder->id()); - footage->SetValid(); - } + footage->set_decoder(decoder->id()); + footage->set_project(project); + footage->set_timestamp(file_info.lastModified().toMSecsSinceEpoch()); + footage->SetValid(); // FIXME: Cache the results so we don't have to probe if this media is added a second time - return item; + return footage; } } diff --git a/app/codec/decoder.h b/app/codec/decoder.h index cca3ce0b6..a096d5b08 100644 --- a/app/codec/decoder.h +++ b/app/codec/decoder.h @@ -102,7 +102,7 @@ public: * TRUE if the Decoder was able to decode this file. FALSE if not. This function should have filled the Footage * object with metadata if it returns TRUE. Otherwise, the Footage object should be untouched. */ - virtual ItemPtr Probe(const QString& filename, const QAtomicInt* cancelled) const = 0; + virtual FootagePtr Probe(const QString& filename, const QAtomicInt* cancelled) const = 0; /** * @brief Open media/allocate memory @@ -199,7 +199,7 @@ public: * * TRUE if a Decoder was successfully able to parse and probe this file. FALSE if not. */ - static ItemPtr ProbeMedia(const QString& filename, const QAtomicInt *cancelled); + static FootagePtr ProbeMedia(Project *project, const QString& filename, const QAtomicInt *cancelled); /** * @brief Create a Decoder instance using a Decoder ID diff --git a/app/codec/ffmpeg/ffmpegdecoder.cpp b/app/codec/ffmpeg/ffmpegdecoder.cpp index 3da0a700b..c5a0f3546 100644 --- a/app/codec/ffmpeg/ffmpegdecoder.cpp +++ b/app/codec/ffmpeg/ffmpegdecoder.cpp @@ -450,7 +450,7 @@ bool FFmpegDecoder::SupportsAudio() return true; } -ItemPtr FFmpegDecoder::Probe(const QString& filename, const QAtomicInt* cancelled) const +FootagePtr FFmpegDecoder::Probe(const QString& filename, const QAtomicInt* cancelled) const { // Variable for receiving errors from FFmpeg int error_code; @@ -578,7 +578,7 @@ ItemPtr FFmpegDecoder::Probe(const QString& filename, const QAtomicInt* cancelle if (avstream->duration == AV_NOPTS_VALUE) { // Loop through stream until we get the whole duration - FFmpegDecoderInstance instance(filename, i); + FFmpegDecoderInstance instance(filename_c, i); AVPacket* pkt = av_packet_alloc(); AVFrame* frame = av_frame_alloc(); diff --git a/app/codec/ffmpeg/ffmpegdecoder.h b/app/codec/ffmpeg/ffmpegdecoder.h index 05cbee75e..b2d91800f 100644 --- a/app/codec/ffmpeg/ffmpegdecoder.h +++ b/app/codec/ffmpeg/ffmpegdecoder.h @@ -147,7 +147,7 @@ public: // Destructor virtual ~FFmpegDecoder() override; - virtual ItemPtr Probe(const QString& filename, const QAtomicInt* cancelled) const override; + virtual FootagePtr Probe(const QString& filename, const QAtomicInt* cancelled) const override; virtual bool Open() override; virtual FramePtr RetrieveVideo(const rational &timecode, const int& divider) override; diff --git a/app/codec/oiio/oiiodecoder.cpp b/app/codec/oiio/oiiodecoder.cpp index fff9893e5..39b86bac5 100644 --- a/app/codec/oiio/oiiodecoder.cpp +++ b/app/codec/oiio/oiiodecoder.cpp @@ -45,7 +45,7 @@ QString OIIODecoder::id() return QStringLiteral("oiio"); } -ItemPtr OIIODecoder::Probe(const QString& filename, const QAtomicInt* cancelled) const +FootagePtr OIIODecoder::Probe(const QString& filename, const QAtomicInt* cancelled) const { if (!FileTypeIsSupported(filename)) { return nullptr; diff --git a/app/codec/oiio/oiiodecoder.h b/app/codec/oiio/oiiodecoder.h index 79a23019c..11e7638ab 100644 --- a/app/codec/oiio/oiiodecoder.h +++ b/app/codec/oiio/oiiodecoder.h @@ -37,7 +37,7 @@ public: virtual QString id() override; - virtual ItemPtr Probe(const QString& filename, const QAtomicInt* cancelled) const override; + virtual FootagePtr Probe(const QString& filename, const QAtomicInt* cancelled) const override; virtual bool Open() override; virtual FramePtr RetrieveVideo(const rational &timecode, const int& divider) override; diff --git a/app/codec/otio/otiodecoder.cpp b/app/codec/otio/otiodecoder.cpp deleted file mode 100644 index eb2fa06bc..000000000 --- a/app/codec/otio/otiodecoder.cpp +++ /dev/null @@ -1,123 +0,0 @@ -/*** - - Olive - Non-Linear Video Editor - Copyright (C) 2019 Olive Team - - 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 "otiodecoder.h" - -#include -#include -#include - -#include "node/block/clip/clip.h" -#include "node/block/gap/gap.h" -#include "project/item/sequence/sequence.h" - -OLIVE_NAMESPACE_ENTER - -OTIODecoder::OTIODecoder() -{ - -} - -QString OTIODecoder::id() -{ - return QStringLiteral("otio"); -} - -ItemPtr OTIODecoder::Probe(const QString& filename, const QAtomicInt* cancelled) const -{ - if (filename.endsWith(QStringLiteral(".otio"), Qt::CaseInsensitive)) { - opentimelineio::v1_0::ErrorStatus es; - - auto timeline = static_cast(opentimelineio::v1_0::SerializableObjectWithMetadata::from_json_file(filename.toStdString(), &es)); - - if (es != opentimelineio::v1_0::ErrorStatus::OK) { - return nullptr; - } - - SequencePtr sequence = std::make_shared(); - - // FIXME: As far as I know, OTIO doesn't store video/audio parameters? - sequence->set_default_parameters(); - - sequence->set_name(QString::fromStdString(timeline->name())); - - for (auto c : timeline->tracks()->children()) { - auto otio_track = static_cast(c.value); - - // Create a new track - TrackOutput* track = nullptr; - - // Determine what kind of track it is - if (otio_track->kind() == "Video") { - track = sequence->viewer_output()->track_list(Timeline::kTrackTypeVideo)->AddTrack(); - } else if (otio_track->kind() == "Audio") { - track = sequence->viewer_output()->track_list(Timeline::kTrackTypeAudio)->AddTrack(); - } else { - qWarning() << "Found unknown track type:" << otio_track->kind().c_str(); - continue; - } - - // Get clips from track - std::map clip_map = otio_track->range_of_all_children(&es); - if (es != opentimelineio::v1_0::ErrorStatus::OK) { - return nullptr; - } - - for (auto it=clip_map.cbegin(); it!=clip_map.cend(); it++) { - - Block* block = nullptr; - - if (it->first->schema_name() == "Clip") { - - block = new ClipBlock(); - - auto otio_clip = static_cast(it->first); - - /* - if (otio_clip->media_reference()->schema_name() == "ExternalReference") { - QString footage_url = QString::fromStdString(static_cast(otio_clip->media_reference())->target_url()); - ItemPtr footage = Decoder::ProbeMedia(footage_url, cancelled); - - } - */ - - } else if (it->first->schema_name() == "Gap") { - - block = new GapBlock(); - - } else { - qWarning() << "Found unknown block type:" << it->first->schema_name().c_str(); - } - - block->SetLabel(QString::fromStdString(it->first->name())); - block->set_length_and_media_out(rational::fromDouble(it->second.duration().to_seconds())); - sequence->AddNode(block); - track->AppendBlock(block); - - } - } - - return sequence; - } - - return nullptr; -} - -OLIVE_NAMESPACE_EXIT diff --git a/app/core.cpp b/app/core.cpp index e85020600..8db737dbd 100644 --- a/app/core.cpp +++ b/app/core.cpp @@ -55,7 +55,8 @@ #include "render/pixelformat.h" #include "render/shaderinfo.h" #ifdef USE_OTIO -#include "task/project/exportotio/exportotiotask.h" +#include "task/project/loadotio/loadotio.h" +#include "task/project/saveotio/saveotio.h" #endif #include "task/project/import/import.h" #include "task/project/import/importerrordialog.h" @@ -273,28 +274,6 @@ void Core::CreateNewProject() AddOpenProject(std::make_shared()); } -#ifdef USE_OTIO -void Core::ExportActiveSequenceAsOTIO() -{ - ProjectPtr project = GetActiveProject(); - - if (project) { - QString fn = QFileDialog::getSaveFileName(main_window_, - tr("Export as OpenTimelineIO"), - QString(), - tr("OpenTimelineIO (*.otio)")); - - if (!fn.isEmpty()) { - fn = FileFunctions::EnsureFilenameExtension(fn, QStringLiteral("otio")); - - ExportOTIOTask* task = new ExportOTIOTask(project, fn); - TaskDialog* dialog = new TaskDialog(task, tr("Export Sequence"), main_window_); - dialog->open(); - } - } -} -#endif - const bool &Core::snapping() const { return snapping_; @@ -486,7 +465,7 @@ void Core::AddOpenProject(ProjectPtr p) void Core::AddOpenProjectFromTask(Task *task) { - ProjectLoadTask* load_task = static_cast(task); + ProjectLoadBaseTask* load_task = static_cast(task); ProjectPtr project = load_task->GetLoadedProject(); MainWindowLayoutInfo layout = load_task->GetLoadedLayout(); @@ -719,7 +698,22 @@ void Core::StartGUI(bool full_screen) void Core::SaveProjectInternal(ProjectPtr project) { // Create save manager - ProjectSaveTask* psm = new ProjectSaveTask(project); + Task* psm; + + if (project->filename().endsWith(QStringLiteral(".otio"), Qt::CaseInsensitive)) { +#ifdef USE_OTIO + psm = new SaveOTIOTask(project); +#else + QMessageBox::critical(main_window_, + tr("Missing OpenTimelineIO Libraries"), + tr("This build was compiled without OpenTimelineIO and therefore " + "cannot open OpenTimelineIO files.")); + return; +#endif + } else { + psm = new ProjectSaveTask(project); + } + TaskDialog* task_dialog = new TaskDialog(psm, tr("Save Project"), main_window_); connect(task_dialog, &TaskDialog::TaskSucceeded, this, &Core::ProjectSaveSucceeded); @@ -891,9 +885,24 @@ bool Core::CloseAllExceptActiveProject() return true; } -QString Core::GetProjectFilter() +QString Core::GetProjectFilter(bool include_any_filter) { - return QStringLiteral("%1 (*.ove)").arg(tr("Olive Project")); + QString filters; + +#ifdef USE_OTIO + if (include_any_filter) { + filters.append(QStringLiteral("All Supported Projects (*.ove *.otio);;")); + } +#endif + + // Append standard filter + filters.append(QStringLiteral("%1 (*.ove)").arg(tr("Olive Project"))); + +#ifdef USE_OTIO + filters.append(QStringLiteral(";;%2 (*.otio)").arg(tr("OpenTimelineIO"))); +#endif + + return filters; } QString Core::GetRecentProjectsFilePath() @@ -914,13 +923,19 @@ bool Core::SaveProject(ProjectPtr p) bool Core::SaveProjectAs(ProjectPtr p) { - QString fn = QFileDialog::getSaveFileName(main_window_, - tr("Save Project As"), - QString(), - GetProjectFilter()); + QFileDialog fd(main_window_, tr("Save Project As")); - if (!fn.isEmpty()) { - fn = FileFunctions::EnsureFilenameExtension(fn, QStringLiteral("ove")); + fd.setNameFilter(GetProjectFilter(false)); + + if (fd.exec() == QDialog::Accepted) { + QString fn = fd.selectedFiles().first(); + + // Somewhat hacky method of extracting the extension from the name filter + const QString& name_filter = fd.selectedNameFilter(); + int ext_index = name_filter.indexOf(QStringLiteral("(*.")) + 3; + QString extension = name_filter.mid(ext_index, name_filter.size() - ext_index - 1); + + fn = FileFunctions::EnsureFilenameExtension(fn, extension); p->set_filename(fn); @@ -958,9 +973,25 @@ void Core::OpenProjectInternal(const QString &filename) } } - ProjectLoadTask* plm = new ProjectLoadTask(filename); + Task* load_task; - TaskDialog* task_dialog = new TaskDialog(plm, tr("Load Project"), main_window()); + if (filename.endsWith(QStringLiteral(".otio"), Qt::CaseInsensitive)) { + // Load OpenTimelineIO project +#ifdef USE_OTIO + load_task = new LoadOTIOTask(filename); +#else + QMessageBox::critical(main_window_, + tr("Missing OpenTimelineIO Libraries"), + tr("This build was compiled without OpenTimelineIO and therefore " + "cannot open OpenTimelineIO files.")); + return; +#endif + } else { + // Fallback to regular OVE project + load_task = new ProjectLoadTask(filename); + } + + TaskDialog* task_dialog = new TaskDialog(load_task, tr("Load Project"), main_window()); connect(task_dialog, &TaskDialog::TaskSucceeded, this, &Core::AddOpenProjectFromTask); @@ -1237,7 +1268,7 @@ bool Core::ValidateFootageInLoadedProject(ProjectPtr project, const QString& pro foreach (ItemPtr item, project_footage) { FootagePtr footage = std::static_pointer_cast(item); - if (!QFileInfo::exists(footage->filename())) { + if (!QFileInfo::exists(footage->filename()) && !project_saved_url.isEmpty()) { // If the footage doesn't exist, it might have moved with the project const QString& project_current_url = project->filename(); @@ -1285,7 +1316,7 @@ void Core::OpenProject() QString file = QFileDialog::getOpenFileName(main_window_, tr("Open Project"), QString(), - GetProjectFilter()); + GetProjectFilter(true)); if (!file.isEmpty()) { OpenProjectInternal(file); diff --git a/app/core.h b/app/core.h index 47d60b6c1..8e3b45db1 100644 --- a/app/core.h +++ b/app/core.h @@ -385,13 +385,6 @@ public slots: */ void CreateNewProject(); -#ifdef USE_OTIO - /** - * @brief Exports the active sequence to OpenTimelineIO - */ - void ExportActiveSequenceAsOTIO(); -#endif - signals: /** * @brief Signal emitted when a project is opened @@ -426,7 +419,7 @@ private: /** * @brief Get the file filter than can be used with QFileDialog to open and save compatible projects */ - static QString GetProjectFilter(); + static QString GetProjectFilter(bool include_any_filter); /** * @brief Returns the filename where the recently opened/saved projects should be stored diff --git a/app/panel/footageviewer/footageviewer.cpp b/app/panel/footageviewer/footageviewer.cpp index 25a278ce0..5ca58884a 100644 --- a/app/panel/footageviewer/footageviewer.cpp +++ b/app/panel/footageviewer/footageviewer.cpp @@ -51,7 +51,7 @@ QList FootageViewerPanel::GetSelectedFootage() const void FootageViewerPanel::SetFootage(Footage *f) { - if (!f->IsValid()) { + if (f && !f->IsValid()) { // Do nothing if footage is invalid return; } diff --git a/app/project/item/footage/footage.cpp b/app/project/item/footage/footage.cpp index 9e41e72ec..cd92604c6 100644 --- a/app/project/item/footage/footage.cpp +++ b/app/project/item/footage/footage.cpp @@ -308,7 +308,7 @@ bool Footage::CompareFootageToItsFilename(FootagePtr footage) } else { // Footage may have changed and we'll have to re-probe it. It also may not have, in which // case nothing needs to change. - ItemPtr item = Decoder::ProbeMedia(footage->filename(), nullptr); + ItemPtr item = Decoder::ProbeMedia(footage->project(), footage->filename(), nullptr); if (item && item->type() == footage->type()) { // Item is the same type, that's a good sign. Let's look for any differences. diff --git a/app/project/item/sequence/sequence.cpp b/app/project/item/sequence/sequence.cpp index 92eb125a0..3f9ede0aa 100644 --- a/app/project/item/sequence/sequence.cpp +++ b/app/project/item/sequence/sequence.cpp @@ -151,7 +151,7 @@ void Sequence::Load(QXmlStreamReader *reader, XMLNodeData& xml_node_data, const // Ensure this and all children are in the main thread // NOTE: It might be good to move the Item system to QObjects so they inherit their thread - if (thread() != qApp->thread()) { + if (QThread::currentThread() != qApp->thread()) { moveToThread(qApp->thread()); } } diff --git a/app/render/backend/renderbackend.cpp b/app/render/backend/renderbackend.cpp index 8e5c0110b..b3bbe23a2 100644 --- a/app/render/backend/renderbackend.cpp +++ b/app/render/backend/renderbackend.cpp @@ -46,7 +46,8 @@ RenderBackend::RenderBackend(QObject *parent) : generate_audio_previews_(false), render_mode_(RenderMode::kOnline), autocache_has_changed_(false), - use_custom_autocache_range_(false) + use_custom_autocache_range_(false), + ignore_next_mouse_button_(false) { instance_lock_.lock(); instances_.append(this); @@ -258,6 +259,11 @@ void RenderBackend::SetAudioParams(const AudioParams ¶ms) audio_params_ = params; } +void RenderBackend::IgnoreNextMouseButton() +{ + ignore_next_mouse_button_ = true; +} + std::list RenderBackend::SplitRangeIntoChunks(const TimeRange &r) { // FIXME: Magic number @@ -523,7 +529,8 @@ void RenderBackend::AutoCacheVideoInvalidated(const TimeRange &range) ClearVideoQueue(); // Hash these frames since that should be relatively quick. - if (!(qApp->mouseButtons() & Qt::LeftButton)) { + if (ignore_next_mouse_button_ || !(qApp->mouseButtons() & Qt::LeftButton)) { + ignore_next_mouse_button_ = false; RenderTicketWatcher* watcher = new RenderTicketWatcher(); QVector frames = viewer_node_->video_frame_cache()->GetFrameListFromTimeRange({range}); autocache_hash_tasks_.insert(watcher, frames); diff --git a/app/render/backend/renderbackend.h b/app/render/backend/renderbackend.h index 55ee2f566..c7e29e317 100644 --- a/app/render/backend/renderbackend.h +++ b/app/render/backend/renderbackend.h @@ -141,6 +141,8 @@ public: video_download_matrix_ = mat; } + void IgnoreNextMouseButton(); + static std::list SplitRangeIntoChunks(const TimeRange& r); public slots: @@ -223,6 +225,8 @@ private: QVector autocache_currently_caching_hashes_; + bool ignore_next_mouse_button_; + private slots: void WorkerFinished(); diff --git a/app/render/playbackcache.h b/app/render/playbackcache.h index b36ceef4e..73a4ee385 100644 --- a/app/render/playbackcache.h +++ b/app/render/playbackcache.h @@ -39,24 +39,16 @@ public: { } - void Invalidate(const TimeRange& r); - - void InvalidateAll(); - const rational& GetLength() { return length_; } - void SetLength(const rational& r); - bool IsFullyValidated() { return invalidated_.isEmpty(); } - void Shift(const rational& from, const rational& to); - const TimeRangeList& GetInvalidatedRanges() { return invalidated_; @@ -69,6 +61,15 @@ public: QString GetCacheDirectory() const; +public slots: + void Invalidate(const TimeRange& r); + + void InvalidateAll(); + + void SetLength(const rational& r); + + void Shift(const rational& from, const rational& to); + signals: void Invalidated(const OLIVE_NAMESPACE::TimeRange& r); diff --git a/app/task/project/CMakeLists.txt b/app/task/project/CMakeLists.txt index 9932cc3df..9c7d5384b 100644 --- a/app/task/project/CMakeLists.txt +++ b/app/task/project/CMakeLists.txt @@ -15,7 +15,8 @@ # along with this program. If not, see . if(OpenTimelineIO_FOUND) - add_subdirectory(exportotio) + add_subdirectory(loadotio) + add_subdirectory(saveotio) endif() add_subdirectory(import) diff --git a/app/task/project/import/import.cpp b/app/task/project/import/import.cpp index 25e723bb4..d006f0ac3 100644 --- a/app/task/project/import/import.cpp +++ b/app/task/project/import/import.cpp @@ -83,7 +83,8 @@ void ProjectImportTask::Import(Folder *folder, QFileInfoList import, int &counte // Strip out "." and ".." (for some reason QDir::NoDotAndDotDot doesn't work with entryInfoList, so we have to // check manually) for (int i=0;iproject(), file_info.absoluteFilePath(), + &IsCancelled()); if (item) { - // Setup metadata - item->set_name(file_info.fileName()); - item->set_project(model_->project()); - - if (item->type() == Item::kFootage) { - FootagePtr footage = std::static_pointer_cast(item); - - footage->set_filename(file_path); - footage->set_timestamp(file_info.lastModified().toMSecsSinceEpoch()); - - // See if this footage is an image sequence - ValidateImageSequence(footage, import, i); - } + // See if this footage is an image sequence + ValidateImageSequence(item, import, i); // Create undoable command that adds the items to the model new ProjectViewModel::AddItemCommand(model_, diff --git a/app/task/project/load/CMakeLists.txt b/app/task/project/load/CMakeLists.txt index 84fc458c1..d08807a8c 100644 --- a/app/task/project/load/CMakeLists.txt +++ b/app/task/project/load/CMakeLists.txt @@ -18,5 +18,7 @@ set(OLIVE_SOURCES ${OLIVE_SOURCES} task/project/load/load.h task/project/load/load.cpp + task/project/load/loadbasetask.h + task/project/load/loadbasetask.cpp PARENT_SCOPE ) diff --git a/app/task/project/load/load.cpp b/app/task/project/load/load.cpp index 979884928..ebdc9fb5b 100644 --- a/app/task/project/load/load.cpp +++ b/app/task/project/load/load.cpp @@ -30,14 +30,13 @@ OLIVE_NAMESPACE_ENTER ProjectLoadTask::ProjectLoadTask(const QString &filename) : - filename_(filename) + ProjectLoadBaseTask(filename) { - SetTitle(tr("Loading '%1'").arg(filename)); } bool ProjectLoadTask::Run() { - QFile project_file(filename_); + QFile project_file(GetFilename()); if (project_file.open(QFile::ReadOnly | QFile::Text)) { QXmlStreamReader reader(&project_file); @@ -62,7 +61,7 @@ bool ProjectLoadTask::Run() } else if (reader.name() == QStringLiteral("project")) { project_ = std::make_shared(); - project_->set_filename(filename_); + project_->set_filename(GetFilename()); project_->Load(&reader, &layout_info_, &IsCancelled()); @@ -95,7 +94,7 @@ bool ProjectLoadTask::Run() } } else { - SetError(tr("Failed to read file \"%1\" for reading.").arg(filename_)); + SetError(tr("Failed to read file \"%1\" for reading.").arg(GetFilename())); return false; } } diff --git a/app/task/project/load/load.h b/app/task/project/load/load.h index 5fa0b0da5..565d3dc73 100644 --- a/app/task/project/load/load.h +++ b/app/task/project/load/load.h @@ -21,50 +21,20 @@ #ifndef PROJECTLOADMANAGER_H #define PROJECTLOADMANAGER_H -#include "project/project.h" -#include "task/task.h" +#include "loadbasetask.h" #include "window/mainwindow/mainwindowlayoutinfo.h" OLIVE_NAMESPACE_ENTER -class ProjectLoadTask : public Task +class ProjectLoadTask : public ProjectLoadBaseTask { Q_OBJECT public: ProjectLoadTask(const QString& filename); - ProjectPtr GetLoadedProject() const - { - return project_; - } - - MainWindowLayoutInfo GetLoadedLayout() const - { - return layout_info_; - } - - /** - * @brief Returns the filename the project was saved as, but not necessarily where it is now - * - * May help for resolving relative paths. - */ - const QString& GetFilenameProjectWasSavedAs() const - { - return project_saved_url_; - } - protected: virtual bool Run() override; -private: - ProjectPtr project_; - - MainWindowLayoutInfo layout_info_; - - QString project_saved_url_; - - QString filename_; - }; OLIVE_NAMESPACE_EXIT diff --git a/app/task/project/load/loadbasetask.cpp b/app/task/project/load/loadbasetask.cpp new file mode 100644 index 000000000..1e499ca9a --- /dev/null +++ b/app/task/project/load/loadbasetask.cpp @@ -0,0 +1,32 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2019 Olive Team + + 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 "loadbasetask.h" + +OLIVE_NAMESPACE_ENTER + +ProjectLoadBaseTask::ProjectLoadBaseTask(const QString &filename) : + project_(nullptr), + filename_(filename) +{ + SetTitle(tr("Loading '%1'").arg(filename)); +} + +OLIVE_NAMESPACE_EXIT diff --git a/app/task/project/load/loadbasetask.h b/app/task/project/load/loadbasetask.h new file mode 100644 index 000000000..7825c2305 --- /dev/null +++ b/app/task/project/load/loadbasetask.h @@ -0,0 +1,74 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2019 Olive Team + + 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 PROJECTLOADBASETASK_H +#define PROJECTLOADBASETASK_H + +#include "project/project.h" +#include "task/task.h" + +OLIVE_NAMESPACE_ENTER + +class ProjectLoadBaseTask : public Task +{ + Q_OBJECT +public: + ProjectLoadBaseTask(const QString& filename); + + ProjectPtr GetLoadedProject() const + { + return project_; + } + + MainWindowLayoutInfo GetLoadedLayout() const + { + return layout_info_; + } + + /** + * @brief Returns the filename the project was saved as, but not necessarily where it is now + * + * May help for resolving relative paths. + */ + const QString& GetFilenameProjectWasSavedAs() const + { + return project_saved_url_; + } + + const QString& GetFilename() const + { + return filename_; + } + +protected: + ProjectPtr project_; + + MainWindowLayoutInfo layout_info_; + + QString project_saved_url_; + +private: + QString filename_; + +}; + +OLIVE_NAMESPACE_EXIT + +#endif // LOADBASETASK_H diff --git a/app/codec/otio/CMakeLists.txt b/app/task/project/loadotio/CMakeLists.txt similarity index 91% rename from app/codec/otio/CMakeLists.txt rename to app/task/project/loadotio/CMakeLists.txt index 03c1517e0..6243a5bcb 100644 --- a/app/codec/otio/CMakeLists.txt +++ b/app/task/project/loadotio/CMakeLists.txt @@ -16,7 +16,7 @@ set(OLIVE_SOURCES ${OLIVE_SOURCES} - codec/otio/otiodecoder.h - codec/otio/otiodecoder.cpp + task/project/loadotio/loadotio.h + task/project/loadotio/loadotio.cpp PARENT_SCOPE ) diff --git a/app/task/project/loadotio/loadotio.cpp b/app/task/project/loadotio/loadotio.cpp new file mode 100644 index 000000000..58de0090b --- /dev/null +++ b/app/task/project/loadotio/loadotio.cpp @@ -0,0 +1,212 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2019 Olive Team + + 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 "loadotio.h" + +#include +#include +#include +#include +#include +#include + +#include "node/block/clip/clip.h" +#include "node/block/gap/gap.h" +#include "node/input/media/audio/audio.h" +#include "node/input/media/video/video.h" +#include "project/item/folder/folder.h" +#include "project/item/sequence/sequence.h" + +#define OTIO opentimelineio::v1_0 + +OLIVE_NAMESPACE_ENTER + +LoadOTIOTask::LoadOTIOTask(const QString& s) : + ProjectLoadBaseTask(s) +{ +} + +bool LoadOTIOTask::Run() +{ + OTIO::ErrorStatus es; + + auto root = OTIO::SerializableObjectWithMetadata::from_json_file(GetFilename().toStdString(), &es); + + if (es != OTIO::ErrorStatus::OK) { + SetError(tr("Failed to load OpenTimelineIO from file \"%1\"").arg(GetFilename())); + return false; + } + + project_ = std::make_shared(); + project_->set_filename(GetFilename()); + + std::vector timelines; + + if (root->schema_name() == "SerializableCollection") { + // This is a number of timelines + std::vector>& root_children = static_cast(root)->children(); + + timelines.resize(root_children.size()); + for (size_t j=0; j(root_children[j].value); + } + } else if (root->schema_name() == "Timeline") { + // This is a single timeline + timelines.push_back(static_cast(root)); + } else { + // Unknown root, we don't know what to do with this + SetError(tr("Unknown OpenTimelineIO root element")); + return false; + } + + // Keep track of imported footage + QMap imported_footage; + + foreach (auto timeline, timelines) { + SequencePtr sequence = std::make_shared(); + sequence->set_name(QString::fromStdString(timeline->name())); + project_->root()->add_child(sequence); + + ViewerOutput* seq_viewer = sequence->viewer_output(); + + // FIXME: As far as I know, OTIO doesn't store video/audio parameters? + sequence->set_default_parameters(); + + for (auto c : timeline->tracks()->children()) { + auto otio_track = static_cast(c.value); + + // Create a new track + TrackOutput* track = nullptr; + + // Determine what kind of track it is + if (otio_track->kind() == "Video") { + track = seq_viewer->track_list(Timeline::kTrackTypeVideo)->AddTrack(); + + if (seq_viewer->track_list(Timeline::kTrackTypeVideo)->GetTrackCount() == 1) { + // If this is the first track, connect it to the viewer + NodeParam::ConnectEdge(track->output(), seq_viewer->texture_input()); + } + } else if (otio_track->kind() == "Audio") { + track = seq_viewer->track_list(Timeline::kTrackTypeAudio)->AddTrack(); + + if (seq_viewer->track_list(Timeline::kTrackTypeAudio)->GetTrackCount() == 1) { + // If this is the first track, connect it to the viewer + NodeParam::ConnectEdge(track->output(), seq_viewer->samples_input()); + } + } else { + qWarning() << "Found unknown track type:" << otio_track->kind().c_str(); + continue; + } + + // Get clips from track + auto clip_map = otio_track->children(); + if (es != OTIO::ErrorStatus::OK) { + SetError(tr("Failed to load clip")); + return false; + } + + for (auto otio_block_retainer : clip_map) { + + auto otio_block = otio_block_retainer.value; + + Block* block = nullptr; + + if (otio_block->schema_name() == "Clip") { + + block = new ClipBlock(); + + } else if (otio_block->schema_name() == "Gap") { + + block = new GapBlock(); + + } else { + + // We don't know what this is yet, just create a gap for now so that *something* is there + qWarning() << "Found unknown block type:" << otio_block->schema_name().c_str(); + block = new GapBlock(); + + } + + block->SetLabel(QString::fromStdString(otio_block->name())); + + rational start_time = rational::fromDouble(static_cast(otio_block)->source_range()->start_time().to_seconds()); + rational duration = rational::fromDouble(static_cast(otio_block)->source_range()->duration().to_seconds()); + + block->set_media_in(start_time); + block->set_length_and_media_out(duration); + sequence->AddNode(block); + track->AppendBlock(block); + + if (otio_block->schema_name() == "Clip") { + auto otio_clip = static_cast(otio_block); + + if (otio_clip->media_reference()->schema_name() == "ExternalReference") { + // Link footage + QString footage_url = QString::fromStdString(static_cast(otio_clip->media_reference())->target_url()); + + FootagePtr probed_item; + + if (imported_footage.contains(footage_url)) { + probed_item = imported_footage.value(footage_url); + } else { + probed_item = Decoder::ProbeMedia(project_.get(), footage_url, &IsCancelled()); + imported_footage.insert(footage_url, probed_item); + project_->root()->add_child(probed_item); + } + + if (probed_item && probed_item->type() == Item::kFootage) { + MediaInput* media; + if (track->track_type() == Timeline::kTrackTypeVideo) { + media = new VideoInput(); + media->SetFootage(probed_item->get_first_stream_of_type(Stream::kVideo)); + } else { + media = new AudioInput(); + media->SetFootage(probed_item->get_first_stream_of_type(Stream::kAudio)); + } + sequence->AddNode(media); + + NodeParam::ConnectEdge(media->output(), static_cast(block)->texture_input()); + } else { + // FIXME: Add to some kind of list that we couldn't find it + } + } + } + + } + } + + sequence->moveToThread(qApp->thread()); + } + + // Ugly hack to move footage streams to main thread + /*foreach (ItemPtr item, imported_footage) { + if (item && item->type() == Item::kFootage) { + foreach (StreamPtr stream, std::static_pointer_cast(item)->streams()) { + stream->moveToThread(qApp->thread()); + } + } + }*/ + + project_->moveToThread(qApp->thread()); + + return true; +} + +OLIVE_NAMESPACE_EXIT diff --git a/app/codec/otio/otiodecoder.h b/app/task/project/loadotio/loadotio.h similarity index 74% rename from app/codec/otio/otiodecoder.h rename to app/task/project/loadotio/loadotio.h index dd88567cd..5e44bb67d 100644 --- a/app/codec/otio/otiodecoder.h +++ b/app/task/project/loadotio/loadotio.h @@ -21,22 +21,19 @@ #ifndef OTIODECODER_H #define OTIODECODER_H -#include "codec/decoder.h" +#include "project/project.h" +#include "task/project/load/loadbasetask.h" OLIVE_NAMESPACE_ENTER -class OTIODecoder : public Decoder +class LoadOTIOTask : public ProjectLoadBaseTask { Q_OBJECT public: - OTIODecoder(); + LoadOTIOTask(const QString& filename); - virtual QString id() override; - - virtual bool Open() override {return false;} - virtual void Close() override {} - - virtual ItemPtr Probe(const QString& filename, const QAtomicInt* cancelled) const override; +protected: + virtual bool Run() override; }; diff --git a/app/task/project/exportotio/CMakeLists.txt b/app/task/project/saveotio/CMakeLists.txt similarity index 89% rename from app/task/project/exportotio/CMakeLists.txt rename to app/task/project/saveotio/CMakeLists.txt index b604d7a1e..4610ecd5f 100644 --- a/app/task/project/exportotio/CMakeLists.txt +++ b/app/task/project/saveotio/CMakeLists.txt @@ -16,7 +16,7 @@ set(OLIVE_SOURCES ${OLIVE_SOURCES} - task/project/exportotio/exportotiotask.h - task/project/exportotio/exportotiotask.cpp + task/project/saveotio/saveotio.h + task/project/saveotio/saveotio.cpp PARENT_SCOPE ) diff --git a/app/task/project/exportotio/exportotiotask.cpp b/app/task/project/saveotio/saveotio.cpp similarity index 90% rename from app/task/project/exportotio/exportotiotask.cpp rename to app/task/project/saveotio/saveotio.cpp index f7cb131f2..08bbaaeeb 100644 --- a/app/task/project/exportotio/exportotiotask.cpp +++ b/app/task/project/saveotio/saveotio.cpp @@ -18,7 +18,7 @@ ***/ -#include "exportotiotask.h" +#include "saveotio.h" #include #include @@ -31,14 +31,13 @@ OLIVE_NAMESPACE_ENTER -ExportOTIOTask::ExportOTIOTask(ProjectPtr project, const QString &filename) : - project_(project), - filename_(filename) +SaveOTIOTask::SaveOTIOTask(ProjectPtr project) : + project_(project) { SetTitle(tr("Exporting project to OpenTimelineIO")); } -bool ExportOTIOTask::Run() +bool SaveOTIOTask::Run() { QList sequences = project_->get_items_of_type(Item::kSequence); @@ -75,12 +74,12 @@ bool ExportOTIOTask::Run() if (serialized.size() == 1) { // Serialize timeline on its own auto t = serialized.front(); - t->to_json_file(filename_.toStdString(), &es); + t->to_json_file(project_->filename().toStdString(), &es); t->possibly_delete(); } else { // Serialize all into a SerializableCollection auto collection = new opentimelineio::v1_0::SerializableCollection("Sequences", serialized); - collection->to_json_file(filename_.toStdString(), &es); + collection->to_json_file(project_->filename().toStdString(), &es); collection->possibly_delete(); // Delete all existing timelines @@ -92,7 +91,7 @@ bool ExportOTIOTask::Run() return (es == opentimelineio::v1_0::ErrorStatus::OK); } -opentimelineio::v1_0::Timeline *ExportOTIOTask::SerializeTimeline(SequencePtr sequence) +opentimelineio::v1_0::Timeline *SaveOTIOTask::SerializeTimeline(SequencePtr sequence) { auto otio_timeline = new opentimelineio::v1_0::Timeline(sequence->name().toStdString()); @@ -105,7 +104,7 @@ opentimelineio::v1_0::Timeline *ExportOTIOTask::SerializeTimeline(SequencePtr se return otio_timeline; } -opentimelineio::v1_0::Track *ExportOTIOTask::SerializeTrack(TrackOutput *track) +opentimelineio::v1_0::Track *SaveOTIOTask::SerializeTrack(TrackOutput *track) { auto otio_track = new opentimelineio::v1_0::Track(); @@ -185,7 +184,7 @@ fail: return nullptr; } -bool ExportOTIOTask::SerializeTrackList(TrackList *list, opentimelineio::v1_0::Timeline* otio_timeline) +bool SaveOTIOTask::SerializeTrackList(TrackList *list, opentimelineio::v1_0::Timeline* otio_timeline) { opentimelineio::v1_0::ErrorStatus es; diff --git a/app/task/project/exportotio/exportotiotask.h b/app/task/project/saveotio/saveotio.h similarity index 91% rename from app/task/project/exportotio/exportotiotask.h rename to app/task/project/saveotio/saveotio.h index 64f2b71c0..2a380cddc 100644 --- a/app/task/project/exportotio/exportotiotask.h +++ b/app/task/project/saveotio/saveotio.h @@ -29,11 +29,11 @@ OLIVE_NAMESPACE_ENTER -class ExportOTIOTask : public Task +class SaveOTIOTask : public Task { Q_OBJECT public: - ExportOTIOTask(ProjectPtr project, const QString& filename); + SaveOTIOTask(ProjectPtr project); protected: virtual bool Run() override; @@ -47,8 +47,6 @@ private: ProjectPtr project_; - QString filename_; - }; OLIVE_NAMESPACE_EXIT diff --git a/app/widget/viewer/viewer.cpp b/app/widget/viewer/viewer.cpp index 2900d84e0..d1932e549 100644 --- a/app/widget/viewer/viewer.cpp +++ b/app/widget/viewer/viewer.cpp @@ -1166,6 +1166,9 @@ void ViewerWidget::UpdateRendererVideoParameters() renderer_->SetVideoParams(GetConnectedNode()->video_params()); + // In case the user is pressing the mouse at this exact moment + renderer_->IgnoreNextMouseButton(); + GetConnectedNode()->video_frame_cache()->InvalidateAll(); display_widget_->SetVideoParams(GetConnectedNode()->video_params()); diff --git a/app/window/mainwindow/mainmenu.cpp b/app/window/mainwindow/mainmenu.cpp index cb8854e02..b33a3e604 100644 --- a/app/window/mainwindow/mainmenu.cpp +++ b/app/window/mainwindow/mainmenu.cpp @@ -62,10 +62,6 @@ MainMenu::MainMenu(MainWindow *parent) : file_menu_->addSeparator(); file_export_menu_ = new Menu(file_menu_); file_export_media_item_ = file_export_menu_->AddItem("export", Core::instance(), &Core::DialogExportShow, "Ctrl+M"); -#ifdef USE_OTIO - file_export_menu_->addSeparator(); - file_export_otio_item_ = file_export_menu_->AddItem("exportotio", Core::instance(), &Core::ExportActiveSequenceAsOTIO); -#endif file_menu_->addSeparator(); file_project_properties_item_ = file_menu_->AddItem("projectproperties", Core::instance(), &Core::DialogProjectPropertiesShow, "Shift+F10"); file_menu_->addSeparator(); @@ -606,9 +602,6 @@ void MainMenu::Retranslate() file_import_item_->setText(tr("&Import...")); file_export_menu_->setTitle(tr("&Export")); file_export_media_item_->setText(tr("&Media...")); -#ifdef USE_OTIO - file_export_otio_item_->setText(tr("&OpenTimelineIO...")); -#endif file_project_properties_item_->setText(tr("&Project Properties...")); file_close_all_projects_item_->setText(tr("Close All Projects")); file_exit_item_->setText(tr("E&xit")); diff --git a/app/window/mainwindow/mainmenu.h b/app/window/mainwindow/mainmenu.h index 116ea1aec..622303900 100644 --- a/app/window/mainwindow/mainmenu.h +++ b/app/window/mainwindow/mainmenu.h @@ -192,9 +192,6 @@ private: QAction* file_import_item_; Menu* file_export_menu_; QAction* file_export_media_item_; -#ifdef USE_OTIO - QAction* file_export_otio_item_; -#endif QAction* file_project_properties_item_; QAction* file_close_project_item_; QAction* file_close_all_projects_item_;