mirror of
https://github.com/mpv-player/mpv
synced 2025-03-20 02:09:52 +00:00
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.
This commit is contained in:
parent
14eb233da9
commit
49d1b42f70
@ -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*
|
||||
|
214
player/client.c
214
player/client.c
@ -12,6 +12,7 @@
|
||||
*/
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <errno.h>
|
||||
#include <assert.h>
|
||||
@ -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)
|
||||
|
@ -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);
|
||||
|
@ -23,6 +23,7 @@
|
||||
#include <stdbool.h>
|
||||
#include <assert.h>
|
||||
#include <time.h>
|
||||
#include <pthread.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
#include <libavutil/avstring.h>
|
||||
@ -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});
|
||||
}
|
||||
|
@ -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 */
|
||||
|
Loading…
Reference in New Issue
Block a user