audio: add device change notification for hotplugging

Not very important for the command line player; but GUI applications
will want to know about this.

This only adds the internal API; support for specific audio outputs
comes later.

This reuses the ao struct as context for the hotplug event listener,
similar to how the "old" device listing API did. This is probably a bit
unclean and confusing. One argument got reusing it is that otherwise
rewriting parts of ao_pulse would be required (because the PulseAudio
API requires so damn much boilerplate). Another is that --ao-defaults is
applied to the hotplug dummy ao struct, which automatically applies such
defaults even to the hotplug context.

Notification works through the property observation mechanism in the
client API. The notification chain is a bit complicated: the AO notifies
the player, which in turn notifies the clients, which in turn will
actually retrieve the device list. (It still has the advantage that it's
slightly cleaner, since the AO stuff doesn't need to know about client
API issues.)

The weird handling of atomic flags in ao.c is because we still don't
require real atomics from the compiler. Otherwise we'd just use atomic
bitwise operations.
This commit is contained in:
wm4 2015-02-12 16:53:56 +01:00
parent c152c59084
commit f061befb33
8 changed files with 151 additions and 33 deletions

View File

@ -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

View File

@ -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.

View File

@ -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)

View File

@ -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);
}

View File

@ -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 */

View File

@ -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;

View File

@ -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)

View File

@ -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);