From b7a71ea70671f3cc991f5d90a411f017caf573c7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thomas=20Wei=C3=9Fschuh?= <thomas@t-8ch.de>
Date: Mon, 13 Dec 2021 00:10:11 +0100
Subject: [PATCH] ao_pipewire: add support for device selection

---
 audio/out/ao_pipewire.c | 202 +++++++++++++++++++++++++++++++++++++---
 1 file changed, 188 insertions(+), 14 deletions(-)

diff --git a/audio/out/ao_pipewire.c b/audio/out/ao_pipewire.c
index 9621bc5229f..6749270e18d 100644
--- a/audio/out/ao_pipewire.c
+++ b/audio/out/ao_pipewire.c
@@ -21,6 +21,7 @@
  */
 
 #include <pipewire/pipewire.h>
+#include <pipewire/global.h>
 #include <spa/param/audio/format-utils.h>
 #include <spa/param/props.h>
 #include <math.h>
@@ -44,6 +45,8 @@
 struct priv {
     struct pw_thread_loop *loop;
     struct pw_stream *stream;
+    struct pw_core *core;
+    struct spa_hook stream_listener;
 
     int buffer_msec;
     bool muted;
@@ -104,6 +107,7 @@ static enum spa_audio_channel mp_speaker_id_to_spa(struct ao *ao, enum mp_speake
     };
 }
 
+
 static void on_process(void *userdata)
 {
     struct ao *ao = userdata;
@@ -227,12 +231,141 @@ static void uninit(struct ao *ao)
     if (p->stream)
         pw_stream_destroy(p->stream);
     p->stream = NULL;
+    if (p->core)
+        pw_core_disconnect(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 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 (strcmp(type, PW_TYPE_INTERFACE_Node) != 0)
+        return;
+
+    if (!props)
+        return;
+
+    const char *class = spa_dict_lookup(props, PW_KEY_MEDIA_CLASS);
+    if (!class || strcmp(class, "Audio/Sink") != 0)
+        return;
+
+    ctx->sink_cb(ctx->ao, id, props, ctx->sink_cb_ctx);
+}
+
+
+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 pw_thread_loop *loop = data;
+    pw_thread_loop_signal(loop, false);
+}
+
+static const struct pw_core_events for_each_sink_core_events = {
+    .version = PW_VERSION_CORE_EVENTS,
+    .done = for_each_sink_done,
+};
+
+static void 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;
+
+    pw_thread_loop_lock(priv->loop);
+
+    pw_core_add_listener(priv->core, &core_listener, &for_each_sink_core_events, priv->loop);
+    registry = pw_core_get_registry(priv->core, PW_VERSION_REGISTRY, 0);
+    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,
+    };
+    pw_registry_add_listener(registry, &registry_listener, &for_each_sink_registry_events, &revents_ctx);
+    pw_thread_loop_wait(priv->loop);
+
+
+    spa_hook_remove(&core_listener);
+    spa_hook_remove(&registry_listener);
+    pw_proxy_destroy((struct pw_proxy *)registry);
+
+    pw_thread_loop_unlock(priv->loop);
+}
+
+
+static void get_target_id_cb(struct ao *ao, uint32_t id, const struct spa_dict *props, void *ctx)
+{
+    int32_t *target_id = ctx;
+
+    const char *name = spa_dict_lookup(props, PW_KEY_NODE_NAME);
+    if (!name)
+        return;
+
+    if (strcmp(name, ao->device) == 0) {
+        *target_id = id;
+    }
+}
+
+static uint32_t get_target_id(struct ao *ao)
+{
+    uint32_t target_id = 0;
+
+    if (ao->device == NULL)
+        return PW_ID_ANY;
+
+    for_each_sink(ao, get_target_id_cb, &target_id);
+
+    return target_id;
+}
+
+static int pipewire_init_boilerplate(struct ao *ao)
+{
+    struct priv *p = ao->priv;
+    struct pw_context *context;
+
+    pw_init(NULL, NULL);
+
+
+    p->loop = pw_thread_loop_new("ao-pipewire", NULL);
+    if (p->loop == NULL)
+        return -1;
+
+    if (pw_thread_loop_start(p->loop) < 0)
+            return -1;
+
+    context = pw_context_new(pw_thread_loop_get_loop(p->loop), NULL, 0);
+    if (!context)
+            return -1;
+
+    p->core = pw_context_connect(context, NULL, 0);
+    if (!p->core)
+            return -1;
+
+    return 0;
+}
+
+
 static int init(struct ao *ao)
 {
     struct priv *p = ao->priv;
@@ -252,6 +385,9 @@ static int init(struct ao *ao)
         NULL
     );
 
+    if (pipewire_init_boilerplate(ao) < 0)
+        goto error;
+
     ao->device_buffer = p->buffer_msec * ao->samplerate / 1000;
 
     pw_properties_setf(props, PW_KEY_NODE_LATENCY, "%d/%d", ao->device_buffer, ao->samplerate);
@@ -282,33 +418,42 @@ static int init(struct ao *ao)
         ao->sstride = ao->channels.num * af_fmt_to_bytes(ao->format);
     }
 
-    pw_init(NULL, NULL);
+    pw_thread_loop_lock(p->loop);
 
-    p->loop = pw_thread_loop_new("ao-pipewire", NULL);
-    if (p->loop == NULL)
+    p->stream = pw_stream_new(
+                    p->core,
+                    "audio-src",
+                    props);
+    if (p->stream == NULL) {
+        pw_thread_loop_unlock(p->loop);
         goto error;
+    }
 
-    p->stream = pw_stream_new_simple(
-                    pw_thread_loop_get_loop(p->loop),
-                    "audio-src",
-                    props,
-                    &stream_events,
-                    ao);
-    if (p->stream == NULL)
+    pw_stream_add_listener(p->stream,
+                    &p->stream_listener,
+                    &stream_events, ao);
+
+    pw_thread_loop_unlock(p->loop);
+
+    uint32_t target_id = get_target_id(ao);
+    if (target_id == 0)
         goto error;
 
+    pw_thread_loop_lock(p->loop);
+
     if (pw_stream_connect(p->stream,
                     PW_DIRECTION_OUTPUT,
-                    PW_ID_ANY,
+                    target_id,
                     PW_STREAM_FLAG_AUTOCONNECT |
                     PW_STREAM_FLAG_INACTIVE |
                     PW_STREAM_FLAG_MAP_BUFFERS |
                     PW_STREAM_FLAG_RT_PROCESS,
-                    params, 1) < 0)
+                    params, 1) < 0) {
+        pw_thread_loop_unlock(p->loop);
         goto error;
+    }
 
-    if (pw_thread_loop_start(p->loop) < 0)
-        goto error;
+    pw_thread_loop_unlock(p->loop);
 
     return 0;
 
@@ -389,6 +534,33 @@ static int control(struct ao *ao, enum aocontrol cmd, void *arg)
     }
 }
 
+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 list_devs(struct ao *ao, struct ao_device_list *list)
+{
+    // we are not using hotplug_{,un}init() because the AO core will only call
+    // the hotplug functions of a single AO. That will probably be ao_pulse.
+    if (pipewire_init_boilerplate(ao) < 0)
+        return;
+
+    ao_device_list_add(list, ao, &(struct ao_device_desc){});
+
+    for_each_sink(ao, add_device_to_list, list);
+
+    uninit(ao);
+}
+
 #define OPT_BASE_STRUCT struct priv
 
 const struct ao_driver audio_out_pipewire = {
@@ -402,6 +574,8 @@ const struct ao_driver audio_out_pipewire = {
 
     .control     = control,
 
+    .list_devs = list_devs,
+
     .priv_size = sizeof(struct priv),
     .priv_defaults = &(const struct priv)
     {
