From bb8dcf088ef57cc077fda5ce5d0acf2d5488094b Mon Sep 17 00:00:00 2001 From: brunoherbelin Date: Tue, 11 Aug 2020 00:11:22 +0200 Subject: [PATCH] work in progress cleanum memory leak and crash :( --- CMakeLists.txt | 1 + ImGuiToolkit.cpp | 213 +----------------------- ImGuiToolkit.h | 7 +- ImGuiVisitor.cpp | 2 +- MediaPlayer.cpp | 214 ++++++++++++++---------- MediaPlayer.h | 27 +-- MediaSource.cpp | 2 +- Session.cpp | 5 + SessionCreator.cpp | 19 +++ SessionCreator.h | 1 + SessionVisitor.cpp | 11 +- Timeline.cpp | 184 ++++++++++++++++----- Timeline.h | 89 +++++++--- UserInterfaceManager.cpp | 343 ++++++++++++++++++++++----------------- defines.h | 3 +- 15 files changed, 590 insertions(+), 531 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 99d3d7b..257f32c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -234,6 +234,7 @@ set(VMIX_SRCS Screenshot.cpp Resource.cpp FileDialog.cpp + Timeline.cpp MediaPlayer.cpp MediaSource.cpp FrameBuffer.cpp diff --git a/ImGuiToolkit.cpp b/ImGuiToolkit.cpp index a5fe4b7..e4c147d 100644 --- a/ImGuiToolkit.cpp +++ b/ImGuiToolkit.cpp @@ -286,7 +286,7 @@ void ImGuiToolkit::HelpMarker(const char* desc) #define NUM_MARKS 10 #define LARGE_TICK_INCREMENT 1 #define LABEL_TICK_INCREMENT 3 -bool ImGuiToolkit::TimelineSlider(const char* label, guint64 *time, guint64 duration, guint64 step, float scale) +bool ImGuiToolkit::TimelineSlider(const char* label, guint64 *time, guint64 duration, guint64 step, const float width) { static guint64 optimal_tick_marks[NUM_MARKS + LABEL_TICK_INCREMENT] = { 100 * MILISECOND, 500 * MILISECOND, 1 * SECOND, 2 * SECOND, 5 * SECOND, 10 * SECOND, 20 * SECOND, 1 * MINUTE, 2 * MINUTE, 5 * MINUTE, 10 * MINUTE, 60 * MINUTE, 60 * MINUTE }; @@ -308,8 +308,7 @@ bool ImGuiToolkit::TimelineSlider(const char* label, guint64 *time, guint64 dura // widget bounding box const float height = 2.f * (fontsize + style.FramePadding.y); ImVec2 pos = window->DC.CursorPos; - ImVec2 size = ImGui::CalcItemSize(ImVec2(-FLT_MIN, 0.0f), ImGui::CalcItemWidth(), height); - size.x *= scale; + ImVec2 size = ImVec2(width, height); ImRect bbox(pos, pos + size); ImGui::ItemSize(size, style.FramePadding.y); if (!ImGui::ItemAdd(bbox, id)) @@ -380,7 +379,7 @@ bool ImGuiToolkit::TimelineSlider(const char* label, guint64 *time, guint64 dura // how many pixels to represent one frame step? float tick_step_pixels = timeline_bbox.GetWidth() * step_; - // tick at each step: add a label every 30 frames (1 second?) + // tick at each step: add a label every 0 frames if (tick_step_pixels > 5.f) { large_tick_step = 10 * step; @@ -468,212 +467,6 @@ bool ImGuiToolkit::TimelineSlider(const char* label, guint64 *time, guint64 dura return left_mouse_press; } -// Draws a timeline showing -// 1) a cursor at position *time in the range [0 duration] -// 2) a line of tick marks indicating time, every step if possible -// 3) a slider handle below the cursor: user can move the slider -// 4) different blocs for each time segment [first second] of the list of segments -// Behavior -// a) Returns TRUE if the left mouse button LMB is pressed over the timeline -// b) the value of *time is changed to the position of the slider handle from user input (LMB) -// c) the list segments is replaced with the list of new segments created by user (empty list otherwise) - -bool ImGuiToolkit::TimelineSliderEdit(const char* label, guint64 *time, guint64 duration, guint64 step, - std::list >& segments) -{ - static guint64 optimal_tick_marks[12] = { 100 * MILISECOND, 500 * MILISECOND, 1 * SECOND, 2 * SECOND, 5 * SECOND, 10 * SECOND, 20 * SECOND, 1 * MINUTE, 2 * MINUTE, 5 * MINUTE, 10 * MINUTE, 60 * MINUTE }; - - // get window - ImGuiWindow* window = ImGui::GetCurrentWindow(); - if (window->SkipItems) - return false; - - // get style & id - const ImGuiStyle& style = ImGui::GetStyle(); - const float fontsize = ImGui::GetFontSize(); - ImGuiContext& g = *GImGui; - const ImGuiID id = window->GetID(label); - - // - // FIRST PREPARE ALL data structures - // - - // widget bounding box - const float height = 2.f * (fontsize + style.FramePadding.y); - ImVec2 pos = window->DC.CursorPos; - ImVec2 size = ImGui::CalcItemSize(ImVec2(-FLT_MIN, 0.0f), ImGui::CalcItemWidth(), height); - ImRect bbox(pos, pos + size); - ImGui::ItemSize(size, style.FramePadding.y); - if (!ImGui::ItemAdd(bbox, id)) - return false; - - // cursor size - const float cursor_scale = 1.f; - const float cursor_width = 0.5f * fontsize * cursor_scale; - - // TIMELINE is inside the bbox, in a slightly smaller bounding box - ImRect timeline_bbox(bbox); - timeline_bbox.Expand( ImVec2(-cursor_width, -style.FramePadding.y) ); - - // SLIDER is inside the timeline - ImRect slider_bbox( timeline_bbox.GetTL() + ImVec2(-cursor_width + 2.f, cursor_width + 4.f ), timeline_bbox.GetBR() + ImVec2( cursor_width - 2.f, 0.f ) ); - - // units conversion: from time to float (calculation made with higher precision first) - float time_ = static_cast ( static_cast(*time) / static_cast(duration) ); - float step_ = static_cast ( static_cast(step) / static_cast(duration) ); - - // - // SECOND GET USER INPUT AND PERFORM CHANGES AND DECISIONS - // - - // read user input from system - bool left_mouse_press = false; - bool right_mouse_press = false; - const bool hovered = ImGui::ItemHoverable(bbox, id); - bool temp_input_is_active = ImGui::TempInputIsActive(id); - if (!temp_input_is_active) - { - const bool focus_requested = ImGui::FocusableItemRegister(window, id); - left_mouse_press = hovered && ImGui::IsMouseDown(ImGuiMouseButton_Left); - right_mouse_press = hovered && ImGui::IsMouseDown(ImGuiMouseButton_Right); - if (focus_requested || left_mouse_press || right_mouse_press || g.NavActivateId == id || g.NavInputId == id) - { - ImGui::SetActiveID(id, window); - ImGui::SetFocusID(id, window); - ImGui::FocusWindow(window); - } - } - - // active slider if inside bb - ImRect grab_slider_bb; - ImU32 grab_slider_color = ImGui::GetColorU32(ImGuiCol_SliderGrab); - ImGuiID slider_id = window->GetID("werwerwsdvcsdgfdghdfsgagewrgsvdfhfdghkjfghsdgsdtgewfszdvgfkjfg"); // FIXME: cleaner way to create a valid but useless id that is never active - if ( slider_bbox.Contains(g.IO.MousePos) ) { - slider_id = id; // active id - grab_slider_color = ImGui::GetColorU32(ImGuiCol_SliderGrabActive); - } - - // time Slider behavior - float time_slider = time_ * 10.f; // x 10 precision on grab - float time_zero = 0.f; - float time_end = 10.f; - bool value_changed = ImGui::SliderBehavior(slider_bbox, slider_id, ImGuiDataType_Float, &time_slider, &time_zero, - &time_end, "%.2f", 1.f, ImGuiSliderFlags_None, &grab_slider_bb); - if (value_changed){ - // g_print("slider %f %ld \n", time_slider, static_cast ( static_cast(time_slider) * static_cast(duration) )); - *time = static_cast ( 0.1 * static_cast(time_slider) * static_cast(duration) ); - } - - // segments behavior - float time_segment_begin = 0.f; -// float time_segment_end = 0.f; - if (right_mouse_press) { - time_segment_begin = 0.f; - } - - // - // THIRD RENDER - // - - // Render the bounding box - const ImU32 frame_col = ImGui::GetColorU32(g.ActiveId == id ? ImGuiCol_FrameBgActive : g.HoveredId == id ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg); - ImGui::RenderFrame(bbox.Min, bbox.Max, frame_col, true, style.FrameRounding); - - // by default, put a tick mark at every frame step and a large mark every second - guint64 tick_step = step; - guint64 large_tick_step = SECOND; - - // how many pixels to represent one frame step? - float tick_step_pixels = timeline_bbox.GetWidth() * step_; - // while there is less than 3 pixels between two tick marks (or at last optimal tick mark) - for ( int i=0; i<10 && tick_step_pixels < 3.f; ++i ) - { - // try to use the optimal tick marks pre-defined - tick_step = optimal_tick_marks[i]; - large_tick_step = optimal_tick_marks[i+1]; - tick_step_pixels = timeline_bbox.GetWidth() * static_cast ( static_cast(tick_step) / static_cast(duration) ); - } - - // render the tick marks along TIMELINE - ImU32 color_in = ImGui::GetColorU32( style.Colors[ImGuiCol_Text] ); - ImU32 color_out = ImGui::GetColorU32( style.Colors[ImGuiCol_TextDisabled] ); - ImU32 color = color_in; - pos = timeline_bbox.GetTL(); - guint64 tick = 0; - float tick_percent = 0.f; - std::list< std::pair >::iterator it = segments.begin(); - while ( tick < duration) - { - // large tick mark every large tick - float tick_length = !(tick%large_tick_step) ? fontsize : style.FramePadding.y; - - // different colors for inside and outside [begin end] of segments - if ( it != segments.end() ) { - - if ( tick < it->first ) - color = color_out; - else if ( tick > it->second ){ - color = color_out; - it ++; - } - else - color = color_in; - } - - // draw a tick mark each step - window->DrawList->AddLine( pos, pos + ImVec2(0.f, tick_length), color); - - // next tick - tick += tick_step; - tick_percent = static_cast ( static_cast(tick) / static_cast(duration) ); - pos = ImLerp(timeline_bbox.GetTL(), timeline_bbox.GetTR(), tick_percent); - } - - // loop over segments and EMPTY the list - // ticks for segments begin & end - for (std::list< std::pair >::iterator s = segments.begin(); s != segments.end(); s++){ - tick_percent = static_cast ( static_cast(s->first) / static_cast(duration) ); - pos = ImLerp(timeline_bbox.GetTL(), timeline_bbox.GetTR(), tick_percent); - window->DrawList->AddLine( pos, pos + ImVec2(0.f, timeline_bbox.GetHeight()), color_in); - tick_percent = static_cast ( static_cast(s->second) / static_cast(duration) ); - pos = ImLerp(timeline_bbox.GetTL(), timeline_bbox.GetTR(), tick_percent); - window->DrawList->AddLine( pos, pos + ImVec2(0.f, timeline_bbox.GetHeight()), color_in); - } - segments.clear(); - - // tick EOF - window->DrawList->AddLine( timeline_bbox.GetTR(), timeline_bbox.GetTR() + ImVec2(0.f, fontsize), color_in); - - // render text : duration and current time - char overlay_buf[24]; - ImVec2 overlay_size = ImVec2(0.f, 0.f); - ImFormatString(overlay_buf, IM_ARRAYSIZE(overlay_buf), "%s", GstToolkit::time_to_string(duration).c_str()); - overlay_size = ImGui::CalcTextSize(overlay_buf, NULL); - overlay_size += ImVec2(3.f, 3.f); - if (overlay_size.x > 0.0f) - ImGui::RenderTextClipped( bbox.GetBR() - overlay_size, bbox.Max, overlay_buf, NULL, &overlay_size); - - ImFormatString(overlay_buf, IM_ARRAYSIZE(overlay_buf), "%s", GstToolkit::time_to_string(*time).c_str()); - overlay_size = ImGui::CalcTextSize(overlay_buf, NULL); - overlay_size = ImVec2(3.f, -3.f - overlay_size.y); - if (overlay_size.x > 0.0f) - ImGui::RenderTextClipped( bbox.GetBL() + overlay_size, bbox.Max, overlay_buf, NULL, &overlay_size); - - - // draw slider grab handle - if (grab_slider_bb.Max.x > grab_slider_bb.Min.x) { - window->DrawList->AddRectFilled(grab_slider_bb.Min, grab_slider_bb.Max, grab_slider_color, style.GrabRounding); - } - - // draw the cursor - color = ImGui::GetColorU32(style.Colors[ImGuiCol_SliderGrab]); - pos = ImLerp(timeline_bbox.GetTL(), timeline_bbox.GetTR(), time_) - ImVec2(cursor_width, 2.f); - ImGui::RenderArrow(window->DrawList, pos, color, ImGuiDir_Up, cursor_scale); - - return left_mouse_press; -} - - void ImGuiToolkit::Bar(float value, float in, float out, float min, float max, const char* title, bool expand) { ImGuiWindow* window = ImGui::GetCurrentWindow(); diff --git a/ImGuiToolkit.h b/ImGuiToolkit.h index d66b939..aacbe99 100644 --- a/ImGuiToolkit.h +++ b/ImGuiToolkit.h @@ -27,11 +27,8 @@ namespace ImGuiToolkit // utility sliders void Bar (float value, float in, float out, float min, float max, const char* title, bool expand); - bool TimelineSlider (const char* label, guint64 *time, guint64 duration, guint64 step, float scale = 1.f); - bool TimelineSliderEdit (const char* label, guint64 *time, guint64 duration, guint64 step, - std::list >& segments); - bool InvisibleSliderInt(const char* label, uint *index, int min, int max, ImVec2 size); - + bool TimelineSlider (const char* label, guint64 *time, guint64 duration, guint64 step, const float width); + bool InvisibleSliderInt(const char* label, uint *index, int min, int max, const ImVec2 size); // fonts from ressources 'fonts/' typedef enum { diff --git a/ImGuiVisitor.cpp b/ImGuiVisitor.cpp index c21e710..c370259 100644 --- a/ImGuiVisitor.cpp +++ b/ImGuiVisitor.cpp @@ -297,7 +297,7 @@ void ImGuiVisitor::visit (Source& s) void ImGuiVisitor::visit (MediaSource& s) { - if ( s.mediaplayer()->duration() == GST_CLOCK_TIME_NONE) { + if ( s.mediaplayer()->isImage() ) { ImGuiToolkit::Icon(2,9); ImGui::SameLine(0, 10); ImGui::Text("Image File"); diff --git a/MediaPlayer.cpp b/MediaPlayer.cpp index 88cf80d..3c8a54e 100644 --- a/MediaPlayer.cpp +++ b/MediaPlayer.cpp @@ -42,9 +42,9 @@ MediaPlayer::MediaPlayer(string name) : id_(name) width_ = par_width_ = 640; height_ = 480; position_ = GST_CLOCK_TIME_NONE; - duration_ = GST_CLOCK_TIME_NONE; - start_position_ = GST_CLOCK_TIME_NONE; - frame_duration_ = GST_CLOCK_TIME_NONE; +// duration_ = GST_CLOCK_TIME_NONE; +// start_position_ = GST_CLOCK_TIME_NONE; +// frame_duration_ = GST_CLOCK_TIME_NONE; desired_state_ = GST_STATE_PAUSED; loop_ = LoopMode::LOOP_REWIND; @@ -82,14 +82,16 @@ void MediaPlayer::open(string path) { // set uri to open filename_ = path; - uri_ = string( gst_uri_construct("file", path.c_str()) ); + gchar *uritmp = gst_filename_to_uri(path.c_str(), NULL); + uri_ = string( uritmp ); + g_free(uritmp); // reset ready_ = false; /* Instantiate the Discoverer */ GError *err = NULL; - discoverer_ = gst_discoverer_new (5 * GST_SECOND, &err); + discoverer_ = gst_discoverer_new (45 * GST_SECOND, &err); if (!discoverer_) { Log::Warning("MediaPlayer Error creating discoverer instance: %s\n", err->message); g_clear_error (&err); @@ -117,7 +119,7 @@ void MediaPlayer::open(string path) void MediaPlayer::execute_open() -{ +{ // Create the simplest gstreamer pipeline possible : // " uridecodebin uri=file:///path_to_file/filename.mp4 ! videoconvert ! appsink " // equivalent to gst-launch-1.0 uridecodebin uri=file:///path_to_file/filename.mp4 ! videoconvert ! ximagesink @@ -169,19 +171,26 @@ void MediaPlayer::execute_open() gst_app_sink_set_max_buffers( GST_APP_SINK(sink), 100); gst_app_sink_set_drop (GST_APP_SINK(sink), true); - // set the callbacks - GstAppSinkCallbacks callbacks; - callbacks.new_preroll = callback_new_preroll; - if (isimage_) { - callbacks.eos = NULL; - callbacks.new_sample = NULL; - } - else { - callbacks.eos = callback_end_of_stream; - callbacks.new_sample = callback_new_sample; - } - gst_app_sink_set_callbacks (GST_APP_SINK(sink), &callbacks, this, NULL); - gst_app_sink_set_emit_signals (GST_APP_SINK(sink), false); +// // set the callbacks +// GstAppSinkCallbacks callbacks; +// callbacks.new_preroll = callback_new_preroll; +// if (isimage_) { +// callbacks.eos = NULL; +// callbacks.new_sample = NULL; +// } +// else { +// callbacks.eos = callback_end_of_stream; +// callbacks.new_sample = callback_new_sample; +// } +// gst_app_sink_set_callbacks (GST_APP_SINK(sink), &callbacks, this, NULL); +// gst_app_sink_set_emit_signals (GST_APP_SINK(sink), false); + + // connect callbacks + g_signal_connect(G_OBJECT(sink), "new-sample", G_CALLBACK (callback_new_sample), this); + g_signal_connect(G_OBJECT(sink), "new-preroll", G_CALLBACK (callback_new_preroll), this); + g_signal_connect(G_OBJECT(sink), "eos", G_CALLBACK (callback_end_of_stream), this); + gst_app_sink_set_emit_signals (GST_APP_SINK(sink), true); + // done with ref to sink gst_object_unref (sink); @@ -204,13 +213,18 @@ void MediaPlayer::execute_open() return; } - // create & init segment array - - // all good Log::Info("MediaPlayer %s Open %s (%s %d x %d)", id_.c_str(), uri_.c_str(), codec_name_.c_str(), width_, height_); ready_ = true; +// // in case discoverer failed to get duration +// if (timeline.end() == GST_CLOCK_TIME_NONE) { +// gint64 d = GST_CLOCK_TIME_NONE; +// if ( gst_element_query_duration(pipeline_, GST_FORMAT_TIME, &d) ) +// timeline.setEnd(d); +// } + + // register media player MediaPlayer::registered_.push_back(this); } @@ -233,62 +247,51 @@ void MediaPlayer::close() discoverer_ = nullptr; } + // not openned? nothing to close! if (!ready_) return; + // un-ready the media player + ready_ = false; + + // reset timeline + timeline.reset(); + // clean up GST if (pipeline_ != nullptr) { - gst_element_set_state (pipeline_, GST_STATE_NULL); + GstStateChangeReturn ret = gst_element_set_state (pipeline_, GST_STATE_NULL); + if (ret == GST_STATE_CHANGE_ASYNC) { + GstState state; + gst_element_get_state (pipeline_, &state, NULL, GST_CLOCK_TIME_NONE); + } gst_object_unref (pipeline_); pipeline_ = nullptr; } - // cleanup eventual remaining frame related memory - for(guint i = 0; i < N_VFRAME; i++){ - frame_[i].access.lock(); - if (frame_[i].vframe.buffer) - gst_video_frame_unmap(&frame_[i].vframe); - frame_[i].status = EMPTY; - frame_[i].access.unlock(); - } +// // cleanup eventual remaining frame related memory +// for(guint i = 0; i < N_VFRAME; i++){ +//// frame_[i].access.lock(); +// if (frame_[i].vframe.buffer) +// gst_video_frame_unmap(&frame_[i].vframe); +// frame_[i].status = EMPTY; +//// frame_[i].access.unlock(); +// } // cleanup opengl texture if (textureindex_) glDeleteTextures(1, &textureindex_); textureindex_ = 0; - // delete picture buffer + // cleanup picture buffer if (pbo_[0]) glDeleteBuffers(2, pbo_); pbo_size_ = 0; - // un-ready the media player - ready_ = false; - + // unregister media player MediaPlayer::registered_.remove(this); } -GstClockTime MediaPlayer::duration() -{ - // cannot play an image - if (isimage_) - return GST_CLOCK_TIME_NONE; - - if (duration_ == GST_CLOCK_TIME_NONE && pipeline_ != nullptr) { - gint64 d = GST_CLOCK_TIME_NONE; - if ( gst_element_query_duration(pipeline_, GST_FORMAT_TIME, &d) ) - duration_ = d; - } - - return duration_; -} - -GstClockTime MediaPlayer::frameDuration() -{ - return frame_duration_; -} - guint MediaPlayer::width() const { return width_; @@ -314,7 +317,7 @@ GstClockTime MediaPlayer::position() pos = p; } - return pos - start_position_; + return pos - timeline.start(); } void MediaPlayer::enable(bool on) @@ -347,6 +350,12 @@ bool MediaPlayer::isEnabled() const return enabled_; } + +bool MediaPlayer::isImage() const +{ + return isimage_; +} + void MediaPlayer::play(bool on) { // cannot play an image @@ -369,7 +378,7 @@ void MediaPlayer::play(bool on) // requesting to play, but stopped at end of stream : rewind first ! if ( desired_state_ == GST_STATE_PLAYING) { - if ( ( rate_>0.0 ? duration_ - position() : position() ) < 2 * frame_duration_ ) + if ( ( rate_>0.0 ? timeline.end() - position() : position() ) < 2 * timeline.step() ) rewind(); } @@ -429,7 +438,7 @@ void MediaPlayer::rewind() execute_seek_command(0); else // playing backward, loop to end - execute_seek_command(duration_ - frame_duration_); + execute_seek_command(timeline.end() - timeline.step()); } @@ -439,7 +448,7 @@ void MediaPlayer::step() if (!enabled_ || isPlaying()) return; - if ( position_ == ( rate_ < 0.0 ? start_position_ : duration() ) ) + if ( position_ == ( rate_ < 0.0 ? timeline.start() : timeline.end() ) ) rewind(); // step @@ -452,7 +461,9 @@ void MediaPlayer::seek(GstClockTime pos) return; // apply seek - GstClockTime target = CLAMP(pos, 0, duration_); + GstClockTime target = CLAMP(pos, 0, timeline.end()); +// GstClockTime target = CLAMP(pos, timeline.start(), timeline.end()); + // TODO: confirm that PTS are not possibly ZERO (use of start() is neceessary) execute_seek_command(target); } @@ -647,14 +658,20 @@ void MediaPlayer::update() // unkock frame after reading it frame_[read_index].access.unlock(); + if (isimage_) + return; + // manage loop mode - if (need_loop && !isimage_) { + if (need_loop) { execute_loop_command(); } // manage timeline -// if (position_!=GST_CLOCK_TIME_NONE) { +// TimeInterval gap; +// if (position_ != GST_CLOCK_TIME_NONE && timeline.gapAt(position_, gap)) { +// seek( (rate_>0.f) ? gap.end : gap.begin); +// // TODO : manage loop when jumping out of timeline // } @@ -688,7 +705,7 @@ void MediaPlayer::execute_seek_command(GstClockTime target) // create seek event with current position (rate changed ?) seek_pos = position(); // target is given but useless - else if ( ABS_DIFF(target, position()) < frame_duration_) { + else if ( ABS_DIFF(target, position()) < timeline.step()) { // ignore request return; } @@ -699,6 +716,24 @@ void MediaPlayer::execute_seek_command(GstClockTime target) if ( ABS(rate_) > 1.0 ) seek_flags |= GST_SEEK_FLAG_TRICKMODE; +// bool ret = false; + +// if (rate_ > 0) { + +// ret = gst_element_seek (pipeline_, rate_, GST_FORMAT_TIME, GST_SEEK_FLAG_FLUSH, +// GST_SEEK_TYPE_SET, seek_pos, +// GST_SEEK_TYPE_NONE, GST_CLOCK_TIME_NONE); +// } +// else { + +// ret = gst_element_seek (pipeline_, rate_, GST_FORMAT_TIME, GST_SEEK_FLAG_FLUSH, +// GST_SEEK_TYPE_NONE, GST_CLOCK_TIME_NONE, +// GST_SEEK_TYPE_SET, seek_pos); +// } + +// if (!ret) +// Log::Warning("MediaPlayer %s Seek failed", gst_element_get_name(pipeline_)); + // create seek event depending on direction GstEvent *seek_event = nullptr; if (rate_ > 0) { @@ -712,12 +747,13 @@ void MediaPlayer::execute_seek_command(GstClockTime target) // Send the event (ASYNC) if (seek_event && !gst_element_send_event(pipeline_, seek_event) ) - Log::Warning("MediaPlayer %s Seek failed", gst_element_get_name(pipeline_)); + Log::Warning("MediaPlayer %s Seek failed", id_.c_str()); else { #ifdef MEDIA_PLAYER_DEBUG - Log::Info("MediaPlayer %s Seek %ld %f", gst_element_get_name(pipeline_), seek_pos, rate_); + Log::Info("MediaPlayer %s Seek %ld %f", id_.c_str(), seek_pos, rate_); #endif } + } void MediaPlayer::setPlaySpeed(double s) @@ -803,13 +839,13 @@ bool MediaPlayer::fill_frame(GstBuffer *buf, MediaPlayer::FrameStatus status) frame_[write_index_].position = buf->pts; // set the start position (i.e. pts of first frame we got) - if (start_position_ == GST_CLOCK_TIME_NONE) - start_position_ = buf->pts; + if (timeline.start() == GST_CLOCK_TIME_NONE) + timeline.setStart(buf->pts); } } // give a position to EOS else { - frame_[write_index_].position = rate_ > 0.0 ? duration() : start_position_; + frame_[write_index_].position = rate_ > 0.0 ? timeline.end() : timeline.start(); } // unlock access to frame @@ -833,7 +869,7 @@ bool MediaPlayer::fill_frame(GstBuffer *buf, MediaPlayer::FrameStatus status) void MediaPlayer::callback_end_of_stream (GstAppSink *, gpointer p) { MediaPlayer *m = (MediaPlayer *)p; - if (m) { + if (m && m->ready_) { m->fill_frame(NULL, MediaPlayer::EOS); } } @@ -849,22 +885,22 @@ GstFlowReturn MediaPlayer::callback_new_preroll (GstAppSink *sink, gpointer p) if (sample != NULL) { // get buffer from sample - GstBuffer *buf = gst_buffer_ref ( gst_sample_get_buffer (sample) ); + GstBuffer *buf = /*gst_buffer_ref */( gst_sample_get_buffer (sample) ); MediaPlayer *m = (MediaPlayer *)p; - if (m) { + if (m && m->ready_) { // fill frame from buffer if ( !m->fill_frame(buf, MediaPlayer::PREROLL) ) ret = GST_FLOW_ERROR; // loop negative rate: emulate an EOS - if (m->playSpeed() < 0.f && buf->pts == m->start_position_) { + if (m->playSpeed() < 0.f && buf->pts <= m->timeline.start()) { m->fill_frame(NULL, MediaPlayer::EOS); } } // free buffers - gst_buffer_unref (buf); +// gst_buffer_unref (buf); gst_sample_unref (sample); } else @@ -884,29 +920,29 @@ GstFlowReturn MediaPlayer::callback_new_sample (GstAppSink *sink, gpointer p) if (sample != NULL && !gst_app_sink_is_eos (sink)) { // get buffer from sample - GstBuffer *buf = gst_buffer_ref ( gst_sample_get_buffer (sample) ); + GstBuffer *buf = /*gst_buffer_ref*/ ( gst_sample_get_buffer (sample) ); MediaPlayer *m = (MediaPlayer *)p; - if (m) { + if (m && m->ready_) { // fill frame with buffer if ( !m->fill_frame(buf, MediaPlayer::SAMPLE) ) ret = GST_FLOW_ERROR; // loop negative rate: emulate an EOS - if (m->playSpeed() < 0.f && buf->pts == m->start_position_) { + if (m->playSpeed() < 0.f && buf->pts <= m->timeline.start()) { m->fill_frame(NULL, MediaPlayer::EOS); } } // free buffer & sample - gst_buffer_unref (buf); +// gst_buffer_unref (buf); gst_sample_unref (sample); } - else - ret = GST_FLOW_FLUSHING; +// else +// ret = GST_FLOW_FLUSHING; return ret; } @@ -967,7 +1003,7 @@ void MediaPlayer::callback_discoverer_process (GstDiscoverer *discoverer, GstDis m->par_width_ = (m->width_ * parn) / pard; // if its a video, it duration, framerate, etc. if ( !m->isimage_ ) { - m->duration_ = gst_discoverer_info_get_duration (info); + m->timeline.setEnd( gst_discoverer_info_get_duration (info) ); m->seekable_ = gst_discoverer_info_get_seekable (info); guint frn = gst_discoverer_video_info_get_framerate_num(vinfo); guint frd = gst_discoverer_video_info_get_framerate_denom(vinfo); @@ -976,20 +1012,22 @@ void MediaPlayer::callback_discoverer_process (GstDiscoverer *discoverer, GstDis frd = 1; } m->framerate_ = static_cast(frn) / static_cast(frd); - m->frame_duration_ = (GST_SECOND * static_cast(frd)) / (static_cast(frn)); + m->timeline.setStep( (GST_SECOND * static_cast(frd)) / (static_cast(frn)) ); } // try to fill-in the codec information GstCaps *caps = gst_discoverer_stream_info_get_caps (tmpinf); if (caps) { - m->codec_name_ = std::string( gst_pb_utils_get_codec_description(caps) ) + " "; + gchar *codecstring = gst_pb_utils_get_codec_description(caps); + m->codec_name_ = std::string( codecstring ) + " "; + g_free(codecstring); gst_caps_unref (caps); } - const GstTagList *tags = gst_discoverer_stream_info_get_tags(tmpinf); - if ( tags ) { - gchar *container = NULL; - gst_tag_list_get_string (tags, GST_TAG_CONTAINER_FORMAT, &container); - if (container) m->codec_name_ += std::string(container); - } +// const GstTagList *tags = gst_discoverer_stream_info_get_tags(tmpinf); +// if ( tags ) { +// gchar *container = NULL; +// gst_tag_list_get_string (tags, GST_TAG_CONTAINER_FORMAT, &container); +// if (container) m->codec_name_ += std::string(container); +// } // exit loop foundvideostream = true; } diff --git a/MediaPlayer.h b/MediaPlayer.h index 4055269..48f945a 100644 --- a/MediaPlayer.h +++ b/MediaPlayer.h @@ -74,6 +74,10 @@ public: * True if enabled * */ bool isEnabled() const; + /** + * True if its an image + * */ + bool isImage() const; /** * Pause / Play * Can play backward if play speed is negative @@ -136,13 +140,13 @@ public: * */ GstClockTime position(); /** - * Get total duration time - * */ - GstClockTime duration(); - /** - * Get duration of one frame - * */ - GstClockTime frameDuration(); + * @brief timeline contains all info on timing: + * - start position : timeline.start() + * - end position : timeline.end() + * - duration : timeline.duration() + * - frame duration : timeline.step() + */ + Timeline timeline; /** * Get framerate of the media * */ @@ -195,9 +199,11 @@ private: guint par_width_; // width to match pixel aspect ratio guint bitrate_; GstClockTime position_; - GstClockTime start_position_; - GstClockTime duration_; - GstClockTime frame_duration_; + +// GstClockTime start_position_; +// GstClockTime duration_; +// GstClockTime frame_duration_; + gdouble rate_; LoopMode loop_; gdouble framerate_; @@ -276,6 +282,7 @@ private: static void callback_end_of_stream (GstAppSink *, gpointer); static GstFlowReturn callback_new_preroll (GstAppSink *, gpointer ); static GstFlowReturn callback_new_sample (GstAppSink *, gpointer); + static void callback_discoverer_process (GstDiscoverer *discoverer, GstDiscovererInfo *info, GError *err, MediaPlayer *m); static void callback_discoverer_finished(GstDiscoverer *discoverer, MediaPlayer *m); diff --git a/MediaSource.cpp b/MediaSource.cpp index 8baeaeb..27c4f63 100644 --- a/MediaSource.cpp +++ b/MediaSource.cpp @@ -86,7 +86,7 @@ void MediaSource::init() attach(renderbuffer); // icon in mixing view - if (mediaplayer_->duration() == GST_CLOCK_TIME_NONE) { + if (mediaplayer_->isImage()) { overlays_[View::MIXING]->attach( new Symbol(Symbol::IMAGE, glm::vec3(0.8f, 0.8f, 0.01f)) ); overlays_[View::LAYER]->attach( new Symbol(Symbol::IMAGE, glm::vec3(0.8f, 0.8f, 0.01f)) ); } diff --git a/Session.cpp b/Session.cpp index 7cc0cb7..b3d2890 100644 --- a/Session.cpp +++ b/Session.cpp @@ -38,6 +38,11 @@ Session::~Session() // erase this source from the list it = deleteSource(*it); } + + delete config_[View::RENDERING]; + delete config_[View::GEOMETRY]; + delete config_[View::LAYER]; + delete config_[View::MIXING]; } void Session::setActive (bool on) diff --git a/SessionCreator.cpp b/SessionCreator.cpp index c1f8129..e6c1c00 100644 --- a/SessionCreator.cpp +++ b/SessionCreator.cpp @@ -44,6 +44,11 @@ SessionCreator::SessionCreator(Session *session): Visitor(), session_(session) } +SessionCreator::~SessionCreator() +{ + delete xmlDoc_; +} + bool SessionCreator::load(const std::string& filename) { XMLError eResult = xmlDoc_->LoadFile(filename.c_str()); @@ -179,6 +184,20 @@ void SessionCreator::visit(MediaPlayer &n) { XMLElement* mediaplayerNode = xmlCurrent_->FirstChildElement("MediaPlayer"); if (mediaplayerNode) { + // timeline + XMLElement *gapselement = mediaplayerNode->FirstChildElement("Gaps"); + if (gapselement) { + XMLElement* gap = gapselement->FirstChildElement("Interval"); + for( ; gap ; gap = gap->NextSiblingElement()) + { + GstClockTime a = GST_CLOCK_TIME_NONE; + GstClockTime b = GST_CLOCK_TIME_NONE; + gap->QueryUnsigned64Attribute("begin", &a); + gap->QueryUnsigned64Attribute("end", &b); + n.timeline.addGap( a, b ); + } + } + // playing properties double speed = 1.0; mediaplayerNode->QueryDoubleAttribute("speed", &speed); n.setPlaySpeed(speed); diff --git a/SessionCreator.h b/SessionCreator.h index 1f6d6af..39e73f1 100644 --- a/SessionCreator.h +++ b/SessionCreator.h @@ -18,6 +18,7 @@ class SessionCreator : public Visitor { public: SessionCreator(Session *session = nullptr); + ~SessionCreator(); bool load(const std::string& filename); inline Session *session() const { return session_; } diff --git a/SessionVisitor.cpp b/SessionVisitor.cpp index 35e1194..38a89b6 100644 --- a/SessionVisitor.cpp +++ b/SessionVisitor.cpp @@ -147,7 +147,16 @@ void SessionVisitor::visit(MediaPlayer &n) newelement->SetAttribute("loop", (int) n.loop()); newelement->SetAttribute("speed", n.playSpeed()); - // TODO Segments + // gaps in timeline + XMLElement *gapselement = xmlDoc_->NewElement("Gaps"); + std::list< std::pair > gaps = n.timeline.gaps(); + for( auto it = gaps.begin(); it!= gaps.end(); it++) { + XMLElement *g = xmlDoc_->NewElement("Interval"); + g->SetAttribute("begin", (*it).first); + g->SetAttribute("end", (*it).second); + gapselement->InsertEndChild(g); + } + newelement->InsertEndChild(gapselement); xmlCurrent_->InsertEndChild(newelement); } diff --git a/Timeline.cpp b/Timeline.cpp index cc1336c..e906ae6 100644 --- a/Timeline.cpp +++ b/Timeline.cpp @@ -17,71 +17,181 @@ Timeline::Timeline() : array_(nullptr) Timeline::~Timeline() { reset(); +// free(array_); } void Timeline::reset() { - start_ = GST_CLOCK_TIME_NONE; - end_ = GST_CLOCK_TIME_NONE; - num_frames_ = 0; - array_size_ = 0; - // clear segment array - if (array_ != nullptr) - free(array_); + // reset timing + timing_.begin = GST_CLOCK_TIME_NONE; + timing_.end = GST_CLOCK_TIME_NONE; + step_ = GST_CLOCK_TIME_NONE; + +// // clear gaps +// gaps_.clear(); + +// // clear segment array +// if (array_ != nullptr) +// free(array_); + +// // avoid crash by providing a valid pointer +// array_size_ = 1; +// array_ = (float *) malloc(sizeof(float)); +// array_[0] = 1.f; +// need_update_ = true; } -void Timeline::init(GstClockTime start, GstClockTime end, GstClockTime frame_duration) +void Timeline::setStart(GstClockTime start) { - reset(); - - start_ = start; - end_ = end; - num_frames_ = (size_t) end_ / (size_t) frame_duration; - - array_size_ = MIN( SEGMENT_ARRAY_MAX_SIZE, num_frames_); - array_ = (float *) malloc(array_size_ * sizeof(float)); - for (int i = 0; i < array_size_; ++i) - array_[i] = 1.f; - - Log::Info("%d frames in timeline", array_size_); + timing_.begin = start; + need_update_ = true; } -bool Timeline::addPlaySegment(GstClockTime begin, GstClockTime end) +void Timeline::setEnd(GstClockTime end) { - return addPlaySegment( MediaSegment(begin, end) ); + timing_.end = end; + need_update_ = true; } -bool Timeline::addPlaySegment(MediaSegment s) +void Timeline::setStep(GstClockTime dt) { - if ( s.is_valid() ) - return segments_.insert(s).second; + step_ = dt; + need_update_ = true; +} + +void Timeline::updateGapsFromArray() +{ +// if (need_update_) +// return; + +// // reset gaps +// gaps_.clear(); + +// // loop over the array to detect gaps +// float status = 1.f; +// GstClockTime begin_gap = GST_CLOCK_TIME_NONE; +// for (size_t i = 0; i < array_size_; ++i) { +// // detect a change of value between two slots +// if ( array_[i] != status) { +// // compute time of the event in array +// GstClockTime t = (timing_.duration() * i) / array_size_; +// // change from 1.f to 0.f : begin of a gap +// if (status) { +// begin_gap = t; +// } +// // change from 0.f to 1.f : end of a gap +// else { +// addGap( begin_gap, t ); +// begin_gap = GST_CLOCK_TIME_NONE; +// } +// // swap +// status = array_[i]; +// } +// } +// // end a potentially pending gap if reached end of array with no end of gap +// if (begin_gap != GST_CLOCK_TIME_NONE) +// addGap( begin_gap, timing_.end ); + +} + +void Timeline::updateArrayFromGaps() +{ +// if (step_ != GST_CLOCK_TIME_NONE && timing_.is_valid()) { + +// // clear segment array +// if (array_ != nullptr) +// free(array_); + +// array_size_ = MIN( SEGMENT_ARRAY_MAX_SIZE, duration() / step_); +// array_ = (float *) malloc(array_size_ * sizeof(float)); +// for (int i = 0; i < array_size_; ++i) +// array_[i] = 1.f; + +// Log::Info("%d frames in timeline", array_size_); + +// // fill the array from gaps +// // NB: this is the less efficient algorithm possible! but we should not keep arrays anyway +// // TODO : implement an ImGui widget to plot a timeline instead of an array +// TimeInterval gap; +// for (size_t i = 0; i < array_size_; ++i) { +// GstClockTime t = (timing_.duration() * i) / array_size_; +// array_[i] = gapAt(t, gap) ? 0.f : 1.f; +// } + +// need_update_ = false; +// } +} + +size_t Timeline::numGaps() +{ + return gaps_.size(); +} + +bool Timeline::gapAt(const GstClockTime t, TimeInterval &gap) +{ +// TimeIntervalSet::const_iterator g = std::find_if(gaps_.begin(), gaps_.end(), includesTime(t)); + +// if ( g != gaps_.end() ) { +// gap = (*g); +// return true; +// } return false; } -bool Timeline::removeAllPlaySegmentOverlap(MediaSegment s) +bool Timeline::addGap(GstClockTime begin, GstClockTime end) { - bool ret = removePlaySegmentAt(s.begin); - return removePlaySegmentAt(s.end) || ret; + return addGap( TimeInterval(begin, end) ); } -bool Timeline::removePlaySegmentAt(GstClockTime t) +bool Timeline::addGap(TimeInterval s) { - MediaSegmentSet::const_iterator s = std::find_if(segments_.begin(), segments_.end(), containsTime(t)); - - if ( s != segments_.end() ) { - segments_.erase(s); - return true; - } +// if ( s.is_valid() ) { +// need_update_ = true; +// return gaps_.insert(s).second; +// } return false; } -std::list< std::pair > Timeline::getPlaySegments() const +bool Timeline::removeGaptAt(GstClockTime t) +{ +// TimeIntervalSet::const_iterator s = std::find_if(gaps_.begin(), gaps_.end(), includesTime(t)); + +// if ( s != gaps_.end() ) { +// gaps_.erase(s); +// need_update_ = true; +// return true; +// } + + return false; +} + +void Timeline::clearGaps() +{ + gaps_.clear(); + need_update_ = true; +} + +std::list< std::pair > Timeline::gaps() const { std::list< std::pair > ret; - for (MediaSegmentSet::iterator it = segments_.begin(); it != segments_.end(); it++) + for (TimeIntervalSet::iterator it = gaps_.begin(); it != gaps_.end(); it++) ret.push_back( std::make_pair( it->begin, it->end ) ); return ret; } + +float *Timeline::array() +{ +// if (need_update_) +// updateArrayFromGaps(); + return array_; +} + +size_t Timeline::arraySize() +{ +// if (need_update_) +// updateArrayFromGaps(); + return array_size_; +} diff --git a/Timeline.h b/Timeline.h index 97ae369..0222a3e 100644 --- a/Timeline.h +++ b/Timeline.h @@ -8,18 +8,18 @@ #include -struct MediaSegment +struct TimeInterval { GstClockTime begin; GstClockTime end; - MediaSegment() + TimeInterval() { begin = GST_CLOCK_TIME_NONE; end = GST_CLOCK_TIME_NONE; } - MediaSegment(GstClockTime b, GstClockTime e) + TimeInterval(GstClockTime b, GstClockTime e) { if ( b < e ) { begin = b; @@ -29,39 +29,55 @@ struct MediaSegment end = GST_CLOCK_TIME_NONE; } } + inline GstClockTime duration() const + { + return is_valid() ? (end - begin) : GST_CLOCK_TIME_NONE; + } inline bool is_valid() const { return begin != GST_CLOCK_TIME_NONE && end != GST_CLOCK_TIME_NONE && begin < end; } - inline bool operator < (const MediaSegment b) const + inline bool operator < (const TimeInterval b) const { return (this->is_valid() && b.is_valid() && this->end < b.begin); } - inline bool operator == (const MediaSegment b) const + inline bool operator == (const TimeInterval b) const { return (this->begin == b.begin && this->end == b.end); } - inline bool operator != (const MediaSegment b) const + inline bool operator != (const TimeInterval b) const { return (this->begin != b.begin || this->end != b.end); } + inline TimeInterval& operator = (const TimeInterval& b) + { + if (this != &b) { + this->begin = b.begin; + this->end = b.end; + } + return *this; + } + inline bool includes(const GstClockTime t) const + { + return (is_valid() && t != GST_CLOCK_TIME_NONE && t > this->begin && t < this->end); + } }; -struct containsTime: public std::unary_function +struct includesTime: public std::unary_function { - inline bool operator()(const MediaSegment s) const + inline bool operator()(const TimeInterval s) const { - return ( s.is_valid() && _t > s.begin && _t < s.end ); + return s.includes(_t); } - containsTime(GstClockTime t) : _t(t) { } + includesTime(GstClockTime t) : _t(t) { } private: GstClockTime _t; }; -typedef std::set MediaSegmentSet; +typedef std::set TimeIntervalSet; class Timeline @@ -71,27 +87,52 @@ public: ~Timeline(); void reset(); - void init(GstClockTime start, GstClockTime end, GstClockTime frame_duration); - bool addPlaySegment(GstClockTime begin, GstClockTime end); - bool addPlaySegment(MediaSegment s); - bool removePlaySegmentAt(GstClockTime t); - bool removeAllPlaySegmentOverlap(MediaSegment s); - std::list< std::pair > getPlaySegments() const; + // global properties of the timeline + // timeline is invalid untill all 3 are set + void setStart(GstClockTime start); + void setEnd(GstClockTime end); + void setStep(GstClockTime dt); - inline float *Array() { return array_; } - inline size_t ArraySize() { return array_size_; } + // get properties + inline GstClockTime start() const { return timing_.begin; } + inline GstClockTime end() const { return timing_.end; } + inline GstClockTime step() const { return step_; } + inline GstClockTime duration() const { return timing_.duration(); } + + // Add / remove gaps in the timeline + bool addGap(TimeInterval s); + bool addGap(GstClockTime begin, GstClockTime end); + bool removeGaptAt(GstClockTime t); + void clearGaps(); + + // get gaps + size_t numGaps(); + bool gapAt(const GstClockTime t, TimeInterval &gap); + std::list< std::pair > gaps() const; + + // direct access to the array representation of the timeline + // TODO : implement an ImGui widget to plot a timeline instead of an array + float *array(); + size_t arraySize(); + // synchronize data structures + void updateGapsFromArray(); + void updateArrayFromGaps(); private: - GstClockTime start_; - GstClockTime end_; - size_t num_frames_; + // global information on the timeline + TimeInterval timing_; + GstClockTime step_; + // main data structure containing list of gaps in the timeline + TimeIntervalSet gaps_; + + // supplementary data structure needed to display and edit the timeline + bool need_update_; + void init_array(); float *array_; size_t array_size_; - MediaSegmentSet segments_; - MediaSegmentSet::iterator current_segment_; }; diff --git a/UserInterfaceManager.cpp b/UserInterfaceManager.cpp index 6161ea8..497d94f 100644 --- a/UserInterfaceManager.cpp +++ b/UserInterfaceManager.cpp @@ -987,7 +987,7 @@ void UserInterface::RenderPreview() if (ImGui::IsItemHovered()) { ImDrawList* draw_list = ImGui::GetWindowDrawList(); - draw_list->AddRectFilled(draw_pos, ImVec2(draw_pos.x + width, draw_pos.y + ImGui::GetTextLineHeightWithSpacing()), IM_COL32(5, 5, 5, 100)); + draw_list->AddRectFilled(draw_pos, ImVec2(draw_pos.x + width, draw_pos.y + ImGui::GetTextLineHeightWithSpacing()), IMGUI_COLOR_OVERLAY); ImGui::SetCursorScreenPos(draw_pos); ImGui::Text(" %d x %d px, %d fps", output->width(), output->height(), int(1000.f / Mixer::manager().dt()) ); } @@ -1066,7 +1066,7 @@ void MediaController::Render() // menu (no title bar) if (ImGui::BeginMenuBar()) { - if (ImGui::BeginMenu(ICON_FA_FILM)) + if (ImGui::BeginMenu(IMGUI_TITLE_MEDIAPLAYER)) { ImGui::MenuItem( ICON_FA_EYE " Preview", nullptr, &Settings::application.widget.media_player_view); @@ -1127,16 +1127,18 @@ void MediaController::Render() static float timeline_zoom = 1.f; const float width = ImGui::GetContentRegionAvail().x; const float timeline_height = 2.f * (ImGui::GetFontSize() + ImGui::GetStyle().FramePadding.y); + const float segments_height = ImGui::GetFontSize(); + const float slider_zoom_width = segments_height; if (Settings::application.widget.media_player_view) { // set an image height to fill the vertical space, minus the height of control bar float image_height = ImGui::GetContentRegionAvail().y; - if (mp_->duration() != GST_CLOCK_TIME_NONE) { - // leave space for buttons, spacing and timeline - image_height -= ImGui::GetFrameHeight() + timeline_height + 3.f * ImGui::GetStyle().ItemSpacing.y; - // leave space for scrollbar - image_height -= ImGui::GetStyle().ScrollbarSize; + if ( !mp_->isImage() ) { + // leave space for buttons and timelines + image_height -= ImGui::GetFrameHeight() + timeline_height + segments_height ; + // leave space for scrollbar & spacing + image_height -= ImGui::GetStyle().ScrollbarSize + 3.f * ImGui::GetStyle().ItemSpacing.y; } // display media @@ -1149,14 +1151,14 @@ void MediaController::Render() // display media information if (ImGui::IsItemHovered()) { - float tooltip_height = (follow_active_source_? 3.f:2.f)* ImGui::GetTextLineHeightWithSpacing(); + float tooltip_height = 3.f * ImGui::GetTextLineHeightWithSpacing(); ImDrawList* draw_list = ImGui::GetWindowDrawList(); - draw_list->AddRectFilled(tooltip_pos, ImVec2(tooltip_pos.x + width, tooltip_pos.y + tooltip_height), IM_COL32(5, 5, 5, 100)); + draw_list->AddRectFilled(ImVec2(tooltip_pos.x - 10.f, tooltip_pos.y), + ImVec2(tooltip_pos.x + width + 10.f, tooltip_pos.y + tooltip_height), IMGUI_COLOR_OVERLAY); ImGui::SetCursorScreenPos(tooltip_pos); - if (follow_active_source_) - ImGui::Text(" %s", mp_->filename().c_str()); + ImGui::Text(" %s", mp_->filename().c_str()); ImGui::Text(" %s", mp_->codec().c_str()); if ( mp_->frameRate() > 0.f ) ImGui::Text(" %d x %d px, %.2f / %.2f fps", mp_->width(), mp_->height(), mp_->updateFrameRate() , mp_->frameRate() ); @@ -1166,16 +1168,18 @@ void MediaController::Render() } // display time - ImGuiToolkit::PushFont(ImGuiToolkit::FONT_LARGE); - ImGui::SetCursorPos( ImVec2(return_to_pos.x + 5, return_to_pos.y - ImGui::GetTextLineHeightWithSpacing()) ); - ImGui::Text("%s", GstToolkit::time_to_string(mp_->position(), GstToolkit::TIME_STRING_FIXED).c_str()); - ImGui::PopFont(); + if ( !mp_->isImage() ) { + ImGuiToolkit::PushFont(ImGuiToolkit::FONT_LARGE); + ImGui::SetCursorPos( ImVec2(return_to_pos.x + 5, return_to_pos.y - ImGui::GetTextLineHeightWithSpacing()) ); + ImGui::Text("%s", GstToolkit::time_to_string(mp_->position(), GstToolkit::TIME_STRING_FIXED).c_str()); + ImGui::PopFont(); + } ImGui::SetCursorPos(return_to_pos); } // Control bar - if ( mp_->isEnabled() && mp_->duration() != GST_CLOCK_TIME_NONE) { + if ( mp_->isEnabled() && !mp_->isImage()) { float spacing = ImGui::GetStyle().ItemInnerSpacing.x; @@ -1195,7 +1199,7 @@ void MediaController::Render() ImGui::PushButtonRepeat(true); if (ImGui::Button( mp_->playSpeed() < 0 ? ICON_FA_BACKWARD :ICON_FA_FORWARD)) - mp_->fastForward (); + mp_->jump (); ImGui::PopButtonRepeat(); } else { @@ -1205,7 +1209,7 @@ void MediaController::Render() ImGui::PushButtonRepeat(true); if (ImGui::Button( mp_->playSpeed() < 0 ? ICON_FA_STEP_BACKWARD : ICON_FA_STEP_FORWARD)) - mp_->seekNextFrame(); + mp_->step(); ImGui::PopButtonRepeat(); } @@ -1224,39 +1228,85 @@ void MediaController::Render() // ImGui::SetNextItemWidth(width - 90.0); if (ImGui::DragFloat( "##Speed", &speed, 0.01f, -10.f, 10.f, "Speed x %.1f", 2.f)) mp_->setPlaySpeed( static_cast(speed) ); - // reset play to x1 + + // reset ImGui::SameLine(0, spacing); - if (ImGuiToolkit::ButtonIcon(19, 15)) { + if (ImGuiToolkit::ButtonIcon(11, 14)) { + timeline_zoom = 1.f; speed = 1.f; mp_->setPlaySpeed( static_cast(speed) ); mp_->setLoop( MediaPlayer::LOOP_REWIND ); } - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(3, 3)); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(3.f, 3.f)); + + // Prepare controllibg the current media player with timeline +// float *array = mp_->timeline.array(); +// size_t array_size = mp_->timeline.arraySize(); + guint64 current_t = mp_->position(); + guint64 seek_t = current_t; // scrolling sub-window ImGui::BeginChild("##scrolling", - ImVec2(ImGui::GetContentRegionAvail().x - 20.0, - timeline_height + ImGui::GetStyle().ScrollbarSize ), + ImVec2(ImGui::GetContentRegionAvail().x - slider_zoom_width - 3.0, + timeline_height + segments_height + ImGui::GetStyle().ScrollbarSize ), false, ImGuiWindowFlags_HorizontalScrollbar); + { + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(1.f, 1.f)); + ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 1.f); - // custom timeline slider - guint64 current_t = mp_->position(); - guint64 seek_t = current_t; - slider_pressed_ = ImGuiToolkit::TimelineSlider("##timeline", &seek_t, - mp_->duration(), mp_->frameDuration(), timeline_zoom); + ImVec2 size = ImGui::CalcItemSize(ImVec2(-FLT_MIN, 0.0f), ImGui::CalcItemWidth(), segments_height -1); + size.x *= timeline_zoom; + + // draw position when entering +// ImVec2 draw_pos = ImGui::GetCursorPos(); + +// // capture user input (invisible) +// uint press_index = mp_->timeline.arraySize(); +// bool pressed = ImGuiToolkit::InvisibleSliderInt("##TimelinePicking", &press_index, 0, array_size-1, size); + +// // behavior on action on array of segments +// static bool active = false; +// static float target_value = 0.f; +// static uint starting_index = array_size; +// if (pressed != active) { +// active = pressed; +// if (pressed) { +// starting_index = press_index; +// target_value = array[starting_index] > 0.f ? 0.f : 1.f; +// } +// else// action on released +// { +// mp_->timeline.updateGapsFromArray(); +// } +// } +// if (active) { +// for (int i = MIN(starting_index, press_index); i < MAX(starting_index, press_index); ++i) +// array[i] = target_value; +// } + +// // back to drawing position to draw the segments data with historgram +// ImGui::SetCursorPos(draw_pos); +// ImGui::PlotHistogram("##TimelineHistogram", array, array_size-1.f, 0, NULL, 0.0f, 1.0f, size); + + // custom timeline slider + slider_pressed_ = ImGuiToolkit::TimelineSlider("##timeline", &seek_t, + mp_->timeline.end(), mp_->timeline.step(), size.x); + + ImGui::PopStyleVar(2); + } ImGui::EndChild(); // zoom slider ImGui::SameLine(); - ImGui::VSliderFloat("##zoom", ImVec2(17, timeline_height), &timeline_zoom, 1.0, 5.f, ""); + ImGui::VSliderFloat("##TimelineZoom", ImVec2(slider_zoom_width, timeline_height + segments_height), &timeline_zoom, 1.0, 5.f, ""); ImGui::PopStyleVar(); // if the seek target time is different from the current position time // (i.e. the difference is less than one frame) - if ( ABS_DIFF (current_t, seek_t) > mp_->frameDuration() ) { + if ( ABS_DIFF (current_t, seek_t) > mp_->timeline.step() ) { // request seek (ASYNC) - mp_->seekTo(seek_t); + mp_->seek(seek_t); slider_pressed_ = false; } // play/stop command should be following the playing mode (buttons) @@ -1267,7 +1317,8 @@ void MediaController::Render() // NB: The seek command performed an ASYNC state change, but // gst_element_get_state called in isPlaying(true) will wait // for the state change to complete : do not remove it! - if ( mp_->isPlaying(true) != media_play ) { +// TODO : DO NOT ask for status every frame : high performance cost + if ( mp_->isPlaying() != media_play ) { mp_->play( media_play ); } @@ -1759,10 +1810,11 @@ void Navigator::RenderNewPannel() std::list recent = Settings::application.recentImport.filenames; for (std::list::iterator path = recent.begin(); path != recent.end(); path++ ) { - if ( SystemToolkit::file_exists(*path)) { - std::string label = path->substr( path->size() - MIN( 35, path->size()) ); + std::string recentpath(*path); + if ( SystemToolkit::file_exists(recentpath)) { + std::string label = SystemToolkit::trunc_filename(recentpath, 35); if (ImGui::Selectable( label.c_str() )) { - new_source_preview_.setSource( Mixer::manager().createSourceFile(path->c_str()), label); + new_source_preview_.setSource( Mixer::manager().createSourceFile(recentpath.c_str()), label); } } } @@ -1773,7 +1825,7 @@ void Navigator::RenderNewPannel() // show preview new_source_preview_.Render(ImGui::GetContentRegionAvail().x IMGUI_RIGHT_ALIGN, true); // or press Validate button - ImGui::SetCursorPosY(ImGui::GetCursorPosY() + ImGui::GetTextLineHeight() / 2.f); + ImGui::Spacing(); if ( ImGui::Button(ICON_FA_CHECK " Create", ImVec2(pannel_width_ - padding_width_, 0)) ) { Mixer::manager().addSource(new_source_preview_.getSource()); selected_button[NAV_NEW] = false; @@ -2023,18 +2075,22 @@ void Navigator::RenderMainPannel() ImGui::ListBoxHeader("##Sessions", 5); static std::string file_info = ""; static std::list::iterator file_selected = sessions_list.end(); - for(auto filename = sessions_list.begin(); filename != sessions_list.end(); filename++) { - if (ImGui::Selectable( SystemToolkit::filename(*filename).c_str(), false, ImGuiSelectableFlags_AllowDoubleClick )) { - if (ImGui::IsMouseDoubleClicked(0)) { - Mixer::manager().open( *filename ); + for(auto it = sessions_list.begin(); it != sessions_list.end(); it++) { + std::string sessionfilename(*it); + std::string shortname = SystemToolkit::filename(*it); + if (sessionfilename.empty()) + break; + if (ImGui::Selectable( shortname.c_str(), false, ImGuiSelectableFlags_AllowDoubleClick )) { + if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) { + Mixer::manager().open( sessionfilename ); session_selected = true; } else { - file_info = SessionCreator::info(*filename); - file_selected = filename; + file_info = SessionCreator::info(sessionfilename); + file_selected = it; } } - if (ImGui::IsItemHovered() && file_selected != filename) { + if (ImGui::IsItemHovered() && file_selected != it) { file_info.clear(); file_selected = sessions_list.end(); } @@ -2105,7 +2161,7 @@ void Navigator::RenderMainPannel() else { ImGui::SetCursorPosY(height_ -h); } - ImGui::SetCursorPosY(ImGui::GetCursorPosY() + ImGui::GetTextLineHeightWithSpacing()); + ImGui::Spacing(); if ( ImGui::Button( ICON_FA_CROW " vimix", ImVec2(ImGui::GetContentRegionAvail().x, 0)) ) UserInterface::manager().show_vimix_config = true; if ( ImGui::Button(" ImGui ")) @@ -2158,55 +2214,7 @@ int hover(const char *label) } -bool hoverer(const char* label, uint *index, int min, int max, ImVec2 size) -{ - // get window - ImGuiWindow* window = ImGui::GetCurrentWindow(); - if (window->SkipItems) - return false; - - // get id -// ImGuiContext& g = *GImGui; - const ImGuiID id = window->GetID(label); - - ImVec2 pos = window->DC.CursorPos; - ImRect bbox(pos, pos + size); - ImGui::ItemSize(size); - if (!ImGui::ItemAdd(bbox, id)) - return false; - - // read user input from system - bool left_mouse_press = false; - const bool hovered = ImGui::ItemHoverable(bbox, id); - bool temp_input_is_active = ImGui::TempInputIsActive(id); - if (!temp_input_is_active) - { - const bool focus_requested = ImGui::FocusableItemRegister(window, id); - left_mouse_press = hovered && ImGui::IsMouseDown(ImGuiMouseButton_Left); - if (focus_requested || left_mouse_press) - { - ImGui::SetActiveID(id, window); - ImGui::SetFocusID(id, window); - ImGui::FocusWindow(window); - } - } - - // time Slider behavior - ImRect grab_slider_bb; - uint _zero = min; - uint _end = max; - bool pressed = ImGui::SliderBehavior(bbox, id, ImGuiDataType_U32, index, &_zero, - &_end, "%d", 1.f, ImGuiSliderFlags_None, &grab_slider_bb); - -// bbox = ImRect(pos, pos + ImVec2(size.x, size.y / 2.f)); -// if (ImGui::IsMouseHoveringRect(bbox.GetTL(), bbox.GetBR())) -// *val = 1.f; -// else -// *val = 0.f; - - return pressed; - -} +#define SEGMENT_ARRAY_MAX 1000 void ShowSandbox(bool* p_open) @@ -2223,80 +2231,109 @@ void ShowSandbox(bool* p_open) std::list< std::pair > segments; segments.push_back( std::make_pair(GST_SECOND*1, GST_SECOND*2) ); - guint64 duration = GST_SECOND * 4; + guint64 duration = GST_SECOND * 6; guint64 step = GST_MSECOND * 20; static guint64 t = 0; - bool slider_pressed = ImGuiToolkit::TimelineSliderEdit("timeline", &t, duration, step, segments); +// bool slider_pressed = ImGuiToolkit::TimelineSlider("timeline", &t, duration, step); + static float *arr = nullptr; + static size_t array_size = 0; + // static float arr[] = { 0.6f, 0.1f, 1.0f, 0.5f, 0.92f, 0.1f, 0.2f, 0.f, 1.f, 0.4f, + // 0.6f, 0.1f, 1.0f, 0.5f, 0.92f, 0.1f, 0.2f, 0.f, 1.f, 0.4f, + // 0.6f, 0.1f, 1.0f, 0.5f, 0.92f, 0.1f, 0.2f, 0.f, 1.f, 0.4f, + // 0.6f, 0.1f, 1.0f, 0.5f, 0.92f, 0.1f, 0.2f, 0.f, 1.f, 0.4f, + // 0.6f, 0.1f, 1.0f, 0.5f, 0.92f, 0.1f, 0.2f, 0.f, 1.f, 0.4f, + // 0.6f, 0.1f, 1.0f, 0.5f, 0.92f, 0.1f, 0.2f, 0.f, 1.f, 0.4f, + // 0.6f, 0.1f, 1.0f, 0.5f, 0.92f, 0.1f, 0.2f, 0.f, 1.f, 0.4f, + // 0.6f, 0.1f, 1.0f, 0.5f, 0.92f, 0.1f, 0.2f, 0.f, 1.f, 0.4f, + // 0.6f, 0.1f, 1.0f, 0.5f, 0.92f, 0.1f, 0.2f, 0.f, 1.f, 0.4f, + // 0.6f, 0.1f, 1.0f, 0.5f, 0.92f, 0.1f, 0.2f, 0.f, 1.f, 0.4f, + // 0.6f, 0.1f, 1.0f, 0.5f, 0.92f, 0.1f, 0.2f, 0.f, 1.f, 0.4f, + // 0.6f, 0.1f, 1.0f, 0.5f, 0.92f, 0.1f, 0.2f, 0.f, 1.f, 0.4f, + // 0.6f, 0.1f, 1.0f, 0.5f, 0.92f, 0.1f, 0.2f, 0.f, 1.f, 0.4f, + // 0.6f, 0.1f, 1.0f, 0.5f, 0.92f, 0.1f, 0.2f, 0.f, 1.f, 0.4f, + // 0.6f, 0.1f, 1.0f, 0.5f, 0.92f, 0.1f, 0.2f, 0.f, 1.f, 0.4f, + // 0.6f, 0.1f, 1.0f, 0.5f, 0.92f, 0.1f, 0.2f, 0.f, 1.f, 0.4f, + // 0.6f, 0.1f, 1.0f, 0.5f, 0.92f, 0.1f, 0.2f, 0.f, 1.f, 0.4f, + // 0.6f, 0.1f, 1.0f, 0.5f, 0.92f, 0.1f, 0.2f, 0.f, 1.f, 0.4f, + // 0.6f, 0.1f, 1.0f, 0.5f, 0.92f, 0.1f, 0.2f, 0.f, 1.f, 0.4f, + // 0.6f, 0.1f, 1.0f, 0.5f, 0.92f, 0.1f, 0.2f, 0.f, 1.f, 0.4f, + // 0.6f, 0.1f, 1.0f, 0.5f, 0.92f, 0.1f, 0.2f, 0.f, 1.f, 0.4f }; + MediaPlayer *mp_ = nullptr; + auto po = MediaPlayer::begin(); + if (po != MediaPlayer::end()) + mp_ = *po; + if (mp_) { +// duration = mp_->duration(); +// step = mp_->frameDuration(); +// t = mp_->position(); - static float arr[] = { 0.6f, 0.1f, 1.0f, 0.5f, 0.92f, 0.1f, 0.2f, 0.f, 1.f, 0.4f, - 0.6f, 0.1f, 1.0f, 0.5f, 0.92f, 0.1f, 0.2f, 0.f, 1.f, 0.4f, - 0.6f, 0.1f, 1.0f, 0.5f, 0.92f, 0.1f, 0.2f, 0.f, 1.f, 0.4f, - 0.6f, 0.1f, 1.0f, 0.5f, 0.92f, 0.1f, 0.2f, 0.f, 1.f, 0.4f, - 0.6f, 0.1f, 1.0f, 0.5f, 0.92f, 0.1f, 0.2f, 0.f, 1.f, 0.4f, - 0.6f, 0.1f, 1.0f, 0.5f, 0.92f, 0.1f, 0.2f, 0.f, 1.f, 0.4f, - 0.6f, 0.1f, 1.0f, 0.5f, 0.92f, 0.1f, 0.2f, 0.f, 1.f, 0.4f, - 0.6f, 0.1f, 1.0f, 0.5f, 0.92f, 0.1f, 0.2f, 0.f, 1.f, 0.4f, - 0.6f, 0.1f, 1.0f, 0.5f, 0.92f, 0.1f, 0.2f, 0.f, 1.f, 0.4f, - 0.6f, 0.1f, 1.0f, 0.5f, 0.92f, 0.1f, 0.2f, 0.f, 1.f, 0.4f, - 0.6f, 0.1f, 1.0f, 0.5f, 0.92f, 0.1f, 0.2f, 0.f, 1.f, 0.4f, - 0.6f, 0.1f, 1.0f, 0.5f, 0.92f, 0.1f, 0.2f, 0.f, 1.f, 0.4f, - 0.6f, 0.1f, 1.0f, 0.5f, 0.92f, 0.1f, 0.2f, 0.f, 1.f, 0.4f, - 0.6f, 0.1f, 1.0f, 0.5f, 0.92f, 0.1f, 0.2f, 0.f, 1.f, 0.4f, - 0.6f, 0.1f, 1.0f, 0.5f, 0.92f, 0.1f, 0.2f, 0.f, 1.f, 0.4f, - 0.6f, 0.1f, 1.0f, 0.5f, 0.92f, 0.1f, 0.2f, 0.f, 1.f, 0.4f, - 0.6f, 0.1f, 1.0f, 0.5f, 0.92f, 0.1f, 0.2f, 0.f, 1.f, 0.4f, - 0.6f, 0.1f, 1.0f, 0.5f, 0.92f, 0.1f, 0.2f, 0.f, 1.f, 0.4f, - 0.6f, 0.1f, 1.0f, 0.5f, 0.92f, 0.1f, 0.2f, 0.f, 1.f, 0.4f, - 0.6f, 0.1f, 1.0f, 0.5f, 0.92f, 0.1f, 0.2f, 0.f, 1.f, 0.4f, - 0.6f, 0.1f, 1.0f, 0.5f, 0.92f, 0.1f, 0.2f, 0.f, 1.f, 0.4f }; +// arr = mp_->timelineArray(); +// array_size = mp_->timelineArraySize(); - ImVec2 size = ImGui::CalcItemSize(ImVec2(-FLT_MIN, 0.0f), ImGui::CalcItemWidth(), 20); +// if (arr == nullptr) { +// array_size = MIN( SEGMENT_ARRAY_MAX, (size_t) duration / (size_t) step); - // draw position when entering - ImVec2 draw_pos = ImGui::GetCursorPos(); - // plot the histogram - uint press_index = IM_ARRAYSIZE(arr); - bool pressed = hoverer("test", &press_index, 0, IM_ARRAYSIZE(arr)-1, size); +// arr = (float *) malloc(array_size * sizeof(float)); - static bool active = false; - static float target_value = 0.f; - static uint starting_index = IM_ARRAYSIZE(arr); +// for (int i = 0; i < array_size; ++i) { +// arr[i] = 1.f; +// } +// } - if (pressed != active) { - active = pressed; - starting_index = press_index; - target_value = arr[starting_index] > 0.f ? 0.f : 1.f; } - if (active) { - for (int i = MIN(starting_index, press_index); i < MAX(starting_index, press_index); ++i) - arr[i] = target_value; + if (arr != nullptr) + { + + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(1, 1)); + ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 1.f); + + ImVec2 size = ImGui::CalcItemSize(ImVec2(-FLT_MIN, 0.0f), ImGui::CalcItemWidth(), 20); + + // draw position when entering + ImVec2 draw_pos = ImGui::GetCursorPos(); + // plot the histogram + uint press_index = IM_ARRAYSIZE(arr); + bool pressed = ImGuiToolkit::InvisibleSliderInt("test", &press_index, 0, array_size-1, size); + + static bool active = false; + static float target_value = 0.f; + static uint starting_index = array_size; + + if (pressed != active) { + active = pressed; + starting_index = press_index; + target_value = arr[starting_index] > 0.f ? 0.f : 1.f; + } + + if (active) { + for (int i = MIN(starting_index, press_index); i < MAX(starting_index, press_index); ++i) + arr[i] = target_value; + } + + // back to + ImGui::SetCursorPos(draw_pos); + ImGui::PlotHistogram("Histogram", arr, array_size-1, 0, NULL, 0.0f, 1.0f, size); + + // ImGui::PopStyleColor(1); + + bool slider_pressed = ImGuiToolkit::TimelineSlider("timeline", &t, duration, step, size.x); + + ImGui::PopStyleVar(2); + + ImGui::Text("Timeline t %" GST_STIME_FORMAT "\n", GST_STIME_ARGS(t)); + ImGui::Text("Timeline Pressed %s", slider_pressed ? "on" : "off"); + ImGui::Text("Hover Pressed %s v = %d", pressed ? "on" : "off", press_index); + + static int w = 0; + ImGui::SetNextItemWidth(size.x); + ImGui::SliderInt("##int", &w, 0, array_size-1); + } - - - // back to - ImGui::SetCursorPos(draw_pos); -// ImGui::PlotLines("Lines", arr, IM_ARRAYSIZE(arr), 0, NULL, 0.0f, 1.0f, size); - -// ImVec4* colors = ImGui::GetStyle().Colors; -// ImGui::PushStyleColor(ImGuiCol_PlotHistogram, colors[ImGuiCol_Tab]); - - ImGui::PlotHistogram("Histogram", arr, IM_ARRAYSIZE(arr)-1, 0, NULL, 0.0f, 1.0f, size); - -// ImGui::PopStyleColor(1); - - ImGui::Text("Timeline t %" GST_STIME_FORMAT "\n", GST_STIME_ARGS(t)); - ImGui::Text("Timeline Pressed %s", slider_pressed ? "on" : "off"); - ImGui::Text("Hover Pressed %s v = %d", pressed ? "on" : "off", press_index); - - static int w = 0; - ImGui::SetNextItemWidth(size.x); - ImGui::SliderInt("##int", &w, 0, IM_ARRAYSIZE(arr)-1); - ImGui::End(); } diff --git a/defines.h b/defines.h index 003a8df..54750da 100644 --- a/defines.h +++ b/defines.h @@ -44,11 +44,12 @@ #define IMGUI_TITLE_MAINWINDOW ICON_FA_CIRCLE_NOTCH " vimix" #define IMGUI_TITLE_MEDIAPLAYER ICON_FA_FILM " Player" #define IMGUI_TITLE_TOOLBOX ICON_FA_WRENCH " Development Tools" -#define IMGUI_TITLE_SHADEREDITOR ICON_FA_CODE " Shader Editor" +#define IMGUI_TITLE_SHADEREDITOR ICON_FA_CODE " Code" #define IMGUI_TITLE_PREVIEW ICON_FA_DESKTOP " Ouput" #define IMGUI_TITLE_DELETE ICON_FA_BROOM " Delete?" #define IMGUI_LABEL_RECENT_FILES " Recent files" #define IMGUI_RIGHT_ALIGN -3.5f * ImGui::GetTextLineHeightWithSpacing() +#define IMGUI_COLOR_OVERLAY IM_COL32(5, 5, 5, 150) #define IMGUI_NOTIFICATION_DURATION 1.5f #ifdef APPLE #define CTRL_MOD "Cmd+"