osc: use property notifications and a timer instead of "tick" events

Traditionally, the OSC used mpv's "tick" event, which was approximately
sent once per video frame. It didn't try to track any other state, and
just updated everything.

This is sort of a problem in many corner cases and non-corner cases. For
example, it would eat CPU in the paused state (probably to some degree
also the mpv core's fault), or would waste power or even throw errors
("event queue overflows") on high FPS video.

Change this to not using the tick event. Instead, react to a number of
property change events. Rate-limit actual redrawing with a timer; the
next update cannot happen sooner than the hardcoded 30ms OSC frame
duration. This has also the effect that multiple successive updates are
(mostly) coalesced.

This means the OSC won't eat your CPU when the player is fucking paused.
(It'll still update if e.g. the cache is growing, though.) There is some
potential for bugs whenever it uses properties that are not explicitly
observed. (In theory we could easily change this to a reactive concept
to avoid such things, but whatever.)
This commit is contained in:
wm4 2019-12-19 12:41:01 +01:00
parent ce9d2c9f8e
commit 48f906249e
1 changed files with 47 additions and 60 deletions

View File

@ -126,8 +126,9 @@ local state = {
message_text, message_text,
message_timeout, message_timeout,
fullscreen = false, fullscreen = false,
timer = nil, tick_timer = nil,
cache_idle = false, tick_last_time = 0, -- when the last tick() was run
cache_state = nil,
idle = false, idle = false,
enabled = true, enabled = true,
input_enabled = true, input_enabled = true,
@ -139,7 +140,7 @@ local state = {
} }
local window_control_box_width = 80 local window_control_box_width = 80
local tick_delay = 0.03
-- --
-- Helperfunctions -- Helperfunctions
@ -1900,7 +1901,7 @@ function osc_init()
if user_opts.seekrangestyle == "none" then if user_opts.seekrangestyle == "none" then
return nil return nil
end end
local cache_state = mp.get_property_native("demuxer-cache-state", nil) local cache_state = state.cache_state
if not cache_state then if not cache_state then
return nil return nil
end end
@ -1909,14 +1910,17 @@ function osc_init()
return nil return nil
end end
local ranges = cache_state["seekable-ranges"] local ranges = cache_state["seekable-ranges"]
for _, range in pairs(ranges) do
range["start"] = 100 * range["start"] / duration
range["end"] = 100 * range["end"] / duration
end
if #ranges == 0 then if #ranges == 0 then
return nil return nil
end end
return ranges local nranges = {}
for _, range in pairs(ranges) do
nranges[#nranges + 1] = {
["start"] = 100 * range["start"] / duration,
["end"] = 100 * range["end"] / duration,
}
end
return nranges
end end
ne.eventresponder["mouse_move"] = --keyframe seeking when mouse is dragged ne.eventresponder["mouse_move"] = --keyframe seeking when mouse is dragged
function (element) function (element)
@ -1980,8 +1984,8 @@ function osc_init()
ne = new_element("cache", "button") ne = new_element("cache", "button")
ne.content = function () ne.content = function ()
local cache_state = mp.get_property_native("demuxer-cache-state", {}) local cache_state = state.cache_state
if not (cache_state["seekable-ranges"] and if not (cache_state and cache_state["seekable-ranges"] and
#cache_state["seekable-ranges"] > 0) then #cache_state["seekable-ranges"] > 0) then
-- probably not a network stream -- probably not a network stream
return "" return ""
@ -2119,7 +2123,7 @@ function hide_osc()
elseif (user_opts.fadeduration > 0) then elseif (user_opts.fadeduration > 0) then
if not(state.osc_visible == false) then if not(state.osc_visible == false) then
state.anitype = "out" state.anitype = "out"
control_timer() request_tick()
end end
else else
osc_visible(false) osc_visible(false)
@ -2128,61 +2132,39 @@ end
function osc_visible(visible) function osc_visible(visible)
state.osc_visible = visible state.osc_visible = visible
control_timer() request_tick()
update_margins() update_margins()
end end
function pause_state(name, enabled) function pause_state(name, enabled)
state.paused = enabled state.paused = enabled
control_timer() request_tick()
end end
function cache_state(name, idle) function cache_state(name, st)
state.cache_idle = idle state.cache_state = st
control_timer() request_tick()
end end
function control_timer() -- Request that tick() is called (which typically re-renders the OSC).
if (state.paused) and (state.osc_visible) and -- The tick is then either executed immediately, or rate-limited if it was
( not(state.cache_idle) or not (state.anitype == nil) ) then -- called a small time ago.
function request_tick()
timer_start() if state.tick_timer == nil then
else state.tick_timer = mp.add_timeout(0, tick)
timer_stop()
end end
end
function timer_start() if not state.tick_timer:is_enabled() then
if not (state.timer_active) then local now = mp.get_time()
msg.trace("timer start") local timeout = tick_delay - (now - state.tick_last_time)
if timeout < 0 then
if (state.timer == nil) then timeout = 0
-- create new timer
state.timer = mp.add_periodic_timer(0.03, tick)
else
-- resume existing one
state.timer:resume()
end end
state.tick_timer.timeout = timeout
state.timer_active = true state.tick_timer:resume()
end end
end end
function timer_stop()
if (state.timer_active) then
msg.trace("timer stop")
if not (state.timer == nil) then
-- kill timer
state.timer:kill()
end
state.timer_active = false
end
end
function mouse_leave() function mouse_leave()
if user_opts.hidetimeout >= 0 then if user_opts.hidetimeout >= 0 then
hide_osc() hide_osc()
@ -2415,7 +2397,7 @@ function process_event(source, what)
if element_has_action(elements[n], action) then if element_has_action(elements[n], action) then
elements[n].eventresponder[action](elements[n]) elements[n].eventresponder[action](elements[n])
end end
tick() request_tick()
end end
end end
@ -2470,6 +2452,12 @@ function tick()
-- Flush OSD -- Flush OSD
mp.set_osd_ass(osc_param.playresy, osc_param.playresy, "") mp.set_osd_ass(osc_param.playresy, osc_param.playresy, "")
end end
state.tick_last_time = mp.get_time()
if state.anitype ~= nil then
request_tick()
end
end end
function do_enable_keybindings() function do_enable_keybindings()
@ -2539,17 +2527,16 @@ mp.observe_property("window-maximized", "bool",
mp.observe_property("idle-active", "bool", mp.observe_property("idle-active", "bool",
function(name, val) function(name, val)
state.idle = val state.idle = val
tick() request_tick()
end end
) )
mp.observe_property("pause", "bool", pause_state) mp.observe_property("pause", "bool", pause_state)
mp.observe_property("cache-idle", "bool", cache_state) mp.observe_property("demuxer-cache-state", "native", cache_state)
mp.observe_property("vo-configured", "bool", function(name, val) mp.observe_property("vo-configured", "bool", function(name, val)
if val then request_tick()
mp.register_event("tick", tick) end)
else mp.observe_property("playback-time", "number", function(name, val)
mp.unregister_event(tick) request_tick()
end
end) end)
-- mouse show/hide bindings -- mouse show/hide bindings