From 8d26f5d78a8017c1ab6704a3b5928f411b7351ca Mon Sep 17 00:00:00 2001 From: brunoherbelin Date: Sat, 15 Nov 2025 08:58:41 +0100 Subject: [PATCH] Add Flag functionality to ControlManager and InputMappingWindow with a new Flag SourceCallback. Fixed media player window control. --- src/ControlManager.cpp | 7 ++++ src/ControlManager.h | 1 + src/InputMappingWindow.cpp | 44 ++++++++++++++++++++--- src/InputMappingWindow.h | 2 +- src/MediaPlayer.cpp | 13 +++++-- src/SessionCreator.cpp | 6 ++++ src/SessionCreator.h | 1 + src/SessionVisitor.cpp | 5 +++ src/SessionVisitor.h | 1 + src/SourceCallback.cpp | 70 +++++++++++++++++++++++++++++++++++++ src/SourceCallback.h | 17 +++++++++ src/SourceControlWindow.cpp | 31 ++++++++-------- src/Visitor.h | 1 + src/defines.h | 4 +-- 14 files changed, 176 insertions(+), 27 deletions(-) diff --git a/src/ControlManager.cpp b/src/ControlManager.cpp index d6195a9..2a6dc35 100644 --- a/src/ControlManager.cpp +++ b/src/ControlManager.cpp @@ -970,6 +970,13 @@ bool Control::receiveSourceAttribute(Source *target, const std::string &attribut } target->setImageProcessingEnabled(on > 0.5f); } + else if ( attribute.compare(OSC_SOURCE_FLAG) == 0) { + float f = -1.f; + if (!arguments.Eos()) { + arguments >> f >> osc::EndMessage; + } + target->call( new Flag( f )); + } /// e.g. '/vimix/current/seek f 0.25' ; seek to 25% of duration /// e.g. '/vimix/current/seek iiii 0 0 25 500' ; seek to time else if ( attribute.compare(OSC_SOURCE_SEEK) == 0) { diff --git a/src/ControlManager.h b/src/ControlManager.h index 2ba62f5..c9e48dd 100644 --- a/src/ControlManager.h +++ b/src/ControlManager.h @@ -78,6 +78,7 @@ #define OSC_SOURCE_FILTER "/filter" #define OSC_SOURCE_UNIFORM "/uniform" #define OSC_SOURCE_BLENDING "/blending" +#define OSC_SOURCE_FLAG "/flag" #define OSC_SESSION "/session" #define OSC_SESSION_VERSION "/version" diff --git a/src/InputMappingWindow.cpp b/src/InputMappingWindow.cpp index e637e0b..376b0a6 100644 --- a/src/InputMappingWindow.cpp +++ b/src/InputMappingWindow.cpp @@ -17,6 +17,7 @@ * along with this program. If not, see . **/ +#include "MediaSource.h" #include #include @@ -39,6 +40,7 @@ #include "SourceCallback.h" #include "ControlManager.h" #include "Metronome.h" +#include "MediaPlayer.h" #include "InputMappingWindow.h" @@ -119,9 +121,9 @@ Target InputMappingWindow::ComboSelectTarget(const Target ¤t) return selected; } -uint InputMappingWindow::ComboSelectCallback(uint current, bool imageprocessing) +uint InputMappingWindow::ComboSelectCallback(uint current, bool imageprocessing, bool ismediaplayer) { - const char* callback_names[23] = { "Select", + const char* callback_names[24] = { "Select", ICON_FA_BULLSEYE " Alpha", ICON_FA_BULLSEYE " Loom", ICON_FA_OBJECT_UNGROUP " Geometry", @@ -133,6 +135,7 @@ uint InputMappingWindow::ComboSelectCallback(uint current, bool imageprocessing) ICON_FA_PLAY_CIRCLE " Speed", ICON_FA_PLAY_CIRCLE " Fast forward", ICON_FA_PLAY_CIRCLE " Seek", + ICON_FA_PLAY_CIRCLE " Flag", " None", " None", " None", @@ -148,7 +151,8 @@ uint InputMappingWindow::ComboSelectCallback(uint current, bool imageprocessing) uint selected = 0; if (ImGui::BeginCombo("##ComboSelectCallback", callback_names[current]) ) { - for (uint i = SourceCallback::CALLBACK_ALPHA; i <= SourceCallback::CALLBACK_SEEK; ++i){ + for (uint i = SourceCallback::CALLBACK_ALPHA; + i <= (ismediaplayer ? SourceCallback::CALLBACK_FLAG : SourceCallback::CALLBACK_PLAY) ; ++i){ if ( ImGui::Selectable( callback_names[i]) ) { selected = i; } @@ -488,6 +492,32 @@ void InputMappingWindow::SliderParametersCallback(SourceCallback *callback, cons } break; + case SourceCallback::CALLBACK_FLAG: + { + Flag *edited = static_cast(callback); + + ImGuiToolkit::Indication(press_tooltip[0], 2, 13); + ImGui::SameLine(0, IMGUI_SAME_LINE / 2); + + int max = -1; + if (Source * const* v = std::get_if(&target)) { + MediaSource *ms = dynamic_cast(*v); + if (ms) + max = ms->mediaplayer()->timeline()->numFlags() - 1; + } + int val = MIN( (int) edited->value(), max); + + ImGui::SetNextItemWidth(right_align); + ImGui::SameLine(0, IMGUI_SAME_LINE / 2); + if (ImGui::SliderInt("##CALLBACK_PLAY_FLAG", &val, -1, max, val < 0 ? "Next Flag" : "Flag <%d>")) + edited->setValue(val ); + + ImGui::SameLine(0, IMGUI_SAME_LINE / 3); + ImGuiToolkit::Indication("Flag to jump to in a video source.", 12, 6); + + } + break; + case SourceCallback::CALLBACK_BRIGHTNESS: { SetBrightness *edited = static_cast(callback); @@ -1362,17 +1392,19 @@ void InputMappingWindow::Render() } // check if target is a Source with image processing enabled + bool ismediaplayer = false; bool withimageprocessing = false; if ( target.index() == 1 ) { if (Source * const* v = std::get_if(&target)) { withimageprocessing = (*v)->imageProcessingEnabled(); + ismediaplayer = dynamic_cast(*v) != nullptr; } } // Select Reaction ImGui::SameLine(0, IMGUI_SAME_LINE); ImGui::SetNextItemWidth(w); - uint type = ComboSelectCallback( callback->type(), withimageprocessing ); + uint type = ComboSelectCallback( callback->type(), withimageprocessing, ismediaplayer ); if (type > 0) { // remove previous callback S->deleteInputCallback(callback); @@ -1431,16 +1463,18 @@ void InputMappingWindow::Render() // possible new target if (temp_new_target.index() > 0) { // check if target is a Source with image processing enabled + bool mediaplayer = false; bool withimageprocessing = false; if ( temp_new_target.index() == 1 ) { if (Source * const* v = std::get_if(&temp_new_target)) { withimageprocessing = (*v)->imageProcessingEnabled(); + mediaplayer = dynamic_cast(*v) != nullptr; } } // step 3: Get input for callback type ImGui::SameLine(0, IMGUI_SAME_LINE); ImGui::SetNextItemWidth(w); - temp_new_callback = ComboSelectCallback( temp_new_callback, withimageprocessing ); + temp_new_callback = ComboSelectCallback( temp_new_callback, withimageprocessing, mediaplayer ); // user selected a callback type if (temp_new_callback > 0) { // step 4 : create new callback and add it to source diff --git a/src/InputMappingWindow.h b/src/InputMappingWindow.h index 4b7fc99..de38163 100644 --- a/src/InputMappingWindow.h +++ b/src/InputMappingWindow.h @@ -16,7 +16,7 @@ class InputMappingWindow : public WorkspaceWindow uint current_input_; Target ComboSelectTarget(const Target ¤t); - uint ComboSelectCallback(uint current, bool imageprocessing); + uint ComboSelectCallback(uint current, bool imageprocessing, bool mediaplayer); void SliderParametersCallback(SourceCallback *callback, const Target &target); public: diff --git a/src/MediaPlayer.cpp b/src/MediaPlayer.cpp index 560c75d..e7f4a84 100644 --- a/src/MediaPlayer.cpp +++ b/src/MediaPlayer.cpp @@ -1134,6 +1134,9 @@ bool MediaPlayer::go_to(GstClockTime pos) if (ABS_DIFF (position_, jumpPts) > 2 * timeline_.step() ) { ret = true; seek( jumpPts ); + + // Revert loop status to default + loop_status_ = LoopStatus::LOOP_STATUS_DEFAULT; } } return ret; @@ -1614,10 +1617,14 @@ bool MediaPlayer::go_to_flag(TimeInterval flag) current_flag_ = flag; // change timeline status accordingly - if ( flag.type == (int) LoopStatus::LOOP_STATUS_BLACKOUT) + if ( flag.type == (int) LoopStatus::LOOP_STATUS_BLACKOUT) { loop_status_ = LoopStatus::LOOP_STATUS_BLACKOUT; - else if ( flag.type == (int) LoopStatus::LOOP_STATUS_STOPPED) - loop_status_ = LoopStatus::LOOP_STATUS_DEFAULT; + execute_play_command(false); + } + else if ( flag.type == (int) LoopStatus::LOOP_STATUS_STOPPED) { + loop_status_ = LoopStatus::LOOP_STATUS_STOPPED; + execute_play_command(false); + } } } return ret; diff --git a/src/SessionCreator.cpp b/src/SessionCreator.cpp index 9d83aa7..83f8444 100644 --- a/src/SessionCreator.cpp +++ b/src/SessionCreator.cpp @@ -1703,6 +1703,12 @@ void SessionLoader::visit (Seek &c) xmlCurrent_->QueryBoolAttribute("bidirectional", &b); c.setBidirectional(b); } +void SessionLoader::visit (Flag &c) +{ + int v = -1; + xmlCurrent_->QueryIntAttribute("value", &v); + c.setValue(v); +} void SessionLoader::visit (SetAlpha &c) { diff --git a/src/SessionCreator.h b/src/SessionCreator.h index ba6306d..6d50809 100644 --- a/src/SessionCreator.h +++ b/src/SessionCreator.h @@ -93,6 +93,7 @@ public: void visit (Play&) override; void visit (PlayFastForward&) override; void visit (Seek&) override; + void visit (Flag&) override; static void XMLToNode(const tinyxml2::XMLElement *xml, Node &n); static void XMLToSourcecore(tinyxml2::XMLElement *xml, SourceCore &s); diff --git a/src/SessionVisitor.cpp b/src/SessionVisitor.cpp index c198f7a..d6b93a2 100644 --- a/src/SessionVisitor.cpp +++ b/src/SessionVisitor.cpp @@ -1058,6 +1058,11 @@ void SessionVisitor::visit (PlayFastForward &c) xmlCurrent_->SetAttribute("duration", c.duration()); } +void SessionVisitor::visit (Flag &c) +{ + xmlCurrent_->SetAttribute("value", (int) c.value()); +} + void SessionVisitor::visit (Seek &c) { xmlCurrent_->SetAttribute("value", (uint64_t) c.value()); diff --git a/src/SessionVisitor.h b/src/SessionVisitor.h index 87f54e1..3034659 100644 --- a/src/SessionVisitor.h +++ b/src/SessionVisitor.h @@ -99,6 +99,7 @@ public: void visit (Play&) override; void visit (PlayFastForward&) override; void visit (Seek&) override; + void visit (Flag&) override; static tinyxml2::XMLElement *NodeToXML(const Node &n, tinyxml2::XMLDocument *doc); static tinyxml2::XMLElement *ImageToXML(const FrameBufferImage *img, tinyxml2::XMLDocument *doc); diff --git a/src/SourceCallback.cpp b/src/SourceCallback.cpp index 05ea61b..28035f2 100644 --- a/src/SourceCallback.cpp +++ b/src/SourceCallback.cpp @@ -72,6 +72,9 @@ SourceCallback *SourceCallback::create(CallbackType type) case SourceCallback::CALLBACK_SEEK: loadedcallback = new Seek; break; + case SourceCallback::CALLBACK_FLAG: + loadedcallback = new Flag; + break; case SourceCallback::CALLBACK_REPLAY: loadedcallback = new RePlay; break; @@ -718,6 +721,73 @@ void Seek::accept(Visitor& v) v.visit(*this); } +Flag::Flag(int target) + : SourceCallback() + , flag_index_(target) +{} + +void Flag::update(Source *s, float dt) +{ + SourceCallback::update(s, dt); + + // access media player if target source is a media source + MediaSource *ms = dynamic_cast(s); + if (ms != nullptr) { + + // can operate on flags if there are some + int num = ms->mediaplayer()->timeline()->numFlags(); + if (num > 1) { + + // default flag index is -1 to mean next flag + if (flag_index_ < 0) { + GstClockTime _time = ms->mediaplayer()->position(); + + if( ms->mediaplayer()->playSpeed() < 0 ) { + // Go to previous flag when playing backward + TimeInterval target_flag = ms->mediaplayer()->timeline()->getPreviousFlag( _time ); + bool has_prev = target_flag.is_valid() && + ( ms->mediaplayer()->loop() == MediaPlayer::LOOP_REWIND || (target_flag.end < _time) ); + if( has_prev) + ms->mediaplayer()->go_to_flag( target_flag ); + } + else { + // go to next flag when playing forward + TimeInterval target_flag = ms->mediaplayer()->timeline()->getNextFlag( _time ); + bool has_next = target_flag.is_valid() && + ( ms->mediaplayer()->loop() == MediaPlayer::LOOP_REWIND || (target_flag.begin > _time) ); + if( has_next ) + ms->mediaplayer()->go_to_flag( target_flag ); + } + } + else if (flag_index_ < num) { + int index = 0; + const TimeIntervalSet flags = ms->mediaplayer()->timeline()->flags(); + for (const auto &flag_Interval : flags) { + if ( index == flag_index_) { + + ms->mediaplayer()->go_to_flag( flag_Interval ); + break; + } + ++index; + } + } + } + } + + status_ = FINISHED; +} + +SourceCallback *Flag::clone() const +{ + return new Flag(flag_index_); +} + +void Flag::accept(Visitor& v) +{ + SourceCallback::accept(v); + v.visit(*this); +} + SetGeometry::SetGeometry(const Group *g, float ms, bool revert) : SourceCallback(), duration_(ms), bidirectional_(revert) { diff --git a/src/SourceCallback.h b/src/SourceCallback.h index 9c37256..c39d3cd 100644 --- a/src/SourceCallback.h +++ b/src/SourceCallback.h @@ -40,6 +40,7 @@ public: CALLBACK_PLAYSPEED, CALLBACK_PLAYFFWD, CALLBACK_SEEK, + CALLBACK_FLAG, CALLBACK_REPLAY, CALLBACK_RESETGEO, CALLBACK_LOCK, @@ -295,6 +296,22 @@ public: void accept (Visitor& v) override; }; +class Flag : public SourceCallback +{ + int flag_index_; + +public: + Flag (int target = -1); + + int value () const { return flag_index_; } + void setValue (int t) { flag_index_ = t; } + + void update (Source *s, float) override; + SourceCallback *clone() const override; + CallbackType type () const override { return CALLBACK_FLAG; } + void accept (Visitor& v) override; +}; + class ResetGeometry : public SourceCallback { public: diff --git a/src/SourceControlWindow.cpp b/src/SourceControlWindow.cpp index 39bb96c..67c50e1 100644 --- a/src/SourceControlWindow.cpp +++ b/src/SourceControlWindow.cpp @@ -17,6 +17,7 @@ * along with this program. If not, see . **/ +#include #include #include #include @@ -946,6 +947,7 @@ bool TimelineSlider (const char* label, guint64 *time, TimeInterval *flag, Timel // // FLAGS // + int index = 0; bool flag_pressed = false; const TimeIntervalSet flags = tl->flags(); for (const auto &flag_Interval : flags) { @@ -972,7 +974,9 @@ bool TimelineSlider (const char* label, guint64 *time, TimeInterval *flag, Timel // show time when hovering if (hovered) - ImGui::SetTooltip(" %s ", GstToolkit::time_to_string(flag_time).c_str()); + ImGui::SetTooltip(" <%d> %s ", index, GstToolkit::time_to_string(flag_time).c_str()); + + ++index; } // @@ -2305,7 +2309,7 @@ void SourceControlWindow::RenderMediaPlayer(MediaSource *ms) // flag buttons if (rendersize.x > buttons_height_ * 6.0f) { - if ( !mediaplayer_mode_ && mediaplayer_active_->timeline()->numFlags() > 0 ) { + if ( mediaplayer_active_->timeline()->numFlags() > 0 ) { GstClockTime _paused_time = mediaplayer_active_->position(); @@ -2317,7 +2321,7 @@ void SourceControlWindow::RenderMediaPlayer(MediaSource *ms) !( mediaplayer_active_->currentFlag().is_valid() && mediaplayer_active_->timeline()->numFlags() == 1) && ( mediaplayer_active_->loop() == MediaPlayer::LOOP_REWIND || (target_flag.end < _paused_time) ); if( ImGuiToolkit::ButtonIcon(6, 0, "Go to previous flag", has_prev) ) - mediaplayer_active_->go_to_flag( target_flag ); + seek_flag = target_flag; } else { // go to next flag when playing forward @@ -2326,11 +2330,11 @@ void SourceControlWindow::RenderMediaPlayer(MediaSource *ms) !( mediaplayer_active_->currentFlag().is_valid() && mediaplayer_active_->timeline()->numFlags() == 1) && ( mediaplayer_active_->loop() == MediaPlayer::LOOP_REWIND || (target_flag.begin > _paused_time) ); if( ImGuiToolkit::ButtonIcon(5, 0, "Go to next flag", has_next) ) - mediaplayer_active_->go_to_flag( target_flag ); + seek_flag = target_flag; } // if stopped at a flag, show flag menu - if (mediaplayer_active_->currentFlag().is_valid()) { + if (!mediaplayer_mode_ && mediaplayer_active_->currentFlag().is_valid()) { ImGui::SameLine(0, h_space_); if (ImGuiToolkit::IconButton(3, 0) || ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup)) { counter_menu_timeout=0; @@ -2396,23 +2400,18 @@ void SourceControlWindow::RenderMediaPlayer(MediaSource *ms) // play/stop command should be following the playing mode (buttons) // AND force to stop when the slider is pressed - bool media_play = mediaplayer_mode_ & (!mediaplayer_slider_pressed_); - - // Flag pressed in timeline - if (seek_flag.is_valid()) { - // go to the flag position - if ( mediaplayer_active_->go_to_flag(seek_flag) ){ - // stop if flag type is 'Stop' (1) or 'Blackout' (2) - if (seek_flag.type > 0) - media_play = false; - } - } + bool media_play = mediaplayer_mode_ & (!mediaplayer_slider_pressed_); // apply play action to media only if status should change if ( mediaplayer_active_->isPlaying() != media_play ) { mediaplayer_active_->play( media_play ); } + // Flag pressed in timeline + if (seek_flag.is_valid()) { + // go to the flag position + mediaplayer_active_->go_to_flag(seek_flag); + } } else { /// diff --git a/src/Visitor.h b/src/Visitor.h index 83d4ea1..e59f157 100644 --- a/src/Visitor.h +++ b/src/Visitor.h @@ -76,6 +76,7 @@ public: virtual void visit (class Play&) {} virtual void visit (class PlayFastForward&) {} virtual void visit (class Seek&) {} + virtual void visit (class Flag&) {} }; diff --git a/src/defines.h b/src/defines.h index a8a6fb6..a43a4b0 100644 --- a/src/defines.h +++ b/src/defines.h @@ -111,8 +111,8 @@ #define IMGUI_LABEL_RECENT_FILES " Recent files" #define IMGUI_LABEL_RECENT_RECORDS " Recent recordings" #define IMGUI_RIGHT_ALIGN -3.8f * ImGui::GetTextLineHeightWithSpacing() -#define IMGUI_SAME_LINE 8 -#define IMGUI_TOP_ALIGN 10 +#define IMGUI_SAME_LINE 8.f +#define IMGUI_TOP_ALIGN 10.f #define IMGUI_COLOR_OVERLAY IM_COL32(5, 5, 5, 150) #define IMGUI_COLOR_LIGHT_OVERLAY IM_COL32(5, 5, 5, 50) #define IMGUI_COLOR_CAPTURE 1.0, 0.55, 0.05