2020-08-05 20:37:47 +00:00
|
|
|
-- Note: anything global is accessible by profile condition expressions.
|
|
|
|
|
|
|
|
local msg = require 'mp.msg'
|
|
|
|
|
|
|
|
local profiles = {}
|
|
|
|
local watched_properties = {} -- indexed by property name (used as a set)
|
|
|
|
local cached_properties = {} -- property name -> last known raw value
|
|
|
|
local properties_to_profiles = {} -- property name -> set of profiles using it
|
|
|
|
local have_dirty_profiles = false -- at least one profile is marked dirty
|
auto_profiles: register hooks for more synchronous profile application
The property observation mechanism is fairly asynchronous to the player
core, and Lua scripts also are (they run in a separate thread). This may
sometimes lead to profiles being applied when it's too load.
For example, you may want to change network options depending on the
input URL - but most of these options would have to be set before the
HTTP access is made. But it could happen that the profile, and thus the
option, was applied at an slightly but arbitrary later time.
This is generally not fixable. But for the most important use-cases,
such as applying changes before media opening or playback
initialization, we can use some of the defined hooks.
Hooks make it synchronous again, by allowing API users (such as scripts)
to block the core because it continues with loading.
For this we simply don't continue a given hook, until we receive an idle
event, and have applied all changes. The idle event is in general used
to wait for property change notifications to settle down. Some of this
relies on the subtle ways guarantees are (maybe) given. See commit
ba70b150fbe for the messy details. I'm not quite sure whether it
actually works, because I can't be bothered to read and understand my
bullshit from half a year ago. Should provide at least some improvement,
though.
2020-08-05 21:28:24 +00:00
|
|
|
local pending_hooks = {} -- as set (keys only, meaningless values)
|
2020-08-05 20:37:47 +00:00
|
|
|
|
|
|
|
-- Used during evaluation of the profile condition, and should contain the
|
|
|
|
-- profile the condition is evaluated for.
|
|
|
|
local current_profile = nil
|
|
|
|
|
2023-04-06 00:49:07 +00:00
|
|
|
-- Cached set of all top-level mpv properities. Only used for extra validation.
|
|
|
|
local property_set = {}
|
|
|
|
for _, property in pairs(mp.get_property_native("property-list")) do
|
|
|
|
property_set[property] = true
|
|
|
|
end
|
|
|
|
|
2020-08-05 20:37:47 +00:00
|
|
|
local function evaluate(profile)
|
|
|
|
msg.verbose("Re-evaluating auto profile " .. profile.name)
|
|
|
|
|
|
|
|
current_profile = profile
|
|
|
|
local status, res = pcall(profile.cond)
|
|
|
|
current_profile = nil
|
|
|
|
|
|
|
|
if not status then
|
|
|
|
-- errors can be "normal", e.g. in case properties are unavailable
|
|
|
|
msg.verbose("Profile condition error on evaluating: " .. res)
|
|
|
|
res = false
|
|
|
|
end
|
2023-04-04 19:38:04 +00:00
|
|
|
res = not not res
|
2020-08-07 17:41:44 +00:00
|
|
|
if res ~= profile.status then
|
|
|
|
if res == true then
|
|
|
|
msg.info("Applying auto profile: " .. profile.name)
|
|
|
|
mp.commandv("apply-profile", profile.name)
|
|
|
|
elseif profile.status == true and profile.has_restore_opt then
|
|
|
|
msg.info("Restoring profile: " .. profile.name)
|
|
|
|
mp.commandv("apply-profile", profile.name, "restore")
|
|
|
|
end
|
2020-08-05 20:37:47 +00:00
|
|
|
end
|
|
|
|
profile.status = res
|
|
|
|
profile.dirty = false
|
|
|
|
end
|
|
|
|
|
|
|
|
local function on_property_change(name, val)
|
|
|
|
cached_properties[name] = val
|
|
|
|
-- Mark all profiles reading this property as dirty, so they get re-evaluated
|
|
|
|
-- the next time the script goes back to sleep.
|
|
|
|
local dependent_profiles = properties_to_profiles[name]
|
|
|
|
if dependent_profiles then
|
|
|
|
for profile, _ in pairs(dependent_profiles) do
|
|
|
|
assert(profile.cond) -- must be a profile table
|
|
|
|
profile.dirty = true
|
|
|
|
have_dirty_profiles = true
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
local function on_idle()
|
|
|
|
-- When events and property notifications stop, re-evaluate all dirty profiles.
|
|
|
|
if have_dirty_profiles then
|
|
|
|
for _, profile in ipairs(profiles) do
|
|
|
|
if profile.dirty then
|
|
|
|
evaluate(profile)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
have_dirty_profiles = false
|
auto_profiles: register hooks for more synchronous profile application
The property observation mechanism is fairly asynchronous to the player
core, and Lua scripts also are (they run in a separate thread). This may
sometimes lead to profiles being applied when it's too load.
For example, you may want to change network options depending on the
input URL - but most of these options would have to be set before the
HTTP access is made. But it could happen that the profile, and thus the
option, was applied at an slightly but arbitrary later time.
This is generally not fixable. But for the most important use-cases,
such as applying changes before media opening or playback
initialization, we can use some of the defined hooks.
Hooks make it synchronous again, by allowing API users (such as scripts)
to block the core because it continues with loading.
For this we simply don't continue a given hook, until we receive an idle
event, and have applied all changes. The idle event is in general used
to wait for property change notifications to settle down. Some of this
relies on the subtle ways guarantees are (maybe) given. See commit
ba70b150fbe for the messy details. I'm not quite sure whether it
actually works, because I can't be bothered to read and understand my
bullshit from half a year ago. Should provide at least some improvement,
though.
2020-08-05 21:28:24 +00:00
|
|
|
-- Release all hooks (the point was to wait until an idle event)
|
|
|
|
while true do
|
|
|
|
local h = next(pending_hooks)
|
|
|
|
if not h then
|
|
|
|
break
|
|
|
|
end
|
|
|
|
pending_hooks[h] = nil
|
|
|
|
h:cont()
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
local function on_hook(h)
|
|
|
|
h:defer()
|
|
|
|
pending_hooks[h] = true
|
2020-08-05 20:37:47 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
function get(name, default)
|
|
|
|
-- Normally, we use the cached value only
|
|
|
|
if not watched_properties[name] then
|
|
|
|
watched_properties[name] = true
|
2023-03-29 01:47:31 +00:00
|
|
|
local res, err = mp.get_property_native(name)
|
2023-04-06 00:49:07 +00:00
|
|
|
-- Property has to not exist and the toplevel of property in the name must also
|
|
|
|
-- not have an existing match in the property set for this to be considered an error.
|
|
|
|
-- This allows things like user-data/test to still work.
|
|
|
|
if err == "property not found" and property_set[name:match("^([^/]+)")] == nil then
|
2023-03-29 01:47:31 +00:00
|
|
|
msg.error("Property '" .. name .. "' was not found.")
|
|
|
|
return default
|
|
|
|
end
|
|
|
|
cached_properties[name] = res
|
2020-08-05 20:37:47 +00:00
|
|
|
mp.observe_property(name, "native", on_property_change)
|
|
|
|
end
|
|
|
|
-- The first time the property is read we need add it to the
|
|
|
|
-- properties_to_profiles table, which will be used to mark the profile
|
|
|
|
-- dirty if a property referenced by it changes.
|
|
|
|
if current_profile then
|
|
|
|
local map = properties_to_profiles[name]
|
|
|
|
if not map then
|
|
|
|
map = {}
|
|
|
|
properties_to_profiles[name] = map
|
|
|
|
end
|
|
|
|
map[current_profile] = true
|
|
|
|
end
|
|
|
|
local val = cached_properties[name]
|
|
|
|
if val == nil then
|
|
|
|
val = default
|
|
|
|
end
|
|
|
|
return val
|
|
|
|
end
|
|
|
|
|
|
|
|
local function magic_get(name)
|
|
|
|
-- Lua identifiers can't contain "-", so in order to match with mpv
|
|
|
|
-- property conventions, replace "_" to "-"
|
|
|
|
name = string.gsub(name, "_", "-")
|
|
|
|
return get(name, nil)
|
|
|
|
end
|
|
|
|
|
|
|
|
local evil_magic = {}
|
|
|
|
setmetatable(evil_magic, {
|
2024-05-12 00:29:26 +00:00
|
|
|
__index = function(_, key)
|
2020-08-05 20:37:47 +00:00
|
|
|
-- interpret everything as property, unless it already exists as
|
|
|
|
-- a non-nil global value
|
|
|
|
local v = _G[key]
|
|
|
|
if type(v) ~= "nil" then
|
|
|
|
return v
|
|
|
|
end
|
|
|
|
return magic_get(key)
|
|
|
|
end,
|
|
|
|
})
|
|
|
|
|
|
|
|
p = {}
|
|
|
|
setmetatable(p, {
|
2024-05-12 00:29:26 +00:00
|
|
|
__index = function(_, key)
|
2020-08-05 20:37:47 +00:00
|
|
|
return magic_get(key)
|
|
|
|
end,
|
|
|
|
})
|
|
|
|
|
|
|
|
local function compile_cond(name, s)
|
2020-12-02 06:38:39 +00:00
|
|
|
local code, chunkname = "return " .. s, "profile " .. name .. " condition"
|
|
|
|
local chunk, err
|
2024-05-12 00:29:26 +00:00
|
|
|
-- luacheck: push
|
|
|
|
-- luacheck: ignore setfenv loadstring
|
2020-12-02 06:38:39 +00:00
|
|
|
if setfenv then -- lua 5.1
|
|
|
|
chunk, err = loadstring(code, chunkname)
|
|
|
|
if chunk then
|
|
|
|
setfenv(chunk, evil_magic)
|
|
|
|
end
|
|
|
|
else -- lua 5.2
|
|
|
|
chunk, err = load(code, chunkname, "t", evil_magic)
|
|
|
|
end
|
2024-05-12 00:29:26 +00:00
|
|
|
-- luacheck: pop
|
2020-08-05 20:37:47 +00:00
|
|
|
if not chunk then
|
|
|
|
msg.error("Profile '" .. name .. "' condition: " .. err)
|
|
|
|
chunk = function() return false end
|
|
|
|
end
|
|
|
|
return chunk
|
|
|
|
end
|
|
|
|
|
2024-01-10 23:43:12 +00:00
|
|
|
local function load_profiles(profiles_property)
|
|
|
|
for _, v in ipairs(profiles_property) do
|
2020-08-05 20:37:47 +00:00
|
|
|
local cond = v["profile-cond"]
|
|
|
|
if cond and #cond > 0 then
|
|
|
|
local profile = {
|
|
|
|
name = v.name,
|
|
|
|
cond = compile_cond(v.name, cond),
|
|
|
|
properties = {},
|
|
|
|
status = nil,
|
|
|
|
dirty = true, -- need re-evaluate
|
2022-01-30 11:46:32 +00:00
|
|
|
has_restore_opt = v["profile-restore"] and v["profile-restore"] ~= "default"
|
2020-08-05 20:37:47 +00:00
|
|
|
}
|
|
|
|
profiles[#profiles + 1] = profile
|
|
|
|
have_dirty_profiles = true
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2024-01-10 23:43:12 +00:00
|
|
|
mp.observe_property("profile-list", "native", function (_, profiles_property)
|
|
|
|
profiles = {}
|
|
|
|
watched_properties = {}
|
|
|
|
cached_properties = {}
|
|
|
|
properties_to_profiles = {}
|
|
|
|
mp.unobserve_property(on_property_change)
|
2020-08-05 20:37:47 +00:00
|
|
|
|
2024-01-10 23:43:12 +00:00
|
|
|
load_profiles(profiles_property)
|
|
|
|
|
|
|
|
if #profiles < 1 and mp.get_property("load-auto-profiles") == "auto" then
|
|
|
|
-- make it exit immediately
|
|
|
|
_G.mp_event_loop = function() end
|
|
|
|
return
|
|
|
|
end
|
|
|
|
|
|
|
|
on_idle() -- re-evaluate all profiles immediately
|
|
|
|
end)
|
2020-08-05 20:37:47 +00:00
|
|
|
|
|
|
|
mp.register_idle(on_idle)
|
auto_profiles: register hooks for more synchronous profile application
The property observation mechanism is fairly asynchronous to the player
core, and Lua scripts also are (they run in a separate thread). This may
sometimes lead to profiles being applied when it's too load.
For example, you may want to change network options depending on the
input URL - but most of these options would have to be set before the
HTTP access is made. But it could happen that the profile, and thus the
option, was applied at an slightly but arbitrary later time.
This is generally not fixable. But for the most important use-cases,
such as applying changes before media opening or playback
initialization, we can use some of the defined hooks.
Hooks make it synchronous again, by allowing API users (such as scripts)
to block the core because it continues with loading.
For this we simply don't continue a given hook, until we receive an idle
event, and have applied all changes. The idle event is in general used
to wait for property change notifications to settle down. Some of this
relies on the subtle ways guarantees are (maybe) given. See commit
ba70b150fbe for the messy details. I'm not quite sure whether it
actually works, because I can't be bothered to read and understand my
bullshit from half a year ago. Should provide at least some improvement,
though.
2020-08-05 21:28:24 +00:00
|
|
|
for _, name in ipairs({"on_load", "on_preloaded", "on_before_start_file"}) do
|
|
|
|
mp.add_hook(name, 50, on_hook)
|
|
|
|
end
|