diff --git a/DOCS/man/input.rst b/DOCS/man/input.rst index 954520147a..f2efd080b8 100644 --- a/DOCS/man/input.rst +++ b/DOCS/man/input.rst @@ -1640,6 +1640,13 @@ Property list human readable free form text. The description is an empty string if none was received. + The special entry with the name set to ``auto`` selects the default audio + output driver and the default device. + + The property can be watched with the property observation mechanism in + the client API and in Lua scripts. (Technically, change notification is + enabled the first time this property is read.) + ``audio-device`` (RW) Set the audio device. This directly reads/writes the ``--audio-device`` option, but on write accesses, the audio output will be scheduled for diff --git a/TOOLS/lua/README.md b/TOOLS/lua/README.md index f7cdf2a590..e9044823cd 100644 --- a/TOOLS/lua/README.md +++ b/TOOLS/lua/README.md @@ -10,3 +10,5 @@ to mpv's command line. Where appropriate, they may also be placed in ~/.config/mpv/scripts/ from where they will be automatically loaded when mpv starts. + +Some of these are just for testing mpv internals. diff --git a/TOOLS/lua/audio-hotplug-test.lua b/TOOLS/lua/audio-hotplug-test.lua new file mode 100644 index 0000000000..8dedc68cbe --- /dev/null +++ b/TOOLS/lua/audio-hotplug-test.lua @@ -0,0 +1,8 @@ +local utils = require("mp.utils") + +mp.observe_property("audio-device-list", "native", function(name, val) + print("Audio device list changed:") + for index, e in ipairs(val) do + print(" - '" .. e.name .. "' (" .. e.description .. ")") + end +end) diff --git a/audio/out/ao.c b/audio/out/ao.c index ccb39f7f9e..a2fa2fb104 100644 --- a/audio/out/ao.c +++ b/audio/out/ao.c @@ -386,14 +386,25 @@ int ao_query_and_reset_events(struct ao *ao, int events) int actual_events = 0; if (atomic_load(&ao->request_reload)) // don't need to reset it actual_events |= AO_EVENT_RELOAD; + if (atomic_load(&ao->request_hotplug)) + actual_events |= AO_EVENT_HOTPLUG; return actual_events & events; } -// Request that the player core destroys and recreates the AO. +// Request that the player core destroys and recreates the AO. Fully thread-safe. void ao_request_reload(struct ao *ao) { atomic_store(&ao->request_reload, true); - mp_input_wakeup(ao->input_ctx); + if (ao->input_ctx) + mp_input_wakeup(ao->input_ctx); +} + +// Notify the player that the device list changed. Fully thread-safe. +void ao_hotplug_event(struct ao *ao) +{ + atomic_store(&ao->request_hotplug, true); + if (ao->input_ctx) + mp_input_wakeup(ao->input_ctx); } bool ao_chmap_sel_adjust(struct ao *ao, const struct mp_chmap_sel *s, @@ -444,26 +455,88 @@ const char *ao_get_detected_device(struct ao *ao) return ao->detected_device; } -struct ao_device_list *ao_get_device_list(struct mpv_global *global) +// --- + +struct ao_hotplug { + struct mpv_global *global; + struct input_ctx *input_ctx; + // A single AO instance is used to listen to hotplug events. It wouldn't + // make much sense to allow multiple AO drivers; all sane platforms have + // a single such audio API. + // This is _not_ the same AO instance as used for playing audio. + struct ao *ao; + // cached + struct ao_device_list *list; + bool needs_update; +}; + +struct ao_hotplug *ao_hotplug_create(struct mpv_global *global, + struct input_ctx *input_ctx) { - struct ao_device_list *list = talloc_zero(NULL, struct ao_device_list); + struct ao_hotplug *hp = talloc_ptrtype(NULL, hp); + *hp = (struct ao_hotplug){ + .global = global, + .input_ctx = input_ctx, + .needs_update = true, + }; + return hp; +} + +static void get_devices(struct ao *ao, struct ao_device_list *list) +{ + int num = list->num_devices; + if (ao->driver->list_devs) + ao->driver->list_devs(ao, list); + // Add at least a default entry + if (list->num_devices == num) + ao_device_list_add(list, ao, &(struct ao_device_desc){"", "Default"}); +} + +bool ao_hotplug_check_update(struct ao_hotplug *hp) +{ + if (hp->ao && ao_query_and_reset_events(hp->ao, AO_EVENT_HOTPLUG)) { + hp->needs_update = true; + atomic_store(&hp->ao->request_hotplug, false); + return true; + } + return false; +} + +// The return value is valid until the next call to this API. +struct ao_device_list *ao_hotplug_get_device_list(struct ao_hotplug *hp) +{ + if (hp->list && !hp->needs_update) + return hp->list; + + talloc_free(hp->list); + struct ao_device_list *list = talloc_zero(hp, struct ao_device_list); + hp->list = list; + MP_TARRAY_APPEND(list, list->devices, list->num_devices, (struct ao_device_desc){"auto", "Autoselect device"}); + for (int n = 0; audio_out_drivers[n]; n++) { const struct ao_driver *d = audio_out_drivers[n]; if (d == &audio_out_null) break; // don't add unsafe/special entries - struct ao *ao = ao_alloc(true, global, NULL, (char *)d->name, NULL); + + struct ao *ao = ao_alloc(true, hp->global, hp->input_ctx, + (char *)d->name, NULL); if (!ao) continue; - int num = list->num_devices; - if (d->list_devs) - d->list_devs(ao, list); - // Add at least a default entry - if (list->num_devices == num) - ao_device_list_add(list, ao, &(struct ao_device_desc){"", "Default"}); - talloc_free(ao); + + if (ao->driver->hotplug_init) { + if (!hp->ao && ao->driver->hotplug_init(ao) >= 0) + hp->ao = ao; // keep this one + if (hp->ao && hp->ao->driver == d) + get_devices(hp->ao, list); + } else { + get_devices(ao, list); + } + if (ao != hp->ao) + talloc_free(ao); } + hp->needs_update = false; return list; } @@ -478,13 +551,24 @@ void ao_device_list_add(struct ao_device_list *list, struct ao *ao, MP_TARRAY_APPEND(list, list->devices, list->num_devices, c); } +void ao_hotplug_destroy(struct ao_hotplug *hp) +{ + if (!hp) + return; + if (hp->ao && hp->ao->driver->hotplug_uninit) + hp->ao->driver->hotplug_uninit(hp->ao); + talloc_free(hp->ao); + talloc_free(hp); +} + void ao_print_devices(struct mpv_global *global, struct mp_log *log) { - struct ao_device_list *list = ao_get_device_list(global); + struct ao_hotplug *hp = ao_hotplug_create(global, NULL); + struct ao_device_list *list = ao_hotplug_get_device_list(hp); mp_info(log, "List of detected audio devices:\n"); for (int n = 0; n < list->num_devices; n++) { struct ao_device_desc *desc = &list->devices[n]; mp_info(log, " '%s' (%s)\n", desc->name, desc->desc); } - talloc_free(list); + ao_hotplug_destroy(hp); } diff --git a/audio/out/ao.h b/audio/out/ao.h index dbbed24873..7b85ec80ba 100644 --- a/audio/out/ao.h +++ b/audio/out/ao.h @@ -48,6 +48,7 @@ enum aocontrol { enum { AO_EVENT_RELOAD = 1, + AO_EVENT_HOTPLUG = 2, }; typedef struct ao_control_vol { @@ -92,8 +93,15 @@ void ao_drain(struct ao *ao); bool ao_eof_reached(struct ao *ao); int ao_query_and_reset_events(struct ao *ao, int events); void ao_request_reload(struct ao *ao); +void ao_hotplug_event(struct ao *ao); + +struct ao_hotplug; +struct ao_hotplug *ao_hotplug_create(struct mpv_global *global, + struct input_ctx *input_ctx); +void ao_hotplug_destroy(struct ao_hotplug *hp); +bool ao_hotplug_check_update(struct ao_hotplug *hp); +struct ao_device_list *ao_hotplug_get_device_list(struct ao_hotplug *hp); -struct ao_device_list *ao_get_device_list(struct mpv_global *global); void ao_print_devices(struct mpv_global *global, struct mp_log *log); #endif /* MPLAYER_AUDIO_OUT_H */ diff --git a/audio/out/internal.h b/audio/out/internal.h index 9414208923..19402861a2 100644 --- a/audio/out/internal.h +++ b/audio/out/internal.h @@ -59,7 +59,8 @@ struct ao { // Used during init: if init fails, redirect to this ao char *redirect; - atomic_bool request_reload; + // Internal events (use ao_request_reload(), ao_hotplug_event()) + atomic_bool request_reload, request_hotplug; int buffer; double def_buffer; @@ -161,17 +162,19 @@ struct ao_driver { // Return the list of devices currently available in the system. Use // ao_device_list_add() to add entries. The selected device will be set as // ao->device (using ao_device_desc.name). - // Warning: the ao struct passed doesn't necessarily have ao_driver->init() - // called on it - in this case, ->uninit() won't be called either - // after this function. The idea is that list_devs can be called - // both when no audio or when audio is active. the latter can - // happen if the audio config change at runtime, and in this case - // we don't want to force a new connection to the audio server - // just to update the device list. For runtime updates, ->init() - // will have been called. In both cases, ao->priv is properly - // allocated. (Runtime updates are not used/supported yet.) + // Warning: the ao struct passed is not initialized with ao_driver->init(). + // Instead, hotplug_init/hotplug_uninit is called. If these + // callbacks are not set, no driver initialization call is done + // on the ao struct. void (*list_devs)(struct ao *ao, struct ao_device_list *list); + // If set, these are called before/after ao_driver->list_devs is called. + // It is also assumed that the driver can do hotplugging - which means + // it is expected to call ao_hotplug_event(ao) whenever the system's + // audio device list changes. The player will then call list_devs() again. + int (*hotplug_init)(struct ao *ao); + void (*hotplug_uninit)(struct ao *ao); + // For option parsing (see vo.h) int priv_size; const void *priv_defaults; diff --git a/player/command.c b/player/command.c index f20e6050b4..07a9206b53 100644 --- a/player/command.c +++ b/player/command.c @@ -90,7 +90,7 @@ struct command_ctx { int num_hooks; int64_t hook_seq; // for hook_handler.seq - struct ao_device_list *cached_ao_devices; + struct ao_hotplug *hotplug; }; struct overlay { @@ -1560,14 +1560,12 @@ static int mp_property_audio_devices(void *ctx, struct m_property *prop, { struct MPContext *mpctx = ctx; struct command_ctx *cmd = mpctx->command_ctx; - if (!cmd->cached_ao_devices) - cmd->cached_ao_devices = ao_get_device_list(mpctx->global); - if (!cmd->cached_ao_devices) - return M_PROPERTY_ERROR; - talloc_steal(cmd, cmd->cached_ao_devices); + if (!cmd->hotplug) + cmd->hotplug = ao_hotplug_create(mpctx->global, mpctx->input); - return m_property_read_list(action, arg, cmd->cached_ao_devices->num_devices, - get_device_entry, cmd->cached_ao_devices); + struct ao_device_list *list = ao_hotplug_get_device_list(cmd->hotplug); + return m_property_read_list(action, arg, list->num_devices, + get_device_entry, list); } static int mp_property_ao(void *ctx, struct m_property *p, int action, void *arg) @@ -3535,6 +3533,7 @@ static const char *const *const mp_event_property_change[] = { "demuxer-cache-duration", "demuxer-cache-idle", "paused-for-cache"), E(MP_EVENT_WIN_RESIZE, "window-scale"), E(MP_EVENT_WIN_STATE, "window-minimized", "display-names"), + E(MP_EVENT_AUDIO_DEVICES, "audio-device-list"), }; #undef E @@ -4819,6 +4818,7 @@ int run_command(MPContext *mpctx, mp_cmd_t *cmd) void command_uninit(struct MPContext *mpctx) { overlay_uninit(mpctx); + ao_hotplug_destroy(mpctx->command_ctx->hotplug); talloc_free(mpctx->command_ctx); mpctx->command_ctx = NULL; } @@ -4868,6 +4868,11 @@ static void command_event(struct MPContext *mpctx, int event, void *arg) // Update chapters - does nothing if something else is visible. set_osd_bar_chapters(mpctx, OSD_BAR_SEEK); } + + // This is a bit messy: ao_hotplug wakes up the player, and then we have + // to recheck the state. Then the client(s) will read the property. + if (ctx->hotplug && ao_hotplug_check_update(ctx->hotplug)) + mp_notify_property(mpctx, "audio-device-list"); } void mp_notify(struct MPContext *mpctx, int event, void *arg) diff --git a/player/command.h b/player/command.h index 30387946d3..96a58b1f80 100644 --- a/player/command.h +++ b/player/command.h @@ -47,6 +47,7 @@ enum { MP_EVENT_CACHE_UPDATE, MP_EVENT_WIN_RESIZE, MP_EVENT_WIN_STATE, + MP_EVENT_AUDIO_DEVICES, }; bool mp_hook_test_completion(struct MPContext *mpctx, char *type);