From 3d702ceaced7e5dcf5a3c3a402452b42f1ae67db Mon Sep 17 00:00:00 2001 From: f0e <7321764+f0e@users.noreply.github.com> Date: Sun, 31 Aug 2025 19:11:04 +1000 Subject: [PATCH 001/181] feat: only show queue notifications when not on main screen again --- src/gui/tasks.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/gui/tasks.cpp b/src/gui/tasks.cpp index 54d37b88..7a563611 100644 --- a/src/gui/tasks.cpp +++ b/src/gui/tasks.cpp @@ -109,9 +109,11 @@ void tasks::add_files(const std::vector& path_strs) { u::log("queueing {}", path); - gui::components::notifications::add( - std::format("Queued '{}' for rendering", path.stem()), ui::NotificationType::INFO - ); + if (gui::renderer::screen != gui::renderer::Screens::MAIN) { + gui::components::notifications::add( + std::format("Queued '{}' for rendering", path.filename()), ui::NotificationType::INFO + ); + } pending_video_paths.push_back(path); } From 28cd6f330295b8292242e2798c5c009ca0913fd0 Mon Sep 17 00:00:00 2001 From: f0e <7321764+f0e@users.noreply.github.com> Date: Thu, 29 May 2025 01:23:07 +1000 Subject: [PATCH 002/181] wip: video element --- .gitmodules | 3 + CMakeLists.txt | 7 ++ src/gui/components/main.cpp | 7 ++ src/gui/render/render.cpp | 28 ++----- src/gui/render/render.h | 12 +-- src/gui/renderer.cpp | 4 + src/gui/ui/elements/image.cpp | 11 +-- src/gui/ui/elements/slider.cpp | 9 ++- src/gui/ui/elements/text.cpp | 1 + src/gui/ui/elements/video.cpp | 125 +++++++++++++++++++++++++++++ src/gui/ui/helpers/video.cpp | 142 +++++++++++++++++++++++++++++++++ src/gui/ui/helpers/video.h | 47 +++++++++++ src/gui/ui/keys.cpp | 2 + src/gui/ui/ui.cpp | 8 +- src/gui/ui/ui.h | 27 ++++++- 15 files changed, 391 insertions(+), 42 deletions(-) create mode 100644 src/gui/ui/elements/video.cpp create mode 100644 src/gui/ui/helpers/video.cpp create mode 100644 src/gui/ui/helpers/video.h diff --git a/.gitmodules b/.gitmodules index e79f10df..10aeaf1d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,9 @@ [submodule "dependencies/imgui"] path = dependencies/imgui url = https://github.com/ocornut/imgui +[submodule "dependencies/mpv"] + path = dependencies/mpv + url = https://github.com/mpv-player/mpv [submodule "dependencies/stb"] path = dependencies/stb url = https://github.com/nothings/stb diff --git a/CMakeLists.txt b/CMakeLists.txt index 5efb79d5..2787e925 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -145,6 +145,13 @@ target_compile_definitions(blur PRIVATE IMGUI_IMPL_OPENGL_LOADER_CUSTOM) # stb target_include_directories(blur PRIVATE ${PROJECT_SOURCE_DIR}/dependencies/stb) +# mpv +find_package(PkgConfig REQUIRED) +pkg_search_module(MPV REQUIRED mpv>=0.33.0) +target_include_directories(blur PRIVATE ${MPV_INCLUDE_DIRS}) +target_link_libraries(blur PRIVATE ${MPV_LIBRARIES}) +target_link_directories(blur PRIVATE ${MPV_LIBRARY_DIRS}) + target_link_libraries( blur PRIVATE blur-common SDL3::SDL3 SDL3_image::SDL3_image Freetype::Freetype glad::glad) diff --git a/src/gui/components/main.cpp b/src/gui/components/main.cpp index 7e9c02f2..4ee9fced 100644 --- a/src/gui/components/main.cpp +++ b/src/gui/components/main.cpp @@ -220,6 +220,13 @@ void main::home_screen(ui::Container& container, float delta_time) { "blur title text", container, title_pos, "blur", gfx::Color::white(), fonts::header_font, FONT_CENTERED_X ); + ui::add_video( + "test video", + container, + "test.mp4", + gfx::Size(container.get_usable_rect().w, container.get_usable_rect().h / 2) + ); + if (!initialisation_res) { ui::add_text( "failed to initialise text", diff --git a/src/gui/render/render.cpp b/src/gui/render/render.cpp index d50b3b02..da230309 100644 --- a/src/gui/render/render.cpp +++ b/src/gui/render/render.cpp @@ -4,11 +4,11 @@ #include #include #include +#include #include "../fonts/dejavu_sans.h" #include "../fonts/eb_garamond.h" #include "../fonts/icons.h" -#include "imgui_internal.h" namespace { gfx::Color interpolate_color(const std::vector& colors, const std::vector& positions, float t) { @@ -166,8 +166,8 @@ void render::ImGuiWrap::end(SDL_Window* window) { // NOLINT(readability-convert- clear_colour.w ); glClear(GL_COLOR_BUFFER_BIT); + ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); - SDL_GL_SwapWindow(window); } void render::line( @@ -609,24 +609,6 @@ void render::image(const gfx::Rect& rect, const Texture& texture, const gfx::Col ); } -void render::image_with_borders( - const gfx::Rect& rect, - const Texture& texture, - const gfx::Color& border_color, - const gfx::Color& inner_border_color, - float border_thickness, - const gfx::Color& tint_color -) { - if (!texture.is_valid()) - return; - - image(rect.shrink(3), texture, tint_color); - - rect_stroke(rect.shrink(2), border_color, border_thickness); - rect_stroke(rect.shrink(1), inner_border_color, border_thickness); - rect_stroke(rect, border_color, border_thickness); -} - void render::image_rounded( const gfx::Rect& rect, const Texture& texture, @@ -669,6 +651,12 @@ void render::rounded_image_with_borders( rounded_rect_stroke(rect, border_color, rounding, rounding_flags, border_thickness); } +void render::borders(const gfx::Rect& rect, const gfx::Color& border_color, const gfx::Color& inner_border_color) { + rect_stroke(rect.shrink(2), border_color, 1.f); + rect_stroke(rect.shrink(1), inner_border_color, 1.f); + rect_stroke(rect, border_color, 1.f); +} + void render::push_clip_rect(const gfx::Rect& rect, bool intersect_clip_rect) { imgui.drawlist->PushClipRect(rect.origin(), rect.max(), intersect_clip_rect); } diff --git a/src/gui/render/render.h b/src/gui/render/render.h index 6a6b69c2..75b52f2e 100644 --- a/src/gui/render/render.h +++ b/src/gui/render/render.h @@ -216,18 +216,8 @@ namespace render { int rotation_pivot_y = 0 ); - // New image functions void image(const gfx::Rect& rect, const Texture& texture, const gfx::Color& tint_color = gfx::Color::white()); - void image_with_borders( - const gfx::Rect& rect, - const Texture& texture, - const gfx::Color& border_color, - const gfx::Color& inner_border_color, - float border_thickness = 1.0f, - const gfx::Color& tint_color = gfx::Color::white() - ); - void image_rounded( const gfx::Rect& rect, const Texture& texture, @@ -247,6 +237,8 @@ namespace render { const gfx::Color& tint_color = gfx::Color::white() ); + void borders(const gfx::Rect& rect, const gfx::Color& border_color, const gfx::Color& inner_border_color); + void push_clip_rect(const gfx::Rect& rect, bool intersect_clip_rect = false); void push_clip_rect(int x1, int y1, int x2, int y2, bool intersect_clip_rect = false); void push_fullscreen_clip_rect(); diff --git a/src/gui/renderer.cpp b/src/gui/renderer.cpp index f6f21267..d615673c 100644 --- a/src/gui/renderer.cpp +++ b/src/gui/renderer.cpp @@ -277,6 +277,10 @@ bool gui::renderer::redraw_window(bool rendered_last, bool want_to_render) { } render::imgui.end(sdl::window); + ui::render_videos(); + + SDL_GL_SwapWindow(sdl::window); + ui::on_frame_end(); return true; diff --git a/src/gui/ui/elements/image.cpp b/src/gui/ui/elements/image.cpp index 1be75d0c..93d07f17 100644 --- a/src/gui/ui/elements/image.cpp +++ b/src/gui/ui/elements/image.cpp @@ -17,13 +17,10 @@ void ui::render_image(const Container& container, const AnimatedElement& element gfx::Color tint_color = image_data.image_color.adjust_alpha(anim); - render::image_with_borders( - element.element->rect, - *image_data.texture, - gfx::Color(155, 155, 155, stroke_alpha), - gfx::Color(80, 80, 80, stroke_alpha), - 1.0f, - tint_color + render::image(element.element->rect.shrink(3), *image_data.texture, tint_color); + + render::borders( + element.element->rect, gfx::Color(155, 155, 155, stroke_alpha), gfx::Color(80, 80, 80, stroke_alpha) ); } diff --git a/src/gui/ui/elements/slider.cpp b/src/gui/ui/elements/slider.cpp index 86a88636..bba7aacb 100644 --- a/src/gui/ui/elements/slider.cpp +++ b/src/gui/ui/elements/slider.cpp @@ -487,6 +487,12 @@ bool ui::update_slider(const Container& container, AnimatedElement& element) { return false; } +void ui::remove_slider(AnimatedElement& element) { + if (slider_observers.contains(element.element->id)) { + slider_observers.erase(element.element->id); + } +} + ui::AnimatedElement* ui::add_slider( const std::string& id, Container& container, @@ -573,7 +579,8 @@ ui::AnimatedElement* ui::add_slider_tied( .tied_value = tied_value, .tied_text = tied_text }, render_slider, - update_slider + update_slider, + remove_slider ); return add_element( diff --git a/src/gui/ui/elements/text.cpp b/src/gui/ui/elements/text.cpp index 044e486e..7aaf1159 100644 --- a/src/gui/ui/elements/text.cpp +++ b/src/gui/ui/elements/text.cpp @@ -133,6 +133,7 @@ ui::AnimatedElement* ui::add_text_fixed( }, render_text, {}, + {}, true ); diff --git a/src/gui/ui/elements/video.cpp b/src/gui/ui/elements/video.cpp new file mode 100644 index 00000000..26c8b6c1 --- /dev/null +++ b/src/gui/ui/elements/video.cpp @@ -0,0 +1,125 @@ +#include "../ui.h" +#include "../../render/render.h" +#include "../keys.h" +#include "../helpers/video.h" + +namespace { + std::unordered_map> video_players; +} + +void ui::render_videos() { + for (auto& [id, player] : video_players) { + if (player) { + player->render(100, 100); + } + } +} + +void ui::render_video(const Container& container, const AnimatedElement& element) { + const auto& video_data = std::get(element.element->data); + float anim = element.animations.at(hasher("main")).current; + + int alpha = anim * 255; + int stroke_alpha = anim * 125; + + render::borders( + element.element->rect, gfx::Color(155, 155, 155, stroke_alpha), gfx::Color(80, 80, 80, stroke_alpha) + ); +} + +bool ui::update_video(const Container& container, AnimatedElement& element) { + const auto& video_data = std::get(element.element->data); + + auto video_player_it = video_players.find(video_data.video_path.string()); + if (video_player_it != video_players.end()) { + auto& video_player = video_player_it->second; + + bool hovered = element.element->rect.contains(keys::mouse_pos) && set_hovered_element(element); + + if (hovered) { + set_cursor(SDL_SYSTEM_CURSOR_POINTER); + + while (!event_queue.empty()) { + auto& event = event_queue.front(); + + bool blah; + video_player->handle_mpv_event(event, blah); + + event_queue.erase(event_queue.begin()); + } + } + } + + return false; +} + +void ui::remove_video(AnimatedElement& element) { + const auto& video_data = std::get(element.element->data); + auto video_player_it = video_players.find(video_data.video_path.string()); + if (video_player_it != video_players.end()) { + video_players.erase(video_player_it); + u::log("Removed video player for {}", video_data.video_path.string()); + } + else { + u::log("No video player found for {}", video_data.video_path.string()); + } +} + +std::optional ui::add_video( + const std::string& id, Container& container, const std::filesystem::path& video_path, const gfx::Size& max_size +) { + VideoPlayer* player = nullptr; + + auto video_player_it = video_players.find(video_path.string()); + if (video_player_it == video_players.end()) { + try { + auto player_ref = video_players.emplace(video_path.string(), std::make_unique()); + player = player_ref.first->second.get(); + player->load_file(video_path.string().c_str()); + u::log_error("{} loaded video from {}", id, video_path.string()); + } + catch (const std::exception& e) { + u::log_error("{} failed to load video from {} ({})", id, video_path.string(), e.what()); + return {}; + } + } + + float aspect_ratio = 16.0f / 9.0f; // todo: proper aspect ratio + + gfx::Rect video_rect(container.current_position, max_size); + + // maintain aspect ratio + float target_width = video_rect.h * aspect_ratio; + float target_height = video_rect.w / aspect_ratio; + + if (target_width <= video_rect.w) { + video_rect.w = static_cast(target_width); + } + else { + video_rect.h = static_cast(target_height); + } + + if (video_rect.h > max_size.h) { + video_rect.h = max_size.h; + video_rect.w = static_cast(max_size.h * aspect_ratio); + } + + if (video_rect.w > max_size.w) { + video_rect.w = max_size.w; + video_rect.h = static_cast(max_size.w / aspect_ratio); + } + + Element element( + id, + ElementType::VIDEO, + video_rect, + VideoElementData{ + .video_path = video_path, + }, + render_video, + update_video, + remove_video + ); + + return add_element(container, std::move(element), container.element_gap); +} diff --git a/src/gui/ui/helpers/video.cpp b/src/gui/ui/helpers/video.cpp new file mode 100644 index 00000000..84513627 --- /dev/null +++ b/src/gui/ui/helpers/video.cpp @@ -0,0 +1,142 @@ +#include "video.h" + +VideoPlayer::~VideoPlayer() { + if (m_mpv_gl) { + mpv_render_context_free(m_mpv_gl); + } + + if (m_mpv) { + mpv_destroy(m_mpv); + } + + u::log("Player properly terminated"); +} + +void VideoPlayer::handle_key_press(SDL_Keycode key) { + if (key == SDLK_SPACE) { + static std::array cmd = { "cycle", "pause", nullptr }; + mpv_command_async(m_mpv, 0, cmd.data()); + } +} + +void VideoPlayer::load_file(const char* file_path) { + static std::array cmd = { "loadfile", file_path, nullptr }; + mpv_command_async(m_mpv, 0, cmd.data()); +} + +void VideoPlayer::render(int w, int h) { + mpv_opengl_fbo fbo{ + .fbo = 0, + .w = w, + .h = h, + .internal_format = 0, + }; + + int flip_y = 1; + + std::vector params{ { .type = MPV_RENDER_PARAM_OPENGL_FBO, .data = &fbo }, + { .type = MPV_RENDER_PARAM_FLIP_Y, .data = &flip_y }, + { .type = MPV_RENDER_PARAM_INVALID } }; + + mpv_render_context_render(m_mpv_gl, params.data()); +} + +void VideoPlayer::handle_mpv_event(const SDL_Event& event, bool& redraw) { + if (event.type == m_wakeup_on_mpv_render_update) { + uint64_t flags = mpv_render_context_update(m_mpv_gl); + if (flags & MPV_RENDER_UPDATE_FRAME) { + redraw = true; + } + } + + if (event.type == m_wakeup_on_mpv_events) { + process_mpv_events(); + } +} + +void VideoPlayer::initialize_mpv() { + m_mpv = mpv_create(); + if (!m_mpv) { + throw std::runtime_error("MPV context creation failed"); + } + + // configure mpv + mpv_set_option_string(m_mpv, "vo", "libmpv"); + + if (mpv_initialize(m_mpv) < 0) { + throw std::runtime_error("MPV initialization failed"); + } + + mpv_request_log_messages(m_mpv, "debug"); + + // set up callbacks + mpv_set_wakeup_callback( + m_mpv, + [](void* data) { + auto* player = static_cast(data); + player->on_mpv_events(); + }, + this + ); + + mpv_opengl_init_params init_params{ + .get_proc_address = [](void* ctx, const char* name) -> void* { + return (void*)SDL_GL_GetProcAddress(name); + }, + }; + + int advanced_control = 1; + + std::vector params{ { .type = MPV_RENDER_PARAM_API_TYPE, + .data = (char*)(MPV_RENDER_API_TYPE_OPENGL) }, + { .type = MPV_RENDER_PARAM_OPENGL_INIT_PARAMS, .data = &init_params }, + { .type = MPV_RENDER_PARAM_ADVANCED_CONTROL, .data = &advanced_control }, + { .type = MPV_RENDER_PARAM_INVALID } }; + + // create mpv render context + if (mpv_render_context_create(&m_mpv_gl, m_mpv, params.data()) < 0) { + throw std::runtime_error("Failed to initialize MPV GL context"); + } + + // set up render update callback + mpv_render_context_set_update_callback( + m_mpv_gl, + [](void* data) { + auto* player = static_cast(data); + player->on_mpv_render_update(); + }, + this + ); +} + +void VideoPlayer::on_mpv_events() { + SDL_Event event = { .type = m_wakeup_on_mpv_events }; + SDL_PushEvent(&event); +} + +void VideoPlayer::on_mpv_render_update() { + SDL_Event event = { .type = m_wakeup_on_mpv_render_update }; + SDL_PushEvent(&event); +} + +void VideoPlayer::process_mpv_events() { + // handle all pending mpv events + while (true) { + mpv_event* mp_event = mpv_wait_event(m_mpv, 0); + + if (mp_event->event_id == MPV_EVENT_NONE) { + break; + } + + if (mp_event->event_id == MPV_EVENT_LOG_MESSAGE) { + auto* msg = static_cast(mp_event->data); + // print specific debug log messages + if (std::strstr(msg->text, "DR image")) { + u::log("Log: {}", msg->text); + } + continue; + } + + u::log("Event: ", mpv_event_name(mp_event->event_id)); + } +} diff --git a/src/gui/ui/helpers/video.h b/src/gui/ui/helpers/video.h new file mode 100644 index 00000000..93994485 --- /dev/null +++ b/src/gui/ui/helpers/video.h @@ -0,0 +1,47 @@ +#pragma once + +#include +#include + +class VideoPlayer { +public: + VideoPlayer() + : m_wakeup_on_mpv_render_update(SDL_RegisterEvents(1)), m_wakeup_on_mpv_events(SDL_RegisterEvents(1)) { + if (m_wakeup_on_mpv_render_update == static_cast(-1) || + m_wakeup_on_mpv_events == static_cast(-1)) + { + throw std::runtime_error("Could not register SDL events"); + } + + initialize_mpv(); + } + + VideoPlayer(const VideoPlayer&) = default; + VideoPlayer(VideoPlayer&&) = delete; + VideoPlayer& operator=(const VideoPlayer&) = default; + VideoPlayer& operator=(VideoPlayer&&) = delete; + + ~VideoPlayer(); + + void handle_key_press(SDL_Keycode key); + + void load_file(const char* file_path); + + void render(int w, int h); + + void handle_mpv_event(const SDL_Event& event, bool& redraw); + +private: + mpv_handle* m_mpv = nullptr; + mpv_render_context* m_mpv_gl = nullptr; + Uint32 m_wakeup_on_mpv_render_update; + Uint32 m_wakeup_on_mpv_events; + + void initialize_mpv(); + + void on_mpv_events(); + + void on_mpv_render_update(); + + void process_mpv_events(); +}; diff --git a/src/gui/ui/keys.cpp b/src/gui/ui/keys.cpp index 374c55cf..8320da7f 100644 --- a/src/gui/ui/keys.cpp +++ b/src/gui/ui/keys.cpp @@ -18,6 +18,8 @@ bool keys::process_event(const SDL_Event& event) { } } + ui::event_queue.push_back(event); + switch (event.type) { case SDL_EVENT_WINDOW_MOUSE_LEAVE: { mouse_pos = { -1, -1 }; diff --git a/src/gui/ui/ui.cpp b/src/gui/ui/ui.cpp index 5fe63540..6ab249ec 100644 --- a/src/gui/ui/ui.cpp +++ b/src/gui/ui/ui.cpp @@ -330,6 +330,8 @@ void ui::on_update_input_end() { // empty text events if they werent processed for some reason text_event_queue.clear(); + event_queue.clear(); + // set cursor based on if an element wanted pointer sdl::set_cursor(desired_cursor); desired_cursor = SDL_SYSTEM_CURSOR_DEFAULT; @@ -400,9 +402,9 @@ bool ui::update_container_frame(Container& container, float delta_time) { need_to_render_animation_update |= animation.update(delta_time); } - if (stale && main_animation.complete) { - // animation complete and element stale, remove - slider_observers.erase(id); + if (stale && main_animation.complete) { // animation complete and element stale, remove + if (element.element->remove_fn) + (*element.element->remove_fn)(element); u::log("removed {}", id); it = container.elements.erase(it); diff --git a/src/gui/ui/ui.h b/src/gui/ui/ui.h index 8c7a1c41..1fddde61 100644 --- a/src/gui/ui/ui.h +++ b/src/gui/ui/ui.h @@ -23,6 +23,7 @@ namespace ui { BAR, TEXT, IMAGE, + VIDEO, BUTTON, NOTIFICATION, SLIDER, @@ -129,6 +130,14 @@ namespace ui { } }; + struct VideoElementData { + std::filesystem::path video_path; + + bool operator==(const VideoElementData& other) const { + return video_path == other.video_path; + } + }; + struct ButtonElementData { std::string text; const render::Font* font; @@ -264,6 +273,7 @@ namespace ui { BarElementData, TextElementData, ImageElementData, + VideoElementData, ButtonElementData, NotificationElementData, SliderElementData, @@ -313,6 +323,7 @@ namespace ui { ElementData data; std::function render_fn; std::optional> update_fn; + std::optional> remove_fn; bool fixed = false; gfx::Rect orig_rect; @@ -323,10 +334,11 @@ namespace ui { ElementData data, std::function render_fn, std::optional> update_fn = std::nullopt, + std::optional> remove_fn = std::nullopt, bool fixed = false ) : id(std::move(id)), type(type), rect(rect), data(std::move(data)), render_fn(std::move(render_fn)), - update_fn(std::move(update_fn)), fixed(fixed), orig_rect(rect) {} + update_fn(std::move(update_fn)), remove_fn(std::move(remove_fn)), fixed(fixed), orig_rect(rect) {} bool update(const Element& other) { this->id = other.id; @@ -334,6 +346,7 @@ namespace ui { this->rect = other.rect; this->render_fn = other.render_fn; this->update_fn = other.update_fn; + this->remove_fn = other.remove_fn; this->fixed = other.fixed; this->orig_rect = other.orig_rect; @@ -400,6 +413,7 @@ namespace ui { inline auto hasher = std::hash{}; inline std::vector text_event_queue; + inline std::vector event_queue; struct SliderObserver { bool init = false; @@ -417,6 +431,12 @@ namespace ui { void render_image(const Container& container, const AnimatedElement& element); + void render_video(const Container& container, const AnimatedElement& element); + bool update_video(const Container& container, AnimatedElement& element); + void remove_video(AnimatedElement& element); + + void render_videos(); + void render_button(const Container& container, const AnimatedElement& element); bool update_button(const Container& container, AnimatedElement& element); @@ -425,6 +445,7 @@ namespace ui { void render_slider(const Container& container, const AnimatedElement& element); bool update_slider(const Container& container, AnimatedElement& element); + void remove_slider(AnimatedElement& element); void render_text_input(const Container& container, const AnimatedElement& element); bool update_text_input(const Container& container, AnimatedElement& element); @@ -526,6 +547,10 @@ namespace ui { gfx::Color image_color = gfx::Color::white() ); // use image_id to distinguish images that have the same filename and reload it (e.g. if its updated) + std::optional add_video( + const std::string& id, Container& container, const std::filesystem::path& video_path, const gfx::Size& max_size + ); + AnimatedElement* add_button( const std::string& id, Container& container, From c9b018f3798876ab6e22de590233edf497ee864f Mon Sep 17 00:00:00 2001 From: f0e <7321764+f0e@users.noreply.github.com> Date: Tue, 26 Aug 2025 21:35:16 +1000 Subject: [PATCH 003/181] feat: video event handling --- src/gui/gui.cpp | 2 ++ src/gui/ui/elements/video.cpp | 16 ++++++++++++++++ src/gui/ui/ui.h | 1 + 3 files changed, 19 insertions(+) diff --git a/src/gui/gui.cpp b/src/gui/gui.cpp index be016efe..78229a50 100644 --- a/src/gui/gui.cpp +++ b/src/gui/gui.cpp @@ -72,6 +72,8 @@ int gui::run() { break; } + ui::handle_videos_event(event, to_render); + if (keys::process_event(event)) { ui::on_update_input_start(); diff --git a/src/gui/ui/elements/video.cpp b/src/gui/ui/elements/video.cpp index 26c8b6c1..64a73e59 100644 --- a/src/gui/ui/elements/video.cpp +++ b/src/gui/ui/elements/video.cpp @@ -15,6 +15,22 @@ void ui::render_videos() { } } +void ui::handle_videos_event(const SDL_Event& event, bool& to_render) { + for (auto& [id, player] : video_players) { + if (player) { + switch (event.type) { + case SDL_EVENT_KEY_DOWN: + player->handle_key_press(event.key.key); + break; + + default: + player->handle_mpv_event(event, to_render); + break; + } + } + } +} + void ui::render_video(const Container& container, const AnimatedElement& element) { const auto& video_data = std::get(element.element->data); float anim = element.animations.at(hasher("main")).current; diff --git a/src/gui/ui/ui.h b/src/gui/ui/ui.h index 1fddde61..7f669a49 100644 --- a/src/gui/ui/ui.h +++ b/src/gui/ui/ui.h @@ -436,6 +436,7 @@ namespace ui { void remove_video(AnimatedElement& element); void render_videos(); + void handle_videos_event(const SDL_Event& event, bool& to_render); void render_button(const Container& container, const AnimatedElement& element); bool update_button(const Container& container, AnimatedElement& element); From a48859603af81ac54bed8d3b6d275971691bb88e Mon Sep 17 00:00:00 2001 From: f0e <7321764+f0e@users.noreply.github.com> Date: Tue, 26 Aug 2025 21:35:41 +1000 Subject: [PATCH 004/181] fix: log type --- src/gui/ui/elements/video.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gui/ui/elements/video.cpp b/src/gui/ui/elements/video.cpp index 64a73e59..2ccc1236 100644 --- a/src/gui/ui/elements/video.cpp +++ b/src/gui/ui/elements/video.cpp @@ -92,7 +92,7 @@ std::optional ui::add_video( auto player_ref = video_players.emplace(video_path.string(), std::make_unique()); player = player_ref.first->second.get(); player->load_file(video_path.string().c_str()); - u::log_error("{} loaded video from {}", id, video_path.string()); + u::log("{} loaded video from {}", id, video_path.string()); } catch (const std::exception& e) { u::log_error("{} failed to load video from {} ({})", id, video_path.string(), e.what()); From 1901bb055507b9375ca3c311d6d486d2aa518caa Mon Sep 17 00:00:00 2001 From: f0e <7321764+f0e@users.noreply.github.com> Date: Tue, 26 Aug 2025 21:35:57 +1000 Subject: [PATCH 005/181] chore: add note from libmpv --- src/gui/ui/helpers/video.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/gui/ui/helpers/video.cpp b/src/gui/ui/helpers/video.cpp index 84513627..34291461 100644 --- a/src/gui/ui/helpers/video.cpp +++ b/src/gui/ui/helpers/video.cpp @@ -85,6 +85,15 @@ void VideoPlayer::initialize_mpv() { }, }; + // Tell libmpv that you will call mpv_render_context_update() on render + // context update callbacks, and that you will _not_ block on the core + // ever (see "Threading" section for what libmpv + // functions you can call at all when this is active). + // In particular, this means you must call e.g. mpv_command_async() + // instead of mpv_command(). + // If you want to use synchronous calls, either make them on a separate + // thread, or remove the option below (this will disable features like + // DR and is not recommended anyway). int advanced_control = 1; std::vector params{ { .type = MPV_RENDER_PARAM_API_TYPE, From 58aee8035bddc1cfc86e1649175e7f8d6260a3fe Mon Sep 17 00:00:00 2001 From: newgan Date: Sun, 31 Aug 2025 01:44:30 -0400 Subject: [PATCH 006/181] style: add_files->add_files_for_render --- src/gui/components/main.cpp | 2 +- src/gui/gui.cpp | 2 +- src/gui/tasks.cpp | 4 ++-- src/gui/tasks.h | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/gui/components/main.cpp b/src/gui/components/main.cpp index 4ee9fced..2735200e 100644 --- a/src/gui/components/main.cpp +++ b/src/gui/components/main.cpp @@ -22,7 +22,7 @@ void main::open_files_button(ui::Container& container, const std::string& label) wpaths.emplace_back(u::string_to_path(*f)); } - tasks::add_files(wpaths); + tasks::add_files_for_render(wpaths); } }; diff --git a/src/gui/gui.cpp b/src/gui/gui.cpp index 78229a50..70c34ea0 100644 --- a/src/gui/gui.cpp +++ b/src/gui/gui.cpp @@ -63,7 +63,7 @@ int gui::run() { } } - tasks::add_files(paths); + tasks::add_files_for_render(paths); break; } diff --git a/src/gui/tasks.cpp b/src/gui/tasks.cpp index 7a563611..945cd63e 100644 --- a/src/gui/tasks.cpp +++ b/src/gui/tasks.cpp @@ -78,7 +78,7 @@ void tasks::run(const std::vector& arguments) { paths.emplace_back(u::string_to_path(argument)); } - add_files(paths); // todo: mac packaged app support (& linux? does it work?) + add_files_for_render(paths); // todo: mac packaged app support (& linux? does it work?) std::thread([] { while (!blur.exiting) { @@ -99,7 +99,7 @@ void tasks::run(const std::vector& arguments) { } } -void tasks::add_files(const std::vector& path_strs) { +void tasks::add_files_for_render(const std::vector& path_strs) { std::lock_guard lock(pending_video_paths_mutex); for (const auto& path_str : path_strs) { diff --git a/src/gui/tasks.h b/src/gui/tasks.h index 2fb3391a..31c50540 100644 --- a/src/gui/tasks.h +++ b/src/gui/tasks.h @@ -5,7 +5,7 @@ namespace tasks { void run(const std::vector& arguments); - void add_files(const std::vector& path_strs); + void add_files_for_render(const std::vector& path_strs); void add_sample_video(const std::filesystem::path& path_str); void process_pending_files(); } From 48d45a8c8a558d7351d15d647c0a0f4d8fbc60fd Mon Sep 17 00:00:00 2001 From: newgan Date: Sun, 31 Aug 2025 16:52:19 -0400 Subject: [PATCH 007/181] temporary(shit) setup for mpv --- src/gui/components/main.cpp | 4 ++-- src/gui/gui.cpp | 3 ++- src/gui/tasks.cpp | 5 +++++ src/gui/tasks.h | 2 ++ src/gui/ui/elements/video.cpp | 14 +++++++++++++- src/gui/ui/helpers/video.cpp | 17 ++++++++++++++--- src/gui/ui/helpers/video.h | 3 +++ src/gui/ui/ui.h | 2 ++ 8 files changed, 43 insertions(+), 7 deletions(-) diff --git a/src/gui/components/main.cpp b/src/gui/components/main.cpp index 2735200e..05f6433a 100644 --- a/src/gui/components/main.cpp +++ b/src/gui/components/main.cpp @@ -22,7 +22,7 @@ void main::open_files_button(ui::Container& container, const std::string& label) wpaths.emplace_back(u::string_to_path(*f)); } - tasks::add_files_for_render(wpaths); + tasks::set_video_player_path(wpaths[0]); } }; @@ -223,7 +223,7 @@ void main::home_screen(ui::Container& container, float delta_time) { ui::add_video( "test video", container, - "test.mp4", + tasks::video_player_path, gfx::Size(container.get_usable_rect().w, container.get_usable_rect().h / 2) ); diff --git a/src/gui/gui.cpp b/src/gui/gui.cpp index 70c34ea0..be96a843 100644 --- a/src/gui/gui.cpp +++ b/src/gui/gui.cpp @@ -63,7 +63,8 @@ int gui::run() { } } - tasks::add_files_for_render(paths); + // tasks::add_files_for_render(paths); + tasks::set_video_player_path(paths[0]); break; } diff --git a/src/gui/tasks.cpp b/src/gui/tasks.cpp index 945cd63e..ed6bc0c8 100644 --- a/src/gui/tasks.cpp +++ b/src/gui/tasks.cpp @@ -12,6 +12,7 @@ #include "components/configs/configs.h" namespace { + std::filesystem::path video_player_path; std::vector pending_video_paths; std::mutex pending_video_paths_mutex; } @@ -180,3 +181,7 @@ void tasks::add_sample_video(const std::filesystem::path& path_str) { gui::components::configs::just_added_sample_video = true; } + +void tasks::set_video_player_path(const std::filesystem::path& path_str) { + video_player_path = path_str; +} diff --git a/src/gui/tasks.h b/src/gui/tasks.h index 31c50540..c674be1e 100644 --- a/src/gui/tasks.h +++ b/src/gui/tasks.h @@ -2,10 +2,12 @@ namespace tasks { inline int finished_renders = 0; + inline std::filesystem::path video_player_path = ""; void run(const std::vector& arguments); void add_files_for_render(const std::vector& path_strs); void add_sample_video(const std::filesystem::path& path_str); void process_pending_files(); + void set_video_player_path(const std::filesystem::path& path_str); } diff --git a/src/gui/ui/elements/video.cpp b/src/gui/ui/elements/video.cpp index 2ccc1236..448da6d8 100644 --- a/src/gui/ui/elements/video.cpp +++ b/src/gui/ui/elements/video.cpp @@ -10,7 +10,7 @@ namespace { void ui::render_videos() { for (auto& [id, player] : video_players) { if (player) { - player->render(100, 100); + player->render(1000, 1000); } } } @@ -38,6 +38,13 @@ void ui::render_video(const Container& container, const AnimatedElement& element int alpha = anim * 255; int stroke_alpha = anim * 125; + render::imgui.drawlist->AddImage( + video_data.player->m_tex, + element.element->rect.origin(), + element.element->rect.max(), + ImVec2(0, 0), + ImVec2(1, 1) + ); render::borders( element.element->rect, gfx::Color(155, 155, 155, stroke_alpha), gfx::Color(80, 80, 80, stroke_alpha) ); @@ -84,9 +91,13 @@ void ui::remove_video(AnimatedElement& element) { std::optional ui::add_video( const std::string& id, Container& container, const std::filesystem::path& video_path, const gfx::Size& max_size ) { + if (video_path.empty()) { + return {}; + } VideoPlayer* player = nullptr; auto video_player_it = video_players.find(video_path.string()); + if (video_player_it == video_players.end()) { try { auto player_ref = video_players.emplace(video_path.string(), std::make_unique()); @@ -131,6 +142,7 @@ std::optional ui::add_video( video_rect, VideoElementData{ .video_path = video_path, + .player = player, }, render_video, update_video, diff --git a/src/gui/ui/helpers/video.cpp b/src/gui/ui/helpers/video.cpp index 34291461..e2137251 100644 --- a/src/gui/ui/helpers/video.cpp +++ b/src/gui/ui/helpers/video.cpp @@ -25,14 +25,25 @@ void VideoPlayer::load_file(const char* file_path) { } void VideoPlayer::render(int w, int h) { + glGenFramebuffers(1, &m_fbo); + glBindFramebuffer(GL_FRAMEBUFFER, m_fbo); + glGenTextures(1, &m_tex); + glBindTexture(GL_TEXTURE_2D, m_tex); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, w, h, 0, GL_RGB, GL_UNSIGNED_BYTE, nullptr); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_tex, 0); + glBindTexture(GL_TEXTURE_2D, 0); + glBindFramebuffer(GL_FRAMEBUFFER, 0); + mpv_opengl_fbo fbo{ - .fbo = 0, + .fbo = (int)m_fbo, .w = w, .h = h, - .internal_format = 0, + .internal_format = GL_RGB, }; - int flip_y = 1; + int flip_y = 0; std::vector params{ { .type = MPV_RENDER_PARAM_OPENGL_FBO, .data = &fbo }, { .type = MPV_RENDER_PARAM_FLIP_Y, .data = &flip_y }, diff --git a/src/gui/ui/helpers/video.h b/src/gui/ui/helpers/video.h index 93994485..fb5cd7bb 100644 --- a/src/gui/ui/helpers/video.h +++ b/src/gui/ui/helpers/video.h @@ -31,6 +31,9 @@ class VideoPlayer { void handle_mpv_event(const SDL_Event& event, bool& redraw); + GLuint m_fbo; + GLuint m_tex; + private: mpv_handle* m_mpv = nullptr; mpv_render_context* m_mpv_gl = nullptr; diff --git a/src/gui/ui/ui.h b/src/gui/ui/ui.h index 7f669a49..95555627 100644 --- a/src/gui/ui/ui.h +++ b/src/gui/ui/ui.h @@ -2,6 +2,7 @@ #include "../render/render.h" #include "helpers/text_input.h" +#include "helpers/video.h" namespace ui { inline size_t frame = 0; @@ -132,6 +133,7 @@ namespace ui { struct VideoElementData { std::filesystem::path video_path; + VideoPlayer* player; bool operator==(const VideoElementData& other) const { return video_path == other.video_path; From 61e62dd60ba14004e33f36e88202af93e5fd07c8 Mon Sep 17 00:00:00 2001 From: newgan Date: Mon, 1 Sep 2025 01:05:05 -0400 Subject: [PATCH 008/181] cleanup --- src/gui/ui/elements/video.cpp | 3 ++- src/gui/ui/helpers/video.cpp | 22 +++++++++++++++++++--- src/gui/ui/helpers/video.h | 15 ++++++++++++--- 3 files changed, 33 insertions(+), 7 deletions(-) diff --git a/src/gui/ui/elements/video.cpp b/src/gui/ui/elements/video.cpp index 448da6d8..fdd69774 100644 --- a/src/gui/ui/elements/video.cpp +++ b/src/gui/ui/elements/video.cpp @@ -39,12 +39,13 @@ void ui::render_video(const Container& container, const AnimatedElement& element int stroke_alpha = anim * 125; render::imgui.drawlist->AddImage( - video_data.player->m_tex, + video_data.player->get_frame_texture_for_render(), element.element->rect.origin(), element.element->rect.max(), ImVec2(0, 0), ImVec2(1, 1) ); + render::borders( element.element->rect, gfx::Color(155, 155, 155, stroke_alpha), gfx::Color(80, 80, 80, stroke_alpha) ); diff --git a/src/gui/ui/helpers/video.cpp b/src/gui/ui/helpers/video.cpp index e2137251..f31ec331 100644 --- a/src/gui/ui/helpers/video.cpp +++ b/src/gui/ui/helpers/video.cpp @@ -24,17 +24,33 @@ void VideoPlayer::load_file(const char* file_path) { mpv_command_async(m_mpv, 0, cmd.data()); } -void VideoPlayer::render(int w, int h) { +void VideoPlayer::gen_fbo_texture() { glGenFramebuffers(1, &m_fbo); glBindFramebuffer(GL_FRAMEBUFFER, m_fbo); + glGenTextures(1, &m_tex); glBindTexture(GL_TEXTURE_2D, m_tex); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, w, h, 0, GL_RGB, GL_UNSIGNED_BYTE, nullptr); - glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_tex, 0); + glBindTexture(GL_TEXTURE_2D, 0); glBindFramebuffer(GL_FRAMEBUFFER, 0); +} + +void VideoPlayer::setup_fbo_texture(int w, int h) const { + glBindFramebuffer(GL_FRAMEBUFFER, m_fbo); + glBindTexture(GL_TEXTURE_2D, m_tex); + + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_tex, 0); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, w, h, 0, GL_RGB, GL_UNSIGNED_BYTE, nullptr); + + glBindFramebuffer(GL_FRAMEBUFFER, 0); + glBindTexture(GL_TEXTURE_2D, 0); +} + +void VideoPlayer::render(int w, int h) { + setup_fbo_texture(w, h); mpv_opengl_fbo fbo{ .fbo = (int)m_fbo, diff --git a/src/gui/ui/helpers/video.h b/src/gui/ui/helpers/video.h index fb5cd7bb..988710fe 100644 --- a/src/gui/ui/helpers/video.h +++ b/src/gui/ui/helpers/video.h @@ -14,6 +14,7 @@ class VideoPlayer { } initialize_mpv(); + gen_fbo_texture(); } VideoPlayer(const VideoPlayer&) = default; @@ -29,10 +30,11 @@ class VideoPlayer { void render(int w, int h); - void handle_mpv_event(const SDL_Event& event, bool& redraw); + [[nodiscard]] GLuint get_frame_texture_for_render() const { + return m_tex; + } - GLuint m_fbo; - GLuint m_tex; + void handle_mpv_event(const SDL_Event& event, bool& redraw); private: mpv_handle* m_mpv = nullptr; @@ -40,6 +42,9 @@ class VideoPlayer { Uint32 m_wakeup_on_mpv_render_update; Uint32 m_wakeup_on_mpv_events; + GLuint m_fbo; + GLuint m_tex; + void initialize_mpv(); void on_mpv_events(); @@ -47,4 +52,8 @@ class VideoPlayer { void on_mpv_render_update(); void process_mpv_events(); + + void gen_fbo_texture(); + + void setup_fbo_texture(int w, int h) const; }; From 32cbd6ceb57d8e02d04494841892621381ea8aa7 Mon Sep 17 00:00:00 2001 From: f0e <7321764+f0e@users.noreply.github.com> Date: Mon, 1 Sep 2025 16:06:51 +1000 Subject: [PATCH 009/181] feat: good mpv options --- src/gui/ui/helpers/video.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/gui/ui/helpers/video.cpp b/src/gui/ui/helpers/video.cpp index f31ec331..1c149347 100644 --- a/src/gui/ui/helpers/video.cpp +++ b/src/gui/ui/helpers/video.cpp @@ -89,9 +89,10 @@ void VideoPlayer::initialize_mpv() { // configure mpv mpv_set_option_string(m_mpv, "vo", "libmpv"); + mpv_set_option_string(m_mpv, "hwdec", "auto-safe"); // enable hardware decoding + mpv_set_option_string(m_mpv, "profile", "gpu-hq"); // high quality profile + mpv_set_option_string(m_mpv, "keep-open", "yes"); - if (mpv_initialize(m_mpv) < 0) { - throw std::runtime_error("MPV initialization failed"); } mpv_request_log_messages(m_mpv, "debug"); From dce738e7aae12fde23ebde2ae65d57978f1aacba Mon Sep 17 00:00:00 2001 From: f0e <7321764+f0e@users.noreply.github.com> Date: Mon, 1 Sep 2025 16:07:29 +1000 Subject: [PATCH 010/181] feat: proper aspect ratio --- src/gui/ui/elements/video.cpp | 106 ++++++++++++------- src/gui/ui/helpers/video.cpp | 185 +++++++++++++++++++++++++++++----- src/gui/ui/helpers/video.h | 27 ++++- 3 files changed, 250 insertions(+), 68 deletions(-) diff --git a/src/gui/ui/elements/video.cpp b/src/gui/ui/elements/video.cpp index fdd69774..dedb02af 100644 --- a/src/gui/ui/elements/video.cpp +++ b/src/gui/ui/elements/video.cpp @@ -5,12 +5,36 @@ namespace { std::unordered_map> video_players; + + VideoPlayer* get_or_add_player(const std::filesystem::path& video_path) { + auto key = video_path.string(); + + try { + auto [it, inserted] = video_players.try_emplace(key, std::make_unique()); + auto* player = it->second.get(); + + if (inserted) { + player->load_file(key.c_str()); + u::log("loaded video from {}", key); + } + + return player; + } + catch (const std::exception& e) { + u::log_error("failed to load video from {} ({})", key, e.what()); + return nullptr; + } + } } void ui::render_videos() { for (auto& [id, player] : video_players) { - if (player) { - player->render(1000, 1000); + if (player && player->is_video_ready()) { + auto dimensions = player->get_video_dimensions(); + if (dimensions) { + auto [width, height] = *dimensions; + player->render(width, height); + } } } } @@ -33,17 +57,27 @@ void ui::handle_videos_event(const SDL_Event& event, bool& to_render) { void ui::render_video(const Container& container, const AnimatedElement& element) { const auto& video_data = std::get(element.element->data); + + if (!video_data.player->is_video_ready()) { + // TODO: loading indicator? if so, remove the code in add_video + return; + } + float anim = element.animations.at(hasher("main")).current; int alpha = anim * 255; int stroke_alpha = anim * 125; + auto usable_rect = element.element->rect.shrink(2); // account for border + + // TODO: render::image render::imgui.drawlist->AddImage( video_data.player->get_frame_texture_for_render(), - element.element->rect.origin(), - element.element->rect.max(), + usable_rect.origin(), + usable_rect.max(), ImVec2(0, 0), - ImVec2(1, 1) + ImVec2(1, 1), + IM_COL32(255, 255, 255, alpha) // apply alpha for fade animations ); render::borders( @@ -95,46 +129,44 @@ std::optional ui::add_video( if (video_path.empty()) { return {}; } - VideoPlayer* player = nullptr; - auto video_player_it = video_players.find(video_path.string()); + VideoPlayer* player = get_or_add_player(video_path); - if (video_player_it == video_players.end()) { - try { - auto player_ref = video_players.emplace(video_path.string(), std::make_unique()); - player = player_ref.first->second.get(); - player->load_file(video_path.string().c_str()); - u::log("{} loaded video from {}", id, video_path.string()); - } - catch (const std::exception& e) { - u::log_error("{} failed to load video from {} ({})", id, video_path.string(), e.what()); - return {}; - } - } + if (!player || !player->is_video_ready()) + return {}; // TODO: loading indicator? remove this. - float aspect_ratio = 16.0f / 9.0f; // todo: proper aspect ratio + gfx::Rect video_rect; - gfx::Rect video_rect(container.current_position, max_size); + auto dimensions = player->get_video_dimensions(); - // maintain aspect ratio - float target_width = video_rect.h * aspect_ratio; - float target_height = video_rect.w / aspect_ratio; + if (dimensions) { + // we have valid dimensions, calculate proper aspect ratio + auto [video_width, video_height] = *dimensions; + float aspect_ratio = static_cast(video_width) / static_cast(video_height); - if (target_width <= video_rect.w) { - video_rect.w = static_cast(target_width); - } - else { - video_rect.h = static_cast(target_height); - } + video_rect = gfx::Rect(container.current_position, max_size); - if (video_rect.h > max_size.h) { - video_rect.h = max_size.h; - video_rect.w = static_cast(max_size.h * aspect_ratio); - } + // maintain aspect ratio while fitting within max_size + float target_width = video_rect.h * aspect_ratio; + float target_height = video_rect.w / aspect_ratio; + + if (target_width <= max_size.w) { + video_rect.w = static_cast(target_width); + } + else { + video_rect.h = static_cast(target_height); + } - if (video_rect.w > max_size.w) { - video_rect.w = max_size.w; - video_rect.h = static_cast(max_size.w / aspect_ratio); + // ensure we don't exceed max dimensions + if (video_rect.h > max_size.h) { + video_rect.h = max_size.h; + video_rect.w = static_cast(max_size.h * aspect_ratio); + } + + if (video_rect.w > max_size.w) { + video_rect.w = max_size.w; + video_rect.h = static_cast(max_size.w / aspect_ratio); + } } Element element( diff --git a/src/gui/ui/helpers/video.cpp b/src/gui/ui/helpers/video.cpp index 1c149347..906a7d13 100644 --- a/src/gui/ui/helpers/video.cpp +++ b/src/gui/ui/helpers/video.cpp @@ -1,6 +1,16 @@ #include "video.h" VideoPlayer::~VideoPlayer() { + // clean up opengl resources + if (m_tex) { + glDeleteTextures(1, &m_tex); + m_tex = 0; + } + if (m_fbo) { + glDeleteFramebuffers(1, &m_fbo); + m_fbo = 0; + } + if (m_mpv_gl) { mpv_render_context_free(m_mpv_gl); } @@ -14,44 +24,86 @@ VideoPlayer::~VideoPlayer() { void VideoPlayer::handle_key_press(SDL_Keycode key) { if (key == SDLK_SPACE) { - static std::array cmd = { "cycle", "pause", nullptr }; + std::array cmd = { "cycle", "pause", nullptr }; mpv_command_async(m_mpv, 0, cmd.data()); } } void VideoPlayer::load_file(const char* file_path) { - static std::array cmd = { "loadfile", file_path, nullptr }; + std::array cmd = { "loadfile", file_path, nullptr }; mpv_command_async(m_mpv, 0, cmd.data()); + m_video_loaded = false; // reset video loaded state } void VideoPlayer::gen_fbo_texture() { glGenFramebuffers(1, &m_fbo); - glBindFramebuffer(GL_FRAMEBUFFER, m_fbo); - glGenTextures(1, &m_tex); - glBindTexture(GL_TEXTURE_2D, m_tex); + glBindTexture(GL_TEXTURE_2D, m_tex); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glBindTexture(GL_TEXTURE_2D, 0); glBindFramebuffer(GL_FRAMEBUFFER, 0); + + // initialize with a default size + m_current_width = 0; + m_current_height = 0; } -void VideoPlayer::setup_fbo_texture(int w, int h) const { - glBindFramebuffer(GL_FRAMEBUFFER, m_fbo); - glBindTexture(GL_TEXTURE_2D, m_tex); +void VideoPlayer::setup_fbo_texture(int w, int h) { + // only recreate texture if dimensions changed + if (w != m_current_width || h != m_current_height) { + glBindFramebuffer(GL_FRAMEBUFFER, m_fbo); + glBindTexture(GL_TEXTURE_2D, m_tex); + + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_tex, 0); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, w, h, 0, GL_RGB, GL_UNSIGNED_BYTE, nullptr); + + // check framebuffer completeness + GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER); + if (status != GL_FRAMEBUFFER_COMPLETE) { + glBindFramebuffer(GL_FRAMEBUFFER, 0); + glBindTexture(GL_TEXTURE_2D, 0); + throw std::runtime_error("Framebuffer not complete"); + } - glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_tex, 0); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, w, h, 0, GL_RGB, GL_UNSIGNED_BYTE, nullptr); + glBindFramebuffer(GL_FRAMEBUFFER, 0); + glBindTexture(GL_TEXTURE_2D, 0); - glBindFramebuffer(GL_FRAMEBUFFER, 0); - glBindTexture(GL_TEXTURE_2D, 0); + m_current_width = w; + m_current_height = h; + + if (glGetError() != GL_NO_ERROR) { + throw std::runtime_error("OpenGL error during FBO setup"); + } + } } -void VideoPlayer::render(int w, int h) { +bool VideoPlayer::render(int w, int h) { + // don't render if we don't have valid dimensions + if (w <= 0 || h <= 0) { + return false; + } + + // don't render until video is actually loaded and ready + if (!m_video_loaded) { + return false; + } + setup_fbo_texture(w, h); + // save current opengl state + GLint prev_fbo = 0; + glGetIntegerv(GL_FRAMEBUFFER_BINDING, &prev_fbo); + + glBindFramebuffer(GL_FRAMEBUFFER, m_fbo); + + // clear the framebuffer + glViewport(0, 0, w, h); + glClearColor(0.0f, 0.0f, 0.0f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); + mpv_opengl_fbo fbo{ .fbo = (int)m_fbo, .w = w, @@ -65,7 +117,17 @@ void VideoPlayer::render(int w, int h) { { .type = MPV_RENDER_PARAM_FLIP_Y, .data = &flip_y }, { .type = MPV_RENDER_PARAM_INVALID } }; - mpv_render_context_render(m_mpv_gl, params.data()); + int result = mpv_render_context_render(m_mpv_gl, params.data()); + if (result < 0) { + u::log_error("MPV render failed: {}", mpv_error_string(result)); + // restore previous framebuffer + glBindFramebuffer(GL_FRAMEBUFFER, prev_fbo); + return false; + } + + // restore previous framebuffer + glBindFramebuffer(GL_FRAMEBUFFER, prev_fbo); + return true; } void VideoPlayer::handle_mpv_event(const SDL_Event& event, bool& redraw) { @@ -93,9 +155,14 @@ void VideoPlayer::initialize_mpv() { mpv_set_option_string(m_mpv, "profile", "gpu-hq"); // high quality profile mpv_set_option_string(m_mpv, "keep-open", "yes"); + int result = mpv_initialize(m_mpv); + if (result < 0) { + mpv_destroy(m_mpv); + m_mpv = nullptr; + throw std::runtime_error("MPV initialization failed: " + std::string(mpv_error_string(result))); } - mpv_request_log_messages(m_mpv, "debug"); + mpv_request_log_messages(m_mpv, "warn"); // set up callbacks mpv_set_wakeup_callback( @@ -131,8 +198,11 @@ void VideoPlayer::initialize_mpv() { { .type = MPV_RENDER_PARAM_INVALID } }; // create mpv render context - if (mpv_render_context_create(&m_mpv_gl, m_mpv, params.data()) < 0) { - throw std::runtime_error("Failed to initialize MPV GL context"); + result = mpv_render_context_create(&m_mpv_gl, m_mpv, params.data()); + if (result < 0) { + mpv_destroy(m_mpv); + m_mpv = nullptr; + throw std::runtime_error("Failed to initialize MPV GL context: " + std::string(mpv_error_string(result))); } // set up render update callback @@ -148,12 +218,16 @@ void VideoPlayer::initialize_mpv() { void VideoPlayer::on_mpv_events() { SDL_Event event = { .type = m_wakeup_on_mpv_events }; - SDL_PushEvent(&event); + if (!SDL_PushEvent(&event)) { + u::log_error("Failed to push MPV event: {}", SDL_GetError()); + } } void VideoPlayer::on_mpv_render_update() { SDL_Event event = { .type = m_wakeup_on_mpv_render_update }; - SDL_PushEvent(&event); + if (!SDL_PushEvent(&event)) { + u::log_error("Failed to push MPV render update event: {}", SDL_GetError()); + } } void VideoPlayer::process_mpv_events() { @@ -165,15 +239,74 @@ void VideoPlayer::process_mpv_events() { break; } - if (mp_event->event_id == MPV_EVENT_LOG_MESSAGE) { - auto* msg = static_cast(mp_event->data); - // print specific debug log messages - if (std::strstr(msg->text, "DR image")) { - u::log("Log: {}", msg->text); + switch (mp_event->event_id) { + case MPV_EVENT_LOG_MESSAGE: { + auto* msg = static_cast(mp_event->data); + if (std::strstr(msg->text, "DR image")) { + u::log("MPV Log: {}", msg->text); + } + break; + } + case MPV_EVENT_START_FILE: + u::log("MPV: Starting file"); + m_video_loaded = false; + break; + case MPV_EVENT_FILE_LOADED: + u::log("MPV: File loaded"); + break; + case MPV_EVENT_VIDEO_RECONFIG: + u::log("MPV: Video reconfigured"); + // video is now ready to render + m_video_loaded = true; + break; + case MPV_EVENT_PLAYBACK_RESTART: + u::log("MPV: Playback restarted"); + break; + case MPV_EVENT_END_FILE: { + auto* end_event = static_cast(mp_event->data); + if (end_event->reason == MPV_END_FILE_REASON_ERROR) { + u::log_error("MPV: File ended with error: {}", mpv_error_string(end_event->error)); + } + else { + u::log("MPV: File ended normally"); + } + m_video_loaded = false; + break; } - continue; + default: + u::log("MPV Event: {}", mpv_event_name(mp_event->event_id)); + break; } + } +} - u::log("Event: ", mpv_event_name(mp_event->event_id)); +std::optional> VideoPlayer::get_video_dimensions() const { + if (!m_mpv || !m_video_loaded) { + return std::nullopt; } + + int64_t width = 0; + int64_t height = 0; + + // try to get display width/height first (accounts for aspect ratio) + int result1 = mpv_get_property(m_mpv, "dwidth", MPV_FORMAT_INT64, &width); + int result2 = mpv_get_property(m_mpv, "dheight", MPV_FORMAT_INT64, &height); + + if (result1 == 0 && result2 == 0 && width > 0 && height > 0) { + return std::make_pair(static_cast(width), static_cast(height)); + } + + // fallback to video width/height + result1 = mpv_get_property(m_mpv, "video-params/w", MPV_FORMAT_INT64, &width); + result2 = mpv_get_property(m_mpv, "video-params/h", MPV_FORMAT_INT64, &height); + + if (result1 == 0 && result2 == 0 && width > 0 && height > 0) { + return std::make_pair(static_cast(width), static_cast(height)); + } + + return std::nullopt; +} + +bool VideoPlayer::is_video_ready() const { + return m_video_loaded; } diff --git a/src/gui/ui/helpers/video.h b/src/gui/ui/helpers/video.h index 988710fe..cc96c7d6 100644 --- a/src/gui/ui/helpers/video.h +++ b/src/gui/ui/helpers/video.h @@ -2,11 +2,14 @@ #include #include +#include +#include class VideoPlayer { public: VideoPlayer() - : m_wakeup_on_mpv_render_update(SDL_RegisterEvents(1)), m_wakeup_on_mpv_events(SDL_RegisterEvents(1)) { + : m_wakeup_on_mpv_render_update(SDL_RegisterEvents(1)), m_wakeup_on_mpv_events(SDL_RegisterEvents(1)), m_fbo(0), + m_tex(0), m_current_width(0), m_current_height(0), m_video_loaded(false) { if (m_wakeup_on_mpv_render_update == static_cast(-1) || m_wakeup_on_mpv_events == static_cast(-1)) { @@ -17,9 +20,9 @@ class VideoPlayer { gen_fbo_texture(); } - VideoPlayer(const VideoPlayer&) = default; + VideoPlayer(const VideoPlayer&) = delete; // should not be copyable VideoPlayer(VideoPlayer&&) = delete; - VideoPlayer& operator=(const VideoPlayer&) = default; + VideoPlayer& operator=(const VideoPlayer&) = delete; VideoPlayer& operator=(VideoPlayer&&) = delete; ~VideoPlayer(); @@ -28,7 +31,8 @@ class VideoPlayer { void load_file(const char* file_path); - void render(int w, int h); + // returns true if render was successful, false if video not ready + bool render(int w, int h); [[nodiscard]] GLuint get_frame_texture_for_render() const { return m_tex; @@ -36,6 +40,12 @@ class VideoPlayer { void handle_mpv_event(const SDL_Event& event, bool& redraw); + // get actual video dimensions - returns nullopt if video not ready + [[nodiscard]] std::optional> get_video_dimensions() const; + + // check if video is loaded and ready to render + [[nodiscard]] bool is_video_ready() const; + private: mpv_handle* m_mpv = nullptr; mpv_render_context* m_mpv_gl = nullptr; @@ -45,6 +55,13 @@ class VideoPlayer { GLuint m_fbo; GLuint m_tex; + // cache current texture dimensions to avoid unnecessary recreations + int m_current_width; + int m_current_height; + + // track if video is loaded and ready to render + bool m_video_loaded; + void initialize_mpv(); void on_mpv_events(); @@ -55,5 +72,5 @@ class VideoPlayer { void gen_fbo_texture(); - void setup_fbo_texture(int w, int h) const; + void setup_fbo_texture(int w, int h); }; From d1c8cf5e3754cde964d78a27952741c7ba29d239 Mon Sep 17 00:00:00 2001 From: f0e <7321764+f0e@users.noreply.github.com> Date: Mon, 1 Sep 2025 16:23:20 +1000 Subject: [PATCH 011/181] fix: continually creating new videoplayers & use shared ptr instead of raw ptrs --- src/gui/ui/elements/video.cpp | 30 ++++++++++++++++++------------ src/gui/ui/ui.h | 2 +- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/src/gui/ui/elements/video.cpp b/src/gui/ui/elements/video.cpp index dedb02af..32645a01 100644 --- a/src/gui/ui/elements/video.cpp +++ b/src/gui/ui/elements/video.cpp @@ -4,25 +4,28 @@ #include "../helpers/video.h" namespace { - std::unordered_map> video_players; + std::unordered_map> video_players; - VideoPlayer* get_or_add_player(const std::filesystem::path& video_path) { + tl::expected, std::string> get_or_add_player(const std::filesystem::path& video_path) { auto key = video_path.string(); try { - auto [it, inserted] = video_players.try_emplace(key, std::make_unique()); - auto* player = it->second.get(); + auto it = video_players.find(key); - if (inserted) { + if (it == video_players.end()) { + auto player = std::make_shared(); player->load_file(key.c_str()); u::log("loaded video from {}", key); + + auto insert_result = video_players.insert({ key, player }); + it = insert_result.first; // safe because insert always returns valid iterator } - return player; + return it->second; } catch (const std::exception& e) { u::log_error("failed to load video from {} ({})", key, e.what()); - return nullptr; + return tl::unexpected("failed to load video"); } } } @@ -126,13 +129,16 @@ void ui::remove_video(AnimatedElement& element) { std::optional ui::add_video( const std::string& id, Container& container, const std::filesystem::path& video_path, const gfx::Size& max_size ) { - if (video_path.empty()) { + if (video_path.empty()) + return {}; + + auto player_res = get_or_add_player(video_path); + if (!player_res) return {}; - } - VideoPlayer* player = get_or_add_player(video_path); + auto player = *player_res; - if (!player || !player->is_video_ready()) + if (!player->is_video_ready()) return {}; // TODO: loading indicator? remove this. gfx::Rect video_rect; @@ -175,7 +181,7 @@ std::optional ui::add_video( video_rect, VideoElementData{ .video_path = video_path, - .player = player, + .player = std::move(player), }, render_video, update_video, diff --git a/src/gui/ui/ui.h b/src/gui/ui/ui.h index 95555627..fb2987a0 100644 --- a/src/gui/ui/ui.h +++ b/src/gui/ui/ui.h @@ -133,7 +133,7 @@ namespace ui { struct VideoElementData { std::filesystem::path video_path; - VideoPlayer* player; + std::shared_ptr player; bool operator==(const VideoElementData& other) const { return video_path == other.video_path; From b094f46d22d02189986d361e2b0c9a451ec85a75 Mon Sep 17 00:00:00 2001 From: f0e <7321764+f0e@users.noreply.github.com> Date: Mon, 1 Sep 2025 16:26:25 +1000 Subject: [PATCH 012/181] refactor: probably no need for fallback --- src/gui/ui/helpers/video.cpp | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/gui/ui/helpers/video.cpp b/src/gui/ui/helpers/video.cpp index 906a7d13..8afda830 100644 --- a/src/gui/ui/helpers/video.cpp +++ b/src/gui/ui/helpers/video.cpp @@ -288,7 +288,6 @@ std::optional> VideoPlayer::get_video_dimensions() const { int64_t width = 0; int64_t height = 0; - // try to get display width/height first (accounts for aspect ratio) int result1 = mpv_get_property(m_mpv, "dwidth", MPV_FORMAT_INT64, &width); int result2 = mpv_get_property(m_mpv, "dheight", MPV_FORMAT_INT64, &height); @@ -296,14 +295,6 @@ std::optional> VideoPlayer::get_video_dimensions() const { return std::make_pair(static_cast(width), static_cast(height)); } - // fallback to video width/height - result1 = mpv_get_property(m_mpv, "video-params/w", MPV_FORMAT_INT64, &width); - result2 = mpv_get_property(m_mpv, "video-params/h", MPV_FORMAT_INT64, &height); - - if (result1 == 0 && result2 == 0 && width > 0 && height > 0) { - return std::make_pair(static_cast(width), static_cast(height)); - } - return std::nullopt; } From 83e64b0bf791508ad99692c91dd0877a8abc0404 Mon Sep 17 00:00:00 2001 From: newgan Date: Mon, 1 Sep 2025 02:34:22 -0400 Subject: [PATCH 013/181] wip frame prog --- src/gui/ui/elements/video.cpp | 9 +++++++-- src/gui/ui/helpers/video.cpp | 17 +++++++++++++++++ src/gui/ui/helpers/video.h | 10 ++++++++++ 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/gui/ui/elements/video.cpp b/src/gui/ui/elements/video.cpp index 32645a01..6ac0cddf 100644 --- a/src/gui/ui/elements/video.cpp +++ b/src/gui/ui/elements/video.cpp @@ -83,8 +83,13 @@ void ui::render_video(const Container& container, const AnimatedElement& element IM_COL32(255, 255, 255, alpha) // apply alpha for fade animations ); - render::borders( - element.element->rect, gfx::Color(155, 155, 155, stroke_alpha), gfx::Color(80, 80, 80, stroke_alpha) + auto frame_data = video_data.player->get_frame_data(); + + render::text( + { usable_rect.center().x + 30, usable_rect.y - fonts::dejavu.height() }, + gfx::Color::white(), + std::format("{}/{}", frame_data.current_frame, frame_data.total_frames), + fonts::dejavu ); } diff --git a/src/gui/ui/helpers/video.cpp b/src/gui/ui/helpers/video.cpp index 8afda830..4ec534ae 100644 --- a/src/gui/ui/helpers/video.cpp +++ b/src/gui/ui/helpers/video.cpp @@ -214,6 +214,8 @@ void VideoPlayer::initialize_mpv() { }, this ); + mpv_observe_property(m_mpv, 0, "estimated-frame-count", MPV_FORMAT_INT64); + mpv_observe_property(m_mpv, 0, "estimated-frame-number", MPV_FORMAT_INT64); } void VideoPlayer::on_mpv_events() { @@ -273,6 +275,21 @@ void VideoPlayer::process_mpv_events() { m_video_loaded = false; break; } + case MPV_EVENT_PROPERTY_CHANGE: { + auto* prop = static_cast(mp_event->data); + + if (strcmp(prop->name, "estimated-frame-count") == 0) { + if (prop->data) { + m_frame_data.total_frames = *static_cast(prop->data); + } + } + else if (strcmp(prop->name, "estimated-frame-number") == 0) { + if (prop->data) { + m_frame_data.current_frame = *static_cast(prop->data); + } + } + break; + } default: u::log("MPV Event: {}", mpv_event_name(mp_event->event_id)); break; diff --git a/src/gui/ui/helpers/video.h b/src/gui/ui/helpers/video.h index cc96c7d6..6913d20c 100644 --- a/src/gui/ui/helpers/video.h +++ b/src/gui/ui/helpers/video.h @@ -5,6 +5,11 @@ #include #include +struct FrameData { + int64_t current_frame = 0; + int64_t total_frames = 0; +}; + class VideoPlayer { public: VideoPlayer() @@ -38,6 +43,10 @@ class VideoPlayer { return m_tex; } + [[nodiscard]] FrameData get_frame_data() const { + return m_frame_data; + } + void handle_mpv_event(const SDL_Event& event, bool& redraw); // get actual video dimensions - returns nullopt if video not ready @@ -61,6 +70,7 @@ class VideoPlayer { // track if video is loaded and ready to render bool m_video_loaded; + FrameData m_frame_data; void initialize_mpv(); From 5583ab09e5921f09811d93f242c97ece6e8eb202 Mon Sep 17 00:00:00 2001 From: f0e <7321764+f0e@users.noreply.github.com> Date: Mon, 1 Sep 2025 16:45:20 +1000 Subject: [PATCH 014/181] feat: video loading indicator --- src/gui/render/render.cpp | 5 +++ src/gui/render/render.h | 2 + src/gui/ui/elements/video.cpp | 73 +++++++++++++++++++---------------- src/gui/ui/ui.h | 3 +- 4 files changed, 49 insertions(+), 34 deletions(-) diff --git a/src/gui/render/render.cpp b/src/gui/render/render.cpp index da230309..cd725b32 100644 --- a/src/gui/render/render.cpp +++ b/src/gui/render/render.cpp @@ -657,6 +657,11 @@ void render::borders(const gfx::Rect& rect, const gfx::Color& border_color, cons rect_stroke(rect, border_color, 1.f); } +void render::loader(const gfx::Rect& rect, const gfx::Color& color) { + // TODO: make this nicer? use in other places too? + text(rect.center(), color, "loading...", fonts::dejavu, FONT_CENTERED_X | FONT_CENTERED_Y); +} + void render::push_clip_rect(const gfx::Rect& rect, bool intersect_clip_rect) { imgui.drawlist->PushClipRect(rect.origin(), rect.max(), intersect_clip_rect); } diff --git a/src/gui/render/render.h b/src/gui/render/render.h index 75b52f2e..3b15d846 100644 --- a/src/gui/render/render.h +++ b/src/gui/render/render.h @@ -239,6 +239,8 @@ namespace render { void borders(const gfx::Rect& rect, const gfx::Color& border_color, const gfx::Color& inner_border_color); + void loader(const gfx::Rect& rect, const gfx::Color& color); + void push_clip_rect(const gfx::Rect& rect, bool intersect_clip_rect = false); void push_clip_rect(int x1, int y1, int x2, int y2, bool intersect_clip_rect = false); void push_fullscreen_clip_rect(); diff --git a/src/gui/ui/elements/video.cpp b/src/gui/ui/elements/video.cpp index 6ac0cddf..8dc3e4db 100644 --- a/src/gui/ui/elements/video.cpp +++ b/src/gui/ui/elements/video.cpp @@ -3,6 +3,9 @@ #include "../keys.h" #include "../helpers/video.h" +constexpr gfx::Size LOADER_SIZE(20, 20); +constexpr gfx::Size LOADER_PAD(5, 5); + namespace { std::unordered_map> video_players; @@ -61,16 +64,19 @@ void ui::handle_videos_event(const SDL_Event& event, bool& to_render) { void ui::render_video(const Container& container, const AnimatedElement& element) { const auto& video_data = std::get(element.element->data); - if (!video_data.player->is_video_ready()) { - // TODO: loading indicator? if so, remove the code in add_video - return; - } - float anim = element.animations.at(hasher("main")).current; int alpha = anim * 255; int stroke_alpha = anim * 125; + if (video_data.loading) { + int loader_alpha = anim * 155; + + gfx::Rect loader_rect = element.element->rect.shrink(LOADER_PAD, true); + render::loader(loader_rect, gfx::Color::white(loader_alpha)); + return; + } + auto usable_rect = element.element->rect.shrink(2); // account for border // TODO: render::image @@ -143,50 +149,51 @@ std::optional ui::add_video( auto player = *player_res; - if (!player->is_video_ready()) - return {}; // TODO: loading indicator? remove this. + gfx::Size elem_size = LOADER_SIZE; + bool loading = !player->is_video_ready(); - gfx::Rect video_rect; + if (!loading) { + auto dimensions = player->get_video_dimensions(); - auto dimensions = player->get_video_dimensions(); + if (dimensions) { + // we have valid dimensions, calculate proper aspect ratio + auto [video_width, video_height] = *dimensions; + float aspect_ratio = static_cast(video_width) / static_cast(video_height); - if (dimensions) { - // we have valid dimensions, calculate proper aspect ratio - auto [video_width, video_height] = *dimensions; - float aspect_ratio = static_cast(video_width) / static_cast(video_height); + elem_size = max_size; - video_rect = gfx::Rect(container.current_position, max_size); + // maintain aspect ratio while fitting within max_size + float target_width = elem_size.h * aspect_ratio; + float target_height = elem_size.w / aspect_ratio; - // maintain aspect ratio while fitting within max_size - float target_width = video_rect.h * aspect_ratio; - float target_height = video_rect.w / aspect_ratio; - - if (target_width <= max_size.w) { - video_rect.w = static_cast(target_width); - } - else { - video_rect.h = static_cast(target_height); - } + if (target_width <= max_size.w) { + elem_size.w = static_cast(target_width); + } + else { + elem_size.h = static_cast(target_height); + } - // ensure we don't exceed max dimensions - if (video_rect.h > max_size.h) { - video_rect.h = max_size.h; - video_rect.w = static_cast(max_size.h * aspect_ratio); - } + // ensure we don't exceed max dimensions + if (elem_size.h > max_size.h) { + elem_size.h = max_size.h; + elem_size.w = static_cast(max_size.h * aspect_ratio); + } - if (video_rect.w > max_size.w) { - video_rect.w = max_size.w; - video_rect.h = static_cast(max_size.w / aspect_ratio); + if (elem_size.w > max_size.w) { + elem_size.w = max_size.w; + elem_size.h = static_cast(max_size.w / aspect_ratio); + } } } Element element( id, ElementType::VIDEO, - video_rect, + gfx::Rect(container.current_position, elem_size), VideoElementData{ .video_path = video_path, .player = std::move(player), + .loading = loading, }, render_video, update_video, diff --git a/src/gui/ui/ui.h b/src/gui/ui/ui.h index fb2987a0..ad2b005d 100644 --- a/src/gui/ui/ui.h +++ b/src/gui/ui/ui.h @@ -134,9 +134,10 @@ namespace ui { struct VideoElementData { std::filesystem::path video_path; std::shared_ptr player; + bool loading; bool operator==(const VideoElementData& other) const { - return video_path == other.video_path; + return video_path == other.video_path && loading == other.loading; } }; From d50949c4963201f97f9a50da37cc42ed70f3af15 Mon Sep 17 00:00:00 2001 From: f0e <7321764+f0e@users.noreply.github.com> Date: Mon, 1 Sep 2025 16:50:16 +1000 Subject: [PATCH 015/181] fix: wip frame progress --- src/gui/ui/elements/video.cpp | 18 +++++++++------- src/gui/ui/helpers/video.cpp | 40 ++++++++++++++++++++--------------- src/gui/ui/helpers/video.h | 11 +--------- 3 files changed, 34 insertions(+), 35 deletions(-) diff --git a/src/gui/ui/elements/video.cpp b/src/gui/ui/elements/video.cpp index 8dc3e4db..034f43ed 100644 --- a/src/gui/ui/elements/video.cpp +++ b/src/gui/ui/elements/video.cpp @@ -89,14 +89,16 @@ void ui::render_video(const Container& container, const AnimatedElement& element IM_COL32(255, 255, 255, alpha) // apply alpha for fade animations ); - auto frame_data = video_data.player->get_frame_data(); - - render::text( - { usable_rect.center().x + 30, usable_rect.y - fonts::dejavu.height() }, - gfx::Color::white(), - std::format("{}/{}", frame_data.current_frame, frame_data.total_frames), - fonts::dejavu - ); + auto frame_data = video_data.player->get_video_frame_data(); + + if (frame_data) { + render::text( + { usable_rect.center().x + 30, usable_rect.y - fonts::dejavu.height() }, + gfx::Color::white(), + std::format("{}/{}", frame_data->current_frame, frame_data->total_frames), + fonts::dejavu + ); + } } bool ui::update_video(const Container& container, AnimatedElement& element) { diff --git a/src/gui/ui/helpers/video.cpp b/src/gui/ui/helpers/video.cpp index 4ec534ae..67953db3 100644 --- a/src/gui/ui/helpers/video.cpp +++ b/src/gui/ui/helpers/video.cpp @@ -275,21 +275,6 @@ void VideoPlayer::process_mpv_events() { m_video_loaded = false; break; } - case MPV_EVENT_PROPERTY_CHANGE: { - auto* prop = static_cast(mp_event->data); - - if (strcmp(prop->name, "estimated-frame-count") == 0) { - if (prop->data) { - m_frame_data.total_frames = *static_cast(prop->data); - } - } - else if (strcmp(prop->name, "estimated-frame-number") == 0) { - if (prop->data) { - m_frame_data.current_frame = *static_cast(prop->data); - } - } - break; - } default: u::log("MPV Event: {}", mpv_event_name(mp_event->event_id)); break; @@ -299,7 +284,7 @@ void VideoPlayer::process_mpv_events() { std::optional> VideoPlayer::get_video_dimensions() const { if (!m_mpv || !m_video_loaded) { - return std::nullopt; + return {}; } int64_t width = 0; @@ -312,7 +297,28 @@ std::optional> VideoPlayer::get_video_dimensions() const { return std::make_pair(static_cast(width), static_cast(height)); } - return std::nullopt; + return {}; +} + +std::optional VideoPlayer::get_video_frame_data() const { + if (!m_mpv || !m_video_loaded) { + return {}; + } + + int64_t current_frame = 0; + int64_t total_frames = 0; + + int result1 = mpv_get_property(m_mpv, "estimated-frame-number", MPV_FORMAT_INT64, ¤t_frame); + int result2 = mpv_get_property(m_mpv, "estimated-frame-count", MPV_FORMAT_INT64, &total_frames); + + if (result1 == 0 && result2 == 0 && current_frame > 0 && total_frames > 0) { + return FrameData{ + .current_frame = current_frame, + .total_frames = total_frames, + }; + } + + return {}; } bool VideoPlayer::is_video_ready() const { diff --git a/src/gui/ui/helpers/video.h b/src/gui/ui/helpers/video.h index 6913d20c..2c70cadd 100644 --- a/src/gui/ui/helpers/video.h +++ b/src/gui/ui/helpers/video.h @@ -36,23 +36,17 @@ class VideoPlayer { void load_file(const char* file_path); - // returns true if render was successful, false if video not ready bool render(int w, int h); [[nodiscard]] GLuint get_frame_texture_for_render() const { return m_tex; } - [[nodiscard]] FrameData get_frame_data() const { - return m_frame_data; - } - void handle_mpv_event(const SDL_Event& event, bool& redraw); - // get actual video dimensions - returns nullopt if video not ready [[nodiscard]] std::optional> get_video_dimensions() const; + [[nodiscard]] std::optional get_video_frame_data() const; - // check if video is loaded and ready to render [[nodiscard]] bool is_video_ready() const; private: @@ -64,13 +58,10 @@ class VideoPlayer { GLuint m_fbo; GLuint m_tex; - // cache current texture dimensions to avoid unnecessary recreations int m_current_width; int m_current_height; - // track if video is loaded and ready to render bool m_video_loaded; - FrameData m_frame_data; void initialize_mpv(); From 096f7b0e850aecfc33278e27b13f6d62cfea736d Mon Sep 17 00:00:00 2001 From: f0e <7321764+f0e@users.noreply.github.com> Date: Mon, 1 Sep 2025 16:58:20 +1000 Subject: [PATCH 016/181] chore: remove leftover observe stuff --- src/gui/ui/helpers/video.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/gui/ui/helpers/video.cpp b/src/gui/ui/helpers/video.cpp index 67953db3..a40cde4d 100644 --- a/src/gui/ui/helpers/video.cpp +++ b/src/gui/ui/helpers/video.cpp @@ -214,8 +214,6 @@ void VideoPlayer::initialize_mpv() { }, this ); - mpv_observe_property(m_mpv, 0, "estimated-frame-count", MPV_FORMAT_INT64); - mpv_observe_property(m_mpv, 0, "estimated-frame-number", MPV_FORMAT_INT64); } void VideoPlayer::on_mpv_events() { From d8570d3331bd77e7ef687ae892e2abd973337b87 Mon Sep 17 00:00:00 2001 From: f0e <7321764+f0e@users.noreply.github.com> Date: Mon, 1 Sep 2025 16:58:51 +1000 Subject: [PATCH 017/181] chore: add todo notes --- src/gui/ui/helpers/video.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/gui/ui/helpers/video.cpp b/src/gui/ui/helpers/video.cpp index a40cde4d..3df454ba 100644 --- a/src/gui/ui/helpers/video.cpp +++ b/src/gui/ui/helpers/video.cpp @@ -285,6 +285,8 @@ std::optional> VideoPlayer::get_video_dimensions() const { return {}; } + // TODO: get this from observer instead? + int64_t width = 0; int64_t height = 0; @@ -303,6 +305,8 @@ std::optional VideoPlayer::get_video_frame_data() const { return {}; } + // TODO: get this from observer instead? + int64_t current_frame = 0; int64_t total_frames = 0; From a398a2703e147b27a4bf21bec284d562b9fa14b9 Mon Sep 17 00:00:00 2001 From: newgan Date: Mon, 1 Sep 2025 03:20:17 -0400 Subject: [PATCH 018/181] feat: wip video track --- src/gui/ui/elements/video.cpp | 19 +++++++++++++++++++ src/gui/ui/ui.h | 3 +++ 2 files changed, 22 insertions(+) diff --git a/src/gui/ui/elements/video.cpp b/src/gui/ui/elements/video.cpp index 034f43ed..e78c86c8 100644 --- a/src/gui/ui/elements/video.cpp +++ b/src/gui/ui/elements/video.cpp @@ -61,6 +61,23 @@ void ui::handle_videos_event(const SDL_Event& event, bool& to_render) { } } +void ui::render_video_track( + const Container& container, const AnimatedElement& element, std::optional frame_data +) { + const auto& video_data = std::get(element.element->data); + float anim = element.animations.at(hasher("main")).current; + + auto usable_rect = element.element->rect.expand(2); + + render::line({ usable_rect.x, usable_rect.y2() }, { usable_rect.x2(), usable_rect.y2() }, gfx::Color::green()); + + if (frame_data) { + float perc = static_cast(frame_data->current_frame) / static_cast(frame_data->total_frames); + auto progress = perc * usable_rect.w; + render::circle_filled({ static_cast(usable_rect.x + progress), usable_rect.y2() }, 99, gfx::Color::red()); + } +} + void ui::render_video(const Container& container, const AnimatedElement& element) { const auto& video_data = std::get(element.element->data); @@ -99,6 +116,8 @@ void ui::render_video(const Container& container, const AnimatedElement& element fonts::dejavu ); } + + render_video_track(container, element, frame_data); } bool ui::update_video(const Container& container, AnimatedElement& element) { diff --git a/src/gui/ui/ui.h b/src/gui/ui/ui.h index ad2b005d..36da63ff 100644 --- a/src/gui/ui/ui.h +++ b/src/gui/ui/ui.h @@ -434,6 +434,9 @@ namespace ui { void render_image(const Container& container, const AnimatedElement& element); + void render_video_track( + const Container& container, const AnimatedElement& element, std::optional video_track + ); void render_video(const Container& container, const AnimatedElement& element); bool update_video(const Container& container, AnimatedElement& element); void remove_video(AnimatedElement& element); From 3722c969754405822a26c67dd4d89d32e70b8f8c Mon Sep 17 00:00:00 2001 From: f0e <7321764+f0e@users.noreply.github.com> Date: Mon, 1 Sep 2025 17:06:28 +1000 Subject: [PATCH 019/181] refactor: mpv_get_property wrapper --- src/gui/ui/helpers/video.cpp | 33 +++++++++------------------------ src/gui/ui/helpers/video.h | 14 ++++++++++++++ 2 files changed, 23 insertions(+), 24 deletions(-) diff --git a/src/gui/ui/helpers/video.cpp b/src/gui/ui/helpers/video.cpp index 3df454ba..75a18b79 100644 --- a/src/gui/ui/helpers/video.cpp +++ b/src/gui/ui/helpers/video.cpp @@ -281,42 +281,27 @@ void VideoPlayer::process_mpv_events() { } std::optional> VideoPlayer::get_video_dimensions() const { - if (!m_mpv || !m_video_loaded) { - return {}; - } - // TODO: get this from observer instead? - int64_t width = 0; - int64_t height = 0; + auto width = get_property("dwidth", MPV_FORMAT_INT64); + auto height = get_property("dheight", MPV_FORMAT_INT64); - int result1 = mpv_get_property(m_mpv, "dwidth", MPV_FORMAT_INT64, &width); - int result2 = mpv_get_property(m_mpv, "dheight", MPV_FORMAT_INT64, &height); - - if (result1 == 0 && result2 == 0 && width > 0 && height > 0) { - return std::make_pair(static_cast(width), static_cast(height)); - } + if (width && height) + return std::make_pair(static_cast(*width), static_cast(*height)); return {}; } std::optional VideoPlayer::get_video_frame_data() const { - if (!m_mpv || !m_video_loaded) { - return {}; - } - // TODO: get this from observer instead? - int64_t current_frame = 0; - int64_t total_frames = 0; - - int result1 = mpv_get_property(m_mpv, "estimated-frame-number", MPV_FORMAT_INT64, ¤t_frame); - int result2 = mpv_get_property(m_mpv, "estimated-frame-count", MPV_FORMAT_INT64, &total_frames); + auto current_frame = get_property("estimated-frame-number", MPV_FORMAT_INT64); + auto total_frames = get_property("estimated-frame-count", MPV_FORMAT_INT64); - if (result1 == 0 && result2 == 0 && current_frame > 0 && total_frames > 0) { + if (current_frame && total_frames) { return FrameData{ - .current_frame = current_frame, - .total_frames = total_frames, + .current_frame = *current_frame, + .total_frames = *total_frames, }; } diff --git a/src/gui/ui/helpers/video.h b/src/gui/ui/helpers/video.h index 2c70cadd..d1de673b 100644 --- a/src/gui/ui/helpers/video.h +++ b/src/gui/ui/helpers/video.h @@ -74,4 +74,18 @@ class VideoPlayer { void gen_fbo_texture(); void setup_fbo_texture(int w, int h); + + template + std::optional get_property(const std::string& key, mpv_format variable_format) const { + if (!m_mpv || !m_video_loaded) + return {}; + + VariableType data = 0; + int res = mpv_get_property(m_mpv, key.c_str(), variable_format, &data); + + if (res != 0) + return {}; + + return data; + } }; From c0b9e07c573bcfb7794562d0fcf7eca6edb80f97 Mon Sep 17 00:00:00 2001 From: f0e <7321764+f0e@users.noreply.github.com> Date: Mon, 1 Sep 2025 23:02:53 +1000 Subject: [PATCH 020/181] feat: waveform, start/end grabs, more stuff --- src/common/utils.cpp | 48 +++++ src/common/utils.h | 1 + src/gui/components/main.cpp | 10 +- src/gui/render/render.cpp | 80 ++++++++ src/gui/render/render.h | 15 ++ src/gui/ui/elements/video.cpp | 40 +--- src/gui/ui/elements/video_track.cpp | 278 ++++++++++++++++++++++++++++ src/gui/ui/helpers/video.cpp | 67 +++++-- src/gui/ui/helpers/video.h | 98 ++++++++-- src/gui/ui/ui.cpp | 4 + src/gui/ui/ui.h | 24 ++- 11 files changed, 598 insertions(+), 67 deletions(-) create mode 100644 src/gui/ui/elements/video_track.cpp diff --git a/src/common/utils.cpp b/src/common/utils.cpp index b27967b3..c2cf9ad9 100644 --- a/src/common/utils.cpp +++ b/src/common/utils.cpp @@ -380,6 +380,54 @@ u::VideoInfo u::get_video_info(const std::filesystem::path& path) { return info; } +std::vector u::get_video_waveform(const std::filesystem::path& path) { + namespace bp = boost::process; + + bp::ipstream pipe_stream; + bp::child c( + boost::filesystem::path{ blur.ffmpeg_path }, + "-v", + "error", + "-i", + path.string(), + "-f", + "s16le", + "-acodec", + "pcm_s16le", + "-ac", + "1", + "-ar", + "44100", + "-", + bp::std_out > pipe_stream, + bp::std_err.null() +#ifdef _WIN32 + , + bp::windows::create_no_window +#endif + ); + + std::vector samples; + std::vector buffer(4096); + + while (pipe_stream.read(buffer.data(), buffer.size()) || pipe_stream.gcount() > 0) { + auto bytes_read = static_cast(pipe_stream.gcount()); + + // Ensure we read full samples + if (bytes_read % 2 != 0) + --bytes_read; + + std::vector chunk(bytes_read / 2); + std::memcpy(chunk.data(), buffer.data(), bytes_read); + + samples.insert(samples.end(), chunk.begin(), chunk.end()); + } + + c.wait(); + + return samples; +} + bool u::test_hardware_device(const std::string& device_type) { namespace bp = boost::process; diff --git a/src/common/utils.h b/src/common/utils.h index 7d370247..35b6745d 100644 --- a/src/common/utils.h +++ b/src/common/utils.h @@ -421,6 +421,7 @@ namespace u { }; VideoInfo get_video_info(const std::filesystem::path& path); + std::vector get_video_waveform(const std::filesystem::path& path); struct EncodingDevice { std::string type; // "nvidia", "amd", "intel", "mac" diff --git a/src/gui/components/main.cpp b/src/gui/components/main.cpp index 05f6433a..7c46d686 100644 --- a/src/gui/components/main.cpp +++ b/src/gui/components/main.cpp @@ -220,13 +220,21 @@ void main::home_screen(ui::Container& container, float delta_time) { "blur title text", container, title_pos, "blur", gfx::Color::white(), fonts::header_font, FONT_CENTERED_X ); - ui::add_video( + auto video = ui::add_video( "test video", container, tasks::video_player_path, gfx::Size(container.get_usable_rect().w, container.get_usable_rect().h / 2) ); + if (video) { + auto video_rect = (*video)->element->rect; + + const auto& video_data = std::get((*video)->element->data); + + ui::add_video_track("test video track", container, video_rect.w, video_data); + } + if (!initialisation_res) { ui::add_text( "failed to initialise text", diff --git a/src/gui/render/render.cpp b/src/gui/render/render.cpp index cd725b32..240a6f33 100644 --- a/src/gui/render/render.cpp +++ b/src/gui/render/render.cpp @@ -6,6 +6,8 @@ #include #include +#include + #include "../fonts/dejavu_sans.h" #include "../fonts/eb_garamond.h" #include "../fonts/icons.h" @@ -662,6 +664,84 @@ void render::loader(const gfx::Rect& rect, const gfx::Color& color) { text(rect.center(), color, "loading...", fonts::dejavu, FONT_CENTERED_X | FONT_CENTERED_Y); } +void render::waveform( + const gfx::Rect& rect, + const gfx::Rect& active_rect, + const gfx::Color& color, + const std::vector& samples, + bool filled +) { + if (samples.empty()) + return; + + const size_t width = rect.w; + const size_t height = rect.h; + + // Find max amplitude to normalize waveform + int16_t max_sample = 1; + for (auto s : samples) { + max_sample = std::max(std::abs(s), max_sample); + } + + for (size_t x = 0; x < width; ++x) { + // Map pixel x to corresponding sample index + float sample_pos = static_cast(x) / width * samples.size(); + auto idx_start = static_cast(sample_pos); + size_t idx_end = + std::min(static_cast(sample_pos + (samples.size() / static_cast(width))), samples.size()); + + // Take max amplitude in this range + int16_t max_amp = 0; + for (size_t i = idx_start; i < idx_end; ++i) { + max_amp = std::max(max_amp, static_cast(std::abs(samples[i]))); + } + + // Normalize to [0,1] + float norm = static_cast(max_amp) / max_sample; + int y_center = rect.y + (height / 2); + int y_half = static_cast(norm * (height / 2.f)); + + auto x_color = color; + if (!active_rect.contains(gfx::Point(rect.x + x, y_center))) + x_color = color.adjust_alpha(0.5f); + + if (filled) { + gfx::Rect r{ static_cast(rect.x + x), y_center - y_half, 1, y_half * 2 }; + rect_filled(r, x_color); + } + else { + gfx::Point p1{ static_cast(rect.x + x), y_center - y_half }; + gfx::Point p2{ static_cast(rect.x + x), y_center + y_half }; + line(p1, p2, x_color, true); + } + } +} + +void render::rect_side(const gfx::Rect& rect, const gfx::Color& color, RectSide side, int thickness) { + switch (side) { + case RectSide::LEFT: { + // Top horizontal + rect_filled(gfx::Rect{ rect.x, rect.y - thickness, rect.w, thickness }, color); + // Vertical + rect_filled( + gfx::Rect{ rect.x - thickness, rect.y - thickness, thickness, rect.h + (thickness * 2) }, color + ); + // Bottom horizontal + rect_filled(gfx::Rect{ rect.x, rect.y + rect.h, rect.w, thickness }, color); + break; + } + case RectSide::RIGHT: { + // Top horizontal + rect_filled(gfx::Rect{ rect.x, rect.y - thickness, rect.w, thickness }, color); + // Vertical + rect_filled(gfx::Rect{ rect.x + rect.w, rect.y - thickness, thickness, rect.h + (thickness * 2) }, color); + // Bottom horizontal + rect_filled(gfx::Rect{ rect.x, rect.y + rect.h, rect.w, thickness }, color); + break; + } + } +} + void render::push_clip_rect(const gfx::Rect& rect, bool intersect_clip_rect) { imgui.drawlist->PushClipRect(rect.origin(), rect.max(), intersect_clip_rect); } diff --git a/src/gui/render/render.h b/src/gui/render/render.h index 3b15d846..d956c549 100644 --- a/src/gui/render/render.h +++ b/src/gui/render/render.h @@ -241,6 +241,21 @@ namespace render { void loader(const gfx::Rect& rect, const gfx::Color& color); + void waveform( + const gfx::Rect& rect, + const gfx::Rect& active_rect, + const gfx::Color& color, + const std::vector& samples, + bool filled = true + ); + + enum class RectSide { + LEFT, + RIGHT + }; + + void rect_side(const gfx::Rect& rect, const gfx::Color& color, RectSide side, int thickness = 1); + void push_clip_rect(const gfx::Rect& rect, bool intersect_clip_rect = false); void push_clip_rect(int x1, int y1, int x2, int y2, bool intersect_clip_rect = false); void push_fullscreen_clip_rect(); diff --git a/src/gui/ui/elements/video.cpp b/src/gui/ui/elements/video.cpp index e78c86c8..ecc77eb7 100644 --- a/src/gui/ui/elements/video.cpp +++ b/src/gui/ui/elements/video.cpp @@ -1,3 +1,5 @@ +#include + #include "../ui.h" #include "../../render/render.h" #include "../keys.h" @@ -17,11 +19,11 @@ namespace { if (it == video_players.end()) { auto player = std::make_shared(); - player->load_file(key.c_str()); + player->load_file(key); u::log("loaded video from {}", key); auto insert_result = video_players.insert({ key, player }); - it = insert_result.first; // safe because insert always returns valid iterator + it = insert_result.first; } return it->second; @@ -61,23 +63,6 @@ void ui::handle_videos_event(const SDL_Event& event, bool& to_render) { } } -void ui::render_video_track( - const Container& container, const AnimatedElement& element, std::optional frame_data -) { - const auto& video_data = std::get(element.element->data); - float anim = element.animations.at(hasher("main")).current; - - auto usable_rect = element.element->rect.expand(2); - - render::line({ usable_rect.x, usable_rect.y2() }, { usable_rect.x2(), usable_rect.y2() }, gfx::Color::green()); - - if (frame_data) { - float perc = static_cast(frame_data->current_frame) / static_cast(frame_data->total_frames); - auto progress = perc * usable_rect.w; - render::circle_filled({ static_cast(usable_rect.x + progress), usable_rect.y2() }, 99, gfx::Color::red()); - } -} - void ui::render_video(const Container& container, const AnimatedElement& element) { const auto& video_data = std::get(element.element->data); @@ -94,7 +79,7 @@ void ui::render_video(const Container& container, const AnimatedElement& element return; } - auto usable_rect = element.element->rect.shrink(2); // account for border + auto usable_rect = element.element->rect.shrink(3); // account for border // TODO: render::image render::imgui.drawlist->AddImage( @@ -106,18 +91,9 @@ void ui::render_video(const Container& container, const AnimatedElement& element IM_COL32(255, 255, 255, alpha) // apply alpha for fade animations ); - auto frame_data = video_data.player->get_video_frame_data(); - - if (frame_data) { - render::text( - { usable_rect.center().x + 30, usable_rect.y - fonts::dejavu.height() }, - gfx::Color::white(), - std::format("{}/{}", frame_data->current_frame, frame_data->total_frames), - fonts::dejavu - ); - } - - render_video_track(container, element, frame_data); + render::borders( + element.element->rect, gfx::Color(155, 155, 155, stroke_alpha), gfx::Color(80, 80, 80, stroke_alpha) + ); } bool ui::update_video(const Container& container, AnimatedElement& element) { diff --git a/src/gui/ui/elements/video_track.cpp b/src/gui/ui/elements/video_track.cpp new file mode 100644 index 00000000..2e1602fb --- /dev/null +++ b/src/gui/ui/elements/video_track.cpp @@ -0,0 +1,278 @@ +#include "../ui.h" +#include "../keys.h" + +constexpr int MIN_TRACK_WIDTH = 250; +constexpr int TRACK_HEIGHT = 40; +constexpr int GRABS_THICKNESS = 1; +constexpr int GRABS_LENGTH = 5; +constexpr gfx::Color GRABS_COLOR(80, 80, 80); +constexpr gfx::Color GRABS_ACTIVE_COLOR(175, 175, 175); +constexpr gfx::Size GRAB_CLICK_EXPANSION(20, 5); + +namespace { + std::unordered_map> waveforms; + + tl::expected*, std::string> get_waveform(const std::filesystem::path& video_path) { + auto key = video_path.string(); + + try { + auto it = waveforms.find(key); + + if (it == waveforms.end()) { + auto waveform = u::get_video_waveform(key); + + auto insert_result = waveforms.insert({ key, waveform }); + it = insert_result.first; + } + + return &it->second; + } + catch (const std::exception& e) { + u::log_error("failed to load video from {} ({})", key, e.what()); + return tl::unexpected("failed to load video"); + } + } + + void update_progress(ui::AnimatedElement& element) { + auto& video_track_data = std::get(element.element->data); + auto& progress_anim = element.animations.at(ui::hasher("progress")); + + auto progress_percent = video_track_data.video_data.player->get_percent_pos(); + if (progress_percent) + progress_anim.set_goal(*progress_percent / 100.f); + } + + struct GrabRects { + gfx::Rect left; + gfx::Rect right; + }; + + GrabRects get_grab_rects(const ui::AnimatedElement& element) { + float left_grab_percent = element.animations.at(ui::hasher("left_grab_percent")).current; + float right_grab_percent = element.animations.at(ui::hasher("right_grab_percent")).current; + + auto left_grab_rect = element.element->rect; + left_grab_rect.x += left_grab_rect.w * left_grab_percent; + left_grab_rect.w = GRABS_LENGTH; + + auto right_grab_rect = element.element->rect; + right_grab_rect.x += right_grab_rect.w * right_grab_percent - GRABS_LENGTH; + right_grab_rect.w = GRABS_LENGTH; + + return { .left = left_grab_rect, .right = right_grab_rect }; + } +} + +void ui::render_video_track(const Container& container, const AnimatedElement& element) { + const auto& video_track_data = std::get(element.element->data); + + float anim = element.animations.at(hasher("main")).current; + float progress = element.animations.at(hasher("progress")).current; + float seeking = element.animations.at(hasher("seeking")).current; + float seek = element.animations.at(hasher("seek")).current; + float left_grab = element.animations.at(hasher("left_grab")).current; + float right_grab = element.animations.at(hasher("right_grab")).current; + + int stroke_alpha = 125; + + render::rect_filled(element.element->rect, gfx::Color::black(stroke_alpha * anim)); + render::rect_stroke(element.element->rect, gfx::Color(155, 155, 155, stroke_alpha * anim)); + + auto grab_rects = get_grab_rects(element); + + render::rect_side( + grab_rects.left, + gfx::Color::lerp(GRABS_COLOR, GRABS_ACTIVE_COLOR, left_grab).adjust_alpha(anim), + render::RectSide::LEFT, + GRABS_THICKNESS + ); + + render::rect_side( + grab_rects.right, + gfx::Color::lerp(GRABS_COLOR, GRABS_ACTIVE_COLOR, right_grab).adjust_alpha(anim), + render::RectSide::RIGHT, + GRABS_THICKNESS + ); + + // dont show progress when grabbing, its implied that you're at where you're grabbing + anim *= (1.f - left_grab); + anim *= (1.f - right_grab); + + if (!video_track_data.video_data.player || !video_track_data.video_data.player->is_video_ready()) + return; + + auto rect = element.element->rect.shrink(1); + + auto active_rect = rect; + active_rect.x = grab_rects.left.x; + active_rect.w = grab_rects.right.x2() - active_rect.x; + + gfx::Point progress_point = rect.origin(); + progress_point.x = rect.x + (progress * rect.w); + + render::line(progress_point, progress_point.offset_y(rect.h), gfx::Color::white(anim * 255), false, 2.f); + + if (seeking > 0.f) { + gfx::Point seek_point = rect.origin(); + seek_point.x = rect.x + (seek * rect.w); + + render::line(seek_point, seek_point.offset_y(rect.h), gfx::Color::white(75 * anim * seeking), false, 2.f); + } + + render::waveform(rect, active_rect, gfx::Color::white(100), *video_track_data.waveform); +} + +bool ui::update_video_track(const Container& container, AnimatedElement& element) { + auto& video_track_data = std::get(element.element->data); + auto rect = element.element->rect; + + // Get animations once + auto get_anim = [&](const char* name) -> auto& { + return element.animations.at(ui::hasher(name)); + }; + + auto& progress_anim = get_anim("progress"); + auto& seeking_anim = get_anim("seeking"); + auto& seek_anim = get_anim("seek"); + + // Grab handle data + struct GrabHandle { + const char* name{}; + gfx::Rect rect; + ui::AnimationState& anim; + ui::AnimationState& percent_anim; + + bool hovered = false; + }; + + auto grab_rects = get_grab_rects(element); + + std::array grabs = { + GrabHandle{ + .name = "left", + .rect = grab_rects.left.expand(GRAB_CLICK_EXPANSION), + .anim = get_anim("left_grab"), + .percent_anim = get_anim("left_grab_percent"), + }, + GrabHandle{ + .name = "right", + .rect = grab_rects.right.expand(GRAB_CLICK_EXPANSION), + .anim = get_anim("right_grab"), + .percent_anim = get_anim("right_grab_percent"), + }, + }; + + // Process both grab handles + for (auto& grab : grabs) { + std::string action = "video track grab " + std::string(grab.name); + + if (!get_active_element()) { + grab.hovered = grab.rect.contains(keys::mouse_pos) && set_hovered_element(element); + if (grab.hovered) { + set_cursor(SDL_SYSTEM_CURSOR_POINTER); + if (keys::is_mouse_down()) { + set_active_element(element, action); + } + } + } + + if (is_active_element(element, action)) { + if (keys::is_mouse_down()) { + grab.anim.set_goal(1.f); + + float mouse_percent = rect.mouse_percent_x(); + + grab.percent_anim.set_goal(mouse_percent); + + // video_track_data.video_data.player->seek(mouse_percent, true, true); + video_track_data.video_data.player->set_end(mouse_percent * 10.f); + + return true; + } + else { + video_track_data.video_data.player->set_paused(true); + + reset_active_element(); + } + } + + grab.anim.set_goal(grab.hovered ? 0.5f : 0.f); + } + + bool hovered = + !grabs[0].hovered && !grabs[1].hovered && rect.contains(keys::mouse_pos) && set_hovered_element(element); + bool active = get_active_element() == &element; + + if (hovered) { + // set_cursor(SDL_SYSTEM_CURSOR_POINTER); + if (keys::is_mouse_down()) + set_active_element(element, "video track"); + } + + if (is_active_element(element, "video track")) { + if (keys::is_mouse_down()) { + seeking_anim.set_goal(1.f); + + float mouse_percent = rect.mouse_percent_x(); + + video_track_data.video_data.player->seek(mouse_percent, true, false); + + if (seeking_anim.current == 0.f) // new seek, start from current. otherwise smoothly animate from last seek + seek_anim.current = progress_anim.current; + + seek_anim.set_goal(mouse_percent); + + return true; + } + else { + reset_active_element(); + } + } + + if (!video_track_data.video_data.player->get_queued_seek()) + seeking_anim.set_goal(0.f); + + return false; +} + +ui::AnimatedElement* ui::add_video_track( + const std::string& id, Container& container, int width, const VideoElementData& video_data +) { + gfx::Size size(std::max(MIN_TRACK_WIDTH, width), TRACK_HEIGHT); + + auto waveform = get_waveform(video_data.video_path); + if (!waveform) + return {}; + + Element element( + id, + ElementType::VIDEO_TRACK, + gfx::Rect(container.current_position, size), + VideoTrackElementData{ + .video_data = video_data, + .waveform = *waveform, + }, + render_video_track, + update_video_track + ); + + auto* elem = add_element( + container, + std::move(element), + container.element_gap, + { + { hasher("main"), AnimationState(25.f) }, + { hasher("progress"), AnimationState(70.f) }, + { hasher("seeking"), AnimationState(70.f) }, + { hasher("seek"), AnimationState(70.f) }, + { hasher("left_grab"), AnimationState(150.f) }, + { hasher("left_grab_percent"), AnimationState(150.f) }, + { hasher("right_grab"), AnimationState(150.f) }, + { hasher("right_grab_percent"), AnimationState(150.f, 1.f) }, + } + ); + + update_progress(*elem); + + return elem; +} diff --git a/src/gui/ui/helpers/video.cpp b/src/gui/ui/helpers/video.cpp index 75a18b79..4ccbb9e8 100644 --- a/src/gui/ui/helpers/video.cpp +++ b/src/gui/ui/helpers/video.cpp @@ -1,6 +1,10 @@ #include "video.h" VideoPlayer::~VideoPlayer() { + m_thread_exit = true; + if (m_mpv_thread.joinable()) + m_mpv_thread.join(); + // clean up opengl resources if (m_tex) { glDeleteTextures(1, &m_tex); @@ -24,14 +28,13 @@ VideoPlayer::~VideoPlayer() { void VideoPlayer::handle_key_press(SDL_Keycode key) { if (key == SDLK_SPACE) { - std::array cmd = { "cycle", "pause", nullptr }; - mpv_command_async(m_mpv, 0, cmd.data()); + run_command_async({ "cycle", "pause" }); } } -void VideoPlayer::load_file(const char* file_path) { - std::array cmd = { "loadfile", file_path, nullptr }; - mpv_command_async(m_mpv, 0, cmd.data()); +void VideoPlayer::load_file(const std::filesystem::path& file_path) { + run_command_async({ "loadfile", file_path }); + m_video_loaded = false; // reset video loaded state } @@ -154,6 +157,7 @@ void VideoPlayer::initialize_mpv() { mpv_set_option_string(m_mpv, "hwdec", "auto-safe"); // enable hardware decoding mpv_set_option_string(m_mpv, "profile", "gpu-hq"); // high quality profile mpv_set_option_string(m_mpv, "keep-open", "yes"); + mpv_set_option_string(m_mpv, "mute", "yes"); int result = mpv_initialize(m_mpv); if (result < 0) { @@ -214,6 +218,41 @@ void VideoPlayer::initialize_mpv() { }, this ); + + m_thread_exit = false; + m_mpv_thread = std::thread(&VideoPlayer::mpv_thread, this); +} + +void VideoPlayer::mpv_thread() { + while (!blur.exiting && !m_thread_exit) { + if (!m_mpv || !m_video_loaded) { + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + continue; + } + + { + std::unique_lock lock(m_mutex); + if (m_queued_seek) { + auto seek = *m_queued_seek; + m_queued_seek = {}; + + if (!m_last_seek || *m_last_seek != seek) { + m_last_seek = seek; + lock.unlock(); + + std::string flags = "absolute-percent"; + if (seek.exact) + flags += "+exact"; + + run_command({ "seek", std::to_string(seek.time * 100), flags }); + + // mpv_set_property_async(m_mpv, 0, "percent-pos", MPV_FORMAT_DOUBLE, &seek_to); + } + } + } + + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + } } void VideoPlayer::on_mpv_events() { @@ -292,20 +331,12 @@ std::optional> VideoPlayer::get_video_dimensions() const { return {}; } -std::optional VideoPlayer::get_video_frame_data() const { - // TODO: get this from observer instead? - - auto current_frame = get_property("estimated-frame-number", MPV_FORMAT_INT64); - auto total_frames = get_property("estimated-frame-count", MPV_FORMAT_INT64); - - if (current_frame && total_frames) { - return FrameData{ - .current_frame = *current_frame, - .total_frames = *total_frames, - }; - } +std::optional VideoPlayer::get_percent_pos() const { + return get_property("percent-pos", MPV_FORMAT_DOUBLE); +} - return {}; +std::optional VideoPlayer::get_duration() const { + return get_property("duration", MPV_FORMAT_DOUBLE); } bool VideoPlayer::is_video_ready() const { diff --git a/src/gui/ui/helpers/video.h b/src/gui/ui/helpers/video.h index d1de673b..21674f0a 100644 --- a/src/gui/ui/helpers/video.h +++ b/src/gui/ui/helpers/video.h @@ -5,16 +5,17 @@ #include #include -struct FrameData { - int64_t current_frame = 0; - int64_t total_frames = 0; +struct Seek { + float time; + bool exact; + + bool operator==(const Seek& other) const = default; }; class VideoPlayer { public: VideoPlayer() - : m_wakeup_on_mpv_render_update(SDL_RegisterEvents(1)), m_wakeup_on_mpv_events(SDL_RegisterEvents(1)), m_fbo(0), - m_tex(0), m_current_width(0), m_current_height(0), m_video_loaded(false) { + : m_wakeup_on_mpv_render_update(SDL_RegisterEvents(1)), m_wakeup_on_mpv_events(SDL_RegisterEvents(1)) { if (m_wakeup_on_mpv_render_update == static_cast(-1) || m_wakeup_on_mpv_events == static_cast(-1)) { @@ -34,7 +35,7 @@ class VideoPlayer { void handle_key_press(SDL_Keycode key); - void load_file(const char* file_path); + void load_file(const std::filesystem::path& file_path); bool render(int w, int h); @@ -45,26 +46,74 @@ class VideoPlayer { void handle_mpv_event(const SDL_Event& event, bool& redraw); [[nodiscard]] std::optional> get_video_dimensions() const; - [[nodiscard]] std::optional get_video_frame_data() const; + [[nodiscard]] std::optional get_percent_pos() const; + [[nodiscard]] std::optional get_duration() const; + + [[nodiscard]] std::optional get_paused() const { + return get_property("pause", MPV_FORMAT_FLAG); + } [[nodiscard]] bool is_video_ready() const; + void seek(float time, bool exact, bool pause) { + std::lock_guard lock(m_mutex); + m_queued_seek = Seek{ + .time = time, + .exact = exact, + }; + } + + void set_paused(bool paused) { + run_command_async({ "set", "pause", paused ? "yes" : "no" }); + } + + void resume() { + run_command_async({ "set", "pause", "no" }); + } + + void set_end(float percent) { + m_end_percent = percent; + } + + std::optional get_queued_seek() { + std::lock_guard lock(m_mutex); + return m_queued_seek; + } + + // std::optional get_seek() { + // std::lock_guard lock(m_mutex); + // if (m_queued_seek) + // return m_queued_seek; + // return m_last_seek; + // } + private: mpv_handle* m_mpv = nullptr; mpv_render_context* m_mpv_gl = nullptr; Uint32 m_wakeup_on_mpv_render_update; Uint32 m_wakeup_on_mpv_events; - GLuint m_fbo; - GLuint m_tex; + GLuint m_fbo{}; + GLuint m_tex{}; + + int m_current_width{}; + int m_current_height{}; - int m_current_width; - int m_current_height; + bool m_video_loaded{}; - bool m_video_loaded; + std::mutex m_mutex; + std::optional m_queued_seek; + std::optional m_last_seek; + + std::thread m_mpv_thread; + std::atomic m_thread_exit{ false }; + + float m_end_percent{}; void initialize_mpv(); + void mpv_thread(); + void on_mpv_events(); void on_mpv_render_update(); @@ -88,4 +137,29 @@ class VideoPlayer { return data; } + + void run_command_impl(const std::vector& command, bool async) { + std::vector cmd; + cmd.reserve(command.size() + 1); + + for (const auto& s : command) { + cmd.push_back(s.c_str()); + } + cmd.push_back(nullptr); + + if (async) { + mpv_command_async(m_mpv, 0, cmd.data()); + } + else { + mpv_command(m_mpv, cmd.data()); + } + } + + void run_command_async(const std::vector& command) { + run_command_impl(command, true); + } + + void run_command(const std::vector& command) { + run_command_impl(command, false); + } }; diff --git a/src/gui/ui/ui.cpp b/src/gui/ui/ui.cpp index 6ab249ec..86e93aa4 100644 --- a/src/gui/ui/ui.cpp +++ b/src/gui/ui/ui.cpp @@ -258,6 +258,10 @@ std::string ui::get_active_element_type() { return active_element_type; } +bool ui::is_active_element(const AnimatedElement& element, const std::string& type) { + return active_element == &element && active_element_type == type; +} + void ui::reset_active_element() { active_element = nullptr; active_element_type = ""; diff --git a/src/gui/ui/ui.h b/src/gui/ui/ui.h index 36da63ff..4ca63a54 100644 --- a/src/gui/ui/ui.h +++ b/src/gui/ui/ui.h @@ -35,6 +35,7 @@ namespace ui { WEIGHTING_GRAPH, TABS, HINT, + VIDEO_TRACK }; struct BarElementData { @@ -272,6 +273,15 @@ namespace ui { } }; + struct VideoTrackElementData { + VideoElementData video_data; + std::vector* waveform; + + bool operator==(const VideoTrackElementData& other) const { + return video_data == other.video_data && waveform == other.waveform; + } + }; + using ElementData = std::variant< BarElementData, TextElementData, @@ -286,7 +296,8 @@ namespace ui { SeparatorElementData, WeightingGraphElementData, TabsElementData, - HintElementData>; + HintElementData, + VideoTrackElementData>; struct AnimationState { float speed; @@ -434,9 +445,6 @@ namespace ui { void render_image(const Container& container, const AnimatedElement& element); - void render_video_track( - const Container& container, const AnimatedElement& element, std::optional video_track - ); void render_video(const Container& container, const AnimatedElement& element); bool update_video(const Container& container, AnimatedElement& element); void remove_video(AnimatedElement& element); @@ -472,6 +480,9 @@ namespace ui { void render_hint(const Container& container, const AnimatedElement& element); + void render_video_track(const Container& container, const AnimatedElement& element); + bool update_video_track(const Container& container, AnimatedElement& element); + void reset_container( Container& container, SDL_Window* window, @@ -654,6 +665,10 @@ namespace ui { const render::Font& font ); + AnimatedElement* add_video_track( + const std::string& id, Container& container, int width, const VideoElementData& video_data + ); + AnimatedElement* add_separator(const std::string& id, Container& container, SeparatorStyle style); void add_spacing(Container& container, int spacing); @@ -669,6 +684,7 @@ namespace ui { void set_active_element(AnimatedElement& element, const std::string& type = ""); AnimatedElement* get_active_element(); std::string get_active_element_type(); + bool is_active_element(const AnimatedElement& element, const std::string& type = ""); void reset_active_element(); bool set_hovered_element(AnimatedElement& element); From 040672c19d48f7864fd9b58960768e83195a5a51 Mon Sep 17 00:00:00 2001 From: f0e <7321764+f0e@users.noreply.github.com> Date: Mon, 1 Sep 2025 23:06:13 +1000 Subject: [PATCH 021/181] fix: stay pointer when dragging grab & little refactor --- src/gui/ui/elements/video_track.cpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/gui/ui/elements/video_track.cpp b/src/gui/ui/elements/video_track.cpp index 2e1602fb..0dddc61d 100644 --- a/src/gui/ui/elements/video_track.cpp +++ b/src/gui/ui/elements/video_track.cpp @@ -166,13 +166,13 @@ bool ui::update_video_track(const Container& container, AnimatedElement& element for (auto& grab : grabs) { std::string action = "video track grab " + std::string(grab.name); - if (!get_active_element()) { - grab.hovered = grab.rect.contains(keys::mouse_pos) && set_hovered_element(element); - if (grab.hovered) { - set_cursor(SDL_SYSTEM_CURSOR_POINTER); - if (keys::is_mouse_down()) { - set_active_element(element, action); - } + grab.hovered = grab.rect.contains(keys::mouse_pos) && set_hovered_element(element); + + if (grab.hovered) { + set_cursor(SDL_SYSTEM_CURSOR_POINTER); + + if (!get_active_element() && keys::is_mouse_down()) { + set_active_element(element, action); } } From 643412bde588bac4e9556b7f429d46fec4540f78 Mon Sep 17 00:00:00 2001 From: f0e <7321764+f0e@users.noreply.github.com> Date: Mon, 1 Sep 2025 23:15:33 +1000 Subject: [PATCH 022/181] feat: tweaking stuff --- src/gui/ui/elements/video_track.cpp | 6 ++++-- src/gui/ui/helpers/video.cpp | 16 ++++++++++------ 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/gui/ui/elements/video_track.cpp b/src/gui/ui/elements/video_track.cpp index 0dddc61d..76e6bbd0 100644 --- a/src/gui/ui/elements/video_track.cpp +++ b/src/gui/ui/elements/video_track.cpp @@ -7,7 +7,7 @@ constexpr int GRABS_THICKNESS = 1; constexpr int GRABS_LENGTH = 5; constexpr gfx::Color GRABS_COLOR(80, 80, 80); constexpr gfx::Color GRABS_ACTIVE_COLOR(175, 175, 175); -constexpr gfx::Size GRAB_CLICK_EXPANSION(20, 5); +constexpr gfx::Size GRAB_CLICK_EXPANSION(15, 5); namespace { std::unordered_map> waveforms; @@ -180,7 +180,9 @@ bool ui::update_video_track(const Container& container, AnimatedElement& element if (keys::is_mouse_down()) { grab.anim.set_goal(1.f); - float mouse_percent = rect.mouse_percent_x(); + float mouse_percent = + rect.mouse_percent_x(); // TODO: when you initially click it if you arent exactly at the right spot + // itll shift the grab a little which is annoying grab.percent_anim.set_goal(mouse_percent); diff --git a/src/gui/ui/helpers/video.cpp b/src/gui/ui/helpers/video.cpp index 4ccbb9e8..8f621d50 100644 --- a/src/gui/ui/helpers/video.cpp +++ b/src/gui/ui/helpers/video.cpp @@ -152,12 +152,16 @@ void VideoPlayer::initialize_mpv() { throw std::runtime_error("MPV context creation failed"); } - // configure mpv - mpv_set_option_string(m_mpv, "vo", "libmpv"); - mpv_set_option_string(m_mpv, "hwdec", "auto-safe"); // enable hardware decoding - mpv_set_option_string(m_mpv, "profile", "gpu-hq"); // high quality profile - mpv_set_option_string(m_mpv, "keep-open", "yes"); - mpv_set_option_string(m_mpv, "mute", "yes"); + // required + mpv_set_option_string( + m_mpv, "vo", "libmpv" + ); // note: this is slower than gpu, but cant do anything abt it - https://github.com/mpv-player/mpv/issues/6829 + + // options + mpv_set_option_string(m_mpv, "hwdec", "yes"); + mpv_set_option_string(m_mpv, "profile", "fast"); + mpv_set_option_string(m_mpv, "keep-open", "yes"); // dont close when finished + // mpv_set_option_string(m_mpv, "mute", "yes"); int result = mpv_initialize(m_mpv); if (result < 0) { From 323f5deedfb2bfc34543397168b71ae898a6ad77 Mon Sep 17 00:00:00 2001 From: f0e <7321764+f0e@users.noreply.github.com> Date: Tue, 2 Sep 2025 00:31:36 +1000 Subject: [PATCH 023/181] feat: trim and pending queue --- src/common/rendering.cpp | 30 ++++-- src/common/rendering.h | 12 +++ src/common/utils.cpp | 7 +- src/common/utils.h | 1 + src/gui/components/main.cpp | 141 ++++++++++++++++++---------- src/gui/components/main.h | 15 ++- src/gui/gui.cpp | 3 +- src/gui/renderer.cpp | 60 ++++++++---- src/gui/tasks.cpp | 117 +++++++++++++++-------- src/gui/tasks.h | 15 ++- src/gui/ui/elements/video_track.cpp | 29 +++--- src/gui/ui/helpers/video.cpp | 1 + src/gui/ui/ui.h | 12 ++- 13 files changed, 299 insertions(+), 144 deletions(-) diff --git a/src/common/rendering.cpp b/src/common/rendering.cpp index 2cbe1c9b..637aa172 100644 --- a/src/common/rendering.cpp +++ b/src/common/rendering.cpp @@ -459,6 +459,8 @@ tl::expected rendering::detail::render_vid const std::shared_ptr& state, const GlobalAppSettings& app_settings, const std::optional& output_path_override, + float start, + float end, const std::function& progress_callback ) { if (!blur.initialised) @@ -491,13 +493,23 @@ tl::expected rendering::detail::render_vid auto vspipe_args = detail::build_base_vspipe_args(input_path, *merged_settings); vspipe_args.insert( vspipe_args.end() - 2, - { // insert before script path and "-" - "-a", - std::format("fps_num={}", video_info.fps_num), - "-a", - std::format("fps_den={}", video_info.fps_den), - "-a", - "color_range=" + (video_info.color_range ? *video_info.color_range : "undefined") } + { + // insert before script path and "-" + "-a", + std::format("fps_num={}", video_info.fps_num), + "-a", + std::format("fps_den={}", video_info.fps_den), + "-a", + "color_range=" + (video_info.color_range ? *video_info.color_range : "undefined"), + "-s", + std::to_string( + int((double)settings.blur_output_fps * video_info.duration * start) + ), // TODO MR: make this exact? + "-e", + std::to_string( + int((double)settings.blur_output_fps * video_info.duration * end) + ), // TODO MR: make this exact? + } ); // build ffmpeg command @@ -596,6 +608,8 @@ rendering::QueueAddRes rendering::VideoRenderQueue::add( const std::optional& config_path, const GlobalAppSettings& app_settings, const std::optional& output_path_override, + float start, + float end, const std::function& progress_callback, const std::function< void(const VideoRenderDetails& render, const tl::expected& result)>& @@ -616,6 +630,8 @@ rendering::QueueAddRes rendering::VideoRenderQueue::add( .settings = config_res.config, .app_settings = app_settings, .output_path_override = output_path_override, + .start = start, + .end = end, .progress_callback = progress_callback, .finish_callback = finish_callback, } diff --git a/src/common/rendering.h b/src/common/rendering.h index 08f782a7..823c3d7b 100644 --- a/src/common/rendering.h +++ b/src/common/rendering.h @@ -38,6 +38,8 @@ namespace rendering { const std::shared_ptr& state, const GlobalAppSettings& app_settings, const std::optional& output_path_override, + float start, + float end, const std::function& progress_callback ); } @@ -108,6 +110,8 @@ namespace rendering { const std::shared_ptr& state, const GlobalAppSettings& app_settings, const std::optional& output_path_override, + float start, + float end, const std::function& progress_callback ); @@ -128,6 +132,10 @@ namespace rendering { BlurSettings settings; GlobalAppSettings app_settings; std::optional output_path_override; + + float start; + float end; + std::function progress_callback; std::function< void(const VideoRenderDetails& render, const tl::expected& result)> @@ -203,6 +211,8 @@ namespace rendering { const std::optional& config_path = {}, const GlobalAppSettings& app_settings = config_app::get_app_config(), const std::optional& output_path_override = {}, + float start = 0.f, + float end = 1.f, const std::function& progress_callback = {}, const std::function& result @@ -222,6 +232,8 @@ namespace rendering { cur.state, cur.app_settings, cur.output_path_override, + cur.start, + cur.end, cur.progress_callback ); diff --git a/src/common/utils.cpp b/src/common/utils.cpp index c2cf9ad9..29f8add3 100644 --- a/src/common/utils.cpp +++ b/src/common/utils.cpp @@ -313,7 +313,6 @@ u::VideoInfo u::get_video_info(const std::filesystem::path& path) { VideoInfo info; bool has_video_stream = false; - double duration = 0.0; std::string codec_name; std::string line; @@ -328,10 +327,10 @@ u::VideoInfo u::get_video_info(const std::filesystem::path& path) { } else if (line.find("duration=") != std::string::npos) { try { - duration = std::stod(line.substr(line.find('=') + 1)); + info.duration = std::stod(line.substr(line.find('=') + 1)); } catch (...) { - duration = 0.0; + info.duration = 0.0; } } else if (line.find("color_range=") != std::string::npos) { @@ -371,7 +370,7 @@ u::VideoInfo u::get_video_info(const std::filesystem::path& path) { // 2. Either it has a non-zero duration or it's an animated format // Static images will typically have duration=0 or N/A bool is_animated_format = u::contains(codec_name, "gif") || u::contains(codec_name, "webp"); - info.has_video_stream = has_video_stream && (duration > 0.1 || is_animated_format); + info.has_video_stream = has_video_stream && (info.duration > 0.1 || is_animated_format); if (info.sample_rate == -1) { // todo: throw? diff --git a/src/common/utils.h b/src/common/utils.h index 35b6745d..91f5d29c 100644 --- a/src/common/utils.h +++ b/src/common/utils.h @@ -418,6 +418,7 @@ namespace u { int sample_rate = -1; int fps_num = -1; int fps_den = -1; + float duration = 0.f; }; VideoInfo get_video_info(const std::filesystem::path& path); diff --git a/src/gui/components/main.cpp b/src/gui/components/main.cpp index 7c46d686..93d2f6b5 100644 --- a/src/gui/components/main.cpp +++ b/src/gui/components/main.cpp @@ -22,7 +22,7 @@ void main::open_files_button(ui::Container& container, const std::string& label) wpaths.emplace_back(u::string_to_path(*f)); } - tasks::set_video_player_path(wpaths[0]); + tasks::add_files(wpaths); } }; @@ -44,7 +44,7 @@ void main::open_files_button(ui::Container& container, const std::string& label) }); }; -void main::render_screen( +void main::render_progress( ui::Container& container, const rendering::VideoRenderDetails& render, size_t render_index, @@ -202,78 +202,115 @@ void main::render_screen( } } -void main::home_screen(ui::Container& container, float delta_time) { - static float bar_percent = 0.f; +void main::render_pending(ui::Container& container, const std::vector>& pending) { + size_t pending_index = 0; - const auto& queue = rendering::video_render_queue.get_queue_copy(); + auto& pending_video = pending[pending_index]; + + std::string render_title_text = u::path_to_string(pending_video->video_path.stem()); + + int queue_size = pending.size() + tasks::finished_renders; + if (queue_size > 1) { + render_title_text = std::format("{} ({}/{})", render_title_text, pending_index + 1, queue_size); + } + + ui::add_text( + std::format("video {} name text", pending_index), + container, + render_title_text, + gfx::Color::white(), + fonts::smaller_header_font, + FONT_CENTERED_X + ); - if (queue.empty()) { - bar_percent = 0.f; + auto video = ui::add_video( + "test video", + container, + pending_video->video_path, + gfx::Size(container.get_usable_rect().w, container.get_usable_rect().h / 2) + ); - gfx::Point title_pos = container.get_usable_rect().center(); - if (container.rect.h > 275) - title_pos.y = int(renderer::PAD_Y + fonts::header_font.height()); - else - title_pos.y = 10 + fonts::header_font.height(); + if (video) { + auto video_rect = (*video)->element->rect; - ui::add_text_fixed( - "blur title text", container, title_pos, "blur", gfx::Color::white(), fonts::header_font, FONT_CENTERED_X + const auto& video_data = std::get((*video)->element->data); + + ui::add_video_track( + "test video track", container, video_rect.w, video_data, pending_video->start, pending_video->end ); + } +} + +void main::render_home(ui::Container& container) { + gfx::Point title_pos = container.get_usable_rect().center(); + if (container.rect.h > 275) + title_pos.y = int(renderer::PAD_Y + fonts::header_font.height()); + else + title_pos.y = 10 + fonts::header_font.height(); + + ui::add_text_fixed( + "blur title text", container, title_pos, "blur", gfx::Color::white(), fonts::header_font, FONT_CENTERED_X + ); - auto video = ui::add_video( - "test video", + if (!initialisation_res) { + ui::add_text( + "failed to initialise text", container, - tasks::video_player_path, - gfx::Size(container.get_usable_rect().w, container.get_usable_rect().h / 2) + "Failed to initialise", + gfx::Color::white(), + fonts::dejavu, + FONT_CENTERED_X ); - if (video) { - auto video_rect = (*video)->element->rect; - - const auto& video_data = std::get((*video)->element->data); + ui::add_text( + "failed to initialise reason", + container, + initialisation_res.error(), + gfx::Color::white(renderer::MUTED_SHADE), + fonts::dejavu, + FONT_CENTERED_X + ); - ui::add_video_track("test video track", container, video_rect.w, video_data); - } + return; + } - if (!initialisation_res) { - ui::add_text( - "failed to initialise text", - container, - "Failed to initialise", - gfx::Color::white(), - fonts::dejavu, - FONT_CENTERED_X - ); + open_files_button(container, "Open files"); - ui::add_text( - "failed to initialise reason", - container, - initialisation_res.error(), - gfx::Color::white(renderer::MUTED_SHADE), - fonts::dejavu, - FONT_CENTERED_X - ); + ui::add_text( + "drop file text", container, "or drop them anywhere", gfx::Color::white(), fonts::dejavu, FONT_CENTERED_X + ); +} - return; - } +main::MainScreen main::screen(ui::Container& container, float delta_time) { + static float bar_percent = 0.f; - open_files_button(container, "Open files"); + const auto& pending = tasks::get_pending_copy(); - ui::add_text( - "drop file text", container, "or drop them anywhere", gfx::Color::white(), fonts::dejavu, FONT_CENTERED_X - ); + if (pending.size() > 0) { + render_pending(container, pending); + return MainScreen::PENDING; } - else { + + const auto& queue = rendering::video_render_queue.get_queue_copy(); + + if (!queue.empty()) { bool is_progress_shown = false; for (const auto [i, render] : u::enumerate(queue)) { bool current = i == 0; - render_screen(container, render, i, current, delta_time, is_progress_shown, bar_percent); + render_progress(container, render, i, current, delta_time, is_progress_shown, bar_percent); } - if (!is_progress_shown) { - bar_percent = 0.f; // Reset when no progress bar is shown - } + if (!is_progress_shown) + bar_percent = 0.f; + + return MainScreen::PROGRESS; } + + bar_percent = 0.f; + + render_home(container); + + return MainScreen::HOME; } diff --git a/src/gui/components/main.h b/src/gui/components/main.h index a5c48d84..d351c6b0 100644 --- a/src/gui/components/main.h +++ b/src/gui/components/main.h @@ -2,11 +2,18 @@ #include "common/rendering.h" #include "../ui/ui.h" +#include "../tasks.h" namespace gui::components::main { void open_files_button(ui::Container& container, const std::string& label); - void render_screen( + enum class MainScreen { + PROGRESS, + PENDING, + HOME + }; + + void render_progress( ui::Container& container, const rendering::VideoRenderDetails& render, size_t render_index, @@ -16,5 +23,9 @@ namespace gui::components::main { float& bar_percent ); - void home_screen(ui::Container& container, float delta_time); + void render_pending(ui::Container& container, const std::vector>& pending); + + void render_home(ui::Container& container); + + MainScreen screen(ui::Container& container, float delta_time); } diff --git a/src/gui/gui.cpp b/src/gui/gui.cpp index be96a843..78229a50 100644 --- a/src/gui/gui.cpp +++ b/src/gui/gui.cpp @@ -63,8 +63,7 @@ int gui::run() { } } - // tasks::add_files_for_render(paths); - tasks::set_video_player_path(paths[0]); + tasks::add_files(paths); break; } diff --git a/src/gui/renderer.cpp b/src/gui/renderer.cpp index d615673c..65b5eee9 100644 --- a/src/gui/renderer.cpp +++ b/src/gui/renderer.cpp @@ -140,28 +140,54 @@ bool gui::renderer::redraw_window(bool rendered_last, bool want_to_render) { case Screens::MAIN: { components::configs::loaded_config = false; - components::main::home_screen(main_container, delta_time); + auto main_screen = components::main::screen(main_container, delta_time); if (initialisation_res) { - auto current_render = rendering::video_render_queue.front(); - if (current_render) { - ui::add_button( - current_render->state->is_paused() ? "resume render button" : "pause render button", - nav_container, - current_render->state->is_paused() ? "Resume" : "Pause", - fonts::dejavu, - [¤t_render] { - current_render->state->toggle_pause(); + switch (main_screen) { + case components::main::MainScreen::HOME: { + break; + } + case components::main::MainScreen::PENDING: { + const auto& pending = tasks::get_pending_copy(); + + if (pending.size() > 0) { + if (pending[0]->video_info) { + ui::add_button("go button", nav_container, "Go bro", fonts::dejavu, [] { + tasks::start_pending_video(0); + }); + } } - ); - ui::set_next_same_line(nav_container); - ui::add_button("stop render button", nav_container, "Cancel", fonts::dejavu, [¤t_render] { - current_render->state->stop(); - }); + components::main::open_files_button(nav_container, "Add files"); + + break; + } + case components::main::MainScreen::PROGRESS: { + auto current_render = rendering::video_render_queue.front(); + if (current_render) { + ui::add_button( + current_render->state->is_paused() ? "resume render button" : "pause render button", + nav_container, + current_render->state->is_paused() ? "Resume" : "Pause", + fonts::dejavu, + [] { + auto current_render = rendering::video_render_queue.front(); + current_render->state->toggle_pause(); + } + ); + } + + ui::set_next_same_line(nav_container); + ui::add_button("stop render button", nav_container, "Cancel", fonts::dejavu, [] { + auto current_render = rendering::video_render_queue.front(); + current_render->state->stop(); + }); + + ui::set_next_same_line(nav_container); + components::main::open_files_button(nav_container, "Add files"); - ui::set_next_same_line(nav_container); - components::main::open_files_button(nav_container, "Add files"); + break; + } } ui::set_next_same_line(nav_container); diff --git a/src/gui/tasks.cpp b/src/gui/tasks.cpp index ed6bc0c8..1f376d0e 100644 --- a/src/gui/tasks.cpp +++ b/src/gui/tasks.cpp @@ -12,9 +12,9 @@ #include "components/configs/configs.h" namespace { - std::filesystem::path video_player_path; - std::vector pending_video_paths; - std::mutex pending_video_paths_mutex; + size_t pending_index = 0; + std::vector> pending_videos; + std::mutex pending_videos_mutex; } void tasks::run(const std::vector& arguments) { @@ -79,15 +79,15 @@ void tasks::run(const std::vector& arguments) { paths.emplace_back(u::string_to_path(argument)); } - add_files_for_render(paths); // todo: mac packaged app support (& linux? does it work?) + add_files(paths); // todo: mac packaged app support (& linux? does it work?) - std::thread([] { + std::thread video_info_thread([] { while (!blur.exiting) { process_pending_files(); std::this_thread::sleep_for(std::chrono::milliseconds(50)); } - }).detach(); + }); while (!blur.exiting) { if (!rendering::video_render_queue.process_next()) { @@ -98,10 +98,12 @@ void tasks::run(const std::vector& arguments) { finished_renders++; } } + + video_info_thread.join(); } -void tasks::add_files_for_render(const std::vector& path_strs) { - std::lock_guard lock(pending_video_paths_mutex); +void tasks::add_files(const std::vector& path_strs) { + std::lock_guard lock(pending_videos_mutex); for (const auto& path_str : path_strs) { std::filesystem::path path = std::filesystem::canonical(path_str); @@ -116,53 +118,48 @@ void tasks::add_files_for_render(const std::vector& path_ ); } - pending_video_paths.push_back(path); + pending_videos.push_back( + std::make_shared(PendingVideo{ + .video_path = path, + }) + ); } } void tasks::process_pending_files() { - std::vector video_paths_to_process; + std::unique_lock lock(pending_videos_mutex); - { - std::lock_guard lock(pending_video_paths_mutex); - if (pending_video_paths.empty()) - return; + // Find first video without info + auto it = std::ranges::find_if(pending_videos, [](const auto& pv) { + return !pv->video_info.has_value(); + }); - video_paths_to_process = std::move(pending_video_paths); - pending_video_paths.clear(); + if (it == pending_videos.end()) { + return; // No videos need processing } - auto app_config = config_app::get_app_config(); + auto video_path = (*it)->video_path; // Copy the path + auto index = std::ranges::distance(pending_videos.begin(), it); + lock.unlock(); // Release lock while doing expensive I/O + + // Get video info (this is the slow operation) + auto video_info = u::get_video_info(video_path); - for (auto& video_path : video_paths_to_process) { - auto video_info = u::get_video_info(video_path); + // Lock again to update the result + lock.lock(); + // Make sure the vector hasn't been modified in a way that invalidates our index + if (index < pending_videos.size() && pending_videos[index]->video_path == video_path) { if (!video_info.has_video_stream) { gui::components::notifications::add( std::format("File is not a valid video or is unreadable: {}", video_path.filename()), ui::NotificationType::NOTIF_ERROR ); - continue; + // Remove invalid video from queue + pending_videos.erase(pending_videos.begin() + index); } - - auto queue_config_res = rendering::video_render_queue.add( - video_path, - video_info, - {}, - config_app::get_app_config(), - {}, - {}, - [](const rendering::VideoRenderDetails& render, - const tl::expected& result) { - gui::renderer::on_render_finished(render, result); - } - ); - - if (app_config.notify_about_config_override) { - if (!queue_config_res.is_global_config) - gui::components::notifications::add( - "Using override config from video folder", ui::NotificationType::INFO - ); + else { + pending_videos[index]->video_info = video_info; } } } @@ -182,6 +179,44 @@ void tasks::add_sample_video(const std::filesystem::path& path_str) { gui::components::configs::just_added_sample_video = true; } -void tasks::set_video_player_path(const std::filesystem::path& path_str) { - video_player_path = path_str; +void tasks::start_pending_video(size_t index) { + std::lock_guard lock(pending_videos_mutex); + + if (index >= pending_videos.size()) { + // TODO MR: handle + return; + } + + auto pending_video = std::move(pending_videos[index]); + pending_videos.erase(pending_videos.begin() + index); + + if (!pending_video->video_info) + return; + + auto app_config = config_app::get_app_config(); + + auto queue_config_res = rendering::video_render_queue.add( + pending_video->video_path, + *pending_video->video_info, + {}, + app_config, + {}, + pending_video->start, + pending_video->end, + {}, + [](const rendering::VideoRenderDetails& render, + const tl::expected& result) { + gui::renderer::on_render_finished(render, result); + } + ); + + // TODO MR: show this in pending screen + if (app_config.notify_about_config_override) { + if (!queue_config_res.is_global_config) + gui::components::notifications::add("Using override config from video folder", ui::NotificationType::INFO); + } +} + +std::vector> tasks::get_pending_copy() { + return pending_videos; } diff --git a/src/gui/tasks.h b/src/gui/tasks.h index c674be1e..978a17b7 100644 --- a/src/gui/tasks.h +++ b/src/gui/tasks.h @@ -2,12 +2,21 @@ namespace tasks { inline int finished_renders = 0; - inline std::filesystem::path video_player_path = ""; + + struct PendingVideo { + std::filesystem::path video_path; + std::optional video_info; + float start = 0.f; + float end = 1.f; + }; void run(const std::vector& arguments); - void add_files_for_render(const std::vector& path_strs); + void add_files(const std::vector& path_strs); void add_sample_video(const std::filesystem::path& path_str); void process_pending_files(); - void set_video_player_path(const std::filesystem::path& path_str); + + void start_pending_video(size_t index); + + std::vector> get_pending_copy(); } diff --git a/src/gui/ui/elements/video_track.cpp b/src/gui/ui/elements/video_track.cpp index 76e6bbd0..b5ec823d 100644 --- a/src/gui/ui/elements/video_track.cpp +++ b/src/gui/ui/elements/video_track.cpp @@ -48,15 +48,14 @@ namespace { }; GrabRects get_grab_rects(const ui::AnimatedElement& element) { - float left_grab_percent = element.animations.at(ui::hasher("left_grab_percent")).current; - float right_grab_percent = element.animations.at(ui::hasher("right_grab_percent")).current; + auto& video_track_data = std::get(element.element->data); auto left_grab_rect = element.element->rect; - left_grab_rect.x += left_grab_rect.w * left_grab_percent; + left_grab_rect.x += left_grab_rect.w * *video_track_data.start; left_grab_rect.w = GRABS_LENGTH; auto right_grab_rect = element.element->rect; - right_grab_rect.x += right_grab_rect.w * right_grab_percent - GRABS_LENGTH; + right_grab_rect.x += right_grab_rect.w * *video_track_data.end - GRABS_LENGTH; right_grab_rect.w = GRABS_LENGTH; return { .left = left_grab_rect, .right = right_grab_rect }; @@ -95,8 +94,9 @@ void ui::render_video_track(const Container& container, const AnimatedElement& e ); // dont show progress when grabbing, its implied that you're at where you're grabbing - anim *= (1.f - left_grab); - anim *= (1.f - right_grab); + float progress_anim = anim; + progress_anim *= (1.f - left_grab); + progress_anim *= (1.f - right_grab); if (!video_track_data.video_data.player || !video_track_data.video_data.player->is_video_ready()) return; @@ -119,11 +119,12 @@ void ui::render_video_track(const Container& container, const AnimatedElement& e render::line(seek_point, seek_point.offset_y(rect.h), gfx::Color::white(75 * anim * seeking), false, 2.f); } - render::waveform(rect, active_rect, gfx::Color::white(100), *video_track_data.waveform); + render::waveform(rect, active_rect, gfx::Color::white(100 * anim), *video_track_data.waveform); } bool ui::update_video_track(const Container& container, AnimatedElement& element) { auto& video_track_data = std::get(element.element->data); + auto rect = element.element->rect; // Get animations once @@ -140,7 +141,7 @@ bool ui::update_video_track(const Container& container, AnimatedElement& element const char* name{}; gfx::Rect rect; ui::AnimationState& anim; - ui::AnimationState& percent_anim; + float* var_ptr; bool hovered = false; }; @@ -152,13 +153,13 @@ bool ui::update_video_track(const Container& container, AnimatedElement& element .name = "left", .rect = grab_rects.left.expand(GRAB_CLICK_EXPANSION), .anim = get_anim("left_grab"), - .percent_anim = get_anim("left_grab_percent"), + .var_ptr = video_track_data.start, }, GrabHandle{ .name = "right", .rect = grab_rects.right.expand(GRAB_CLICK_EXPANSION), .anim = get_anim("right_grab"), - .percent_anim = get_anim("right_grab_percent"), + .var_ptr = video_track_data.end, }, }; @@ -184,7 +185,7 @@ bool ui::update_video_track(const Container& container, AnimatedElement& element rect.mouse_percent_x(); // TODO: when you initially click it if you arent exactly at the right spot // itll shift the grab a little which is annoying - grab.percent_anim.set_goal(mouse_percent); + *grab.var_ptr = mouse_percent; // video_track_data.video_data.player->seek(mouse_percent, true, true); video_track_data.video_data.player->set_end(mouse_percent * 10.f); @@ -238,7 +239,7 @@ bool ui::update_video_track(const Container& container, AnimatedElement& element } ui::AnimatedElement* ui::add_video_track( - const std::string& id, Container& container, int width, const VideoElementData& video_data + const std::string& id, Container& container, int width, const VideoElementData& video_data, float& start, float& end ) { gfx::Size size(std::max(MIN_TRACK_WIDTH, width), TRACK_HEIGHT); @@ -253,6 +254,8 @@ ui::AnimatedElement* ui::add_video_track( VideoTrackElementData{ .video_data = video_data, .waveform = *waveform, + .start = &start, + .end = &end, }, render_video_track, update_video_track @@ -268,9 +271,7 @@ ui::AnimatedElement* ui::add_video_track( { hasher("seeking"), AnimationState(70.f) }, { hasher("seek"), AnimationState(70.f) }, { hasher("left_grab"), AnimationState(150.f) }, - { hasher("left_grab_percent"), AnimationState(150.f) }, { hasher("right_grab"), AnimationState(150.f) }, - { hasher("right_grab_percent"), AnimationState(150.f, 1.f) }, } ); diff --git a/src/gui/ui/helpers/video.cpp b/src/gui/ui/helpers/video.cpp index 8f621d50..9e9fbee3 100644 --- a/src/gui/ui/helpers/video.cpp +++ b/src/gui/ui/helpers/video.cpp @@ -161,6 +161,7 @@ void VideoPlayer::initialize_mpv() { mpv_set_option_string(m_mpv, "hwdec", "yes"); mpv_set_option_string(m_mpv, "profile", "fast"); mpv_set_option_string(m_mpv, "keep-open", "yes"); // dont close when finished + mpv_set_option_string(m_mpv, "pause", "yes"); // mpv_set_option_string(m_mpv, "mute", "yes"); int result = mpv_initialize(m_mpv); diff --git a/src/gui/ui/ui.h b/src/gui/ui/ui.h index 4ca63a54..43fedd0d 100644 --- a/src/gui/ui/ui.h +++ b/src/gui/ui/ui.h @@ -276,9 +276,12 @@ namespace ui { struct VideoTrackElementData { VideoElementData video_data; std::vector* waveform; + float* start; + float* end; bool operator==(const VideoTrackElementData& other) const { - return video_data == other.video_data && waveform == other.waveform; + return video_data == other.video_data && waveform == other.waveform && start == other.start && + end == other.end; } }; @@ -666,7 +669,12 @@ namespace ui { ); AnimatedElement* add_video_track( - const std::string& id, Container& container, int width, const VideoElementData& video_data + const std::string& id, + Container& container, + int width, + const VideoElementData& video_data, + float& start, + float& end ); AnimatedElement* add_separator(const std::string& id, Container& container, SeparatorStyle style); From 08f2719aef14d0d8aa46ebb03710ec3bc7008099 Mon Sep 17 00:00:00 2001 From: f0e <7321764+f0e@users.noreply.github.com> Date: Tue, 2 Sep 2025 02:09:17 +1000 Subject: [PATCH 024/181] feat: wip show multiple videos --- src/common/utils.cpp | 37 ++-- src/common/utils.h | 48 ++++- src/gui/components/main.cpp | 14 +- src/gui/gui_pch.h | 3 + src/gui/gui_utils.cpp | 66 +++++++ src/gui/gui_utils.h | 11 ++ src/gui/render/render.cpp | 1 + src/gui/renderer.cpp | 4 - src/gui/ui/elements/video.cpp | 201 --------------------- src/gui/ui/elements/video_track.cpp | 14 +- src/gui/ui/elements/videos.cpp | 271 ++++++++++++++++++++++++++++ src/gui/ui/ui.h | 17 +- 12 files changed, 432 insertions(+), 255 deletions(-) create mode 100644 src/gui/gui_utils.cpp create mode 100644 src/gui/gui_utils.h delete mode 100644 src/gui/ui/elements/video.cpp create mode 100644 src/gui/ui/elements/videos.cpp diff --git a/src/common/utils.cpp b/src/common/utils.cpp index 29f8add3..0a19e226 100644 --- a/src/common/utils.cpp +++ b/src/common/utils.cpp @@ -383,27 +383,26 @@ std::vector u::get_video_waveform(const std::filesystem::path& path) { namespace bp = boost::process; bp::ipstream pipe_stream; - bp::child c( - boost::filesystem::path{ blur.ffmpeg_path }, - "-v", - "error", - "-i", - path.string(), - "-f", - "s16le", - "-acodec", - "pcm_s16le", - "-ac", - "1", - "-ar", - "44100", - "-", + + auto c = u::run_command( + blur.ffmpeg_path, + { + "-v", + "error", + "-i", + path.string(), + "-f", + "s16le", + "-acodec", + "pcm_s16le", + "-ac", + "1", + "-ar", + "44100", + "-", + }, bp::std_out > pipe_stream, bp::std_err.null() -#ifdef _WIN32 - , - bp::windows::create_no_window -#endif ); std::vector samples; diff --git a/src/common/utils.h b/src/common/utils.h index 91f5d29c..ff3e6be7 100644 --- a/src/common/utils.h +++ b/src/common/utils.h @@ -226,10 +226,6 @@ namespace u { std::is_const_v, typename container_type::const_iterator, typename container_type::iterator>; - using pointer_type = std::conditional_t< - std::is_const_v, - typename container_type::const_pointer, - typename container_type::pointer>; using reference_type = std::conditional_t< std::is_const_v, typename container_type::const_reference, @@ -237,7 +233,7 @@ namespace u { constexpr enumerate_wrapper(container_type& c) : container(c) {} - struct enumerate_wrapper_iter { + struct iter { size_t index; iterator_type value; @@ -245,18 +241,37 @@ namespace u { return value != other; } - constexpr enumerate_wrapper_iter& operator++() { + constexpr iter& operator++() { ++index; ++value; return *this; } constexpr std::pair operator*() { - return std::pair{ index, *value }; + return { index, *value }; + } + }; + + struct reverse_iter { + size_t index; + std::reverse_iterator value; + + constexpr bool operator!=(const std::reverse_iterator& other) const { + return value != other; + } + + constexpr reverse_iter& operator++() { + ++value; + --index; + return *this; + } + + constexpr std::pair operator*() { + return { index, *value }; } }; - constexpr enumerate_wrapper_iter begin() { + constexpr iter begin() { return { 0, std::begin(container) }; } @@ -264,6 +279,23 @@ namespace u { return std::end(container); } + // --- Reverse range wrapper --- + struct reverse_range { + enumerate_wrapper& parent; + + constexpr reverse_iter begin() { + return { parent.container.size() - 1, std::rbegin(parent.container) }; + } + + constexpr std::reverse_iterator end() { + return std::rend(parent.container); + } + }; + + constexpr reverse_range reverse() { + return reverse_range{ *this }; + } + container_type& container; }; diff --git a/src/gui/components/main.cpp b/src/gui/components/main.cpp index 93d2f6b5..b86c0d54 100644 --- a/src/gui/components/main.cpp +++ b/src/gui/components/main.cpp @@ -220,15 +220,15 @@ void main::render_pending(ui::Container& container, const std::vectorvideo_path, - gfx::Size(container.get_usable_rect().w, container.get_usable_rect().h / 2) - ); + std::vector video_paths; + for (const auto& pv : pending) { + video_paths.push_back(pv->video_path); + } + + auto video = ui::add_videos("test video", container, video_paths); if (video) { auto video_rect = (*video)->element->rect; diff --git a/src/gui/gui_pch.h b/src/gui/gui_pch.h index f62f6cf4..d10a9693 100644 --- a/src/gui/gui_pch.h +++ b/src/gui/gui_pch.h @@ -14,5 +14,8 @@ #include // #include #include +#include #define IMGUI_USER_CONFIG "gui/render/imconfig.h" + +#include "gui_utils.h" diff --git a/src/gui/gui_utils.cpp b/src/gui/gui_utils.cpp new file mode 100644 index 00000000..ed48b02d --- /dev/null +++ b/src/gui/gui_utils.cpp @@ -0,0 +1,66 @@ +#include "gui_utils.h" + +#include "render/render.h" + +std::shared_ptr gui_utils::get_video_thumbnail( + const std::filesystem::path& path, int width, int height, double timestamp +) { + namespace bp = boost::process; + + bp::ipstream pipe_stream; + + auto c = u::run_command( + blur.ffmpeg_path, + { + "-v", + "error", + "-ss", + std::to_string(timestamp), // seek to timestamp + "-i", + u::path_to_string(path), + "-frames:v", + "1", // extract 1 frame + "-f", + "rawvideo", // raw RGB output + "-pix_fmt", + "RGB24", // pixel format + "-", // stdout + }, + bp::std_out > pipe_stream, + bp::std_err > stdout + ); + + const size_t frame_size = width * height * 3; // RGB24: 3 bytes per pixel + std::vector buffer(frame_size); + size_t total_read = 0; + + while (total_read < frame_size && + pipe_stream.read(reinterpret_cast(buffer.data()) + total_read, frame_size - total_read)) + { + total_read += static_cast(pipe_stream.gcount()); + } + + c.wait(); + + if (total_read != frame_size) { + return nullptr; // Failed to read full frame + } + + // Create SDL surface from raw RGB data + SDL_Surface* surface = SDL_CreateSurfaceFrom( + width, + height, + SDL_PIXELFORMAT_RGB24, // RGB24 format + buffer.data(), + width * 3 // pitch (bytes per row) + ); + + if (!surface) { + return nullptr; + } + + auto tex = std::make_shared(); + tex->load_from_surface(surface); + + return tex; +} diff --git a/src/gui/gui_utils.h b/src/gui/gui_utils.h new file mode 100644 index 00000000..9bbf94e8 --- /dev/null +++ b/src/gui/gui_utils.h @@ -0,0 +1,11 @@ +#pragma once + +namespace render { + class Texture; +} + +namespace gui_utils { + std::shared_ptr get_video_thumbnail( + const std::filesystem::path& path, int width, int height, double timestamp + ); +} diff --git a/src/gui/render/render.cpp b/src/gui/render/render.cpp index 240a6f33..4925fa3a 100644 --- a/src/gui/render/render.cpp +++ b/src/gui/render/render.cpp @@ -170,6 +170,7 @@ void render::ImGuiWrap::end(SDL_Window* window) { // NOLINT(readability-convert- glClear(GL_COLOR_BUFFER_BIT); ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); + SDL_GL_SwapWindow(window); } void render::line( diff --git a/src/gui/renderer.cpp b/src/gui/renderer.cpp index 65b5eee9..00da53d0 100644 --- a/src/gui/renderer.cpp +++ b/src/gui/renderer.cpp @@ -303,10 +303,6 @@ bool gui::renderer::redraw_window(bool rendered_last, bool want_to_render) { } render::imgui.end(sdl::window); - ui::render_videos(); - - SDL_GL_SwapWindow(sdl::window); - ui::on_frame_end(); return true; diff --git a/src/gui/ui/elements/video.cpp b/src/gui/ui/elements/video.cpp deleted file mode 100644 index ecc77eb7..00000000 --- a/src/gui/ui/elements/video.cpp +++ /dev/null @@ -1,201 +0,0 @@ -#include - -#include "../ui.h" -#include "../../render/render.h" -#include "../keys.h" -#include "../helpers/video.h" - -constexpr gfx::Size LOADER_SIZE(20, 20); -constexpr gfx::Size LOADER_PAD(5, 5); - -namespace { - std::unordered_map> video_players; - - tl::expected, std::string> get_or_add_player(const std::filesystem::path& video_path) { - auto key = video_path.string(); - - try { - auto it = video_players.find(key); - - if (it == video_players.end()) { - auto player = std::make_shared(); - player->load_file(key); - u::log("loaded video from {}", key); - - auto insert_result = video_players.insert({ key, player }); - it = insert_result.first; - } - - return it->second; - } - catch (const std::exception& e) { - u::log_error("failed to load video from {} ({})", key, e.what()); - return tl::unexpected("failed to load video"); - } - } -} - -void ui::render_videos() { - for (auto& [id, player] : video_players) { - if (player && player->is_video_ready()) { - auto dimensions = player->get_video_dimensions(); - if (dimensions) { - auto [width, height] = *dimensions; - player->render(width, height); - } - } - } -} - -void ui::handle_videos_event(const SDL_Event& event, bool& to_render) { - for (auto& [id, player] : video_players) { - if (player) { - switch (event.type) { - case SDL_EVENT_KEY_DOWN: - player->handle_key_press(event.key.key); - break; - - default: - player->handle_mpv_event(event, to_render); - break; - } - } - } -} - -void ui::render_video(const Container& container, const AnimatedElement& element) { - const auto& video_data = std::get(element.element->data); - - float anim = element.animations.at(hasher("main")).current; - - int alpha = anim * 255; - int stroke_alpha = anim * 125; - - if (video_data.loading) { - int loader_alpha = anim * 155; - - gfx::Rect loader_rect = element.element->rect.shrink(LOADER_PAD, true); - render::loader(loader_rect, gfx::Color::white(loader_alpha)); - return; - } - - auto usable_rect = element.element->rect.shrink(3); // account for border - - // TODO: render::image - render::imgui.drawlist->AddImage( - video_data.player->get_frame_texture_for_render(), - usable_rect.origin(), - usable_rect.max(), - ImVec2(0, 0), - ImVec2(1, 1), - IM_COL32(255, 255, 255, alpha) // apply alpha for fade animations - ); - - render::borders( - element.element->rect, gfx::Color(155, 155, 155, stroke_alpha), gfx::Color(80, 80, 80, stroke_alpha) - ); -} - -bool ui::update_video(const Container& container, AnimatedElement& element) { - const auto& video_data = std::get(element.element->data); - - auto video_player_it = video_players.find(video_data.video_path.string()); - if (video_player_it != video_players.end()) { - auto& video_player = video_player_it->second; - - bool hovered = element.element->rect.contains(keys::mouse_pos) && set_hovered_element(element); - - if (hovered) { - set_cursor(SDL_SYSTEM_CURSOR_POINTER); - - while (!event_queue.empty()) { - auto& event = event_queue.front(); - - bool blah; - video_player->handle_mpv_event(event, blah); - - event_queue.erase(event_queue.begin()); - } - } - } - - return false; -} - -void ui::remove_video(AnimatedElement& element) { - const auto& video_data = std::get(element.element->data); - auto video_player_it = video_players.find(video_data.video_path.string()); - if (video_player_it != video_players.end()) { - video_players.erase(video_player_it); - u::log("Removed video player for {}", video_data.video_path.string()); - } - else { - u::log("No video player found for {}", video_data.video_path.string()); - } -} - -std::optional ui::add_video( - const std::string& id, Container& container, const std::filesystem::path& video_path, const gfx::Size& max_size -) { - if (video_path.empty()) - return {}; - - auto player_res = get_or_add_player(video_path); - if (!player_res) - return {}; - - auto player = *player_res; - - gfx::Size elem_size = LOADER_SIZE; - bool loading = !player->is_video_ready(); - - if (!loading) { - auto dimensions = player->get_video_dimensions(); - - if (dimensions) { - // we have valid dimensions, calculate proper aspect ratio - auto [video_width, video_height] = *dimensions; - float aspect_ratio = static_cast(video_width) / static_cast(video_height); - - elem_size = max_size; - - // maintain aspect ratio while fitting within max_size - float target_width = elem_size.h * aspect_ratio; - float target_height = elem_size.w / aspect_ratio; - - if (target_width <= max_size.w) { - elem_size.w = static_cast(target_width); - } - else { - elem_size.h = static_cast(target_height); - } - - // ensure we don't exceed max dimensions - if (elem_size.h > max_size.h) { - elem_size.h = max_size.h; - elem_size.w = static_cast(max_size.h * aspect_ratio); - } - - if (elem_size.w > max_size.w) { - elem_size.w = max_size.w; - elem_size.h = static_cast(max_size.w / aspect_ratio); - } - } - } - - Element element( - id, - ElementType::VIDEO, - gfx::Rect(container.current_position, elem_size), - VideoElementData{ - .video_path = video_path, - .player = std::move(player), - .loading = loading, - }, - render_video, - update_video, - remove_video - ); - - return add_element(container, std::move(element), container.element_gap); -} diff --git a/src/gui/ui/elements/video_track.cpp b/src/gui/ui/elements/video_track.cpp index b5ec823d..5ea0283b 100644 --- a/src/gui/ui/elements/video_track.cpp +++ b/src/gui/ui/elements/video_track.cpp @@ -37,7 +37,7 @@ namespace { auto& video_track_data = std::get(element.element->data); auto& progress_anim = element.animations.at(ui::hasher("progress")); - auto progress_percent = video_track_data.video_data.player->get_percent_pos(); + auto progress_percent = video_track_data.video_data.players[0]->get_percent_pos(); if (progress_percent) progress_anim.set_goal(*progress_percent / 100.f); } @@ -98,7 +98,7 @@ void ui::render_video_track(const Container& container, const AnimatedElement& e progress_anim *= (1.f - left_grab); progress_anim *= (1.f - right_grab); - if (!video_track_data.video_data.player || !video_track_data.video_data.player->is_video_ready()) + if (!video_track_data.video_data.players[0] || !video_track_data.video_data.players[0]->is_video_ready()) return; auto rect = element.element->rect.shrink(1); @@ -188,12 +188,12 @@ bool ui::update_video_track(const Container& container, AnimatedElement& element *grab.var_ptr = mouse_percent; // video_track_data.video_data.player->seek(mouse_percent, true, true); - video_track_data.video_data.player->set_end(mouse_percent * 10.f); + video_track_data.video_data.players[0]->set_end(mouse_percent * 10.f); return true; } else { - video_track_data.video_data.player->set_paused(true); + video_track_data.video_data.players[0]->set_paused(true); reset_active_element(); } @@ -218,7 +218,7 @@ bool ui::update_video_track(const Container& container, AnimatedElement& element float mouse_percent = rect.mouse_percent_x(); - video_track_data.video_data.player->seek(mouse_percent, true, false); + video_track_data.video_data.players[0]->seek(mouse_percent, true, false); if (seeking_anim.current == 0.f) // new seek, start from current. otherwise smoothly animate from last seek seek_anim.current = progress_anim.current; @@ -232,7 +232,7 @@ bool ui::update_video_track(const Container& container, AnimatedElement& element } } - if (!video_track_data.video_data.player->get_queued_seek()) + if (!video_track_data.video_data.players[0]->get_queued_seek()) seeking_anim.set_goal(0.f); return false; @@ -243,7 +243,7 @@ ui::AnimatedElement* ui::add_video_track( ) { gfx::Size size(std::max(MIN_TRACK_WIDTH, width), TRACK_HEIGHT); - auto waveform = get_waveform(video_data.video_path); + auto waveform = get_waveform(video_data.video_paths[0]); if (!waveform) return {}; diff --git a/src/gui/ui/elements/videos.cpp b/src/gui/ui/elements/videos.cpp new file mode 100644 index 00000000..dc30b568 --- /dev/null +++ b/src/gui/ui/elements/videos.cpp @@ -0,0 +1,271 @@ +#include "../ui.h" +#include "../../render/render.h" +#include "../keys.h" +#include "../helpers/video.h" + +constexpr gfx::Size LOADER_SIZE(20, 20); +constexpr gfx::Size LOADER_PAD(5, 5); +constexpr size_t MAX_BACKGROUND_VIDEOS = 10; + +namespace { + std::unordered_map> video_players; + + // std::mutex thumbnail_mutex; + // std::unordered_map> thumbnails; + + tl::expected, std::string> get_or_add_player(const std::filesystem::path& video_path) { + auto key = video_path.string(); + + try { + auto it = video_players.find(key); + + if (it == video_players.end()) { + auto player = std::make_shared(); + player->load_file(key); + u::log("loaded video from {}", key); + + auto insert_result = video_players.insert({ key, player }); + it = insert_result.first; + } + + return it->second; + } + catch (const std::exception& e) { + u::log_error("failed to load video from {} ({})", key, e.what()); + return tl::unexpected("failed to load video"); + } + } + + // void create_thumbnail_async(const std::filesystem::path& video_path) { + // auto key = video_path.string(); + // { + // std::lock_guard lock(thumbnail_mutex); + + // if (thumbnails.contains(key)) + // return; + // } + + // std::thread([video_path, key] { + // std::lock_guard lock(thumbnail_mutex); + + // auto thumbnail = gui_utils::get_video_thumbnail(video_path, 100, 100, 0.0); + + // thumbnails.insert({ key, thumbnail }); + // }).detach(); + // } + + // std::optional> get_thumbnail(const std::filesystem::path& video_path) { + // auto key = video_path.string(); + + // std::lock_guard lock(thumbnail_mutex); + + // if (thumbnails.contains(key)) + // return {}; + + // return thumbnails[key]; + // } + + gfx::Size get_size_from_dimensions(const ui::Container& container, const std::shared_ptr& player) { + gfx::Size size = LOADER_SIZE; + + auto dimensions = player->get_video_dimensions(); + if (!dimensions) + return size; + + // we have valid dimensions, calculate proper aspect ratio + auto [video_width, video_height] = *dimensions; + float aspect_ratio = static_cast(video_width) / static_cast(video_height); + + gfx::Size max_size(container.get_usable_rect().w, container.get_usable_rect().h / 2); + size = max_size; + + // maintain aspect ratio while fitting within max_size + float target_width = size.h * aspect_ratio; + float target_height = size.w / aspect_ratio; + + if (target_width <= max_size.w) { + size.w = static_cast(target_width); + } + else { + size.h = static_cast(target_height); + } + + // ensure we don't exceed max dimensions + if (size.h > max_size.h) { + size.h = max_size.h; + size.w = static_cast(max_size.h * aspect_ratio); + } + + if (size.w > max_size.w) { + size.w = max_size.w; + size.h = static_cast(max_size.w / aspect_ratio); + } + + return size; + } +} + +void ui::handle_videos_event(const SDL_Event& event, bool& to_render) { + for (auto& [id, player] : video_players) { + if (player) { + switch (event.type) { + case SDL_EVENT_KEY_DOWN: + player->handle_key_press(event.key.key); + break; + + default: + player->handle_mpv_event(event, to_render); + break; + } + } + } +} + +void ui::render_videos(const Container& container, const AnimatedElement& element) { + const auto& video_data = std::get(element.element->data); + + float anim = element.animations.at(hasher("main")).current; + + int alpha = anim * 255; + + if (video_data.loading) { + int loader_alpha = anim * 155; + + gfx::Rect loader_rect = element.element->rect.shrink(LOADER_PAD, true); + render::loader(loader_rect, gfx::Color::white(loader_alpha)); + return; + } + + auto usable_rect = element.element->rect.shrink(3); // account for border + + const float START_FADE = 0.5f; + float fade_step = START_FADE / video_data.players.size(); + + int offset = 0; + + for (auto [i, player] : u::enumerate(video_data.players)) { + float fade = i == 0 ? 0.f : START_FADE + (fade_step * i); + if (fade >= 1.f) + continue; + + auto player_size = get_size_from_dimensions(container, player); + gfx::Rect player_rect(element.element->rect.origin().offset_x(offset), player_size); + + float player_alpha = alpha * (1.f - fade); + + if (player && player->is_video_ready()) { + bool to_render = player->render(player_rect.w, player_rect.h); + + if (to_render) { + // TODO: render::image + render::imgui.drawlist->AddImage( + player->get_frame_texture_for_render(), + player_rect.origin(), + player_rect.max(), + ImVec2(0, 0), + ImVec2(1, 1), + IM_COL32(255, 255, 255, player_alpha) // apply alpha for fade animations + ); + } + } + // else { + // auto thumbnail_texture = get_thumbnail(video_data.video_paths[i]); // TEMPORARY [i] ACCESS TODO: PROPER + // if (thumbnail_texture) + // render::image(player_rect, *thumbnail_texture.value()); + // } + + render::borders(player_rect, gfx::Color(50, 50, 50, alpha), gfx::Color(15, 15, 15, alpha)); + + offset += player_rect.w + 30; + } +} + +bool ui::update_videos(const Container& container, AnimatedElement& element) { + const auto& video_data = std::get(element.element->data); + + if (video_data.players.size() == 0) + return false; + + const auto& player = video_data.players[0]; + if (!player) + return false; + + bool hovered = element.element->rect.contains(keys::mouse_pos) && set_hovered_element(element); + + if (hovered) { + set_cursor(SDL_SYSTEM_CURSOR_POINTER); + + while (!event_queue.empty()) { + auto& event = event_queue.front(); + + bool blah; + player->handle_mpv_event(event, blah); + + event_queue.erase(event_queue.begin()); + } + } + + return false; +} + +void ui::remove_videos(AnimatedElement& element) { + const auto& video_data = std::get(element.element->data); + + for (const auto& video_path : video_data.video_paths) { + auto video_player_it = video_players.find(video_path.string()); + + if (video_player_it != video_players.end()) { + video_players.erase(video_player_it); + u::log("Removed video player for {}", video_path.string()); + } + else { + u::log("No video player found for {}", video_path.string()); + } + } +} + +std::optional ui::add_videos( + const std::string& id, Container& container, const std::vector& video_paths +) { + if (video_paths.empty()) + return {}; + + gfx::Size elem_size = LOADER_SIZE; + std::vector> players; + + for (auto [i, path] : u::enumerate(video_paths)) { + // if (i != 0) { + // players.push_back(nullptr); + // create_thumbnail_async(path); + // continue; + // } + if (i > MAX_BACKGROUND_VIDEOS) + continue; + + auto player_res = get_or_add_player(path); + auto player = *player_res; + players.push_back(player); + + if (!player_res || i > 0) + continue; + + if (player->is_video_ready()) { + elem_size = get_size_from_dimensions(container, player); + } + } + + Element element( + id, + ElementType::VIDEO, + gfx::Rect(container.current_position, elem_size), + VideoElementData{ + .video_paths = video_paths, + .players = std::move(players), + }, + render_videos, + update_videos, + remove_videos + ); + + return add_element(container, std::move(element), container.element_gap); +} diff --git a/src/gui/ui/ui.h b/src/gui/ui/ui.h index 43fedd0d..74c342e4 100644 --- a/src/gui/ui/ui.h +++ b/src/gui/ui/ui.h @@ -133,12 +133,12 @@ namespace ui { }; struct VideoElementData { - std::filesystem::path video_path; - std::shared_ptr player; + std::vector video_paths; + std::vector> players; bool loading; bool operator==(const VideoElementData& other) const { - return video_path == other.video_path && loading == other.loading; + return video_paths == other.video_paths && players == other.players && loading == other.loading; } }; @@ -448,11 +448,10 @@ namespace ui { void render_image(const Container& container, const AnimatedElement& element); - void render_video(const Container& container, const AnimatedElement& element); - bool update_video(const Container& container, AnimatedElement& element); - void remove_video(AnimatedElement& element); + void render_videos(const Container& container, const AnimatedElement& element); + bool update_videos(const Container& container, AnimatedElement& element); + void remove_videos(AnimatedElement& element); - void render_videos(); void handle_videos_event(const SDL_Event& event, bool& to_render); void render_button(const Container& container, const AnimatedElement& element); @@ -568,8 +567,8 @@ namespace ui { gfx::Color image_color = gfx::Color::white() ); // use image_id to distinguish images that have the same filename and reload it (e.g. if its updated) - std::optional add_video( - const std::string& id, Container& container, const std::filesystem::path& video_path, const gfx::Size& max_size + std::optional add_videos( + const std::string& id, Container& container, const std::vector& video_paths ); AnimatedElement* add_button( From 26aefab6f7a65882466da40b603423bcc0fa4d6a Mon Sep 17 00:00:00 2001 From: f0e <7321764+f0e@users.noreply.github.com> Date: Tue, 2 Sep 2025 03:40:06 +1000 Subject: [PATCH 025/181] feat: merge video & video track, animations, show side by side, switching --- src/gui/components/main.cpp | 15 +- src/gui/ui/elements/video_track.cpp | 281 ---------------- src/gui/ui/elements/videos.cpp | 501 ++++++++++++++++++++++++---- src/gui/ui/ui.h | 57 ++-- 4 files changed, 458 insertions(+), 396 deletions(-) delete mode 100644 src/gui/ui/elements/video_track.cpp diff --git a/src/gui/components/main.cpp b/src/gui/components/main.cpp index b86c0d54..f06c02c9 100644 --- a/src/gui/components/main.cpp +++ b/src/gui/components/main.cpp @@ -203,7 +203,8 @@ void main::render_progress( } void main::render_pending(ui::Container& container, const std::vector>& pending) { - size_t pending_index = 0; + static size_t pending_index = 0; + pending_index = (size_t)std::clamp((int)pending_index, 0, int(pending.size() - 1)); auto& pending_video = pending[pending_index]; @@ -228,17 +229,7 @@ void main::render_pending(ui::Container& container, const std::vectorvideo_path); } - auto video = ui::add_videos("test video", container, video_paths); - - if (video) { - auto video_rect = (*video)->element->rect; - - const auto& video_data = std::get((*video)->element->data); - - ui::add_video_track( - "test video track", container, video_rect.w, video_data, pending_video->start, pending_video->end - ); - } + ui::add_videos("test video", container, video_paths, pending_index, pending_video->start, pending_video->end); } void main::render_home(ui::Container& container) { diff --git a/src/gui/ui/elements/video_track.cpp b/src/gui/ui/elements/video_track.cpp deleted file mode 100644 index 5ea0283b..00000000 --- a/src/gui/ui/elements/video_track.cpp +++ /dev/null @@ -1,281 +0,0 @@ -#include "../ui.h" -#include "../keys.h" - -constexpr int MIN_TRACK_WIDTH = 250; -constexpr int TRACK_HEIGHT = 40; -constexpr int GRABS_THICKNESS = 1; -constexpr int GRABS_LENGTH = 5; -constexpr gfx::Color GRABS_COLOR(80, 80, 80); -constexpr gfx::Color GRABS_ACTIVE_COLOR(175, 175, 175); -constexpr gfx::Size GRAB_CLICK_EXPANSION(15, 5); - -namespace { - std::unordered_map> waveforms; - - tl::expected*, std::string> get_waveform(const std::filesystem::path& video_path) { - auto key = video_path.string(); - - try { - auto it = waveforms.find(key); - - if (it == waveforms.end()) { - auto waveform = u::get_video_waveform(key); - - auto insert_result = waveforms.insert({ key, waveform }); - it = insert_result.first; - } - - return &it->second; - } - catch (const std::exception& e) { - u::log_error("failed to load video from {} ({})", key, e.what()); - return tl::unexpected("failed to load video"); - } - } - - void update_progress(ui::AnimatedElement& element) { - auto& video_track_data = std::get(element.element->data); - auto& progress_anim = element.animations.at(ui::hasher("progress")); - - auto progress_percent = video_track_data.video_data.players[0]->get_percent_pos(); - if (progress_percent) - progress_anim.set_goal(*progress_percent / 100.f); - } - - struct GrabRects { - gfx::Rect left; - gfx::Rect right; - }; - - GrabRects get_grab_rects(const ui::AnimatedElement& element) { - auto& video_track_data = std::get(element.element->data); - - auto left_grab_rect = element.element->rect; - left_grab_rect.x += left_grab_rect.w * *video_track_data.start; - left_grab_rect.w = GRABS_LENGTH; - - auto right_grab_rect = element.element->rect; - right_grab_rect.x += right_grab_rect.w * *video_track_data.end - GRABS_LENGTH; - right_grab_rect.w = GRABS_LENGTH; - - return { .left = left_grab_rect, .right = right_grab_rect }; - } -} - -void ui::render_video_track(const Container& container, const AnimatedElement& element) { - const auto& video_track_data = std::get(element.element->data); - - float anim = element.animations.at(hasher("main")).current; - float progress = element.animations.at(hasher("progress")).current; - float seeking = element.animations.at(hasher("seeking")).current; - float seek = element.animations.at(hasher("seek")).current; - float left_grab = element.animations.at(hasher("left_grab")).current; - float right_grab = element.animations.at(hasher("right_grab")).current; - - int stroke_alpha = 125; - - render::rect_filled(element.element->rect, gfx::Color::black(stroke_alpha * anim)); - render::rect_stroke(element.element->rect, gfx::Color(155, 155, 155, stroke_alpha * anim)); - - auto grab_rects = get_grab_rects(element); - - render::rect_side( - grab_rects.left, - gfx::Color::lerp(GRABS_COLOR, GRABS_ACTIVE_COLOR, left_grab).adjust_alpha(anim), - render::RectSide::LEFT, - GRABS_THICKNESS - ); - - render::rect_side( - grab_rects.right, - gfx::Color::lerp(GRABS_COLOR, GRABS_ACTIVE_COLOR, right_grab).adjust_alpha(anim), - render::RectSide::RIGHT, - GRABS_THICKNESS - ); - - // dont show progress when grabbing, its implied that you're at where you're grabbing - float progress_anim = anim; - progress_anim *= (1.f - left_grab); - progress_anim *= (1.f - right_grab); - - if (!video_track_data.video_data.players[0] || !video_track_data.video_data.players[0]->is_video_ready()) - return; - - auto rect = element.element->rect.shrink(1); - - auto active_rect = rect; - active_rect.x = grab_rects.left.x; - active_rect.w = grab_rects.right.x2() - active_rect.x; - - gfx::Point progress_point = rect.origin(); - progress_point.x = rect.x + (progress * rect.w); - - render::line(progress_point, progress_point.offset_y(rect.h), gfx::Color::white(anim * 255), false, 2.f); - - if (seeking > 0.f) { - gfx::Point seek_point = rect.origin(); - seek_point.x = rect.x + (seek * rect.w); - - render::line(seek_point, seek_point.offset_y(rect.h), gfx::Color::white(75 * anim * seeking), false, 2.f); - } - - render::waveform(rect, active_rect, gfx::Color::white(100 * anim), *video_track_data.waveform); -} - -bool ui::update_video_track(const Container& container, AnimatedElement& element) { - auto& video_track_data = std::get(element.element->data); - - auto rect = element.element->rect; - - // Get animations once - auto get_anim = [&](const char* name) -> auto& { - return element.animations.at(ui::hasher(name)); - }; - - auto& progress_anim = get_anim("progress"); - auto& seeking_anim = get_anim("seeking"); - auto& seek_anim = get_anim("seek"); - - // Grab handle data - struct GrabHandle { - const char* name{}; - gfx::Rect rect; - ui::AnimationState& anim; - float* var_ptr; - - bool hovered = false; - }; - - auto grab_rects = get_grab_rects(element); - - std::array grabs = { - GrabHandle{ - .name = "left", - .rect = grab_rects.left.expand(GRAB_CLICK_EXPANSION), - .anim = get_anim("left_grab"), - .var_ptr = video_track_data.start, - }, - GrabHandle{ - .name = "right", - .rect = grab_rects.right.expand(GRAB_CLICK_EXPANSION), - .anim = get_anim("right_grab"), - .var_ptr = video_track_data.end, - }, - }; - - // Process both grab handles - for (auto& grab : grabs) { - std::string action = "video track grab " + std::string(grab.name); - - grab.hovered = grab.rect.contains(keys::mouse_pos) && set_hovered_element(element); - - if (grab.hovered) { - set_cursor(SDL_SYSTEM_CURSOR_POINTER); - - if (!get_active_element() && keys::is_mouse_down()) { - set_active_element(element, action); - } - } - - if (is_active_element(element, action)) { - if (keys::is_mouse_down()) { - grab.anim.set_goal(1.f); - - float mouse_percent = - rect.mouse_percent_x(); // TODO: when you initially click it if you arent exactly at the right spot - // itll shift the grab a little which is annoying - - *grab.var_ptr = mouse_percent; - - // video_track_data.video_data.player->seek(mouse_percent, true, true); - video_track_data.video_data.players[0]->set_end(mouse_percent * 10.f); - - return true; - } - else { - video_track_data.video_data.players[0]->set_paused(true); - - reset_active_element(); - } - } - - grab.anim.set_goal(grab.hovered ? 0.5f : 0.f); - } - - bool hovered = - !grabs[0].hovered && !grabs[1].hovered && rect.contains(keys::mouse_pos) && set_hovered_element(element); - bool active = get_active_element() == &element; - - if (hovered) { - // set_cursor(SDL_SYSTEM_CURSOR_POINTER); - if (keys::is_mouse_down()) - set_active_element(element, "video track"); - } - - if (is_active_element(element, "video track")) { - if (keys::is_mouse_down()) { - seeking_anim.set_goal(1.f); - - float mouse_percent = rect.mouse_percent_x(); - - video_track_data.video_data.players[0]->seek(mouse_percent, true, false); - - if (seeking_anim.current == 0.f) // new seek, start from current. otherwise smoothly animate from last seek - seek_anim.current = progress_anim.current; - - seek_anim.set_goal(mouse_percent); - - return true; - } - else { - reset_active_element(); - } - } - - if (!video_track_data.video_data.players[0]->get_queued_seek()) - seeking_anim.set_goal(0.f); - - return false; -} - -ui::AnimatedElement* ui::add_video_track( - const std::string& id, Container& container, int width, const VideoElementData& video_data, float& start, float& end -) { - gfx::Size size(std::max(MIN_TRACK_WIDTH, width), TRACK_HEIGHT); - - auto waveform = get_waveform(video_data.video_paths[0]); - if (!waveform) - return {}; - - Element element( - id, - ElementType::VIDEO_TRACK, - gfx::Rect(container.current_position, size), - VideoTrackElementData{ - .video_data = video_data, - .waveform = *waveform, - .start = &start, - .end = &end, - }, - render_video_track, - update_video_track - ); - - auto* elem = add_element( - container, - std::move(element), - container.element_gap, - { - { hasher("main"), AnimationState(25.f) }, - { hasher("progress"), AnimationState(70.f) }, - { hasher("seeking"), AnimationState(70.f) }, - { hasher("seek"), AnimationState(70.f) }, - { hasher("left_grab"), AnimationState(150.f) }, - { hasher("right_grab"), AnimationState(150.f) }, - } - ); - - update_progress(*elem); - - return elem; -} diff --git a/src/gui/ui/elements/videos.cpp b/src/gui/ui/elements/videos.cpp index dc30b568..73138dc2 100644 --- a/src/gui/ui/elements/videos.cpp +++ b/src/gui/ui/elements/videos.cpp @@ -3,9 +3,22 @@ #include "../keys.h" #include "../helpers/video.h" +// videos constexpr gfx::Size LOADER_SIZE(20, 20); constexpr gfx::Size LOADER_PAD(5, 5); -constexpr size_t MAX_BACKGROUND_VIDEOS = 10; +constexpr size_t MAX_BACKGROUND_VIDEOS = 100; // TODO MR: HANDLE +constexpr float START_FADE = 0.5f; +constexpr int VIDEO_GAP = 30; + +// track +constexpr int TRACK_GAP = 10; +constexpr int MIN_TRACK_WIDTH = 250; +constexpr int TRACK_HEIGHT = 40; +constexpr int GRABS_THICKNESS = 1; +constexpr int GRABS_LENGTH = 5; +constexpr gfx::Color GRABS_COLOR(80, 80, 80); +constexpr gfx::Color GRABS_ACTIVE_COLOR(175, 175, 175); +constexpr gfx::Size GRAB_CLICK_EXPANSION(15, 5); namespace { std::unordered_map> video_players; @@ -68,6 +81,9 @@ namespace { gfx::Size get_size_from_dimensions(const ui::Container& container, const std::shared_ptr& player) { gfx::Size size = LOADER_SIZE; + if (!player) + return size; + auto dimensions = player->get_video_dimensions(); if (!dimensions) return size; @@ -103,6 +119,100 @@ namespace { return size; } + + std::vector get_video_rects(const ui::AnimatedElement& element, const gfx::Rect& rect) { + const auto& video_data = std::get(element.element->data); + + std::vector rects; + + float offset = element.animations.at(ui::hasher("video_offset")).current; + + for (auto [i, video] : u::enumerate(video_data.videos)) { + gfx::Rect player_rect(rect.origin().offset_x(offset), video.size); + + rects.push_back(player_rect); + + offset += player_rect.w + VIDEO_GAP; + } + + return rects; + } + + // track + std::unordered_map> waveforms; + + tl::expected*, std::string> get_waveform(const std::filesystem::path& video_path) { + auto key = video_path.string(); + + try { + auto it = waveforms.find(key); + + if (it == waveforms.end()) { + auto waveform = u::get_video_waveform(key); + + auto insert_result = waveforms.insert({ key, waveform }); + it = insert_result.first; + } + + return &it->second; + } + catch (const std::exception& e) { + u::log_error("failed to load video from {} ({})", key, e.what()); + return tl::unexpected("failed to load video"); + } + } + + void update_progress(ui::AnimatedElement& element) { + const auto& video_data = std::get(element.element->data); + + if (video_data.videos.size() == 0 || *video_data.index < 0 || *video_data.index >= video_data.videos.size()) + return; + + const auto& video = video_data.videos[*video_data.index]; + if (!video.player) + return; + + auto& progress_anim = element.animations.at(ui::hasher("progress")); + + auto progress_percent = video.player->get_percent_pos(); + if (progress_percent) + progress_anim.set_goal(*progress_percent / 100.f); + } + + struct GrabRects { + gfx::Rect left; + gfx::Rect right; + }; + + GrabRects get_grab_rects(const ui::AnimatedElement& element, const gfx::Rect& rect) { + auto& video_data = std::get(element.element->data); + + auto left_grab_rect = rect; + left_grab_rect.x += left_grab_rect.w * *video_data.start; + left_grab_rect.w = GRABS_LENGTH; + + auto right_grab_rect = rect; + right_grab_rect.x += right_grab_rect.w * *video_data.end - GRABS_LENGTH; + right_grab_rect.w = GRABS_LENGTH; + + return { .left = left_grab_rect, .right = right_grab_rect }; + } + + // both + struct Positions { + gfx::Rect videos_rect; + gfx::Rect track_rect; + }; + + Positions get_positions(const gfx::Point& origin, const gfx::Size& video_size) { + auto videos_rect = gfx::Rect{ origin, video_size }; + auto track_rect = gfx::Rect{ + videos_rect.bottom_left().offset_y(TRACK_GAP), + gfx::Size(std::max(videos_rect.w, MIN_TRACK_WIDTH), TRACK_HEIGHT), + }; + + return { .videos_rect = videos_rect, .track_rect = track_rect }; + } } void ui::handle_videos_event(const SDL_Event& event, bool& to_render) { @@ -121,117 +231,335 @@ void ui::handle_videos_event(const SDL_Event& event, bool& to_render) { } } -void ui::render_videos(const Container& container, const AnimatedElement& element) { - const auto& video_data = std::get(element.element->data); +void render_videos_actual(const ui::Container& container, const ui::AnimatedElement& element, gfx::Rect rect) { + const auto& video_data = std::get(element.element->data); - float anim = element.animations.at(hasher("main")).current; + float anim = element.animations.at(ui::hasher("main")).current; int alpha = anim * 255; - if (video_data.loading) { - int loader_alpha = anim * 155; - - gfx::Rect loader_rect = element.element->rect.shrink(LOADER_PAD, true); - render::loader(loader_rect, gfx::Color::white(loader_alpha)); - return; - } - - auto usable_rect = element.element->rect.shrink(3); // account for border + float fade_step = START_FADE / video_data.videos.size(); - const float START_FADE = 0.5f; - float fade_step = START_FADE / video_data.players.size(); + std::vector rects = get_video_rects(element, rect); - int offset = 0; - - for (auto [i, player] : u::enumerate(video_data.players)) { - float fade = i == 0 ? 0.f : START_FADE + (fade_step * i); + for (auto [i, video] : u::enumerate(video_data.videos)) { + float fade = i == *video_data.index ? 0.f : START_FADE + (fade_step * i); if (fade >= 1.f) continue; - auto player_size = get_size_from_dimensions(container, player); - gfx::Rect player_rect(element.element->rect.origin().offset_x(offset), player_size); + auto video_rect = rects[i]; + auto inner_rect = video_rect.shrink(1); float player_alpha = alpha * (1.f - fade); - if (player && player->is_video_ready()) { - bool to_render = player->render(player_rect.w, player_rect.h); - - if (to_render) { - // TODO: render::image - render::imgui.drawlist->AddImage( - player->get_frame_texture_for_render(), - player_rect.origin(), - player_rect.max(), - ImVec2(0, 0), - ImVec2(1, 1), - IM_COL32(255, 255, 255, player_alpha) // apply alpha for fade animations - ); - } + if (video.player && video.player->is_video_ready() && video.player->render(inner_rect.w, inner_rect.h)) { + // TODO: render::image + render::imgui.drawlist->AddImage( + video.player->get_frame_texture_for_render(), + inner_rect.origin(), + inner_rect.max(), + ImVec2(0, 0), + ImVec2(1, 1), + IM_COL32(255, 255, 255, player_alpha) // apply alpha for fade animations + ); } + else { + gfx::Rect loader_rect = inner_rect.shrink(LOADER_PAD, true); + render::loader(loader_rect, gfx::Color::white(155 * anim)); + return; + } + // else { - // auto thumbnail_texture = get_thumbnail(video_data.video_paths[i]); // TEMPORARY [i] ACCESS TODO: PROPER + // auto thumbnail_texture = get_thumbnail(video_data.videos[i]); // TEMPORARY [i] ACCESS TODO: PROPER // if (thumbnail_texture) - // render::image(player_rect, *thumbnail_texture.value()); + // render::image(video_rect, *thumbnail_texture.value()); // } - render::borders(player_rect, gfx::Color(50, 50, 50, alpha), gfx::Color(15, 15, 15, alpha)); + render::borders(video_rect, gfx::Color(50, 50, 50, alpha), gfx::Color(15, 15, 15, alpha)); + } +} + +void render_track(const ui::Container& container, const ui::AnimatedElement& element, gfx::Rect rect) { + const auto& video_data = std::get(element.element->data); + + if (video_data.videos.size() == 0 || *video_data.index < 0 || *video_data.index >= video_data.videos.size()) + return; + + const auto& video = video_data.videos[*video_data.index]; + if (!video.player) + return; + + float anim = element.animations.at(ui::hasher("main")).current; + float progress = element.animations.at(ui::hasher("progress")).current; + float seeking = element.animations.at(ui::hasher("seeking")).current; + float seek = element.animations.at(ui::hasher("seek")).current; + float left_grab = element.animations.at(ui::hasher("left_grab")).current; + float right_grab = element.animations.at(ui::hasher("right_grab")).current; + + int stroke_alpha = 125; + + render::rect_filled(rect, gfx::Color::black(stroke_alpha * anim)); + render::rect_stroke(rect, gfx::Color(155, 155, 155, stroke_alpha * anim)); + + auto grab_rects = get_grab_rects(element, rect); + + render::rect_side( + grab_rects.left, + gfx::Color::lerp(GRABS_COLOR, GRABS_ACTIVE_COLOR, left_grab).adjust_alpha(anim), + render::RectSide::LEFT, + GRABS_THICKNESS + ); + + render::rect_side( + grab_rects.right, + gfx::Color::lerp(GRABS_COLOR, GRABS_ACTIVE_COLOR, right_grab).adjust_alpha(anim), + render::RectSide::RIGHT, + GRABS_THICKNESS + ); + + // dont show progress when grabbing, its implied that you're at where you're grabbing + float progress_anim = anim; + progress_anim *= (1.f - left_grab); + progress_anim *= (1.f - right_grab); + + if (!video.player || !video.player->is_video_ready()) + return; + + rect = rect.shrink(1); + + auto active_rect = rect; + active_rect.x = grab_rects.left.x; + active_rect.w = grab_rects.right.x2() - active_rect.x; + + gfx::Point progress_point = rect.origin(); + progress_point.x = rect.x + (progress * rect.w); + + render::line(progress_point, progress_point.offset_y(rect.h), gfx::Color::white(anim * 255), false, 2.f); + + if (seeking > 0.f) { + gfx::Point seek_point = rect.origin(); + seek_point.x = rect.x + (seek * rect.w); - offset += player_rect.w + 30; + render::line(seek_point, seek_point.offset_y(rect.h), gfx::Color::white(75 * anim * seeking), false, 2.f); } + + render::waveform(rect, active_rect, gfx::Color::white(100 * anim), *video.waveform); } -bool ui::update_videos(const Container& container, AnimatedElement& element) { - const auto& video_data = std::get(element.element->data); +void ui::render_videos(const Container& container, const AnimatedElement& element) { + const auto& video_data = std::get(element.element->data); - if (video_data.players.size() == 0) - return false; + auto positions = get_positions(element.element->rect.origin(), video_data.active_video_size); - const auto& player = video_data.players[0]; - if (!player) - return false; + render_videos_actual(container, element, positions.videos_rect); + render_track(container, element, positions.track_rect); + + auto grab_rects = get_grab_rects(element, positions.track_rect); +} + +bool update_track( + const ui::Container& container, + ui::AnimatedElement& element, + const ui::VideoElementData::Video& video, + gfx::Rect rect +) { + const auto& video_data = std::get(element.element->data); + + auto& progress_anim = element.animations.at(ui::hasher("progress")); + auto& seeking_anim = element.animations.at(ui::hasher("seeking")); + auto& seek_anim = element.animations.at(ui::hasher("seek")); + + // Grab handle data + struct GrabHandle { + const char* name{}; + gfx::Rect rect; + ui::AnimationState& anim; + float* var_ptr{}; + + bool hovered = false; + }; + + auto grab_rects = get_grab_rects(element, rect); + + std::array grabs = { + GrabHandle{ + .name = "left", + .rect = grab_rects.left.expand(GRAB_CLICK_EXPANSION), + .anim = element.animations.at(ui::hasher("left_grab")), + .var_ptr = video_data.start, + }, + GrabHandle{ + .name = "right", + .rect = grab_rects.right.expand(GRAB_CLICK_EXPANSION), + .anim = element.animations.at(ui::hasher("right_grab")), + .var_ptr = video_data.end, + }, + }; + + // Process both grab handles + for (auto& grab : grabs) { + std::string action = "video track grab " + std::string(grab.name); + + grab.hovered = grab.rect.contains(keys::mouse_pos) && set_hovered_element(element); + + if (grab.hovered) { + ui::set_cursor(SDL_SYSTEM_CURSOR_POINTER); + + if (!ui::get_active_element() && keys::is_mouse_down()) { + ui::set_active_element(element, action); + } + } + + if (is_active_element(element, action)) { + if (keys::is_mouse_down()) { + grab.anim.set_goal(1.f); - bool hovered = element.element->rect.contains(keys::mouse_pos) && set_hovered_element(element); + float mouse_percent = + rect.mouse_percent_x(); // TODO: when you initially click it if you arent exactly at the right spot + // itll shift the grab a little which is annoying + + *grab.var_ptr = mouse_percent; + + // video_data.player->seek(mouse_percent, true, true); + video.player->set_end(mouse_percent * 10.f); + + return true; + } + else { + video.player->set_paused(true); + + ui::reset_active_element(); + } + } + + grab.anim.set_goal(grab.hovered ? 0.5f : 0.f); + } + + bool hovered = + !grabs[0].hovered && !grabs[1].hovered && rect.contains(keys::mouse_pos) && set_hovered_element(element); + bool active = ui::get_active_element() == &element; if (hovered) { - set_cursor(SDL_SYSTEM_CURSOR_POINTER); + // set_cursor(SDL_SYSTEM_CURSOR_POINTER); + if (keys::is_mouse_down()) + set_active_element(element, "video track"); + } + + if (is_active_element(element, "video track")) { + if (keys::is_mouse_down()) { + seeking_anim.set_goal(1.f); + + float mouse_percent = rect.mouse_percent_x(); + + video.player->seek(mouse_percent, true, false); + + if (seeking_anim.current == 0.f) // new seek, start from current. otherwise smoothly animate from last seek + seek_anim.current = progress_anim.current; + + seek_anim.set_goal(mouse_percent); + + return true; + } + else { + ui::reset_active_element(); + } + } + + if (!video.player->get_queued_seek()) + seeking_anim.set_goal(0.f); + + return false; +} + +bool update_videos_actual(const ui::Container& container, ui::AnimatedElement& element, gfx::Rect rect) { + const auto& video_data = std::get(element.element->data); - while (!event_queue.empty()) { - auto& event = event_queue.front(); + // TODO MR: idk if you even need this, been working fine without (it was bugged) + for (auto [i, video] : u::enumerate(video_data.videos)) { + if (i == *video_data.index) { + while (!ui::event_queue.empty()) { + auto& event = ui::event_queue.front(); - bool blah; - player->handle_mpv_event(event, blah); + bool blah; + video.player->handle_mpv_event(event, blah); - event_queue.erase(event_queue.begin()); + ui::event_queue.erase(ui::event_queue.begin()); + } + } + } + + // update offset + float offset = 0; + for (int i = 0; i < *video_data.index; ++i) { + offset -= video_data.videos[i].size.w + VIDEO_GAP; // shift left by widths + spacing + } + + auto& offset_anim = element.animations.at(ui::hasher("video_offset")); + offset_anim.set_goal(offset); + + std::vector rects = get_video_rects(element, rect); + + for (auto [i, video] : u::enumerate(video_data.videos)) { + if (rects[i].contains(keys::mouse_pos)) { + if (keys::is_mouse_down()) { + keys::on_mouse_press_handled(SDL_BUTTON_LEFT); + *video_data.index = i; + } } } return false; } +bool ui::update_videos(const Container& container, AnimatedElement& element) { + const auto& video_data = std::get(element.element->data); + + if (video_data.videos.size() == 0 || *video_data.index < 0 || *video_data.index >= video_data.videos.size()) + return false; + + const auto& video = video_data.videos[*video_data.index]; + if (!video.player) + return false; + + auto positions = get_positions(element.element->rect.origin(), video_data.active_video_size); + + bool res = false; + + res |= update_videos_actual(container, element, positions.videos_rect); + res |= update_track(container, element, video, positions.track_rect); + + return res; +} + void ui::remove_videos(AnimatedElement& element) { const auto& video_data = std::get(element.element->data); - for (const auto& video_path : video_data.video_paths) { - auto video_player_it = video_players.find(video_path.string()); + for (const auto& video : video_data.videos) { + auto video_player_it = video_players.find(video.path.string()); if (video_player_it != video_players.end()) { video_players.erase(video_player_it); - u::log("Removed video player for {}", video_path.string()); + u::log("Removed video player for {}", video.path.string()); } else { - u::log("No video player found for {}", video_path.string()); + u::log("No video player found for {}", video.path.string()); } } } std::optional ui::add_videos( - const std::string& id, Container& container, const std::vector& video_paths + const std::string& id, + Container& container, + const std::vector& video_paths, + size_t& index, + float& start, + float& end ) { if (video_paths.empty()) return {}; - gfx::Size elem_size = LOADER_SIZE; - std::vector> players; + gfx::Size active_video_size = LOADER_SIZE; + std::vector videos; for (auto [i, path] : u::enumerate(video_paths)) { // if (i != 0) { @@ -244,28 +572,61 @@ std::optional ui::add_videos( auto player_res = get_or_add_player(path); auto player = *player_res; - players.push_back(player); + auto size = get_size_from_dimensions(container, player); - if (!player_res || i > 0) - continue; - - if (player->is_video_ready()) { - elem_size = get_size_from_dimensions(container, player); + auto waveform = get_waveform(path); + if (!waveform) { + // TODO MR: + return {}; } + + videos.push_back( + VideoElementData::Video{ + .path = path, + .player = player, + .size = size, + .waveform = *waveform, + } + ); + + if (i == index) + active_video_size = size; } + auto positions = get_positions(container.current_position, active_video_size); + Element element( id, ElementType::VIDEO, - gfx::Rect(container.current_position, elem_size), + gfx::Rect(positions.videos_rect.origin(), positions.track_rect.max()), VideoElementData{ - .video_paths = video_paths, - .players = std::move(players), + .videos = std::move(videos), + .active_video_size = active_video_size, + .index = &index, + .start = &start, + .end = &end, }, render_videos, update_videos, remove_videos ); - return add_element(container, std::move(element), container.element_gap); + auto* elem = add_element( + container, + std::move(element), + container.element_gap, + { + { hasher("main"), AnimationState(25.f) }, + { hasher("video_offset"), AnimationState(25.f) }, + { hasher("progress"), AnimationState(70.f) }, + { hasher("seeking"), AnimationState(70.f) }, + { hasher("seek"), AnimationState(70.f) }, + { hasher("left_grab"), AnimationState(150.f) }, + { hasher("right_grab"), AnimationState(150.f) }, + } + ); + + update_progress(*elem); + + return elem; } diff --git a/src/gui/ui/ui.h b/src/gui/ui/ui.h index 74c342e4..dcac9e85 100644 --- a/src/gui/ui/ui.h +++ b/src/gui/ui/ui.h @@ -34,8 +34,7 @@ namespace ui { SEPARATOR, WEIGHTING_GRAPH, TABS, - HINT, - VIDEO_TRACK + HINT }; struct BarElementData { @@ -133,12 +132,24 @@ namespace ui { }; struct VideoElementData { - std::vector video_paths; - std::vector> players; - bool loading; + struct Video { + std::filesystem::path path; + std::shared_ptr player; + gfx::Size size; + std::vector* waveform; + + bool operator==(const Video& other) const = default; + }; + + std::vector