refactor: use cicular buffer for cursor.history

Circular buffers are more efficient then moving the entries around.

Cursor velocity is now calculated on the fly, with from the current
cursor position and time as well a cursor position that is either
the youngest sample >100ms old, or the oldest sample in the buffer,
which is 10 elements in size. Restricting the sample selection based on
time prevents the filter window from becomming too long in case of
setups with low cursor update frequency, while still bring more
responsive velocity measurements to systems with higher cursor update
frequencies.

Removed the timer for exact seeking after fast seeks, as fast/exact
seeks can now be controlled with cursor speed and thus there is no need
for this anymore.
This commit is contained in:
Christoph Heinrich
2023-08-24 20:03:34 +02:00
committed by Tomas Klaen
parent 776ca17f66
commit 3f21df1a9d
3 changed files with 86 additions and 51 deletions

View File

@@ -16,11 +16,6 @@ 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)
self.seek_timer:kill()
-- Release any dragging when file gets unloaded
mp.register_event('end-file', function() self.pressed = false end)
@@ -53,13 +48,6 @@ 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
@@ -82,7 +70,6 @@ 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)
@@ -116,14 +103,12 @@ 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() self:update_pixels_per_frame() end
function Timeline:on_prop_framerate() self:update_pixels_per_frame() end
function Timeline:on_prop_duration() self:decide_enabled() 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
function Timeline:on_display() self:update_dimensions() end
function Timeline:handle_cursor_up()
self.seek_timer:kill()
if self.pressed then
mp.set_property_native('pause', self.pressed.pause)
self.pressed = false
@@ -137,10 +122,8 @@ 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 not state.is_video or math.abs(cursor.velocity.x) > 80 * math.max(1, self.pixels_per_frame) then
if state.is_video and math.abs(cursor.get_velocity().x) / self.width * state.duration > 30 then
self:set_from_cursor(true)
self.seek_timer:kill()
self.seek_timer:resume()
else self:set_from_cursor() end
end
end

View File

@@ -202,3 +202,59 @@ function Class:init() end
function Class:destroy() end
function class(parent) return setmetatable({}, {__index = parent or Class}) end
---@class CircularBuffer<T> : Class
CircularBuffer = class()
function CircularBuffer:new(max_size) return Class.new(self, max_size) --[[@as CircularBuffer]] end
function CircularBuffer:init(max_size)
self.max_size = max_size
self.size = 0
self.pos = 0
self.data = {}
end
function CircularBuffer:insert(item)
self.pos = self.pos % self.max_size + 1
self.data[self.pos] = item
if self.size < self.max_size then self.size = self.size + 1 end
end
function CircularBuffer:get(i)
return i <= self.size and self.data[(self.pos + i - 1) % self.size + 1] or nil
end
local function iter(self, i)
if i == self.size then return nil end
i = i + 1
return i, self:get(i)
end
function CircularBuffer:iter()
return iter, self, 0
end
local function iter_rev(self, i)
if i == 1 then return nil end
i = i - 1
return i, self:get(i)
end
function CircularBuffer:iter_rev()
return iter_rev, self, self.size + 1
end
function CircularBuffer:head()
return self.data[self.pos]
end
function CircularBuffer:tail()
if self.size < 1 then return nil end
return self.data[self.pos % self.size + 1]
end
function CircularBuffer:clear()
for i = self.size, 1, -1 do self.data[i] = nil end
self.size = 0
self.pos = 0
end

View File

@@ -316,10 +316,7 @@ cursor = {
on_wheel_down = nil,
on_wheel_up = nil,
allow_dragging = false,
---@type {x: number, y: number, time: number}[]
history = {},
history_size = 10,
velocity = {x = 0, y = 0},
history = CircularBuffer:new(10),
-- Called at the beginning of each render
reset_handlers = function()
cursor.on_primary_down, cursor.on_primary_up = nil, nil
@@ -342,6 +339,26 @@ cursor = {
cursor.wheel_enabled = enable_wheel
end
end,
find_history_sample = function()
local time = mp.get_time()
for _, e in cursor.history:iter_rev() do
if time - e.time > 0.1 then
return e
end
end
return cursor.history:tail()
end,
get_velocity = function()
local snap = cursor.find_history_sample()
if snap then
local x, y, time = cursor.x - snap.x, cursor.y - snap.y, mp.get_time()
local time_diff = time - snap.time
if time_diff > 0.001 then
return { x = x / time_diff, y = y / time_diff }
end
end
return { x = 0, y = 0 }
end,
move = function(x, y)
local old_x, old_y = cursor.x, cursor.y
@@ -360,8 +377,8 @@ cursor = {
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
cursor.hidden = true
cursor.history:clear()
-- Slowly fadeout elements that are currently visible
for _, element_name in ipairs({'timeline', 'volume', 'top_bar'}) do
@@ -375,28 +392,12 @@ cursor = {
Elements:trigger('global_mouse_leave')
elseif cursor.hidden then
cursor.hidden, cursor.history = false, {}
cursor.velocity.x, cursor.velocity.y = 0, 0
cursor.hidden = false
cursor.history:clear()
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
cursor.history:insert({x = cursor.x, y = cursor.y, time = mp.get_time()})
end
Elements:proximity_trigger('mouse_move')
@@ -424,11 +425,8 @@ cursor = {
-- 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 #cursor.history < 10 then
return false
end
local prev = cursor.history[#cursor.history - 9]
local prev = cursor.find_history_sample()
if not prev then return false end
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
@@ -455,7 +453,6 @@ 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,
@@ -829,7 +826,6 @@ 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))