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. --- DOCS/interface-changes.rst | 2 + DOCS/man/mpv.rst | 131 +++++++++++++++++++++++++++++++++-- DOCS/man/options.rst | 5 ++ options/m_config_frontend.c | 16 +++++ options/m_config_frontend.h | 3 + options/options.c | 4 ++ options/options.h | 1 + options/parse_configfile.c | 3 + player/core.h | 2 +- player/lua.c | 3 + player/lua/auto_profiles.lua | 158 +++++++++++++++++++++++++++++++++++++++++++ player/scripting.c | 2 + wscript_build.py | 3 +- 13 files changed, 326 insertions(+), 7 deletions(-) create mode 100644 player/lua/auto_profiles.lua diff --git a/DOCS/interface-changes.rst b/DOCS/interface-changes.rst index 4cbeb98cce..e58c13216d 100644 --- a/DOCS/interface-changes.rst +++ b/DOCS/interface-changes.rst @@ -72,6 +72,8 @@ Interface changes - remove --video-sync-adrop-size option (implementation was changed, no replacement for what this option did) - undeprecate --video-sync=display-adrop + - deprecate legacy auto profiles (profiles starting with "extension." and + "protocol."). Use conditional auto profiles instead. --- mpv 0.32.0 --- - change behavior when using legacy option syntax with options that start with two dashes (``--`` instead of a ``-``). Now, using the recommended diff --git a/DOCS/man/mpv.rst b/DOCS/man/mpv.rst index dbd700cc9e..a7202864e4 100644 --- a/DOCS/man/mpv.rst +++ b/DOCS/man/mpv.rst @@ -694,10 +694,130 @@ or at runtime with the ``apply-profile `` command. profile=big-cache -Auto profiles -------------- +Conditional auto profiles +------------------------- + +Profiles which have the ``profile-cond`` option set are applied automatically +if the associated condition matches (unless auto profiles are disabled). The +option takes a string, which is interpreted as Lua condition. If evaluating the +expression returns true, the profile is applied, if it returns false, it is +ignored. This Lua code execution is not sandboxed. + +Any variables in condition expressions can reference properties. If an +identifier is not already by defined by Lua or mpv, it is interpreted as +property. For example, ``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``. + +A more robust way to access properties is using ``p.property_name`` or +``get("property-name", default_value)``. The automatic variable to property +magic will break if a new identifier with the same name is introduced (for +example, if a function named ``pause()`` were added, ``pause`` would return a +function value instead of the value of the ``pause`` property). + +Note that if a property is not available, it will return ``nil``, which can +cause errors if used in expressions. These are logged in verbose mode, and the +expression is considered to be false. + +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. + +.. admonition:: Example + + Make only HD video look funny: + + :: + + [something] + profile-desc=HD video sucks + profile-cond=width >= 1280 + hue=-50 + + If you want the profile to be reverted if the condition goes to false again, + you need to do this by manually creating an inverse profile: + + :: + + [something] + profile-desc=Flip video when entering fullscreen + profile-cond=fullscreen + vf=vflip + + [something2] + profile-desc=Inverse of [something] + profile-cond=not fullscreen + vf= + + This sets the video filter chain to ``vflip`` when entering fullscreen. The + first profile does not cause the filter to be removed when leaving + fullscreen. A second profile has to be defined, which is explicitly applied + on leaving fullscreen, and which explicitly clears the filter list. (This + would also clear the filter list at program start when starting the player + in windowed mode.) + +.. warning:: + + Every time an involved property changes, the condition is evaluated again. + If your condition uses ``p.playback_time`` for example, the condition is + re-evaluated approximately on every video frame. This is probably slow. + +This feature is managed by an internal Lua script. Conditions are executed as +Lua code within this script. Its environment contains at least the following +things: + +``(function environment table)`` + Every Lua function has an environment table. This is used for identifier + access. There is no named Lua symbol for it; it is implicit. + + The environment does "magic" accesses to mpv properties. If an identifier + is not already defined in ``_G``, it retrieves the mpv property of the same + name. Any occurrences of ``_`` in the name are replaced with ``-`` before + reading the property. The returned value is as retrieved by + ``mp.get_property_native(name)``. Internally, a cache of property values, + updated by observing the property is used instead, so properties that are + not observable will be stuck at the initial value forever. + + If you want to access properties, that actually contain ``_`` in the name, + use ``get()`` (which does not perform transliteration). + + Internally, the environment table has a ``__index`` meta method set, which + performs the access logic. + +``p`` + A "magic" table similar to the environment table. Unlike the latter, this + does not prefer accessing variables defined in ``_G`` - it always accesses + properties. + +``get(name [, def])`` + Read a property and return its value. If the property value is ``nil`` (e.g. + if the property does not exist), ``def`` is returned. + + This is superficially similar to ``mp.get_property_native(name)``. An + important difference is that this accesses the property cache, and enables + the change detection logic (which is essential to the dynamic runtime + behavior of auto profiles). Also, it does not return an error value as + second return value. + + The "magic" tables mentioned above use this function as backend. It does not + perform the ``_`` transliteration. + +In addition, the same environment as in a blank mpv Lua script is present. For +example, ``math`` is defined and gives access to the Lua standard math library. + +.. warning:: + + This feature is subject to change indefinitely. You might be forced to + adjust your profiles on mpv updates. + +Legacy auto profiles +-------------------- -Some profiles are loaded automatically. The following example demonstrates this: +Some profiles are loaded automatically using a legacy mechanism. The following +example demonstrates this: .. admonition:: Auto profile loading @@ -705,14 +825,15 @@ Some profiles are loaded automatically. The following example demonstrates this: [extension.mkv] profile-desc="profile for .mkv files" - vf=flip + vf=vflip The profile name follows the schema ``type.name``, where type can be ``protocol`` for the input/output protocol in use (see ``--list-protocols``), and ``extension`` for the extension of the path of the currently played file (*not* the file format). -This feature is very limited, and there are no other auto profiles. +This feature is very limited, and is considered soft-deprecated. Use conditional +auto profiles. Using mpv from other programs or scripts ======================================== diff --git a/DOCS/man/options.rst b/DOCS/man/options.rst index 95bec1c3d3..ad7d449b33 100644 --- a/DOCS/man/options.rst +++ b/DOCS/man/options.rst @@ -999,6 +999,11 @@ Program Behavior show the console, and ``ESC`` to hide it again. (This is based on a user script called ``repl.lua``.) +``--load-auto-profiles=`` + Enable the builtin script that does auto profiles (default: auto). See + `Conditional auto profiles`_ for details. ``auto`` will load the script, + but immediately unload it if there are no conditional profiles. + ``--player-operation-mode=`` For enabling "pseudo GUI mode", which means that the defaults for some options are changed. This option should not normally be used directly, but diff --git a/options/m_config_frontend.c b/options/m_config_frontend.c index 467c13eb8f..94cc9c015a 100644 --- a/options/m_config_frontend.c +++ b/options/m_config_frontend.c @@ -50,8 +50,10 @@ struct m_profile { struct m_profile *next; char *name; char *desc; + char *cond; int num_opts; // Option/value pair array. + // name,value = opts[n*2+0],opts[n*2+1] char **opts; }; @@ -85,6 +87,10 @@ static int show_profile(struct m_config *config, bstr param) MP_INFO(config, "Profile %s: %s\n", p->name, p->desc ? p->desc : ""); config->profile_depth++; + if (p->cond) { + MP_INFO(config, "%*sprofile-cond=%s\n", config->profile_depth, "", + p->cond); + } for (int i = 0; i < p->num_opts; i++) { MP_INFO(config, "%*s%s=%s\n", config->profile_depth, "", p->opts[2 * i], p->opts[2 * i + 1]); @@ -884,6 +890,14 @@ void m_profile_set_desc(struct m_profile *p, bstr desc) p->desc = bstrto0(p, desc); } +void m_profile_set_cond(struct m_profile *p, bstr cond) +{ + TA_FREEP(&p->cond); + cond = bstr_strip(cond); + if (cond.len) + p->cond = bstrto0(p, cond); +} + int m_config_set_profile_option(struct m_config *config, struct m_profile *p, bstr name, bstr val) { @@ -944,6 +958,8 @@ struct mpv_node m_config_get_profiles(struct m_config *config) node_map_add_string(entry, "name", profile->name); if (profile->desc) node_map_add_string(entry, "profile-desc", profile->desc); + if (profile->cond) + node_map_add_string(entry, "profile-cond", profile->cond); struct mpv_node *opts = node_map_add(entry, "options", MPV_FORMAT_NODE_ARRAY); diff --git a/options/m_config_frontend.h b/options/m_config_frontend.h index 19fbcadf25..81bc78b6e1 100644 --- a/options/m_config_frontend.h +++ b/options/m_config_frontend.h @@ -232,6 +232,9 @@ struct m_profile *m_config_add_profile(struct m_config *config, char *name); */ void m_profile_set_desc(struct m_profile *p, bstr desc); +// Set auto profile condition of a profile. +void m_profile_set_cond(struct m_profile *p, bstr cond); + /* Add an option to a profile. * Used by the config file parser when defining a profile. * diff --git a/options/options.c b/options/options.c index 4467c106ba..63c99053a8 100644 --- a/options/options.c +++ b/options/options.c @@ -426,6 +426,9 @@ static const m_option_t mp_opts[] = { .flags = UPDATE_BUILTIN_SCRIPTS}, {"load-osd-console", OPT_FLAG(lua_load_console), .flags = UPDATE_BUILTIN_SCRIPTS}, + {"load-auto-profiles", + OPT_CHOICE(lua_load_auto_profiles, {"no", 0}, {"yes", 1}, {"auto", -1}), + .flags = UPDATE_BUILTIN_SCRIPTS}, #endif // ------------------------- stream options -------------------- @@ -944,6 +947,7 @@ static const struct MPOpts mp_default_opts = { .lua_ytdl_raw_options = NULL, .lua_load_stats = 1, .lua_load_console = 1, + .lua_load_auto_profiles = -1, #endif .auto_load_scripts = 1, .loop_times = 1, diff --git a/options/options.h b/options/options.h index 45d0747358..ea5ece65a1 100644 --- a/options/options.h +++ b/options/options.h @@ -144,6 +144,7 @@ typedef struct MPOpts { char **lua_ytdl_raw_options; int lua_load_stats; int lua_load_console; + int lua_load_auto_profiles; int auto_load_scripts; diff --git a/options/parse_configfile.c b/options/parse_configfile.c index 14b30e87b4..96b607d554 100644 --- a/options/parse_configfile.c +++ b/options/parse_configfile.c @@ -131,6 +131,9 @@ int m_config_parse(m_config_t *config, const char *location, bstr data, if (bstr_equals0(option, "profile-desc")) { m_profile_set_desc(profile, value); res = 0; + } else if (bstr_equals0(option, "profile-cond")) { + m_profile_set_cond(profile, value); + res = 0; } else { res = m_config_set_profile_option(config, profile, option, value); } 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) diff --git a/wscript_build.py b/wscript_build.py index 6e5838f771..c57619b353 100644 --- a/wscript_build.py +++ b/wscript_build.py @@ -85,7 +85,8 @@ def build(ctx): ) lua_files = ["defaults.lua", "assdraw.lua", "options.lua", "osc.lua", - "ytdl_hook.lua", "stats.lua", "console.lua"] + "ytdl_hook.lua", "stats.lua", "console.lua", + "auto_profiles.lua"] for fn in lua_files: fn = "player/lua/" + fn -- cgit v1.2.3