aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/pipewire_audio.c
diff options
context:
space:
mode:
authordec05eba <dec05eba@protonmail.com>2026-07-25 02:54:47 +0200
committerdec05eba <dec05eba@protonmail.com>2026-07-25 02:54:47 +0200
commit8dcf40270d142bbdab77f91640d3a78009d8f936 (patch)
tree03fb214740bfef253d149c431baeb7dd0b05921e /src/pipewire_audio.c
parent6e876fe1172b82b806fbed769ea5cda46edd4f4f (diff)
pipewire audio: only record applications and non virtual sources
Diffstat (limited to 'src/pipewire_audio.c')
-rw-r--r--src/pipewire_audio.c186
1 files changed, 157 insertions, 29 deletions
diff --git a/src/pipewire_audio.c b/src/pipewire_audio.c
index ed8705b..ec4fde1 100644
--- a/src/pipewire_audio.c
+++ b/src/pipewire_audio.c
@@ -81,6 +81,10 @@ static bool requested_link_matches_name_case_insensitive_any_type(const gsr_pipe
return false;
}
+static bool gsr_pipewire_audio_node_is_application(const gsr_pipewire_audio_node *node) {
+ return node->type == GSR_PIPEWIRE_AUDIO_NODE_TYPE_STREAM_OUTPUT && node->is_application && !node->is_virtual;
+}
+
static bool requested_link_has_type(const gsr_pipewire_audio_requested_link *requested_link, gsr_pipewire_audio_requested_type type) {
for(int i = 0; i < requested_link->num_outputs; ++i) {
if(requested_link->outputs[i].type == type)
@@ -191,8 +195,7 @@ static void gsr_pipewire_audio_create_link(gsr_pipewire_audio *self, const gsr_p
if(output_node->type != requested_link->output_type)
continue;
- /* Virtual nodes arent applications, dont care about them when capturing applications (or when using inverted application capture) */
- if(output_node->is_virtual && requested_link->output_type == GSR_PIPEWIRE_AUDIO_NODE_TYPE_STREAM_OUTPUT)
+ if(requested_link->output_type == GSR_PIPEWIRE_AUDIO_NODE_TYPE_STREAM_OUTPUT && !gsr_pipewire_audio_node_is_application(output_node))
continue;
const bool requested_link_matches_app = requested_link_matches_name_case_insensitive_any_type(self, requested_link, output_node->name);
@@ -414,6 +417,137 @@ static bool array_ensure_capacity(void **array, size_t size, size_t *capacity_it
return true;
}
+struct gsr_pipewire_audio_node_binding {
+ gsr_pipewire_audio *self;
+ struct pw_proxy *proxy;
+ struct spa_hook object_listener;
+ struct spa_hook proxy_listener;
+ uint32_t node_id;
+};
+
+static gsr_pipewire_audio_node* gsr_pipewire_audio_get_node_by_id(gsr_pipewire_audio *self, uint32_t node_id) {
+ for(size_t i = 0; i < self->num_stream_nodes; ++i) {
+ if(self->stream_nodes[i].id == node_id)
+ return &self->stream_nodes[i];
+ }
+ return NULL;
+}
+
+static void gsr_pipewire_audio_add_node(gsr_pipewire_audio *self, uint32_t id, const char *node_name, gsr_pipewire_audio_node_type type, bool is_virtual, bool is_application) {
+ if(!array_ensure_capacity((void**)&self->stream_nodes, self->num_stream_nodes, &self->stream_nodes_capacity_items, sizeof(gsr_pipewire_audio_node)))
+ return;
+
+ char *node_name_copy = strdup(node_name);
+ if(!node_name_copy)
+ return;
+
+ self->stream_nodes[self->num_stream_nodes].id = id;
+ self->stream_nodes[self->num_stream_nodes].name = node_name_copy;
+ self->stream_nodes[self->num_stream_nodes].type = type;
+ self->stream_nodes[self->num_stream_nodes].is_virtual = is_virtual;
+ self->stream_nodes[self->num_stream_nodes].is_application = is_application;
+ ++self->num_stream_nodes;
+
+ gsr_pipewire_audio_create_links(self);
+}
+
+static bool node_properties_is_virtual(const char *node_virtual, const char *media_class) {
+ if(node_virtual && (strcmp(node_virtual, "true") == 0 || strcmp(node_virtual, "1") == 0))
+ return true;
+ return media_class && string_ends_with(media_class, "/Virtual");
+}
+
+static void on_node_info_cb(void *data, const struct pw_node_info *info) {
+ gsr_pipewire_audio_node_binding *node_binding = data;
+ gsr_pipewire_audio *self = node_binding->self;
+ if(!info || !info->props || !self->running)
+ return;
+
+ const char *node_name = spa_dict_lookup(info->props, PW_KEY_NODE_NAME);
+ if(!node_name)
+ return;
+
+ const char *media_class = spa_dict_lookup(info->props, PW_KEY_MEDIA_CLASS);
+ const char *node_virtual = spa_dict_lookup(info->props, PW_KEY_NODE_VIRTUAL);
+ const bool is_virtual = node_properties_is_virtual(node_virtual, media_class);
+
+ /* application.id is optional and not all applications set it (firefox for example doesn't).
+ Nodes not created by an application (loopback and filter streams for example) have none of these properties. */
+ const bool is_application = spa_dict_lookup(info->props, PW_KEY_APP_ID) != NULL
+ || spa_dict_lookup(info->props, PW_KEY_APP_NAME) != NULL
+ || spa_dict_lookup(info->props, PW_KEY_APP_PROCESS_BINARY) != NULL;
+
+ gsr_pipewire_audio_node *existing_node = gsr_pipewire_audio_get_node_by_id(self, node_binding->node_id);
+ if(existing_node) {
+ existing_node->is_virtual = is_virtual;
+ if(is_application)
+ existing_node->is_application = true;
+ } else {
+ gsr_pipewire_audio_add_node(self, node_binding->node_id, node_name, GSR_PIPEWIRE_AUDIO_NODE_TYPE_STREAM_OUTPUT, is_virtual, is_application);
+ }
+}
+
+static const struct pw_node_events node_events = {
+ PW_VERSION_NODE_EVENTS,
+ .info = on_node_info_cb,
+};
+
+static void on_node_proxy_removed_cb(void *data) {
+ gsr_pipewire_audio_node_binding *node_binding = data;
+ pw_proxy_destroy(node_binding->proxy);
+}
+
+static void gsr_pipewire_audio_forget_node_binding(gsr_pipewire_audio *self, const gsr_pipewire_audio_node_binding *node_binding) {
+ for(size_t i = 0; i < self->num_node_bindings; ++i) {
+ if(self->node_bindings[i] == node_binding) {
+ self->node_bindings[i] = self->node_bindings[self->num_node_bindings - 1];
+ --self->num_node_bindings;
+ break;
+ }
+ }
+}
+
+static void on_node_proxy_destroy_cb(void *data) {
+ gsr_pipewire_audio_node_binding *node_binding = data;
+ spa_hook_remove(&node_binding->object_listener);
+ spa_hook_remove(&node_binding->proxy_listener);
+ gsr_pipewire_audio_forget_node_binding(node_binding->self, node_binding);
+ free(node_binding);
+}
+
+static const struct pw_proxy_events node_proxy_events = {
+ PW_VERSION_PROXY_EVENTS,
+ .removed = on_node_proxy_removed_cb,
+ .destroy = on_node_proxy_destroy_cb,
+};
+
+/* The registry only broadcasts a small set of the node properties (node.name, media.class, application.name, etc).
+ Binding to the node is required to get the other properties (node.virtual, application.id, etc) from the node info event. */
+static void gsr_pipewire_audio_bind_node(gsr_pipewire_audio *self, uint32_t id) {
+ if(!array_ensure_capacity((void**)&self->node_bindings, self->num_node_bindings, &self->node_bindings_capacity_items, sizeof(gsr_pipewire_audio_node_binding*)))
+ return;
+
+ gsr_pipewire_audio_node_binding *node_binding = calloc(1, sizeof(gsr_pipewire_audio_node_binding));
+ if(!node_binding)
+ return;
+
+ struct pw_proxy *proxy = pw_registry_bind(self->registry, id, PW_TYPE_INTERFACE_Node, PW_VERSION_NODE, 0);
+ if(!proxy) {
+ fprintf(stderr, "gsr error: gsr_pipewire_audio_bind_node: failed to bind to node %u\n", id);
+ free(node_binding);
+ return;
+ }
+
+ node_binding->self = self;
+ node_binding->proxy = proxy;
+ node_binding->node_id = id;
+ pw_proxy_add_object_listener(proxy, &node_binding->object_listener, &node_events, node_binding);
+ pw_proxy_add_listener(proxy, &node_binding->proxy_listener, &node_proxy_events, node_binding);
+
+ self->node_bindings[self->num_node_bindings] = node_binding;
+ ++self->num_node_bindings;
+}
+
static void registry_event_global(void *data, uint32_t id, uint32_t permissions,
const char *type, uint32_t version,
const struct spa_dict *props)
@@ -433,32 +567,15 @@ static void registry_event_global(void *data, uint32_t id, uint32_t permissions,
const bool is_stream_input = media_class && strcmp(media_class, "Stream/Input/Audio") == 0;
const bool is_sink = media_class && string_starts_with(media_class, "Audio/Sink"); // Matches Audio/Sink/Virtual as well
const bool is_source = media_class && string_starts_with(media_class, "Audio/Source"); // Matches Audio/Source/Virtual as well
- if(node_name && (is_stream_output || is_stream_input || is_sink || is_source)) {
- //const char *application_binary = spa_dict_lookup(props, PW_KEY_APP_PROCESS_BINARY);
- //const char *application_name = spa_dict_lookup(props, PW_KEY_APP_NAME);
- //fprintf(stderr, " node name: %s, app binary: %s, app name: %s\n", node_name, application_binary, application_name);
-
- if(!array_ensure_capacity((void**)&self->stream_nodes, self->num_stream_nodes, &self->stream_nodes_capacity_items, sizeof(gsr_pipewire_audio_node)))
- return;
-
- const char *node_virtual = spa_dict_lookup(props, PW_KEY_NODE_VIRTUAL);
- const bool is_virtual = (node_virtual && (strcmp(node_virtual, "true") == 0 || strcmp(node_virtual, "1") == 0)) || string_ends_with(media_class, "/Virtual");
-
- char *node_name_copy = strdup(node_name);
- if(node_name_copy) {
- self->stream_nodes[self->num_stream_nodes].id = id;
- self->stream_nodes[self->num_stream_nodes].name = node_name_copy;
- self->stream_nodes[self->num_stream_nodes].is_virtual = is_virtual;
- if(is_stream_output)
- self->stream_nodes[self->num_stream_nodes].type = GSR_PIPEWIRE_AUDIO_NODE_TYPE_STREAM_OUTPUT;
- else if(is_stream_input)
- self->stream_nodes[self->num_stream_nodes].type = GSR_PIPEWIRE_AUDIO_NODE_TYPE_STREAM_INPUT;
- else if(is_sink || is_source)
- self->stream_nodes[self->num_stream_nodes].type = GSR_PIPEWIRE_AUDIO_NODE_TYPE_SINK_OR_SOURCE;
- ++self->num_stream_nodes;
-
- gsr_pipewire_audio_create_links(self);
- }
+ if(node_name && is_stream_output) {
+ /* Stream output nodes (application audio) are added when the full node info is received instead */
+ gsr_pipewire_audio_bind_node(self, id);
+ } else if(node_name && (is_stream_input || is_sink || is_source)) {
+ gsr_pipewire_audio_node_type node_type = GSR_PIPEWIRE_AUDIO_NODE_TYPE_SINK_OR_SOURCE;
+ if(is_stream_input)
+ node_type = GSR_PIPEWIRE_AUDIO_NODE_TYPE_STREAM_INPUT;
+
+ gsr_pipewire_audio_add_node(self, id, node_name, node_type, node_properties_is_virtual(NULL, media_class), false);
}
} else if(strcmp(type, PW_TYPE_INTERFACE_Port) == 0) {
const char *port_name = spa_dict_lookup(props, PW_KEY_PORT_NAME);
@@ -682,6 +799,17 @@ void gsr_pipewire_audio_deinit(gsr_pipewire_audio *self) {
self->metadata_proxy = NULL;
}
+ /* pw_proxy_destroy removes the node binding from node_bindings (in the proxy destroy event) */
+ while(self->num_node_bindings > 0) {
+ pw_proxy_destroy(self->node_bindings[0]->proxy);
+ }
+
+ if(self->node_bindings) {
+ free(self->node_bindings);
+ self->node_bindings = NULL;
+ self->node_bindings_capacity_items = 0;
+ }
+
spa_hook_remove(&self->registry_listener);
spa_hook_remove(&self->core_listener);
@@ -847,7 +975,7 @@ void gsr_pipewire_audio_for_each_app(gsr_pipewire_audio *self, gsr_pipewire_audi
pw_thread_loop_lock(self->thread_loop);
for(int i = 0; i < (int)self->num_stream_nodes; ++i) {
const gsr_pipewire_audio_node *node = &self->stream_nodes[i];
- if(node->type != GSR_PIPEWIRE_AUDIO_NODE_TYPE_STREAM_OUTPUT || node->is_virtual)
+ if(!gsr_pipewire_audio_node_is_application(node))
continue;
bool duplicate_app = false;