summaryrefslogtreecommitdiff
path: root/mpv/scripts/auto-profiles.lua
diff options
context:
space:
mode:
Diffstat (limited to 'mpv/scripts/auto-profiles.lua')
-rw-r--r--mpv/scripts/auto-profiles.lua198
1 files changed, 198 insertions, 0 deletions
diff --git a/mpv/scripts/auto-profiles.lua b/mpv/scripts/auto-profiles.lua
new file mode 100644
index 0000000..32e1809
--- /dev/null
+++ b/mpv/scripts/auto-profiles.lua
@@ -0,0 +1,198 @@
+--[[
+
+Automatically apply profiles based on runtime conditions.
+At least mpv 0.21.0 is required.
+
+This script queries the list of loaded config profiles, and checks the
+"profile-desc" field of each profile. If it starts with "cond:", the script
+parses the string as Lua expression, and evaluates it. If the expression
+returns true, the profile is applied, if it returns false, it is ignored.
+
+Expressions can reference properties by accessing "p". For example, "p.pause"
+would return the current pause status. If the variable name contains any "_"
+characters, they are turned into "-". For example, "playback_time" would
+return the property "playback-time". (Although you can also just write
+p["playback-time"].)
+
+Note that if a property is not available, it will return nil, which can cause
+errors if used in expressions. These are printed and ignored, and the
+expression is considered to be false. You can also write e.g.
+get("playback-time", 0) instead of p.playback_time to default to 0.
+
+Whenever a property referenced by a profile condition changes, the condition
+is re-evaluated. If the return value of the condition changes from false or
+error to true, the profile is applied.
+
+Note that profiles cannot be "unapplied", so you may have to define inverse
+profiles with inverse conditions do undo a profile.
+
+Using profile-desc is just a hack - maybe it will be changed later.
+
+Supported --script-opts:
+
+ auto-profiles: if set to "no", the script disables itself (but will still
+ listen to property notifications etc. - if you set it to
+ "yes" again, it will re-evaluate the current state)
+
+Example profiles:
+
+# the profile names aren't used (except for logging), but must not clash with
+# other profiles
+[test]
+profile-desc=cond:p.playback_time>10
+video-zoom=2
+
+# you would need this to actually "unapply" the "test" profile
+[test-revert]
+profile-desc=cond:p.playback_time<=10
+video-zoom=0
+
+--]]
+
+local lua_modules = mp.find_config_file('lua-modules')
+if lua_modules then
+ package.path = package.path .. ';' .. lua_modules .. '/?.lua'
+end
+
+local f = require 'auto-profiles-functions'
+local utils = require 'mp.utils'
+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
+
+-- Used during evaluation of the profile condition, and should contain the
+-- profile the condition is evaluated for.
+local current_profile = nil
+
+local function evaluate(profile)
+ msg.verbose("Re-evaluate 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.info("Error evaluating: " .. res)
+ res = false
+ elseif type(res) ~= "boolean" then
+ msg.error("Profile '" .. profile.name .. "' did not return a boolean.")
+ res = false
+ end
+ if res ~= profile.status and res == true then
+ msg.info("Applying profile " .. profile.name)
+ mp.commandv("apply-profile", profile.name)
+ 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()
+ if mp.get_opt("auto-profiles") == "no" then
+ return
+ end
+
+ -- 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
+end
+
+mp.register_idle(on_idle)
+
+local evil_meta_magic = {
+ __index = function(table, key)
+ -- 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
+ -- Lua identifiers can't contain "-", so in order to match with mpv
+ -- property conventions, replace "_" to "-"
+ key = string.gsub(key, "_", "-")
+ -- Normally, we use the cached value only (to reduce CPU usage I guess?)
+ if not watched_properties[key] then
+ watched_properties[key] = true
+ mp.observe_property(key, "native", on_property_change)
+ cached_properties[key] = mp.get_property_native(key)
+ 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[key]
+ if not map then
+ map = {}
+ properties_to_profiles[key] = map
+ end
+ map[current_profile] = true
+ end
+ return cached_properties[key]
+ end,
+}
+
+local evil_magic = {}
+setmetatable(evil_magic, evil_meta_magic)
+
+local function compile_cond(name, s)
+ chunk, err = loadstring("return " .. s, "profile " .. name .. " condition")
+ if not chunk then
+ msg.error("Profile '" .. name .. "' condition: " .. err)
+ return function() return false end
+ end
+ return chunk
+end
+
+for i, v in ipairs(mp.get_property_native("profile-list")) do
+ local desc = v["profile-desc"]
+ if desc and desc:sub(1, 5) == "cond:" then
+ local profile = {
+ name = v.name,
+ cond = compile_cond(v.name, desc:sub(6)),
+ properties = {},
+ status = nil,
+ dirty = true, -- need re-evaluate
+ }
+ profiles[#profiles + 1] = profile
+ have_dirty_profiles = true
+ end
+end
+
+-- these definitions are for use by the condition expressions
+
+p = evil_magic
+
+function get(property_name, default)
+ local val = p[property_name]
+ if val == nil then
+ val = default
+ end
+ return val
+end
+
+-- re-evaluate all profiles immediately
+on_idle()