From 13d354e46d27fd0c433880839abcf9096dbcbc2f Mon Sep 17 00:00:00 2001 From: wm4 Date: Wed, 5 Aug 2020 22:37:47 +0200 Subject: auto_profiles: add this script This is taken from a somewhat older proof-of-concept script. The basic idea, and most of the implementation, is still the same. The way the profiles are actually defined changed. I still feel bad about this being a Lua script, and running user expressions as Lua code in a vaguely defined environment, but I guess as far as balance of effort/maintenance/results goes, this is fine. It's a bit bloated (the Lua scripting state is at least 150KB or so in total), so in order to enable this by default, I decided it should unload itself by default if no auto-profiles are used. (And currently, it does not actually rescan the profile list if a new config file is loaded some time later, so the script would do nothing anyway if no auto profiles were defined.) This still requires defining inverse profiles for "unapplying" a profile. Also this is still somewhat racy. Both will probably be alleviated to some degree in the future. --- player/core.h | 2 +- player/lua.c | 3 + player/lua/auto_profiles.lua | 158 +++++++++++++++++++++++++++++++++++++++++++ player/scripting.c | 2 + 4 files changed, 164 insertions(+), 1 deletion(-) create mode 100644 player/lua/auto_profiles.lua (limited to 'player') diff --git a/player/core.h b/player/core.h index 8bafb707f4..9b468492bf 100644 --- a/player/core.h +++ b/player/core.h @@ -445,7 +445,7 @@ typedef struct MPContext { struct mp_ipc_ctx *ipc_ctx; - int64_t builtin_script_ids[4]; + int64_t builtin_script_ids[5]; pthread_mutex_t abort_lock; diff --git a/player/lua.c b/player/lua.c index d1badac3b4..440e3c6ff8 100644 --- a/player/lua.c +++ b/player/lua.c @@ -78,6 +78,9 @@ static const char * const builtin_lua_scripts[][2] = { }, {"@console.lua", # include "generated/player/lua/console.lua.inc" + }, + {"@auto_profiles.lua", +# include "generated/player/lua/auto_profiles.lua.inc" }, {0} }; diff --git a/player/lua/auto_profiles.lua b/player/lua/auto_profiles.lua new file mode 100644 index 0000000000..aebef28688 --- /dev/null +++ b/player/lua/auto_profiles.lua @@ -0,0 +1,158 @@ +-- Note: anything global is accessible by profile condition expressions. + +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-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 + elseif type(res) ~= "boolean" then + msg.verbose("Profile condition did not return a boolean, but " + .. type(res) .. ".") + res = false + end + if res ~= profile.status and res == true then + msg.info("Applying auto 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() + -- 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 + +function get(name, default) + -- Normally, we use the cached value only + if not watched_properties[name] then + watched_properties[name] = true + mp.observe_property(name, "native", on_property_change) + cached_properties[name] = mp.get_property_native(name) + 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, { + __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 + return magic_get(key) + end, +}) + +p = {} +setmetatable(p, { + __index = function(table, key) + return magic_get(key) + end, +}) + +local function compile_cond(name, s) + -- (pre 5.2 ignores the extra arguments) + local chunk, err = load("return " .. s, "profile " .. name .. " condition", + "t", evil_magic) + if not chunk then + msg.error("Profile '" .. name .. "' condition: " .. err) + chunk = function() return false end + end + if setfenv then + setfenv(chunk, evil_magic) + end + return chunk +end + +local function load_profiles() + for i, v in ipairs(mp.get_property_native("profile-list")) do + 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 + } + profiles[#profiles + 1] = profile + have_dirty_profiles = true + end + end +end + +load_profiles() + +if #profiles < 1 and mp.get_property("load-auto-profiles") == "auto" then + -- make it exist immediately + _G.mp_event_loop = function() end + return +end + +mp.register_idle(on_idle) +on_idle() -- re-evaluate all profiles immediately diff --git a/player/scripting.c b/player/scripting.c index 6b891d92aa..24e2931539 100644 --- a/player/scripting.c +++ b/player/scripting.c @@ -262,6 +262,8 @@ void mp_load_builtin_scripts(struct MPContext *mpctx) load_builtin_script(mpctx, 1, mpctx->opts->lua_load_ytdl, "@ytdl_hook.lua"); load_builtin_script(mpctx, 2, mpctx->opts->lua_load_stats, "@stats.lua"); load_builtin_script(mpctx, 3, mpctx->opts->lua_load_console, "@console.lua"); + load_builtin_script(mpctx, 4, mpctx->opts->lua_load_auto_profiles, + "@auto_profiles.lua"); } bool mp_load_scripts(struct MPContext *mpctx) -- cgit v1.2.3