feat: fast seek in timeline based on cursor velocity

This commit is contained in:
tomasklaen
2023-08-23 11:03:42 +02:00
committed by Tomas Klaen
parent d1e9f9c4eb
commit 776ca17f66
2 changed files with 89 additions and 63 deletions

View File

@@ -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()

View File

@@ -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))