From 88ae914b1ef2b76362c527985bd459b0d8226d45 Mon Sep 17 00:00:00 2001 From: wm4 Date: Mon, 10 Feb 2014 21:01:35 +0100 Subject: Add a client API Add a client API, which is intended to be a stable API to get some rough control over the player. Basically, it reflects what can be done with input.conf commands or the old slavemode. It will replace the old slavemode (and enable the implementation of a new slave protocol). --- player/client.c | 856 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ player/client.h | 24 ++ player/command.c | 29 +- player/command.h | 14 +- player/core.h | 9 +- player/loadfile.c | 19 +- player/main.c | 208 ++++++++----- player/playloop.c | 15 +- 8 files changed, 1056 insertions(+), 118 deletions(-) create mode 100644 player/client.c create mode 100644 player/client.h (limited to 'player') diff --git a/player/client.c b/player/client.c new file mode 100644 index 0000000000..937d5bad84 --- /dev/null +++ b/player/client.c @@ -0,0 +1,856 @@ +/* Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include +#include +#include + +#include "common/common.h" +#include "common/msg.h" +#include "common/msg_control.h" +#include "input/input.h" +#include "misc/ring.h" +#include "options/m_config.h" +#include "options/m_option.h" +#include "options/m_property.h" +#include "osdep/threads.h" + +#include "command.h" +#include "core.h" +#include "client.h" + +struct mp_client_api { + struct MPContext *mpctx; + + pthread_mutex_t lock; + + // -- protected by lock + struct mpv_handle **clients; + int num_clients; +}; + +struct mpv_handle { + // -- immmutable + char *name; + struct mp_log *log; + struct MPContext *mpctx; + struct mp_client_api *clients; + + // -- not thread-safe + struct mpv_event *cur_event; + + pthread_mutex_t lock; + pthread_cond_t wakeup; + + // -- protected by lock + + uint64_t event_mask; + bool queued_wakeup; + bool shutdown; + bool choke_warning; + void (*wakeup_cb)(void *d); + void *wakeup_cb_ctx; + + struct mp_ring *events; // stores mpv_event + int max_events; // allocated number of entries in events + int reserved_events; // number of entries reserved for replies + + struct mp_log_buffer *messages; + int messages_level; +}; + +void mp_clients_init(struct MPContext *mpctx) +{ + mpctx->clients = talloc_ptrtype(NULL, mpctx->clients); + *mpctx->clients = (struct mp_client_api) { + .mpctx = mpctx, + }; + pthread_mutex_init(&mpctx->clients->lock, NULL); +} + +void mp_clients_destroy(struct MPContext *mpctx) +{ + if (!mpctx->clients) + return; + assert(mpctx->clients->num_clients == 0); + pthread_mutex_destroy(&mpctx->clients->lock); + talloc_free(mpctx->clients); + mpctx->clients = NULL; +} + +int mp_clients_num(struct MPContext *mpctx) +{ + pthread_mutex_lock(&mpctx->clients->lock); + int num_clients = mpctx->clients->num_clients; + pthread_mutex_unlock(&mpctx->clients->lock); + return num_clients; +} + +static struct mpv_handle *find_client(struct mp_client_api *clients, + const char *name) +{ + for (int n = 0; n < clients->num_clients; n++) { + if (strcmp(clients->clients[n]->name, name) == 0) + return clients->clients[n]; + } + return NULL; +} + +struct mpv_handle *mp_new_client(struct mp_client_api *clients, const char *name) +{ + pthread_mutex_lock(&clients->lock); + + char *unique_name = NULL; + if (find_client(clients, name)) { + for (int n = 2; n < 1000; n++) { + unique_name = talloc_asprintf(NULL, "%s%d", name, n); + if (!find_client(clients, unique_name)) + break; + talloc_free(unique_name); + unique_name = NULL; + } + if (!unique_name) { + pthread_mutex_unlock(&clients->lock); + return NULL; + } + } + if (!unique_name) + unique_name = talloc_strdup(NULL, name); + + int num_events = 1000; + + struct mpv_handle *client = talloc_ptrtype(NULL, client); + *client = (struct mpv_handle){ + .name = talloc_steal(client, unique_name), + .log = mp_log_new(client, clients->mpctx->log, unique_name), + .mpctx = clients->mpctx, + .clients = clients, + .cur_event = talloc_zero(client, struct mpv_event), + .events = mp_ring_new(client, num_events * sizeof(struct mpv_event)), + .max_events = num_events, + .event_mask = ((uint64_t)-1) & ~(1ULL << MPV_EVENT_TICK), + }; + pthread_mutex_init(&client->lock, NULL); + pthread_cond_init(&client->wakeup, NULL); + + MP_TARRAY_APPEND(clients, clients->clients, clients->num_clients, client); + + pthread_mutex_unlock(&clients->lock); + + return client; +} + +const char *mpv_client_name(mpv_handle *ctx) +{ + return ctx->name; +} + +struct mp_log *mp_client_get_log(struct mpv_handle *ctx) +{ + return ctx->log; +} + +static void wakeup_client(struct mpv_handle *ctx) +{ + pthread_cond_signal(&ctx->wakeup); + if (ctx->wakeup_cb) + ctx->wakeup_cb(ctx->wakeup_cb_ctx); +} + +void mpv_set_wakeup_callback(mpv_handle *ctx, void (*cb)(void *d), void *d) +{ + pthread_mutex_lock(&ctx->lock); + ctx->wakeup_cb = cb; + ctx->wakeup_cb_ctx = d; + pthread_mutex_unlock(&ctx->lock); +} + +void mpv_suspend(mpv_handle *ctx) +{ + mp_dispatch_suspend(ctx->mpctx->dispatch); +} + +void mpv_resume(mpv_handle *ctx) +{ + mp_dispatch_resume(ctx->mpctx->dispatch); +} + +void mpv_destroy(mpv_handle *ctx) +{ + struct mp_client_api *clients = ctx->clients; + + pthread_mutex_lock(&clients->lock); + for (int n = 0; n < clients->num_clients; n++) { + if (clients->clients[n] == ctx) { + MP_TARRAY_REMOVE_AT(clients->clients, clients->num_clients, n); + while (mp_ring_buffered(ctx->events)) { + struct mpv_event event; + int r = mp_ring_read(ctx->events, (unsigned char *)&event, + sizeof(event)); + assert(r == sizeof(event)); + talloc_free(event.data); + } + mp_msg_log_buffer_destroy(ctx->messages); + pthread_cond_destroy(&ctx->wakeup); + pthread_mutex_destroy(&ctx->lock); + talloc_free(ctx); + ctx = NULL; + // shutdown_clients() sleeps to avoid wasting CPU + mp_input_wakeup(clients->mpctx->input); + // TODO: make core quit if there are no clients + break; + } + } + pthread_mutex_unlock(&clients->lock); + assert(!ctx); +} + +mpv_handle *mpv_create(void) +{ + struct MPContext *mpctx = mp_create(); + mpv_handle *ctx = mp_new_client(mpctx->clients, "main"); + if (ctx) { + // Set some defaults. + mpv_set_option_string(ctx, "idle", "yes"); + mpv_set_option_string(ctx, "terminal", "no"); + mpv_set_option_string(ctx, "osc", "no"); + mpv_set_option_string(ctx, "input-default-bindings", "no"); + } else { + mp_destroy(mpctx); + } + return ctx; +} + +static void *playback_thread(void *p) +{ + struct MPContext *mpctx = p; + + pthread_detach(pthread_self()); + + mp_play_files(mpctx); + + // This actually waits until all clients are gone before actually + // destroying mpctx. + mp_destroy(mpctx); + + return NULL; +} + +int mpv_initialize(mpv_handle *ctx) +{ + if (mp_initialize(ctx->mpctx) < 0) + return MPV_ERROR_INVALID_PARAMETER; + + pthread_t thread; + if (pthread_create(&thread, NULL, playback_thread, ctx->mpctx) != 0) + return MPV_ERROR_NOMEM; + + return 0; +} + +// Reserve an entry in the ring buffer. This can be used to guarantee that the +// reply can be made, even if the buffer becomes congested _after_ sending +// the request. +// Returns an error code if the buffer is full. +static int reserve_reply(struct mpv_handle *ctx) +{ + int res = MPV_ERROR_EVENT_QUEUE_FULL; + pthread_mutex_lock(&ctx->lock); + if (ctx->reserved_events < ctx->max_events) { + ctx->reserved_events++; + res = 0; + } + pthread_mutex_unlock(&ctx->lock); + return res; +} + +static int send_event(struct mpv_handle *ctx, struct mpv_event *event) +{ + pthread_mutex_lock(&ctx->lock); + if (!(ctx->event_mask & (1ULL << event->event_id))) { + pthread_mutex_unlock(&ctx->lock); + return 0; + } + int num_events = mp_ring_available(ctx->events) / sizeof(*event); + int r = 0; + if (num_events > ctx->reserved_events) { + r = mp_ring_write(ctx->events, (unsigned char *)event, sizeof(*event)); + if (r != sizeof(*event)) + abort(); + wakeup_client(ctx); + } + if (!r && !ctx->choke_warning) { + mp_err(ctx->log, "Too many events queued.\n"); + ctx->choke_warning = true; + } + pthread_mutex_unlock(&ctx->lock); + return r ? 0 : -1; +} + +// Send a reply; the reply must have been previously reserved with +// reserve_reply (otherwise, use send_event()). +static void send_reply(struct mpv_handle *ctx, uint64_t userdata, + struct mpv_event *event) +{ + event->reply_userdata = userdata; + pthread_mutex_lock(&ctx->lock); + assert(ctx->reserved_events > 0); + ctx->reserved_events--; + int r = mp_ring_write(ctx->events, (unsigned char *)event, sizeof(*event)); + if (r != sizeof(*event)) + abort(); + wakeup_client(ctx); + pthread_mutex_unlock(&ctx->lock); +} + +static void status_reply(struct mpv_handle *ctx, int event, + uint64_t userdata, int status) +{ + struct mpv_event reply = { + .event_id = event, + .error = status, + }; + send_reply(ctx, userdata, &reply); +} + +void mp_client_broadcast_event(struct MPContext *mpctx, int event, void *data) +{ + struct mp_client_api *clients = mpctx->clients; + + struct mpv_event event_data = { + .event_id = event, + .data = data, + }; + + pthread_mutex_lock(&clients->lock); + + for (int n = 0; n < clients->num_clients; n++) + send_event(clients->clients[n], &event_data); + + pthread_mutex_unlock(&clients->lock); + + talloc_free(data); +} + +int mp_client_send_event(struct MPContext *mpctx, const char *client_name, + int event, void *data) +{ + struct mp_client_api *clients = mpctx->clients; + int r = 0; + + struct mpv_event event_data = { + .event_id = event, + .data = data, + }; + + pthread_mutex_lock(&clients->lock); + + struct mpv_handle *ctx = find_client(clients, client_name); + if (ctx) { + r = send_event(ctx, &event_data); + } else { + r = -1; + talloc_free(data); + } + + pthread_mutex_unlock(&clients->lock); + + return r; +} + +int mpv_request_event(mpv_handle *ctx, mpv_event_id event, int enable) +{ + if (!mpv_event_name(event) || enable < 0 || enable > 1) + return MPV_ERROR_INVALID_PARAMETER; + pthread_mutex_lock(&ctx->lock); + uint64_t bit = 1LLU << event; + ctx->event_mask = enable ? ctx->event_mask | bit : ctx->event_mask & ~bit; + pthread_mutex_unlock(&ctx->lock); + return 0; +} + +mpv_event *mpv_wait_event(mpv_handle *ctx, double timeout) +{ + mpv_event *event = ctx->cur_event; + + struct timespec deadline = mpthread_get_deadline(timeout); + + pthread_mutex_lock(&ctx->lock); + + *event = (mpv_event){0}; + talloc_free_children(event); + + while (1) { + if (mp_ring_buffered(ctx->events)) { + int r = + mp_ring_read(ctx->events, (unsigned char*)event, sizeof(*event)); + if (r != sizeof(*event)) + abort(); + talloc_steal(event, event->data); + break; + } + if (ctx->shutdown) { + event->event_id = MPV_EVENT_SHUTDOWN; + break; + } + if (ctx->messages) { + // Poll the log message queue. Currently we can't/don't do better. + struct mp_log_buffer_entry *msg = + mp_msg_log_buffer_read(ctx->messages); + if (msg) { + event->event_id = MPV_EVENT_LOG_MESSAGE; + struct mpv_event_log_message *cmsg = talloc_ptrtype(event, cmsg); + *cmsg = (struct mpv_event_log_message){ + .prefix = talloc_steal(event, msg->prefix), + .level = mp_log_levels[msg->level], + .text = talloc_steal(event, msg->text), + }; + event->data = cmsg; + talloc_free(msg); + break; + } + } + if (ctx->queued_wakeup) + break; + if (timeout <= 0) + break; + pthread_cond_timedwait(&ctx->wakeup, &ctx->lock, &deadline); + } + ctx->queued_wakeup = false; + + pthread_mutex_unlock(&ctx->lock); + + return event; +} + +void mpv_wakeup(mpv_handle *ctx) +{ + pthread_mutex_lock(&ctx->lock); + ctx->queued_wakeup = true; + wakeup_client(ctx); + pthread_mutex_unlock(&ctx->lock); +} + +int mpv_set_option(mpv_handle *ctx, const char *name, mpv_format format, + void *data) +{ + if (ctx->mpctx->initialized) { + char prop[100]; + snprintf(prop, sizeof(prop), "options/%s", name); + int err = mpv_set_property(ctx, name, format, data); + switch (err) { + case MPV_ERROR_PROPERTY_UNAVAILABLE: + case MPV_ERROR_PROPERTY_ERROR: + return MPV_ERROR_OPTION_ERROR; + case MPV_ERROR_PROPERTY_FORMAT: + return MPV_ERROR_OPTION_FORMAT; + case MPV_ERROR_PROPERTY_NOT_FOUND: + return MPV_ERROR_OPTION_NOT_FOUND; + default: + return err; + } + } else { + if (format != MPV_FORMAT_STRING) + return MPV_ERROR_OPTION_FORMAT; + const char *value = data; + int err = m_config_set_option0(ctx->mpctx->mconfig, name, value); + switch (err) { + case M_OPT_MISSING_PARAM: + case M_OPT_INVALID: + case M_OPT_OUT_OF_RANGE: + return MPV_ERROR_OPTION_ERROR; + case M_OPT_UNKNOWN: + return MPV_ERROR_OPTION_NOT_FOUND; + default: + if (err >= 0) + return 0; + return MPV_ERROR_OPTION_ERROR; + } + } +} + +int mpv_set_option_string(mpv_handle *ctx, const char *name, const char *data) +{ + return mpv_set_option(ctx, name, MPV_FORMAT_STRING, (void *)data); +} + +// Run a command in the playback thread. +// Note: once some things are fixed (like vo_opengl not being safe to be +// called from any thread other than the playback thread), this can +// be replaced by a simpler method. +static void run_locked(mpv_handle *ctx, void (*fn)(void *fn_data), void *fn_data) +{ + mp_dispatch_run(ctx->mpctx->dispatch, fn, fn_data); +} + +// Run a command asynchronously. It's the responsibility of the caller to +// actually send the reply. This helper merely saves a small part of the +// required boilerplate to do so. +// fn: callback to execute the request +// fn_data: opaque caller-defined argument for fn. This will be automatically +// freed with talloc_free(fn_data). +static int run_async(mpv_handle *ctx, void (*fn)(void *fn_data), void *fn_data) +{ + int err = reserve_reply(ctx); + if (err < 0) { + talloc_free(fn_data); + return err; + } + mp_dispatch_enqueue_autofree(ctx->mpctx->dispatch, fn, fn_data); + return 0; +} + +struct cmd_request { + struct MPContext *mpctx; + struct mp_cmd *cmd; + int status; + struct mpv_handle *reply_ctx; + uint64_t userdata; +}; + +static void cmd_fn(void *data) +{ + struct cmd_request *req = data; + run_command(req->mpctx, req->cmd); + req->status = 0; + talloc_free(req->cmd); + if (req->reply_ctx) { + status_reply(req->reply_ctx, MPV_EVENT_COMMAND_REPLY, + req->userdata, req->status); + } +} + +static int run_client_command(mpv_handle *ctx, struct mp_cmd *cmd) +{ + if (!ctx->mpctx->initialized) + return MPV_ERROR_UNINITIALIZED; + if (!cmd) + return MPV_ERROR_INVALID_PARAMETER; + + struct cmd_request req = { + .mpctx = ctx->mpctx, + .cmd = cmd, + }; + run_locked(ctx, cmd_fn, &req); + return req.status; +} + +int mpv_command(mpv_handle *ctx, const char **args) +{ + return run_client_command(ctx, mp_input_parse_cmd_strv(ctx->log, 0, args, + ctx->name)); +} + +int mpv_command_string(mpv_handle *ctx, const char *args) +{ + return run_client_command(ctx, + mp_input_parse_cmd(ctx->mpctx->input, bstr0((char*)args), ctx->name)); +} + +int mpv_command_async(mpv_handle *ctx, uint64_t ud, const char **args) +{ + if (!ctx->mpctx->initialized) + return MPV_ERROR_UNINITIALIZED; + + struct mp_cmd *cmd = mp_input_parse_cmd_strv(ctx->log, 0, args, ""); + if (!cmd) + return MPV_ERROR_INVALID_PARAMETER; + + struct cmd_request *req = talloc_ptrtype(NULL, req); + *req = (struct cmd_request){ + .mpctx = ctx->mpctx, + .cmd = cmd, + .reply_ctx = ctx, + .userdata = ud, + }; + return run_async(ctx, cmd_fn, req); +} + +static int translate_property_error(int errc) +{ + switch (errc) { + case M_PROPERTY_OK: return 0; + case M_PROPERTY_ERROR: return MPV_ERROR_PROPERTY_ERROR; + case M_PROPERTY_UNAVAILABLE: return MPV_ERROR_PROPERTY_UNAVAILABLE; + case M_PROPERTY_NOT_IMPLEMENTED: return MPV_ERROR_PROPERTY_ERROR; + case M_PROPERTY_UNKNOWN: return MPV_ERROR_PROPERTY_NOT_FOUND; + // shouldn't happen + default: return MPV_ERROR_PROPERTY_ERROR; + } +} + +struct setproperty_request { + struct MPContext *mpctx; + const char *name; + int format; + void *data; + int status; + struct mpv_handle *reply_ctx; + uint64_t userdata; +}; + +static int property_format_to_set_cmd(int format) +{ + switch (format) { + case MPV_FORMAT_STRING: return M_PROPERTY_SET_STRING; + default: return MPV_ERROR_PROPERTY_FORMAT; + } +} + +static void setproperty_fn(void *arg) +{ + struct setproperty_request *req = arg; + + int cmd = property_format_to_set_cmd(req->format); + if (cmd < 0) { + req->status = cmd; + } else { + int err = mp_property_do(req->name, cmd, req->data, req->mpctx); + req->status = translate_property_error(err); + } + + if (req->reply_ctx) { + status_reply(req->reply_ctx, MPV_EVENT_SET_PROPERTY_REPLY, + req->userdata, req->status); + } +} + +int mpv_set_property(mpv_handle *ctx, const char *name, mpv_format format, + void *data) +{ + if (!ctx->mpctx->initialized) + return MPV_ERROR_UNINITIALIZED; + + struct setproperty_request req = { + .mpctx = ctx->mpctx, + .name = name, + .format = format, + .data = data, + }; + run_locked(ctx, setproperty_fn, &req); + return req.status; +} + +int mpv_set_property_string(mpv_handle *ctx, const char *name, const char *data) +{ + return mpv_set_property(ctx, name, MPV_FORMAT_STRING, (void *)data); +} + +int mpv_set_property_async(mpv_handle *ctx, uint64_t ud, const char *name, + mpv_format format, void *data) +{ + if (!ctx->mpctx->initialized) + return MPV_ERROR_UNINITIALIZED; + + struct setproperty_request *req = talloc_ptrtype(NULL, req); + *req = (struct setproperty_request){ + .mpctx = ctx->mpctx, + .name = talloc_strdup(req, name), + .format = MPV_FORMAT_STRING, + .data = talloc_strdup(req, data), // for now always a string + .reply_ctx = ctx, + .userdata = ud, + }; + return run_async(ctx, setproperty_fn, req); +} + +static int property_format_to_get_cmd(int format) +{ + switch (format) { + case MPV_FORMAT_STRING: return M_PROPERTY_GET_STRING; + case MPV_FORMAT_OSD_STRING: return M_PROPERTY_PRINT; + default: return MPV_ERROR_PROPERTY_FORMAT; + } +} + +struct getproperty_request { + struct MPContext *mpctx; + const char *name; + mpv_format format; + void *data; + int status; + struct mpv_handle *reply_ctx; + uint64_t userdata; +}; + +static void getproperty_fn(void *arg) +{ + struct getproperty_request *req = arg; + + char *xdata = NULL; // currently, we support strings only + void *data = req->data ? req->data : &xdata; + + int cmd = property_format_to_get_cmd(req->format); + if (cmd < 0) { + req->status = cmd; + } else { + int err = mp_property_do(req->name, cmd, data, req->mpctx); + req->status = translate_property_error(err); + } + + if (req->reply_ctx) { + struct mpv_event_property *prop = talloc_ptrtype(NULL, prop); + *prop = (struct mpv_event_property){ + .name = talloc_steal(prop, (char *)req->name), + .format = req->format, + .data = talloc_steal(prop, xdata), + }; + struct mpv_event reply = { + .event_id = MPV_EVENT_GET_PROPERTY_REPLY, + .data = prop, + .error = req->status, + .reply_userdata = req->userdata, + }; + send_reply(req->reply_ctx, req->userdata, &reply); + } +} + +int mpv_get_property(mpv_handle *ctx, const char *name, mpv_format format, + void *data) +{ + if (!ctx->mpctx->initialized) + return MPV_ERROR_UNINITIALIZED; + + struct getproperty_request req = { + .mpctx = ctx->mpctx, + .name = name, + .format = format, + .data = data, + }; + run_locked(ctx, getproperty_fn, &req); + return req.status; +} + +char *mpv_get_property_string(mpv_handle *ctx, const char *name) +{ + char *str = NULL; + mpv_get_property(ctx, name, MPV_FORMAT_STRING, &str); + return str; +} + +char *mpv_get_property_osd_string(mpv_handle *ctx, const char *name) +{ + char *str = NULL; + mpv_get_property(ctx, name, MPV_FORMAT_OSD_STRING, &str); + return str; +} + +int mpv_get_property_async(mpv_handle *ctx, uint64_t ud, const char *name, + mpv_format format) +{ + if (!ctx->mpctx->initialized) + return MPV_ERROR_UNINITIALIZED; + + struct getproperty_request *req = talloc_ptrtype(NULL, req); + *req = (struct getproperty_request){ + .mpctx = ctx->mpctx, + .name = talloc_strdup(req, name), + .format = format, + .reply_ctx = ctx, + .userdata = ud, + }; + return run_async(ctx, getproperty_fn, req); +} + +int mpv_request_log_messages(mpv_handle *ctx, const char *min_level) +{ + int level = -1; + for (int n = 0; n < MSGL_MAX + 1; n++) { + if (mp_log_levels[n] && strcmp(min_level, mp_log_levels[n]) == 0) { + level = n; + break; + } + } + if (level < 0 && strcmp(min_level, "no") != 0) + return MPV_ERROR_INVALID_PARAMETER; + + pthread_mutex_lock(&ctx->lock); + + if (!ctx->messages) + ctx->messages_level = -1; + + if (ctx->messages_level != level) { + mp_msg_log_buffer_destroy(ctx->messages); + ctx->messages = NULL; + if (level >= 0) { + ctx->messages = + mp_msg_log_buffer_new(ctx->mpctx->global, 1000, level); + } + ctx->messages_level = level; + } + + pthread_mutex_unlock(&ctx->lock); + return 0; +} + +unsigned long mpv_client_api_version(void) +{ + return MPV_CLIENT_API_VERSION; +} + +static const char *err_table[] = { + [-MPV_ERROR_SUCCESS] = "success", + [-MPV_ERROR_EVENT_QUEUE_FULL] = "event queue full", + [-MPV_ERROR_NOMEM] = "memory allocation failed", + [-MPV_ERROR_UNINITIALIZED] = "core not uninitialized", + [-MPV_ERROR_INVALID_PARAMETER] = "invalid parameter", + [-MPV_ERROR_OPTION_NOT_FOUND] = "option not found", + [-MPV_ERROR_OPTION_FORMAT] = "unsupported format for setting option", + [-MPV_ERROR_OPTION_ERROR] = "error setting option", + [-MPV_ERROR_PROPERTY_NOT_FOUND] = "property not found", + [-MPV_ERROR_PROPERTY_FORMAT] = "unsupported format for setting property", + [-MPV_ERROR_PROPERTY_UNAVAILABLE] = "property unavailable", + [-MPV_ERROR_PROPERTY_ERROR] = "error accessing property", +}; + +const char *mpv_error_string(int error) +{ + error = -error; + if (error < 0) + error = 0; + const char *name = NULL; + if (error < MP_ARRAY_SIZE(err_table)) + name = err_table[error]; + return name ? name : "unknown error"; +} + +static const char *event_table[] = { + [MPV_EVENT_NONE] = "none", + [MPV_EVENT_SHUTDOWN] = "shutdown", + [MPV_EVENT_LOG_MESSAGE] = "log-message", + [MPV_EVENT_GET_PROPERTY_REPLY] = "get-property-reply", + [MPV_EVENT_SET_PROPERTY_REPLY] = "set-property-reply", + [MPV_EVENT_COMMAND_REPLY] = "command-reply", + [MPV_EVENT_START_FILE] = "start-file", + [MPV_EVENT_END_FILE] = "end-file", + [MPV_EVENT_PLAYBACK_START] = "playback-start", + [MPV_EVENT_TRACKS_CHANGED] = "tracks-changed", + [MPV_EVENT_TRACK_SWITCHED] = "track-switched", + [MPV_EVENT_IDLE] = "idle", + [MPV_EVENT_PAUSE] = "pause", + [MPV_EVENT_UNPAUSE] = "unpause", + [MPV_EVENT_TICK] = "tick", + [MPV_EVENT_SCRIPT_INPUT_DISPATCH] = "script-input-dispatch", +}; + +const char *mpv_event_name(mpv_event_id event) +{ + if (event < 0 || event >= MP_ARRAY_SIZE(event_table)) + return NULL; + return event_table[event]; +} + +void mpv_free(void *data) +{ + talloc_free(data); +} diff --git a/player/client.h b/player/client.h new file mode 100644 index 0000000000..4c0c23c614 --- /dev/null +++ b/player/client.h @@ -0,0 +1,24 @@ +#ifndef MP_CLIENT_H_ +#define MP_CLIENT_H_ + +#include + +#include "libmpv/client.h" + +struct MPContext; +struct mpv_handle; +struct mp_client_api; +struct mp_log; + +void mp_clients_init(struct MPContext *mpctx); +void mp_clients_destroy(struct MPContext *mpctx); +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); + +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); + +#endif diff --git a/player/command.c b/player/command.c index 1b2c6abab3..2d0f7c0d9a 100644 --- a/player/command.c +++ b/player/command.c @@ -30,6 +30,7 @@ #include "config.h" #include "talloc.h" +#include "client.h" #include "common/msg.h" #include "common/msg_control.h" #include "command.h" @@ -3178,16 +3179,14 @@ void command_init(struct MPContext *mpctx) }; } -// Notify that a property might have changed. -void mp_notify_property(struct MPContext *mpctx, const char *property) -{ - mp_notify(mpctx, MP_EVENT_PROPERTY, (void *)property); -} - -void mp_notify(struct MPContext *mpctx, enum mp_event event, void *arg) +void mp_notify(struct MPContext *mpctx, int event, void *arg) { struct command_ctx *ctx = mpctx->command_ctx; ctx->events |= 1u << event; + if (event == MPV_EVENT_START_FILE) + ctx->last_seek_pts = MP_NOPTS_VALUE; + + mp_client_broadcast_event(mpctx, event, arg); } static void handle_script_event(struct MPContext *mpctx, const char *name, @@ -3202,10 +3201,10 @@ void mp_flush_events(struct MPContext *mpctx) { struct command_ctx *ctx = mpctx->command_ctx; - ctx->events |= (1u << MP_EVENT_TICK); + ctx->events |= (1u << MPV_EVENT_TICK); for (int n = 0; n < 16; n++) { - enum mp_event event = n; + int event = n; unsigned mask = 1 << event; if (ctx->events & mask) { // The event handler could set event flags again; in this case let @@ -3213,17 +3212,15 @@ void mp_flush_events(struct MPContext *mpctx) ctx->events &= ~mask; const char *name = NULL; switch (event) { - case MP_EVENT_TICK: name = "tick"; break; - case MP_EVENT_TRACKS_CHANGED: name = "track-layout"; break; - case MP_EVENT_PLAYBACK_START: name = "playback-start"; break; - case MP_EVENT_START_FILE: name = "start"; break; - case MP_EVENT_END_FILE: name = "end"; break; + case MPV_EVENT_TICK: name = "tick"; break; + case MPV_EVENT_TRACKS_CHANGED: name = "track-layout"; break; + case MPV_EVENT_PLAYBACK_START: name = "playback-start"; break; + case MPV_EVENT_START_FILE: name = "start"; break; + case MPV_EVENT_END_FILE: name = "end"; break; default: ; } if (name) handle_script_event(mpctx, name, ""); - if (event == MP_EVENT_START_FILE) - ctx->last_seek_pts = MP_NOPTS_VALUE; } } } diff --git a/player/command.h b/player/command.h index a676709851..8047fd747a 100644 --- a/player/command.h +++ b/player/command.h @@ -34,19 +34,7 @@ int mp_property_do(const char* name, int action, void* val, const struct m_option *mp_get_property_list(void); -enum mp_event { - MP_EVENT_NONE, - MP_EVENT_TICK, - MP_EVENT_PROPERTY, // char*, property that is changed - MP_EVENT_TRACKS_CHANGED, - MP_EVENT_START_FILE, - MP_EVENT_PLAYBACK_START, - MP_EVENT_END_FILE, -}; - -void mp_notify(struct MPContext *mpctx, enum mp_event event, void *arg); -void mp_notify_property(struct MPContext *mpctx, const char *property); - +void mp_notify(struct MPContext *mpctx, int event, void *arg); void mp_flush_events(struct MPContext *mpctx); #endif /* MPLAYER_COMMAND_H */ diff --git a/player/core.h b/player/core.h index 556903b50d..d39ac64dab 100644 --- a/player/core.h +++ b/player/core.h @@ -140,12 +140,16 @@ enum { #define NUM_PTRACKS 2 typedef struct MPContext { + bool initialized; struct mpv_global *global; struct MPOpts *opts; struct mp_log *log; - struct mp_log *statusline; struct m_config *mconfig; struct input_ctx *input; + struct mp_client_api *clients; + struct mp_dispatch_queue *dispatch; + + struct mp_log *statusline; struct osd_state *osd; struct mp_osd_msg *osd_msg_stack; char *term_osd_text; @@ -385,6 +389,9 @@ void mp_set_playlist_entry(struct MPContext *mpctx, struct playlist_entry *e); void mp_play_files(struct MPContext *mpctx); // main.c +int mp_initialize(struct MPContext *mpctx); +struct MPContext *mp_create(void); +void mp_destroy(struct MPContext *mpctx); void mp_print_version(struct mp_log *log, int always); // misc.c diff --git a/player/loadfile.c b/player/loadfile.c index 662a714ec0..b289ef4922 100644 --- a/player/loadfile.c +++ b/player/loadfile.c @@ -57,6 +57,7 @@ #include "core.h" #include "command.h" +#include "libmpv/client.h" #if HAVE_DVBIN #include "stream/dvbin.h" @@ -470,7 +471,7 @@ static struct track *add_stream_track(struct MPContext *mpctx, demuxer_select_track(track->demuxer, stream, false); - mp_notify(mpctx, MP_EVENT_TRACKS_CHANGED, NULL); + mp_notify(mpctx, MPV_EVENT_TRACKS_CHANGED, NULL); return track; } @@ -503,7 +504,7 @@ static void add_dvd_tracks(struct MPContext *mpctx) stream_control(stream, STREAM_CTRL_GET_LANG, &req); track->lang = talloc_strdup(track, req.name); - mp_notify(mpctx, MP_EVENT_TRACKS_CHANGED, NULL); + mp_notify(mpctx, MPV_EVENT_TRACKS_CHANGED, NULL); } } demuxer_enable_autoselect(demuxer); @@ -666,15 +667,12 @@ void mp_switch_track_n(struct MPContext *mpctx, int order, enum stream_type type if (type == STREAM_VIDEO) { mpctx->opts->video_id = user_tid; reinit_video_chain(mpctx); - mp_notify_property(mpctx, "vid"); } else if (type == STREAM_AUDIO) { mpctx->opts->audio_id = user_tid; reinit_audio_chain(mpctx); - mp_notify_property(mpctx, "aid"); } else if (type == STREAM_SUB) { mpctx->opts->sub_id = user_tid; reinit_subs(mpctx, 0); - mp_notify_property(mpctx, "sid"); } } else if (order == 1) { if (type == STREAM_SUB) { @@ -683,6 +681,7 @@ void mp_switch_track_n(struct MPContext *mpctx, int order, enum stream_type type } } + mp_notify(mpctx, MPV_EVENT_TRACK_SWITCHED, NULL); osd_changed_all(mpctx->osd); talloc_free(mpctx->track_layout_hash); @@ -738,7 +737,7 @@ bool mp_remove_track(struct MPContext *mpctx, struct track *track) mpctx->num_tracks--; talloc_free(track); - mp_notify(mpctx, MP_EVENT_TRACKS_CHANGED, NULL); + mp_notify(mpctx, MPV_EVENT_TRACKS_CHANGED, NULL); return true; } @@ -1052,7 +1051,7 @@ static void play_current_file(struct MPContext *mpctx) mpctx->initialized_flags |= INITIALIZED_PLAYBACK; - mp_notify(mpctx, MP_EVENT_START_FILE, NULL); + mp_notify(mpctx, MPV_EVENT_START_FILE, NULL); mp_flush_events(mpctx); mpctx->stop_play = 0; @@ -1345,7 +1344,7 @@ goto_reopen_demuxer: ; if (mpctx->opts->pause) pause_player(mpctx); - mp_notify(mpctx, MP_EVENT_PLAYBACK_START, NULL); + mp_notify(mpctx, MPV_EVENT_PLAYBACK_START, NULL); playback_start = mp_time_sec(); mpctx->error_playing = false; @@ -1410,8 +1409,8 @@ terminate_playback: // don't jump here after ao/vo/getch initialization! mpctx->playlist->current->init_failed = init_failed; } - mp_notify(mpctx, MP_EVENT_TRACKS_CHANGED, NULL); - mp_notify(mpctx, MP_EVENT_END_FILE, NULL); + mp_notify(mpctx, MPV_EVENT_TRACKS_CHANGED, NULL); + mp_notify(mpctx, MPV_EVENT_END_FILE, NULL); mp_flush_events(mpctx); } diff --git a/player/main.c b/player/main.c index e0371cc5ca..c455a0641c 100644 --- a/player/main.c +++ b/player/main.c @@ -31,6 +31,7 @@ #include "osdep/priority.h" #include "osdep/terminal.h" #include "osdep/timer.h" +#include "osdep/threads.h" #include "common/av_log.h" #include "common/codecs.h" @@ -61,6 +62,7 @@ #include "video/out/vo.h" #include "core.h" +#include "client.h" #include "lua.h" #include "command.h" #include "screenshot.h" @@ -89,6 +91,8 @@ #endif #endif +static bool terminal_initialized; + const char mp_help_text[] = "Usage: mpv [options] [url|path/]filename\n" "\n" @@ -113,11 +117,20 @@ void mp_print_version(struct mp_log *log, int always) mp_msg(log, v, "\n"); } -static MP_NORETURN void exit_player(struct MPContext *mpctx, - enum exit_reason how) +static void shutdown_clients(struct MPContext *mpctx) { - int rc; - uninit_player(mpctx, INITIALIZED_ALL); + while (mpctx->clients && mp_clients_num(mpctx)) { + mp_client_broadcast_event(mpctx, MPV_EVENT_SHUTDOWN, NULL); + mp_dispatch_queue_process(mpctx->dispatch, 0); + mp_input_get_cmd(mpctx->input, 100, 1); + } + mp_clients_destroy(mpctx); +} + +void mp_destroy(struct MPContext *mpctx) +{ + if (mpctx->initialized) + uninit_player(mpctx, INITIALIZED_ALL); #if HAVE_ENCODING encode_lavc_finish(mpctx->encode_lavc_ctx); @@ -129,10 +142,7 @@ static MP_NORETURN void exit_player(struct MPContext *mpctx, #if HAVE_LUA mp_lua_uninit(mpctx); #endif - -#if HAVE_COCOA - cocoa_set_input_context(NULL); -#endif + shutdown_clients(mpctx); command_uninit(mpctx); @@ -141,14 +151,27 @@ static MP_NORETURN void exit_player(struct MPContext *mpctx, osd_free(mpctx->osd); #if HAVE_LIBASS - ass_library_done(mpctx->ass_library); - mpctx->ass_library = NULL; + if (mpctx->ass_library) + ass_library_done(mpctx->ass_library); #endif if (mpctx->opts->use_terminal) getch2_disable(); uninit_libav(mpctx->global); + mp_msg_uninit(mpctx->global); + talloc_free(mpctx); +} + +static MP_NORETURN void exit_player(struct MPContext *mpctx, + enum exit_reason how) +{ + int rc; + +#if HAVE_COCOA + cocoa_set_input_context(NULL); +#endif + if (how != EXIT_NONE) { const char *reason; switch (how) { @@ -183,11 +206,7 @@ static MP_NORETURN void exit_player(struct MPContext *mpctx, } } - // must be last since e.g. mp_msg uses option values - // that will be freed by this. - - mp_msg_uninit(mpctx->global); - talloc_free(mpctx); + mp_destroy(mpctx); #if HAVE_COCOA terminate_cocoa_application(); @@ -284,8 +303,6 @@ static void osdep_preinit(int *p_argc, char ***p_argv) if (pSetSearchPathMode) pSetSearchPathMode(BASE_SEARCH_PATH_ENABLE_SAFE_SEARCHMODE); #endif - - mp_time_init(); } static int cfg_include(void *ctx, char *filename, int flags) @@ -294,14 +311,10 @@ static int cfg_include(void *ctx, char *filename, int flags) return m_config_parse_config_file(mpctx->mconfig, filename, flags); } -static int mpv_main(int argc, char *argv[]) +struct MPContext *mp_create(void) { - osdep_preinit(&argc, &argv); - - if (argc >= 1) { - argc--; - argv++; - } + mp_time_init(); + GetCpuCaps(&gCpuCaps); struct MPContext *mpctx = talloc(NULL, MPContext); *mpctx = (struct MPContext){ @@ -309,6 +322,7 @@ static int mpv_main(int argc, char *argv[]) .term_osd_contents = talloc_strdup(mpctx, ""), .osd_progbar = { .type = -1 }, .playlist = talloc_struct(mpctx, struct playlist, {0}), + .dispatch = mp_dispatch_create(mpctx), }; mpctx->global = talloc_zero(mpctx, struct mpv_global); @@ -331,68 +345,35 @@ static int mpv_main(int argc, char *argv[]) mpctx->mconfig->use_profiles = true; mpctx->mconfig->is_toplevel = true; - struct MPOpts *opts = mpctx->opts; - mpctx->global->opts = opts; + mpctx->global->opts = mpctx->opts; - char *verbose_env = getenv("MPV_VERBOSE"); - if (verbose_env) - opts->verbose = atoi(verbose_env); - - // Preparse the command line - m_config_preparse_command_line(mpctx->mconfig, mpctx->global, argc, argv); - - mp_msg_update_msglevels(mpctx->global); - - if (opts->use_terminal) - terminal_init(); - - init_libav(mpctx->global); - GetCpuCaps(&gCpuCaps); screenshot_init(mpctx); mpctx->mixer = mixer_init(mpctx, mpctx->global); command_init(mpctx); + init_libav(mpctx->global); + mp_clients_init(mpctx); - mp_print_version(mpctx->log, false); - - if (!mp_parse_cfgfiles(mpctx)) - exit_player(mpctx, EXIT_ERROR); - - int r = m_config_parse_mp_command_line(mpctx->mconfig, mpctx->playlist, - mpctx->global, argc, argv); - if (r < 0) { - if (r <= M_OPT_EXIT) { - exit_player(mpctx, EXIT_NONE); - } else { - exit_player(mpctx, EXIT_ERROR); - } - } - - mp_msg_update_msglevels(mpctx->global); + return mpctx; +} - if (handle_help_options(mpctx)) - exit_player(mpctx, EXIT_NONE); +// Finish mpctx initialization. This must be done after setting up all options. +// Some of the initializations depend on the options, and can't be changed or +// undone later. +// cplayer: true if called by the command line player, false for client API +// Returns: <0 on error, 0 on success. +int mp_initialize(struct MPContext *mpctx) +{ + struct MPOpts *opts = mpctx->opts; - MP_VERBOSE(mpctx, "Configuration: " CONFIGURATION "\n"); - MP_VERBOSE(mpctx, "Command line:"); - for (int i = 0; i < argc; i++) - MP_VERBOSE(mpctx, " '%s'", argv[i]); - MP_VERBOSE(mpctx, "\n"); + assert(!mpctx->initialized); - if (!mpctx->playlist->first && !opts->player_idle_mode) { - mp_print_version(mpctx->log, true); - MP_INFO(mpctx, "%s", mp_help_text); - exit_player(mpctx, EXIT_NONE); + if (mpctx->opts->use_terminal && !terminal_initialized) { + terminal_initialized = true; + terminal_init(); } -#if HAVE_PRIORITY - set_priority(); -#endif - mpctx->input = mp_input_init(mpctx->global); stream_set_interrupt_callback(mp_input_check_interrupt, mpctx->input); -#if HAVE_COCOA - cocoa_set_input_context(mpctx->input); -#endif #if HAVE_ENCODING if (opts->encode_output.file && *opts->encode_output.file) { @@ -400,7 +381,7 @@ static int mpv_main(int argc, char *argv[]) mpctx->global); if(!mpctx->encode_lavc_ctx) { MP_INFO(mpctx, "Encoding initialization failed."); - exit_player(mpctx, EXIT_ERROR); + return -1; } m_config_set_option0(mpctx->mconfig, "vo", "lavc"); m_config_set_option0(mpctx->mconfig, "ao", "lavc"); @@ -431,6 +412,9 @@ static int mpv_main(int argc, char *argv[]) mpctx->osd = osd_create(mpctx->global); + // From this point on, all mpctx members are initialized. + mpctx->initialized = true; + if (opts->force_vo) { opts->fixed_vo = 1; mpctx->video_out = init_best_video_out(mpctx->global, mpctx->input, @@ -438,7 +422,7 @@ static int mpv_main(int argc, char *argv[]) if (!mpctx->video_out) { MP_FATAL(mpctx, "Error opening/initializing " "the selected video_out (-vo) device.\n"); - exit_player(mpctx, EXIT_ERROR); + return -1; } mpctx->mouse_cursor_visible = true; mpctx->initialized_flags |= INITIALIZED_VO; @@ -460,6 +444,78 @@ static int mpv_main(int argc, char *argv[]) if (!mpctx->playlist->current) mpctx->playlist->current = mpctx->playlist->first; + return 0; +} + +static int mpv_main(int argc, char *argv[]) +{ + osdep_preinit(&argc, &argv); + + if (argc >= 1) { + argc--; + argv++; + } + + struct MPContext *mpctx = mp_create(); + struct MPOpts *opts = mpctx->opts; + + char *verbose_env = getenv("MPV_VERBOSE"); + if (verbose_env) + opts->verbose = atoi(verbose_env); + + // Preparse the command line + m_config_preparse_command_line(mpctx->mconfig, mpctx->global, argc, argv); + + if (mpctx->opts->use_terminal && !terminal_initialized) { + terminal_initialized = true; + terminal_init(); + } + + mp_msg_update_msglevels(mpctx->global); + + mp_print_version(mpctx->log, false); + + if (!mp_parse_cfgfiles(mpctx)) + exit_player(mpctx, EXIT_ERROR); + + int r = m_config_parse_mp_command_line(mpctx->mconfig, mpctx->playlist, + mpctx->global, argc, argv); + if (r < 0) { + if (r <= M_OPT_EXIT) { + exit_player(mpctx, EXIT_NONE); + } else { + exit_player(mpctx, EXIT_ERROR); + } + } + + mp_msg_update_msglevels(mpctx->global); + + if (handle_help_options(mpctx)) + exit_player(mpctx, EXIT_NONE); + + MP_VERBOSE(mpctx, "Configuration: " CONFIGURATION "\n"); + MP_VERBOSE(mpctx, "Command line:"); + for (int i = 0; i < argc; i++) + MP_VERBOSE(mpctx, " '%s'", argv[i]); + MP_VERBOSE(mpctx, "\n"); + + if (!mpctx->playlist->first && !opts->player_idle_mode) { + mp_print_version(mpctx->log, true); + MP_INFO(mpctx, "%s", mp_help_text); + exit_player(mpctx, EXIT_NONE); + } + +#if HAVE_PRIORITY + set_priority(); +#endif + + if (mp_initialize(mpctx) < 0) + exit_player(mpctx, EXIT_ERROR); + +#if HAVE_COCOA + cocoa_set_input_context(mpctx->input); +#endif + mp_play_files(mpctx); exit_player(mpctx, mpctx->stop_play == PT_QUIT ? EXIT_QUIT : mpctx->quit_player_rc); diff --git a/player/playloop.c b/player/playloop.c index e28d387801..d97fedccbc 100644 --- a/player/playloop.c +++ b/player/playloop.c @@ -34,6 +34,7 @@ #include "input/input.h" #include "osdep/terminal.h" +#include "osdep/threads.h" #include "osdep/timer.h" #include "audio/mixer.h" @@ -50,6 +51,7 @@ #include "core.h" #include "screenshot.h" #include "command.h" +#include "libmpv/client.h" #define WAKEUP_PERIOD 0.5 @@ -79,7 +81,7 @@ static const char av_desync_help_text[] = void pause_player(struct MPContext *mpctx) { - mp_notify_property(mpctx, "pause"); + mp_notify(mpctx, MPV_EVENT_PAUSE, NULL); mpctx->opts->pause = 1; @@ -110,7 +112,7 @@ void pause_player(struct MPContext *mpctx) void unpause_player(struct MPContext *mpctx) { - mp_notify_property(mpctx, "pause"); + mp_notify(mpctx, MPV_EVENT_UNPAUSE, NULL); mpctx->opts->pause = 0; @@ -749,12 +751,14 @@ static void handle_input_and_seek_coalesce(struct MPContext *mpctx) mp_cmd_t *cmd; while ((cmd = mp_input_get_cmd(mpctx->input, 0, 1)) != NULL) { + mp_dispatch_queue_process(mpctx->dispatch, 0); cmd = mp_input_get_cmd(mpctx->input, 0, 0); run_command(mpctx, cmd); mp_cmd_free(cmd); if (mpctx->stop_play) break; } + mp_dispatch_queue_process(mpctx->dispatch, 0); } void add_frame_pts(struct MPContext *mpctx, double pts) @@ -1166,9 +1170,14 @@ void run_playloop(struct MPContext *mpctx) screenshot_flip(mpctx); new_frame_shown = true; + mp_notify(mpctx, MPV_EVENT_TICK, NULL); + break; } // video + if (!video_left || mpctx->paused) + mp_notify(mpctx, MPV_EVENT_TICK, NULL); + video_left &= mpctx->sync_audio_to_video; // force no-video semantics if (mpctx->d_audio && (mpctx->restart_playback ? !video_left : @@ -1308,6 +1317,7 @@ void run_playloop(struct MPContext *mpctx) void idle_loop(struct MPContext *mpctx) { // ================= idle loop (STOP state) ========================= + mp_notify(mpctx, MPV_EVENT_IDLE, NULL); bool need_reinit = true; while (mpctx->opts->player_idle_mode && !mpctx->playlist->current && mpctx->stop_play != PT_QUIT) @@ -1331,6 +1341,7 @@ void idle_loop(struct MPContext *mpctx) run_command(mpctx, cmd); mp_cmd_free(cmd); mp_flush_events(mpctx); + mp_dispatch_queue_process(mpctx->dispatch, 0); if (mpctx->opts->use_terminal) getch2_poll(); } -- cgit v1.2.3