diff --git a/patches/6207.patch b/patches/6207.patch index e34c456..783cb86 100644 --- a/patches/6207.patch +++ b/patches/6207.patch @@ -1,4 +1,4 @@ -From 5ef5ef7f4ae3afa4b41a89c6e1a07d17ec9aec19 Mon Sep 17 00:00:00 2001 +From 4d25fde40080c4fb55583b23703a8bbc8b507924 Mon Sep 17 00:00:00 2001 From: Dimitris Papaioannou Date: Sun, 26 Jun 2022 17:14:24 +0300 Subject: [PATCH] linux-pipewire: Add PipeWire audio captures @@ -7,18 +7,18 @@ 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 + - .../pipewire-audio-capture-app.c | 938 ++++++++++++++++++ - .../pipewire-audio-capture-device.c | 543 ++++++++++ - plugins/linux-pipewire/pipewire-audio.c | 582 +++++++++++ - plugins/linux-pipewire/pipewire-audio.h | 172 ++++ - 7 files changed, 2251 insertions(+), 1 deletion(-) + .../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(-) 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 -index 31a55e68b60d..1e23a3163d26 100644 +index 31a55e68b60d7..1e23a3163d26b 100644 --- a/plugins/linux-pipewire/CMakeLists.txt +++ b/plugins/linux-pipewire/CMakeLists.txt @@ -38,7 +38,11 @@ target_sources( @@ -35,7 +35,7 @@ index 31a55e68b60d..1e23a3163d26 100644 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 -index a9e222a99568..edde250b57c7 100644 +index a9e222a995686..edde250b57c76 100644 --- 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" @@ -49,7 +49,7 @@ index a9e222a99568..edde250b57c7 100644 +Application="Application" +ExceptApp="Capture all apps except selected" diff --git a/plugins/linux-pipewire/linux-pipewire.c b/plugins/linux-pipewire/linux-pipewire.c -index fea91254ac19..0307fed6f0ca 100644 +index 798ae2fe8ca9f..253256dfd469f 100644 --- a/plugins/linux-pipewire/linux-pipewire.c +++ b/plugins/linux-pipewire/linux-pipewire.c @@ -2,6 +2,7 @@ @@ -80,11 +80,11 @@ index fea91254ac19..0307fed6f0ca 100644 diff --git a/plugins/linux-pipewire/pipewire-audio-capture-app.c b/plugins/linux-pipewire/pipewire-audio-capture-app.c new file mode 100644 -index 000000000000..e14677367bf8 +index 0000000000000..461d2c8f55f26 --- /dev/null +++ b/plugins/linux-pipewire/pipewire-audio-capture-app.c -@@ -0,0 +1,938 @@ -+/* pipewire-audio-capture-apps.c +@@ -0,0 +1,918 @@ ++/* pipewire-audio-capture-app.c + * + * Copyright 2022 Dimitris Papaioannou + * @@ -108,7 +108,7 @@ index 000000000000..e14677367bf8 + +#include + -+/** Source for capturing applciation audio using PipeWire */ ++/* Source for capturing applciation audio using PipeWire */ + +struct target_node_port { + const char *channel; @@ -123,7 +123,7 @@ index 000000000000..e14677367bf8 + const char *binary; + uint32_t id; + struct spa_list ports; -+ size_t *p_n_targets; ++ uint32_t *p_n_targets; + + struct spa_hook node_listener; + @@ -148,11 +148,17 @@ index 000000000000..e14677367bf8 + uint32_t id; +}; + ++/** 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 ++*/ +struct obs_pw_audio_capture_app { + struct obs_pw_audio_instance pw; + -+ struct obs_pw_audio_stream audio; -+ + /** The app capture sink automatically mixes + * the audio of all the app streams */ + struct { @@ -164,7 +170,7 @@ index 000000000000..e14677367bf8 + struct dstr position; + DARRAY(struct capture_sink_port) ports; + -+ /** Links between app streams and the capture sink */ ++ /* Links between app streams and the capture sink */ + struct spa_list links; + } sink; + @@ -173,19 +179,19 @@ index 000000000000..e14677367bf8 + struct spa_list system_sinks; + struct { + struct obs_pw_audio_default_node_metadata metadata; -+ struct pw_proxy *sink; -+ struct spa_hook sink_listener; -+ struct spa_hook sink_proxy_listener; -+ } default_info; ++ struct pw_proxy *proxy; ++ struct spa_hook node_listener; ++ struct spa_hook proxy_listener; ++ } default_sink; + + struct spa_list targets; -+ size_t n_targets; ++ uint32_t n_targets; + + struct dstr target; + bool except_app; +}; + -+/** System sinks */ ++/* System sinks */ +static void system_sink_destroy_cb(void *data) +{ + struct system_sink *s = data; @@ -212,7 +218,7 @@ index 000000000000..e14677367bf8 +} +/* ------------------------------------------------- */ + -+/** Target nodes and ports */ ++/* Target nodes and ports */ +static void port_destroy_cb(void *data) +{ + struct target_node_port *p = data; @@ -238,14 +244,14 @@ index 000000000000..e14677367bf8 + bfree((void *)node->name); +} + -+static struct target_node_port * -+node_register_port(struct obs_pw_audio_capture_app *pwac, -+ struct target_node *node, uint32_t global_id, -+ const char *channel) ++static struct target_node_port *node_register_port(struct target_node *node, ++ uint32_t global_id, ++ struct pw_registry *registry, ++ const char *channel) +{ -+ struct pw_proxy *port_proxy = -+ pw_registry_bind(pwac->pw.registry, global_id, -+ PW_TYPE_INTERFACE_Port, PW_VERSION_PORT, 0); ++ struct pw_proxy *port_proxy = pw_registry_bind(registry, global_id, ++ PW_TYPE_INTERFACE_Port, ++ PW_VERSION_PORT, 0); + if (!port_proxy) { + return NULL; + } @@ -325,7 +331,7 @@ index 000000000000..e14677367bf8 +} +/* ------------------------------------------------- */ + -+/** App streams <-> Capture sink links */ ++/* App streams <-> Capture sink links */ +static void link_bound_cb(void *data, uint32_t global_id) +{ + struct capture_sink_link *link = data; @@ -346,7 +352,7 @@ index 000000000000..e14677367bf8 + port->id, node_id); + + uint32_t p = 0; -+ if (pwac->sink.channels == 1 && /** Mono capture sink */ ++ if (pwac->sink.channels == 1 && /* Mono capture sink */ + pwac->sink.ports.num >= 1) { + p = pwac->sink.ports.array[0].id; + } else { @@ -381,6 +387,8 @@ index 000000000000..e14677367bf8 + pwac->pw.core, "link-factory", PW_TYPE_INTERFACE_Link, + PW_VERSION_LINK, &link_props->dict, 0); + ++ obs_pw_audio_instance_sync(&pwac->pw); ++ + pw_properties_free(link_props); + + if (!link_proxy) { @@ -397,8 +405,6 @@ index 000000000000..e14677367bf8 + obs_pw_audio_proxied_object_init(&link->obj, link_proxy, + &pwac->sink.links, link_bound_cb, + link_destroy_cb, link); -+ -+ obs_pw_audio_instance_sync(&pwac->pw); +} + +static void link_node_to_sink(struct obs_pw_audio_capture_app *pwac, @@ -412,7 +418,7 @@ index 000000000000..e14677367bf8 +} +/* ------------------------------------------------- */ + -+/** App capture sink */ ++/* App capture sink */ + +/** The app capture sink is created when there + * is info about the system's default sink. @@ -497,7 +503,7 @@ index 000000000000..e14677367bf8 +{ + pwac->except_app = except; + -+ if (target && *target) { ++ if (target) { + dstr_copy(&pwac->target, target); + } + @@ -527,8 +533,7 @@ index 000000000000..e14677367bf8 + 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, -+ "Audio/Sink/Virtual", PW_KEY_NODE_ALWAYS_PROCESS, "true", -+ PW_KEY_NODE_PAUSE_ON_IDLE, "false", PW_KEY_NODE_VIRTUAL, "true", ++ "Audio/Sink/Virtual", PW_KEY_NODE_VIRTUAL, "true", + SPA_KEY_AUDIO_POSITION, position, NULL); + + pw_properties_setf(sink_props, PW_KEY_AUDIO_CHANNELS, "%u", channels); @@ -538,6 +543,8 @@ index 000000000000..e14677367bf8 + PW_VERSION_NODE, + &sink_props->dict, 0); + ++ obs_pw_audio_instance_sync(&pwac->pw); ++ + pw_properties_free(sink_props); + + if (!pwac->sink.proxy) { @@ -554,11 +561,9 @@ index 000000000000..e14677367bf8 + pw_proxy_add_listener(pwac->sink.proxy, &pwac->sink.proxy_listener, + &sink_proxy_events, pwac); + -+ obs_pw_audio_instance_sync(&pwac->pw); -+ + while (pwac->sink.id == SPA_ID_INVALID || + pwac->sink.ports.num != channels) { -+ /** Iterate until the sink is bound and all the ports are registered */ ++ /* Iterate until the sink is bound and all the ports are registered */ + pw_loop_iterate(pw_thread_loop_get_loop(pwac->pw.thread_loop), + -1); + } @@ -571,17 +576,11 @@ index 000000000000..e14677367bf8 + + pwac->sink.autoconnect_targets = true; + -+ if (!pwac->audio.stream) { -+ return true; -+ } -+ -+ if (obs_pw_audio_stream_connect( -+ &pwac->audio, PW_DIRECTION_INPUT, pwac->sink.id, -+ PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS, -+ channels) < 0) { ++ if (obs_pw_audio_stream_connect(&pwac->pw.audio, pwac->sink.id, ++ channels) < 0) { + blog(LOG_WARNING, + "[pipewire] Error connecting stream %p to app capture sink %u", -+ pwac->audio.stream, pwac->sink.id); ++ pwac->pw.audio.stream, pwac->sink.id); + } + + return true; @@ -589,17 +588,18 @@ index 000000000000..e14677367bf8 + +static void destroy_capture_sink(struct obs_pw_audio_capture_app *pwac) +{ -+ /** Links are automatically destroyed by PipeWire */ ++ /* Links are automatically destroyed by PipeWire */ + + if (!pwac->sink.proxy) { + return; + } + -+ if (pwac->audio.stream) { -+ pw_stream_disconnect(pwac->audio.stream); ++ if (pw_stream_get_state(pwac->pw.audio.stream, NULL) != ++ PW_STREAM_STATE_UNCONNECTED) { ++ pw_stream_disconnect(pwac->pw.audio.stream); + } -+ pwac->sink.autoconnect_targets = false; + ++ pwac->sink.autoconnect_targets = false; + pw_proxy_destroy(pwac->sink.proxy); + obs_pw_audio_instance_sync(&pwac->pw); +} @@ -608,13 +608,13 @@ index 000000000000..e14677367bf8 +/* Default system sink */ +static void on_default_sink_info_cb(void *data, const struct pw_node_info *info) +{ -+ struct obs_pw_audio_capture_app *pwac = data; -+ + if ((info->change_mask & PW_NODE_CHANGE_MASK_PROPS) == 0 || + !info->props || !info->props->n_items) { + return; + } + ++ struct obs_pw_audio_capture_app *pwac = data; ++ + /** 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 @@ -631,17 +631,17 @@ index 000000000000..e14677367bf8 + channels = "2"; + position = "FL,FR"; + } else if (astrstri(position, "AUX")) { -+ /** Pro Audio sinks use AUX0,AUX1... and so on as their position (see link above) */ ++ /* Pro Audio sinks use AUX0,AUX1... and so on as their position (see link above) */ + channels = "2"; + position = "FL,FR"; + } + -+ uint32_t c = atoi(channels); ++ uint32_t c = strtoul(channels, NULL, 10); + if (!c) { + return; + } + -+ /** No need to create a new capture sink if the channels are the same */ ++ /* No need to create a new capture sink if the channels are the same */ + if (pwac->sink.channels == c && !dstr_is_empty(&pwac->sink.position) && + dstr_cmp(&pwac->sink.position, position) == 0) { + return; @@ -660,16 +660,19 @@ index 000000000000..e14677367bf8 +static void on_default_sink_proxy_removed_cb(void *data) +{ + struct obs_pw_audio_capture_app *pwac = data; -+ pw_proxy_destroy(pwac->default_info.sink); ++ pw_proxy_destroy(pwac->default_sink.proxy); +} + +static void on_default_sink_proxy_destroy_cb(void *data) +{ + struct obs_pw_audio_capture_app *pwac = data; -+ spa_hook_remove(&pwac->default_info.sink_proxy_listener); -+ spa_zero(pwac->default_info.sink_proxy_listener); ++ spa_hook_remove(&pwac->default_sink.node_listener); ++ spa_zero(pwac->default_sink.node_listener); + -+ pwac->default_info.sink = NULL; ++ spa_hook_remove(&pwac->default_sink.proxy_listener); ++ spa_zero(pwac->default_sink.proxy_listener); ++ ++ pwac->default_sink.proxy = NULL; +} + +static const struct pw_proxy_events default_sink_proxy_events = { @@ -684,7 +687,7 @@ index 000000000000..e14677367bf8 + + blog(LOG_DEBUG, "[pipewire] New default sink %s", name); + -+ /** Find the new default sink and bind to it to get its channel info */ ++ /* Find the new default sink and bind to it to get its channel info */ + struct system_sink *t, *s = NULL; + spa_list_for_each(t, &pwac->system_sinks, obj.link) + { @@ -697,14 +700,14 @@ index 000000000000..e14677367bf8 + return; + } + -+ if (pwac->default_info.sink) { -+ pw_proxy_destroy(pwac->default_info.sink); ++ if (pwac->default_sink.proxy) { ++ pw_proxy_destroy(pwac->default_sink.proxy); + } + -+ pwac->default_info.sink = pw_registry_bind(pwac->pw.registry, s->id, -+ PW_TYPE_INTERFACE_Node, -+ PW_VERSION_NODE, 0); -+ if (!pwac->default_info.sink) { ++ 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) { + if (!pwac->sink.proxy) { + blog(LOG_WARNING, + "[pipewire] Failed to get default sink info, app capture sink defaulting to stereo"); @@ -713,11 +716,11 @@ index 000000000000..e14677367bf8 + return; + } + -+ pw_proxy_add_object_listener(pwac->default_info.sink, -+ &pwac->default_info.sink_listener, ++ pw_proxy_add_object_listener(pwac->default_sink.proxy, ++ &pwac->default_sink.node_listener, + &default_sink_events, pwac); -+ pw_proxy_add_listener(pwac->default_info.sink, -+ &pwac->default_info.sink_proxy_listener, ++ pw_proxy_add_listener(pwac->default_sink.proxy, ++ &pwac->default_sink.proxy_listener, + &default_sink_proxy_events, pwac); +} +/* ------------------------------------------------- */ @@ -744,12 +747,12 @@ index 000000000000..e14677367bf8 + return; + } + -+ uint32_t node_id = atoi(nid); ++ uint32_t node_id = strtoul(nid, NULL, 10); + + if (astrcmpi(dir, "in") == 0 && node_id == pwac->sink.id) { + register_capture_sink_port(pwac, id, chn); + } else if (astrcmpi(dir, "out") == 0) { -+ /** Possibly a target port */ ++ /* Possibly a target port */ + struct target_node *t, *n = NULL; + spa_list_for_each(t, &pwac->targets, obj.link) + { @@ -762,8 +765,8 @@ index 000000000000..e14677367bf8 + return; + } + -+ struct target_node_port *p = -+ node_register_port(pwac, n, id, chn); ++ struct target_node_port *p = node_register_port( ++ n, id, pwac->pw.registry, chn); + + if (p && pwac->sink.autoconnect_targets && + node_is_targeted(pwac, n)) { @@ -779,7 +782,7 @@ index 000000000000..e14677367bf8 + } + + if (strcmp(media_class, "Stream/Output/Audio") == 0) { -+ /** Target node */ ++ /* Target node */ + const char *node_app_name = + spa_dict_lookup(props, PW_KEY_APP_NAME); + @@ -799,7 +802,7 @@ index 000000000000..e14677367bf8 + } + + if (!obs_pw_audio_default_node_metadata_listen( -+ &pwac->default_info.metadata, &pwac->pw, id, true, ++ &pwac->default_sink.metadata, &pwac->pw, id, true, + default_node_cb, pwac) && + !pwac->sink.proxy) { + blog(LOG_WARNING, @@ -822,8 +825,8 @@ index 000000000000..e14677367bf8 + struct obs_pw_audio_capture_app *pwac = + bzalloc(sizeof(struct obs_pw_audio_capture_app)); + -+ if (!obs_pw_audio_instance_init(&pwac->pw)) { -+ pw_thread_loop_lock(pwac->pw.thread_loop); ++ if (!obs_pw_audio_instance_init(&pwac->pw, ®istry_events, pwac, true, ++ false, source)) { + obs_pw_audio_instance_destroy(&pwac->pw); + + bfree(pwac); @@ -840,21 +843,6 @@ index 000000000000..e14677367bf8 + dstr_init_copy(&pwac->target, obs_data_get_string(settings, "Target")); + pwac->except_app = obs_data_get_bool(settings, "ExceptApp"); + -+ pw_thread_loop_lock(pwac->pw.thread_loop); -+ -+ pw_registry_add_listener(pwac->pw.registry, &pwac->pw.registry_listener, -+ ®istry_events, pwac); -+ -+ struct pw_properties *stream_props = -+ obs_pw_audio_stream_properties(true, false); -+ if (obs_pw_audio_stream_init(&pwac->audio, &pwac->pw, stream_props, -+ source)) { -+ blog(LOG_INFO, "[pipewire] Created stream %p", -+ pwac->audio.stream); -+ } else { -+ blog(LOG_WARNING, "[pipewire] Failed to create stream"); -+ } -+ + obs_pw_audio_instance_sync(&pwac->pw); + pw_thread_loop_wait(pwac->pw.thread_loop); + pw_thread_loop_unlock(pwac->pw.thread_loop); @@ -901,7 +889,7 @@ index 000000000000..e14677367bf8 + : &node->name); + } + -+ /** Only show one entry per app */ ++ /* Show just one entry per app */ + + qsort(targets_arr.array, targets_arr.num, sizeof(char *), cmp_targets); + @@ -946,19 +934,13 @@ index 000000000000..e14677367bf8 +static void pipewire_audio_capture_app_show(void *data) +{ + struct obs_pw_audio_capture_app *pwac = data; -+ -+ if (pwac->audio.stream) { -+ pw_stream_set_active(pwac->audio.stream, true); -+ } ++ pw_stream_set_active(pwac->pw.audio.stream, true); +} + +static void pipewire_audio_capture_app_hide(void *data) +{ + struct obs_pw_audio_capture_app *pwac = data; -+ -+ if (pwac->audio.stream) { -+ pw_stream_set_active(pwac->audio.stream, false); -+ } ++ pw_stream_set_active(pwac->pw.audio.stream, false); +} + +static void pipewire_audio_capture_app_destroy(void *data) @@ -978,15 +960,13 @@ index 000000000000..e14677367bf8 + pw_proxy_destroy(s->obj.proxy); + } + -+ obs_pw_audio_stream_destroy(&pwac->audio); -+ + destroy_capture_sink(pwac); + -+ if (pwac->default_info.sink) { -+ pw_proxy_destroy(pwac->default_info.sink); ++ if (pwac->default_sink.proxy) { ++ pw_proxy_destroy(pwac->default_sink.proxy); + } -+ if (pwac->default_info.metadata.proxy) { -+ pw_proxy_destroy(pwac->default_info.metadata.proxy); ++ if (pwac->default_sink.metadata.proxy) { ++ pw_proxy_destroy(pwac->default_sink.metadata.proxy); + } + + obs_pw_audio_instance_destroy(&pwac->pw); @@ -1024,11 +1004,11 @@ index 000000000000..e14677367bf8 +} diff --git a/plugins/linux-pipewire/pipewire-audio-capture-device.c b/plugins/linux-pipewire/pipewire-audio-capture-device.c new file mode 100644 -index 000000000000..b493f862e584 +index 0000000000000..b474ddad39fac --- /dev/null +++ b/plugins/linux-pipewire/pipewire-audio-capture-device.c -@@ -0,0 +1,543 @@ -+/* pipewire-audio-capture.c +@@ -0,0 +1,520 @@ ++/* pipewire-audio-capture-device.c + * + * Copyright 2022 Dimitris Papaioannou + * @@ -1052,7 +1032,8 @@ index 000000000000..b493f862e584 + +#include + -+/** Source for capturing device audio using PipeWire */ ++/* Source for capturing device audio using PipeWire */ ++ +enum obs_pw_audio_capture_device_type { + PIPEWIRE_AUDIO_CAPTURE_DEVICE_INPUT, + PIPEWIRE_AUDIO_CAPTURE_DEVICE_OUTPUT, @@ -1078,8 +1059,6 @@ index 000000000000..b493f862e584 + + struct obs_pw_audio_instance pw; + -+ struct obs_pw_audio_stream audio; -+ + struct { + struct obs_pw_audio_default_node_metadata metadata; + bool autoconnect; @@ -1096,35 +1075,33 @@ index 000000000000..b493f862e584 +static void start_streaming(struct obs_pw_audio_capture_device *pwac, + struct target_node *node) +{ -+ if (!pwac->audio.stream || !node || !node->channels) { ++ if (!node || !node->channels) { + return; + } + + dstr_copy(&pwac->target_name, node->name); + -+ if (pw_stream_get_state(pwac->audio.stream, NULL) != ++ if (pw_stream_get_state(pwac->pw.audio.stream, NULL) != + PW_STREAM_STATE_UNCONNECTED) { + if (node->id == pwac->connected_id) { -+ /** Already connected to this node */ ++ /* Already connected to this node */ + return; + } -+ pw_stream_disconnect(pwac->audio.stream); ++ pw_stream_disconnect(pwac->pw.audio.stream); + } + -+ if (obs_pw_audio_stream_connect( -+ &pwac->audio, PW_DIRECTION_INPUT, node->id, -+ PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS, -+ node->channels) == 0) { ++ if (obs_pw_audio_stream_connect(&pwac->pw.audio, node->id, ++ node->channels) == 0) { + pwac->connected_id = node->id; + blog(LOG_INFO, "[pipewire] %p streaming from %u", -+ pwac->audio.stream, node->id); ++ pwac->pw.audio.stream, node->id); + } else { + pwac->connected_id = SPA_ID_INVALID; + blog(LOG_WARNING, "[pipewire] Error connecting stream %p", -+ pwac->audio.stream); ++ pwac->pw.audio.stream); + } + -+ pw_stream_set_active(pwac->audio.stream, ++ pw_stream_set_active(pwac->pw.audio.stream, + obs_source_active(pwac->source)); +} + @@ -1168,7 +1145,7 @@ index 000000000000..b493f862e584 + return; + } + -+ uint32_t c = atoi(channels); ++ uint32_t c = strtoul(channels, NULL, 10); + + struct target_node *n = data; + if (n->channels == c) { @@ -1183,8 +1160,7 @@ index 000000000000..b493f862e584 + 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) || -+ (pwac->audio.stream && -+ pw_stream_get_state(pwac->audio.stream, NULL) == ++ (pw_stream_get_state(pwac->pw.audio.stream, NULL) == + PW_STREAM_STATE_UNCONNECTED && + !dstr_is_empty(&pwac->target_name) && + dstr_cmp(&pwac->target_name, n->name) == 0)) { @@ -1275,7 +1251,7 @@ index 000000000000..b493f862e584 + return; + } + -+ /** Target device */ ++ /* Target device */ + if ((pwac->capture_type == + PIPEWIRE_AUDIO_CAPTURE_DEVICE_INPUT && + (strcmp(media_class, "Audio/Source") == 0 || @@ -1326,7 +1302,7 @@ index 000000000000..b493f862e584 + if (id == pwac->connected_id) { + pwac->connected_id = SPA_ID_INVALID; + -+ pw_stream_disconnect(pwac->audio.stream); ++ pw_stream_disconnect(pwac->pw.audio.stream); + + if (!pwac->default_info.autoconnect && + !dstr_is_empty(&pwac->target_name)) { @@ -1352,8 +1328,10 @@ index 000000000000..b493f862e584 + struct obs_pw_audio_capture_device *pwac = + bzalloc(sizeof(struct obs_pw_audio_capture_device)); + -+ if (!obs_pw_audio_instance_init(&pwac->pw)) { -+ pw_thread_loop_lock(pwac->pw.thread_loop); ++ if (!obs_pw_audio_instance_init( ++ &pwac->pw, ®istry_events, pwac, ++ capture_type == PIPEWIRE_AUDIO_CAPTURE_DEVICE_OUTPUT, true, ++ source)) { + obs_pw_audio_instance_destroy(&pwac->pw); + + bfree(pwac); @@ -1379,21 +1357,6 @@ index 000000000000..b493f862e584 + dstr_init_copy(&pwac->target_name, + obs_data_get_string(settings, "TargetName")); + -+ pw_thread_loop_lock(pwac->pw.thread_loop); -+ -+ pw_registry_add_listener(pwac->pw.registry, &pwac->pw.registry_listener, -+ ®istry_events, pwac); -+ -+ struct pw_properties *props = obs_pw_audio_stream_properties( -+ capture_type == PIPEWIRE_AUDIO_CAPTURE_DEVICE_OUTPUT, true); -+ if (obs_pw_audio_stream_init(&pwac->audio, &pwac->pw, props, -+ pwac->source)) { -+ blog(LOG_INFO, "[pipewire] Created stream %p", -+ pwac->audio.stream); -+ } else { -+ blog(LOG_WARNING, "[pipewire] Failed to create stream"); -+ } -+ + obs_pw_audio_instance_sync(&pwac->pw); + pw_thread_loop_wait(pwac->pw.thread_loop); + pw_thread_loop_unlock(pwac->pw.thread_loop); @@ -1407,6 +1370,7 @@ index 000000000000..b493f862e584 + return pipewire_audio_capture_create( + settings, source, PIPEWIRE_AUDIO_CAPTURE_DEVICE_INPUT); +} ++ +static void *pipewire_audio_capture_output_create(obs_data_t *settings, + obs_source_t *source) +{ @@ -1484,19 +1448,13 @@ index 000000000000..b493f862e584 +static void pipewire_audio_capture_show(void *data) +{ + struct obs_pw_audio_capture_device *pwac = data; -+ -+ if (pwac->audio.stream) { -+ pw_stream_set_active(pwac->audio.stream, true); -+ } ++ pw_stream_set_active(pwac->pw.audio.stream, true); +} + +static void pipewire_audio_capture_hide(void *data) +{ + struct obs_pw_audio_capture_device *pwac = data; -+ -+ if (pwac->audio.stream) { -+ pw_stream_set_active(pwac->audio.stream, false); -+ } ++ pw_stream_set_active(pwac->pw.audio.stream, false); +} + +static void pipewire_audio_capture_destroy(void *data) @@ -1511,8 +1469,6 @@ index 000000000000..b493f862e584 + pw_proxy_destroy(n->obj.proxy); + } + -+ obs_pw_audio_stream_destroy(&pwac->audio); -+ + if (pwac->default_info.metadata.proxy) { + pw_proxy_destroy(pwac->default_info.metadata.proxy); + } @@ -1530,6 +1486,7 @@ index 000000000000..b493f862e584 + UNUSED_PARAMETER(data); + return obs_module_text("PipeWireAudioCaptureInput"); +} ++ +static const char *pipewire_audio_capture_output_name(void *data) +{ + UNUSED_PARAMETER(data); @@ -1573,10 +1530,10 @@ index 000000000000..b493f862e584 +} diff --git a/plugins/linux-pipewire/pipewire-audio.c b/plugins/linux-pipewire/pipewire-audio.c new file mode 100644 -index 000000000000..77a690579f95 +index 0000000000000..ea761221ba10f --- /dev/null +++ b/plugins/linux-pipewire/pipewire-audio.c -@@ -0,0 +1,582 @@ +@@ -0,0 +1,574 @@ +/* pipewire-audio.c + * + * Copyright 2022 Dimitris Papaioannou @@ -1603,10 +1560,10 @@ index 000000000000..77a690579f95 + +#include + -+/** Utilities */ ++/* Utilities */ +bool json_object_find(const char *obj, const char *key, char *value, size_t len) +{ -+ /** From PipeWire's source */ ++ /* From PipeWire's source */ + + struct spa_json it[2]; + const char *v; @@ -1630,99 +1587,9 @@ index 000000000000..77a690579f95 +} +/* ------------------------------------------------- */ + -+/** Common PipeWire components */ -+static void on_core_done_cb(void *data, uint32_t id, int seq) -+{ -+ 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) -+{ -+ 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"); -+ pw_thread_loop_unlock(pw->thread_loop); -+ return false; -+ } -+ -+ pw->core = pw_context_connect(pw->context, NULL, 0); -+ if (!pw->core) { -+ blog(LOG_WARNING, "[pipewire] Error creating PipeWire core"); -+ pw_thread_loop_unlock(pw->thread_loop); -+ 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) { -+ pw_thread_loop_unlock(pw->thread_loop); -+ return false; -+ } -+ -+ pw_thread_loop_unlock(pw->thread_loop); -+ return true; -+} -+ -+void obs_pw_audio_instance_destroy(struct obs_pw_audio_instance *pw) -+{ -+ 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); -+} -+/* ------------------------------------------------- */ -+ +/* PipeWire stream wrapper */ -+void obs_channels_to_spa_audio_position(uint32_t *position, uint32_t channels) ++void obs_channels_to_spa_audio_position(enum spa_audio_channel *position, ++ uint32_t channels) +{ + switch (channels) { + case 1: @@ -1787,6 +1654,14 @@ index 000000000000..77a690579f95 + return AUDIO_FORMAT_32BIT; + case SPA_AUDIO_FORMAT_F32_LE: + return AUDIO_FORMAT_FLOAT; ++ 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; + default: + return AUDIO_FORMAT_UNKNOWN; + } @@ -1820,7 +1695,6 @@ index 000000000000..77a690579f95 + struct spa_audio_info_raw audio_info; + + if (spa_format_audio_raw_parse(param, &audio_info) < 0) { -+ info->frame_size = 0; + info->sample_rate = 0; + info->format = AUDIO_FORMAT_UNKNOWN; + info->speakers = SPEAKERS_UNKNOWN; @@ -1831,8 +1705,6 @@ index 000000000000..77a690579f95 + 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); -+ info->frame_size = -+ get_audio_bytes_per_channel(info->format) * audio_info.channels; + + return true; +} @@ -1851,23 +1723,25 @@ index 000000000000..77a690579f95 + + struct spa_buffer *buf = b->buffer; + -+ void *d = buf->datas[0].data; -+ if (!d || !s->info.frame_size || !s->info.sample_rate || -+ buf->datas[0].type != SPA_DATA_MemPtr) { ++ if (!s->info.sample_rate || buf->datas[0].type != SPA_DATA_MemPtr) { + goto queue; + } + -+ struct obs_source_audio out; -+ out.data[0] = d; -+ out.frames = buf->datas[0].chunk->size / s->info.frame_size; -+ out.speakers = s->info.speakers; -+ out.format = s->info.format; -+ out.samples_per_sec = s->info.sample_rate; ++ 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; ++ } + + 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) -+ * which is used in the linux-jack plugin to correctly set the timestamp ++ * which is used in the linux-jack plugin to correctly set the timestamp + * (https://github.com/obsproject/obs-studio/blob/27.2.4/plugins/linux-jack/jack-wrapper.c#L87) */ + + float period_usecs = @@ -1913,21 +1787,10 @@ index 000000000000..77a690579f95 + s->stream); + } else { + blog(LOG_INFO, -+ "[pipewire] %p Got format: rate %u - channels %u - format %u - frame size %u", ++ "[pipewire] %p Got format: rate %u - channels %u - format %u", + s->stream, s->info.sample_rate, s->info.speakers, -+ s->info.format, s->info.frame_size); ++ s->info.format); + } -+ -+ uint8_t buffer[1024]; -+ 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_ParamIO, SPA_PARAM_IO, SPA_PARAM_IO_id, -+ SPA_POD_Id(SPA_IO_Position), SPA_PARAM_IO_size, -+ SPA_POD_Int(sizeof(struct spa_io_position))); -+ -+ pw_stream_update_params(s->stream, params, 1); +} + +static void on_io_changed_cb(void *data, uint32_t id, void *area, uint32_t size) @@ -1949,40 +1812,10 @@ index 000000000000..77a690579f95 + .io_changed = on_io_changed_cb, +}; + -+bool obs_pw_audio_stream_init(struct obs_pw_audio_stream *s, -+ struct obs_pw_audio_instance *pw, -+ struct pw_properties *props, obs_source_t *output) -+{ -+ s->output = output; -+ s->stream = pw_stream_new(pw->core, "OBS Studio", props); -+ -+ if (!s->stream) { -+ return false; -+ } -+ -+ pw_stream_add_listener(s->stream, &s->stream_listener, &stream_events, -+ s); -+ -+ return true; -+} -+ -+void obs_pw_audio_stream_destroy(struct obs_pw_audio_stream *s) -+{ -+ if (s->stream) { -+ spa_hook_remove(&s->stream_listener); -+ pw_stream_disconnect(s->stream); -+ pw_stream_destroy(s->stream); -+ -+ memset(s, 0, sizeof(struct obs_pw_audio_stream)); -+ } -+} -+ +int obs_pw_audio_stream_connect(struct obs_pw_audio_stream *s, -+ enum spa_direction direction, -+ uint32_t target_id, enum pw_stream_flags flags, -+ uint32_t audio_channels) ++ uint32_t target_id, uint32_t audio_channels) +{ -+ uint32_t pos[8]; ++ enum spa_audio_channel pos[8]; + obs_channels_to_spa_audio_position(pos, audio_channels); + + uint8_t buffer[2048]; @@ -1995,32 +1828,147 @@ index 000000000000..77a690579f95 + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw), + SPA_FORMAT_AUDIO_channels, SPA_POD_Int(audio_channels), + SPA_FORMAT_AUDIO_position, -+ SPA_POD_Array(sizeof(uint32_t), SPA_TYPE_Id, audio_channels, -+ pos), ++ SPA_POD_Array(sizeof(enum spa_audio_channel), SPA_TYPE_Id, ++ audio_channels, pos), + SPA_FORMAT_AUDIO_format, + SPA_POD_CHOICE_ENUM_Id( -+ 4, SPA_AUDIO_FORMAT_U8, SPA_AUDIO_FORMAT_S16_LE, -+ SPA_AUDIO_FORMAT_S32_LE, SPA_AUDIO_FORMAT_F32_LE)); ++ 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)); + -+ return pw_stream_connect(s->stream, direction, target_id, flags, params, -+ 1); ++ 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); ++} ++/* ------------------------------------------------- */ ++ ++/* Common PipeWire components */ ++static void on_core_done_cb(void *data, uint32_t id, int seq) ++{ ++ struct obs_pw_audio_instance *pw = data; ++ ++ if (id == PW_ID_CORE && pw->seq == seq) { ++ pw_thread_loop_signal(pw->thread_loop, false); ++ } +} + -+struct pw_properties *obs_pw_audio_stream_properties(bool capture_sink, -+ bool want_driver) ++static void on_core_error_cb(void *data, uint32_t id, int seq, int res, ++ const char *message) +{ -+ return 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, -+ want_driver ? "true" : "false", PW_KEY_STREAM_CAPTURE_SINK, -+ capture_sink ? "true" : "false", NULL); ++ 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); +} +/* ------------------------------------------------- */ + +/* PipeWire metadata */ -+ +static int on_metadata_property_cb(void *data, uint32_t id, const char *key, + const char *type, const char *value) +{ @@ -2106,7 +2054,7 @@ index 000000000000..77a690579f95 +} +/* ------------------------------------------------- */ + -+/** Proxied objects */ ++/* Proxied objects */ +static void on_proxy_bound_cb(void *data, uint32_t global_id) +{ + struct obs_pw_audio_proxied_object *obj = data; @@ -2155,16 +2103,17 @@ index 000000000000..77a690579f95 + + spa_list_append(list, &obj->link); + ++ spa_zero(obj->proxy_listener); + 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 -index 000000000000..fc196826f46d +index 0000000000000..316276df75e54 --- /dev/null +++ b/plugins/linux-pipewire/pipewire-audio.h -@@ -0,0 +1,172 @@ +@@ -0,0 +1,155 @@ +/* pipewire-audio.h + * + * Copyright 2022 Dimitris Papaioannou @@ -2185,7 +2134,7 @@ index 000000000000..fc196826f46d + * SPDX-License-Identifier: GPL-2.0-or-later + */ + -+/** Stuff used by the PipeWire audio capture sources */ ++/* Stuff used by the PipeWire audio capture sources */ + +#pragma once + @@ -2195,46 +2144,12 @@ index 000000000000..fc196826f46d +#include +#include + -+/** -+ * Common PipeWire components -+ */ -+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; -+}; -+ -+/** -+ * Initialize a PipeWire instance -+ * @return true on success, false on error -+ */ -+bool obs_pw_audio_instance_init(struct obs_pw_audio_instance *pw); -+ -+/** -+ * Destroy a PipeWire instance -+ * @warning Call with the thread loop locked -+ */ -+void obs_pw_audio_instance_destroy(struct obs_pw_audio_instance *pw); -+ -+/** -+ * Trigger a PipeWire core sync -+ */ -+void obs_pw_audio_instance_sync(struct obs_pw_audio_instance *pw); -+/* ------------------------------------------------- */ -+ +/* PipeWire Stream wrapper */ + +/** + * Audio metadata + */ +struct obs_pw_audio_info { -+ uint32_t frame_size; + uint32_t sample_rate; + enum audio_format format; + enum speaker_layout speakers; @@ -2253,33 +2168,51 @@ index 000000000000..fc196826f46d +}; + +/** -+ * Initialize a stream -+ * @return true on success, false on error -+ */ -+bool obs_pw_audio_stream_init(struct obs_pw_audio_stream *s, -+ struct obs_pw_audio_instance *pw, -+ struct pw_properties *props, -+ obs_source_t *output); -+ -+/** -+ * Destroy a stream -+ */ -+void obs_pw_audio_stream_destroy(struct obs_pw_audio_stream *s); -+ -+/** + * 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, -+ enum spa_direction direction, -+ uint32_t target_id, enum pw_stream_flags flags, -+ uint32_t channels); ++ uint32_t target_id, uint32_t channels); ++/* ------------------------------------------------- */ + +/** -+ * Default PipeWire stream properties ++ * Common PipeWire components + */ -+struct pw_properties *obs_pw_audio_stream_properties(bool capture_sink, -+ bool want_driver); ++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; ++}; ++ ++/** ++ * Initialize a PipeWire instance ++ * @warning The thread loop is left locked ++ * @return true on success, false on error ++ */ ++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); ++ ++/** ++ * Destroy a PipeWire instance ++ * @warning Call with the thread loop locked ++ */ ++void obs_pw_audio_instance_destroy(struct obs_pw_audio_instance *pw); ++ ++/** ++ * Trigger a PipeWire core sync ++ */ ++void obs_pw_audio_instance_sync(struct obs_pw_audio_instance *pw); +/* ------------------------------------------------- */ + +/** @@ -2333,8 +2266,6 @@ index 000000000000..fc196826f46d +/* ------------------------------------------------- */ + +/* Sources */ -+ +void pipewire_audio_capture_load(void); +void pipewire_audio_capture_app_load(void); +/* ------------------------------------------------- */ - diff --git a/patches/8293.patch b/patches/8293.patch index 9933adc..cda637d 100644 --- a/patches/8293.patch +++ b/patches/8293.patch @@ -1,7 +1,23 @@ -diff '--color=auto' -ru a/plugins/linux-pipewire/pipewire.c b/plugins/linux-pipewire/pipewire.c ---- a/plugins/linux-pipewire/pipewire.c 2023-02-04 13:17:10.000000000 +0300 -+++ b/plugins/linux-pipewire/pipewire.c 2023-03-04 20:51:36.512199751 +0300 -@@ -125,6 +125,41 @@ +From 037f33d27d521202f31e4590a7594e92f9767823 Mon Sep 17 00:00:00 2001 +From: columbarius +Date: Sat, 21 Jan 2023 08:45:38 +0100 +Subject: [PATCH] pipewire: calc spa buffer size + +Modern GPUs might support a lot of modifiers. As such the hardcoded size +for the ENUM_Formats PipeWire params are not enough and we should +calculate the required size manually. + +Added logs if a ENUM_Format can't be assembled, which happens when the +buffer size was not large enough. +--- + plugins/linux-pipewire/pipewire.c | 47 +++++++++++++++++++++++++++++-- + 1 file changed, 45 insertions(+), 2 deletions(-) + +diff --git a/plugins/linux-pipewire/pipewire.c b/plugins/linux-pipewire/pipewire.c +index ada92cfe70007..43af0e7c7ef2a 100644 +--- a/plugins/linux-pipewire/pipewire.c ++++ b/plugins/linux-pipewire/pipewire.c +@@ -144,6 +144,41 @@ static void update_pw_versions(obs_pipewire *obs_pw, const char *version) blog(LOG_WARNING, "[pipewire] failed to parse server version"); } @@ -40,10 +56,10 @@ diff '--color=auto' -ru a/plugins/linux-pipewire/pipewire.c b/plugins/linux-pipe + return size; +} + - static void teardown_pipewire(obs_pipewire_data *obs_pw) + static void teardown_pipewire(obs_pipewire *obs_pw) { if (obs_pw->thread_loop) { -@@ -319,6 +354,10 @@ +@@ -408,6 +443,10 @@ static bool build_format_params(obs_pipewire *obs_pw, obs_pw->format_info.array[i].spa_format, obs_pw->format_info.array[i].modifiers.array, obs_pw->format_info.array[i].modifiers.num); @@ -54,7 +70,7 @@ diff '--color=auto' -ru a/plugins/linux-pipewire/pipewire.c b/plugins/linux-pipe } build_shm: -@@ -326,6 +365,10 @@ +@@ -415,6 +454,10 @@ static bool build_format_params(obs_pipewire *obs_pw, params[params_count++] = build_format( pod_builder, &obs_pw->video_info, obs_pw->format_info.array[i].spa_format, NULL, 0); @@ -65,7 +81,7 @@ diff '--color=auto' -ru a/plugins/linux-pipewire/pipewire.c b/plugins/linux-pipe } *param_list = params; *n_params = params_count; -@@ -432,7 +475,7 @@ +@@ -521,7 +564,7 @@ static void renegotiate_format(void *data, uint64_t expirations) pw_thread_loop_lock(obs_pw->thread_loop); @@ -74,12 +90,12 @@ diff '--color=auto' -ru a/plugins/linux-pipewire/pipewire.c b/plugins/linux-pipe struct spa_pod_builder pod_builder = SPA_POD_BUILDER_INIT(params_buffer, sizeof(params_buffer)); uint32_t n_params; -@@ -757,7 +800,7 @@ +@@ -967,7 +1010,7 @@ void obs_pipewire_connect_stream(obs_pipewire *obs_pw, int pipewire_node, struct spa_pod_builder pod_builder; const struct spa_pod **params = NULL; uint32_t n_params; - uint8_t params_buffer[2048]; + uint8_t params_buffer[calc_spa_enumformat_buffer_size(obs_pw)]; - obs_pw->thread_loop = pw_thread_loop_new("PipeWire thread loop", NULL); - obs_pw->context = pw_context_new( + pw_thread_loop_lock(obs_pw->thread_loop); +