diff --git a/src/BaseToolkit.cpp b/src/BaseToolkit.cpp index 4530778..0e2041c 100644 --- a/src/BaseToolkit.cpp +++ b/src/BaseToolkit.cpp @@ -211,6 +211,24 @@ bool BaseToolkit::is_a_number(const std::string& str, int *val) return isanumber; } +bool BaseToolkit::is_a_value(const std::string& str, float *val) +{ + bool isavalue = false; + + try { + *val = std::stof(str); + isavalue = true; + } + catch (const std::invalid_argument&) { + // avoids crash + } + catch (const std::out_of_range&) { + // avoids crash + } + + return isavalue; +} + std::string BaseToolkit::common_prefix( const std::list & allStrings ) { if (allStrings.empty()) diff --git a/src/BaseToolkit.h b/src/BaseToolkit.h index 0acb79d..c13d417 100644 --- a/src/BaseToolkit.h +++ b/src/BaseToolkit.h @@ -34,9 +34,12 @@ std::list splitted(const std::string& str, char delim); // rebuilds a splitted string std::string joinned(std::list strlist, char separator = ' '); -// returns true if the string +// returns true if the string contains an integer bool is_a_number(const std::string& str, int *val); +// returns true if the string contains a float value +bool is_a_value(const std::string& str, float *val); + // find common parts in a list of strings std::string common_prefix(const std::list &allStrings); std::string common_suffix(const std::list &allStrings); diff --git a/src/MediaPlayer.cpp b/src/MediaPlayer.cpp index 9aa5ed7..12204a3 100644 --- a/src/MediaPlayer.cpp +++ b/src/MediaPlayer.cpp @@ -725,7 +725,7 @@ void MediaPlayer::rewind(bool force) } -void MediaPlayer::step() +void MediaPlayer::step(uint milisecond) { // useful only when Paused if (!enabled_ || isPlaying() || pending_) @@ -736,7 +736,9 @@ void MediaPlayer::step() rewind(); else { // step event - GstEvent *stepevent = gst_event_new_step (GST_FORMAT_BUFFERS, 1, ABS(rate_), TRUE, FALSE); + if (milisecond < media_.dt) + milisecond = media_.dt; + GstEvent *stepevent = gst_event_new_step (GST_FORMAT_TIME, milisecond, ABS(rate_), TRUE, FALSE); // Metronome if (metro_sync_) { @@ -750,11 +752,11 @@ void MediaPlayer::step() Metronome::manager().executeAtPhase( steplater ); else Metronome::manager().executeAtBeat( steplater ); - } else // execute immediately gst_element_send_event (pipeline_, stepevent); + } } @@ -793,12 +795,16 @@ void MediaPlayer::seek(GstClockTime pos) } -void MediaPlayer::jump() +void MediaPlayer::jump(uint milisecond) { if (!enabled_ || !isPlaying()) return; - gst_element_send_event (pipeline_, gst_event_new_step (GST_FORMAT_BUFFERS, 1, 30.f * ABS(rate_), TRUE, FALSE)); + gst_element_send_event (pipeline_, gst_event_new_step (GST_FORMAT_TIME, + CLAMP(milisecond, 1, 1000) * GST_MSECOND, + ABS(rate_), + TRUE, FALSE)); + } void MediaPlayer::init_texture(guint index) diff --git a/src/MediaPlayer.h b/src/MediaPlayer.h index ba1a389..4ca2411 100644 --- a/src/MediaPlayer.h +++ b/src/MediaPlayer.h @@ -156,17 +156,17 @@ public: * */ void setLoop(LoopMode mode); /** - * Seek to next frame when paused + * Step when paused * (aka next frame) * Can go backward if play speed is negative * */ - void step(); + void step(uint milisecond = 0); /** - * Jump fast when playing + * Step forward when playing * (aka fast-forward) * Can go backward if play speed is negative * */ - void jump(); + void jump(uint milisecond = 200); /** * Seek to zero * */ diff --git a/src/Mixer.cpp b/src/Mixer.cpp index 434d9af..b5155bb 100644 --- a/src/Mixer.cpp +++ b/src/Mixer.cpp @@ -214,7 +214,6 @@ void Mixer::update() // recreating it in the current session RenderSource *failedRender = dynamic_cast(*it); if (failedRender != nullptr) { - g_printerr("%s failed render \n", failedRender->initials()); // try to recreate the failed render source if ( !recreateSource(failedRender) ) // delete the source if could not diff --git a/src/SessionCreator.cpp b/src/SessionCreator.cpp index b087ef2..71cb08d 100644 --- a/src/SessionCreator.cpp +++ b/src/SessionCreator.cpp @@ -1383,6 +1383,17 @@ void SessionLoader::visit (Play &c) c.setBidirectional(b); } +void SessionLoader::visit (PlayFastForward &c) +{ + float d = 0.f; + xmlCurrent_->QueryFloatAttribute("step", &d); + c.setValue(d); + + d = 0.f; + xmlCurrent_->QueryFloatAttribute("duration", &d); + c.setDuration(d); +} + void SessionLoader::visit (SetAlpha &c) { float a = 0.f; diff --git a/src/SessionCreator.h b/src/SessionCreator.h index 2e94f9a..0cbe005 100644 --- a/src/SessionCreator.h +++ b/src/SessionCreator.h @@ -83,6 +83,7 @@ public: void visit (Resize&) override; void visit (Turn&) override; void visit (Play&) override; + void visit (PlayFastForward&) 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 fe0757f..f45c6bc 100644 --- a/src/SessionVisitor.cpp +++ b/src/SessionVisitor.cpp @@ -910,6 +910,12 @@ void SessionVisitor::visit (Play &c) xmlCurrent_->SetAttribute("bidirectional", c.bidirectional()); } +void SessionVisitor::visit (PlayFastForward &c) +{ + xmlCurrent_->SetAttribute("step", c.value()); + xmlCurrent_->SetAttribute("duration", c.duration()); +} + void SessionVisitor::visit (SetAlpha &c) { xmlCurrent_->SetAttribute("alpha", c.value()); diff --git a/src/SessionVisitor.h b/src/SessionVisitor.h index 37698f6..48d0e4e 100644 --- a/src/SessionVisitor.h +++ b/src/SessionVisitor.h @@ -91,6 +91,7 @@ public: void visit (Resize&) override; void visit (Turn&) override; void visit (Play&) override; + void visit (PlayFastForward&) 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 a63505c..7335c28 100644 --- a/src/SourceCallback.cpp +++ b/src/SourceCallback.cpp @@ -56,6 +56,15 @@ SourceCallback *SourceCallback::create(CallbackType type) case SourceCallback::CALLBACK_PLAY: loadedcallback = new Play; break; + case SourceCallback::CALLBACK_PLAYSPEED: + loadedcallback = new PlaySpeed; + break; + case SourceCallback::CALLBACK_PLAYFFWD: + loadedcallback = new PlayFastForward; + break; + case SourceCallback::CALLBACK_SEEK: + loadedcallback = new Seek; + break; case SourceCallback::CALLBACK_REPLAY: loadedcallback = new RePlay; break; @@ -65,9 +74,6 @@ SourceCallback *SourceCallback::create(CallbackType type) case SourceCallback::CALLBACK_LOCK: loadedcallback = new Lock; break; - case SourceCallback::CALLBACK_SEEK: - loadedcallback = new Seek; - break; case SourceCallback::CALLBACK_BRIGHTNESS: loadedcallback = new SetBrightness; break; @@ -524,7 +530,93 @@ SourceCallback *RePlay::clone() const return new RePlay; } -Seek::Seek(float v, float ms, bool r) : ValueSourceCallback(v, ms, r) + +PlaySpeed::PlaySpeed(float v, float ms, bool r) : ValueSourceCallback(v, ms, r) +{ +} + +float PlaySpeed::readValue(Source *s) const +{ + double ret = 1.f; + // access media player if target source is a media source + MediaSource *ms = dynamic_cast(s); + if (ms != nullptr) { + ret = (float) ms->mediaplayer()->playSpeed(); + } + + return (float)ret; +} + +void PlaySpeed::writeValue(Source *s, float val) +{ + // access media player if target source is a media source + MediaSource *ms = dynamic_cast(s); + if (ms != nullptr) { + ms->mediaplayer()->setPlaySpeed((double) val); + } +} + +PlayFastForward::PlayFastForward(uint seekstep, float ms) : SourceCallback(), media_(nullptr), + step_(seekstep), duration_(ms), playspeed_(0.) +{ + +} + +void PlayFastForward::update(Source *s, float dt) +{ + SourceCallback::update(s, dt); + + // on first start, get the media player + if ( status_ == READY ){ + // works for media source only + MediaSource *ms = dynamic_cast(s); + if (ms != nullptr) { + MediaPlayer *mp = ms->mediaplayer(); + // works only if media player is enabled and valid + if (mp && mp->isEnabled() && !mp->isImage()) { + media_ = mp; + playspeed_ = media_->playSpeed(); + } + } + status_ = media_ ? ACTIVE : FINISHED; + } + + if ( status_ == ACTIVE ) { + + // perform fast forward + if (media_->isPlaying()) + media_->jump(step_); + else + media_->step(step_); + + // time-out + if ( elapsed_ - delay_ > duration_ ) + // done + status_ = FINISHED; + } + + if (media_ && status_ == FINISHED) + media_->setPlaySpeed( playspeed_ ); + +} + +void PlayFastForward::multiply (float factor) +{ + step_ *= MAX( 1, (uint) ceil( ABS(factor) ) ); +} + +SourceCallback *PlayFastForward::clone() const +{ + return new PlayFastForward(step_, duration_); +} + +void PlayFastForward::accept(Visitor& v) +{ + SourceCallback::accept(v); + v.visit(*this); +} + +Seek::Seek(float t, float ms, bool r) : ValueSourceCallback(t, ms, r) { } @@ -534,16 +626,13 @@ float Seek::readValue(Source *s) const // access media player if target source is a media source MediaSource *ms = dynamic_cast(s); if (ms != nullptr) { - GstClockTime media_duration = ms->mediaplayer()->timeline()->duration(); GstClockTime media_position = ms->mediaplayer()->position(); - - if (GST_CLOCK_TIME_IS_VALID(media_duration) && media_duration > 0 && - GST_CLOCK_TIME_IS_VALID(media_position) && media_position > 0){ - ret = static_cast(media_position) / static_cast(media_duration); + if (GST_CLOCK_TIME_IS_VALID(media_position) && media_position > 0){ + ret = GST_TIME_AS_SECONDS( static_cast(media_position) ); } } - return (float)ret; + return (float) ret; } void Seek::writeValue(Source *s, float val) @@ -552,9 +641,11 @@ void Seek::writeValue(Source *s, float val) MediaSource *ms = dynamic_cast(s); if (ms != nullptr) { GstClockTime media_duration = ms->mediaplayer()->timeline()->duration(); - double media_position = glm::clamp( (double) val, 0.0, 1.0); - if (GST_CLOCK_TIME_IS_VALID(media_duration)) - ms->mediaplayer()->seek( media_position * media_duration ); + GstClockTime t = (double) GST_SECOND * (double) val; + if (GST_CLOCK_TIME_IS_VALID(t) && + GST_CLOCK_TIME_IS_VALID(media_duration) && + t < media_duration ) + ms->mediaplayer()->seek( t ); } } diff --git a/src/SourceCallback.h b/src/SourceCallback.h index fe7b788..f762c7f 100644 --- a/src/SourceCallback.h +++ b/src/SourceCallback.h @@ -35,10 +35,12 @@ public: CALLBACK_TURN, CALLBACK_DEPTH, CALLBACK_PLAY, + CALLBACK_PLAYSPEED, + CALLBACK_PLAYFFWD, + CALLBACK_SEEK, CALLBACK_REPLAY, CALLBACK_RESETGEO, CALLBACK_LOCK, - CALLBACK_SEEK, CALLBACK_BRIGHTNESS, CALLBACK_CONTRAST, CALLBACK_SATURATION, @@ -234,6 +236,45 @@ public: CallbackType type () const override { return CALLBACK_REPLAY; } }; +class PlaySpeed : public ValueSourceCallback +{ + float readValue(Source *s) const override; + void writeValue(Source *s, float val) override; +public: + PlaySpeed (float v = 1.f, float ms = 0.f, bool revert = false); + CallbackType type () const override { return CALLBACK_PLAYSPEED; } +}; + +class PlayFastForward : public SourceCallback +{ + class MediaPlayer *media_; + uint step_; + float duration_; + double playspeed_; +public: + PlayFastForward (uint seekstep = 1, float ms = FLT_MAX); + + uint value () const { return step_; } + void setValue (uint s) { step_ = s; } + float duration () const { return duration_; } + void setDuration (float ms) { duration_ = ms; } + + void update (Source *s, float) override; + void multiply (float factor) override; + SourceCallback *clone() const override; + CallbackType type () const override { return CALLBACK_PLAYFFWD; } + void accept (Visitor& v) override; +}; + +class Seek : public ValueSourceCallback +{ + float readValue(Source *s) const override; + void writeValue(Source *s, float val) override; +public: + Seek (float t = 0.f, float ms = 0.f, bool revert = false); + CallbackType type () const override { return CALLBACK_SEEK; } +}; + class ResetGeometry : public SourceCallback { public: @@ -328,15 +369,6 @@ public: void accept (Visitor& v) override; }; -class Seek : public ValueSourceCallback -{ - float readValue(Source *s) const override; - void writeValue(Source *s, float val) override; -public: - Seek (float v = 0.f, float ms = 0.f, bool revert = false); - CallbackType type () const override { return CALLBACK_SEEK; } -}; - class SetBrightness : public ValueSourceCallback { float readValue(Source *s) const override; diff --git a/src/UserInterfaceManager.cpp b/src/UserInterfaceManager.cpp index 639e82a..6dacf75 100644 --- a/src/UserInterfaceManager.cpp +++ b/src/UserInterfaceManager.cpp @@ -4916,7 +4916,7 @@ Target InputMappingInterface::ComboSelectTarget(const Target ¤t) uint InputMappingInterface::ComboSelectCallback(uint current, bool imageprocessing) { - const char* callback_names[21] = { "Select", + const char* callback_names[23] = { "Select", ICON_FA_BULLSEYE " Alpha", ICON_FA_BULLSEYE " Loom", ICON_FA_OBJECT_UNGROUP " Geometry", @@ -4925,7 +4925,9 @@ uint InputMappingInterface::ComboSelectCallback(uint current, bool imageprocessi ICON_FA_OBJECT_UNGROUP " Turn", ICON_FA_LAYER_GROUP " Depth", ICON_FA_PLAY_CIRCLE " Play", - " None", + ICON_FA_PLAY_CIRCLE " Play Speed", + ICON_FA_PLAY_CIRCLE " Play Fast Fwd", + ICON_FA_PLAY_CIRCLE " Play Seek", " None", " None", " None", @@ -4941,7 +4943,7 @@ uint InputMappingInterface::ComboSelectCallback(uint current, bool imageprocessi uint selected = 0; if (ImGui::BeginCombo("##ComboSelectCallback", callback_names[current]) ) { - for (uint i = SourceCallback::CALLBACK_ALPHA; i <= SourceCallback::CALLBACK_PLAY; ++i){ + for (uint i = SourceCallback::CALLBACK_ALPHA; i <= SourceCallback::CALLBACK_SEEK; ++i){ if ( ImGui::Selectable( callback_names[i]) ) { selected = i; } @@ -5075,10 +5077,9 @@ void InputMappingInterface::SliderParametersCallback(SourceCallback *callback, c ImGui::SameLine(0, IMGUI_SAME_LINE / 2); ImGui::TextDisabled("Invalid"); } - - } break; + case SourceCallback::CALLBACK_GRAB: { ImGuiToolkit::Indication(press_tooltip[2], 18, 5); @@ -5092,6 +5093,7 @@ void InputMappingInterface::SliderParametersCallback(SourceCallback *callback, c ImGuiToolkit::Indication("Vector (x,y) moving the source horizontally and vertically.", 6, 15); } break; + case SourceCallback::CALLBACK_RESIZE: { ImGuiToolkit::Indication(press_tooltip[2], 18, 5); @@ -5106,6 +5108,7 @@ void InputMappingInterface::SliderParametersCallback(SourceCallback *callback, c } break; + case SourceCallback::CALLBACK_TURN: { ImGuiToolkit::Indication(press_tooltip[2], 18, 5); @@ -5116,10 +5119,11 @@ void InputMappingInterface::SliderParametersCallback(SourceCallback *callback, c if ( ImGui::SliderAngle("##CALLBACK_TURN", &val, -180.f, 180.f) ) edited->setValue(val ); - ImGui::SameLine(0, IMGUI_SAME_LINE / 2); + ImGui::SameLine(0, IMGUI_SAME_LINE / 3); ImGuiToolkit::Indication("Rotation of the source (speed in \u00B0/s),\nclockwise (>0), counterclockwise (<0)", 18, 9); } break; + case SourceCallback::CALLBACK_DEPTH: { SetDepth *edited = static_cast(callback); @@ -5144,6 +5148,7 @@ void InputMappingInterface::SliderParametersCallback(SourceCallback *callback, c ImGuiToolkit::Indication("Target depth brings the source\nfront (12) or back (0).", 6, 6); } break; + case SourceCallback::CALLBACK_PLAY: { Play *edited = static_cast(callback); @@ -5158,7 +5163,101 @@ void InputMappingInterface::SliderParametersCallback(SourceCallback *callback, c if (ImGui::SliderInt("##CALLBACK_PLAY", &val, 0, 1, "Pause | Play ")) edited->setValue(val>0); ImGui::SameLine(0, IMGUI_SAME_LINE / 2); - ImGuiToolkit::Indication("Play or pause the source.", 16, 7); + ImGuiToolkit::Indication("Play or pause the source.", 12, 7); + } + break; + + case SourceCallback::CALLBACK_PLAYSPEED: + { + PlaySpeed *edited = static_cast(callback); + + bool bd = edited->bidirectional(); + if ( ImGuiToolkit::IconToggle(2, 13, 3, 13, &bd, press_tooltip ) ) + edited->setBidirectional(bd); + + ClosestIndex d = std::for_each(speed_values.begin(), speed_values.end(), ClosestIndex(edited->duration())); + int speed_index = d.index; + ImGui::SameLine(0, IMGUI_SAME_LINE / 2); + if (ImGuiToolkit::IconMultistate(speed_icon, &speed_index, speed_tooltip )) + edited->setDuration(speed_values[speed_index]); + + float val = edited->value(); + ImGui::SetNextItemWidth(right_align); + ImGui::SameLine(0, IMGUI_SAME_LINE / 2); + if (ImGui::SliderFloat("##CALLBACK_PLAYSPEED", &val, 0.1f, 20.f, "x %.1f")) + edited->setValue(val); + + ImGui::SameLine(0, IMGUI_SAME_LINE / 2); + ImGuiToolkit::Indication("Set play speed of a video source.", 16, 7); + } + break; + + case SourceCallback::CALLBACK_PLAYFFWD: + { + PlayFastForward *edited = static_cast(callback); + + ImGuiToolkit::Indication(press_tooltip[2], 18, 5); + ImGui::SameLine(0, IMGUI_SAME_LINE / 2); + + int val = (int) edited->value(); + ImGui::SetNextItemWidth(right_align); + ImGui::SameLine(0, IMGUI_SAME_LINE / 2); + if (ImGui::SliderInt("##CALLBACK_PLAYFFWD", &val, 30, 1000, "%d ms")) + edited->setValue( MAX(1, val) ); + + ImGui::SameLine(0, IMGUI_SAME_LINE / 2); + ImGuiToolkit::Indication("Fast forward in a video source, by steps of the given duration (in miliseconds).", 13, 7); + } + break; + + case SourceCallback::CALLBACK_SEEK: + { + Seek *edited = static_cast(callback); + + bool bd = edited->bidirectional(); + if ( ImGuiToolkit::IconToggle(2, 13, 3, 13, &bd, press_tooltip ) ) + edited->setBidirectional(bd); + + // get value (seconds) and convert to minutes and seconds + float val = edited->value(); + int min = (int) floor(val / 60.f); + float sec = val - 60.f * (float) min; + + // filtering for reading MM:SS.MS text entry + static bool valid = true; + static std::regex RegExTime("([0-9]+\\:)*([0-5][0-9]|[0-9])(\\.[0-9]+)*"); + struct TextFilters { static int FilterTime(ImGuiInputTextCallbackData* data) { if (data->EventChar < 256 && strchr("0123456789.:", (char)data->EventChar)) return 0; return 1; } }; + char buf6[64] = ""; + sprintf(buf6, "%d:%.2f", min, sec ); + + // Text input field for MM:SS:MS seek target time + ImGui::SetNextItemWidth(right_align); + ImGui::SameLine(0, IMGUI_SAME_LINE / 2); + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, valid ? 1.0f : 0.2f, valid ? 1.0f : 0.2f, 1.f)); + ImGui::InputText("##CALLBACK_SEEK", buf6, 64, ImGuiInputTextFlags_CallbackCharFilter, TextFilters::FilterTime); + valid = std::regex_match(buf6, RegExTime); + if (ImGui::IsItemDeactivatedAfterEdit()) { + if (valid){ + // user confirmed the entry and the input is valid + min = 0; + std::string minutes = buf6; + std::string seconds = buf6; + const size_t min_idx = minutes.rfind(':'); + if (string::npos != min_idx) { + seconds = minutes.substr(min_idx+1); + minutes.erase(min_idx); + BaseToolkit::is_a_number(minutes, &min); + } + if (BaseToolkit::is_a_value(seconds, &sec)) + edited->setValue( sec + 60.f * (float) min ); + } + // force to test validity next frame + valid = false; + } + ImGui::PopStyleColor(); + + ImGui::SameLine(0, IMGUI_SAME_LINE / 3); + ImGuiToolkit::Indication("Jump to the given time in a video source (in minutes and seconds, MM:SS.MS).", 15, 7); } break; @@ -5282,7 +5381,7 @@ void InputMappingInterface::SliderParametersCallback(SourceCallback *callback, c if (ImGui::SliderFloat("##CALLBACK_THRESHOLD", &val, 0.f, 1.f, "%.1f")) edited->setValue(val); - ImGui::SameLine(0, IMGUI_SAME_LINE / 2); + ImGui::SameLine(0, IMGUI_SAME_LINE / 3); ImGuiToolkit::Indication("Set Threshold color correction.", 8, 1); } break; diff --git a/src/Visitor.h b/src/Visitor.h index 8bfa5f7..67c23df 100644 --- a/src/Visitor.h +++ b/src/Visitor.h @@ -62,6 +62,7 @@ class Grab; class Resize; class Turn; class Play; +class PlayFastForward; // Declares the interface for the visitors @@ -129,6 +130,7 @@ public: virtual void visit (Resize&) {} virtual void visit (Turn&) {} virtual void visit (Play&) {} + virtual void visit (PlayFastForward&) {} };