diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-15 20:36:56 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-15 20:36:56 +0000 |
commit | 51de1d8436100f725f3576aefa24a2bd2057bc28 (patch) | |
tree | c6d1d5264b6d40a8d7ca34129f36b7d61e188af3 /audio/out/ao_pipewire.c | |
parent | Initial commit. (diff) | |
download | mpv-51de1d8436100f725f3576aefa24a2bd2057bc28.tar.xz mpv-51de1d8436100f725f3576aefa24a2bd2057bc28.zip |
Adding upstream version 0.37.0.upstream/0.37.0
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'audio/out/ao_pipewire.c')
-rw-r--r-- | audio/out/ao_pipewire.c | 883 |
1 files changed, 883 insertions, 0 deletions
diff --git a/audio/out/ao_pipewire.c b/audio/out/ao_pipewire.c new file mode 100644 index 0000000..3fbcbf6 --- /dev/null +++ b/audio/out/ao_pipewire.c @@ -0,0 +1,883 @@ +/* + * PipeWire audio output driver. + * Copyright (C) 2021 Thomas Weißschuh <thomas@t-8ch.de> + * Copyright (C) 2021 Oschowa <oschowa@web.de> + * Copyright (C) 2020 Andreas Kempf <aakempf@gmail.com> + * + * This file is part of mpv. + * + * mpv is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * mpv is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see <http://www.gnu.org/licenses/>. + */ + +#include <pipewire/pipewire.h> +#include <pipewire/global.h> +#include <spa/param/audio/format-utils.h> +#include <spa/param/props.h> +#include <spa/utils/result.h> +#include <math.h> + +#include "common/msg.h" +#include "options/m_config.h" +#include "options/m_option.h" +#include "ao.h" +#include "audio/format.h" +#include "internal.h" +#include "osdep/timer.h" + +#if !PW_CHECK_VERSION(0, 3, 50) +static inline int pw_stream_get_time_n(struct pw_stream *stream, struct pw_time *time, size_t size) { + return pw_stream_get_time(stream, time); +} +#endif + +#if !PW_CHECK_VERSION(0, 3, 57) +// Earlier versions segfault on zeroed hooks +#define spa_hook_remove(hook) if ((hook)->link.prev) spa_hook_remove(hook) +#endif + +enum init_state { + INIT_STATE_NONE, + INIT_STATE_SUCCESS, + INIT_STATE_ERROR, +}; + +enum { + VOLUME_MODE_CHANNEL, + VOLUME_MODE_GLOBAL, +}; + +struct priv { + struct pw_thread_loop *loop; + struct pw_stream *stream; + struct pw_core *core; + struct spa_hook stream_listener; + struct spa_hook core_listener; + enum init_state init_state; + + bool muted; + float volume; + + struct { + int buffer_msec; + char *remote; + int volume_mode; + } options; + + struct { + struct pw_registry *registry; + struct spa_hook registry_listener; + struct spa_list sinks; + } hotplug; +}; + +struct id_list { + uint32_t id; + struct spa_list node; +}; + +static enum spa_audio_format af_fmt_to_pw(struct ao *ao, enum af_format format) +{ + switch (format) { + case AF_FORMAT_U8: return SPA_AUDIO_FORMAT_U8; + case AF_FORMAT_S16: return SPA_AUDIO_FORMAT_S16; + case AF_FORMAT_S32: return SPA_AUDIO_FORMAT_S32; + case AF_FORMAT_FLOAT: return SPA_AUDIO_FORMAT_F32; + case AF_FORMAT_DOUBLE: return SPA_AUDIO_FORMAT_F64; + case AF_FORMAT_U8P: return SPA_AUDIO_FORMAT_U8P; + case AF_FORMAT_S16P: return SPA_AUDIO_FORMAT_S16P; + case AF_FORMAT_S32P: return SPA_AUDIO_FORMAT_S32P; + case AF_FORMAT_FLOATP: return SPA_AUDIO_FORMAT_F32P; + case AF_FORMAT_DOUBLEP: return SPA_AUDIO_FORMAT_F64P; + default: + MP_WARN(ao, "Unhandled format %d\n", format); + return SPA_AUDIO_FORMAT_UNKNOWN; + } +} + +static enum spa_audio_channel mp_speaker_id_to_spa(struct ao *ao, enum mp_speaker_id mp_speaker_id) +{ + switch (mp_speaker_id) { + case MP_SPEAKER_ID_FL: return SPA_AUDIO_CHANNEL_FL; + case MP_SPEAKER_ID_FR: return SPA_AUDIO_CHANNEL_FR; + case MP_SPEAKER_ID_FC: return SPA_AUDIO_CHANNEL_FC; + case MP_SPEAKER_ID_LFE: return SPA_AUDIO_CHANNEL_LFE; + case MP_SPEAKER_ID_BL: return SPA_AUDIO_CHANNEL_RL; + case MP_SPEAKER_ID_BR: return SPA_AUDIO_CHANNEL_RR; + case MP_SPEAKER_ID_FLC: return SPA_AUDIO_CHANNEL_FLC; + case MP_SPEAKER_ID_FRC: return SPA_AUDIO_CHANNEL_FRC; + case MP_SPEAKER_ID_BC: return SPA_AUDIO_CHANNEL_RC; + case MP_SPEAKER_ID_SL: return SPA_AUDIO_CHANNEL_SL; + case MP_SPEAKER_ID_SR: return SPA_AUDIO_CHANNEL_SR; + case MP_SPEAKER_ID_TC: return SPA_AUDIO_CHANNEL_TC; + case MP_SPEAKER_ID_TFL: return SPA_AUDIO_CHANNEL_TFL; + case MP_SPEAKER_ID_TFC: return SPA_AUDIO_CHANNEL_TFC; + case MP_SPEAKER_ID_TFR: return SPA_AUDIO_CHANNEL_TFR; + case MP_SPEAKER_ID_TBL: return SPA_AUDIO_CHANNEL_TRL; + case MP_SPEAKER_ID_TBC: return SPA_AUDIO_CHANNEL_TRC; + case MP_SPEAKER_ID_TBR: return SPA_AUDIO_CHANNEL_TRR; + case MP_SPEAKER_ID_DL: return SPA_AUDIO_CHANNEL_FL; + case MP_SPEAKER_ID_DR: return SPA_AUDIO_CHANNEL_FR; + case MP_SPEAKER_ID_WL: return SPA_AUDIO_CHANNEL_FL; + case MP_SPEAKER_ID_WR: return SPA_AUDIO_CHANNEL_FR; + case MP_SPEAKER_ID_SDL: return SPA_AUDIO_CHANNEL_SL; + case MP_SPEAKER_ID_SDR: return SPA_AUDIO_CHANNEL_SL; + case MP_SPEAKER_ID_LFE2: return SPA_AUDIO_CHANNEL_LFE2; + case MP_SPEAKER_ID_TSL: return SPA_AUDIO_CHANNEL_TSL; + case MP_SPEAKER_ID_TSR: return SPA_AUDIO_CHANNEL_TSR; + case MP_SPEAKER_ID_BFC: return SPA_AUDIO_CHANNEL_BC; + case MP_SPEAKER_ID_BFL: return SPA_AUDIO_CHANNEL_BLC; + case MP_SPEAKER_ID_BFR: return SPA_AUDIO_CHANNEL_BRC; + case MP_SPEAKER_ID_NA: return SPA_AUDIO_CHANNEL_NA; + default: + MP_WARN(ao, "Unhandled channel %d\n", mp_speaker_id); + return SPA_AUDIO_CHANNEL_UNKNOWN; + }; +} + +static void on_process(void *userdata) +{ + struct ao *ao = userdata; + struct priv *p = ao->priv; + struct pw_time time; + struct pw_buffer *b; + void *data[MP_NUM_CHANNELS]; + + if ((b = pw_stream_dequeue_buffer(p->stream)) == NULL) { + MP_WARN(ao, "out of buffers: %s\n", strerror(errno)); + return; + } + + struct spa_buffer *buf = b->buffer; + + int bytes_per_channel = buf->datas[0].maxsize / ao->channels.num; + int nframes = bytes_per_channel / ao->sstride; +#if PW_CHECK_VERSION(0, 3, 49) + if (b->requested != 0) + nframes = MPMIN(b->requested, nframes); +#endif + + for (int i = 0; i < buf->n_datas; i++) + data[i] = buf->datas[i].data; + + pw_stream_get_time_n(p->stream, &time, sizeof(time)); + if (time.rate.denom == 0) + time.rate.denom = ao->samplerate; + if (time.rate.num == 0) + time.rate.num = 1; + + int64_t end_time = mp_time_ns(); + /* time.queued is always going to be 0, so we don't need to care */ + end_time += (nframes * 1e9 / ao->samplerate) + + ((double) time.delay * SPA_NSEC_PER_SEC * time.rate.num / time.rate.denom); + + int samples = ao_read_data_nonblocking(ao, data, nframes, end_time); + b->size = samples; + + for (int i = 0; i < buf->n_datas; i++) { + buf->datas[i].chunk->size = samples * ao->sstride; + buf->datas[i].chunk->offset = 0; + buf->datas[i].chunk->stride = ao->sstride; + } + + pw_stream_queue_buffer(p->stream, b); + + MP_TRACE(ao, "queued %d of %d samples\n", samples, nframes); +} + +static void on_param_changed(void *userdata, uint32_t id, const struct spa_pod *param) +{ + struct ao *ao = userdata; + struct priv *p = ao->priv; + const struct spa_pod *params[1]; + uint8_t buffer[1024]; + struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer)); + + /* We want to know when our node is linked. + * As there is no proper callback for this we use the Latency param for this + */ + if (id == SPA_PARAM_Latency) { + p->init_state = INIT_STATE_SUCCESS; + pw_thread_loop_signal(p->loop, false); + } + + if (param == NULL || id != SPA_PARAM_Format) + return; + + int buffer_size = ao->device_buffer * af_fmt_to_bytes(ao->format) * ao->channels.num; + + params[0] = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers, + SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(ao->num_planes), + SPA_PARAM_BUFFERS_size, SPA_POD_CHOICE_RANGE_Int( + buffer_size, 0, INT32_MAX), + SPA_PARAM_BUFFERS_stride, SPA_POD_Int(ao->sstride)); + if (!params[0]) { + MP_ERR(ao, "Could not build parameter pod\n"); + return; + } + + if (pw_stream_update_params(p->stream, params, 1) < 0) { + MP_ERR(ao, "Could not update stream parameters\n"); + return; + } +} + +static void on_state_changed(void *userdata, enum pw_stream_state old, enum pw_stream_state state, const char *error) +{ + struct ao *ao = userdata; + struct priv *p = ao->priv; + MP_DBG(ao, "Stream state changed: old_state=%s state=%s error=%s\n", + pw_stream_state_as_string(old), pw_stream_state_as_string(state), error); + + if (state == PW_STREAM_STATE_ERROR) { + MP_WARN(ao, "Stream in error state, trying to reload...\n"); + p->init_state = INIT_STATE_ERROR; + pw_thread_loop_signal(p->loop, false); + ao_request_reload(ao); + } + + if (state == PW_STREAM_STATE_UNCONNECTED && old != PW_STREAM_STATE_UNCONNECTED) { + MP_WARN(ao, "Stream disconnected, trying to reload...\n"); + ao_request_reload(ao); + } +} + +static float spa_volume_to_mp_volume(float vol) +{ + return vol * 100; +} + +static float mp_volume_to_spa_volume(float vol) +{ + return vol / 100; +} + +static float volume_avg(float* vols, uint32_t n) +{ + float sum = 0.0; + for (int i = 0; i < n; i++) + sum += vols[i]; + return sum / n; +} + +static void on_control_info(void *userdata, uint32_t id, + const struct pw_stream_control *control) +{ + struct ao *ao = userdata; + struct priv *p = ao->priv; + + switch (id) { + case SPA_PROP_mute: + if (control->n_values == 1) + p->muted = control->values[0] >= 0.5; + break; + case SPA_PROP_channelVolumes: + if (p->options.volume_mode != VOLUME_MODE_CHANNEL) + break; + if (control->n_values > 0) + p->volume = volume_avg(control->values, control->n_values); + break; + case SPA_PROP_volume: + if (p->options.volume_mode != VOLUME_MODE_GLOBAL) + break; + if (control->n_values > 0) + p->volume = control->values[0]; + break; + } +} + +static const struct pw_stream_events stream_events = { + .version = PW_VERSION_STREAM_EVENTS, + .param_changed = on_param_changed, + .process = on_process, + .state_changed = on_state_changed, + .control_info = on_control_info, +}; + +static void uninit(struct ao *ao) +{ + struct priv *p = ao->priv; + if (p->loop) + pw_thread_loop_stop(p->loop); + spa_hook_remove(&p->stream_listener); + spa_zero(p->stream_listener); + if (p->stream) + pw_stream_destroy(p->stream); + p->stream = NULL; + if (p->core) + pw_context_destroy(pw_core_get_context(p->core)); + p->core = NULL; + if (p->loop) + pw_thread_loop_destroy(p->loop); + p->loop = NULL; + pw_deinit(); +} + +struct registry_event_global_ctx { + struct ao *ao; + void (*sink_cb) (struct ao *ao, uint32_t id, const struct spa_dict *props, void *sink_cb_ctx); + void *sink_cb_ctx; +}; + +static bool is_sink_node(const char *type, const struct spa_dict *props) +{ + if (strcmp(type, PW_TYPE_INTERFACE_Node) != 0) + return false; + + if (!props) + return false; + + const char *class = spa_dict_lookup(props, PW_KEY_MEDIA_CLASS); + if (!class || strcmp(class, "Audio/Sink") != 0) + return false; + + return true; +} + +static void for_each_sink_registry_event_global(void *data, uint32_t id, + uint32_t permissions, const + char *type, uint32_t version, + const struct spa_dict *props) +{ + struct registry_event_global_ctx *ctx = data; + + if (!is_sink_node(type, props)) + return; + + ctx->sink_cb(ctx->ao, id, props, ctx->sink_cb_ctx); +} + + +struct for_each_done_ctx { + struct pw_thread_loop *loop; + bool done; +}; + +static const struct pw_registry_events for_each_sink_registry_events = { + .version = PW_VERSION_REGISTRY_EVENTS, + .global = for_each_sink_registry_event_global, +}; + +static void for_each_sink_done(void *data, uint32_t it, int seq) +{ + struct for_each_done_ctx *ctx = data; + ctx->done = true; + pw_thread_loop_signal(ctx->loop, false); +} + +static const struct pw_core_events for_each_sink_core_events = { + .version = PW_VERSION_CORE_EVENTS, + .done = for_each_sink_done, +}; + +static int for_each_sink(struct ao *ao, void (cb) (struct ao *ao, uint32_t id, + const struct spa_dict *props, void *ctx), void *cb_ctx) +{ + struct priv *priv = ao->priv; + struct pw_registry *registry; + struct spa_hook core_listener; + struct for_each_done_ctx done_ctx = { + .loop = priv->loop, + .done = false, + }; + int ret = -1; + + pw_thread_loop_lock(priv->loop); + + spa_zero(core_listener); + if (pw_core_add_listener(priv->core, &core_listener, &for_each_sink_core_events, &done_ctx) < 0) + goto unlock_loop; + + registry = pw_core_get_registry(priv->core, PW_VERSION_REGISTRY, 0); + if (!registry) + goto remove_core_listener; + + pw_core_sync(priv->core, 0, 0); + + struct spa_hook registry_listener; + struct registry_event_global_ctx revents_ctx = { + .ao = ao, + .sink_cb = cb, + .sink_cb_ctx = cb_ctx, + }; + spa_zero(registry_listener); + if (pw_registry_add_listener(registry, ®istry_listener, &for_each_sink_registry_events, &revents_ctx) < 0) + goto destroy_registry; + + while (!done_ctx.done) + pw_thread_loop_wait(priv->loop); + + spa_hook_remove(®istry_listener); + + ret = 0; + +destroy_registry: + pw_proxy_destroy((struct pw_proxy *)registry); + +remove_core_listener: + spa_hook_remove(&core_listener); + +unlock_loop: + pw_thread_loop_unlock(priv->loop); + + return ret; +} + +static void have_sink(struct ao *ao, uint32_t id, const struct spa_dict *props, void *ctx) +{ + bool *b = ctx; + *b = true; +} + +static bool session_has_sinks(struct ao *ao) +{ + bool b = false; + + if (for_each_sink(ao, have_sink, &b) < 0) + MP_WARN(ao, "Could not list devices, sink detection may be wrong\n"); + + return b; +} + +static void on_error(void *data, uint32_t id, int seq, int res, const char *message) +{ + struct ao *ao = data; + + MP_WARN(ao, "Error during playback: %s, %s\n", spa_strerror(res), message); +} + +static void on_core_info(void *data, const struct pw_core_info *info) +{ + struct ao *ao = data; + + MP_VERBOSE(ao, "Core user: %s\n", info->user_name); + MP_VERBOSE(ao, "Core host: %s\n", info->host_name); + MP_VERBOSE(ao, "Core version: %s\n", info->version); + MP_VERBOSE(ao, "Core name: %s\n", info->name); +} + +static const struct pw_core_events core_events = { + .version = PW_VERSION_CORE_EVENTS, + .error = on_error, + .info = on_core_info, +}; + +static int pipewire_init_boilerplate(struct ao *ao) +{ + struct priv *p = ao->priv; + struct pw_context *context; + + pw_init(NULL, NULL); + + MP_VERBOSE(ao, "Headers version: %s\n", pw_get_headers_version()); + MP_VERBOSE(ao, "Library version: %s\n", pw_get_library_version()); + + p->loop = pw_thread_loop_new("mpv/ao/pipewire", NULL); + if (p->loop == NULL) + return -1; + + pw_thread_loop_lock(p->loop); + + if (pw_thread_loop_start(p->loop) < 0) + goto error; + + context = pw_context_new( + pw_thread_loop_get_loop(p->loop), + pw_properties_new(PW_KEY_CONFIG_NAME, "client-rt.conf", NULL), + 0); + if (!context) + goto error; + + p->core = pw_context_connect( + context, + pw_properties_new(PW_KEY_REMOTE_NAME, p->options.remote, NULL), + 0); + if (!p->core) { + MP_MSG(ao, ao->probing ? MSGL_V : MSGL_ERR, + "Could not connect to context '%s': %s\n", + p->options.remote, strerror(errno)); + pw_context_destroy(context); + goto error; + } + + if (pw_core_add_listener(p->core, &p->core_listener, &core_events, ao) < 0) + goto error; + + pw_thread_loop_unlock(p->loop); + + if (!session_has_sinks(ao)) { + MP_VERBOSE(ao, "PipeWire does not have any audio sinks, skipping\n"); + return -1; + } + + return 0; + +error: + pw_thread_loop_unlock(p->loop); + return -1; +} + +static void wait_for_init_done(struct ao *ao) +{ + struct priv *p = ao->priv; + struct timespec abstime; + int r; + + r = pw_thread_loop_get_time(p->loop, &abstime, 50 * SPA_NSEC_PER_MSEC); + if (r < 0) { + MP_WARN(ao, "Could not get timeout for initialization: %s\n", spa_strerror(r)); + return; + } + + while (p->init_state == INIT_STATE_NONE) { + r = pw_thread_loop_timed_wait_full(p->loop, &abstime); + if (r < 0) { + MP_WARN(ao, "Could not wait for initialization: %s\n", spa_strerror(r)); + return; + } + } +} + +static int init(struct ao *ao) +{ + struct priv *p = ao->priv; + uint8_t buffer[1024]; + struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer)); + const struct spa_pod *params[1]; + struct pw_properties *props = pw_properties_new( + PW_KEY_MEDIA_TYPE, "Audio", + PW_KEY_MEDIA_CATEGORY, "Playback", + PW_KEY_MEDIA_ROLE, ao->init_flags & AO_INIT_MEDIA_ROLE_MUSIC ? "Music" : "Movie", + PW_KEY_NODE_NAME, ao->client_name, + PW_KEY_NODE_DESCRIPTION, ao->client_name, + PW_KEY_APP_NAME, ao->client_name, + PW_KEY_APP_ID, ao->client_name, + PW_KEY_APP_ICON_NAME, ao->client_name, + PW_KEY_NODE_ALWAYS_PROCESS, "true", + PW_KEY_TARGET_OBJECT, ao->device, + NULL + ); + + if (pipewire_init_boilerplate(ao) < 0) + goto error_props; + + if (p->options.buffer_msec) { + ao->device_buffer = p->options.buffer_msec * ao->samplerate / 1000; + + pw_properties_setf(props, PW_KEY_NODE_LATENCY, "%d/%d", ao->device_buffer, ao->samplerate); + } + + pw_properties_setf(props, PW_KEY_NODE_RATE, "1/%d", ao->samplerate); + + enum spa_audio_format spa_format = af_fmt_to_pw(ao, ao->format); + if (spa_format == SPA_AUDIO_FORMAT_UNKNOWN) { + ao->format = AF_FORMAT_FLOATP; + spa_format = SPA_AUDIO_FORMAT_F32P; + } + + struct spa_audio_info_raw audio_info = { + .format = spa_format, + .rate = ao->samplerate, + .channels = ao->channels.num, + }; + + for (int i = 0; i < ao->channels.num; i++) + audio_info.position[i] = mp_speaker_id_to_spa(ao, ao->channels.speaker[i]); + + params[0] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat, &audio_info); + if (!params[0]) + goto error_props; + + if (af_fmt_is_planar(ao->format)) { + ao->num_planes = ao->channels.num; + ao->sstride = af_fmt_to_bytes(ao->format); + } else { + ao->num_planes = 1; + ao->sstride = ao->channels.num * af_fmt_to_bytes(ao->format); + } + + pw_thread_loop_lock(p->loop); + + p->stream = pw_stream_new(p->core, "audio-src", props); + if (p->stream == NULL) { + pw_thread_loop_unlock(p->loop); + goto error; + } + + pw_stream_add_listener(p->stream, &p->stream_listener, &stream_events, ao); + + enum pw_stream_flags flags = PW_STREAM_FLAG_AUTOCONNECT | + PW_STREAM_FLAG_INACTIVE | + PW_STREAM_FLAG_MAP_BUFFERS | + PW_STREAM_FLAG_RT_PROCESS; + + if (ao->init_flags & AO_INIT_EXCLUSIVE) + flags |= PW_STREAM_FLAG_EXCLUSIVE; + + if (pw_stream_connect(p->stream, + PW_DIRECTION_OUTPUT, PW_ID_ANY, flags, params, 1) < 0) { + pw_thread_loop_unlock(p->loop); + goto error; + } + + wait_for_init_done(ao); + + pw_thread_loop_unlock(p->loop); + + if (p->init_state == INIT_STATE_ERROR) + goto error; + + return 0; + +error_props: + pw_properties_free(props); +error: + uninit(ao); + return -1; +} + +static void reset(struct ao *ao) +{ + struct priv *p = ao->priv; + pw_thread_loop_lock(p->loop); + pw_stream_set_active(p->stream, false); + pw_stream_flush(p->stream, false); + pw_thread_loop_unlock(p->loop); +} + +static void start(struct ao *ao) +{ + struct priv *p = ao->priv; + pw_thread_loop_lock(p->loop); + pw_stream_set_active(p->stream, true); + pw_thread_loop_unlock(p->loop); +} + +#define CONTROL_RET(r) (!r ? CONTROL_OK : CONTROL_ERROR) + +static int control(struct ao *ao, enum aocontrol cmd, void *arg) +{ + struct priv *p = ao->priv; + + switch (cmd) { + case AOCONTROL_GET_VOLUME: { + float *vol = arg; + *vol = spa_volume_to_mp_volume(p->volume); + return CONTROL_OK; + } + case AOCONTROL_GET_MUTE: { + bool *muted = arg; + *muted = p->muted; + return CONTROL_OK; + } + case AOCONTROL_SET_VOLUME: + case AOCONTROL_SET_MUTE: + case AOCONTROL_UPDATE_STREAM_TITLE: { + int ret; + + pw_thread_loop_lock(p->loop); + switch (cmd) { + case AOCONTROL_SET_VOLUME: { + float *vol = arg; + uint8_t n = ao->channels.num; + if (p->options.volume_mode == VOLUME_MODE_CHANNEL) { + float values[MP_NUM_CHANNELS] = {0}; + for (int i = 0; i < n; i++) + values[i] = mp_volume_to_spa_volume(*vol); + ret = CONTROL_RET(pw_stream_set_control( + p->stream, SPA_PROP_channelVolumes, n, values, 0)); + } else { + float value = mp_volume_to_spa_volume(*vol); + ret = CONTROL_RET(pw_stream_set_control( + p->stream, SPA_PROP_volume, 1, &value, 0)); + } + break; + } + case AOCONTROL_SET_MUTE: { + bool *muted = arg; + float value = *muted ? 1.f : 0.f; + ret = CONTROL_RET(pw_stream_set_control(p->stream, SPA_PROP_mute, 1, &value, 0)); + break; + } + case AOCONTROL_UPDATE_STREAM_TITLE: { + char *title = arg; + struct spa_dict_item items[1]; + items[0] = SPA_DICT_ITEM_INIT(PW_KEY_MEDIA_NAME, title); + ret = CONTROL_RET(pw_stream_update_properties(p->stream, &SPA_DICT_INIT(items, MP_ARRAY_SIZE(items)))); + break; + } + default: + ret = CONTROL_NA; + } + pw_thread_loop_unlock(p->loop); + return ret; + } + default: + return CONTROL_UNKNOWN; + } +} + +static void add_device_to_list(struct ao *ao, uint32_t id, const struct spa_dict *props, void *ctx) +{ + struct ao_device_list *list = ctx; + const char *name = spa_dict_lookup(props, PW_KEY_NODE_NAME); + + if (!name) + return; + + const char *description = spa_dict_lookup(props, PW_KEY_NODE_DESCRIPTION); + + ao_device_list_add(list, ao, &(struct ao_device_desc){name, description}); +} + +static void hotplug_registry_global_cb(void *data, uint32_t id, + uint32_t permissions, const char *type, + uint32_t version, const struct spa_dict *props) +{ + struct ao *ao = data; + struct priv *priv = ao->priv; + + if (!is_sink_node(type, props)) + return; + + pw_thread_loop_lock(priv->loop); + struct id_list *item = talloc(ao, struct id_list); + item->id = id; + spa_list_init(&item->node); + spa_list_append(&priv->hotplug.sinks, &item->node); + pw_thread_loop_unlock(priv->loop); + + ao_hotplug_event(ao); +} + +static void hotplug_registry_global_remove_cb(void *data, uint32_t id) +{ + struct ao *ao = data; + struct priv *priv = ao->priv; + bool removed_sink = false; + + struct id_list *e; + + pw_thread_loop_lock(priv->loop); + spa_list_for_each(e, &priv->hotplug.sinks, node) { + if (e->id == id) { + removed_sink = true; + spa_list_remove(&e->node); + talloc_free(e); + break; + } + } + + pw_thread_loop_unlock(priv->loop); + + if (removed_sink) + ao_hotplug_event(ao); +} + +static const struct pw_registry_events hotplug_registry_events = { + .version = PW_VERSION_REGISTRY_EVENTS, + .global = hotplug_registry_global_cb, + .global_remove = hotplug_registry_global_remove_cb, +}; + +static int hotplug_init(struct ao *ao) +{ + struct priv *priv = ao->priv; + + int res = pipewire_init_boilerplate(ao); + if (res) + goto error_no_unlock; + + pw_thread_loop_lock(priv->loop); + + spa_zero(priv->hotplug); + spa_list_init(&priv->hotplug.sinks); + + priv->hotplug.registry = pw_core_get_registry(priv->core, PW_VERSION_REGISTRY, 0); + if (!priv->hotplug.registry) + goto error; + + if (pw_registry_add_listener(priv->hotplug.registry, &priv->hotplug.registry_listener, &hotplug_registry_events, ao) < 0) { + pw_proxy_destroy((struct pw_proxy *)priv->hotplug.registry); + goto error; + } + + pw_thread_loop_unlock(priv->loop); + + return res; + +error: + pw_thread_loop_unlock(priv->loop); +error_no_unlock: + uninit(ao); + return -1; +} + +static void hotplug_uninit(struct ao *ao) +{ + struct priv *priv = ao->priv; + + pw_thread_loop_lock(priv->loop); + + spa_hook_remove(&priv->hotplug.registry_listener); + pw_proxy_destroy((struct pw_proxy *)priv->hotplug.registry); + + pw_thread_loop_unlock(priv->loop); + uninit(ao); +} + +static void list_devs(struct ao *ao, struct ao_device_list *list) +{ + ao_device_list_add(list, ao, &(struct ao_device_desc){}); + + if (for_each_sink(ao, add_device_to_list, list) < 0) + MP_WARN(ao, "Could not list devices, list may be incomplete\n"); +} + +#define OPT_BASE_STRUCT struct priv + +const struct ao_driver audio_out_pipewire = { + .description = "PipeWire audio output", + .name = "pipewire", + + .init = init, + .uninit = uninit, + .reset = reset, + .start = start, + + .control = control, + + .hotplug_init = hotplug_init, + .hotplug_uninit = hotplug_uninit, + .list_devs = list_devs, + + .priv_size = sizeof(struct priv), + .priv_defaults = &(const struct priv) + { + .loop = NULL, + .stream = NULL, + .init_state = INIT_STATE_NONE, + .options.buffer_msec = 0, + .options.volume_mode = VOLUME_MODE_CHANNEL, + }, + .options_prefix = "pipewire", + .options = (const struct m_option[]) { + {"buffer", OPT_CHOICE(options.buffer_msec, {"native", 0}), + M_RANGE(1, 2000)}, + {"remote", OPT_STRING(options.remote) }, + {"volume-mode", OPT_CHOICE(options.volume_mode, + {"channel", VOLUME_MODE_CHANNEL}, {"global", VOLUME_MODE_GLOBAL})}, + {0} + }, +}; |