From 49d1b42f7088c0d41df346437b64fe20bbaac22f Mon Sep 17 00:00:00 2001 From: wm4 Date: Sat, 5 Apr 2014 23:54:21 +0200 Subject: client API: add a way to notify clients of property changes This turned out ridiculously complex. I think it will have to be simplified some day. Main reason for the complexity are: - filtering properties by forcing clients to observe individual properties explicitly (to avoid spamming clients with changes they don't want) - optional retrieval of property value with the notification (the basic idea was that this is more user friendly) - allowing to the client to specify a format in which the value should be retrieved (because if a property changes its type, the client API couldn't convert it properly, and compatibility would break) I don't know yet which of these are important, and everything could change. In particular, the interface and semantics should be adjusted to reduce the implementation complexity. While I consider the API complete, there could (and probably will) be bugs left. Also while the implementation is complete, it's inefficient. The complexity of the property matching is O(a*b*c) with a clients, b observed properties, and c properties changing at once. I threw away an earlier implementation using bitmasks, because it was too unwieldy. --- libmpv/client.h | 61 +++++++++++++++- player/client.c | 214 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- player/client.h | 1 + player/command.c | 31 ++++++++ player/command.h | 2 + 5 files changed, 306 insertions(+), 3 deletions(-) diff --git a/libmpv/client.h b/libmpv/client.h index ecc9f09275..35c193f64a 100644 --- a/libmpv/client.h +++ b/libmpv/client.h @@ -725,6 +725,57 @@ char *mpv_get_property_osd_string(mpv_handle *ctx, const char *name); int mpv_get_property_async(mpv_handle *ctx, uint64_t reply_userdata, const char *name, mpv_format format); +/** + * Get a notification whenever the given property changes. You will receive + * updates as MPV_EVENT_PROPERTY_CHANGE. Note that this is not very precise: + * it can send updates even if the property in fact did not change, or (in + * some cases) not send updates even if the property changed - it usually + * depends on the property. It's a valid feature request to ask for better + * update handling of a specific property. + * + * Property changes are coalesced: the change events are returned only once the + * event queue becomes empty (e.g. mpv_wait_event() would block or return + * MPV_EVENT_NONE), and then only one event per changed property is returned. + * + * Keep in mind that you will get change notifications even if you change a + * property yourself. Try to avoid endless feedback loops, which could happen + * if you react to change notifications which you caused yourself. + * + * If the format parameter is set to something other than MPV_FORMAT_NONE, the + * current property value will be returned as part of mpv_event_property. + * + * Warning: if a property is unavailable or retrieving it caused an error, + * MPV_FORMAT_NONE will be set in mpv_event_property, even if the + * format parameter was set to a different value. In this case, the + * mpv_event_property.data field is invalid. + * + * Observing a property that doesn't exist is allowed, although it may still + * cause some sporadic change events. + * + * @param reply_userdata This will be used for the mpv_event.reply_userdata + * field for the received MPV_EVENT_PROPERTY_CHANGE + * events. (Also see section about asynchronous calls, + * although this function is somewhat different from + * actual asynchronous calls.) + * Also see mpv_unobserve_property(). + * @param name The property name. + * @param format see enum mpv_format. Can be MPV_FORMAT_NONE to omit values + * from the change events. + * @return error code (usually fails only on OOM) + */ +int mpv_observe_property(mpv_handle *mpv, uint64_t reply_userdata, + const char *name, mpv_format format); + +/** + * Undo mpv_observe_property(). This will remove all observed properties for + * which the given number was passed as reply_userdata to mpv_observe_property. + * + * @param registered_reply_userdata ID that was passed to mpv_observe_property + * @return negative value is an error code, number of removed properties on + * success (includes the case when 0 were removed) + */ +int mpv_unobserve_property(mpv_handle *mpv, uint64_t registered_reply_userdata); + typedef enum mpv_event_id { /** * Nothing happened. Happens on timeouts or sporadic wakeups. @@ -843,7 +894,12 @@ typedef enum mpv_event_id { * segment switches. The main purpose is allowing the client to detect * when a seek request is finished. */ - MPV_EVENT_PLAYBACK_RESTART = 21 + MPV_EVENT_PLAYBACK_RESTART = 21, + /** + * Event sent due to mpv_observe_property(). + * See also mpv_event and mpv_event_property. + */ + MPV_EVENT_PROPERTY_CHANGE = 22 } mpv_event_id; /** @@ -980,8 +1036,9 @@ typedef struct mpv_event { */ uint64_t reply_userdata; /** - * The meaning and contents of data member depend on the event_id: + * The meaning and contents of the data member depend on the event_id: * MPV_EVENT_GET_PROPERTY_REPLY: mpv_event_property* + * MPV_EVENT_PROPERTY_CHANGE: mpv_event_property* * MPV_EVENT_LOG_MESSAGE: mpv_event_log_message* * MPV_EVENT_PAUSE: mpv_event_pause_reason* * MPV_EVENT_UNPAUSE: mpv_event_pause_reason* diff --git a/player/client.c b/player/client.c index f33dcc2bf6..f35cb4a659 100644 --- a/player/client.c +++ b/player/client.c @@ -12,6 +12,7 @@ */ #include +#include #include #include #include @@ -30,6 +31,18 @@ #include "core.h" #include "client.h" +/* + * Locking hierarchy: + * + * MPContext > mp_client_api.lock > mpv_handle.lock + * + * MPContext strictly speaking has no locks, and instead implicitly managed + * by MPContext.dispatch, which basically stops the playback thread at defined + * points in order to let clients access it in a synchronized manner. Since + * MPContext code accesses the client API, it's on top of the lock hierarchy. + * + */ + struct mp_client_api { struct MPContext *mpctx; @@ -40,6 +53,19 @@ struct mp_client_api { int num_clients; }; +struct observe_property { + char *name; + int64_t reply_id; + mpv_format format; + bool changed; // property change should be signaled to user + bool need_new_value; // a new value should be retrieved + bool updating; // a new value is being retrieved + bool dead; // property unobserved while retrieving value + bool value_valid; + union m_option_value value; + struct mpv_handle *client; +}; + struct mpv_handle { // -- immmutable char *name; @@ -49,6 +75,7 @@ struct mpv_handle { // -- not thread-safe struct mpv_event *cur_event; + struct mpv_event_property cur_property_event; pthread_mutex_t lock; pthread_cond_t wakeup; @@ -68,10 +95,17 @@ struct mpv_handle { int num_events; // number of readable events int reserved_events; // number of entries reserved for replies + struct observe_property **properties; + int num_properties; + int lowest_changed; + int properties_updating; + struct mp_log_buffer *messages; int messages_level; }; +static bool gen_property_change_event(struct mpv_handle *ctx); + void mp_clients_init(struct MPContext *mpctx) { mpctx->clients = talloc_ptrtype(NULL, mpctx->clients); @@ -198,7 +232,7 @@ void mpv_destroy(mpv_handle *ctx) // yet replied. In order to avoid that trying to reply to a removed client // causes a crash, block until all asynchronous requests were served. ctx->event_mask = 0; - while (ctx->reserved_events) + while (ctx->reserved_events || ctx->properties_updating) pthread_cond_wait(&ctx->wakeup, &ctx->lock); pthread_mutex_unlock(&ctx->lock); @@ -440,6 +474,8 @@ mpv_event *mpv_wait_event(mpv_handle *ctx, double timeout) talloc_steal(event, event->data); break; } + if (gen_property_change_event(ctx)) + break; if (ctx->shutdown) { event->event_id = MPV_EVENT_SHUTDOWN; break; @@ -941,6 +977,181 @@ int mpv_get_property_async(mpv_handle *ctx, uint64_t ud, const char *name, return run_async(ctx, getproperty_fn, req); } +static void property_free(void *p) +{ + struct observe_property *prop = p; + const struct m_option *type = get_mp_type_get(prop->format); + if (type) + m_option_free(type, &prop->value); +} + +int mpv_observe_property(mpv_handle *ctx, uint64_t userdata, + const char *name, mpv_format format) +{ + if (format != MPV_FORMAT_NONE && !get_mp_type_get(format)) + return MPV_ERROR_PROPERTY_FORMAT; + // Explicitly disallow this, because it would require a special code path. + if (format == MPV_FORMAT_OSD_STRING) + return MPV_ERROR_PROPERTY_FORMAT; + + pthread_mutex_lock(&ctx->lock); + struct observe_property *prop = talloc_ptrtype(ctx, prop); + talloc_set_destructor(prop, property_free); + *prop = (struct observe_property){ + .client = ctx, + .name = talloc_strdup(prop, name), + .reply_id = userdata, + .format = format, + .changed = true, + .need_new_value = true, + }; + MP_TARRAY_APPEND(ctx, ctx->properties, ctx->num_properties, prop); + ctx->lowest_changed = 0; + pthread_mutex_unlock(&ctx->lock); + return 0; +} + +int mpv_unobserve_property(mpv_handle *ctx, uint64_t userdata) +{ + pthread_mutex_lock(&ctx->lock); + int count = 0; + for (int n = ctx->num_properties - 1; n >= 0; n--) { + struct observe_property *prop = ctx->properties[n]; + if (prop->reply_id == userdata) { + if (prop->updating) { + prop->dead = true; + } else { + // In case mpv_unobserve_property() is called after mpv_wait_event() + // returned, and the mpv_event still references the name somehow, + // make sure it's not freed while in use. The same can happen + // with the value update mechanism. + talloc_steal(ctx->cur_event, prop); + } + MP_TARRAY_REMOVE_AT(ctx->properties, ctx->num_properties, n); + count++; + } + } + ctx->lowest_changed = 0; + pthread_mutex_unlock(&ctx->lock); + return count; +} + +static int prefix_len(const char *p) +{ + const char *end = strchr(p, '/'); + return end ? end - p : strlen(p); +} + +static bool match_property(const char *a, const char *b) +{ + if (strcmp(b, "*") == 0) + return true; + int len_a = prefix_len(a); + int len_b = prefix_len(b); + return strncmp(a, b, MPMIN(len_a, len_b)) == 0; +} + +// Broadcast that properties have changed. +void mp_client_property_change(struct MPContext *mpctx, const char **list) +{ + struct mp_client_api *clients = mpctx->clients; + + pthread_mutex_lock(&clients->lock); + + for (int n = 0; n < clients->num_clients; n++) { + struct mpv_handle *client = clients->clients[n]; + pthread_mutex_lock(&client->lock); + + client->lowest_changed = client->num_properties; + for (int i = 0; i < client->num_properties; i++) { + struct observe_property *prop = client->properties[i]; + if (!prop->changed && !prop->need_new_value) { + for (int x = 0; list && list[x]; x++) { + if (match_property(prop->name, list[x])) { + prop->changed = prop->need_new_value = true; + break; + } + } + } + if ((prop->changed || prop->updating) && i < client->lowest_changed) + client->lowest_changed = i; + } + if (client->lowest_changed < client->num_properties) + wakeup_client(client); + pthread_mutex_unlock(&client->lock); + } + + pthread_mutex_unlock(&clients->lock); +} + +static void update_prop(void *p) +{ + struct observe_property *prop = p; + struct mpv_handle *ctx = prop->client; + + const struct m_option *type = get_mp_type_get(prop->format); + union m_option_value val = {0}; + + struct getproperty_request req = { + .mpctx = ctx->mpctx, + .name = prop->name, + .format = prop->format, + .data = &val, + }; + + getproperty_fn(&req); + + pthread_mutex_lock(&ctx->lock); + ctx->properties_updating--; + prop->updating = false; + prop->changed = true; + prop->value_valid = req.status >= 0; + if (prop->value_valid) { + m_option_free(type, &prop->value); + memcpy(&prop->value, &val, type->type->size); + } + if (prop->dead) + talloc_steal(ctx->cur_event, prop); + wakeup_client(ctx); + pthread_mutex_unlock(&ctx->lock); +} + +// Set ctx->cur_event to a generated property change event, if there is any +// outstanding property. +static bool gen_property_change_event(struct mpv_handle *ctx) +{ + int start = ctx->lowest_changed; + ctx->lowest_changed = ctx->num_properties; + for (int n = start; n < ctx->num_properties; n++) { + struct observe_property *prop = ctx->properties[n]; + if ((prop->changed || prop->updating) && n < ctx->lowest_changed) + ctx->lowest_changed = n; + if (prop->changed) { + bool new_val = prop->need_new_value; + prop->changed = prop->need_new_value = false; + if (prop->format && new_val) { + ctx->properties_updating++; + prop->updating = true; + mp_dispatch_enqueue(ctx->mpctx->dispatch, update_prop, prop); + } else { + ctx->cur_property_event = (struct mpv_event_property){ + .name = prop->name, + .format = prop->value_valid ? prop->format : 0, + }; + if (prop->value_valid) + ctx->cur_property_event.data = &prop->value; + *ctx->cur_event = (struct mpv_event){ + .event_id = MPV_EVENT_PROPERTY_CHANGE, + .reply_userdata = prop->reply_id, + .data = &ctx->cur_property_event, + }; + return true; + } + } + } + return false; +} + int mpv_request_log_messages(mpv_handle *ctx, const char *min_level) { int level = -1; @@ -1026,6 +1237,7 @@ static const char *event_table[] = { [MPV_EVENT_METADATA_UPDATE] = "metadata-update", [MPV_EVENT_SEEK] = "seek", [MPV_EVENT_PLAYBACK_RESTART] = "playback-restart", + [MPV_EVENT_PROPERTY_CHANGE] = "property-change", }; const char *mpv_event_name(mpv_event_id event) diff --git a/player/client.h b/player/client.h index 4c0c23c614..6e078e9d7b 100644 --- a/player/client.h +++ b/player/client.h @@ -17,6 +17,7 @@ int mp_clients_num(struct MPContext *mpctx); void mp_client_broadcast_event(struct MPContext *mpctx, int event, void *data); int mp_client_send_event(struct MPContext *mpctx, const char *client_name, int event, void *data); +void mp_client_property_change(struct MPContext *mpctx, const char **list); struct mpv_handle *mp_new_client(struct mp_client_api *clients, const char *name); struct mp_log *mp_client_get_log(struct mpv_handle *ctx); diff --git a/player/command.c b/player/command.c index 14654af14b..127ef1e42d 100644 --- a/player/command.c +++ b/player/command.c @@ -23,6 +23,7 @@ #include #include #include +#include #include #include @@ -2272,6 +2273,29 @@ static const m_option_t mp_properties[] = { {0}, }; +// Each entry describes which properties an event (possibly) changes. +#define E(x, ...) [x] = (const char*[]){__VA_ARGS__, NULL} +const char **mp_event_property_change[] = { + E(MPV_EVENT_START_FILE, "*"), + E(MPV_EVENT_END_FILE, "*"), + E(MPV_EVENT_FILE_LOADED, "*"), + E(MPV_EVENT_TRACKS_CHANGED, "track-list"), + E(MPV_EVENT_TRACK_SWITCHED, "vid", "video", "aid", "audio", "sid", "sub", + "secondary-sid"), + E(MPV_EVENT_IDLE, "*"), + E(MPV_EVENT_PAUSE, "pause"), + E(MPV_EVENT_UNPAUSE, "pause"), + E(MPV_EVENT_TICK, "time-pos", "stream-pos", "stream-time-pos", "avsync", + "percent-pos", "time-remaining", "playtime-remaining"), + E(MPV_EVENT_VIDEO_RECONFIG, "video-out-params", "video-params", + "video-format", "video-codec", "video-bitrate", "dwidth", "dheight", + "width", "height", "fps", "aspect"), + E(MPV_EVENT_AUDIO_RECONFIG, "audio-format", "audio-codec", "audio-bitrate", + "samplerate", "channels", "audio"), + E(MPV_EVENT_METADATA_UPDATE, "metadata"), +}; +#undef E + const struct m_option *mp_get_property_list(void) { return mp_properties; @@ -3468,4 +3492,11 @@ void mp_notify(struct MPContext *mpctx, int event, void *arg) ctx->last_seek_pts = MP_NOPTS_VALUE; mp_client_broadcast_event(mpctx, event, arg); + if (event >= 0 && event < MP_ARRAY_SIZE(mp_event_property_change)) + mp_client_property_change(mpctx, mp_event_property_change[event]); +} + +void mp_notify_property(struct MPContext *mpctx, char *property) +{ + mp_client_property_change(mpctx, (const char*[]){property, NULL}); } diff --git a/player/command.h b/player/command.h index a04bfac343..795a759906 100644 --- a/player/command.h +++ b/player/command.h @@ -34,7 +34,9 @@ int mp_property_do(const char* name, int action, void* val, struct MPContext *mpctx); const struct m_option *mp_get_property_list(void); +int mp_find_property_index(const char *property); void mp_notify(struct MPContext *mpctx, int event, void *arg); +void mp_notify_property(struct MPContext *mpctx, char *property); #endif /* MPLAYER_COMMAND_H */ -- cgit v1.2.3