diff --git a/DOCS/man/input.rst b/DOCS/man/input.rst index 7c93843619..082776f66d 100644 --- a/DOCS/man/input.rst +++ b/DOCS/man/input.rst @@ -1253,6 +1253,12 @@ the player freeze randomly. Basically, nobody should use this API. The C API is described in the header files. The Lua API is described in the Lua section. +Before a hook is actually invoked on an API clients, it will attempt to return +new values for all observed properties that were changed before the hook. This +may make it easier for an application to set defined "barriers" between property +change notifications by registering hooks. (That means these hooks will have an +effect, even if you do nothing and make them continue immediately.) + The following hooks are currently defined: ``on_load`` @@ -1286,6 +1292,16 @@ The following hooks are currently defined: Run before closing a file, and before actually uninitializing everything. It's not possible to resume playback in this state. +``on_before_start_file`` + Run before a ``start-file`` event is sent. (If any client changes the + current playlist entry, or sends a quit command to the player, the + corresponding event will not actually happen after the hook returns.) + Useful to drain property changes before a new file is loaded. + +``on_after_end_file`` + Run after an ``end-file`` event. Useful to drain property changes after a + file has finished. + Input Command Prefixes ---------------------- diff --git a/TOOLS/lua/test-hooks.lua b/TOOLS/lua/test-hooks.lua new file mode 100644 index 0000000000..4e84d9e465 --- /dev/null +++ b/TOOLS/lua/test-hooks.lua @@ -0,0 +1,32 @@ +local utils = require("mp.utils") + +function hardsleep() + os.execute("sleep 1s") +end + +local hooks = {"on_before_start_file", "on_load", "on_load_fail", + "on_preloaded", "on_unload", "on_after_end_file"} + +for _, name in ipairs(hooks) do + mp.add_hook(name, 0, function() + print("--- hook: " .. name) + hardsleep() + print(" ... continue") + end) +end + +local events = {"start-file", "end-file", "file-loaded", "seek", + "playback-restart", "idle", "shutdown"} +for _, name in ipairs(events) do + mp.register_event(name, function() + print("--- event: " .. name) + end) +end + +local props = {"path", "metadata"} +for _, name in ipairs(props) do + mp.observe_property(name, "native", function(name, val) + print("property '" .. name .. "' changed to '" .. + utils.to_string(val) .. "'") + end) +end diff --git a/player/client.c b/player/client.c index 815c64c005..460632805f 100644 --- a/player/client.c +++ b/player/client.c @@ -102,6 +102,7 @@ struct observe_property { union m_option_value value; uint64_t value_ret_ts; // logical timestamp of value returned to user union m_option_value value_ret; + bool waiting_for_hook; // flag for draining old property changes on a hook }; struct mpv_handle { @@ -140,6 +141,7 @@ struct mpv_handle { size_t async_counter; // pending other async events bool choked; // recovering from queue overflow bool destroying; // pending destruction; no API accesses allowed + bool hook_pending; // hook events are returned after draining properties struct observe_property **properties; int num_properties; @@ -847,6 +849,27 @@ int mpv_request_event(mpv_handle *ctx, mpv_event_id event, int enable) return 0; } +// Set waiting_for_hook==true for all possibly pending properties. +static void set_wait_for_hook_flags(mpv_handle *ctx) +{ + for (int n = 0; n < ctx->num_properties; n++) { + struct observe_property *prop = ctx->properties[n]; + + if (prop->value_ret_ts != prop->change_ts) + prop->waiting_for_hook = true; + } +} + +// Return whether any property still has waiting_for_hook set. +static bool check_for_for_hook_flags(mpv_handle *ctx) +{ + for (int n = 0; n < ctx->num_properties; n++) { + if (ctx->properties[n]->waiting_for_hook) + return true; + } + return false; +} + mpv_event *mpv_wait_event(mpv_handle *ctx, double timeout) { mpv_event *event = ctx->cur_event; @@ -874,8 +897,24 @@ mpv_event *mpv_wait_event(mpv_handle *ctx, double timeout) event->event_id = MPV_EVENT_QUEUE_OVERFLOW; break; } - if (ctx->num_events) { - *event = ctx->events[ctx->first_event]; + struct mpv_event *ev = + ctx->num_events ? &ctx->events[ctx->first_event] : NULL; + if (ev && ev->event_id == MPV_EVENT_HOOK) { + // Give old property notifications priority over hooks. This is a + // guarantee given to clients to simplify their logic. New property + // changes after this are treated normally, so + if (!ctx->hook_pending) { + ctx->hook_pending = true; + set_wait_for_hook_flags(ctx); + } + if (check_for_for_hook_flags(ctx)) { + ev = NULL; // delay + } else { + ctx->hook_pending = false; + } + } + if (ev) { + *event = *ev; ctx->first_event = (ctx->first_event + 1) % ctx->max_events; ctx->num_events--; talloc_steal(event, event->data); @@ -1608,7 +1647,7 @@ static void send_client_property_changes(struct mpv_handle *ctx) ctx->async_counter -= 1; prop_unref(prop); - // Set of observed properties was changed or something similar + // Set if observed properties was changed or something similar // => start over, retry next time. if (cur_ts != ctx->properties_change_ts || ctx->destroying) { m_option_free(type, &val); @@ -1638,10 +1677,14 @@ static void send_client_property_changes(struct mpv_handle *ctx) changed = true; } + if (prop->waiting_for_hook) + ctx->new_property_events = true; // make sure to wakeup + // Avoid retriggering the change event if the property didn't change, // and the previous value was actually returned to the client. if (!changed && prop->value_ret_ts == prop->value_ts) { prop->value_ret_ts = prop->change_ts; // no change => no event + prop->waiting_for_hook = false; } else { ctx->new_property_events = true; } @@ -1704,6 +1747,7 @@ static bool gen_property_change_event(struct mpv_handle *ctx) prop->value_ret_ts != prop->value_ts) // other value than last time? { prop->value_ret_ts = prop->value_ts; + prop->waiting_for_hook = false; prop_unref(ctx->cur_property); ctx->cur_property = prop; prop->refcount += 1; diff --git a/player/loadfile.c b/player/loadfile.c index d2bdb47ccd..9eb58c0f4d 100644 --- a/player/loadfile.c +++ b/player/loadfile.c @@ -931,7 +931,8 @@ static void process_hooks(struct MPContext *mpctx, char *name) while (!mp_hook_test_completion(mpctx, name)) { mp_idle(mpctx); - // We have no idea what blocks a hook, so just do a full abort. + // We have no idea what blocks a hook, so just do a full abort. This + // does nothing for hooks that happen outside of playback. if (mpctx->stop_play) mp_abort_playback_async(mpctx); } @@ -1374,13 +1375,17 @@ static void play_current_file(struct MPContext *mpctx) double playback_start = -1e100; assert(mpctx->stop_play); + mpctx->stop_play = 0; + + process_hooks(mpctx, "on_before_start_file"); + if (mpctx->stop_play) + return; mp_notify(mpctx, MPV_EVENT_START_FILE, NULL); mp_cancel_reset(mpctx->playback_abort); mpctx->error_playing = MPV_ERROR_LOADING_FAILED; - mpctx->stop_play = 0; mpctx->filename = NULL; mpctx->shown_aframes = 0; mpctx->shown_vframes = 0; @@ -1701,6 +1706,8 @@ terminate_playback: } assert(mpctx->stop_play); + + process_hooks(mpctx, "on_after_end_file"); } // Determine the next file to play. Note that if this function returns non-NULL,