summaryrefslogtreecommitdiffstats
path: root/audio/out/ao_pipewire.c
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-15 20:36:56 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-15 20:36:56 +0000
commit51de1d8436100f725f3576aefa24a2bd2057bc28 (patch)
treec6d1d5264b6d40a8d7ca34129f36b7d61e188af3 /audio/out/ao_pipewire.c
parentInitial commit. (diff)
downloadmpv-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.c883
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, &registry_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(&registry_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}
+ },
+};