diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 970f498e4..f57448568 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -376,7 +376,7 @@ jobs: ctest -C ${{ matrix.build-type }} -V if: matrix.os-arch == 'x86_64' # ARM64 tests naturally won't be able to run on x86_64 runners - - name: Create Package + - name: Bundle Application working-directory: ${{ runner.workspace }}/build shell: bash run: | @@ -444,9 +444,18 @@ jobs: # Sign application codesign --deep --sign "Developer ID Application: Olive Studios LLC" $BUNDLE_NAME + - name: Deploy + shell: bash + working-directory: ${{ runner.workspace }}/build/deploy + run: | + ln -s /Applications Applications + cd .. + hdiutil create img.dmg -volname Olive -fs HFS+ -srcfolder deploy + hdiutil convert img.dmg -format UDZO -o $PKGNAME.dmg + - name: Upload Artifact to GitHub uses: actions/upload-artifact@v2 continue-on-error: true with: name: ${{ env.PKGNAME }} - path: ${{ runner.workspace }}/build/deploy + path: ${{ runner.workspace }}/build/${{ env.PKGNAME }}.dmg diff --git a/.gitignore b/.gitignore index 43c997614..b10c5cddf 100644 --- a/.gitignore +++ b/.gitignore @@ -90,6 +90,10 @@ CMakeLists.txt.user* # QtCreator 4.8< compilation database compile_commands.json +# Hand-written compilation database listing compilation flags for clangd to use when parsing the code, similarly to the compilation database above +# https://clangd.llvm.org/design/compile-commands#where-do-compile-commands-come-from +compile_flags.txt + # QtCreator local machine specific files for imported projects *creator.user* diff --git a/app/codec/decoder.cpp b/app/codec/decoder.cpp index 666cec629..434376c06 100644 --- a/app/codec/decoder.cpp +++ b/app/codec/decoder.cpp @@ -45,7 +45,6 @@ Decoder::Decoder() : void Decoder::IncrementAccessTime(qint64 t) { - QMutexLocker locker(&mutex_); last_accessed_ += t; } @@ -156,7 +155,6 @@ Decoder::RetrieveAudioStatus Decoder::RetrieveAudio(SampleBuffer &dest, const Ti qint64 Decoder::GetLastAccessedTime() { - QMutexLocker locker(&mutex_); return last_accessed_; } @@ -215,19 +213,6 @@ DecoderPtr Decoder::CreateFromID(const QString &id) return nullptr; } -int64_t Decoder::GetTimeInTimebaseUnits(const rational &time, const rational &timebase, int64_t start_time) -{ - int64_t t = Timecode::time_to_timestamp(time, timebase); - t += start_time; - return t; -} - -rational Decoder::GetTimestampInTimeUnits(int64_t time, const rational &timebase, int64_t start_time) -{ - time -= start_time; - return Timecode::timestamp_to_time(time, timebase); -} - void Decoder::SignalProcessingProgress(int64_t ts, int64_t duration) { if (duration != AV_NOPTS_VALUE && duration != 0) { @@ -294,10 +279,13 @@ bool Decoder::ConformAudioInternal(const QVector &filenames, const Audi return false; } -bool Decoder::RetrieveAudioFromConform(SampleBuffer &sample_buffer, const QVector &conform_filenames, const TimeRange& range, LoopMode loop_mode, const AudioParams &input_params) +bool Decoder::RetrieveAudioFromConform(SampleBuffer &sample_buffer, const QVector &conform_filenames, TimeRange range, LoopMode loop_mode, const AudioParams &input_params) { PlanarFileDevice input; if (input.open(conform_filenames, QFile::ReadOnly)) { + // Offset range by audio start offset + range -= GetAudioStartOffset(); + qint64 read_index = input_params.time_to_bytes(range.in()) / input_params.channel_count(); qint64 write_index = 0; diff --git a/app/codec/decoder.h b/app/codec/decoder.h index 983efb753..d0c974846 100644 --- a/app/codec/decoder.h +++ b/app/codec/decoder.h @@ -294,8 +294,7 @@ protected: return stream_; } - static int64_t GetTimeInTimebaseUnits(const rational& time, const rational& timebase, int64_t start_time); - static rational GetTimestampInTimeUnits(int64_t time, const rational& timebase, int64_t start_time); + virtual rational GetAudioStartOffset() const { return 0; } signals: /** @@ -307,13 +306,13 @@ signals: private: void UpdateLastAccessed(); - bool RetrieveAudioFromConform(SampleBuffer &sample_buffer, const QVector &conform_filenames, const TimeRange &range, LoopMode loop_mode, const AudioParams ¶ms); + bool RetrieveAudioFromConform(SampleBuffer &sample_buffer, const QVector &conform_filenames, TimeRange range, LoopMode loop_mode, const AudioParams ¶ms); CodecStream stream_; QMutex mutex_; - qint64 last_accessed_; + std::atomic_int64_t last_accessed_; TexturePtr cached_texture_; rational cached_time_; diff --git a/app/codec/encoder.cpp b/app/codec/encoder.cpp index cb5080402..4888ed5a6 100644 --- a/app/codec/encoder.cpp +++ b/app/codec/encoder.cpp @@ -214,6 +214,7 @@ void EncodingParams::Save(QXmlStreamWriter *writer) const writer->writeTextElement(QStringLiteral("width"), QString::number(video_params_.width())); writer->writeTextElement(QStringLiteral("height"), QString::number(video_params_.height())); writer->writeTextElement(QStringLiteral("format"), QString::number(video_params_.format())); + writer->writeTextElement(QStringLiteral("pixelaspect"), video_params_.pixel_aspect_ratio().toString()); writer->writeTextElement(QStringLiteral("timebase"), video_params_.time_base().toString()); writer->writeTextElement(QStringLiteral("divider"), QString::number(video_params_.divider())); writer->writeTextElement(QStringLiteral("bitrate"), QString::number(video_bit_rate_)); @@ -258,6 +259,7 @@ void EncodingParams::Save(QXmlStreamWriter *writer) const writer->writeTextElement(QStringLiteral("samplerate"), QString::number(audio_params_.sample_rate())); writer->writeTextElement(QStringLiteral("channellayout"), QString::number(audio_params_.channel_layout())); writer->writeTextElement(QStringLiteral("format"), QString::number(audio_params_.format())); + writer->writeTextElement(QStringLiteral("bitrate"), QString::number(audio_bit_rate_)); } writer->writeStartElement(QStringLiteral("subtitles")); @@ -398,6 +400,8 @@ bool EncodingParams::LoadV1(QXmlStreamReader *reader) video_params_.set_height(reader->readElementText().toInt()); } else if (reader->name() == QStringLiteral("format")) { video_params_.set_format(static_cast(reader->readElementText().toInt())); + } else if (reader->name() == QStringLiteral("pixelaspect")) { + video_params_.set_pixel_aspect_ratio(rational::fromString(reader->readElementText())); } else if (reader->name() == QStringLiteral("timebase")) { video_params_.set_time_base(rational::fromString(reader->readElementText())); } else if (reader->name() == QStringLiteral("divider")) { @@ -448,6 +452,11 @@ bool EncodingParams::LoadV1(QXmlStreamReader *reader) reader->skipCurrentElement(); } } + + // HACK: Resolve bug where I forgot to serialize pixel aspect ratio + if (video_params_.pixel_aspect_ratio().isNull()) { + video_params_.set_pixel_aspect_ratio(1); + } } else if (reader->name() == QStringLiteral("audio")) { XMLAttributeLoop(reader, attr) { if (attr.name() == QStringLiteral("enabled")) { @@ -464,10 +473,17 @@ bool EncodingParams::LoadV1(QXmlStreamReader *reader) audio_params_.set_channel_layout(reader->readElementText().toULongLong()); } else if (reader->name() == QStringLiteral("format")) { audio_params_.set_format(static_cast(reader->readElementText().toInt())); + } else if (reader->name() == QStringLiteral("bitrate")) { + audio_bit_rate_ = reader->readElementText().toLongLong(); } else { reader->skipCurrentElement(); } } + + // HACK: Resolve bug where I forgot to serialize the audio bit rate + if (!audio_bit_rate_) { + audio_bit_rate_ = 320000; + } } else if (reader->name() == QStringLiteral("subtitles")) { XMLAttributeLoop(reader, attr) { if (attr.name() == QStringLiteral("enabled")) { diff --git a/app/codec/ffmpeg/ffmpegdecoder.cpp b/app/codec/ffmpeg/ffmpegdecoder.cpp index e4168479d..d2292f002 100644 --- a/app/codec/ffmpeg/ffmpegdecoder.cpp +++ b/app/codec/ffmpeg/ffmpegdecoder.cpp @@ -40,12 +40,9 @@ extern "C" { #include #include "codec/planarfiledevice.h" -#include "common/define.h" #include "common/ffmpegutils.h" #include "common/filefunctions.h" #include "common/timecodefunctions.h" -#include "render/framehashcache.h" -#include "render/diskmanager.h" #include "render/renderer.h" #include "render/subtitleparams.h" @@ -143,7 +140,7 @@ bool FFmpegDecoder::OpenInternal() TexturePtr FFmpegDecoder::RetrieveVideoInternal(const RetrieveVideoParams &p) { - if (AVFramePtr f = RetrieveFrame(p.time, p.cancelled)) { + if (AVFramePtr f = RetrieveFrame(p.time, p.src_interlacing, p.cancelled)) { if (p.cancelled && p.cancelled->IsCancelled()) { return nullptr; } @@ -235,9 +232,9 @@ TexturePtr FFmpegDecoder::RetrieveVideoInternal(const RetrieveVideoParams &p) job.Insert(QStringLiteral("u_channel"), NodeValue(NodeValue::kTexture, QVariant::fromValue(u_plane))); job.Insert(QStringLiteral("v_channel"), NodeValue(NodeValue::kTexture, QVariant::fromValue(v_plane))); job.Insert(QStringLiteral("bits_per_pixel"), NodeValue(NodeValue::kInt, bits_per_pixel)); - job.Insert(QStringLiteral("full_range"), NodeValue(NodeValue::kBoolean, f->color_range == AVCOL_RANGE_JPEG)); + job.Insert(QStringLiteral("full_range"), NodeValue(NodeValue::kBoolean, hw_in->color_range == AVCOL_RANGE_JPEG)); - const int *yuv_coeffs = sws_getCoefficients(FFmpegUtils::GetSwsColorspaceFromAVColorSpace(f.get()->colorspace)); + const int *yuv_coeffs = sws_getCoefficients(FFmpegUtils::GetSwsColorspaceFromAVColorSpace(hw_in->colorspace)); job.Insert(QStringLiteral("yuv_crv"), NodeValue(NodeValue::kInt, yuv_coeffs[0])); job.Insert(QStringLiteral("yuv_cgu"), NodeValue(NodeValue::kInt, yuv_coeffs[2])); job.Insert(QStringLiteral("yuv_cgv"), NodeValue(NodeValue::kInt, yuv_coeffs[3])); @@ -246,7 +243,7 @@ TexturePtr FFmpegDecoder::RetrieveVideoInternal(const RetrieveVideoParams &p) int interlacing = 0; if (p.src_interlacing != VideoParams::kInterlaceNone) { if (frame_rate_tb_.isNull()) { - frame_rate_tb_ = av_guess_frame_rate(instance_.fmt_ctx(), instance_.avstream(), f.get()); + frame_rate_tb_ = av_guess_frame_rate(instance_.fmt_ctx(), instance_.avstream(), hw_in); // Double frame rate for interlaced fields frame_rate_tb_ *= 2; @@ -256,7 +253,7 @@ TexturePtr FFmpegDecoder::RetrieveVideoInternal(const RetrieveVideoParams &p) } int64_t req = Timecode::time_to_timestamp(p.time, frame_rate_tb_); - int64_t frm = Timecode::rescale_timestamp(f->pts - instance_.avstream()->start_time, instance_.avstream()->time_base, frame_rate_tb_); + int64_t frm = Timecode::rescale_timestamp(hw_in->pts - instance_.avstream()->start_time, instance_.avstream()->time_base, frame_rate_tb_); bool first = (req == frm); bool top_first = (p.src_interlacing == VideoParams::kInterlacedTopFirst); @@ -268,6 +265,8 @@ TexturePtr FFmpegDecoder::RetrieveVideoInternal(const RetrieveVideoParams &p) tex = p.renderer->CreateTexture(vp); p.renderer->BlitToTexture(Yuv2RgbShader, job, tex.get(), false); + + av_frame_unref(working_frame_); } } @@ -311,6 +310,18 @@ void FFmpegDecoder::CloseInternal() native_output_pix_fmt_ = VideoParams::kFormatInvalid; } +rational FFmpegDecoder::GetAudioStartOffset() const +{ + auto f = instance_.fmt_ctx(); + if (f) { + rational fmt_start = rational(instance_.fmt_ctx()->start_time, AV_TIME_BASE); + rational str_start = rational(instance_.avstream()->time_base) * instance_.avstream()->start_time; + return str_start - fmt_start; + } else { + return 0; + } +} + QString FFmpegDecoder::id() const { return QStringLiteral("ffmpeg"); @@ -787,11 +798,19 @@ void FFmpegDecoder::ClearFrameCache() } } -AVFramePtr FFmpegDecoder::RetrieveFrame(const rational& time, CancelAtom *cancelled) +AVFramePtr FFmpegDecoder::RetrieveFrame(const rational& time, VideoParams::Interlacing interlacing, CancelAtom *cancelled) { - int64_t target_ts = GetTimeInTimebaseUnits(time, instance_.avstream()->time_base, instance_.avstream()->start_time); + int64_t target_ts = Timecode::time_to_timestamp(time, instance_.avstream()->time_base); - const int64_t min_seek = -instance_.avstream()->start_time; + if (interlacing != VideoParams::kInterlaceNone && !IsPixelFormatGLSLCompatible(static_cast(instance_.avstream()->codecpar->format))) { + target_ts *= 2; + } + + if (instance_.fmt_ctx()->start_time != AV_NOPTS_VALUE) { + target_ts += av_rescale_q(instance_.fmt_ctx()->start_time, {1, AV_TIME_BASE}, instance_.avstream()->time_base); + } + + const int64_t min_seek = 0; int64_t seek_ts = std::max(min_seek, target_ts - MaximumQueueSize()); bool still_seeking = false; @@ -991,8 +1010,10 @@ bool FFmpegDecoder::InitScaler(AVFrame *input, const RetrieveVideoParams& params // Link filters as necessary AVFilterContext *last_filter = buffersrc_ctx_; + bool glsl_available = IsPixelFormatGLSLCompatible(static_cast(input->format)); + // Add deinterlace filter if necessary - if (filter_params_.src_interlacing != VideoParams::kInterlaceNone) { + if (filter_params_.src_interlacing != VideoParams::kInterlaceNone && !glsl_available) { AVFilterContext* deint_filter; snprintf(filter_args, kFilterArgSz, "mode=1:parity=%s", @@ -1006,14 +1027,14 @@ bool FFmpegDecoder::InitScaler(AVFrame *input, const RetrieveVideoParams& params } // Add scale filter if necessary - int dst_width, dst_height; if (filter_params_.divider > 1) { AVFilterContext* scale_filter; + int dst_width, dst_height; dst_width = VideoParams::GetScaledDimension(src_width, filter_params_.divider); dst_height = VideoParams::GetScaledDimension(src_height, filter_params_.divider); - snprintf(filter_args, kFilterArgSz, "w=%d:h=%d:flags=fast_bilinear:interl=-1", + snprintf(filter_args, kFilterArgSz, "w=%d:h=%d:flags=fast_bilinear:interl=0", dst_width, dst_height); @@ -1021,13 +1042,10 @@ bool FFmpegDecoder::InitScaler(AVFrame *input, const RetrieveVideoParams& params avfilter_link(last_filter, 0, scale_filter, 0); last_filter = scale_filter; - } else { - dst_width = src_width; - dst_height = src_height; } // Add format filter if necessary - if (ideal_pix_fmt != input->format && !IsPixelFormatGLSLCompatible(static_cast(input->format))) { + if (ideal_pix_fmt != input->format && !glsl_available) { AVFilterContext* format_filter; snprintf(filter_args, kFilterArgSz, "pix_fmts=%u", ideal_pix_fmt); @@ -1129,6 +1147,7 @@ int FFmpegDecoder::MaximumQueueSize() FFmpegDecoder::Instance::Instance() : fmt_ctx_(nullptr), codec_ctx_(nullptr), + avstream_(nullptr), opts_(nullptr) { } diff --git a/app/codec/ffmpeg/ffmpegdecoder.h b/app/codec/ffmpeg/ffmpegdecoder.h index e018cfc75..cd60c5d05 100644 --- a/app/codec/ffmpeg/ffmpegdecoder.h +++ b/app/codec/ffmpeg/ffmpegdecoder.h @@ -67,6 +67,8 @@ protected: virtual bool ConformAudioInternal(const QVector& filenames, const AudioParams ¶ms, CancelAtom *cancelled) override; virtual void CloseInternal() override; + virtual rational GetAudioStartOffset() const override; + private: class Instance { @@ -148,7 +150,7 @@ private: void ClearFrameCache(); - AVFramePtr RetrieveFrame(const rational &time, CancelAtom *cancelled); + AVFramePtr RetrieveFrame(const rational &time, VideoParams::Interlacing interlacing, CancelAtom *cancelled); void RemoveFirstFrame(); diff --git a/app/core.cpp b/app/core.cpp index 1dd1317c2..a4aaef6fc 100644 --- a/app/core.cpp +++ b/app/core.cpp @@ -1365,6 +1365,11 @@ void Core::PushRecentlyOpenedProject(const QString& s) recent_projects_.move(existing_index, 0); } else { recent_projects_.prepend(s); + + const int kMaximumRecentProjects = 10; + while (recent_projects_.size() > kMaximumRecentProjects) { + recent_projects_.removeLast(); + } } emit OpenRecentListChanged(); diff --git a/app/dialog/export/export.cpp b/app/dialog/export/export.cpp index 6210b63f4..64573cf4f 100644 --- a/app/dialog/export/export.cpp +++ b/app/dialog/export/export.cpp @@ -47,7 +47,8 @@ namespace olive { ExportDialog::ExportDialog(ViewerOutput *viewer_node, bool stills_only_mode, QWidget *parent) : super(parent), viewer_node_(viewer_node), - stills_only_mode_(stills_only_mode) + stills_only_mode_(stills_only_mode), + loading_presets_(false) { QHBoxLayout* layout = new QHBoxLayout(this); @@ -258,7 +259,8 @@ ExportDialog::ExportDialog(ViewerOutput *viewer_node, bool stills_only_mode, QWi // If the viewer already has cached params, use them if (!stills_only_mode_ && viewer_node_->GetLastUsedEncodingParams().IsValid()) { - SetParams(viewer_node_->GetLastUsedEncodingParams()); + // This will automatically set the param data + QtUtils::SetComboBoxData(preset_combobox_, kPresetLastUsed); } else { SetDefaults(); } @@ -432,6 +434,10 @@ void ExportDialog::SavePreset() void ExportDialog::PresetComboBoxChanged() { + if (loading_presets_) { + return; + } + QComboBox *c = static_cast(sender()); int preset_number = c->currentData().toInt(); @@ -536,6 +542,8 @@ void ExportDialog::ResolutionChanged() void ExportDialog::LoadPresets() { + loading_presets_ = true; + preset_combobox_->clear(); presets_.clear(); @@ -562,6 +570,8 @@ void ExportDialog::LoadPresets() f.close(); } } + + loading_presets_ = false; } void ExportDialog::SetDefaultFilename() @@ -697,6 +707,7 @@ EncodingParams ExportDialog::GenerateParams() const void ExportDialog::SetParams(const EncodingParams &e) { format_combobox_->SetFormat(e.format()); + FormatChanged(format_combobox_->GetFormat()); if (e.has_custom_range() && viewer_node_->GetWorkArea()->enabled()) { range_combobox_->setCurrentIndex(kRangeInToOut); diff --git a/app/dialog/export/export.h b/app/dialog/export/export.h index 307e8b0d4..594b16380 100644 --- a/app/dialog/export/export.h +++ b/app/dialog/export/export.h @@ -125,6 +125,8 @@ private: bool stills_only_mode_; + bool loading_presets_; + private slots: void BrowseFilename(); diff --git a/app/node/block/clip/clip.cpp b/app/node/block/clip/clip.cpp index 2d3ac32cb..110a26ee4 100644 --- a/app/node/block/clip/clip.cpp +++ b/app/node/block/clip/clip.cpp @@ -364,7 +364,7 @@ void ClipBlock::InvalidateCache(const TimeRange& range, const QString& from, int } // Find connected viewer node - auto viewers = FindInputNodesConnectedToInput(NodeInput(this, kBufferIn)); + auto viewers = FindInputNodesConnectedToInput(NodeInput(this, kBufferIn), 1); ViewerOutput *new_connected_viewer = viewers.isEmpty() ? nullptr : viewers.first(); if (new_connected_viewer != connected_viewer_) { @@ -543,7 +543,7 @@ TimeRange ClipBlock::media_range() const MultiCamNode *ClipBlock::FindMulticam() { - auto v = FindInputNodesConnectedToInput(NodeInput(this, kBufferIn)); + auto v = FindInputNodesConnectedToInput(NodeInput(this, kBufferIn), 1); if (v.empty()) { return nullptr; } else { diff --git a/app/node/input/multicam/multicamnode.cpp b/app/node/input/multicam/multicamnode.cpp index 8bdb5a5dd..2ff887069 100644 --- a/app/node/input/multicam/multicamnode.cpp +++ b/app/node/input/multicam/multicamnode.cpp @@ -1,11 +1,15 @@ #include "multicamnode.h" +#include "node/project/sequence/sequence.h" + namespace olive { #define super Node const QString MultiCamNode::kCurrentInput = QStringLiteral("current_in"); const QString MultiCamNode::kSourcesInput = QStringLiteral("sources_in"); +const QString MultiCamNode::kSequenceInput = QStringLiteral("sequence_in"); +const QString MultiCamNode::kSequenceTypeInput = QStringLiteral("sequence_type_in"); MultiCamNode::MultiCamNode() { @@ -17,6 +21,11 @@ MultiCamNode::MultiCamNode() AddInput(kSourcesInput, NodeValue::kNone, InputFlags(kInputFlagNotKeyframable | kInputFlagArray)); SetInputProperty(kSourcesInput, QStringLiteral("arraystart"), 1); + + AddInput(kSequenceInput, NodeValue::kNone, InputFlags(kInputFlagNotKeyframable)); + AddInput(kSequenceTypeInput, NodeValue::kCombo, InputFlags(kInputFlagStatic | kInputFlagHidden)); + + sequence_ = nullptr; } QString MultiCamNode::Name() const @@ -43,7 +52,7 @@ Node::ActiveElements MultiCamNode::GetActiveElementsAtTime(const QString &input, { if (input == kSourcesInput) { int src = GetCurrentSource(); - if (src >= 0 && src < InputArraySize(kSourcesInput)) { + if (src >= 0 && src < GetSourceCount()) { Node::ActiveElements a; a.add(src); return a; @@ -71,12 +80,70 @@ void MultiCamNode::IndexToRowCols(int index, int total_rows, int total_cols, int *row = index/total_cols; } +Node *MultiCamNode::GetConnectedRenderOutput(const QString &input, int element) const +{ + if (sequence_ && input == kSourcesInput && element >= 0 && element < GetSourceCount()) { + return GetTrackList()->GetTrackAt(element); + } else { + return Node::GetConnectedRenderOutput(input, element); + } +} + +bool MultiCamNode::IsInputConnectedForRender(const QString &input, int element) const +{ + if (sequence_ && input == kSourcesInput && element >= 0 && element < GetSourceCount()) { + return true; + } else { + return Node::IsInputConnectedForRender(input, element); + } +} + +QVector MultiCamNode::IgnoreInputsForRendering() const +{ + return {kSequenceInput}; +} + +void MultiCamNode::InputConnectedEvent(const QString &input, int element, Node *output) +{ + if (input == kSequenceInput) { + if (Sequence *s = dynamic_cast(output)) { + SetInputFlags(kSequenceTypeInput, GetInputFlags(kSequenceTypeInput) & InputFlag(~kInputFlagHidden)); + sequence_ = s; + } + } +} + +void MultiCamNode::InputDisconnectedEvent(const QString &input, int element, Node *output) +{ + if (input == kSequenceInput) { + SetInputFlags(kSequenceTypeInput, GetInputFlags(kSequenceTypeInput) | kInputFlagHidden); + sequence_ = nullptr; + } +} + +TrackList *MultiCamNode::GetTrackList() const +{ + return sequence_->track_list(static_cast(GetStandardValue(kSequenceTypeInput).toInt())); +} + void MultiCamNode::Retranslate() { super::Retranslate(); SetInputName(kCurrentInput, tr("Current")); SetInputName(kSourcesInput, tr("Sources")); + SetInputName(kSequenceInput, tr("Sequence")); + SetInputName(kSequenceTypeInput, tr("Sequence Type")); + SetComboBoxStrings(kSequenceTypeInput, {tr("Video"), tr("Audio")}); +} + +int MultiCamNode::GetSourceCount() const +{ + if (sequence_) { + return GetTrackList()->GetTrackCount(); + } else { + return InputArraySize(kSourcesInput); + } } void MultiCamNode::GetRowsAndColumns(int sources, int *rows_in, int *cols_in) diff --git a/app/node/input/multicam/multicamnode.h b/app/node/input/multicam/multicamnode.h index 56a9ed858..ead53c63c 100644 --- a/app/node/input/multicam/multicamnode.h +++ b/app/node/input/multicam/multicamnode.h @@ -2,9 +2,12 @@ #define MULTICAMNODE_H #include "node/node.h" +#include "node/output/track/tracklist.h" namespace olive { +class Sequence; + class MultiCamNode : public Node { Q_OBJECT @@ -26,16 +29,15 @@ public: static const QString kCurrentInput; static const QString kSourcesInput; + static const QString kSequenceInput; + static const QString kSequenceTypeInput; int GetCurrentSource() const { return GetStandardValue(kCurrentInput).toInt(); } - int GetSourceCount() const - { - return InputArraySize(kSourcesInput); - } + int GetSourceCount() const; static void GetRowsAndColumns(int sources, int *rows, int *cols); void GetRowsAndColumns(int *rows, int *cols) const @@ -43,6 +45,11 @@ public: return GetRowsAndColumns(GetSourceCount(), rows, cols); } + void SetSequenceType(Track::Type t) + { + SetStandardValue(kSequenceTypeInput, t); + } + static void IndexToRowCols(int index, int total_rows, int total_cols, int *row, int *col); static int RowsColsToIndex(int row, int col, int total_rows, int total_cols) @@ -50,6 +57,20 @@ public: return col + row * total_cols; } + virtual Node *GetConnectedRenderOutput(const QString& input, int element = -1) const override; + virtual bool IsInputConnectedForRender(const QString& input, int element = -1) const override; + + virtual QVector IgnoreInputsForRendering() const override; + +protected: + virtual void InputConnectedEvent(const QString &input, int element, Node *output) override; + virtual void InputDisconnectedEvent(const QString &input, int element, Node *output) override; + +private: + TrackList *GetTrackList() const; + + Sequence *sequence_; + }; } diff --git a/app/node/node.cpp b/app/node/node.cpp index c677b130f..1d8cb3f4c 100644 --- a/app/node/node.cpp +++ b/app/node/node.cpp @@ -1287,6 +1287,26 @@ int Node::GetInternalInputArraySize(const QString &input) return array_immediates_.value(input).size(); } +void FindWaysNodeArrivesHereRecursively(const Node *output, const Node *input, QVector &v) +{ + for (auto it=input->input_connections().cbegin(); it!=input->input_connections().cend(); it++) { + if (it->second == output) { + v.append(it->first); + } else { + FindWaysNodeArrivesHereRecursively(output, it->second, v); + } + } +} + +QVector Node::FindWaysNodeArrivesHere(const Node *output) const +{ + QVector v; + + FindWaysNodeArrivesHereRecursively(output, this, v); + + return v; +} + void Node::SetInputName(const QString &id, const QString &name) { Input* i = GetInternalInputData(id); diff --git a/app/node/node.h b/app/node/node.h index 3ac69a6f9..0dbfdaa3e 100644 --- a/app/node/node.h +++ b/app/node/node.h @@ -212,6 +212,11 @@ public: return input_ids_; } + virtual QVector IgnoreInputsForRendering() const + { + return QVector(); + } + class ActiveElements { public: @@ -418,6 +423,15 @@ public: return IsInputConnected(input.input(), input.element()); } + virtual bool IsInputConnectedForRender(const QString& input, int element = -1) const + { + return IsInputConnected(input, element); + } + bool IsInputConnectedForRender(const NodeInput& input) const + { + return IsInputConnectedForRender(input.input(), input.element()); + } + bool IsInputStatic(const QString& input, int element = -1) const { return !IsInputConnected(input, element) && !IsInputKeyframing(input, element); @@ -435,6 +449,16 @@ public: return GetConnectedOutput(input.input(), input.element()); } + virtual Node *GetConnectedRenderOutput(const QString& input, int element = -1) const + { + return GetConnectedOutput(input, element); + } + + Node *GetConnectedRenderOutput(const NodeInput& input) const + { + return GetConnectedRenderOutput(input.input(), input.element()); + } + bool IsUsingStandardValue(const QString& input, int track, int element = -1) const; NodeValue::Type GetInputDataType(const QString& id) const; @@ -796,6 +820,15 @@ public: */ bool InputsFrom(const QString& id, bool recursively) const; + + /** + * @brief Find inputs that `output` outputs to in order to arrive at this node + * + * Traverse this node's inputs recursively looking for `output`, and return a list of + * edges that `output` uses to get to `this` node. + */ + QVector FindWaysNodeArrivesHere(const Node *output) const; + /** * @brief Determines how many paths go from this node out to another node */ @@ -820,13 +853,13 @@ public: * @brief Find nodes of a certain type that this Node takes inputs from */ template - QVector FindInputNodes() const; + QVector FindInputNodes(int maximum = 0) const; /** * @brief Find nodes of a certain type that this Node takes inputs from */ template - static QVector FindInputNodesConnectedToInput(const NodeInput &input); + static QVector FindInputNodesConnectedToInput(const NodeInput &input, int maximum = 0); template /** @@ -967,6 +1000,90 @@ public: folder_ = folder; } + class ArrayInsertCommand : public UndoCommand + { + public: + ArrayInsertCommand(Node* node, const QString& input, int index) : + node_(node), + input_(input), + index_(index) + { + } + + virtual Project* GetRelevantProject() const override; + + protected: + virtual void redo() override + { + node_->InputArrayInsert(input_, index_, false); + } + + virtual void undo() override + { + node_->InputArrayRemove(input_, index_, false); + } + + private: + Node* node_; + QString input_; + int index_; + + }; + + class ArrayResizeCommand : public UndoCommand + { + public: + ArrayResizeCommand(Node* node, const QString& input, int size) : + node_(node), + input_(input), + size_(size) + {} + + virtual Project* GetRelevantProject() const override; + + protected: + virtual void redo() override + { + old_size_ = node_->InputArraySize(input_); + + if (old_size_ > size_) { + // Decreasing in size, disconnect any extraneous edges + for (int i=size_; iinput_connections().at(input); + + removed_connections_[input] = output; + + DisconnectEdge(output, input); + } catch (std::out_of_range&) {} + } + } + + node_->ArrayResizeInternal(input_, size_); + } + + virtual void undo() override + { + for (auto it=removed_connections_.cbegin(); it!=removed_connections_.cend(); it++) { + ConnectEdge(it->second, it->first); + } + removed_connections_.clear(); + + node_->ArrayResizeInternal(input_, old_size_); + } + + private: + Node* node_; + QString input_; + int size_; + int old_size_; + + InputConnections removed_connections_; + + }; + class ArrayRemoveCommand : public UndoCommand { public: @@ -1203,90 +1320,6 @@ signals: void InputFlagsChanged(const QString &input, const InputFlags &flags); private: - class ArrayInsertCommand : public UndoCommand - { - public: - ArrayInsertCommand(Node* node, const QString& input, int index) : - node_(node), - input_(input), - index_(index) - { - } - - virtual Project* GetRelevantProject() const override; - - protected: - virtual void redo() override - { - node_->InputArrayInsert(input_, index_, false); - } - - virtual void undo() override - { - node_->InputArrayRemove(input_, index_, false); - } - - private: - Node* node_; - QString input_; - int index_; - - }; - - class ArrayResizeCommand : public UndoCommand - { - public: - ArrayResizeCommand(Node* node, const QString& input, int size) : - node_(node), - input_(input), - size_(size) - {} - - virtual Project* GetRelevantProject() const override; - - protected: - virtual void redo() override - { - old_size_ = node_->InputArraySize(input_); - - if (old_size_ > size_) { - // Decreasing in size, disconnect any extraneous edges - for (int i=size_; iinput_connections().at(input); - - removed_connections_[input] = output; - - DisconnectEdge(output, input); - } catch (std::out_of_range&) {} - } - } - - node_->ArrayResizeInternal(input_, size_); - } - - virtual void undo() override - { - for (auto it=removed_connections_.cbegin(); it!=removed_connections_.cend(); it++) { - ConnectEdge(it->second, it->first); - } - removed_connections_.clear(); - - node_->ArrayResizeInternal(input_, old_size_); - } - - private: - Node* node_; - QString input_; - int size_; - int old_size_; - - InputConnections removed_connections_; - - }; - struct Input { NodeValue::Type type; InputFlags flags; @@ -1367,10 +1400,10 @@ private: * @brief Find nodes of a certain type that this Node takes inputs from */ template - static void FindInputNodesConnectedToInputInternal(const NodeInput &input, QVector& list); + static void FindInputNodesConnectedToInputInternal(const NodeInput &input, QVector& list, int maximum); template - static void FindInputNodeInternal(const Node* n, QVector& list); + static void FindInputNodeInternal(const Node* n, QVector& list, int maximum); template static void FindOutputNodeInternal(const Node* n, QVector& list); @@ -1479,7 +1512,7 @@ private slots: }; template -void Node::FindInputNodesConnectedToInputInternal(const NodeInput &input, QVector &list) +void Node::FindInputNodesConnectedToInputInternal(const NodeInput &input, QVector &list, int maximum) { Node* edge = input.GetConnectedOutput(); if (!edge) { @@ -1490,35 +1523,38 @@ void Node::FindInputNodesConnectedToInputInternal(const NodeInput &input, QVecto if (cast_test) { list.append(cast_test); + if (maximum != 0 && list.size() == maximum) { + return; + } } - FindInputNodeInternal(edge, list); + FindInputNodeInternal(edge, list, maximum); } template -QVector Node::FindInputNodesConnectedToInput(const NodeInput &input) +QVector Node::FindInputNodesConnectedToInput(const NodeInput &input, int maximum) { QVector list; - FindInputNodesConnectedToInputInternal(input, list); + FindInputNodesConnectedToInputInternal(input, list, maximum); return list; } template -void Node::FindInputNodeInternal(const Node* n, QVector &list) +void Node::FindInputNodeInternal(const Node* n, QVector &list, int maximum) { for (auto it=n->input_connections_.cbegin(); it!=n->input_connections_.cend(); it++) { - FindInputNodesConnectedToInputInternal(it->first, list); + FindInputNodesConnectedToInputInternal(it->first, list, maximum); } } template -QVector Node::FindInputNodes() const +QVector Node::FindInputNodes(int maximum) const { QVector list; - FindInputNodeInternal(this, list); + FindInputNodeInternal(this, list, maximum); return list; } @@ -1540,7 +1576,7 @@ void Node::FindOutputNodeInternal(const Node* n, QVector& list) list.append(cast_test); } - FindOutputNodeInternal(connected); + FindOutputNodeInternal(connected, list); } } diff --git a/app/node/output/viewer/viewer.cpp b/app/node/output/viewer/viewer.cpp index 34ec101d6..50d90f0b3 100644 --- a/app/node/output/viewer/viewer.cpp +++ b/app/node/output/viewer/viewer.cpp @@ -39,7 +39,8 @@ ViewerOutput::ViewerOutput(bool create_buffer_inputs, bool create_default_stream video_length_(0), audio_length_(0), autocache_input_video_(false), - autocache_input_audio_(false) + autocache_input_audio_(false), + waveform_requests_enabled_(false) { AddInput(kVideoParamsInput, NodeValue::kVideoParams, InputFlags(kInputFlagNotConnectable | kInputFlagNotKeyframable | kInputFlagArray | kInputFlagHidden)); @@ -231,7 +232,9 @@ void ViewerOutput::InvalidateCache(const TimeRange& range, const QString& from, } } else if (from == kSamplesInput) { TimeRange max_range = InputTimeAdjustment(from, element, TimeRange(0, GetAudioLength())); - connected->waveform_cache()->Request(range.Intersected(max_range)); + if (waveform_requests_enabled_) { + connected->waveform_cache()->Request(range.Intersected(max_range)); + } if (autocache_input_audio_) { connected->audio_playback_cache()->Request(range.Intersected(max_range)); } @@ -387,13 +390,15 @@ Node::ValueHint ViewerOutput::GetConnectedSampleValueHint() return GetValueHintForInput(kSamplesInput); } -void ViewerOutput::ConnectedToPreviewEvent() +void ViewerOutput::SetWaveformEnabled(bool e) { - if (Node *connected = this->GetConnectedSampleOutput()) { - TimeRange max_range = InputTimeAdjustment(kSamplesInput, -1, TimeRange(0, GetAudioLength())); - TimeRangeList invalid = connected->waveform_cache()->GetInvalidatedRanges(max_range); - for (const TimeRange &r : invalid) { - connected->waveform_cache()->Request(r); + if ((waveform_requests_enabled_ = e)) { + if (Node *connected = this->GetConnectedSampleOutput()) { + TimeRange max_range = InputTimeAdjustment(kSamplesInput, -1, TimeRange(0, GetAudioLength())); + TimeRangeList invalid = connected->waveform_cache()->GetInvalidatedRanges(max_range); + for (const TimeRange &r : invalid) { + connected->waveform_cache()->Request(r); + } } } } diff --git a/app/node/output/viewer/viewer.h b/app/node/output/viewer/viewer.h index 4a1bf10a2..b99ebc30b 100644 --- a/app/node/output/viewer/viewer.h +++ b/app/node/output/viewer/viewer.h @@ -182,7 +182,7 @@ public: virtual ValueHint GetConnectedSampleValueHint(); - virtual void ConnectedToPreviewEvent() override; + void SetWaveformEnabled(bool e); bool IsVideoAutoCacheEnabled() const { qDebug() << "sequence ac is a stub"; return false; } void SetVideoAutoCacheEnabled(bool e) { qDebug() << "sequence ac is a stub"; } @@ -251,6 +251,8 @@ private: EncodingParams last_used_encoding_params_; + bool waveform_requests_enabled_; + }; } diff --git a/app/node/project/footage/footage.cpp b/app/node/project/footage/footage.cpp index 307ce247b..210d448f5 100644 --- a/app/node/project/footage/footage.cpp +++ b/app/node/project/footage/footage.cpp @@ -276,7 +276,8 @@ void Footage::Value(const NodeValueRow &value, const NodeGlobals &globals, NodeV // Adjust footage job's divider if (globals.vparams().divider() > 1) { // Use a divider appropriate for this target resolution - vp.set_divider(VideoParams::GetDividerForTargetResolution(vp.width(), vp.height(), globals.vparams().effective_width(), globals.vparams().effective_height())); + int calculated = VideoParams::GetDividerForTargetResolution(vp.width(), vp.height(), globals.vparams().effective_width(), globals.vparams().effective_height()); + vp.set_divider(std::min(calculated, globals.vparams().divider())); } else { // Render everything at full res vp.set_divider(1); @@ -544,7 +545,12 @@ void Footage::CheckFootage() if (!fn.isEmpty()) { QFileInfo info(fn); - qint64 current_file_timestamp = info.lastModified().toMSecsSinceEpoch(); + qint64 current_file_timestamp; + if (!info.lastModified().isValid()) { + current_file_timestamp = 0; + } else { + current_file_timestamp = info.lastModified().toMSecsSinceEpoch(); + } if (current_file_timestamp != timestamp()) { // File has changed! diff --git a/app/node/traverser.cpp b/app/node/traverser.cpp index bb95a6463..ee1fa0fbd 100644 --- a/app/node/traverser.cpp +++ b/app/node/traverser.cpp @@ -38,11 +38,16 @@ NodeValueDatabase NodeTraverser::GenerateDatabase(const Node* node, const TimeRa } // We need to insert tables into the database for each input + auto ignore = node->IgnoreInputsForRendering(); foreach (const QString& input, node->inputs()) { if (IsCancelled()) { return NodeValueDatabase(); } + if (ignore.contains(input)) { + continue; + } + database.Insert(input, ProcessInput(node, input, range)); } @@ -187,12 +192,12 @@ NodeGlobals NodeTraverser::GenerateGlobals(const VideoParams &vparams, const Aud NodeValueTable NodeTraverser::ProcessInput(const Node* node, const QString& input, const TimeRange& range) { // If input is connected, retrieve value directly - if (node->IsInputConnected(input)) { + if (node->IsInputConnectedForRender(input)) { TimeRange adjusted_range = node->InputTimeAdjustment(input, -1, range); // Value will equal something from the connected node, follow it - Node *output = node->GetConnectedOutput(input); + Node *output = node->GetConnectedRenderOutput(input); NodeValueTable table = GenerateTable(output, adjusted_range, node); return table; @@ -242,8 +247,8 @@ void NodeTraverser::ProcessInputElement(NodeValueTableArray &array_tbl, const No NodeValueTable& sub_tbl = array_tbl[element]; TimeRange adjusted_range = node->InputTimeAdjustment(input, element, range); - if (node->IsInputConnected(input, element)) { - Node *output = node->GetConnectedOutput(input, element); + if (node->IsInputConnectedForRender(input, element)) { + Node *output = node->GetConnectedRenderOutput(input, element); sub_tbl = GenerateTable(output, adjusted_range, node); } else { QVariant input_value = node->GetValueAtTime(input, adjusted_range.in(), element); diff --git a/app/render/audioparams.cpp b/app/render/audioparams.cpp index d5dd635b7..3712f8cde 100644 --- a/app/render/audioparams.cpp +++ b/app/render/audioparams.cpp @@ -350,4 +350,9 @@ AudioParams::Format AudioParams::GetPlanarEquivalent(Format fmt) return kFormatInvalid; } +void AudioParams::calculate_channel_count() +{ + channel_count_ = av_get_channel_layout_nb_channels(channel_layout()); +} + } diff --git a/app/render/audioparams.h b/app/render/audioparams.h index a58767328..aff223239 100644 --- a/app/render/audioparams.h +++ b/app/render/audioparams.h @@ -256,10 +256,7 @@ private: duration_ = 0; } - void calculate_channel_count() - { - channel_count_ = av_get_channel_layout_nb_channels(channel_layout()); - } + void calculate_channel_count(); int sample_rate_; diff --git a/app/render/opengl/openglrenderer.cpp b/app/render/opengl/openglrenderer.cpp index 2cb608783..b0c91d7ab 100644 --- a/app/render/opengl/openglrenderer.cpp +++ b/app/render/opengl/openglrenderer.cpp @@ -364,7 +364,7 @@ void OpenGLRenderer::Flush() { GL_PREAMBLE; - functions_->glFinish(); + functions_->glFlush(); } Color OpenGLRenderer::GetPixelFromTexture(Texture *texture, const QPointF &pt) diff --git a/app/render/previewautocacher.cpp b/app/render/previewautocacher.cpp index 59ff3d3c9..08624be67 100644 --- a/app/render/previewautocacher.cpp +++ b/app/render/previewautocacher.cpp @@ -41,6 +41,7 @@ PreviewAutoCacher::PreviewAutoCacher(QObject *parent) : viewer_node_(nullptr), use_custom_range_(false), pause_renders_(false), + pause_thumbnails_(false), single_frame_render_(nullptr), display_color_processor_(nullptr), multicam_(nullptr), @@ -583,6 +584,14 @@ void PreviewAutoCacher::SetRendersPaused(bool e) } } +void PreviewAutoCacher::SetThumbnailsPaused(bool e) +{ + pause_thumbnails_ = e; + if (!e) { + TryRender(); + } +} + void PreviewAutoCacher::NodeAdded(Node *node) { graph_update_queue_.push_back({QueuedJob::kNodeAdded, node, NodeInput(), nullptr}); @@ -663,29 +672,31 @@ void PreviewAutoCacher::TryRender() const int max_tasks = 4; // Handle video tasks - while (!pending_video_jobs_.empty()) { - VideoJob &d = pending_video_jobs_.front(); + if (!pause_thumbnails_) { + while (!pending_video_jobs_.empty()) { + VideoJob &d = pending_video_jobs_.front(); - if (Node *copy = copy_map_.value(d.node)) { - // Queue next frames - rational t; - while (running_video_tasks_.size() < max_tasks && d.iterator.GetNext(&t)) { - RenderFrame(copy, t, d.cache, false); + if (Node *copy = copy_map_.value(d.node)) { + // Queue next frames + rational t; + while (running_video_tasks_.size() < max_tasks && d.iterator.GetNext(&t)) { + RenderFrame(copy, t, d.cache, false); - emit SignalCacheProxyTaskProgress(double(d.iterator.frame_index()) / double(d.iterator.size())); + emit SignalCacheProxyTaskProgress(double(d.iterator.frame_index()) / double(d.iterator.size())); - if (!d.iterator.HasNext()) { - emit StopCacheProxyTasks(); + if (!d.iterator.HasNext()) { + emit StopCacheProxyTasks(); + } } + } else { + qCritical() << "Failed to find node copy for video job"; } - } else { - qCritical() << "Failed to find node copy for video job"; - } - if (d.iterator.HasNext()) { - break; - } else { - pending_video_jobs_.pop_front(); + if (d.iterator.HasNext()) { + break; + } else { + pending_video_jobs_.pop_front(); + } } } diff --git a/app/render/previewautocacher.h b/app/render/previewautocacher.h index 28c16deb7..21efb9912 100644 --- a/app/render/previewautocacher.h +++ b/app/render/previewautocacher.h @@ -89,6 +89,7 @@ public: bool IsRenderingCustomRange() const; void SetRendersPaused(bool e); + void SetThumbnailsPaused(bool e); void SetMulticamNode(MultiCamNode *n) { multicam_ = n; } @@ -178,6 +179,7 @@ private: TimeRange custom_autocache_range_; bool pause_renders_; + bool pause_thumbnails_; RenderTicketPtr single_frame_render_; QMap > video_immediate_passthroughs_; diff --git a/app/render/renderprocessor.cpp b/app/render/renderprocessor.cpp index 2b42f9e67..d723d54e7 100644 --- a/app/render/renderprocessor.cpp +++ b/app/render/renderprocessor.cpp @@ -300,13 +300,11 @@ NodeValueDatabase RenderProcessor::GenerateDatabase(const Node *node, const Time if (const MultiCamNode *multicam = dynamic_cast(node)) { if (Node::ValueToPtr(ticket_->property("multicam")) == multicam) { - int sz = multicam->InputArraySize(multicam->kSourcesInput); - NodeValueTableArray arr; + int sz = multicam->GetSourceCount(); QVector multicam_tex(sz); for (int i=0; ikSourcesInput, i, range); - - NodeValue val = GenerateRowValueElement(multicam, multicam->kSourcesInput, i, &arr.at(i), range); + NodeValueTable t = GenerateTable(multicam->GetConnectedRenderOutput(multicam->kSourcesInput, i), range, multicam); + NodeValue val = GenerateRowValueElement(multicam, multicam->kSourcesInput, i, &t, range); ResolveJobs(val); multicam_tex[i] = val.toTexture(); diff --git a/app/render/videoparams.cpp b/app/render/videoparams.cpp index 7ac14904e..77d785d24 100644 --- a/app/render/videoparams.cpp +++ b/app/render/videoparams.cpp @@ -164,6 +164,7 @@ bool VideoParams::operator==(const VideoParams &rhs) const return width() == rhs.width() && height() == rhs.height() && depth() == rhs.depth() + && interlacing() == rhs.interlacing() && time_base() == rhs.time_base() && format() == rhs.format() && pixel_aspect_ratio() == rhs.pixel_aspect_ratio() @@ -262,7 +263,7 @@ void VideoParams::calculate_effective_size() { effective_width_ = GetScaledDimension(width(), divider_); effective_height_ = GetScaledDimension(height(), divider_); - effective_depth_ = GetScaledDimension(depth(), divider_); + effective_depth_ = (depth() == 1) ? depth() : GetScaledDimension(depth(), divider_); calculate_square_pixel_width(); } diff --git a/app/shaders/chromakey.frag b/app/shaders/chromakey.frag index dc665b2e4..d3ee70e6c 100644 --- a/app/shaders/chromakey.frag +++ b/app/shaders/chromakey.frag @@ -100,8 +100,7 @@ void main() { mask = 1.0 - mask; } - col.rgb *= mask; - col.w = mask; + col *= mask; if (!mask_only_in) { frag_color = col; diff --git a/app/widget/multicam/multicamwidget.cpp b/app/widget/multicam/multicamwidget.cpp index 6afbd9ac8..5156fd4ed 100644 --- a/app/widget/multicam/multicamwidget.cpp +++ b/app/widget/multicam/multicamwidget.cpp @@ -58,10 +58,18 @@ MulticamWidget::MulticamWidget(QWidget *parent) : void MulticamWidget::SetMulticamNodeInternal(ViewerOutput *viewer, MultiCamNode *n, ClipBlock *clip) { - ConnectViewerNode(viewer); - node_ = n; - display_->SetMulticamNode(n); - clip_ = clip; + if (GetConnectedNode() != viewer) { + ConnectViewerNode(viewer); + } + + if (node_ != n) { + node_ = n; + display_->SetMulticamNode(n); + } + + if (clip_ != clip) { + clip_ = clip; + } } void MulticamWidget::SetMulticamNode(ViewerOutput *viewer, MultiCamNode *n, ClipBlock *clip, const rational &time) @@ -145,6 +153,8 @@ void MulticamWidget::Switch(int source, bool split_clip) Core::instance()->undo_stack()->push(command); display_->update(); + + emit Switched(); } void MulticamWidget::DisplayClicked(const QPoint &p) diff --git a/app/widget/multicam/multicamwidget.h b/app/widget/multicam/multicamwidget.h index 8691c5668..a6e5f311a 100644 --- a/app/widget/multicam/multicamwidget.h +++ b/app/widget/multicam/multicamwidget.h @@ -42,6 +42,9 @@ protected: virtual void DisconnectNodeEvent(ViewerOutput *n) override; virtual void TimeChangedEvent(const rational &t) override; +signals: + void Switched(); + private: void SetMulticamNodeInternal(ViewerOutput *viewer, MultiCamNode *n, ClipBlock *clip); diff --git a/app/widget/timelinewidget/timelinewidget.cpp b/app/widget/timelinewidget/timelinewidget.cpp index 9941d061a..b68881868 100644 --- a/app/widget/timelinewidget/timelinewidget.cpp +++ b/app/widget/timelinewidget/timelinewidget.cpp @@ -1235,30 +1235,23 @@ void TimelineWidget::ShowContextMenu() reveal_in_project->setData(reinterpret_cast(clip->connected_viewer())); connect(reveal_in_project, &QAction::triggered, this, &TimelineWidget::RevealInProject); - /*if (Sequence *sequence = dynamic_cast(clip->connected_viewer())) { - Menu *multicam_menu = new Menu(tr("Multi-Cam"), &menu); - menu.addMenu(multicam_menu); - - QAction *multicam_enabled = multicam_menu->addAction(tr("Enabled")); + if (Sequence *sequence = dynamic_cast(clip->connected_viewer())) { + QAction *multicam_enabled = menu.addAction(tr("Multi-Cam")); multicam_enabled->setCheckable(true); - auto mcn = sequence->FindOutputNode(); - multicam_enabled->setChecked(!mcn.empty()); + MultiCamNode *mcn = nullptr; + auto paths = clip->FindWaysNodeArrivesHere(sequence); - multicam_menu->addSeparator(); - - QAction *multicam_update = multicam_menu->addAction(tr("Update")); - multicam_update->setEnabled(!mcn.empty()); - - if (!mcn.empty()) { - auto n = mcn.first(); - multicam_enabled->setProperty("multicam", Node::PtrToValue(n)); - multicam_update->setProperty("multicam", Node::PtrToValue(n)); + for (const NodeInput &i : paths) { + if ((mcn = dynamic_cast(i.node()))) { + break; + } } + multicam_enabled->setChecked(mcn); + connect(multicam_enabled, &QAction::triggered, this, &TimelineWidget::MulticamEnabledTriggered); - connect(multicam_update, &QAction::triggered, this, &TimelineWidget::MulticamUpdateTriggered); - }*/ + } } } @@ -1492,16 +1485,57 @@ void TimelineWidget::CacheDiscard() void TimelineWidget::MulticamEnabledTriggered(bool e) { - if (e) { - // Add multicam node - } else if (MultiCamNode *m = Node::ValueToPtr(sender()->property("multicam"))) { - // Remove multicam node - } -} + MultiUndoCommand *command = new MultiUndoCommand(); -void TimelineWidget::MulticamUpdateTriggered() -{ - // Update multicam node + for (Block *b : qAsConst(selected_blocks_)) { + if (ClipBlock *c = dynamic_cast(b)) { + if (Sequence *s = dynamic_cast(c->connected_viewer())) { + if (e) { + + // Adding multicams + // Create multicam node and add it to the graph + MultiCamNode *n = new MultiCamNode(); + n->SetSequenceType(c->GetTrackType()); + command->add_child(new NodeAddCommand(s->parent(), n)); + + + // For each output the sequence has to this clip, disconnect it and + // connect to the multicam instead + QVector inputs = c->FindWaysNodeArrivesHere(s); + for (const NodeInput &i : inputs) { + command->add_child(new NodeEdgeRemoveCommand(s, i)); + command->add_child(new NodeEdgeAddCommand(n, i)); + } + + command->add_child(new NodeEdgeAddCommand(s, NodeInput(n, n->kSequenceInput))); + + // Move sequence node one unit back, and place multicam in sequence's spot + QPointF sequence_pos = c->GetNodePositionInContext(s); + command->add_child(new NodeSetPositionCommand(s, c, sequence_pos - QPointF(1, 0))); + command->add_child(new NodeSetPositionCommand(n, c, sequence_pos)); + + } else { + + // Removing multicams + // Locate first multicam that specifically ends up at this clip + QVector inputs = c->FindWaysNodeArrivesHere(s); + for (const NodeInput &i : inputs) { + if (MultiCamNode *mcn = dynamic_cast(i.node())) { + for (auto it=mcn->output_connections().cbegin(); it!=mcn->output_connections().cend(); it++) { + command->add_child(new NodeEdgeRemoveCommand(it->first, it->second)); + command->add_child(new NodeEdgeAddCommand(s, it->second)); + } + + command->add_child(new NodeRemoveAndDisconnectCommand(mcn)); + } + } + + } + } + } + } + + Core::instance()->undo_stack()->pushIfHasChildren(command); } void TimelineWidget::AddGhost(TimelineViewGhostItem *ghost) diff --git a/app/widget/timelinewidget/timelinewidget.h b/app/widget/timelinewidget/timelinewidget.h index 998e56869..5161f6d55 100644 --- a/app/widget/timelinewidget/timelinewidget.h +++ b/app/widget/timelinewidget/timelinewidget.h @@ -447,7 +447,6 @@ private slots: void CacheDiscard(); void MulticamEnabledTriggered(bool e); - void MulticamUpdateTriggered(); }; diff --git a/app/widget/viewer/viewer.cpp b/app/widget/viewer/viewer.cpp index 1ec330c19..3beb3f44e 100644 --- a/app/widget/viewer/viewer.cpp +++ b/app/widget/viewer/viewer.cpp @@ -427,7 +427,15 @@ void ViewerWidget::StartCapture(TimelineWidget *source, const TimeRange &time, c void ViewerWidget::ConnectMulticamWidget(MulticamWidget *p) { + if (multicam_panel_) { + disconnect(multicam_panel_, &MulticamWidget::Switched, this, &ViewerWidget::DetectMulticamNodeNow); + } + multicam_panel_ = p; + + if (multicam_panel_) { + connect(multicam_panel_, &MulticamWidget::Switched, this, &ViewerWidget::DetectMulticamNodeNow); + } } FramePtr ViewerWidget::DecodeCachedImage(const QString &cache_path, const QUuid &cache_id, const int64_t& time) @@ -607,6 +615,11 @@ void ViewerWidget::SaveFrameAsImage() Core::instance()->OpenExportDialogForViewer(GetConnectedNode(), GetTime(), true); } +void ViewerWidget::DetectMulticamNodeNow() +{ + DetectMulticamNode(GetTime()); +} + void ViewerWidget::CloseAudioProcessor() { audio_processor_.Close(); @@ -695,6 +708,10 @@ void ViewerWidget::UpdateWaveformViewFromMode() waveform_view_->setVisible(waveform_mode_ == kWFViewerAndWaveform || waveform_mode_ == kWFWaveformOnly || (waveform_mode_ == kWFAutomatic && prefer_waveform)); waveform_view_->setSizePolicy(QSizePolicy::Expanding, waveform_mode_ == kWFViewerAndWaveform ? QSizePolicy::Maximum : QSizePolicy::Expanding); + + if (GetConnectedNode()) { + GetConnectedNode()->SetWaveformEnabled(waveform_view_->isVisible()); + } } void ViewerWidget::QueueNextAudioBuffer() @@ -903,7 +920,7 @@ void ViewerWidget::PlayInternal(int speed, bool in_to_out_only) if (viewer != this) { viewer->PauseInternal(); } - viewer->auto_cacher_->SetRendersPaused(true); + viewer->auto_cacher_->SetThumbnailsPaused(true); } RenderManager::instance()->SetAggressiveGarbageCollection(true); @@ -1005,7 +1022,7 @@ void ViewerWidget::PauseInternal() UpdateAudioProcessor(); foreach (ViewerWidget* viewer, instances_) { - viewer->auto_cacher_->SetRendersPaused(false); + viewer->auto_cacher_->SetThumbnailsPaused(false); } UpdateTextureFromNode(); @@ -1796,7 +1813,7 @@ void ViewerWidget::SetZoomFromMenu(QAction *action) void ViewerWidget::ViewerInvalidatedVideoRange(const TimeRange &range) { // If our current frame is within this range, we need to update - if (GetTime() >= range.in() && (GetTime() < range.out() || range.in() == range.out())) { + if (!IsPlaying() && GetTime() >= range.in() && (GetTime() < range.out() || range.in() == range.out())) { QMetaObject::invokeMethod(this, &ViewerWidget::UpdateTextureFromNode, Qt::QueuedConnection); } } diff --git a/app/widget/viewer/viewer.h b/app/widget/viewer/viewer.h index c465aabc9..e4f0d7289 100644 --- a/app/widget/viewer/viewer.h +++ b/app/widget/viewer/viewer.h @@ -117,7 +117,7 @@ public: if (!IsPlaying()) { // If is playing, this will happen by the next frame automatically - DetectMulticamNode(GetTime()); + DetectMulticamNodeNow(); UpdateTextureFromNode(); } } @@ -128,7 +128,7 @@ public: if (!IsPlaying()) { // If is playing, this will happen by the next frame automatically - DetectMulticamNode(GetTime()); + DetectMulticamNodeNow(); UpdateTextureFromNode(); } } @@ -421,6 +421,8 @@ private slots: void SaveFrameAsImage(); + void DetectMulticamNodeNow(); + }; } diff --git a/app/window/mainwindow/mainwindow.cpp b/app/window/mainwindow/mainwindow.cpp index 79b589901..20adf7800 100644 --- a/app/window/mainwindow/mainwindow.cpp +++ b/app/window/mainwindow/mainwindow.cpp @@ -555,7 +555,9 @@ void MainWindow::UpdateTitle() void MainWindow::TimelineCloseRequested() { - RemoveTimelinePanel(static_cast(sender())); + TimelinePanel *t = static_cast(sender()); + RemoveTimelinePanel(t); + main_time_panels_.removeOne(t); } void MainWindow::ProjectCloseRequested()