From 776ca17f66d4b7e7d0a180d4b493c1acd8c122cd Mon Sep 17 00:00:00 2001 From: tomasklaen Date: Wed, 23 Aug 2023 11:03:42 +0200 Subject: [PATCH] feat: fast seek in timeline based on cursor velocity --- scripts/uosc/elements/Timeline.lua | 14 ++- scripts/uosc/main.lua | 138 ++++++++++++++++------------- 2 files changed, 89 insertions(+), 63 deletions(-) diff --git a/scripts/uosc/elements/Timeline.lua b/scripts/uosc/elements/Timeline.lua index 29708bf..68ae159 100644 --- a/scripts/uosc/elements/Timeline.lua +++ b/scripts/uosc/elements/Timeline.lua @@ -16,6 +16,7 @@ function Timeline:init() self.top_border = options.timeline_border self.is_hovered = false self.has_thumbnail = false + self.pixels_per_frame = 1 -- Delayed seeking timer self.seek_timer = mp.add_timeout(0.05, function() self:set_from_cursor() end) @@ -52,6 +53,13 @@ end function Timeline:get_is_hovered() return self.enabled and self.is_hovered end +function Timeline:update_pixels_per_frame() + if state.is_video and state.framerate and state.duration then + self.pixels_per_frame = self.width / (state.duration * state.framerate) + else + self.pixels_per_frame = 1 + end +end function Timeline:update_dimensions() if state.fullormaxed then self.size_min = options.timeline_size_min_fullscreen @@ -74,6 +82,7 @@ function Timeline:update_dimensions() if Elements.top_bar.enabled then available_space = available_space - Elements.top_bar.size end self.obstructed = available_space < self.size_max + 10 self:decide_enabled() + self:update_pixels_per_frame() end function Timeline:get_time_at_x(x) @@ -107,7 +116,8 @@ function Timeline:handle_cursor_down() self:set_from_cursor() cursor.on_primary_up = function() self:handle_cursor_up() end end -function Timeline:on_prop_duration() self:decide_enabled() end +function Timeline:on_prop_duration() self:decide_enabled() self:update_pixels_per_frame() end +function Timeline:on_prop_framerate() self:update_pixels_per_frame() end function Timeline:on_prop_time() self:decide_enabled() end function Timeline:on_prop_border() self:update_dimensions() end function Timeline:on_prop_fullormaxed() self:update_dimensions() end @@ -127,7 +137,7 @@ function Timeline:on_global_mouse_move() if self.pressed then self.pressed.distance = self.pressed.distance + get_point_to_point_proximity(self.pressed.last, cursor) self.pressed.last.x, self.pressed.last.y = cursor.x, cursor.y - if self.width / state.duration < 10 then + if not state.is_video or math.abs(cursor.velocity.x) > 80 * math.max(1, self.pixels_per_frame) then self:set_from_cursor(true) self.seek_timer:kill() self.seek_timer:resume() diff --git a/scripts/uosc/main.lua b/scripts/uosc/main.lua index 2819b1e..e3238a7 100644 --- a/scripts/uosc/main.lua +++ b/scripts/uosc/main.lua @@ -316,8 +316,10 @@ cursor = { on_wheel_down = nil, on_wheel_up = nil, allow_dragging = false, - history = {}, -- {x, y}[] history + ---@type {x: number, y: number, time: number}[] + history = {}, history_size = 10, + velocity = {x = 0, y = 0}, -- Called at the beginning of each render reset_handlers = function() cursor.on_primary_down, cursor.on_primary_up = nil, nil @@ -340,9 +342,73 @@ cursor = { cursor.wheel_enabled = enable_wheel end end, + move = function(x, y) + local old_x, old_y = cursor.x, cursor.y + + -- mpv reports initial mouse position on linux as (0, 0), which always + -- displays the top bar, so we hardcode cursor position as infinity until + -- we receive a first real mouse move event with coordinates other than 0,0. + if not state.first_real_mouse_move_received then + if x > 0 and y > 0 then state.first_real_mouse_move_received = true + else x, y = INFINITY, INFINITY end + end + + -- Add 0.5 to be in the middle of the pixel + cursor.x, cursor.y = (x + 0.5) / display.scale_x, (y + 0.5) / display.scale_y + + if old_x ~= cursor.x or old_y ~= cursor.y then + Elements:update_proximities() + + if cursor.x == INFINITY or cursor.y == INFINITY then + cursor.hidden, cursor.history = true, {} + cursor.velocity.x, cursor.velocity.y = 0, 0 + + -- Slowly fadeout elements that are currently visible + for _, element_name in ipairs({'timeline', 'volume', 'top_bar'}) do + local element = Elements[element_name] + if element and element.proximity > 0 then + element:tween_property('forced_visibility', element:get_visibility(), 0, function() + element.forced_visibility = nil + end) + end + end + + Elements:trigger('global_mouse_leave') + elseif cursor.hidden then + cursor.hidden, cursor.history = false, {} + cursor.velocity.x, cursor.velocity.y = 0, 0 + Elements:trigger('global_mouse_enter') + else + -- Update history + local new_index = #cursor.history + 1 + local last = {x = x, y = y, time = mp.get_time()} + if #cursor.history >= cursor.history_size then + new_index = #cursor.history + for i = 1, #cursor.history, 1 do cursor.history[i] = cursor.history[i + 1] end + end + cursor.history[new_index] = last + + -- Update velocity + if #cursor.history > 5 then + local first = cursor.history[1] + local time_delta = (last.time - first.time) + cursor.velocity.x = (last.x - first.x) / time_delta + cursor.velocity.y = (last.y - first.y) / time_delta + else + cursor.velocity.x, cursor.velocity.y = 0, 0 + end + end + + Elements:proximity_trigger('mouse_move') + cursor.queue_autohide() + end + + request_render() + end, + leave = function () cursor.move(INFINITY, INFINITY) end, -- Cursor auto-hiding after period of inactivity autohide = function() - if not cursor.on_primary_up and not Menu:is_open() then handle_mouse_leave() end + if not cursor.on_primary_up and not Menu:is_open() then cursor.leave() end end, autohide_timer = (function() local timer = mp.add_timeout(mp.get_property_native('cursor-autohide') / 1000, function() cursor.autohide() end) @@ -355,15 +421,15 @@ cursor = { cursor.autohide_timer:resume() end end, - -- Calculates distance in which cursor reaches rectangle if it continues moving in the same path. + -- Calculates distance in which cursor reaches rectangle if it continues moving on the same path. -- Returns `nil` if cursor is not moving towards the rectangle. direction_to_rectangle_distance = function(rect) - if cursor.hidden or not cursor.history[1] then + if cursor.hidden or #cursor.history < 10 then return false end - local prev_x, prev_y = cursor.history[1][1], cursor.history[1][2] - local end_x, end_y = cursor.x + (cursor.x - prev_x) * 1e10, cursor.y + (cursor.y - prev_y) * 1e10 + local prev = cursor.history[#cursor.history - 9] + local end_x, end_y = cursor.x + (cursor.x - prev.x) * 1e10, cursor.y + (cursor.y - prev.y) * 1e10 return get_ray_to_rectangle_distance(cursor.x, cursor.y, end_x, end_y, rect) end } @@ -389,6 +455,7 @@ state = { duration = nil, -- current media duration time_human = nil, -- current playback time in human format destination_time_human = nil, -- depends on options.destination_time + framerate = 1, pause = mp.get_property_native('pause'), chapters = {}, current_chapter = nil, @@ -464,7 +531,7 @@ function update_fullormaxed() state.fullormaxed = state.fullscreen or state.maximized update_display_dimensions() Elements:trigger('prop_fullormaxed', state.fullormaxed) - update_cursor_position(INFINITY, INFINITY) + cursor.leave() end function update_human_times() @@ -541,58 +608,6 @@ function set_state(name, value) Elements:trigger('prop_' .. name, value) end -function update_cursor_position(x, y) - local old_x, old_y = cursor.x, cursor.y - - -- mpv reports initial mouse position on linux as (0, 0), which always - -- displays the top bar, so we hardcode cursor position as infinity until - -- we receive a first real mouse move event with coordinates other than 0,0. - if not state.first_real_mouse_move_received then - if x > 0 and y > 0 then state.first_real_mouse_move_received = true - else x, y = INFINITY, INFINITY end - end - - -- Add 0.5 to be in the middle of the pixel - cursor.x, cursor.y = (x + 0.5) / display.scale_x, (y + 0.5) / display.scale_y - - if old_x ~= cursor.x or old_y ~= cursor.y then - Elements:update_proximities() - - if cursor.x == INFINITY or cursor.y == INFINITY then - cursor.hidden, cursor.history = true, {} - Elements:trigger('global_mouse_leave') - elseif cursor.hidden then - cursor.hidden, cursor.history = false, {} - Elements:trigger('global_mouse_enter') - else - -- Update cursor history - for i = 1, cursor.history_size - 1, 1 do - cursor.history[i] = cursor.history[i + 1] - end - cursor.history[cursor.history_size] = {x, y} - end - - Elements:proximity_trigger('mouse_move') - cursor.queue_autohide() - end - - request_render() -end - -function handle_mouse_leave() - -- Slowly fadeout elements that are currently visible - for _, element_name in ipairs({'timeline', 'volume', 'top_bar'}) do - local element = Elements[element_name] - if element and element.proximity > 0 then - element:tween_property('forced_visibility', element:get_visibility(), 0, function() - element.forced_visibility = nil - end) - end - end - - update_cursor_position(INFINITY, INFINITY) -end - function handle_file_end() local resume = false if not state.loop_file then @@ -669,9 +684,9 @@ end function handle_mouse_pos(_, mouse) if not mouse then return end if cursor.hover_raw and not mouse.hover then - handle_mouse_leave() + cursor.leave() else - update_cursor_position(mouse.x, mouse.y) + cursor.move(mouse.x, mouse.y) end cursor.hover_raw = mouse.hover end @@ -814,6 +829,7 @@ end) mp.observe_property('display-hidpi-scale', 'native', create_state_setter('hidpi_scale', update_display_dimensions)) mp.observe_property('cache', 'string', create_state_setter('cache')) mp.observe_property('cache-buffering-state', 'number', create_state_setter('cache_buffering')) +mp.observe_property('demuxer-rawvideo-fps', 'number', create_state_setter('framerate')) mp.observe_property('demuxer-via-network', 'native', create_state_setter('is_stream', function() Elements:trigger('dispositions') end))