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.
This commit is contained in:
wm4 2020-08-05 22:37:47 +02:00
parent f457f3839a
commit 13d354e46d
13 changed files with 326 additions and 7 deletions

View File

@ -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

View File

@ -694,10 +694,130 @@ or at runtime with the ``apply-profile <name>`` command.
profile=big-cache
Auto profiles
-------------
Conditional auto profiles
-------------------------
Some profiles are loaded automatically. The following example demonstrates this:
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 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
========================================

View File

@ -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=<yes|no|auto>``
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=<cplayer|pseudo-gui>``
For enabling "pseudo GUI mode", which means that the defaults for some
options are changed. This option should not normally be used directly, but

View File

@ -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);

View File

@ -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.
*

View File

@ -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,

View File

@ -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;

View File

@ -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);
}

View File

@ -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;

View File

@ -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}
};

View File

@ -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

View File

@ -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)

View File

@ -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