2023-03-08 16:06:29 +01:00
|
|
|
From 4d25fde40080c4fb55583b23703a8bbc8b507924 Mon Sep 17 00:00:00 2001
|
2023-02-23 21:25:12 +01:00
|
|
|
From: Dimitris Papaioannou <dimtpap@protonmail.com>
|
|
|
|
Date: Sun, 26 Jun 2022 17:14:24 +0300
|
|
|
|
Subject: [PATCH] linux-pipewire: Add PipeWire audio captures
|
|
|
|
|
|
|
|
---
|
|
|
|
plugins/linux-pipewire/CMakeLists.txt | 6 +-
|
|
|
|
plugins/linux-pipewire/data/locale/en-US.ini | 6 +
|
|
|
|
plugins/linux-pipewire/linux-pipewire.c | 5 +
|
2023-03-08 16:06:29 +01:00
|
|
|
.../pipewire-audio-capture-app.c | 918 ++++++++++++++++++
|
|
|
|
.../pipewire-audio-capture-device.c | 520 ++++++++++
|
|
|
|
plugins/linux-pipewire/pipewire-audio.c | 574 +++++++++++
|
|
|
|
plugins/linux-pipewire/pipewire-audio.h | 155 +++
|
|
|
|
7 files changed, 2183 insertions(+), 1 deletion(-)
|
2023-02-23 21:25:12 +01:00
|
|
|
create mode 100644 plugins/linux-pipewire/pipewire-audio-capture-app.c
|
|
|
|
create mode 100644 plugins/linux-pipewire/pipewire-audio-capture-device.c
|
|
|
|
create mode 100644 plugins/linux-pipewire/pipewire-audio.c
|
|
|
|
create mode 100644 plugins/linux-pipewire/pipewire-audio.h
|
|
|
|
|
|
|
|
diff --git a/plugins/linux-pipewire/CMakeLists.txt b/plugins/linux-pipewire/CMakeLists.txt
|
2023-03-08 16:06:29 +01:00
|
|
|
index 31a55e68b60d7..1e23a3163d26b 100644
|
2023-02-23 21:25:12 +01:00
|
|
|
--- a/plugins/linux-pipewire/CMakeLists.txt
|
|
|
|
+++ b/plugins/linux-pipewire/CMakeLists.txt
|
|
|
|
@@ -38,7 +38,11 @@ target_sources(
|
|
|
|
portal.c
|
|
|
|
portal.h
|
|
|
|
screencast-portal.c
|
|
|
|
- screencast-portal.h)
|
|
|
|
+ screencast-portal.h
|
|
|
|
+ pipewire-audio.c
|
|
|
|
+ pipewire-audio.h
|
|
|
|
+ pipewire-audio-capture-device.c
|
|
|
|
+ pipewire-audio-capture-app.c)
|
|
|
|
|
|
|
|
target_link_libraries(
|
|
|
|
linux-pipewire PRIVATE OBS::libobs OBS::obsglad PipeWire::PipeWire GIO::GIO
|
|
|
|
diff --git a/plugins/linux-pipewire/data/locale/en-US.ini b/plugins/linux-pipewire/data/locale/en-US.ini
|
2023-03-08 16:06:29 +01:00
|
|
|
index a9e222a995686..edde250b57c76 100644
|
2023-02-23 21:25:12 +01:00
|
|
|
--- a/plugins/linux-pipewire/data/locale/en-US.ini
|
|
|
|
+++ b/plugins/linux-pipewire/data/locale/en-US.ini
|
|
|
|
@@ -3,3 +3,9 @@ PipeWireSelectMonitor="Select Monitor"
|
|
|
|
PipeWireSelectWindow="Select Window"
|
|
|
|
PipeWireWindowCapture="Window Capture (PipeWire)"
|
|
|
|
ShowCursor="Show Cursor"
|
|
|
|
+PipeWireAudioCaptureInput="Audio Input Capture (PipeWire)"
|
|
|
|
+PipeWireAudioCaptureOutput="Audio Output Capture (PipeWire)"
|
|
|
|
+PipeWireAudioCaptureApplication="Application Audio Capture (PipeWire)"
|
|
|
|
+Device="Device"
|
|
|
|
+Application="Application"
|
|
|
|
+ExceptApp="Capture all apps except selected"
|
|
|
|
diff --git a/plugins/linux-pipewire/linux-pipewire.c b/plugins/linux-pipewire/linux-pipewire.c
|
2023-03-08 16:06:29 +01:00
|
|
|
index 798ae2fe8ca9f..253256dfd469f 100644
|
2023-02-23 21:25:12 +01:00
|
|
|
--- a/plugins/linux-pipewire/linux-pipewire.c
|
|
|
|
+++ b/plugins/linux-pipewire/linux-pipewire.c
|
|
|
|
@@ -2,6 +2,7 @@
|
|
|
|
*
|
|
|
|
* Copyright 2021 columbarius <co1umbarius@protonmail.com>
|
|
|
|
* Copyright 2021 Georges Basile Stavracas Neto <georges.stavracas@gmail.com>
|
|
|
|
+ * Copyright 2022 Dimitris Papaioannou <dimtpap@protonmail.com>
|
|
|
|
*
|
|
|
|
* This program is free software: you can redistribute it and/or modify
|
|
|
|
* it under the terms of the GNU General Public License as published by
|
|
|
|
@@ -24,6 +25,7 @@
|
|
|
|
|
|
|
|
#include <pipewire/pipewire.h>
|
|
|
|
#include "screencast-portal.h"
|
|
|
|
+#include "pipewire-audio.h"
|
|
|
|
|
|
|
|
OBS_DECLARE_MODULE()
|
|
|
|
OBS_MODULE_USE_DEFAULT_LOCALE("linux-pipewire", "en-US")
|
|
|
|
@@ -38,6 +40,9 @@ bool obs_module_load(void)
|
|
|
|
|
|
|
|
screencast_portal_load();
|
|
|
|
|
|
|
|
+ pipewire_audio_capture_load();
|
|
|
|
+ pipewire_audio_capture_app_load();
|
|
|
|
+
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
diff --git a/plugins/linux-pipewire/pipewire-audio-capture-app.c b/plugins/linux-pipewire/pipewire-audio-capture-app.c
|
|
|
|
new file mode 100644
|
2023-03-08 16:06:29 +01:00
|
|
|
index 0000000000000..461d2c8f55f26
|
2023-02-23 21:25:12 +01:00
|
|
|
--- /dev/null
|
|
|
|
+++ b/plugins/linux-pipewire/pipewire-audio-capture-app.c
|
2023-03-08 16:06:29 +01:00
|
|
|
@@ -0,0 +1,918 @@
|
|
|
|
+/* pipewire-audio-capture-app.c
|
2023-02-23 21:25:12 +01:00
|
|
|
+ *
|
|
|
|
+ * Copyright 2022 Dimitris Papaioannou <dimtpap@protonmail.com>
|
|
|
|
+ *
|
|
|
|
+ * This program is free software: you can redistribute it and/or modify
|
|
|
|
+ * it under the terms of the GNU General Public License as published by
|
|
|
|
+ * the Free Software Foundation, either version 2 of the License, or
|
|
|
|
+ * (at your option) any later version.
|
|
|
|
+ *
|
|
|
|
+ * This program 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 General Public License for more details.
|
|
|
|
+ *
|
|
|
|
+ * You should have received a copy of the GNU General Public License
|
|
|
|
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
+ *
|
|
|
|
+ * SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
+ */
|
|
|
|
+
|
|
|
|
+#include "pipewire-audio.h"
|
|
|
|
+
|
|
|
|
+#include <util/dstr.h>
|
|
|
|
+
|
2023-03-08 16:06:29 +01:00
|
|
|
+/* Source for capturing applciation audio using PipeWire */
|
2023-02-23 21:25:12 +01:00
|
|
|
+
|
|
|
|
+struct target_node_port {
|
|
|
|
+ const char *channel;
|
|
|
|
+ uint32_t id;
|
|
|
|
+
|
|
|
|
+ struct obs_pw_audio_proxied_object obj;
|
|
|
|
+};
|
|
|
|
+
|
|
|
|
+struct target_node {
|
|
|
|
+ const char *name;
|
|
|
|
+ const char *app_name;
|
|
|
|
+ const char *binary;
|
|
|
|
+ uint32_t id;
|
|
|
|
+ struct spa_list ports;
|
2023-03-08 16:06:29 +01:00
|
|
|
+ uint32_t *p_n_targets;
|
2023-02-23 21:25:12 +01:00
|
|
|
+
|
|
|
|
+ struct spa_hook node_listener;
|
|
|
|
+
|
|
|
|
+ struct obs_pw_audio_proxied_object obj;
|
|
|
|
+};
|
|
|
|
+
|
|
|
|
+struct system_sink {
|
|
|
|
+ const char *name;
|
|
|
|
+ uint32_t id;
|
|
|
|
+
|
|
|
|
+ struct obs_pw_audio_proxied_object obj;
|
|
|
|
+};
|
|
|
|
+
|
|
|
|
+struct capture_sink_link {
|
|
|
|
+ uint32_t id;
|
|
|
|
+
|
|
|
|
+ struct obs_pw_audio_proxied_object obj;
|
|
|
|
+};
|
|
|
|
+
|
|
|
|
+struct capture_sink_port {
|
|
|
|
+ const char *channel;
|
|
|
|
+ uint32_t id;
|
|
|
|
+};
|
|
|
|
+
|
2023-03-08 16:06:29 +01:00
|
|
|
+/** This source basically works like this:
|
|
|
|
+ - Keep track of output streams and their ports, system sinks and the default sink
|
|
|
|
+
|
|
|
|
+ - Keep track of the channels of the default system sink and create a new virtual sink,
|
|
|
|
+ destroying the previously made one, with the same channels, then connect the stream to it
|
|
|
|
+
|
|
|
|
+ - Connect any registered or new stream ports to the sink
|
|
|
|
+*/
|
2023-02-23 21:25:12 +01:00
|
|
|
+struct obs_pw_audio_capture_app {
|
|
|
|
+ struct obs_pw_audio_instance pw;
|
|
|
|
+
|
|
|
|
+ /** The app capture sink automatically mixes
|
|
|
|
+ * the audio of all the app streams */
|
|
|
|
+ struct {
|
|
|
|
+ struct pw_proxy *proxy;
|
|
|
|
+ struct spa_hook proxy_listener;
|
|
|
|
+ bool autoconnect_targets;
|
|
|
|
+ uint32_t id;
|
|
|
|
+ uint32_t channels;
|
|
|
|
+ struct dstr position;
|
|
|
|
+ DARRAY(struct capture_sink_port) ports;
|
|
|
|
+
|
2023-03-08 16:06:29 +01:00
|
|
|
+ /* Links between app streams and the capture sink */
|
2023-02-23 21:25:12 +01:00
|
|
|
+ struct spa_list links;
|
|
|
|
+ } sink;
|
|
|
|
+
|
|
|
|
+ /** Need the default system sink to create
|
|
|
|
+ * the app capture sink with the same audio channels */
|
|
|
|
+ struct spa_list system_sinks;
|
|
|
|
+ struct {
|
|
|
|
+ struct obs_pw_audio_default_node_metadata metadata;
|
2023-03-08 16:06:29 +01:00
|
|
|
+ struct pw_proxy *proxy;
|
|
|
|
+ struct spa_hook node_listener;
|
|
|
|
+ struct spa_hook proxy_listener;
|
|
|
|
+ } default_sink;
|
2023-02-23 21:25:12 +01:00
|
|
|
+
|
|
|
|
+ struct spa_list targets;
|
2023-03-08 16:06:29 +01:00
|
|
|
+ uint32_t n_targets;
|
2023-02-23 21:25:12 +01:00
|
|
|
+
|
|
|
|
+ struct dstr target;
|
|
|
|
+ bool except_app;
|
|
|
|
+};
|
|
|
|
+
|
2023-03-08 16:06:29 +01:00
|
|
|
+/* System sinks */
|
2023-02-23 21:25:12 +01:00
|
|
|
+static void system_sink_destroy_cb(void *data)
|
|
|
|
+{
|
|
|
|
+ struct system_sink *s = data;
|
|
|
|
+ bfree((void *)s->name);
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+static void register_system_sink(struct obs_pw_audio_capture_app *pwac,
|
|
|
|
+ uint32_t global_id, const char *name)
|
|
|
|
+{
|
|
|
|
+ struct pw_proxy *sink_proxy =
|
|
|
|
+ pw_registry_bind(pwac->pw.registry, global_id,
|
|
|
|
+ PW_TYPE_INTERFACE_Node, PW_VERSION_NODE, 0);
|
|
|
|
+ if (!sink_proxy) {
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ struct system_sink *sink = bmalloc(sizeof(struct system_sink));
|
|
|
|
+ sink->name = bstrdup(name);
|
|
|
|
+ sink->id = global_id;
|
|
|
|
+
|
|
|
|
+ obs_pw_audio_proxied_object_init(&sink->obj, sink_proxy,
|
|
|
|
+ &pwac->system_sinks, NULL,
|
|
|
|
+ system_sink_destroy_cb, sink);
|
|
|
|
+}
|
|
|
|
+/* ------------------------------------------------- */
|
|
|
|
+
|
2023-03-08 16:06:29 +01:00
|
|
|
+/* Target nodes and ports */
|
2023-02-23 21:25:12 +01:00
|
|
|
+static void port_destroy_cb(void *data)
|
|
|
|
+{
|
|
|
|
+ struct target_node_port *p = data;
|
|
|
|
+ bfree((void *)p->channel);
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+static void node_destroy_cb(void *data)
|
|
|
|
+{
|
|
|
|
+ struct target_node *node = data;
|
|
|
|
+
|
|
|
|
+ spa_hook_remove(&node->node_listener);
|
|
|
|
+
|
|
|
|
+ struct target_node_port *p, *tp;
|
|
|
|
+ spa_list_for_each_safe(p, tp, &node->ports, obj.link)
|
|
|
|
+ {
|
|
|
|
+ pw_proxy_destroy(p->obj.proxy);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ (*node->p_n_targets)--;
|
|
|
|
+
|
|
|
|
+ bfree((void *)node->binary);
|
|
|
|
+ bfree((void *)node->app_name);
|
|
|
|
+ bfree((void *)node->name);
|
|
|
|
+}
|
|
|
|
+
|
2023-03-08 16:06:29 +01:00
|
|
|
+static struct target_node_port *node_register_port(struct target_node *node,
|
|
|
|
+ uint32_t global_id,
|
|
|
|
+ struct pw_registry *registry,
|
|
|
|
+ const char *channel)
|
2023-02-23 21:25:12 +01:00
|
|
|
+{
|
2023-03-08 16:06:29 +01:00
|
|
|
+ struct pw_proxy *port_proxy = pw_registry_bind(registry, global_id,
|
|
|
|
+ PW_TYPE_INTERFACE_Port,
|
|
|
|
+ PW_VERSION_PORT, 0);
|
2023-02-23 21:25:12 +01:00
|
|
|
+ if (!port_proxy) {
|
|
|
|
+ return NULL;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ struct target_node_port *port =
|
|
|
|
+ bmalloc(sizeof(struct target_node_port));
|
|
|
|
+ port->channel = bstrdup(channel);
|
|
|
|
+ port->id = global_id;
|
|
|
|
+
|
|
|
|
+ obs_pw_audio_proxied_object_init(&port->obj, port_proxy, &node->ports,
|
|
|
|
+ NULL, port_destroy_cb, port);
|
|
|
|
+
|
|
|
|
+ return port;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+static void on_node_info_cb(void *data, const struct pw_node_info *info)
|
|
|
|
+{
|
|
|
|
+ if ((info->change_mask & PW_NODE_CHANGE_MASK_PROPS) == 0 ||
|
|
|
|
+ !info->props || !info->props->n_items) {
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ const char *binary =
|
|
|
|
+ spa_dict_lookup(info->props, PW_KEY_APP_PROCESS_BINARY);
|
|
|
|
+ if (!binary) {
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ struct target_node *node = data;
|
|
|
|
+ bfree((void *)node->binary);
|
|
|
|
+ node->binary = bstrdup(binary);
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+static const struct pw_node_events node_events = {
|
|
|
|
+ PW_VERSION_NODE_EVENTS,
|
|
|
|
+ .info = on_node_info_cb,
|
|
|
|
+};
|
|
|
|
+
|
|
|
|
+static void register_target_node(struct obs_pw_audio_capture_app *pwac,
|
|
|
|
+ uint32_t global_id, const char *app_name,
|
|
|
|
+ const char *name)
|
|
|
|
+{
|
|
|
|
+ struct pw_proxy *node_proxy =
|
|
|
|
+ pw_registry_bind(pwac->pw.registry, global_id,
|
|
|
|
+ PW_TYPE_INTERFACE_Node, PW_VERSION_NODE, 0);
|
|
|
|
+ if (!node_proxy) {
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ struct target_node *node = bmalloc(sizeof(struct target_node));
|
|
|
|
+ node->name = bstrdup(name);
|
|
|
|
+ node->app_name = bstrdup(app_name);
|
|
|
|
+ node->binary = NULL;
|
|
|
|
+ node->id = global_id;
|
|
|
|
+ node->p_n_targets = &pwac->n_targets;
|
|
|
|
+ spa_list_init(&node->ports);
|
|
|
|
+
|
|
|
|
+ pwac->n_targets++;
|
|
|
|
+
|
|
|
|
+ obs_pw_audio_proxied_object_init(&node->obj, node_proxy, &pwac->targets,
|
|
|
|
+ NULL, node_destroy_cb, node);
|
|
|
|
+ pw_proxy_add_object_listener(node_proxy, &node->node_listener,
|
|
|
|
+ &node_events, node);
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+static bool node_is_targeted(struct obs_pw_audio_capture_app *pwac,
|
|
|
|
+ struct target_node *node)
|
|
|
|
+{
|
|
|
|
+ if (dstr_is_empty(&pwac->target)) {
|
|
|
|
+ return false;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return (dstr_cmpi(&pwac->target, node->binary) == 0 ||
|
|
|
|
+ dstr_cmpi(&pwac->target, node->app_name) == 0 ||
|
|
|
|
+ dstr_cmpi(&pwac->target, node->name) == 0) ^
|
|
|
|
+ pwac->except_app;
|
|
|
|
+}
|
|
|
|
+/* ------------------------------------------------- */
|
|
|
|
+
|
2023-03-08 16:06:29 +01:00
|
|
|
+/* App streams <-> Capture sink links */
|
2023-02-23 21:25:12 +01:00
|
|
|
+static void link_bound_cb(void *data, uint32_t global_id)
|
|
|
|
+{
|
|
|
|
+ struct capture_sink_link *link = data;
|
|
|
|
+ link->id = global_id;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+static void link_destroy_cb(void *data)
|
|
|
|
+{
|
|
|
|
+ struct capture_sink_link *link = data;
|
|
|
|
+ blog(LOG_DEBUG, "[pipewire] Link %u destroyed", link->id);
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+static void link_port_to_sink(struct obs_pw_audio_capture_app *pwac,
|
|
|
|
+ struct target_node_port *port, uint32_t node_id)
|
|
|
|
+{
|
|
|
|
+ blog(LOG_DEBUG,
|
|
|
|
+ "[pipewire] Connecting port %u of node %u to app capture sink",
|
|
|
|
+ port->id, node_id);
|
|
|
|
+
|
|
|
|
+ uint32_t p = 0;
|
2023-03-08 16:06:29 +01:00
|
|
|
+ if (pwac->sink.channels == 1 && /* Mono capture sink */
|
2023-02-23 21:25:12 +01:00
|
|
|
+ pwac->sink.ports.num >= 1) {
|
|
|
|
+ p = pwac->sink.ports.array[0].id;
|
|
|
|
+ } else {
|
|
|
|
+ for (size_t i = 0; i < pwac->sink.ports.num; i++) {
|
|
|
|
+ if (astrcmpi(pwac->sink.ports.array[i].channel,
|
|
|
|
+ port->channel) == 0) {
|
|
|
|
+ p = pwac->sink.ports.array[i].id;
|
|
|
|
+ break;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if (!p) {
|
|
|
|
+ blog(LOG_WARNING,
|
|
|
|
+ "[pipewire] Could not connect port %u of node %u to app capture sink. No port of app capture sink has channel %s",
|
|
|
|
+ port->id, node_id, port->channel);
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ struct pw_properties *link_props =
|
|
|
|
+ pw_properties_new(PW_KEY_OBJECT_LINGER, "false",
|
|
|
|
+ PW_KEY_FACTORY_NAME, "link-factory", NULL);
|
|
|
|
+
|
|
|
|
+ pw_properties_setf(link_props, PW_KEY_LINK_OUTPUT_NODE, "%u", node_id);
|
|
|
|
+ pw_properties_setf(link_props, PW_KEY_LINK_OUTPUT_PORT, "%u", port->id);
|
|
|
|
+
|
|
|
|
+ pw_properties_setf(link_props, PW_KEY_LINK_INPUT_NODE, "%u",
|
|
|
|
+ pwac->sink.id);
|
|
|
|
+ pw_properties_setf(link_props, PW_KEY_LINK_INPUT_PORT, "%u", p);
|
|
|
|
+
|
|
|
|
+ struct pw_proxy *link_proxy = pw_core_create_object(
|
|
|
|
+ pwac->pw.core, "link-factory", PW_TYPE_INTERFACE_Link,
|
|
|
|
+ PW_VERSION_LINK, &link_props->dict, 0);
|
|
|
|
+
|
2023-03-08 16:06:29 +01:00
|
|
|
+ obs_pw_audio_instance_sync(&pwac->pw);
|
|
|
|
+
|
2023-02-23 21:25:12 +01:00
|
|
|
+ pw_properties_free(link_props);
|
|
|
|
+
|
|
|
|
+ if (!link_proxy) {
|
|
|
|
+ blog(LOG_WARNING,
|
|
|
|
+ "[pipewire] Could not connect port %u of node %u to app capture sink",
|
|
|
|
+ port->id, node_id);
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ struct capture_sink_link *link =
|
|
|
|
+ bmalloc(sizeof(struct capture_sink_link));
|
|
|
|
+ link->id = SPA_ID_INVALID;
|
|
|
|
+
|
|
|
|
+ obs_pw_audio_proxied_object_init(&link->obj, link_proxy,
|
|
|
|
+ &pwac->sink.links, link_bound_cb,
|
|
|
|
+ link_destroy_cb, link);
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+static void link_node_to_sink(struct obs_pw_audio_capture_app *pwac,
|
|
|
|
+ struct target_node *node)
|
|
|
|
+{
|
|
|
|
+ struct target_node_port *p;
|
|
|
|
+ spa_list_for_each(p, &node->ports, obj.link)
|
|
|
|
+ {
|
|
|
|
+ link_port_to_sink(pwac, p, node->id);
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+/* ------------------------------------------------- */
|
|
|
|
+
|
2023-03-08 16:06:29 +01:00
|
|
|
+/* App capture sink */
|
2023-02-23 21:25:12 +01:00
|
|
|
+
|
|
|
|
+/** The app capture sink is created when there
|
|
|
|
+ * is info about the system's default sink.
|
|
|
|
+ * See the on_metadata and on_default_sink callbacks */
|
|
|
|
+
|
|
|
|
+static void on_sink_proxy_bound_cb(void *data, uint32_t global_id)
|
|
|
|
+{
|
|
|
|
+ struct obs_pw_audio_capture_app *pwac = data;
|
|
|
|
+ pwac->sink.id = global_id;
|
|
|
|
+ da_init(pwac->sink.ports);
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+static void on_sink_proxy_removed_cb(void *data)
|
|
|
|
+{
|
|
|
|
+ struct obs_pw_audio_capture_app *pwac = data;
|
|
|
|
+ blog(LOG_WARNING,
|
|
|
|
+ "[pipewire] App capture sink %u has been destroyed by the PipeWire remote",
|
|
|
|
+ pwac->sink.id);
|
|
|
|
+ pw_proxy_destroy(pwac->sink.proxy);
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+static void on_sink_proxy_destroy_cb(void *data)
|
|
|
|
+{
|
|
|
|
+ struct obs_pw_audio_capture_app *pwac = data;
|
|
|
|
+
|
|
|
|
+ spa_hook_remove(&pwac->sink.proxy_listener);
|
|
|
|
+ spa_zero(pwac->sink.proxy_listener);
|
|
|
|
+
|
|
|
|
+ for (size_t i = 0; i < pwac->sink.ports.num; i++) {
|
|
|
|
+ struct capture_sink_port *p = &pwac->sink.ports.array[i];
|
|
|
|
+ bfree((void *)p->channel);
|
|
|
|
+ }
|
|
|
|
+ da_free(pwac->sink.ports);
|
|
|
|
+
|
|
|
|
+ pwac->sink.channels = 0;
|
|
|
|
+ dstr_free(&pwac->sink.position);
|
|
|
|
+
|
|
|
|
+ pwac->sink.autoconnect_targets = false;
|
|
|
|
+ pwac->sink.proxy = NULL;
|
|
|
|
+
|
|
|
|
+ blog(LOG_DEBUG, "[pipewire] App capture sink %u destroyed",
|
|
|
|
+ pwac->sink.id);
|
|
|
|
+
|
|
|
|
+ pwac->sink.id = SPA_ID_INVALID;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+static void on_sink_proxy_error_cb(void *data, int seq, int res,
|
|
|
|
+ const char *message)
|
|
|
|
+{
|
|
|
|
+ UNUSED_PARAMETER(data);
|
|
|
|
+ blog(LOG_ERROR, "[pipewire] App capture sink error: seq:%d res:%d :%s",
|
|
|
|
+ seq, res, message);
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+static const struct pw_proxy_events sink_proxy_events = {
|
|
|
|
+ PW_VERSION_PROXY_EVENTS,
|
|
|
|
+ .bound = on_sink_proxy_bound_cb,
|
|
|
|
+ .removed = on_sink_proxy_removed_cb,
|
|
|
|
+ .destroy = on_sink_proxy_destroy_cb,
|
|
|
|
+ .error = on_sink_proxy_error_cb,
|
|
|
|
+};
|
|
|
|
+
|
|
|
|
+static void register_capture_sink_port(struct obs_pw_audio_capture_app *pwac,
|
|
|
|
+ uint32_t global_id, const char *channel)
|
|
|
|
+{
|
|
|
|
+ struct capture_sink_port *port = da_push_back_new(pwac->sink.ports);
|
|
|
|
+ port->channel = bstrdup(channel);
|
|
|
|
+ port->id = global_id;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+static void destroy_sink_links(struct obs_pw_audio_capture_app *pwac)
|
|
|
|
+{
|
|
|
|
+ struct capture_sink_link *l, *t;
|
|
|
|
+ spa_list_for_each_safe(l, t, &pwac->sink.links, obj.link)
|
|
|
|
+ {
|
|
|
|
+ pw_proxy_destroy(l->obj.proxy);
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+static void connect_targets(struct obs_pw_audio_capture_app *pwac,
|
|
|
|
+ const char *target, bool except)
|
|
|
|
+{
|
|
|
|
+ pwac->except_app = except;
|
|
|
|
+
|
2023-03-08 16:06:29 +01:00
|
|
|
+ if (target) {
|
2023-02-23 21:25:12 +01:00
|
|
|
+ dstr_copy(&pwac->target, target);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if (!pwac->sink.proxy) {
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ destroy_sink_links(pwac);
|
|
|
|
+
|
|
|
|
+ if (dstr_is_empty(&pwac->target)) {
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ struct target_node *n;
|
|
|
|
+ spa_list_for_each(n, &pwac->targets, obj.link)
|
|
|
|
+ {
|
|
|
|
+ if (node_is_targeted(pwac, n)) {
|
|
|
|
+ link_node_to_sink(pwac, n);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+static bool make_capture_sink(struct obs_pw_audio_capture_app *pwac,
|
|
|
|
+ uint32_t channels, const char *position)
|
|
|
|
+{
|
|
|
|
+ struct pw_properties *sink_props = pw_properties_new(
|
|
|
|
+ PW_KEY_NODE_NAME, "OBS Studio", PW_KEY_NODE_DESCRIPTION,
|
|
|
|
+ "OBS App Audio Capture Sink", PW_KEY_FACTORY_NAME,
|
|
|
|
+ "support.null-audio-sink", PW_KEY_MEDIA_CLASS,
|
2023-03-08 16:06:29 +01:00
|
|
|
+ "Audio/Sink/Virtual", PW_KEY_NODE_VIRTUAL, "true",
|
2023-03-04 18:33:16 +01:00
|
|
|
+ SPA_KEY_AUDIO_POSITION, position, NULL);
|
2023-02-23 21:25:12 +01:00
|
|
|
+
|
|
|
|
+ pw_properties_setf(sink_props, PW_KEY_AUDIO_CHANNELS, "%u", channels);
|
|
|
|
+
|
|
|
|
+ pwac->sink.proxy = pw_core_create_object(pwac->pw.core, "adapter",
|
|
|
|
+ PW_TYPE_INTERFACE_Node,
|
|
|
|
+ PW_VERSION_NODE,
|
|
|
|
+ &sink_props->dict, 0);
|
|
|
|
+
|
2023-03-08 16:06:29 +01:00
|
|
|
+ obs_pw_audio_instance_sync(&pwac->pw);
|
|
|
|
+
|
2023-02-23 21:25:12 +01:00
|
|
|
+ pw_properties_free(sink_props);
|
|
|
|
+
|
|
|
|
+ if (!pwac->sink.proxy) {
|
|
|
|
+ blog(LOG_WARNING,
|
|
|
|
+ "[pipewire] Failed to create app capture sink");
|
|
|
|
+ return false;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ pwac->sink.channels = channels;
|
|
|
|
+ dstr_copy(&pwac->sink.position, position);
|
|
|
|
+
|
|
|
|
+ pwac->sink.id = SPA_ID_INVALID;
|
|
|
|
+
|
|
|
|
+ pw_proxy_add_listener(pwac->sink.proxy, &pwac->sink.proxy_listener,
|
|
|
|
+ &sink_proxy_events, pwac);
|
|
|
|
+
|
|
|
|
+ while (pwac->sink.id == SPA_ID_INVALID ||
|
|
|
|
+ pwac->sink.ports.num != channels) {
|
2023-03-08 16:06:29 +01:00
|
|
|
+ /* Iterate until the sink is bound and all the ports are registered */
|
2023-02-23 21:25:12 +01:00
|
|
|
+ pw_loop_iterate(pw_thread_loop_get_loop(pwac->pw.thread_loop),
|
|
|
|
+ -1);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ blog(LOG_INFO,
|
|
|
|
+ "[pipewire] Created app capture sink %u with %u channels and position %s",
|
|
|
|
+ pwac->sink.id, channels, position);
|
|
|
|
+
|
|
|
|
+ connect_targets(pwac, NULL, pwac->except_app);
|
|
|
|
+
|
|
|
|
+ pwac->sink.autoconnect_targets = true;
|
|
|
|
+
|
2023-03-08 16:06:29 +01:00
|
|
|
+ if (obs_pw_audio_stream_connect(&pwac->pw.audio, pwac->sink.id,
|
|
|
|
+ channels) < 0) {
|
2023-02-23 21:25:12 +01:00
|
|
|
+ blog(LOG_WARNING,
|
|
|
|
+ "[pipewire] Error connecting stream %p to app capture sink %u",
|
2023-03-08 16:06:29 +01:00
|
|
|
+ pwac->pw.audio.stream, pwac->sink.id);
|
2023-02-23 21:25:12 +01:00
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return true;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+static void destroy_capture_sink(struct obs_pw_audio_capture_app *pwac)
|
|
|
|
+{
|
2023-03-08 16:06:29 +01:00
|
|
|
+ /* Links are automatically destroyed by PipeWire */
|
2023-02-23 21:25:12 +01:00
|
|
|
+
|
|
|
|
+ if (!pwac->sink.proxy) {
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
2023-03-08 16:06:29 +01:00
|
|
|
+ if (pw_stream_get_state(pwac->pw.audio.stream, NULL) !=
|
|
|
|
+ PW_STREAM_STATE_UNCONNECTED) {
|
|
|
|
+ pw_stream_disconnect(pwac->pw.audio.stream);
|
2023-02-23 21:25:12 +01:00
|
|
|
+ }
|
|
|
|
+
|
2023-03-08 16:06:29 +01:00
|
|
|
+ pwac->sink.autoconnect_targets = false;
|
2023-02-23 21:25:12 +01:00
|
|
|
+ pw_proxy_destroy(pwac->sink.proxy);
|
|
|
|
+ obs_pw_audio_instance_sync(&pwac->pw);
|
|
|
|
+}
|
|
|
|
+/* ------------------------------------------------- */
|
|
|
|
+
|
|
|
|
+/* Default system sink */
|
|
|
|
+static void on_default_sink_info_cb(void *data, const struct pw_node_info *info)
|
|
|
|
+{
|
|
|
|
+ if ((info->change_mask & PW_NODE_CHANGE_MASK_PROPS) == 0 ||
|
|
|
|
+ !info->props || !info->props->n_items) {
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
2023-03-08 16:06:29 +01:00
|
|
|
+ struct obs_pw_audio_capture_app *pwac = data;
|
|
|
|
+
|
2023-02-23 21:25:12 +01:00
|
|
|
+ /** Use stereo if
|
|
|
|
+ * - The default sink uses the Pro Audio profile, since all streams will be configured to use stereo
|
|
|
|
+ * https://gitlab.freedesktop.org/pipewire/pipewire/-/wikis/FAQ#what-is-the-pro-audio-profile
|
|
|
|
+ * - The default sink doesn't have the needed props and there isn't already an app capture sink */
|
|
|
|
+
|
|
|
|
+ const char *channels =
|
|
|
|
+ spa_dict_lookup(info->props, PW_KEY_AUDIO_CHANNELS);
|
|
|
|
+ const char *position =
|
|
|
|
+ spa_dict_lookup(info->props, SPA_KEY_AUDIO_POSITION);
|
|
|
|
+ if (!channels || !position) {
|
|
|
|
+ if (pwac->sink.proxy) {
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+ channels = "2";
|
|
|
|
+ position = "FL,FR";
|
|
|
|
+ } else if (astrstri(position, "AUX")) {
|
2023-03-08 16:06:29 +01:00
|
|
|
+ /* Pro Audio sinks use AUX0,AUX1... and so on as their position (see link above) */
|
2023-02-23 21:25:12 +01:00
|
|
|
+ channels = "2";
|
|
|
|
+ position = "FL,FR";
|
|
|
|
+ }
|
|
|
|
+
|
2023-03-08 16:06:29 +01:00
|
|
|
+ uint32_t c = strtoul(channels, NULL, 10);
|
2023-02-23 21:25:12 +01:00
|
|
|
+ if (!c) {
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
2023-03-08 16:06:29 +01:00
|
|
|
+ /* No need to create a new capture sink if the channels are the same */
|
2023-02-23 21:25:12 +01:00
|
|
|
+ if (pwac->sink.channels == c && !dstr_is_empty(&pwac->sink.position) &&
|
|
|
|
+ dstr_cmp(&pwac->sink.position, position) == 0) {
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ destroy_capture_sink(pwac);
|
|
|
|
+
|
|
|
|
+ make_capture_sink(pwac, c, position);
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+static const struct pw_node_events default_sink_events = {
|
|
|
|
+ PW_VERSION_NODE_EVENTS,
|
|
|
|
+ .info = on_default_sink_info_cb,
|
|
|
|
+};
|
|
|
|
+
|
|
|
|
+static void on_default_sink_proxy_removed_cb(void *data)
|
|
|
|
+{
|
|
|
|
+ struct obs_pw_audio_capture_app *pwac = data;
|
2023-03-08 16:06:29 +01:00
|
|
|
+ pw_proxy_destroy(pwac->default_sink.proxy);
|
2023-02-23 21:25:12 +01:00
|
|
|
+}
|
|
|
|
+
|
|
|
|
+static void on_default_sink_proxy_destroy_cb(void *data)
|
|
|
|
+{
|
|
|
|
+ struct obs_pw_audio_capture_app *pwac = data;
|
2023-03-08 16:06:29 +01:00
|
|
|
+ spa_hook_remove(&pwac->default_sink.node_listener);
|
|
|
|
+ spa_zero(pwac->default_sink.node_listener);
|
|
|
|
+
|
|
|
|
+ spa_hook_remove(&pwac->default_sink.proxy_listener);
|
|
|
|
+ spa_zero(pwac->default_sink.proxy_listener);
|
2023-02-23 21:25:12 +01:00
|
|
|
+
|
2023-03-08 16:06:29 +01:00
|
|
|
+ pwac->default_sink.proxy = NULL;
|
2023-02-23 21:25:12 +01:00
|
|
|
+}
|
|
|
|
+
|
|
|
|
+static const struct pw_proxy_events default_sink_proxy_events = {
|
|
|
|
+ PW_VERSION_PROXY_EVENTS,
|
|
|
|
+ .removed = on_default_sink_proxy_removed_cb,
|
|
|
|
+ .destroy = on_default_sink_proxy_destroy_cb,
|
|
|
|
+};
|
|
|
|
+
|
|
|
|
+static void default_node_cb(void *data, const char *name)
|
|
|
|
+{
|
|
|
|
+ struct obs_pw_audio_capture_app *pwac = data;
|
|
|
|
+
|
|
|
|
+ blog(LOG_DEBUG, "[pipewire] New default sink %s", name);
|
|
|
|
+
|
2023-03-08 16:06:29 +01:00
|
|
|
+ /* Find the new default sink and bind to it to get its channel info */
|
2023-02-23 21:25:12 +01:00
|
|
|
+ struct system_sink *t, *s = NULL;
|
|
|
|
+ spa_list_for_each(t, &pwac->system_sinks, obj.link)
|
|
|
|
+ {
|
|
|
|
+ if (strcmp(name, t->name) == 0) {
|
|
|
|
+ s = t;
|
|
|
|
+ break;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ if (!s) {
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
2023-03-08 16:06:29 +01:00
|
|
|
+ if (pwac->default_sink.proxy) {
|
|
|
|
+ pw_proxy_destroy(pwac->default_sink.proxy);
|
2023-02-23 21:25:12 +01:00
|
|
|
+ }
|
|
|
|
+
|
2023-03-08 16:06:29 +01:00
|
|
|
+ pwac->default_sink.proxy = pw_registry_bind(pwac->pw.registry, s->id,
|
|
|
|
+ PW_TYPE_INTERFACE_Node,
|
|
|
|
+ PW_VERSION_NODE, 0);
|
|
|
|
+ if (!pwac->default_sink.proxy) {
|
2023-02-23 21:25:12 +01:00
|
|
|
+ if (!pwac->sink.proxy) {
|
|
|
|
+ blog(LOG_WARNING,
|
|
|
|
+ "[pipewire] Failed to get default sink info, app capture sink defaulting to stereo");
|
|
|
|
+ make_capture_sink(pwac, 2, "FL,FR");
|
|
|
|
+ }
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
2023-03-08 16:06:29 +01:00
|
|
|
+ pw_proxy_add_object_listener(pwac->default_sink.proxy,
|
|
|
|
+ &pwac->default_sink.node_listener,
|
2023-02-23 21:25:12 +01:00
|
|
|
+ &default_sink_events, pwac);
|
2023-03-08 16:06:29 +01:00
|
|
|
+ pw_proxy_add_listener(pwac->default_sink.proxy,
|
|
|
|
+ &pwac->default_sink.proxy_listener,
|
2023-02-23 21:25:12 +01:00
|
|
|
+ &default_sink_proxy_events, pwac);
|
|
|
|
+}
|
|
|
|
+/* ------------------------------------------------- */
|
|
|
|
+
|
|
|
|
+/* Registry */
|
|
|
|
+static void on_global_cb(void *data, uint32_t id, uint32_t permissions,
|
|
|
|
+ const char *type, uint32_t version,
|
|
|
|
+ const struct spa_dict *props)
|
|
|
|
+{
|
|
|
|
+ UNUSED_PARAMETER(permissions);
|
|
|
|
+ UNUSED_PARAMETER(version);
|
|
|
|
+
|
|
|
|
+ if (!props || !type) {
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ struct obs_pw_audio_capture_app *pwac = data;
|
|
|
|
+
|
|
|
|
+ if (strcmp(type, PW_TYPE_INTERFACE_Port) == 0) {
|
|
|
|
+ const char *nid, *dir, *chn;
|
|
|
|
+ if (!(nid = spa_dict_lookup(props, PW_KEY_NODE_ID)) ||
|
|
|
|
+ !(dir = spa_dict_lookup(props, PW_KEY_PORT_DIRECTION)) ||
|
|
|
|
+ !(chn = spa_dict_lookup(props, PW_KEY_AUDIO_CHANNEL))) {
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
2023-03-08 16:06:29 +01:00
|
|
|
+ uint32_t node_id = strtoul(nid, NULL, 10);
|
2023-02-23 21:25:12 +01:00
|
|
|
+
|
|
|
|
+ if (astrcmpi(dir, "in") == 0 && node_id == pwac->sink.id) {
|
|
|
|
+ register_capture_sink_port(pwac, id, chn);
|
|
|
|
+ } else if (astrcmpi(dir, "out") == 0) {
|
2023-03-08 16:06:29 +01:00
|
|
|
+ /* Possibly a target port */
|
2023-02-23 21:25:12 +01:00
|
|
|
+ struct target_node *t, *n = NULL;
|
|
|
|
+ spa_list_for_each(t, &pwac->targets, obj.link)
|
|
|
|
+ {
|
|
|
|
+ if (t->id == node_id) {
|
|
|
|
+ n = t;
|
|
|
|
+ break;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ if (!n) {
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
2023-03-08 16:06:29 +01:00
|
|
|
+ struct target_node_port *p = node_register_port(
|
|
|
|
+ n, id, pwac->pw.registry, chn);
|
2023-02-23 21:25:12 +01:00
|
|
|
+
|
|
|
|
+ if (p && pwac->sink.autoconnect_targets &&
|
|
|
|
+ node_is_targeted(pwac, n)) {
|
|
|
|
+ link_port_to_sink(pwac, p, n->id);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ } else if (strcmp(type, PW_TYPE_INTERFACE_Node) == 0) {
|
|
|
|
+ const char *node_name, *media_class;
|
|
|
|
+ if (!(node_name = spa_dict_lookup(props, PW_KEY_NODE_NAME)) ||
|
|
|
|
+ !(media_class =
|
|
|
|
+ spa_dict_lookup(props, PW_KEY_MEDIA_CLASS))) {
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if (strcmp(media_class, "Stream/Output/Audio") == 0) {
|
2023-03-08 16:06:29 +01:00
|
|
|
+ /* Target node */
|
2023-02-23 21:25:12 +01:00
|
|
|
+ const char *node_app_name =
|
|
|
|
+ spa_dict_lookup(props, PW_KEY_APP_NAME);
|
|
|
|
+
|
|
|
|
+ if (!node_app_name) {
|
|
|
|
+ node_app_name = node_name;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ register_target_node(pwac, id, node_app_name,
|
|
|
|
+ node_name);
|
|
|
|
+ } else if (strcmp(media_class, "Audio/Sink") == 0) {
|
|
|
|
+ register_system_sink(pwac, id, node_name);
|
|
|
|
+ }
|
|
|
|
+ } else if (strcmp(type, PW_TYPE_INTERFACE_Metadata) == 0) {
|
|
|
|
+ const char *name = spa_dict_lookup(props, PW_KEY_METADATA_NAME);
|
|
|
|
+ if (!name || strcmp(name, "default") != 0) {
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if (!obs_pw_audio_default_node_metadata_listen(
|
2023-03-08 16:06:29 +01:00
|
|
|
+ &pwac->default_sink.metadata, &pwac->pw, id, true,
|
2023-02-23 21:25:12 +01:00
|
|
|
+ default_node_cb, pwac) &&
|
|
|
|
+ !pwac->sink.proxy) {
|
|
|
|
+ blog(LOG_WARNING,
|
|
|
|
+ "[pipewire] Failed to get default metadata, app capture sink defaulting to stereo");
|
|
|
|
+ make_capture_sink(pwac, 2, "FL,FR");
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+static const struct pw_registry_events registry_events = {
|
|
|
|
+ PW_VERSION_REGISTRY_EVENTS,
|
|
|
|
+ .global = on_global_cb,
|
|
|
|
+};
|
|
|
|
+/* ------------------------------------------------- */
|
|
|
|
+
|
|
|
|
+/* Source */
|
|
|
|
+static void *pipewire_audio_capture_app_create(obs_data_t *settings,
|
|
|
|
+ obs_source_t *source)
|
|
|
|
+{
|
|
|
|
+ struct obs_pw_audio_capture_app *pwac =
|
|
|
|
+ bzalloc(sizeof(struct obs_pw_audio_capture_app));
|
|
|
|
+
|
2023-03-08 16:06:29 +01:00
|
|
|
+ if (!obs_pw_audio_instance_init(&pwac->pw, ®istry_events, pwac, true,
|
|
|
|
+ false, source)) {
|
2023-02-23 21:25:12 +01:00
|
|
|
+ obs_pw_audio_instance_destroy(&pwac->pw);
|
|
|
|
+
|
|
|
|
+ bfree(pwac);
|
|
|
|
+ return NULL;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ spa_list_init(&pwac->targets);
|
|
|
|
+ spa_list_init(&pwac->sink.links);
|
|
|
|
+ spa_list_init(&pwac->system_sinks);
|
|
|
|
+
|
|
|
|
+ pwac->sink.id = SPA_ID_INVALID;
|
|
|
|
+ dstr_init(&pwac->sink.position);
|
|
|
|
+
|
|
|
|
+ dstr_init_copy(&pwac->target, obs_data_get_string(settings, "Target"));
|
|
|
|
+ pwac->except_app = obs_data_get_bool(settings, "ExceptApp");
|
|
|
|
+
|
|
|
|
+ obs_pw_audio_instance_sync(&pwac->pw);
|
|
|
|
+ pw_thread_loop_wait(pwac->pw.thread_loop);
|
|
|
|
+ pw_thread_loop_unlock(pwac->pw.thread_loop);
|
|
|
|
+
|
|
|
|
+ return pwac;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+static void pipewire_audio_capture_app_defaults(obs_data_t *settings)
|
|
|
|
+{
|
|
|
|
+ obs_data_set_default_bool(settings, "ExceptApp", false);
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+static int cmp_targets(const void *a, const void *b)
|
|
|
|
+{
|
|
|
|
+ const char *a_str = *(char **)a;
|
|
|
|
+ const char *b_str = *(char **)b;
|
|
|
|
+ return strcmp(a_str, b_str);
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+static obs_properties_t *pipewire_audio_capture_app_properties(void *data)
|
|
|
|
+{
|
|
|
|
+ struct obs_pw_audio_capture_app *pwac = data;
|
|
|
|
+
|
|
|
|
+ obs_properties_t *p = obs_properties_create();
|
|
|
|
+
|
|
|
|
+ obs_property_t *prop_targets_list = obs_properties_add_list(
|
|
|
|
+ p, "Target", obs_module_text("Application"),
|
|
|
|
+ OBS_COMBO_TYPE_EDITABLE, OBS_COMBO_FORMAT_STRING);
|
|
|
|
+
|
|
|
|
+ obs_properties_add_bool(p, "ExceptApp", obs_module_text("ExceptApp"));
|
|
|
|
+
|
|
|
|
+ DARRAY(char *) targets_arr;
|
|
|
|
+ da_init(targets_arr);
|
|
|
|
+
|
|
|
|
+ pw_thread_loop_lock(pwac->pw.thread_loop);
|
|
|
|
+
|
|
|
|
+ da_reserve(targets_arr, pwac->n_targets);
|
|
|
|
+
|
|
|
|
+ struct target_node *node;
|
|
|
|
+ spa_list_for_each(node, &pwac->targets, obj.link)
|
|
|
|
+ {
|
|
|
|
+ da_push_back(targets_arr, node->binary ? &node->binary
|
|
|
|
+ : node->app_name ? &node->app_name
|
|
|
|
+ : &node->name);
|
|
|
|
+ }
|
|
|
|
+
|
2023-03-08 16:06:29 +01:00
|
|
|
+ /* Show just one entry per app */
|
2023-02-23 21:25:12 +01:00
|
|
|
+
|
|
|
|
+ qsort(targets_arr.array, targets_arr.num, sizeof(char *), cmp_targets);
|
|
|
|
+
|
|
|
|
+ for (size_t i = 0; i < targets_arr.num; i++) {
|
|
|
|
+ if (i == 0 || strcmp(targets_arr.array[i - 1],
|
|
|
|
+ targets_arr.array[i]) != 0) {
|
|
|
|
+ obs_property_list_add_string(
|
|
|
|
+ prop_targets_list, targets_arr.array[i], NULL);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ pw_thread_loop_unlock(pwac->pw.thread_loop);
|
|
|
|
+
|
|
|
|
+ da_free(targets_arr);
|
|
|
|
+
|
|
|
|
+ return p;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+static void pipewire_audio_capture_app_update(void *data, obs_data_t *settings)
|
|
|
|
+{
|
|
|
|
+ struct obs_pw_audio_capture_app *pwac = data;
|
|
|
|
+
|
|
|
|
+ bool except = obs_data_get_bool(settings, "ExceptApp");
|
|
|
|
+
|
|
|
|
+ const char *new_target = obs_data_get_string(settings, "Target");
|
|
|
|
+
|
|
|
|
+ pw_thread_loop_lock(pwac->pw.thread_loop);
|
|
|
|
+
|
|
|
|
+ if (except == pwac->except_app &&
|
|
|
|
+ dstr_cmpi(&pwac->target, new_target) == 0) {
|
|
|
|
+ pw_thread_loop_unlock(pwac->pw.thread_loop);
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ connect_targets(pwac, new_target, except);
|
|
|
|
+
|
|
|
|
+ obs_pw_audio_instance_sync(&pwac->pw);
|
|
|
|
+ pw_thread_loop_wait(pwac->pw.thread_loop);
|
|
|
|
+ pw_thread_loop_unlock(pwac->pw.thread_loop);
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+static void pipewire_audio_capture_app_show(void *data)
|
|
|
|
+{
|
|
|
|
+ struct obs_pw_audio_capture_app *pwac = data;
|
2023-03-08 16:06:29 +01:00
|
|
|
+ pw_stream_set_active(pwac->pw.audio.stream, true);
|
2023-02-23 21:25:12 +01:00
|
|
|
+}
|
|
|
|
+
|
|
|
|
+static void pipewire_audio_capture_app_hide(void *data)
|
|
|
|
+{
|
|
|
|
+ struct obs_pw_audio_capture_app *pwac = data;
|
2023-03-08 16:06:29 +01:00
|
|
|
+ pw_stream_set_active(pwac->pw.audio.stream, false);
|
2023-02-23 21:25:12 +01:00
|
|
|
+}
|
|
|
|
+
|
|
|
|
+static void pipewire_audio_capture_app_destroy(void *data)
|
|
|
|
+{
|
|
|
|
+ struct obs_pw_audio_capture_app *pwac = data;
|
|
|
|
+
|
|
|
|
+ pw_thread_loop_lock(pwac->pw.thread_loop);
|
|
|
|
+
|
|
|
|
+ struct target_node *n, *tn;
|
|
|
|
+ spa_list_for_each_safe(n, tn, &pwac->targets, obj.link)
|
|
|
|
+ {
|
|
|
|
+ pw_proxy_destroy(n->obj.proxy);
|
|
|
|
+ }
|
|
|
|
+ struct system_sink *s, *ts;
|
|
|
|
+ spa_list_for_each_safe(s, ts, &pwac->system_sinks, obj.link)
|
|
|
|
+ {
|
|
|
|
+ pw_proxy_destroy(s->obj.proxy);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ destroy_capture_sink(pwac);
|
|
|
|
+
|
2023-03-08 16:06:29 +01:00
|
|
|
+ if (pwac->default_sink.proxy) {
|
|
|
|
+ pw_proxy_destroy(pwac->default_sink.proxy);
|
2023-02-23 21:25:12 +01:00
|
|
|
+ }
|
2023-03-08 16:06:29 +01:00
|
|
|
+ if (pwac->default_sink.metadata.proxy) {
|
|
|
|
+ pw_proxy_destroy(pwac->default_sink.metadata.proxy);
|
2023-02-23 21:25:12 +01:00
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ obs_pw_audio_instance_destroy(&pwac->pw);
|
|
|
|
+
|
|
|
|
+ dstr_free(&pwac->sink.position);
|
|
|
|
+ dstr_free(&pwac->target);
|
|
|
|
+
|
|
|
|
+ bfree(pwac);
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+static const char *pipewire_audio_capture_app_name(void *data)
|
|
|
|
+{
|
|
|
|
+ UNUSED_PARAMETER(data);
|
|
|
|
+ return obs_module_text("PipeWireAudioCaptureApplication");
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+void pipewire_audio_capture_app_load(void)
|
|
|
|
+{
|
|
|
|
+ const struct obs_source_info pipewire_audio_capture_application = {
|
|
|
|
+ .id = "pipewire-audio-capture-application",
|
|
|
|
+ .type = OBS_SOURCE_TYPE_INPUT,
|
|
|
|
+ .output_flags = OBS_SOURCE_AUDIO | OBS_SOURCE_DO_NOT_DUPLICATE,
|
|
|
|
+ .get_name = pipewire_audio_capture_app_name,
|
|
|
|
+ .create = pipewire_audio_capture_app_create,
|
|
|
|
+ .get_defaults = pipewire_audio_capture_app_defaults,
|
|
|
|
+ .get_properties = pipewire_audio_capture_app_properties,
|
|
|
|
+ .update = pipewire_audio_capture_app_update,
|
|
|
|
+ .show = pipewire_audio_capture_app_show,
|
|
|
|
+ .hide = pipewire_audio_capture_app_hide,
|
|
|
|
+ .destroy = pipewire_audio_capture_app_destroy,
|
|
|
|
+ .icon_type = OBS_ICON_TYPE_PROCESS_AUDIO_OUTPUT,
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ obs_register_source(&pipewire_audio_capture_application);
|
|
|
|
+}
|
|
|
|
diff --git a/plugins/linux-pipewire/pipewire-audio-capture-device.c b/plugins/linux-pipewire/pipewire-audio-capture-device.c
|
|
|
|
new file mode 100644
|
2023-03-08 16:06:29 +01:00
|
|
|
index 0000000000000..b474ddad39fac
|
2023-02-23 21:25:12 +01:00
|
|
|
--- /dev/null
|
|
|
|
+++ b/plugins/linux-pipewire/pipewire-audio-capture-device.c
|
2023-03-08 16:06:29 +01:00
|
|
|
@@ -0,0 +1,520 @@
|
|
|
|
+/* pipewire-audio-capture-device.c
|
2023-02-23 21:25:12 +01:00
|
|
|
+ *
|
|
|
|
+ * Copyright 2022 Dimitris Papaioannou <dimtpap@protonmail.com>
|
|
|
|
+ *
|
|
|
|
+ * This program is free software: you can redistribute it and/or modify
|
|
|
|
+ * it under the terms of the GNU General Public License as published by
|
|
|
|
+ * the Free Software Foundation, either version 2 of the License, or
|
|
|
|
+ * (at your option) any later version.
|
|
|
|
+ *
|
|
|
|
+ * This program 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 General Public License for more details.
|
|
|
|
+ *
|
|
|
|
+ * You should have received a copy of the GNU General Public License
|
|
|
|
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
+ *
|
|
|
|
+ * SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
+ */
|
|
|
|
+
|
|
|
|
+#include "pipewire-audio.h"
|
|
|
|
+
|
|
|
|
+#include <util/dstr.h>
|
|
|
|
+
|
2023-03-08 16:06:29 +01:00
|
|
|
+/* Source for capturing device audio using PipeWire */
|
|
|
|
+
|
2023-02-23 21:25:12 +01:00
|
|
|
+enum obs_pw_audio_capture_device_type {
|
|
|
|
+ PIPEWIRE_AUDIO_CAPTURE_DEVICE_INPUT,
|
|
|
|
+ PIPEWIRE_AUDIO_CAPTURE_DEVICE_OUTPUT,
|
|
|
|
+};
|
|
|
|
+
|
|
|
|
+struct target_node {
|
|
|
|
+ const char *friendly_name;
|
|
|
|
+ const char *name;
|
|
|
|
+ uint32_t id;
|
|
|
|
+ uint32_t channels;
|
|
|
|
+
|
|
|
|
+ struct spa_hook node_listener;
|
|
|
|
+
|
|
|
|
+ struct obs_pw_audio_capture_device *pwac;
|
|
|
|
+
|
|
|
|
+ struct obs_pw_audio_proxied_object obj;
|
|
|
|
+};
|
|
|
|
+
|
|
|
|
+struct obs_pw_audio_capture_device {
|
|
|
|
+ obs_source_t *source;
|
|
|
|
+
|
|
|
|
+ enum obs_pw_audio_capture_device_type capture_type;
|
|
|
|
+
|
|
|
|
+ struct obs_pw_audio_instance pw;
|
|
|
|
+
|
|
|
|
+ struct {
|
|
|
|
+ struct obs_pw_audio_default_node_metadata metadata;
|
|
|
|
+ bool autoconnect;
|
|
|
|
+ uint32_t node_id;
|
|
|
|
+ struct dstr name;
|
|
|
|
+ } default_info;
|
|
|
|
+
|
|
|
|
+ struct spa_list targets;
|
|
|
|
+
|
|
|
|
+ struct dstr target_name;
|
|
|
|
+ uint32_t connected_id;
|
|
|
|
+};
|
|
|
|
+
|
|
|
|
+static void start_streaming(struct obs_pw_audio_capture_device *pwac,
|
|
|
|
+ struct target_node *node)
|
|
|
|
+{
|
2023-03-08 16:06:29 +01:00
|
|
|
+ if (!node || !node->channels) {
|
2023-02-23 21:25:12 +01:00
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ dstr_copy(&pwac->target_name, node->name);
|
|
|
|
+
|
2023-03-08 16:06:29 +01:00
|
|
|
+ if (pw_stream_get_state(pwac->pw.audio.stream, NULL) !=
|
2023-02-23 21:25:12 +01:00
|
|
|
+ PW_STREAM_STATE_UNCONNECTED) {
|
|
|
|
+ if (node->id == pwac->connected_id) {
|
2023-03-08 16:06:29 +01:00
|
|
|
+ /* Already connected to this node */
|
2023-02-23 21:25:12 +01:00
|
|
|
+ return;
|
|
|
|
+ }
|
2023-03-08 16:06:29 +01:00
|
|
|
+ pw_stream_disconnect(pwac->pw.audio.stream);
|
2023-02-23 21:25:12 +01:00
|
|
|
+ }
|
|
|
|
+
|
2023-03-08 16:06:29 +01:00
|
|
|
+ if (obs_pw_audio_stream_connect(&pwac->pw.audio, node->id,
|
|
|
|
+ node->channels) == 0) {
|
2023-02-23 21:25:12 +01:00
|
|
|
+ pwac->connected_id = node->id;
|
|
|
|
+ blog(LOG_INFO, "[pipewire] %p streaming from %u",
|
2023-03-08 16:06:29 +01:00
|
|
|
+ pwac->pw.audio.stream, node->id);
|
2023-02-23 21:25:12 +01:00
|
|
|
+ } else {
|
|
|
|
+ pwac->connected_id = SPA_ID_INVALID;
|
|
|
|
+ blog(LOG_WARNING, "[pipewire] Error connecting stream %p",
|
2023-03-08 16:06:29 +01:00
|
|
|
+ pwac->pw.audio.stream);
|
2023-02-23 21:25:12 +01:00
|
|
|
+ }
|
|
|
|
+
|
2023-03-08 16:06:29 +01:00
|
|
|
+ pw_stream_set_active(pwac->pw.audio.stream,
|
2023-02-23 21:25:12 +01:00
|
|
|
+ obs_source_active(pwac->source));
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+struct target_node *get_node_by_name(struct obs_pw_audio_capture_device *pwac,
|
|
|
|
+ const char *name)
|
|
|
|
+{
|
|
|
|
+ struct target_node *n;
|
|
|
|
+ spa_list_for_each(n, &pwac->targets, obj.link)
|
|
|
|
+ {
|
|
|
|
+ if (strcmp(n->name, name) == 0) {
|
|
|
|
+ return n;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ return NULL;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+struct target_node *get_node_by_id(struct obs_pw_audio_capture_device *pwac,
|
|
|
|
+ uint32_t id)
|
|
|
|
+{
|
|
|
|
+ struct target_node *n;
|
|
|
|
+ spa_list_for_each(n, &pwac->targets, obj.link)
|
|
|
|
+ {
|
|
|
|
+ if (n->id == id) {
|
|
|
|
+ return n;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ return NULL;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+/* Target node */
|
|
|
|
+static void on_node_info_cb(void *data, const struct pw_node_info *info)
|
|
|
|
+{
|
|
|
|
+ if ((info->change_mask & PW_NODE_CHANGE_MASK_PROPS) == 0 ||
|
|
|
|
+ !info->props || !info->props->n_items) {
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ const char *channels =
|
|
|
|
+ spa_dict_lookup(info->props, PW_KEY_AUDIO_CHANNELS);
|
|
|
|
+ if (!channels) {
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
2023-03-08 16:06:29 +01:00
|
|
|
+ uint32_t c = strtoul(channels, NULL, 10);
|
2023-02-23 21:25:12 +01:00
|
|
|
+
|
|
|
|
+ struct target_node *n = data;
|
|
|
|
+ if (n->channels == c) {
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+ n->channels = c;
|
|
|
|
+
|
|
|
|
+ struct obs_pw_audio_capture_device *pwac = n->pwac;
|
|
|
|
+
|
|
|
|
+ /** If this is the default device and the stream is not already connected to it
|
|
|
|
+ * or the stream is unconnected and this node has the desired target name */
|
|
|
|
+ if ((pwac->default_info.autoconnect && pwac->connected_id != n->id &&
|
|
|
|
+ !dstr_is_empty(&pwac->default_info.name) &&
|
|
|
|
+ dstr_cmp(&pwac->default_info.name, n->name) == 0) ||
|
2023-03-08 16:06:29 +01:00
|
|
|
+ (pw_stream_get_state(pwac->pw.audio.stream, NULL) ==
|
2023-02-23 21:25:12 +01:00
|
|
|
+ PW_STREAM_STATE_UNCONNECTED &&
|
|
|
|
+ !dstr_is_empty(&pwac->target_name) &&
|
|
|
|
+ dstr_cmp(&pwac->target_name, n->name) == 0)) {
|
|
|
|
+ start_streaming(pwac, n);
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+static const struct pw_node_events node_events = {
|
|
|
|
+ PW_VERSION_NODE_EVENTS,
|
|
|
|
+ .info = on_node_info_cb,
|
|
|
|
+};
|
|
|
|
+
|
|
|
|
+static void node_destroy_cb(void *data)
|
|
|
|
+{
|
|
|
|
+ struct target_node *n = data;
|
|
|
|
+
|
|
|
|
+ spa_hook_remove(&n->node_listener);
|
|
|
|
+
|
|
|
|
+ bfree((void *)n->friendly_name);
|
|
|
|
+ bfree((void *)n->name);
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+static void register_target_node(struct obs_pw_audio_capture_device *pwac,
|
|
|
|
+ const char *friendly_name, const char *name,
|
|
|
|
+ uint32_t global_id)
|
|
|
|
+{
|
|
|
|
+ struct pw_proxy *node_proxy =
|
|
|
|
+ pw_registry_bind(pwac->pw.registry, global_id,
|
|
|
|
+ PW_TYPE_INTERFACE_Node, PW_VERSION_NODE, 0);
|
|
|
|
+ if (!node_proxy) {
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ struct target_node *n = bmalloc(sizeof(struct target_node));
|
|
|
|
+ n->friendly_name = bstrdup(friendly_name);
|
|
|
|
+ n->name = bstrdup(name);
|
|
|
|
+ n->id = global_id;
|
|
|
|
+ n->channels = 0;
|
|
|
|
+ n->pwac = pwac;
|
|
|
|
+
|
|
|
|
+ obs_pw_audio_proxied_object_init(&n->obj, node_proxy, &pwac->targets,
|
|
|
|
+ NULL, node_destroy_cb, n);
|
|
|
|
+
|
|
|
|
+ spa_zero(n->node_listener);
|
|
|
|
+ pw_proxy_add_object_listener(n->obj.proxy, &n->node_listener,
|
|
|
|
+ &node_events, n);
|
|
|
|
+}
|
|
|
|
+/* ------------------------------------------------- */
|
|
|
|
+
|
|
|
|
+/* Default device metadata */
|
|
|
|
+static void default_node_cb(void *data, const char *name)
|
|
|
|
+{
|
|
|
|
+ struct obs_pw_audio_capture_device *pwac = data;
|
|
|
|
+
|
|
|
|
+ blog(LOG_DEBUG, "[pipewire] New default device %s", name);
|
|
|
|
+
|
|
|
|
+ dstr_copy(&pwac->default_info.name, name);
|
|
|
|
+
|
|
|
|
+ struct target_node *n = get_node_by_name(pwac, name);
|
|
|
|
+ if (n) {
|
|
|
|
+ pwac->default_info.node_id = n->id;
|
|
|
|
+ if (pwac->default_info.autoconnect) {
|
|
|
|
+ start_streaming(pwac, n);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+/* ------------------------------------------------- */
|
|
|
|
+
|
|
|
|
+/* Registry */
|
|
|
|
+static void on_global_cb(void *data, uint32_t id, uint32_t permissions,
|
|
|
|
+ const char *type, uint32_t version,
|
|
|
|
+ const struct spa_dict *props)
|
|
|
|
+{
|
|
|
|
+ UNUSED_PARAMETER(permissions);
|
|
|
|
+ UNUSED_PARAMETER(version);
|
|
|
|
+
|
|
|
|
+ struct obs_pw_audio_capture_device *pwac = data;
|
|
|
|
+
|
|
|
|
+ if (!props || !type) {
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if (strcmp(type, PW_TYPE_INTERFACE_Node) == 0) {
|
|
|
|
+ const char *node_name, *media_class;
|
|
|
|
+ if (!(node_name = spa_dict_lookup(props, PW_KEY_NODE_NAME)) ||
|
|
|
|
+ !(media_class =
|
|
|
|
+ spa_dict_lookup(props, PW_KEY_MEDIA_CLASS))) {
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
2023-03-08 16:06:29 +01:00
|
|
|
+ /* Target device */
|
2023-02-23 21:25:12 +01:00
|
|
|
+ if ((pwac->capture_type ==
|
|
|
|
+ PIPEWIRE_AUDIO_CAPTURE_DEVICE_INPUT &&
|
|
|
|
+ (strcmp(media_class, "Audio/Source") == 0 ||
|
|
|
|
+ strcmp(media_class, "Audio/Source/Virtual") == 0)) ||
|
|
|
|
+ (pwac->capture_type ==
|
|
|
|
+ PIPEWIRE_AUDIO_CAPTURE_DEVICE_OUTPUT &&
|
|
|
|
+ strcmp(media_class, "Audio/Sink") == 0)) {
|
|
|
|
+ const char *node_friendly_name =
|
|
|
|
+ spa_dict_lookup(props, PW_KEY_NODE_NICK);
|
|
|
|
+ if (!node_friendly_name) {
|
|
|
|
+ node_friendly_name = spa_dict_lookup(
|
|
|
|
+ props, PW_KEY_NODE_DESCRIPTION);
|
|
|
|
+ if (!node_friendly_name) {
|
|
|
|
+ node_friendly_name = node_name;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ register_target_node(pwac, node_friendly_name,
|
|
|
|
+ node_name, id);
|
|
|
|
+ }
|
|
|
|
+ } else if (strcmp(type, PW_TYPE_INTERFACE_Metadata) == 0) {
|
|
|
|
+ const char *name = spa_dict_lookup(props, PW_KEY_METADATA_NAME);
|
|
|
|
+ if (!name || strcmp(name, "default") != 0) {
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if (!obs_pw_audio_default_node_metadata_listen(
|
|
|
|
+ &pwac->default_info.metadata, &pwac->pw, id,
|
|
|
|
+ pwac->capture_type ==
|
|
|
|
+ PIPEWIRE_AUDIO_CAPTURE_DEVICE_OUTPUT,
|
|
|
|
+ default_node_cb, pwac)) {
|
|
|
|
+ blog(LOG_WARNING,
|
|
|
|
+ "[pipewire] Failed to get default metadata, cannot detect default audio devices");
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+static void on_global_remove_cb(void *data, uint32_t id)
|
|
|
|
+{
|
|
|
|
+ struct obs_pw_audio_capture_device *pwac = data;
|
|
|
|
+
|
|
|
|
+ if (pwac->default_info.node_id == id) {
|
|
|
|
+ pwac->default_info.node_id = SPA_ID_INVALID;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /** If the node we're connected to is removed,
|
|
|
|
+ * try to find one with the same name and connect to it. */
|
|
|
|
+ if (id == pwac->connected_id) {
|
|
|
|
+ pwac->connected_id = SPA_ID_INVALID;
|
|
|
|
+
|
2023-03-08 16:06:29 +01:00
|
|
|
+ pw_stream_disconnect(pwac->pw.audio.stream);
|
2023-02-23 21:25:12 +01:00
|
|
|
+
|
|
|
|
+ if (!pwac->default_info.autoconnect &&
|
|
|
|
+ !dstr_is_empty(&pwac->target_name)) {
|
|
|
|
+ start_streaming(pwac,
|
|
|
|
+ get_node_by_name(
|
|
|
|
+ pwac, pwac->target_name.array));
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+static const struct pw_registry_events registry_events = {
|
|
|
|
+ PW_VERSION_REGISTRY_EVENTS,
|
|
|
|
+ .global = on_global_cb,
|
|
|
|
+ .global_remove = on_global_remove_cb,
|
|
|
|
+};
|
|
|
|
+/* ------------------------------------------------- */
|
|
|
|
+
|
|
|
|
+/* Source */
|
|
|
|
+static void *pipewire_audio_capture_create(
|
|
|
|
+ obs_data_t *settings, obs_source_t *source,
|
|
|
|
+ enum obs_pw_audio_capture_device_type capture_type)
|
|
|
|
+{
|
|
|
|
+ struct obs_pw_audio_capture_device *pwac =
|
|
|
|
+ bzalloc(sizeof(struct obs_pw_audio_capture_device));
|
|
|
|
+
|
2023-03-08 16:06:29 +01:00
|
|
|
+ if (!obs_pw_audio_instance_init(
|
|
|
|
+ &pwac->pw, ®istry_events, pwac,
|
|
|
|
+ capture_type == PIPEWIRE_AUDIO_CAPTURE_DEVICE_OUTPUT, true,
|
|
|
|
+ source)) {
|
2023-02-23 21:25:12 +01:00
|
|
|
+ obs_pw_audio_instance_destroy(&pwac->pw);
|
|
|
|
+
|
|
|
|
+ bfree(pwac);
|
|
|
|
+ return NULL;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ pwac->source = source;
|
|
|
|
+ pwac->capture_type = capture_type;
|
|
|
|
+ pwac->default_info.node_id = SPA_ID_INVALID;
|
|
|
|
+ pwac->connected_id = SPA_ID_INVALID;
|
|
|
|
+
|
|
|
|
+ spa_list_init(&pwac->targets);
|
|
|
|
+
|
|
|
|
+ if (obs_data_get_int(settings, "TargetId") != PW_ID_ANY) {
|
|
|
|
+ /** Reset id setting, PipeWire node ids may not persist between sessions.
|
|
|
|
+ * Connecting to saved target will happen based on the TargetName setting
|
|
|
|
+ * once target has connected */
|
|
|
|
+ obs_data_set_int(settings, "TargetId", 0);
|
|
|
|
+ } else {
|
|
|
|
+ pwac->default_info.autoconnect = true;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ dstr_init_copy(&pwac->target_name,
|
|
|
|
+ obs_data_get_string(settings, "TargetName"));
|
|
|
|
+
|
|
|
|
+ obs_pw_audio_instance_sync(&pwac->pw);
|
|
|
|
+ pw_thread_loop_wait(pwac->pw.thread_loop);
|
|
|
|
+ pw_thread_loop_unlock(pwac->pw.thread_loop);
|
|
|
|
+
|
|
|
|
+ return pwac;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+static void *pipewire_audio_capture_input_create(obs_data_t *settings,
|
|
|
|
+ obs_source_t *source)
|
|
|
|
+{
|
|
|
|
+ return pipewire_audio_capture_create(
|
|
|
|
+ settings, source, PIPEWIRE_AUDIO_CAPTURE_DEVICE_INPUT);
|
|
|
|
+}
|
2023-03-08 16:06:29 +01:00
|
|
|
+
|
2023-02-23 21:25:12 +01:00
|
|
|
+static void *pipewire_audio_capture_output_create(obs_data_t *settings,
|
|
|
|
+ obs_source_t *source)
|
|
|
|
+{
|
|
|
|
+ return pipewire_audio_capture_create(
|
|
|
|
+ settings, source, PIPEWIRE_AUDIO_CAPTURE_DEVICE_OUTPUT);
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+static void pipewire_audio_capture_defaults(obs_data_t *settings)
|
|
|
|
+{
|
|
|
|
+ obs_data_set_default_int(settings, "TargetId", PW_ID_ANY);
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+static obs_properties_t *pipewire_audio_capture_properties(void *data)
|
|
|
|
+{
|
|
|
|
+ struct obs_pw_audio_capture_device *pwac = data;
|
|
|
|
+
|
|
|
|
+ obs_properties_t *p = obs_properties_create();
|
|
|
|
+
|
|
|
|
+ obs_property_t *prop_targets_list = obs_properties_add_list(
|
|
|
|
+ p, "TargetId", obs_module_text("Device"), OBS_COMBO_TYPE_LIST,
|
|
|
|
+ OBS_COMBO_FORMAT_INT);
|
|
|
|
+
|
|
|
|
+ obs_property_list_add_int(prop_targets_list, obs_module_text("Default"),
|
|
|
|
+ PW_ID_ANY);
|
|
|
|
+
|
|
|
|
+ pw_thread_loop_lock(pwac->pw.thread_loop);
|
|
|
|
+
|
|
|
|
+ struct target_node *n;
|
|
|
|
+ spa_list_for_each(n, &pwac->targets, obj.link)
|
|
|
|
+ {
|
|
|
|
+ obs_property_list_add_int(prop_targets_list, n->friendly_name,
|
|
|
|
+ n->id);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ pw_thread_loop_unlock(pwac->pw.thread_loop);
|
|
|
|
+
|
|
|
|
+ return p;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+static void pipewire_audio_capture_update(void *data, obs_data_t *settings)
|
|
|
|
+{
|
|
|
|
+ struct obs_pw_audio_capture_device *pwac = data;
|
|
|
|
+
|
|
|
|
+ uint32_t new_node_id = obs_data_get_int(settings, "TargetId");
|
|
|
|
+
|
|
|
|
+ pw_thread_loop_lock(pwac->pw.thread_loop);
|
|
|
|
+
|
|
|
|
+ if (new_node_id == PW_ID_ANY) {
|
|
|
|
+ pwac->default_info.autoconnect = true;
|
|
|
|
+
|
|
|
|
+ if (pwac->default_info.node_id != SPA_ID_INVALID) {
|
|
|
|
+ start_streaming(
|
|
|
|
+ pwac,
|
|
|
|
+ get_node_by_id(pwac,
|
|
|
|
+ pwac->default_info.node_id));
|
|
|
|
+ }
|
|
|
|
+ goto unlock;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ pwac->default_info.autoconnect = false;
|
|
|
|
+
|
|
|
|
+ struct target_node *new_node = get_node_by_id(pwac, new_node_id);
|
|
|
|
+ if (!new_node) {
|
|
|
|
+ goto unlock;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ start_streaming(pwac, new_node);
|
|
|
|
+
|
|
|
|
+ obs_data_set_string(settings, "TargetName", pwac->target_name.array);
|
|
|
|
+
|
|
|
|
+unlock:
|
|
|
|
+ pw_thread_loop_unlock(pwac->pw.thread_loop);
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+static void pipewire_audio_capture_show(void *data)
|
|
|
|
+{
|
|
|
|
+ struct obs_pw_audio_capture_device *pwac = data;
|
2023-03-08 16:06:29 +01:00
|
|
|
+ pw_stream_set_active(pwac->pw.audio.stream, true);
|
2023-02-23 21:25:12 +01:00
|
|
|
+}
|
|
|
|
+
|
|
|
|
+static void pipewire_audio_capture_hide(void *data)
|
|
|
|
+{
|
|
|
|
+ struct obs_pw_audio_capture_device *pwac = data;
|
2023-03-08 16:06:29 +01:00
|
|
|
+ pw_stream_set_active(pwac->pw.audio.stream, false);
|
2023-02-23 21:25:12 +01:00
|
|
|
+}
|
|
|
|
+
|
|
|
|
+static void pipewire_audio_capture_destroy(void *data)
|
|
|
|
+{
|
|
|
|
+ struct obs_pw_audio_capture_device *pwac = data;
|
|
|
|
+
|
|
|
|
+ pw_thread_loop_lock(pwac->pw.thread_loop);
|
|
|
|
+
|
|
|
|
+ struct target_node *n, *tn;
|
|
|
|
+ spa_list_for_each_safe(n, tn, &pwac->targets, obj.link)
|
|
|
|
+ {
|
|
|
|
+ pw_proxy_destroy(n->obj.proxy);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if (pwac->default_info.metadata.proxy) {
|
|
|
|
+ pw_proxy_destroy(pwac->default_info.metadata.proxy);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ obs_pw_audio_instance_destroy(&pwac->pw);
|
|
|
|
+
|
|
|
|
+ dstr_free(&pwac->default_info.name);
|
|
|
|
+ dstr_free(&pwac->target_name);
|
|
|
|
+
|
|
|
|
+ bfree(pwac);
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+static const char *pipewire_audio_capture_input_name(void *data)
|
|
|
|
+{
|
|
|
|
+ UNUSED_PARAMETER(data);
|
|
|
|
+ return obs_module_text("PipeWireAudioCaptureInput");
|
|
|
|
+}
|
2023-03-08 16:06:29 +01:00
|
|
|
+
|
2023-02-23 21:25:12 +01:00
|
|
|
+static const char *pipewire_audio_capture_output_name(void *data)
|
|
|
|
+{
|
|
|
|
+ UNUSED_PARAMETER(data);
|
|
|
|
+ return obs_module_text("PipeWireAudioCaptureOutput");
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+void pipewire_audio_capture_load(void)
|
|
|
|
+{
|
|
|
|
+ const struct obs_source_info pipewire_audio_capture_input = {
|
|
|
|
+ .id = "pipewire-audio-capture-input",
|
|
|
|
+ .type = OBS_SOURCE_TYPE_INPUT,
|
|
|
|
+ .output_flags = OBS_SOURCE_AUDIO | OBS_SOURCE_DO_NOT_DUPLICATE,
|
|
|
|
+ .get_name = pipewire_audio_capture_input_name,
|
|
|
|
+ .create = pipewire_audio_capture_input_create,
|
|
|
|
+ .get_defaults = pipewire_audio_capture_defaults,
|
|
|
|
+ .get_properties = pipewire_audio_capture_properties,
|
|
|
|
+ .update = pipewire_audio_capture_update,
|
|
|
|
+ .show = pipewire_audio_capture_show,
|
|
|
|
+ .hide = pipewire_audio_capture_hide,
|
|
|
|
+ .destroy = pipewire_audio_capture_destroy,
|
|
|
|
+ .icon_type = OBS_ICON_TYPE_AUDIO_INPUT,
|
|
|
|
+ };
|
|
|
|
+ const struct obs_source_info pipewire_audio_capture_output = {
|
|
|
|
+ .id = "pipewire-audio-capture-output",
|
|
|
|
+ .type = OBS_SOURCE_TYPE_INPUT,
|
|
|
|
+ .output_flags = OBS_SOURCE_AUDIO | OBS_SOURCE_DO_NOT_DUPLICATE |
|
|
|
|
+ OBS_SOURCE_DO_NOT_SELF_MONITOR,
|
|
|
|
+ .get_name = pipewire_audio_capture_output_name,
|
|
|
|
+ .create = pipewire_audio_capture_output_create,
|
|
|
|
+ .get_defaults = pipewire_audio_capture_defaults,
|
|
|
|
+ .get_properties = pipewire_audio_capture_properties,
|
|
|
|
+ .update = pipewire_audio_capture_update,
|
|
|
|
+ .show = pipewire_audio_capture_show,
|
|
|
|
+ .hide = pipewire_audio_capture_hide,
|
|
|
|
+ .destroy = pipewire_audio_capture_destroy,
|
|
|
|
+ .icon_type = OBS_ICON_TYPE_AUDIO_OUTPUT,
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ obs_register_source(&pipewire_audio_capture_input);
|
|
|
|
+ obs_register_source(&pipewire_audio_capture_output);
|
|
|
|
+}
|
|
|
|
diff --git a/plugins/linux-pipewire/pipewire-audio.c b/plugins/linux-pipewire/pipewire-audio.c
|
|
|
|
new file mode 100644
|
2023-03-08 16:06:29 +01:00
|
|
|
index 0000000000000..ea761221ba10f
|
2023-02-23 21:25:12 +01:00
|
|
|
--- /dev/null
|
|
|
|
+++ b/plugins/linux-pipewire/pipewire-audio.c
|
2023-03-08 16:06:29 +01:00
|
|
|
@@ -0,0 +1,574 @@
|
2023-02-23 21:25:12 +01:00
|
|
|
+/* pipewire-audio.c
|
|
|
|
+ *
|
|
|
|
+ * Copyright 2022 Dimitris Papaioannou <dimtpap@protonmail.com>
|
|
|
|
+ *
|
|
|
|
+ * This program is free software: you can redistribute it and/or modify
|
|
|
|
+ * it under the terms of the GNU General Public License as published by
|
|
|
|
+ * the Free Software Foundation, either version 2 of the License, or
|
|
|
|
+ * (at your option) any later version.
|
|
|
|
+ *
|
|
|
|
+ * This program 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 General Public License for more details.
|
|
|
|
+ *
|
|
|
|
+ * You should have received a copy of the GNU General Public License
|
|
|
|
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
+ *
|
|
|
|
+ * SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
+ */
|
|
|
|
+
|
|
|
|
+#include "pipewire-audio.h"
|
|
|
|
+
|
|
|
|
+#include <util/platform.h>
|
|
|
|
+
|
|
|
|
+#include <spa/utils/json.h>
|
|
|
|
+
|
2023-03-08 16:06:29 +01:00
|
|
|
+/* Utilities */
|
2023-02-23 21:25:12 +01:00
|
|
|
+bool json_object_find(const char *obj, const char *key, char *value, size_t len)
|
|
|
|
+{
|
2023-03-08 16:06:29 +01:00
|
|
|
+ /* From PipeWire's source */
|
2023-02-23 21:25:12 +01:00
|
|
|
+
|
|
|
|
+ struct spa_json it[2];
|
|
|
|
+ const char *v;
|
|
|
|
+ char k[128];
|
|
|
|
+
|
|
|
|
+ spa_json_init(&it[0], obj, strlen(obj));
|
|
|
|
+ if (spa_json_enter_object(&it[0], &it[1]) <= 0) {
|
|
|
|
+ return false;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ while (spa_json_get_string(&it[1], k, sizeof(k)) > 0) {
|
|
|
|
+ if (spa_streq(k, key)) {
|
|
|
|
+ if (spa_json_get_string(&it[1], value, len) > 0) {
|
|
|
|
+ return true;
|
|
|
|
+ }
|
|
|
|
+ } else if (spa_json_next(&it[1], &v) <= 0) {
|
|
|
|
+ break;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ return false;
|
|
|
|
+}
|
|
|
|
+/* ------------------------------------------------- */
|
|
|
|
+
|
|
|
|
+/* PipeWire stream wrapper */
|
2023-03-08 16:06:29 +01:00
|
|
|
+void obs_channels_to_spa_audio_position(enum spa_audio_channel *position,
|
|
|
|
+ uint32_t channels)
|
2023-02-23 21:25:12 +01:00
|
|
|
+{
|
|
|
|
+ switch (channels) {
|
|
|
|
+ case 1:
|
|
|
|
+ position[0] = SPA_AUDIO_CHANNEL_MONO;
|
|
|
|
+ break;
|
|
|
|
+ case 2:
|
|
|
|
+ position[0] = SPA_AUDIO_CHANNEL_FL;
|
|
|
|
+ position[1] = SPA_AUDIO_CHANNEL_FR;
|
|
|
|
+ break;
|
|
|
|
+ case 3:
|
|
|
|
+ position[0] = SPA_AUDIO_CHANNEL_FL;
|
|
|
|
+ position[1] = SPA_AUDIO_CHANNEL_FR;
|
|
|
|
+ position[2] = SPA_AUDIO_CHANNEL_LFE;
|
|
|
|
+ break;
|
|
|
|
+ case 4:
|
|
|
|
+ position[0] = SPA_AUDIO_CHANNEL_FL;
|
|
|
|
+ position[1] = SPA_AUDIO_CHANNEL_FR;
|
|
|
|
+ position[2] = SPA_AUDIO_CHANNEL_FC;
|
|
|
|
+ position[3] = SPA_AUDIO_CHANNEL_RC;
|
|
|
|
+ break;
|
|
|
|
+ case 5:
|
|
|
|
+ position[0] = SPA_AUDIO_CHANNEL_FL;
|
|
|
|
+ position[1] = SPA_AUDIO_CHANNEL_FR;
|
|
|
|
+ position[2] = SPA_AUDIO_CHANNEL_FC;
|
|
|
|
+ position[3] = SPA_AUDIO_CHANNEL_LFE;
|
|
|
|
+ position[4] = SPA_AUDIO_CHANNEL_RC;
|
|
|
|
+ break;
|
|
|
|
+ case 6:
|
|
|
|
+ position[0] = SPA_AUDIO_CHANNEL_FL;
|
|
|
|
+ position[1] = SPA_AUDIO_CHANNEL_FR;
|
|
|
|
+ position[2] = SPA_AUDIO_CHANNEL_FC;
|
|
|
|
+ position[3] = SPA_AUDIO_CHANNEL_LFE;
|
|
|
|
+ position[4] = SPA_AUDIO_CHANNEL_RL;
|
|
|
|
+ position[5] = SPA_AUDIO_CHANNEL_RR;
|
|
|
|
+ break;
|
|
|
|
+ case 8:
|
|
|
|
+ position[0] = SPA_AUDIO_CHANNEL_FL;
|
|
|
|
+ position[1] = SPA_AUDIO_CHANNEL_FR;
|
|
|
|
+ position[2] = SPA_AUDIO_CHANNEL_FC;
|
|
|
|
+ position[3] = SPA_AUDIO_CHANNEL_LFE;
|
|
|
|
+ position[4] = SPA_AUDIO_CHANNEL_RL;
|
|
|
|
+ position[5] = SPA_AUDIO_CHANNEL_RR;
|
|
|
|
+ position[6] = SPA_AUDIO_CHANNEL_SL;
|
|
|
|
+ position[7] = SPA_AUDIO_CHANNEL_SR;
|
|
|
|
+ break;
|
|
|
|
+ default:
|
|
|
|
+ for (size_t i = 0; i < channels; i++) {
|
|
|
|
+ position[i] = SPA_AUDIO_CHANNEL_UNKNOWN;
|
|
|
|
+ }
|
|
|
|
+ break;
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+enum audio_format spa_to_obs_audio_format(enum spa_audio_format format)
|
|
|
|
+{
|
|
|
|
+ switch (format) {
|
|
|
|
+ case SPA_AUDIO_FORMAT_U8:
|
|
|
|
+ return AUDIO_FORMAT_U8BIT;
|
|
|
|
+ case SPA_AUDIO_FORMAT_S16_LE:
|
|
|
|
+ return AUDIO_FORMAT_16BIT;
|
|
|
|
+ case SPA_AUDIO_FORMAT_S32_LE:
|
|
|
|
+ return AUDIO_FORMAT_32BIT;
|
|
|
|
+ case SPA_AUDIO_FORMAT_F32_LE:
|
|
|
|
+ return AUDIO_FORMAT_FLOAT;
|
2023-03-08 16:06:29 +01:00
|
|
|
+ case SPA_AUDIO_FORMAT_U8P:
|
|
|
|
+ return AUDIO_FORMAT_U8BIT_PLANAR;
|
|
|
|
+ case SPA_AUDIO_FORMAT_S16P:
|
|
|
|
+ return AUDIO_FORMAT_16BIT_PLANAR;
|
|
|
|
+ case SPA_AUDIO_FORMAT_S32P:
|
|
|
|
+ return AUDIO_FORMAT_32BIT_PLANAR;
|
|
|
|
+ case SPA_AUDIO_FORMAT_F32P:
|
|
|
|
+ return AUDIO_FORMAT_FLOAT_PLANAR;
|
2023-02-23 21:25:12 +01:00
|
|
|
+ default:
|
|
|
|
+ return AUDIO_FORMAT_UNKNOWN;
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+enum speaker_layout spa_to_obs_speakers(uint32_t channels)
|
|
|
|
+{
|
|
|
|
+ switch (channels) {
|
|
|
|
+ case 1:
|
|
|
|
+ return SPEAKERS_MONO;
|
|
|
|
+ case 2:
|
|
|
|
+ return SPEAKERS_STEREO;
|
|
|
|
+ case 3:
|
|
|
|
+ return SPEAKERS_2POINT1;
|
|
|
|
+ case 4:
|
|
|
|
+ return SPEAKERS_4POINT0;
|
|
|
|
+ case 5:
|
|
|
|
+ return SPEAKERS_4POINT1;
|
|
|
|
+ case 6:
|
|
|
|
+ return SPEAKERS_5POINT1;
|
|
|
|
+ case 8:
|
|
|
|
+ return SPEAKERS_7POINT1;
|
|
|
|
+ default:
|
|
|
|
+ return SPEAKERS_UNKNOWN;
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+bool spa_to_obs_pw_audio_info(struct obs_pw_audio_info *info,
|
|
|
|
+ const struct spa_pod *param)
|
|
|
|
+{
|
|
|
|
+ struct spa_audio_info_raw audio_info;
|
|
|
|
+
|
|
|
|
+ if (spa_format_audio_raw_parse(param, &audio_info) < 0) {
|
|
|
|
+ info->sample_rate = 0;
|
|
|
|
+ info->format = AUDIO_FORMAT_UNKNOWN;
|
|
|
|
+ info->speakers = SPEAKERS_UNKNOWN;
|
|
|
|
+
|
|
|
|
+ return false;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ info->sample_rate = audio_info.rate;
|
|
|
|
+ info->speakers = spa_to_obs_speakers(audio_info.channels);
|
|
|
|
+ info->format = spa_to_obs_audio_format(audio_info.format);
|
|
|
|
+
|
|
|
|
+ return true;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+static void on_process_cb(void *data)
|
|
|
|
+{
|
|
|
|
+ uint64_t now = os_gettime_ns();
|
|
|
|
+
|
|
|
|
+ struct obs_pw_audio_stream *s = data;
|
|
|
|
+
|
|
|
|
+ struct pw_buffer *b = pw_stream_dequeue_buffer(s->stream);
|
|
|
|
+
|
|
|
|
+ if (!b) {
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ struct spa_buffer *buf = b->buffer;
|
|
|
|
+
|
2023-03-08 16:06:29 +01:00
|
|
|
+ if (!s->info.sample_rate || buf->datas[0].type != SPA_DATA_MemPtr) {
|
2023-02-23 21:25:12 +01:00
|
|
|
+ goto queue;
|
|
|
|
+ }
|
|
|
|
+
|
2023-03-08 16:06:29 +01:00
|
|
|
+ struct obs_source_audio out = {
|
|
|
|
+ .frames = s->pos->clock.duration,
|
|
|
|
+ .speakers = s->info.speakers,
|
|
|
|
+ .format = s->info.format,
|
|
|
|
+ .samples_per_sec = s->info.sample_rate,
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ for (size_t i = 0; i < buf->n_datas && i < 8; i++) {
|
|
|
|
+ out.data[i] = buf->datas[i].data;
|
|
|
|
+ }
|
2023-02-23 21:25:12 +01:00
|
|
|
+
|
|
|
|
+ if (s->pos && (s->info.sample_rate * s->pos->clock.rate_diff)) {
|
|
|
|
+ /** Taken from PipeWire's implementation of JACK's jack_get_cycle_times
|
|
|
|
+ * (https://gitlab.freedesktop.org/pipewire/pipewire/-/blob/0.3.52/pipewire-jack/src/pipewire-jack.c#L5639)
|
2023-03-08 16:06:29 +01:00
|
|
|
+ * which is used in the linux-jack plugin to correctly set the timestamp
|
2023-02-23 21:25:12 +01:00
|
|
|
+ * (https://github.com/obsproject/obs-studio/blob/27.2.4/plugins/linux-jack/jack-wrapper.c#L87) */
|
|
|
|
+
|
|
|
|
+ float period_usecs =
|
|
|
|
+ s->pos->clock.duration * (float)SPA_USEC_PER_SEC /
|
|
|
|
+ (s->info.sample_rate * s->pos->clock.rate_diff);
|
|
|
|
+
|
|
|
|
+ out.timestamp = now - (uint64_t)(period_usecs * 1000);
|
|
|
|
+ } else {
|
|
|
|
+ out.timestamp = now - audio_frames_to_ns(s->info.sample_rate,
|
|
|
|
+ out.frames);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ obs_source_output_audio(s->output, &out);
|
|
|
|
+
|
|
|
|
+queue:
|
|
|
|
+ pw_stream_queue_buffer(s->stream, b);
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+static void on_state_changed_cb(void *data, enum pw_stream_state old,
|
|
|
|
+ enum pw_stream_state state, const char *error)
|
|
|
|
+{
|
|
|
|
+ UNUSED_PARAMETER(old);
|
|
|
|
+
|
|
|
|
+ struct obs_pw_audio_stream *s = data;
|
|
|
|
+
|
|
|
|
+ blog(LOG_DEBUG, "[pipewire] Stream %p state: \"%s\" (error: %s)",
|
|
|
|
+ s->stream, pw_stream_state_as_string(state),
|
|
|
|
+ error ? error : "none");
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+static void on_param_changed_cb(void *data, uint32_t id,
|
|
|
|
+ const struct spa_pod *param)
|
|
|
|
+{
|
|
|
|
+ if (!param || id != SPA_PARAM_Format) {
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ struct obs_pw_audio_stream *s = data;
|
|
|
|
+
|
|
|
|
+ if (!spa_to_obs_pw_audio_info(&s->info, param)) {
|
|
|
|
+ blog(LOG_WARNING,
|
|
|
|
+ "[pipewire] Stream %p failed to parse audio format info",
|
|
|
|
+ s->stream);
|
|
|
|
+ } else {
|
|
|
|
+ blog(LOG_INFO,
|
2023-03-08 16:06:29 +01:00
|
|
|
+ "[pipewire] %p Got format: rate %u - channels %u - format %u",
|
2023-02-23 21:25:12 +01:00
|
|
|
+ s->stream, s->info.sample_rate, s->info.speakers,
|
2023-03-08 16:06:29 +01:00
|
|
|
+ s->info.format);
|
2023-02-23 21:25:12 +01:00
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+static void on_io_changed_cb(void *data, uint32_t id, void *area, uint32_t size)
|
|
|
|
+{
|
|
|
|
+ UNUSED_PARAMETER(size);
|
|
|
|
+
|
|
|
|
+ struct obs_pw_audio_stream *s = data;
|
|
|
|
+
|
|
|
|
+ if (id == SPA_IO_Position) {
|
|
|
|
+ s->pos = area;
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+static const struct pw_stream_events stream_events = {
|
|
|
|
+ PW_VERSION_STREAM_EVENTS,
|
|
|
|
+ .process = on_process_cb,
|
|
|
|
+ .state_changed = on_state_changed_cb,
|
|
|
|
+ .param_changed = on_param_changed_cb,
|
|
|
|
+ .io_changed = on_io_changed_cb,
|
|
|
|
+};
|
|
|
|
+
|
|
|
|
+int obs_pw_audio_stream_connect(struct obs_pw_audio_stream *s,
|
2023-03-08 16:06:29 +01:00
|
|
|
+ uint32_t target_id, uint32_t audio_channels)
|
2023-02-23 21:25:12 +01:00
|
|
|
+{
|
2023-03-08 16:06:29 +01:00
|
|
|
+ enum spa_audio_channel pos[8];
|
2023-02-23 21:25:12 +01:00
|
|
|
+ obs_channels_to_spa_audio_position(pos, audio_channels);
|
|
|
|
+
|
|
|
|
+ uint8_t buffer[2048];
|
|
|
|
+ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
|
|
|
|
+ const struct spa_pod *params[1];
|
|
|
|
+
|
|
|
|
+ params[0] = spa_pod_builder_add_object(
|
|
|
|
+ &b, SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat,
|
|
|
|
+ SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio),
|
|
|
|
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw),
|
|
|
|
+ SPA_FORMAT_AUDIO_channels, SPA_POD_Int(audio_channels),
|
|
|
|
+ SPA_FORMAT_AUDIO_position,
|
2023-03-08 16:06:29 +01:00
|
|
|
+ SPA_POD_Array(sizeof(enum spa_audio_channel), SPA_TYPE_Id,
|
|
|
|
+ audio_channels, pos),
|
2023-02-23 21:25:12 +01:00
|
|
|
+ SPA_FORMAT_AUDIO_format,
|
|
|
|
+ SPA_POD_CHOICE_ENUM_Id(
|
2023-03-08 16:06:29 +01:00
|
|
|
+ 8, SPA_AUDIO_FORMAT_U8, SPA_AUDIO_FORMAT_S16_LE,
|
|
|
|
+ SPA_AUDIO_FORMAT_S32_LE, SPA_AUDIO_FORMAT_F32_LE,
|
|
|
|
+ SPA_AUDIO_FORMAT_U8P, SPA_AUDIO_FORMAT_S16P,
|
|
|
|
+ SPA_AUDIO_FORMAT_S32P, SPA_AUDIO_FORMAT_F32P));
|
2023-02-23 21:25:12 +01:00
|
|
|
+
|
2023-03-08 16:06:29 +01:00
|
|
|
+ return pw_stream_connect(s->stream, PW_DIRECTION_INPUT, target_id,
|
|
|
|
+ PW_STREAM_FLAG_AUTOCONNECT |
|
|
|
|
+ PW_STREAM_FLAG_MAP_BUFFERS |
|
|
|
|
+ PW_STREAM_FLAG_DONT_RECONNECT,
|
|
|
|
+ params, 1);
|
2023-02-23 21:25:12 +01:00
|
|
|
+}
|
2023-03-08 16:06:29 +01:00
|
|
|
+/* ------------------------------------------------- */
|
2023-02-23 21:25:12 +01:00
|
|
|
+
|
2023-03-08 16:06:29 +01:00
|
|
|
+/* Common PipeWire components */
|
|
|
|
+static void on_core_done_cb(void *data, uint32_t id, int seq)
|
2023-02-23 21:25:12 +01:00
|
|
|
+{
|
2023-03-08 16:06:29 +01:00
|
|
|
+ struct obs_pw_audio_instance *pw = data;
|
|
|
|
+
|
|
|
|
+ if (id == PW_ID_CORE && pw->seq == seq) {
|
|
|
|
+ pw_thread_loop_signal(pw->thread_loop, false);
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+static void on_core_error_cb(void *data, uint32_t id, int seq, int res,
|
|
|
|
+ const char *message)
|
|
|
|
+{
|
|
|
|
+ struct obs_pw_audio_instance *pw = data;
|
|
|
|
+
|
|
|
|
+ blog(LOG_ERROR, "[pipewire] Error id:%u seq:%d res:%d :%s", id, seq,
|
|
|
|
+ res, message);
|
|
|
|
+
|
|
|
|
+ pw_thread_loop_signal(pw->thread_loop, false);
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+static const struct pw_core_events core_events = {
|
|
|
|
+ PW_VERSION_CORE_EVENTS,
|
|
|
|
+ .done = on_core_done_cb,
|
|
|
|
+ .error = on_core_error_cb,
|
|
|
|
+};
|
|
|
|
+
|
|
|
|
+bool obs_pw_audio_instance_init(
|
|
|
|
+ struct obs_pw_audio_instance *pw,
|
|
|
|
+ const struct pw_registry_events *registry_events,
|
|
|
|
+ void *registry_cb_data, bool stream_capture_sink,
|
|
|
|
+ bool stream_want_driver, obs_source_t *stream_output)
|
|
|
|
+{
|
|
|
|
+ pw->thread_loop = pw_thread_loop_new("PipeWire thread loop", NULL);
|
|
|
|
+ pw->context = pw_context_new(pw_thread_loop_get_loop(pw->thread_loop),
|
|
|
|
+ NULL, 0);
|
|
|
|
+
|
|
|
|
+ pw_thread_loop_lock(pw->thread_loop);
|
|
|
|
+
|
|
|
|
+ if (pw_thread_loop_start(pw->thread_loop) < 0) {
|
|
|
|
+ blog(LOG_WARNING,
|
|
|
|
+ "[pipewire] Error starting threaded mainloop");
|
|
|
|
+ return false;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ pw->core = pw_context_connect(pw->context, NULL, 0);
|
|
|
|
+ if (!pw->core) {
|
|
|
|
+ blog(LOG_WARNING, "[pipewire] Error creating PipeWire core");
|
|
|
|
+ return false;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ pw_core_add_listener(pw->core, &pw->core_listener, &core_events, pw);
|
|
|
|
+
|
|
|
|
+ pw->registry = pw_core_get_registry(pw->core, PW_VERSION_REGISTRY, 0);
|
|
|
|
+ if (!pw->registry) {
|
|
|
|
+ return false;
|
|
|
|
+ }
|
|
|
|
+ pw_registry_add_listener(pw->registry, &pw->registry_listener,
|
|
|
|
+ registry_events, registry_cb_data);
|
|
|
|
+
|
|
|
|
+ pw->audio.output = stream_output;
|
|
|
|
+ pw->audio.stream = pw_stream_new(
|
|
|
|
+ pw->core, "OBS Studio",
|
|
|
|
+ pw_properties_new(
|
|
|
|
+ PW_KEY_NODE_NAME, "OBS Studio", PW_KEY_NODE_DESCRIPTION,
|
|
|
|
+ "OBS Audio Capture", PW_KEY_MEDIA_TYPE, "Audio",
|
|
|
|
+ PW_KEY_MEDIA_CATEGORY, "Capture", PW_KEY_MEDIA_ROLE,
|
|
|
|
+ "Production", PW_KEY_NODE_WANT_DRIVER,
|
|
|
|
+ stream_want_driver ? "true" : "false",
|
|
|
|
+ PW_KEY_STREAM_CAPTURE_SINK,
|
|
|
|
+ stream_capture_sink ? "true" : "false", NULL));
|
|
|
|
+
|
|
|
|
+ if (!pw->audio.stream) {
|
|
|
|
+ blog(LOG_WARNING, "[pipewire] Failed to create stream");
|
|
|
|
+ return false;
|
|
|
|
+ }
|
|
|
|
+ blog(LOG_INFO, "[pipewire] Created stream %p", pw->audio.stream);
|
|
|
|
+
|
|
|
|
+ pw_stream_add_listener(pw->audio.stream, &pw->audio.stream_listener,
|
|
|
|
+ &stream_events, &pw->audio);
|
|
|
|
+
|
|
|
|
+ return true;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+void obs_pw_audio_instance_destroy(struct obs_pw_audio_instance *pw)
|
|
|
|
+{
|
|
|
|
+ if (pw->audio.stream) {
|
|
|
|
+ spa_hook_remove(&pw->audio.stream_listener);
|
|
|
|
+ if (pw_stream_get_state(pw->audio.stream, NULL) !=
|
|
|
|
+ PW_STREAM_STATE_UNCONNECTED) {
|
|
|
|
+ pw_stream_disconnect(pw->audio.stream);
|
|
|
|
+ }
|
|
|
|
+ pw_stream_destroy(pw->audio.stream);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if (pw->registry) {
|
|
|
|
+ spa_hook_remove(&pw->registry_listener);
|
|
|
|
+ spa_zero(pw->registry_listener);
|
|
|
|
+ pw_proxy_destroy((struct pw_proxy *)pw->registry);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ pw_thread_loop_unlock(pw->thread_loop);
|
|
|
|
+ pw_thread_loop_stop(pw->thread_loop);
|
|
|
|
+
|
|
|
|
+ if (pw->core) {
|
|
|
|
+ spa_hook_remove(&pw->core_listener);
|
|
|
|
+ spa_zero(pw->core_listener);
|
|
|
|
+ pw_core_disconnect(pw->core);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if (pw->context) {
|
|
|
|
+ pw_context_destroy(pw->context);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ pw_thread_loop_destroy(pw->thread_loop);
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+void obs_pw_audio_instance_sync(struct obs_pw_audio_instance *pw)
|
|
|
|
+{
|
|
|
|
+ pw->seq = pw_core_sync(pw->core, PW_ID_CORE, pw->seq);
|
2023-02-23 21:25:12 +01:00
|
|
|
+}
|
|
|
|
+/* ------------------------------------------------- */
|
|
|
|
+
|
|
|
|
+/* PipeWire metadata */
|
|
|
|
+static int on_metadata_property_cb(void *data, uint32_t id, const char *key,
|
|
|
|
+ const char *type, const char *value)
|
|
|
|
+{
|
|
|
|
+ UNUSED_PARAMETER(type);
|
|
|
|
+
|
|
|
|
+ struct obs_pw_audio_default_node_metadata *metadata = data;
|
|
|
|
+
|
|
|
|
+ if (metadata->default_node_callback && id == PW_ID_CORE && key &&
|
|
|
|
+ value &&
|
|
|
|
+ strcmp(key, metadata->wants_sink ? "default.audio.sink"
|
|
|
|
+ : "default.audio.source") == 0) {
|
|
|
|
+ char val[128];
|
|
|
|
+ if (!json_object_find(value, "name", val, sizeof(val)) ||
|
|
|
|
+ !*val) {
|
|
|
|
+ return 0;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ metadata->default_node_callback(metadata->data, val);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return 0;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+static const struct pw_metadata_events metadata_events = {
|
|
|
|
+ PW_VERSION_METADATA_EVENTS,
|
|
|
|
+ .property = on_metadata_property_cb,
|
|
|
|
+};
|
|
|
|
+
|
|
|
|
+static void on_metadata_proxy_removed_cb(void *data)
|
|
|
|
+{
|
|
|
|
+ struct obs_pw_audio_default_node_metadata *metadata = data;
|
|
|
|
+ pw_proxy_destroy(metadata->proxy);
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+static void on_metadata_proxy_destroy_cb(void *data)
|
|
|
|
+{
|
|
|
|
+ struct obs_pw_audio_default_node_metadata *metadata = data;
|
|
|
|
+
|
|
|
|
+ spa_hook_remove(&metadata->metadata_listener);
|
|
|
|
+ spa_hook_remove(&metadata->proxy_listener);
|
|
|
|
+ spa_zero(metadata->metadata_listener);
|
|
|
|
+ spa_zero(metadata->proxy_listener);
|
|
|
|
+
|
|
|
|
+ metadata->proxy = NULL;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+static const struct pw_proxy_events metadata_proxy_events = {
|
|
|
|
+ PW_VERSION_PROXY_EVENTS,
|
|
|
|
+ .removed = on_metadata_proxy_removed_cb,
|
|
|
|
+ .destroy = on_metadata_proxy_destroy_cb,
|
|
|
|
+};
|
|
|
|
+
|
|
|
|
+bool obs_pw_audio_default_node_metadata_listen(
|
|
|
|
+ struct obs_pw_audio_default_node_metadata *metadata,
|
|
|
|
+ struct obs_pw_audio_instance *pw, uint32_t global_id, bool wants_sink,
|
|
|
|
+ void (*default_node_callback)(void *data, const char *name), void *data)
|
|
|
|
+{
|
|
|
|
+ if (metadata->proxy) {
|
|
|
|
+ pw_proxy_destroy(metadata->proxy);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ struct pw_proxy *metadata_proxy = pw_registry_bind(
|
|
|
|
+ pw->registry, global_id, PW_TYPE_INTERFACE_Metadata,
|
|
|
|
+ PW_VERSION_METADATA, 0);
|
|
|
|
+ if (!metadata_proxy) {
|
|
|
|
+ return false;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ metadata->proxy = metadata_proxy;
|
|
|
|
+
|
|
|
|
+ metadata->wants_sink = wants_sink;
|
|
|
|
+
|
|
|
|
+ metadata->default_node_callback = default_node_callback;
|
|
|
|
+ metadata->data = data;
|
|
|
|
+
|
|
|
|
+ pw_proxy_add_object_listener(metadata->proxy,
|
|
|
|
+ &metadata->metadata_listener,
|
|
|
|
+ &metadata_events, metadata);
|
|
|
|
+ pw_proxy_add_listener(metadata->proxy, &metadata->proxy_listener,
|
|
|
|
+ &metadata_proxy_events, metadata);
|
|
|
|
+
|
|
|
|
+ return true;
|
|
|
|
+}
|
|
|
|
+/* ------------------------------------------------- */
|
|
|
|
+
|
2023-03-08 16:06:29 +01:00
|
|
|
+/* Proxied objects */
|
2023-02-23 21:25:12 +01:00
|
|
|
+static void on_proxy_bound_cb(void *data, uint32_t global_id)
|
|
|
|
+{
|
|
|
|
+ struct obs_pw_audio_proxied_object *obj = data;
|
|
|
|
+ if (obj->bound_callback) {
|
|
|
|
+ obj->bound_callback(obj->data, global_id);
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+static void on_proxy_removed_cb(void *data)
|
|
|
|
+{
|
|
|
|
+ struct obs_pw_audio_proxied_object *obj = data;
|
|
|
|
+ pw_proxy_destroy(obj->proxy);
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+static void on_proxy_destroy_cb(void *data)
|
|
|
|
+{
|
|
|
|
+ struct obs_pw_audio_proxied_object *obj = data;
|
|
|
|
+ spa_hook_remove(&obj->proxy_listener);
|
|
|
|
+
|
|
|
|
+ spa_list_remove(&obj->link);
|
|
|
|
+
|
|
|
|
+ if (obj->destroy_callback) {
|
|
|
|
+ obj->destroy_callback(obj->data);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ bfree(obj->data);
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+static const struct pw_proxy_events proxy_events = {
|
|
|
|
+ PW_VERSION_PROXY_EVENTS,
|
|
|
|
+ .bound = on_proxy_bound_cb,
|
|
|
|
+ .removed = on_proxy_removed_cb,
|
|
|
|
+ .destroy = on_proxy_destroy_cb,
|
|
|
|
+};
|
|
|
|
+
|
|
|
|
+void obs_pw_audio_proxied_object_init(
|
|
|
|
+ struct obs_pw_audio_proxied_object *obj, struct pw_proxy *proxy,
|
|
|
|
+ struct spa_list *list,
|
|
|
|
+ void (*bound_callback)(void *data, uint32_t global_id),
|
|
|
|
+ void (*destroy_callback)(void *data), void *data)
|
|
|
|
+{
|
|
|
|
+ obj->proxy = proxy;
|
|
|
|
+ obj->bound_callback = bound_callback;
|
|
|
|
+ obj->destroy_callback = destroy_callback;
|
|
|
|
+ obj->data = data;
|
|
|
|
+
|
|
|
|
+ spa_list_append(list, &obj->link);
|
|
|
|
+
|
2023-03-08 16:06:29 +01:00
|
|
|
+ spa_zero(obj->proxy_listener);
|
2023-02-23 21:25:12 +01:00
|
|
|
+ pw_proxy_add_listener(obj->proxy, &obj->proxy_listener, &proxy_events,
|
|
|
|
+ obj);
|
|
|
|
+}
|
|
|
|
+/* ------------------------------------------------- */
|
|
|
|
diff --git a/plugins/linux-pipewire/pipewire-audio.h b/plugins/linux-pipewire/pipewire-audio.h
|
|
|
|
new file mode 100644
|
2023-03-08 16:06:29 +01:00
|
|
|
index 0000000000000..316276df75e54
|
2023-02-23 21:25:12 +01:00
|
|
|
--- /dev/null
|
|
|
|
+++ b/plugins/linux-pipewire/pipewire-audio.h
|
2023-03-08 16:06:29 +01:00
|
|
|
@@ -0,0 +1,155 @@
|
2023-02-23 21:25:12 +01:00
|
|
|
+/* pipewire-audio.h
|
|
|
|
+ *
|
|
|
|
+ * Copyright 2022 Dimitris Papaioannou <dimtpap@protonmail.com>
|
|
|
|
+ *
|
|
|
|
+ * This program is free software: you can redistribute it and/or modify
|
|
|
|
+ * it under the terms of the GNU General Public License as published by
|
|
|
|
+ * the Free Software Foundation, either version 2 of the License, or
|
|
|
|
+ * (at your option) any later version.
|
|
|
|
+ *
|
|
|
|
+ * This program 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 General Public License for more details.
|
|
|
|
+ *
|
|
|
|
+ * You should have received a copy of the GNU General Public License
|
|
|
|
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
+ *
|
|
|
|
+ * SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
+ */
|
|
|
|
+
|
2023-03-08 16:06:29 +01:00
|
|
|
+/* Stuff used by the PipeWire audio capture sources */
|
2023-02-23 21:25:12 +01:00
|
|
|
+
|
|
|
|
+#pragma once
|
|
|
|
+
|
|
|
|
+#include <obs-module.h>
|
|
|
|
+
|
|
|
|
+#include <pipewire/pipewire.h>
|
|
|
|
+#include <pipewire/extensions/metadata.h>
|
|
|
|
+#include <spa/param/audio/format-utils.h>
|
|
|
|
+
|
|
|
|
+/* PipeWire Stream wrapper */
|
|
|
|
+
|
|
|
|
+/**
|
|
|
|
+ * Audio metadata
|
|
|
|
+ */
|
|
|
|
+struct obs_pw_audio_info {
|
|
|
|
+ uint32_t sample_rate;
|
|
|
|
+ enum audio_format format;
|
|
|
|
+ enum speaker_layout speakers;
|
|
|
|
+};
|
|
|
|
+
|
|
|
|
+/**
|
|
|
|
+ * PipeWire stream wrapper that outputs to an OBS source
|
|
|
|
+ */
|
|
|
|
+struct obs_pw_audio_stream {
|
|
|
|
+ struct pw_stream *stream;
|
|
|
|
+ struct spa_hook stream_listener;
|
|
|
|
+ struct obs_pw_audio_info info;
|
|
|
|
+ struct spa_io_position *pos;
|
|
|
|
+
|
|
|
|
+ obs_source_t *output;
|
|
|
|
+};
|
|
|
|
+
|
|
|
|
+/**
|
2023-03-08 16:06:29 +01:00
|
|
|
+ * Connect a stream with the default params
|
|
|
|
+ * @return 0 on success, < 0 on error
|
|
|
|
+ */
|
|
|
|
+int obs_pw_audio_stream_connect(struct obs_pw_audio_stream *s,
|
|
|
|
+ uint32_t target_id, uint32_t channels);
|
|
|
|
+/* ------------------------------------------------- */
|
|
|
|
+
|
|
|
|
+/**
|
|
|
|
+ * Common PipeWire components
|
2023-02-23 21:25:12 +01:00
|
|
|
+ */
|
2023-03-08 16:06:29 +01:00
|
|
|
+struct obs_pw_audio_instance {
|
|
|
|
+ struct pw_thread_loop *thread_loop;
|
|
|
|
+ struct pw_context *context;
|
|
|
|
+
|
|
|
|
+ struct pw_core *core;
|
|
|
|
+ struct spa_hook core_listener;
|
|
|
|
+ int seq;
|
|
|
|
+
|
|
|
|
+ struct pw_registry *registry;
|
|
|
|
+ struct spa_hook registry_listener;
|
|
|
|
+
|
|
|
|
+ struct obs_pw_audio_stream audio;
|
|
|
|
+};
|
2023-02-23 21:25:12 +01:00
|
|
|
+
|
|
|
|
+/**
|
2023-03-08 16:06:29 +01:00
|
|
|
+ * Initialize a PipeWire instance
|
|
|
|
+ * @warning The thread loop is left locked
|
|
|
|
+ * @return true on success, false on error
|
2023-02-23 21:25:12 +01:00
|
|
|
+ */
|
2023-03-08 16:06:29 +01:00
|
|
|
+bool obs_pw_audio_instance_init(
|
|
|
|
+ struct obs_pw_audio_instance *pw,
|
|
|
|
+ const struct pw_registry_events *registry_events,
|
|
|
|
+ void *registry_cb_data, bool stream_capture_sink,
|
|
|
|
+ bool stream_want_driver, obs_source_t *stream_output);
|
2023-02-23 21:25:12 +01:00
|
|
|
+
|
|
|
|
+/**
|
2023-03-08 16:06:29 +01:00
|
|
|
+ * Destroy a PipeWire instance
|
|
|
|
+ * @warning Call with the thread loop locked
|
2023-02-23 21:25:12 +01:00
|
|
|
+ */
|
2023-03-08 16:06:29 +01:00
|
|
|
+void obs_pw_audio_instance_destroy(struct obs_pw_audio_instance *pw);
|
2023-02-23 21:25:12 +01:00
|
|
|
+
|
|
|
|
+/**
|
2023-03-08 16:06:29 +01:00
|
|
|
+ * Trigger a PipeWire core sync
|
2023-02-23 21:25:12 +01:00
|
|
|
+ */
|
2023-03-08 16:06:29 +01:00
|
|
|
+void obs_pw_audio_instance_sync(struct obs_pw_audio_instance *pw);
|
2023-02-23 21:25:12 +01:00
|
|
|
+/* ------------------------------------------------- */
|
|
|
|
+
|
|
|
|
+/**
|
|
|
|
+ * PipeWire metadata
|
|
|
|
+ */
|
|
|
|
+struct obs_pw_audio_default_node_metadata {
|
|
|
|
+ struct pw_proxy *proxy;
|
|
|
|
+ struct spa_hook proxy_listener;
|
|
|
|
+ struct spa_hook metadata_listener;
|
|
|
|
+
|
|
|
|
+ bool wants_sink;
|
|
|
|
+
|
|
|
|
+ void (*default_node_callback)(void *data, const char *name);
|
|
|
|
+ void *data;
|
|
|
|
+};
|
|
|
|
+
|
|
|
|
+/**
|
|
|
|
+ * Add listeners to the metadata
|
|
|
|
+ * @return true on success, false on error
|
|
|
|
+ */
|
|
|
|
+bool obs_pw_audio_default_node_metadata_listen(
|
|
|
|
+ struct obs_pw_audio_default_node_metadata *metadata,
|
|
|
|
+ struct obs_pw_audio_instance *pw, uint32_t global_id, bool wants_sink,
|
|
|
|
+ void (*default_node_callback)(void *data, const char *name),
|
|
|
|
+ void *data);
|
|
|
|
+/* ------------------------------------------------- */
|
|
|
|
+
|
|
|
|
+/**
|
|
|
|
+ * Generic proxy handler for PipeWire objects tracked in lists
|
|
|
|
+ */
|
|
|
|
+struct obs_pw_audio_proxied_object {
|
|
|
|
+ void *data;
|
|
|
|
+
|
|
|
|
+ void (*bound_callback)(void *data, uint32_t global_id);
|
|
|
|
+ void (*destroy_callback)(void *data);
|
|
|
|
+
|
|
|
|
+ struct pw_proxy *proxy;
|
|
|
|
+ struct spa_hook proxy_listener;
|
|
|
|
+
|
|
|
|
+ struct spa_list link;
|
|
|
|
+};
|
|
|
|
+
|
|
|
|
+/**
|
|
|
|
+ * Initialize a proxied object
|
|
|
|
+ */
|
|
|
|
+void obs_pw_audio_proxied_object_init(
|
|
|
|
+ struct obs_pw_audio_proxied_object *obj, struct pw_proxy *proxy,
|
|
|
|
+ struct spa_list *list,
|
|
|
|
+ void (*bound_callback)(void *data, uint32_t global_id),
|
|
|
|
+ void (*destroy_callback)(void *data), void *data);
|
|
|
|
+/* ------------------------------------------------- */
|
|
|
|
+
|
|
|
|
+/* Sources */
|
|
|
|
+void pipewire_audio_capture_load(void);
|
|
|
|
+void pipewire_audio_capture_app_load(void);
|
|
|
|
+/* ------------------------------------------------- */
|