summaryrefslogtreecommitdiffstats
path: root/src/modules
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/modules/flatpak-utils.h156
-rw-r--r--src/modules/meson.build609
-rw-r--r--src/modules/module-access.c364
-rw-r--r--src/modules/module-adapter.c394
-rw-r--r--src/modules/module-adapter/adapter.c548
-rw-r--r--src/modules/module-adapter/adapter.h48
-rw-r--r--src/modules/module-avb.c129
-rw-r--r--src/modules/module-avb/aaf.h102
-rw-r--r--src/modules/module-avb/acmp.c476
-rw-r--r--src/modules/module-avb/acmp.h99
-rw-r--r--src/modules/module-avb/adp.c381
-rw-r--r--src/modules/module-avb/adp.h105
-rw-r--r--src/modules/module-avb/aecp-aem-descriptors.h247
-rw-r--r--src/modules/module-avb/aecp-aem.c285
-rw-r--r--src/modules/module-avb/aecp-aem.h345
-rw-r--r--src/modules/module-avb/aecp.c168
-rw-r--r--src/modules/module-avb/aecp.h60
-rw-r--r--src/modules/module-avb/avb.c106
-rw-r--r--src/modules/module-avb/avb.h44
-rw-r--r--src/modules/module-avb/avdecc.c335
-rw-r--r--src/modules/module-avb/descriptors.h274
-rw-r--r--src/modules/module-avb/iec61883.h110
-rw-r--r--src/modules/module-avb/internal.h166
-rw-r--r--src/modules/module-avb/maap.c469
-rw-r--r--src/modules/module-avb/maap.h70
-rw-r--r--src/modules/module-avb/mmrp.c233
-rw-r--r--src/modules/module-avb/mmrp.h68
-rw-r--r--src/modules/module-avb/mrp.c612
-rw-r--r--src/modules/module-avb/mrp.h181
-rw-r--r--src/modules/module-avb/msrp.c459
-rw-r--r--src/modules/module-avb/msrp.h134
-rw-r--r--src/modules/module-avb/mvrp.c297
-rw-r--r--src/modules/module-avb/mvrp.h62
-rw-r--r--src/modules/module-avb/packets.h101
-rw-r--r--src/modules/module-avb/srp.c59
-rw-r--r--src/modules/module-avb/srp.h32
-rw-r--r--src/modules/module-avb/stream.c589
-rw-r--r--src/modules/module-avb/stream.h104
-rw-r--r--src/modules/module-avb/utils.h86
-rw-r--r--src/modules/module-client-device.c232
-rw-r--r--src/modules/module-client-device/client-device.h44
-rw-r--r--src/modules/module-client-device/protocol-native.c556
-rw-r--r--src/modules/module-client-device/proxy-device.c90
-rw-r--r--src/modules/module-client-device/resource-device.c155
-rw-r--r--src/modules/module-client-node.c229
-rw-r--r--src/modules/module-client-node/client-node.c1777
-rw-r--r--src/modules/module-client-node/client-node.h60
-rw-r--r--src/modules/module-client-node/protocol-native.c1259
-rw-r--r--src/modules/module-client-node/remote-node.c1339
-rw-r--r--src/modules/module-client-node/v0/client-node.c1447
-rw-r--r--src/modules/module-client-node/v0/client-node.h101
-rw-r--r--src/modules/module-client-node/v0/ext-client-node.h414
-rw-r--r--src/modules/module-client-node/v0/protocol-native.c534
-rw-r--r--src/modules/module-client-node/v0/transport.c262
-rw-r--r--src/modules/module-client-node/v0/transport.h59
-rw-r--r--src/modules/module-combine-stream.c1063
-rw-r--r--src/modules/module-echo-cancel.c1350
-rw-r--r--src/modules/module-example-sink.c498
-rw-r--r--src/modules/module-example-source.c504
-rw-r--r--src/modules/module-fallback-sink.c473
-rw-r--r--src/modules/module-filter-chain.c2411
-rw-r--r--src/modules/module-filter-chain/biquad.c364
-rw-r--r--src/modules/module-filter-chain/biquad.h57
-rw-r--r--src/modules/module-filter-chain/builtin_plugin.c1035
-rw-r--r--src/modules/module-filter-chain/convolver.c425
-rw-r--r--src/modules/module-filter-chain/convolver.h34
-rw-r--r--src/modules/module-filter-chain/dsp-ops-avx.c85
-rw-r--r--src/modules/module-filter-chain/dsp-ops-c.c174
-rw-r--r--src/modules/module-filter-chain/dsp-ops-sse.c142
-rw-r--r--src/modules/module-filter-chain/dsp-ops.c114
-rw-r--r--src/modules/module-filter-chain/dsp-ops.h140
-rw-r--r--src/modules/module-filter-chain/ladspa.h603
-rw-r--r--src/modules/module-filter-chain/ladspa_plugin.c275
-rw-r--r--src/modules/module-filter-chain/lv2_plugin.c522
-rw-r--r--src/modules/module-filter-chain/pffft.c2381
-rw-r--r--src/modules/module-filter-chain/pffft.h181
-rw-r--r--src/modules/module-filter-chain/plugin.h112
-rw-r--r--src/modules/module-link-factory.c565
-rw-r--r--src/modules/module-loopback.c737
-rw-r--r--src/modules/module-metadata.c241
-rw-r--r--src/modules/module-metadata/metadata.c316
-rw-r--r--src/modules/module-metadata/protocol-native.c354
-rw-r--r--src/modules/module-metadata/proxy-metadata.c92
-rw-r--r--src/modules/module-pipe-tunnel.c730
-rw-r--r--src/modules/module-portal.c348
-rw-r--r--src/modules/module-profiler.c463
-rw-r--r--src/modules/module-profiler/protocol-native.c128
-rw-r--r--src/modules/module-protocol-native.c1528
-rw-r--r--src/modules/module-protocol-native/connection.c866
-rw-r--r--src/modules/module-protocol-native/connection.h112
-rw-r--r--src/modules/module-protocol-native/defs.h49
-rw-r--r--src/modules/module-protocol-native/local-socket.c169
-rw-r--r--src/modules/module-protocol-native/portal-screencast.c41
-rw-r--r--src/modules/module-protocol-native/protocol-footer.c152
-rw-r--r--src/modules/module-protocol-native/protocol-footer.h59
-rw-r--r--src/modules/module-protocol-native/protocol-native.c2236
-rw-r--r--src/modules/module-protocol-native/test-connection.c225
-rw-r--r--src/modules/module-protocol-native/v0/interfaces.h534
-rw-r--r--src/modules/module-protocol-native/v0/protocol-native.c1371
-rw-r--r--src/modules/module-protocol-native/v0/typemap.h282
-rw-r--r--src/modules/module-protocol-pulse.c387
-rw-r--r--src/modules/module-protocol-pulse/client.c390
-rw-r--r--src/modules/module-protocol-pulse/client.h136
-rw-r--r--src/modules/module-protocol-pulse/cmd.c136
-rw-r--r--src/modules/module-protocol-pulse/cmd.h32
-rw-r--r--src/modules/module-protocol-pulse/collect.c549
-rw-r--r--src/modules/module-protocol-pulse/collect.h166
-rw-r--r--src/modules/module-protocol-pulse/commands.h208
-rw-r--r--src/modules/module-protocol-pulse/dbus-name.c87
-rw-r--r--src/modules/module-protocol-pulse/dbus-name.h33
-rw-r--r--src/modules/module-protocol-pulse/defs.h265
-rw-r--r--src/modules/module-protocol-pulse/extension.c45
-rw-r--r--src/modules/module-protocol-pulse/extension.h47
-rw-r--r--src/modules/module-protocol-pulse/extensions/ext-device-manager.c8
-rw-r--r--src/modules/module-protocol-pulse/extensions/ext-device-restore.c340
-rw-r--r--src/modules/module-protocol-pulse/extensions/ext-stream-restore.c330
-rw-r--r--src/modules/module-protocol-pulse/extensions/registry.h13
-rw-r--r--src/modules/module-protocol-pulse/format.c861
-rw-r--r--src/modules/module-protocol-pulse/format.h233
-rw-r--r--src/modules/module-protocol-pulse/internal.h108
-rw-r--r--src/modules/module-protocol-pulse/log.h34
-rw-r--r--src/modules/module-protocol-pulse/manager.c1028
-rw-r--r--src/modules/module-protocol-pulse/manager.h145
-rw-r--r--src/modules/module-protocol-pulse/message-handler.c143
-rw-r--r--src/modules/module-protocol-pulse/message-handler.h8
-rw-r--r--src/modules/module-protocol-pulse/message.c879
-rw-r--r--src/modules/module-protocol-pulse/message.h75
-rw-r--r--src/modules/module-protocol-pulse/module.c333
-rw-r--r--src/modules/module-protocol-pulse/module.h94
-rw-r--r--src/modules/module-protocol-pulse/modules/module-always-sink.c122
-rw-r--r--src/modules/module-protocol-pulse/modules/module-combine-sink.c341
-rw-r--r--src/modules/module-protocol-pulse/modules/module-echo-cancel.c276
-rw-r--r--src/modules/module-protocol-pulse/modules/module-gsettings.c298
-rw-r--r--src/modules/module-protocol-pulse/modules/module-ladspa-sink.c257
-rw-r--r--src/modules/module-protocol-pulse/modules/module-ladspa-source.c265
-rw-r--r--src/modules/module-protocol-pulse/modules/module-loopback.c245
-rw-r--r--src/modules/module-protocol-pulse/modules/module-native-protocol-tcp.c127
-rw-r--r--src/modules/module-protocol-pulse/modules/module-null-sink.c228
-rw-r--r--src/modules/module-protocol-pulse/modules/module-pipe-sink.c210
-rw-r--r--src/modules/module-protocol-pulse/modules/module-pipe-source.c210
-rw-r--r--src/modules/module-protocol-pulse/modules/module-raop-discover.c112
-rw-r--r--src/modules/module-protocol-pulse/modules/module-remap-sink.c253
-rw-r--r--src/modules/module-protocol-pulse/modules/module-remap-source.c260
-rw-r--r--src/modules/module-protocol-pulse/modules/module-roc-sink-input.c201
-rw-r--r--src/modules/module-protocol-pulse/modules/module-roc-sink.c197
-rw-r--r--src/modules/module-protocol-pulse/modules/module-roc-source.c206
-rw-r--r--src/modules/module-protocol-pulse/modules/module-rtp-recv.c164
-rw-r--r--src/modules/module-protocol-pulse/modules/module-rtp-send.c224
-rw-r--r--src/modules/module-protocol-pulse/modules/module-simple-protocol-tcp.c210
-rw-r--r--src/modules/module-protocol-pulse/modules/module-switch-on-connect.c291
-rw-r--r--src/modules/module-protocol-pulse/modules/module-tunnel-sink.c230
-rw-r--r--src/modules/module-protocol-pulse/modules/module-tunnel-source.c220
-rw-r--r--src/modules/module-protocol-pulse/modules/module-x11-bell.c130
-rw-r--r--src/modules/module-protocol-pulse/modules/module-zeroconf-discover.c133
-rw-r--r--src/modules/module-protocol-pulse/modules/module-zeroconf-publish.c746
-rw-r--r--src/modules/module-protocol-pulse/operation.c92
-rw-r--r--src/modules/module-protocol-pulse/operation.h50
-rw-r--r--src/modules/module-protocol-pulse/pending-sample.c50
-rw-r--r--src/modules/module-protocol-pulse/pending-sample.h48
-rw-r--r--src/modules/module-protocol-pulse/pulse-server.c5689
-rw-r--r--src/modules/module-protocol-pulse/pulse-server.h56
-rw-r--r--src/modules/module-protocol-pulse/quirks.c75
-rw-r--r--src/modules/module-protocol-pulse/quirks.h36
-rw-r--r--src/modules/module-protocol-pulse/remap.c58
-rw-r--r--src/modules/module-protocol-pulse/remap.h52
-rw-r--r--src/modules/module-protocol-pulse/reply.c84
-rw-r--r--src/modules/module-protocol-pulse/reply.h42
-rw-r--r--src/modules/module-protocol-pulse/sample-play.c211
-rw-r--r--src/modules/module-protocol-pulse/sample-play.h76
-rw-r--r--src/modules/module-protocol-pulse/sample.c50
-rw-r--r--src/modules/module-protocol-pulse/sample.h61
-rw-r--r--src/modules/module-protocol-pulse/server.c1087
-rw-r--r--src/modules/module-protocol-pulse/server.h60
-rw-r--r--src/modules/module-protocol-pulse/stream.c428
-rw-r--r--src/modules/module-protocol-pulse/stream.h141
-rw-r--r--src/modules/module-protocol-pulse/utils.c207
-rw-r--r--src/modules/module-protocol-pulse/utils.h40
-rw-r--r--src/modules/module-protocol-pulse/volume.c114
-rw-r--r--src/modules/module-protocol-pulse/volume.h85
-rw-r--r--src/modules/module-protocol-simple.c911
-rw-r--r--src/modules/module-pulse-tunnel.c1080
-rw-r--r--src/modules/module-raop-discover.c547
-rw-r--r--src/modules/module-raop-sink.c1848
-rw-r--r--src/modules/module-raop/rtsp-client.c631
-rw-r--r--src/modules/module-raop/rtsp-client.h92
-rw-r--r--src/modules/module-roc-sink.c514
-rw-r--r--src/modules/module-roc-source.c546
-rw-r--r--src/modules/module-roc/common.h71
-rw-r--r--src/modules/module-rt.c1095
-rw-r--r--src/modules/module-rtp-sink.c975
-rw-r--r--src/modules/module-rtp-source.c1200
-rw-r--r--src/modules/module-rtp/rtp.h78
-rw-r--r--src/modules/module-rtp/sap.h58
-rw-r--r--src/modules/module-session-manager.c74
-rw-r--r--src/modules/module-session-manager/client-endpoint/client-endpoint.c296
-rw-r--r--src/modules/module-session-manager/client-endpoint/client-endpoint.h62
-rw-r--r--src/modules/module-session-manager/client-endpoint/endpoint-stream.c352
-rw-r--r--src/modules/module-session-manager/client-endpoint/endpoint-stream.h64
-rw-r--r--src/modules/module-session-manager/client-endpoint/endpoint.c385
-rw-r--r--src/modules/module-session-manager/client-endpoint/endpoint.h61
-rw-r--r--src/modules/module-session-manager/client-session/client-session.c295
-rw-r--r--src/modules/module-session-manager/client-session/client-session.h62
-rw-r--r--src/modules/module-session-manager/client-session/endpoint-link.c369
-rw-r--r--src/modules/module-session-manager/client-session/endpoint-link.h65
-rw-r--r--src/modules/module-session-manager/client-session/session.c344
-rw-r--r--src/modules/module-session-manager/client-session/session.h62
-rw-r--r--src/modules/module-session-manager/endpoint-link.c590
-rw-r--r--src/modules/module-session-manager/endpoint-stream.c581
-rw-r--r--src/modules/module-session-manager/endpoint.c590
-rw-r--r--src/modules/module-session-manager/protocol-native.c3083
-rw-r--r--src/modules/module-session-manager/proxy-session-manager.c188
-rw-r--r--src/modules/module-session-manager/session.c578
-rw-r--r--src/modules/module-x11-bell.c364
-rw-r--r--src/modules/module-zeroconf-discover.c565
-rw-r--r--src/modules/module-zeroconf-discover/avahi-poll.c201
-rw-r--r--src/modules/module-zeroconf-discover/avahi-poll.h31
-rw-r--r--src/modules/spa/meson.build31
-rw-r--r--src/modules/spa/module-device-factory.c281
-rw-r--r--src/modules/spa/module-device.c127
-rw-r--r--src/modules/spa/module-node-factory.c280
-rw-r--r--src/modules/spa/module-node.c129
-rw-r--r--src/modules/spa/spa-device.c161
-rw-r--r--src/modules/spa/spa-device.h62
-rw-r--r--src/modules/spa/spa-node.c292
-rw-r--r--src/modules/spa/spa-node.h63
225 files changed, 84701 insertions, 0 deletions
diff --git a/src/modules/flatpak-utils.h b/src/modules/flatpak-utils.h
new file mode 100644
index 0000000..8952ac4
--- /dev/null
+++ b/src/modules/flatpak-utils.h
@@ -0,0 +1,156 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef FLATPAK_UTILS_H
+#define FLATPAK_UTILS_H
+
+#include <stdio.h>
+#include <string.h>
+#include <fcntl.h>
+#include <sys/mman.h>
+#include <sys/stat.h>
+#ifdef HAVE_SYS_VFS_H
+#include <sys/vfs.h>
+#endif
+
+#include "config.h"
+
+#ifdef HAVE_GLIB2
+#include <glib.h>
+#endif
+
+#include <spa/utils/result.h>
+#include <pipewire/log.h>
+
+static int pw_check_flatpak_parse_metadata(const char *buf, size_t size, char **app_id, char **devices)
+{
+#ifdef HAVE_GLIB2
+ /*
+ * See flatpak-metadata(5)
+ *
+ * The .flatpak-info file is in GLib key_file .ini format.
+ */
+ g_autoptr(GKeyFile) metadata = NULL;
+ char *s;
+
+ metadata = g_key_file_new();
+ if (!g_key_file_load_from_data(metadata, buf, size, G_KEY_FILE_NONE, NULL))
+ return -EINVAL;
+
+ if (app_id) {
+ s = g_key_file_get_value(metadata, "Application", "name", NULL);
+ *app_id = s ? strdup(s) : NULL;
+ g_free(s);
+ }
+
+ if (devices) {
+ s = g_key_file_get_value(metadata, "Context", "devices", NULL);
+ *devices = s ? strdup(s) : NULL;
+ g_free(s);
+ }
+
+ return 0;
+#else
+ return -ENOTSUP;
+#endif
+}
+
+static int pw_check_flatpak(pid_t pid, char **app_id, char **devices)
+{
+#if defined(__linux__)
+ char root_path[2048];
+ int root_fd, info_fd, res;
+ struct stat stat_buf;
+
+ if (app_id)
+ *app_id = NULL;
+ if (devices)
+ *devices = NULL;
+
+ snprintf(root_path, sizeof(root_path), "/proc/%d/root", (int)pid);
+ root_fd = openat (AT_FDCWD, root_path, O_RDONLY | O_NONBLOCK | O_DIRECTORY | O_CLOEXEC | O_NOCTTY);
+ if (root_fd == -1) {
+ res = -errno;
+ if (res == -EACCES) {
+ struct statfs buf;
+ /* Access to the root dir isn't allowed. This can happen if the root is on a fuse
+ * filesystem, such as in a toolbox container. We will never have a fuse rootfs
+ * in the flatpak case, so in that case its safe to ignore this and
+ * continue to detect other types of apps. */
+ if (statfs(root_path, &buf) == 0 &&
+ buf.f_type == 0x65735546) /* FUSE_SUPER_MAGIC */
+ return 0;
+ }
+ /* Not able to open the root dir shouldn't happen. Probably the app died and
+ * we're failing due to /proc/$pid not existing. In that case fail instead
+ * of treating this as privileged. */
+ pw_log_info("failed to open \"%s\": %s", root_path, spa_strerror(res));
+ return res;
+ }
+ info_fd = openat (root_fd, ".flatpak-info", O_RDONLY | O_CLOEXEC | O_NOCTTY);
+ close (root_fd);
+ if (info_fd == -1) {
+ if (errno == ENOENT) {
+ pw_log_debug("no .flatpak-info, client on the host");
+ /* No file => on the host */
+ return 0;
+ }
+ res = -errno;
+ pw_log_error("error opening .flatpak-info: %m");
+ return res;
+ }
+ if (fstat (info_fd, &stat_buf) != 0 || !S_ISREG (stat_buf.st_mode)) {
+ /* Some weird fd => failure, assume sandboxed */
+ pw_log_error("error fstat .flatpak-info: %m");
+ } else if (app_id || devices) {
+ /* Parse the application ID if needed */
+ const size_t size = stat_buf.st_size;
+
+ if (size > 0) {
+ void *buf = mmap(NULL, size, PROT_READ, MAP_PRIVATE, info_fd, 0);
+ if (buf != MAP_FAILED) {
+ res = pw_check_flatpak_parse_metadata(buf, size, app_id, devices);
+ munmap(buf, size);
+ } else {
+ res = -errno;
+ }
+ } else {
+ res = -EINVAL;
+ }
+
+ if (res == -EINVAL)
+ pw_log_error("PID %d .flatpak-info file is malformed",
+ (int)pid);
+ else if (res < 0)
+ pw_log_error("PID %d .flatpak-info parsing failed: %s",
+ (int)pid, spa_strerror(res));
+ }
+ close(info_fd);
+ return 1;
+#else
+ return 0;
+#endif
+}
+
+#endif /* FLATPAK_UTILS_H */
diff --git a/src/modules/meson.build b/src/modules/meson.build
new file mode 100644
index 0000000..89e4233
--- /dev/null
+++ b/src/modules/meson.build
@@ -0,0 +1,609 @@
+subdir('spa')
+
+# The list of "main" source files for modules, the ones that have the
+# doxygen documentation
+module_sources = [
+ 'module-access.c',
+ 'module-adapter.c',
+ 'module-avb.c',
+ 'module-client-device.c',
+ 'module-client-node.c',
+ 'module-combine-stream.c',
+ 'module-echo-cancel.c',
+ 'module-example-sink.c',
+ 'module-example-source.c',
+ 'module-fallback-sink.c',
+ 'module-filter-chain.c',
+ 'module-link-factory.c',
+ 'module-loopback.c',
+ 'module-metadata.c',
+ 'module-pipe-tunnel.c',
+ 'module-portal.c',
+ 'module-profiler.c',
+ 'module-protocol-native.c',
+ 'module-protocol-pulse.c',
+ 'module-protocol-simple.c',
+ 'module-pulse-tunnel.c',
+ 'module-rt.c',
+ 'module-raop-discover.c',
+ 'module-raop-sink.c',
+ 'module-rtp-source.c',
+ 'module-rtp-sink.c',
+ 'module-session-manager.c',
+ 'module-zeroconf-discover.c',
+ 'module-roc-source.c',
+ 'module-roc-sink.c',
+ 'module-x11-bell.c',
+]
+
+pipewire_module_access_deps = [spa_dep, mathlib, dl_lib, pipewire_dep]
+if flatpak_support
+ pipewire_module_access_deps += glib2_dep
+endif
+
+pipewire_module_access = shared_library('pipewire-module-access', [ 'module-access.c' ],
+ include_directories : [configinc],
+ install : true,
+ install_dir : modules_install_dir,
+ install_rpath: modules_install_dir,
+ dependencies : pipewire_module_access_deps
+)
+
+pipewire_module_loopback = shared_library('pipewire-module-loopback',
+ [ 'module-loopback.c' ],
+ include_directories : [configinc],
+ install : true,
+ install_dir : modules_install_dir,
+ install_rpath: modules_install_dir,
+ dependencies : [spa_dep, mathlib, dl_lib, pipewire_dep],
+)
+
+simd_cargs = []
+simd_dependencies = []
+
+if have_sse
+ filter_chain_sse = static_library('filter_chain_sse',
+ ['module-filter-chain/pffft.c',
+ 'module-filter-chain/dsp-ops-sse.c' ],
+ c_args : [sse_args, '-O3', '-DHAVE_SSE'],
+ dependencies : [ spa_dep ],
+ install : false
+ )
+ simd_cargs += ['-DHAVE_SSE']
+ simd_dependencies += filter_chain_sse
+endif
+if have_avx
+ filter_chain_avx = static_library('filter_chain_avx',
+ ['module-filter-chain/dsp-ops-avx.c' ],
+ c_args : [avx_args, fma_args,'-O3', '-DHAVE_AVX'],
+ dependencies : [ spa_dep ],
+ install : false
+ )
+ simd_cargs += ['-DHAVE_AVX']
+ simd_dependencies += filter_chain_avx
+endif
+if have_neon
+ filter_chain_neon = static_library('filter_chain_neon',
+ ['module-filter-chain/pffft.c' ],
+ c_args : [neon_args, '-O3', '-DHAVE_NEON'],
+ dependencies : [ spa_dep ],
+ install : false
+ )
+ simd_cargs += ['-DHAVE_NEON']
+ simd_dependencies += filter_chain_neon
+endif
+
+filter_chain_c = static_library('filter_chain_c',
+ ['module-filter-chain/pffft.c',
+ 'module-filter-chain/dsp-ops.c',
+ 'module-filter-chain/dsp-ops-c.c' ],
+ c_args : [simd_cargs, '-O3', '-DPFFFT_SIMD_DISABLE'],
+ dependencies : [ spa_dep ],
+ install : false
+)
+simd_dependencies += filter_chain_c
+
+filter_chain_sources = [
+ 'module-filter-chain.c',
+ 'module-filter-chain/biquad.c',
+ 'module-filter-chain/ladspa_plugin.c',
+ 'module-filter-chain/builtin_plugin.c',
+ 'module-filter-chain/convolver.c'
+]
+filter_chain_dependencies = [
+ mathlib, dl_lib, pipewire_dep, sndfile_dep, audioconvert_dep
+]
+
+if lilv_lib.found()
+ filter_chain_sources += [
+ 'module-filter-chain/lv2_plugin.c'
+ ]
+ filter_chain_dependencies += [ lilv_lib ]
+endif
+
+
+pipewire_module_filter_chain = shared_library('pipewire-module-filter-chain',
+ filter_chain_sources,
+ include_directories : [configinc],
+ install : true,
+ install_dir : modules_install_dir,
+ install_rpath: modules_install_dir,
+ link_with : simd_dependencies,
+ dependencies : filter_chain_dependencies,
+)
+
+pipewire_module_echo_cancel_sources = [
+ 'module-echo-cancel.c',
+]
+
+pipewire_module_combine_stream = shared_library('pipewire-module-combine-stream',
+ [ 'module-combine-stream.c' ],
+ include_directories : [configinc],
+ install : false,
+ install_dir : modules_install_dir,
+ install_rpath: modules_install_dir,
+ dependencies : [spa_dep, dl_lib, pipewire_dep],
+)
+
+pipewire_module_echo_cancel = shared_library('pipewire-module-echo-cancel',
+ pipewire_module_echo_cancel_sources,
+ include_directories : [configinc],
+ install : true,
+ install_dir : modules_install_dir,
+ install_rpath: modules_install_dir,
+ dependencies : [mathlib, dl_lib, pipewire_dep],
+)
+
+pipewire_module_profiler = shared_library('pipewire-module-profiler',
+ [ 'module-profiler.c',
+ 'module-profiler/protocol-native.c', ],
+ include_directories : [configinc],
+ install : true,
+ install_dir : modules_install_dir,
+ install_rpath: modules_install_dir,
+ dependencies : [spa_dep, mathlib, dl_lib, pipewire_dep],
+)
+
+pipewire_module_rt = shared_library('pipewire-module-rt', [ 'module-rt.c' ],
+ include_directories : [configinc],
+ install : true,
+ install_dir : modules_install_dir,
+ install_rpath: modules_install_dir,
+ dependencies : [dbus_dep, mathlib, dl_lib, pipewire_dep],
+)
+
+build_module_rtkit = dbus_dep.found() and (get_option('legacy-rtkit') == true)
+if build_module_rtkit
+# TODO: This serves as a temporary alias to prevent breaking existing setups
+# while `module-rtkit` is being migrated to `module-rt`
+pipewire_module_rtkit = shared_library('pipewire-module-rtkit', [ 'module-rt.c' ],
+ include_directories : [configinc],
+ install : true,
+ install_dir : modules_install_dir,
+ install_rpath: modules_install_dir,
+ dependencies : [dbus_dep, mathlib, dl_lib, pipewire_dep],
+)
+endif
+summary({'rt': '@0@ RTKit'.format(build_module_rtkit ? 'with' : 'without')}, section: 'Optional Modules')
+
+build_module_portal = dbus_dep.found()
+if build_module_portal
+pipewire_module_portal = shared_library('pipewire-module-portal', [ 'module-portal.c' ],
+ include_directories : [configinc],
+ install : true,
+ install_dir : modules_install_dir,
+ install_rpath: modules_install_dir,
+ dependencies : [dbus_dep, mathlib, dl_lib, pipewire_dep],
+)
+endif
+summary({'portal': build_module_portal}, bool_yn: true, section: 'Optional Modules')
+
+pipewire_module_client_device = shared_library('pipewire-module-client-device',
+ [ 'module-client-device.c',
+ 'module-client-device/resource-device.c',
+ 'module-client-device/proxy-device.c',
+ 'module-client-device/protocol-native.c', ],
+ include_directories : [configinc],
+ install : true,
+ install_dir : modules_install_dir,
+ install_rpath: modules_install_dir,
+ dependencies : [spa_dep, mathlib, dl_lib, pipewire_dep],
+)
+
+pipewire_module_link_factory = shared_library('pipewire-module-link-factory',
+ [ 'module-link-factory.c' ],
+ include_directories : [configinc],
+ install : true,
+ install_dir : modules_install_dir,
+ install_rpath: modules_install_dir,
+ dependencies : [spa_dep, mathlib, dl_lib, pipewire_dep],
+)
+
+pipewire_module_protocol_deps = [mathlib, dl_lib, pipewire_dep]
+
+if systemd_dep.found()
+ pipewire_module_protocol_deps += systemd_dep
+endif
+
+pipewire_module_protocol_native = shared_library('pipewire-module-protocol-native',
+ [ 'module-protocol-native.c',
+ 'module-protocol-native/local-socket.c',
+ 'module-protocol-native/portal-screencast.c',
+ 'module-protocol-native/protocol-native.c',
+ 'module-protocol-native/v0/protocol-native.c',
+ 'module-protocol-native/protocol-footer.c',
+ 'module-protocol-native/connection.c' ],
+ include_directories : [configinc],
+ install : true,
+ install_dir : modules_install_dir,
+ install_rpath: modules_install_dir,
+ dependencies : pipewire_module_protocol_deps,
+)
+
+pipewire_module_protocol_pulse_deps = pipewire_module_protocol_deps
+
+pipewire_module_protocol_pulse_sources = [
+ 'module-protocol-pulse.c',
+ 'module-protocol-pulse/client.c',
+ 'module-protocol-pulse/collect.c',
+ 'module-protocol-pulse/cmd.c',
+ 'module-protocol-pulse/extension.c',
+ 'module-protocol-pulse/extensions/ext-device-manager.c',
+ 'module-protocol-pulse/extensions/ext-device-restore.c',
+ 'module-protocol-pulse/extensions/ext-stream-restore.c',
+ 'module-protocol-pulse/format.c',
+ 'module-protocol-pulse/manager.c',
+ 'module-protocol-pulse/message.c',
+ 'module-protocol-pulse/message-handler.c',
+ 'module-protocol-pulse/module.c',
+ 'module-protocol-pulse/operation.c',
+ 'module-protocol-pulse/pending-sample.c',
+ 'module-protocol-pulse/pulse-server.c',
+ 'module-protocol-pulse/quirks.c',
+ 'module-protocol-pulse/remap.c',
+ 'module-protocol-pulse/reply.c',
+ 'module-protocol-pulse/sample.c',
+ 'module-protocol-pulse/sample-play.c',
+ 'module-protocol-pulse/server.c',
+ 'module-protocol-pulse/stream.c',
+ 'module-protocol-pulse/utils.c',
+ 'module-protocol-pulse/volume.c',
+ 'module-protocol-pulse/modules/module-always-sink.c',
+ 'module-protocol-pulse/modules/module-combine-sink.c',
+ 'module-protocol-pulse/modules/module-echo-cancel.c',
+ 'module-protocol-pulse/modules/module-ladspa-sink.c',
+ 'module-protocol-pulse/modules/module-ladspa-source.c',
+ 'module-protocol-pulse/modules/module-loopback.c',
+ 'module-protocol-pulse/modules/module-native-protocol-tcp.c',
+ 'module-protocol-pulse/modules/module-null-sink.c',
+ 'module-protocol-pulse/modules/module-pipe-source.c',
+ 'module-protocol-pulse/modules/module-pipe-sink.c',
+ 'module-protocol-pulse/modules/module-raop-discover.c',
+ 'module-protocol-pulse/modules/module-remap-sink.c',
+ 'module-protocol-pulse/modules/module-remap-source.c',
+ 'module-protocol-pulse/modules/module-roc-sink.c',
+ 'module-protocol-pulse/modules/module-roc-sink-input.c',
+ 'module-protocol-pulse/modules/module-roc-source.c',
+ 'module-protocol-pulse/modules/module-rtp-recv.c',
+ 'module-protocol-pulse/modules/module-rtp-send.c',
+ 'module-protocol-pulse/modules/module-simple-protocol-tcp.c',
+ 'module-protocol-pulse/modules/module-switch-on-connect.c',
+ 'module-protocol-pulse/modules/module-tunnel-sink.c',
+ 'module-protocol-pulse/modules/module-tunnel-source.c',
+ 'module-protocol-pulse/modules/module-x11-bell.c',
+ 'module-protocol-pulse/modules/module-zeroconf-discover.c',
+]
+
+if dbus_dep.found()
+ pipewire_module_protocol_pulse_sources += [
+ 'module-protocol-pulse/dbus-name.c',
+ ]
+ pipewire_module_protocol_pulse_deps += dbus_dep
+endif
+
+if avahi_dep.found()
+ pipewire_module_protocol_pulse_sources += [
+ 'module-protocol-pulse/modules/module-zeroconf-publish.c',
+ 'module-zeroconf-discover/avahi-poll.c',
+ ]
+ pipewire_module_protocol_pulse_deps += avahi_dep
+ cdata.set('HAVE_AVAHI', true)
+endif
+
+if gio_dep.found()
+ pipewire_module_protocol_pulse_sources += [
+ 'module-protocol-pulse/modules/module-gsettings.c',
+ ]
+ pipewire_module_protocol_pulse_deps += gio_dep
+ cdata.set('HAVE_GIO', true)
+endif
+
+if flatpak_support
+ pipewire_module_protocol_pulse_deps += glib2_dep
+endif
+
+pipewire_module_protocol_pulse = shared_library('pipewire-module-protocol-pulse',
+ pipewire_module_protocol_pulse_sources,
+ include_directories : [configinc],
+ install : true,
+ install_dir : modules_install_dir,
+ install_rpath: modules_install_dir,
+ dependencies : pipewire_module_protocol_pulse_deps,
+)
+
+build_module_pulse_tunnel = pulseaudio_dep.found()
+if build_module_pulse_tunnel
+ pipewire_module_pulse_tunnel = shared_library('pipewire-module-pulse-tunnel',
+ [ 'module-pulse-tunnel.c',
+ 'module-protocol-pulse/format.c' ],
+ include_directories : [configinc],
+ install : true,
+ install_dir : modules_install_dir,
+ install_rpath: modules_install_dir,
+ dependencies : [mathlib, dl_lib, pipewire_dep, pulseaudio_dep],
+)
+endif
+summary({'pulse-tunnel': build_module_pulse_tunnel}, bool_yn: true, section: 'Optional Modules')
+
+pipewire_module_pipe_tunnel = shared_library('pipewire-module-pipe-tunnel',
+ [ 'module-pipe-tunnel.c' ],
+ include_directories : [configinc],
+ install : true,
+ install_dir : modules_install_dir,
+ install_rpath: modules_install_dir,
+ dependencies : [mathlib, dl_lib, pipewire_dep],
+)
+
+pipewire_module_protocol_simple = shared_library('pipewire-module-protocol-simple',
+ [ 'module-protocol-simple.c' ],
+ include_directories : [configinc],
+ install : true,
+ install_dir : modules_install_dir,
+ install_rpath: modules_install_dir,
+ dependencies : pipewire_module_protocol_deps,
+)
+
+pipewire_module_example_sink = shared_library('pipewire-module-example-sink',
+ [ 'module-example-sink.c' ],
+ include_directories : [configinc],
+ install : false,
+ install_dir : modules_install_dir,
+ install_rpath: modules_install_dir,
+ dependencies : [spa_dep, mathlib, dl_lib, pipewire_dep],
+)
+
+pipewire_module_example_sink = shared_library('pipewire-module-example-source',
+ [ 'module-example-source.c' ],
+ include_directories : [configinc],
+ install : false,
+ install_dir : modules_install_dir,
+ install_rpath: modules_install_dir,
+ dependencies : [spa_dep, mathlib, dl_lib, pipewire_dep],
+)
+
+pipewire_module_client_node = shared_library('pipewire-module-client-node',
+ [ 'module-client-node.c',
+ 'module-client-node/remote-node.c',
+ 'module-client-node/client-node.c',
+ 'module-client-node/protocol-native.c',
+ 'module-client-node/v0/client-node.c',
+ 'module-client-node/v0/transport.c',
+ 'module-client-node/v0/protocol-native.c',
+ 'spa/spa-node.c', ],
+ include_directories : [configinc],
+ link_with : pipewire_module_protocol_native,
+ install : true,
+ install_dir : modules_install_dir,
+ install_rpath: modules_install_dir,
+ dependencies : [spa_dep, mathlib, dl_lib, pipewire_dep],
+)
+
+pipewire_module_metadata = shared_library('pipewire-module-metadata',
+ [ 'module-metadata.c',
+ 'module-metadata/proxy-metadata.c',
+ 'module-metadata/metadata.c',
+ 'module-metadata/protocol-native.c'],
+ include_directories : [configinc],
+ link_with : pipewire_module_protocol_native,
+ install : true,
+ install_dir : modules_install_dir,
+ install_rpath: modules_install_dir,
+ dependencies : [spa_dep, mathlib, dl_lib, pipewire_dep],
+)
+
+test('pw-test-protocol-native',
+ executable('pw-test-protocol-native',
+ [ 'module-protocol-native/test-connection.c',
+ 'module-protocol-native/connection.c' ],
+ c_args : libpipewire_c_args,
+ include_directories : [configinc ],
+ dependencies : [spa_dep, pipewire_dep],
+ install : installed_tests_enabled,
+ install_dir : installed_tests_execdir,
+ ),
+ env : [
+ 'SPA_PLUGIN_DIR=@0@'.format(spa_dep.get_variable('plugindir')),
+ 'PIPEWIRE_CONFIG_DIR=@0@'.format(pipewire_dep.get_variable('confdatadir')),
+ 'PIPEWIRE_MODULE_DIR=@0@'.format(pipewire_dep.get_variable('moduledir')),
+ ]
+)
+
+if installed_tests_enabled
+ test_conf = configuration_data()
+ test_conf.set('exec', installed_tests_execdir / 'pw-test-protocol-native')
+ configure_file(
+ input: installed_tests_template,
+ output: 'pw-test-protocol-native.test',
+ install_dir: installed_tests_metadir,
+ configuration: test_conf
+ )
+endif
+
+pipewire_module_adapter = shared_library('pipewire-module-adapter',
+ [ 'module-adapter.c',
+ 'module-adapter/adapter.c',
+ 'spa/spa-node.c' ],
+ include_directories : [configinc],
+ install : true,
+ install_dir : modules_install_dir,
+ install_rpath: modules_install_dir,
+ dependencies : [mathlib, dl_lib, rt_lib, pipewire_dep],
+)
+
+pipewire_module_session_manager = shared_library('pipewire-module-session-manager',
+ [ 'module-session-manager.c',
+ 'module-session-manager/client-endpoint/client-endpoint.c',
+ 'module-session-manager/client-endpoint/endpoint-stream.c',
+ 'module-session-manager/client-endpoint/endpoint.c',
+ 'module-session-manager/client-session/client-session.c',
+ 'module-session-manager/client-session/endpoint-link.c',
+ 'module-session-manager/client-session/session.c',
+ 'module-session-manager/endpoint-link.c',
+ 'module-session-manager/endpoint-stream.c',
+ 'module-session-manager/endpoint.c',
+ 'module-session-manager/protocol-native.c',
+ 'module-session-manager/proxy-session-manager.c',
+ 'module-session-manager/session.c',
+ ],
+ include_directories : [configinc],
+ install : true,
+ install_dir : modules_install_dir,
+ install_rpath: modules_install_dir,
+ dependencies : [spa_dep, mathlib, dl_lib, pipewire_dep],
+)
+
+build_module_zeroconf_discover = avahi_dep.found()
+if build_module_zeroconf_discover
+pipewire_module_zeroconf_discover = shared_library('pipewire-module-zeroconf-discover',
+ [ 'module-zeroconf-discover.c',
+ 'module-protocol-pulse/format.c',
+ 'module-zeroconf-discover/avahi-poll.c' ],
+ include_directories : [configinc],
+ install : true,
+ install_dir : modules_install_dir,
+ install_rpath: modules_install_dir,
+ dependencies : [mathlib, dl_lib, rt_lib, pipewire_dep, avahi_dep],
+)
+endif
+summary({'zeroconf-discover': build_module_zeroconf_discover}, bool_yn: true, section: 'Optional Modules')
+
+build_module_raop_discover = avahi_dep.found()
+if build_module_raop_discover
+pipewire_module_raop_discover = shared_library('pipewire-module-raop-discover',
+ [ 'module-raop-discover.c',
+ 'module-zeroconf-discover/avahi-poll.c' ],
+ include_directories : [configinc],
+ install : true,
+ install_dir : modules_install_dir,
+ install_rpath: modules_install_dir,
+ dependencies : [mathlib, dl_lib, rt_lib, pipewire_dep, avahi_dep],
+)
+endif
+summary({'raop-discover (needs Avahi)': build_module_raop_discover}, bool_yn: true, section: 'Optional Modules')
+
+build_module_raop = openssl_lib.found()
+if build_module_raop
+pipewire_module_raop_sink = shared_library('pipewire-module-raop-sink',
+ [ 'module-raop-sink.c',
+ 'module-raop/rtsp-client.c' ],
+ include_directories : [configinc],
+ install : true,
+ install_dir : modules_install_dir,
+ install_rpath: modules_install_dir,
+ dependencies : [mathlib, dl_lib, rt_lib, pipewire_dep, openssl_lib],
+)
+endif
+summary({'raop-sink (requires OpenSSL)': build_module_raop}, bool_yn: true, section: 'Optional Modules')
+
+roc_dep = dependency('roc', required: get_option('roc'))
+summary({'ROC': roc_dep.found()}, bool_yn: true, section: 'Streaming between daemons')
+
+pipewire_module_rtp_source = shared_library('pipewire-module-rtp-source',
+ [ 'module-rtp-source.c' ],
+ include_directories : [configinc],
+ install : true,
+ install_dir : modules_install_dir,
+ install_rpath: modules_install_dir,
+ dependencies : [mathlib, dl_lib, rt_lib, pipewire_dep],
+)
+
+pipewire_module_rtp_sink = shared_library('pipewire-module-rtp-sink',
+ [ 'module-rtp-sink.c' ],
+ include_directories : [configinc],
+ install : true,
+ install_dir : modules_install_dir,
+ install_rpath: modules_install_dir,
+ dependencies : [mathlib, dl_lib, rt_lib, pipewire_dep],
+)
+
+build_module_roc = roc_dep.found()
+if build_module_roc
+pipewire_module_roc_sink = shared_library('pipewire-module-roc-sink',
+ [ 'module-roc-sink.c' ],
+ include_directories : [configinc],
+ install : true,
+ install_dir : modules_install_dir,
+ install_rpath: modules_install_dir,
+ dependencies : [mathlib, dl_lib, rt_lib, pipewire_dep, roc_dep],
+)
+
+pipewire_module_roc_source = shared_library('pipewire-module-roc-source',
+ [ 'module-roc-source.c' ],
+ include_directories : [configinc],
+ install : true,
+ install_dir : modules_install_dir,
+ install_rpath: modules_install_dir,
+ dependencies : [mathlib, dl_lib, rt_lib, pipewire_dep, roc_dep],
+)
+endif
+summary({'roc-sink': build_module_roc}, bool_yn: true, section: 'Optional Modules')
+summary({'roc-source': build_module_roc}, bool_yn: true, section: 'Optional Modules')
+
+build_module_x11_bell = x11_dep.found() and canberra_dep.found()
+if build_module_x11_bell
+pipewire_module_x11_bell = shared_library('pipewire-module-x11-bell',
+ [ 'module-x11-bell.c' ],
+ include_directories : [configinc],
+ install : true,
+ install_dir : modules_install_dir,
+ install_rpath: modules_install_dir,
+ dependencies : [mathlib, dl_lib, rt_lib, pipewire_dep, x11_dep, xfixes_dep, canberra_dep],
+)
+endif
+summary({'x11-bell': build_module_x11_bell}, bool_yn: true, section: 'Optional Modules')
+
+pipewire_module_fallback_sink = shared_library('pipewire-module-fallback-sink',
+ [ 'module-fallback-sink.c' ],
+ include_directories : [configinc],
+ install : true,
+ install_dir : modules_install_dir,
+ install_rpath: modules_install_dir,
+ dependencies : [mathlib, dl_lib, rt_lib, pipewire_dep],
+)
+
+build_module_avb = get_option('avb').require(host_machine.system() == 'linux', error_message: 'AVB support is only available on Linux').allowed()
+if build_module_avb
+pipewire_module_avb = shared_library('pipewire-module-avb',
+ [ 'module-avb.c',
+ 'module-avb/avb.c',
+ 'module-avb/adp.c',
+ 'module-avb/acmp.c',
+ 'module-avb/aecp.c',
+ 'module-avb/aecp-aem.c',
+ 'module-avb/avdecc.c',
+ 'module-avb/maap.c',
+ 'module-avb/mmrp.c',
+ 'module-avb/mrp.c',
+ 'module-avb/msrp.c',
+ 'module-avb/mvrp.c',
+ 'module-avb/srp.c',
+ 'module-avb/stream.c'
+ ],
+ include_directories : [configinc],
+ install : true,
+ install_dir : modules_install_dir,
+ install_rpath: modules_install_dir,
+ dependencies : [mathlib, dl_lib, rt_lib, pipewire_dep],
+)
+endif
+summary({'avb': build_module_avb}, bool_yn: true, section: 'Optional Modules')
diff --git a/src/modules/module-access.c b/src/modules/module-access.c
new file mode 100644
index 0000000..db12ad7
--- /dev/null
+++ b/src/modules/module-access.c
@@ -0,0 +1,364 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+
+#include "config.h"
+
+#ifdef HAVE_SYS_VFS_H
+#include <sys/vfs.h>
+#endif
+#ifdef HAVE_SYS_MOUNT_H
+#include <sys/mount.h>
+#endif
+
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+#include <spa/utils/json.h>
+
+#include <pipewire/impl.h>
+
+#include "flatpak-utils.h"
+
+/** \page page_module_access PipeWire Module: Access
+ *
+ *
+ * The `access` module performs access checks on clients. The access check
+ * is only performed once per client, subsequent checks return the same
+ * resolution.
+ *
+ * Permissions assigned to a client are configured as arguments to this
+ * module, see the example configuration below. A special use-case is Flatpak
+ * where the permission management is delegated.
+ *
+ * This module sets the \ref PW_KEY_ACCESS property to one of
+ * - `allowed`: the client is explicitly allowed to access all resources
+ * - `rejected`: the client does not have access to any resources and a
+ * resource error is generated
+ * - `restricted`: the client is restricted, see note below
+ * - `flatpak`: restricted, special case for clients running inside flatpak,
+ * see note below
+ * - `$access.force`: the value of the `access.force` argument given in the
+ * module configuration.
+ * - `unrestricted`: the client is allowed to access all resources. This is the
+ * default for clients not listed in any of the `access.*` options
+ * unless the client requested reduced permissions in \ref
+ * PW_KEY_CLIENT_ACCESS.
+ *
+ * \note Clients with a resolution other than `allowed` or `rejected` rely
+ * on an external actor to update that property once permission is
+ * granted or rejected.
+ *
+ * For connections from applications running inside Flatpak not mediated
+ * by a portal, the `access` module itself sets the `pipewire.access.portal.app_id`
+ * property to the Flatpak application ID.
+ *
+ * ## Module Options
+ *
+ * Options specific to the behavior of this module
+ *
+ * - ``access.allowed = []``: an array of paths of allowed applications
+ * - ``access.rejected = []``: an array of paths of rejected applications
+ * - ``access.restricted = []``: an array of paths of restricted applications
+ * - ``access.force = <str>``: forces an external permissions check (e.g. a flatpak
+ * portal)
+ *
+ * ## General options
+ *
+ * Options with well-known behavior:
+ *
+ * - \ref PW_KEY_ACCESS
+ * - \ref PW_KEY_CLIENT_ACCESS
+ *
+ * ## Example configuration
+ *
+ *\code{.unparsed}
+ * context.modules = [
+ * { name = libpipewire-module-access
+ * args = {
+ * access.allowed = [
+ * /usr/bin/pipewire-media-session
+ * /usr/bin/important-thing
+ * ]
+ *
+ * access.rejected = [
+ * /usr/bin/microphone-snooper
+ * ]
+ *
+ * #access.restricted = [ ]
+ *
+ * # Anything not in the above lists gets assigned the
+ * # access.force permission.
+ * #access.force = flatpak
+ * }
+ * }
+ *]
+ *\endcode
+ *
+ * \see pw_resource_error
+ * \see pw_impl_client_update_permissions
+ */
+
+#define NAME "access"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+#define MODULE_USAGE "[ access.force=flatpak ] " \
+ "[ access.allowed=<cmd-line> ] " \
+ "[ access.rejected=<cmd-line> ] " \
+ "[ access.restricted=<cmd-line> ] " \
+
+static const struct spa_dict_item module_props[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Perform access check" },
+ { PW_KEY_MODULE_USAGE, MODULE_USAGE },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+struct impl {
+ struct pw_context *context;
+ struct pw_properties *properties;
+
+ struct spa_hook context_listener;
+ struct spa_hook module_listener;
+};
+
+static int check_cmdline(struct pw_impl_client *client, int pid, const char *str)
+{
+ char path[2048], key[1024];
+ ssize_t len;
+ int fd, res;
+ struct spa_json it[2];
+
+ sprintf(path, "/proc/%u/cmdline", pid);
+
+ fd = open(path, O_RDONLY);
+ if (fd < 0) {
+ res = -errno;
+ goto exit;
+ }
+ if ((len = read(fd, path, sizeof(path)-1)) < 0) {
+ res = -errno;
+ goto exit_close;
+ }
+ path[len] = '\0';
+
+ spa_json_init(&it[0], str, strlen(str));
+ if ((res = spa_json_enter_array(&it[0], &it[1])) <= 0)
+ goto exit_close;
+
+ while (spa_json_get_string(&it[1], key, sizeof(key)) > 0) {
+ if (spa_streq(path, key)) {
+ res = 1;
+ goto exit_close;
+ }
+ }
+ res = 0;
+exit_close:
+ close(fd);
+exit:
+ return res;
+}
+
+static void
+context_check_access(void *data, struct pw_impl_client *client)
+{
+ struct impl *impl = data;
+ struct pw_permission permissions[1];
+ struct spa_dict_item items[2];
+ const struct pw_properties *props;
+ const char *str, *access;
+ char *flatpak_app_id = NULL;
+ int nitems = 0;
+ int pid, res;
+
+ pid = -EINVAL;
+ if ((props = pw_impl_client_get_properties(client)) != NULL) {
+ if ((str = pw_properties_get(props, PW_KEY_ACCESS)) != NULL) {
+ pw_log_info("client %p: has already access: '%s'", client, str);
+ return;
+ }
+ pw_properties_fetch_int32(props, PW_KEY_SEC_PID, &pid);
+ }
+
+ if (pid < 0) {
+ pw_log_info("client %p: no trusted pid found, assuming not sandboxed", client);
+ access = "no-pid";
+ goto granted;
+ } else {
+ pw_log_info("client %p has trusted pid %d", client, pid);
+ }
+
+ if (impl->properties && (str = pw_properties_get(impl->properties, "access.allowed")) != NULL) {
+ res = check_cmdline(client, pid, str);
+ if (res < 0) {
+ pw_log_warn("%p: client %p allowed check failed: %s",
+ impl, client, spa_strerror(res));
+ } else if (res > 0) {
+ access = "allowed";
+ goto granted;
+ }
+ }
+
+ if (impl->properties && (str = pw_properties_get(impl->properties, "access.rejected")) != NULL) {
+ res = check_cmdline(client, pid, str);
+ if (res < 0) {
+ pw_log_warn("%p: client %p rejected check failed: %s",
+ impl, client, spa_strerror(res));
+ } else if (res > 0) {
+ res = -EACCES;
+ access = "rejected";
+ goto rejected;
+ }
+ }
+
+ if (impl->properties && (str = pw_properties_get(impl->properties, "access.restricted")) != NULL) {
+ res = check_cmdline(client, pid, str);
+ if (res < 0) {
+ pw_log_warn("%p: client %p restricted check failed: %s",
+ impl, client, spa_strerror(res));
+ }
+ else if (res > 0) {
+ pw_log_debug(" %p: restricted client %p added", impl, client);
+ access = "restricted";
+ goto wait_permissions;
+ }
+ }
+ if (impl->properties &&
+ (access = pw_properties_get(impl->properties, "access.force")) != NULL)
+ goto wait_permissions;
+
+ res = pw_check_flatpak(pid, &flatpak_app_id, NULL);
+ if (res != 0) {
+ if (res < 0) {
+ if (res == -EACCES) {
+ access = "unrestricted";
+ goto granted;
+ }
+ pw_log_warn("%p: client %p sandbox check failed: %s",
+ impl, client, spa_strerror(res));
+ }
+ else if (res > 0) {
+ pw_log_debug(" %p: flatpak client %p added", impl, client);
+ }
+ access = "flatpak";
+ items[nitems++] = SPA_DICT_ITEM_INIT("pipewire.access.portal.app_id",
+ flatpak_app_id);
+ goto wait_permissions;
+ }
+
+ if ((access = pw_properties_get(props, PW_KEY_CLIENT_ACCESS)) == NULL)
+ access = "unrestricted";
+
+ if (spa_streq(access, "unrestricted") || spa_streq(access, "allowed"))
+ goto granted;
+ else
+ goto wait_permissions;
+
+granted:
+ pw_log_info("%p: client %p '%s' access granted", impl, client, access);
+ items[nitems++] = SPA_DICT_ITEM_INIT(PW_KEY_ACCESS, access);
+ pw_impl_client_update_properties(client, &SPA_DICT_INIT(items, nitems));
+
+ permissions[0] = PW_PERMISSION_INIT(PW_ID_ANY, PW_PERM_ALL);
+ pw_impl_client_update_permissions(client, 1, permissions);
+ goto done;
+
+wait_permissions:
+ pw_log_info("%p: client %p wait for '%s' permissions",
+ impl, client, access);
+ items[nitems++] = SPA_DICT_ITEM_INIT(PW_KEY_ACCESS, access);
+ pw_impl_client_update_properties(client, &SPA_DICT_INIT(items, nitems));
+ goto done;
+
+rejected:
+ pw_resource_error(pw_impl_client_get_core_resource(client), res, access);
+ items[nitems++] = SPA_DICT_ITEM_INIT(PW_KEY_ACCESS, access);
+ pw_impl_client_update_properties(client, &SPA_DICT_INIT(items, nitems));
+ goto done;
+
+done:
+ free(flatpak_app_id);
+ return;
+}
+
+static const struct pw_context_events context_events = {
+ PW_VERSION_CONTEXT_EVENTS,
+ .check_access = context_check_access,
+};
+
+static void module_destroy(void *data)
+{
+ struct impl *impl = data;
+
+ spa_hook_remove(&impl->context_listener);
+ spa_hook_remove(&impl->module_listener);
+
+ pw_properties_free(impl->properties);
+
+ free(impl);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy,
+};
+
+SPA_EXPORT
+int pipewire__module_init(struct pw_impl_module *module, const char *args)
+{
+ struct pw_context *context = pw_impl_module_get_context(module);
+ struct pw_properties *props;
+ struct impl *impl;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ impl = calloc(1, sizeof(struct impl));
+ if (impl == NULL)
+ return -errno;
+
+ pw_log_debug("module %p: new %s", impl, args);
+
+ if (args)
+ props = pw_properties_new_string(args);
+ else
+ props = NULL;
+
+ impl->context = context;
+ impl->properties = props;
+
+ pw_context_add_listener(context, &impl->context_listener, &context_events, impl);
+ pw_impl_module_add_listener(module, &impl->module_listener, &module_events, impl);
+
+ pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_props));
+
+ return 0;
+}
diff --git a/src/modules/module-adapter.c b/src/modules/module-adapter.c
new file mode 100644
index 0000000..edc183b
--- /dev/null
+++ b/src/modules/module-adapter.c
@@ -0,0 +1,394 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <errno.h>
+#include <dlfcn.h>
+
+#include "config.h"
+
+#include <spa/node/node.h>
+#include <spa/utils/hook.h>
+#include <spa/utils/result.h>
+
+#include <pipewire/impl.h>
+
+#include "modules/spa/spa-node.h"
+#include "module-adapter/adapter.h"
+
+/** \page page_module_adapter PipeWire Module: Adapter
+ */
+#define NAME "adapter"
+
+PW_LOG_TOPIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+#define FACTORY_USAGE SPA_KEY_FACTORY_NAME"=<factory-name> " \
+ "["SPA_KEY_LIBRARY_NAME"=<library-name>] " \
+ ADAPTER_USAGE
+
+static const struct spa_dict_item module_props[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Manage adapter nodes" },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+struct factory_data {
+ struct pw_impl_factory *factory;
+ struct spa_hook factory_listener;
+
+ struct spa_list node_list;
+
+ struct pw_context *context;
+ struct pw_impl_module *module;
+ struct spa_hook module_listener;
+};
+
+struct node_data {
+ struct factory_data *data;
+ struct spa_list link;
+ struct pw_impl_node *adapter;
+ struct pw_impl_node *follower;
+ struct spa_handle *handle;
+ struct spa_hook adapter_listener;
+ struct pw_resource *resource;
+ struct pw_resource *bound_resource;
+ struct spa_hook resource_listener;
+ uint32_t new_id;
+ unsigned int linger;
+};
+
+static void resource_destroy(void *data)
+{
+ struct node_data *nd = data;
+
+ pw_log_debug("%p: destroy %p", nd, nd->adapter);
+ spa_hook_remove(&nd->resource_listener);
+ nd->bound_resource = NULL;
+ if (nd->adapter && !nd->linger)
+ pw_impl_node_destroy(nd->adapter);
+}
+
+static const struct pw_resource_events resource_events = {
+ PW_VERSION_RESOURCE_EVENTS,
+ .destroy = resource_destroy
+};
+
+static void node_destroy(void *data)
+{
+ struct node_data *nd = data;
+ pw_log_debug("%p: destroy %p", nd, nd->adapter);
+ spa_list_remove(&nd->link);
+ nd->adapter = NULL;
+}
+
+static void node_free(void *data)
+{
+ struct node_data *nd = data;
+
+ pw_log_debug("%p: free %p", nd, nd->follower);
+
+ if (nd->bound_resource != NULL)
+ spa_hook_remove(&nd->resource_listener);
+
+ spa_hook_remove(&nd->adapter_listener);
+
+ if (nd->follower)
+ pw_impl_node_destroy(nd->follower);
+ if (nd->handle)
+ pw_unload_spa_handle(nd->handle);
+}
+
+static void node_initialized(void *data)
+{
+ struct node_data *nd = data;
+ struct pw_impl_client *client;
+ struct pw_resource *bound_resource;
+ struct pw_global *global;
+ int res;
+
+ if (nd->resource == NULL)
+ return;
+
+ client = pw_resource_get_client(nd->resource);
+ global = pw_impl_node_get_global(nd->adapter);
+
+ res = pw_global_bind(global, client,
+ PW_PERM_ALL, PW_VERSION_NODE, nd->new_id);
+ if (res < 0)
+ goto error_bind;
+
+ if ((bound_resource = pw_impl_client_find_resource(client, nd->new_id)) == NULL) {
+ res = -EIO;
+ goto error_bind;
+ }
+
+ nd->bound_resource = bound_resource;
+ pw_resource_add_listener(bound_resource, &nd->resource_listener, &resource_events, nd);
+ return;
+
+error_bind:
+ pw_resource_errorf_id(nd->resource, nd->new_id, res,
+ "can't bind adapter node: %s", spa_strerror(res));
+ return;
+}
+
+
+static const struct pw_impl_node_events node_events = {
+ PW_VERSION_IMPL_NODE_EVENTS,
+ .destroy = node_destroy,
+ .free = node_free,
+ .initialized = node_initialized,
+};
+
+static void *create_object(void *_data,
+ struct pw_resource *resource,
+ const char *type,
+ uint32_t version,
+ struct pw_properties *properties,
+ uint32_t new_id)
+{
+ struct factory_data *d = _data;
+ struct pw_impl_client *client;
+ struct pw_impl_node *adapter, *follower;
+ struct spa_node *spa_follower;
+ const char *str;
+ int res;
+ struct node_data *nd;
+ bool linger, do_register;
+ struct spa_handle *handle = NULL;
+ const struct pw_properties *p;
+
+ if (properties == NULL)
+ goto error_properties;
+
+ pw_properties_setf(properties, PW_KEY_FACTORY_ID, "%d",
+ pw_impl_factory_get_info(d->factory)->id);
+
+ linger = pw_properties_get_bool(properties, PW_KEY_OBJECT_LINGER, false);
+ do_register = pw_properties_get_bool(properties, PW_KEY_OBJECT_REGISTER, true);
+
+ p = pw_context_get_properties(d->context);
+ pw_properties_set(properties, "clock.quantum-limit",
+ pw_properties_get(p, "default.clock.quantum-limit"));
+
+ client = resource ? pw_resource_get_client(resource): NULL;
+ if (client && !linger) {
+ pw_properties_setf(properties, PW_KEY_CLIENT_ID, "%d",
+ pw_impl_client_get_info(client)->id);
+ }
+
+ follower = NULL;
+ spa_follower = NULL;
+ str = pw_properties_get(properties, "adapt.follower.node");
+ if (str != NULL) {
+ if (sscanf(str, "pointer:%p", &follower) != 1)
+ goto error_properties;
+ spa_follower = pw_impl_node_get_implementation(follower);
+ }
+ str = pw_properties_get(properties, "adapt.follower.spa-node");
+ if (str != NULL) {
+ if (sscanf(str, "pointer:%p", &spa_follower) != 1)
+ goto error_properties;
+ }
+ if (spa_follower == NULL) {
+ void *iface;
+ const char *factory_name;
+
+ factory_name = pw_properties_get(properties, SPA_KEY_FACTORY_NAME);
+ if (factory_name == NULL)
+ goto error_properties;
+
+ handle = pw_context_load_spa_handle(d->context,
+ factory_name,
+ properties ? &properties->dict : NULL);
+ if (handle == NULL)
+ goto error_errno;
+
+ if ((res = spa_handle_get_interface(handle, SPA_TYPE_INTERFACE_Node, &iface)) < 0)
+ goto error_res;
+
+ spa_follower = iface;
+ }
+ if (spa_follower == NULL) {
+ res = -EINVAL;
+ goto error_res;
+ }
+
+ adapter = pw_adapter_new(pw_impl_module_get_context(d->module),
+ spa_follower,
+ properties,
+ sizeof(struct node_data));
+ properties = NULL;
+
+ if (adapter == NULL) {
+ if (errno == ENOMEM || errno == EBUSY)
+ goto error_errno;
+ else
+ goto error_usage;
+ }
+
+ nd = pw_adapter_get_user_data(adapter);
+ nd->data = d;
+ nd->adapter = adapter;
+ nd->follower = follower;
+ nd->handle = handle;
+ nd->resource = resource;
+ nd->new_id = new_id;
+ nd->linger = linger;
+ spa_list_append(&d->node_list, &nd->link);
+
+ pw_impl_node_add_listener(adapter, &nd->adapter_listener, &node_events, nd);
+
+ if (do_register)
+ pw_impl_node_register(adapter, NULL);
+ else
+ pw_impl_node_initialized(adapter);
+
+ return adapter;
+
+error_properties:
+ res = -EINVAL;
+ pw_resource_errorf_id(resource, new_id, res, "usage: " FACTORY_USAGE);
+ goto error_cleanup;
+error_errno:
+ res = -errno;
+error_res:
+ pw_resource_errorf_id(resource, new_id, res, "can't create node: %s", spa_strerror(res));
+ goto error_cleanup;
+error_usage:
+ res = -EINVAL;
+ pw_log_error("usage: "ADAPTER_USAGE);
+ pw_resource_errorf_id(resource, new_id, res, "usage: "ADAPTER_USAGE);
+ goto error_cleanup;
+error_cleanup:
+ pw_properties_free(properties);
+ if (handle)
+ pw_unload_spa_handle(handle);
+ errno = -res;
+ return NULL;
+}
+
+static const struct pw_impl_factory_implementation impl_factory = {
+ PW_VERSION_IMPL_FACTORY_IMPLEMENTATION,
+ .create_object = create_object,
+};
+
+static void factory_destroy(void *data)
+{
+ struct factory_data *d = data;
+ struct node_data *nd;
+
+ spa_hook_remove(&d->factory_listener);
+
+ spa_list_consume(nd, &d->node_list, link)
+ pw_impl_node_destroy(nd->adapter);
+
+ d->factory = NULL;
+ if (d->module)
+ pw_impl_module_destroy(d->module);
+}
+
+static const struct pw_impl_factory_events factory_events = {
+ PW_VERSION_IMPL_FACTORY_EVENTS,
+ .destroy = factory_destroy,
+};
+
+static void module_destroy(void *data)
+{
+ struct factory_data *d = data;
+
+ pw_log_debug("%p: destroy", d);
+ spa_hook_remove(&d->module_listener);
+ d->module = NULL;
+
+ if (d->factory)
+ pw_impl_factory_destroy(d->factory);
+}
+
+static void module_registered(void *data)
+{
+ struct factory_data *d = data;
+ struct pw_impl_module *module = d->module;
+ struct pw_impl_factory *factory = d->factory;
+ struct spa_dict_item items[1];
+ char id[16];
+ int res;
+
+ snprintf(id, sizeof(id), "%d", pw_impl_module_get_info(module)->id);
+ items[0] = SPA_DICT_ITEM_INIT(PW_KEY_MODULE_ID, id);
+ pw_impl_factory_update_properties(factory, &SPA_DICT_INIT(items, 1));
+
+ if ((res = pw_impl_factory_register(factory, NULL)) < 0) {
+ pw_log_error("%p: can't register factory: %s", factory, spa_strerror(res));
+ }
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy,
+ .registered = module_registered,
+};
+
+SPA_EXPORT
+int pipewire__module_init(struct pw_impl_module *module, const char *args)
+{
+ struct pw_context *context = pw_impl_module_get_context(module);
+ struct pw_impl_factory *factory;
+ struct factory_data *data;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ factory = pw_context_create_factory(context,
+ "adapter",
+ PW_TYPE_INTERFACE_Node,
+ PW_VERSION_NODE,
+ pw_properties_new(
+ PW_KEY_FACTORY_USAGE, FACTORY_USAGE,
+ NULL),
+ sizeof(*data));
+ if (factory == NULL)
+ return -errno;
+
+ data = pw_impl_factory_get_user_data(factory);
+ data->factory = factory;
+ data->context = context;
+ data->module = module;
+ spa_list_init(&data->node_list);
+
+ pw_log_debug("module %p: new", module);
+
+ pw_impl_factory_add_listener(factory, &data->factory_listener,
+ &factory_events, data);
+ pw_impl_factory_set_implementation(factory,
+ &impl_factory,
+ data);
+
+ pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_props));
+
+ pw_impl_module_add_listener(module, &data->module_listener, &module_events, data);
+
+ return 0;
+}
diff --git a/src/modules/module-adapter/adapter.c b/src/modules/module-adapter/adapter.c
new file mode 100644
index 0000000..19c5713
--- /dev/null
+++ b/src/modules/module-adapter/adapter.c
@@ -0,0 +1,548 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <errno.h>
+#include <math.h>
+#include <time.h>
+
+#include "config.h"
+
+#include <spa/node/node.h>
+#include <spa/node/utils.h>
+#include <spa/utils/hook.h>
+#include <spa/utils/result.h>
+#include <spa/utils/names.h>
+#include <spa/utils/string.h>
+#include <spa/utils/type-info.h>
+#include <spa/param/format.h>
+#include <spa/param/audio/format.h>
+#include <spa/param/audio/format-utils.h>
+#include <spa/param/format-utils.h>
+#include <spa/debug/types.h>
+#include <spa/utils/json-pod.h>
+
+#include "pipewire/pipewire.h"
+
+#include "modules/spa/spa-node.h"
+
+#define NAME "adapter"
+
+PW_LOG_TOPIC_EXTERN(mod_topic);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+struct buffer {
+ struct spa_buffer buf;
+ struct spa_data datas[1];
+ struct spa_chunk chunk[1];
+};
+
+struct node {
+ struct pw_context *context;
+
+ struct pw_impl_node *node;
+ struct spa_hook node_listener;
+
+ struct spa_node *follower;
+
+ void *user_data;
+ enum pw_direction direction;
+ struct pw_properties *props;
+
+ uint32_t media_type;
+ uint32_t media_subtype;
+
+ struct spa_list ports;
+};
+
+/** \endcond */
+static void node_free(void *data)
+{
+ struct node *n = data;
+ spa_hook_remove(&n->node_listener);
+ pw_properties_free(n->props);
+}
+
+static void node_port_init(void *data, struct pw_impl_port *port)
+{
+ struct node *n = data;
+ const struct pw_properties *old;
+ enum pw_direction direction;
+ struct pw_properties *new;
+ const char *str, *path, *desc, *nick, *name, *node_name, *media_class, *prop_port_names, *override_device_prefix;
+ char position[8], *prefix;
+ bool is_monitor, is_device, is_duplex, is_virtual, is_control = false, no_device_port_prefix;
+
+ direction = pw_impl_port_get_direction(port);
+
+ old = pw_impl_port_get_properties(port);
+
+ is_monitor = pw_properties_get_bool(old, PW_KEY_PORT_MONITOR, false);
+ if (!is_monitor && direction != n->direction)
+ return;
+
+ if ((str = pw_properties_get(old, PW_KEY_FORMAT_DSP)) != NULL)
+ is_control = spa_streq(str, "8 bit raw midi");
+
+ path = pw_properties_get(n->props, PW_KEY_OBJECT_PATH);
+ media_class = pw_properties_get(n->props, PW_KEY_MEDIA_CLASS);
+
+ if (media_class != NULL &&
+ (strstr(media_class, "Sink") != NULL ||
+ strstr(media_class, "Source") != NULL))
+ is_device = true;
+ else
+ is_device = false;
+
+ is_duplex = media_class != NULL && strstr(media_class, "Duplex") != NULL;
+ is_virtual = media_class != NULL && strstr(media_class, "Virtual") != NULL;
+
+ new = pw_properties_new(NULL, NULL);
+
+ override_device_prefix = pw_properties_get(n->props, PW_KEY_NODE_DEVICE_PORT_NAME_PREFIX);
+
+ if (is_control)
+ prefix = direction == PW_DIRECTION_INPUT ?
+ "control" : "notify";
+ else if (is_duplex)
+ prefix = direction == PW_DIRECTION_INPUT ?
+ "playback" : "capture";
+ else if (is_virtual)
+ prefix = direction == PW_DIRECTION_INPUT ?
+ "input" : "capture";
+ else if (is_device)
+ prefix = direction == PW_DIRECTION_INPUT ?
+ override_device_prefix != NULL ?
+ strdup(override_device_prefix) : "playback"
+ : is_monitor ? "monitor" : override_device_prefix != NULL ?
+ strdup(override_device_prefix) : "capture";
+ else
+ prefix = direction == PW_DIRECTION_INPUT ?
+ "input" : is_monitor ? "monitor" : "output";
+
+ if ((str = pw_properties_get(old, PW_KEY_AUDIO_CHANNEL)) == NULL ||
+ spa_streq(str, "UNK")) {
+ snprintf(position, sizeof(position), "%d", pw_impl_port_get_id(port) + 1);
+ str = position;
+ }
+ if (direction == n->direction) {
+ if (is_device) {
+ pw_properties_set(new, PW_KEY_PORT_PHYSICAL, "true");
+ pw_properties_set(new, PW_KEY_PORT_TERMINAL, "true");
+ }
+ }
+
+ desc = pw_properties_get(n->props, PW_KEY_NODE_DESCRIPTION);
+ nick = pw_properties_get(n->props, PW_KEY_NODE_NICK);
+ name = pw_properties_get(n->props, PW_KEY_NODE_NAME);
+
+ if ((node_name = desc) == NULL && (node_name = nick) == NULL &&
+ (node_name = name) == NULL)
+ node_name = "node";
+
+ pw_properties_setf(new, PW_KEY_OBJECT_PATH, "%s:%s_%d",
+ path ? path : node_name, prefix, pw_impl_port_get_id(port));
+
+ no_device_port_prefix = is_device && !is_monitor
+ && override_device_prefix != NULL && strlen(override_device_prefix) == 0;
+
+ if (is_control)
+ pw_properties_setf(new, PW_KEY_PORT_NAME, "%s", prefix);
+ else if (no_device_port_prefix)
+ pw_properties_setf(new, PW_KEY_PORT_NAME, "%s", str);
+ else
+ pw_properties_setf(new, PW_KEY_PORT_NAME, "%s_%s", prefix, str);
+
+ if ((node_name = nick) == NULL && (node_name = desc) == NULL &&
+ (node_name = name) == NULL)
+ node_name = "node";
+
+ if (is_control)
+ pw_properties_setf(new, PW_KEY_PORT_ALIAS, "%s:%s",
+ node_name, prefix);
+ else
+ pw_properties_setf(new, PW_KEY_PORT_ALIAS, "%s:%s_%s",
+ node_name, prefix, str);
+
+ prop_port_names = pw_properties_get(n->props, PW_KEY_NODE_CHANNELNAMES);
+ if (prop_port_names) {
+ struct spa_json it[2];
+ char v[256];
+
+ spa_json_init(&it[0], prop_port_names, strlen(prop_port_names));
+ if (spa_json_enter_array(&it[0], &it[1]) <= 0)
+ spa_json_init(&it[1], prop_port_names, strlen(prop_port_names));
+
+ uint32_t i;
+ for (i = 0; i < pw_impl_port_get_id(port) + 1; i++)
+ if (spa_json_get_string(&it[1], v, sizeof(v)) <= 0)
+ break;
+
+ if (i == pw_impl_port_get_id(port) + 1 && strlen(v) > 0) {
+ if (no_device_port_prefix) {
+ pw_properties_setf(new, PW_KEY_PORT_NAME, "%s", v);
+ } else {
+ pw_properties_setf(new, PW_KEY_PORT_NAME, "%s_%s", prefix, v);
+ }
+ }
+ }
+
+ pw_impl_port_update_properties(port, &new->dict);
+ pw_properties_free(new);
+}
+
+static const struct pw_impl_node_events node_events = {
+ PW_VERSION_IMPL_NODE_EVENTS,
+ .free = node_free,
+ .port_init = node_port_init,
+};
+
+static int handle_node_param(struct pw_impl_node *node, const char *key, const char *value)
+{
+ const struct spa_type_info *ti;
+ uint8_t buffer[1024];
+ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
+ struct spa_pod *pod;
+ int res;
+
+ ti = spa_debug_type_find_short(spa_type_param, key);
+ if (ti == NULL)
+ return -ENOENT;
+
+ if ((res = spa_json_to_pod(&b, 0, ti, value, strlen(value))) < 0)
+ return res;
+
+ if ((pod = spa_pod_builder_deref(&b, 0)) == NULL)
+ return -ENOSPC;
+
+ if ((res = pw_impl_node_set_param(node, ti->type, 0, pod)) < 0)
+ return res;
+
+ return 0;
+}
+
+static int find_format(struct spa_node *node, enum pw_direction direction,
+ uint32_t *media_type, uint32_t *media_subtype)
+{
+ uint32_t state = 0;
+ uint8_t buffer[4096];
+ struct spa_pod_builder b;
+ int res;
+ struct spa_pod *format;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ if ((res = spa_node_port_enum_params_sync(node,
+ direction == PW_DIRECTION_INPUT ?
+ SPA_DIRECTION_INPUT :
+ SPA_DIRECTION_OUTPUT, 0,
+ SPA_PARAM_EnumFormat, &state,
+ NULL, &format, &b)) != 1) {
+ res = res < 0 ? res : -ENOENT;
+ pw_log_warn("%p: can't get format: %s", node, spa_strerror(res));
+ return res;
+ }
+
+ if ((res = spa_format_parse(format, media_type, media_subtype)) < 0)
+ return res;
+
+ pw_log_debug("%p: %s/%s", node,
+ spa_debug_type_find_name(spa_type_media_type, *media_type),
+ spa_debug_type_find_name(spa_type_media_subtype, *media_subtype));
+ return 0;
+}
+
+static int do_auto_port_config(struct node *n, const char *str)
+{
+ uint32_t state = 0, i;
+ uint8_t buffer[4096];
+ struct spa_pod_builder b;
+#define POSITION_PRESERVE 0
+#define POSITION_AUX 1
+#define POSITION_UNKNOWN 2
+ int res, position = POSITION_PRESERVE;
+ struct spa_pod *param;
+ uint32_t media_type, media_subtype;
+ bool have_format = false, monitor = false, control = false;
+ struct spa_audio_info format = { 0, };
+ enum spa_param_port_config_mode mode = SPA_PARAM_PORT_CONFIG_MODE_none;
+ struct spa_json it[2];
+ char key[1024], val[256];
+
+ spa_json_init(&it[0], str, strlen(str));
+ if (spa_json_enter_object(&it[0], &it[1]) <= 0)
+ return -EINVAL;
+
+ while (spa_json_get_string(&it[1], key, sizeof(key)) > 0) {
+ if (spa_json_get_string(&it[1], val, sizeof(val)) <= 0)
+ break;
+
+ if (spa_streq(key, "mode")) {
+ mode = spa_debug_type_find_type_short(spa_type_param_port_config_mode, val);
+ if (mode == SPA_ID_INVALID)
+ mode = SPA_PARAM_PORT_CONFIG_MODE_none;
+ } else if (spa_streq(key, "monitor")) {
+ monitor = spa_atob(val);
+ } else if (spa_streq(key, "control")) {
+ control = spa_atob(val);
+ } else if (spa_streq(key, "position")) {
+ if (spa_streq(val, "unknown"))
+ position = POSITION_UNKNOWN;
+ else if (spa_streq(val, "aux"))
+ position = POSITION_AUX;
+ else
+ position = POSITION_PRESERVE;
+ }
+ }
+
+ while (true) {
+ struct spa_audio_info info = { 0, };
+ struct spa_pod *position = NULL;
+ uint32_t n_position = 0;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ if ((res = spa_node_port_enum_params_sync(n->follower,
+ n->direction == PW_DIRECTION_INPUT ?
+ SPA_DIRECTION_INPUT :
+ SPA_DIRECTION_OUTPUT, 0,
+ SPA_PARAM_EnumFormat, &state,
+ NULL, &param, &b)) != 1)
+ break;
+
+ if ((res = spa_format_parse(param, &media_type, &media_subtype)) < 0)
+ continue;
+
+ if (media_type != SPA_MEDIA_TYPE_audio ||
+ media_subtype != SPA_MEDIA_SUBTYPE_raw)
+ continue;
+
+ spa_pod_object_fixate((struct spa_pod_object*)param);
+
+ if (spa_pod_parse_object(param,
+ SPA_TYPE_OBJECT_Format, NULL,
+ SPA_FORMAT_AUDIO_format, SPA_POD_Id(&info.info.raw.format),
+ SPA_FORMAT_AUDIO_rate, SPA_POD_Int(&info.info.raw.rate),
+ SPA_FORMAT_AUDIO_channels, SPA_POD_Int(&info.info.raw.channels),
+ SPA_FORMAT_AUDIO_position, SPA_POD_OPT_Pod(&position)) < 0)
+ continue;
+
+ if (position != NULL)
+ n_position = spa_pod_copy_array(position, SPA_TYPE_Id,
+ info.info.raw.position, SPA_AUDIO_MAX_CHANNELS);
+ if (n_position == 0 || n_position != info.info.raw.channels)
+ SPA_FLAG_SET(info.info.raw.flags, SPA_AUDIO_FLAG_UNPOSITIONED);
+
+ if (format.info.raw.channels >= info.info.raw.channels)
+ continue;
+
+ format = info;
+ have_format = true;
+ }
+ if (!have_format)
+ return -ENOENT;
+
+ if (position == POSITION_AUX) {
+ for (i = 0; i < format.info.raw.channels; i++)
+ format.info.raw.position[i] = SPA_AUDIO_CHANNEL_START_Aux + i;
+ } else if (position == POSITION_UNKNOWN) {
+ for (i = 0; i < format.info.raw.channels; i++)
+ format.info.raw.position[i] = SPA_AUDIO_CHANNEL_UNKNOWN;
+ }
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ param = spa_format_audio_raw_build(&b, SPA_PARAM_Format, &format.info.raw);
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamPortConfig, SPA_PARAM_PortConfig,
+ SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(n->direction),
+ SPA_PARAM_PORT_CONFIG_mode, SPA_POD_Id(mode),
+ SPA_PARAM_PORT_CONFIG_monitor, SPA_POD_Bool(monitor),
+ SPA_PARAM_PORT_CONFIG_control, SPA_POD_Bool(control),
+ SPA_PARAM_PORT_CONFIG_format, SPA_POD_Pod(param));
+ pw_impl_node_set_param(n->node, SPA_PARAM_PortConfig, 0, param);
+
+ return 0;
+}
+
+struct info_data {
+ struct spa_hook listener;
+ struct spa_node *node;
+ struct pw_properties *props;
+ uint32_t n_input_ports;
+ uint32_t max_input_ports;
+ uint32_t n_output_ports;
+ uint32_t max_output_ports;
+};
+
+static void info_event(void *data, const struct spa_node_info *info)
+{
+ struct info_data *d = data;
+
+ pw_properties_update(d->props, info->props);
+
+ d->max_input_ports = info->max_input_ports;
+ d->max_output_ports = info->max_output_ports;
+}
+
+static void port_info_event(void *data, enum spa_direction direction, uint32_t port,
+ const struct spa_port_info *info)
+{
+ struct info_data *d = data;
+
+ if (direction == SPA_DIRECTION_OUTPUT)
+ d->n_output_ports++;
+ else if (direction == SPA_DIRECTION_INPUT)
+ d->n_input_ports++;
+}
+
+static const struct spa_node_events node_info_events = {
+ .version = SPA_VERSION_NODE_EVENTS,
+ .info = info_event,
+ .port_info = port_info_event,
+};
+
+struct pw_impl_node *pw_adapter_new(struct pw_context *context,
+ struct spa_node *follower,
+ struct pw_properties *props,
+ size_t user_data_size)
+{
+ struct pw_impl_node *node;
+ struct node *n;
+ const char *str, *factory_name;
+ enum pw_direction direction;
+ int res;
+ uint32_t media_type, media_subtype;
+ const struct spa_dict_item *it;
+ struct pw_properties *copy;
+ struct info_data info;
+
+ spa_zero(info);
+ info.node = follower;
+ info.props = props;
+
+ res = spa_node_add_listener(info.node, &info.listener, &node_info_events, &info);
+ if (res < 0)
+ goto error;
+
+ spa_hook_remove(&info.listener);
+
+ pw_log_debug("%p: in %d/%d out %d/%d", info.node,
+ info.n_input_ports, info.max_input_ports,
+ info.n_output_ports, info.max_output_ports);
+
+ if (info.n_output_ports > 0) {
+ direction = PW_DIRECTION_OUTPUT;
+ } else if (info.n_input_ports > 0) {
+ direction = PW_DIRECTION_INPUT;
+ } else {
+ res = -EINVAL;
+ goto error;
+ }
+
+ if ((str = pw_properties_get(props, PW_KEY_NODE_ID)) != NULL)
+ pw_properties_set(props, PW_KEY_NODE_SESSION, str);
+
+ if (pw_properties_get(props, "factory.mode") == NULL) {
+ if (direction == PW_DIRECTION_INPUT)
+ str = "merge";
+ else
+ str = "split";
+ pw_properties_set(props, "factory.mode", str);
+ }
+
+ if ((res = find_format(follower, direction, &media_type, &media_subtype)) < 0)
+ goto error;
+
+ if (media_type == SPA_MEDIA_TYPE_audio) {
+ pw_properties_setf(props, "audio.adapt.follower", "pointer:%p", follower);
+ pw_properties_set(props, SPA_KEY_LIBRARY_NAME, "audioconvert/libspa-audioconvert");
+ if (pw_properties_get(props, PW_KEY_MEDIA_CLASS) == NULL)
+ pw_properties_setf(props, PW_KEY_MEDIA_CLASS, "Audio/%s",
+ direction == PW_DIRECTION_INPUT ? "Sink" : "Source");
+ factory_name = SPA_NAME_AUDIO_ADAPT;
+ }
+ else if (media_type == SPA_MEDIA_TYPE_video) {
+ pw_properties_setf(props, "video.adapt.follower", "pointer:%p", follower);
+ pw_properties_set(props, SPA_KEY_LIBRARY_NAME, "videoconvert/libspa-videoconvert");
+ if (pw_properties_get(props, PW_KEY_MEDIA_CLASS) == NULL)
+ pw_properties_setf(props, PW_KEY_MEDIA_CLASS, "Video/%s",
+ direction == PW_DIRECTION_INPUT ? "Sink" : "Source");
+ factory_name = SPA_NAME_VIDEO_ADAPT;
+ } else {
+ res = -ENOTSUP;
+ goto error;
+ }
+
+ copy = pw_properties_new(NULL, NULL);
+ spa_dict_for_each(it, &props->dict) {
+ if (!spa_strstartswith(it->key, "node.param.") &&
+ !spa_strstartswith(it->key, "port.param."))
+ pw_properties_set(copy, it->key, it->value);
+ }
+ node = pw_spa_node_load(context,
+ factory_name,
+ PW_SPA_NODE_FLAG_ACTIVATE | PW_SPA_NODE_FLAG_NO_REGISTER,
+ copy, sizeof(struct node) + user_data_size);
+ if (node == NULL) {
+ res = -errno;
+ pw_log_error("can't load spa node: %m");
+ goto error;
+ }
+
+ n = pw_spa_node_get_user_data(node);
+ n->context = context;
+ n->node = node;
+ n->follower = follower;
+ n->direction = direction;
+ n->props = props;
+ n->media_type = media_type;
+ n->media_subtype = media_subtype;
+ spa_list_init(&n->ports);
+
+ if (user_data_size > 0)
+ n->user_data = SPA_PTROFF(n, sizeof(struct node), void);
+
+ pw_impl_node_add_listener(node, &n->node_listener, &node_events, n);
+
+ if ((str = pw_properties_get(props, "adapter.auto-port-config")) != NULL)
+ do_auto_port_config(n, str);
+
+ spa_dict_for_each(it, &props->dict) {
+ if (spa_strstartswith(it->key, "node.param.")) {
+ if ((res = handle_node_param(node, &it->key[11], it->value)) < 0)
+ pw_log_warn("can't set node param: %s", spa_strerror(res));
+ }
+ }
+ return node;
+
+error:
+ pw_properties_free(props);
+ errno = -res;
+ return NULL;
+}
+
+void *pw_adapter_get_user_data(struct pw_impl_node *node)
+{
+ struct node *n = pw_spa_node_get_user_data(node);
+ return n->user_data;
+}
diff --git a/src/modules/module-adapter/adapter.h b/src/modules/module-adapter/adapter.h
new file mode 100644
index 0000000..80fa9c7
--- /dev/null
+++ b/src/modules/module-adapter/adapter.h
@@ -0,0 +1,48 @@
+/* PipeWire
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_ADAPTER_H
+#define PIPEWIRE_ADAPTER_H
+
+#include <pipewire/impl.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define ADAPTER_USAGE PW_KEY_NODE_NAME"=<string> "
+
+struct pw_impl_node *
+pw_adapter_new(struct pw_context *context,
+ struct spa_node *follower,
+ struct pw_properties *properties,
+ size_t user_data_size);
+
+void *pw_adapter_get_user_data(struct pw_impl_node *node);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PIPEWIRE_ADAPTER_H */
diff --git a/src/modules/module-avb.c b/src/modules/module-avb.c
new file mode 100644
index 0000000..2359c2d
--- /dev/null
+++ b/src/modules/module-avb.c
@@ -0,0 +1,129 @@
+/* PipeWire
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+
+#include "config.h"
+
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+#include <spa/utils/json.h>
+
+#include <pipewire/impl.h>
+#include <pipewire/i18n.h>
+
+#include "module-avb/avb.h"
+
+/** \page page_module_avb PipeWire Module: AVB
+ */
+
+#define NAME "avb"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+#define MODULE_USAGE " "
+
+static const struct spa_dict_item module_props[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Manage an AVB network" },
+ { PW_KEY_MODULE_USAGE, MODULE_USAGE },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+
+struct impl {
+ struct pw_context *context;
+
+ struct pw_impl_module *module;
+ struct spa_hook module_listener;
+
+ struct pw_avb *avb;
+};
+
+static void impl_free(struct impl *impl)
+{
+ free(impl);
+}
+
+static void module_destroy(void *data)
+{
+ struct impl *impl = data;
+ spa_hook_remove(&impl->module_listener);
+ impl_free(impl);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy,
+};
+
+SPA_EXPORT
+int pipewire__module_init(struct pw_impl_module *module, const char *args)
+{
+ struct pw_context *context = pw_impl_module_get_context(module);
+ struct pw_properties *props;
+ struct impl *impl;
+ int res;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ impl = calloc(1, sizeof(struct impl));
+ if (impl == NULL)
+ goto error_errno;
+
+ pw_log_debug("module %p: new %s", impl, args);
+
+ if (args == NULL)
+ args = "";
+
+ props = pw_properties_new_string(args);
+ if (props == NULL)
+ goto error_errno;
+
+ impl->module = module;
+ impl->context = context;
+
+ impl->avb = pw_avb_new(context, props, 0);
+ if (impl->avb == NULL)
+ goto error_errno;
+
+ pw_impl_module_add_listener(module, &impl->module_listener, &module_events, impl);
+
+ pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_props));
+
+ return 0;
+
+error_errno:
+ res = -errno;
+ if (impl)
+ impl_free(impl);
+ return res;
+}
diff --git a/src/modules/module-avb/aaf.h b/src/modules/module-avb/aaf.h
new file mode 100644
index 0000000..b444ce2
--- /dev/null
+++ b/src/modules/module-avb/aaf.h
@@ -0,0 +1,102 @@
+/* AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef AVB_AAF_H
+#define AVB_AAF_H
+
+struct avb_packet_aaf {
+ uint8_t subtype;
+#if __BYTE_ORDER == __BIG_ENDIAN
+ unsigned sv:1;
+ unsigned version:3;
+ unsigned mr:1;
+ unsigned _r1:1;
+ unsigned gv:1;
+ unsigned tv:1;
+
+ uint8_t seq_num;
+
+ unsigned _r2:7;
+ unsigned tu:1;
+#elif __BYTE_ORDER == __LITTLE_ENDIAN
+ unsigned tv:1;
+ unsigned gv:1;
+ unsigned _r1:1;
+ unsigned mr:1;
+ unsigned version:3;
+ unsigned sv:1;
+
+ uint8_t seq_num;
+
+ unsigned tu:1;
+ unsigned _r2:7;
+#endif
+ uint64_t stream_id;
+ uint32_t timestamp;
+#define AVB_AAF_FORMAT_USER 0x00
+#define AVB_AAF_FORMAT_FLOAT_32BIT 0x01
+#define AVB_AAF_FORMAT_INT_32BIT 0x02
+#define AVB_AAF_FORMAT_INT_24BIT 0x03
+#define AVB_AAF_FORMAT_INT_16BIT 0x04
+#define AVB_AAF_FORMAT_AES3_32BIT 0x05
+ uint8_t format;
+
+#define AVB_AAF_PCM_NSR_USER 0x00
+#define AVB_AAF_PCM_NSR_8KHZ 0x01
+#define AVB_AAF_PCM_NSR_16KHZ 0x02
+#define AVB_AAF_PCM_NSR_32KHZ 0x03
+#define AVB_AAF_PCM_NSR_44_1KHZ 0x04
+#define AVB_AAF_PCM_NSR_48KHZ 0x05
+#define AVB_AAF_PCM_NSR_88_2KHZ 0x06
+#define AVB_AAF_PCM_NSR_96KHZ 0x07
+#define AVB_AAF_PCM_NSR_176_4KHZ 0x08
+#define AVB_AAF_PCM_NSR_192KHZ 0x09
+#define AVB_AAF_PCM_NSR_24KHZ 0x0A
+#if __BYTE_ORDER == __BIG_ENDIAN
+ unsigned nsr:4;
+ unsigned _r3:4;
+#elif __BYTE_ORDER == __LITTLE_ENDIAN
+ unsigned _r3:4;
+ unsigned nsr:4;
+#endif
+ uint8_t chan_per_frame;
+ uint8_t bit_depth;
+ uint16_t data_len;
+
+#define AVB_AAF_PCM_SP_NORMAL 0x00
+#define AVB_AAF_PCM_SP_SPARSE 0x01
+#if __BYTE_ORDER == __BIG_ENDIAN
+ unsigned _r4:3;
+ unsigned sp:1;
+ unsigned event:4;
+#elif __BYTE_ORDER == __LITTLE_ENDIAN
+ unsigned event:4;
+ unsigned sp:1;
+ unsigned _r4:3;
+#endif
+ uint8_t _r5;
+ uint8_t payload[0];
+} __attribute__ ((__packed__));
+
+#endif /* AVB_AAF_H */
diff --git a/src/modules/module-avb/acmp.c b/src/modules/module-avb/acmp.c
new file mode 100644
index 0000000..a7a409a
--- /dev/null
+++ b/src/modules/module-avb/acmp.c
@@ -0,0 +1,476 @@
+/* AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <spa/utils/json.h>
+#include <spa/debug/mem.h>
+
+#include <pipewire/pipewire.h>
+
+#include "acmp.h"
+#include "msrp.h"
+#include "internal.h"
+#include "stream.h"
+
+static const uint8_t mac[6] = AVB_BROADCAST_MAC;
+
+struct pending {
+ struct spa_list link;
+ uint64_t last_time;
+ uint64_t timeout;
+ uint16_t old_sequence_id;
+ uint16_t sequence_id;
+ uint16_t retry;
+ size_t size;
+ void *ptr;
+};
+
+struct acmp {
+ struct server *server;
+ struct spa_hook server_listener;
+
+#define PENDING_TALKER 0
+#define PENDING_LISTENER 1
+#define PENDING_CONTROLLER 2
+ struct spa_list pending[3];
+ uint16_t sequence_id[3];
+};
+
+static void *pending_new(struct acmp *acmp, uint32_t type, uint64_t now, uint32_t timeout_ms,
+ const void *m, size_t size)
+{
+ struct pending *p;
+ struct avb_ethernet_header *h;
+ struct avb_packet_acmp *pm;
+
+ p = calloc(1, sizeof(*p) + size);
+ if (p == NULL)
+ return NULL;
+ p->last_time = now;
+ p->timeout = timeout_ms * SPA_NSEC_PER_MSEC;
+ p->sequence_id = acmp->sequence_id[type]++;
+ p->size = size;
+ p->ptr = SPA_PTROFF(p, sizeof(*p), void);
+ memcpy(p->ptr, m, size);
+
+ h = p->ptr;
+ pm = SPA_PTROFF(h, sizeof(*h), void);
+ p->old_sequence_id = ntohs(pm->sequence_id);
+ pm->sequence_id = htons(p->sequence_id);
+ spa_list_append(&acmp->pending[type], &p->link);
+
+ return p->ptr;
+}
+
+static struct pending *pending_find(struct acmp *acmp, uint32_t type, uint16_t sequence_id)
+{
+ struct pending *p;
+ spa_list_for_each(p, &acmp->pending[type], link)
+ if (p->sequence_id == sequence_id)
+ return p;
+ return NULL;
+}
+
+static void pending_free(struct acmp *acmp, struct pending *p)
+{
+ spa_list_remove(&p->link);
+ free(p);
+}
+
+struct msg_info {
+ uint16_t type;
+ const char *name;
+ int (*handle) (struct acmp *acmp, uint64_t now, const void *m, int len);
+};
+
+static int reply_not_supported(struct acmp *acmp, uint8_t type, const void *m, int len)
+{
+ struct server *server = acmp->server;
+ uint8_t buf[len];
+ struct avb_ethernet_header *h = (void*)buf;
+ struct avb_packet_acmp *reply = SPA_PTROFF(h, sizeof(*h), void);
+
+ memcpy(h, m, len);
+ AVB_PACKET_ACMP_SET_MESSAGE_TYPE(reply, type);
+ AVB_PACKET_ACMP_SET_STATUS(reply, AVB_ACMP_STATUS_NOT_SUPPORTED);
+
+ return avb_server_send_packet(server, h->src, AVB_TSN_ETH, buf, len);
+}
+
+static int retry_pending(struct acmp *acmp, uint64_t now, struct pending *p)
+{
+ struct server *server = acmp->server;
+ struct avb_ethernet_header *h = p->ptr;
+ p->retry++;
+ p->last_time = now;
+ return avb_server_send_packet(server, h->dest, AVB_TSN_ETH, p->ptr, p->size);
+}
+
+static int handle_connect_tx_command(struct acmp *acmp, uint64_t now, const void *m, int len)
+{
+ struct server *server = acmp->server;
+ uint8_t buf[len];
+ struct avb_ethernet_header *h = (void*)buf;
+ struct avb_packet_acmp *reply = SPA_PTROFF(h, sizeof(*h), void);
+ const struct avb_packet_acmp *p = SPA_PTROFF(m, sizeof(*h), void);
+ int status = AVB_ACMP_STATUS_SUCCESS;
+ struct stream *stream;
+
+ if (be64toh(p->talker_guid) != server->entity_id)
+ return 0;
+
+ memcpy(buf, m, len);
+ stream = server_find_stream(server, SPA_DIRECTION_OUTPUT,
+ reply->talker_unique_id);
+ if (stream == NULL) {
+ status = AVB_ACMP_STATUS_TALKER_NO_STREAM_INDEX;
+ goto done;
+ }
+
+ AVB_PACKET_ACMP_SET_MESSAGE_TYPE(reply, AVB_ACMP_MESSAGE_TYPE_CONNECT_TX_RESPONSE);
+ reply->stream_id = htobe64(stream->id);
+
+ stream_activate(stream, now);
+
+ memcpy(reply->stream_dest_mac, stream->addr, 6);
+ reply->connection_count = htons(1);
+ reply->stream_vlan_id = htons(stream->vlan_id);
+
+done:
+ AVB_PACKET_ACMP_SET_STATUS(reply, status);
+ return avb_server_send_packet(server, h->dest, AVB_TSN_ETH, buf, len);
+}
+
+static int handle_connect_tx_response(struct acmp *acmp, uint64_t now, const void *m, int len)
+{
+ struct server *server = acmp->server;
+ struct avb_ethernet_header *h;
+ const struct avb_packet_acmp *resp = SPA_PTROFF(m, sizeof(*h), void);
+ struct avb_packet_acmp *reply;
+ struct pending *pending;
+ uint16_t sequence_id;
+ struct stream *stream;
+ int res;
+
+ if (be64toh(resp->listener_guid) != server->entity_id)
+ return 0;
+
+ sequence_id = ntohs(resp->sequence_id);
+
+ pending = pending_find(acmp, PENDING_TALKER, sequence_id);
+ if (pending == NULL)
+ return 0;
+
+ h = pending->ptr;
+ pending->size = SPA_MIN((int)pending->size, len);
+ memcpy(h, m, pending->size);
+
+ reply = SPA_PTROFF(h, sizeof(*h), void);
+ reply->sequence_id = htons(pending->old_sequence_id);
+ AVB_PACKET_ACMP_SET_MESSAGE_TYPE(reply, AVB_ACMP_MESSAGE_TYPE_CONNECT_RX_RESPONSE);
+
+ stream = server_find_stream(server, SPA_DIRECTION_INPUT,
+ ntohs(reply->listener_unique_id));
+ if (stream == NULL)
+ return 0;
+
+ stream->peer_id = be64toh(reply->stream_id);
+ memcpy(stream->addr, reply->stream_dest_mac, 6);
+ stream_activate(stream, now);
+
+ res = avb_server_send_packet(server, h->dest, AVB_TSN_ETH, h, pending->size);
+
+ pending_free(acmp, pending);
+
+ return res;
+}
+
+static int handle_disconnect_tx_command(struct acmp *acmp, uint64_t now, const void *m, int len)
+{
+ struct server *server = acmp->server;
+ uint8_t buf[len];
+ struct avb_ethernet_header *h = (void*)buf;
+ struct avb_packet_acmp *reply = SPA_PTROFF(h, sizeof(*h), void);
+ const struct avb_packet_acmp *p = SPA_PTROFF(m, sizeof(*h), void);
+ int status = AVB_ACMP_STATUS_SUCCESS;
+ struct stream *stream;
+
+ if (be64toh(p->talker_guid) != server->entity_id)
+ return 0;
+
+ memcpy(buf, m, len);
+ stream = server_find_stream(server, SPA_DIRECTION_OUTPUT,
+ reply->talker_unique_id);
+ if (stream == NULL) {
+ status = AVB_ACMP_STATUS_TALKER_NO_STREAM_INDEX;
+ goto done;
+ }
+
+ AVB_PACKET_ACMP_SET_MESSAGE_TYPE(reply, AVB_ACMP_MESSAGE_TYPE_DISCONNECT_TX_RESPONSE);
+
+ stream_deactivate(stream, now);
+
+done:
+ AVB_PACKET_ACMP_SET_STATUS(reply, status);
+ return avb_server_send_packet(server, h->dest, AVB_TSN_ETH, buf, len);
+}
+
+static int handle_disconnect_tx_response(struct acmp *acmp, uint64_t now, const void *m, int len)
+{
+ struct server *server = acmp->server;
+ struct avb_ethernet_header *h;
+ struct avb_packet_acmp *reply;
+ const struct avb_packet_acmp *resp = SPA_PTROFF(m, sizeof(*h), void);
+ struct pending *pending;
+ uint16_t sequence_id;
+ struct stream *stream;
+ int res;
+
+ if (be64toh(resp->listener_guid) != server->entity_id)
+ return 0;
+
+ sequence_id = ntohs(resp->sequence_id);
+
+ pending = pending_find(acmp, PENDING_TALKER, sequence_id);
+ if (pending == NULL)
+ return 0;
+
+ h = pending->ptr;
+ pending->size = SPA_MIN((int)pending->size, len);
+ memcpy(h, m, pending->size);
+
+ reply = SPA_PTROFF(h, sizeof(*h), void);
+ reply->sequence_id = htons(pending->old_sequence_id);
+ AVB_PACKET_ACMP_SET_MESSAGE_TYPE(reply, AVB_ACMP_MESSAGE_TYPE_DISCONNECT_RX_RESPONSE);
+
+ stream = server_find_stream(server, SPA_DIRECTION_INPUT,
+ reply->listener_unique_id);
+ if (stream == NULL)
+ return 0;
+
+ stream_deactivate(stream, now);
+
+ res = avb_server_send_packet(server, h->dest, AVB_TSN_ETH, h, pending->size);
+
+ pending_free(acmp, pending);
+
+ return res;
+}
+
+static int handle_connect_rx_command(struct acmp *acmp, uint64_t now, const void *m, int len)
+{
+ struct server *server = acmp->server;
+ struct avb_ethernet_header *h;
+ const struct avb_packet_acmp *p = SPA_PTROFF(m, sizeof(*h), void);
+ struct avb_packet_acmp *cmd;
+
+ if (be64toh(p->listener_guid) != server->entity_id)
+ return 0;
+
+ h = pending_new(acmp, PENDING_TALKER, now,
+ AVB_ACMP_TIMEOUT_CONNECT_TX_COMMAND_MS, m, len);
+ if (h == NULL)
+ return -errno;
+
+ cmd = SPA_PTROFF(h, sizeof(*h), void);
+ AVB_PACKET_ACMP_SET_MESSAGE_TYPE(cmd, AVB_ACMP_MESSAGE_TYPE_CONNECT_TX_COMMAND);
+ AVB_PACKET_ACMP_SET_STATUS(cmd, AVB_ACMP_STATUS_SUCCESS);
+
+ return avb_server_send_packet(server, h->dest, AVB_TSN_ETH, h, len);
+}
+
+static int handle_ignore(struct acmp *acmp, uint64_t now, const void *m, int len)
+{
+ return 0;
+}
+
+static int handle_disconnect_rx_command(struct acmp *acmp, uint64_t now, const void *m, int len)
+{
+ struct server *server = acmp->server;
+ struct avb_ethernet_header *h;
+ const struct avb_packet_acmp *p = SPA_PTROFF(m, sizeof(*h), void);
+ struct avb_packet_acmp *cmd;
+
+ if (be64toh(p->listener_guid) != server->entity_id)
+ return 0;
+
+ h = pending_new(acmp, PENDING_TALKER, now,
+ AVB_ACMP_TIMEOUT_DISCONNECT_TX_COMMAND_MS, m, len);
+ if (h == NULL)
+ return -errno;
+
+ cmd = SPA_PTROFF(h, sizeof(*h), void);
+ AVB_PACKET_ACMP_SET_MESSAGE_TYPE(cmd, AVB_ACMP_MESSAGE_TYPE_DISCONNECT_TX_COMMAND);
+ AVB_PACKET_ACMP_SET_STATUS(cmd, AVB_ACMP_STATUS_SUCCESS);
+
+ return avb_server_send_packet(server, h->dest, AVB_TSN_ETH, h, len);
+}
+
+static const struct msg_info msg_info[] = {
+ { AVB_ACMP_MESSAGE_TYPE_CONNECT_TX_COMMAND, "connect-tx-command", handle_connect_tx_command, },
+ { AVB_ACMP_MESSAGE_TYPE_CONNECT_TX_RESPONSE, "connect-tx-response", handle_connect_tx_response, },
+ { AVB_ACMP_MESSAGE_TYPE_DISCONNECT_TX_COMMAND, "disconnect-tx-command", handle_disconnect_tx_command, },
+ { AVB_ACMP_MESSAGE_TYPE_DISCONNECT_TX_RESPONSE, "disconnect-tx-response", handle_disconnect_tx_response, },
+ { AVB_ACMP_MESSAGE_TYPE_GET_TX_STATE_COMMAND, "get-tx-state-command", NULL, },
+ { AVB_ACMP_MESSAGE_TYPE_GET_TX_STATE_RESPONSE, "get-tx-state-response", handle_ignore, },
+ { AVB_ACMP_MESSAGE_TYPE_CONNECT_RX_COMMAND, "connect-rx-command", handle_connect_rx_command, },
+ { AVB_ACMP_MESSAGE_TYPE_CONNECT_RX_RESPONSE, "connect-rx-response", handle_ignore, },
+ { AVB_ACMP_MESSAGE_TYPE_DISCONNECT_RX_COMMAND, "disconnect-rx-command", handle_disconnect_rx_command, },
+ { AVB_ACMP_MESSAGE_TYPE_DISCONNECT_RX_RESPONSE, "disconnect-rx-response", handle_ignore, },
+ { AVB_ACMP_MESSAGE_TYPE_GET_RX_STATE_COMMAND, "get-rx-state-command", NULL, },
+ { AVB_ACMP_MESSAGE_TYPE_GET_RX_STATE_RESPONSE, "get-rx-state-response", handle_ignore, },
+ { AVB_ACMP_MESSAGE_TYPE_GET_TX_CONNECTION_COMMAND, "get-tx-connection-command", NULL, },
+ { AVB_ACMP_MESSAGE_TYPE_GET_TX_CONNECTION_RESPONSE, "get-tx-connection-response", handle_ignore, },
+};
+
+static inline const struct msg_info *find_msg_info(uint16_t type, const char *name)
+{
+ SPA_FOR_EACH_ELEMENT_VAR(msg_info, i) {
+ if ((name == NULL && type == i->type) ||
+ (name != NULL && spa_streq(name, i->name)))
+ return i;
+ }
+ return NULL;
+}
+
+static int acmp_message(void *data, uint64_t now, const void *message, int len)
+{
+ struct acmp *acmp = data;
+ struct server *server = acmp->server;
+ const struct avb_ethernet_header *h = message;
+ const struct avb_packet_acmp *p = SPA_PTROFF(h, sizeof(*h), void);
+ const struct msg_info *info;
+ int message_type;
+
+ if (ntohs(h->type) != AVB_TSN_ETH)
+ return 0;
+ if (memcmp(h->dest, mac, 6) != 0 &&
+ memcmp(h->dest, server->mac_addr, 6) != 0)
+ return 0;
+
+ if (AVB_PACKET_GET_SUBTYPE(&p->hdr) != AVB_SUBTYPE_ACMP)
+ return 0;
+
+ message_type = AVB_PACKET_ACMP_GET_MESSAGE_TYPE(p);
+
+ info = find_msg_info(message_type, NULL);
+ if (info == NULL)
+ return 0;
+
+ pw_log_info("got ACMP message %s", info->name);
+
+ if (info->handle == NULL)
+ return reply_not_supported(acmp, message_type | 1, message, len);
+
+ return info->handle(acmp, now, message, len);
+}
+
+static void acmp_destroy(void *data)
+{
+ struct acmp *acmp = data;
+ spa_hook_remove(&acmp->server_listener);
+ free(acmp);
+}
+
+static void check_timeout(struct acmp *acmp, uint64_t now, uint16_t type)
+{
+ struct pending *p, *t;
+
+ spa_list_for_each_safe(p, t, &acmp->pending[type], link) {
+ if (p->last_time + p->timeout > now)
+ continue;
+
+ if (p->retry == 0) {
+ pw_log_info("%p: pending timeout, retry", p);
+ retry_pending(acmp, now, p);
+ } else {
+ pw_log_info("%p: pending timeout, fail", p);
+ pending_free(acmp, p);
+ }
+ }
+}
+static void acmp_periodic(void *data, uint64_t now)
+{
+ struct acmp *acmp = data;
+ check_timeout(acmp, now, PENDING_TALKER);
+ check_timeout(acmp, now, PENDING_LISTENER);
+ check_timeout(acmp, now, PENDING_CONTROLLER);
+}
+
+static int do_help(struct acmp *acmp, const char *args, FILE *out)
+{
+ fprintf(out, "{ \"type\": \"help\","
+ "\"text\": \""
+ "/adp/help: this help \\n"
+ "\" }");
+ return 0;
+}
+
+static int acmp_command(void *data, uint64_t now, const char *command, const char *args, FILE *out)
+{
+ struct acmp *acmp = data;
+ int res;
+
+ if (!spa_strstartswith(command, "/acmp/"))
+ return 0;
+
+ command += strlen("/acmp/");
+
+ if (spa_streq(command, "help"))
+ res = do_help(acmp, args, out);
+ else
+ res = -ENOTSUP;
+
+ return res;
+}
+
+static const struct server_events server_events = {
+ AVB_VERSION_SERVER_EVENTS,
+ .destroy = acmp_destroy,
+ .message = acmp_message,
+ .periodic = acmp_periodic,
+ .command = acmp_command
+};
+
+struct avb_acmp *avb_acmp_register(struct server *server)
+{
+ struct acmp *acmp;
+
+ acmp = calloc(1, sizeof(*acmp));
+ if (acmp == NULL)
+ return NULL;
+
+ acmp->server = server;
+ spa_list_init(&acmp->pending[PENDING_TALKER]);
+ spa_list_init(&acmp->pending[PENDING_LISTENER]);
+ spa_list_init(&acmp->pending[PENDING_CONTROLLER]);
+
+ avdecc_server_add_listener(server, &acmp->server_listener, &server_events, acmp);
+
+ return (struct avb_acmp*)acmp;
+}
+
+void avb_acmp_unregister(struct avb_acmp *acmp)
+{
+ acmp_destroy(acmp);
+}
diff --git a/src/modules/module-avb/acmp.h b/src/modules/module-avb/acmp.h
new file mode 100644
index 0000000..5a41c66
--- /dev/null
+++ b/src/modules/module-avb/acmp.h
@@ -0,0 +1,99 @@
+/* AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef AVB_ACMP_H
+#define AVB_ACMP_H
+
+#include "packets.h"
+#include "internal.h"
+
+#define AVB_ACMP_MESSAGE_TYPE_CONNECT_TX_COMMAND 0
+#define AVB_ACMP_MESSAGE_TYPE_CONNECT_TX_RESPONSE 1
+#define AVB_ACMP_MESSAGE_TYPE_DISCONNECT_TX_COMMAND 2
+#define AVB_ACMP_MESSAGE_TYPE_DISCONNECT_TX_RESPONSE 3
+#define AVB_ACMP_MESSAGE_TYPE_GET_TX_STATE_COMMAND 4
+#define AVB_ACMP_MESSAGE_TYPE_GET_TX_STATE_RESPONSE 5
+#define AVB_ACMP_MESSAGE_TYPE_CONNECT_RX_COMMAND 6
+#define AVB_ACMP_MESSAGE_TYPE_CONNECT_RX_RESPONSE 7
+#define AVB_ACMP_MESSAGE_TYPE_DISCONNECT_RX_COMMAND 8
+#define AVB_ACMP_MESSAGE_TYPE_DISCONNECT_RX_RESPONSE 9
+#define AVB_ACMP_MESSAGE_TYPE_GET_RX_STATE_COMMAND 10
+#define AVB_ACMP_MESSAGE_TYPE_GET_RX_STATE_RESPONSE 11
+#define AVB_ACMP_MESSAGE_TYPE_GET_TX_CONNECTION_COMMAND 12
+#define AVB_ACMP_MESSAGE_TYPE_GET_TX_CONNECTION_RESPONSE 13
+
+#define AVB_ACMP_STATUS_SUCCESS 0
+#define AVB_ACMP_STATUS_LISTENER_UNKNOWN_ID 1
+#define AVB_ACMP_STATUS_TALKER_UNKNOWN_ID 2
+#define AVB_ACMP_STATUS_TALKER_DEST_MAC_FAIL 3
+#define AVB_ACMP_STATUS_TALKER_NO_STREAM_INDEX 4
+#define AVB_ACMP_STATUS_TALKER_NO_BANDWIDTH 5
+#define AVB_ACMP_STATUS_TALKER_EXCLUSIVE 6
+#define AVB_ACMP_STATUS_LISTENER_TALKER_TIMEOUT 7
+#define AVB_ACMP_STATUS_LISTENER_EXCLUSIVE 8
+#define AVB_ACMP_STATUS_STATE_UNAVAILABLE 9
+#define AVB_ACMP_STATUS_NOT_CONNECTED 10
+#define AVB_ACMP_STATUS_NO_SUCH_CONNECTION 11
+#define AVB_ACMP_STATUS_COULD_NOT_SEND_MESSAGE 12
+#define AVB_ACMP_STATUS_TALKER_MISBEHAVING 13
+#define AVB_ACMP_STATUS_LISTENER_MISBEHAVING 14
+#define AVB_ACMP_STATUS_RESERVED 15
+#define AVB_ACMP_STATUS_CONTROLLER_NOT_AUTHORIZED 16
+#define AVB_ACMP_STATUS_INCOMPATIBLE_REQUEST 17
+#define AVB_ACMP_STATUS_LISTENER_INVALID_CONNECTION 18
+#define AVB_ACMP_STATUS_NOT_SUPPORTED 31
+
+#define AVB_ACMP_TIMEOUT_CONNECT_TX_COMMAND_MS 2000
+#define AVB_ACMP_TIMEOUT_DISCONNECT_TX_COMMAND_MS 200
+#define AVB_ACMP_TIMEOUT_GET_TX_STATE_COMMAND 200
+#define AVB_ACMP_TIMEOUT_CONNECT_RX_COMMAND_MS 4500
+#define AVB_ACMP_TIMEOUT_DISCONNECT_RX_COMMAND_MS 500
+#define AVB_ACMP_TIMEOUT_GET_RX_STATE_COMMAND_MS 200
+#define AVB_ACMP_TIMEOUT_GET_TX_CONNECTION_COMMAND 200
+
+struct avb_packet_acmp {
+ struct avb_packet_header hdr;
+ uint64_t stream_id;
+ uint64_t controller_guid;
+ uint64_t talker_guid;
+ uint64_t listener_guid;
+ uint16_t talker_unique_id;
+ uint16_t listener_unique_id;
+ char stream_dest_mac[6];
+ uint16_t connection_count;
+ uint16_t sequence_id;
+ uint16_t flags;
+ uint16_t stream_vlan_id;
+ uint16_t reserved;
+} __attribute__ ((__packed__));
+
+#define AVB_PACKET_ACMP_SET_MESSAGE_TYPE(p,v) AVB_PACKET_SET_SUB1(&(p)->hdr, v)
+#define AVB_PACKET_ACMP_SET_STATUS(p,v) AVB_PACKET_SET_SUB2(&(p)->hdr, v)
+
+#define AVB_PACKET_ACMP_GET_MESSAGE_TYPE(p) AVB_PACKET_GET_SUB1(&(p)->hdr)
+#define AVB_PACKET_ACMP_GET_STATUS(p) AVB_PACKET_GET_SUB2(&(p)->hdr)
+
+struct avb_acmp *avb_acmp_register(struct server *server);
+
+#endif /* AVB_ACMP_H */
diff --git a/src/modules/module-avb/adp.c b/src/modules/module-avb/adp.c
new file mode 100644
index 0000000..6b13c41
--- /dev/null
+++ b/src/modules/module-avb/adp.c
@@ -0,0 +1,381 @@
+/* AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <spa/utils/json.h>
+
+#include <pipewire/pipewire.h>
+
+#include "adp.h"
+#include "aecp-aem-descriptors.h"
+#include "internal.h"
+#include "utils.h"
+
+static const uint8_t mac[6] = AVB_BROADCAST_MAC;
+
+struct entity {
+ struct spa_list link;
+ uint64_t entity_id;
+ uint64_t last_time;
+ int valid_time;
+ unsigned advertise:1;
+ size_t len;
+ uint8_t buf[128];
+};
+
+struct adp {
+ struct server *server;
+ struct spa_hook server_listener;
+
+ struct spa_list entities;
+ uint32_t available_index;
+};
+
+static struct entity *find_entity_by_id(struct adp *adp, uint64_t id)
+{
+ struct entity *e;
+ spa_list_for_each(e, &adp->entities, link)
+ if (e->entity_id == id)
+ return e;
+ return NULL;
+}
+static void entity_free(struct entity *e)
+{
+ spa_list_remove(&e->link);
+ free(e);
+}
+
+static int send_departing(struct adp *adp, uint64_t now, struct entity *e)
+{
+ struct avb_ethernet_header *h = (void*)e->buf;
+ struct avb_packet_adp *p = SPA_PTROFF(h, sizeof(*h), void);
+
+ AVB_PACKET_ADP_SET_MESSAGE_TYPE(p, AVB_ADP_MESSAGE_TYPE_ENTITY_DEPARTING);
+ p->available_index = htonl(adp->available_index++);
+ avb_server_send_packet(adp->server, mac, AVB_TSN_ETH, e->buf, e->len);
+ e->last_time = now;
+ return 0;
+}
+
+static int send_advertise(struct adp *adp, uint64_t now, struct entity *e)
+{
+ struct avb_ethernet_header *h = (void*)e->buf;
+ struct avb_packet_adp *p = SPA_PTROFF(h, sizeof(*h), void);
+
+ AVB_PACKET_ADP_SET_MESSAGE_TYPE(p, AVB_ADP_MESSAGE_TYPE_ENTITY_AVAILABLE);
+ p->available_index = htonl(adp->available_index++);
+ avb_server_send_packet(adp->server, mac, AVB_TSN_ETH, e->buf, e->len);
+ e->last_time = now;
+ return 0;
+}
+
+static int send_discover(struct adp *adp, uint64_t entity_id)
+{
+ uint8_t buf[128];
+ struct avb_ethernet_header *h = (void*)buf;
+ struct avb_packet_adp *p = SPA_PTROFF(h, sizeof(*h), void);
+ size_t len = sizeof(*h) + sizeof(*p);
+
+ spa_memzero(buf, sizeof(buf));
+ AVB_PACKET_SET_SUBTYPE(&p->hdr, AVB_SUBTYPE_ADP);
+ AVB_PACKET_SET_LENGTH(&p->hdr, AVB_ADP_CONTROL_DATA_LENGTH);
+ AVB_PACKET_ADP_SET_MESSAGE_TYPE(p, AVB_ADP_MESSAGE_TYPE_ENTITY_DISCOVER);
+ p->entity_id = htonl(entity_id);
+ avb_server_send_packet(adp->server, mac, AVB_TSN_ETH, buf, len);
+ return 0;
+}
+
+static int adp_message(void *data, uint64_t now, const void *message, int len)
+{
+ struct adp *adp = data;
+ struct server *server = adp->server;
+ const struct avb_ethernet_header *h = message;
+ const struct avb_packet_adp *p = SPA_PTROFF(h, sizeof(*h), void);
+ struct entity *e;
+ int message_type;
+ char buf[128];
+ uint64_t entity_id;
+
+ if (ntohs(h->type) != AVB_TSN_ETH)
+ return 0;
+ if (memcmp(h->dest, mac, 6) != 0 &&
+ memcmp(h->dest, server->mac_addr, 6) != 0)
+ return 0;
+
+ if (AVB_PACKET_GET_SUBTYPE(&p->hdr) != AVB_SUBTYPE_ADP ||
+ AVB_PACKET_GET_LENGTH(&p->hdr) < AVB_ADP_CONTROL_DATA_LENGTH)
+ return 0;
+
+ message_type = AVB_PACKET_ADP_GET_MESSAGE_TYPE(p);
+ entity_id = be64toh(p->entity_id);
+
+ e = find_entity_by_id(adp, entity_id);
+
+ switch (message_type) {
+ case AVB_ADP_MESSAGE_TYPE_ENTITY_AVAILABLE:
+ if (e == NULL) {
+ e = calloc(1, sizeof(*e));
+ if (e == NULL)
+ return -errno;
+
+ memcpy(e->buf, message, len);
+ e->len = len;
+ e->valid_time = AVB_PACKET_ADP_GET_VALID_TIME(p);
+ e->entity_id = entity_id;
+ spa_list_append(&adp->entities, &e->link);
+ pw_log_info("entity %s available",
+ avb_utils_format_id(buf, sizeof(buf), entity_id));
+ }
+ e->last_time = now;
+ break;
+ case AVB_ADP_MESSAGE_TYPE_ENTITY_DEPARTING:
+ if (e != NULL) {
+ pw_log_info("entity %s departing",
+ avb_utils_format_id(buf, sizeof(buf), entity_id));
+ entity_free(e);
+ }
+ break;
+ case AVB_ADP_MESSAGE_TYPE_ENTITY_DISCOVER:
+ pw_log_info("entity %s advertise",
+ avb_utils_format_id(buf, sizeof(buf), entity_id));
+ if (entity_id == 0UL) {
+ spa_list_for_each(e, &adp->entities, link)
+ if (e->advertise)
+ send_advertise(adp, now, e);
+ } else if (e != NULL &&
+ e->advertise && e->entity_id == entity_id) {
+ send_advertise(adp, now, e);
+ }
+ break;
+ default:
+ return -EINVAL;
+ }
+ return 0;
+}
+
+static void adp_destroy(void *data)
+{
+ struct adp *adp = data;
+ spa_hook_remove(&adp->server_listener);
+ free(adp);
+}
+
+static void check_timeout(struct adp *adp, uint64_t now)
+{
+ struct entity *e, *t;
+ char buf[128];
+
+ spa_list_for_each_safe(e, t, &adp->entities, link) {
+ if (e->last_time + (e->valid_time + 2) * SPA_NSEC_PER_SEC > now)
+ continue;
+
+ pw_log_info("entity %s timeout",
+ avb_utils_format_id(buf, sizeof(buf), e->entity_id));
+
+ if (e->advertise)
+ send_departing(adp, now, e);
+
+ entity_free(e);
+ }
+}
+static void check_readvertize(struct adp *adp, uint64_t now, struct entity *e)
+{
+ char buf[128];
+
+ if (!e->advertise)
+ return;
+
+ if (e->last_time + (e->valid_time / 2) * SPA_NSEC_PER_SEC > now)
+ return;
+
+ pw_log_debug("entity %s readvertise",
+ avb_utils_format_id(buf, sizeof(buf), e->entity_id));
+
+ send_advertise(adp, now, e);
+}
+
+static int check_advertise(struct adp *adp, uint64_t now)
+{
+ struct server *server = adp->server;
+ const struct descriptor *d;
+ struct avb_aem_desc_entity *entity;
+ struct avb_aem_desc_avb_interface *avb_interface;
+ struct entity *e;
+ uint64_t entity_id;
+ struct avb_ethernet_header *h;
+ struct avb_packet_adp *p;
+ char buf[128];
+
+ d = server_find_descriptor(server, AVB_AEM_DESC_ENTITY, 0);
+ if (d == NULL)
+ return 0;
+
+ entity = d->ptr;
+ entity_id = be64toh(entity->entity_id);
+
+ if ((e = find_entity_by_id(adp, entity_id)) != NULL) {
+ if (e->advertise)
+ check_readvertize(adp, now, e);
+ return 0;
+ }
+
+ d = server_find_descriptor(server, AVB_AEM_DESC_AVB_INTERFACE, 0);
+ avb_interface = d ? d->ptr : NULL;
+
+ pw_log_info("entity %s advertise",
+ avb_utils_format_id(buf, sizeof(buf), entity_id));
+
+ e = calloc(1, sizeof(*e));
+ if (e == NULL)
+ return -errno;
+
+ e->advertise = true;
+ e->valid_time = 10;
+ e->last_time = now;
+ e->entity_id = entity_id;
+ e->len = sizeof(*h) + sizeof(*p);
+
+ h = (void*)e->buf;
+ p = SPA_PTROFF(h, sizeof(*h), void);
+ AVB_PACKET_SET_LENGTH(&p->hdr, AVB_ADP_CONTROL_DATA_LENGTH);
+ AVB_PACKET_SET_SUBTYPE(&p->hdr, AVB_SUBTYPE_ADP);
+ AVB_PACKET_ADP_SET_MESSAGE_TYPE(p, AVB_ADP_MESSAGE_TYPE_ENTITY_AVAILABLE);
+ AVB_PACKET_ADP_SET_VALID_TIME(p, e->valid_time);
+
+ p->entity_id = entity->entity_id;
+ p->entity_model_id = entity->entity_model_id;
+ p->entity_capabilities = entity->entity_capabilities;
+ p->talker_stream_sources = entity->talker_stream_sources;
+ p->talker_capabilities = entity->talker_capabilities;
+ p->listener_stream_sinks = entity->listener_stream_sinks;
+ p->listener_capabilities = entity->listener_capabilities;
+ p->controller_capabilities = entity->controller_capabilities;
+ p->available_index = entity->available_index;
+ if (avb_interface) {
+ p->gptp_grandmaster_id = avb_interface->clock_identity;
+ p->gptp_domain_number = avb_interface->domain_number;
+ }
+ p->identify_control_index = 0;
+ p->interface_index = 0;
+ p->association_id = entity->association_id;
+
+ spa_list_append(&adp->entities, &e->link);
+
+ return 0;
+}
+
+static void adp_periodic(void *data, uint64_t now)
+{
+ struct adp *adp = data;
+ check_timeout(adp, now);
+ check_advertise(adp, now);
+}
+
+static int do_help(struct adp *adp, const char *args, FILE *out)
+{
+ fprintf(out, "{ \"type\": \"help\","
+ "\"text\": \""
+ "/adp/help: this help \\n"
+ "/adp/discover [{ \"entity-id\": <id> }] : trigger discover\\n"
+ "\" }");
+ return 0;
+}
+
+static int do_discover(struct adp *adp, const char *args, FILE *out)
+{
+ struct spa_json it[2];
+ char key[128];
+ uint64_t entity_id = 0ULL;
+
+ spa_json_init(&it[0], args, strlen(args));
+ if (spa_json_enter_object(&it[0], &it[1]) <= 0)
+ return -EINVAL;
+
+ while (spa_json_get_string(&it[1], key, sizeof(key)) > 0) {
+ int len;
+ const char *value;
+ uint64_t id_val;
+
+ if ((len = spa_json_next(&it[1], &value)) <= 0)
+ break;
+
+ if (spa_json_is_null(value, len))
+ continue;
+
+ if (spa_streq(key, "entity-id")) {
+ if (avb_utils_parse_id(value, len, &id_val) >= 0)
+ entity_id = id_val;
+ }
+ }
+ send_discover(adp, entity_id);
+ return 0;
+}
+
+static int adp_command(void *data, uint64_t now, const char *command, const char *args, FILE *out)
+{
+ struct adp *adp = data;
+ int res;
+
+ if (!spa_strstartswith(command, "/adp/"))
+ return 0;
+
+ command += strlen("/adp/");
+
+ if (spa_streq(command, "help"))
+ res = do_help(adp, args, out);
+ else if (spa_streq(command, "discover"))
+ res = do_discover(adp, args, out);
+ else
+ res = -ENOTSUP;
+
+ return res;
+}
+
+static const struct server_events server_events = {
+ AVB_VERSION_SERVER_EVENTS,
+ .destroy = adp_destroy,
+ .message = adp_message,
+ .periodic = adp_periodic,
+ .command = adp_command
+};
+
+struct avb_adp *avb_adp_register(struct server *server)
+{
+ struct adp *adp;
+
+ adp = calloc(1, sizeof(*adp));
+ if (adp == NULL)
+ return NULL;
+
+ adp->server = server;
+ spa_list_init(&adp->entities);
+
+ avdecc_server_add_listener(server, &adp->server_listener, &server_events, adp);
+
+ return (struct avb_adp*)adp;
+}
+
+void avb_adp_unregister(struct avb_adp *adp)
+{
+ adp_destroy(adp);
+}
diff --git a/src/modules/module-avb/adp.h b/src/modules/module-avb/adp.h
new file mode 100644
index 0000000..c546088
--- /dev/null
+++ b/src/modules/module-avb/adp.h
@@ -0,0 +1,105 @@
+/* AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef AVB_ADP_H
+#define AVB_ADP_H
+
+#include "packets.h"
+#include "internal.h"
+
+#define AVB_ADP_MESSAGE_TYPE_ENTITY_AVAILABLE 0
+#define AVB_ADP_MESSAGE_TYPE_ENTITY_DEPARTING 1
+#define AVB_ADP_MESSAGE_TYPE_ENTITY_DISCOVER 2
+
+#define AVB_ADP_ENTITY_CAPABILITY_EFU_MODE (1u<<0)
+#define AVB_ADP_ENTITY_CAPABILITY_ADDRESS_ACCESS_SUPPORTED (1u<<1)
+#define AVB_ADP_ENTITY_CAPABILITY_GATEWAY_ENTITY (1u<<2)
+#define AVB_ADP_ENTITY_CAPABILITY_AEM_SUPPORTED (1u<<3)
+#define AVB_ADP_ENTITY_CAPABILITY_LEGACY_AVC (1u<<4)
+#define AVB_ADP_ENTITY_CAPABILITY_ASSOCIATION_ID_SUPPORTED (1u<<5)
+#define AVB_ADP_ENTITY_CAPABILITY_ASSOCIATION_ID_VALID (1u<<6)
+#define AVB_ADP_ENTITY_CAPABILITY_VENDOR_UNIQUE_SUPPORTED (1u<<7)
+#define AVB_ADP_ENTITY_CAPABILITY_CLASS_A_SUPPORTED (1u<<8)
+#define AVB_ADP_ENTITY_CAPABILITY_CLASS_B_SUPPORTED (1u<<9)
+#define AVB_ADP_ENTITY_CAPABILITY_GPTP_SUPPORTED (1u<<10)
+#define AVB_ADP_ENTITY_CAPABILITY_AEM_AUTHENTICATION_SUPPORTED (1u<<11)
+#define AVB_ADP_ENTITY_CAPABILITY_AEM_AUTHENTICATION_REQUIRED (1u<<12)
+#define AVB_ADP_ENTITY_CAPABILITY_AEM_PERSISTENT_ACQUIRE_SUPPORTED (1u<<13)
+#define AVB_ADP_ENTITY_CAPABILITY_AEM_IDENTIFY_CONTROL_INDEX_VALID (1u<<14)
+#define AVB_ADP_ENTITY_CAPABILITY_AEM_INTERFACE_INDEX_VALID (1u<<15)
+#define AVB_ADP_ENTITY_CAPABILITY_GENERAL_CONTROLLER_IGNORE (1u<<16)
+#define AVB_ADP_ENTITY_CAPABILITY_ENTITY_NOT_READY (1u<<17)
+
+#define AVB_ADP_TALKER_CAPABILITY_IMPLEMENTED (1u<<0)
+#define AVB_ADP_TALKER_CAPABILITY_OTHER_SOURCE (1u<<9)
+#define AVB_ADP_TALKER_CAPABILITY_CONTROL_SOURCE (1u<<10)
+#define AVB_ADP_TALKER_CAPABILITY_MEDIA_CLOCK_SOURCE (1u<<11)
+#define AVB_ADP_TALKER_CAPABILITY_SMPTE_SOURCE (1u<<12)
+#define AVB_ADP_TALKER_CAPABILITY_MIDI_SOURCE (1u<<13)
+#define AVB_ADP_TALKER_CAPABILITY_AUDIO_SOURCE (1u<<14)
+#define AVB_ADP_TALKER_CAPABILITY_VIDEO_SOURCE (1u<<15)
+
+#define AVB_ADP_LISTENER_CAPABILITY_IMPLEMENTED (1u<<0)
+#define AVB_ADP_LISTENER_CAPABILITY_OTHER_SINK (1u<<9)
+#define AVB_ADP_LISTENER_CAPABILITY_CONTROL_SINK (1u<<10)
+#define AVB_ADP_LISTENER_CAPABILITY_MEDIA_CLOCK_SINK (1u<<11)
+#define AVB_ADP_LISTENER_CAPABILITY_SMPTE_SINK (1u<<12)
+#define AVB_ADP_LISTENER_CAPABILITY_MIDI_SINK (1u<<13)
+#define AVB_ADP_LISTENER_CAPABILITY_AUDIO_SINK (1u<<14)
+#define AVB_ADP_LISTENER_CAPABILITY_VIDEO_SINK (1u<<15)
+
+#define AVB_ADP_CONTROLLER_CAPABILITY_IMPLEMENTED (1u<<0)
+#define AVB_ADP_CONTROLLER_CAPABILITY_LAYER3_PROXY (1u<<1)
+
+#define AVB_ADP_CONTROL_DATA_LENGTH 56
+
+struct avb_packet_adp {
+ struct avb_packet_header hdr;
+ uint64_t entity_id;
+ uint64_t entity_model_id;
+ uint32_t entity_capabilities;
+ uint16_t talker_stream_sources;
+ uint16_t talker_capabilities;
+ uint16_t listener_stream_sinks;
+ uint16_t listener_capabilities;
+ uint32_t controller_capabilities;
+ uint32_t available_index;
+ uint64_t gptp_grandmaster_id;
+ uint8_t gptp_domain_number;
+ uint8_t reserved0[3];
+ uint16_t identify_control_index;
+ uint16_t interface_index;
+ uint64_t association_id;
+ uint32_t reserved1;
+} __attribute__ ((__packed__));
+
+#define AVB_PACKET_ADP_SET_MESSAGE_TYPE(p,v) AVB_PACKET_SET_SUB1(&(p)->hdr, v)
+#define AVB_PACKET_ADP_SET_VALID_TIME(p,v) AVB_PACKET_SET_SUB2(&(p)->hdr, v)
+
+#define AVB_PACKET_ADP_GET_MESSAGE_TYPE(p) AVB_PACKET_GET_SUB1(&(p)->hdr)
+#define AVB_PACKET_ADP_GET_VALID_TIME(p) AVB_PACKET_GET_SUB2(&(p)->hdr)
+
+struct avb_adp *avb_adp_register(struct server *server);
+
+#endif /* AVB_ADP_H */
diff --git a/src/modules/module-avb/aecp-aem-descriptors.h b/src/modules/module-avb/aecp-aem-descriptors.h
new file mode 100644
index 0000000..101c331
--- /dev/null
+++ b/src/modules/module-avb/aecp-aem-descriptors.h
@@ -0,0 +1,247 @@
+/* AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef AVB_AECP_AEM_DESCRIPTORS_H
+#define AVB_AECP_AEM_DESCRIPTORS_H
+
+#include "internal.h"
+
+#define AVB_AEM_DESC_ENTITY 0x0000
+#define AVB_AEM_DESC_CONFIGURATION 0x0001
+#define AVB_AEM_DESC_AUDIO_UNIT 0x0002
+#define AVB_AEM_DESC_VIDEO_UNIT 0x0003
+#define AVB_AEM_DESC_SENSOR_UNIT 0x0004
+#define AVB_AEM_DESC_STREAM_INPUT 0x0005
+#define AVB_AEM_DESC_STREAM_OUTPUT 0x0006
+#define AVB_AEM_DESC_JACK_INPUT 0x0007
+#define AVB_AEM_DESC_JACK_OUTPUT 0x0008
+#define AVB_AEM_DESC_AVB_INTERFACE 0x0009
+#define AVB_AEM_DESC_CLOCK_SOURCE 0x000a
+#define AVB_AEM_DESC_MEMORY_OBJECT 0x000b
+#define AVB_AEM_DESC_LOCALE 0x000c
+#define AVB_AEM_DESC_STRINGS 0x000d
+#define AVB_AEM_DESC_STREAM_PORT_INPUT 0x000e
+#define AVB_AEM_DESC_STREAM_PORT_OUTPUT 0x000f
+#define AVB_AEM_DESC_EXTERNAL_PORT_INPUT 0x0010
+#define AVB_AEM_DESC_EXTERNAL_PORT_OUTPUT 0x0011
+#define AVB_AEM_DESC_INTERNAL_PORT_INPUT 0x0012
+#define AVB_AEM_DESC_INTERNAL_PORT_OUTPUT 0x0013
+#define AVB_AEM_DESC_AUDIO_CLUSTER 0x0014
+#define AVB_AEM_DESC_VIDEO_CLUSTER 0x0015
+#define AVB_AEM_DESC_SENSOR_CLUSTER 0x0016
+#define AVB_AEM_DESC_AUDIO_MAP 0x0017
+#define AVB_AEM_DESC_VIDEO_MAP 0x0018
+#define AVB_AEM_DESC_SENSOR_MAP 0x0019
+#define AVB_AEM_DESC_CONTROL 0x001a
+#define AVB_AEM_DESC_SIGNAL_SELECTOR 0x001b
+#define AVB_AEM_DESC_MIXER 0x001c
+#define AVB_AEM_DESC_MATRIX 0x001d
+#define AVB_AEM_DESC_MATRIX_SIGNAL 0x001e
+#define AVB_AEM_DESC_SIGNAL_SPLITTER 0x001f
+#define AVB_AEM_DESC_SIGNAL_COMBINER 0x0020
+#define AVB_AEM_DESC_SIGNAL_DEMULTIPLEXER 0x0021
+#define AVB_AEM_DESC_SIGNAL_MULTIPLEXER 0x0022
+#define AVB_AEM_DESC_SIGNAL_TRANSCODER 0x0023
+#define AVB_AEM_DESC_CLOCK_DOMAIN 0x0024
+#define AVB_AEM_DESC_CONTROL_BLOCK 0x0025
+#define AVB_AEM_DESC_INVALID 0xffff
+
+struct avb_aem_desc_entity {
+ uint64_t entity_id;
+ uint64_t entity_model_id;
+ uint32_t entity_capabilities;
+ uint16_t talker_stream_sources;
+ uint16_t talker_capabilities;
+ uint16_t listener_stream_sinks;
+ uint16_t listener_capabilities;
+ uint32_t controller_capabilities;
+ uint32_t available_index;
+ uint64_t association_id;
+ char entity_name[64];
+ uint16_t vendor_name_string;
+ uint16_t model_name_string;
+ char firmware_version[64];
+ char group_name[64];
+ char serial_number[64];
+ uint16_t configurations_count;
+ uint16_t current_configuration;
+} __attribute__ ((__packed__));
+
+struct avb_aem_desc_descriptor_count {
+ uint16_t descriptor_type;
+ uint16_t descriptor_count;
+} __attribute__ ((__packed__));
+
+struct avb_aem_desc_configuration {
+ char object_name[64];
+ uint16_t localized_description;
+ uint16_t descriptor_counts_count;
+ uint16_t descriptor_counts_offset;
+ struct avb_aem_desc_descriptor_count descriptor_counts[0];
+} __attribute__ ((__packed__));
+
+struct avb_aem_desc_sampling_rate {
+ uint32_t pull_frequency;
+} __attribute__ ((__packed__));
+
+struct avb_aem_desc_audio_unit {
+ char object_name[64];
+ uint16_t localized_description;
+ uint16_t clock_domain_index;
+ uint16_t number_of_stream_input_ports;
+ uint16_t base_stream_input_port;
+ uint16_t number_of_stream_output_ports;
+ uint16_t base_stream_output_port;
+ uint16_t number_of_external_input_ports;
+ uint16_t base_external_input_port;
+ uint16_t number_of_external_output_ports;
+ uint16_t base_external_output_port;
+ uint16_t number_of_internal_input_ports;
+ uint16_t base_internal_input_port;
+ uint16_t number_of_internal_output_ports;
+ uint16_t base_internal_output_port;
+ uint16_t number_of_controls;
+ uint16_t base_control;
+ uint16_t number_of_signal_selectors;
+ uint16_t base_signal_selector;
+ uint16_t number_of_mixers;
+ uint16_t base_mixer;
+ uint16_t number_of_matrices;
+ uint16_t base_matrix;
+ uint16_t number_of_splitters;
+ uint16_t base_splitter;
+ uint16_t number_of_combiners;
+ uint16_t base_combiner;
+ uint16_t number_of_demultiplexers;
+ uint16_t base_demultiplexer;
+ uint16_t number_of_multiplexers;
+ uint16_t base_multiplexer;
+ uint16_t number_of_transcoders;
+ uint16_t base_transcoder;
+ uint16_t number_of_control_blocks;
+ uint16_t base_control_block;
+ uint32_t current_sampling_rate;
+ uint16_t sampling_rates_offset;
+ uint16_t sampling_rates_count;
+ struct avb_aem_desc_sampling_rate sampling_rates[0];
+} __attribute__ ((__packed__));
+
+#define AVB_AEM_DESC_STREAM_FLAG_SYNC_SOURCE (1u<<0)
+#define AVB_AEM_DESC_STREAM_FLAG_CLASS_A (1u<<1)
+#define AVB_AEM_DESC_STREAM_FLAG_CLASS_B (1u<<2)
+#define AVB_AEM_DESC_STREAM_FLAG_SUPPORTS_ENCRYPTED (1u<<3)
+#define AVB_AEM_DESC_STREAM_FLAG_PRIMARY_BACKUP_SUPPORTED (1u<<4)
+#define AVB_AEM_DESC_STREAM_FLAG_PRIMARY_BACKUP_VALID (1u<<5)
+#define AVB_AEM_DESC_STREAM_FLAG_SECONDARY_BACKUP_SUPPORTED (1u<<6)
+#define AVB_AEM_DESC_STREAM_FLAG_SECONDARY_BACKUP_VALID (1u<<7)
+#define AVB_AEM_DESC_STREAM_FLAG_TERTIARY_BACKUP_SUPPORTED (1u<<8)
+#define AVB_AEM_DESC_STREAM_FLAG_TERTIARY_BACKUP_VALID (1u<<9)
+
+struct avb_aem_desc_stream {
+ char object_name[64];
+ uint16_t localized_description;
+ uint16_t clock_domain_index;
+ uint16_t stream_flags;
+ uint64_t current_format;
+ uint16_t formats_offset;
+ uint16_t number_of_formats;
+ uint64_t backup_talker_entity_id_0;
+ uint16_t backup_talker_unique_id_0;
+ uint64_t backup_talker_entity_id_1;
+ uint16_t backup_talker_unique_id_1;
+ uint64_t backup_talker_entity_id_2;
+ uint16_t backup_talker_unique_id_2;
+ uint64_t backedup_talker_entity_id;
+ uint16_t backedup_talker_unique;
+ uint16_t avb_interface_index;
+ uint32_t buffer_length;
+ uint64_t stream_formats[0];
+} __attribute__ ((__packed__));
+
+#define AVB_AEM_DESC_AVB_INTERFACE_FLAG_GPTP_GRANDMASTER_SUPPORTED (1<<0)
+#define AVB_AEM_DESC_AVB_INTERFACE_FLAG_GPTP_SUPPORTED (1<<1)
+#define AVB_AEM_DESC_AVB_INTERFACE_FLAG_SRP_SUPPORTED (1<<2)
+
+struct avb_aem_desc_avb_interface {
+ char object_name[64];
+ uint16_t localized_description;
+ uint8_t mac_address[6];
+ uint16_t interface_flags;
+ uint64_t clock_identity;
+ uint8_t priority1;
+ uint8_t clock_class;
+ uint16_t offset_scaled_log_variance;
+ uint8_t clock_accuracy;
+ uint8_t priority2;
+ uint8_t domain_number;
+ int8_t log_sync_interval;
+ int8_t log_announce_interval;
+ int8_t log_pdelay_interval;
+ uint16_t port_number;
+} __attribute__ ((__packed__));
+
+#define AVB_AEM_DESC_CLOCK_SOURCE_TYPE_INTERNAL 0x0000
+#define AVB_AEM_DESC_CLOCK_SOURCE_TYPE_EXTERNAL 0x0001
+#define AVB_AEM_DESC_CLOCK_SOURCE_TYPE_INPUT_STREAM 0x0002
+#define AVB_AEM_DESC_CLOCK_SOURCE_TYPE_MEDIA_CLOCK_STREAM 0x0003
+#define AVB_AEM_DESC_CLOCK_SOURCE_TYPE_EXPANSION 0xffff
+
+struct avb_aem_desc_clock_source {
+ char object_name[64];
+ uint16_t localized_description;
+ uint16_t clock_source_flags;
+ uint16_t clock_source_type;
+ uint64_t clock_source_identifier;
+ uint16_t clock_source_location_type;
+ uint16_t clock_source_location_index;
+} __attribute__ ((__packed__));
+
+struct avb_aem_desc_locale {
+ char locale_identifier[64];
+ uint16_t number_of_strings;
+ uint16_t base_strings;
+} __attribute__ ((__packed__));
+
+struct avb_aem_desc_strings {
+ char string_0[64];
+ char string_1[64];
+ char string_2[64];
+ char string_3[64];
+ char string_4[64];
+ char string_5[64];
+ char string_6[64];
+} __attribute__ ((__packed__));
+
+struct avb_aem_desc_stream_port {
+ uint16_t clock_domain_index;
+ uint16_t port_flags;
+ uint16_t number_of_controls;
+ uint16_t base_control;
+ uint16_t number_of_clusters;
+ uint16_t base_cluster;
+ uint16_t number_of_maps;
+ uint16_t base_map;
+} __attribute__ ((__packed__));
+
+#endif /* AVB_AECP_AEM_DESCRIPTORS_H */
diff --git a/src/modules/module-avb/aecp-aem.c b/src/modules/module-avb/aecp-aem.c
new file mode 100644
index 0000000..d191330
--- /dev/null
+++ b/src/modules/module-avb/aecp-aem.c
@@ -0,0 +1,285 @@
+/* AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "aecp-aem.h"
+#include "aecp-aem-descriptors.h"
+
+static int reply_status(struct aecp *aecp, int status, const void *m, int len)
+{
+ struct server *server = aecp->server;
+ uint8_t buf[len];
+ struct avb_ethernet_header *h = (void*)buf;
+ struct avb_packet_aecp_header *reply = SPA_PTROFF(h, sizeof(*h), void);
+
+ memcpy(buf, m, len);
+ AVB_PACKET_AECP_SET_MESSAGE_TYPE(reply, AVB_AECP_MESSAGE_TYPE_AEM_RESPONSE);
+ AVB_PACKET_AECP_SET_STATUS(reply, status);
+
+ return avb_server_send_packet(server, h->src, AVB_TSN_ETH, buf, len);
+}
+
+static int reply_not_implemented(struct aecp *aecp, const void *m, int len)
+{
+ return reply_status(aecp, AVB_AECP_AEM_STATUS_NOT_IMPLEMENTED, m, len);
+}
+
+static int reply_success(struct aecp *aecp, const void *m, int len)
+{
+ return reply_status(aecp, AVB_AECP_AEM_STATUS_SUCCESS, m, len);
+}
+
+/* ACQUIRE_ENTITY */
+static int handle_acquire_entity(struct aecp *aecp, const void *m, int len)
+{
+ struct server *server = aecp->server;
+ const struct avb_packet_aecp_aem *p = m;
+ const struct avb_packet_aecp_aem_acquire *ae;
+ const struct descriptor *desc;
+ uint16_t desc_type, desc_id;
+
+ ae = (const struct avb_packet_aecp_aem_acquire*)p->payload;
+
+ desc_type = ntohs(ae->descriptor_type);
+ desc_id = ntohs(ae->descriptor_id);
+
+ desc = server_find_descriptor(server, desc_type, desc_id);
+ if (desc == NULL)
+ return reply_status(aecp, AVB_AECP_AEM_STATUS_NO_SUCH_DESCRIPTOR, p, len);
+
+ if (desc_type != AVB_AEM_DESC_ENTITY || desc_id != 0)
+ return reply_not_implemented(aecp, m, len);
+
+ return reply_success(aecp, m, len);
+}
+
+/* LOCK_ENTITY */
+static int handle_lock_entity(struct aecp *aecp, const void *m, int len)
+{
+ struct server *server = aecp->server;
+ const struct avb_packet_aecp_aem *p = m;
+ const struct avb_packet_aecp_aem_acquire *ae;
+ const struct descriptor *desc;
+ uint16_t desc_type, desc_id;
+
+ ae = (const struct avb_packet_aecp_aem_acquire*)p->payload;
+
+ desc_type = ntohs(ae->descriptor_type);
+ desc_id = ntohs(ae->descriptor_id);
+
+ desc = server_find_descriptor(server, desc_type, desc_id);
+ if (desc == NULL)
+ return reply_status(aecp, AVB_AECP_AEM_STATUS_NO_SUCH_DESCRIPTOR, p, len);
+
+ if (desc_type != AVB_AEM_DESC_ENTITY || desc_id != 0)
+ return reply_not_implemented(aecp, m, len);
+
+ return reply_success(aecp, m, len);
+}
+
+/* READ_DESCRIPTOR */
+static int handle_read_descriptor(struct aecp *aecp, const void *m, int len)
+{
+ struct server *server = aecp->server;
+ const struct avb_ethernet_header *h = m;
+ const struct avb_packet_aecp_aem *p = SPA_PTROFF(h, sizeof(*h), void);
+ struct avb_packet_aecp_aem *reply;
+ const struct avb_packet_aecp_aem_read_descriptor *rd;
+ uint16_t desc_type, desc_id;
+ const struct descriptor *desc;
+ uint8_t buf[2048];
+ size_t size, psize;
+
+ rd = (struct avb_packet_aecp_aem_read_descriptor*)p->payload;
+
+ desc_type = ntohs(rd->descriptor_type);
+ desc_id = ntohs(rd->descriptor_id);
+
+ pw_log_info("descriptor type:%04x index:%d", desc_type, desc_id);
+
+ desc = server_find_descriptor(server, desc_type, desc_id);
+ if (desc == NULL)
+ return reply_status(aecp, AVB_AECP_AEM_STATUS_NO_SUCH_DESCRIPTOR, m, len);
+
+ memcpy(buf, m, len);
+
+ psize = sizeof(*rd);
+ size = sizeof(*h) + sizeof(*reply) + psize;
+
+ memcpy(buf + size, desc->ptr, desc->size);
+ size += desc->size;
+ psize += desc->size;
+
+ h = (void*)buf;
+ reply = SPA_PTROFF(h, sizeof(*h), void);
+ AVB_PACKET_AECP_SET_MESSAGE_TYPE(&reply->aecp, AVB_AECP_MESSAGE_TYPE_AEM_RESPONSE);
+ AVB_PACKET_AECP_SET_STATUS(&reply->aecp, AVB_AECP_AEM_STATUS_SUCCESS);
+ AVB_PACKET_SET_LENGTH(&reply->aecp.hdr, psize + 12);
+
+ return avb_server_send_packet(server, h->src, AVB_TSN_ETH, buf, size);
+}
+
+/* GET_AVB_INFO */
+static int handle_get_avb_info(struct aecp *aecp, const void *m, int len)
+{
+ struct server *server = aecp->server;
+ const struct avb_ethernet_header *h = m;
+ const struct avb_packet_aecp_aem *p = SPA_PTROFF(h, sizeof(*h), void);
+ struct avb_packet_aecp_aem *reply;
+ struct avb_packet_aecp_aem_get_avb_info *i;
+ struct avb_aem_desc_avb_interface *avb_interface;
+ uint16_t desc_type, desc_id;
+ const struct descriptor *desc;
+ uint8_t buf[2048];
+ size_t size, psize;
+
+ i = (struct avb_packet_aecp_aem_get_avb_info*)p->payload;
+
+ desc_type = ntohs(i->descriptor_type);
+ desc_id = ntohs(i->descriptor_id);
+
+ desc = server_find_descriptor(server, desc_type, desc_id);
+ if (desc == NULL)
+ return reply_status(aecp, AVB_AECP_AEM_STATUS_NO_SUCH_DESCRIPTOR, m, len);
+
+ if (desc_type != AVB_AEM_DESC_AVB_INTERFACE || desc_id != 0)
+ return reply_not_implemented(aecp, m, len);
+
+ avb_interface = desc->ptr;
+
+ memcpy(buf, m, len);
+
+ psize = sizeof(*i);
+ size = sizeof(*h) + sizeof(*reply) + psize;
+
+ h = (void*)buf;
+ reply = SPA_PTROFF(h, sizeof(*h), void);
+ AVB_PACKET_AECP_SET_MESSAGE_TYPE(&reply->aecp, AVB_AECP_MESSAGE_TYPE_AEM_RESPONSE);
+ AVB_PACKET_AECP_SET_STATUS(&reply->aecp, AVB_AECP_AEM_STATUS_SUCCESS);
+ AVB_PACKET_SET_LENGTH(&reply->aecp.hdr, psize + 12);
+
+ i = (struct avb_packet_aecp_aem_get_avb_info*)reply->payload;
+ i->gptp_grandmaster_id = avb_interface->clock_identity;
+ i->propagation_delay = htonl(0);
+ i->gptp_domain_number = avb_interface->domain_number;
+ i->flags = 0;
+ i->msrp_mappings_count = htons(0);
+
+ return avb_server_send_packet(server, h->src, AVB_TSN_ETH, buf, size);
+}
+
+/* AEM_COMMAND */
+struct cmd_info {
+ uint16_t type;
+ const char *name;
+ int (*handle) (struct aecp *aecp, const void *p, int len);
+};
+
+static const struct cmd_info cmd_info[] = {
+ { AVB_AECP_AEM_CMD_ACQUIRE_ENTITY, "acquire-entity", handle_acquire_entity, },
+ { AVB_AECP_AEM_CMD_LOCK_ENTITY, "lock-entity", handle_lock_entity, },
+ { AVB_AECP_AEM_CMD_ENTITY_AVAILABLE, "entity-available", NULL, },
+ { AVB_AECP_AEM_CMD_CONTROLLER_AVAILABLE, "controller-available", NULL, },
+ { AVB_AECP_AEM_CMD_READ_DESCRIPTOR, "read-descriptor", handle_read_descriptor, },
+ { AVB_AECP_AEM_CMD_WRITE_DESCRIPTOR, "write-descriptor", NULL, },
+ { AVB_AECP_AEM_CMD_SET_CONFIGURATION, "set-configuration", NULL, },
+ { AVB_AECP_AEM_CMD_GET_CONFIGURATION, "get-configuration", NULL, },
+ { AVB_AECP_AEM_CMD_SET_STREAM_FORMAT, "set-stream-format", NULL, },
+ { AVB_AECP_AEM_CMD_GET_STREAM_FORMAT, "get-stream-format", NULL, },
+ { AVB_AECP_AEM_CMD_SET_VIDEO_FORMAT, "set-video-format", NULL, },
+ { AVB_AECP_AEM_CMD_GET_VIDEO_FORMAT, "get-video-format", NULL, },
+ { AVB_AECP_AEM_CMD_SET_SENSOR_FORMAT, "set-sensor-format", NULL, },
+ { AVB_AECP_AEM_CMD_GET_SENSOR_FORMAT, "get-sensor-format", NULL, },
+ { AVB_AECP_AEM_CMD_SET_STREAM_INFO, "set-stream-info", NULL, },
+ { AVB_AECP_AEM_CMD_GET_STREAM_INFO, "get-stream-info", NULL, },
+ { AVB_AECP_AEM_CMD_SET_NAME, "set-name", NULL, },
+ { AVB_AECP_AEM_CMD_GET_NAME, "get-name", NULL, },
+ { AVB_AECP_AEM_CMD_SET_ASSOCIATION_ID, "set-association-id", NULL, },
+ { AVB_AECP_AEM_CMD_GET_ASSOCIATION_ID, "get-association-id", NULL, },
+ { AVB_AECP_AEM_CMD_SET_SAMPLING_RATE, "set-sampling-rate", NULL, },
+ { AVB_AECP_AEM_CMD_GET_SAMPLING_RATE, "get-sampling-rate", NULL, },
+ { AVB_AECP_AEM_CMD_SET_CLOCK_SOURCE, "set-clock-source", NULL, },
+ { AVB_AECP_AEM_CMD_GET_CLOCK_SOURCE, "get-clock-source", NULL, },
+ { AVB_AECP_AEM_CMD_SET_CONTROL, "set-control", NULL, },
+ { AVB_AECP_AEM_CMD_GET_CONTROL, "get-control", NULL, },
+ { AVB_AECP_AEM_CMD_INCREMENT_CONTROL, "increment-control", NULL, },
+ { AVB_AECP_AEM_CMD_DECREMENT_CONTROL, "decrement-control", NULL, },
+ { AVB_AECP_AEM_CMD_SET_SIGNAL_SELECTOR, "set-signal-selector", NULL, },
+ { AVB_AECP_AEM_CMD_GET_SIGNAL_SELECTOR, "get-signal-selector", NULL, },
+ { AVB_AECP_AEM_CMD_SET_MIXER, "set-mixer", NULL, },
+ { AVB_AECP_AEM_CMD_GET_MIXER, "get-mixer", NULL, },
+ { AVB_AECP_AEM_CMD_SET_MATRIX, "set-matrix", NULL, },
+ { AVB_AECP_AEM_CMD_GET_MATRIX, "get-matrix", NULL, },
+ { AVB_AECP_AEM_CMD_START_STREAMING, "start-streaming", NULL, },
+ { AVB_AECP_AEM_CMD_STOP_STREAMING, "stop-streaming", NULL, },
+ { AVB_AECP_AEM_CMD_REGISTER_UNSOLICITED_NOTIFICATION, "register-unsolicited-notification", NULL, },
+ { AVB_AECP_AEM_CMD_DEREGISTER_UNSOLICITED_NOTIFICATION, "deregister-unsolicited-notification", NULL, },
+ { AVB_AECP_AEM_CMD_IDENTIFY_NOTIFICATION, "identify-notification", NULL, },
+ { AVB_AECP_AEM_CMD_GET_AVB_INFO, "get-avb-info", handle_get_avb_info, },
+ { AVB_AECP_AEM_CMD_GET_AS_PATH, "get-as-path", NULL, },
+ { AVB_AECP_AEM_CMD_GET_COUNTERS, "get-counters", NULL, },
+ { AVB_AECP_AEM_CMD_REBOOT, "reboot", NULL, },
+ { AVB_AECP_AEM_CMD_GET_AUDIO_MAP, "get-audio-map", NULL, },
+ { AVB_AECP_AEM_CMD_ADD_AUDIO_MAPPINGS, "add-audio-mappings", NULL, },
+ { AVB_AECP_AEM_CMD_REMOVE_AUDIO_MAPPINGS, "remove-audio-mappings", NULL, },
+ { AVB_AECP_AEM_CMD_GET_VIDEO_MAP, "get-video-map", NULL, },
+ { AVB_AECP_AEM_CMD_ADD_VIDEO_MAPPINGS, "add-video-mappings", NULL, },
+ { AVB_AECP_AEM_CMD_REMOVE_VIDEO_MAPPINGS, "remove-video-mappings", NULL, },
+ { AVB_AECP_AEM_CMD_GET_SENSOR_MAP, "get-sensor-map", NULL, }
+};
+
+static inline const struct cmd_info *find_cmd_info(uint16_t type, const char *name)
+{
+ SPA_FOR_EACH_ELEMENT_VAR(cmd_info, i) {
+ if ((name == NULL && type == i->type) ||
+ (name != NULL && spa_streq(name, i->name)))
+ return i;
+ }
+ return NULL;
+}
+
+int avb_aecp_aem_handle_command(struct aecp *aecp, const void *m, int len)
+{
+ const struct avb_ethernet_header *h = m;
+ const struct avb_packet_aecp_aem *p = SPA_PTROFF(h, sizeof(*h), void);
+ uint16_t cmd_type;
+ const struct cmd_info *info;
+
+ cmd_type = AVB_PACKET_AEM_GET_COMMAND_TYPE(p);
+
+ info = find_cmd_info(cmd_type, NULL);
+ if (info == NULL)
+ return reply_not_implemented(aecp, m, len);
+
+ pw_log_info("aem command %s", info->name);
+
+ if (info->handle == NULL)
+ return reply_not_implemented(aecp, m, len);
+
+ return info->handle(aecp, m, len);
+}
+
+int avb_aecp_aem_handle_response(struct aecp *aecp, const void *m, int len)
+{
+ return 0;
+}
diff --git a/src/modules/module-avb/aecp-aem.h b/src/modules/module-avb/aecp-aem.h
new file mode 100644
index 0000000..dcf26b5
--- /dev/null
+++ b/src/modules/module-avb/aecp-aem.h
@@ -0,0 +1,345 @@
+/* AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef AVB_AEM_H
+#define AVB_AEM_H
+
+#include "aecp.h"
+
+#define AVB_AECP_AEM_STATUS_SUCCESS 0
+#define AVB_AECP_AEM_STATUS_NOT_IMPLEMENTED 1
+#define AVB_AECP_AEM_STATUS_NO_SUCH_DESCRIPTOR 2
+#define AVB_AECP_AEM_STATUS_ENTITY_LOCKED 3
+#define AVB_AECP_AEM_STATUS_ENTITY_ACQUIRED 4
+#define AVB_AECP_AEM_STATUS_NOT_AUTHENTICATED 5
+#define AVB_AECP_AEM_STATUS_AUTHENTICATION_DISABLED 6
+#define AVB_AECP_AEM_STATUS_BAD_ARGUMENTS 7
+#define AVB_AECP_AEM_STATUS_NO_RESOURCES 8
+#define AVB_AECP_AEM_STATUS_IN_PROGRESS 9
+#define AVB_AECP_AEM_STATUS_ENTITY_MISBEHAVING 10
+#define AVB_AECP_AEM_STATUS_NOT_SUPPORTED 11
+#define AVB_AECP_AEM_STATUS_STREAM_IS_RUNNING 12
+
+#define AVB_AECP_AEM_CMD_ACQUIRE_ENTITY 0x0000
+#define AVB_AECP_AEM_CMD_LOCK_ENTITY 0x0001
+#define AVB_AECP_AEM_CMD_ENTITY_AVAILABLE 0x0002
+#define AVB_AECP_AEM_CMD_CONTROLLER_AVAILABLE 0x0003
+#define AVB_AECP_AEM_CMD_READ_DESCRIPTOR 0x0004
+#define AVB_AECP_AEM_CMD_WRITE_DESCRIPTOR 0x0005
+#define AVB_AECP_AEM_CMD_SET_CONFIGURATION 0x0006
+#define AVB_AECP_AEM_CMD_GET_CONFIGURATION 0x0007
+#define AVB_AECP_AEM_CMD_SET_STREAM_FORMAT 0x0008
+#define AVB_AECP_AEM_CMD_GET_STREAM_FORMAT 0x0009
+#define AVB_AECP_AEM_CMD_SET_VIDEO_FORMAT 0x000a
+#define AVB_AECP_AEM_CMD_GET_VIDEO_FORMAT 0x000b
+#define AVB_AECP_AEM_CMD_SET_SENSOR_FORMAT 0x000c
+#define AVB_AECP_AEM_CMD_GET_SENSOR_FORMAT 0x000d
+#define AVB_AECP_AEM_CMD_SET_STREAM_INFO 0x000e
+#define AVB_AECP_AEM_CMD_GET_STREAM_INFO 0x000f
+#define AVB_AECP_AEM_CMD_SET_NAME 0x0010
+#define AVB_AECP_AEM_CMD_GET_NAME 0x0011
+#define AVB_AECP_AEM_CMD_SET_ASSOCIATION_ID 0x0012
+#define AVB_AECP_AEM_CMD_GET_ASSOCIATION_ID 0x0013
+#define AVB_AECP_AEM_CMD_SET_SAMPLING_RATE 0x0014
+#define AVB_AECP_AEM_CMD_GET_SAMPLING_RATE 0x0015
+#define AVB_AECP_AEM_CMD_SET_CLOCK_SOURCE 0x0016
+#define AVB_AECP_AEM_CMD_GET_CLOCK_SOURCE 0x0017
+#define AVB_AECP_AEM_CMD_SET_CONTROL 0x0018
+#define AVB_AECP_AEM_CMD_GET_CONTROL 0x0019
+#define AVB_AECP_AEM_CMD_INCREMENT_CONTROL 0x001a
+#define AVB_AECP_AEM_CMD_DECREMENT_CONTROL 0x001b
+#define AVB_AECP_AEM_CMD_SET_SIGNAL_SELECTOR 0x001c
+#define AVB_AECP_AEM_CMD_GET_SIGNAL_SELECTOR 0x001d
+#define AVB_AECP_AEM_CMD_SET_MIXER 0x001e
+#define AVB_AECP_AEM_CMD_GET_MIXER 0x001f
+#define AVB_AECP_AEM_CMD_SET_MATRIX 0x0020
+#define AVB_AECP_AEM_CMD_GET_MATRIX 0x0021
+#define AVB_AECP_AEM_CMD_START_STREAMING 0x0022
+#define AVB_AECP_AEM_CMD_STOP_STREAMING 0x0023
+#define AVB_AECP_AEM_CMD_REGISTER_UNSOLICITED_NOTIFICATION 0x0024
+#define AVB_AECP_AEM_CMD_DEREGISTER_UNSOLICITED_NOTIFICATION 0x0025
+#define AVB_AECP_AEM_CMD_IDENTIFY_NOTIFICATION 0x0026
+#define AVB_AECP_AEM_CMD_GET_AVB_INFO 0x0027
+#define AVB_AECP_AEM_CMD_GET_AS_PATH 0x0028
+#define AVB_AECP_AEM_CMD_GET_COUNTERS 0x0029
+#define AVB_AECP_AEM_CMD_REBOOT 0x002a
+#define AVB_AECP_AEM_CMD_GET_AUDIO_MAP 0x002b
+#define AVB_AECP_AEM_CMD_ADD_AUDIO_MAPPINGS 0x002c
+#define AVB_AECP_AEM_CMD_REMOVE_AUDIO_MAPPINGS 0x002d
+#define AVB_AECP_AEM_CMD_GET_VIDEO_MAP 0x002e
+#define AVB_AECP_AEM_CMD_ADD_VIDEO_MAPPINGS 0x002f
+#define AVB_AECP_AEM_CMD_REMOVE_VIDEO_MAPPINGS 0x0030
+#define AVB_AECP_AEM_CMD_GET_SENSOR_MAP 0x0031
+#define AVB_AECP_AEM_CMD_ADD_SENSOR_MAPPINGS 0x0032
+#define AVB_AECP_AEM_CMD_REMOVE_SENSOR_MAPPINGS 0x0033
+#define AVB_AECP_AEM_CMD_START_OPERATION 0x0034
+#define AVB_AECP_AEM_CMD_ABORT_OPERATION 0x0035
+#define AVB_AECP_AEM_CMD_OPERATION_STATUS 0x0036
+#define AVB_AECP_AEM_CMD_AUTH_ADD_KEY 0x0037
+#define AVB_AECP_AEM_CMD_AUTH_DELETE_KEY 0x0038
+#define AVB_AECP_AEM_CMD_AUTH_GET_KEY_LIST 0x0039
+#define AVB_AECP_AEM_CMD_AUTH_GET_KEY 0x003a
+#define AVB_AECP_AEM_CMD_AUTH_ADD_KEY_TO_CHAIN 0x003b
+#define AVB_AECP_AEM_CMD_AUTH_DELETE_KEY_FROM_CHAIN 0x003c
+#define AVB_AECP_AEM_CMD_AUTH_GET_KEYCHAIN_LIST 0x003d
+#define AVB_AECP_AEM_CMD_AUTH_GET_IDENTITY 0x003e
+#define AVB_AECP_AEM_CMD_AUTH_ADD_TOKEN 0x003f
+#define AVB_AECP_AEM_CMD_AUTH_DELETE_TOKEN 0x0040
+#define AVB_AECP_AEM_CMD_AUTHENTICATE 0x0041
+#define AVB_AECP_AEM_CMD_DEAUTHENTICATE 0x0042
+#define AVB_AECP_AEM_CMD_ENABLE_TRANSPORT_SECURITY 0x0043
+#define AVB_AECP_AEM_CMD_DISABLE_TRANSPORT_SECURITY 0x0044
+#define AVB_AECP_AEM_CMD_ENABLE_STREAM_ENCRYPTION 0x0045
+#define AVB_AECP_AEM_CMD_DISABLE_STREAM_ENCRYPTION 0x0046
+#define AVB_AECP_AEM_CMD_SET_MEMORY_OBJECT_LENGTH 0x0047
+#define AVB_AECP_AEM_CMD_GET_MEMORY_OBJECT_LENGTH 0x0048
+#define AVB_AECP_AEM_CMD_SET_STREAM_BACKUP 0x0049
+#define AVB_AECP_AEM_CMD_GET_STREAM_BACKUP 0x004a
+#define AVB_AECP_AEM_CMD_EXPANSION 0x7fff
+
+#define AVB_AEM_ACQUIRE_ENTITY_PERSISTENT_FLAG (1<<0)
+
+struct avb_packet_aecp_aem_acquire {
+ uint32_t flags;
+ uint64_t owner_guid;
+ uint16_t descriptor_type;
+ uint16_t descriptor_id;
+} __attribute__ ((__packed__));
+
+struct avb_packet_aecp_aem_lock {
+ uint32_t flags;
+ uint64_t locked_guid;
+ uint16_t descriptor_type;
+ uint16_t descriptor_id;
+} __attribute__ ((__packed__));
+
+struct avb_packet_aecp_aem_read_descriptor {
+ uint16_t configuration;
+ uint8_t reserved[2];
+ uint16_t descriptor_type;
+ uint16_t descriptor_id;
+} __attribute__ ((__packed__));
+
+struct avb_packet_aecp_aem_setget_configuration {
+ uint16_t reserved;
+ uint16_t configuration_index;
+} __attribute__ ((__packed__));
+
+struct avb_packet_aecp_aem_setget_stream_format {
+ uint16_t descriptor_type;
+ uint16_t descriptor_id;
+ uint64_t stream_format;
+} __attribute__ ((__packed__));
+
+struct avb_packet_aecp_aem_setget_video_format {
+ uint16_t descriptor_type;
+ uint16_t descriptor_id;
+ uint32_t format_specific;
+ uint16_t aspect_ratio;
+ uint16_t color_space;
+ uint32_t frame_size;
+} __attribute__ ((__packed__));
+
+struct avb_packet_aecp_aem_setget_sensor_format {
+ uint16_t descriptor_type;
+ uint16_t descriptor_id;
+ uint64_t sensor_format;
+} __attribute__ ((__packed__));
+
+
+#define AVB_AEM_STREAM_INFO_FLAG_CLASS_B (1u<<0)
+#define AVB_AEM_STREAM_INFO_FLAG_FAST_CONNECT (1u<<1)
+#define AVB_AEM_STREAM_INFO_FLAG_SAVED_STATE (1u<<2)
+#define AVB_AEM_STREAM_INFO_FLAG_STREAMING_WAIT (1u<<3)
+#define AVB_AEM_STREAM_INFO_FLAG_ENCRYPTED_PDU (1u<<4)
+#define AVB_AEM_STREAM_INFO_FLAG_STREAM_VLAN_ID_VALID (1u<<25)
+#define AVB_AEM_STREAM_INFO_FLAG_CONNECTED (1u<<26)
+#define AVB_AEM_STREAM_INFO_FLAG_MSRP_FAILURE_VALID (1u<<27)
+#define AVB_AEM_STREAM_INFO_FLAG_STREAM_DEST_MAC_VALID (1u<<28)
+#define AVB_AEM_STREAM_INFO_FLAG_MSRP_ACC_LAT_VALID (1u<<29)
+#define AVB_AEM_STREAM_INFO_FLAG_STREAM_ID_VALID (1u<<30)
+#define AVB_AEM_STREAM_INFO_FLAG_STREAM_FORMAT_VALID (1u<<31)
+
+struct avb_packet_aecp_aem_setget_stream_info {
+ uint16_t descriptor_type;
+ uint16_t descriptor_index;
+ uint32_t aem_stream_info_flags;
+ uint64_t stream_format;
+ uint64_t stream_id;
+ uint32_t msrp_accumulated_latency;
+ uint8_t stream_dest_mac[6];
+ uint8_t msrp_failure_code;
+ uint8_t reserved;
+ uint64_t msrp_failure_bridge_id;
+ uint16_t stream_vlan_id;
+ uint16_t reserved2;
+} __attribute__ ((__packed__));
+
+struct avb_packet_aecp_aem_setget_name {
+ uint16_t descriptor_type;
+ uint16_t descriptor_index;
+ uint16_t name_index;
+ uint16_t configuration_index;
+ char name[64];
+} __attribute__ ((__packed__));
+
+struct avb_packet_aecp_aem_setget_association_id {
+ uint16_t descriptor_type;
+ uint16_t descriptor_index;
+ uint64_t association_id;
+} __attribute__ ((__packed__));
+
+struct avb_packet_aecp_aem_setget_sampling_rate {
+ uint16_t descriptor_type;
+ uint16_t descriptor_id;
+ uint32_t sampling_rate;
+} __attribute__ ((__packed__));
+
+struct avb_packet_aecp_aem_setget_clock_source {
+ uint16_t descriptor_type;
+ uint16_t descriptor_id;
+ uint16_t clock_source_index;
+ uint16_t reserved;
+} __attribute__ ((__packed__));
+
+struct avb_packet_aecp_aem_setget_control {
+ uint16_t descriptor_type;
+ uint16_t descriptor_id;
+} __attribute__ ((__packed__));
+
+struct avb_packet_aecp_aem_incdec_control {
+ uint16_t descriptor_type;
+ uint16_t descriptor_id;
+ uint16_t index_count;
+ uint16_t reserved;
+} __attribute__ ((__packed__));
+
+struct avb_packet_aecp_aem_setget_signal_selector {
+ uint16_t descriptor_type;
+ uint16_t descriptor_id;
+ uint16_t signal_type;
+ uint16_t signal_index;
+ uint16_t signal_output;
+ uint16_t reserved;
+} __attribute__ ((__packed__));
+
+struct avb_packet_aecp_aem_setget_mixer {
+ uint16_t descriptor_type;
+ uint16_t descriptor_id;
+} __attribute__ ((__packed__));
+
+struct avb_packet_aecp_aem_setget_matrix {
+ uint16_t descriptor_type;
+ uint16_t descriptor_index;
+ uint16_t matrix_column;
+ uint16_t matrix_row;
+ uint16_t region_width;
+ uint16_t region_height;
+ uint16_t rep_direction_value_count;
+ uint16_t item_offset;
+} __attribute__ ((__packed__));
+
+struct avb_packet_aecp_aem_startstop_streaming {
+ uint16_t descriptor_type;
+ uint16_t descriptor_id;
+} __attribute__ ((__packed__));
+
+struct avb_packet_aecp_aem_identify_notification {
+ uint16_t descriptor_type;
+ uint16_t descriptor_id;
+} __attribute__ ((__packed__));
+
+struct avb_packet_aecp_aem_msrp_mapping {
+ uint8_t traffic_class;
+ uint8_t priority;
+ uint16_t vlan_id;
+} __attribute__ ((__packed__));
+
+#define AVB_AEM_AVB_INFO_FLAG_GPTP_GRANDMASTER_SUPPORTED (1u<<0)
+#define AVB_AEM_AVB_INFO_FLAG_GPTP_ENABLED (1u<<1)
+#define AVB_AEM_AVB_INFO_FLAG_SRP_ENABLED (1u<<2)
+
+struct avb_packet_aecp_aem_get_avb_info {
+ uint16_t descriptor_type;
+ uint16_t descriptor_id;
+ uint64_t gptp_grandmaster_id;
+ uint32_t propagation_delay;
+ uint8_t gptp_domain_number;
+ uint8_t flags;
+ uint16_t msrp_mappings_count;
+ uint8_t msrp_mappings[0];
+} __attribute__ ((__packed__));
+
+struct avb_packet_aecp_aem_get_as_path {
+ uint16_t descriptor_index;
+ uint16_t reserved;
+} __attribute__ ((__packed__));
+
+struct avb_packet_aecp_aem_get_counters {
+ uint16_t descriptor_type;
+ uint16_t descriptor_id;
+ uint32_t counters_valid;
+ uint8_t counters_block[0];
+} __attribute__ ((__packed__));
+
+struct avb_packet_aecp_aem_reboot {
+ uint16_t descriptor_type;
+ uint16_t descriptor_id;
+} __attribute__ ((__packed__));
+
+struct avb_packet_aecp_aem_start_operation {
+ uint16_t descriptor_type;
+ uint16_t descriptor_id;
+ uint16_t operation_id;
+ uint16_t operation_type;
+} __attribute__ ((__packed__));
+
+struct avb_packet_aecp_aem_operation_status {
+ uint16_t descriptor_type;
+ uint16_t descriptor_id;
+ uint16_t operation_id;
+ uint16_t percent_complete;
+} __attribute__ ((__packed__));
+
+struct avb_packet_aecp_aem {
+ struct avb_packet_aecp_header aecp;
+#if __BYTE_ORDER == __BIG_ENDIAN
+ unsigned u:1;
+ unsigned cmd1:7;
+#elif __BYTE_ORDER == __LITTLE_ENDIAN
+ unsigned cmd1:7;
+ unsigned u:1;
+#endif
+ uint8_t cmd2;
+ uint8_t payload[0];
+} __attribute__ ((__packed__));
+
+#define AVB_PACKET_AEM_SET_COMMAND_TYPE(p,v) ((p)->cmd1 = ((v) >> 8),(p)->cmd2 = (v))
+
+#define AVB_PACKET_AEM_GET_COMMAND_TYPE(p) ((p)->cmd1 << 8 | (p)->cmd2)
+
+int avb_aecp_aem_handle_command(struct aecp *aecp, const void *m, int len);
+int avb_aecp_aem_handle_response(struct aecp *aecp, const void *m, int len);
+
+#endif /* AVB_AEM_H */
diff --git a/src/modules/module-avb/aecp.c b/src/modules/module-avb/aecp.c
new file mode 100644
index 0000000..d581f81
--- /dev/null
+++ b/src/modules/module-avb/aecp.c
@@ -0,0 +1,168 @@
+/* AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <spa/utils/json.h>
+#include <spa/debug/mem.h>
+
+#include <pipewire/pipewire.h>
+
+#include "aecp.h"
+#include "aecp-aem.h"
+#include "internal.h"
+
+static const uint8_t mac[6] = AVB_BROADCAST_MAC;
+
+struct msg_info {
+ uint16_t type;
+ const char *name;
+ int (*handle) (struct aecp *aecp, const void *p, int len);
+};
+
+static int reply_not_implemented(struct aecp *aecp, const void *p, int len)
+{
+ struct server *server = aecp->server;
+ uint8_t buf[len];
+ struct avb_ethernet_header *h = (void*)buf;
+ struct avb_packet_aecp_header *reply = SPA_PTROFF(h, sizeof(*h), void);
+
+ memcpy(h, p, len);
+ AVB_PACKET_AECP_SET_STATUS(reply, AVB_AECP_STATUS_NOT_IMPLEMENTED);
+
+ return avb_server_send_packet(server, h->src, AVB_TSN_ETH, buf, len);
+}
+
+static const struct msg_info msg_info[] = {
+ { AVB_AECP_MESSAGE_TYPE_AEM_COMMAND, "aem-command", avb_aecp_aem_handle_command, },
+ { AVB_AECP_MESSAGE_TYPE_AEM_RESPONSE, "aem-response", avb_aecp_aem_handle_response, },
+ { AVB_AECP_MESSAGE_TYPE_ADDRESS_ACCESS_COMMAND, "address-access-command", NULL, },
+ { AVB_AECP_MESSAGE_TYPE_ADDRESS_ACCESS_RESPONSE, "address-access-response", NULL, },
+ { AVB_AECP_MESSAGE_TYPE_AVC_COMMAND, "avc-command", NULL, },
+ { AVB_AECP_MESSAGE_TYPE_AVC_RESPONSE, "avc-response", NULL, },
+ { AVB_AECP_MESSAGE_TYPE_VENDOR_UNIQUE_COMMAND, "vendor-unique-command", NULL, },
+ { AVB_AECP_MESSAGE_TYPE_VENDOR_UNIQUE_RESPONSE, "vendor-unique-response", NULL, },
+ { AVB_AECP_MESSAGE_TYPE_EXTENDED_COMMAND, "extended-command", NULL, },
+ { AVB_AECP_MESSAGE_TYPE_EXTENDED_RESPONSE, "extended-response", NULL, },
+};
+
+static inline const struct msg_info *find_msg_info(uint16_t type, const char *name)
+{
+ SPA_FOR_EACH_ELEMENT_VAR(msg_info, i) {
+ if ((name == NULL && type == i->type) ||
+ (name != NULL && spa_streq(name, i->name)))
+ return i;
+ }
+ return NULL;
+}
+
+static int aecp_message(void *data, uint64_t now, const void *message, int len)
+{
+ struct aecp *aecp = data;
+ struct server *server = aecp->server;
+ const struct avb_ethernet_header *h = message;
+ const struct avb_packet_aecp_header *p = SPA_PTROFF(h, sizeof(*h), void);
+ const struct msg_info *info;
+ int message_type;
+
+ if (ntohs(h->type) != AVB_TSN_ETH)
+ return 0;
+ if (memcmp(h->dest, mac, 6) != 0 &&
+ memcmp(h->dest, server->mac_addr, 6) != 0)
+ return 0;
+ if (AVB_PACKET_GET_SUBTYPE(&p->hdr) != AVB_SUBTYPE_AECP)
+ return 0;
+
+ message_type = AVB_PACKET_AECP_GET_MESSAGE_TYPE(p);
+
+ info = find_msg_info(message_type, NULL);
+ if (info == NULL)
+ return reply_not_implemented(aecp, message, len);
+
+ pw_log_debug("got AECP message %s", info->name);
+
+ if (info->handle == NULL)
+ return reply_not_implemented(aecp, message, len);
+
+ return info->handle(aecp, message, len);
+}
+
+static void aecp_destroy(void *data)
+{
+ struct aecp *aecp = data;
+ spa_hook_remove(&aecp->server_listener);
+ free(aecp);
+}
+
+static int do_help(struct aecp *aecp, const char *args, FILE *out)
+{
+ fprintf(out, "{ \"type\": \"help\","
+ "\"text\": \""
+ "/adp/help: this help \\n"
+ "\" }");
+ return 0;
+}
+
+static int aecp_command(void *data, uint64_t now, const char *command, const char *args, FILE *out)
+{
+ struct aecp *aecp = data;
+ int res;
+
+ if (!spa_strstartswith(command, "/aecp/"))
+ return 0;
+
+ command += strlen("/aecp/");
+
+ if (spa_streq(command, "help"))
+ res = do_help(aecp, args, out);
+ else
+ res = -ENOTSUP;
+
+ return res;
+}
+
+static const struct server_events server_events = {
+ AVB_VERSION_SERVER_EVENTS,
+ .destroy = aecp_destroy,
+ .message = aecp_message,
+ .command = aecp_command
+};
+
+struct avb_aecp *avb_aecp_register(struct server *server)
+{
+ struct aecp *aecp;
+
+ aecp = calloc(1, sizeof(*aecp));
+ if (aecp == NULL)
+ return NULL;
+
+ aecp->server = server;
+
+ avdecc_server_add_listener(server, &aecp->server_listener, &server_events, aecp);
+
+ return (struct avb_aecp*)aecp;
+}
+
+void avb_aecp_unregister(struct avb_aecp *aecp)
+{
+ aecp_destroy(aecp);
+}
diff --git a/src/modules/module-avb/aecp.h b/src/modules/module-avb/aecp.h
new file mode 100644
index 0000000..a3515f0
--- /dev/null
+++ b/src/modules/module-avb/aecp.h
@@ -0,0 +1,60 @@
+/* AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef AVB_AECP_H
+#define AVB_AECP_H
+
+#include "packets.h"
+#include "internal.h"
+
+#define AVB_AECP_MESSAGE_TYPE_AEM_COMMAND 0
+#define AVB_AECP_MESSAGE_TYPE_AEM_RESPONSE 1
+#define AVB_AECP_MESSAGE_TYPE_ADDRESS_ACCESS_COMMAND 2
+#define AVB_AECP_MESSAGE_TYPE_ADDRESS_ACCESS_RESPONSE 3
+#define AVB_AECP_MESSAGE_TYPE_AVC_COMMAND 4
+#define AVB_AECP_MESSAGE_TYPE_AVC_RESPONSE 5
+#define AVB_AECP_MESSAGE_TYPE_VENDOR_UNIQUE_COMMAND 6
+#define AVB_AECP_MESSAGE_TYPE_VENDOR_UNIQUE_RESPONSE 7
+#define AVB_AECP_MESSAGE_TYPE_EXTENDED_COMMAND 14
+#define AVB_AECP_MESSAGE_TYPE_EXTENDED_RESPONSE 15
+
+#define AVB_AECP_STATUS_SUCCESS 0
+#define AVB_AECP_STATUS_NOT_IMPLEMENTED 1
+
+struct avb_packet_aecp_header {
+ struct avb_packet_header hdr;
+ uint64_t target_guid;
+ uint64_t controller_guid;
+ uint16_t sequence_id;
+} __attribute__ ((__packed__));
+
+#define AVB_PACKET_AECP_SET_MESSAGE_TYPE(p,v) AVB_PACKET_SET_SUB1(&(p)->hdr, v)
+#define AVB_PACKET_AECP_SET_STATUS(p,v) AVB_PACKET_SET_SUB2(&(p)->hdr, v)
+
+#define AVB_PACKET_AECP_GET_MESSAGE_TYPE(p) AVB_PACKET_GET_SUB1(&(p)->hdr)
+#define AVB_PACKET_AECP_GET_STATUS(p) AVB_PACKET_GET_SUB2(&(p)->hdr)
+
+struct avb_aecp *avb_aecp_register(struct server *server);
+
+#endif /* AVB_AECP_H */
diff --git a/src/modules/module-avb/avb.c b/src/modules/module-avb/avb.c
new file mode 100644
index 0000000..2afdc21
--- /dev/null
+++ b/src/modules/module-avb/avb.c
@@ -0,0 +1,106 @@
+/* PipeWire
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "internal.h"
+
+#include <spa/support/cpu.h>
+
+struct pw_avb *pw_avb_new(struct pw_context *context,
+ struct pw_properties *props, size_t user_data_size)
+{
+ struct impl *impl;
+ const struct spa_support *support;
+ uint32_t n_support;
+ struct spa_cpu *cpu;
+ const char *str;
+ int res = 0;
+
+ impl = calloc(1, sizeof(*impl) + user_data_size);
+ if (impl == NULL)
+ goto error_exit;
+
+ if (props == NULL)
+ props = pw_properties_new(NULL, NULL);
+ if (props == NULL)
+ goto error_free;
+
+ support = pw_context_get_support(context, &n_support);
+ cpu = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_CPU);
+
+ pw_context_conf_update_props(context, "avb.properties", props);
+
+ if ((str = pw_properties_get(props, "vm.overrides")) != NULL) {
+ if (cpu != NULL && spa_cpu_get_vm_type(cpu) != SPA_CPU_VM_NONE)
+ pw_properties_update_string(props, str, strlen(str));
+ pw_properties_set(props, "vm.overrides", NULL);
+ }
+
+ impl->context = context;
+ impl->loop = pw_context_get_main_loop(context);
+ impl->props = props;
+ impl->core = pw_context_get_object(context, PW_TYPE_INTERFACE_Core);
+ if (impl->core == NULL) {
+ str = pw_properties_get(props, PW_KEY_REMOTE_NAME);
+ impl->core = pw_context_connect(context,
+ pw_properties_new(
+ PW_KEY_REMOTE_NAME, str,
+ NULL),
+ 0);
+ impl->do_disconnect = true;
+ }
+ if (impl->core == NULL) {
+ res = -errno;
+ pw_log_error("can't connect: %m");
+ goto error_free;
+ }
+
+ spa_list_init(&impl->servers);
+
+ avdecc_server_new(impl, &props->dict);
+
+ return (struct pw_avb*)impl;
+
+error_free:
+ free(impl);
+error_exit:
+ pw_properties_free(props);
+ if (res < 0)
+ errno = -res;
+ return NULL;
+}
+
+static void impl_free(struct impl *impl)
+{
+ struct server *s;
+
+ spa_list_consume(s, &impl->servers, link)
+ avdecc_server_free(s);
+ free(impl);
+}
+
+void pw_avb_destroy(struct pw_avb *avb)
+{
+ struct impl *impl = (struct impl*)avb;
+ impl_free(impl);
+}
diff --git a/src/modules/module-avb/avb.h b/src/modules/module-avb/avb.h
new file mode 100644
index 0000000..cad7dd2
--- /dev/null
+++ b/src/modules/module-avb/avb.h
@@ -0,0 +1,44 @@
+/* PipeWire
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_AVB_H
+#define PIPEWIRE_AVB_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct pw_context;
+struct pw_properties;
+struct pw_avb;
+
+struct pw_avb *pw_avb_new(struct pw_context *context,
+ struct pw_properties *props, size_t user_data_size);
+void pw_avb_destroy(struct pw_avb *avb);
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* PIPEWIRE_AVB_H */
diff --git a/src/modules/module-avb/avdecc.c b/src/modules/module-avb/avdecc.c
new file mode 100644
index 0000000..308ba48
--- /dev/null
+++ b/src/modules/module-avb/avdecc.c
@@ -0,0 +1,335 @@
+/* PipeWire
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <linux/if_ether.h>
+#include <linux/if_packet.h>
+#include <linux/filter.h>
+#include <linux/net_tstamp.h>
+#include <limits.h>
+#include <net/if.h>
+#include <arpa/inet.h>
+#include <sys/ioctl.h>
+#include <unistd.h>
+
+#include <spa/support/cpu.h>
+#include <spa/debug/mem.h>
+
+#include <pipewire/pipewire.h>
+
+#include "avb.h"
+#include "packets.h"
+#include "internal.h"
+#include "stream.h"
+#include "acmp.h"
+#include "adp.h"
+#include "aecp.h"
+#include "maap.h"
+#include "mmrp.h"
+#include "msrp.h"
+#include "mvrp.h"
+#include "descriptors.h"
+#include "utils.h"
+
+#define DEFAULT_INTERVAL 1
+
+#define server_emit(s,m,v,...) spa_hook_list_call(&s->listener_list, struct server_events, m, v, ##__VA_ARGS__)
+#define server_emit_destroy(s) server_emit(s, destroy, 0)
+#define server_emit_message(s,n,m,l) server_emit(s, message, 0, n, m, l)
+#define server_emit_periodic(s,n) server_emit(s, periodic, 0, n)
+#define server_emit_command(s,n,c,a,f) server_emit(s, command, 0, n, c, a, f)
+
+static void on_timer_event(void *data, uint64_t expirations)
+{
+ struct server *server = data;
+ struct timespec now;
+ clock_gettime(CLOCK_REALTIME, &now);
+ server_emit_periodic(server, SPA_TIMESPEC_TO_NSEC(&now));
+}
+
+static void on_socket_data(void *data, int fd, uint32_t mask)
+{
+ struct server *server = data;
+ struct timespec now;
+
+ if (mask & SPA_IO_IN) {
+ int len;
+ uint8_t buffer[2048];
+
+ len = recv(fd, buffer, sizeof(buffer), 0);
+
+ if (len < 0) {
+ pw_log_warn("got recv error: %m");
+ }
+ else if (len < (int)sizeof(struct avb_packet_header)) {
+ pw_log_warn("short packet received (%d < %d)", len,
+ (int)sizeof(struct avb_packet_header));
+ } else {
+ clock_gettime(CLOCK_REALTIME, &now);
+ server_emit_message(server, SPA_TIMESPEC_TO_NSEC(&now), buffer, len);
+ }
+ }
+}
+
+int avb_server_send_packet(struct server *server, const uint8_t dest[6],
+ uint16_t type, void *data, size_t size)
+{
+ struct avb_ethernet_header *hdr = (struct avb_ethernet_header*)data;
+ int res = 0;
+
+ memcpy(hdr->dest, dest, ETH_ALEN);
+ memcpy(hdr->src, server->mac_addr, ETH_ALEN);
+ hdr->type = htons(type);
+
+ if (send(server->source->fd, data, size, 0) < 0) {
+ res = -errno;
+ pw_log_warn("got send error: %m");
+ }
+ return res;
+}
+
+static int load_filter(int fd, uint16_t eth, const uint8_t dest[6], const uint8_t mac[6])
+{
+ struct sock_fprog filter;
+ struct sock_filter bpf_code[] = {
+ BPF_STMT(BPF_LD|BPF_H|BPF_ABS, 12),
+ BPF_JUMP(BPF_JMP|BPF_JEQ, eth, 0, 8),
+ BPF_STMT(BPF_LD|BPF_W|BPF_ABS, 2),
+ BPF_JUMP(BPF_JMP|BPF_JEQ, (dest[2] << 24) |
+ (dest[3] << 16) |
+ (dest[4] << 8) |
+ (dest[5]), 0, 2),
+ BPF_STMT(BPF_LD|BPF_H|BPF_ABS, 0),
+ BPF_JUMP(BPF_JMP|BPF_JEQ, (dest[0] << 8) |
+ (dest[1]), 3, 4),
+ BPF_JUMP(BPF_JMP|BPF_JEQ, (mac[2] << 24) |
+ (mac[3] << 16) |
+ (mac[4] << 8) |
+ (mac[5]), 0, 3),
+ BPF_STMT(BPF_LD|BPF_H|BPF_ABS, 0),
+ BPF_JUMP(BPF_JMP|BPF_JEQ, (mac[0] << 8) |
+ (mac[1]), 0, 1),
+ BPF_STMT(BPF_RET, 0x00040000),
+ BPF_STMT(BPF_RET, 0x00000000),
+ };
+ filter.len = sizeof(bpf_code) / 8;
+ filter.filter = bpf_code;
+
+ if (setsockopt(fd, SOL_SOCKET, SO_ATTACH_FILTER,
+ &filter, sizeof(filter)) < 0) {
+ pw_log_error("setsockopt(ATTACH_FILTER) failed: %m");
+ return -errno;
+ }
+ return 0;
+}
+
+int avb_server_make_socket(struct server *server, uint16_t type, const uint8_t mac[6])
+{
+ int fd, res;
+ struct ifreq req;
+ struct packet_mreq mreq;
+ struct sockaddr_ll sll;
+
+ fd = socket(AF_PACKET, SOCK_RAW|SOCK_NONBLOCK, htons(ETH_P_ALL));
+ if (fd < 0) {
+ pw_log_error("socket() failed: %m");
+ return -errno;
+ }
+
+ spa_zero(req);
+ snprintf(req.ifr_name, sizeof(req.ifr_name), "%s", server->ifname);
+ if (ioctl(fd, SIOCGIFINDEX, &req) < 0) {
+ res = -errno;
+ pw_log_error("SIOCGIFINDEX %s failed: %m", server->ifname);
+ goto error_close;
+ }
+ server->ifindex = req.ifr_ifindex;
+
+ spa_zero(req);
+ snprintf(req.ifr_name, sizeof(req.ifr_name), "%s", server->ifname);
+ if (ioctl(fd, SIOCGIFHWADDR, &req) < 0) {
+ res = -errno;
+ pw_log_error("SIOCGIFHWADDR %s failed: %m", server->ifname);
+ goto error_close;
+ }
+ memcpy(server->mac_addr, req.ifr_hwaddr.sa_data, sizeof(server->mac_addr));
+
+ server->entity_id = (uint64_t)server->mac_addr[0] << 56 |
+ (uint64_t)server->mac_addr[1] << 48 |
+ (uint64_t)server->mac_addr[2] << 40 |
+ (uint64_t)0xff << 32 |
+ (uint64_t)0xfe << 24 |
+ (uint64_t)server->mac_addr[3] << 16 |
+ (uint64_t)server->mac_addr[4] << 8 |
+ (uint64_t)server->mac_addr[5];
+
+ spa_zero(sll);
+ sll.sll_family = AF_PACKET;
+ sll.sll_protocol = htons(ETH_P_ALL);
+ sll.sll_ifindex = server->ifindex;
+ if (bind(fd, (struct sockaddr *) &sll, sizeof(sll)) < 0) {
+ res = -errno;
+ pw_log_error("bind() failed: %m");
+ goto error_close;
+ }
+
+ spa_zero(mreq);
+ mreq.mr_ifindex = server->ifindex;
+ mreq.mr_type = PACKET_MR_MULTICAST;
+ mreq.mr_alen = ETH_ALEN;
+ memcpy(mreq.mr_address, mac, ETH_ALEN);
+
+ if (setsockopt(fd, SOL_PACKET, PACKET_ADD_MEMBERSHIP,
+ &mreq, sizeof(mreq)) < 0) {
+ res = -errno;
+ pw_log_error("setsockopt(ADD_MEMBERSHIP) failed: %m");
+ goto error_close;
+ }
+
+ if ((res = load_filter(fd, type, mac, server->mac_addr)) < 0)
+ goto error_close;
+
+ return fd;
+
+error_close:
+ close(fd);
+ return res;
+}
+
+static int setup_socket(struct server *server)
+{
+ struct impl *impl = server->impl;
+ int fd, res;
+ static const uint8_t bmac[6] = AVB_BROADCAST_MAC;
+ struct timespec value, interval;
+
+ fd = avb_server_make_socket(server, AVB_TSN_ETH, bmac);
+ if (fd < 0)
+ return fd;
+
+ pw_log_info("0x%"PRIx64" %d", server->entity_id, server->ifindex);
+
+ server->source = pw_loop_add_io(impl->loop, fd, SPA_IO_IN, true, on_socket_data, server);
+ if (server->source == NULL) {
+ res = -errno;
+ pw_log_error("server %p: can't create server source: %m", impl);
+ goto error_no_source;
+ }
+ server->timer = pw_loop_add_timer(impl->loop, on_timer_event, server);
+ if (server->timer == NULL) {
+ res = -errno;
+ pw_log_error("server %p: can't create timer source: %m", impl);
+ goto error_no_timer;
+ }
+ value.tv_sec = 0;
+ value.tv_nsec = 1;
+ interval.tv_sec = DEFAULT_INTERVAL;
+ interval.tv_nsec = 0;
+ pw_loop_update_timer(impl->loop, server->timer, &value, &interval, false);
+
+ return 0;
+
+error_no_timer:
+ pw_loop_destroy_source(impl->loop, server->source);
+ server->source = NULL;
+error_no_source:
+ close(fd);
+ return res;
+}
+
+struct server *avdecc_server_new(struct impl *impl, struct spa_dict *props)
+{
+ struct server *server;
+ int res = 0;
+
+ server = calloc(1, sizeof(*server));
+ if (server == NULL)
+ return NULL;
+
+ server->impl = impl;
+ spa_list_append(&impl->servers, &server->link);
+ server->ifname = strdup(spa_dict_lookup(props, "ifname"));
+ spa_hook_list_init(&server->listener_list);
+ spa_list_init(&server->descriptors);
+ spa_list_init(&server->streams);
+
+ server->debug_messages = false;
+
+ if ((res = setup_socket(server)) < 0)
+ goto error_free;
+
+ init_descriptors(server);
+
+ server->mrp = avb_mrp_new(server);
+ if (server->mrp == NULL)
+ goto error_free;
+
+ avb_aecp_register(server);
+ server->maap = avb_maap_register(server);
+ server->mmrp = avb_mmrp_register(server);
+ server->msrp = avb_msrp_register(server);
+ server->mvrp = avb_mvrp_register(server);
+ avb_adp_register(server);
+ avb_acmp_register(server);
+
+ server->domain_attr = avb_msrp_attribute_new(server->msrp,
+ AVB_MSRP_ATTRIBUTE_TYPE_DOMAIN);
+ server->domain_attr->attr.domain.sr_class_id = AVB_MSRP_CLASS_ID_DEFAULT;
+ server->domain_attr->attr.domain.sr_class_priority = AVB_MSRP_PRIORITY_DEFAULT;
+ server->domain_attr->attr.domain.sr_class_vid = htons(AVB_DEFAULT_VLAN);
+
+ avb_mrp_attribute_begin(server->domain_attr->mrp, 0);
+ avb_mrp_attribute_join(server->domain_attr->mrp, 0, true);
+
+ server_create_stream(server, SPA_DIRECTION_INPUT, 0);
+ server_create_stream(server, SPA_DIRECTION_OUTPUT, 0);
+
+ avb_maap_reserve(server->maap, 1);
+
+ return server;
+
+error_free:
+ free(server);
+ if (res < 0)
+ errno = -res;
+ return NULL;
+}
+
+void avdecc_server_add_listener(struct server *server, struct spa_hook *listener,
+ const struct server_events *events, void *data)
+{
+ spa_hook_list_append(&server->listener_list, listener, events, data);
+}
+
+void avdecc_server_free(struct server *server)
+{
+ struct impl *impl = server->impl;
+
+ spa_list_remove(&server->link);
+ if (server->source)
+ pw_loop_destroy_source(impl->loop, server->source);
+ if (server->timer)
+ pw_loop_destroy_source(impl->loop, server->source);
+ spa_hook_list_clean(&server->listener_list);
+ free(server);
+}
diff --git a/src/modules/module-avb/descriptors.h b/src/modules/module-avb/descriptors.h
new file mode 100644
index 0000000..56397e3
--- /dev/null
+++ b/src/modules/module-avb/descriptors.h
@@ -0,0 +1,274 @@
+/* PipeWire
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "aecp-aem.h"
+#include "aecp-aem-descriptors.h"
+#include "internal.h"
+
+void init_descriptors(struct server *server)
+{
+ server_add_descriptor(server, AVB_AEM_DESC_STRINGS, 0,
+ sizeof(struct avb_aem_desc_strings),
+ &(struct avb_aem_desc_strings)
+ {
+ .string_0 = "PipeWire",
+ .string_1 = "Configuration 1",
+ .string_2 = "Wim Taymans",
+ });
+ server_add_descriptor(server, AVB_AEM_DESC_LOCALE, 0,
+ sizeof(struct avb_aem_desc_locale),
+ &(struct avb_aem_desc_locale)
+ {
+ .locale_identifier = "en-EN",
+ .number_of_strings = htons(1),
+ .base_strings = htons(0)
+ });
+ server_add_descriptor(server, AVB_AEM_DESC_ENTITY, 0,
+ sizeof(struct avb_aem_desc_entity),
+ &(struct avb_aem_desc_entity)
+ {
+ .entity_id = htobe64(server->entity_id),
+ .entity_model_id = htobe64(0),
+ .entity_capabilities = htonl(
+ AVB_ADP_ENTITY_CAPABILITY_AEM_SUPPORTED |
+ AVB_ADP_ENTITY_CAPABILITY_CLASS_A_SUPPORTED |
+ AVB_ADP_ENTITY_CAPABILITY_GPTP_SUPPORTED |
+ AVB_ADP_ENTITY_CAPABILITY_AEM_IDENTIFY_CONTROL_INDEX_VALID |
+ AVB_ADP_ENTITY_CAPABILITY_AEM_INTERFACE_INDEX_VALID),
+
+ .talker_stream_sources = htons(8),
+ .talker_capabilities = htons(
+ AVB_ADP_TALKER_CAPABILITY_IMPLEMENTED |
+ AVB_ADP_TALKER_CAPABILITY_AUDIO_SOURCE),
+ .listener_stream_sinks = htons(8),
+ .listener_capabilities = htons(
+ AVB_ADP_LISTENER_CAPABILITY_IMPLEMENTED |
+ AVB_ADP_LISTENER_CAPABILITY_AUDIO_SINK),
+ .controller_capabilities = htons(0),
+ .available_index = htonl(0),
+ .association_id = htobe64(0),
+ .entity_name = "PipeWire",
+ .vendor_name_string = htons(2),
+ .model_name_string = htons(0),
+ .firmware_version = "0.3.48",
+ .group_name = "",
+ .serial_number = "",
+ .configurations_count = htons(1),
+ .current_configuration = htons(0)
+ });
+ struct {
+ struct avb_aem_desc_configuration desc;
+ struct avb_aem_desc_descriptor_count descriptor_counts[8];
+ } __attribute__ ((__packed__)) config =
+ {
+ {
+ .object_name = "Configuration 1",
+ .localized_description = htons(1),
+ .descriptor_counts_count = htons(8),
+ .descriptor_counts_offset = htons(
+ 4 + sizeof(struct avb_aem_desc_configuration)),
+ },
+ .descriptor_counts = {
+ { htons(AVB_AEM_DESC_AUDIO_UNIT), htons(1) },
+ { htons(AVB_AEM_DESC_STREAM_INPUT), htons(1) },
+ { htons(AVB_AEM_DESC_STREAM_OUTPUT), htons(1) },
+ { htons(AVB_AEM_DESC_AVB_INTERFACE), htons(1) },
+ { htons(AVB_AEM_DESC_CLOCK_SOURCE), htons(1) },
+ { htons(AVB_AEM_DESC_CONTROL), htons(2) },
+ { htons(AVB_AEM_DESC_LOCALE), htons(1) },
+ { htons(AVB_AEM_DESC_CLOCK_DOMAIN), htons(1) }
+ }
+ };
+ server_add_descriptor(server, AVB_AEM_DESC_CONFIGURATION, 0,
+ sizeof(config), &config);
+
+ struct {
+ struct avb_aem_desc_audio_unit desc;
+ struct avb_aem_desc_sampling_rate sampling_rates[6];
+ } __attribute__ ((__packed__)) audio_unit =
+ {
+ {
+ .object_name = "PipeWire",
+ .localized_description = htons(0),
+ .clock_domain_index = htons(0),
+ .number_of_stream_input_ports = htons(1),
+ .base_stream_input_port = htons(0),
+ .number_of_stream_output_ports = htons(1),
+ .base_stream_output_port = htons(0),
+ .number_of_external_input_ports = htons(8),
+ .base_external_input_port = htons(0),
+ .number_of_external_output_ports = htons(8),
+ .base_external_output_port = htons(0),
+ .number_of_internal_input_ports = htons(0),
+ .base_internal_input_port = htons(0),
+ .number_of_internal_output_ports = htons(0),
+ .base_internal_output_port = htons(0),
+ .number_of_controls = htons(0),
+ .base_control = htons(0),
+ .number_of_signal_selectors = htons(0),
+ .base_signal_selector = htons(0),
+ .number_of_mixers = htons(0),
+ .base_mixer = htons(0),
+ .number_of_matrices = htons(0),
+ .base_matrix = htons(0),
+ .number_of_splitters = htons(0),
+ .base_splitter = htons(0),
+ .number_of_combiners = htons(0),
+ .base_combiner = htons(0),
+ .number_of_demultiplexers = htons(0),
+ .base_demultiplexer = htons(0),
+ .number_of_multiplexers = htons(0),
+ .base_multiplexer = htons(0),
+ .number_of_transcoders = htons(0),
+ .base_transcoder = htons(0),
+ .number_of_control_blocks = htons(0),
+ .base_control_block = htons(0),
+ .current_sampling_rate = htonl(48000),
+ .sampling_rates_offset = htons(
+ 4 + sizeof(struct avb_aem_desc_audio_unit)),
+ .sampling_rates_count = htons(6),
+ },
+ .sampling_rates = {
+ { .pull_frequency = htonl(44100) },
+ { .pull_frequency = htonl(48000) },
+ { .pull_frequency = htonl(88200) },
+ { .pull_frequency = htonl(96000) },
+ { .pull_frequency = htonl(176400) },
+ { .pull_frequency = htonl(192000) },
+ }
+ };
+ server_add_descriptor(server, AVB_AEM_DESC_AUDIO_UNIT, 0,
+ sizeof(audio_unit), &audio_unit);
+
+ struct {
+ struct avb_aem_desc_stream desc;
+ uint64_t stream_formats[6];
+ } __attribute__ ((__packed__)) stream_input_0 =
+ {
+ {
+ .object_name = "Stream Input 1",
+ .localized_description = htons(0xffff),
+ .clock_domain_index = htons(0),
+ .stream_flags = htons(
+ AVB_AEM_DESC_STREAM_FLAG_SYNC_SOURCE |
+ AVB_AEM_DESC_STREAM_FLAG_CLASS_A),
+ .current_format = htobe64(0x00a0020840000800ULL),
+ .formats_offset = htons(
+ 4 + sizeof(struct avb_aem_desc_stream)),
+ .number_of_formats = htons(6),
+ .backup_talker_entity_id_0 = htobe64(0),
+ .backup_talker_unique_id_0 = htons(0),
+ .backup_talker_entity_id_1 = htobe64(0),
+ .backup_talker_unique_id_1 = htons(0),
+ .backup_talker_entity_id_2 = htobe64(0),
+ .backup_talker_unique_id_2 = htons(0),
+ .backedup_talker_entity_id = htobe64(0),
+ .backedup_talker_unique = htons(0),
+ .avb_interface_index = htons(0),
+ .buffer_length = htons(8)
+ },
+ .stream_formats = {
+ htobe64(0x00a0010860000800ULL),
+ htobe64(0x00a0020860000800ULL),
+ htobe64(0x00a0030860000800ULL),
+ htobe64(0x00a0040860000800ULL),
+ htobe64(0x00a0050860000800ULL),
+ htobe64(0x00a0060860000800ULL),
+ },
+ };
+ server_add_descriptor(server, AVB_AEM_DESC_STREAM_INPUT, 0,
+ sizeof(stream_input_0), &stream_input_0);
+
+ struct {
+ struct avb_aem_desc_stream desc;
+ uint64_t stream_formats[6];
+ } __attribute__ ((__packed__)) stream_output_0 =
+ {
+ {
+ .object_name = "Stream Output 1",
+ .localized_description = htons(0xffff),
+ .clock_domain_index = htons(0),
+ .stream_flags = htons(
+ AVB_AEM_DESC_STREAM_FLAG_CLASS_A),
+ .current_format = htobe64(0x00a0020840000800ULL),
+ .formats_offset = htons(
+ 4 + sizeof(struct avb_aem_desc_stream)),
+ .number_of_formats = htons(6),
+ .backup_talker_entity_id_0 = htobe64(0),
+ .backup_talker_unique_id_0 = htons(0),
+ .backup_talker_entity_id_1 = htobe64(0),
+ .backup_talker_unique_id_1 = htons(0),
+ .backup_talker_entity_id_2 = htobe64(0),
+ .backup_talker_unique_id_2 = htons(0),
+ .backedup_talker_entity_id = htobe64(0),
+ .backedup_talker_unique = htons(0),
+ .avb_interface_index = htons(0),
+ .buffer_length = htons(8)
+ },
+ .stream_formats = {
+ htobe64(0x00a0010860000800ULL),
+ htobe64(0x00a0020860000800ULL),
+ htobe64(0x00a0030860000800ULL),
+ htobe64(0x00a0040860000800ULL),
+ htobe64(0x00a0050860000800ULL),
+ htobe64(0x00a0060860000800ULL),
+ },
+ };
+ server_add_descriptor(server, AVB_AEM_DESC_STREAM_OUTPUT, 0,
+ sizeof(stream_output_0), &stream_output_0);
+
+ struct avb_aem_desc_avb_interface avb_interface = {
+ .localized_description = htons(0xffff),
+ .interface_flags = htons(
+ AVB_AEM_DESC_AVB_INTERFACE_FLAG_GPTP_GRANDMASTER_SUPPORTED),
+ .clock_identity = htobe64(0),
+ .priority1 = 0,
+ .clock_class = 0,
+ .offset_scaled_log_variance = htons(0),
+ .clock_accuracy = 0,
+ .priority2 = 0,
+ .domain_number = 0,
+ .log_sync_interval = 0,
+ .log_announce_interval = 0,
+ .log_pdelay_interval = 0,
+ .port_number = 0,
+ };
+ strncpy(avb_interface.object_name, server->ifname, 63);
+ memcpy(avb_interface.mac_address, server->mac_addr, 6);
+ server_add_descriptor(server, AVB_AEM_DESC_AVB_INTERFACE, 0,
+ sizeof(avb_interface), &avb_interface);
+
+ struct avb_aem_desc_clock_source clock_source = {
+ .object_name = "Stream Clock",
+ .localized_description = htons(0xffff),
+ .clock_source_flags = htons(0),
+ .clock_source_type = htons(
+ AVB_AEM_DESC_CLOCK_SOURCE_TYPE_INPUT_STREAM),
+ .clock_source_identifier = htobe64(0),
+ .clock_source_location_type = htons(AVB_AEM_DESC_STREAM_INPUT),
+ .clock_source_location_index = htons(0),
+ };
+ server_add_descriptor(server, AVB_AEM_DESC_CLOCK_SOURCE, 0,
+ sizeof(clock_source), &clock_source);
+}
diff --git a/src/modules/module-avb/iec61883.h b/src/modules/module-avb/iec61883.h
new file mode 100644
index 0000000..6ca8724
--- /dev/null
+++ b/src/modules/module-avb/iec61883.h
@@ -0,0 +1,110 @@
+/* AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef AVB_IEC61883_H
+#define AVB_IEC61883_H
+
+#include "packets.h"
+
+struct avb_packet_iec61883 {
+ uint8_t subtype;
+#if __BYTE_ORDER == __BIG_ENDIAN
+ unsigned sv:1;
+ unsigned version:3;
+ unsigned mr:1;
+ unsigned _r1:1;
+ unsigned gv:1;
+ unsigned tv:1;
+
+ uint8_t seq_num;
+
+ unsigned _r2:7;
+ unsigned tu:1;
+#elif __BYTE_ORDER == __LITTLE_ENDIAN
+ unsigned tv:1;
+ unsigned gv:1;
+ unsigned _r1:1;
+ unsigned mr:1;
+ unsigned version:3;
+ unsigned sv:1;
+
+ uint8_t seq_num;
+
+ unsigned tu:1;
+ unsigned _r2:7;
+#endif
+ uint64_t stream_id;
+ uint32_t timestamp;
+ uint32_t gateway_info;
+ uint16_t data_len;
+#if __BYTE_ORDER == __BIG_ENDIAN
+ uint8_t tag:2;
+ uint8_t channel:6;
+
+ uint8_t tcode:4;
+ uint8_t app:4;
+
+ uint8_t qi1:2; /* CIP Quadlet Indicator 1 */
+ uint8_t sid:6; /* CIP Source ID */
+
+ uint8_t dbs; /* CIP Data Block Size */
+
+ uint8_t fn:2; /* CIP Fraction Number */
+ uint8_t qpc:3; /* CIP Quadlet Padding Count */
+ uint8_t sph:1; /* CIP Source Packet Header */
+ uint8_t _r3:2;
+
+ uint8_t dbc; /* CIP Data Block Continuity */
+
+ uint8_t qi2:2; /* CIP Quadlet Indicator 2 */
+ uint8_t format_id:6; /* CIP Format ID */
+#elif __BYTE_ORDER == __LITTLE_ENDIAN
+ uint8_t channel:6;
+ uint8_t tag:2;
+
+ uint8_t app:4;
+ uint8_t tcode:4;
+
+ uint8_t sid:6; /* CIP Source ID */
+ uint8_t qi1:2; /* CIP Quadlet Indicator 1 */
+
+ uint8_t dbs; /* CIP Data Block Size */
+
+ uint8_t _r3:2;
+ uint8_t sph:1; /* CIP Source Packet Header */
+ uint8_t qpc:3; /* CIP Quadlet Padding Count */
+ uint8_t fn:2; /* CIP Fraction Number */
+
+ uint8_t dbc; /* CIP Data Block Continuity */
+
+ uint8_t format_id:6; /* CIP Format ID */
+ uint8_t qi2:2; /* CIP Quadlet Indicator 2 */
+#endif
+ uint8_t fdf; /* CIP Format Dependent Field */
+ uint16_t syt;
+
+ uint8_t payload[0];
+} __attribute__ ((__packed__));
+
+#endif /* AVB_IEC61883_H */
diff --git a/src/modules/module-avb/internal.h b/src/modules/module-avb/internal.h
new file mode 100644
index 0000000..9d29d92
--- /dev/null
+++ b/src/modules/module-avb/internal.h
@@ -0,0 +1,166 @@
+/* PipeWire
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef AVB_INTERNAL_H
+#define AVB_INTERNAL_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <pipewire/pipewire.h>
+
+struct server;
+struct avb_mrp;
+
+#define AVB_TSN_ETH 0x22f0
+#define AVB_BROADCAST_MAC { 0x91, 0xe0, 0xf0, 0x01, 0x00, 0x00 };
+
+struct impl {
+ struct pw_loop *loop;
+ struct pw_context *context;
+ struct spa_hook context_listener;
+ struct pw_core *core;
+ unsigned do_disconnect:1;
+
+ struct pw_properties *props;
+
+ struct spa_list servers;
+};
+
+struct server_events {
+#define AVB_VERSION_SERVER_EVENTS 0
+ uint32_t version;
+
+ /** the server is destroyed */
+ void (*destroy) (void *data);
+
+ int (*message) (void *data, uint64_t now, const void *message, int len);
+
+ void (*periodic) (void *data, uint64_t now);
+
+ int (*command) (void *data, uint64_t now, const char *command, const char *args, FILE *out);
+};
+
+struct descriptor {
+ struct spa_list link;
+ uint16_t type;
+ uint16_t index;
+ uint32_t size;
+ void *ptr;
+};
+
+struct server {
+ struct spa_list link;
+ struct impl *impl;
+
+ char *ifname;
+ uint8_t mac_addr[6];
+ uint64_t entity_id;
+ int ifindex;
+
+ struct spa_source *source;
+ struct spa_source *timer;
+
+ struct spa_hook_list listener_list;
+
+ struct spa_list descriptors;
+ struct spa_list streams;
+
+ unsigned debug_messages:1;
+
+ struct avb_mrp *mrp;
+ struct avb_mmrp *mmrp;
+ struct avb_mvrp *mvrp;
+ struct avb_msrp *msrp;
+ struct avb_maap *maap;
+
+ struct avb_msrp_attribute *domain_attr;
+};
+
+#include "stream.h"
+
+static inline const struct descriptor *server_find_descriptor(struct server *server,
+ uint16_t type, uint16_t index)
+{
+ struct descriptor *d;
+ spa_list_for_each(d, &server->descriptors, link) {
+ if (d->type == type &&
+ d->index == index)
+ return d;
+ }
+ return NULL;
+}
+static inline void *server_add_descriptor(struct server *server,
+ uint16_t type, uint16_t index, size_t size, void *ptr)
+{
+ struct descriptor *d;
+
+ if ((d = calloc(1, sizeof(struct descriptor) + size)) == NULL)
+ return NULL;
+
+ d->type = type;
+ d->index = index;
+ d->size = size;
+ d->ptr = SPA_PTROFF(d, sizeof(struct descriptor), void);
+ if (ptr)
+ memcpy(d->ptr, ptr, size);
+ spa_list_append(&server->descriptors, &d->link);
+ return d->ptr;
+}
+
+static inline struct stream *server_find_stream(struct server *server,
+ enum spa_direction direction, uint16_t index)
+{
+ struct stream *s;
+ spa_list_for_each(s, &server->streams, link) {
+ if (s->direction == direction &&
+ s->index == index)
+ return s;
+ }
+ return NULL;
+}
+
+struct server *avdecc_server_new(struct impl *impl, struct spa_dict *props);
+void avdecc_server_free(struct server *server);
+
+void avdecc_server_add_listener(struct server *server, struct spa_hook *listener,
+ const struct server_events *events, void *data);
+
+int avb_server_make_socket(struct server *server, uint16_t type, const uint8_t mac[6]);
+
+int avb_server_send_packet(struct server *server, const uint8_t dest[6],
+ uint16_t type, void *data, size_t size);
+
+struct aecp {
+ struct server *server;
+ struct spa_hook server_listener;
+};
+
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* AVB_INTERNAL_H */
diff --git a/src/modules/module-avb/maap.c b/src/modules/module-avb/maap.c
new file mode 100644
index 0000000..7d195be
--- /dev/null
+++ b/src/modules/module-avb/maap.c
@@ -0,0 +1,469 @@
+/* AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <unistd.h>
+
+#include <spa/utils/json.h>
+
+#include <pipewire/pipewire.h>
+
+#include "utils.h"
+#include "maap.h"
+
+#define MAAP_ALLOCATION_POOL_SIZE 0xFE00
+#define MAAP_ALLOCATION_POOL_BASE { 0x91, 0xe0, 0xf0, 0x00, 0x00, 0x00 }
+static uint8_t maap_base[6] = MAAP_ALLOCATION_POOL_BASE;
+
+#define MAAP_PROBE_RETRANSMITS 3
+
+#define MAAP_PROBE_INTERVAL_MS 500
+#define MAAP_PROBE_INTERVAL_VAR_MS 100
+
+#define MAAP_ANNOUNCE_INTERVAL_MS 3000
+#define MAAP_ANNOUNCE_INTERVAL_VAR_MS 2000
+
+struct maap {
+ struct server *server;
+ struct spa_hook server_listener;
+
+ struct pw_properties *props;
+
+ struct spa_source *source;
+
+#define STATE_IDLE 0
+#define STATE_PROBE 1
+#define STATE_ANNOUNCE 2
+ uint32_t state;
+ uint64_t timeout;
+ uint32_t probe_count;
+
+ unsigned short xsubi[3];
+
+ uint16_t offset;
+ uint16_t count;
+};
+
+static const char *message_type_as_string(uint8_t message_type)
+{
+ switch (message_type) {
+ case AVB_MAAP_MESSAGE_TYPE_PROBE:
+ return "PROBE";
+ case AVB_MAAP_MESSAGE_TYPE_DEFEND:
+ return "DEFEND";
+ case AVB_MAAP_MESSAGE_TYPE_ANNOUNCE:
+ return "ANNOUNCE";
+ }
+ return "INVALID";
+}
+
+static void maap_message_debug(struct maap *maap, const struct avb_packet_maap *p)
+{
+ uint32_t v;
+ const uint8_t *addr;
+
+ v = AVB_PACKET_MAAP_GET_MESSAGE_TYPE(p);
+ pw_log_info("message-type: %d (%s)", v, message_type_as_string(v));
+ pw_log_info(" maap-version: %d", AVB_PACKET_MAAP_GET_MAAP_VERSION(p));
+ pw_log_info(" length: %d", AVB_PACKET_GET_LENGTH(&p->hdr));
+
+ pw_log_info(" stream-id: 0x%"PRIx64, AVB_PACKET_MAAP_GET_STREAM_ID(p));
+ addr = AVB_PACKET_MAAP_GET_REQUEST_START(p);
+ pw_log_info(" request-start: %02x:%02x:%02x:%02x:%02x:%02x",
+ addr[0], addr[1], addr[2], addr[3], addr[4], addr[5]);
+ pw_log_info(" request-count: %d", AVB_PACKET_MAAP_GET_REQUEST_COUNT(p));
+ addr = AVB_PACKET_MAAP_GET_CONFLICT_START(p);
+ pw_log_info(" conflict-start: %02x:%02x:%02x:%02x:%02x:%02x",
+ addr[0], addr[1], addr[2], addr[3], addr[4], addr[5]);
+ pw_log_info(" conflict-count: %d", AVB_PACKET_MAAP_GET_CONFLICT_COUNT(p));
+}
+
+#define PROBE_TIMEOUT(n) ((n) + (MAAP_PROBE_INTERVAL_MS + \
+ drand48() * MAAP_PROBE_INTERVAL_VAR_MS) * SPA_NSEC_PER_MSEC)
+#define ANNOUNCE_TIMEOUT(n) ((n) + (MAAP_ANNOUNCE_INTERVAL_MS + \
+ drand48() * MAAP_ANNOUNCE_INTERVAL_VAR_MS) * SPA_NSEC_PER_MSEC)
+
+static int make_new_address(struct maap *maap, uint64_t now, int range)
+{
+ maap->offset = nrand48(maap->xsubi) % (MAAP_ALLOCATION_POOL_SIZE - range);
+ maap->count = range;
+ maap->state = STATE_PROBE;
+ maap->probe_count = MAAP_PROBE_RETRANSMITS;
+ maap->timeout = PROBE_TIMEOUT(now);
+ return 0;
+}
+
+static uint16_t maap_check_conflict(struct maap *maap, const uint8_t request_start[6],
+ uint16_t request_count, uint8_t conflict_start[6])
+{
+ uint16_t our_start, our_end;
+ uint16_t req_start, req_end;
+ uint16_t conf_start, conf_count = 0;
+
+ if (memcmp(request_start, maap_base, 4) != 0)
+ return 0;
+
+ our_start = maap->offset;
+ our_end = our_start + maap->count;
+ req_start = request_start[4] << 8 | request_start[5];
+ req_end = req_start + request_count;
+
+ if (our_start >= req_start && our_start <= req_end) {
+ conf_start = our_start;
+ conf_count = SPA_MIN(our_end, req_end) - our_start;
+ }
+ else if (req_start >= our_start && req_start <= our_end) {
+ conf_start = req_start;
+ conf_count = SPA_MIN(req_end, our_end) - req_start;
+ }
+ if (conf_count == 0)
+ return 0;
+
+ conflict_start[4] = conf_start >> 8;
+ conflict_start[5] = conf_start;
+ return conf_count;
+}
+
+static int send_packet(struct maap *maap, uint64_t now,
+ uint8_t type, const uint8_t conflict_start[6], uint16_t conflict_count)
+{
+ struct avb_ethernet_header *h;
+ struct avb_packet_maap *p;
+ uint8_t buf[1024];
+ uint8_t bmac[6] = AVB_MAAP_MAC;
+ int res = 0;
+ uint8_t start[6];
+
+ spa_memzero(buf, sizeof(buf));
+ h = (void*)buf;
+ p = SPA_PTROFF(h, sizeof(*h), void);
+
+ memcpy(h->dest, bmac, 6);
+ memcpy(h->src, maap->server->mac_addr, 6);
+ h->type = htons(AVB_TSN_ETH);
+
+ p->hdr.subtype = AVB_SUBTYPE_MAAP;
+ AVB_PACKET_SET_LENGTH(&p->hdr, sizeof(*p));
+
+ AVB_PACKET_MAAP_SET_MAAP_VERSION(p, 1);
+ AVB_PACKET_MAAP_SET_MESSAGE_TYPE(p, type);
+
+ memcpy(start, maap_base, 4);
+ start[4] = maap->offset >> 8;
+ start[5] = maap->offset;
+ AVB_PACKET_MAAP_SET_REQUEST_START(p, start);
+ AVB_PACKET_MAAP_SET_REQUEST_COUNT(p, maap->count);
+ if (conflict_count) {
+ AVB_PACKET_MAAP_SET_CONFLICT_START(p, conflict_start);
+ AVB_PACKET_MAAP_SET_CONFLICT_COUNT(p, conflict_count);
+ }
+
+ if (maap->server->debug_messages) {
+ pw_log_info("send: %d (%s)", type, message_type_as_string(type));
+ maap_message_debug(maap, p);
+ }
+
+ if (send(maap->source->fd, p, sizeof(*h) + sizeof(*p), 0) < 0) {
+ res = -errno;
+ pw_log_warn("got send error: %m");
+ }
+ return res;
+}
+
+static int handle_probe(struct maap *maap, uint64_t now, const struct avb_packet_maap *p)
+{
+ uint8_t conflict_start[6];
+ uint16_t conflict_count;
+
+ conflict_count = maap_check_conflict(maap, p->request_start, ntohs(p->request_count),
+ conflict_start);
+ if (conflict_count == 0)
+ return 0;
+
+ switch (maap->state) {
+ case STATE_PROBE:
+ make_new_address(maap, now, 8);
+ break;
+ case STATE_ANNOUNCE:
+ send_packet(maap, now, AVB_MAAP_MESSAGE_TYPE_DEFEND, conflict_start, conflict_count);
+ break;
+ }
+ return 0;
+}
+
+static int handle_defend(struct maap *maap, uint64_t now, const struct avb_packet_maap *p)
+{
+ uint8_t conflict_start[6];
+ uint16_t conflict_count;
+
+ conflict_count = maap_check_conflict(maap, p->conflict_start, ntohs(p->conflict_count),
+ conflict_start);
+ if (conflict_count != 0)
+ make_new_address(maap, now, 8);
+ return 0;
+}
+
+static int maap_message(struct maap *maap, uint64_t now, const void *message, int len)
+{
+ const struct avb_packet_maap *p = message;
+
+ if (AVB_PACKET_GET_SUBTYPE(&p->hdr) != AVB_SUBTYPE_MAAP)
+ return 0;
+
+ if (maap->server->debug_messages)
+ maap_message_debug(maap, p);
+
+ switch (AVB_PACKET_MAAP_GET_MESSAGE_TYPE(p)) {
+ case AVB_MAAP_MESSAGE_TYPE_PROBE:
+ handle_probe(maap, now, p);
+ break;
+ case AVB_MAAP_MESSAGE_TYPE_DEFEND:
+ case AVB_MAAP_MESSAGE_TYPE_ANNOUNCE:
+ handle_defend(maap, now, p);
+ break;
+ }
+ return 0;
+}
+
+static void on_socket_data(void *data, int fd, uint32_t mask)
+{
+ struct maap *maap = data;
+ struct timespec now;
+
+ if (mask & SPA_IO_IN) {
+ int len;
+ uint8_t buffer[2048];
+
+ len = recv(fd, buffer, sizeof(buffer), 0);
+
+ if (len < 0) {
+ pw_log_warn("got recv error: %m");
+ }
+ else if (len < (int)sizeof(struct avb_packet_header)) {
+ pw_log_warn("short packet received (%d < %d)", len,
+ (int)sizeof(struct avb_packet_header));
+ } else {
+ clock_gettime(CLOCK_REALTIME, &now);
+ maap_message(maap, SPA_TIMESPEC_TO_NSEC(&now), buffer, len);
+ }
+ }
+}
+
+static int load_state(struct maap *maap)
+{
+ const char *str;
+ char key[512];
+ struct spa_json it[3];
+ bool have_offset = false;
+ int count = 0, offset = 0;
+
+ snprintf(key, sizeof(key), "maap.%s", maap->server->ifname);
+ pw_conf_load_state("module-avb", key, maap->props);
+
+ if ((str = pw_properties_get(maap->props, "maap.addresses")) == NULL)
+ return 0;
+
+ spa_json_init(&it[0], str, strlen(str));
+ if (spa_json_enter_array(&it[0], &it[1]) <= 0)
+ return 0;
+
+ if (spa_json_enter_object(&it[1], &it[2]) <= 0)
+ return 0;
+
+ while (spa_json_get_string(&it[2], key, sizeof(key)) > 0) {
+ const char *val;
+ int len;
+
+ if ((len = spa_json_next(&it[2], &val)) <= 0)
+ break;
+
+ if (spa_streq(key, "start")) {
+ uint8_t addr[6];
+ if (avb_utils_parse_addr(val, len, addr) >= 0 &&
+ memcmp(addr, maap_base, 4) == 0) {
+ offset = addr[4] << 8 | addr[5];
+ have_offset = true;
+ }
+ }
+ else if (spa_streq(key, "count")) {
+ spa_json_parse_int(val, len, &count);
+ }
+ }
+ if (count > 0 && have_offset) {
+ maap->count = count;
+ maap->offset = offset;
+ maap->state = STATE_PROBE;
+ maap->probe_count = MAAP_PROBE_RETRANSMITS;
+ maap->timeout = PROBE_TIMEOUT(0);
+ }
+ return 0;
+}
+
+static int save_state(struct maap *maap)
+{
+ char *ptr;
+ size_t size;
+ FILE *f;
+ char key[512];
+ uint32_t count;
+
+ if ((f = open_memstream(&ptr, &size)) == NULL)
+ return -errno;
+
+ fprintf(f, "[ ");
+ fprintf(f, "{ \"start\": \"%02x:%02x:%02x:%02x:%02x:%02x\", ",
+ maap_base[0], maap_base[1], maap_base[2],
+ maap_base[3], (maap->offset >> 8) & 0xff,
+ maap->offset & 0xff);
+ fprintf(f, " \"count\": %u } ", maap->count);
+ fprintf(f, "]");
+ fclose(f);
+
+ count = pw_properties_set(maap->props, "maap.addresses", ptr);
+ free(ptr);
+
+ if (count > 0) {
+ snprintf(key, sizeof(key), "maap.%s", maap->server->ifname);
+ pw_conf_save_state("module-avb", key, maap->props);
+ }
+ return 0;
+}
+
+static void maap_periodic(void *data, uint64_t now)
+{
+ struct maap *maap = data;
+
+ if (now < maap->timeout)
+ return;
+
+ switch(maap->state) {
+ case STATE_IDLE:
+ break;
+ case STATE_PROBE:
+ send_packet(maap, now, AVB_MAAP_MESSAGE_TYPE_PROBE, NULL, 0);
+ if (--maap->probe_count == 0) {
+ maap->state = STATE_ANNOUNCE;
+ save_state(maap);
+ }
+ maap->timeout = PROBE_TIMEOUT(now);
+ break;
+ case STATE_ANNOUNCE:
+ send_packet(maap, now, AVB_MAAP_MESSAGE_TYPE_ANNOUNCE, NULL, 0);
+ maap->timeout = ANNOUNCE_TIMEOUT(now);
+ break;
+ }
+}
+
+static void maap_free(struct maap *maap)
+{
+ pw_loop_destroy_source(maap->server->impl->loop, maap->source);
+ spa_hook_remove(&maap->server_listener);
+ pw_properties_free(maap->props);
+ free(maap);
+}
+
+static void maap_destroy(void *data)
+{
+ struct maap *maap = data;
+ maap_free(maap);
+}
+
+static const struct server_events server_events = {
+ AVB_VERSION_SERVER_EVENTS,
+ .destroy = maap_destroy,
+ .periodic = maap_periodic,
+};
+
+struct avb_maap *avb_maap_register(struct server *server)
+{
+ struct maap *maap;
+ uint8_t bmac[6] = AVB_MAAP_MAC;
+ int fd, res;
+
+ fd = avb_server_make_socket(server, AVB_TSN_ETH, bmac);
+ if (fd < 0) {
+ res = fd;
+ goto error;
+ }
+
+ maap = calloc(1, sizeof(*maap));
+ if (maap == NULL) {
+ res = -errno;
+ goto error_close;
+ }
+ maap->props = pw_properties_new(NULL, NULL);
+ if (maap->props == NULL) {
+ res = -errno;
+ goto error_free;
+ }
+
+ maap->server = server;
+ pw_log_info("0x%"PRIx64" %d", server->entity_id, server->ifindex);
+
+ if (pw_getrandom(maap->xsubi, sizeof(maap->xsubi), 0) != sizeof(maap->xsubi)) {
+ res = -errno;
+ goto error_free;
+ }
+ load_state(maap);
+
+ maap->source = pw_loop_add_io(server->impl->loop, fd, SPA_IO_IN, true, on_socket_data, maap);
+ if (maap->source == NULL) {
+ res = -errno;
+ pw_log_error("maap %p: can't create maap source: %m", maap);
+ goto error_free;
+ }
+ avdecc_server_add_listener(server, &maap->server_listener, &server_events, maap);
+
+ return (struct avb_maap *)maap;
+
+error_free:
+ free(maap);
+error_close:
+ close(fd);
+error:
+ errno = -res;
+ return NULL;
+}
+
+int avb_maap_reserve(struct avb_maap *m, uint32_t count)
+{
+ struct maap *maap = (struct maap*)m;
+ if (count > maap->count)
+ make_new_address(maap, 0, count);
+ return 0;
+}
+
+int avb_maap_get_address(struct avb_maap *m, uint8_t addr[6], uint32_t index)
+{
+ struct maap *maap = (struct maap*)m;
+ uint16_t offset;
+
+ if (maap->state != STATE_ANNOUNCE)
+ return -EAGAIN;
+
+ memcpy(addr, maap_base, 6);
+ offset = maap->offset + index;
+ addr[4] = offset >> 8;
+ addr[5] = offset;
+ return 0;
+}
diff --git a/src/modules/module-avb/maap.h b/src/modules/module-avb/maap.h
new file mode 100644
index 0000000..6e56f8e
--- /dev/null
+++ b/src/modules/module-avb/maap.h
@@ -0,0 +1,70 @@
+/* AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef AVB_MAAP_H
+#define AVB_MAAP_H
+
+#include "packets.h"
+#include "internal.h"
+
+#define AVB_TSN_ETH 0x22f0
+#define AVB_MAAP_MAC { 0x91, 0xe0, 0xf0, 0x00, 0xff, 0x00 };
+
+#define AVB_MAAP_MESSAGE_TYPE_PROBE 1
+#define AVB_MAAP_MESSAGE_TYPE_DEFEND 2
+#define AVB_MAAP_MESSAGE_TYPE_ANNOUNCE 3
+
+struct avb_packet_maap {
+ struct avb_packet_header hdr;
+ uint64_t stream_id;
+ uint8_t request_start[6];
+ uint16_t request_count;
+ uint8_t conflict_start[6];
+ uint16_t conflict_count;
+} __attribute__ ((__packed__));
+
+#define AVB_PACKET_MAAP_SET_MESSAGE_TYPE(p,v) AVB_PACKET_SET_SUB1(&(p)->hdr, v)
+#define AVB_PACKET_MAAP_SET_MAAP_VERSION(p,v) AVB_PACKET_SET_SUB2(&(p)->hdr, v)
+#define AVB_PACKET_MAAP_SET_STREAM_ID(p,v) ((p)->stream_id = htobe64(v))
+#define AVB_PACKET_MAAP_SET_REQUEST_START(p,v) memcpy((p)->request_start, (v), 6)
+#define AVB_PACKET_MAAP_SET_REQUEST_COUNT(p,v) ((p)->request_count = htons(v))
+#define AVB_PACKET_MAAP_SET_CONFLICT_START(p,v) memcpy((p)->conflict_start, (v), 6)
+#define AVB_PACKET_MAAP_SET_CONFLICT_COUNT(p,v) ((p)->conflict_count = htons(v))
+
+#define AVB_PACKET_MAAP_GET_MESSAGE_TYPE(p) AVB_PACKET_GET_SUB1(&(p)->hdr)
+#define AVB_PACKET_MAAP_GET_MAAP_VERSION(p) AVB_PACKET_GET_SUB2(&(p)->hdr)
+#define AVB_PACKET_MAAP_GET_STREAM_ID(p) be64toh((p)->stream_id)
+#define AVB_PACKET_MAAP_GET_REQUEST_START(p) ((p)->request_start)
+#define AVB_PACKET_MAAP_GET_REQUEST_COUNT(p) ntohs((p)->request_count)
+#define AVB_PACKET_MAAP_GET_CONFLICT_START(p) ((p)->conflict_start)
+#define AVB_PACKET_MAAP_GET_CONFLICT_COUNT(p) ntohs((p)->conflict_count)
+
+struct avb_maap;
+
+struct avb_maap *avb_maap_register(struct server *server);
+
+int avb_maap_reserve(struct avb_maap *maap, uint32_t count);
+int avb_maap_get_address(struct avb_maap *maap, uint8_t addr[6], uint32_t index);
+
+#endif /* AVB_MAAP_H */
diff --git a/src/modules/module-avb/mmrp.c b/src/modules/module-avb/mmrp.c
new file mode 100644
index 0000000..022aea8
--- /dev/null
+++ b/src/modules/module-avb/mmrp.c
@@ -0,0 +1,233 @@
+/* AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <unistd.h>
+
+#include <pipewire/pipewire.h>
+
+#include "utils.h"
+#include "mmrp.h"
+
+static const uint8_t mmrp_mac[6] = AVB_MMRP_MAC;
+
+struct attr {
+ struct avb_mmrp_attribute attr;
+ struct spa_list link;
+};
+
+struct mmrp {
+ struct server *server;
+ struct spa_hook server_listener;
+
+ struct spa_source *source;
+
+ struct spa_list attributes;
+};
+
+static bool mmrp_check_header(void *data, const void *hdr, size_t *hdr_size, bool *has_params)
+{
+ const struct avb_packet_mmrp_msg *msg = hdr;
+ uint8_t attr_type = msg->attribute_type;
+
+ if (!AVB_MMRP_ATTRIBUTE_TYPE_VALID(attr_type))
+ return false;
+
+ *hdr_size = sizeof(*msg);
+ *has_params = false;
+ return true;
+}
+
+static int mmrp_attr_event(void *data, uint64_t now, uint8_t attribute_type, uint8_t event)
+{
+ struct mmrp *mmrp = data;
+ struct attr *a;
+ spa_list_for_each(a, &mmrp->attributes, link)
+ if (a->attr.type == attribute_type)
+ avb_mrp_attribute_update_state(a->attr.mrp, now, event);
+ return 0;
+}
+
+static void debug_service_requirement(const struct avb_packet_mmrp_service_requirement *t)
+{
+ char buf[128];
+ pw_log_info("service requirement");
+ pw_log_info(" %s", avb_utils_format_addr(buf, sizeof(buf), t->addr));
+}
+
+static int process_service_requirement(struct mmrp *mmrp, uint64_t now, uint8_t attr_type,
+ const void *m, uint8_t event, uint8_t param, int num)
+{
+ const struct avb_packet_mmrp_service_requirement *t = m;
+ struct attr *a;
+
+ debug_service_requirement(t);
+
+ spa_list_for_each(a, &mmrp->attributes, link)
+ if (a->attr.type == attr_type &&
+ memcmp(a->attr.attr.service_requirement.addr, t->addr, 6) == 0)
+ avb_mrp_attribute_rx_event(a->attr.mrp, now, event);
+ return 0;
+}
+
+static void debug_process_mac(const struct avb_packet_mmrp_mac *t)
+{
+ char buf[128];
+ pw_log_info("mac");
+ pw_log_info(" %s", avb_utils_format_addr(buf, sizeof(buf), t->addr));
+}
+
+static int process_mac(struct mmrp *mmrp, uint64_t now, uint8_t attr_type,
+ const void *m, uint8_t event, uint8_t param, int num)
+{
+ const struct avb_packet_mmrp_mac *t = m;
+ struct attr *a;
+
+ debug_process_mac(t);
+
+ spa_list_for_each(a, &mmrp->attributes, link)
+ if (a->attr.type == attr_type &&
+ memcmp(a->attr.attr.mac.addr, t->addr, 6) == 0)
+ avb_mrp_attribute_rx_event(a->attr.mrp, now, event);
+ return 0;
+}
+
+static const struct {
+ int (*dispatch) (struct mmrp *mmrp, uint64_t now, uint8_t attr_type,
+ const void *m, uint8_t event, uint8_t param, int num);
+} dispatch[] = {
+ [AVB_MMRP_ATTRIBUTE_TYPE_SERVICE_REQUIREMENT] = { process_service_requirement, },
+ [AVB_MMRP_ATTRIBUTE_TYPE_MAC] = { process_mac, },
+};
+
+static int mmrp_process(void *data, uint64_t now, uint8_t attribute_type, const void *value,
+ uint8_t event, uint8_t param, int index)
+{
+ struct mmrp *mmrp = data;
+ return dispatch[attribute_type].dispatch(mmrp, now,
+ attribute_type, value, event, param, index);
+}
+
+static const struct avb_mrp_parse_info info = {
+ AVB_VERSION_MRP_PARSE_INFO,
+ .check_header = mmrp_check_header,
+ .attr_event = mmrp_attr_event,
+ .process = mmrp_process,
+};
+
+static int mmrp_message(struct mmrp *mmrp, uint64_t now, const void *message, int len)
+{
+ pw_log_debug("MMRP");
+ return avb_mrp_parse_packet(mmrp->server->mrp,
+ now, message, len, &info, mmrp);
+}
+
+static void on_socket_data(void *data, int fd, uint32_t mask)
+{
+ struct mmrp *mmrp = data;
+ struct timespec now;
+
+ if (mask & SPA_IO_IN) {
+ int len;
+ uint8_t buffer[2048];
+
+ len = recv(fd, buffer, sizeof(buffer), 0);
+
+ if (len < 0) {
+ pw_log_warn("got recv error: %m");
+ }
+ else if (len < (int)sizeof(struct avb_packet_header)) {
+ pw_log_warn("short packet received (%d < %d)", len,
+ (int)sizeof(struct avb_packet_header));
+ } else {
+ clock_gettime(CLOCK_REALTIME, &now);
+ mmrp_message(mmrp, SPA_TIMESPEC_TO_NSEC(&now), buffer, len);
+ }
+ }
+}
+static void mmrp_destroy(void *data)
+{
+ struct mmrp *mmrp = data;
+ spa_hook_remove(&mmrp->server_listener);
+ pw_loop_destroy_source(mmrp->server->impl->loop, mmrp->source);
+ free(mmrp);
+}
+
+static const struct server_events server_events = {
+ AVB_VERSION_SERVER_EVENTS,
+ .destroy = mmrp_destroy,
+};
+
+struct avb_mmrp_attribute *avb_mmrp_attribute_new(struct avb_mmrp *m,
+ uint8_t type)
+{
+ struct mmrp *mmrp = (struct mmrp*)m;
+ struct avb_mrp_attribute *attr;
+ struct attr *a;
+
+ attr = avb_mrp_attribute_new(mmrp->server->mrp, sizeof(struct attr));
+
+ a = attr->user_data;
+ a->attr.mrp = attr;
+ a->attr.type = type;
+ spa_list_append(&mmrp->attributes, &a->link);
+
+ return &a->attr;
+}
+
+struct avb_mmrp *avb_mmrp_register(struct server *server)
+{
+ struct mmrp *mmrp;
+ int fd, res;
+
+ fd = avb_server_make_socket(server, AVB_MMRP_ETH, mmrp_mac);
+ if (fd < 0) {
+ errno = -fd;
+ return NULL;
+ }
+ mmrp = calloc(1, sizeof(*mmrp));
+ if (mmrp == NULL) {
+ res = -errno;
+ goto error_close;
+ }
+
+ mmrp->server = server;
+ spa_list_init(&mmrp->attributes);
+
+ mmrp->source = pw_loop_add_io(server->impl->loop, fd, SPA_IO_IN, true, on_socket_data, mmrp);
+ if (mmrp->source == NULL) {
+ res = -errno;
+ pw_log_error("mmrp %p: can't create mmrp source: %m", mmrp);
+ goto error_no_source;
+ }
+ avdecc_server_add_listener(server, &mmrp->server_listener, &server_events, mmrp);
+
+ return (struct avb_mmrp*)mmrp;
+
+error_no_source:
+ free(mmrp);
+error_close:
+ close(fd);
+ errno = -res;
+ return NULL;
+}
diff --git a/src/modules/module-avb/mmrp.h b/src/modules/module-avb/mmrp.h
new file mode 100644
index 0000000..b7bcf8c
--- /dev/null
+++ b/src/modules/module-avb/mmrp.h
@@ -0,0 +1,68 @@
+/* AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef AVB_MMRP_H
+#define AVB_MMRP_H
+
+#include "mrp.h"
+#include "internal.h"
+
+#define AVB_MMRP_ETH 0x88f6
+#define AVB_MMRP_MAC { 0x01, 0x80, 0xc2, 0x00, 0x00, 0x20 }
+
+#define AVB_MMRP_ATTRIBUTE_TYPE_SERVICE_REQUIREMENT 1
+#define AVB_MMRP_ATTRIBUTE_TYPE_MAC 2
+#define AVB_MMRP_ATTRIBUTE_TYPE_VALID(t) ((t)>=1 && (t)<=2)
+
+struct avb_packet_mmrp_msg {
+ uint8_t attribute_type;
+ uint8_t attribute_length;
+ uint8_t attribute_list[0];
+} __attribute__ ((__packed__));
+
+struct avb_packet_mmrp_service_requirement {
+ unsigned char addr[6];
+} __attribute__ ((__packed__));
+
+struct avb_packet_mmrp_mac {
+ unsigned char addr[6];
+} __attribute__ ((__packed__));
+
+struct avb_mmrp;
+
+struct avb_mmrp_attribute {
+ struct avb_mrp_attribute *mrp;
+ uint8_t type;
+ union {
+ struct avb_packet_mmrp_service_requirement service_requirement;
+ struct avb_packet_mmrp_mac mac;
+ } attr;
+};
+
+struct avb_mmrp_attribute *avb_mmrp_attribute_new(struct avb_mmrp *mmrp,
+ uint8_t type);
+
+struct avb_mmrp *avb_mmrp_register(struct server *server);
+
+#endif /* AVB_MMRP_H */
diff --git a/src/modules/module-avb/mrp.c b/src/modules/module-avb/mrp.c
new file mode 100644
index 0000000..7b6bc46
--- /dev/null
+++ b/src/modules/module-avb/mrp.c
@@ -0,0 +1,612 @@
+/* AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <pipewire/pipewire.h>
+
+#include "mrp.h"
+
+#define MRP_JOINTIMER_MS 100
+#define MRP_LVTIMER_MS 1000
+#define MRP_LVATIMER_MS 10000
+#define MRP_PERIODTIMER_MS 1000
+
+#define mrp_emit(s,m,v,...) spa_hook_list_call(&s->listener_list, struct avb_mrp_events, m, v, ##__VA_ARGS__)
+#define mrp_emit_event(s,n,e) mrp_emit(s,event,0,n,e)
+#define mrp_emit_notify(s,n,a,e) mrp_emit(s,notify,0,n,a,e)
+
+#define mrp_attribute_emit(a,m,v,...) spa_hook_list_call(&a->listener_list, struct avb_mrp_attribute_events, m, v, ##__VA_ARGS__)
+#define mrp_attribute_emit_notify(a,n,e) mrp_attribute_emit(a,notify,0,n,e)
+
+
+struct mrp;
+
+struct attribute {
+ struct avb_mrp_attribute attr;
+ struct mrp *mrp;
+ struct spa_list link;
+ uint8_t applicant_state;
+ uint8_t registrar_state;
+ uint64_t leave_timeout;
+ unsigned joined:1;
+ struct spa_hook_list listener_list;
+};
+
+struct mrp {
+ struct server *server;
+ struct spa_hook server_listener;
+
+ struct spa_hook_list listener_list;
+
+ struct spa_list attributes;
+
+ uint64_t periodic_timeout;
+ uint64_t leave_all_timeout;
+ uint64_t join_timeout;
+};
+
+static void mrp_destroy(void *data)
+{
+ struct mrp *mrp = data;
+ spa_hook_remove(&mrp->server_listener);
+ free(mrp);
+}
+
+static void global_event(struct mrp *mrp, uint64_t now, uint8_t event)
+{
+ struct attribute *a;
+ spa_list_for_each(a, &mrp->attributes, link)
+ avb_mrp_attribute_update_state(&a->attr, now, event);
+ mrp_emit_event(mrp, now, event);
+}
+
+static void mrp_periodic(void *data, uint64_t now)
+{
+ struct mrp *mrp = data;
+ bool leave_all = false;
+ struct attribute *a;
+
+ if (now > mrp->periodic_timeout) {
+ if (mrp->periodic_timeout > 0)
+ global_event(mrp, now, AVB_MRP_EVENT_PERIODIC);
+ mrp->periodic_timeout = now + MRP_PERIODTIMER_MS * SPA_NSEC_PER_MSEC;
+ }
+ if (now > mrp->leave_all_timeout) {
+ if (mrp->leave_all_timeout > 0) {
+ global_event(mrp, now, AVB_MRP_EVENT_RX_LVA);
+ leave_all = true;
+ }
+ mrp->leave_all_timeout = now + (MRP_LVATIMER_MS + (random() % (MRP_LVATIMER_MS / 2)))
+ * SPA_NSEC_PER_MSEC;
+ }
+
+ if (now > mrp->join_timeout) {
+ if (mrp->join_timeout > 0) {
+ uint8_t event = leave_all ? AVB_MRP_EVENT_TX_LVA : AVB_MRP_EVENT_TX;
+ global_event(mrp, now, event);
+ }
+ mrp->join_timeout = now + MRP_JOINTIMER_MS * SPA_NSEC_PER_MSEC;
+ }
+
+ spa_list_for_each(a, &mrp->attributes, link) {
+ if (a->leave_timeout > 0 && now > a->leave_timeout) {
+ a->leave_timeout = 0;
+ avb_mrp_attribute_update_state(&a->attr, now, AVB_MRP_EVENT_LV_TIMER);
+ }
+ }
+}
+
+static const struct server_events server_events = {
+ AVB_VERSION_SERVER_EVENTS,
+ .destroy = mrp_destroy,
+ .periodic = mrp_periodic,
+};
+
+int avb_mrp_parse_packet(struct avb_mrp *mrp, uint64_t now, const void *pkt, int len,
+ const struct avb_mrp_parse_info *info, void *data)
+{
+ uint8_t *e = SPA_PTROFF(pkt, len, uint8_t);
+ uint8_t *m = SPA_PTROFF(pkt, sizeof(struct avb_packet_mrp), uint8_t);
+
+ while (m < e && (m[0] != 0 || m[1] != 0)) {
+ const struct avb_packet_mrp_hdr *hdr = (const struct avb_packet_mrp_hdr*)m;
+ uint8_t attr_type = hdr->attribute_type;
+ uint8_t attr_len = hdr->attribute_length;
+ size_t hdr_size;
+ bool has_param;
+
+ if (!info->check_header(data, hdr, &hdr_size, &has_param))
+ return -EINVAL;
+
+ m += hdr_size;
+
+ while (m < e && (m[0] != 0 || m[1] != 0)) {
+ const struct avb_packet_mrp_vector *v =
+ (const struct avb_packet_mrp_vector*)m;
+ uint16_t i, num_values = AVB_MRP_VECTOR_GET_NUM_VALUES(v);
+ uint8_t event_len = (num_values+2)/3;
+ uint8_t param_len = has_param ? (num_values+3)/4 : 0;
+ int plen = sizeof(*v) + attr_len + event_len + param_len;
+ const uint8_t *first = v->first_value;
+ uint8_t event[3], param[4] = { 0, };
+
+ if (m + plen > e)
+ return -EPROTO;
+
+ if (v->lva)
+ info->attr_event(data, now, attr_type, AVB_MRP_EVENT_RX_LVA);
+
+ for (i = 0; i < num_values; i++) {
+ if (i % 3 == 0) {
+ uint8_t ep = first[attr_len + i/3];
+ event[2] = ep % 6; ep /= 6;
+ event[1] = ep % 6; ep /= 6;
+ event[0] = ep % 6;
+ }
+ if (has_param && (i % 4 == 0)) {
+ uint8_t ep = first[attr_len + event_len + i/4];
+ param[3] = ep % 4; ep /= 4;
+ param[2] = ep % 4; ep /= 4;
+ param[1] = ep % 4; ep /= 4;
+ param[0] = ep % 4;
+ }
+ info->process(data, now, attr_type, first,
+ event[i%3], param[i%4], i);
+ }
+ m += plen;
+ }
+ m += 2;
+ }
+ return 0;
+}
+
+const char *avb_mrp_notify_name(uint8_t notify)
+{
+ switch(notify) {
+ case AVB_MRP_NOTIFY_NEW:
+ return "new";
+ case AVB_MRP_NOTIFY_JOIN:
+ return "join";
+ case AVB_MRP_NOTIFY_LEAVE:
+ return "leave";
+ }
+ return "unknown";
+}
+
+const char *avb_mrp_send_name(uint8_t send)
+{
+ switch(send) {
+ case AVB_MRP_SEND_NEW:
+ return "new";
+ case AVB_MRP_SEND_JOININ:
+ return "joinin";
+ case AVB_MRP_SEND_IN:
+ return "in";
+ case AVB_MRP_SEND_JOINMT:
+ return "joinmt";
+ case AVB_MRP_SEND_MT:
+ return "mt";
+ case AVB_MRP_SEND_LV:
+ return "leave";
+ }
+ return "unknown";
+}
+
+struct avb_mrp_attribute *avb_mrp_attribute_new(struct avb_mrp *m,
+ size_t user_size)
+{
+ struct mrp *mrp = (struct mrp*)m;
+ struct attribute *a;
+
+ a = calloc(1, sizeof(*a) + user_size);
+ if (a == NULL)
+ return NULL;
+
+ a->mrp = mrp;
+ a->attr.user_data = SPA_PTROFF(a, sizeof(*a), void);
+ spa_hook_list_init(&a->listener_list);
+ spa_list_append(&mrp->attributes, &a->link);
+
+ return &a->attr;
+}
+
+void avb_mrp_attribute_destroy(struct avb_mrp_attribute *attr)
+{
+ struct attribute *a = SPA_CONTAINER_OF(attr, struct attribute, attr);
+ spa_list_remove(&a->link);
+ free(a);
+}
+
+void avb_mrp_attribute_add_listener(struct avb_mrp_attribute *attr, struct spa_hook *listener,
+ const struct avb_mrp_attribute_events *events, void *data)
+{
+ struct attribute *a = SPA_CONTAINER_OF(attr, struct attribute, attr);
+ spa_hook_list_append(&a->listener_list, listener, events, data);
+}
+
+void avb_mrp_attribute_update_state(struct avb_mrp_attribute *attr, uint64_t now,
+ int event)
+{
+ struct attribute *a = SPA_CONTAINER_OF(attr, struct attribute, attr);
+ struct mrp *mrp = a->mrp;
+ uint8_t notify = 0, state;
+ uint8_t send = 0;
+
+ state = a->registrar_state;
+
+ switch (event) {
+ case AVB_MRP_EVENT_BEGIN:
+ state = AVB_MRP_MT;
+ break;
+ case AVB_MRP_EVENT_RX_NEW:
+ notify = AVB_MRP_NOTIFY_NEW;
+ switch (state) {
+ case AVB_MRP_LV:
+ a->leave_timeout = 0;
+ break;
+ }
+ state = AVB_MRP_IN;
+ break;
+ case AVB_MRP_EVENT_RX_JOININ:
+ case AVB_MRP_EVENT_RX_JOINMT:
+ switch (state) {
+ case AVB_MRP_LV:
+ a->leave_timeout = 0;
+ break;
+ case AVB_MRP_MT:
+ notify = AVB_MRP_NOTIFY_JOIN;
+ break;
+ }
+ state = AVB_MRP_IN;
+ break;
+ case AVB_MRP_EVENT_RX_LV:
+ case AVB_MRP_EVENT_RX_LVA:
+ case AVB_MRP_EVENT_TX_LVA:
+ case AVB_MRP_EVENT_REDECLARE:
+ switch (state) {
+ case AVB_MRP_IN:
+ a->leave_timeout = now + MRP_LVTIMER_MS * SPA_NSEC_PER_MSEC;
+ //state = AVB_MRP_LV;
+ break;
+ }
+ break;
+ case AVB_MRP_EVENT_FLUSH:
+ switch (state) {
+ case AVB_MRP_LV:
+ notify = AVB_MRP_NOTIFY_LEAVE;
+ break;
+ }
+ state = AVB_MRP_MT;
+ break;
+ case AVB_MRP_EVENT_LV_TIMER:
+ switch (state) {
+ case AVB_MRP_LV:
+ notify = AVB_MRP_NOTIFY_LEAVE;
+ state = AVB_MRP_MT;
+ break;
+ }
+ break;
+ default:
+ break;
+ }
+ if (notify) {
+ mrp_attribute_emit_notify(a, now, notify);
+ mrp_emit_notify(mrp, now, &a->attr, notify);
+ }
+
+ if (a->registrar_state != state || notify) {
+ pw_log_debug("attr %p: %d %d -> %d %d", a, event, a->registrar_state, state, notify);
+ a->registrar_state = state;
+ }
+
+ state = a->applicant_state;
+
+ switch (event) {
+ case AVB_MRP_EVENT_BEGIN:
+ state = AVB_MRP_VO;
+ break;
+ case AVB_MRP_EVENT_NEW:
+ switch (state) {
+ case AVB_MRP_VN:
+ case AVB_MRP_AN:
+ break;
+ default:
+ state = AVB_MRP_VN;
+ break;
+ }
+ break;
+ case AVB_MRP_EVENT_JOIN:
+ switch (state) {
+ case AVB_MRP_VO:
+ case AVB_MRP_LO:
+ state = AVB_MRP_VP;
+ break;
+ case AVB_MRP_LA:
+ state = AVB_MRP_AA;
+ break;
+ case AVB_MRP_AO:
+ state = AVB_MRP_AP;
+ break;
+ case AVB_MRP_QO:
+ state = AVB_MRP_QP;
+ break;
+ }
+ break;
+ case AVB_MRP_EVENT_LV:
+ switch (state) {
+ case AVB_MRP_VP:
+ state = AVB_MRP_VO;
+ break;
+ case AVB_MRP_VN:
+ case AVB_MRP_AN:
+ case AVB_MRP_AA:
+ case AVB_MRP_QA:
+ state = AVB_MRP_LA;
+ break;
+ case AVB_MRP_AP:
+ state = AVB_MRP_AO;
+ break;
+ case AVB_MRP_QP:
+ state = AVB_MRP_QO;
+ break;
+ }
+ break;
+ case AVB_MRP_EVENT_RX_JOININ:
+ switch (state) {
+ case AVB_MRP_VO:
+ state = AVB_MRP_AO;
+ break;
+ case AVB_MRP_VP:
+ state = AVB_MRP_AP;
+ break;
+ case AVB_MRP_AA:
+ state = AVB_MRP_QA;
+ break;
+ case AVB_MRP_AO:
+ state = AVB_MRP_QO;
+ break;
+ case AVB_MRP_AP:
+ state = AVB_MRP_QP;
+ break;
+ }
+ SPA_FALLTHROUGH;
+ case AVB_MRP_EVENT_RX_IN:
+ switch (state) {
+ case AVB_MRP_AA:
+ state = AVB_MRP_QA;
+ break;
+ }
+ break;
+ case AVB_MRP_EVENT_RX_JOINMT:
+ case AVB_MRP_EVENT_RX_MT:
+ switch (state) {
+ case AVB_MRP_QA:
+ state = AVB_MRP_AA;
+ break;
+ case AVB_MRP_QO:
+ state = AVB_MRP_AO;
+ break;
+ case AVB_MRP_QP:
+ state = AVB_MRP_AP;
+ break;
+ case AVB_MRP_LO:
+ state = AVB_MRP_VO;
+ break;
+ }
+ break;
+ case AVB_MRP_EVENT_RX_LV:
+ case AVB_MRP_EVENT_RX_LVA:
+ case AVB_MRP_EVENT_REDECLARE:
+ switch (state) {
+ case AVB_MRP_VO:
+ case AVB_MRP_AO:
+ case AVB_MRP_QO:
+ state = AVB_MRP_LO;
+ break;
+ case AVB_MRP_AN:
+ state = AVB_MRP_VN;
+ break;
+ case AVB_MRP_AA:
+ case AVB_MRP_QA:
+ case AVB_MRP_AP:
+ case AVB_MRP_QP:
+ state = AVB_MRP_VP;
+ break;
+ }
+ break;
+ case AVB_MRP_EVENT_PERIODIC:
+ switch (state) {
+ case AVB_MRP_QA:
+ state = AVB_MRP_AA;
+ break;
+ case AVB_MRP_QP:
+ state = AVB_MRP_AP;
+ break;
+ }
+ break;
+ case AVB_MRP_EVENT_TX:
+ switch (state) {
+ case AVB_MRP_VP:
+ case AVB_MRP_AA:
+ case AVB_MRP_AP:
+ if (a->registrar_state == AVB_MRP_IN)
+ send = AVB_MRP_SEND_JOININ;
+ else
+ send = AVB_MRP_SEND_JOINMT;
+ break;
+ case AVB_MRP_VN:
+ case AVB_MRP_AN:
+ send = AVB_MRP_SEND_NEW;
+ break;
+ case AVB_MRP_LA:
+ send = AVB_MRP_SEND_LV;
+ break;
+ case AVB_MRP_LO:
+ if (a->registrar_state == AVB_MRP_IN)
+ send = AVB_MRP_SEND_IN;
+ else
+ send = AVB_MRP_SEND_MT;
+ break;
+ }
+ switch (state) {
+ case AVB_MRP_VP:
+ state = AVB_MRP_AA;
+ break;
+ case AVB_MRP_VN:
+ state = AVB_MRP_AN;
+ break;
+ case AVB_MRP_AN:
+ if(a->registrar_state == AVB_MRP_IN)
+ state = AVB_MRP_QA;
+ else
+ state = AVB_MRP_AA;
+ break;
+ case AVB_MRP_AA:
+ case AVB_MRP_AP:
+ state = AVB_MRP_QA;
+ break;
+ case AVB_MRP_LA:
+ case AVB_MRP_LO:
+ state = AVB_MRP_VO;
+ break;
+ }
+ break;
+ case AVB_MRP_EVENT_TX_LVA:
+ {
+ switch (state) {
+ case AVB_MRP_VP:
+ if (a->registrar_state == AVB_MRP_IN)
+ send = AVB_MRP_SEND_IN;
+ else
+ send = AVB_MRP_SEND_MT;
+ break;
+ case AVB_MRP_VN:
+ case AVB_MRP_AN:
+ send = AVB_MRP_SEND_NEW;
+ break;
+ case AVB_MRP_AA:
+ case AVB_MRP_QA:
+ case AVB_MRP_AP:
+ case AVB_MRP_QP:
+ if (a->registrar_state == AVB_MRP_IN)
+ send = AVB_MRP_SEND_JOININ;
+ else
+ send = AVB_MRP_SEND_JOINMT;
+ break;
+ }
+ switch (state) {
+ case AVB_MRP_VO:
+ case AVB_MRP_LA:
+ case AVB_MRP_AO:
+ case AVB_MRP_QO:
+ state = AVB_MRP_LO;
+ break;
+ case AVB_MRP_VP:
+ state = AVB_MRP_AA;
+ break;
+ case AVB_MRP_VN:
+ state = AVB_MRP_AN;
+ break;
+ case AVB_MRP_AN:
+ case AVB_MRP_AA:
+ case AVB_MRP_AP:
+ case AVB_MRP_QP:
+ state = AVB_MRP_QA;
+ break;
+ }
+ break;
+ }
+ default:
+ break;
+ }
+ if (a->applicant_state != state || send) {
+ pw_log_debug("attr %p: %d %d -> %d %d", a, event, a->applicant_state, state, send);
+ a->applicant_state = state;
+ }
+ if (a->joined)
+ a->attr.pending_send = send;
+}
+
+void avb_mrp_attribute_rx_event(struct avb_mrp_attribute *attr, uint64_t now, uint8_t event)
+{
+ static const int map[] = {
+ [AVB_MRP_ATTRIBUTE_EVENT_NEW] = AVB_MRP_EVENT_RX_NEW,
+ [AVB_MRP_ATTRIBUTE_EVENT_JOININ] = AVB_MRP_EVENT_RX_JOININ,
+ [AVB_MRP_ATTRIBUTE_EVENT_IN] = AVB_MRP_EVENT_RX_IN,
+ [AVB_MRP_ATTRIBUTE_EVENT_JOINMT] = AVB_MRP_EVENT_RX_JOINMT,
+ [AVB_MRP_ATTRIBUTE_EVENT_MT] = AVB_MRP_EVENT_RX_MT,
+ [AVB_MRP_ATTRIBUTE_EVENT_LV] = AVB_MRP_EVENT_RX_LV,
+ };
+ avb_mrp_attribute_update_state(attr, now, map[event]);
+}
+
+void avb_mrp_attribute_begin(struct avb_mrp_attribute *attr, uint64_t now)
+{
+ struct attribute *a = SPA_CONTAINER_OF(attr, struct attribute, attr);
+ a->leave_timeout = 0;
+ avb_mrp_attribute_update_state(attr, now, AVB_MRP_EVENT_BEGIN);
+}
+
+void avb_mrp_attribute_join(struct avb_mrp_attribute *attr, uint64_t now, bool is_new)
+{
+ struct attribute *a = SPA_CONTAINER_OF(attr, struct attribute, attr);
+ a->joined = true;
+ int event = is_new ? AVB_MRP_EVENT_NEW : AVB_MRP_EVENT_JOIN;
+ avb_mrp_attribute_update_state(attr, now, event);
+}
+
+void avb_mrp_attribute_leave(struct avb_mrp_attribute *attr, uint64_t now)
+{
+ struct attribute *a = SPA_CONTAINER_OF(attr, struct attribute, attr);
+ avb_mrp_attribute_update_state(attr, now, AVB_MRP_EVENT_LV);
+ a->joined = false;
+}
+
+void avb_mrp_destroy(struct avb_mrp *mrp)
+{
+ mrp_destroy(mrp);
+}
+
+struct avb_mrp *avb_mrp_new(struct server *server)
+{
+ struct mrp *mrp;
+
+ mrp = calloc(1, sizeof(*mrp));
+ if (mrp == NULL)
+ return NULL;
+
+ mrp->server = server;
+ spa_list_init(&mrp->attributes);
+ spa_hook_list_init(&mrp->listener_list);
+
+ avdecc_server_add_listener(server, &mrp->server_listener, &server_events, mrp);
+
+ return (struct avb_mrp*)mrp;
+}
+
+void avb_mrp_add_listener(struct avb_mrp *m, struct spa_hook *listener,
+ const struct avb_mrp_events *events, void *data)
+{
+ struct mrp *mrp = (struct mrp*)m;
+ spa_hook_list_append(&mrp->listener_list, listener, events, data);
+}
diff --git a/src/modules/module-avb/mrp.h b/src/modules/module-avb/mrp.h
new file mode 100644
index 0000000..0a05d4b
--- /dev/null
+++ b/src/modules/module-avb/mrp.h
@@ -0,0 +1,181 @@
+/* AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef AVB_MRP_H
+#define AVB_MRP_H
+
+#include "packets.h"
+#include "internal.h"
+
+#define AVB_MRP_PROTOCOL_VERSION 0
+
+struct avb_packet_mrp {
+ struct avb_ethernet_header eth;
+ uint8_t version;
+} __attribute__ ((__packed__));
+
+struct avb_packet_mrp_hdr {
+ uint8_t attribute_type;
+ uint8_t attribute_length;
+} __attribute__ ((__packed__));
+
+struct avb_packet_mrp_vector {
+#if __BYTE_ORDER == __BIG_ENDIAN
+ unsigned lva:3;
+ unsigned nv1:5;
+#elif __BYTE_ORDER == __LITTLE_ENDIAN
+ unsigned nv1:5;
+ unsigned lva:3;
+#endif
+ uint8_t nv2;
+ uint8_t first_value[0];
+} __attribute__ ((__packed__));
+
+#define AVB_MRP_VECTOR_SET_NUM_VALUES(a,v) ((a)->nv1 = ((v) >> 8),(a)->nv2 = (v))
+#define AVB_MRP_VECTOR_GET_NUM_VALUES(a) ((a)->nv1 << 8 | (a)->nv2)
+
+struct avb_packet_mrp_footer {
+ uint16_t end_mark;
+} __attribute__ ((__packed__));
+
+/* applicant states */
+#define AVB_MRP_VO 0 /* Very anxious Observer */
+#define AVB_MRP_VP 1 /* Very anxious Passive */
+#define AVB_MRP_VN 2 /* Very anxious New */
+#define AVB_MRP_AN 3 /* Anxious New */
+#define AVB_MRP_AA 4 /* Anxious Active */
+#define AVB_MRP_QA 5 /* Quiet Active */
+#define AVB_MRP_LA 6 /* Leaving Active */
+#define AVB_MRP_AO 7 /* Anxious Observer */
+#define AVB_MRP_QO 8 /* Quiet Observer */
+#define AVB_MRP_AP 9 /* Anxious Passive */
+#define AVB_MRP_QP 10 /* Quiet Passive */
+#define AVB_MRP_LO 11 /* Leaving Observer */
+
+/* registrar states */
+#define AVB_MRP_IN 16
+#define AVB_MRP_LV 17
+#define AVB_MRP_MT 18
+
+/* events */
+#define AVB_MRP_EVENT_BEGIN 0
+#define AVB_MRP_EVENT_NEW 1
+#define AVB_MRP_EVENT_JOIN 2
+#define AVB_MRP_EVENT_LV 3
+#define AVB_MRP_EVENT_TX 4
+#define AVB_MRP_EVENT_TX_LVA 5
+#define AVB_MRP_EVENT_TX_LVAF 6
+#define AVB_MRP_EVENT_RX_NEW 7
+#define AVB_MRP_EVENT_RX_JOININ 8
+#define AVB_MRP_EVENT_RX_IN 9
+#define AVB_MRP_EVENT_RX_JOINMT 10
+#define AVB_MRP_EVENT_RX_MT 11
+#define AVB_MRP_EVENT_RX_LV 12
+#define AVB_MRP_EVENT_RX_LVA 13
+#define AVB_MRP_EVENT_FLUSH 14
+#define AVB_MRP_EVENT_REDECLARE 15
+#define AVB_MRP_EVENT_PERIODIC 16
+#define AVB_MRP_EVENT_LV_TIMER 17
+#define AVB_MRP_EVENT_LVA_TIMER 18
+
+/* attribute events */
+#define AVB_MRP_ATTRIBUTE_EVENT_NEW 0
+#define AVB_MRP_ATTRIBUTE_EVENT_JOININ 1
+#define AVB_MRP_ATTRIBUTE_EVENT_IN 2
+#define AVB_MRP_ATTRIBUTE_EVENT_JOINMT 3
+#define AVB_MRP_ATTRIBUTE_EVENT_MT 4
+#define AVB_MRP_ATTRIBUTE_EVENT_LV 5
+
+#define AVB_MRP_SEND_NEW 1
+#define AVB_MRP_SEND_JOININ 2
+#define AVB_MRP_SEND_IN 3
+#define AVB_MRP_SEND_JOINMT 4
+#define AVB_MRP_SEND_MT 5
+#define AVB_MRP_SEND_LV 6
+
+#define AVB_MRP_NOTIFY_NEW 1
+#define AVB_MRP_NOTIFY_JOIN 2
+#define AVB_MRP_NOTIFY_LEAVE 3
+
+const char *avb_mrp_notify_name(uint8_t notify);
+const char *avb_mrp_send_name(uint8_t send);
+
+struct avb_mrp_attribute {
+ uint8_t pending_send;
+ void *user_data;
+};
+
+struct avb_mrp_attribute_events {
+#define AVB_VERSION_MRP_ATTRIBUTE_EVENTS 0
+ uint32_t version;
+
+ void (*notify) (void *data, uint64_t now, uint8_t notify);
+};
+
+struct avb_mrp_attribute *avb_mrp_attribute_new(struct avb_mrp *mrp,
+ size_t user_size);
+void avb_mrp_attribute_destroy(struct avb_mrp_attribute *attr);
+
+void avb_mrp_attribute_update_state(struct avb_mrp_attribute *attr, uint64_t now, int event);
+
+void avb_mrp_attribute_rx_event(struct avb_mrp_attribute *attr, uint64_t now, uint8_t event);
+
+void avb_mrp_attribute_begin(struct avb_mrp_attribute *attr, uint64_t now);
+void avb_mrp_attribute_join(struct avb_mrp_attribute *attr, uint64_t now, bool is_new);
+void avb_mrp_attribute_leave(struct avb_mrp_attribute *attr, uint64_t now);
+
+void avb_mrp_attribute_add_listener(struct avb_mrp_attribute *attr, struct spa_hook *listener,
+ const struct avb_mrp_attribute_events *events, void *data);
+
+struct avb_mrp_parse_info {
+#define AVB_VERSION_MRP_PARSE_INFO 0
+ uint32_t version;
+
+ bool (*check_header) (void *data, const void *hdr, size_t *hdr_size, bool *has_params);
+
+ int (*attr_event) (void *data, uint64_t now, uint8_t attribute_type, uint8_t event);
+
+ int (*process) (void *data, uint64_t now, uint8_t attribute_type, const void *value,
+ uint8_t event, uint8_t param, int index);
+};
+
+int avb_mrp_parse_packet(struct avb_mrp *mrp, uint64_t now, const void *pkt, int size,
+ const struct avb_mrp_parse_info *cb, void *data);
+
+struct avb_mrp_events {
+#define AVB_VERSION_MRP_EVENTS 0
+ uint32_t version;
+
+ void (*event) (void *data, uint64_t now, uint8_t event);
+
+ void (*notify) (void *data, uint64_t now, struct avb_mrp_attribute *attr, uint8_t notify);
+};
+
+struct avb_mrp *avb_mrp_new(struct server *server);
+void avb_mrp_destroy(struct avb_mrp *mrp);
+
+void avb_mrp_add_listener(struct avb_mrp *mrp, struct spa_hook *listener,
+ const struct avb_mrp_events *events, void *data);
+
+#endif /* AVB_MRP_H */
diff --git a/src/modules/module-avb/msrp.c b/src/modules/module-avb/msrp.c
new file mode 100644
index 0000000..85d3ff9
--- /dev/null
+++ b/src/modules/module-avb/msrp.c
@@ -0,0 +1,459 @@
+/* AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <unistd.h>
+
+#include <spa/debug/mem.h>
+
+#include <pipewire/pipewire.h>
+
+#include "utils.h"
+#include "msrp.h"
+
+static const uint8_t msrp_mac[6] = AVB_MSRP_MAC;
+
+struct attr {
+ struct avb_msrp_attribute attr;
+ struct msrp *msrp;
+ struct spa_hook listener;
+ struct spa_list link;
+};
+
+struct msrp {
+ struct server *server;
+ struct spa_hook server_listener;
+ struct spa_hook mrp_listener;
+
+ struct spa_source *source;
+
+ struct spa_list attributes;
+};
+
+static void debug_msrp_talker_common(const struct avb_packet_msrp_talker *t)
+{
+ char buf[128];
+ pw_log_info(" stream-id: %s", avb_utils_format_id(buf, sizeof(buf), be64toh(t->stream_id)));
+ pw_log_info(" dest-addr: %s", avb_utils_format_addr(buf, sizeof(buf), t->dest_addr));
+ pw_log_info(" vlan-id: %d", ntohs(t->vlan_id));
+ pw_log_info(" tspec-max-frame-size: %d", ntohs(t->tspec_max_frame_size));
+ pw_log_info(" tspec-max-interval-frames: %d", ntohs(t->tspec_max_interval_frames));
+ pw_log_info(" priority: %d", t->priority);
+ pw_log_info(" rank: %d", t->rank);
+ pw_log_info(" accumulated-latency: %d", ntohl(t->accumulated_latency));
+}
+
+static void debug_msrp_talker(const struct avb_packet_msrp_talker *t)
+{
+ pw_log_info("talker");
+ debug_msrp_talker_common(t);
+}
+
+static void notify_talker(struct msrp *msrp, uint64_t now, struct attr *attr, uint8_t notify)
+{
+ pw_log_info("> notify talker: %s", avb_mrp_notify_name(notify));
+ debug_msrp_talker(&attr->attr.attr.talker);
+}
+
+static int process_talker(struct msrp *msrp, uint64_t now, uint8_t attr_type,
+ const void *m, uint8_t event, uint8_t param, int num)
+{
+ const struct avb_packet_msrp_talker *t = m;
+ struct attr *a;
+ spa_list_for_each(a, &msrp->attributes, link)
+ if (a->attr.type == attr_type &&
+ a->attr.attr.talker.stream_id == t->stream_id) {
+ a->attr.attr.talker = *t;
+ avb_mrp_attribute_rx_event(a->attr.mrp, now, event);
+ }
+ return 0;
+}
+static int encode_talker(struct msrp *msrp, struct attr *a, void *m)
+{
+ struct avb_packet_msrp_msg *msg = m;
+ struct avb_packet_mrp_vector *v;
+ struct avb_packet_msrp_talker *t;
+ struct avb_packet_mrp_footer *f;
+ uint8_t *ev;
+ size_t attr_list_length = sizeof(*v) + sizeof(*t) + sizeof(*f) + 1;
+
+ msg->attribute_type = AVB_MSRP_ATTRIBUTE_TYPE_TALKER_ADVERTISE;
+ msg->attribute_length = sizeof(*t);
+ msg->attribute_list_length = htons(attr_list_length);
+
+ v = (struct avb_packet_mrp_vector *)msg->attribute_list;
+ v->lva = 0;
+ AVB_MRP_VECTOR_SET_NUM_VALUES(v, 1);
+
+ t = (struct avb_packet_msrp_talker *)v->first_value;
+ *t = a->attr.attr.talker;
+
+ ev = SPA_PTROFF(t, sizeof(*t), uint8_t);
+ *ev = a->attr.mrp->pending_send * 6 * 6;
+
+ f = SPA_PTROFF(ev, sizeof(*ev), struct avb_packet_mrp_footer);
+ f->end_mark = 0;
+
+ return attr_list_length + sizeof(*msg);
+}
+
+
+static void debug_msrp_talker_fail(const struct avb_packet_msrp_talker_fail *t)
+{
+ char buf[128];
+ pw_log_info("talker fail");
+ debug_msrp_talker_common(&t->talker);
+ pw_log_info(" bridge-id: %s", avb_utils_format_id(buf, sizeof(buf), be64toh(t->bridge_id)));
+ pw_log_info(" failure-code: %d", t->failure_code);
+}
+
+static int process_talker_fail(struct msrp *msrp, uint64_t now, uint8_t attr_type,
+ const void *m, uint8_t event, uint8_t param, int num)
+{
+ const struct avb_packet_msrp_talker_fail *t = m;
+ struct attr *a;
+
+ debug_msrp_talker_fail(t);
+
+ spa_list_for_each(a, &msrp->attributes, link)
+ if (a->attr.type == attr_type &&
+ a->attr.attr.talker_fail.talker.stream_id == t->talker.stream_id)
+ avb_mrp_attribute_rx_event(a->attr.mrp, now, event);
+ return 0;
+}
+
+static void debug_msrp_listener(const struct avb_packet_msrp_listener *l, uint8_t param)
+{
+ char buf[128];
+ pw_log_info("listener");
+ pw_log_info(" %s", avb_utils_format_id(buf, sizeof(buf), be64toh(l->stream_id)));
+ pw_log_info(" %d", param);
+}
+
+static void notify_listener(struct msrp *msrp, uint64_t now, struct attr *attr, uint8_t notify)
+{
+ pw_log_info("> notify listener: %s", avb_mrp_notify_name(notify));
+ debug_msrp_listener(&attr->attr.attr.listener, attr->attr.param);
+}
+
+static int process_listener(struct msrp *msrp, uint64_t now, uint8_t attr_type,
+ const void *m, uint8_t event, uint8_t param, int num)
+{
+ const struct avb_packet_msrp_listener *l = m;
+ struct attr *a;
+ spa_list_for_each(a, &msrp->attributes, link)
+ if (a->attr.type == attr_type &&
+ a->attr.attr.listener.stream_id == l->stream_id)
+ avb_mrp_attribute_rx_event(a->attr.mrp, now, event);
+ return 0;
+}
+static int encode_listener(struct msrp *msrp, struct attr *a, void *m)
+{
+ struct avb_packet_msrp_msg *msg = m;
+ struct avb_packet_mrp_vector *v;
+ struct avb_packet_msrp_listener *l;
+ struct avb_packet_mrp_footer *f;
+ uint8_t *ev;
+ size_t attr_list_length = sizeof(*v) + sizeof(*l) + sizeof(*f) + 1 + 1;
+
+ msg->attribute_type = AVB_MSRP_ATTRIBUTE_TYPE_LISTENER;
+ msg->attribute_length = sizeof(*l);
+ msg->attribute_list_length = htons(attr_list_length);
+
+ v = (struct avb_packet_mrp_vector *)msg->attribute_list;
+ v->lva = 0;
+ AVB_MRP_VECTOR_SET_NUM_VALUES(v, 1);
+
+ l = (struct avb_packet_msrp_listener *)v->first_value;
+ *l = a->attr.attr.listener;
+
+ ev = SPA_PTROFF(l, sizeof(*l), uint8_t);
+ *ev = a->attr.mrp->pending_send * 6 * 6;
+
+ ev = SPA_PTROFF(ev, sizeof(*ev), uint8_t);
+ *ev = a->attr.param * 4 * 4 * 4;
+
+ f = SPA_PTROFF(ev, sizeof(*ev), struct avb_packet_mrp_footer);
+ f->end_mark = 0;
+
+ return attr_list_length + sizeof(*msg);
+}
+
+static void debug_msrp_domain(const struct avb_packet_msrp_domain *d)
+{
+ pw_log_info("domain");
+ pw_log_info(" id: %d", d->sr_class_id);
+ pw_log_info(" prio: %d", d->sr_class_priority);
+ pw_log_info(" vid: %d", ntohs(d->sr_class_vid));
+}
+
+static void notify_domain(struct msrp *msrp, uint64_t now, struct attr *attr, uint8_t notify)
+{
+ pw_log_info("> notify domain: %s", avb_mrp_notify_name(notify));
+ debug_msrp_domain(&attr->attr.attr.domain);
+}
+
+static int process_domain(struct msrp *msrp, uint64_t now, uint8_t attr_type,
+ const void *m, uint8_t event, uint8_t param, int num)
+{
+ struct attr *a;
+ spa_list_for_each(a, &msrp->attributes, link)
+ if (a->attr.type == attr_type)
+ avb_mrp_attribute_rx_event(a->attr.mrp, now, event);
+ return 0;
+}
+
+static int encode_domain(struct msrp *msrp, struct attr *a, void *m)
+{
+ struct avb_packet_msrp_msg *msg = m;
+ struct avb_packet_mrp_vector *v;
+ struct avb_packet_msrp_domain *d;
+ struct avb_packet_mrp_footer *f;
+ uint8_t *ev;
+ size_t attr_list_length = sizeof(*v) + sizeof(*d) + sizeof(*f) + 1;
+
+ msg->attribute_type = AVB_MSRP_ATTRIBUTE_TYPE_DOMAIN;
+ msg->attribute_length = sizeof(*d);
+ msg->attribute_list_length = htons(attr_list_length);
+
+ v = (struct avb_packet_mrp_vector *)msg->attribute_list;
+ v->lva = 0;
+ AVB_MRP_VECTOR_SET_NUM_VALUES(v, 1);
+
+ d = (struct avb_packet_msrp_domain *)v->first_value;
+ *d = a->attr.attr.domain;
+
+ ev = SPA_PTROFF(d, sizeof(*d), uint8_t);
+ *ev = a->attr.mrp->pending_send * 36;
+
+ f = SPA_PTROFF(ev, sizeof(*ev), struct avb_packet_mrp_footer);
+ f->end_mark = 0;
+
+ return attr_list_length + sizeof(*msg);
+}
+
+static const struct {
+ const char *name;
+ int (*process) (struct msrp *msrp, uint64_t now, uint8_t attr_type,
+ const void *m, uint8_t event, uint8_t param, int num);
+ int (*encode) (struct msrp *msrp, struct attr *attr, void *m);
+ void (*notify) (struct msrp *msrp, uint64_t now, struct attr *attr, uint8_t notify);
+} dispatch[] = {
+ [AVB_MSRP_ATTRIBUTE_TYPE_TALKER_ADVERTISE] = { "talker", process_talker, encode_talker, notify_talker, },
+ [AVB_MSRP_ATTRIBUTE_TYPE_TALKER_FAILED] = { "talker-fail", process_talker_fail, NULL, NULL },
+ [AVB_MSRP_ATTRIBUTE_TYPE_LISTENER] = { "listener", process_listener, encode_listener, notify_listener },
+ [AVB_MSRP_ATTRIBUTE_TYPE_DOMAIN] = { "domain", process_domain, encode_domain, notify_domain, },
+};
+
+static bool msrp_check_header(void *data, const void *hdr, size_t *hdr_size, bool *has_params)
+{
+ const struct avb_packet_msrp_msg *msg = hdr;
+ uint8_t attr_type = msg->attribute_type;
+
+ if (!AVB_MSRP_ATTRIBUTE_TYPE_VALID(attr_type))
+ return false;
+
+ *hdr_size = sizeof(*msg);
+ *has_params = attr_type == AVB_MSRP_ATTRIBUTE_TYPE_LISTENER;
+ return true;
+}
+
+static int msrp_attr_event(void *data, uint64_t now, uint8_t attribute_type, uint8_t event)
+{
+ struct msrp *msrp = data;
+ struct attr *a;
+ spa_list_for_each(a, &msrp->attributes, link)
+ if (a->attr.type == attribute_type)
+ avb_mrp_attribute_update_state(a->attr.mrp, now, event);
+ return 0;
+}
+
+static int msrp_process(void *data, uint64_t now, uint8_t attribute_type, const void *value,
+ uint8_t event, uint8_t param, int index)
+{
+ struct msrp *msrp = data;
+ return dispatch[attribute_type].process(msrp, now,
+ attribute_type, value, event, param, index);
+}
+
+static const struct avb_mrp_parse_info info = {
+ AVB_VERSION_MRP_PARSE_INFO,
+ .check_header = msrp_check_header,
+ .attr_event = msrp_attr_event,
+ .process = msrp_process,
+};
+
+
+static int msrp_message(struct msrp *msrp, uint64_t now, const void *message, int len)
+{
+ return avb_mrp_parse_packet(msrp->server->mrp,
+ now, message, len, &info, msrp);
+}
+static void on_socket_data(void *data, int fd, uint32_t mask)
+{
+ struct msrp *msrp = data;
+ struct timespec now;
+
+ if (mask & SPA_IO_IN) {
+ int len;
+ uint8_t buffer[2048];
+
+ len = recv(fd, buffer, sizeof(buffer), 0);
+
+ if (len < 0) {
+ pw_log_warn("got recv error: %m");
+ }
+ else if (len < (int)sizeof(struct avb_packet_header)) {
+ pw_log_warn("short packet received (%d < %d)", len,
+ (int)sizeof(struct avb_packet_header));
+ } else {
+ clock_gettime(CLOCK_REALTIME, &now);
+ msrp_message(msrp, SPA_TIMESPEC_TO_NSEC(&now), buffer, len);
+ }
+ }
+}
+
+static void msrp_destroy(void *data)
+{
+ struct msrp *msrp = data;
+ spa_hook_remove(&msrp->server_listener);
+ pw_loop_destroy_source(msrp->server->impl->loop, msrp->source);
+ free(msrp);
+}
+
+static const struct server_events server_events = {
+ AVB_VERSION_SERVER_EVENTS,
+ .destroy = msrp_destroy,
+};
+
+static void msrp_notify(void *data, uint64_t now, uint8_t notify)
+{
+ struct attr *a = data;
+ struct msrp *msrp = a->msrp;
+ return dispatch[a->attr.type].notify(msrp, now, a, notify);
+}
+
+static const struct avb_mrp_attribute_events mrp_attr_events = {
+ AVB_VERSION_MRP_ATTRIBUTE_EVENTS,
+ .notify = msrp_notify,
+};
+
+struct avb_msrp_attribute *avb_msrp_attribute_new(struct avb_msrp *m,
+ uint8_t type)
+{
+ struct msrp *msrp = (struct msrp*)m;
+ struct avb_mrp_attribute *attr;
+ struct attr *a;
+
+ attr = avb_mrp_attribute_new(msrp->server->mrp, sizeof(struct attr));
+
+ a = attr->user_data;
+ a->msrp = msrp;
+ a->attr.mrp = attr;
+ a->attr.type = type;
+ spa_list_append(&msrp->attributes, &a->link);
+ avb_mrp_attribute_add_listener(attr, &a->listener, &mrp_attr_events, a);
+
+ return &a->attr;
+}
+
+static void msrp_event(void *data, uint64_t now, uint8_t event)
+{
+ struct msrp *msrp = data;
+ uint8_t buffer[2048];
+ struct avb_packet_mrp *p = (struct avb_packet_mrp*)buffer;
+ struct avb_packet_mrp_footer *f;
+ void *msg = SPA_PTROFF(buffer, sizeof(*p), void);
+ struct attr *a;
+ int len, count = 0;
+ size_t total = sizeof(*p) + 2;
+
+ p->version = AVB_MRP_PROTOCOL_VERSION;
+
+ spa_list_for_each(a, &msrp->attributes, link) {
+ if (!a->attr.mrp->pending_send)
+ continue;
+ if (dispatch[a->attr.type].encode == NULL)
+ continue;
+
+ pw_log_debug("send %s %s", dispatch[a->attr.type].name,
+ avb_mrp_send_name(a->attr.mrp->pending_send));
+
+ len = dispatch[a->attr.type].encode(msrp, a, msg);
+ if (len < 0)
+ break;
+
+ count++;
+ msg = SPA_PTROFF(msg, len, void);
+ total += len;
+ }
+ f = (struct avb_packet_mrp_footer *)msg;
+ f->end_mark = 0;
+
+ if (count > 0)
+ avb_server_send_packet(msrp->server, msrp_mac, AVB_MSRP_ETH,
+ buffer, total);
+}
+
+static const struct avb_mrp_events mrp_events = {
+ AVB_VERSION_MRP_EVENTS,
+ .event = msrp_event,
+};
+
+struct avb_msrp *avb_msrp_register(struct server *server)
+{
+ struct msrp *msrp;
+ int fd, res;
+
+ fd = avb_server_make_socket(server, AVB_MSRP_ETH, msrp_mac);
+ if (fd < 0) {
+ errno = -fd;
+ return NULL;
+ }
+ msrp = calloc(1, sizeof(*msrp));
+ if (msrp == NULL) {
+ res = -errno;
+ goto error_close;
+ }
+
+ msrp->server = server;
+ spa_list_init(&msrp->attributes);
+
+ msrp->source = pw_loop_add_io(server->impl->loop, fd, SPA_IO_IN, true, on_socket_data, msrp);
+ if (msrp->source == NULL) {
+ res = -errno;
+ pw_log_error("msrp %p: can't create msrp source: %m", msrp);
+ goto error_no_source;
+ }
+ avdecc_server_add_listener(server, &msrp->server_listener, &server_events, msrp);
+ avb_mrp_add_listener(server->mrp, &msrp->mrp_listener, &mrp_events, msrp);
+
+ return (struct avb_msrp*)msrp;
+
+error_no_source:
+ free(msrp);
+error_close:
+ close(fd);
+ errno = -res;
+ return NULL;
+}
diff --git a/src/modules/module-avb/msrp.h b/src/modules/module-avb/msrp.h
new file mode 100644
index 0000000..0922e6b
--- /dev/null
+++ b/src/modules/module-avb/msrp.h
@@ -0,0 +1,134 @@
+/* AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef AVB_MSRP_H
+#define AVB_MSRP_H
+
+#include "internal.h"
+#include "mrp.h"
+
+#define AVB_MSRP_ETH 0x22ea
+#define AVB_MSRP_MAC { 0x01, 0x80, 0xc2, 0x00, 0x00, 0xe };
+
+#define AVB_MSRP_ATTRIBUTE_TYPE_TALKER_ADVERTISE 1
+#define AVB_MSRP_ATTRIBUTE_TYPE_TALKER_FAILED 2
+#define AVB_MSRP_ATTRIBUTE_TYPE_LISTENER 3
+#define AVB_MSRP_ATTRIBUTE_TYPE_DOMAIN 4
+#define AVB_MSRP_ATTRIBUTE_TYPE_VALID(t) ((t)>=1 && (t)<=4)
+
+struct avb_packet_msrp_msg {
+ uint8_t attribute_type;
+ uint8_t attribute_length;
+ uint16_t attribute_list_length;
+ uint8_t attribute_list[0];
+} __attribute__ ((__packed__));
+
+#define AVB_MSRP_TSPEC_MAX_INTERVAL_FRAMES_DEFAULT 1
+#define AVB_MSRP_RANK_DEFAULT 1
+#define AVB_MSRP_PRIORITY_DEFAULT 3
+
+struct avb_packet_msrp_talker {
+ uint64_t stream_id;
+ uint8_t dest_addr[6];
+ uint16_t vlan_id;
+ uint16_t tspec_max_frame_size;
+ uint16_t tspec_max_interval_frames;
+#if __BYTE_ORDER == __BIG_ENDIAN
+ unsigned priority:3;
+ unsigned rank:1;
+ unsigned reserved:4;
+#elif __BYTE_ORDER == __LITTLE_ENDIAN
+ unsigned reserved:4;
+ unsigned rank:1;
+ unsigned priority:3;
+#endif
+ uint32_t accumulated_latency;
+} __attribute__ ((__packed__));
+
+/* failure codes */
+#define AVB_MRP_FAIL_BANDWIDTH 1
+#define AVB_MRP_FAIL_BRIDGE 2
+#define AVB_MRP_FAIL_TC_BANDWIDTH 3
+#define AVB_MRP_FAIL_ID_BUSY 4
+#define AVB_MRP_FAIL_DSTADDR_BUSY 5
+#define AVB_MRP_FAIL_PREEMPTED 6
+#define AVB_MRP_FAIL_LATENCY_CHNG 7
+#define AVB_MRP_FAIL_PORT_NOT_AVB 8
+#define AVB_MRP_FAIL_DSTADDR_FULL 9
+#define AVB_MRP_FAIL_AVB_MRP_RESOURCE 10
+#define AVB_MRP_FAIL_MMRP_RESOURCE 11
+#define AVB_MRP_FAIL_DSTADDR_FAIL 12
+#define AVB_MRP_FAIL_PRIO_NOT_SR 13
+#define AVB_MRP_FAIL_FRAME_SIZE 14
+#define AVB_MRP_FAIL_FANIN_EXCEED 15
+#define AVB_MRP_FAIL_STREAM_CHANGE 16
+#define AVB_MRP_FAIL_VLAN_BLOCKED 17
+#define AVB_MRP_FAIL_VLAN_DISABLED 18
+#define AVB_MRP_FAIL_SR_PRIO_ERR 19
+
+struct avb_packet_msrp_talker_fail {
+ struct avb_packet_msrp_talker talker;
+ uint64_t bridge_id;
+ uint8_t failure_code;
+} __attribute__ ((__packed__));
+
+struct avb_packet_msrp_listener {
+ uint64_t stream_id;
+} __attribute__ ((__packed__));
+
+/* domain discovery */
+#define AVB_MSRP_CLASS_ID_DEFAULT 6
+#define AVB_DEFAULT_VLAN 2
+
+struct avb_packet_msrp_domain {
+ uint8_t sr_class_id;
+ uint8_t sr_class_priority;
+ uint16_t sr_class_vid;
+} __attribute__ ((__packed__));
+
+#define AVB_MSRP_LISTENER_PARAM_IGNORE 0
+#define AVB_MSRP_LISTENER_PARAM_ASKING_FAILED 1
+#define AVB_MSRP_LISTENER_PARAM_READY 2
+#define AVB_MSRP_LISTENER_PARAM_READY_FAILED 3
+
+struct avb_msrp_attribute {
+ struct avb_mrp_attribute *mrp;
+ uint8_t type;
+ uint8_t param;
+ union {
+ struct avb_packet_msrp_talker talker;
+ struct avb_packet_msrp_talker_fail talker_fail;
+ struct avb_packet_msrp_listener listener;
+ struct avb_packet_msrp_domain domain;
+ } attr;
+};
+
+struct avb_msrp;
+
+struct avb_msrp_attribute *avb_msrp_attribute_new(struct avb_msrp *msrp,
+ uint8_t type);
+
+struct avb_msrp *avb_msrp_register(struct server *server);
+
+#endif /* AVB_MSRP_H */
diff --git a/src/modules/module-avb/mvrp.c b/src/modules/module-avb/mvrp.c
new file mode 100644
index 0000000..2f5f6ea
--- /dev/null
+++ b/src/modules/module-avb/mvrp.c
@@ -0,0 +1,297 @@
+/* AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <unistd.h>
+
+#include <pipewire/pipewire.h>
+
+#include "mvrp.h"
+
+static const uint8_t mvrp_mac[6] = AVB_MVRP_MAC;
+
+struct attr {
+ struct avb_mvrp_attribute attr;
+ struct spa_hook listener;
+ struct spa_list link;
+ struct mvrp *mvrp;
+};
+
+struct mvrp {
+ struct server *server;
+ struct spa_hook server_listener;
+ struct spa_hook mrp_listener;
+
+ struct spa_source *source;
+
+ struct spa_list attributes;
+};
+
+static bool mvrp_check_header(void *data, const void *hdr, size_t *hdr_size, bool *has_params)
+{
+ const struct avb_packet_mvrp_msg *msg = hdr;
+ uint8_t attr_type = msg->attribute_type;
+
+ if (!AVB_MVRP_ATTRIBUTE_TYPE_VALID(attr_type))
+ return false;
+
+ *hdr_size = sizeof(*msg);
+ *has_params = false;
+ return true;
+}
+
+static int mvrp_attr_event(void *data, uint64_t now, uint8_t attribute_type, uint8_t event)
+{
+ struct mvrp *mvrp = data;
+ struct attr *a;
+ spa_list_for_each(a, &mvrp->attributes, link)
+ if (a->attr.type == attribute_type)
+ avb_mrp_attribute_rx_event(a->attr.mrp, now, event);
+ return 0;
+}
+
+static void debug_vid(const struct avb_packet_mvrp_vid *t)
+{
+ pw_log_info("vid");
+ pw_log_info(" %d", ntohs(t->vlan));
+}
+
+static int process_vid(struct mvrp *mvrp, uint64_t now, uint8_t attr_type,
+ const void *m, uint8_t event, uint8_t param, int num)
+{
+ return mvrp_attr_event(mvrp, now, attr_type, event);
+}
+
+static int encode_vid(struct mvrp *mvrp, struct attr *a, void *m)
+{
+ struct avb_packet_mvrp_msg *msg = m;
+ struct avb_packet_mrp_vector *v;
+ struct avb_packet_mvrp_vid *d;
+ struct avb_packet_mrp_footer *f;
+ uint8_t *ev;
+ size_t attr_list_length = sizeof(*v) + sizeof(*d) + sizeof(*f) + 1;
+
+ msg->attribute_type = AVB_MVRP_ATTRIBUTE_TYPE_VID;
+ msg->attribute_length = sizeof(*d);
+
+ v = (struct avb_packet_mrp_vector *)msg->attribute_list;
+ v->lva = 0;
+ AVB_MRP_VECTOR_SET_NUM_VALUES(v, 1);
+
+ d = (struct avb_packet_mvrp_vid *)v->first_value;
+ *d = a->attr.attr.vid;
+
+ ev = SPA_PTROFF(d, sizeof(*d), uint8_t);
+ *ev = a->attr.mrp->pending_send * 36;
+
+ f = SPA_PTROFF(ev, sizeof(*ev), struct avb_packet_mrp_footer);
+ f->end_mark = 0;
+
+ return attr_list_length + sizeof(*msg);
+}
+
+static void notify_vid(struct mvrp *mvrp, uint64_t now, struct attr *attr, uint8_t notify)
+{
+ pw_log_info("> notify vid: %s", avb_mrp_notify_name(notify));
+ debug_vid(&attr->attr.attr.vid);
+}
+
+static const struct {
+ const char *name;
+ int (*process) (struct mvrp *mvrp, uint64_t now, uint8_t attr_type,
+ const void *m, uint8_t event, uint8_t param, int num);
+ int (*encode) (struct mvrp *mvrp, struct attr *attr, void *m);
+ void (*notify) (struct mvrp *mvrp, uint64_t now, struct attr *attr, uint8_t notify);
+} dispatch[] = {
+ [AVB_MVRP_ATTRIBUTE_TYPE_VID] = { "vid", process_vid, encode_vid, notify_vid },
+};
+
+static int mvrp_process(void *data, uint64_t now, uint8_t attribute_type, const void *value,
+ uint8_t event, uint8_t param, int index)
+{
+ struct mvrp *mvrp = data;
+ return dispatch[attribute_type].process(mvrp, now,
+ attribute_type, value, event, param, index);
+}
+
+static const struct avb_mrp_parse_info info = {
+ AVB_VERSION_MRP_PARSE_INFO,
+ .check_header = mvrp_check_header,
+ .attr_event = mvrp_attr_event,
+ .process = mvrp_process,
+};
+
+static int mvrp_message(struct mvrp *mvrp, uint64_t now, const void *message, int len)
+{
+ pw_log_debug("MVRP");
+ return avb_mrp_parse_packet(mvrp->server->mrp,
+ now, message, len, &info, mvrp);
+}
+
+static void on_socket_data(void *data, int fd, uint32_t mask)
+{
+ struct mvrp *mvrp = data;
+ struct timespec now;
+
+ if (mask & SPA_IO_IN) {
+ int len;
+ uint8_t buffer[2048];
+
+ len = recv(fd, buffer, sizeof(buffer), 0);
+
+ if (len < 0) {
+ pw_log_warn("got recv error: %m");
+ }
+ else if (len < (int)sizeof(struct avb_packet_header)) {
+ pw_log_warn("short packet received (%d < %d)", len,
+ (int)sizeof(struct avb_packet_header));
+ } else {
+ clock_gettime(CLOCK_REALTIME, &now);
+ mvrp_message(mvrp, SPA_TIMESPEC_TO_NSEC(&now), buffer, len);
+ }
+ }
+}
+
+static void mvrp_destroy(void *data)
+{
+ struct mvrp *mvrp = data;
+ spa_hook_remove(&mvrp->server_listener);
+ pw_loop_destroy_source(mvrp->server->impl->loop, mvrp->source);
+ free(mvrp);
+}
+
+static const struct server_events server_events = {
+ AVB_VERSION_SERVER_EVENTS,
+ .destroy = mvrp_destroy,
+};
+
+static void mvrp_notify(void *data, uint64_t now, uint8_t notify)
+{
+ struct attr *a = data;
+ struct mvrp *mvrp = a->mvrp;
+ return dispatch[a->attr.type].notify(mvrp, now, a, notify);
+}
+
+static const struct avb_mrp_attribute_events mrp_attr_events = {
+ AVB_VERSION_MRP_ATTRIBUTE_EVENTS,
+ .notify = mvrp_notify,
+};
+
+struct avb_mvrp_attribute *avb_mvrp_attribute_new(struct avb_mvrp *m,
+ uint8_t type)
+{
+ struct mvrp *mvrp = (struct mvrp*)m;
+ struct avb_mrp_attribute *attr;
+ struct attr *a;
+
+ attr = avb_mrp_attribute_new(mvrp->server->mrp, sizeof(struct attr));
+
+ a = attr->user_data;
+ a->attr.mrp = attr;
+ a->attr.type = type;
+ spa_list_append(&mvrp->attributes, &a->link);
+ avb_mrp_attribute_add_listener(attr, &a->listener, &mrp_attr_events, a);
+
+ return &a->attr;
+}
+
+static void mvrp_event(void *data, uint64_t now, uint8_t event)
+{
+ struct mvrp *mvrp = data;
+ uint8_t buffer[2048];
+ struct avb_packet_mrp *p = (struct avb_packet_mrp*)buffer;
+ struct avb_packet_mrp_footer *f;
+ void *msg = SPA_PTROFF(buffer, sizeof(*p), void);
+ struct attr *a;
+ int len, count = 0;
+ size_t total = sizeof(*p) + 2;
+
+ p->version = AVB_MRP_PROTOCOL_VERSION;
+
+ spa_list_for_each(a, &mvrp->attributes, link) {
+ if (!a->attr.mrp->pending_send)
+ continue;
+ if (dispatch[a->attr.type].encode == NULL)
+ continue;
+
+ pw_log_debug("send %s %s", dispatch[a->attr.type].name,
+ avb_mrp_send_name(a->attr.mrp->pending_send));
+
+ len = dispatch[a->attr.type].encode(mvrp, a, msg);
+ if (len < 0)
+ break;
+
+ count++;
+ msg = SPA_PTROFF(msg, len, void);
+ total += len;
+ }
+ f = (struct avb_packet_mrp_footer *)msg;
+ f->end_mark = 0;
+
+ if (count > 0)
+ avb_server_send_packet(mvrp->server, mvrp_mac, AVB_MVRP_ETH,
+ buffer, total);
+}
+
+static const struct avb_mrp_events mrp_events = {
+ AVB_VERSION_MRP_EVENTS,
+ .event = mvrp_event,
+};
+
+struct avb_mvrp *avb_mvrp_register(struct server *server)
+{
+ struct mvrp *mvrp;
+ int fd, res;
+
+ fd = avb_server_make_socket(server, AVB_MVRP_ETH, mvrp_mac);
+ if (fd < 0) {
+ errno = -fd;
+ return NULL;
+ }
+ mvrp = calloc(1, sizeof(*mvrp));
+ if (mvrp == NULL) {
+ res = -errno;
+ goto error_close;
+ }
+
+ mvrp->server = server;
+ spa_list_init(&mvrp->attributes);
+
+ mvrp->source = pw_loop_add_io(server->impl->loop, fd, SPA_IO_IN, true, on_socket_data, mvrp);
+ if (mvrp->source == NULL) {
+ res = -errno;
+ pw_log_error("mvrp %p: can't create mvrp source: %m", mvrp);
+ goto error_no_source;
+ }
+ avdecc_server_add_listener(server, &mvrp->server_listener, &server_events, mvrp);
+ avb_mrp_add_listener(server->mrp, &mvrp->mrp_listener, &mrp_events, mvrp);
+
+ return (struct avb_mvrp*)mvrp;
+
+error_no_source:
+ free(mvrp);
+error_close:
+ close(fd);
+ errno = -res;
+ return NULL;
+}
diff --git a/src/modules/module-avb/mvrp.h b/src/modules/module-avb/mvrp.h
new file mode 100644
index 0000000..da3d5dc
--- /dev/null
+++ b/src/modules/module-avb/mvrp.h
@@ -0,0 +1,62 @@
+/* AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef AVB_MVRP_H
+#define AVB_MVRP_H
+
+#include "mrp.h"
+#include "internal.h"
+
+#define AVB_MVRP_ETH 0x88f5
+#define AVB_MVRP_MAC { 0x01, 0x80, 0xc2, 0x00, 0x00, 0x21 };
+
+struct avb_packet_mvrp_msg {
+ uint8_t attribute_type;
+ uint8_t attribute_length;
+ uint8_t attribute_list[0];
+} __attribute__ ((__packed__));
+
+#define AVB_MVRP_ATTRIBUTE_TYPE_VID 1
+#define AVB_MVRP_ATTRIBUTE_TYPE_VALID(t) ((t)==1)
+
+struct avb_packet_mvrp_vid {
+ uint16_t vlan;
+} __attribute__ ((__packed__));
+
+struct avb_mvrp;
+
+struct avb_mvrp_attribute {
+ struct avb_mrp_attribute *mrp;
+ uint8_t type;
+ union {
+ struct avb_packet_mvrp_vid vid;
+ } attr;
+};
+
+struct avb_mvrp_attribute *avb_mvrp_attribute_new(struct avb_mvrp *mvrp,
+ uint8_t type);
+
+struct avb_mvrp *avb_mvrp_register(struct server *server);
+
+#endif /* AVB_MVRP_H */
diff --git a/src/modules/module-avb/packets.h b/src/modules/module-avb/packets.h
new file mode 100644
index 0000000..f35738a
--- /dev/null
+++ b/src/modules/module-avb/packets.h
@@ -0,0 +1,101 @@
+/* Spa AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef AVB_PACKETS_H
+#define AVB_PACKETS_H
+
+#include <arpa/inet.h>
+
+#define AVB_SUBTYPE_61883_IIDC 0x00
+#define AVB_SUBTYPE_MMA_STREAM 0x01
+#define AVB_SUBTYPE_AAF 0x02
+#define AVB_SUBTYPE_CVF 0x03
+#define AVB_SUBTYPE_CRF 0x04
+#define AVB_SUBTYPE_TSCF 0x05
+#define AVB_SUBTYPE_SVF 0x06
+#define AVB_SUBTYPE_RVF 0x07
+#define AVB_SUBTYPE_AEF_CONTINUOUS 0x6E
+#define AVB_SUBTYPE_VSF_STREAM 0x6F
+#define AVB_SUBTYPE_EF_STREAM 0x7F
+#define AVB_SUBTYPE_NTSCF 0x82
+#define AVB_SUBTYPE_ESCF 0xEC
+#define AVB_SUBTYPE_EECF 0xED
+#define AVB_SUBTYPE_AEF_DISCRETE 0xEE
+#define AVB_SUBTYPE_ADP 0xFA
+#define AVB_SUBTYPE_AECP 0xFB
+#define AVB_SUBTYPE_ACMP 0xFC
+#define AVB_SUBTYPE_MAAP 0xFE
+#define AVB_SUBTYPE_EF_CONTROL 0xFF
+
+struct avb_ethernet_header {
+ uint8_t dest[6];
+ uint8_t src[6];
+ uint16_t type;
+} __attribute__ ((__packed__));
+
+struct avb_frame_header {
+ uint8_t dest[6];
+ uint8_t src[6];
+ uint16_t type; /* 802.1Q Virtual Lan 0x8100 */
+ uint16_t prio_cfi_id;
+ uint16_t etype;
+} __attribute__ ((__packed__));
+
+struct avb_packet_header {
+ uint8_t subtype;
+#if __BYTE_ORDER == __BIG_ENDIAN
+ unsigned sv:1; /* stream_id valid */
+ unsigned version:3;
+ unsigned subtype_data1:4;
+
+ unsigned subtype_data2:5;
+ unsigned len1:3;
+#elif __BYTE_ORDER == __LITTLE_ENDIAN
+ unsigned subtype_data1:4;
+ unsigned version:3;
+ unsigned sv:1;
+
+ unsigned len1:3;
+ unsigned subtype_data2:5;
+#elif
+#error "Unknown byte order"
+#endif
+ uint8_t len2:8;
+} __attribute__ ((__packed__));
+
+#define AVB_PACKET_SET_SUBTYPE(p,v) ((p)->subtype = (v))
+#define AVB_PACKET_SET_SV(p,v) ((p)->sv = (v))
+#define AVB_PACKET_SET_VERSION(p,v) ((p)->version = (v))
+#define AVB_PACKET_SET_SUB1(p,v) ((p)->subtype_data1 = (v))
+#define AVB_PACKET_SET_SUB2(p,v) ((p)->subtype_data2 = (v))
+#define AVB_PACKET_SET_LENGTH(p,v) ((p)->len1 = ((v) >> 8),(p)->len2 = (v))
+
+#define AVB_PACKET_GET_SUBTYPE(p) ((p)->subtype)
+#define AVB_PACKET_GET_SV(p) ((p)->sv)
+#define AVB_PACKET_GET_VERSION(p) ((p)->version)
+#define AVB_PACKET_GET_SUB1(p) ((p)->subtype_data1)
+#define AVB_PACKET_GET_SUB2(p) ((p)->subtype_data2)
+#define AVB_PACKET_GET_LENGTH(p) ((p)->len1 << 8 | (p)->len2)
+
+#endif /* AVB_PACKETS_H */
diff --git a/src/modules/module-avb/srp.c b/src/modules/module-avb/srp.c
new file mode 100644
index 0000000..89d75f1
--- /dev/null
+++ b/src/modules/module-avb/srp.c
@@ -0,0 +1,59 @@
+/* AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <pipewire/pipewire.h>
+
+#include "srp.h"
+
+struct srp {
+ struct server *server;
+ struct spa_hook server_listener;
+};
+
+static void srp_destroy(void *data)
+{
+ struct srp *srp = data;
+ spa_hook_remove(&srp->server_listener);
+ free(srp);
+}
+
+static const struct server_events server_events = {
+ AVB_VERSION_SERVER_EVENTS,
+ .destroy = srp_destroy,
+};
+
+int avb_srp_register(struct server *server)
+{
+ struct srp *srp;
+
+ srp = calloc(1, sizeof(*srp));
+ if (srp == NULL)
+ return -errno;
+
+ srp->server = server;
+
+ avdecc_server_add_listener(server, &srp->server_listener, &server_events, srp);
+
+ return 0;
+}
diff --git a/src/modules/module-avb/srp.h b/src/modules/module-avb/srp.h
new file mode 100644
index 0000000..853321f
--- /dev/null
+++ b/src/modules/module-avb/srp.h
@@ -0,0 +1,32 @@
+/* AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef AVB_SRP_H
+#define AVB_SRP_H
+
+#include "internal.h"
+
+int avb_srp_register(struct server *server);
+
+#endif /* AVB_SRP_H */
diff --git a/src/modules/module-avb/stream.c b/src/modules/module-avb/stream.c
new file mode 100644
index 0000000..0f5b148
--- /dev/null
+++ b/src/modules/module-avb/stream.c
@@ -0,0 +1,589 @@
+/* AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <unistd.h>
+#include <linux/if_ether.h>
+#include <linux/if_packet.h>
+#include <linux/net_tstamp.h>
+#include <net/if.h>
+#include <sys/ioctl.h>
+
+#include <spa/debug/mem.h>
+#include <spa/pod/builder.h>
+#include <spa/param/audio/format-utils.h>
+
+#include "iec61883.h"
+#include "stream.h"
+#include "utils.h"
+#include "aecp-aem-descriptors.h"
+
+static void on_stream_destroy(void *d)
+{
+ struct stream *stream = d;
+ spa_hook_remove(&stream->stream_listener);
+ stream->stream = NULL;
+}
+
+static void on_source_stream_process(void *data)
+{
+ struct stream *stream = data;
+ struct pw_buffer *buf;
+ struct spa_data *d;
+ uint32_t index, n_bytes;
+ int32_t avail, wanted;
+
+ if ((buf = pw_stream_dequeue_buffer(stream->stream)) == NULL) {
+ pw_log_debug("out of buffers: %m");
+ return;
+ }
+
+ d = buf->buffer->datas;
+
+ wanted = buf->requested ? buf->requested * stream->stride : d[0].maxsize;
+
+ n_bytes = SPA_MIN(d[0].maxsize, (uint32_t)wanted);
+
+ avail = spa_ringbuffer_get_read_index(&stream->ring, &index);
+
+ if (avail < wanted) {
+ pw_log_debug("capture underrun %d < %d", avail, wanted);
+ memset(d[0].data, 0, n_bytes);
+ } else {
+ spa_ringbuffer_read_data(&stream->ring,
+ stream->buffer_data,
+ stream->buffer_size,
+ index % stream->buffer_size,
+ d[0].data, n_bytes);
+ index += n_bytes;
+ spa_ringbuffer_read_update(&stream->ring, index);
+ }
+
+ d[0].chunk->size = n_bytes;
+ d[0].chunk->stride = stream->stride;
+ d[0].chunk->offset = 0;
+ buf->size = n_bytes / stream->stride;
+
+ pw_stream_queue_buffer(stream->stream, buf);
+}
+
+static const struct pw_stream_events source_stream_events = {
+ PW_VERSION_STREAM_EVENTS,
+ .destroy = on_stream_destroy,
+ .process = on_source_stream_process
+};
+
+static inline void
+set_iovec(struct spa_ringbuffer *rbuf, void *buffer, uint32_t size,
+ uint32_t offset, struct iovec *iov, uint32_t len)
+{
+ iov[0].iov_len = SPA_MIN(len, size - offset);
+ iov[0].iov_base = SPA_PTROFF(buffer, offset, void);
+ iov[1].iov_len = len - iov[0].iov_len;
+ iov[1].iov_base = buffer;
+}
+
+static int flush_write(struct stream *stream, uint64_t current_time)
+{
+ int32_t avail;
+ uint32_t index;
+ uint64_t ptime, txtime;
+ int pdu_count;
+ ssize_t n;
+ struct avb_frame_header *h = (void*)stream->pdu;
+ struct avb_packet_iec61883 *p = SPA_PTROFF(h, sizeof(*h), void);
+ uint8_t dbc;
+
+ avail = spa_ringbuffer_get_read_index(&stream->ring, &index);
+
+ pdu_count = (avail / stream->stride) / stream->frames_per_pdu;
+
+ txtime = current_time + stream->t_uncertainty;
+ ptime = txtime + stream->mtt;
+ dbc = stream->dbc;
+
+ while (pdu_count--) {
+ *(uint64_t*)CMSG_DATA(stream->cmsg) = txtime;
+
+ set_iovec(&stream->ring,
+ stream->buffer_data,
+ stream->buffer_size,
+ index % stream->buffer_size,
+ &stream->iov[1], stream->payload_size);
+
+ p->seq_num = stream->pdu_seq++;
+ p->tv = 1;
+ p->timestamp = ptime;
+ p->dbc = dbc;
+
+ n = sendmsg(stream->source->fd, &stream->msg, MSG_NOSIGNAL);
+ if (n < 0 || n != (ssize_t)stream->pdu_size) {
+ pw_log_error("sendmsg() failed %zd != %zd: %m",
+ n, stream->pdu_size);
+ }
+ txtime += stream->pdu_period;
+ ptime += stream->pdu_period;
+ index += stream->payload_size;
+ dbc += stream->frames_per_pdu;
+ }
+ stream->dbc = dbc;
+ spa_ringbuffer_read_update(&stream->ring, index);
+ return 0;
+}
+
+static void on_sink_stream_process(void *data)
+{
+ struct stream *stream = data;
+ struct pw_buffer *buf;
+ struct spa_data *d;
+ int32_t filled;
+ uint32_t index, offs, avail, size;
+ struct timespec now;
+
+ if ((buf = pw_stream_dequeue_buffer(stream->stream)) == NULL) {
+ pw_log_debug("out of buffers: %m");
+ return;
+ }
+
+ d = buf->buffer->datas;
+
+ offs = SPA_MIN(d[0].chunk->offset, d[0].maxsize);
+ size = SPA_MIN(d[0].chunk->size, d[0].maxsize - offs);
+ avail = size - offs;
+
+ filled = spa_ringbuffer_get_write_index(&stream->ring, &index);
+
+ if (filled >= (int32_t)stream->buffer_size) {
+ pw_log_warn("playback overrun %d >= %zd", filled, stream->buffer_size);
+ } else {
+ spa_ringbuffer_write_data(&stream->ring,
+ stream->buffer_data,
+ stream->buffer_size,
+ index % stream->buffer_size,
+ SPA_PTROFF(d[0].data, offs, void), avail);
+ index += avail;
+ spa_ringbuffer_write_update(&stream->ring, index);
+ }
+ pw_stream_queue_buffer(stream->stream, buf);
+
+ clock_gettime(CLOCK_TAI, &now);
+ flush_write(stream, SPA_TIMESPEC_TO_NSEC(&now));
+}
+
+static void setup_pdu(struct stream *stream)
+{
+ struct avb_frame_header *h;
+ struct avb_packet_iec61883 *p;
+ ssize_t payload_size, hdr_size, pdu_size;
+
+ spa_memzero(stream->pdu, sizeof(stream->pdu));
+ h = (struct avb_frame_header*)stream->pdu;
+ p = SPA_PTROFF(h, sizeof(*h), void);
+
+ hdr_size = sizeof(*h) + sizeof(*p);
+ payload_size = stream->stride * stream->frames_per_pdu;
+ pdu_size = hdr_size + payload_size;
+
+ h->type = htons(0x8100);
+ h->prio_cfi_id = htons((stream->prio << 13) | stream->vlan_id);
+ h->etype = htons(0x22f0);
+
+ if (stream->direction == SPA_DIRECTION_OUTPUT) {
+ p->subtype = AVB_SUBTYPE_61883_IIDC;
+ p->sv = 1;
+ p->stream_id = htobe64(stream->id);
+ p->data_len = htons(payload_size+8);
+ p->tag = 0x1;
+ p->channel = 0x1f;
+ p->tcode = 0xa;
+ p->sid = 0x3f;
+ p->dbs = stream->info.info.raw.channels;
+ p->qi2 = 0x2;
+ p->format_id = 0x10;
+ p->fdf = 0x2;
+ p->syt = htons(0x0008);
+ }
+ stream->hdr_size = hdr_size;
+ stream->payload_size = payload_size;
+ stream->pdu_size = pdu_size;
+}
+
+static int setup_msg(struct stream *stream)
+{
+ stream->iov[0].iov_base = stream->pdu;
+ stream->iov[0].iov_len = stream->hdr_size;
+ stream->iov[1].iov_base = SPA_PTROFF(stream->pdu, stream->hdr_size, void);
+ stream->iov[1].iov_len = stream->payload_size;
+ stream->iov[2].iov_base = SPA_PTROFF(stream->pdu, stream->hdr_size, void);
+ stream->iov[2].iov_len = 0;
+ stream->msg.msg_name = &stream->sock_addr;
+ stream->msg.msg_namelen = sizeof(stream->sock_addr);
+ stream->msg.msg_iov = stream->iov;
+ stream->msg.msg_iovlen = 3;
+ stream->msg.msg_control = stream->control;
+ stream->msg.msg_controllen = sizeof(stream->control);
+ stream->cmsg = CMSG_FIRSTHDR(&stream->msg);
+ stream->cmsg->cmsg_level = SOL_SOCKET;
+ stream->cmsg->cmsg_type = SCM_TXTIME;
+ stream->cmsg->cmsg_len = CMSG_LEN(sizeof(__u64));
+ return 0;
+}
+
+static const struct pw_stream_events sink_stream_events = {
+ PW_VERSION_STREAM_EVENTS,
+ .destroy = on_stream_destroy,
+ .process = on_sink_stream_process
+};
+
+struct stream *server_create_stream(struct server *server,
+ enum spa_direction direction, uint16_t index)
+{
+ struct stream *stream;
+ const struct descriptor *desc;
+ uint32_t n_params;
+ const struct spa_pod *params[1];
+ uint8_t buffer[1024];
+ struct spa_pod_builder b;
+ int res;
+
+ desc = server_find_descriptor(server,
+ direction == SPA_DIRECTION_INPUT ?
+ AVB_AEM_DESC_STREAM_INPUT :
+ AVB_AEM_DESC_STREAM_OUTPUT, index);
+ if (desc == NULL)
+ return NULL;
+
+ stream = calloc(1, sizeof(*stream));
+ if (stream == NULL)
+ return NULL;
+
+ stream->server = server;
+ stream->direction = direction;
+ stream->index = index;
+ stream->desc = desc;
+ spa_list_append(&server->streams, &stream->link);
+
+ stream->prio = AVB_MSRP_PRIORITY_DEFAULT;
+ stream->vlan_id = AVB_DEFAULT_VLAN;
+
+ stream->id = (uint64_t)server->mac_addr[0] << 56 |
+ (uint64_t)server->mac_addr[1] << 48 |
+ (uint64_t)server->mac_addr[2] << 40 |
+ (uint64_t)server->mac_addr[3] << 32 |
+ (uint64_t)server->mac_addr[4] << 24 |
+ (uint64_t)server->mac_addr[5] << 16 |
+ htons(index);
+
+ stream->vlan_attr = avb_mvrp_attribute_new(server->mvrp,
+ AVB_MVRP_ATTRIBUTE_TYPE_VID);
+ stream->vlan_attr->attr.vid.vlan = htons(stream->vlan_id);
+
+ stream->buffer_data = calloc(1, BUFFER_SIZE);
+ stream->buffer_size = BUFFER_SIZE;
+ spa_ringbuffer_init(&stream->ring);
+
+ if (direction == SPA_DIRECTION_INPUT) {
+ stream->stream = pw_stream_new(server->impl->core, "source",
+ pw_properties_new(
+ PW_KEY_MEDIA_CLASS, "Audio/Source",
+ PW_KEY_NODE_NAME, "avb.source",
+ PW_KEY_NODE_DESCRIPTION, "AVB Source",
+ PW_KEY_NODE_WANT_DRIVER, "true",
+ NULL));
+ } else {
+ stream->stream = pw_stream_new(server->impl->core, "sink",
+ pw_properties_new(
+ PW_KEY_MEDIA_CLASS, "Audio/Sink",
+ PW_KEY_NODE_NAME, "avb.sink",
+ PW_KEY_NODE_DESCRIPTION, "AVB Sink",
+ PW_KEY_NODE_WANT_DRIVER, "true",
+ NULL));
+ }
+ if (stream->stream == NULL)
+ goto error_free;
+
+ pw_stream_add_listener(stream->stream,
+ &stream->stream_listener,
+ direction == SPA_DIRECTION_INPUT ?
+ &source_stream_events :
+ &sink_stream_events,
+ stream);
+
+ stream->info.info.raw.format = SPA_AUDIO_FORMAT_S24_32_BE;
+ stream->info.info.raw.flags = SPA_AUDIO_FLAG_UNPOSITIONED;
+ stream->info.info.raw.rate = 48000;
+ stream->info.info.raw.channels = 8;
+ stream->stride = stream->info.info.raw.channels * 4;
+
+ n_params = 0;
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ params[n_params++] = spa_format_audio_raw_build(&b,
+ SPA_PARAM_EnumFormat, &stream->info.info.raw);
+
+ if ((res = pw_stream_connect(stream->stream,
+ pw_direction_reverse(direction),
+ PW_ID_ANY,
+ PW_STREAM_FLAG_MAP_BUFFERS |
+ PW_STREAM_FLAG_INACTIVE |
+ PW_STREAM_FLAG_RT_PROCESS,
+ params, n_params)) < 0)
+ goto error_free_stream;
+
+ stream->frames_per_pdu = 6;
+ stream->pdu_period = SPA_NSEC_PER_SEC * stream->frames_per_pdu /
+ stream->info.info.raw.rate;
+
+ setup_pdu(stream);
+ setup_msg(stream);
+
+ stream->listener_attr = avb_msrp_attribute_new(server->msrp,
+ AVB_MSRP_ATTRIBUTE_TYPE_LISTENER);
+ stream->talker_attr = avb_msrp_attribute_new(server->msrp,
+ AVB_MSRP_ATTRIBUTE_TYPE_TALKER_ADVERTISE);
+ stream->talker_attr->attr.talker.vlan_id = htons(stream->vlan_id);
+ stream->talker_attr->attr.talker.tspec_max_frame_size = htons(32 + stream->frames_per_pdu * stream->stride);
+ stream->talker_attr->attr.talker.tspec_max_interval_frames =
+ htons(AVB_MSRP_TSPEC_MAX_INTERVAL_FRAMES_DEFAULT);
+ stream->talker_attr->attr.talker.priority = stream->prio;
+ stream->talker_attr->attr.talker.rank = AVB_MSRP_RANK_DEFAULT;
+ stream->talker_attr->attr.talker.accumulated_latency = htonl(95);
+
+ return stream;
+
+error_free_stream:
+ pw_stream_destroy(stream->stream);
+ errno = -res;
+error_free:
+ free(stream);
+ return NULL;
+}
+
+void stream_destroy(struct stream *stream)
+{
+ avb_mrp_attribute_destroy(stream->listener_attr->mrp);
+ spa_list_remove(&stream->link);
+ free(stream);
+}
+
+static int setup_socket(struct stream *stream)
+{
+ struct server *server = stream->server;
+ int fd, res;
+ char buf[128];
+ struct ifreq req;
+
+ fd = socket(AF_PACKET, SOCK_RAW | SOCK_NONBLOCK, htons(ETH_P_ALL));
+ if (fd < 0) {
+ pw_log_error("socket() failed: %m");
+ return -errno;
+ }
+
+ spa_zero(req);
+ snprintf(req.ifr_name, sizeof(req.ifr_name), "%s", server->ifname);
+ res = ioctl(fd, SIOCGIFINDEX, &req);
+ if (res < 0) {
+ pw_log_error("SIOCGIFINDEX %s failed: %m", server->ifname);
+ res = -errno;
+ goto error_close;
+ }
+
+ spa_zero(stream->sock_addr);
+ stream->sock_addr.sll_family = AF_PACKET;
+ stream->sock_addr.sll_protocol = htons(ETH_P_TSN);
+ stream->sock_addr.sll_ifindex = req.ifr_ifindex;
+
+ if (stream->direction == SPA_DIRECTION_OUTPUT) {
+ struct sock_txtime txtime_cfg;
+
+ res = setsockopt(fd, SOL_SOCKET, SO_PRIORITY, &stream->prio,
+ sizeof(stream->prio));
+ if (res < 0) {
+ pw_log_error("setsockopt(SO_PRIORITY %d) failed: %m", stream->prio);
+ res = -errno;
+ goto error_close;
+ }
+
+ txtime_cfg.clockid = CLOCK_TAI;
+ txtime_cfg.flags = 0;
+ res = setsockopt(fd, SOL_SOCKET, SO_TXTIME, &txtime_cfg,
+ sizeof(txtime_cfg));
+ if (res < 0) {
+ pw_log_error("setsockopt(SO_TXTIME) failed: %m");
+ res = -errno;
+ goto error_close;
+ }
+ } else {
+ struct packet_mreq mreq;
+
+ res = bind(fd, (struct sockaddr *) &stream->sock_addr, sizeof(stream->sock_addr));
+ if (res < 0) {
+ pw_log_error("bind() failed: %m");
+ res = -errno;
+ goto error_close;
+ }
+
+ spa_zero(mreq);
+ mreq.mr_ifindex = req.ifr_ifindex;
+ mreq.mr_type = PACKET_MR_MULTICAST;
+ mreq.mr_alen = ETH_ALEN;
+ memcpy(&mreq.mr_address, stream->addr, ETH_ALEN);
+ res = setsockopt(fd, SOL_PACKET, PACKET_ADD_MEMBERSHIP,
+ &mreq, sizeof(struct packet_mreq));
+
+ pw_log_info("join %s", avb_utils_format_addr(buf, 128, stream->addr));
+
+ if (res < 0) {
+ pw_log_error("setsockopt(ADD_MEMBERSHIP) failed: %m");
+ res = -errno;
+ goto error_close;
+ }
+ }
+ return fd;
+
+error_close:
+ close(fd);
+ return res;
+}
+
+static void handle_iec61883_packet(struct stream *stream,
+ struct avb_packet_iec61883 *p, int len)
+{
+ uint32_t index, n_bytes;
+ int32_t filled;
+
+ filled = spa_ringbuffer_get_write_index(&stream->ring, &index);
+ n_bytes = ntohs(p->data_len) - 8;
+
+ if (filled + n_bytes > stream->buffer_size) {
+ pw_log_debug("capture overrun");
+ } else {
+ spa_ringbuffer_write_data(&stream->ring,
+ stream->buffer_data,
+ stream->buffer_size,
+ index % stream->buffer_size,
+ p->payload, n_bytes);
+ index += n_bytes;
+ spa_ringbuffer_write_update(&stream->ring, index);
+ }
+}
+
+static void on_socket_data(void *data, int fd, uint32_t mask)
+{
+ struct stream *stream = data;
+
+ if (mask & SPA_IO_IN) {
+ int len;
+ uint8_t buffer[2048];
+
+ len = recv(fd, buffer, sizeof(buffer), 0);
+
+ if (len < 0) {
+ pw_log_warn("got recv error: %m");
+ }
+ else if (len < (int)sizeof(struct avb_packet_header)) {
+ pw_log_warn("short packet received (%d < %d)", len,
+ (int)sizeof(struct avb_packet_header));
+ } else {
+ struct avb_frame_header *h = (void*)buffer;
+ struct avb_packet_iec61883 *p = SPA_PTROFF(h, sizeof(*h), void);
+
+ if (memcmp(h->dest, stream->addr, 6) != 0 ||
+ p->subtype != AVB_SUBTYPE_61883_IIDC)
+ return;
+
+ handle_iec61883_packet(stream, p, len - sizeof(*h));
+ }
+ }
+}
+
+int stream_activate(struct stream *stream, uint64_t now)
+{
+ struct server *server = stream->server;
+ struct avb_frame_header *h = (void*)stream->pdu;
+ int fd, res;
+
+ if (stream->source == NULL) {
+ if ((fd = setup_socket(stream)) < 0)
+ return fd;
+
+ stream->source = pw_loop_add_io(server->impl->loop, fd,
+ SPA_IO_IN, true, on_socket_data, stream);
+ if (stream->source == NULL) {
+ res = -errno;
+ pw_log_error("stream %p: can't create source: %m", stream);
+ close(fd);
+ return res;
+ }
+ }
+
+ avb_mrp_attribute_begin(stream->vlan_attr->mrp, now);
+ avb_mrp_attribute_join(stream->vlan_attr->mrp, now, true);
+
+ if (stream->direction == SPA_DIRECTION_INPUT) {
+ stream->listener_attr->attr.listener.stream_id = htobe64(stream->peer_id);
+ stream->listener_attr->param = AVB_MSRP_LISTENER_PARAM_READY;
+ avb_mrp_attribute_begin(stream->listener_attr->mrp, now);
+ avb_mrp_attribute_join(stream->listener_attr->mrp, now, true);
+
+ stream->talker_attr->attr.talker.stream_id = htobe64(stream->peer_id);
+ avb_mrp_attribute_begin(stream->talker_attr->mrp, now);
+ } else {
+ if ((res = avb_maap_get_address(server->maap, stream->addr, stream->index)) < 0)
+ return res;
+
+ stream->listener_attr->attr.listener.stream_id = htobe64(stream->id);
+ stream->listener_attr->param = AVB_MSRP_LISTENER_PARAM_IGNORE;
+ avb_mrp_attribute_begin(stream->listener_attr->mrp, now);
+
+ stream->talker_attr->attr.talker.stream_id = htobe64(stream->id);
+ memcpy(stream->talker_attr->attr.talker.dest_addr, stream->addr, 6);
+
+ stream->sock_addr.sll_halen = ETH_ALEN;
+ memcpy(&stream->sock_addr.sll_addr, stream->addr, ETH_ALEN);
+ memcpy(h->dest, stream->addr, 6);
+ memcpy(h->src, server->mac_addr, 6);
+ avb_mrp_attribute_begin(stream->talker_attr->mrp, now);
+ avb_mrp_attribute_join(stream->talker_attr->mrp, now, true);
+ }
+ pw_stream_set_active(stream->stream, true);
+ return 0;
+}
+
+int stream_deactivate(struct stream *stream, uint64_t now)
+{
+ pw_stream_set_active(stream->stream, false);
+
+ if (stream->source != NULL) {
+ pw_loop_destroy_source(stream->server->impl->loop, stream->source);
+ stream->source = NULL;
+ }
+
+ avb_mrp_attribute_leave(stream->vlan_attr->mrp, now);
+
+ if (stream->direction == SPA_DIRECTION_INPUT) {
+ avb_mrp_attribute_leave(stream->listener_attr->mrp, now);
+ } else {
+ avb_mrp_attribute_leave(stream->talker_attr->mrp, now);
+ }
+ return 0;
+}
diff --git a/src/modules/module-avb/stream.h b/src/modules/module-avb/stream.h
new file mode 100644
index 0000000..7062e25
--- /dev/null
+++ b/src/modules/module-avb/stream.h
@@ -0,0 +1,104 @@
+/* AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef AVB_STREAM_H
+#define AVB_STREAM_H
+
+#include <sys/socket.h>
+#include <sys/types.h>
+#include <linux/if_packet.h>
+#include <net/if.h>
+
+#include <spa/utils/ringbuffer.h>
+#include <spa/param/audio/format.h>
+
+#include <pipewire/pipewire.h>
+
+#define BUFFER_SIZE (1u<<16)
+#define BUFFER_MASK (BUFFER_SIZE-1)
+
+struct stream {
+ struct spa_list link;
+
+ struct server *server;
+
+ uint16_t direction;
+ uint16_t index;
+ const struct descriptor *desc;
+ uint64_t id;
+ uint64_t peer_id;
+
+ struct pw_stream *stream;
+ struct spa_hook stream_listener;
+
+ uint8_t addr[6];
+ struct spa_source *source;
+ int prio;
+ int vlan_id;
+ int mtt;
+ int t_uncertainty;
+ uint32_t frames_per_pdu;
+ int ptime_tolerance;
+
+ uint8_t pdu[2048];
+ size_t hdr_size;
+ size_t payload_size;
+ size_t pdu_size;
+ int64_t pdu_period;
+ uint8_t pdu_seq;
+ uint8_t prev_seq;
+ uint8_t dbc;
+
+ struct iovec iov[3];
+ struct sockaddr_ll sock_addr;
+ struct msghdr msg;
+ char control[CMSG_SPACE(sizeof(uint64_t))];
+ struct cmsghdr *cmsg;
+
+ struct spa_ringbuffer ring;
+ void *buffer_data;
+ size_t buffer_size;
+
+ uint64_t format;
+ uint32_t stride;
+ struct spa_audio_info info;
+
+ struct avb_msrp_attribute *talker_attr;
+ struct avb_msrp_attribute *listener_attr;
+ struct avb_mvrp_attribute *vlan_attr;
+};
+
+#include "msrp.h"
+#include "mvrp.h"
+#include "maap.h"
+
+struct stream *server_create_stream(struct server *server,
+ enum spa_direction direction, uint16_t index);
+
+void stream_destroy(struct stream *stream);
+
+int stream_activate(struct stream *stream, uint64_t now);
+int stream_deactivate(struct stream *stream, uint64_t now);
+
+#endif /* AVB_STREAM_H */
diff --git a/src/modules/module-avb/utils.h b/src/modules/module-avb/utils.h
new file mode 100644
index 0000000..f626722
--- /dev/null
+++ b/src/modules/module-avb/utils.h
@@ -0,0 +1,86 @@
+/* AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef AVB_UTILS_H
+#define AVB_UTILS_H
+
+#include <spa/utils/json.h>
+
+#include "internal.h"
+
+static inline char *avb_utils_format_id(char *str, size_t size, const uint64_t id)
+{
+ snprintf(str, size, "%02x:%02x:%02x:%02x:%02x:%02x:%04x",
+ (uint8_t)(id >> 56),
+ (uint8_t)(id >> 48),
+ (uint8_t)(id >> 40),
+ (uint8_t)(id >> 32),
+ (uint8_t)(id >> 24),
+ (uint8_t)(id >> 16),
+ (uint16_t)(id));
+ return str;
+}
+
+static inline int avb_utils_parse_id(const char *str, int len, uint64_t *id)
+{
+ char s[64];
+ uint8_t v[6];
+ uint16_t unique_id;
+ if (spa_json_parse_stringn(str, len, s, sizeof(s)) <= 0)
+ return -EINVAL;
+ if (sscanf(s, "%hhx:%hhx:%hhx:%hhx:%hhx:%hhx:%hx",
+ &v[0], &v[1], &v[2], &v[3],
+ &v[4], &v[5], &unique_id) == 7) {
+ *id = (uint64_t) v[0] << 56 |
+ (uint64_t) v[1] << 48 |
+ (uint64_t) v[2] << 40 |
+ (uint64_t) v[3] << 32 |
+ (uint64_t) v[4] << 24 |
+ (uint64_t) v[5] << 16 |
+ unique_id;
+ } else if (!spa_atou64(str, id, 0))
+ return -EINVAL;
+ return 0;
+}
+
+static inline char *avb_utils_format_addr(char *str, size_t size, const uint8_t addr[6])
+{
+ snprintf(str, size, "%02x:%02x:%02x:%02x:%02x:%02x",
+ addr[0], addr[1], addr[2], addr[3], addr[4], addr[5]);
+ return str;
+}
+static inline int avb_utils_parse_addr(const char *str, int len, uint8_t addr[6])
+{
+ char s[64];
+ uint8_t v[6];
+ if (spa_json_parse_stringn(str, len, s, sizeof(s)) <= 0)
+ return -EINVAL;
+ if (sscanf(s, "%hhx:%hhx:%hhx:%hhx:%hhx:%hhx",
+ &v[0], &v[1], &v[2], &v[3], &v[4], &v[5]) != 6)
+ return -EINVAL;
+ memcpy(addr, v, 6);
+ return 0;
+}
+
+#endif /* AVB_UTILS_H */
diff --git a/src/modules/module-client-device.c b/src/modules/module-client-device.c
new file mode 100644
index 0000000..cf3bae3
--- /dev/null
+++ b/src/modules/module-client-device.c
@@ -0,0 +1,232 @@
+/* PipeWire
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <errno.h>
+#include <dlfcn.h>
+
+#include "config.h"
+
+#include <spa/utils/result.h>
+
+#include <pipewire/impl.h>
+
+#include "module-client-device/client-device.h"
+
+/** \page page_module_client_device PipeWire Module: Client Device
+ */
+
+#define NAME "client-device"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+static const struct spa_dict_item module_props[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Allow clients to create and control remote devices" },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+struct pw_proxy *pw_core_spa_device_export(struct pw_core *core,
+ const char *type, const struct spa_dict *props, void *object,
+ size_t user_data_size);
+
+struct pw_protocol *pw_protocol_native_ext_client_device_init(struct pw_context *context);
+
+struct factory_data {
+ struct pw_impl_factory *factory;
+ struct spa_hook factory_listener;
+
+ struct pw_impl_module *module;
+ struct spa_hook module_listener;
+
+ struct pw_export_type export_spadevice;
+};
+
+static void *create_object(void *_data,
+ struct pw_resource *resource,
+ const char *type,
+ uint32_t version,
+ struct pw_properties *properties,
+ uint32_t new_id)
+{
+ struct factory_data *data = _data;
+ struct pw_impl_factory *factory = data->factory;
+ void *result;
+ struct pw_resource *device_resource;
+ struct pw_impl_client *client;
+ int res;
+
+ if (resource == NULL) {
+ res = -EINVAL;
+ goto error_exit;
+ }
+
+ client = pw_resource_get_client(resource);
+ device_resource = pw_resource_new(client, new_id, PW_PERM_ALL, type, version, 0);
+ if (device_resource == NULL) {
+ res = -errno;
+ goto error_resource;
+ }
+
+ if (properties == NULL)
+ properties = pw_properties_new(NULL, NULL);
+ if (properties == NULL) {
+ res = -errno;
+ goto error_properties;
+ }
+
+ pw_properties_setf(properties, PW_KEY_FACTORY_ID, "%d",
+ pw_global_get_id(pw_impl_factory_get_global(factory)));
+ pw_properties_setf(properties, PW_KEY_CLIENT_ID, "%d",
+ pw_global_get_id(pw_impl_client_get_global(client)));
+
+ result = pw_client_device_new(device_resource, properties);
+ if (result == NULL) {
+ res = -errno;
+ goto error_device;
+ }
+ return result;
+
+error_resource:
+ pw_log_error("can't create resource: %s", spa_strerror(res));
+ pw_resource_errorf_id(resource, new_id, res, "can't create resource: %s", spa_strerror(res));
+ goto error_exit;
+error_properties:
+ pw_log_error("can't create properties: %s", spa_strerror(res));
+ pw_resource_errorf_id(resource, new_id, res, "can't create properties: %s", spa_strerror(res));
+ goto error_exit_free;
+error_device:
+ pw_log_error("can't create device: %s", spa_strerror(res));
+ pw_resource_errorf_id(resource, new_id, res, "can't create device: %s", spa_strerror(res));
+ goto error_exit_free;
+
+error_exit_free:
+ pw_resource_remove(device_resource);
+error_exit:
+ errno = -res;
+ return NULL;
+}
+
+static const struct pw_impl_factory_implementation impl_factory = {
+ PW_VERSION_IMPL_FACTORY_IMPLEMENTATION,
+ .create_object = create_object,
+};
+
+static void factory_destroy(void *data)
+{
+ struct factory_data *d = data;
+ spa_hook_remove(&d->factory_listener);
+ d->factory = NULL;
+ if (d->module)
+ pw_impl_module_destroy(d->module);
+}
+
+static const struct pw_impl_factory_events factory_events = {
+ PW_VERSION_IMPL_FACTORY_EVENTS,
+ .destroy = factory_destroy,
+};
+
+static void module_destroy(void *data)
+{
+ struct factory_data *d = data;
+ spa_hook_remove(&d->module_listener);
+ spa_list_remove(&d->export_spadevice.link);
+ d->module = NULL;
+ if (d->factory)
+ pw_impl_factory_destroy(d->factory);
+}
+
+static void module_registered(void *data)
+{
+ struct factory_data *d = data;
+ struct pw_impl_module *module = d->module;
+ struct pw_impl_factory *factory = d->factory;
+ struct spa_dict_item items[1];
+ char id[16];
+ int res;
+
+ snprintf(id, sizeof(id), "%d", pw_global_get_id(pw_impl_module_get_global(module)));
+ items[0] = SPA_DICT_ITEM_INIT(PW_KEY_MODULE_ID, id);
+ pw_impl_factory_update_properties(factory, &SPA_DICT_INIT(items, 1));
+
+ if ((res = pw_impl_factory_register(factory, NULL)) < 0) {
+ pw_log_error("%p: can't register factory: %s", factory, spa_strerror(res));
+ }
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy,
+ .registered = module_registered,
+};
+
+SPA_EXPORT
+int pipewire__module_init(struct pw_impl_module *module, const char *args)
+{
+ struct pw_context *context = pw_impl_module_get_context(module);
+ struct pw_impl_factory *factory;
+ struct factory_data *data;
+ int res;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+ factory = pw_context_create_factory(context,
+ "client-device",
+ SPA_TYPE_INTERFACE_Device,
+ SPA_VERSION_DEVICE,
+ pw_properties_new(
+ PW_KEY_FACTORY_USAGE, CLIENT_DEVICE_USAGE,
+ NULL),
+ sizeof(*data));
+ if (factory == NULL)
+ return -errno;
+
+ data = pw_impl_factory_get_user_data(factory);
+ data->factory = factory;
+ data->module = module;
+
+ pw_log_debug("module %p: new", module);
+
+ pw_impl_factory_set_implementation(factory,
+ &impl_factory,
+ data);
+
+ data->export_spadevice.type = SPA_TYPE_INTERFACE_Device;
+ data->export_spadevice.func = pw_core_spa_device_export;
+ if ((res = pw_context_register_export_type(context, &data->export_spadevice)) < 0)
+ goto error;
+
+ pw_protocol_native_ext_client_device_init(context);
+
+ pw_impl_factory_add_listener(factory, &data->factory_listener, &factory_events, data);
+ pw_impl_module_add_listener(module, &data->module_listener, &module_events, data);
+
+ pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_props));
+
+ return 0;
+error:
+ pw_impl_factory_destroy(data->factory);
+ return res;
+}
diff --git a/src/modules/module-client-device/client-device.h b/src/modules/module-client-device/client-device.h
new file mode 100644
index 0000000..ada2b7d
--- /dev/null
+++ b/src/modules/module-client-device/client-device.h
@@ -0,0 +1,44 @@
+/* PipeWire
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_CLIENT_DEVICE_H
+#define PIPEWIRE_CLIENT_DEVICE_H
+
+#include <pipewire/impl.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define CLIENT_DEVICE_USAGE "["PW_KEY_DEVICE_NAME"=<string>]"
+
+struct pw_device *
+pw_client_device_new(struct pw_resource *resource,
+ struct pw_properties *properties);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PIPEWIRE_CLIENT_DEVICE_H */
diff --git a/src/modules/module-client-device/protocol-native.c b/src/modules/module-client-device/protocol-native.c
new file mode 100644
index 0000000..2734aac
--- /dev/null
+++ b/src/modules/module-client-device/protocol-native.c
@@ -0,0 +1,556 @@
+/* PipeWire
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+
+#include <spa/pod/builder.h>
+#include <spa/pod/parser.h>
+#include <spa/utils/result.h>
+
+#include <pipewire/impl.h>
+
+#include <pipewire/extensions/protocol-native.h>
+
+#define MAX_DICT 1024
+#define MAX_PARAM_INFO 128
+
+static inline void push_item(struct spa_pod_builder *b, const struct spa_dict_item *item)
+{
+ const char *str;
+ spa_pod_builder_string(b, item->key);
+ str = item->value;
+ if (spa_strstartswith(str, "pointer:"))
+ str = "";
+ spa_pod_builder_string(b, str);
+}
+
+static inline int parse_item(struct spa_pod_parser *prs, struct spa_dict_item *item)
+{
+ int res;
+ if ((res = spa_pod_parser_get(prs,
+ SPA_POD_String(&item->key),
+ SPA_POD_String(&item->value),
+ NULL)) < 0)
+ return res;
+ if (spa_strstartswith(item->value, "pointer:"))
+ item->value = "";
+ return 0;
+}
+
+#define parse_dict(prs,d) \
+do { \
+ uint32_t i; \
+ if (spa_pod_parser_get(prs, \
+ SPA_POD_Int(&(d)->n_items), NULL) < 0) \
+ return -EINVAL; \
+ if ((d)->n_items > 0) { \
+ if ((d)->n_items > MAX_DICT) \
+ return -ENOSPC; \
+ (d)->items = alloca((d)->n_items * sizeof(struct spa_dict_item)); \
+ for (i = 0; i < (d)->n_items; i++) { \
+ if (parse_item(prs, (struct spa_dict_item *) &(d)->items[i]) < 0) \
+ return -EINVAL; \
+ } \
+ } \
+} while(0)
+
+#define parse_param_info(prs,n_params,params) \
+do { \
+ uint32_t i; \
+ if (spa_pod_parser_get(prs, \
+ SPA_POD_Int(&(n_params)), NULL) < 0) \
+ return -EINVAL; \
+ if (n_params > 0) { \
+ if (n_params > MAX_PARAM_INFO) \
+ return -ENOSPC; \
+ params = alloca(n_params * sizeof(struct spa_param_info)); \
+ for (i = 0; i < n_params; i++) { \
+ if (spa_pod_parser_get(prs, \
+ SPA_POD_Id(&(params[i]).id), \
+ SPA_POD_Int(&(params[i]).flags), NULL) < 0) \
+ return -EINVAL; \
+ } \
+ } \
+} while(0)
+
+static int device_marshal_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct spa_device_events *events,
+ void *data)
+{
+ struct pw_resource *resource = object;
+ pw_resource_add_object_listener(resource, listener, events, data);
+ return 0;
+}
+
+static int device_demarshal_add_listener(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ return -ENOTSUP;
+}
+
+static int device_marshal_sync(void *object, int seq)
+{
+ struct pw_protocol_native_message *msg;
+ struct pw_resource *resource = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource, SPA_DEVICE_METHOD_SYNC, &msg);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(SPA_RESULT_RETURN_ASYNC(msg->seq)));
+
+ return pw_protocol_native_end_resource(resource, b);
+}
+
+static int device_demarshal_sync(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_parser prs;
+ int seq;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&seq)) < 0)
+ return -EINVAL;
+
+ pw_proxy_notify(proxy, struct spa_device_methods, sync, 0, seq);
+ return 0;
+}
+
+static int device_marshal_enum_params(void *object, int seq,
+ uint32_t id, uint32_t index, uint32_t max,
+ const struct spa_pod *filter)
+{
+ struct pw_protocol_native_message *msg;
+ struct pw_resource *resource = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource, SPA_DEVICE_METHOD_ENUM_PARAMS, &msg);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(SPA_RESULT_RETURN_ASYNC(msg->seq)),
+ SPA_POD_Id(id),
+ SPA_POD_Int(index),
+ SPA_POD_Int(max),
+ SPA_POD_Pod(filter));
+
+ return pw_protocol_native_end_resource(resource, b);
+}
+
+static int device_demarshal_enum_params(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_parser prs;
+ uint32_t id, index, max;
+ int seq;
+ struct spa_pod *filter;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&seq),
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&index),
+ SPA_POD_Int(&max),
+ SPA_POD_Pod(&filter)) < 0)
+ return -EINVAL;
+
+ pw_proxy_notify(proxy, struct spa_device_methods, enum_params, 0,
+ seq, id, index, max, filter);
+ return 0;
+}
+
+static int device_marshal_set_param(void *object,
+ uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource, SPA_DEVICE_METHOD_SET_PARAM, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Id(id),
+ SPA_POD_Int(flags),
+ SPA_POD_Pod(param));
+
+ return pw_protocol_native_end_resource(resource, b);
+}
+
+static int device_demarshal_set_param(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_parser prs;
+ uint32_t id, flags;
+ struct spa_pod *param;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&flags),
+ SPA_POD_Pod(&param)) < 0)
+ return -EINVAL;
+
+ pw_proxy_notify(proxy, struct spa_device_methods, set_param, 0,
+ id, flags, param);
+ return 0;
+}
+
+static void device_marshal_info(void *data,
+ const struct spa_device_info *info)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_builder *b;
+ struct spa_pod_frame f[2];
+ uint32_t i, n_items;
+
+ b = pw_protocol_native_begin_proxy(proxy, SPA_DEVICE_EVENT_INFO, NULL);
+
+ spa_pod_builder_push_struct(b, &f[0]);
+ if (info) {
+ uint64_t change_mask = info->change_mask;
+
+ change_mask &= SPA_DEVICE_CHANGE_MASK_FLAGS |
+ SPA_DEVICE_CHANGE_MASK_PROPS |
+ SPA_DEVICE_CHANGE_MASK_PARAMS;
+
+ n_items = info->props ? info->props->n_items : 0;
+
+ spa_pod_builder_push_struct(b, &f[1]);
+ spa_pod_builder_add(b,
+ SPA_POD_Long(change_mask),
+ SPA_POD_Long(info->flags),
+ SPA_POD_Int(n_items), NULL);
+ for (i = 0; i < n_items; i++)
+ push_item(b, &info->props->items[i]);
+ spa_pod_builder_add(b,
+ SPA_POD_Int(info->n_params), NULL);
+ for (i = 0; i < info->n_params; i++) {
+ spa_pod_builder_add(b,
+ SPA_POD_Id(info->params[i].id),
+ SPA_POD_Int(info->params[i].flags), NULL);
+ }
+ spa_pod_builder_pop(b, &f[1]);
+ } else {
+ spa_pod_builder_add(b,
+ SPA_POD_Pod(NULL), NULL);
+ }
+ spa_pod_builder_pop(b, &f[0]);
+
+ pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int device_demarshal_info(void *data,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_parser prs;
+ struct spa_pod *ipod;
+ struct spa_device_info info = SPA_DEVICE_INFO_INIT(), *infop;
+ struct spa_dict props = SPA_DICT_INIT(NULL, 0);
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_PodStruct(&ipod)) < 0)
+ return -EINVAL;
+
+ if (ipod) {
+ struct spa_pod_parser p2;
+ struct spa_pod_frame f2;
+ infop = &info;
+
+ spa_pod_parser_pod(&p2, ipod);
+ if (spa_pod_parser_push_struct(&p2, &f2) < 0 ||
+ spa_pod_parser_get(&p2,
+ SPA_POD_Long(&info.change_mask),
+ SPA_POD_Long(&info.flags), NULL) < 0)
+ return -EINVAL;
+
+ info.change_mask &= SPA_DEVICE_CHANGE_MASK_FLAGS |
+ SPA_DEVICE_CHANGE_MASK_PROPS |
+ SPA_DEVICE_CHANGE_MASK_PARAMS;
+
+ parse_dict(&p2, &props);
+ if (props.n_items > 0)
+ info.props = &props;
+
+ parse_param_info(&p2, info.n_params, info.params);
+ }
+ else {
+ infop = NULL;
+ }
+ pw_resource_notify(resource, struct spa_device_events, info, 0, infop);
+ return 0;
+}
+
+static void device_marshal_result(void *data,
+ int seq, int res, uint32_t type, const void *result)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_builder *b;
+ struct spa_pod_frame f[2];
+
+ b = pw_protocol_native_begin_proxy(proxy, SPA_DEVICE_EVENT_RESULT, NULL);
+ spa_pod_builder_push_struct(b, &f[0]);
+ spa_pod_builder_add(b,
+ SPA_POD_Int(seq),
+ SPA_POD_Int(res),
+ SPA_POD_Id(type),
+ NULL);
+
+ switch (type) {
+ case SPA_RESULT_TYPE_DEVICE_PARAMS:
+ {
+ const struct spa_result_device_params *r = result;
+ spa_pod_builder_add(b,
+ SPA_POD_Id(r->id),
+ SPA_POD_Int(r->index),
+ SPA_POD_Int(r->next),
+ SPA_POD_Pod(r->param),
+ NULL);
+ break;
+ }
+ default:
+ break;
+ }
+
+ spa_pod_builder_pop(b, &f[0]);
+
+ pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int device_demarshal_result(void *data,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_parser prs;
+ struct spa_pod_frame f[1];
+ int seq, res;
+ uint32_t type;
+ const void *result;
+ struct spa_result_device_params params;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_push_struct(&prs, &f[0]) < 0 ||
+ spa_pod_parser_get(&prs,
+ SPA_POD_Int(&seq),
+ SPA_POD_Int(&res),
+ SPA_POD_Id(&type),
+ NULL) < 0)
+ return -EINVAL;
+
+ switch (type) {
+ case SPA_RESULT_TYPE_DEVICE_PARAMS:
+ if (spa_pod_parser_get(&prs,
+ SPA_POD_Id(&params.id),
+ SPA_POD_Int(&params.index),
+ SPA_POD_Int(&params.next),
+ SPA_POD_PodObject(&params.param),
+ NULL) < 0)
+ return -EINVAL;
+
+ result = &params;
+ break;
+
+ default:
+ result = NULL;
+ break;
+ }
+
+ pw_resource_notify(resource, struct spa_device_events, result, 0, seq, res, type, result);
+ return 0;
+}
+
+static void device_marshal_event(void *data, const struct spa_event *event)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_proxy(proxy, SPA_DEVICE_EVENT_EVENT, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Pod(event));
+
+ pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int device_demarshal_event(void *data,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_parser prs;
+ struct spa_event *event;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_PodObject(&event)) < 0)
+ return -EINVAL;
+
+ pw_resource_notify(resource, struct spa_device_events, event, 0, event);
+ return 0;
+}
+
+static void device_marshal_object_info(void *data, uint32_t id,
+ const struct spa_device_object_info *info)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_builder *b;
+ struct spa_pod_frame f[2];
+ uint32_t i, n_items;
+
+ b = pw_protocol_native_begin_proxy(proxy, SPA_DEVICE_EVENT_OBJECT_INFO, NULL);
+ spa_pod_builder_push_struct(b, &f[0]);
+ spa_pod_builder_add(b,
+ SPA_POD_Int(id),
+ NULL);
+ if (info) {
+ uint64_t change_mask = info->change_mask;
+
+ change_mask &= SPA_DEVICE_OBJECT_CHANGE_MASK_FLAGS |
+ SPA_DEVICE_OBJECT_CHANGE_MASK_PROPS;
+
+ n_items = info->props ? info->props->n_items : 0;
+
+ spa_pod_builder_push_struct(b, &f[1]);
+ spa_pod_builder_add(b,
+ SPA_POD_String(info->type),
+ SPA_POD_Long(change_mask),
+ SPA_POD_Long(info->flags),
+ SPA_POD_Int(n_items), NULL);
+ for (i = 0; i < n_items; i++)
+ push_item(b, &info->props->items[i]);
+ spa_pod_builder_pop(b, &f[1]);
+ } else {
+ spa_pod_builder_add(b,
+ SPA_POD_Pod(NULL), NULL);
+ }
+ spa_pod_builder_pop(b, &f[0]);
+
+ pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int device_demarshal_object_info(void *data,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_parser prs;
+ struct spa_device_object_info info = SPA_DEVICE_OBJECT_INFO_INIT(), *infop;
+ struct spa_pod *ipod;
+ struct spa_dict props = SPA_DICT_INIT(NULL, 0);
+ uint32_t id;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&id),
+ SPA_POD_PodStruct(&ipod)) < 0)
+ return -EINVAL;
+
+ if (ipod) {
+ struct spa_pod_parser p2;
+ struct spa_pod_frame f2;
+ infop = &info;
+
+ spa_pod_parser_pod(&p2, ipod);
+ if (spa_pod_parser_push_struct(&p2, &f2) < 0 ||
+ spa_pod_parser_get(&p2,
+ SPA_POD_String(&info.type),
+ SPA_POD_Long(&info.change_mask),
+ SPA_POD_Long(&info.flags), NULL) < 0)
+ return -EINVAL;
+
+ info.change_mask &= SPA_DEVICE_OBJECT_CHANGE_MASK_FLAGS |
+ SPA_DEVICE_CHANGE_MASK_PROPS;
+
+ parse_dict(&p2, &props);
+ if (props.n_items > 0)
+ info.props = &props;
+ } else {
+ infop = NULL;
+ }
+
+ pw_resource_notify(resource, struct spa_device_events, object_info, 0, id, infop);
+ return 0;
+}
+
+static const struct spa_device_methods pw_protocol_native_device_method_marshal = {
+ SPA_VERSION_DEVICE_METHODS,
+ .add_listener = &device_marshal_add_listener,
+ .sync = &device_marshal_sync,
+ .enum_params = &device_marshal_enum_params,
+ .set_param = &device_marshal_set_param
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_device_method_demarshal[SPA_DEVICE_METHOD_NUM] =
+{
+ [SPA_DEVICE_METHOD_ADD_LISTENER] = { &device_demarshal_add_listener, 0 },
+ [SPA_DEVICE_METHOD_SYNC] = { &device_demarshal_sync, 0 },
+ [SPA_DEVICE_METHOD_ENUM_PARAMS] = { &device_demarshal_enum_params, 0 },
+ [SPA_DEVICE_METHOD_SET_PARAM] = { &device_demarshal_set_param, 0 },
+};
+
+static const struct spa_device_events pw_protocol_native_device_event_marshal = {
+ SPA_VERSION_DEVICE_EVENTS,
+ .info = &device_marshal_info,
+ .result = &device_marshal_result,
+ .event = &device_marshal_event,
+ .object_info = &device_marshal_object_info,
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_device_event_demarshal[SPA_DEVICE_EVENT_NUM] =
+{
+ [SPA_DEVICE_EVENT_INFO] = { &device_demarshal_info, 0 },
+ [SPA_DEVICE_EVENT_RESULT] = { &device_demarshal_result, 0 },
+ [SPA_DEVICE_EVENT_EVENT] = { &device_demarshal_event, 0 },
+ [SPA_DEVICE_EVENT_OBJECT_INFO] = { &device_demarshal_object_info, 0 },
+};
+
+static const struct pw_protocol_marshal pw_protocol_native_client_device_marshal = {
+ SPA_TYPE_INTERFACE_Device,
+ SPA_VERSION_DEVICE,
+ PW_PROTOCOL_MARSHAL_FLAG_IMPL,
+ SPA_DEVICE_EVENT_NUM,
+ SPA_DEVICE_METHOD_NUM,
+ .client_marshal = &pw_protocol_native_device_event_marshal,
+ .server_demarshal = pw_protocol_native_device_event_demarshal,
+ .server_marshal = &pw_protocol_native_device_method_marshal,
+ .client_demarshal = pw_protocol_native_device_method_demarshal,
+};
+
+struct pw_protocol *pw_protocol_native_ext_client_device_init(struct pw_context *context)
+{
+ struct pw_protocol *protocol;
+
+ protocol = pw_context_find_protocol(context, PW_TYPE_INFO_PROTOCOL_Native);
+
+ if (protocol == NULL)
+ return NULL;
+
+ pw_protocol_add_marshal(protocol, &pw_protocol_native_client_device_marshal);
+
+ return protocol;
+}
diff --git a/src/modules/module-client-device/proxy-device.c b/src/modules/module-client-device/proxy-device.c
new file mode 100644
index 0000000..43bdf8e
--- /dev/null
+++ b/src/modules/module-client-device/proxy-device.c
@@ -0,0 +1,90 @@
+/* PipeWire
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdio.h>
+#include <unistd.h>
+#include <errno.h>
+
+#include <spa/pod/parser.h>
+#include <spa/monitor/device.h>
+
+#include "pipewire/pipewire.h"
+
+struct device_data {
+ struct spa_device *device;
+ struct spa_hook device_listener;
+ struct spa_hook device_methods;
+
+ struct pw_proxy *proxy;
+ struct spa_hook proxy_listener;
+};
+
+static void proxy_device_destroy(void *_data)
+{
+ struct device_data *data = _data;
+ spa_hook_remove(&data->device_listener);
+ spa_hook_remove(&data->device_methods);
+ spa_hook_remove(&data->proxy_listener);
+}
+
+static const struct pw_proxy_events proxy_events = {
+ PW_VERSION_PROXY_EVENTS,
+ .destroy = proxy_device_destroy,
+};
+
+struct pw_proxy *pw_core_spa_device_export(struct pw_core *core,
+ const char *type, const struct spa_dict *props, void *object,
+ size_t user_data_size)
+{
+ struct spa_device *device = object;
+ struct spa_interface *iface, *diface;
+ struct pw_proxy *proxy;
+ struct device_data *data;
+
+ proxy = pw_core_create_object(core,
+ "client-device",
+ SPA_TYPE_INTERFACE_Device,
+ SPA_VERSION_DEVICE,
+ props,
+ user_data_size + sizeof(struct device_data));
+ if (proxy == NULL)
+ return NULL;
+
+ data = pw_proxy_get_user_data(proxy);
+ data = SPA_PTROFF(data, user_data_size, struct device_data);
+ data->device = device;
+ data->proxy = proxy;
+
+ iface = (struct spa_interface*)proxy;
+ diface = (struct spa_interface*)device;
+
+ pw_proxy_add_listener(proxy, &data->proxy_listener, &proxy_events, data);
+
+ pw_proxy_add_object_listener(proxy, &data->device_methods,
+ diface->cb.funcs, diface->cb.data);
+ spa_device_add_listener(device, &data->device_listener,
+ iface->cb.funcs, iface->cb.data);
+
+ return proxy;
+}
diff --git a/src/modules/module-client-device/resource-device.c b/src/modules/module-client-device/resource-device.c
new file mode 100644
index 0000000..0f5a40b
--- /dev/null
+++ b/src/modules/module-client-device/resource-device.c
@@ -0,0 +1,155 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <errno.h>
+#include <unistd.h>
+#include <time.h>
+
+#include <spa/monitor/device.h>
+#include <spa/monitor/utils.h>
+#include <spa/pod/filter.h>
+#include <spa/pod/parser.h>
+#include <spa/debug/types.h>
+
+#include <pipewire/impl.h>
+
+struct impl {
+ struct pw_context *context;
+ struct pw_impl_device *device;
+ struct spa_hook device_listener;
+
+ struct pw_resource *resource;
+ struct spa_hook resource_listener;
+ struct spa_hook object_listener;
+
+ unsigned int registered:1;
+};
+
+static void device_info(void *data, const struct spa_device_info *info)
+{
+ struct impl *impl = data;
+ if (!impl->registered) {
+ pw_impl_device_set_implementation(impl->device,
+ (struct spa_device*)impl->resource);
+ pw_impl_device_register(impl->device, NULL);
+ impl->registered = true;
+ }
+}
+
+static const struct spa_device_events object_events = {
+ SPA_VERSION_DEVICE_EVENTS,
+ .info = device_info,
+};
+
+static void device_resource_destroy(void *data)
+{
+ struct impl *impl = data;
+
+ pw_log_debug("client-device %p: destroy", impl);
+
+ impl->resource = NULL;
+ spa_hook_remove(&impl->device_listener);
+ spa_hook_remove(&impl->resource_listener);
+ spa_hook_remove(&impl->object_listener);
+ pw_impl_device_destroy(impl->device);
+}
+
+static const struct pw_resource_events resource_events = {
+ PW_VERSION_RESOURCE_EVENTS,
+ .destroy = device_resource_destroy,
+};
+
+static void device_destroy(void *data)
+{
+ struct impl *impl = data;
+
+ pw_log_debug("client-device %p: destroy", impl);
+
+ impl->device = NULL;
+ spa_hook_remove(&impl->device_listener);
+ spa_hook_remove(&impl->resource_listener);
+ spa_hook_remove(&impl->object_listener);
+ pw_resource_destroy(impl->resource);
+}
+
+static void device_initialized(void *data)
+{
+ struct impl *impl = data;
+ struct pw_impl_device *device = impl->device;
+ struct pw_global *global = pw_impl_device_get_global(device);
+ uint32_t id = pw_global_get_id(global);
+
+ pw_log_debug("client-device %p: initialized global:%d", impl, id);
+ pw_resource_set_bound_id(impl->resource, id);
+}
+
+static const struct pw_impl_device_events device_events = {
+ PW_VERSION_IMPL_DEVICE_EVENTS,
+ .destroy = device_destroy,
+ .initialized = device_initialized,
+};
+
+struct pw_impl_device *pw_client_device_new(struct pw_resource *resource,
+ struct pw_properties *properties)
+{
+ struct impl *impl;
+ struct pw_impl_device *device;
+ struct pw_impl_client *client = pw_resource_get_client(resource);
+ struct pw_context *context = pw_impl_client_get_context(client);
+
+ if (properties == NULL)
+ properties = pw_properties_new(NULL, NULL);
+ if (properties == NULL)
+ return NULL;
+
+ pw_properties_setf(properties, PW_KEY_CLIENT_ID, "%d",
+ pw_impl_client_get_info(client)->id);
+
+ device = pw_context_create_device(context, properties, sizeof(struct impl));
+ if (device == NULL)
+ return NULL;
+
+ impl = pw_impl_device_get_user_data(device);
+ impl->device = device;
+ impl->context = context;
+ impl->resource = resource;
+
+ pw_impl_device_add_listener(impl->device,
+ &impl->device_listener,
+ &device_events, impl);
+
+ pw_resource_add_listener(impl->resource,
+ &impl->resource_listener,
+ &resource_events,
+ impl);
+ pw_resource_add_object_listener(impl->resource,
+ &impl->object_listener,
+ &object_events,
+ impl);
+
+ return device;
+}
diff --git a/src/modules/module-client-node.c b/src/modules/module-client-node.c
new file mode 100644
index 0000000..4dc9bf8
--- /dev/null
+++ b/src/modules/module-client-node.c
@@ -0,0 +1,229 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <errno.h>
+#include <dlfcn.h>
+
+#include "config.h"
+
+#include <spa/utils/result.h>
+
+#include <pipewire/impl.h>
+
+#include "module-client-node/v0/client-node.h"
+#include "module-client-node/client-node.h"
+
+/** \page page_module_client_node PipeWire Module: Client Node
+ */
+
+#define NAME "client-node"
+
+PW_LOG_TOPIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+static const struct spa_dict_item module_props[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Allow clients to create and control remote nodes" },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+struct pw_proxy *pw_core_node_export(struct pw_core *core,
+ const char *type, const struct spa_dict *props, void *object, size_t user_data_size);
+struct pw_proxy *pw_core_spa_node_export(struct pw_core *core,
+ const char *type, const struct spa_dict *props, void *object, size_t user_data_size);
+
+struct pw_protocol *pw_protocol_native_ext_client_node_init(struct pw_context *context);
+struct pw_protocol *pw_protocol_native_ext_client_node0_init(struct pw_context *context);
+
+struct factory_data {
+ struct pw_impl_factory *factory;
+ struct spa_hook factory_listener;
+
+ struct pw_impl_module *module;
+ struct spa_hook module_listener;
+
+ struct pw_export_type export_node;
+ struct pw_export_type export_spanode;
+};
+
+static void *create_object(void *_data,
+ struct pw_resource *resource,
+ const char *type,
+ uint32_t version,
+ struct pw_properties *properties,
+ uint32_t new_id)
+{
+ void *result;
+ struct pw_resource *node_resource;
+ struct pw_impl_client *client;
+ int res;
+
+ if (resource == NULL) {
+ res = -EINVAL;
+ goto error_exit;
+ }
+
+ client = pw_resource_get_client(resource);
+ node_resource = pw_resource_new(client, new_id, PW_PERM_ALL, type, version, 0);
+ if (node_resource == NULL) {
+ res = -errno;
+ goto error_resource;
+ }
+
+ if (version == 0) {
+ result = pw_impl_client_node0_new(node_resource, properties);
+ } else {
+ result = pw_impl_client_node_new(node_resource, properties, true);
+ }
+ if (result == NULL) {
+ res = -errno;
+ goto error_node;
+ }
+ return result;
+
+error_resource:
+ pw_log_error("can't create resource: %s", spa_strerror(res));
+ pw_resource_errorf_id(resource, new_id, res, "can't create resource: %s", spa_strerror(res));
+ goto error_exit;
+error_node:
+ pw_log_error("can't create node: %s", spa_strerror(res));
+ pw_resource_errorf_id(resource, new_id, res, "can't create node: %s", spa_strerror(res));
+ goto error_exit;
+error_exit:
+ errno = -res;
+ return NULL;
+}
+
+static const struct pw_impl_factory_implementation impl_factory = {
+ PW_VERSION_IMPL_FACTORY_IMPLEMENTATION,
+ .create_object = create_object,
+};
+
+static void factory_destroy(void *data)
+{
+ struct factory_data *d = data;
+ spa_hook_remove(&d->factory_listener);
+ d->factory = NULL;
+ if (d->module)
+ pw_impl_module_destroy(d->module);
+}
+
+static const struct pw_impl_factory_events factory_events = {
+ PW_VERSION_IMPL_FACTORY_EVENTS,
+ .destroy = factory_destroy,
+};
+
+static void module_destroy(void *data)
+{
+ struct factory_data *d = data;
+
+ spa_hook_remove(&d->module_listener);
+ spa_list_remove(&d->export_node.link);
+ spa_list_remove(&d->export_spanode.link);
+
+ d->module = NULL;
+ if (d->factory)
+ pw_impl_factory_destroy(d->factory);
+}
+
+static void module_registered(void *data)
+{
+ struct factory_data *d = data;
+ struct pw_impl_module *module = d->module;
+ struct pw_impl_factory *factory = d->factory;
+ struct spa_dict_item items[1];
+ char id[16];
+ int res;
+
+ snprintf(id, sizeof(id), "%d", pw_global_get_id(pw_impl_module_get_global(module)));
+ items[0] = SPA_DICT_ITEM_INIT(PW_KEY_MODULE_ID, id);
+ pw_impl_factory_update_properties(factory, &SPA_DICT_INIT(items, 1));
+
+ if ((res = pw_impl_factory_register(factory, NULL)) < 0) {
+ pw_log_error("%p: can't register factory: %s", factory, spa_strerror(res));
+ }
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy,
+ .registered = module_registered,
+};
+
+SPA_EXPORT
+int pipewire__module_init(struct pw_impl_module *module, const char *args)
+{
+ struct pw_context *context = pw_impl_module_get_context(module);
+ struct pw_impl_factory *factory;
+ struct factory_data *data;
+ int res;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ factory = pw_context_create_factory(context,
+ "client-node",
+ PW_TYPE_INTERFACE_ClientNode,
+ PW_VERSION_CLIENT_NODE,
+ NULL,
+ sizeof(*data));
+ if (factory == NULL)
+ return -errno;
+
+ data = pw_impl_factory_get_user_data(factory);
+ data->factory = factory;
+ data->module = module;
+
+ pw_log_debug("module %p: new", module);
+
+ pw_impl_factory_set_implementation(factory,
+ &impl_factory,
+ data);
+
+ data->export_node.type = PW_TYPE_INTERFACE_Node;
+ data->export_node.func = pw_core_node_export;
+ if ((res = pw_context_register_export_type(context, &data->export_node)) < 0)
+ goto error;
+
+ data->export_spanode.type = SPA_TYPE_INTERFACE_Node;
+ data->export_spanode.func = pw_core_spa_node_export;
+ if ((res = pw_context_register_export_type(context, &data->export_spanode)) < 0)
+ goto error_remove;
+
+ pw_protocol_native_ext_client_node_init(context);
+ pw_protocol_native_ext_client_node0_init(context);
+
+ pw_impl_factory_add_listener(factory, &data->factory_listener, &factory_events, data);
+ pw_impl_module_add_listener(module, &data->module_listener, &module_events, data);
+
+ pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_props));
+
+ return 0;
+error_remove:
+ spa_list_remove(&data->export_node.link);
+error:
+ pw_impl_factory_destroy(data->factory);
+ return res;
+}
diff --git a/src/modules/module-client-node/client-node.c b/src/modules/module-client-node/client-node.c
new file mode 100644
index 0000000..d5c133a
--- /dev/null
+++ b/src/modules/module-client-node/client-node.c
@@ -0,0 +1,1777 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <errno.h>
+#include <unistd.h>
+#include <time.h>
+
+#include <spa/support/system.h>
+#include <spa/node/node.h>
+#include <spa/node/utils.h>
+#include <spa/pod/filter.h>
+#include <spa/pod/dynamic.h>
+#include <spa/pod/parser.h>
+#include <spa/debug/types.h>
+
+#include <pipewire/pipewire.h>
+#include "pipewire/private.h"
+
+#include "modules/spa/spa-node.h"
+#include "client-node.h"
+
+PW_LOG_TOPIC_EXTERN(mod_topic);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+/** \cond */
+
+#define MAX_BUFFERS 64
+#define MAX_METAS 16u
+#define MAX_DATAS 64u
+#define MAX_AREAS 2048
+
+#define CHECK_FREE_PORT(this,d,p) (p <= pw_map_get_size(&this->ports[d]) && !CHECK_PORT(this,d,p))
+#define CHECK_PORT(this,d,p) (pw_map_lookup(&this->ports[d], p) != NULL)
+#define GET_PORT(this,d,p) (pw_map_lookup(&this->ports[d], p))
+
+#define CHECK_PORT_BUFFER(this,b,p) (b < p->n_buffers)
+
+struct buffer {
+ struct spa_buffer *outbuf;
+ struct spa_buffer buffer;
+ struct spa_meta metas[MAX_METAS];
+ struct spa_data datas[MAX_DATAS];
+ struct pw_memblock *mem;
+};
+
+struct mix {
+ unsigned int valid:1;
+ uint32_t id;
+ struct port *port;
+ uint32_t peer_id;
+ uint32_t n_buffers;
+ struct buffer buffers[MAX_BUFFERS];
+};
+
+struct params {
+ uint32_t n_params;
+ struct spa_pod **params;
+};
+
+struct port {
+ struct pw_impl_port *port;
+ struct node *node;
+ struct impl *impl;
+
+ enum spa_direction direction;
+ uint32_t id;
+
+ struct spa_node mix_node;
+
+ struct spa_port_info info;
+ struct pw_properties *properties;
+
+ struct params params;
+
+ unsigned int removed:1;
+ unsigned int destroyed:1;
+
+ struct pw_array mix;
+};
+
+struct node {
+ struct spa_node node;
+
+ struct impl *impl;
+
+ struct spa_log *log;
+ struct spa_loop *data_loop;
+ struct spa_system *data_system;
+
+ struct spa_hook_list hooks;
+ struct spa_callbacks callbacks;
+
+ struct pw_resource *resource;
+ struct pw_impl_client *client;
+
+ struct spa_source data_source;
+ int writefd;
+
+ struct pw_map ports[2];
+
+ struct port dummy;
+
+ struct params params;
+};
+
+struct impl {
+ struct pw_impl_client_node this;
+
+ struct pw_context *context;
+
+ struct node node;
+
+ struct pw_map io_map;
+ struct pw_memblock *io_areas;
+
+ struct pw_memblock *activation;
+
+ struct spa_hook node_listener;
+ struct spa_hook resource_listener;
+ struct spa_hook object_listener;
+
+ uint32_t node_id;
+
+ uint32_t bind_node_version;
+ uint32_t bind_node_id;
+
+ int fds[2];
+ int other_fds[2];
+};
+
+#define pw_client_node_resource(r,m,v,...) \
+ pw_resource_call_res(r,struct pw_client_node_events,m,v,__VA_ARGS__)
+
+#define pw_client_node_resource_transport(r,...) \
+ pw_client_node_resource(r,transport,0,__VA_ARGS__)
+#define pw_client_node_resource_set_param(r,...) \
+ pw_client_node_resource(r,set_param,0,__VA_ARGS__)
+#define pw_client_node_resource_set_io(r,...) \
+ pw_client_node_resource(r,set_io,0,__VA_ARGS__)
+#define pw_client_node_resource_event(r,...) \
+ pw_client_node_resource(r,event,0,__VA_ARGS__)
+#define pw_client_node_resource_command(r,...) \
+ pw_client_node_resource(r,command,0,__VA_ARGS__)
+#define pw_client_node_resource_add_port(r,...) \
+ pw_client_node_resource(r,add_port,0,__VA_ARGS__)
+#define pw_client_node_resource_remove_port(r,...) \
+ pw_client_node_resource(r,remove_port,0,__VA_ARGS__)
+#define pw_client_node_resource_port_set_param(r,...) \
+ pw_client_node_resource(r,port_set_param,0,__VA_ARGS__)
+#define pw_client_node_resource_port_use_buffers(r,...) \
+ pw_client_node_resource(r,port_use_buffers,0,__VA_ARGS__)
+#define pw_client_node_resource_port_set_io(r,...) \
+ pw_client_node_resource(r,port_set_io,0,__VA_ARGS__)
+#define pw_client_node_resource_set_activation(r,...) \
+ pw_client_node_resource(r,set_activation,0,__VA_ARGS__)
+#define pw_client_node_resource_port_set_mix_info(r,...) \
+ pw_client_node_resource(r,port_set_mix_info,1,__VA_ARGS__)
+
+static int update_params(struct params *p, uint32_t n_params, const struct spa_pod **params)
+{
+ uint32_t i;
+ for (i = 0; i < p->n_params; i++)
+ free(p->params[i]);
+ p->n_params = n_params;
+ if (p->n_params == 0) {
+ free(p->params);
+ p->params = NULL;
+ } else {
+ struct spa_pod **np;
+ np = pw_reallocarray(p->params, p->n_params, sizeof(struct spa_pod *));
+ if (np == NULL) {
+ pw_log_error("%p: can't realloc: %m", p);
+ free(p->params);
+ p->params = NULL;
+ p->n_params = 0;
+ return -errno;
+ }
+ p->params = np;
+ }
+ for (i = 0; i < p->n_params; i++)
+ p->params[i] = params[i] ? spa_pod_copy(params[i]) : NULL;
+ return 0;
+}
+
+static int
+do_port_use_buffers(struct impl *impl,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t mix_id,
+ uint32_t flags,
+ struct spa_buffer **buffers,
+ uint32_t n_buffers);
+
+/** \endcond */
+
+static struct mix *find_mix(struct port *p, uint32_t mix_id)
+{
+ struct mix *mix;
+ size_t len;
+
+ if (mix_id == SPA_ID_INVALID)
+ mix_id = 0;
+ else
+ mix_id++;
+
+ len = pw_array_get_len(&p->mix, struct mix);
+ if (mix_id >= len) {
+ size_t need = sizeof(struct mix) * (mix_id + 1 - len);
+ void *ptr = pw_array_add(&p->mix, need);
+ if (ptr == NULL)
+ return NULL;
+ memset(ptr, 0, need);
+ }
+ mix = pw_array_get_unchecked(&p->mix, mix_id, struct mix);
+ return mix;
+}
+
+static void mix_init(struct mix *mix, struct port *p, uint32_t id)
+{
+ mix->valid = true;
+ mix->id = id;
+ mix->port = p;
+ mix->n_buffers = 0;
+}
+
+static struct mix *ensure_mix(struct impl *impl, struct port *p, uint32_t mix_id)
+{
+ struct mix *mix;
+
+ if ((mix = find_mix(p, mix_id)) == NULL)
+ return NULL;
+ if (mix->valid)
+ return mix;
+ mix_init(mix, p, mix_id);
+ return mix;
+}
+
+static void clear_data(struct node *this, struct spa_data *d)
+{
+ struct impl *impl = this->impl;
+
+ switch (d->type) {
+ case SPA_DATA_MemId:
+ {
+ uint32_t id;
+ struct pw_memblock *m;
+
+ id = SPA_PTR_TO_UINT32(d->data);
+ m = pw_mempool_find_id(this->client->pool, id);
+ if (m) {
+ pw_log_debug("%p: mem %d", impl, m->id);
+ pw_memblock_unref(m);
+ }
+ break;
+ }
+ case SPA_DATA_MemFd:
+ case SPA_DATA_DmaBuf:
+ pw_log_debug("%p: close fd:%d", impl, (int)d->fd);
+ close(d->fd);
+ break;
+ default:
+ break;
+ }
+}
+
+static int clear_buffers(struct node *this, struct mix *mix)
+{
+ uint32_t i, j;
+
+ for (i = 0; i < mix->n_buffers; i++) {
+ struct buffer *b = &mix->buffers[i];
+
+ spa_log_debug(this->log, "%p: clear buffer %d", this, i);
+
+ for (j = 0; j < b->buffer.n_datas; j++) {
+ struct spa_data *d = &b->datas[j];
+ clear_data(this, d);
+ }
+ pw_memblock_unref(b->mem);
+ }
+ mix->n_buffers = 0;
+ return 0;
+}
+
+static void mix_clear(struct node *this, struct mix *mix)
+{
+ struct port *port = mix->port;
+
+ if (!mix->valid)
+ return;
+ do_port_use_buffers(this->impl, port->direction, port->id,
+ mix->id, 0, NULL, 0);
+ mix->valid = false;
+}
+
+static int impl_node_enum_params(void *object, int seq,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+ struct node *this = object;
+ uint8_t buffer[1024];
+ struct spa_pod_dynamic_builder b;
+ struct spa_result_node_params result;
+ uint32_t count = 0;
+ bool found = false;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(num != 0, -EINVAL);
+
+ result.id = id;
+ result.next = 0;
+
+ while (true) {
+ struct spa_pod *param;
+
+ result.index = result.next++;
+ if (result.index >= this->params.n_params)
+ break;
+
+ param = this->params.params[result.index];
+
+ if (param == NULL || !spa_pod_is_object_id(param, id))
+ continue;
+
+ found = true;
+
+ if (result.index < start)
+ continue;
+
+ spa_pod_dynamic_builder_init(&b, buffer, sizeof(buffer), 4096);
+ if (spa_pod_filter(&b.b, &result.param, param, filter) == 0) {
+ pw_log_debug("%p: %d param %u", this, seq, result.index);
+ spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+ count++;
+ }
+ spa_pod_dynamic_builder_clean(&b);
+
+ if (count == num)
+ break;
+ }
+ return found ? 0 : -ENOENT;
+}
+
+static int impl_node_set_param(void *object, uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct node *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ if (this->resource == NULL)
+ return param == NULL ? 0 : -EIO;
+
+ return pw_client_node_resource_set_param(this->resource, id, flags, param);
+}
+
+static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size)
+{
+ struct node *this = object;
+ struct impl *impl = this->impl;
+ struct pw_memmap *mm, *old;
+ uint32_t memid, mem_offset, mem_size;
+ uint32_t tag[5] = { impl->node_id, id, };
+
+ if (impl->this.flags & 1)
+ return 0;
+
+ old = pw_mempool_find_tag(this->client->pool, tag, sizeof(tag));
+
+ if (data) {
+ mm = pw_mempool_import_map(this->client->pool,
+ impl->context->pool, data, size, tag);
+ if (mm == NULL)
+ return -errno;
+
+ mem_offset = mm->offset;
+ memid = mm->block->id;
+ mem_size = size;
+ }
+ else {
+ memid = SPA_ID_INVALID;
+ mem_offset = mem_size = 0;
+ }
+ pw_memmap_free(old);
+
+ if (this->resource == NULL)
+ return data == NULL ? 0 : -EIO;
+
+ return pw_client_node_resource_set_io(this->resource,
+ id,
+ memid,
+ mem_offset, mem_size);
+}
+
+static int impl_node_send_command(void *object, const struct spa_command *command)
+{
+ struct node *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(command != NULL, -EINVAL);
+
+ pw_log_debug("%p: send command %d", this, SPA_COMMAND_TYPE(command));
+
+ if (this->resource == NULL)
+ return -EIO;
+
+ return pw_client_node_resource_command(this->resource, command);
+}
+
+
+static void emit_port_info(struct node *this, struct port *port)
+{
+ spa_node_emit_port_info(&this->hooks,
+ port->direction, port->id, &port->info);
+}
+
+static int impl_node_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct spa_node_events *events,
+ void *data)
+{
+ struct node *this = object;
+ struct spa_hook_list save;
+ union pw_map_item *item;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_hook_list_isolate(&this->hooks, &save, listener, events, data);
+
+ pw_array_for_each(item, &this->ports[SPA_DIRECTION_INPUT].items) {
+ if (item->data)
+ emit_port_info(this, item->data);
+ }
+ pw_array_for_each(item, &this->ports[SPA_DIRECTION_OUTPUT].items) {
+ if (item->data)
+ emit_port_info(this, item->data);
+ }
+ spa_hook_list_join(&this->hooks, &save);
+
+ return 0;
+}
+
+static int
+impl_node_set_callbacks(void *object,
+ const struct spa_node_callbacks *callbacks,
+ void *data)
+{
+ struct node *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ this->callbacks = SPA_CALLBACKS_INIT(callbacks, data);
+
+ return 0;
+}
+
+static int
+impl_node_sync(void *object, int seq)
+{
+ struct node *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ pw_log_debug("%p: sync", this);
+
+ if (this->resource == NULL)
+ return -EIO;
+
+ return pw_resource_ping(this->resource, seq);
+}
+
+static void
+do_update_port(struct node *this,
+ struct port *port,
+ uint32_t change_mask,
+ uint32_t n_params,
+ const struct spa_pod **params,
+ const struct spa_port_info *info)
+{
+ if (change_mask & PW_CLIENT_NODE_PORT_UPDATE_PARAMS) {
+ spa_log_debug(this->log, "%p: port %u update %d params", this, port->id, n_params);
+ update_params(&port->params, n_params, params);
+ }
+
+ if (change_mask & PW_CLIENT_NODE_PORT_UPDATE_INFO) {
+ pw_properties_free(port->properties);
+ port->properties = NULL;
+ port->info.props = NULL;
+ port->info.n_params = 0;
+ port->info.params = NULL;
+
+ if (info) {
+ port->info = *info;
+ if (info->props) {
+ port->properties = pw_properties_new_dict(info->props);
+ port->info.props = &port->properties->dict;
+ }
+ port->info.n_params = 0;
+ port->info.params = NULL;
+ spa_node_emit_port_info(&this->hooks, port->direction, port->id, info);
+ }
+ }
+}
+
+static void
+clear_port(struct node *this, struct port *port)
+{
+ struct mix *mix;
+
+ spa_log_debug(this->log, "%p: clear port %p", this, port);
+
+ do_update_port(this, port,
+ PW_CLIENT_NODE_PORT_UPDATE_PARAMS |
+ PW_CLIENT_NODE_PORT_UPDATE_INFO, 0, NULL, NULL);
+
+ pw_array_for_each(mix, &port->mix)
+ mix_clear(this, mix);
+ pw_array_clear(&port->mix);
+ pw_array_init(&port->mix, sizeof(struct mix) * 2);
+
+ pw_map_insert_at(&this->ports[port->direction], port->id, NULL);
+
+ if (!port->removed)
+ spa_node_emit_port_info(&this->hooks, port->direction, port->id, NULL);
+}
+
+static int
+impl_node_add_port(void *object, enum spa_direction direction, uint32_t port_id,
+ const struct spa_dict *props)
+{
+ struct node *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(CHECK_FREE_PORT(this, direction, port_id), -EINVAL);
+
+ if (this->resource == NULL)
+ return -EIO;
+
+ return pw_client_node_resource_add_port(this->resource, direction, port_id, props);
+}
+
+static int
+impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_id)
+{
+ struct node *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ if (this->resource == NULL)
+ return -EIO;
+
+ return pw_client_node_resource_remove_port(this->resource, direction, port_id);
+}
+
+static int
+impl_node_port_enum_params(void *object, int seq,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+ struct node *this = object;
+ struct port *port;
+ uint8_t buffer[1024];
+ struct spa_pod_dynamic_builder b;
+ struct spa_result_node_params result;
+ uint32_t count = 0;
+ bool found = false;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(num != 0, -EINVAL);
+
+ port = GET_PORT(this, direction, port_id);
+ spa_return_val_if_fail(port != NULL, -EINVAL);
+
+ pw_log_debug("%p: seq:%d port %d.%d id:%u start:%u num:%u n_params:%d",
+ this, seq, direction, port_id, id, start, num, port->params.n_params);
+
+ result.id = id;
+ result.next = 0;
+
+ while (true) {
+ struct spa_pod *param;
+
+ result.index = result.next++;
+ if (result.index >= port->params.n_params)
+ break;
+
+ param = port->params.params[result.index];
+
+ if (param == NULL || !spa_pod_is_object_id(param, id))
+ continue;
+
+ found = true;
+
+ if (result.index < start)
+ continue;
+
+ spa_pod_dynamic_builder_init(&b, buffer, sizeof(buffer), 4096);
+ if (spa_pod_filter(&b.b, &result.param, param, filter) == 0) {
+ pw_log_debug("%p: %d param %u", this, seq, result.index);
+ spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+ count++;
+ }
+ spa_pod_dynamic_builder_clean(&b);
+
+ if (count == num)
+ break;
+ }
+ return found ? 0 : -ENOENT;
+}
+
+static int
+impl_node_port_set_param(void *object,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct node *this = object;
+ struct port *port;
+ struct mix *mix;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ port = GET_PORT(this, direction, port_id);
+ if(port == NULL)
+ return param == NULL ? 0 : -EINVAL;
+
+ pw_log_debug("%p: port %d.%d set param %s %d", this,
+ direction, port_id,
+ spa_debug_type_find_name(spa_type_param, id), id);
+
+ if (id == SPA_PARAM_Format) {
+ pw_array_for_each(mix, &port->mix)
+ clear_buffers(this, mix);
+ }
+ if (this->resource == NULL)
+ return param == NULL ? 0 : -EIO;
+
+ return pw_client_node_resource_port_set_param(this->resource,
+ direction, port_id,
+ id, flags,
+ param);
+}
+
+static int do_port_set_io(struct impl *impl,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t mix_id,
+ uint32_t id, void *data, size_t size)
+{
+ struct node *this = &impl->node;
+ uint32_t memid, mem_offset, mem_size;
+ struct port *port;
+ struct mix *mix;
+ uint32_t tag[5] = { impl->node_id, direction, port_id, mix_id, id };
+ struct pw_memmap *mm, *old;
+
+ pw_log_debug("%p: %s port %d.%d set io %p %zd", this,
+ direction == SPA_DIRECTION_INPUT ? "input" : "output",
+ port_id, mix_id, data, size);
+
+ port = GET_PORT(this, direction, port_id);
+ if (port == NULL)
+ return data == NULL ? 0 : -EINVAL;
+
+ if ((mix = find_mix(port, mix_id)) == NULL || !mix->valid)
+ return -EINVAL;
+
+ old = pw_mempool_find_tag(this->client->pool, tag, sizeof(tag));
+
+ if (data) {
+ mm = pw_mempool_import_map(this->client->pool,
+ impl->context->pool, data, size, tag);
+ if (mm == NULL)
+ return -errno;
+
+ mem_offset = mm->offset;
+ memid = mm->block->id;
+ mem_size = size;
+ }
+ else {
+ memid = SPA_ID_INVALID;
+ mem_offset = mem_size = 0;
+ }
+ pw_memmap_free(old);
+
+ if (this->resource == NULL)
+ return data == NULL ? 0 : -EIO;
+
+ return pw_client_node_resource_port_set_io(this->resource,
+ direction, port_id,
+ mix_id,
+ id,
+ memid,
+ mem_offset, mem_size);
+}
+
+static int
+impl_node_port_set_io(void *object,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t id,
+ void *data, size_t size)
+{
+ /* ignore io on the node itself, we only care about the io on the
+ * port mixers, the io on the node ports itself is handled on the
+ * client side */
+ return -EINVAL;
+}
+
+static int
+do_port_use_buffers(struct impl *impl,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t mix_id,
+ uint32_t flags,
+ struct spa_buffer **buffers,
+ uint32_t n_buffers)
+{
+ struct node *this = &impl->node;
+ struct port *p;
+ struct mix *mix;
+ uint32_t i, j;
+ struct pw_client_node_buffer *mb;
+
+ p = GET_PORT(this, direction, port_id);
+ if (p == NULL)
+ return n_buffers == 0 ? 0 : -EINVAL;
+
+ if (n_buffers > MAX_BUFFERS)
+ return -ENOSPC;
+
+ spa_log_debug(this->log, "%p: %s port %d.%d use buffers %p %u flags:%08x", this,
+ direction == SPA_DIRECTION_INPUT ? "input" : "output",
+ port_id, mix_id, buffers, n_buffers, flags);
+
+ if ((mix = find_mix(p, mix_id)) == NULL || !mix->valid)
+ return -EINVAL;
+
+ if (direction == SPA_DIRECTION_OUTPUT) {
+ mix_id = SPA_ID_INVALID;
+ if ((mix = find_mix(p, mix_id)) == NULL || !mix->valid)
+ return -EINVAL;
+ }
+
+ clear_buffers(this, mix);
+
+ if (n_buffers > 0) {
+ mb = alloca(n_buffers * sizeof(struct pw_client_node_buffer));
+ } else {
+ mb = NULL;
+ }
+
+ if (this->resource == NULL)
+ return n_buffers == 0 ? 0 : -EIO;
+
+ if (p->destroyed)
+ return 0;
+
+ for (i = 0; i < n_buffers; i++) {
+ struct buffer *b = &mix->buffers[i];
+ struct pw_memblock *mem, *m;
+ void *baseptr, *endptr;
+
+ b->outbuf = buffers[i];
+ memcpy(&b->buffer, buffers[i], sizeof(struct spa_buffer));
+ b->buffer.datas = b->datas;
+ b->buffer.metas = b->metas;
+
+ if (buffers[i]->n_metas > 0)
+ baseptr = buffers[i]->metas[0].data;
+ else if (buffers[i]->n_datas > 0)
+ baseptr = buffers[i]->datas[0].chunk;
+ else
+ return -EINVAL;
+
+ if ((mem = pw_mempool_find_ptr(impl->context->pool, baseptr)) == NULL)
+ return -EINVAL;
+
+ endptr = SPA_PTROFF(baseptr, buffers[i]->n_datas * sizeof(struct spa_chunk), void);
+ for (j = 0; j < buffers[i]->n_metas; j++) {
+ endptr = SPA_PTROFF(endptr, SPA_ROUND_UP_N(buffers[i]->metas[j].size, 8), void);
+ }
+ for (j = 0; j < buffers[i]->n_datas; j++) {
+ struct spa_data *d = &buffers[i]->datas[j];
+ if (d->type == SPA_DATA_MemPtr) {
+ if ((m = pw_mempool_find_ptr(impl->context->pool, d->data)) == NULL ||
+ m != mem)
+ return -EINVAL;
+ endptr = SPA_MAX(endptr, SPA_PTROFF(d->data, d->maxsize, void));
+ }
+ }
+ if (endptr > SPA_PTROFF(baseptr, mem->size, void))
+ return -EINVAL;
+
+ m = pw_mempool_import_block(this->client->pool, mem);
+ if (m == NULL)
+ return -errno;
+
+ b->mem = m;
+
+ mb[i].buffer = &b->buffer;
+ mb[i].mem_id = m->id;
+ mb[i].offset = SPA_PTRDIFF(baseptr, mem->map->ptr);
+ mb[i].size = SPA_PTRDIFF(endptr, baseptr);
+ spa_log_debug(this->log, "%p: buffer %d %d %d %d", this, i, mb[i].mem_id,
+ mb[i].offset, mb[i].size);
+
+ b->buffer.n_metas = SPA_MIN(buffers[i]->n_metas, MAX_METAS);
+ for (j = 0; j < b->buffer.n_metas; j++)
+ memcpy(&b->buffer.metas[j], &buffers[i]->metas[j], sizeof(struct spa_meta));
+
+ b->buffer.n_datas = SPA_MIN(buffers[i]->n_datas, MAX_DATAS);
+ for (j = 0; j < b->buffer.n_datas; j++) {
+ struct spa_data *d = &buffers[i]->datas[j];
+
+ memcpy(&b->datas[j], d, sizeof(struct spa_data));
+
+ if (flags & SPA_NODE_BUFFERS_FLAG_ALLOC)
+ continue;
+
+ switch (d->type) {
+ case SPA_DATA_DmaBuf:
+ case SPA_DATA_MemFd:
+ {
+ uint32_t flags = PW_MEMBLOCK_FLAG_DONT_CLOSE;
+
+ if (d->flags & SPA_DATA_FLAG_READABLE)
+ flags |= PW_MEMBLOCK_FLAG_READABLE;
+ if (d->flags & SPA_DATA_FLAG_WRITABLE)
+ flags |= PW_MEMBLOCK_FLAG_WRITABLE;
+
+ spa_log_debug(this->log, "mem %d type:%d fd:%d", j, d->type, (int)d->fd);
+ m = pw_mempool_import(this->client->pool,
+ flags, d->type, d->fd);
+ if (m == NULL)
+ return -errno;
+
+ b->datas[j].type = SPA_DATA_MemId;
+ b->datas[j].data = SPA_UINT32_TO_PTR(m->id);
+ break;
+ }
+ case SPA_DATA_MemPtr:
+ spa_log_debug(this->log, "mem %d %zd", j, SPA_PTRDIFF(d->data, baseptr));
+ b->datas[j].data = SPA_INT_TO_PTR(SPA_PTRDIFF(d->data, baseptr));
+ break;
+ default:
+ b->datas[j].type = SPA_ID_INVALID;
+ b->datas[j].data = NULL;
+ spa_log_error(this->log, "invalid memory type %d", d->type);
+ break;
+ }
+ }
+ }
+ mix->n_buffers = n_buffers;
+
+ return pw_client_node_resource_port_use_buffers(this->resource,
+ direction, port_id, mix_id, flags,
+ n_buffers, mb);
+}
+
+static int
+impl_node_port_use_buffers(void *object,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t flags,
+ struct spa_buffer **buffers,
+ uint32_t n_buffers)
+{
+ struct node *this = object;
+ struct impl *impl;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ impl = this->impl;
+
+ return do_port_use_buffers(impl, direction, port_id,
+ SPA_ID_INVALID, flags, buffers, n_buffers);
+}
+
+static int
+impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id)
+{
+ struct node *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(CHECK_PORT(this, SPA_DIRECTION_OUTPUT, port_id), -EINVAL);
+
+ spa_log_trace_fp(this->log, "reuse buffer %d", buffer_id);
+
+ return -ENOTSUP;
+}
+
+static int impl_node_process(void *object)
+{
+ struct node *this = object;
+ struct impl *impl = this->impl;
+ struct pw_impl_node *n = impl->this.node;
+ struct timespec ts;
+
+ spa_log_trace_fp(this->log, "%p: send process driver:%p", this, impl->this.node->driver_node);
+
+ if (SPA_UNLIKELY(spa_system_clock_gettime(this->data_system, CLOCK_MONOTONIC, &ts) < 0))
+ spa_zero(ts);
+
+ n->rt.activation->status = PW_NODE_ACTIVATION_TRIGGERED;
+ n->rt.activation->signal_time = SPA_TIMESPEC_TO_NSEC(&ts);
+
+ if (SPA_UNLIKELY(spa_system_eventfd_write(this->data_system, this->writefd, 1) < 0))
+ spa_log_warn(this->log, "%p: error %m", this);
+
+ return SPA_STATUS_OK;
+}
+
+static struct pw_node *
+client_node_get_node(void *data,
+ uint32_t version,
+ size_t user_data_size)
+{
+ struct impl *impl = data;
+ struct node *this = &impl->node;
+ uint32_t new_id = user_data_size;
+
+ pw_log_debug("%p: bind %u/%u", this, new_id, version);
+
+ impl->bind_node_version = version;
+ impl->bind_node_id = new_id;
+ pw_map_insert_at(&this->client->objects, new_id, NULL);
+
+ return NULL;
+}
+
+static int
+client_node_update(void *data,
+ uint32_t change_mask,
+ uint32_t n_params,
+ const struct spa_pod **params,
+ const struct spa_node_info *info)
+{
+ struct impl *impl = data;
+ struct node *this = &impl->node;
+
+ if (change_mask & PW_CLIENT_NODE_UPDATE_PARAMS) {
+ pw_log_debug("%p: update %d params", this, n_params);
+ update_params(&this->params, n_params, params);
+ }
+ if (change_mask & PW_CLIENT_NODE_UPDATE_INFO) {
+ spa_node_emit_info(&this->hooks, info);
+ }
+ pw_log_debug("%p: got node update", this);
+ return 0;
+}
+
+static int
+client_node_port_update(void *data,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t change_mask,
+ uint32_t n_params,
+ const struct spa_pod **params,
+ const struct spa_port_info *info)
+{
+ struct impl *impl = data;
+ struct node *this = &impl->node;
+ struct port *port;
+ bool remove;
+
+ spa_log_debug(this->log, "%p: got port update change:%08x params:%d",
+ this, change_mask, n_params);
+
+ remove = (change_mask == 0);
+
+ port = GET_PORT(this, direction, port_id);
+
+ if (remove) {
+ if (port == NULL)
+ return 0;
+ port->destroyed = true;
+ clear_port(this, port);
+ } else {
+ struct port *target;
+
+ if (port == NULL) {
+ if (!CHECK_FREE_PORT(this, direction, port_id))
+ return -EINVAL;
+
+ target = &this->dummy;
+ spa_zero(this->dummy);
+ target->direction = direction;
+ target->id = port_id;
+ } else
+ target = port;
+
+ do_update_port(this,
+ target,
+ change_mask,
+ n_params, params,
+ info);
+ }
+ return 0;
+}
+
+static int client_node_set_active(void *data, bool active)
+{
+ struct impl *impl = data;
+ struct node *this = &impl->node;
+ spa_log_debug(this->log, "%p: active:%d", this, active);
+ return pw_impl_node_set_active(impl->this.node, active);
+}
+
+static int client_node_event(void *data, const struct spa_event *event)
+{
+ struct impl *impl = data;
+ struct node *this = &impl->node;
+ spa_node_emit_event(&this->hooks, event);
+ return 0;
+}
+
+static int client_node_port_buffers(void *data,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t mix_id,
+ uint32_t n_buffers,
+ struct spa_buffer **buffers)
+{
+ struct impl *impl = data;
+ struct node *this = &impl->node;
+ struct port *p;
+ struct mix *mix;
+ uint32_t i, j;
+
+ spa_log_debug(this->log, "%p: %s port %d.%d buffers %p %u", this,
+ direction == SPA_DIRECTION_INPUT ? "input" : "output",
+ port_id, mix_id, buffers, n_buffers);
+
+ p = GET_PORT(this, direction, port_id);
+ spa_return_val_if_fail(p != NULL, -EINVAL);
+
+ if (direction == SPA_DIRECTION_OUTPUT)
+ mix_id = SPA_ID_INVALID;
+
+ if ((mix = find_mix(p, mix_id)) == NULL || !mix->valid)
+ return -EINVAL;
+
+ if (mix->n_buffers != n_buffers)
+ return -EINVAL;
+
+ for (i = 0; i < n_buffers; i++) {
+ struct spa_buffer *oldbuf, *newbuf;
+ struct buffer *b = &mix->buffers[i];
+
+ oldbuf = b->outbuf;
+ newbuf = buffers[i];
+
+ spa_log_debug(this->log, "buffer %d n_datas:%d", i, newbuf->n_datas);
+
+ if (oldbuf->n_datas != newbuf->n_datas)
+ return -EINVAL;
+
+ for (j = 0; j < b->buffer.n_datas; j++) {
+ struct spa_chunk *oldchunk = oldbuf->datas[j].chunk;
+ struct spa_data *d = &newbuf->datas[j];
+
+ /* overwrite everything except the chunk */
+ oldbuf->datas[j] = *d;
+ oldbuf->datas[j].chunk = oldchunk;
+
+ b->datas[j].type = d->type;
+ b->datas[j].fd = d->fd;
+
+ spa_log_debug(this->log, " data %d type:%d fl:%08x fd:%d, offs:%d max:%d",
+ j, d->type, d->flags, (int) d->fd, d->mapoffset,
+ d->maxsize);
+ }
+ }
+ mix->n_buffers = n_buffers;
+
+ return 0;
+}
+
+static const struct pw_client_node_methods client_node_methods = {
+ PW_VERSION_CLIENT_NODE_METHODS,
+ .get_node = client_node_get_node,
+ .update = client_node_update,
+ .port_update = client_node_port_update,
+ .set_active = client_node_set_active,
+ .event = client_node_event,
+ .port_buffers = client_node_port_buffers,
+};
+
+static void node_on_data_fd_events(struct spa_source *source)
+{
+ struct node *this = source->data;
+
+ if (source->rmask & (SPA_IO_ERR | SPA_IO_HUP)) {
+ spa_log_warn(this->log, "%p: got error", this);
+ return;
+ }
+
+ if (source->rmask & SPA_IO_IN) {
+ uint64_t cmd;
+ struct pw_impl_node *node = this->impl->this.node;
+
+ if (SPA_UNLIKELY(spa_system_eventfd_read(this->data_system,
+ this->data_source.fd, &cmd) < 0))
+ pw_log_warn("%p: read failed %m", this);
+ else if (SPA_UNLIKELY(cmd > 1))
+ pw_log_info("(%s-%u) client missed %"PRIu64" wakeups",
+ node->name, node->info.id, cmd - 1);
+
+ spa_log_trace_fp(this->log, "%p: got ready", this);
+ spa_node_call_ready(&this->callbacks, SPA_STATUS_HAVE_DATA);
+ }
+}
+
+static const struct spa_node_methods impl_node = {
+ SPA_VERSION_NODE_METHODS,
+ .add_listener = impl_node_add_listener,
+ .set_callbacks = impl_node_set_callbacks,
+ .sync = impl_node_sync,
+ .enum_params = impl_node_enum_params,
+ .set_param = impl_node_set_param,
+ .set_io = impl_node_set_io,
+ .send_command = impl_node_send_command,
+ .add_port = impl_node_add_port,
+ .remove_port = impl_node_remove_port,
+ .port_enum_params = impl_node_port_enum_params,
+ .port_set_param = impl_node_port_set_param,
+ .port_use_buffers = impl_node_port_use_buffers,
+ .port_set_io = impl_node_port_set_io,
+ .port_reuse_buffer = impl_node_port_reuse_buffer,
+ .process = impl_node_process,
+};
+
+static int
+node_init(struct node *this,
+ struct spa_dict *info,
+ const struct spa_support *support,
+ uint32_t n_support)
+{
+ this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log);
+ this->data_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataLoop);
+ this->data_system = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataSystem);
+
+ if (this->data_loop == NULL) {
+ spa_log_error(this->log, "a data-loop is needed");
+ return -EINVAL;
+ }
+ if (this->data_system == NULL) {
+ spa_log_error(this->log, "a data-system is needed");
+ return -EINVAL;
+ }
+
+ this->node.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_Node,
+ SPA_VERSION_NODE,
+ &impl_node, this);
+ spa_hook_list_init(&this->hooks);
+
+ this->data_source.func = node_on_data_fd_events;
+ this->data_source.data = this;
+ this->data_source.fd = -1;
+ this->data_source.mask = SPA_IO_IN | SPA_IO_ERR | SPA_IO_HUP;
+ this->data_source.rmask = 0;
+
+ return 0;
+}
+
+static int node_clear(struct node *this)
+{
+ update_params(&this->params, 0, NULL);
+ return 0;
+}
+
+static int do_remove_source(struct spa_loop *loop,
+ bool async,
+ uint32_t seq,
+ const void *data,
+ size_t size,
+ void *user_data)
+{
+ struct spa_source *source = user_data;
+ spa_loop_remove_source(loop, source);
+ return 0;
+}
+
+static void client_node_resource_destroy(void *data)
+{
+ struct impl *impl = data;
+ struct pw_impl_client_node *this = &impl->this;
+ struct node *node = &impl->node;
+
+ pw_log_debug("%p: destroy", node);
+
+ impl->node.resource = this->resource = NULL;
+ spa_hook_remove(&impl->resource_listener);
+ spa_hook_remove(&impl->object_listener);
+
+ if (node->data_source.fd != -1) {
+ spa_loop_invoke(node->data_loop,
+ do_remove_source,
+ SPA_ID_INVALID,
+ NULL,
+ 0,
+ true,
+ &node->data_source);
+ }
+ if (this->node)
+ pw_impl_node_destroy(this->node);
+}
+
+static void client_node_resource_error(void *data, int seq, int res, const char *message)
+{
+ struct impl *impl = data;
+ struct node *this = &impl->node;
+ struct spa_result_node_error result;
+
+ pw_log_error("%p: error seq:%d %d (%s)", this, seq, res, message);
+ result.message = message;
+ spa_node_emit_result(&this->hooks, seq, res, SPA_RESULT_TYPE_NODE_ERROR, &result);
+}
+
+static void client_node_resource_pong(void *data, int seq)
+{
+ struct impl *impl = data;
+ struct node *this = &impl->node;
+
+ pw_log_debug("%p: got pong, emit result %d", this, seq);
+ spa_node_emit_result(&this->hooks, seq, 0, 0, NULL);
+}
+
+void pw_impl_client_node_registered(struct pw_impl_client_node *this, struct pw_global *global)
+{
+ struct impl *impl = SPA_CONTAINER_OF(this, struct impl, this);
+ struct pw_impl_node *node = this->node;
+ struct pw_impl_client *client = impl->node.client;
+ uint32_t node_id = global->id;
+
+ pw_log_debug("%p: %d", &impl->node, node_id);
+
+ impl->activation = pw_mempool_import_block(client->pool, node->activation);
+ if (impl->activation == NULL) {
+ pw_log_debug("%p: can't import block: %m", &impl->node);
+ return;
+ }
+ impl->node_id = node_id;
+
+ if (this->resource == NULL)
+ return;
+
+ pw_resource_set_bound_id(this->resource, node_id);
+
+ pw_client_node_resource_transport(this->resource,
+ impl->other_fds[0],
+ impl->other_fds[1],
+ impl->activation->id,
+ 0,
+ sizeof(struct pw_node_activation));
+
+ if (impl->bind_node_id) {
+ pw_global_bind(global, client, PW_PERM_ALL,
+ impl->bind_node_version, impl->bind_node_id);
+ }
+}
+
+static void node_initialized(void *data)
+{
+ struct impl *impl = data;
+ struct pw_impl_client_node *this = &impl->this;
+ struct node *node = &impl->node;
+ struct pw_global *global;
+ struct spa_system *data_system = impl->node.data_system;
+ size_t size;
+
+ impl->fds[0] = spa_system_eventfd_create(data_system, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK);
+ impl->fds[1] = spa_system_eventfd_create(data_system, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK);
+ impl->other_fds[0] = impl->fds[1];
+ impl->other_fds[1] = impl->fds[0];
+ node->data_source.fd = impl->fds[0];
+ node->writefd = impl->fds[1];
+
+ spa_loop_add_source(node->data_loop, &node->data_source);
+ pw_log_debug("%p: transport read-fd:%d write-fd:%d", node, impl->fds[0], impl->fds[1]);
+
+ size = sizeof(struct spa_io_buffers) * MAX_AREAS;
+
+ impl->io_areas = pw_mempool_alloc(impl->context->pool,
+ PW_MEMBLOCK_FLAG_READWRITE |
+ PW_MEMBLOCK_FLAG_MAP |
+ PW_MEMBLOCK_FLAG_SEAL,
+ SPA_DATA_MemFd, size);
+ if (impl->io_areas == NULL)
+ return;
+
+ pw_log_debug("%p: io areas %p", node, impl->io_areas->map->ptr);
+
+ if ((global = pw_impl_node_get_global(this->node)) != NULL)
+ pw_impl_client_node_registered(this, global);
+}
+
+static void node_free(void *data)
+{
+ struct impl *impl = data;
+ struct pw_impl_client_node *this = &impl->this;
+ struct node *node = &impl->node;
+ struct spa_system *data_system = node->data_system;
+ uint32_t tag[5] = { impl->node_id, };
+ struct pw_memmap *mm;
+
+ this->node = NULL;
+
+ pw_log_debug("%p: free", node);
+ node_clear(node);
+
+ spa_hook_remove(&impl->node_listener);
+
+ while ((mm = pw_mempool_find_tag(node->client->pool, tag, sizeof(uint32_t))) != NULL)
+ pw_memmap_free(mm);
+
+ if (this->resource)
+ pw_resource_destroy(this->resource);
+
+ if (impl->activation)
+ pw_memblock_unref(impl->activation);
+ if (impl->io_areas)
+ pw_memblock_unref(impl->io_areas);
+
+ pw_map_clear(&impl->node.ports[0]);
+ pw_map_clear(&impl->node.ports[1]);
+ pw_map_clear(&impl->io_map);
+
+ if (impl->fds[0] != -1)
+ spa_system_close(data_system, impl->fds[0]);
+ if (impl->fds[1] != -1)
+ spa_system_close(data_system, impl->fds[1]);
+ free(impl);
+}
+
+static int port_init_mix(void *data, struct pw_impl_port_mix *mix)
+{
+ struct port *port = data;
+ struct impl *impl = port->impl;
+ struct mix *m;
+
+ if ((m = ensure_mix(impl, port, mix->port.port_id)) == NULL)
+ return -ENOMEM;
+
+ mix->id = pw_map_insert_new(&impl->io_map, NULL);
+ if (mix->id == SPA_ID_INVALID) {
+ m->valid = false;
+ return -errno;
+ }
+ if (mix->id > MAX_AREAS) {
+ pw_map_remove(&impl->io_map, mix->id);
+ m->valid = false;
+ return -ENOMEM;
+ }
+
+ mix->io = SPA_PTROFF(impl->io_areas->map->ptr,
+ mix->id * sizeof(struct spa_io_buffers), void);
+ *mix->io = SPA_IO_BUFFERS_INIT;
+
+ m->peer_id = mix->peer_id;
+
+ pw_log_debug("%p: init mix id:%d io:%p base:%p", impl,
+ mix->id, mix->io, impl->io_areas->map->ptr);
+
+ return 0;
+}
+
+static int port_release_mix(void *data, struct pw_impl_port_mix *mix)
+{
+ struct port *port = data;
+ struct impl *impl = port->impl;
+ struct node *this = &impl->node;
+ struct mix *m;
+
+ pw_log_debug("%p: remove mix id:%d io:%p base:%p",
+ this, mix->id, mix->io, impl->io_areas->map->ptr);
+
+ if ((m = find_mix(port, mix->port.port_id)) == NULL || !m->valid)
+ return -EINVAL;
+
+ pw_map_remove(&impl->io_map, mix->id);
+ m->valid = false;
+
+ return 0;
+}
+
+static const struct pw_impl_port_implementation port_impl = {
+ PW_VERSION_PORT_IMPLEMENTATION,
+ .init_mix = port_init_mix,
+ .release_mix = port_release_mix,
+};
+
+static int
+impl_mix_port_enum_params(void *object, int seq,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+ struct port *port = object;
+
+ if (port->direction != direction)
+ return -ENOTSUP;
+
+ return impl_node_port_enum_params(&port->node->node, seq, direction, port->id,
+ id, start, num, filter);
+}
+
+static int
+impl_mix_port_set_param(void *object,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ return -ENOTSUP;
+}
+
+static int
+impl_mix_add_port(void *object, enum spa_direction direction, uint32_t mix_id,
+ const struct spa_dict *props)
+{
+ struct port *port = object;
+ pw_log_debug("%p: add port %d:%d.%d", object, direction, port->id, mix_id);
+ return 0;
+}
+
+static int
+impl_mix_remove_port(void *object, enum spa_direction direction, uint32_t mix_id)
+{
+ struct port *port = object;
+ pw_log_debug("%p: remove port %d:%d.%d", object, direction, port->id, mix_id);
+ return 0;
+}
+
+static int
+impl_mix_port_use_buffers(void *object,
+ enum spa_direction direction,
+ uint32_t mix_id,
+ uint32_t flags,
+ struct spa_buffer **buffers,
+ uint32_t n_buffers)
+{
+ struct port *port = object;
+ struct impl *impl = port->impl;
+
+ return do_port_use_buffers(impl, direction, port->id, mix_id, flags, buffers, n_buffers);
+}
+
+static int impl_mix_port_set_io(void *object,
+ enum spa_direction direction, uint32_t mix_id,
+ uint32_t id, void *data, size_t size)
+{
+ struct port *p = object;
+ struct pw_impl_port *port = p->port;
+ struct impl *impl = port->owner_data;
+ struct node *this = &impl->node;
+ struct pw_impl_port_mix *mix;
+
+ mix = pw_map_lookup(&port->mix_port_map, mix_id);
+ if (mix == NULL)
+ return -EINVAL;
+
+ if (id == SPA_IO_Buffers) {
+ if (data && size >= sizeof(struct spa_io_buffers))
+ mix->io = data;
+ else
+ mix->io = NULL;
+
+ if (mix->io != NULL && this->resource && this->resource->version >= 4)
+ pw_client_node_resource_port_set_mix_info(this->resource,
+ direction, port->port_id,
+ mix->port.port_id, mix->peer_id, NULL);
+ }
+
+ return do_port_set_io(impl,
+ direction, port->port_id, mix->port.port_id,
+ id, data, size);
+}
+
+static int
+impl_mix_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id)
+{
+ struct port *p = object;
+ return impl_node_port_reuse_buffer(&p->node->node, p->id, buffer_id);
+}
+
+static int impl_mix_process(void *object)
+{
+ return SPA_STATUS_HAVE_DATA;
+}
+
+static const struct spa_node_methods impl_port_mix = {
+ SPA_VERSION_NODE_METHODS,
+ .port_enum_params = impl_mix_port_enum_params,
+ .port_set_param = impl_mix_port_set_param,
+ .add_port = impl_mix_add_port,
+ .remove_port = impl_mix_remove_port,
+ .port_use_buffers = impl_mix_port_use_buffers,
+ .port_set_io = impl_mix_port_set_io,
+ .port_reuse_buffer = impl_mix_port_reuse_buffer,
+ .process = impl_mix_process,
+};
+
+static void node_port_init(void *data, struct pw_impl_port *port)
+{
+ struct impl *impl = data;
+ struct port *p = pw_impl_port_get_user_data(port);
+ struct node *this = &impl->node;
+
+ pw_log_debug("%p: port %p init", this, port);
+
+ *p = this->dummy;
+ p->port = port;
+ p->node = this;
+ p->direction = port->direction;
+ p->id = port->port_id;
+ p->impl = impl;
+ pw_array_init(&p->mix, sizeof(struct mix) * 2);
+ p->mix_node.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_Node,
+ SPA_VERSION_NODE,
+ &impl_port_mix, p);
+ ensure_mix(impl, p, SPA_ID_INVALID);
+
+ pw_map_insert_at(&this->ports[p->direction], p->id, p);
+ return;
+}
+
+static void node_port_added(void *data, struct pw_impl_port *port)
+{
+ struct impl *impl = data;
+ struct port *p = pw_impl_port_get_user_data(port);
+
+ port->flags |= PW_IMPL_PORT_FLAG_NO_MIXER;
+
+ port->impl = SPA_CALLBACKS_INIT(&port_impl, p);
+ port->owner_data = impl;
+
+ pw_impl_port_set_mix(port, &p->mix_node,
+ PW_IMPL_PORT_MIX_FLAG_MULTI |
+ PW_IMPL_PORT_MIX_FLAG_MIX_ONLY);
+}
+
+static void node_port_removed(void *data, struct pw_impl_port *port)
+{
+ struct impl *impl = data;
+ struct node *this = &impl->node;
+ struct port *p = pw_impl_port_get_user_data(port);
+
+ pw_log_debug("%p: port %p remove", this, port);
+
+ p->removed = true;
+ clear_port(this, p);
+}
+
+static void node_peer_added(void *data, struct pw_impl_node *peer)
+{
+ struct impl *impl = data;
+ struct node *this = &impl->node;
+ struct pw_memblock *m;
+
+ if (peer == impl->this.node)
+ return;
+
+ m = pw_mempool_import_block(this->client->pool, peer->activation);
+ if (m == NULL) {
+ pw_log_debug("%p: can't ensure mem: %m", this);
+ return;
+ }
+ pw_log_debug("%p: peer %p id:%u added mem_id:%u", &impl->this, peer,
+ peer->info.id, m->id);
+
+ if (this->resource == NULL)
+ return;
+
+ pw_client_node_resource_set_activation(this->resource,
+ peer->info.id,
+ peer->source.fd,
+ m->id,
+ 0,
+ sizeof(struct pw_node_activation));
+}
+
+static void node_peer_removed(void *data, struct pw_impl_node *peer)
+{
+ struct impl *impl = data;
+ struct node *this = &impl->node;
+ struct pw_memblock *m;
+
+ if (peer == impl->this.node)
+ return;
+
+ m = pw_mempool_find_fd(this->client->pool, peer->activation->fd);
+ if (m == NULL) {
+ pw_log_warn("%p: unknown peer %p fd:%d", this, peer,
+ peer->source.fd);
+ return;
+ }
+ pw_log_debug("%p: peer %p %u removed", this, peer,
+ peer->info.id);
+
+ if (this->resource != NULL) {
+ pw_client_node_resource_set_activation(this->resource,
+ peer->info.id,
+ -1,
+ SPA_ID_INVALID,
+ 0,
+ 0);
+ }
+
+ pw_memblock_unref(m);
+}
+
+static void node_driver_changed(void *data, struct pw_impl_node *old, struct pw_impl_node *driver)
+{
+ struct impl *impl = data;
+ struct node *this = &impl->node;
+
+ pw_log_debug("%p: driver changed %p -> %p", this, old, driver);
+
+ node_peer_removed(data, old);
+ node_peer_added(data, driver);
+}
+
+static const struct pw_impl_node_events node_events = {
+ PW_VERSION_IMPL_NODE_EVENTS,
+ .free = node_free,
+ .initialized = node_initialized,
+ .port_init = node_port_init,
+ .port_added = node_port_added,
+ .port_removed = node_port_removed,
+ .peer_added = node_peer_added,
+ .peer_removed = node_peer_removed,
+ .driver_changed = node_driver_changed,
+};
+
+static const struct pw_resource_events resource_events = {
+ PW_VERSION_RESOURCE_EVENTS,
+ .destroy = client_node_resource_destroy,
+ .error = client_node_resource_error,
+ .pong = client_node_resource_pong,
+};
+
+/** Create a new client node
+ * \param client an owner \ref pw_client
+ * \param id an id
+ * \param name a name
+ * \param properties extra properties
+ * \return a newly allocated client node
+ *
+ * Create a new \ref pw_impl_node.
+ *
+ * \memberof pw_impl_client_node
+ */
+struct pw_impl_client_node *pw_impl_client_node_new(struct pw_resource *resource,
+ struct pw_properties *properties,
+ bool do_register)
+{
+ struct impl *impl;
+ struct pw_impl_client_node *this;
+ struct pw_impl_client *client = pw_resource_get_client(resource);
+ struct pw_context *context = pw_impl_client_get_context(client);
+ const struct spa_support *support;
+ uint32_t n_support;
+ int res;
+
+ impl = calloc(1, sizeof(struct impl));
+ if (impl == NULL) {
+ res = -errno;
+ goto error_exit_cleanup;
+ }
+
+ if (properties == NULL)
+ properties = pw_properties_new(NULL, NULL);
+ if (properties == NULL) {
+ res = -errno;
+ goto error_exit_free;
+ }
+
+ pw_properties_setf(properties, PW_KEY_CLIENT_ID, "%d", client->global->id);
+
+ this = &impl->this;
+
+ impl->context = context;
+ impl->fds[0] = impl->fds[1] = -1;
+ pw_log_debug("%p: new", &impl->node);
+
+ support = pw_context_get_support(impl->context, &n_support);
+ node_init(&impl->node, NULL, support, n_support);
+ impl->node.impl = impl;
+ impl->node.resource = resource;
+ impl->node.client = client;
+ this->flags = do_register ? 0 : 1;
+
+ pw_map_init(&impl->node.ports[0], 64, 64);
+ pw_map_init(&impl->node.ports[1], 64, 64);
+ pw_map_init(&impl->io_map, 64, 64);
+
+ this->resource = resource;
+ this->node = pw_spa_node_new(context,
+ PW_SPA_NODE_FLAG_ASYNC |
+ (do_register ? 0 : PW_SPA_NODE_FLAG_NO_REGISTER),
+ (struct spa_node *)&impl->node.node,
+ NULL,
+ properties, 0);
+
+ if (this->node == NULL)
+ goto error_no_node;
+
+ this->node->remote = true;
+ this->flags = 0;
+
+ pw_resource_add_listener(this->resource,
+ &impl->resource_listener,
+ &resource_events,
+ impl);
+ pw_resource_add_object_listener(this->resource,
+ &impl->object_listener,
+ &client_node_methods,
+ impl);
+
+ this->node->port_user_data_size = sizeof(struct port);
+
+ pw_impl_node_add_listener(this->node, &impl->node_listener, &node_events, impl);
+
+ return this;
+
+error_no_node:
+ res = -errno;
+ node_clear(&impl->node);
+ properties = NULL;
+ goto error_exit_free;
+
+error_exit_free:
+ free(impl);
+error_exit_cleanup:
+ if (resource)
+ pw_resource_destroy(resource);
+ pw_properties_free(properties);
+ errno = -res;
+ return NULL;
+}
+
+/** Destroy a client node
+ * \param node the client node to destroy
+ * \memberof pw_impl_client_node
+ */
+void pw_impl_client_node_destroy(struct pw_impl_client_node *node)
+{
+ pw_resource_destroy(node->resource);
+}
diff --git a/src/modules/module-client-node/client-node.h b/src/modules/module-client-node/client-node.h
new file mode 100644
index 0000000..f7e060a
--- /dev/null
+++ b/src/modules/module-client-node/client-node.h
@@ -0,0 +1,60 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_CLIENT_NODE_H
+#define PIPEWIRE_CLIENT_NODE_H
+
+#include <pipewire/impl.h>
+#include <pipewire/extensions/client-node.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/** \class pw_impl_client_node
+ *
+ * PipeWire client node interface
+ */
+struct pw_impl_client_node {
+ struct pw_impl_node *node;
+
+ struct pw_resource *resource;
+ uint32_t flags;
+};
+
+struct pw_impl_client_node *
+pw_impl_client_node_new(struct pw_resource *resource,
+ struct pw_properties *properties,
+ bool do_register);
+
+void
+pw_impl_client_node_destroy(struct pw_impl_client_node *node);
+
+void pw_impl_client_node_registered(struct pw_impl_client_node *node, struct pw_global *global);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PIPEWIRE_CLIENT_NODE_H */
diff --git a/src/modules/module-client-node/protocol-native.c b/src/modules/module-client-node/protocol-native.c
new file mode 100644
index 0000000..dd51692
--- /dev/null
+++ b/src/modules/module-client-node/protocol-native.c
@@ -0,0 +1,1259 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+
+#include <spa/pod/builder.h>
+#include <spa/pod/parser.h>
+#include <spa/utils/result.h>
+
+#include <pipewire/impl.h>
+
+#include <pipewire/extensions/protocol-native.h>
+#include <pipewire/extensions/client-node.h>
+
+#define MAX_DICT 1024
+#define MAX_PARAMS 4096
+#define MAX_PARAM_INFO 128
+#define MAX_BUFFERS 64
+#define MAX_METAS 16u
+#define MAX_DATAS 64u
+
+PW_LOG_TOPIC_EXTERN(mod_topic);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+static inline void push_item(struct spa_pod_builder *b, const struct spa_dict_item *item)
+{
+ const char *str;
+ spa_pod_builder_string(b, item->key);
+ str = item->value;
+ if (spa_strstartswith(str, "pointer:"))
+ str = "";
+ spa_pod_builder_string(b, str);
+}
+
+static void push_dict(struct spa_pod_builder *b, const struct spa_dict *dict)
+{
+ uint32_t i, n_items;
+ struct spa_pod_frame f;
+
+ n_items = dict ? dict->n_items : 0;
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_int(b, n_items);
+ for (i = 0; i < n_items; i++)
+ push_item(b, &dict->items[i]);
+ spa_pod_builder_pop(b, &f);
+}
+
+static inline int parse_item(struct spa_pod_parser *prs, struct spa_dict_item *item)
+{
+ int res;
+ if ((res = spa_pod_parser_get(prs,
+ SPA_POD_String(&item->key),
+ SPA_POD_String(&item->value),
+ NULL)) < 0)
+ return res;
+ if (spa_strstartswith(item->value, "pointer:"))
+ item->value = "";
+ return 0;
+}
+
+#define parse_dict(prs,d) \
+do { \
+ uint32_t i; \
+ if (spa_pod_parser_get(prs, \
+ SPA_POD_Int(&(d)->n_items), NULL) < 0) \
+ return -EINVAL; \
+ (d)->items = NULL; \
+ if ((d)->n_items > 0) { \
+ if ((d)->n_items > MAX_DICT) \
+ return -ENOSPC; \
+ (d)->items = alloca((d)->n_items * sizeof(struct spa_dict_item)); \
+ for (i = 0; i < (d)->n_items; i++) { \
+ if (parse_item(prs, (struct spa_dict_item *) &(d)->items[i]) < 0) \
+ return -EINVAL; \
+ } \
+ } \
+} while(0)
+
+#define parse_dict_struct(prs,f,dict) \
+do { \
+ if (spa_pod_parser_push_struct(prs, f) < 0) \
+ return -EINVAL; \
+ parse_dict(prs, dict); \
+ spa_pod_parser_pop(prs, f); \
+} while(0)
+
+#define parse_params(prs,n_params,params) \
+do { \
+ uint32_t i; \
+ if (spa_pod_parser_get(prs, \
+ SPA_POD_Int(&n_params), NULL) < 0) \
+ return -EINVAL; \
+ params = NULL; \
+ if (n_params > 0) { \
+ if (n_params > MAX_PARAMS) \
+ return -ENOSPC; \
+ params = alloca(n_params * sizeof(struct spa_pod *)); \
+ for (i = 0; i < n_params; i++) { \
+ if (spa_pod_parser_get(prs, \
+ SPA_POD_PodObject(&params[i]), NULL) < 0) \
+ return -EINVAL; \
+ } \
+ } \
+} while(0)
+
+#define parse_param_info(prs,n_params,params) \
+do { \
+ uint32_t i; \
+ if (spa_pod_parser_get(prs, \
+ SPA_POD_Int(&(n_params)), NULL) < 0) \
+ return -EINVAL; \
+ params = NULL; \
+ if (n_params > 0) { \
+ if (n_params > MAX_PARAM_INFO) \
+ return -ENOSPC; \
+ params = alloca(n_params * sizeof(struct spa_param_info)); \
+ for (i = 0; i < n_params; i++) { \
+ if (spa_pod_parser_get(prs, \
+ SPA_POD_Id(&(params[i]).id), \
+ SPA_POD_Int(&(params[i]).flags), NULL) < 0) \
+ return -EINVAL; \
+ } \
+ } \
+} while(0)
+
+static int client_node_marshal_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct pw_client_node_events *events,
+ void *data)
+{
+ struct pw_proxy *proxy = object;
+ pw_proxy_add_object_listener(proxy, listener, events, data);
+ return 0;
+}
+
+static struct pw_node *
+client_node_marshal_get_node(void *object, uint32_t version, size_t user_data_size)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+ struct pw_proxy *res;
+ uint32_t new_id;
+
+ res = pw_proxy_new(object, PW_TYPE_INTERFACE_Node, version, user_data_size);
+ if (res == NULL)
+ return NULL;
+
+ new_id = pw_proxy_get_id(res);
+
+ b = pw_protocol_native_begin_proxy(proxy, PW_CLIENT_NODE_METHOD_GET_NODE, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(version),
+ SPA_POD_Int(new_id));
+
+ pw_protocol_native_end_proxy(proxy, b);
+
+ return (struct pw_node *) res;
+}
+
+static int
+client_node_marshal_update(void *object,
+ uint32_t change_mask,
+ uint32_t n_params,
+ const struct spa_pod **params,
+ const struct spa_node_info *info)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+ struct spa_pod_frame f[2];
+ uint32_t i, n_items, n_info_params;
+
+ b = pw_protocol_native_begin_proxy(proxy, PW_CLIENT_NODE_METHOD_UPDATE, NULL);
+
+ spa_pod_builder_push_struct(b, &f[0]);
+ spa_pod_builder_add(b,
+ SPA_POD_Int(change_mask),
+ SPA_POD_Int(n_params), NULL);
+
+ for (i = 0; i < n_params; i++)
+ spa_pod_builder_add(b, SPA_POD_Pod(params[i]), NULL);
+
+ if (info) {
+ uint64_t change_mask = info->change_mask;
+
+ change_mask &= SPA_NODE_CHANGE_MASK_FLAGS |
+ SPA_NODE_CHANGE_MASK_PROPS |
+ SPA_NODE_CHANGE_MASK_PARAMS;
+
+ n_items = info->props && (change_mask & SPA_NODE_CHANGE_MASK_PROPS) ?
+ info->props->n_items : 0;
+ n_info_params = (change_mask & SPA_NODE_CHANGE_MASK_PARAMS) ?
+ info->n_params : 0;
+
+ spa_pod_builder_push_struct(b, &f[1]);
+ spa_pod_builder_add(b,
+ SPA_POD_Int(info->max_input_ports),
+ SPA_POD_Int(info->max_output_ports),
+ SPA_POD_Long(change_mask),
+ SPA_POD_Long(info->flags),
+ SPA_POD_Int(n_items), NULL);
+ for (i = 0; i < n_items; i++)
+ push_item(b, &info->props->items[i]);
+ spa_pod_builder_add(b,
+ SPA_POD_Int(n_info_params), NULL);
+ for (i = 0; i < n_info_params; i++) {
+ spa_pod_builder_add(b,
+ SPA_POD_Id(info->params[i].id),
+ SPA_POD_Int(info->params[i].flags), NULL);
+ }
+ spa_pod_builder_pop(b, &f[1]);
+
+ } else {
+ spa_pod_builder_add(b,
+ SPA_POD_Pod(NULL), NULL);
+ }
+ spa_pod_builder_pop(b, &f[0]);
+
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int
+client_node_marshal_port_update(void *object,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t change_mask,
+ uint32_t n_params,
+ const struct spa_pod **params,
+ const struct spa_port_info *info)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+ struct spa_pod_frame f[2];
+ uint32_t i, n_items;
+
+ b = pw_protocol_native_begin_proxy(proxy, PW_CLIENT_NODE_METHOD_PORT_UPDATE, NULL);
+
+ spa_pod_builder_push_struct(b, &f[0]);
+ spa_pod_builder_add(b,
+ SPA_POD_Int(direction),
+ SPA_POD_Int(port_id),
+ SPA_POD_Int(change_mask),
+ SPA_POD_Int(n_params), NULL);
+
+ for (i = 0; i < n_params; i++)
+ spa_pod_builder_add(b,
+ SPA_POD_Pod(params[i]), NULL);
+
+ if (info) {
+ uint64_t change_mask = info->change_mask;
+
+ n_items = info->props ? info->props->n_items : 0;
+
+ change_mask &= SPA_PORT_CHANGE_MASK_FLAGS |
+ SPA_PORT_CHANGE_MASK_RATE |
+ SPA_PORT_CHANGE_MASK_PROPS |
+ SPA_PORT_CHANGE_MASK_PARAMS;
+
+ spa_pod_builder_push_struct(b, &f[1]);
+ spa_pod_builder_add(b,
+ SPA_POD_Long(change_mask),
+ SPA_POD_Long(info->flags),
+ SPA_POD_Int(info->rate.num),
+ SPA_POD_Int(info->rate.denom),
+ SPA_POD_Int(n_items), NULL);
+ for (i = 0; i < n_items; i++)
+ push_item(b, &info->props->items[i]);
+ spa_pod_builder_add(b,
+ SPA_POD_Int(info->n_params), NULL);
+ for (i = 0; i < info->n_params; i++) {
+ spa_pod_builder_add(b,
+ SPA_POD_Id(info->params[i].id),
+ SPA_POD_Int(info->params[i].flags), NULL);
+ }
+ spa_pod_builder_pop(b, &f[1]);
+
+ } else {
+ spa_pod_builder_add(b,
+ SPA_POD_Pod(NULL), NULL);
+ }
+ spa_pod_builder_pop(b, &f[0]);
+
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int client_node_marshal_set_active(void *object, bool active)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_proxy(proxy, PW_CLIENT_NODE_METHOD_SET_ACTIVE, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Bool(active));
+
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int client_node_marshal_event_method(void *object, const struct spa_event *event)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_proxy(proxy, PW_CLIENT_NODE_METHOD_EVENT, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Pod(event));
+
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int
+client_node_marshal_port_buffers(void *object,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t mix_id,
+ uint32_t n_buffers,
+ struct spa_buffer **buffers)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+ struct spa_pod_frame f[2];
+ uint32_t i, j;
+
+ b = pw_protocol_native_begin_proxy(proxy, PW_CLIENT_NODE_METHOD_PORT_BUFFERS, NULL);
+
+ spa_pod_builder_push_struct(b, &f[0]);
+ spa_pod_builder_add(b,
+ SPA_POD_Int(direction),
+ SPA_POD_Int(port_id),
+ SPA_POD_Int(mix_id),
+ SPA_POD_Int(n_buffers), NULL);
+
+ for (i = 0; i < n_buffers; i++) {
+ struct spa_buffer *buf = buffers[i];
+
+ spa_pod_builder_add(b,
+ SPA_POD_Int(buf->n_datas), NULL);
+
+ for (j = 0; j < buf->n_datas; j++) {
+ struct spa_data *d = &buf->datas[j];
+ spa_pod_builder_add(b,
+ SPA_POD_Id(d->type),
+ SPA_POD_Fd(pw_protocol_native_add_proxy_fd(proxy, d->fd)),
+ SPA_POD_Int(d->flags),
+ SPA_POD_Int(d->mapoffset),
+ SPA_POD_Int(d->maxsize), NULL);
+ }
+ }
+ spa_pod_builder_pop(b, &f[0]);
+
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int client_node_demarshal_transport(void *data, const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ uint32_t mem_id, offset, sz;
+ int64_t ridx, widx;
+ int readfd, writefd;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Fd(&ridx),
+ SPA_POD_Fd(&widx),
+ SPA_POD_Int(&mem_id),
+ SPA_POD_Int(&offset),
+ SPA_POD_Int(&sz)) < 0)
+ return -EINVAL;
+
+ readfd = pw_protocol_native_get_proxy_fd(proxy, ridx);
+ writefd = pw_protocol_native_get_proxy_fd(proxy, widx);
+
+ if (readfd < 0 || writefd < 0)
+ return -EINVAL;
+
+ pw_proxy_notify(proxy, struct pw_client_node_events, transport, 0,
+ readfd, writefd, mem_id,
+ offset, sz);
+ return 0;
+}
+
+static int client_node_demarshal_set_param(void *data, const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ uint32_t id, flags;
+ const struct spa_pod *param = NULL;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&flags),
+ SPA_POD_PodObject(&param)) < 0)
+ return -EINVAL;
+
+ pw_proxy_notify(proxy, struct pw_client_node_events, set_param, 0, id, flags, param);
+ return 0;
+}
+
+static int client_node_demarshal_event_event(void *data, const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ const struct spa_event *event;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_PodObject(&event)) < 0)
+ return -EINVAL;
+
+ pw_proxy_notify(proxy, struct pw_client_node_events, event, 0, event);
+ return 0;
+}
+
+static int client_node_demarshal_command(void *data, const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ const struct spa_command *command;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_PodObject(&command)) < 0)
+ return -EINVAL;
+
+ pw_proxy_notify(proxy, struct pw_client_node_events, command, 0, command);
+ return 0;
+}
+
+static int client_node_demarshal_add_port(void *data, const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ struct spa_pod_frame f[2];
+ int32_t direction, port_id;
+ struct spa_dict props = SPA_DICT_INIT(NULL, 0);
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_push_struct(&prs, &f[0]) < 0)
+ return -EINVAL;
+
+ if (spa_pod_parser_get(&prs,
+ SPA_POD_Int(&direction),
+ SPA_POD_Int(&port_id), NULL) < 0)
+ return -EINVAL;
+
+ parse_dict_struct(&prs, &f[1], &props);
+
+ pw_proxy_notify(proxy, struct pw_client_node_events, add_port, 0, direction, port_id,
+ props.n_items ? &props : NULL);
+ return 0;
+}
+
+static int client_node_demarshal_remove_port(void *data, const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ int32_t direction, port_id;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&direction),
+ SPA_POD_Int(&port_id)) < 0)
+ return -EINVAL;
+
+ pw_proxy_notify(proxy, struct pw_client_node_events, remove_port, 0, direction, port_id);
+ return 0;
+}
+
+static int client_node_demarshal_port_set_param(void *data, const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ uint32_t direction, port_id, id, flags;
+ const struct spa_pod *param = NULL;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&direction),
+ SPA_POD_Int(&port_id),
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&flags),
+ SPA_POD_PodObject(&param)) < 0)
+ return -EINVAL;
+
+ pw_proxy_notify(proxy, struct pw_client_node_events, port_set_param, 0,
+ direction, port_id, id, flags, param);
+ return 0;
+}
+
+static int client_node_demarshal_port_use_buffers(void *data, const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ struct spa_pod_frame f;
+ uint32_t direction, port_id, mix_id, flags, n_buffers, data_id;
+ struct pw_client_node_buffer *buffers;
+ uint32_t i, j;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_push_struct(&prs, &f) < 0 ||
+ spa_pod_parser_get(&prs,
+ SPA_POD_Int(&direction),
+ SPA_POD_Int(&port_id),
+ SPA_POD_Int(&mix_id),
+ SPA_POD_Int(&flags),
+ SPA_POD_Int(&n_buffers), NULL) < 0)
+ return -EINVAL;
+
+ if (n_buffers > MAX_BUFFERS)
+ return -ENOSPC;
+
+ buffers = alloca(sizeof(struct pw_client_node_buffer) * n_buffers);
+ for (i = 0; i < n_buffers; i++) {
+ struct spa_buffer *buf = buffers[i].buffer = alloca(sizeof(struct spa_buffer));
+
+ if (spa_pod_parser_get(&prs,
+ SPA_POD_Int(&buffers[i].mem_id),
+ SPA_POD_Int(&buffers[i].offset),
+ SPA_POD_Int(&buffers[i].size),
+ SPA_POD_Int(&buf->n_metas), NULL) < 0)
+ return -EINVAL;
+
+ if (buf->n_metas > MAX_METAS)
+ return -ENOSPC;
+
+ buf->metas = alloca(sizeof(struct spa_meta) * buf->n_metas);
+ for (j = 0; j < buf->n_metas; j++) {
+ struct spa_meta *m = &buf->metas[j];
+
+ if (spa_pod_parser_get(&prs,
+ SPA_POD_Id(&m->type),
+ SPA_POD_Int(&m->size), NULL) < 0)
+ return -EINVAL;
+
+ m->data = NULL;
+ }
+ if (spa_pod_parser_get(&prs,
+ SPA_POD_Int(&buf->n_datas), NULL) < 0)
+ return -EINVAL;
+
+ if (buf->n_datas > MAX_DATAS)
+ return -ENOSPC;
+
+ buf->datas = alloca(sizeof(struct spa_data) * buf->n_datas);
+ for (j = 0; j < buf->n_datas; j++) {
+ struct spa_data *d = &buf->datas[j];
+
+ if (spa_pod_parser_get(&prs,
+ SPA_POD_Id(&d->type),
+ SPA_POD_Int(&data_id),
+ SPA_POD_Int(&d->flags),
+ SPA_POD_Int(&d->mapoffset),
+ SPA_POD_Int(&d->maxsize), NULL) < 0)
+ return -EINVAL;
+
+ d->fd = -1;
+ d->data = SPA_UINT32_TO_PTR(data_id);
+ d->chunk = NULL;
+ }
+ }
+ pw_proxy_notify(proxy, struct pw_client_node_events, port_use_buffers, 0,
+ direction,
+ port_id,
+ mix_id,
+ flags,
+ n_buffers, buffers);
+ return 0;
+}
+
+static int client_node_demarshal_port_set_io(void *data, const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ uint32_t direction, port_id, mix_id, id, memid, off, sz;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&direction),
+ SPA_POD_Int(&port_id),
+ SPA_POD_Int(&mix_id),
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&memid),
+ SPA_POD_Int(&off),
+ SPA_POD_Int(&sz)) < 0)
+ return -EINVAL;
+
+ pw_proxy_notify(proxy, struct pw_client_node_events, port_set_io, 0,
+ direction, port_id, mix_id,
+ id, memid,
+ off, sz);
+ return 0;
+}
+
+static int client_node_demarshal_set_activation(void *data, const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ uint32_t node_id, memid, off, sz;
+ int64_t sigidx;
+ int signalfd;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&node_id),
+ SPA_POD_Fd(&sigidx),
+ SPA_POD_Int(&memid),
+ SPA_POD_Int(&off),
+ SPA_POD_Int(&sz)) < 0)
+ return -EINVAL;
+
+ signalfd = pw_protocol_native_get_proxy_fd(proxy, sigidx);
+
+ pw_proxy_notify(proxy, struct pw_client_node_events, set_activation, 0,
+ node_id,
+ signalfd,
+ memid,
+ off, sz);
+ return 0;
+}
+
+static int client_node_demarshal_port_set_mix_info(void *data, const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ uint32_t direction, port_id, mix_id, peer_id;
+ struct spa_pod_frame f[2];
+ struct spa_dict props = SPA_DICT_INIT(NULL, 0);
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_push_struct(&prs, &f[0]) < 0 ||
+ spa_pod_parser_get(&prs,
+ SPA_POD_Int(&direction),
+ SPA_POD_Int(&port_id),
+ SPA_POD_Int(&mix_id),
+ SPA_POD_Int(&peer_id), NULL) < 0)
+ return -EINVAL;
+
+ parse_dict_struct(&prs, &f[1], &props);
+
+ pw_proxy_notify(proxy, struct pw_client_node_events, port_set_mix_info, 1,
+ direction, port_id, mix_id,
+ peer_id, &props);
+ return 0;
+}
+
+static int client_node_demarshal_set_io(void *data, const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ uint32_t id, memid, off, sz;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&memid),
+ SPA_POD_Int(&off),
+ SPA_POD_Int(&sz)) < 0)
+ return -EINVAL;
+
+ pw_proxy_notify(proxy, struct pw_client_node_events, set_io, 0,
+ id, memid, off, sz);
+ return 0;
+}
+
+static int client_node_marshal_transport(void *data, int readfd, int writefd,
+ uint32_t mem_id, uint32_t offset, uint32_t size)
+{
+ struct pw_protocol_native_message *msg;
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource, PW_CLIENT_NODE_EVENT_TRANSPORT, &msg);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Fd(pw_protocol_native_add_resource_fd(resource, readfd)),
+ SPA_POD_Fd(pw_protocol_native_add_resource_fd(resource, writefd)),
+ SPA_POD_Int(mem_id),
+ SPA_POD_Int(offset),
+ SPA_POD_Int(size));
+
+ return pw_protocol_native_end_resource(resource, b);
+}
+
+static int
+client_node_marshal_set_param(void *data, uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource, PW_CLIENT_NODE_EVENT_SET_PARAM, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Id(id),
+ SPA_POD_Int(flags),
+ SPA_POD_Pod(param));
+
+ return pw_protocol_native_end_resource(resource, b);
+}
+
+static int client_node_marshal_event_event(void *data, const struct spa_event *event)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource, PW_CLIENT_NODE_EVENT_EVENT, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Pod(event));
+
+ return pw_protocol_native_end_resource(resource, b);
+}
+
+static int
+client_node_marshal_command(void *data, const struct spa_command *command)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource, PW_CLIENT_NODE_EVENT_COMMAND, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Pod(command));
+
+ return pw_protocol_native_end_resource(resource, b);
+}
+
+static int
+client_node_marshal_add_port(void *data,
+ enum spa_direction direction, uint32_t port_id,
+ const struct spa_dict *props)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+ struct spa_pod_frame f;
+
+ b = pw_protocol_native_begin_resource(resource, PW_CLIENT_NODE_EVENT_ADD_PORT, NULL);
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_add(b,
+ SPA_POD_Int(direction),
+ SPA_POD_Int(port_id), NULL);
+ push_dict(b, props);
+ spa_pod_builder_pop(b, &f);
+
+ return pw_protocol_native_end_resource(resource, b);
+}
+
+static int
+client_node_marshal_remove_port(void *data,
+ enum spa_direction direction, uint32_t port_id)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource, PW_CLIENT_NODE_EVENT_REMOVE_PORT, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(direction),
+ SPA_POD_Int(port_id));
+
+ return pw_protocol_native_end_resource(resource, b);
+}
+
+static int
+client_node_marshal_port_set_param(void *data,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t id,
+ uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource, PW_CLIENT_NODE_EVENT_PORT_SET_PARAM, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(direction),
+ SPA_POD_Int(port_id),
+ SPA_POD_Id(id),
+ SPA_POD_Int(flags),
+ SPA_POD_Pod(param));
+
+ return pw_protocol_native_end_resource(resource, b);
+}
+
+static int
+client_node_marshal_port_use_buffers(void *data,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t mix_id,
+ uint32_t flags,
+ uint32_t n_buffers, struct pw_client_node_buffer *buffers)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+ struct spa_pod_frame f;
+ uint32_t i, j;
+
+ b = pw_protocol_native_begin_resource(resource, PW_CLIENT_NODE_EVENT_PORT_USE_BUFFERS, NULL);
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_add(b,
+ SPA_POD_Int(direction),
+ SPA_POD_Int(port_id),
+ SPA_POD_Int(mix_id),
+ SPA_POD_Int(flags),
+ SPA_POD_Int(n_buffers), NULL);
+
+ for (i = 0; i < n_buffers; i++) {
+ struct spa_buffer *buf = buffers[i].buffer;
+
+ spa_pod_builder_add(b,
+ SPA_POD_Int(buffers[i].mem_id),
+ SPA_POD_Int(buffers[i].offset),
+ SPA_POD_Int(buffers[i].size),
+ SPA_POD_Int(buf->n_metas), NULL);
+
+ for (j = 0; j < buf->n_metas; j++) {
+ struct spa_meta *m = &buf->metas[j];
+ spa_pod_builder_add(b,
+ SPA_POD_Id(m->type),
+ SPA_POD_Int(m->size), NULL);
+ }
+ spa_pod_builder_add(b,
+ SPA_POD_Int(buf->n_datas), NULL);
+ for (j = 0; j < buf->n_datas; j++) {
+ struct spa_data *d = &buf->datas[j];
+ spa_pod_builder_add(b,
+ SPA_POD_Id(d->type),
+ SPA_POD_Int(SPA_PTR_TO_UINT32(d->data)),
+ SPA_POD_Int(d->flags),
+ SPA_POD_Int(d->mapoffset),
+ SPA_POD_Int(d->maxsize), NULL);
+ }
+ }
+ spa_pod_builder_pop(b, &f);
+
+ return pw_protocol_native_end_resource(resource, b);
+}
+
+static int
+client_node_marshal_port_set_io(void *data,
+ uint32_t direction,
+ uint32_t port_id,
+ uint32_t mix_id,
+ uint32_t id,
+ uint32_t memid,
+ uint32_t offset,
+ uint32_t size)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource, PW_CLIENT_NODE_EVENT_PORT_SET_IO, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(direction),
+ SPA_POD_Int(port_id),
+ SPA_POD_Int(mix_id),
+ SPA_POD_Id(id),
+ SPA_POD_Int(memid),
+ SPA_POD_Int(offset),
+ SPA_POD_Int(size));
+
+ return pw_protocol_native_end_resource(resource, b);
+}
+
+static int
+client_node_marshal_set_activation(void *data,
+ uint32_t node_id,
+ int signalfd,
+ uint32_t memid,
+ uint32_t offset,
+ uint32_t size)
+{
+ struct pw_protocol_native_message *msg;
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource, PW_CLIENT_NODE_EVENT_SET_ACTIVATION, &msg);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(node_id),
+ SPA_POD_Fd(pw_protocol_native_add_resource_fd(resource, signalfd)),
+ SPA_POD_Int(memid),
+ SPA_POD_Int(offset),
+ SPA_POD_Int(size));
+
+ return pw_protocol_native_end_resource(resource, b);
+}
+
+static int
+client_node_marshal_set_io(void *data,
+ uint32_t id,
+ uint32_t memid,
+ uint32_t offset,
+ uint32_t size)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource, PW_CLIENT_NODE_EVENT_SET_IO, NULL);
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Id(id),
+ SPA_POD_Int(memid),
+ SPA_POD_Int(offset),
+ SPA_POD_Int(size));
+ return pw_protocol_native_end_resource(resource, b);
+}
+
+static int
+client_node_marshal_port_set_mix_info(void *data,
+ uint32_t direction,
+ uint32_t port_id,
+ uint32_t mix_id,
+ uint32_t peer_id,
+ const struct spa_dict *props)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+ struct spa_pod_frame f;
+
+ b = pw_protocol_native_begin_resource(resource, PW_CLIENT_NODE_EVENT_PORT_SET_MIX_INFO, NULL);
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_add(b,
+ SPA_POD_Int(direction),
+ SPA_POD_Int(port_id),
+ SPA_POD_Int(mix_id),
+ SPA_POD_Int(peer_id), NULL);
+ push_dict(b, props);
+ spa_pod_builder_pop(b, &f);
+
+ return pw_protocol_native_end_resource(resource, b);
+}
+
+
+static int client_node_demarshal_get_node(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ int32_t version, new_id;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&version),
+ SPA_POD_Int(&new_id)) < 0)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_client_node_methods, get_node, 0,
+ version, new_id);
+}
+
+static int client_node_demarshal_update(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ struct spa_pod_frame f[2];
+ uint32_t change_mask, n_params;
+ const struct spa_pod **params = NULL;
+ struct spa_node_info info = SPA_NODE_INFO_INIT(), *infop = NULL;
+ struct spa_pod *ipod;
+ struct spa_dict props = SPA_DICT_INIT(NULL, 0);
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_push_struct(&prs, &f[0]) < 0 ||
+ spa_pod_parser_get(&prs,
+ SPA_POD_Int(&change_mask), NULL) < 0)
+ return -EINVAL;
+
+ parse_params(&prs, n_params, params);
+
+ if (spa_pod_parser_get(&prs,
+ SPA_POD_PodStruct(&ipod), NULL) < 0)
+ return -EINVAL;
+
+ if (ipod) {
+ struct spa_pod_parser p2;
+ struct spa_pod_frame f2;
+ infop = &info;
+
+ spa_pod_parser_pod(&p2, ipod);
+ if (spa_pod_parser_push_struct(&p2, &f2) < 0 ||
+ spa_pod_parser_get(&p2,
+ SPA_POD_Int(&info.max_input_ports),
+ SPA_POD_Int(&info.max_output_ports),
+ SPA_POD_Long(&info.change_mask),
+ SPA_POD_Long(&info.flags), NULL) < 0)
+ return -EINVAL;
+
+ info.change_mask &= SPA_NODE_CHANGE_MASK_FLAGS |
+ SPA_NODE_CHANGE_MASK_PROPS |
+ SPA_NODE_CHANGE_MASK_PARAMS;
+
+ parse_dict(&p2, &props);
+ if (props.n_items > 0)
+ info.props = &props;
+
+ parse_param_info(&p2, info.n_params, info.params);
+ }
+
+ pw_resource_notify(resource, struct pw_client_node_methods, update, 0, change_mask,
+ n_params,
+ params, infop);
+ return 0;
+}
+
+static int client_node_demarshal_port_update(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ struct spa_pod_frame f;
+ uint32_t direction, port_id, change_mask, n_params;
+ const struct spa_pod **params = NULL;
+ struct spa_port_info info = SPA_PORT_INFO_INIT(), *infop = NULL;
+ struct spa_pod *ipod;
+ struct spa_dict props = SPA_DICT_INIT(NULL, 0);
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_push_struct(&prs, &f) < 0 ||
+ spa_pod_parser_get(&prs,
+ SPA_POD_Int(&direction),
+ SPA_POD_Int(&port_id),
+ SPA_POD_Int(&change_mask), NULL) < 0)
+ return -EINVAL;
+
+ parse_params(&prs, n_params, params);
+
+ if (spa_pod_parser_get(&prs,
+ SPA_POD_PodStruct(&ipod), NULL) < 0)
+ return -EINVAL;
+
+ if (ipod) {
+ struct spa_pod_parser p2;
+ struct spa_pod_frame f2;
+ infop = &info;
+
+ spa_pod_parser_pod(&p2, ipod);
+ if (spa_pod_parser_push_struct(&p2, &f2) < 0 ||
+ spa_pod_parser_get(&p2,
+ SPA_POD_Long(&info.change_mask),
+ SPA_POD_Long(&info.flags),
+ SPA_POD_Int(&info.rate.num),
+ SPA_POD_Int(&info.rate.denom), NULL) < 0)
+ return -EINVAL;
+
+ info.change_mask &= SPA_PORT_CHANGE_MASK_FLAGS |
+ SPA_PORT_CHANGE_MASK_RATE |
+ SPA_PORT_CHANGE_MASK_PROPS |
+ SPA_PORT_CHANGE_MASK_PARAMS;
+
+ parse_dict(&p2, &props);
+ if (props.n_items > 0)
+ info.props = &props;
+
+ parse_param_info(&p2, info.n_params, info.params);
+ }
+
+ pw_resource_notify(resource, struct pw_client_node_methods, port_update, 0, direction,
+ port_id,
+ change_mask,
+ n_params,
+ params, infop);
+ return 0;
+}
+
+static int client_node_demarshal_set_active(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ bool active;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Bool(&active)) < 0)
+ return -EINVAL;
+
+ pw_resource_notify(resource, struct pw_client_node_methods, set_active, 0, active);
+ return 0;
+}
+
+static int client_node_demarshal_event_method(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ const struct spa_event *event;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_PodObject(&event)) < 0)
+ return -EINVAL;
+
+ pw_resource_notify(resource, struct pw_client_node_methods, event, 0, event);
+ return 0;
+}
+
+static int client_node_demarshal_port_buffers(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ struct spa_pod_frame f;
+ uint32_t i, j, direction, port_id, mix_id, n_buffers;
+ int64_t data_fd;
+ struct spa_buffer **buffers = NULL;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_push_struct(&prs, &f) < 0 ||
+ spa_pod_parser_get(&prs,
+ SPA_POD_Int(&direction),
+ SPA_POD_Int(&port_id),
+ SPA_POD_Int(&mix_id),
+ SPA_POD_Int(&n_buffers), NULL) < 0)
+ return -EINVAL;
+
+ if (n_buffers > MAX_BUFFERS)
+ return -ENOSPC;
+
+ buffers = alloca(sizeof(struct spa_buffer*) * n_buffers);
+ for (i = 0; i < n_buffers; i++) {
+ struct spa_buffer *buf = buffers[i] = alloca(sizeof(struct spa_buffer));
+
+ spa_zero(*buf);
+ if (spa_pod_parser_get(&prs,
+ SPA_POD_Int(&buf->n_datas), NULL) < 0)
+ return -EINVAL;
+
+ if (buf->n_datas > MAX_DATAS)
+ return -ENOSPC;
+
+ buf->datas = alloca(sizeof(struct spa_data) * buf->n_datas);
+ for (j = 0; j < buf->n_datas; j++) {
+ struct spa_data *d = &buf->datas[j];
+
+ if (spa_pod_parser_get(&prs,
+ SPA_POD_Id(&d->type),
+ SPA_POD_Fd(&data_fd),
+ SPA_POD_Int(&d->flags),
+ SPA_POD_Int(&d->mapoffset),
+ SPA_POD_Int(&d->maxsize), NULL) < 0)
+ return -EINVAL;
+
+ d->fd = pw_protocol_native_get_resource_fd(resource, data_fd);
+ }
+ }
+
+ pw_resource_notify(resource, struct pw_client_node_methods, port_buffers, 0,
+ direction, port_id, mix_id, n_buffers, buffers);
+
+ return 0;
+}
+
+static const struct pw_client_node_methods pw_protocol_native_client_node_method_marshal = {
+ PW_VERSION_CLIENT_NODE_METHODS,
+ .add_listener = &client_node_marshal_add_listener,
+ .get_node = &client_node_marshal_get_node,
+ .update = &client_node_marshal_update,
+ .port_update = &client_node_marshal_port_update,
+ .set_active = &client_node_marshal_set_active,
+ .event = &client_node_marshal_event_method,
+ .port_buffers = &client_node_marshal_port_buffers
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_client_node_method_demarshal[PW_CLIENT_NODE_METHOD_NUM] =
+{
+ [PW_CLIENT_NODE_METHOD_ADD_LISTENER] = { NULL, 0 },
+ [PW_CLIENT_NODE_METHOD_GET_NODE] = { &client_node_demarshal_get_node, 0 },
+ [PW_CLIENT_NODE_METHOD_UPDATE] = { &client_node_demarshal_update, 0 },
+ [PW_CLIENT_NODE_METHOD_PORT_UPDATE] = { &client_node_demarshal_port_update, 0 },
+ [PW_CLIENT_NODE_METHOD_SET_ACTIVE] = { &client_node_demarshal_set_active, 0 },
+ [PW_CLIENT_NODE_METHOD_EVENT] = { &client_node_demarshal_event_method, 0 },
+ [PW_CLIENT_NODE_METHOD_PORT_BUFFERS] = { &client_node_demarshal_port_buffers, 0 }
+};
+
+static const struct pw_client_node_events pw_protocol_native_client_node_event_marshal = {
+ PW_VERSION_CLIENT_NODE_EVENTS,
+ .transport = &client_node_marshal_transport,
+ .set_param = &client_node_marshal_set_param,
+ .set_io = &client_node_marshal_set_io,
+ .event = &client_node_marshal_event_event,
+ .command = &client_node_marshal_command,
+ .add_port = &client_node_marshal_add_port,
+ .remove_port = &client_node_marshal_remove_port,
+ .port_set_param = &client_node_marshal_port_set_param,
+ .port_use_buffers = &client_node_marshal_port_use_buffers,
+ .port_set_io = &client_node_marshal_port_set_io,
+ .set_activation = &client_node_marshal_set_activation,
+ .port_set_mix_info = &client_node_marshal_port_set_mix_info,
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_client_node_event_demarshal[PW_CLIENT_NODE_EVENT_NUM] =
+{
+ [PW_CLIENT_NODE_EVENT_TRANSPORT] = { &client_node_demarshal_transport, 0 },
+ [PW_CLIENT_NODE_EVENT_SET_PARAM] = { &client_node_demarshal_set_param, 0 },
+ [PW_CLIENT_NODE_EVENT_SET_IO] = { &client_node_demarshal_set_io, 0 },
+ [PW_CLIENT_NODE_EVENT_EVENT] = { &client_node_demarshal_event_event, 0 },
+ [PW_CLIENT_NODE_EVENT_COMMAND] = { &client_node_demarshal_command, 0 },
+ [PW_CLIENT_NODE_EVENT_ADD_PORT] = { &client_node_demarshal_add_port, 0 },
+ [PW_CLIENT_NODE_EVENT_REMOVE_PORT] = { &client_node_demarshal_remove_port, 0 },
+ [PW_CLIENT_NODE_EVENT_PORT_SET_PARAM] = { &client_node_demarshal_port_set_param, 0 },
+ [PW_CLIENT_NODE_EVENT_PORT_USE_BUFFERS] = { &client_node_demarshal_port_use_buffers, 0 },
+ [PW_CLIENT_NODE_EVENT_PORT_SET_IO] = { &client_node_demarshal_port_set_io, 0 },
+ [PW_CLIENT_NODE_EVENT_SET_ACTIVATION] = { &client_node_demarshal_set_activation, 0 },
+ [PW_CLIENT_NODE_EVENT_PORT_SET_MIX_INFO] = { &client_node_demarshal_port_set_mix_info, 0 }
+};
+
+static const struct pw_protocol_marshal pw_protocol_native_client_node_marshal = {
+ PW_TYPE_INTERFACE_ClientNode,
+ PW_VERSION_CLIENT_NODE,
+ 0,
+ PW_CLIENT_NODE_METHOD_NUM,
+ PW_CLIENT_NODE_EVENT_NUM,
+ .client_marshal = &pw_protocol_native_client_node_method_marshal,
+ .server_demarshal = &pw_protocol_native_client_node_method_demarshal,
+ .server_marshal = &pw_protocol_native_client_node_event_marshal,
+ .client_demarshal = pw_protocol_native_client_node_event_demarshal,
+};
+
+struct pw_protocol *pw_protocol_native_ext_client_node_init(struct pw_context *context)
+{
+ struct pw_protocol *protocol;
+
+ protocol = pw_context_find_protocol(context, PW_TYPE_INFO_PROTOCOL_Native);
+
+ if (protocol == NULL)
+ return NULL;
+
+ pw_protocol_add_marshal(protocol, &pw_protocol_native_client_node_marshal);
+
+ return protocol;
+}
diff --git a/src/modules/module-client-node/remote-node.c b/src/modules/module-client-node/remote-node.c
new file mode 100644
index 0000000..051ab07
--- /dev/null
+++ b/src/modules/module-client-node/remote-node.c
@@ -0,0 +1,1339 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdio.h>
+#include <unistd.h>
+#include <sys/socket.h>
+#include <sys/un.h>
+#include <errno.h>
+#include <time.h>
+#include <sys/mman.h>
+
+#include <spa/pod/parser.h>
+#include <spa/pod/dynamic.h>
+#include <spa/node/utils.h>
+#include <spa/utils/result.h>
+#include <spa/debug/types.h>
+
+#include "pipewire/pipewire.h"
+#include "pipewire/private.h"
+
+#include "pipewire/extensions/protocol-native.h"
+#include "pipewire/extensions/client-node.h"
+
+#define MAX_BUFFERS 64
+
+PW_LOG_TOPIC_EXTERN(mod_topic);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+/** \cond */
+static bool mlock_warned = false;
+
+struct buffer {
+ uint32_t id;
+ struct spa_buffer *buf;
+ struct pw_memmap *mem;
+};
+
+struct mix {
+ struct spa_list link;
+ struct pw_impl_port *port;
+ uint32_t mix_id;
+ struct pw_impl_port_mix mix;
+ struct pw_array buffers;
+ bool active;
+};
+
+struct node_data {
+ struct pw_context *context;
+
+ struct pw_mempool *pool;
+
+ uint32_t remote_id;
+ int rtwritefd;
+ struct pw_memmap *activation;
+
+ struct spa_list mix[2];
+ struct spa_list free_mix;
+
+ struct pw_impl_node *node;
+ struct spa_hook node_listener;
+ unsigned int do_free:1;
+ unsigned int have_transport:1;
+ unsigned int allow_mlock:1;
+ unsigned int warn_mlock:1;
+
+ struct pw_client_node *client_node;
+ struct spa_hook client_node_listener;
+ struct spa_hook proxy_client_node_listener;
+
+ struct spa_list links;
+};
+
+struct link {
+ struct spa_list link;
+ struct node_data *data;
+ struct pw_memmap *map;
+ struct pw_node_target target;
+ uint32_t node_id;
+ int signalfd;
+};
+
+/** \endcond */
+
+static struct link *find_activation(struct spa_list *links, uint32_t node_id)
+{
+ struct link *l;
+
+ spa_list_for_each(l, links, link) {
+ if (l->node_id == node_id)
+ return l;
+ }
+ return NULL;
+}
+
+static int
+do_deactivate_link(struct spa_loop *loop,
+ bool async, uint32_t seq, const void *data, size_t size, void *user_data)
+{
+ struct link *link = user_data;
+ pw_log_trace("link %p deactivate", link);
+ spa_list_remove(&link->target.link);
+ return 0;
+}
+
+static void clear_link(struct node_data *data, struct link *link)
+{
+ struct pw_context *context = data->context;
+ pw_log_debug("link %p", link);
+ pw_loop_invoke(context->data_loop,
+ do_deactivate_link, SPA_ID_INVALID, NULL, 0, true, link);
+ pw_memmap_free(link->map);
+ spa_system_close(context->data_system, link->signalfd);
+ spa_list_remove(&link->link);
+ free(link);
+}
+
+static void clean_transport(struct node_data *data)
+{
+ struct link *l;
+ uint32_t tag[5] = { data->remote_id, };
+ struct pw_memmap *mm;
+
+ if (!data->have_transport)
+ return;
+
+ spa_list_consume(l, &data->links, link)
+ clear_link(data, l);
+
+ while ((mm = pw_mempool_find_tag(data->pool, tag, sizeof(uint32_t))) != NULL) {
+ if (mm->tag[1] == SPA_ID_INVALID)
+ spa_node_set_io(data->node->node, mm->tag[2], NULL, 0);
+
+ pw_memmap_free(mm);
+ }
+
+ pw_memmap_free(data->activation);
+ data->node->rt.activation = data->node->activation->map->ptr;
+
+ spa_system_close(data->context->data_system, data->rtwritefd);
+ data->have_transport = false;
+}
+
+static void mix_init(struct mix *mix, struct pw_impl_port *port, uint32_t mix_id)
+{
+ pw_log_debug("port %p: mix init %d.%d", port, port->port_id, mix_id);
+ mix->port = port;
+ mix->mix_id = mix_id;
+ pw_impl_port_init_mix(port, &mix->mix);
+ mix->active = false;
+ pw_array_init(&mix->buffers, 32);
+ pw_array_ensure_size(&mix->buffers, sizeof(struct buffer) * 64);
+}
+
+static int
+do_deactivate_mix(struct spa_loop *loop,
+ bool async, uint32_t seq, const void *data, size_t size, void *user_data)
+{
+ struct mix *mix = user_data;
+ spa_list_remove(&mix->mix.rt_link);
+ return 0;
+}
+
+static int
+deactivate_mix(struct node_data *data, struct mix *mix)
+{
+ if (mix->active) {
+ pw_log_debug("node %p: mix %p deactivate", data, mix);
+ pw_loop_invoke(data->context->data_loop,
+ do_deactivate_mix, SPA_ID_INVALID, NULL, 0, true, mix);
+ mix->active = false;
+ }
+ return 0;
+}
+
+static int
+do_activate_mix(struct spa_loop *loop,
+ bool async, uint32_t seq, const void *data, size_t size, void *user_data)
+{
+ struct mix *mix = user_data;
+
+ spa_list_append(&mix->port->rt.mix_list, &mix->mix.rt_link);
+ return 0;
+}
+
+static int
+activate_mix(struct node_data *data, struct mix *mix)
+{
+ if (!mix->active) {
+ pw_log_debug("node %p: mix %p activate", data, mix);
+ pw_loop_invoke(data->context->data_loop,
+ do_activate_mix, SPA_ID_INVALID, NULL, 0, false, mix);
+ mix->active = true;
+ }
+ return 0;
+}
+
+static struct mix *find_mix(struct node_data *data,
+ enum spa_direction direction, uint32_t port_id, uint32_t mix_id)
+{
+ struct mix *mix;
+
+ spa_list_for_each(mix, &data->mix[direction], link) {
+ if (mix->port->port_id == port_id &&
+ mix->mix_id == mix_id) {
+ pw_log_debug("port %p: found mix %d:%d.%d", mix->port,
+ direction, port_id, mix_id);
+ return mix;
+ }
+ }
+ return NULL;
+}
+
+static struct mix *ensure_mix(struct node_data *data,
+ enum spa_direction direction, uint32_t port_id, uint32_t mix_id)
+{
+ struct mix *mix;
+ struct pw_impl_port *port;
+
+ if ((mix = find_mix(data, direction, port_id, mix_id)))
+ return mix;
+
+ port = pw_impl_node_find_port(data->node, direction, port_id);
+ if (port == NULL)
+ return NULL;
+
+ if (spa_list_is_empty(&data->free_mix)) {
+ if ((mix = calloc(1, sizeof(*mix))) == NULL)
+ return NULL;
+ } else {
+ mix = spa_list_first(&data->free_mix, struct mix, link);
+ spa_list_remove(&mix->link);
+ }
+
+ mix_init(mix, port, mix_id);
+ spa_list_append(&data->mix[direction], &mix->link);
+
+ return mix;
+}
+
+
+static int client_node_transport(void *_data,
+ int readfd, int writefd, uint32_t mem_id, uint32_t offset, uint32_t size)
+{
+ struct node_data *data = _data;
+ struct pw_proxy *proxy = (struct pw_proxy*)data->client_node;
+
+ clean_transport(data);
+
+ data->activation = pw_mempool_map_id(data->pool, mem_id,
+ PW_MEMMAP_FLAG_READWRITE, offset, size, NULL);
+ if (data->activation == NULL) {
+ pw_log_warn("remote-node %p: can't map activation: %m", proxy);
+ return -errno;
+ }
+
+ data->node->rt.activation = data->activation->ptr;
+
+ pw_log_debug("remote-node %p: fds:%d %d node:%u activation:%p",
+ proxy, readfd, writefd, data->remote_id, data->activation->ptr);
+
+ data->rtwritefd = writefd;
+ spa_system_close(data->context->data_system, data->node->source.fd);
+ data->node->source.fd = readfd;
+
+ data->have_transport = true;
+
+ if (data->node->active)
+ pw_client_node_set_active(data->client_node, true);
+
+ return 0;
+}
+
+static int add_node_update(struct node_data *data, uint32_t change_mask, uint32_t info_mask)
+{
+ struct pw_impl_node *node = data->node;
+ struct spa_node_info ni = SPA_NODE_INFO_INIT();
+ uint32_t n_params = 0;
+ struct spa_pod **params = NULL;
+ int res;
+
+ if (change_mask & PW_CLIENT_NODE_UPDATE_PARAMS) {
+ uint32_t i, idx, id;
+ uint8_t buf[4096];
+ struct spa_pod_dynamic_builder b;
+
+ for (i = 0; i < node->info.n_params; i++) {
+ struct spa_pod *param;
+
+ id = node->info.params[i].id;
+ if (id == SPA_PARAM_Invalid)
+ continue;
+
+ for (idx = 0;;) {
+ spa_pod_dynamic_builder_init(&b, buf, sizeof(buf), 4096);
+
+ res = spa_node_enum_params_sync(node->node,
+ id, &idx, NULL, &param, &b.b);
+ if (res == 1) {
+ void *p;
+ p = pw_reallocarray(params, n_params + 1, sizeof(struct spa_pod *));
+ if (p == NULL) {
+ res = -errno;
+ pw_log_error("realloc failed: %m");
+ } else {
+ params = p;
+ params[n_params++] = spa_pod_copy(param);
+ }
+ }
+ spa_pod_dynamic_builder_clean(&b);
+ if (res != 1)
+ break;
+ }
+ }
+ }
+ if (change_mask & PW_CLIENT_NODE_UPDATE_INFO) {
+ ni.max_input_ports = node->info.max_input_ports;
+ ni.max_output_ports = node->info.max_output_ports;
+ ni.change_mask = info_mask;
+ ni.flags = node->spa_flags;
+ ni.props = node->info.props;
+ ni.params = node->info.params;
+ ni.n_params = node->info.n_params;
+ }
+
+ res = pw_client_node_update(data->client_node,
+ change_mask,
+ n_params,
+ (const struct spa_pod **)params,
+ &ni);
+
+ if (params) {
+ while (n_params > 0)
+ free(params[--n_params]);
+ free(params);
+ }
+ return res;
+}
+
+static int add_port_update(struct node_data *data, struct pw_impl_port *port, uint32_t change_mask)
+{
+ struct spa_port_info pi = SPA_PORT_INFO_INIT();
+ uint32_t n_params = 0;
+ struct spa_pod **params = NULL;
+ int res;
+
+ if (change_mask & PW_CLIENT_NODE_PORT_UPDATE_PARAMS) {
+ uint32_t i, idx, id;
+ uint8_t buf[4096];
+ struct spa_pod_dynamic_builder b;
+
+ for (i = 0; i < port->info.n_params; i++) {
+ struct spa_pod *param;
+
+ id = port->info.params[i].id;
+ if (id == SPA_PARAM_Invalid)
+ continue;
+
+ for (idx = 0;;) {
+ spa_pod_dynamic_builder_init(&b, buf, sizeof(buf), 4096);
+
+ res = spa_node_port_enum_params_sync(port->node->node,
+ port->direction, port->port_id,
+ id, &idx, NULL, &param, &b.b);
+ if (res == 1) {
+ void *p;
+ p = pw_reallocarray(params, n_params + 1, sizeof(struct spa_pod*));
+ if (p == NULL) {
+ res = -errno;
+ pw_log_error("realloc failed: %m");
+ } else {
+ params = p;
+ params[n_params++] = spa_pod_copy(param);
+ }
+ }
+ spa_pod_dynamic_builder_clean(&b);
+
+ if (res != 1)
+ break;
+
+ }
+ }
+ }
+ if (change_mask & PW_CLIENT_NODE_PORT_UPDATE_INFO) {
+ pi.change_mask = SPA_PORT_CHANGE_MASK_FLAGS |
+ SPA_PORT_CHANGE_MASK_RATE |
+ SPA_PORT_CHANGE_MASK_PROPS |
+ SPA_PORT_CHANGE_MASK_PARAMS;
+ pi.flags = port->spa_flags;
+ pi.rate = SPA_FRACTION(0, 1);
+ pi.props = &port->properties->dict;
+ SPA_FLAG_CLEAR(pi.flags, SPA_PORT_FLAG_DYNAMIC_DATA);
+ pi.n_params = port->info.n_params;
+ pi.params = port->info.params;
+ }
+
+ res = pw_client_node_port_update(data->client_node,
+ port->direction,
+ port->port_id,
+ change_mask,
+ n_params,
+ (const struct spa_pod **)params,
+ &pi);
+ if (params) {
+ while (n_params > 0)
+ free(params[--n_params]);
+ free(params);
+ }
+ return res;
+}
+
+static int
+client_node_set_param(void *_data, uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct node_data *data = _data;
+ struct pw_proxy *proxy = (struct pw_proxy*)data->client_node;
+ int res;
+
+ pw_log_debug("node %p: set_param %s:", proxy,
+ spa_debug_type_find_name(spa_type_param, id));
+
+ res = spa_node_set_param(data->node->node, id, flags, param);
+
+ if (res < 0) {
+ pw_log_error("node %p: set_param %s (%d) %p: %s", proxy,
+ spa_debug_type_find_name(spa_type_param, id),
+ id, param, spa_strerror(res));
+ pw_proxy_errorf(proxy, res, "node_set_param(%s) failed: %s",
+ spa_debug_type_find_name(spa_type_param, id),
+ spa_strerror(res));
+ }
+ return res;
+}
+
+static int
+client_node_set_io(void *_data,
+ uint32_t id,
+ uint32_t memid,
+ uint32_t offset,
+ uint32_t size)
+{
+ struct node_data *data = _data;
+ struct pw_proxy *proxy = (struct pw_proxy*)data->client_node;
+ struct pw_memmap *old, *mm;
+ void *ptr;
+ uint32_t tag[5] = { data->remote_id, SPA_ID_INVALID, id, };
+ int res;
+
+ old = pw_mempool_find_tag(data->pool, tag, sizeof(tag));
+
+ if (memid == SPA_ID_INVALID) {
+ mm = ptr = NULL;
+ size = 0;
+ } else {
+ mm = pw_mempool_map_id(data->pool, memid,
+ PW_MEMMAP_FLAG_READWRITE, offset, size, tag);
+ if (mm == NULL) {
+ pw_log_warn("can't map memory id %u: %m", memid);
+ res = -errno;
+ goto exit;
+ }
+ ptr = mm->ptr;
+ }
+
+ pw_log_debug("node %p: set io %s %p", proxy,
+ spa_debug_type_find_name(spa_type_io, id), ptr);
+
+ res = spa_node_set_io(data->node->node, id, ptr, size);
+
+ pw_memmap_free(old);
+exit:
+ if (res < 0) {
+ pw_log_error("node %p: set_io: %s", proxy, spa_strerror(res));
+ pw_proxy_errorf(proxy, res, "node_set_io failed: %s", spa_strerror(res));
+ }
+ return res;
+}
+
+static int client_node_event(void *data, const struct spa_event *event)
+{
+ pw_log_warn("unhandled node event %d", SPA_EVENT_TYPE(event));
+ return -ENOTSUP;
+}
+
+static int client_node_command(void *_data, const struct spa_command *command)
+{
+ struct node_data *data = _data;
+ struct pw_proxy *proxy = (struct pw_proxy*)data->client_node;
+ int res;
+
+ switch (SPA_NODE_COMMAND_ID(command)) {
+ case SPA_NODE_COMMAND_Pause:
+ pw_log_debug("node %p: pause", proxy);
+
+ if ((res = pw_impl_node_set_state(data->node, PW_NODE_STATE_IDLE)) < 0) {
+ pw_log_warn("node %p: pause failed", proxy);
+ pw_proxy_error(proxy, res, "pause failed");
+ }
+
+ break;
+ case SPA_NODE_COMMAND_Start:
+ pw_log_debug("node %p: start", proxy);
+
+ if ((res = pw_impl_node_set_state(data->node, PW_NODE_STATE_RUNNING)) < 0) {
+ pw_log_warn("node %p: start failed", proxy);
+ pw_proxy_error(proxy, res, "start failed");
+ }
+ break;
+
+ case SPA_NODE_COMMAND_Suspend:
+ pw_log_debug("node %p: suspend", proxy);
+ if ((res = pw_impl_node_set_state(data->node, PW_NODE_STATE_SUSPENDED)) < 0) {
+ pw_log_warn("node %p: suspend failed", proxy);
+ pw_proxy_error(proxy, res, "suspend failed");
+ }
+ break;
+ case SPA_NODE_COMMAND_RequestProcess:
+ res = pw_impl_node_send_command(data->node, command);
+ break;
+ default:
+ pw_log_warn("unhandled node command %d", SPA_NODE_COMMAND_ID(command));
+ res = -ENOTSUP;
+ pw_proxy_errorf(proxy, res, "command %d not supported", SPA_NODE_COMMAND_ID(command));
+ }
+ return res;
+}
+
+static int
+client_node_add_port(void *_data, enum spa_direction direction, uint32_t port_id,
+ const struct spa_dict *props)
+{
+ struct node_data *data = _data;
+ struct pw_proxy *proxy = (struct pw_proxy*)data->client_node;
+ pw_log_warn("add port not supported");
+ pw_proxy_error(proxy, -ENOTSUP, "add port not supported");
+ return -ENOTSUP;
+}
+
+static int
+client_node_remove_port(void *_data, enum spa_direction direction, uint32_t port_id)
+{
+ struct node_data *data = _data;
+ struct pw_proxy *proxy = (struct pw_proxy*)data->client_node;
+ pw_log_warn("remove port not supported");
+ pw_proxy_error(proxy, -ENOTSUP, "remove port not supported");
+ return -ENOTSUP;
+}
+
+static int clear_buffers(struct node_data *data, struct mix *mix)
+{
+ struct pw_impl_port *port = mix->port;
+ struct buffer *b;
+ int res;
+
+ pw_log_debug("port %p: clear %zd buffers mix:%d", port,
+ pw_array_get_len(&mix->buffers, struct buffer *),
+ mix->mix_id);
+
+ if ((res = pw_impl_port_use_buffers(port, &mix->mix, 0, NULL, 0)) < 0) {
+ pw_log_error("port %p: error clear buffers %s", port, spa_strerror(res));
+ return res;
+ }
+
+ pw_array_for_each(b, &mix->buffers) {
+ pw_log_debug("port %p: clear buffer %d map %p %p",
+ port, b->id, b->mem, b->buf);
+ pw_memmap_free(b->mem);
+ free(b->buf);
+ }
+ mix->buffers.size = 0;
+ return 0;
+}
+
+static int
+client_node_port_set_param(void *_data,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct node_data *data = _data;
+ struct pw_proxy *proxy = (struct pw_proxy*)data->client_node;
+ struct pw_impl_port *port;
+ int res;
+
+ port = pw_impl_node_find_port(data->node, direction, port_id);
+ if (port == NULL) {
+ res = -EINVAL;
+ goto error_exit;
+ }
+
+ pw_log_debug("port %p: set_param %s %p", port,
+ spa_debug_type_find_name(spa_type_param, id), param);
+
+ res = pw_impl_port_set_param(port, id, flags, param);
+ if (res < 0)
+ goto error_exit;
+
+ if (id == SPA_PARAM_Format) {
+ struct mix *mix;
+ spa_list_for_each(mix, &data->mix[direction], link) {
+ if (mix->port->port_id == port_id)
+ clear_buffers(data, mix);
+ }
+ }
+ return res;
+
+error_exit:
+ pw_log_error("port %p: set_param %d %p: %s", port, id, param, spa_strerror(res));
+ pw_proxy_errorf(proxy, res, "port_set_param(%s) failed: %s",
+ spa_debug_type_find_name(spa_type_param, id),
+ spa_strerror(res));
+ return res;
+}
+
+static int
+client_node_port_use_buffers(void *_data,
+ enum spa_direction direction, uint32_t port_id, uint32_t mix_id,
+ uint32_t flags,
+ uint32_t n_buffers, struct pw_client_node_buffer *buffers)
+{
+ struct node_data *data = _data;
+ struct pw_proxy *proxy = (struct pw_proxy*)data->client_node;
+ struct buffer *bid;
+ uint32_t i, j;
+ struct spa_buffer *b, **bufs;
+ struct mix *mix;
+ int res, prot;
+
+ mix = ensure_mix(data, direction, port_id, mix_id);
+ if (mix == NULL) {
+ res = -ENOENT;
+ goto error_exit;
+ }
+
+ if (n_buffers > MAX_BUFFERS)
+ return -ENOSPC;
+
+ prot = PW_MEMMAP_FLAG_READWRITE;
+
+ /* clear previous buffers */
+ clear_buffers(data, mix);
+
+ bufs = alloca(n_buffers * sizeof(struct spa_buffer *));
+
+ for (i = 0; i < n_buffers; i++) {
+ size_t size;
+ off_t offset;
+ struct pw_memmap *mm;
+
+ mm = pw_mempool_map_id(data->pool, buffers[i].mem_id,
+ prot, buffers[i].offset, buffers[i].size, NULL);
+ if (mm == NULL) {
+ res = -errno;
+ goto error_exit_cleanup;
+ }
+
+ bid = pw_array_add(&mix->buffers, sizeof(struct buffer));
+ if (bid == NULL) {
+ res = -errno;
+ goto error_exit_cleanup;
+ }
+ bid->id = i;
+ bid->mem = mm;
+
+ if (data->allow_mlock && mlock(mm->ptr, mm->size) < 0)
+ if (errno != ENOMEM || !mlock_warned) {
+ pw_log(data->warn_mlock ? SPA_LOG_LEVEL_WARN : SPA_LOG_LEVEL_DEBUG,
+ "Failed to mlock memory %p %u: %s",
+ mm->ptr, mm->size,
+ errno == ENOMEM ?
+ "This is not a problem but for best performance, "
+ "consider increasing RLIMIT_MEMLOCK" : strerror(errno));
+ mlock_warned |= errno == ENOMEM;
+ }
+
+ size = sizeof(struct spa_buffer);
+ for (j = 0; j < buffers[i].buffer->n_metas; j++)
+ size += sizeof(struct spa_meta);
+ for (j = 0; j < buffers[i].buffer->n_datas; j++)
+ size += sizeof(struct spa_data);
+
+ b = bid->buf = malloc(size);
+ if (b == NULL) {
+ res = -errno;
+ goto error_exit_cleanup;
+ }
+ memcpy(b, buffers[i].buffer, sizeof(struct spa_buffer));
+
+ b->metas = SPA_PTROFF(b, sizeof(struct spa_buffer), struct spa_meta);
+ b->datas = SPA_PTROFF(b->metas, sizeof(struct spa_meta) * b->n_metas,
+ struct spa_data);
+
+ pw_log_debug("add buffer mem:%d id:%d offset:%u size:%u %p", mm->block->id,
+ bid->id, buffers[i].offset, buffers[i].size, bid->buf);
+
+ offset = 0;
+ for (j = 0; j < b->n_metas; j++) {
+ struct spa_meta *m = &b->metas[j];
+ memcpy(m, &buffers[i].buffer->metas[j], sizeof(struct spa_meta));
+ m->data = SPA_PTROFF(mm->ptr, offset, void);
+ offset += SPA_ROUND_UP_N(m->size, 8);
+ }
+
+ for (j = 0; j < b->n_datas; j++) {
+ struct spa_data *d = &b->datas[j];
+
+ memcpy(d, &buffers[i].buffer->datas[j], sizeof(struct spa_data));
+ d->chunk =
+ SPA_PTROFF(mm->ptr, offset + sizeof(struct spa_chunk) * j,
+ struct spa_chunk);
+
+ if (flags & SPA_NODE_BUFFERS_FLAG_ALLOC)
+ continue;
+
+ if (d->type == SPA_DATA_MemId) {
+ uint32_t mem_id = SPA_PTR_TO_UINT32(d->data);
+ struct pw_memblock *bm;
+
+ bm = pw_mempool_find_id(data->pool, mem_id);
+ if (bm == NULL) {
+ pw_log_error("unknown buffer mem %u", mem_id);
+ res = -ENODEV;
+ goto error_exit_cleanup;
+ }
+
+ d->fd = bm->fd;
+ d->type = bm->type;
+ d->data = NULL;
+
+ pw_log_debug(" data %d %u -> fd %d maxsize %d",
+ j, bm->id, bm->fd, d->maxsize);
+ } else if (d->type == SPA_DATA_MemPtr) {
+ int offs = SPA_PTR_TO_INT(d->data);
+ d->data = SPA_PTROFF(mm->ptr, offs, void);
+ d->fd = -1;
+ pw_log_debug(" data %d id:%u -> mem:%p offs:%d maxsize:%d",
+ j, bid->id, d->data, offs, d->maxsize);
+ } else {
+ pw_log_warn("unknown buffer data type %d", d->type);
+ }
+ }
+ bufs[i] = b;
+ }
+
+ if ((res = pw_impl_port_use_buffers(mix->port, &mix->mix, flags, bufs, n_buffers)) < 0)
+ goto error_exit_cleanup;
+
+ if (flags & SPA_NODE_BUFFERS_FLAG_ALLOC) {
+ pw_client_node_port_buffers(data->client_node,
+ direction, port_id, mix_id,
+ n_buffers,
+ bufs);
+ }
+ return res;
+
+error_exit_cleanup:
+ clear_buffers(data, mix);
+error_exit:
+ pw_log_error("port %p: use_buffers: %d %s", mix, res, spa_strerror(res));
+ pw_proxy_errorf(proxy, res, "port_use_buffers error: %s", spa_strerror(res));
+ return res;
+}
+
+static int
+client_node_port_set_io(void *_data,
+ uint32_t direction,
+ uint32_t port_id,
+ uint32_t mix_id,
+ uint32_t id,
+ uint32_t memid,
+ uint32_t offset,
+ uint32_t size)
+{
+ struct node_data *data = _data;
+ struct pw_proxy *proxy = (struct pw_proxy*)data->client_node;
+ struct mix *mix;
+ struct pw_memmap *mm, *old;
+ void *ptr;
+ int res = 0;
+ uint32_t tag[5] = { data->remote_id, direction, port_id, mix_id, id };
+
+ mix = ensure_mix(data, direction, port_id, mix_id);
+ if (mix == NULL) {
+ res = -ENOENT;
+ goto exit;
+ }
+
+ old = pw_mempool_find_tag(data->pool, tag, sizeof(tag));
+
+ if (memid == SPA_ID_INVALID) {
+ mm = ptr = NULL;
+ size = 0;
+ }
+ else {
+ mm = pw_mempool_map_id(data->pool, memid,
+ PW_MEMMAP_FLAG_READWRITE, offset, size, tag);
+ if (mm == NULL) {
+ pw_log_warn("can't map memory id %u: %m", memid);
+ res = -errno;
+ goto exit;
+ }
+ ptr = mm->ptr;
+ }
+
+ pw_log_debug("port %p: set io:%s new:%p old:%p", mix->port,
+ spa_debug_type_find_name(spa_type_io, id), ptr, mix->mix.io);
+
+ if (id == SPA_IO_Buffers) {
+ if (ptr == NULL && mix->mix.io)
+ deactivate_mix(data, mix);
+ }
+
+ if ((res = spa_node_port_set_io(mix->port->mix,
+ direction, mix->mix.port.port_id, id, ptr, size)) < 0) {
+ if (res == -ENOTSUP)
+ res = 0;
+ else
+ goto exit_free;
+ }
+ if (id == SPA_IO_Buffers) {
+ mix->mix.io = ptr;
+ if (ptr)
+ activate_mix(data, mix);
+ }
+exit_free:
+ pw_memmap_free(old);
+exit:
+ if (res < 0) {
+ pw_log_error("port %p: set_io: %s", mix, spa_strerror(res));
+ pw_proxy_errorf(proxy, res, "port_set_io failed: %s", spa_strerror(res));
+ }
+ return res;
+}
+
+static int link_signal_func(void *user_data)
+{
+ struct link *link = user_data;
+ struct spa_system *data_system = link->data->context->data_system;
+
+ pw_log_trace_fp("link %p: signal %p", link, link->target.activation);
+ if (SPA_UNLIKELY(spa_system_eventfd_write(data_system, link->signalfd, 1) < 0))
+ pw_log_warn("link %p: write failed %m", link);
+
+ return 0;
+}
+
+static int
+do_activate_link(struct spa_loop *loop,
+ bool async, uint32_t seq, const void *data, size_t size, void *user_data)
+{
+ struct link *link = user_data;
+ struct node_data *d = link->data;
+ pw_log_trace("link %p activate", link);
+ spa_list_append(&d->node->rt.target_list, &link->target.link);
+ return 0;
+}
+
+static int
+client_node_set_activation(void *_data,
+ uint32_t node_id,
+ int signalfd,
+ uint32_t memid,
+ uint32_t offset,
+ uint32_t size)
+{
+ struct node_data *data = _data;
+ struct pw_proxy *proxy = (struct pw_proxy*)data->client_node;
+ struct pw_impl_node *node = data->node;
+ struct pw_memmap *mm;
+ void *ptr;
+ struct link *link;
+ int res = 0;
+
+ if (data->remote_id == node_id) {
+ pw_log_debug("node %p: our activation %u: %u %u %u", node, node_id,
+ memid, offset, size);
+ spa_system_close(data->context->data_system, signalfd);
+ return 0;
+ }
+
+ if (memid == SPA_ID_INVALID) {
+ mm = ptr = NULL;
+ size = 0;
+ } else {
+ mm = pw_mempool_map_id(data->pool, memid,
+ PW_MEMMAP_FLAG_READWRITE, offset, size, NULL);
+ if (mm == NULL) {
+ res = -errno;
+ goto error_exit;
+ }
+ ptr = mm->ptr;
+ }
+ pw_log_debug("node %p: set activation %d %p %u %u", node, node_id, ptr, offset, size);
+
+ if (ptr) {
+ link = calloc(1, sizeof(struct link));
+ if (link == NULL) {
+ res = -errno;
+ goto error_exit;
+ }
+ link->data = data;
+ link->node_id = node_id;
+ link->map = mm;
+ link->target.activation = ptr;
+ link->signalfd = signalfd;
+ link->target.signal_func = link_signal_func;
+ link->target.data = link;
+ link->target.node = NULL;
+ spa_list_append(&data->links, &link->link);
+
+ pw_loop_invoke(data->context->data_loop,
+ do_activate_link, SPA_ID_INVALID, NULL, 0, false, link);
+
+ pw_log_debug("node %p: link %p: fd:%d id:%u state %p required %d, pending %d",
+ node, link, signalfd,
+ link->target.activation->position.clock.id,
+ &link->target.activation->state[0],
+ link->target.activation->state[0].required,
+ link->target.activation->state[0].pending);
+ } else {
+ link = find_activation(&data->links, node_id);
+ if (link == NULL) {
+ res = -ENOENT;
+ goto error_exit;
+ }
+ clear_link(data, link);
+ }
+ return res;
+
+error_exit:
+ pw_log_error("node %p: set activation %d: %s", node, node_id, spa_strerror(res));
+ pw_proxy_errorf(proxy, res, "set_activation: %s", spa_strerror(res));
+ return res;
+}
+
+static const struct pw_client_node_events client_node_events = {
+ PW_VERSION_CLIENT_NODE_EVENTS,
+ .transport = client_node_transport,
+ .set_param = client_node_set_param,
+ .set_io = client_node_set_io,
+ .event = client_node_event,
+ .command = client_node_command,
+ .add_port = client_node_add_port,
+ .remove_port = client_node_remove_port,
+ .port_set_param = client_node_port_set_param,
+ .port_use_buffers = client_node_port_use_buffers,
+ .port_set_io = client_node_port_set_io,
+ .set_activation = client_node_set_activation,
+};
+
+static void do_node_init(struct node_data *data)
+{
+ struct pw_impl_port *port;
+
+ pw_log_debug("%p: node %p init", data, data->node);
+ add_node_update(data, PW_CLIENT_NODE_UPDATE_PARAMS |
+ PW_CLIENT_NODE_UPDATE_INFO,
+ SPA_NODE_CHANGE_MASK_FLAGS |
+ SPA_NODE_CHANGE_MASK_PROPS |
+ SPA_NODE_CHANGE_MASK_PARAMS);
+
+ spa_list_for_each(port, &data->node->input_ports, link) {
+ add_port_update(data, port,
+ PW_CLIENT_NODE_PORT_UPDATE_PARAMS |
+ PW_CLIENT_NODE_PORT_UPDATE_INFO);
+ }
+ spa_list_for_each(port, &data->node->output_ports, link) {
+ add_port_update(data, port,
+ PW_CLIENT_NODE_PORT_UPDATE_PARAMS |
+ PW_CLIENT_NODE_PORT_UPDATE_INFO);
+ }
+}
+
+static void clear_mix(struct node_data *data, struct mix *mix)
+{
+ pw_log_debug("port %p: mix clear %d.%d", mix->port, mix->port->port_id, mix->mix_id);
+
+ deactivate_mix(data, mix);
+
+ spa_list_remove(&mix->link);
+
+ clear_buffers(data, mix);
+ pw_array_clear(&mix->buffers);
+
+ spa_list_append(&data->free_mix, &mix->link);
+ pw_impl_port_release_mix(mix->port, &mix->mix);
+}
+
+static void clean_node(struct node_data *d)
+{
+ struct mix *mix;
+
+ if (d->have_transport) {
+ spa_list_consume(mix, &d->mix[SPA_DIRECTION_INPUT], link)
+ clear_mix(d, mix);
+ spa_list_consume(mix, &d->mix[SPA_DIRECTION_OUTPUT], link)
+ clear_mix(d, mix);
+ }
+ spa_list_consume(mix, &d->free_mix, link) {
+ spa_list_remove(&mix->link);
+ free(mix);
+ }
+ clean_transport(d);
+}
+
+static void node_destroy(void *data)
+{
+ struct node_data *d = data;
+
+ pw_log_debug("%p: destroy", d);
+
+ clean_node(d);
+}
+
+static void node_free(void *data)
+{
+ struct node_data *d = data;
+ pw_log_debug("%p: free", d);
+ d->node = NULL;
+}
+
+static void node_info_changed(void *data, const struct pw_node_info *info)
+{
+ struct node_data *d = data;
+ uint32_t change_mask, info_mask;
+
+ pw_log_debug("info changed %p", d);
+
+ if (d->client_node == NULL)
+ return;
+
+ change_mask = PW_CLIENT_NODE_UPDATE_INFO;
+ info_mask = SPA_NODE_CHANGE_MASK_FLAGS;
+ if (info->change_mask & PW_NODE_CHANGE_MASK_PROPS) {
+ info_mask |= SPA_NODE_CHANGE_MASK_PROPS;
+ }
+ if (info->change_mask & PW_NODE_CHANGE_MASK_PARAMS) {
+ change_mask |= PW_CLIENT_NODE_UPDATE_PARAMS;
+ info_mask |= SPA_NODE_CHANGE_MASK_PARAMS;
+ }
+ add_node_update(d, change_mask, info_mask);
+}
+
+static void node_port_info_changed(void *data, struct pw_impl_port *port,
+ const struct pw_port_info *info)
+{
+ struct node_data *d = data;
+ uint32_t change_mask = 0;
+
+ pw_log_debug("info changed %p", d);
+
+ if (d->client_node == NULL)
+ return;
+
+ if (info->change_mask & PW_PORT_CHANGE_MASK_PROPS)
+ change_mask |= PW_CLIENT_NODE_PORT_UPDATE_INFO;
+ if (info->change_mask & PW_PORT_CHANGE_MASK_PARAMS) {
+ change_mask |= PW_CLIENT_NODE_PORT_UPDATE_PARAMS;
+ change_mask |= PW_CLIENT_NODE_PORT_UPDATE_INFO;
+ }
+ add_port_update(d, port, change_mask);
+}
+
+static void node_port_removed(void *data, struct pw_impl_port *port)
+{
+ struct node_data *d = data;
+ struct mix *mix, *tmp;
+
+ pw_log_debug("removed %p", d);
+
+ if (d->client_node == NULL)
+ return;
+
+ pw_client_node_port_update(d->client_node,
+ port->direction,
+ port->port_id,
+ 0, 0, NULL, NULL);
+
+ spa_list_for_each_safe(mix, tmp, &d->mix[port->direction], link) {
+ if (mix->port == port)
+ clear_mix(d, mix);
+ }
+}
+
+static void node_active_changed(void *data, bool active)
+{
+ struct node_data *d = data;
+ pw_log_debug("active %d", active);
+
+ if (d->client_node == NULL)
+ return;
+
+ pw_client_node_set_active(d->client_node, active);
+}
+
+static void node_event(void *data, const struct spa_event *event)
+{
+ struct node_data *d = data;
+ pw_log_debug("%p", d);
+
+ if (d->client_node == NULL)
+ return;
+ pw_client_node_event(d->client_node, event);
+}
+
+static const struct pw_impl_node_events node_events = {
+ PW_VERSION_IMPL_NODE_EVENTS,
+ .destroy = node_destroy,
+ .free = node_free,
+ .info_changed = node_info_changed,
+ .port_info_changed = node_port_info_changed,
+ .port_removed = node_port_removed,
+ .active_changed = node_active_changed,
+ .event = node_event,
+};
+
+static void client_node_removed(void *_data)
+{
+ struct node_data *data = _data;
+ pw_log_debug("%p: removed", data);
+
+ spa_hook_remove(&data->proxy_client_node_listener);
+ spa_hook_remove(&data->client_node_listener);
+
+ if (data->node) {
+ spa_hook_remove(&data->node_listener);
+ pw_impl_node_set_state(data->node, PW_NODE_STATE_SUSPENDED);
+
+ clean_node(data);
+
+ if (data->do_free)
+ pw_impl_node_destroy(data->node);
+ }
+ data->client_node = NULL;
+}
+
+static void client_node_destroy(void *_data)
+{
+ struct node_data *data = _data;
+
+ pw_log_debug("%p: destroy", data);
+ client_node_removed(_data);
+}
+
+static void client_node_bound(void *_data, uint32_t global_id)
+{
+ struct node_data *data = _data;
+ pw_log_debug("%p: bound %u", data, global_id);
+ data->remote_id = global_id;
+}
+
+static const struct pw_proxy_events proxy_client_node_events = {
+ PW_VERSION_PROXY_EVENTS,
+ .removed = client_node_removed,
+ .destroy = client_node_destroy,
+ .bound = client_node_bound,
+};
+
+static int node_ready(void *d, int status)
+{
+ struct node_data *data = d;
+ struct pw_impl_node *node = data->node;
+ struct pw_node_activation *a = node->rt.activation;
+ struct spa_system *data_system = data->context->data_system;
+ struct timespec ts;
+ struct pw_impl_port *p;
+
+ pw_log_trace_fp("node %p: ready driver:%d exported:%d status:%d", node,
+ node->driver, node->exported, status);
+
+ if (status & SPA_STATUS_HAVE_DATA) {
+ spa_list_for_each(p, &node->rt.output_mix, rt.node_link)
+ spa_node_process(p->mix);
+ }
+
+ spa_system_clock_gettime(data_system, CLOCK_MONOTONIC, &ts);
+ a->status = PW_NODE_ACTIVATION_TRIGGERED;
+ a->signal_time = SPA_TIMESPEC_TO_NSEC(&ts);
+
+ if (SPA_UNLIKELY(spa_system_eventfd_write(data_system, data->rtwritefd, 1) < 0))
+ pw_log_warn("node %p: write failed %m", node);
+
+ return 0;
+}
+
+static int node_reuse_buffer(void *data, uint32_t port_id, uint32_t buffer_id)
+{
+ return 0;
+}
+
+static int node_xrun(void *d, uint64_t trigger, uint64_t delay, struct spa_pod *info)
+{
+ struct node_data *data = d;
+ struct pw_impl_node *node = data->node;
+ struct pw_node_activation *a = node->rt.activation;
+
+ a->xrun_count++;
+ a->xrun_time = trigger;
+ a->xrun_delay = delay;
+ a->max_delay = SPA_MAX(a->max_delay, delay);
+
+ pw_log_debug("node %p: XRun! count:%u time:%"PRIu64" delay:%"PRIu64" max:%"PRIu64,
+ node, a->xrun_count, trigger, delay, a->max_delay);
+
+ pw_context_driver_emit_xrun(data->context, node);
+
+ return 0;
+}
+
+static const struct spa_node_callbacks node_callbacks = {
+ SPA_VERSION_NODE_CALLBACKS,
+ .ready = node_ready,
+ .reuse_buffer = node_reuse_buffer,
+ .xrun = node_xrun
+};
+
+static struct pw_proxy *node_export(struct pw_core *core, void *object, bool do_free,
+ size_t user_data_size)
+{
+ struct pw_impl_node *node = object;
+ struct pw_proxy *client_node;
+ struct node_data *data;
+
+ user_data_size = SPA_ROUND_UP_N(user_data_size, __alignof__(struct node_data));
+
+ client_node = pw_core_create_object(core,
+ "client-node",
+ PW_TYPE_INTERFACE_ClientNode,
+ PW_VERSION_CLIENT_NODE,
+ &node->properties->dict,
+ user_data_size + sizeof(struct node_data));
+ if (client_node == NULL)
+ goto error;
+
+ data = pw_proxy_get_user_data(client_node);
+ data = SPA_PTROFF(data, user_data_size, struct node_data);
+ data->pool = pw_core_get_mempool(core);
+ data->node = node;
+ data->do_free = do_free;
+ data->context = pw_impl_node_get_context(node);
+ data->client_node = (struct pw_client_node *)client_node;
+ data->remote_id = SPA_ID_INVALID;
+
+
+ data->allow_mlock = pw_properties_get_bool(node->properties, "mem.allow-mlock",
+ data->context->settings.mem_allow_mlock);
+
+ data->warn_mlock = pw_properties_get_bool(node->properties, "mem.warn-mlock",
+ data->context->settings.mem_warn_mlock);
+
+ node->exported = true;
+
+ spa_list_init(&data->free_mix);
+ spa_list_init(&data->mix[0]);
+ spa_list_init(&data->mix[1]);
+
+ spa_list_init(&data->links);
+
+ pw_proxy_add_listener(client_node,
+ &data->proxy_client_node_listener,
+ &proxy_client_node_events, data);
+
+ spa_node_set_callbacks(node->node, &node_callbacks, data);
+ pw_impl_node_add_listener(node, &data->node_listener, &node_events, data);
+
+ pw_client_node_add_listener(data->client_node,
+ &data->client_node_listener,
+ &client_node_events,
+ data);
+ do_node_init(data);
+
+ return client_node;
+error:
+ if (do_free)
+ pw_impl_node_destroy(node);
+ return NULL;
+
+}
+
+struct pw_proxy *pw_core_node_export(struct pw_core *core,
+ const char *type, const struct spa_dict *props, void *object,
+ size_t user_data_size)
+{
+ struct pw_impl_node *node = object;
+
+ if (props)
+ pw_impl_node_update_properties(node, props);
+ return node_export(core, object, false, user_data_size);
+}
+
+struct pw_proxy *pw_core_spa_node_export(struct pw_core *core,
+ const char *type, const struct spa_dict *props, void *object,
+ size_t user_data_size)
+{
+ struct pw_impl_node *node;
+ struct pw_proxy *proxy;
+ const char *str;
+ bool do_register;
+
+ str = props ? spa_dict_lookup(props, PW_KEY_OBJECT_REGISTER) : NULL;
+ do_register = str ? pw_properties_parse_bool(str) : true;
+
+ node = pw_context_create_node(pw_core_get_context(core),
+ props ? pw_properties_new_dict(props) : NULL, 0);
+ if (node == NULL)
+ return NULL;
+
+ pw_impl_node_set_implementation(node, (struct spa_node*)object);
+
+ if (do_register)
+ pw_impl_node_register(node, NULL);
+
+ proxy = node_export(core, node, true, user_data_size);
+ if (proxy)
+ pw_impl_node_set_active(node, true);
+
+ return proxy;
+}
diff --git a/src/modules/module-client-node/v0/client-node.c b/src/modules/module-client-node/v0/client-node.c
new file mode 100644
index 0000000..e70a7c2
--- /dev/null
+++ b/src/modules/module-client-node/v0/client-node.c
@@ -0,0 +1,1447 @@
+/* PipeWire
+ *
+ * Copyright © 2015 Wim Taymans <wim.taymans@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <errno.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <dlfcn.h>
+#include <sys/socket.h>
+#include <sys/mman.h>
+
+#include <spa/node/node.h>
+#include <spa/node/utils.h>
+#include <spa/node/io.h>
+#include <spa/pod/filter.h>
+#include <spa/utils/keys.h>
+
+#define PW_ENABLE_DEPRECATED
+
+#include "pipewire/pipewire.h"
+#include "pipewire/private.h"
+
+#include "pipewire/context.h"
+#include "modules/spa/spa-node.h"
+#include "client-node.h"
+#include "transport.h"
+
+PW_LOG_TOPIC_EXTERN(mod_topic);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+/** \cond */
+
+#define MAX_INPUTS 64
+#define MAX_OUTPUTS 64
+
+#define MAX_BUFFERS 64
+
+#define CHECK_IN_PORT_ID(this,d,p) ((d) == SPA_DIRECTION_INPUT && (p) < MAX_INPUTS)
+#define CHECK_OUT_PORT_ID(this,d,p) ((d) == SPA_DIRECTION_OUTPUT && (p) < MAX_OUTPUTS)
+#define CHECK_PORT_ID(this,d,p) (CHECK_IN_PORT_ID(this,d,p) || CHECK_OUT_PORT_ID(this,d,p))
+#define CHECK_FREE_IN_PORT(this,d,p) (CHECK_IN_PORT_ID(this,d,p) && !(this)->in_ports[p].valid)
+#define CHECK_FREE_OUT_PORT(this,d,p) (CHECK_OUT_PORT_ID(this,d,p) && !(this)->out_ports[p].valid)
+#define CHECK_FREE_PORT(this,d,p) (CHECK_FREE_IN_PORT (this,d,p) || CHECK_FREE_OUT_PORT (this,d,p))
+#define CHECK_IN_PORT(this,d,p) (CHECK_IN_PORT_ID(this,d,p) && (this)->in_ports[p].valid)
+#define CHECK_OUT_PORT(this,d,p) (CHECK_OUT_PORT_ID(this,d,p) && (this)->out_ports[p].valid)
+#define CHECK_PORT(this,d,p) (CHECK_IN_PORT (this,d,p) || CHECK_OUT_PORT (this,d,p))
+
+#define GET_IN_PORT(this,p) (&this->in_ports[p])
+#define GET_OUT_PORT(this,p) (&this->out_ports[p])
+#define GET_PORT(this,d,p) (d == SPA_DIRECTION_INPUT ? GET_IN_PORT(this,p) : GET_OUT_PORT(this,p))
+
+#define CHECK_PORT_BUFFER(this,b,p) (b < p->n_buffers)
+
+extern uint32_t pw_protocol_native0_type_from_v2(struct pw_impl_client *client, uint32_t type);
+extern uint32_t pw_protocol_native0_name_to_v2(struct pw_impl_client *client, const char *name);
+
+struct mem {
+ uint32_t id;
+ int ref;
+ int fd;
+ uint32_t type;
+ uint32_t flags;
+};
+
+struct buffer {
+ struct spa_buffer *outbuf;
+ struct spa_buffer buffer;
+ struct spa_meta metas[4];
+ struct spa_data datas[4];
+ bool outstanding;
+ uint32_t memid;
+};
+
+struct port {
+ uint32_t id;
+ enum spa_direction direction;
+
+ bool valid;
+ struct spa_port_info info;
+ struct pw_properties *properties;
+
+ bool have_format;
+ uint32_t n_params;
+ struct spa_pod **params;
+ struct spa_io_buffers *io;
+
+ uint32_t n_buffers;
+ struct buffer buffers[MAX_BUFFERS];
+};
+
+struct node {
+ struct spa_node node;
+
+ struct impl *impl;
+
+ struct spa_log *log;
+ struct spa_loop *data_loop;
+ struct spa_system *data_system;
+
+ struct spa_hook_list hooks;
+ struct spa_callbacks callbacks;
+
+ struct spa_io_position *position;
+
+ struct pw_resource *resource;
+
+ struct spa_source data_source;
+ int writefd;
+
+ struct spa_node_info info;
+
+ uint32_t n_inputs;
+ uint32_t n_outputs;
+ struct port in_ports[MAX_INPUTS];
+ struct port out_ports[MAX_OUTPUTS];
+
+ uint32_t n_params;
+ struct spa_pod **params;
+
+ uint32_t seq;
+ uint32_t init_pending;
+};
+
+struct impl {
+ struct pw_impl_client_node0 this;
+
+ bool client_reuse;
+
+ struct pw_context *context;
+
+ struct node node;
+
+ struct pw_client_node0_transport *transport;
+
+ struct spa_hook node_listener;
+ struct spa_hook resource_listener;
+ struct spa_hook object_listener;
+
+ struct pw_array mems;
+
+ int fds[2];
+ int other_fds[2];
+
+ uint32_t input_ready;
+ bool out_pending;
+};
+
+/** \endcond */
+
+static struct mem *ensure_mem(struct impl *impl, int fd, uint32_t type, uint32_t flags)
+{
+ struct mem *m, *f = NULL;
+
+ pw_array_for_each(m, &impl->mems) {
+ if (m->ref <= 0)
+ f = m;
+ else if (m->fd == fd)
+ goto found;
+ }
+
+ if (f == NULL) {
+ m = pw_array_add(&impl->mems, sizeof(struct mem));
+ m->id = pw_array_get_len(&impl->mems, struct mem) - 1;
+ m->ref = 0;
+ }
+ else {
+ m = f;
+ }
+ m->fd = fd;
+ m->type = type;
+ m->flags = flags;
+
+ pw_client_node0_resource_add_mem(impl->node.resource,
+ m->id,
+ type,
+ m->fd,
+ m->flags);
+ found:
+ m->ref++;
+ return m;
+}
+
+
+static int clear_buffers(struct node *this, struct port *port)
+{
+ uint32_t i, j;
+ struct impl *impl = this->impl;
+
+ for (i = 0; i < port->n_buffers; i++) {
+ struct buffer *b = &port->buffers[i];
+ struct mem *m;
+
+ spa_log_debug(this->log, "node %p: clear buffer %d", this, i);
+
+ for (j = 0; j < b->buffer.n_datas; j++) {
+ struct spa_data *d = &b->datas[j];
+
+ if (d->type == SPA_DATA_DmaBuf ||
+ d->type == SPA_DATA_MemFd) {
+ uint32_t id;
+
+ id = SPA_PTR_TO_UINT32(b->buffer.datas[j].data);
+ m = pw_array_get_unchecked(&impl->mems, id, struct mem);
+ m->ref--;
+ }
+ }
+ m = pw_array_get_unchecked(&impl->mems, b->memid, struct mem);
+ m->ref--;
+ }
+ port->n_buffers = 0;
+ return 0;
+}
+
+static void emit_port_info(struct node *this, struct port *port)
+{
+ spa_node_emit_port_info(&this->hooks,
+ port->direction, port->id, &port->info);
+}
+
+static int impl_node_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct spa_node_events *events,
+ void *data)
+{
+ struct node *this = object;
+ struct spa_hook_list save;
+ uint32_t i;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_hook_list_isolate(&this->hooks, &save, listener, events, data);
+
+ for (i = 0; i < MAX_INPUTS; i++) {
+ if (this->in_ports[i].valid)
+ emit_port_info(this, &this->in_ports[i]);
+ }
+ for (i = 0; i < MAX_OUTPUTS; i++) {
+ if (this->out_ports[i].valid)
+ emit_port_info(this, &this->out_ports[i]);
+ }
+ spa_hook_list_join(&this->hooks, &save);
+
+ return 0;
+}
+
+static int impl_node_enum_params(void *object, int seq,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+ struct node *this = object;
+ uint8_t buffer[1024];
+ struct spa_pod_builder b = { 0 };
+ struct spa_result_node_params result;
+ uint32_t count = 0;
+ bool found = false;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(num != 0, -EINVAL);
+
+ result.id = id;
+ result.next = 0;
+
+ while (true) {
+ struct spa_pod *param;
+
+ result.index = result.next++;
+ if (result.index >= this->n_params)
+ break;
+
+ param = this->params[result.index];
+
+ if (param == NULL || !spa_pod_is_object_id(param, id))
+ continue;
+
+ found = true;
+
+ if (result.index < start)
+ continue;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ if (spa_pod_filter(&b, &result.param, param, filter) != 0)
+ continue;
+
+ pw_log_debug("%p: %d param %u", this, seq, result.index);
+ spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+ if (++count == num)
+ break;
+ }
+ return found ? 0 : -ENOENT;
+}
+
+static int impl_node_set_param(void *object, uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct node *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ if (this->resource == NULL)
+ return -EIO;
+
+ pw_client_node0_resource_set_param(this->resource, this->seq, id, flags, param);
+
+ return SPA_RESULT_RETURN_ASYNC(this->seq++);
+}
+
+static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size)
+{
+ struct node *this = object;
+ int res = 0;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ switch(id) {
+ case SPA_IO_Position:
+ this->position = data;
+ break;
+ default:
+ res = -ENOTSUP;
+ break;
+ }
+ return res;
+}
+
+static inline void do_flush(struct node *this)
+{
+ if (spa_system_eventfd_write(this->data_system, this->writefd, 1) < 0)
+ spa_log_warn(this->log, "node %p: error flushing : %s", this, strerror(errno));
+
+}
+
+static int send_clock_update(struct node *this)
+{
+ struct pw_impl_client *client = this->resource->client;
+ uint32_t type = pw_protocol_native0_name_to_v2(client, SPA_TYPE_INFO_NODE_COMMAND_BASE "ClockUpdate");
+ struct timespec ts;
+ int64_t now;
+
+ clock_gettime(CLOCK_MONOTONIC, &ts);
+ now = SPA_TIMESPEC_TO_NSEC(&ts);
+ pw_log_trace("%p: now %"PRIi64, this, now);
+
+ struct spa_command_node0_clock_update cu =
+ SPA_COMMAND_NODE0_CLOCK_UPDATE_INIT(type,
+ SPA_COMMAND_NODE0_CLOCK_UPDATE_TIME |
+ SPA_COMMAND_NODE0_CLOCK_UPDATE_SCALE |
+ SPA_COMMAND_NODE0_CLOCK_UPDATE_STATE |
+ SPA_COMMAND_NODE0_CLOCK_UPDATE_LATENCY, /* change_mask */
+ SPA_USEC_PER_SEC, /* rate */
+ now / SPA_NSEC_PER_USEC, /* ticks */
+ now, /* monotonic_time */
+ 0, /* offset */
+ (1 << 16) | 1, /* scale */
+ SPA_CLOCK0_STATE_RUNNING, /* state */
+ SPA_COMMAND_NODE0_CLOCK_UPDATE_FLAG_LIVE, /* flags */
+ 0); /* latency */
+
+ pw_client_node0_resource_command(this->resource, this->seq, (const struct spa_command*)&cu);
+ return SPA_RESULT_RETURN_ASYNC(this->seq++);
+}
+
+static int impl_node_send_command(void *object, const struct spa_command *command)
+{
+ struct node *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(command != NULL, -EINVAL);
+
+ if (this->resource == NULL)
+ return -EIO;
+
+ if (SPA_NODE_COMMAND_ID(command) == SPA_NODE_COMMAND_Start) {
+ send_clock_update(this);
+ }
+
+ pw_client_node0_resource_command(this->resource, this->seq, command);
+ return SPA_RESULT_RETURN_ASYNC(this->seq++);
+}
+
+static int
+impl_node_set_callbacks(void *object,
+ const struct spa_node_callbacks *callbacks,
+ void *data)
+{
+ struct node *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ this->callbacks = SPA_CALLBACKS_INIT(callbacks, data);
+
+ return 0;
+}
+
+static int
+impl_node_sync(void *object, int seq)
+{
+ struct node *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ pw_log_debug("%p: sync %p", this, this->resource);
+
+ if (this->resource == NULL)
+ return -EIO;
+
+ this->init_pending = SPA_RESULT_RETURN_ASYNC(this->seq++);
+
+ return this->init_pending;
+}
+
+
+extern struct spa_pod *pw_protocol_native0_pod_from_v2(struct pw_impl_client *client, const struct spa_pod *pod);
+extern int pw_protocol_native0_pod_to_v2(struct pw_impl_client *client, const struct spa_pod *pod,
+ struct spa_pod_builder *b);
+
+static void
+do_update_port(struct node *this,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t change_mask,
+ uint32_t n_params,
+ const struct spa_pod **params,
+ const struct spa_port_info *info)
+{
+ struct port *port;
+
+ port = GET_PORT(this, direction, port_id);
+
+ if (!port->valid) {
+ spa_log_debug(this->log, "node %p: adding port %d, direction %d",
+ this, port_id, direction);
+ port->id = port_id;
+ port->direction = direction;
+ port->have_format = false;
+ port->valid = true;
+
+ if (direction == SPA_DIRECTION_INPUT)
+ this->n_inputs++;
+ else
+ this->n_outputs++;
+ }
+
+ if (change_mask & PW_CLIENT_NODE0_PORT_UPDATE_PARAMS) {
+ uint32_t i;
+
+ port->have_format = false;
+
+ spa_log_debug(this->log, "node %p: port %u update %d params", this, port_id, n_params);
+ for (i = 0; i < port->n_params; i++)
+ free(port->params[i]);
+ port->n_params = n_params;
+ if (port->n_params == 0) {
+ free(port->params);
+ port->params = NULL;
+ } else {
+ void *p;
+ p = pw_reallocarray(port->params, port->n_params, sizeof(struct spa_pod *));
+ if (p == NULL) {
+ pw_log_error("%p: port %u can't realloc: %m", this, port_id);
+ free(port->params);
+ port->n_params = 0;
+ }
+ port->params = p;
+ }
+ for (i = 0; i < port->n_params; i++) {
+ port->params[i] = params[i] ?
+ pw_protocol_native0_pod_from_v2(this->resource->client, params[i]) : NULL;
+
+ if (port->params[i] && spa_pod_is_object_id(port->params[i], SPA_PARAM_Format))
+ port->have_format = true;
+ }
+ }
+
+ if (change_mask & PW_CLIENT_NODE0_PORT_UPDATE_INFO) {
+ pw_properties_free(port->properties);
+ port->properties = NULL;
+ port->info.props = NULL;
+ port->info.n_params = 0;
+ port->info.params = NULL;
+
+ if (info) {
+ port->info = *info;
+ if (info->props) {
+ port->properties = pw_properties_new_dict(info->props);
+ port->info.props = &port->properties->dict;
+ }
+ }
+ spa_node_emit_port_info(&this->hooks, direction, port_id, info);
+ }
+}
+
+static void
+clear_port(struct node *this,
+ struct port *port, enum spa_direction direction, uint32_t port_id)
+{
+ do_update_port(this,
+ direction,
+ port_id,
+ PW_CLIENT_NODE0_PORT_UPDATE_PARAMS |
+ PW_CLIENT_NODE0_PORT_UPDATE_INFO, 0, NULL, NULL);
+ clear_buffers(this, port);
+}
+
+static void do_uninit_port(struct node *this, enum spa_direction direction, uint32_t port_id)
+{
+ struct port *port;
+
+ spa_log_debug(this->log, "node %p: removing port %d", this, port_id);
+
+ if (direction == SPA_DIRECTION_INPUT) {
+ port = GET_IN_PORT(this, port_id);
+ this->n_inputs--;
+ } else {
+ port = GET_OUT_PORT(this, port_id);
+ this->n_outputs--;
+ }
+ clear_port(this, port, direction, port_id);
+ port->valid = false;
+ spa_node_emit_port_info(&this->hooks, direction, port_id, NULL);
+}
+
+static int
+impl_node_add_port(void *object, enum spa_direction direction, uint32_t port_id,
+ const struct spa_dict *props)
+{
+ struct node *this = object;
+ struct port *port;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(CHECK_FREE_PORT(this, direction, port_id), -EINVAL);
+
+ port = GET_PORT(this, direction, port_id);
+ clear_port(this, port, direction, port_id);
+
+ return 0;
+}
+
+static int
+impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_id)
+{
+ struct node *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ do_uninit_port(this, direction, port_id);
+
+ return 0;
+}
+
+static int
+impl_node_port_enum_params(void *object, int seq,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+ struct node *this = object;
+ struct port *port;
+ uint8_t buffer[1024];
+ struct spa_pod_builder b = { 0 };
+ struct spa_result_node_params result;
+ uint32_t count = 0;
+ bool found = false;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(num != 0, -EINVAL);
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ port = GET_PORT(this, direction, port_id);
+
+ pw_log_debug("%p: %d port %d.%d %u %u %u", this, seq,
+ direction, port_id, id, start, num);
+
+ result.id = id;
+ result.next = 0;
+
+ while (true) {
+ struct spa_pod *param;
+
+ result.index = result.next++;
+ if (result.index >= port->n_params)
+ break;
+
+ param = port->params[result.index];
+
+ if (param == NULL || !spa_pod_is_object_id(param, id))
+ continue;
+
+ found = true;
+
+ if (result.index < start)
+ continue;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ if (spa_pod_filter(&b, &result.param, param, filter) < 0)
+ continue;
+
+ pw_log_debug("%p: %d param %u", this, seq, result.index);
+ spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+ if (++count == num)
+ break;
+ }
+ return found ? 0 : -ENOENT;
+}
+
+static int
+impl_node_port_set_param(void *object,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct node *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ if (this->resource == NULL)
+ return -EIO;
+
+ pw_client_node0_resource_port_set_param(this->resource,
+ this->seq,
+ direction, port_id,
+ id, flags,
+ param);
+ return SPA_RESULT_RETURN_ASYNC(this->seq++);
+}
+
+static int
+impl_node_port_set_io(void *object,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t id,
+ void *data, size_t size)
+{
+ struct node *this = object;
+ struct impl *impl;
+ struct pw_memblock *mem;
+ struct mem *m;
+ uint32_t memid, mem_offset, mem_size;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ impl = this->impl;
+
+ spa_log_debug(this->log, "node %p: port %d.%d set io %d %p", this,
+ direction, port_id, id, data);
+
+ if (id == SPA_IO_Buffers) {
+ struct port *port = GET_PORT(this, direction, port_id);
+ port->io = data;
+ }
+
+ if (this->resource == NULL)
+ return -EIO;
+
+
+ if (data) {
+ if ((mem = pw_mempool_find_ptr(impl->context->pool, data)) == NULL)
+ return -EINVAL;
+
+ mem_offset = SPA_PTRDIFF(data, mem->map->ptr);
+ mem_size = mem->size;
+ if (mem_size - mem_offset < size)
+ return -EINVAL;
+
+ mem_offset += mem->map->offset;
+ m = ensure_mem(impl, mem->fd, SPA_DATA_MemFd, mem->flags);
+ memid = m->id;
+ }
+ else {
+ memid = SPA_ID_INVALID;
+ mem_offset = mem_size = 0;
+ }
+
+ pw_client_node0_resource_port_set_io(this->resource,
+ this->seq,
+ direction, port_id,
+ id,
+ memid,
+ mem_offset, mem_size);
+ return SPA_RESULT_RETURN_ASYNC(this->seq++);
+}
+
+static int
+impl_node_port_use_buffers(void *object,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t flags,
+ struct spa_buffer **buffers,
+ uint32_t n_buffers)
+{
+ struct node *this = object;
+ struct impl *impl;
+ struct port *port;
+ uint32_t i, j;
+ struct pw_client_node0_buffer *mb;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ impl = this->impl;
+ spa_log_debug(this->log, "node %p: use buffers %p %u", this, buffers, n_buffers);
+
+ port = GET_PORT(this, direction, port_id);
+
+ if (!port->have_format)
+ return -EIO;
+
+ clear_buffers(this, port);
+
+ if (n_buffers > 0) {
+ mb = alloca(n_buffers * sizeof(struct pw_client_node0_buffer));
+ } else {
+ mb = NULL;
+ }
+
+ port->n_buffers = n_buffers;
+
+ if (this->resource == NULL)
+ return -EIO;
+
+ for (i = 0; i < n_buffers; i++) {
+ struct buffer *b = &port->buffers[i];
+ struct pw_memblock *mem;
+ struct mem *m;
+ size_t data_size;
+ void *baseptr;
+
+ b->outbuf = buffers[i];
+ memcpy(&b->buffer, buffers[i], sizeof(struct spa_buffer));
+ b->buffer.datas = b->datas;
+ b->buffer.metas = b->metas;
+
+ if (buffers[i]->n_metas > 0)
+ baseptr = buffers[i]->metas[0].data;
+ else if (buffers[i]->n_datas > 0)
+ baseptr = buffers[i]->datas[0].chunk;
+ else
+ return -EINVAL;
+
+ if ((mem = pw_mempool_find_ptr(impl->context->pool, baseptr)) == NULL)
+ return -EINVAL;
+
+ data_size = 0;
+ for (j = 0; j < buffers[i]->n_metas; j++) {
+ data_size += buffers[i]->metas[j].size;
+ }
+ for (j = 0; j < buffers[i]->n_datas; j++) {
+ struct spa_data *d = buffers[i]->datas;
+ data_size += sizeof(struct spa_chunk);
+ if (d->type == SPA_DATA_MemPtr)
+ data_size += d->maxsize;
+ }
+
+ m = ensure_mem(impl, mem->fd, SPA_DATA_MemFd, mem->flags);
+ b->memid = m->id;
+
+ mb[i].buffer = &b->buffer;
+ mb[i].mem_id = b->memid;
+ mb[i].offset = SPA_PTRDIFF(baseptr, SPA_PTROFF(mem->map->ptr, mem->map->offset, void));
+ mb[i].size = data_size;
+
+ for (j = 0; j < buffers[i]->n_metas; j++)
+ memcpy(&b->buffer.metas[j], &buffers[i]->metas[j], sizeof(struct spa_meta));
+ b->buffer.n_metas = j;
+
+ for (j = 0; j < buffers[i]->n_datas; j++) {
+ struct spa_data *d = &buffers[i]->datas[j];
+
+ memcpy(&b->buffer.datas[j], d, sizeof(struct spa_data));
+
+ if (d->type == SPA_DATA_DmaBuf ||
+ d->type == SPA_DATA_MemFd) {
+ m = ensure_mem(impl, d->fd, d->type, d->flags);
+ b->buffer.datas[j].data = SPA_UINT32_TO_PTR(m->id);
+ } else if (d->type == SPA_DATA_MemPtr) {
+ b->buffer.datas[j].data = SPA_INT_TO_PTR(SPA_PTRDIFF(d->data, baseptr));
+ } else {
+ b->buffer.datas[j].type = SPA_ID_INVALID;
+ b->buffer.datas[j].data = 0;
+ spa_log_error(this->log, "invalid memory type %d", d->type);
+ }
+ }
+ }
+
+ pw_client_node0_resource_port_use_buffers(this->resource,
+ this->seq,
+ direction, port_id,
+ n_buffers, mb);
+
+ return SPA_RESULT_RETURN_ASYNC(this->seq++);
+}
+
+static int
+impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id)
+{
+ struct node *this = object;
+ struct impl *impl;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(CHECK_OUT_PORT(this, SPA_DIRECTION_OUTPUT, port_id), -EINVAL);
+
+ impl = this->impl;
+
+ spa_log_trace(this->log, "reuse buffer %d", buffer_id);
+
+ pw_client_node0_transport_add_message(impl->transport, (struct pw_client_node0_message *)
+ &PW_CLIENT_NODE0_MESSAGE_PORT_REUSE_BUFFER_INIT(port_id, buffer_id));
+ do_flush(this);
+
+ return 0;
+}
+
+static int impl_node_process_input(struct spa_node *node)
+{
+ struct node *this = SPA_CONTAINER_OF(node, struct node, node);
+ struct impl *impl = this->impl;
+// bool client_reuse = impl->client_reuse;
+ uint32_t i;
+ int res;
+
+ if (impl->input_ready == 0) {
+ /* the client is not ready to receive our buffers, recycle them */
+ pw_log_trace("node not ready, recycle buffers");
+ for (i = 0; i < MAX_INPUTS; i++) {
+ struct port *p = &this->in_ports[i];
+ struct spa_io_buffers *io = p->io;
+
+ if (!p->valid || io == NULL)
+ continue;
+
+ io->status = SPA_STATUS_NEED_DATA;
+ }
+ res = SPA_STATUS_NEED_DATA;
+ }
+ else {
+ for (i = 0; i < MAX_INPUTS; i++) {
+ struct port *p = &this->in_ports[i];
+ struct spa_io_buffers *io = p->io;
+
+ if (!p->valid || io == NULL)
+ continue;
+
+ pw_log_trace("set io status to %d %d", io->status, io->buffer_id);
+ impl->transport->inputs[p->id] = *io;
+
+ /* explicitly recycle buffers when the client is not going to do it */
+// if (!client_reuse && (pp = p->peer))
+// spa_node_port_reuse_buffer(pp->node->implementation,
+// pp->port_id, io->buffer_id);
+ }
+ pw_client_node0_transport_add_message(impl->transport,
+ &PW_CLIENT_NODE0_MESSAGE_INIT(PW_CLIENT_NODE0_MESSAGE_PROCESS_INPUT));
+ do_flush(this);
+
+ impl->input_ready--;
+ res = SPA_STATUS_OK;
+ }
+ return res;
+}
+
+#if 0
+/** this is used for clients providing data to pipewire and currently
+ * not supported in the compat layer */
+static int impl_node_process_output(struct spa_node *node)
+{
+ struct node *this;
+ struct impl *impl;
+ uint32_t i;
+
+ this = SPA_CONTAINER_OF(node, struct node, node);
+ impl = this->impl;
+
+ if (impl->out_pending)
+ goto done;
+
+ impl->out_pending = true;
+
+ for (i = 0; i < MAX_OUTPUTS; i++) {
+ struct port *p = &this->out_ports[i];
+ struct spa_io_buffers *io = p->io;
+
+ if (!p->valid || io == NULL)
+ continue;
+
+ impl->transport->outputs[p->id] = *io;
+
+ pw_log_trace("%d %d -> %d %d", io->status, io->buffer_id,
+ impl->transport->outputs[p->id].status,
+ impl->transport->outputs[p->id].buffer_id);
+ }
+
+ done:
+ pw_client_node0_transport_add_message(impl->transport,
+ &PW_CLIENT_NODE0_MESSAGE_INIT(PW_CLIENT_NODE0_MESSAGE_PROCESS_OUTPUT));
+ do_flush(this);
+
+ return SPA_STATUS_OK;
+}
+#endif
+
+static int impl_node_process(void *object)
+{
+ struct node *this = object;
+ struct impl *impl = this->impl;
+ struct pw_impl_node *n = impl->this.node;
+
+ return impl_node_process_input(n->node);
+}
+
+static int handle_node_message(struct node *this, struct pw_client_node0_message *message)
+{
+ struct impl *impl = SPA_CONTAINER_OF(this, struct impl, node);
+ uint32_t i;
+
+ switch (PW_CLIENT_NODE0_MESSAGE_TYPE(message)) {
+ case PW_CLIENT_NODE0_MESSAGE_HAVE_OUTPUT:
+ for (i = 0; i < MAX_OUTPUTS; i++) {
+ struct port *p = &this->out_ports[i];
+ struct spa_io_buffers *io = p->io;
+ if (!p->valid || io == NULL)
+ continue;
+ *io = impl->transport->outputs[p->id];
+ pw_log_trace("have output %d %d", io->status, io->buffer_id);
+ }
+ impl->out_pending = false;
+ spa_node_call_ready(&this->callbacks, SPA_STATUS_HAVE_DATA);
+ break;
+
+ case PW_CLIENT_NODE0_MESSAGE_NEED_INPUT:
+ for (i = 0; i < MAX_INPUTS; i++) {
+ struct port *p = &this->in_ports[i];
+ struct spa_io_buffers *io = p->io;
+ if (!p->valid || io == NULL)
+ continue;
+ pw_log_trace("need input %d %d", i, p->id);
+ *io = impl->transport->inputs[p->id];
+ pw_log_trace("need input %d %d", io->status, io->buffer_id);
+ }
+ impl->input_ready++;
+ spa_node_call_ready(&this->callbacks, SPA_STATUS_NEED_DATA);
+ break;
+
+ case PW_CLIENT_NODE0_MESSAGE_PORT_REUSE_BUFFER:
+ if (impl->client_reuse) {
+ struct pw_client_node0_message_port_reuse_buffer *p =
+ (struct pw_client_node0_message_port_reuse_buffer *) message;
+ spa_node_call_reuse_buffer(&this->callbacks, p->body.port_id.value,
+ p->body.buffer_id.value);
+ }
+ break;
+
+ default:
+ pw_log_warn("unhandled message %d", PW_CLIENT_NODE0_MESSAGE_TYPE(message));
+ return -ENOTSUP;
+ }
+ return 0;
+}
+
+static void setup_transport(struct impl *impl)
+{
+ struct node *this = &impl->node;
+ uint32_t max_inputs = 0, max_outputs = 0, n_inputs = 0, n_outputs = 0;
+ struct spa_dict_item items[1];
+
+ n_inputs = this->n_inputs;
+ max_inputs = this->info.max_input_ports == 0 ? this->n_inputs : this->info.max_input_ports;
+ n_outputs = this->n_outputs;
+ max_outputs = this->info.max_output_ports == 0 ? this->n_outputs : this->info.max_output_ports;
+
+ impl->transport = pw_client_node0_transport_new(impl->context, max_inputs, max_outputs);
+ impl->transport->area->n_input_ports = n_inputs;
+ impl->transport->area->n_output_ports = n_outputs;
+
+ if (n_inputs > 0) {
+ items[0] = SPA_DICT_ITEM_INIT(SPA_KEY_MEDIA_CLASS, "Stream/Input/Video");
+ } else {
+ items[0] = SPA_DICT_ITEM_INIT(SPA_KEY_MEDIA_CLASS, "Stream/Output/Video");
+ }
+ pw_impl_node_update_properties(impl->this.node, &SPA_DICT_INIT(items, 1));
+}
+
+static void
+client_node0_done(void *data, int seq, int res)
+{
+ struct impl *impl = data;
+ struct node *this = &impl->node;
+
+ if (seq == 0 && res == 0 && impl->transport == NULL)
+ setup_transport(impl);
+
+ pw_log_debug("seq:%d res:%d pending:%d", seq, res, this->init_pending);
+ spa_node_emit_result(&this->hooks, seq, res, 0, NULL);
+
+ if (this->init_pending != SPA_ID_INVALID) {
+ spa_node_emit_result(&this->hooks, this->init_pending, res, 0, NULL);
+ this->init_pending = SPA_ID_INVALID;
+ }
+}
+
+static void
+client_node0_update(void *data,
+ uint32_t change_mask,
+ uint32_t max_input_ports,
+ uint32_t max_output_ports,
+ uint32_t n_params,
+ const struct spa_pod **params)
+{
+ struct impl *impl = data;
+ struct node *this = &impl->node;
+
+ if (change_mask & PW_CLIENT_NODE0_UPDATE_MAX_INPUTS)
+ this->info.max_input_ports = max_input_ports;
+ if (change_mask & PW_CLIENT_NODE0_UPDATE_MAX_OUTPUTS)
+ this->info.max_output_ports = max_output_ports;
+ if (change_mask & PW_CLIENT_NODE0_UPDATE_PARAMS) {
+ uint32_t i;
+ spa_log_debug(this->log, "node %p: update %d params", this, n_params);
+
+ for (i = 0; i < this->n_params; i++)
+ free(this->params[i]);
+ this->n_params = n_params;
+ if (this->n_params == 0) {
+ free(this->params);
+ this->params = NULL;
+ } else {
+ void *p;
+ p = pw_reallocarray(this->params, this->n_params, sizeof(struct spa_pod *));
+ if (p == NULL) {
+ pw_log_error("%p: can't realloc: %m", this);
+ free(this->params);
+ this->n_params = 0;
+ }
+ this->params = p;
+ }
+ for (i = 0; i < this->n_params; i++)
+ this->params[i] = params[i] ? spa_pod_copy(params[i]) : NULL;
+ }
+ if (change_mask & (PW_CLIENT_NODE0_UPDATE_MAX_INPUTS | PW_CLIENT_NODE0_UPDATE_MAX_OUTPUTS)) {
+ spa_node_emit_info(&this->hooks, &this->info);
+ }
+
+ spa_log_debug(this->log, "node %p: got node update max_in %u, max_out %u", this,
+ this->info.max_input_ports, this->info.max_output_ports);
+}
+
+static void
+client_node0_port_update(void *data,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t change_mask,
+ uint32_t n_params,
+ const struct spa_pod **params,
+ const struct spa_port_info *info)
+{
+ struct impl *impl = data;
+ struct node *this = &impl->node;
+ bool remove;
+
+ spa_log_debug(this->log, "node %p: got port update", this);
+ if (!CHECK_PORT_ID(this, direction, port_id))
+ return;
+
+ remove = (change_mask == 0);
+
+ if (remove) {
+ do_uninit_port(this, direction, port_id);
+ } else {
+ do_update_port(this,
+ direction,
+ port_id,
+ change_mask,
+ n_params, params, info);
+ }
+}
+
+static void client_node0_set_active(void *data, bool active)
+{
+ struct impl *impl = data;
+ pw_impl_node_set_active(impl->this.node, active);
+}
+
+static void client_node0_event(void *data, struct spa_event *event)
+{
+ struct impl *impl = data;
+ struct node *this = &impl->node;
+
+ switch (SPA_EVENT_TYPE(event)) {
+ case SPA_NODE0_EVENT_RequestClockUpdate:
+ send_clock_update(this);
+ break;
+ default:
+ spa_node_emit_event(&this->hooks, event);
+ }
+}
+
+static const struct pw_client_node0_methods client_node0_methods = {
+ PW_VERSION_CLIENT_NODE0_METHODS,
+ .done = client_node0_done,
+ .update = client_node0_update,
+ .port_update = client_node0_port_update,
+ .set_active = client_node0_set_active,
+ .event = client_node0_event,
+};
+
+static void node_on_data_fd_events(struct spa_source *source)
+{
+ struct node *this = source->data;
+ struct impl *impl = this->impl;
+
+ if (source->rmask & (SPA_IO_ERR | SPA_IO_HUP)) {
+ spa_log_warn(this->log, "node %p: got error", this);
+ return;
+ }
+
+ if (source->rmask & SPA_IO_IN) {
+ struct pw_client_node0_message message;
+ uint64_t cmd;
+
+ if (spa_system_eventfd_read(this->data_system, this->data_source.fd, &cmd) < 0)
+ spa_log_warn(this->log, "node %p: error reading message: %s",
+ this, strerror(errno));
+
+ while (pw_client_node0_transport_next_message(impl->transport, &message) == 1) {
+ struct pw_client_node0_message *msg = alloca(SPA_POD_SIZE(&message));
+ pw_client_node0_transport_parse_message(impl->transport, msg);
+ handle_node_message(this, msg);
+ }
+ }
+}
+
+static const struct spa_node_methods impl_node = {
+ SPA_VERSION_NODE_METHODS,
+ .add_listener = impl_node_add_listener,
+ .set_callbacks = impl_node_set_callbacks,
+ .sync = impl_node_sync,
+ .enum_params = impl_node_enum_params,
+ .set_param = impl_node_set_param,
+ .set_io = impl_node_set_io,
+ .send_command = impl_node_send_command,
+ .add_port = impl_node_add_port,
+ .remove_port = impl_node_remove_port,
+ .port_enum_params = impl_node_port_enum_params,
+ .port_set_param = impl_node_port_set_param,
+ .port_use_buffers = impl_node_port_use_buffers,
+ .port_set_io = impl_node_port_set_io,
+ .port_reuse_buffer = impl_node_port_reuse_buffer,
+ .process = impl_node_process,
+};
+
+static int
+node_init(struct node *this,
+ struct spa_dict *info,
+ const struct spa_support *support,
+ uint32_t n_support)
+{
+ this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log);
+ this->data_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataLoop);
+ this->data_system = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataSystem);
+
+ if (this->data_loop == NULL) {
+ spa_log_error(this->log, "a data-loop is needed");
+ return -EINVAL;
+ }
+
+ this->node.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_Node,
+ SPA_VERSION_NODE,
+ &impl_node, this);
+ spa_hook_list_init(&this->hooks);
+
+ this->data_source.func = node_on_data_fd_events;
+ this->data_source.data = this;
+ this->data_source.fd = -1;
+ this->data_source.mask = SPA_IO_IN | SPA_IO_ERR | SPA_IO_HUP;
+ this->data_source.rmask = 0;
+
+ this->seq = 1;
+ this->init_pending = SPA_ID_INVALID;
+
+ return SPA_RESULT_RETURN_ASYNC(this->seq++);
+}
+
+static int node_clear(struct node *this)
+{
+ uint32_t i;
+
+ for (i = 0; i < MAX_INPUTS; i++) {
+ if (this->in_ports[i].valid)
+ clear_port(this, &this->in_ports[i], SPA_DIRECTION_INPUT, i);
+ }
+ for (i = 0; i < MAX_OUTPUTS; i++) {
+ if (this->out_ports[i].valid)
+ clear_port(this, &this->out_ports[i], SPA_DIRECTION_OUTPUT, i);
+ }
+
+ return 0;
+}
+
+static int do_remove_source(struct spa_loop *loop,
+ bool async,
+ uint32_t seq,
+ const void *data,
+ size_t size,
+ void *user_data)
+{
+ struct spa_source *source = user_data;
+ spa_loop_remove_source(loop, source);
+ return 0;
+}
+
+static void client_node0_resource_destroy(void *data)
+{
+ struct impl *impl = data;
+ struct pw_impl_client_node0 *this = &impl->this;
+ struct node *node = &impl->node;
+
+ pw_log_debug("client-node %p: destroy", impl);
+
+ impl->node.resource = this->resource = NULL;
+ spa_hook_remove(&impl->resource_listener);
+ spa_hook_remove(&impl->object_listener);
+
+ if (node->data_source.fd != -1) {
+ spa_loop_invoke(node->data_loop,
+ do_remove_source,
+ SPA_ID_INVALID,
+ NULL,
+ 0,
+ true,
+ &node->data_source);
+ }
+ if (this->node)
+ pw_impl_node_destroy(this->node);
+}
+
+static void node_initialized(void *data)
+{
+ struct impl *impl = data;
+ struct pw_impl_client_node0 *this = &impl->this;
+ struct pw_impl_node *node = this->node;
+ struct spa_system *data_system = impl->node.data_system;
+
+ if (this->resource == NULL)
+ return;
+
+ impl->fds[0] = spa_system_eventfd_create(data_system, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK);
+ impl->fds[1] = spa_system_eventfd_create(data_system, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK);
+ impl->node.data_source.fd = impl->fds[0];
+ impl->node.writefd = impl->fds[1];
+ impl->other_fds[0] = impl->fds[1];
+ impl->other_fds[1] = impl->fds[0];
+
+ spa_loop_add_source(impl->node.data_loop, &impl->node.data_source);
+ pw_log_debug("client-node %p: transport fd %d %d", node, impl->fds[0], impl->fds[1]);
+
+ pw_client_node0_resource_transport(this->resource,
+ pw_global_get_id(pw_impl_node_get_global(node)),
+ impl->other_fds[0],
+ impl->other_fds[1],
+ impl->transport);
+}
+
+static void node_free(void *data)
+{
+ struct impl *impl = data;
+ struct pw_impl_client_node0 *this = &impl->this;
+ struct spa_system *data_system = impl->node.data_system;
+
+ this->node = NULL;
+
+ pw_log_debug("client-node %p: free", &impl->this);
+ node_clear(&impl->node);
+
+ if (impl->transport)
+ pw_client_node0_transport_destroy(impl->transport);
+
+ spa_hook_remove(&impl->node_listener);
+
+ if (this->resource)
+ pw_resource_destroy(this->resource);
+
+ pw_array_clear(&impl->mems);
+
+ if (impl->fds[0] != -1)
+ spa_system_close(data_system, impl->fds[0]);
+ if (impl->fds[1] != -1)
+ spa_system_close(data_system, impl->fds[1]);
+ free(impl);
+}
+
+static const struct pw_impl_node_events node_events = {
+ PW_VERSION_IMPL_NODE_EVENTS,
+ .free = node_free,
+ .initialized = node_initialized,
+};
+
+static const struct pw_resource_events resource_events = {
+ PW_VERSION_RESOURCE_EVENTS,
+ .destroy = client_node0_resource_destroy,
+};
+
+static void convert_properties(struct pw_properties *properties)
+{
+ static const struct {
+ const char *from, *to;
+ } props[] = {
+ { "pipewire.autoconnect", PW_KEY_NODE_AUTOCONNECT, },
+ /* XXX deprecated */
+ { "pipewire.target.node", PW_KEY_NODE_TARGET, }
+ };
+
+ const char *str;
+
+ SPA_FOR_EACH_ELEMENT_VAR(props, p) {
+ if ((str = pw_properties_get(properties, p->from)) != NULL) {
+ pw_properties_set(properties, p->to, str);
+ pw_properties_set(properties, p->from, NULL);
+ }
+ }
+}
+
+/** Create a new client node
+ * \param client an owner \ref pw_client
+ * \param id an id
+ * \param name a name
+ * \param properties extra properties
+ * \return a newly allocated client node
+ *
+ * Create a new \ref pw_impl_node.
+ *
+ * \memberof pw_impl_client_node
+ */
+struct pw_impl_client_node0 *pw_impl_client_node0_new(struct pw_resource *resource,
+ struct pw_properties *properties)
+{
+ struct impl *impl;
+ struct pw_impl_client_node0 *this;
+ struct pw_impl_client *client = pw_resource_get_client(resource);
+ struct pw_context *context = pw_impl_client_get_context(client);
+ const struct spa_support *support;
+ uint32_t n_support;
+ const char *name;
+ int res;
+
+ impl = calloc(1, sizeof(struct impl));
+ if (impl == NULL)
+ return NULL;
+
+ this = &impl->this;
+
+ if (properties == NULL)
+ properties = pw_properties_new(NULL, NULL);
+ if (properties == NULL) {
+ res = -errno;
+ goto error_exit_free;
+ }
+ convert_properties(properties);
+
+ pw_properties_setf(properties, PW_KEY_CLIENT_ID, "%d", client->global->id);
+
+ impl->context = context;
+ impl->fds[0] = impl->fds[1] = -1;
+ pw_log_debug("client-node %p: new", impl);
+
+ support = pw_context_get_support(impl->context, &n_support);
+
+ node_init(&impl->node, NULL, support, n_support);
+ impl->node.impl = impl;
+
+ pw_array_init(&impl->mems, 64);
+
+ if ((name = pw_properties_get(properties, "node.name")) == NULL)
+ name = "client-node";
+ pw_properties_set(properties, PW_KEY_MEDIA_TYPE, "Video");
+
+ impl->node.resource = resource;
+ this->resource = resource;
+ this->node = pw_spa_node_new(context,
+ PW_SPA_NODE_FLAG_ASYNC,
+ &impl->node.node,
+ NULL,
+ properties, 0);
+ if (this->node == NULL) {
+ res = -errno;
+ goto error_no_node;
+ }
+
+ impl->client_reuse = pw_properties_get_bool(properties, "pipewire.client.reuse", false);
+
+ pw_resource_add_listener(this->resource,
+ &impl->resource_listener,
+ &resource_events,
+ impl);
+ pw_resource_add_object_listener(this->resource,
+ &impl->object_listener,
+ &client_node0_methods,
+ impl);
+
+
+ pw_impl_node_add_listener(this->node, &impl->node_listener, &node_events, impl);
+
+ return this;
+
+error_no_node:
+ pw_resource_destroy(this->resource);
+ node_clear(&impl->node);
+error_exit_free:
+ free(impl);
+ errno = -res;
+ return NULL;
+}
+
+/** Destroy a client node
+ * \param node the client node to destroy
+ * \memberof pw_impl_client_node
+ */
+void pw_impl_client_node0_destroy(struct pw_impl_client_node0 *node)
+{
+ pw_resource_destroy(node->resource);
+}
diff --git a/src/modules/module-client-node/v0/client-node.h b/src/modules/module-client-node/v0/client-node.h
new file mode 100644
index 0000000..04b77d4
--- /dev/null
+++ b/src/modules/module-client-node/v0/client-node.h
@@ -0,0 +1,101 @@
+/* PipeWire
+ *
+ * Copyright © 2015 Wim Taymans <wim.taymans@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_CLIENT_NODE0_H
+#define PIPEWIRE_CLIENT_NODE0_H
+
+#include <pipewire/impl.h>
+
+#include "ext-client-node.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/** The state of the clock */
+enum spa_clock0_state {
+ SPA_CLOCK0_STATE_STOPPED, /*< the clock is stopped */
+ SPA_CLOCK0_STATE_PAUSED, /*< the clock is paused */
+ SPA_CLOCK0_STATE_RUNNING, /*< the clock is running */
+};
+
+struct spa_command_node0_clock_update_body {
+ struct spa_pod_object_body body;
+#define SPA_COMMAND_NODE0_CLOCK_UPDATE_TIME (1 << 0)
+#define SPA_COMMAND_NODE0_CLOCK_UPDATE_SCALE (1 << 1)
+#define SPA_COMMAND_NODE0_CLOCK_UPDATE_STATE (1 << 2)
+#define SPA_COMMAND_NODE0_CLOCK_UPDATE_LATENCY (1 << 3)
+ struct spa_pod_int change_mask SPA_ALIGNED(8);
+ struct spa_pod_int rate SPA_ALIGNED(8);
+ struct spa_pod_long ticks SPA_ALIGNED(8);
+ struct spa_pod_long monotonic_time SPA_ALIGNED(8);
+ struct spa_pod_long offset SPA_ALIGNED(8);
+ struct spa_pod_int scale SPA_ALIGNED(8);
+ struct spa_pod_int state SPA_ALIGNED(8);
+#define SPA_COMMAND_NODE0_CLOCK_UPDATE_FLAG_LIVE (1 << 0)
+ struct spa_pod_int flags SPA_ALIGNED(8);
+ struct spa_pod_long latency SPA_ALIGNED(8);
+};
+
+struct spa_command_node0_clock_update {
+ struct spa_pod pod;
+ struct spa_command_node0_clock_update_body body;
+};
+
+#define SPA_COMMAND_NODE0_CLOCK_UPDATE_INIT(type,change_mask,rate,ticks,monotonic_time,offset,scale,state,flags,latency) \
+ SPA_COMMAND_INIT_FULL(struct spa_command_node0_clock_update, \
+ sizeof(struct spa_command_node0_clock_update_body), 0, type, \
+ SPA_POD_INIT_Int(change_mask), \
+ SPA_POD_INIT_Int(rate), \
+ SPA_POD_INIT_Long(ticks), \
+ SPA_POD_INIT_Long(monotonic_time), \
+ SPA_POD_INIT_Long(offset), \
+ SPA_POD_INIT_Int(scale), \
+ SPA_POD_INIT_Int(state), \
+ SPA_POD_INIT_Int(flags), \
+ SPA_POD_INIT_Long(latency))
+
+
+/** \class pw_impl_client_node0
+ *
+ * PipeWire client node interface
+ */
+struct pw_impl_client_node0 {
+ struct pw_impl_node *node;
+
+ struct pw_resource *resource;
+};
+
+struct pw_impl_client_node0 *
+pw_impl_client_node0_new(struct pw_resource *resource,
+ struct pw_properties *properties);
+
+void
+pw_impl_client_node0_destroy(struct pw_impl_client_node0 *node);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PIPEWIRE_CLIENT_NODE0_H */
diff --git a/src/modules/module-client-node/v0/ext-client-node.h b/src/modules/module-client-node/v0/ext-client-node.h
new file mode 100644
index 0000000..21ba597
--- /dev/null
+++ b/src/modules/module-client-node/v0/ext-client-node.h
@@ -0,0 +1,414 @@
+/* PipeWire
+ *
+ * Copyright © 2016 Wim Taymans <wim.taymans@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef __PIPEWIRE_EXT_CLIENT_NODE0_H__
+#define __PIPEWIRE_EXT_CLIENT_NODE0_H__
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <spa/utils/defs.h>
+#include <spa/param/param.h>
+#include <spa/node/node.h>
+
+#include <pipewire/proxy.h>
+
+#define PW_TYPE_INTERFACE_ClientNode PW_TYPE_INFO_INTERFACE_BASE "ClientNode"
+
+#define PW_VERSION_CLIENT_NODE0 0
+
+struct pw_client_node0_message;
+
+/** Shared structure between client and server \memberof pw_client_node */
+struct pw_client_node0_area {
+ uint32_t max_input_ports; /**< max input ports of the node */
+ uint32_t n_input_ports; /**< number of input ports of the node */
+ uint32_t max_output_ports; /**< max output ports of the node */
+ uint32_t n_output_ports; /**< number of output ports of the node */
+};
+
+/** \class pw_client_node0_transport
+ *
+ * \brief Transport object
+ *
+ * The transport object contains shared data and ringbuffers to exchange
+ * events and data between the server and the client in a low-latency and
+ * lockfree way.
+ */
+struct pw_client_node0_transport {
+ struct pw_client_node0_area *area; /**< the transport area */
+ struct spa_io_buffers *inputs; /**< array of buffer input io */
+ struct spa_io_buffers *outputs; /**< array of buffer output io */
+ void *input_data; /**< input memory for ringbuffer */
+ struct spa_ringbuffer *input_buffer; /**< ringbuffer for input memory */
+ void *output_data; /**< output memory for ringbuffer */
+ struct spa_ringbuffer *output_buffer; /**< ringbuffer for output memory */
+
+ /** Destroy a transport
+ * \param trans a transport to destroy
+ * \memberof pw_client_node0_transport
+ */
+ void (*destroy) (struct pw_client_node0_transport *trans);
+
+ /** Add a message to the transport
+ * \param trans the transport to send the message on
+ * \param message the message to add
+ * \return 0 on success, < 0 on error
+ *
+ * Write \a message to the shared ringbuffer.
+ */
+ int (*add_message) (struct pw_client_node0_transport *trans, struct pw_client_node0_message *message);
+
+ /** Get next message from a transport
+ * \param trans the transport to get the message of
+ * \param[out] message the message to read
+ * \return < 0 on error, 1 when a message is available,
+ * 0 when no more messages are available.
+ *
+ * Get the skeleton next message from \a trans into \a message. This function will
+ * only read the head and object body of the message.
+ *
+ * After the complete size of the message has been calculated, you should call
+ * \ref parse_message() to read the complete message contents.
+ */
+ int (*next_message) (struct pw_client_node0_transport *trans, struct pw_client_node0_message *message);
+
+ /** Parse the complete message on transport
+ * \param trans the transport to read from
+ * \param[out] message memory that can hold the complete message
+ * \return 0 on success, < 0 on error
+ *
+ * Use this function after \ref next_message().
+ */
+ int (*parse_message) (struct pw_client_node0_transport *trans, void *message);
+};
+
+#define pw_client_node0_transport_destroy(t) ((t)->destroy((t)))
+#define pw_client_node0_transport_add_message(t,m) ((t)->add_message((t), (m)))
+#define pw_client_node0_transport_next_message(t,m) ((t)->next_message((t), (m)))
+#define pw_client_node0_transport_parse_message(t,m) ((t)->parse_message((t), (m)))
+
+enum pw_client_node0_message_type {
+ PW_CLIENT_NODE0_MESSAGE_HAVE_OUTPUT, /*< signal that the node has output */
+ PW_CLIENT_NODE0_MESSAGE_NEED_INPUT, /*< signal that the node needs input */
+ PW_CLIENT_NODE0_MESSAGE_PROCESS_INPUT, /*< instruct the node to process input */
+ PW_CLIENT_NODE0_MESSAGE_PROCESS_OUTPUT, /*< instruct the node output is processed */
+ PW_CLIENT_NODE0_MESSAGE_PORT_REUSE_BUFFER, /*< reuse a buffer */
+};
+
+struct pw_client_node0_message_body {
+ struct spa_pod_int type SPA_ALIGNED(8); /*< one of enum pw_client_node0_message_type */
+};
+
+struct pw_client_node0_message {
+ struct spa_pod_struct pod;
+ struct pw_client_node0_message_body body;
+};
+
+struct pw_client_node0_message_port_reuse_buffer_body {
+ struct spa_pod_int type SPA_ALIGNED(8); /*< PW_CLIENT_NODE0_MESSAGE_PORT_REUSE_BUFFER */
+ struct spa_pod_int port_id SPA_ALIGNED(8); /*< port id */
+ struct spa_pod_int buffer_id SPA_ALIGNED(8); /*< buffer id to reuse */
+};
+
+struct pw_client_node0_message_port_reuse_buffer {
+ struct spa_pod_struct pod;
+ struct pw_client_node0_message_port_reuse_buffer_body body;
+};
+
+#define PW_CLIENT_NODE0_MESSAGE_TYPE(message) (((struct pw_client_node0_message*)(message))->body.type.value)
+
+#define PW_CLIENT_NODE0_MESSAGE_INIT(message) ((struct pw_client_node0_message) \
+ { { { sizeof(struct pw_client_node0_message_body), SPA_TYPE_Struct } }, \
+ { SPA_POD_INIT_Int(message) } })
+
+#define PW_CLIENT_NODE0_MESSAGE_INIT_FULL(type,size,message,...) (type) \
+ { { { size, SPA_TYPE_Struct } }, \
+ { SPA_POD_INIT_Int(message), ##__VA_ARGS__ } } \
+
+#define PW_CLIENT_NODE0_MESSAGE_PORT_REUSE_BUFFER_INIT(port_id,buffer_id) \
+ PW_CLIENT_NODE0_MESSAGE_INIT_FULL(struct pw_client_node0_message_port_reuse_buffer, \
+ sizeof(struct pw_client_node0_message_port_reuse_buffer_body), \
+ PW_CLIENT_NODE0_MESSAGE_PORT_REUSE_BUFFER, \
+ SPA_POD_INIT_Int(port_id), \
+ SPA_POD_INIT_Int(buffer_id))
+
+/** information about a buffer */
+struct pw_client_node0_buffer {
+ uint32_t mem_id; /**< the memory id for the metadata */
+ uint32_t offset; /**< offset in memory */
+ uint32_t size; /**< size in memory */
+ struct spa_buffer *buffer; /**< buffer describing metadata and buffer memory */
+};
+
+#define PW_CLIENT_NODE0_METHOD_DONE 0
+#define PW_CLIENT_NODE0_METHOD_UPDATE 1
+#define PW_CLIENT_NODE0_METHOD_PORT_UPDATE 2
+#define PW_CLIENT_NODE0_METHOD_SET_ACTIVE 3
+#define PW_CLIENT_NODE0_METHOD_EVENT 4
+#define PW_CLIENT_NODE0_METHOD_DESTROY 5
+#define PW_CLIENT_NODE0_METHOD_NUM 6
+
+/** \ref pw_client_node methods */
+struct pw_client_node0_methods {
+#define PW_VERSION_CLIENT_NODE0_METHODS 0
+ uint32_t version;
+
+ /** Complete an async operation */
+ void (*done) (void *object, int seq, int res);
+
+ /**
+ * Update the node ports and properties
+ *
+ * Update the maximum number of ports and the params of the
+ * client node.
+ * \param change_mask bitfield with changed parameters
+ * \param max_input_ports new max input ports
+ * \param max_output_ports new max output ports
+ * \param params new params
+ */
+ void (*update) (void *object,
+#define PW_CLIENT_NODE0_UPDATE_MAX_INPUTS (1 << 0)
+#define PW_CLIENT_NODE0_UPDATE_MAX_OUTPUTS (1 << 1)
+#define PW_CLIENT_NODE0_UPDATE_PARAMS (1 << 2)
+ uint32_t change_mask,
+ uint32_t max_input_ports,
+ uint32_t max_output_ports,
+ uint32_t n_params,
+ const struct spa_pod **params);
+
+ /**
+ * Update a node port
+ *
+ * Update the information of one port of a node.
+ * \param direction the direction of the port
+ * \param port_id the port id to update
+ * \param change_mask a bitfield of changed items
+ * \param n_params number of port parameters
+ * \param params array of port parameters
+ * \param info port information
+ */
+ void (*port_update) (void *object,
+ enum spa_direction direction,
+ uint32_t port_id,
+#define PW_CLIENT_NODE0_PORT_UPDATE_PARAMS (1 << 0)
+#define PW_CLIENT_NODE0_PORT_UPDATE_INFO (1 << 1)
+ uint32_t change_mask,
+ uint32_t n_params,
+ const struct spa_pod **params,
+ const struct spa_port_info *info);
+ /**
+ * Activate or deactivate the node
+ */
+ void (*set_active) (void *object, bool active);
+ /**
+ * Send an event to the node
+ * \param event the event to send
+ */
+ void (*event) (void *object, struct spa_event *event);
+ /**
+ * Destroy the client_node
+ */
+ void (*destroy) (void *object);
+};
+
+#define PW_CLIENT_NODE0_EVENT_ADD_MEM 0
+#define PW_CLIENT_NODE0_EVENT_TRANSPORT 1
+#define PW_CLIENT_NODE0_EVENT_SET_PARAM 2
+#define PW_CLIENT_NODE0_EVENT_EVENT 3
+#define PW_CLIENT_NODE0_EVENT_COMMAND 4
+#define PW_CLIENT_NODE0_EVENT_ADD_PORT 5
+#define PW_CLIENT_NODE0_EVENT_REMOVE_PORT 6
+#define PW_CLIENT_NODE0_EVENT_PORT_SET_PARAM 7
+#define PW_CLIENT_NODE0_EVENT_PORT_USE_BUFFERS 8
+#define PW_CLIENT_NODE0_EVENT_PORT_COMMAND 9
+#define PW_CLIENT_NODE0_EVENT_PORT_SET_IO 10
+#define PW_CLIENT_NODE0_EVENT_NUM 11
+
+/** \ref pw_client_node events */
+struct pw_client_node0_events {
+#define PW_VERSION_CLIENT_NODE0_EVENTS 0
+ uint32_t version;
+ /**
+ * Memory was added to a node
+ *
+ * \param mem_id the id of the memory
+ * \param type the memory type
+ * \param memfd the fd of the memory
+ * \param flags flags for the \a memfd
+ */
+ void (*add_mem) (void *data,
+ uint32_t mem_id,
+ uint32_t type,
+ int memfd,
+ uint32_t flags);
+ /**
+ * Notify of a new transport area
+ *
+ * The transport area is used to exchange real-time commands between
+ * the client and the server.
+ *
+ * \param node_id the node id created for this client node
+ * \param readfd fd for signal data can be read
+ * \param writefd fd for signal data can be written
+ * \param transport the shared transport area
+ */
+ void (*transport) (void *data,
+ uint32_t node_id,
+ int readfd,
+ int writefd,
+ struct pw_client_node0_transport *transport);
+ /**
+ * Notify of a property change
+ *
+ * When the server configures the properties on the node
+ * this event is sent
+ *
+ * \param seq a sequence number
+ * \param id the id of the parameter
+ * \param flags parameter flags
+ * \param param the param to set
+ */
+ void (*set_param) (void *data, uint32_t seq,
+ uint32_t id, uint32_t flags,
+ const struct spa_pod *param);
+ /**
+ * Receive an event from the client node
+ * \param event the received event */
+ void (*event) (void *data, const struct spa_event *event);
+ /**
+ * Notify of a new node command
+ *
+ * \param seq a sequence number
+ * \param command the command
+ */
+ void (*command) (void *data, uint32_t seq, const struct spa_command *command);
+ /**
+ * A new port was added to the node
+ *
+ * The server can at any time add a port to the node when there
+ * are free ports available.
+ *
+ * \param seq a sequence number
+ * \param direction the direction of the port
+ * \param port_id the new port id
+ */
+ void (*add_port) (void *data,
+ uint32_t seq,
+ enum spa_direction direction,
+ uint32_t port_id);
+ /**
+ * A port was removed from the node
+ *
+ * \param seq a sequence number
+ * \param direction a port direction
+ * \param port_id the remove port id
+ */
+ void (*remove_port) (void *data,
+ uint32_t seq,
+ enum spa_direction direction,
+ uint32_t port_id);
+ /**
+ * A parameter was configured on the port
+ *
+ * \param seq a sequence number
+ * \param direction a port direction
+ * \param port_id the port id
+ * \param id the id of the parameter
+ * \param flags flags used when setting the param
+ * \param param the new param
+ */
+ void (*port_set_param) (void *data,
+ uint32_t seq,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t id, uint32_t flags,
+ const struct spa_pod *param);
+ /**
+ * Notify the port of buffers
+ *
+ * \param seq a sequence number
+ * \param direction a port direction
+ * \param port_id the port id
+ * \param n_buffer the number of buffers
+ * \param buffers and array of buffer descriptions
+ */
+ void (*port_use_buffers) (void *data,
+ uint32_t seq,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t n_buffers,
+ struct pw_client_node0_buffer *buffers);
+ /**
+ * Notify of a new port command
+ *
+ * \param direction a port direction
+ * \param port_id the port id
+ * \param command the command
+ */
+ void (*port_command) (void *data,
+ enum spa_direction direction,
+ uint32_t port_id,
+ const struct spa_command *command);
+
+ /**
+ * Configure the io area with \a id of \a port_id.
+ *
+ * \param seq a sequence number
+ * \param direction the direction of the port
+ * \param port_id the port id
+ * \param id the id of the io area to set
+ * \param mem_id the id of the memory to use
+ * \param offset offset of io area in memory
+ * \param size size of the io area
+ */
+ void (*port_set_io) (void *data,
+ uint32_t seq,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t id,
+ uint32_t mem_id,
+ uint32_t offset,
+ uint32_t size);
+};
+#define pw_client_node0_resource(r,m,v,...) pw_resource_call(r, struct pw_client_node0_events, m, v, ##__VA_ARGS__)
+
+#define pw_client_node0_resource_add_mem(r,...) pw_client_node0_resource(r,add_mem,0,__VA_ARGS__)
+#define pw_client_node0_resource_transport(r,...) pw_client_node0_resource(r,transport,0,__VA_ARGS__)
+#define pw_client_node0_resource_set_param(r,...) pw_client_node0_resource(r,set_param,0,__VA_ARGS__)
+#define pw_client_node0_resource_event(r,...) pw_client_node0_resource(r,event,0,__VA_ARGS__)
+#define pw_client_node0_resource_command(r,...) pw_client_node0_resource(r,command,0,__VA_ARGS__)
+#define pw_client_node0_resource_add_port(r,...) pw_client_node0_resource(r,add_port,0,__VA_ARGS__)
+#define pw_client_node0_resource_remove_port(r,...) pw_client_node0_resource(r,remove_port,0,__VA_ARGS__)
+#define pw_client_node0_resource_port_set_param(r,...) pw_client_node0_resource(r,port_set_param,0,__VA_ARGS__)
+#define pw_client_node0_resource_port_use_buffers(r,...) pw_client_node0_resource(r,port_use_buffers,0,__VA_ARGS__)
+#define pw_client_node0_resource_port_command(r,...) pw_client_node0_resource(r,port_command,0,__VA_ARGS__)
+#define pw_client_node0_resource_port_set_io(r,...) pw_client_node0_resource(r,port_set_io,0,__VA_ARGS__)
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* __PIPEWIRE_EXT_CLIENT_NODE0_H__ */
diff --git a/src/modules/module-client-node/v0/protocol-native.c b/src/modules/module-client-node/v0/protocol-native.c
new file mode 100644
index 0000000..86d8fe9
--- /dev/null
+++ b/src/modules/module-client-node/v0/protocol-native.c
@@ -0,0 +1,534 @@
+/* PipeWire
+ *
+ * Copyright © 2017 Wim Taymans <wim.taymans@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+
+#include <spa/pod/parser.h>
+#include <spa/pod/builder.h>
+#include <spa/utils/type-info.h>
+
+#include "pipewire/impl.h"
+
+#include "pipewire/extensions/protocol-native.h"
+
+#include "ext-client-node.h"
+
+#include "transport.h"
+
+#define PW_PROTOCOL_NATIVE_FLAG_REMAP (1<<0)
+
+extern uint32_t pw_protocol_native0_find_type(struct pw_impl_client *client, const char *type);
+extern int pw_protocol_native0_pod_to_v2(struct pw_impl_client *client, const struct spa_pod *pod,
+ struct spa_pod_builder *b);
+extern struct spa_pod * pw_protocol_native0_pod_from_v2(struct pw_impl_client *client,
+ const struct spa_pod *pod);
+extern uint32_t pw_protocol_native0_type_to_v2(struct pw_impl_client *client,
+ const struct spa_type_info *info, uint32_t type);
+
+static void
+client_node_marshal_add_mem(void *data,
+ uint32_t mem_id,
+ uint32_t type,
+ int memfd, uint32_t flags)
+{
+ struct pw_resource *resource = data;
+ struct pw_impl_client *client = pw_resource_get_client(resource);
+ struct spa_pod_builder *b;
+ const char *typename;
+
+ switch (type) {
+ case SPA_DATA_MemFd:
+ typename = "Spa:Enum:DataType:Fd:MemFd";
+ break;
+ case SPA_DATA_DmaBuf:
+ typename = "Spa:Enum:DataType:Fd:DmaBuf";
+ break;
+ default:
+ case SPA_DATA_MemPtr:
+ return;
+
+ }
+ b = pw_protocol_native_begin_resource(resource, PW_CLIENT_NODE0_EVENT_ADD_MEM, NULL);
+
+ spa_pod_builder_add_struct(b,
+ "i", mem_id,
+ "I", pw_protocol_native0_find_type(client, typename),
+ "i", pw_protocol_native_add_resource_fd(resource, memfd),
+ "i", flags);
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static void client_node_marshal_transport(void *data, uint32_t node_id, int readfd, int writefd,
+ struct pw_client_node0_transport *transport)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+ struct pw_client_node0_transport_info info;
+
+ pw_client_node0_transport_get_info(transport, &info);
+
+ b = pw_protocol_native_begin_resource(resource, PW_CLIENT_NODE0_EVENT_TRANSPORT, NULL);
+
+ spa_pod_builder_add_struct(b,
+ "i", node_id,
+ "i", pw_protocol_native_add_resource_fd(resource, readfd),
+ "i", pw_protocol_native_add_resource_fd(resource, writefd),
+ "i", pw_protocol_native_add_resource_fd(resource, info.memfd),
+ "i", info.offset,
+ "i", info.size);
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static void
+client_node_marshal_set_param(void *data, uint32_t seq, uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource, PW_CLIENT_NODE0_EVENT_SET_PARAM, NULL);
+
+ spa_pod_builder_add_struct(b,
+ "i", seq,
+ "I", id,
+ "i", flags,
+ "P", param);
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static void client_node_marshal_event_event(void *data, const struct spa_event *event)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource, PW_CLIENT_NODE0_EVENT_EVENT, NULL);
+
+ spa_pod_builder_add_struct(b, "P", event);
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static void
+client_node_marshal_command(void *data, uint32_t seq, const struct spa_command *command)
+{
+ struct pw_resource *resource = data;
+ struct pw_impl_client *client = pw_resource_get_client(resource);
+ struct spa_pod_builder *b;
+ struct spa_pod_frame f;
+
+ b = pw_protocol_native_begin_resource(resource, PW_CLIENT_NODE0_EVENT_COMMAND, NULL);
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_add(b, "i", seq, NULL);
+ if (SPA_COMMAND_TYPE(command) == 0)
+ spa_pod_builder_add(b, "P", command, NULL);
+ else
+ pw_protocol_native0_pod_to_v2(client, (struct spa_pod *)command, b);
+ spa_pod_builder_pop(b, &f);
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static void
+client_node_marshal_add_port(void *data,
+ uint32_t seq, enum spa_direction direction, uint32_t port_id)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource, PW_CLIENT_NODE0_EVENT_ADD_PORT, NULL);
+
+ spa_pod_builder_add_struct(b,
+ "i", seq,
+ "i", direction,
+ "i", port_id);
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static void
+client_node_marshal_remove_port(void *data,
+ uint32_t seq, enum spa_direction direction, uint32_t port_id)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource, PW_CLIENT_NODE0_EVENT_REMOVE_PORT, NULL);
+
+ spa_pod_builder_add_struct(b,
+ "i", seq,
+ "i", direction,
+ "i", port_id);
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static void
+client_node_marshal_port_set_param(void *data,
+ uint32_t seq,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t id,
+ uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct pw_resource *resource = data;
+ struct pw_impl_client *client = pw_resource_get_client(resource);
+ struct spa_pod_builder *b;
+ struct spa_pod_frame f;
+ const char *typename;
+
+ b = pw_protocol_native_begin_resource(resource, PW_CLIENT_NODE0_EVENT_PORT_SET_PARAM, NULL);
+
+ switch (id) {
+ case SPA_PARAM_Props:
+ typename = "Spa:Enum:ParamId:Props";
+ break;
+ case SPA_PARAM_Format:
+ typename = "Spa:Enum:ParamId:Format";
+ break;
+ default:
+ return;
+ }
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_add(b,
+ "i", seq,
+ "i", direction,
+ "i", port_id,
+ "I", pw_protocol_native0_find_type(client, typename),
+ "i", flags, NULL);
+ pw_protocol_native0_pod_to_v2(client, param, b);
+ spa_pod_builder_pop(b, &f);
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static void
+client_node_marshal_port_use_buffers(void *data,
+ uint32_t seq,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t n_buffers, struct pw_client_node0_buffer *buffers)
+{
+ struct pw_resource *resource = data;
+ struct pw_impl_client *client = pw_resource_get_client(resource);
+ struct spa_pod_builder *b;
+ struct spa_pod_frame f;
+ uint32_t i, j;
+
+ b = pw_protocol_native_begin_resource(resource, PW_CLIENT_NODE0_EVENT_PORT_USE_BUFFERS, NULL);
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_add(b,
+ "i", seq,
+ "i", direction,
+ "i", port_id,
+ "i", n_buffers, NULL);
+
+ for (i = 0; i < n_buffers; i++) {
+ struct spa_buffer *buf = buffers[i].buffer;
+
+ spa_pod_builder_add(b,
+ "i", buffers[i].mem_id,
+ "i", buffers[i].offset,
+ "i", buffers[i].size,
+ "i", i,
+ "i", buf->n_metas, NULL);
+
+ for (j = 0; j < buf->n_metas; j++) {
+ struct spa_meta *m = &buf->metas[j];
+ spa_pod_builder_add(b,
+ "I", pw_protocol_native0_type_to_v2(client, spa_type_meta_type, m->type),
+ "i", m->size, NULL);
+ }
+ spa_pod_builder_add(b, "i", buf->n_datas, NULL);
+ for (j = 0; j < buf->n_datas; j++) {
+ struct spa_data *d = &buf->datas[j];
+ spa_pod_builder_add(b,
+ "I", pw_protocol_native0_type_to_v2(client, spa_type_data_type, d->type),
+ "i", SPA_PTR_TO_UINT32(d->data),
+ "i", d->flags,
+ "i", d->mapoffset,
+ "i", d->maxsize, NULL);
+ }
+ }
+ spa_pod_builder_pop(b, &f);
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static void
+client_node_marshal_port_command(void *data,
+ uint32_t direction,
+ uint32_t port_id,
+ const struct spa_command *command)
+{
+ struct pw_resource *resource = data;
+ struct pw_impl_client *client = pw_resource_get_client(resource);
+ struct spa_pod_builder *b;
+ struct spa_pod_frame f;
+
+ b = pw_protocol_native_begin_resource(resource, PW_CLIENT_NODE0_EVENT_PORT_COMMAND, NULL);
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_add(b,
+ "i", direction,
+ "i", port_id, NULL);
+ pw_protocol_native0_pod_to_v2(client, (struct spa_pod *)command, b);
+ spa_pod_builder_pop(b, &f);
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static void
+client_node_marshal_port_set_io(void *data,
+ uint32_t seq,
+ uint32_t direction,
+ uint32_t port_id,
+ uint32_t id,
+ uint32_t memid,
+ uint32_t offset,
+ uint32_t size)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource, PW_CLIENT_NODE0_EVENT_PORT_SET_IO, NULL);
+
+ spa_pod_builder_add_struct(b,
+ "i", seq,
+ "i", direction,
+ "i", port_id,
+ "I", id,
+ "i", memid,
+ "i", offset,
+ "i", size);
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+
+static int client_node_demarshal_done(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ uint32_t seq, res;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ "i", &seq,
+ "i", &res) < 0)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_client_node0_methods, done, 0, seq, res);
+}
+
+static int client_node_demarshal_update(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ struct spa_pod_frame f;
+ uint32_t change_mask, max_input_ports, max_output_ports, n_params;
+ const struct spa_pod **params;
+ uint32_t i;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_push_struct(&prs, &f) < 0 ||
+ spa_pod_parser_get(&prs,
+ "i", &change_mask,
+ "i", &max_input_ports,
+ "i", &max_output_ports,
+ "i", &n_params, NULL) < 0)
+ return -EINVAL;
+
+ params = alloca(n_params * sizeof(struct spa_pod *));
+ for (i = 0; i < n_params; i++)
+ if (spa_pod_parser_get(&prs, "O", &params[i], NULL) < 0)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_client_node0_methods, update, 0, change_mask,
+ max_input_ports,
+ max_output_ports,
+ n_params,
+ params);
+}
+
+static int client_node_demarshal_port_update(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ struct spa_pod_frame f[2];
+ uint32_t i, direction, port_id, change_mask, n_params;
+ const struct spa_pod **params = NULL;
+ struct spa_port_info info = { 0 }, *infop = NULL;
+ struct spa_dict props;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_push_struct(&prs, &f[0]) < 0 ||
+ spa_pod_parser_get(&prs,
+ "i", &direction,
+ "i", &port_id,
+ "i", &change_mask,
+ "i", &n_params, NULL) < 0)
+ return -EINVAL;
+
+ params = alloca(n_params * sizeof(struct spa_pod *));
+ for (i = 0; i < n_params; i++)
+ if (spa_pod_parser_get(&prs, "O", &params[i], NULL) < 0)
+ return -EINVAL;
+
+
+ if (spa_pod_parser_push_struct(&prs, &f[1]) >= 0) {
+ infop = &info;
+
+ if (spa_pod_parser_get(&prs,
+ "i", &info.flags,
+ "i", &info.rate,
+ "i", &props.n_items, NULL) < 0)
+ return -EINVAL;
+
+ if (props.n_items > 0) {
+ info.props = &props;
+
+ props.items = alloca(props.n_items * sizeof(struct spa_dict_item));
+ for (i = 0; i < props.n_items; i++) {
+ if (spa_pod_parser_get(&prs,
+ "s", &props.items[i].key,
+ "s", &props.items[i].value,
+ NULL) < 0)
+ return -EINVAL;
+ }
+ }
+ }
+
+ return pw_resource_notify(resource, struct pw_client_node0_methods, port_update, 0, direction,
+ port_id,
+ change_mask,
+ n_params,
+ params, infop);
+}
+
+static int client_node_demarshal_set_active(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ int active;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ "b", &active) < 0)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_client_node0_methods, set_active, 0, active);
+}
+
+static int client_node_demarshal_event_method(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct pw_impl_client *client = pw_resource_get_client(resource);
+ struct spa_pod_parser prs;
+ struct spa_event *event;
+ int res;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ "O", &event) < 0)
+ return -EINVAL;
+
+ event = (struct spa_event*)pw_protocol_native0_pod_from_v2(client, (struct spa_pod *)event);
+
+ res = pw_resource_notify(resource, struct pw_client_node0_methods, event, 0, event);
+ free(event);
+
+ return res;
+}
+
+static int client_node_demarshal_destroy(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ int res;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs, NULL) < 0)
+ return -EINVAL;
+
+ res = pw_resource_notify(resource, struct pw_client_node0_methods, destroy, 0);
+ pw_resource_destroy(resource);
+ return res;
+}
+
+static const struct pw_protocol_native_demarshal pw_protocol_native_client_node_method_demarshal[] = {
+ { &client_node_demarshal_done, 0, 0 },
+ { &client_node_demarshal_update, 0, PW_PROTOCOL_NATIVE_FLAG_REMAP },
+ { &client_node_demarshal_port_update, 0, PW_PROTOCOL_NATIVE_FLAG_REMAP },
+ { &client_node_demarshal_set_active, 0, 0 },
+ { &client_node_demarshal_event_method, 0, PW_PROTOCOL_NATIVE_FLAG_REMAP },
+ { &client_node_demarshal_destroy, 0, 0 },
+};
+
+static const struct pw_client_node0_events pw_protocol_native_client_node_event_marshal = {
+ PW_VERSION_CLIENT_NODE0_EVENTS,
+ &client_node_marshal_add_mem,
+ &client_node_marshal_transport,
+ &client_node_marshal_set_param,
+ &client_node_marshal_event_event,
+ &client_node_marshal_command,
+ &client_node_marshal_add_port,
+ &client_node_marshal_remove_port,
+ &client_node_marshal_port_set_param,
+ &client_node_marshal_port_use_buffers,
+ &client_node_marshal_port_command,
+ &client_node_marshal_port_set_io,
+};
+
+static const struct pw_protocol_marshal pw_protocol_native_client_node_marshal = {
+ PW_TYPE_INTERFACE_ClientNode,
+ PW_VERSION_CLIENT_NODE0,
+ PW_CLIENT_NODE0_METHOD_NUM,
+ PW_CLIENT_NODE0_EVENT_NUM,
+ 0,
+ NULL,
+ .server_demarshal = &pw_protocol_native_client_node_method_demarshal,
+ .server_marshal = &pw_protocol_native_client_node_event_marshal,
+ NULL,
+};
+
+struct pw_protocol *pw_protocol_native_ext_client_node0_init(struct pw_context *context)
+{
+ struct pw_protocol *protocol;
+
+ protocol = pw_context_find_protocol(context, PW_TYPE_INFO_PROTOCOL_Native);
+
+ if (protocol == NULL)
+ return NULL;
+
+ pw_protocol_add_marshal(protocol, &pw_protocol_native_client_node_marshal);
+
+ return protocol;
+}
diff --git a/src/modules/module-client-node/v0/transport.c b/src/modules/module-client-node/v0/transport.c
new file mode 100644
index 0000000..fa84795
--- /dev/null
+++ b/src/modules/module-client-node/v0/transport.c
@@ -0,0 +1,262 @@
+/* PipeWire
+ *
+ * Copyright © 2016 Wim Taymans <wim.taymans@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <unistd.h>
+#include <errno.h>
+#include <sys/mman.h>
+
+#include <spa/utils/ringbuffer.h>
+#include <spa/node/io.h>
+
+#include <pipewire/impl.h>
+#include <pipewire/private.h>
+
+#include "ext-client-node.h"
+
+#include "transport.h"
+
+/** \cond */
+
+#define INPUT_BUFFER_SIZE (1<<12)
+#define OUTPUT_BUFFER_SIZE (1<<12)
+
+struct transport {
+ struct pw_client_node0_transport trans;
+
+ struct pw_memblock *mem;
+ size_t offset;
+
+ struct pw_client_node0_message current;
+ uint32_t current_index;
+};
+/** \endcond */
+
+static size_t area_get_size(struct pw_client_node0_area *area)
+{
+ size_t size;
+ size = sizeof(struct pw_client_node0_area);
+ size += area->max_input_ports * sizeof(struct spa_io_buffers);
+ size += area->max_output_ports * sizeof(struct spa_io_buffers);
+ size += sizeof(struct spa_ringbuffer);
+ size += INPUT_BUFFER_SIZE;
+ size += sizeof(struct spa_ringbuffer);
+ size += OUTPUT_BUFFER_SIZE;
+ return size;
+}
+
+static void transport_setup_area(void *p, struct pw_client_node0_transport *trans)
+{
+ struct pw_client_node0_area *a;
+
+ trans->area = a = p;
+ p = SPA_PTROFF(p, sizeof(struct pw_client_node0_area), struct spa_io_buffers);
+
+ trans->inputs = p;
+ p = SPA_PTROFF(p, a->max_input_ports * sizeof(struct spa_io_buffers), void);
+
+ trans->outputs = p;
+ p = SPA_PTROFF(p, a->max_output_ports * sizeof(struct spa_io_buffers), void);
+
+ trans->input_buffer = p;
+ p = SPA_PTROFF(p, sizeof(struct spa_ringbuffer), void);
+
+ trans->input_data = p;
+ p = SPA_PTROFF(p, INPUT_BUFFER_SIZE, void);
+
+ trans->output_buffer = p;
+ p = SPA_PTROFF(p, sizeof(struct spa_ringbuffer), void);
+
+ trans->output_data = p;
+ p = SPA_PTROFF(p, OUTPUT_BUFFER_SIZE, void);
+}
+
+static void transport_reset_area(struct pw_client_node0_transport *trans)
+{
+ uint32_t i;
+ struct pw_client_node0_area *a = trans->area;
+
+ for (i = 0; i < a->max_input_ports; i++) {
+ trans->inputs[i] = SPA_IO_BUFFERS_INIT;
+ }
+ for (i = 0; i < a->max_output_ports; i++) {
+ trans->outputs[i] = SPA_IO_BUFFERS_INIT;
+ }
+ spa_ringbuffer_init(trans->input_buffer);
+ spa_ringbuffer_init(trans->output_buffer);
+}
+
+static void destroy(struct pw_client_node0_transport *trans)
+{
+ struct transport *impl = (struct transport *) trans;
+
+ pw_log_debug("transport %p: destroy", trans);
+
+ pw_memblock_free(impl->mem);
+ free(impl);
+}
+
+static int add_message(struct pw_client_node0_transport *trans, struct pw_client_node0_message *message)
+{
+ struct transport *impl = (struct transport *) trans;
+ int32_t filled, avail;
+ uint32_t size, index;
+
+ if (impl == NULL || message == NULL)
+ return -EINVAL;
+
+ filled = spa_ringbuffer_get_write_index(trans->output_buffer, &index);
+ avail = OUTPUT_BUFFER_SIZE - filled;
+ size = SPA_POD_SIZE(message);
+ if (avail < (int)size)
+ return -ENOSPC;
+
+ spa_ringbuffer_write_data(trans->output_buffer,
+ trans->output_data, OUTPUT_BUFFER_SIZE,
+ index & (OUTPUT_BUFFER_SIZE - 1), message, size);
+ spa_ringbuffer_write_update(trans->output_buffer, index + size);
+
+ return 0;
+}
+
+static int next_message(struct pw_client_node0_transport *trans, struct pw_client_node0_message *message)
+{
+ struct transport *impl = (struct transport *) trans;
+ int32_t avail;
+
+ if (impl == NULL || message == NULL)
+ return -EINVAL;
+
+ avail = spa_ringbuffer_get_read_index(trans->input_buffer, &impl->current_index);
+ if (avail < (int) sizeof(struct pw_client_node0_message))
+ return 0;
+
+ spa_ringbuffer_read_data(trans->input_buffer,
+ trans->input_data, INPUT_BUFFER_SIZE,
+ impl->current_index & (INPUT_BUFFER_SIZE - 1),
+ &impl->current, sizeof(struct pw_client_node0_message));
+
+ if (avail < (int) SPA_POD_SIZE(&impl->current))
+ return 0;
+
+ *message = impl->current;
+
+ return 1;
+}
+
+static int parse_message(struct pw_client_node0_transport *trans, void *message)
+{
+ struct transport *impl = (struct transport *) trans;
+ uint32_t size;
+
+ if (impl == NULL || message == NULL)
+ return -EINVAL;
+
+ size = SPA_POD_SIZE(&impl->current);
+
+ spa_ringbuffer_read_data(trans->input_buffer,
+ trans->input_data, INPUT_BUFFER_SIZE,
+ impl->current_index & (INPUT_BUFFER_SIZE - 1), message, size);
+ spa_ringbuffer_read_update(trans->input_buffer, impl->current_index + size);
+
+ return 0;
+}
+
+/** Create a new transport
+ * \param max_input_ports maximum number of input_ports
+ * \param max_output_ports maximum number of output_ports
+ * \return a newly allocated \ref pw_client_node0_transport
+ * \memberof pw_client_node0_transport
+ */
+struct pw_client_node0_transport *
+pw_client_node0_transport_new(struct pw_context *context,
+ uint32_t max_input_ports, uint32_t max_output_ports)
+{
+ struct transport *impl;
+ struct pw_client_node0_transport *trans;
+ struct pw_client_node0_area area = { 0 };
+
+ area.max_input_ports = max_input_ports;
+ area.n_input_ports = 0;
+ area.max_output_ports = max_output_ports;
+ area.n_output_ports = 0;
+
+ impl = calloc(1, sizeof(struct transport));
+ if (impl == NULL)
+ return NULL;
+
+ pw_log_debug("transport %p: new %d %d", impl, max_input_ports, max_output_ports);
+
+ trans = &impl->trans;
+ impl->offset = 0;
+
+ impl->mem = pw_mempool_alloc(context->pool,
+ PW_MEMBLOCK_FLAG_READWRITE |
+ PW_MEMBLOCK_FLAG_MAP |
+ PW_MEMBLOCK_FLAG_SEAL,
+ SPA_DATA_MemFd, area_get_size(&area));
+ if (impl->mem == NULL) {
+ free(impl);
+ return NULL;
+ }
+
+ memcpy(impl->mem->map->ptr, &area, sizeof(struct pw_client_node0_area));
+ transport_setup_area(impl->mem->map->ptr, trans);
+ transport_reset_area(trans);
+
+ trans->destroy = destroy;
+ trans->add_message = add_message;
+ trans->next_message = next_message;
+ trans->parse_message = parse_message;
+
+ return trans;
+}
+
+struct pw_client_node0_transport *
+pw_client_node0_transport_new_from_info(struct pw_client_node0_transport_info *info)
+{
+ errno = ENOTSUP;
+ return NULL;
+}
+
+/** Get transport info
+ * \param trans the transport to get info of
+ * \param[out] info transport info
+ * \return 0 on success
+ *
+ * Fill \a info with the transport info of \a trans. This information can be
+ * passed to the client to set up the shared transport.
+ *
+ * \memberof pw_client_node0_transport
+ */
+int pw_client_node0_transport_get_info(struct pw_client_node0_transport *trans,
+ struct pw_client_node0_transport_info *info)
+{
+ struct transport *impl = (struct transport *) trans;
+
+ info->memfd = impl->mem->fd;
+ info->offset = impl->offset;
+ info->size = impl->mem->size;
+
+ return 0;
+}
diff --git a/src/modules/module-client-node/v0/transport.h b/src/modules/module-client-node/v0/transport.h
new file mode 100644
index 0000000..3abcd6d
--- /dev/null
+++ b/src/modules/module-client-node/v0/transport.h
@@ -0,0 +1,59 @@
+/* PipeWire
+ *
+ * Copyright © 2016 Wim Taymans <wim.taymans@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef __PIPEWIRE_CLIENT_NODE0_TRANSPORT_H__
+#define __PIPEWIRE_CLIENT_NODE0_TRANSPORT_H__
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <string.h>
+
+#include <spa/utils/defs.h>
+
+#include <pipewire/mem.h>
+
+/** information about the transport region \memberof pw_client_node */
+struct pw_client_node0_transport_info {
+ int memfd; /**< the memfd of the transport area */
+ uint32_t offset; /**< offset to map \a memfd at */
+ uint32_t size; /**< size of memfd mapping */
+};
+
+struct pw_client_node0_transport *
+pw_client_node0_transport_new(struct pw_context *context, uint32_t max_input_ports, uint32_t max_output_ports);
+
+struct pw_client_node0_transport *
+pw_client_node0_transport_new_from_info(struct pw_client_node0_transport_info *info);
+
+int
+pw_client_node0_transport_get_info(struct pw_client_node0_transport *trans,
+ struct pw_client_node0_transport_info *info);
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* __PIPEWIRE_CLIENT_NODE0_TRANSPORT_H__ */
diff --git a/src/modules/module-combine-stream.c b/src/modules/module-combine-stream.c
new file mode 100644
index 0000000..88b5644
--- /dev/null
+++ b/src/modules/module-combine-stream.c
@@ -0,0 +1,1063 @@
+/* PipeWire
+ *
+ * Copyright © 2023 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <signal.h>
+#include <limits.h>
+#include <math.h>
+
+#include "config.h"
+
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+#include <spa/utils/json.h>
+#include <spa/utils/ringbuffer.h>
+#include <spa/debug/types.h>
+#include <spa/pod/builder.h>
+#include <spa/param/audio/format-utils.h>
+#include <spa/param/audio/raw.h>
+
+#include <pipewire/impl.h>
+#include <pipewire/i18n.h>
+
+/** \page page_module_combine_stream PipeWire Module: Combine Stream
+ *
+ * The combine stream can make:
+ *
+ * - a new virtual sink that forwards audio to other sinks
+ * - a new virtual source that combines audio from other sources
+ *
+ * ## Module Options
+ *
+ * - `node.name`: a unique name for the stream
+ * - `node.description`: a human readable name for the stream
+ * - `combine.mode` = capture | playback | sink | source, default sink
+ * - `combine.props = {}`: properties to be passed to the sink/source
+ * - `stream.props = {}`: properties to be passed to the streams
+ *
+ * ## General options
+ *
+ * Options with well-known behavior.
+ *
+ * - \ref PW_KEY_REMOTE_NAME
+ * - \ref PW_KEY_AUDIO_CHANNELS
+ * - \ref SPA_KEY_AUDIO_POSITION
+ * - \ref PW_KEY_MEDIA_NAME
+ * - \ref PW_KEY_NODE_LATENCY
+ * - \ref PW_KEY_NODE_NAME
+ * - \ref PW_KEY_NODE_DESCRIPTION
+ * - \ref PW_KEY_NODE_GROUP
+ * - \ref PW_KEY_NODE_VIRTUAL
+ * - \ref PW_KEY_MEDIA_CLASS
+ *
+ * ## Stream options
+ *
+ * - `audio.position`: Set the stream channel map. By default this is the same channel
+ * map as the combine stream.
+ * - `combine.audio.position`: map the combine audio positions to the stream positions.
+ * combine input channels are mapped one-by-one to stream output channels.
+ *
+ * ## Example configuration
+ *
+ *\code{.unparsed}
+ * context.modules = [
+ * { name = libpipewire-module-combine-stream
+ * args = {
+ * combine.mode = sink
+ * node.name = "combine_sink"
+ * node.description = "My Combine Sink"
+ * combine.props = {
+ * audio.position = [ FL FR ]
+ * }
+ * stream.props = {
+ * }
+ * stream.rules = [
+ * {
+ * matches = [
+ * # any of the items in matches needs to match, if one does,
+ * # actions are emited.
+ * {
+ * # all keys must match the value. ~ in value starts regex.
+ * #node.name = "~alsa_input.*"
+ * media.class = "Audio/Sink"
+ * }
+ * ]
+ * actions = {
+ * create-stream = {
+ * #combine.audio.position = [ FL FR ]
+ * #audio.position = [ FL FR ]
+ * }
+ * }
+ * }
+ * ]
+ * }
+ * }
+ * ]
+ *\endcode
+ *
+ * Below is an example configuration that makes a 5.1 virtual audio sink
+ * from 3 separate stereo sinks.
+ *
+ *\code{.unparsed}
+ * context.modules = [
+ * { name = libpipewire-module-combine-stream
+ * args = {
+ * combine.mode = sink
+ * node.name = "combine_sink_5_1"
+ * node.description = "My 5.1 Combine Sink"
+ * combine.props = {
+ * audio.position = [ FL FR FC LFE SL SR ]
+ * }
+ * stream.props = {
+ * stream.dont-remix = true # link matching channels without remixing
+ * }
+ * stream.rules = [
+ * { matches = [
+ * { media.class = "Audio/Sink"
+ * node.name = "alsa_output.usb-Topping_E30-00.analog-stereo"
+ * } ]
+ * actions = { create-stream = {
+ * combine.audio.position = [ FL FR ]
+ * audio.position = [ FL FR ]
+ * } } }
+ * { matches = [
+ * { media.class = "Audio/Sink"
+ * node.name = "alsa_output.usb-BEHRINGER_UMC404HD_192k-00.pro-output-0"
+ * } ]
+ * actions = { create-stream = {
+ * combine.audio.position = [ FC LFE ]
+ * audio.position = [ AUX0 AUX1 ]
+ * } } }
+ * { matches = [
+ * { media.class = "Audio/Sink"
+ * node.name = "alsa_output.pci-0000_00_1b.0.analog-stereo"
+ * } ]
+ * actions = { create-stream = {
+ * combine.audio.position = [ SL SR ]
+ * audio.position = [ FL FR ]
+ * } } }
+ * ]
+ * }
+ * }
+ * ]
+ *\endcode
+ *
+ * Below is an example configuration that makes a 4.0 virtual audio source
+ * from 2 separate stereo sources.
+ *
+ *\code{.unparsed}
+ * context.modules = [
+ * { name = libpipewire-module-combine-stream
+ * args = {
+ * combine.mode = source
+ * node.name = "combine_source_4_0"
+ * node.description = "My 4.0 Combine Source"
+ * combine.props = {
+ * audio.position = [ FL FR SL SR ]
+ * }
+ * stream.props = {
+ * stream.dont-remix = true
+ * }
+ * stream.rules = [
+ * { matches = [
+ * { media.class = "Audio/Source"
+ * node.name = "alsa_input.usb-046d_HD_Pro_Webcam_C920_09D53E1F-02.analog-stereo"
+ * } ]
+ * actions = { create-stream = {
+ * audio.position = [ FL FR ]
+ * combine.audio.position = [ FL FR ]
+ * } } }
+ * { matches = [
+ * { media.class = "Audio/Source"
+ * node.name = "alsa_input.usb-046d_0821_9534DE90-00.analog-stereo"
+ * } ]
+ * actions = { create-stream = {
+ * audio.position = [ FL FR ]
+ * combine.audio.position = [ SL SR ]
+ * } } }
+ * ]
+ * }
+ * }
+ * ]
+ *\endcode
+ */
+
+#define NAME "combine-stream"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+#define DEFAULT_CHANNELS 2
+#define DEFAULT_POSITION "[ FL FR ]"
+
+#define MODULE_USAGE "[ node.latency=<latency as fraction> ] " \
+ "[ combine.mode=<mode of stream, playback|capture|sink|source>, default:sink ] " \
+ "[ node.name=<name of the stream> ] " \
+ "[ node.description=<description of the stream> ] " \
+ "[ audio.channels=<number of channels, default:"SPA_STRINGIFY(DEFAULT_CHANNELS) "> ] " \
+ "[ audio.position=<channel map, default:"DEFAULT_POSITION"> ] " \
+ "[ combine.props=<properties> ] " \
+ "[ stream.props=<properties> ] " \
+ "[ stream.rules=<properties> ] "
+
+
+static const struct spa_dict_item module_props[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Combine multiple streams into a single stream" },
+ { PW_KEY_MODULE_USAGE, MODULE_USAGE },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+struct impl {
+ struct pw_context *context;
+ struct pw_data_loop *data_loop;
+
+ struct pw_properties *props;
+
+#define MODE_SINK 0
+#define MODE_SOURCE 1
+#define MODE_CAPTURE 2
+#define MODE_PLAYBACK 3
+ uint32_t mode;
+ struct pw_impl_module *module;
+
+ struct spa_hook module_listener;
+
+ struct pw_core *core;
+ struct spa_hook core_proxy_listener;
+ struct spa_hook core_listener;
+
+ struct pw_registry *registry;
+ struct spa_hook registry_listener;
+
+ struct pw_properties *combine_props;
+ struct pw_stream *combine;
+ struct spa_hook combine_listener;
+ struct pw_stream_events combine_events;
+ uint32_t combine_id;
+
+ struct pw_properties *stream_props;
+
+ struct spa_audio_info_raw info;
+
+ unsigned int do_disconnect:1;
+
+ struct spa_list streams;
+ uint32_t n_streams;
+};
+
+struct stream {
+ uint32_t id;
+
+ struct impl *impl;
+
+ struct spa_list link;
+ struct pw_stream *stream;
+ struct spa_hook stream_listener;
+ struct pw_stream_events stream_events;
+
+ struct spa_audio_info_raw info;
+ uint32_t remap[SPA_AUDIO_MAX_CHANNELS];
+
+ unsigned int ready:1;
+ unsigned int added:1;
+};
+
+static uint32_t channel_from_name(const char *name)
+{
+ int i;
+ for (i = 0; spa_type_audio_channel[i].name; i++) {
+ if (spa_streq(name, spa_debug_type_short_name(spa_type_audio_channel[i].name)))
+ return spa_type_audio_channel[i].type;
+ }
+ return SPA_AUDIO_CHANNEL_UNKNOWN;
+}
+
+static void parse_position(struct spa_audio_info_raw *info, const char *val, size_t len)
+{
+ struct spa_json it[2];
+ char v[256];
+
+ spa_json_init(&it[0], val, len);
+ if (spa_json_enter_array(&it[0], &it[1]) <= 0)
+ spa_json_init(&it[1], val, len);
+
+ info->channels = 0;
+ while (spa_json_get_string(&it[1], v, sizeof(v)) > 0 &&
+ info->channels < SPA_AUDIO_MAX_CHANNELS) {
+ info->position[info->channels++] = channel_from_name(v);
+ }
+}
+
+static void parse_audio_info(const struct pw_properties *props, struct spa_audio_info_raw *info)
+{
+ const char *str;
+
+ spa_zero(*info);
+ info->format = SPA_AUDIO_FORMAT_F32P;
+ info->channels = pw_properties_get_uint32(props, PW_KEY_AUDIO_CHANNELS, 0);
+ info->channels = SPA_MIN(info->channels, SPA_AUDIO_MAX_CHANNELS);
+ if ((str = pw_properties_get(props, SPA_KEY_AUDIO_POSITION)) != NULL)
+ parse_position(info, str, strlen(str));
+ if (info->channels == 0)
+ parse_position(info, DEFAULT_POSITION, strlen(DEFAULT_POSITION));
+}
+
+static struct stream *find_stream(struct impl *impl, uint32_t id)
+{
+ struct stream *s;
+ spa_list_for_each(s, &impl->streams, link)
+ if (s->id == id)
+ return s;
+ return NULL;
+}
+
+static int do_add_stream(struct spa_loop *loop, bool async, uint32_t seq,
+ const void *data, size_t size, void *user_data)
+{
+ struct stream *s = user_data;
+ struct impl *impl = s->impl;
+ if (!s->added) {
+ spa_list_append(&impl->streams, &s->link);
+ impl->n_streams++;
+ s->added = true;
+ }
+ return 0;
+}
+
+static int do_remove_stream(struct spa_loop *loop, bool async, uint32_t seq,
+ const void *data, size_t size, void *user_data)
+{
+ struct stream *s = user_data;
+ if (s->added) {
+ spa_list_remove(&s->link);
+ s->impl->n_streams--;
+ s->added = false;
+ }
+ return 0;
+}
+
+static void destroy_stream(struct stream *s)
+{
+ pw_log_debug("destroy stream %d", s->id);
+
+ pw_data_loop_invoke(s->impl->data_loop, do_remove_stream, 0, NULL, 0, true, s);
+
+ if (s->stream) {
+ spa_hook_remove(&s->stream_listener);
+ pw_stream_destroy(s->stream);
+ }
+ free(s);
+}
+
+static void stream_destroy(void *d)
+{
+ struct stream *s = d;
+ spa_hook_remove(&s->stream_listener);
+ s->stream = NULL;
+ destroy_stream(s);
+}
+
+static void stream_input_process(void *d)
+{
+ struct stream *s = d, *t;
+ struct impl *impl = s->impl;
+ bool ready = true;
+
+ s->ready = true;
+ pw_log_debug("stream ready %p", s);
+ spa_list_for_each(t, &impl->streams, link) {
+ if (!t->ready) {
+ ready = false;
+ break;
+ }
+ }
+ if (ready) {
+ pw_log_debug("do trigger");
+ pw_stream_trigger_process(impl->combine);
+ }
+}
+
+static void stream_state_changed(void *d, enum pw_stream_state old,
+ enum pw_stream_state state, const char *error)
+{
+ struct stream *s = d;
+ switch (state) {
+ case PW_STREAM_STATE_ERROR:
+ case PW_STREAM_STATE_UNCONNECTED:
+ stream_destroy(s);
+ break;
+ default:
+ break;
+ }
+}
+
+static const struct pw_stream_events stream_events = {
+ PW_VERSION_STREAM_EVENTS,
+ .destroy = stream_destroy,
+ .state_changed = stream_state_changed,
+};
+
+struct stream_info {
+ struct impl *impl;
+ uint32_t id;
+ const struct spa_dict *props;
+ struct pw_properties *stream_props;
+};
+
+static int create_stream(struct stream_info *info)
+{
+ struct impl *impl = info->impl;
+ int res;
+ uint32_t n_params, i, j;
+ const struct spa_pod *params[1];
+ const char *str, *node_name;
+ uint8_t buffer[1024];
+ struct spa_pod_builder b;
+ struct spa_audio_info_raw remap_info, tmp_info;
+ struct stream *s;
+ enum pw_stream_flags flags;
+ enum pw_direction direction;
+
+ node_name = spa_dict_lookup(info->props, "node.name");
+ if (node_name == NULL)
+ node_name = spa_dict_lookup(info->props, "object.serial");
+ if (node_name == NULL)
+ return -EIO;
+
+ pw_log_info("create stream for %d %s", info->id, node_name);
+
+ s = calloc(1, sizeof(*s));
+ if (s == NULL)
+ goto error_errno;
+
+ s->id = info->id;
+ s->impl = impl;
+
+ s->info = impl->info;
+ if ((str = pw_properties_get(info->stream_props, SPA_KEY_AUDIO_POSITION)) != NULL)
+ parse_position(&s->info, str, strlen(str));
+ if (s->info.channels == 0)
+ s->info = impl->info;
+
+ spa_zero(remap_info);
+ if ((str = pw_properties_get(info->stream_props, "combine.audio.position")) != NULL)
+ parse_position(&remap_info, str, strlen(str));
+ if (remap_info.channels == 0)
+ remap_info = s->info;
+
+ tmp_info = impl->info;
+ for (i = 0; i < remap_info.channels; i++) {
+ s->remap[i] = i;
+ for (j = 0; j < tmp_info.channels; j++) {
+ if (tmp_info.position[j] == remap_info.position[i]) {
+ s->remap[i] = j;
+ break;
+ }
+ }
+ pw_log_info("remap %d -> %d", i, s->remap[i]);
+ }
+
+ str = pw_properties_get(impl->props, PW_KEY_NODE_DESCRIPTION);
+ if (str == NULL)
+ str = pw_properties_get(impl->props, PW_KEY_NODE_NAME);
+ if (str == NULL)
+ str = node_name;
+
+ if (pw_properties_get(info->stream_props, PW_KEY_MEDIA_NAME) == NULL)
+ pw_properties_setf(info->stream_props, PW_KEY_MEDIA_NAME,
+ "%s output", str);
+ if (pw_properties_get(info->stream_props, PW_KEY_NODE_DESCRIPTION) == NULL)
+ pw_properties_setf(info->stream_props, PW_KEY_NODE_DESCRIPTION,
+ "%s output", str);
+
+ str = pw_properties_get(impl->props, PW_KEY_NODE_NAME);
+ if (str == NULL)
+ str = "combine_stream";
+
+ if (pw_properties_get(info->stream_props, PW_KEY_NODE_NAME) == NULL)
+ pw_properties_setf(info->stream_props, PW_KEY_NODE_NAME,
+ "output.%s_%s", str, node_name);
+ if (pw_properties_get(info->stream_props, PW_KEY_TARGET_OBJECT) == NULL)
+ pw_properties_set(info->stream_props, PW_KEY_TARGET_OBJECT, node_name);
+
+ s->stream = pw_stream_new(impl->core, "Combine stream", info->stream_props);
+ info->stream_props = NULL;
+ if (s->stream == NULL)
+ goto error_errno;
+
+ s->stream_events = stream_events;
+
+ flags = PW_STREAM_FLAG_AUTOCONNECT |
+ PW_STREAM_FLAG_MAP_BUFFERS |
+ PW_STREAM_FLAG_RT_PROCESS;
+
+ if (impl->mode == MODE_SINK || impl->mode == MODE_CAPTURE) {
+ direction = PW_DIRECTION_OUTPUT;
+ flags |= PW_STREAM_FLAG_TRIGGER;
+ } else {
+ direction = PW_DIRECTION_INPUT;
+ s->stream_events.process = stream_input_process;
+ }
+
+ pw_stream_add_listener(s->stream,
+ &s->stream_listener,
+ &s->stream_events, s);
+
+ n_params = 0;
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ params[n_params++] = spa_format_audio_raw_build(&b,
+ SPA_PARAM_EnumFormat, &s->info);
+
+ if ((res = pw_stream_connect(s->stream,
+ direction, PW_ID_ANY, flags, params, n_params)) < 0)
+ goto error;
+
+ pw_data_loop_invoke(impl->data_loop, do_add_stream, 0, NULL, 0, true, s);
+ return 0;
+
+error_errno:
+ res = -errno;
+error:
+ if (s)
+ destroy_stream(s);
+ return res;
+}
+
+static int rule_matched(void *data, const char *location, const char *action,
+ const char *str, size_t len)
+{
+ struct stream_info *i = data;
+ struct impl *impl = i->impl;
+ int res = 0;
+
+ if (spa_streq(action, "create-stream")) {
+ i->stream_props = pw_properties_copy(impl->stream_props);
+
+ pw_properties_update_string(i->stream_props, str, len);
+
+ res = create_stream(i);
+
+ pw_properties_free(i->stream_props);
+ }
+
+ return res;
+}
+
+static void registry_event_global(void *data, uint32_t id,
+ uint32_t permissions, const char *type, uint32_t version,
+ const struct spa_dict *props)
+{
+ struct impl *impl = data;
+ const char *str;
+ struct stream_info info;
+
+ if (!spa_streq(type, PW_TYPE_INTERFACE_Node) || props == NULL)
+ return;
+
+ if (id == impl->combine_id)
+ return;
+
+ spa_zero(info);
+ info.impl = impl;
+ info.id = id;
+ info.props = props;
+
+ str = pw_properties_get(impl->props, "stream.rules");
+ if (str == NULL) {
+ if (impl->mode == MODE_CAPTURE || impl->mode == MODE_SINK)
+ str = "[ { matches = [ { media.class = \"Audio/Sink\" } ] "
+ " actions = { create-stream = {} } } ]";
+ else if (impl->mode == MODE_PLAYBACK || impl->mode == MODE_SOURCE)
+ str = "[ { matches = [ { media.class = \"Audio/Source\" } ] "
+ " actions = { create-stream = {} } } ]";
+ }
+ pw_conf_match_rules(str, strlen(str), NAME, props, rule_matched, &info);
+}
+
+static void registry_event_global_remove(void *data, uint32_t id)
+{
+ struct impl *impl = data;
+ struct stream *s;
+
+ s = find_stream(impl, id);
+ if (s == NULL)
+ return;
+
+ destroy_stream(s);
+}
+
+static const struct pw_registry_events registry_events = {
+ PW_VERSION_REGISTRY_EVENTS,
+ .global = registry_event_global,
+ .global_remove = registry_event_global_remove,
+};
+
+static void combine_destroy(void *d)
+{
+ struct impl *impl = d;
+ spa_hook_remove(&impl->combine_listener);
+ impl->combine = NULL;
+}
+
+static void combine_state_changed(void *d, enum pw_stream_state old,
+ enum pw_stream_state state, const char *error)
+{
+ struct impl *impl = d;
+ switch (state) {
+ case PW_STREAM_STATE_ERROR:
+ case PW_STREAM_STATE_UNCONNECTED:
+ pw_impl_module_schedule_destroy(impl->module);
+ break;
+ case PW_STREAM_STATE_PAUSED:
+ impl->combine_id = pw_stream_get_node_id(impl->combine);
+ pw_log_info("got combine id %d", impl->combine_id);
+ break;
+ case PW_STREAM_STATE_STREAMING:
+ break;
+ default:
+ break;
+ }
+}
+
+static void combine_input_process(void *d)
+{
+ struct impl *impl = d;
+ struct pw_buffer *in, *out;
+ struct stream *s;
+
+ if ((in = pw_stream_dequeue_buffer(impl->combine)) == NULL) {
+ pw_log_debug("out of buffers: %m");
+ return;
+ }
+
+ spa_list_for_each(s, &impl->streams, link) {
+ uint32_t j;
+
+ if (s->stream == NULL)
+ continue;
+
+ if ((out = pw_stream_dequeue_buffer(s->stream)) == NULL) {
+ pw_log_warn("out of playback buffers: %m");
+ goto do_trigger;
+ }
+
+ for (j = 0; j < out->buffer->n_datas; j++) {
+ struct spa_data *ds, *dd;
+ uint32_t outsize = 0, remap;
+ int32_t stride = 0;
+
+ dd = &out->buffer->datas[j];
+
+ remap = s->remap[j];
+ if (remap < in->buffer->n_datas) {
+ uint32_t offs, size;
+
+ ds = &in->buffer->datas[remap];
+
+ offs = SPA_MIN(ds->chunk->offset, ds->maxsize);
+ size = SPA_MIN(ds->chunk->size, ds->maxsize - offs);
+
+ memcpy(dd->data,
+ SPA_PTROFF(ds->data, offs, void), size);
+
+ outsize = SPA_MAX(outsize, size);
+ stride = SPA_MAX(stride, ds->chunk->stride);
+ } else {
+ memset(dd->data, 0, outsize);
+ }
+ dd->chunk->offset = 0;
+ dd->chunk->size = outsize;
+ dd->chunk->stride = stride;
+ }
+ pw_stream_queue_buffer(s->stream, out);
+do_trigger:
+ pw_stream_trigger_process(s->stream);
+ }
+ pw_stream_queue_buffer(impl->combine, in);
+}
+
+static void combine_output_process(void *d)
+{
+ struct impl *impl = d;
+ struct pw_buffer *in, *out;
+ struct stream *s;
+
+ if ((out = pw_stream_dequeue_buffer(impl->combine)) == NULL) {
+ pw_log_debug("out of buffers: %m");
+ return;
+ }
+
+ spa_list_for_each(s, &impl->streams, link) {
+ uint32_t j;
+
+ if (s->stream == NULL)
+ continue;
+
+ if ((in = pw_stream_dequeue_buffer(s->stream)) == NULL) {
+ pw_log_warn("%p: out of capture buffers: %m", s);
+ continue;
+ }
+ s->ready = false;
+
+ for (j = 0; j < in->buffer->n_datas; j++) {
+ struct spa_data *ds, *dd;
+ uint32_t outsize = 0, remap;
+ int32_t stride = 0;
+
+ ds = &in->buffer->datas[j];
+
+ /* FIXME, need to do mixing for overlapping streams */
+ remap = s->remap[j];
+ if (remap < out->buffer->n_datas) {
+ uint32_t offs, size;
+
+ dd = &out->buffer->datas[remap];
+
+ offs = SPA_MIN(ds->chunk->offset, ds->maxsize);
+ size = SPA_MIN(ds->chunk->size, ds->maxsize - offs);
+ size = SPA_MIN(size, dd->maxsize);
+
+ memcpy(dd->data,
+ SPA_PTROFF(ds->data, offs, void), size);
+
+ outsize = SPA_MAX(outsize, size);
+ stride = SPA_MAX(stride, ds->chunk->stride);
+
+ dd->chunk->offset = 0;
+ dd->chunk->size = outsize;
+ dd->chunk->stride = stride;
+ }
+ }
+ pw_stream_queue_buffer(s->stream, in);
+ }
+ pw_stream_queue_buffer(impl->combine, out);
+}
+
+static const struct pw_stream_events combine_events = {
+ PW_VERSION_STREAM_EVENTS,
+ .destroy = combine_destroy,
+ .state_changed = combine_state_changed,
+};
+
+static int create_combine(struct impl *impl)
+{
+ int res;
+ uint32_t n_params;
+ const struct spa_pod *params[1];
+ uint8_t buffer[1024];
+ struct spa_pod_builder b;
+ enum pw_direction direction;
+ enum pw_stream_flags flags;
+
+ impl->combine = pw_stream_new(impl->core, "Combine stream", impl->combine_props);
+ impl->combine_props = NULL;
+
+ if (impl->combine == NULL)
+ return -errno;
+
+ flags = PW_STREAM_FLAG_AUTOCONNECT |
+ PW_STREAM_FLAG_MAP_BUFFERS |
+ PW_STREAM_FLAG_RT_PROCESS;
+
+ impl->combine_events = combine_events;
+
+ if (impl->mode == MODE_SINK || impl->mode == MODE_CAPTURE) {
+ direction = PW_DIRECTION_INPUT;
+ impl->combine_events.process = combine_input_process;
+ } else {
+ direction = PW_DIRECTION_OUTPUT;
+ impl->combine_events.process = combine_output_process;
+ flags |= PW_STREAM_FLAG_TRIGGER;
+ }
+
+ pw_stream_add_listener(impl->combine,
+ &impl->combine_listener,
+ &impl->combine_events, impl);
+
+ n_params = 0;
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ params[n_params++] = spa_format_audio_raw_build(&b,
+ SPA_PARAM_EnumFormat, &impl->info);
+
+ if ((res = pw_stream_connect(impl->combine,
+ direction, PW_ID_ANY, flags, params, n_params)) < 0)
+ return res;
+
+ return 0;
+}
+
+static void core_error(void *data, uint32_t id, int seq, int res, const char *message)
+{
+ struct impl *impl = data;
+
+ pw_log_error("error id:%u seq:%d res:%d (%s): %s",
+ id, seq, res, spa_strerror(res), message);
+
+ if (id == PW_ID_CORE && res == -EPIPE)
+ pw_impl_module_schedule_destroy(impl->module);
+}
+
+static const struct pw_core_events core_events = {
+ PW_VERSION_CORE_EVENTS,
+ .error = core_error,
+};
+
+static void core_removed(void *d)
+{
+ struct impl *impl = d;
+ if (impl->core) {
+ spa_hook_remove(&impl->core_listener);
+ impl->core = NULL;
+ }
+ if (impl->registry) {
+ spa_hook_remove(&impl->registry_listener);
+ pw_proxy_destroy((struct pw_proxy*)impl->registry);
+ impl->registry = NULL;
+ }
+ pw_impl_module_schedule_destroy(impl->module);
+}
+
+static const struct pw_proxy_events core_proxy_events = {
+ .removed = core_removed,
+};
+
+static void impl_destroy(struct impl *impl)
+{
+ struct stream *s;
+
+ spa_list_consume(s, &impl->streams, link)
+ destroy_stream(s);
+
+ if (impl->combine)
+ pw_stream_destroy(impl->combine);
+
+ if (impl->registry) {
+ spa_hook_remove(&impl->registry_listener);
+ pw_proxy_destroy((struct pw_proxy*)impl->registry);
+ impl->registry = NULL;
+ }
+ if (impl->core) {
+ spa_hook_remove(&impl->core_listener);
+ if (impl->do_disconnect)
+ pw_core_disconnect(impl->core);
+ impl->core = NULL;
+ }
+
+ pw_properties_free(impl->stream_props);
+ pw_properties_free(impl->combine_props);
+ pw_properties_free(impl->props);
+
+ free(impl);
+}
+
+static void module_destroy(void *data)
+{
+ struct impl *impl = data;
+ spa_hook_remove(&impl->module_listener);
+ impl_destroy(impl);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy,
+};
+
+static void copy_props(const struct pw_properties *props, struct pw_properties *target,
+ const char *key)
+{
+ const char *str;
+ if ((str = pw_properties_get(props, key)) != NULL) {
+ if (pw_properties_get(target, key) == NULL)
+ pw_properties_set(target, key, str);
+ }
+}
+
+SPA_EXPORT
+int pipewire__module_init(struct pw_impl_module *module, const char *args)
+{
+ struct pw_context *context = pw_impl_module_get_context(module);
+ struct pw_properties *props = NULL;
+ uint32_t id = pw_global_get_id(pw_impl_module_get_global(module));
+ uint32_t pid = getpid();
+ struct impl *impl;
+ const char *str, *prefix;
+ int res;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ impl = calloc(1, sizeof(struct impl));
+ if (impl == NULL)
+ return -errno;
+
+ pw_log_debug("module %p: new %s", impl, args);
+ impl->data_loop = pw_context_get_data_loop(context);
+
+ spa_list_init(&impl->streams);
+
+ if (args == NULL)
+ args = "";
+
+ props = pw_properties_new_string(args);
+ if (props == NULL) {
+ res = -errno;
+ pw_log_error( "can't create properties: %m");
+ goto error;
+ }
+ impl->props = props;
+
+ if ((str = pw_properties_get(props, "combine.mode")) == NULL)
+ str = "sink";
+
+ if (spa_streq(str, "sink")) {
+ impl->mode = MODE_SINK;
+ prefix = "sink";
+ } else if (spa_streq(str, "capture")) {
+ impl->mode = MODE_CAPTURE;
+ prefix = "capture";
+ } else if (spa_streq(str, "source")) {
+ impl->mode = MODE_SOURCE;
+ prefix = "source";
+ } else if (spa_streq(str, "playback")) {
+ impl->mode = MODE_PLAYBACK;
+ prefix = "playback";
+ } else {
+ pw_log_warn("unknown combine.mode '%s', using 'sink'", str);
+ impl->mode = MODE_SINK;
+ prefix = "sink";
+ }
+
+ impl->combine_props = pw_properties_new(NULL, NULL);
+ impl->stream_props = pw_properties_new(NULL, NULL);
+ if (impl->combine_props == NULL || impl->stream_props == NULL) {
+ res = -errno;
+ pw_log_error( "can't create properties: %m");
+ goto error;
+ }
+
+ impl->module = module;
+ impl->context = context;
+
+ if (pw_properties_get(props, PW_KEY_NODE_GROUP) == NULL)
+ pw_properties_setf(props, PW_KEY_NODE_GROUP, "combine-%s-%u-%u",
+ prefix, pid, id);
+ if (pw_properties_get(props, PW_KEY_NODE_LINK_GROUP) == NULL)
+ pw_properties_setf(props, PW_KEY_NODE_LINK_GROUP, "combine-%s-%u-%u",
+ prefix, pid, id);
+ if (pw_properties_get(props, PW_KEY_NODE_VIRTUAL) == NULL)
+ pw_properties_set(props, PW_KEY_NODE_VIRTUAL, "true");
+ if (pw_properties_get(props, "resample.prefill") == NULL)
+ pw_properties_set(props, "resample.prefill", "true");
+
+ if (pw_properties_get(props, PW_KEY_MEDIA_CLASS) == NULL) {
+ if (impl->mode == MODE_SINK)
+ pw_properties_set(props, PW_KEY_MEDIA_CLASS, "Audio/Sink");
+ else if (impl->mode == MODE_SOURCE)
+ pw_properties_set(props, PW_KEY_MEDIA_CLASS, "Audio/Source");
+ }
+
+ if (pw_properties_get(props, PW_KEY_NODE_NAME) == NULL)
+ pw_properties_setf(props, PW_KEY_NODE_NAME, "combine-%s-%u-%u",
+ prefix, pid, id);
+ if (pw_properties_get(props, PW_KEY_NODE_DESCRIPTION) == NULL)
+ pw_properties_setf(props, PW_KEY_NODE_DESCRIPTION,
+ "Combine %s", prefix);
+
+ if ((str = pw_properties_get(props, "combine.props")) != NULL)
+ pw_properties_update_string(impl->combine_props, str, strlen(str));
+ if ((str = pw_properties_get(props, "stream.props")) != NULL)
+ pw_properties_update_string(impl->stream_props, str, strlen(str));
+
+ copy_props(props, impl->combine_props, PW_KEY_AUDIO_CHANNELS);
+ copy_props(props, impl->combine_props, SPA_KEY_AUDIO_POSITION);
+ copy_props(props, impl->combine_props, PW_KEY_NODE_NAME);
+ copy_props(props, impl->combine_props, PW_KEY_NODE_DESCRIPTION);
+ copy_props(props, impl->combine_props, PW_KEY_NODE_GROUP);
+ copy_props(props, impl->combine_props, PW_KEY_NODE_LINK_GROUP);
+ copy_props(props, impl->combine_props, PW_KEY_NODE_LATENCY);
+ copy_props(props, impl->combine_props, PW_KEY_NODE_VIRTUAL);
+ copy_props(props, impl->combine_props, PW_KEY_MEDIA_CLASS);
+ copy_props(props, impl->combine_props, "resample.prefill");
+
+ parse_audio_info(impl->combine_props, &impl->info);
+
+ copy_props(props, impl->stream_props, PW_KEY_NODE_GROUP);
+ copy_props(props, impl->stream_props, PW_KEY_NODE_VIRTUAL);
+ copy_props(props, impl->stream_props, PW_KEY_NODE_LINK_GROUP);
+ copy_props(props, impl->stream_props, "resample.prefill");
+
+ if (pw_properties_get(impl->stream_props, PW_KEY_MEDIA_ROLE) == NULL)
+ pw_properties_set(props, PW_KEY_MEDIA_ROLE, "filter");
+ if (pw_properties_get(impl->stream_props, PW_KEY_NODE_PASSIVE) == NULL)
+ pw_properties_set(impl->stream_props, PW_KEY_NODE_PASSIVE, "true");
+ if (pw_properties_get(impl->stream_props, PW_KEY_NODE_DONT_RECONNECT) == NULL)
+ pw_properties_set(impl->stream_props, PW_KEY_NODE_DONT_RECONNECT, "true");
+
+ impl->core = pw_context_get_object(impl->context, PW_TYPE_INTERFACE_Core);
+ if (impl->core == NULL) {
+ str = pw_properties_get(props, PW_KEY_REMOTE_NAME);
+ impl->core = pw_context_connect(impl->context,
+ pw_properties_new(
+ PW_KEY_REMOTE_NAME, str,
+ NULL),
+ 0);
+ impl->do_disconnect = true;
+ }
+ if (impl->core == NULL) {
+ res = -errno;
+ pw_log_error("can't connect: %m");
+ goto error;
+ }
+
+ pw_proxy_add_listener((struct pw_proxy*)impl->core,
+ &impl->core_proxy_listener,
+ &core_proxy_events, impl);
+ pw_core_add_listener(impl->core,
+ &impl->core_listener,
+ &core_events, impl);
+
+ if ((res = create_combine(impl)) < 0)
+ goto error;
+
+ impl->registry = pw_core_get_registry(impl->core, PW_VERSION_REGISTRY, 0);
+ pw_registry_add_listener(impl->registry, &impl->registry_listener,
+ &registry_events, impl);
+
+ pw_impl_module_add_listener(module, &impl->module_listener, &module_events, impl);
+
+ pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_props));
+
+ return 0;
+
+error:
+ impl_destroy(impl);
+ return res;
+}
diff --git a/src/modules/module-echo-cancel.c b/src/modules/module-echo-cancel.c
new file mode 100644
index 0000000..34c16c7
--- /dev/null
+++ b/src/modules/module-echo-cancel.c
@@ -0,0 +1,1350 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans
+ * © 2021 Arun Raghavan <arun@asymptotic.io>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "config.h"
+
+#include <errno.h>
+#include <fcntl.h>
+#include <getopt.h>
+#include <limits.h>
+#include <math.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#include <spa/debug/types.h>
+#include <spa/param/audio/format-utils.h>
+#include <spa/param/audio/raw.h>
+#include <spa/param/latency-utils.h>
+#include <spa/pod/builder.h>
+#include <spa/pod/dynamic.h>
+#include <spa/support/plugin.h>
+#include <spa/utils/json.h>
+#include <spa/utils/names.h>
+#include <spa/utils/result.h>
+#include <spa/utils/ringbuffer.h>
+#include <spa/utils/string.h>
+#include <spa/support/plugin-loader.h>
+#include <spa/interfaces/audio/aec.h>
+
+#include <pipewire/impl.h>
+#include <pipewire/pipewire.h>
+
+#include <pipewire/extensions/profiler.h>
+
+/** \page page_module_echo_cancel PipeWire Module: Echo Cancel
+ *
+ * The `echo-cancel` module performs echo cancellation. The module creates
+ * virtual `echo-cancel-capture` source and `echo-cancel-playback` sink
+ * nodes and the associated streams.
+ *
+ * ## Module Options
+ *
+ * Options specific to the behavior of this module
+ *
+ * - `capture.props = {}`: properties to be passed to the capture stream
+ * - `source.props = {}`: properties to be passed to the source stream
+ * - `sink.props = {}`: properties to be passed to the sink stream
+ * - `playback.props = {}`: properties to be passed to the playback stream
+ * - `library.name = <str>`: the echo cancellation library Currently supported:
+ * `aec/libspa-aec-webrtc`. Leave unset to use the default method (`aec/libspa-aec-webrtc`).
+ * - `aec.args = <str>`: arguments to pass to the echo cancellation method
+ * - `monitor.mode`: Instead of making a sink, make a stream that captures from
+ * the monitor ports of the default sink.
+ *
+ * ## General options
+ *
+ * Options with well-known behavior:
+ *
+ * - \ref PW_KEY_AUDIO_RATE
+ * - \ref PW_KEY_AUDIO_CHANNELS
+ * - \ref SPA_KEY_AUDIO_POSITION
+ * - \ref PW_KEY_MEDIA_CLASS
+ * - \ref PW_KEY_NODE_LATENCY
+ * - \ref PW_KEY_NODE_NAME
+ * - \ref PW_KEY_NODE_DESCRIPTION
+ * - \ref PW_KEY_NODE_GROUP
+ * - \ref PW_KEY_NODE_LINK_GROUP
+ * - \ref PW_KEY_NODE_VIRTUAL
+ * - \ref PW_KEY_NODE_LATENCY
+ * - \ref PW_KEY_REMOTE_NAME
+ *
+ * ## Example configuration
+ *\code{.unparsed}
+ * context.modules = [
+ * { name = libpipewire-module-echo-cancel
+ * args = {
+ * # library.name = aec/libspa-aec-webrtc
+ * # node.latency = 1024/48000
+ * # monitor.mode = false
+ * capture.props = {
+ * node.name = "Echo Cancellation Capture"
+ * }
+ * source.props = {
+ * node.name = "Echo Cancellation Source"
+ * }
+ * sink.props = {
+ * node.name = "Echo Cancellation Sink"
+ * }
+ * playback.props = {
+ * node.name = "Echo Cancellation Playback"
+ * }
+ * }
+ * }
+ *]
+ *\endcode
+ *
+ */
+
+/**
+ * .--------. .---------. .--------. .----------. .-------.
+ * | source | --> | capture | --> | | --> | source | --> | app |
+ * '--------' '---------' | echo | '----------' '-------'
+ * | cancel |
+ * .--------. .---------. | | .----------. .--------.
+ * | app | --> | sink | --> | | --> | playback | --> | sink |
+ * '--------' '---------' '--------' '----------' '--------'
+ */
+#define NAME "echo-cancel"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+#define DEFAULT_RATE 48000
+#define DEFAULT_CHANNELS 2
+#define DEFAULT_POSITION "[ FL FR ]"
+
+/* Hopefully this is enough for any combination of AEC engine and resampler
+ * input requirement for rate matching */
+#define MAX_BUFSIZE_MS 100
+#define DELAY_MS 0
+
+static const struct spa_dict_item module_props[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Echo Cancellation" },
+ { PW_KEY_MODULE_USAGE, " [ remote.name=<remote> ] "
+ "[ node.latency=<latency as fraction> ] "
+ "[ audio.rate=<sample rate> ] "
+ "[ audio.channels=<number of channels> ] "
+ "[ audio.position=<channel map> ] "
+ "[ buffer.max_size=<max buffer size in ms> ] "
+ "[ buffer.play_delay=<delay as fraction> ] "
+ "[ library.name =<library name> ] "
+ "[ aec.args=<aec arguments> ] "
+ "[ capture.props=<properties> ] "
+ "[ source.props=<properties> ] "
+ "[ sink.props=<properties> ] "
+ "[ playback.props=<properties> ] " },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+struct impl {
+ struct pw_context *context;
+
+ struct pw_impl_module *module;
+ struct spa_hook module_listener;
+
+ struct pw_core *core;
+ struct spa_hook core_proxy_listener;
+ struct spa_hook core_listener;
+
+ struct spa_audio_info_raw info;
+
+ struct pw_properties *capture_props;
+ struct pw_stream *capture;
+ struct spa_hook capture_listener;
+ struct spa_audio_info_raw capture_info;
+
+ struct pw_properties *source_props;
+ struct pw_stream *source;
+ struct spa_hook source_listener;
+ struct spa_audio_info_raw source_info;
+
+ void *rec_buffer[SPA_AUDIO_MAX_CHANNELS];
+ uint32_t rec_ringsize;
+ struct spa_ringbuffer rec_ring;
+
+ struct pw_properties *playback_props;
+ struct pw_stream *playback;
+ struct spa_hook playback_listener;
+ struct spa_audio_info_raw playback_info;
+
+ struct pw_properties *sink_props;
+ struct pw_stream *sink;
+ struct spa_hook sink_listener;
+ void *play_buffer[SPA_AUDIO_MAX_CHANNELS];
+ uint32_t play_ringsize;
+ struct spa_ringbuffer play_ring;
+ struct spa_ringbuffer play_delayed_ring;
+ struct spa_audio_info_raw sink_info;
+
+ void *out_buffer[SPA_AUDIO_MAX_CHANNELS];
+ uint32_t out_ringsize;
+ struct spa_ringbuffer out_ring;
+
+ struct spa_audio_aec *aec;
+ uint32_t aec_blocksize;
+
+ unsigned int capture_ready:1;
+ unsigned int sink_ready:1;
+
+ unsigned int do_disconnect:1;
+
+ uint32_t max_buffer_size;
+ uint32_t buffer_delay;
+ uint32_t current_delay;
+
+ struct spa_handle *spa_handle;
+ struct spa_plugin_loader *loader;
+
+ bool monitor_mode;
+};
+
+static void process(struct impl *impl)
+{
+ struct pw_buffer *cout;
+ struct pw_buffer *pout = NULL;
+ float rec_buf[impl->info.channels][impl->aec_blocksize / sizeof(float)];
+ float play_buf[impl->info.channels][impl->aec_blocksize / sizeof(float)];
+ float play_delayed_buf[impl->info.channels][impl->aec_blocksize / sizeof(float)];
+ float out_buf[impl->info.channels][impl->aec_blocksize / sizeof(float)];
+ const float *rec[impl->info.channels];
+ const float *play[impl->info.channels];
+ const float *play_delayed[impl->info.channels];
+ float *out[impl->info.channels];
+ struct spa_data *dd;
+ uint32_t i, size;
+ uint32_t rindex, pindex, oindex, pdindex, avail;
+ int32_t stride = 0;
+
+ if (impl->playback != NULL && (pout = pw_stream_dequeue_buffer(impl->playback)) == NULL) {
+ pw_log_debug("out of playback buffers: %m");
+ goto done;
+ }
+
+ size = impl->aec_blocksize;
+
+ /* First read a block from the playback and capture ring buffers */
+
+ spa_ringbuffer_get_read_index(&impl->rec_ring, &rindex);
+ spa_ringbuffer_get_read_index(&impl->play_ring, &pindex);
+ spa_ringbuffer_get_read_index(&impl->play_delayed_ring, &pdindex);
+
+ for (i = 0; i < impl->info.channels; i++) {
+ /* captured samples, with echo from sink */
+ rec[i] = &rec_buf[i][0];
+ /* echo from sink */
+ play[i] = &play_buf[i][0];
+ /* echo from sink delayed */
+ play_delayed[i] = &play_delayed_buf[i][0];
+ /* filtered samples, without echo from sink */
+ out[i] = &out_buf[i][0];
+
+ stride = 0;
+ spa_ringbuffer_read_data(&impl->rec_ring, impl->rec_buffer[i],
+ impl->rec_ringsize, rindex % impl->rec_ringsize,
+ (void*)rec[i], size);
+
+ stride = 0;
+ spa_ringbuffer_read_data(&impl->play_ring, impl->play_buffer[i],
+ impl->play_ringsize, pindex % impl->play_ringsize,
+ (void *)play[i], size);
+
+ stride = 0;
+ spa_ringbuffer_read_data(&impl->play_delayed_ring, impl->play_buffer[i],
+ impl->play_ringsize, pdindex % impl->play_ringsize,
+ (void *)play_delayed[i], size);
+
+ if (pout != NULL) {
+ /* output to sink, just copy */
+ dd = &pout->buffer->datas[i];
+ memcpy(dd->data, play[i], size);
+
+ dd->chunk->offset = 0;
+ dd->chunk->size = size;
+ dd->chunk->stride = stride;
+ }
+ }
+
+ spa_ringbuffer_read_update(&impl->rec_ring, rindex + size);
+ spa_ringbuffer_read_update(&impl->play_ring, pindex + size);
+ spa_ringbuffer_read_update(&impl->play_delayed_ring, pdindex + size);
+
+ if (impl->playback != NULL)
+ pw_stream_queue_buffer(impl->playback, pout);
+
+ if (SPA_UNLIKELY (impl->current_delay < impl->buffer_delay)) {
+ uint32_t delay_left = impl->buffer_delay - impl->current_delay;
+ uint32_t silence_size;
+
+ /* don't run the canceller until play_buffer has been filled,
+ * copy silence to output in the meantime */
+ silence_size = SPA_MIN(size, delay_left * sizeof(float));
+ for (i = 0; i < impl->info.channels; i++)
+ memset(out[i], 0, silence_size);
+ impl->current_delay += silence_size / sizeof(float);
+ pw_log_debug("current_delay %d", impl->current_delay);
+
+ if (silence_size != size) {
+ const float *pd[impl->info.channels];
+ float *o[impl->info.channels];
+
+ for (i = 0; i < impl->info.channels; i++) {
+ pd[i] = play_delayed[i] + delay_left;
+ o[i] = out[i] + delay_left;
+ }
+ spa_audio_aec_run(impl->aec, rec, pd, o, size / sizeof(float) - delay_left);
+ }
+ } else {
+ /* run the canceller */
+ spa_audio_aec_run(impl->aec, rec, play_delayed, out, size / sizeof(float));
+ }
+
+ /* Next, copy over the output to the output ringbuffer */
+ avail = spa_ringbuffer_get_write_index(&impl->out_ring, &oindex);
+ if (avail + size > impl->out_ringsize) {
+ uint32_t rindex, drop;
+
+ /* Drop enough so we have size bytes left */
+ drop = avail + size - impl->out_ringsize;
+ pw_log_debug("output ringbuffer xrun %d + %u > %u, dropping %u",
+ avail, size, impl->out_ringsize, drop);
+
+ spa_ringbuffer_get_read_index(&impl->out_ring, &rindex);
+ spa_ringbuffer_read_update(&impl->out_ring, rindex + drop);
+
+ avail += drop;
+ }
+
+ for (i = 0; i < impl->info.channels; i++) {
+ /* captured samples, with echo from sink */
+ spa_ringbuffer_write_data(&impl->out_ring, impl->out_buffer[i],
+ impl->out_ringsize, oindex % impl->out_ringsize,
+ (void *)out[i], size);
+ }
+
+ spa_ringbuffer_write_update(&impl->out_ring, oindex + size);
+
+ /* And finally take data from the output ringbuffer and make it
+ * available on the source */
+
+ avail = spa_ringbuffer_get_read_index(&impl->out_ring, &oindex);
+ while (avail >= size) {
+ if ((cout = pw_stream_dequeue_buffer(impl->source)) == NULL) {
+ pw_log_debug("out of source buffers: %m");
+ break;
+ }
+
+ for (i = 0; i < impl->info.channels; i++) {
+ dd = &cout->buffer->datas[i];
+ spa_ringbuffer_read_data(&impl->out_ring, impl->out_buffer[i],
+ impl->out_ringsize, oindex % impl->out_ringsize,
+ (void *)dd->data, size);
+ dd->chunk->offset = 0;
+ dd->chunk->size = size;
+ dd->chunk->stride = 0;
+ }
+
+ pw_stream_queue_buffer(impl->source, cout);
+
+ oindex += size;
+ spa_ringbuffer_read_update(&impl->out_ring, oindex);
+ avail -= size;
+ }
+
+done:
+ impl->sink_ready = false;
+ impl->capture_ready = false;
+}
+
+static void capture_destroy(void *d)
+{
+ struct impl *impl = d;
+ spa_hook_remove(&impl->capture_listener);
+ impl->capture = NULL;
+}
+
+static void capture_process(void *data)
+{
+ struct impl *impl = data;
+ struct pw_buffer *buf;
+ struct spa_data *d;
+ uint32_t i, index, offs, size;
+ int32_t avail;
+
+ if ((buf = pw_stream_dequeue_buffer(impl->capture)) == NULL) {
+ pw_log_debug("out of capture buffers: %m");
+ return;
+ }
+ d = &buf->buffer->datas[0];
+
+ offs = SPA_MIN(d->chunk->offset, d->maxsize);
+ size = SPA_MIN(d->chunk->size, d->maxsize - offs);
+
+ avail = spa_ringbuffer_get_write_index(&impl->rec_ring, &index);
+
+ if (avail + size > impl->rec_ringsize) {
+ uint32_t rindex, drop;
+
+ /* Drop enough so we have size bytes left */
+ drop = avail + size - impl->rec_ringsize;
+ pw_log_debug("capture ringbuffer xrun %d + %u > %u, dropping %u",
+ avail, size, impl->rec_ringsize, drop);
+
+ spa_ringbuffer_get_read_index(&impl->rec_ring, &rindex);
+ spa_ringbuffer_read_update(&impl->rec_ring, rindex + drop);
+
+ avail += drop;
+ }
+
+ /* If we don't know what size to push yet, use the canceller blocksize
+ * if it has a specific requirement, else keep the block size the same
+ * on input and output or what the resampler needs */
+ if (impl->aec_blocksize == 0) {
+ impl->aec_blocksize = size;
+ pw_log_debug("Setting AEC block size to %u", impl->aec_blocksize);
+ }
+
+ for (i = 0; i < impl->info.channels; i++) {
+ /* captured samples, with echo from sink */
+ d = &buf->buffer->datas[i];
+
+ offs = SPA_MIN(d->chunk->offset, d->maxsize);
+ size = SPA_MIN(d->chunk->size, d->maxsize - offs);
+
+ spa_ringbuffer_write_data(&impl->rec_ring, impl->rec_buffer[i],
+ impl->rec_ringsize, index % impl->rec_ringsize,
+ SPA_PTROFF(d->data, offs, void), size);
+ }
+
+ spa_ringbuffer_write_update(&impl->rec_ring, index + size);
+
+ if (avail + size >= impl->aec_blocksize) {
+ impl->capture_ready = true;
+ if (impl->sink_ready)
+ process(impl);
+ }
+
+ pw_stream_queue_buffer(impl->capture, buf);
+}
+
+static void capture_state_changed(void *data, enum pw_stream_state old,
+ enum pw_stream_state state, const char *error)
+{
+ struct impl *impl = data;
+ switch (state) {
+ case PW_STREAM_STATE_PAUSED:
+ pw_stream_flush(impl->source, false);
+ pw_stream_flush(impl->capture, false);
+ break;
+ case PW_STREAM_STATE_UNCONNECTED:
+ pw_log_info("%p: input unconnected", impl);
+ pw_impl_module_schedule_destroy(impl->module);
+ break;
+ case PW_STREAM_STATE_ERROR:
+ pw_log_info("%p: input error: %s", impl, error);
+ break;
+ default:
+ break;
+ }
+}
+
+static void source_state_changed(void *data, enum pw_stream_state old,
+ enum pw_stream_state state, const char *error)
+{
+ struct impl *impl = data;
+ int res;
+
+ switch (state) {
+ case PW_STREAM_STATE_PAUSED:
+ pw_stream_flush(impl->source, false);
+ pw_stream_flush(impl->capture, false);
+
+ if (old == PW_STREAM_STATE_STREAMING) {
+ pw_log_debug("%p: deactivate %s", impl, impl->aec->name);
+ res = spa_audio_aec_deactivate(impl->aec);
+ if (res < 0 && res != -EOPNOTSUPP) {
+ pw_log_error("aec plugin %s deactivate failed: %s", impl->aec->name, spa_strerror(res));
+ }
+ }
+ break;
+ case PW_STREAM_STATE_STREAMING:
+ pw_log_debug("%p: activate %s", impl, impl->aec->name);
+ res = spa_audio_aec_activate(impl->aec);
+ if (res < 0 && res != -EOPNOTSUPP) {
+ pw_log_error("aec plugin %s activate failed: %s", impl->aec->name, spa_strerror(res));
+ }
+ break;
+ case PW_STREAM_STATE_UNCONNECTED:
+ pw_log_info("%p: input unconnected", impl);
+ pw_impl_module_schedule_destroy(impl->module);
+ break;
+ case PW_STREAM_STATE_ERROR:
+ pw_log_info("%p: input error: %s", impl, error);
+ break;
+ default:
+ break;
+ }
+}
+
+static void input_param_latency_changed(struct impl *impl, const struct spa_pod *param)
+{
+ struct spa_latency_info latency;
+ uint8_t buffer[1024];
+ struct spa_pod_builder b;
+ const struct spa_pod *params[1];
+
+ if (spa_latency_parse(param, &latency) < 0)
+ return;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ params[0] = spa_latency_build(&b, SPA_PARAM_Latency, &latency);
+
+ if (latency.direction == SPA_DIRECTION_INPUT)
+ pw_stream_update_params(impl->source, params, 1);
+ else
+ pw_stream_update_params(impl->capture, params, 1);
+}
+static struct spa_pod* get_props_param(struct impl* impl, struct spa_pod_builder* b)
+{
+ if (spa_audio_aec_get_params(impl->aec, NULL) > 0) {
+ struct spa_pod_frame f[2];
+ spa_pod_builder_push_object(
+ b, &f[0], SPA_TYPE_OBJECT_Props, SPA_PARAM_Props);
+ spa_pod_builder_prop(b, SPA_PROP_params, 0);
+ spa_pod_builder_push_struct(b, &f[1]);
+
+ spa_audio_aec_get_params(impl->aec, b);
+
+ spa_pod_builder_pop(b, &f[1]);
+ return spa_pod_builder_pop(b, &f[0]);
+ }
+ return NULL;
+}
+
+static void input_param_changed(void *data, uint32_t id, const struct spa_pod* param)
+{
+ struct spa_pod_object* obj = (struct spa_pod_object*)param;
+ const struct spa_pod_prop* prop;
+ struct impl* impl = data;
+ switch (id) {
+ case SPA_PARAM_Latency:
+ input_param_latency_changed(impl, param);
+ break;
+ case SPA_PARAM_Props:
+ if (param != NULL) {
+ uint8_t buffer[1024];
+ struct spa_pod_dynamic_builder b;
+ const struct spa_pod* params[1];
+ SPA_POD_OBJECT_FOREACH(obj, prop)
+ {
+ if (prop->key == SPA_PROP_params) {
+ spa_audio_aec_set_params(impl->aec, &prop->value);
+ }
+ }
+
+ spa_pod_dynamic_builder_init(&b, buffer, sizeof(buffer), 4096);
+ params[0] = get_props_param(impl, &b.b);
+ if (params[0]) {
+ pw_stream_update_params(impl->capture, params, 1);
+ pw_stream_update_params(impl->playback, params, 1);
+ }
+ spa_pod_dynamic_builder_clean(&b);
+ } else {
+ pw_log_warn("param is null");
+ }
+ break;
+ }
+}
+
+static const struct pw_stream_events capture_events = {
+ PW_VERSION_STREAM_EVENTS,
+ .destroy = capture_destroy,
+ .state_changed = capture_state_changed,
+ .process = capture_process,
+ .param_changed = input_param_changed
+};
+
+static void source_destroy(void *d)
+{
+ struct impl *impl = d;
+ spa_hook_remove(&impl->source_listener);
+ impl->source = NULL;
+}
+
+static const struct pw_stream_events source_events = {
+ PW_VERSION_STREAM_EVENTS,
+ .destroy = source_destroy,
+ .state_changed = source_state_changed,
+ .param_changed = input_param_changed
+};
+
+static void output_state_changed(void *data, enum pw_stream_state old,
+ enum pw_stream_state state, const char *error)
+{
+ struct impl *impl = data;
+ switch (state) {
+ case PW_STREAM_STATE_PAUSED:
+ pw_stream_flush(impl->sink, false);
+ if (impl->playback != NULL)
+ pw_stream_flush(impl->playback, false);
+ if (old == PW_STREAM_STATE_STREAMING) {
+ impl->current_delay = 0;
+ }
+ break;
+ case PW_STREAM_STATE_UNCONNECTED:
+ pw_log_info("%p: output unconnected", impl);
+ pw_impl_module_schedule_destroy(impl->module);
+ break;
+ case PW_STREAM_STATE_ERROR:
+ pw_log_info("%p: output error: %s", impl, error);
+ break;
+ default:
+ break;
+ }
+}
+
+static void output_param_latency_changed(struct impl *impl, const struct spa_pod *param)
+{
+ struct spa_latency_info latency;
+ uint8_t buffer[1024];
+ struct spa_pod_builder b;
+ const struct spa_pod *params[1];
+
+ if (spa_latency_parse(param, &latency) < 0)
+ return;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ params[0] = spa_latency_build(&b, SPA_PARAM_Latency, &latency);
+
+ if (latency.direction == SPA_DIRECTION_INPUT)
+ pw_stream_update_params(impl->sink, params, 1);
+ else if (impl->playback != NULL)
+ pw_stream_update_params(impl->playback, params, 1);
+}
+
+static void output_param_changed(void *data, uint32_t id, const struct spa_pod *param)
+{
+ struct spa_pod_object *obj = (struct spa_pod_object *) param;
+ const struct spa_pod_prop *prop;
+ struct impl *impl = data;
+ switch (id) {
+ case SPA_PARAM_Latency:
+ output_param_latency_changed(impl, param);
+ break;
+ case SPA_PARAM_Props:
+ if (param != NULL) {
+ uint8_t buffer[1024];
+ struct spa_pod_dynamic_builder b;
+ const struct spa_pod* params[1];
+
+ SPA_POD_OBJECT_FOREACH(obj, prop)
+ {
+ if (prop->key == SPA_PROP_params) {
+ spa_audio_aec_set_params(impl->aec, &prop->value);
+ }
+ }
+ spa_pod_dynamic_builder_init(&b, buffer, sizeof(buffer), 4096);
+ params[0] = get_props_param(impl, &b.b);
+ if (params[0] != NULL) {
+ pw_stream_update_params(impl->capture, params, 1);
+ pw_stream_update_params(impl->playback, params, 1);
+ }
+ spa_pod_dynamic_builder_clean(&b);
+ }
+ break;
+ }
+}
+
+static void sink_destroy(void *d)
+{
+ struct impl *impl = d;
+ spa_hook_remove(&impl->sink_listener);
+ impl->sink = NULL;
+}
+
+static void sink_process(void *data)
+{
+ struct impl *impl = data;
+ struct pw_buffer *buf;
+ struct spa_data *d;
+ uint32_t i, index, offs, size;
+ int32_t avail;
+
+ if ((buf = pw_stream_dequeue_buffer(impl->sink)) == NULL) {
+ pw_log_debug("out of sink buffers: %m");
+ return;
+ }
+ d = &buf->buffer->datas[0];
+
+ offs = SPA_MIN(d->chunk->offset, d->maxsize);
+ size = SPA_MIN(d->chunk->size, d->maxsize - offs);
+
+ avail = spa_ringbuffer_get_write_index(&impl->play_ring, &index);
+
+ if (avail + size > impl->play_ringsize) {
+ uint32_t rindex, drop;
+
+ /* Drop enough so we have size bytes left */
+ drop = avail + size - impl->play_ringsize;
+ pw_log_debug("sink ringbuffer xrun %d + %u > %u, dropping %u",
+ avail, size, impl->play_ringsize, drop);
+
+ spa_ringbuffer_get_read_index(&impl->play_ring, &rindex);
+ spa_ringbuffer_read_update(&impl->play_ring, rindex + drop);
+
+ spa_ringbuffer_get_read_index(&impl->play_delayed_ring, &rindex);
+ spa_ringbuffer_read_update(&impl->play_delayed_ring, rindex + drop);
+
+ avail += drop;
+ }
+
+ if (impl->aec_blocksize == 0) {
+ impl->aec_blocksize = size;
+ pw_log_debug("Setting AEC block size to %u", impl->aec_blocksize);
+ }
+
+ for (i = 0; i < impl->info.channels; i++) {
+ /* echo from sink */
+ d = &buf->buffer->datas[i];
+
+ offs = SPA_MIN(d->chunk->offset, d->maxsize);
+ size = SPA_MIN(d->chunk->size, d->maxsize - offs);
+
+ spa_ringbuffer_write_data(&impl->play_ring, impl->play_buffer[i],
+ impl->play_ringsize, index % impl->play_ringsize,
+ SPA_PTROFF(d->data, offs, void), size);
+ }
+ spa_ringbuffer_write_update(&impl->play_ring, index + size);
+
+ if (avail + size >= impl->aec_blocksize) {
+ impl->sink_ready = true;
+ if (impl->capture_ready)
+ process(impl);
+ }
+
+ pw_stream_queue_buffer(impl->sink, buf);
+}
+
+static void playback_destroy(void *d)
+{
+ struct impl *impl = d;
+ if (impl->playback != NULL) {
+ spa_hook_remove(&impl->playback_listener);
+ impl->playback = NULL;
+ }
+}
+
+static const struct pw_stream_events playback_events = {
+ PW_VERSION_STREAM_EVENTS,
+ .destroy = playback_destroy,
+ .state_changed = output_state_changed,
+ .param_changed = output_param_changed
+};
+static const struct pw_stream_events sink_events = {
+ PW_VERSION_STREAM_EVENTS,
+ .destroy = sink_destroy,
+ .process = sink_process,
+ .state_changed = output_state_changed,
+ .param_changed = output_param_changed
+};
+
+static int setup_streams(struct impl *impl)
+{
+ int res;
+ uint32_t n_params, i;
+ uint32_t offsets[512];
+ const struct spa_pod *params[512];
+ struct spa_pod_dynamic_builder b;
+ uint32_t index;
+
+ impl->capture = pw_stream_new(impl->core,
+ "Echo-Cancel Capture", impl->capture_props);
+ impl->capture_props = NULL;
+ if (impl->capture == NULL)
+ return -errno;
+
+ pw_stream_add_listener(impl->capture,
+ &impl->capture_listener,
+ &capture_events, impl);
+
+ impl->source = pw_stream_new(impl->core,
+ "Echo-Cancel Source", impl->source_props);
+ impl->source_props = NULL;
+ if (impl->source == NULL)
+ return -errno;
+
+ pw_stream_add_listener(impl->source,
+ &impl->source_listener,
+ &source_events, impl);
+
+ if (impl->monitor_mode) {
+ impl->playback = NULL;
+ } else {
+ impl->playback = pw_stream_new(impl->core,
+ "Echo-Cancel Playback", impl->playback_props);
+ impl->playback_props = NULL;
+ if (impl->playback == NULL)
+ return -errno;
+
+ pw_stream_add_listener(impl->playback,
+ &impl->playback_listener,
+ &playback_events, impl);
+ }
+
+ impl->sink = pw_stream_new(impl->core,
+ "Echo-Cancel Sink", impl->sink_props);
+ impl->sink_props = NULL;
+ if (impl->sink == NULL)
+ return -errno;
+
+ pw_stream_add_listener(impl->sink,
+ &impl->sink_listener,
+ &sink_events, impl);
+
+ n_params = 0;
+ spa_pod_dynamic_builder_init(&b, NULL, 0, 4096);
+
+ offsets[n_params++] = b.b.state.offset;
+ spa_format_audio_raw_build(&b.b, SPA_PARAM_EnumFormat, &impl->capture_info);
+
+ int nbr_of_external_props = spa_audio_aec_enum_props(impl->aec, 0, NULL);
+ if (nbr_of_external_props > 0) {
+ for (int i = 0; i < nbr_of_external_props; i++) {
+ offsets[n_params++] = b.b.state.offset;
+ spa_audio_aec_enum_props(impl->aec, i, &b.b);
+ }
+ get_props_param(impl, &b.b);
+ }
+
+ for (i = 0; i < n_params; i++)
+ params[i] = spa_pod_builder_deref(&b.b, offsets[i]);
+
+ if ((res = pw_stream_connect(impl->capture,
+ PW_DIRECTION_INPUT,
+ PW_ID_ANY,
+ PW_STREAM_FLAG_AUTOCONNECT |
+ PW_STREAM_FLAG_MAP_BUFFERS |
+ PW_STREAM_FLAG_RT_PROCESS,
+ params, n_params)) < 0) {
+ spa_pod_dynamic_builder_clean(&b);
+ return res;
+ }
+
+ offsets[0] = b.b.state.offset;
+ spa_format_audio_raw_build(&b.b, SPA_PARAM_EnumFormat, &impl->source_info);
+
+ for (i = 0; i < n_params; i++)
+ params[i] = spa_pod_builder_deref(&b.b, offsets[i]);
+
+ if ((res = pw_stream_connect(impl->source,
+ PW_DIRECTION_OUTPUT,
+ PW_ID_ANY,
+ PW_STREAM_FLAG_MAP_BUFFERS |
+ PW_STREAM_FLAG_RT_PROCESS,
+ params, n_params)) < 0) {
+ spa_pod_dynamic_builder_clean(&b);
+ return res;
+ }
+
+ offsets[0] = b.b.state.offset;
+ spa_format_audio_raw_build(&b.b, SPA_PARAM_EnumFormat, &impl->sink_info);
+
+ for (i = 0; i < n_params; i++)
+ params[i] = spa_pod_builder_deref(&b.b, offsets[i]);
+
+ if ((res = pw_stream_connect(impl->sink,
+ PW_DIRECTION_INPUT,
+ PW_ID_ANY,
+ impl->playback != NULL ?
+ PW_STREAM_FLAG_MAP_BUFFERS | PW_STREAM_FLAG_RT_PROCESS :
+ PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS | PW_STREAM_FLAG_RT_PROCESS,
+ params, n_params)) < 0) {
+ spa_pod_dynamic_builder_clean(&b);
+ return res;
+ }
+
+ offsets[0] = b.b.state.offset;
+ spa_format_audio_raw_build(&b.b, SPA_PARAM_EnumFormat, &impl->playback_info);
+
+ for (i = 0; i < n_params; i++)
+ params[i] = spa_pod_builder_deref(&b.b, offsets[i]);
+
+ if (impl->playback != NULL && (res = pw_stream_connect(impl->playback,
+ PW_DIRECTION_OUTPUT,
+ PW_ID_ANY,
+ PW_STREAM_FLAG_AUTOCONNECT |
+ PW_STREAM_FLAG_MAP_BUFFERS |
+ PW_STREAM_FLAG_RT_PROCESS,
+ params, n_params)) < 0) {
+ spa_pod_dynamic_builder_clean(&b);
+ return res;
+ }
+
+ spa_pod_dynamic_builder_clean(&b);
+
+ impl->rec_ringsize = sizeof(float) * impl->max_buffer_size * impl->info.rate / 1000;
+ impl->play_ringsize = sizeof(float) * ((impl->max_buffer_size * impl->info.rate / 1000) + impl->buffer_delay);
+ impl->out_ringsize = sizeof(float) * impl->max_buffer_size * impl->info.rate / 1000;
+ for (i = 0; i < impl->info.channels; i++) {
+ impl->rec_buffer[i] = malloc(impl->rec_ringsize);
+ impl->play_buffer[i] = malloc(impl->play_ringsize);
+ impl->out_buffer[i] = malloc(impl->out_ringsize);
+ }
+ spa_ringbuffer_init(&impl->rec_ring);
+ spa_ringbuffer_init(&impl->play_ring);
+ spa_ringbuffer_init(&impl->play_delayed_ring);
+ spa_ringbuffer_init(&impl->out_ring);
+
+ spa_ringbuffer_get_write_index(&impl->play_ring, &index);
+ spa_ringbuffer_write_update(&impl->play_ring, index + (sizeof(float) * (impl->buffer_delay)));
+ spa_ringbuffer_get_read_index(&impl->play_ring, &index);
+ spa_ringbuffer_read_update(&impl->play_ring, index + (sizeof(float) * (impl->buffer_delay)));
+
+ return 0;
+}
+
+static void core_error(void *data, uint32_t id, int seq, int res, const char *message)
+{
+ struct impl *impl = data;
+
+ if (res == -ENOENT) {
+ pw_log_info("id:%u seq:%d res:%d (%s): %s",
+ id, seq, res, spa_strerror(res), message);
+ } else {
+ pw_log_warn("error id:%u seq:%d res:%d (%s): %s",
+ id, seq, res, spa_strerror(res), message);
+ }
+
+ if (id == PW_ID_CORE && res == -EPIPE)
+ pw_impl_module_schedule_destroy(impl->module);
+}
+
+static const struct pw_core_events core_events = {
+ PW_VERSION_CORE_EVENTS,
+ .error = core_error,
+};
+
+static void core_destroy(void *d)
+{
+ struct impl *impl = d;
+ spa_hook_remove(&impl->core_listener);
+ impl->core = NULL;
+ pw_impl_module_schedule_destroy(impl->module);
+}
+
+static const struct pw_proxy_events core_proxy_events = {
+ .destroy = core_destroy,
+};
+
+static void impl_destroy(struct impl *impl)
+{
+ uint32_t i;
+ if (impl->capture)
+ pw_stream_destroy(impl->capture);
+ if (impl->source)
+ pw_stream_destroy(impl->source);
+ if (impl->playback)
+ pw_stream_destroy(impl->playback);
+ if (impl->sink)
+ pw_stream_destroy(impl->sink);
+ if (impl->core && impl->do_disconnect)
+ pw_core_disconnect(impl->core);
+ if (impl->spa_handle)
+ spa_plugin_loader_unload(impl->loader, impl->spa_handle);
+ pw_properties_free(impl->capture_props);
+ pw_properties_free(impl->source_props);
+ pw_properties_free(impl->playback_props);
+ pw_properties_free(impl->sink_props);
+
+ for (i = 0; i < impl->info.channels; i++) {
+ if (impl->rec_buffer[i])
+ free(impl->rec_buffer[i]);
+ if (impl->play_buffer[i])
+ free(impl->play_buffer[i]);
+ if (impl->out_buffer[i])
+ free(impl->out_buffer[i]);
+ }
+
+ free(impl);
+}
+
+static void module_destroy(void *data)
+{
+ struct impl *impl = data;
+ spa_hook_remove(&impl->module_listener);
+ impl_destroy(impl);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy,
+};
+
+static uint32_t channel_from_name(const char *name)
+{
+ int i;
+ for (i = 0; spa_type_audio_channel[i].name; i++) {
+ if (spa_streq(name, spa_debug_type_short_name(spa_type_audio_channel[i].name)))
+ return spa_type_audio_channel[i].type;
+ }
+ return SPA_AUDIO_CHANNEL_UNKNOWN;
+}
+
+static void parse_position(struct spa_audio_info_raw *info, const char *val, size_t len)
+{
+ struct spa_json it[2];
+ char v[256];
+
+ spa_json_init(&it[0], val, len);
+ if (spa_json_enter_array(&it[0], &it[1]) <= 0)
+ spa_json_init(&it[1], val, len);
+
+ info->channels = 0;
+ while (spa_json_get_string(&it[1], v, sizeof(v)) > 0 &&
+ info->channels < SPA_AUDIO_MAX_CHANNELS) {
+ info->position[info->channels++] = channel_from_name(v);
+ }
+}
+
+static void parse_audio_info(struct pw_properties *props, struct spa_audio_info_raw *info)
+{
+ const char *str;
+
+ *info = SPA_AUDIO_INFO_RAW_INIT(
+ .format = SPA_AUDIO_FORMAT_F32P);
+ info->rate = pw_properties_get_uint32(props, PW_KEY_AUDIO_RATE, info->rate);
+ if (info->rate == 0)
+ info->rate = DEFAULT_RATE;
+
+ info->channels = pw_properties_get_uint32(props, PW_KEY_AUDIO_CHANNELS, info->channels);
+ info->channels = SPA_MIN(info->channels, SPA_AUDIO_MAX_CHANNELS);
+ if ((str = pw_properties_get(props, SPA_KEY_AUDIO_POSITION)) != NULL)
+ parse_position(info, str, strlen(str));
+ if (info->channels == 0)
+ parse_position(info, DEFAULT_POSITION, strlen(DEFAULT_POSITION));
+}
+
+static void copy_props(struct impl *impl, struct pw_properties *props, const char *key)
+{
+ const char *str;
+ if ((str = pw_properties_get(props, key)) != NULL) {
+ if (pw_properties_get(impl->capture_props, key) == NULL)
+ pw_properties_set(impl->capture_props, key, str);
+ if (pw_properties_get(impl->source_props, key) == NULL)
+ pw_properties_set(impl->source_props, key, str);
+ if (pw_properties_get(impl->playback_props, key) == NULL)
+ pw_properties_set(impl->playback_props, key, str);
+ if (pw_properties_get(impl->sink_props, key) == NULL)
+ pw_properties_set(impl->sink_props, key, str);
+ }
+}
+
+SPA_EXPORT
+int pipewire__module_init(struct pw_impl_module *module, const char *args)
+{
+ struct pw_context *context = pw_impl_module_get_context(module);
+ struct pw_properties *props, *aec_props;
+ struct impl *impl;
+ uint32_t id = pw_global_get_id(pw_impl_module_get_global(module));
+ uint32_t pid = getpid();
+ const char *str;
+ const char *path;
+ int res = 0;
+ struct spa_handle *handle = NULL;
+ void *iface;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ impl = calloc(1, sizeof(struct impl));
+ if (impl == NULL)
+ return -errno;
+
+ pw_log_debug("module %p: new %s", impl, args);
+
+ if (args)
+ props = pw_properties_new_string(args);
+ else
+ props = pw_properties_new(NULL, NULL);
+ if (props == NULL) {
+ res = -errno;
+ pw_log_error( "can't create properties: %m");
+ goto error;
+ }
+
+ impl->capture_props = pw_properties_new(NULL, NULL);
+ impl->source_props = pw_properties_new(NULL, NULL);
+ impl->playback_props = pw_properties_new(NULL, NULL);
+ impl->sink_props = pw_properties_new(NULL, NULL);
+ if (impl->source_props == NULL || impl->sink_props == NULL ||
+ impl->capture_props == NULL || impl->playback_props == NULL) {
+ res = -errno;
+ pw_log_error( "can't create properties: %m");
+ goto error;
+ }
+
+ impl->monitor_mode = false;
+ if ((str = pw_properties_get(props, "monitor.mode")) != NULL)
+ impl->monitor_mode = pw_properties_parse_bool(str);
+
+ impl->module = module;
+ impl->context = context;
+
+ if (pw_properties_get(props, PW_KEY_NODE_GROUP) == NULL)
+ pw_properties_setf(props, PW_KEY_NODE_GROUP, "echo-cancel-%u-%u", pid, id);
+ if (pw_properties_get(props, PW_KEY_NODE_LINK_GROUP) == NULL)
+ pw_properties_setf(props, PW_KEY_NODE_LINK_GROUP, "echo-cancel-%u-%u", pid, id);
+ if (pw_properties_get(props, PW_KEY_NODE_VIRTUAL) == NULL)
+ pw_properties_set(props, PW_KEY_NODE_VIRTUAL, "true");
+ if (pw_properties_get(props, "resample.prefill") == NULL)
+ pw_properties_set(props, "resample.prefill", "true");
+
+ parse_audio_info(props, &impl->info);
+
+ impl->capture_info = impl->info;
+ impl->source_info = impl->info;
+ impl->sink_info = impl->info;
+ impl->playback_info = impl->info;
+
+ if ((str = pw_properties_get(props, "capture.props")) != NULL)
+ pw_properties_update_string(impl->capture_props, str, strlen(str));
+ if ((str = pw_properties_get(props, "source.props")) != NULL)
+ pw_properties_update_string(impl->source_props, str, strlen(str));
+ if ((str = pw_properties_get(props, "sink.props")) != NULL)
+ pw_properties_update_string(impl->sink_props, str, strlen(str));
+ if ((str = pw_properties_get(props, "playback.props")) != NULL)
+ pw_properties_update_string(impl->playback_props, str, strlen(str));
+
+ if (pw_properties_get(impl->capture_props, PW_KEY_NODE_NAME) == NULL)
+ pw_properties_set(impl->capture_props, PW_KEY_NODE_NAME, "echo-cancel-capture");
+ if (pw_properties_get(impl->capture_props, PW_KEY_NODE_DESCRIPTION) == NULL)
+ pw_properties_set(impl->capture_props, PW_KEY_NODE_DESCRIPTION, "Echo-Cancel Capture");
+ if (pw_properties_get(impl->capture_props, PW_KEY_NODE_PASSIVE) == NULL)
+ pw_properties_set(impl->capture_props, PW_KEY_NODE_PASSIVE, "true");
+
+ if (pw_properties_get(impl->source_props, PW_KEY_NODE_NAME) == NULL)
+ pw_properties_set(impl->source_props, PW_KEY_NODE_NAME, "echo-cancel-source");
+ if (pw_properties_get(impl->source_props, PW_KEY_NODE_DESCRIPTION) == NULL)
+ pw_properties_set(impl->source_props, PW_KEY_NODE_DESCRIPTION, "Echo-Cancel Source");
+ if (pw_properties_get(impl->source_props, PW_KEY_MEDIA_CLASS) == NULL)
+ pw_properties_set(impl->source_props, PW_KEY_MEDIA_CLASS, "Audio/Source");
+
+ if (pw_properties_get(impl->playback_props, PW_KEY_NODE_NAME) == NULL)
+ pw_properties_set(impl->playback_props, PW_KEY_NODE_NAME, "echo-cancel-playback");
+ if (pw_properties_get(impl->playback_props, PW_KEY_NODE_DESCRIPTION) == NULL)
+ pw_properties_set(impl->playback_props, PW_KEY_NODE_DESCRIPTION, "Echo-Cancel Playback");
+ if (pw_properties_get(impl->playback_props, PW_KEY_NODE_PASSIVE) == NULL)
+ pw_properties_set(impl->playback_props, PW_KEY_NODE_PASSIVE, "true");
+
+ if (pw_properties_get(impl->sink_props, PW_KEY_NODE_NAME) == NULL)
+ pw_properties_set(impl->sink_props, PW_KEY_NODE_NAME, "echo-cancel-sink");
+ if (pw_properties_get(impl->sink_props, PW_KEY_NODE_DESCRIPTION) == NULL)
+ pw_properties_set(impl->sink_props, PW_KEY_NODE_DESCRIPTION, "Echo-Cancel Sink");
+ if (pw_properties_get(impl->sink_props, PW_KEY_MEDIA_CLASS) == NULL)
+ pw_properties_set(impl->sink_props, PW_KEY_MEDIA_CLASS,
+ impl->monitor_mode ? "Stream/Input/Audio" : "Audio/Sink");
+ if (impl->monitor_mode) {
+ if (pw_properties_get(impl->sink_props, PW_KEY_NODE_PASSIVE) == NULL)
+ pw_properties_set(impl->sink_props, PW_KEY_NODE_PASSIVE, "true");
+ if (pw_properties_get(impl->sink_props, PW_KEY_STREAM_MONITOR) == NULL)
+ pw_properties_set(impl->sink_props, PW_KEY_STREAM_MONITOR, "true");
+ if (pw_properties_get(impl->sink_props, PW_KEY_STREAM_CAPTURE_SINK) == NULL)
+ pw_properties_set(impl->sink_props, PW_KEY_STREAM_CAPTURE_SINK, "true");
+ }
+
+ if ((str = pw_properties_get(props, "aec.method")) != NULL)
+ pw_log_warn("aec.method is not supported anymore use library.name");
+
+ /* Use webrtc as default */
+ if ((path = pw_properties_get(props, "library.name")) == NULL)
+ path = "aec/libspa-aec-webrtc";
+
+ const struct spa_support *support;
+ uint32_t n_support;
+
+ support = pw_context_get_support(context, &n_support);
+ impl->loader = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_PluginLoader);
+ if (impl->loader == NULL) {
+ pw_log_error("a plugin loader is needed");
+ return -EINVAL;
+ }
+
+ struct spa_dict_item info_items[] = {
+ { SPA_KEY_LIBRARY_NAME, path },
+ };
+ struct spa_dict info = SPA_DICT_INIT_ARRAY(info_items);
+
+ handle = spa_plugin_loader_load(impl->loader, SPA_NAME_AEC, &info);
+ if (handle == NULL) {
+ pw_log_error("aec plugin %s not available library.name %s", SPA_NAME_AEC, path);
+ return -ENOENT;
+ }
+
+ if ((res = spa_handle_get_interface(handle, SPA_TYPE_INTERFACE_AUDIO_AEC, &iface)) < 0) {
+ pw_log_error("can't get %s interface %d", SPA_TYPE_INTERFACE_AUDIO_AEC, res);
+ return res;
+ }
+ impl->aec = iface;
+ impl->spa_handle = handle;
+
+ if (impl->aec->iface.version > SPA_VERSION_AUDIO_AEC) {
+ pw_log_error("codec plugin %s has incompatible ABI version (%d > %d)",
+ SPA_NAME_AEC, impl->aec->iface.version, SPA_VERSION_AUDIO_AEC);
+ res = -ENOENT;
+ goto error;
+ }
+
+ pw_log_info("Using plugin AEC %s with version %d", impl->aec->name,
+ impl->aec->iface.version);
+
+ if ((str = pw_properties_get(props, "aec.args")) != NULL)
+ aec_props = pw_properties_new_string(str);
+ else
+ aec_props = pw_properties_new(NULL, NULL);
+
+ res = spa_audio_aec_init(impl->aec, &aec_props->dict, &impl->info);
+
+ pw_properties_free(aec_props);
+
+ if (res < 0) {
+ pw_log_error("aec plugin %s create failed: %s", impl->aec->name,
+ spa_strerror(res));
+ goto error;
+ }
+
+ if (impl->aec->latency) {
+ unsigned int num, denom, req_num, req_denom;
+ unsigned int factor = 0;
+ unsigned int new_num = 0;
+
+ spa_assert_se(sscanf(impl->aec->latency, "%u/%u", &num, &denom) == 2);
+
+ if ((str = pw_properties_get(props, PW_KEY_NODE_LATENCY)) != NULL) {
+ sscanf(str, "%u/%u", &req_num, &req_denom);
+ factor = (req_num * denom) / (req_denom * num);
+ new_num = req_num / factor * factor;
+ }
+
+ if (factor == 0 || new_num == 0) {
+ pw_log_info("Setting node latency to %s", impl->aec->latency);
+ pw_properties_set(props, PW_KEY_NODE_LATENCY, impl->aec->latency);
+ impl->aec_blocksize = sizeof(float) * impl->info.rate * num / denom;
+ } else {
+ pw_log_info("Setting node latency to %u/%u", new_num, req_denom);
+ pw_properties_setf(props, PW_KEY_NODE_LATENCY, "%u/%u", new_num, req_denom);
+ impl->aec_blocksize = sizeof(float) * impl->info.rate * num / denom * factor;
+ }
+ } else {
+ /* Implementation doesn't care about the block size */
+ impl->aec_blocksize = 0;
+ }
+
+ impl->core = pw_context_get_object(impl->context, PW_TYPE_INTERFACE_Core);
+ if (impl->core == NULL) {
+ str = pw_properties_get(props, PW_KEY_REMOTE_NAME);
+ impl->core = pw_context_connect(impl->context,
+ pw_properties_new(
+ PW_KEY_REMOTE_NAME, str,
+ NULL),
+ 0);
+ impl->do_disconnect = true;
+ }
+ if (impl->core == NULL) {
+ res = -errno;
+ pw_log_error("can't connect: %m");
+ goto error;
+ }
+
+ copy_props(impl, props, PW_KEY_NODE_GROUP);
+ copy_props(impl, props, PW_KEY_NODE_LINK_GROUP);
+ copy_props(impl, props, PW_KEY_NODE_VIRTUAL);
+ copy_props(impl, props, PW_KEY_NODE_LATENCY);
+ copy_props(impl, props, SPA_KEY_AUDIO_CHANNELS);
+ copy_props(impl, props, SPA_KEY_AUDIO_POSITION);
+ copy_props(impl, props, "resample.prefill");
+
+ if ((str = pw_properties_get(impl->capture_props, SPA_KEY_AUDIO_POSITION)) != NULL)
+ parse_position(&impl->capture_info, str, strlen(str));
+ if ((str = pw_properties_get(impl->source_props, SPA_KEY_AUDIO_POSITION)) != NULL)
+ parse_position(&impl->source_info, str, strlen(str));
+ if ((str = pw_properties_get(impl->sink_props, SPA_KEY_AUDIO_POSITION)) != NULL)
+ parse_position(&impl->sink_info, str, strlen(str));
+ if ((str = pw_properties_get(impl->playback_props, SPA_KEY_AUDIO_POSITION)) != NULL)
+ parse_position(&impl->playback_info, str, strlen(str));
+
+ if (impl->capture_info.channels != impl->info.channels)
+ impl->capture_info = impl->info;
+ if (impl->source_info.channels != impl->info.channels)
+ impl->source_info = impl->info;
+ if (impl->sink_info.channels != impl->info.channels)
+ impl->sink_info = impl->info;
+ if (impl->playback_info.channels != impl->info.channels)
+ impl->playback_info = impl->info;
+
+ impl->max_buffer_size = pw_properties_get_uint32(props,"buffer.max_size", MAX_BUFSIZE_MS);
+
+ if ((str = pw_properties_get(props, "buffer.play_delay")) != NULL) {
+ int req_num, req_denom;
+ if (sscanf(str, "%u/%u", &req_num, &req_denom) == 2) {
+ if (req_denom != 0) {
+ impl->buffer_delay = (impl->info.rate*req_num)/req_denom;
+ } else {
+ impl->buffer_delay = DELAY_MS * impl->info.rate / 1000;
+ pw_log_warn("Sample rate for buffer.play_delay is 0 using default");
+ }
+ } else {
+ impl->buffer_delay = DELAY_MS * impl->info.rate / 1000;
+ pw_log_warn("Wrong value/format for buffer.play_delay using default");
+ }
+ } else {
+ impl->buffer_delay = DELAY_MS * impl->info.rate / 1000;
+ }
+
+ pw_properties_free(props);
+
+ pw_proxy_add_listener((struct pw_proxy*)impl->core,
+ &impl->core_proxy_listener,
+ &core_proxy_events, impl);
+ pw_core_add_listener(impl->core,
+ &impl->core_listener,
+ &core_events, impl);
+
+ setup_streams(impl);
+
+ pw_impl_module_add_listener(module, &impl->module_listener, &module_events, impl);
+
+ pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_props));
+
+ return 0;
+
+error:
+ pw_properties_free(props);
+ impl_destroy(impl);
+ return res;
+}
diff --git a/src/modules/module-example-sink.c b/src/modules/module-example-sink.c
new file mode 100644
index 0000000..d999922
--- /dev/null
+++ b/src/modules/module-example-sink.c
@@ -0,0 +1,498 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <signal.h>
+#include <limits.h>
+#include <math.h>
+
+#include "config.h"
+
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+#include <spa/utils/json.h>
+#include <spa/utils/ringbuffer.h>
+#include <spa/debug/types.h>
+#include <spa/pod/builder.h>
+#include <spa/param/audio/format-utils.h>
+#include <spa/param/audio/raw.h>
+
+#include <pipewire/impl.h>
+#include <pipewire/i18n.h>
+
+/** \page page_module_example_sink PipeWire Module: Example Sink
+ *
+ * The example sink is a good starting point for writing a custom
+ * sink. We refer to the source code for more information.
+ *
+ * ## Module Options
+ *
+ * - `node.name`: a unique name for the stream
+ * - `node.description`: a human readable name for the stream
+ * - `stream.props = {}`: properties to be passed to the stream
+ *
+ * ## General options
+ *
+ * Options with well-known behavior.
+ *
+ * - \ref PW_KEY_REMOTE_NAME
+ * - \ref PW_KEY_AUDIO_FORMAT
+ * - \ref PW_KEY_AUDIO_RATE
+ * - \ref PW_KEY_AUDIO_CHANNELS
+ * - \ref SPA_KEY_AUDIO_POSITION
+ * - \ref PW_KEY_MEDIA_NAME
+ * - \ref PW_KEY_NODE_LATENCY
+ * - \ref PW_KEY_NODE_NAME
+ * - \ref PW_KEY_NODE_DESCRIPTION
+ * - \ref PW_KEY_NODE_GROUP
+ * - \ref PW_KEY_NODE_VIRTUAL
+ * - \ref PW_KEY_MEDIA_CLASS
+ *
+ * ## Example configuration
+ *
+ *\code{.unparsed}
+ * context.modules = [
+ * { name = libpipewire-module-example-sink
+ * args = {
+ * node.name = "example_sink"
+ * node.description = "My Example Sink"
+ * stream.props = {
+ * audio.position = [ FL FR ]
+ * }
+ * }
+ * }
+ * ]
+ *\endcode
+ */
+
+#define NAME "example-sink"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+#define DEFAULT_FORMAT "S16"
+#define DEFAULT_RATE 48000
+#define DEFAULT_CHANNELS 2
+#define DEFAULT_POSITION "[ FL FR ]"
+
+#define MODULE_USAGE "[ node.latency=<latency as fraction> ] " \
+ "[ node.name=<name of the nodes> ] " \
+ "[ node.description=<description of the nodes> ] " \
+ "[ audio.format=<format, default:"DEFAULT_FORMAT"> ] " \
+ "[ audio.rate=<sample rate, default: "SPA_STRINGIFY(DEFAULT_RATE)"> ] " \
+ "[ audio.channels=<number of channels, default:"SPA_STRINGIFY(DEFAULT_CHANNELS) "> ] " \
+ "[ audio.position=<channel map, default:"DEFAULT_POSITION"> ] " \
+ "[ stream.props=<properties> ] "
+
+
+static const struct spa_dict_item module_props[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "An example audio sink" },
+ { PW_KEY_MODULE_USAGE, MODULE_USAGE },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+struct impl {
+ struct pw_context *context;
+
+ struct pw_properties *props;
+
+ struct pw_impl_module *module;
+
+ struct spa_hook module_listener;
+
+ struct pw_core *core;
+ struct spa_hook core_proxy_listener;
+ struct spa_hook core_listener;
+
+ struct pw_properties *stream_props;
+ struct pw_stream *stream;
+ struct spa_hook stream_listener;
+ struct spa_audio_info_raw info;
+ uint32_t frame_size;
+
+ unsigned int do_disconnect:1;
+};
+
+static void stream_destroy(void *d)
+{
+ struct impl *impl = d;
+ spa_hook_remove(&impl->stream_listener);
+ impl->stream = NULL;
+}
+
+static void stream_state_changed(void *d, enum pw_stream_state old,
+ enum pw_stream_state state, const char *error)
+{
+ struct impl *impl = d;
+ switch (state) {
+ case PW_STREAM_STATE_ERROR:
+ case PW_STREAM_STATE_UNCONNECTED:
+ pw_impl_module_schedule_destroy(impl->module);
+ break;
+ case PW_STREAM_STATE_PAUSED:
+ case PW_STREAM_STATE_STREAMING:
+ break;
+ default:
+ break;
+ }
+}
+
+static void playback_stream_process(void *d)
+{
+ struct impl *impl = d;
+ struct pw_buffer *buf;
+ struct spa_data *bd;
+ void *data;
+ uint32_t offs, size;
+
+ if ((buf = pw_stream_dequeue_buffer(impl->stream)) == NULL) {
+ pw_log_debug("out of buffers: %m");
+ return;
+ }
+
+ bd = &buf->buffer->datas[0];
+
+ offs = SPA_MIN(bd->chunk->offset, bd->maxsize);
+ size = SPA_MIN(bd->chunk->size, bd->maxsize - offs);
+ data = SPA_PTROFF(bd->data, offs, void);
+
+ /* write buffer contents here */
+ pw_log_info("got buffer of size %d and data %p", size, data);
+
+ pw_stream_queue_buffer(impl->stream, buf);
+}
+
+static const struct pw_stream_events playback_stream_events = {
+ PW_VERSION_STREAM_EVENTS,
+ .destroy = stream_destroy,
+ .state_changed = stream_state_changed,
+ .process = playback_stream_process
+};
+
+static int create_stream(struct impl *impl)
+{
+ int res;
+ uint32_t n_params;
+ const struct spa_pod *params[1];
+ uint8_t buffer[1024];
+ struct spa_pod_builder b;
+
+ impl->stream = pw_stream_new(impl->core, "example sink", impl->stream_props);
+ impl->stream_props = NULL;
+
+ if (impl->stream == NULL)
+ return -errno;
+
+ pw_stream_add_listener(impl->stream,
+ &impl->stream_listener,
+ &playback_stream_events, impl);
+
+ n_params = 0;
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ params[n_params++] = spa_format_audio_raw_build(&b,
+ SPA_PARAM_EnumFormat, &impl->info);
+
+ if ((res = pw_stream_connect(impl->stream,
+ PW_DIRECTION_INPUT,
+ PW_ID_ANY,
+ PW_STREAM_FLAG_AUTOCONNECT |
+ PW_STREAM_FLAG_MAP_BUFFERS |
+ PW_STREAM_FLAG_RT_PROCESS,
+ params, n_params)) < 0)
+ return res;
+
+ return 0;
+}
+
+static void core_error(void *data, uint32_t id, int seq, int res, const char *message)
+{
+ struct impl *impl = data;
+
+ pw_log_error("error id:%u seq:%d res:%d (%s): %s",
+ id, seq, res, spa_strerror(res), message);
+
+ if (id == PW_ID_CORE && res == -EPIPE)
+ pw_impl_module_schedule_destroy(impl->module);
+}
+
+static const struct pw_core_events core_events = {
+ PW_VERSION_CORE_EVENTS,
+ .error = core_error,
+};
+
+static void core_destroy(void *d)
+{
+ struct impl *impl = d;
+ spa_hook_remove(&impl->core_listener);
+ impl->core = NULL;
+ pw_impl_module_schedule_destroy(impl->module);
+}
+
+static const struct pw_proxy_events core_proxy_events = {
+ .destroy = core_destroy,
+};
+
+static void impl_destroy(struct impl *impl)
+{
+ if (impl->stream)
+ pw_stream_destroy(impl->stream);
+ if (impl->core && impl->do_disconnect)
+ pw_core_disconnect(impl->core);
+
+ pw_properties_free(impl->stream_props);
+ pw_properties_free(impl->props);
+
+ free(impl);
+}
+
+static void module_destroy(void *data)
+{
+ struct impl *impl = data;
+ spa_hook_remove(&impl->module_listener);
+ impl_destroy(impl);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy,
+};
+
+static inline uint32_t format_from_name(const char *name, size_t len)
+{
+ int i;
+ for (i = 0; spa_type_audio_format[i].name; i++) {
+ if (strncmp(name, spa_debug_type_short_name(spa_type_audio_format[i].name), len) == 0)
+ return spa_type_audio_format[i].type;
+ }
+ return SPA_AUDIO_FORMAT_UNKNOWN;
+}
+
+static uint32_t channel_from_name(const char *name)
+{
+ int i;
+ for (i = 0; spa_type_audio_channel[i].name; i++) {
+ if (spa_streq(name, spa_debug_type_short_name(spa_type_audio_channel[i].name)))
+ return spa_type_audio_channel[i].type;
+ }
+ return SPA_AUDIO_CHANNEL_UNKNOWN;
+}
+
+static void parse_position(struct spa_audio_info_raw *info, const char *val, size_t len)
+{
+ struct spa_json it[2];
+ char v[256];
+
+ spa_json_init(&it[0], val, len);
+ if (spa_json_enter_array(&it[0], &it[1]) <= 0)
+ spa_json_init(&it[1], val, len);
+
+ info->channels = 0;
+ while (spa_json_get_string(&it[1], v, sizeof(v)) > 0 &&
+ info->channels < SPA_AUDIO_MAX_CHANNELS) {
+ info->position[info->channels++] = channel_from_name(v);
+ }
+}
+
+static void parse_audio_info(const struct pw_properties *props, struct spa_audio_info_raw *info)
+{
+ const char *str;
+
+ spa_zero(*info);
+ if ((str = pw_properties_get(props, PW_KEY_AUDIO_FORMAT)) == NULL)
+ str = DEFAULT_FORMAT;
+ info->format = format_from_name(str, strlen(str));
+
+ info->rate = pw_properties_get_uint32(props, PW_KEY_AUDIO_RATE, info->rate);
+ if (info->rate == 0)
+ info->rate = DEFAULT_RATE;
+
+ info->channels = pw_properties_get_uint32(props, PW_KEY_AUDIO_CHANNELS, info->channels);
+ info->channels = SPA_MIN(info->channels, SPA_AUDIO_MAX_CHANNELS);
+ if ((str = pw_properties_get(props, SPA_KEY_AUDIO_POSITION)) != NULL)
+ parse_position(info, str, strlen(str));
+ if (info->channels == 0)
+ parse_position(info, DEFAULT_POSITION, strlen(DEFAULT_POSITION));
+}
+
+static int calc_frame_size(const struct spa_audio_info_raw *info)
+{
+ int res = info->channels;
+ switch (info->format) {
+ case SPA_AUDIO_FORMAT_U8:
+ case SPA_AUDIO_FORMAT_S8:
+ case SPA_AUDIO_FORMAT_ALAW:
+ case SPA_AUDIO_FORMAT_ULAW:
+ return res;
+ case SPA_AUDIO_FORMAT_S16:
+ case SPA_AUDIO_FORMAT_S16_OE:
+ case SPA_AUDIO_FORMAT_U16:
+ return res * 2;
+ case SPA_AUDIO_FORMAT_S24:
+ case SPA_AUDIO_FORMAT_S24_OE:
+ case SPA_AUDIO_FORMAT_U24:
+ return res * 3;
+ case SPA_AUDIO_FORMAT_S24_32:
+ case SPA_AUDIO_FORMAT_S24_32_OE:
+ case SPA_AUDIO_FORMAT_S32:
+ case SPA_AUDIO_FORMAT_S32_OE:
+ case SPA_AUDIO_FORMAT_U32:
+ case SPA_AUDIO_FORMAT_U32_OE:
+ case SPA_AUDIO_FORMAT_F32:
+ case SPA_AUDIO_FORMAT_F32_OE:
+ return res * 4;
+ case SPA_AUDIO_FORMAT_F64:
+ case SPA_AUDIO_FORMAT_F64_OE:
+ return res * 8;
+ default:
+ return 0;
+ }
+}
+
+static void copy_props(struct impl *impl, struct pw_properties *props, const char *key)
+{
+ const char *str;
+ if ((str = pw_properties_get(props, key)) != NULL) {
+ if (pw_properties_get(impl->stream_props, key) == NULL)
+ pw_properties_set(impl->stream_props, key, str);
+ }
+}
+
+SPA_EXPORT
+int pipewire__module_init(struct pw_impl_module *module, const char *args)
+{
+ struct pw_context *context = pw_impl_module_get_context(module);
+ struct pw_properties *props = NULL;
+ uint32_t id = pw_global_get_id(pw_impl_module_get_global(module));
+ uint32_t pid = getpid();
+ struct impl *impl;
+ const char *str;
+ int res;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ impl = calloc(1, sizeof(struct impl));
+ if (impl == NULL)
+ return -errno;
+
+ pw_log_debug("module %p: new %s", impl, args);
+
+ if (args == NULL)
+ args = "";
+
+ props = pw_properties_new_string(args);
+ if (props == NULL) {
+ res = -errno;
+ pw_log_error( "can't create properties: %m");
+ goto error;
+ }
+ impl->props = props;
+
+ impl->stream_props = pw_properties_new(NULL, NULL);
+ if (impl->stream_props == NULL) {
+ res = -errno;
+ pw_log_error( "can't create properties: %m");
+ goto error;
+ }
+
+ impl->module = module;
+ impl->context = context;
+
+ if (pw_properties_get(props, PW_KEY_NODE_VIRTUAL) == NULL)
+ pw_properties_set(props, PW_KEY_NODE_VIRTUAL, "true");
+
+ if (pw_properties_get(props, PW_KEY_MEDIA_CLASS) == NULL)
+ pw_properties_set(props, PW_KEY_MEDIA_CLASS, "Audio/Sink");
+
+ if (pw_properties_get(props, PW_KEY_NODE_NAME) == NULL)
+ pw_properties_setf(props, PW_KEY_NODE_NAME, "example-sink-%u-%u", pid, id);
+ if (pw_properties_get(props, PW_KEY_NODE_DESCRIPTION) == NULL)
+ pw_properties_set(props, PW_KEY_NODE_DESCRIPTION,
+ pw_properties_get(props, PW_KEY_NODE_NAME));
+
+ if ((str = pw_properties_get(props, "stream.props")) != NULL)
+ pw_properties_update_string(impl->stream_props, str, strlen(str));
+
+ copy_props(impl, props, PW_KEY_AUDIO_RATE);
+ copy_props(impl, props, PW_KEY_AUDIO_CHANNELS);
+ copy_props(impl, props, SPA_KEY_AUDIO_POSITION);
+ copy_props(impl, props, PW_KEY_NODE_NAME);
+ copy_props(impl, props, PW_KEY_NODE_DESCRIPTION);
+ copy_props(impl, props, PW_KEY_NODE_GROUP);
+ copy_props(impl, props, PW_KEY_NODE_LATENCY);
+ copy_props(impl, props, PW_KEY_NODE_VIRTUAL);
+ copy_props(impl, props, PW_KEY_MEDIA_CLASS);
+
+ parse_audio_info(impl->stream_props, &impl->info);
+
+ impl->frame_size = calc_frame_size(&impl->info);
+ if (impl->frame_size == 0) {
+ res = -EINVAL;
+ pw_log_error( "can't parse audio format");
+ goto error;
+ }
+
+ impl->core = pw_context_get_object(impl->context, PW_TYPE_INTERFACE_Core);
+ if (impl->core == NULL) {
+ str = pw_properties_get(props, PW_KEY_REMOTE_NAME);
+ impl->core = pw_context_connect(impl->context,
+ pw_properties_new(
+ PW_KEY_REMOTE_NAME, str,
+ NULL),
+ 0);
+ impl->do_disconnect = true;
+ }
+ if (impl->core == NULL) {
+ res = -errno;
+ pw_log_error("can't connect: %m");
+ goto error;
+ }
+
+ pw_proxy_add_listener((struct pw_proxy*)impl->core,
+ &impl->core_proxy_listener,
+ &core_proxy_events, impl);
+ pw_core_add_listener(impl->core,
+ &impl->core_listener,
+ &core_events, impl);
+
+ if ((res = create_stream(impl)) < 0)
+ goto error;
+
+ pw_impl_module_add_listener(module, &impl->module_listener, &module_events, impl);
+
+ pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_props));
+
+ return 0;
+
+error:
+ impl_destroy(impl);
+ return res;
+}
diff --git a/src/modules/module-example-source.c b/src/modules/module-example-source.c
new file mode 100644
index 0000000..1db62df
--- /dev/null
+++ b/src/modules/module-example-source.c
@@ -0,0 +1,504 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <signal.h>
+#include <limits.h>
+#include <math.h>
+
+#include "config.h"
+
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+#include <spa/utils/json.h>
+#include <spa/utils/ringbuffer.h>
+#include <spa/debug/types.h>
+#include <spa/pod/builder.h>
+#include <spa/param/audio/format-utils.h>
+#include <spa/param/audio/raw.h>
+
+#include <pipewire/impl.h>
+#include <pipewire/i18n.h>
+
+/** \page page_module_example_source PipeWire Module: Example Source
+ *
+ * The example source is a good starting point for writing a custom
+ * source. We refer to the source code for more information.
+ *
+ * ## Module Options
+ *
+ * - `node.name`: a unique name for the stream
+ * - `node.description`: a human readable name for the stream
+ * - `stream.props = {}`: properties to be passed to the stream
+ *
+ * ## General options
+ *
+ * Options with well-known behavior.
+ *
+ * - \ref PW_KEY_REMOTE_NAME
+ * - \ref PW_KEY_AUDIO_FORMAT
+ * - \ref PW_KEY_AUDIO_RATE
+ * - \ref PW_KEY_AUDIO_CHANNELS
+ * - \ref SPA_KEY_AUDIO_POSITION
+ * - \ref PW_KEY_MEDIA_NAME
+ * - \ref PW_KEY_NODE_LATENCY
+ * - \ref PW_KEY_NODE_NAME
+ * - \ref PW_KEY_NODE_DESCRIPTION
+ * - \ref PW_KEY_NODE_GROUP
+ * - \ref PW_KEY_NODE_VIRTUAL
+ * - \ref PW_KEY_MEDIA_CLASS
+ *
+ * ## Example configuration
+ *
+ *\code{.unparsed}
+ * context.modules = [
+ * { name = libpipewire-module-example-source
+ * args = {
+ * node.name = "example_source"
+ * node.description = "My Example Source"
+ * stream.props = {
+ * audio.position = [ FL FR ]
+ * }
+ * }
+ * }
+ * ]
+ *\endcode
+ */
+
+#define NAME "example-source"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+#define DEFAULT_FORMAT "S16"
+#define DEFAULT_RATE 48000
+#define DEFAULT_CHANNELS 2
+#define DEFAULT_POSITION "[ FL FR ]"
+
+#define MODULE_USAGE "[ node.latency=<latency as fraction> ] " \
+ "[ node.name=<name of the nodes> ] " \
+ "[ node.description=<description of the nodes> ] " \
+ "[ audio.format=<format, default:"DEFAULT_FORMAT"> ] " \
+ "[ audio.rate=<sample rate, default: "SPA_STRINGIFY(DEFAULT_RATE)"> ] " \
+ "[ audio.channels=<number of channels, default:"SPA_STRINGIFY(DEFAULT_CHANNELS) "> ] " \
+ "[ audio.position=<channel map, default:"DEFAULT_POSITION"> ] " \
+ "[ stream.props=<properties> ] "
+
+
+static const struct spa_dict_item module_props[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "An example audio source" },
+ { PW_KEY_MODULE_USAGE, MODULE_USAGE },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+struct impl {
+ struct pw_context *context;
+
+ struct pw_properties *props;
+
+ struct pw_impl_module *module;
+
+ struct spa_hook module_listener;
+
+ struct pw_core *core;
+ struct spa_hook core_proxy_listener;
+ struct spa_hook core_listener;
+
+ struct pw_properties *stream_props;
+ struct pw_stream *stream;
+ struct spa_hook stream_listener;
+ struct spa_audio_info_raw info;
+ uint32_t frame_size;
+
+ unsigned int do_disconnect:1;
+ unsigned int unloading:1;
+};
+
+static void stream_destroy(void *d)
+{
+ struct impl *impl = d;
+ spa_hook_remove(&impl->stream_listener);
+ impl->stream = NULL;
+}
+
+static void stream_state_changed(void *d, enum pw_stream_state old,
+ enum pw_stream_state state, const char *error)
+{
+ struct impl *impl = d;
+ switch (state) {
+ case PW_STREAM_STATE_ERROR:
+ case PW_STREAM_STATE_UNCONNECTED:
+ pw_impl_module_schedule_destroy(impl->module);
+ break;
+ case PW_STREAM_STATE_PAUSED:
+ case PW_STREAM_STATE_STREAMING:
+ break;
+ default:
+ break;
+ }
+}
+
+static void capture_stream_process(void *d)
+{
+ struct impl *impl = d;
+ struct pw_buffer *buf;
+ struct spa_data *bd;
+ void *data;
+ uint32_t size;
+
+ if ((buf = pw_stream_dequeue_buffer(impl->stream)) == NULL) {
+ pw_log_debug("out of buffers: %m");
+ return;
+ }
+
+ bd = &buf->buffer->datas[0];
+
+ data = bd->data;
+ size = buf->requested ? buf->requested * impl->frame_size : bd->maxsize;
+
+ /* fill buffer contents here */
+ pw_log_info("fill buffer data %p with up to %u bytes", data, size);
+
+ bd->chunk->size = size;
+ bd->chunk->stride = impl->frame_size;
+ bd->chunk->offset = 0;
+ buf->size = size / impl->frame_size;
+
+ pw_stream_queue_buffer(impl->stream, buf);
+}
+
+static const struct pw_stream_events capture_stream_events = {
+ PW_VERSION_STREAM_EVENTS,
+ .destroy = stream_destroy,
+ .state_changed = stream_state_changed,
+ .process = capture_stream_process
+};
+
+static int create_stream(struct impl *impl)
+{
+ int res;
+ uint32_t n_params;
+ const struct spa_pod *params[1];
+ uint8_t buffer[1024];
+ struct spa_pod_builder b;
+
+ impl->stream = pw_stream_new(impl->core, "example source", impl->stream_props);
+ impl->stream_props = NULL;
+
+ if (impl->stream == NULL)
+ return -errno;
+
+ pw_stream_add_listener(impl->stream,
+ &impl->stream_listener,
+ &capture_stream_events, impl);
+
+ n_params = 0;
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ params[n_params++] = spa_format_audio_raw_build(&b,
+ SPA_PARAM_EnumFormat, &impl->info);
+
+ if ((res = pw_stream_connect(impl->stream,
+ PW_DIRECTION_OUTPUT,
+ PW_ID_ANY,
+ PW_STREAM_FLAG_AUTOCONNECT |
+ PW_STREAM_FLAG_MAP_BUFFERS |
+ PW_STREAM_FLAG_RT_PROCESS,
+ params, n_params)) < 0)
+ return res;
+
+ return 0;
+}
+
+static void core_error(void *data, uint32_t id, int seq, int res, const char *message)
+{
+ struct impl *impl = data;
+
+ pw_log_error("error id:%u seq:%d res:%d (%s): %s",
+ id, seq, res, spa_strerror(res), message);
+
+ if (id == PW_ID_CORE && res == -EPIPE)
+ pw_impl_module_schedule_destroy(impl->module);
+}
+
+static const struct pw_core_events core_events = {
+ PW_VERSION_CORE_EVENTS,
+ .error = core_error,
+};
+
+static void core_destroy(void *d)
+{
+ struct impl *impl = d;
+ spa_hook_remove(&impl->core_listener);
+ impl->core = NULL;
+ pw_impl_module_schedule_destroy(impl->module);
+}
+
+static const struct pw_proxy_events core_proxy_events = {
+ .destroy = core_destroy,
+};
+
+static void impl_destroy(struct impl *impl)
+{
+ if (impl->stream)
+ pw_stream_destroy(impl->stream);
+ if (impl->core && impl->do_disconnect)
+ pw_core_disconnect(impl->core);
+
+ pw_properties_free(impl->stream_props);
+ pw_properties_free(impl->props);
+
+ free(impl);
+}
+
+static void module_destroy(void *data)
+{
+ struct impl *impl = data;
+ impl->unloading = true;
+ spa_hook_remove(&impl->module_listener);
+ impl_destroy(impl);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy,
+};
+
+static inline uint32_t format_from_name(const char *name, size_t len)
+{
+ int i;
+ for (i = 0; spa_type_audio_format[i].name; i++) {
+ if (strncmp(name, spa_debug_type_short_name(spa_type_audio_format[i].name), len) == 0)
+ return spa_type_audio_format[i].type;
+ }
+ return SPA_AUDIO_FORMAT_UNKNOWN;
+}
+
+static uint32_t channel_from_name(const char *name)
+{
+ int i;
+ for (i = 0; spa_type_audio_channel[i].name; i++) {
+ if (spa_streq(name, spa_debug_type_short_name(spa_type_audio_channel[i].name)))
+ return spa_type_audio_channel[i].type;
+ }
+ return SPA_AUDIO_CHANNEL_UNKNOWN;
+}
+
+static void parse_position(struct spa_audio_info_raw *info, const char *val, size_t len)
+{
+ struct spa_json it[2];
+ char v[256];
+
+ spa_json_init(&it[0], val, len);
+ if (spa_json_enter_array(&it[0], &it[1]) <= 0)
+ spa_json_init(&it[1], val, len);
+
+ info->channels = 0;
+ while (spa_json_get_string(&it[1], v, sizeof(v)) > 0 &&
+ info->channels < SPA_AUDIO_MAX_CHANNELS) {
+ info->position[info->channels++] = channel_from_name(v);
+ }
+}
+
+static void parse_audio_info(const struct pw_properties *props, struct spa_audio_info_raw *info)
+{
+ const char *str;
+
+ spa_zero(*info);
+ if ((str = pw_properties_get(props, PW_KEY_AUDIO_FORMAT)) == NULL)
+ str = DEFAULT_FORMAT;
+ info->format = format_from_name(str, strlen(str));
+
+ info->rate = pw_properties_get_uint32(props, PW_KEY_AUDIO_RATE, info->rate);
+ if (info->rate == 0)
+ info->rate = DEFAULT_RATE;
+
+ info->channels = pw_properties_get_uint32(props, PW_KEY_AUDIO_CHANNELS, info->channels);
+ info->channels = SPA_MIN(info->channels, SPA_AUDIO_MAX_CHANNELS);
+ if ((str = pw_properties_get(props, SPA_KEY_AUDIO_POSITION)) != NULL)
+ parse_position(info, str, strlen(str));
+ if (info->channels == 0)
+ parse_position(info, DEFAULT_POSITION, strlen(DEFAULT_POSITION));
+}
+
+static int calc_frame_size(const struct spa_audio_info_raw *info)
+{
+ int res = info->channels;
+ switch (info->format) {
+ case SPA_AUDIO_FORMAT_U8:
+ case SPA_AUDIO_FORMAT_S8:
+ case SPA_AUDIO_FORMAT_ALAW:
+ case SPA_AUDIO_FORMAT_ULAW:
+ return res;
+ case SPA_AUDIO_FORMAT_S16:
+ case SPA_AUDIO_FORMAT_S16_OE:
+ case SPA_AUDIO_FORMAT_U16:
+ return res * 2;
+ case SPA_AUDIO_FORMAT_S24:
+ case SPA_AUDIO_FORMAT_S24_OE:
+ case SPA_AUDIO_FORMAT_U24:
+ return res * 3;
+ case SPA_AUDIO_FORMAT_S24_32:
+ case SPA_AUDIO_FORMAT_S24_32_OE:
+ case SPA_AUDIO_FORMAT_S32:
+ case SPA_AUDIO_FORMAT_S32_OE:
+ case SPA_AUDIO_FORMAT_U32:
+ case SPA_AUDIO_FORMAT_U32_OE:
+ case SPA_AUDIO_FORMAT_F32:
+ case SPA_AUDIO_FORMAT_F32_OE:
+ return res * 4;
+ case SPA_AUDIO_FORMAT_F64:
+ case SPA_AUDIO_FORMAT_F64_OE:
+ return res * 8;
+ default:
+ return 0;
+ }
+}
+
+static void copy_props(struct impl *impl, struct pw_properties *props, const char *key)
+{
+ const char *str;
+ if ((str = pw_properties_get(props, key)) != NULL) {
+ if (pw_properties_get(impl->stream_props, key) == NULL)
+ pw_properties_set(impl->stream_props, key, str);
+ }
+}
+
+SPA_EXPORT
+int pipewire__module_init(struct pw_impl_module *module, const char *args)
+{
+ struct pw_context *context = pw_impl_module_get_context(module);
+ uint32_t id = pw_global_get_id(pw_impl_module_get_global(module));
+ uint32_t pid = getpid();
+ struct pw_properties *props = NULL;
+ struct impl *impl;
+ const char *str;
+ int res;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ impl = calloc(1, sizeof(struct impl));
+ if (impl == NULL)
+ return -errno;
+
+ pw_log_debug("module %p: new %s", impl, args);
+
+ if (args == NULL)
+ args = "";
+
+ props = pw_properties_new_string(args);
+ if (props == NULL) {
+ res = -errno;
+ pw_log_error( "can't create properties: %m");
+ goto error;
+ }
+ impl->props = props;
+
+ impl->stream_props = pw_properties_new(NULL, NULL);
+ if (impl->stream_props == NULL) {
+ res = -errno;
+ pw_log_error( "can't create properties: %m");
+ goto error;
+ }
+
+ impl->module = module;
+ impl->context = context;
+
+ if (pw_properties_get(props, PW_KEY_NODE_VIRTUAL) == NULL)
+ pw_properties_set(props, PW_KEY_NODE_VIRTUAL, "true");
+
+ if (pw_properties_get(props, PW_KEY_MEDIA_CLASS) == NULL)
+ pw_properties_set(props, PW_KEY_MEDIA_CLASS, "Audio/Source");
+
+ if (pw_properties_get(props, PW_KEY_NODE_NAME) == NULL)
+ pw_properties_setf(props, PW_KEY_NODE_NAME, "example-source-%u-%u", pid, id);
+ if (pw_properties_get(props, PW_KEY_NODE_DESCRIPTION) == NULL)
+ pw_properties_set(props, PW_KEY_NODE_DESCRIPTION,
+ pw_properties_get(props, PW_KEY_NODE_NAME));
+
+ if ((str = pw_properties_get(props, "stream.props")) != NULL)
+ pw_properties_update_string(impl->stream_props, str, strlen(str));
+
+ copy_props(impl, props, PW_KEY_AUDIO_RATE);
+ copy_props(impl, props, PW_KEY_AUDIO_CHANNELS);
+ copy_props(impl, props, SPA_KEY_AUDIO_POSITION);
+ copy_props(impl, props, PW_KEY_NODE_NAME);
+ copy_props(impl, props, PW_KEY_NODE_DESCRIPTION);
+ copy_props(impl, props, PW_KEY_NODE_GROUP);
+ copy_props(impl, props, PW_KEY_NODE_LATENCY);
+ copy_props(impl, props, PW_KEY_NODE_VIRTUAL);
+ copy_props(impl, props, PW_KEY_MEDIA_CLASS);
+
+ parse_audio_info(impl->stream_props, &impl->info);
+
+ impl->frame_size = calc_frame_size(&impl->info);
+ if (impl->frame_size == 0) {
+ res = -EINVAL;
+ pw_log_error( "can't parse audio format");
+ goto error;
+ }
+
+ impl->core = pw_context_get_object(impl->context, PW_TYPE_INTERFACE_Core);
+ if (impl->core == NULL) {
+ str = pw_properties_get(props, PW_KEY_REMOTE_NAME);
+ impl->core = pw_context_connect(impl->context,
+ pw_properties_new(
+ PW_KEY_REMOTE_NAME, str,
+ NULL),
+ 0);
+ impl->do_disconnect = true;
+ }
+ if (impl->core == NULL) {
+ res = -errno;
+ pw_log_error("can't connect: %m");
+ goto error;
+ }
+
+ pw_proxy_add_listener((struct pw_proxy*)impl->core,
+ &impl->core_proxy_listener,
+ &core_proxy_events, impl);
+ pw_core_add_listener(impl->core,
+ &impl->core_listener,
+ &core_events, impl);
+
+ if ((res = create_stream(impl)) < 0)
+ goto error;
+
+ pw_impl_module_add_listener(module, &impl->module_listener, &module_events, impl);
+
+ pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_props));
+
+ return 0;
+
+error:
+ impl_destroy(impl);
+ return res;
+}
diff --git a/src/modules/module-fallback-sink.c b/src/modules/module-fallback-sink.c
new file mode 100644
index 0000000..9a277d1
--- /dev/null
+++ b/src/modules/module-fallback-sink.c
@@ -0,0 +1,473 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+
+#include "config.h"
+
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+#include <spa/param/audio/raw.h>
+
+#include <pipewire/impl.h>
+#include <pipewire/i18n.h>
+
+/** \page page_module_fallback_sink PipeWire Module: Fallback Sink
+ *
+ * Fallback sink, which appear dynamically when no other sinks are
+ * present. This is only useful for Pulseaudio compatibility.
+ */
+
+#define NAME "fallback-sink"
+
+#define DEFAULT_SINK_NAME "auto_null"
+#define DEFAULT_SINK_DESCRIPTION _("Dummy Output")
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+#define MODULE_USAGE ("[ sink.name=<str> ] " \
+ "[ sink.description=<str> ] ")
+
+static const struct spa_dict_item module_props[] = {
+ { PW_KEY_MODULE_AUTHOR, "Pauli Virtanen <pav@iki.fi>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Dynamically appearing fallback sink" },
+ { PW_KEY_MODULE_USAGE, MODULE_USAGE },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+struct bitmap {
+ uint8_t *data;
+ size_t size;
+ size_t items;
+};
+
+struct impl {
+ struct pw_context *context;
+
+ struct pw_impl_module *module;
+ struct spa_hook module_listener;
+
+ struct pw_core *core;
+ struct pw_registry *registry;
+ struct pw_proxy *sink;
+
+ struct spa_hook core_listener;
+ struct spa_hook core_proxy_listener;
+ struct spa_hook registry_listener;
+ struct spa_hook sink_listener;
+
+ struct pw_properties *properties;
+
+ struct bitmap sink_ids;
+ struct bitmap fallback_sink_ids;
+
+ int check_seq;
+
+ unsigned int do_disconnect:1;
+ unsigned int scheduled:1;
+};
+
+static int bitmap_add(struct bitmap *map, uint32_t i)
+{
+ const uint32_t pos = (i >> 3);
+ const uint8_t mask = 1 << (i & 0x7);
+
+ if (pos >= map->size) {
+ size_t new_size = map->size + pos + 16;
+ void *p;
+
+ p = realloc(map->data, new_size);
+ if (!p)
+ return -errno;
+
+ memset((uint8_t*)p + map->size, 0, new_size - map->size);
+ map->data = p;
+ map->size = new_size;
+ }
+
+ if (map->data[pos] & mask)
+ return 1;
+
+ map->data[pos] |= mask;
+ ++map->items;
+
+ return 0;
+}
+
+static bool bitmap_remove(struct bitmap *map, uint32_t i)
+{
+ const uint32_t pos = (i >> 3);
+ const uint8_t mask = 1 << (i & 0x7);
+
+ if (pos >= map->size)
+ return false;
+
+ if (!(map->data[pos] & mask))
+ return false;
+
+ map->data[pos] &= ~mask;
+ --map->items;
+
+ return true;
+}
+
+static void bitmap_free(struct bitmap *map)
+{
+ free(map->data);
+ spa_zero(*map);
+}
+
+static int add_id(struct bitmap *map, uint32_t id)
+{
+ int res;
+
+ if (id == SPA_ID_INVALID)
+ return -EINVAL;
+
+ if ((res = bitmap_add(map, id)) < 0)
+ pw_log_error("%s", spa_strerror(res));
+
+ return res;
+}
+
+static void reschedule_check(struct impl *impl)
+{
+ if (!impl->scheduled)
+ return;
+
+ impl->check_seq = pw_core_sync(impl->core, 0, impl->check_seq);
+}
+
+static void schedule_check(struct impl *impl)
+{
+ if (impl->scheduled)
+ return;
+
+ impl->scheduled = true;
+ impl->check_seq = pw_core_sync(impl->core, 0, impl->check_seq);
+}
+
+static void sink_proxy_removed(void *data)
+{
+ struct impl *impl = data;
+
+ pw_proxy_destroy(impl->sink);
+}
+
+static void sink_proxy_bound(void *data, uint32_t id)
+{
+ struct impl *impl = data;
+
+ add_id(&impl->sink_ids, id);
+ add_id(&impl->fallback_sink_ids, id);
+
+ reschedule_check(impl);
+ schedule_check(impl);
+}
+
+static void sink_proxy_destroy(void *data)
+{
+ struct impl *impl = data;
+
+ pw_log_debug("fallback dummy sink destroyed");
+
+ spa_hook_remove(&impl->sink_listener);
+ impl->sink = NULL;
+}
+
+static const struct pw_proxy_events sink_proxy_events = {
+ PW_VERSION_PROXY_EVENTS,
+ .removed = sink_proxy_removed,
+ .bound = sink_proxy_bound,
+ .destroy = sink_proxy_destroy,
+};
+
+static int sink_create(struct impl *impl)
+{
+ if (impl->sink)
+ return 0;
+
+ pw_log_info("creating fallback dummy sink");
+
+ impl->sink = pw_core_create_object(impl->core,
+ "adapter", PW_TYPE_INTERFACE_Node, PW_VERSION_NODE,
+ impl->properties ? &impl->properties->dict : NULL, 0);
+ if (impl->sink == NULL)
+ return -errno;
+
+ pw_proxy_add_listener(impl->sink, &impl->sink_listener, &sink_proxy_events, impl);
+
+ return 0;
+}
+
+static void sink_destroy(struct impl *impl)
+{
+ if (!impl->sink)
+ return;
+
+ pw_log_info("removing fallback dummy sink");
+ pw_proxy_destroy(impl->sink);
+}
+
+static void check_sinks(struct impl *impl)
+{
+ int res;
+
+ pw_log_debug("seeing %zu sink(s), %zu fallback sink(s)",
+ impl->sink_ids.items, impl->fallback_sink_ids.items);
+
+ if (impl->sink_ids.items > impl->fallback_sink_ids.items) {
+ sink_destroy(impl);
+ } else {
+ if ((res = sink_create(impl)) < 0)
+ pw_log_error("error creating sink: %s", spa_strerror(res));
+ }
+}
+
+static void registry_event_global(void *data, uint32_t id,
+ uint32_t permissions, const char *type, uint32_t version,
+ const struct spa_dict *props)
+{
+ struct impl *impl = data;
+ const char *str;
+
+ reschedule_check(impl);
+
+ if (!props)
+ return;
+
+ if (!spa_streq(type, PW_TYPE_INTERFACE_Node))
+ return;
+
+ str = spa_dict_lookup(props, PW_KEY_MEDIA_CLASS);
+ if (!(spa_streq(str, "Audio/Sink") || spa_streq(str, "Audio/Sink/Virtual")))
+ return;
+
+ add_id(&impl->sink_ids, id);
+ schedule_check(impl);
+}
+
+static void registry_event_global_remove(void *data, uint32_t id)
+{
+ struct impl *impl = data;
+
+ reschedule_check(impl);
+
+ bitmap_remove(&impl->fallback_sink_ids, id);
+ if (bitmap_remove(&impl->sink_ids, id))
+ schedule_check(impl);
+}
+
+static const struct pw_registry_events registry_events = {
+ PW_VERSION_REGISTRY_EVENTS,
+ .global = registry_event_global,
+ .global_remove = registry_event_global_remove,
+};
+
+static void core_done(void *data, uint32_t id, int seq)
+{
+ struct impl *impl = data;
+
+ if (seq == impl->check_seq) {
+ impl->scheduled = false;
+ check_sinks(impl);
+ }
+}
+
+static const struct pw_core_events core_events = {
+ PW_VERSION_CORE_EVENTS,
+ .done = core_done,
+};
+
+static void core_proxy_removed(void *data)
+{
+ struct impl *impl = data;
+
+ if (impl->registry) {
+ spa_hook_remove(&impl->registry_listener);
+ pw_proxy_destroy((struct pw_proxy*)impl->registry);
+ impl->registry = NULL;
+ }
+}
+
+static void core_proxy_destroy(void *data)
+{
+ struct impl *impl = data;
+
+ spa_hook_remove(&impl->core_listener);
+ spa_hook_remove(&impl->core_proxy_listener);
+ impl->core = NULL;
+}
+
+static const struct pw_proxy_events core_proxy_events = {
+ PW_VERSION_PROXY_EVENTS,
+ .destroy = core_proxy_destroy,
+ .removed = core_proxy_removed,
+};
+
+static void impl_destroy(struct impl *impl)
+{
+ sink_destroy(impl);
+
+ if (impl->registry) {
+ spa_hook_remove(&impl->registry_listener);
+ pw_proxy_destroy((struct pw_proxy*)impl->registry);
+ impl->registry = NULL;
+ }
+
+ if (impl->core) {
+ spa_hook_remove(&impl->core_listener);
+ spa_hook_remove(&impl->core_proxy_listener);
+ if (impl->do_disconnect)
+ pw_core_disconnect(impl->core);
+ impl->core = NULL;
+ }
+
+ if (impl->properties) {
+ pw_properties_free(impl->properties);
+ impl->properties = NULL;
+ }
+
+ bitmap_free(&impl->sink_ids);
+ bitmap_free(&impl->fallback_sink_ids);
+
+ free(impl);
+}
+
+static void module_destroy(void *data)
+{
+ struct impl *impl = data;
+ spa_hook_remove(&impl->module_listener);
+ impl_destroy(impl);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy,
+};
+
+SPA_EXPORT
+int pipewire__module_init(struct pw_impl_module *module, const char *args)
+{
+ struct pw_context *context = pw_impl_module_get_context(module);
+ struct pw_properties *props = NULL;
+ struct impl *impl = NULL;
+ const char *str;
+ int res;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ impl = calloc(1, sizeof(struct impl));
+ if (impl == NULL)
+ goto error_errno;
+
+ pw_log_debug("module %p: new %s", impl, args);
+
+ if (args == NULL)
+ args = "";
+
+ impl->module = module;
+ impl->context = context;
+
+ props = pw_properties_new_string(args);
+ if (props == NULL)
+ goto error_errno;
+
+ impl->properties = pw_properties_new(NULL, NULL);
+ if (impl->properties == NULL)
+ goto error_errno;
+
+ if ((str = pw_properties_get(props, "sink.name")) == NULL)
+ str = DEFAULT_SINK_NAME;
+ pw_properties_set(impl->properties, PW_KEY_NODE_NAME, str);
+
+ if ((str = pw_properties_get(props, "sink.description")) == NULL)
+ str = DEFAULT_SINK_DESCRIPTION;
+ pw_properties_set(impl->properties, PW_KEY_NODE_DESCRIPTION, str);
+
+ pw_properties_setf(impl->properties, SPA_KEY_AUDIO_RATE, "%u", 48000);
+ pw_properties_setf(impl->properties, SPA_KEY_AUDIO_CHANNELS, "%u", 2);
+ pw_properties_set(impl->properties, SPA_KEY_AUDIO_POSITION, "FL,FR");
+
+ pw_properties_set(impl->properties, PW_KEY_MEDIA_CLASS, "Audio/Sink");
+ pw_properties_set(impl->properties, PW_KEY_FACTORY_NAME, "support.null-audio-sink");
+ pw_properties_set(impl->properties, PW_KEY_NODE_VIRTUAL, "true");
+ pw_properties_set(impl->properties, "monitor.channel-volumes", "true");
+
+ impl->core = pw_context_get_object(impl->context, PW_TYPE_INTERFACE_Core);
+ if (impl->core == NULL) {
+ str = pw_properties_get(props, PW_KEY_REMOTE_NAME);
+ impl->core = pw_context_connect(impl->context,
+ pw_properties_new(
+ PW_KEY_REMOTE_NAME, str,
+ NULL),
+ 0);
+ impl->do_disconnect = true;
+ }
+ if (impl->core == NULL) {
+ res = -errno;
+ pw_log_error("can't connect: %m");
+ goto error;
+ }
+
+ pw_proxy_add_listener((struct pw_proxy*)impl->core,
+ &impl->core_proxy_listener, &core_proxy_events,
+ impl);
+
+ pw_core_add_listener(impl->core, &impl->core_listener, &core_events, impl);
+
+ impl->registry = pw_core_get_registry(impl->core,
+ PW_VERSION_REGISTRY, 0);
+ if (impl->registry == NULL)
+ goto error_errno;
+
+ pw_registry_add_listener(impl->registry,
+ &impl->registry_listener,
+ &registry_events, impl);
+
+ pw_impl_module_add_listener(module, &impl->module_listener, &module_events, impl);
+
+ pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_props));
+
+ schedule_check(impl);
+
+ pw_properties_free(props);
+ return 0;
+
+error_errno:
+ res = -errno;
+error:
+ if (props)
+ pw_properties_free(props);
+ if (impl)
+ impl_destroy(impl);
+ return res;
+}
diff --git a/src/modules/module-filter-chain.c b/src/modules/module-filter-chain.c
new file mode 100644
index 0000000..fd41d20
--- /dev/null
+++ b/src/modules/module-filter-chain.c
@@ -0,0 +1,2411 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <dlfcn.h>
+#include <unistd.h>
+
+#include "config.h"
+
+#include "module-filter-chain/plugin.h"
+
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+#include <spa/utils/json.h>
+#include <spa/support/cpu.h>
+#include <spa/param/latency-utils.h>
+#include <spa/pod/dynamic.h>
+#include <spa/debug/types.h>
+
+#include <pipewire/utils.h>
+#include <pipewire/impl.h>
+#include <pipewire/extensions/profiler.h>
+
+#define NAME "filter-chain"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+/**
+ * \page page_module_filter_chain PipeWire Module: Filter-Chain
+ *
+ * The filter-chain allows you to create an arbitrary processing graph
+ * from LADSPA, LV2 and builtin filters. This filter can be made into a
+ * virtual sink/source or between any 2 nodes in the graph.
+ *
+ * The filter chain is built with 2 streams, a capture stream providing
+ * the input to the filter chain and a playback stream sending out the
+ * filtered stream to the next nodes in the graph.
+ *
+ * Because both ends of the filter-chain are built with streams, the session
+ * manager can manage the configuration and connection with the sinks and
+ * sources automatically.
+ *
+ * ## Module Options
+ *
+ * - `node.description`: a human readable name for the filter chain
+ * - `filter.graph = []`: a description of the filter graph to run, see below
+ * - `capture.props = {}`: properties to be passed to the input stream
+ * - `playback.props = {}`: properties to be passed to the output stream
+ *
+ * ## Filter graph description
+ *
+ * The general structure of the graph description is as follows:
+ *
+ *\code{.unparsed}
+ * filter.graph = {
+ * nodes = [
+ * {
+ * type = <ladspa | lv2 | builtin>
+ * name = <name>
+ * plugin = <plugin>
+ * label = <label>
+ * config = {
+ * <configkey> = <value> ...
+ * }
+ * control = {
+ * <controlname|controlindex> = <value> ...
+ * }
+ * }
+ * ...
+ * ]
+ * links = [
+ * { output = <portname> input = <portname> }
+ * ...
+ * ]
+ * inputs = [ <portname> ... ]
+ * outputs = [ <portname> ... ]
+ * }
+ *\endcode
+ *
+ * ### Nodes
+ *
+ * Nodes describe the processing filters in the graph. Use a tool like lv2ls
+ * or listplugins to get a list of available plugins, labels and the port names.
+ *
+ * - `type` is one of `ladspa`, `lv2` or `builtin`
+ * - `name` is the name for this node, you might need this later to refer to this node
+ * and its ports when setting controls or making links.
+ * - `plugin` is the type specific plugin name.
+ * - For LADSPA plugins it will append `.so` to find the shared object with that
+ * name in the LADSPA plugin path.
+ * - For LV2, this is the plugin URI obtained with lv2ls.
+ * - For builtin this is ignored
+ * - `label` is the type specific filter inside the plugin.
+ * - For LADSPA this is the label
+ * - For LV2 this is unused
+ * - For builtin this is the name of the filter to use
+ *
+ * - `config` contains a filter specific configuration section. The convolver
+ * plugin needs this.
+ * - `control` contains the initial values for the control ports of the filter.
+ *
+ * ### Links
+ *
+ * Links can be made between ports of nodes. The `portname` is given as
+ * `<node_name>:<port_name>`.
+ *
+ * You can tee the output of filters to multiple other filters. You need to
+ * use a mixer if you want the output of multiple filters to go into one
+ * filter input port.
+ *
+ * links can be omited when the graph has just 1 filter.
+ *
+ * ### Inputs and Outputs
+ *
+ * These are the entry and exit ports into the graph definition. Their number
+ * defines the number of channels used by the filter-chain.
+ *
+ * The `<portname>` can be `null` when a channel is to be ignored.
+ *
+ * Each input/output in the graph can only be linked to one filter input/output.
+ * You need to use the copy builtin filter if the stream signal needs to be routed
+ * to multiple filters. You need to use the mixer builtin plugin if multiple graph
+ * outputs need to go to one output stream.
+ *
+ * inputs and outputs can be omitted, in which case the filter-chain will use all
+ * inputs from the first filter and all outputs from the last filter node. The
+ * graph will then be duplicated as many times to match the number of input/output
+ * channels of the streams.
+ *
+ * ## Builtin filters
+ *
+ * There are some useful builtin filters available. You select them with the label
+ * of the filter node.
+ *
+ * ### Mixer
+ *
+ * Use the `mixer` plugin if you have multiple input signals that need to be mixed together.
+ *
+ * The mixer plugin has up to 8 input ports labeled "In 1" to "In 8" and each with
+ * a gain control labeled "Gain 1" to "Gain 8". There is an output port labeled
+ * "Out". Unused input ports will be ignoded and not cause overhead.
+ *
+ * ### Copy
+ *
+ * Use the `copy` plugin if you need to copy a stream input signal to multiple filters.
+ *
+ * It has one input port "In" and one output port "Out".
+ *
+ * ### Biquads
+ *
+ * Biquads can be used to do all kinds of filtering. They are also used when creating
+ * equalizers.
+ *
+ * All biquad filters have an input port "In" and an output port "Out". They have
+ * a "Freq", "Q" and "Gain" control. Their meaning depends on the particular biquad that
+ * is used. The following labels can be used:
+ *
+ * - `bq_lowpass` a lowpass filter.
+ * - `bq_highpass` a highpass filter.
+ * - `bq_bandpass` a bandpass filter.
+ * - `bq_lowshelf` a low shelf filter.
+ * - `bq_highshelf` a high shelf filter.
+ * - `bq_peaking` a peaking filter.
+ * - `bq_notch` a notch filter.
+ * - `bq_allpass` an allpass filter.
+ *
+ * ### Convolver
+ *
+ * The convolver can be used to apply an impulse response to a signal. It is usually used
+ * for reverbs or virtual surround. The convolver is implemented with a fast FFT
+ * implementation.
+ *
+ * The convolver has an input port "In" and an output port "Out". It requires a config
+ * section in the node declaration in this format:
+ *
+ *\code{.unparsed}
+ * filter.graph = {
+ * nodes = [
+ * {
+ * type = builtin
+ * name = ...
+ * label = convolver
+ * config = {
+ * blocksize = ...
+ * tailsize = ...
+ * gain = ...
+ * delay = ...
+ * filename = ...
+ * offset = ...
+ * length = ...
+ * channel = ...
+ * }
+ * ...
+ * }
+ * }
+ * ...
+ * }
+ *\endcode
+ *
+ * - `blocksize` specifies the size of the blocks to use in the FFT. It is a value
+ * between 64 and 256. When not specified, this value is
+ * computed automatically from the number of samples in the file.
+ * - `tailsize` specifies the size of the tail blocks to use in the FFT.
+ * - `gain` the overall gain to apply to the IR file.
+ * - `delay` The extra delay (in samples) to add to the IR.
+ * - `filename` The IR to load or create. Possible values are:
+ * - `/hilbert` creates a [hilbert function](https://en.wikipedia.org/wiki/Hilbert_transform)
+ * that can be used to phase shift the signal by +/-90 degrees. The
+ * `length` will be used as the number of coefficients.
+ * - `/dirac` creates a [Dirac function](https://en.wikipedia.org/wiki/Dirac_delta_function) that
+ * can be used as gain.
+ * - A filename to load as the IR. This needs to be a file format supported
+ * by sndfile.
+ * - `offset` The sample offset in the file as the start of the IR.
+ * - `length` The number of samples to use as the IR.
+ * - `channel` The channel to use from the file as the IR.
+ *
+ * ### Delay
+ *
+ * The delay can be used to delay a signal in time.
+ *
+ * The delay has an input port "In" and an output port "Out". It also has
+ * a "Delay (s)" control port. It requires a config section in the node declaration
+ * in this format:
+ *
+ *\code{.unparsed}
+ * filter.graph = {
+ * nodes = [
+ * {
+ * type = builtin
+ * name = ...
+ * label = delay
+ * config = {
+ * "max-delay" = ...
+ * }
+ * control = {
+ * "Delay (s)" = ...
+ * }
+ * ...
+ * }
+ * }
+ * ...
+ * }
+ *\endcode
+ *
+ * - `max-delay` the maximum delay in seconds. The "Delay (s)" parameter will
+ * be clamped to this value.
+ *
+ * ## General options
+ *
+ * Options with well-known behavior. Most options can be added to the global
+ * configuration or the individual streams:
+ *
+ * - \ref PW_KEY_REMOTE_NAME
+ * - \ref PW_KEY_AUDIO_RATE
+ * - \ref PW_KEY_AUDIO_CHANNELS
+ * - \ref SPA_KEY_AUDIO_POSITION
+ * - \ref PW_KEY_MEDIA_NAME
+ * - \ref PW_KEY_NODE_LATENCY
+ * - \ref PW_KEY_NODE_DESCRIPTION
+ * - \ref PW_KEY_NODE_GROUP
+ * - \ref PW_KEY_NODE_LINK_GROUP
+ * - \ref PW_KEY_NODE_VIRTUAL
+ * - \ref PW_KEY_NODE_NAME: See notes below. If not specified, defaults to
+ * 'filter-chain-<pid>-<module-id>'.
+ *
+ * Stream only properties:
+ *
+ * - \ref PW_KEY_MEDIA_CLASS
+ * - \ref PW_KEY_NODE_NAME: if not given per stream, the global node.name will be
+ * prefixed with 'input.' and 'output.' to generate a capture and playback
+ * stream node.name respectively.
+ *
+ * ## Example configuration of a virtual source
+ *
+ * This example uses the rnnoise LADSPA plugin to create a new
+ * virtual source.
+ *
+ *\code{.unparsed}
+ * context.modules = [
+ * { name = libpipewire-module-filter-chain
+ * args = {
+ * node.description = "Noise Canceling source"
+ * media.name = "Noise Canceling source"
+ * filter.graph = {
+ * nodes = [
+ * {
+ * type = ladspa
+ * name = rnnoise
+ * plugin = ladspa/librnnoise_ladspa
+ * label = noise_suppressor_stereo
+ * control = {
+ * "VAD Threshold (%)" 50.0
+ * }
+ * }
+ * ]
+ * }
+ * capture.props = {
+ * node.name = "capture.rnnoise_source"
+ * node.passive = true
+ * }
+ * playback.props = {
+ * node.name = "rnnoise_source"
+ * media.class = Audio/Source
+ * }
+ * }
+ * }
+ * ]
+ *\endcode
+ *
+ * ## Example configuration of a Dolby Surround encoder virtual Sink
+ *
+ * This example uses the ladpsa surround encoder to encode a 5.1 signal
+ * to a stereo Dolby Surround signal.
+ *
+ *\code{.unparsed}
+ *
+ *\code{.unparsed}
+ * context.modules = [
+ * { name = libpipewire-module-filter-chain
+ * args = {
+ * node.description = "Dolby Surround Sink"
+ * media.name = "Dolby Surround Sink"
+ * filter.graph = {
+ * nodes = [
+ * {
+ * type = builtin
+ * name = mixer
+ * label = mixer
+ * control = { "Gain 1" = 0.5 "Gain 2" = 0.5 }
+ * }
+ * {
+ * type = ladspa
+ * name = enc
+ * plugin = surround_encoder_1401
+ * label = surroundEncoder
+ * }
+ * ]
+ * links = [
+ * { output = "mixer:Out" input = "enc:S" }
+ * ]
+ * inputs = [ "enc:L" "enc:R" "enc:C" null "mixer:In 1" "mixer:In 2" ]
+ * outputs = [ "enc:Lt" "enc:Rt" ]
+ * }
+ * capture.props = {
+ * node.name = "effect_input.dolby_surround"
+ * media.class = Audio/Sink
+ * audio.channels = 6
+ * audio.position = [ FL FR FC LFE SL SR ]
+ * }
+ * playback.props = {
+ * node.name = "effect_output.dolby_surround"
+ * node.passive = true
+ * audio.channels = 2
+ * audio.position = [ FL FR ]
+ * }
+ * }
+ * }
+ * ]
+ *\endcode
+ */
+static const struct spa_dict_item module_props[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Create filter chain streams" },
+ { PW_KEY_MODULE_USAGE, " [ remote.name=<remote> ] "
+ "[ node.latency=<latency as fraction> ] "
+ "[ node.description=<description of the nodes> ] "
+ "[ audio.rate=<sample rate> ] "
+ "[ audio.channels=<number of channels> ] "
+ "[ audio.position=<channel map> ] "
+ "filter.graph = [ "
+ " nodes = [ "
+ " { "
+ " type = <ladspa | lv2 | builtin> "
+ " name = <name> "
+ " plugin = <plugin> "
+ " label = <label> "
+ " config = { "
+ " <configkey> = <value> ... "
+ " } "
+ " control = { "
+ " <controlname|controlindex> = <value> ... "
+ " } "
+ " } "
+ " ] "
+ " links = [ "
+ " { output = <portname> input = <portname> } ... "
+ " ] "
+ " inputs = [ <portname> ... ] "
+ " outputs = [ <portname> ... ] "
+ "] "
+ "[ capture.props=<properties> ] "
+ "[ playback.props=<properties> ] " },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+#include <unistd.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <signal.h>
+#include <getopt.h>
+#include <limits.h>
+#include <math.h>
+
+#include <spa/utils/result.h>
+#include <spa/pod/builder.h>
+#include <spa/param/audio/format-utils.h>
+#include <spa/param/audio/raw.h>
+
+#include <pipewire/pipewire.h>
+
+#define MAX_HNDL 64
+#define MAX_SAMPLES 8192
+
+static float silence_data[MAX_SAMPLES];
+static float discard_data[MAX_SAMPLES];
+
+struct plugin {
+ struct spa_list link;
+ int ref;
+ char type[64];
+ char path[PATH_MAX];
+
+ struct fc_plugin *plugin;
+ struct spa_list descriptor_list;
+};
+
+struct descriptor {
+ struct spa_list link;
+ int ref;
+ struct plugin *plugin;
+ char label[256];
+
+ const struct fc_descriptor *desc;
+
+ uint32_t n_input;
+ uint32_t n_output;
+ uint32_t n_control;
+ uint32_t n_notify;
+ unsigned long *input;
+ unsigned long *output;
+ unsigned long *control;
+ unsigned long *notify;
+ float *default_control;
+};
+
+struct port {
+ struct spa_list link;
+ struct node *node;
+
+ uint32_t idx;
+ unsigned long p;
+
+ struct spa_list link_list;
+ uint32_t n_links;
+ uint32_t external;
+
+ float control_data;
+ float *audio_data[MAX_HNDL];
+};
+
+struct node {
+ struct spa_list link;
+ struct graph *graph;
+
+ struct descriptor *desc;
+
+ char name[256];
+ char *config;
+
+ struct port *input_port;
+ struct port *output_port;
+ struct port *control_port;
+ struct port *notify_port;
+
+ uint32_t n_hndl;
+ void *hndl[MAX_HNDL];
+
+ unsigned int n_deps;
+ unsigned int visited:1;
+ unsigned int disabled:1;
+};
+
+struct link {
+ struct spa_list link;
+
+ struct spa_list input_link;
+ struct spa_list output_link;
+
+ struct port *output;
+ struct port *input;
+};
+
+struct graph_port {
+ const struct fc_descriptor *desc;
+ void **hndl;
+ uint32_t port;
+ unsigned next:1;
+};
+
+struct graph_hndl {
+ const struct fc_descriptor *desc;
+ void **hndl;
+};
+
+struct graph {
+ struct impl *impl;
+
+ struct spa_list node_list;
+ struct spa_list link_list;
+
+ uint32_t n_input;
+ struct graph_port *input;
+
+ uint32_t n_output;
+ struct graph_port *output;
+
+ uint32_t n_hndl;
+ struct graph_hndl *hndl;
+
+ uint32_t n_control;
+ struct port **control_port;
+};
+
+struct impl {
+ struct pw_context *context;
+
+ struct pw_impl_module *module;
+
+ struct spa_hook module_listener;
+
+ struct pw_core *core;
+ struct spa_hook core_proxy_listener;
+ struct spa_hook core_listener;
+
+ struct dsp_ops dsp;
+
+ struct spa_list plugin_list;
+
+ struct pw_properties *capture_props;
+ struct pw_stream *capture;
+ struct spa_hook capture_listener;
+ struct spa_audio_info_raw capture_info;
+
+ struct pw_properties *playback_props;
+ struct pw_stream *playback;
+ struct spa_hook playback_listener;
+ struct spa_audio_info_raw playback_info;
+
+ unsigned int do_disconnect:1;
+
+ long unsigned rate;
+
+ struct graph graph;
+};
+
+static int graph_instantiate(struct graph *graph);
+static void graph_cleanup(struct graph *graph);
+
+
+static void capture_destroy(void *d)
+{
+ struct impl *impl = d;
+ spa_hook_remove(&impl->capture_listener);
+ impl->capture = NULL;
+}
+
+static void capture_process(void *d)
+{
+ struct impl *impl = d;
+ pw_stream_trigger_process(impl->playback);
+}
+
+static void playback_process(void *d)
+{
+ struct impl *impl = d;
+ struct pw_buffer *in, *out;
+ struct graph *graph = &impl->graph;
+ uint32_t i, j, insize = 0, outsize = 0, n_hndl = graph->n_hndl;
+ int32_t stride = 0;
+ struct graph_port *port;
+ struct spa_data *bd;
+
+ if ((in = pw_stream_dequeue_buffer(impl->capture)) == NULL)
+ pw_log_debug("%p: out of capture buffers: %m", impl);
+
+ if ((out = pw_stream_dequeue_buffer(impl->playback)) == NULL)
+ pw_log_debug("%p: out of playback buffers: %m", impl);
+
+ if (in == NULL || out == NULL)
+ goto done;
+
+ for (i = 0, j = 0; i < in->buffer->n_datas; i++) {
+ uint32_t offs, size;
+
+ bd = &in->buffer->datas[i];
+
+ offs = SPA_MIN(bd->chunk->offset, bd->maxsize);
+ size = SPA_MIN(bd->chunk->size, bd->maxsize - offs);
+
+ while (j < graph->n_input) {
+ port = &graph->input[j++];
+ if (port->desc)
+ port->desc->connect_port(*port->hndl, port->port,
+ SPA_PTROFF(bd->data, offs, void));
+ if (!port->next)
+ break;
+
+ }
+ insize = i == 0 ? size : SPA_MIN(insize, size);
+ stride = SPA_MAX(stride, bd->chunk->stride);
+ }
+ outsize = insize;
+
+ for (i = 0; i < out->buffer->n_datas; i++) {
+ bd = &out->buffer->datas[i];
+
+ outsize = SPA_MIN(outsize, bd->maxsize);
+
+ port = i < graph->n_output ? &graph->output[i] : NULL;
+
+ if (port && port->desc)
+ port->desc->connect_port(*port->hndl, port->port, bd->data);
+ else
+ memset(bd->data, 0, outsize);
+
+ bd->chunk->offset = 0;
+ bd->chunk->size = outsize;
+ bd->chunk->stride = stride;
+ }
+
+ pw_log_trace_fp("%p: stride:%d in:%d out:%d requested:%"PRIu64" (%"PRIu64")", impl,
+ stride, insize, outsize, out->requested, out->requested * stride);
+
+ for (i = 0; i < n_hndl; i++) {
+ struct graph_hndl *hndl = &graph->hndl[i];
+ hndl->desc->run(*hndl->hndl, outsize / sizeof(float));
+ }
+
+done:
+ if (in != NULL)
+ pw_stream_queue_buffer(impl->capture, in);
+ if (out != NULL)
+ pw_stream_queue_buffer(impl->playback, out);
+}
+
+static float get_default(struct impl *impl, struct descriptor *desc, uint32_t p)
+{
+ struct fc_port *port = &desc->desc->ports[p];
+ return port->def;
+}
+
+static struct node *find_node(struct graph *graph, const char *name)
+{
+ struct node *node;
+ spa_list_for_each(node, &graph->node_list, link) {
+ if (spa_streq(node->name, name))
+ return node;
+ }
+ return NULL;
+}
+
+/* find a port by name. Valid syntax is:
+ * "<node_name>:<port_name>"
+ * "<node_name>:<port_id>"
+ * "<port_name>"
+ * "<port_id>"
+ * When no node_name is given, the port is assumed in the current node. */
+static struct port *find_port(struct node *node, const char *name, int descriptor)
+{
+ char *col, *node_name, *port_name, *str;
+ struct port *ports;
+ const struct fc_descriptor *d;
+ uint32_t i, n_ports, port_id = SPA_ID_INVALID;
+
+ str = strdupa(name);
+ col = strchr(str, ':');
+ if (col != NULL) {
+ struct node *find;
+ node_name = str;
+ port_name = col + 1;
+ *col = '\0';
+ find = find_node(node->graph, node_name);
+ if (find == NULL) {
+ /* it's possible that the : is part of the port name,
+ * try again without splitting things up. */
+ *col = ':';
+ col = NULL;
+ } else {
+ node = find;
+ }
+ }
+ if (col == NULL) {
+ node_name = node->name;
+ port_name = str;
+ }
+ if (node == NULL)
+ return NULL;
+
+ if (!spa_atou32(port_name, &port_id, 0))
+ port_id = SPA_ID_INVALID;
+
+ if (FC_IS_PORT_INPUT(descriptor)) {
+ if (FC_IS_PORT_CONTROL(descriptor)) {
+ ports = node->control_port;
+ n_ports = node->desc->n_control;
+ } else {
+ ports = node->input_port;
+ n_ports = node->desc->n_input;
+ }
+ } else if (FC_IS_PORT_OUTPUT(descriptor)) {
+ if (FC_IS_PORT_CONTROL(descriptor)) {
+ ports = node->notify_port;
+ n_ports = node->desc->n_notify;
+ } else {
+ ports = node->output_port;
+ n_ports = node->desc->n_output;
+ }
+ } else
+ return NULL;
+
+ d = node->desc->desc;
+ for (i = 0; i < n_ports; i++) {
+ struct port *port = &ports[i];
+ if (i == port_id ||
+ spa_streq(d->ports[port->p].name, port_name))
+ return port;
+ }
+ return NULL;
+}
+
+static struct spa_pod *get_prop_info(struct graph *graph, struct spa_pod_builder *b, uint32_t idx)
+{
+ struct impl *impl = graph->impl;
+ struct spa_pod_frame f[2];
+ struct port *port = graph->control_port[idx];
+ struct node *node = port->node;
+ struct descriptor *desc = node->desc;
+ const struct fc_descriptor *d = desc->desc;
+ struct fc_port *p = &d->ports[port->p];
+ float def, min, max;
+ char name[512];
+ uint32_t rate = impl->rate ? impl->rate : 48000;
+
+ if (p->hint & FC_HINT_SAMPLE_RATE) {
+ def = p->def * rate;
+ min = p->min * rate;
+ max = p->max * rate;
+ } else {
+ def = p->def;
+ min = p->min;
+ max = p->max;
+ }
+
+ if (node->name[0] != '\0')
+ snprintf(name, sizeof(name), "%s:%s", node->name, p->name);
+ else
+ snprintf(name, sizeof(name), "%s", p->name);
+
+ spa_pod_builder_push_object(b, &f[0],
+ SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo);
+ spa_pod_builder_add (b,
+ SPA_PROP_INFO_name, SPA_POD_String(name),
+ 0);
+ spa_pod_builder_prop(b, SPA_PROP_INFO_type, 0);
+ if (p->hint & FC_HINT_BOOLEAN) {
+ if (min == max) {
+ spa_pod_builder_bool(b, def <= 0.0f ? false : true);
+ } else {
+ spa_pod_builder_push_choice(b, &f[1], SPA_CHOICE_Enum, 0);
+ spa_pod_builder_bool(b, def <= 0.0f ? false : true);
+ spa_pod_builder_bool(b, false);
+ spa_pod_builder_bool(b, true);
+ spa_pod_builder_pop(b, &f[1]);
+ }
+ } else if (p->hint & FC_HINT_INTEGER) {
+ if (min == max) {
+ spa_pod_builder_int(b, def);
+ } else {
+ spa_pod_builder_push_choice(b, &f[1], SPA_CHOICE_Range, 0);
+ spa_pod_builder_int(b, def);
+ spa_pod_builder_int(b, min);
+ spa_pod_builder_int(b, max);
+ spa_pod_builder_pop(b, &f[1]);
+ }
+ } else {
+ if (min == max) {
+ spa_pod_builder_float(b, def);
+ } else {
+ spa_pod_builder_push_choice(b, &f[1], SPA_CHOICE_Range, 0);
+ spa_pod_builder_float(b, def);
+ spa_pod_builder_float(b, min);
+ spa_pod_builder_float(b, max);
+ spa_pod_builder_pop(b, &f[1]);
+ }
+ }
+ spa_pod_builder_prop(b, SPA_PROP_INFO_params, 0);
+ spa_pod_builder_bool(b, true);
+ return spa_pod_builder_pop(b, &f[0]);
+}
+
+static struct spa_pod *get_props_param(struct graph *graph, struct spa_pod_builder *b)
+{
+ struct spa_pod_frame f[2];
+ uint32_t i;
+ char name[512];
+
+ spa_pod_builder_push_object(b, &f[0],
+ SPA_TYPE_OBJECT_Props, SPA_PARAM_Props);
+ spa_pod_builder_prop(b, SPA_PROP_params, 0);
+ spa_pod_builder_push_struct(b, &f[1]);
+
+ for (i = 0; i < graph->n_control; i++) {
+ struct port *port = graph->control_port[i];
+ struct node *node = port->node;
+ struct descriptor *desc = node->desc;
+ const struct fc_descriptor *d = desc->desc;
+ struct fc_port *p = &d->ports[port->p];
+
+ if (node->name[0] != '\0')
+ snprintf(name, sizeof(name), "%s:%s", node->name, p->name);
+ else
+ snprintf(name, sizeof(name), "%s", p->name);
+
+ spa_pod_builder_string(b, name);
+ if (p->hint & FC_HINT_BOOLEAN) {
+ spa_pod_builder_bool(b, port->control_data <= 0.0f ? false : true);
+ } else if (p->hint & FC_HINT_INTEGER) {
+ spa_pod_builder_int(b, port->control_data);
+ } else {
+ spa_pod_builder_float(b, port->control_data);
+ }
+ }
+ spa_pod_builder_pop(b, &f[1]);
+ return spa_pod_builder_pop(b, &f[0]);
+}
+
+static int set_control_value(struct node *node, const char *name, float *value)
+{
+ struct descriptor *desc;
+ struct port *port;
+ float old;
+
+ port = find_port(node, name, FC_PORT_INPUT | FC_PORT_CONTROL);
+ if (port == NULL)
+ return -ENOENT;
+
+ node = port->node;
+ desc = node->desc;
+
+ old = port->control_data;
+ port->control_data = value ? *value : desc->default_control[port->idx];
+ pw_log_info("control %d ('%s') from %f to %f", port->idx, name, old, port->control_data);
+ return old == port->control_data ? 0 : 1;
+}
+
+static int parse_params(struct graph *graph, const struct spa_pod *pod)
+{
+ struct spa_pod_parser prs;
+ struct spa_pod_frame f;
+ int res, changed = 0;
+ struct node *def_node;
+
+ def_node = spa_list_first(&graph->node_list, struct node, link);
+
+ spa_pod_parser_pod(&prs, pod);
+ if (spa_pod_parser_push_struct(&prs, &f) < 0)
+ return 0;
+
+ while (true) {
+ const char *name;
+ float value, *val = NULL;
+ double dbl_val;
+ bool bool_val;
+ int32_t int_val;
+
+ if (spa_pod_parser_get_string(&prs, &name) < 0)
+ break;
+ if (spa_pod_parser_get_float(&prs, &value) >= 0) {
+ val = &value;
+ } else if (spa_pod_parser_get_double(&prs, &dbl_val) >= 0) {
+ value = dbl_val;
+ val = &value;
+ } else if (spa_pod_parser_get_int(&prs, &int_val) >= 0) {
+ value = int_val;
+ val = &value;
+ } else if (spa_pod_parser_get_bool(&prs, &bool_val) >= 0) {
+ value = bool_val ? 1.0f : 0.0f;
+ val = &value;
+ } else {
+ struct spa_pod *pod;
+ spa_pod_parser_get_pod(&prs, &pod);
+ }
+ if ((res = set_control_value(def_node, name, val)) > 0)
+ changed += res;
+ }
+ return changed;
+}
+
+static void graph_reset(struct graph *graph)
+{
+ uint32_t i;
+ for (i = 0; i < graph->n_hndl; i++) {
+ struct graph_hndl *hndl = &graph->hndl[i];
+ const struct fc_descriptor *d = hndl->desc;
+ if (hndl->hndl == NULL || *hndl->hndl == NULL)
+ continue;
+ if (d->deactivate)
+ d->deactivate(*hndl->hndl);
+ if (d->activate)
+ d->activate(*hndl->hndl);
+ }
+}
+static void param_props_changed(struct impl *impl, const struct spa_pod *param)
+{
+ struct spa_pod_object *obj = (struct spa_pod_object *) param;
+ const struct spa_pod_prop *prop;
+ struct graph *graph = &impl->graph;
+ int changed = 0;
+
+ SPA_POD_OBJECT_FOREACH(obj, prop) {
+ if (prop->key == SPA_PROP_params)
+ changed += parse_params(graph, &prop->value);
+ }
+ if (changed > 0) {
+ uint8_t buffer[1024];
+ struct spa_pod_dynamic_builder b;
+ const struct spa_pod *params[1];
+
+ spa_pod_dynamic_builder_init(&b, buffer, sizeof(buffer), 4096);
+ params[0] = get_props_param(graph, &b.b);
+
+ pw_stream_update_params(impl->capture, params, 1);
+ spa_pod_dynamic_builder_clean(&b);
+ }
+}
+
+static void param_latency_changed(struct impl *impl, const struct spa_pod *param)
+{
+ struct spa_latency_info latency;
+ uint8_t buffer[1024];
+ struct spa_pod_builder b;
+ const struct spa_pod *params[1];
+
+ if (spa_latency_parse(param, &latency) < 0)
+ return;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ params[0] = spa_latency_build(&b, SPA_PARAM_Latency, &latency);
+
+ if (latency.direction == SPA_DIRECTION_INPUT)
+ pw_stream_update_params(impl->capture, params, 1);
+ else
+ pw_stream_update_params(impl->playback, params, 1);
+}
+
+static void state_changed(void *data, enum pw_stream_state old,
+ enum pw_stream_state state, const char *error)
+{
+ struct impl *impl = data;
+ struct graph *graph = &impl->graph;
+
+ switch (state) {
+ case PW_STREAM_STATE_PAUSED:
+ pw_stream_flush(impl->playback, false);
+ pw_stream_flush(impl->capture, false);
+ graph_reset(graph);
+ break;
+ case PW_STREAM_STATE_UNCONNECTED:
+ pw_log_info("module %p: unconnected", impl);
+ pw_impl_module_schedule_destroy(impl->module);
+ break;
+ case PW_STREAM_STATE_ERROR:
+ pw_log_info("module %p: error: %s", impl, error);
+ break;
+ default:
+ break;
+ }
+}
+
+static void param_changed(void *data, uint32_t id, const struct spa_pod *param)
+{
+ struct impl *impl = data;
+ struct graph *graph = &impl->graph;
+ int res;
+
+ switch (id) {
+ case SPA_PARAM_Format:
+ if (param == NULL) {
+ graph_cleanup(graph);
+ } else {
+ struct spa_audio_info_raw info;
+ spa_zero(info);
+ if ((res = spa_format_audio_raw_parse(param, &info)) < 0)
+ goto error;
+ if (info.rate == 0) {
+ res = -EINVAL;
+ goto error;
+ }
+ impl->rate = info.rate;
+ if ((res = graph_instantiate(graph)) < 0)
+ goto error;
+ }
+ break;
+ case SPA_PARAM_Props:
+ if (param != NULL)
+ param_props_changed(impl, param);
+ break;
+ case SPA_PARAM_Latency:
+ param_latency_changed(impl, param);
+ break;
+ }
+ return;
+
+error:
+ pw_stream_set_error(impl->capture, res, "can't start graph: %s",
+ spa_strerror(res));
+}
+
+static const struct pw_stream_events in_stream_events = {
+ PW_VERSION_STREAM_EVENTS,
+ .destroy = capture_destroy,
+ .process = capture_process,
+ .state_changed = state_changed,
+ .param_changed = param_changed
+};
+
+static void playback_destroy(void *d)
+{
+ struct impl *impl = d;
+ spa_hook_remove(&impl->playback_listener);
+ impl->playback = NULL;
+}
+
+static const struct pw_stream_events out_stream_events = {
+ PW_VERSION_STREAM_EVENTS,
+ .destroy = playback_destroy,
+ .process = playback_process,
+ .state_changed = state_changed,
+ .param_changed = param_changed
+};
+
+static int setup_streams(struct impl *impl)
+{
+ int res;
+ uint32_t i, n_params, *offs;
+ struct pw_array offsets;
+ const struct spa_pod **params = NULL;
+ struct spa_pod_dynamic_builder b;
+ struct graph *graph = &impl->graph;
+
+ impl->capture = pw_stream_new(impl->core,
+ "filter capture", impl->capture_props);
+ impl->capture_props = NULL;
+ if (impl->capture == NULL)
+ return -errno;
+
+ pw_stream_add_listener(impl->capture,
+ &impl->capture_listener,
+ &in_stream_events, impl);
+
+ impl->playback = pw_stream_new(impl->core,
+ "filter playback", impl->playback_props);
+ impl->playback_props = NULL;
+ if (impl->playback == NULL)
+ return -errno;
+
+ pw_stream_add_listener(impl->playback,
+ &impl->playback_listener,
+ &out_stream_events, impl);
+
+ spa_pod_dynamic_builder_init(&b, NULL, 0, 4096);
+ pw_array_init(&offsets, 512);
+
+ if ((offs = pw_array_add(&offsets, sizeof(uint32_t))) == NULL) {
+ res = -errno;
+ goto done;
+ }
+ *offs = b.b.state.offset;
+ spa_format_audio_raw_build(&b.b,
+ SPA_PARAM_EnumFormat, &impl->capture_info);
+
+ for (i = 0; i < graph->n_control; i++) {
+ if ((offs = pw_array_add(&offsets, sizeof(uint32_t))) != NULL)
+ *offs = b.b.state.offset;
+ get_prop_info(graph, &b.b, i);
+ }
+
+ if ((offs = pw_array_add(&offsets, sizeof(uint32_t))) != NULL)
+ *offs = b.b.state.offset;
+ get_props_param(graph, &b.b);
+
+ n_params = pw_array_get_len(&offsets, uint32_t);
+ if (n_params == 0) {
+ res = -ENOMEM;
+ goto done;
+ }
+ if ((params = calloc(n_params, sizeof(struct spa_pod*))) == NULL) {
+ res = -errno;
+ goto done;
+ }
+
+ offs = offsets.data;
+ for (i = 0; i < n_params; i++)
+ params[i] = spa_pod_builder_deref(&b.b, offs[i]);
+
+ res = pw_stream_connect(impl->capture,
+ PW_DIRECTION_INPUT,
+ PW_ID_ANY,
+ PW_STREAM_FLAG_AUTOCONNECT |
+ PW_STREAM_FLAG_MAP_BUFFERS |
+ PW_STREAM_FLAG_RT_PROCESS,
+ params, n_params);
+
+ spa_pod_dynamic_builder_clean(&b);
+ if (res < 0)
+ goto done;
+
+ n_params = 0;
+ spa_pod_dynamic_builder_init(&b, NULL, 0, 4096);
+ params[n_params++] = spa_format_audio_raw_build(&b.b,
+ SPA_PARAM_EnumFormat, &impl->playback_info);
+
+ res = pw_stream_connect(impl->playback,
+ PW_DIRECTION_OUTPUT,
+ PW_ID_ANY,
+ PW_STREAM_FLAG_AUTOCONNECT |
+ PW_STREAM_FLAG_MAP_BUFFERS |
+ PW_STREAM_FLAG_RT_PROCESS |
+ PW_STREAM_FLAG_TRIGGER,
+ params, n_params);
+ spa_pod_dynamic_builder_clean(&b);
+
+done:
+ free(params);
+ pw_array_clear(&offsets);
+
+ return res < 0 ? res : 0;
+}
+
+static uint32_t count_array(struct spa_json *json)
+{
+ struct spa_json it = *json;
+ char v[256];
+ uint32_t count = 0;
+ while (spa_json_get_string(&it, v, sizeof(v)) > 0)
+ count++;
+ return count;
+}
+
+static void plugin_unref(struct plugin *hndl)
+{
+ if (--hndl->ref > 0)
+ return;
+
+ fc_plugin_free(hndl->plugin);
+
+ spa_list_remove(&hndl->link);
+ free(hndl);
+}
+
+static struct plugin *plugin_load(struct impl *impl, const char *type, const char *path)
+{
+ struct fc_plugin *pl = NULL;
+ struct plugin *hndl;
+ const struct spa_support *support;
+ uint32_t n_support;
+
+ spa_list_for_each(hndl, &impl->plugin_list, link) {
+ if (spa_streq(hndl->type, type) &&
+ spa_streq(hndl->path, path)) {
+ hndl->ref++;
+ return hndl;
+ }
+ }
+ support = pw_context_get_support(impl->context, &n_support);
+
+ if (spa_streq(type, "builtin")) {
+ pl = load_builtin_plugin(support, n_support, &impl->dsp, path, NULL);
+ }
+ else if (spa_streq(type, "ladspa")) {
+ pl = load_ladspa_plugin(support, n_support, &impl->dsp, path, NULL);
+ }
+ else if (spa_streq(type, "lv2")) {
+#ifdef HAVE_LILV
+ pl = load_lv2_plugin(support, n_support, &impl->dsp, path, NULL);
+#else
+ pw_log_error("filter-chain is compiled without lv2 support");
+ pl = NULL;
+ errno = ENOTSUP;
+#endif
+ } else {
+ pw_log_error("invalid plugin type '%s'", type);
+ pl = NULL;
+ errno = EINVAL;
+ }
+ if (pl == NULL)
+ goto exit;
+
+ hndl = calloc(1, sizeof(*hndl));
+ if (!hndl)
+ return NULL;
+
+ hndl->ref = 1;
+ snprintf(hndl->type, sizeof(hndl->type), "%s", type);
+ snprintf(hndl->path, sizeof(hndl->path), "%s", path);
+
+ pw_log_info("successfully opened '%s'", path);
+
+ hndl->plugin = pl;
+
+ spa_list_init(&hndl->descriptor_list);
+ spa_list_append(&impl->plugin_list, &hndl->link);
+
+ return hndl;
+exit:
+ return NULL;
+}
+
+static void descriptor_unref(struct descriptor *desc)
+{
+ if (--desc->ref > 0)
+ return;
+
+ spa_list_remove(&desc->link);
+ plugin_unref(desc->plugin);
+ if (desc->desc)
+ fc_descriptor_free(desc->desc);
+ free(desc->input);
+ free(desc->output);
+ free(desc->control);
+ free(desc->default_control);
+ free(desc->notify);
+ free(desc);
+}
+
+static struct descriptor *descriptor_load(struct impl *impl, const char *type,
+ const char *plugin, const char *label)
+{
+ struct plugin *hndl;
+ struct descriptor *desc;
+ const struct fc_descriptor *d;
+ uint32_t i, n_input, n_output, n_control, n_notify;
+ unsigned long p;
+ int res;
+
+ if ((hndl = plugin_load(impl, type, plugin)) == NULL)
+ return NULL;
+
+ spa_list_for_each(desc, &hndl->descriptor_list, link) {
+ if (spa_streq(desc->label, label)) {
+ desc->ref++;
+
+ /*
+ * since ladspa_handle_load() increments the reference count of the handle,
+ * if the descriptor is found, then the handle's reference count
+ * has already been incremented to account for the descriptor,
+ * so we need to unref handle here since we're merely reusing
+ * thedescriptor, not creating a new one
+ */
+ plugin_unref(hndl);
+ return desc;
+ }
+ }
+
+ desc = calloc(1, sizeof(*desc));
+ desc->ref = 1;
+ desc->plugin = hndl;
+ spa_list_init(&desc->link);
+
+ if ((d = hndl->plugin->make_desc(hndl->plugin, label)) == NULL) {
+ pw_log_error("cannot find label %s", label);
+ res = -ENOENT;
+ goto exit;
+ }
+ desc->desc = d;
+ snprintf(desc->label, sizeof(desc->label), "%s", label);
+
+ n_input = n_output = n_control = n_notify = 0;
+ for (p = 0; p < d->n_ports; p++) {
+ struct fc_port *fp = &d->ports[p];
+ if (FC_IS_PORT_AUDIO(fp->flags)) {
+ if (FC_IS_PORT_INPUT(fp->flags))
+ n_input++;
+ else if (FC_IS_PORT_OUTPUT(fp->flags))
+ n_output++;
+ } else if (FC_IS_PORT_CONTROL(fp->flags)) {
+ if (FC_IS_PORT_INPUT(fp->flags))
+ n_control++;
+ else if (FC_IS_PORT_OUTPUT(fp->flags))
+ n_notify++;
+ }
+ }
+ desc->input = calloc(n_input, sizeof(unsigned long));
+ desc->output = calloc(n_output, sizeof(unsigned long));
+ desc->control = calloc(n_control, sizeof(unsigned long));
+ desc->default_control = calloc(n_control, sizeof(float));
+ desc->notify = calloc(n_notify, sizeof(unsigned long));
+
+ for (p = 0; p < d->n_ports; p++) {
+ struct fc_port *fp = &d->ports[p];
+
+ if (FC_IS_PORT_AUDIO(fp->flags)) {
+ if (FC_IS_PORT_INPUT(fp->flags)) {
+ pw_log_info("using port %lu ('%s') as input %d", p,
+ fp->name, desc->n_input);
+ desc->input[desc->n_input++] = p;
+ }
+ else if (FC_IS_PORT_OUTPUT(fp->flags)) {
+ pw_log_info("using port %lu ('%s') as output %d", p,
+ fp->name, desc->n_output);
+ desc->output[desc->n_output++] = p;
+ }
+ } else if (FC_IS_PORT_CONTROL(fp->flags)) {
+ if (FC_IS_PORT_INPUT(fp->flags)) {
+ pw_log_info("using port %lu ('%s') as control %d", p,
+ fp->name, desc->n_control);
+ desc->control[desc->n_control++] = p;
+ }
+ else if (FC_IS_PORT_OUTPUT(fp->flags)) {
+ pw_log_info("using port %lu ('%s') as notify %d", p,
+ fp->name, desc->n_notify);
+ desc->notify[desc->n_notify++] = p;
+ }
+ }
+ }
+ if (desc->n_input == 0 && desc->n_output == 0) {
+ pw_log_error("plugin has no input and no output ports");
+ res = -ENOTSUP;
+ goto exit;
+ }
+ for (i = 0; i < desc->n_control; i++) {
+ p = desc->control[i];
+ desc->default_control[i] = get_default(impl, desc, p);
+ pw_log_info("control %d ('%s') default to %f", i,
+ d->ports[p].name, desc->default_control[i]);
+ }
+ spa_list_append(&hndl->descriptor_list, &desc->link);
+
+ return desc;
+
+exit:
+ descriptor_unref(desc);
+ errno = -res;
+ return NULL;
+}
+
+/**
+ * {
+ * ...
+ * }
+ */
+static int parse_config(struct node *node, struct spa_json *config)
+{
+ const char *val;
+ int len;
+
+ if ((len = spa_json_next(config, &val)) <= 0)
+ return len;
+
+ if (spa_json_is_null(val, len))
+ return 0;
+
+ if (spa_json_is_container(val, len))
+ len = spa_json_container_len(config, val, len);
+
+ if ((node->config = malloc(len+1)) == NULL)
+ return -errno;
+
+ spa_json_parse_stringn(val, len, node->config, len+1);
+
+ return 0;
+}
+
+/**
+ * {
+ * "Reverb tail" = 2.0
+ * ...
+ * }
+ */
+static int parse_control(struct node *node, struct spa_json *control)
+{
+ char key[256];
+
+ while (spa_json_get_string(control, key, sizeof(key)) > 0) {
+ float fl;
+ const char *val;
+ int res, len;
+
+ if ((len = spa_json_next(control, &val)) < 0)
+ break;
+
+ if (spa_json_parse_float(val, len, &fl) <= 0) {
+ pw_log_warn("control '%s' expects a number, ignoring", key);
+ }
+ else if ((res = set_control_value(node, key, &fl)) < 0) {
+ pw_log_warn("control '%s' can not be set: %s", key, spa_strerror(res));
+ }
+ }
+ return 0;
+}
+
+/**
+ * output = [name:][portname]
+ * input = [name:][portname]
+ * ...
+ */
+static int parse_link(struct graph *graph, struct spa_json *json)
+{
+ char key[256];
+ char output[256] = "";
+ char input[256] = "";
+ const char *val;
+ struct node *def_node;
+ struct port *in_port, *out_port;
+ struct link *link;
+
+ if (spa_list_is_empty(&graph->node_list)) {
+ pw_log_error("can't make links in graph without nodes");
+ return -EINVAL;
+ }
+
+ while (spa_json_get_string(json, key, sizeof(key)) > 0) {
+ if (spa_streq(key, "output")) {
+ if (spa_json_get_string(json, output, sizeof(output)) <= 0) {
+ pw_log_error("output expects a string");
+ return -EINVAL;
+ }
+ }
+ else if (spa_streq(key, "input")) {
+ if (spa_json_get_string(json, input, sizeof(input)) <= 0) {
+ pw_log_error("input expects a string");
+ return -EINVAL;
+ }
+ }
+ else if (spa_json_next(json, &val) < 0)
+ break;
+ }
+ def_node = spa_list_first(&graph->node_list, struct node, link);
+ if ((out_port = find_port(def_node, output, FC_PORT_OUTPUT)) == NULL) {
+ pw_log_error("unknown output port %s", output);
+ return -ENOENT;
+ }
+ def_node = spa_list_last(&graph->node_list, struct node, link);
+ if ((in_port = find_port(def_node, input, FC_PORT_INPUT)) == NULL) {
+ pw_log_error("unknown input port %s", input);
+ return -ENOENT;
+ }
+ if (in_port->n_links > 0) {
+ pw_log_info("Can't have more than 1 link to %s, use a mixer", input);
+ return -ENOTSUP;
+ }
+
+ if ((link = calloc(1, sizeof(*link))) == NULL)
+ return -errno;
+
+ link->output = out_port;
+ link->input = in_port;
+
+ pw_log_info("linking %s:%s -> %s:%s",
+ out_port->node->name,
+ out_port->node->desc->desc->ports[out_port->p].name,
+ in_port->node->name,
+ in_port->node->desc->desc->ports[in_port->p].name);
+
+ spa_list_append(&out_port->link_list, &link->output_link);
+ out_port->n_links++;
+ spa_list_append(&in_port->link_list, &link->input_link);
+ in_port->n_links++;
+
+ in_port->node->n_deps++;
+
+ spa_list_append(&graph->link_list, &link->link);
+
+ return 0;
+}
+
+static void link_free(struct link *link)
+{
+ spa_list_remove(&link->input_link);
+ link->input->n_links--;
+ link->input->node->n_deps--;
+ spa_list_remove(&link->output_link);
+ link->output->n_links--;
+ spa_list_remove(&link->link);
+ free(link);
+}
+
+/**
+ * type = ladspa
+ * name = rev
+ * plugin = g2reverb
+ * label = G2reverb
+ * config = {
+ * ...
+ * }
+ * control = {
+ * ...
+ * }
+ */
+static int load_node(struct graph *graph, struct spa_json *json)
+{
+ struct spa_json control, config;
+ struct descriptor *desc;
+ struct node *node;
+ const char *val;
+ char key[256];
+ char type[256] = "";
+ char name[256] = "";
+ char plugin[256] = "";
+ char label[256] = "";
+ bool have_control = false;
+ bool have_config = false;
+ uint32_t i;
+ int res;
+
+ while (spa_json_get_string(json, key, sizeof(key)) > 0) {
+ if (spa_streq("type", key)) {
+ if (spa_json_get_string(json, type, sizeof(type)) <= 0) {
+ pw_log_error("type expects a string");
+ return -EINVAL;
+ }
+ } else if (spa_streq("name", key)) {
+ if (spa_json_get_string(json, name, sizeof(name)) <= 0) {
+ pw_log_error("name expects a string");
+ return -EINVAL;
+ }
+ } else if (spa_streq("plugin", key)) {
+ if (spa_json_get_string(json, plugin, sizeof(plugin)) <= 0) {
+ pw_log_error("plugin expects a string");
+ return -EINVAL;
+ }
+ } else if (spa_streq("label", key)) {
+ if (spa_json_get_string(json, label, sizeof(label)) <= 0) {
+ pw_log_error("label expects a string");
+ return -EINVAL;
+ }
+ } else if (spa_streq("control", key)) {
+ if (spa_json_enter_object(json, &control) <= 0) {
+ pw_log_error("control expects an object");
+ return -EINVAL;
+ }
+ have_control = true;
+ } else if (spa_streq("config", key)) {
+ config = SPA_JSON_SAVE(json);
+ have_config = true;
+ if (spa_json_next(json, &val) < 0)
+ break;
+ } else if (spa_json_next(json, &val) < 0)
+ break;
+ }
+
+ if (spa_streq(type, "builtin"))
+ snprintf(plugin, sizeof(plugin), "%s", "builtin");
+
+ pw_log_info("loading type:%s plugin:%s label:%s", type, plugin, label);
+
+ if ((desc = descriptor_load(graph->impl, type, plugin, label)) == NULL)
+ return -errno;
+
+ node = calloc(1, sizeof(*node));
+ if (node == NULL)
+ return -errno;
+
+ node->graph = graph;
+ node->desc = desc;
+ snprintf(node->name, sizeof(node->name), "%s", name);
+
+ node->input_port = calloc(desc->n_input, sizeof(struct port));
+ node->output_port = calloc(desc->n_output, sizeof(struct port));
+ node->control_port = calloc(desc->n_control, sizeof(struct port));
+ node->notify_port = calloc(desc->n_notify, sizeof(struct port));
+
+ pw_log_info("loaded n_input:%d n_output:%d n_control:%d n_notify:%d",
+ desc->n_input, desc->n_output,
+ desc->n_control, desc->n_notify);
+
+ for (i = 0; i < desc->n_input; i++) {
+ struct port *port = &node->input_port[i];
+ port->node = node;
+ port->idx = i;
+ port->external = SPA_ID_INVALID;
+ port->p = desc->input[i];
+ spa_list_init(&port->link_list);
+ }
+ for (i = 0; i < desc->n_output; i++) {
+ struct port *port = &node->output_port[i];
+ port->node = node;
+ port->idx = i;
+ port->external = SPA_ID_INVALID;
+ port->p = desc->output[i];
+ spa_list_init(&port->link_list);
+ }
+ for (i = 0; i < desc->n_control; i++) {
+ struct port *port = &node->control_port[i];
+ port->node = node;
+ port->idx = i;
+ port->external = SPA_ID_INVALID;
+ port->p = desc->control[i];
+ spa_list_init(&port->link_list);
+ port->control_data = desc->default_control[i];
+ }
+ for (i = 0; i < desc->n_notify; i++) {
+ struct port *port = &node->notify_port[i];
+ port->node = node;
+ port->idx = i;
+ port->external = SPA_ID_INVALID;
+ port->p = desc->notify[i];
+ spa_list_init(&port->link_list);
+ }
+ if (have_config)
+ if ((res = parse_config(node, &config)) < 0)
+ pw_log_warn("error parsing config: %s", spa_strerror(res));
+ if (have_control)
+ parse_control(node, &control);
+
+ spa_list_append(&graph->node_list, &node->link);
+
+ return 0;
+}
+
+static void node_cleanup(struct node *node)
+{
+ const struct fc_descriptor *d = node->desc->desc;
+ uint32_t i;
+
+ for (i = 0; i < node->n_hndl; i++) {
+ if (node->hndl[i] == NULL)
+ continue;
+ if (d->deactivate)
+ d->deactivate(node->hndl[i]);
+ d->cleanup(node->hndl[i]);
+ node->hndl[i] = NULL;
+ }
+}
+
+static int port_ensure_data(struct port *port, uint32_t i)
+{
+ float *data;
+ if ((data = port->audio_data[i]) == NULL) {
+ data = calloc(1, MAX_SAMPLES * sizeof(float));
+ if (data == NULL) {
+ pw_log_error("cannot create port data: %m");
+ return -errno;
+ }
+ }
+ port->audio_data[i] = data;
+ return 0;
+}
+
+static void port_free_data(struct port *port, uint32_t i)
+{
+ free(port->audio_data[i]);
+ port->audio_data[i] = NULL;
+}
+
+static void node_free(struct node *node)
+{
+ uint32_t i, j;
+
+ spa_list_remove(&node->link);
+ for (i = 0; i < node->n_hndl; i++) {
+ for (j = 0; j < node->desc->n_output; j++)
+ port_free_data(&node->output_port[j], i);
+ }
+ node_cleanup(node);
+ descriptor_unref(node->desc);
+ free(node->input_port);
+ free(node->output_port);
+ free(node->control_port);
+ free(node->notify_port);
+ free(node);
+}
+
+static void graph_cleanup(struct graph *graph)
+{
+ struct node *node;
+ spa_list_for_each(node, &graph->node_list, link)
+ node_cleanup(node);
+}
+
+static int graph_instantiate(struct graph *graph)
+{
+ struct impl *impl = graph->impl;
+ struct node *node;
+ struct port *port;
+ struct link *link;
+ struct descriptor *desc;
+ const struct fc_descriptor *d;
+ uint32_t i, j;
+ int res;
+
+ spa_list_for_each(node, &graph->node_list, link) {
+ float *sd = silence_data, *dd = discard_data;
+
+ node_cleanup(node);
+
+ desc = node->desc;
+ d = desc->desc;
+ if (d->flags & FC_DESCRIPTOR_SUPPORTS_NULL_DATA)
+ sd = dd = NULL;
+
+ for (i = 0; i < node->n_hndl; i++) {
+ pw_log_info("instantiate %s %d rate:%lu", d->name, i, impl->rate);
+ if ((node->hndl[i] = d->instantiate(d, impl->rate, i, node->config)) == NULL) {
+ pw_log_error("cannot create plugin instance: %m");
+ res = -errno;
+ goto error;
+ }
+ for (j = 0; j < desc->n_input; j++) {
+ port = &node->input_port[j];
+ d->connect_port(node->hndl[i], port->p, sd);
+
+ spa_list_for_each(link, &port->link_list, input_link) {
+ struct port *peer = link->output;
+ if ((res = port_ensure_data(peer, i)) < 0)
+ goto error;
+ pw_log_info("connect input port %s[%d]:%s %p",
+ node->name, i, d->ports[port->p].name,
+ peer->audio_data[i]);
+ d->connect_port(node->hndl[i], port->p, peer->audio_data[i]);
+ }
+ }
+ for (j = 0; j < desc->n_output; j++) {
+ port = &node->output_port[j];
+ if ((res = port_ensure_data(port, i)) < 0)
+ goto error;
+ pw_log_info("connect output port %s[%d]:%s %p",
+ node->name, i, d->ports[port->p].name,
+ port->audio_data[i]);
+ d->connect_port(node->hndl[i], port->p, port->audio_data[i]);
+ }
+ for (j = 0; j < desc->n_control; j++) {
+ port = &node->control_port[j];
+ d->connect_port(node->hndl[i], port->p, &port->control_data);
+ }
+ for (j = 0; j < desc->n_notify; j++) {
+ port = &node->notify_port[j];
+ d->connect_port(node->hndl[i], port->p, &port->control_data);
+ }
+ if (d->activate)
+ d->activate(node->hndl[i]);
+ }
+ }
+ return 0;
+error:
+ graph_cleanup(graph);
+ return res;
+}
+
+static struct node *find_next_node(struct graph *graph)
+{
+ struct node *node;
+ spa_list_for_each(node, &graph->node_list, link) {
+ if (node->n_deps == 0 && !node->visited) {
+ node->visited = true;
+ return node;
+ }
+ }
+ return NULL;
+}
+
+static int setup_graph(struct graph *graph, struct spa_json *inputs, struct spa_json *outputs)
+{
+ struct impl *impl = graph->impl;
+ struct node *node, *first, *last;
+ struct port *port;
+ struct link *link;
+ struct graph_port *gp;
+ struct graph_hndl *gh;
+ uint32_t i, j, n_nodes, n_input, n_output, n_control, n_hndl = 0;
+ int res;
+ struct descriptor *desc;
+ const struct fc_descriptor *d;
+ char v[256];
+
+ first = spa_list_first(&graph->node_list, struct node, link);
+ last = spa_list_last(&graph->node_list, struct node, link);
+
+ /* calculate the number of inputs and outputs into the graph.
+ * If we have a list of inputs/outputs, just count them. Otherwise
+ * we count all input ports of the first node and all output
+ * ports of the last node */
+ if (inputs != NULL) {
+ n_input = count_array(inputs);
+ } else {
+ n_input = first->desc->n_input;
+ }
+ if (outputs != NULL) {
+ n_output = count_array(outputs);
+ } else {
+ n_output = last->desc->n_output;
+ }
+ if (n_input == 0) {
+ pw_log_error("no inputs");
+ res = -EINVAL;
+ goto error;
+ }
+ if (n_output == 0) {
+ pw_log_error("no outputs");
+ res = -EINVAL;
+ goto error;
+ }
+
+ if (impl->capture_info.channels == 0)
+ impl->capture_info.channels = n_input;
+ if (impl->playback_info.channels == 0)
+ impl->playback_info.channels = n_output;
+
+ /* compare to the requested number of channels and duplicate the
+ * graph n_hndl times when needed. */
+ n_hndl = impl->capture_info.channels / n_input;
+ if (n_hndl != impl->playback_info.channels / n_output) {
+ pw_log_error("invalid channels. The capture stream has %1$d channels and "
+ "the filter has %2$d inputs. The playback stream has %3$d channels "
+ "and the filter has %4$d outputs. capture:%1$d / input:%2$d != "
+ "playback:%3$d / output:%4$d. Check inputs and outputs objects.",
+ impl->capture_info.channels, n_input,
+ impl->playback_info.channels, n_output);
+ res = -EINVAL;
+ goto error;
+ }
+ if (n_hndl > MAX_HNDL) {
+ pw_log_error("too many channels. %d > %d", n_hndl, MAX_HNDL);
+ res = -EINVAL;
+ goto error;
+ }
+ if (n_hndl == 0) {
+ n_hndl = 1;
+ pw_log_warn("The capture stream has %1$d channels and "
+ "the filter has %2$d inputs. The playback stream has %3$d channels "
+ "and the filter has %4$d outputs. Some filter ports will be "
+ "unconnected..",
+ impl->capture_info.channels, n_input,
+ impl->playback_info.channels, n_output);
+ }
+ pw_log_info("using %d instances %d %d", n_hndl, n_input, n_output);
+
+ /* now go over all nodes and create instances. */
+ n_control = 0;
+ n_nodes = 0;
+ spa_list_for_each(node, &graph->node_list, link) {
+ node->n_hndl = n_hndl;
+ desc = node->desc;
+ n_control += desc->n_control;
+ n_nodes++;
+ }
+ graph->n_input = 0;
+ graph->input = calloc(n_input * 16 * n_hndl, sizeof(struct graph_port));
+ graph->n_output = 0;
+ graph->output = calloc(n_output * n_hndl, sizeof(struct graph_port));
+
+ /* now collect all input and output ports for all the handles. */
+ for (i = 0; i < n_hndl; i++) {
+ if (inputs == NULL) {
+ desc = first->desc;
+ d = desc->desc;
+ for (j = 0; j < desc->n_input; j++) {
+ gp = &graph->input[graph->n_input++];
+ pw_log_info("input port %s[%d]:%s",
+ first->name, i, d->ports[desc->input[j]].name);
+ gp->desc = d;
+ gp->hndl = &first->hndl[i];
+ gp->port = desc->input[j];
+ }
+ } else {
+ struct spa_json it = *inputs;
+ while (spa_json_get_string(&it, v, sizeof(v)) > 0) {
+ if (spa_streq(v, "null")) {
+ gp = &graph->input[graph->n_input++];
+ gp->desc = NULL;
+ pw_log_info("ignore input port %d", graph->n_input);
+ } else if ((port = find_port(first, v, FC_PORT_INPUT)) == NULL) {
+ res = -ENOENT;
+ pw_log_error("input port %s not found", v);
+ goto error;
+ } else {
+ desc = port->node->desc;
+ d = desc->desc;
+ if (i == 0 && port->external != SPA_ID_INVALID) {
+ pw_log_error("input port %s[%d]:%s already used as input %d, use mixer",
+ port->node->name, i, d->ports[port->p].name,
+ port->external);
+ res = -EBUSY;
+ goto error;
+ }
+ if (port->n_links > 0) {
+ pw_log_error("input port %s[%d]:%s already used by link, use mixer",
+ port->node->name, i, d->ports[port->p].name);
+ res = -EBUSY;
+ goto error;
+ }
+
+ if (d->flags & FC_DESCRIPTOR_COPY) {
+ for (j = 0; j < desc->n_output; j++) {
+ struct port *p = &port->node->output_port[j];
+ struct link *link;
+
+ gp = NULL;
+ spa_list_for_each(link, &p->link_list, output_link) {
+ struct port *peer = link->input;
+
+ pw_log_info("copy input port %s[%d]:%s",
+ port->node->name, i,
+ d->ports[port->p].name);
+ peer->external = graph->n_input;
+ gp = &graph->input[graph->n_input++];
+ gp->desc = peer->node->desc->desc;
+ gp->hndl = &peer->node->hndl[i];
+ gp->port = peer->p;
+ gp->next = true;
+ }
+ if (gp != NULL)
+ gp->next = false;
+ }
+ port->node->disabled = true;
+ } else {
+ pw_log_info("input port %s[%d]:%s",
+ port->node->name, i, d->ports[port->p].name);
+ port->external = graph->n_input;
+ gp = &graph->input[graph->n_input++];
+ gp->desc = d;
+ gp->hndl = &port->node->hndl[i];
+ gp->port = port->p;
+ gp->next = false;
+ }
+ }
+ }
+ }
+ if (outputs == NULL) {
+ desc = last->desc;
+ d = desc->desc;
+ for (j = 0; j < desc->n_output; j++) {
+ gp = &graph->output[graph->n_output++];
+ pw_log_info("output port %s[%d]:%s",
+ last->name, i, d->ports[desc->output[j]].name);
+ gp->desc = d;
+ gp->hndl = &last->hndl[i];
+ gp->port = desc->output[j];
+ }
+ } else {
+ struct spa_json it = *outputs;
+ while (spa_json_get_string(&it, v, sizeof(v)) > 0) {
+ gp = &graph->output[graph->n_output];
+ if (spa_streq(v, "null")) {
+ gp->desc = NULL;
+ pw_log_info("silence output port %d", graph->n_output);
+ } else if ((port = find_port(last, v, FC_PORT_OUTPUT)) == NULL) {
+ res = -ENOENT;
+ pw_log_error("output port %s not found", v);
+ goto error;
+ } else {
+ desc = port->node->desc;
+ d = desc->desc;
+ if (i == 0 && port->external != SPA_ID_INVALID) {
+ pw_log_error("output port %s[%d]:%s already used as output %d, use copy",
+ port->node->name, i, d->ports[port->p].name,
+ port->external);
+ res = -EBUSY;
+ goto error;
+ }
+ if (port->n_links > 0) {
+ pw_log_error("output port %s[%d]:%s already used by link, use copy",
+ port->node->name, i, d->ports[port->p].name);
+ res = -EBUSY;
+ goto error;
+ }
+ pw_log_info("output port %s[%d]:%s",
+ port->node->name, i, d->ports[port->p].name);
+ port->external = graph->n_output;
+ gp->desc = d;
+ gp->hndl = &port->node->hndl[i];
+ gp->port = port->p;
+ }
+ graph->n_output++;
+ }
+ }
+ }
+
+ /* order all nodes based on dependencies */
+ graph->n_hndl = 0;
+ graph->hndl = calloc(n_nodes * n_hndl, sizeof(struct graph_hndl));
+ graph->n_control = 0;
+ graph->control_port = calloc(n_control, sizeof(struct port *));
+ while (true) {
+ if ((node = find_next_node(graph)) == NULL)
+ break;
+
+ desc = node->desc;
+ d = desc->desc;
+
+ if (!node->disabled) {
+ for (i = 0; i < n_hndl; i++) {
+ gh = &graph->hndl[graph->n_hndl++];
+ gh->hndl = &node->hndl[i];
+ gh->desc = d;
+ }
+ }
+ for (i = 0; i < desc->n_output; i++) {
+ spa_list_for_each(link, &node->output_port[i].link_list, output_link)
+ link->input->node->n_deps--;
+ }
+
+ /* collect all control ports on the graph */
+ for (i = 0; i < desc->n_control; i++) {
+ graph->control_port[graph->n_control] = &node->control_port[i];
+ graph->n_control++;
+ }
+ }
+ res = 0;
+error:
+ return res;
+}
+
+/**
+ * filter.graph = {
+ * nodes = [
+ * { ... } ...
+ * ]
+ * links = [
+ * { ... } ...
+ * ]
+ * inputs = [ ]
+ * outputs = [ ]
+ * }
+ */
+static int load_graph(struct graph *graph, struct pw_properties *props)
+{
+ struct spa_json it[3];
+ struct spa_json inputs, outputs, *pinputs = NULL, *poutputs = NULL;
+ struct spa_json nodes, *pnodes = NULL, links, *plinks = NULL;
+ const char *json, *val;
+ char key[256];
+ int res;
+
+ spa_list_init(&graph->node_list);
+ spa_list_init(&graph->link_list);
+
+ if ((json = pw_properties_get(props, "filter.graph")) == NULL) {
+ pw_log_error("missing filter.graph property");
+ return -EINVAL;
+ }
+
+ spa_json_init(&it[0], json, strlen(json));
+ if (spa_json_enter_object(&it[0], &it[1]) <= 0) {
+ pw_log_error("filter.graph must be an object");
+ return -EINVAL;
+ }
+
+ while (spa_json_get_string(&it[1], key, sizeof(key)) > 0) {
+ if (spa_streq("nodes", key)) {
+ if (spa_json_enter_array(&it[1], &nodes) <= 0) {
+ pw_log_error("nodes expects an array");
+ return -EINVAL;
+ }
+ pnodes = &nodes;
+ }
+ else if (spa_streq("links", key)) {
+ if (spa_json_enter_array(&it[1], &links) <= 0) {
+ pw_log_error("links expects an array");
+ return -EINVAL;
+ }
+ plinks = &links;
+ }
+ else if (spa_streq("inputs", key)) {
+ if (spa_json_enter_array(&it[1], &inputs) <= 0) {
+ pw_log_error("inputs expects an array");
+ return -EINVAL;
+ }
+ pinputs = &inputs;
+ }
+ else if (spa_streq("outputs", key)) {
+ if (spa_json_enter_array(&it[1], &outputs) <= 0) {
+ pw_log_error("outputs expects an array");
+ return -EINVAL;
+ }
+ poutputs = &outputs;
+ } else if (spa_json_next(&it[1], &val) < 0)
+ break;
+ }
+ if (pnodes == NULL) {
+ pw_log_error("filter.graph is missing a nodes array");
+ return -EINVAL;
+ }
+ while (spa_json_enter_object(pnodes, &it[2]) > 0) {
+ if ((res = load_node(graph, &it[2])) < 0)
+ return res;
+ }
+ if (plinks != NULL) {
+ while (spa_json_enter_object(plinks, &it[2]) > 0) {
+ if ((res = parse_link(graph, &it[2])) < 0)
+ return res;
+ }
+ }
+ return setup_graph(graph, pinputs, poutputs);
+}
+
+static void graph_free(struct graph *graph)
+{
+ struct link *link;
+ struct node *node;
+ spa_list_consume(link, &graph->link_list, link)
+ link_free(link);
+ spa_list_consume(node, &graph->node_list, link)
+ node_free(node);
+ free(graph->input);
+ free(graph->output);
+ free(graph->hndl);
+ free(graph->control_port);
+}
+
+static void core_error(void *data, uint32_t id, int seq, int res, const char *message)
+{
+ struct impl *impl = data;
+
+ if (res == -ENOENT) {
+ pw_log_info("message id:%u seq:%d res:%d (%s): %s",
+ id, seq, res, spa_strerror(res), message);
+ } else {
+ pw_log_warn("error id:%u seq:%d res:%d (%s): %s",
+ id, seq, res, spa_strerror(res), message);
+ }
+
+ if (id == PW_ID_CORE && res == -EPIPE)
+ pw_impl_module_schedule_destroy(impl->module);
+}
+
+static const struct pw_core_events core_events = {
+ PW_VERSION_CORE_EVENTS,
+ .error = core_error,
+};
+
+static void core_destroy(void *d)
+{
+ struct impl *impl = d;
+ spa_hook_remove(&impl->core_listener);
+ impl->core = NULL;
+ pw_impl_module_schedule_destroy(impl->module);
+}
+
+static const struct pw_proxy_events core_proxy_events = {
+ .destroy = core_destroy,
+};
+
+static void impl_destroy(struct impl *impl)
+{
+ /* disconnect both streams before destroying any of them */
+ if (impl->capture)
+ pw_stream_disconnect(impl->capture);
+ if (impl->playback)
+ pw_stream_disconnect(impl->playback);
+
+ if (impl->capture)
+ pw_stream_destroy(impl->capture);
+ if (impl->playback)
+ pw_stream_destroy(impl->playback);
+
+ if (impl->core && impl->do_disconnect)
+ pw_core_disconnect(impl->core);
+
+ pw_properties_free(impl->capture_props);
+ pw_properties_free(impl->playback_props);
+ graph_free(&impl->graph);
+ free(impl);
+}
+
+static void module_destroy(void *data)
+{
+ struct impl *impl = data;
+ spa_hook_remove(&impl->module_listener);
+ impl_destroy(impl);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy,
+};
+
+static uint32_t channel_from_name(const char *name)
+{
+ int i;
+ for (i = 0; spa_type_audio_channel[i].name; i++) {
+ if (spa_streq(name, spa_debug_type_short_name(spa_type_audio_channel[i].name)))
+ return spa_type_audio_channel[i].type;
+ }
+ return SPA_AUDIO_CHANNEL_UNKNOWN;
+}
+
+static void parse_position(struct spa_audio_info_raw *info, const char *val, size_t len)
+{
+ struct spa_json it[2];
+ char v[256];
+
+ spa_json_init(&it[0], val, len);
+ if (spa_json_enter_array(&it[0], &it[1]) <= 0)
+ spa_json_init(&it[1], val, len);
+
+ info->channels = 0;
+ while (spa_json_get_string(&it[1], v, sizeof(v)) > 0 &&
+ info->channels < SPA_AUDIO_MAX_CHANNELS) {
+ info->position[info->channels++] = channel_from_name(v);
+ }
+}
+
+static void parse_audio_info(struct pw_properties *props, struct spa_audio_info_raw *info)
+{
+ const char *str;
+
+ *info = SPA_AUDIO_INFO_RAW_INIT(
+ .format = SPA_AUDIO_FORMAT_F32P);
+ info->rate = pw_properties_get_int32(props, PW_KEY_AUDIO_RATE, info->rate);
+ info->channels = pw_properties_get_int32(props, PW_KEY_AUDIO_CHANNELS, info->channels);
+ info->channels = SPA_MIN(info->channels, SPA_AUDIO_MAX_CHANNELS);
+ if ((str = pw_properties_get(props, SPA_KEY_AUDIO_POSITION)) != NULL)
+ parse_position(info, str, strlen(str));
+}
+
+static void copy_props(struct impl *impl, struct pw_properties *props, const char *key)
+{
+ const char *str;
+ if ((str = pw_properties_get(props, key)) != NULL) {
+ if (pw_properties_get(impl->capture_props, key) == NULL)
+ pw_properties_set(impl->capture_props, key, str);
+ if (pw_properties_get(impl->playback_props, key) == NULL)
+ pw_properties_set(impl->playback_props, key, str);
+ }
+}
+
+SPA_EXPORT
+int pipewire__module_init(struct pw_impl_module *module, const char *args)
+{
+ struct pw_context *context = pw_impl_module_get_context(module);
+ struct pw_properties *props;
+ struct impl *impl;
+ uint32_t id = pw_global_get_id(pw_impl_module_get_global(module));
+ uint32_t pid = getpid();
+ const char *str;
+ int res;
+ const struct spa_support *support;
+ uint32_t n_support;
+ struct spa_cpu *cpu_iface;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ impl = calloc(1, sizeof(struct impl));
+ if (impl == NULL)
+ return -errno;
+
+ pw_log_debug("module %p: new %s", impl, args);
+
+ if (args)
+ props = pw_properties_new_string(args);
+ else
+ props = pw_properties_new(NULL, NULL);
+
+ if (props == NULL) {
+ res = -errno;
+ pw_log_error( "can't create properties: %m");
+ goto error;
+ }
+
+ impl->capture_props = pw_properties_new(NULL, NULL);
+ impl->playback_props = pw_properties_new(NULL, NULL);
+ if (impl->capture_props == NULL || impl->playback_props == NULL) {
+ res = -errno;
+ pw_log_error( "can't create properties: %m");
+ goto error;
+ }
+
+ impl->module = module;
+ impl->context = context;
+ impl->graph.impl = impl;
+
+ spa_list_init(&impl->plugin_list);
+
+ support = pw_context_get_support(impl->context, &n_support);
+
+ cpu_iface = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_CPU);
+ impl->dsp.cpu_flags = cpu_iface ? spa_cpu_get_flags(cpu_iface) : 0;
+ dsp_ops_init(&impl->dsp);
+
+ if (pw_properties_get(props, PW_KEY_NODE_GROUP) == NULL)
+ pw_properties_setf(props, PW_KEY_NODE_GROUP, "filter-chain-%u-%u", pid, id);
+ if (pw_properties_get(props, PW_KEY_NODE_LINK_GROUP) == NULL)
+ pw_properties_setf(props, PW_KEY_NODE_LINK_GROUP, "filter-chain-%u-%u", pid, id);
+ if (pw_properties_get(props, PW_KEY_NODE_VIRTUAL) == NULL)
+ pw_properties_set(props, PW_KEY_NODE_VIRTUAL, "true");
+ if (pw_properties_get(props, "resample.prefill") == NULL)
+ pw_properties_set(props, "resample.prefill", "true");
+ if (pw_properties_get(props, PW_KEY_NODE_DESCRIPTION) == NULL)
+ pw_properties_setf(props, PW_KEY_NODE_DESCRIPTION, "filter-chain-%u-%u", pid, id);
+
+ if ((str = pw_properties_get(props, "capture.props")) != NULL)
+ pw_properties_update_string(impl->capture_props, str, strlen(str));
+ if ((str = pw_properties_get(props, "playback.props")) != NULL)
+ pw_properties_update_string(impl->playback_props, str, strlen(str));
+
+ copy_props(impl, props, PW_KEY_AUDIO_RATE);
+ copy_props(impl, props, PW_KEY_AUDIO_CHANNELS);
+ copy_props(impl, props, SPA_KEY_AUDIO_POSITION);
+ copy_props(impl, props, PW_KEY_NODE_DESCRIPTION);
+ copy_props(impl, props, PW_KEY_NODE_GROUP);
+ copy_props(impl, props, PW_KEY_NODE_LINK_GROUP);
+ copy_props(impl, props, PW_KEY_NODE_LATENCY);
+ copy_props(impl, props, PW_KEY_NODE_VIRTUAL);
+ copy_props(impl, props, PW_KEY_MEDIA_NAME);
+ copy_props(impl, props, "resample.prefill");
+
+ parse_audio_info(impl->capture_props, &impl->capture_info);
+ parse_audio_info(impl->playback_props, &impl->playback_info);
+
+ if (impl->capture_info.rate && !impl->playback_info.rate)
+ impl->playback_info.rate = impl->capture_info.rate;
+ else if (impl->playback_info.rate && !impl->capture_info.rate)
+ impl->capture_info.rate = !impl->playback_info.rate;
+ else if (impl->capture_info.rate != impl->playback_info.rate) {
+ pw_log_warn("Both capture and playback rate are set, but"
+ " they are different. Using the highest of two. This behaviour"
+ " is deprecated, please use equal rates in the module config");
+ impl->playback_info.rate = impl->capture_info.rate =
+ SPA_MAX(impl->playback_info.rate, impl->capture_info.rate);
+ }
+
+ if ((str = pw_properties_get(props, PW_KEY_NODE_NAME)) == NULL) {
+ pw_properties_setf(props, PW_KEY_NODE_NAME,
+ "filter-chain-%u-%u", pid, id);
+ str = pw_properties_get(props, PW_KEY_NODE_NAME);
+ }
+ if (pw_properties_get(impl->capture_props, PW_KEY_NODE_NAME) == NULL)
+ pw_properties_setf(impl->capture_props, PW_KEY_NODE_NAME,
+ "input.%s", str);
+ if (pw_properties_get(impl->playback_props, PW_KEY_NODE_NAME) == NULL)
+ pw_properties_setf(impl->playback_props, PW_KEY_NODE_NAME,
+ "output.%s", str);
+
+ if (pw_properties_get(impl->capture_props, PW_KEY_MEDIA_NAME) == NULL)
+ pw_properties_setf(impl->capture_props, PW_KEY_MEDIA_NAME, "%s input",
+ pw_properties_get(impl->capture_props, PW_KEY_NODE_DESCRIPTION));
+ if (pw_properties_get(impl->playback_props, PW_KEY_MEDIA_NAME) == NULL)
+ pw_properties_setf(impl->playback_props, PW_KEY_MEDIA_NAME, "%s output",
+ pw_properties_get(impl->playback_props, PW_KEY_NODE_DESCRIPTION));
+
+ if ((res = load_graph(&impl->graph, props)) < 0) {
+ pw_log_error("can't load graph: %s", spa_strerror(res));
+ goto error;
+ }
+
+ impl->core = pw_context_get_object(impl->context, PW_TYPE_INTERFACE_Core);
+ if (impl->core == NULL) {
+ str = pw_properties_get(props, PW_KEY_REMOTE_NAME);
+ impl->core = pw_context_connect(impl->context,
+ pw_properties_new(
+ PW_KEY_REMOTE_NAME, str,
+ NULL),
+ 0);
+ impl->do_disconnect = true;
+ }
+ if (impl->core == NULL) {
+ res = -errno;
+ pw_log_error("can't connect: %m");
+ goto error;
+ }
+ pw_properties_free(props);
+
+ pw_proxy_add_listener((struct pw_proxy*)impl->core,
+ &impl->core_proxy_listener,
+ &core_proxy_events, impl);
+ pw_core_add_listener(impl->core,
+ &impl->core_listener,
+ &core_events, impl);
+
+ setup_streams(impl);
+
+ pw_impl_module_add_listener(module, &impl->module_listener, &module_events, impl);
+
+ pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_props));
+
+ return 0;
+
+error:
+ pw_properties_free(props);
+ impl_destroy(impl);
+ return res;
+}
diff --git a/src/modules/module-filter-chain/biquad.c b/src/modules/module-filter-chain/biquad.c
new file mode 100644
index 0000000..58c8dd0
--- /dev/null
+++ b/src/modules/module-filter-chain/biquad.c
@@ -0,0 +1,364 @@
+/* Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+/* Copyright (C) 2010 Google Inc. All rights reserved.
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE.WEBKIT file.
+ */
+
+#include <math.h>
+#include "biquad.h"
+
+#ifndef M_PI
+#define M_PI 3.14159265358979323846
+#endif
+
+static void set_coefficient(struct biquad *bq, double b0, double b1, double b2,
+ double a0, double a1, double a2)
+{
+ double a0_inv = 1 / a0;
+ bq->b0 = b0 * a0_inv;
+ bq->b1 = b1 * a0_inv;
+ bq->b2 = b2 * a0_inv;
+ bq->a1 = a1 * a0_inv;
+ bq->a2 = a2 * a0_inv;
+}
+
+static void biquad_lowpass(struct biquad *bq, double cutoff, double resonance)
+{
+ /* Limit cutoff to 0 to 1. */
+ cutoff = fmax(0.0, fmin(cutoff, 1.0));
+
+ if (cutoff == 1 || cutoff == 0) {
+ /* When cutoff is 1, the z-transform is 1.
+ * When cutoff is zero, nothing gets through the filter, so set
+ * coefficients up correctly.
+ */
+ set_coefficient(bq, cutoff, 0, 0, 1, 0, 0);
+ return;
+ }
+
+ /* Compute biquad coefficients for lowpass filter */
+ resonance = fmax(0.0, resonance); /* can't go negative */
+ double g = pow(10.0, 0.05 * resonance);
+ double d = sqrt((4 - sqrt(16 - 16 / (g * g))) / 2);
+
+ double theta = M_PI * cutoff;
+ double sn = 0.5 * d * sin(theta);
+ double beta = 0.5 * (1 - sn) / (1 + sn);
+ double gamma = (0.5 + beta) * cos(theta);
+ double alpha = 0.25 * (0.5 + beta - gamma);
+
+ double b0 = 2 * alpha;
+ double b1 = 2 * 2 * alpha;
+ double b2 = 2 * alpha;
+ double a1 = 2 * -gamma;
+ double a2 = 2 * beta;
+
+ set_coefficient(bq, b0, b1, b2, 1, a1, a2);
+}
+
+static void biquad_highpass(struct biquad *bq, double cutoff, double resonance)
+{
+ /* Limit cutoff to 0 to 1. */
+ cutoff = fmax(0.0, fmin(cutoff, 1.0));
+
+ if (cutoff == 1 || cutoff == 0) {
+ /* When cutoff is one, the z-transform is 0. */
+ /* When cutoff is zero, we need to be careful because the above
+ * gives a quadratic divided by the same quadratic, with poles
+ * and zeros on the unit circle in the same place. When cutoff
+ * is zero, the z-transform is 1.
+ */
+ set_coefficient(bq, 1 - cutoff, 0, 0, 1, 0, 0);
+ return;
+ }
+
+ /* Compute biquad coefficients for highpass filter */
+ resonance = fmax(0.0, resonance); /* can't go negative */
+ double g = pow(10.0, 0.05 * resonance);
+ double d = sqrt((4 - sqrt(16 - 16 / (g * g))) / 2);
+
+ double theta = M_PI * cutoff;
+ double sn = 0.5 * d * sin(theta);
+ double beta = 0.5 * (1 - sn) / (1 + sn);
+ double gamma = (0.5 + beta) * cos(theta);
+ double alpha = 0.25 * (0.5 + beta + gamma);
+
+ double b0 = 2 * alpha;
+ double b1 = 2 * -2 * alpha;
+ double b2 = 2 * alpha;
+ double a1 = 2 * -gamma;
+ double a2 = 2 * beta;
+
+ set_coefficient(bq, b0, b1, b2, 1, a1, a2);
+}
+
+static void biquad_bandpass(struct biquad *bq, double frequency, double Q)
+{
+ /* No negative frequencies allowed. */
+ frequency = fmax(0.0, frequency);
+
+ /* Don't let Q go negative, which causes an unstable filter. */
+ Q = fmax(0.0, Q);
+
+ if (frequency <= 0 || frequency >= 1) {
+ /* When the cutoff is zero, the z-transform approaches 0, if Q
+ * > 0. When both Q and cutoff are zero, the z-transform is
+ * pretty much undefined. What should we do in this case?
+ * For now, just make the filter 0. When the cutoff is 1, the
+ * z-transform also approaches 0.
+ */
+ set_coefficient(bq, 0, 0, 0, 1, 0, 0);
+ return;
+ }
+ if (Q <= 0) {
+ /* When Q = 0, the above formulas have problems. If we
+ * look at the z-transform, we can see that the limit
+ * as Q->0 is 1, so set the filter that way.
+ */
+ set_coefficient(bq, 1, 0, 0, 1, 0, 0);
+ return;
+ }
+
+ double w0 = M_PI * frequency;
+ double alpha = sin(w0) / (2 * Q);
+ double k = cos(w0);
+
+ double b0 = alpha;
+ double b1 = 0;
+ double b2 = -alpha;
+ double a0 = 1 + alpha;
+ double a1 = -2 * k;
+ double a2 = 1 - alpha;
+
+ set_coefficient(bq, b0, b1, b2, a0, a1, a2);
+}
+
+static void biquad_lowshelf(struct biquad *bq, double frequency, double db_gain)
+{
+ /* Clip frequencies to between 0 and 1, inclusive. */
+ frequency = fmax(0.0, fmin(frequency, 1.0));
+
+ double A = pow(10.0, db_gain / 40);
+
+ if (frequency == 1) {
+ /* The z-transform is a constant gain. */
+ set_coefficient(bq, A * A, 0, 0, 1, 0, 0);
+ return;
+ }
+ if (frequency <= 0) {
+ /* When frequency is 0, the z-transform is 1. */
+ set_coefficient(bq, 1, 0, 0, 1, 0, 0);
+ return;
+ }
+
+ double w0 = M_PI * frequency;
+ double S = 1; /* filter slope (1 is max value) */
+ double alpha = 0.5 * sin(w0) * sqrt((A + 1 / A) * (1 / S - 1) + 2);
+ double k = cos(w0);
+ double k2 = 2 * sqrt(A) * alpha;
+ double a_plus_one = A + 1;
+ double a_minus_one = A - 1;
+
+ double b0 = A * (a_plus_one - a_minus_one * k + k2);
+ double b1 = 2 * A * (a_minus_one - a_plus_one * k);
+ double b2 = A * (a_plus_one - a_minus_one * k - k2);
+ double a0 = a_plus_one + a_minus_one * k + k2;
+ double a1 = -2 * (a_minus_one + a_plus_one * k);
+ double a2 = a_plus_one + a_minus_one * k - k2;
+
+ set_coefficient(bq, b0, b1, b2, a0, a1, a2);
+}
+
+static void biquad_highshelf(struct biquad *bq, double frequency,
+ double db_gain)
+{
+ /* Clip frequencies to between 0 and 1, inclusive. */
+ frequency = fmax(0.0, fmin(frequency, 1.0));
+
+ double A = pow(10.0, db_gain / 40);
+
+ if (frequency == 1) {
+ /* The z-transform is 1. */
+ set_coefficient(bq, 1, 0, 0, 1, 0, 0);
+ return;
+ }
+ if (frequency <= 0) {
+ /* When frequency = 0, the filter is just a gain, A^2. */
+ set_coefficient(bq, A * A, 0, 0, 1, 0, 0);
+ return;
+ }
+
+ double w0 = M_PI * frequency;
+ double S = 1; /* filter slope (1 is max value) */
+ double alpha = 0.5 * sin(w0) * sqrt((A + 1 / A) * (1 / S - 1) + 2);
+ double k = cos(w0);
+ double k2 = 2 * sqrt(A) * alpha;
+ double a_plus_one = A + 1;
+ double a_minus_one = A - 1;
+
+ double b0 = A * (a_plus_one + a_minus_one * k + k2);
+ double b1 = -2 * A * (a_minus_one + a_plus_one * k);
+ double b2 = A * (a_plus_one + a_minus_one * k - k2);
+ double a0 = a_plus_one - a_minus_one * k + k2;
+ double a1 = 2 * (a_minus_one - a_plus_one * k);
+ double a2 = a_plus_one - a_minus_one * k - k2;
+
+ set_coefficient(bq, b0, b1, b2, a0, a1, a2);
+}
+
+static void biquad_peaking(struct biquad *bq, double frequency, double Q,
+ double db_gain)
+{
+ /* Clip frequencies to between 0 and 1, inclusive. */
+ frequency = fmax(0.0, fmin(frequency, 1.0));
+
+ /* Don't let Q go negative, which causes an unstable filter. */
+ Q = fmax(0.0, Q);
+
+ double A = pow(10.0, db_gain / 40);
+
+ if (frequency <= 0 || frequency >= 1) {
+ /* When frequency is 0 or 1, the z-transform is 1. */
+ set_coefficient(bq, 1, 0, 0, 1, 0, 0);
+ return;
+ }
+ if (Q <= 0) {
+ /* When Q = 0, the above formulas have problems. If we
+ * look at the z-transform, we can see that the limit
+ * as Q->0 is A^2, so set the filter that way.
+ */
+ set_coefficient(bq, A * A, 0, 0, 1, 0, 0);
+ return;
+ }
+
+ double w0 = M_PI * frequency;
+ double alpha = sin(w0) / (2 * Q);
+ double k = cos(w0);
+
+ double b0 = 1 + alpha * A;
+ double b1 = -2 * k;
+ double b2 = 1 - alpha * A;
+ double a0 = 1 + alpha / A;
+ double a1 = -2 * k;
+ double a2 = 1 - alpha / A;
+
+ set_coefficient(bq, b0, b1, b2, a0, a1, a2);
+}
+
+static void biquad_notch(struct biquad *bq, double frequency, double Q)
+{
+ /* Clip frequencies to between 0 and 1, inclusive. */
+ frequency = fmax(0.0, fmin(frequency, 1.0));
+
+ /* Don't let Q go negative, which causes an unstable filter. */
+ Q = fmax(0.0, Q);
+
+ if (frequency <= 0 || frequency >= 1) {
+ /* When frequency is 0 or 1, the z-transform is 1. */
+ set_coefficient(bq, 1, 0, 0, 1, 0, 0);
+ return;
+ }
+ if (Q <= 0) {
+ /* When Q = 0, the above formulas have problems. If we
+ * look at the z-transform, we can see that the limit
+ * as Q->0 is 0, so set the filter that way.
+ */
+ set_coefficient(bq, 0, 0, 0, 1, 0, 0);
+ return;
+ }
+
+ double w0 = M_PI * frequency;
+ double alpha = sin(w0) / (2 * Q);
+ double k = cos(w0);
+
+ double b0 = 1;
+ double b1 = -2 * k;
+ double b2 = 1;
+ double a0 = 1 + alpha;
+ double a1 = -2 * k;
+ double a2 = 1 - alpha;
+
+ set_coefficient(bq, b0, b1, b2, a0, a1, a2);
+}
+
+static void biquad_allpass(struct biquad *bq, double frequency, double Q)
+{
+ /* Clip frequencies to between 0 and 1, inclusive. */
+ frequency = fmax(0.0, fmin(frequency, 1.0));
+
+ /* Don't let Q go negative, which causes an unstable filter. */
+ Q = fmax(0.0, Q);
+
+ if (frequency <= 0 || frequency >= 1) {
+ /* When frequency is 0 or 1, the z-transform is 1. */
+ set_coefficient(bq, 1, 0, 0, 1, 0, 0);
+ return;
+ }
+
+ if (Q <= 0) {
+ /* When Q = 0, the above formulas have problems. If we
+ * look at the z-transform, we can see that the limit
+ * as Q->0 is -1, so set the filter that way.
+ */
+ set_coefficient(bq, -1, 0, 0, 1, 0, 0);
+ return;
+ }
+
+ double w0 = M_PI * frequency;
+ double alpha = sin(w0) / (2 * Q);
+ double k = cos(w0);
+
+ double b0 = 1 - alpha;
+ double b1 = -2 * k;
+ double b2 = 1 + alpha;
+ double a0 = 1 + alpha;
+ double a1 = -2 * k;
+ double a2 = 1 - alpha;
+
+ set_coefficient(bq, b0, b1, b2, a0, a1, a2);
+}
+
+void biquad_set(struct biquad *bq, enum biquad_type type, double freq, double Q,
+ double gain)
+{
+ /* Clear history values. */
+ bq->x1 = 0;
+ bq->x2 = 0;
+ bq->y1 = 0;
+ bq->y2 = 0;
+
+ switch (type) {
+ case BQ_LOWPASS:
+ biquad_lowpass(bq, freq, Q);
+ break;
+ case BQ_HIGHPASS:
+ biquad_highpass(bq, freq, Q);
+ break;
+ case BQ_BANDPASS:
+ biquad_bandpass(bq, freq, Q);
+ break;
+ case BQ_LOWSHELF:
+ biquad_lowshelf(bq, freq, gain);
+ break;
+ case BQ_HIGHSHELF:
+ biquad_highshelf(bq, freq, gain);
+ break;
+ case BQ_PEAKING:
+ biquad_peaking(bq, freq, Q, gain);
+ break;
+ case BQ_NOTCH:
+ biquad_notch(bq, freq, Q);
+ break;
+ case BQ_ALLPASS:
+ biquad_allpass(bq, freq, Q);
+ break;
+ case BQ_NONE:
+ /* Default is an identity filter. */
+ set_coefficient(bq, 1, 0, 0, 1, 0, 0);
+ break;
+ }
+}
diff --git a/src/modules/module-filter-chain/biquad.h b/src/modules/module-filter-chain/biquad.h
new file mode 100644
index 0000000..650b263
--- /dev/null
+++ b/src/modules/module-filter-chain/biquad.h
@@ -0,0 +1,57 @@
+/* Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#ifndef BIQUAD_H_
+#define BIQUAD_H_
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* The biquad filter parameters. The transfer function H(z) is (b0 + b1 * z^(-1)
+ * + b2 * z^(-2)) / (1 + a1 * z^(-1) + a2 * z^(-2)). The previous two inputs
+ * are stored in x1 and x2, and the previous two outputs are stored in y1 and
+ * y2.
+ *
+ * We use double during the coefficients calculation for better accuracy, but
+ * float is used during the actual filtering for faster computation.
+ */
+struct biquad {
+ float b0, b1, b2;
+ float a1, a2;
+ float x1, x2;
+ float y1, y2;
+};
+
+/* The type of the biquad filters */
+enum biquad_type {
+ BQ_NONE,
+ BQ_LOWPASS,
+ BQ_HIGHPASS,
+ BQ_BANDPASS,
+ BQ_LOWSHELF,
+ BQ_HIGHSHELF,
+ BQ_PEAKING,
+ BQ_NOTCH,
+ BQ_ALLPASS
+};
+
+/* Initialize a biquad filter parameters from its type and parameters.
+ * Args:
+ * bq - The biquad filter we want to set.
+ * type - The type of the biquad filter.
+ * frequency - The value should be in the range [0, 1]. It is relative to
+ * half of the sampling rate.
+ * Q - Quality factor. See Web Audio API for details.
+ * gain - The value is in dB. See Web Audio API for details.
+ */
+void biquad_set(struct biquad *bq, enum biquad_type type, double freq, double Q,
+ double gain);
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* BIQUAD_H_ */
diff --git a/src/modules/module-filter-chain/builtin_plugin.c b/src/modules/module-filter-chain/builtin_plugin.c
new file mode 100644
index 0000000..068df99
--- /dev/null
+++ b/src/modules/module-filter-chain/builtin_plugin.c
@@ -0,0 +1,1035 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "config.h"
+
+#include <float.h>
+#include <math.h>
+#ifdef HAVE_SNDFILE
+#include <sndfile.h>
+#endif
+
+#include <spa/utils/json.h>
+#include <spa/utils/result.h>
+#include <spa/support/cpu.h>
+#include <spa/plugins/audioconvert/resample.h>
+
+#include <pipewire/log.h>
+
+#include "plugin.h"
+
+#include "biquad.h"
+#include "pffft.h"
+#include "convolver.h"
+#include "dsp-ops.h"
+
+#define MAX_RATES 32u
+
+static struct dsp_ops *dsp_ops;
+
+struct builtin {
+ unsigned long rate;
+ float *port[64];
+
+ struct biquad bq;
+ float freq;
+ float Q;
+ float gain;
+};
+
+static void *builtin_instantiate(const struct fc_descriptor * Descriptor,
+ unsigned long SampleRate, int index, const char *config)
+{
+ struct builtin *impl;
+
+ impl = calloc(1, sizeof(*impl));
+ if (impl == NULL)
+ return NULL;
+
+ impl->rate = SampleRate;
+
+ return impl;
+}
+
+static void builtin_connect_port(void *Instance, unsigned long Port, float * DataLocation)
+{
+ struct builtin *impl = Instance;
+ impl->port[Port] = DataLocation;
+}
+
+static void builtin_cleanup(void * Instance)
+{
+ struct builtin *impl = Instance;
+ free(impl);
+}
+
+/** copy */
+static void copy_run(void * Instance, unsigned long SampleCount)
+{
+ struct builtin *impl = Instance;
+ float *in = impl->port[1], *out = impl->port[0];
+ dsp_ops_copy(dsp_ops, out, in, SampleCount);
+}
+
+static struct fc_port copy_ports[] = {
+ { .index = 0,
+ .name = "Out",
+ .flags = FC_PORT_OUTPUT | FC_PORT_AUDIO,
+ },
+ { .index = 1,
+ .name = "In",
+ .flags = FC_PORT_INPUT | FC_PORT_AUDIO,
+ }
+};
+
+static const struct fc_descriptor copy_desc = {
+ .name = "copy",
+ .flags = FC_DESCRIPTOR_COPY,
+
+ .n_ports = 2,
+ .ports = copy_ports,
+
+ .instantiate = builtin_instantiate,
+ .connect_port = builtin_connect_port,
+ .run = copy_run,
+ .cleanup = builtin_cleanup,
+};
+
+/** mixer */
+static void mixer_run(void * Instance, unsigned long SampleCount)
+{
+ struct builtin *impl = Instance;
+ int i, n_src = 0;
+ float *out = impl->port[0];
+ const void *src[8];
+ float gains[8];
+
+ if (out == NULL)
+ return;
+
+ for (i = 0; i < 8; i++) {
+ float *in = impl->port[1+i];
+ float gain = impl->port[9+i][0];
+
+ if (in == NULL || gain == 0.0f)
+ continue;
+
+ src[n_src] = in;
+ gains[n_src++] = gain;
+ }
+ dsp_ops_mix_gain(dsp_ops, out, src, gains, n_src, SampleCount);
+}
+
+static struct fc_port mixer_ports[] = {
+ { .index = 0,
+ .name = "Out",
+ .flags = FC_PORT_OUTPUT | FC_PORT_AUDIO,
+ },
+
+ { .index = 1,
+ .name = "In 1",
+ .flags = FC_PORT_INPUT | FC_PORT_AUDIO,
+ },
+ { .index = 2,
+ .name = "In 2",
+ .flags = FC_PORT_INPUT | FC_PORT_AUDIO,
+ },
+ { .index = 3,
+ .name = "In 3",
+ .flags = FC_PORT_INPUT | FC_PORT_AUDIO,
+ },
+ { .index = 4,
+ .name = "In 4",
+ .flags = FC_PORT_INPUT | FC_PORT_AUDIO,
+ },
+ { .index = 5,
+ .name = "In 5",
+ .flags = FC_PORT_INPUT | FC_PORT_AUDIO,
+ },
+ { .index = 6,
+ .name = "In 6",
+ .flags = FC_PORT_INPUT | FC_PORT_AUDIO,
+ },
+ { .index = 7,
+ .name = "In 7",
+ .flags = FC_PORT_INPUT | FC_PORT_AUDIO,
+ },
+ { .index = 8,
+ .name = "In 8",
+ .flags = FC_PORT_INPUT | FC_PORT_AUDIO,
+ },
+
+ { .index = 9,
+ .name = "Gain 1",
+ .flags = FC_PORT_INPUT | FC_PORT_CONTROL,
+ .def = 1.0f, .min = 0.0f, .max = 10.0f
+ },
+ { .index = 10,
+ .name = "Gain 2",
+ .flags = FC_PORT_INPUT | FC_PORT_CONTROL,
+ .def = 1.0f, .min = 0.0f, .max = 10.0f
+ },
+ { .index = 11,
+ .name = "Gain 3",
+ .flags = FC_PORT_INPUT | FC_PORT_CONTROL,
+ .def = 1.0f, .min = 0.0f, .max = 10.0f
+ },
+ { .index = 12,
+ .name = "Gain 4",
+ .flags = FC_PORT_INPUT | FC_PORT_CONTROL,
+ .def = 1.0f, .min = 0.0f, .max = 10.0f
+ },
+ { .index = 13,
+ .name = "Gain 5",
+ .flags = FC_PORT_INPUT | FC_PORT_CONTROL,
+ .def = 1.0f, .min = 0.0f, .max = 10.0f
+ },
+ { .index = 14,
+ .name = "Gain 6",
+ .flags = FC_PORT_INPUT | FC_PORT_CONTROL,
+ .def = 1.0f, .min = 0.0f, .max = 10.0f
+ },
+ { .index = 15,
+ .name = "Gain 7",
+ .flags = FC_PORT_INPUT | FC_PORT_CONTROL,
+ .def = 1.0f, .min = 0.0f, .max = 10.0f
+ },
+ { .index = 16,
+ .name = "Gain 8",
+ .flags = FC_PORT_INPUT | FC_PORT_CONTROL,
+ .def = 1.0f, .min = 0.0f, .max = 10.0f
+ },
+};
+
+static const struct fc_descriptor mixer_desc = {
+ .name = "mixer",
+ .flags = FC_DESCRIPTOR_SUPPORTS_NULL_DATA,
+
+ .n_ports = 17,
+ .ports = mixer_ports,
+
+ .instantiate = builtin_instantiate,
+ .connect_port = builtin_connect_port,
+ .run = mixer_run,
+ .cleanup = builtin_cleanup,
+};
+
+static struct fc_port bq_ports[] = {
+ { .index = 0,
+ .name = "Out",
+ .flags = FC_PORT_OUTPUT | FC_PORT_AUDIO,
+ },
+ { .index = 1,
+ .name = "In",
+ .flags = FC_PORT_INPUT | FC_PORT_AUDIO,
+ },
+ { .index = 2,
+ .name = "Freq",
+ .flags = FC_PORT_INPUT | FC_PORT_CONTROL,
+ .hint = FC_HINT_SAMPLE_RATE,
+ .def = 0.0f, .min = 0.0f, .max = 1.0f,
+ },
+ { .index = 3,
+ .name = "Q",
+ .flags = FC_PORT_INPUT | FC_PORT_CONTROL,
+ .def = 0.0f, .min = 0.0f, .max = 10.0f,
+ },
+ { .index = 4,
+ .name = "Gain",
+ .flags = FC_PORT_INPUT | FC_PORT_CONTROL,
+ .def = 0.0f, .min = -120.0f, .max = 20.0f,
+ },
+};
+
+static void bq_run(struct builtin *impl, unsigned long samples, int type)
+{
+ struct biquad *bq = &impl->bq;
+ float *out = impl->port[0];
+ float *in = impl->port[1];
+ float freq = impl->port[2][0];
+ float Q = impl->port[3][0];
+ float gain = impl->port[4][0];
+
+ if (impl->freq != freq || impl->Q != Q || impl->gain != gain) {
+ impl->freq = freq;
+ impl->Q = Q;
+ impl->gain = gain;
+ biquad_set(bq, type, freq * 2 / impl->rate, Q, gain);
+ }
+ dsp_ops_biquad_run(dsp_ops, bq, out, in, samples);
+}
+
+/** bq_lowpass */
+static void bq_lowpass_run(void * Instance, unsigned long SampleCount)
+{
+ struct builtin *impl = Instance;
+ bq_run(impl, SampleCount, BQ_LOWPASS);
+}
+
+static const struct fc_descriptor bq_lowpass_desc = {
+ .name = "bq_lowpass",
+
+ .n_ports = 5,
+ .ports = bq_ports,
+
+ .instantiate = builtin_instantiate,
+ .connect_port = builtin_connect_port,
+ .run = bq_lowpass_run,
+ .cleanup = builtin_cleanup,
+};
+
+/** bq_highpass */
+static void bq_highpass_run(void * Instance, unsigned long SampleCount)
+{
+ struct builtin *impl = Instance;
+ bq_run(impl, SampleCount, BQ_HIGHPASS);
+}
+
+static const struct fc_descriptor bq_highpass_desc = {
+ .name = "bq_highpass",
+
+ .n_ports = 5,
+ .ports = bq_ports,
+
+ .instantiate = builtin_instantiate,
+ .connect_port = builtin_connect_port,
+ .run = bq_highpass_run,
+ .cleanup = builtin_cleanup,
+};
+
+/** bq_bandpass */
+static void bq_bandpass_run(void * Instance, unsigned long SampleCount)
+{
+ struct builtin *impl = Instance;
+ bq_run(impl, SampleCount, BQ_BANDPASS);
+}
+
+static const struct fc_descriptor bq_bandpass_desc = {
+ .name = "bq_bandpass",
+
+ .n_ports = 5,
+ .ports = bq_ports,
+
+ .instantiate = builtin_instantiate,
+ .connect_port = builtin_connect_port,
+ .run = bq_bandpass_run,
+ .cleanup = builtin_cleanup,
+};
+
+/** bq_lowshelf */
+static void bq_lowshelf_run(void * Instance, unsigned long SampleCount)
+{
+ struct builtin *impl = Instance;
+ bq_run(impl, SampleCount, BQ_LOWSHELF);
+}
+
+static const struct fc_descriptor bq_lowshelf_desc = {
+ .name = "bq_lowshelf",
+
+ .n_ports = 5,
+ .ports = bq_ports,
+
+ .instantiate = builtin_instantiate,
+ .connect_port = builtin_connect_port,
+ .run = bq_lowshelf_run,
+ .cleanup = builtin_cleanup,
+};
+
+/** bq_highshelf */
+static void bq_highshelf_run(void * Instance, unsigned long SampleCount)
+{
+ struct builtin *impl = Instance;
+ bq_run(impl, SampleCount, BQ_HIGHSHELF);
+}
+
+static const struct fc_descriptor bq_highshelf_desc = {
+ .name = "bq_highshelf",
+
+ .n_ports = 5,
+ .ports = bq_ports,
+
+ .instantiate = builtin_instantiate,
+ .connect_port = builtin_connect_port,
+ .run = bq_highshelf_run,
+ .cleanup = builtin_cleanup,
+};
+
+/** bq_peaking */
+static void bq_peaking_run(void * Instance, unsigned long SampleCount)
+{
+ struct builtin *impl = Instance;
+ bq_run(impl, SampleCount, BQ_PEAKING);
+}
+
+static const struct fc_descriptor bq_peaking_desc = {
+ .name = "bq_peaking",
+
+ .n_ports = 5,
+ .ports = bq_ports,
+
+ .instantiate = builtin_instantiate,
+ .connect_port = builtin_connect_port,
+ .run = bq_peaking_run,
+ .cleanup = builtin_cleanup,
+};
+
+/** bq_notch */
+static void bq_notch_run(void * Instance, unsigned long SampleCount)
+{
+ struct builtin *impl = Instance;
+ bq_run(impl, SampleCount, BQ_NOTCH);
+}
+
+static const struct fc_descriptor bq_notch_desc = {
+ .name = "bq_notch",
+
+ .n_ports = 5,
+ .ports = bq_ports,
+
+ .instantiate = builtin_instantiate,
+ .connect_port = builtin_connect_port,
+ .run = bq_notch_run,
+ .cleanup = builtin_cleanup,
+};
+
+
+/** bq_allpass */
+static void bq_allpass_run(void * Instance, unsigned long SampleCount)
+{
+ struct builtin *impl = Instance;
+ bq_run(impl, SampleCount, BQ_ALLPASS);
+}
+
+static const struct fc_descriptor bq_allpass_desc = {
+ .name = "bq_allpass",
+
+ .n_ports = 5,
+ .ports = bq_ports,
+
+ .instantiate = builtin_instantiate,
+ .connect_port = builtin_connect_port,
+ .run = bq_allpass_run,
+ .cleanup = builtin_cleanup,
+};
+
+/** convolve */
+struct convolver_impl {
+ unsigned long rate;
+ float *port[64];
+
+ struct convolver *conv;
+};
+
+#ifdef HAVE_SNDFILE
+static float *read_samples_from_sf(SNDFILE *f, SF_INFO info, float gain, int delay,
+ int offset, int length, int channel, long unsigned *rate, int *n_samples) {
+ float *samples;
+ int i, n;
+
+ if (length <= 0)
+ length = info.frames;
+ else
+ length = SPA_MIN(length, info.frames);
+
+ length -= SPA_MIN(offset, length);
+
+ n = delay + length;
+ if (n == 0)
+ return NULL;
+
+ samples = calloc(n * info.channels, sizeof(float));
+ if (samples == NULL)
+ return NULL;
+
+ if (offset > 0)
+ sf_seek(f, offset, SEEK_SET);
+ sf_readf_float(f, samples + (delay * info.channels), length);
+
+ channel = channel % info.channels;
+
+ for (i = 0; i < n; i++)
+ samples[i] = samples[info.channels * i + channel] * gain;
+
+ *n_samples = n;
+ *rate = info.samplerate;
+ return samples;
+}
+#endif
+
+static float *read_closest(char **filenames, float gain, int delay, int offset,
+ int length, int channel, long unsigned *rate, int *n_samples)
+{
+#ifdef HAVE_SNDFILE
+ SF_INFO infos[MAX_RATES];
+ SNDFILE *fs[MAX_RATES];
+
+ spa_zero(infos);
+ spa_zero(fs);
+
+ int diff = INT_MAX;
+ uint32_t best = 0, i;
+
+ for (i = 0; i < MAX_RATES && filenames[i] && filenames[i][0]; i++) {
+ fs[i] = sf_open(filenames[i], SFM_READ, &infos[i]);
+ if (!fs[i])
+ continue;
+
+ if (labs((long)infos[i].samplerate - (long)*rate) < diff) {
+ best = i;
+ diff = labs((long)infos[i].samplerate - (long)*rate);
+ pw_log_debug("new closest match: %d", infos[i].samplerate);
+ }
+ }
+
+ pw_log_debug("loading %s", filenames[best]);
+ float *samples = read_samples_from_sf(fs[best], infos[best], gain, delay,
+ offset, length, channel, rate, n_samples);
+
+ for (i = 0; i < MAX_RATES; i++)
+ if (fs[i])
+ sf_close(fs[i]);
+
+ return samples;
+#else
+ pw_log_error("compiled without sndfile support, can't load samples: "
+ "using dirac impulse");
+ float *samples = calloc(1, sizeof(float));
+ samples[0] = gain;
+ *n_samples = 1;
+ return samples;
+#endif
+}
+
+static float *create_hilbert(const char *filename, float gain, int delay, int offset,
+ int length, int *n_samples)
+{
+ float *samples, v;
+ int i, n, h;
+
+ if (length <= 0)
+ length = 1024;
+
+ length -= SPA_MIN(offset, length);
+
+ n = delay + length;
+ if (n == 0)
+ return NULL;
+
+ samples = calloc(n, sizeof(float));
+ if (samples == NULL)
+ return NULL;
+
+ gain *= 2 / M_PI;
+ h = length / 2;
+ for (i = 1; i < h; i += 2) {
+ v = (gain / i) * (0.43f + 0.57f * cosf(i * M_PI / h));
+ samples[delay + h + i] = -v;
+ samples[delay + h - i] = v;
+ }
+ *n_samples = n;
+ return samples;
+}
+
+static float *create_dirac(const char *filename, float gain, int delay, int offset,
+ int length, int *n_samples)
+{
+ float *samples;
+ int n;
+
+ n = delay + 1;
+
+ samples = calloc(n, sizeof(float));
+ if (samples == NULL)
+ return NULL;
+
+ samples[delay] = gain;
+
+ *n_samples = n;
+ return samples;
+}
+
+static float *resample_buffer(float *samples, int *n_samples,
+ unsigned long in_rate, unsigned long out_rate, uint32_t quality)
+{
+ uint32_t in_len, out_len, total_out = 0;
+ int out_n_samples;
+ float *out_samples, *out_buf, *in_buf;
+ struct resample r;
+ int res;
+
+ spa_zero(r);
+ r.channels = 1;
+ r.i_rate = in_rate;
+ r.o_rate = out_rate;
+ r.cpu_flags = dsp_ops->cpu_flags;
+ r.quality = quality;
+ if ((res = resample_native_init(&r)) < 0) {
+ pw_log_error("resampling failed: %s", spa_strerror(res));
+ errno = -res;
+ return NULL;
+ }
+
+ out_n_samples = SPA_ROUND_UP(*n_samples * out_rate, in_rate) / in_rate;
+ out_samples = calloc(out_n_samples, sizeof(float));
+ if (out_samples == NULL)
+ goto error;
+
+ in_len = *n_samples;
+ in_buf = samples;
+ out_len = out_n_samples;
+ out_buf = out_samples;
+
+ pw_log_info("Resampling filter: rate: %lu => %lu, n_samples: %u => %u, q:%u",
+ in_rate, out_rate, in_len, out_len, quality);
+
+ resample_process(&r, (void*)&in_buf, &in_len, (void*)&out_buf, &out_len);
+ pw_log_debug("resampled: %u -> %u samples", in_len, out_len);
+ total_out += out_len;
+
+ in_len = resample_delay(&r);
+ in_buf = calloc(in_len, sizeof(float));
+ if (in_buf == NULL)
+ goto error;
+
+ out_buf = out_samples + total_out;
+ out_len = out_n_samples - total_out;
+
+ pw_log_debug("flushing resampler: %u in %u out", in_len, out_len);
+ resample_process(&r, (void*)&in_buf, &in_len, (void*)&out_buf, &out_len);
+ pw_log_debug("flushed: %u -> %u samples", in_len, out_len);
+ total_out += out_len;
+
+ free(in_buf);
+ free(samples);
+ resample_free(&r);
+
+ *n_samples = total_out;
+
+ float gain = (float)in_rate / (float)out_rate;
+ for (uint32_t i = 0; i < total_out; i++)
+ out_samples[i] = out_samples[i] * gain;
+
+ return out_samples;
+
+error:
+ resample_free(&r);
+ free(samples);
+ free(out_samples);
+ return NULL;
+}
+
+static void * convolver_instantiate(const struct fc_descriptor * Descriptor,
+ unsigned long SampleRate, int index, const char *config)
+{
+ struct convolver_impl *impl;
+ float *samples;
+ int offset = 0, length = 0, channel = index, n_samples, len;
+ uint32_t i = 0;
+ struct spa_json it[3];
+ const char *val;
+ char key[256], v[256];
+ char *filenames[MAX_RATES] = { 0 };
+ int blocksize = 0, tailsize = 0;
+ int delay = 0;
+ int resample_quality = RESAMPLE_DEFAULT_QUALITY;
+ float gain = 1.0f;
+ unsigned long rate;
+
+ errno = EINVAL;
+ if (config == NULL)
+ return NULL;
+
+ spa_json_init(&it[0], config, strlen(config));
+ if (spa_json_enter_object(&it[0], &it[1]) <= 0)
+ return NULL;
+
+ while (spa_json_get_string(&it[1], key, sizeof(key)) > 0) {
+ if (spa_streq(key, "blocksize")) {
+ if (spa_json_get_int(&it[1], &blocksize) <= 0) {
+ pw_log_error("convolver:blocksize requires a number");
+ return NULL;
+ }
+ }
+ else if (spa_streq(key, "tailsize")) {
+ if (spa_json_get_int(&it[1], &tailsize) <= 0) {
+ pw_log_error("convolver:tailsize requires a number");
+ return NULL;
+ }
+ }
+ else if (spa_streq(key, "gain")) {
+ if (spa_json_get_float(&it[1], &gain) <= 0) {
+ pw_log_error("convolver:gain requires a number");
+ return NULL;
+ }
+ }
+ else if (spa_streq(key, "delay")) {
+ if (spa_json_get_int(&it[1], &delay) <= 0) {
+ pw_log_error("convolver:delay requires a number");
+ return NULL;
+ }
+ }
+ else if (spa_streq(key, "filename")) {
+ if ((len = spa_json_next(&it[1], &val)) <= 0) {
+ pw_log_error("convolver:filename requires a string or an array");
+ return NULL;
+ }
+ if (spa_json_is_array(val, len)) {
+ spa_json_enter(&it[1], &it[2]);
+ while (spa_json_get_string(&it[2], v, sizeof(v)) > 0 &&
+ i < SPA_N_ELEMENTS(filenames)) {
+ filenames[i] = strdup(v);
+ i++;
+ }
+ }
+ else if (spa_json_parse_stringn(val, len, v, sizeof(v)) <= 0) {
+ pw_log_error("convolver:filename requires a string or an array");
+ return NULL;
+ } else {
+ filenames[i] = strdup(v);
+ }
+ }
+ else if (spa_streq(key, "offset")) {
+ if (spa_json_get_int(&it[1], &offset) <= 0) {
+ pw_log_error("convolver:offset requires a number");
+ return NULL;
+ }
+ }
+ else if (spa_streq(key, "length")) {
+ if (spa_json_get_int(&it[1], &length) <= 0) {
+ pw_log_error("convolver:length requires a number");
+ return NULL;
+ }
+ }
+ else if (spa_streq(key, "channel")) {
+ if (spa_json_get_int(&it[1], &channel) <= 0) {
+ pw_log_error("convolver:channel requires a number");
+ return NULL;
+ }
+ }
+ else if (spa_streq(key, "resample_quality")) {
+ if (spa_json_get_int(&it[1], &resample_quality) <= 0) {
+ pw_log_error("convolver:resample_quality requires a number");
+ return NULL;
+ }
+ }
+ else if (spa_json_next(&it[1], &val) < 0)
+ break;
+ }
+ if (filenames[0] == NULL) {
+ pw_log_error("convolver:filename was not given");
+ return NULL;
+ }
+
+ if (delay < 0)
+ delay = 0;
+ if (offset < 0)
+ offset = 0;
+
+ if (spa_streq(filenames[0], "/hilbert")) {
+ samples = create_hilbert(filenames[0], gain, delay, offset,
+ length, &n_samples);
+ } else if (spa_streq(filenames[0], "/dirac")) {
+ samples = create_dirac(filenames[0], gain, delay, offset,
+ length, &n_samples);
+ } else {
+ rate = SampleRate;
+ samples = read_closest(filenames, gain, delay, offset,
+ length, channel, &rate, &n_samples);
+ if (samples != NULL && rate != SampleRate)
+ samples = resample_buffer(samples, &n_samples,
+ rate, SampleRate, resample_quality);
+ }
+ if (samples == NULL) {
+ errno = ENOENT;
+ return NULL;
+ }
+
+ for (i = 0; i < MAX_RATES; i++)
+ if (filenames[i])
+ free(filenames[i]);
+
+ if (blocksize <= 0)
+ blocksize = SPA_CLAMP(n_samples, 64, 256);
+ if (tailsize <= 0)
+ tailsize = SPA_CLAMP(4096, blocksize, 32768);
+
+ pw_log_info("using n_samples:%u %d:%d blocksize", n_samples,
+ blocksize, tailsize);
+
+ impl = calloc(1, sizeof(*impl));
+ if (impl == NULL)
+ goto error;
+
+ impl->rate = SampleRate;
+
+ impl->conv = convolver_new(dsp_ops, blocksize, tailsize, samples, n_samples);
+ if (impl->conv == NULL)
+ goto error;
+
+ free(samples);
+
+ return impl;
+error:
+ free(samples);
+ free(impl);
+ return NULL;
+}
+
+static void convolver_connect_port(void * Instance, unsigned long Port,
+ float * DataLocation)
+{
+ struct convolver_impl *impl = Instance;
+ impl->port[Port] = DataLocation;
+}
+
+static void convolver_cleanup(void * Instance)
+{
+ struct convolver_impl *impl = Instance;
+ if (impl->conv)
+ convolver_free(impl->conv);
+ free(impl);
+}
+
+static struct fc_port convolve_ports[] = {
+ { .index = 0,
+ .name = "Out",
+ .flags = FC_PORT_OUTPUT | FC_PORT_AUDIO,
+ },
+ { .index = 1,
+ .name = "In",
+ .flags = FC_PORT_INPUT | FC_PORT_AUDIO,
+ },
+};
+
+static void convolver_deactivate(void * Instance)
+{
+ struct convolver_impl *impl = Instance;
+ convolver_reset(impl->conv);
+}
+
+static void convolve_run(void * Instance, unsigned long SampleCount)
+{
+ struct convolver_impl *impl = Instance;
+ convolver_run(impl->conv, impl->port[1], impl->port[0], SampleCount);
+}
+
+static const struct fc_descriptor convolve_desc = {
+ .name = "convolver",
+
+ .n_ports = 2,
+ .ports = convolve_ports,
+
+ .instantiate = convolver_instantiate,
+ .connect_port = convolver_connect_port,
+ .deactivate = convolver_deactivate,
+ .run = convolve_run,
+ .cleanup = convolver_cleanup,
+};
+
+/** delay */
+struct delay_impl {
+ unsigned long rate;
+ float *port[4];
+
+ float delay;
+ uint32_t delay_samples;
+ uint32_t buffer_samples;
+ float *buffer;
+ uint32_t ptr;
+};
+
+static void delay_cleanup(void * Instance)
+{
+ struct delay_impl *impl = Instance;
+ free(impl->buffer);
+ free(impl);
+}
+
+static void *delay_instantiate(const struct fc_descriptor * Descriptor,
+ unsigned long SampleRate, int index, const char *config)
+{
+ struct delay_impl *impl;
+ struct spa_json it[2];
+ const char *val;
+ char key[256];
+ float max_delay = 1.0f;
+
+ if (config == NULL) {
+ errno = EINVAL;
+ return NULL;
+ }
+
+ spa_json_init(&it[0], config, strlen(config));
+ if (spa_json_enter_object(&it[0], &it[1]) <= 0)
+ return NULL;
+
+ while (spa_json_get_string(&it[1], key, sizeof(key)) > 0) {
+ if (spa_streq(key, "max-delay")) {
+ if (spa_json_get_float(&it[1], &max_delay) <= 0) {
+ pw_log_error("delay:max-delay requires a number");
+ return NULL;
+ }
+ }
+ else if (spa_json_next(&it[1], &val) < 0)
+ break;
+ }
+ if (max_delay <= 0.0f)
+ max_delay = 1.0f;
+
+ impl = calloc(1, sizeof(*impl));
+ if (impl == NULL)
+ return NULL;
+
+ impl->rate = SampleRate;
+ impl->buffer_samples = max_delay * impl->rate;
+ pw_log_info("max-delay:%f seconds rate:%lu samples:%d", max_delay, impl->rate, impl->buffer_samples);
+
+ impl->buffer = calloc(impl->buffer_samples, sizeof(float));
+ if (impl->buffer == NULL) {
+ delay_cleanup(impl);
+ return NULL;
+ }
+ return impl;
+}
+
+static void delay_connect_port(void * Instance, unsigned long Port,
+ float * DataLocation)
+{
+ struct delay_impl *impl = Instance;
+ if (Port > 2)
+ return;
+ impl->port[Port] = DataLocation;
+}
+
+static void delay_run(void * Instance, unsigned long SampleCount)
+{
+ struct delay_impl *impl = Instance;
+ float *in = impl->port[1], *out = impl->port[0];
+ float delay = impl->port[2][0];
+ unsigned long n;
+ uint32_t r, w;
+
+ if (delay != impl->delay) {
+ impl->delay_samples = SPA_CLAMP(delay * impl->rate, 0, impl->buffer_samples-1);
+ impl->delay = delay;
+ }
+ r = impl->ptr;
+ w = impl->ptr + impl->delay_samples;
+ if (w >= impl->buffer_samples)
+ w -= impl->buffer_samples;
+
+ for (n = 0; n < SampleCount; n++) {
+ impl->buffer[w] = in[n];
+ out[n] = impl->buffer[r];
+ if (++r >= impl->buffer_samples)
+ r = 0;
+ if (++w >= impl->buffer_samples)
+ w = 0;
+ }
+ impl->ptr = r;
+}
+
+static struct fc_port delay_ports[] = {
+ { .index = 0,
+ .name = "Out",
+ .flags = FC_PORT_OUTPUT | FC_PORT_AUDIO,
+ },
+ { .index = 1,
+ .name = "In",
+ .flags = FC_PORT_INPUT | FC_PORT_AUDIO,
+ },
+ { .index = 2,
+ .name = "Delay (s)",
+ .flags = FC_PORT_INPUT | FC_PORT_CONTROL,
+ .def = 0.0f, .min = 0.0f, .max = 100.0f
+ },
+};
+
+static const struct fc_descriptor delay_desc = {
+ .name = "delay",
+
+ .n_ports = 3,
+ .ports = delay_ports,
+
+ .instantiate = delay_instantiate,
+ .connect_port = delay_connect_port,
+ .run = delay_run,
+ .cleanup = delay_cleanup,
+};
+
+static const struct fc_descriptor * builtin_descriptor(unsigned long Index)
+{
+ switch(Index) {
+ case 0:
+ return &mixer_desc;
+ case 1:
+ return &bq_lowpass_desc;
+ case 2:
+ return &bq_highpass_desc;
+ case 3:
+ return &bq_bandpass_desc;
+ case 4:
+ return &bq_lowshelf_desc;
+ case 5:
+ return &bq_highshelf_desc;
+ case 6:
+ return &bq_peaking_desc;
+ case 7:
+ return &bq_notch_desc;
+ case 8:
+ return &bq_allpass_desc;
+ case 9:
+ return &copy_desc;
+ case 10:
+ return &convolve_desc;
+ case 11:
+ return &delay_desc;
+ }
+ return NULL;
+}
+
+static const struct fc_descriptor *builtin_make_desc(struct fc_plugin *plugin, const char *name)
+{
+ unsigned long i;
+ for (i = 0; ;i++) {
+ const struct fc_descriptor *d = builtin_descriptor(i);
+ if (d == NULL)
+ break;
+ if (spa_streq(d->name, name))
+ return d;
+ }
+ return NULL;
+}
+
+static struct fc_plugin builtin_plugin = {
+ .make_desc = builtin_make_desc
+};
+
+struct fc_plugin *load_builtin_plugin(const struct spa_support *support, uint32_t n_support,
+ struct dsp_ops *dsp, const char *plugin, const char *config)
+{
+ dsp_ops = dsp;
+ pffft_select_cpu(dsp->cpu_flags);
+ return &builtin_plugin;
+}
diff --git a/src/modules/module-filter-chain/convolver.c b/src/modules/module-filter-chain/convolver.c
new file mode 100644
index 0000000..6bed5b1
--- /dev/null
+++ b/src/modules/module-filter-chain/convolver.c
@@ -0,0 +1,425 @@
+/* PipeWire
+ *
+ * Copyright (c) 2017 HiFi-LoFi
+ * Copyright © 2021 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * Adapted from https://github.com/HiFi-LoFi/FFTConvolver
+ */
+#include "convolver.h"
+
+#include <spa/utils/defs.h>
+
+#include <math.h>
+
+static struct dsp_ops *dsp;
+
+struct convolver1 {
+ int blockSize;
+ int segSize;
+ int segCount;
+ int fftComplexSize;
+
+ float **segments;
+ float **segmentsIr;
+
+ float *fft_buffer;
+
+ void *fft;
+ void *ifft;
+
+ float *pre_mult;
+ float *conv;
+ float *overlap;
+
+ float *inputBuffer;
+ int inputBufferFill;
+
+ int current;
+ float scale;
+};
+
+static void *fft_alloc(int size)
+{
+ size_t nb_bytes = size * sizeof(float);
+#define ALIGNMENT 64
+ void *p, *p0 = malloc(nb_bytes + ALIGNMENT);
+ if (!p0)
+ return (void *)0;
+ p = (void *)(((size_t)p0 + ALIGNMENT) & (~((size_t)(ALIGNMENT - 1))));
+ *((void **)p - 1) = p0;
+ return p;
+}
+static void fft_free(void *p)
+{
+ if (p)
+ free(*((void **)p - 1));
+}
+
+static inline void fft_cpx_clear(float *v, int size)
+{
+ dsp_ops_clear(dsp, v, size * 2);
+}
+static float *fft_cpx_alloc(int size)
+{
+ return fft_alloc(size * 2);
+}
+
+static void fft_cpx_free(float *cpx)
+{
+ fft_free(cpx);
+}
+
+static int next_power_of_two(int val)
+{
+ int r = 1;
+ while (r < val)
+ r *= 2;
+ return r;
+}
+
+static void convolver1_reset(struct convolver1 *conv)
+{
+ int i;
+ for (i = 0; i < conv->segCount; i++)
+ fft_cpx_clear(conv->segments[i], conv->fftComplexSize);
+ dsp_ops_clear(dsp, conv->overlap, conv->blockSize);
+ dsp_ops_clear(dsp, conv->inputBuffer, conv->segSize);
+ fft_cpx_clear(conv->pre_mult, conv->fftComplexSize);
+ fft_cpx_clear(conv->conv, conv->fftComplexSize);
+ conv->inputBufferFill = 0;
+ conv->current = 0;
+}
+
+static struct convolver1 *convolver1_new(int block, const float *ir, int irlen)
+{
+ struct convolver1 *conv;
+ int i;
+
+ if (block == 0)
+ return NULL;
+
+ while (irlen > 0 && fabs(ir[irlen-1]) < 0.000001f)
+ irlen--;
+
+ conv = calloc(1, sizeof(*conv));
+ if (conv == NULL)
+ return NULL;
+
+ if (irlen == 0)
+ return conv;
+
+ conv->blockSize = next_power_of_two(block);
+ conv->segSize = 2 * conv->blockSize;
+ conv->segCount = (irlen + conv->blockSize-1) / conv->blockSize;
+ conv->fftComplexSize = (conv->segSize / 2) + 1;
+
+ conv->fft = dsp_ops_fft_new(dsp, conv->segSize, true);
+ if (conv->fft == NULL)
+ goto error;
+ conv->ifft = dsp_ops_fft_new(dsp, conv->segSize, true);
+ if (conv->ifft == NULL)
+ goto error;
+
+ conv->fft_buffer = fft_alloc(conv->segSize);
+ if (conv->fft_buffer == NULL)
+ goto error;
+
+ conv->segments = calloc(sizeof(float*), conv->segCount);
+ conv->segmentsIr = calloc(sizeof(float*), conv->segCount);
+
+ for (i = 0; i < conv->segCount; i++) {
+ int left = irlen - (i * conv->blockSize);
+ int copy = SPA_MIN(conv->blockSize, left);
+
+ conv->segments[i] = fft_cpx_alloc(conv->fftComplexSize);
+ conv->segmentsIr[i] = fft_cpx_alloc(conv->fftComplexSize);
+
+ dsp_ops_copy(dsp, conv->fft_buffer, &ir[i * conv->blockSize], copy);
+ if (copy < conv->segSize)
+ dsp_ops_clear(dsp, conv->fft_buffer + copy, conv->segSize - copy);
+
+ dsp_ops_fft_run(dsp, conv->fft, 1, conv->fft_buffer, conv->segmentsIr[i]);
+ }
+ conv->pre_mult = fft_cpx_alloc(conv->fftComplexSize);
+ conv->conv = fft_cpx_alloc(conv->fftComplexSize);
+ conv->overlap = fft_alloc(conv->blockSize);
+ conv->inputBuffer = fft_alloc(conv->segSize);
+ conv->scale = 1.0f / conv->segSize;
+ convolver1_reset(conv);
+
+ return conv;
+error:
+ if (conv->fft)
+ dsp_ops_fft_free(dsp, conv->fft);
+ if (conv->ifft)
+ dsp_ops_fft_free(dsp, conv->ifft);
+ if (conv->fft_buffer)
+ fft_free(conv->fft_buffer);
+ free(conv);
+ return NULL;
+}
+
+static void convolver1_free(struct convolver1 *conv)
+{
+ int i;
+ for (i = 0; i < conv->segCount; i++) {
+ fft_cpx_free(conv->segments[i]);
+ fft_cpx_free(conv->segmentsIr[i]);
+ }
+ if (conv->fft)
+ dsp_ops_fft_free(dsp, conv->fft);
+ if (conv->ifft)
+ dsp_ops_fft_free(dsp, conv->ifft);
+ if (conv->fft_buffer)
+ fft_free(conv->fft_buffer);
+ free(conv->segments);
+ free(conv->segmentsIr);
+ fft_cpx_free(conv->pre_mult);
+ fft_cpx_free(conv->conv);
+ fft_free(conv->overlap);
+ fft_free(conv->inputBuffer);
+ free(conv);
+}
+
+static int convolver1_run(struct convolver1 *conv, const float *input, float *output, int len)
+{
+ int i, processed = 0;
+
+ if (conv == NULL || conv->segCount == 0) {
+ dsp_ops_clear(dsp, output, len);
+ return len;
+ }
+
+ while (processed < len) {
+ const int processing = SPA_MIN(len - processed, conv->blockSize - conv->inputBufferFill);
+ const int inputBufferPos = conv->inputBufferFill;
+
+ dsp_ops_copy(dsp, conv->inputBuffer + inputBufferPos, input + processed, processing);
+ if (inputBufferPos == 0 && processing < conv->blockSize)
+ dsp_ops_clear(dsp, conv->inputBuffer + processing, conv->blockSize - processing);
+
+ dsp_ops_fft_run(dsp, conv->fft, 1, conv->inputBuffer, conv->segments[conv->current]);
+
+ if (conv->segCount > 1) {
+ if (conv->inputBufferFill == 0) {
+ int indexAudio = (conv->current + 1) % conv->segCount;
+
+ dsp_ops_fft_cmul(dsp, conv->fft, conv->pre_mult,
+ conv->segmentsIr[1],
+ conv->segments[indexAudio],
+ conv->fftComplexSize, conv->scale);
+
+ for (i = 2; i < conv->segCount; i++) {
+ indexAudio = (conv->current + i) % conv->segCount;
+
+ dsp_ops_fft_cmuladd(dsp, conv->fft,
+ conv->pre_mult,
+ conv->pre_mult,
+ conv->segmentsIr[i],
+ conv->segments[indexAudio],
+ conv->fftComplexSize, conv->scale);
+ }
+ }
+ dsp_ops_fft_cmuladd(dsp, conv->fft,
+ conv->conv,
+ conv->pre_mult,
+ conv->segments[conv->current],
+ conv->segmentsIr[0],
+ conv->fftComplexSize, conv->scale);
+ } else {
+ dsp_ops_fft_cmul(dsp, conv->fft,
+ conv->conv,
+ conv->segments[conv->current],
+ conv->segmentsIr[0],
+ conv->fftComplexSize, conv->scale);
+ }
+
+ dsp_ops_fft_run(dsp, conv->ifft, -1, conv->conv, conv->fft_buffer);
+
+ dsp_ops_sum(dsp, output + processed, conv->fft_buffer + inputBufferPos,
+ conv->overlap + inputBufferPos, processing);
+
+ conv->inputBufferFill += processing;
+ if (conv->inputBufferFill == conv->blockSize) {
+ conv->inputBufferFill = 0;
+
+ dsp_ops_copy(dsp, conv->overlap, conv->fft_buffer + conv->blockSize, conv->blockSize);
+
+ conv->current = (conv->current > 0) ? (conv->current - 1) : (conv->segCount - 1);
+ }
+
+ processed += processing;
+ }
+ return len;
+}
+
+struct convolver
+{
+ int headBlockSize;
+ int tailBlockSize;
+ struct convolver1 *headConvolver;
+ struct convolver1 *tailConvolver0;
+ float *tailOutput0;
+ float *tailPrecalculated0;
+ struct convolver1 *tailConvolver;
+ float *tailOutput;
+ float *tailPrecalculated;
+ float *tailInput;
+ int tailInputFill;
+ int precalculatedPos;
+};
+
+void convolver_reset(struct convolver *conv)
+{
+ if (conv->headConvolver)
+ convolver1_reset(conv->headConvolver);
+ if (conv->tailConvolver0) {
+ convolver1_reset(conv->tailConvolver0);
+ dsp_ops_clear(dsp, conv->tailOutput0, conv->tailBlockSize);
+ dsp_ops_clear(dsp, conv->tailPrecalculated0, conv->tailBlockSize);
+ }
+ if (conv->tailConvolver) {
+ convolver1_reset(conv->tailConvolver);
+ dsp_ops_clear(dsp, conv->tailOutput, conv->tailBlockSize);
+ dsp_ops_clear(dsp, conv->tailPrecalculated, conv->tailBlockSize);
+ }
+ conv->tailInputFill = 0;
+ conv->precalculatedPos = 0;
+}
+
+struct convolver *convolver_new(struct dsp_ops *dsp_ops, int head_block, int tail_block, const float *ir, int irlen)
+{
+ struct convolver *conv;
+ int head_ir_len;
+
+ dsp = dsp_ops;
+
+ if (head_block == 0 || tail_block == 0)
+ return NULL;
+
+ head_block = SPA_MAX(1, head_block);
+ if (head_block > tail_block)
+ SPA_SWAP(head_block, tail_block);
+
+ while (irlen > 0 && fabs(ir[irlen-1]) < 0.000001f)
+ irlen--;
+
+ conv = calloc(1, sizeof(*conv));
+ if (conv == NULL)
+ return NULL;
+
+ if (irlen == 0)
+ return conv;
+
+ conv->headBlockSize = next_power_of_two(head_block);
+ conv->tailBlockSize = next_power_of_two(tail_block);
+
+ head_ir_len = SPA_MIN(irlen, conv->tailBlockSize);
+ conv->headConvolver = convolver1_new(conv->headBlockSize, ir, head_ir_len);
+
+ if (irlen > conv->tailBlockSize) {
+ int conv1IrLen = SPA_MIN(irlen - conv->tailBlockSize, conv->tailBlockSize);
+ conv->tailConvolver0 = convolver1_new(conv->headBlockSize, ir + conv->tailBlockSize, conv1IrLen);
+ conv->tailOutput0 = fft_alloc(conv->tailBlockSize);
+ conv->tailPrecalculated0 = fft_alloc(conv->tailBlockSize);
+ }
+
+ if (irlen > 2 * conv->tailBlockSize) {
+ int tailIrLen = irlen - (2 * conv->tailBlockSize);
+ conv->tailConvolver = convolver1_new(conv->tailBlockSize, ir + (2 * conv->tailBlockSize), tailIrLen);
+ conv->tailOutput = fft_alloc(conv->tailBlockSize);
+ conv->tailPrecalculated = fft_alloc(conv->tailBlockSize);
+ }
+
+ if (conv->tailConvolver0 || conv->tailConvolver)
+ conv->tailInput = fft_alloc(conv->tailBlockSize);
+
+ convolver_reset(conv);
+
+ return conv;
+}
+
+void convolver_free(struct convolver *conv)
+{
+ if (conv->headConvolver)
+ convolver1_free(conv->headConvolver);
+ if (conv->tailConvolver0)
+ convolver1_free(conv->tailConvolver0);
+ if (conv->tailConvolver)
+ convolver1_free(conv->tailConvolver);
+ fft_free(conv->tailOutput0);
+ fft_free(conv->tailPrecalculated0);
+ fft_free(conv->tailOutput);
+ fft_free(conv->tailPrecalculated);
+ fft_free(conv->tailInput);
+ free(conv);
+}
+
+int convolver_run(struct convolver *conv, const float *input, float *output, int length)
+{
+ convolver1_run(conv->headConvolver, input, output, length);
+
+ if (conv->tailInput) {
+ int processed = 0;
+
+ while (processed < length) {
+ int remaining = length - processed;
+ int processing = SPA_MIN(remaining, conv->headBlockSize - (conv->tailInputFill % conv->headBlockSize));
+
+ if (conv->tailPrecalculated0)
+ dsp_ops_sum(dsp, &output[processed], &output[processed],
+ &conv->tailPrecalculated0[conv->precalculatedPos],
+ processing);
+ if (conv->tailPrecalculated)
+ dsp_ops_sum(dsp, &output[processed], &output[processed],
+ &conv->tailPrecalculated[conv->precalculatedPos],
+ processing);
+ conv->precalculatedPos += processing;
+
+ dsp_ops_copy(dsp, conv->tailInput + conv->tailInputFill, input + processed, processing);
+ conv->tailInputFill += processing;
+
+ if (conv->tailPrecalculated0 && (conv->tailInputFill % conv->headBlockSize == 0)) {
+ int blockOffset = conv->tailInputFill - conv->headBlockSize;
+ convolver1_run(conv->tailConvolver0,
+ conv->tailInput + blockOffset,
+ conv->tailOutput0 + blockOffset,
+ conv->headBlockSize);
+ if (conv->tailInputFill == conv->tailBlockSize)
+ SPA_SWAP(conv->tailPrecalculated0, conv->tailOutput0);
+ }
+
+ if (conv->tailPrecalculated &&
+ conv->tailInputFill == conv->tailBlockSize) {
+ SPA_SWAP(conv->tailPrecalculated, conv->tailOutput);
+ convolver1_run(conv->tailConvolver, conv->tailInput,
+ conv->tailOutput, conv->tailBlockSize);
+ }
+ if (conv->tailInputFill == conv->tailBlockSize) {
+ conv->tailInputFill = 0;
+ conv->precalculatedPos = 0;
+ }
+ processed += processing;
+ }
+ }
+ return 0;
+}
diff --git a/src/modules/module-filter-chain/convolver.h b/src/modules/module-filter-chain/convolver.h
new file mode 100644
index 0000000..1988114
--- /dev/null
+++ b/src/modules/module-filter-chain/convolver.h
@@ -0,0 +1,34 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdint.h>
+#include <stddef.h>
+
+#include "dsp-ops.h"
+
+struct convolver *convolver_new(struct dsp_ops *dsp, int block, int tail, const float *ir, int irlen);
+void convolver_free(struct convolver *conv);
+
+void convolver_reset(struct convolver *conv);
+int convolver_run(struct convolver *conv, const float *input, float *output, int length);
diff --git a/src/modules/module-filter-chain/dsp-ops-avx.c b/src/modules/module-filter-chain/dsp-ops-avx.c
new file mode 100644
index 0000000..7ea5456
--- /dev/null
+++ b/src/modules/module-filter-chain/dsp-ops-avx.c
@@ -0,0 +1,85 @@
+/* Spa
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <math.h>
+
+#include <spa/utils/defs.h>
+
+#include "dsp-ops.h"
+
+#include <immintrin.h>
+
+void dsp_sum_avx(struct dsp_ops *ops, float *r, const float *a, const float *b, uint32_t n_samples)
+{
+ uint32_t n, unrolled;
+ __m256 in[4];
+
+ unrolled = n_samples & ~31;
+
+ if (SPA_LIKELY(SPA_IS_ALIGNED(r, 32)) &&
+ SPA_LIKELY(SPA_IS_ALIGNED(a, 32)) &&
+ SPA_LIKELY(SPA_IS_ALIGNED(b, 32))) {
+ for (n = 0; n < unrolled; n += 32) {
+ in[0] = _mm256_load_ps(&a[n+ 0]);
+ in[1] = _mm256_load_ps(&a[n+ 8]);
+ in[2] = _mm256_load_ps(&a[n+16]);
+ in[3] = _mm256_load_ps(&a[n+24]);
+
+ in[0] = _mm256_add_ps(in[0], _mm256_load_ps(&b[n+ 0]));
+ in[1] = _mm256_add_ps(in[1], _mm256_load_ps(&b[n+ 8]));
+ in[2] = _mm256_add_ps(in[2], _mm256_load_ps(&b[n+16]));
+ in[3] = _mm256_add_ps(in[3], _mm256_load_ps(&b[n+24]));
+
+ _mm256_store_ps(&r[n+ 0], in[0]);
+ _mm256_store_ps(&r[n+ 8], in[1]);
+ _mm256_store_ps(&r[n+16], in[2]);
+ _mm256_store_ps(&r[n+24], in[3]);
+ }
+ } else {
+ for (n = 0; n < unrolled; n += 32) {
+ in[0] = _mm256_loadu_ps(&a[n+ 0]);
+ in[1] = _mm256_loadu_ps(&a[n+ 8]);
+ in[2] = _mm256_loadu_ps(&a[n+16]);
+ in[3] = _mm256_loadu_ps(&a[n+24]);
+
+ in[0] = _mm256_add_ps(in[0], _mm256_loadu_ps(&b[n+ 0]));
+ in[1] = _mm256_add_ps(in[1], _mm256_loadu_ps(&b[n+ 8]));
+ in[2] = _mm256_add_ps(in[2], _mm256_loadu_ps(&b[n+16]));
+ in[3] = _mm256_add_ps(in[3], _mm256_loadu_ps(&b[n+24]));
+
+ _mm256_storeu_ps(&r[n+ 0], in[0]);
+ _mm256_storeu_ps(&r[n+ 8], in[1]);
+ _mm256_storeu_ps(&r[n+16], in[2]);
+ _mm256_storeu_ps(&r[n+24], in[3]);
+ }
+ }
+ for (; n < n_samples; n++) {
+ __m128 in[1];
+ in[0] = _mm_load_ss(&a[n]);
+ in[0] = _mm_add_ss(in[0], _mm_load_ss(&b[n]));
+ _mm_store_ss(&r[n], in[0]);
+ }
+}
diff --git a/src/modules/module-filter-chain/dsp-ops-c.c b/src/modules/module-filter-chain/dsp-ops-c.c
new file mode 100644
index 0000000..a913f5a
--- /dev/null
+++ b/src/modules/module-filter-chain/dsp-ops-c.c
@@ -0,0 +1,174 @@
+/* Spa
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <math.h>
+#include <float.h>
+
+#include <spa/utils/defs.h>
+
+#include "pffft.h"
+#include "dsp-ops.h"
+
+void dsp_clear_c(struct dsp_ops *ops, void * SPA_RESTRICT dst, uint32_t n_samples)
+{
+ memset(dst, 0, sizeof(float) * n_samples);
+}
+
+static inline void dsp_add_c(struct dsp_ops *ops, void * SPA_RESTRICT dst,
+ const void * SPA_RESTRICT src, uint32_t n_samples)
+{
+ uint32_t i;
+ const float *s = src;
+ float *d = dst;
+ for (i = 0; i < n_samples; i++)
+ d[i] += s[i];
+}
+
+static inline void dsp_gain_c(struct dsp_ops *ops, void * SPA_RESTRICT dst,
+ const void * SPA_RESTRICT src, float gain, uint32_t n_samples)
+{
+ uint32_t i;
+ const float *s = src;
+ float *d = dst;
+ for (i = 0; i < n_samples; i++)
+ d[i] = s[i] * gain;
+}
+
+static inline void dsp_gain_add_c(struct dsp_ops *ops, void * SPA_RESTRICT dst,
+ const void * SPA_RESTRICT src, float gain, uint32_t n_samples)
+{
+ uint32_t i;
+ const float *s = src;
+ float *d = dst;
+ for (i = 0; i < n_samples; i++)
+ d[i] += s[i] * gain;
+}
+
+
+void dsp_copy_c(struct dsp_ops *ops, void * SPA_RESTRICT dst,
+ const void * SPA_RESTRICT src, uint32_t n_samples)
+{
+ if (dst != src)
+ spa_memcpy(dst, src, sizeof(float) * n_samples);
+}
+
+void dsp_mix_gain_c(struct dsp_ops *ops,
+ void * SPA_RESTRICT dst,
+ const void * SPA_RESTRICT src[],
+ float gain[], uint32_t n_src, uint32_t n_samples)
+{
+ uint32_t i;
+ if (n_src == 0) {
+ dsp_clear_c(ops, dst, n_samples);
+ } else if (n_src == 1) {
+ if (dst != src[0])
+ dsp_copy_c(ops, dst, src[0], n_samples);
+ } else {
+ if (gain[0] == 1.0f)
+ dsp_copy_c(ops, dst, src[0], n_samples);
+ else
+ dsp_gain_c(ops, dst, src[0], gain[0], n_samples);
+
+ for (i = 1; i < n_src; i++) {
+ if (gain[i] == 1.0f)
+ dsp_add_c(ops, dst, src[i], n_samples);
+ else
+ dsp_gain_add_c(ops, dst, src[i], gain[i], n_samples);
+ }
+ }
+}
+
+void dsp_biquad_run_c(struct dsp_ops *ops, struct biquad *bq,
+ float *out, const float *in, uint32_t n_samples)
+{
+ float x1, x2, y1, y2;
+ float b0, b1, b2, a1, a2;
+ uint32_t i;
+
+ x1 = bq->x1;
+ x2 = bq->x2;
+ y1 = bq->y1;
+ y2 = bq->y2;
+ b0 = bq->b0;
+ b1 = bq->b1;
+ b2 = bq->b2;
+ a1 = bq->a1;
+ a2 = bq->a2;
+ for (i = 0; i < n_samples; i++) {
+ float x = in[i];
+ float y = b0 * x + b1 * x1 + b2 * x2 - a1 * y1 - a2 * y2;
+ out[i] = y;
+ x2 = x1;
+ x1 = x;
+ y2 = y1;
+ y1 = y;
+ }
+#define F(x) (-FLT_MIN < (x) && (x) < FLT_MIN ? 0.0f : (x))
+ bq->x1 = F(x1);
+ bq->x2 = F(x2);
+ bq->y1 = F(y1);
+ bq->y2 = F(y2);
+#undef F
+}
+
+void dsp_sum_c(struct dsp_ops *ops, float * dst,
+ const float * SPA_RESTRICT a, const float * SPA_RESTRICT b, uint32_t n_samples)
+{
+ uint32_t i;
+ for (i = 0; i < n_samples; i++)
+ dst[i] = a[i] + b[i];
+}
+
+void *dsp_fft_new_c(struct dsp_ops *ops, int32_t size, bool real)
+{
+ return pffft_new_setup(size, real ? PFFFT_REAL : PFFFT_COMPLEX);
+}
+
+void dsp_fft_free_c(struct dsp_ops *ops, void *fft)
+{
+ pffft_destroy_setup(fft);
+}
+void dsp_fft_run_c(struct dsp_ops *ops, void *fft, int direction,
+ const float * SPA_RESTRICT src, float * SPA_RESTRICT dst)
+{
+ pffft_transform(fft, src, dst, NULL, direction < 0 ? PFFFT_BACKWARD : PFFFT_FORWARD);
+}
+
+void dsp_fft_cmul_c(struct dsp_ops *ops, void *fft,
+ float * SPA_RESTRICT dst, const float * SPA_RESTRICT a,
+ const float * SPA_RESTRICT b, uint32_t len, const float scale)
+{
+ pffft_zconvolve(fft, a, b, dst, scale);
+}
+
+void dsp_fft_cmuladd_c(struct dsp_ops *ops, void *fft,
+ float * SPA_RESTRICT dst, const float * SPA_RESTRICT src,
+ const float * SPA_RESTRICT a, const float * SPA_RESTRICT b,
+ uint32_t len, const float scale)
+{
+ pffft_zconvolve_accumulate(fft, a, b, src, dst, scale);
+}
+
diff --git a/src/modules/module-filter-chain/dsp-ops-sse.c b/src/modules/module-filter-chain/dsp-ops-sse.c
new file mode 100644
index 0000000..bcc3499
--- /dev/null
+++ b/src/modules/module-filter-chain/dsp-ops-sse.c
@@ -0,0 +1,142 @@
+/* Spa
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <math.h>
+
+#include <spa/utils/defs.h>
+
+#include "dsp-ops.h"
+
+#include <xmmintrin.h>
+
+void dsp_mix_gain_sse(struct dsp_ops *ops,
+ void * SPA_RESTRICT dst,
+ const void * SPA_RESTRICT src[],
+ float gain[], uint32_t n_src, uint32_t n_samples)
+{
+ if (n_src == 0) {
+ memset(dst, 0, n_samples * sizeof(float));
+ } else if (n_src == 1) {
+ if (dst != src[0])
+ spa_memcpy(dst, src[0], n_samples * sizeof(float));
+ } else {
+ uint32_t n, i, unrolled;
+ __m128 in[4], g;
+ const float **s = (const float **)src;
+ float *d = dst;
+
+ if (SPA_LIKELY(SPA_IS_ALIGNED(dst, 16))) {
+ unrolled = n_samples & ~15;
+ for (i = 0; i < n_src; i++) {
+ if (SPA_UNLIKELY(!SPA_IS_ALIGNED(src[i], 16))) {
+ unrolled = 0;
+ break;
+ }
+ }
+ } else
+ unrolled = 0;
+
+ for (n = 0; n < unrolled; n += 16) {
+ g = _mm_set1_ps(gain[0]);
+ in[0] = _mm_mul_ps(g, _mm_load_ps(&s[0][n+ 0]));
+ in[1] = _mm_mul_ps(g, _mm_load_ps(&s[0][n+ 4]));
+ in[2] = _mm_mul_ps(g, _mm_load_ps(&s[0][n+ 8]));
+ in[3] = _mm_mul_ps(g, _mm_load_ps(&s[0][n+12]));
+
+ for (i = 1; i < n_src; i++) {
+ g = _mm_set1_ps(gain[i]);
+ in[0] = _mm_add_ps(in[0], _mm_mul_ps(g, _mm_load_ps(&s[i][n+ 0])));
+ in[1] = _mm_add_ps(in[1], _mm_mul_ps(g, _mm_load_ps(&s[i][n+ 4])));
+ in[2] = _mm_add_ps(in[2], _mm_mul_ps(g, _mm_load_ps(&s[i][n+ 8])));
+ in[3] = _mm_add_ps(in[3], _mm_mul_ps(g, _mm_load_ps(&s[i][n+12])));
+ }
+ _mm_store_ps(&d[n+ 0], in[0]);
+ _mm_store_ps(&d[n+ 4], in[1]);
+ _mm_store_ps(&d[n+ 8], in[2]);
+ _mm_store_ps(&d[n+12], in[3]);
+ }
+ for (; n < n_samples; n++) {
+ g = _mm_set_ss(gain[0]);
+ in[0] = _mm_mul_ss(g, _mm_load_ss(&s[0][n]));
+ for (i = 1; i < n_src; i++) {
+ g = _mm_set_ss(gain[i]);
+ in[0] = _mm_add_ss(in[0], _mm_mul_ss(g, _mm_load_ss(&s[i][n])));
+ }
+ _mm_store_ss(&d[n], in[0]);
+ }
+ }
+}
+
+void dsp_sum_sse(struct dsp_ops *ops, float *r, const float *a, const float *b, uint32_t n_samples)
+{
+ uint32_t n, unrolled;
+ __m128 in[4];
+
+ unrolled = n_samples & ~15;
+
+ if (SPA_LIKELY(SPA_IS_ALIGNED(r, 16)) &&
+ SPA_LIKELY(SPA_IS_ALIGNED(a, 16)) &&
+ SPA_LIKELY(SPA_IS_ALIGNED(b, 16))) {
+ for (n = 0; n < unrolled; n += 16) {
+ in[0] = _mm_load_ps(&a[n+ 0]);
+ in[1] = _mm_load_ps(&a[n+ 4]);
+ in[2] = _mm_load_ps(&a[n+ 8]);
+ in[3] = _mm_load_ps(&a[n+12]);
+
+ in[0] = _mm_add_ps(in[0], _mm_load_ps(&b[n+ 0]));
+ in[1] = _mm_add_ps(in[1], _mm_load_ps(&b[n+ 4]));
+ in[2] = _mm_add_ps(in[2], _mm_load_ps(&b[n+ 8]));
+ in[3] = _mm_add_ps(in[3], _mm_load_ps(&b[n+12]));
+
+ _mm_store_ps(&r[n+ 0], in[0]);
+ _mm_store_ps(&r[n+ 4], in[1]);
+ _mm_store_ps(&r[n+ 8], in[2]);
+ _mm_store_ps(&r[n+12], in[3]);
+ }
+ } else {
+ for (n = 0; n < unrolled; n += 16) {
+ in[0] = _mm_loadu_ps(&a[n+ 0]);
+ in[1] = _mm_loadu_ps(&a[n+ 4]);
+ in[2] = _mm_loadu_ps(&a[n+ 8]);
+ in[3] = _mm_loadu_ps(&a[n+12]);
+
+ in[0] = _mm_add_ps(in[0], _mm_loadu_ps(&b[n+ 0]));
+ in[1] = _mm_add_ps(in[1], _mm_loadu_ps(&b[n+ 4]));
+ in[2] = _mm_add_ps(in[2], _mm_loadu_ps(&b[n+ 8]));
+ in[3] = _mm_add_ps(in[3], _mm_loadu_ps(&b[n+12]));
+
+ _mm_storeu_ps(&r[n+ 0], in[0]);
+ _mm_storeu_ps(&r[n+ 4], in[1]);
+ _mm_storeu_ps(&r[n+ 8], in[2]);
+ _mm_storeu_ps(&r[n+12], in[3]);
+ }
+ }
+ for (; n < n_samples; n++) {
+ in[0] = _mm_load_ss(&a[n]);
+ in[0] = _mm_add_ss(in[0], _mm_load_ss(&b[n]));
+ _mm_store_ss(&r[n], in[0]);
+ }
+}
diff --git a/src/modules/module-filter-chain/dsp-ops.c b/src/modules/module-filter-chain/dsp-ops.c
new file mode 100644
index 0000000..d35d5c9
--- /dev/null
+++ b/src/modules/module-filter-chain/dsp-ops.c
@@ -0,0 +1,114 @@
+/* Spa
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <math.h>
+
+#include <spa/support/cpu.h>
+#include <spa/utils/defs.h>
+#include <spa/param/audio/format-utils.h>
+
+#include "dsp-ops.h"
+
+struct dsp_info {
+ uint32_t cpu_flags;
+
+ struct dsp_ops_funcs funcs;
+};
+
+static struct dsp_info dsp_table[] =
+{
+#if defined (HAVE_AVX)
+ { SPA_CPU_FLAG_AVX,
+ .funcs.clear = dsp_clear_c,
+ .funcs.copy = dsp_copy_c,
+ .funcs.mix_gain = dsp_mix_gain_sse,
+ .funcs.biquad_run = dsp_biquad_run_c,
+ .funcs.sum = dsp_sum_avx,
+ .funcs.fft_new = dsp_fft_new_c,
+ .funcs.fft_free = dsp_fft_free_c,
+ .funcs.fft_run = dsp_fft_run_c,
+ .funcs.fft_cmul = dsp_fft_cmul_c,
+ .funcs.fft_cmuladd = dsp_fft_cmuladd_c,
+ },
+#endif
+#if defined (HAVE_SSE)
+ { SPA_CPU_FLAG_SSE,
+ .funcs.clear = dsp_clear_c,
+ .funcs.copy = dsp_copy_c,
+ .funcs.mix_gain = dsp_mix_gain_sse,
+ .funcs.biquad_run = dsp_biquad_run_c,
+ .funcs.sum = dsp_sum_sse,
+ .funcs.fft_new = dsp_fft_new_c,
+ .funcs.fft_free = dsp_fft_free_c,
+ .funcs.fft_run = dsp_fft_run_c,
+ .funcs.fft_cmul = dsp_fft_cmul_c,
+ .funcs.fft_cmuladd = dsp_fft_cmuladd_c,
+ },
+#endif
+ { 0,
+ .funcs.clear = dsp_clear_c,
+ .funcs.copy = dsp_copy_c,
+ .funcs.mix_gain = dsp_mix_gain_c,
+ .funcs.biquad_run = dsp_biquad_run_c,
+ .funcs.sum = dsp_sum_c,
+ .funcs.fft_new = dsp_fft_new_c,
+ .funcs.fft_free = dsp_fft_free_c,
+ .funcs.fft_run = dsp_fft_run_c,
+ .funcs.fft_cmul = dsp_fft_cmul_c,
+ .funcs.fft_cmuladd = dsp_fft_cmuladd_c,
+ },
+};
+
+#define MATCH_CPU_FLAGS(a,b) ((a) == 0 || ((a) & (b)) == a)
+
+static const struct dsp_info *find_dsp_info(uint32_t cpu_flags)
+{
+ SPA_FOR_EACH_ELEMENT_VAR(dsp_table, t) {
+ if (MATCH_CPU_FLAGS(t->cpu_flags, cpu_flags))
+ return t;
+ }
+ return NULL;
+}
+
+static void impl_dsp_ops_free(struct dsp_ops *ops)
+{
+ spa_zero(*ops);
+}
+
+int dsp_ops_init(struct dsp_ops *ops)
+{
+ const struct dsp_info *info;
+
+ info = find_dsp_info(ops->cpu_flags);
+ if (info == NULL)
+ return -ENOTSUP;
+
+ ops->priv = info;
+ ops->free = impl_dsp_ops_free;
+ ops->funcs = info->funcs;
+
+ return 0;
+}
diff --git a/src/modules/module-filter-chain/dsp-ops.h b/src/modules/module-filter-chain/dsp-ops.h
new file mode 100644
index 0000000..bd77062
--- /dev/null
+++ b/src/modules/module-filter-chain/dsp-ops.h
@@ -0,0 +1,140 @@
+/* Spa
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef DSP_OPS_H
+#define DSP_OPS_H
+
+#include <spa/utils/defs.h>
+
+#include "biquad.h"
+
+struct dsp_ops;
+
+struct dsp_ops_funcs {
+ void (*clear) (struct dsp_ops *ops, void * SPA_RESTRICT dst, uint32_t n_samples);
+ void (*copy) (struct dsp_ops *ops,
+ void * SPA_RESTRICT dst,
+ const void * SPA_RESTRICT src, uint32_t n_samples);
+ void (*mix_gain) (struct dsp_ops *ops,
+ void * SPA_RESTRICT dst,
+ const void * SPA_RESTRICT src[],
+ float gain[], uint32_t n_src, uint32_t n_samples);
+ void (*biquad_run) (struct dsp_ops *ops, struct biquad *bq,
+ float *out, const float *in, uint32_t n_samples);
+ void (*sum) (struct dsp_ops *ops,
+ float * dst, const float * SPA_RESTRICT a,
+ const float * SPA_RESTRICT b, uint32_t n_samples);
+
+ void *(*fft_new) (struct dsp_ops *ops, int32_t size, bool real);
+ void (*fft_free) (struct dsp_ops *ops, void *fft);
+ void (*fft_run) (struct dsp_ops *ops, void *fft, int direction,
+ const float * SPA_RESTRICT src, float * SPA_RESTRICT dst);
+ void (*fft_cmul) (struct dsp_ops *ops, void *fft,
+ float * SPA_RESTRICT dst, const float * SPA_RESTRICT a,
+ const float * SPA_RESTRICT b, uint32_t len, const float scale);
+ void (*fft_cmuladd) (struct dsp_ops *ops, void *fft,
+ float * dst, const float * src,
+ const float * SPA_RESTRICT a, const float * SPA_RESTRICT b,
+ uint32_t len, const float scale);
+};
+
+struct dsp_ops {
+ uint32_t cpu_flags;
+
+ void (*free) (struct dsp_ops *ops);
+
+ struct dsp_ops_funcs funcs;
+
+ const void *priv;
+};
+
+int dsp_ops_init(struct dsp_ops *ops);
+
+#define dsp_ops_free(ops) (ops)->free(ops)
+
+#define dsp_ops_clear(ops,...) (ops)->funcs.clear(ops, __VA_ARGS__)
+#define dsp_ops_copy(ops,...) (ops)->funcs.copy(ops, __VA_ARGS__)
+#define dsp_ops_mix_gain(ops,...) (ops)->funcs.mix_gain(ops, __VA_ARGS__)
+#define dsp_ops_biquad_run(ops,...) (ops)->funcs.biquad_run(ops, __VA_ARGS__)
+#define dsp_ops_sum(ops,...) (ops)->funcs.sum(ops, __VA_ARGS__)
+
+#define dsp_ops_fft_new(ops,...) (ops)->funcs.fft_new(ops, __VA_ARGS__)
+#define dsp_ops_fft_free(ops,...) (ops)->funcs.fft_free(ops, __VA_ARGS__)
+#define dsp_ops_fft_run(ops,...) (ops)->funcs.fft_run(ops, __VA_ARGS__)
+#define dsp_ops_fft_cmul(ops,...) (ops)->funcs.fft_cmul(ops, __VA_ARGS__)
+#define dsp_ops_fft_cmuladd(ops,...) (ops)->funcs.fft_cmuladd(ops, __VA_ARGS__)
+
+#define MAKE_CLEAR_FUNC(arch) \
+void dsp_clear_##arch(struct dsp_ops *ops, void * SPA_RESTRICT dst, uint32_t n_samples)
+#define MAKE_COPY_FUNC(arch) \
+void dsp_copy_##arch(struct dsp_ops *ops, void * SPA_RESTRICT dst, \
+ const void * SPA_RESTRICT src, uint32_t n_samples)
+#define MAKE_MIX_GAIN_FUNC(arch) \
+void dsp_mix_gain_##arch(struct dsp_ops *ops, void * SPA_RESTRICT dst, \
+ const void * SPA_RESTRICT src[], float gain[], uint32_t n_src, uint32_t n_samples)
+#define MAKE_BIQUAD_RUN_FUNC(arch) \
+void dsp_biquad_run_##arch (struct dsp_ops *ops, struct biquad *bq, \
+ float *out, const float *in, uint32_t n_samples)
+#define MAKE_SUM_FUNC(arch) \
+void dsp_sum_##arch (struct dsp_ops *ops, float * SPA_RESTRICT dst, \
+ const float * SPA_RESTRICT a, const float * SPA_RESTRICT b, uint32_t n_samples)
+
+#define MAKE_FFT_NEW_FUNC(arch) \
+void *dsp_fft_new_##arch(struct dsp_ops *ops, int32_t size, bool real)
+#define MAKE_FFT_FREE_FUNC(arch) \
+void dsp_fft_free_##arch(struct dsp_ops *ops, void *fft)
+#define MAKE_FFT_RUN_FUNC(arch) \
+void dsp_fft_run_##arch(struct dsp_ops *ops, void *fft, int direction, \
+ const float * SPA_RESTRICT src, float * SPA_RESTRICT dst)
+#define MAKE_FFT_CMUL_FUNC(arch) \
+void dsp_fft_cmul_##arch(struct dsp_ops *ops, void *fft, \
+ float * SPA_RESTRICT dst, const float * SPA_RESTRICT a, \
+ const float * SPA_RESTRICT b, uint32_t len, const float scale)
+#define MAKE_FFT_CMULADD_FUNC(arch) \
+void dsp_fft_cmuladd_##arch(struct dsp_ops *ops, void *fft, \
+ float * dst, const float * src, \
+ const float * SPA_RESTRICT a, const float * SPA_RESTRICT b, \
+ uint32_t len, const float scale)
+
+MAKE_CLEAR_FUNC(c);
+MAKE_COPY_FUNC(c);
+MAKE_MIX_GAIN_FUNC(c);
+MAKE_BIQUAD_RUN_FUNC(c);
+MAKE_SUM_FUNC(c);
+
+MAKE_FFT_NEW_FUNC(c);
+MAKE_FFT_FREE_FUNC(c);
+MAKE_FFT_RUN_FUNC(c);
+MAKE_FFT_CMUL_FUNC(c);
+MAKE_FFT_CMULADD_FUNC(c);
+
+#if defined (HAVE_SSE)
+MAKE_MIX_GAIN_FUNC(sse);
+MAKE_SUM_FUNC(sse);
+#endif
+#if defined (HAVE_AVX)
+MAKE_SUM_FUNC(avx);
+#endif
+
+#endif /* DSP_OPS_H */
diff --git a/src/modules/module-filter-chain/ladspa.h b/src/modules/module-filter-chain/ladspa.h
new file mode 100644
index 0000000..b1a9c4e
--- /dev/null
+++ b/src/modules/module-filter-chain/ladspa.h
@@ -0,0 +1,603 @@
+/* ladspa.h
+
+ Linux Audio Developer's Simple Plugin API Version 1.1[LGPL].
+ Copyright (C) 2000-2002 Richard W.E. Furse, Paul Barton-Davis,
+ Stefan Westerfeld.
+
+ This library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public License
+ as published by the Free Software Foundation; either version 2.1 of
+ the License, or (at your option) any later version.
+
+ This library is distributed in the hope that it will be useful, but
+ WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with this library; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ USA. */
+
+#ifndef LADSPA_INCLUDED
+#define LADSPA_INCLUDED
+
+#define LADSPA_VERSION "1.1"
+#define LADSPA_VERSION_MAJOR 1
+#define LADSPA_VERSION_MINOR 1
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/*****************************************************************************/
+
+/* Overview:
+
+ There is a large number of synthesis packages in use or development
+ on the Linux platform at this time. This API (`The Linux Audio
+ Developer's Simple Plugin API') attempts to give programmers the
+ ability to write simple `plugin' audio processors in C/C++ and link
+ them dynamically (`plug') into a range of these packages (`hosts').
+ It should be possible for any host and any plugin to communicate
+ completely through this interface.
+
+ This API is deliberately short and simple. To achieve compatibility
+ with a range of promising Linux sound synthesis packages it
+ attempts to find the `greatest common divisor' in their logical
+ behaviour. Having said this, certain limiting decisions are
+ implicit, notably the use of a fixed type (LADSPA_Data) for all
+ data transfer and absence of a parameterised `initialisation'
+ phase. See below for the LADSPA_Data typedef.
+
+ Plugins are expected to distinguish between control and audio
+ data. Plugins have `ports' that are inputs or outputs for audio or
+ control data and each plugin is `run' for a `block' corresponding
+ to a short time interval measured in samples. Audio data is
+ communicated using arrays of LADSPA_Data, allowing a block of audio
+ to be processed by the plugin in a single pass. Control data is
+ communicated using single LADSPA_Data values. Control data has a
+ single value at the start of a call to the `run()' or `run_adding()'
+ function, and may be considered to remain this value for its
+ duration. The plugin may assume that all its input and output ports
+ have been connected to the relevant data location (see the
+ `connect_port()' function below) before it is asked to run.
+
+ Plugins will reside in shared object files suitable for dynamic
+ linking by dlopen() and family. The file will provide a number of
+ `plugin types' that can be used to instantiate actual plugins
+ (sometimes known as `plugin instances') that can be connected
+ together to perform tasks.
+
+ This API contains very limited error-handling. */
+
+/*****************************************************************************/
+
+/* Fundamental data type passed in and out of plugin. This data type
+ is used to communicate audio samples and control values. It is
+ assumed that the plugin will work sensibly given any numeric input
+ value although it may have a preferred range (see hints below).
+
+ For audio it is generally assumed that 1.0f is the `0dB' reference
+ amplitude and is a `normal' signal level. */
+
+typedef float LADSPA_Data;
+
+/*****************************************************************************/
+
+/* Special Plugin Properties:
+
+ Optional features of the plugin type are encapsulated in the
+ LADSPA_Properties type. This is assembled by ORing individual
+ properties together. */
+
+typedef int LADSPA_Properties;
+
+/* Property LADSPA_PROPERTY_REALTIME indicates that the plugin has a
+ real-time dependency (e.g. listens to a MIDI device) and so its
+ output must not be cached or subject to significant latency. */
+#define LADSPA_PROPERTY_REALTIME 0x1
+
+/* Property LADSPA_PROPERTY_INPLACE_BROKEN indicates that the plugin
+ may cease to work correctly if the host elects to use the same data
+ location for both input and output (see connect_port()). This
+ should be avoided as enabling this flag makes it impossible for
+ hosts to use the plugin to process audio `in-place.' */
+#define LADSPA_PROPERTY_INPLACE_BROKEN 0x2
+
+/* Property LADSPA_PROPERTY_HARD_RT_CAPABLE indicates that the plugin
+ is capable of running not only in a conventional host but also in a
+ `hard real-time' environment. To qualify for this the plugin must
+ satisfy all of the following:
+
+ (1) The plugin must not use malloc(), free() or other heap memory
+ management within its run() or run_adding() functions. All new
+ memory used in run() must be managed via the stack. These
+ restrictions only apply to the run() function.
+
+ (2) The plugin will not attempt to make use of any library
+ functions with the exceptions of functions in the ANSI standard C
+ and C maths libraries, which the host is expected to provide.
+
+ (3) The plugin will not access files, devices, pipes, sockets, IPC
+ or any other mechanism that might result in process or thread
+ blocking.
+
+ (4) The plugin will take an amount of time to execute a run() or
+ run_adding() call approximately of form (A+B*SampleCount) where A
+ and B depend on the machine and host in use. This amount of time
+ may not depend on input signals or plugin state. The host is left
+ the responsibility to perform timings to estimate upper bounds for
+ A and B. */
+#define LADSPA_PROPERTY_HARD_RT_CAPABLE 0x4
+
+#define LADSPA_IS_REALTIME(x) ((x) & LADSPA_PROPERTY_REALTIME)
+#define LADSPA_IS_INPLACE_BROKEN(x) ((x) & LADSPA_PROPERTY_INPLACE_BROKEN)
+#define LADSPA_IS_HARD_RT_CAPABLE(x) ((x) & LADSPA_PROPERTY_HARD_RT_CAPABLE)
+
+/*****************************************************************************/
+
+/* Plugin Ports:
+
+ Plugins have `ports' that are inputs or outputs for audio or
+ data. Ports can communicate arrays of LADSPA_Data (for audio
+ inputs/outputs) or single LADSPA_Data values (for control
+ input/outputs). This information is encapsulated in the
+ LADSPA_PortDescriptor type which is assembled by ORing individual
+ properties together.
+
+ Note that a port must be an input or an output port but not both
+ and that a port must be a control or audio port but not both. */
+
+typedef int LADSPA_PortDescriptor;
+
+/* Property LADSPA_PORT_INPUT indicates that the port is an input. */
+#define LADSPA_PORT_INPUT 0x1
+
+/* Property LADSPA_PORT_OUTPUT indicates that the port is an output. */
+#define LADSPA_PORT_OUTPUT 0x2
+
+/* Property LADSPA_PORT_CONTROL indicates that the port is a control
+ port. */
+#define LADSPA_PORT_CONTROL 0x4
+
+/* Property LADSPA_PORT_AUDIO indicates that the port is a audio
+ port. */
+#define LADSPA_PORT_AUDIO 0x8
+
+#define LADSPA_IS_PORT_INPUT(x) ((x) & LADSPA_PORT_INPUT)
+#define LADSPA_IS_PORT_OUTPUT(x) ((x) & LADSPA_PORT_OUTPUT)
+#define LADSPA_IS_PORT_CONTROL(x) ((x) & LADSPA_PORT_CONTROL)
+#define LADSPA_IS_PORT_AUDIO(x) ((x) & LADSPA_PORT_AUDIO)
+
+/*****************************************************************************/
+
+/* Plugin Port Range Hints:
+
+ The host may wish to provide a representation of data entering or
+ leaving a plugin (e.g. to generate a GUI automatically). To make
+ this more meaningful, the plugin should provide `hints' to the host
+ describing the usual values taken by the data.
+
+ Note that these are only hints. The host may ignore them and the
+ plugin must not assume that data supplied to it is meaningful. If
+ the plugin receives invalid input data it is expected to continue
+ to run without failure and, where possible, produce a sensible
+ output (e.g. a high-pass filter given a negative cutoff frequency
+ might switch to an all-pass mode).
+
+ Hints are meaningful for all input and output ports but hints for
+ input control ports are expected to be particularly useful.
+
+ More hint information is encapsulated in the
+ LADSPA_PortRangeHintDescriptor type which is assembled by ORing
+ individual hint types together. Hints may require further
+ LowerBound and UpperBound information.
+
+ All the hint information for a particular port is aggregated in the
+ LADSPA_PortRangeHint structure. */
+
+typedef int LADSPA_PortRangeHintDescriptor;
+
+/* Hint LADSPA_HINT_BOUNDED_BELOW indicates that the LowerBound field
+ of the LADSPA_PortRangeHint should be considered meaningful. The
+ value in this field should be considered the (inclusive) lower
+ bound of the valid range. If LADSPA_HINT_SAMPLE_RATE is also
+ specified then the value of LowerBound should be multiplied by the
+ sample rate. */
+#define LADSPA_HINT_BOUNDED_BELOW 0x1
+
+/* Hint LADSPA_HINT_BOUNDED_ABOVE indicates that the UpperBound field
+ of the LADSPA_PortRangeHint should be considered meaningful. The
+ value in this field should be considered the (inclusive) upper
+ bound of the valid range. If LADSPA_HINT_SAMPLE_RATE is also
+ specified then the value of UpperBound should be multiplied by the
+ sample rate. */
+#define LADSPA_HINT_BOUNDED_ABOVE 0x2
+
+/* Hint LADSPA_HINT_TOGGLED indicates that the data item should be
+ considered a Boolean toggle. Data less than or equal to zero should
+ be considered `off' or `false,' and data above zero should be
+ considered `on' or `true.' LADSPA_HINT_TOGGLED may not be used in
+ conjunction with any other hint except LADSPA_HINT_DEFAULT_0 or
+ LADSPA_HINT_DEFAULT_1. */
+#define LADSPA_HINT_TOGGLED 0x4
+
+/* Hint LADSPA_HINT_SAMPLE_RATE indicates that any bounds specified
+ should be interpreted as multiples of the sample rate. For
+ instance, a frequency range from 0Hz to the Nyquist frequency (half
+ the sample rate) could be requested by this hint in conjunction
+ with LowerBound = 0 and UpperBound = 0.5. Hosts that support bounds
+ at all must support this hint to retain meaning. */
+#define LADSPA_HINT_SAMPLE_RATE 0x8
+
+/* Hint LADSPA_HINT_LOGARITHMIC indicates that it is likely that the
+ user will find it more intuitive to view values using a logarithmic
+ scale. This is particularly useful for frequencies and gains. */
+#define LADSPA_HINT_LOGARITHMIC 0x10
+
+/* Hint LADSPA_HINT_INTEGER indicates that a user interface would
+ probably wish to provide a stepped control taking only integer
+ values. Any bounds set should be slightly wider than the actual
+ integer range required to avoid floating point rounding errors. For
+ instance, the integer set {0,1,2,3} might be described as [-0.1,
+ 3.1]. */
+#define LADSPA_HINT_INTEGER 0x20
+
+/* The various LADSPA_HINT_HAS_DEFAULT_* hints indicate a `normal'
+ value for the port that is sensible as a default. For instance,
+ this value is suitable for use as an initial value in a user
+ interface or as a value the host might assign to a control port
+ when the user has not provided one. Defaults are encoded using a
+ mask so only one default may be specified for a port. Some of the
+ hints make use of lower and upper bounds, in which case the
+ relevant bound or bounds must be available and
+ LADSPA_HINT_SAMPLE_RATE must be applied as usual. The resulting
+ default must be rounded if LADSPA_HINT_INTEGER is present. Default
+ values were introduced in LADSPA v1.1. */
+#define LADSPA_HINT_DEFAULT_MASK 0x3C0
+
+/* This default values indicates that no default is provided. */
+#define LADSPA_HINT_DEFAULT_NONE 0x0
+
+/* This default hint indicates that the suggested lower bound for the
+ port should be used. */
+#define LADSPA_HINT_DEFAULT_MINIMUM 0x40
+
+/* This default hint indicates that a low value between the suggested
+ lower and upper bounds should be chosen. For ports with
+ LADSPA_HINT_LOGARITHMIC, this should be exp(log(lower) * 0.75 +
+ log(upper) * 0.25). Otherwise, this should be (lower * 0.75 + upper
+ * 0.25). */
+#define LADSPA_HINT_DEFAULT_LOW 0x80
+
+/* This default hint indicates that a middle value between the
+ suggested lower and upper bounds should be chosen. For ports with
+ LADSPA_HINT_LOGARITHMIC, this should be exp(log(lower) * 0.5 +
+ log(upper) * 0.5). Otherwise, this should be (lower * 0.5 + upper *
+ 0.5). */
+#define LADSPA_HINT_DEFAULT_MIDDLE 0xC0
+
+/* This default hint indicates that a high value between the suggested
+ lower and upper bounds should be chosen. For ports with
+ LADSPA_HINT_LOGARITHMIC, this should be exp(log(lower) * 0.25 +
+ log(upper) * 0.75). Otherwise, this should be (lower * 0.25 + upper
+ * 0.75). */
+#define LADSPA_HINT_DEFAULT_HIGH 0x100
+
+/* This default hint indicates that the suggested upper bound for the
+ port should be used. */
+#define LADSPA_HINT_DEFAULT_MAXIMUM 0x140
+
+/* This default hint indicates that the number 0 should be used. Note
+ that this default may be used in conjunction with
+ LADSPA_HINT_TOGGLED. */
+#define LADSPA_HINT_DEFAULT_0 0x200
+
+/* This default hint indicates that the number 1 should be used. Note
+ that this default may be used in conjunction with
+ LADSPA_HINT_TOGGLED. */
+#define LADSPA_HINT_DEFAULT_1 0x240
+
+/* This default hint indicates that the number 100 should be used. */
+#define LADSPA_HINT_DEFAULT_100 0x280
+
+/* This default hint indicates that the Hz frequency of `concert A'
+ should be used. This will be 440 unless the host uses an unusual
+ tuning convention, in which case it may be within a few Hz. */
+#define LADSPA_HINT_DEFAULT_440 0x2C0
+
+#define LADSPA_IS_HINT_BOUNDED_BELOW(x) ((x) & LADSPA_HINT_BOUNDED_BELOW)
+#define LADSPA_IS_HINT_BOUNDED_ABOVE(x) ((x) & LADSPA_HINT_BOUNDED_ABOVE)
+#define LADSPA_IS_HINT_TOGGLED(x) ((x) & LADSPA_HINT_TOGGLED)
+#define LADSPA_IS_HINT_SAMPLE_RATE(x) ((x) & LADSPA_HINT_SAMPLE_RATE)
+#define LADSPA_IS_HINT_LOGARITHMIC(x) ((x) & LADSPA_HINT_LOGARITHMIC)
+#define LADSPA_IS_HINT_INTEGER(x) ((x) & LADSPA_HINT_INTEGER)
+
+#define LADSPA_IS_HINT_HAS_DEFAULT(x) ((x) & LADSPA_HINT_DEFAULT_MASK)
+#define LADSPA_IS_HINT_DEFAULT_MINIMUM(x) (((x) & LADSPA_HINT_DEFAULT_MASK) \
+ == LADSPA_HINT_DEFAULT_MINIMUM)
+#define LADSPA_IS_HINT_DEFAULT_LOW(x) (((x) & LADSPA_HINT_DEFAULT_MASK) \
+ == LADSPA_HINT_DEFAULT_LOW)
+#define LADSPA_IS_HINT_DEFAULT_MIDDLE(x) (((x) & LADSPA_HINT_DEFAULT_MASK) \
+ == LADSPA_HINT_DEFAULT_MIDDLE)
+#define LADSPA_IS_HINT_DEFAULT_HIGH(x) (((x) & LADSPA_HINT_DEFAULT_MASK) \
+ == LADSPA_HINT_DEFAULT_HIGH)
+#define LADSPA_IS_HINT_DEFAULT_MAXIMUM(x) (((x) & LADSPA_HINT_DEFAULT_MASK) \
+ == LADSPA_HINT_DEFAULT_MAXIMUM)
+#define LADSPA_IS_HINT_DEFAULT_0(x) (((x) & LADSPA_HINT_DEFAULT_MASK) \
+ == LADSPA_HINT_DEFAULT_0)
+#define LADSPA_IS_HINT_DEFAULT_1(x) (((x) & LADSPA_HINT_DEFAULT_MASK) \
+ == LADSPA_HINT_DEFAULT_1)
+#define LADSPA_IS_HINT_DEFAULT_100(x) (((x) & LADSPA_HINT_DEFAULT_MASK) \
+ == LADSPA_HINT_DEFAULT_100)
+#define LADSPA_IS_HINT_DEFAULT_440(x) (((x) & LADSPA_HINT_DEFAULT_MASK) \
+ == LADSPA_HINT_DEFAULT_440)
+
+typedef struct _LADSPA_PortRangeHint {
+
+ /* Hints about the port. */
+ LADSPA_PortRangeHintDescriptor HintDescriptor;
+
+ /* Meaningful when hint LADSPA_HINT_BOUNDED_BELOW is active. When
+ LADSPA_HINT_SAMPLE_RATE is also active then this value should be
+ multiplied by the relevant sample rate. */
+ LADSPA_Data LowerBound;
+
+ /* Meaningful when hint LADSPA_HINT_BOUNDED_ABOVE is active. When
+ LADSPA_HINT_SAMPLE_RATE is also active then this value should be
+ multiplied by the relevant sample rate. */
+ LADSPA_Data UpperBound;
+
+} LADSPA_PortRangeHint;
+
+/*****************************************************************************/
+
+/* Plugin Handles:
+
+ This plugin handle indicates a particular instance of the plugin
+ concerned. It is valid to compare this to NULL (0 for C++) but
+ otherwise the host should not attempt to interpret it. The plugin
+ may use it to reference internal instance data. */
+
+typedef void * LADSPA_Handle;
+
+/*****************************************************************************/
+
+/* Descriptor for a Type of Plugin:
+
+ This structure is used to describe a plugin type. It provides a
+ number of functions to examine the type, instantiate it, link it to
+ buffers and workspaces and to run it. */
+
+typedef struct _LADSPA_Descriptor {
+
+ /* This numeric identifier indicates the plugin type
+ uniquely. Plugin programmers may reserve ranges of IDs from a
+ central body to avoid clashes. Hosts may assume that IDs are
+ below 0x1000000. */
+ unsigned long UniqueID;
+
+ /* This identifier can be used as a unique, case-sensitive
+ identifier for the plugin type within the plugin file. Plugin
+ types should be identified by file and label rather than by index
+ or plugin name, which may be changed in new plugin
+ versions. Labels must not contain white-space characters. */
+ const char * Label;
+
+ /* This indicates a number of properties of the plugin. */
+ LADSPA_Properties Properties;
+
+ /* This member points to the null-terminated name of the plugin
+ (e.g. "Sine Oscillator"). */
+ const char * Name;
+
+ /* This member points to the null-terminated string indicating the
+ maker of the plugin. This can be an empty string but not NULL. */
+ const char * Maker;
+
+ /* This member points to the null-terminated string indicating any
+ copyright applying to the plugin. If no Copyright applies the
+ string "None" should be used. */
+ const char * Copyright;
+
+ /* This indicates the number of ports (input AND output) present on
+ the plugin. */
+ unsigned long PortCount;
+
+ /* This member indicates an array of port descriptors. Valid indices
+ vary from 0 to PortCount-1. */
+ const LADSPA_PortDescriptor * PortDescriptors;
+
+ /* This member indicates an array of null-terminated strings
+ describing ports (e.g. "Frequency (Hz)"). Valid indices vary from
+ 0 to PortCount-1. */
+ const char * const * PortNames;
+
+ /* This member indicates an array of range hints for each port (see
+ above). Valid indices vary from 0 to PortCount-1. */
+ const LADSPA_PortRangeHint * PortRangeHints;
+
+ /* This may be used by the plugin developer to pass any custom
+ implementation data into an instantiate call. It must not be used
+ or interpreted by the host. It is expected that most plugin
+ writers will not use this facility as LADSPA_Handle should be
+ used to hold instance data. */
+ void * ImplementationData;
+
+ /* This member is a function pointer that instantiates a plugin. A
+ handle is returned indicating the new plugin instance. The
+ instantiation function accepts a sample rate as a parameter. The
+ plugin descriptor from which this instantiate function was found
+ must also be passed. This function must return NULL if
+ instantiation fails.
+
+ Note that instance initialisation should generally occur in
+ activate() rather than here. */
+ LADSPA_Handle (*instantiate)(const struct _LADSPA_Descriptor * Descriptor,
+ unsigned long SampleRate);
+
+ /* This member is a function pointer that connects a port on an
+ instantiated plugin to a memory location at which a block of data
+ for the port will be read/written. The data location is expected
+ to be an array of LADSPA_Data for audio ports or a single
+ LADSPA_Data value for control ports. Memory issues will be
+ managed by the host. The plugin must read/write the data at these
+ locations every time run() or run_adding() is called and the data
+ present at the time of this connection call should not be
+ considered meaningful.
+
+ connect_port() may be called more than once for a plugin instance
+ to allow the host to change the buffers that the plugin is
+ reading or writing. These calls may be made before or after
+ activate() or deactivate() calls.
+
+ connect_port() must be called at least once for each port before
+ run() or run_adding() is called. When working with blocks of
+ LADSPA_Data the plugin should pay careful attention to the block
+ size passed to the run function as the block allocated may only
+ just be large enough to contain the block of samples.
+
+ Plugin writers should be aware that the host may elect to use the
+ same buffer for more than one port and even use the same buffer
+ for both input and output (see LADSPA_PROPERTY_INPLACE_BROKEN).
+ However, overlapped buffers or use of a single buffer for both
+ audio and control data may result in unexpected behaviour. */
+ void (*connect_port)(LADSPA_Handle Instance,
+ unsigned long Port,
+ LADSPA_Data * DataLocation);
+
+ /* This member is a function pointer that initialises a plugin
+ instance and activates it for use. This is separated from
+ instantiate() to aid real-time support and so that hosts can
+ reinitialise a plugin instance by calling deactivate() and then
+ activate(). In this case the plugin instance must reset all state
+ information dependent on the history of the plugin instance
+ except for any data locations provided by connect_port() and any
+ gain set by set_run_adding_gain(). If there is nothing for
+ activate() to do then the plugin writer may provide a NULL rather
+ than an empty function.
+
+ When present, hosts must call this function once before run() (or
+ run_adding()) is called for the first time. This call should be
+ made as close to the run() call as possible and indicates to
+ real-time plugins that they are now live. Plugins should not rely
+ on a prompt call to run() after activate(). activate() may not be
+ called again unless deactivate() is called first. Note that
+ connect_port() may be called before or after a call to
+ activate(). */
+ void (*activate)(LADSPA_Handle Instance);
+
+ /* This method is a function pointer that runs an instance of a
+ plugin for a block. Two parameters are required: the first is a
+ handle to the particular instance to be run and the second
+ indicates the block size (in samples) for which the plugin
+ instance may run.
+
+ Note that if an activate() function exists then it must be called
+ before run() or run_adding(). If deactivate() is called for a
+ plugin instance then the plugin instance may not be reused until
+ activate() has been called again.
+
+ If the plugin has the property LADSPA_PROPERTY_HARD_RT_CAPABLE
+ then there are various things that the plugin should not do
+ within the run() or run_adding() functions (see above). */
+ void (*run)(LADSPA_Handle Instance,
+ unsigned long SampleCount);
+
+ /* This method is a function pointer that runs an instance of a
+ plugin for a block. This has identical behaviour to run() except
+ in the way data is output from the plugin. When run() is used,
+ values are written directly to the memory areas associated with
+ the output ports. However when run_adding() is called, values
+ must be added to the values already present in the memory
+ areas. Furthermore, output values written must be scaled by the
+ current gain set by set_run_adding_gain() (see below) before
+ addition.
+
+ run_adding() is optional. When it is not provided by a plugin,
+ this function pointer must be set to NULL. When it is provided,
+ the function set_run_adding_gain() must be provided also. */
+ void (*run_adding)(LADSPA_Handle Instance,
+ unsigned long SampleCount);
+
+ /* This method is a function pointer that sets the output gain for
+ use when run_adding() is called (see above). If this function is
+ never called the gain is assumed to default to 1. Gain
+ information should be retained when activate() or deactivate()
+ are called.
+
+ This function should be provided by the plugin if and only if the
+ run_adding() function is provided. When it is absent this
+ function pointer must be set to NULL. */
+ void (*set_run_adding_gain)(LADSPA_Handle Instance,
+ LADSPA_Data Gain);
+
+ /* This is the counterpart to activate() (see above). If there is
+ nothing for deactivate() to do then the plugin writer may provide
+ a NULL rather than an empty function.
+
+ Hosts must deactivate all activated units after they have been
+ run() (or run_adding()) for the last time. This call should be
+ made as close to the last run() call as possible and indicates to
+ real-time plugins that they are no longer live. Plugins should
+ not rely on prompt deactivation. Note that connect_port() may be
+ called before or after a call to deactivate().
+
+ Deactivation is not similar to pausing as the plugin instance
+ will be reinitialised when activate() is called to reuse it. */
+ void (*deactivate)(LADSPA_Handle Instance);
+
+ /* Once an instance of a plugin has been finished with it can be
+ deleted using the following function. The instance handle passed
+ ceases to be valid after this call.
+
+ If activate() was called for a plugin instance then a
+ corresponding call to deactivate() must be made before cleanup()
+ is called. */
+ void (*cleanup)(LADSPA_Handle Instance);
+
+} LADSPA_Descriptor;
+
+/**********************************************************************/
+
+/* Accessing a Plugin: */
+
+/* The exact mechanism by which plugins are loaded is host-dependent,
+ however all most hosts will need to know is the name of shared
+ object file containing the plugin types. To allow multiple hosts to
+ share plugin types, hosts may wish to check for environment
+ variable LADSPA_PATH. If present, this should contain a
+ colon-separated path indicating directories that should be searched
+ (in order) when loading plugin types.
+
+ A plugin programmer must include a function called
+ "ladspa_descriptor" with the following function prototype within
+ the shared object file. This function will have C-style linkage (if
+ you are using C++ this is taken care of by the `extern "C"' clause
+ at the top of the file).
+
+ A host will find the plugin shared object file by one means or
+ another, find the ladspa_descriptor() function, call it, and
+ proceed from there.
+
+ Plugin types are accessed by index (not ID) using values from 0
+ upwards. Out of range indexes must result in this function
+ returning NULL, so the plugin count can be determined by checking
+ for the least index that results in NULL being returned. */
+
+const LADSPA_Descriptor * ladspa_descriptor(unsigned long Index);
+
+/* Datatype corresponding to the ladspa_descriptor() function. */
+typedef const LADSPA_Descriptor *
+(*LADSPA_Descriptor_Function)(unsigned long Index);
+
+/**********************************************************************/
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* LADSPA_INCLUDED */
+
+/* EOF */
diff --git a/src/modules/module-filter-chain/ladspa_plugin.c b/src/modules/module-filter-chain/ladspa_plugin.c
new file mode 100644
index 0000000..195d1ab
--- /dev/null
+++ b/src/modules/module-filter-chain/ladspa_plugin.c
@@ -0,0 +1,275 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "config.h"
+
+#include <dlfcn.h>
+#include <math.h>
+
+#include <spa/utils/defs.h>
+#include <spa/utils/list.h>
+#include <spa/utils/string.h>
+
+#include <pipewire/log.h>
+#include <pipewire/utils.h>
+
+#include "plugin.h"
+#include "ladspa.h"
+
+struct plugin {
+ struct fc_plugin plugin;
+ void *handle;
+ LADSPA_Descriptor_Function desc_func;
+};
+
+struct descriptor {
+ struct fc_descriptor desc;
+ const LADSPA_Descriptor *d;
+};
+
+static void *ladspa_instantiate(const struct fc_descriptor *desc,
+ unsigned long SampleRate, int index, const char *config)
+{
+ struct descriptor *d = (struct descriptor *)desc;
+ return d->d->instantiate(d->d, SampleRate);
+}
+
+static const LADSPA_Descriptor *find_desc(LADSPA_Descriptor_Function desc_func, const char *name)
+{
+ unsigned long i;
+ for (i = 0; ;i++) {
+ const LADSPA_Descriptor *d = desc_func(i);
+ if (d == NULL)
+ break;
+ if (spa_streq(d->Label, name))
+ return d;
+ }
+ return NULL;
+}
+
+static float get_default(struct fc_port *port, LADSPA_PortRangeHintDescriptor hint,
+ LADSPA_Data lower, LADSPA_Data upper)
+{
+ LADSPA_Data def;
+
+ switch (hint & LADSPA_HINT_DEFAULT_MASK) {
+ case LADSPA_HINT_DEFAULT_MINIMUM:
+ def = lower;
+ break;
+ case LADSPA_HINT_DEFAULT_MAXIMUM:
+ def = upper;
+ break;
+ case LADSPA_HINT_DEFAULT_LOW:
+ if (LADSPA_IS_HINT_LOGARITHMIC(hint))
+ def = (LADSPA_Data) expf(logf(lower) * 0.75f + logf(upper) * 0.25f);
+ else
+ def = (LADSPA_Data) (lower * 0.75f + upper * 0.25f);
+ break;
+ case LADSPA_HINT_DEFAULT_MIDDLE:
+ if (LADSPA_IS_HINT_LOGARITHMIC(hint))
+ def = (LADSPA_Data) expf(logf(lower) * 0.5f + logf(upper) * 0.5f);
+ else
+ def = (LADSPA_Data) (lower * 0.5f + upper * 0.5f);
+ break;
+ case LADSPA_HINT_DEFAULT_HIGH:
+ if (LADSPA_IS_HINT_LOGARITHMIC(hint))
+ def = (LADSPA_Data) expf(logf(lower) * 0.25f + logf(upper) * 0.75f);
+ else
+ def = (LADSPA_Data) (lower * 0.25f + upper * 0.75f);
+ break;
+ case LADSPA_HINT_DEFAULT_0:
+ def = 0.0f;
+ break;
+ case LADSPA_HINT_DEFAULT_1:
+ def = 1.0f;
+ break;
+ case LADSPA_HINT_DEFAULT_100:
+ def = 100.0f;
+ break;
+ case LADSPA_HINT_DEFAULT_440:
+ def = 440.0f;
+ break;
+ default:
+ if (upper == lower)
+ def = upper;
+ else
+ def = SPA_CLAMPF(0.5f * upper, lower, upper);
+ break;
+ }
+ if (LADSPA_IS_HINT_INTEGER(hint))
+ def = roundf(def);
+ return def;
+}
+
+static void ladspa_port_update_ranges(struct descriptor *dd, struct fc_port *port)
+{
+ const LADSPA_Descriptor *d = dd->d;
+ unsigned long p = port->index;
+ LADSPA_PortRangeHintDescriptor hint = d->PortRangeHints[p].HintDescriptor;
+ LADSPA_Data lower, upper;
+
+ lower = d->PortRangeHints[p].LowerBound;
+ upper = d->PortRangeHints[p].UpperBound;
+
+ port->hint = hint;
+ port->def = get_default(port, hint, lower, upper);
+ port->min = lower;
+ port->max = upper;
+}
+
+static void ladspa_free(const struct fc_descriptor *desc)
+{
+ struct descriptor *d = (struct descriptor*)desc;
+ free(d->desc.ports);
+ free(d);
+}
+
+static const struct fc_descriptor *ladspa_make_desc(struct fc_plugin *plugin, const char *name)
+{
+ struct plugin *p = (struct plugin *)plugin;
+ struct descriptor *desc;
+ const LADSPA_Descriptor *d;
+ uint32_t i;
+
+ d = find_desc(p->desc_func, name);
+ if (d == NULL)
+ return NULL;
+
+ desc = calloc(1, sizeof(*desc));
+ desc->d = d;
+
+ desc->desc.instantiate = ladspa_instantiate;
+ desc->desc.cleanup = d->cleanup;
+ desc->desc.connect_port = d->connect_port;
+ desc->desc.activate = d->activate;
+ desc->desc.deactivate = d->deactivate;
+ desc->desc.run = d->run;
+
+ desc->desc.free = ladspa_free;
+
+ desc->desc.name = d->Label;
+ desc->desc.flags = 0;
+
+ desc->desc.n_ports = d->PortCount;
+ desc->desc.ports = calloc(desc->desc.n_ports, sizeof(struct fc_port));
+
+ for (i = 0; i < desc->desc.n_ports; i++) {
+ desc->desc.ports[i].index = i;
+ desc->desc.ports[i].name = d->PortNames[i];
+ desc->desc.ports[i].flags = d->PortDescriptors[i];
+ ladspa_port_update_ranges(desc, &desc->desc.ports[i]);
+ }
+ return &desc->desc;
+}
+
+static void ladspa_unload(struct fc_plugin *plugin)
+{
+ struct plugin *p = (struct plugin *)plugin;
+ if (p->handle)
+ dlclose(p->handle);
+ free(p);
+}
+
+static struct fc_plugin *ladspa_handle_load_by_path(const char *path)
+{
+ struct plugin *p;
+ int res;
+
+ p = calloc(1, sizeof(*p));
+ if (!p)
+ return NULL;
+
+ p->handle = dlopen(path, RTLD_NOW);
+ if (!p->handle) {
+ pw_log_debug("failed to open '%s': %s", path, dlerror());
+ res = -ENOENT;
+ goto exit;
+ }
+
+ pw_log_info("successfully opened '%s'", path);
+
+ p->desc_func = (LADSPA_Descriptor_Function) dlsym(p->handle, "ladspa_descriptor");
+ if (!p->desc_func) {
+ pw_log_warn("cannot find descriptor function in '%s': %s", path, dlerror());
+ res = -ENOSYS;
+ goto exit;
+ }
+ p->plugin.make_desc = ladspa_make_desc;
+ p->plugin.unload = ladspa_unload;
+
+ return &p->plugin;
+
+exit:
+ if (p->handle)
+ dlclose(p->handle);
+ free(p);
+ errno = -res;
+ return NULL;
+}
+
+struct fc_plugin *load_ladspa_plugin(const struct spa_support *support, uint32_t n_support,
+ struct dsp_ops *dsp, const char *plugin, const char *config)
+{
+ struct fc_plugin *pl = NULL;
+
+ if (plugin[0] != '/') {
+ const char *search_dirs, *p;
+ char path[PATH_MAX];
+ size_t len;
+
+ search_dirs = getenv("LADSPA_PATH");
+ if (!search_dirs)
+ search_dirs = "/usr/lib64/ladspa:/usr/lib/ladspa:" LIBDIR;
+
+ /*
+ * set the errno for the case when `ladspa_handle_load_by_path()`
+ * is never called, which can only happen if the supplied
+ * LADSPA_PATH contains too long paths
+ */
+ errno = ENAMETOOLONG;
+
+ while ((p = pw_split_walk(NULL, ":", &len, &search_dirs))) {
+ int pathlen;
+
+ if (len >= sizeof(path))
+ continue;
+
+ pathlen = snprintf(path, sizeof(path), "%.*s/%s.so", (int) len, p, plugin);
+ if (pathlen < 0 || (size_t) pathlen >= sizeof(path))
+ continue;
+
+ pl = ladspa_handle_load_by_path(path);
+ if (pl != NULL)
+ break;
+ }
+ }
+ else {
+ pl = ladspa_handle_load_by_path(plugin);
+ }
+
+ if (pl == NULL)
+ pw_log_error("failed to load plugin '%s': %s", plugin, strerror(errno));
+
+ return pl;
+}
diff --git a/src/modules/module-filter-chain/lv2_plugin.c b/src/modules/module-filter-chain/lv2_plugin.c
new file mode 100644
index 0000000..f0bd836
--- /dev/null
+++ b/src/modules/module-filter-chain/lv2_plugin.c
@@ -0,0 +1,522 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <dlfcn.h>
+#include <math.h>
+
+#include <spa/utils/defs.h>
+#include <spa/utils/list.h>
+#include <spa/utils/string.h>
+#include <spa/support/loop.h>
+
+#include <pipewire/log.h>
+#include <pipewire/utils.h>
+#include <pipewire/array.h>
+
+#include <lilv/lilv.h>
+
+#if defined __has_include
+# if __has_include (<lv2/atom/atom.h>)
+
+ #include <lv2/atom/atom.h>
+ #include <lv2/buf-size/buf-size.h>
+ #include <lv2/worker/worker.h>
+ #include <lv2/options/options.h>
+ #include <lv2/parameters/parameters.h>
+
+# else
+
+ #include <lv2/lv2plug.in/ns/ext/atom/atom.h>
+ #include <lv2/lv2plug.in/ns/ext/buf-size/buf-size.h>
+ #include <lv2/lv2plug.in/ns/ext/worker/worker.h>
+ #include <lv2/lv2plug.in/ns/ext/options/options.h>
+ #include <lv2/lv2plug.in/ns/ext/parameters/parameters.h>
+
+# endif
+
+#endif
+
+#include "plugin.h"
+
+static struct context *_context;
+
+typedef struct URITable {
+ struct pw_array array;
+} URITable;
+
+static void uri_table_init(URITable *table)
+{
+ pw_array_init(&table->array, 1024);
+}
+
+static void uri_table_destroy(URITable *table)
+{
+ char **p;
+ pw_array_for_each(p, &table->array)
+ free(*p);
+ pw_array_clear(&table->array);
+}
+
+static LV2_URID uri_table_map(LV2_URID_Map_Handle handle, const char *uri)
+{
+ URITable *table = (URITable*)handle;
+ char **p;
+ size_t i = 0;
+
+ pw_array_for_each(p, &table->array) {
+ i++;
+ if (spa_streq(*p, uri))
+ goto done;
+ }
+ pw_array_add_ptr(&table->array, strdup(uri));
+ i = pw_array_get_len(&table->array, char*);
+done:
+ return i;
+}
+
+static const char *uri_table_unmap(LV2_URID_Map_Handle handle, LV2_URID urid)
+{
+ URITable *table = (URITable*)handle;
+
+ if (urid > 0 && urid <= pw_array_get_len(&table->array, char*))
+ return *pw_array_get_unchecked(&table->array, urid, char*);
+ return NULL;
+}
+
+struct context {
+ int ref;
+ LilvWorld *world;
+
+ struct spa_loop *data_loop;
+ struct spa_loop *main_loop;
+
+ LilvNode *lv2_InputPort;
+ LilvNode *lv2_OutputPort;
+ LilvNode *lv2_AudioPort;
+ LilvNode *lv2_ControlPort;
+ LilvNode *lv2_Optional;
+ LilvNode *atom_AtomPort;
+ LilvNode *atom_Sequence;
+ LilvNode *urid_map;
+ LilvNode *powerOf2BlockLength;
+ LilvNode *fixedBlockLength;
+ LilvNode *boundedBlockLength;
+ LilvNode* worker_schedule;
+ LilvNode* worker_iface;
+
+ URITable uri_table;
+ LV2_URID_Map map;
+ LV2_Feature map_feature;
+ LV2_URID_Unmap unmap;
+ LV2_Feature unmap_feature;
+
+ LV2_URID atom_Int;
+ LV2_URID atom_Float;
+};
+
+#define context_map(c,uri) ((c)->map.map((c)->map.handle,(uri)))
+
+static void context_free(struct context *c)
+{
+ if (c->world) {
+ lilv_node_free(c->worker_schedule);
+ lilv_node_free(c->powerOf2BlockLength);
+ lilv_node_free(c->fixedBlockLength);
+ lilv_node_free(c->boundedBlockLength);
+ lilv_node_free(c->urid_map);
+ lilv_node_free(c->atom_Sequence);
+ lilv_node_free(c->atom_AtomPort);
+ lilv_node_free(c->lv2_Optional);
+ lilv_node_free(c->lv2_ControlPort);
+ lilv_node_free(c->lv2_AudioPort);
+ lilv_node_free(c->lv2_OutputPort);
+ lilv_node_free(c->lv2_InputPort);
+ lilv_world_free(c->world);
+ }
+ uri_table_destroy(&c->uri_table);
+ free(c);
+}
+
+static const LV2_Feature buf_size_features[3] = {
+ { LV2_BUF_SIZE__powerOf2BlockLength, NULL },
+ { LV2_BUF_SIZE__fixedBlockLength, NULL },
+ { LV2_BUF_SIZE__boundedBlockLength, NULL },
+};
+
+static struct context *context_new(const struct spa_support *support, uint32_t n_support)
+{
+ struct context *c;
+
+ c = calloc(1, sizeof(*c));
+ if (c == NULL)
+ return NULL;
+
+ uri_table_init(&c->uri_table);
+ c->world = lilv_world_new();
+ if (c->world == NULL)
+ goto error;
+
+ lilv_world_load_all(c->world);
+
+ c->lv2_InputPort = lilv_new_uri(c->world, LV2_CORE__InputPort);
+ c->lv2_OutputPort = lilv_new_uri(c->world, LV2_CORE__OutputPort);
+ c->lv2_AudioPort = lilv_new_uri(c->world, LV2_CORE__AudioPort);
+ c->lv2_ControlPort = lilv_new_uri(c->world, LV2_CORE__ControlPort);
+ c->lv2_Optional = lilv_new_uri(c->world, LV2_CORE__connectionOptional);
+ c->atom_AtomPort = lilv_new_uri(c->world, LV2_ATOM__AtomPort);
+ c->atom_Sequence = lilv_new_uri(c->world, LV2_ATOM__Sequence);
+ c->urid_map = lilv_new_uri(c->world, LV2_URID__map);
+ c->powerOf2BlockLength = lilv_new_uri(c->world, LV2_BUF_SIZE__powerOf2BlockLength);
+ c->fixedBlockLength = lilv_new_uri(c->world, LV2_BUF_SIZE__fixedBlockLength);
+ c->boundedBlockLength = lilv_new_uri(c->world, LV2_BUF_SIZE__boundedBlockLength);
+ c->worker_schedule = lilv_new_uri(c->world, LV2_WORKER__schedule);
+ c->worker_iface = lilv_new_uri(c->world, LV2_WORKER__interface);
+
+ c->map.handle = &c->uri_table;
+ c->map.map = uri_table_map;
+ c->map_feature.URI = LV2_URID_MAP_URI;
+ c->map_feature.data = &c->map;
+ c->unmap.handle = &c->uri_table;
+ c->unmap.unmap = uri_table_unmap;
+ c->unmap_feature.URI = LV2_URID_UNMAP_URI;
+ c->unmap_feature.data = &c->unmap;
+
+ c->atom_Int = context_map(c, LV2_ATOM__Int);
+ c->atom_Float = context_map(c, LV2_ATOM__Float);
+
+ c->data_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataLoop);
+ c->main_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Loop);
+
+ return c;
+error:
+ context_free(c);
+ return NULL;
+}
+
+static struct context *context_ref(const struct spa_support *support, uint32_t n_support)
+{
+ if (_context == NULL) {
+ _context = context_new(support, n_support);
+ if (_context == NULL)
+ return NULL;
+ }
+ _context->ref++;
+ return _context;
+}
+
+static void context_unref(struct context *context)
+{
+ if (--_context->ref == 0) {
+ context_free(_context);
+ _context = NULL;
+ }
+}
+
+struct plugin {
+ struct fc_plugin plugin;
+ struct context *c;
+ const LilvPlugin *p;
+};
+
+struct descriptor {
+ struct fc_descriptor desc;
+ struct plugin *p;
+};
+
+struct instance {
+ struct descriptor *desc;
+ LilvInstance *instance;
+ LV2_Worker_Schedule work_schedule;
+ LV2_Feature work_schedule_feature;
+ LV2_Options_Option options[6];
+ LV2_Feature options_feature;
+
+ const LV2_Feature *features[7];
+
+ const LV2_Worker_Interface *work_iface;
+
+ int32_t block_length;
+};
+
+static int
+do_respond(struct spa_loop *loop, bool async, uint32_t seq, const void *data,
+ size_t size, void *user_data)
+{
+ struct instance *i = (struct instance*)user_data;
+ i->work_iface->work_response(i->instance->lv2_handle, size, data);
+ return 0;
+}
+
+/** Called by the plugin to respond to non-RT work. */
+static LV2_Worker_Status
+work_respond(LV2_Worker_Respond_Handle handle, uint32_t size, const void *data)
+{
+ struct instance *i = (struct instance*)handle;
+ struct context *c = i->desc->p->c;
+ spa_loop_invoke(c->data_loop, do_respond, 1, data, size, false, i);
+ return LV2_WORKER_SUCCESS;
+}
+
+static int
+do_schedule(struct spa_loop *loop, bool async, uint32_t seq, const void *data,
+ size_t size, void *user_data)
+{
+ struct instance *i = (struct instance*)user_data;
+ i->work_iface->work(i->instance->lv2_handle, work_respond, i, size, data);
+ return 0;
+}
+
+/** Called by the plugin to schedule non-RT work. */
+static LV2_Worker_Status
+work_schedule(LV2_Worker_Schedule_Handle handle, uint32_t size, const void *data)
+{
+ struct instance *i = (struct instance*)handle;
+ struct context *c = i->desc->p->c;
+ spa_loop_invoke(c->main_loop, do_schedule, 1, data, size, false, i);
+ return LV2_WORKER_SUCCESS;
+}
+
+static void *lv2_instantiate(const struct fc_descriptor *desc,
+ unsigned long SampleRate, int index, const char *config)
+{
+ struct descriptor *d = (struct descriptor*)desc;
+ struct plugin *p = d->p;
+ struct context *c = p->c;
+ struct instance *i;
+ uint32_t n_features = 0;
+ static const int32_t min_block_length = 1;
+ static const int32_t max_block_length = 8192;
+ static const int32_t seq_size = 32768;
+ float fsample_rate = SampleRate;
+
+ i = calloc(1, sizeof(*i));
+ if (i == NULL)
+ return NULL;
+
+ i->block_length = 1024;
+ i->desc = d;
+ i->features[n_features++] = &c->map_feature;
+ i->features[n_features++] = &c->unmap_feature;
+ i->features[n_features++] = &buf_size_features[0];
+ i->features[n_features++] = &buf_size_features[1];
+ i->features[n_features++] = &buf_size_features[2];
+ if (lilv_plugin_has_feature(p->p, c->worker_schedule)) {
+ i->work_schedule.handle = i;
+ i->work_schedule.schedule_work = work_schedule;
+ i->work_schedule_feature.URI = LV2_WORKER__schedule;
+ i->work_schedule_feature.data = &i->work_schedule;
+ i->features[n_features++] = &i->work_schedule_feature;
+ }
+
+ i->options[0] = (LV2_Options_Option) { LV2_OPTIONS_INSTANCE, 0,
+ context_map(c, LV2_BUF_SIZE__minBlockLength), sizeof(int32_t),
+ c->atom_Int, &min_block_length };
+ i->options[1] = (LV2_Options_Option) { LV2_OPTIONS_INSTANCE, 0,
+ context_map(c, LV2_BUF_SIZE__maxBlockLength), sizeof(int32_t),
+ c->atom_Int, &max_block_length };
+ i->options[2] = (LV2_Options_Option) { LV2_OPTIONS_INSTANCE, 0,
+ context_map(c, LV2_BUF_SIZE__sequenceSize), sizeof(int32_t),
+ c->atom_Int, &seq_size };
+ i->options[3] = (LV2_Options_Option) { LV2_OPTIONS_INSTANCE, 0,
+ context_map(c, "http://lv2plug.in/ns/ext/buf-size#nominalBlockLength"), sizeof(int32_t),
+ c->atom_Int, &i->block_length },
+ i->options[4] = (LV2_Options_Option) { LV2_OPTIONS_INSTANCE, 0,
+ context_map(c, LV2_PARAMETERS__sampleRate), sizeof(float),
+ c->atom_Float, &fsample_rate };
+ i->options[5] = (LV2_Options_Option) { LV2_OPTIONS_INSTANCE, 0, 0, 0, 0, NULL };
+
+ i->options_feature.URI = LV2_OPTIONS__options;
+ i->options_feature.data = i->options;
+ i->features[n_features++] = &i->options_feature;
+
+ i->instance = lilv_plugin_instantiate(p->p, SampleRate, i->features);
+ if (i->instance == NULL) {
+ free(i);
+ return NULL;
+ }
+ if (lilv_plugin_has_extension_data(p->p, c->worker_iface)) {
+ i->work_iface = (const LV2_Worker_Interface*)
+ lilv_instance_get_extension_data(i->instance, LV2_WORKER__interface);
+ }
+
+ return i;
+}
+
+static void lv2_cleanup(void *instance)
+{
+ struct instance *i = instance;
+ lilv_instance_free(i->instance);
+ free(i);
+}
+
+static void lv2_connect_port(void *instance, unsigned long port, float *data)
+{
+ struct instance *i = instance;
+ lilv_instance_connect_port(i->instance, port, data);
+}
+
+static void lv2_activate(void *instance)
+{
+ struct instance *i = instance;
+ lilv_instance_activate(i->instance);
+}
+
+static void lv2_deactivate(void *instance)
+{
+ struct instance *i = instance;
+ lilv_instance_deactivate(i->instance);
+}
+
+static void lv2_run(void *instance, unsigned long SampleCount)
+{
+ struct instance *i = instance;
+ lilv_instance_run(i->instance, SampleCount);
+ if (i->work_iface != NULL && i->work_iface->end_run != NULL)
+ i->work_iface->end_run(i->instance);
+}
+
+static void lv2_free(const struct fc_descriptor *desc)
+{
+ struct descriptor *d = (struct descriptor*)desc;
+ free((char*)d->desc.name);
+ free(d->desc.ports);
+ free(d);
+}
+
+static const struct fc_descriptor *lv2_make_desc(struct fc_plugin *plugin, const char *name)
+{
+ struct plugin *p = (struct plugin *)plugin;
+ struct context *c = p->c;
+ struct descriptor *desc;
+ uint32_t i;
+ float *mins, *maxes, *controls;
+
+ desc = calloc(1, sizeof(*desc));
+ if (desc == NULL)
+ return NULL;
+
+ desc->p = p;
+ desc->desc.instantiate = lv2_instantiate;
+ desc->desc.cleanup = lv2_cleanup;
+ desc->desc.connect_port = lv2_connect_port;
+ desc->desc.activate = lv2_activate;
+ desc->desc.deactivate = lv2_deactivate;
+ desc->desc.run = lv2_run;
+
+ desc->desc.free = lv2_free;
+
+ desc->desc.name = strdup(name);
+ desc->desc.flags = 0;
+
+ desc->desc.n_ports = lilv_plugin_get_num_ports(p->p);
+ desc->desc.ports = calloc(desc->desc.n_ports, sizeof(struct fc_port));
+
+ mins = alloca(desc->desc.n_ports * sizeof(float));
+ maxes = alloca(desc->desc.n_ports * sizeof(float));
+ controls = alloca(desc->desc.n_ports * sizeof(float));
+
+ lilv_plugin_get_port_ranges_float(p->p, mins, maxes, controls);
+
+ for (i = 0; i < desc->desc.n_ports; i++) {
+ const LilvPort *port = lilv_plugin_get_port_by_index(p->p, i);
+ const LilvNode *symbol = lilv_port_get_symbol(p->p, port);
+ struct fc_port *fp = &desc->desc.ports[i];
+
+ fp->index = i;
+ fp->name = strdup(lilv_node_as_string(symbol));
+
+ fp->flags = 0;
+ if (lilv_port_is_a(p->p, port, c->lv2_InputPort))
+ fp->flags |= FC_PORT_INPUT;
+ if (lilv_port_is_a(p->p, port, c->lv2_OutputPort))
+ fp->flags |= FC_PORT_OUTPUT;
+ if (lilv_port_is_a(p->p, port, c->lv2_ControlPort))
+ fp->flags |= FC_PORT_CONTROL;
+ if (lilv_port_is_a(p->p, port, c->lv2_AudioPort))
+ fp->flags |= FC_PORT_AUDIO;
+
+ fp->hint = 0;
+ fp->min = mins[i];
+ fp->max = maxes[i];
+ fp->def = controls[i];
+ }
+ return &desc->desc;
+}
+
+static void lv2_unload(struct fc_plugin *plugin)
+{
+ struct plugin *p = (struct plugin *)plugin;
+ context_unref(p->c);
+ free(p);
+}
+
+struct fc_plugin *load_lv2_plugin(const struct spa_support *support, uint32_t n_support,
+ struct dsp_ops *ops, const char *plugin_uri, const char *config)
+{
+ struct context *c;
+ const LilvPlugins *plugins;
+ const LilvPlugin *plugin;
+ LilvNode *uri;
+ int res;
+ struct plugin *p;
+
+ c = context_ref(support, n_support);
+ if (c == NULL)
+ return NULL;
+
+ uri = lilv_new_uri(c->world, plugin_uri);
+ if (uri == NULL) {
+ pw_log_warn("invalid URI %s", plugin_uri);
+ res = -EINVAL;
+ goto error_unref;
+ }
+
+ plugins = lilv_world_get_all_plugins(c->world);
+ plugin = lilv_plugins_get_by_uri(plugins, uri);
+ lilv_node_free(uri);
+
+ if (plugin == NULL) {
+ pw_log_warn("can't load plugin %s", plugin_uri);
+ res = -EINVAL;
+ goto error_unref;
+ }
+
+ p = calloc(1, sizeof(*p));
+ if (!p) {
+ res = -errno;
+ goto error_unref;
+ }
+ p->p = plugin;
+ p->c = c;
+
+ p->plugin.make_desc = lv2_make_desc;
+ p->plugin.unload = lv2_unload;
+
+ return &p->plugin;
+
+error_unref:
+ context_unref(c);
+ errno = -res;
+ return NULL;
+}
diff --git a/src/modules/module-filter-chain/pffft.c b/src/modules/module-filter-chain/pffft.c
new file mode 100644
index 0000000..0370191
--- /dev/null
+++ b/src/modules/module-filter-chain/pffft.c
@@ -0,0 +1,2381 @@
+/* Copyright (c) 2013 Julien Pommier ( pommier@modartt.com )
+
+ Based on original fortran 77 code from FFTPACKv4 from NETLIB
+ (http://www.netlib.org/fftpack), authored by Dr Paul Swarztrauber
+ of NCAR, in 1985.
+
+ As confirmed by the NCAR fftpack software curators, the following
+ FFTPACKv5 license applies to FFTPACKv4 sources. My changes are
+ released under the same terms.
+
+ FFTPACK license:
+
+ http://www.cisl.ucar.edu/css/software/fftpack5/ftpk.html
+
+ Copyright (c) 2004 the University Corporation for Atmospheric
+ Research ("UCAR"). All rights reserved. Developed by NCAR's
+ Computational and Information Systems Laboratory, UCAR,
+ www.cisl.ucar.edu.
+
+ Redistribution and use of the Software in source and binary forms,
+ with or without modification, is permitted provided that the
+ following conditions are met:
+
+ - Neither the names of NCAR's Computational and Information Systems
+ Laboratory, the University Corporation for Atmospheric Research,
+ nor the names of its sponsors or contributors may be used to
+ endorse or promote products derived from this Software without
+ specific prior written permission.
+
+ - Redistributions of source code must retain the above copyright
+ notices, this list of conditions, and the disclaimer below.
+
+ - Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions, and the disclaimer below in the
+ documentation and/or other materials provided with the
+ distribution.
+
+ THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING, BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE CONTRIBUTORS OR COPYRIGHT
+ HOLDERS BE LIABLE FOR ANY CLAIM, INDIRECT, INCIDENTAL, SPECIAL,
+ EXEMPLARY, OR CONSEQUENTIAL DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE
+ SOFTWARE.
+
+ PFFFT : a Pretty Fast FFT.
+
+ This file is largerly based on the original FFTPACK implementation, modified in
+ order to take advantage of SIMD instructions of modern CPUs.
+*/
+
+/*
+ ChangeLog:
+ - 2011/10/02, version 1: This is the very first release of this file.
+*/
+
+#include "pffft.h"
+#include <stdlib.h>
+#include <stdio.h>
+#include <math.h>
+#include <stdint.h>
+#include <assert.h>
+
+#include <spa/support/cpu.h>
+
+/* detect compiler flavour */
+#if defined(_MSC_VER)
+#define COMPILER_MSVC
+#elif defined(__GNUC__)
+#define COMPILER_GCC
+#endif
+
+#if defined(COMPILER_GCC)
+#define ALWAYS_INLINE(return_type) inline return_type __attribute__ ((always_inline))
+#define NEVER_INLINE(return_type) return_type __attribute__ ((noinline))
+#define RESTRICT __restrict
+#define VLA_ARRAY_ON_STACK(type__, varname__, size__) type__ varname__[size__];
+#elif defined(COMPILER_MSVC)
+#define ALWAYS_INLINE(return_type) __forceinline return_type
+#define NEVER_INLINE(return_type) __declspec(noinline) return_type
+#define RESTRICT __restrict
+#define VLA_ARRAY_ON_STACK(type__, varname__, size__) type__ *varname__ = (type__*)_alloca(size__ * sizeof(type__))
+#endif
+
+/*
+ vector support macros: the rest of the code is independent of
+ SSE/Altivec/NEON -- adding support for other platforms with 4-element
+ vectors should be limited to these macros
+*/
+
+// define PFFFT_SIMD_DISABLE if you want to use scalar code instead of simd code
+//#define PFFFT_SIMD_DISABLE
+
+/*
+ Altivec support macros
+*/
+#if !defined(PFFFT_SIMD_DISABLE) && (defined(HAVE_ALTIVEC))
+typedef vector float v4sf;
+#define SIMD_SZ 4
+#define VZERO() ((vector float) vec_splat_u8(0))
+#define VMUL(a,b) vec_madd(a,b, VZERO())
+#define VADD(a,b) vec_add(a,b)
+#define VMADD(a,b,c) vec_madd(a,b,c)
+#define VSUB(a,b) vec_sub(a,b)
+inline v4sf ld_ps1(const float *p)
+{
+ v4sf v = vec_lde(0, p);
+ return vec_splat(vec_perm(v, v, vec_lvsl(0, p)), 0);
+}
+
+#define LD_PS1(p) ld_ps1(&p)
+#define INTERLEAVE2(in1, in2, out1, out2) { v4sf tmp__ = vec_mergeh(in1, in2); out2 = vec_mergel(in1, in2); out1 = tmp__; }
+#define UNINTERLEAVE2(in1, in2, out1, out2) { \
+ vector unsigned char vperm1 = (vector unsigned char)(0,1,2,3,8,9,10,11,16,17,18,19,24,25,26,27); \
+ vector unsigned char vperm2 = (vector unsigned char)(4,5,6,7,12,13,14,15,20,21,22,23,28,29,30,31); \
+ v4sf tmp__ = vec_perm(in1, in2, vperm1); out2 = vec_perm(in1, in2, vperm2); out1 = tmp__; \
+ }
+#define VTRANSPOSE4(x0,x1,x2,x3) { \
+ v4sf y0 = vec_mergeh(x0, x2); \
+ v4sf y1 = vec_mergel(x0, x2); \
+ v4sf y2 = vec_mergeh(x1, x3); \
+ v4sf y3 = vec_mergel(x1, x3); \
+ x0 = vec_mergeh(y0, y2); \
+ x1 = vec_mergel(y0, y2); \
+ x2 = vec_mergeh(y1, y3); \
+ x3 = vec_mergel(y1, y3); \
+ }
+#define VSWAPHL(a,b) vec_perm(a,b, (vector unsigned char)(16,17,18,19,20,21,22,23,8,9,10,11,12,13,14,15))
+#define VALIGNED(ptr) ((((uintptr_t)(ptr)) & 0xF) == 0)
+#define pffft_funcs pffft_funcs_altivec
+#define new_setup_simd new_setup_altivec
+#define zreorder_simd zreorder_altivec
+#define zconvolve_accumulate_simd zconvolve_accumulate_altivec
+#define zconvolve_simd zconvolve_altivec
+#define transform_simd transform_altivec
+
+/*
+ SSE1 support macros
+*/
+#elif !defined(PFFFT_SIMD_DISABLE) && (defined(HAVE_SSE))
+
+#include <xmmintrin.h>
+typedef __m128 v4sf;
+#define SIMD_SZ 4 // 4 floats by simd vector -- this is pretty much hardcoded in the preprocess/finalize functions anyway so you will have to work if you want to enable AVX with its 256-bit vectors.
+#define VZERO() _mm_setzero_ps()
+#define VMUL(a,b) _mm_mul_ps(a,b)
+#define VADD(a,b) _mm_add_ps(a,b)
+#define VMADD(a,b,c) _mm_add_ps(_mm_mul_ps(a,b), c)
+#define VSUB(a,b) _mm_sub_ps(a,b)
+#define LD_PS1(p) _mm_set1_ps(p)
+#define INTERLEAVE2(in1, in2, out1, out2) { v4sf tmp__ = _mm_unpacklo_ps(in1, in2); out2 = _mm_unpackhi_ps(in1, in2); out1 = tmp__; }
+#define UNINTERLEAVE2(in1, in2, out1, out2) { v4sf tmp__ = _mm_shuffle_ps(in1, in2, _MM_SHUFFLE(2,0,2,0)); out2 = _mm_shuffle_ps(in1, in2, _MM_SHUFFLE(3,1,3,1)); out1 = tmp__; }
+#define VTRANSPOSE4(x0,x1,x2,x3) _MM_TRANSPOSE4_PS(x0,x1,x2,x3)
+#define VSWAPHL(a,b) _mm_shuffle_ps(b, a, _MM_SHUFFLE(3,2,1,0))
+#define VALIGNED(ptr) ((((uintptr_t)(ptr)) & 0xF) == 0)
+#define pffft_funcs pffft_funcs_sse
+#define new_setup_simd new_setup_sse
+#define zreorder_simd zreorder_sse
+#define zconvolve_accumulate_simd zconvolve_accumulate_sse
+#define zconvolve_simd zconvolve_sse
+#define transform_simd transform_sse
+
+/*
+ ARM NEON support macros
+*/
+#elif !defined(PFFFT_SIMD_DISABLE) && (defined(HAVE_NEON))
+#include <arm_neon.h>
+typedef float32x4_t v4sf;
+#define SIMD_SZ 4
+#define VZERO() vdupq_n_f32(0)
+#define VMUL(a,b) vmulq_f32(a,b)
+#define VADD(a,b) vaddq_f32(a,b)
+#define VMADD(a,b,c) vmlaq_f32(c,a,b)
+#define VSUB(a,b) vsubq_f32(a,b)
+#define LD_PS1(p) vld1q_dup_f32(&(p))
+#define INTERLEAVE2(in1, in2, out1, out2) { float32x4x2_t tmp__ = vzipq_f32(in1,in2); out1=tmp__.val[0]; out2=tmp__.val[1]; }
+#define UNINTERLEAVE2(in1, in2, out1, out2) { float32x4x2_t tmp__ = vuzpq_f32(in1,in2); out1=tmp__.val[0]; out2=tmp__.val[1]; }
+#define VTRANSPOSE4(x0,x1,x2,x3) { \
+ float32x4x2_t t0_ = vzipq_f32(x0, x2); \
+ float32x4x2_t t1_ = vzipq_f32(x1, x3); \
+ float32x4x2_t u0_ = vzipq_f32(t0_.val[0], t1_.val[0]); \
+ float32x4x2_t u1_ = vzipq_f32(t0_.val[1], t1_.val[1]); \
+ x0 = u0_.val[0]; x1 = u0_.val[1]; x2 = u1_.val[0]; x3 = u1_.val[1]; \
+ }
+// marginally faster version
+//# define VTRANSPOSE4(x0,x1,x2,x3) { asm("vtrn.32 %q0, %q1;\n vtrn.32 %q2,%q3\n vswp %f0,%e2\n vswp %f1,%e3" : "+w"(x0), "+w"(x1), "+w"(x2), "+w"(x3)::); }
+#define VSWAPHL(a,b) vcombine_f32(vget_low_f32(b), vget_high_f32(a))
+#define VALIGNED(ptr) ((((uintptr_t)(ptr)) & 0x3) == 0)
+#define pffft_funcs pffft_funcs_neon
+#define new_setup_simd new_setup_neon
+#define zreorder_simd zreorder_neon
+#define zconvolve_accumulate_simd zconvolve_accumulate_neon
+#define zconvolve_simd zconvolve_neon
+#define transform_simd transform_neon
+#else
+#if !defined(PFFFT_SIMD_DISABLE)
+#warning "building with simd disabled !\n";
+#define PFFFT_SIMD_DISABLE // fallback to scalar code
+#endif
+#endif
+
+// fallback mode for situations where SSE/Altivec are not available, use scalar mode instead
+#ifdef PFFFT_SIMD_DISABLE
+typedef float v4sf;
+#define SIMD_SZ 1
+#define VZERO() 0.f
+#define VMUL(a,b) ((a)*(b))
+#define VADD(a,b) ((a)+(b))
+#define VMADD(a,b,c) ((a)*(b)+(c))
+#define VSUB(a,b) ((a)-(b))
+#define LD_PS1(p) (p)
+#define VALIGNED(ptr) ((((uintptr_t)(ptr)) & 0x3) == 0)
+#define pffft_funcs pffft_funcs_c
+#define new_setup_simd new_setup_c
+#define zreorder_simd zreorder_c
+#define zconvolve_accumulate_simd zconvolve_accumulate_c
+#define zconvolve_simd zconvolve_c
+#define transform_simd transform_c
+#endif
+
+// shortcuts for complex multiplcations
+#define VCPLXMUL(ar,ai,br,bi) { v4sf tmp; tmp=VMUL(ar,bi); ar=VMUL(ar,br); ar=VSUB(ar,VMUL(ai,bi)); ai=VMUL(ai,br); ai=VADD(ai,tmp); }
+#define VCPLXMULCONJ(ar,ai,br,bi) { v4sf tmp; tmp=VMUL(ar,bi); ar=VMUL(ar,br); ar=VADD(ar,VMUL(ai,bi)); ai=VMUL(ai,br); ai=VSUB(ai,tmp); }
+#ifndef SVMUL
+// multiply a scalar with a vector
+#define SVMUL(f,v) VMUL(LD_PS1(f),v)
+#endif
+
+#if !defined(PFFFT_SIMD_DISABLE)
+typedef union v4sf_union {
+ v4sf v;
+ float f[4];
+} v4sf_union;
+
+#include <string.h>
+
+#define assertv4(v,f0,f1,f2,f3) assert(v.f[0] == (f0) && v.f[1] == (f1) && v.f[2] == (f2) && v.f[3] == (f3))
+
+/* detect bugs with the vector support macros */
+static void validate_pffft_simd(void)
+{
+ float f[16] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 };
+ v4sf_union a0, a1, a2, a3, t, u;
+ memcpy(a0.f, f, 4 * sizeof(float));
+ memcpy(a1.f, f + 4, 4 * sizeof(float));
+ memcpy(a2.f, f + 8, 4 * sizeof(float));
+ memcpy(a3.f, f + 12, 4 * sizeof(float));
+
+ t = a0;
+ u = a1;
+ t.v = VZERO();
+ printf("VZERO=[%2g %2g %2g %2g]\n", t.f[0], t.f[1], t.f[2], t.f[3]);
+ assertv4(t, 0, 0, 0, 0);
+ t.v = VADD(a1.v, a2.v);
+ printf("VADD(4:7,8:11)=[%2g %2g %2g %2g]\n", t.f[0], t.f[1], t.f[2],
+ t.f[3]);
+ assertv4(t, 12, 14, 16, 18);
+ t.v = VMUL(a1.v, a2.v);
+ printf("VMUL(4:7,8:11)=[%2g %2g %2g %2g]\n", t.f[0], t.f[1], t.f[2],
+ t.f[3]);
+ assertv4(t, 32, 45, 60, 77);
+ t.v = VMADD(a1.v, a2.v, a0.v);
+ printf("VMADD(4:7,8:11,0:3)=[%2g %2g %2g %2g]\n", t.f[0], t.f[1],
+ t.f[2], t.f[3]);
+ assertv4(t, 32, 46, 62, 80);
+
+ INTERLEAVE2(a1.v, a2.v, t.v, u.v);
+ printf("INTERLEAVE2(4:7,8:11)=[%2g %2g %2g %2g] [%2g %2g %2g %2g]\n",
+ t.f[0], t.f[1], t.f[2], t.f[3], u.f[0], u.f[1], u.f[2], u.f[3]);
+ assertv4(t, 4, 8, 5, 9);
+ assertv4(u, 6, 10, 7, 11);
+ UNINTERLEAVE2(a1.v, a2.v, t.v, u.v);
+ printf("UNINTERLEAVE2(4:7,8:11)=[%2g %2g %2g %2g] [%2g %2g %2g %2g]\n",
+ t.f[0], t.f[1], t.f[2], t.f[3], u.f[0], u.f[1], u.f[2], u.f[3]);
+ assertv4(t, 4, 6, 8, 10);
+ assertv4(u, 5, 7, 9, 11);
+
+ t.v = LD_PS1(f[15]);
+ printf("LD_PS1(15)=[%2g %2g %2g %2g]\n", t.f[0], t.f[1], t.f[2],
+ t.f[3]);
+ assertv4(t, 15, 15, 15, 15);
+ t.v = VSWAPHL(a1.v, a2.v);
+ printf("VSWAPHL(4:7,8:11)=[%2g %2g %2g %2g]\n", t.f[0], t.f[1], t.f[2],
+ t.f[3]);
+ assertv4(t, 8, 9, 6, 7);
+ VTRANSPOSE4(a0.v, a1.v, a2.v, a3.v);
+ printf
+ ("VTRANSPOSE4(0:3,4:7,8:11,12:15)=[%2g %2g %2g %2g] [%2g %2g %2g %2g] [%2g %2g %2g %2g] [%2g %2g %2g %2g]\n",
+ a0.f[0], a0.f[1], a0.f[2], a0.f[3], a1.f[0], a1.f[1], a1.f[2],
+ a1.f[3], a2.f[0], a2.f[1], a2.f[2], a2.f[3], a3.f[0], a3.f[1],
+ a3.f[2], a3.f[3]);
+ assertv4(a0, 0, 4, 8, 12);
+ assertv4(a1, 1, 5, 9, 13);
+ assertv4(a2, 2, 6, 10, 14);
+ assertv4(a3, 3, 7, 11, 15);
+}
+#else
+static void validate_pffft_simd(void)
+{
+} // allow test_pffft.c to call this function even when simd is not available..
+#endif //!PFFFT_SIMD_DISABLE
+
+/*
+ passf2 and passb2 has been merged here, fsign = -1 for passf2, +1 for passb2
+*/
+static NEVER_INLINE(void) passf2_ps(int ido, int l1, const v4sf * cc, v4sf * ch,
+ const float *wa1, float fsign)
+{
+ int k, i;
+ int l1ido = l1 * ido;
+ if (ido <= 2) {
+ for (k = 0; k < l1ido; k += ido, ch += ido, cc += 2 * ido) {
+ ch[0] = VADD(cc[0], cc[ido + 0]);
+ ch[l1ido] = VSUB(cc[0], cc[ido + 0]);
+ ch[1] = VADD(cc[1], cc[ido + 1]);
+ ch[l1ido + 1] = VSUB(cc[1], cc[ido + 1]);
+ }
+ } else {
+ for (k = 0; k < l1ido; k += ido, ch += ido, cc += 2 * ido) {
+ for (i = 0; i < ido - 1; i += 2) {
+ v4sf tr2 = VSUB(cc[i + 0], cc[i + ido + 0]);
+ v4sf ti2 = VSUB(cc[i + 1], cc[i + ido + 1]);
+ v4sf wr = LD_PS1(wa1[i]), wi =
+ VMUL(LD_PS1(fsign), LD_PS1(wa1[i + 1]));
+ ch[i] = VADD(cc[i + 0], cc[i + ido + 0]);
+ ch[i + 1] = VADD(cc[i + 1], cc[i + ido + 1]);
+ VCPLXMUL(tr2, ti2, wr, wi);
+ ch[i + l1ido] = tr2;
+ ch[i + l1ido + 1] = ti2;
+ }
+ }
+ }
+}
+
+/*
+ passf3 and passb3 has been merged here, fsign = -1 for passf3, +1 for passb3
+*/
+static NEVER_INLINE(void) passf3_ps(int ido, int l1, const v4sf * cc, v4sf * ch,
+ const float *wa1, const float *wa2,
+ float fsign)
+{
+ static const float taur = -0.5f;
+ float taui = 0.866025403784439f * fsign;
+ int i, k;
+ v4sf tr2, ti2, cr2, ci2, cr3, ci3, dr2, di2, dr3, di3;
+ int l1ido = l1 * ido;
+ float wr1, wi1, wr2, wi2;
+ assert(ido > 2);
+ for (k = 0; k < l1ido; k += ido, cc += 3 * ido, ch += ido) {
+ for (i = 0; i < ido - 1; i += 2) {
+ tr2 = VADD(cc[i + ido], cc[i + 2 * ido]);
+ cr2 = VADD(cc[i], SVMUL(taur, tr2));
+ ch[i] = VADD(cc[i], tr2);
+ ti2 = VADD(cc[i + ido + 1], cc[i + 2 * ido + 1]);
+ ci2 = VADD(cc[i + 1], SVMUL(taur, ti2));
+ ch[i + 1] = VADD(cc[i + 1], ti2);
+ cr3 = SVMUL(taui, VSUB(cc[i + ido], cc[i + 2 * ido]));
+ ci3 =
+ SVMUL(taui,
+ VSUB(cc[i + ido + 1], cc[i + 2 * ido + 1]));
+ dr2 = VSUB(cr2, ci3);
+ dr3 = VADD(cr2, ci3);
+ di2 = VADD(ci2, cr3);
+ di3 = VSUB(ci2, cr3);
+ wr1 = wa1[i], wi1 = fsign * wa1[i + 1], wr2 =
+ wa2[i], wi2 = fsign * wa2[i + 1];
+ VCPLXMUL(dr2, di2, LD_PS1(wr1), LD_PS1(wi1));
+ ch[i + l1ido] = dr2;
+ ch[i + l1ido + 1] = di2;
+ VCPLXMUL(dr3, di3, LD_PS1(wr2), LD_PS1(wi2));
+ ch[i + 2 * l1ido] = dr3;
+ ch[i + 2 * l1ido + 1] = di3;
+ }
+ }
+} /* passf3 */
+
+static NEVER_INLINE(void) passf4_ps(int ido, int l1, const v4sf * cc, v4sf * ch,
+ const float *wa1, const float *wa2,
+ const float *wa3, float fsign)
+{
+ /* isign == -1 for forward transform and +1 for backward transform */
+
+ int i, k;
+ v4sf ci2, ci3, ci4, cr2, cr3, cr4, ti1, ti2, ti3, ti4, tr1, tr2, tr3,
+ tr4;
+ int l1ido = l1 * ido;
+ if (ido == 2) {
+ for (k = 0; k < l1ido; k += ido, ch += ido, cc += 4 * ido) {
+ tr1 = VSUB(cc[0], cc[2 * ido + 0]);
+ tr2 = VADD(cc[0], cc[2 * ido + 0]);
+ ti1 = VSUB(cc[1], cc[2 * ido + 1]);
+ ti2 = VADD(cc[1], cc[2 * ido + 1]);
+ ti4 =
+ VMUL(VSUB(cc[1 * ido + 0], cc[3 * ido + 0]),
+ LD_PS1(fsign));
+ tr4 =
+ VMUL(VSUB(cc[3 * ido + 1], cc[1 * ido + 1]),
+ LD_PS1(fsign));
+ tr3 = VADD(cc[ido + 0], cc[3 * ido + 0]);
+ ti3 = VADD(cc[ido + 1], cc[3 * ido + 1]);
+
+ ch[0 * l1ido + 0] = VADD(tr2, tr3);
+ ch[0 * l1ido + 1] = VADD(ti2, ti3);
+ ch[1 * l1ido + 0] = VADD(tr1, tr4);
+ ch[1 * l1ido + 1] = VADD(ti1, ti4);
+ ch[2 * l1ido + 0] = VSUB(tr2, tr3);
+ ch[2 * l1ido + 1] = VSUB(ti2, ti3);
+ ch[3 * l1ido + 0] = VSUB(tr1, tr4);
+ ch[3 * l1ido + 1] = VSUB(ti1, ti4);
+ }
+ } else {
+ for (k = 0; k < l1ido; k += ido, ch += ido, cc += 4 * ido) {
+ for (i = 0; i < ido - 1; i += 2) {
+ float wr1, wi1, wr2, wi2, wr3, wi3;
+ tr1 = VSUB(cc[i + 0], cc[i + 2 * ido + 0]);
+ tr2 = VADD(cc[i + 0], cc[i + 2 * ido + 0]);
+ ti1 = VSUB(cc[i + 1], cc[i + 2 * ido + 1]);
+ ti2 = VADD(cc[i + 1], cc[i + 2 * ido + 1]);
+ tr4 =
+ VMUL(VSUB
+ (cc[i + 3 * ido + 1],
+ cc[i + 1 * ido + 1]), LD_PS1(fsign));
+ ti4 =
+ VMUL(VSUB
+ (cc[i + 1 * ido + 0],
+ cc[i + 3 * ido + 0]), LD_PS1(fsign));
+ tr3 =
+ VADD(cc[i + ido + 0], cc[i + 3 * ido + 0]);
+ ti3 =
+ VADD(cc[i + ido + 1], cc[i + 3 * ido + 1]);
+
+ ch[i] = VADD(tr2, tr3);
+ cr3 = VSUB(tr2, tr3);
+ ch[i + 1] = VADD(ti2, ti3);
+ ci3 = VSUB(ti2, ti3);
+
+ cr2 = VADD(tr1, tr4);
+ cr4 = VSUB(tr1, tr4);
+ ci2 = VADD(ti1, ti4);
+ ci4 = VSUB(ti1, ti4);
+ wr1 = wa1[i], wi1 = fsign * wa1[i + 1];
+ VCPLXMUL(cr2, ci2, LD_PS1(wr1), LD_PS1(wi1));
+ wr2 = wa2[i], wi2 = fsign * wa2[i + 1];
+ ch[i + l1ido] = cr2;
+ ch[i + l1ido + 1] = ci2;
+
+ VCPLXMUL(cr3, ci3, LD_PS1(wr2), LD_PS1(wi2));
+ wr3 = wa3[i], wi3 = fsign * wa3[i + 1];
+ ch[i + 2 * l1ido] = cr3;
+ ch[i + 2 * l1ido + 1] = ci3;
+
+ VCPLXMUL(cr4, ci4, LD_PS1(wr3), LD_PS1(wi3));
+ ch[i + 3 * l1ido] = cr4;
+ ch[i + 3 * l1ido + 1] = ci4;
+ }
+ }
+ }
+} /* passf4 */
+
+/*
+ passf5 and passb5 has been merged here, fsign = -1 for passf5, +1 for passb5
+*/
+static NEVER_INLINE(void) passf5_ps(int ido, int l1, const v4sf * cc, v4sf * ch,
+ const float *wa1, const float *wa2,
+ const float *wa3, const float *wa4,
+ float fsign)
+{
+ static const float tr11 = .309016994374947f;
+ const float ti11 = .951056516295154f * fsign;
+ static const float tr12 = -.809016994374947f;
+ const float ti12 = .587785252292473f * fsign;
+
+ /* Local variables */
+ int i, k;
+ v4sf ci2, ci3, ci4, ci5, di3, di4, di5, di2, cr2, cr3, cr5, cr4, ti2,
+ ti3, ti4, ti5, dr3, dr4, dr5, dr2, tr2, tr3, tr4, tr5;
+
+ float wr1, wi1, wr2, wi2, wr3, wi3, wr4, wi4;
+
+#define cc_ref(a_1,a_2) cc[(a_2-1)*ido + a_1 + 1]
+#define ch_ref(a_1,a_3) ch[(a_3-1)*l1*ido + a_1 + 1]
+
+ assert(ido > 2);
+ for (k = 0; k < l1; ++k, cc += 5 * ido, ch += ido) {
+ for (i = 0; i < ido - 1; i += 2) {
+ ti5 = VSUB(cc_ref(i, 2), cc_ref(i, 5));
+ ti2 = VADD(cc_ref(i, 2), cc_ref(i, 5));
+ ti4 = VSUB(cc_ref(i, 3), cc_ref(i, 4));
+ ti3 = VADD(cc_ref(i, 3), cc_ref(i, 4));
+ tr5 = VSUB(cc_ref(i - 1, 2), cc_ref(i - 1, 5));
+ tr2 = VADD(cc_ref(i - 1, 2), cc_ref(i - 1, 5));
+ tr4 = VSUB(cc_ref(i - 1, 3), cc_ref(i - 1, 4));
+ tr3 = VADD(cc_ref(i - 1, 3), cc_ref(i - 1, 4));
+ ch_ref(i - 1, 1) =
+ VADD(cc_ref(i - 1, 1), VADD(tr2, tr3));
+ ch_ref(i, 1) = VADD(cc_ref(i, 1), VADD(ti2, ti3));
+ cr2 =
+ VADD(cc_ref(i - 1, 1),
+ VADD(SVMUL(tr11, tr2), SVMUL(tr12, tr3)));
+ ci2 =
+ VADD(cc_ref(i, 1),
+ VADD(SVMUL(tr11, ti2), SVMUL(tr12, ti3)));
+ cr3 =
+ VADD(cc_ref(i - 1, 1),
+ VADD(SVMUL(tr12, tr2), SVMUL(tr11, tr3)));
+ ci3 =
+ VADD(cc_ref(i, 1),
+ VADD(SVMUL(tr12, ti2), SVMUL(tr11, ti3)));
+ cr5 = VADD(SVMUL(ti11, tr5), SVMUL(ti12, tr4));
+ ci5 = VADD(SVMUL(ti11, ti5), SVMUL(ti12, ti4));
+ cr4 = VSUB(SVMUL(ti12, tr5), SVMUL(ti11, tr4));
+ ci4 = VSUB(SVMUL(ti12, ti5), SVMUL(ti11, ti4));
+ dr3 = VSUB(cr3, ci4);
+ dr4 = VADD(cr3, ci4);
+ di3 = VADD(ci3, cr4);
+ di4 = VSUB(ci3, cr4);
+ dr5 = VADD(cr2, ci5);
+ dr2 = VSUB(cr2, ci5);
+ di5 = VSUB(ci2, cr5);
+ di2 = VADD(ci2, cr5);
+ wr1 = wa1[i], wi1 = fsign * wa1[i + 1], wr2 =
+ wa2[i], wi2 = fsign * wa2[i + 1];
+ wr3 = wa3[i], wi3 = fsign * wa3[i + 1], wr4 =
+ wa4[i], wi4 = fsign * wa4[i + 1];
+ VCPLXMUL(dr2, di2, LD_PS1(wr1), LD_PS1(wi1));
+ ch_ref(i - 1, 2) = dr2;
+ ch_ref(i, 2) = di2;
+ VCPLXMUL(dr3, di3, LD_PS1(wr2), LD_PS1(wi2));
+ ch_ref(i - 1, 3) = dr3;
+ ch_ref(i, 3) = di3;
+ VCPLXMUL(dr4, di4, LD_PS1(wr3), LD_PS1(wi3));
+ ch_ref(i - 1, 4) = dr4;
+ ch_ref(i, 4) = di4;
+ VCPLXMUL(dr5, di5, LD_PS1(wr4), LD_PS1(wi4));
+ ch_ref(i - 1, 5) = dr5;
+ ch_ref(i, 5) = di5;
+ }
+ }
+#undef ch_ref
+#undef cc_ref
+}
+
+static NEVER_INLINE(void) radf2_ps(int ido, int l1, const v4sf * RESTRICT cc,
+ v4sf * RESTRICT ch, const float *wa1)
+{
+ static const float minus_one = -1.f;
+ int i, k, l1ido = l1 * ido;
+ for (k = 0; k < l1ido; k += ido) {
+ v4sf a = cc[k], b = cc[k + l1ido];
+ ch[2 * k] = VADD(a, b);
+ ch[2 * (k + ido) - 1] = VSUB(a, b);
+ }
+ if (ido < 2)
+ return;
+ if (ido != 2) {
+ for (k = 0; k < l1ido; k += ido) {
+ for (i = 2; i < ido; i += 2) {
+ v4sf tr2 = cc[i - 1 + k + l1ido], ti2 =
+ cc[i + k + l1ido];
+ v4sf br = cc[i - 1 + k], bi = cc[i + k];
+ VCPLXMULCONJ(tr2, ti2, LD_PS1(wa1[i - 2]),
+ LD_PS1(wa1[i - 1]));
+ ch[i + 2 * k] = VADD(bi, ti2);
+ ch[2 * (k + ido) - i] = VSUB(ti2, bi);
+ ch[i - 1 + 2 * k] = VADD(br, tr2);
+ ch[2 * (k + ido) - i - 1] = VSUB(br, tr2);
+ }
+ }
+ if (ido % 2 == 1)
+ return;
+ }
+ for (k = 0; k < l1ido; k += ido) {
+ ch[2 * k + ido] = SVMUL(minus_one, cc[ido - 1 + k + l1ido]);
+ ch[2 * k + ido - 1] = cc[k + ido - 1];
+ }
+} /* radf2 */
+
+static NEVER_INLINE(void) radb2_ps(int ido, int l1, const v4sf * cc, v4sf * ch,
+ const float *wa1)
+{
+ static const float minus_two = -2;
+ int i, k, l1ido = l1 * ido;
+ v4sf a, b, c, d, tr2, ti2;
+ for (k = 0; k < l1ido; k += ido) {
+ a = cc[2 * k];
+ b = cc[2 * (k + ido) - 1];
+ ch[k] = VADD(a, b);
+ ch[k + l1ido] = VSUB(a, b);
+ }
+ if (ido < 2)
+ return;
+ if (ido != 2) {
+ for (k = 0; k < l1ido; k += ido) {
+ for (i = 2; i < ido; i += 2) {
+ a = cc[i - 1 + 2 * k];
+ b = cc[2 * (k + ido) - i - 1];
+ c = cc[i + 0 + 2 * k];
+ d = cc[2 * (k + ido) - i + 0];
+ ch[i - 1 + k] = VADD(a, b);
+ tr2 = VSUB(a, b);
+ ch[i + 0 + k] = VSUB(c, d);
+ ti2 = VADD(c, d);
+ VCPLXMUL(tr2, ti2, LD_PS1(wa1[i - 2]),
+ LD_PS1(wa1[i - 1]));
+ ch[i - 1 + k + l1ido] = tr2;
+ ch[i + 0 + k + l1ido] = ti2;
+ }
+ }
+ if (ido % 2 == 1)
+ return;
+ }
+ for (k = 0; k < l1ido; k += ido) {
+ a = cc[2 * k + ido - 1];
+ b = cc[2 * k + ido];
+ ch[k + ido - 1] = VADD(a, a);
+ ch[k + ido - 1 + l1ido] = SVMUL(minus_two, b);
+ }
+} /* radb2 */
+
+static void radf3_ps(int ido, int l1, const v4sf * RESTRICT cc,
+ v4sf * RESTRICT ch, const float *wa1, const float *wa2)
+{
+ static const float taur = -0.5f;
+ static const float taui = 0.866025403784439f;
+ int i, k, ic;
+ v4sf ci2, di2, di3, cr2, dr2, dr3, ti2, ti3, tr2, tr3, wr1, wi1, wr2,
+ wi2;
+ for (k = 0; k < l1; k++) {
+ cr2 = VADD(cc[(k + l1) * ido], cc[(k + 2 * l1) * ido]);
+ ch[3 * k * ido] = VADD(cc[k * ido], cr2);
+ ch[(3 * k + 2) * ido] =
+ SVMUL(taui,
+ VSUB(cc[(k + l1 * 2) * ido], cc[(k + l1) * ido]));
+ ch[ido - 1 + (3 * k + 1) * ido] =
+ VADD(cc[k * ido], SVMUL(taur, cr2));
+ }
+ if (ido == 1)
+ return;
+ for (k = 0; k < l1; k++) {
+ for (i = 2; i < ido; i += 2) {
+ ic = ido - i;
+ wr1 = LD_PS1(wa1[i - 2]);
+ wi1 = LD_PS1(wa1[i - 1]);
+ dr2 = cc[i - 1 + (k + l1) * ido];
+ di2 = cc[i + (k + l1) * ido];
+ VCPLXMULCONJ(dr2, di2, wr1, wi1);
+
+ wr2 = LD_PS1(wa2[i - 2]);
+ wi2 = LD_PS1(wa2[i - 1]);
+ dr3 = cc[i - 1 + (k + l1 * 2) * ido];
+ di3 = cc[i + (k + l1 * 2) * ido];
+ VCPLXMULCONJ(dr3, di3, wr2, wi2);
+
+ cr2 = VADD(dr2, dr3);
+ ci2 = VADD(di2, di3);
+ ch[i - 1 + 3 * k * ido] =
+ VADD(cc[i - 1 + k * ido], cr2);
+ ch[i + 3 * k * ido] = VADD(cc[i + k * ido], ci2);
+ tr2 = VADD(cc[i - 1 + k * ido], SVMUL(taur, cr2));
+ ti2 = VADD(cc[i + k * ido], SVMUL(taur, ci2));
+ tr3 = SVMUL(taui, VSUB(di2, di3));
+ ti3 = SVMUL(taui, VSUB(dr3, dr2));
+ ch[i - 1 + (3 * k + 2) * ido] = VADD(tr2, tr3);
+ ch[ic - 1 + (3 * k + 1) * ido] = VSUB(tr2, tr3);
+ ch[i + (3 * k + 2) * ido] = VADD(ti2, ti3);
+ ch[ic + (3 * k + 1) * ido] = VSUB(ti3, ti2);
+ }
+ }
+} /* radf3 */
+
+static void radb3_ps(int ido, int l1, const v4sf * RESTRICT cc,
+ v4sf * RESTRICT ch, const float *wa1, const float *wa2)
+{
+ static const float taur = -0.5f;
+ static const float taui = 0.866025403784439f;
+ static const float taui_2 = 0.866025403784439f * 2;
+ int i, k, ic;
+ v4sf ci2, ci3, di2, di3, cr2, cr3, dr2, dr3, ti2, tr2;
+ for (k = 0; k < l1; k++) {
+ tr2 = cc[ido - 1 + (3 * k + 1) * ido];
+ tr2 = VADD(tr2, tr2);
+ cr2 = VMADD(LD_PS1(taur), tr2, cc[3 * k * ido]);
+ ch[k * ido] = VADD(cc[3 * k * ido], tr2);
+ ci3 = SVMUL(taui_2, cc[(3 * k + 2) * ido]);
+ ch[(k + l1) * ido] = VSUB(cr2, ci3);
+ ch[(k + 2 * l1) * ido] = VADD(cr2, ci3);
+ }
+ if (ido == 1)
+ return;
+ for (k = 0; k < l1; k++) {
+ for (i = 2; i < ido; i += 2) {
+ ic = ido - i;
+ tr2 =
+ VADD(cc[i - 1 + (3 * k + 2) * ido],
+ cc[ic - 1 + (3 * k + 1) * ido]);
+ cr2 = VMADD(LD_PS1(taur), tr2, cc[i - 1 + 3 * k * ido]);
+ ch[i - 1 + k * ido] =
+ VADD(cc[i - 1 + 3 * k * ido], tr2);
+ ti2 =
+ VSUB(cc[i + (3 * k + 2) * ido],
+ cc[ic + (3 * k + 1) * ido]);
+ ci2 = VMADD(LD_PS1(taur), ti2, cc[i + 3 * k * ido]);
+ ch[i + k * ido] = VADD(cc[i + 3 * k * ido], ti2);
+ cr3 =
+ SVMUL(taui,
+ VSUB(cc[i - 1 + (3 * k + 2) * ido],
+ cc[ic - 1 + (3 * k + 1) * ido]));
+ ci3 =
+ SVMUL(taui,
+ VADD(cc[i + (3 * k + 2) * ido],
+ cc[ic + (3 * k + 1) * ido]));
+ dr2 = VSUB(cr2, ci3);
+ dr3 = VADD(cr2, ci3);
+ di2 = VADD(ci2, cr3);
+ di3 = VSUB(ci2, cr3);
+ VCPLXMUL(dr2, di2, LD_PS1(wa1[i - 2]),
+ LD_PS1(wa1[i - 1]));
+ ch[i - 1 + (k + l1) * ido] = dr2;
+ ch[i + (k + l1) * ido] = di2;
+ VCPLXMUL(dr3, di3, LD_PS1(wa2[i - 2]),
+ LD_PS1(wa2[i - 1]));
+ ch[i - 1 + (k + 2 * l1) * ido] = dr3;
+ ch[i + (k + 2 * l1) * ido] = di3;
+ }
+ }
+} /* radb3 */
+
+static NEVER_INLINE(void) radf4_ps(int ido, int l1, const v4sf * RESTRICT cc,
+ v4sf * RESTRICT ch,
+ const float *RESTRICT wa1,
+ const float *RESTRICT wa2,
+ const float *RESTRICT wa3)
+{
+ static const float minus_hsqt2 = (float)-0.7071067811865475;
+ int i, k, l1ido = l1 * ido;
+ {
+ const v4sf *RESTRICT cc_ = cc, *RESTRICT cc_end = cc + l1ido;
+ v4sf *RESTRICT ch_ = ch;
+ while (cc < cc_end) {
+ // this loop represents between 25% and 40% of total radf4_ps cost !
+ v4sf a0 = cc[0], a1 = cc[l1ido];
+ v4sf a2 = cc[2 * l1ido], a3 = cc[3 * l1ido];
+ v4sf tr1 = VADD(a1, a3);
+ v4sf tr2 = VADD(a0, a2);
+ ch[2 * ido - 1] = VSUB(a0, a2);
+ ch[2 * ido] = VSUB(a3, a1);
+ ch[0] = VADD(tr1, tr2);
+ ch[4 * ido - 1] = VSUB(tr2, tr1);
+ cc += ido;
+ ch += 4 * ido;
+ }
+ cc = cc_;
+ ch = ch_;
+ }
+ if (ido < 2)
+ return;
+ if (ido != 2) {
+ for (k = 0; k < l1ido; k += ido) {
+ const v4sf *RESTRICT pc = (v4sf *) (cc + 1 + k);
+ for (i = 2; i < ido; i += 2, pc += 2) {
+ int ic = ido - i;
+ v4sf wr, wi, cr2, ci2, cr3, ci3, cr4, ci4;
+ v4sf tr1, ti1, tr2, ti2, tr3, ti3, tr4, ti4;
+
+ cr2 = pc[1 * l1ido + 0];
+ ci2 = pc[1 * l1ido + 1];
+ wr = LD_PS1(wa1[i - 2]);
+ wi = LD_PS1(wa1[i - 1]);
+ VCPLXMULCONJ(cr2, ci2, wr, wi);
+
+ cr3 = pc[2 * l1ido + 0];
+ ci3 = pc[2 * l1ido + 1];
+ wr = LD_PS1(wa2[i - 2]);
+ wi = LD_PS1(wa2[i - 1]);
+ VCPLXMULCONJ(cr3, ci3, wr, wi);
+
+ cr4 = pc[3 * l1ido];
+ ci4 = pc[3 * l1ido + 1];
+ wr = LD_PS1(wa3[i - 2]);
+ wi = LD_PS1(wa3[i - 1]);
+ VCPLXMULCONJ(cr4, ci4, wr, wi);
+
+ /* at this point, on SSE, five of "cr2 cr3 cr4 ci2 ci3 ci4" should be loaded in registers */
+
+ tr1 = VADD(cr2, cr4);
+ tr4 = VSUB(cr4, cr2);
+ tr2 = VADD(pc[0], cr3);
+ tr3 = VSUB(pc[0], cr3);
+ ch[i - 1 + 4 * k] = VADD(tr1, tr2);
+ ch[ic - 1 + 4 * k + 3 * ido] = VSUB(tr2, tr1); // at this point tr1 and tr2 can be disposed
+ ti1 = VADD(ci2, ci4);
+ ti4 = VSUB(ci2, ci4);
+ ch[i - 1 + 4 * k + 2 * ido] = VADD(ti4, tr3);
+ ch[ic - 1 + 4 * k + 1 * ido] = VSUB(tr3, ti4); // dispose tr3, ti4
+ ti2 = VADD(pc[1], ci3);
+ ti3 = VSUB(pc[1], ci3);
+ ch[i + 4 * k] = VADD(ti1, ti2);
+ ch[ic + 4 * k + 3 * ido] = VSUB(ti1, ti2);
+ ch[i + 4 * k + 2 * ido] = VADD(tr4, ti3);
+ ch[ic + 4 * k + 1 * ido] = VSUB(tr4, ti3);
+ }
+ }
+ if (ido % 2 == 1)
+ return;
+ }
+ for (k = 0; k < l1ido; k += ido) {
+ v4sf a = cc[ido - 1 + k + l1ido], b =
+ cc[ido - 1 + k + 3 * l1ido];
+ v4sf c = cc[ido - 1 + k], d = cc[ido - 1 + k + 2 * l1ido];
+ v4sf ti1 = SVMUL(minus_hsqt2, VADD(a, b));
+ v4sf tr1 = SVMUL(minus_hsqt2, VSUB(b, a));
+ ch[ido - 1 + 4 * k] = VADD(tr1, c);
+ ch[ido - 1 + 4 * k + 2 * ido] = VSUB(c, tr1);
+ ch[4 * k + 1 * ido] = VSUB(ti1, d);
+ ch[4 * k + 3 * ido] = VADD(ti1, d);
+ }
+} /* radf4 */
+
+static NEVER_INLINE(void) radb4_ps(int ido, int l1, const v4sf * RESTRICT cc,
+ v4sf * RESTRICT ch,
+ const float *RESTRICT wa1,
+ const float *RESTRICT wa2,
+ const float *RESTRICT wa3)
+{
+ static const float minus_sqrt2 = (float)-1.414213562373095;
+ static const float two = 2.f;
+ int i, k, l1ido = l1 * ido;
+ v4sf ci2, ci3, ci4, cr2, cr3, cr4, ti1, ti2, ti3, ti4, tr1, tr2, tr3,
+ tr4;
+ {
+ const v4sf *RESTRICT cc_ = cc, *RESTRICT ch_end = ch + l1ido;
+ v4sf *ch_ = ch;
+ while (ch < ch_end) {
+ v4sf a = cc[0], b = cc[4 * ido - 1];
+ v4sf c = cc[2 * ido], d = cc[2 * ido - 1];
+ tr3 = SVMUL(two, d);
+ tr2 = VADD(a, b);
+ tr1 = VSUB(a, b);
+ tr4 = SVMUL(two, c);
+ ch[0 * l1ido] = VADD(tr2, tr3);
+ ch[2 * l1ido] = VSUB(tr2, tr3);
+ ch[1 * l1ido] = VSUB(tr1, tr4);
+ ch[3 * l1ido] = VADD(tr1, tr4);
+
+ cc += 4 * ido;
+ ch += ido;
+ }
+ cc = cc_;
+ ch = ch_;
+ }
+ if (ido < 2)
+ return;
+ if (ido != 2) {
+ for (k = 0; k < l1ido; k += ido) {
+ const v4sf *RESTRICT pc = (v4sf *) (cc - 1 + 4 * k);
+ v4sf *RESTRICT ph = (v4sf *) (ch + k + 1);
+ for (i = 2; i < ido; i += 2) {
+
+ tr1 = VSUB(pc[i], pc[4 * ido - i]);
+ tr2 = VADD(pc[i], pc[4 * ido - i]);
+ ti4 = VSUB(pc[2 * ido + i], pc[2 * ido - i]);
+ tr3 = VADD(pc[2 * ido + i], pc[2 * ido - i]);
+ ph[0] = VADD(tr2, tr3);
+ cr3 = VSUB(tr2, tr3);
+
+ ti3 =
+ VSUB(pc[2 * ido + i + 1],
+ pc[2 * ido - i + 1]);
+ tr4 =
+ VADD(pc[2 * ido + i + 1],
+ pc[2 * ido - i + 1]);
+ cr2 = VSUB(tr1, tr4);
+ cr4 = VADD(tr1, tr4);
+
+ ti1 = VADD(pc[i + 1], pc[4 * ido - i + 1]);
+ ti2 = VSUB(pc[i + 1], pc[4 * ido - i + 1]);
+
+ ph[1] = VADD(ti2, ti3);
+ ph += l1ido;
+ ci3 = VSUB(ti2, ti3);
+ ci2 = VADD(ti1, ti4);
+ ci4 = VSUB(ti1, ti4);
+ VCPLXMUL(cr2, ci2, LD_PS1(wa1[i - 2]),
+ LD_PS1(wa1[i - 1]));
+ ph[0] = cr2;
+ ph[1] = ci2;
+ ph += l1ido;
+ VCPLXMUL(cr3, ci3, LD_PS1(wa2[i - 2]),
+ LD_PS1(wa2[i - 1]));
+ ph[0] = cr3;
+ ph[1] = ci3;
+ ph += l1ido;
+ VCPLXMUL(cr4, ci4, LD_PS1(wa3[i - 2]),
+ LD_PS1(wa3[i - 1]));
+ ph[0] = cr4;
+ ph[1] = ci4;
+ ph = ph - 3 * l1ido + 2;
+ }
+ }
+ if (ido % 2 == 1)
+ return;
+ }
+ for (k = 0; k < l1ido; k += ido) {
+ int i0 = 4 * k + ido;
+ v4sf c = cc[i0 - 1], d = cc[i0 + 2 * ido - 1];
+ v4sf a = cc[i0 + 0], b = cc[i0 + 2 * ido + 0];
+ tr1 = VSUB(c, d);
+ tr2 = VADD(c, d);
+ ti1 = VADD(b, a);
+ ti2 = VSUB(b, a);
+ ch[ido - 1 + k + 0 * l1ido] = VADD(tr2, tr2);
+ ch[ido - 1 + k + 1 * l1ido] =
+ SVMUL(minus_sqrt2, VSUB(ti1, tr1));
+ ch[ido - 1 + k + 2 * l1ido] = VADD(ti2, ti2);
+ ch[ido - 1 + k + 3 * l1ido] =
+ SVMUL(minus_sqrt2, VADD(ti1, tr1));
+ }
+} /* radb4 */
+
+static void radf5_ps(int ido, int l1, const v4sf * RESTRICT cc,
+ v4sf * RESTRICT ch, const float *wa1, const float *wa2,
+ const float *wa3, const float *wa4)
+{
+ static const float tr11 = .309016994374947f;
+ static const float ti11 = .951056516295154f;
+ static const float tr12 = -.809016994374947f;
+ static const float ti12 = .587785252292473f;
+
+ /* System generated locals */
+ int cc_offset, ch_offset;
+
+ /* Local variables */
+ int i, k, ic;
+ v4sf ci2, di2, ci4, ci5, di3, di4, di5, ci3, cr2, cr3, dr2, dr3, dr4,
+ dr5, cr5, cr4, ti2, ti3, ti5, ti4, tr2, tr3, tr4, tr5;
+ int idp2;
+
+#define cc_ref(a_1,a_2,a_3) cc[((a_3)*l1 + (a_2))*ido + a_1]
+#define ch_ref(a_1,a_2,a_3) ch[((a_3)*5 + (a_2))*ido + a_1]
+
+ /* Parameter adjustments */
+ ch_offset = 1 + ido * 6;
+ ch -= ch_offset;
+ cc_offset = 1 + ido * (1 + l1);
+ cc -= cc_offset;
+
+ /* Function Body */
+ for (k = 1; k <= l1; ++k) {
+ cr2 = VADD(cc_ref(1, k, 5), cc_ref(1, k, 2));
+ ci5 = VSUB(cc_ref(1, k, 5), cc_ref(1, k, 2));
+ cr3 = VADD(cc_ref(1, k, 4), cc_ref(1, k, 3));
+ ci4 = VSUB(cc_ref(1, k, 4), cc_ref(1, k, 3));
+ ch_ref(1, 1, k) = VADD(cc_ref(1, k, 1), VADD(cr2, cr3));
+ ch_ref(ido, 2, k) =
+ VADD(cc_ref(1, k, 1),
+ VADD(SVMUL(tr11, cr2), SVMUL(tr12, cr3)));
+ ch_ref(1, 3, k) = VADD(SVMUL(ti11, ci5), SVMUL(ti12, ci4));
+ ch_ref(ido, 4, k) =
+ VADD(cc_ref(1, k, 1),
+ VADD(SVMUL(tr12, cr2), SVMUL(tr11, cr3)));
+ ch_ref(1, 5, k) = VSUB(SVMUL(ti12, ci5), SVMUL(ti11, ci4));
+ //printf("pffft: radf5, k=%d ch_ref=%f, ci4=%f\n", k, ch_ref(1, 5, k), ci4);
+ }
+ if (ido == 1) {
+ return;
+ }
+ idp2 = ido + 2;
+ for (k = 1; k <= l1; ++k) {
+ for (i = 3; i <= ido; i += 2) {
+ ic = idp2 - i;
+ dr2 = LD_PS1(wa1[i - 3]);
+ di2 = LD_PS1(wa1[i - 2]);
+ dr3 = LD_PS1(wa2[i - 3]);
+ di3 = LD_PS1(wa2[i - 2]);
+ dr4 = LD_PS1(wa3[i - 3]);
+ di4 = LD_PS1(wa3[i - 2]);
+ dr5 = LD_PS1(wa4[i - 3]);
+ di5 = LD_PS1(wa4[i - 2]);
+ VCPLXMULCONJ(dr2, di2, cc_ref(i - 1, k, 2),
+ cc_ref(i, k, 2));
+ VCPLXMULCONJ(dr3, di3, cc_ref(i - 1, k, 3),
+ cc_ref(i, k, 3));
+ VCPLXMULCONJ(dr4, di4, cc_ref(i - 1, k, 4),
+ cc_ref(i, k, 4));
+ VCPLXMULCONJ(dr5, di5, cc_ref(i - 1, k, 5),
+ cc_ref(i, k, 5));
+ cr2 = VADD(dr2, dr5);
+ ci5 = VSUB(dr5, dr2);
+ cr5 = VSUB(di2, di5);
+ ci2 = VADD(di2, di5);
+ cr3 = VADD(dr3, dr4);
+ ci4 = VSUB(dr4, dr3);
+ cr4 = VSUB(di3, di4);
+ ci3 = VADD(di3, di4);
+ ch_ref(i - 1, 1, k) =
+ VADD(cc_ref(i - 1, k, 1), VADD(cr2, cr3));
+ ch_ref(i, 1, k) = VSUB(cc_ref(i, k, 1), VADD(ci2, ci3)); //
+ tr2 =
+ VADD(cc_ref(i - 1, k, 1),
+ VADD(SVMUL(tr11, cr2), SVMUL(tr12, cr3)));
+ ti2 = VSUB(cc_ref(i, k, 1), VADD(SVMUL(tr11, ci2), SVMUL(tr12, ci3))); //
+ tr3 =
+ VADD(cc_ref(i - 1, k, 1),
+ VADD(SVMUL(tr12, cr2), SVMUL(tr11, cr3)));
+ ti3 = VSUB(cc_ref(i, k, 1), VADD(SVMUL(tr12, ci2), SVMUL(tr11, ci3))); //
+ tr5 = VADD(SVMUL(ti11, cr5), SVMUL(ti12, cr4));
+ ti5 = VADD(SVMUL(ti11, ci5), SVMUL(ti12, ci4));
+ tr4 = VSUB(SVMUL(ti12, cr5), SVMUL(ti11, cr4));
+ ti4 = VSUB(SVMUL(ti12, ci5), SVMUL(ti11, ci4));
+ ch_ref(i - 1, 3, k) = VSUB(tr2, tr5);
+ ch_ref(ic - 1, 2, k) = VADD(tr2, tr5);
+ ch_ref(i, 3, k) = VADD(ti2, ti5);
+ ch_ref(ic, 2, k) = VSUB(ti5, ti2);
+ ch_ref(i - 1, 5, k) = VSUB(tr3, tr4);
+ ch_ref(ic - 1, 4, k) = VADD(tr3, tr4);
+ ch_ref(i, 5, k) = VADD(ti3, ti4);
+ ch_ref(ic, 4, k) = VSUB(ti4, ti3);
+ }
+ }
+#undef cc_ref
+#undef ch_ref
+} /* radf5 */
+
+static void radb5_ps(int ido, int l1, const v4sf * RESTRICT cc,
+ v4sf * RESTRICT ch, const float *wa1, const float *wa2,
+ const float *wa3, const float *wa4)
+{
+ static const float tr11 = .309016994374947f;
+ static const float ti11 = .951056516295154f;
+ static const float tr12 = -.809016994374947f;
+ static const float ti12 = .587785252292473f;
+
+ int cc_offset, ch_offset;
+
+ /* Local variables */
+ int i, k, ic;
+ v4sf ci2, ci3, ci4, ci5, di3, di4, di5, di2, cr2, cr3, cr5, cr4, ti2,
+ ti3, ti4, ti5, dr3, dr4, dr5, dr2, tr2, tr3, tr4, tr5;
+ int idp2;
+
+#define cc_ref(a_1,a_2,a_3) cc[((a_3)*5 + (a_2))*ido + a_1]
+#define ch_ref(a_1,a_2,a_3) ch[((a_3)*l1 + (a_2))*ido + a_1]
+
+ /* Parameter adjustments */
+ ch_offset = 1 + ido * (1 + l1);
+ ch -= ch_offset;
+ cc_offset = 1 + ido * 6;
+ cc -= cc_offset;
+
+ /* Function Body */
+ for (k = 1; k <= l1; ++k) {
+ ti5 = VADD(cc_ref(1, 3, k), cc_ref(1, 3, k));
+ ti4 = VADD(cc_ref(1, 5, k), cc_ref(1, 5, k));
+ tr2 = VADD(cc_ref(ido, 2, k), cc_ref(ido, 2, k));
+ tr3 = VADD(cc_ref(ido, 4, k), cc_ref(ido, 4, k));
+ ch_ref(1, k, 1) = VADD(cc_ref(1, 1, k), VADD(tr2, tr3));
+ cr2 =
+ VADD(cc_ref(1, 1, k),
+ VADD(SVMUL(tr11, tr2), SVMUL(tr12, tr3)));
+ cr3 =
+ VADD(cc_ref(1, 1, k),
+ VADD(SVMUL(tr12, tr2), SVMUL(tr11, tr3)));
+ ci5 = VADD(SVMUL(ti11, ti5), SVMUL(ti12, ti4));
+ ci4 = VSUB(SVMUL(ti12, ti5), SVMUL(ti11, ti4));
+ ch_ref(1, k, 2) = VSUB(cr2, ci5);
+ ch_ref(1, k, 3) = VSUB(cr3, ci4);
+ ch_ref(1, k, 4) = VADD(cr3, ci4);
+ ch_ref(1, k, 5) = VADD(cr2, ci5);
+ }
+ if (ido == 1) {
+ return;
+ }
+ idp2 = ido + 2;
+ for (k = 1; k <= l1; ++k) {
+ for (i = 3; i <= ido; i += 2) {
+ ic = idp2 - i;
+ ti5 = VADD(cc_ref(i, 3, k), cc_ref(ic, 2, k));
+ ti2 = VSUB(cc_ref(i, 3, k), cc_ref(ic, 2, k));
+ ti4 = VADD(cc_ref(i, 5, k), cc_ref(ic, 4, k));
+ ti3 = VSUB(cc_ref(i, 5, k), cc_ref(ic, 4, k));
+ tr5 = VSUB(cc_ref(i - 1, 3, k), cc_ref(ic - 1, 2, k));
+ tr2 = VADD(cc_ref(i - 1, 3, k), cc_ref(ic - 1, 2, k));
+ tr4 = VSUB(cc_ref(i - 1, 5, k), cc_ref(ic - 1, 4, k));
+ tr3 = VADD(cc_ref(i - 1, 5, k), cc_ref(ic - 1, 4, k));
+ ch_ref(i - 1, k, 1) =
+ VADD(cc_ref(i - 1, 1, k), VADD(tr2, tr3));
+ ch_ref(i, k, 1) = VADD(cc_ref(i, 1, k), VADD(ti2, ti3));
+ cr2 =
+ VADD(cc_ref(i - 1, 1, k),
+ VADD(SVMUL(tr11, tr2), SVMUL(tr12, tr3)));
+ ci2 =
+ VADD(cc_ref(i, 1, k),
+ VADD(SVMUL(tr11, ti2), SVMUL(tr12, ti3)));
+ cr3 =
+ VADD(cc_ref(i - 1, 1, k),
+ VADD(SVMUL(tr12, tr2), SVMUL(tr11, tr3)));
+ ci3 =
+ VADD(cc_ref(i, 1, k),
+ VADD(SVMUL(tr12, ti2), SVMUL(tr11, ti3)));
+ cr5 = VADD(SVMUL(ti11, tr5), SVMUL(ti12, tr4));
+ ci5 = VADD(SVMUL(ti11, ti5), SVMUL(ti12, ti4));
+ cr4 = VSUB(SVMUL(ti12, tr5), SVMUL(ti11, tr4));
+ ci4 = VSUB(SVMUL(ti12, ti5), SVMUL(ti11, ti4));
+ dr3 = VSUB(cr3, ci4);
+ dr4 = VADD(cr3, ci4);
+ di3 = VADD(ci3, cr4);
+ di4 = VSUB(ci3, cr4);
+ dr5 = VADD(cr2, ci5);
+ dr2 = VSUB(cr2, ci5);
+ di5 = VSUB(ci2, cr5);
+ di2 = VADD(ci2, cr5);
+ VCPLXMUL(dr2, di2, LD_PS1(wa1[i - 3]),
+ LD_PS1(wa1[i - 2]));
+ VCPLXMUL(dr3, di3, LD_PS1(wa2[i - 3]),
+ LD_PS1(wa2[i - 2]));
+ VCPLXMUL(dr4, di4, LD_PS1(wa3[i - 3]),
+ LD_PS1(wa3[i - 2]));
+ VCPLXMUL(dr5, di5, LD_PS1(wa4[i - 3]),
+ LD_PS1(wa4[i - 2]));
+
+ ch_ref(i - 1, k, 2) = dr2;
+ ch_ref(i, k, 2) = di2;
+ ch_ref(i - 1, k, 3) = dr3;
+ ch_ref(i, k, 3) = di3;
+ ch_ref(i - 1, k, 4) = dr4;
+ ch_ref(i, k, 4) = di4;
+ ch_ref(i - 1, k, 5) = dr5;
+ ch_ref(i, k, 5) = di5;
+ }
+ }
+#undef cc_ref
+#undef ch_ref
+} /* radb5 */
+
+static NEVER_INLINE(v4sf *) rfftf1_ps(int n, const v4sf * input_readonly,
+ v4sf * work1, v4sf * work2,
+ const float *wa, const int *ifac)
+{
+ v4sf *in = (v4sf *) input_readonly;
+ v4sf *out = (in == work2 ? work1 : work2);
+ int nf = ifac[1], k1;
+ int l2 = n;
+ int iw = n - 1;
+ assert(in != out && work1 != work2);
+ for (k1 = 1; k1 <= nf; ++k1) {
+ int kh = nf - k1;
+ int ip = ifac[kh + 2];
+ int l1 = l2 / ip;
+ int ido = n / l2;
+ iw -= (ip - 1) * ido;
+ switch (ip) {
+ case 5:{
+ int ix2 = iw + ido;
+ int ix3 = ix2 + ido;
+ int ix4 = ix3 + ido;
+ radf5_ps(ido, l1, in, out, &wa[iw], &wa[ix2],
+ &wa[ix3], &wa[ix4]);
+ } break;
+ case 4:{
+ int ix2 = iw + ido;
+ int ix3 = ix2 + ido;
+ radf4_ps(ido, l1, in, out, &wa[iw], &wa[ix2],
+ &wa[ix3]);
+ } break;
+ case 3:{
+ int ix2 = iw + ido;
+ radf3_ps(ido, l1, in, out, &wa[iw], &wa[ix2]);
+ } break;
+ case 2:
+ radf2_ps(ido, l1, in, out, &wa[iw]);
+ break;
+ default:
+ assert(0);
+ break;
+ }
+ l2 = l1;
+ if (out == work2) {
+ out = work1;
+ in = work2;
+ } else {
+ out = work2;
+ in = work1;
+ }
+ }
+ return in; /* this is in fact the output .. */
+} /* rfftf1 */
+
+static NEVER_INLINE(v4sf *) rfftb1_ps(int n, const v4sf * input_readonly,
+ v4sf * work1, v4sf * work2,
+ const float *wa, const int *ifac)
+{
+ v4sf *in = (v4sf *) input_readonly;
+ v4sf *out = (in == work2 ? work1 : work2);
+ int nf = ifac[1], k1;
+ int l1 = 1;
+ int iw = 0;
+ assert(in != out);
+ for (k1 = 1; k1 <= nf; k1++) {
+ int ip = ifac[k1 + 1];
+ int l2 = ip * l1;
+ int ido = n / l2;
+ switch (ip) {
+ case 5:{
+ int ix2 = iw + ido;
+ int ix3 = ix2 + ido;
+ int ix4 = ix3 + ido;
+ radb5_ps(ido, l1, in, out, &wa[iw], &wa[ix2],
+ &wa[ix3], &wa[ix4]);
+ } break;
+ case 4:{
+ int ix2 = iw + ido;
+ int ix3 = ix2 + ido;
+ radb4_ps(ido, l1, in, out, &wa[iw], &wa[ix2],
+ &wa[ix3]);
+ } break;
+ case 3:{
+ int ix2 = iw + ido;
+ radb3_ps(ido, l1, in, out, &wa[iw], &wa[ix2]);
+ } break;
+ case 2:
+ radb2_ps(ido, l1, in, out, &wa[iw]);
+ break;
+ default:
+ assert(0);
+ break;
+ }
+ l1 = l2;
+ iw += (ip - 1) * ido;
+
+ if (out == work2) {
+ out = work1;
+ in = work2;
+ } else {
+ out = work2;
+ in = work1;
+ }
+ }
+ return in; /* this is in fact the output .. */
+}
+
+static int decompose(int n, int *ifac, const int *ntryh)
+{
+ int nl = n, nf = 0, i, j = 0;
+ for (j = 0; ntryh[j]; ++j) {
+ int ntry = ntryh[j];
+ while (nl != 1) {
+ int nq = nl / ntry;
+ int nr = nl - ntry * nq;
+ if (nr == 0) {
+ ifac[2 + nf++] = ntry;
+ nl = nq;
+ if (ntry == 2 && nf != 1) {
+ for (i = 2; i <= nf; ++i) {
+ int ib = nf - i + 2;
+ ifac[ib + 1] = ifac[ib];
+ }
+ ifac[2] = 2;
+ }
+ } else
+ break;
+ }
+ }
+ ifac[0] = n;
+ ifac[1] = nf;
+ return nf;
+}
+
+static void rffti1_ps(int n, float *wa, int *ifac)
+{
+ static const int ntryh[] = { 4, 2, 3, 5, 0 };
+ int k1, j, ii;
+
+ int nf = decompose(n, ifac, ntryh);
+ float argh = (2 * M_PI) / n;
+ int is = 0;
+ int nfm1 = nf - 1;
+ int l1 = 1;
+ for (k1 = 1; k1 <= nfm1; k1++) {
+ int ip = ifac[k1 + 1];
+ int ld = 0;
+ int l2 = l1 * ip;
+ int ido = n / l2;
+ int ipm = ip - 1;
+ for (j = 1; j <= ipm; ++j) {
+ float argld;
+ int i = is, fi = 0;
+ ld += l1;
+ argld = ld * argh;
+ for (ii = 3; ii <= ido; ii += 2) {
+ i += 2;
+ fi += 1;
+ wa[i - 2] = cos(fi * argld);
+ wa[i - 1] = sin(fi * argld);
+ }
+ is += ido;
+ }
+ l1 = l2;
+ }
+} /* rffti1 */
+
+static void cffti1_ps(int n, float *wa, int *ifac)
+{
+ static const int ntryh[] = { 5, 3, 4, 2, 0 };
+ int k1, j, ii;
+
+ int nf = decompose(n, ifac, ntryh);
+ float argh = (2 * M_PI) / (float)n;
+ int i = 1;
+ int l1 = 1;
+ for (k1 = 1; k1 <= nf; k1++) {
+ int ip = ifac[k1 + 1];
+ int ld = 0;
+ int l2 = l1 * ip;
+ int ido = n / l2;
+ int idot = ido + ido + 2;
+ int ipm = ip - 1;
+ for (j = 1; j <= ipm; j++) {
+ float argld;
+ int i1 = i, fi = 0;
+ wa[i - 1] = 1;
+ wa[i] = 0;
+ ld += l1;
+ argld = ld * argh;
+ for (ii = 4; ii <= idot; ii += 2) {
+ i += 2;
+ fi += 1;
+ wa[i - 1] = cos(fi * argld);
+ wa[i] = sin(fi * argld);
+ }
+ if (ip > 5) {
+ wa[i1 - 1] = wa[i - 1];
+ wa[i1] = wa[i];
+ }
+ }
+ l1 = l2;
+ }
+} /* cffti1 */
+
+static v4sf *cfftf1_ps(int n, const v4sf * input_readonly, v4sf * work1, v4sf * work2,
+ const float *wa, const int *ifac, int isign)
+{
+ v4sf *in = (v4sf *) input_readonly;
+ v4sf *out = (in == work2 ? work1 : work2);
+ int nf = ifac[1], k1;
+ int l1 = 1;
+ int iw = 0;
+ assert(in != out && work1 != work2);
+ for (k1 = 2; k1 <= nf + 1; k1++) {
+ int ip = ifac[k1];
+ int l2 = ip * l1;
+ int ido = n / l2;
+ int idot = ido + ido;
+ switch (ip) {
+ case 5:{
+ int ix2 = iw + idot;
+ int ix3 = ix2 + idot;
+ int ix4 = ix3 + idot;
+ passf5_ps(idot, l1, in, out, &wa[iw], &wa[ix2],
+ &wa[ix3], &wa[ix4], isign);
+ } break;
+ case 4:{
+ int ix2 = iw + idot;
+ int ix3 = ix2 + idot;
+ passf4_ps(idot, l1, in, out, &wa[iw], &wa[ix2],
+ &wa[ix3], isign);
+ } break;
+ case 2:{
+ passf2_ps(idot, l1, in, out, &wa[iw], isign);
+ }
+ break;
+ case 3:{
+ int ix2 = iw + idot;
+ passf3_ps(idot, l1, in, out, &wa[iw], &wa[ix2],
+ isign);
+ } break;
+ default:
+ assert(0);
+ }
+ l1 = l2;
+ iw += (ip - 1) * idot;
+ if (out == work2) {
+ out = work1;
+ in = work2;
+ } else {
+ out = work2;
+ in = work1;
+ }
+ }
+
+ return in; /* this is in fact the output .. */
+}
+
+struct PFFFT_Setup {
+ int N;
+ int Ncvec; // nb of complex simd vectors (N/4 if PFFFT_COMPLEX, N/8 if PFFFT_REAL)
+ int ifac[15];
+ pffft_transform_t transform;
+ v4sf *data; // allocated room for twiddle coefs
+ float *e; // points into 'data' , N/4*3 elements
+ float *twiddle; // points into 'data', N/4 elements
+};
+
+struct funcs {
+ PFFFT_Setup * (*new_setup) (int N, pffft_transform_t transform);
+ void (*transform) (PFFFT_Setup *setup, const float *input, float *output, float *work, pffft_direction_t direction, int ordered);
+ void (*zreorder)(PFFFT_Setup *setup, const float *input, float *output, pffft_direction_t direction);
+ void (*zconvolve_accumulate)(PFFFT_Setup *setup, const float *dft_a, const float *dft_b, const float *dft_c, float *dft_ab, float scaling);
+ void (*zconvolve)(PFFFT_Setup *setup, const float *dft_a, const float *dft_b, float *dft_ab, float scaling);
+ int (*simd_size)(void);
+ void (*validate)(void);
+};
+
+static PFFFT_Setup *new_setup_simd(int N, pffft_transform_t transform)
+{
+ PFFFT_Setup *s = (PFFFT_Setup *) malloc(sizeof(PFFFT_Setup));
+ int k, m;
+ /* unfortunately, the fft size must be a multiple of 16 for complex FFTs
+ and 32 for real FFTs -- a lot of stuff would need to be rewritten to
+ handle other cases (or maybe just switch to a scalar fft, I don't know..) */
+ if (transform == PFFFT_REAL) {
+ assert((N % (2 * SIMD_SZ * SIMD_SZ)) == 0 && N > 0);
+ }
+ if (transform == PFFFT_COMPLEX) {
+ assert((N % (SIMD_SZ * SIMD_SZ)) == 0 && N > 0);
+ }
+ //assert((N % 32) == 0);
+ s->N = N;
+ s->transform = transform;
+ /* nb of complex simd vectors */
+ s->Ncvec = (transform == PFFFT_REAL ? N / 2 : N) / SIMD_SZ;
+ s->data = (v4sf *) pffft_aligned_malloc(2 * s->Ncvec * sizeof(v4sf));
+ s->e = (float *)s->data;
+ s->twiddle =
+ (float *)(s->data + (2 * s->Ncvec * (SIMD_SZ - 1)) / SIMD_SZ);
+
+ if (transform == PFFFT_REAL) {
+ for (k = 0; k < s->Ncvec; ++k) {
+ int i = k / SIMD_SZ;
+ int j = k % SIMD_SZ;
+ for (m = 0; m < SIMD_SZ - 1; ++m) {
+ float A = -2 * M_PI * (m + 1) * k / N;
+ s->e[(2 * (i * 3 + m) + 0) * SIMD_SZ + j] =
+ cos(A);
+ s->e[(2 * (i * 3 + m) + 1) * SIMD_SZ + j] =
+ sin(A);
+ }
+ }
+ rffti1_ps(N / SIMD_SZ, s->twiddle, s->ifac);
+ } else {
+ for (k = 0; k < s->Ncvec; ++k) {
+ int i = k / SIMD_SZ;
+ int j = k % SIMD_SZ;
+ for (m = 0; m < SIMD_SZ - 1; ++m) {
+ float A = -2 * M_PI * (m + 1) * k / N;
+ s->e[(2 * (i * 3 + m) + 0) * SIMD_SZ + j] =
+ cos(A);
+ s->e[(2 * (i * 3 + m) + 1) * SIMD_SZ + j] =
+ sin(A);
+ }
+ }
+ cffti1_ps(N / SIMD_SZ, s->twiddle, s->ifac);
+ }
+
+ /* check that N is decomposable with allowed prime factors */
+ for (k = 0, m = 1; k < s->ifac[1]; ++k) {
+ m *= s->ifac[2 + k];
+ }
+ if (m != N / SIMD_SZ) {
+ pffft_destroy_setup(s);
+ s = 0;
+ }
+
+ return s;
+}
+
+#if !defined(PFFFT_SIMD_DISABLE)
+
+/* [0 0 1 2 3 4 5 6 7 8] -> [0 8 7 6 5 4 3 2 1] */
+static void reversed_copy(int N, const v4sf * in, int in_stride, v4sf * out)
+{
+ v4sf g0, g1;
+ int k;
+ INTERLEAVE2(in[0], in[1], g0, g1);
+ in += in_stride;
+
+ *--out = VSWAPHL(g0, g1); // [g0l, g0h], [g1l g1h] -> [g1l, g0h]
+ for (k = 1; k < N; ++k) {
+ v4sf h0, h1;
+ INTERLEAVE2(in[0], in[1], h0, h1);
+ in += in_stride;
+ *--out = VSWAPHL(g1, h0);
+ *--out = VSWAPHL(h0, h1);
+ g1 = h1;
+ }
+ *--out = VSWAPHL(g1, g0);
+}
+
+static void unreversed_copy(int N, const v4sf * in, v4sf * out, int out_stride)
+{
+ v4sf g0, g1, h0, h1;
+ int k;
+ g0 = g1 = in[0];
+ ++in;
+ for (k = 1; k < N; ++k) {
+ h0 = *in++;
+ h1 = *in++;
+ g1 = VSWAPHL(g1, h0);
+ h0 = VSWAPHL(h0, h1);
+ UNINTERLEAVE2(h0, g1, out[0], out[1]);
+ out += out_stride;
+ g1 = h1;
+ }
+ h0 = *in++;
+ h1 = g0;
+ g1 = VSWAPHL(g1, h0);
+ h0 = VSWAPHL(h0, h1);
+ UNINTERLEAVE2(h0, g1, out[0], out[1]);
+}
+
+static void zreorder_simd(PFFFT_Setup * setup, const float *in, float *out,
+ pffft_direction_t direction)
+{
+ int k, N = setup->N, Ncvec = setup->Ncvec;
+ const v4sf *vin = (const v4sf *)in;
+ v4sf *vout = (v4sf *) out;
+ assert(in != out);
+ if (setup->transform == PFFFT_REAL) {
+ int k, dk = N / 32;
+ if (direction == PFFFT_FORWARD) {
+ for (k = 0; k < dk; ++k) {
+ INTERLEAVE2(vin[k * 8 + 0], vin[k * 8 + 1],
+ vout[2 * (0 * dk + k) + 0],
+ vout[2 * (0 * dk + k) + 1]);
+ INTERLEAVE2(vin[k * 8 + 4], vin[k * 8 + 5],
+ vout[2 * (2 * dk + k) + 0],
+ vout[2 * (2 * dk + k) + 1]);
+ }
+ reversed_copy(dk, vin + 2, 8, (v4sf *) (out + N / 2));
+ reversed_copy(dk, vin + 6, 8, (v4sf *) (out + N));
+ } else {
+ for (k = 0; k < dk; ++k) {
+ UNINTERLEAVE2(vin[2 * (0 * dk + k) + 0],
+ vin[2 * (0 * dk + k) + 1],
+ vout[k * 8 + 0], vout[k * 8 + 1]);
+ UNINTERLEAVE2(vin[2 * (2 * dk + k) + 0],
+ vin[2 * (2 * dk + k) + 1],
+ vout[k * 8 + 4], vout[k * 8 + 5]);
+ }
+ unreversed_copy(dk, (v4sf *) (in + N / 4),
+ (v4sf *) (out + N - 6 * SIMD_SZ), -8);
+ unreversed_copy(dk, (v4sf *) (in + 3 * N / 4),
+ (v4sf *) (out + N - 2 * SIMD_SZ), -8);
+ }
+ } else {
+ if (direction == PFFFT_FORWARD) {
+ for (k = 0; k < Ncvec; ++k) {
+ int kk = (k / 4) + (k % 4) * (Ncvec / 4);
+ INTERLEAVE2(vin[k * 2], vin[k * 2 + 1],
+ vout[kk * 2], vout[kk * 2 + 1]);
+ }
+ } else {
+ for (k = 0; k < Ncvec; ++k) {
+ int kk = (k / 4) + (k % 4) * (Ncvec / 4);
+ UNINTERLEAVE2(vin[kk * 2], vin[kk * 2 + 1],
+ vout[k * 2], vout[k * 2 + 1]);
+ }
+ }
+ }
+}
+
+static void pffft_cplx_finalize(int Ncvec, const v4sf * in, v4sf * out, const v4sf * e)
+{
+ int k, dk = Ncvec / SIMD_SZ; // number of 4x4 matrix blocks
+ v4sf r0, i0, r1, i1, r2, i2, r3, i3;
+ v4sf sr0, dr0, sr1, dr1, si0, di0, si1, di1;
+ assert(in != out);
+ for (k = 0; k < dk; ++k) {
+ r0 = in[8 * k + 0];
+ i0 = in[8 * k + 1];
+ r1 = in[8 * k + 2];
+ i1 = in[8 * k + 3];
+ r2 = in[8 * k + 4];
+ i2 = in[8 * k + 5];
+ r3 = in[8 * k + 6];
+ i3 = in[8 * k + 7];
+ VTRANSPOSE4(r0, r1, r2, r3);
+ VTRANSPOSE4(i0, i1, i2, i3);
+ VCPLXMUL(r1, i1, e[k * 6 + 0], e[k * 6 + 1]);
+ VCPLXMUL(r2, i2, e[k * 6 + 2], e[k * 6 + 3]);
+ VCPLXMUL(r3, i3, e[k * 6 + 4], e[k * 6 + 5]);
+
+ sr0 = VADD(r0, r2);
+ dr0 = VSUB(r0, r2);
+ sr1 = VADD(r1, r3);
+ dr1 = VSUB(r1, r3);
+ si0 = VADD(i0, i2);
+ di0 = VSUB(i0, i2);
+ si1 = VADD(i1, i3);
+ di1 = VSUB(i1, i3);
+
+ /*
+ transformation for each column is:
+
+ [1 1 1 1 0 0 0 0] [r0]
+ [1 0 -1 0 0 -1 0 1] [r1]
+ [1 -1 1 -1 0 0 0 0] [r2]
+ [1 0 -1 0 0 1 0 -1] [r3]
+ [0 0 0 0 1 1 1 1] * [i0]
+ [0 1 0 -1 1 0 -1 0] [i1]
+ [0 0 0 0 1 -1 1 -1] [i2]
+ [0 -1 0 1 1 0 -1 0] [i3]
+ */
+
+ r0 = VADD(sr0, sr1);
+ i0 = VADD(si0, si1);
+ r1 = VADD(dr0, di1);
+ i1 = VSUB(di0, dr1);
+ r2 = VSUB(sr0, sr1);
+ i2 = VSUB(si0, si1);
+ r3 = VSUB(dr0, di1);
+ i3 = VADD(di0, dr1);
+
+ *out++ = r0;
+ *out++ = i0;
+ *out++ = r1;
+ *out++ = i1;
+ *out++ = r2;
+ *out++ = i2;
+ *out++ = r3;
+ *out++ = i3;
+ }
+}
+
+static void pffft_cplx_preprocess(int Ncvec, const v4sf * in, v4sf * out,
+ const v4sf * e)
+{
+ int k, dk = Ncvec / SIMD_SZ; // number of 4x4 matrix blocks
+ v4sf r0, i0, r1, i1, r2, i2, r3, i3;
+ v4sf sr0, dr0, sr1, dr1, si0, di0, si1, di1;
+ assert(in != out);
+ for (k = 0; k < dk; ++k) {
+ r0 = in[8 * k + 0];
+ i0 = in[8 * k + 1];
+ r1 = in[8 * k + 2];
+ i1 = in[8 * k + 3];
+ r2 = in[8 * k + 4];
+ i2 = in[8 * k + 5];
+ r3 = in[8 * k + 6];
+ i3 = in[8 * k + 7];
+
+ sr0 = VADD(r0, r2);
+ dr0 = VSUB(r0, r2);
+ sr1 = VADD(r1, r3);
+ dr1 = VSUB(r1, r3);
+ si0 = VADD(i0, i2);
+ di0 = VSUB(i0, i2);
+ si1 = VADD(i1, i3);
+ di1 = VSUB(i1, i3);
+
+ r0 = VADD(sr0, sr1);
+ i0 = VADD(si0, si1);
+ r1 = VSUB(dr0, di1);
+ i1 = VADD(di0, dr1);
+ r2 = VSUB(sr0, sr1);
+ i2 = VSUB(si0, si1);
+ r3 = VADD(dr0, di1);
+ i3 = VSUB(di0, dr1);
+
+ VCPLXMULCONJ(r1, i1, e[k * 6 + 0], e[k * 6 + 1]);
+ VCPLXMULCONJ(r2, i2, e[k * 6 + 2], e[k * 6 + 3]);
+ VCPLXMULCONJ(r3, i3, e[k * 6 + 4], e[k * 6 + 5]);
+
+ VTRANSPOSE4(r0, r1, r2, r3);
+ VTRANSPOSE4(i0, i1, i2, i3);
+
+ *out++ = r0;
+ *out++ = i0;
+ *out++ = r1;
+ *out++ = i1;
+ *out++ = r2;
+ *out++ = i2;
+ *out++ = r3;
+ *out++ = i3;
+ }
+}
+
+static ALWAYS_INLINE(void) pffft_real_finalize_4x4(const v4sf * in0,
+ const v4sf * in1,
+ const v4sf * in,
+ const v4sf * e, v4sf * out)
+{
+ v4sf r0, i0, r1, i1, r2, i2, r3, i3;
+ v4sf sr0, dr0, sr1, dr1, si0, di0, si1, di1;
+ r0 = *in0;
+ i0 = *in1;
+ r1 = *in++;
+ i1 = *in++;
+ r2 = *in++;
+ i2 = *in++;
+ r3 = *in++;
+ i3 = *in++;
+ VTRANSPOSE4(r0, r1, r2, r3);
+ VTRANSPOSE4(i0, i1, i2, i3);
+
+ /*
+ transformation for each column is:
+
+ [1 1 1 1 0 0 0 0] [r0]
+ [1 0 -1 0 0 -1 0 1] [r1]
+ [1 0 -1 0 0 1 0 -1] [r2]
+ [1 -1 1 -1 0 0 0 0] [r3]
+ [0 0 0 0 1 1 1 1] * [i0]
+ [0 -1 0 1 -1 0 1 0] [i1]
+ [0 -1 0 1 1 0 -1 0] [i2]
+ [0 0 0 0 -1 1 -1 1] [i3]
+ */
+
+ //cerr << "matrix initial, before e , REAL:\n 1: " << r0 << "\n 1: " << r1 << "\n 1: " << r2 << "\n 1: " << r3 << "\n";
+ //cerr << "matrix initial, before e, IMAG :\n 1: " << i0 << "\n 1: " << i1 << "\n 1: " << i2 << "\n 1: " << i3 << "\n";
+
+ VCPLXMUL(r1, i1, e[0], e[1]);
+ VCPLXMUL(r2, i2, e[2], e[3]);
+ VCPLXMUL(r3, i3, e[4], e[5]);
+
+ //cerr << "matrix initial, real part:\n 1: " << r0 << "\n 1: " << r1 << "\n 1: " << r2 << "\n 1: " << r3 << "\n";
+ //cerr << "matrix initial, imag part:\n 1: " << i0 << "\n 1: " << i1 << "\n 1: " << i2 << "\n 1: " << i3 << "\n";
+
+ sr0 = VADD(r0, r2);
+ dr0 = VSUB(r0, r2);
+ sr1 = VADD(r1, r3);
+ dr1 = VSUB(r3, r1);
+ si0 = VADD(i0, i2);
+ di0 = VSUB(i0, i2);
+ si1 = VADD(i1, i3);
+ di1 = VSUB(i3, i1);
+
+ r0 = VADD(sr0, sr1);
+ r3 = VSUB(sr0, sr1);
+ i0 = VADD(si0, si1);
+ i3 = VSUB(si1, si0);
+ r1 = VADD(dr0, di1);
+ r2 = VSUB(dr0, di1);
+ i1 = VSUB(dr1, di0);
+ i2 = VADD(dr1, di0);
+
+ *out++ = r0;
+ *out++ = i0;
+ *out++ = r1;
+ *out++ = i1;
+ *out++ = r2;
+ *out++ = i2;
+ *out++ = r3;
+ *out++ = i3;
+
+}
+
+static NEVER_INLINE(void) pffft_real_finalize(int Ncvec, const v4sf * in,
+ v4sf * out, const v4sf * e)
+{
+ int k, dk = Ncvec / SIMD_SZ; // number of 4x4 matrix blocks
+ /* fftpack order is f0r f1r f1i f2r f2i ... f(n-1)r f(n-1)i f(n)r */
+
+ v4sf_union cr, ci, *uout = (v4sf_union *) out;
+ v4sf save = in[7], zero = VZERO();
+ float xr0, xi0, xr1, xi1, xr2, xi2, xr3, xi3;
+ static const float s = M_SQRT2 / 2;
+
+ cr.v = in[0];
+ ci.v = in[Ncvec * 2 - 1];
+ assert(in != out);
+ pffft_real_finalize_4x4(&zero, &zero, in + 1, e, out);
+
+ /*
+ [cr0 cr1 cr2 cr3 ci0 ci1 ci2 ci3]
+
+ [Xr(1)] ] [1 1 1 1 0 0 0 0]
+ [Xr(N/4) ] [0 0 0 0 1 s 0 -s]
+ [Xr(N/2) ] [1 0 -1 0 0 0 0 0]
+ [Xr(3N/4)] [0 0 0 0 1 -s 0 s]
+ [Xi(1) ] [1 -1 1 -1 0 0 0 0]
+ [Xi(N/4) ] [0 0 0 0 0 -s -1 -s]
+ [Xi(N/2) ] [0 -1 0 1 0 0 0 0]
+ [Xi(3N/4)] [0 0 0 0 0 -s 1 -s]
+ */
+
+ xr0 = (cr.f[0] + cr.f[2]) + (cr.f[1] + cr.f[3]);
+ uout[0].f[0] = xr0;
+ xi0 = (cr.f[0] + cr.f[2]) - (cr.f[1] + cr.f[3]);
+ uout[1].f[0] = xi0;
+ xr2 = (cr.f[0] - cr.f[2]);
+ uout[4].f[0] = xr2;
+ xi2 = (cr.f[3] - cr.f[1]);
+ uout[5].f[0] = xi2;
+ xr1 = ci.f[0] + s * (ci.f[1] - ci.f[3]);
+ uout[2].f[0] = xr1;
+ xi1 = -ci.f[2] - s * (ci.f[1] + ci.f[3]);
+ uout[3].f[0] = xi1;
+ xr3 = ci.f[0] - s * (ci.f[1] - ci.f[3]);
+ uout[6].f[0] = xr3;
+ xi3 = ci.f[2] - s * (ci.f[1] + ci.f[3]);
+ uout[7].f[0] = xi3;
+
+ for (k = 1; k < dk; ++k) {
+ v4sf save_next = in[8 * k + 7];
+ pffft_real_finalize_4x4(&save, &in[8 * k + 0], in + 8 * k + 1,
+ e + k * 6, out + k * 8);
+ save = save_next;
+ }
+
+}
+
+static ALWAYS_INLINE(void) pffft_real_preprocess_4x4(const v4sf * in,
+ const v4sf * e, v4sf * out,
+ int first)
+{
+ v4sf r0 = in[0], i0 = in[1], r1 = in[2], i1 = in[3], r2 = in[4], i2 =
+ in[5], r3 = in[6], i3 = in[7];
+ /*
+ transformation for each column is:
+
+ [1 1 1 1 0 0 0 0] [r0]
+ [1 0 0 -1 0 -1 -1 0] [r1]
+ [1 -1 -1 1 0 0 0 0] [r2]
+ [1 0 0 -1 0 1 1 0] [r3]
+ [0 0 0 0 1 -1 1 -1] * [i0]
+ [0 -1 1 0 1 0 0 1] [i1]
+ [0 0 0 0 1 1 -1 -1] [i2]
+ [0 1 -1 0 1 0 0 1] [i3]
+ */
+
+ v4sf sr0 = VADD(r0, r3), dr0 = VSUB(r0, r3);
+ v4sf sr1 = VADD(r1, r2), dr1 = VSUB(r1, r2);
+ v4sf si0 = VADD(i0, i3), di0 = VSUB(i0, i3);
+ v4sf si1 = VADD(i1, i2), di1 = VSUB(i1, i2);
+
+ r0 = VADD(sr0, sr1);
+ r2 = VSUB(sr0, sr1);
+ r1 = VSUB(dr0, si1);
+ r3 = VADD(dr0, si1);
+ i0 = VSUB(di0, di1);
+ i2 = VADD(di0, di1);
+ i1 = VSUB(si0, dr1);
+ i3 = VADD(si0, dr1);
+
+ VCPLXMULCONJ(r1, i1, e[0], e[1]);
+ VCPLXMULCONJ(r2, i2, e[2], e[3]);
+ VCPLXMULCONJ(r3, i3, e[4], e[5]);
+
+ VTRANSPOSE4(r0, r1, r2, r3);
+ VTRANSPOSE4(i0, i1, i2, i3);
+
+ if (!first) {
+ *out++ = r0;
+ *out++ = i0;
+ }
+ *out++ = r1;
+ *out++ = i1;
+ *out++ = r2;
+ *out++ = i2;
+ *out++ = r3;
+ *out++ = i3;
+}
+
+static NEVER_INLINE(void) pffft_real_preprocess(int Ncvec, const v4sf * in,
+ v4sf * out, const v4sf * e)
+{
+ int k, dk = Ncvec / SIMD_SZ; // number of 4x4 matrix blocks
+ /* fftpack order is f0r f1r f1i f2r f2i ... f(n-1)r f(n-1)i f(n)r */
+
+ v4sf_union Xr, Xi, *uout = (v4sf_union *) out;
+ float cr0, ci0, cr1, ci1, cr2, ci2, cr3, ci3;
+ static const float s = M_SQRT2;
+ assert(in != out);
+ for (k = 0; k < 4; ++k) {
+ Xr.f[k] = ((float *)in)[8 * k];
+ Xi.f[k] = ((float *)in)[8 * k + 4];
+ }
+
+ pffft_real_preprocess_4x4(in, e, out + 1, 1); // will write only 6 values
+
+ /*
+ [Xr0 Xr1 Xr2 Xr3 Xi0 Xi1 Xi2 Xi3]
+
+ [cr0] [1 0 2 0 1 0 0 0]
+ [cr1] [1 0 0 0 -1 0 -2 0]
+ [cr2] [1 0 -2 0 1 0 0 0]
+ [cr3] [1 0 0 0 -1 0 2 0]
+ [ci0] [0 2 0 2 0 0 0 0]
+ [ci1] [0 s 0 -s 0 -s 0 -s]
+ [ci2] [0 0 0 0 0 -2 0 2]
+ [ci3] [0 -s 0 s 0 -s 0 -s]
+ */
+ for (k = 1; k < dk; ++k) {
+ pffft_real_preprocess_4x4(in + 8 * k, e + k * 6,
+ out - 1 + k * 8, 0);
+ }
+
+ cr0 = (Xr.f[0] + Xi.f[0]) + 2 * Xr.f[2];
+ uout[0].f[0] = cr0;
+ cr1 = (Xr.f[0] - Xi.f[0]) - 2 * Xi.f[2];
+ uout[0].f[1] = cr1;
+ cr2 = (Xr.f[0] + Xi.f[0]) - 2 * Xr.f[2];
+ uout[0].f[2] = cr2;
+ cr3 = (Xr.f[0] - Xi.f[0]) + 2 * Xi.f[2];
+ uout[0].f[3] = cr3;
+ ci0 = 2 * (Xr.f[1] + Xr.f[3]);
+ uout[2 * Ncvec - 1].f[0] = ci0;
+ ci1 = s * (Xr.f[1] - Xr.f[3]) - s * (Xi.f[1] + Xi.f[3]);
+ uout[2 * Ncvec - 1].f[1] = ci1;
+ ci2 = 2 * (Xi.f[3] - Xi.f[1]);
+ uout[2 * Ncvec - 1].f[2] = ci2;
+ ci3 = -s * (Xr.f[1] - Xr.f[3]) - s * (Xi.f[1] + Xi.f[3]);
+ uout[2 * Ncvec - 1].f[3] = ci3;
+}
+
+static void transform_simd(PFFFT_Setup * setup, const float *finput,
+ float *foutput, float * scratch,
+ pffft_direction_t direction, int ordered)
+{
+ int k, Ncvec = setup->Ncvec;
+ int nf_odd = (setup->ifac[1] & 1);
+
+ // temporary buffer is allocated on the stack if the scratch pointer is NULL
+ int stack_allocate = (scratch == 0 ? Ncvec * 2 : 1);
+ VLA_ARRAY_ON_STACK(v4sf, scratch_on_stack, stack_allocate);
+
+ const v4sf *vinput = (const v4sf *)finput;
+ v4sf *voutput = (v4sf *) foutput;
+ v4sf *buff[2] = { voutput, scratch ? (v4sf*)scratch : scratch_on_stack };
+ int ib = (nf_odd ^ ordered ? 1 : 0);
+
+ assert(VALIGNED(finput) && VALIGNED(foutput));
+
+ //assert(finput != foutput);
+ if (direction == PFFFT_FORWARD) {
+ ib = !ib;
+ if (setup->transform == PFFFT_REAL) {
+ ib = (rfftf1_ps(Ncvec * 2, vinput, buff[ib], buff[!ib],
+ setup->twiddle,
+ &setup->ifac[0]) == buff[0] ? 0 : 1);
+ pffft_real_finalize(Ncvec, buff[ib], buff[!ib],
+ (v4sf *) setup->e);
+ } else {
+ v4sf *tmp = buff[ib];
+ for (k = 0; k < Ncvec; ++k) {
+ UNINTERLEAVE2(vinput[k * 2], vinput[k * 2 + 1],
+ tmp[k * 2], tmp[k * 2 + 1]);
+ }
+ ib = (cfftf1_ps(Ncvec, buff[ib], buff[!ib], buff[ib],
+ setup->twiddle, &setup->ifac[0],
+ -1) == buff[0] ? 0 : 1);
+ pffft_cplx_finalize(Ncvec, buff[ib], buff[!ib],
+ (v4sf *) setup->e);
+ }
+ if (ordered) {
+ pffft_zreorder(setup, (float *)buff[!ib],
+ (float *)buff[ib], PFFFT_FORWARD);
+ } else
+ ib = !ib;
+ } else {
+ if (vinput == buff[ib]) {
+ ib = !ib; // may happen when finput == foutput
+ }
+ if (ordered) {
+ pffft_zreorder(setup, (float *)vinput,
+ (float *)buff[ib], PFFFT_BACKWARD);
+ vinput = buff[ib];
+ ib = !ib;
+ }
+ if (setup->transform == PFFFT_REAL) {
+ pffft_real_preprocess(Ncvec, vinput, buff[ib],
+ (v4sf *) setup->e);
+ ib = (rfftb1_ps
+ (Ncvec * 2, buff[ib], buff[0], buff[1],
+ setup->twiddle,
+ &setup->ifac[0]) == buff[0] ? 0 : 1);
+ } else {
+ pffft_cplx_preprocess(Ncvec, vinput, buff[ib],
+ (v4sf *) setup->e);
+ ib = (cfftf1_ps
+ (Ncvec, buff[ib], buff[0], buff[1],
+ setup->twiddle, &setup->ifac[0],
+ +1) == buff[0] ? 0 : 1);
+ for (k = 0; k < Ncvec; ++k) {
+ INTERLEAVE2(buff[ib][k * 2],
+ buff[ib][k * 2 + 1],
+ buff[ib][k * 2],
+ buff[ib][k * 2 + 1]);
+ }
+ }
+ }
+
+ if (buff[ib] != voutput) {
+ /* extra copy required -- this situation should only happen when finput == foutput */
+ assert(finput == foutput);
+ for (k = 0; k < Ncvec; ++k) {
+ v4sf a = buff[ib][2 * k], b = buff[ib][2 * k + 1];
+ voutput[2 * k] = a;
+ voutput[2 * k + 1] = b;
+ }
+ ib = !ib;
+ }
+ assert(buff[ib] == voutput);
+}
+
+static void zconvolve_accumulate_simd(PFFFT_Setup * s, const float *a, const float *b,
+ const float *c, float *ab, float scaling)
+{
+ const int Ncvec2 = s->Ncvec * 2;
+ const v4sf *RESTRICT va = (const v4sf *)a;
+ const v4sf *RESTRICT vb = (const v4sf *)b;
+ v4sf *RESTRICT vab = (v4sf *) ab;
+ v4sf *RESTRICT vc = (v4sf *) c;
+ v4sf vscal = LD_PS1(scaling);
+ float ar, ai, br, bi, cr, ci;
+ int i;
+
+#ifdef __arm__
+ __builtin_prefetch(va);
+ __builtin_prefetch(vb);
+ __builtin_prefetch(c);
+ __builtin_prefetch(va + 2);
+ __builtin_prefetch(vb + 2);
+ __builtin_prefetch(c + 2);
+ __builtin_prefetch(va + 4);
+ __builtin_prefetch(vb + 4);
+ __builtin_prefetch(c + 4);
+ __builtin_prefetch(va + 6);
+ __builtin_prefetch(vb + 6);
+ __builtin_prefetch(c + 6);
+#endif
+
+ assert(VALIGNED(a) && VALIGNED(b) && VALIGNED(ab));
+ ar = ((v4sf_union *) va)[0].f[0];
+ ai = ((v4sf_union *) va)[1].f[0];
+ br = ((v4sf_union *) vb)[0].f[0];
+ bi = ((v4sf_union *) vb)[1].f[0];
+ cr = ((v4sf_union *) vc)[0].f[0];
+ ci = ((v4sf_union *) vc)[1].f[0];
+
+ for (i = 0; i < Ncvec2; i += 4) {
+ v4sf ar, ai, br, bi;
+ ar = va[i + 0];
+ ai = va[i + 1];
+ br = vb[i + 0];
+ bi = vb[i + 1];
+ VCPLXMUL(ar, ai, br, bi);
+ vab[i + 0] = VMADD(ar, vscal, vc[i + 0]);
+ vab[i + 1] = VMADD(ai, vscal, vc[i + 1]);
+ ar = va[i + 2];
+ ai = va[i + 3];
+ br = vb[i + 2];
+ bi = vb[i + 3];
+ VCPLXMUL(ar, ai, br, bi);
+ vab[i + 2] = VMADD(ar, vscal, vc[i + 2]);
+ vab[i + 3] = VMADD(ai, vscal, vc[i + 3]);
+ }
+ if (s->transform == PFFFT_REAL) {
+ ((v4sf_union *) vab)[0].f[0] = cr + ar * br * scaling;
+ ((v4sf_union *) vab)[1].f[0] = ci + ai * bi * scaling;
+ }
+}
+
+static void zconvolve_simd(PFFFT_Setup * s, const float *a, const float *b,
+ float *ab, float scaling)
+{
+ v4sf vscal = LD_PS1(scaling);
+ const v4sf * RESTRICT va = (const v4sf*)a;
+ const v4sf * RESTRICT vb = (const v4sf*)b;
+ v4sf * RESTRICT vab = (v4sf*)ab;
+ float sar, sai, sbr, sbi;
+ const int Ncvec2 = s->Ncvec * 2;
+ int i;
+
+#ifdef __arm__
+ __builtin_prefetch(va);
+ __builtin_prefetch(vb);
+ __builtin_prefetch(vab);
+ __builtin_prefetch(va+2);
+ __builtin_prefetch(vb+2);
+ __builtin_prefetch(vab+2);
+ __builtin_prefetch(va+4);
+ __builtin_prefetch(vb+4);
+ __builtin_prefetch(vab+4);
+ __builtin_prefetch(va+6);
+ __builtin_prefetch(vb+6);
+ __builtin_prefetch(vab+6);
+#endif
+
+ assert(VALIGNED(a) && VALIGNED(b) && VALIGNED(ab));
+ sar = ((v4sf_union*)va)[0].f[0];
+ sai = ((v4sf_union*)va)[1].f[0];
+ sbr = ((v4sf_union*)vb)[0].f[0];
+ sbi = ((v4sf_union*)vb)[1].f[0];
+
+ /* default routine, works fine for non-arm cpus with current compilers */
+ for (i = 0; i < Ncvec2; i += 4) {
+ v4sf var, vai, vbr, vbi;
+ var = va[i + 0];
+ vai = va[i + 1];
+ vbr = vb[i + 0];
+ vbi = vb[i + 1];
+ VCPLXMUL(var, vai, vbr, vbi);
+ vab[i + 0] = VMUL(var, vscal);
+ vab[i + 1] = VMUL(vai, vscal);
+ var = va[i + 2];
+ vai = va[i + 3];
+ vbr = vb[i + 2];
+ vbi = vb[i + 3];
+ VCPLXMUL(var, vai, vbr, vbi);
+ vab[i + 2] = VMUL(var, vscal);
+ vab[i + 3] = VMUL(vai, vscal);
+ }
+
+ if (s->transform == PFFFT_REAL) {
+ ((v4sf_union*)vab)[0].f[0] = sar * sbr * scaling;
+ ((v4sf_union*)vab)[1].f[0] = sai * sbi * scaling;
+ }
+}
+
+#else // defined(PFFFT_SIMD_DISABLE)
+
+// standard routine using scalar floats, without SIMD stuff.
+
+static void zreorder_simd(PFFFT_Setup * setup, const float *in, float *out,
+ pffft_direction_t direction)
+{
+ int k, N = setup->N;
+ if (setup->transform == PFFFT_COMPLEX) {
+ for (k = 0; k < 2 * N; ++k)
+ out[k] = in[k];
+ return;
+ } else if (direction == PFFFT_FORWARD) {
+ float x_N = in[N - 1];
+ for (k = N - 1; k > 1; --k)
+ out[k] = in[k - 1];
+ out[0] = in[0];
+ out[1] = x_N;
+ } else {
+ float x_N = in[1];
+ for (k = 1; k < N - 1; ++k)
+ out[k] = in[k + 1];
+ out[0] = in[0];
+ out[N - 1] = x_N;
+ }
+}
+
+static void transform_simd(PFFFT_Setup * setup, const float *input,
+ float *output, float *scratch,
+ pffft_direction_t direction, int ordered)
+{
+ int Ncvec = setup->Ncvec;
+ int nf_odd = (setup->ifac[1] & 1);
+
+ // temporary buffer is allocated on the stack if the scratch pointer is NULL
+ int stack_allocate = (scratch == 0 ? Ncvec * 2 : 1);
+ VLA_ARRAY_ON_STACK(v4sf, scratch_on_stack, stack_allocate);
+ float *buff[2];
+ int ib;
+ if (scratch == 0)
+ scratch = scratch_on_stack;
+ buff[0] = output;
+ buff[1] = scratch;
+
+ if (setup->transform == PFFFT_COMPLEX)
+ ordered = 0; // it is always ordered.
+ ib = (nf_odd ^ ordered ? 1 : 0);
+
+ if (direction == PFFFT_FORWARD) {
+ if (setup->transform == PFFFT_REAL) {
+ ib = (rfftf1_ps(Ncvec * 2, input, buff[ib], buff[!ib],
+ setup->twiddle,
+ &setup->ifac[0]) == buff[0] ? 0 : 1);
+ } else {
+ ib = (cfftf1_ps(Ncvec, input, buff[ib], buff[!ib],
+ setup->twiddle, &setup->ifac[0],
+ -1) == buff[0] ? 0 : 1);
+ }
+ if (ordered) {
+ pffft_zreorder(setup, buff[ib], buff[!ib],
+ PFFFT_FORWARD);
+ ib = !ib;
+ }
+ } else {
+ if (input == buff[ib]) {
+ ib = !ib; // may happen when finput == foutput
+ }
+ if (ordered) {
+ pffft_zreorder(setup, input, buff[!ib], PFFFT_BACKWARD);
+ input = buff[!ib];
+ }
+ if (setup->transform == PFFFT_REAL) {
+ ib = (rfftb1_ps(Ncvec * 2, input, buff[ib], buff[!ib],
+ setup->twiddle,
+ &setup->ifac[0]) == buff[0] ? 0 : 1);
+ } else {
+ ib = (cfftf1_ps(Ncvec, input, buff[ib], buff[!ib],
+ setup->twiddle, &setup->ifac[0],
+ +1) == buff[0] ? 0 : 1);
+ }
+ }
+ if (buff[ib] != output) {
+ int k;
+ // extra copy required -- this situation should happens only when finput == foutput
+ assert(input == output);
+ for (k = 0; k < Ncvec; ++k) {
+ float a = buff[ib][2 * k], b = buff[ib][2 * k + 1];
+ output[2 * k] = a;
+ output[2 * k + 1] = b;
+ }
+ ib = !ib;
+ }
+ assert(buff[ib] == output);
+}
+
+
+static void zconvolve_accumulate_simd(PFFFT_Setup * s, const float *a,
+ const float *b, const float *c, float *ab, float scaling)
+{
+ int i, Ncvec2 = s->Ncvec * 2;
+
+ if (s->transform == PFFFT_REAL) {
+ // take care of the fftpack ordering
+ ab[0] = c[0] + a[0] * b[0] * scaling;
+ ab[Ncvec2 - 1] = c[Ncvec2 - 1] + a[Ncvec2 - 1] * b[Ncvec2 - 1] * scaling;
+ ++ab;
+ ++c;
+ ++a;
+ ++b;
+ Ncvec2 -= 2;
+ }
+ for (i = 0; i < Ncvec2; i += 2) {
+ float ar, ai, br, bi;
+ ar = a[i + 0];
+ ai = a[i + 1];
+ br = b[i + 0];
+ bi = b[i + 1];
+ VCPLXMUL(ar, ai, br, bi);
+ ab[i + 0] = c[i + 0] + ar * scaling;
+ ab[i + 1] = c[i + 1] + ai * scaling;
+ }
+}
+
+static void zconvolve_simd(PFFFT_Setup * s, const float *a,
+ const float *b, float *ab, float scaling)
+{
+ int i, Ncvec2 = s->Ncvec * 2;
+
+ if (s->transform == PFFFT_REAL) {
+ // take care of the fftpack ordering
+ ab[0] = a[0] * b[0] * scaling;
+ ab[Ncvec2 - 1] =
+ a[Ncvec2 - 1] * b[Ncvec2 - 1] * scaling;
+ ++ab;
+ ++a;
+ ++b;
+ Ncvec2 -= 2;
+ }
+ for (i = 0; i < Ncvec2; i += 2) {
+ float ar, ai, br, bi;
+ ar = a[i + 0];
+ ai = a[i + 1];
+ br = b[i + 0];
+ bi = b[i + 1];
+ VCPLXMUL(ar, ai, br, bi);
+ ab[i + 0] = ar * scaling;
+ ab[i + 1] = ai * scaling;
+ }
+}
+#endif // defined(PFFFT_SIMD_DISABLE)
+
+static int simd_size_simd(void)
+{
+ return SIMD_SZ;
+}
+
+struct funcs pffft_funcs = {
+ .new_setup = new_setup_simd,
+ .transform = transform_simd,
+ .zreorder = zreorder_simd,
+ .zconvolve_accumulate = zconvolve_accumulate_simd,
+ .zconvolve = zconvolve_simd,
+ .simd_size = simd_size_simd,
+ .validate = validate_pffft_simd,
+};
+
+#if defined(PFFFT_SIMD_DISABLE)
+
+extern struct funcs pffft_funcs_c;
+#if (defined(HAVE_SSE))
+extern struct funcs pffft_funcs_sse;
+#endif
+#if (defined(HAVE_ALTIVEC))
+extern struct funcs pffft_funcs_altivec;
+#endif
+#if (defined(HAVE_NEON))
+extern struct funcs pffft_funcs_neon;
+#endif
+
+static struct funcs *funcs = &pffft_funcs_c;
+
+/* SSE and co like 16-bytes aligned pointers */
+#define MALLOC_V4SF_ALIGNMENT 64 // with a 64-byte alignment, we are even aligned on L2 cache lines...
+void *pffft_aligned_malloc(size_t nb_bytes)
+{
+ void *p, *p0 = malloc(nb_bytes + MALLOC_V4SF_ALIGNMENT);
+ if (!p0)
+ return (void *)0;
+ p = (void *)(((size_t)p0 + MALLOC_V4SF_ALIGNMENT) &
+ (~((size_t)(MALLOC_V4SF_ALIGNMENT - 1))));
+ *((void **)p - 1) = p0;
+ return p;
+}
+
+void pffft_aligned_free(void *p)
+{
+ if (p)
+ free(*((void **)p - 1));
+}
+
+int pffft_simd_size(void)
+{
+ return funcs->simd_size();
+}
+
+PFFFT_Setup *pffft_new_setup(int N, pffft_transform_t transform)
+{
+ return funcs->new_setup(N, transform);
+}
+
+void pffft_destroy_setup(PFFFT_Setup * s)
+{
+ pffft_aligned_free(s->data);
+ free(s);
+}
+
+void pffft_transform(PFFFT_Setup *setup, const float *input, float *output, float *work, pffft_direction_t direction)
+{
+ return funcs->transform(setup, input, output, work, direction, 0);
+}
+
+void pffft_transform_ordered(PFFFT_Setup *setup, const float *input, float *output, float *work, pffft_direction_t direction)
+{
+ return funcs->transform(setup, input, output, work, direction, 1);
+}
+
+void pffft_zreorder(PFFFT_Setup *setup, const float *input, float *output, pffft_direction_t direction)
+{
+ return funcs->zreorder(setup, input, output, direction);
+}
+
+void pffft_zconvolve_accumulate(PFFFT_Setup *setup, const float *dft_a, const float *dft_b, const float *c, float *dft_ab, float scaling)
+{
+ return funcs->zconvolve_accumulate(setup, dft_a, dft_b, c, dft_ab, scaling);
+}
+
+void pffft_zconvolve(PFFFT_Setup *setup, const float *dft_a, const float *dft_b, float *dft_ab, float scaling)
+{
+ return funcs->zconvolve(setup, dft_a, dft_b, dft_ab, scaling);
+}
+
+void pffft_select_cpu(int flags)
+{
+ funcs = &pffft_funcs_c;
+#if defined(HAVE_SSE)
+ if (flags & SPA_CPU_FLAG_SSE)
+ funcs = &pffft_funcs_sse;
+#endif
+#if defined(HAVE_NEON)
+ if (flags & SPA_CPU_FLAG_NEON)
+ funcs = &pffft_funcs_neon;
+#endif
+#if defined(HAVE_ALTIVEC)
+ if (flags & SPA_CPU_FLAG_ALTIVEC)
+ funcs = &pffft_funcs_altivec;
+#endif
+}
+
+#endif
diff --git a/src/modules/module-filter-chain/pffft.h b/src/modules/module-filter-chain/pffft.h
new file mode 100644
index 0000000..ac8e271
--- /dev/null
+++ b/src/modules/module-filter-chain/pffft.h
@@ -0,0 +1,181 @@
+/* Copyright (c) 2013 Julien Pommier ( pommier@modartt.com )
+
+ Based on original fortran 77 code from FFTPACKv4 from NETLIB,
+ authored by Dr Paul Swarztrauber of NCAR, in 1985.
+
+ As confirmed by the NCAR fftpack software curators, the following
+ FFTPACKv5 license applies to FFTPACKv4 sources. My changes are
+ released under the same terms.
+
+ FFTPACK license:
+
+ http://www.cisl.ucar.edu/css/software/fftpack5/ftpk.html
+
+ Copyright (c) 2004 the University Corporation for Atmospheric
+ Research ("UCAR"). All rights reserved. Developed by NCAR's
+ Computational and Information Systems Laboratory, UCAR,
+ www.cisl.ucar.edu.
+
+ Redistribution and use of the Software in source and binary forms,
+ with or without modification, is permitted provided that the
+ following conditions are met:
+
+ - Neither the names of NCAR's Computational and Information Systems
+ Laboratory, the University Corporation for Atmospheric Research,
+ nor the names of its sponsors or contributors may be used to
+ endorse or promote products derived from this Software without
+ specific prior written permission.
+
+ - Redistributions of source code must retain the above copyright
+ notices, this list of conditions, and the disclaimer below.
+
+ - Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions, and the disclaimer below in the
+ documentation and/or other materials provided with the
+ distribution.
+
+ THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING, BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE CONTRIBUTORS OR COPYRIGHT
+ HOLDERS BE LIABLE FOR ANY CLAIM, INDIRECT, INCIDENTAL, SPECIAL,
+ EXEMPLARY, OR CONSEQUENTIAL DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE
+ SOFTWARE.
+*/
+
+/*
+ PFFFT : a Pretty Fast FFT.
+
+ This is basically an adaptation of the single precision fftpack
+ (v4) as found on netlib taking advantage of SIMD instruction found
+ on cpus such as intel x86 (SSE1), powerpc (Altivec), and arm (NEON).
+
+ For architectures where no SIMD instruction is available, the code
+ falls back to a scalar version.
+
+ Restrictions:
+
+ - 1D transforms only, with 32-bit single precision.
+
+ - supports only transforms for inputs of length N of the form
+ N=(2^a)*(3^b)*(5^c), a >= 5, b >=0, c >= 0 (32, 48, 64, 96, 128,
+ 144, 160, etc are all acceptable lengths). Performance is best for
+ 128<=N<=8192.
+
+ - all (float*) pointers in the functions below are expected to
+ have an "simd-compatible" alignment, that is 16 bytes on x86 and
+ powerpc CPUs.
+
+ You can allocate such buffers with the functions
+ pffft_aligned_malloc / pffft_aligned_free (or with stuff like
+ posix_memalign..)
+
+*/
+
+#ifndef PFFFT_H
+#define PFFFT_H
+
+#include <stddef.h> // for size_t
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+ /* opaque struct holding internal stuff (precomputed twiddle factors)
+ this struct can be shared by many threads as it contains only
+ read-only data.
+ */
+ typedef struct PFFFT_Setup PFFFT_Setup;
+
+ /* direction of the transform */
+ typedef enum { PFFFT_FORWARD, PFFFT_BACKWARD } pffft_direction_t;
+
+ /* type of transform */
+ typedef enum { PFFFT_REAL, PFFFT_COMPLEX } pffft_transform_t;
+
+ /*
+ prepare for performing transforms of size N -- the returned
+ PFFFT_Setup structure is read-only so it can safely be shared by
+ multiple concurrent threads.
+ */
+ PFFFT_Setup *pffft_new_setup(int N, pffft_transform_t transform);
+ void pffft_destroy_setup(PFFFT_Setup *);
+ /*
+ Perform a Fourier transform , The z-domain data is stored in the
+ most efficient order for transforming it back, or using it for
+ convolution. If you need to have its content sorted in the
+ "usual" way, that is as an array of interleaved complex numbers,
+ either use pffft_transform_ordered , or call pffft_zreorder after
+ the forward fft, and before the backward fft.
+
+ Transforms are not scaled: PFFFT_BACKWARD(PFFFT_FORWARD(x)) = N*x.
+ Typically you will want to scale the backward transform by 1/N.
+
+ The 'work' pointer should point to an area of N (2*N for complex
+ fft) floats, properly aligned. If 'work' is NULL, then stack will
+ be used instead (this is probably the best strategy for small
+ FFTs, say for N < 16384).
+
+ input and output may alias.
+ */
+ void pffft_transform(PFFFT_Setup *setup, const float *input, float *output, float *work, pffft_direction_t direction);
+
+ /*
+ Similar to pffft_transform, but makes sure that the output is
+ ordered as expected (interleaved complex numbers). This is
+ similar to calling pffft_transform and then pffft_zreorder.
+
+ input and output may alias.
+ */
+ void pffft_transform_ordered(PFFFT_Setup *setup, const float *input, float *output, float *work, pffft_direction_t direction);
+
+ /*
+ call pffft_zreorder(.., PFFFT_FORWARD) after pffft_transform(...,
+ PFFFT_FORWARD) if you want to have the frequency components in
+ the correct "canonical" order, as interleaved complex numbers.
+
+ (for real transforms, both 0-frequency and half frequency
+ components, which are real, are assembled in the first entry as
+ F(0)+i*F(n/2+1). Note that the original fftpack did place
+ F(n/2+1) at the end of the arrays).
+
+ input and output should not alias.
+ */
+ void pffft_zreorder(PFFFT_Setup *setup, const float *input, float *output, pffft_direction_t direction);
+
+ /*
+ Perform a multiplication of the frequency components of dft_a and
+ dft_b and accumulate them into dft_ab. The arrays should have
+ been obtained with pffft_transform(.., PFFFT_FORWARD) and should
+ *not* have been reordered with pffft_zreorder (otherwise just
+ perform the operation yourself as the dft coefs are stored as
+ interleaved complex numbers).
+
+ the operation performed is: dft_ab = dft_c + (dft_a * fdt_b)*scaling
+
+ The dft_a, dft_b and dft_ab pointers may alias.
+ */
+ void pffft_zconvolve_accumulate(PFFFT_Setup *setup, const float *dft_a, const float *dft_b, const float *dft_c, float *dft_ab, float scaling);
+
+ void pffft_zconvolve(PFFFT_Setup *setup, const float *dft_a, const float *dft_b, float *dft_ab, float scaling);
+
+ /*
+ the float buffers must have the correct alignment (16-byte boundary
+ on intel and powerpc). This function may be used to obtain such
+ correctly aligned buffers.
+ */
+ void *pffft_aligned_malloc(size_t nb_bytes);
+ void pffft_aligned_free(void *);
+
+ /* return 4 or 1 depending on whether support for SSE/Altivec instructions was enabled when building pffft.c */
+ int pffft_simd_size(void);
+
+ void pffft_select_cpu(int flags);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif // PFFFT_H
diff --git a/src/modules/module-filter-chain/plugin.h b/src/modules/module-filter-chain/plugin.h
new file mode 100644
index 0000000..72946e4
--- /dev/null
+++ b/src/modules/module-filter-chain/plugin.h
@@ -0,0 +1,112 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PLUGIN_H
+#define PLUGIN_H
+
+#include <stdint.h>
+#include <stddef.h>
+#include <errno.h>
+#include <stdio.h>
+#include <limits.h>
+
+#include <spa/support/plugin.h>
+#include <spa/utils/defs.h>
+#include <spa/utils/list.h>
+#include <spa/utils/string.h>
+
+#include "dsp-ops.h"
+
+struct fc_plugin {
+ const struct fc_descriptor *(*make_desc)(struct fc_plugin *plugin, const char *name);
+ void (*unload) (struct fc_plugin *plugin);
+};
+
+struct fc_port {
+ uint32_t index;
+ const char *name;
+#define FC_PORT_INPUT (1ULL << 0)
+#define FC_PORT_OUTPUT (1ULL << 1)
+#define FC_PORT_CONTROL (1ULL << 2)
+#define FC_PORT_AUDIO (1ULL << 3)
+ uint64_t flags;
+
+#define FC_HINT_BOOLEAN (1ULL << 2)
+#define FC_HINT_SAMPLE_RATE (1ULL << 3)
+#define FC_HINT_INTEGER (1ULL << 5)
+ uint64_t hint;
+ float def;
+ float min;
+ float max;
+};
+
+#define FC_IS_PORT_INPUT(x) ((x) & FC_PORT_INPUT)
+#define FC_IS_PORT_OUTPUT(x) ((x) & FC_PORT_OUTPUT)
+#define FC_IS_PORT_CONTROL(x) ((x) & FC_PORT_CONTROL)
+#define FC_IS_PORT_AUDIO(x) ((x) & FC_PORT_AUDIO)
+
+struct fc_descriptor {
+ const char *name;
+#define FC_DESCRIPTOR_SUPPORTS_NULL_DATA (1ULL << 0)
+#define FC_DESCRIPTOR_COPY (1ULL << 1)
+ uint64_t flags;
+
+ void (*free) (const struct fc_descriptor *desc);
+
+ uint32_t n_ports;
+ struct fc_port *ports;
+
+ void *(*instantiate) (const struct fc_descriptor *desc,
+ unsigned long SampleRate, int index, const char *config);
+
+ void (*cleanup) (void *instance);
+
+ void (*connect_port) (void *instance, unsigned long port, float *data);
+
+ void (*activate) (void *instance);
+ void (*deactivate) (void *instance);
+
+ void (*run) (void *instance, unsigned long SampleCount);
+};
+
+static inline void fc_plugin_free(struct fc_plugin *plugin)
+{
+ if (plugin->unload)
+ plugin->unload(plugin);
+}
+
+static inline void fc_descriptor_free(const struct fc_descriptor *desc)
+{
+ if (desc->free)
+ desc->free(desc);
+}
+
+struct fc_plugin *load_ladspa_plugin(const struct spa_support *support, uint32_t n_support,
+ struct dsp_ops *dsp, const char *path, const char *config);
+struct fc_plugin *load_lv2_plugin(const struct spa_support *support, uint32_t n_support,
+ struct dsp_ops *dsp, const char *path, const char *config);
+struct fc_plugin *load_builtin_plugin(const struct spa_support *support, uint32_t n_support,
+ struct dsp_ops *dsp, const char *path, const char *config);
+
+#endif /* PLUGIN_H */
diff --git a/src/modules/module-link-factory.c b/src/modules/module-link-factory.c
new file mode 100644
index 0000000..509b211
--- /dev/null
+++ b/src/modules/module-link-factory.c
@@ -0,0 +1,565 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <errno.h>
+#include <dlfcn.h>
+
+#include "config.h"
+
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+
+#include <pipewire/impl.h>
+
+/** \page page_module_link_factory PipeWire Module: Link Factory
+ */
+
+#define NAME "link-factory"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+#define FACTORY_USAGE PW_KEY_LINK_OUTPUT_NODE"=<output-node> " \
+ "["PW_KEY_LINK_OUTPUT_PORT"=<output-port>] " \
+ PW_KEY_LINK_INPUT_NODE"=<input-node> " \
+ "["PW_KEY_LINK_INPUT_PORT"=<input-port>] " \
+ "["PW_KEY_OBJECT_LINGER"=<bool>] " \
+ "["PW_KEY_LINK_PASSIVE"=<bool>]"
+
+static const struct spa_dict_item module_props[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Allow clients to create links" },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+struct factory_data {
+ struct pw_context *context;
+
+ struct pw_impl_module *module;
+ struct spa_hook module_listener;
+
+ struct pw_impl_factory *factory;
+ struct spa_hook factory_listener;
+
+ struct spa_list link_list;
+
+ struct pw_work_queue *work;
+};
+
+struct link_data {
+ struct factory_data *data;
+ struct spa_list l;
+ struct pw_impl_link *link;
+ struct spa_hook link_listener;
+
+ struct pw_resource *resource;
+ struct spa_hook resource_listener;
+
+ struct pw_global *global;
+ struct spa_hook global_listener;
+
+ struct pw_resource *factory_resource;
+ uint32_t new_id;
+ bool linger;
+};
+
+static void resource_destroy(void *data)
+{
+ struct link_data *ld = data;
+ spa_hook_remove(&ld->resource_listener);
+ ld->resource = NULL;
+ if (ld->global)
+ pw_global_destroy(ld->global);
+}
+
+static const struct pw_resource_events resource_events = {
+ PW_VERSION_RESOURCE_EVENTS,
+ .destroy = resource_destroy
+};
+
+static void global_destroy(void *data)
+{
+ struct link_data *ld = data;
+ struct factory_data *d = ld->data;
+ pw_work_queue_cancel(d->work, ld, SPA_ID_INVALID);
+ spa_hook_remove(&ld->global_listener);
+ ld->global = NULL;
+}
+
+static const struct pw_global_events global_events = {
+ PW_VERSION_GLOBAL_EVENTS,
+ .destroy = global_destroy
+};
+
+static void link_destroy(void *data)
+{
+ struct link_data *ld = data;
+ spa_list_remove(&ld->l);
+ spa_hook_remove(&ld->link_listener);
+ if (ld->global)
+ spa_hook_remove(&ld->global_listener);
+ if (ld->resource)
+ spa_hook_remove(&ld->resource_listener);
+}
+
+static void link_initialized(void *data)
+{
+ struct link_data *ld = data;
+ struct pw_impl_client *client;
+ int res;
+
+ if (ld->factory_resource == NULL)
+ return;
+
+ client = pw_resource_get_client(ld->factory_resource);
+ ld->global = pw_impl_link_get_global(ld->link);
+ pw_global_add_listener(ld->global, &ld->global_listener, &global_events, ld);
+
+ res = pw_global_bind(ld->global, client, PW_PERM_ALL, PW_VERSION_LINK, ld->new_id);
+ if (res < 0)
+ goto error_bind;
+
+ if (!ld->linger) {
+ ld->resource = pw_impl_client_find_resource(client, ld->new_id);
+ if (ld->resource == NULL) {
+ res = -ENOENT;
+ goto error_bind;
+ }
+ pw_resource_add_listener(ld->resource, &ld->resource_listener, &resource_events, ld);
+ }
+ return;
+
+error_bind:
+ pw_resource_errorf_id(ld->factory_resource, ld->new_id, res,
+ "can't bind link: %s", spa_strerror(res));
+}
+
+static void destroy_link(void *obj, void *data, int res, uint32_t id)
+{
+ struct link_data *ld = data;
+ if (ld->global)
+ pw_global_destroy(ld->global);
+}
+
+static void link_state_changed(void *data, enum pw_link_state old,
+ enum pw_link_state state, const char *error)
+{
+ struct link_data *ld = data;
+ struct factory_data *d = ld->data;
+
+ switch (state) {
+ case PW_LINK_STATE_ERROR:
+ if (ld->linger)
+ pw_work_queue_add(d->work, ld, 0, destroy_link, ld);
+ break;
+ default:
+ break;
+ }
+}
+
+static const struct pw_impl_link_events link_events = {
+ PW_VERSION_IMPL_LINK_EVENTS,
+ .destroy = link_destroy,
+ .initialized = link_initialized,
+ .state_changed = link_state_changed
+};
+
+static struct pw_impl_port *get_port(struct pw_impl_node *node, enum spa_direction direction)
+{
+ struct pw_impl_port *p;
+ struct pw_context *context = pw_impl_node_get_context(node);
+ int res;
+
+ p = pw_impl_node_find_port(node, direction, PW_ID_ANY);
+
+ if (p == NULL || pw_impl_port_is_linked(p)) {
+ uint32_t port_id;
+
+ port_id = pw_impl_node_get_free_port_id(node, direction);
+ if (port_id == SPA_ID_INVALID)
+ return NULL;
+
+ p = pw_context_create_port(context, direction, port_id, NULL, 0);
+ if (p == NULL)
+ return NULL;
+
+ if ((res = pw_impl_port_add(p, node)) < 0) {
+ pw_log_warn("can't add port: %s", spa_strerror(res));
+ errno = -res;
+ return NULL;
+ }
+ }
+ return p;
+}
+
+struct find_port {
+ uint32_t id;
+ const char *name;
+ enum spa_direction direction;
+ struct pw_impl_node *node;
+ struct pw_impl_port *port;
+};
+
+static int find_port_func(void *data, struct pw_global *global)
+{
+ struct find_port *find = data;
+ const char *str;
+ const struct pw_properties *props;
+
+ if (!pw_global_is_type(global, PW_TYPE_INTERFACE_Port))
+ return 0;
+ if (pw_global_get_id(global) == find->id)
+ goto found;
+
+ props = pw_global_get_properties(global);
+ if ((str = pw_properties_get(props, PW_KEY_OBJECT_PATH)) != NULL &&
+ spa_streq(str, find->name))
+ goto found;
+ return 0;
+found:
+ find->port = pw_global_get_object(global);
+ return 1;
+}
+
+static int find_node_port_func(void *data, struct pw_impl_port *port)
+{
+ struct find_port *find = data;
+ const char *str;
+ const struct pw_properties *props;
+
+ if (pw_impl_port_get_id(port) == find->id)
+ goto found;
+
+ props = pw_impl_port_get_properties(port);
+ if ((str = pw_properties_get(props, PW_KEY_PORT_NAME)) != NULL &&
+ spa_streq(str, find->name))
+ goto found;
+ if ((str = pw_properties_get(props, PW_KEY_PORT_ALIAS)) != NULL &&
+ spa_streq(str, find->name))
+ goto found;
+ if ((str = pw_properties_get(props, PW_KEY_OBJECT_PATH)) != NULL &&
+ spa_streq(str, find->name))
+ goto found;
+ return 0;
+found:
+ find->port = port;
+ return 1;
+}
+
+static struct pw_impl_port *find_port(struct pw_context *context,
+ struct pw_impl_node *node, enum spa_direction direction, const char *name)
+{
+ struct find_port find = {
+ .id = SPA_ID_INVALID,
+ .name = name,
+ .direction = direction,
+ .node = node
+ };
+ spa_atou32(name, &find.id, 0);
+
+ if (find.id != SPA_ID_INVALID) {
+ struct pw_global *global = pw_context_find_global(context, find.id);
+ /* find port by global id */
+ if (global != NULL && pw_global_is_type(global, PW_TYPE_INTERFACE_Port))
+ return pw_global_get_object(global);
+ }
+ if (node != NULL) {
+ /* find port by local id */
+ if (find.id != SPA_ID_INVALID) {
+ find.port = pw_impl_node_find_port(node, find.direction, find.id);
+ if (find.port != NULL)
+ return find.port;
+ }
+ /* find port by local name */
+ if (pw_impl_node_for_each_port(find.node, find.direction,
+ find_node_port_func, &find) == 1)
+ return find.port;
+
+ } else {
+ /* find port by name */
+ if (pw_context_for_each_global(context, find_port_func, &find) == 1)
+ return find.port;
+ }
+ return NULL;
+}
+
+struct find_node {
+ uint32_t id;
+ const char *name;
+ struct pw_impl_node *node;
+};
+
+static int find_node_func(void *data, struct pw_global *global)
+{
+ struct find_node *find = data;
+ const char *str;
+ const struct pw_properties *props;
+
+ if (!pw_global_is_type(global, PW_TYPE_INTERFACE_Node))
+ return 0;
+ if (pw_global_get_id(global) == find->id)
+ goto found;
+
+ props = pw_global_get_properties(global);
+ if ((str = pw_properties_get(props, PW_KEY_NODE_NAME)) != NULL &&
+ spa_streq(str, find->name))
+ goto found;
+ if ((str = pw_properties_get(props, PW_KEY_NODE_NICK)) != NULL &&
+ spa_streq(str, find->name))
+ goto found;
+ if ((str = pw_properties_get(props, PW_KEY_NODE_DESCRIPTION)) != NULL &&
+ spa_streq(str, find->name))
+ goto found;
+ if ((str = pw_properties_get(props, PW_KEY_OBJECT_PATH)) != NULL &&
+ spa_streq(str, find->name))
+ goto found;
+ return 0;
+found:
+ find->node = pw_global_get_object(global);
+ return 1;
+}
+
+static struct pw_impl_node *find_node(struct pw_context *context, const char *name)
+{
+ struct find_node find = {
+ .id = SPA_ID_INVALID,
+ .name = name,
+ };
+ spa_atou32(name, &find.id, 0);
+
+ if (find.id != SPA_ID_INVALID) {
+ struct pw_global *global = pw_context_find_global(context, find.id);
+ if (global != NULL && pw_global_is_type(global, PW_TYPE_INTERFACE_Node))
+ return pw_global_get_object(global);
+ }
+ if (pw_context_for_each_global(context, find_node_func, &find) == 1)
+ return find.node;
+ return NULL;
+}
+
+static void *create_object(void *_data,
+ struct pw_resource *resource,
+ const char *type,
+ uint32_t version,
+ struct pw_properties *properties,
+ uint32_t new_id)
+{
+ struct factory_data *d = _data;
+ struct pw_impl_client *client = NULL;
+ struct pw_impl_node *output_node, *input_node;
+ struct pw_impl_port *outport = NULL, *inport = NULL;
+ struct pw_context *context = d->context;
+ struct pw_impl_link *link;
+ const char *output_node_str, *input_node_str;
+ const char *output_port_str, *input_port_str;
+ struct link_data *ld;
+ int res;
+ bool linger;
+
+ if (properties == NULL)
+ goto error_properties;
+
+ if ((output_node_str = pw_properties_get(properties, PW_KEY_LINK_OUTPUT_NODE)) != NULL)
+ output_node = find_node(context, output_node_str);
+ else
+ output_node = NULL;
+
+ if ((output_port_str = pw_properties_get(properties, PW_KEY_LINK_OUTPUT_PORT)) != NULL)
+ outport = find_port(context, output_node, SPA_DIRECTION_OUTPUT, output_port_str);
+ else if (output_node != NULL)
+ outport = get_port(output_node, SPA_DIRECTION_OUTPUT);
+ if (outport == NULL)
+ goto error_output_port;
+
+ if ((input_node_str = pw_properties_get(properties, PW_KEY_LINK_INPUT_NODE)) != NULL)
+ input_node = find_node(context, input_node_str);
+ else
+ input_node = NULL;
+
+ if ((input_port_str = pw_properties_get(properties, PW_KEY_LINK_INPUT_PORT)) != NULL)
+ inport = find_port(context, input_node, SPA_DIRECTION_INPUT, input_port_str);
+ else if (input_node != NULL)
+ inport = get_port(input_node, SPA_DIRECTION_INPUT);
+ if (inport == NULL)
+ goto error_input_port;
+
+ linger = pw_properties_get_bool(properties, PW_KEY_OBJECT_LINGER, false);
+
+ pw_properties_setf(properties, PW_KEY_FACTORY_ID, "%d",
+ pw_impl_factory_get_info(d->factory)->id);
+
+ client = resource ? pw_resource_get_client(resource) : NULL;
+ if (client && !linger)
+ pw_properties_setf(properties, PW_KEY_CLIENT_ID, "%d",
+ pw_impl_client_get_info(client)->id);
+
+
+ link = pw_context_create_link(context, outport, inport, NULL, properties, sizeof(struct link_data));
+ properties = NULL;
+ if (link == NULL) {
+ res = -errno;
+ goto error_create_link;
+ }
+
+ ld = pw_impl_link_get_user_data(link);
+ ld->data = d;
+ ld->factory_resource = resource;
+ ld->link = link;
+ ld->new_id = new_id;
+ ld->linger = linger;
+ spa_list_append(&d->link_list, &ld->l);
+
+ pw_impl_link_add_listener(link, &ld->link_listener, &link_events, ld);
+ if ((res = pw_impl_link_register(link, NULL)) < 0)
+ goto error_link_register;
+
+ return link;
+
+error_properties:
+ res = -EINVAL;
+ pw_resource_errorf_id(resource, new_id, res, NAME": no properties. usage:"FACTORY_USAGE);
+ goto error_exit;
+error_output_port:
+ res = -EINVAL;
+ pw_resource_errorf_id(resource, new_id, res, NAME": unknown output port %s", output_port_str);
+ goto error_exit;
+error_input_port:
+ res = -EINVAL;
+ pw_resource_errorf_id(resource, new_id, res, NAME": unknown input port %s", input_port_str);
+ goto error_exit;
+error_create_link:
+ pw_resource_errorf_id(resource, new_id, res, NAME": can't link ports %d and %d: %s",
+ pw_impl_port_get_info(outport)->id, pw_impl_port_get_info(inport)->id,
+ spa_strerror(res));
+ goto error_exit;
+error_link_register:
+ pw_resource_errorf_id(resource, new_id, res, NAME": can't register link: %s", spa_strerror(res));
+ goto error_exit;
+error_exit:
+ pw_properties_free(properties);
+ errno = -res;
+ return NULL;
+}
+
+static const struct pw_impl_factory_implementation impl_factory = {
+ PW_VERSION_IMPL_FACTORY_IMPLEMENTATION,
+ .create_object = create_object,
+};
+
+static void factory_destroy(void *data)
+{
+ struct factory_data *d = data;
+ struct link_data *ld, *t;
+
+ spa_hook_remove(&d->factory_listener);
+
+ spa_list_for_each_safe(ld, t, &d->link_list, l)
+ pw_impl_link_destroy(ld->link);
+
+ d->factory = NULL;
+ if (d->module)
+ pw_impl_module_destroy(d->module);
+}
+
+static const struct pw_impl_factory_events factory_events = {
+ PW_VERSION_IMPL_FACTORY_EVENTS,
+ .destroy = factory_destroy,
+};
+
+static void module_destroy(void *data)
+{
+ struct factory_data *d = data;
+ spa_hook_remove(&d->module_listener);
+ d->module = NULL;
+ if (d->factory)
+ pw_impl_factory_destroy(d->factory);
+}
+
+static void module_registered(void *data)
+{
+ struct factory_data *d = data;
+ struct pw_impl_module *module = d->module;
+ struct pw_impl_factory *factory = d->factory;
+ struct spa_dict_item items[1];
+ char id[16];
+ int res;
+
+ snprintf(id, sizeof(id), "%d", pw_global_get_id(pw_impl_module_get_global(module)));
+ items[0] = SPA_DICT_ITEM_INIT(PW_KEY_MODULE_ID, id);
+ pw_impl_factory_update_properties(factory, &SPA_DICT_INIT(items, 1));
+
+ if ((res = pw_impl_factory_register(factory, NULL)) < 0) {
+ pw_log_error("%p: can't register factory: %s", factory, spa_strerror(res));
+ }
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy,
+ .registered = module_registered,
+};
+
+SPA_EXPORT
+int pipewire__module_init(struct pw_impl_module *module, const char *args)
+{
+ struct pw_context *context = pw_impl_module_get_context(module);
+ struct pw_impl_factory *factory;
+ struct factory_data *data;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ factory = pw_context_create_factory(context,
+ "link-factory",
+ PW_TYPE_INTERFACE_Link,
+ PW_VERSION_LINK,
+ pw_properties_new(
+ PW_KEY_FACTORY_USAGE, FACTORY_USAGE,
+ NULL),
+ sizeof(*data));
+ if (factory == NULL)
+ return -errno;
+
+ data = pw_impl_factory_get_user_data(factory);
+ data->factory = factory;
+ data->module = module;
+ data->context = context;
+ data->work = pw_context_get_work_queue(context);
+
+ spa_list_init(&data->link_list);
+
+ pw_log_debug("module %p: new", module);
+
+ pw_impl_factory_set_implementation(factory,
+ &impl_factory,
+ data);
+
+ pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_props));
+
+ pw_impl_factory_add_listener(factory, &data->factory_listener, &factory_events, data);
+ pw_impl_module_add_listener(module, &data->module_listener, &module_events, data);
+
+ return 0;
+}
diff --git a/src/modules/module-loopback.c b/src/modules/module-loopback.c
new file mode 100644
index 0000000..7d66539
--- /dev/null
+++ b/src/modules/module-loopback.c
@@ -0,0 +1,737 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+
+#include "config.h"
+
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+#include <spa/utils/json.h>
+#include <spa/utils/ringbuffer.h>
+#include <spa/param/latency-utils.h>
+#include <spa/debug/types.h>
+
+#include <pipewire/impl.h>
+#include <pipewire/extensions/profiler.h>
+
+/** \page page_module_loopback PipeWire Module: Loopback
+ *
+ * The loopback module passes the output of a capture stream unmodified to a playback stream.
+ * It can be used to construct a link between a source and sink but also to
+ * create new virtual sinks or sources or to remap channel between streams.
+ *
+ * Because both ends of the loopback are built with streams, the session manager can
+ * manage the configuration and connection with the sinks and sources.
+ *
+ * ## Module Options
+ *
+ * - `node.description`: a human readable name for the loopback streams
+ * - `target.delay.sec`: delay in seconds as float (Since 0.3.60)
+ * - `capture.props = {}`: properties to be passed to the input stream
+ * - `playback.props = {}`: properties to be passed to the output stream
+ *
+ * ## General options
+ *
+ * Options with well-known behavior. Most options can be added to the global
+ * configuration or the individual streams:
+ *
+ * - \ref PW_KEY_REMOTE_NAME
+ * - \ref PW_KEY_AUDIO_RATE
+ * - \ref PW_KEY_AUDIO_CHANNELS
+ * - \ref SPA_KEY_AUDIO_POSITION
+ * - \ref PW_KEY_MEDIA_NAME
+ * - \ref PW_KEY_NODE_LATENCY
+ * - \ref PW_KEY_NODE_DESCRIPTION
+ * - \ref PW_KEY_NODE_GROUP
+ * - \ref PW_KEY_NODE_LINK_GROUP
+ * - \ref PW_KEY_NODE_VIRTUAL
+ * - \ref PW_KEY_NODE_NAME: See notes below. If not specified, defaults to
+ * 'loopback-<pid>-<module-id>'.
+ *
+ * Stream only properties:
+ *
+ * - \ref PW_KEY_MEDIA_CLASS
+ * - \ref PW_KEY_NODE_NAME: if not given per stream, the global node.name will be
+ * prefixed with 'input.' and 'output.' to generate a capture and playback
+ * stream node.name respectively.
+ *
+ * ## Example configuration of a virtual sink
+ *
+ * This Virtual sink routes stereo input to the rear channels of a 7.1 sink.
+ *
+ *\code{.unparsed}
+ * context.modules = [
+ * { name = libpipewire-module-loopback
+ * args = {
+ * node.description = "CM106 Stereo Pair 2"
+ * #target.delay.sec = 1.5
+ * capture.props = {
+ * node.name = "CM106_stereo_pair_2"
+ * media.class = "Audio/Sink"
+ * audio.position = [ FL FR ]
+ * }
+ * playback.props = {
+ * node.name = "playback.CM106_stereo_pair_2"
+ * audio.position = [ RL RR ]
+ * target.object = "alsa_output.usb-0d8c_USB_Sound_Device-00.analog-surround-71"
+ * node.dont-reconnect = true
+ * stream.dont-remix = true
+ * node.passive = true
+ * }
+ * }
+ * }
+ * ]
+ *\endcode
+ *
+ * ## See also
+ *
+ * `pw-loopback` : a tool that loads the loopback module with given parameters.
+ */
+
+#define NAME "loopback"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+static const struct spa_dict_item module_props[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Create loopback streams" },
+ { PW_KEY_MODULE_USAGE, " [ remote.name=<remote> ] "
+ "[ node.latency=<latency as fraction> ] "
+ "[ node.description=<description of the nodes> ] "
+ "[ audio.rate=<sample rate> ] "
+ "[ audio.channels=<number of channels> ] "
+ "[ audio.position=<channel map> ] "
+ "[ target.delay.sec=<delay as seconds in float> ] "
+ "[ capture.props=<properties> ] "
+ "[ playback.props=<properties> ] " },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+#include <stdlib.h>
+#include <signal.h>
+#include <getopt.h>
+#include <limits.h>
+#include <math.h>
+
+#include <spa/pod/builder.h>
+#include <spa/param/audio/format-utils.h>
+#include <spa/param/audio/raw.h>
+
+#include <pipewire/pipewire.h>
+
+struct impl {
+ struct pw_context *context;
+
+ struct pw_impl_module *module;
+
+ struct spa_hook module_listener;
+
+ struct pw_core *core;
+ struct spa_hook core_proxy_listener;
+ struct spa_hook core_listener;
+
+ struct pw_properties *capture_props;
+ struct pw_stream *capture;
+ struct spa_hook capture_listener;
+ struct spa_audio_info_raw capture_info;
+ struct spa_latency_info capture_latency;
+
+ struct pw_properties *playback_props;
+ struct pw_stream *playback;
+ struct spa_hook playback_listener;
+ struct spa_audio_info_raw playback_info;
+ struct spa_latency_info playback_latency;
+
+ unsigned int do_disconnect:1;
+ unsigned int recalc_delay:1;
+
+ float target_delay;
+ struct spa_ringbuffer buffer;
+ uint8_t *buffer_data;
+ uint32_t buffer_size;
+};
+
+static void capture_destroy(void *d)
+{
+ struct impl *impl = d;
+ spa_hook_remove(&impl->capture_listener);
+ impl->capture = NULL;
+}
+
+static void recalculate_delay(struct impl *impl)
+{
+ uint32_t target = impl->capture_info.rate * impl->target_delay, cdelay, pdelay;
+ uint32_t delay, w;
+ struct pw_time pwt;
+
+ pw_stream_get_time_n(impl->playback, &pwt, sizeof(pwt));
+ pdelay = pwt.delay;
+ pw_stream_get_time_n(impl->capture, &pwt, sizeof(pwt));
+ cdelay = pwt.delay;
+
+ delay = target - SPA_MIN(target, pdelay + cdelay);
+ delay = SPA_MIN(delay, impl->buffer_size / 4);
+
+ spa_ringbuffer_get_write_index(&impl->buffer, &w);
+ spa_ringbuffer_read_update(&impl->buffer, w - (delay * 4));
+
+ pw_log_info("target:%d c:%d + p:%d + delay:%d = (%d)",
+ target, cdelay, pdelay, delay,
+ cdelay + pdelay + delay);
+}
+
+static void capture_process(void *d)
+{
+ struct impl *impl = d;
+ pw_stream_trigger_process(impl->playback);
+}
+
+static void playback_process(void *d)
+{
+ struct impl *impl = d;
+ struct pw_buffer *in, *out;
+ uint32_t i;
+
+ if (impl->recalc_delay) {
+ recalculate_delay(impl);
+ impl->recalc_delay = false;
+ }
+
+ if ((in = pw_stream_dequeue_buffer(impl->capture)) == NULL)
+ pw_log_debug("out of capture buffers: %m");
+
+ if ((out = pw_stream_dequeue_buffer(impl->playback)) == NULL)
+ pw_log_debug("out of playback buffers: %m");
+
+ if (in != NULL && out != NULL) {
+ uint32_t outsize = UINT32_MAX;
+ int32_t stride = 0;
+ struct spa_data *d;
+ const void *src[in->buffer->n_datas];
+ uint32_t r, w, buffer_size;
+
+ for (i = 0; i < in->buffer->n_datas; i++) {
+ uint32_t offs, size;
+
+ d = &in->buffer->datas[i];
+ offs = SPA_MIN(d->chunk->offset, d->maxsize);
+ size = SPA_MIN(d->chunk->size, d->maxsize - offs);
+
+ src[i] = SPA_PTROFF(d->data, offs, void);
+ outsize = SPA_MIN(outsize, size);
+ stride = SPA_MAX(stride, d->chunk->stride);
+ }
+ if (impl->buffer_size > 0) {
+ buffer_size = impl->buffer_size;
+ spa_ringbuffer_get_write_index(&impl->buffer, &w);
+ for (i = 0; i < in->buffer->n_datas; i++) {
+ void *buffer_data = &impl->buffer_data[i * buffer_size];
+ spa_ringbuffer_write_data(&impl->buffer,
+ buffer_data, buffer_size,
+ w % buffer_size, src[i], outsize);
+ src[i] = buffer_data;
+ }
+ w += outsize;
+ spa_ringbuffer_write_update(&impl->buffer, w);
+ spa_ringbuffer_get_read_index(&impl->buffer, &r);
+ } else {
+ r = 0;
+ buffer_size = outsize;
+ }
+ for (i = 0; i < out->buffer->n_datas; i++) {
+ d = &out->buffer->datas[i];
+
+ outsize = SPA_MIN(outsize, d->maxsize);
+
+ if (i < in->buffer->n_datas)
+ spa_ringbuffer_read_data(&impl->buffer,
+ src[i], buffer_size,
+ r % buffer_size,
+ d->data, outsize);
+ else
+ memset(d->data, 0, outsize);
+
+ d->chunk->offset = 0;
+ d->chunk->size = outsize;
+ d->chunk->stride = stride;
+ }
+ if (impl->buffer_size > 0) {
+ r += outsize;
+ spa_ringbuffer_read_update(&impl->buffer, r);
+ }
+ }
+
+ if (in != NULL)
+ pw_stream_queue_buffer(impl->capture, in);
+ if (out != NULL)
+ pw_stream_queue_buffer(impl->playback, out);
+}
+
+static void param_latency_changed(struct impl *impl, const struct spa_pod *param,
+ struct spa_latency_info *info, struct pw_stream *other)
+{
+ struct spa_latency_info latency;
+ uint8_t buffer[1024];
+ struct spa_pod_builder b;
+ const struct spa_pod *params[1];
+
+ if (spa_latency_parse(param, &latency) < 0)
+ return;
+
+ *info = latency;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ params[0] = spa_latency_build(&b, SPA_PARAM_Latency, &latency);
+ pw_stream_update_params(other, params, 1);
+
+ impl->recalc_delay = true;
+}
+
+static void stream_state_changed(void *data, enum pw_stream_state old,
+ enum pw_stream_state state, const char *error)
+{
+ struct impl *impl = data;
+ switch (state) {
+ case PW_STREAM_STATE_PAUSED:
+ pw_stream_flush(impl->playback, false);
+ pw_stream_flush(impl->capture, false);
+ impl->recalc_delay = true;
+ break;
+ case PW_STREAM_STATE_UNCONNECTED:
+ pw_log_info("module %p: unconnected", impl);
+ pw_impl_module_schedule_destroy(impl->module);
+ break;
+ case PW_STREAM_STATE_ERROR:
+ pw_log_info("module %p: error: %s", impl, error);
+ break;
+ default:
+ break;
+ }
+}
+
+static void recalculate_buffer(struct impl *impl)
+{
+ if (impl->target_delay > 0.0f) {
+ uint32_t delay = impl->capture_info.rate * impl->target_delay;
+ void *data;
+
+ impl->buffer_size = (delay + (1u<<15)) * 4;
+ data = realloc(impl->buffer_data, impl->buffer_size * impl->capture_info.channels);
+ if (data == NULL) {
+ pw_log_warn("can't allocate delay buffer, delay disabled: %m");
+ impl->buffer_size = 0;
+ free(impl->buffer_data);
+ }
+ impl->buffer_data = data;
+ spa_ringbuffer_init(&impl->buffer);
+ } else {
+ impl->buffer_size = 0;
+ free(impl->buffer_data);
+ impl->buffer_data = NULL;
+ }
+ pw_log_info("configured delay:%f buffer:%d", impl->target_delay, impl->buffer_size);
+ impl->recalc_delay = true;
+}
+
+static void capture_param_changed(void *data, uint32_t id, const struct spa_pod *param)
+{
+ struct impl *impl = data;
+
+ switch (id) {
+ case SPA_PARAM_Format:
+ {
+ struct spa_audio_info_raw info;
+ if (param == NULL)
+ return;
+ if (spa_format_audio_raw_parse(param, &info) < 0)
+ return;
+ if (info.rate == 0 ||
+ info.channels == 0 ||
+ info.channels > SPA_AUDIO_MAX_CHANNELS)
+ return;
+
+ impl->capture_info = info;
+ recalculate_buffer(impl);
+ break;
+ }
+ case SPA_PARAM_Latency:
+ param_latency_changed(impl, param, &impl->capture_latency, impl->playback);
+ break;
+ }
+}
+
+static const struct pw_stream_events in_stream_events = {
+ PW_VERSION_STREAM_EVENTS,
+ .destroy = capture_destroy,
+ .process = capture_process,
+ .state_changed = stream_state_changed,
+ .param_changed = capture_param_changed,
+};
+
+static void playback_destroy(void *d)
+{
+ struct impl *impl = d;
+ spa_hook_remove(&impl->playback_listener);
+ impl->playback = NULL;
+}
+
+static void playback_param_changed(void *data, uint32_t id, const struct spa_pod *param)
+{
+ struct impl *impl = data;
+
+ switch (id) {
+ case SPA_PARAM_Latency:
+ param_latency_changed(impl, param, &impl->playback_latency, impl->capture);
+ break;
+ }
+}
+static const struct pw_stream_events out_stream_events = {
+ PW_VERSION_STREAM_EVENTS,
+ .destroy = playback_destroy,
+ .process = playback_process,
+ .state_changed = stream_state_changed,
+ .param_changed = playback_param_changed,
+};
+
+static int setup_streams(struct impl *impl)
+{
+ int res;
+ uint32_t n_params;
+ const struct spa_pod *params[1];
+ uint8_t buffer[1024];
+ struct spa_pod_builder b;
+
+ impl->capture = pw_stream_new(impl->core,
+ "loopback capture", impl->capture_props);
+ impl->capture_props = NULL;
+ if (impl->capture == NULL)
+ return -errno;
+
+ pw_stream_add_listener(impl->capture,
+ &impl->capture_listener,
+ &in_stream_events, impl);
+
+ impl->playback = pw_stream_new(impl->core,
+ "loopback playback", impl->playback_props);
+ impl->playback_props = NULL;
+ if (impl->playback == NULL)
+ return -errno;
+
+ pw_stream_add_listener(impl->playback,
+ &impl->playback_listener,
+ &out_stream_events, impl);
+
+ /* connect playback first to activate it before capture triggers it */
+ n_params = 0;
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ params[n_params++] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat,
+ &impl->playback_info);
+ if ((res = pw_stream_connect(impl->playback,
+ PW_DIRECTION_OUTPUT,
+ PW_ID_ANY,
+ PW_STREAM_FLAG_AUTOCONNECT |
+ PW_STREAM_FLAG_MAP_BUFFERS |
+ PW_STREAM_FLAG_RT_PROCESS |
+ PW_STREAM_FLAG_TRIGGER,
+ params, n_params)) < 0)
+ return res;
+
+ n_params = 0;
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ params[n_params++] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat,
+ &impl->capture_info);
+ if ((res = pw_stream_connect(impl->capture,
+ PW_DIRECTION_INPUT,
+ PW_ID_ANY,
+ PW_STREAM_FLAG_AUTOCONNECT |
+ PW_STREAM_FLAG_MAP_BUFFERS |
+ PW_STREAM_FLAG_RT_PROCESS,
+ params, n_params)) < 0)
+ return res;
+
+ return 0;
+}
+
+static void core_error(void *data, uint32_t id, int seq, int res, const char *message)
+{
+ struct impl *impl = data;
+
+ if (res == -ENOENT) {
+ pw_log_info("message id:%u seq:%d res:%d (%s): %s",
+ id, seq, res, spa_strerror(res), message);
+ } else {
+ pw_log_warn("error id:%u seq:%d res:%d (%s): %s",
+ id, seq, res, spa_strerror(res), message);
+ }
+
+ if (id == PW_ID_CORE && res == -EPIPE)
+ pw_impl_module_schedule_destroy(impl->module);
+}
+
+static const struct pw_core_events core_events = {
+ PW_VERSION_CORE_EVENTS,
+ .error = core_error,
+};
+
+static void core_destroy(void *d)
+{
+ struct impl *impl = d;
+ spa_hook_remove(&impl->core_listener);
+ impl->core = NULL;
+ pw_impl_module_schedule_destroy(impl->module);
+}
+
+static const struct pw_proxy_events core_proxy_events = {
+ .destroy = core_destroy,
+};
+
+static void impl_destroy(struct impl *impl)
+{
+ /* deactivate both streams before destroying any of them */
+ if (impl->capture)
+ pw_stream_set_active(impl->capture, false);
+ if (impl->playback)
+ pw_stream_set_active(impl->playback, false);
+
+ if (impl->capture)
+ pw_stream_destroy(impl->capture);
+ if (impl->playback)
+ pw_stream_destroy(impl->playback);
+
+ if (impl->core && impl->do_disconnect)
+ pw_core_disconnect(impl->core);
+
+ pw_properties_free(impl->capture_props);
+ pw_properties_free(impl->playback_props);
+ free(impl);
+}
+
+static void module_destroy(void *data)
+{
+ struct impl *impl = data;
+ spa_hook_remove(&impl->module_listener);
+ impl_destroy(impl);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy,
+};
+
+static uint32_t channel_from_name(const char *name)
+{
+ int i;
+ for (i = 0; spa_type_audio_channel[i].name; i++) {
+ if (spa_streq(name, spa_debug_type_short_name(spa_type_audio_channel[i].name)))
+ return spa_type_audio_channel[i].type;
+ }
+ return SPA_AUDIO_CHANNEL_UNKNOWN;
+}
+
+static void parse_position(struct spa_audio_info_raw *info, const char *val, size_t len)
+{
+ struct spa_json it[2];
+ char v[256];
+
+ spa_json_init(&it[0], val, len);
+ if (spa_json_enter_array(&it[0], &it[1]) <= 0)
+ spa_json_init(&it[1], val, len);
+
+ info->channels = 0;
+ while (spa_json_get_string(&it[1], v, sizeof(v)) > 0 &&
+ info->channels < SPA_AUDIO_MAX_CHANNELS) {
+ info->position[info->channels++] = channel_from_name(v);
+ }
+}
+
+static void parse_audio_info(struct pw_properties *props, struct spa_audio_info_raw *info)
+{
+ const char *str;
+
+ *info = SPA_AUDIO_INFO_RAW_INIT(
+ .format = SPA_AUDIO_FORMAT_F32P);
+ info->rate = pw_properties_get_int32(props, PW_KEY_AUDIO_RATE, 0);
+ info->channels = pw_properties_get_uint32(props, PW_KEY_AUDIO_CHANNELS, 0);
+ info->channels = SPA_MIN(info->channels, SPA_AUDIO_MAX_CHANNELS);
+ if ((str = pw_properties_get(props, SPA_KEY_AUDIO_POSITION)) != NULL)
+ parse_position(info, str, strlen(str));
+}
+
+static void copy_props(struct impl *impl, struct pw_properties *props, const char *key)
+{
+ const char *str;
+ if ((str = pw_properties_get(props, key)) != NULL) {
+ if (pw_properties_get(impl->capture_props, key) == NULL)
+ pw_properties_set(impl->capture_props, key, str);
+ if (pw_properties_get(impl->playback_props, key) == NULL)
+ pw_properties_set(impl->playback_props, key, str);
+ }
+}
+
+SPA_EXPORT
+int pipewire__module_init(struct pw_impl_module *module, const char *args)
+{
+ struct pw_context *context = pw_impl_module_get_context(module);
+ struct pw_properties *props;
+ struct impl *impl;
+ uint32_t id = pw_global_get_id(pw_impl_module_get_global(module));
+ uint32_t pid = getpid();
+ const char *str;
+ int res;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ impl = calloc(1, sizeof(struct impl));
+ if (impl == NULL)
+ return -errno;
+
+ pw_log_debug("module %p: new %s", impl, args);
+
+ if (args)
+ props = pw_properties_new_string(args);
+ else
+ props = pw_properties_new(NULL, NULL);
+ if (props == NULL) {
+ res = -errno;
+ pw_log_error( "can't create properties: %m");
+ goto error;
+ }
+
+ impl->capture_props = pw_properties_new(NULL, NULL);
+ impl->playback_props = pw_properties_new(NULL, NULL);
+ if (impl->capture_props == NULL || impl->playback_props == NULL) {
+ res = -errno;
+ pw_log_error( "can't create properties: %m");
+ goto error;
+ }
+
+ impl->module = module;
+ impl->context = context;
+
+ if (pw_properties_get(props, PW_KEY_NODE_GROUP) == NULL)
+ pw_properties_setf(props, PW_KEY_NODE_GROUP, "loopback-%u-%u", pid, id);
+ if (pw_properties_get(props, PW_KEY_NODE_LINK_GROUP) == NULL)
+ pw_properties_setf(props, PW_KEY_NODE_LINK_GROUP, "loopback-%u-%u", pid, id);
+ if (pw_properties_get(props, PW_KEY_NODE_VIRTUAL) == NULL)
+ pw_properties_set(props, PW_KEY_NODE_VIRTUAL, "true");
+ if (pw_properties_get(props, "resample.prefill") == NULL)
+ pw_properties_set(props, "resample.prefill", "true");
+
+ if ((str = pw_properties_get(props, "capture.props")) != NULL)
+ pw_properties_update_string(impl->capture_props, str, strlen(str));
+ if ((str = pw_properties_get(props, "playback.props")) != NULL)
+ pw_properties_update_string(impl->playback_props, str, strlen(str));
+
+ if ((str = pw_properties_get(props, "target.delay.sec")) != NULL)
+ spa_atof(str, &impl->target_delay);
+ if (impl->target_delay > 0.0f &&
+ pw_properties_get(props, PW_KEY_NODE_LATENCY) == NULL)
+ /* a source and sink (USB) usually have a 1.5 quantum delay, so we use
+ * a 2 times smaller quantum to compensate */
+ pw_properties_setf(props, PW_KEY_NODE_LATENCY, "%u/%u",
+ (unsigned)(impl->target_delay * 48000 / 3), 48000);
+
+ copy_props(impl, props, PW_KEY_AUDIO_RATE);
+ copy_props(impl, props, PW_KEY_AUDIO_CHANNELS);
+ copy_props(impl, props, SPA_KEY_AUDIO_POSITION);
+ copy_props(impl, props, PW_KEY_NODE_DESCRIPTION);
+ copy_props(impl, props, PW_KEY_NODE_GROUP);
+ copy_props(impl, props, PW_KEY_NODE_LINK_GROUP);
+ copy_props(impl, props, PW_KEY_NODE_LATENCY);
+ copy_props(impl, props, PW_KEY_NODE_VIRTUAL);
+ copy_props(impl, props, PW_KEY_MEDIA_NAME);
+ copy_props(impl, props, "resample.prefill");
+
+ if ((str = pw_properties_get(props, PW_KEY_NODE_NAME)) == NULL) {
+ pw_properties_setf(props, PW_KEY_NODE_NAME,
+ "loopback-%u-%u", pid, id);
+ str = pw_properties_get(props, PW_KEY_NODE_NAME);
+ }
+ if (pw_properties_get(impl->capture_props, PW_KEY_NODE_NAME) == NULL)
+ pw_properties_setf(impl->capture_props, PW_KEY_NODE_NAME,
+ "input.%s", str);
+ if (pw_properties_get(impl->playback_props, PW_KEY_NODE_NAME) == NULL)
+ pw_properties_setf(impl->playback_props, PW_KEY_NODE_NAME,
+ "output.%s", str);
+ if (pw_properties_get(impl->capture_props, PW_KEY_NODE_DESCRIPTION) == NULL)
+ pw_properties_set(impl->capture_props, PW_KEY_NODE_DESCRIPTION, str);
+ if (pw_properties_get(impl->playback_props, PW_KEY_NODE_DESCRIPTION) == NULL)
+ pw_properties_set(impl->playback_props, PW_KEY_NODE_DESCRIPTION, str);
+
+ parse_audio_info(impl->capture_props, &impl->capture_info);
+ parse_audio_info(impl->playback_props, &impl->playback_info);
+
+ if (pw_properties_get(impl->capture_props, PW_KEY_MEDIA_NAME) == NULL)
+ pw_properties_setf(impl->capture_props, PW_KEY_MEDIA_NAME, "%s input",
+ pw_properties_get(impl->capture_props, PW_KEY_NODE_DESCRIPTION));
+ if (pw_properties_get(impl->playback_props, PW_KEY_MEDIA_NAME) == NULL)
+ pw_properties_setf(impl->playback_props, PW_KEY_MEDIA_NAME, "%s output",
+ pw_properties_get(impl->playback_props, PW_KEY_NODE_DESCRIPTION));
+
+ impl->core = pw_context_get_object(impl->context, PW_TYPE_INTERFACE_Core);
+ if (impl->core == NULL) {
+ str = pw_properties_get(props, PW_KEY_REMOTE_NAME);
+ impl->core = pw_context_connect(impl->context,
+ pw_properties_new(
+ PW_KEY_REMOTE_NAME, str,
+ NULL),
+ 0);
+ impl->do_disconnect = true;
+ }
+ if (impl->core == NULL) {
+ res = -errno;
+ pw_log_error("can't connect: %m");
+ goto error;
+ }
+
+ pw_properties_free(props);
+
+ pw_proxy_add_listener((struct pw_proxy*)impl->core,
+ &impl->core_proxy_listener,
+ &core_proxy_events, impl);
+ pw_core_add_listener(impl->core,
+ &impl->core_listener,
+ &core_events, impl);
+
+ setup_streams(impl);
+
+ pw_impl_module_add_listener(module, &impl->module_listener, &module_events, impl);
+
+ pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_props));
+
+ return 0;
+
+error:
+ pw_properties_free(props);
+ impl_destroy(impl);
+ return res;
+}
diff --git a/src/modules/module-metadata.c b/src/modules/module-metadata.c
new file mode 100644
index 0000000..b8619a4
--- /dev/null
+++ b/src/modules/module-metadata.c
@@ -0,0 +1,241 @@
+/* PipeWire
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <errno.h>
+#include <dlfcn.h>
+
+#include "config.h"
+
+#include <spa/utils/result.h>
+
+#include <pipewire/impl.h>
+#include <pipewire/extensions/metadata.h>
+
+/** \page page_module_metadata PipeWire Module: Metadata
+ */
+
+#define NAME "metadata"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+static const struct spa_dict_item module_props[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Allow clients to create metadata store" },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+
+void * pw_metadata_new(struct pw_context *context, struct pw_resource *resource,
+ struct pw_properties *properties);
+
+struct pw_proxy *pw_core_metadata_export(struct pw_core *core,
+ const char *type, const struct spa_dict *props, void *object, size_t user_data_size);
+
+int pw_protocol_native_ext_metadata_init(struct pw_context *context);
+
+struct factory_data {
+ struct pw_impl_factory *factory;
+ struct spa_hook factory_listener;
+
+ struct pw_impl_module *module;
+ struct spa_hook module_listener;
+
+ struct pw_export_type export_metadata;
+};
+
+static void *create_object(void *_data,
+ struct pw_resource *resource,
+ const char *type,
+ uint32_t version,
+ struct pw_properties *properties,
+ uint32_t new_id)
+{
+ struct factory_data *data = _data;
+ struct pw_context *context = pw_impl_module_get_context(data->module);
+ void *result;
+ struct pw_resource *metadata_resource = NULL;
+ struct pw_impl_client *client = resource ? pw_resource_get_client(resource) : NULL;
+ int res;
+
+ if (properties == NULL)
+ properties = pw_properties_new(NULL, NULL);
+ if (properties == NULL)
+ return NULL;
+
+ pw_properties_setf(properties, PW_KEY_FACTORY_ID, "%d",
+ pw_impl_factory_get_info(data->factory)->id);
+ pw_properties_setf(properties, PW_KEY_MODULE_ID, "%d",
+ pw_impl_module_get_info(data->module)->id);
+
+ if (pw_properties_get(properties, PW_KEY_METADATA_NAME) == NULL)
+ pw_properties_set(properties, PW_KEY_METADATA_NAME, "default");
+
+ if (client) {
+ metadata_resource = pw_resource_new(client, new_id, PW_PERM_ALL, type, version, 0);
+ if (metadata_resource == NULL) {
+ res = -errno;
+ goto error_resource;
+ }
+
+ pw_properties_setf(properties, PW_KEY_CLIENT_ID, "%d",
+ pw_impl_client_get_info(client)->id);
+
+ result = pw_metadata_new(context, metadata_resource, properties);
+ if (result == NULL) {
+ properties = NULL;
+ res = -errno;
+ goto error_node;
+ }
+ } else {
+ result = pw_context_create_metadata(context, NULL, properties, 0);
+ if (result == NULL) {
+ properties = NULL;
+ res = -errno;
+ goto error_node;
+ }
+ pw_impl_metadata_register(result, NULL);
+ }
+ return result;
+
+error_resource:
+ pw_resource_errorf_id(resource, new_id, res,
+ "can't create resource: %s", spa_strerror(res));
+ goto error_exit;
+error_node:
+ pw_resource_errorf_id(resource, new_id, res,
+ "can't create metadata: %s", spa_strerror(res));
+ goto error_exit_free;
+
+error_exit_free:
+ if (metadata_resource)
+ pw_resource_remove(metadata_resource);
+error_exit:
+ pw_properties_free(properties);
+ errno = -res;
+ return NULL;
+}
+
+static const struct pw_impl_factory_implementation impl_factory = {
+ PW_VERSION_IMPL_FACTORY_IMPLEMENTATION,
+ .create_object = create_object,
+};
+
+static void factory_destroy(void *data)
+{
+ struct factory_data *d = data;
+ spa_hook_remove(&d->factory_listener);
+ d->factory = NULL;
+ if (d->module)
+ pw_impl_module_destroy(d->module);
+}
+
+static const struct pw_impl_factory_events factory_events = {
+ PW_VERSION_IMPL_FACTORY_EVENTS,
+ .destroy = factory_destroy,
+};
+
+static void module_destroy(void *data)
+{
+ struct factory_data *d = data;
+ spa_hook_remove(&d->module_listener);
+ spa_list_remove(&d->export_metadata.link);
+ d->module = NULL;
+ if (d->factory)
+ pw_impl_factory_destroy(d->factory);
+}
+
+static void module_registered(void *data)
+{
+ struct factory_data *d = data;
+ struct pw_impl_module *module = d->module;
+ struct pw_impl_factory *factory = d->factory;
+ struct spa_dict_item items[1];
+ char id[16];
+ int res;
+
+ snprintf(id, sizeof(id), "%d", pw_global_get_id(pw_impl_module_get_global(module)));
+ items[0] = SPA_DICT_ITEM_INIT(PW_KEY_MODULE_ID, id);
+ pw_impl_factory_update_properties(factory, &SPA_DICT_INIT(items, 1));
+
+ if ((res = pw_impl_factory_register(factory, NULL)) < 0) {
+ pw_log_error("%p: can't register factory: %s", factory, spa_strerror(res));
+ }
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy,
+ .registered = module_registered,
+};
+
+SPA_EXPORT
+int pipewire__module_init(struct pw_impl_module *module, const char *args)
+{
+ struct pw_context *context = pw_impl_module_get_context(module);
+ struct pw_impl_factory *factory;
+ struct factory_data *data;
+ int res;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ if ((res = pw_protocol_native_ext_metadata_init(context)) < 0)
+ return res;
+
+ factory = pw_context_create_factory(context,
+ "metadata",
+ PW_TYPE_INTERFACE_Metadata,
+ PW_VERSION_METADATA,
+ NULL,
+ sizeof(*data));
+ if (factory == NULL)
+ return -errno;
+
+ data = pw_impl_factory_get_user_data(factory);
+ data->factory = factory;
+ data->module = module;
+
+ pw_log_debug("module %p: new", module);
+
+ pw_impl_factory_set_implementation(factory,
+ &impl_factory,
+ data);
+
+ data->export_metadata.type = PW_TYPE_INTERFACE_Metadata;
+ data->export_metadata.func = pw_core_metadata_export;
+ if ((res = pw_context_register_export_type(context, &data->export_metadata)) < 0)
+ goto error;
+
+ pw_impl_factory_add_listener(factory, &data->factory_listener, &factory_events, data);
+ pw_impl_module_add_listener(module, &data->module_listener, &module_events, data);
+
+ pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_props));
+
+ return 0;
+error:
+ pw_impl_factory_destroy(data->factory);
+ return res;
+}
diff --git a/src/modules/module-metadata/metadata.c b/src/modules/module-metadata/metadata.c
new file mode 100644
index 0000000..d863bea
--- /dev/null
+++ b/src/modules/module-metadata/metadata.c
@@ -0,0 +1,316 @@
+/* PipeWire
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <spa/utils/result.h>
+
+#include <pipewire/impl.h>
+
+#include <pipewire/extensions/metadata.h>
+
+#define NAME "metadata"
+
+struct impl {
+ struct spa_hook context_listener;
+
+ struct pw_global *global;
+ struct spa_hook global_listener;
+
+ struct pw_metadata *metadata;
+ struct pw_resource *resource;
+ struct spa_hook resource_listener;
+ int pending;
+};
+
+struct resource_data {
+ struct impl *impl;
+
+ struct pw_resource *resource;
+ struct spa_hook resource_listener;
+ struct spa_hook object_listener;
+ struct spa_hook metadata_listener;
+ struct spa_hook impl_resource_listener;
+ int pong_seq;
+};
+
+#define pw_metadata_resource(r,m,v,...) \
+ pw_resource_call_res(r,struct pw_metadata_events,m,v,__VA_ARGS__)
+
+#define pw_metadata_resource_property(r,...) \
+ pw_metadata_resource(r,property,0,__VA_ARGS__)
+
+static int metadata_property(void *data,
+ uint32_t subject,
+ const char *key,
+ const char *type,
+ const char *value)
+{
+ struct resource_data *d = data;
+ struct pw_resource *resource = d->resource;
+ struct pw_impl_client *client = pw_resource_get_client(resource);
+ struct impl *impl = d->impl;
+
+ if (impl->pending == 0 || d->pong_seq != 0) {
+ if (pw_impl_client_check_permissions(client, subject, PW_PERM_R) >= 0)
+ pw_metadata_resource_property(d->resource, subject, key, type, value);
+ }
+ return 0;
+}
+
+static const struct pw_metadata_events metadata_events = {
+ PW_VERSION_METADATA_EVENTS,
+ .property = metadata_property,
+};
+
+static int metadata_set_property(void *object,
+ uint32_t subject,
+ const char *key,
+ const char *type,
+ const char *value)
+{
+ struct resource_data *d = object;
+ struct impl *impl = d->impl;
+ struct pw_resource *resource = d->resource;
+ struct pw_impl_client *client = pw_resource_get_client(resource);
+ int res;
+
+ if ((res = pw_impl_client_check_permissions(client, subject, PW_PERM_R | PW_PERM_M)) < 0)
+ goto error;
+
+ pw_metadata_set_property(impl->metadata, subject, key, type, value);
+ return 0;
+
+error:
+ pw_resource_errorf(resource, res, "set property error for id %d: %s",
+ subject, spa_strerror(res));
+ return res;
+}
+
+static int metadata_clear(void *object)
+{
+ struct resource_data *d = object;
+ struct impl *impl = d->impl;
+ pw_metadata_clear(impl->metadata);
+ return 0;
+}
+
+static const struct pw_metadata_methods metadata_methods = {
+ PW_VERSION_METADATA_METHODS,
+ .set_property = metadata_set_property,
+ .clear = metadata_clear,
+};
+
+
+static void global_unbind(void *data)
+{
+ struct resource_data *d = data;
+ if (d->resource) {
+ spa_hook_remove(&d->resource_listener);
+ spa_hook_remove(&d->object_listener);
+ spa_hook_remove(&d->metadata_listener);
+ spa_hook_remove(&d->impl_resource_listener);
+ }
+}
+
+static const struct pw_resource_events resource_events = {
+ PW_VERSION_RESOURCE_EVENTS,
+ .destroy = global_unbind,
+};
+
+static void remove_pending(struct resource_data *d)
+{
+ if (d->pong_seq != 0) {
+ pw_impl_client_set_busy(pw_resource_get_client(d->resource), false);
+ d->pong_seq = 0;
+ d->impl->pending--;
+ }
+}
+
+static void impl_resource_destroy(void *data)
+{
+ struct resource_data *d = data;
+ remove_pending(d);
+}
+
+static void impl_resource_pong(void *data, int seq)
+{
+ struct resource_data *d = data;
+ if (d->pong_seq == seq)
+ remove_pending(d);
+}
+
+static const struct pw_resource_events impl_resource_events = {
+ PW_VERSION_RESOURCE_EVENTS,
+ .destroy = impl_resource_destroy,
+ .pong = impl_resource_pong,
+};
+
+static int
+global_bind(void *object, struct pw_impl_client *client, uint32_t permissions,
+ uint32_t version, uint32_t id)
+{
+ struct impl *impl = object;
+ struct pw_resource *resource;
+ struct resource_data *data;
+
+ resource = pw_resource_new(client, id, permissions, PW_TYPE_INTERFACE_Metadata, version, sizeof(*data));
+ if (resource == NULL)
+ return -errno;
+
+ data = pw_resource_get_user_data(resource);
+ data->impl = impl;
+ data->resource = resource;
+
+ pw_global_add_resource(impl->global, resource);
+
+ /* listen for when the resource goes away */
+ pw_resource_add_listener(resource,
+ &data->resource_listener,
+ &resource_events, data);
+
+ /* resource methods -> implementation */
+ pw_resource_add_object_listener(resource,
+ &data->object_listener,
+ &metadata_methods, data);
+
+ pw_impl_client_set_busy(client, true);
+
+ /* implementation events -> resource */
+ pw_metadata_add_listener(impl->metadata,
+ &data->metadata_listener,
+ &metadata_events, data);
+
+ pw_resource_add_listener(impl->resource,
+ &data->impl_resource_listener,
+ &impl_resource_events, data);
+
+ data->pong_seq = pw_resource_ping(impl->resource, data->pong_seq);
+ impl->pending++;
+
+ return 0;
+}
+
+static void global_destroy(void *data)
+{
+ struct impl *impl = data;
+ spa_hook_remove(&impl->global_listener);
+ impl->global = NULL;
+ if (impl->resource)
+ pw_resource_destroy(impl->resource);
+}
+
+static const struct pw_global_events global_events = {
+ PW_VERSION_GLOBAL_EVENTS,
+ .destroy = global_destroy,
+};
+
+
+static void context_global_removed(void *data, struct pw_global *global)
+{
+ struct impl *impl = data;
+ pw_metadata_set_property(impl->metadata,
+ pw_global_get_id(global), NULL, NULL, NULL);
+}
+
+static const struct pw_context_events context_events = {
+ PW_VERSION_CONTEXT_EVENTS,
+ .global_removed = context_global_removed,
+};
+
+static void global_resource_destroy(void *data)
+{
+ struct impl *impl = data;
+ spa_hook_remove(&impl->context_listener);
+ spa_hook_remove(&impl->resource_listener);
+ impl->resource = NULL;
+ impl->metadata = NULL;
+ if (impl->global)
+ pw_global_destroy(impl->global);
+ free(impl);
+}
+
+static const struct pw_resource_events global_resource_events = {
+ PW_VERSION_RESOURCE_EVENTS,
+ .destroy = global_resource_destroy,
+};
+
+void *
+pw_metadata_new(struct pw_context *context, struct pw_resource *resource,
+ struct pw_properties *properties)
+{
+ struct impl *impl;
+ char serial_str[32];
+ struct spa_dict_item items[1] = {
+ SPA_DICT_ITEM_INIT(PW_KEY_OBJECT_SERIAL, serial_str),
+ };
+ struct spa_dict extra_props = SPA_DICT_INIT_ARRAY(items);
+ static const char * const keys[] = {
+ PW_KEY_OBJECT_SERIAL,
+ NULL
+ };
+
+ if (properties == NULL)
+ properties = pw_properties_new(NULL, NULL);
+ if (properties == NULL)
+ return NULL;
+
+ impl = calloc(1, sizeof(*impl));
+ if (impl == NULL) {
+ pw_properties_free(properties);
+ return NULL;
+ }
+
+ pw_resource_install_marshal(resource, true);
+
+ impl->global = pw_global_new(context,
+ PW_TYPE_INTERFACE_Metadata,
+ PW_VERSION_METADATA,
+ properties,
+ global_bind, impl);
+ if (impl->global == NULL) {
+ free(impl);
+ return NULL;
+ }
+ impl->resource = resource;
+ impl->metadata = (struct pw_metadata*)resource;
+
+ spa_scnprintf(serial_str, sizeof(serial_str), "%"PRIu64,
+ pw_global_get_serial(impl->global));
+ pw_global_update_keys(impl->global, &extra_props, keys);
+
+ pw_context_add_listener(context, &impl->context_listener,
+ &context_events, impl);
+
+ pw_global_add_listener(impl->global,
+ &impl->global_listener,
+ &global_events, impl);
+
+ pw_resource_set_bound_id(resource, pw_global_get_id(impl->global));
+ pw_global_register(impl->global);
+
+ pw_resource_add_listener(resource,
+ &impl->resource_listener,
+ &global_resource_events, impl);
+
+ return impl;
+}
diff --git a/src/modules/module-metadata/protocol-native.c b/src/modules/module-metadata/protocol-native.c
new file mode 100644
index 0000000..54958db
--- /dev/null
+++ b/src/modules/module-metadata/protocol-native.c
@@ -0,0 +1,354 @@
+/* PipeWire
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+
+#include <spa/pod/builder.h>
+#include <spa/pod/parser.h>
+#include <spa/utils/result.h>
+
+#include <pipewire/pipewire.h>
+
+#include <pipewire/extensions/protocol-native.h>
+#include <pipewire/extensions/metadata.h>
+
+static int metadata_resource_marshal_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct pw_metadata_events *events,
+ void *data)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_builder *b;
+
+ pw_resource_add_object_listener(resource, listener, events, data);
+
+ b = pw_protocol_native_begin_resource(resource, PW_METADATA_METHOD_ADD_LISTENER, NULL);
+ return pw_protocol_native_end_resource(resource, b);
+}
+
+static int metadata_proxy_marshal_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct pw_metadata_events *events,
+ void *data)
+{
+ struct pw_proxy *proxy = object;
+ pw_proxy_add_object_listener(proxy, listener, events, data);
+ return 0;
+}
+
+static int metadata_resource_demarshal_add_listener(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ return -ENOTSUP;
+}
+
+static const struct pw_metadata_events pw_protocol_native_metadata_client_event_marshal;
+
+static int metadata_proxy_demarshal_add_listener(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_hook listener;
+ int res;
+
+ spa_zero(listener);
+ res = pw_proxy_notify(proxy, struct pw_metadata_methods, add_listener, 0,
+ &listener, &pw_protocol_native_metadata_client_event_marshal, object);
+ spa_hook_remove(&listener);
+
+ return res;
+}
+
+static void metadata_marshal_set_property(struct spa_pod_builder *b, uint32_t subject,
+ const char *key, const char *type, const char *value)
+{
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(subject),
+ SPA_POD_String(key),
+ SPA_POD_String(type),
+ SPA_POD_String(value));
+}
+
+static int metadata_proxy_marshal_set_property(void *object, uint32_t subject,
+ const char *key, const char *type, const char *value)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+ b = pw_protocol_native_begin_proxy(proxy, PW_METADATA_METHOD_SET_PROPERTY, NULL);
+ metadata_marshal_set_property(b, subject, key, type, value);
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+static int metadata_resource_marshal_set_property(void *object, uint32_t subject,
+ const char *key, const char *type, const char *value)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_builder *b;
+ b = pw_protocol_native_begin_resource(resource, PW_METADATA_METHOD_SET_PROPERTY, NULL);
+ metadata_marshal_set_property(b, subject, key, type, value);
+ return pw_protocol_native_end_resource(resource, b);
+}
+
+static int metadata_demarshal_set_property(struct spa_pod_parser *prs, uint32_t *subject,
+ char **key, char **type, char **value)
+{
+ return spa_pod_parser_get_struct(prs,
+ SPA_POD_Int(subject),
+ SPA_POD_String(key),
+ SPA_POD_String(type),
+ SPA_POD_String(value));
+}
+
+static int metadata_proxy_demarshal_set_property(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_parser prs;
+ uint32_t subject;
+ char *key, *type, *value;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (metadata_demarshal_set_property(&prs, &subject, &key, &type, &value) < 0)
+ return -EINVAL;
+ return pw_proxy_notify(proxy, struct pw_metadata_methods, set_property, 0, subject, key, type, value);
+}
+
+static int metadata_resource_demarshal_set_property(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ uint32_t subject;
+ char *key, *type, *value;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (metadata_demarshal_set_property(&prs, &subject, &key, &type, &value) < 0)
+ return -EINVAL;
+ return pw_resource_notify(resource, struct pw_metadata_methods, set_property, 0, subject, key, type, value);
+}
+
+static void metadata_marshal_clear(struct spa_pod_builder *b)
+{
+ spa_pod_builder_add_struct(b, SPA_POD_None());
+}
+
+static int metadata_proxy_marshal_clear(void *object)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+ b = pw_protocol_native_begin_proxy(proxy, PW_METADATA_METHOD_CLEAR, NULL);
+ metadata_marshal_clear(b);
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+static int metadata_resource_marshal_clear(void *object)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_builder *b;
+ b = pw_protocol_native_begin_resource(resource, PW_METADATA_METHOD_CLEAR, NULL);
+ metadata_marshal_clear(b);
+ return pw_protocol_native_end_resource(resource, b);
+}
+
+static int metadata_proxy_demarshal_clear(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_parser prs;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs, SPA_POD_None()) < 0)
+ return -EINVAL;
+ pw_proxy_notify(proxy, struct pw_metadata_methods, clear, 0);
+ return 0;
+}
+
+static int metadata_resource_demarshal_clear(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs, SPA_POD_None()) < 0)
+ return -EINVAL;
+ pw_resource_notify(resource, struct pw_metadata_methods, clear, 0);
+ return 0;
+}
+
+static void metadata_marshal_property(struct spa_pod_builder *b, uint32_t subject,
+ const char *key, const char *type, const char *value)
+{
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(subject),
+ SPA_POD_String(key),
+ SPA_POD_String(type),
+ SPA_POD_String(value));
+}
+
+static int metadata_proxy_marshal_property(void *object, uint32_t subject,
+ const char *key, const char *type, const char *value)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+ b = pw_protocol_native_begin_proxy(proxy, PW_METADATA_EVENT_PROPERTY, NULL);
+ metadata_marshal_property(b, subject, key, type, value);
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int metadata_resource_marshal_property(void *object, uint32_t subject,
+ const char *key, const char *type, const char *value)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_builder *b;
+ b = pw_protocol_native_begin_resource(resource, PW_METADATA_EVENT_PROPERTY, NULL);
+ metadata_marshal_property(b, subject, key, type, value);
+ return pw_protocol_native_end_resource(resource, b);
+}
+
+static int metadata_demarshal_property(struct spa_pod_parser *prs,
+ uint32_t *subject, char **key, char **type, char **value)
+{
+ return spa_pod_parser_get_struct(prs,
+ SPA_POD_Int(subject),
+ SPA_POD_String(key),
+ SPA_POD_String(type),
+ SPA_POD_String(value));
+}
+
+static int metadata_proxy_demarshal_property(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_parser prs;
+ uint32_t subject;
+ char *key, *type, *value;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (metadata_demarshal_property(&prs,
+ &subject, &key, &type, &value) < 0)
+ return -EINVAL;
+ pw_proxy_notify(proxy, struct pw_metadata_events, property, 0, subject, key, type, value);
+ return 0;
+}
+
+static int metadata_resource_demarshal_property(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ uint32_t subject;
+ char *key, *type, *value;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (metadata_demarshal_property(&prs,
+ &subject, &key, &type, &value) < 0)
+ return -EINVAL;
+ pw_resource_notify(resource, struct pw_metadata_events, property, 0, subject, key, type, value);
+ return 0;
+}
+
+static const struct pw_metadata_methods pw_protocol_native_metadata_client_method_marshal = {
+ PW_VERSION_METADATA_METHODS,
+ .add_listener = &metadata_proxy_marshal_add_listener,
+ .set_property = &metadata_proxy_marshal_set_property,
+ .clear = &metadata_proxy_marshal_clear,
+};
+static const struct pw_metadata_methods pw_protocol_native_metadata_server_method_marshal = {
+ PW_VERSION_METADATA_METHODS,
+ .add_listener = &metadata_resource_marshal_add_listener,
+ .set_property = &metadata_resource_marshal_set_property,
+ .clear = &metadata_resource_marshal_clear,
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_metadata_client_method_demarshal[PW_METADATA_METHOD_NUM] =
+{
+ [PW_METADATA_METHOD_ADD_LISTENER] = { &metadata_proxy_demarshal_add_listener, 0 },
+ [PW_METADATA_METHOD_SET_PROPERTY] = { &metadata_proxy_demarshal_set_property, PW_PERM_W },
+ [PW_METADATA_METHOD_CLEAR] = { &metadata_proxy_demarshal_clear, PW_PERM_W },
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_metadata_server_method_demarshal[PW_METADATA_METHOD_NUM] =
+{
+ [PW_METADATA_METHOD_ADD_LISTENER] = { &metadata_resource_demarshal_add_listener, 0 },
+ [PW_METADATA_METHOD_SET_PROPERTY] = { &metadata_resource_demarshal_set_property, PW_PERM_W },
+ [PW_METADATA_METHOD_CLEAR] = { &metadata_resource_demarshal_clear, PW_PERM_W },
+};
+
+static const struct pw_metadata_events pw_protocol_native_metadata_client_event_marshal = {
+ PW_VERSION_METADATA_EVENTS,
+ .property = &metadata_proxy_marshal_property,
+};
+
+static const struct pw_metadata_events pw_protocol_native_metadata_server_event_marshal = {
+ PW_VERSION_METADATA_EVENTS,
+ .property = &metadata_resource_marshal_property,
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_metadata_client_event_demarshal[PW_METADATA_EVENT_NUM] =
+{
+ [PW_METADATA_EVENT_PROPERTY] = { &metadata_proxy_demarshal_property, 0 },
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_metadata_server_event_demarshal[PW_METADATA_EVENT_NUM] =
+{
+ [PW_METADATA_EVENT_PROPERTY] = { &metadata_resource_demarshal_property, 0 },
+};
+
+static const struct pw_protocol_marshal pw_protocol_native_metadata_marshal = {
+ PW_TYPE_INTERFACE_Metadata,
+ PW_VERSION_METADATA,
+ 0,
+ PW_METADATA_METHOD_NUM,
+ PW_METADATA_EVENT_NUM,
+ .client_marshal = &pw_protocol_native_metadata_client_method_marshal,
+ .server_demarshal = pw_protocol_native_metadata_server_method_demarshal,
+ .server_marshal = &pw_protocol_native_metadata_server_event_marshal,
+ .client_demarshal = pw_protocol_native_metadata_client_event_demarshal,
+};
+
+static const struct pw_protocol_marshal pw_protocol_native_metadata_impl_marshal = {
+ PW_TYPE_INTERFACE_Metadata,
+ PW_VERSION_METADATA,
+ PW_PROTOCOL_MARSHAL_FLAG_IMPL,
+ PW_METADATA_EVENT_NUM,
+ PW_METADATA_METHOD_NUM,
+ .client_marshal = &pw_protocol_native_metadata_client_event_marshal,
+ .server_demarshal = pw_protocol_native_metadata_server_event_demarshal,
+ .server_marshal = &pw_protocol_native_metadata_server_method_marshal,
+ .client_demarshal = pw_protocol_native_metadata_client_method_demarshal,
+};
+
+int pw_protocol_native_ext_metadata_init(struct pw_context *context)
+{
+ struct pw_protocol *protocol;
+
+ protocol = pw_context_find_protocol(context, PW_TYPE_INFO_PROTOCOL_Native);
+ if (protocol == NULL)
+ return -EPROTO;
+
+ pw_protocol_add_marshal(protocol, &pw_protocol_native_metadata_marshal);
+ pw_protocol_add_marshal(protocol, &pw_protocol_native_metadata_impl_marshal);
+ return 0;
+}
diff --git a/src/modules/module-metadata/proxy-metadata.c b/src/modules/module-metadata/proxy-metadata.c
new file mode 100644
index 0000000..e8b747f
--- /dev/null
+++ b/src/modules/module-metadata/proxy-metadata.c
@@ -0,0 +1,92 @@
+/* PipeWire
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdio.h>
+#include <unistd.h>
+#include <errno.h>
+
+#include <spa/pod/parser.h>
+
+#include "pipewire/pipewire.h"
+#include "pipewire/extensions/metadata.h"
+
+struct object_data {
+ struct pw_metadata *object;
+ struct spa_hook object_listener;
+ struct spa_hook object_methods;
+
+ struct pw_proxy *proxy;
+ struct spa_hook proxy_listener;
+};
+
+static void proxy_object_destroy(void *_data)
+{
+ struct object_data *data = _data;
+ spa_hook_remove(&data->proxy_listener);
+ spa_hook_remove(&data->object_listener);
+ spa_hook_remove(&data->object_methods);
+}
+
+static const struct pw_proxy_events proxy_events = {
+ PW_VERSION_PROXY_EVENTS,
+ .destroy = proxy_object_destroy,
+};
+
+struct pw_proxy *pw_core_metadata_export(struct pw_core *core,
+ const char *type, const struct spa_dict *props, void *object,
+ size_t user_data_size)
+{
+ struct pw_metadata *meta = object;
+ struct spa_interface *iface, *miface;
+ struct pw_proxy *proxy;
+ struct object_data *data;
+
+ proxy = pw_core_create_object(core,
+ "metadata",
+ PW_TYPE_INTERFACE_Metadata,
+ PW_VERSION_METADATA,
+ props,
+ user_data_size + sizeof(struct object_data));
+ if (proxy == NULL)
+ return NULL;
+
+ data = pw_proxy_get_user_data(proxy);
+ data = SPA_PTROFF(data, user_data_size, struct object_data);
+ data->object = object;
+ data->proxy = proxy;
+
+ iface = (struct spa_interface*)proxy;
+ miface = (struct spa_interface*)meta;
+
+ pw_proxy_install_marshal(proxy, true);
+
+ pw_proxy_add_listener(proxy, &data->proxy_listener, &proxy_events, data);
+
+ pw_proxy_add_object_listener(proxy, &data->object_methods,
+ miface->cb.funcs, miface->cb.data);
+ pw_metadata_add_listener(meta, &data->object_listener,
+ iface->cb.funcs, iface->cb.data);
+
+ return proxy;
+}
diff --git a/src/modules/module-pipe-tunnel.c b/src/modules/module-pipe-tunnel.c
new file mode 100644
index 0000000..a593305
--- /dev/null
+++ b/src/modules/module-pipe-tunnel.c
@@ -0,0 +1,730 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Sanchayan Maity <sanchayan@asymptotic.io>
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <signal.h>
+#include <limits.h>
+#include <math.h>
+
+#include "config.h"
+
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+#include <spa/utils/json.h>
+#include <spa/utils/ringbuffer.h>
+#include <spa/utils/dll.h>
+#include <spa/debug/types.h>
+#include <spa/pod/builder.h>
+#include <spa/param/audio/format-utils.h>
+#include <spa/param/latency-utils.h>
+#include <spa/param/audio/raw.h>
+
+#include <pipewire/impl.h>
+#include <pipewire/i18n.h>
+
+/** \page page_module_pipe_tunnel PipeWire Module: Unix Pipe Tunnel
+ *
+ * The pipe-tunnel module provides a source or sink that tunnels all audio to
+ * or from a unix pipe respectively.
+ *
+ * ## Module Options
+ *
+ * - `tunnel.mode`: the desired tunnel to create. (Default `playback`)
+ * - `pipe.filename`: the filename of the pipe.
+ * - `stream.props`: Extra properties for the local stream.
+ *
+ * When `tunnel.mode` is `capture`, a capture stream on the default source is
+ * created. Samples read from the pipe will be the contents of the captured source.
+ *
+ * When `tunnel.mode` is `sink`, a sink node is created. Samples read from the
+ * pipe will be the samples played on the sink.
+ *
+ * When `tunnel.mode` is `playback`, a playback stream on the default sink is
+ * created. Samples written to the pipe will be played on the sink.
+ *
+ * When `tunnel.mode` is `source`, a source node is created. Samples written to
+ * the pipe will be made available to streams connected to the source.
+ *
+ * When `pipe.filename` is not given, a default fifo in `/tmp/fifo_input` or
+ * `/tmp/fifo_output` will be created that can be written and read respectively,
+ * depending on the selected `tunnel.mode`.
+ *
+ * ## General options
+ *
+ * Options with well-known behavior.
+ *
+ * - \ref PW_KEY_REMOTE_NAME
+ * - \ref PW_KEY_AUDIO_FORMAT
+ * - \ref PW_KEY_AUDIO_RATE
+ * - \ref PW_KEY_AUDIO_CHANNELS
+ * - \ref SPA_KEY_AUDIO_POSITION
+ * - \ref PW_KEY_NODE_LATENCY
+ * - \ref PW_KEY_NODE_NAME
+ * - \ref PW_KEY_NODE_DESCRIPTION
+ * - \ref PW_KEY_NODE_GROUP
+ * - \ref PW_KEY_NODE_VIRTUAL
+ * - \ref PW_KEY_MEDIA_CLASS
+ * - \ref PW_KEY_TARGET_OBJECT to specify the remote name or serial id to link to
+ *
+ * When not otherwise specified, the pipe will accept or produce a
+ * 16 bits, stereo, 48KHz sample stream.
+ *
+ * ## Example configuration of a pipe playback stream
+ *
+ *\code{.unparsed}
+ * context.modules = [
+ * { name = libpipewire-module-pipe-tunnel
+ * args = {
+ * tunnel.mode = playback
+ * # Set the pipe name to tunnel to
+ * pipe.filename = "/tmp/fifo_output"
+ * #audio.format=<sample format>
+ * #audio.rate=<sample rate>
+ * #audio.channels=<number of channels>
+ * #audio.position=<channel map>
+ * #target.object=<remote target node>
+ * stream.props = {
+ * # extra sink properties
+ * }
+ * }
+ * }
+ * ]
+ *\endcode
+ */
+
+#define NAME "pipe-tunnel"
+
+#define DEFAULT_CAPTURE_FILENAME "/tmp/fifo_input"
+#define DEFAULT_PLAYBACK_FILENAME "/tmp/fifo_output"
+
+#define DEFAULT_FORMAT "S16"
+#define DEFAULT_RATE 48000
+#define DEFAULT_CHANNELS 2
+#define DEFAULT_POSITION "[ FL FR ]"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+#define MODULE_USAGE "[ remote.name=<remote> ] " \
+ "[ node.latency=<latency as fraction> ] " \
+ "[ node.name=<name of the nodes> ] " \
+ "[ node.description=<description of the nodes> ] " \
+ "[ target.object=<remote node target name or serial> ] "\
+ "[ audio.format=<sample format> ] " \
+ "[ audio.rate=<sample rate> ] " \
+ "[ audio.channels=<number of channels> ] " \
+ "[ audio.position=<channel map> ] " \
+ "[ tunnel.mode=capture|playback|sink|source " \
+ "[ pipe.filename=<filename> ]" \
+ "[ stream.props=<properties> ] "
+
+
+static const struct spa_dict_item module_props[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Create a UNIX pipe tunnel" },
+ { PW_KEY_MODULE_USAGE, MODULE_USAGE },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+struct impl {
+ struct pw_context *context;
+
+#define MODE_PLAYBACK 0
+#define MODE_CAPTURE 1
+#define MODE_SINK 2
+#define MODE_SOURCE 3
+ uint32_t mode;
+ struct pw_properties *props;
+
+ struct pw_impl_module *module;
+
+ struct spa_hook module_listener;
+
+ struct pw_core *core;
+ struct spa_hook core_proxy_listener;
+ struct spa_hook core_listener;
+
+ char *filename;
+ unsigned int unlink_fifo;
+ int fd;
+
+ struct pw_properties *stream_props;
+ enum pw_direction direction;
+ struct pw_stream *stream;
+ struct spa_hook stream_listener;
+ struct spa_audio_info_raw info;
+ uint32_t frame_size;
+
+ unsigned int do_disconnect:1;
+ uint32_t leftover_count;
+ uint8_t *leftover;
+};
+
+static void stream_destroy(void *d)
+{
+ struct impl *impl = d;
+ spa_hook_remove(&impl->stream_listener);
+ impl->stream = NULL;
+}
+
+static void stream_state_changed(void *d, enum pw_stream_state old,
+ enum pw_stream_state state, const char *error)
+{
+ struct impl *impl = d;
+ switch (state) {
+ case PW_STREAM_STATE_ERROR:
+ case PW_STREAM_STATE_UNCONNECTED:
+ pw_impl_module_schedule_destroy(impl->module);
+ break;
+ case PW_STREAM_STATE_PAUSED:
+ break;
+ case PW_STREAM_STATE_STREAMING:
+ break;
+ default:
+ break;
+ }
+}
+
+static void playback_stream_process(void *data)
+{
+ struct impl *impl = data;
+ struct pw_buffer *buf;
+ uint32_t i, size, offs;
+ ssize_t written;
+
+ if ((buf = pw_stream_dequeue_buffer(impl->stream)) == NULL) {
+ pw_log_debug("out of buffers: %m");
+ return;
+ }
+
+ for (i = 0; i < buf->buffer->n_datas; i++) {
+ struct spa_data *d;
+ d = &buf->buffer->datas[i];
+
+ offs = SPA_MIN(d->chunk->offset, d->maxsize);
+ size = SPA_MIN(d->chunk->size, d->maxsize - offs);
+
+ while (size > 0) {
+ written = write(impl->fd, SPA_MEMBER(d->data, offs, void), size);
+ if (written < 0) {
+ if (errno == EINTR) {
+ /* retry if interrupted */
+ continue;
+ } else if (errno == EAGAIN || errno == EWOULDBLOCK) {
+ /* Don't continue writing */
+ break;
+ } else {
+ pw_log_warn("Failed to write to pipe sink");
+ }
+ }
+ offs += written;
+ size -= written;
+ }
+ }
+ pw_stream_queue_buffer(impl->stream, buf);
+}
+
+static void capture_stream_process(void *data)
+{
+ struct impl *impl = data;
+ struct pw_buffer *buf;
+ struct spa_data *d;
+ uint32_t req;
+ ssize_t nread;
+
+ if ((buf = pw_stream_dequeue_buffer(impl->stream)) == NULL) {
+ pw_log_debug("out of buffers: %m");
+ return;
+ }
+
+ d = &buf->buffer->datas[0];
+
+ if ((req = buf->requested * impl->frame_size) == 0)
+ req = 4096 * impl->frame_size;
+
+ req = SPA_MIN(req, d->maxsize);
+
+ d->chunk->offset = 0;
+ d->chunk->stride = impl->frame_size;
+ d->chunk->size = SPA_MIN(req, impl->leftover_count);
+ memcpy(d->data, impl->leftover, d->chunk->size);
+ req -= d->chunk->size;
+
+ nread = read(impl->fd, SPA_PTROFF(d->data, d->chunk->size, void), req);
+ if (nread < 0) {
+ const bool important = !(errno == EINTR
+ || errno == EAGAIN
+ || errno == EWOULDBLOCK);
+
+ if (important)
+ pw_log_warn("failed to read from pipe (%s): %s",
+ impl->filename, strerror(errno));
+ }
+ else {
+ d->chunk->size += nread;
+ }
+
+ impl->leftover_count = d->chunk->size % impl->frame_size;
+ d->chunk->size -= impl->leftover_count;
+ memcpy(impl->leftover, SPA_PTROFF(d->data, d->chunk->size, void), impl->leftover_count);
+
+ pw_stream_queue_buffer(impl->stream, buf);
+}
+
+static const struct pw_stream_events playback_stream_events = {
+ PW_VERSION_STREAM_EVENTS,
+ .destroy = stream_destroy,
+ .state_changed = stream_state_changed,
+ .process = playback_stream_process
+};
+
+static const struct pw_stream_events capture_stream_events = {
+ PW_VERSION_STREAM_EVENTS,
+ .destroy = stream_destroy,
+ .state_changed = stream_state_changed,
+ .process = capture_stream_process
+};
+
+static int create_stream(struct impl *impl)
+{
+ int res;
+ uint32_t n_params;
+ const struct spa_pod *params[1];
+ uint8_t buffer[1024];
+ struct spa_pod_builder b;
+
+ impl->stream = pw_stream_new(impl->core, "pipe", impl->stream_props);
+ impl->stream_props = NULL;
+
+ if (impl->stream == NULL)
+ return -errno;
+
+ if (impl->direction == PW_DIRECTION_OUTPUT) {
+ pw_stream_add_listener(impl->stream,
+ &impl->stream_listener,
+ &capture_stream_events, impl);
+ } else {
+ pw_stream_add_listener(impl->stream,
+ &impl->stream_listener,
+ &playback_stream_events, impl);
+ }
+
+ n_params = 0;
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ params[n_params++] = spa_format_audio_raw_build(&b,
+ SPA_PARAM_EnumFormat, &impl->info);
+
+ if ((res = pw_stream_connect(impl->stream,
+ impl->direction,
+ PW_ID_ANY,
+ PW_STREAM_FLAG_AUTOCONNECT |
+ PW_STREAM_FLAG_MAP_BUFFERS |
+ PW_STREAM_FLAG_RT_PROCESS,
+ params, n_params)) < 0)
+ return res;
+
+ return 0;
+}
+
+static int create_fifo(struct impl *impl)
+{
+ struct stat st;
+ const char *filename;
+ bool do_unlink_fifo = false;
+ int fd = -1, res;
+
+ if ((filename = pw_properties_get(impl->props, "pipe.filename")) == NULL)
+ filename = impl->direction == PW_DIRECTION_INPUT ?
+ DEFAULT_CAPTURE_FILENAME :
+ DEFAULT_PLAYBACK_FILENAME;
+
+ if (mkfifo(filename, 0666) < 0) {
+ if (errno != EEXIST) {
+ res = -errno;
+ pw_log_error("mkfifo('%s'): %s", filename, spa_strerror(res));
+ goto error;
+ }
+ } else {
+ /*
+ * Our umask is 077, so the pipe won't be created with the
+ * requested permissions. Let's fix the permissions with chmod().
+ */
+ if (chmod(filename, 0666) < 0)
+ pw_log_warn("chmod('%s'): %s", filename, spa_strerror(-errno));
+
+ do_unlink_fifo = true;
+ }
+
+ if ((fd = open(filename, O_RDWR | O_CLOEXEC | O_NONBLOCK, 0)) < 0) {
+ res = -errno;
+ pw_log_error("open('%s'): %s", filename, spa_strerror(res));
+ goto error;
+ }
+
+ if (fstat(fd, &st) < 0) {
+ res = -errno;
+ pw_log_error("fstat('%s'): %s", filename, spa_strerror(res));
+ goto error;
+ }
+
+ if (!S_ISFIFO(st.st_mode)) {
+ res = -EINVAL;
+ pw_log_error("'%s' is not a FIFO.", filename);
+ goto error;
+ }
+ pw_log_info("%s fifo '%s' with format:%s channels:%d rate:%d",
+ impl->direction == PW_DIRECTION_OUTPUT ? "reading from" : "writing to",
+ filename,
+ spa_debug_type_find_name(spa_type_audio_format, impl->info.format),
+ impl->info.channels, impl->info.rate);
+
+ impl->filename = strdup(filename);
+ impl->unlink_fifo = do_unlink_fifo;
+ impl->fd = fd;
+ return 0;
+
+error:
+ if (do_unlink_fifo)
+ unlink(filename);
+ if (fd >= 0)
+ close(fd);
+ return res;
+}
+
+static void core_error(void *data, uint32_t id, int seq, int res, const char *message)
+{
+ struct impl *impl = data;
+
+ pw_log_error("error id:%u seq:%d res:%d (%s): %s",
+ id, seq, res, spa_strerror(res), message);
+
+ if (id == PW_ID_CORE && res == -EPIPE)
+ pw_impl_module_schedule_destroy(impl->module);
+}
+
+static const struct pw_core_events core_events = {
+ PW_VERSION_CORE_EVENTS,
+ .error = core_error,
+};
+
+static void core_destroy(void *d)
+{
+ struct impl *impl = d;
+ spa_hook_remove(&impl->core_listener);
+ impl->core = NULL;
+ pw_impl_module_schedule_destroy(impl->module);
+}
+
+static const struct pw_proxy_events core_proxy_events = {
+ .destroy = core_destroy,
+};
+
+static void impl_destroy(struct impl *impl)
+{
+ if (impl->stream)
+ pw_stream_destroy(impl->stream);
+ if (impl->core && impl->do_disconnect)
+ pw_core_disconnect(impl->core);
+
+ if (impl->filename) {
+ if (impl->unlink_fifo)
+ unlink(impl->filename);
+ free(impl->filename);
+ }
+ if (impl->fd >= 0)
+ close(impl->fd);
+
+ pw_properties_free(impl->stream_props);
+ pw_properties_free(impl->props);
+
+ free(impl->leftover);
+ free(impl);
+}
+
+static void module_destroy(void *data)
+{
+ struct impl *impl = data;
+ spa_hook_remove(&impl->module_listener);
+ impl_destroy(impl);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy,
+};
+
+static uint32_t channel_from_name(const char *name)
+{
+ int i;
+ for (i = 0; spa_type_audio_channel[i].name; i++) {
+ if (spa_streq(name, spa_debug_type_short_name(spa_type_audio_channel[i].name)))
+ return spa_type_audio_channel[i].type;
+ }
+ return SPA_AUDIO_CHANNEL_UNKNOWN;
+}
+
+static void parse_position(struct spa_audio_info_raw *info, const char *val, size_t len)
+{
+ struct spa_json it[2];
+ char v[256];
+
+ spa_json_init(&it[0], val, len);
+ if (spa_json_enter_array(&it[0], &it[1]) <= 0)
+ spa_json_init(&it[1], val, len);
+
+ info->channels = 0;
+ while (spa_json_get_string(&it[1], v, sizeof(v)) > 0 &&
+ info->channels < SPA_AUDIO_MAX_CHANNELS) {
+ info->position[info->channels++] = channel_from_name(v);
+ }
+}
+
+static inline uint32_t format_from_name(const char *name, size_t len)
+{
+ int i;
+ for (i = 0; spa_type_audio_format[i].name; i++) {
+ if (strncmp(name, spa_debug_type_short_name(spa_type_audio_format[i].name), len) == 0)
+ return spa_type_audio_format[i].type;
+ }
+ return SPA_AUDIO_FORMAT_UNKNOWN;
+}
+
+static void parse_audio_info(const struct pw_properties *props, struct spa_audio_info_raw *info)
+{
+ const char *str;
+
+ spa_zero(*info);
+ if ((str = pw_properties_get(props, PW_KEY_AUDIO_FORMAT)) == NULL)
+ str = DEFAULT_FORMAT;
+ info->format = format_from_name(str, strlen(str));
+
+ info->rate = pw_properties_get_uint32(props, PW_KEY_AUDIO_RATE, info->rate);
+ if (info->rate == 0)
+ info->rate = DEFAULT_RATE;
+
+ info->channels = pw_properties_get_uint32(props, PW_KEY_AUDIO_CHANNELS, info->channels);
+ info->channels = SPA_MIN(info->channels, SPA_AUDIO_MAX_CHANNELS);
+ if ((str = pw_properties_get(props, SPA_KEY_AUDIO_POSITION)) != NULL)
+ parse_position(info, str, strlen(str));
+ if (info->channels == 0)
+ parse_position(info, DEFAULT_POSITION, strlen(DEFAULT_POSITION));
+}
+
+static int calc_frame_size(const struct spa_audio_info_raw *info)
+{
+ int res = info->channels;
+ switch (info->format) {
+ case SPA_AUDIO_FORMAT_U8:
+ case SPA_AUDIO_FORMAT_S8:
+ case SPA_AUDIO_FORMAT_ALAW:
+ case SPA_AUDIO_FORMAT_ULAW:
+ return res;
+ case SPA_AUDIO_FORMAT_S16:
+ case SPA_AUDIO_FORMAT_S16_OE:
+ case SPA_AUDIO_FORMAT_U16:
+ return res * 2;
+ case SPA_AUDIO_FORMAT_S24:
+ case SPA_AUDIO_FORMAT_S24_OE:
+ case SPA_AUDIO_FORMAT_U24:
+ return res * 3;
+ case SPA_AUDIO_FORMAT_S24_32:
+ case SPA_AUDIO_FORMAT_S24_32_OE:
+ case SPA_AUDIO_FORMAT_S32:
+ case SPA_AUDIO_FORMAT_S32_OE:
+ case SPA_AUDIO_FORMAT_U32:
+ case SPA_AUDIO_FORMAT_U32_OE:
+ case SPA_AUDIO_FORMAT_F32:
+ case SPA_AUDIO_FORMAT_F32_OE:
+ return res * 4;
+ case SPA_AUDIO_FORMAT_F64:
+ case SPA_AUDIO_FORMAT_F64_OE:
+ return res * 8;
+ default:
+ return 0;
+ }
+}
+
+static void copy_props(struct impl *impl, struct pw_properties *props, const char *key)
+{
+ const char *str;
+ if ((str = pw_properties_get(props, key)) != NULL) {
+ if (pw_properties_get(impl->stream_props, key) == NULL)
+ pw_properties_set(impl->stream_props, key, str);
+ }
+}
+
+SPA_EXPORT
+int pipewire__module_init(struct pw_impl_module *module, const char *args)
+{
+ struct pw_context *context = pw_impl_module_get_context(module);
+ struct pw_properties *props = NULL;
+ struct impl *impl;
+ const char *str, *media_class = NULL;
+ int res;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ impl = calloc(1, sizeof(struct impl));
+ if (impl == NULL)
+ return -errno;
+
+ impl->fd = -1;
+
+ pw_log_debug("module %p: new %s", impl, args);
+
+ if (args == NULL)
+ args = "";
+
+ props = pw_properties_new_string(args);
+ if (props == NULL) {
+ res = -errno;
+ pw_log_error( "can't create properties: %m");
+ goto error;
+ }
+ impl->props = props;
+
+ impl->stream_props = pw_properties_new(NULL, NULL);
+ if (impl->stream_props == NULL) {
+ res = -errno;
+ pw_log_error( "can't create properties: %m");
+ goto error;
+ }
+
+ impl->module = module;
+ impl->context = context;
+
+ if ((str = pw_properties_get(props, "tunnel.mode")) == NULL)
+ str = "playback";
+
+ if (spa_streq(str, "capture")) {
+ impl->mode = MODE_CAPTURE;
+ impl->direction = PW_DIRECTION_INPUT;
+ } else if (spa_streq(str, "playback")) {
+ impl->mode = MODE_PLAYBACK;
+ impl->direction = PW_DIRECTION_OUTPUT;
+ }else if (spa_streq(str, "sink")) {
+ impl->mode = MODE_SINK;
+ impl->direction = PW_DIRECTION_INPUT;
+ media_class = "Audio/Sink";
+ } else if (spa_streq(str, "source")) {
+ impl->mode = MODE_SOURCE;
+ impl->direction = PW_DIRECTION_OUTPUT;
+ media_class = "Audio/Source";
+ } else {
+ pw_log_error("invalid tunnel.mode '%s'", str);
+ res = -EINVAL;
+ goto error;
+ }
+
+ if (pw_properties_get(props, PW_KEY_NODE_VIRTUAL) == NULL)
+ pw_properties_set(props, PW_KEY_NODE_VIRTUAL, "true");
+ if (pw_properties_get(props, PW_KEY_MEDIA_CLASS) == NULL)
+ pw_properties_set(props, PW_KEY_MEDIA_CLASS, media_class);
+
+ if ((str = pw_properties_get(props, "stream.props")) != NULL)
+ pw_properties_update_string(impl->stream_props, str, strlen(str));
+
+ copy_props(impl, props, PW_KEY_AUDIO_FORMAT);
+ copy_props(impl, props, PW_KEY_AUDIO_RATE);
+ copy_props(impl, props, PW_KEY_AUDIO_CHANNELS);
+ copy_props(impl, props, SPA_KEY_AUDIO_POSITION);
+ copy_props(impl, props, PW_KEY_NODE_NAME);
+ copy_props(impl, props, PW_KEY_NODE_DESCRIPTION);
+ copy_props(impl, props, PW_KEY_NODE_GROUP);
+ copy_props(impl, props, PW_KEY_NODE_LATENCY);
+ copy_props(impl, props, PW_KEY_NODE_VIRTUAL);
+ copy_props(impl, props, PW_KEY_MEDIA_CLASS);
+ copy_props(impl, props, PW_KEY_TARGET_OBJECT);
+ copy_props(impl, props, "pipe.filename");
+
+ parse_audio_info(impl->stream_props, &impl->info);
+
+ impl->frame_size = calc_frame_size(&impl->info);
+ if (impl->frame_size == 0) {
+ pw_log_error("unsupported audio format:%d channels:%d",
+ impl->info.format, impl->info.channels);
+ res = -EINVAL;
+ goto error;
+ }
+ if (impl->info.rate != 0 &&
+ pw_properties_get(props, PW_KEY_NODE_RATE) == NULL)
+ pw_properties_setf(props, PW_KEY_NODE_RATE,
+ "1/%u", impl->info.rate),
+
+ copy_props(impl, props, PW_KEY_NODE_RATE);
+
+ impl->leftover = calloc(1, impl->frame_size);
+ if (impl->leftover == NULL) {
+ res = -errno;
+ pw_log_error("can't alloc leftover buffer: %m");
+ goto error;
+ }
+
+ impl->core = pw_context_get_object(impl->context, PW_TYPE_INTERFACE_Core);
+ if (impl->core == NULL) {
+ str = pw_properties_get(props, PW_KEY_REMOTE_NAME);
+ impl->core = pw_context_connect(impl->context,
+ pw_properties_new(
+ PW_KEY_REMOTE_NAME, str,
+ NULL),
+ 0);
+ impl->do_disconnect = true;
+ }
+ if (impl->core == NULL) {
+ res = -errno;
+ pw_log_error("can't connect: %m");
+ goto error;
+ }
+
+ pw_proxy_add_listener((struct pw_proxy*)impl->core,
+ &impl->core_proxy_listener,
+ &core_proxy_events, impl);
+ pw_core_add_listener(impl->core,
+ &impl->core_listener,
+ &core_events, impl);
+
+ if ((res = create_fifo(impl)) < 0)
+ goto error;
+
+ if ((res = create_stream(impl)) < 0)
+ goto error;
+
+ pw_impl_module_add_listener(module, &impl->module_listener, &module_events, impl);
+
+ pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_props));
+
+ return 0;
+
+error:
+ impl_destroy(impl);
+ return res;
+}
diff --git a/src/modules/module-portal.c b/src/modules/module-portal.c
new file mode 100644
index 0000000..ee5aa73
--- /dev/null
+++ b/src/modules/module-portal.c
@@ -0,0 +1,348 @@
+/* PipeWire
+ *
+ * Copyright © 2016 Wim Taymans <wim.taymans@gmail.com>
+ * Copyright © 2019 Red Hat Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+
+#include "config.h"
+
+#include <dbus/dbus.h>
+
+#include <spa/utils/string.h>
+#include <spa/utils/result.h>
+#include <spa/support/dbus.h>
+
+#include "pipewire/context.h"
+#include "pipewire/impl-client.h"
+#include "pipewire/log.h"
+#include "pipewire/module.h"
+#include "pipewire/utils.h"
+
+/** \page page_module_portal PipeWire Module: Portal
+ *
+ * The `portal` module performs access control management for clients started
+ * inside an XDG portal.
+ *
+ * The module connects to the session DBus and subscribes to
+ * `NameOwnerChanged` signals for the `org.freedesktop.portal.Desktop` name.
+ * The PID of the DBus name owner is the portal.
+ *
+ * A client connection from the portal PID to PipeWire gets assigned a \ref
+ * PW_KEY_ACCESS of `"portal"` and set to permissions ALL - it is the
+ * responsibility of the portal to limit the permissions before passing the
+ * connection on to the client. See \ref page_access for details on
+ * permissions.
+ *
+ * Clients connecting from other PIDs are ignored by this module.
+ *
+ * ## Module Options
+ *
+ * There are no module-specific options.
+ *
+ * ## General options
+ *
+ * There are no general options for this module.
+ *
+ * ## Example configuration
+ *\code{.unparsed}
+ * context.modules = [
+ * { name = libpipewire-portal }
+ * ]
+ *\endcode
+ *
+ */
+
+#define NAME "portal"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+struct impl {
+ struct pw_context *context;
+ struct pw_properties *properties;
+
+ struct spa_dbus_connection *conn;
+ DBusConnection *bus;
+
+ struct spa_hook context_listener;
+ struct spa_hook module_listener;
+
+ DBusPendingCall *portal_pid_pending;
+ pid_t portal_pid;
+};
+
+static void
+context_check_access(void *data, struct pw_impl_client *client)
+{
+ struct impl *impl = data;
+ const struct pw_properties *props;
+ struct pw_permission permissions[1];
+ struct spa_dict_item items[1];
+ pid_t pid;
+
+ if (impl->portal_pid == 0)
+ return;
+
+ if ((props = pw_impl_client_get_properties(client)) == NULL)
+ return;
+
+ if (pw_properties_fetch_int32(props, PW_KEY_SEC_PID, &pid) < 0)
+ return;
+
+ if (pid != impl->portal_pid)
+ return;
+
+ items[0] = SPA_DICT_ITEM_INIT(PW_KEY_ACCESS, "portal");
+ pw_impl_client_update_properties(client, &SPA_DICT_INIT(items, 1));
+
+ pw_log_info("%p: portal managed client %p added", impl, client);
+
+ /* portal makes this connection and will change the permissions before
+ * handing this connection to the client */
+ permissions[0] = PW_PERMISSION_INIT(PW_ID_ANY, PW_PERM_ALL);
+ pw_impl_client_update_permissions(client, 1, permissions);
+ return;
+}
+
+static const struct pw_context_events context_events = {
+ PW_VERSION_CONTEXT_EVENTS,
+ .check_access = context_check_access,
+};
+
+static void module_destroy(void *data)
+{
+ struct impl *impl = data;
+
+ spa_hook_remove(&impl->context_listener);
+ spa_hook_remove(&impl->module_listener);
+
+ if (impl->bus)
+ dbus_connection_unref(impl->bus);
+ spa_dbus_connection_destroy(impl->conn);
+
+ pw_properties_free(impl->properties);
+
+ free(impl);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy,
+};
+
+static void on_portal_pid_received(DBusPendingCall *pending,
+ void *user_data)
+{
+ struct impl *impl = user_data;
+ DBusMessage *m;
+ DBusError error;
+ uint32_t portal_pid = 0;
+
+ m = dbus_pending_call_steal_reply(pending);
+ dbus_pending_call_unref(pending);
+ impl->portal_pid_pending = NULL;
+
+ if (!m) {
+ pw_log_error("Failed to receive portal pid");
+ return;
+ }
+ if (dbus_message_is_error(m, DBUS_ERROR_NAME_HAS_NO_OWNER)) {
+ pw_log_info("Portal is not running");
+ return;
+ }
+ if (dbus_message_get_type(m) == DBUS_MESSAGE_TYPE_ERROR) {
+ const char *message = "unknown";
+ dbus_message_get_args(m, NULL, DBUS_TYPE_STRING, &message, DBUS_TYPE_INVALID);
+ pw_log_warn("Failed to receive portal pid: %s: %s",
+ dbus_message_get_error_name(m), message);
+ return;
+ }
+
+ dbus_error_init(&error);
+ dbus_message_get_args(m, &error, DBUS_TYPE_UINT32, &portal_pid,
+ DBUS_TYPE_INVALID);
+ dbus_message_unref(m);
+
+ if (dbus_error_is_set(&error)) {
+ impl->portal_pid = 0;
+ pw_log_warn("Could not get portal pid: %s", error.message);
+ dbus_error_free(&error);
+ } else {
+ pw_log_info("Got portal pid %d", portal_pid);
+ impl->portal_pid = portal_pid;
+ }
+}
+
+static void update_portal_pid(struct impl *impl)
+{
+ DBusMessage *m;
+ const char *name;
+ DBusPendingCall *pending;
+
+ impl->portal_pid = 0;
+
+ m = dbus_message_new_method_call("org.freedesktop.DBus",
+ "/org/freedesktop/DBus",
+ "org.freedesktop.DBus",
+ "GetConnectionUnixProcessID");
+
+ name = "org.freedesktop.portal.Desktop";
+ dbus_message_append_args(m,
+ DBUS_TYPE_STRING, &name,
+ DBUS_TYPE_INVALID);
+
+ dbus_connection_send_with_reply(impl->bus, m, &pending, -1);
+ dbus_pending_call_set_notify(pending, on_portal_pid_received, impl, NULL);
+ if (impl->portal_pid_pending != NULL) {
+ dbus_pending_call_cancel(impl->portal_pid_pending);
+ dbus_pending_call_unref(impl->portal_pid_pending);
+ }
+ impl->portal_pid_pending = pending;
+}
+
+static DBusHandlerResult name_owner_changed_handler(DBusConnection *connection,
+ DBusMessage *message,
+ void *user_data)
+{
+ struct impl *impl = user_data;
+ const char *name;
+ const char *old_owner;
+ const char *new_owner;
+
+ if (!dbus_message_is_signal(message, "org.freedesktop.DBus",
+ "NameOwnerChanged"))
+ return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+
+ if (!dbus_message_get_args(message, NULL,
+ DBUS_TYPE_STRING, &name,
+ DBUS_TYPE_STRING, &old_owner,
+ DBUS_TYPE_STRING, &new_owner,
+ DBUS_TYPE_INVALID)) {
+ pw_log_error("Failed to get OwnerChanged args");
+ return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+ }
+
+ if (!spa_streq(name, "org.freedesktop.portal.Desktop"))
+ return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+
+ if (spa_streq(new_owner, "")) {
+ impl->portal_pid = 0;
+ if (impl->portal_pid_pending != NULL) {
+ dbus_pending_call_cancel(impl->portal_pid_pending);
+ dbus_pending_call_unref(impl->portal_pid_pending);
+ }
+ }
+ else {
+ update_portal_pid(impl);
+ }
+
+ return DBUS_HANDLER_RESULT_HANDLED;
+}
+
+static int init_dbus_connection(struct impl *impl)
+{
+ DBusError error;
+
+ impl->bus = spa_dbus_connection_get(impl->conn);
+ if (impl->bus == NULL)
+ return -EIO;
+
+ /* XXX: we don't handle dbus reconnection yet, so ref the handle instead */
+ dbus_connection_ref(impl->bus);
+
+ dbus_error_init(&error);
+
+ dbus_bus_add_match(impl->bus,
+ "type='signal',\
+ sender='org.freedesktop.DBus',\
+ interface='org.freedesktop.DBus',\
+ member='NameOwnerChanged'",
+ &error);
+ if (dbus_error_is_set(&error)) {
+ pw_log_error("Failed to add name owner changed listener: %s",
+ error.message);
+ dbus_error_free(&error);
+ return -EIO;
+ }
+
+ dbus_connection_add_filter(impl->bus, name_owner_changed_handler,
+ impl, NULL);
+
+ update_portal_pid(impl);
+
+ return 0;
+}
+
+SPA_EXPORT
+int pipewire__module_init(struct pw_impl_module *module, const char *args)
+{
+ struct pw_context *context = pw_impl_module_get_context(module);
+ struct impl *impl;
+ struct spa_dbus *dbus;
+ const struct spa_support *support;
+ uint32_t n_support;
+ int res;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ support = pw_context_get_support(context, &n_support);
+
+ dbus = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DBus);
+ if (dbus == NULL)
+ return -ENOTSUP;
+
+ impl = calloc(1, sizeof(struct impl));
+ if (impl == NULL)
+ return -ENOMEM;
+
+ pw_log_debug("module %p: new", impl);
+
+ impl->context = context;
+ impl->properties = args ? pw_properties_new_string(args) : NULL;
+
+ impl->conn = spa_dbus_get_connection(dbus, SPA_DBUS_TYPE_SESSION);
+ if (impl->conn == NULL) {
+ res = -errno;
+ goto error;
+ }
+
+ if ((res = init_dbus_connection(impl)) < 0)
+ goto error;
+
+ pw_context_add_listener(context, &impl->context_listener, &context_events, impl);
+ pw_impl_module_add_listener(module, &impl->module_listener, &module_events, impl);
+
+ return 0;
+
+ error:
+ free(impl);
+ pw_log_error("Failed to connect to session bus: %s", spa_strerror(res));
+ return res;
+}
diff --git a/src/modules/module-profiler.c b/src/modules/module-profiler.c
new file mode 100644
index 0000000..e519e08
--- /dev/null
+++ b/src/modules/module-profiler.c
@@ -0,0 +1,463 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+
+#include "config.h"
+
+#include <spa/utils/result.h>
+#include <spa/utils/ringbuffer.h>
+#include <spa/param/profiler.h>
+
+#include <pipewire/private.h>
+#include <pipewire/impl.h>
+#include <pipewire/extensions/profiler.h>
+
+/** \page page_module_profiler PipeWire Module: Profiler
+ *
+ * The profiler module provides a Profiler interface for applications that
+ * can be used to receive profiling information.
+ *
+ * Use tools like pw-top and pw-profiler to collect profiling information
+ * about the pipewire graph.
+ *
+ * ## Example configuration
+ *
+ * The module has no arguments and is usually added to the config file of
+ * the main pipewire daemon.
+ *
+ *\code{.unparsed}
+ * context.modules = [
+ * { name = libpipewire-module-profiler }
+ * ]
+ *\endcode
+ *
+ * ## See also
+ *
+ * - `pw-top`: a tool to display realtime profiler data
+ * - `pw-profiler`: a tool to collect and render profiler data
+ */
+
+#define NAME "profiler"
+
+PW_LOG_TOPIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+#define TMP_BUFFER (16 * 1024)
+#define MAX_BUFFER (8 * 1024 * 1024)
+#define MIN_FLUSH (16 * 1024)
+#define DEFAULT_IDLE 5
+#define DEFAULT_INTERVAL 1
+
+int pw_protocol_native_ext_profiler_init(struct pw_context *context);
+
+#define pw_profiler_resource(r,m,v,...) \
+ pw_resource_call(r,struct pw_profiler_events,m,v,__VA_ARGS__)
+
+#define pw_profiler_resource_profile(r,...) \
+ pw_profiler_resource(r,profile,0,__VA_ARGS__)
+
+static const struct spa_dict_item module_props[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Generate Profiling data" },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+struct impl {
+ struct pw_context *context;
+ struct pw_properties *properties;
+
+ struct spa_hook context_listener;
+ struct spa_hook module_listener;
+
+ struct pw_global *global;
+ struct spa_hook global_listener;
+
+ int64_t count;
+ uint32_t busy;
+ uint32_t empty;
+ struct spa_source *flush_timeout;
+ unsigned int flushing:1;
+ unsigned int listening:1;
+
+ struct spa_ringbuffer buffer;
+ uint8_t tmp[TMP_BUFFER];
+ uint8_t data[MAX_BUFFER];
+
+ uint8_t flush[MAX_BUFFER + sizeof(struct spa_pod_struct)];
+};
+
+struct resource_data {
+ struct impl *impl;
+
+ struct pw_resource *resource;
+ struct spa_hook resource_listener;
+};
+
+static void start_flush(struct impl *impl)
+{
+ struct timespec value, interval;
+
+ value.tv_sec = 0;
+ value.tv_nsec = 1;
+ interval.tv_sec = DEFAULT_INTERVAL;
+ interval.tv_nsec = 0;
+ pw_loop_update_timer(impl->context->main_loop,
+ impl->flush_timeout, &value, &interval, false);
+ impl->flushing = true;
+}
+
+static void stop_flush(struct impl *impl)
+{
+ struct timespec value, interval;
+
+ if (!impl->flushing)
+ return;
+
+ value.tv_sec = 0;
+ value.tv_nsec = 0;
+ interval.tv_sec = 0;
+ interval.tv_nsec = 0;
+ pw_loop_update_timer(impl->context->main_loop,
+ impl->flush_timeout, &value, &interval, false);
+ impl->flushing = false;
+}
+
+static void flush_timeout(void *data, uint64_t expirations)
+{
+ struct impl *impl = data;
+ int32_t avail;
+ uint32_t idx;
+ struct spa_pod_struct *p;
+ struct pw_resource *resource;
+
+ avail = spa_ringbuffer_get_read_index(&impl->buffer, &idx);
+
+ pw_log_trace("%p avail %d", impl, avail);
+
+ if (avail <= 0) {
+ if (++impl->empty == DEFAULT_IDLE)
+ stop_flush(impl);
+ return;
+ }
+ impl->empty = 0;
+
+ p = (struct spa_pod_struct *)impl->flush;
+ *p = SPA_POD_INIT_Struct(avail);
+
+ spa_ringbuffer_read_data(&impl->buffer, impl->data, MAX_BUFFER,
+ idx % MAX_BUFFER,
+ SPA_PTROFF(p, sizeof(struct spa_pod_struct), void), avail);
+ spa_ringbuffer_read_update(&impl->buffer, idx + avail);
+
+ spa_list_for_each(resource, &impl->global->resource_list, link)
+ pw_profiler_resource_profile(resource, &p->pod);
+}
+
+static void context_do_profile(void *data, struct pw_impl_node *node)
+{
+ struct impl *impl = data;
+ struct spa_pod_builder b;
+ struct spa_pod_frame f[2];
+ struct pw_node_activation *a = node->rt.activation;
+ struct spa_io_position *pos = &a->position;
+ struct pw_node_target *t;
+ int32_t filled;
+ uint32_t idx, avail;
+
+ if (SPA_FLAG_IS_SET(pos->clock.flags, SPA_IO_CLOCK_FLAG_FREEWHEEL))
+ return;
+
+ spa_pod_builder_init(&b, impl->tmp, sizeof(impl->tmp));
+ spa_pod_builder_push_object(&b, &f[0],
+ SPA_TYPE_OBJECT_Profiler, 0);
+
+ spa_pod_builder_prop(&b, SPA_PROFILER_info, 0);
+ spa_pod_builder_add_struct(&b,
+ SPA_POD_Long(impl->count),
+ SPA_POD_Float(a->cpu_load[0]),
+ SPA_POD_Float(a->cpu_load[1]),
+ SPA_POD_Float(a->cpu_load[2]),
+ SPA_POD_Int(a->xrun_count));
+
+ spa_pod_builder_prop(&b, SPA_PROFILER_clock, 0);
+ spa_pod_builder_add_struct(&b,
+ SPA_POD_Int(pos->clock.flags),
+ SPA_POD_Int(pos->clock.id),
+ SPA_POD_String(pos->clock.name),
+ SPA_POD_Long(pos->clock.nsec),
+ SPA_POD_Fraction(&pos->clock.rate),
+ SPA_POD_Long(pos->clock.position),
+ SPA_POD_Long(pos->clock.duration),
+ SPA_POD_Long(pos->clock.delay),
+ SPA_POD_Double(pos->clock.rate_diff),
+ SPA_POD_Long(pos->clock.next_nsec));
+
+
+ spa_pod_builder_prop(&b, SPA_PROFILER_driverBlock, 0);
+ spa_pod_builder_add_struct(&b,
+ SPA_POD_Int(node->info.id),
+ SPA_POD_String(node->name),
+ SPA_POD_Long(a->prev_signal_time),
+ SPA_POD_Long(a->signal_time),
+ SPA_POD_Long(a->awake_time),
+ SPA_POD_Long(a->finish_time),
+ SPA_POD_Int(a->status),
+ SPA_POD_Fraction(&node->latency));
+
+ spa_list_for_each(t, &node->rt.target_list, link) {
+ struct pw_impl_node *n = t->node;
+ struct pw_node_activation *na;
+ struct spa_fraction latency;
+
+ if (n == NULL || n == node)
+ continue;
+
+ latency = n->latency;
+ if (n->force_quantum != 0)
+ latency.num = n->force_quantum;
+ if (n->force_rate != 0)
+ latency.denom = n->force_rate;
+ else if (n->rate.denom != 0)
+ latency.denom = n->rate.denom;
+
+ na = n->rt.activation;
+ spa_pod_builder_prop(&b, SPA_PROFILER_followerBlock, 0);
+ spa_pod_builder_add_struct(&b,
+ SPA_POD_Int(n->info.id),
+ SPA_POD_String(n->name),
+ SPA_POD_Long(a->signal_time),
+ SPA_POD_Long(na->signal_time),
+ SPA_POD_Long(na->awake_time),
+ SPA_POD_Long(na->finish_time),
+ SPA_POD_Int(na->status),
+ SPA_POD_Fraction(&latency));
+ }
+ spa_pod_builder_pop(&b, &f[0]);
+
+ if (b.state.offset > sizeof(impl->tmp))
+ goto done;
+
+ filled = spa_ringbuffer_get_write_index(&impl->buffer, &idx);
+ if (filled < 0 || filled > MAX_BUFFER) {
+ pw_log_warn("%p: queue xrun %d", impl, filled);
+ goto done;
+ }
+ avail = MAX_BUFFER - filled;
+ if (avail < b.state.offset) {
+ pw_log_warn("%p: queue full %d < %d", impl, avail, b.state.offset);
+ goto done;
+ }
+ spa_ringbuffer_write_data(&impl->buffer,
+ impl->data, MAX_BUFFER,
+ idx % MAX_BUFFER,
+ b.data, b.state.offset);
+ spa_ringbuffer_write_update(&impl->buffer, idx + b.state.offset);
+
+ if (!impl->flushing || filled + b.state.offset > MIN_FLUSH)
+ start_flush(impl);
+done:
+ impl->count++;
+}
+
+static const struct pw_context_driver_events context_events = {
+ PW_VERSION_CONTEXT_DRIVER_EVENTS,
+ .incomplete = context_do_profile,
+ .complete = context_do_profile,
+};
+
+static int do_stop(struct spa_loop *loop,
+ bool async, uint32_t seq, const void *data, size_t size, void *user_data)
+{
+ struct impl *impl = user_data;
+ spa_hook_remove(&impl->context_listener);
+ return 0;
+}
+
+static void stop_listener(struct impl *impl)
+{
+ if (impl->listening) {
+ pw_loop_invoke(impl->context->data_loop,
+ do_stop, SPA_ID_INVALID, NULL, 0, true, impl);
+ impl->listening = false;
+ }
+}
+
+static void resource_destroy(void *data)
+{
+ struct impl *impl = data;
+ if (--impl->busy == 0) {
+ pw_log_info("%p: stopping profiler", impl);
+ stop_listener(impl);
+ }
+}
+
+static const struct pw_resource_events resource_events = {
+ PW_VERSION_RESOURCE_EVENTS,
+ .destroy = resource_destroy,
+};
+
+static int
+do_start(struct spa_loop *loop,
+ bool async, uint32_t seq, const void *data, size_t size, void *user_data)
+{
+ struct impl *impl = user_data;
+ spa_hook_list_append(&impl->context->driver_listener_list,
+ &impl->context_listener,
+ &context_events, impl);
+ return 0;
+}
+static int
+global_bind(void *object, struct pw_impl_client *client, uint32_t permissions,
+ uint32_t version, uint32_t id)
+{
+ struct impl *impl = object;
+ struct pw_global *global = impl->global;
+ struct pw_resource *resource;
+ struct resource_data *data;
+
+ resource = pw_resource_new(client, id, permissions,
+ PW_TYPE_INTERFACE_Profiler, version, sizeof(*data));
+ if (resource == NULL)
+ return -errno;
+
+ data = pw_resource_get_user_data(resource);
+ data->impl = impl;
+ data->resource = resource;
+ pw_global_add_resource(global, resource);
+
+ pw_resource_add_listener(resource, &data->resource_listener,
+ &resource_events, impl);
+
+ if (++impl->busy == 1) {
+ pw_log_info("%p: starting profiler", impl);
+ pw_loop_invoke(impl->context->data_loop,
+ do_start, SPA_ID_INVALID, NULL, 0, false, impl);
+ impl->listening = true;
+ }
+ return 0;
+}
+
+static void module_destroy(void *data)
+{
+ struct impl *impl = data;
+
+ if (impl->global != NULL)
+ pw_global_destroy(impl->global);
+
+ spa_hook_remove(&impl->module_listener);
+
+ pw_properties_free(impl->properties);
+
+ pw_loop_destroy_source(pw_context_get_main_loop(impl->context), impl->flush_timeout);
+
+ free(impl);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy,
+};
+
+static void global_destroy(void *data)
+{
+ struct impl *impl = data;
+
+ stop_listener(impl);
+ stop_flush(impl);
+
+ spa_hook_remove(&impl->global_listener);
+ impl->global = NULL;
+}
+
+static const struct pw_global_events global_events = {
+ PW_VERSION_GLOBAL_EVENTS,
+ .destroy = global_destroy,
+};
+
+SPA_EXPORT
+int pipewire__module_init(struct pw_impl_module *module, const char *args)
+{
+ struct pw_context *context = pw_impl_module_get_context(module);
+ struct pw_properties *props;
+ struct impl *impl;
+ struct pw_loop *main_loop = pw_context_get_main_loop(context);
+ static const char * const keys[] = {
+ PW_KEY_OBJECT_SERIAL,
+ NULL
+ };
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ impl = calloc(1, sizeof(struct impl));
+ if (impl == NULL)
+ return -errno;
+
+ pw_protocol_native_ext_profiler_init(context);
+
+ pw_log_debug("module %p: new %s", impl, args);
+
+ if (args)
+ props = pw_properties_new_string(args);
+ else
+ props = pw_properties_new(NULL, NULL);
+
+ impl->context = context;
+ impl->properties = props;
+
+ spa_ringbuffer_init(&impl->buffer);
+
+ impl->global = pw_global_new(context,
+ PW_TYPE_INTERFACE_Profiler,
+ PW_VERSION_PROFILER,
+ pw_properties_copy(props),
+ global_bind, impl);
+ if (impl->global == NULL) {
+ free(impl);
+ return -errno;
+ }
+ pw_properties_setf(impl->properties, PW_KEY_OBJECT_ID, "%d", impl->global->id);
+ pw_properties_setf(impl->properties, PW_KEY_OBJECT_SERIAL, "%"PRIu64,
+ pw_global_get_serial(impl->global));
+
+ impl->flush_timeout = pw_loop_add_timer(main_loop, flush_timeout, impl);
+
+ pw_global_update_keys(impl->global, &impl->properties->dict, keys);
+
+ pw_impl_module_add_listener(module, &impl->module_listener, &module_events, impl);
+
+ pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_props));
+
+ pw_global_register(impl->global);
+
+ pw_global_add_listener(impl->global, &impl->global_listener, &global_events, impl);
+
+ return 0;
+}
diff --git a/src/modules/module-profiler/protocol-native.c b/src/modules/module-profiler/protocol-native.c
new file mode 100644
index 0000000..ac0644c
--- /dev/null
+++ b/src/modules/module-profiler/protocol-native.c
@@ -0,0 +1,128 @@
+/* PipeWire
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+
+#include <spa/pod/builder.h>
+#include <spa/pod/parser.h>
+#include <spa/utils/result.h>
+
+#include <pipewire/pipewire.h>
+
+#include <pipewire/extensions/protocol-native.h>
+#include <pipewire/extensions/profiler.h>
+
+PW_LOG_TOPIC_EXTERN(mod_topic);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+static int profiler_proxy_marshal_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct pw_profiler_events *events,
+ void *data)
+{
+ struct pw_proxy *proxy = object;
+ pw_proxy_add_object_listener(proxy, listener, events, data);
+ return 0;
+}
+
+static int profiler_demarshal_add_listener(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ return -ENOTSUP;
+}
+
+static void profiler_resource_marshal_profile(void *object, const struct spa_pod *pod)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource, PW_PROFILER_EVENT_PROFILE, NULL);
+
+ spa_pod_builder_add_struct(b, SPA_POD_Pod(pod));
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static int profiler_proxy_demarshal_profile(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_parser prs;
+ struct spa_pod *pod;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+
+ if (spa_pod_parser_get_struct(&prs, SPA_POD_Pod(&pod)) < 0)
+ return -EINVAL;
+
+ pw_proxy_notify(proxy, struct pw_profiler_events, profile, 0, pod);
+ return 0;
+}
+
+
+static const struct pw_profiler_methods pw_protocol_native_profiler_client_method_marshal = {
+ PW_VERSION_PROFILER_METHODS,
+ .add_listener = &profiler_proxy_marshal_add_listener,
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_profiler_server_method_demarshal[PW_PROFILER_METHOD_NUM] =
+{
+ [PW_PROFILER_METHOD_ADD_LISTENER] = { &profiler_demarshal_add_listener, 0 },
+};
+
+static const struct pw_profiler_events pw_protocol_native_profiler_server_event_marshal = {
+ PW_VERSION_PROFILER_EVENTS,
+ .profile = &profiler_resource_marshal_profile,
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_profiler_client_event_demarshal[PW_PROFILER_EVENT_NUM] =
+{
+ [PW_PROFILER_EVENT_PROFILE] = { &profiler_proxy_demarshal_profile, 0 },
+};
+
+static const struct pw_protocol_marshal pw_protocol_native_profiler_marshal = {
+ PW_TYPE_INTERFACE_Profiler,
+ PW_VERSION_PROFILER,
+ 0,
+ PW_PROFILER_METHOD_NUM,
+ PW_PROFILER_EVENT_NUM,
+ .client_marshal = &pw_protocol_native_profiler_client_method_marshal,
+ .server_demarshal = pw_protocol_native_profiler_server_method_demarshal,
+ .server_marshal = &pw_protocol_native_profiler_server_event_marshal,
+ .client_demarshal = pw_protocol_native_profiler_client_event_demarshal,
+};
+
+int pw_protocol_native_ext_profiler_init(struct pw_context *context)
+{
+ struct pw_protocol *protocol;
+
+ protocol = pw_context_find_protocol(context, PW_TYPE_INFO_PROTOCOL_Native);
+ if (protocol == NULL)
+ return -EPROTO;
+
+ pw_protocol_add_marshal(protocol, &pw_protocol_native_profiler_marshal);
+ return 0;
+}
diff --git a/src/modules/module-protocol-native.c b/src/modules/module-protocol-native.c
new file mode 100644
index 0000000..6fdc79b
--- /dev/null
+++ b/src/modules/module-protocol-native.c
@@ -0,0 +1,1528 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "config.h"
+
+#include <string.h>
+#include <stdio.h>
+#include <unistd.h>
+#include <errno.h>
+#include <sys/socket.h>
+#include <sys/types.h>
+#include <sys/un.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <sys/file.h>
+#include <ctype.h>
+#ifdef HAVE_PWD_H
+#include <pwd.h>
+#endif
+#if defined(__FreeBSD__) || defined(__MidnightBSD__)
+#include <sys/ucred.h>
+#endif
+
+#include <spa/pod/iter.h>
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+
+#ifdef HAVE_SYSTEMD
+#include <systemd/sd-daemon.h>
+#endif
+
+#include <pipewire/impl.h>
+#include <pipewire/extensions/protocol-native.h>
+
+#include "pipewire/private.h"
+
+#include "modules/module-protocol-native/connection.h"
+#include "modules/module-protocol-native/defs.h"
+#include "modules/module-protocol-native/protocol-footer.h"
+
+
+#define NAME "protocol-native"
+PW_LOG_TOPIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+PW_LOG_TOPIC(mod_topic_connection, "conn." NAME);
+
+#undef spa_debug
+#define spa_debug(...) pw_logt_debug(mod_topic_connection, __VA_ARGS__)
+
+#include <spa/debug/pod.h>
+#include <spa/debug/types.h>
+
+/** \page page_module_protocol_native PipeWire Module: Protocol Native
+ *
+ * The native protocol module implements the PipeWire communication between
+ * a client and a server using unix local sockets.
+ *
+ * Normally this module is loaded in both client and server config files
+ * so that they cam communicate.
+ *
+ * ## Module Options
+ *
+ * The module has no options.
+ *
+ * ## General Options
+ *
+ * The name of the core is obtained as:
+ *
+ * - PIPEWIRE_CORE : the environment variable with the name of the core
+ * - \ref PW_KEY_CORE_NAME : in the context properties
+ * - a name based on the process id
+ *
+ * The context will also become a server if:
+ *
+ * - PIPEWIRE_DAEMON : the environment is true
+ * - \ref PW_KEY_CORE_DAEMON : in the context properties is true
+ *
+ * The socket will be located in the directory obtained by looking at the
+ * following environment variables:
+ *
+ * - PIPEWIRE_RUNTIME_DIR
+ * - XDG_RUNTIME_DIR
+ * - USERPROFILE
+ *
+ * The socket address will be written into the notification file descriptor
+ * if the following environment variable is set:
+ *
+ * - PIPEWIRE_NOTIFICATION_FD
+ *
+ * When a client connect, the connection will be made to:
+ *
+ * - PIPEWIRE_REMOTE : the environment with the remote name
+ * - \ref PW_KEY_REMOTE_NAME : the property in the context.
+ * - The default remote named "pipewire-0"
+ *
+ * A Special remote named "internal" can be used to make a connection to the
+ * local context. This can be done even when the server is not a daemon. It can
+ * be used to treat a local context as if it was a server.
+ *
+ * ## Example configuration
+ *
+ *\code{.unparsed}
+ * context.modules = [
+ { name = libpipewire-module-protocol-native }
+ * ]
+ *\endcode
+ */
+
+#ifndef UNIX_PATH_MAX
+#define UNIX_PATH_MAX 108
+#endif
+
+static const struct spa_dict_item module_props[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Native protocol using unix sockets" },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+/* Required for s390x */
+#ifndef SO_PEERSEC
+#define SO_PEERSEC 31
+#endif
+
+static bool debug_messages = 0;
+
+#define LOCK_SUFFIX ".lock"
+#define LOCK_SUFFIXLEN 5
+
+void pw_protocol_native_init(struct pw_protocol *protocol);
+void pw_protocol_native0_init(struct pw_protocol *protocol);
+
+struct protocol_data {
+ struct pw_impl_module *module;
+ struct spa_hook module_listener;
+ struct pw_protocol *protocol;
+
+ struct server *local;
+};
+
+struct client {
+ struct pw_protocol_client this;
+ struct pw_context *context;
+
+ struct spa_source *source;
+
+ struct pw_protocol_native_connection *connection;
+ struct spa_hook conn_listener;
+
+ int ref;
+
+ struct footer_core_global_state footer_state;
+
+ unsigned int connected:1;
+ unsigned int disconnecting:1;
+ unsigned int need_flush:1;
+ unsigned int paused:1;
+};
+
+static void client_unref(struct client *impl)
+{
+ if (--impl->ref == 0)
+ free(impl);
+}
+
+struct server {
+ struct pw_protocol_server this;
+
+ int fd_lock;
+ struct sockaddr_un addr;
+ char lock_addr[UNIX_PATH_MAX + LOCK_SUFFIXLEN];
+
+ struct pw_loop *loop;
+ struct spa_source *source;
+ struct spa_source *resume;
+ unsigned int activated:1;
+};
+
+struct client_data {
+ struct pw_impl_client *client;
+ struct spa_hook client_listener;
+
+ struct spa_list protocol_link;
+ struct server *server;
+
+ struct spa_source *source;
+ struct pw_protocol_native_connection *connection;
+ struct spa_hook conn_listener;
+
+ struct footer_client_global_state footer_state;
+
+ unsigned int busy:1;
+ unsigned int need_flush:1;
+
+ struct protocol_compat_v2 compat_v2;
+};
+
+static void debug_msg(const char *prefix, const struct pw_protocol_native_message *msg, bool hex)
+{
+ struct spa_pod *pod;
+ pw_logt_debug(mod_topic_connection,
+ "%s: id:%d op:%d size:%d seq:%d", prefix,
+ msg->id, msg->opcode, msg->size, msg->seq);
+
+ if ((pod = get_first_pod_from_data(msg->data, msg->size, 0)) != NULL)
+ spa_debug_pod(0, NULL, pod);
+ else
+ hex = true;
+ if (hex)
+ spa_debug_mem(0, msg->data, msg->size);
+
+ pw_logt_debug(mod_topic_connection, "%s ****", prefix);
+
+}
+
+static void pre_demarshal(struct pw_protocol_native_connection *conn,
+ const struct pw_protocol_native_message *msg,
+ void *object, const struct footer_demarshal *opcodes, size_t n_opcodes)
+{
+ struct spa_pod *footer = NULL;
+ struct spa_pod_parser parser;
+ struct spa_pod_frame f[2];
+ uint32_t opcode;
+ int ret;
+
+ footer = pw_protocol_native_connection_get_footer(conn, msg);
+ if (footer == NULL)
+ return; /* No valid footer. Ignore silently. */
+
+ /*
+ * Version 3 footer
+ *
+ * spa_pod Struct { [Id opcode, Struct { ... }]* }
+ */
+
+ spa_pod_parser_pod(&parser, footer);
+ if (spa_pod_parser_push_struct(&parser, &f[0]) < 0) {
+ pw_log_error("malformed message footer");
+ return;
+ }
+
+ while (1) {
+ if (spa_pod_parser_get_id(&parser, &opcode) < 0)
+ break;
+ if (spa_pod_parser_push_struct(&parser, &f[1]) < 0)
+ break;
+ if (opcode < n_opcodes) {
+ if ((ret = opcodes[opcode].demarshal(object, &parser)) < 0)
+ pw_log_error("failed processing message footer (opcode %u): %d (%s)",
+ opcode, ret, spa_strerror(ret));
+ } else {
+ /* Ignore (don't log errors), in case we need to extend this later. */
+ pw_log_debug("unknown message footer opcode %u", opcode);
+ }
+ spa_pod_parser_pop(&parser, &f[1]);
+ }
+}
+
+static int
+process_messages(struct client_data *data)
+{
+ struct pw_protocol_native_connection *conn = data->connection;
+ struct pw_impl_client *client = data->client;
+ struct pw_context *context = client->context;
+ const struct pw_protocol_native_message *msg;
+ struct pw_resource *resource;
+ int res;
+
+ context->current_client = client;
+
+ /* when the client is busy processing an async action, stop processing messages
+ * for the client until it finishes the action */
+ while (!data->busy) {
+ const struct pw_protocol_native_demarshal *demarshal;
+ const struct pw_protocol_marshal *marshal;
+ uint32_t permissions, required;
+
+ res = pw_protocol_native_connection_get_next(conn, &msg);
+ if (res < 0) {
+ if (res == -EAGAIN)
+ break;
+ goto error;
+ }
+ if (res == 0)
+ break;
+
+ if (client->core_resource == NULL) {
+ res = -EPROTO;
+ goto error;
+ }
+
+ client->recv_seq = msg->seq;
+
+ pw_log_trace("%p: got message %d from %u", client->protocol,
+ msg->opcode, msg->id);
+
+ if (debug_messages)
+ debug_msg("<<<<<< in", msg, false);
+
+ pre_demarshal(conn, msg, client, footer_client_demarshal,
+ SPA_N_ELEMENTS(footer_client_demarshal));
+
+ resource = pw_impl_client_find_resource(client, msg->id);
+ if (resource == NULL) {
+ pw_resource_errorf(client->core_resource,
+ -ENOENT, "unknown resource %u op:%u", msg->id, msg->opcode);
+ continue;
+ }
+
+ marshal = pw_resource_get_marshal(resource);
+ if (marshal == NULL || msg->opcode >= marshal->n_client_methods) {
+ pw_resource_errorf_id(resource, msg->id,
+ -ENOSYS, "invalid method id:%u op:%u",
+ msg->id, msg->opcode);
+ continue;
+ }
+
+ demarshal = marshal->server_demarshal;
+ if (!demarshal[msg->opcode].func) {
+ pw_resource_errorf_id(resource, msg->id,
+ -ENOTSUP, "function not supported id:%u op:%u",
+ msg->id, msg->opcode);
+ continue;
+ }
+
+ permissions = pw_resource_get_permissions(resource);
+ required = demarshal[msg->opcode].permissions | PW_PERM_X;
+
+ if ((required & permissions) != required) {
+ pw_resource_errorf_id(resource, msg->id,
+ -EACCES, "no permission to call method %u on %u "
+ "(requires "PW_PERMISSION_FORMAT", have "PW_PERMISSION_FORMAT")",
+ msg->opcode, msg->id,
+ PW_PERMISSION_ARGS(required), PW_PERMISSION_ARGS(permissions));
+ continue;
+ }
+
+ resource->refcount++;
+ pw_protocol_native_connection_enter(conn);
+ res = demarshal[msg->opcode].func(resource, msg);
+ pw_protocol_native_connection_leave(conn);
+ pw_resource_unref(resource);
+
+ if (res < 0) {
+ pw_resource_errorf_id(resource, msg->id,
+ res, "invalid message id:%u op:%u (%s)",
+ msg->id, msg->opcode, spa_strerror(res));
+ debug_msg("*invalid message*", msg, true);
+ }
+ }
+ res = 0;
+done:
+ context->current_client = NULL;
+
+ return res;
+
+error:
+ pw_resource_errorf(client->core_resource, res, "client error %d (%s)",
+ res, spa_strerror(res));
+ goto done;
+}
+
+static void
+client_busy_changed(void *data, bool busy)
+{
+ struct client_data *c = data;
+ struct server *s = c->server;
+ struct pw_impl_client *client = c->client;
+ uint32_t mask = c->source->mask;
+
+ c->busy = busy;
+
+ SPA_FLAG_UPDATE(mask, SPA_IO_IN, !busy);
+
+ pw_log_debug("%p: busy changed %d", client->protocol, busy);
+ pw_loop_update_io(client->context->main_loop, c->source, mask);
+
+ if (!busy)
+ pw_loop_signal_event(s->loop, s->resume);
+}
+
+static void handle_client_error(struct pw_impl_client *client, int res, const char *msg)
+{
+ if (res == -EPIPE || res == -ECONNRESET)
+ pw_log_info("%p: %s: client %p disconnected", client->protocol, msg, client);
+ else
+ pw_log_error("%p: %s: client %p error %d (%s)", client->protocol, msg,
+ client, res, spa_strerror(res));
+ if (!client->destroyed)
+ pw_impl_client_destroy(client);
+}
+
+static void
+connection_data(void *data, int fd, uint32_t mask)
+{
+ struct client_data *this = data;
+ struct pw_impl_client *client = this->client;
+ int res;
+
+ client->refcount++;
+
+ if (mask & SPA_IO_HUP) {
+ res = -EPIPE;
+ goto error;
+ }
+ if (mask & SPA_IO_ERR) {
+ res = -EIO;
+ goto error;
+ }
+ if (mask & SPA_IO_IN) {
+ if ((res = process_messages(this)) < 0)
+ goto error;
+ }
+ if (mask & SPA_IO_OUT || this->need_flush) {
+ this->need_flush = false;
+ res = pw_protocol_native_connection_flush(this->connection);
+ if (res >= 0) {
+ pw_loop_update_io(client->context->main_loop,
+ this->source, this->source->mask & ~SPA_IO_OUT);
+ } else if (res != -EAGAIN)
+ goto error;
+ }
+done:
+ pw_impl_client_unref(client);
+ return;
+error:
+ handle_client_error(client, res, "connection_data");
+ goto done;
+}
+
+static void client_destroy(void *data)
+{
+ struct client_data *this = data;
+ pw_log_debug("%p: destroy", this);
+ spa_list_remove(&this->protocol_link);
+}
+
+static void client_free(void *data)
+{
+ struct client_data *this = data;
+ struct pw_impl_client *client = this->client;
+
+ pw_log_debug("%p: free", this);
+ spa_hook_remove(&this->client_listener);
+
+ if (this->source)
+ pw_loop_destroy_source(client->context->main_loop, this->source);
+ if (this->connection)
+ pw_protocol_native_connection_destroy(this->connection);
+
+ pw_map_clear(&this->compat_v2.types);
+}
+
+static const struct pw_impl_client_events client_events = {
+ PW_VERSION_IMPL_CLIENT_EVENTS,
+ .destroy = client_destroy,
+ .free = client_free,
+ .busy_changed = client_busy_changed,
+};
+
+static void on_server_connection_destroy(void *data)
+{
+ struct client_data *this = data;
+ spa_hook_remove(&this->conn_listener);
+}
+
+static void on_start(void *data, uint32_t version)
+{
+ struct client_data *this = data;
+ struct pw_impl_client *client = this->client;
+
+ pw_log_debug("version %d", version);
+
+ if (client->core_resource != NULL)
+ pw_resource_remove(client->core_resource);
+
+ if (pw_global_bind(pw_impl_core_get_global(client->core), client,
+ PW_PERM_ALL, version, 0) < 0)
+ return;
+
+ if (version == 0)
+ client->compat_v2 = &this->compat_v2;
+
+ return;
+}
+
+static void on_server_need_flush(void *data)
+{
+ struct client_data *this = data;
+ struct pw_impl_client *client = this->client;
+
+ pw_log_trace("need flush");
+ this->need_flush = true;
+
+ if (this->source && !(this->source->mask & SPA_IO_OUT)) {
+ pw_loop_update_io(client->context->main_loop,
+ this->source, this->source->mask | SPA_IO_OUT);
+ }
+}
+
+static const struct pw_protocol_native_connection_events server_conn_events = {
+ PW_VERSION_PROTOCOL_NATIVE_CONNECTION_EVENTS,
+ .destroy = on_server_connection_destroy,
+ .start = on_start,
+ .need_flush = on_server_need_flush,
+};
+
+static bool check_print(const uint8_t *buffer, int len)
+{
+ int i;
+ while (len > 1 && buffer[len-1] == 0)
+ len--;
+ for (i = 0; i < len; i++)
+ if (!isprint(buffer[i]))
+ return false;
+ return true;
+}
+
+static struct client_data *client_new(struct server *s, int fd)
+{
+ struct client_data *this;
+ struct pw_impl_client *client;
+ struct pw_protocol *protocol = s->this.protocol;
+ socklen_t len;
+#if defined(__FreeBSD__) || defined(__MidnightBSD__)
+ struct xucred xucred;
+#else
+ struct ucred ucred;
+#endif
+ struct pw_context *context = protocol->context;
+ struct pw_properties *props;
+ uint8_t buffer[1024];
+ struct protocol_data *d = pw_protocol_get_user_data(protocol);
+ int i, res;
+
+ props = pw_properties_new(PW_KEY_PROTOCOL, "protocol-native", NULL);
+ if (props == NULL)
+ goto exit;
+
+#if defined(__linux__)
+ len = sizeof(ucred);
+ if (getsockopt(fd, SOL_SOCKET, SO_PEERCRED, &ucred, &len) < 0) {
+ pw_log_warn("server %p: no peercred: %m", s);
+ } else {
+ pw_properties_setf(props, PW_KEY_SEC_PID, "%d", ucred.pid);
+ pw_properties_setf(props, PW_KEY_SEC_UID, "%d", ucred.uid);
+ pw_properties_setf(props, PW_KEY_SEC_GID, "%d", ucred.gid);
+ }
+
+ len = sizeof(buffer);
+ if (getsockopt(fd, SOL_SOCKET, SO_PEERSEC, buffer, &len) < 0) {
+ if (errno == ENOPROTOOPT)
+ pw_log_info("server %p: security label not available", s);
+ else
+ pw_log_warn("server %p: security label error: %m", s);
+ } else {
+ if (!check_print(buffer, len)) {
+ char *hex, *p;
+ static const char *ch = "0123456789abcdef";
+
+ p = hex = alloca(len * 2 + 10);
+ p += snprintf(p, 5, "hex:");
+ for(i = 0; i < (int)len; i++)
+ p += snprintf(p, 3, "%c%c",
+ ch[buffer[i] >> 4], ch[buffer[i] & 0xf]);
+ pw_properties_set(props, PW_KEY_SEC_LABEL, hex);
+
+ } else {
+ /* buffer is not null terminated, must use length explicitly */
+ pw_properties_setf(props, PW_KEY_SEC_LABEL, "%.*s",
+ (int)len, buffer);
+ }
+ }
+#elif defined(__FreeBSD__) || defined(__MidnightBSD__)
+ len = sizeof(xucred);
+ if (getsockopt(fd, 0, LOCAL_PEERCRED, &xucred, &len) < 0) {
+ pw_log_warn("server %p: no peercred: %m", s);
+ } else {
+#if __FreeBSD__ >= 13
+ pw_properties_setf(props, PW_KEY_SEC_PID, "%d", xucred.cr_pid);
+#endif
+ pw_properties_setf(props, PW_KEY_SEC_UID, "%d", xucred.cr_uid);
+ pw_properties_setf(props, PW_KEY_SEC_GID, "%d", xucred.cr_gid);
+ // this is what Linuxulator does at the moment, see sys/compat/linux/linux_socket.c
+ pw_properties_set(props, PW_KEY_SEC_LABEL, "unconfined");
+ }
+#endif
+
+ pw_properties_setf(props, PW_KEY_MODULE_ID, "%d", d->module->global->id);
+
+ client = pw_context_create_client(s->this.core,
+ protocol, props, sizeof(struct client_data));
+ if (client == NULL)
+ goto exit;
+
+ this = pw_impl_client_get_user_data(client);
+ spa_list_append(&s->this.client_list, &this->protocol_link);
+
+ this->server = s;
+ this->client = client;
+ pw_map_init(&this->compat_v2.types, 0, 32);
+
+ pw_impl_client_add_listener(client, &this->client_listener, &client_events, this);
+
+ this->source = pw_loop_add_io(pw_context_get_main_loop(context),
+ fd, SPA_IO_ERR | SPA_IO_HUP, true,
+ connection_data, this);
+ if (this->source == NULL) {
+ res = -errno;
+ goto cleanup_client;
+ }
+
+ this->connection = pw_protocol_native_connection_new(protocol->context, fd);
+ if (this->connection == NULL) {
+ res = -errno;
+ goto cleanup_client;
+ }
+
+ pw_protocol_native_connection_add_listener(this->connection,
+ &this->conn_listener,
+ &server_conn_events,
+ this);
+
+ if ((res = pw_impl_client_register(client, NULL)) < 0)
+ goto cleanup_client;
+
+ if (!client->busy)
+ pw_loop_update_io(pw_context_get_main_loop(context),
+ this->source, this->source->mask | SPA_IO_IN);
+
+ return this;
+
+cleanup_client:
+ pw_impl_client_destroy(client);
+ errno = -res;
+exit:
+ return NULL;
+}
+
+static const char *
+get_runtime_dir(void)
+{
+ const char *runtime_dir;
+
+ runtime_dir = getenv("PIPEWIRE_RUNTIME_DIR");
+ if (runtime_dir == NULL)
+ runtime_dir = getenv("XDG_RUNTIME_DIR");
+ if (runtime_dir == NULL)
+ runtime_dir = getenv("USERPROFILE");
+ return runtime_dir;
+}
+
+
+static int init_socket_name(struct server *s, const char *name)
+{
+ int name_size;
+ const char *runtime_dir;
+ bool path_is_absolute;
+
+ path_is_absolute = name[0] == '/';
+
+ runtime_dir = get_runtime_dir();
+
+ pw_log_debug("name:%s runtime_dir:%s", name, runtime_dir);
+
+ if (runtime_dir == NULL && !path_is_absolute) {
+ pw_log_error("server %p: name %s is not an absolute path and no runtime dir found. "
+ "Set one of PIPEWIRE_RUNTIME_DIR, XDG_RUNTIME_DIR or "
+ "USERPROFILE in the environment", s, name);
+ return -ENOENT;
+ }
+
+ s->addr.sun_family = AF_LOCAL;
+ if (path_is_absolute)
+ name_size = snprintf(s->addr.sun_path, sizeof(s->addr.sun_path),
+ "%s", name) + 1;
+ else
+ name_size = snprintf(s->addr.sun_path, sizeof(s->addr.sun_path),
+ "%s/%s", runtime_dir, name) + 1;
+
+ if (name_size > (int) sizeof(s->addr.sun_path)) {
+ if (path_is_absolute)
+ pw_log_error("server %p: socket path \"%s\" plus null terminator exceeds %i bytes",
+ s, name, (int) sizeof(s->addr.sun_path));
+ else
+ pw_log_error("server %p: socket path \"%s/%s\" plus null terminator exceeds %i bytes",
+ s, runtime_dir, name, (int) sizeof(s->addr.sun_path));
+ *s->addr.sun_path = 0;
+ return -ENAMETOOLONG;
+ }
+ return 0;
+}
+
+static int lock_socket(struct server *s)
+{
+ int res;
+
+ snprintf(s->lock_addr, sizeof(s->lock_addr), "%s%s", s->addr.sun_path, LOCK_SUFFIX);
+
+ s->fd_lock = open(s->lock_addr, O_CREAT | O_CLOEXEC,
+ (S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP));
+
+ if (s->fd_lock < 0) {
+ res = -errno;
+ pw_log_error("server %p: unable to open lockfile '%s': %m", s, s->lock_addr);
+ goto err;
+ }
+
+ if (flock(s->fd_lock, LOCK_EX | LOCK_NB) < 0) {
+ res = -errno;
+ pw_log_error("server %p: unable to lock lockfile '%s': %m"
+ " (maybe another daemon is running)",
+ s, s->lock_addr);
+ goto err_fd;
+ }
+ return 0;
+
+err_fd:
+ close(s->fd_lock);
+ s->fd_lock = -1;
+err:
+ *s->lock_addr = 0;
+ *s->addr.sun_path = 0;
+ return res;
+}
+
+static void
+socket_data(void *data, int fd, uint32_t mask)
+{
+ struct server *s = data;
+ struct client_data *client;
+ struct sockaddr_un name;
+ socklen_t length;
+ int client_fd;
+
+ length = sizeof(name);
+ client_fd = accept4(fd, (struct sockaddr *) &name, &length, SOCK_CLOEXEC);
+ if (client_fd < 0) {
+ pw_log_error("server %p: failed to accept: %m", s);
+ return;
+ }
+
+ client = client_new(s, client_fd);
+ if (client == NULL) {
+ pw_log_error("server %p: failed to create client", s);
+ close(client_fd);
+ return;
+ }
+}
+
+static int write_socket_address(struct server *s)
+{
+ long v;
+ int fd, res = 0;
+ char *endptr;
+ const char *env = getenv("PIPEWIRE_NOTIFICATION_FD");
+
+ if (env == NULL || env[0] == '\0')
+ return 0;
+
+ errno = 0;
+ v = strtol(env, &endptr, 10);
+ if (endptr[0] != '\0')
+ errno = EINVAL;
+ if (errno != 0) {
+ res = -errno;
+ pw_log_error("server %p: strtol() failed with error: %m", s);
+ goto error;
+ }
+ fd = (int)v;
+ if (v != fd) {
+ res = -ERANGE;
+ pw_log_error("server %p: invalid fd %ld: %s", s, v, spa_strerror(res));
+ goto error;
+ }
+ if (dprintf(fd, "%s\n", s->addr.sun_path) < 0) {
+ res = -errno;
+ pw_log_error("server %p: dprintf() failed with error: %m", s);
+ goto error;
+ }
+ close(fd);
+ unsetenv("PIPEWIRE_NOTIFICATION_FD");
+ return 0;
+
+error:
+ return res;
+}
+
+static int add_socket(struct pw_protocol *protocol, struct server *s)
+{
+ socklen_t size;
+ int fd = -1, res;
+ bool activated = false;
+
+#ifdef HAVE_SYSTEMD
+ {
+ int i, n = sd_listen_fds(0);
+ for (i = 0; i < n; ++i) {
+ if (sd_is_socket_unix(SD_LISTEN_FDS_START + i, SOCK_STREAM,
+ 1, s->addr.sun_path, 0) > 0) {
+ fd = SD_LISTEN_FDS_START + i;
+ activated = true;
+ pw_log_info("server %p: Found socket activation socket for '%s'",
+ s, s->addr.sun_path);
+ break;
+ }
+ }
+ }
+#endif
+
+ if (fd < 0) {
+ struct stat socket_stat;
+
+ if ((fd = socket(PF_LOCAL, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0)) < 0) {
+ res = -errno;
+ goto error;
+ }
+ if (stat(s->addr.sun_path, &socket_stat) < 0) {
+ if (errno != ENOENT) {
+ res = -errno;
+ pw_log_error("server %p: stat %s failed with error: %m",
+ s, s->addr.sun_path);
+ goto error_close;
+ }
+ } else if (socket_stat.st_mode & S_IWUSR || socket_stat.st_mode & S_IWGRP) {
+ unlink(s->addr.sun_path);
+ }
+
+ size = offsetof(struct sockaddr_un, sun_path) + strlen(s->addr.sun_path);
+ if (bind(fd, (struct sockaddr *) &s->addr, size) < 0) {
+ res = -errno;
+ pw_log_error("server %p: bind() failed with error: %m", s);
+ goto error_close;
+ }
+
+ if (listen(fd, 128) < 0) {
+ res = -errno;
+ pw_log_error("server %p: listen() failed with error: %m", s);
+ goto error_close;
+ }
+ }
+
+ res = write_socket_address(s);
+ if (res < 0) {
+ pw_log_error("server %p: failed to write socket address: %s", s,
+ spa_strerror(res));
+ goto error_close;
+ }
+ s->activated = activated;
+ s->loop = pw_context_get_main_loop(protocol->context);
+ if (s->loop == NULL) {
+ res = -errno;
+ goto error_close;
+ }
+ s->source = pw_loop_add_io(s->loop, fd, SPA_IO_IN, true, socket_data, s);
+ if (s->source == NULL) {
+ res = -errno;
+ goto error_close;
+ }
+ return 0;
+
+error_close:
+ close(fd);
+error:
+ return res;
+
+}
+
+static int impl_steal_fd(struct pw_protocol_client *client)
+{
+ struct client *impl = SPA_CONTAINER_OF(client, struct client, this);
+ int fd;
+
+ if (impl->source == NULL)
+ return -EIO;
+
+ fd = fcntl(impl->source->fd, F_DUPFD_CLOEXEC, 3);
+ if (fd < 0)
+ return -errno;
+
+ pw_protocol_client_disconnect(client);
+ return fd;
+}
+
+static int
+process_remote(struct client *impl)
+{
+ const struct pw_protocol_native_message *msg;
+ struct pw_protocol_native_connection *conn = impl->connection;
+ struct pw_core *this = impl->this.core;
+ int res = 0;
+
+ impl->ref++;
+ while (!impl->disconnecting && !impl->paused) {
+ struct pw_proxy *proxy;
+ const struct pw_protocol_native_demarshal *demarshal;
+ const struct pw_protocol_marshal *marshal;
+
+ res = pw_protocol_native_connection_get_next(conn, &msg);
+ if (res < 0) {
+ if (res == -EAGAIN)
+ res = 0;
+ break;
+ }
+ if (res == 0)
+ break;
+
+ pw_log_trace("%p: got message %d from %u seq:%d",
+ this, msg->opcode, msg->id, msg->seq);
+
+ this->recv_seq = msg->seq;
+
+ if (debug_messages)
+ debug_msg("<<<<<< in", msg, false);
+
+ pre_demarshal(conn, msg, this, footer_core_demarshal,
+ SPA_N_ELEMENTS(footer_core_demarshal));
+
+ proxy = pw_core_find_proxy(this, msg->id);
+ if (proxy == NULL || proxy->zombie) {
+ if (proxy == NULL)
+ pw_log_error("%p: could not find proxy %u", this, msg->id);
+ else
+ pw_log_debug("%p: zombie proxy %u", this, msg->id);
+
+ /* FIXME close fds */
+ continue;
+ }
+
+ marshal = pw_proxy_get_marshal(proxy);
+ if (marshal == NULL || msg->opcode >= marshal->n_server_methods) {
+ pw_log_error("%p: invalid method %u for %u (%d)",
+ this, msg->opcode, msg->id,
+ marshal ? marshal->n_server_methods : (uint32_t)-1);
+ continue;
+ }
+
+ demarshal = marshal->client_demarshal;
+ if (!demarshal[msg->opcode].func) {
+ pw_log_error("%p: function %d not implemented on %u",
+ this, msg->opcode, msg->id);
+ continue;
+ }
+ proxy->refcount++;
+ pw_protocol_native_connection_enter(conn);
+ res = demarshal[msg->opcode].func(proxy, msg);
+ pw_protocol_native_connection_leave(conn);
+ pw_proxy_unref(proxy);
+
+ if (res < 0) {
+ pw_log_error("%p: invalid message received %u for %u: %s",
+ this, msg->opcode, msg->id, spa_strerror(res));
+ debug_msg("*invalid*", msg, true);
+ }
+ res = 0;
+ }
+ client_unref(impl);
+ return res;
+}
+
+static void
+on_remote_data(void *data, int fd, uint32_t mask)
+{
+ struct client *impl = data;
+ struct pw_core *this = impl->this.core;
+ struct pw_proxy *core_proxy = (struct pw_proxy*)this;
+ struct pw_protocol_native_connection *conn = impl->connection;
+ struct pw_context *context = pw_core_get_context(this);
+ struct pw_loop *loop = pw_context_get_main_loop(context);
+ int res;
+
+ core_proxy->refcount++;
+ impl->ref++;
+
+ if (mask & (SPA_IO_ERR | SPA_IO_HUP)) {
+ res = -EPIPE;
+ goto error;
+ }
+ if (mask & SPA_IO_IN) {
+ if ((res = process_remote(impl)) < 0)
+ goto error;
+ }
+ if (mask & SPA_IO_OUT || impl->need_flush) {
+ if (!impl->connected) {
+ socklen_t len = sizeof res;
+
+ if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &res, &len) < 0) {
+ res = -errno;
+ pw_log_error("getsockopt: %m");
+ goto error;
+ }
+ if (res != 0) {
+ res = -res;
+ goto error;
+ }
+ impl->connected = true;
+ pw_log_debug("%p: connected, fd %d", impl, fd);
+ }
+ impl->need_flush = false;
+ res = pw_protocol_native_connection_flush(conn);
+ if (res >= 0) {
+ pw_loop_update_io(loop, impl->source,
+ impl->source->mask & ~SPA_IO_OUT);
+ } else if (res != -EAGAIN)
+ goto error;
+ }
+
+done:
+ client_unref(impl);
+ pw_proxy_unref(core_proxy);
+ return;
+error:
+ pw_log_debug("%p: got connection error %d (%s)", impl, res, spa_strerror(res));
+ if (impl->source) {
+ pw_loop_destroy_source(loop, impl->source);
+ impl->source = NULL;
+ }
+ pw_proxy_notify(core_proxy,
+ struct pw_core_events, error, 0, 0,
+ this->recv_seq, res, "connection error");
+ goto done;
+}
+
+static int impl_connect_fd(struct pw_protocol_client *client, int fd, bool do_close)
+{
+ struct client *impl = SPA_CONTAINER_OF(client, struct client, this);
+
+ impl->connected = false;
+ impl->disconnecting = false;
+
+ pw_protocol_native_connection_set_fd(impl->connection, fd);
+ impl->source = pw_loop_add_io(impl->context->main_loop,
+ fd,
+ SPA_IO_IN | SPA_IO_OUT | SPA_IO_HUP | SPA_IO_ERR,
+ do_close, on_remote_data, impl);
+ if (impl->source == NULL)
+ return -errno;
+
+ return 0;
+}
+
+static void impl_disconnect(struct pw_protocol_client *client)
+{
+ struct client *impl = SPA_CONTAINER_OF(client, struct client, this);
+
+ impl->disconnecting = true;
+
+ if (impl->source)
+ pw_loop_destroy_source(impl->context->main_loop, impl->source);
+ impl->source = NULL;
+
+ pw_protocol_native_connection_set_fd(impl->connection, -1);
+}
+
+static void impl_destroy(struct pw_protocol_client *client)
+{
+ struct client *impl = SPA_CONTAINER_OF(client, struct client, this);
+
+ impl_disconnect(client);
+
+ if (impl->connection)
+ pw_protocol_native_connection_destroy(impl->connection);
+ impl->connection = NULL;
+
+ spa_list_remove(&client->link);
+ client_unref(impl);
+}
+
+static int impl_set_paused(struct pw_protocol_client *client, bool paused)
+{
+ struct client *impl = SPA_CONTAINER_OF(client, struct client, this);
+ uint32_t mask;
+
+ if (impl->source == NULL)
+ return -EIO;
+
+ mask = impl->source->mask;
+
+ impl->paused = paused;
+
+ SPA_FLAG_UPDATE(mask, SPA_IO_IN, !paused);
+
+ pw_log_debug("%p: paused %d", client->protocol, paused);
+ pw_loop_update_io(impl->context->main_loop, impl->source, mask);
+
+ return paused ? 0 : process_remote(impl);
+}
+
+static int pw_protocol_native_connect_internal(struct pw_protocol_client *client,
+ const struct spa_dict *props,
+ void (*done_callback) (void *data, int res),
+ void *data)
+{
+ int res, sv[2];
+ struct pw_protocol *protocol = client->protocol;
+ struct protocol_data *d = pw_protocol_get_user_data(protocol);
+ struct server *s = d->local;
+ struct pw_permission permissions[1];
+ struct client_data *c;
+
+ pw_log_debug("server %p: internal connect", s);
+
+ if (socketpair(PF_LOCAL, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0, sv) < 0) {
+ res = -errno;
+ pw_log_error("server %p: socketpair() failed with error: %m", s);
+ goto error;
+ }
+
+ c = client_new(s, sv[0]);
+ if (c == NULL) {
+ res = -errno;
+ pw_log_error("server %p: failed to create client: %m", s);
+ goto error_close;
+ }
+ permissions[0] = PW_PERMISSION_INIT(PW_ID_ANY, PW_PERM_ALL);
+ pw_impl_client_update_permissions(c->client, 1, permissions);
+
+ res = pw_protocol_client_connect_fd(client, sv[1], true);
+done:
+ if (done_callback)
+ done_callback(data, res);
+ return res;
+
+error_close:
+ close(sv[0]);
+ close(sv[1]);
+error:
+ goto done;
+}
+
+static void on_client_connection_destroy(void *data)
+{
+ struct client *impl = data;
+ spa_hook_remove(&impl->conn_listener);
+}
+
+static void on_client_need_flush(void *data)
+{
+ struct client *impl = data;
+
+ pw_log_trace("need flush");
+ impl->need_flush = true;
+
+ if (impl->source && !(impl->source->mask & SPA_IO_OUT)) {
+ pw_loop_update_io(impl->context->main_loop,
+ impl->source, impl->source->mask | SPA_IO_OUT);
+ }
+}
+
+static const struct pw_protocol_native_connection_events client_conn_events = {
+ PW_VERSION_PROTOCOL_NATIVE_CONNECTION_EVENTS,
+ .destroy = on_client_connection_destroy,
+ .need_flush = on_client_need_flush,
+};
+
+static struct pw_protocol_client *
+impl_new_client(struct pw_protocol *protocol,
+ struct pw_core *core,
+ const struct spa_dict *props)
+{
+ struct client *impl;
+ struct pw_protocol_client *this;
+ const char *str = NULL;
+ int res;
+
+ if ((impl = calloc(1, sizeof(struct client))) == NULL)
+ return NULL;
+
+ pw_log_debug("%p: new client %p", protocol, impl);
+
+ this = &impl->this;
+ this->protocol = protocol;
+ this->core = core;
+
+ impl->ref = 1;
+ impl->context = protocol->context;
+ impl->connection = pw_protocol_native_connection_new(protocol->context, -1);
+ if (impl->connection == NULL) {
+ res = -errno;
+ goto error_free;
+ }
+ pw_protocol_native_connection_add_listener(impl->connection,
+ &impl->conn_listener,
+ &client_conn_events,
+ impl);
+
+ if (props) {
+ str = spa_dict_lookup(props, PW_KEY_REMOTE_INTENTION);
+ if (str == NULL &&
+ (str = spa_dict_lookup(props, PW_KEY_REMOTE_NAME)) != NULL &&
+ spa_streq(str, "internal"))
+ str = "internal";
+ }
+ if (str == NULL)
+ str = "generic";
+
+ pw_log_debug("%p: connect %s", protocol, str);
+
+ if (spa_streq(str, "screencast"))
+ this->connect = pw_protocol_native_connect_portal_screencast;
+ else if (spa_streq(str, "internal"))
+ this->connect = pw_protocol_native_connect_internal;
+ else
+ this->connect = pw_protocol_native_connect_local_socket;
+
+ this->steal_fd = impl_steal_fd;
+ this->connect_fd = impl_connect_fd;
+ this->disconnect = impl_disconnect;
+ this->destroy = impl_destroy;
+ this->set_paused = impl_set_paused;
+
+ spa_list_append(&protocol->client_list, &this->link);
+
+ return this;
+
+error_free:
+ free(impl);
+ errno = -res;
+ return NULL;
+}
+
+static void destroy_server(struct pw_protocol_server *server)
+{
+ struct server *s = SPA_CONTAINER_OF(server, struct server, this);
+ struct client_data *data, *tmp;
+
+ pw_log_debug("%p: server %p", s->this.protocol, s);
+
+ spa_list_remove(&server->link);
+
+ spa_list_for_each_safe(data, tmp, &server->client_list, protocol_link)
+ pw_impl_client_destroy(data->client);
+
+ if (s->source)
+ pw_loop_destroy_source(s->loop, s->source);
+ if (s->resume)
+ pw_loop_destroy_source(s->loop, s->resume);
+ if (s->addr.sun_path[0] && !s->activated)
+ unlink(s->addr.sun_path);
+ if (s->lock_addr[0])
+ unlink(s->lock_addr);
+ if (s->fd_lock != -1)
+ close(s->fd_lock);
+ free(s);
+}
+
+static void do_resume(void *_data, uint64_t count)
+{
+ struct server *server = _data;
+ struct pw_protocol_server *this = &server->this;
+ struct client_data *data, *tmp;
+ int res;
+
+ pw_log_debug("flush");
+
+ spa_list_for_each_safe(data, tmp, &this->client_list, protocol_link) {
+ data->client->refcount++;
+ if ((res = process_messages(data)) < 0)
+ handle_client_error(data->client, res, "do_resume");
+ pw_impl_client_unref(data->client);
+ }
+ return;
+}
+
+static const char *
+get_server_name(const struct spa_dict *props)
+{
+ const char *name = NULL;
+
+ name = getenv("PIPEWIRE_CORE");
+ if (name == NULL && props != NULL)
+ name = spa_dict_lookup(props, PW_KEY_CORE_NAME);
+ if (name == NULL)
+ name = PW_DEFAULT_REMOTE;
+ return name;
+}
+
+static struct server *
+create_server(struct pw_protocol *protocol,
+ struct pw_impl_core *core,
+ const struct spa_dict *props)
+{
+ struct pw_protocol_server *this;
+ struct server *s;
+
+ if ((s = calloc(1, sizeof(struct server))) == NULL)
+ return NULL;
+
+ s->fd_lock = -1;
+
+ this = &s->this;
+ this->protocol = protocol;
+ this->core = core;
+ spa_list_init(&this->client_list);
+ this->destroy = destroy_server;
+
+ spa_list_append(&protocol->server_list, &this->link);
+
+ pw_log_debug("%p: created server %p", protocol, this);
+
+ return s;
+}
+
+static struct pw_protocol_server *
+impl_add_server(struct pw_protocol *protocol,
+ struct pw_impl_core *core,
+ const struct spa_dict *props)
+{
+ struct pw_protocol_server *this;
+ struct server *s;
+ const char *name;
+ int res;
+
+ if ((s = create_server(protocol, core, props)) == NULL)
+ return NULL;
+
+ this = &s->this;
+
+ name = get_server_name(props);
+
+ if ((res = init_socket_name(s, name)) < 0)
+ goto error;
+
+ if ((res = lock_socket(s)) < 0)
+ goto error;
+
+ if ((res = add_socket(protocol, s)) < 0)
+ goto error;
+
+ if ((s->resume = pw_loop_add_event(s->loop, do_resume, s)) == NULL)
+ goto error;
+
+ pw_log_info("%p: Listening on '%s'", protocol, name);
+
+ return this;
+
+error:
+ destroy_server(this);
+ errno = -res;
+ return NULL;
+}
+
+static const struct pw_protocol_implementation protocol_impl = {
+ PW_VERSION_PROTOCOL_IMPLEMENTATION,
+ .new_client = impl_new_client,
+ .add_server = impl_add_server,
+};
+
+static struct spa_pod_builder *
+impl_ext_begin_proxy(struct pw_proxy *proxy, uint8_t opcode, struct pw_protocol_native_message **msg)
+{
+ struct client *impl = SPA_CONTAINER_OF(proxy->core->conn, struct client, this);
+ return pw_protocol_native_connection_begin(impl->connection, proxy->id, opcode, msg);
+}
+
+static uint32_t impl_ext_add_proxy_fd(struct pw_proxy *proxy, int fd)
+{
+ struct client *impl = SPA_CONTAINER_OF(proxy->core->conn, struct client, this);
+ return pw_protocol_native_connection_add_fd(impl->connection, fd);
+}
+
+static int impl_ext_get_proxy_fd(struct pw_proxy *proxy, uint32_t index)
+{
+ struct client *impl = SPA_CONTAINER_OF(proxy->core->conn, struct client, this);
+ return pw_protocol_native_connection_get_fd(impl->connection, index);
+}
+
+static void assert_single_pod(struct spa_pod_builder *builder)
+{
+ /*
+ * Check the invariant that the message we just marshaled
+ * consists of at most one POD.
+ */
+ struct spa_pod *pod = builder->data;
+ spa_assert(builder->data == NULL ||
+ builder->state.offset < sizeof(struct spa_pod) ||
+ builder->state.offset == SPA_POD_SIZE(pod));
+}
+
+static int impl_ext_end_proxy(struct pw_proxy *proxy,
+ struct spa_pod_builder *builder)
+{
+ struct pw_core *core = proxy->core;
+ struct client *impl = SPA_CONTAINER_OF(core->conn, struct client, this);
+ assert_single_pod(builder);
+ marshal_core_footers(&impl->footer_state, core, builder);
+ return core->send_seq = pw_protocol_native_connection_end(impl->connection, builder);
+}
+
+static struct spa_pod_builder *
+impl_ext_begin_resource(struct pw_resource *resource,
+ uint8_t opcode, struct pw_protocol_native_message **msg)
+{
+ struct client_data *data = resource->client->user_data;
+ return pw_protocol_native_connection_begin(data->connection, resource->id, opcode, msg);
+}
+
+static uint32_t impl_ext_add_resource_fd(struct pw_resource *resource, int fd)
+{
+ struct client_data *data = resource->client->user_data;
+ return pw_protocol_native_connection_add_fd(data->connection, fd);
+}
+static int impl_ext_get_resource_fd(struct pw_resource *resource, uint32_t index)
+{
+ struct client_data *data = resource->client->user_data;
+ return pw_protocol_native_connection_get_fd(data->connection, index);
+}
+
+static int impl_ext_end_resource(struct pw_resource *resource,
+ struct spa_pod_builder *builder)
+{
+ struct client_data *data = resource->client->user_data;
+ struct pw_impl_client *client = resource->client;
+ assert_single_pod(builder);
+ marshal_client_footers(&data->footer_state, client, builder);
+ return client->send_seq = pw_protocol_native_connection_end(data->connection, builder);
+}
+static const struct pw_protocol_native_ext protocol_ext_impl = {
+ PW_VERSION_PROTOCOL_NATIVE_EXT,
+ .begin_proxy = impl_ext_begin_proxy,
+ .add_proxy_fd = impl_ext_add_proxy_fd,
+ .get_proxy_fd = impl_ext_get_proxy_fd,
+ .end_proxy = impl_ext_end_proxy,
+ .begin_resource = impl_ext_begin_resource,
+ .add_resource_fd = impl_ext_add_resource_fd,
+ .get_resource_fd = impl_ext_get_resource_fd,
+ .end_resource = impl_ext_end_resource,
+};
+
+static void module_destroy(void *data)
+{
+ struct protocol_data *d = data;
+
+ spa_hook_remove(&d->module_listener);
+
+ pw_protocol_destroy(d->protocol);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy,
+};
+
+static int need_server(struct pw_context *context, const struct spa_dict *props)
+{
+ const char *val = NULL;
+
+ val = getenv("PIPEWIRE_DAEMON");
+ if (val == NULL && props != NULL)
+ val = spa_dict_lookup(props, PW_KEY_CORE_DAEMON);
+ if (val && pw_properties_parse_bool(val))
+ return 1;
+ return 0;
+}
+
+SPA_EXPORT
+int pipewire__module_init(struct pw_impl_module *module, const char *args)
+{
+ struct pw_context *context = pw_impl_module_get_context(module);
+ struct pw_protocol *this;
+ struct protocol_data *d;
+ const struct pw_properties *props;
+ int res;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+ PW_LOG_TOPIC_INIT(mod_topic_connection);
+
+ if (pw_context_find_protocol(context, PW_TYPE_INFO_PROTOCOL_Native) != NULL)
+ return 0;
+
+ this = pw_protocol_new(context, PW_TYPE_INFO_PROTOCOL_Native, sizeof(struct protocol_data));
+ if (this == NULL)
+ return -errno;
+
+ debug_messages = mod_topic_connection->level >= SPA_LOG_LEVEL_DEBUG;
+
+ this->implementation = &protocol_impl;
+ this->extension = &protocol_ext_impl;
+
+ pw_protocol_native_init(this);
+ pw_protocol_native0_init(this);
+
+ pw_log_debug("%p: new debug:%d", this, debug_messages);
+
+ d = pw_protocol_get_user_data(this);
+ d->protocol = this;
+ d->module = module;
+
+ props = pw_context_get_properties(context);
+ d->local = create_server(this, context->core, &props->dict);
+
+ if (need_server(context, &props->dict)) {
+ if (impl_add_server(this, context->core, &props->dict) == NULL) {
+ res = -errno;
+ goto error_cleanup;
+ }
+ }
+
+ pw_impl_module_add_listener(module, &d->module_listener, &module_events, d);
+
+ pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_props));
+
+ return 0;
+
+error_cleanup:
+ pw_protocol_destroy(this);
+ return res;
+}
diff --git a/src/modules/module-protocol-native/connection.c b/src/modules/module-protocol-native/connection.c
new file mode 100644
index 0000000..1ba256c
--- /dev/null
+++ b/src/modules/module-protocol-native/connection.c
@@ -0,0 +1,866 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdint.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <string.h>
+#include <errno.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <sys/socket.h>
+
+#include <spa/utils/result.h>
+#include <spa/pod/builder.h>
+
+#include <pipewire/pipewire.h>
+
+PW_LOG_TOPIC_EXTERN(mod_topic);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+PW_LOG_TOPIC_EXTERN(mod_topic_connection);
+
+#undef spa_debug
+#define spa_debug(...) pw_logt_debug(mod_topic_connection, __VA_ARGS__)
+#include <spa/debug/pod.h>
+
+#include "connection.h"
+#include "defs.h"
+
+#define MAX_BUFFER_SIZE (1024 * 32)
+#define MAX_FDS 1024u
+#define MAX_FDS_MSG 28
+
+#define HDR_SIZE_V0 8
+#define HDR_SIZE 16
+
+struct buffer {
+ uint8_t *buffer_data;
+ size_t buffer_size;
+ size_t buffer_maxsize;
+ int fds[MAX_FDS];
+ uint32_t n_fds;
+
+ uint32_t seq;
+ size_t offset;
+ size_t fds_offset;
+ struct pw_protocol_native_message msg;
+};
+
+struct reenter_item {
+ void *old_buffer_data;
+ struct pw_protocol_native_message return_msg;
+ struct spa_list link;
+};
+
+struct impl {
+ struct pw_protocol_native_connection this;
+ struct pw_context *context;
+
+ struct buffer in, out;
+ struct spa_pod_builder builder;
+
+ struct spa_list reenter_stack;
+ uint32_t pending_reentering;
+
+ uint32_t version;
+ size_t hdr_size;
+};
+
+/** \endcond */
+
+/** Get an fd from a connection
+ *
+ * \param conn the connection
+ * \param index the index of the fd to get
+ * \return the fd at \a index or -ENOENT when no such fd exists
+ *
+ * \memberof pw_protocol_native_connection
+ */
+int pw_protocol_native_connection_get_fd(struct pw_protocol_native_connection *conn, uint32_t index)
+{
+ struct impl *impl = SPA_CONTAINER_OF(conn, struct impl, this);
+ struct buffer *buf = &impl->in;
+
+ if (index == SPA_ID_INVALID)
+ return -1;
+
+ if (index >= buf->msg.n_fds)
+ return -ENOENT;
+
+ return buf->msg.fds[index];
+}
+
+/** Add an fd to a connection
+ *
+ * \param conn the connection
+ * \param fd the fd to add
+ * \return the index of the fd or SPA_IDX_INVALID when an error occurred
+ *
+ * \memberof pw_protocol_native_connection
+ */
+uint32_t pw_protocol_native_connection_add_fd(struct pw_protocol_native_connection *conn, int fd)
+{
+ struct impl *impl = SPA_CONTAINER_OF(conn, struct impl, this);
+ struct buffer *buf = &impl->out;
+ uint32_t index, i;
+
+ if (fd < 0)
+ return SPA_IDX_INVALID;
+
+ for (i = 0; i < buf->msg.n_fds; i++) {
+ if (buf->msg.fds[i] == fd)
+ return i;
+ }
+
+ index = buf->msg.n_fds;
+ if (index + buf->n_fds >= MAX_FDS) {
+ pw_log_error("connection %p: too many fds (%d)", conn, MAX_FDS);
+ return SPA_IDX_INVALID;
+ }
+
+ buf->msg.fds[index] = fcntl(fd, F_DUPFD_CLOEXEC, 0);
+ if (buf->msg.fds[index] == -1) {
+ pw_log_error("connection %p: can't DUP fd:%d %m", conn, fd);
+ return SPA_IDX_INVALID;
+ }
+ buf->msg.n_fds++;
+ pw_log_debug("connection %p: add fd %d (new fd:%d) at index %d",
+ conn, fd, buf->msg.fds[index], index);
+
+ return index;
+}
+
+static void *connection_ensure_size(struct pw_protocol_native_connection *conn, struct buffer *buf, size_t size)
+{
+ int res;
+
+ if (buf->buffer_size + size > buf->buffer_maxsize) {
+ void *np;
+ size_t ns;
+
+ ns = SPA_ROUND_UP_N(buf->buffer_size + size, MAX_BUFFER_SIZE);
+ np = realloc(buf->buffer_data, ns);
+ if (np == NULL) {
+ res = -errno;
+ free(buf->buffer_data);
+ buf->buffer_maxsize = 0;
+ spa_hook_list_call(&conn->listener_list,
+ struct pw_protocol_native_connection_events,
+ error, 0, res);
+ errno = -res;
+ return NULL;
+ }
+ buf->buffer_maxsize = ns;
+ buf->buffer_data = np;
+ pw_log_debug("connection %p: resize buffer to %zd %zd %zd",
+ conn, buf->buffer_size, size, buf->buffer_maxsize);
+ }
+ return (uint8_t *) buf->buffer_data + buf->buffer_size;
+}
+
+static void handle_connection_error(struct pw_protocol_native_connection *conn, int res)
+{
+ if (res == EPIPE || res == ECONNRESET)
+ pw_log_info("connection %p: could not recvmsg on fd:%d: %s", conn, conn->fd, strerror(res));
+ else
+ pw_log_error("connection %p: could not recvmsg on fd:%d: %s", conn, conn->fd, strerror(res));
+}
+
+static size_t cmsg_data_length(const struct cmsghdr *cmsg)
+{
+ const void *begin = CMSG_DATA(cmsg);
+ const void *end = SPA_PTROFF(cmsg, cmsg->cmsg_len, void);
+
+ spa_assert(begin <= end);
+
+ return SPA_PTRDIFF(end, begin);
+}
+
+static void close_all_fds(struct msghdr *msg, struct cmsghdr *from)
+{
+ for (; from != NULL; from = CMSG_NXTHDR(msg, from)) {
+ if (from->cmsg_level != SOL_SOCKET || from->cmsg_type != SCM_RIGHTS)
+ continue;
+
+ size_t n_fds = cmsg_data_length(from) / sizeof(int);
+ for (size_t i = 0; i < n_fds; i++) {
+ const void *p = SPA_PTROFF(CMSG_DATA(from), sizeof(int) * i, void);
+ int fd;
+
+ memcpy(&fd, p, sizeof(fd));
+ pw_log_debug("%p: close fd:%d", msg, fd);
+ close(fd);
+ }
+ }
+}
+
+static int refill_buffer(struct pw_protocol_native_connection *conn, struct buffer *buf)
+{
+ ssize_t len;
+ struct cmsghdr *cmsg = NULL;
+ struct msghdr msg = { 0 };
+ struct iovec iov[1];
+ union {
+ char cmsgbuf[CMSG_SPACE(MAX_FDS_MSG * sizeof(int))];
+ struct cmsghdr align;
+ } cmsgbuf;
+ int n_fds = 0;
+ size_t avail;
+
+ avail = buf->buffer_maxsize - buf->buffer_size;
+
+ iov[0].iov_base = buf->buffer_data + buf->buffer_size;
+ iov[0].iov_len = avail;
+ msg.msg_iov = iov;
+ msg.msg_iovlen = 1;
+ msg.msg_control = &cmsgbuf;
+ msg.msg_controllen = sizeof(cmsgbuf);
+ msg.msg_flags = MSG_CMSG_CLOEXEC | MSG_DONTWAIT;
+
+ while (true) {
+ len = recvmsg(conn->fd, &msg, msg.msg_flags);
+ if (msg.msg_flags & MSG_CTRUNC)
+ goto cmsgs_truncated;
+ if (len == 0 && avail != 0)
+ return -EPIPE;
+ else if (len < 0) {
+ if (errno == EINTR)
+ continue;
+ if (errno != EAGAIN && errno != EWOULDBLOCK)
+ goto recv_error;
+ return -EAGAIN;
+ }
+ break;
+ }
+
+ buf->buffer_size += len;
+
+ /* handle control messages */
+ for (cmsg = CMSG_FIRSTHDR(&msg); cmsg != NULL; cmsg = CMSG_NXTHDR(&msg, cmsg)) {
+ if (cmsg->cmsg_level != SOL_SOCKET || cmsg->cmsg_type != SCM_RIGHTS)
+ continue;
+
+ n_fds = cmsg_data_length(cmsg) / sizeof(int);
+ if (n_fds + buf->n_fds > MAX_FDS)
+ goto too_many_fds;
+ memcpy(&buf->fds[buf->n_fds], CMSG_DATA(cmsg), n_fds * sizeof(int));
+ buf->n_fds += n_fds;
+ }
+ pw_log_trace("connection %p: %d read %zd bytes and %d fds", conn, conn->fd, len,
+ n_fds);
+
+ return 0;
+
+ /* ERRORS */
+recv_error:
+ handle_connection_error(conn, errno);
+ return -errno;
+
+cmsgs_truncated:
+ close_all_fds(&msg, CMSG_FIRSTHDR(&msg));
+ return -EPROTO;
+
+too_many_fds:
+ close_all_fds(&msg, cmsg);
+ return -EPROTO;
+}
+
+static void clear_buffer(struct buffer *buf, bool fds)
+{
+ uint32_t i;
+ if (fds) {
+ for (i = 0; i < buf->n_fds; i++) {
+ pw_log_debug("%p: close fd:%d", buf, buf->fds[i]);
+ close(buf->fds[i]);
+ }
+ }
+ buf->n_fds = 0;
+ buf->buffer_size = 0;
+ buf->offset = 0;
+ buf->fds_offset = 0;
+}
+
+/** Prepare connection for calling from reentered context.
+ *
+ * This ensures that message buffers returned by get_next are not invalidated by additional
+ * calls made after enter. Leave invalidates the buffers at the higher stack level.
+ *
+ * \memberof pw_protocol_native_connection
+ */
+void pw_protocol_native_connection_enter(struct pw_protocol_native_connection *conn)
+{
+ struct impl *impl = SPA_CONTAINER_OF(conn, struct impl, this);
+
+ /* Postpone processing until get_next is actually called */
+ ++impl->pending_reentering;
+}
+
+static void pop_reenter_stack(struct impl *impl, uint32_t count)
+{
+ while (count > 0) {
+ struct reenter_item *item;
+
+ item = spa_list_last(&impl->reenter_stack, struct reenter_item, link);
+ spa_list_remove(&item->link);
+
+ free(item->return_msg.fds);
+ free(item->old_buffer_data);
+ free(item);
+
+ --count;
+ }
+}
+
+void pw_protocol_native_connection_leave(struct pw_protocol_native_connection *conn)
+{
+ struct impl *impl = SPA_CONTAINER_OF(conn, struct impl, this);
+
+ if (impl->pending_reentering > 0) {
+ --impl->pending_reentering;
+ } else {
+ pw_log_trace("connection %p: reenter: pop", impl);
+ pop_reenter_stack(impl, 1);
+ }
+}
+
+static int ensure_stack_level(struct impl *impl, struct pw_protocol_native_message **msg)
+{
+ void *data;
+ struct buffer *buf = &impl->in;
+ struct reenter_item *item, *new_item = NULL;
+
+ item = spa_list_last(&impl->reenter_stack, struct reenter_item, link);
+
+ if (SPA_LIKELY(impl->pending_reentering == 0)) {
+ new_item = item;
+ } else {
+ uint32_t new_count;
+
+ pw_log_trace("connection %p: reenter: push %d levels",
+ impl, impl->pending_reentering);
+
+ /* Append empty item(s) to the reenter stack */
+ for (new_count = 0; new_count < impl->pending_reentering; ++new_count) {
+ new_item = calloc(1, sizeof(struct reenter_item));
+ if (new_item == NULL) {
+ pop_reenter_stack(impl, new_count);
+ return -ENOMEM;
+ }
+ spa_list_append(&impl->reenter_stack, &new_item->link);
+ }
+
+ /*
+ * Stack level increased: we have to switch to a new message data buffer, because
+ * data of returned messages is contained in the buffer and might still be in
+ * use on the lower stack levels.
+ *
+ * We stash the buffer for the previous stack level, and allocate a new one for
+ * the new stack level. If there was a previous buffer for the previous level, we
+ * know its contents are no longer in use (the only active buffer at that stack
+ * level is buf->buffer_data), and we can recycle it as the new buffer (realloc
+ * instead of calloc).
+ *
+ * The current data contained in the buffer needs to be copied to the new buffer.
+ */
+
+ data = realloc(item->old_buffer_data, buf->buffer_maxsize);
+ if (data == NULL) {
+ pop_reenter_stack(impl, new_count);
+ return -ENOMEM;
+ }
+
+ item->old_buffer_data = buf->buffer_data;
+
+ memcpy(data, buf->buffer_data, buf->buffer_size);
+ buf->buffer_data = data;
+
+ impl->pending_reentering = 0;
+ }
+ if (new_item == NULL)
+ return -EIO;
+
+ /* Ensure fds buffer is allocated */
+ if (SPA_UNLIKELY(new_item->return_msg.fds == NULL)) {
+ data = calloc(MAX_FDS, sizeof(int));
+ if (data == NULL)
+ return -ENOMEM;
+ new_item->return_msg.fds = data;
+ }
+
+ *msg = &new_item->return_msg;
+
+ return 0;
+}
+
+/** Make a new connection object for the given socket
+ *
+ * \param fd the socket
+ * \returns a newly allocated connection object
+ *
+ * \memberof pw_protocol_native_connection
+ */
+struct pw_protocol_native_connection *pw_protocol_native_connection_new(struct pw_context *context, int fd)
+{
+ struct impl *impl;
+ struct pw_protocol_native_connection *this;
+ struct reenter_item *reenter_item;
+
+ impl = calloc(1, sizeof(struct impl));
+ if (impl == NULL)
+ return NULL;
+
+ impl->context = context;
+
+ this = &impl->this;
+
+ pw_log_debug("connection %p: new fd:%d", this, fd);
+
+ this->fd = fd;
+ spa_hook_list_init(&this->listener_list);
+
+ impl->hdr_size = HDR_SIZE;
+ impl->version = 3;
+
+ impl->out.buffer_data = calloc(1, MAX_BUFFER_SIZE);
+ impl->out.buffer_maxsize = MAX_BUFFER_SIZE;
+ impl->in.buffer_data = calloc(1, MAX_BUFFER_SIZE);
+ impl->in.buffer_maxsize = MAX_BUFFER_SIZE;
+
+ reenter_item = calloc(1, sizeof(struct reenter_item));
+
+ if (impl->out.buffer_data == NULL || impl->in.buffer_data == NULL || reenter_item == NULL)
+ goto no_mem;
+
+ spa_list_init(&impl->reenter_stack);
+ spa_list_append(&impl->reenter_stack, &reenter_item->link);
+
+ return this;
+
+no_mem:
+ free(impl->out.buffer_data);
+ free(impl->in.buffer_data);
+ free(reenter_item);
+ free(impl);
+ return NULL;
+}
+
+int pw_protocol_native_connection_set_fd(struct pw_protocol_native_connection *conn, int fd)
+{
+ pw_log_debug("connection %p: fd:%d", conn, fd);
+ conn->fd = fd;
+ return 0;
+}
+
+/** Destroy a connection
+ *
+ * \param conn the connection to destroy
+ *
+ * \memberof pw_protocol_native_connection
+ */
+void pw_protocol_native_connection_destroy(struct pw_protocol_native_connection *conn)
+{
+ struct impl *impl = SPA_CONTAINER_OF(conn, struct impl, this);
+
+ pw_log_debug("connection %p: destroy", conn);
+
+ spa_hook_list_call(&conn->listener_list, struct pw_protocol_native_connection_events, destroy, 0);
+
+ spa_hook_list_clean(&conn->listener_list);
+
+ clear_buffer(&impl->out, true);
+ clear_buffer(&impl->in, true);
+ free(impl->out.buffer_data);
+ free(impl->in.buffer_data);
+
+ while (!spa_list_is_empty(&impl->reenter_stack))
+ pop_reenter_stack(impl, 1);
+
+ free(impl);
+}
+
+static int prepare_packet(struct pw_protocol_native_connection *conn, struct buffer *buf)
+{
+ struct impl *impl = SPA_CONTAINER_OF(conn, struct impl, this);
+ uint8_t *data;
+ size_t size, len;
+ uint32_t *p;
+
+ data = buf->buffer_data + buf->offset;
+ size = buf->buffer_size - buf->offset;
+
+ if (size < impl->hdr_size)
+ return impl->hdr_size;
+
+ p = (uint32_t *) data;
+
+ buf->msg.id = p[0];
+ buf->msg.opcode = p[1] >> 24;
+ len = p[1] & 0xffffff;
+
+ if (buf->msg.id == 0 && buf->msg.opcode == 1) {
+ if (p[3] >= 4) {
+ pw_log_warn("old version detected");
+ impl->version = 0;
+ impl->hdr_size = HDR_SIZE_V0;
+ } else {
+ impl->version = 3;
+ impl->hdr_size = HDR_SIZE;
+ }
+ spa_hook_list_call(&conn->listener_list,
+ struct pw_protocol_native_connection_events,
+ start, 0, impl->version);
+ }
+ if (impl->version >= 3) {
+ buf->msg.seq = p[2];
+ buf->msg.n_fds = p[3];
+ } else {
+ buf->msg.seq = 0;
+ buf->msg.n_fds = 0;
+ }
+
+ data += impl->hdr_size;
+ size -= impl->hdr_size;
+ buf->msg.fds = &buf->fds[buf->fds_offset];
+
+ if (buf->msg.n_fds + buf->fds_offset > MAX_FDS)
+ return -EPROTO;
+
+ if (size < len)
+ return len;
+
+ buf->msg.size = len;
+ buf->msg.data = data;
+
+ buf->offset += impl->hdr_size + len;
+ buf->fds_offset += buf->msg.n_fds;
+
+ if (buf->offset >= buf->buffer_size)
+ clear_buffer(buf, false);
+
+ return 0;
+}
+
+/** Move to the next packet in the connection
+ *
+ * \param conn the connection
+ * \param opcode address of result opcode
+ * \param dest_id address of result destination id
+ * \param dt pointer to packet data
+ * \param sz size of packet data
+ * \return true on success
+ *
+ * Get the next packet in \a conn and store the opcode and destination
+ * id as well as the packet data and size.
+ *
+ * \memberof pw_protocol_native_connection
+ */
+int
+pw_protocol_native_connection_get_next(struct pw_protocol_native_connection *conn,
+ const struct pw_protocol_native_message **msg)
+{
+ struct impl *impl = SPA_CONTAINER_OF(conn, struct impl, this);
+ int len, res;
+ struct buffer *buf;
+ struct pw_protocol_native_message *return_msg;
+ int *fds;
+
+ if ((res = ensure_stack_level(impl, &return_msg)) < 0)
+ return res;
+
+ buf = &impl->in;
+
+ while (1) {
+ len = prepare_packet(conn, buf);
+ if (len < 0)
+ return len;
+ if (len == 0)
+ break;
+
+ if (connection_ensure_size(conn, buf, len) == NULL)
+ return -errno;
+ if ((res = refill_buffer(conn, buf)) < 0)
+ return res;
+ }
+
+ /* Returned msg struct should be safe vs. reentering */
+ fds = return_msg->fds;
+ *return_msg = buf->msg;
+ if (buf->msg.n_fds > 0) {
+ memcpy(fds, buf->msg.fds, buf->msg.n_fds * sizeof(int));
+ }
+ return_msg->fds = fds;
+
+ *msg = return_msg;
+
+ return 1;
+}
+
+/** Get footer data from the tail of the current packet.
+ *
+ * \param conn the connection
+ * \param msg current message
+ * \return footer POD, or NULL if no valid footer present
+ *
+ * \memberof pw_protocol_native_connection
+ */
+struct spa_pod *pw_protocol_native_connection_get_footer(struct pw_protocol_native_connection *conn,
+ const struct pw_protocol_native_message *msg)
+{
+ struct impl *impl = SPA_CONTAINER_OF(conn, struct impl, this);
+ struct spa_pod *pod;
+
+ if (impl->version != 3)
+ return NULL;
+
+ /*
+ * Protocol version 3 footer: a single SPA POD
+ */
+
+ /* Footer immediately follows the message POD, if it is present */
+ if ((pod = get_first_pod_from_data(msg->data, msg->size, 0)) == NULL)
+ return NULL;
+ pod = get_first_pod_from_data(msg->data, msg->size, SPA_POD_SIZE(pod));
+ if (pod == NULL)
+ return NULL;
+ pw_log_trace("connection %p: recv message footer, size:%zu",
+ conn, (size_t)SPA_POD_SIZE(pod));
+ return pod;
+}
+
+static inline void *begin_write(struct pw_protocol_native_connection *conn, uint32_t size)
+{
+ struct impl *impl = SPA_CONTAINER_OF(conn, struct impl, this);
+ uint32_t *p;
+ struct buffer *buf = &impl->out;
+ /* header and size for payload */
+ if ((p = connection_ensure_size(conn, buf, impl->hdr_size + size)) == NULL)
+ return NULL;
+
+ return SPA_PTROFF(p, impl->hdr_size, void);
+}
+
+static int builder_overflow(void *data, uint32_t size)
+{
+ struct impl *impl = data;
+ struct spa_pod_builder *b = &impl->builder;
+
+ b->size = SPA_ROUND_UP_N(size, 4096);
+ if ((b->data = begin_write(&impl->this, b->size)) == NULL)
+ return -errno;
+ return 0;
+}
+
+static const struct spa_pod_builder_callbacks builder_callbacks = {
+ SPA_VERSION_POD_BUILDER_CALLBACKS,
+ .overflow = builder_overflow
+};
+
+struct spa_pod_builder *
+pw_protocol_native_connection_begin(struct pw_protocol_native_connection *conn,
+ uint32_t id, uint8_t opcode,
+ struct pw_protocol_native_message **msg)
+{
+ struct impl *impl = SPA_CONTAINER_OF(conn, struct impl, this);
+ struct buffer *buf = &impl->out;
+
+ buf->msg.id = id;
+ buf->msg.opcode = opcode;
+ impl->builder = SPA_POD_BUILDER_INIT(NULL, 0);
+ spa_pod_builder_set_callbacks(&impl->builder, &builder_callbacks, impl);
+ if (impl->version >= 3) {
+ buf->msg.n_fds = 0;
+ buf->msg.fds = &buf->fds[buf->n_fds];
+ } else {
+ buf->msg.n_fds = buf->n_fds;
+ buf->msg.fds = &buf->fds[0];
+ }
+
+ buf->msg.seq = buf->seq;
+ if (msg)
+ *msg = &buf->msg;
+ return &impl->builder;
+}
+
+int
+pw_protocol_native_connection_end(struct pw_protocol_native_connection *conn,
+ struct spa_pod_builder *builder)
+{
+ struct impl *impl = SPA_CONTAINER_OF(conn, struct impl, this);
+ uint32_t *p, size = builder->state.offset;
+ struct buffer *buf = &impl->out;
+ int res;
+
+ if ((p = connection_ensure_size(conn, buf, impl->hdr_size + size)) == NULL)
+ return -errno;
+
+ p[0] = buf->msg.id;
+ p[1] = (buf->msg.opcode << 24) | (size & 0xffffff);
+ if (impl->version >= 3) {
+ p[2] = buf->msg.seq;
+ p[3] = buf->msg.n_fds;
+ }
+
+ buf->buffer_size += impl->hdr_size + size;
+ if (impl->version >= 3)
+ buf->n_fds += buf->msg.n_fds;
+ else
+ buf->n_fds = buf->msg.n_fds;
+
+ if (mod_topic_connection->level >= SPA_LOG_LEVEL_DEBUG) {
+ pw_logt_debug(mod_topic_connection,
+ ">>>>>>>>> out: id:%d op:%d size:%d seq:%d",
+ buf->msg.id, buf->msg.opcode, size, buf->msg.seq);
+ spa_debug_pod(0, NULL, SPA_PTROFF(p, impl->hdr_size, struct spa_pod));
+ pw_logt_debug(mod_topic_connection,
+ ">>>>>>>>> out: done");
+ }
+
+ buf->seq = (buf->seq + 1) & SPA_ASYNC_SEQ_MASK;
+ res = SPA_RESULT_RETURN_ASYNC(buf->msg.seq);
+
+ spa_hook_list_call(&conn->listener_list,
+ struct pw_protocol_native_connection_events, need_flush, 0);
+
+ return res;
+}
+
+/** Flush the connection object
+ *
+ * \param conn the connection object
+ * \return 0 on success < 0 error code on error
+ *
+ * Write the queued messages on the connection to the socket
+ *
+ * \memberof pw_protocol_native_connection
+ */
+int pw_protocol_native_connection_flush(struct pw_protocol_native_connection *conn)
+{
+ struct impl *impl = SPA_CONTAINER_OF(conn, struct impl, this);
+ ssize_t sent, outsize;
+ struct msghdr msg = { 0 };
+ struct iovec iov[1];
+ struct cmsghdr *cmsg;
+ union {
+ char cmsgbuf[CMSG_SPACE(MAX_FDS_MSG * sizeof(int))];
+ struct cmsghdr align;
+ } cmsgbuf;
+ int res = 0, *fds;
+ uint32_t fds_len, to_close, n_fds, outfds, i;
+ struct buffer *buf;
+ void *data;
+ size_t size;
+
+ buf = &impl->out;
+ data = buf->buffer_data;
+ size = buf->buffer_size;
+ fds = buf->fds;
+ n_fds = buf->n_fds;
+ to_close = 0;
+
+ while (size > 0) {
+ if (n_fds > MAX_FDS_MSG) {
+ outfds = MAX_FDS_MSG;
+ outsize = SPA_MIN(sizeof(uint32_t), size);
+ } else {
+ outfds = n_fds;
+ outsize = size;
+ }
+
+ fds_len = outfds * sizeof(int);
+
+ iov[0].iov_base = data;
+ iov[0].iov_len = outsize;
+ msg.msg_iov = iov;
+ msg.msg_iovlen = 1;
+
+ if (outfds > 0) {
+ msg.msg_control = &cmsgbuf;
+ msg.msg_controllen = CMSG_SPACE(fds_len);
+ cmsg = CMSG_FIRSTHDR(&msg);
+ cmsg->cmsg_level = SOL_SOCKET;
+ cmsg->cmsg_type = SCM_RIGHTS;
+ cmsg->cmsg_len = CMSG_LEN(fds_len);
+ memcpy(CMSG_DATA(cmsg), fds, fds_len);
+ msg.msg_controllen = cmsg->cmsg_len;
+ } else {
+ msg.msg_control = NULL;
+ msg.msg_controllen = 0;
+ }
+
+ while (true) {
+ sent = sendmsg(conn->fd, &msg, MSG_NOSIGNAL | MSG_DONTWAIT);
+ if (sent < 0) {
+ if (errno == EINTR)
+ continue;
+ else {
+ res = -errno;
+ goto exit;
+ }
+ }
+ break;
+ }
+ pw_log_trace("connection %p: %d written %zd bytes and %u fds", conn, conn->fd, sent,
+ outfds);
+
+ size -= sent;
+ data = SPA_PTROFF(data, sent, void);
+ n_fds -= outfds;
+ fds += outfds;
+ to_close += outfds;
+ }
+
+ res = 0;
+
+exit:
+ if (size > 0)
+ memmove(buf->buffer_data, data, size);
+ buf->buffer_size = size;
+ for (i = 0; i < to_close; i++) {
+ pw_log_debug("%p: close fd:%d", conn, buf->fds[i]);
+ close(buf->fds[i]);
+ }
+ if (n_fds > 0)
+ memmove(buf->fds, fds, n_fds * sizeof(int));
+ buf->n_fds = n_fds;
+ return res;
+}
+
+/** Clear the connection object
+ *
+ * \param conn the connection object
+ * \return 0 on success
+ *
+ * Remove all queued messages from \a conn
+ *
+ * \memberof pw_protocol_native_connection
+ */
+int pw_protocol_native_connection_clear(struct pw_protocol_native_connection *conn)
+{
+ struct impl *impl = SPA_CONTAINER_OF(conn, struct impl, this);
+
+ clear_buffer(&impl->out, true);
+ clear_buffer(&impl->in, true);
+
+ return 0;
+}
diff --git a/src/modules/module-protocol-native/connection.h b/src/modules/module-protocol-native/connection.h
new file mode 100644
index 0000000..d93829c
--- /dev/null
+++ b/src/modules/module-protocol-native/connection.h
@@ -0,0 +1,112 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_PROTOCOL_NATIVE_CONNECTION_H
+#define PIPEWIRE_PROTOCOL_NATIVE_CONNECTION_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <spa/utils/defs.h>
+#include <spa/utils/hook.h>
+
+#include <pipewire/extensions/protocol-native.h>
+
+struct pw_protocol_native_connection_events {
+#define PW_VERSION_PROTOCOL_NATIVE_CONNECTION_EVENTS 0
+ uint32_t version;
+
+ void (*destroy) (void *data);
+
+ void (*error) (void *data, int error);
+
+ void (*need_flush) (void *data);
+
+ void (*start) (void *data, uint32_t version);
+};
+
+/** \class pw_protocol_native_connection
+ *
+ * \brief Manages the connection between client and server
+ *
+ * The \ref pw_protocol_native_connection handles the connection between client
+ * and server on a given socket.
+ */
+struct pw_protocol_native_connection {
+ int fd; /**< the socket */
+
+ struct spa_hook_list listener_list;
+};
+
+static inline void
+pw_protocol_native_connection_add_listener(struct pw_protocol_native_connection *conn,
+ struct spa_hook *listener,
+ const struct pw_protocol_native_connection_events *events,
+ void *data)
+{
+ spa_hook_list_append(&conn->listener_list, listener, events, data);
+}
+
+struct pw_protocol_native_connection *
+pw_protocol_native_connection_new(struct pw_context *context, int fd);
+
+int pw_protocol_native_connection_set_fd(struct pw_protocol_native_connection *conn, int fd);
+
+void
+pw_protocol_native_connection_destroy(struct pw_protocol_native_connection *conn);
+
+int
+pw_protocol_native_connection_get_next(struct pw_protocol_native_connection *conn,
+ const struct pw_protocol_native_message **msg);
+
+uint32_t pw_protocol_native_connection_add_fd(struct pw_protocol_native_connection *conn, int fd);
+int pw_protocol_native_connection_get_fd(struct pw_protocol_native_connection *conn, uint32_t index);
+
+struct spa_pod_builder *
+pw_protocol_native_connection_begin(struct pw_protocol_native_connection *conn,
+ uint32_t id, uint8_t opcode,
+ struct pw_protocol_native_message **msg);
+
+int
+pw_protocol_native_connection_end(struct pw_protocol_native_connection *conn,
+ struct spa_pod_builder *builder);
+
+int
+pw_protocol_native_connection_flush(struct pw_protocol_native_connection *conn);
+
+int
+pw_protocol_native_connection_clear(struct pw_protocol_native_connection *conn);
+
+void pw_protocol_native_connection_enter(struct pw_protocol_native_connection *conn);
+void pw_protocol_native_connection_leave(struct pw_protocol_native_connection *conn);
+
+struct spa_pod *pw_protocol_native_connection_get_footer(struct pw_protocol_native_connection *conn,
+ const struct pw_protocol_native_message *msg);
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* PIPEWIRE_PROTOCOL_NATIVE_CONNECTION_H */
diff --git a/src/modules/module-protocol-native/defs.h b/src/modules/module-protocol-native/defs.h
new file mode 100644
index 0000000..dc3c625
--- /dev/null
+++ b/src/modules/module-protocol-native/defs.h
@@ -0,0 +1,49 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+int pw_protocol_native_connect_local_socket(struct pw_protocol_client *client,
+ const struct spa_dict *props,
+ void (*done_callback) (void *data, int res),
+ void *data);
+int pw_protocol_native_connect_portal_screencast(struct pw_protocol_client *client,
+ const struct spa_dict *props,
+ void (*done_callback) (void *data, int res),
+ void *data);
+
+static inline void *get_first_pod_from_data(void *data, uint32_t maxsize, uint64_t offset)
+{
+ void *pod;
+ if (maxsize <= offset)
+ return NULL;
+
+ /* spa_pod_parser_advance() rounds up, so round down here to compensate */
+ maxsize = SPA_ROUND_DOWN_N(maxsize - offset, 8);
+ if (maxsize < sizeof(struct spa_pod))
+ return NULL;
+
+ pod = SPA_PTROFF(data, offset, void);
+ if (SPA_POD_BODY_SIZE(pod) > maxsize - sizeof(struct spa_pod))
+ return NULL;
+ return pod;
+}
diff --git a/src/modules/module-protocol-native/local-socket.c b/src/modules/module-protocol-native/local-socket.c
new file mode 100644
index 0000000..cbae203
--- /dev/null
+++ b/src/modules/module-protocol-native/local-socket.c
@@ -0,0 +1,169 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "config.h"
+
+#include <stdint.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <string.h>
+#include <errno.h>
+#include <unistd.h>
+#include <sys/socket.h>
+#include <sys/un.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <sys/file.h>
+#ifdef HAVE_PWD_H
+#include <pwd.h>
+#endif
+
+#include <pipewire/pipewire.h>
+
+#define DEFAULT_SYSTEM_RUNTIME_DIR "/run/pipewire"
+
+PW_LOG_TOPIC_EXTERN(mod_topic);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+static const char *
+get_remote(const struct spa_dict *props)
+{
+ const char *name;
+
+ name = getenv("PIPEWIRE_REMOTE");
+ if ((name == NULL || name[0] == '\0') && props)
+ name = spa_dict_lookup(props, PW_KEY_REMOTE_NAME);
+ if (name == NULL || name[0] == '\0')
+ name = PW_DEFAULT_REMOTE;
+ return name;
+}
+
+static const char *
+get_runtime_dir(void)
+{
+ const char *runtime_dir;
+
+ runtime_dir = getenv("PIPEWIRE_RUNTIME_DIR");
+ if (runtime_dir == NULL)
+ runtime_dir = getenv("XDG_RUNTIME_DIR");
+ if (runtime_dir == NULL)
+ runtime_dir = getenv("USERPROFILE");
+ return runtime_dir;
+}
+
+static const char *
+get_system_dir(void)
+{
+ return DEFAULT_SYSTEM_RUNTIME_DIR;
+}
+
+static int try_connect(struct pw_protocol_client *client,
+ const char *runtime_dir, const char *name,
+ void (*done_callback) (void *data, int res),
+ void *data)
+{
+ struct sockaddr_un addr;
+ socklen_t size;
+ int res, name_size, fd;
+
+ pw_log_info("connecting to '%s' runtime_dir:%s", name, runtime_dir);
+
+ if ((fd = socket(PF_LOCAL, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0)) < 0) {
+ res = -errno;
+ goto error;
+ }
+
+ memset(&addr, 0, sizeof(addr));
+ addr.sun_family = AF_LOCAL;
+ if (runtime_dir == NULL)
+ name_size = snprintf(addr.sun_path, sizeof(addr.sun_path), "%s", name) + 1;
+ else
+ name_size = snprintf(addr.sun_path, sizeof(addr.sun_path), "%s/%s", runtime_dir, name) + 1;
+
+ if (name_size > (int) sizeof addr.sun_path) {
+ if (runtime_dir == NULL)
+ pw_log_error("client %p: socket path \"%s\" plus null terminator exceeds %i bytes",
+ client, name, (int) sizeof(addr.sun_path));
+ else
+ pw_log_error("client %p: socket path \"%s/%s\" plus null terminator exceeds %i bytes",
+ client, runtime_dir, name, (int) sizeof(addr.sun_path));
+ res = -ENAMETOOLONG;
+ goto error_close;
+ };
+
+ size = offsetof(struct sockaddr_un, sun_path) + name_size;
+
+ if (connect(fd, (struct sockaddr *) &addr, size) < 0) {
+ pw_log_debug("connect to '%s' failed: %m", name);
+ if (errno == ENOENT)
+ errno = EHOSTDOWN;
+ if (errno == EAGAIN) {
+ pw_log_info("client %p: connect pending, fd %d", client, fd);
+ } else {
+ res = -errno;
+ goto error_close;
+ }
+ }
+
+ res = pw_protocol_client_connect_fd(client, fd, true);
+
+ if (done_callback)
+ done_callback(data, res);
+
+ return res;
+
+error_close:
+ close(fd);
+error:
+ return res;
+}
+
+int pw_protocol_native_connect_local_socket(struct pw_protocol_client *client,
+ const struct spa_dict *props,
+ void (*done_callback) (void *data, int res),
+ void *data)
+{
+ const char *runtime_dir, *name;
+ int res;
+
+ name = get_remote(props);
+ if (name == NULL)
+ return -EINVAL;
+
+ if (name[0] == '/') {
+ res = try_connect(client, NULL, name, done_callback, data);
+ } else {
+ runtime_dir = get_runtime_dir();
+ if (runtime_dir != NULL) {
+ res = try_connect(client, runtime_dir, name, done_callback, data);
+ if (res >= 0)
+ goto exit;
+ }
+ runtime_dir = get_system_dir();
+ if (runtime_dir != NULL)
+ res = try_connect(client, runtime_dir, name, done_callback, data);
+ }
+exit:
+ return res;
+}
diff --git a/src/modules/module-protocol-native/portal-screencast.c b/src/modules/module-protocol-native/portal-screencast.c
new file mode 100644
index 0000000..7cb6614
--- /dev/null
+++ b/src/modules/module-protocol-native/portal-screencast.c
@@ -0,0 +1,41 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdint.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <string.h>
+#include <errno.h>
+#include <unistd.h>
+#include <sys/socket.h>
+
+#include <pipewire/pipewire.h>
+
+int pw_protocol_native_connect_portal_screencast(struct pw_protocol_client *client,
+ const struct spa_dict *props,
+ void (*done_callback) (void *data, int res),
+ void *data)
+{
+ return -ENOTSUP;
+}
diff --git a/src/modules/module-protocol-native/protocol-footer.c b/src/modules/module-protocol-native/protocol-footer.c
new file mode 100644
index 0000000..9b6fe62
--- /dev/null
+++ b/src/modules/module-protocol-native/protocol-footer.c
@@ -0,0 +1,152 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdio.h>
+#include <errno.h>
+
+#include <spa/pod/builder.h>
+#include <spa/pod/parser.h>
+#include <spa/utils/result.h>
+
+#include <pipewire/private.h>
+
+#include "connection.h"
+#include "protocol-footer.h"
+#include "defs.h"
+
+PW_LOG_TOPIC_EXTERN(mod_topic);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+struct footer_builder {
+ struct spa_pod_builder *builder;
+ struct spa_pod_frame outer;
+ struct spa_pod_frame inner;
+ unsigned int started:1;
+};
+
+#define FOOTER_BUILDER_INIT(builder) ((struct footer_builder) { (builder) })
+
+static void start_footer_entry(struct footer_builder *fb, uint32_t opcode)
+{
+ if (!fb->started) {
+ spa_pod_builder_push_struct(fb->builder, &fb->outer);
+ fb->started = true;
+ }
+
+ spa_pod_builder_id(fb->builder, opcode);
+ spa_pod_builder_push_struct(fb->builder, &fb->inner);
+}
+
+static void end_footer_entry(struct footer_builder *fb)
+{
+ spa_pod_builder_pop(fb->builder, &fb->inner);
+}
+
+static void end_footer(struct footer_builder *fb)
+{
+ if (!fb->started)
+ return;
+
+ spa_pod_builder_pop(fb->builder, &fb->outer);
+}
+
+void marshal_core_footers(struct footer_core_global_state *state, struct pw_core *core,
+ struct spa_pod_builder *builder)
+{
+ struct footer_builder fb = FOOTER_BUILDER_INIT(builder);
+
+ if (core->recv_generation != state->last_recv_generation) {
+ state->last_recv_generation = core->recv_generation;
+
+ pw_log_trace("core %p: send client registry generation:%"PRIu64,
+ core, core->recv_generation);
+
+ start_footer_entry(&fb, FOOTER_CLIENT_OPCODE_GENERATION);
+ spa_pod_builder_long(fb.builder, core->recv_generation);
+ end_footer_entry(&fb);
+ }
+
+ end_footer(&fb);
+}
+
+void marshal_client_footers(struct footer_client_global_state *state, struct pw_impl_client *client,
+ struct spa_pod_builder *builder)
+{
+ struct footer_builder fb = FOOTER_BUILDER_INIT(builder);
+
+ if (client->context->generation != client->sent_generation) {
+ client->sent_generation = client->context->generation;
+
+ pw_log_trace("impl-client %p: send server registry generation:%"PRIu64,
+ client, client->context->generation);
+
+ start_footer_entry(&fb, FOOTER_CORE_OPCODE_GENERATION);
+ spa_pod_builder_long(fb.builder, client->context->generation);
+ end_footer_entry(&fb);
+ }
+
+ end_footer(&fb);
+}
+
+int demarshal_core_generation(void *object, struct spa_pod_parser *parser)
+{
+ struct pw_core *core = object;
+ int64_t generation;
+
+ if (spa_pod_parser_get_long(parser, &generation) < 0)
+ return -EINVAL;
+
+ core->recv_generation = SPA_MAX(core->recv_generation,
+ (uint64_t)generation);
+
+ pw_log_trace("core %p: recv server registry generation:%"PRIu64,
+ core, generation);
+
+ return 0;
+}
+
+int demarshal_client_generation(void *object, struct spa_pod_parser *parser)
+{
+ struct pw_impl_client *client = object;
+ int64_t generation;
+
+ if (spa_pod_parser_get_long(parser, &generation) < 0)
+ return -EINVAL;
+
+ client->recv_generation = SPA_MAX(client->recv_generation,
+ (uint64_t)generation);
+
+ pw_log_trace("impl-client %p: recv client registry generation:%"PRIu64,
+ client, generation);
+
+ return 0;
+}
+
+const struct footer_demarshal footer_core_demarshal[FOOTER_CORE_OPCODE_LAST] = {
+ [FOOTER_CORE_OPCODE_GENERATION] = (struct footer_demarshal){ .demarshal = demarshal_core_generation },
+};
+
+const struct footer_demarshal footer_client_demarshal[FOOTER_CLIENT_OPCODE_LAST] = {
+ [FOOTER_CLIENT_OPCODE_GENERATION] = (struct footer_demarshal){ .demarshal = demarshal_client_generation },
+};
diff --git a/src/modules/module-protocol-native/protocol-footer.h b/src/modules/module-protocol-native/protocol-footer.h
new file mode 100644
index 0000000..bdbec04
--- /dev/null
+++ b/src/modules/module-protocol-native/protocol-footer.h
@@ -0,0 +1,59 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+/*
+ * Protocol footer.
+ *
+ * For passing around general state data that is not associated with
+ * messages sent to objects.
+ */
+
+enum {
+ FOOTER_CORE_OPCODE_GENERATION = 0,
+ FOOTER_CORE_OPCODE_LAST
+};
+
+enum {
+ FOOTER_CLIENT_OPCODE_GENERATION = 0,
+ FOOTER_CLIENT_OPCODE_LAST
+};
+
+struct footer_core_global_state {
+ uint64_t last_recv_generation;
+};
+
+struct footer_client_global_state {
+};
+
+struct footer_demarshal {
+ int (*demarshal)(void *object, struct spa_pod_parser *parser);
+};
+
+extern const struct footer_demarshal footer_core_demarshal[FOOTER_CORE_OPCODE_LAST];
+extern const struct footer_demarshal footer_client_demarshal[FOOTER_CLIENT_OPCODE_LAST];
+
+void marshal_core_footers(struct footer_core_global_state *state, struct pw_core *core,
+ struct spa_pod_builder *builder);
+void marshal_client_footers(struct footer_client_global_state *state, struct pw_impl_client *client,
+ struct spa_pod_builder *builder);
diff --git a/src/modules/module-protocol-native/protocol-native.c b/src/modules/module-protocol-native/protocol-native.c
new file mode 100644
index 0000000..1c2d83e
--- /dev/null
+++ b/src/modules/module-protocol-native/protocol-native.c
@@ -0,0 +1,2236 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdio.h>
+#include <errno.h>
+
+#include <spa/pod/builder.h>
+#include <spa/pod/parser.h>
+#include <spa/utils/result.h>
+
+#include <pipewire/impl.h>
+#include <pipewire/extensions/protocol-native.h>
+
+#include "connection.h"
+
+#define MAX_DICT 1024
+#define MAX_PARAM_INFO 128
+#define MAX_PERMISSIONS 4096
+
+PW_LOG_TOPIC_EXTERN(mod_topic);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+static int core_method_marshal_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct pw_core_events *events,
+ void *data)
+{
+ struct pw_proxy *proxy = object;
+ pw_proxy_add_object_listener(proxy, listener, events, data);
+ return 0;
+}
+
+static int core_method_marshal_hello(void *object, uint32_t version)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_proxy(proxy, PW_CORE_METHOD_HELLO, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(version));
+
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int core_method_marshal_sync(void *object, uint32_t id, int seq)
+{
+ struct pw_protocol_native_message *msg;
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_proxy(proxy, PW_CORE_METHOD_SYNC, &msg);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(id),
+ SPA_POD_Int(SPA_RESULT_RETURN_ASYNC(msg->seq)));
+
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int core_method_marshal_pong(void *object, uint32_t id, int seq)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_proxy(proxy, PW_CORE_METHOD_PONG, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(id),
+ SPA_POD_Int(seq));
+
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int core_method_marshal_error(void *object, uint32_t id, int seq, int res, const char *error)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_proxy(proxy, PW_CORE_METHOD_ERROR, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(id),
+ SPA_POD_Int(seq),
+ SPA_POD_Int(res),
+ SPA_POD_String(error));
+
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static struct pw_registry * core_method_marshal_get_registry(void *object,
+ uint32_t version, size_t user_data_size)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+ struct pw_proxy *res;
+ uint32_t new_id;
+
+ res = pw_proxy_new(object, PW_TYPE_INTERFACE_Registry, version, user_data_size);
+ if (res == NULL)
+ return NULL;
+
+ new_id = pw_proxy_get_id(res);
+
+ b = pw_protocol_native_begin_proxy(proxy, PW_CORE_METHOD_GET_REGISTRY, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(version),
+ SPA_POD_Int(new_id));
+
+ pw_protocol_native_end_proxy(proxy, b);
+
+ return (struct pw_registry *) res;
+}
+
+static inline void push_item(struct spa_pod_builder *b, const struct spa_dict_item *item)
+{
+ const char *str;
+ spa_pod_builder_string(b, item->key);
+ str = item->value;
+ if (spa_strstartswith(str, "pointer:"))
+ str = "";
+ spa_pod_builder_string(b, str);
+}
+
+static void push_dict(struct spa_pod_builder *b, const struct spa_dict *dict)
+{
+ uint32_t i, n_items;
+ struct spa_pod_frame f;
+
+ n_items = dict ? dict->n_items : 0;
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_int(b, n_items);
+ for (i = 0; i < n_items; i++)
+ push_item(b, &dict->items[i]);
+ spa_pod_builder_pop(b, &f);
+}
+
+static inline int parse_item(struct spa_pod_parser *prs, struct spa_dict_item *item)
+{
+ int res;
+ if ((res = spa_pod_parser_get(prs,
+ SPA_POD_String(&item->key),
+ SPA_POD_String(&item->value),
+ NULL)) < 0)
+ return res;
+ if (spa_strstartswith(item->value, "pointer:"))
+ item->value = "";
+ return 0;
+}
+
+#define parse_dict(prs,d) \
+do { \
+ if (spa_pod_parser_get(prs, \
+ SPA_POD_Int(&(d)->n_items), NULL) < 0) \
+ return -EINVAL; \
+ (d)->items = NULL; \
+ if ((d)->n_items > 0) { \
+ uint32_t i; \
+ if ((d)->n_items > MAX_DICT) \
+ return -ENOSPC; \
+ (d)->items = alloca((d)->n_items * sizeof(struct spa_dict_item)); \
+ for (i = 0; i < (d)->n_items; i++) { \
+ if (parse_item(prs, (struct spa_dict_item *) &(d)->items[i]) < 0) \
+ return -EINVAL; \
+ } \
+ } \
+} while(0)
+
+#define parse_dict_struct(prs,f,dict) \
+do { \
+ if (spa_pod_parser_push_struct(prs, f) < 0) \
+ return -EINVAL; \
+ parse_dict(prs, dict); \
+ spa_pod_parser_pop(prs, f); \
+} while(0)
+
+static void push_params(struct spa_pod_builder *b, uint32_t n_params,
+ const struct spa_param_info *params)
+{
+ uint32_t i;
+ struct spa_pod_frame f;
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_int(b, n_params);
+ for (i = 0; i < n_params; i++) {
+ spa_pod_builder_id(b, params[i].id);
+ spa_pod_builder_int(b, params[i].flags);
+ }
+ spa_pod_builder_pop(b, &f);
+}
+
+
+#define parse_params_struct(prs,f,params,n_params) \
+do { \
+ if (spa_pod_parser_push_struct(prs, f) < 0 || \
+ spa_pod_parser_get(prs, \
+ SPA_POD_Int(&(n_params)), NULL) < 0) \
+ return -EINVAL; \
+ (params) = NULL; \
+ if ((n_params) > 0) { \
+ uint32_t i; \
+ if ((n_params) > MAX_PARAM_INFO) \
+ return -ENOSPC; \
+ (params) = alloca((n_params) * sizeof(struct spa_param_info)); \
+ for (i = 0; i < (n_params); i++) { \
+ if (spa_pod_parser_get(prs, \
+ SPA_POD_Id(&(params)[i].id), \
+ SPA_POD_Int(&(params)[i].flags), NULL) < 0) \
+ return -EINVAL; \
+ } \
+ } \
+ spa_pod_parser_pop(prs, f); \
+} while(0)
+
+
+#define parse_permissions_struct(prs,f,n_permissions,permissions) \
+do { \
+ if (spa_pod_parser_push_struct(prs, f) < 0 || \
+ spa_pod_parser_get(prs, \
+ SPA_POD_Int(&(n_permissions)), NULL) < 0) \
+ return -EINVAL; \
+ (permissions) = NULL; \
+ if ((n_permissions) > 0) { \
+ uint32_t i; \
+ if ((n_permissions) > MAX_PERMISSIONS) \
+ return -ENOSPC; \
+ (permissions) = alloca((n_permissions) * sizeof(struct pw_permission)); \
+ for (i = 0; i < (n_permissions); i++) { \
+ if (spa_pod_parser_get(prs, \
+ SPA_POD_Int(&(permissions)[i].id), \
+ SPA_POD_Int(&(permissions)[i].permissions), NULL) < 0) \
+ return -EINVAL; \
+ } \
+ } \
+ spa_pod_parser_pop(prs, f); \
+} while(0)
+
+static void *
+core_method_marshal_create_object(void *object,
+ const char *factory_name,
+ const char *type, uint32_t version,
+ const struct spa_dict *props, size_t user_data_size)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+ struct spa_pod_frame f;
+ struct pw_proxy *res;
+ uint32_t new_id;
+
+ res = pw_proxy_new(object, type, version, user_data_size);
+ if (res == NULL)
+ return NULL;
+
+ new_id = pw_proxy_get_id(res);
+
+ b = pw_protocol_native_begin_proxy(proxy, PW_CORE_METHOD_CREATE_OBJECT, NULL);
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_add(b,
+ SPA_POD_String(factory_name),
+ SPA_POD_String(type),
+ SPA_POD_Int(version),
+ NULL);
+ push_dict(b, props);
+ spa_pod_builder_int(b, new_id);
+ spa_pod_builder_pop(b, &f);
+
+ pw_protocol_native_end_proxy(proxy, b);
+
+ return (void *)res;
+}
+
+static int
+core_method_marshal_destroy(void *object, void *p)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+ uint32_t id = pw_proxy_get_id(p);
+
+ b = pw_protocol_native_begin_proxy(proxy, PW_CORE_METHOD_DESTROY, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(id));
+
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int core_event_demarshal_info(void *data, const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_dict props = SPA_DICT_INIT(NULL, 0);
+ struct pw_core_info info = { .props = &props };
+ struct spa_pod_frame f[2];
+ struct spa_pod_parser prs;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_push_struct(&prs, &f[0]) < 0)
+ return -EINVAL;
+ if (spa_pod_parser_get(&prs,
+ SPA_POD_Int(&info.id),
+ SPA_POD_Int(&info.cookie),
+ SPA_POD_String(&info.user_name),
+ SPA_POD_String(&info.host_name),
+ SPA_POD_String(&info.version),
+ SPA_POD_String(&info.name),
+ SPA_POD_Long(&info.change_mask), NULL) < 0)
+ return -EINVAL;
+
+
+ parse_dict_struct(&prs, &f[1], &props);
+
+ return pw_proxy_notify(proxy, struct pw_core_events, info, 0, &info);
+}
+
+static int core_event_demarshal_done(void *data, const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ uint32_t id, seq;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&id),
+ SPA_POD_Int(&seq)) < 0)
+ return -EINVAL;
+
+ if (id == SPA_ID_INVALID)
+ return 0;
+
+ return pw_proxy_notify(proxy, struct pw_core_events, done, 0, id, seq);
+}
+
+static int core_event_demarshal_ping(void *data, const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ uint32_t id, seq;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&id),
+ SPA_POD_Int(&seq)) < 0)
+ return -EINVAL;
+
+ return pw_proxy_notify(proxy, struct pw_core_events, ping, 0, id, seq);
+}
+
+static int core_event_demarshal_error(void *data, const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ uint32_t id, res;
+ int seq;
+ const char *error;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&id),
+ SPA_POD_Int(&seq),
+ SPA_POD_Int(&res),
+ SPA_POD_String(&error)) < 0)
+ return -EINVAL;
+
+ return pw_proxy_notify(proxy, struct pw_core_events, error, 0, id, seq, res, error);
+}
+
+static int core_event_demarshal_remove_id(void *data, const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ uint32_t id;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs, SPA_POD_Int(&id)) < 0)
+ return -EINVAL;
+
+ return pw_proxy_notify(proxy, struct pw_core_events, remove_id, 0, id);
+}
+
+static int core_event_demarshal_bound_id(void *data, const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ uint32_t id, global_id;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&id),
+ SPA_POD_Int(&global_id)) < 0)
+ return -EINVAL;
+
+ return pw_proxy_notify(proxy, struct pw_core_events, bound_id, 0, id, global_id);
+}
+
+static int core_event_demarshal_add_mem(void *data, const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ uint32_t id, type, flags;
+ int64_t idx;
+ int fd;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&id),
+ SPA_POD_Id(&type),
+ SPA_POD_Fd(&idx),
+ SPA_POD_Int(&flags)) < 0)
+ return -EINVAL;
+
+ fd = pw_protocol_native_get_proxy_fd(proxy, idx);
+
+ return pw_proxy_notify(proxy, struct pw_core_events, add_mem, 0, id, type, fd, flags);
+}
+
+static int core_event_demarshal_remove_mem(void *data, const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ uint32_t id;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&id)) < 0)
+ return -EINVAL;
+
+ return pw_proxy_notify(proxy, struct pw_core_events, remove_mem, 0, id);
+}
+
+static void core_event_marshal_info(void *data, const struct pw_core_info *info)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+ struct spa_pod_frame f;
+
+ b = pw_protocol_native_begin_resource(resource, PW_CORE_EVENT_INFO, NULL);
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_add(b,
+ SPA_POD_Int(info->id),
+ SPA_POD_Int(info->cookie),
+ SPA_POD_String(info->user_name),
+ SPA_POD_String(info->host_name),
+ SPA_POD_String(info->version),
+ SPA_POD_String(info->name),
+ SPA_POD_Long(info->change_mask),
+ NULL);
+ push_dict(b, info->change_mask & PW_CORE_CHANGE_MASK_PROPS ? info->props : NULL);
+ spa_pod_builder_pop(b, &f);
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static void core_event_marshal_done(void *data, uint32_t id, int seq)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource, PW_CORE_EVENT_DONE, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(id),
+ SPA_POD_Int(seq));
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static void core_event_marshal_ping(void *data, uint32_t id, int seq)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+ struct pw_protocol_native_message *msg;
+
+ b = pw_protocol_native_begin_resource(resource, PW_CORE_EVENT_PING, &msg);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(id),
+ SPA_POD_Int(SPA_RESULT_RETURN_ASYNC(msg->seq)));
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static void core_event_marshal_error(void *data, uint32_t id, int seq, int res, const char *error)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource, PW_CORE_EVENT_ERROR, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(id),
+ SPA_POD_Int(seq),
+ SPA_POD_Int(res),
+ SPA_POD_String(error));
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static void core_event_marshal_remove_id(void *data, uint32_t id)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource, PW_CORE_EVENT_REMOVE_ID, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(id));
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static void core_event_marshal_bound_id(void *data, uint32_t id, uint32_t global_id)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource, PW_CORE_EVENT_BOUND_ID, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(id),
+ SPA_POD_Int(global_id));
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static void core_event_marshal_add_mem(void *data, uint32_t id, uint32_t type, int fd, uint32_t flags)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource, PW_CORE_EVENT_ADD_MEM, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(id),
+ SPA_POD_Id(type),
+ SPA_POD_Fd(pw_protocol_native_add_resource_fd(resource, fd)),
+ SPA_POD_Int(flags));
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static void core_event_marshal_remove_mem(void *data, uint32_t id)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource, PW_CORE_EVENT_REMOVE_MEM, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(id));
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static int core_method_demarshal_hello(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ uint32_t version;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&version)) < 0)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_core_methods, hello, 0, version);
+}
+
+static int core_method_demarshal_sync(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ uint32_t id, seq;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&id),
+ SPA_POD_Int(&seq)) < 0)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_core_methods, sync, 0, id, seq);
+}
+
+static int core_method_demarshal_pong(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ uint32_t id, seq;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&id),
+ SPA_POD_Int(&seq)) < 0)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_core_methods, pong, 0, id, seq);
+}
+
+static int core_method_demarshal_error(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ uint32_t id, res;
+ int seq;
+ const char *error;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&id),
+ SPA_POD_Int(&seq),
+ SPA_POD_Int(&res),
+ SPA_POD_String(&error)) < 0)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_core_methods, error, 0, id, seq, res, error);
+}
+
+static int core_method_demarshal_get_registry(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ int32_t version, new_id;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&version),
+ SPA_POD_Int(&new_id)) < 0)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_core_methods, get_registry, 0, version, new_id);
+}
+
+static int core_method_demarshal_create_object(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ struct spa_pod_frame f[2];
+ uint32_t version, new_id;
+ const char *factory_name, *type;
+ struct spa_dict props = SPA_DICT_INIT(NULL, 0);
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_push_struct(&prs, &f[0]) < 0 ||
+ spa_pod_parser_get(&prs,
+ SPA_POD_String(&factory_name),
+ SPA_POD_String(&type),
+ SPA_POD_Int(&version),
+ NULL) < 0)
+ return -EINVAL;
+
+ parse_dict_struct(&prs, &f[1], &props);
+
+ if (spa_pod_parser_get(&prs,
+ SPA_POD_Int(&new_id), NULL) < 0)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_core_methods, create_object, 0, factory_name,
+ type, version,
+ &props, new_id);
+}
+
+static int core_method_demarshal_destroy(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct pw_impl_client *client = pw_resource_get_client(resource);
+ struct pw_resource *r;
+ struct spa_pod_parser prs;
+ uint32_t id;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&id)) < 0)
+ return -EINVAL;
+
+ pw_log_debug("client %p: destroy resource %u", client, id);
+
+ if ((r = pw_impl_client_find_resource(client, id)) == NULL)
+ goto no_resource;
+
+ return pw_resource_notify(resource, struct pw_core_methods, destroy, 0, r);
+
+ no_resource:
+ pw_log_debug("client %p: unknown resource %u op:%u", client, id, msg->opcode);
+ pw_resource_errorf(resource, -ENOENT, "unknown resource %d op:%u", id, msg->opcode);
+ return 0;
+}
+
+static int registry_method_marshal_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct pw_registry_events *events,
+ void *data)
+{
+ struct pw_proxy *proxy = object;
+ pw_proxy_add_object_listener(proxy, listener, events, data);
+ return 0;
+}
+
+static void registry_marshal_global(void *data, uint32_t id, uint32_t permissions,
+ const char *type, uint32_t version, const struct spa_dict *props)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+ struct spa_pod_frame f;
+
+ b = pw_protocol_native_begin_resource(resource, PW_REGISTRY_EVENT_GLOBAL, NULL);
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_add(b,
+ SPA_POD_Int(id),
+ SPA_POD_Int(permissions),
+ SPA_POD_String(type),
+ SPA_POD_Int(version),
+ NULL);
+ push_dict(b, props);
+ spa_pod_builder_pop(b, &f);
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static void registry_marshal_global_remove(void *data, uint32_t id)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource, PW_REGISTRY_EVENT_GLOBAL_REMOVE, NULL);
+
+ spa_pod_builder_add_struct(b, SPA_POD_Int(id));
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static int registry_demarshal_bind(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ uint32_t id, version, new_id;
+ char *type;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&id),
+ SPA_POD_String(&type),
+ SPA_POD_Int(&version),
+ SPA_POD_Int(&new_id)) < 0)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_registry_methods, bind, 0, id, type, version, new_id);
+}
+
+static int registry_demarshal_destroy(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ uint32_t id;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&id)) < 0)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_registry_methods, destroy, 0, id);
+}
+
+static int module_method_marshal_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct pw_module_events *events,
+ void *data)
+{
+ struct pw_proxy *proxy = object;
+ pw_proxy_add_object_listener(proxy, listener, events, data);
+ return 0;
+}
+
+static void module_marshal_info(void *data, const struct pw_module_info *info)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+ struct spa_pod_frame f;
+
+ b = pw_protocol_native_begin_resource(resource, PW_MODULE_EVENT_INFO, NULL);
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_add(b,
+ SPA_POD_Int(info->id),
+ SPA_POD_String(info->name),
+ SPA_POD_String(info->filename),
+ SPA_POD_String(info->args),
+ SPA_POD_Long(info->change_mask),
+ NULL);
+ push_dict(b, info->change_mask & PW_MODULE_CHANGE_MASK_PROPS ? info->props : NULL);
+ spa_pod_builder_pop(b, &f);
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static int module_demarshal_info(void *data, const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ struct spa_pod_frame f[2];
+ struct spa_dict props = SPA_DICT_INIT(NULL, 0);
+ struct pw_module_info info = { .props = &props };
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_push_struct(&prs, &f[0]) < 0 ||
+ spa_pod_parser_get(&prs,
+ SPA_POD_Int(&info.id),
+ SPA_POD_String(&info.name),
+ SPA_POD_String(&info.filename),
+ SPA_POD_String(&info.args),
+ SPA_POD_Long(&info.change_mask), NULL) < 0)
+ return -EINVAL;
+
+ parse_dict_struct(&prs, &f[1], &props);
+
+ return pw_proxy_notify(proxy, struct pw_module_events, info, 0, &info);
+}
+
+static int device_method_marshal_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct pw_device_events *events,
+ void *data)
+{
+ struct pw_proxy *proxy = object;
+ pw_proxy_add_object_listener(proxy, listener, events, data);
+ return 0;
+}
+
+static void device_marshal_info(void *data, const struct pw_device_info *info)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+ struct spa_pod_frame f;
+
+ b = pw_protocol_native_begin_resource(resource, PW_DEVICE_EVENT_INFO, NULL);
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_add(b,
+ SPA_POD_Int(info->id),
+ SPA_POD_Long(info->change_mask),
+ NULL);
+ push_dict(b, info->change_mask & PW_DEVICE_CHANGE_MASK_PROPS ? info->props : NULL);
+ push_params(b, info->n_params, info->params);
+ spa_pod_builder_pop(b, &f);
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static int device_demarshal_info(void *data, const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ struct spa_pod_frame f[2];
+ struct spa_dict props = SPA_DICT_INIT(NULL, 0);
+ struct pw_device_info info = { .props = &props };
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_push_struct(&prs, &f[0]) < 0 ||
+ spa_pod_parser_get(&prs,
+ SPA_POD_Int(&info.id),
+ SPA_POD_Long(&info.change_mask), NULL) < 0)
+ return -EINVAL;
+
+ parse_dict_struct(&prs, &f[1], &props);
+ parse_params_struct(&prs, &f[1], info.params, info.n_params);
+
+ return pw_proxy_notify(proxy, struct pw_device_events, info, 0, &info);
+}
+
+static void device_marshal_param(void *data, int seq, uint32_t id, uint32_t index, uint32_t next,
+ const struct spa_pod *param)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource, PW_DEVICE_EVENT_PARAM, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(seq),
+ SPA_POD_Id(id),
+ SPA_POD_Int(index),
+ SPA_POD_Int(next),
+ SPA_POD_Pod(param));
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static int device_demarshal_param(void *data, const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ uint32_t id, index, next;
+ int seq;
+ struct spa_pod *param;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&seq),
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&index),
+ SPA_POD_Int(&next),
+ SPA_POD_Pod(&param)) < 0)
+ return -EINVAL;
+
+ return pw_proxy_notify(proxy, struct pw_device_events, param, 0,
+ seq, id, index, next, param);
+}
+
+static int device_marshal_subscribe_params(void *object, uint32_t *ids, uint32_t n_ids)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_proxy(proxy, PW_DEVICE_METHOD_SUBSCRIBE_PARAMS, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Array(sizeof(uint32_t), SPA_TYPE_Id, n_ids, ids));
+
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int device_demarshal_subscribe_params(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ uint32_t csize, ctype, n_ids;
+ uint32_t *ids;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Array(&csize, &ctype, &n_ids, &ids)) < 0)
+ return -EINVAL;
+
+ if (ctype != SPA_TYPE_Id)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_device_methods, subscribe_params, 0,
+ ids, n_ids);
+}
+
+static int device_marshal_enum_params(void *object, int seq,
+ uint32_t id, uint32_t index, uint32_t num, const struct spa_pod *filter)
+{
+ struct pw_protocol_native_message *msg;
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_proxy(proxy, PW_DEVICE_METHOD_ENUM_PARAMS, &msg);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(SPA_RESULT_RETURN_ASYNC(msg->seq)),
+ SPA_POD_Id(id),
+ SPA_POD_Int(index),
+ SPA_POD_Int(num),
+ SPA_POD_Pod(filter));
+
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int device_demarshal_enum_params(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ uint32_t id, index, num;
+ int seq;
+ struct spa_pod *filter;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&seq),
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&index),
+ SPA_POD_Int(&num),
+ SPA_POD_Pod(&filter)) < 0)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_device_methods, enum_params, 0,
+ seq, id, index, num, filter);
+}
+
+static int device_marshal_set_param(void *object, uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_proxy(proxy, PW_DEVICE_METHOD_SET_PARAM, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Id(id),
+ SPA_POD_Int(flags),
+ SPA_POD_Pod(param));
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int device_demarshal_set_param(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ uint32_t id, flags;
+ struct spa_pod *param;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&flags),
+ SPA_POD_Pod(&param)) < 0)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_device_methods, set_param, 0, id, flags, param);
+}
+
+static int factory_method_marshal_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct pw_factory_events *events,
+ void *data)
+{
+ struct pw_proxy *proxy = object;
+ pw_proxy_add_object_listener(proxy, listener, events, data);
+ return 0;
+}
+
+static void factory_marshal_info(void *data, const struct pw_factory_info *info)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+ struct spa_pod_frame f;
+
+ b = pw_protocol_native_begin_resource(resource, PW_FACTORY_EVENT_INFO, NULL);
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_add(b,
+ SPA_POD_Int(info->id),
+ SPA_POD_String(info->name),
+ SPA_POD_String(info->type),
+ SPA_POD_Int(info->version),
+ SPA_POD_Long(info->change_mask),
+ NULL);
+ push_dict(b, info->change_mask & PW_FACTORY_CHANGE_MASK_PROPS ? info->props : NULL);
+ spa_pod_builder_pop(b, &f);
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static int factory_demarshal_info(void *data, const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ struct spa_pod_frame f[2];
+ struct spa_dict props = SPA_DICT_INIT(NULL, 0);
+ struct pw_factory_info info = { .props = &props };
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_push_struct(&prs, &f[0]) < 0 ||
+ spa_pod_parser_get(&prs,
+ SPA_POD_Int(&info.id),
+ SPA_POD_String(&info.name),
+ SPA_POD_String(&info.type),
+ SPA_POD_Int(&info.version),
+ SPA_POD_Long(&info.change_mask), NULL) < 0)
+ return -EINVAL;
+
+ parse_dict_struct(&prs, &f[1], &props);
+
+ return pw_proxy_notify(proxy, struct pw_factory_events, info, 0, &info);
+}
+
+static int node_method_marshal_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct pw_node_events *events,
+ void *data)
+{
+ struct pw_proxy *proxy = object;
+ pw_proxy_add_object_listener(proxy, listener, events, data);
+ return 0;
+}
+
+static void node_marshal_info(void *data, const struct pw_node_info *info)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+ struct spa_pod_frame f;
+
+ b = pw_protocol_native_begin_resource(resource, PW_NODE_EVENT_INFO, NULL);
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_add(b,
+ SPA_POD_Int(info->id),
+ SPA_POD_Int(info->max_input_ports),
+ SPA_POD_Int(info->max_output_ports),
+ SPA_POD_Long(info->change_mask),
+ SPA_POD_Int(info->n_input_ports),
+ SPA_POD_Int(info->n_output_ports),
+ SPA_POD_Id(info->state),
+ SPA_POD_String(info->error),
+ NULL);
+ push_dict(b, info->change_mask & PW_NODE_CHANGE_MASK_PROPS ? info->props : NULL);
+ push_params(b, info->n_params, info->params);
+ spa_pod_builder_pop(b, &f);
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static int node_demarshal_info(void *data, const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ struct spa_pod_frame f[2];
+ struct spa_dict props = SPA_DICT_INIT(NULL, 0);
+ struct pw_node_info info = { .props = &props };
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_push_struct(&prs, &f[0]) < 0 ||
+ spa_pod_parser_get(&prs,
+ SPA_POD_Int(&info.id),
+ SPA_POD_Int(&info.max_input_ports),
+ SPA_POD_Int(&info.max_output_ports),
+ SPA_POD_Long(&info.change_mask),
+ SPA_POD_Int(&info.n_input_ports),
+ SPA_POD_Int(&info.n_output_ports),
+ SPA_POD_Id(&info.state),
+ SPA_POD_String(&info.error), NULL) < 0)
+ return -EINVAL;
+
+ parse_dict_struct(&prs, &f[1], &props);
+ parse_params_struct(&prs, &f[1], info.params, info.n_params);
+
+ return pw_proxy_notify(proxy, struct pw_node_events, info, 0, &info);
+}
+
+static void node_marshal_param(void *data, int seq, uint32_t id,
+ uint32_t index, uint32_t next, const struct spa_pod *param)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource, PW_NODE_EVENT_PARAM, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(seq),
+ SPA_POD_Id(id),
+ SPA_POD_Int(index),
+ SPA_POD_Int(next),
+ SPA_POD_Pod(param));
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static int node_demarshal_param(void *data, const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ uint32_t id, index, next;
+ int seq;
+ struct spa_pod *param;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&seq),
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&index),
+ SPA_POD_Int(&next),
+ SPA_POD_Pod(&param)) < 0)
+ return -EINVAL;
+
+ return pw_proxy_notify(proxy, struct pw_node_events, param, 0,
+ seq, id, index, next, param);
+}
+
+static int node_marshal_subscribe_params(void *object, uint32_t *ids, uint32_t n_ids)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_proxy(proxy, PW_NODE_METHOD_SUBSCRIBE_PARAMS, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Array(sizeof(uint32_t), SPA_TYPE_Id, n_ids, ids));
+
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int node_demarshal_subscribe_params(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ uint32_t csize, ctype, n_ids;
+ uint32_t *ids;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Array(&csize, &ctype, &n_ids, &ids)) < 0)
+ return -EINVAL;
+
+ if (ctype != SPA_TYPE_Id)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_node_methods, subscribe_params, 0,
+ ids, n_ids);
+}
+
+static int node_marshal_enum_params(void *object, int seq, uint32_t id,
+ uint32_t index, uint32_t num, const struct spa_pod *filter)
+{
+ struct pw_protocol_native_message *msg;
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_proxy(proxy, PW_NODE_METHOD_ENUM_PARAMS, &msg);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(SPA_RESULT_RETURN_ASYNC(msg->seq)),
+ SPA_POD_Id(id),
+ SPA_POD_Int(index),
+ SPA_POD_Int(num),
+ SPA_POD_Pod(filter));
+
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int node_demarshal_enum_params(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ uint32_t id, index, num;
+ int seq;
+ struct spa_pod *filter;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&seq),
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&index),
+ SPA_POD_Int(&num),
+ SPA_POD_Pod(&filter)) < 0)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_node_methods, enum_params, 0,
+ seq, id, index, num, filter);
+}
+
+static int node_marshal_set_param(void *object, uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_proxy(proxy, PW_NODE_METHOD_SET_PARAM, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Id(id),
+ SPA_POD_Int(flags),
+ SPA_POD_Pod(param));
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int node_demarshal_set_param(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ uint32_t id, flags;
+ struct spa_pod *param;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&flags),
+ SPA_POD_Pod(&param)) < 0)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_node_methods, set_param, 0, id, flags, param);
+}
+
+static int node_marshal_send_command(void *object, const struct spa_command *command)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_proxy(proxy, PW_NODE_METHOD_SEND_COMMAND, NULL);
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Pod(command));
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int node_demarshal_send_command(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ const struct spa_command *command;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Pod(&command)) < 0)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_node_methods, send_command, 0, command);
+}
+
+static int port_method_marshal_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct pw_port_events *events,
+ void *data)
+{
+ struct pw_proxy *proxy = object;
+ pw_proxy_add_object_listener(proxy, listener, events, data);
+ return 0;
+}
+
+static void port_marshal_info(void *data, const struct pw_port_info *info)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+ struct spa_pod_frame f;
+
+ b = pw_protocol_native_begin_resource(resource, PW_PORT_EVENT_INFO, NULL);
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_add(b,
+ SPA_POD_Int(info->id),
+ SPA_POD_Int(info->direction),
+ SPA_POD_Long(info->change_mask),
+ NULL);
+ push_dict(b, info->change_mask & PW_PORT_CHANGE_MASK_PROPS ? info->props : NULL);
+ push_params(b, info->n_params, info->params);
+ spa_pod_builder_pop(b, &f);
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static int port_demarshal_info(void *data, const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ struct spa_pod_frame f[2];
+ struct spa_dict props = SPA_DICT_INIT(NULL, 0);
+ struct pw_port_info info = { .props = &props };
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_push_struct(&prs, &f[0]) < 0 ||
+ spa_pod_parser_get(&prs,
+ SPA_POD_Int(&info.id),
+ SPA_POD_Int(&info.direction),
+ SPA_POD_Long(&info.change_mask), NULL) < 0)
+ return -EINVAL;
+
+ parse_dict_struct(&prs, &f[1], &props);
+ parse_params_struct(&prs, &f[1], info.params, info.n_params);
+
+ return pw_proxy_notify(proxy, struct pw_port_events, info, 0, &info);
+}
+
+static void port_marshal_param(void *data, int seq, uint32_t id,
+ uint32_t index, uint32_t next, const struct spa_pod *param)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource, PW_PORT_EVENT_PARAM, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(seq),
+ SPA_POD_Id(id),
+ SPA_POD_Int(index),
+ SPA_POD_Int(next),
+ SPA_POD_Pod(param));
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static int port_demarshal_param(void *data, const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ uint32_t id, index, next;
+ int seq;
+ struct spa_pod *param;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&seq),
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&index),
+ SPA_POD_Int(&next),
+ SPA_POD_Pod(&param)) < 0)
+ return -EINVAL;
+
+ return pw_proxy_notify(proxy, struct pw_port_events, param, 0,
+ seq, id, index, next, param);
+}
+
+static int port_marshal_subscribe_params(void *object, uint32_t *ids, uint32_t n_ids)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_proxy(proxy, PW_PORT_METHOD_SUBSCRIBE_PARAMS, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Array(sizeof(uint32_t), SPA_TYPE_Id, n_ids, ids));
+
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int port_demarshal_subscribe_params(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ uint32_t csize, ctype, n_ids;
+ uint32_t *ids;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Array(&csize, &ctype, &n_ids, &ids)) < 0)
+ return -EINVAL;
+
+ if (ctype != SPA_TYPE_Id)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_port_methods, subscribe_params, 0,
+ ids, n_ids);
+}
+
+static int port_marshal_enum_params(void *object, int seq, uint32_t id,
+ uint32_t index, uint32_t num, const struct spa_pod *filter)
+{
+ struct pw_protocol_native_message *msg;
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_proxy(proxy, PW_PORT_METHOD_ENUM_PARAMS, &msg);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(SPA_RESULT_RETURN_ASYNC(msg->seq)),
+ SPA_POD_Id(id),
+ SPA_POD_Int(index),
+ SPA_POD_Int(num),
+ SPA_POD_Pod(filter));
+
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int port_demarshal_enum_params(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ uint32_t id, index, num;
+ int seq;
+ struct spa_pod *filter;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&seq),
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&index),
+ SPA_POD_Int(&num),
+ SPA_POD_Pod(&filter)) < 0)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_port_methods, enum_params, 0,
+ seq, id, index, num, filter);
+}
+
+static int client_method_marshal_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct pw_client_events *events,
+ void *data)
+{
+ struct pw_proxy *proxy = object;
+ pw_proxy_add_object_listener(proxy, listener, events, data);
+ return 0;
+}
+
+static void client_marshal_info(void *data, const struct pw_client_info *info)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+ struct spa_pod_frame f;
+
+ b = pw_protocol_native_begin_resource(resource, PW_CLIENT_EVENT_INFO, NULL);
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_add(b,
+ SPA_POD_Int(info->id),
+ SPA_POD_Long(info->change_mask),
+ NULL);
+ push_dict(b, info->change_mask & PW_CLIENT_CHANGE_MASK_PROPS ? info->props : NULL);
+ spa_pod_builder_pop(b, &f);
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static int client_demarshal_info(void *data, const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ struct spa_pod_frame f[2];
+ struct spa_dict props = SPA_DICT_INIT(NULL, 0);
+ struct pw_client_info info = { .props = &props };
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_push_struct(&prs, &f[0]) < 0 ||
+ spa_pod_parser_get(&prs,
+ SPA_POD_Int(&info.id),
+ SPA_POD_Long(&info.change_mask), NULL) < 0)
+ return -EINVAL;
+
+ parse_dict_struct(&prs, &f[1], &props);
+
+ return pw_proxy_notify(proxy, struct pw_client_events, info, 0, &info);
+}
+
+static void client_marshal_permissions(void *data, uint32_t index, uint32_t n_permissions,
+ const struct pw_permission *permissions)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+ struct spa_pod_frame f[2];
+ uint32_t i, n = 0;
+
+ b = pw_protocol_native_begin_resource(resource, PW_CLIENT_EVENT_PERMISSIONS, NULL);
+
+ for (i = 0; i < n_permissions; i++) {
+ if (permissions[i].permissions != PW_PERM_INVALID)
+ n++;
+ }
+
+ spa_pod_builder_push_struct(b, &f[0]);
+ spa_pod_builder_int(b, index);
+ spa_pod_builder_push_struct(b, &f[1]);
+ spa_pod_builder_int(b, n);
+
+ for (i = 0; i < n_permissions; i++) {
+ if (permissions[i].permissions == PW_PERM_INVALID)
+ continue;
+ spa_pod_builder_int(b, permissions[i].id);
+ spa_pod_builder_int(b, permissions[i].permissions);
+ }
+ spa_pod_builder_pop(b, &f[1]);
+ spa_pod_builder_pop(b, &f[0]);
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static int client_demarshal_permissions(void *data, const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct pw_permission *permissions;
+ struct spa_pod_parser prs;
+ struct spa_pod_frame f[2];
+ uint32_t index, n_permissions;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_push_struct(&prs, &f[0]) < 0 ||
+ spa_pod_parser_get(&prs,
+ SPA_POD_Int(&index), NULL) < 0)
+ return -EINVAL;
+
+ parse_permissions_struct(&prs, &f[1], n_permissions, permissions);
+
+ return pw_proxy_notify(proxy, struct pw_client_events, permissions, 0, index, n_permissions, permissions);
+}
+
+static int client_marshal_error(void *object, uint32_t id, int res, const char *error)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_proxy(proxy, PW_CLIENT_METHOD_ERROR, NULL);
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(id),
+ SPA_POD_Int(res),
+ SPA_POD_String(error));
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int client_demarshal_error(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ uint32_t id, res;
+ const char *error;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&id),
+ SPA_POD_Int(&res),
+ SPA_POD_String(&error)) < 0)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_client_methods, error, 0, id, res, error);
+}
+
+static int client_marshal_get_permissions(void *object, uint32_t index, uint32_t num)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_proxy(proxy, PW_CLIENT_METHOD_GET_PERMISSIONS, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(index),
+ SPA_POD_Int(num));
+
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int client_marshal_update_properties(void *object, const struct spa_dict *props)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+ struct spa_pod_frame f;
+
+ b = pw_protocol_native_begin_proxy(proxy, PW_CLIENT_METHOD_UPDATE_PROPERTIES, NULL);
+
+ spa_pod_builder_push_struct(b, &f);
+ push_dict(b, props);
+ spa_pod_builder_pop(b, &f);
+
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int client_demarshal_update_properties(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_dict props = SPA_DICT_INIT(NULL, 0);
+ struct spa_pod_parser prs;
+ struct spa_pod_frame f[2];
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_push_struct(&prs, &f[0]) < 0)
+ return -EINVAL;
+
+ parse_dict_struct(&prs, &f[1], &props);
+
+ return pw_resource_notify(resource, struct pw_client_methods, update_properties, 0,
+ &props);
+}
+
+static int client_demarshal_get_permissions(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ uint32_t index, num;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&index),
+ SPA_POD_Int(&num)) < 0)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_client_methods, get_permissions, 0, index, num);
+}
+
+static int client_marshal_update_permissions(void *object, uint32_t n_permissions,
+ const struct pw_permission *permissions)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+ struct spa_pod_frame f;
+ uint32_t i;
+
+ b = pw_protocol_native_begin_proxy(proxy, PW_CLIENT_METHOD_UPDATE_PERMISSIONS, NULL);
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_int(b, n_permissions);
+ for (i = 0; i < n_permissions; i++) {
+ spa_pod_builder_int(b, permissions[i].id);
+ spa_pod_builder_int(b, permissions[i].permissions);
+ }
+ spa_pod_builder_pop(b, &f);
+
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int client_demarshal_update_permissions(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct pw_permission *permissions;
+ struct spa_pod_parser prs;
+ struct spa_pod_frame f[1];
+ uint32_t n_permissions;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+
+ parse_permissions_struct(&prs, &f[0], n_permissions, permissions);
+
+ return pw_resource_notify(resource, struct pw_client_methods, update_permissions, 0,
+ n_permissions, permissions);
+}
+
+static int link_method_marshal_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct pw_link_events *events,
+ void *data)
+{
+ struct pw_proxy *proxy = object;
+ pw_proxy_add_object_listener(proxy, listener, events, data);
+ return 0;
+}
+
+static void link_marshal_info(void *data, const struct pw_link_info *info)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+ struct spa_pod_frame f;
+
+ b = pw_protocol_native_begin_resource(resource, PW_LINK_EVENT_INFO, NULL);
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_add(b,
+ SPA_POD_Int(info->id),
+ SPA_POD_Int(info->output_node_id),
+ SPA_POD_Int(info->output_port_id),
+ SPA_POD_Int(info->input_node_id),
+ SPA_POD_Int(info->input_port_id),
+ SPA_POD_Long(info->change_mask),
+ SPA_POD_Int(info->state),
+ SPA_POD_String(info->error),
+ SPA_POD_Pod(info->format),
+ NULL);
+ push_dict(b, info->change_mask & PW_LINK_CHANGE_MASK_PROPS ? info->props : NULL);
+ spa_pod_builder_pop(b, &f);
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static int link_demarshal_info(void *data, const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ struct spa_pod_frame f[2];
+ struct spa_dict props = SPA_DICT_INIT(NULL, 0);
+ struct pw_link_info info = { .props = &props };
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_push_struct(&prs, &f[0]) < 0 ||
+ spa_pod_parser_get(&prs,
+ SPA_POD_Int(&info.id),
+ SPA_POD_Int(&info.output_node_id),
+ SPA_POD_Int(&info.output_port_id),
+ SPA_POD_Int(&info.input_node_id),
+ SPA_POD_Int(&info.input_port_id),
+ SPA_POD_Long(&info.change_mask),
+ SPA_POD_Int(&info.state),
+ SPA_POD_String(&info.error),
+ SPA_POD_Pod(&info.format), NULL) < 0)
+ return -EINVAL;
+
+ parse_dict_struct(&prs, &f[1], &props);
+
+ return pw_proxy_notify(proxy, struct pw_link_events, info, 0, &info);
+}
+
+static int registry_demarshal_global(void *data, const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ struct spa_pod_frame f[2];
+ uint32_t id, permissions, version;
+ char *type;
+ struct spa_dict props = SPA_DICT_INIT(NULL, 0);
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_push_struct(&prs, &f[0]) < 0 ||
+ spa_pod_parser_get(&prs,
+ SPA_POD_Int(&id),
+ SPA_POD_Int(&permissions),
+ SPA_POD_String(&type),
+ SPA_POD_Int(&version), NULL) < 0)
+ return -EINVAL;
+
+ parse_dict_struct(&prs, &f[1], &props);
+
+ return pw_proxy_notify(proxy, struct pw_registry_events,
+ global, 0, id, permissions, type, version, &props);
+}
+
+static int registry_demarshal_global_remove(void *data, const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ uint32_t id;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&id)) < 0)
+ return -EINVAL;
+
+ return pw_proxy_notify(proxy, struct pw_registry_events, global_remove, 0, id);
+}
+
+static void * registry_marshal_bind(void *object, uint32_t id,
+ const char *type, uint32_t version, size_t user_data_size)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+ struct pw_proxy *res;
+ uint32_t new_id;
+
+ res = pw_proxy_new(object, type, version, user_data_size);
+ if (res == NULL)
+ return NULL;
+
+ new_id = pw_proxy_get_id(res);
+
+ b = pw_protocol_native_begin_proxy(proxy, PW_REGISTRY_METHOD_BIND, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(id),
+ SPA_POD_String(type),
+ SPA_POD_Int(version),
+ SPA_POD_Int(new_id));
+
+ pw_protocol_native_end_proxy(proxy, b);
+
+ return (void *) res;
+}
+
+static int registry_marshal_destroy(void *object, uint32_t id)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_proxy(proxy, PW_REGISTRY_METHOD_DESTROY, NULL);
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(id));
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static const struct pw_core_methods pw_protocol_native_core_method_marshal = {
+ PW_VERSION_CORE_METHODS,
+ .add_listener = &core_method_marshal_add_listener,
+ .hello = &core_method_marshal_hello,
+ .sync = &core_method_marshal_sync,
+ .pong = &core_method_marshal_pong,
+ .error = &core_method_marshal_error,
+ .get_registry = &core_method_marshal_get_registry,
+ .create_object = &core_method_marshal_create_object,
+ .destroy = &core_method_marshal_destroy,
+};
+
+static const struct pw_protocol_native_demarshal pw_protocol_native_core_method_demarshal[PW_CORE_METHOD_NUM] = {
+ [PW_CORE_METHOD_ADD_LISTENER] = { NULL, 0, },
+ [PW_CORE_METHOD_HELLO] = { &core_method_demarshal_hello, 0, },
+ [PW_CORE_METHOD_SYNC] = { &core_method_demarshal_sync, 0, },
+ [PW_CORE_METHOD_PONG] = { &core_method_demarshal_pong, 0, },
+ [PW_CORE_METHOD_ERROR] = { &core_method_demarshal_error, 0, },
+ [PW_CORE_METHOD_GET_REGISTRY] = { &core_method_demarshal_get_registry, 0, },
+ [PW_CORE_METHOD_CREATE_OBJECT] = { &core_method_demarshal_create_object, 0, },
+ [PW_CORE_METHOD_DESTROY] = { &core_method_demarshal_destroy, 0, }
+};
+
+static const struct pw_core_events pw_protocol_native_core_event_marshal = {
+ PW_VERSION_CORE_EVENTS,
+ .info = &core_event_marshal_info,
+ .done = &core_event_marshal_done,
+ .ping = &core_event_marshal_ping,
+ .error = &core_event_marshal_error,
+ .remove_id = &core_event_marshal_remove_id,
+ .bound_id = &core_event_marshal_bound_id,
+ .add_mem = &core_event_marshal_add_mem,
+ .remove_mem = &core_event_marshal_remove_mem,
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_core_event_demarshal[PW_CORE_EVENT_NUM] =
+{
+ [PW_CORE_EVENT_INFO] = { &core_event_demarshal_info, 0, },
+ [PW_CORE_EVENT_DONE] = { &core_event_demarshal_done, 0, },
+ [PW_CORE_EVENT_PING] = { &core_event_demarshal_ping, 0, },
+ [PW_CORE_EVENT_ERROR] = { &core_event_demarshal_error, 0, },
+ [PW_CORE_EVENT_REMOVE_ID] = { &core_event_demarshal_remove_id, 0, },
+ [PW_CORE_EVENT_BOUND_ID] = { &core_event_demarshal_bound_id, 0, },
+ [PW_CORE_EVENT_ADD_MEM] = { &core_event_demarshal_add_mem, 0, },
+ [PW_CORE_EVENT_REMOVE_MEM] = { &core_event_demarshal_remove_mem, 0, },
+};
+
+static const struct pw_protocol_marshal pw_protocol_native_core_marshal = {
+ PW_TYPE_INTERFACE_Core,
+ PW_VERSION_CORE,
+ 0,
+ PW_CORE_METHOD_NUM,
+ PW_CORE_EVENT_NUM,
+ .client_marshal = &pw_protocol_native_core_method_marshal,
+ .server_demarshal = pw_protocol_native_core_method_demarshal,
+ .server_marshal = &pw_protocol_native_core_event_marshal,
+ .client_demarshal = pw_protocol_native_core_event_demarshal,
+};
+
+static const struct pw_registry_methods pw_protocol_native_registry_method_marshal = {
+ PW_VERSION_REGISTRY_METHODS,
+ .add_listener = &registry_method_marshal_add_listener,
+ .bind = &registry_marshal_bind,
+ .destroy = &registry_marshal_destroy,
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_registry_method_demarshal[PW_REGISTRY_METHOD_NUM] =
+{
+ [PW_REGISTRY_METHOD_ADD_LISTENER] = { NULL, 0, },
+ [PW_REGISTRY_METHOD_BIND] = { &registry_demarshal_bind, 0, },
+ [PW_REGISTRY_METHOD_DESTROY] = { &registry_demarshal_destroy, 0, },
+};
+
+static const struct pw_registry_events pw_protocol_native_registry_event_marshal = {
+ PW_VERSION_REGISTRY_EVENTS,
+ .global = &registry_marshal_global,
+ .global_remove = &registry_marshal_global_remove,
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_registry_event_demarshal[PW_REGISTRY_EVENT_NUM] =
+{
+ [PW_REGISTRY_EVENT_GLOBAL] = { &registry_demarshal_global, 0, },
+ [PW_REGISTRY_EVENT_GLOBAL_REMOVE] = { &registry_demarshal_global_remove, 0, }
+};
+
+static const struct pw_protocol_marshal pw_protocol_native_registry_marshal = {
+ PW_TYPE_INTERFACE_Registry,
+ PW_VERSION_REGISTRY,
+ 0,
+ PW_REGISTRY_METHOD_NUM,
+ PW_REGISTRY_EVENT_NUM,
+ .client_marshal = &pw_protocol_native_registry_method_marshal,
+ .server_demarshal = pw_protocol_native_registry_method_demarshal,
+ .server_marshal = &pw_protocol_native_registry_event_marshal,
+ .client_demarshal = pw_protocol_native_registry_event_demarshal,
+};
+
+static const struct pw_module_events pw_protocol_native_module_event_marshal = {
+ PW_VERSION_MODULE_EVENTS,
+ .info = &module_marshal_info,
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_module_event_demarshal[PW_MODULE_EVENT_NUM] =
+{
+ [PW_MODULE_EVENT_INFO] = { &module_demarshal_info, 0, },
+};
+
+
+static const struct pw_module_methods pw_protocol_native_module_method_marshal = {
+ PW_VERSION_MODULE_METHODS,
+ .add_listener = &module_method_marshal_add_listener,
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_module_method_demarshal[PW_MODULE_METHOD_NUM] =
+{
+ [PW_MODULE_METHOD_ADD_LISTENER] = { NULL, 0, },
+};
+
+static const struct pw_protocol_marshal pw_protocol_native_module_marshal = {
+ PW_TYPE_INTERFACE_Module,
+ PW_VERSION_MODULE,
+ 0,
+ PW_MODULE_METHOD_NUM,
+ PW_MODULE_EVENT_NUM,
+ .client_marshal = &pw_protocol_native_module_method_marshal,
+ .server_demarshal = pw_protocol_native_module_method_demarshal,
+ .server_marshal = &pw_protocol_native_module_event_marshal,
+ .client_demarshal = pw_protocol_native_module_event_demarshal,
+};
+
+static const struct pw_factory_events pw_protocol_native_factory_event_marshal = {
+ PW_VERSION_FACTORY_EVENTS,
+ .info = &factory_marshal_info,
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_factory_event_demarshal[PW_FACTORY_EVENT_NUM] =
+{
+ [PW_FACTORY_EVENT_INFO] = { &factory_demarshal_info, 0, },
+};
+
+static const struct pw_factory_methods pw_protocol_native_factory_method_marshal = {
+ PW_VERSION_FACTORY_METHODS,
+ .add_listener = &factory_method_marshal_add_listener,
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_factory_method_demarshal[PW_FACTORY_METHOD_NUM] =
+{
+ [PW_FACTORY_METHOD_ADD_LISTENER] = { NULL, 0, },
+};
+
+static const struct pw_protocol_marshal pw_protocol_native_factory_marshal = {
+ PW_TYPE_INTERFACE_Factory,
+ PW_VERSION_FACTORY,
+ 0,
+ PW_FACTORY_METHOD_NUM,
+ PW_FACTORY_EVENT_NUM,
+ .client_marshal = &pw_protocol_native_factory_method_marshal,
+ .server_demarshal = pw_protocol_native_factory_method_demarshal,
+ .server_marshal = &pw_protocol_native_factory_event_marshal,
+ .client_demarshal = pw_protocol_native_factory_event_demarshal,
+};
+
+static const struct pw_device_methods pw_protocol_native_device_method_marshal = {
+ PW_VERSION_DEVICE_METHODS,
+ .add_listener = &device_method_marshal_add_listener,
+ .subscribe_params = &device_marshal_subscribe_params,
+ .enum_params = &device_marshal_enum_params,
+ .set_param = &device_marshal_set_param,
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_device_method_demarshal[PW_DEVICE_METHOD_NUM] = {
+ [PW_DEVICE_METHOD_ADD_LISTENER] = { NULL, 0, },
+ [PW_DEVICE_METHOD_SUBSCRIBE_PARAMS] = { &device_demarshal_subscribe_params, 0, },
+ [PW_DEVICE_METHOD_ENUM_PARAMS] = { &device_demarshal_enum_params, 0, },
+ [PW_DEVICE_METHOD_SET_PARAM] = { &device_demarshal_set_param, PW_PERM_W, },
+};
+
+static const struct pw_device_events pw_protocol_native_device_event_marshal = {
+ PW_VERSION_DEVICE_EVENTS,
+ .info = &device_marshal_info,
+ .param = &device_marshal_param,
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_device_event_demarshal[PW_DEVICE_EVENT_NUM] = {
+ [PW_DEVICE_EVENT_INFO] = { &device_demarshal_info, 0, },
+ [PW_DEVICE_EVENT_PARAM] = { &device_demarshal_param, 0, }
+};
+
+static const struct pw_protocol_marshal pw_protocol_native_device_marshal = {
+ PW_TYPE_INTERFACE_Device,
+ PW_VERSION_DEVICE,
+ 0,
+ PW_DEVICE_METHOD_NUM,
+ PW_DEVICE_EVENT_NUM,
+ .client_marshal = &pw_protocol_native_device_method_marshal,
+ .server_demarshal = pw_protocol_native_device_method_demarshal,
+ .server_marshal = &pw_protocol_native_device_event_marshal,
+ .client_demarshal = pw_protocol_native_device_event_demarshal,
+};
+
+static const struct pw_node_methods pw_protocol_native_node_method_marshal = {
+ PW_VERSION_NODE_METHODS,
+ .add_listener = &node_method_marshal_add_listener,
+ .subscribe_params = &node_marshal_subscribe_params,
+ .enum_params = &node_marshal_enum_params,
+ .set_param = &node_marshal_set_param,
+ .send_command = &node_marshal_send_command,
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_node_method_demarshal[PW_NODE_METHOD_NUM] =
+{
+ [PW_NODE_METHOD_ADD_LISTENER] = { NULL, 0, },
+ [PW_NODE_METHOD_SUBSCRIBE_PARAMS] = { &node_demarshal_subscribe_params, 0, },
+ [PW_NODE_METHOD_ENUM_PARAMS] = { &node_demarshal_enum_params, 0, },
+ [PW_NODE_METHOD_SET_PARAM] = { &node_demarshal_set_param, PW_PERM_W, },
+ [PW_NODE_METHOD_SEND_COMMAND] = { &node_demarshal_send_command, PW_PERM_W, },
+};
+
+static const struct pw_node_events pw_protocol_native_node_event_marshal = {
+ PW_VERSION_NODE_EVENTS,
+ .info = &node_marshal_info,
+ .param = &node_marshal_param,
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_node_event_demarshal[PW_NODE_EVENT_NUM] = {
+ [PW_NODE_EVENT_INFO] = { &node_demarshal_info, 0, },
+ [PW_NODE_EVENT_PARAM] = { &node_demarshal_param, 0, }
+};
+
+static const struct pw_protocol_marshal pw_protocol_native_node_marshal = {
+ PW_TYPE_INTERFACE_Node,
+ PW_VERSION_NODE,
+ 0,
+ PW_NODE_METHOD_NUM,
+ PW_NODE_EVENT_NUM,
+ .client_marshal = &pw_protocol_native_node_method_marshal,
+ .server_demarshal = pw_protocol_native_node_method_demarshal,
+ .server_marshal = &pw_protocol_native_node_event_marshal,
+ .client_demarshal = pw_protocol_native_node_event_demarshal,
+};
+
+
+static const struct pw_port_methods pw_protocol_native_port_method_marshal = {
+ PW_VERSION_PORT_METHODS,
+ .add_listener = &port_method_marshal_add_listener,
+ .subscribe_params = &port_marshal_subscribe_params,
+ .enum_params = &port_marshal_enum_params,
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_port_method_demarshal[PW_PORT_METHOD_NUM] =
+{
+ [PW_PORT_METHOD_ADD_LISTENER] = { NULL, 0, },
+ [PW_PORT_METHOD_SUBSCRIBE_PARAMS] = { &port_demarshal_subscribe_params, 0, },
+ [PW_PORT_METHOD_ENUM_PARAMS] = { &port_demarshal_enum_params, 0, },
+};
+
+static const struct pw_port_events pw_protocol_native_port_event_marshal = {
+ PW_VERSION_PORT_EVENTS,
+ .info = &port_marshal_info,
+ .param = &port_marshal_param,
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_port_event_demarshal[PW_PORT_EVENT_NUM] =
+{
+ [PW_PORT_EVENT_INFO] = { &port_demarshal_info, 0, },
+ [PW_PORT_EVENT_PARAM] = { &port_demarshal_param, 0, }
+};
+
+static const struct pw_protocol_marshal pw_protocol_native_port_marshal = {
+ PW_TYPE_INTERFACE_Port,
+ PW_VERSION_PORT,
+ 0,
+ PW_PORT_METHOD_NUM,
+ PW_PORT_EVENT_NUM,
+ .client_marshal = &pw_protocol_native_port_method_marshal,
+ .server_demarshal = pw_protocol_native_port_method_demarshal,
+ .server_marshal = &pw_protocol_native_port_event_marshal,
+ .client_demarshal = pw_protocol_native_port_event_demarshal,
+};
+
+static const struct pw_client_methods pw_protocol_native_client_method_marshal = {
+ PW_VERSION_CLIENT_METHODS,
+ .add_listener = &client_method_marshal_add_listener,
+ .error = &client_marshal_error,
+ .update_properties = &client_marshal_update_properties,
+ .get_permissions = &client_marshal_get_permissions,
+ .update_permissions = &client_marshal_update_permissions,
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_client_method_demarshal[PW_CLIENT_METHOD_NUM] =
+{
+ [PW_CLIENT_METHOD_ADD_LISTENER] = { NULL, 0, },
+ [PW_CLIENT_METHOD_ERROR] = { &client_demarshal_error, PW_PERM_W, },
+ [PW_CLIENT_METHOD_UPDATE_PROPERTIES] = { &client_demarshal_update_properties, PW_PERM_W, },
+ [PW_CLIENT_METHOD_GET_PERMISSIONS] = { &client_demarshal_get_permissions, 0, },
+ [PW_CLIENT_METHOD_UPDATE_PERMISSIONS] = { &client_demarshal_update_permissions, PW_PERM_W, },
+};
+
+static const struct pw_client_events pw_protocol_native_client_event_marshal = {
+ PW_VERSION_CLIENT_EVENTS,
+ .info = &client_marshal_info,
+ .permissions = &client_marshal_permissions,
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_client_event_demarshal[PW_CLIENT_EVENT_NUM] =
+{
+ [PW_CLIENT_EVENT_INFO] = { &client_demarshal_info, 0, },
+ [PW_CLIENT_EVENT_PERMISSIONS] = { &client_demarshal_permissions, 0, }
+};
+
+static const struct pw_protocol_marshal pw_protocol_native_client_marshal = {
+ PW_TYPE_INTERFACE_Client,
+ PW_VERSION_CLIENT,
+ 0,
+ PW_CLIENT_METHOD_NUM,
+ PW_CLIENT_EVENT_NUM,
+ .client_marshal = &pw_protocol_native_client_method_marshal,
+ .server_demarshal = pw_protocol_native_client_method_demarshal,
+ .server_marshal = &pw_protocol_native_client_event_marshal,
+ .client_demarshal = pw_protocol_native_client_event_demarshal,
+};
+
+
+static const struct pw_link_methods pw_protocol_native_link_method_marshal = {
+ PW_VERSION_LINK_METHODS,
+ .add_listener = &link_method_marshal_add_listener,
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_link_method_demarshal[PW_LINK_METHOD_NUM] =
+{
+ [PW_LINK_METHOD_ADD_LISTENER] = { NULL, 0, },
+};
+
+static const struct pw_link_events pw_protocol_native_link_event_marshal = {
+ PW_VERSION_LINK_EVENTS,
+ .info = &link_marshal_info,
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_link_event_demarshal[PW_LINK_EVENT_NUM] =
+{
+ [PW_LINK_EVENT_INFO] = { &link_demarshal_info, 0, }
+};
+
+static const struct pw_protocol_marshal pw_protocol_native_link_marshal = {
+ PW_TYPE_INTERFACE_Link,
+ PW_VERSION_LINK,
+ 0,
+ PW_LINK_METHOD_NUM,
+ PW_LINK_EVENT_NUM,
+ .client_marshal = &pw_protocol_native_link_method_marshal,
+ .server_demarshal = pw_protocol_native_link_method_demarshal,
+ .server_marshal = &pw_protocol_native_link_event_marshal,
+ .client_demarshal = pw_protocol_native_link_event_demarshal,
+};
+
+void pw_protocol_native_init(struct pw_protocol *protocol)
+{
+ pw_protocol_add_marshal(protocol, &pw_protocol_native_core_marshal);
+ pw_protocol_add_marshal(protocol, &pw_protocol_native_registry_marshal);
+ pw_protocol_add_marshal(protocol, &pw_protocol_native_module_marshal);
+ pw_protocol_add_marshal(protocol, &pw_protocol_native_device_marshal);
+ pw_protocol_add_marshal(protocol, &pw_protocol_native_node_marshal);
+ pw_protocol_add_marshal(protocol, &pw_protocol_native_port_marshal);
+ pw_protocol_add_marshal(protocol, &pw_protocol_native_factory_marshal);
+ pw_protocol_add_marshal(protocol, &pw_protocol_native_client_marshal);
+ pw_protocol_add_marshal(protocol, &pw_protocol_native_link_marshal);
+}
diff --git a/src/modules/module-protocol-native/test-connection.c b/src/modules/module-protocol-native/test-connection.c
new file mode 100644
index 0000000..c7d2f69
--- /dev/null
+++ b/src/modules/module-protocol-native/test-connection.c
@@ -0,0 +1,225 @@
+/* PipeWire
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <sys/socket.h>
+
+#include <spa/pod/builder.h>
+#include <spa/pod/parser.h>
+#include <spa/utils/result.h>
+
+#include <pipewire/pipewire.h>
+
+#include "connection.h"
+
+#define NAME "protocol-native"
+PW_LOG_TOPIC(mod_topic, "mod." NAME);
+PW_LOG_TOPIC(mod_topic_connection, "conn." NAME);
+
+static void test_create(struct pw_protocol_native_connection *conn)
+{
+ const struct pw_protocol_native_message *msg;
+ int res;
+
+ res = pw_protocol_native_connection_get_next(conn, &msg);
+ spa_assert_se(res != 1);
+
+ res = pw_protocol_native_connection_get_fd(conn, 0);
+ spa_assert_se(res == -ENOENT);
+
+ res = pw_protocol_native_connection_flush(conn);
+ spa_assert_se(res == 0);
+
+ res = pw_protocol_native_connection_clear(conn);
+ spa_assert_se(res == 0);
+}
+
+static void write_message(struct pw_protocol_native_connection *conn, int fd)
+{
+ struct pw_protocol_native_message *msg;
+ struct spa_pod_builder *b;
+ int seq = -1, res;
+
+ b = pw_protocol_native_connection_begin(conn, 1, 5, &msg);
+ spa_assert_se(b != NULL);
+ spa_assert_se(msg->seq != -1);
+
+ seq = SPA_RESULT_RETURN_ASYNC(msg->seq);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(42),
+ SPA_POD_Id(SPA_TYPE_Object),
+ SPA_POD_Int(pw_protocol_native_connection_add_fd(conn, fd)));
+
+ res = pw_protocol_native_connection_end(conn, b);
+ spa_assert_se(seq == res);
+}
+
+static int read_message(struct pw_protocol_native_connection *conn,
+ const struct pw_protocol_native_message **pmsg)
+{
+ struct spa_pod_parser prs;
+ const struct pw_protocol_native_message *msg;
+ int res, fd;
+ uint32_t v_int, v_id, fdidx;
+
+ res = pw_protocol_native_connection_get_next(conn, &msg);
+ if (res != 1) {
+ pw_log_error("got %d", res);
+ return -1;
+ }
+
+ if (pmsg)
+ *pmsg = msg;
+
+ spa_assert_se(msg->opcode == 5);
+ spa_assert_se(msg->id == 1);
+ spa_assert_se(msg->data != NULL);
+ spa_assert_se(msg->size > 0);
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&v_int),
+ SPA_POD_Id(&v_id),
+ SPA_POD_Int(&fdidx)) < 0)
+ spa_assert_not_reached();
+
+ fd = pw_protocol_native_connection_get_fd(conn, fdidx);
+ spa_assert_se(fd != -ENOENT);
+ pw_log_debug("got fd %d %d", fdidx, fd);
+ return 0;
+}
+
+static void test_read_write(struct pw_protocol_native_connection *in,
+ struct pw_protocol_native_connection *out)
+{
+ write_message(out, 1);
+ pw_protocol_native_connection_flush(out);
+ write_message(out, 2);
+ pw_protocol_native_connection_flush(out);
+ spa_assert_se(read_message(in, NULL) == 0);
+ spa_assert_se(read_message(in, NULL) == 0);
+ spa_assert_se(read_message(in, NULL) == -1);
+
+ write_message(out, 1);
+ write_message(out, 2);
+ pw_protocol_native_connection_flush(out);
+ spa_assert_se(read_message(in, NULL) == 0);
+ spa_assert_se(read_message(in, NULL) == 0);
+ spa_assert_se(read_message(in, NULL) == -1);
+}
+
+static void test_reentering(struct pw_protocol_native_connection *in,
+ struct pw_protocol_native_connection *out)
+{
+ const struct pw_protocol_native_message *msg1, *msg2;
+ int i;
+
+#define READ_MSG(idx) \
+ spa_assert_se(read_message(in, &msg ## idx) == 0); \
+ spa_assert_se((msg ## idx)->n_fds == 1); \
+ spa_assert_se((msg ## idx)->size < sizeof(buf ## idx)); \
+ fd ## idx = (msg ## idx)->fds[0]; \
+ memcpy(buf ## idx, (msg ## idx)->data, (msg ## idx)->size); \
+ size ## idx = (msg ## idx)->size
+
+#define CHECK_MSG(idx) \
+ spa_assert_se((msg ## idx)->fds[0] == fd ## idx); \
+ spa_assert_se(memcmp((msg ## idx)->data, buf ## idx, size ## idx) == 0)
+
+ for (i = 0; i < 50; ++i) {
+ int fd1, fd2;
+ char buf1[1024], buf2[1024];
+ int size1, size2;
+
+ write_message(out, 1);
+ write_message(out, 2);
+ write_message(out, 1);
+ write_message(out, 2);
+ write_message(out, 1);
+ pw_protocol_native_connection_flush(out);
+
+ READ_MSG(1);
+ pw_protocol_native_connection_enter(in); /* 1 */
+ READ_MSG(2);
+ CHECK_MSG(1);
+ pw_protocol_native_connection_enter(in); /* 2 */
+ pw_protocol_native_connection_leave(in); /* 2 */
+ CHECK_MSG(1);
+ CHECK_MSG(2);
+ pw_protocol_native_connection_enter(in); /* 2 */
+ pw_protocol_native_connection_enter(in); /* 3 */
+ spa_assert_se(read_message(in, NULL) == 0);
+ CHECK_MSG(1);
+ CHECK_MSG(2);
+ pw_protocol_native_connection_leave(in); /* 3 */
+ spa_assert_se(read_message(in, NULL) == 0);
+ CHECK_MSG(1);
+ CHECK_MSG(2);
+ pw_protocol_native_connection_leave(in); /* 2 */
+ CHECK_MSG(2);
+ spa_assert_se(read_message(in, NULL) == 0);
+ CHECK_MSG(1);
+ pw_protocol_native_connection_leave(in); /* 1 */
+ CHECK_MSG(1);
+ }
+}
+
+int main(int argc, char *argv[])
+{
+ struct pw_main_loop *loop;
+ struct pw_context *context;
+ struct pw_protocol_native_connection *in, *out;
+ int fds[2];
+
+ pw_init(&argc, &argv);
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+ PW_LOG_TOPIC_INIT(mod_topic_connection);
+
+ loop = pw_main_loop_new(NULL);
+ spa_assert_se(loop != NULL);
+ context = pw_context_new(pw_main_loop_get_loop(loop), NULL, 0);
+
+ if (socketpair(AF_UNIX, SOCK_STREAM, 0, fds) < 0) {
+ spa_assert_not_reached();
+ return -1;
+ }
+
+ in = pw_protocol_native_connection_new(context, fds[0]);
+ spa_assert_se(in != NULL);
+ out = pw_protocol_native_connection_new(context, fds[1]);
+ spa_assert_se(out != NULL);
+
+ test_create(in);
+ test_create(out);
+ test_read_write(in, out);
+ test_reentering(in, out);
+
+ pw_protocol_native_connection_destroy(in);
+ pw_protocol_native_connection_destroy(out);
+ pw_context_destroy(context);
+ pw_main_loop_destroy(loop);
+
+ return 0;
+}
diff --git a/src/modules/module-protocol-native/v0/interfaces.h b/src/modules/module-protocol-native/v0/interfaces.h
new file mode 100644
index 0000000..dca9672
--- /dev/null
+++ b/src/modules/module-protocol-native/v0/interfaces.h
@@ -0,0 +1,534 @@
+/* PipeWire
+ *
+ * Copyright © 2016 Wim Taymans <wim.taymans@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_INTERFACES_V0_H
+#define PIPEWIRE_INTERFACES_V0_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <spa/utils/defs.h>
+#include <spa/param/param.h>
+#include <spa/node/node.h>
+
+#include <pipewire/pipewire.h>
+
+/** Core */
+
+#define PW_VERSION_CORE_V0 0
+
+#define PW_CORE_V0_METHOD_HELLO 0
+#define PW_CORE_V0_METHOD_UPDATE_TYPES 1
+#define PW_CORE_V0_METHOD_SYNC 2
+#define PW_CORE_V0_METHOD_GET_REGISTRY 3
+#define PW_CORE_V0_METHOD_CLIENT_UPDATE 4
+#define PW_CORE_V0_METHOD_PERMISSIONS 5
+#define PW_CORE_V0_METHOD_CREATE_OBJECT 6
+#define PW_CORE_V0_METHOD_DESTROY 7
+#define PW_CORE_V0_METHOD_NUM 8
+
+/**
+ * Key to update default permissions of globals without specific
+ * permissions. value is "[r][w][x]" */
+#define PW_CORE_PERMISSIONS_DEFAULT "permissions.default"
+
+/**
+ * Key to update specific permissions of a global. If the global
+ * did not have specific permissions, it will first be assigned
+ * the default permissions before it is updated.
+ * Value is "<global-id>:[r][w][x]"*/
+#define PW_CORE_PERMISSIONS_GLOBAL "permissions.global"
+
+/**
+ * Key to update specific permissions of all existing globals.
+ * This is equivalent to using \ref PW_CORE_PERMISSIONS_GLOBAL
+ * on each global id individually that did not have specific
+ * permissions.
+ * Value is "[r][w][x]" */
+#define PW_CORE_PERMISSIONS_EXISTING "permissions.existing"
+
+#define PW_LINK_OUTPUT_NODE_ID "link.output_node.id"
+#define PW_LINK_OUTPUT_PORT_ID "link.output_port.id"
+#define PW_LINK_INPUT_NODE_ID "link.input_node.id"
+#define PW_LINK_INPUT_PORT_ID "link.input_port.id"
+
+/**
+ * \struct pw_core_v0_methods
+ * \brief Core methods
+ *
+ * The core global object. This is a singleton object used for
+ * creating new objects in the remote PipeWire instance. It is
+ * also used for internal features.
+ */
+struct pw_core_v0_methods {
+#define PW_VERSION_CORE_V0_METHODS 0
+ uint32_t version;
+ /**
+ * Start a conversation with the server. This will send
+ * the core info and server types.
+ *
+ * All the existing resources for the client (except the core
+ * resource) will be destroyed.
+ */
+ void (*hello) (void *object);
+ /**
+ * Update the type map
+ *
+ * Send a type map update to the PipeWire server. The server uses this
+ * information to keep a mapping between client types and the server types.
+ * \param first_id the id of the first type
+ * \param types the types as a string
+ * \param n_types the number of types
+ */
+ void (*update_types) (void *object,
+ uint32_t first_id,
+ const char **types,
+ uint32_t n_types);
+ /**
+ * Do server roundtrip
+ *
+ * Ask the server to emit the 'done' event with \a id.
+ * Since methods are handled in-order and events are delivered
+ * in-order, this can be used as a barrier to ensure all previous
+ * methods and the resulting events have been handled.
+ * \param seq the sequence number passed to the done event
+ */
+ void (*sync) (void *object, uint32_t seq);
+ /**
+ * Get the registry object
+ *
+ * Create a registry object that allows the client to list and bind
+ * the global objects available from the PipeWire server
+ * \param version the client proxy id
+ * \param id the client proxy id
+ */
+ void (*get_registry) (void *object, uint32_t version, uint32_t new_id);
+ /**
+ * Update the client properties
+ * \param props the new client properties
+ */
+ void (*client_update) (void *object, const struct spa_dict *props);
+ /**
+ * Manage the permissions of the global objects
+ *
+ * Update the permissions of the global objects using the
+ * dictionary with properties.
+ *
+ * Globals can use the default permissions or can have specific
+ * permissions assigned to them.
+ *
+ * \param id the global id to change
+ * \param props dictionary with permission properties
+ */
+ void (*permissions) (void *object, const struct spa_dict *props);
+ /**
+ * Create a new object on the PipeWire server from a factory.
+ * Use a \a factory_name of "client-node" to create a
+ * \ref pw_client_node.
+ *
+ * \param factory_name the factory name to use
+ * \param type the interface to bind to
+ * \param version the version of the interface
+ * \param props extra properties
+ * \param new_id the client proxy id
+ */
+ void (*create_object) (void *object,
+ const char *factory_name,
+ uint32_t type,
+ uint32_t version,
+ const struct spa_dict *props,
+ uint32_t new_id);
+
+ /**
+ * Destroy an object id
+ *
+ * \param id the object id to destroy
+ */
+ void (*destroy) (void *object, uint32_t id);
+};
+
+#define PW_CORE_V0_EVENT_UPDATE_TYPES 0
+#define PW_CORE_V0_EVENT_DONE 1
+#define PW_CORE_V0_EVENT_ERROR 2
+#define PW_CORE_V0_EVENT_REMOVE_ID 3
+#define PW_CORE_V0_EVENT_INFO 4
+#define PW_CORE_V0_EVENT_NUM 5
+
+/** \struct pw_core_v0_events
+ * \brief Core events
+ * \ingroup pw_core_interface The pw_core interface
+ */
+struct pw_core_v0_events {
+#define PW_VERSION_CORE_V0_EVENTS 0
+ uint32_t version;
+ /**
+ * Update the type map
+ *
+ * Send a type map update to the client. The client uses this
+ * information to keep a mapping between server types and the client types.
+ * \param first_id the id of the first type
+ * \param types the types as a string
+ * \param n_types the number of \a types
+ */
+ void (*update_types) (void *data,
+ uint32_t first_id,
+ const char **types,
+ uint32_t n_types);
+ /**
+ * Emit a done event
+ *
+ * The done event is emitted as a result of a sync method with the
+ * same sequence number.
+ * \param seq the sequence number passed to the sync method call
+ */
+ void (*done) (void *data, uint32_t seq);
+ /**
+ * Fatal error event
+ *
+ * The error event is sent out when a fatal (non-recoverable)
+ * error has occurred. The id argument is the object where
+ * the error occurred, most often in response to a request to that
+ * object. The message is a brief description of the error,
+ * for (debugging) convenience.
+ * \param id object where the error occurred
+ * \param res error code
+ * \param error error description
+ */
+ void (*error) (void *data, uint32_t id, int res, const char *error, ...);
+ /**
+ * Remove an object ID
+ *
+ * This event is used internally by the object ID management
+ * logic. When a client deletes an object, the server will send
+ * this event to acknowledge that it has seen the delete request.
+ * When the client receives this event, it will know that it can
+ * safely reuse the object ID.
+ * \param id deleted object ID
+ */
+ void (*remove_id) (void *data, uint32_t id);
+ /**
+ * Notify new core info
+ *
+ * \param info new core info
+ */
+ void (*info) (void *data, struct pw_core_info *info);
+};
+
+#define pw_core_resource_v0_update_types(r,...) pw_resource_notify(r,struct pw_core_v0_events,update_types,__VA_ARGS__)
+#define pw_core_resource_v0_done(r,...) pw_resource_notify(r,struct pw_core_v0_events,done,__VA_ARGS__)
+#define pw_core_resource_v0_error(r,...) pw_resource_notify(r,struct pw_core_v0_events,error,__VA_ARGS__)
+#define pw_core_resource_v0_remove_id(r,...) pw_resource_notify(r,struct pw_core_v0_events,remove_id,__VA_ARGS__)
+#define pw_core_resource_v0_info(r,...) pw_resource_notify(r,struct pw_core_v0_events,info,__VA_ARGS__)
+
+
+#define PW_VERSION_REGISTRY_V0 0
+
+/** \page page_registry Registry
+ *
+ * \section page_registry_overview Overview
+ *
+ * The registry object is a singleton object that keeps track of
+ * global objects on the PipeWire instance. See also \ref page_global.
+ *
+ * Global objects typically represent an actual object in PipeWire
+ * (for example, a module or node) or they are singleton
+ * objects such as the core.
+ *
+ * When a client creates a registry object, the registry object
+ * will emit a global event for each global currently in the
+ * registry. Globals come and go as a result of device hotplugs or
+ * reconfiguration or other events, and the registry will send out
+ * global and global_remove events to keep the client up to date
+ * with the changes. To mark the end of the initial burst of
+ * events, the client can use the pw_core.sync methosd immediately
+ * after calling pw_core.get_registry.
+ *
+ * A client can bind to a global object by using the bind
+ * request. This creates a client-side proxy that lets the object
+ * emit events to the client and lets the client invoke methods on
+ * the object. See \ref page_proxy
+ *
+ * Clients can also change the permissions of the global objects that
+ * it can see. This is interesting when you want to configure a
+ * pipewire session before handing it to another application. You
+ * can, for example, hide certain existing or new objects or limit
+ * the access permissions on an object.
+ */
+#define PW_REGISTRY_V0_METHOD_BIND 0
+#define PW_REGISTRY_V0_METHOD_NUM 1
+
+/** Registry methods */
+struct pw_registry_v0_methods {
+#define PW_VERSION_REGISTRY_V0_METHODS 0
+ uint32_t version;
+ /**
+ * Bind to a global object
+ *
+ * Bind to the global object with \a id and use the client proxy
+ * with new_id as the proxy. After this call, methods can be
+ * send to the remote global object and events can be received
+ *
+ * \param id the global id to bind to
+ * \param type the interface type to bind to
+ * \param version the interface version to use
+ * \param new_id the client proxy to use
+ */
+ void (*bind) (void *object, uint32_t id, uint32_t type, uint32_t version, uint32_t new_id);
+};
+
+#define PW_REGISTRY_V0_EVENT_GLOBAL 0
+#define PW_REGISTRY_V0_EVENT_GLOBAL_REMOVE 1
+#define PW_REGISTRY_V0_EVENT_NUM 2
+
+/** Registry events */
+struct pw_registry_v0_events {
+#define PW_VERSION_REGISTRY_V0_EVENTS 0
+ uint32_t version;
+ /**
+ * Notify of a new global object
+ *
+ * The registry emits this event when a new global object is
+ * available.
+ *
+ * \param id the global object id
+ * \param parent_id the parent global id
+ * \param permissions the permissions of the object
+ * \param type the type of the interface
+ * \param version the version of the interface
+ * \param props extra properties of the global
+ */
+ void (*global) (void *data, uint32_t id, uint32_t parent_id,
+ uint32_t permissions, uint32_t type, uint32_t version,
+ const struct spa_dict *props);
+ /**
+ * Notify of a global object removal
+ *
+ * Emitted when a global object was removed from the registry.
+ * If the client has any bindings to the global, it should destroy
+ * those.
+ *
+ * \param id the id of the global that was removed
+ */
+ void (*global_remove) (void *data, uint32_t id);
+};
+
+#define pw_registry_resource_v0_global(r,...) pw_resource_notify(r,struct pw_registry_v0_events,global,__VA_ARGS__)
+#define pw_registry_resource_v0_global_remove(r,...) pw_resource_notify(r,struct pw_registry_v0_events,global_remove,__VA_ARGS__)
+
+
+#define PW_VERSION_MODULE_V0 0
+
+#define PW_MODULE_V0_EVENT_INFO 0
+#define PW_MODULE_V0_EVENT_NUM 1
+
+/** Module events */
+struct pw_module_v0_events {
+#define PW_VERSION_MODULE_V0_EVENTS 0
+ uint32_t version;
+ /**
+ * Notify module info
+ *
+ * \param info info about the module
+ */
+ void (*info) (void *data, struct pw_module_info *info);
+};
+
+#define pw_module_resource_v0_info(r,...) pw_resource_notify(r,struct pw_module_v0_events,info,__VA_ARGS__)
+
+#define PW_VERSION_NODE_V0 0
+
+#define PW_NODE_V0_EVENT_INFO 0
+#define PW_NODE_V0_EVENT_PARAM 1
+#define PW_NODE_V0_EVENT_NUM 2
+
+/** Node events */
+struct pw_node_v0_events {
+#define PW_VERSION_NODE_V0_EVENTS 0
+ uint32_t version;
+ /**
+ * Notify node info
+ *
+ * \param info info about the node
+ */
+ void (*info) (void *data, struct pw_node_info *info);
+ /**
+ * Notify a node param
+ *
+ * Event emitted as a result of the enum_params method.
+ *
+ * \param id the param id
+ * \param index the param index
+ * \param next the param index of the next param
+ * \param param the parameter
+ */
+ void (*param) (void *data,
+ uint32_t id, uint32_t index, uint32_t next,
+ const struct spa_pod *param);
+};
+
+#define pw_node_resource_v0_info(r,...) pw_resource_notify(r,struct pw_node_v0_events,info,__VA_ARGS__)
+#define pw_node_resource_v0_param(r,...) pw_resource_notify(r,struct pw_node_v0_events,param,__VA_ARGS__)
+
+#define PW_NODE_V0_METHOD_ENUM_PARAMS 0
+#define PW_NODE_V0_METHOD_NUM 1
+
+/** Node methods */
+struct pw_node_v0_methods {
+#define PW_VERSION_NODE_V0_METHODS 0
+ uint32_t version;
+ /**
+ * Enumerate node parameters
+ *
+ * Start enumeration of node parameters. For each param, a
+ * param event will be emitted.
+ *
+ * \param id the parameter id to enum or PW_ID_ANY for all
+ * \param start the start index or 0 for the first param
+ * \param num the maximum number of params to retrieve
+ * \param filter a param filter or NULL
+ */
+ void (*enum_params) (void *object, uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter);
+};
+
+#define PW_VERSION_PORT_V0 0
+
+#define PW_PORT_V0_EVENT_INFO 0
+#define PW_PORT_V0_EVENT_PARAM 1
+#define PW_PORT_V0_EVENT_NUM 2
+
+/** Port events */
+struct pw_port_v0_events {
+#define PW_VERSION_PORT_V0_EVENTS 0
+ uint32_t version;
+ /**
+ * Notify port info
+ *
+ * \param info info about the port
+ */
+ void (*info) (void *data, struct pw_port_info *info);
+ /**
+ * Notify a port param
+ *
+ * Event emitted as a result of the enum_params method.
+ *
+ * \param id the param id
+ * \param index the param index
+ * \param next the param index of the next param
+ * \param param the parameter
+ */
+ void (*param) (void *data,
+ uint32_t id, uint32_t index, uint32_t next,
+ const struct spa_pod *param);
+};
+
+#define pw_port_resource_v0_info(r,...) pw_resource_notify(r,struct pw_port_v0_events,info,__VA_ARGS__)
+#define pw_port_resource_v0_param(r,...) pw_resource_notify(r,struct pw_port_v0_events,param,__VA_ARGS__)
+
+#define PW_PORT_V0_METHOD_ENUM_PARAMS 0
+#define PW_PORT_V0_METHOD_NUM 1
+
+/** Port methods */
+struct pw_port_v0_methods {
+#define PW_VERSION_PORT_V0_METHODS 0
+ uint32_t version;
+ /**
+ * Enumerate port parameters
+ *
+ * Start enumeration of port parameters. For each param, a
+ * param event will be emitted.
+ *
+ * \param id the parameter id to enumerate
+ * \param start the start index or 0 for the first param
+ * \param num the maximum number of params to retrieve
+ * \param filter a param filter or NULL
+ */
+ void (*enum_params) (void *object, uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter);
+};
+
+#define PW_VERSION_FACTORY_V0 0
+
+#define PW_FACTORY_V0_EVENT_INFO 0
+#define PW_FACTORY_V0_EVENT_NUM 1
+
+/** Factory events */
+struct pw_factory_v0_events {
+#define PW_VERSION_FACTORY_V0_EVENTS 0
+ uint32_t version;
+ /**
+ * Notify factory info
+ *
+ * \param info info about the factory
+ */
+ void (*info) (void *data, struct pw_factory_info *info);
+};
+
+#define pw_factory_resource_v0_info(r,...) pw_resource_notify(r,struct pw_factory_v0_events,info,__VA_ARGS__)
+
+#define PW_VERSION_CLIENT_V0 0
+
+#define PW_CLIENT_V0_EVENT_INFO 0
+#define PW_CLIENT_V0_EVENT_NUM 1
+
+/** Client events */
+struct pw_client_v0_events {
+#define PW_VERSION_CLIENT_V0_EVENTS 0
+ uint32_t version;
+ /**
+ * Notify client info
+ *
+ * \param info info about the client
+ */
+ void (*info) (void *data, struct pw_client_info *info);
+};
+
+#define pw_client_resource_v0_info(r,...) pw_resource_notify(r,struct pw_client_v0_events,info,__VA_ARGS__)
+
+
+#define PW_VERSION_LINK_V0 0
+
+#define PW_LINK_V0_EVENT_INFO 0
+#define PW_LINK_V0_EVENT_NUM 1
+
+/** Link events */
+struct pw_link_v0_events {
+#define PW_VERSION_LINK_V0_EVENTS 0
+ uint32_t version;
+ /**
+ * Notify link info
+ *
+ * \param info info about the link
+ */
+ void (*info) (void *data, struct pw_link_info *info);
+};
+
+#define pw_link_resource_v0_info(r,...) pw_resource_notify(r,struct pw_link_v0_events,info,__VA_ARGS__)
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* PIPEWIRE_INTERFACES_V0_H */
diff --git a/src/modules/module-protocol-native/v0/protocol-native.c b/src/modules/module-protocol-native/v0/protocol-native.c
new file mode 100644
index 0000000..d6173ac
--- /dev/null
+++ b/src/modules/module-protocol-native/v0/protocol-native.c
@@ -0,0 +1,1371 @@
+/* PipeWire
+ *
+ * Copyright © 2017 Wim Taymans <wim.taymans@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdio.h>
+#include <errno.h>
+
+#include "spa/pod/parser.h"
+#include "spa/pod/builder.h"
+#include "spa/debug/types.h"
+#include "spa/utils/string.h"
+
+#include "pipewire/pipewire.h"
+#include "pipewire/private.h"
+#include "pipewire/protocol.h"
+#include "pipewire/resource.h"
+#include "pipewire/extensions/protocol-native.h"
+#include "pipewire/extensions/metadata.h"
+#include "pipewire/extensions/session-manager.h"
+#include "pipewire/extensions/client-node.h"
+
+#include "interfaces.h"
+#include "typemap.h"
+
+#include "../connection.h"
+
+PW_LOG_TOPIC_EXTERN(mod_topic);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+#define PW_PROTOCOL_NATIVE_FLAG_REMAP (1<<0)
+
+SPA_EXPORT
+uint32_t pw_protocol_native0_find_type(struct pw_impl_client *client, const char *type)
+{
+ uint32_t i;
+ for (i = 0; i < SPA_N_ELEMENTS(type_map); i++) {
+ if (spa_streq(type_map[i].type, type))
+ return i;
+ }
+ return SPA_ID_INVALID;
+}
+
+static void
+update_types_server(struct pw_resource *resource)
+{
+ struct spa_pod_builder *b;
+ struct spa_pod_frame f;
+ uint32_t i;
+
+ b = pw_protocol_native_begin_resource(resource, PW_CORE_V0_EVENT_UPDATE_TYPES, NULL);
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_add(b,
+ "i", 0,
+ "i", SPA_N_ELEMENTS(type_map), NULL);
+
+ for (i = 0; i < SPA_N_ELEMENTS(type_map); i++) {
+ spa_pod_builder_add(b, "s", type_map[i].type, NULL);
+ }
+ spa_pod_builder_pop(b, &f);
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+
+static void core_marshal_info(void *data, const struct pw_core_info *info)
+{
+ struct pw_resource *resource = data;
+ struct pw_impl_client *client = pw_resource_get_client(resource);
+ struct protocol_compat_v2 *compat_v2 = client->compat_v2;
+ struct spa_pod_builder *b;
+ uint32_t i, n_items;
+ uint64_t change_mask = 0;
+ struct spa_pod_frame f;
+ struct pw_protocol_native_message *msg;
+
+#define PW_CORE_V0_CHANGE_MASK_USER_NAME (1 << 0)
+#define PW_CORE_V0_CHANGE_MASK_HOST_NAME (1 << 1)
+#define PW_CORE_V0_CHANGE_MASK_VERSION (1 << 2)
+#define PW_CORE_V0_CHANGE_MASK_NAME (1 << 3)
+#define PW_CORE_V0_CHANGE_MASK_COOKIE (1 << 4)
+#define PW_CORE_V0_CHANGE_MASK_PROPS (1 << 5)
+
+ if (compat_v2->send_types) {
+ update_types_server(resource);
+ change_mask |= PW_CORE_V0_CHANGE_MASK_USER_NAME |
+ PW_CORE_V0_CHANGE_MASK_HOST_NAME |
+ PW_CORE_V0_CHANGE_MASK_VERSION |
+ PW_CORE_V0_CHANGE_MASK_NAME |
+ PW_CORE_V0_CHANGE_MASK_COOKIE;
+ compat_v2->send_types = false;
+ }
+ b = pw_protocol_native_begin_resource(resource, PW_CORE_V0_EVENT_INFO, &msg);
+
+ n_items = info->props ? info->props->n_items : 0;
+
+ if (info->change_mask & PW_CORE_CHANGE_MASK_PROPS)
+ change_mask |= PW_CORE_V0_CHANGE_MASK_PROPS;
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_add(b,
+ "i", info->id,
+ "l", change_mask,
+ "s", info->user_name,
+ "s", info->host_name,
+ "s", info->version,
+ "s", info->name,
+ "i", info->cookie,
+ "i", n_items, NULL);
+
+ for (i = 0; i < n_items; i++) {
+ spa_pod_builder_add(b,
+ "s", info->props->items[i].key,
+ "s", info->props->items[i].value, NULL);
+ }
+ spa_pod_builder_pop(b, &f);
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static void core_marshal_done(void *data, uint32_t id, int seq)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource, PW_CORE_V0_EVENT_DONE, NULL);
+
+ spa_pod_builder_add_struct(b, "i", seq);
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static void core_marshal_error(void *data, uint32_t id, int seq, int res, const char *error)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource, PW_CORE_V0_EVENT_ERROR, NULL);
+
+ spa_pod_builder_add_struct(b,
+ "i", id,
+ "i", res,
+ "s", error);
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static void core_marshal_remove_id(void *data, uint32_t id)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource, PW_CORE_V0_EVENT_REMOVE_ID, NULL);
+
+ spa_pod_builder_add_struct(b, "i", id);
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static int core_demarshal_client_update(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct pw_impl_client *client = pw_resource_get_client(resource);
+ struct spa_dict props;
+ struct spa_pod_parser prs;
+ struct spa_pod_frame f;
+ uint32_t i;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_push_struct(&prs, &f) < 0 ||
+ spa_pod_parser_get(&prs,
+ "i", &props.n_items, NULL) < 0)
+ return -EINVAL;
+
+ props.items = alloca(props.n_items * sizeof(struct spa_dict_item));
+ for (i = 0; i < props.n_items; i++) {
+ if (spa_pod_parser_get(&prs,
+ "s", &props.items[i].key,
+ "s", &props.items[i].value,
+ NULL) < 0)
+ return -EINVAL;
+ }
+ pw_impl_client_update_properties(client, &props);
+ return 0;
+}
+
+static uint32_t parse_perms(const char *str)
+{
+ uint32_t perms = 0;
+
+ while (*str != '\0') {
+ switch (*str++) {
+ case 'r':
+ perms |= PW_PERM_R;
+ break;
+ case 'w':
+ perms |= PW_PERM_W;
+ break;
+ case 'x':
+ perms |= PW_PERM_X;
+ break;
+ }
+ }
+ return perms;
+}
+
+static int core_demarshal_permissions(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_dict props;
+ struct spa_pod_parser prs;
+ struct spa_pod_frame f;
+ uint32_t i, n_permissions;
+ struct pw_permission *permissions, defperm = { 0, };
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_push_struct(&prs, &f) < 0 ||
+ spa_pod_parser_get(&prs, "i", &props.n_items, NULL) < 0)
+ return -EINVAL;
+
+ props.items = alloca(props.n_items * sizeof(struct spa_dict_item));
+
+ n_permissions = 0;
+ permissions = alloca(props.n_items * sizeof(struct pw_permission));
+
+ for (i = 0; i < props.n_items; i++) {
+ uint32_t id, perms;
+ const char *str;
+
+ if (spa_pod_parser_get(&prs,
+ "s", &props.items[i].key,
+ "s", &props.items[i].value,
+ NULL) < 0)
+ return -EINVAL;
+
+ str = props.items[i].value;
+ /* first set global permissions */
+ if (spa_streq(props.items[i].key, PW_CORE_PERMISSIONS_GLOBAL)) {
+ size_t len;
+
+ /* <global-id>:[r][w][x] */
+ len = strcspn(str, ":");
+ if (len == 0)
+ continue;
+ id = atoi(str);
+ perms = parse_perms(str + len);
+ permissions[n_permissions++] = PW_PERMISSION_INIT(id, perms);
+ } else if (spa_streq(props.items[i].key, PW_CORE_PERMISSIONS_DEFAULT)) {
+ perms = parse_perms(str);
+ defperm = PW_PERMISSION_INIT(PW_ID_ANY, perms);
+ }
+ }
+ /* add default permission if set */
+ if (defperm.id == PW_ID_ANY)
+ permissions[n_permissions++] = defperm;
+
+ for (i = 0; i < n_permissions; i++) {
+ pw_log_debug("%d: %d: %08x", i, permissions[i].id, permissions[i].permissions);
+ }
+
+ return pw_impl_client_update_permissions(resource->client,
+ n_permissions, permissions);
+}
+
+static int core_demarshal_hello(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ void *ptr;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ "P", &ptr) < 0)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_core_methods, hello, 0, 2);
+}
+
+static int core_demarshal_sync(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ uint32_t seq;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs, "i", &seq) < 0)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_core_methods, sync, 0, 0, seq);
+}
+
+static int core_demarshal_get_registry(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ int32_t version, new_id;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ "i", &version,
+ "i", &new_id) < 0)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_core_methods, get_registry, 0, version, new_id);
+}
+
+SPA_EXPORT
+uint32_t pw_protocol_native0_type_from_v2(struct pw_impl_client *client, uint32_t type)
+{
+ void *t;
+ uint32_t index;
+ struct protocol_compat_v2 *compat_v2 = client->compat_v2;
+
+ if ((t = pw_map_lookup(&compat_v2->types, type)) == NULL)
+ return SPA_ID_INVALID;
+
+ index = PW_MAP_PTR_TO_ID(t);
+ if (index >= SPA_N_ELEMENTS(type_map))
+ return SPA_ID_INVALID;
+
+ return type_map[index].id;
+}
+
+SPA_EXPORT
+const char * pw_protocol_native0_name_from_v2(struct pw_impl_client *client, uint32_t type)
+{
+ void *t;
+ uint32_t index;
+ struct protocol_compat_v2 *compat_v2 = client->compat_v2;
+
+ if ((t = pw_map_lookup(&compat_v2->types, type)) == NULL)
+ return NULL;
+
+ index = PW_MAP_PTR_TO_ID(t);
+ if (index >= SPA_N_ELEMENTS(type_map))
+ return NULL;
+
+ return type_map[index].name;
+}
+
+SPA_EXPORT
+uint32_t pw_protocol_native0_name_to_v2(struct pw_impl_client *client, const char *name)
+{
+ uint32_t i;
+ /* match name to type table and return index */
+ for (i = 0; i < SPA_N_ELEMENTS(type_map); i++) {
+ if (type_map[i].name != NULL && spa_streq(type_map[i].name, name))
+ return i;
+ }
+ return SPA_ID_INVALID;
+}
+
+SPA_EXPORT
+uint32_t pw_protocol_native0_type_to_v2(struct pw_impl_client *client,
+ const struct spa_type_info *info, uint32_t type)
+{
+ const char *name;
+
+ /** find full name of type in type_info */
+ if ((name = spa_debug_type_find_name(info, type)) == NULL)
+ return SPA_ID_INVALID;
+
+ return pw_protocol_native0_name_to_v2(client, name);
+}
+
+struct spa_pod_prop_body0 {
+ uint32_t key;
+#define SPA_POD_PROP0_RANGE_NONE 0 /**< no range */
+#define SPA_POD_PROP0_RANGE_MIN_MAX 1 /**< property has range */
+#define SPA_POD_PROP0_RANGE_STEP 2 /**< property has range with step */
+#define SPA_POD_PROP0_RANGE_ENUM 3 /**< property has enumeration */
+#define SPA_POD_PROP0_RANGE_FLAGS 4 /**< property has flags */
+#define SPA_POD_PROP0_RANGE_MASK 0xf /**< mask to select range type */
+#define SPA_POD_PROP0_FLAG_UNSET (1 << 4) /**< property value is unset */
+#define SPA_POD_PROP0_FLAG_OPTIONAL (1 << 5) /**< property value is optional */
+#define SPA_POD_PROP0_FLAG_READONLY (1 << 6) /**< property is readonly */
+#define SPA_POD_PROP0_FLAG_DEPRECATED (1 << 7) /**< property is deprecated */
+#define SPA_POD_PROP0_FLAG_INFO (1 << 8) /**< property is informational and is not
+ * used when filtering */
+ uint32_t flags;
+ struct spa_pod value;
+ /* array with elements of value.size follows,
+ * first element is value/default, rest are alternatives */
+};
+
+/* v2 iterates object as containing spa_pod */
+#define SPA_POD_OBJECT_BODY_FOREACH0(body, size, iter) \
+ for ((iter) = SPA_PTROFF((body), sizeof(struct spa_pod_object_body), struct spa_pod); \
+ spa_pod_is_inside(body, size, iter); \
+ (iter) = spa_pod_next(iter))
+
+#define SPA_POD_PROP_ALTERNATIVE_FOREACH0(body, _size, iter) \
+ for ((iter) = SPA_PTROFF((body), (body)->value.size + \
+ sizeof(struct spa_pod_prop_body0), __typeof__(*iter)); \
+ (iter) <= SPA_PTROFF((body), (_size)-(body)->value.size, __typeof__(*iter)); \
+ (iter) = SPA_PTROFF((iter), (body)->value.size, __typeof__(*iter)))
+
+#define SPA0_POD_PROP_N_VALUES(b,size) (((size) - sizeof(struct spa_pod_prop_body0)) / (b)->value.size)
+
+static int remap_from_v2(uint32_t type, void *body, uint32_t size, struct pw_impl_client *client,
+ struct spa_pod_builder *builder)
+{
+ int res;
+
+ switch (type) {
+ case SPA_TYPE_Id:
+ spa_pod_builder_id(builder, pw_protocol_native0_type_from_v2(client, *(int32_t*) body));
+ break;
+
+ /** choice was props in v2 */
+ case SPA_TYPE_Choice:
+ {
+ struct spa_pod_prop_body0 *b = body;
+ struct spa_pod_frame f;
+ void *alt;
+ uint32_t key = pw_protocol_native0_type_from_v2(client, b->key);
+ enum spa_choice_type type;
+
+ spa_pod_builder_prop(builder, key, 0);
+
+ switch (b->flags & SPA_POD_PROP0_RANGE_MASK) {
+ default:
+ case SPA_POD_PROP0_RANGE_NONE:
+ type = SPA_CHOICE_None;
+ break;
+ case SPA_POD_PROP0_RANGE_MIN_MAX:
+ type = SPA_CHOICE_Range;
+ break;
+ case SPA_POD_PROP0_RANGE_STEP:
+ type = SPA_CHOICE_Step;
+ break;
+ case SPA_POD_PROP0_RANGE_ENUM:
+ type = SPA_CHOICE_Enum;
+ break;
+ case SPA_POD_PROP0_RANGE_FLAGS:
+ type = SPA_CHOICE_Flags;
+ break;
+ }
+ if (!SPA_FLAG_IS_SET(b->flags, SPA_POD_PROP0_FLAG_UNSET) &&
+ SPA0_POD_PROP_N_VALUES(b, size) == 1)
+ type = SPA_CHOICE_None;
+
+ spa_pod_builder_push_choice(builder, &f, type, 0);
+
+ if (b->value.type == SPA_TYPE_Id) {
+ uint32_t id;
+ if ((res = spa_pod_get_id(&b->value, &id)) < 0)
+ return res;
+ spa_pod_builder_id(builder, pw_protocol_native0_type_from_v2(client, id));
+ SPA_POD_PROP_ALTERNATIVE_FOREACH0(b, size, alt)
+ if ((res = remap_from_v2(b->value.type, alt, b->value.size, client, builder)) < 0)
+ return res;
+ } else {
+ spa_pod_builder_raw(builder, &b->value, size - sizeof(struct spa_pod));
+ }
+
+ spa_pod_builder_pop(builder, &f);
+
+ break;
+ }
+ case SPA_TYPE_Object:
+ {
+ struct spa_pod_object_body *b = body;
+ struct spa_pod *p;
+ struct spa_pod_frame f;
+ uint32_t type, count = 0;
+
+ /* type and id are switched */
+ type = pw_protocol_native0_type_from_v2(client, b->id),
+ spa_pod_builder_push_object(builder, &f, type,
+ pw_protocol_native0_type_from_v2(client, b->type));
+
+ /* object contained pods in v2 */
+ SPA_POD_OBJECT_BODY_FOREACH0(b, size, p) {
+ if (type == SPA_TYPE_OBJECT_Format && count < 2) {
+ uint32_t id;
+ if (spa_pod_get_id(p, &id) < 0)
+ continue;
+ id = pw_protocol_native0_type_from_v2(client, id);
+
+ if (count == 0) {
+ spa_pod_builder_prop(builder, SPA_FORMAT_mediaType, 0);
+ spa_pod_builder_id(builder, id);
+ }
+ if (count == 1) {
+ spa_pod_builder_prop(builder, SPA_FORMAT_mediaSubtype, 0);
+ spa_pod_builder_id(builder, id);
+ }
+ count++;
+ continue;
+ }
+ if ((res = remap_from_v2(p->type,
+ SPA_POD_BODY(p),
+ p->size,
+ client, builder)) < 0)
+ return res;
+ }
+ spa_pod_builder_pop(builder, &f);
+ break;
+ }
+ case SPA_TYPE_Struct:
+ {
+ struct spa_pod *b = body, *p;
+ struct spa_pod_frame f;
+
+ spa_pod_builder_push_struct(builder, &f);
+ SPA_POD_FOREACH(b, size, p)
+ if ((res = remap_from_v2(p->type, SPA_POD_BODY(p), p->size, client, builder)) < 0)
+ return res;
+ spa_pod_builder_pop(builder, &f);
+ break;
+ }
+ default:
+ break;
+ }
+ return 0;
+}
+
+static int remap_to_v2(struct pw_impl_client *client, const struct spa_type_info *info,
+ uint32_t type, void *body, uint32_t size,
+ struct spa_pod_builder *builder)
+{
+ int res;
+
+ switch (type) {
+ case SPA_TYPE_Id:
+ spa_pod_builder_id(builder, pw_protocol_native0_type_to_v2(client, info, *(int32_t*) body));
+ break;
+
+ case SPA_TYPE_Object:
+ {
+ struct spa_pod_object_body *b = body;
+ struct spa_pod_prop *p;
+ struct spa_pod_frame f[2];
+ uint32_t type;
+ const struct spa_type_info *ti, *ii;
+
+ ti = spa_debug_type_find(info, b->type);
+ ii = ti ? spa_debug_type_find(ti->values, 0) : NULL;
+
+ if (b->type == SPA_TYPE_COMMAND_Node ||
+ b->type == SPA_TYPE_EVENT_Node) {
+ spa_pod_builder_push_object(builder, &f[0], 0,
+ pw_protocol_native0_type_to_v2(client, ii ? ii->values : NULL, b->id));
+ } else {
+ ii = ii ? spa_debug_type_find(ii->values, b->id) : NULL;
+ /* type and id are switched */
+ type = pw_protocol_native0_type_to_v2(client, info, b->type),
+ spa_pod_builder_push_object(builder, &f[0],
+ pw_protocol_native0_type_to_v2(client, ii ? ii->values : NULL, b->id), type);
+ }
+
+
+ info = ti ? ti->values : info;
+
+ SPA_POD_OBJECT_BODY_FOREACH(b, size, p) {
+ uint32_t key, flags;
+ uint32_t n_vals, choice;
+ struct spa_pod *values;
+
+ ii = spa_debug_type_find(info, p->key);
+
+ values = spa_pod_get_values(&p->value, &n_vals, &choice);
+
+ if (b->type == SPA_TYPE_OBJECT_Format &&
+ (p->key == SPA_FORMAT_mediaType ||
+ p->key == SPA_FORMAT_mediaSubtype)) {
+ uint32_t val;
+
+ if (spa_pod_get_id(values, &val) < 0)
+ continue;
+ spa_pod_builder_id(builder,
+ pw_protocol_native0_type_to_v2(client, ii ? ii->values : NULL, val));
+ continue;
+ }
+
+ flags = 0;
+ switch(choice) {
+ case SPA_CHOICE_None:
+ flags |= SPA_POD_PROP0_RANGE_NONE;
+ break;
+ case SPA_CHOICE_Range:
+ flags |= SPA_POD_PROP0_RANGE_MIN_MAX | SPA_POD_PROP0_FLAG_UNSET;
+ break;
+ case SPA_CHOICE_Step:
+ flags |= SPA_POD_PROP0_RANGE_STEP | SPA_POD_PROP0_FLAG_UNSET;
+ break;
+ case SPA_CHOICE_Enum:
+ flags |= SPA_POD_PROP0_RANGE_ENUM | SPA_POD_PROP0_FLAG_UNSET;
+ break;
+ case SPA_CHOICE_Flags:
+ flags |= SPA_POD_PROP0_RANGE_FLAGS | SPA_POD_PROP0_FLAG_UNSET;
+ break;
+ }
+
+ key = pw_protocol_native0_type_to_v2(client, info, p->key);
+
+ spa_pod_builder_push_choice(builder, &f[1], key, flags);
+
+ if (values->type == SPA_TYPE_Id) {
+ uint32_t i, *id = SPA_POD_BODY(values);
+
+ for (i = 0; i < n_vals; i++) {
+ spa_pod_builder_id(builder,
+ pw_protocol_native0_type_to_v2(client, ii ? ii->values : NULL, id[i]));
+ }
+
+ } else {
+ spa_pod_builder_raw(builder, values, sizeof(struct spa_pod) + n_vals * values->size);
+ }
+ spa_pod_builder_pop(builder, &f[1]);
+ }
+ spa_pod_builder_pop(builder, &f[0]);
+ break;
+ }
+ case SPA_TYPE_Struct:
+ {
+ struct spa_pod *b = body, *p;
+ struct spa_pod_frame f;
+
+ spa_pod_builder_push_struct(builder, &f);
+ SPA_POD_FOREACH(b, size, p)
+ if ((res = remap_to_v2(client, info, p->type, SPA_POD_BODY(p), p->size, builder)) < 0)
+ return res;
+ spa_pod_builder_pop(builder, &f);
+ break;
+ }
+ default:
+ break;
+ }
+ return 0;
+}
+
+
+
+
+SPA_EXPORT
+struct spa_pod * pw_protocol_native0_pod_from_v2(struct pw_impl_client *client, const struct spa_pod *pod)
+{
+ uint8_t buffer[4096];
+ struct spa_pod *copy;
+ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, 4096);
+ int res;
+
+ if (pod == NULL)
+ return NULL;
+
+ if ((res = remap_from_v2(SPA_POD_TYPE(pod),
+ SPA_POD_BODY(pod),
+ SPA_POD_BODY_SIZE(pod),
+ client, &b)) < 0) {
+ errno = -res;
+ return NULL;
+ }
+ copy = spa_pod_copy(b.data);
+ return copy;
+}
+
+SPA_EXPORT
+int pw_protocol_native0_pod_to_v2(struct pw_impl_client *client, const struct spa_pod *pod,
+ struct spa_pod_builder *b)
+{
+ int res;
+
+ if (pod == NULL) {
+ spa_pod_builder_none(b);
+ return 0;
+ }
+
+ if ((res = remap_to_v2(client, pw_type_info(),
+ SPA_POD_TYPE(pod),
+ SPA_POD_BODY(pod),
+ SPA_POD_BODY_SIZE(pod),
+ b)) < 0) {
+ return -res;
+ }
+ return 0;
+}
+
+static int core_demarshal_create_object(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct pw_impl_client *client = pw_resource_get_client(resource);
+ struct spa_pod_parser prs;
+ struct spa_pod_frame f;
+ uint32_t version, type, new_id, i;
+ const char *factory_name, *type_name;
+ struct spa_dict props;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_push_struct(&prs, &f) < 0 ||
+ spa_pod_parser_get(&prs,
+ "s", &factory_name,
+ "I", &type,
+ "i", &version,
+ "i", &props.n_items, NULL) < 0)
+ return -EINVAL;
+
+ props.items = alloca(props.n_items * sizeof(struct spa_dict_item));
+ for (i = 0; i < props.n_items; i++) {
+ if (spa_pod_parser_get(&prs,
+ "s", &props.items[i].key,
+ "s", &props.items[i].value, NULL) < 0)
+ return -EINVAL;
+ }
+ if (spa_pod_parser_get(&prs, "i", &new_id, NULL) < 0)
+ return -EINVAL;
+
+ type_name = pw_protocol_native0_name_from_v2(client, type);
+ if (type_name == NULL)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_core_methods, create_object, 0, factory_name,
+ type_name, version,
+ &props, new_id);
+}
+
+static int core_demarshal_destroy(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object, *r;
+ struct pw_impl_client *client = pw_resource_get_client(resource);
+ struct spa_pod_parser prs;
+ uint32_t id;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ "i", &id, NULL) < 0)
+ return -EINVAL;
+
+ pw_log_debug("client %p: destroy resource %u", client, id);
+
+ if ((r = pw_impl_client_find_resource(client, id)) == NULL)
+ goto no_resource;
+
+ return pw_resource_notify(resource, struct pw_core_methods, destroy, 0, r);
+
+no_resource:
+ pw_log_error("client %p: unknown resource %u op:%u", client, id, msg->opcode);
+ pw_resource_errorf(resource, -ENOENT, "unknown resource %d op:%u", id, msg->opcode);
+ return 0;
+}
+
+static int core_demarshal_update_types_server(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct pw_impl_client *client = pw_resource_get_client(resource);
+ struct protocol_compat_v2 *compat_v2 = client->compat_v2;
+ struct spa_pod_parser prs;
+ uint32_t first_id, n_types;
+ struct spa_pod_frame f;
+ const char **types;
+ uint32_t i;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_push_struct(&prs, &f) < 0 ||
+ spa_pod_parser_get(&prs,
+ "i", &first_id,
+ "i", &n_types,
+ NULL) < 0)
+ return -EINVAL;
+
+ if (first_id == 0)
+ compat_v2->send_types = true;
+
+ types = alloca(n_types * sizeof(char *));
+ for (i = 0; i < n_types; i++) {
+ if (spa_pod_parser_get(&prs, "s", &types[i], NULL) < 0)
+ return -EINVAL;
+ }
+
+ for (i = 0; i < n_types; i++, first_id++) {
+ uint32_t type_id = pw_protocol_native0_find_type(client, types[i]);
+ if (type_id == SPA_ID_INVALID)
+ continue;
+ if (pw_map_insert_at(&compat_v2->types, first_id, PW_MAP_ID_TO_PTR(type_id)) < 0)
+ pw_log_error("can't add type %d->%d for client", first_id, type_id);
+ }
+ return 0;
+}
+
+static void registry_marshal_global(void *data, uint32_t id, uint32_t permissions,
+ const char *type, uint32_t version, const struct spa_dict *props)
+{
+ struct pw_resource *resource = data;
+ struct pw_impl_client *client = pw_resource_get_client(resource);
+ struct spa_pod_builder *b;
+ struct spa_pod_frame f;
+ uint32_t i, n_items, parent_id;
+ uint32_t type_id;
+ const char *str;
+
+ type_id = pw_protocol_native0_find_type(client, type);
+ if (type_id == SPA_ID_INVALID)
+ return;
+
+ b = pw_protocol_native_begin_resource(resource, PW_REGISTRY_V0_EVENT_GLOBAL, NULL);
+
+ n_items = props ? props->n_items : 0;
+
+ parent_id = 0;
+ if (props) {
+ if (spa_streq(type, PW_TYPE_INTERFACE_Port)) {
+ if ((str = spa_dict_lookup(props, "node.id")) != NULL)
+ parent_id = atoi(str);
+ } else if (spa_streq(type, PW_TYPE_INTERFACE_Node)) {
+ if ((str = spa_dict_lookup(props, "device.id")) != NULL)
+ parent_id = atoi(str);
+ } else if (spa_streq(type, PW_TYPE_INTERFACE_Client) ||
+ spa_streq(type, PW_TYPE_INTERFACE_Device) ||
+ spa_streq(type, PW_TYPE_INTERFACE_Factory)) {
+ if ((str = spa_dict_lookup(props, "module.id")) != NULL)
+ parent_id = atoi(str);
+ }
+ }
+
+ version = 0;
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_add(b,
+ "i", id,
+ "i", parent_id,
+ "i", permissions,
+ "I", type_id,
+ "i", version,
+ "i", n_items, NULL);
+
+ for (i = 0; i < n_items; i++) {
+ spa_pod_builder_add(b,
+ "s", props->items[i].key,
+ "s", props->items[i].value, NULL);
+ }
+ spa_pod_builder_pop(b, &f);
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static void registry_marshal_global_remove(void *data, uint32_t id)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource, PW_REGISTRY_V0_EVENT_GLOBAL_REMOVE, NULL);
+
+ spa_pod_builder_add_struct(b, "i", id);
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static int registry_demarshal_bind(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct pw_impl_client *client = pw_resource_get_client(resource);
+ struct spa_pod_parser prs;
+ uint32_t id, version, type, new_id;
+ const char *type_name;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ "i", &id,
+ "I", &type,
+ "i", &version,
+ "i", &new_id) < 0)
+ return -EINVAL;
+
+ type_name = pw_protocol_native0_name_from_v2(client, type);
+ if (type_name == NULL)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_registry_methods, bind, 0, id, type_name, version, new_id);
+}
+
+static void module_marshal_info(void *data, const struct pw_module_info *info)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+ struct spa_pod_frame f;
+ uint32_t i, n_items;
+
+ b = pw_protocol_native_begin_resource(resource, PW_MODULE_V0_EVENT_INFO, NULL);
+
+ n_items = info->props ? info->props->n_items : 0;
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_add(b,
+ "i", info->id,
+ "l", info->change_mask,
+ "s", info->name,
+ "s", info->filename,
+ "s", info->args,
+ "i", n_items, NULL);
+
+ for (i = 0; i < n_items; i++) {
+ spa_pod_builder_add(b,
+ "s", info->props->items[i].key,
+ "s", info->props->items[i].value, NULL);
+ }
+ spa_pod_builder_pop(b, &f);
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static void factory_marshal_info(void *data, const struct pw_factory_info *info)
+{
+ struct pw_resource *resource = data;
+ struct pw_impl_client *client = pw_resource_get_client(resource);
+ struct spa_pod_builder *b;
+ struct spa_pod_frame f;
+ uint32_t i, n_items, type, version;
+
+ type = pw_protocol_native0_find_type(client, info->type);
+ if (type == SPA_ID_INVALID)
+ return;
+
+ b = pw_protocol_native_begin_resource(resource, PW_FACTORY_V0_EVENT_INFO, NULL);
+
+ n_items = info->props ? info->props->n_items : 0;
+
+ version = 0;
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_add(b,
+ "i", info->id,
+ "l", info->change_mask,
+ "s", info->name,
+ "I", type,
+ "i", version,
+ "i", n_items, NULL);
+
+ for (i = 0; i < n_items; i++) {
+ spa_pod_builder_add(b,
+ "s", info->props->items[i].key,
+ "s", info->props->items[i].value, NULL);
+ }
+ spa_pod_builder_pop(b, &f);
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static void node_marshal_info(void *data, const struct pw_node_info *info)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+ struct spa_pod_frame f;
+ uint32_t i, n_items;
+
+ b = pw_protocol_native_begin_resource(resource, PW_NODE_V0_EVENT_INFO, NULL);
+
+ n_items = info->props ? info->props->n_items : 0;
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_add(b,
+ "i", info->id,
+ "l", info->change_mask,
+ "s", "node.name",
+ "i", info->max_input_ports,
+ "i", info->n_input_ports,
+ "i", info->max_output_ports,
+ "i", info->n_output_ports,
+ "i", info->state,
+ "s", info->error,
+ "i", n_items, NULL);
+
+ for (i = 0; i < n_items; i++) {
+ spa_pod_builder_add(b,
+ "s", info->props->items[i].key,
+ "s", info->props->items[i].value, NULL);
+ }
+ spa_pod_builder_pop(b, &f);
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static void node_marshal_param(void *data, int seq, uint32_t id, uint32_t index, uint32_t next,
+ const struct spa_pod *param)
+{
+ struct pw_resource *resource = data;
+ struct pw_impl_client *client = pw_resource_get_client(resource);
+ struct spa_pod_builder *b;
+ struct spa_pod_frame f;
+
+ b = pw_protocol_native_begin_resource(resource, PW_NODE_V0_EVENT_PARAM, NULL);
+
+ id = pw_protocol_native0_type_to_v2(client, spa_type_param, id),
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_add(b,
+ "I", id,
+ "i", index,
+ "i", next,
+ NULL);
+ pw_protocol_native0_pod_to_v2(client, (struct spa_pod *)param, b);
+ spa_pod_builder_pop(b, &f);
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static int node_demarshal_enum_params(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct pw_impl_client *client = pw_resource_get_client(resource);
+ struct spa_pod_parser prs;
+ uint32_t id, index, num;
+ struct spa_pod *filter;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ "I", &id,
+ "i", &index,
+ "i", &num,
+ "P", &filter) < 0)
+ return -EINVAL;
+
+ id = pw_protocol_native0_type_from_v2(client, id);
+ filter = NULL;
+
+ return pw_resource_notify(resource, struct pw_node_methods, enum_params, 0,
+ 0, id, index, num, filter);
+}
+
+static void port_marshal_info(void *data, const struct pw_port_info *info)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+ struct spa_pod_frame f;
+ uint32_t i, n_items;
+ uint64_t change_mask = 0;
+ const char *port_name;
+
+ b = pw_protocol_native_begin_resource(resource, PW_PORT_V0_EVENT_INFO, NULL);
+
+ n_items = info->props ? info->props->n_items : 0;
+
+#define PW_PORT_V0_CHANGE_MASK_NAME (1 << 0)
+#define PW_PORT_V0_CHANGE_MASK_PROPS (1 << 1)
+#define PW_PORT_V0_CHANGE_MASK_ENUM_PARAMS (1 << 2)
+
+ change_mask |= PW_PORT_V0_CHANGE_MASK_NAME;
+ if (info->change_mask & PW_PORT_CHANGE_MASK_PROPS)
+ change_mask |= PW_PORT_V0_CHANGE_MASK_PROPS;
+ if (info->change_mask & PW_PORT_CHANGE_MASK_PARAMS)
+ change_mask |= PW_PORT_V0_CHANGE_MASK_ENUM_PARAMS;
+
+ port_name = NULL;
+ if (info->props != NULL)
+ port_name = spa_dict_lookup(info->props, "port.name");
+ if (port_name == NULL)
+ port_name = "port.name";
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_add(b,
+ "i", info->id,
+ "l", change_mask,
+ "s", port_name,
+ "i", n_items, NULL);
+
+ for (i = 0; i < n_items; i++) {
+ spa_pod_builder_add(b,
+ "s", info->props->items[i].key,
+ "s", info->props->items[i].value, NULL);
+ }
+ spa_pod_builder_pop(b, &f);
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static void port_marshal_param(void *data, int seq, uint32_t id, uint32_t index, uint32_t next,
+ const struct spa_pod *param)
+{
+ struct pw_resource *resource = data;
+ struct pw_impl_client *client = pw_resource_get_client(resource);
+ struct spa_pod_builder *b;
+ struct spa_pod_frame f;
+
+ b = pw_protocol_native_begin_resource(resource, PW_PORT_V0_EVENT_PARAM, NULL);
+
+ id = pw_protocol_native0_type_to_v2(client, spa_type_param, id),
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_add(b,
+ "I", id,
+ "i", index,
+ "i", next,
+ NULL);
+ pw_protocol_native0_pod_to_v2(client, (struct spa_pod *)param, b);
+ spa_pod_builder_pop(b, &f);
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static int port_demarshal_enum_params(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct pw_impl_client *client = pw_resource_get_client(resource);
+ struct spa_pod_parser prs;
+ uint32_t id, index, num;
+ struct spa_pod *filter;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ "I", &id,
+ "i", &index,
+ "i", &num,
+ "P", &filter) < 0)
+ return -EINVAL;
+
+ id = pw_protocol_native0_type_from_v2(client, id);
+ filter = NULL;
+
+ return pw_resource_notify(resource, struct pw_port_methods, enum_params, 0,
+ 0, id, index, num, filter);
+}
+
+static void client_marshal_info(void *data, const struct pw_client_info *info)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+ struct spa_pod_frame f;
+ uint32_t i, n_items;
+
+ b = pw_protocol_native_begin_resource(resource, PW_CLIENT_V0_EVENT_INFO, NULL);
+
+ n_items = info->props ? info->props->n_items : 0;
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_add(b,
+ "i", info->id,
+ "l", info->change_mask,
+ "i", n_items, NULL);
+
+ for (i = 0; i < n_items; i++) {
+ spa_pod_builder_add(b,
+ "s", info->props->items[i].key,
+ "s", info->props->items[i].value, NULL);
+ }
+ spa_pod_builder_pop(b, &f);
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static void client_marshal_permissions(void *data, uint32_t index, uint32_t n_permissions,
+ const struct pw_permission *permissions)
+{
+}
+
+
+static void link_marshal_info(void *data, const struct pw_link_info *info)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+ struct spa_pod_frame f;
+ uint32_t i, n_items;
+
+ b = pw_protocol_native_begin_resource(resource, PW_LINK_V0_EVENT_INFO, NULL);
+
+ n_items = info->props ? info->props->n_items : 0;
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_add(b,
+ "i", info->id,
+ "l", info->change_mask,
+ "i", info->output_node_id,
+ "i", info->output_port_id,
+ "i", info->input_node_id,
+ "i", info->input_port_id,
+ "P", info->format,
+ "i", n_items, NULL);
+
+ for (i = 0; i < n_items; i++) {
+ spa_pod_builder_add(b,
+ "s", info->props->items[i].key,
+ "s", info->props->items[i].value, NULL);
+ }
+ spa_pod_builder_pop(b, &f);
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static const struct pw_protocol_native_demarshal pw_protocol_native_core_method_demarshal[PW_CORE_V0_METHOD_NUM] = {
+ [PW_CORE_V0_METHOD_HELLO] = { &core_demarshal_hello, 0, },
+ [PW_CORE_V0_METHOD_UPDATE_TYPES] = { &core_demarshal_update_types_server, 0, },
+ [PW_CORE_V0_METHOD_SYNC] = { &core_demarshal_sync, 0, },
+ [PW_CORE_V0_METHOD_GET_REGISTRY] = { &core_demarshal_get_registry, 0, },
+ [PW_CORE_V0_METHOD_CLIENT_UPDATE] = { &core_demarshal_client_update, 0, },
+ [PW_CORE_V0_METHOD_PERMISSIONS] = { &core_demarshal_permissions, 0, },
+ [PW_CORE_V0_METHOD_CREATE_OBJECT] = { &core_demarshal_create_object, 0, PW_PROTOCOL_NATIVE_FLAG_REMAP, },
+ [PW_CORE_V0_METHOD_DESTROY] = { &core_demarshal_destroy, 0, }
+};
+
+static const struct pw_core_events pw_protocol_native_core_event_marshal = {
+ PW_VERSION_CORE_EVENTS,
+ .info = &core_marshal_info,
+ .done = &core_marshal_done,
+ .error = &core_marshal_error,
+ .remove_id = &core_marshal_remove_id,
+};
+
+static const struct pw_protocol_marshal pw_protocol_native_core_marshal = {
+ PW_TYPE_INTERFACE_Core,
+ PW_VERSION_CORE_V0,
+ PW_CORE_V0_METHOD_NUM,
+ PW_CORE_EVENT_NUM,
+ 0,
+ NULL,
+ pw_protocol_native_core_method_demarshal,
+ &pw_protocol_native_core_event_marshal,
+ NULL
+};
+
+static const struct pw_protocol_native_demarshal pw_protocol_native_registry_method_demarshal[] = {
+ [PW_REGISTRY_V0_METHOD_BIND] = { &registry_demarshal_bind, 0, PW_PROTOCOL_NATIVE_FLAG_REMAP, },
+};
+
+static const struct pw_registry_events pw_protocol_native_registry_event_marshal = {
+ PW_VERSION_REGISTRY_EVENTS,
+ .global = &registry_marshal_global,
+ .global_remove = &registry_marshal_global_remove,
+};
+
+static const struct pw_protocol_marshal pw_protocol_native_registry_marshal = {
+ PW_TYPE_INTERFACE_Registry,
+ PW_VERSION_REGISTRY_V0,
+ PW_REGISTRY_V0_METHOD_NUM,
+ PW_REGISTRY_EVENT_NUM,
+ 0,
+ NULL,
+ pw_protocol_native_registry_method_demarshal,
+ &pw_protocol_native_registry_event_marshal,
+ NULL
+};
+
+static const struct pw_module_events pw_protocol_native_module_event_marshal = {
+ PW_VERSION_MODULE_EVENTS,
+ .info = &module_marshal_info,
+};
+
+static const struct pw_protocol_marshal pw_protocol_native_module_marshal = {
+ PW_TYPE_INTERFACE_Module,
+ PW_VERSION_MODULE_V0,
+ 0,
+ PW_MODULE_EVENT_NUM,
+ 0,
+ NULL, NULL,
+ &pw_protocol_native_module_event_marshal,
+ NULL
+};
+
+static const struct pw_factory_events pw_protocol_native_factory_event_marshal = {
+ PW_VERSION_FACTORY_EVENTS,
+ .info = &factory_marshal_info,
+};
+
+static const struct pw_protocol_marshal pw_protocol_native_factory_marshal = {
+ PW_TYPE_INTERFACE_Factory,
+ PW_VERSION_FACTORY_V0,
+ 0,
+ PW_FACTORY_EVENT_NUM,
+ 0,
+ NULL, NULL,
+ &pw_protocol_native_factory_event_marshal,
+ NULL,
+};
+
+static const struct pw_protocol_native_demarshal pw_protocol_native_node_method_demarshal[] = {
+ [PW_NODE_V0_METHOD_ENUM_PARAMS] = { &node_demarshal_enum_params, 0, PW_PROTOCOL_NATIVE_FLAG_REMAP, },
+};
+
+static const struct pw_node_events pw_protocol_native_node_event_marshal = {
+ PW_VERSION_NODE_EVENTS,
+ .info = &node_marshal_info,
+ .param = &node_marshal_param,
+};
+
+static const struct pw_protocol_marshal pw_protocol_native_node_marshal = {
+ PW_TYPE_INTERFACE_Node,
+ PW_VERSION_NODE_V0,
+ PW_NODE_V0_METHOD_NUM,
+ PW_NODE_EVENT_NUM,
+ 0,
+ NULL,
+ pw_protocol_native_node_method_demarshal,
+ &pw_protocol_native_node_event_marshal,
+ NULL
+};
+
+
+static const struct pw_protocol_native_demarshal pw_protocol_native_port_method_demarshal[] = {
+ [PW_PORT_V0_METHOD_ENUM_PARAMS] = { &port_demarshal_enum_params, 0, PW_PROTOCOL_NATIVE_FLAG_REMAP, },
+};
+
+static const struct pw_port_events pw_protocol_native_port_event_marshal = {
+ PW_VERSION_PORT_EVENTS,
+ .info = &port_marshal_info,
+ .param = &port_marshal_param,
+};
+
+static const struct pw_protocol_marshal pw_protocol_native_port_marshal = {
+ PW_TYPE_INTERFACE_Port,
+ PW_VERSION_PORT_V0,
+ PW_PORT_V0_METHOD_NUM,
+ PW_PORT_EVENT_NUM,
+ 0,
+ NULL,
+ pw_protocol_native_port_method_demarshal,
+ &pw_protocol_native_port_event_marshal,
+ NULL
+};
+
+static const struct pw_client_events pw_protocol_native_client_event_marshal = {
+ PW_VERSION_CLIENT_EVENTS,
+ .info = &client_marshal_info,
+ .permissions = &client_marshal_permissions,
+};
+
+static const struct pw_protocol_marshal pw_protocol_native_client_marshal = {
+ PW_TYPE_INTERFACE_Client,
+ PW_VERSION_CLIENT_V0,
+ 0,
+ PW_CLIENT_EVENT_NUM,
+ 0,
+ NULL, NULL,
+ &pw_protocol_native_client_event_marshal,
+ NULL,
+};
+
+static const struct pw_link_events pw_protocol_native_link_event_marshal = {
+ PW_VERSION_LINK_EVENTS,
+ .info = &link_marshal_info,
+};
+
+static const struct pw_protocol_marshal pw_protocol_native_link_marshal = {
+ PW_TYPE_INTERFACE_Link,
+ PW_VERSION_LINK_V0,
+ 0,
+ PW_LINK_EVENT_NUM,
+ 0,
+ NULL, NULL,
+ &pw_protocol_native_link_event_marshal,
+ NULL
+};
+
+void pw_protocol_native0_init(struct pw_protocol *protocol)
+{
+ pw_protocol_add_marshal(protocol, &pw_protocol_native_core_marshal);
+ pw_protocol_add_marshal(protocol, &pw_protocol_native_registry_marshal);
+ pw_protocol_add_marshal(protocol, &pw_protocol_native_module_marshal);
+ pw_protocol_add_marshal(protocol, &pw_protocol_native_node_marshal);
+ pw_protocol_add_marshal(protocol, &pw_protocol_native_port_marshal);
+ pw_protocol_add_marshal(protocol, &pw_protocol_native_factory_marshal);
+ pw_protocol_add_marshal(protocol, &pw_protocol_native_client_marshal);
+ pw_protocol_add_marshal(protocol, &pw_protocol_native_link_marshal);
+}
diff --git a/src/modules/module-protocol-native/v0/typemap.h b/src/modules/module-protocol-native/v0/typemap.h
new file mode 100644
index 0000000..1d80ea2
--- /dev/null
+++ b/src/modules/module-protocol-native/v0/typemap.h
@@ -0,0 +1,282 @@
+static const struct type_info {
+ const char *type;
+ const char *name;
+ uint32_t id;
+} type_map[] = {
+ { "Spa:Interface:TypeMap", SPA_TYPE_INFO_INTERFACE_BASE, 0, },
+ { "Spa:Interface:Log", SPA_TYPE_INTERFACE_Log, 0, },
+ { "Spa:Interface:Loop", SPA_TYPE_INTERFACE_Loop, 0, },
+ { "Spa:Interface:LoopControl", SPA_TYPE_INTERFACE_LoopControl, 0, },
+ { "Spa:Interface:LoopUtils", SPA_TYPE_INTERFACE_LoopUtils, 0, },
+ { "PipeWire:Interface:Core", PW_TYPE_INTERFACE_Core, 0, },
+ { "PipeWire:Interface:Registry", PW_TYPE_INTERFACE_Registry, 0, },
+ { "PipeWire:Interface:Node", PW_TYPE_INTERFACE_Node, 0, },
+ { "PipeWire:Interface:Port", PW_TYPE_INTERFACE_Port,0, },
+ { "PipeWire:Interface:Factory", PW_TYPE_INTERFACE_Factory, 0, },
+ { "PipeWire:Interface:Link", PW_TYPE_INTERFACE_Link, 0, },
+ { "PipeWire:Interface:Client", PW_TYPE_INTERFACE_Client, 0, },
+ { "PipeWire:Interface:Module", PW_TYPE_INTERFACE_Module, 0, },
+ { "PipeWire:Interface:Device", PW_TYPE_INTERFACE_Device, 0, },
+
+ { "PipeWire:Interface:Metadata", PW_TYPE_INTERFACE_Metadata, 0, },
+ { "PipeWire:Interface:Session", PW_TYPE_INTERFACE_Session, 0, },
+ { "PipeWire:Interface:Endpoint", PW_TYPE_INTERFACE_Endpoint, 0, },
+ { "PipeWire:Interface:EndpointStream", PW_TYPE_INTERFACE_EndpointStream, 0, },
+ { "PipeWire:Interface:EndpointLink", PW_TYPE_INTERFACE_EndpointLink, 0, },
+
+ { "PipeWire:Interface:ClientNode", PW_TYPE_INTERFACE_ClientNode, 0, },
+ { "PipeWire:Interface:ClientSession", PW_TYPE_INTERFACE_ClientSession, 0, },
+ { "PipeWire:Interface:ClientEndpoint", PW_TYPE_INTERFACE_ClientEndpoint, 0, },
+
+ { "Spa:Interface:Node", SPA_TYPE_INTERFACE_Node, 0, },
+ { "Spa:Interface:Clock", },
+ { "Spa:Interface:Monitor", },
+ { "Spa:Interface:Device", SPA_TYPE_INTERFACE_Device, 0, },
+ { "Spa:POD:Object:Param:Format", SPA_TYPE_INFO_Format, SPA_TYPE_OBJECT_Format, },
+ { "Spa:POD:Object:Param:Props", SPA_TYPE_INFO_Props, SPA_TYPE_OBJECT_Props, },
+ { "Spa:Pointer:IO:Buffers", },
+ { "Spa:Pointer:IO:Control:Range", },
+ { "Spa:Pointer:IO:Prop", },
+ { "Spa:Enum:ParamId:List", },
+ { "Spa:POD:Object:Param:List", },
+ { "Spa:POD:Object:Param:List:id", },
+ { "Spa:Enum:ParamId:PropInfo", SPA_TYPE_INFO_PARAM_ID_BASE "PropInfo", SPA_PARAM_PropInfo, },
+ { "Spa:POD:Object:Param:PropInfo", SPA_TYPE_INFO_PROP_INFO_BASE, SPA_PROP_INFO_START, },
+ { "Spa:POD:Object:Param:PropInfo:id", SPA_TYPE_INFO_PROP_INFO_BASE "id", SPA_PROP_INFO_id, },
+ { "Spa:POD:Object:Param:PropInfo:name", SPA_TYPE_INFO_PROP_INFO_BASE "name", SPA_PROP_INFO_name, },
+ { "Spa:POD:Object:Param:PropInfo:type", SPA_TYPE_INFO_PROP_INFO_BASE "typ", SPA_PROP_INFO_type, },
+ { "Spa:POD:Object:Param:PropInfo:labels", SPA_TYPE_INFO_PROP_INFO_BASE "labels", SPA_PROP_INFO_labels, },
+ { "Spa:Enum:ParamId:Props", SPA_TYPE_INFO_PARAM_ID_BASE "Props", SPA_PARAM_Props, },
+ { "Spa:Enum:ParamId:EnumFormat", SPA_TYPE_INFO_PARAM_ID_BASE "EnumFormat", SPA_PARAM_EnumFormat,},
+ { "Spa:Enum:ParamId:Format", SPA_TYPE_INFO_PARAM_ID_BASE "Format", SPA_PARAM_Format, },
+ { "Spa:Enum:ParamId:Buffers", SPA_TYPE_INFO_PARAM_ID_BASE "Buffers", SPA_PARAM_Buffers },
+ { "Spa:Enum:ParamId:Meta", SPA_TYPE_INFO_PARAM_ID_BASE "Meta", SPA_PARAM_Meta, },
+ { "Spa:Pointer:Meta:Header", SPA_TYPE_INFO_META_BASE "Header", SPA_META_Header, },
+ { "Spa:Pointer:Meta:VideoCrop", SPA_TYPE_INFO_META_REGION_BASE "VideoCrop", SPA_META_VideoCrop, },
+ { "Spa:Pointer:Meta:VideoDamage", SPA_TYPE_INFO_META_ARRAY_REGION_BASE "VideoDamage", SPA_META_VideoDamage, },
+ { "Spa:Pointer:Meta:Bitmap", SPA_TYPE_INFO_META_BASE "Bitmap", SPA_META_Bitmap, },
+ { "Spa:Pointer:Meta:Cursor", SPA_TYPE_INFO_META_BASE "Cursor", SPA_META_Cursor, },
+ { "Spa:Enum:DataType:MemPtr", SPA_TYPE_INFO_DATA_BASE "MemPtr", SPA_DATA_MemPtr, },
+ { "Spa:Enum:DataType:Fd:MemFd", SPA_TYPE_INFO_DATA_FD_BASE "MemFd", SPA_DATA_MemFd, },
+ { "Spa:Enum:DataType:Fd:DmaBuf", SPA_TYPE_INFO_DATA_FD_BASE "DmaBuf", SPA_DATA_DmaBuf, },
+ { "Spa:POD:Object:Event:Node:Error", SPA_TYPE_INFO_NODE_EVENT_BASE "Error", SPA_NODE_EVENT_Error, },
+ { "Spa:POD:Object:Event:Node:Buffering", SPA_TYPE_INFO_NODE_EVENT_BASE "Buffering", SPA_NODE_EVENT_Buffering, },
+ { "Spa:POD:Object:Event:Node:RequestRefresh", SPA_TYPE_INFO_NODE_EVENT_BASE "RequestRefresh", SPA_NODE_EVENT_RequestRefresh, },
+ { "Spa:POD:Object:Event:Node:RequestClockUpdate", SPA_TYPE_INFO_NODE_EVENT_BASE "RequestClockUpdate", SPA_NODE0_EVENT_RequestClockUpdate, },
+ { "Spa:POD:Object:Command:Node", SPA_TYPE_INFO_COMMAND_BASE "Node", SPA_TYPE_COMMAND_Node,},
+ { "Spa:POD:Object:Command:Node:Suspend", SPA_TYPE_INFO_NODE_COMMAND_BASE "Suspend", SPA_NODE_COMMAND_Suspend,},
+ { "Spa:POD:Object:Command:Node:Pause", SPA_TYPE_INFO_NODE_COMMAND_BASE "Pause", SPA_NODE_COMMAND_Pause, },
+ { "Spa:POD:Object:Command:Node:Start", SPA_TYPE_INFO_NODE_COMMAND_BASE "Start", SPA_NODE_COMMAND_Start, },
+ { "Spa:POD:Object:Command:Node:Enable", SPA_TYPE_INFO_NODE_COMMAND_BASE "Enable", SPA_NODE_COMMAND_Enable, },
+ { "Spa:POD:Object:Command:Node:Disable", SPA_TYPE_INFO_NODE_COMMAND_BASE "Disable", SPA_NODE_COMMAND_Disable, },
+ { "Spa:POD:Object:Command:Node:Flush", SPA_TYPE_INFO_NODE_COMMAND_BASE "Flush", SPA_NODE_COMMAND_Flush, },
+ { "Spa:POD:Object:Command:Node:Drain", SPA_TYPE_INFO_NODE_COMMAND_BASE "Drain", SPA_NODE_COMMAND_Drain, },
+ { "Spa:POD:Object:Command:Node:Marker", SPA_TYPE_INFO_NODE_COMMAND_BASE "Marker", SPA_NODE_COMMAND_Marker, },
+ { "Spa:POD:Object:Command:Node:ClockUpdate", SPA_TYPE_INFO_NODE_COMMAND_BASE "ClockUpdate", SPA_NODE0_COMMAND_ClockUpdate, },
+ { "Spa:POD:Object:Event:Monitor:Added", },
+ { "Spa:POD:Object:Event:Monitor:Removed", },
+ { "Spa:POD:Object:Event:Monitor:Changed", },
+ { "Spa:POD:Object:MonitorItem", },
+ { "Spa:POD:Object:MonitorItem:id", },
+ { "Spa:POD:Object:MonitorItem:flags", },
+ { "Spa:POD:Object:MonitorItem:state", },
+ { "Spa:POD:Object:MonitorItem:name", },
+ { "Spa:POD:Object:MonitorItem:class", },
+ { "Spa:POD:Object:MonitorItem:info", },
+ { "Spa:POD:Object:MonitorItem:factory", },
+ { "Spa:POD:Object:Param:Buffers", SPA_TYPE_INFO_PARAM_Buffers, SPA_TYPE_OBJECT_ParamBuffers, },
+ { "Spa:POD:Object:Param:Buffers:size", SPA_TYPE_INFO_PARAM_BLOCK_INFO_BASE "size", SPA_PARAM_BUFFERS_size, },
+ { "Spa:POD:Object:Param:Buffers:stride", SPA_TYPE_INFO_PARAM_BLOCK_INFO_BASE "stride", SPA_PARAM_BUFFERS_stride, },
+ { "Spa:POD:Object:Param:Buffers:buffers", SPA_TYPE_INFO_PARAM_BUFFERS_BASE "buffers", SPA_PARAM_BUFFERS_buffers, },
+ { "Spa:POD:Object:Param:Buffers:align",SPA_TYPE_INFO_PARAM_BLOCK_INFO_BASE "align", SPA_PARAM_BUFFERS_align, },
+ { "Spa:POD:Object:Param:Meta", SPA_TYPE_INFO_PARAM_Meta, SPA_TYPE_OBJECT_ParamMeta, },
+ { "Spa:POD:Object:Param:Meta:type", SPA_TYPE_INFO_PARAM_META_BASE "type", SPA_PARAM_META_type, },
+ { "Spa:POD:Object:Param:Meta:size", SPA_TYPE_INFO_PARAM_META_BASE "size", SPA_PARAM_META_size, },
+ { "Spa:POD:Object:Param:IO:id", },
+ { "Spa:POD:Object:Param:IO:size", },
+ { "Spa:Enum:ParamId:IO:Buffers", },
+ { "Spa:POD:Object:Param:IO:Buffers", },
+ { "Spa:Enum:ParamId:IO:Control", },
+ { "Spa:POD:Object:Param:IO:Control", },
+ { "Spa:Enum:ParamId:IO:Props:In", },
+ { "Spa:Enum:ParamId:IO:Props:Out", },
+ { "Spa:POD:Object:Param:IO:Prop", },
+ { "Spa:Interface:DBus", SPA_TYPE_INFO_INTERFACE_BASE "DBus", 0, },
+ { "Spa:Enum:MediaType:audio", SPA_TYPE_INFO_MEDIA_TYPE_BASE "audio", SPA_MEDIA_TYPE_audio, },
+ { "Spa:Enum:MediaType:video", SPA_TYPE_INFO_MEDIA_TYPE_BASE "video", SPA_MEDIA_TYPE_video, },
+ { "Spa:Enum:MediaType:image", SPA_TYPE_INFO_MEDIA_TYPE_BASE "image", SPA_MEDIA_TYPE_image, },
+ { "Spa:Enum:MediaType:binary", SPA_TYPE_INFO_MEDIA_TYPE_BASE "binary", SPA_MEDIA_TYPE_binary, },
+ { "Spa:Enum:MediaType:stream", SPA_TYPE_INFO_MEDIA_TYPE_BASE "stream", SPA_MEDIA_TYPE_stream, },
+ { "Spa:Enum:MediaType:application", SPA_TYPE_INFO_MEDIA_TYPE_BASE "application", SPA_MEDIA_TYPE_application, },
+ { "Spa:Enum:MediaSubtype:raw", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "raw", SPA_MEDIA_SUBTYPE_raw, },
+ { "Spa:Enum:MediaSubtype:dsp", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "dsp", SPA_MEDIA_SUBTYPE_dsp, },
+ { "Spa:Enum:MediaSubtype:control", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "control", SPA_MEDIA_SUBTYPE_control, },
+ { "Spa:Enum:MediaSubtype:h264", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "h264", SPA_MEDIA_SUBTYPE_h264, },
+ { "Spa:Enum:MediaSubtype:mjpg", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "mjpg", SPA_MEDIA_SUBTYPE_mjpg, },
+ { "Spa:Enum:MediaSubtype:dv", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "dv", SPA_MEDIA_SUBTYPE_dv, },
+ { "Spa:Enum:MediaSubtype:mpegts", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "mpegts", SPA_MEDIA_SUBTYPE_mpegts, },
+ { "Spa:Enum:MediaSubtype:h263", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "h263", SPA_MEDIA_SUBTYPE_h263, },
+ { "Spa:Enum:MediaSubtype:mpeg1", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "mpeg1", SPA_MEDIA_SUBTYPE_mpeg1, },
+ { "Spa:Enum:MediaSubtype:mpeg2", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "mpeg2", SPA_MEDIA_SUBTYPE_mpeg2, },
+ { "Spa:Enum:MediaSubtype:mpeg4", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "mpeg4", SPA_MEDIA_SUBTYPE_mpeg4, },
+ { "Spa:Enum:MediaSubtype:xvid", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "xvid", SPA_MEDIA_SUBTYPE_raw, },
+ { "Spa:Enum:MediaSubtype:vc1", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "vc1", SPA_MEDIA_SUBTYPE_raw, },
+ { "Spa:Enum:MediaSubtype:vp8", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "vp8", SPA_MEDIA_SUBTYPE_raw, },
+ { "Spa:Enum:MediaSubtype:vp9", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "vp9", SPA_MEDIA_SUBTYPE_raw, },
+ { "Spa:Enum:MediaSubtype:jpeg", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "jpeg", SPA_MEDIA_SUBTYPE_raw, },
+ { "Spa:Enum:MediaSubtype:bayer", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "bayer", SPA_MEDIA_SUBTYPE_raw, },
+ { "Spa:Enum:MediaSubtype:mp3", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "mp3", SPA_MEDIA_SUBTYPE_raw, },
+ { "Spa:Enum:MediaSubtype:aac", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "aac", SPA_MEDIA_SUBTYPE_raw, },
+ { "Spa:Enum:MediaSubtype:vorbis", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "vorbis", SPA_MEDIA_SUBTYPE_raw, },
+ { "Spa:Enum:MediaSubtype:wma", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "wma", SPA_MEDIA_SUBTYPE_raw, },
+ { "Spa:Enum:MediaSubtype:ra", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "ra", SPA_MEDIA_SUBTYPE_raw, },
+ { "Spa:Enum:MediaSubtype:sbc", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "sbc", SPA_MEDIA_SUBTYPE_raw, },
+ { "Spa:Enum:MediaSubtype:adpcm", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "adpcm", SPA_MEDIA_SUBTYPE_raw, },
+ { "Spa:Enum:MediaSubtype:g723", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "g723", SPA_MEDIA_SUBTYPE_raw, },
+ { "Spa:Enum:MediaSubtype:g726", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "g726", SPA_MEDIA_SUBTYPE_raw, },
+ { "Spa:Enum:MediaSubtype:g729", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "g729", SPA_MEDIA_SUBTYPE_raw, },
+ { "Spa:Enum:MediaSubtype:amr", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "amr", SPA_MEDIA_SUBTYPE_raw, },
+ { "Spa:Enum:MediaSubtype:gsm", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "gsm", SPA_MEDIA_SUBTYPE_raw, },
+ { "Spa:Enum:MediaSubtype:midi", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "midi", SPA_MEDIA_SUBTYPE_raw, },
+ { "Spa:POD:Object:Param:Format:Video:format", SPA_TYPE_INFO_FORMAT_VIDEO_BASE "format", SPA_FORMAT_VIDEO_format,},
+ { "Spa:POD:Object:Param:Format:Video:size", SPA_TYPE_INFO_FORMAT_VIDEO_BASE "size", SPA_FORMAT_VIDEO_size,},
+ { "Spa:POD:Object:Param:Format:Video:framerate", SPA_TYPE_INFO_FORMAT_VIDEO_BASE "framerate", SPA_FORMAT_VIDEO_framerate},
+ { "Spa:POD:Object:Param:Format:Video:max-framerate", SPA_TYPE_INFO_FORMAT_VIDEO_BASE "maxFramerate", SPA_FORMAT_VIDEO_maxFramerate},
+ { "Spa:POD:Object:Param:Format:Video:views", SPA_TYPE_INFO_FORMAT_VIDEO_BASE "views", SPA_FORMAT_VIDEO_views},
+ { "Spa:POD:Object:Param:Format:Video:interlace-mode", },
+ { "Spa:POD:Object:Param:Format:Video:pixel-aspect-ratio", },
+ { "Spa:POD:Object:Param:Format:Video:multiview-mode", },
+ { "Spa:POD:Object:Param:Format:Video:multiview-flags", },
+ { "Spa:POD:Object:Param:Format:Video:chroma-site", },
+ { "Spa:POD:Object:Param:Format:Video:color-range", },
+ { "Spa:POD:Object:Param:Format:Video:color-matrix", },
+ { "Spa:POD:Object:Param:Format:Video:transfer-function", },
+ { "Spa:POD:Object:Param:Format:Video:color-primaries", },
+ { "Spa:POD:Object:Param:Format:Video:profile", },
+ { "Spa:POD:Object:Param:Format:Video:level", },
+ { "Spa:POD:Object:Param:Format:Video:stream-format", },
+ { "Spa:POD:Object:Param:Format:Video:alignment", },
+ { "Spa:POD:Object:Param:Format:Audio:format", SPA_TYPE_INFO_FORMAT_AUDIO_BASE "format", SPA_FORMAT_AUDIO_format,},
+ { "Spa:POD:Object:Param:Format:Audio:flags", },
+ { "Spa:POD:Object:Param:Format:Audio:layout", },
+ { "Spa:POD:Object:Param:Format:Audio:rate", },
+ { "Spa:POD:Object:Param:Format:Audio:channels", },
+ { "Spa:POD:Object:Param:Format:Audio:channel-mask", },
+ { "Spa:Enum:VideoFormat:encoded", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "encoded", SPA_VIDEO_FORMAT_ENCODED,},
+ { "Spa:Enum:VideoFormat:I420", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "I420", SPA_VIDEO_FORMAT_I420,},
+ { "Spa:Enum:VideoFormat:YV12", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "YV12", SPA_VIDEO_FORMAT_YV12,},
+ { "Spa:Enum:VideoFormat:YUY2", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "YUY2", SPA_VIDEO_FORMAT_YUY2,},
+ { "Spa:Enum:VideoFormat:UYVY", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "UYVY", SPA_VIDEO_FORMAT_UYVY,},
+ { "Spa:Enum:VideoFormat:AYUV", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "AYUV", SPA_VIDEO_FORMAT_AYUV,},
+ { "Spa:Enum:VideoFormat:RGBx", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "RGBx", SPA_VIDEO_FORMAT_RGBx,},
+ { "Spa:Enum:VideoFormat:BGRx", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "BGRx", SPA_VIDEO_FORMAT_BGRx,},
+ { "Spa:Enum:VideoFormat:xRGB", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "xRGB", SPA_VIDEO_FORMAT_xRGB,},
+ { "Spa:Enum:VideoFormat:xBGR", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "xBGR", SPA_VIDEO_FORMAT_xBGR,},
+ { "Spa:Enum:VideoFormat:RGBA", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "RGBA", SPA_VIDEO_FORMAT_RGBA,},
+ { "Spa:Enum:VideoFormat:BGRA", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "BGRA", SPA_VIDEO_FORMAT_BGRA,},
+ { "Spa:Enum:VideoFormat:ARGB", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "ARGB", SPA_VIDEO_FORMAT_ARGB,},
+ { "Spa:Enum:VideoFormat:ABGR", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "ABGR", SPA_VIDEO_FORMAT_ABGR,},
+ { "Spa:Enum:VideoFormat:RGB", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "RGB", SPA_VIDEO_FORMAT_RGB,},
+ { "Spa:Enum:VideoFormat:BGR", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "BGR", SPA_VIDEO_FORMAT_BGR,},
+ { "Spa:Enum:VideoFormat:Y41B", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "Y41B", SPA_VIDEO_FORMAT_Y41B,},
+ { "Spa:Enum:VideoFormat:Y42B", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "Y42B", SPA_VIDEO_FORMAT_Y42B,},
+ { "Spa:Enum:VideoFormat:YVYU", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "YVYU", SPA_VIDEO_FORMAT_YVYU,},
+ { "Spa:Enum:VideoFormat:Y444", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "Y444", SPA_VIDEO_FORMAT_Y444,},
+ { "Spa:Enum:VideoFormat:v210", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "v210", SPA_VIDEO_FORMAT_v210,},
+ { "Spa:Enum:VideoFormat:v216", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "v216", SPA_VIDEO_FORMAT_v216,},
+ { "Spa:Enum:VideoFormat:NV12", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "NV12", SPA_VIDEO_FORMAT_NV12,},
+ { "Spa:Enum:VideoFormat:NV21", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "NV21", SPA_VIDEO_FORMAT_NV21,},
+ { "Spa:Enum:VideoFormat:GRAY8", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "GRAY8", SPA_VIDEO_FORMAT_GRAY8,},
+ { "Spa:Enum:VideoFormat:GRAY16_BE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "GRAY16_BE", SPA_VIDEO_FORMAT_GRAY16_BE,},
+ { "Spa:Enum:VideoFormat:GRAY16_LE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "GRAY16_LE", SPA_VIDEO_FORMAT_GRAY16_LE,},
+ { "Spa:Enum:VideoFormat:v308", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "v308", SPA_VIDEO_FORMAT_v308,},
+ { "Spa:Enum:VideoFormat:RGB16", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "RGB16", SPA_VIDEO_FORMAT_RGB16,},
+ { "Spa:Enum:VideoFormat:BGR16", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "BGR16", SPA_VIDEO_FORMAT_BGR16,},
+ { "Spa:Enum:VideoFormat:RGB15", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "RGB15", SPA_VIDEO_FORMAT_RGB15,},
+ { "Spa:Enum:VideoFormat:BGR15", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "BGR15", SPA_VIDEO_FORMAT_BGR15,},
+ { "Spa:Enum:VideoFormat:UYVP", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "UYVP", SPA_VIDEO_FORMAT_UYVP,},
+ { "Spa:Enum:VideoFormat:A420", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "A420", SPA_VIDEO_FORMAT_A420,},
+ { "Spa:Enum:VideoFormat:RGB8P", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "RGB8P", SPA_VIDEO_FORMAT_RGB8P,},
+ { "Spa:Enum:VideoFormat:YUV9", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "YUV9", SPA_VIDEO_FORMAT_YUV9,},
+ { "Spa:Enum:VideoFormat:YVU9", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "YVU9", SPA_VIDEO_FORMAT_YVU9,},
+ { "Spa:Enum:VideoFormat:IYU1", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "IYU1", SPA_VIDEO_FORMAT_IYU1,},
+ { "Spa:Enum:VideoFormat:ARGB64", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "ARGB64", SPA_VIDEO_FORMAT_ARGB64,},
+ { "Spa:Enum:VideoFormat:AYUV64", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "AYUV64", SPA_VIDEO_FORMAT_AYUV64,},
+ { "Spa:Enum:VideoFormat:r210", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "r210", SPA_VIDEO_FORMAT_r210,},
+ { "Spa:Enum:VideoFormat:I420_10BE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "I420_10BE", SPA_VIDEO_FORMAT_I420_10BE,},
+ { "Spa:Enum:VideoFormat:I420_10LE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "I420_10LE", SPA_VIDEO_FORMAT_I420_10LE,},
+ { "Spa:Enum:VideoFormat:I422_10BE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "I422_10BE", SPA_VIDEO_FORMAT_I422_10BE,},
+ { "Spa:Enum:VideoFormat:I422_10LE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "I422_10LE", SPA_VIDEO_FORMAT_I422_10LE,},
+ { "Spa:Enum:VideoFormat:Y444_10BE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "Y444_10BE", SPA_VIDEO_FORMAT_Y444_10BE,},
+ { "Spa:Enum:VideoFormat:Y444_10LE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "Y444_10LE", SPA_VIDEO_FORMAT_Y444_10LE,},
+ { "Spa:Enum:VideoFormat:GBR", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "GBR", SPA_VIDEO_FORMAT_GBR,},
+ { "Spa:Enum:VideoFormat:GBR_10BE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "GBR_10BE", SPA_VIDEO_FORMAT_GBR_10BE,},
+ { "Spa:Enum:VideoFormat:GBR_10LE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "GBR_10LE", SPA_VIDEO_FORMAT_GBR_10LE,},
+ { "Spa:Enum:VideoFormat:NV16", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "NV16", SPA_VIDEO_FORMAT_NV16,},
+ { "Spa:Enum:VideoFormat:NV24", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "NV24", SPA_VIDEO_FORMAT_NV24,},
+ { "Spa:Enum:VideoFormat:NV12_64Z32", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "NV12_64Z32", SPA_VIDEO_FORMAT_NV12_64Z32,},
+ { "Spa:Enum:VideoFormat:A420_10BE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "A420_10BE", SPA_VIDEO_FORMAT_A420_10BE,},
+ { "Spa:Enum:VideoFormat:A420_10LE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "A420_10LE", SPA_VIDEO_FORMAT_A420_10LE,},
+ { "Spa:Enum:VideoFormat:A422_10BE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "A422_10BE", SPA_VIDEO_FORMAT_A422_10BE,},
+ { "Spa:Enum:VideoFormat:A422_10LE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "A422_10LE", SPA_VIDEO_FORMAT_A422_10LE,},
+ { "Spa:Enum:VideoFormat:A444_10BE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "A444_10BE", SPA_VIDEO_FORMAT_A444_10BE,},
+ { "Spa:Enum:VideoFormat:A444_10LE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "A444_10LE", SPA_VIDEO_FORMAT_A444_10LE,},
+ { "Spa:Enum:VideoFormat:NV61", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "NV61", SPA_VIDEO_FORMAT_NV61,},
+ { "Spa:Enum:VideoFormat:P010_10BE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "P010_10BE", SPA_VIDEO_FORMAT_P010_10BE,},
+ { "Spa:Enum:VideoFormat:P010_10LE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "P010_10LE", SPA_VIDEO_FORMAT_P010_10LE,},
+ { "Spa:Enum:VideoFormat:IYU2", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "IYU2", SPA_VIDEO_FORMAT_IYU2,},
+ { "Spa:Enum:VideoFormat:VYUY", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "VYUY", SPA_VIDEO_FORMAT_VYUY,},
+ { "Spa:Enum:VideoFormat:GBRA", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "GBRA", SPA_VIDEO_FORMAT_GBRA,},
+ { "Spa:Enum:VideoFormat:GBRA_10BE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "GBRA_10BE", SPA_VIDEO_FORMAT_GBRA_10BE,},
+ { "Spa:Enum:VideoFormat:GBRA_10LE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "GBRA_10LE", SPA_VIDEO_FORMAT_GBRA_10LE,},
+ { "Spa:Enum:VideoFormat:GBR_12BE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "GBR_12BE", SPA_VIDEO_FORMAT_GBR_12BE,},
+ { "Spa:Enum:VideoFormat:GBR_12LE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "GBR_12LE", SPA_VIDEO_FORMAT_GBR_12LE,},
+ { "Spa:Enum:VideoFormat:GBRA_12BE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "GBRA_12BE", SPA_VIDEO_FORMAT_GBRA_12BE,},
+ { "Spa:Enum:VideoFormat:GBRA_12LE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "GBRA_12LE", SPA_VIDEO_FORMAT_GBRA_12LE,},
+ { "Spa:Enum:VideoFormat:I420_12BE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "I420_12BE", SPA_VIDEO_FORMAT_I420_12BE,},
+ { "Spa:Enum:VideoFormat:I420_12LE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "I420_12LE", SPA_VIDEO_FORMAT_I420_12LE,},
+ { "Spa:Enum:VideoFormat:I422_12BE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "I422_12BE", SPA_VIDEO_FORMAT_I422_12BE,},
+ { "Spa:Enum:VideoFormat:I422_12LE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "I422_12LE", SPA_VIDEO_FORMAT_I422_12LE,},
+ { "Spa:Enum:VideoFormat:Y444_12BE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "Y444_12BE", SPA_VIDEO_FORMAT_Y444_12BE,},
+ { "Spa:Enum:VideoFormat:Y444_12LE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "Y444_12LE", SPA_VIDEO_FORMAT_Y444_12LE,},
+ { "Spa:Enum:VideoFormat:xRGB_210LE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "xRGB_210LE", SPA_VIDEO_FORMAT_xRGB_210LE,},
+ { "Spa:Enum:VideoFormat:xBGR_210LE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "xBGR_210LE", SPA_VIDEO_FORMAT_xBGR_210LE,},
+ { "Spa:Enum:VideoFormat:RGBx_102LE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "RGBx_102LE", SPA_VIDEO_FORMAT_RGBx_102LE,},
+ { "Spa:Enum:VideoFormat:BGRx_102LE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "BGRx_102LE", SPA_VIDEO_FORMAT_BGRx_102LE,},
+ { "Spa:Enum:VideoFormat:ARGB_210LE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "ARGB_210LE", SPA_VIDEO_FORMAT_ARGB_210LE,},
+ { "Spa:Enum:VideoFormat:ABGR_210LE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "ABGR_210LE", SPA_VIDEO_FORMAT_ABGR_210LE,},
+ { "Spa:Enum:VideoFormat:RGBA_102LE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "RGBA_102LE", SPA_VIDEO_FORMAT_RGBA_102LE,},
+ { "Spa:Enum:VideoFormat:BGRA_102LE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "BGRA_102LE", SPA_VIDEO_FORMAT_BGRA_102LE,},
+ { "Spa:Enum:AudioFormat:ENCODED", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "ENCODED", SPA_AUDIO_FORMAT_ENCODED,},
+ { "Spa:Enum:AudioFormat:S8", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S8", SPA_AUDIO_FORMAT_S8, },
+ { "Spa:Enum:AudioFormat:U8", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U8", SPA_AUDIO_FORMAT_U8, },
+ { "Spa:Enum:AudioFormat:S16LE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S16_LE", SPA_AUDIO_FORMAT_S16_LE, },
+ { "Spa:Enum:AudioFormat:U16LE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U16_LE", SPA_AUDIO_FORMAT_U16_LE, },
+ { "Spa:Enum:AudioFormat:S24_32LE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S24_32_LE", SPA_AUDIO_FORMAT_S24_32_LE, },
+ { "Spa:Enum:AudioFormat:U24_32LE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U24_32_LE", SPA_AUDIO_FORMAT_U24_32_LE, },
+ { "Spa:Enum:AudioFormat:S32LE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S32_LE", SPA_AUDIO_FORMAT_S32_LE, },
+ { "Spa:Enum:AudioFormat:U32LE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U32_LE", SPA_AUDIO_FORMAT_U32_LE, },
+ { "Spa:Enum:AudioFormat:S24LE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S24_LE", SPA_AUDIO_FORMAT_S24_LE, },
+ { "Spa:Enum:AudioFormat:U24LE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U24_LE", SPA_AUDIO_FORMAT_U24_LE, },
+ { "Spa:Enum:AudioFormat:S20LE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S20_LE", SPA_AUDIO_FORMAT_S20_LE, },
+ { "Spa:Enum:AudioFormat:U20LE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U20_LE", SPA_AUDIO_FORMAT_U20_LE, },
+ { "Spa:Enum:AudioFormat:S18LE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S18_LE", SPA_AUDIO_FORMAT_S18_LE, },
+ { "Spa:Enum:AudioFormat:U18LE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U18_LE", SPA_AUDIO_FORMAT_U18_LE, },
+ { "Spa:Enum:AudioFormat:F32LE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "F32_LE", SPA_AUDIO_FORMAT_F32_LE, },
+ { "Spa:Enum:AudioFormat:F64LE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "F64_LE", SPA_AUDIO_FORMAT_F64_LE, },
+ { "Spa:Enum:AudioFormat:S16BE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S16_BE", SPA_AUDIO_FORMAT_S16_BE, },
+ { "Spa:Enum:AudioFormat:U16BE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U16_BE", SPA_AUDIO_FORMAT_U16_BE, },
+ { "Spa:Enum:AudioFormat:S24_32BE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S24_32_BE", SPA_AUDIO_FORMAT_S24_32_BE, },
+ { "Spa:Enum:AudioFormat:U24_32BE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U24_32_BE", SPA_AUDIO_FORMAT_U24_32_BE, },
+ { "Spa:Enum:AudioFormat:S32BE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S32_BE", SPA_AUDIO_FORMAT_S32_BE, },
+ { "Spa:Enum:AudioFormat:U32BE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U32_BE", SPA_AUDIO_FORMAT_U32_BE, },
+ { "Spa:Enum:AudioFormat:S24BE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S24_BE", SPA_AUDIO_FORMAT_S24_BE, },
+ { "Spa:Enum:AudioFormat:U24BE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U24_BE", SPA_AUDIO_FORMAT_U24_BE, },
+ { "Spa:Enum:AudioFormat:S20BE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S20_BE", SPA_AUDIO_FORMAT_S20_BE, },
+ { "Spa:Enum:AudioFormat:U20BE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U20_BE", SPA_AUDIO_FORMAT_U20_BE, },
+ { "Spa:Enum:AudioFormat:S18BE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S18_BE", SPA_AUDIO_FORMAT_S18_BE, },
+ { "Spa:Enum:AudioFormat:U18BE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U18_BE", SPA_AUDIO_FORMAT_U18_BE, },
+ { "Spa:Enum:AudioFormat:F32BE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "F32_BE", SPA_AUDIO_FORMAT_F32_BE, },
+ { "Spa:Enum:AudioFormat:F64BE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "F64_BE", SPA_AUDIO_FORMAT_F64_BE, },
+ { "Spa:Enum:AudioFormat:F32P", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "F32P", SPA_AUDIO_FORMAT_F32P, },
+};
diff --git a/src/modules/module-protocol-pulse.c b/src/modules/module-protocol-pulse.c
new file mode 100644
index 0000000..eaf4c2f
--- /dev/null
+++ b/src/modules/module-protocol-pulse.c
@@ -0,0 +1,387 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+
+#include "config.h"
+
+#include <spa/utils/result.h>
+
+#include <pipewire/impl.h>
+
+#include "module-protocol-pulse/pulse-server.h"
+
+/** \page page_module_protocol_pulse PipeWire Module: Protocol Pulse
+ *
+ * This module implements a complete PulseAudio server on top of
+ * PipeWire. This is only the server implementation, client are expected
+ * to use the original PulseAudio client library. This provides a
+ * high level of compatibility with existing applications; in fact,
+ * all usual PulseAudio tools such as pavucontrol, pactl, pamon, paplay
+ * should continue to work as they did before.
+ *
+ * This module is usually loaded as part of a standalone pipewire process,
+ * called pipewire-pulse, with the pipewire-pulse.conf config file.
+ *
+ * The pulse server implements a sample cache that is otherwise not
+ * available in PipeWire.
+ *
+ * ## Module Options
+ *
+ * The module arguments can be the contents of the pulse.properties but
+ * it is recommended to make a separate pulse.properties section in the
+ * config file so that overrides can be done.
+ *
+ * ## pulse.properties
+ *
+ * A config section with server properties can be given.
+ *
+ *\code{.unparsed}
+ * pulse.properties = {
+ * # the addresses this server listens on
+ * server.address = [
+ * "unix:native"
+ * #"unix:/tmp/something" # absolute paths may be used
+ * #"tcp:4713" # IPv4 and IPv6 on all addresses
+ * #"tcp:[::]:9999" # IPv6 on all addresses
+ * #"tcp:127.0.0.1:8888" # IPv4 on a single address
+ * #
+ * #{ address = "tcp:4713" # address
+ * # max-clients = 64 # maximum number of clients
+ * # listen-backlog = 32 # backlog in the server listen queue
+ * # client.access = "restricted" # permissions for clients
+ * #}
+ * ]
+ * #pulse.min.req = 256/48000 # 5ms
+ * #pulse.default.req = 960/48000 # 20 milliseconds
+ * #pulse.min.frag = 256/48000 # 5ms
+ * #pulse.default.frag = 96000/48000 # 2 seconds
+ * #pulse.default.tlength = 96000/48000 # 2 seconds
+ * #pulse.min.quantum = 256/48000 # 5ms
+ * #pulse.default.format = F32
+ * #pulse.default.position = [ FL FR ]
+ * # These overrides are only applied when running in a vm.
+ * vm.overrides = {
+ * pulse.min.quantum = 1024/48000 # 22ms
+ * }
+ * }
+ *\endcode
+ *
+ * ### Connection options
+ *
+ *\code{.unparsed}
+ * ...
+ * server.address = [
+ * "unix:native"
+ * # "tcp:4713"
+ * ]
+ * ...
+ *\endcode
+ *
+ * The addresses the server listens on when starting. Uncomment the `tcp:4713` entry to also
+ * make the server listen on a tcp socket. This is equivalent to loading `module-native-protocol-tcp`.
+ *
+ * There is also a slightly more verbose syntax with more options:
+ *
+ *\code{.unparsed}
+ * ....
+ * server.address = [
+ * { address = "tcp:4713" # address
+ * max-clients = 64 # maximum number of clients
+ * listen-backlog = 32 # backlog in the server listen queue
+ * client.access = "restricted" # permissions for clients
+ * }
+ * ....
+ *\endcode
+ *
+ * Use `client.access` to use one of the access methods to restrict the permissions given to
+ * clients connected via this address.
+ *
+ * By default network access is given the "restricted" permissions. The session manager is responsible
+ * for assigning permission to clients with restricted permissions (usually read-only permissions).
+ *
+ * ### Playback buffering options
+ *
+ *\code{.unparsed}
+ * pulse.min.req = 256/48000 # 5ms
+ *\endcode
+ *
+ * The minimum amount of data to request for clients. The client requested
+ * values will be clamped to this value. Lowering this value together with
+ * tlength can decrease latency if the client wants this, but increase CPU overhead.
+ *
+ *\code{.unparsed}
+ * pulse.default.req = 960/48000 # 20 milliseconds
+ *\endcode
+ *
+ * The default amount of data to request for clients. If the client does not
+ * specify any particular value, this default will be used. Lowering this value
+ * together with tlength can decrease latency but increase CPU overhead.
+ *
+ *\code{.unparsed}
+ * pulse.default.tlength = 96000/48000 # 2 seconds
+ *\endcode
+ *
+ * The target amount of data to buffer on the server side. If the client did not
+ * specify a value, this default will be used. Lower values can decrease the
+ * latency.
+ *
+ * ### Record buffering options
+ *
+ *\code{.unparsed}
+ * pulse.min.frag = 256/48000 # 5ms
+ *\endcode
+ *
+ * The minimum allowed size of the capture buffer before it is sent to a client.
+ * The requested value of the client will be clamped to this. Lowering this value
+ * can reduce latency at the expense of more CPU usage.
+ *
+ *\code{.unparsed}
+ * pulse.default.frag = 96000/48000 # 2 seconds
+ *\endcode
+ *
+ * The default size of the capture buffer before it is sent to a client. If the client
+ * did not specify any value, this default will be used. Lowering this value can
+ * reduce latency at the expense of more CPU usage.
+ *
+ * ### Scheduling options
+ *
+ *\code{.unparsed}
+ * pulse.min.quantum = 256/48000 # 5ms
+ *\endcode
+ *
+ * The minimum quantum (buffer size in samples) to use for pulseaudio clients.
+ * This value is calculated based on the frag and req/tlength for record and
+ * playback streams respectively and then clamped to this value to ensure no
+ * pulseaudio client asks for too small quantums. Lowering this value might
+ * decrease latency at the expense of more CPU usage.
+ *
+ * ### Format options
+ *
+ *\code{.unparsed}
+ * pulse.default.format = F32
+ *\endcode
+ *
+ * Some modules will default to this format when no other format was given. This
+ * is equivalent to the PulseAudio `default-sample-format` option in
+ * `/etc/pulse/daemon.conf`.
+ *
+ *\code{.unparsed}
+ * pulse.default.position = [ FL FR ]
+ *\endcode
+ *
+ * Some modules will default to this channelmap (with its number of channels).
+ * This is equivalent to the PulseAudio `default-sample-channels` and
+ * `default-channel-map` options in `/etc/pulse/daemon.conf`.
+ *
+ * ### VM options
+ *
+ *\code{.unparsed}
+ * vm.overrides = {
+ * pulse.min.quantum = 1024/48000 # 22ms
+ * }
+ *\endcode
+ *
+ * When running in a VM, the `vm.override` section will override the properties
+ * in pulse.properties with the given values. This might be interesting because
+ * VMs usually can't support the low latency settings that are possible on real
+ * hardware.
+ *
+ * ## Stream settings and rules
+ *
+ * Streams created by module-protocol-pulse will use the stream.properties
+ * section and stream.rules sections as usual.
+ *
+ * ## Application settings (Rules)
+ *
+ * The pulse protocol module supports generic config rules. It supports a pulse.rules
+ * section with a `quirks` and an `update-props` action.
+ *
+ *\code{.unparsed}
+ * pulse.rules = [
+ * {
+ * # skype does not want to use devices that don't have an S16 sample format.
+ * matches = [
+ * { application.process.binary = "teams" }
+ * { application.process.binary = "teams-insiders" }
+ * { application.process.binary = "skypeforlinux" }
+ * ]
+ * actions = { quirks = [ force-s16-info ] }
+ * }
+ * {
+ * # speech dispatcher asks for too small latency and then underruns.
+ * matches = [ { application.name = "~speech-dispatcher*" } ]
+ * actions = {
+ * update-props = {
+ * pulse.min.req = 1024/48000 # 21ms
+ * pulse.min.quantum = 1024/48000 # 21ms
+ * }
+ * }
+ * }
+ * ]
+ *\endcode
+ *
+ * ### Quirks
+ *
+ * The quirks action takes an array of quirks to apply for the client.
+ *
+ * * `force-s16-info` makes the sink and source introspect code pretend that the sample format
+ * is S16 (16 bits) samples. Some application refuse the sink/source if this
+ * is not the case.
+ * * `remove-capture-dont-move` Removes the DONT_MOVE flag on capture streams. Some applications
+ * set this flag so that the stream can't be moved anymore with tools such as
+ * pavucontrol.
+ *
+ * ### update-props
+ *
+ * Takes an object with the properties to update on the client. Common actions are to
+ * tweak the quantum values.
+ *
+ * ## Example configuration
+ *
+ *\code{.unparsed}
+ * context.modules = [
+ * { name = libpipewire-module-protocol-pulse
+ * args = { }
+ * }
+ * ]
+ *
+ * pulse.properties = {
+ * server.address = [ "unix:native" ]
+ * }
+ *
+ * pulse.rules = [
+ * {
+ * # skype does not want to use devices that don't have an S16 sample format.
+ * matches = [
+ * { application.process.binary = "teams" }
+ * { application.process.binary = "teams-insiders" }
+ * { application.process.binary = "skypeforlinux" }
+ * ]
+ * actions = { quirks = [ force-s16-info ] }
+ * }
+ * {
+ * # speech dispatcher asks for too small latency and then underruns.
+ * matches = [ { application.name = "~speech-dispatcher*" } ]
+ * actions = {
+ * update-props = {
+ * pulse.min.req = 1024/48000 # 21ms
+ * pulse.min.quantum = 1024/48000 # 21ms
+ * }
+ * }
+ * }
+ * ]
+ *\endcode
+ */
+
+#define NAME "protocol-pulse"
+
+PW_LOG_TOPIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+PW_LOG_TOPIC(pulse_conn, "conn." NAME);
+PW_LOG_TOPIC(pulse_ext_dev_restore, "mod." NAME ".device-restore");
+PW_LOG_TOPIC(pulse_ext_stream_restore, "mod." NAME ".stream-restore");
+
+#define MODULE_USAGE PW_PROTOCOL_PULSE_USAGE
+
+static const struct spa_dict_item module_props[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Implement a PulseAudio server" },
+ { PW_KEY_MODULE_USAGE, MODULE_USAGE },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+struct impl {
+ struct pw_context *context;
+
+ struct spa_hook module_listener;
+
+ struct pw_protocol_pulse *pulse;
+};
+
+static void impl_free(struct impl *impl)
+{
+ spa_hook_remove(&impl->module_listener);
+ if (impl->pulse)
+ pw_protocol_pulse_destroy(impl->pulse);
+ free(impl);
+}
+
+static void module_destroy(void *data)
+{
+ struct impl *impl = data;
+ pw_log_debug("module %p: destroy", impl);
+ impl_free(impl);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy,
+};
+
+SPA_EXPORT
+int pipewire__module_init(struct pw_impl_module *module, const char *args)
+{
+ struct pw_context *context = pw_impl_module_get_context(module);
+ struct pw_properties *props;
+ struct impl *impl;
+ int res;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+ PW_LOG_TOPIC_INIT(pulse_conn);
+ /* it's easier to init these here than adding an init() call to the
+ * extensions */
+ PW_LOG_TOPIC_INIT(pulse_ext_dev_restore);
+ PW_LOG_TOPIC_INIT(pulse_ext_stream_restore);
+
+ impl = calloc(1, sizeof(struct impl));
+ if (impl == NULL)
+ return -errno;
+
+ pw_log_debug("module %p: new %s", impl, args);
+
+ if (args)
+ props = pw_properties_new_string(args);
+ else
+ props = NULL;
+
+ impl->pulse = pw_protocol_pulse_new(context, props, 0);
+ if (impl->pulse == NULL) {
+ res = -errno;
+ free(impl);
+ return res;
+ }
+
+ pw_impl_module_add_listener(module, &impl->module_listener, &module_events, impl);
+
+ pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_props));
+
+ return 0;
+}
diff --git a/src/modules/module-protocol-pulse/client.c b/src/modules/module-protocol-pulse/client.c
new file mode 100644
index 0000000..1e6d202
--- /dev/null
+++ b/src/modules/module-protocol-pulse/client.c
@@ -0,0 +1,390 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <arpa/inet.h>
+#include <sys/socket.h>
+
+#include <spa/utils/defs.h>
+#include <spa/utils/hook.h>
+#include <spa/utils/list.h>
+#include <pipewire/core.h>
+#include <pipewire/log.h>
+#include <pipewire/loop.h>
+#include <pipewire/map.h>
+#include <pipewire/properties.h>
+
+#include "client.h"
+#include "commands.h"
+#include "defs.h"
+#include "internal.h"
+#include "log.h"
+#include "manager.h"
+#include "message.h"
+#include "operation.h"
+#include "pending-sample.h"
+#include "server.h"
+#include "stream.h"
+
+#define client_emit_disconnect(c) spa_hook_list_call(&(c)->listener_list, struct client_events, disconnect, 0)
+
+struct client *client_new(struct server *server)
+{
+ struct client *client = calloc(1, sizeof(*client));
+ if (client == NULL)
+ return NULL;
+
+ client->ref = 1;
+ client->server = server;
+ client->impl = server->impl;
+ client->connect_tag = SPA_ID_INVALID;
+
+ pw_map_init(&client->streams, 16, 16);
+ spa_list_init(&client->out_messages);
+ spa_list_init(&client->operations);
+ spa_list_init(&client->pending_samples);
+ spa_list_init(&client->pending_streams);
+ spa_hook_list_init(&client->listener_list);
+
+ spa_list_append(&server->clients, &client->link);
+ server->n_clients++;
+
+ return client;
+}
+
+static int client_free_stream(void *item, void *data)
+{
+ struct stream *s = item;
+
+ stream_free(s);
+ return 0;
+}
+
+/*
+ * tries to detach the client from the server,
+ * but it does not drop the server's reference
+ */
+bool client_detach(struct client *client)
+{
+ struct impl *impl = client->impl;
+ struct server *server = client->server;
+
+ if (server == NULL)
+ return false;
+
+ pw_log_debug("client %p: detaching from server %p", client, server);
+
+ /* remove from the `server->clients` list */
+ spa_list_remove(&client->link);
+ spa_list_append(&impl->cleanup_clients, &client->link);
+
+ server->n_clients--;
+ if (server->wait_clients > 0 && --server->wait_clients == 0) {
+ int mask = server->source->mask;
+ SPA_FLAG_SET(mask, SPA_IO_IN);
+ pw_loop_update_io(impl->loop, server->source, mask);
+ }
+
+ client->server = NULL;
+
+ return true;
+}
+
+void client_disconnect(struct client *client)
+{
+ struct impl *impl = client->impl;
+
+ if (client->disconnect)
+ return;
+
+ client_emit_disconnect(client);
+
+ /* the client must be detached from the server to disconnect */
+ spa_assert(client->server == NULL);
+
+ client->disconnect = true;
+
+ pw_map_for_each(&client->streams, client_free_stream, client);
+
+ if (client->source) {
+ pw_loop_destroy_source(impl->loop, client->source);
+ client->source = NULL;
+ }
+
+ if (client->manager) {
+ pw_manager_destroy(client->manager);
+ client->manager = NULL;
+ }
+}
+
+void client_free(struct client *client)
+{
+ struct impl *impl = client->impl;
+ struct pending_sample *p;
+ struct message *msg;
+ struct operation *o;
+
+ pw_log_debug("client %p: free", client);
+
+ client_detach(client);
+ client_disconnect(client);
+
+ /* remove from the `impl->cleanup_clients` list */
+ spa_list_remove(&client->link);
+
+ spa_list_consume(p, &client->pending_samples, link)
+ pending_sample_free(p);
+
+ if (client->message)
+ message_free(client->message, false, false);
+
+ spa_list_consume(msg, &client->out_messages, link)
+ message_free(msg, true, false);
+
+ spa_list_consume(o, &client->operations, link)
+ operation_free(o);
+
+ if (client->core)
+ pw_core_disconnect(client->core);
+
+ pw_map_clear(&client->streams);
+
+ pw_work_queue_cancel(impl->work_queue, client, SPA_ID_INVALID);
+
+ free(client->default_sink);
+ free(client->default_source);
+
+ free(client->temporary_default_sink);
+ free(client->temporary_default_source);
+
+ pw_properties_free(client->props);
+ pw_properties_free(client->routes);
+
+ spa_hook_list_clean(&client->listener_list);
+
+ free(client);
+}
+
+int client_queue_message(struct client *client, struct message *msg)
+{
+ struct impl *impl = client->impl;
+ int res;
+
+ if (msg == NULL)
+ return -EINVAL;
+
+ if (client->disconnect) {
+ res = -ENOTCONN;
+ goto error;
+ }
+
+ if (msg->length == 0) {
+ res = 0;
+ goto error;
+ } else if (msg->length > msg->allocated) {
+ res = -ENOMEM;
+ goto error;
+ }
+
+ msg->offset = 0;
+ spa_list_append(&client->out_messages, &msg->link);
+
+ uint32_t mask = client->source->mask;
+ if (!SPA_FLAG_IS_SET(mask, SPA_IO_OUT)) {
+ SPA_FLAG_SET(mask, SPA_IO_OUT);
+ pw_loop_update_io(impl->loop, client->source, mask);
+ }
+
+ client->new_msg_since_last_flush = true;
+
+ return 0;
+
+error:
+ message_free(msg, false, false);
+ return res;
+}
+
+static int client_try_flush_messages(struct client *client)
+{
+ pw_log_trace("client %p: flushing", client);
+
+ spa_assert(!client->disconnect);
+
+ while (!spa_list_is_empty(&client->out_messages)) {
+ struct message *m = spa_list_first(&client->out_messages, struct message, link);
+ struct descriptor desc;
+ const void *data;
+ size_t size;
+
+ if (client->out_index < sizeof(desc)) {
+ desc.length = htonl(m->length);
+ desc.channel = htonl(m->channel);
+ desc.offset_hi = 0;
+ desc.offset_lo = 0;
+ desc.flags = 0;
+
+ data = SPA_PTROFF(&desc, client->out_index, void);
+ size = sizeof(desc) - client->out_index;
+ } else if (client->out_index < m->length + sizeof(desc)) {
+ uint32_t idx = client->out_index - sizeof(desc);
+ data = m->data + idx;
+ size = m->length - idx;
+ } else {
+ if (debug_messages && m->channel == SPA_ID_INVALID)
+ message_dump(SPA_LOG_LEVEL_INFO, m);
+ message_free(m, true, false);
+ client->out_index = 0;
+ continue;
+ }
+
+ while (true) {
+ ssize_t sent = send(client->source->fd, data, size, MSG_NOSIGNAL | MSG_DONTWAIT);
+ if (sent < 0) {
+ int res = -errno;
+ if (res == -EINTR)
+ continue;
+ return res;
+ }
+ client->out_index += sent;
+ break;
+ }
+ }
+ return 0;
+}
+
+int client_flush_messages(struct client *client)
+{
+ client->new_msg_since_last_flush = false;
+
+ int res = client_try_flush_messages(client);
+ if (res >= 0) {
+ uint32_t mask = client->source->mask;
+
+ if (SPA_FLAG_IS_SET(mask, SPA_IO_OUT)) {
+ SPA_FLAG_CLEAR(mask, SPA_IO_OUT);
+ pw_loop_update_io(client->impl->loop, client->source, mask);
+ }
+ } else {
+ if (res != -EAGAIN && res != -EWOULDBLOCK)
+ return res;
+ }
+ return 0;
+}
+
+static bool drop_from_out_queue(struct client *client, struct message *m)
+{
+ spa_assert(!spa_list_is_empty(&client->out_messages));
+
+ struct message *first = spa_list_first(&client->out_messages, struct message, link);
+ if (m == first && client->out_index > 0)
+ return false;
+
+ message_free(m, true, false);
+
+ return true;
+}
+
+/* returns true if an event with the (mask, event, index) triplet should be dropped because it is redundant */
+static bool client_prune_subscribe_events(struct client *client, uint32_t event, uint32_t index)
+{
+ struct message *m, *t;
+
+ if ((event & SUBSCRIPTION_EVENT_TYPE_MASK) == SUBSCRIPTION_EVENT_NEW)
+ return false;
+
+ /* NOTE: reverse iteration */
+ spa_list_for_each_safe_reverse(m, t, &client->out_messages, link) {
+ if (m->extra[0] != COMMAND_SUBSCRIBE_EVENT)
+ continue;
+ if ((m->extra[1] ^ event) & SUBSCRIPTION_EVENT_FACILITY_MASK)
+ continue;
+ if (m->extra[2] != index)
+ continue;
+
+ if ((event & SUBSCRIPTION_EVENT_TYPE_MASK) == SUBSCRIPTION_EVENT_REMOVE) {
+ /* This object is being removed, hence there is
+ * point in keeping the old events regarding
+ * entry in the queue. */
+
+ bool is_new = (m->extra[1] & SUBSCRIPTION_EVENT_TYPE_MASK) == SUBSCRIPTION_EVENT_NEW;
+
+ if (drop_from_out_queue(client, m)) {
+ pw_log_debug("client %p: dropped redundant event due to remove event for object %u",
+ client, index);
+
+ /* if the NEW event for the current object could successfully be dropped,
+ there is no need to deliver the REMOVE event */
+ if (is_new)
+ goto drop;
+ }
+
+ /* stop if the NEW event for the current object is reached */
+ if (is_new)
+ break;
+ }
+
+ if ((event & SUBSCRIPTION_EVENT_TYPE_MASK) == SUBSCRIPTION_EVENT_CHANGE) {
+ /* This object has changed. If a "new" or "change" event for
+ * this object is still in the queue we can exit. */
+ goto drop;
+ }
+ }
+
+ return false;
+
+drop:
+ pw_log_debug("client %p: dropped redundant event for object %u", client, index);
+
+ return true;
+}
+
+int client_queue_subscribe_event(struct client *client, uint32_t mask, uint32_t event, uint32_t index)
+{
+ if (client->disconnect)
+ return -ENOTCONN;
+
+ if (!(client->subscribed & mask))
+ return 0;
+
+ pw_log_debug("client %p: SUBSCRIBE event:%08x index:%u", client, event, index);
+
+ if (client_prune_subscribe_events(client, event, index))
+ return 0;
+
+ struct message *reply = message_alloc(client->impl, -1, 0);
+ reply->extra[0] = COMMAND_SUBSCRIBE_EVENT;
+ reply->extra[1] = event;
+ reply->extra[2] = index;
+
+ message_put(reply,
+ TAG_U32, COMMAND_SUBSCRIBE_EVENT,
+ TAG_U32, -1,
+ TAG_U32, event,
+ TAG_U32, index,
+ TAG_INVALID);
+
+ return client_queue_message(client, reply);
+}
diff --git a/src/modules/module-protocol-pulse/client.h b/src/modules/module-protocol-pulse/client.h
new file mode 100644
index 0000000..ed0ee81
--- /dev/null
+++ b/src/modules/module-protocol-pulse/client.h
@@ -0,0 +1,136 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PULSER_SERVER_CLIENT_H
+#define PULSER_SERVER_CLIENT_H
+
+#include <stdbool.h>
+#include <stdint.h>
+
+#include <spa/utils/list.h>
+#include <spa/utils/hook.h>
+#include <pipewire/map.h>
+
+struct impl;
+struct server;
+struct message;
+struct spa_source;
+struct pw_properties;
+struct pw_core;
+struct pw_manager;
+struct pw_manager_object;
+struct pw_properties;
+
+struct descriptor {
+ uint32_t length;
+ uint32_t channel;
+ uint32_t offset_hi;
+ uint32_t offset_lo;
+ uint32_t flags;
+};
+
+struct client {
+ struct spa_list link;
+ struct impl *impl;
+ struct server *server;
+
+ int ref;
+ const char *name; /* owned by `client::props` */
+
+ struct spa_source *source;
+
+ uint32_t version;
+
+ struct pw_properties *props;
+
+ uint64_t quirks;
+
+ struct pw_core *core;
+ struct pw_manager *manager;
+ struct spa_hook manager_listener;
+
+ uint32_t subscribed;
+
+ struct pw_manager_object *metadata_default;
+ char *default_sink;
+ char *default_source;
+ char *temporary_default_sink; /**< pending value, for MOVE_* commands */
+ char *temporary_default_source; /**< pending value, for MOVE_* commands */
+ struct pw_manager_object *metadata_routes;
+ struct pw_properties *routes;
+
+ uint32_t connect_tag;
+
+ uint32_t in_index;
+ uint32_t out_index;
+ struct descriptor desc;
+ struct message *message;
+
+ struct pw_map streams;
+ struct spa_list out_messages;
+
+ struct spa_list operations;
+
+ struct spa_list pending_samples;
+
+ struct spa_list pending_streams;
+
+ unsigned int disconnect:1;
+ unsigned int new_msg_since_last_flush:1;
+ unsigned int authenticated:1;
+
+ struct pw_manager_object *prev_default_sink;
+ struct pw_manager_object *prev_default_source;
+
+ struct spa_hook_list listener_list;
+};
+
+struct client_events {
+#define VERSION_CLIENT_EVENTS 0
+ uint32_t version;
+
+ void (*disconnect) (void *data);
+};
+
+struct client *client_new(struct server *server);
+bool client_detach(struct client *client);
+void client_disconnect(struct client *client);
+void client_free(struct client *client);
+int client_queue_message(struct client *client, struct message *msg);
+int client_flush_messages(struct client *client);
+int client_queue_subscribe_event(struct client *client, uint32_t mask, uint32_t event, uint32_t id);
+
+static inline void client_unref(struct client *client)
+{
+ if (--client->ref == 0)
+ client_free(client);
+}
+
+static inline void client_add_listener(struct client *client, struct spa_hook *listener,
+ const struct client_events *events, void *data)
+{
+ spa_hook_list_append(&client->listener_list, listener, events, data);
+}
+
+#endif /* PULSER_SERVER_CLIENT_H */
diff --git a/src/modules/module-protocol-pulse/cmd.c b/src/modules/module-protocol-pulse/cmd.c
new file mode 100644
index 0000000..e8e6406
--- /dev/null
+++ b/src/modules/module-protocol-pulse/cmd.c
@@ -0,0 +1,136 @@
+/* PipeWire
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <spa/utils/json.h>
+
+#include <pipewire/utils.h>
+
+#include "module.h"
+#include "cmd.h"
+
+static const char WHITESPACE[] = " \t\n\r";
+
+static int do_load_module(struct impl *impl, char *args, const char *flags)
+{
+ int res, n;
+ struct module *module;
+ char *a[2] = { NULL };
+
+ n = pw_split_ip(args, WHITESPACE, 2, a);
+ if (n < 1) {
+ pw_log_info("load-module expects module name");
+ return -EINVAL;
+ }
+
+ module = module_create(impl, a[0], a[1]);
+ if (module == NULL)
+ return -errno;
+ if ((res = module_load(module)) < 0)
+ return res;
+
+ return res;
+}
+
+static int do_cmd(struct impl *impl, const char *cmd, char *args, const char *flags)
+{
+ int res = 0;
+ if (spa_streq(cmd, "load-module")) {
+ res = do_load_module(impl, args, flags);
+ } else {
+ pw_log_warn("ignoring unknown command `%s` with args `%s`",
+ cmd, args);
+ }
+ if (res < 0) {
+ if (flags && strstr(flags, "nofail")) {
+ pw_log_info("nofail command %s %s: %s",
+ cmd, args, spa_strerror(res));
+ res = 0;
+ } else {
+ pw_log_error("can't run command %s %s: %s",
+ cmd, args, spa_strerror(res));
+ }
+ }
+ return res;
+}
+
+/*
+ * pulse.cmd = [
+ * { cmd = <command> [ args = "<arguments>" ] }
+ * ...
+ * ]
+ */
+static int parse_cmd(void *user_data, const char *location,
+ const char *section, const char *str, size_t len)
+{
+ struct impl *impl = user_data;
+ struct spa_json it[3];
+ char key[512], *s;
+ int res = 0;
+
+ s = strndup(str, len);
+ spa_json_init(&it[0], s, len);
+ if (spa_json_enter_array(&it[0], &it[1]) < 0) {
+ pw_log_error("config file error: pulse.cmd is not an array");
+ res = -EINVAL;
+ goto exit;
+ }
+
+ while (spa_json_enter_object(&it[1], &it[2]) > 0) {
+ char *cmd = NULL, *args = NULL, *flags = NULL;
+
+ while (spa_json_get_string(&it[2], key, sizeof(key)) > 0) {
+ const char *val;
+ int len;
+
+ if ((len = spa_json_next(&it[2], &val)) <= 0)
+ break;
+
+ if (spa_streq(key, "cmd")) {
+ cmd = (char*)val;
+ spa_json_parse_stringn(val, len, cmd, len+1);
+ } else if (spa_streq(key, "args")) {
+ args = (char*)val;
+ spa_json_parse_stringn(val, len, args, len+1);
+ } else if (spa_streq(key, "flags")) {
+ if (spa_json_is_container(val, len))
+ len = spa_json_container_len(&it[2], val, len);
+ flags = (char*)val;
+ spa_json_parse_stringn(val, len, flags, len+1);
+ }
+ }
+ if (cmd != NULL)
+ res = do_cmd(impl, cmd, args, flags);
+ if (res < 0)
+ break;
+ }
+exit:
+ free(s);
+ return res;
+}
+
+int cmd_run(struct impl *impl)
+{
+ return pw_context_conf_section_for_each(impl->context, "pulse.cmd",
+ parse_cmd, impl);
+}
diff --git a/src/modules/module-protocol-pulse/cmd.h b/src/modules/module-protocol-pulse/cmd.h
new file mode 100644
index 0000000..81f528c
--- /dev/null
+++ b/src/modules/module-protocol-pulse/cmd.h
@@ -0,0 +1,32 @@
+/* PipeWire
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PULSER_SERVER_CMD_H
+#define PULSER_SERVER_CMD_H
+
+#include "internal.h"
+
+int cmd_run(struct impl *impl);
+
+#endif /* PULSER_SERVER_CMD_H */
diff --git a/src/modules/module-protocol-pulse/collect.c b/src/modules/module-protocol-pulse/collect.c
new file mode 100644
index 0000000..ac6fd6e
--- /dev/null
+++ b/src/modules/module-protocol-pulse/collect.c
@@ -0,0 +1,549 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <spa/param/props.h>
+#include <spa/pod/builder.h>
+#include <spa/pod/parser.h>
+#include <spa/utils/string.h>
+#include <pipewire/pipewire.h>
+
+#include "collect.h"
+#include "defs.h"
+#include "log.h"
+#include "manager.h"
+
+void select_best(struct selector *s, struct pw_manager_object *o)
+{
+ int32_t prio = 0;
+
+ if (o->props &&
+ pw_properties_fetch_int32(o->props, PW_KEY_PRIORITY_SESSION, &prio) == 0) {
+ if (s->best == NULL || prio > s->score) {
+ s->best = o;
+ s->score = prio;
+ }
+ }
+}
+
+struct pw_manager_object *select_object(struct pw_manager *m, struct selector *s)
+{
+ struct pw_manager_object *o;
+ const char *str;
+
+ spa_list_for_each(o, &m->object_list, link) {
+ if (o->creating || o->removing)
+ continue;
+ if (s->type != NULL && !s->type(o))
+ continue;
+ if (o->id == s->id)
+ return o;
+ if (o->index == s->index)
+ return o;
+ if (s->accumulate)
+ s->accumulate(s, o);
+ if (o->props && s->key != NULL && s->value != NULL &&
+ (str = pw_properties_get(o->props, s->key)) != NULL &&
+ spa_streq(str, s->value))
+ return o;
+ if (s->value != NULL && (uint32_t)atoi(s->value) == o->index)
+ return o;
+ }
+ return s->best;
+}
+
+uint32_t id_to_index(struct pw_manager *m, uint32_t id)
+{
+ struct pw_manager_object *o;
+ spa_list_for_each(o, &m->object_list, link) {
+ if (o->id == id)
+ return o->index;
+ }
+ return SPA_ID_INVALID;
+}
+
+bool collect_is_linked(struct pw_manager *m, uint32_t id, enum pw_direction direction)
+{
+ struct pw_manager_object *o;
+ uint32_t in_node, out_node;
+
+ spa_list_for_each(o, &m->object_list, link) {
+ if (o->props == NULL || !pw_manager_object_is_link(o))
+ continue;
+
+ if (pw_properties_fetch_uint32(o->props, PW_KEY_LINK_OUTPUT_NODE, &out_node) != 0 ||
+ pw_properties_fetch_uint32(o->props, PW_KEY_LINK_INPUT_NODE, &in_node) != 0)
+ continue;
+
+ if ((direction == PW_DIRECTION_OUTPUT && id == out_node) ||
+ (direction == PW_DIRECTION_INPUT && id == in_node))
+ return true;
+ }
+ return false;
+}
+
+struct pw_manager_object *find_peer_for_link(struct pw_manager *m,
+ struct pw_manager_object *o, uint32_t id, enum pw_direction direction)
+{
+ struct pw_manager_object *p;
+ uint32_t in_node, out_node;
+
+ if (o->props == NULL)
+ return NULL;
+
+ if (pw_properties_fetch_uint32(o->props, PW_KEY_LINK_OUTPUT_NODE, &out_node) != 0 ||
+ pw_properties_fetch_uint32(o->props, PW_KEY_LINK_INPUT_NODE, &in_node) != 0)
+ return NULL;
+
+ if (direction == PW_DIRECTION_OUTPUT && id == out_node) {
+ struct selector sel = { .id = in_node, .type = pw_manager_object_is_sink, };
+ if ((p = select_object(m, &sel)) != NULL)
+ return p;
+ }
+ if (direction == PW_DIRECTION_INPUT && id == in_node) {
+ struct selector sel = { .id = out_node, .type = pw_manager_object_is_recordable, };
+ if ((p = select_object(m, &sel)) != NULL)
+ return p;
+ }
+ return NULL;
+}
+
+struct pw_manager_object *find_linked(struct pw_manager *m, uint32_t id, enum pw_direction direction)
+{
+ struct pw_manager_object *o, *p;
+
+ spa_list_for_each(o, &m->object_list, link) {
+ if (!pw_manager_object_is_link(o))
+ continue;
+ if ((p = find_peer_for_link(m, o, id, direction)) != NULL)
+ return p;
+ }
+ return NULL;
+}
+
+void collect_card_info(struct pw_manager_object *card, struct card_info *info)
+{
+ struct pw_manager_param *p;
+
+ spa_list_for_each(p, &card->param_list, link) {
+ switch (p->id) {
+ case SPA_PARAM_EnumProfile:
+ info->n_profiles++;
+ break;
+ case SPA_PARAM_Profile:
+ spa_pod_parse_object(p->param,
+ SPA_TYPE_OBJECT_ParamProfile, NULL,
+ SPA_PARAM_PROFILE_index, SPA_POD_Int(&info->active_profile));
+ break;
+ case SPA_PARAM_EnumRoute:
+ info->n_ports++;
+ break;
+ }
+ }
+}
+
+uint32_t collect_profile_info(struct pw_manager_object *card, struct card_info *card_info,
+ struct profile_info *profile_info)
+{
+ struct pw_manager_param *p;
+ struct profile_info *pi;
+ uint32_t n;
+
+ n = 0;
+ spa_list_for_each(p, &card->param_list, link) {
+ struct spa_pod *classes = NULL;
+
+ if (p->id != SPA_PARAM_EnumProfile)
+ continue;
+
+ pi = &profile_info[n];
+ spa_zero(*pi);
+
+ if (spa_pod_parse_object(p->param,
+ SPA_TYPE_OBJECT_ParamProfile, NULL,
+ SPA_PARAM_PROFILE_index, SPA_POD_Int(&pi->index),
+ SPA_PARAM_PROFILE_name, SPA_POD_String(&pi->name),
+ SPA_PARAM_PROFILE_description, SPA_POD_OPT_String(&pi->description),
+ SPA_PARAM_PROFILE_priority, SPA_POD_OPT_Int(&pi->priority),
+ SPA_PARAM_PROFILE_available, SPA_POD_OPT_Id(&pi->available),
+ SPA_PARAM_PROFILE_classes, SPA_POD_OPT_Pod(&classes)) < 0) {
+ continue;
+ }
+ if (pi->description == NULL)
+ pi->description = pi->name;
+ if (pi->index == card_info->active_profile)
+ card_info->active_profile_name = pi->name;
+
+ if (classes != NULL) {
+ struct spa_pod *iter;
+
+ SPA_POD_STRUCT_FOREACH(classes, iter) {
+ struct spa_pod_parser prs;
+ char *class;
+ uint32_t count;
+
+ spa_pod_parser_pod(&prs, iter);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_String(&class),
+ SPA_POD_Int(&count)) < 0)
+ continue;
+
+ if (spa_streq(class, "Audio/Sink"))
+ pi->n_sinks += count;
+ else if (spa_streq(class, "Audio/Source"))
+ pi->n_sources += count;
+ }
+ }
+ n++;
+ }
+ if (card_info->active_profile_name == NULL && n > 0)
+ card_info->active_profile_name = profile_info[0].name;
+
+ return n;
+}
+
+uint32_t find_profile_index(struct pw_manager_object *card, const char *name)
+{
+ struct pw_manager_param *p;
+
+ spa_list_for_each(p, &card->param_list, link) {
+ uint32_t index;
+ const char *test_name;
+
+ if (p->id != SPA_PARAM_EnumProfile)
+ continue;
+
+ if (spa_pod_parse_object(p->param,
+ SPA_TYPE_OBJECT_ParamProfile, NULL,
+ SPA_PARAM_PROFILE_index, SPA_POD_Int(&index),
+ SPA_PARAM_PROFILE_name, SPA_POD_String(&test_name)) < 0)
+ continue;
+
+ if (spa_streq(test_name, name))
+ return index;
+
+ }
+ return SPA_ID_INVALID;
+}
+
+void collect_device_info(struct pw_manager_object *device, struct pw_manager_object *card,
+ struct device_info *dev_info, bool monitor, struct defs *defs)
+{
+ struct pw_manager_param *p;
+
+ if (card && !monitor) {
+ spa_list_for_each(p, &card->param_list, link) {
+ uint32_t index, dev;
+ struct spa_pod *props;
+
+ if (p->id != SPA_PARAM_Route)
+ continue;
+
+ if (spa_pod_parse_object(p->param,
+ SPA_TYPE_OBJECT_ParamRoute, NULL,
+ SPA_PARAM_ROUTE_index, SPA_POD_Int(&index),
+ SPA_PARAM_ROUTE_device, SPA_POD_Int(&dev),
+ SPA_PARAM_ROUTE_props, SPA_POD_OPT_Pod(&props)) < 0)
+ continue;
+ if (dev != dev_info->device)
+ continue;
+ dev_info->active_port = index;
+ if (props) {
+ volume_parse_param(props, &dev_info->volume_info, monitor);
+ dev_info->have_volume = true;
+ }
+ }
+ }
+
+ spa_list_for_each(p, &device->param_list, link) {
+ switch (p->id) {
+ case SPA_PARAM_EnumFormat:
+ {
+ struct spa_pod *copy = spa_pod_copy(p->param);
+ spa_pod_fixate(copy);
+ format_parse_param(copy, true, &dev_info->ss, &dev_info->map,
+ &defs->sample_spec, &defs->channel_map);
+ free(copy);
+ break;
+ }
+ case SPA_PARAM_Format:
+ format_parse_param(p->param, true, &dev_info->ss, &dev_info->map,
+ NULL, NULL);
+ break;
+
+ case SPA_PARAM_Props:
+ if (!dev_info->have_volume) {
+ volume_parse_param(p->param, &dev_info->volume_info, monitor);
+ dev_info->have_volume = true;
+ }
+ dev_info->have_iec958codecs = spa_pod_find_prop(p->param,
+ NULL, SPA_PROP_iec958Codecs) != NULL;
+ break;
+ }
+ }
+ if (dev_info->ss.channels != dev_info->map.channels)
+ dev_info->ss.channels = dev_info->map.channels;
+ if (dev_info->volume_info.volume.channels != dev_info->map.channels)
+ dev_info->volume_info.volume.channels = dev_info->map.channels;
+}
+
+static bool array_contains(uint32_t *vals, uint32_t n_vals, uint32_t val)
+{
+ uint32_t n;
+ if (vals == NULL || n_vals == 0)
+ return false;
+ for (n = 0; n < n_vals; n++)
+ if (vals[n] == val)
+ return true;
+ return false;
+}
+
+uint32_t collect_port_info(struct pw_manager_object *card, struct card_info *card_info,
+ struct device_info *dev_info, struct port_info *port_info)
+{
+ struct pw_manager_param *p;
+ uint32_t n;
+
+ if (card == NULL)
+ return 0;
+
+ n = 0;
+ spa_list_for_each(p, &card->param_list, link) {
+ struct spa_pod *devices = NULL, *profiles = NULL;
+ struct port_info *pi;
+
+ if (p->id != SPA_PARAM_EnumRoute)
+ continue;
+
+ pi = &port_info[n];
+ spa_zero(*pi);
+
+ if (spa_pod_parse_object(p->param,
+ SPA_TYPE_OBJECT_ParamRoute, NULL,
+ SPA_PARAM_ROUTE_index, SPA_POD_Int(&pi->index),
+ SPA_PARAM_ROUTE_direction, SPA_POD_Id(&pi->direction),
+ SPA_PARAM_ROUTE_name, SPA_POD_String(&pi->name),
+ SPA_PARAM_ROUTE_description, SPA_POD_OPT_String(&pi->description),
+ SPA_PARAM_ROUTE_priority, SPA_POD_OPT_Int(&pi->priority),
+ SPA_PARAM_ROUTE_available, SPA_POD_OPT_Id(&pi->available),
+ SPA_PARAM_ROUTE_info, SPA_POD_OPT_Pod(&pi->info),
+ SPA_PARAM_ROUTE_devices, SPA_POD_OPT_Pod(&devices),
+ SPA_PARAM_ROUTE_profiles, SPA_POD_OPT_Pod(&profiles)) < 0)
+ continue;
+
+ if (pi->description == NULL)
+ pi->description = pi->name;
+ if (devices)
+ pi->devices = spa_pod_get_array(devices, &pi->n_devices);
+ if (profiles)
+ pi->profiles = spa_pod_get_array(profiles, &pi->n_profiles);
+
+ if (dev_info != NULL) {
+ if (pi->direction != dev_info->direction)
+ continue;
+ if (!array_contains(pi->profiles, pi->n_profiles, card_info->active_profile))
+ continue;
+ if (!array_contains(pi->devices, pi->n_devices, dev_info->device))
+ continue;
+ if (pi->index == dev_info->active_port)
+ dev_info->active_port_name = pi->name;
+ }
+
+ while (pi->info != NULL) {
+ struct spa_pod_parser prs;
+ struct spa_pod_frame f[1];
+ uint32_t n;
+ const char *key, *value;
+
+ spa_pod_parser_pod(&prs, pi->info);
+ if (spa_pod_parser_push_struct(&prs, &f[0]) < 0 ||
+ spa_pod_parser_get_int(&prs, (int32_t*)&pi->n_props) < 0)
+ break;
+
+ for (n = 0; n < pi->n_props; n++) {
+ if (spa_pod_parser_get(&prs,
+ SPA_POD_String(&key),
+ SPA_POD_String(&value),
+ NULL) < 0)
+ break;
+ if (spa_streq(key, "port.availability-group"))
+ pi->availability_group = value;
+ else if (spa_streq(key, "port.type"))
+ pi->type = port_type_value(value);
+ }
+ spa_pod_parser_pop(&prs, &f[0]);
+ break;
+ }
+ n++;
+ }
+ if (dev_info != NULL && dev_info->active_port_name == NULL && n > 0)
+ dev_info->active_port_name = port_info[0].name;
+ return n;
+}
+
+uint32_t find_port_index(struct pw_manager_object *card, uint32_t direction, const char *port_name)
+{
+ struct pw_manager_param *p;
+
+ spa_list_for_each(p, &card->param_list, link) {
+ uint32_t index, dir;
+ const char *name;
+
+ if (p->id != SPA_PARAM_EnumRoute)
+ continue;
+
+ if (spa_pod_parse_object(p->param,
+ SPA_TYPE_OBJECT_ParamRoute, NULL,
+ SPA_PARAM_ROUTE_index, SPA_POD_Int(&index),
+ SPA_PARAM_ROUTE_direction, SPA_POD_Id(&dir),
+ SPA_PARAM_ROUTE_name, SPA_POD_String(&name)) < 0)
+ continue;
+ if (dir != direction)
+ continue;
+ if (spa_streq(name, port_name))
+ return index;
+
+ }
+ return SPA_ID_INVALID;
+}
+
+struct spa_dict *collect_props(struct spa_pod *info, struct spa_dict *dict)
+{
+ struct spa_pod_parser prs;
+ struct spa_pod_frame f[1];
+ int32_t n, n_items;
+
+ spa_pod_parser_pod(&prs, info);
+ if (spa_pod_parser_push_struct(&prs, &f[0]) < 0 ||
+ spa_pod_parser_get_int(&prs, &n_items) < 0)
+ return NULL;
+
+ for (n = 0; n < n_items; n++) {
+ if (spa_pod_parser_get(&prs,
+ SPA_POD_String(&dict->items[n].key),
+ SPA_POD_String(&dict->items[n].value),
+ NULL) < 0)
+ break;
+ }
+ spa_pod_parser_pop(&prs, &f[0]);
+ dict->n_items = n;
+ return dict;
+}
+
+uint32_t collect_transport_codec_info(struct pw_manager_object *card,
+ struct transport_codec_info *codecs, uint32_t max_codecs,
+ uint32_t *active)
+{
+ struct pw_manager_param *p;
+ uint32_t n_codecs = 0;
+
+ *active = SPA_ID_INVALID;
+
+ if (card == NULL)
+ return 0;
+
+ spa_list_for_each(p, &card->param_list, link) {
+ uint32_t iid;
+ const struct spa_pod_choice *type;
+ const struct spa_pod_struct *labels;
+ struct spa_pod_parser prs;
+ struct spa_pod_frame f;
+ int32_t *id;
+ bool first;
+
+ if (p->id != SPA_PARAM_PropInfo)
+ continue;
+
+ if (spa_pod_parse_object(p->param,
+ SPA_TYPE_OBJECT_PropInfo, NULL,
+ SPA_PROP_INFO_id, SPA_POD_Id(&iid),
+ SPA_PROP_INFO_type, SPA_POD_PodChoice(&type),
+ SPA_PROP_INFO_labels, SPA_POD_PodStruct(&labels)) < 0)
+ continue;
+
+ if (iid != SPA_PROP_bluetoothAudioCodec)
+ continue;
+
+ if (SPA_POD_CHOICE_TYPE(type) != SPA_CHOICE_Enum ||
+ SPA_POD_TYPE(SPA_POD_CHOICE_CHILD(type)) != SPA_TYPE_Int)
+ continue;
+
+ /*
+ * XXX: PropInfo currently uses Int, not Id, in type and labels.
+ */
+
+ /* Codec name list */
+ first = true;
+ SPA_POD_CHOICE_FOREACH(type, id) {
+ if (first) {
+ /* Skip default */
+ first = false;
+ continue;
+ }
+ if (n_codecs >= max_codecs)
+ break;
+ codecs[n_codecs++].id = *id;
+ }
+
+ /* Codec description list */
+ spa_pod_parser_pod(&prs, (struct spa_pod *)labels);
+ if (spa_pod_parser_push_struct(&prs, &f) < 0)
+ continue;
+
+ while (1) {
+ int32_t id;
+ const char *desc;
+ uint32_t j;
+
+ if (spa_pod_parser_get_int(&prs, &id) < 0 ||
+ spa_pod_parser_get_string(&prs, &desc) < 0)
+ break;
+
+ for (j = 0; j < n_codecs; ++j) {
+ if (codecs[j].id == (uint32_t)id)
+ codecs[j].description = desc;
+ }
+ }
+ }
+
+ /* Active codec */
+ spa_list_for_each(p, &card->param_list, link) {
+ uint32_t j;
+ uint32_t id;
+
+ if (p->id != SPA_PARAM_Props)
+ continue;
+
+ if (spa_pod_parse_object(p->param,
+ SPA_TYPE_OBJECT_Props, NULL,
+ SPA_PROP_bluetoothAudioCodec, SPA_POD_Id(&id)) < 0)
+ continue;
+
+ for (j = 0; j < n_codecs; ++j) {
+ if (codecs[j].id == id)
+ *active = j;
+ }
+ }
+
+ return n_codecs;
+}
diff --git a/src/modules/module-protocol-pulse/collect.h b/src/modules/module-protocol-pulse/collect.h
new file mode 100644
index 0000000..d4b85ab
--- /dev/null
+++ b/src/modules/module-protocol-pulse/collect.h
@@ -0,0 +1,166 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ * Copyright © 2021 Sanchayan Maity <sanchayan@asymptotic.io>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PULSE_SERVER_COLLECT_H
+#define PULSE_SERVER_COLLECT_H
+
+#include <stdbool.h>
+#include <stdint.h>
+
+#include <spa/param/bluetooth/audio.h>
+#include <pipewire/pipewire.h>
+
+#include "internal.h"
+#include "format.h"
+#include "volume.h"
+
+struct pw_manager;
+struct pw_manager_object;
+
+/* ========================================================================== */
+
+struct selector {
+ bool (*type) (struct pw_manager_object *o);
+ uint32_t id;
+ uint32_t index;
+ const char *key;
+ const char *value;
+ void (*accumulate) (struct selector *sel, struct pw_manager_object *o);
+ int32_t score;
+ struct pw_manager_object *best;
+};
+
+struct pw_manager_object *select_object(struct pw_manager *m, struct selector *s);
+uint32_t id_to_index(struct pw_manager *m, uint32_t id);
+void select_best(struct selector *s, struct pw_manager_object *o);
+
+/* ========================================================================== */
+
+struct device_info {
+ uint32_t direction;
+
+ struct sample_spec ss;
+ struct channel_map map;
+ struct volume_info volume_info;
+ unsigned int have_volume:1;
+ unsigned int have_iec958codecs:1;
+
+ uint32_t device;
+ uint32_t active_port;
+ const char *active_port_name;
+};
+
+#define DEVICE_INFO_INIT(_dir) \
+ (struct device_info) { \
+ .direction = _dir, \
+ .ss = SAMPLE_SPEC_INIT, \
+ .map = CHANNEL_MAP_INIT, \
+ .volume_info = VOLUME_INFO_INIT, \
+ .device = SPA_ID_INVALID, \
+ .active_port = SPA_ID_INVALID, \
+ }
+
+void collect_device_info(struct pw_manager_object *device, struct pw_manager_object *card,
+ struct device_info *dev_info, bool monitor, struct defs *defs);
+
+/* ========================================================================== */
+
+struct card_info {
+ uint32_t n_profiles;
+ uint32_t active_profile;
+ const char *active_profile_name;
+
+ uint32_t n_ports;
+};
+
+#define CARD_INFO_INIT \
+ (struct card_info) { \
+ .active_profile = SPA_ID_INVALID, \
+ }
+
+void collect_card_info(struct pw_manager_object *card, struct card_info *info);
+
+/* ========================================================================== */
+
+struct profile_info {
+ uint32_t index;
+ const char *name;
+ const char *description;
+ uint32_t priority;
+ uint32_t available;
+ uint32_t n_sources;
+ uint32_t n_sinks;
+};
+
+uint32_t collect_profile_info(struct pw_manager_object *card, struct card_info *card_info,
+ struct profile_info *profile_info);
+
+/* ========================================================================== */
+
+struct port_info {
+ uint32_t index;
+ uint32_t direction;
+ const char *name;
+ const char *description;
+ uint32_t priority;
+ uint32_t available;
+
+ const char *availability_group;
+ uint32_t type;
+
+ uint32_t n_devices;
+ uint32_t *devices;
+ uint32_t n_profiles;
+ uint32_t *profiles;
+
+ uint32_t n_props;
+ struct spa_pod *info;
+};
+
+uint32_t collect_port_info(struct pw_manager_object *card, struct card_info *card_info,
+ struct device_info *dev_info, struct port_info *port_info);
+
+/* ========================================================================== */
+
+struct transport_codec_info {
+ enum spa_bluetooth_audio_codec id;
+ const char *description;
+};
+
+uint32_t collect_transport_codec_info(struct pw_manager_object *card,
+ struct transport_codec_info *codecs, uint32_t max_codecs,
+ uint32_t *active);
+
+/* ========================================================================== */
+
+struct spa_dict *collect_props(struct spa_pod *info, struct spa_dict *dict);
+uint32_t find_profile_index(struct pw_manager_object *card, const char *name);
+uint32_t find_port_index(struct pw_manager_object *card, uint32_t direction, const char *port_name);
+struct pw_manager_object *find_peer_for_link(struct pw_manager *m,
+ struct pw_manager_object *o, uint32_t id, enum pw_direction direction);
+struct pw_manager_object *find_linked(struct pw_manager *m, uint32_t id, enum pw_direction direction);
+bool collect_is_linked(struct pw_manager *m, uint32_t id, enum pw_direction direction);
+
+#endif
diff --git a/src/modules/module-protocol-pulse/commands.h b/src/modules/module-protocol-pulse/commands.h
new file mode 100644
index 0000000..9dd1d74
--- /dev/null
+++ b/src/modules/module-protocol-pulse/commands.h
@@ -0,0 +1,208 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PULSE_SERVER_COMMANDS_H
+#define PULSE_SERVER_COMMANDS_H
+
+#include <stdint.h>
+
+struct client;
+struct message;
+
+enum pulseaudio_command {
+ /* Generic commands */
+ COMMAND_ERROR,
+ COMMAND_TIMEOUT, /* pseudo command */
+ COMMAND_REPLY,
+
+ /* CLIENT->SERVER */
+ COMMAND_CREATE_PLAYBACK_STREAM, /* Payload changed in v9, v12 (0.9.0, 0.9.8) */
+ COMMAND_DELETE_PLAYBACK_STREAM,
+ COMMAND_CREATE_RECORD_STREAM, /* Payload changed in v9, v12 (0.9.0, 0.9.8) */
+ COMMAND_DELETE_RECORD_STREAM,
+ COMMAND_EXIT,
+ COMMAND_AUTH,
+ COMMAND_SET_CLIENT_NAME,
+ COMMAND_LOOKUP_SINK,
+ COMMAND_LOOKUP_SOURCE,
+ COMMAND_DRAIN_PLAYBACK_STREAM,
+ COMMAND_STAT,
+ COMMAND_GET_PLAYBACK_LATENCY,
+ COMMAND_CREATE_UPLOAD_STREAM,
+ COMMAND_DELETE_UPLOAD_STREAM,
+ COMMAND_FINISH_UPLOAD_STREAM,
+ COMMAND_PLAY_SAMPLE,
+ COMMAND_REMOVE_SAMPLE,
+
+ COMMAND_GET_SERVER_INFO,
+ COMMAND_GET_SINK_INFO,
+ COMMAND_GET_SINK_INFO_LIST,
+ COMMAND_GET_SOURCE_INFO,
+ COMMAND_GET_SOURCE_INFO_LIST,
+ COMMAND_GET_MODULE_INFO,
+ COMMAND_GET_MODULE_INFO_LIST,
+ COMMAND_GET_CLIENT_INFO,
+ COMMAND_GET_CLIENT_INFO_LIST,
+ COMMAND_GET_SINK_INPUT_INFO, /* Payload changed in v11 (0.9.7) */
+ COMMAND_GET_SINK_INPUT_INFO_LIST, /* Payload changed in v11 (0.9.7) */
+ COMMAND_GET_SOURCE_OUTPUT_INFO,
+ COMMAND_GET_SOURCE_OUTPUT_INFO_LIST,
+ COMMAND_GET_SAMPLE_INFO,
+ COMMAND_GET_SAMPLE_INFO_LIST,
+ COMMAND_SUBSCRIBE,
+
+ COMMAND_SET_SINK_VOLUME,
+ COMMAND_SET_SINK_INPUT_VOLUME,
+ COMMAND_SET_SOURCE_VOLUME,
+
+ COMMAND_SET_SINK_MUTE,
+ COMMAND_SET_SOURCE_MUTE,
+
+ COMMAND_CORK_PLAYBACK_STREAM,
+ COMMAND_FLUSH_PLAYBACK_STREAM,
+ COMMAND_TRIGGER_PLAYBACK_STREAM,
+
+ COMMAND_SET_DEFAULT_SINK,
+ COMMAND_SET_DEFAULT_SOURCE,
+
+ COMMAND_SET_PLAYBACK_STREAM_NAME,
+ COMMAND_SET_RECORD_STREAM_NAME,
+
+ COMMAND_KILL_CLIENT,
+ COMMAND_KILL_SINK_INPUT,
+ COMMAND_KILL_SOURCE_OUTPUT,
+
+ COMMAND_LOAD_MODULE,
+ COMMAND_UNLOAD_MODULE,
+
+ /* Obsolete */
+ COMMAND_ADD_AUTOLOAD___OBSOLETE,
+ COMMAND_REMOVE_AUTOLOAD___OBSOLETE,
+ COMMAND_GET_AUTOLOAD_INFO___OBSOLETE,
+ COMMAND_GET_AUTOLOAD_INFO_LIST___OBSOLETE,
+
+ COMMAND_GET_RECORD_LATENCY,
+ COMMAND_CORK_RECORD_STREAM,
+ COMMAND_FLUSH_RECORD_STREAM,
+ COMMAND_PREBUF_PLAYBACK_STREAM,
+
+ /* SERVER->CLIENT */
+ COMMAND_REQUEST,
+ COMMAND_OVERFLOW,
+ COMMAND_UNDERFLOW,
+ COMMAND_PLAYBACK_STREAM_KILLED,
+ COMMAND_RECORD_STREAM_KILLED,
+ COMMAND_SUBSCRIBE_EVENT,
+
+ /* A few more client->server commands */
+
+ /* Supported since protocol v10 (0.9.5) */
+ COMMAND_MOVE_SINK_INPUT,
+ COMMAND_MOVE_SOURCE_OUTPUT,
+
+ /* Supported since protocol v11 (0.9.7) */
+ COMMAND_SET_SINK_INPUT_MUTE,
+
+ COMMAND_SUSPEND_SINK,
+ COMMAND_SUSPEND_SOURCE,
+
+ /* Supported since protocol v12 (0.9.8) */
+ COMMAND_SET_PLAYBACK_STREAM_BUFFER_ATTR,
+ COMMAND_SET_RECORD_STREAM_BUFFER_ATTR,
+
+ COMMAND_UPDATE_PLAYBACK_STREAM_SAMPLE_RATE,
+ COMMAND_UPDATE_RECORD_STREAM_SAMPLE_RATE,
+
+ /* SERVER->CLIENT */
+ COMMAND_PLAYBACK_STREAM_SUSPENDED,
+ COMMAND_RECORD_STREAM_SUSPENDED,
+ COMMAND_PLAYBACK_STREAM_MOVED,
+ COMMAND_RECORD_STREAM_MOVED,
+
+ /* Supported since protocol v13 (0.9.11) */
+ COMMAND_UPDATE_RECORD_STREAM_PROPLIST,
+ COMMAND_UPDATE_PLAYBACK_STREAM_PROPLIST,
+ COMMAND_UPDATE_CLIENT_PROPLIST,
+ COMMAND_REMOVE_RECORD_STREAM_PROPLIST,
+ COMMAND_REMOVE_PLAYBACK_STREAM_PROPLIST,
+ COMMAND_REMOVE_CLIENT_PROPLIST,
+
+ /* SERVER->CLIENT */
+ COMMAND_STARTED,
+
+ /* Supported since protocol v14 (0.9.12) */
+ COMMAND_EXTENSION,
+ /* Supported since protocol v15 (0.9.15) */
+ COMMAND_GET_CARD_INFO,
+ COMMAND_GET_CARD_INFO_LIST,
+ COMMAND_SET_CARD_PROFILE,
+
+ COMMAND_CLIENT_EVENT,
+ COMMAND_PLAYBACK_STREAM_EVENT,
+ COMMAND_RECORD_STREAM_EVENT,
+
+ /* SERVER->CLIENT */
+ COMMAND_PLAYBACK_BUFFER_ATTR_CHANGED,
+ COMMAND_RECORD_BUFFER_ATTR_CHANGED,
+
+ /* Supported since protocol v16 (0.9.16) */
+ COMMAND_SET_SINK_PORT,
+ COMMAND_SET_SOURCE_PORT,
+
+ /* Supported since protocol v22 (1.0) */
+ COMMAND_SET_SOURCE_OUTPUT_VOLUME,
+ COMMAND_SET_SOURCE_OUTPUT_MUTE,
+
+ /* Supported since protocol v27 (3.0) */
+ COMMAND_SET_PORT_LATENCY_OFFSET,
+
+ /* Supported since protocol v30 (6.0) */
+ /* BOTH DIRECTIONS */
+ COMMAND_ENABLE_SRBCHANNEL,
+ COMMAND_DISABLE_SRBCHANNEL,
+
+ /* Supported since protocol v31 (9.0)
+ * BOTH DIRECTIONS */
+ COMMAND_REGISTER_MEMFD_SHMID,
+
+ /* Supported since protocol v35 (15.0) */
+ COMMAND_SEND_OBJECT_MESSAGE,
+
+ COMMAND_MAX
+};
+
+enum command_access_flag {
+ COMMAND_ACCESS_WITHOUT_AUTH = (1 << 0),
+ COMMAND_ACCESS_WITHOUT_MANAGER = (1 << 1),
+};
+
+struct command {
+ const char *name;
+ int (*run) (struct client *client, uint32_t command, uint32_t tag, struct message *msg);
+ uint32_t access;
+};
+
+extern const struct command commands[COMMAND_MAX];
+
+#endif /* PULSE_SERVER_COMMANDS_H */
diff --git a/src/modules/module-protocol-pulse/dbus-name.c b/src/modules/module-protocol-pulse/dbus-name.c
new file mode 100644
index 0000000..b82bc45
--- /dev/null
+++ b/src/modules/module-protocol-pulse/dbus-name.c
@@ -0,0 +1,87 @@
+/* PipeWire
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+
+#include <dbus/dbus.h>
+
+#include <spa/support/dbus.h>
+#include <spa/support/plugin.h>
+#include <pipewire/context.h>
+
+#include "log.h"
+#include "dbus-name.h"
+
+void *dbus_request_name(struct pw_context *context, const char *name)
+{
+ struct spa_dbus *dbus;
+ struct spa_dbus_connection *conn;
+ const struct spa_support *support;
+ uint32_t n_support;
+ DBusConnection *bus;
+ DBusError error;
+
+ support = pw_context_get_support(context, &n_support);
+
+ dbus = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DBus);
+ if (dbus == NULL) {
+ errno = ENOTSUP;
+ return NULL;
+ }
+
+ conn = spa_dbus_get_connection(dbus, SPA_DBUS_TYPE_SESSION);
+ if (conn == NULL)
+ return NULL;
+
+ bus = spa_dbus_connection_get(conn);
+ if (bus == NULL) {
+ spa_dbus_connection_destroy(conn);
+ return NULL;
+ }
+
+ dbus_error_init(&error);
+
+ if (dbus_bus_request_name(bus, name,
+ DBUS_NAME_FLAG_DO_NOT_QUEUE,
+ &error) == DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER)
+ return conn;
+
+ if (dbus_error_is_set(&error))
+ pw_log_error("Failed to acquire %s: %s: %s", name, error.name, error.message);
+ else
+ pw_log_error("D-Bus name %s already taken.", name);
+
+ dbus_error_free(&error);
+
+ spa_dbus_connection_destroy(conn);
+
+ errno = EEXIST;
+ return NULL;
+}
+
+void dbus_release_name(void *data)
+{
+ struct spa_dbus_connection *conn = data;
+ spa_dbus_connection_destroy(conn);
+}
diff --git a/src/modules/module-protocol-pulse/dbus-name.h b/src/modules/module-protocol-pulse/dbus-name.h
new file mode 100644
index 0000000..a15fb80
--- /dev/null
+++ b/src/modules/module-protocol-pulse/dbus-name.h
@@ -0,0 +1,33 @@
+/* PipeWire
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PULSE_SERVER_DBUS_NAME_H
+#define PULSE_SERVER_DBUS_NAME_H
+
+struct pw_context;
+
+void *dbus_request_name(struct pw_context *context, const char *name);
+void dbus_release_name(void *data);
+
+#endif /* PULSE_SERVER_DBUS_NAME_H */
diff --git a/src/modules/module-protocol-pulse/defs.h b/src/modules/module-protocol-pulse/defs.h
new file mode 100644
index 0000000..2e9a43e
--- /dev/null
+++ b/src/modules/module-protocol-pulse/defs.h
@@ -0,0 +1,265 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PULSE_SERVER_DEFS_H
+#define PULSE_SERVER_DEFS_H
+
+#include <pipewire/node.h>
+
+#define FLAG_SHMDATA 0x80000000LU
+#define FLAG_SHMDATA_MEMFD_BLOCK 0x20000000LU
+#define FLAG_SHMRELEASE 0x40000000LU
+#define FLAG_SHMREVOKE 0xC0000000LU
+#define FLAG_SHMMASK 0xFF000000LU
+#define FLAG_SEEKMASK 0x000000FFLU
+#define FLAG_SHMWRITABLE 0x00800000LU
+
+#define SEEK_RELATIVE 0
+#define SEEK_ABSOLUTE 1
+#define SEEK_RELATIVE_ON_READ 2
+#define SEEK_RELATIVE_END 3
+
+#define FRAME_SIZE_MAX_ALLOW (1024*1024*16)
+
+#define PROTOCOL_FLAG_MASK 0xffff0000u
+#define PROTOCOL_VERSION_MASK 0x0000ffffu
+#define PROTOCOL_VERSION 35
+
+#define NATIVE_COOKIE_LENGTH 256
+#define MAX_TAG_SIZE (64*1024)
+
+#define MIN_BUFFERS 1u
+#define MAX_BUFFERS 4u
+
+#define MAXLENGTH (4u*1024*1024) /* 4MB */
+
+#define SCACHE_ENTRY_SIZE_MAX (1024*1024*16)
+
+#define MODULE_INDEX_MASK 0xfffffffu
+#define MODULE_EXTENSION_FLAG (1u << 28)
+#define MODULE_FLAG (1u << 29)
+
+#define DEFAULT_SINK "@DEFAULT_SINK@"
+#define DEFAULT_SOURCE "@DEFAULT_SOURCE@"
+#define DEFAULT_MONITOR "@DEFAULT_MONITOR@"
+
+enum error_code {
+ ERR_OK = 0, /**< No error */
+ ERR_ACCESS, /**< Access failure */
+ ERR_COMMAND, /**< Unknown command */
+ ERR_INVALID, /**< Invalid argument */
+ ERR_EXIST, /**< Entity exists */
+ ERR_NOENTITY, /**< No such entity */
+ ERR_CONNECTIONREFUSED, /**< Connection refused */
+ ERR_PROTOCOL, /**< Protocol error */
+ ERR_TIMEOUT, /**< Timeout */
+ ERR_AUTHKEY, /**< No authentication key */
+ ERR_INTERNAL, /**< Internal error */
+ ERR_CONNECTIONTERMINATED, /**< Connection terminated */
+ ERR_KILLED, /**< Entity killed */
+ ERR_INVALIDSERVER, /**< Invalid server */
+ ERR_MODINITFAILED, /**< Module initialization failed */
+ ERR_BADSTATE, /**< Bad state */
+ ERR_NODATA, /**< No data */
+ ERR_VERSION, /**< Incompatible protocol version */
+ ERR_TOOLARGE, /**< Data too large */
+ ERR_NOTSUPPORTED, /**< Operation not supported \since 0.9.5 */
+ ERR_UNKNOWN, /**< The error code was unknown to the client */
+ ERR_NOEXTENSION, /**< Extension does not exist. \since 0.9.12 */
+ ERR_OBSOLETE, /**< Obsolete functionality. \since 0.9.15 */
+ ERR_NOTIMPLEMENTED, /**< Missing implementation. \since 0.9.15 */
+ ERR_FORKED, /**< The caller forked without calling execve() and tried to reuse the context. \since 0.9.15 */
+ ERR_IO, /**< An IO error happened. \since 0.9.16 */
+ ERR_BUSY, /**< Device or resource busy. \since 0.9.17 */
+ ERR_MAX /**< Not really an error but the first invalid error code */
+};
+
+static inline int res_to_err(int res)
+{
+ switch (res) {
+ case 0: return ERR_OK;
+ case -EACCES: case -EPERM: return ERR_ACCESS;
+ case -ENOTTY: return ERR_COMMAND;
+ case -EINVAL: return ERR_INVALID;
+ case -EEXIST: return ERR_EXIST;
+ case -ENOENT: case -ESRCH: case -ENXIO: case -ENODEV: return ERR_NOENTITY;
+ case -ECONNREFUSED:
+#ifdef ENONET
+ case -ENONET:
+#endif
+ case -EHOSTDOWN: case -ENETDOWN: return ERR_CONNECTIONREFUSED;
+ case -EPROTO: case -EBADMSG: return ERR_PROTOCOL;
+ case -ETIMEDOUT:
+#ifdef ETIME
+ case -ETIME:
+#endif
+ return ERR_TIMEOUT;
+#ifdef ENOKEY
+ case -ENOKEY: return ERR_AUTHKEY;
+#endif
+ case -ECONNRESET: case -EPIPE: return ERR_CONNECTIONTERMINATED;
+#ifdef EBADFD
+ case -EBADFD: return ERR_BADSTATE;
+#endif
+#ifdef ENODATA
+ case -ENODATA: return ERR_NODATA;
+#endif
+ case -EOVERFLOW: case -E2BIG: case -EFBIG:
+ case -ERANGE: case -ENAMETOOLONG: return ERR_TOOLARGE;
+ case -ENOTSUP: case -EPROTONOSUPPORT: case -ESOCKTNOSUPPORT: return ERR_NOTSUPPORTED;
+ case -ENOSYS: return ERR_NOTIMPLEMENTED;
+ case -EIO: return ERR_IO;
+ case -EBUSY: return ERR_BUSY;
+ case -ENFILE: case -EMFILE: return ERR_INTERNAL;
+ }
+ return ERR_UNKNOWN;
+}
+
+enum {
+ SUBSCRIPTION_MASK_NULL = 0x0000U,
+ SUBSCRIPTION_MASK_SINK = 0x0001U,
+ SUBSCRIPTION_MASK_SOURCE = 0x0002U,
+ SUBSCRIPTION_MASK_SINK_INPUT = 0x0004U,
+ SUBSCRIPTION_MASK_SOURCE_OUTPUT = 0x0008U,
+ SUBSCRIPTION_MASK_MODULE = 0x0010U,
+ SUBSCRIPTION_MASK_CLIENT = 0x0020U,
+ SUBSCRIPTION_MASK_SAMPLE_CACHE = 0x0040U,
+ SUBSCRIPTION_MASK_SERVER = 0x0080U,
+ SUBSCRIPTION_MASK_AUTOLOAD = 0x0100U,
+ SUBSCRIPTION_MASK_CARD = 0x0200U,
+ SUBSCRIPTION_MASK_ALL = 0x02ffU
+};
+
+enum {
+ SUBSCRIPTION_EVENT_SINK = 0x0000U,
+ SUBSCRIPTION_EVENT_SOURCE = 0x0001U,
+ SUBSCRIPTION_EVENT_SINK_INPUT = 0x0002U,
+ SUBSCRIPTION_EVENT_SOURCE_OUTPUT = 0x0003U,
+ SUBSCRIPTION_EVENT_MODULE = 0x0004U,
+ SUBSCRIPTION_EVENT_CLIENT = 0x0005U,
+ SUBSCRIPTION_EVENT_SAMPLE_CACHE = 0x0006U,
+ SUBSCRIPTION_EVENT_SERVER = 0x0007U,
+ SUBSCRIPTION_EVENT_AUTOLOAD = 0x0008U,
+ SUBSCRIPTION_EVENT_CARD = 0x0009U,
+ SUBSCRIPTION_EVENT_FACILITY_MASK = 0x000FU,
+
+ SUBSCRIPTION_EVENT_NEW = 0x0000U,
+ SUBSCRIPTION_EVENT_CHANGE = 0x0010U,
+ SUBSCRIPTION_EVENT_REMOVE = 0x0020U,
+ SUBSCRIPTION_EVENT_TYPE_MASK = 0x0030U
+};
+
+enum {
+ STATE_INVALID = -1,
+ STATE_RUNNING = 0,
+ STATE_IDLE = 1,
+ STATE_SUSPENDED = 2,
+ STATE_INIT = -2,
+ STATE_UNLINKED = -3
+};
+
+static inline int node_state(enum pw_node_state state)
+{
+ switch (state) {
+ case PW_NODE_STATE_ERROR:
+ return STATE_UNLINKED;
+ case PW_NODE_STATE_CREATING:
+ return STATE_INIT;
+ case PW_NODE_STATE_SUSPENDED:
+ return STATE_SUSPENDED;
+ case PW_NODE_STATE_IDLE:
+ return STATE_IDLE;
+ case PW_NODE_STATE_RUNNING:
+ return STATE_RUNNING;
+ }
+ return STATE_INVALID;
+}
+
+enum {
+ SINK_HW_VOLUME_CTRL = 0x0001U,
+ SINK_LATENCY = 0x0002U,
+ SINK_HARDWARE = 0x0004U,
+ SINK_NETWORK = 0x0008U,
+ SINK_HW_MUTE_CTRL = 0x0010U,
+ SINK_DECIBEL_VOLUME = 0x0020U,
+ SINK_FLAT_VOLUME = 0x0040U,
+ SINK_DYNAMIC_LATENCY = 0x0080U,
+ SINK_SET_FORMATS = 0x0100U,
+};
+
+enum {
+ SOURCE_HW_VOLUME_CTRL = 0x0001U,
+ SOURCE_LATENCY = 0x0002U,
+ SOURCE_HARDWARE = 0x0004U,
+ SOURCE_NETWORK = 0x0008U,
+ SOURCE_HW_MUTE_CTRL = 0x0010U,
+ SOURCE_DECIBEL_VOLUME = 0x0020U,
+ SOURCE_DYNAMIC_LATENCY = 0x0040U,
+ SOURCE_FLAT_VOLUME = 0x0080U,
+};
+
+static const char * const port_types[] = {
+ "unknown",
+ "aux",
+ "speaker",
+ "headphones",
+ "line",
+ "mic",
+ "headset",
+ "handset",
+ "earpiece",
+ "spdif",
+ "hdmi",
+ "tv",
+ "radio",
+ "video",
+ "usb",
+ "bluetooth",
+ "portable",
+ "handsfree",
+ "car",
+ "hifi",
+ "phone",
+ "network",
+ "analog",
+};
+
+static inline uint32_t port_type_value(const char *port_type)
+{
+ uint32_t i;
+ for (i = 0; i < SPA_N_ELEMENTS(port_types); i++) {
+ if (strcmp(port_types[i], port_type) == 0)
+ return i;
+ }
+ return 0;
+}
+
+#define METADATA_DEFAULT_SINK "default.audio.sink"
+#define METADATA_DEFAULT_SOURCE "default.audio.source"
+#define METADATA_CONFIG_DEFAULT_SINK "default.configured.audio.sink"
+#define METADATA_CONFIG_DEFAULT_SOURCE "default.configured.audio.source"
+#define METADATA_TARGET_NODE "target.node"
+#define METADATA_TARGET_OBJECT "target.object"
+
+#endif /* PULSE_SERVER_DEFS_H */
diff --git a/src/modules/module-protocol-pulse/extension.c b/src/modules/module-protocol-pulse/extension.c
new file mode 100644
index 0000000..0ffa4c5
--- /dev/null
+++ b/src/modules/module-protocol-pulse/extension.c
@@ -0,0 +1,45 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <spa/utils/defs.h>
+#include <spa/utils/string.h>
+
+#include "defs.h"
+#include "extension.h"
+#include "extensions/registry.h"
+
+static const struct extension extensions[] = {
+ { "module-stream-restore", 0 | MODULE_EXTENSION_FLAG, do_extension_stream_restore, },
+ { "module-device-restore", 1 | MODULE_EXTENSION_FLAG, do_extension_device_restore, },
+ { "module-device-manager", 2 | MODULE_EXTENSION_FLAG, do_extension_device_manager, },
+};
+
+const struct extension *extension_find(uint32_t index, const char *name)
+{
+ SPA_FOR_EACH_ELEMENT_VAR(extensions, ext) {
+ if (index == ext->index || spa_streq(name, ext->name))
+ return ext;
+ }
+ return NULL;
+}
diff --git a/src/modules/module-protocol-pulse/extension.h b/src/modules/module-protocol-pulse/extension.h
new file mode 100644
index 0000000..3c1b059
--- /dev/null
+++ b/src/modules/module-protocol-pulse/extension.h
@@ -0,0 +1,47 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PULSE_SERVER_EXTENSION_H
+#define PULSE_SERVER_EXTENSION_H
+
+#include <stdint.h>
+
+struct client;
+struct message;
+
+struct extension_sub {
+ const char *name;
+ uint32_t command;
+ int (*process)(struct client *client, uint32_t command, uint32_t tag, struct message *m);
+};
+
+struct extension {
+ const char *name;
+ uint32_t index;
+ int (*process)(struct client *client, uint32_t tag, struct message *m);
+};
+
+const struct extension *extension_find(uint32_t index, const char *name);
+
+#endif /* PULSE_SERVER_EXTENSION_H */
diff --git a/src/modules/module-protocol-pulse/extensions/ext-device-manager.c b/src/modules/module-protocol-pulse/extensions/ext-device-manager.c
new file mode 100644
index 0000000..2ba080e
--- /dev/null
+++ b/src/modules/module-protocol-pulse/extensions/ext-device-manager.c
@@ -0,0 +1,8 @@
+#include <errno.h>
+
+#include "registry.h"
+
+int do_extension_device_manager(struct client *client, uint32_t tag, struct message *m)
+{
+ return -ENOTSUP;
+}
diff --git a/src/modules/module-protocol-pulse/extensions/ext-device-restore.c b/src/modules/module-protocol-pulse/extensions/ext-device-restore.c
new file mode 100644
index 0000000..4fab654
--- /dev/null
+++ b/src/modules/module-protocol-pulse/extensions/ext-device-restore.c
@@ -0,0 +1,340 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#define EXT_DEVICE_RESTORE_VERSION 1
+
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <spa/utils/defs.h>
+#include <spa/utils/dict.h>
+#include <spa/utils/string.h>
+#include <spa/utils/json.h>
+#include <pipewire/log.h>
+#include <pipewire/properties.h>
+
+#include "../client.h"
+#include "../collect.h"
+#include "../defs.h"
+#include "../extension.h"
+#include "../format.h"
+#include "../manager.h"
+#include "../message.h"
+#include "../reply.h"
+#include "../volume.h"
+#include "registry.h"
+
+PW_LOG_TOPIC_EXTERN(pulse_ext_dev_restore);
+#undef PW_LOG_TOPIC_DEFAULT
+#define PW_LOG_TOPIC_DEFAULT pulse_ext_dev_restore
+
+#define DEVICE_TYPE_SINK 0
+#define DEVICE_TYPE_SOURCE 1
+
+static int do_extension_device_restore_test(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ struct message *reply;
+
+ reply = reply_new(client, tag);
+ message_put(reply,
+ TAG_U32, EXT_DEVICE_RESTORE_VERSION,
+ TAG_INVALID);
+
+ return client_queue_message(client, reply);
+}
+
+static int do_extension_device_restore_subscribe(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ return reply_simple_ack(client, tag);
+}
+
+
+struct format_data {
+ struct client *client;
+ struct message *reply;
+};
+
+static int do_sink_read_format(void *data, struct pw_manager_object *o)
+{
+ struct format_data *d = data;
+ struct pw_manager_param *p;
+ struct format_info info[32];
+ uint32_t i, n_info = 0;
+
+ if (!pw_manager_object_is_sink(o))
+ return 0;
+
+ spa_list_for_each(p, &o->param_list, link) {
+ uint32_t index = 0;
+
+ if (p->id != SPA_PARAM_EnumFormat)
+ continue;
+
+ while (n_info < SPA_N_ELEMENTS(info)) {
+ spa_zero(info[n_info]);
+ if (format_info_from_param(&info[n_info], p->param, index++) < 0)
+ break;
+ if (info[n_info].encoding == ENCODING_ANY) {
+ format_info_clear(&info[n_info]);
+ continue;
+ }
+ n_info++;
+ }
+ }
+ message_put(d->reply,
+ TAG_U32, DEVICE_TYPE_SINK,
+ TAG_U32, o->index, /* sink index */
+ TAG_U8, n_info, /* n_formats */
+ TAG_INVALID);
+ for (i = 0; i < n_info; i++) {
+ message_put(d->reply,
+ TAG_FORMAT_INFO, &info[i],
+ TAG_INVALID);
+ format_info_clear(&info[i]);
+ }
+ return 0;
+}
+
+static int do_extension_device_restore_read_formats_all(struct client *client,
+ uint32_t command, uint32_t tag, struct message *m)
+{
+ struct pw_manager *manager = client->manager;
+ struct format_data data;
+
+ spa_zero(data);
+ data.client = client;
+ data.reply = reply_new(client, tag);
+
+ pw_manager_for_each_object(manager, do_sink_read_format, &data);
+
+ return client_queue_message(client, data.reply);
+}
+
+static int do_extension_device_restore_read_formats(struct client *client,
+ uint32_t command, uint32_t tag, struct message *m)
+{
+ struct pw_manager *manager = client->manager;
+ struct format_data data;
+ uint32_t type, sink_index;
+ struct selector sel;
+ struct pw_manager_object *o;
+ int res;
+
+ if ((res = message_get(m,
+ TAG_U32, &type,
+ TAG_U32, &sink_index,
+ TAG_INVALID)) < 0)
+ return -EPROTO;
+
+ if (type != DEVICE_TYPE_SINK) {
+ pw_log_info("Device format reading is only supported on sinks");
+ return -ENOTSUP;
+ }
+
+ spa_zero(sel);
+ sel.index = sink_index;
+ sel.type = pw_manager_object_is_sink;
+
+ o = select_object(manager, &sel);
+ if (o == NULL)
+ return -ENOENT;
+
+ spa_zero(data);
+ data.client = client;
+ data.reply = reply_new(client, tag);
+
+ do_sink_read_format(&data, o);
+
+ return client_queue_message(client, data.reply);
+}
+
+static int set_card_codecs(struct pw_manager_object *o, uint32_t port_index,
+ uint32_t device_id, uint32_t n_codecs, uint32_t *codecs)
+{
+ char buf[1024];
+ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buf, sizeof(buf));
+ struct spa_pod_frame f[2];
+ struct spa_pod *param;
+
+ if (!SPA_FLAG_IS_SET(o->permissions, PW_PERM_W | PW_PERM_X))
+ return -EACCES;
+
+ if (o->proxy == NULL)
+ return -ENOENT;
+
+ spa_pod_builder_push_object(&b, &f[0],
+ SPA_TYPE_OBJECT_ParamRoute, SPA_PARAM_Route);
+ spa_pod_builder_add(&b,
+ SPA_PARAM_ROUTE_index, SPA_POD_Int(port_index),
+ SPA_PARAM_ROUTE_device, SPA_POD_Int(device_id),
+ 0);
+ spa_pod_builder_prop(&b, SPA_PARAM_ROUTE_props, 0);
+ spa_pod_builder_push_object(&b, &f[1],
+ SPA_TYPE_OBJECT_Props, SPA_PARAM_Props);
+ spa_pod_builder_add(&b,
+ SPA_PROP_iec958Codecs, SPA_POD_Array(sizeof(uint32_t),
+ SPA_TYPE_Id, n_codecs, codecs), 0);
+ spa_pod_builder_pop(&b, &f[1]);
+ spa_pod_builder_prop(&b, SPA_PARAM_ROUTE_save, 0);
+ spa_pod_builder_bool(&b, true);
+ param = spa_pod_builder_pop(&b, &f[0]);
+
+ pw_device_set_param((struct pw_device*)o->proxy,
+ SPA_PARAM_Route, 0, param);
+ return 0;
+}
+
+static int set_node_codecs(struct pw_manager_object *o, uint32_t n_codecs, uint32_t *codecs)
+{
+ char buf[1024];
+ struct spa_pod_builder b;
+ struct spa_pod *param;
+
+ if (!SPA_FLAG_IS_SET(o->permissions, PW_PERM_W | PW_PERM_X))
+ return -EACCES;
+
+ if (o->proxy == NULL)
+ return -ENOENT;
+
+ spa_pod_builder_init(&b, buf, sizeof(buf));
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_Props, SPA_PARAM_Props,
+ SPA_PROP_iec958Codecs, SPA_POD_Array(sizeof(uint32_t),
+ SPA_TYPE_Id, n_codecs, codecs));
+
+ pw_node_set_param((struct pw_node*)o->proxy,
+ SPA_PARAM_Props, 0, param);
+
+ return 0;
+}
+
+
+static int do_extension_device_restore_save_formats(struct client *client,
+ uint32_t command, uint32_t tag, struct message *m)
+{
+ struct impl *impl = client->impl;
+ struct pw_manager *manager = client->manager;
+ struct selector sel;
+ struct pw_manager_object *o, *card = NULL;
+ struct pw_node_info *info;
+ int res;
+ uint32_t type, sink_index, card_id = SPA_ID_INVALID;
+ uint8_t i, n_formats;
+ uint32_t n_codecs = 0, codec, iec958codecs[32];
+ struct device_info dev_info;
+ const char *str;
+
+ if ((res = message_get(m,
+ TAG_U32, &type,
+ TAG_U32, &sink_index,
+ TAG_U8, &n_formats,
+ TAG_INVALID)) < 0)
+ return -EPROTO;
+ if (n_formats < 1)
+ return -EPROTO;
+
+ if (type != DEVICE_TYPE_SINK)
+ return -ENOTSUP;
+
+ for (i = 0; i < n_formats; ++i) {
+ struct format_info format;
+ spa_zero(format);
+ if (message_get(m,
+ TAG_FORMAT_INFO, &format,
+ TAG_INVALID) < 0)
+ return -EPROTO;
+
+ codec = format_encoding2id(format.encoding);
+ if (codec != SPA_ID_INVALID && n_codecs < SPA_N_ELEMENTS(iec958codecs))
+ iec958codecs[n_codecs++] = codec;
+
+ format_info_clear(&format);
+ }
+ if (n_codecs == 0)
+ return -ENOTSUP;
+
+ spa_zero(sel);
+ sel.index = sink_index;
+ sel.type = pw_manager_object_is_sink;
+
+ o = select_object(manager, &sel);
+ if (o == NULL || (info = o->info) == NULL || info->props == NULL)
+ return -ENOENT;
+
+ dev_info = DEVICE_INFO_INIT(SPA_DIRECTION_INPUT);
+
+ if ((str = spa_dict_lookup(info->props, PW_KEY_DEVICE_ID)) != NULL)
+ card_id = (uint32_t)atoi(str);
+ if ((str = spa_dict_lookup(info->props, "card.profile.device")) != NULL)
+ dev_info.device = (uint32_t)atoi(str);
+ if (card_id != SPA_ID_INVALID) {
+ struct selector sel = { .id = card_id, .type = pw_manager_object_is_card, };
+ card = select_object(manager, &sel);
+ }
+ collect_device_info(o, card, &dev_info, false, &impl->defs);
+
+ if (card != NULL && dev_info.active_port != SPA_ID_INVALID) {
+ res = set_card_codecs(card, dev_info.active_port,
+ dev_info.device, n_codecs, iec958codecs);
+ } else {
+ res = set_node_codecs(o, n_codecs, iec958codecs);
+ }
+ if (res < 0)
+ return res;
+
+ return reply_simple_ack(client, tag);
+}
+
+static const struct extension_sub ext_device_restore[] = {
+ { "TEST", 0, do_extension_device_restore_test, },
+ { "SUBSCRIBE", 1, do_extension_device_restore_subscribe, },
+ { "EVENT", 2, },
+ { "READ_FORMATS_ALL", 3, do_extension_device_restore_read_formats_all, },
+ { "READ_FORMATS", 4, do_extension_device_restore_read_formats, },
+ { "SAVE_FORMATS", 5, do_extension_device_restore_save_formats, },
+};
+
+int do_extension_device_restore(struct client *client, uint32_t tag, struct message *m)
+{
+ uint32_t command;
+ int res;
+
+ if ((res = message_get(m,
+ TAG_U32, &command,
+ TAG_INVALID)) < 0)
+ return -EPROTO;
+
+ if (command >= SPA_N_ELEMENTS(ext_device_restore))
+ return -ENOTSUP;
+ if (ext_device_restore[command].process == NULL)
+ return -EPROTO;
+
+ pw_log_info("client %p [%s]: EXT_DEVICE_RESTORE_%s tag:%u",
+ client, client->name, ext_device_restore[command].name, tag);
+
+ return ext_device_restore[command].process(client, command, tag, m);
+}
diff --git a/src/modules/module-protocol-pulse/extensions/ext-stream-restore.c b/src/modules/module-protocol-pulse/extensions/ext-stream-restore.c
new file mode 100644
index 0000000..76c7332
--- /dev/null
+++ b/src/modules/module-protocol-pulse/extensions/ext-stream-restore.c
@@ -0,0 +1,330 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#define EXT_STREAM_RESTORE_VERSION 1
+
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <spa/utils/defs.h>
+#include <spa/utils/dict.h>
+#include <spa/utils/string.h>
+#include <spa/utils/json.h>
+#include <pipewire/log.h>
+#include <pipewire/properties.h>
+
+#include "../client.h"
+#include "../defs.h"
+#include "../extension.h"
+#include "../format.h"
+#include "../manager.h"
+#include "../message.h"
+#include "../remap.h"
+#include "../reply.h"
+#include "../volume.h"
+#include "registry.h"
+
+PW_LOG_TOPIC_EXTERN(pulse_ext_stream_restore);
+#undef PW_LOG_TOPIC_DEFAULT
+#define PW_LOG_TOPIC_DEFAULT pulse_ext_stream_restore
+
+static int do_extension_stream_restore_test(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ struct message *reply;
+
+ reply = reply_new(client, tag);
+ message_put(reply,
+ TAG_U32, EXT_STREAM_RESTORE_VERSION,
+ TAG_INVALID);
+
+ return client_queue_message(client, reply);
+}
+
+static int key_from_name(const char *name, char *key, size_t maxlen)
+{
+ const char *media_class, *select, *str;
+
+ if (spa_strstartswith(name, "sink-input-"))
+ media_class = "Output/Audio";
+ else if (spa_strstartswith(name, "source-output-"))
+ media_class = "Input/Audio";
+ else
+ return -1;
+
+ if ((str = strstr(name, "-by-media-role:")) != NULL) {
+ const struct str_map *map;
+ str += strlen("-by-media-role:");
+ map = str_map_find(media_role_map, NULL, str);
+ str = map ? map->pw_str : str;
+ select = "media.role";
+ }
+ else if ((str = strstr(name, "-by-application-id:")) != NULL) {
+ str += strlen("-by-application-id:");
+ select = "application.id";
+ }
+ else if ((str = strstr(name, "-by-application-name:")) != NULL) {
+ str += strlen("-by-application-name:");
+ select = "application.name";
+ }
+ else if ((str = strstr(name, "-by-media-name:")) != NULL) {
+ str += strlen("-by-media-name:");
+ select = "media.name";
+ } else
+ return -1;
+
+ snprintf(key, maxlen, "restore.stream.%s.%s:%s",
+ media_class, select, str);
+ return 0;
+}
+
+static int key_to_name(const char *key, char *name, size_t maxlen)
+{
+ const char *type, *select, *str;
+
+ if (spa_strstartswith(key, "restore.stream.Output/Audio."))
+ type = "sink-input";
+ else if (spa_strstartswith(key, "restore.stream.Input/Audio."))
+ type = "source-output";
+ else
+ type = "stream";
+
+ if ((str = strstr(key, ".media.role:")) != NULL) {
+ const struct str_map *map;
+ str += strlen(".media.role:");
+ map = str_map_find(media_role_map, str, NULL);
+ select = "media-role";
+ str = map ? map->pa_str : str;
+ }
+ else if ((str = strstr(key, ".application.id:")) != NULL) {
+ str += strlen(".application.id:");
+ select = "application-id";
+ }
+ else if ((str = strstr(key, ".application.name:")) != NULL) {
+ str += strlen(".application.name:");
+ select = "application-name";
+ }
+ else if ((str = strstr(key, ".media.name:")) != NULL) {
+ str += strlen(".media.name:");
+ select = "media-name";
+ }
+ else
+ return -1;
+
+ snprintf(name, maxlen, "%s-by-%s:%s", type, select, str);
+ return 0;
+
+}
+
+static int do_extension_stream_restore_read(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ struct message *reply;
+ const struct spa_dict_item *item;
+
+ reply = reply_new(client, tag);
+
+ spa_dict_for_each(item, &client->routes->dict) {
+ struct spa_json it[3];
+ const char *value;
+ char name[1024], key[128];
+ char device_name[1024] = "\0";
+ bool mute = false;
+ struct volume vol = VOLUME_INIT;
+ struct channel_map map = CHANNEL_MAP_INIT;
+ float volume = 0.0f;
+
+ if (key_to_name(item->key, name, sizeof(name)) < 0)
+ continue;
+
+ pw_log_debug("%s -> %s: %s", item->key, name, item->value);
+
+ spa_json_init(&it[0], item->value, strlen(item->value));
+ if (spa_json_enter_object(&it[0], &it[1]) <= 0)
+ continue;
+
+ while (spa_json_get_string(&it[1], key, sizeof(key)) > 0) {
+ if (spa_streq(key, "volume")) {
+ if (spa_json_get_float(&it[1], &volume) <= 0)
+ continue;
+ }
+ else if (spa_streq(key, "mute")) {
+ if (spa_json_get_bool(&it[1], &mute) <= 0)
+ continue;
+ }
+ else if (spa_streq(key, "volumes")) {
+ vol = VOLUME_INIT;
+ if (spa_json_enter_array(&it[1], &it[2]) <= 0)
+ continue;
+
+ for (vol.channels = 0; vol.channels < CHANNELS_MAX; vol.channels++) {
+ if (spa_json_get_float(&it[2], &vol.values[vol.channels]) <= 0)
+ break;
+ }
+ }
+ else if (spa_streq(key, "channels")) {
+ if (spa_json_enter_array(&it[1], &it[2]) <= 0)
+ continue;
+
+ for (map.channels = 0; map.channels < CHANNELS_MAX; map.channels++) {
+ char chname[16];
+ if (spa_json_get_string(&it[2], chname, sizeof(chname)) <= 0)
+ break;
+ map.map[map.channels] = channel_name2id(chname);
+ }
+ }
+ else if (spa_streq(key, "target-node")) {
+ if (spa_json_get_string(&it[1], device_name, sizeof(device_name)) <= 0)
+ continue;
+ }
+ else if (spa_json_next(&it[1], &value) <= 0)
+ break;
+ }
+ message_put(reply,
+ TAG_STRING, name,
+ TAG_CHANNEL_MAP, &map,
+ TAG_CVOLUME, &vol,
+ TAG_STRING, device_name[0] ? device_name : NULL,
+ TAG_BOOLEAN, mute,
+ TAG_INVALID);
+ }
+
+ return client_queue_message(client, reply);
+}
+
+static int do_extension_stream_restore_write(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ int res;
+ uint32_t mode;
+ bool apply;
+
+ if ((res = message_get(m,
+ TAG_U32, &mode,
+ TAG_BOOLEAN, &apply,
+ TAG_INVALID)) < 0)
+ return -EPROTO;
+
+ while (m->offset < m->length) {
+ const char *name, *device_name = NULL;
+ struct channel_map map;
+ struct volume vol;
+ bool mute = false;
+ uint32_t i;
+ FILE *f;
+ char *ptr;
+ size_t size;
+ char key[1024], buf[128];
+
+ spa_zero(map);
+ spa_zero(vol);
+
+ if (message_get(m,
+ TAG_STRING, &name,
+ TAG_CHANNEL_MAP, &map,
+ TAG_CVOLUME, &vol,
+ TAG_STRING, &device_name,
+ TAG_BOOLEAN, &mute,
+ TAG_INVALID) < 0)
+ return -EPROTO;
+
+ if (name == NULL || name[0] == '\0')
+ return -EPROTO;
+
+ if ((f = open_memstream(&ptr, &size)) == NULL)
+ return -errno;
+
+ fprintf(f, "{");
+ fprintf(f, " \"mute\": %s", mute ? "true" : "false");
+ if (vol.channels > 0) {
+ fprintf(f, ", \"volumes\": [");
+ for (i = 0; i < vol.channels; i++)
+ fprintf(f, "%s%s", (i == 0 ? " ":", "),
+ spa_json_format_float(buf, sizeof(buf), vol.values[i]));
+ fprintf(f, " ]");
+ }
+ if (map.channels > 0) {
+ fprintf(f, ", \"channels\": [");
+ for (i = 0; i < map.channels; i++)
+ fprintf(f, "%s\"%s\"", (i == 0 ? " ":", "), channel_id2name(map.map[i]));
+ fprintf(f, " ]");
+ }
+ if (device_name != NULL && device_name[0] &&
+ (client->default_source == NULL || !spa_streq(device_name, client->default_source)) &&
+ (client->default_sink == NULL || !spa_streq(device_name, client->default_sink)))
+ fprintf(f, ", \"target-node\": \"%s\"", device_name);
+ fprintf(f, " }");
+ fclose(f);
+ if (key_from_name(name, key, sizeof(key)) >= 0) {
+ pw_log_debug("%s -> %s: %s", name, key, ptr);
+ if ((res = pw_manager_set_metadata(client->manager,
+ client->metadata_routes,
+ PW_ID_CORE, key, "Spa:String:JSON", "%s", ptr)) < 0)
+ pw_log_warn("failed to set metadata %s = %s, %s", key, ptr, strerror(-res));
+ }
+ free(ptr);
+ }
+
+ return reply_simple_ack(client, tag);
+}
+
+static int do_extension_stream_restore_delete(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ return reply_simple_ack(client, tag);
+}
+
+static int do_extension_stream_restore_subscribe(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ return reply_simple_ack(client, tag);
+}
+
+static const struct extension_sub ext_stream_restore[] = {
+ { "TEST", 0, do_extension_stream_restore_test, },
+ { "READ", 1, do_extension_stream_restore_read, },
+ { "WRITE", 2, do_extension_stream_restore_write, },
+ { "DELETE", 3, do_extension_stream_restore_delete, },
+ { "SUBSCRIBE", 4, do_extension_stream_restore_subscribe, },
+ { "EVENT", 5, },
+};
+
+int do_extension_stream_restore(struct client *client, uint32_t tag, struct message *m)
+{
+ uint32_t command;
+ int res;
+
+ if ((res = message_get(m,
+ TAG_U32, &command,
+ TAG_INVALID)) < 0)
+ return -EPROTO;
+
+ if (command >= SPA_N_ELEMENTS(ext_stream_restore))
+ return -ENOTSUP;
+ if (ext_stream_restore[command].process == NULL)
+ return -EPROTO;
+
+ pw_log_info("client %p [%s]: EXT_STREAM_RESTORE_%s tag:%u",
+ client, client->name, ext_stream_restore[command].name, tag);
+
+ return ext_stream_restore[command].process(client, command, tag, m);
+}
diff --git a/src/modules/module-protocol-pulse/extensions/registry.h b/src/modules/module-protocol-pulse/extensions/registry.h
new file mode 100644
index 0000000..ab9faf7
--- /dev/null
+++ b/src/modules/module-protocol-pulse/extensions/registry.h
@@ -0,0 +1,13 @@
+#ifndef PIPEWIRE_PULSE_EXTENSION_REGISTRY_H
+#define PIPEWIRE_PULSE_EXTENSION_REGISTRY_H
+
+#include <stdint.h>
+
+struct client;
+struct message;
+
+int do_extension_stream_restore(struct client *client, uint32_t tag, struct message *m);
+int do_extension_device_restore(struct client *client, uint32_t tag, struct message *m);
+int do_extension_device_manager(struct client *client, uint32_t tag, struct message *m);
+
+#endif /* PIPEWIRE_PULSE_EXTENSION_REGISTRY_H */
diff --git a/src/modules/module-protocol-pulse/format.c b/src/modules/module-protocol-pulse/format.c
new file mode 100644
index 0000000..ced75cf
--- /dev/null
+++ b/src/modules/module-protocol-pulse/format.c
@@ -0,0 +1,861 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <spa/utils/string.h>
+#include <spa/debug/types.h>
+#include <spa/param/audio/format.h>
+#include <spa/param/audio/format-utils.h>
+#include <spa/param/audio/raw.h>
+#include <spa/utils/json.h>
+
+#include "format.h"
+
+static const struct format audio_formats[] = {
+ [SAMPLE_U8] = { SAMPLE_U8, SPA_AUDIO_FORMAT_U8, "u8", 1 },
+ [SAMPLE_ALAW] = { SAMPLE_ALAW, SPA_AUDIO_FORMAT_ALAW, "aLaw", 1 },
+ [SAMPLE_ULAW] = { SAMPLE_ULAW, SPA_AUDIO_FORMAT_ULAW, "uLaw", 1 },
+ [SAMPLE_S16LE] = { SAMPLE_S16LE, SPA_AUDIO_FORMAT_S16_LE, "s16le", 2 },
+ [SAMPLE_S16BE] = { SAMPLE_S16BE, SPA_AUDIO_FORMAT_S16_BE, "s16be", 2 },
+ [SAMPLE_FLOAT32LE] = { SAMPLE_FLOAT32LE, SPA_AUDIO_FORMAT_F32_LE, "float32le", 4 },
+ [SAMPLE_FLOAT32BE] = { SAMPLE_FLOAT32BE, SPA_AUDIO_FORMAT_F32_BE, "float32be", 4 },
+ [SAMPLE_S32LE] = { SAMPLE_S32LE, SPA_AUDIO_FORMAT_S32_LE, "s32le", 4 },
+ [SAMPLE_S32BE] = { SAMPLE_S32BE, SPA_AUDIO_FORMAT_S32_BE, "s32be", 4 },
+ [SAMPLE_S24LE] = { SAMPLE_S24LE, SPA_AUDIO_FORMAT_S24_LE, "s24le", 3 },
+ [SAMPLE_S24BE] = { SAMPLE_S24BE, SPA_AUDIO_FORMAT_S24_BE, "s24be", 3 },
+ [SAMPLE_S24_32LE] = { SAMPLE_S24_32LE, SPA_AUDIO_FORMAT_S24_32_LE, "s24-32le", 4 },
+ [SAMPLE_S24_32BE] = { SAMPLE_S24_32BE, SPA_AUDIO_FORMAT_S24_32_BE, "s24-32be", 4 },
+
+#if __BYTE_ORDER == __BIG_ENDIAN
+ { SAMPLE_S16BE, SPA_AUDIO_FORMAT_S16_BE, "s16ne", 2 },
+ { SAMPLE_FLOAT32BE, SPA_AUDIO_FORMAT_F32_BE, "float32ne", 4 },
+ { SAMPLE_S32BE, SPA_AUDIO_FORMAT_S32_BE, "s32ne", 4 },
+ { SAMPLE_S24BE, SPA_AUDIO_FORMAT_S24_BE, "s24ne", 3 },
+ { SAMPLE_S24_32BE, SPA_AUDIO_FORMAT_S24_32_BE, "s24-32ne", 4 },
+#elif __BYTE_ORDER == __LITTLE_ENDIAN
+ { SAMPLE_S16LE, SPA_AUDIO_FORMAT_S16_LE, "s16ne", 2 },
+ { SAMPLE_FLOAT32LE, SPA_AUDIO_FORMAT_F32_LE, "float32ne", 4 },
+ { SAMPLE_S32LE, SPA_AUDIO_FORMAT_S32_LE, "s32ne", 4 },
+ { SAMPLE_S24LE, SPA_AUDIO_FORMAT_S24_LE, "s24ne", 3 },
+ { SAMPLE_S24_32LE, SPA_AUDIO_FORMAT_S24_32_LE, "s24-32ne", 4 },
+#endif
+ /* planar formats, we just report them as interleaved */
+ { SAMPLE_U8, SPA_AUDIO_FORMAT_U8P, "u8ne", 1 },
+ { SAMPLE_S16NE, SPA_AUDIO_FORMAT_S16P, "s16ne", 2 },
+ { SAMPLE_S24_32NE, SPA_AUDIO_FORMAT_S24_32P, "s24-32ne", 4 },
+ { SAMPLE_S32NE, SPA_AUDIO_FORMAT_S32P, "s32ne", 4 },
+ { SAMPLE_S24NE, SPA_AUDIO_FORMAT_S24P, "s24ne", 3 },
+ { SAMPLE_FLOAT32NE, SPA_AUDIO_FORMAT_F32P, "float32ne", 4 },
+};
+
+static const struct channel audio_channels[] = {
+ [CHANNEL_POSITION_MONO] = { SPA_AUDIO_CHANNEL_MONO, "mono", },
+
+ [CHANNEL_POSITION_FRONT_LEFT] = { SPA_AUDIO_CHANNEL_FL, "front-left", },
+ [CHANNEL_POSITION_FRONT_RIGHT] = { SPA_AUDIO_CHANNEL_FR, "front-right", },
+ [CHANNEL_POSITION_FRONT_CENTER] = { SPA_AUDIO_CHANNEL_FC, "front-center", },
+
+ [CHANNEL_POSITION_REAR_CENTER] = { SPA_AUDIO_CHANNEL_RC, "rear-center", },
+ [CHANNEL_POSITION_REAR_LEFT] = { SPA_AUDIO_CHANNEL_RL, "rear-left", },
+ [CHANNEL_POSITION_REAR_RIGHT] = { SPA_AUDIO_CHANNEL_RR, "rear-right", },
+
+ [CHANNEL_POSITION_LFE] = { SPA_AUDIO_CHANNEL_LFE, "lfe", },
+ [CHANNEL_POSITION_FRONT_LEFT_OF_CENTER] = { SPA_AUDIO_CHANNEL_FLC, "front-left-of-center", },
+ [CHANNEL_POSITION_FRONT_RIGHT_OF_CENTER] = { SPA_AUDIO_CHANNEL_FRC, "front-right-of-center", },
+
+ [CHANNEL_POSITION_SIDE_LEFT] = { SPA_AUDIO_CHANNEL_SL, "side-left", },
+ [CHANNEL_POSITION_SIDE_RIGHT] = { SPA_AUDIO_CHANNEL_SR, "side-right", },
+
+ [CHANNEL_POSITION_AUX0] = { SPA_AUDIO_CHANNEL_AUX0, "aux0", },
+ [CHANNEL_POSITION_AUX1] = { SPA_AUDIO_CHANNEL_AUX1, "aux1", },
+ [CHANNEL_POSITION_AUX2] = { SPA_AUDIO_CHANNEL_AUX2, "aux2", },
+ [CHANNEL_POSITION_AUX3] = { SPA_AUDIO_CHANNEL_AUX3, "aux3", },
+ [CHANNEL_POSITION_AUX4] = { SPA_AUDIO_CHANNEL_AUX4, "aux4", },
+ [CHANNEL_POSITION_AUX5] = { SPA_AUDIO_CHANNEL_AUX5, "aux5", },
+ [CHANNEL_POSITION_AUX6] = { SPA_AUDIO_CHANNEL_AUX6, "aux6", },
+ [CHANNEL_POSITION_AUX7] = { SPA_AUDIO_CHANNEL_AUX7, "aux7", },
+ [CHANNEL_POSITION_AUX8] = { SPA_AUDIO_CHANNEL_AUX8, "aux8", },
+ [CHANNEL_POSITION_AUX9] = { SPA_AUDIO_CHANNEL_AUX9, "aux9", },
+ [CHANNEL_POSITION_AUX10] = { SPA_AUDIO_CHANNEL_AUX10, "aux10", },
+ [CHANNEL_POSITION_AUX11] = { SPA_AUDIO_CHANNEL_AUX11, "aux11", },
+ [CHANNEL_POSITION_AUX12] = { SPA_AUDIO_CHANNEL_AUX12, "aux12", },
+ [CHANNEL_POSITION_AUX13] = { SPA_AUDIO_CHANNEL_AUX13, "aux13", },
+ [CHANNEL_POSITION_AUX14] = { SPA_AUDIO_CHANNEL_AUX14, "aux14", },
+ [CHANNEL_POSITION_AUX15] = { SPA_AUDIO_CHANNEL_AUX15, "aux15", },
+ [CHANNEL_POSITION_AUX16] = { SPA_AUDIO_CHANNEL_AUX16, "aux16", },
+ [CHANNEL_POSITION_AUX17] = { SPA_AUDIO_CHANNEL_AUX17, "aux17", },
+ [CHANNEL_POSITION_AUX18] = { SPA_AUDIO_CHANNEL_AUX18, "aux18", },
+ [CHANNEL_POSITION_AUX19] = { SPA_AUDIO_CHANNEL_AUX19, "aux19", },
+ [CHANNEL_POSITION_AUX20] = { SPA_AUDIO_CHANNEL_AUX20, "aux20", },
+ [CHANNEL_POSITION_AUX21] = { SPA_AUDIO_CHANNEL_AUX21, "aux21", },
+ [CHANNEL_POSITION_AUX22] = { SPA_AUDIO_CHANNEL_AUX22, "aux22", },
+ [CHANNEL_POSITION_AUX23] = { SPA_AUDIO_CHANNEL_AUX23, "aux23", },
+ [CHANNEL_POSITION_AUX24] = { SPA_AUDIO_CHANNEL_AUX24, "aux24", },
+ [CHANNEL_POSITION_AUX25] = { SPA_AUDIO_CHANNEL_AUX25, "aux25", },
+ [CHANNEL_POSITION_AUX26] = { SPA_AUDIO_CHANNEL_AUX26, "aux26", },
+ [CHANNEL_POSITION_AUX27] = { SPA_AUDIO_CHANNEL_AUX27, "aux27", },
+ [CHANNEL_POSITION_AUX28] = { SPA_AUDIO_CHANNEL_AUX28, "aux28", },
+ [CHANNEL_POSITION_AUX29] = { SPA_AUDIO_CHANNEL_AUX29, "aux29", },
+ [CHANNEL_POSITION_AUX30] = { SPA_AUDIO_CHANNEL_AUX30, "aux30", },
+ [CHANNEL_POSITION_AUX31] = { SPA_AUDIO_CHANNEL_AUX31, "aux31", },
+
+ [CHANNEL_POSITION_TOP_CENTER] = { SPA_AUDIO_CHANNEL_TC, "top-center", },
+
+ [CHANNEL_POSITION_TOP_FRONT_LEFT] = { SPA_AUDIO_CHANNEL_TFL, "top-front-left", },
+ [CHANNEL_POSITION_TOP_FRONT_RIGHT] = { SPA_AUDIO_CHANNEL_TFR, "top-front-right", },
+ [CHANNEL_POSITION_TOP_FRONT_CENTER] = { SPA_AUDIO_CHANNEL_TFC, "top-front-center", },
+
+ [CHANNEL_POSITION_TOP_REAR_LEFT] = { SPA_AUDIO_CHANNEL_TRL, "top-rear-left", },
+ [CHANNEL_POSITION_TOP_REAR_RIGHT] = { SPA_AUDIO_CHANNEL_TRR, "top-rear-right", },
+ [CHANNEL_POSITION_TOP_REAR_CENTER] = { SPA_AUDIO_CHANNEL_TRC, "top-rear-center", },
+};
+
+uint32_t format_pa2id(enum sample_format format)
+{
+ if (format < 0 || format >= SAMPLE_MAX)
+ return SPA_AUDIO_FORMAT_UNKNOWN;
+ return audio_formats[format].id;
+}
+
+const char *format_id2name(uint32_t format)
+{
+ int i;
+ for (i = 0; spa_type_audio_format[i].name; i++) {
+ if (spa_type_audio_format[i].type == format)
+ return spa_debug_type_short_name(spa_type_audio_format[i].name);
+ }
+ return "UNKNOWN";
+}
+
+uint32_t format_name2id(const char *name)
+{
+ int i;
+ for (i = 0; spa_type_audio_format[i].name; i++) {
+ if (spa_streq(name, spa_debug_type_short_name(spa_type_audio_format[i].name)))
+ return spa_type_audio_format[i].type;
+ }
+ return SPA_AUDIO_CHANNEL_UNKNOWN;
+}
+
+uint32_t format_paname2id(const char *name, size_t size)
+{
+ SPA_FOR_EACH_ELEMENT_VAR(audio_formats, f) {
+ if (f->name != NULL &&
+ strncmp(name, f->name, size) == 0)
+ return f->id;
+ }
+ return SPA_AUDIO_FORMAT_UNKNOWN;
+}
+
+enum sample_format format_id2pa(uint32_t id)
+{
+ SPA_FOR_EACH_ELEMENT_VAR(audio_formats, f) {
+ if (id == f->id)
+ return f->pa;
+ }
+ return SAMPLE_INVALID;
+}
+
+const char *format_id2paname(uint32_t id)
+{
+ SPA_FOR_EACH_ELEMENT_VAR(audio_formats, f) {
+ if (id == f->id && f->name != NULL)
+ return f->name;
+ }
+ return "invalid";
+}
+
+uint32_t sample_spec_frame_size(const struct sample_spec *ss)
+{
+ switch (ss->format) {
+ case SPA_AUDIO_FORMAT_U8:
+ case SPA_AUDIO_FORMAT_U8P:
+ case SPA_AUDIO_FORMAT_S8:
+ case SPA_AUDIO_FORMAT_S8P:
+ case SPA_AUDIO_FORMAT_ULAW:
+ case SPA_AUDIO_FORMAT_ALAW:
+ return ss->channels;
+ case SPA_AUDIO_FORMAT_S16_LE:
+ case SPA_AUDIO_FORMAT_S16_BE:
+ case SPA_AUDIO_FORMAT_S16P:
+ case SPA_AUDIO_FORMAT_U16_LE:
+ case SPA_AUDIO_FORMAT_U16_BE:
+ return 2 * ss->channels;
+ case SPA_AUDIO_FORMAT_S24_LE:
+ case SPA_AUDIO_FORMAT_S24_BE:
+ case SPA_AUDIO_FORMAT_S24P:
+ case SPA_AUDIO_FORMAT_U24_LE:
+ case SPA_AUDIO_FORMAT_U24_BE:
+ case SPA_AUDIO_FORMAT_S20_LE:
+ case SPA_AUDIO_FORMAT_S20_BE:
+ case SPA_AUDIO_FORMAT_U20_LE:
+ case SPA_AUDIO_FORMAT_U20_BE:
+ case SPA_AUDIO_FORMAT_S18_LE:
+ case SPA_AUDIO_FORMAT_S18_BE:
+ case SPA_AUDIO_FORMAT_U18_LE:
+ case SPA_AUDIO_FORMAT_U18_BE:
+ return 3 * ss->channels;
+ case SPA_AUDIO_FORMAT_F32_LE:
+ case SPA_AUDIO_FORMAT_F32_BE:
+ case SPA_AUDIO_FORMAT_F32P:
+ case SPA_AUDIO_FORMAT_S32_LE:
+ case SPA_AUDIO_FORMAT_S32_BE:
+ case SPA_AUDIO_FORMAT_S32P:
+ case SPA_AUDIO_FORMAT_U32_LE:
+ case SPA_AUDIO_FORMAT_U32_BE:
+ case SPA_AUDIO_FORMAT_S24_32_LE:
+ case SPA_AUDIO_FORMAT_S24_32_BE:
+ case SPA_AUDIO_FORMAT_S24_32P:
+ case SPA_AUDIO_FORMAT_U24_32_LE:
+ case SPA_AUDIO_FORMAT_U24_32_BE:
+ return 4 * ss->channels;
+ case SPA_AUDIO_FORMAT_F64_LE:
+ case SPA_AUDIO_FORMAT_F64_BE:
+ case SPA_AUDIO_FORMAT_F64P:
+ return 8 * ss->channels;
+ default:
+ return 0;
+ }
+}
+
+bool sample_spec_valid(const struct sample_spec *ss)
+{
+ return (sample_spec_frame_size(ss) > 0 &&
+ ss->rate > 0 && ss->rate <= RATE_MAX &&
+ ss->channels > 0 && ss->channels <= CHANNELS_MAX);
+}
+
+uint32_t channel_pa2id(enum channel_position channel)
+{
+ if (channel < 0 || (size_t)channel >= SPA_N_ELEMENTS(audio_channels))
+ return SPA_AUDIO_CHANNEL_UNKNOWN;
+
+ return audio_channels[channel].channel;
+}
+
+const char *channel_id2name(uint32_t channel)
+{
+ int i;
+ for (i = 0; spa_type_audio_channel[i].name; i++) {
+ if (spa_type_audio_channel[i].type == channel)
+ return spa_debug_type_short_name(spa_type_audio_channel[i].name);
+ }
+ return "UNK";
+}
+
+uint32_t channel_name2id(const char *name)
+{
+ int i;
+ for (i = 0; spa_type_audio_channel[i].name; i++) {
+ if (strcmp(name, spa_debug_type_short_name(spa_type_audio_channel[i].name)) == 0)
+ return spa_type_audio_channel[i].type;
+ }
+ return SPA_AUDIO_CHANNEL_UNKNOWN;
+}
+
+enum channel_position channel_id2pa(uint32_t id, uint32_t *aux)
+{
+ size_t i;
+ for (i = 0; i < SPA_N_ELEMENTS(audio_channels); i++) {
+ if (id == audio_channels[i].channel)
+ return i;
+ }
+ return CHANNEL_POSITION_AUX0 + ((*aux)++ & 31);
+}
+
+const char *channel_id2paname(uint32_t id, uint32_t *aux)
+{
+ SPA_FOR_EACH_ELEMENT_VAR(audio_channels, i) {
+ if (id == i->channel && i->name != NULL)
+ return i->name;
+ }
+ return audio_channels[CHANNEL_POSITION_AUX0 + ((*aux)++ & 31)].name;
+}
+
+uint32_t channel_paname2id(const char *name, size_t size)
+{
+ SPA_FOR_EACH_ELEMENT_VAR(audio_channels, i) {
+ if (strncmp(name, i->name, size) == 0)
+ return i->channel;
+ }
+ return SPA_AUDIO_CHANNEL_UNKNOWN;
+}
+
+
+void channel_map_to_positions(const struct channel_map *map, uint32_t *pos)
+{
+ int i;
+ for (i = 0; i < map->channels; i++)
+ pos[i] = map->map[i];
+}
+
+void channel_map_parse(const char *str, struct channel_map *map)
+{
+ const char *p = str;
+ size_t len;
+
+ if (spa_streq(p, "stereo")) {
+ *map = (struct channel_map) {
+ .channels = 2,
+ .map[0] = SPA_AUDIO_CHANNEL_FL,
+ .map[1] = SPA_AUDIO_CHANNEL_FR,
+ };
+ } else if (spa_streq(p, "surround-21")) {
+ *map = (struct channel_map) {
+ .channels = 3,
+ .map[0] = SPA_AUDIO_CHANNEL_FL,
+ .map[1] = SPA_AUDIO_CHANNEL_FR,
+ .map[2] = SPA_AUDIO_CHANNEL_LFE,
+ };
+ } else if (spa_streq(p, "surround-40")) {
+ *map = (struct channel_map) {
+ .channels = 4,
+ .map[0] = SPA_AUDIO_CHANNEL_FL,
+ .map[1] = SPA_AUDIO_CHANNEL_FR,
+ .map[2] = SPA_AUDIO_CHANNEL_RL,
+ .map[3] = SPA_AUDIO_CHANNEL_RR,
+ };
+ } else if (spa_streq(p, "surround-41")) {
+ *map = (struct channel_map) {
+ .channels = 5,
+ .map[0] = SPA_AUDIO_CHANNEL_FL,
+ .map[1] = SPA_AUDIO_CHANNEL_FR,
+ .map[2] = SPA_AUDIO_CHANNEL_RL,
+ .map[3] = SPA_AUDIO_CHANNEL_RR,
+ .map[4] = SPA_AUDIO_CHANNEL_LFE,
+ };
+ } else if (spa_streq(p, "surround-50")) {
+ *map = (struct channel_map) {
+ .channels = 5,
+ .map[0] = SPA_AUDIO_CHANNEL_FL,
+ .map[1] = SPA_AUDIO_CHANNEL_FR,
+ .map[2] = SPA_AUDIO_CHANNEL_RL,
+ .map[3] = SPA_AUDIO_CHANNEL_RR,
+ .map[4] = SPA_AUDIO_CHANNEL_FC,
+ };
+ } else if (spa_streq(p, "surround-51")) {
+ *map = (struct channel_map) {
+ .channels = 6,
+ .map[0] = SPA_AUDIO_CHANNEL_FL,
+ .map[1] = SPA_AUDIO_CHANNEL_FR,
+ .map[2] = SPA_AUDIO_CHANNEL_RL,
+ .map[3] = SPA_AUDIO_CHANNEL_RR,
+ .map[4] = SPA_AUDIO_CHANNEL_FC,
+ .map[5] = SPA_AUDIO_CHANNEL_LFE,
+ };
+ } else if (spa_streq(p, "surround-71")) {
+ *map = (struct channel_map) {
+ .channels = 8,
+ .map[0] = SPA_AUDIO_CHANNEL_FL,
+ .map[1] = SPA_AUDIO_CHANNEL_FR,
+ .map[2] = SPA_AUDIO_CHANNEL_RL,
+ .map[3] = SPA_AUDIO_CHANNEL_RR,
+ .map[4] = SPA_AUDIO_CHANNEL_FC,
+ .map[5] = SPA_AUDIO_CHANNEL_LFE,
+ .map[6] = SPA_AUDIO_CHANNEL_SL,
+ .map[7] = SPA_AUDIO_CHANNEL_SR,
+ };
+ } else {
+ map->channels = 0;
+ while (*p && map->channels < SPA_AUDIO_MAX_CHANNELS) {
+ if ((len = strcspn(p, ",")) == 0)
+ break;
+ map->map[map->channels++] = channel_paname2id(p, len);
+ p += len + strspn(p+len, ",");
+ }
+ }
+}
+
+bool channel_map_valid(const struct channel_map *map)
+{
+ uint8_t i;
+ uint32_t aux = 0;
+ if (map->channels == 0 || map->channels > CHANNELS_MAX)
+ return false;
+ for (i = 0; i < map->channels; i++)
+ if (channel_id2pa(map->map[i], &aux) >= CHANNEL_POSITION_MAX)
+ return false;
+ return true;
+}
+
+struct encoding_info {
+ const char *name;
+ uint32_t id;
+};
+
+static const struct encoding_info encoding_names[] = {
+ [ENCODING_ANY] = { "ANY", 0 },
+ [ENCODING_PCM] = { "PCM", SPA_AUDIO_IEC958_CODEC_PCM },
+ [ENCODING_AC3_IEC61937] = { "AC3-IEC61937", SPA_AUDIO_IEC958_CODEC_AC3 },
+ [ENCODING_EAC3_IEC61937] = { "EAC3-IEC61937", SPA_AUDIO_IEC958_CODEC_EAC3 },
+ [ENCODING_MPEG_IEC61937] = { "MPEG-IEC61937", SPA_AUDIO_IEC958_CODEC_MPEG },
+ [ENCODING_DTS_IEC61937] = { "DTS-IEC61937", SPA_AUDIO_IEC958_CODEC_DTS },
+ [ENCODING_MPEG2_AAC_IEC61937] = { "MPEG2-AAC-IEC61937", SPA_AUDIO_IEC958_CODEC_MPEG2_AAC },
+ [ENCODING_TRUEHD_IEC61937] = { "TRUEHD-IEC61937", SPA_AUDIO_IEC958_CODEC_TRUEHD },
+ [ENCODING_DTSHD_IEC61937] = { "DTSHD-IEC61937", SPA_AUDIO_IEC958_CODEC_DTSHD },
+};
+
+const char *format_encoding2name(enum encoding enc)
+{
+ if (enc >= 0 && enc < (int)SPA_N_ELEMENTS(encoding_names) &&
+ encoding_names[enc].name != NULL)
+ return encoding_names[enc].name;
+ return "INVALID";
+}
+uint32_t format_encoding2id(enum encoding enc)
+{
+ if (enc >= 0 && enc < (int)SPA_N_ELEMENTS(encoding_names) &&
+ encoding_names[enc].name != NULL)
+ return encoding_names[enc].id;
+ return SPA_ID_INVALID;
+}
+
+static enum encoding format_encoding_from_id(uint32_t id)
+{
+ int i;
+ for (i = 0; i < (int)SPA_N_ELEMENTS(encoding_names); i++) {
+ if (encoding_names[i].id == id)
+ return i;
+ }
+ return ENCODING_ANY;
+}
+
+int format_parse_param(const struct spa_pod *param, bool collect,
+ struct sample_spec *ss, struct channel_map *map,
+ const struct sample_spec *def_ss, const struct channel_map *def_map)
+{
+ struct spa_audio_info info = { 0 };
+ uint32_t i;
+
+ if (spa_format_parse(param, &info.media_type, &info.media_subtype) < 0)
+ return -ENOTSUP;
+
+ if (info.media_type != SPA_MEDIA_TYPE_audio)
+ return -ENOTSUP;
+
+ switch (info.media_subtype) {
+ case SPA_MEDIA_SUBTYPE_raw:
+ if (spa_format_audio_raw_parse(param, &info.info.raw) < 0)
+ return -ENOTSUP;
+ if (def_ss != NULL) {
+ if (ss != NULL)
+ *ss = *def_ss;
+ } else {
+ if (info.info.raw.format == 0 ||
+ info.info.raw.rate == 0 ||
+ info.info.raw.channels == 0 ||
+ info.info.raw.channels > SPA_AUDIO_MAX_CHANNELS)
+ return -ENOTSUP;
+ }
+ break;
+ case SPA_MEDIA_SUBTYPE_iec958:
+ {
+ struct spa_audio_info_iec958 iec;
+
+ if (collect)
+ break;
+
+ if (spa_format_audio_iec958_parse(param, &iec) < 0)
+ return -ENOTSUP;
+
+ info.info.raw.format = SPA_AUDIO_FORMAT_S16;
+ info.info.raw.rate = iec.rate;
+ info.info.raw.position[0] = SPA_AUDIO_CHANNEL_FL;
+ info.info.raw.position[1] = SPA_AUDIO_CHANNEL_FR;
+ switch (iec.codec) {
+ case SPA_AUDIO_IEC958_CODEC_TRUEHD:
+ case SPA_AUDIO_IEC958_CODEC_DTSHD:
+ info.info.raw.channels = 8;
+ info.info.raw.position[2] = SPA_AUDIO_CHANNEL_FC;
+ info.info.raw.position[3] = SPA_AUDIO_CHANNEL_LFE;
+ info.info.raw.position[4] = SPA_AUDIO_CHANNEL_SL;
+ info.info.raw.position[5] = SPA_AUDIO_CHANNEL_SR;
+ info.info.raw.position[6] = SPA_AUDIO_CHANNEL_RL;
+ info.info.raw.position[7] = SPA_AUDIO_CHANNEL_RR;
+ break;
+ default:
+ info.info.raw.channels = 2;
+ break;
+ }
+ break;
+ }
+ default:
+ return -ENOTSUP;
+ }
+ if (ss) {
+ if (info.info.raw.format)
+ ss->format = info.info.raw.format;
+ if (info.info.raw.rate)
+ ss->rate = info.info.raw.rate;
+ if (info.info.raw.channels)
+ ss->channels = info.info.raw.channels;
+ }
+ if (map) {
+ if (info.info.raw.channels) {
+ map->channels = info.info.raw.channels;
+ for (i = 0; i < map->channels; i++)
+ map->map[i] = info.info.raw.position[i];
+ }
+ }
+ return 0;
+}
+
+const struct spa_pod *format_build_param(struct spa_pod_builder *b, uint32_t id,
+ const struct sample_spec *spec, const struct channel_map *map)
+{
+ struct spa_pod_frame f;
+
+ spa_pod_builder_push_object(b, &f, SPA_TYPE_OBJECT_Format, id);
+ spa_pod_builder_add(b,
+ SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw),
+ 0);
+ if (spec->format != SPA_AUDIO_FORMAT_UNKNOWN)
+ spa_pod_builder_add(b,
+ SPA_FORMAT_AUDIO_format, SPA_POD_Id(spec->format), 0);
+ else {
+ spa_pod_builder_add(b,
+ SPA_FORMAT_AUDIO_format, SPA_POD_CHOICE_ENUM_Id(14,
+ SPA_AUDIO_FORMAT_F32,
+ SPA_AUDIO_FORMAT_F32,
+ SPA_AUDIO_FORMAT_F32_OE,
+ SPA_AUDIO_FORMAT_S32,
+ SPA_AUDIO_FORMAT_S32_OE,
+ SPA_AUDIO_FORMAT_S24_32,
+ SPA_AUDIO_FORMAT_S24_32_OE,
+ SPA_AUDIO_FORMAT_S24,
+ SPA_AUDIO_FORMAT_S24_OE,
+ SPA_AUDIO_FORMAT_S16,
+ SPA_AUDIO_FORMAT_S16_OE,
+ SPA_AUDIO_FORMAT_ULAW,
+ SPA_AUDIO_FORMAT_ALAW,
+ SPA_AUDIO_FORMAT_U8),
+ 0);
+
+ }
+
+ if (spec->rate != 0)
+ spa_pod_builder_add(b,
+ SPA_FORMAT_AUDIO_rate, SPA_POD_Int(spec->rate), 0);
+ if (spec->channels != 0) {
+ spa_pod_builder_add(b,
+ SPA_FORMAT_AUDIO_channels, SPA_POD_Int(spec->channels), 0);
+
+ if (map && map->channels == spec->channels) {
+ uint32_t positions[SPA_AUDIO_MAX_CHANNELS];
+ channel_map_to_positions(map, positions);
+ spa_pod_builder_add(b, SPA_FORMAT_AUDIO_position,
+ SPA_POD_Array(sizeof(uint32_t), SPA_TYPE_Id,
+ spec->channels, positions), 0);
+ }
+ }
+ return spa_pod_builder_pop(b, &f);
+}
+
+int format_info_from_spec(struct format_info *info, const struct sample_spec *ss,
+ const struct channel_map *map)
+{
+ spa_zero(*info);
+ info->encoding = ENCODING_PCM;
+ if ((info->props = pw_properties_new(NULL, NULL)) == NULL)
+ return -errno;
+
+ pw_properties_setf(info->props, "format.sample_format", "\"%s\"",
+ format_id2paname(ss->format));
+ pw_properties_setf(info->props, "format.rate", "%d", ss->rate);
+ pw_properties_setf(info->props, "format.channels", "%d", ss->channels);
+ if (map && map->channels == ss->channels) {
+ char chmap[1024] = "";
+ int i, o, r;
+ uint32_t aux = 0;
+
+ for (i = 0, o = 0; i < map->channels; i++) {
+ r = snprintf(chmap+o, sizeof(chmap)-o, "%s%s", i == 0 ? "" : ",",
+ channel_id2paname(map->map[i], &aux));
+ if (r < 0 || o + r >= (int)sizeof(chmap))
+ return -ENOSPC;
+ o += r;
+ }
+ pw_properties_setf(info->props, "format.channel_map", "\"%s\"", chmap);
+ }
+ return 0;
+}
+
+static int add_int(struct format_info *info, const char *k, struct spa_pod *param,
+ uint32_t key)
+{
+ const struct spa_pod_prop *prop;
+ struct spa_pod *val;
+ uint32_t i, n_values, choice;
+ int32_t *values;
+
+ prop = spa_pod_find_prop(param, NULL, key);
+ if (prop == NULL)
+ return -ENOENT;
+
+ val = spa_pod_get_values(&prop->value, &n_values, &choice);
+ if (val->type != SPA_TYPE_Int)
+ return -ENOTSUP;
+
+ values = SPA_POD_BODY(val);
+
+ switch (choice) {
+ case SPA_CHOICE_None:
+ pw_properties_setf(info->props, k, "%d", values[0]);
+ break;
+ case SPA_CHOICE_Range:
+ pw_properties_setf(info->props, k, "{ \"min\": %d, \"max\": %d }",
+ values[1], values[2]);
+ break;
+ case SPA_CHOICE_Enum:
+ {
+ char *ptr;
+ size_t size;
+ FILE *f;
+
+ if ((f = open_memstream(&ptr, &size)) == NULL)
+ return -errno;
+
+ fprintf(f, "[");
+ for (i = 1; i < n_values; i++)
+ fprintf(f, "%s %d", i == 1 ? "" : ",", values[i]);
+ fprintf(f, " ]");
+ fclose(f);
+
+ pw_properties_set(info->props, k, ptr);
+ free(ptr);
+ break;
+ }
+ default:
+ return -ENOTSUP;
+ }
+ return 0;
+}
+
+static int format_info_pcm_from_param(struct format_info *info, struct spa_pod *param, uint32_t index)
+{
+ if (index > 0)
+ return -ENOENT;
+
+ info->encoding = ENCODING_PCM;
+ /* don't add params here yet, pulseaudio doesn't do that either */
+ return 0;
+}
+
+static int format_info_iec958_from_param(struct format_info *info, struct spa_pod *param, uint32_t index)
+{
+ const struct spa_pod_prop *prop;
+ struct spa_pod *val;
+ uint32_t n_values, *values, choice;
+
+ prop = spa_pod_find_prop(param, NULL, SPA_FORMAT_AUDIO_iec958Codec);
+ if (prop == NULL)
+ return -ENOENT;
+
+ val = spa_pod_get_values(&prop->value, &n_values, &choice);
+ if (val->type != SPA_TYPE_Id)
+ return -ENOTSUP;
+
+ if (index >= n_values)
+ return -ENOENT;
+
+ values = SPA_POD_BODY(val);
+
+ switch (choice) {
+ case SPA_CHOICE_None:
+ info->encoding = format_encoding_from_id(values[index]);
+ break;
+ case SPA_CHOICE_Enum:
+ info->encoding = format_encoding_from_id(values[index+1]);
+ break;
+ default:
+ return -ENOTSUP;
+ }
+
+ if ((info->props = pw_properties_new(NULL, NULL)) == NULL)
+ return -errno;
+
+ add_int(info, "format.rate", param, SPA_FORMAT_AUDIO_rate);
+
+ return 0;
+}
+
+int format_info_from_param(struct format_info *info, struct spa_pod *param, uint32_t index)
+{
+ uint32_t media_type, media_subtype;
+ int res;
+
+ if (spa_format_parse(param, &media_type, &media_subtype) < 0)
+ return -ENOTSUP;
+
+ if (media_type != SPA_MEDIA_TYPE_audio)
+ return -ENOTSUP;
+
+ switch(media_subtype) {
+ case SPA_MEDIA_SUBTYPE_raw:
+ res = format_info_pcm_from_param(info, param, index);
+ break;
+ case SPA_MEDIA_SUBTYPE_iec958:
+ res = format_info_iec958_from_param(info, param, index);
+ break;
+ default:
+ return -ENOTSUP;
+ }
+ return res;
+}
+
+static uint32_t format_info_get_format(const struct format_info *info)
+{
+ const char *str, *val;
+ struct spa_json it[2];
+ int len;
+
+ if ((str = pw_properties_get(info->props, "format.sample_format")) == NULL)
+ return SPA_AUDIO_FORMAT_UNKNOWN;
+
+ spa_json_init(&it[0], str, strlen(str));
+ if ((len = spa_json_next(&it[0], &val)) <= 0)
+ return SPA_AUDIO_FORMAT_UNKNOWN;
+
+ if (spa_json_is_string(val, len))
+ return format_paname2id(val+1, len-2);
+
+ return SPA_AUDIO_FORMAT_UNKNOWN;
+}
+
+static int format_info_get_rate(const struct format_info *info)
+{
+ const char *str, *val;
+ struct spa_json it[2];
+ int len, v;
+
+ if ((str = pw_properties_get(info->props, "format.rate")) == NULL)
+ return -ENOENT;
+
+ spa_json_init(&it[0], str, strlen(str));
+ if ((len = spa_json_next(&it[0], &val)) <= 0)
+ return -EINVAL;
+ if (spa_json_is_int(val, len)) {
+ if (spa_json_parse_int(val, len, &v) <= 0)
+ return -EINVAL;
+ return v;
+ }
+ return -ENOTSUP;
+}
+
+int format_info_to_spec(const struct format_info *info, struct sample_spec *ss,
+ struct channel_map *map)
+{
+ const char *str, *val;
+ struct spa_json it[2];
+ float f;
+ int res, len;
+
+ spa_zero(*ss);
+ spa_zero(*map);
+
+ if (info->encoding != ENCODING_PCM)
+ return -ENOTSUP;
+ if (info->props == NULL)
+ return -ENOENT;
+
+ if ((ss->format = format_info_get_format(info)) == SPA_AUDIO_FORMAT_UNKNOWN)
+ return -ENOTSUP;
+
+ if ((res = format_info_get_rate(info)) < 0)
+ return res;
+ ss->rate = res;
+
+ if ((str = pw_properties_get(info->props, "format.channels")) == NULL)
+ return -ENOENT;
+
+ spa_json_init(&it[0], str, strlen(str));
+ if ((len = spa_json_next(&it[0], &val)) <= 0)
+ return -EINVAL;
+ if (spa_json_is_float(val, len)) {
+ if (spa_json_parse_float(val, len, &f) <= 0)
+ return -EINVAL;
+ ss->channels = f;
+ } else if (spa_json_is_array(val, len)) {
+ return -ENOTSUP;
+ } else if (spa_json_is_object(val, len)) {
+ return -ENOTSUP;
+ } else
+ return -ENOTSUP;
+
+ if ((str = pw_properties_get(info->props, "format.channel_map")) != NULL) {
+ spa_json_init(&it[0], str, strlen(str));
+ if ((len = spa_json_next(&it[0], &val)) <= 0)
+ return -EINVAL;
+ if (!spa_json_is_string(val, len))
+ return -EINVAL;
+ while ((*str == '\"' || *str == ',') &&
+ (len = strcspn(++str, "\",")) > 0) {
+ map->map[map->channels++] = channel_paname2id(str, len);
+ str += len;
+ }
+ }
+ return 0;
+}
+
+const struct spa_pod *format_info_build_param(struct spa_pod_builder *b, uint32_t id,
+ const struct format_info *info, uint32_t *rate)
+{
+ struct sample_spec ss;
+ struct channel_map map;
+ const struct spa_pod *param = NULL;
+ int res;
+
+ switch (info->encoding) {
+ case ENCODING_PCM:
+ if ((res = format_info_to_spec(info, &ss, &map)) < 0) {
+ errno = -res;
+ return NULL;
+ }
+ *rate = ss.rate;
+ param = format_build_param(b, id, &ss, &map);
+ break;
+ case ENCODING_AC3_IEC61937:
+ case ENCODING_EAC3_IEC61937:
+ case ENCODING_MPEG_IEC61937:
+ case ENCODING_DTS_IEC61937:
+ case ENCODING_MPEG2_AAC_IEC61937:
+ case ENCODING_TRUEHD_IEC61937:
+ case ENCODING_DTSHD_IEC61937:
+ {
+ struct spa_audio_info_iec958 i = { 0 };
+ i.codec = format_encoding2id(info->encoding);
+ if ((res = format_info_get_rate(info)) <= 0) {
+ errno = -res;
+ return NULL;
+ }
+ i.rate = res;
+ param = spa_format_audio_iec958_build(b, id, &i);
+ break;
+ }
+ default:
+ errno = ENOTSUP;
+ break;
+ }
+ return param;
+}
diff --git a/src/modules/module-protocol-pulse/format.h b/src/modules/module-protocol-pulse/format.h
new file mode 100644
index 0000000..4300fc0
--- /dev/null
+++ b/src/modules/module-protocol-pulse/format.h
@@ -0,0 +1,233 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PULSE_SERVER_FORMAT_H
+#define PULSE_SERVER_FORMAT_H
+
+#include <spa/utils/defs.h>
+#include <pipewire/properties.h>
+
+struct spa_pod;
+struct spa_pod_builder;
+
+#define RATE_MAX (48000u*8u)
+#define CHANNELS_MAX (64u)
+
+enum sample_format {
+ SAMPLE_U8,
+ SAMPLE_ALAW,
+ SAMPLE_ULAW,
+ SAMPLE_S16LE,
+ SAMPLE_S16BE,
+ SAMPLE_FLOAT32LE,
+ SAMPLE_FLOAT32BE,
+ SAMPLE_S32LE,
+ SAMPLE_S32BE,
+ SAMPLE_S24LE,
+ SAMPLE_S24BE,
+ SAMPLE_S24_32LE,
+ SAMPLE_S24_32BE,
+ SAMPLE_MAX,
+ SAMPLE_INVALID = -1
+};
+
+#if __BYTE_ORDER == __BIG_ENDIAN
+#define SAMPLE_S16NE SAMPLE_S16BE
+#define SAMPLE_FLOAT32NE SAMPLE_FLOAT32BE
+#define SAMPLE_S32NE SAMPLE_S32BE
+#define SAMPLE_S24NE SAMPLE_S24BE
+#define SAMPLE_S24_32NE SAMPLE_S24_32BE
+#elif __BYTE_ORDER == __LITTLE_ENDIAN
+#define SAMPLE_S16NE SAMPLE_S16LE
+#define SAMPLE_FLOAT32NE SAMPLE_FLOAT32LE
+#define SAMPLE_S32NE SAMPLE_S32LE
+#define SAMPLE_S24NE SAMPLE_S24LE
+#define SAMPLE_S24_32NE SAMPLE_S24_32LE
+#endif
+
+struct format {
+ uint32_t pa;
+ uint32_t id;
+ const char *name;
+ uint32_t size;
+};
+
+struct sample_spec {
+ uint32_t format;
+ uint32_t rate;
+ uint8_t channels;
+};
+#define SAMPLE_SPEC_INIT \
+ (struct sample_spec) { \
+ .format = SPA_AUDIO_FORMAT_UNKNOWN, \
+ .rate = 0, \
+ .channels = 0, \
+ }
+
+enum channel_position {
+ CHANNEL_POSITION_INVALID = -1,
+ CHANNEL_POSITION_MONO = 0,
+ CHANNEL_POSITION_FRONT_LEFT,
+ CHANNEL_POSITION_FRONT_RIGHT,
+ CHANNEL_POSITION_FRONT_CENTER,
+
+ CHANNEL_POSITION_REAR_CENTER,
+ CHANNEL_POSITION_REAR_LEFT,
+ CHANNEL_POSITION_REAR_RIGHT,
+
+ CHANNEL_POSITION_LFE,
+ CHANNEL_POSITION_FRONT_LEFT_OF_CENTER,
+ CHANNEL_POSITION_FRONT_RIGHT_OF_CENTER,
+
+ CHANNEL_POSITION_SIDE_LEFT,
+ CHANNEL_POSITION_SIDE_RIGHT,
+ CHANNEL_POSITION_AUX0,
+ CHANNEL_POSITION_AUX1,
+ CHANNEL_POSITION_AUX2,
+ CHANNEL_POSITION_AUX3,
+ CHANNEL_POSITION_AUX4,
+ CHANNEL_POSITION_AUX5,
+ CHANNEL_POSITION_AUX6,
+ CHANNEL_POSITION_AUX7,
+ CHANNEL_POSITION_AUX8,
+ CHANNEL_POSITION_AUX9,
+ CHANNEL_POSITION_AUX10,
+ CHANNEL_POSITION_AUX11,
+ CHANNEL_POSITION_AUX12,
+ CHANNEL_POSITION_AUX13,
+ CHANNEL_POSITION_AUX14,
+ CHANNEL_POSITION_AUX15,
+ CHANNEL_POSITION_AUX16,
+ CHANNEL_POSITION_AUX17,
+ CHANNEL_POSITION_AUX18,
+ CHANNEL_POSITION_AUX19,
+ CHANNEL_POSITION_AUX20,
+ CHANNEL_POSITION_AUX21,
+ CHANNEL_POSITION_AUX22,
+ CHANNEL_POSITION_AUX23,
+ CHANNEL_POSITION_AUX24,
+ CHANNEL_POSITION_AUX25,
+ CHANNEL_POSITION_AUX26,
+ CHANNEL_POSITION_AUX27,
+ CHANNEL_POSITION_AUX28,
+ CHANNEL_POSITION_AUX29,
+ CHANNEL_POSITION_AUX30,
+ CHANNEL_POSITION_AUX31,
+
+ CHANNEL_POSITION_TOP_CENTER,
+
+ CHANNEL_POSITION_TOP_FRONT_LEFT,
+ CHANNEL_POSITION_TOP_FRONT_RIGHT,
+ CHANNEL_POSITION_TOP_FRONT_CENTER,
+
+ CHANNEL_POSITION_TOP_REAR_LEFT,
+ CHANNEL_POSITION_TOP_REAR_RIGHT,
+ CHANNEL_POSITION_TOP_REAR_CENTER,
+
+ CHANNEL_POSITION_MAX
+};
+
+struct channel {
+ uint32_t channel;
+ const char *name;
+};
+
+struct channel_map {
+ uint8_t channels;
+ uint32_t map[CHANNELS_MAX];
+};
+
+#define CHANNEL_MAP_INIT \
+ (struct channel_map) { \
+ .channels = 0, \
+ }
+
+enum encoding {
+ ENCODING_ANY,
+ ENCODING_PCM,
+ ENCODING_AC3_IEC61937,
+ ENCODING_EAC3_IEC61937,
+ ENCODING_MPEG_IEC61937,
+ ENCODING_DTS_IEC61937,
+ ENCODING_MPEG2_AAC_IEC61937,
+ ENCODING_TRUEHD_IEC61937,
+ ENCODING_DTSHD_IEC61937,
+ ENCODING_MAX,
+ ENCODING_INVALID = -1,
+};
+
+struct format_info {
+ enum encoding encoding;
+ struct pw_properties *props;
+};
+
+uint32_t format_pa2id(enum sample_format format);
+const char *format_id2name(uint32_t format);
+uint32_t format_name2id(const char *name);
+uint32_t format_paname2id(const char *name, size_t size);
+enum sample_format format_id2pa(uint32_t id);
+const char *format_id2paname(uint32_t id);
+const char *format_encoding2name(enum encoding enc);
+uint32_t format_encoding2id(enum encoding enc);
+
+uint32_t sample_spec_frame_size(const struct sample_spec *ss);
+bool sample_spec_valid(const struct sample_spec *ss);
+
+uint32_t channel_pa2id(enum channel_position channel);
+const char *channel_id2name(uint32_t channel);
+uint32_t channel_name2id(const char *name);
+enum channel_position channel_id2pa(uint32_t id, uint32_t *aux);
+const char *channel_id2paname(uint32_t id, uint32_t *aux);
+uint32_t channel_paname2id(const char *name, size_t size);
+
+void channel_map_to_positions(const struct channel_map *map, uint32_t *pos);
+void channel_map_parse(const char *str, struct channel_map *map);
+bool channel_map_valid(const struct channel_map *map);
+
+int format_parse_param(const struct spa_pod *param, bool collect, struct sample_spec *ss,
+ struct channel_map *map, const struct sample_spec *def_ss,
+ const struct channel_map *def_map);
+
+const struct spa_pod *format_build_param(struct spa_pod_builder *b, uint32_t id,
+ const struct sample_spec *spec, const struct channel_map *map);
+
+int format_info_from_spec(struct format_info *info, const struct sample_spec *ss,
+ const struct channel_map *map);
+int format_info_from_param(struct format_info *info, struct spa_pod *param, uint32_t index);
+
+const struct spa_pod *format_info_build_param(struct spa_pod_builder *b, uint32_t id,
+ const struct format_info *info, uint32_t *rate);
+
+int format_info_from_spec(struct format_info *info, const struct sample_spec *ss,
+ const struct channel_map *map);
+int format_info_to_spec(const struct format_info *info, struct sample_spec *ss,
+ struct channel_map *map);
+
+static inline void format_info_clear(struct format_info *info)
+{
+ pw_properties_free(info->props);
+ spa_zero(*info);
+}
+
+#endif
diff --git a/src/modules/module-protocol-pulse/internal.h b/src/modules/module-protocol-pulse/internal.h
new file mode 100644
index 0000000..1d5731c
--- /dev/null
+++ b/src/modules/module-protocol-pulse/internal.h
@@ -0,0 +1,108 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PULSE_SERVER_INTERNAL_H
+#define PULSE_SERVER_INTERNAL_H
+
+#include "config.h"
+
+#include <stdbool.h>
+#include <stdint.h>
+
+#include <spa/utils/defs.h>
+#include <spa/utils/ringbuffer.h>
+#include <pipewire/map.h>
+#include <pipewire/private.h>
+
+#include "format.h"
+#include "server.h"
+
+struct pw_loop;
+struct pw_context;
+struct pw_work_queue;
+struct pw_properties;
+
+struct defs {
+ struct spa_fraction min_req;
+ struct spa_fraction default_req;
+ struct spa_fraction min_frag;
+ struct spa_fraction default_frag;
+ struct spa_fraction default_tlength;
+ struct spa_fraction min_quantum;
+ struct sample_spec sample_spec;
+ struct channel_map channel_map;
+ uint32_t quantum_limit;
+ uint32_t idle_timeout;
+};
+
+struct stats {
+ uint32_t n_allocated;
+ uint32_t allocated;
+ uint32_t n_accumulated;
+ uint32_t accumulated;
+ uint32_t sample_cache;
+};
+
+struct impl {
+ struct pw_loop *loop;
+ struct pw_context *context;
+ struct spa_hook context_listener;
+
+ struct pw_properties *props;
+ void *dbus_name;
+
+ struct ratelimit rate_limit;
+
+ struct spa_hook_list hooks;
+ struct spa_list servers;
+
+ struct pw_work_queue *work_queue;
+ struct spa_list cleanup_clients;
+
+ struct pw_map samples;
+ struct pw_map modules;
+
+ struct spa_list free_messages;
+ struct defs defs;
+ struct stats stat;
+};
+
+struct impl_events {
+#define VERSION_IMPL_EVENTS 0
+ uint32_t version;
+
+ void (*server_started) (void *data, struct server *server);
+
+ void (*server_stopped) (void *data, struct server *server);
+};
+
+void impl_add_listener(struct impl *impl,
+ struct spa_hook *listener,
+ const struct impl_events *events, void *data);
+
+extern bool debug_messages;
+
+void broadcast_subscribe_event(struct impl *impl, uint32_t mask, uint32_t event, uint32_t id);
+
+#endif
diff --git a/src/modules/module-protocol-pulse/log.h b/src/modules/module-protocol-pulse/log.h
new file mode 100644
index 0000000..e1f5ca7
--- /dev/null
+++ b/src/modules/module-protocol-pulse/log.h
@@ -0,0 +1,34 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Red Hat, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+
+#ifndef PULSE_LOG_H
+#define PULSE_LOG_H
+
+#include <pipewire/log.h>
+
+PW_LOG_TOPIC_EXTERN(mod_topic);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+#endif /* PULSE_LOG_H */
diff --git a/src/modules/module-protocol-pulse/manager.c b/src/modules/module-protocol-pulse/manager.c
new file mode 100644
index 0000000..7aa8d70
--- /dev/null
+++ b/src/modules/module-protocol-pulse/manager.c
@@ -0,0 +1,1028 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "manager.h"
+
+#include <spa/pod/iter.h>
+#include <spa/pod/parser.h>
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+#include <pipewire/extensions/metadata.h>
+
+#include "log.h"
+#include "module-protocol-pulse/server.h"
+
+#define manager_emit_sync(m) spa_hook_list_call(&(m)->hooks, struct pw_manager_events, sync, 0)
+#define manager_emit_added(m,o) spa_hook_list_call(&(m)->hooks, struct pw_manager_events, added, 0, o)
+#define manager_emit_updated(m,o) spa_hook_list_call(&(m)->hooks, struct pw_manager_events, updated, 0, o)
+#define manager_emit_removed(m,o) spa_hook_list_call(&(m)->hooks, struct pw_manager_events, removed, 0, o)
+#define manager_emit_metadata(m,o,s,k,t,v) spa_hook_list_call(&(m)->hooks, struct pw_manager_events, metadata,0,o,s,k,t,v)
+#define manager_emit_disconnect(m) spa_hook_list_call(&(m)->hooks, struct pw_manager_events, disconnect, 0)
+#define manager_emit_object_data_timeout(m,o,k) spa_hook_list_call(&(m)->hooks, struct pw_manager_events, object_data_timeout,0,o,k)
+
+struct object;
+
+struct manager {
+ struct pw_manager this;
+
+ struct pw_loop *loop;
+
+ struct spa_hook core_listener;
+ struct spa_hook registry_listener;
+ int sync_seq;
+
+ struct spa_hook_list hooks;
+};
+
+struct object_info {
+ const char *type;
+ uint32_t version;
+ const void *events;
+ void (*init) (struct object *object);
+ void (*destroy) (struct object *object);
+};
+
+struct object_data {
+ struct spa_list link;
+ struct object *object;
+ const char *key;
+ size_t size;
+ struct spa_source *timer;
+};
+
+struct object {
+ struct pw_manager_object this;
+
+ struct manager *manager;
+
+ const struct object_info *info;
+
+ struct spa_list pending_list;
+
+ struct spa_hook proxy_listener;
+ struct spa_hook object_listener;
+
+ struct spa_list data_list;
+};
+
+static int core_sync(struct manager *m)
+{
+ m->sync_seq = pw_core_sync(m->this.core, PW_ID_CORE, m->sync_seq);
+ pw_log_debug("sync start %u", m->sync_seq);
+ return m->sync_seq;
+}
+
+static uint32_t clear_params(struct spa_list *param_list, uint32_t id)
+{
+ struct pw_manager_param *p, *t;
+ uint32_t count = 0;
+
+ spa_list_for_each_safe(p, t, param_list, link) {
+ if (id == SPA_ID_INVALID || p->id == id) {
+ spa_list_remove(&p->link);
+ free(p);
+ count++;
+ }
+ }
+ return count;
+}
+
+static struct pw_manager_param *add_param(struct spa_list *params,
+ int seq, uint32_t id, const struct spa_pod *param)
+{
+ struct pw_manager_param *p;
+
+ if (id == SPA_ID_INVALID) {
+ if (param == NULL || !spa_pod_is_object(param)) {
+ errno = EINVAL;
+ return NULL;
+ }
+ id = SPA_POD_OBJECT_ID(param);
+ }
+
+ p = malloc(sizeof(*p) + (param != NULL ? SPA_POD_SIZE(param) : 0));
+ if (p == NULL)
+ return NULL;
+
+ p->id = id;
+ p->seq = seq;
+ if (param != NULL) {
+ p->param = SPA_PTROFF(p, sizeof(*p), struct spa_pod);
+ memcpy(p->param, param, SPA_POD_SIZE(param));
+ } else {
+ clear_params(params, id);
+ p->param = NULL;
+ }
+ spa_list_append(params, &p->link);
+
+ return p;
+}
+
+static bool has_param(struct spa_list *param_list, struct pw_manager_param *p)
+{
+ struct pw_manager_param *t;
+ spa_list_for_each(t, param_list, link) {
+ if (p->id == t->id &&
+ SPA_POD_SIZE(p->param) == SPA_POD_SIZE(t->param) &&
+ memcmp(p->param, t->param, SPA_POD_SIZE(p->param)) == 0)
+ return true;
+ }
+ return false;
+}
+
+static struct object *find_object_by_id(struct manager *m, uint32_t id)
+{
+ struct object *o;
+ spa_list_for_each(o, &m->this.object_list, this.link) {
+ if (o->this.id == id)
+ return o;
+ }
+ return NULL;
+}
+
+static void object_update_params(struct object *o)
+{
+ struct pw_manager_param *p, *t;
+ uint32_t i;
+
+ for (i = 0; i < o->this.n_params; i++) {
+ spa_list_for_each_safe(p, t, &o->pending_list, link) {
+ if (p->id == o->this.params[i].id &&
+ p->seq != o->this.params[i].seq &&
+ p->param != NULL) {
+ spa_list_remove(&p->link);
+ free(p);
+ }
+ }
+ }
+
+ spa_list_consume(p, &o->pending_list, link) {
+ spa_list_remove(&p->link);
+ if (p->param == NULL) {
+ clear_params(&o->this.param_list, p->id);
+ free(p);
+ } else {
+ spa_list_append(&o->this.param_list, &p->link);
+ }
+ }
+}
+
+static void object_data_free(struct object_data *d)
+{
+ spa_list_remove(&d->link);
+ if (d->timer) {
+ pw_loop_destroy_source(d->object->manager->loop, d->timer);
+ d->timer = NULL;
+ }
+ free(d);
+}
+
+static void object_destroy(struct object *o)
+{
+ struct manager *m = o->manager;
+ struct object_data *d;
+ spa_list_remove(&o->this.link);
+ m->this.n_objects--;
+ if (o->this.proxy)
+ pw_proxy_destroy(o->this.proxy);
+ pw_properties_free(o->this.props);
+ if (o->this.message_object_path)
+ free(o->this.message_object_path);
+ clear_params(&o->this.param_list, SPA_ID_INVALID);
+ clear_params(&o->pending_list, SPA_ID_INVALID);
+ spa_list_consume(d, &o->data_list, link)
+ object_data_free(d);
+ free(o);
+}
+
+/* core */
+static const struct object_info core_info = {
+ .type = PW_TYPE_INTERFACE_Core,
+ .version = PW_VERSION_CORE,
+};
+
+/* client */
+static void client_event_info(void *data, const struct pw_client_info *info)
+{
+ struct object *o = data;
+ int changed = 0;
+
+ pw_log_debug("object %p: id:%d change-mask:%08"PRIx64, o, o->this.id, info->change_mask);
+
+ info = o->this.info = pw_client_info_merge(o->this.info, info, o->this.changed == 0);
+ if (info == NULL)
+ return;
+
+ if (info->change_mask & PW_CLIENT_CHANGE_MASK_PROPS)
+ changed++;
+
+ if (changed) {
+ o->this.changed += changed;
+ core_sync(o->manager);
+ }
+}
+
+static const struct pw_client_events client_events = {
+ PW_VERSION_CLIENT_EVENTS,
+ .info = client_event_info,
+};
+
+static void client_destroy(struct object *o)
+{
+ if (o->this.info) {
+ pw_client_info_free(o->this.info);
+ o->this.info = NULL;
+ }
+}
+
+static const struct object_info client_info = {
+ .type = PW_TYPE_INTERFACE_Client,
+ .version = PW_VERSION_CLIENT,
+ .events = &client_events,
+ .destroy = client_destroy,
+};
+
+/* module */
+static void module_event_info(void *data, const struct pw_module_info *info)
+{
+ struct object *o = data;
+ int changed = 0;
+
+ pw_log_debug("object %p: id:%d change-mask:%08"PRIx64, o, o->this.id, info->change_mask);
+
+ info = o->this.info = pw_module_info_merge(o->this.info, info, o->this.changed == 0);
+ if (info == NULL)
+ return;
+
+ if (info->change_mask & PW_MODULE_CHANGE_MASK_PROPS)
+ changed++;
+
+ if (changed) {
+ o->this.changed += changed;
+ core_sync(o->manager);
+ }
+}
+
+static const struct pw_module_events module_events = {
+ PW_VERSION_MODULE_EVENTS,
+ .info = module_event_info,
+};
+
+static void module_destroy(struct object *o)
+{
+ if (o->this.info) {
+ pw_module_info_free(o->this.info);
+ o->this.info = NULL;
+ }
+}
+
+static const struct object_info module_info = {
+ .type = PW_TYPE_INTERFACE_Module,
+ .version = PW_VERSION_MODULE,
+ .events = &module_events,
+ .destroy = module_destroy,
+};
+
+/* device */
+static void device_event_info(void *data, const struct pw_device_info *info)
+{
+ struct object *o = data;
+ uint32_t i, changed = 0;
+
+ pw_log_debug("object %p: id:%d change-mask:%08"PRIx64, o, o->this.id, info->change_mask);
+
+ info = o->this.info = pw_device_info_merge(o->this.info, info, o->this.changed == 0);
+ if (info == NULL)
+ return;
+
+ o->this.n_params = info->n_params;
+ o->this.params = info->params;
+
+ if (info->change_mask & PW_DEVICE_CHANGE_MASK_PROPS)
+ changed++;
+
+ if (info->change_mask & PW_DEVICE_CHANGE_MASK_PARAMS) {
+ for (i = 0; i < info->n_params; i++) {
+ uint32_t id = info->params[i].id;
+ int res;
+
+ if (info->params[i].user == 0)
+ continue;
+ info->params[i].user = 0;
+
+ switch (id) {
+ case SPA_PARAM_EnumProfile:
+ case SPA_PARAM_Profile:
+ case SPA_PARAM_EnumRoute:
+ changed++;
+ break;
+ case SPA_PARAM_Route:
+ break;
+ }
+ add_param(&o->pending_list, info->params[i].seq, id, NULL);
+ if (!(info->params[i].flags & SPA_PARAM_INFO_READ))
+ continue;
+
+ res = pw_device_enum_params((struct pw_device*)o->this.proxy,
+ ++info->params[i].seq, id, 0, -1, NULL);
+ if (SPA_RESULT_IS_ASYNC(res))
+ info->params[i].seq = res;
+ }
+ }
+ if (changed) {
+ o->this.changed += changed;
+ core_sync(o->manager);
+ }
+}
+static struct object *find_device(struct manager *m, uint32_t card_id, uint32_t device)
+{
+ struct object *o;
+
+ spa_list_for_each(o, &m->this.object_list, this.link) {
+ struct pw_node_info *info;
+ const char *str;
+
+ if (!spa_streq(o->this.type, PW_TYPE_INTERFACE_Node))
+ continue;
+
+ if ((info = o->this.info) != NULL &&
+ (str = spa_dict_lookup(info->props, PW_KEY_DEVICE_ID)) != NULL &&
+ (uint32_t)atoi(str) == card_id &&
+ (str = spa_dict_lookup(info->props, "card.profile.device")) != NULL &&
+ (uint32_t)atoi(str) == device)
+ return o;
+ }
+ return NULL;
+}
+
+static void device_event_param(void *data, int seq,
+ uint32_t id, uint32_t index, uint32_t next,
+ const struct spa_pod *param)
+{
+ struct object *o = data, *dev;
+ struct manager *m = o->manager;
+ struct pw_manager_param *p;
+
+ p = add_param(&o->pending_list, seq, id, param);
+ if (p == NULL)
+ return;
+
+ if (id == SPA_PARAM_Route && !has_param(&o->this.param_list, p)) {
+ uint32_t idx, device;
+ if (spa_pod_parse_object(param,
+ SPA_TYPE_OBJECT_ParamRoute, NULL,
+ SPA_PARAM_ROUTE_index, SPA_POD_Int(&idx),
+ SPA_PARAM_ROUTE_device, SPA_POD_Int(&device)) < 0)
+ return;
+
+ if ((dev = find_device(m, o->this.id, device)) != NULL) {
+ dev->this.changed++;
+ core_sync(o->manager);
+ }
+ }
+}
+
+static const struct pw_device_events device_events = {
+ PW_VERSION_DEVICE_EVENTS,
+ .info = device_event_info,
+ .param = device_event_param,
+};
+
+static void device_destroy(struct object *o)
+{
+ if (o->this.info) {
+ pw_device_info_free(o->this.info);
+ o->this.info = NULL;
+ }
+}
+
+static const struct object_info device_info = {
+ .type = PW_TYPE_INTERFACE_Device,
+ .version = PW_VERSION_DEVICE,
+ .events = &device_events,
+ .destroy = device_destroy,
+};
+
+/* node */
+static void node_event_info(void *data, const struct pw_node_info *info)
+{
+ struct object *o = data;
+ uint32_t i, changed = 0;
+
+ pw_log_debug("object %p: id:%d change-mask:%08"PRIx64, o, o->this.id, info->change_mask);
+
+ info = o->this.info = pw_node_info_merge(o->this.info, info, o->this.changed == 0);
+ if (info == NULL)
+ return;
+
+ o->this.n_params = info->n_params;
+ o->this.params = info->params;
+
+ if (info->change_mask & PW_NODE_CHANGE_MASK_STATE)
+ changed++;
+
+ if (info->change_mask & PW_NODE_CHANGE_MASK_PROPS)
+ changed++;
+
+ if (info->change_mask & PW_NODE_CHANGE_MASK_PARAMS) {
+ for (i = 0; i < info->n_params; i++) {
+ uint32_t id = info->params[i].id;
+ int res;
+
+ if (info->params[i].user == 0)
+ continue;
+ info->params[i].user = 0;
+
+ changed++;
+ add_param(&o->pending_list, info->params[i].seq, id, NULL);
+ if (!(info->params[i].flags & SPA_PARAM_INFO_READ))
+ continue;
+
+ res = pw_node_enum_params((struct pw_node*)o->this.proxy,
+ ++info->params[i].seq, id, 0, -1, NULL);
+ if (SPA_RESULT_IS_ASYNC(res))
+ info->params[i].seq = res;
+ }
+ }
+ if (changed) {
+ o->this.changed += changed;
+ core_sync(o->manager);
+ }
+}
+
+static void node_event_param(void *data, int seq,
+ uint32_t id, uint32_t index, uint32_t next,
+ const struct spa_pod *param)
+{
+ struct object *o = data;
+ add_param(&o->pending_list, seq, id, param);
+}
+
+static const struct pw_node_events node_events = {
+ PW_VERSION_NODE_EVENTS,
+ .info = node_event_info,
+ .param = node_event_param,
+};
+
+static void node_destroy(struct object *o)
+{
+ if (o->this.info) {
+ pw_node_info_free(o->this.info);
+ o->this.info = NULL;
+ }
+}
+
+static const struct object_info node_info = {
+ .type = PW_TYPE_INTERFACE_Node,
+ .version = PW_VERSION_NODE,
+ .events = &node_events,
+ .destroy = node_destroy,
+};
+
+/* link */
+static const struct object_info link_info = {
+ .type = PW_TYPE_INTERFACE_Link,
+ .version = PW_VERSION_LINK,
+};
+
+/* metadata */
+static int metadata_property(void *data,
+ uint32_t subject,
+ const char *key,
+ const char *type,
+ const char *value)
+{
+ struct object *o = data;
+ struct manager *m = o->manager;
+ manager_emit_metadata(m, &o->this, subject, key, type, value);
+ return 0;
+}
+
+static const struct pw_metadata_events metadata_events = {
+ PW_VERSION_METADATA_EVENTS,
+ .property = metadata_property,
+};
+
+static void metadata_init(struct object *object)
+{
+ struct object *o = object;
+ struct manager *m = o->manager;
+ o->this.creating = false;
+ manager_emit_added(m, &o->this);
+}
+
+static const struct object_info metadata_info = {
+ .type = PW_TYPE_INTERFACE_Metadata,
+ .version = PW_VERSION_METADATA,
+ .events = &metadata_events,
+ .init = metadata_init,
+};
+
+static const struct object_info *objects[] =
+{
+ &core_info,
+ &module_info,
+ &client_info,
+ &device_info,
+ &node_info,
+ &link_info,
+ &metadata_info,
+};
+
+static const struct object_info *find_info(const char *type, uint32_t version)
+{
+ SPA_FOR_EACH_ELEMENT_VAR(objects, i) {
+ if (spa_streq((*i)->type, type) &&
+ (*i)->version <= version)
+ return *i;
+ }
+ return NULL;
+}
+
+static void
+destroy_removed(void *data)
+{
+ struct object *o = data;
+ pw_proxy_destroy(o->this.proxy);
+}
+
+static void
+destroy_proxy(void *data)
+{
+ struct object *o = data;
+
+ spa_assert(o->info);
+
+ if (o->info->events)
+ spa_hook_remove(&o->object_listener);
+ spa_hook_remove(&o->proxy_listener);
+
+ if (o->info->destroy)
+ o->info->destroy(o);
+
+ o->this.proxy = NULL;
+}
+
+static const struct pw_proxy_events proxy_events = {
+ PW_VERSION_PROXY_EVENTS,
+ .removed = destroy_removed,
+ .destroy = destroy_proxy,
+};
+
+static void registry_event_global(void *data, uint32_t id,
+ uint32_t permissions, const char *type, uint32_t version,
+ const struct spa_dict *props)
+{
+ struct manager *m = data;
+ struct object *o;
+ const struct object_info *info;
+ const char *str;
+ struct pw_proxy *proxy;
+
+ info = find_info(type, version);
+ if (info == NULL)
+ return;
+
+ proxy = pw_registry_bind(m->this.registry,
+ id, type, info->version, 0);
+ if (proxy == NULL)
+ return;
+
+ o = calloc(1, sizeof(*o));
+ if (o == NULL) {
+ pw_log_error("can't alloc object for %u %s/%d: %m", id, type, version);
+ pw_proxy_destroy(proxy);
+ return;
+ }
+ str = props ? spa_dict_lookup(props, PW_KEY_OBJECT_SERIAL) : NULL;
+ if (!spa_atou64(str, &o->this.serial, 0))
+ o->this.serial = SPA_ID_INVALID;
+
+ o->this.id = id;
+ o->this.permissions = permissions;
+ o->this.type = info->type;
+ o->this.version = version;
+ o->this.index = o->this.serial < (1ULL<<32) ? o->this.serial : SPA_ID_INVALID;
+ o->this.props = props ? pw_properties_new_dict(props) : NULL;
+ o->this.proxy = proxy;
+ o->this.creating = true;
+ spa_list_init(&o->this.param_list);
+ spa_list_init(&o->pending_list);
+ spa_list_init(&o->data_list);
+
+ o->manager = m;
+ o->info = info;
+ spa_list_append(&m->this.object_list, &o->this.link);
+ m->this.n_objects++;
+
+ if (info->events)
+ pw_proxy_add_object_listener(proxy,
+ &o->object_listener,
+ o->info->events, o);
+ pw_proxy_add_listener(proxy,
+ &o->proxy_listener,
+ &proxy_events, o);
+
+ if (info->init)
+ info->init(o);
+
+ core_sync(m);
+}
+
+static void registry_event_global_remove(void *data, uint32_t id)
+{
+ struct manager *m = data;
+ struct object *o;
+
+ if ((o = find_object_by_id(m, id)) == NULL)
+ return;
+
+ o->this.removing = true;
+
+ if (!o->this.creating)
+ manager_emit_removed(m, &o->this);
+
+ object_destroy(o);
+}
+
+static const struct pw_registry_events registry_events = {
+ PW_VERSION_REGISTRY_EVENTS,
+ .global = registry_event_global,
+ .global_remove = registry_event_global_remove,
+};
+
+static void on_core_info(void *data, const struct pw_core_info *info)
+{
+ struct manager *m = data;
+ m->this.info = pw_core_info_merge(m->this.info, info, true);
+}
+
+static void on_core_done(void *data, uint32_t id, int seq)
+{
+ struct manager *m = data;
+ struct object *o;
+
+ if (id == PW_ID_CORE) {
+ if (m->sync_seq != seq)
+ return;
+
+ pw_log_debug("sync end %u/%u", m->sync_seq, seq);
+
+ manager_emit_sync(m);
+
+ spa_list_for_each(o, &m->this.object_list, this.link)
+ object_update_params(o);
+
+ spa_list_for_each(o, &m->this.object_list, this.link) {
+ if (o->this.creating) {
+ o->this.creating = false;
+ manager_emit_added(m, &o->this);
+ o->this.changed = 0;
+ } else if (o->this.changed > 0) {
+ manager_emit_updated(m, &o->this);
+ o->this.changed = 0;
+ }
+ }
+ }
+}
+
+static void on_core_error(void *data, uint32_t id, int seq, int res, const char *message)
+{
+ struct manager *m = data;
+
+ if (id == PW_ID_CORE && res == -EPIPE) {
+ pw_log_debug("connection error: %d, %s", res, message);
+ manager_emit_disconnect(m);
+ }
+}
+
+static const struct pw_core_events core_events = {
+ PW_VERSION_CORE_EVENTS,
+ .done = on_core_done,
+ .info = on_core_info,
+ .error = on_core_error
+};
+
+struct pw_manager *pw_manager_new(struct pw_core *core)
+{
+ struct manager *m;
+ struct pw_context *context;
+
+ m = calloc(1, sizeof(*m));
+ if (m == NULL)
+ return NULL;
+
+ m->this.core = core;
+ m->this.registry = pw_core_get_registry(m->this.core,
+ PW_VERSION_REGISTRY, 0);
+ if (m->this.registry == NULL) {
+ free(m);
+ return NULL;
+ }
+
+ context = pw_core_get_context(core);
+ m->loop = pw_context_get_main_loop(context);
+
+ spa_hook_list_init(&m->hooks);
+
+ spa_list_init(&m->this.object_list);
+
+ pw_core_add_listener(m->this.core,
+ &m->core_listener,
+ &core_events, m);
+ pw_registry_add_listener(m->this.registry,
+ &m->registry_listener,
+ &registry_events, m);
+
+ return &m->this;
+}
+
+void pw_manager_add_listener(struct pw_manager *manager,
+ struct spa_hook *listener,
+ const struct pw_manager_events *events, void *data)
+{
+ struct manager *m = SPA_CONTAINER_OF(manager, struct manager, this);
+ spa_hook_list_append(&m->hooks, listener, events, data);
+ core_sync(m);
+}
+
+int pw_manager_set_metadata(struct pw_manager *manager,
+ struct pw_manager_object *metadata,
+ uint32_t subject, const char *key, const char *type,
+ const char *format, ...)
+{
+ struct manager *m = SPA_CONTAINER_OF(manager, struct manager, this);
+ struct object *s;
+ va_list args;
+ char buf[1024];
+ char *value;
+
+ if ((s = find_object_by_id(m, subject)) == NULL)
+ return -ENOENT;
+ if (!SPA_FLAG_IS_SET(s->this.permissions, PW_PERM_M))
+ return -EACCES;
+
+ if (metadata == NULL)
+ return -ENOTSUP;
+ if (!SPA_FLAG_IS_SET(metadata->permissions, PW_PERM_W|PW_PERM_X))
+ return -EACCES;
+ if (metadata->proxy == NULL)
+ return -ENOENT;
+
+ if (type != NULL) {
+ va_start(args, format);
+ vsnprintf(buf, sizeof(buf), format, args);
+ va_end(args);
+ value = buf;
+ } else {
+ spa_assert(format == NULL);
+ value = NULL;
+ }
+
+ pw_metadata_set_property(metadata->proxy,
+ subject, key, type, value);
+ return 0;
+}
+
+int pw_manager_for_each_object(struct pw_manager *manager,
+ int (*callback) (void *data, struct pw_manager_object *object),
+ void *data)
+{
+ struct manager *m = SPA_CONTAINER_OF(manager, struct manager, this);
+ struct object *o;
+ int res;
+
+ spa_list_for_each(o, &m->this.object_list, this.link) {
+ if (o->this.creating || o->this.removing)
+ continue;
+ if ((res = callback(data, &o->this)) != 0)
+ return res;
+ }
+ return 0;
+}
+
+void pw_manager_destroy(struct pw_manager *manager)
+{
+ struct manager *m = SPA_CONTAINER_OF(manager, struct manager, this);
+ struct object *o;
+
+ spa_hook_list_clean(&m->hooks);
+
+ spa_hook_remove(&m->core_listener);
+
+ spa_list_consume(o, &m->this.object_list, this.link)
+ object_destroy(o);
+
+ spa_hook_remove(&m->registry_listener);
+ pw_proxy_destroy((struct pw_proxy*)m->this.registry);
+
+ if (m->this.info)
+ pw_core_info_free(m->this.info);
+
+ free(m);
+}
+
+static struct object_data *object_find_data(struct object *o, const char *key)
+{
+ struct object_data *d;
+ spa_list_for_each(d, &o->data_list, link) {
+ if (spa_streq(d->key, key))
+ return d;
+ }
+ return NULL;
+}
+
+void *pw_manager_object_add_data(struct pw_manager_object *obj, const char *key, size_t size)
+{
+ struct object *o = SPA_CONTAINER_OF(obj, struct object, this);
+ struct object_data *d;
+
+ d = object_find_data(o, key);
+ if (d != NULL) {
+ if (d->size == size)
+ goto done;
+ object_data_free(d);
+ }
+
+ d = calloc(1, sizeof(struct object_data) + size);
+ if (d == NULL)
+ return NULL;
+
+ d->object = o;
+ d->key = key;
+ d->size = size;
+
+ spa_list_append(&o->data_list, &d->link);
+
+done:
+ return SPA_PTROFF(d, sizeof(struct object_data), void);
+}
+
+static void object_data_timeout(void *data, uint64_t count)
+{
+ struct object_data *d = data;
+ struct object *o = d->object;
+ struct manager *m = o->manager;
+
+ pw_log_debug("manager:%p object id:%d data '%s' lifetime ends",
+ m, o->this.id, d->key);
+
+ if (d->timer) {
+ pw_loop_destroy_source(m->loop, d->timer);
+ d->timer = NULL;
+ }
+
+ manager_emit_object_data_timeout(m, &o->this, d->key);
+}
+
+void *pw_manager_object_add_temporary_data(struct pw_manager_object *obj, const char *key,
+ size_t size, uint64_t lifetime_nsec)
+{
+ struct object *o = SPA_CONTAINER_OF(obj, struct object, this);
+ struct object_data *d;
+ void *data;
+ struct timespec timeout = {0}, interval = {0};
+
+ data = pw_manager_object_add_data(obj, key, size);
+ if (data == NULL)
+ return NULL;
+
+ d = SPA_PTROFF(data, -sizeof(struct object_data), void);
+
+ if (d->timer == NULL)
+ d->timer = pw_loop_add_timer(o->manager->loop, object_data_timeout, d);
+ if (d->timer == NULL)
+ return NULL;
+
+ timeout.tv_sec = lifetime_nsec / SPA_NSEC_PER_SEC;
+ timeout.tv_nsec = lifetime_nsec % SPA_NSEC_PER_SEC;
+ pw_loop_update_timer(o->manager->loop, d->timer, &timeout, &interval, false);
+
+ return data;
+}
+
+void *pw_manager_object_get_data(struct pw_manager_object *obj, const char *id)
+{
+ struct object *o = SPA_CONTAINER_OF(obj, struct object, this);
+ struct object_data *d = object_find_data(o, id);
+
+ return d ? SPA_PTROFF(d, sizeof(*d), void) : NULL;
+}
+
+int pw_manager_sync(struct pw_manager *manager)
+{
+ struct manager *m = SPA_CONTAINER_OF(manager, struct manager, this);
+ return core_sync(m);
+}
+
+bool pw_manager_object_is_client(struct pw_manager_object *o)
+{
+ return spa_streq(o->type, PW_TYPE_INTERFACE_Client);
+}
+
+bool pw_manager_object_is_module(struct pw_manager_object *o)
+{
+ return spa_streq(o->type, PW_TYPE_INTERFACE_Module);
+}
+
+bool pw_manager_object_is_card(struct pw_manager_object *o)
+{
+ const char *str;
+ return spa_streq(o->type, PW_TYPE_INTERFACE_Device) &&
+ o->props != NULL &&
+ (str = pw_properties_get(o->props, PW_KEY_MEDIA_CLASS)) != NULL &&
+ spa_streq(str, "Audio/Device");
+}
+
+bool pw_manager_object_is_sink(struct pw_manager_object *o)
+{
+ const char *str;
+ return spa_streq(o->type, PW_TYPE_INTERFACE_Node) &&
+ o->props != NULL &&
+ (str = pw_properties_get(o->props, PW_KEY_MEDIA_CLASS)) != NULL &&
+ (spa_streq(str, "Audio/Sink") || spa_streq(str, "Audio/Duplex"));
+}
+
+bool pw_manager_object_is_source(struct pw_manager_object *o)
+{
+ const char *str;
+ return spa_streq(o->type, PW_TYPE_INTERFACE_Node) &&
+ o->props != NULL &&
+ (str = pw_properties_get(o->props, PW_KEY_MEDIA_CLASS)) != NULL &&
+ (spa_streq(str, "Audio/Source") ||
+ spa_streq(str, "Audio/Duplex") ||
+ spa_streq(str, "Audio/Source/Virtual"));
+}
+
+bool pw_manager_object_is_monitor(struct pw_manager_object *o)
+{
+ const char *str;
+ return spa_streq(o->type, PW_TYPE_INTERFACE_Node) &&
+ o->props != NULL &&
+ (str = pw_properties_get(o->props, PW_KEY_MEDIA_CLASS)) != NULL &&
+ (spa_streq(str, "Audio/Sink"));
+}
+
+bool pw_manager_object_is_virtual(struct pw_manager_object *o)
+{
+ const char *str;
+ struct pw_node_info *info;
+ return spa_streq(o->type, PW_TYPE_INTERFACE_Node) &&
+ (info = o->info) != NULL && info->props != NULL &&
+ (str = spa_dict_lookup(info->props, PW_KEY_NODE_VIRTUAL)) != NULL &&
+ pw_properties_parse_bool(str);
+}
+
+bool pw_manager_object_is_source_or_monitor(struct pw_manager_object *o)
+{
+ return pw_manager_object_is_source(o) || pw_manager_object_is_monitor(o);
+}
+
+bool pw_manager_object_is_sink_input(struct pw_manager_object *o)
+{
+ const char *str;
+ return spa_streq(o->type, PW_TYPE_INTERFACE_Node) &&
+ o->props != NULL &&
+ (str = pw_properties_get(o->props, PW_KEY_MEDIA_CLASS)) != NULL &&
+ spa_streq(str, "Stream/Output/Audio");
+}
+
+bool pw_manager_object_is_source_output(struct pw_manager_object *o)
+{
+ const char *str;
+ return spa_streq(o->type, PW_TYPE_INTERFACE_Node) &&
+ o->props != NULL &&
+ (str = pw_properties_get(o->props, PW_KEY_MEDIA_CLASS)) != NULL &&
+ spa_streq(str, "Stream/Input/Audio");
+}
+
+bool pw_manager_object_is_recordable(struct pw_manager_object *o)
+{
+ return pw_manager_object_is_source(o) || pw_manager_object_is_sink(o) || pw_manager_object_is_sink_input(o);
+}
+
+bool pw_manager_object_is_link(struct pw_manager_object *o)
+{
+ return spa_streq(o->type, PW_TYPE_INTERFACE_Link);
+}
diff --git a/src/modules/module-protocol-pulse/manager.h b/src/modules/module-protocol-pulse/manager.h
new file mode 100644
index 0000000..56aea25
--- /dev/null
+++ b/src/modules/module-protocol-pulse/manager.h
@@ -0,0 +1,145 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_MANAGER_H
+#define PIPEWIRE_MANAGER_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <spa/utils/defs.h>
+#include <spa/pod/pod.h>
+
+#include <pipewire/pipewire.h>
+
+struct pw_manager_object;
+
+struct pw_manager_events {
+#define PW_VERSION_MANAGER_EVENTS 0
+ uint32_t version;
+
+ void (*destroy) (void *data);
+
+ void (*sync) (void *data);
+
+ void (*added) (void *data, struct pw_manager_object *object);
+
+ void (*updated) (void *data, struct pw_manager_object *object);
+
+ void (*removed) (void *data, struct pw_manager_object *object);
+
+ void (*metadata) (void *data, struct pw_manager_object *object,
+ uint32_t subject, const char *key,
+ const char *type, const char *value);
+
+ void (*disconnect) (void *data);
+
+ void (*object_data_timeout) (void *data, struct pw_manager_object *object,
+ const char *key);
+};
+
+struct pw_manager {
+ struct pw_core *core;
+ struct pw_registry *registry;
+
+ struct pw_core_info *info;
+
+ uint32_t n_objects;
+ struct spa_list object_list;
+};
+
+struct pw_manager_param {
+ uint32_t id;
+ int32_t seq;
+ struct spa_list link; /**< link in manager_object param_list */
+ struct spa_pod *param;
+};
+
+struct pw_manager_object {
+ struct spa_list link; /**< link in manager object_list */
+ uint64_t serial;
+ uint32_t id;
+ uint32_t permissions;
+ const char *type;
+ uint32_t version;
+ uint32_t index;
+ struct pw_properties *props;
+ struct pw_proxy *proxy;
+ char *message_object_path;
+ int (*message_handler)(struct pw_manager *m, struct pw_manager_object *o,
+ const char *message, const char *params, char **response);
+
+ int changed;
+ void *info;
+ struct spa_param_info *params;
+ uint32_t n_params;
+
+ struct spa_list param_list;
+ unsigned int creating:1;
+ unsigned int removing:1;
+};
+
+struct pw_manager *pw_manager_new(struct pw_core *core);
+
+void pw_manager_add_listener(struct pw_manager *manager,
+ struct spa_hook *listener,
+ const struct pw_manager_events *events, void *data);
+
+int pw_manager_sync(struct pw_manager *manager);
+
+void pw_manager_destroy(struct pw_manager *manager);
+
+int pw_manager_set_metadata(struct pw_manager *manager,
+ struct pw_manager_object *metadata,
+ uint32_t subject, const char *key, const char *type,
+ const char *format, ...) SPA_PRINTF_FUNC(6,7);
+
+int pw_manager_for_each_object(struct pw_manager *manager,
+ int (*callback) (void *data, struct pw_manager_object *object),
+ void *data);
+
+void *pw_manager_object_add_data(struct pw_manager_object *o, const char *key, size_t size);
+void *pw_manager_object_get_data(struct pw_manager_object *obj, const char *key);
+void *pw_manager_object_add_temporary_data(struct pw_manager_object *o, const char *key,
+ size_t size, uint64_t lifetime_nsec);
+
+bool pw_manager_object_is_client(struct pw_manager_object *o);
+bool pw_manager_object_is_module(struct pw_manager_object *o);
+bool pw_manager_object_is_card(struct pw_manager_object *o);
+bool pw_manager_object_is_sink(struct pw_manager_object *o);
+bool pw_manager_object_is_source(struct pw_manager_object *o);
+bool pw_manager_object_is_monitor(struct pw_manager_object *o);
+bool pw_manager_object_is_virtual(struct pw_manager_object *o);
+bool pw_manager_object_is_source_or_monitor(struct pw_manager_object *o);
+bool pw_manager_object_is_sink_input(struct pw_manager_object *o);
+bool pw_manager_object_is_source_output(struct pw_manager_object *o);
+bool pw_manager_object_is_recordable(struct pw_manager_object *o);
+bool pw_manager_object_is_link(struct pw_manager_object *o);
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* PIPEWIRE_MANAGER_H */
diff --git a/src/modules/module-protocol-pulse/message-handler.c b/src/modules/module-protocol-pulse/message-handler.c
new file mode 100644
index 0000000..8763ea7
--- /dev/null
+++ b/src/modules/module-protocol-pulse/message-handler.c
@@ -0,0 +1,143 @@
+#include <stdint.h>
+
+#include <regex.h>
+
+#include <spa/param/props.h>
+#include <spa/pod/builder.h>
+#include <spa/pod/pod.h>
+#include <spa/utils/defs.h>
+#include <spa/utils/json.h>
+#include <spa/utils/string.h>
+
+#include <pipewire/pipewire.h>
+
+#include "collect.h"
+#include "log.h"
+#include "manager.h"
+#include "message-handler.h"
+
+static int bluez_card_object_message_handler(struct pw_manager *m, struct pw_manager_object *o, const char *message, const char *params, char **response)
+{
+ struct transport_codec_info codecs[64];
+ uint32_t n_codecs, active;
+
+ pw_log_debug(": bluez-card %p object message:'%s' params:'%s'", o, message, params);
+
+ n_codecs = collect_transport_codec_info(o, codecs, SPA_N_ELEMENTS(codecs), &active);
+
+ if (n_codecs == 0)
+ return -EINVAL;
+
+ if (spa_streq(message, "switch-codec")) {
+ char codec[256];
+ struct spa_json it;
+ char buf[1024];
+ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buf, sizeof(buf));
+ struct spa_pod_frame f[1];
+ struct spa_pod *param;
+ uint32_t codec_id = SPA_ID_INVALID;
+
+ /* Parse args */
+ if (params == NULL)
+ return -EINVAL;
+
+ spa_json_init(&it, params, strlen(params));
+ if (spa_json_get_string(&it, codec, sizeof(codec)) <= 0)
+ return -EINVAL;
+
+ codec_id = atoi(codec);
+
+ /* Switch codec */
+ spa_pod_builder_push_object(&b, &f[0],
+ SPA_TYPE_OBJECT_Props, SPA_PARAM_Props);
+ spa_pod_builder_add(&b,
+ SPA_PROP_bluetoothAudioCodec, SPA_POD_Id(codec_id), 0);
+ param = spa_pod_builder_pop(&b, &f[0]);
+
+ pw_device_set_param((struct pw_device *)o->proxy,
+ SPA_PARAM_Props, 0, param);
+ return 0;
+ } else if (spa_streq(message, "list-codecs")) {
+ uint32_t i;
+ FILE *r;
+ size_t size;
+ bool first = true;
+
+ r = open_memstream(response, &size);
+ if (r == NULL)
+ return -errno;
+
+ fputc('[', r);
+ for (i = 0; i < n_codecs; ++i) {
+ const char *desc = codecs[i].description;
+ fprintf(r, "%s{\"name\":\"%d\",\"description\":\"%s\"}",
+ first ? "" : ",",
+ (int)codecs[i].id, desc ? desc : "Unknown");
+ first = false;
+ }
+ fputc(']', r);
+
+ return fclose(r) ? -errno : 0;
+ } else if (spa_streq(message, "get-codec")) {
+ if (active == SPA_ID_INVALID)
+ *response = strdup("null");
+ else
+ *response = spa_aprintf("\"%d\"", (int)codecs[active].id);
+ return *response ? 0 : -ENOMEM;
+ }
+
+ return -ENOSYS;
+}
+
+static int core_object_message_handler(struct pw_manager *m, struct pw_manager_object *o, const char *message, const char *params, char **response)
+{
+ pw_log_debug(": core %p object message:'%s' params:'%s'", o, message, params);
+
+ if (spa_streq(message, "list-handlers")) {
+ FILE *r;
+ size_t size;
+ bool first = true;
+
+ r = open_memstream(response, &size);
+ if (r == NULL)
+ return -errno;
+
+ fputc('[', r);
+ spa_list_for_each(o, &m->object_list, link) {
+ if (o->message_object_path) {
+ fprintf(r, "%s{\"name\":\"%s\",\"description\":\"%s\"}",
+ first ? "" : ",",
+ o->message_object_path, o->type);
+ first = false;
+ }
+ }
+ fputc(']', r);
+ return fclose(r) ? -errno : 0;
+ }
+
+ return -ENOSYS;
+}
+
+void register_object_message_handlers(struct pw_manager_object *o)
+{
+ const char *str;
+
+ if (o->id == PW_ID_CORE) {
+ free(o->message_object_path);
+ o->message_object_path = strdup("/core");
+ o->message_handler = core_object_message_handler;
+ return;
+ }
+
+ if (pw_manager_object_is_card(o) && o->props != NULL &&
+ (str = pw_properties_get(o->props, PW_KEY_DEVICE_API)) != NULL &&
+ spa_streq(str, "bluez5")) {
+ str = pw_properties_get(o->props, PW_KEY_DEVICE_NAME);
+ if (str) {
+ free(o->message_object_path);
+ o->message_object_path = spa_aprintf("/card/%s/bluez", str);
+ o->message_handler = bluez_card_object_message_handler;
+ }
+ return;
+ }
+}
diff --git a/src/modules/module-protocol-pulse/message-handler.h b/src/modules/module-protocol-pulse/message-handler.h
new file mode 100644
index 0000000..da2a7b0
--- /dev/null
+++ b/src/modules/module-protocol-pulse/message-handler.h
@@ -0,0 +1,8 @@
+#ifndef PULSE_SERVER_MESSAGE_HANDLER_H
+#define PULSE_SERVER_MESSAGE_HANDLER_H
+
+struct pw_manager_object;
+
+void register_object_message_handlers(struct pw_manager_object *o);
+
+#endif /* PULSE_SERVER_MESSAGE_HANDLER_H */
diff --git a/src/modules/module-protocol-pulse/message.c b/src/modules/module-protocol-pulse/message.c
new file mode 100644
index 0000000..daf4d2e
--- /dev/null
+++ b/src/modules/module-protocol-pulse/message.c
@@ -0,0 +1,879 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <arpa/inet.h>
+#include <math.h>
+
+#include <spa/debug/buffer.h>
+#include <spa/utils/defs.h>
+#include <spa/utils/string.h>
+#include <pipewire/log.h>
+
+#include "defs.h"
+#include "format.h"
+#include "internal.h"
+#include "message.h"
+#include "remap.h"
+#include "volume.h"
+
+#define MAX_SIZE (256*1024)
+#define MAX_ALLOCATED (16*1024 *1024)
+
+#define VOLUME_MUTED ((uint32_t) 0U)
+#define VOLUME_NORM ((uint32_t) 0x10000U)
+#define VOLUME_MAX ((uint32_t) UINT32_MAX/2)
+
+#define PA_CHANNELS_MAX (32u)
+
+PW_LOG_TOPIC_EXTERN(pulse_conn);
+#define PW_LOG_TOPIC_DEFAULT pulse_conn
+
+static inline uint32_t volume_from_linear(float vol)
+{
+ uint32_t v;
+ if (vol <= 0.0f)
+ v = VOLUME_MUTED;
+ else
+ v = SPA_CLAMP((uint64_t) lround(cbrt(vol) * VOLUME_NORM),
+ VOLUME_MUTED, VOLUME_MAX);
+ return v;
+}
+
+static inline float volume_to_linear(uint32_t vol)
+{
+ float v = ((float)vol) / VOLUME_NORM;
+ return v * v * v;
+}
+
+static int read_u8(struct message *m, uint8_t *val)
+{
+ if (m->offset + 1 > m->length)
+ return -ENOSPC;
+ *val = m->data[m->offset];
+ m->offset++;
+ return 0;
+}
+
+static int read_u32(struct message *m, uint32_t *val)
+{
+ if (m->offset + 4 > m->length)
+ return -ENOSPC;
+ memcpy(val, &m->data[m->offset], 4);
+ *val = ntohl(*val);
+ m->offset += 4;
+ return 0;
+}
+static int read_u64(struct message *m, uint64_t *val)
+{
+ uint32_t tmp;
+ int res;
+ if ((res = read_u32(m, &tmp)) < 0)
+ return res;
+ *val = ((uint64_t)tmp) << 32;
+ if ((res = read_u32(m, &tmp)) < 0)
+ return res;
+ *val |= tmp;
+ return 0;
+}
+
+static int read_sample_spec(struct message *m, struct sample_spec *ss)
+{
+ int res;
+ uint8_t tmp;
+ if ((res = read_u8(m, &tmp)) < 0)
+ return res;
+ ss->format = format_pa2id(tmp);
+ if ((res = read_u8(m, &ss->channels)) < 0)
+ return res;
+ return read_u32(m, &ss->rate);
+}
+
+static int read_props(struct message *m, struct pw_properties *props, bool remap)
+{
+ int res;
+
+ while (true) {
+ const char *key;
+ const void *data;
+ uint32_t length;
+ size_t size;
+ const struct str_map *map;
+
+ if ((res = message_get(m,
+ TAG_STRING, &key,
+ TAG_INVALID)) < 0)
+ return res;
+
+ if (key == NULL)
+ break;
+
+ if ((res = message_get(m,
+ TAG_U32, &length,
+ TAG_INVALID)) < 0)
+ return res;
+ if (length > MAX_TAG_SIZE)
+ return -EINVAL;
+
+ if ((res = message_get(m,
+ TAG_ARBITRARY, &data, &size,
+ TAG_INVALID)) < 0)
+ return res;
+
+ if (remap && (map = str_map_find(props_key_map, NULL, key)) != NULL) {
+ key = map->pw_str;
+ if (map->child != NULL &&
+ (map = str_map_find(map->child, NULL, data)) != NULL)
+ data = map->pw_str;
+ }
+ pw_properties_set(props, key, data);
+ }
+ return 0;
+}
+
+static int read_arbitrary(struct message *m, const void **val, size_t *length)
+{
+ uint32_t len;
+ int res;
+ if ((res = read_u32(m, &len)) < 0)
+ return res;
+ if (m->offset + len > m->length)
+ return -ENOSPC;
+ *val = m->data + m->offset;
+ m->offset += len;
+ if (length)
+ *length = len;
+ return 0;
+}
+
+static int read_string(struct message *m, char **str)
+{
+ uint32_t n, maxlen;
+ if (m->offset + 1 > m->length)
+ return -ENOSPC;
+ maxlen = m->length - m->offset;
+ n = strnlen(SPA_PTROFF(m->data, m->offset, char), maxlen);
+ if (n == maxlen)
+ return -EINVAL;
+ *str = SPA_PTROFF(m->data, m->offset, char);
+ m->offset += n + 1;
+ return 0;
+}
+
+static int read_timeval(struct message *m, struct timeval *tv)
+{
+ int res;
+ uint32_t tmp;
+
+ if ((res = read_u32(m, &tmp)) < 0)
+ return res;
+ tv->tv_sec = tmp;
+ if ((res = read_u32(m, &tmp)) < 0)
+ return res;
+ tv->tv_usec = tmp;
+ return 0;
+}
+
+static int read_channel_map(struct message *m, struct channel_map *map)
+{
+ int res;
+ uint8_t i, tmp;
+
+ if ((res = read_u8(m, &map->channels)) < 0)
+ return res;
+ if (map->channels > CHANNELS_MAX)
+ return -EINVAL;
+ for (i = 0; i < map->channels; i ++) {
+ if ((res = read_u8(m, &tmp)) < 0)
+ return res;
+ map->map[i] = channel_pa2id(tmp);
+ }
+ return 0;
+}
+static int read_volume(struct message *m, float *vol)
+{
+ int res;
+ uint32_t v;
+ if ((res = read_u32(m, &v)) < 0)
+ return res;
+ *vol = volume_to_linear(v);
+ return 0;
+}
+
+static int read_cvolume(struct message *m, struct volume *vol)
+{
+ int res;
+ uint8_t i;
+
+ if ((res = read_u8(m, &vol->channels)) < 0)
+ return res;
+ if (vol->channels > CHANNELS_MAX)
+ return -EINVAL;
+ for (i = 0; i < vol->channels; i ++) {
+ if ((res = read_volume(m, &vol->values[i])) < 0)
+ return res;
+ }
+ return 0;
+}
+
+static int read_format_info(struct message *m, struct format_info *info)
+{
+ int res;
+ uint8_t tag, encoding;
+
+ spa_zero(*info);
+ if ((res = read_u8(m, &tag)) < 0)
+ return res;
+ if (tag != TAG_U8)
+ return -EPROTO;
+ if ((res = read_u8(m, &encoding)) < 0)
+ return res;
+ info->encoding = encoding;
+
+ if ((res = read_u8(m, &tag)) < 0)
+ return res;
+ if (tag != TAG_PROPLIST)
+ return -EPROTO;
+
+ info->props = pw_properties_new(NULL, NULL);
+ if (info->props == NULL)
+ return -errno;
+ if ((res = read_props(m, info->props, false)) < 0)
+ format_info_clear(info);
+ return res;
+}
+
+int message_get(struct message *m, ...)
+{
+ va_list va;
+ int res = 0;
+
+ va_start(va, m);
+
+ while (true) {
+ int tag = va_arg(va, int);
+ uint8_t dtag;
+ if (tag == TAG_INVALID)
+ break;
+
+ if ((res = read_u8(m, &dtag)) < 0)
+ goto done;
+
+ switch (dtag) {
+ case TAG_STRING:
+ if (tag != TAG_STRING)
+ goto invalid;
+ if ((res = read_string(m, va_arg(va, char**))) < 0)
+ goto done;
+ break;
+ case TAG_STRING_NULL:
+ if (tag != TAG_STRING)
+ goto invalid;
+ *va_arg(va, char**) = NULL;
+ break;
+ case TAG_U8:
+ if (dtag != tag)
+ goto invalid;
+ if ((res = read_u8(m, va_arg(va, uint8_t*))) < 0)
+ goto done;
+ break;
+ case TAG_U32:
+ if (dtag != tag)
+ goto invalid;
+ if ((res = read_u32(m, va_arg(va, uint32_t*))) < 0)
+ goto done;
+ break;
+ case TAG_S64:
+ case TAG_U64:
+ case TAG_USEC:
+ if (dtag != tag)
+ goto invalid;
+ if ((res = read_u64(m, va_arg(va, uint64_t*))) < 0)
+ goto done;
+ break;
+ case TAG_SAMPLE_SPEC:
+ if (dtag != tag)
+ goto invalid;
+ if ((res = read_sample_spec(m, va_arg(va, struct sample_spec*))) < 0)
+ goto done;
+ break;
+ case TAG_ARBITRARY:
+ {
+ const void **val = va_arg(va, const void**);
+ size_t *len = va_arg(va, size_t*);
+ if (dtag != tag)
+ goto invalid;
+ if ((res = read_arbitrary(m, val, len)) < 0)
+ goto done;
+ break;
+ }
+ case TAG_BOOLEAN_TRUE:
+ if (tag != TAG_BOOLEAN)
+ goto invalid;
+ *va_arg(va, bool*) = true;
+ break;
+ case TAG_BOOLEAN_FALSE:
+ if (tag != TAG_BOOLEAN)
+ goto invalid;
+ *va_arg(va, bool*) = false;
+ break;
+ case TAG_TIMEVAL:
+ if (dtag != tag)
+ goto invalid;
+ if ((res = read_timeval(m, va_arg(va, struct timeval*))) < 0)
+ goto done;
+ break;
+ case TAG_CHANNEL_MAP:
+ if (dtag != tag)
+ goto invalid;
+ if ((res = read_channel_map(m, va_arg(va, struct channel_map*))) < 0)
+ goto done;
+ break;
+ case TAG_CVOLUME:
+ if (dtag != tag)
+ goto invalid;
+ if ((res = read_cvolume(m, va_arg(va, struct volume*))) < 0)
+ goto done;
+ break;
+ case TAG_PROPLIST:
+ if (dtag != tag)
+ goto invalid;
+ if ((res = read_props(m, va_arg(va, struct pw_properties*), true)) < 0)
+ goto done;
+ break;
+ case TAG_VOLUME:
+ if (dtag != tag)
+ goto invalid;
+ if ((res = read_volume(m, va_arg(va, float*))) < 0)
+ goto done;
+ break;
+ case TAG_FORMAT_INFO:
+ if (dtag != tag)
+ goto invalid;
+ if ((res = read_format_info(m, va_arg(va, struct format_info*))) < 0)
+ goto done;
+ break;
+ }
+ }
+ res = 0;
+ goto done;
+
+invalid:
+ res = -EINVAL;
+
+done:
+ va_end(va);
+
+ return res;
+}
+
+static int ensure_size(struct message *m, uint32_t size)
+{
+ uint32_t alloc, diff;
+ void *data;
+
+ if (m->length > m->allocated)
+ return -ENOMEM;
+
+ if (m->length + size <= m->allocated)
+ return size;
+
+ alloc = SPA_ROUND_UP_N(SPA_MAX(m->allocated + size, 4096u), 4096u);
+ diff = alloc - m->allocated;
+ if ((data = realloc(m->data, alloc)) == NULL) {
+ free(m->data);
+ m->data = NULL;
+ m->impl->stat.allocated -= m->allocated;
+ m->allocated = 0;
+ return -errno;
+ }
+ m->impl->stat.allocated += diff;
+ m->impl->stat.accumulated += diff;
+ m->data = data;
+ m->allocated = alloc;
+ return size;
+}
+
+static void write_8(struct message *m, uint8_t val)
+{
+ if (ensure_size(m, 1) > 0)
+ m->data[m->length] = val;
+ m->length++;
+}
+
+static void write_32(struct message *m, uint32_t val)
+{
+ val = htonl(val);
+ if (ensure_size(m, 4) > 0)
+ memcpy(m->data + m->length, &val, 4);
+ m->length += 4;
+}
+
+static void write_string(struct message *m, const char *s)
+{
+ write_8(m, s ? TAG_STRING : TAG_STRING_NULL);
+ if (s != NULL) {
+ int len = strlen(s) + 1;
+ if (ensure_size(m, len) > 0)
+ strcpy(SPA_PTROFF(m->data, m->length, char), s);
+ m->length += len;
+ }
+}
+static void write_u8(struct message *m, uint8_t val)
+{
+ write_8(m, TAG_U8);
+ write_8(m, val);
+}
+
+static void write_u32(struct message *m, uint32_t val)
+{
+ write_8(m, TAG_U32);
+ write_32(m, val);
+}
+
+static void write_64(struct message *m, uint8_t tag, uint64_t val)
+{
+ write_8(m, tag);
+ write_32(m, val >> 32);
+ write_32(m, val);
+}
+
+static void write_sample_spec(struct message *m, struct sample_spec *ss)
+{
+ uint32_t channels = SPA_MIN(ss->channels, PA_CHANNELS_MAX);
+ write_8(m, TAG_SAMPLE_SPEC);
+ write_8(m, format_id2pa(ss->format));
+ write_8(m, channels);
+ write_32(m, ss->rate);
+}
+
+static void write_arbitrary(struct message *m, const void *p, size_t length)
+{
+ write_8(m, TAG_ARBITRARY);
+ write_32(m, length);
+ if (ensure_size(m, length) > 0)
+ memcpy(m->data + m->length, p, length);
+ m->length += length;
+}
+
+static void write_boolean(struct message *m, bool val)
+{
+ write_8(m, val ? TAG_BOOLEAN_TRUE : TAG_BOOLEAN_FALSE);
+}
+
+static void write_timeval(struct message *m, struct timeval *tv)
+{
+ write_8(m, TAG_TIMEVAL);
+ write_32(m, tv->tv_sec);
+ write_32(m, tv->tv_usec);
+}
+
+static void write_channel_map(struct message *m, struct channel_map *map)
+{
+ uint8_t i;
+ uint32_t aux = 0, channels = SPA_MIN(map->channels, PA_CHANNELS_MAX);
+ write_8(m, TAG_CHANNEL_MAP);
+ write_8(m, channels);
+ for (i = 0; i < channels; i ++)
+ write_8(m, channel_id2pa(map->map[i], &aux));
+}
+
+static void write_volume(struct message *m, float vol)
+{
+ write_8(m, TAG_VOLUME);
+ write_32(m, volume_from_linear(vol));
+}
+
+static void write_cvolume(struct message *m, struct volume *vol)
+{
+ uint8_t i;
+ uint32_t channels = SPA_MIN(vol->channels, PA_CHANNELS_MAX);
+ write_8(m, TAG_CVOLUME);
+ write_8(m, channels);
+ for (i = 0; i < channels; i ++)
+ write_32(m, volume_from_linear(vol->values[i]));
+}
+
+static void add_stream_group(struct message *m, struct spa_dict *dict, const char *key,
+ const char *media_class, const char *media_role)
+{
+ const char *str, *id, *prefix;
+ char *b;
+ int l;
+
+ if (media_class == NULL)
+ return;
+ if (spa_streq(media_class, "Stream/Output/Audio"))
+ prefix = "sink-input";
+ else if (spa_streq(media_class, "Stream/Input/Audio"))
+ prefix = "source-output";
+ else
+ return;
+
+ if ((str = media_role) != NULL)
+ id = "media-role";
+ else if ((str = spa_dict_lookup(dict, PW_KEY_APP_ID)) != NULL)
+ id = "application-id";
+ else if ((str = spa_dict_lookup(dict, PW_KEY_APP_NAME)) != NULL)
+ id = "application-name";
+ else if ((str = spa_dict_lookup(dict, PW_KEY_MEDIA_NAME)) != NULL)
+ id = "media-name";
+ else
+ return;
+
+ write_string(m, key);
+ l = strlen(prefix) + strlen(id) + strlen(str) + 6; /* "-by-" , ":" and \0 */
+ b = alloca(l);
+ snprintf(b, l, "%s-by-%s:%s", prefix, id, str);
+ write_u32(m, l);
+ write_arbitrary(m, b, l);
+}
+
+static void write_dict(struct message *m, struct spa_dict *dict, bool remap)
+{
+ const struct spa_dict_item *it;
+
+ write_8(m, TAG_PROPLIST);
+ if (dict != NULL) {
+ const char *media_class = NULL, *media_role = NULL;
+ spa_dict_for_each(it, dict) {
+ const char *key = it->key;
+ const char *val = it->value;
+ int l;
+ const struct str_map *map;
+
+ if (remap && (map = str_map_find(props_key_map, key, NULL)) != NULL) {
+ key = map->pa_str;
+ if (map->child != NULL &&
+ (map = str_map_find(map->child, val, NULL)) != NULL)
+ val = map->pa_str;
+ }
+ if (spa_streq(key, "media.class"))
+ media_class = val;
+ if (spa_streq(key, "media.role"))
+ media_role = val;
+
+ write_string(m, key);
+ l = strlen(val) + 1;
+ write_u32(m, l);
+ write_arbitrary(m, val, l);
+
+ }
+ if (remap)
+ add_stream_group(m, dict, "module-stream-restore.id",
+ media_class, media_role);
+ }
+ write_string(m, NULL);
+}
+
+static void write_format_info(struct message *m, struct format_info *info)
+{
+ write_8(m, TAG_FORMAT_INFO);
+ write_u8(m, (uint8_t) info->encoding);
+ write_dict(m, info->props ? &info->props->dict : NULL, false);
+}
+
+int message_put(struct message *m, ...)
+{
+ va_list va;
+
+ if (m == NULL)
+ return -EINVAL;
+
+ va_start(va, m);
+
+ while (true) {
+ int tag = va_arg(va, int);
+ if (tag == TAG_INVALID)
+ break;
+
+ switch (tag) {
+ case TAG_STRING:
+ write_string(m, va_arg(va, const char *));
+ break;
+ case TAG_U8:
+ write_u8(m, (uint8_t)va_arg(va, int));
+ break;
+ case TAG_U32:
+ write_u32(m, (uint32_t)va_arg(va, uint32_t));
+ break;
+ case TAG_S64:
+ case TAG_U64:
+ case TAG_USEC:
+ write_64(m, tag, va_arg(va, uint64_t));
+ break;
+ case TAG_SAMPLE_SPEC:
+ write_sample_spec(m, va_arg(va, struct sample_spec*));
+ break;
+ case TAG_ARBITRARY:
+ {
+ const void *p = va_arg(va, const void*);
+ size_t length = va_arg(va, size_t);
+ write_arbitrary(m, p, length);
+ break;
+ }
+ case TAG_BOOLEAN:
+ write_boolean(m, va_arg(va, int));
+ break;
+ case TAG_TIMEVAL:
+ write_timeval(m, va_arg(va, struct timeval*));
+ break;
+ case TAG_CHANNEL_MAP:
+ write_channel_map(m, va_arg(va, struct channel_map*));
+ break;
+ case TAG_CVOLUME:
+ write_cvolume(m, va_arg(va, struct volume*));
+ break;
+ case TAG_PROPLIST:
+ write_dict(m, va_arg(va, struct spa_dict*), true);
+ break;
+ case TAG_VOLUME:
+ write_volume(m, va_arg(va, double));
+ break;
+ case TAG_FORMAT_INFO:
+ write_format_info(m, va_arg(va, struct format_info*));
+ break;
+ }
+ }
+ va_end(va);
+
+ if (m->length > m->allocated)
+ return -ENOMEM;
+
+ return 0;
+}
+
+int message_dump(enum spa_log_level level, struct message *m)
+{
+ int res;
+ uint32_t i, offset = m->offset, o;
+
+ pw_log(level, "message: len:%d alloc:%u", m->length, m->allocated);
+ while (true) {
+ uint8_t tag;
+
+ o = m->offset;
+ if (read_u8(m, &tag) < 0)
+ break;
+
+ switch (tag) {
+ case TAG_STRING:
+ {
+ char *val;
+ if ((res = read_string(m, &val)) < 0)
+ return res;
+ pw_log(level, "%u: string: '%s'", o, val);
+ break;
+ }
+ case TAG_STRING_NULL:
+ pw_log(level, "%u: string: NULL", o);
+ break;
+ case TAG_U8:
+ {
+ uint8_t val;
+ if ((res = read_u8(m, &val)) < 0)
+ return res;
+ pw_log(level, "%u: u8: %u", o, val);
+ break;
+ }
+ case TAG_U32:
+ {
+ uint32_t val;
+ if ((res = read_u32(m, &val)) < 0)
+ return res;
+ pw_log(level, "%u: u32: %u", o, val);
+ break;
+ }
+ case TAG_S64:
+ {
+ uint64_t val;
+ if ((res = read_u64(m, &val)) < 0)
+ return res;
+ pw_log(level, "%u: s64: %"PRIi64"", o, (int64_t)val);
+ break;
+ }
+ case TAG_U64:
+ {
+ uint64_t val;
+ if ((res = read_u64(m, &val)) < 0)
+ return res;
+ pw_log(level, "%u: u64: %"PRIu64"", o, val);
+ break;
+ }
+ case TAG_USEC:
+ {
+ uint64_t val;
+ if ((res = read_u64(m, &val)) < 0)
+ return res;
+ pw_log(level, "%u: u64: %"PRIu64"", o, val);
+ break;
+ }
+ case TAG_SAMPLE_SPEC:
+ {
+ struct sample_spec ss;
+ if ((res = read_sample_spec(m, &ss)) < 0)
+ return res;
+ pw_log(level, "%u: ss: format:%s rate:%d channels:%u", o,
+ format_id2name(ss.format), ss.rate,
+ ss.channels);
+ break;
+ }
+ case TAG_ARBITRARY:
+ {
+ const void *mem;
+ size_t len;
+ if ((res = read_arbitrary(m, &mem, &len)) < 0)
+ return res;
+ spa_debug_mem(0, mem, len);
+ break;
+ }
+ case TAG_BOOLEAN_TRUE:
+ pw_log(level, "%u: bool: true", o);
+ break;
+ case TAG_BOOLEAN_FALSE:
+ pw_log(level, "%u: bool: false", o);
+ break;
+ case TAG_TIMEVAL:
+ {
+ struct timeval tv;
+ if ((res = read_timeval(m, &tv)) < 0)
+ return res;
+ pw_log(level, "%u: timeval: %lu:%lu", o, tv.tv_sec, tv.tv_usec);
+ break;
+ }
+ case TAG_CHANNEL_MAP:
+ {
+ struct channel_map map;
+ if ((res = read_channel_map(m, &map)) < 0)
+ return res;
+ pw_log(level, "%u: channelmap: channels:%u", o, map.channels);
+ for (i = 0; i < map.channels; i++)
+ pw_log(level, " %d: %s", i, channel_id2name(map.map[i]));
+ break;
+ }
+ case TAG_CVOLUME:
+ {
+ struct volume vol;
+ if ((res = read_cvolume(m, &vol)) < 0)
+ return res;
+ pw_log(level, "%u: cvolume: channels:%u", o, vol.channels);
+ for (i = 0; i < vol.channels; i++)
+ pw_log(level, " %d: %f", i, vol.values[i]);
+ break;
+ }
+ case TAG_PROPLIST:
+ {
+ struct pw_properties *props = pw_properties_new(NULL, NULL);
+ const struct spa_dict_item *it;
+ res = read_props(m, props, false);
+ if (res >= 0) {
+ pw_log(level, "%u: props: n_items:%u", o, props->dict.n_items);
+ spa_dict_for_each(it, &props->dict)
+ pw_log(level, " '%s': '%s'", it->key, it->value);
+ }
+ pw_properties_free(props);
+ if (res < 0)
+ return res;
+ break;
+ }
+ case TAG_VOLUME:
+ {
+ float vol;
+ if ((res = read_volume(m, &vol)) < 0)
+ return res;
+ pw_log(level, "%u: volume: %f", o, vol);
+ break;
+ }
+ case TAG_FORMAT_INFO:
+ {
+ struct format_info info;
+ const struct spa_dict_item *it;
+ if ((res = read_format_info(m, &info)) < 0)
+ return res;
+ pw_log(level, "%u: format-info: enc:%s n_items:%u",
+ o, format_encoding2name(info.encoding),
+ info.props->dict.n_items);
+ spa_dict_for_each(it, &info.props->dict)
+ pw_log(level, " '%s': '%s'", it->key, it->value);
+ break;
+ }
+ }
+ }
+ m->offset = offset;
+
+ return 0;
+}
+
+struct message *message_alloc(struct impl *impl, uint32_t channel, uint32_t size)
+{
+ struct message *msg;
+
+ if (!spa_list_is_empty(&impl->free_messages)) {
+ msg = spa_list_first(&impl->free_messages, struct message, link);
+ spa_list_remove(&msg->link);
+ pw_log_trace("using recycled message %p size:%d", msg, size);
+
+ spa_assert(msg->impl == impl);
+ } else {
+ if ((msg = calloc(1, sizeof(*msg))) == NULL)
+ return NULL;
+
+ pw_log_trace("new message %p size:%d", msg, size);
+ msg->impl = impl;
+ msg->impl->stat.n_allocated++;
+ msg->impl->stat.n_accumulated++;
+ }
+
+ if (ensure_size(msg, size) < 0) {
+ message_free(msg, false, true);
+ return NULL;
+ }
+
+ spa_zero(msg->extra);
+ msg->channel = channel;
+ msg->offset = 0;
+ msg->length = size;
+
+ return msg;
+}
+
+void message_free(struct message *msg, bool dequeue, bool destroy)
+{
+ if (dequeue)
+ spa_list_remove(&msg->link);
+
+ if (msg->impl->stat.allocated > MAX_ALLOCATED || msg->allocated > MAX_SIZE)
+ destroy = true;
+
+ if (destroy) {
+ pw_log_trace("destroy message %p size:%d", msg, msg->allocated);
+ msg->impl->stat.n_allocated--;
+ msg->impl->stat.allocated -= msg->allocated;
+ free(msg->data);
+ free(msg);
+ } else {
+ pw_log_trace("recycle message %p size:%d/%d", msg, msg->length, msg->allocated);
+ spa_list_append(&msg->impl->free_messages, &msg->link);
+ msg->length = 0;
+ }
+}
diff --git a/src/modules/module-protocol-pulse/message.h b/src/modules/module-protocol-pulse/message.h
new file mode 100644
index 0000000..ad95292
--- /dev/null
+++ b/src/modules/module-protocol-pulse/message.h
@@ -0,0 +1,75 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PULSE_SERVER_MESSAGE_H
+#define PULSE_SERVER_MESSAGE_H
+
+#include <stdbool.h>
+#include <stdint.h>
+
+#include <spa/utils/list.h>
+#include <spa/support/log.h>
+
+struct impl;
+
+struct message {
+ struct spa_list link;
+ struct impl *impl;
+ uint32_t extra[4];
+ uint32_t channel;
+ uint32_t allocated;
+ uint32_t length;
+ uint32_t offset;
+ uint8_t *data;
+};
+
+enum {
+ TAG_INVALID = 0,
+ TAG_STRING = 't',
+ TAG_STRING_NULL = 'N',
+ TAG_U32 = 'L',
+ TAG_U8 = 'B',
+ TAG_U64 = 'R',
+ TAG_S64 = 'r',
+ TAG_SAMPLE_SPEC = 'a',
+ TAG_ARBITRARY = 'x',
+ TAG_BOOLEAN_TRUE = '1',
+ TAG_BOOLEAN_FALSE = '0',
+ TAG_BOOLEAN = TAG_BOOLEAN_TRUE,
+ TAG_TIMEVAL = 'T',
+ TAG_USEC = 'U' /* 64bit unsigned */,
+ TAG_CHANNEL_MAP = 'm',
+ TAG_CVOLUME = 'v',
+ TAG_PROPLIST = 'P',
+ TAG_VOLUME = 'V',
+ TAG_FORMAT_INFO = 'f',
+};
+
+struct message *message_alloc(struct impl *impl, uint32_t channel, uint32_t size);
+void message_free(struct message *msg, bool dequeue, bool destroy);
+int message_get(struct message *m, ...);
+int message_put(struct message *m, ...);
+int message_dump(enum spa_log_level level, struct message *m);
+
+#endif /* PULSE_SERVER_MESSAGE_H */
diff --git a/src/modules/module-protocol-pulse/module.c b/src/modules/module-protocol-pulse/module.c
new file mode 100644
index 0000000..ec10404
--- /dev/null
+++ b/src/modules/module-protocol-pulse/module.c
@@ -0,0 +1,333 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Georges Basile Stavracas Neto
+ * Copyright © 2021 Wim Taymans <wim.taymans@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdlib.h>
+#include <string.h>
+#include <ctype.h>
+
+#include <spa/utils/defs.h>
+#include <spa/utils/list.h>
+#include <spa/utils/hook.h>
+#include <spa/utils/string.h>
+#include <pipewire/log.h>
+#include <pipewire/map.h>
+#include <pipewire/properties.h>
+#include <pipewire/work-queue.h>
+
+#include "defs.h"
+#include "format.h"
+#include "internal.h"
+#include "log.h"
+#include "module.h"
+#include "remap.h"
+
+static void on_module_unload(void *obj, void *data, int res, uint32_t index)
+{
+ struct module *module = obj;
+ module_unload(module);
+}
+
+void module_schedule_unload(struct module *module)
+{
+ if (module->unloading)
+ return;
+
+ pw_work_queue_add(module->impl->work_queue, module, 0, on_module_unload, NULL);
+ module->unloading = true;
+}
+
+static struct module *module_new(struct impl *impl, const struct module_info *info)
+{
+ struct module *module;
+
+ module = calloc(1, sizeof(*module) + info->data_size);
+ if (module == NULL)
+ return NULL;
+
+ module->index = SPA_ID_INVALID;
+ module->impl = impl;
+ module->info = info;
+ spa_hook_list_init(&module->listener_list);
+ module->user_data = SPA_PTROFF(module, sizeof(*module), void);
+ module->loaded = false;
+
+ return module;
+}
+
+void module_add_listener(struct module *module,
+ struct spa_hook *listener,
+ const struct module_events *events, void *data)
+{
+ spa_hook_list_append(&module->listener_list, listener, events, data);
+}
+
+int module_load(struct module *module)
+{
+ pw_log_info("load module index:%u name:%s", module->index, module->info->name);
+ if (module->info->load == NULL)
+ return -ENOTSUP;
+ /* subscription event is sent when the module does a
+ * module_emit_loaded() */
+ return module->info->load(module);
+}
+
+void module_free(struct module *module)
+{
+ struct impl *impl = module->impl;
+
+ module_emit_destroy(module);
+
+ if (module->index != SPA_ID_INVALID)
+ pw_map_remove(&impl->modules, module->index & MODULE_INDEX_MASK);
+
+ if (module->unloading)
+ pw_work_queue_cancel(impl->work_queue, module, SPA_ID_INVALID);
+
+ spa_hook_list_clean(&module->listener_list);
+ pw_properties_free(module->props);
+
+ free((char*)module->args);
+
+ free(module);
+}
+
+int module_unload(struct module *module)
+{
+ struct impl *impl = module->impl;
+ int res = 0;
+
+ pw_log_info("unload module index:%u name:%s", module->index, module->info->name);
+
+ if (module->info->unload)
+ res = module->info->unload(module);
+
+ if (module->loaded)
+ broadcast_subscribe_event(impl,
+ SUBSCRIPTION_MASK_MODULE,
+ SUBSCRIPTION_EVENT_REMOVE | SUBSCRIPTION_EVENT_MODULE,
+ module->index);
+
+ module_free(module);
+
+ return res;
+}
+
+/** utils */
+void module_args_add_props(struct pw_properties *props, const char *str)
+{
+ char *s = strdup(str), *p = s, *e, f;
+ const char *k, *v;
+ const struct str_map *map;
+
+ while (*p) {
+ while (*p && isspace(*p))
+ p++;
+ e = strchr(p, '=');
+ if (e == NULL)
+ break;
+ *e = '\0';
+ k = p;
+ p = e+1;
+
+ if (*p == '\"') {
+ p++;
+ f = '\"';
+ } else if (*p == '\'') {
+ p++;
+ f = '\'';
+ } else {
+ f = ' ';
+ }
+ v = p;
+ for (e = p; *e ; e++) {
+ if (*e == f)
+ break;
+ if (*e == '\\')
+ e++;
+ }
+ p = e;
+ if (*e != '\0')
+ p++;
+ *e = '\0';
+
+ if ((map = str_map_find(props_key_map, NULL, k)) != NULL) {
+ k = map->pw_str;
+ if (map->child != NULL &&
+ (map = str_map_find(map->child, NULL, v)) != NULL)
+ v = map->pw_str;
+ }
+ pw_properties_set(props, k, v);
+ }
+ free(s);
+}
+
+int module_args_to_audioinfo(struct impl *impl, struct pw_properties *props, struct spa_audio_info_raw *info)
+{
+ const char *str;
+ uint32_t i;
+
+ /* We don't use any incoming format setting and use our native format */
+ spa_zero(*info);
+ info->flags = SPA_AUDIO_FLAG_UNPOSITIONED;
+ info->format = SPA_AUDIO_FORMAT_F32P;
+
+ if ((str = pw_properties_get(props, "channels")) != NULL) {
+ info->channels = pw_properties_parse_int(str);
+ if (info->channels == 0 || info->channels > SPA_AUDIO_MAX_CHANNELS) {
+ pw_log_error("invalid channels '%s'", str);
+ return -EINVAL;
+ }
+ pw_properties_set(props, "channels", NULL);
+ }
+ if ((str = pw_properties_get(props, "channel_map")) != NULL) {
+ struct channel_map map;
+
+ channel_map_parse(str, &map);
+ if (map.channels == 0 || map.channels > SPA_AUDIO_MAX_CHANNELS) {
+ pw_log_error("invalid channel_map '%s'", str);
+ return -EINVAL;
+ }
+ if (info->channels == 0)
+ info->channels = map.channels;
+ if (info->channels != map.channels) {
+ pw_log_error("Mismatched channel map");
+ return -EINVAL;
+ }
+ channel_map_to_positions(&map, info->position);
+ info->flags &= ~SPA_AUDIO_FLAG_UNPOSITIONED;
+ pw_properties_set(props, "channel_map", NULL);
+ } else {
+ if (info->channels == 0)
+ info->channels = impl->defs.sample_spec.channels;
+
+ if (info->channels == impl->defs.channel_map.channels) {
+ channel_map_to_positions(&impl->defs.channel_map, info->position);
+ } else if (info->channels == 1) {
+ info->position[0] = SPA_AUDIO_CHANNEL_MONO;
+ } else if (info->channels == 2) {
+ info->position[0] = SPA_AUDIO_CHANNEL_FL;
+ info->position[1] = SPA_AUDIO_CHANNEL_FR;
+ } else {
+ /* FIXME add more mappings */
+ for (i = 0; i < info->channels; i++)
+ info->position[i] = SPA_AUDIO_CHANNEL_UNKNOWN;
+ }
+ if (info->position[0] != SPA_AUDIO_CHANNEL_UNKNOWN)
+ info->flags &= ~SPA_AUDIO_FLAG_UNPOSITIONED;
+ }
+
+ if ((str = pw_properties_get(props, "rate")) != NULL) {
+ info->rate = pw_properties_parse_int(str);
+ pw_properties_set(props, "rate", NULL);
+ } else {
+ info->rate = 0;
+ }
+ return 0;
+}
+
+bool module_args_parse_bool(const char *v)
+{
+ if (spa_streq(v, "1") || !strcasecmp(v, "y") || !strcasecmp(v, "t") ||
+ !strcasecmp(v, "yes") || !strcasecmp(v, "true") || !strcasecmp(v, "on"))
+ return true;
+ return false;
+}
+
+static const struct module_info *find_module_info(const char *name)
+{
+ extern const struct module_info __start_pw_mod_pulse_modules[];
+ extern const struct module_info __stop_pw_mod_pulse_modules[];
+
+ const struct module_info *info = __start_pw_mod_pulse_modules;
+
+ for (; info < __stop_pw_mod_pulse_modules; info++) {
+ if (spa_streq(info->name, name))
+ return info;
+ }
+
+ spa_assert(info == __stop_pw_mod_pulse_modules);
+
+ return NULL;
+}
+
+static int find_module_by_name(void *item_data, void *data)
+{
+ const char *name = data;
+ const struct module *module = item_data;
+ return spa_streq(module->info->name, name) ? 1 : 0;
+}
+
+struct module *module_create(struct impl *impl, const char *name, const char *args)
+{
+ const struct module_info *info;
+ struct module *module;
+
+ info = find_module_info(name);
+ if (info == NULL) {
+ errno = ENOENT;
+ return NULL;
+ }
+
+ if (info->load_once) {
+ int exists;
+ exists = pw_map_for_each(&impl->modules, find_module_by_name,
+ (void *)name);
+ if (exists) {
+ errno = EEXIST;
+ return NULL;
+ }
+ }
+
+ module = module_new(impl, info);
+ if (module == NULL)
+ return NULL;
+
+ module->props = pw_properties_new(NULL, NULL);
+ if (module->props == NULL) {
+ module_free(module);
+ return NULL;
+ }
+
+ if (args)
+ module_args_add_props(module->props, args);
+
+ int res = module->info->prepare(module);
+ if (res < 0) {
+ module_free(module);
+ errno = -res;
+ return NULL;
+ }
+
+ module->index = pw_map_insert_new(&impl->modules, module);
+ if (module->index == SPA_ID_INVALID) {
+ module_unload(module);
+ return NULL;
+ }
+
+ module->args = args ? strdup(args) : NULL;
+ module->index |= MODULE_FLAG;
+
+ return module;
+}
diff --git a/src/modules/module-protocol-pulse/module.h b/src/modules/module-protocol-pulse/module.h
new file mode 100644
index 0000000..1a6ffb0
--- /dev/null
+++ b/src/modules/module-protocol-pulse/module.h
@@ -0,0 +1,94 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Georges Basile Stavracas Neto
+ * Copyright © 2021 Wim Taymans <wim.taymans@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_PULSE_MODULE_H
+#define PIPEWIRE_PULSE_MODULE_H
+
+#include <spa/param/audio/raw.h>
+#include <spa/utils/hook.h>
+
+#include "internal.h"
+
+struct module;
+struct pw_properties;
+
+struct module_info {
+ const char *name;
+
+ unsigned int load_once:1;
+
+ int (*prepare) (struct module *module);
+ int (*load) (struct module *module);
+ int (*unload) (struct module *module);
+
+ const struct spa_dict *properties;
+ size_t data_size;
+};
+
+#define DEFINE_MODULE_INFO(name) \
+ __attribute__((used)) \
+ __attribute__((retain)) \
+ __attribute__((section("pw_mod_pulse_modules"))) \
+ __attribute__((aligned(__alignof__(struct module_info)))) \
+ const struct module_info name
+
+struct module_events {
+#define VERSION_MODULE_EVENTS 0
+ uint32_t version;
+
+ void (*loaded) (void *data, int result);
+ void (*destroy) (void *data);
+};
+
+struct module {
+ uint32_t index;
+ const char *args;
+ struct pw_properties *props;
+ struct impl *impl;
+ const struct module_info *info;
+ struct spa_hook_list listener_list;
+ void *user_data;
+ unsigned int loaded:1;
+ unsigned int unloading:1;
+};
+
+#define module_emit_loaded(m,r) spa_hook_list_call(&m->listener_list, struct module_events, loaded, 0, r)
+#define module_emit_destroy(m) spa_hook_list_call(&(m)->listener_list, struct module_events, destroy, 0)
+
+struct module *module_create(struct impl *impl, const char *name, const char *args);
+void module_free(struct module *module);
+int module_load(struct module *module);
+int module_unload(struct module *module);
+void module_schedule_unload(struct module *module);
+
+void module_add_listener(struct module *module,
+ struct spa_hook *listener,
+ const struct module_events *events, void *data);
+
+void module_args_add_props(struct pw_properties *props, const char *str);
+int module_args_to_audioinfo(struct impl *impl, struct pw_properties *props, struct spa_audio_info_raw *info);
+bool module_args_parse_bool(const char *str);
+
+#endif
diff --git a/src/modules/module-protocol-pulse/modules/module-always-sink.c b/src/modules/module-protocol-pulse/modules/module-always-sink.c
new file mode 100644
index 0000000..7549c09
--- /dev/null
+++ b/src/modules/module-protocol-pulse/modules/module-always-sink.c
@@ -0,0 +1,122 @@
+/* PipeWire
+ *
+ * Copyright © 2022 Wim Taymans <wim.taymans@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <pipewire/pipewire.h>
+
+#include "../module.h"
+
+#define NAME "always-sink"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+struct module_always_sink_data {
+ struct module *module;
+
+ struct pw_impl_module *mod;
+ struct spa_hook mod_listener;
+};
+
+static void module_destroy(void *data)
+{
+ struct module_always_sink_data *d = data;
+ spa_hook_remove(&d->mod_listener);
+ d->mod = NULL;
+ module_schedule_unload(d->module);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy
+};
+
+static int module_always_sink_load(struct module *module)
+{
+ struct module_always_sink_data *data = module->user_data;
+ FILE *f;
+ char *args;
+ const char *str;
+ size_t size;
+
+ if ((f = open_memstream(&args, &size)) == NULL)
+ return -errno;
+
+ fprintf(f, "{");
+ if ((str = pw_properties_get(module->props, "sink_name")) != NULL)
+ fprintf(f, " sink.name = \"%s\"", str);
+ fprintf(f, " }");
+ fclose(f);
+
+ data->mod = pw_context_load_module(module->impl->context,
+ "libpipewire-module-fallback-sink",
+ args, NULL);
+ free(args);
+
+ if (data->mod == NULL)
+ return -errno;
+
+ pw_impl_module_add_listener(data->mod,
+ &data->mod_listener,
+ &module_events, data);
+ return 0;
+}
+
+static int module_always_sink_unload(struct module *module)
+{
+ struct module_always_sink_data *d = module->user_data;
+
+ if (d->mod) {
+ spa_hook_remove(&d->mod_listener);
+ pw_impl_module_destroy(d->mod);
+ d->mod = NULL;
+ }
+ return 0;
+}
+
+static const struct spa_dict_item module_always_sink_info[] = {
+ { PW_KEY_MODULE_AUTHOR, "Pauli Virtanen <pav@iki.fi>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Always keeps at least one sink loaded even if it's a null one" },
+ { PW_KEY_MODULE_USAGE, "sink_name=<name of sink>" },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+static int module_always_sink_prepare(struct module * const module)
+{
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ struct module_always_sink_data * const data = module->user_data;
+ data->module = module;
+
+ return 0;
+}
+
+DEFINE_MODULE_INFO(module_always_sink) = {
+ .name = "module-always-sink",
+ .load_once = true,
+ .prepare = module_always_sink_prepare,
+ .load = module_always_sink_load,
+ .unload = module_always_sink_unload,
+ .properties = &SPA_DICT_INIT_ARRAY(module_always_sink_info),
+ .data_size = sizeof(struct module_always_sink_data),
+};
diff --git a/src/modules/module-protocol-pulse/modules/module-combine-sink.c b/src/modules/module-protocol-pulse/modules/module-combine-sink.c
new file mode 100644
index 0000000..86536f7
--- /dev/null
+++ b/src/modules/module-protocol-pulse/modules/module-combine-sink.c
@@ -0,0 +1,341 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans <wim.taymans@gmail.com>
+ * Copyright © 2021 Arun Raghavan <arun@asymptotic.io>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <spa/param/audio/format-utils.h>
+#include <spa/utils/json.h>
+
+#include <pipewire/pipewire.h>
+#include <pipewire/utils.h>
+
+#include "../manager.h"
+#include "../module.h"
+
+#define NAME "combine-sink"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+#define MAX_SINKS 64 /* ... good enough for anyone */
+
+#define TIMEOUT_SINKS_MSEC 2000
+
+static const struct spa_dict_item module_combine_sink_info[] = {
+ { PW_KEY_MODULE_AUTHOR, "Arun Raghavan <arun@asymptotic.io>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Combine multiple sinks into a single sink" },
+ { PW_KEY_MODULE_USAGE, "sink_name=<name of the sink> "
+ "sink_properties=<properties for the sink> "
+ /* not a great name, but for backwards compatibility... */
+ "slaves=<sinks to combine> "
+ "rate=<sample rate> "
+ "channels=<number of channels> "
+ "channel_map=<channel map> "
+ "remix=<remix channels> " },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+struct module_combine_sink_data;
+
+struct module_combine_sink_data {
+ struct module *module;
+
+ struct pw_core *core;
+ struct spa_hook core_listener;
+ struct pw_manager *manager;
+ struct spa_hook manager_listener;
+
+ struct pw_impl_module *mod;
+ struct spa_hook mod_listener;
+
+ char *sink_name;
+ char **sink_names;
+ struct pw_properties *combine_props;
+
+ struct spa_source *sinks_timeout;
+
+ struct spa_audio_info_raw info;
+
+ unsigned int sinks_pending;
+ unsigned int remix:1;
+ unsigned int load_emitted:1;
+ unsigned int start_error:1;
+};
+
+static void check_initialized(struct module_combine_sink_data *data)
+{
+ struct module *module = data->module;
+
+ if (data->load_emitted)
+ return;
+
+ if (data->start_error) {
+ pw_log_debug("module load error");
+ data->load_emitted = true;
+ module_emit_loaded(module, -EIO);
+ } else if (data->sinks_pending == 0) {
+ pw_log_debug("module loaded");
+ data->load_emitted = true;
+ module_emit_loaded(module, 0);
+ }
+}
+
+static void manager_added(void *d, struct pw_manager_object *o)
+{
+ struct module_combine_sink_data *data = d;
+ const char *str;
+ uint32_t val = 0;
+ struct pw_node_info *info;
+
+ if (!spa_streq(o->type, PW_TYPE_INTERFACE_Node) ||
+ (info = o->info) == NULL || info->props == NULL)
+ return;
+
+ str = spa_dict_lookup(info->props, "pulse.module.id");
+ if (str == NULL || !spa_atou32(str, &val, 0) || val != data->module->index)
+ return;
+
+ pw_log_info("found our %s, pending:%d",
+ pw_properties_get(o->props, PW_KEY_NODE_NAME),
+ data->sinks_pending);
+
+ if (!pw_manager_object_is_sink(o)) {
+ if (data->sinks_pending > 0)
+ data->sinks_pending--;
+ }
+ check_initialized(data);
+ return;
+}
+
+static const struct pw_manager_events manager_events = {
+ PW_VERSION_MANAGER_EVENTS,
+ .added = manager_added,
+};
+
+static void on_sinks_timeout(void *d, uint64_t count)
+{
+ struct module_combine_sink_data *data = d;
+
+ if (data->load_emitted)
+ return;
+
+ data->start_error = true;
+ check_initialized(data);
+}
+
+static void module_destroy(void *data)
+{
+ struct module_combine_sink_data *d = data;
+ spa_hook_remove(&d->mod_listener);
+ d->mod = NULL;
+ module_schedule_unload(d->module);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy
+};
+
+static int module_combine_sink_load(struct module *module)
+{
+ struct module_combine_sink_data *data = module->user_data;
+ uint32_t i;
+ FILE *f;
+ char *args;
+ size_t size;
+
+ data->core = pw_context_connect(module->impl->context, NULL, 0);
+ if (data->core == NULL)
+ return -errno;
+
+ if ((f = open_memstream(&args, &size)) == NULL)
+ return -errno;
+
+ fprintf(f, "{");
+ fprintf(f, " node.name = %s", data->sink_name);
+ fprintf(f, " node.description = %s", data->sink_name);
+ if (data->info.rate != 0)
+ fprintf(f, " audio.rate = %u", data->info.rate);
+ if (data->info.channels != 0) {
+ fprintf(f, " audio.channels = %u", data->info.channels);
+ if (!(data->info.flags & SPA_AUDIO_FLAG_UNPOSITIONED)) {
+ fprintf(f, " audio.position = [ ");
+ for (i = 0; i < data->info.channels; i++)
+ fprintf(f, "%s%s", i == 0 ? "" : ",",
+ channel_id2name(data->info.position[i]));
+ fprintf(f, " ]");
+ }
+ }
+ fprintf(f, " combine.props = {");
+ fprintf(f, " pulse.module.id = %u", module->index);
+ pw_properties_serialize_dict(f, &data->combine_props->dict, 0);
+ fprintf(f, " } stream.props = {");
+ if (!data->remix)
+ fprintf(f, " "PW_KEY_STREAM_DONT_REMIX" = true");
+ fprintf(f, " pulse.module.id = %u", module->index);
+ fprintf(f, " } stream.rules = [");
+ if (data->sink_names == NULL) {
+ fprintf(f, " { matches = [ { media.class = \"Audio/Sink\" } ]");
+ fprintf(f, " actions = { create-stream = { } } }");
+ } else {
+ for (i = 0; data->sink_names[i] != NULL; i++) {
+ char name[1024];
+ spa_json_encode_string(name, sizeof(name)-1, data->sink_names[i]);
+ fprintf(f, " { matches = [ { media.class = \"Audio/Sink\" ");
+ fprintf(f, " node.name = %s } ]", name);
+ fprintf(f, " actions = { create-stream = { } } }");
+ }
+ }
+ fprintf(f, " ]");
+ fprintf(f, "}");
+ fclose(f);
+
+ data->mod = pw_context_load_module(module->impl->context,
+ "libpipewire-module-combine-stream",
+ args, NULL);
+ free(args);
+
+ if (data->mod == NULL)
+ return -errno;
+
+ pw_impl_module_add_listener(data->mod,
+ &data->mod_listener,
+ &module_events, data);
+
+ data->manager = pw_manager_new(data->core);
+ if (data->manager == NULL)
+ return -errno;
+
+ pw_manager_add_listener(data->manager, &data->manager_listener,
+ &manager_events, data);
+
+ data->sinks_timeout = pw_loop_add_timer(module->impl->loop, on_sinks_timeout, data);
+ if (data->sinks_timeout) {
+ struct timespec timeout = {0};
+ timeout.tv_sec = TIMEOUT_SINKS_MSEC / 1000;
+ timeout.tv_nsec = (TIMEOUT_SINKS_MSEC % 1000) * SPA_NSEC_PER_MSEC;
+ pw_loop_update_timer(module->impl->loop, data->sinks_timeout, &timeout, NULL, false);
+ }
+ return data->load_emitted ? 0 : SPA_RESULT_RETURN_ASYNC(0);
+}
+
+static int module_combine_sink_unload(struct module *module)
+{
+ struct module_combine_sink_data *d = module->user_data;
+
+ if (d->sinks_timeout != NULL)
+ pw_loop_destroy_source(module->impl->loop, d->sinks_timeout);
+
+ if (d->mod != NULL) {
+ spa_hook_remove(&d->mod_listener);
+ pw_impl_module_destroy(d->mod);
+ d->mod = NULL;
+ }
+ if (d->manager != NULL) {
+ spa_hook_remove(&d->manager_listener);
+ pw_manager_destroy(d->manager);
+ }
+ if (d->core != NULL) {
+ spa_hook_remove(&d->core_listener);
+ pw_core_disconnect(d->core);
+ }
+ pw_free_strv(d->sink_names);
+ free(d->sink_name);
+ pw_properties_free(d->combine_props);
+ return 0;
+}
+
+static int module_combine_sink_prepare(struct module * const module)
+{
+ struct module_combine_sink_data * const d = module->user_data;
+ struct pw_properties * const props = module->props;
+ struct pw_properties *combine_props = NULL;
+ const char *str;
+ char *sink_name = NULL, **sink_names = NULL;
+ struct spa_audio_info_raw info = { 0 };
+ int res;
+ int num_sinks = 0;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ combine_props = pw_properties_new(NULL, NULL);
+
+ if ((str = pw_properties_get(props, "sink_name")) != NULL) {
+ sink_name = strdup(str);
+ pw_properties_set(props, "sink_name", NULL);
+ } else {
+ sink_name = strdup("combined");
+ }
+
+ if ((str = pw_properties_get(module->props, "sink_properties")) != NULL)
+ module_args_add_props(combine_props, str);
+
+ if ((str = pw_properties_get(props, "slaves")) != NULL) {
+ sink_names = pw_split_strv(str, ",", MAX_SINKS, &num_sinks);
+ pw_properties_set(props, "slaves", NULL);
+ }
+ d->remix = true;
+ if ((str = pw_properties_get(props, "remix")) != NULL) {
+ d->remix = pw_properties_parse_bool(str);
+ pw_properties_set(props, "remix", NULL);
+ }
+
+ if ((str = pw_properties_get(props, "adjust_time")) != NULL) {
+ pw_log_info("The `adjust_time` modarg is ignored");
+ pw_properties_set(props, "adjust_time", NULL);
+ }
+
+ if ((str = pw_properties_get(props, "resample_method")) != NULL) {
+ pw_log_info("The `resample_method` modarg is ignored");
+ pw_properties_set(props, "resample_method", NULL);
+ }
+
+ if (module_args_to_audioinfo(module->impl, props, &info) < 0) {
+ res = -EINVAL;
+ goto out;
+ }
+
+ d->module = module;
+ d->info = info;
+ d->sink_name = sink_name;
+ d->sink_names = sink_names;
+ d->sinks_pending = (sink_names == NULL) ? 0 : num_sinks;
+ d->combine_props = combine_props;
+
+ return 0;
+out:
+ free(sink_name);
+ pw_free_strv(sink_names);
+ pw_properties_free(combine_props);
+
+ return res;
+}
+
+DEFINE_MODULE_INFO(module_combine_sink) = {
+ .name = "module-combine-sink",
+ .prepare = module_combine_sink_prepare,
+ .load = module_combine_sink_load,
+ .unload = module_combine_sink_unload,
+ .properties = &SPA_DICT_INIT_ARRAY(module_combine_sink_info),
+ .data_size = sizeof(struct module_combine_sink_data),
+};
diff --git a/src/modules/module-protocol-pulse/modules/module-echo-cancel.c b/src/modules/module-protocol-pulse/modules/module-echo-cancel.c
new file mode 100644
index 0000000..c2ab8ad
--- /dev/null
+++ b/src/modules/module-protocol-pulse/modules/module-echo-cancel.c
@@ -0,0 +1,276 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans <wim.taymans@gmail.com>
+ * Copyright © 2021 Arun Raghavan <arun@asymptotic.io>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <spa/param/audio/format-utils.h>
+#include <spa/utils/hook.h>
+#include <spa/utils/json.h>
+#include <pipewire/pipewire.h>
+
+#include "../defs.h"
+#include "../module.h"
+
+#define NAME "echo-cancel"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+struct module_echo_cancel_data {
+ struct module *module;
+
+ struct pw_impl_module *mod;
+ struct spa_hook mod_listener;
+
+ struct pw_properties *props;
+ struct pw_properties *capture_props;
+ struct pw_properties *source_props;
+ struct pw_properties *sink_props;
+ struct pw_properties *playback_props;
+
+ struct spa_audio_info_raw info;
+};
+
+static void module_destroy(void *data)
+{
+ struct module_echo_cancel_data *d = data;
+ spa_hook_remove(&d->mod_listener);
+ d->mod = NULL;
+ module_schedule_unload(d->module);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy
+};
+
+static int module_echo_cancel_load(struct module *module)
+{
+ struct module_echo_cancel_data *data = module->user_data;
+ FILE *f;
+ const char *str;
+ char *args;
+ size_t size;
+ uint32_t i;
+
+ if ((f = open_memstream(&args, &size)) == NULL)
+ return -errno;
+
+ fprintf(f, "{");
+ /* Can't just serialise this dict because the "null" method gets
+ * interpreted as a JSON null */
+ if ((str = pw_properties_get(data->props, "aec.method")))
+ fprintf(f, " aec.method = \"%s\"", str);
+ if ((str = pw_properties_get(data->props, "aec.args")))
+ fprintf(f, " aec.args = \"%s\"", str);
+ if (data->info.rate != 0)
+ fprintf(f, " audio.rate = %u", data->info.rate);
+ if (data->info.channels != 0) {
+ fprintf(f, " audio.channels = %u", data->info.channels);
+ if (!(data->info.flags & SPA_AUDIO_FLAG_UNPOSITIONED)) {
+ fprintf(f, " audio.position = [ ");
+ for (i = 0; i < data->info.channels; i++)
+ fprintf(f, "%s%s", i == 0 ? "" : ",",
+ channel_id2name(data->info.position[i]));
+ fprintf(f, " ]");
+ }
+ }
+ fprintf(f, " capture.props = {");
+ pw_properties_serialize_dict(f, &data->capture_props->dict, 0);
+ fprintf(f, " } source.props = {");
+ pw_properties_serialize_dict(f, &data->source_props->dict, 0);
+ fprintf(f, " } sink.props = {");
+ pw_properties_serialize_dict(f, &data->sink_props->dict, 0);
+ fprintf(f, " } playback.props = {");
+ pw_properties_serialize_dict(f, &data->playback_props->dict, 0);
+ fprintf(f, " } }");
+ fclose(f);
+
+ data->mod = pw_context_load_module(module->impl->context,
+ "libpipewire-module-echo-cancel",
+ args, NULL);
+ free(args);
+
+ if (data->mod == NULL)
+ return -errno;
+
+ pw_impl_module_add_listener(data->mod,
+ &data->mod_listener,
+ &module_events, data);
+
+ return 0;
+}
+
+static int module_echo_cancel_unload(struct module *module)
+{
+ struct module_echo_cancel_data *d = module->user_data;
+
+ if (d->mod) {
+ spa_hook_remove(&d->mod_listener);
+ pw_impl_module_destroy(d->mod);
+ d->mod = NULL;
+ }
+
+ pw_properties_free(d->props);
+ pw_properties_free(d->capture_props);
+ pw_properties_free(d->source_props);
+ pw_properties_free(d->sink_props);
+ pw_properties_free(d->playback_props);
+
+ return 0;
+}
+
+static const struct spa_dict_item module_echo_cancel_info[] = {
+ { PW_KEY_MODULE_AUTHOR, "Arun Raghavan <arun@asymptotic.io>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Acoustic echo canceller" },
+ { PW_KEY_MODULE_USAGE, "source_name=<name for the source> "
+ "source_properties=<properties for the source> "
+ "source_master=<name of source to filter> "
+ "sink_name=<name for the sink> "
+ "sink_properties=<properties for the sink> "
+ "sink_master=<name of sink to filter> "
+ "rate=<sample rate> "
+ "channels=<number of channels> "
+ "channel_map=<channel map> "
+ "aec_method=<implementation to use> "
+ "aec_args=<parameters for the AEC engine> "
+#if 0
+ /* These are not implemented because they don't
+ * really make sense in the PipeWire context */
+ "format=<sample format> "
+ "adjust_time=<how often to readjust rates in s> "
+ "adjust_threshold=<how much drift to readjust after in ms> "
+ "autoloaded=<set if this module is being loaded automatically> "
+ "save_aec=<save AEC data in /tmp> "
+ "use_volume_sharing=<yes or no> "
+ "use_master_format=<yes or no> "
+#endif
+ },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+static int module_echo_cancel_prepare(struct module * const module)
+{
+ struct module_echo_cancel_data * const d = module->user_data;
+ struct pw_properties * const props = module->props;
+ struct pw_properties *aec_props = NULL, *sink_props = NULL, *source_props = NULL;
+ struct pw_properties *playback_props = NULL, *capture_props = NULL;
+ const char *str;
+ struct spa_audio_info_raw info = { 0 };
+ int res;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ aec_props = pw_properties_new(NULL, NULL);
+ capture_props = pw_properties_new(NULL, NULL);
+ source_props = pw_properties_new(NULL, NULL);
+ sink_props = pw_properties_new(NULL, NULL);
+ playback_props = pw_properties_new(NULL, NULL);
+ if (!aec_props || !source_props || !sink_props || !capture_props || !playback_props) {
+ res = -EINVAL;
+ goto out;
+ }
+
+ if ((str = pw_properties_get(props, "source_name")) != NULL) {
+ pw_properties_set(source_props, PW_KEY_NODE_NAME, str);
+ pw_properties_set(props, "source_name", NULL);
+ } else {
+ pw_properties_set(source_props, PW_KEY_NODE_NAME, "echo-cancel-source");
+ }
+
+ if ((str = pw_properties_get(props, "sink_name")) != NULL) {
+ pw_properties_set(sink_props, PW_KEY_NODE_NAME, str);
+ pw_properties_set(props, "sink_name", NULL);
+ } else {
+ pw_properties_set(sink_props, PW_KEY_NODE_NAME, "echo-cancel-sink");
+ }
+
+ if ((str = pw_properties_get(props, "source_master")) != NULL) {
+ if (spa_strendswith(str, ".monitor")) {
+ pw_properties_setf(capture_props, PW_KEY_TARGET_OBJECT,
+ "%.*s", (int)strlen(str)-8, str);
+ pw_properties_set(capture_props, PW_KEY_STREAM_CAPTURE_SINK,
+ "true");
+ } else {
+ pw_properties_set(capture_props, PW_KEY_TARGET_OBJECT, str);
+ }
+ pw_properties_set(props, "source_master", NULL);
+ }
+
+ if ((str = pw_properties_get(props, "sink_master")) != NULL) {
+ pw_properties_set(playback_props, PW_KEY_TARGET_OBJECT, str);
+ pw_properties_set(props, "sink_master", NULL);
+ }
+
+ if (module_args_to_audioinfo(module->impl, props, &info) < 0) {
+ res = -EINVAL;
+ goto out;
+ }
+
+ if ((str = pw_properties_get(props, "source_properties")) != NULL) {
+ module_args_add_props(source_props, str);
+ pw_properties_set(props, "source_properties", NULL);
+ }
+
+ if ((str = pw_properties_get(props, "sink_properties")) != NULL) {
+ module_args_add_props(sink_props, str);
+ pw_properties_set(props, "sink_properties", NULL);
+ }
+
+ if ((str = pw_properties_get(props, "aec_method")) != NULL) {
+ pw_properties_set(aec_props, "aec.method", str);
+ pw_properties_set(props, "aec_method", NULL);
+ }
+
+ if ((str = pw_properties_get(props, "aec_args")) != NULL) {
+ pw_properties_set(aec_props, "aec.args", str);
+ pw_properties_set(props, "aec_args", NULL);
+ }
+
+ d->module = module;
+ d->props = aec_props;
+ d->capture_props = capture_props;
+ d->source_props = source_props;
+ d->sink_props = sink_props;
+ d->playback_props = playback_props;
+ d->info = info;
+
+ return 0;
+out:
+ pw_properties_free(aec_props);
+ pw_properties_free(playback_props);
+ pw_properties_free(sink_props);
+ pw_properties_free(source_props);
+ pw_properties_free(capture_props);
+
+ return res;
+}
+
+DEFINE_MODULE_INFO(module_echo_cancel) = {
+ .name = "module-echo-cancel",
+ .prepare = module_echo_cancel_prepare,
+ .load = module_echo_cancel_load,
+ .unload = module_echo_cancel_unload,
+ .properties = &SPA_DICT_INIT_ARRAY(module_echo_cancel_info),
+ .data_size = sizeof(struct module_echo_cancel_data),
+};
diff --git a/src/modules/module-protocol-pulse/modules/module-gsettings.c b/src/modules/module-protocol-pulse/modules/module-gsettings.c
new file mode 100644
index 0000000..36e216b
--- /dev/null
+++ b/src/modules/module-protocol-pulse/modules/module-gsettings.c
@@ -0,0 +1,298 @@
+/* PipeWire
+ *
+ * Copyright © 2022 Wim Taymans <wim.taymans@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <gio/gio.h>
+#include <glib.h>
+
+#include <spa/debug/mem.h>
+#include <pipewire/pipewire.h>
+#include <pipewire/thread.h>
+
+#include "../module.h"
+
+#define NAME "gsettings"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+#define PA_GSETTINGS_MODULE_GROUP_SCHEMA "org.freedesktop.pulseaudio.module-group"
+#define PA_GSETTINGS_MODULE_GROUPS_SCHEMA "org.freedesktop.pulseaudio.module-groups"
+#define PA_GSETTINGS_MODULE_GROUPS_PATH "/org/freedesktop/pulseaudio/module-groups/"
+
+#define MAX_MODULES 10
+
+struct module_gsettings_data {
+ struct module *module;
+
+ GMainContext *context;
+ GMainLoop *loop;
+ struct spa_thread *thr;
+
+ GSettings *settings;
+ gchar **group_names;
+
+ struct spa_list groups;
+};
+
+struct group {
+ struct spa_list link;
+ char *name;
+ struct module *module;
+ struct spa_hook module_listener;
+};
+
+struct info {
+ bool enabled;
+ char *name;
+ char *module[MAX_MODULES];
+ char *args[MAX_MODULES];
+};
+
+static void clean_info(const struct info *info)
+{
+ int i;
+ for (i = 0; i < MAX_MODULES; i++) {
+ g_free(info->module[i]);
+ g_free(info->args[i]);
+ }
+ g_free(info->name);
+}
+
+static void unload_module(struct module_gsettings_data *d, struct group *g)
+{
+ spa_list_remove(&g->link);
+ g_free(g->name);
+ if (g->module)
+ module_unload(g->module);
+ free(g);
+}
+
+static void unload_group(struct module_gsettings_data *d, const char *name)
+{
+ struct group *g, *t;
+ spa_list_for_each_safe(g, t, &d->groups, link) {
+ if (spa_streq(g->name, name))
+ unload_module(d, g);
+ }
+}
+static void module_destroy(void *data)
+{
+ struct group *g = data;
+ if (g->module) {
+ spa_hook_remove(&g->module_listener);
+ g->module = NULL;
+ }
+}
+
+static const struct module_events module_gsettings_events = {
+ VERSION_MODULE_EVENTS,
+ .destroy = module_destroy
+};
+
+static int load_group(struct module_gsettings_data *d, const struct info *info)
+{
+ struct group *g;
+ int i, res;
+
+ for (i = 0; i < MAX_MODULES; i++) {
+ if (info->module[i] == NULL || strlen(info->module[i]) <= 0)
+ break;
+
+ g = calloc(1, sizeof(struct group));
+ if (g == NULL)
+ return -errno;
+
+ g->name = strdup(info->name);
+ g->module = module_create(d->module->impl, info->module[i], info->args[i]);
+ if (g->module == NULL) {
+ pw_log_info("can't create module:%s args:%s: %m",
+ info->module[i], info->args[i]);
+ } else {
+ module_add_listener(g->module, &g->module_listener,
+ &module_gsettings_events, g);
+ if ((res = module_load(g->module)) < 0) {
+ pw_log_warn("can't load module:%s args:%s: %s",
+ info->module[i], info->args[i],
+ spa_strerror(res));
+ }
+ }
+ spa_list_append(&d->groups, &g->link);
+ }
+ return 0;
+}
+
+static int
+do_handle_info(struct spa_loop *loop,
+ bool async, uint32_t seq, const void *data, size_t size, void *user_data)
+{
+ struct module_gsettings_data *d = user_data;
+ const struct info *info = data;
+
+ unload_group(d, info->name);
+ if (info->enabled)
+ load_group(d, info);
+
+ clean_info(info);
+ return 0;
+}
+
+static void handle_module_group(struct module_gsettings_data *d, gchar *name)
+{
+ struct impl *impl = d->module->impl;
+ GSettings *settings;
+ gchar p[1024];
+ struct info info;
+ int i;
+
+ snprintf(p, sizeof(p), PA_GSETTINGS_MODULE_GROUPS_PATH"%s/", name);
+
+ settings = g_settings_new_with_path(PA_GSETTINGS_MODULE_GROUP_SCHEMA, p);
+ if (settings == NULL)
+ return;
+
+ spa_zero(info);
+ info.name = strdup(p);
+ info.enabled = g_settings_get_boolean(settings, "enabled");
+
+ for (i = 0; i < MAX_MODULES; i++) {
+ snprintf(p, sizeof(p), "name%d", i);
+ info.module[i] = g_settings_get_string(settings, p);
+
+ snprintf(p, sizeof(p), "args%i", i);
+ info.args[i] = g_settings_get_string(settings, p);
+ }
+ pw_loop_invoke(impl->loop, do_handle_info, 0,
+ &info, sizeof(info), false, d);
+
+ g_object_unref(G_OBJECT(settings));
+}
+
+static void module_group_callback(GSettings *settings, gchar *key, gpointer user_data)
+{
+ struct module_gsettings_data *d = g_object_get_data(G_OBJECT(settings), "module-data");
+ handle_module_group(d, user_data);
+}
+
+static void *do_loop(void *user_data)
+{
+ struct module_gsettings_data *d = user_data;
+
+ pw_log_info("enter");
+ g_main_context_push_thread_default(d->context);
+
+ d->loop = g_main_loop_new(d->context, FALSE);
+
+ g_main_loop_run(d->loop);
+
+ g_main_context_pop_thread_default(d->context);
+ g_main_loop_unref (d->loop);
+ d->loop = NULL;
+ pw_log_info("leave");
+
+ return NULL;
+}
+
+static int module_gsettings_load(struct module *module)
+{
+ struct module_gsettings_data *data = module->user_data;
+ gchar **name;
+
+ data->context = g_main_context_new();
+ g_main_context_push_thread_default(data->context);
+
+ data->settings = g_settings_new(PA_GSETTINGS_MODULE_GROUPS_SCHEMA);
+ if (data->settings == NULL)
+ return -EIO;
+
+ data->group_names = g_settings_list_children(data->settings);
+
+ for (name = data->group_names; *name; name++) {
+ GSettings *child = g_settings_get_child(data->settings, *name);
+ /* The child may have been removed between the
+ * g_settings_list_children() and g_settings_get_child() calls. */
+ if (child == NULL)
+ continue;
+
+ g_object_set_data(G_OBJECT(child), "module-data", data);
+ g_signal_connect(child, "changed", (GCallback) module_group_callback, *name);
+ handle_module_group(data, *name);
+ }
+ g_main_context_pop_thread_default(data->context);
+
+ data->thr = pw_thread_utils_create(NULL, do_loop, data);
+ return 0;
+}
+
+static gboolean
+do_stop(gpointer data)
+{
+ struct module_gsettings_data *d = data;
+ if (d->loop)
+ g_main_loop_quit(d->loop);
+ return FALSE;
+}
+
+static int module_gsettings_unload(struct module *module)
+{
+ struct module_gsettings_data *d = module->user_data;
+ struct group *g;
+
+ g_main_context_invoke(d->context, do_stop, d);
+ pw_thread_utils_join(d->thr, NULL);
+ g_main_context_unref(d->context);
+
+ spa_list_consume(g, &d->groups, link)
+ unload_module(d, g);
+
+ g_strfreev(d->group_names);
+ g_object_unref(G_OBJECT(d->settings));
+ return 0;
+}
+
+static int module_gsettings_prepare(struct module * const module)
+{
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ struct module_gsettings_data * const data = module->user_data;
+ spa_list_init(&data->groups);
+ data->module = module;
+
+ return 0;
+}
+
+static const struct spa_dict_item module_gsettings_info[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "GSettings Adapter" },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+DEFINE_MODULE_INFO(module_gsettings) = {
+ .name = "module-gsettings",
+ .load_once = true,
+ .prepare = module_gsettings_prepare,
+ .load = module_gsettings_load,
+ .unload = module_gsettings_unload,
+ .properties = &SPA_DICT_INIT_ARRAY(module_gsettings_info),
+ .data_size = sizeof(struct module_gsettings_data),
+};
diff --git a/src/modules/module-protocol-pulse/modules/module-ladspa-sink.c b/src/modules/module-protocol-pulse/modules/module-ladspa-sink.c
new file mode 100644
index 0000000..3b3b700
--- /dev/null
+++ b/src/modules/module-protocol-pulse/modules/module-ladspa-sink.c
@@ -0,0 +1,257 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans <wim.taymans@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <spa/utils/hook.h>
+#include <spa/utils/json.h>
+#include <spa/param/audio/format-utils.h>
+
+#include <pipewire/pipewire.h>
+
+#include "../defs.h"
+#include "../module.h"
+
+#define NAME "ladspa-sink"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+struct module_ladspa_sink_data {
+ struct module *module;
+
+ struct pw_impl_module *mod;
+ struct spa_hook mod_listener;
+
+ struct pw_properties *capture_props;
+ struct pw_properties *playback_props;
+};
+
+static void module_destroy(void *data)
+{
+ struct module_ladspa_sink_data *d = data;
+ spa_hook_remove(&d->mod_listener);
+ d->mod = NULL;
+ module_schedule_unload(d->module);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy
+};
+
+static int module_ladspa_sink_load(struct module *module)
+{
+ struct module_ladspa_sink_data *data = module->user_data;
+ FILE *f;
+ char *args;
+ const char *str, *plugin, *label;
+ size_t size;
+
+ if ((plugin = pw_properties_get(module->props, "plugin")) == NULL)
+ return -EINVAL;
+ if ((label = pw_properties_get(module->props, "label")) == NULL)
+ return -EINVAL;
+
+ pw_properties_setf(data->capture_props, PW_KEY_NODE_GROUP, "ladspa-sink-%u", module->index);
+ pw_properties_setf(data->playback_props, PW_KEY_NODE_GROUP, "ladspa-sink-%u", module->index);
+ pw_properties_setf(data->capture_props, "pulse.module.id", "%u", module->index);
+ pw_properties_setf(data->playback_props, "pulse.module.id", "%u", module->index);
+
+ if ((f = open_memstream(&args, &size)) == NULL)
+ return -errno;
+
+ fprintf(f, "{");
+ pw_properties_serialize_dict(f, &module->props->dict, 0);
+ fprintf(f, " filter.graph = {");
+ fprintf(f, " nodes = [ { ");
+ fprintf(f, " type = ladspa ");
+ fprintf(f, " plugin = \"%s\" ", plugin);
+ fprintf(f, " label = \"%s\" ", label);
+ if ((str = pw_properties_get(module->props, "control")) != NULL) {
+ size_t len;
+ const char *s, *state = NULL;
+ int count = 0;
+
+ fprintf(f, " control = {");
+ while ((s = pw_split_walk(str, ", ", &len, &state))) {
+ fprintf(f, " \"%d\" = %.*s", count, (int)len, s);
+ count++;
+ }
+ fprintf(f, " }");
+ }
+ fprintf(f, " } ]");
+ if ((str = pw_properties_get(module->props, "inputs")) != NULL)
+ fprintf(f, " inputs = [ %s ] ", str);
+ if ((str = pw_properties_get(module->props, "outputs")) != NULL)
+ fprintf(f, " outputs = [ %s ] ", str);
+ fprintf(f, " }");
+ fprintf(f, " capture.props = {");
+ pw_properties_serialize_dict(f, &data->capture_props->dict, 0);
+ fprintf(f, " } playback.props = {");
+ pw_properties_serialize_dict(f, &data->playback_props->dict, 0);
+ fprintf(f, " } }");
+ fclose(f);
+
+ data->mod = pw_context_load_module(module->impl->context,
+ "libpipewire-module-filter-chain",
+ args, NULL);
+ free(args);
+
+ if (data->mod == NULL)
+ return -errno;
+
+ pw_impl_module_add_listener(data->mod,
+ &data->mod_listener,
+ &module_events, data);
+
+ return 0;
+}
+
+static int module_ladspa_sink_unload(struct module *module)
+{
+ struct module_ladspa_sink_data *d = module->user_data;
+
+ if (d->mod) {
+ spa_hook_remove(&d->mod_listener);
+ pw_impl_module_destroy(d->mod);
+ d->mod = NULL;
+ }
+
+ pw_properties_free(d->capture_props);
+ pw_properties_free(d->playback_props);
+
+ return 0;
+}
+
+static const struct spa_dict_item module_ladspa_sink_info[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Virtual LADSPA sink" },
+ { PW_KEY_MODULE_USAGE,
+ "sink_name=<name for the sink> "
+ "sink_properties=<properties for the sink> "
+ "sink_input_properties=<properties for the sink input> "
+ "master=<name of sink to filter> "
+ "sink_master=<name of sink to filter> "
+ "format=<sample format> "
+ "rate=<sample rate> "
+ "channels=<number of channels> "
+ "channel_map=<input channel map> "
+ "plugin=<ladspa plugin name> "
+ "label=<ladspa plugin label> "
+ "control=<comma separated list of input control values> "
+ "input_ladspaport_map=<comma separated list of input LADSPA port names> "
+ "output_ladspaport_map=<comma separated list of output LADSPA port names> "},
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+static void position_to_props(struct spa_audio_info_raw *info, struct pw_properties *props)
+{
+ char *s, *p;
+ uint32_t i;
+
+ pw_properties_setf(props, SPA_KEY_AUDIO_CHANNELS, "%u", info->channels);
+ p = s = alloca(info->channels * 8);
+ for (i = 0; i < info->channels; i++)
+ p += spa_scnprintf(p, 8, "%s%s", i == 0 ? "" : ",",
+ channel_id2name(info->position[i]));
+ pw_properties_set(props, SPA_KEY_AUDIO_POSITION, s);
+}
+
+static int module_ladspa_sink_prepare(struct module * const module)
+{
+ struct module_ladspa_sink_data * const d = module->user_data;
+ struct pw_properties * const props = module->props;
+ struct pw_properties *playback_props = NULL, *capture_props = NULL;
+ const char *str;
+ struct spa_audio_info_raw capture_info = { 0 };
+ struct spa_audio_info_raw playback_info = { 0 };
+ int res;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ capture_props = pw_properties_new(NULL, NULL);
+ playback_props = pw_properties_new(NULL, NULL);
+ if (!capture_props || !playback_props) {
+ res = -EINVAL;
+ goto out;
+ }
+
+ if ((str = pw_properties_get(props, "sink_name")) != NULL) {
+ pw_properties_set(capture_props, PW_KEY_NODE_NAME, str);
+ pw_properties_set(props, "sink_name", NULL);
+ }
+ if ((str = pw_properties_get(props, "sink_properties")) != NULL) {
+ module_args_add_props(capture_props, str);
+ pw_properties_set(props, "sink_properties", NULL);
+ }
+ if (pw_properties_get(capture_props, PW_KEY_MEDIA_CLASS) == NULL)
+ pw_properties_set(capture_props, PW_KEY_MEDIA_CLASS, "Audio/Sink");
+ if (pw_properties_get(capture_props, PW_KEY_DEVICE_CLASS) == NULL)
+ pw_properties_set(capture_props, PW_KEY_DEVICE_CLASS, "filter");
+
+ if ((str = pw_properties_get(capture_props, PW_KEY_NODE_DESCRIPTION)) == NULL) {
+ str = pw_properties_get(capture_props, PW_KEY_NODE_NAME);
+ pw_properties_setf(props, PW_KEY_NODE_DESCRIPTION,
+ "%s Sink", str);
+ } else {
+ pw_properties_set(props, PW_KEY_NODE_DESCRIPTION, str);
+ }
+
+ if ((str = pw_properties_get(props, "master")) != NULL ||
+ (str = pw_properties_get(props, "sink_master")) != NULL) {
+ pw_properties_set(playback_props, PW_KEY_TARGET_OBJECT, str);
+ pw_properties_set(props, "master", NULL);
+ }
+
+ if (module_args_to_audioinfo(module->impl, props, &capture_info) < 0) {
+ res = -EINVAL;
+ goto out;
+ }
+ playback_info = capture_info;
+
+ position_to_props(&capture_info, capture_props);
+ position_to_props(&playback_info, playback_props);
+
+ if (pw_properties_get(playback_props, PW_KEY_NODE_PASSIVE) == NULL)
+ pw_properties_set(playback_props, PW_KEY_NODE_PASSIVE, "true");
+
+ d->module = module;
+ d->capture_props = capture_props;
+ d->playback_props = playback_props;
+
+ return 0;
+out:
+ pw_properties_free(playback_props);
+ pw_properties_free(capture_props);
+
+ return res;
+}
+
+DEFINE_MODULE_INFO(module_ladspa_sink) = {
+ .name = "module-ladspa-sink",
+ .prepare = module_ladspa_sink_prepare,
+ .load = module_ladspa_sink_load,
+ .unload = module_ladspa_sink_unload,
+ .properties = &SPA_DICT_INIT_ARRAY(module_ladspa_sink_info),
+ .data_size = sizeof(struct module_ladspa_sink_data),
+};
diff --git a/src/modules/module-protocol-pulse/modules/module-ladspa-source.c b/src/modules/module-protocol-pulse/modules/module-ladspa-source.c
new file mode 100644
index 0000000..9533350
--- /dev/null
+++ b/src/modules/module-protocol-pulse/modules/module-ladspa-source.c
@@ -0,0 +1,265 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans <wim.taymans@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <spa/utils/hook.h>
+#include <spa/utils/json.h>
+#include <spa/param/audio/format-utils.h>
+
+#include <pipewire/pipewire.h>
+
+#include "../defs.h"
+#include "../module.h"
+
+#define NAME "ladspa-source"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+struct module_ladspa_source_data {
+ struct module *module;
+
+ struct pw_impl_module *mod;
+ struct spa_hook mod_listener;
+
+ struct pw_properties *capture_props;
+ struct pw_properties *playback_props;
+};
+
+static void module_destroy(void *data)
+{
+ struct module_ladspa_source_data *d = data;
+ spa_hook_remove(&d->mod_listener);
+ d->mod = NULL;
+ module_schedule_unload(d->module);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy
+};
+
+static int module_ladspa_source_load(struct module *module)
+{
+ struct module_ladspa_source_data *data = module->user_data;
+ FILE *f;
+ char *args;
+ const char *str, *plugin, *label;
+ size_t size;
+
+ if ((plugin = pw_properties_get(module->props, "plugin")) == NULL)
+ return -EINVAL;
+ if ((label = pw_properties_get(module->props, "label")) == NULL)
+ return -EINVAL;
+
+ pw_properties_setf(data->capture_props, PW_KEY_NODE_GROUP, "ladspa-source-%u", module->index);
+ pw_properties_setf(data->playback_props, PW_KEY_NODE_GROUP, "ladspa-source-%u", module->index);
+ pw_properties_setf(data->capture_props, "pulse.module.id", "%u", module->index);
+ pw_properties_setf(data->playback_props, "pulse.module.id", "%u", module->index);
+
+ if ((f = open_memstream(&args, &size)) == NULL)
+ return -errno;
+
+ fprintf(f, "{");
+ pw_properties_serialize_dict(f, &module->props->dict, 0);
+ fprintf(f, " filter.graph = {");
+ fprintf(f, " nodes = [ { ");
+ fprintf(f, " type = ladspa ");
+ fprintf(f, " plugin = \"%s\" ", plugin);
+ fprintf(f, " label = \"%s\" ", label);
+ if ((str = pw_properties_get(module->props, "control")) != NULL) {
+ size_t len;
+ const char *s, *state = NULL;
+ int count = 0;
+
+ fprintf(f, " control = {");
+ while ((s = pw_split_walk(str, ", ", &len, &state))) {
+ fprintf(f, " \"%d\" = %.*s", count, (int)len, s);
+ count++;
+ }
+ fprintf(f, " }");
+ }
+ fprintf(f, " } ]");
+ if ((str = pw_properties_get(module->props, "inputs")) != NULL)
+ fprintf(f, " inputs = [ %s ] ", str);
+ if ((str = pw_properties_get(module->props, "outputs")) != NULL)
+ fprintf(f, " outputs = [ %s ] ", str);
+ fprintf(f, " }");
+ fprintf(f, " capture.props = {");
+ pw_properties_serialize_dict(f, &data->capture_props->dict, 0);
+ fprintf(f, " } playback.props = {");
+ pw_properties_serialize_dict(f, &data->playback_props->dict, 0);
+ fprintf(f, " } }");
+ fclose(f);
+
+ data->mod = pw_context_load_module(module->impl->context,
+ "libpipewire-module-filter-chain",
+ args, NULL);
+ free(args);
+
+ if (data->mod == NULL)
+ return -errno;
+
+ pw_impl_module_add_listener(data->mod,
+ &data->mod_listener,
+ &module_events, data);
+
+ return 0;
+}
+
+static int module_ladspa_source_unload(struct module *module)
+{
+ struct module_ladspa_source_data *d = module->user_data;
+
+ if (d->mod) {
+ spa_hook_remove(&d->mod_listener);
+ pw_impl_module_destroy(d->mod);
+ d->mod = NULL;
+ }
+
+ pw_properties_free(d->capture_props);
+ pw_properties_free(d->playback_props);
+
+ return 0;
+}
+
+static const struct spa_dict_item module_ladspa_source_info[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Virtual LADSPA source" },
+ { PW_KEY_MODULE_USAGE,
+ "source_name=<name for the source> "
+ "source_properties=<properties for the source> "
+ "source_output_properties=<properties for the source output> "
+ "master=<name of source to filter> "
+ "source_master=<name of source to filter> "
+ "format=<sample format> "
+ "rate=<sample rate> "
+ "channels=<number of channels> "
+ "channel_map=<input channel map> "
+ "plugin=<ladspa plugin name> "
+ "label=<ladspa plugin label> "
+ "control=<comma separated list of input control values> "
+ "input_ladspaport_map=<comma separated list of input LADSPA port names> "
+ "output_ladspaport_map=<comma separated list of output LADSPA port names> "},
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+static void position_to_props(struct spa_audio_info_raw *info, struct pw_properties *props)
+{
+ char *s, *p;
+ uint32_t i;
+
+ pw_properties_setf(props, SPA_KEY_AUDIO_CHANNELS, "%u", info->channels);
+ p = s = alloca(info->channels * 8);
+ for (i = 0; i < info->channels; i++)
+ p += spa_scnprintf(p, 8, "%s%s", i == 0 ? "" : ",",
+ channel_id2name(info->position[i]));
+ pw_properties_set(props, SPA_KEY_AUDIO_POSITION, s);
+}
+
+static int module_ladspa_source_prepare(struct module * const module)
+{
+ struct module_ladspa_source_data * const d = module->user_data;
+ struct pw_properties * const props = module->props;
+ struct pw_properties *playback_props = NULL, *capture_props = NULL;
+ const char *str;
+ struct spa_audio_info_raw capture_info = { 0 };
+ struct spa_audio_info_raw playback_info = { 0 };
+ int res;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ capture_props = pw_properties_new(NULL, NULL);
+ playback_props = pw_properties_new(NULL, NULL);
+ if (!capture_props || !playback_props) {
+ res = -EINVAL;
+ goto out;
+ }
+
+ if ((str = pw_properties_get(props, "source_name")) != NULL) {
+ pw_properties_set(playback_props, PW_KEY_NODE_NAME, str);
+ pw_properties_set(props, "source_name", NULL);
+ }
+ if ((str = pw_properties_get(props, "source_properties")) != NULL) {
+ module_args_add_props(playback_props, str);
+ pw_properties_set(props, "source_properties", NULL);
+ }
+ if (pw_properties_get(playback_props, PW_KEY_MEDIA_CLASS) == NULL)
+ pw_properties_set(playback_props, PW_KEY_MEDIA_CLASS, "Audio/Source");
+ if (pw_properties_get(playback_props, PW_KEY_DEVICE_CLASS) == NULL)
+ pw_properties_set(playback_props, PW_KEY_DEVICE_CLASS, "filter");
+
+ if ((str = pw_properties_get(playback_props, PW_KEY_NODE_DESCRIPTION)) == NULL) {
+ str = pw_properties_get(playback_props, PW_KEY_NODE_NAME);
+ pw_properties_setf(props, PW_KEY_NODE_DESCRIPTION,
+ "%s Source", str);
+ } else {
+ pw_properties_set(props, PW_KEY_NODE_DESCRIPTION, str);
+ }
+
+ if ((str = pw_properties_get(props, "master")) != NULL ||
+ (str = pw_properties_get(props, "source_master")) != NULL) {
+ if (spa_strendswith(str, ".monitor")) {
+ pw_properties_setf(capture_props, PW_KEY_TARGET_OBJECT,
+ "%.*s", (int)strlen(str)-8, str);
+ pw_properties_set(capture_props, PW_KEY_STREAM_CAPTURE_SINK,
+ "true");
+ } else {
+ pw_properties_set(capture_props, PW_KEY_TARGET_OBJECT, str);
+ }
+ pw_properties_set(props, "source_master", NULL);
+ pw_properties_set(props, "master", NULL);
+ }
+
+ if (module_args_to_audioinfo(module->impl, props, &playback_info) < 0) {
+ res = -EINVAL;
+ goto out;
+ }
+ capture_info = playback_info;
+
+ position_to_props(&capture_info, capture_props);
+ position_to_props(&playback_info, playback_props);
+
+ if (pw_properties_get(capture_props, PW_KEY_NODE_PASSIVE) == NULL)
+ pw_properties_set(capture_props, PW_KEY_NODE_PASSIVE, "true");
+
+ d->module = module;
+ d->capture_props = capture_props;
+ d->playback_props = playback_props;
+
+ return 0;
+out:
+ pw_properties_free(playback_props);
+ pw_properties_free(capture_props);
+
+ return res;
+}
+
+DEFINE_MODULE_INFO(module_ladspa_source) = {
+ .name = "module-ladspa-source",
+ .prepare = module_ladspa_source_prepare,
+ .load = module_ladspa_source_load,
+ .unload = module_ladspa_source_unload,
+ .properties = &SPA_DICT_INIT_ARRAY(module_ladspa_source_info),
+ .data_size = sizeof(struct module_ladspa_source_data),
+};
diff --git a/src/modules/module-protocol-pulse/modules/module-loopback.c b/src/modules/module-protocol-pulse/modules/module-loopback.c
new file mode 100644
index 0000000..614ee50
--- /dev/null
+++ b/src/modules/module-protocol-pulse/modules/module-loopback.c
@@ -0,0 +1,245 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans <wim.taymans@gmail.com>
+ * Copyright © 2021 Arun Raghavan <arun@asymptotic.io>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <spa/param/audio/format-utils.h>
+#include <spa/utils/hook.h>
+#include <spa/utils/json.h>
+#include <pipewire/pipewire.h>
+
+#include "../defs.h"
+#include "../module.h"
+
+#define NAME "loopback"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+struct module_loopback_data {
+ struct module *module;
+
+ struct pw_impl_module *mod;
+ struct spa_hook mod_listener;
+
+ struct pw_properties *capture_props;
+ struct pw_properties *playback_props;
+
+ struct spa_audio_info_raw info;
+ uint32_t latency_msec;
+};
+
+static void module_destroy(void *data)
+{
+ struct module_loopback_data *d = data;
+ spa_hook_remove(&d->mod_listener);
+ d->mod = NULL;
+ module_schedule_unload(d->module);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy
+};
+
+static int module_loopback_load(struct module *module)
+{
+ struct module_loopback_data *data = module->user_data;
+ FILE *f;
+ char *args;
+ size_t size, i;
+ char val[256];
+
+ pw_properties_setf(data->capture_props, PW_KEY_NODE_GROUP, "loopback-%u", module->index);
+ pw_properties_setf(data->playback_props, PW_KEY_NODE_GROUP, "loopback-%u", module->index);
+ pw_properties_setf(data->capture_props, "pulse.module.id", "%u", module->index);
+ pw_properties_setf(data->playback_props, "pulse.module.id", "%u", module->index);
+
+ if ((f = open_memstream(&args, &size)) == NULL)
+ return -errno;
+
+ fprintf(f, "{");
+ if (data->info.channels != 0) {
+ fprintf(f, " audio.channels = %u", data->info.channels);
+ if (!(data->info.flags & SPA_AUDIO_FLAG_UNPOSITIONED)) {
+ fprintf(f, " audio.position = [ ");
+ for (i = 0; i < data->info.channels; i++)
+ fprintf(f, "%s%s", i == 0 ? "" : ",",
+ channel_id2name(data->info.position[i]));
+ fprintf(f, " ]");
+ }
+ }
+ if (data->latency_msec != 0)
+ fprintf(f, " target.delay.sec = %s",
+ spa_json_format_float(val, sizeof(val),
+ data->latency_msec / 1000.0f));
+ fprintf(f, " capture.props = {");
+ pw_properties_serialize_dict(f, &data->capture_props->dict, 0);
+ fprintf(f, " } playback.props = {");
+ pw_properties_serialize_dict(f, &data->playback_props->dict, 0);
+ fprintf(f, " } }");
+ fclose(f);
+
+ data->mod = pw_context_load_module(module->impl->context,
+ "libpipewire-module-loopback",
+ args, NULL);
+ free(args);
+
+ if (data->mod == NULL)
+ return -errno;
+
+ pw_impl_module_add_listener(data->mod,
+ &data->mod_listener,
+ &module_events, data);
+
+ return 0;
+}
+
+static int module_loopback_unload(struct module *module)
+{
+ struct module_loopback_data *d = module->user_data;
+
+ if (d->mod) {
+ spa_hook_remove(&d->mod_listener);
+ pw_impl_module_destroy(d->mod);
+ d->mod = NULL;
+ }
+
+ pw_properties_free(d->capture_props);
+ pw_properties_free(d->playback_props);
+
+ return 0;
+}
+
+static const struct spa_dict_item module_loopback_info[] = {
+ { PW_KEY_MODULE_AUTHOR, "Arun Raghavan <arun@asymptotic.io>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Loopback from source to sink" },
+ { PW_KEY_MODULE_USAGE, "source=<source to connect to> "
+ "sink=<sink to connect to> "
+ "latency_msec=<latency in ms> "
+ "rate=<sample rate> "
+ "channels=<number of channels> "
+ "channel_map=<channel map> "
+ "sink_input_properties=<proplist> "
+ "source_output_properties=<proplist> "
+ "source_dont_move=<boolean> "
+ "sink_dont_move=<boolean> "
+ "remix=<remix channels?> " },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+static int module_loopback_prepare(struct module * const module)
+{
+ struct module_loopback_data * const d = module->user_data;
+ struct pw_properties * const props = module->props;
+ struct pw_properties *playback_props = NULL, *capture_props = NULL;
+ const char *str;
+ struct spa_audio_info_raw info = { 0 };
+ int res;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ capture_props = pw_properties_new(NULL, NULL);
+ playback_props = pw_properties_new(NULL, NULL);
+ if (!capture_props || !playback_props) {
+ res = -EINVAL;
+ goto out;
+ }
+
+ /* The following modargs are not implemented:
+ * adjust_time, max_latency_msec, fast_adjust_threshold_msec: these are just not relevant
+ */
+
+ if ((str = pw_properties_get(props, "source")) != NULL) {
+ if (spa_strendswith(str, ".monitor")) {
+ pw_properties_setf(capture_props, PW_KEY_TARGET_OBJECT,
+ "%.*s", (int)strlen(str)-8, str);
+ pw_properties_set(capture_props, PW_KEY_STREAM_CAPTURE_SINK,
+ "true");
+ } else {
+ pw_properties_set(capture_props, PW_KEY_TARGET_OBJECT, str);
+ }
+ pw_properties_set(props, "source", NULL);
+ }
+
+ if ((str = pw_properties_get(props, "sink")) != NULL) {
+ pw_properties_set(playback_props, PW_KEY_TARGET_OBJECT, str);
+ pw_properties_set(props, "sink", NULL);
+ }
+
+ if (module_args_to_audioinfo(module->impl, props, &info) < 0) {
+ res = -EINVAL;
+ goto out;
+ }
+
+ if ((str = pw_properties_get(props, "source_dont_move")) != NULL) {
+ pw_properties_set(capture_props, PW_KEY_NODE_DONT_RECONNECT, str);
+ pw_properties_set(props, "source_dont_move", NULL);
+ }
+
+ if ((str = pw_properties_get(props, "sink_dont_move")) != NULL) {
+ pw_properties_set(playback_props, PW_KEY_NODE_DONT_RECONNECT, str);
+ pw_properties_set(props, "sink_dont_move", NULL);
+ }
+
+ if ((str = pw_properties_get(props, "remix")) != NULL) {
+ /* Note that the boolean is inverted */
+ pw_properties_set(playback_props, PW_KEY_STREAM_DONT_REMIX,
+ module_args_parse_bool(str) ? "false" : "true");
+ pw_properties_set(props, "remix", NULL);
+ }
+
+ if ((str = pw_properties_get(props, "latency_msec")) != NULL)
+ d->latency_msec = atoi(str);
+
+ if ((str = pw_properties_get(props, "sink_input_properties")) != NULL) {
+ module_args_add_props(playback_props, str);
+ pw_properties_set(props, "sink_input_properties", NULL);
+ }
+
+ if ((str = pw_properties_get(props, "source_output_properties")) != NULL) {
+ module_args_add_props(capture_props, str);
+ pw_properties_set(props, "source_output_properties", NULL);
+ }
+
+ d->module = module;
+ d->capture_props = capture_props;
+ d->playback_props = playback_props;
+ d->info = info;
+
+ return 0;
+out:
+ pw_properties_free(playback_props);
+ pw_properties_free(capture_props);
+
+ return res;
+}
+
+DEFINE_MODULE_INFO(module_loopback) = {
+ .name = "module-loopback",
+ .prepare = module_loopback_prepare,
+ .load = module_loopback_load,
+ .unload = module_loopback_unload,
+ .properties = &SPA_DICT_INIT_ARRAY(module_loopback_info),
+ .data_size = sizeof(struct module_loopback_data),
+};
diff --git a/src/modules/module-protocol-pulse/modules/module-native-protocol-tcp.c b/src/modules/module-protocol-pulse/modules/module-native-protocol-tcp.c
new file mode 100644
index 0000000..e8f4c14
--- /dev/null
+++ b/src/modules/module-protocol-pulse/modules/module-native-protocol-tcp.c
@@ -0,0 +1,127 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans <wim.taymans@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <pipewire/pipewire.h>
+
+#include "../module.h"
+#include "../pulse-server.h"
+#include "../server.h"
+
+#define NAME "protocol-tcp"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+struct module_native_protocol_tcp_data {
+ struct module *module;
+ struct pw_array servers;
+};
+
+static int module_native_protocol_tcp_load(struct module *module)
+{
+ struct module_native_protocol_tcp_data *data = module->user_data;
+ struct impl *impl = module->impl;
+ const char *address;
+ int res;
+
+ if ((address = pw_properties_get(module->props, "pulse.tcp")) == NULL)
+ return -EIO;
+
+ pw_array_init(&data->servers, sizeof(struct server *));
+
+ res = servers_create_and_start(impl, address, &data->servers);
+ if (res < 0)
+ return res;
+
+ return 0;
+}
+
+static int module_native_protocol_tcp_unload(struct module *module)
+{
+ struct module_native_protocol_tcp_data *d = module->user_data;
+ struct server **s;
+
+ pw_array_for_each (s, &d->servers)
+ server_free(*s);
+
+ pw_array_clear(&d->servers);
+
+ return 0;
+}
+
+static const struct spa_dict_item module_native_protocol_tcp_info[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Native protocol (TCP sockets)" },
+ { PW_KEY_MODULE_USAGE, "port=<TCP port number> "
+ "listen=<address to listen on> "
+ "auth-anonymous=<don't check for cookies?>"},
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+static int module_native_protocol_tcp_prepare(struct module * const module)
+{
+ struct module_native_protocol_tcp_data * const d = module->user_data;
+ struct pw_properties * const props = module->props;
+ const char *port, *listen, *auth;
+ FILE *f;
+ char *args;
+ size_t size;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ if ((port = pw_properties_get(props, "port")) == NULL)
+ port = SPA_STRINGIFY(PW_PROTOCOL_PULSE_DEFAULT_PORT);
+
+ listen = pw_properties_get(props, "listen");
+
+ auth = pw_properties_get(props, "auth-anonymous");
+
+ f = open_memstream(&args, &size);
+ if (f == NULL)
+ return -errno;
+
+ fprintf(f, "[ { ");
+ fprintf(f, " \"address\": \"tcp:%s%s%s\" ",
+ listen ? listen : "", listen ? ":" : "", port);
+ if (auth && module_args_parse_bool(auth))
+ fprintf(f, " \"client.access\": \"unrestricted\" ");
+ fprintf(f, "} ]");
+ fclose(f);
+
+ pw_properties_set(props, "pulse.tcp", args);
+ free(args);
+
+ d->module = module;
+
+ return 0;
+}
+
+DEFINE_MODULE_INFO(module_native_protocol_tcp) = {
+ .name = "module-native-protocol-tcp",
+ .prepare = module_native_protocol_tcp_prepare,
+ .load = module_native_protocol_tcp_load,
+ .unload = module_native_protocol_tcp_unload,
+ .properties = &SPA_DICT_INIT_ARRAY(module_native_protocol_tcp_info),
+ .data_size = sizeof(struct module_native_protocol_tcp_data),
+};
diff --git a/src/modules/module-protocol-pulse/modules/module-null-sink.c b/src/modules/module-protocol-pulse/modules/module-null-sink.c
new file mode 100644
index 0000000..caae598
--- /dev/null
+++ b/src/modules/module-protocol-pulse/modules/module-null-sink.c
@@ -0,0 +1,228 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Georges Basile Stavracas Neto
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <pipewire/pipewire.h>
+
+#include "../manager.h"
+#include "../module.h"
+
+#define NAME "null-sink"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+struct module_null_sink_data {
+ struct pw_core *core;
+ struct spa_hook core_listener;
+
+ struct pw_proxy *proxy;
+ struct spa_hook proxy_listener;
+};
+
+static void module_null_sink_proxy_removed(void *data)
+{
+ struct module *module = data;
+ struct module_null_sink_data *d = module->user_data;
+ pw_proxy_destroy(d->proxy);
+}
+
+static void module_null_sink_proxy_destroy(void *data)
+{
+ struct module *module = data;
+ struct module_null_sink_data *d = module->user_data;
+
+ pw_log_info("proxy %p destroy", d->proxy);
+
+ spa_hook_remove(&d->proxy_listener);
+ d->proxy = NULL;
+
+ module_schedule_unload(module);
+}
+
+static void module_null_sink_proxy_bound(void *data, uint32_t global_id)
+{
+ struct module *module = data;
+ struct module_null_sink_data *d = module->user_data;
+
+ pw_log_info("proxy %p bound", d->proxy);
+
+ module_emit_loaded(module, 0);
+}
+
+static void module_null_sink_proxy_error(void *data, int seq, int res, const char *message)
+{
+ struct module *module = data;
+ struct module_null_sink_data *d = module->user_data;
+
+ pw_log_info("proxy %p error %d", d->proxy, res);
+
+ pw_proxy_destroy(d->proxy);
+}
+
+static const struct pw_proxy_events proxy_events = {
+ PW_VERSION_PROXY_EVENTS,
+ .removed = module_null_sink_proxy_removed,
+ .bound = module_null_sink_proxy_bound,
+ .error = module_null_sink_proxy_error,
+ .destroy = module_null_sink_proxy_destroy,
+};
+
+static void module_null_sink_core_error(void *data, uint32_t id, int seq, int res, const char *message)
+{
+ struct module *module = data;
+
+ pw_log_warn("error id:%u seq:%d res:%d (%s): %s",
+ id, seq, res, spa_strerror(res), message);
+
+ if (id == PW_ID_CORE && res == -EPIPE)
+ module_schedule_unload(module);
+}
+
+static const struct pw_core_events core_events = {
+ PW_VERSION_CORE_EVENTS,
+ .error = module_null_sink_core_error,
+};
+
+static int module_null_sink_load(struct module *module)
+{
+ struct module_null_sink_data *d = module->user_data;
+
+ d->core = pw_context_connect(module->impl->context, NULL, 0);
+ if (d->core == NULL)
+ return -errno;
+
+ pw_core_add_listener(d->core, &d->core_listener, &core_events, module);
+
+ pw_properties_setf(module->props, "pulse.module.id", "%u", module->index);
+
+ d->proxy = pw_core_create_object(d->core,
+ "adapter", PW_TYPE_INTERFACE_Node, PW_VERSION_NODE,
+ module->props ? &module->props->dict : NULL, 0);
+ if (d->proxy == NULL)
+ return -errno;
+
+ pw_proxy_add_listener(d->proxy, &d->proxy_listener, &proxy_events, module);
+
+ return SPA_RESULT_RETURN_ASYNC(0);
+}
+
+static int module_null_sink_unload(struct module *module)
+{
+ struct module_null_sink_data *d = module->user_data;
+
+ if (d->proxy != NULL) {
+ spa_hook_remove(&d->proxy_listener);
+ pw_proxy_destroy(d->proxy);
+ d->proxy = NULL;
+ }
+
+ if (d->core != NULL) {
+ spa_hook_remove(&d->core_listener);
+ pw_core_disconnect(d->core);
+ d->core = NULL;
+ }
+
+ return 0;
+}
+
+static const struct spa_dict_item module_null_sink_info[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "A NULL sink" },
+ { PW_KEY_MODULE_USAGE, "sink_name=<name of sink> "
+ "sink_properties=<properties for the sink> "
+ "format=<sample format> "
+ "rate=<sample rate> "
+ "channels=<number of channels> "
+ "channel_map=<channel map>" },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+static int module_null_sink_prepare(struct module * const module)
+{
+ struct pw_properties * const props = module->props;
+ const char *str;
+ struct spa_audio_info_raw info = { 0 };
+ uint32_t i;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ if ((str = pw_properties_get(props, "sink_name")) != NULL) {
+ pw_properties_set(props, PW_KEY_NODE_NAME, str);
+ pw_properties_set(props, "sink_name", NULL);
+ }
+ else {
+ pw_properties_set(props, PW_KEY_NODE_NAME, "null-sink");
+ }
+
+ if ((str = pw_properties_get(props, "sink_properties")) != NULL) {
+ module_args_add_props(props, str);
+ pw_properties_set(props, "sink_properties", NULL);
+ }
+
+ if (module_args_to_audioinfo(module->impl, props, &info) < 0)
+ return -EINVAL;
+
+ if (info.rate)
+ pw_properties_setf(props, SPA_KEY_AUDIO_RATE, "%u", info.rate);
+ if (info.channels) {
+ char *s, *p;
+
+ pw_properties_setf(props, SPA_KEY_AUDIO_CHANNELS, "%u", info.channels);
+
+ p = s = alloca(info.channels * 8);
+ for (i = 0; i < info.channels; i++)
+ p += spa_scnprintf(p, 8, "%s%s", i == 0 ? "" : ",",
+ channel_id2name(info.position[i]));
+ pw_properties_set(props, SPA_KEY_AUDIO_POSITION, s);
+ }
+
+ if (pw_properties_get(props, PW_KEY_MEDIA_CLASS) == NULL)
+ pw_properties_set(props, PW_KEY_MEDIA_CLASS, "Audio/Sink");
+
+ if ((str = pw_properties_get(props, PW_KEY_NODE_DESCRIPTION)) == NULL) {
+ const char *name, *class;
+
+ name = pw_properties_get(props, PW_KEY_NODE_NAME);
+ class = pw_properties_get(props, PW_KEY_MEDIA_CLASS);
+ pw_properties_setf(props, PW_KEY_NODE_DESCRIPTION,
+ "%s%s%s%ssink",
+ name, (name[0] == '\0') ? "" : " ",
+ class ? class : "", (class && class[0] != '\0') ? " " : "");
+ }
+ pw_properties_set(props, PW_KEY_FACTORY_NAME, "support.null-audio-sink");
+
+ if (pw_properties_get(props, "monitor.channel-volumes") == NULL)
+ pw_properties_set(props, "monitor.channel-volumes", "true");
+
+ return 0;
+}
+
+DEFINE_MODULE_INFO(module_null_sink) = {
+ .name = "module-null-sink",
+ .prepare = module_null_sink_prepare,
+ .load = module_null_sink_load,
+ .unload = module_null_sink_unload,
+ .properties = &SPA_DICT_INIT_ARRAY(module_null_sink_info),
+ .data_size = sizeof(struct module_null_sink_data),
+};
diff --git a/src/modules/module-protocol-pulse/modules/module-pipe-sink.c b/src/modules/module-protocol-pulse/modules/module-pipe-sink.c
new file mode 100644
index 0000000..2cc36db
--- /dev/null
+++ b/src/modules/module-protocol-pulse/modules/module-pipe-sink.c
@@ -0,0 +1,210 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans <wim.taymans@gmail.com>
+ * Copyright © 2021 Sanchayan Maity <sanchayan@asymptotic.io>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <fcntl.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#include <pipewire/pipewire.h>
+#include <spa/param/audio/format-utils.h>
+#include <spa/utils/hook.h>
+
+#include "../defs.h"
+#include "../module.h"
+
+#define NAME "pipe-sink"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+struct module_pipesink_data {
+ struct module *module;
+
+ struct spa_hook mod_listener;
+ struct pw_impl_module *mod;
+
+ struct pw_properties *capture_props;
+ struct spa_audio_info_raw info;
+ char *filename;
+};
+
+static void module_destroy(void *data)
+{
+ struct module_pipesink_data *d = data;
+ spa_hook_remove(&d->mod_listener);
+ d->mod = NULL;
+ module_schedule_unload(d->module);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy
+};
+
+static int module_pipe_sink_load(struct module *module)
+{
+ struct module_pipesink_data *data = module->user_data;
+ FILE *f;
+ char *args;
+ size_t size;
+ uint32_t i;
+
+ pw_properties_setf(data->capture_props, "pulse.module.id",
+ "%u", module->index);
+
+ if ((f = open_memstream(&args, &size)) == NULL)
+ return -errno;
+
+ fprintf(f, "{");
+ fprintf(f, " \"tunnel.mode\" = \"sink\" ");
+ if (data->filename != NULL)
+ fprintf(f, " \"pipe.filename\": \"%s\"", data->filename);
+ if (data->info.format != 0)
+ fprintf(f, " \"audio.format\": \"%s\"", format_id2name(data->info.format));
+ if (data->info.rate != 0)
+ fprintf(f, " \"audio.rate\": %u,", data->info.rate);
+ if (data->info.channels != 0) {
+ fprintf(f, " \"audio.channels\": %u,", data->info.channels);
+ if (!(data->info.flags & SPA_AUDIO_FLAG_UNPOSITIONED)) {
+ fprintf(f, " \"audio.position\": [ ");
+ for (i = 0; i < data->info.channels; i++)
+ fprintf(f, "%s\"%s\"", i == 0 ? "" : ",",
+ channel_id2name(data->info.position[i]));
+ fprintf(f, " ],");
+ }
+ }
+ fprintf(f, " \"stream.props\": {");
+ pw_properties_serialize_dict(f, &data->capture_props->dict, 0);
+ fprintf(f, " } }");
+ fclose(f);
+
+ data->mod = pw_context_load_module(module->impl->context,
+ "libpipewire-module-pipe-tunnel",
+ args, NULL);
+
+ free(args);
+
+ if (data->mod == NULL)
+ return -errno;
+
+ pw_impl_module_add_listener(data->mod,
+ &data->mod_listener,
+ &module_events, data);
+ return 0;
+}
+
+static int module_pipe_sink_unload(struct module *module)
+{
+ struct module_pipesink_data *d = module->user_data;
+
+ if (d->mod) {
+ spa_hook_remove(&d->mod_listener);
+ pw_impl_module_destroy(d->mod);
+ d->mod = NULL;
+ }
+ pw_properties_free(d->capture_props);
+ free(d->filename);
+ return 0;
+}
+
+static const struct spa_dict_item module_pipe_sink_info[] = {
+ { PW_KEY_MODULE_AUTHOR, "Sanchayan Maity <sanchayan@asymptotic.io>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Pipe sink" },
+ { PW_KEY_MODULE_USAGE, "file=<name of the FIFO special file to use> "
+ "sink_name=<name for the sink> "
+ "sink_properties=<sink properties> "
+ "format=<sample format> "
+ "rate=<sample rate> "
+ "channels=<number of channels> "
+ "channel_map=<channel map> " },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+static int module_pipe_sink_prepare(struct module * const module)
+{
+ struct module_pipesink_data * const d = module->user_data;
+ struct pw_properties * const props = module->props;
+ struct pw_properties *capture_props = NULL;
+ struct spa_audio_info_raw info = { 0 };
+ const char *str;
+ char *filename = NULL;
+ int res = 0;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ capture_props = pw_properties_new(NULL, NULL);
+ if (!capture_props) {
+ res = -EINVAL;
+ goto out;
+ }
+
+ if (module_args_to_audioinfo(module->impl, props, &info) < 0) {
+ res = -EINVAL;
+ goto out;
+ }
+
+ info.format = SPA_AUDIO_FORMAT_S16;
+ if ((str = pw_properties_get(props, "format")) != NULL) {
+ info.format = format_paname2id(str, strlen(str));
+ pw_properties_set(props, "format", NULL);
+ }
+ if ((str = pw_properties_get(props, "sink_name")) != NULL) {
+ pw_properties_set(capture_props, PW_KEY_NODE_NAME, str);
+ pw_properties_set(props, "sink_name", NULL);
+ }
+ if ((str = pw_properties_get(props, "sink_properties")) != NULL)
+ module_args_add_props(capture_props, str);
+
+ if ((str = pw_properties_get(props, "file")) != NULL) {
+ filename = strdup(str);
+ pw_properties_set(props, "file", NULL);
+ }
+ if ((str = pw_properties_get(capture_props, PW_KEY_DEVICE_ICON_NAME)) == NULL)
+ pw_properties_set(capture_props, PW_KEY_DEVICE_ICON_NAME,
+ "audio-card");
+ if ((str = pw_properties_get(capture_props, PW_KEY_NODE_NAME)) == NULL)
+ pw_properties_set(capture_props, PW_KEY_NODE_NAME,
+ "fifo_output");
+
+ d->module = module;
+ d->capture_props = capture_props;
+ d->info = info;
+ d->filename = filename;
+
+ return 0;
+out:
+ pw_properties_free(capture_props);
+ free(filename);
+ return res;
+}
+
+DEFINE_MODULE_INFO(module_pipe_sink) = {
+ .name = "module-pipe-sink",
+ .prepare = module_pipe_sink_prepare,
+ .load = module_pipe_sink_load,
+ .unload = module_pipe_sink_unload,
+ .properties = &SPA_DICT_INIT_ARRAY(module_pipe_sink_info),
+ .data_size = sizeof(struct module_pipesink_data),
+};
diff --git a/src/modules/module-protocol-pulse/modules/module-pipe-source.c b/src/modules/module-protocol-pulse/modules/module-pipe-source.c
new file mode 100644
index 0000000..6153635
--- /dev/null
+++ b/src/modules/module-protocol-pulse/modules/module-pipe-source.c
@@ -0,0 +1,210 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans <wim.taymans@gmail.com>
+ * Copyright © 2021 Sanchayan Maity <sanchayan@asymptotic.io>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <fcntl.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#include <pipewire/pipewire.h>
+#include <spa/param/audio/format-utils.h>
+#include <spa/utils/hook.h>
+
+#include "../defs.h"
+#include "../module.h"
+
+#define NAME "pipe-source"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+struct module_pipesrc_data {
+ struct module *module;
+
+ struct spa_hook mod_listener;
+ struct pw_impl_module *mod;
+
+ struct pw_properties *playback_props;
+ struct spa_audio_info_raw info;
+ char *filename;
+};
+
+static void module_destroy(void *data)
+{
+ struct module_pipesrc_data *d = data;
+ spa_hook_remove(&d->mod_listener);
+ d->mod = NULL;
+ module_schedule_unload(d->module);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy
+};
+
+static int module_pipe_source_load(struct module *module)
+{
+ struct module_pipesrc_data *data = module->user_data;
+ FILE *f;
+ char *args;
+ size_t size;
+ uint32_t i;
+
+ pw_properties_setf(data->playback_props, "pulse.module.id",
+ "%u", module->index);
+
+ if ((f = open_memstream(&args, &size)) == NULL)
+ return -errno;
+
+ fprintf(f, "{");
+ fprintf(f, " \"tunnel.mode\" = \"source\" ");
+ if (data->filename != NULL)
+ fprintf(f, " \"pipe.filename\": \"%s\"", data->filename);
+ if (data->info.format != 0)
+ fprintf(f, " \"audio.format\": \"%s\"", format_id2name(data->info.format));
+ if (data->info.rate != 0)
+ fprintf(f, " \"audio.rate\": %u,", data->info.rate);
+ if (data->info.channels != 0) {
+ fprintf(f, " \"audio.channels\": %u,", data->info.channels);
+ if (!(data->info.flags & SPA_AUDIO_FLAG_UNPOSITIONED)) {
+ fprintf(f, " \"audio.position\": [ ");
+ for (i = 0; i < data->info.channels; i++)
+ fprintf(f, "%s\"%s\"", i == 0 ? "" : ",",
+ channel_id2name(data->info.position[i]));
+ fprintf(f, " ],");
+ }
+ }
+ fprintf(f, " \"stream.props\": {");
+ pw_properties_serialize_dict(f, &data->playback_props->dict, 0);
+ fprintf(f, " } }");
+ fclose(f);
+
+ data->mod = pw_context_load_module(module->impl->context,
+ "libpipewire-module-pipe-tunnel",
+ args, NULL);
+
+ free(args);
+
+ if (data->mod == NULL)
+ return -errno;
+
+ pw_impl_module_add_listener(data->mod,
+ &data->mod_listener,
+ &module_events, data);
+ return 0;
+}
+
+static int module_pipe_source_unload(struct module *module)
+{
+ struct module_pipesrc_data *d = module->user_data;
+
+ if (d->mod) {
+ spa_hook_remove(&d->mod_listener);
+ pw_impl_module_destroy(d->mod);
+ d->mod = NULL;
+ }
+ pw_properties_free(d->playback_props);
+ free(d->filename);
+ return 0;
+}
+
+static const struct spa_dict_item module_pipe_source_info[] = {
+ { PW_KEY_MODULE_AUTHOR, "Sanchayan Maity <sanchayan@asymptotic.io>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Pipe source" },
+ { PW_KEY_MODULE_USAGE, "file=<name of the FIFO special file to use> "
+ "source_name=<name for the source> "
+ "source_properties=<source properties> "
+ "format=<sample format> "
+ "rate=<sample rate> "
+ "channels=<number of channels> "
+ "channel_map=<channel map> " },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+static int module_pipe_source_prepare(struct module * const module)
+{
+ struct module_pipesrc_data * const d = module->user_data;
+ struct pw_properties * const props = module->props;
+ struct pw_properties *playback_props = NULL;
+ struct spa_audio_info_raw info = { 0 };
+ const char *str;
+ char *filename = NULL;
+ int res = 0;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ playback_props = pw_properties_new(NULL, NULL);
+ if (!playback_props) {
+ res = -errno;
+ goto out;
+ }
+
+ if (module_args_to_audioinfo(module->impl, props, &info) < 0) {
+ res = -EINVAL;
+ goto out;
+ }
+
+ info.format = SPA_AUDIO_FORMAT_S16;
+ if ((str = pw_properties_get(props, "format")) != NULL) {
+ info.format = format_paname2id(str, strlen(str));
+ pw_properties_set(props, "format", NULL);
+ }
+ if ((str = pw_properties_get(props, "source_name")) != NULL) {
+ pw_properties_set(playback_props, PW_KEY_NODE_NAME, str);
+ pw_properties_set(props, "source_name", NULL);
+ }
+ if ((str = pw_properties_get(props, "source_properties")) != NULL)
+ module_args_add_props(playback_props, str);
+
+ if ((str = pw_properties_get(props, "file")) != NULL) {
+ filename = strdup(str);
+ pw_properties_set(props, "file", NULL);
+ }
+ if ((str = pw_properties_get(playback_props, PW_KEY_DEVICE_ICON_NAME)) == NULL)
+ pw_properties_set(playback_props, PW_KEY_DEVICE_ICON_NAME,
+ "audio-input-microphone");
+ if ((str = pw_properties_get(playback_props, PW_KEY_NODE_NAME)) == NULL)
+ pw_properties_set(playback_props, PW_KEY_NODE_NAME,
+ "fifo_input");
+
+ d->module = module;
+ d->playback_props = playback_props;
+ d->info = info;
+ d->filename = filename;
+
+ return 0;
+out:
+ pw_properties_free(playback_props);
+ free(filename);
+ return res;
+}
+
+DEFINE_MODULE_INFO(module_pipe_source) = {
+ .name = "module-pipe-source",
+ .prepare = module_pipe_source_prepare,
+ .load = module_pipe_source_load,
+ .unload = module_pipe_source_unload,
+ .properties = &SPA_DICT_INIT_ARRAY(module_pipe_source_info),
+ .data_size = sizeof(struct module_pipesrc_data),
+};
diff --git a/src/modules/module-protocol-pulse/modules/module-raop-discover.c b/src/modules/module-protocol-pulse/modules/module-raop-discover.c
new file mode 100644
index 0000000..e406189
--- /dev/null
+++ b/src/modules/module-protocol-pulse/modules/module-raop-discover.c
@@ -0,0 +1,112 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans <wim.taymans@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <spa/utils/hook.h>
+#include <pipewire/pipewire.h>
+
+#include "../defs.h"
+#include "../module.h"
+
+#define NAME "raop-discover"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+
+struct module_raop_discover_data {
+ struct module *module;
+
+ struct spa_hook mod_listener;
+ struct pw_impl_module *mod;
+};
+
+static void module_destroy(void *data)
+{
+ struct module_raop_discover_data *d = data;
+ spa_hook_remove(&d->mod_listener);
+ d->mod = NULL;
+ module_schedule_unload(d->module);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy
+};
+
+static int module_raop_discover_load(struct module *module)
+{
+ struct module_raop_discover_data *data = module->user_data;
+
+ data->mod = pw_context_load_module(module->impl->context,
+ "libpipewire-module-raop-discover",
+ NULL, NULL);
+ if (data->mod == NULL)
+ return -errno;
+
+ pw_impl_module_add_listener(data->mod,
+ &data->mod_listener,
+ &module_events, data);
+
+ return 0;
+}
+
+static int module_raop_discover_unload(struct module *module)
+{
+ struct module_raop_discover_data *d = module->user_data;
+
+ if (d->mod) {
+ spa_hook_remove(&d->mod_listener);
+ pw_impl_module_destroy(d->mod);
+ d->mod = NULL;
+ }
+
+ return 0;
+}
+
+static const struct spa_dict_item module_raop_discover_info[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.con>" },
+ { PW_KEY_MODULE_DESCRIPTION, "mDNS/DNS-SD Service Discovery of RAOP devices" },
+ { PW_KEY_MODULE_USAGE, "" },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+static int module_raop_discover_prepare(struct module * const module)
+{
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ struct module_raop_discover_data * const data = module->user_data;
+ data->module = module;
+
+ return 0;
+}
+
+DEFINE_MODULE_INFO(module_raop_discover) = {
+ .name = "module-raop-discover",
+ .load_once = true,
+ .prepare = module_raop_discover_prepare,
+ .load = module_raop_discover_load,
+ .unload = module_raop_discover_unload,
+ .properties = &SPA_DICT_INIT_ARRAY(module_raop_discover_info),
+ .data_size = sizeof(struct module_raop_discover_data),
+};
diff --git a/src/modules/module-protocol-pulse/modules/module-remap-sink.c b/src/modules/module-protocol-pulse/modules/module-remap-sink.c
new file mode 100644
index 0000000..f6b57f0
--- /dev/null
+++ b/src/modules/module-protocol-pulse/modules/module-remap-sink.c
@@ -0,0 +1,253 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans <wim.taymans@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <spa/param/audio/format-utils.h>
+#include <spa/utils/hook.h>
+#include <spa/utils/json.h>
+#include <pipewire/pipewire.h>
+
+#include "../defs.h"
+#include "../module.h"
+
+#define NAME "remap-sink"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+struct module_remap_sink_data {
+ struct module *module;
+
+ struct pw_impl_module *mod;
+ struct spa_hook mod_listener;
+
+ struct pw_properties *capture_props;
+ struct pw_properties *playback_props;
+};
+
+static void module_destroy(void *data)
+{
+ struct module_remap_sink_data *d = data;
+ spa_hook_remove(&d->mod_listener);
+ d->mod = NULL;
+ module_schedule_unload(d->module);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy
+};
+
+static int module_remap_sink_load(struct module *module)
+{
+ struct module_remap_sink_data *data = module->user_data;
+ FILE *f;
+ char *args;
+ size_t size;
+
+ pw_properties_setf(data->capture_props, PW_KEY_NODE_GROUP, "remap-sink-%u", module->index);
+ pw_properties_setf(data->playback_props, PW_KEY_NODE_GROUP, "remap-sink-%u", module->index);
+ pw_properties_setf(data->capture_props, "pulse.module.id", "%u", module->index);
+ pw_properties_setf(data->playback_props, "pulse.module.id", "%u", module->index);
+
+ if ((f = open_memstream(&args, &size)) == NULL)
+ return -errno;
+
+ fprintf(f, "{");
+ pw_properties_serialize_dict(f, &module->props->dict, 0);
+ fprintf(f, " capture.props = {");
+ pw_properties_serialize_dict(f, &data->capture_props->dict, 0);
+ fprintf(f, " } playback.props = {");
+ pw_properties_serialize_dict(f, &data->playback_props->dict, 0);
+ fprintf(f, " } }");
+ fclose(f);
+
+ data->mod = pw_context_load_module(module->impl->context,
+ "libpipewire-module-loopback",
+ args, NULL);
+ free(args);
+
+ if (data->mod == NULL)
+ return -errno;
+
+ pw_impl_module_add_listener(data->mod,
+ &data->mod_listener,
+ &module_events, data);
+
+ return 0;
+}
+
+static int module_remap_sink_unload(struct module *module)
+{
+ struct module_remap_sink_data *d = module->user_data;
+
+ if (d->mod) {
+ spa_hook_remove(&d->mod_listener);
+ pw_impl_module_destroy(d->mod);
+ d->mod = NULL;
+ }
+
+ pw_properties_free(d->capture_props);
+ pw_properties_free(d->playback_props);
+
+ return 0;
+}
+
+static const struct spa_dict_item module_remap_sink_info[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Remap sink channels" },
+ { PW_KEY_MODULE_USAGE, "sink_name=<name for the sink> "
+ "sink_properties=<properties for the sink> "
+ "master=<name of sink to remap> "
+ "master_channel_map=<channel map> "
+ "format=<sample format> "
+ "rate=<sample rate> "
+ "channels=<number of channels> "
+ "channel_map=<channel map> "
+ "resample_method=<resampler> "
+ "remix=<remix channels?>" },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+static void position_to_props(struct spa_audio_info_raw *info, struct pw_properties *props)
+{
+ char *s, *p;
+ uint32_t i;
+
+ pw_properties_setf(props, SPA_KEY_AUDIO_CHANNELS, "%u", info->channels);
+ p = s = alloca(info->channels * 8);
+ for (i = 0; i < info->channels; i++)
+ p += spa_scnprintf(p, 8, "%s%s", i == 0 ? "" : ",",
+ channel_id2name(info->position[i]));
+ pw_properties_set(props, SPA_KEY_AUDIO_POSITION, s);
+}
+
+static int module_remap_sink_prepare(struct module * const module)
+{
+ struct module_remap_sink_data * const d = module->user_data;
+ struct pw_properties * const props = module->props;
+ struct pw_properties *playback_props = NULL, *capture_props = NULL;
+ const char *str, *master;
+ struct spa_audio_info_raw capture_info = { 0 };
+ struct spa_audio_info_raw playback_info = { 0 };
+ int res;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ capture_props = pw_properties_new(NULL, NULL);
+ playback_props = pw_properties_new(NULL, NULL);
+ if (!capture_props || !playback_props) {
+ res = -EINVAL;
+ goto out;
+ }
+
+ master = pw_properties_get(props, "master");
+ if (pw_properties_get(props, "sink_name") == NULL) {
+ pw_properties_setf(props, "sink_name", "%s.remapped",
+ master ? master : "default");
+ }
+ if ((str = pw_properties_get(props, "sink_name")) != NULL) {
+ pw_properties_set(capture_props, PW_KEY_NODE_NAME, str);
+ pw_properties_setf(playback_props, PW_KEY_NODE_NAME, "output.%s", str);
+ pw_properties_set(props, "sink_name", NULL);
+ }
+ if ((str = pw_properties_get(props, "sink_properties")) != NULL) {
+ module_args_add_props(capture_props, str);
+ pw_properties_set(props, "sink_properties", NULL);
+ }
+ if (pw_properties_get(capture_props, PW_KEY_MEDIA_CLASS) == NULL)
+ pw_properties_set(capture_props, PW_KEY_MEDIA_CLASS, "Audio/Sink");
+ if (pw_properties_get(capture_props, PW_KEY_DEVICE_CLASS) == NULL)
+ pw_properties_set(capture_props, PW_KEY_DEVICE_CLASS, "filter");
+
+ if ((str = pw_properties_get(capture_props, PW_KEY_MEDIA_NAME)) != NULL)
+ pw_properties_set(props, PW_KEY_MEDIA_NAME, str);
+ if ((str = pw_properties_get(capture_props, PW_KEY_NODE_DESCRIPTION)) != NULL) {
+ pw_properties_set(props, PW_KEY_NODE_DESCRIPTION, str);
+ } else {
+ str = pw_properties_get(capture_props, PW_KEY_NODE_NAME);
+ if (master != NULL || str == NULL) {
+ pw_properties_setf(props, PW_KEY_NODE_DESCRIPTION,
+ "Remapped %s sink",
+ master ? master : "default");
+ } else {
+ pw_properties_setf(props, PW_KEY_NODE_DESCRIPTION,
+ "%s sink", str);
+ }
+ }
+ if ((str = pw_properties_get(props, "master")) != NULL) {
+ pw_properties_set(playback_props, PW_KEY_TARGET_OBJECT, str);
+ pw_properties_set(props, "master", NULL);
+ }
+
+ if (module_args_to_audioinfo(module->impl, props, &capture_info) < 0) {
+ res = -EINVAL;
+ goto out;
+ }
+ playback_info = capture_info;
+
+ if ((str = pw_properties_get(props, "master_channel_map")) != NULL) {
+ struct channel_map map;
+
+ channel_map_parse(str, &map);
+ if (map.channels == 0 || map.channels > SPA_AUDIO_MAX_CHANNELS) {
+ pw_log_error("invalid channel_map '%s'", str);
+ res = -EINVAL;
+ goto out;
+ }
+ channel_map_to_positions(&map, playback_info.position);
+ pw_properties_set(props, "master_channel_map", NULL);
+ }
+ position_to_props(&capture_info, capture_props);
+ position_to_props(&playback_info, playback_props);
+
+ if ((str = pw_properties_get(props, "remix")) != NULL) {
+ /* Note that the boolean is inverted */
+ pw_properties_set(playback_props, PW_KEY_STREAM_DONT_REMIX,
+ module_args_parse_bool(str) ? "false" : "true");
+ pw_properties_set(props, "remix", NULL);
+ }
+
+ if (pw_properties_get(playback_props, PW_KEY_NODE_PASSIVE) == NULL)
+ pw_properties_set(playback_props, PW_KEY_NODE_PASSIVE, "true");
+
+ d->module = module;
+ d->capture_props = capture_props;
+ d->playback_props = playback_props;
+
+ return 0;
+out:
+ pw_properties_free(playback_props);
+ pw_properties_free(capture_props);
+
+ return res;
+}
+
+DEFINE_MODULE_INFO(module_remap_sink) = {
+ .name = "module-remap-sink",
+ .prepare = module_remap_sink_prepare,
+ .load = module_remap_sink_load,
+ .unload = module_remap_sink_unload,
+ .properties = &SPA_DICT_INIT_ARRAY(module_remap_sink_info),
+ .data_size = sizeof(struct module_remap_sink_data),
+};
diff --git a/src/modules/module-protocol-pulse/modules/module-remap-source.c b/src/modules/module-protocol-pulse/modules/module-remap-source.c
new file mode 100644
index 0000000..5ee6092
--- /dev/null
+++ b/src/modules/module-protocol-pulse/modules/module-remap-source.c
@@ -0,0 +1,260 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans <wim.taymans@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <spa/param/audio/format-utils.h>
+#include <spa/utils/hook.h>
+#include <spa/utils/json.h>
+#include <pipewire/pipewire.h>
+
+#include "../defs.h"
+#include "../module.h"
+
+#define NAME "remap-sink"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+struct module_remap_source_data {
+ struct module *module;
+
+ struct pw_impl_module *mod;
+ struct spa_hook mod_listener;
+
+ struct pw_properties *capture_props;
+ struct pw_properties *playback_props;
+};
+
+static void module_destroy(void *data)
+{
+ struct module_remap_source_data *d = data;
+ spa_hook_remove(&d->mod_listener);
+ d->mod = NULL;
+ module_schedule_unload(d->module);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy
+};
+
+static int module_remap_source_load(struct module *module)
+{
+ struct module_remap_source_data *data = module->user_data;
+ FILE *f;
+ char *args;
+ size_t size;
+
+ pw_properties_setf(data->capture_props, PW_KEY_NODE_GROUP, "remap-source-%u", module->index);
+ pw_properties_setf(data->playback_props, PW_KEY_NODE_GROUP, "remap-source-%u", module->index);
+ pw_properties_setf(data->capture_props, "pulse.module.id", "%u", module->index);
+ pw_properties_setf(data->playback_props, "pulse.module.id", "%u", module->index);
+
+ if ((f = open_memstream(&args, &size)) == NULL)
+ return -errno;
+
+ fprintf(f, "{");
+ pw_properties_serialize_dict(f, &module->props->dict, 0);
+ fprintf(f, " capture.props = { ");
+ pw_properties_serialize_dict(f, &data->capture_props->dict, 0);
+ fprintf(f, " } playback.props = { ");
+ pw_properties_serialize_dict(f, &data->playback_props->dict, 0);
+ fprintf(f, " } }");
+ fclose(f);
+
+ data->mod = pw_context_load_module(module->impl->context,
+ "libpipewire-module-loopback",
+ args, NULL);
+ free(args);
+
+ if (data->mod == NULL)
+ return -errno;
+
+ pw_impl_module_add_listener(data->mod,
+ &data->mod_listener,
+ &module_events, data);
+
+ return 0;
+}
+
+static int module_remap_source_unload(struct module *module)
+{
+ struct module_remap_source_data *d = module->user_data;
+
+ if (d->mod) {
+ spa_hook_remove(&d->mod_listener);
+ pw_impl_module_destroy(d->mod);
+ d->mod = NULL;
+ }
+
+ pw_properties_free(d->capture_props);
+ pw_properties_free(d->playback_props);
+
+ return 0;
+}
+
+static const struct spa_dict_item module_remap_source_info[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Remap source channels" },
+ { PW_KEY_MODULE_USAGE, "source_name=<name for the source> "
+ "source_properties=<properties for the source> "
+ "master=<name of source to filter> "
+ "master_channel_map=<channel map> "
+ "format=<sample format> "
+ "rate=<sample rate> "
+ "channels=<number of channels> "
+ "channel_map=<channel map> "
+ "resample_method=<resampler> "
+ "remix=<remix channels?>" },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+static void position_to_props(struct spa_audio_info_raw *info, struct pw_properties *props)
+{
+ char *s, *p;
+ uint32_t i;
+
+ pw_properties_setf(props, SPA_KEY_AUDIO_CHANNELS, "%u", info->channels);
+ p = s = alloca(info->channels * 8);
+ for (i = 0; i < info->channels; i++)
+ p += spa_scnprintf(p, 8, "%s%s", i == 0 ? "" : ",",
+ channel_id2name(info->position[i]));
+ pw_properties_set(props, SPA_KEY_AUDIO_POSITION, s);
+}
+
+static int module_remap_source_prepare(struct module * const module)
+{
+ struct module_remap_source_data * const d = module->user_data;
+ struct pw_properties * const props = module->props;
+ struct pw_properties *playback_props = NULL, *capture_props = NULL;
+ const char *str, *master;
+ struct spa_audio_info_raw capture_info = { 0 };
+ struct spa_audio_info_raw playback_info = { 0 };
+ int res;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ capture_props = pw_properties_new(NULL, NULL);
+ playback_props = pw_properties_new(NULL, NULL);
+ if (!capture_props || !playback_props) {
+ res = -EINVAL;
+ goto out;
+ }
+
+ master = pw_properties_get(props, "master");
+ if (pw_properties_get(props, "source_name") == NULL) {
+ pw_properties_setf(props, "source_name", "%s.remapped",
+ master ? master : "default");
+ }
+ if ((str = pw_properties_get(props, "source_name")) != NULL) {
+ pw_properties_set(playback_props, PW_KEY_NODE_NAME, str);
+ pw_properties_setf(capture_props, PW_KEY_NODE_NAME, "input.%s", str);
+ pw_properties_set(props, "source_name", NULL);
+ }
+ if ((str = pw_properties_get(props, "source_properties")) != NULL) {
+ module_args_add_props(playback_props, str);
+ pw_properties_set(props, "source_properties", NULL);
+ }
+ if (pw_properties_get(playback_props, PW_KEY_MEDIA_CLASS) == NULL)
+ pw_properties_set(playback_props, PW_KEY_MEDIA_CLASS, "Audio/Source");
+ if (pw_properties_get(playback_props, PW_KEY_DEVICE_CLASS) == NULL)
+ pw_properties_set(playback_props, PW_KEY_DEVICE_CLASS, "filter");
+
+ if ((str = pw_properties_get(playback_props, PW_KEY_MEDIA_NAME)) != NULL)
+ pw_properties_set(props, PW_KEY_MEDIA_NAME, str);
+ if ((str = pw_properties_get(playback_props, PW_KEY_NODE_DESCRIPTION)) != NULL) {
+ pw_properties_set(props, PW_KEY_NODE_DESCRIPTION, str);
+ } else {
+ str = pw_properties_get(playback_props, PW_KEY_NODE_NAME);
+ if (master != NULL || str == NULL) {
+ pw_properties_setf(props, PW_KEY_NODE_DESCRIPTION,
+ "Remapped %s source",
+ master ? master : "default");
+ } else {
+ pw_properties_setf(props, PW_KEY_NODE_DESCRIPTION,
+ "%s source", str);
+ }
+ }
+ if ((str = pw_properties_get(props, "master")) != NULL) {
+ if (spa_strendswith(str, ".monitor")) {
+ pw_properties_setf(capture_props, PW_KEY_TARGET_OBJECT,
+ "%.*s", (int)strlen(str)-8, str);
+ pw_properties_set(capture_props, PW_KEY_STREAM_CAPTURE_SINK,
+ "true");
+ } else {
+ pw_properties_set(capture_props, PW_KEY_TARGET_OBJECT, str);
+ }
+ pw_properties_set(props, "master", NULL);
+ }
+
+ if (module_args_to_audioinfo(module->impl, props, &playback_info) < 0) {
+ res = -EINVAL;
+ goto out;
+ }
+ capture_info = playback_info;
+
+ if ((str = pw_properties_get(props, "master_channel_map")) != NULL) {
+ struct channel_map map;
+
+ channel_map_parse(str, &map);
+ if (map.channels == 0 || map.channels > SPA_AUDIO_MAX_CHANNELS) {
+ pw_log_error("invalid channel_map '%s'", str);
+ res = -EINVAL;
+ goto out;
+ }
+ channel_map_to_positions(&map, capture_info.position);
+ pw_properties_set(props, "master_channel_map", NULL);
+ }
+ position_to_props(&playback_info, playback_props);
+ position_to_props(&capture_info, capture_props);
+
+ if ((str = pw_properties_get(props, "remix")) != NULL) {
+ /* Note that the boolean is inverted */
+ pw_properties_set(capture_props, PW_KEY_STREAM_DONT_REMIX,
+ module_args_parse_bool(str) ? "false" : "true");
+ pw_properties_set(props, "remix", NULL);
+ }
+
+ if (pw_properties_get(capture_props, PW_KEY_NODE_PASSIVE) == NULL)
+ pw_properties_set(capture_props, PW_KEY_NODE_PASSIVE, "true");
+
+ d->module = module;
+ d->capture_props = capture_props;
+ d->playback_props = playback_props;
+
+ return 0;
+out:
+ pw_properties_free(playback_props);
+ pw_properties_free(capture_props);
+
+ return res;
+}
+
+DEFINE_MODULE_INFO(module_remap_source) = {
+ .name = "module-remap-source",
+ .prepare = module_remap_source_prepare,
+ .load = module_remap_source_load,
+ .unload = module_remap_source_unload,
+ .properties = &SPA_DICT_INIT_ARRAY(module_remap_source_info),
+ .data_size = sizeof(struct module_remap_source_data),
+};
diff --git a/src/modules/module-protocol-pulse/modules/module-roc-sink-input.c b/src/modules/module-protocol-pulse/modules/module-roc-sink-input.c
new file mode 100644
index 0000000..3a672ac
--- /dev/null
+++ b/src/modules/module-protocol-pulse/modules/module-roc-sink-input.c
@@ -0,0 +1,201 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans <wim.taymans@gmail.com>
+ * Copyright © 2021 Sanchayan Maity <sanchayan@asymptotic.io>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <spa/utils/hook.h>
+#include <pipewire/pipewire.h>
+
+#include "../defs.h"
+#include "../module.h"
+
+#define NAME "roc-source"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+struct module_roc_sink_input_data {
+ struct module *module;
+
+ struct spa_hook mod_listener;
+ struct pw_impl_module *mod;
+
+ struct pw_properties *source_props;
+ struct pw_properties *roc_props;
+};
+
+static void module_destroy(void *data)
+{
+ struct module_roc_sink_input_data *d = data;
+ spa_hook_remove(&d->mod_listener);
+ d->mod = NULL;
+ module_schedule_unload(d->module);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy
+};
+
+static int module_roc_sink_input_load(struct module *module)
+{
+ struct module_roc_sink_input_data *data = module->user_data;
+ FILE *f;
+ char *args;
+ size_t size;
+
+ pw_properties_setf(data->source_props, "pulse.module.id",
+ "%u", module->index);
+
+ if ((f = open_memstream(&args, &size)) == NULL)
+ return -errno;
+
+ fprintf(f, "{");
+ pw_properties_serialize_dict(f, &data->roc_props->dict, 0);
+ fprintf(f, " source.props = {");
+ pw_properties_serialize_dict(f, &data->source_props->dict, 0);
+ fprintf(f, " } }");
+ fclose(f);
+
+ data->mod = pw_context_load_module(module->impl->context,
+ "libpipewire-module-roc-source",
+ args, NULL);
+
+ free(args);
+
+ if (data->mod == NULL)
+ return -errno;
+
+ pw_impl_module_add_listener(data->mod,
+ &data->mod_listener,
+ &module_events, data);
+
+ return 0;
+}
+
+static int module_roc_sink_input_unload(struct module *module)
+{
+ struct module_roc_sink_input_data *d = module->user_data;
+
+ if (d->mod) {
+ spa_hook_remove(&d->mod_listener);
+ pw_impl_module_destroy(d->mod);
+ d->mod = NULL;
+ }
+
+ pw_properties_free(d->roc_props);
+ pw_properties_free(d->source_props);
+
+ return 0;
+}
+
+static const struct spa_dict_item module_roc_sink_input_info[] = {
+ { PW_KEY_MODULE_AUTHOR, "Sanchayan Maity <sanchayan@asymptotic.io>" },
+ { PW_KEY_MODULE_DESCRIPTION, "roc sink-input" },
+ { PW_KEY_MODULE_USAGE, "sink=<name for the sink> "
+ "sink_input_properties=<properties for the sink_input> "
+ "resampler_profile=<empty>|disable|high|medium|low "
+ "fec_code=<empty>|disable|rs8m|ldpc "
+ "sess_latency_msec=<target network latency in milliseconds> "
+ "local_ip=<local receiver ip> "
+ "local_source_port=<local receiver port for source packets> "
+ "local_repair_port=<local receiver port for repair packets> " },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+static int module_roc_sink_input_prepare(struct module * const module)
+{
+ struct module_roc_sink_input_data * const d = module->user_data;
+ struct pw_properties * const props = module->props;
+ struct pw_properties *source_props = NULL, *roc_props = NULL;
+ const char *str;
+ int res;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ source_props = pw_properties_new(NULL, NULL);
+ roc_props = pw_properties_new(NULL, NULL);
+ if (!source_props || !roc_props) {
+ res = -errno;
+ goto out;
+ }
+
+ if ((str = pw_properties_get(props, "sink")) != NULL) {
+ pw_properties_set(source_props, PW_KEY_TARGET_OBJECT, str);
+ pw_properties_set(props, "sink", NULL);
+ }
+ if ((str = pw_properties_get(props, "sink_input_properties")) != NULL) {
+ module_args_add_props(source_props, str);
+ pw_properties_set(props, "sink_input_properties", NULL);
+ }
+
+ if ((str = pw_properties_get(props, "local_ip")) != NULL) {
+ pw_properties_set(roc_props, "local.ip", str);
+ pw_properties_set(props, "local_ip", NULL);
+ }
+
+ if ((str = pw_properties_get(props, "local_source_port")) != NULL) {
+ pw_properties_set(roc_props, "local.source.port", str);
+ pw_properties_set(props, "local_source_port", NULL);
+ }
+
+ if ((str = pw_properties_get(props, "local_repair_port")) != NULL) {
+ pw_properties_set(roc_props, "local.repair.port", str);
+ pw_properties_set(props, "local_repair_port", NULL);
+ }
+
+ if ((str = pw_properties_get(props, "sess_latency_msec")) != NULL) {
+ pw_properties_set(roc_props, "sess.latency.msec", str);
+ pw_properties_set(props, "sess_latency_msec", NULL);
+ }
+
+ if ((str = pw_properties_get(props, "resampler_profile")) != NULL) {
+ pw_properties_set(roc_props, "resampler.profile", str);
+ pw_properties_set(props, "resampler_profile", NULL);
+ }
+
+ if ((str = pw_properties_get(props, "fec_code")) != NULL) {
+ pw_properties_set(roc_props, "fec.code", str);
+ pw_properties_set(props, "fec_code", NULL);
+ }
+
+ d->module = module;
+ d->source_props = source_props;
+ d->roc_props = roc_props;
+
+ return 0;
+out:
+ pw_properties_free(source_props);
+ pw_properties_free(roc_props);
+
+ return res;
+}
+
+DEFINE_MODULE_INFO(module_roc_sink_input) = {
+ .name = "module-roc-sink-input",
+ .prepare = module_roc_sink_input_prepare,
+ .load = module_roc_sink_input_load,
+ .unload = module_roc_sink_input_unload,
+ .properties = &SPA_DICT_INIT_ARRAY(module_roc_sink_input_info),
+ .data_size = sizeof(struct module_roc_sink_input_data),
+};
diff --git a/src/modules/module-protocol-pulse/modules/module-roc-sink.c b/src/modules/module-protocol-pulse/modules/module-roc-sink.c
new file mode 100644
index 0000000..2dd5bb8
--- /dev/null
+++ b/src/modules/module-protocol-pulse/modules/module-roc-sink.c
@@ -0,0 +1,197 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans <wim.taymans@gmail.com>
+ * Copyright © 2021 Sanchayan Maity <sanchayan@asymptotic.io>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <spa/utils/hook.h>
+#include <pipewire/pipewire.h>
+
+#include "../defs.h"
+#include "../module.h"
+
+#define NAME "roc-sink"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+struct module_roc_sink_data {
+ struct module *module;
+
+ struct spa_hook mod_listener;
+ struct pw_impl_module *mod;
+
+ struct pw_properties *sink_props;
+ struct pw_properties *roc_props;
+};
+
+static void module_destroy(void *data)
+{
+ struct module_roc_sink_data *d = data;
+ spa_hook_remove(&d->mod_listener);
+ d->mod = NULL;
+ module_schedule_unload(d->module);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy
+};
+
+static int module_roc_sink_load(struct module *module)
+{
+ struct module_roc_sink_data *data = module->user_data;
+ FILE *f;
+ char *args;
+ size_t size;
+
+ pw_properties_setf(data->sink_props, "pulse.module.id",
+ "%u", module->index);
+
+ if ((f = open_memstream(&args, &size)) == NULL)
+ return -errno;
+
+ fprintf(f, "{");
+ pw_properties_serialize_dict(f, &data->roc_props->dict, 0);
+ fprintf(f, " sink.props = {");
+ pw_properties_serialize_dict(f, &data->sink_props->dict, 0);
+ fprintf(f, " } }");
+ fclose(f);
+
+ data->mod = pw_context_load_module(module->impl->context,
+ "libpipewire-module-roc-sink",
+ args, NULL);
+
+ free(args);
+
+ if (data->mod == NULL)
+ return -errno;
+
+ pw_impl_module_add_listener(data->mod,
+ &data->mod_listener,
+ &module_events, data);
+
+ return 0;
+}
+
+static int module_roc_sink_unload(struct module *module)
+{
+ struct module_roc_sink_data *d = module->user_data;
+
+ if (d->mod) {
+ spa_hook_remove(&d->mod_listener);
+ pw_impl_module_destroy(d->mod);
+ d->mod = NULL;
+ }
+
+ pw_properties_free(d->roc_props);
+ pw_properties_free(d->sink_props);
+
+ return 0;
+}
+
+static const struct spa_dict_item module_roc_sink_info[] = {
+ { PW_KEY_MODULE_AUTHOR, "Sanchayan Maity <sanchayan@asymptotic.io>" },
+ { PW_KEY_MODULE_DESCRIPTION, "roc sink" },
+ { PW_KEY_MODULE_USAGE, "sink_name=<name for the sink> "
+ "sink_properties=<properties for the sink> "
+ "fec_code=<empty>|disable|rs8m|ldpc "
+ "remote_ip=<remote receiver ip> "
+ "remote_source_port=<remote receiver port for source packets> "
+ "remote_repair_port=<remote receiver port for repair packets> " },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+static int module_roc_sink_prepare(struct module * const module)
+{
+ struct module_roc_sink_data * const d = module->user_data;
+ struct pw_properties * const props = module->props;
+ struct pw_properties *sink_props = NULL, *roc_props = NULL;
+ const char *str;
+ int res;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ sink_props = pw_properties_new(NULL, NULL);
+ roc_props = pw_properties_new(NULL, NULL);
+ if (!sink_props || !roc_props) {
+ res = -errno;
+ goto out;
+ }
+
+ if ((str = pw_properties_get(props, "sink_name")) != NULL) {
+ pw_properties_set(sink_props, PW_KEY_NODE_NAME, str);
+ pw_properties_set(props, "sink_name", NULL);
+ }
+ if ((str = pw_properties_get(props, "sink_properties")) != NULL) {
+ module_args_add_props(sink_props, str);
+ pw_properties_set(props, "sink_properties", NULL);
+ }
+
+ if ((str = pw_properties_get(props, PW_KEY_MEDIA_CLASS)) == NULL) {
+ pw_properties_set(props, PW_KEY_MEDIA_CLASS, "Audio/Sink");
+ pw_properties_set(sink_props, PW_KEY_MEDIA_CLASS, "Audio/Sink");
+ }
+
+ if ((str = pw_properties_get(props, "remote_ip")) != NULL) {
+ pw_properties_set(roc_props, "remote.ip", str);
+ pw_properties_set(props, "remote_ip", NULL);
+ } else {
+ pw_log_error("Remote IP not specified");
+ res = -EINVAL;
+ goto out;
+ }
+
+ if ((str = pw_properties_get(props, "remote_source_port")) != NULL) {
+ pw_properties_set(roc_props, "remote.source.port", str);
+ pw_properties_set(props, "remote_source_port", NULL);
+ }
+
+ if ((str = pw_properties_get(props, "remote_repair_port")) != NULL) {
+ pw_properties_set(roc_props, "remote.repair.port", str);
+ pw_properties_set(props, "remote_repair_port", NULL);
+ }
+ if ((str = pw_properties_get(props, "fec_code")) != NULL) {
+ pw_properties_set(roc_props, "fec.code", str);
+ pw_properties_set(props, "fec_code", NULL);
+ }
+
+ d->module = module;
+ d->sink_props = sink_props;
+ d->roc_props = roc_props;
+
+ return 0;
+out:
+ pw_properties_free(sink_props);
+ pw_properties_free(roc_props);
+
+ return res;
+}
+
+DEFINE_MODULE_INFO(module_roc_sink) = {
+ .name = "module-roc-sink",
+ .prepare = module_roc_sink_prepare,
+ .load = module_roc_sink_load,
+ .unload = module_roc_sink_unload,
+ .properties = &SPA_DICT_INIT_ARRAY(module_roc_sink_info),
+ .data_size = sizeof(struct module_roc_sink_data),
+};
diff --git a/src/modules/module-protocol-pulse/modules/module-roc-source.c b/src/modules/module-protocol-pulse/modules/module-roc-source.c
new file mode 100644
index 0000000..681f27a
--- /dev/null
+++ b/src/modules/module-protocol-pulse/modules/module-roc-source.c
@@ -0,0 +1,206 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans <wim.taymans@gmail.com>
+ * Copyright © 2021 Sanchayan Maity <sanchayan@asymptotic.io>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <spa/utils/hook.h>
+#include <pipewire/pipewire.h>
+
+#include "../defs.h"
+#include "../module.h"
+
+#define NAME "roc-source"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+struct module_roc_source_data {
+ struct module *module;
+
+ struct spa_hook mod_listener;
+ struct pw_impl_module *mod;
+
+ struct pw_properties *source_props;
+ struct pw_properties *roc_props;
+};
+
+static void module_destroy(void *data)
+{
+ struct module_roc_source_data *d = data;
+ spa_hook_remove(&d->mod_listener);
+ d->mod = NULL;
+ module_schedule_unload(d->module);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy
+};
+
+static int module_roc_source_load(struct module *module)
+{
+ struct module_roc_source_data *data = module->user_data;
+ FILE *f;
+ char *args;
+ size_t size;
+
+ pw_properties_setf(data->source_props, "pulse.module.id",
+ "%u", module->index);
+
+ if ((f = open_memstream(&args, &size)) == NULL)
+ return -errno;
+
+ fprintf(f, "{");
+ pw_properties_serialize_dict(f, &data->roc_props->dict, 0);
+ fprintf(f, " source.props = {");
+ pw_properties_serialize_dict(f, &data->source_props->dict, 0);
+ fprintf(f, " } }");
+ fclose(f);
+
+ data->mod = pw_context_load_module(module->impl->context,
+ "libpipewire-module-roc-source",
+ args, NULL);
+
+ free(args);
+
+ if (data->mod == NULL)
+ return -errno;
+
+ pw_impl_module_add_listener(data->mod,
+ &data->mod_listener,
+ &module_events, data);
+
+ return 0;
+}
+
+static int module_roc_source_unload(struct module *module)
+{
+ struct module_roc_source_data *d = module->user_data;
+
+ if (d->mod) {
+ spa_hook_remove(&d->mod_listener);
+ pw_impl_module_destroy(d->mod);
+ d->mod = NULL;
+ }
+
+ pw_properties_free(d->roc_props);
+ pw_properties_free(d->source_props);
+
+ return 0;
+}
+
+static const struct spa_dict_item module_roc_source_info[] = {
+ { PW_KEY_MODULE_AUTHOR, "Sanchayan Maity <sanchayan@asymptotic.io>" },
+ { PW_KEY_MODULE_DESCRIPTION, "roc source" },
+ { PW_KEY_MODULE_USAGE, "source_name=<name for the source> "
+ "source_properties=<properties for the source> "
+ "resampler_profile=<empty>|disable|high|medium|low "
+ "fec_code=<empty>|disable|rs8m|ldpc "
+ "sess_latency_msec=<target network latency in milliseconds> "
+ "local_ip=<local receiver ip> "
+ "local_source_port=<local receiver port for source packets> "
+ "local_repair_port=<local receiver port for repair packets> " },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+static int module_roc_source_prepare(struct module * const module)
+{
+ struct module_roc_source_data * const d = module->user_data;
+ struct pw_properties * const props = module->props;
+ struct pw_properties *source_props = NULL, *roc_props = NULL;
+ const char *str;
+ int res;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ source_props = pw_properties_new(NULL, NULL);
+ roc_props = pw_properties_new(NULL, NULL);
+ if (!source_props || !roc_props) {
+ res = -errno;
+ goto out;
+ }
+
+ if ((str = pw_properties_get(props, "source_name")) != NULL) {
+ pw_properties_set(source_props, PW_KEY_NODE_NAME, str);
+ pw_properties_set(props, "source_name", NULL);
+ }
+ if ((str = pw_properties_get(props, "source_properties")) != NULL) {
+ module_args_add_props(source_props, str);
+ pw_properties_set(props, "source_properties", NULL);
+ }
+
+ if ((str = pw_properties_get(props, PW_KEY_MEDIA_CLASS)) == NULL) {
+ pw_properties_set(props, PW_KEY_MEDIA_CLASS, "Audio/Source");
+ pw_properties_set(source_props, PW_KEY_MEDIA_CLASS, "Audio/Source");
+ }
+
+ if ((str = pw_properties_get(props, "local_ip")) != NULL) {
+ pw_properties_set(roc_props, "local.ip", str);
+ pw_properties_set(props, "local_ip", NULL);
+ }
+
+ if ((str = pw_properties_get(props, "local_source_port")) != NULL) {
+ pw_properties_set(roc_props, "local.source.port", str);
+ pw_properties_set(props, "local_source_port", NULL);
+ }
+
+ if ((str = pw_properties_get(props, "local_repair_port")) != NULL) {
+ pw_properties_set(roc_props, "local.repair.port", str);
+ pw_properties_set(props, "local_repair_port", NULL);
+ }
+
+ if ((str = pw_properties_get(props, "sess_latency_msec")) != NULL) {
+ pw_properties_set(roc_props, "sess.latency.msec", str);
+ pw_properties_set(props, "sess_latency_msec", NULL);
+ }
+
+ if ((str = pw_properties_get(props, "resampler_profile")) != NULL) {
+ pw_properties_set(roc_props, "resampler.profile", str);
+ pw_properties_set(props, "resampler_profile", NULL);
+ }
+
+ if ((str = pw_properties_get(props, "fec_code")) != NULL) {
+ pw_properties_set(roc_props, "fec.code", str);
+ pw_properties_set(props, "fec_code", NULL);
+ }
+
+ d->module = module;
+ d->source_props = source_props;
+ d->roc_props = roc_props;
+
+ return 0;
+out:
+ pw_properties_free(source_props);
+ pw_properties_free(roc_props);
+
+ return res;
+}
+
+DEFINE_MODULE_INFO(module_roc_source) = {
+ .name = "module-roc-source",
+ .prepare = module_roc_source_prepare,
+ .load = module_roc_source_load,
+ .unload = module_roc_source_unload,
+ .properties = &SPA_DICT_INIT_ARRAY(module_roc_source_info),
+ .data_size = sizeof(struct module_roc_source_data),
+};
diff --git a/src/modules/module-protocol-pulse/modules/module-rtp-recv.c b/src/modules/module-protocol-pulse/modules/module-rtp-recv.c
new file mode 100644
index 0000000..fdaecd7
--- /dev/null
+++ b/src/modules/module-protocol-pulse/modules/module-rtp-recv.c
@@ -0,0 +1,164 @@
+/* PipeWire
+ *
+ * Copyright © 2022 Wim Taymans <wim.taymans@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <spa/utils/hook.h>
+#include <pipewire/pipewire.h>
+
+#include "../defs.h"
+#include "../module.h"
+
+#define NAME "rtp-recv"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+struct module_rtp_recv_data {
+ struct module *module;
+
+ struct spa_hook mod_listener;
+ struct pw_impl_module *mod;
+
+ struct pw_properties *stream_props;
+ struct pw_properties *global_props;
+};
+
+static void module_destroy(void *data)
+{
+ struct module_rtp_recv_data *d = data;
+ spa_hook_remove(&d->mod_listener);
+ d->mod = NULL;
+ module_schedule_unload(d->module);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy
+};
+
+static int module_rtp_recv_load(struct module *module)
+{
+ struct module_rtp_recv_data *data = module->user_data;
+ FILE *f;
+ char *args;
+ size_t size;
+
+ pw_properties_setf(data->stream_props, "pulse.module.id",
+ "%u", module->index);
+
+ if ((f = open_memstream(&args, &size)) == NULL)
+ return -errno;
+
+ fprintf(f, "{");
+ pw_properties_serialize_dict(f, &data->global_props->dict, 0);
+ fprintf(f, " stream.props = {");
+ pw_properties_serialize_dict(f, &data->stream_props->dict, 0);
+ fprintf(f, " } }");
+ fclose(f);
+
+ data->mod = pw_context_load_module(module->impl->context,
+ "libpipewire-module-rtp-source",
+ args, NULL);
+
+ free(args);
+
+ if (data->mod == NULL)
+ return -errno;
+
+ pw_impl_module_add_listener(data->mod,
+ &data->mod_listener,
+ &module_events, data);
+
+ return 0;
+}
+
+static int module_rtp_recv_unload(struct module *module)
+{
+ struct module_rtp_recv_data *d = module->user_data;
+
+ if (d->mod) {
+ spa_hook_remove(&d->mod_listener);
+ pw_impl_module_destroy(d->mod);
+ d->mod = NULL;
+ }
+
+ pw_properties_free(d->global_props);
+ pw_properties_free(d->stream_props);
+
+ return 0;
+}
+
+static const struct spa_dict_item module_rtp_recv_info[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Receive data from a network via RTP/SAP/SDP" },
+ { PW_KEY_MODULE_USAGE, "sink=<name of the sink> "
+ "sap_address=<multicast address to listen on> "
+ "latency_msec=<latency in ms> " },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+static int module_rtp_recv_prepare(struct module * const module)
+{
+ struct module_rtp_recv_data * const d = module->user_data;
+ struct pw_properties * const props = module->props;
+ struct pw_properties *stream_props = NULL, *global_props = NULL;
+ const char *str;
+ int res;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ stream_props = pw_properties_new(NULL, NULL);
+ global_props = pw_properties_new(NULL, NULL);
+ if (!stream_props || !global_props) {
+ res = -errno;
+ goto out;
+ }
+ if ((str = pw_properties_get(props, "sink")) != NULL)
+ pw_properties_set(stream_props, PW_KEY_TARGET_OBJECT, str);
+
+ if ((str = pw_properties_get(props, "sap_address")) != NULL)
+ pw_properties_set(global_props, "sap.ip", str);
+
+ if ((str = pw_properties_get(props, "latency_msec")) != NULL)
+ pw_properties_set(global_props, "sess.latency.msec", str);
+
+ d->module = module;
+ d->stream_props = stream_props;
+ d->global_props = global_props;
+
+ return 0;
+out:
+ pw_properties_free(stream_props);
+ pw_properties_free(global_props);
+
+ return res;
+}
+
+DEFINE_MODULE_INFO(module_rtp_recv) = {
+ .name = "module-rtp-recv",
+ .prepare = module_rtp_recv_prepare,
+ .load = module_rtp_recv_load,
+ .unload = module_rtp_recv_unload,
+ .properties = &SPA_DICT_INIT_ARRAY(module_rtp_recv_info),
+ .data_size = sizeof(struct module_rtp_recv_data),
+};
diff --git a/src/modules/module-protocol-pulse/modules/module-rtp-send.c b/src/modules/module-protocol-pulse/modules/module-rtp-send.c
new file mode 100644
index 0000000..b9aad05
--- /dev/null
+++ b/src/modules/module-protocol-pulse/modules/module-rtp-send.c
@@ -0,0 +1,224 @@
+/* PipeWire
+ *
+ * Copyright © 2022 Wim Taymans <wim.taymans@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <spa/utils/hook.h>
+#include <pipewire/pipewire.h>
+
+#include "../defs.h"
+#include "../module.h"
+
+#define NAME "rtp-send"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+struct module_rtp_send_data {
+ struct module *module;
+
+ struct spa_hook mod_listener;
+ struct pw_impl_module *mod;
+
+ struct pw_properties *stream_props;
+ struct pw_properties *global_props;
+ struct spa_audio_info_raw info;
+};
+
+static void module_destroy(void *data)
+{
+ struct module_rtp_send_data *d = data;
+ spa_hook_remove(&d->mod_listener);
+ d->mod = NULL;
+ module_schedule_unload(d->module);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy
+};
+
+static int module_rtp_send_load(struct module *module)
+{
+ struct module_rtp_send_data *data = module->user_data;
+ FILE *f;
+ char *args;
+ size_t size;
+ uint32_t i;
+
+ pw_properties_setf(data->stream_props, "pulse.module.id",
+ "%u", module->index);
+
+ if ((f = open_memstream(&args, &size)) == NULL)
+ return -errno;
+
+ fprintf(f, "{");
+ pw_properties_serialize_dict(f, &data->global_props->dict, 0);
+ if (data->info.format != 0)
+ fprintf(f, " \"audio.format\": \"%s\"", format_id2name(data->info.format));
+ if (data->info.rate != 0)
+ fprintf(f, " \"audio.rate\": %u,", data->info.rate);
+ if (data->info.channels != 0) {
+ fprintf(f, " \"audio.channels\": %u,", data->info.channels);
+ if (!(data->info.flags & SPA_AUDIO_FLAG_UNPOSITIONED)) {
+ fprintf(f, " \"audio.position\": [ ");
+ for (i = 0; i < data->info.channels; i++)
+ fprintf(f, "%s\"%s\"", i == 0 ? "" : ",",
+ channel_id2name(data->info.position[i]));
+ fprintf(f, " ],");
+ }
+ }
+ fprintf(f, " stream.props = {");
+ pw_properties_serialize_dict(f, &data->stream_props->dict, 0);
+ fprintf(f, " } }");
+ fclose(f);
+
+ data->mod = pw_context_load_module(module->impl->context,
+ "libpipewire-module-rtp-sink",
+ args, NULL);
+
+ free(args);
+
+ if (data->mod == NULL)
+ return -errno;
+
+ pw_impl_module_add_listener(data->mod,
+ &data->mod_listener,
+ &module_events, data);
+
+ return 0;
+}
+
+static int module_rtp_send_unload(struct module *module)
+{
+ struct module_rtp_send_data *d = module->user_data;
+
+ if (d->mod) {
+ spa_hook_remove(&d->mod_listener);
+ pw_impl_module_destroy(d->mod);
+ d->mod = NULL;
+ }
+
+ pw_properties_free(d->global_props);
+ pw_properties_free(d->stream_props);
+
+ return 0;
+}
+
+static const struct spa_dict_item module_rtp_send_info[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Read data from source and send it to the network via RTP/SAP/SDP" },
+ { PW_KEY_MODULE_USAGE, "source=<name of the source> "
+ "format=<sample format> "
+ "channels=<number of channels> "
+ "rate=<sample rate> "
+ "destination_ip=<destination IP address> "
+ "source_ip=<source IP address> "
+ "port=<port number> "
+ "mtu=<maximum transfer unit> "
+ "loop=<loopback to local host?> "
+ "ttl=<ttl value> "
+ "inhibit_auto_suspend=<always|never|only_with_non_monitor_sources> "
+ "stream_name=<name of the stream> "
+ "enable_opus=<enable OPUS codec>" },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+static int module_rtp_send_prepare(struct module * const module)
+{
+ struct module_rtp_send_data * const d = module->user_data;
+ struct pw_properties * const props = module->props;
+ struct pw_properties *stream_props = NULL, *global_props = NULL;
+ struct spa_audio_info_raw info = { 0 };
+ const char *str;
+ int res;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ stream_props = pw_properties_new(NULL, NULL);
+ global_props = pw_properties_new(NULL, NULL);
+ if (!stream_props || !global_props) {
+ res = -errno;
+ goto out;
+ }
+
+ if ((str = pw_properties_get(props, "source")) != NULL) {
+ if (spa_strendswith(str, ".monitor")) {
+ pw_properties_setf(stream_props, PW_KEY_TARGET_OBJECT,
+ "%.*s", (int)strlen(str)-8, str);
+ pw_properties_set(stream_props, PW_KEY_STREAM_CAPTURE_SINK,
+ "true");
+ } else {
+ pw_properties_set(stream_props, PW_KEY_TARGET_OBJECT, str);
+ }
+ }
+ if (module_args_to_audioinfo(module->impl, props, &info) < 0) {
+ res = -EINVAL;
+ goto out;
+ }
+ info.format = 0;
+ if ((str = pw_properties_get(props, "format")) != NULL) {
+ if ((info.format = format_paname2id(str, strlen(str))) ==
+ SPA_AUDIO_FORMAT_UNKNOWN) {
+ pw_log_error("unknown format %s", str);
+ res = -EINVAL;
+ goto out;
+ }
+ }
+
+ if ((str = pw_properties_get(props, "destination_ip")) != NULL)
+ pw_properties_set(global_props, "destination.ip", str);
+ if ((str = pw_properties_get(props, "source_ip")) != NULL)
+ pw_properties_set(global_props, "source.ip", str);
+ if ((str = pw_properties_get(props, "port")) != NULL)
+ pw_properties_set(global_props, "destination.port", str);
+ if ((str = pw_properties_get(props, "mtu")) != NULL)
+ pw_properties_set(global_props, "net.mtu", str);
+ if ((str = pw_properties_get(props, "loop")) != NULL)
+ pw_properties_set(global_props, "net.loop",
+ module_args_parse_bool(str) ? "true" : "false");
+ if ((str = pw_properties_get(props, "ttl")) != NULL)
+ pw_properties_set(global_props, "net.ttl", str);
+ if ((str = pw_properties_get(props, "stream_name")) != NULL)
+ pw_properties_set(global_props, "sess.name", str);
+
+ d->module = module;
+ d->stream_props = stream_props;
+ d->global_props = global_props;
+ d->info = info;
+
+ return 0;
+out:
+ pw_properties_free(stream_props);
+ pw_properties_free(global_props);
+
+ return res;
+}
+
+DEFINE_MODULE_INFO(module_rtp_send) = {
+ .name = "module-rtp-send",
+ .prepare = module_rtp_send_prepare,
+ .load = module_rtp_send_load,
+ .unload = module_rtp_send_unload,
+ .properties = &SPA_DICT_INIT_ARRAY(module_rtp_send_info),
+ .data_size = sizeof(struct module_rtp_send_data),
+};
diff --git a/src/modules/module-protocol-pulse/modules/module-simple-protocol-tcp.c b/src/modules/module-protocol-pulse/modules/module-simple-protocol-tcp.c
new file mode 100644
index 0000000..3d0f7d7
--- /dev/null
+++ b/src/modules/module-protocol-pulse/modules/module-simple-protocol-tcp.c
@@ -0,0 +1,210 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans <wim.taymans@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <pipewire/impl.h>
+#include <pipewire/pipewire.h>
+
+#include "../defs.h"
+#include "../module.h"
+
+#define NAME "simple-protocol-tcp"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+struct module_simple_protocol_tcp_data {
+ struct module *module;
+ struct pw_impl_module *mod;
+ struct spa_hook mod_listener;
+
+ struct pw_properties *module_props;
+
+ struct spa_audio_info_raw info;
+};
+
+static void module_destroy(void *data)
+{
+ struct module_simple_protocol_tcp_data *d = data;
+
+ spa_hook_remove(&d->mod_listener);
+ d->mod = NULL;
+
+ module_schedule_unload(d->module);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy
+};
+
+static int module_simple_protocol_tcp_load(struct module *module)
+{
+ struct module_simple_protocol_tcp_data *data = module->user_data;
+ struct impl *impl = module->impl;
+ char *args;
+ size_t size;
+ uint32_t i;
+ FILE *f;
+
+ if ((f = open_memstream(&args, &size)) == NULL)
+ return -errno;
+
+ fprintf(f, "{");
+ if (data->info.rate != 0)
+ fprintf(f, " \"audio.rate\": %u,", data->info.rate);
+ if (data->info.channels != 0) {
+ fprintf(f, " \"audio.channels\": %u,", data->info.channels);
+ if (!(data->info.flags & SPA_AUDIO_FLAG_UNPOSITIONED)) {
+ fprintf(f, " \"audio.position\": [ ");
+ for (i = 0; i < data->info.channels; i++)
+ fprintf(f, "%s\"%s\"", i == 0 ? "" : ",",
+ channel_id2name(data->info.position[i]));
+ fprintf(f, " ],");
+ }
+ }
+ pw_properties_serialize_dict(f, &data->module_props->dict, 0);
+ fprintf(f, "}");
+ fclose(f);
+
+ data->mod = pw_context_load_module(impl->context,
+ "libpipewire-module-protocol-simple",
+ args, NULL);
+ free(args);
+
+ if (data->mod == NULL)
+ return -errno;
+
+ pw_impl_module_add_listener(data->mod, &data->mod_listener, &module_events, data);
+
+ return 0;
+}
+
+static int module_simple_protocol_tcp_unload(struct module *module)
+{
+ struct module_simple_protocol_tcp_data *d = module->user_data;
+
+ if (d->mod != NULL) {
+ spa_hook_remove(&d->mod_listener);
+ pw_impl_module_destroy(d->mod);
+ }
+
+ pw_properties_free(d->module_props);
+
+ return 0;
+}
+
+static const struct spa_dict_item module_simple_protocol_tcp_info[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Simple protocol (TCP sockets)" },
+ { PW_KEY_MODULE_USAGE, "rate=<sample rate> "
+ "format=<sample format> "
+ "channels=<number of channels> "
+ "channel_map=<number of channels> "
+ "sink=<sink to connect to> "
+ "source=<source to connect to> "
+ "playback=<enable playback?> "
+ "record=<enable record?> "
+ "port=<TCP port number> "
+ "listen=<address to listen on>" },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+static int module_simple_protocol_tcp_prepare(struct module * const module)
+{
+ struct module_simple_protocol_tcp_data * const d = module->user_data;
+ struct pw_properties * const props = module->props;
+ struct pw_properties *module_props = NULL;
+ const char *str, *port, *listen;
+ struct spa_audio_info_raw info = { 0 };
+ int res;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ module_props = pw_properties_new(NULL, NULL);
+ if (module_props == NULL) {
+ res = -errno;
+ goto out;
+ }
+
+ if ((str = pw_properties_get(props, "format")) != NULL) {
+ pw_properties_set(module_props, "audio.format",
+ format_id2name(format_paname2id(str, strlen(str))));
+ pw_properties_set(props, "format", NULL);
+ }
+ if (module_args_to_audioinfo(module->impl, props, &info) < 0) {
+ res = -EINVAL;
+ goto out;
+ }
+ if ((str = pw_properties_get(props, "playback")) != NULL) {
+ pw_properties_set(module_props, "playback", str);
+ pw_properties_set(props, "playback", NULL);
+ }
+ if ((str = pw_properties_get(props, "record")) != NULL) {
+ pw_properties_set(module_props, "capture", str);
+ pw_properties_set(props, "record", NULL);
+ }
+
+ if ((str = pw_properties_get(props, "source")) != NULL) {
+ if (spa_strendswith(str, ".monitor")) {
+ pw_properties_setf(module_props, "capture.node",
+ "%.*s", (int)strlen(str)-8, str);
+ pw_properties_set(module_props, PW_KEY_STREAM_CAPTURE_SINK,
+ "true");
+ } else {
+ pw_properties_set(module_props, "capture.node", str);
+ }
+ pw_properties_set(props, "source", NULL);
+ }
+
+ if ((str = pw_properties_get(props, "sink")) != NULL) {
+ pw_properties_set(module_props, "playback.node", str);
+ pw_properties_set(props, "sink", NULL);
+ }
+
+ if ((port = pw_properties_get(props, "port")) == NULL)
+ port = "4711";
+ listen = pw_properties_get(props, "listen");
+
+ pw_properties_setf(module_props, "server.address", "[ \"tcp:%s%s%s\" ]",
+ listen ? listen : "", listen ? ":" : "", port);
+
+ d->module = module;
+ d->module_props = module_props;
+ d->info = info;
+
+ return 0;
+out:
+ pw_properties_free(module_props);
+
+ return res;
+}
+
+DEFINE_MODULE_INFO(module_simple_protocol_tcp) = {
+ .name = "module-simple-protocol-tcp",
+ .prepare = module_simple_protocol_tcp_prepare,
+ .load = module_simple_protocol_tcp_load,
+ .unload = module_simple_protocol_tcp_unload,
+ .properties = &SPA_DICT_INIT_ARRAY(module_simple_protocol_tcp_info),
+ .data_size = sizeof(struct module_simple_protocol_tcp_data),
+};
diff --git a/src/modules/module-protocol-pulse/modules/module-switch-on-connect.c b/src/modules/module-protocol-pulse/modules/module-switch-on-connect.c
new file mode 100644
index 0000000..6354193
--- /dev/null
+++ b/src/modules/module-protocol-pulse/modules/module-switch-on-connect.c
@@ -0,0 +1,291 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans <wim.taymans@gmail.com>
+ * Copyright © 2021 Pauli Virtanen <pav@iki.fi>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <spa/utils/hook.h>
+#include <spa/utils/json.h>
+#include <spa/utils/string.h>
+
+#include <regex.h>
+
+#include "../defs.h"
+#include "../module.h"
+
+#include "../manager.h"
+#include "../collect.h"
+
+#define NAME "switch-on-connect"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+/* Ignore HDMI by default */
+#define DEFAULT_BLOCKLIST "hdmi"
+
+struct module_switch_on_connect_data {
+ struct module *module;
+
+ struct pw_core *core;
+ struct pw_manager *manager;
+ struct spa_hook core_listener;
+ struct spa_hook manager_listener;
+ struct pw_manager_object *metadata_default;
+
+ regex_t blocklist;
+
+ int sync_seq;
+
+ unsigned int only_from_unavailable:1;
+ unsigned int ignore_virtual:1;
+ unsigned int started:1;
+};
+
+static void handle_metadata(struct module_switch_on_connect_data *d, struct pw_manager_object *old,
+ struct pw_manager_object *new, const char *name)
+{
+ if (spa_streq(name, "default")) {
+ if (d->metadata_default == old)
+ d->metadata_default = new;
+ }
+}
+
+static void manager_added(void *data, struct pw_manager_object *o)
+{
+ struct module_switch_on_connect_data *d = data;
+ struct pw_node_info *info = o->info;
+ struct pw_device_info *card_info = NULL;
+ uint32_t card_id = SPA_ID_INVALID;
+ struct pw_manager_object *card = NULL;
+ const char *str, *bus, *name;
+
+ if (spa_streq(o->type, PW_TYPE_INTERFACE_Metadata)) {
+ if (o->props != NULL &&
+ (str = pw_properties_get(o->props, PW_KEY_METADATA_NAME)) != NULL)
+ handle_metadata(d, NULL, o, str);
+ }
+
+ if (!d->metadata_default || !d->started)
+ return;
+
+ if (!(pw_manager_object_is_sink(o) || pw_manager_object_is_source_or_monitor(o)))
+ return;
+
+ if (!info || !info->props)
+ return;
+
+ name = spa_dict_lookup(info->props, PW_KEY_NODE_NAME);
+ if (!name)
+ return;
+
+ /* Find card */
+ if ((str = spa_dict_lookup(info->props, PW_KEY_DEVICE_ID)) != NULL)
+ card_id = (uint32_t)atoi(str);
+ if (card_id != SPA_ID_INVALID) {
+ struct selector sel = { .id = card_id, .type = pw_manager_object_is_card, };
+ card = select_object(d->manager, &sel);
+ }
+ if (!card)
+ return;
+ card_info = card->info;
+ if (!card_info || !card_info->props)
+ return;
+
+ pw_log_debug("considering switching to %s", name);
+
+ /* If internal device, only consider hdmi sinks */
+ str = spa_dict_lookup(info->props, "api.alsa.path");
+ bus = spa_dict_lookup(card_info->props, PW_KEY_DEVICE_BUS);
+ if ((spa_streq(bus, "pci") || spa_streq(bus, "isa")) &&
+ !(pw_manager_object_is_sink(o) && spa_strstartswith(str, "hdmi"))) {
+ pw_log_debug("not switching to internal device");
+ return;
+ }
+
+ if (regexec(&d->blocklist, name, 0, NULL, 0) == 0) {
+ pw_log_debug("not switching to blocklisted device");
+ return;
+ }
+
+ if (d->ignore_virtual && spa_dict_lookup(info->props, PW_KEY_DEVICE_API) == NULL) {
+ pw_log_debug("not switching to virtual device");
+ return;
+ }
+
+ if (d->only_from_unavailable) {
+ /* XXX: not implemented */
+ }
+
+ /* Switch default */
+ pw_log_debug("switching to %s", name);
+
+ pw_manager_set_metadata(d->manager, d->metadata_default,
+ PW_ID_CORE,
+ pw_manager_object_is_sink(o) ? METADATA_CONFIG_DEFAULT_SINK
+ : METADATA_CONFIG_DEFAULT_SOURCE,
+ "Spa:String:JSON", "{ \"name\"\"%s\" }", name);
+}
+
+static void manager_sync(void *data)
+{
+ struct module_switch_on_connect_data *d = data;
+
+ /* Manager emits devices/etc next --- enable started flag after that */
+ if (!d->started)
+ d->sync_seq = pw_core_sync(d->core, PW_ID_CORE, d->sync_seq);
+}
+
+static const struct pw_manager_events manager_events = {
+ PW_VERSION_MANAGER_EVENTS,
+ .added = manager_added,
+ .sync = manager_sync,
+};
+
+static void on_core_done(void *data, uint32_t id, int seq)
+{
+ struct module_switch_on_connect_data *d = data;
+ if (seq == d->sync_seq) {
+ pw_log_debug("%p: started", d);
+ d->started = true;
+ }
+}
+
+static const struct pw_core_events core_events = {
+ PW_VERSION_CORE_EVENTS,
+ .done = on_core_done,
+};
+
+static int module_switch_on_connect_load(struct module *module)
+{
+ struct impl *impl = module->impl;
+ struct module_switch_on_connect_data *d = module->user_data;
+ int res;
+
+ d->core = pw_context_connect(impl->context, NULL, 0);
+ if (d->core == NULL) {
+ res = -errno;
+ goto error;
+ }
+
+ d->manager = pw_manager_new(d->core);
+ if (d->manager == NULL) {
+ res = -errno;
+ pw_core_disconnect(d->core);
+ d->core = NULL;
+ goto error;
+ }
+
+ pw_manager_add_listener(d->manager, &d->manager_listener, &manager_events, d);
+ pw_core_add_listener(d->core, &d->core_listener, &core_events, d);
+
+ /* Postpone setting started flag after initial nodes emitted */
+ pw_manager_sync(d->manager);
+
+ return 0;
+
+error:
+ pw_log_error("%p: failed to connect: %s", impl, spa_strerror(res));
+ return res;
+}
+
+static int module_switch_on_connect_unload(struct module *module)
+{
+ struct module_switch_on_connect_data *d = module->user_data;
+
+ if (d->manager) {
+ spa_hook_remove(&d->manager_listener);
+ pw_manager_destroy(d->manager);
+ d->manager = NULL;
+ }
+
+ if (d->core) {
+ spa_hook_remove(&d->core_listener);
+ pw_core_disconnect(d->core);
+ d->core = NULL;
+ }
+
+ regfree(&d->blocklist);
+
+ return 0;
+}
+
+static const struct spa_dict_item module_switch_on_connect_info[] = {
+ { PW_KEY_MODULE_AUTHOR, "Pauli Virtanen <pav@iki.fi>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Switch to new devices on connect. "
+ "This module exists for Pulseaudio compatibility, and is useful only when some applications "
+ "try to manage the default sinks/sources themselves and interfere with PipeWire's builtin "
+ "default device switching." },
+ { PW_KEY_MODULE_USAGE, "only_from_unavailable=<boolean, only switch from unavailable ports (not implemented yet)> "
+ "ignore_virtual=<boolean, ignore new virtual sinks and sources, defaults to true> "
+ "blocklist=<regex, ignore matching devices, default=hdmi> " },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+static int module_switch_on_connect_prepare(struct module * const module)
+{
+ struct module_switch_on_connect_data * const d = module->user_data;
+ struct pw_properties * const props = module->props;
+ bool only_from_unavailable = false, ignore_virtual = true;
+ const char *str;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ if ((str = pw_properties_get(props, "only_from_unavailable")) != NULL) {
+ only_from_unavailable = module_args_parse_bool(str);
+ pw_properties_set(props, "only_from_unavailable", NULL);
+ }
+
+ if ((str = pw_properties_get(props, "ignore_virtual")) != NULL) {
+ ignore_virtual = module_args_parse_bool(str);
+ pw_properties_set(props, "ignore_virtual", NULL);
+ }
+
+ if ((str = pw_properties_get(props, "blocklist")) == NULL)
+ str = DEFAULT_BLOCKLIST;
+
+ if (regcomp(&d->blocklist, str, REG_NOSUB | REG_EXTENDED) != 0)
+ return -EINVAL;
+
+ pw_properties_set(props, "blocklist", NULL);
+
+ d->module = module;
+ d->ignore_virtual = ignore_virtual;
+ d->only_from_unavailable = only_from_unavailable;
+
+ if (d->only_from_unavailable) {
+ /* XXX: not implemented */
+ pw_log_warn("only_from_unavailable is not implemented");
+ }
+
+ return 0;
+}
+
+DEFINE_MODULE_INFO(module_switch_on_connect) = {
+ .name = "module-switch-on-connect",
+ .load_once = true,
+ .prepare = module_switch_on_connect_prepare,
+ .load = module_switch_on_connect_load,
+ .unload = module_switch_on_connect_unload,
+ .properties = &SPA_DICT_INIT_ARRAY(module_switch_on_connect_info),
+ .data_size = sizeof(struct module_switch_on_connect_data),
+};
diff --git a/src/modules/module-protocol-pulse/modules/module-tunnel-sink.c b/src/modules/module-protocol-pulse/modules/module-tunnel-sink.c
new file mode 100644
index 0000000..55f9535
--- /dev/null
+++ b/src/modules/module-protocol-pulse/modules/module-tunnel-sink.c
@@ -0,0 +1,230 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans <wim.taymans@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <spa/param/audio/format-utils.h>
+#include <spa/utils/hook.h>
+#include <spa/utils/json.h>
+
+#include <pipewire/pipewire.h>
+#include <pipewire/i18n.h>
+
+#include "../defs.h"
+#include "../module.h"
+
+#define NAME "tunnel-sink"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+struct module_tunnel_sink_data {
+ struct module *module;
+
+ struct pw_impl_module *mod;
+ struct spa_hook mod_listener;
+
+ uint32_t latency_msec;
+
+ struct pw_properties *stream_props;
+};
+
+static void module_destroy(void *data)
+{
+ struct module_tunnel_sink_data *d = data;
+ spa_hook_remove(&d->mod_listener);
+ d->mod = NULL;
+ module_schedule_unload(d->module);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy
+};
+
+static int module_tunnel_sink_load(struct module *module)
+{
+ struct module_tunnel_sink_data *data = module->user_data;
+ FILE *f;
+ char *args;
+ size_t size;
+ const char *server;
+
+ server = pw_properties_get(module->props, "server");
+
+ pw_properties_setf(data->stream_props, "pulse.module.id",
+ "%u", module->index);
+
+ if ((f = open_memstream(&args, &size)) == NULL)
+ return -errno;
+
+ fprintf(f, "{");
+ pw_properties_serialize_dict(f, &module->props->dict, 0);
+ fprintf(f, " pulse.server.address = \"%s\" ", server);
+ fprintf(f, " tunnel.mode = sink ");
+ if (data->latency_msec > 0)
+ fprintf(f, " pulse.latency = %u ", data->latency_msec);
+ fprintf(f, " stream.props = {");
+ pw_properties_serialize_dict(f, &data->stream_props->dict, 0);
+ fprintf(f, " } }");
+ fclose(f);
+
+ data->mod = pw_context_load_module(module->impl->context,
+ "libpipewire-module-pulse-tunnel",
+ args, NULL);
+ free(args);
+
+ if (data->mod == NULL)
+ return -errno;
+
+ pw_impl_module_add_listener(data->mod,
+ &data->mod_listener,
+ &module_events, data);
+
+ return 0;
+}
+
+static int module_tunnel_sink_unload(struct module *module)
+{
+ struct module_tunnel_sink_data *d = module->user_data;
+
+ if (d->mod) {
+ spa_hook_remove(&d->mod_listener);
+ pw_impl_module_destroy(d->mod);
+ d->mod = NULL;
+ }
+
+ pw_properties_free(d->stream_props);
+
+ return 0;
+}
+
+static const struct spa_dict_item module_tunnel_sink_info[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Create a network sink which connects to a remote PulseAudio server" },
+ { PW_KEY_MODULE_USAGE,
+ "server=<address> "
+ "sink=<name of the remote sink> "
+ "sink_name=<name for the local sink> "
+ "sink_properties=<properties for the local sink> "
+ "format=<sample format> "
+ "channels=<number of channels> "
+ "rate=<sample rate> "
+ "channel_map=<channel map> "
+ "latency_msec=<fixed latency in ms> "
+ "cookie=<cookie file path>" },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+static void audio_info_to_props(struct spa_audio_info_raw *info, struct pw_properties *props)
+{
+ char *s, *p;
+ uint32_t i;
+
+ pw_properties_setf(props, SPA_KEY_AUDIO_CHANNELS, "%u", info->channels);
+ p = s = alloca(info->channels * 8);
+ for (i = 0; i < info->channels; i++)
+ p += spa_scnprintf(p, 8, "%s%s", i == 0 ? "" : ",",
+ channel_id2name(info->position[i]));
+ pw_properties_set(props, SPA_KEY_AUDIO_POSITION, s);
+}
+
+static int module_tunnel_sink_prepare(struct module * const module)
+{
+ struct module_tunnel_sink_data * const d = module->user_data;
+ struct pw_properties * const props = module->props;
+ struct pw_properties *stream_props = NULL;
+ const char *str, *server, *remote_sink_name;
+ struct spa_audio_info_raw info = { 0 };
+ int res;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ stream_props = pw_properties_new(NULL, NULL);
+ if (stream_props == NULL) {
+ res = -ENOMEM;
+ goto out;
+ }
+
+ remote_sink_name = pw_properties_get(props, "sink");
+ if (remote_sink_name)
+ pw_properties_set(props, PW_KEY_TARGET_OBJECT, remote_sink_name);
+
+ if ((server = pw_properties_get(props, "server")) == NULL) {
+ pw_log_error("no server given");
+ res = -EINVAL;
+ goto out;
+ }
+
+ pw_properties_setf(stream_props, PW_KEY_NODE_DESCRIPTION,
+ _("Tunnel to %s/%s"), server,
+ remote_sink_name ? remote_sink_name : "");
+ pw_properties_set(stream_props, PW_KEY_MEDIA_CLASS, "Audio/Sink");
+
+ if ((str = pw_properties_get(props, "sink_name")) != NULL) {
+ pw_properties_set(stream_props, PW_KEY_NODE_NAME, str);
+ pw_properties_set(props, "sink_name", NULL);
+ } else {
+ pw_properties_setf(stream_props, PW_KEY_NODE_NAME,
+ "tunnel-sink.%s", server);
+ }
+
+ if ((str = pw_properties_get(props, "sink_properties")) != NULL) {
+ module_args_add_props(stream_props, str);
+ pw_properties_set(props, "sink_properties", NULL);
+ }
+ if (module_args_to_audioinfo(module->impl, props, &info) < 0) {
+ res = -EINVAL;
+ goto out;
+ }
+
+ audio_info_to_props(&info, stream_props);
+ if ((str = pw_properties_get(props, "format")) != NULL) {
+ uint32_t id = format_paname2id(str, strlen(str));
+ if (id == SPA_AUDIO_FORMAT_UNKNOWN) {
+ res = -EINVAL;
+ goto out;
+ }
+
+ pw_properties_set(stream_props, PW_KEY_AUDIO_FORMAT, format_id2name(id));
+ }
+
+ d->module = module;
+ d->stream_props = stream_props;
+
+ pw_properties_fetch_uint32(props, "latency_msec", &d->latency_msec);
+
+ return 0;
+out:
+ pw_properties_free(stream_props);
+
+ return res;
+}
+
+DEFINE_MODULE_INFO(module_tunnel_sink) = {
+ .name = "module-tunnel-sink",
+ .prepare = module_tunnel_sink_prepare,
+ .load = module_tunnel_sink_load,
+ .unload = module_tunnel_sink_unload,
+ .properties = &SPA_DICT_INIT_ARRAY(module_tunnel_sink_info),
+ .data_size = sizeof(struct module_tunnel_sink_data),
+};
diff --git a/src/modules/module-protocol-pulse/modules/module-tunnel-source.c b/src/modules/module-protocol-pulse/modules/module-tunnel-source.c
new file mode 100644
index 0000000..fa17c16
--- /dev/null
+++ b/src/modules/module-protocol-pulse/modules/module-tunnel-source.c
@@ -0,0 +1,220 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans <wim.taymans@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <spa/param/audio/format-utils.h>
+#include <spa/utils/hook.h>
+#include <spa/utils/json.h>
+
+#include <pipewire/pipewire.h>
+#include <pipewire/i18n.h>
+
+#include "../defs.h"
+#include "../module.h"
+
+#define NAME "tunnel-source"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+struct module_tunnel_source_data {
+ struct module *module;
+
+ struct pw_impl_module *mod;
+ struct spa_hook mod_listener;
+
+ uint32_t latency_msec;
+
+ struct pw_properties *stream_props;
+};
+
+static void module_destroy(void *data)
+{
+ struct module_tunnel_source_data *d = data;
+ spa_hook_remove(&d->mod_listener);
+ d->mod = NULL;
+ module_schedule_unload(d->module);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy
+};
+
+static int module_tunnel_source_load(struct module *module)
+{
+ struct module_tunnel_source_data *data = module->user_data;
+ FILE *f;
+ char *args;
+ size_t size;
+ const char *server;
+
+ pw_properties_setf(data->stream_props, "pulse.module.id",
+ "%u", module->index);
+
+ server = pw_properties_get(module->props, "server");
+
+ if ((f = open_memstream(&args, &size)) == NULL)
+ return -errno;
+
+ fprintf(f, "{");
+ pw_properties_serialize_dict(f, &module->props->dict, 0);
+ fprintf(f, " pulse.server.address = \"%s\" ", server);
+ fprintf(f, " tunnel.mode = source ");
+ if (data->latency_msec > 0)
+ fprintf(f, " pulse.latency = %u ", data->latency_msec);
+ fprintf(f, " stream.props = {");
+ pw_properties_serialize_dict(f, &data->stream_props->dict, 0);
+ fprintf(f, " } }");
+ fclose(f);
+
+ data->mod = pw_context_load_module(module->impl->context,
+ "libpipewire-module-pulse-tunnel",
+ args, NULL);
+ free(args);
+
+ if (data->mod == NULL)
+ return -errno;
+
+ pw_impl_module_add_listener(data->mod,
+ &data->mod_listener,
+ &module_events, data);
+
+ return 0;
+}
+
+static int module_tunnel_source_unload(struct module *module)
+{
+ struct module_tunnel_source_data *d = module->user_data;
+
+ if (d->mod) {
+ spa_hook_remove(&d->mod_listener);
+ pw_impl_module_destroy(d->mod);
+ d->mod = NULL;
+ }
+
+ pw_properties_free(d->stream_props);
+
+ return 0;
+}
+
+static const struct spa_dict_item module_tunnel_source_info[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Create a network source which connects to a remote PulseAudio server" },
+ { PW_KEY_MODULE_USAGE,
+ "server=<address> "
+ "source=<name of the remote source> "
+ "source_name=<name for the local source> "
+ "source_properties=<properties for the local source> "
+ "format=<sample format> "
+ "channels=<number of channels> "
+ "rate=<sample rate> "
+ "channel_map=<channel map> "
+ "latency_msec=<fixed latency in ms> "
+ "cookie=<cookie file path>" },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+static void audio_info_to_props(struct spa_audio_info_raw *info, struct pw_properties *props)
+{
+ char *s, *p;
+ uint32_t i;
+
+ pw_properties_setf(props, SPA_KEY_AUDIO_CHANNELS, "%u", info->channels);
+ p = s = alloca(info->channels * 8);
+ for (i = 0; i < info->channels; i++)
+ p += spa_scnprintf(p, 8, "%s%s", i == 0 ? "" : ",",
+ channel_id2name(info->position[i]));
+ pw_properties_set(props, SPA_KEY_AUDIO_POSITION, s);
+}
+
+static int module_tunnel_source_prepare(struct module * const module)
+{
+ struct module_tunnel_source_data * const d = module->user_data;
+ struct pw_properties * const props = module->props;
+ struct pw_properties *stream_props = NULL;
+ const char *str, *server, *remote_source_name;
+ struct spa_audio_info_raw info = { 0 };
+ int res;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ stream_props = pw_properties_new(NULL, NULL);
+ if (stream_props == NULL) {
+ res = -ENOMEM;
+ goto out;
+ }
+
+ remote_source_name = pw_properties_get(props, "source");
+ if (remote_source_name)
+ pw_properties_set(props, PW_KEY_TARGET_OBJECT, remote_source_name);
+
+ if ((server = pw_properties_get(props, "server")) == NULL) {
+ pw_log_error("no server given");
+ res = -EINVAL;
+ goto out;
+ }
+
+ pw_properties_setf(stream_props, PW_KEY_NODE_DESCRIPTION,
+ _("Tunnel to %s/%s"), server,
+ remote_source_name ? remote_source_name : "");
+ pw_properties_set(stream_props, PW_KEY_MEDIA_CLASS, "Audio/Source");
+
+ if ((str = pw_properties_get(props, "source_name")) != NULL) {
+ pw_properties_set(stream_props, PW_KEY_NODE_NAME, str);
+ pw_properties_set(props, "source_name", NULL);
+ } else {
+ pw_properties_setf(stream_props, PW_KEY_NODE_NAME,
+ "tunnel-source.%s", server);
+ }
+ if ((str = pw_properties_get(props, "source_properties")) != NULL) {
+ module_args_add_props(stream_props, str);
+ pw_properties_set(props, "source_properties", NULL);
+ }
+ if (module_args_to_audioinfo(module->impl, props, &info) < 0) {
+ res = -EINVAL;
+ goto out;
+ }
+
+ audio_info_to_props(&info, stream_props);
+
+ d->module = module;
+ d->stream_props = stream_props;
+
+ pw_properties_fetch_uint32(props, "latency_msec", &d->latency_msec);
+
+ return 0;
+out:
+ pw_properties_free(stream_props);
+
+ return res;
+}
+
+DEFINE_MODULE_INFO(module_tunnel_source) = {
+ .name = "module-tunnel-source",
+ .prepare = module_tunnel_source_prepare,
+ .load = module_tunnel_source_load,
+ .unload = module_tunnel_source_unload,
+ .properties = &SPA_DICT_INIT_ARRAY(module_tunnel_source_info),
+ .data_size = sizeof(struct module_tunnel_source_data),
+};
diff --git a/src/modules/module-protocol-pulse/modules/module-x11-bell.c b/src/modules/module-protocol-pulse/modules/module-x11-bell.c
new file mode 100644
index 0000000..9d7e217
--- /dev/null
+++ b/src/modules/module-protocol-pulse/modules/module-x11-bell.c
@@ -0,0 +1,130 @@
+/* PipeWire
+ *
+ * Copyright © 2022 Wim Taymans <wim.taymans@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <pipewire/pipewire.h>
+
+#include "../module.h"
+
+#define NAME "x11-bell"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+struct module_x11_bell_data {
+ struct module *module;
+
+ struct pw_impl_module *mod;
+ struct spa_hook mod_listener;
+};
+
+static void module_destroy(void *data)
+{
+ struct module_x11_bell_data *d = data;
+ spa_hook_remove(&d->mod_listener);
+ d->mod = NULL;
+ module_schedule_unload(d->module);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy
+};
+
+static int module_x11_bell_load(struct module *module)
+{
+ struct module_x11_bell_data *data = module->user_data;
+ FILE *f;
+ char *args;
+ const char *str;
+ size_t size;
+
+ if ((f = open_memstream(&args, &size)) == NULL)
+ return -errno;
+
+ fprintf(f, "{");
+ if ((str = pw_properties_get(module->props, "sink")) != NULL)
+ fprintf(f, " sink.name = \"%s\"", str);
+ if ((str = pw_properties_get(module->props, "sample")) != NULL)
+ fprintf(f, " sample.name = \"%s\"", str);
+ if ((str = pw_properties_get(module->props, "display")) != NULL)
+ fprintf(f, " x11.display = \"%s\"", str);
+ if ((str = pw_properties_get(module->props, "xauthority")) != NULL)
+ fprintf(f, " x11.xauthority = \"%s\"", str);
+ fprintf(f, " }");
+ fclose(f);
+
+ data->mod = pw_context_load_module(module->impl->context,
+ "libpipewire-module-x11-bell",
+ args, NULL);
+ free(args);
+
+ if (data->mod == NULL)
+ return -errno;
+
+ pw_impl_module_add_listener(data->mod,
+ &data->mod_listener,
+ &module_events, data);
+ return 0;
+}
+
+static int module_x11_bell_unload(struct module *module)
+{
+ struct module_x11_bell_data *d = module->user_data;
+
+ if (d->mod) {
+ spa_hook_remove(&d->mod_listener);
+ pw_impl_module_destroy(d->mod);
+ d->mod = NULL;
+ }
+ return 0;
+}
+
+static int module_x11_bell_prepare(struct module * const module)
+{
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ struct module_x11_bell_data * const data = module->user_data;
+ data->module = module;
+
+ return 0;
+}
+
+static const struct spa_dict_item module_x11_bell_info[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "X11 bell interceptor" },
+ { PW_KEY_MODULE_USAGE, "sink=<sink to connect to> "
+ "sample=<the sample to play> "
+ "display=<X11 display> "
+ "xauthority=<X11 Authority>" },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+DEFINE_MODULE_INFO(module_x11_bell) = {
+ .name = "module-x11-bell",
+ .prepare = module_x11_bell_prepare,
+ .load = module_x11_bell_load,
+ .unload = module_x11_bell_unload,
+ .properties = &SPA_DICT_INIT_ARRAY(module_x11_bell_info),
+ .data_size = sizeof(struct module_x11_bell_data),
+};
diff --git a/src/modules/module-protocol-pulse/modules/module-zeroconf-discover.c b/src/modules/module-protocol-pulse/modules/module-zeroconf-discover.c
new file mode 100644
index 0000000..ccdaf27
--- /dev/null
+++ b/src/modules/module-protocol-pulse/modules/module-zeroconf-discover.c
@@ -0,0 +1,133 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans <wim.taymans@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <spa/utils/hook.h>
+#include <pipewire/pipewire.h>
+
+#include "../defs.h"
+#include "../module.h"
+
+#define NAME "zeroconf-discover"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+
+struct module_zeroconf_discover_data {
+ struct module *module;
+
+ struct spa_hook mod_listener;
+ struct pw_impl_module *mod;
+
+ uint32_t latency_msec;
+};
+
+static void module_destroy(void *data)
+{
+ struct module_zeroconf_discover_data *d = data;
+ spa_hook_remove(&d->mod_listener);
+ d->mod = NULL;
+ module_schedule_unload(d->module);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy
+};
+
+static int module_zeroconf_discover_load(struct module *module)
+{
+ struct module_zeroconf_discover_data *data = module->user_data;
+ FILE *f;
+ char *args;
+ size_t size;
+
+ if ((f = open_memstream(&args, &size)) == NULL)
+ return -errno;
+
+ fprintf(f, "{");
+ if (data->latency_msec > 0)
+ fprintf(f, " pulse.latency = %u ", data->latency_msec);
+ fprintf(f, "}");
+ fclose(f);
+
+ data->mod = pw_context_load_module(module->impl->context,
+ "libpipewire-module-zeroconf-discover",
+ args, NULL);
+
+ free(args);
+
+ if (data->mod == NULL)
+ return -errno;
+
+ pw_impl_module_add_listener(data->mod,
+ &data->mod_listener,
+ &module_events, data);
+
+ return 0;
+}
+
+static int module_zeroconf_discover_unload(struct module *module)
+{
+ struct module_zeroconf_discover_data *d = module->user_data;
+
+ if (d->mod) {
+ spa_hook_remove(&d->mod_listener);
+ pw_impl_module_destroy(d->mod);
+ d->mod = NULL;
+ }
+
+ return 0;
+}
+
+static const struct spa_dict_item module_zeroconf_discover_info[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.con>" },
+ { PW_KEY_MODULE_DESCRIPTION, "mDNS/DNS-SD Service Discovery" },
+ { PW_KEY_MODULE_USAGE,
+ "latency_msec=<fixed latency in ms> " },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+static int module_zeroconf_discover_prepare(struct module * const module)
+{
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ struct pw_properties * const props = module->props;
+ struct module_zeroconf_discover_data * const data = module->user_data;
+ data->module = module;
+
+ pw_properties_fetch_uint32(props, "latency_msec", &data->latency_msec);
+
+ return 0;
+}
+
+DEFINE_MODULE_INFO(module_zeroconf_discover) = {
+ .name = "module-zeroconf-discover",
+ .load_once = true,
+ .prepare = module_zeroconf_discover_prepare,
+ .load = module_zeroconf_discover_load,
+ .unload = module_zeroconf_discover_unload,
+ .properties = &SPA_DICT_INIT_ARRAY(module_zeroconf_discover_info),
+ .data_size = sizeof(struct module_zeroconf_discover_data),
+};
diff --git a/src/modules/module-protocol-pulse/modules/module-zeroconf-publish.c b/src/modules/module-protocol-pulse/modules/module-zeroconf-publish.c
new file mode 100644
index 0000000..2f04867
--- /dev/null
+++ b/src/modules/module-protocol-pulse/modules/module-zeroconf-publish.c
@@ -0,0 +1,746 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans <wim.taymans@gmail.com>
+ * Copyright © 2021 Sanchayan Maity <sanchayan@asymptotic.io>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <sys/utsname.h>
+#include <arpa/inet.h>
+
+#include <pipewire/pipewire.h>
+
+#include "../collect.h"
+#include "../defs.h"
+#include "../manager.h"
+#include "../module.h"
+#include "../pulse-server.h"
+#include "../server.h"
+#include "../../module-zeroconf-discover/avahi-poll.h"
+
+#include <avahi-client/client.h>
+#include <avahi-client/publish.h>
+#include <avahi-common/alternative.h>
+#include <avahi-common/error.h>
+#include <avahi-common/domain.h>
+#include <avahi-common/malloc.h>
+
+#define NAME "zeroconf-publish"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+#define SERVICE_TYPE_SINK "_pulse-sink._tcp"
+#define SERVICE_TYPE_SOURCE "_pulse-source._tcp"
+#define SERVICE_TYPE_SERVER "_pulse-server._tcp"
+#define SERVICE_SUBTYPE_SINK_HARDWARE "_hardware._sub."SERVICE_TYPE_SINK
+#define SERVICE_SUBTYPE_SINK_VIRTUAL "_virtual._sub."SERVICE_TYPE_SINK
+#define SERVICE_SUBTYPE_SOURCE_HARDWARE "_hardware._sub."SERVICE_TYPE_SOURCE
+#define SERVICE_SUBTYPE_SOURCE_VIRTUAL "_virtual._sub."SERVICE_TYPE_SOURCE
+#define SERVICE_SUBTYPE_SOURCE_MONITOR "_monitor._sub."SERVICE_TYPE_SOURCE
+#define SERVICE_SUBTYPE_SOURCE_NON_MONITOR "_non-monitor._sub."SERVICE_TYPE_SOURCE
+
+#define SERVICE_DATA_ID "module-zeroconf-publish.service"
+
+enum service_subtype {
+ SUBTYPE_HARDWARE,
+ SUBTYPE_VIRTUAL,
+ SUBTYPE_MONITOR
+};
+
+struct service {
+ struct spa_list link;
+
+ struct module_zeroconf_publish_data *userdata;
+
+ AvahiEntryGroup *entry_group;
+ AvahiStringList *txt;
+ struct server *server;
+
+ const char *service_type;
+ enum service_subtype subtype;
+
+ char *name;
+ bool is_sink;
+
+ struct sample_spec ss;
+ struct channel_map cm;
+ struct pw_properties *props;
+
+ char service_name[AVAHI_LABEL_MAX];
+ unsigned published:1;
+};
+
+struct module_zeroconf_publish_data {
+ struct module *module;
+
+ struct pw_core *core;
+ struct pw_manager *manager;
+
+ struct spa_hook core_listener;
+ struct spa_hook manager_listener;
+ struct spa_hook impl_listener;
+
+ AvahiPoll *avahi_poll;
+ AvahiClient *client;
+
+ /* lists of services */
+ struct spa_list pending;
+ struct spa_list published;
+};
+
+static void on_core_error(void *data, uint32_t id, int seq, int res, const char *message)
+{
+ struct module_zeroconf_publish_data *d = data;
+ struct module *module = d->module;
+
+ pw_log_error("error id:%u seq:%d res:%d (%s): %s",
+ id, seq, res, spa_strerror(res), message);
+
+ if (id == PW_ID_CORE && res == -EPIPE)
+ module_schedule_unload(module);
+}
+
+static const struct pw_core_events core_events = {
+ PW_VERSION_CORE_EVENTS,
+ .error = on_core_error,
+};
+
+static void get_service_name(struct pw_manager_object *o, char *buf, size_t length)
+{
+ const char *hn, *un, *n;
+
+ hn = pw_get_host_name();
+ un = pw_get_user_name();
+ n = pw_properties_get(o->props, PW_KEY_NODE_DESCRIPTION);
+
+ snprintf(buf, length, "%s@%s: %s", un, hn, n);
+}
+
+static void service_free(struct service *s)
+{
+ pw_log_debug("service %p: free", s);
+
+ if (s->entry_group)
+ avahi_entry_group_free(s->entry_group);
+
+ if (s->name)
+ free(s->name);
+
+ pw_properties_free(s->props);
+ avahi_string_list_free(s->txt);
+ spa_list_remove(&s->link);
+}
+
+static void unpublish_service(struct service *s)
+{
+ spa_list_remove(&s->link);
+ spa_list_append(&s->userdata->pending, &s->link);
+ s->published = false;
+ s->server = NULL;
+}
+
+static void unpublish_all_services(struct module_zeroconf_publish_data *d)
+{
+ struct service *s;
+
+ spa_list_consume(s, &d->published, link)
+ unpublish_service(s);
+}
+
+static char* channel_map_snprint(char *s, size_t l, const struct channel_map *map)
+{
+ unsigned channel;
+ bool first = true;
+ char *e;
+ uint32_t aux = 0;
+
+ spa_assert(s);
+ spa_assert(l > 0);
+ spa_assert(map);
+
+ if (!channel_map_valid(map)) {
+ snprintf(s, l, "(invalid)");
+ return s;
+ }
+
+ *(e = s) = 0;
+
+ for (channel = 0; channel < map->channels && l > 1; channel++) {
+ l -= spa_scnprintf(e, l, "%s%s",
+ first ? "" : ",",
+ channel_id2paname(map->map[channel], &aux));
+
+ e = strchr(e, 0);
+ first = false;
+ }
+
+ return s;
+}
+
+static void fill_service_data(struct module_zeroconf_publish_data *d, struct service *s,
+ struct pw_manager_object *o)
+{
+ struct impl *impl = d->module->impl;
+ bool is_sink = pw_manager_object_is_sink(o);
+ bool is_source = pw_manager_object_is_source(o);
+ struct pw_node_info *info = o->info;
+ const char *name, *desc, *str;
+ uint32_t card_id = SPA_ID_INVALID;
+ struct pw_manager *manager = d->manager;
+ struct pw_manager_object *card = NULL;
+ struct card_info card_info = CARD_INFO_INIT;
+ struct device_info dev_info = is_sink ?
+ DEVICE_INFO_INIT(PW_DIRECTION_OUTPUT) : DEVICE_INFO_INIT(PW_DIRECTION_INPUT);
+ uint32_t flags = 0;
+
+ if (info == NULL || info->props == NULL)
+ return;
+
+ name = spa_dict_lookup(info->props, PW_KEY_NODE_NAME);
+ if ((desc = spa_dict_lookup(info->props, PW_KEY_NODE_DESCRIPTION)) == NULL)
+ desc = name ? name : "Unknown";
+ if (name == NULL)
+ name = "unknown";
+
+ if ((str = spa_dict_lookup(info->props, PW_KEY_DEVICE_ID)) != NULL)
+ card_id = (uint32_t)atoi(str);
+ if ((str = spa_dict_lookup(info->props, "card.profile.device")) != NULL)
+ dev_info.device = (uint32_t)atoi(str);
+ if (card_id != SPA_ID_INVALID) {
+ struct selector sel = { .id = card_id, .type = pw_manager_object_is_card, };
+ card = select_object(manager, &sel);
+ }
+ if (card)
+ collect_card_info(card, &card_info);
+
+ collect_device_info(o, card, &dev_info, false, &impl->defs);
+
+ if ((str = spa_dict_lookup(info->props, PW_KEY_DEVICE_API)) != NULL) {
+ if (is_sink)
+ flags |= SINK_HARDWARE;
+ else if (is_source)
+ flags |= SOURCE_HARDWARE;
+ }
+
+ s->ss = dev_info.ss;
+ s->cm = dev_info.map;
+ s->name = strdup(name);
+ s->props = pw_properties_copy(o->props);
+
+ if (is_sink) {
+ s->is_sink = true;
+ s->service_type = SERVICE_TYPE_SINK;
+ s->subtype = flags & SINK_HARDWARE ? SUBTYPE_HARDWARE : SUBTYPE_VIRTUAL;
+ } else if (is_source) {
+ s->is_sink = false;
+ s->service_type = SERVICE_TYPE_SOURCE;
+ s->subtype = flags & SOURCE_HARDWARE ? SUBTYPE_HARDWARE : SUBTYPE_VIRTUAL;
+ } else
+ spa_assert_not_reached();
+}
+
+static struct service *create_service(struct module_zeroconf_publish_data *d, struct pw_manager_object *o)
+{
+ struct service *s;
+
+ s = pw_manager_object_add_data(o, SERVICE_DATA_ID, sizeof(*s));
+ if (s == NULL)
+ return NULL;
+
+ s->userdata = d;
+ s->entry_group = NULL;
+ get_service_name(o, s->service_name, sizeof(s->service_name));
+ spa_list_append(&d->pending, &s->link);
+
+ fill_service_data(d, s, o);
+
+ pw_log_debug("service %p: created for object %p", s, o);
+
+ return s;
+}
+
+static AvahiStringList* txt_record_server_data(struct pw_core_info *info, AvahiStringList *l)
+{
+ const char *t;
+ struct utsname u;
+
+ spa_assert(info);
+
+ l = avahi_string_list_add_pair(l, "server-version", PACKAGE_NAME" "PACKAGE_VERSION);
+
+ t = pw_get_user_name();
+ l = avahi_string_list_add_pair(l, "user-name", t);
+
+ if (uname(&u) >= 0) {
+ char sysname[sizeof(u.sysname) + sizeof(u.machine) + sizeof(u.release)];
+
+ snprintf(sysname, sizeof(sysname), "%s %s %s", u.sysname, u.machine, u.release);
+ l = avahi_string_list_add_pair(l, "uname", sysname);
+ }
+
+ t = pw_get_host_name();
+ l = avahi_string_list_add_pair(l, "fqdn", t);
+ l = avahi_string_list_add_printf(l, "cookie=0x%08x", info->cookie);
+
+ return l;
+}
+
+static void clear_entry_group(struct service *s)
+{
+ if (s->entry_group == NULL)
+ return;
+
+ avahi_entry_group_free(s->entry_group);
+ s->entry_group = NULL;
+}
+
+static void publish_service(struct service *s);
+
+static void service_entry_group_callback(AvahiEntryGroup *g, AvahiEntryGroupState state, void *userdata)
+{
+ struct service *s = userdata;
+
+ spa_assert(s);
+ if (!s->published) {
+ pw_log_info("cancel unpublished service: %s", s->service_name);
+ clear_entry_group(s);
+ return;
+ }
+
+ switch (state) {
+ case AVAHI_ENTRY_GROUP_ESTABLISHED:
+ pw_log_info("established service: %s", s->service_name);
+ break;
+ case AVAHI_ENTRY_GROUP_COLLISION:
+ {
+ char *t;
+
+ t = avahi_alternative_service_name(s->service_name);
+ pw_log_info("service name collision: renaming '%s' to '%s'", s->service_name, t);
+ snprintf(s->service_name, sizeof(s->service_name), "%s", t);
+ avahi_free(t);
+
+ unpublish_service(s);
+ publish_service(s);
+ break;
+ }
+ case AVAHI_ENTRY_GROUP_FAILURE:
+ pw_log_error("failed to establish service '%s': %s",
+ s->service_name,
+ avahi_strerror(avahi_client_errno(avahi_entry_group_get_client(g))));
+ unpublish_service(s);
+ clear_entry_group(s);
+ break;
+
+ case AVAHI_ENTRY_GROUP_UNCOMMITED:
+ case AVAHI_ENTRY_GROUP_REGISTERING:
+ break;
+ }
+}
+
+#define PA_CHANNEL_MAP_SNPRINT_MAX (CHANNELS_MAX * 32)
+
+static AvahiStringList *get_service_txt(const struct service *s)
+{
+ static const char * const subtype_text[] = {
+ [SUBTYPE_HARDWARE] = "hardware",
+ [SUBTYPE_VIRTUAL] = "virtual",
+ [SUBTYPE_MONITOR] = "monitor"
+ };
+
+ static const struct mapping {
+ const char *pw_key, *txt_key;
+ } mappings[] = {
+ { PW_KEY_NODE_DESCRIPTION, "description" },
+ { PW_KEY_DEVICE_VENDOR_NAME, "vendor-name" },
+ { PW_KEY_DEVICE_PRODUCT_NAME, "product-name" },
+ { PW_KEY_DEVICE_CLASS, "class" },
+ { PW_KEY_DEVICE_FORM_FACTOR, "form-factor" },
+ { PW_KEY_DEVICE_ICON_NAME, "icon-name" },
+ };
+
+ char cm[PA_CHANNEL_MAP_SNPRINT_MAX];
+ AvahiStringList *txt = NULL;
+
+ txt = txt_record_server_data(s->userdata->manager->info, txt);
+
+ txt = avahi_string_list_add_pair(txt, "device", s->name);
+ txt = avahi_string_list_add_printf(txt, "rate=%u", s->ss.rate);
+ txt = avahi_string_list_add_printf(txt, "channels=%u", s->ss.channels);
+ txt = avahi_string_list_add_pair(txt, "format", format_id2paname(s->ss.format));
+ txt = avahi_string_list_add_pair(txt, "channel_map", channel_map_snprint(cm, sizeof(cm), &s->cm));
+ txt = avahi_string_list_add_pair(txt, "subtype", subtype_text[s->subtype]);
+
+ SPA_FOR_EACH_ELEMENT_VAR(mappings, m) {
+ const char *value = pw_properties_get(s->props, m->pw_key);
+ if (value != NULL)
+ txt = avahi_string_list_add_pair(txt, m->txt_key, value);
+ }
+
+ return txt;
+}
+
+static struct server *find_server(struct service *s, int *proto, uint16_t *port)
+{
+ struct module_zeroconf_publish_data *d = s->userdata;
+ struct impl *impl = d->module->impl;
+ struct server *server;
+
+ spa_list_for_each(server, &impl->servers, link) {
+ if (server->addr.ss_family == AF_INET) {
+ *proto = AVAHI_PROTO_INET;
+ *port = ntohs(((struct sockaddr_in*) &server->addr)->sin_port);
+ return server;
+ } else if (server->addr.ss_family == AF_INET6) {
+ *proto = AVAHI_PROTO_INET6;
+ *port = ntohs(((struct sockaddr_in6*) &server->addr)->sin6_port);
+ return server;
+ }
+ }
+
+ return NULL;
+}
+
+static void publish_service(struct service *s)
+{
+ struct module_zeroconf_publish_data *d = s->userdata;
+ int proto;
+ uint16_t port;
+
+ struct server *server = find_server(s, &proto, &port);
+ if (!server)
+ return;
+
+ pw_log_debug("found server:%p proto:%d port:%d", server, proto, port);
+
+ if (!d->client || avahi_client_get_state(d->client) != AVAHI_CLIENT_S_RUNNING)
+ return;
+
+ s->published = true;
+ if (!s->entry_group) {
+ s->entry_group = avahi_entry_group_new(d->client, service_entry_group_callback, s);
+ if (s->entry_group == NULL) {
+ pw_log_error("avahi_entry_group_new(): %s",
+ avahi_strerror(avahi_client_errno(d->client)));
+ goto error;
+ }
+ } else {
+ avahi_entry_group_reset(s->entry_group);
+ }
+
+ if (s->txt == NULL)
+ s->txt = get_service_txt(s);
+
+ if (avahi_entry_group_add_service_strlst(
+ s->entry_group,
+ AVAHI_IF_UNSPEC, proto,
+ 0,
+ s->service_name,
+ s->service_type,
+ NULL,
+ NULL,
+ port,
+ s->txt) < 0) {
+ pw_log_error("avahi_entry_group_add_service_strlst(): %s",
+ avahi_strerror(avahi_client_errno(d->client)));
+ goto error;
+ }
+
+ if (avahi_entry_group_add_service_subtype(
+ s->entry_group,
+ AVAHI_IF_UNSPEC, proto,
+ 0,
+ s->service_name,
+ s->service_type,
+ NULL,
+ s->is_sink ? (s->subtype == SUBTYPE_HARDWARE ? SERVICE_SUBTYPE_SINK_HARDWARE : SERVICE_SUBTYPE_SINK_VIRTUAL) :
+ (s->subtype == SUBTYPE_HARDWARE ? SERVICE_SUBTYPE_SOURCE_HARDWARE : (s->subtype == SUBTYPE_VIRTUAL ? SERVICE_SUBTYPE_SOURCE_VIRTUAL : SERVICE_SUBTYPE_SOURCE_MONITOR))) < 0) {
+
+ pw_log_error("avahi_entry_group_add_service_subtype(): %s",
+ avahi_strerror(avahi_client_errno(d->client)));
+ goto error;
+ }
+
+ if (!s->is_sink && s->subtype != SUBTYPE_MONITOR) {
+ if (avahi_entry_group_add_service_subtype(
+ s->entry_group,
+ AVAHI_IF_UNSPEC, proto,
+ 0,
+ s->service_name,
+ SERVICE_TYPE_SOURCE,
+ NULL,
+ SERVICE_SUBTYPE_SOURCE_NON_MONITOR) < 0) {
+ pw_log_error("avahi_entry_group_add_service_subtype(): %s",
+ avahi_strerror(avahi_client_errno(d->client)));
+ goto error;
+ }
+ }
+
+ if (avahi_entry_group_commit(s->entry_group) < 0) {
+ pw_log_error("avahi_entry_group_commit(): %s",
+ avahi_strerror(avahi_client_errno(d->client)));
+ goto error;
+ }
+
+ spa_list_remove(&s->link);
+ spa_list_append(&d->published, &s->link);
+ s->server = server;
+
+ pw_log_info("created service: %s", s->service_name);
+ return;
+
+error:
+ s->published = false;
+ return;
+}
+
+static void publish_pending(struct module_zeroconf_publish_data *data)
+{
+ struct service *s, *next;
+
+ spa_list_for_each_safe(s, next, &data->pending, link)
+ publish_service(s);
+}
+
+static void clear_pending_entry_groups(struct module_zeroconf_publish_data *data)
+{
+ struct service *s;
+
+ spa_list_for_each(s, &data->pending, link)
+ clear_entry_group(s);
+}
+
+static void client_callback(AvahiClient *c, AvahiClientState state, void *d)
+{
+ struct module_zeroconf_publish_data *data = d;
+
+ spa_assert(c);
+ spa_assert(data);
+
+ data->client = c;
+
+ switch (state) {
+ case AVAHI_CLIENT_S_RUNNING:
+ pw_log_info("the avahi daemon is up and running");
+ publish_pending(data);
+ break;
+ case AVAHI_CLIENT_S_COLLISION:
+ pw_log_error("host name collision");
+ unpublish_all_services(d);
+ break;
+ case AVAHI_CLIENT_FAILURE:
+ {
+ int err = avahi_client_errno(data->client);
+
+ pw_log_error("avahi client failure: %s", avahi_strerror(err));
+
+ unpublish_all_services(data);
+ clear_pending_entry_groups(data);
+ avahi_client_free(data->client);
+ data->client = NULL;
+
+ if (err == AVAHI_ERR_DISCONNECTED) {
+ data->client = avahi_client_new(data->avahi_poll, AVAHI_CLIENT_NO_FAIL, client_callback, data, &err);
+ if (data->client == NULL)
+ pw_log_error("failed to create avahi client: %s", avahi_strerror(err));
+ }
+
+ if (data->client == NULL)
+ module_schedule_unload(data->module);
+
+ break;
+ }
+ case AVAHI_CLIENT_CONNECTING:
+ pw_log_info("connecting to the avahi daemon...");
+ break;
+ default:
+ break;
+ }
+}
+
+static void manager_removed(void *d, struct pw_manager_object *o)
+{
+ if (!pw_manager_object_is_sink(o) && !pw_manager_object_is_source(o))
+ return;
+
+ struct service *s = pw_manager_object_get_data(o, SERVICE_DATA_ID);
+ if (s == NULL)
+ return;
+
+ service_free(s);
+}
+
+static void manager_added(void *d, struct pw_manager_object *o)
+{
+ struct service *s;
+ struct pw_node_info *info;
+ const char *str;
+
+ if (!pw_manager_object_is_sink(o) && !pw_manager_object_is_source(o))
+ return;
+
+ info = o->info;
+ if (info == NULL || info->props == NULL)
+ return;
+
+ if ((str = spa_dict_lookup(info->props, PW_KEY_NODE_NETWORK)) != NULL &&
+ spa_atob(str))
+ return;
+
+ s = create_service(d, o);
+ if (s == NULL)
+ return;
+
+ publish_service(s);
+}
+
+static const struct pw_manager_events manager_events = {
+ PW_VERSION_MANAGER_EVENTS,
+ .added = manager_added,
+ .removed = manager_removed,
+};
+
+
+static void impl_server_started(void *data, struct server *server)
+{
+ struct module_zeroconf_publish_data *d = data;
+ pw_log_info("a new server is started, try publish");
+ publish_pending(d);
+}
+
+static void impl_server_stopped(void *data, struct server *server)
+{
+ struct module_zeroconf_publish_data *d = data;
+ pw_log_info("a server stopped, try republish");
+
+ struct service *s, *tmp;
+ spa_list_for_each_safe(s, tmp, &d->published, link) {
+ if (s->server == server)
+ unpublish_service(s);
+ }
+
+ publish_pending(d);
+}
+
+static const struct impl_events impl_events = {
+ VERSION_IMPL_EVENTS,
+ .server_started = impl_server_started,
+ .server_stopped = impl_server_stopped,
+};
+
+static int module_zeroconf_publish_load(struct module *module)
+{
+ struct module_zeroconf_publish_data *data = module->user_data;
+ struct pw_loop *loop;
+ int error;
+
+ data->core = pw_context_connect(module->impl->context, NULL, 0);
+ if (data->core == NULL) {
+ pw_log_error("failed to connect to pipewire: %m");
+ return -errno;
+ }
+
+ pw_core_add_listener(data->core,
+ &data->core_listener,
+ &core_events, data);
+
+ loop = pw_context_get_main_loop(module->impl->context);
+ data->avahi_poll = pw_avahi_poll_new(loop);
+
+ data->client = avahi_client_new(data->avahi_poll, AVAHI_CLIENT_NO_FAIL,
+ client_callback, data, &error);
+ if (!data->client) {
+ pw_log_error("failed to create avahi client: %s", avahi_strerror(error));
+ return -errno;
+ }
+
+ data->manager = pw_manager_new(data->core);
+ if (data->manager == NULL) {
+ pw_log_error("failed to create pipewire manager: %m");
+ return -errno;
+ }
+
+ pw_manager_add_listener(data->manager, &data->manager_listener,
+ &manager_events, data);
+
+ impl_add_listener(module->impl, &data->impl_listener, &impl_events, data);
+
+ return 0;
+}
+
+static int module_zeroconf_publish_unload(struct module *module)
+{
+ struct module_zeroconf_publish_data *d = module->user_data;
+ struct service *s;
+
+ spa_hook_remove(&d->impl_listener);
+
+ unpublish_all_services(d);
+
+ spa_list_consume(s, &d->pending, link)
+ service_free(s);
+
+ if (d->client)
+ avahi_client_free(d->client);
+
+ if (d->avahi_poll)
+ pw_avahi_poll_free(d->avahi_poll);
+
+ if (d->manager != NULL) {
+ spa_hook_remove(&d->manager_listener);
+ pw_manager_destroy(d->manager);
+ }
+
+ if (d->core != NULL) {
+ spa_hook_remove(&d->core_listener);
+ pw_core_disconnect(d->core);
+ }
+
+ return 0;
+}
+
+static const struct spa_dict_item module_zeroconf_publish_info[] = {
+ { PW_KEY_MODULE_AUTHOR, "Sanchayan Maity <sanchayan@asymptotic.io" },
+ { PW_KEY_MODULE_DESCRIPTION, "mDNS/DNS-SD Service Publish" },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+static int module_zeroconf_publish_prepare(struct module * const module)
+{
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ struct module_zeroconf_publish_data * const data = module->user_data;
+ data->module = module;
+ spa_list_init(&data->pending);
+ spa_list_init(&data->published);
+
+ return 0;
+}
+
+DEFINE_MODULE_INFO(module_zeroconf_publish) = {
+ .name = "module-zeroconf-publish",
+ .prepare = module_zeroconf_publish_prepare,
+ .load = module_zeroconf_publish_load,
+ .unload = module_zeroconf_publish_unload,
+ .properties = &SPA_DICT_INIT_ARRAY(module_zeroconf_publish_info),
+ .data_size = sizeof(struct module_zeroconf_publish_data),
+};
diff --git a/src/modules/module-protocol-pulse/operation.c b/src/modules/module-protocol-pulse/operation.c
new file mode 100644
index 0000000..1c9833d
--- /dev/null
+++ b/src/modules/module-protocol-pulse/operation.c
@@ -0,0 +1,92 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdlib.h>
+
+#include <spa/utils/list.h>
+#include <pipewire/log.h>
+
+#include "client.h"
+#include "log.h"
+#include "manager.h"
+#include "operation.h"
+#include "reply.h"
+
+int operation_new_cb(struct client *client, uint32_t tag,
+ void (*callback)(void *data, struct client *client, uint32_t tag),
+ void *data)
+{
+ struct operation *o;
+
+ if ((o = calloc(1, sizeof(*o))) == NULL)
+ return -errno;
+
+ o->client = client;
+ o->tag = tag;
+ o->callback = callback;
+ o->data = data;
+
+ spa_list_append(&client->operations, &o->link);
+ pw_manager_sync(client->manager);
+
+ pw_log_debug("client %p [%s]: new operation tag:%u", client, client->name, tag);
+
+ return 0;
+}
+
+int operation_new(struct client *client, uint32_t tag)
+{
+ return operation_new_cb(client, tag, NULL, NULL);
+}
+
+void operation_free(struct operation *o)
+{
+ spa_list_remove(&o->link);
+ free(o);
+}
+
+struct operation *operation_find(struct client *client, uint32_t tag)
+{
+ struct operation *o;
+ spa_list_for_each(o, &client->operations, link) {
+ if (o->tag == tag)
+ return o;
+ }
+ return NULL;
+}
+
+void operation_complete(struct operation *o)
+{
+ struct client *client = o->client;
+
+ pw_log_info("[%s]: tag:%u complete", client->name, o->tag);
+
+ spa_list_remove(&o->link);
+
+ if (o->callback)
+ o->callback(o->data, client, o->tag);
+ else
+ reply_simple_ack(client, o->tag);
+ free(o);
+}
diff --git a/src/modules/module-protocol-pulse/operation.h b/src/modules/module-protocol-pulse/operation.h
new file mode 100644
index 0000000..1fa07cc
--- /dev/null
+++ b/src/modules/module-protocol-pulse/operation.h
@@ -0,0 +1,50 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PULSER_SERVER_OPERATION_H
+#define PULSER_SERVER_OPERATION_H
+
+#include <stdint.h>
+
+#include <spa/utils/list.h>
+
+struct client;
+
+struct operation {
+ struct spa_list link;
+ struct client *client;
+ uint32_t tag;
+ void (*callback) (void *data, struct client *client, uint32_t tag);
+ void *data;
+};
+
+int operation_new(struct client *client, uint32_t tag);
+int operation_new_cb(struct client *client, uint32_t tag,
+ void (*callback) (void *data, struct client *client, uint32_t tag),
+ void *data);
+struct operation *operation_find(struct client *client, uint32_t tag);
+void operation_free(struct operation *o);
+void operation_complete(struct operation *o);
+
+#endif /* PULSER_SERVER_OPERATION_H */
diff --git a/src/modules/module-protocol-pulse/pending-sample.c b/src/modules/module-protocol-pulse/pending-sample.c
new file mode 100644
index 0000000..399fc3b
--- /dev/null
+++ b/src/modules/module-protocol-pulse/pending-sample.c
@@ -0,0 +1,50 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <spa/utils/list.h>
+#include <spa/utils/hook.h>
+#include <pipewire/work-queue.h>
+
+#include "client.h"
+#include "internal.h"
+#include "log.h"
+#include "operation.h"
+#include "pending-sample.h"
+#include "sample-play.h"
+
+void pending_sample_free(struct pending_sample *ps)
+{
+ struct client * const client = ps->client;
+ struct impl * const impl = client->impl;
+ struct operation *o;
+
+ spa_list_remove(&ps->link);
+ spa_hook_remove(&ps->listener);
+ pw_work_queue_cancel(impl->work_queue, ps, SPA_ID_INVALID);
+
+ if ((o = operation_find(client, ps->tag)) != NULL)
+ operation_free(o);
+
+ sample_play_destroy(ps->play);
+}
diff --git a/src/modules/module-protocol-pulse/pending-sample.h b/src/modules/module-protocol-pulse/pending-sample.h
new file mode 100644
index 0000000..f467ca7
--- /dev/null
+++ b/src/modules/module-protocol-pulse/pending-sample.h
@@ -0,0 +1,48 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PULSE_SERVER_PENDING_SAMPLE_H
+#define PULSE_SERVER_PENDING_SAMPLE_H
+
+#include <stdint.h>
+
+#include <spa/utils/list.h>
+#include <spa/utils/hook.h>
+
+struct client;
+struct sample_play;
+
+struct pending_sample {
+ struct spa_list link;
+ struct client *client;
+ struct sample_play *play;
+ struct spa_hook listener;
+ uint32_t tag;
+ unsigned ready:1;
+ unsigned done:1;
+};
+
+void pending_sample_free(struct pending_sample *ps);
+
+#endif /* PULSE_SERVER_PENDING_SAMPLE_H */
diff --git a/src/modules/module-protocol-pulse/pulse-server.c b/src/modules/module-protocol-pulse/pulse-server.c
new file mode 100644
index 0000000..41a814a
--- /dev/null
+++ b/src/modules/module-protocol-pulse/pulse-server.c
@@ -0,0 +1,5689 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "config.h"
+
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include <sys/time.h>
+
+#include <pipewire/log.h>
+
+#include "log.h"
+
+#include <spa/support/cpu.h>
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+#include <spa/debug/dict.h>
+#include <spa/debug/mem.h>
+#include <spa/debug/types.h>
+#include <spa/param/audio/raw.h>
+#include <spa/pod/pod.h>
+#include <spa/param/audio/format-utils.h>
+#include <spa/param/props.h>
+#include <spa/utils/ringbuffer.h>
+#include <spa/utils/json.h>
+
+#include <pipewire/pipewire.h>
+#include <pipewire/extensions/metadata.h>
+
+#include "pulse-server.h"
+#include "client.h"
+#include "collect.h"
+#include "commands.h"
+#include "cmd.h"
+#include "dbus-name.h"
+#include "defs.h"
+#include "extension.h"
+#include "format.h"
+#include "internal.h"
+#include "manager.h"
+#include "message.h"
+#include "message-handler.h"
+#include "module.h"
+#include "operation.h"
+#include "pending-sample.h"
+#include "quirks.h"
+#include "reply.h"
+#include "sample.h"
+#include "sample-play.h"
+#include "server.h"
+#include "stream.h"
+#include "utils.h"
+#include "volume.h"
+
+#define DEFAULT_MIN_REQ "256/48000"
+#define DEFAULT_DEFAULT_REQ "960/48000"
+#define DEFAULT_MIN_FRAG "256/48000"
+#define DEFAULT_DEFAULT_FRAG "96000/48000"
+#define DEFAULT_DEFAULT_TLENGTH "96000/48000"
+#define DEFAULT_MIN_QUANTUM "256/48000"
+#define DEFAULT_FORMAT "F32"
+#define DEFAULT_POSITION "[ FL FR ]"
+#define DEFAULT_IDLE_TIMEOUT "0"
+
+#define MAX_FORMATS 32
+/* The max amount of data we send in one block when capturing. In PulseAudio this
+ * size is derived from the mempool PA_MEMPOOL_SLOT_SIZE */
+#define MAX_BLOCK (64*1024)
+
+#define TEMPORARY_MOVE_TIMEOUT (SPA_NSEC_PER_SEC)
+
+PW_LOG_TOPIC_EXTERN(pulse_conn);
+
+bool debug_messages = false;
+
+struct latency_offset_data {
+ int64_t prev_latency_offset;
+ uint8_t initialized:1;
+};
+
+struct temporary_move_data {
+ uint32_t peer_index;
+ uint8_t used:1;
+};
+
+static struct sample *find_sample(struct impl *impl, uint32_t index, const char *name)
+{
+ union pw_map_item *item;
+
+ if (index != SPA_ID_INVALID)
+ return pw_map_lookup(&impl->samples, index);
+
+ pw_array_for_each(item, &impl->samples.items) {
+ struct sample *s = item->data;
+ if (!pw_map_item_is_free(item) &&
+ spa_streq(s->name, name))
+ return s;
+ }
+ return NULL;
+}
+
+void broadcast_subscribe_event(struct impl *impl, uint32_t mask, uint32_t event, uint32_t index)
+{
+ struct server *s;
+ spa_list_for_each(s, &impl->servers, link) {
+ struct client *c;
+ spa_list_for_each(c, &s->clients, link)
+ client_queue_subscribe_event(c, mask, event, index);
+ }
+}
+
+static int do_command_auth(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ struct message *reply;
+ uint32_t version;
+ const void *cookie;
+ size_t len;
+
+ if (message_get(m,
+ TAG_U32, &version,
+ TAG_ARBITRARY, &cookie, &len,
+ TAG_INVALID) < 0) {
+ return -EPROTO;
+ }
+ if (version < 8)
+ return -EPROTO;
+ if (len != NATIVE_COOKIE_LENGTH)
+ return -EINVAL;
+
+ if ((version & PROTOCOL_VERSION_MASK) >= 13)
+ version &= PROTOCOL_VERSION_MASK;
+
+ client->version = version;
+ client->authenticated = true;
+
+ pw_log_info("client:%p AUTH tag:%u version:%d", client, tag, version);
+
+ reply = reply_new(client, tag);
+ message_put(reply,
+ TAG_U32, PROTOCOL_VERSION,
+ TAG_INVALID);
+
+ return client_queue_message(client, reply);
+}
+
+static int reply_set_client_name(struct client *client, uint32_t tag)
+{
+ struct pw_manager *manager = client->manager;
+ struct message *reply;
+ struct pw_client *c;
+ uint32_t id, index;
+
+ c = pw_core_get_client(client->core);
+ if (c == NULL)
+ return -ENOENT;
+
+ id = pw_proxy_get_bound_id((struct pw_proxy*)c);
+ index = id_to_index(manager, id);
+
+ pw_log_info("[%s] reply tag:%u id:%u index:%u", client->name, tag, id, index);
+
+ reply = reply_new(client, tag);
+
+ if (client->version >= 13) {
+ message_put(reply,
+ TAG_U32, index, /* client index */
+ TAG_INVALID);
+ }
+ return client_queue_message(client, reply);
+}
+
+static void manager_sync(void *data)
+{
+ struct client *client = data;
+ struct operation *o;
+
+ pw_log_debug("%p: manager sync", client);
+
+ if (client->connect_tag != SPA_ID_INVALID) {
+ reply_set_client_name(client, client->connect_tag);
+ client->connect_tag = SPA_ID_INVALID;
+ }
+
+ client->ref++;
+ spa_list_consume(o, &client->operations, link)
+ operation_complete(o);
+ client_unref(client);
+}
+
+static struct stream *find_stream(struct client *client, uint32_t index)
+{
+ union pw_map_item *item;
+ pw_array_for_each(item, &client->streams.items) {
+ struct stream *s = item->data;
+ if (!pw_map_item_is_free(item) &&
+ s->index == index)
+ return s;
+ }
+ return NULL;
+}
+
+static int send_object_event(struct client *client, struct pw_manager_object *o,
+ uint32_t type)
+{
+ uint32_t event = 0, mask = 0, res_index = o->index;
+
+ if (pw_manager_object_is_sink(o)) {
+ client_queue_subscribe_event(client,
+ SUBSCRIPTION_MASK_SINK,
+ SUBSCRIPTION_EVENT_SINK | type,
+ res_index);
+ }
+ if (pw_manager_object_is_source_or_monitor(o)) {
+ mask = SUBSCRIPTION_MASK_SOURCE;
+ event = SUBSCRIPTION_EVENT_SOURCE;
+ }
+ else if (pw_manager_object_is_sink_input(o)) {
+ mask = SUBSCRIPTION_MASK_SINK_INPUT;
+ event = SUBSCRIPTION_EVENT_SINK_INPUT;
+ }
+ else if (pw_manager_object_is_source_output(o)) {
+ mask = SUBSCRIPTION_MASK_SOURCE_OUTPUT;
+ event = SUBSCRIPTION_EVENT_SOURCE_OUTPUT;
+ }
+ else if (pw_manager_object_is_module(o)) {
+ mask = SUBSCRIPTION_MASK_MODULE;
+ event = SUBSCRIPTION_EVENT_MODULE;
+ }
+ else if (pw_manager_object_is_client(o)) {
+ mask = SUBSCRIPTION_MASK_CLIENT;
+ event = SUBSCRIPTION_EVENT_CLIENT;
+ }
+ else if (pw_manager_object_is_card(o)) {
+ mask = SUBSCRIPTION_MASK_CARD;
+ event = SUBSCRIPTION_EVENT_CARD;
+ } else
+ event = SPA_ID_INVALID;
+
+ if (event != SPA_ID_INVALID)
+ client_queue_subscribe_event(client,
+ mask,
+ event | type,
+ res_index);
+ return 0;
+}
+
+static uint32_t get_temporary_move_target(struct client *client, struct pw_manager_object *o)
+{
+ struct temporary_move_data *d;
+
+ d = pw_manager_object_get_data(o, "temporary_move_data");
+ if (d == NULL || d->peer_index == SPA_ID_INVALID)
+ return SPA_ID_INVALID;
+
+ pw_log_debug("[%s] using temporary move target for index:%d -> index:%d",
+ client->name, o->index, d->peer_index);
+ d->used = true;
+ return d->peer_index;
+}
+
+static void set_temporary_move_target(struct client *client, struct pw_manager_object *o, uint32_t index)
+{
+ struct temporary_move_data *d;
+
+ if (!pw_manager_object_is_sink_input(o) && !pw_manager_object_is_source_output(o))
+ return;
+
+ if (index == SPA_ID_INVALID) {
+ d = pw_manager_object_get_data(o, "temporary_move_data");
+ if (d == NULL)
+ return;
+ if (d->peer_index != SPA_ID_INVALID)
+ pw_log_debug("cleared temporary move target for index:%d", o->index);
+ d->peer_index = SPA_ID_INVALID;
+ d->used = false;
+ return;
+ }
+
+ d = pw_manager_object_add_temporary_data(o, "temporary_move_data",
+ sizeof(struct temporary_move_data),
+ TEMPORARY_MOVE_TIMEOUT);
+ if (d == NULL)
+ return;
+
+ pw_log_debug("[%s] set temporary move target for index:%d to index:%d",
+ client->name, o->index, index);
+ d->peer_index = index;
+ d->used = false;
+}
+
+static void temporary_move_target_timeout(struct client *client, struct pw_manager_object *o)
+{
+ struct temporary_move_data *d = pw_manager_object_get_data(o, "temporary_move_data");
+ struct pw_manager_object *peer;
+
+ /*
+ * Send change event if the temporary data was used, and the peer
+ * is not what we claimed.
+ */
+
+ if (d == NULL || d->peer_index == SPA_ID_INVALID || !d->used)
+ goto done;
+
+ peer = find_linked(client->manager, o->id, pw_manager_object_is_sink_input(o) ?
+ PW_DIRECTION_OUTPUT : PW_DIRECTION_INPUT);
+ if (peer == NULL || peer->index != d->peer_index) {
+ pw_log_debug("[%s] temporary move timeout for index:%d, send change event",
+ client->name, o->index);
+ send_object_event(client, o, SUBSCRIPTION_EVENT_CHANGE);
+ }
+
+done:
+ set_temporary_move_target(client, o, SPA_ID_INVALID);
+}
+
+static struct pw_manager_object *find_device(struct client *client,
+ uint32_t index, const char *name, bool sink, bool *is_monitor);
+
+static int64_t get_node_latency_offset(struct pw_manager_object *o)
+{
+ int64_t latency_offset = 0LL;
+ struct pw_manager_param *p;
+
+ spa_list_for_each(p, &o->param_list, link) {
+ if (p->id != SPA_PARAM_Props)
+ continue;
+ if (spa_pod_parse_object(p->param,
+ SPA_TYPE_OBJECT_Props, NULL,
+ SPA_PROP_latencyOffsetNsec, SPA_POD_Long(&latency_offset)) == 1)
+ break;
+ }
+ return latency_offset;
+}
+
+static void send_latency_offset_subscribe_event(struct client *client, struct pw_manager_object *o)
+{
+ struct pw_manager *manager = client->manager;
+ struct latency_offset_data *d;
+ struct pw_node_info *info;
+ const char *str;
+ uint32_t card_id = SPA_ID_INVALID;
+ int64_t latency_offset = 0LL;
+ bool changed = false;
+
+ if (!pw_manager_object_is_sink(o) && !pw_manager_object_is_source_or_monitor(o))
+ return;
+
+ /*
+ * Pulseaudio sends card change events on latency offset change.
+ */
+ if ((info = o->info) == NULL || info->props == NULL)
+ return;
+ if ((str = spa_dict_lookup(info->props, PW_KEY_DEVICE_ID)) != NULL)
+ card_id = (uint32_t)atoi(str);
+ if (card_id == SPA_ID_INVALID)
+ return;
+
+ d = pw_manager_object_add_data(o, "latency_offset_data", sizeof(struct latency_offset_data));
+ if (d == NULL)
+ return;
+
+ latency_offset = get_node_latency_offset(o);
+ changed = (!d->initialized || latency_offset != d->prev_latency_offset);
+
+ d->prev_latency_offset = latency_offset;
+ d->initialized = true;
+
+ if (changed)
+ client_queue_subscribe_event(client,
+ SUBSCRIPTION_MASK_CARD,
+ SUBSCRIPTION_EVENT_CARD | SUBSCRIPTION_EVENT_CHANGE,
+ id_to_index(manager, card_id));
+}
+
+static void send_default_change_subscribe_event(struct client *client, bool sink, bool source)
+{
+ struct pw_manager_object *def;
+ bool changed = false;
+
+ if (sink) {
+ def = find_device(client, SPA_ID_INVALID, NULL, true, NULL);
+ if (client->prev_default_sink != def) {
+ client->prev_default_sink = def;
+ changed = true;
+ }
+ }
+
+ if (source) {
+ def = find_device(client, SPA_ID_INVALID, NULL, false, NULL);
+ if (client->prev_default_source != def) {
+ client->prev_default_source = def;
+ changed = true;
+ }
+ }
+
+ if (changed)
+ client_queue_subscribe_event(client,
+ SUBSCRIPTION_MASK_SERVER,
+ SUBSCRIPTION_EVENT_CHANGE |
+ SUBSCRIPTION_EVENT_SERVER,
+ -1);
+}
+
+static void handle_metadata(struct client *client, struct pw_manager_object *old,
+ struct pw_manager_object *new, const char *name)
+{
+ if (spa_streq(name, "default")) {
+ if (client->metadata_default == old)
+ client->metadata_default = new;
+ }
+ else if (spa_streq(name, "route-settings")) {
+ if (client->metadata_routes == old)
+ client->metadata_routes = new;
+ }
+}
+
+static uint32_t frac_to_bytes_round_up(struct spa_fraction val, const struct sample_spec *ss)
+{
+ uint64_t u;
+ u = (uint64_t) (val.num * 1000000UL * (uint64_t) ss->rate) / val.denom;
+ u = (u + 1000000UL - 1) / 1000000UL;
+ u *= sample_spec_frame_size(ss);
+ return (uint32_t) u;
+}
+
+static void clamp_latency(struct stream *s, struct spa_fraction *lat)
+{
+ if (lat->num * s->min_quantum.denom / lat->denom < s->min_quantum.num)
+ lat->num = (s->min_quantum.num * lat->denom +
+ (s->min_quantum.denom -1)) / s->min_quantum.denom;
+}
+
+static uint64_t fix_playback_buffer_attr(struct stream *s, struct buffer_attr *attr,
+ uint32_t rate, struct spa_fraction *lat)
+{
+ uint32_t frame_size, max_prebuf, minreq, latency, max_latency, maxlength;
+ struct defs *defs = &s->impl->defs;
+
+ if ((frame_size = s->frame_size) == 0)
+ frame_size = sample_spec_frame_size(&s->ss);
+ if (frame_size == 0)
+ frame_size = 4;
+
+ maxlength = SPA_ROUND_DOWN(MAXLENGTH, frame_size);
+
+ pw_log_info("[%s] maxlength:%u tlength:%u minreq:%u prebuf:%u max:%u",
+ s->client->name, attr->maxlength, attr->tlength,
+ attr->minreq, attr->prebuf, maxlength);
+
+ minreq = frac_to_bytes_round_up(s->min_req, &s->ss);
+ max_latency = defs->quantum_limit * frame_size;
+
+ if (attr->maxlength == (uint32_t) -1 || attr->maxlength > maxlength)
+ attr->maxlength = maxlength;
+ else
+ attr->maxlength = SPA_ROUND_DOWN(attr->maxlength, frame_size);
+
+ minreq = SPA_MIN(minreq, attr->maxlength);
+
+ if (attr->tlength == (uint32_t) -1)
+ attr->tlength = frac_to_bytes_round_up(s->default_tlength, &s->ss);
+ attr->tlength = SPA_CLAMP(attr->tlength, minreq, attr->maxlength);
+ attr->tlength = SPA_ROUND_UP(attr->tlength, frame_size);
+
+ if (attr->minreq == (uint32_t) -1) {
+ uint32_t process = frac_to_bytes_round_up(s->default_req, &s->ss);
+ /* With low-latency, tlength/4 gives a decent default in all of traditional,
+ * adjust latency and early request modes. */
+ uint32_t m = attr->tlength / 4;
+ m = SPA_ROUND_DOWN(m, frame_size);
+ attr->minreq = SPA_MIN(process, m);
+ }
+ attr->minreq = SPA_MAX(attr->minreq, minreq);
+
+ if (attr->tlength < attr->minreq+frame_size)
+ attr->tlength = SPA_MIN(attr->minreq + frame_size, attr->maxlength);
+
+ if (s->early_requests) {
+ latency = attr->minreq;
+ } else if (s->adjust_latency) {
+ if (attr->tlength > attr->minreq * 2)
+ latency = SPA_MIN(max_latency, (attr->tlength - attr->minreq * 2) / 2);
+ else
+ latency = attr->minreq;
+
+ latency = SPA_ROUND_DOWN(latency, frame_size);
+
+ if (attr->tlength >= latency)
+ attr->tlength -= latency;
+ } else {
+ if (attr->tlength > attr->minreq * 2)
+ latency = SPA_MIN(max_latency, attr->tlength - attr->minreq * 2);
+ else
+ latency = attr->minreq;
+ }
+
+ if (attr->tlength < latency + 2 * attr->minreq)
+ attr->tlength = SPA_MIN(latency + 2 * attr->minreq, attr->maxlength);
+
+ attr->minreq = SPA_ROUND_DOWN(attr->minreq, frame_size);
+ if (attr->minreq <= 0) {
+ attr->minreq = frame_size;
+ attr->tlength += frame_size*2;
+ }
+ if (attr->tlength <= attr->minreq)
+ attr->tlength = SPA_MIN(attr->minreq*2 + frame_size, attr->maxlength);
+
+ max_prebuf = attr->tlength + frame_size - attr->minreq;
+ if (attr->prebuf == (uint32_t) -1 || attr->prebuf > max_prebuf)
+ attr->prebuf = max_prebuf;
+ attr->prebuf = SPA_ROUND_DOWN(attr->prebuf, frame_size);
+
+ attr->fragsize = 0;
+
+ lat->num = latency / frame_size;
+ lat->denom = rate;
+ clamp_latency(s, lat);
+
+ pw_log_info("[%s] maxlength:%u tlength:%u minreq:%u/%u prebuf:%u latency:%u/%u %u",
+ s->client->name, attr->maxlength, attr->tlength,
+ attr->minreq, minreq, attr->prebuf, lat->num, lat->denom, frame_size);
+
+ return lat->num * SPA_USEC_PER_SEC / lat->denom;
+}
+
+static uint64_t set_playback_buffer_attr(struct stream *s, struct buffer_attr *attr)
+{
+ struct spa_fraction lat;
+ uint64_t lat_usec;
+ struct spa_dict_item items[6];
+ char latency[32], rate[32];
+ char attr_maxlength[32];
+ char attr_tlength[32];
+ char attr_prebuf[32];
+ char attr_minreq[32];
+
+ lat_usec = fix_playback_buffer_attr(s, attr, s->ss.rate, &lat);
+
+ s->attr = *attr;
+
+ snprintf(latency, sizeof(latency), "%u/%u", lat.num, lat.denom);
+ snprintf(rate, sizeof(rate), "1/%u", lat.denom);
+ snprintf(attr_maxlength, sizeof(attr_maxlength), "%u", s->attr.maxlength);
+ snprintf(attr_tlength, sizeof(attr_tlength), "%u", s->attr.tlength);
+ snprintf(attr_prebuf, sizeof(attr_prebuf), "%u", s->attr.prebuf);
+ snprintf(attr_minreq, sizeof(attr_minreq), "%u", s->attr.minreq);
+
+ items[0] = SPA_DICT_ITEM_INIT(PW_KEY_NODE_LATENCY, latency);
+ items[1] = SPA_DICT_ITEM_INIT(PW_KEY_NODE_RATE, rate);
+ items[2] = SPA_DICT_ITEM_INIT("pulse.attr.maxlength", attr_maxlength);
+ items[3] = SPA_DICT_ITEM_INIT("pulse.attr.tlength", attr_tlength);
+ items[4] = SPA_DICT_ITEM_INIT("pulse.attr.prebuf", attr_prebuf);
+ items[5] = SPA_DICT_ITEM_INIT("pulse.attr.minreq", attr_minreq);
+ pw_stream_update_properties(s->stream, &SPA_DICT_INIT(items, 6));
+
+ if (s->attr.prebuf > 0)
+ s->in_prebuf = true;
+
+ return lat_usec;
+}
+
+static int reply_create_playback_stream(struct stream *stream, struct pw_manager_object *peer)
+{
+ struct client *client = stream->client;
+ struct pw_manager *manager = client->manager;
+ struct message *reply;
+ uint32_t missing, peer_index;
+ const char *peer_name;
+ uint64_t lat_usec;
+
+ stream->buffer = calloc(1, MAXLENGTH);
+ if (stream->buffer == NULL)
+ return -errno;
+
+ lat_usec = set_playback_buffer_attr(stream, &stream->attr);
+
+ missing = stream_pop_missing(stream);
+ stream->index = id_to_index(manager, stream->id);
+ stream->lat_usec = lat_usec;
+
+ pw_log_info("[%s] reply CREATE_PLAYBACK_STREAM tag:%u index:%u missing:%u lat:%"PRIu64,
+ client->name, stream->create_tag, stream->index, missing, lat_usec);
+
+ reply = reply_new(client, stream->create_tag);
+ message_put(reply,
+ TAG_U32, stream->channel, /* stream index/channel */
+ TAG_U32, stream->index, /* sink_input/stream index */
+ TAG_U32, missing, /* missing/requested bytes */
+ TAG_INVALID);
+
+ if (peer && pw_manager_object_is_sink(peer)) {
+ peer_index = peer->index;
+ peer_name = pw_properties_get(peer->props, PW_KEY_NODE_NAME);
+ } else {
+ peer_index = SPA_ID_INVALID;
+ peer_name = NULL;
+ }
+
+ if (client->version >= 9) {
+ message_put(reply,
+ TAG_U32, stream->attr.maxlength,
+ TAG_U32, stream->attr.tlength,
+ TAG_U32, stream->attr.prebuf,
+ TAG_U32, stream->attr.minreq,
+ TAG_INVALID);
+ }
+ if (client->version >= 12) {
+ message_put(reply,
+ TAG_SAMPLE_SPEC, &stream->ss,
+ TAG_CHANNEL_MAP, &stream->map,
+ TAG_U32, peer_index, /* sink index */
+ TAG_STRING, peer_name, /* sink name */
+ TAG_BOOLEAN, false, /* sink suspended state */
+ TAG_INVALID);
+ }
+ if (client->version >= 13) {
+ message_put(reply,
+ TAG_USEC, lat_usec, /* sink configured latency */
+ TAG_INVALID);
+ }
+ if (client->version >= 21) {
+ struct format_info info;
+ spa_zero(info);
+ info.encoding = ENCODING_PCM;
+ message_put(reply,
+ TAG_FORMAT_INFO, &info, /* sink_input format */
+ TAG_INVALID);
+ }
+
+ stream->create_tag = SPA_ID_INVALID;
+
+ return client_queue_message(client, reply);
+}
+
+static uint64_t fix_record_buffer_attr(struct stream *s, struct buffer_attr *attr,
+ uint32_t rate, struct spa_fraction *lat)
+{
+ uint32_t frame_size, minfrag, latency, maxlength;
+
+ if ((frame_size = s->frame_size) == 0)
+ frame_size = sample_spec_frame_size(&s->ss);
+ if (frame_size == 0)
+ frame_size = 4;
+
+ maxlength = SPA_ROUND_DOWN(MAXLENGTH, frame_size);
+
+ pw_log_info("[%s] maxlength:%u fragsize:%u framesize:%u",
+ s->client->name, attr->maxlength, attr->fragsize,
+ frame_size);
+
+ if (attr->maxlength == (uint32_t) -1 || attr->maxlength > maxlength)
+ attr->maxlength = maxlength;
+ else
+ attr->maxlength = SPA_ROUND_DOWN(attr->maxlength, frame_size);
+ attr->maxlength = SPA_MAX(attr->maxlength, frame_size);
+
+ minfrag = frac_to_bytes_round_up(s->min_frag, &s->ss);
+
+ if (attr->fragsize == (uint32_t) -1 || attr->fragsize == 0)
+ attr->fragsize = frac_to_bytes_round_up(s->default_frag, &s->ss);
+ attr->fragsize = SPA_CLAMP(attr->fragsize, minfrag, attr->maxlength);
+ attr->fragsize = SPA_ROUND_UP(attr->fragsize, frame_size);
+
+ attr->tlength = attr->minreq = attr->prebuf = 0;
+
+ /* make sure we can queue at least to fragsize without overruns */
+ if (attr->maxlength < attr->fragsize * 4) {
+ attr->maxlength = attr->fragsize * 4;
+ if (attr->maxlength > maxlength) {
+ attr->maxlength = maxlength;
+ attr->fragsize = SPA_ROUND_DOWN(maxlength / 4, frame_size);
+ }
+ }
+
+ latency = attr->fragsize;
+
+ lat->num = latency / frame_size;
+ lat->denom = rate;
+ clamp_latency(s, lat);
+
+ pw_log_info("[%s] maxlength:%u fragsize:%u minfrag:%u latency:%u/%u",
+ s->client->name, attr->maxlength, attr->fragsize, minfrag,
+ lat->num, lat->denom);
+
+ return lat->num * SPA_USEC_PER_SEC / lat->denom;
+}
+
+static uint64_t set_record_buffer_attr(struct stream *s, struct buffer_attr *attr)
+{
+ struct spa_dict_item items[4];
+ char latency[32], rate[32];
+ char attr_maxlength[32];
+ char attr_fragsize[32];
+ struct spa_fraction lat;
+ uint64_t lat_usec;
+
+ lat_usec = fix_record_buffer_attr(s, attr, s->ss.rate, &lat);
+
+ s->attr = *attr;
+
+ snprintf(latency, sizeof(latency), "%u/%u", lat.num, lat.denom);
+ snprintf(rate, sizeof(rate), "1/%u", lat.denom);
+
+ snprintf(attr_maxlength, sizeof(attr_maxlength), "%u", s->attr.maxlength);
+ snprintf(attr_fragsize, sizeof(attr_fragsize), "%u", s->attr.fragsize);
+
+ items[0] = SPA_DICT_ITEM_INIT(PW_KEY_NODE_LATENCY, latency);
+ items[1] = SPA_DICT_ITEM_INIT(PW_KEY_NODE_RATE, rate);
+ items[2] = SPA_DICT_ITEM_INIT("pulse.attr.maxlength", attr_maxlength);
+ items[3] = SPA_DICT_ITEM_INIT("pulse.attr.fragsize", attr_fragsize);
+ pw_stream_update_properties(s->stream, &SPA_DICT_INIT(items, 4));
+
+ return lat_usec;
+}
+
+static int reply_create_record_stream(struct stream *stream, struct pw_manager_object *peer)
+{
+ struct client *client = stream->client;
+ struct pw_manager *manager = client->manager;
+ char *tmp;
+ struct message *reply;
+ const char *peer_name, *name;
+ uint32_t peer_index;
+ uint64_t lat_usec;
+
+ stream->buffer = calloc(1, MAXLENGTH);
+ if (stream->buffer == NULL)
+ return -errno;
+
+ lat_usec = set_record_buffer_attr(stream, &stream->attr);
+
+ stream->index = id_to_index(manager, stream->id);
+ stream->lat_usec = lat_usec;
+
+ pw_log_info("[%s] reply CREATE_RECORD_STREAM tag:%u index:%u latency:%"PRIu64,
+ client->name, stream->create_tag, stream->index, lat_usec);
+
+ reply = reply_new(client, stream->create_tag);
+ message_put(reply,
+ TAG_U32, stream->channel, /* stream index/channel */
+ TAG_U32, stream->index, /* source_output/stream index */
+ TAG_INVALID);
+
+ if (peer && pw_manager_object_is_sink_input(peer))
+ peer = find_linked(manager, peer->id, PW_DIRECTION_OUTPUT);
+ if (peer && pw_manager_object_is_source_or_monitor(peer)) {
+ name = pw_properties_get(peer->props, PW_KEY_NODE_NAME);
+ peer_index = peer->index;
+ if (!pw_manager_object_is_source(peer)) {
+ size_t len = (name ? strlen(name) : 5) + 10;
+ peer_name = tmp = alloca(len);
+ snprintf(tmp, len, "%s.monitor", name ? name : "sink");
+ } else {
+ peer_name = name;
+ }
+ } else {
+ peer_index = SPA_ID_INVALID;
+ peer_name = NULL;
+ }
+
+ if (client->version >= 9) {
+ message_put(reply,
+ TAG_U32, stream->attr.maxlength,
+ TAG_U32, stream->attr.fragsize,
+ TAG_INVALID);
+ }
+ if (client->version >= 12) {
+ message_put(reply,
+ TAG_SAMPLE_SPEC, &stream->ss,
+ TAG_CHANNEL_MAP, &stream->map,
+ TAG_U32, peer_index, /* source index */
+ TAG_STRING, peer_name, /* source name */
+ TAG_BOOLEAN, false, /* source suspended state */
+ TAG_INVALID);
+ }
+ if (client->version >= 13) {
+ message_put(reply,
+ TAG_USEC, lat_usec, /* source configured latency */
+ TAG_INVALID);
+ }
+ if (client->version >= 22) {
+ struct format_info info;
+ spa_zero(info);
+ info.encoding = ENCODING_PCM;
+ message_put(reply,
+ TAG_FORMAT_INFO, &info, /* source_output format */
+ TAG_INVALID);
+ }
+
+ stream->create_tag = SPA_ID_INVALID;
+
+ return client_queue_message(client, reply);
+}
+
+static int reply_create_stream(struct stream *stream, struct pw_manager_object *peer)
+{
+ stream->peer_index = peer->index;
+ return stream->direction == PW_DIRECTION_OUTPUT ?
+ reply_create_playback_stream(stream, peer) :
+ reply_create_record_stream(stream, peer);
+}
+
+static void manager_added(void *data, struct pw_manager_object *o)
+{
+ struct client *client = data;
+ struct pw_manager *manager = client->manager;
+ const char *str;
+
+ register_object_message_handlers(o);
+
+ if (strcmp(o->type, PW_TYPE_INTERFACE_Core) == 0 && manager->info != NULL) {
+ struct pw_core_info *info = manager->info;
+ if (info->props) {
+ if ((str = spa_dict_lookup(info->props, "default.clock.rate")) != NULL)
+ client->impl->defs.sample_spec.rate = atoi(str);
+ if ((str = spa_dict_lookup(info->props, "default.clock.quantum-limit")) != NULL)
+ client->impl->defs.quantum_limit = atoi(str);
+ }
+ }
+
+ if (spa_streq(o->type, PW_TYPE_INTERFACE_Metadata)) {
+ if (o->props != NULL &&
+ (str = pw_properties_get(o->props, PW_KEY_METADATA_NAME)) != NULL)
+ handle_metadata(client, NULL, o, str);
+ }
+
+ if (spa_streq(o->type, PW_TYPE_INTERFACE_Link)) {
+ struct stream *s, *t;
+ struct pw_manager_object *peer = NULL;
+ union pw_map_item *item;
+ pw_array_for_each(item, &client->streams.items) {
+ struct stream *s = item->data;
+ const char *peer_name;
+
+ if (pw_map_item_is_free(item) || s->pending)
+ continue;
+ if (s->peer_index == SPA_ID_INVALID)
+ continue;
+
+ peer = find_peer_for_link(manager, o, s->id, s->direction);
+ if (peer == NULL || peer->props == NULL ||
+ peer->index == s->peer_index)
+ continue;
+
+ s->peer_index = peer->index;
+
+ peer_name = pw_properties_get(peer->props, PW_KEY_NODE_NAME);
+ if (peer_name && s->direction == PW_DIRECTION_INPUT &&
+ pw_manager_object_is_monitor(peer)) {
+ int len = strlen(peer_name) + 10;
+ char *tmp = alloca(len);
+ snprintf(tmp, len, "%s.monitor", peer_name);
+ peer_name = tmp;
+ }
+ if (peer_name != NULL)
+ stream_send_moved(s, peer->index, peer_name);
+ }
+ spa_list_for_each_safe(s, t, &client->pending_streams, link) {
+ peer = find_peer_for_link(manager, o, s->id, s->direction);
+ if (peer) {
+ reply_create_stream(s, peer);
+ spa_list_remove(&s->link);
+ s->pending = false;
+ }
+ }
+ }
+
+ send_object_event(client, o, SUBSCRIPTION_EVENT_NEW);
+
+ /* Adding sinks etc. may also change defaults */
+ send_default_change_subscribe_event(client, pw_manager_object_is_sink(o), pw_manager_object_is_source_or_monitor(o));
+}
+
+static void manager_updated(void *data, struct pw_manager_object *o)
+{
+ struct client *client = data;
+
+ send_object_event(client, o, SUBSCRIPTION_EVENT_CHANGE);
+
+ set_temporary_move_target(client, o, SPA_ID_INVALID);
+
+ send_latency_offset_subscribe_event(client, o);
+ send_default_change_subscribe_event(client, pw_manager_object_is_sink(o), pw_manager_object_is_source_or_monitor(o));
+}
+
+static void manager_removed(void *data, struct pw_manager_object *o)
+{
+ struct client *client = data;
+ const char *str;
+
+ send_object_event(client, o, SUBSCRIPTION_EVENT_REMOVE);
+
+ send_default_change_subscribe_event(client, pw_manager_object_is_sink(o), pw_manager_object_is_source_or_monitor(o));
+
+ if (spa_streq(o->type, PW_TYPE_INTERFACE_Metadata)) {
+ if (o->props != NULL &&
+ (str = pw_properties_get(o->props, PW_KEY_METADATA_NAME)) != NULL)
+ handle_metadata(client, o, NULL, str);
+ }
+}
+
+static void manager_object_data_timeout(void *data, struct pw_manager_object *o, const char *key)
+{
+ struct client *client = data;
+
+ if (spa_streq(key, "temporary_move_data"))
+ temporary_move_target_timeout(client, o);
+}
+
+static int json_object_find(const char *obj, const char *key, char *value, size_t len)
+{
+ 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 -EINVAL;
+
+ 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)
+ continue;
+ return 0;
+ } else {
+ if (spa_json_next(&it[1], &v) <= 0)
+ break;
+ }
+ }
+ return -ENOENT;
+}
+
+static void manager_metadata(void *data, struct pw_manager_object *o,
+ uint32_t subject, const char *key, const char *type, const char *value)
+{
+ struct client *client = data;
+ bool changed = false;
+
+ pw_log_debug("meta id:%d subject:%d key:%s type:%s value:%s",
+ o->id, subject, key, type, value);
+
+ if (subject == PW_ID_CORE && o == client->metadata_default) {
+ char name[1024];
+
+ if (key == NULL || spa_streq(key, "default.audio.sink")) {
+ if (value != NULL) {
+ if (json_object_find(value,
+ "name", name, sizeof(name)) < 0)
+ value = NULL;
+ else
+ value = name;
+ }
+ if ((changed = !spa_streq(client->default_sink, value))) {
+ free(client->default_sink);
+ client->default_sink = value ? strdup(value) : NULL;
+ }
+ free(client->temporary_default_sink);
+ client->temporary_default_sink = NULL;
+ }
+ if (key == NULL || spa_streq(key, "default.audio.source")) {
+ if (value != NULL) {
+ if (json_object_find(value,
+ "name", name, sizeof(name)) < 0)
+ value = NULL;
+ else
+ value = name;
+ }
+ if ((changed = !spa_streq(client->default_source, value))) {
+ free(client->default_source);
+ client->default_source = value ? strdup(value) : NULL;
+ }
+ free(client->temporary_default_source);
+ client->temporary_default_source = NULL;
+ }
+ if (changed)
+ send_default_change_subscribe_event(client, true, true);
+ }
+ if (subject == PW_ID_CORE && o == client->metadata_routes) {
+ if (key == NULL)
+ pw_properties_clear(client->routes);
+ else
+ pw_properties_set(client->routes, key, value);
+ }
+}
+
+
+static void do_free_client(void *obj, void *data, int res, uint32_t id)
+{
+ struct client *client = obj;
+ client_free(client);
+}
+
+static void manager_disconnect(void *data)
+{
+ struct client *client = data;
+ pw_log_debug("manager_disconnect()");
+ pw_work_queue_add(client->impl->work_queue, client, 0,
+ do_free_client, NULL);
+}
+
+static const struct pw_manager_events manager_events = {
+ PW_VERSION_MANAGER_EVENTS,
+ .sync = manager_sync,
+ .added = manager_added,
+ .updated = manager_updated,
+ .removed = manager_removed,
+ .metadata = manager_metadata,
+ .disconnect = manager_disconnect,
+ .object_data_timeout = manager_object_data_timeout,
+};
+
+static int do_set_client_name(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ struct impl *impl = client->impl;
+ const char *name = NULL;
+ int res = 0, changed = 0;
+
+ if (client->version < 13) {
+ if (message_get(m,
+ TAG_STRING, &name,
+ TAG_INVALID) < 0)
+ return -EPROTO;
+ if (name)
+ changed += pw_properties_set(client->props,
+ PW_KEY_APP_NAME, name);
+ } else {
+ if (message_get(m,
+ TAG_PROPLIST, client->props,
+ TAG_INVALID) < 0)
+ return -EPROTO;
+ changed++;
+ }
+
+ client_update_quirks(client);
+
+ client->name = pw_properties_get(client->props, PW_KEY_APP_NAME);
+ pw_log_info("[%s] %s tag:%d", client->name,
+ commands[command].name, tag);
+
+ if (client->core == NULL) {
+ client->core = pw_context_connect(impl->context,
+ pw_properties_copy(client->props), 0);
+ if (client->core == NULL) {
+ res = -errno;
+ goto error;
+ }
+ client->manager = pw_manager_new(client->core);
+ if (client->manager == NULL) {
+ res = -errno;
+ goto error;
+ }
+ client->connect_tag = tag;
+ pw_manager_add_listener(client->manager, &client->manager_listener,
+ &manager_events, client);
+ } else {
+ if (changed)
+ pw_core_update_properties(client->core, &client->props->dict);
+
+ if (client->connect_tag == SPA_ID_INVALID)
+ res = reply_set_client_name(client, tag);
+ }
+
+ return res;
+error:
+ pw_log_error("%p: failed to connect client: %s", impl, spa_strerror(res));
+ return res;
+
+}
+
+static int do_subscribe(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ uint32_t mask;
+
+ if (message_get(m,
+ TAG_U32, &mask,
+ TAG_INVALID) < 0)
+ return -EPROTO;
+
+ pw_log_info("[%s] SUBSCRIBE tag:%u mask:%08x",
+ client->name, tag, mask);
+
+ if (mask & ~SUBSCRIPTION_MASK_ALL)
+ return -EINVAL;
+
+ client->subscribed = mask;
+
+ return reply_simple_ack(client, tag);
+}
+
+static void stream_control_info(void *data, uint32_t id,
+ const struct pw_stream_control *control)
+{
+ struct stream *stream = data;
+
+ switch (id) {
+ case SPA_PROP_channelVolumes:
+ stream->volume.channels = control->n_values;
+ memcpy(stream->volume.values, control->values, control->n_values * sizeof(float));
+ pw_log_info("stream %p: volume changed %f", stream, stream->volume.values[0]);
+ break;
+ case SPA_PROP_mute:
+ stream->muted = control->values[0] >= 0.5;
+ pw_log_info("stream %p: mute changed %d", stream, stream->muted);
+ break;
+ }
+}
+
+static void do_destroy_stream(void *obj, void *data, int res, uint32_t id)
+{
+ struct stream *stream = obj;
+
+ stream_free(stream);
+}
+
+static void stream_state_changed(void *data, enum pw_stream_state old,
+ enum pw_stream_state state, const char *error)
+{
+ struct stream *stream = data;
+ struct client *client = stream->client;
+ struct impl *impl = client->impl;
+ bool destroy_stream = false;
+
+ switch (state) {
+ case PW_STREAM_STATE_ERROR:
+ reply_error(client, -1, stream->create_tag, -EIO);
+ destroy_stream = true;
+ break;
+ case PW_STREAM_STATE_UNCONNECTED:
+ if (stream->create_tag != SPA_ID_INVALID)
+ reply_error(client, -1, stream->create_tag, -ENOENT);
+ else
+ stream->killed = true;
+ destroy_stream = true;
+ break;
+ case PW_STREAM_STATE_PAUSED:
+ stream->id = pw_stream_get_node_id(stream->stream);
+ break;
+ case PW_STREAM_STATE_CONNECTING:
+ case PW_STREAM_STATE_STREAMING:
+ break;
+ }
+
+ if (destroy_stream) {
+ pw_work_queue_add(impl->work_queue, stream, 0,
+ do_destroy_stream, NULL);
+ }
+}
+
+static const struct spa_pod *get_buffers_param(struct stream *s,
+ struct buffer_attr *attr, struct spa_pod_builder *b)
+{
+ const struct spa_pod *param;
+ uint32_t blocks, buffers, size, maxsize, stride;
+ struct defs *defs = &s->impl->defs;
+
+ blocks = 1;
+ stride = s->frame_size;
+
+ maxsize = defs->quantum_limit * 32 * s->frame_size;
+ if (s->direction == PW_DIRECTION_OUTPUT) {
+ size = attr->minreq;
+ } else {
+ size = attr->fragsize;
+ }
+ buffers = SPA_CLAMP(maxsize / size, MIN_BUFFERS, MAX_BUFFERS);
+
+ pw_log_info("[%s] stride %d maxsize %d size %u buffers %d", s->client->name,
+ stride, maxsize, size, buffers);
+
+ param = spa_pod_builder_add_object(b,
+ SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers,
+ SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(buffers,
+ MIN_BUFFERS, MAX_BUFFERS),
+ SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(blocks),
+ SPA_PARAM_BUFFERS_size, SPA_POD_CHOICE_RANGE_Int(
+ size, size, maxsize),
+ SPA_PARAM_BUFFERS_stride, SPA_POD_Int(stride));
+ return param;
+}
+
+static void stream_param_changed(void *data, uint32_t id, const struct spa_pod *param)
+{
+ struct stream *stream = data;
+ const struct spa_pod *params[4];
+ uint32_t n_params = 0;
+ uint8_t buffer[4096];
+ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
+ int res;
+
+ if (id != SPA_PARAM_Format || param == NULL)
+ return;
+
+ if ((res = format_parse_param(param, false, &stream->ss, &stream->map, NULL, NULL)) < 0) {
+ pw_stream_set_error(stream->stream, res, "format not supported");
+ return;
+ }
+
+ pw_log_info("[%s] got format:%s rate:%u channels:%u", stream->client->name,
+ format_id2name(stream->ss.format),
+ stream->ss.rate, stream->ss.channels);
+
+ stream->frame_size = sample_spec_frame_size(&stream->ss);
+ if (stream->frame_size == 0) {
+ pw_stream_set_error(stream->stream, res, "format not supported");
+ return;
+ }
+ stream->rate = stream->ss.rate;
+
+ if (stream->create_tag != SPA_ID_INVALID) {
+ struct pw_manager_object *peer;
+
+ if (stream->volume_set) {
+ pw_stream_set_control(stream->stream,
+ SPA_PROP_channelVolumes, stream->volume.channels, stream->volume.values, 0);
+ }
+ if (stream->muted_set) {
+ float val = stream->muted ? 1.0f : 0.0f;
+ pw_stream_set_control(stream->stream,
+ SPA_PROP_mute, 1, &val, 0);
+ }
+ if (stream->corked)
+ stream_set_paused(stream, true, "cork after create");
+
+ /* if peer exists, reply immediately, otherwise reply when the link is created */
+ peer = find_linked(stream->client->manager, stream->id, stream->direction);
+ if (peer) {
+ reply_create_stream(stream, peer);
+ } else {
+ spa_list_append(&stream->client->pending_streams, &stream->link);
+ stream->pending = true;
+ }
+ }
+
+ params[n_params++] = get_buffers_param(stream, &stream->attr, &b);
+ pw_stream_update_params(stream->stream, params, n_params);
+}
+
+static void stream_io_changed(void *data, uint32_t id, void *area, uint32_t size)
+{
+ struct stream *stream = data;
+ switch (id) {
+ case SPA_IO_Position:
+ stream->position = area;
+ break;
+ }
+}
+
+struct process_data {
+ struct pw_time pwt;
+ uint32_t read_inc;
+ uint32_t write_inc;
+ uint32_t underrun_for;
+ uint32_t playing_for;
+ uint32_t minreq;
+ uint32_t quantum;
+ unsigned int underrun:1;
+ unsigned int idle:1;
+};
+
+static int
+do_process_done(struct spa_loop *loop,
+ bool async, uint32_t seq, const void *data, size_t size, void *user_data)
+{
+ struct stream *stream = user_data;
+ struct client *client = stream->client;
+ struct impl *impl = client->impl;
+ const struct process_data *pd = data;
+ uint32_t index, towrite;
+ int32_t avail;
+
+ stream->timestamp = pd->pwt.now;
+ stream->delay = pd->pwt.buffered * SPA_USEC_PER_SEC / stream->ss.rate;
+ if (pd->pwt.rate.denom > 0)
+ stream->delay += pd->pwt.delay * SPA_USEC_PER_SEC * pd->pwt.rate.num / pd->pwt.rate.denom;
+
+ if (stream->direction == PW_DIRECTION_OUTPUT) {
+ if (pd->quantum != stream->last_quantum)
+ stream_update_minreq(stream, pd->minreq);
+ stream->last_quantum = pd->quantum;
+
+ stream->read_index += pd->read_inc;
+ if (stream->corked) {
+ if (stream->underrun_for != (uint64_t)-1)
+ stream->underrun_for += pd->underrun_for;
+ stream->playing_for = 0;
+ return 0;
+ }
+ if (pd->underrun != stream->is_underrun) {
+ stream->is_underrun = pd->underrun;
+ stream->underrun_for = 0;
+ stream->playing_for = 0;
+ if (pd->underrun)
+ stream_send_underflow(stream, stream->read_index);
+ else
+ stream_send_started(stream);
+ }
+ if (pd->idle) {
+ if (!stream->is_idle) {
+ stream->idle_time = stream->timestamp;
+ } else if (!stream->is_paused &&
+ stream->idle_timeout_sec > 0 &&
+ stream->timestamp - stream->idle_time >
+ (stream->idle_timeout_sec * SPA_NSEC_PER_SEC)) {
+ stream_set_paused(stream, true, "long underrun");
+ }
+ }
+ stream->is_idle = pd->idle;
+ stream->playing_for += pd->playing_for;
+ if (stream->underrun_for != (uint64_t)-1)
+ stream->underrun_for += pd->underrun_for;
+
+ stream_send_request(stream);
+ } else {
+ struct message *msg;
+ stream->write_index += pd->write_inc;
+
+ avail = spa_ringbuffer_get_read_index(&stream->ring, &index);
+
+ if (!spa_list_is_empty(&client->out_messages)) {
+ pw_log_debug("%p: [%s] pending read:%u avail:%d",
+ stream, client->name, index, avail);
+ return 0;
+ }
+
+ if (avail <= 0) {
+ /* underrun, can't really happen but if it does we
+ * do nothing and wait for more data */
+ pw_log_warn("%p: [%s] underrun read:%u avail:%d",
+ stream, client->name, index, avail);
+ } else {
+ if ((uint32_t)avail > stream->attr.maxlength) {
+ uint32_t skip = avail - stream->attr.fragsize;
+ /* overrun, catch up to latest fragment and send it */
+ pw_log_warn("%p: [%s] overrun recover read:%u avail:%d max:%u skip:%u",
+ stream, client->name, index, avail, stream->attr.maxlength, skip);
+ index += skip;
+ stream->read_index += skip;
+ avail = stream->attr.fragsize;
+ }
+ pw_log_trace("avail:%d index:%u", avail, index);
+
+ while ((uint32_t)avail >= stream->attr.fragsize) {
+ towrite = SPA_MIN(avail, MAX_BLOCK);
+ towrite = SPA_MIN(towrite, stream->attr.fragsize);
+ towrite = SPA_ROUND_DOWN(towrite, stream->frame_size);
+
+ msg = message_alloc(impl, stream->channel, towrite);
+ if (msg == NULL)
+ return -errno;
+
+ spa_ringbuffer_read_data(&stream->ring,
+ stream->buffer, MAXLENGTH,
+ index % MAXLENGTH,
+ msg->data, towrite);
+
+ client_queue_message(client, msg);
+
+ index += towrite;
+ avail -= towrite;
+ stream->read_index += towrite;
+ }
+ spa_ringbuffer_read_update(&stream->ring, index);
+ }
+ }
+ return 0;
+}
+
+
+static void stream_process(void *data)
+{
+ struct stream *stream = data;
+ struct client *client = stream->client;
+ struct impl *impl = stream->impl;
+ void *p;
+ struct pw_buffer *buffer;
+ struct spa_buffer *buf;
+ struct spa_data *d;
+ uint32_t offs, size, minreq = 0, index;
+ struct process_data pd;
+ bool do_flush = false;
+
+ if (stream->create_tag != SPA_ID_INVALID)
+ return;
+
+ pw_log_trace_fp("%p: process", stream);
+ buffer = pw_stream_dequeue_buffer(stream->stream);
+ if (buffer == NULL)
+ return;
+
+ buf = buffer->buffer;
+ d = &buf->datas[0];
+ if ((p = d->data) == NULL)
+ return;
+
+ spa_zero(pd);
+
+ if (stream->direction == PW_DIRECTION_OUTPUT) {
+ int32_t avail = spa_ringbuffer_get_read_index(&stream->ring, &index);
+
+ minreq = buffer->requested * stream->frame_size;
+ if (minreq == 0)
+ minreq = stream->attr.minreq;
+
+ pd.minreq = minreq;
+ pd.quantum = stream->position ? stream->position->clock.duration : minreq;
+
+ if (avail < (int32_t)minreq || stream->corked) {
+ /* underrun, produce a silence buffer */
+ size = SPA_MIN(d->maxsize, minreq);
+ memset(p, 0, size);
+
+ if (stream->draining && !stream->corked) {
+ stream->draining = false;
+ do_flush = true;
+ } else {
+ pd.underrun_for = size;
+ pd.underrun = true;
+ }
+ if ((stream->attr.prebuf == 0 || do_flush) && !stream->corked) {
+ if (avail > 0) {
+ avail = SPA_MIN((uint32_t)avail, size);
+ spa_ringbuffer_read_data(&stream->ring,
+ stream->buffer, MAXLENGTH,
+ index % MAXLENGTH,
+ p, avail);
+ }
+ index += size;
+ pd.read_inc = size;
+ spa_ringbuffer_read_update(&stream->ring, index);
+
+ pd.playing_for = size;
+ }
+ pd.idle = true;
+ pw_log_debug("%p: [%s] underrun read:%u avail:%d max:%u",
+ stream, client->name, index, avail, minreq);
+ } else {
+ if (avail > (int32_t)stream->attr.maxlength) {
+ uint32_t skip = avail - stream->attr.maxlength;
+ /* overrun, reported by other side, here we skip
+ * ahead to the oldest data. */
+ pw_log_debug("%p: [%s] overrun read:%u avail:%d max:%u skip:%u",
+ stream, client->name, index, avail,
+ stream->attr.maxlength, skip);
+ index += skip;
+ pd.read_inc = skip;
+ avail = stream->attr.maxlength;
+ }
+ size = SPA_MIN(d->maxsize, (uint32_t)avail);
+ size = SPA_MIN(size, minreq);
+
+ spa_ringbuffer_read_data(&stream->ring,
+ stream->buffer, MAXLENGTH,
+ index % MAXLENGTH,
+ p, size);
+
+ index += size;
+ pd.read_inc += size;
+ spa_ringbuffer_read_update(&stream->ring, index);
+
+ pd.playing_for = size;
+ pd.underrun = false;
+ }
+ d->chunk->offset = 0;
+ d->chunk->stride = stream->frame_size;
+ d->chunk->size = size;
+ buffer->size = size / stream->frame_size;
+ } else {
+ int32_t filled = spa_ringbuffer_get_write_index(&stream->ring, &index);
+
+ offs = SPA_MIN(d->chunk->offset, d->maxsize);
+ size = SPA_MIN(d->chunk->size, d->maxsize - offs);
+
+ if (filled < 0) {
+ /* underrun, can't really happen because we never read more
+ * than what's available on the other side */
+ pw_log_warn("%p: [%s] underrun write:%u filled:%d",
+ stream, client->name, index, filled);
+ } else if ((uint32_t)filled + size > stream->attr.maxlength) {
+ /* overrun, can happen when the other side is not
+ * reading fast enough. We still write our data into the
+ * ringbuffer and expect the other side to warn and catch up. */
+ pw_log_debug("%p: [%s] overrun write:%u filled:%d size:%u max:%u",
+ stream, client->name, index, filled,
+ size, stream->attr.maxlength);
+ }
+
+ spa_ringbuffer_write_data(&stream->ring,
+ stream->buffer, MAXLENGTH,
+ index % MAXLENGTH,
+ SPA_PTROFF(p, offs, void),
+ SPA_MIN(size, MAXLENGTH));
+
+ index += size;
+ pd.write_inc = size;
+ spa_ringbuffer_write_update(&stream->ring, index);
+ }
+ pw_stream_queue_buffer(stream->stream, buffer);
+
+ if (do_flush)
+ pw_stream_flush(stream->stream, true);
+
+ pw_stream_get_time_n(stream->stream, &pd.pwt, sizeof(pd.pwt));
+
+ pw_loop_invoke(impl->loop,
+ do_process_done, 1, &pd, sizeof(pd), false, stream);
+}
+
+static void stream_drained(void *data)
+{
+ struct stream *stream = data;
+ if (stream->drain_tag != 0) {
+ pw_log_info("[%s] drained channel:%u tag:%d",
+ stream->client->name, stream->channel,
+ stream->drain_tag);
+ reply_simple_ack(stream->client, stream->drain_tag);
+ stream->drain_tag = 0;
+
+ pw_stream_set_active(stream->stream, !stream->is_paused);
+ }
+}
+
+static const struct pw_stream_events stream_events =
+{
+ PW_VERSION_STREAM_EVENTS,
+ .control_info = stream_control_info,
+ .state_changed = stream_state_changed,
+ .param_changed = stream_param_changed,
+ .io_changed = stream_io_changed,
+ .process = stream_process,
+ .drained = stream_drained,
+};
+
+static void log_format_info(struct impl *impl, enum spa_log_level level, struct format_info *format)
+{
+ const struct spa_dict_item *it;
+ pw_logt(level, mod_topic, "%p: format %s",
+ impl, format_encoding2name(format->encoding));
+ spa_dict_for_each(it, &format->props->dict)
+ pw_logt(level, mod_topic, "%p: '%s': '%s'",
+ impl, it->key, it->value);
+}
+
+static int do_create_playback_stream(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ struct impl *impl = client->impl;
+ const char *name = NULL;
+ int res;
+ struct sample_spec ss;
+ struct channel_map map;
+ uint32_t sink_index, syncid, rate = 0;
+ const char *sink_name;
+ struct buffer_attr attr = { 0 };
+ bool corked = false,
+ no_remap = false,
+ no_remix = false,
+ fix_format = false,
+ fix_rate = false,
+ fix_channels = false,
+ no_move = false,
+ variable_rate = false,
+ muted = false,
+ adjust_latency = false,
+ early_requests = false,
+ dont_inhibit_auto_suspend = false,
+ volume_set = true,
+ muted_set = false,
+ fail_on_suspend = false,
+ relative_volume = false,
+ passthrough = false;
+ struct volume volume;
+ struct pw_properties *props = NULL;
+ uint8_t n_formats = 0;
+ struct stream *stream = NULL;
+ uint32_t n_params = 0, n_valid_formats = 0, flags;
+ const struct spa_pod *params[MAX_FORMATS];
+ uint8_t buffer[4096];
+ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
+
+ props = pw_properties_copy(client->props);
+ if (props == NULL)
+ goto error_errno;
+
+ if (client->version < 13) {
+ if ((res = message_get(m,
+ TAG_STRING, &name,
+ TAG_INVALID)) < 0)
+ goto error_protocol;
+ if (name == NULL)
+ goto error_protocol;
+ }
+ if (message_get(m,
+ TAG_SAMPLE_SPEC, &ss,
+ TAG_CHANNEL_MAP, &map,
+ TAG_U32, &sink_index,
+ TAG_STRING, &sink_name,
+ TAG_U32, &attr.maxlength,
+ TAG_BOOLEAN, &corked,
+ TAG_U32, &attr.tlength,
+ TAG_U32, &attr.prebuf,
+ TAG_U32, &attr.minreq,
+ TAG_U32, &syncid,
+ TAG_CVOLUME, &volume,
+ TAG_INVALID) < 0)
+ goto error_protocol;
+
+ pw_log_info("[%s] CREATE_PLAYBACK_STREAM tag:%u corked:%u sink-name:%s sink-index:%u",
+ client->name, tag, corked, sink_name, sink_index);
+
+ if (sink_index != SPA_ID_INVALID && sink_name != NULL)
+ goto error_invalid;
+
+ if (client->version >= 12) {
+ if (message_get(m,
+ TAG_BOOLEAN, &no_remap,
+ TAG_BOOLEAN, &no_remix,
+ TAG_BOOLEAN, &fix_format,
+ TAG_BOOLEAN, &fix_rate,
+ TAG_BOOLEAN, &fix_channels,
+ TAG_BOOLEAN, &no_move,
+ TAG_BOOLEAN, &variable_rate,
+ TAG_INVALID) < 0)
+ goto error_protocol;
+ }
+ if (client->version >= 13) {
+ if (message_get(m,
+ TAG_BOOLEAN, &muted,
+ TAG_BOOLEAN, &adjust_latency,
+ TAG_PROPLIST, props,
+ TAG_INVALID) < 0)
+ goto error_protocol;
+ }
+ if (client->version >= 14) {
+ if (message_get(m,
+ TAG_BOOLEAN, &volume_set,
+ TAG_BOOLEAN, &early_requests,
+ TAG_INVALID) < 0)
+ goto error_protocol;
+ }
+ if (client->version >= 15) {
+ if (message_get(m,
+ TAG_BOOLEAN, &muted_set,
+ TAG_BOOLEAN, &dont_inhibit_auto_suspend,
+ TAG_BOOLEAN, &fail_on_suspend,
+ TAG_INVALID) < 0)
+ goto error_protocol;
+ }
+ if (client->version >= 17) {
+ if (message_get(m,
+ TAG_BOOLEAN, &relative_volume,
+ TAG_INVALID) < 0)
+ goto error_protocol;
+ }
+ if (client->version >= 18) {
+ if (message_get(m,
+ TAG_BOOLEAN, &passthrough,
+ TAG_INVALID) < 0)
+ goto error_protocol;
+ }
+
+ if (client->version >= 21) {
+ if (message_get(m,
+ TAG_U8, &n_formats,
+ TAG_INVALID) < 0)
+ goto error_protocol;
+
+ if (n_formats) {
+ uint8_t i;
+ for (i = 0; i < n_formats; i++) {
+ struct format_info format;
+ uint32_t r;
+
+ if (message_get(m,
+ TAG_FORMAT_INFO, &format,
+ TAG_INVALID) < 0)
+ goto error_protocol;
+
+ if (n_params < MAX_FORMATS &&
+ (params[n_params] = format_info_build_param(&b,
+ SPA_PARAM_EnumFormat, &format, &r)) != NULL) {
+ n_params++;
+ n_valid_formats++;
+ if (r > rate)
+ rate = r;
+ } else {
+ log_format_info(impl, SPA_LOG_LEVEL_WARN, &format);
+ }
+ format_info_clear(&format);
+ }
+ }
+ }
+ if (sample_spec_valid(&ss)) {
+ if (fix_format || fix_rate || fix_channels) {
+ struct sample_spec sfix = ss;
+ if (fix_format)
+ sfix.format = SPA_AUDIO_FORMAT_UNKNOWN;
+ if (fix_rate)
+ sfix.rate = 0;
+ if (fix_channels)
+ sfix.channels = 0;
+ if (n_params < MAX_FORMATS &&
+ (params[n_params] = format_build_param(&b,
+ SPA_PARAM_EnumFormat, &sfix,
+ sfix.channels > 0 ? &map : NULL)) != NULL) {
+ n_params++;
+ n_valid_formats++;
+ }
+ }
+ else if (n_params < MAX_FORMATS &&
+ (params[n_params] = format_build_param(&b,
+ SPA_PARAM_EnumFormat, &ss,
+ ss.channels > 0 ? &map : NULL)) != NULL) {
+ n_params++;
+ n_valid_formats++;
+ } else {
+ pw_log_warn("%p: unsupported format:%s rate:%d channels:%u",
+ impl, format_id2name(ss.format), ss.rate,
+ ss.channels);
+ }
+ rate = ss.rate;
+ }
+
+ if (m->offset != m->length)
+ goto error_protocol;
+
+ if (n_valid_formats == 0)
+ goto error_no_formats;
+
+ stream = stream_new(client, STREAM_TYPE_PLAYBACK, tag, &ss, &map, &attr);
+ if (stream == NULL)
+ goto error_errno;
+
+ stream->corked = corked;
+ stream->adjust_latency = adjust_latency;
+ stream->early_requests = early_requests;
+ stream->volume = volume;
+ stream->volume_set = volume_set;
+ stream->muted = muted;
+ stream->muted_set = muted_set;
+ stream->is_underrun = true;
+ stream->underrun_for = -1;
+
+ if (rate != 0) {
+ struct spa_fraction lat;
+ fix_playback_buffer_attr(stream, &attr, rate, &lat);
+ pw_properties_setf(props, PW_KEY_NODE_RATE, "1/%u", rate);
+ pw_properties_setf(props, PW_KEY_NODE_LATENCY, "%u/%u",
+ lat.num, lat.denom);
+ }
+ if (no_remix)
+ pw_properties_set(props, PW_KEY_STREAM_DONT_REMIX, "true");
+ flags = 0;
+ if (no_move)
+ flags |= PW_STREAM_FLAG_DONT_RECONNECT;
+
+ if (sink_name != NULL) {
+ pw_properties_set(props,
+ PW_KEY_TARGET_OBJECT, sink_name);
+ } else if (sink_index != SPA_ID_INVALID && sink_index != 0) {
+ pw_properties_setf(props,
+ PW_KEY_TARGET_OBJECT, "%u", sink_index);
+ }
+
+ stream->stream = pw_stream_new(client->core, name, props);
+ props = NULL;
+ if (stream->stream == NULL)
+ goto error_errno;
+
+ pw_log_debug("%p: new stream %p channel:%d passthrough:%d",
+ impl, stream, stream->channel, passthrough);
+
+ pw_stream_add_listener(stream->stream,
+ &stream->stream_listener,
+ &stream_events, stream);
+
+ pw_stream_connect(stream->stream,
+ PW_DIRECTION_OUTPUT,
+ SPA_ID_INVALID,
+ flags |
+ PW_STREAM_FLAG_AUTOCONNECT |
+ PW_STREAM_FLAG_RT_PROCESS |
+ PW_STREAM_FLAG_MAP_BUFFERS,
+ params, n_params);
+
+ return 0;
+
+error_errno:
+ res = -errno;
+ goto error;
+error_protocol:
+ res = -EPROTO;
+ goto error;
+error_no_formats:
+ res = -ENOTSUP;
+ goto error;
+error_invalid:
+ res = -EINVAL;
+ goto error;
+error:
+ pw_properties_free(props);
+ if (stream)
+ stream_free(stream);
+ return res;
+}
+
+static int do_create_record_stream(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ struct impl *impl = client->impl;
+ const char *name = NULL;
+ int res;
+ struct sample_spec ss;
+ struct channel_map map;
+ uint32_t source_index;
+ const char *source_name;
+ struct buffer_attr attr = { 0 };
+ bool corked = false,
+ no_remap = false,
+ no_remix = false,
+ fix_format = false,
+ fix_rate = false,
+ fix_channels = false,
+ no_move = false,
+ variable_rate = false,
+ peak_detect = false,
+ adjust_latency = false,
+ early_requests = false,
+ dont_inhibit_auto_suspend = false,
+ volume_set = true,
+ muted = false,
+ muted_set = false,
+ fail_on_suspend = false,
+ relative_volume = false,
+ passthrough = false;
+ uint32_t direct_on_input_idx = SPA_ID_INVALID;
+ struct volume volume = VOLUME_INIT;
+ struct pw_properties *props = NULL;
+ uint8_t n_formats = 0;
+ struct stream *stream = NULL;
+ uint32_t n_params = 0, n_valid_formats = 0, flags, id, rate = 0;
+ const struct spa_pod *params[MAX_FORMATS];
+ uint8_t buffer[4096];
+ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
+
+ props = pw_properties_copy(client->props);
+ if (props == NULL)
+ goto error_errno;
+
+ if (client->version < 13) {
+ if (message_get(m,
+ TAG_STRING, &name,
+ TAG_INVALID) < 0)
+ goto error_protocol;
+ if (name == NULL)
+ goto error_protocol;
+ }
+ if (message_get(m,
+ TAG_SAMPLE_SPEC, &ss,
+ TAG_CHANNEL_MAP, &map,
+ TAG_U32, &source_index,
+ TAG_STRING, &source_name,
+ TAG_U32, &attr.maxlength,
+ TAG_BOOLEAN, &corked,
+ TAG_U32, &attr.fragsize,
+ TAG_INVALID) < 0)
+ goto error_protocol;
+
+ pw_log_info("[%s] CREATE_RECORD_STREAM tag:%u corked:%u source-name:%s source-index:%u",
+ client->name, tag, corked, source_name, source_index);
+
+ if (source_index != SPA_ID_INVALID && source_name != NULL)
+ goto error_invalid;
+
+ if (client->version >= 12) {
+ if (message_get(m,
+ TAG_BOOLEAN, &no_remap,
+ TAG_BOOLEAN, &no_remix,
+ TAG_BOOLEAN, &fix_format,
+ TAG_BOOLEAN, &fix_rate,
+ TAG_BOOLEAN, &fix_channels,
+ TAG_BOOLEAN, &no_move,
+ TAG_BOOLEAN, &variable_rate,
+ TAG_INVALID) < 0)
+ goto error_protocol;
+ }
+ if (client->version >= 13) {
+ if (message_get(m,
+ TAG_BOOLEAN, &peak_detect,
+ TAG_BOOLEAN, &adjust_latency,
+ TAG_PROPLIST, props,
+ TAG_U32, &direct_on_input_idx,
+ TAG_INVALID) < 0)
+ goto error_protocol;
+ }
+ if (client->version >= 14) {
+ if (message_get(m,
+ TAG_BOOLEAN, &early_requests,
+ TAG_INVALID) < 0)
+ goto error_protocol;
+ }
+ if (client->version >= 15) {
+ if (message_get(m,
+ TAG_BOOLEAN, &dont_inhibit_auto_suspend,
+ TAG_BOOLEAN, &fail_on_suspend,
+ TAG_INVALID) < 0)
+ goto error_protocol;
+ }
+ if (client->version >= 22) {
+ if (message_get(m,
+ TAG_U8, &n_formats,
+ TAG_INVALID) < 0)
+ goto error_protocol;
+
+ if (n_formats) {
+ uint8_t i;
+ for (i = 0; i < n_formats; i++) {
+ struct format_info format;
+ uint32_t r;
+
+ if (message_get(m,
+ TAG_FORMAT_INFO, &format,
+ TAG_INVALID) < 0)
+ goto error_protocol;
+
+ if (n_params < MAX_FORMATS &&
+ (params[n_params] = format_info_build_param(&b,
+ SPA_PARAM_EnumFormat, &format, &r)) != NULL) {
+ n_params++;
+ n_valid_formats++;
+ if (r > rate)
+ rate = r;
+ } else {
+ log_format_info(impl, SPA_LOG_LEVEL_WARN, &format);
+ }
+ format_info_clear(&format);
+ }
+ }
+ if (message_get(m,
+ TAG_CVOLUME, &volume,
+ TAG_BOOLEAN, &muted,
+ TAG_BOOLEAN, &volume_set,
+ TAG_BOOLEAN, &muted_set,
+ TAG_BOOLEAN, &relative_volume,
+ TAG_BOOLEAN, &passthrough,
+ TAG_INVALID) < 0)
+ goto error_protocol;
+ } else {
+ volume_set = false;
+ }
+ if (sample_spec_valid(&ss)) {
+ if (fix_format || fix_rate || fix_channels) {
+ struct sample_spec sfix = ss;
+ if (fix_format)
+ sfix.format = SPA_AUDIO_FORMAT_UNKNOWN;
+ if (fix_rate)
+ sfix.rate = 0;
+ if (fix_channels)
+ sfix.channels = 0;
+ if (n_params < MAX_FORMATS &&
+ (params[n_params] = format_build_param(&b,
+ SPA_PARAM_EnumFormat, &sfix,
+ sfix.channels > 0 ? &map : NULL)) != NULL) {
+ n_params++;
+ n_valid_formats++;
+ }
+ }
+ else if (n_params < MAX_FORMATS &&
+ (params[n_params] = format_build_param(&b,
+ SPA_PARAM_EnumFormat, &ss,
+ ss.channels > 0 ? &map : NULL)) != NULL) {
+ n_params++;
+ n_valid_formats++;
+ } else {
+ pw_log_warn("%p: unsupported format:%s rate:%d channels:%u",
+ impl, format_id2name(ss.format), ss.rate,
+ ss.channels);
+ }
+ rate = ss.rate;
+ }
+ if (m->offset != m->length)
+ goto error_protocol;
+
+ if (n_valid_formats == 0)
+ goto error_no_formats;
+
+ stream = stream_new(client, STREAM_TYPE_RECORD, tag, &ss, &map, &attr);
+ if (stream == NULL)
+ goto error_errno;
+
+ stream->corked = corked;
+ stream->adjust_latency = adjust_latency;
+ stream->early_requests = early_requests;
+ stream->volume = volume;
+ stream->volume_set = volume_set;
+ stream->muted = muted;
+ stream->muted_set = muted_set;
+
+ if (client->quirks & QUIRK_REMOVE_CAPTURE_DONT_MOVE)
+ no_move = false;
+
+ if (rate != 0) {
+ struct spa_fraction lat;
+ fix_record_buffer_attr(stream, &attr, rate, &lat);
+ pw_properties_setf(props, PW_KEY_NODE_RATE, "1/%u", rate);
+ pw_properties_setf(props, PW_KEY_NODE_LATENCY, "%u/%u",
+ lat.num, lat.denom);
+ }
+ if (peak_detect)
+ pw_properties_set(props, PW_KEY_STREAM_MONITOR, "true");
+ if (no_remix)
+ pw_properties_set(props, PW_KEY_STREAM_DONT_REMIX, "true");
+ flags = 0;
+ if (no_move)
+ flags |= PW_STREAM_FLAG_DONT_RECONNECT;
+
+ if (direct_on_input_idx != SPA_ID_INVALID) {
+ source_index = direct_on_input_idx;
+ } else if (source_name != NULL) {
+ if ((id = atoi(source_name)) != 0)
+ source_index = id;
+ }
+ if (source_index != SPA_ID_INVALID && source_index != 0) {
+ pw_properties_setf(props,
+ PW_KEY_TARGET_OBJECT, "%u", source_index);
+ } else if (source_name != NULL) {
+ if (spa_strendswith(source_name, ".monitor")) {
+ pw_properties_setf(props,
+ PW_KEY_TARGET_OBJECT,
+ "%.*s", (int)strlen(source_name)-8, source_name);
+ pw_properties_set(props,
+ PW_KEY_STREAM_CAPTURE_SINK, "true");
+ } else {
+ pw_properties_set(props,
+ PW_KEY_TARGET_OBJECT, source_name);
+ }
+ }
+
+ stream->stream = pw_stream_new(client->core, name, props);
+ props = NULL;
+ if (stream->stream == NULL)
+ goto error_errno;
+
+ pw_stream_add_listener(stream->stream,
+ &stream->stream_listener,
+ &stream_events, stream);
+
+ pw_stream_connect(stream->stream,
+ PW_DIRECTION_INPUT,
+ SPA_ID_INVALID,
+ flags |
+ PW_STREAM_FLAG_AUTOCONNECT |
+ PW_STREAM_FLAG_RT_PROCESS |
+ PW_STREAM_FLAG_MAP_BUFFERS,
+ params, n_params);
+
+ return 0;
+
+error_errno:
+ res = -errno;
+ goto error;
+error_protocol:
+ res = -EPROTO;
+ goto error;
+error_no_formats:
+ res = -ENOTSUP;
+ goto error;
+error_invalid:
+ res = -EINVAL;
+ goto error;
+error:
+ pw_properties_free(props);
+ if (stream)
+ stream_free(stream);
+ return res;
+}
+
+static int do_delete_stream(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ uint32_t channel;
+ struct stream *stream;
+ int res;
+
+ if ((res = message_get(m,
+ TAG_U32, &channel,
+ TAG_INVALID)) < 0)
+ return -EPROTO;
+
+ pw_log_info("[%s] DELETE_STREAM tag:%u channel:%u",
+ client->name, tag, channel);
+
+ stream = pw_map_lookup(&client->streams, channel);
+ if (stream == NULL)
+ return -ENOENT;
+ if (command == COMMAND_DELETE_PLAYBACK_STREAM &&
+ stream->type != STREAM_TYPE_PLAYBACK)
+ return -ENOENT;
+ if (command == COMMAND_DELETE_RECORD_STREAM &&
+ stream->type != STREAM_TYPE_RECORD)
+ return -ENOENT;
+ if (command == COMMAND_DELETE_UPLOAD_STREAM &&
+ stream->type != STREAM_TYPE_UPLOAD)
+ return -ENOENT;
+
+ stream_free(stream);
+
+ return reply_simple_ack(client, tag);
+}
+
+static int do_get_playback_latency(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ struct impl *impl = client->impl;
+ struct message *reply;
+ uint32_t channel;
+ struct timeval tv, now;
+ struct stream *stream;
+ int res;
+
+ if ((res = message_get(m,
+ TAG_U32, &channel,
+ TAG_TIMEVAL, &tv,
+ TAG_INVALID)) < 0)
+ return -EPROTO;
+
+ pw_log_debug("%p: %s tag:%u channel:%u", impl, commands[command].name, tag, channel);
+ stream = pw_map_lookup(&client->streams, channel);
+ if (stream == NULL || stream->type != STREAM_TYPE_PLAYBACK)
+ return -ENOENT;
+
+ pw_log_debug("read:0x%"PRIx64" write:0x%"PRIx64" queued:%"PRIi64" delay:%"PRIi64
+ " playing:%"PRIu64,
+ stream->read_index, stream->write_index,
+ stream->write_index - stream->read_index, stream->delay,
+ stream->playing_for);
+
+ gettimeofday(&now, NULL);
+
+ reply = reply_new(client, tag);
+ message_put(reply,
+ TAG_USEC, stream->delay, /* sink latency + queued samples */
+ TAG_USEC, 0LL, /* always 0 */
+ TAG_BOOLEAN, stream->playing_for > 0 &&
+ !stream->corked, /* playing state */
+ TAG_TIMEVAL, &tv,
+ TAG_TIMEVAL, &now,
+ TAG_S64, stream->write_index,
+ TAG_S64, stream->read_index,
+ TAG_INVALID);
+
+ if (client->version >= 13) {
+ message_put(reply,
+ TAG_U64, stream->underrun_for,
+ TAG_U64, stream->playing_for,
+ TAG_INVALID);
+ }
+ return client_queue_message(client, reply);
+}
+
+static int do_get_record_latency(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ struct impl *impl = client->impl;
+ struct message *reply;
+ uint32_t channel;
+ struct timeval tv, now;
+ struct stream *stream;
+ int res;
+
+ if ((res = message_get(m,
+ TAG_U32, &channel,
+ TAG_TIMEVAL, &tv,
+ TAG_INVALID)) < 0)
+ return -EPROTO;
+
+ pw_log_debug("%p: %s channel:%u", impl, commands[command].name, channel);
+ stream = pw_map_lookup(&client->streams, channel);
+ if (stream == NULL || stream->type != STREAM_TYPE_RECORD)
+ return -ENOENT;
+
+ pw_log_debug("read:0x%"PRIx64" write:0x%"PRIx64" queued:%"PRIi64" delay:%"PRIi64,
+ stream->read_index, stream->write_index,
+ stream->write_index - stream->read_index, stream->delay);
+
+
+ gettimeofday(&now, NULL);
+ reply = reply_new(client, tag);
+ message_put(reply,
+ TAG_USEC, 0LL, /* monitor latency */
+ TAG_USEC, stream->delay, /* source latency + queued */
+ TAG_BOOLEAN, !stream->corked, /* playing state */
+ TAG_TIMEVAL, &tv,
+ TAG_TIMEVAL, &now,
+ TAG_S64, stream->write_index,
+ TAG_S64, stream->read_index,
+ TAG_INVALID);
+
+ return client_queue_message(client, reply);
+}
+
+static int do_create_upload_stream(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ const char *name;
+ struct sample_spec ss;
+ struct channel_map map;
+ struct pw_properties *props = NULL;
+ uint32_t length;
+ struct stream *stream = NULL;
+ struct message *reply;
+ int res;
+
+ if ((props = pw_properties_copy(client->props)) == NULL)
+ goto error_errno;
+
+ if ((res = message_get(m,
+ TAG_STRING, &name,
+ TAG_SAMPLE_SPEC, &ss,
+ TAG_CHANNEL_MAP, &map,
+ TAG_U32, &length,
+ TAG_INVALID)) < 0)
+ goto error_proto;
+
+ if (client->version >= 13) {
+ if ((res = message_get(m,
+ TAG_PROPLIST, props,
+ TAG_INVALID)) < 0)
+ goto error_proto;
+
+ } else {
+ pw_properties_set(props, PW_KEY_MEDIA_NAME, name);
+ }
+ if (name == NULL)
+ name = pw_properties_get(props, "event.id");
+ if (name == NULL)
+ name = pw_properties_get(props, PW_KEY_MEDIA_NAME);
+
+ if (name == NULL ||
+ !sample_spec_valid(&ss) ||
+ !channel_map_valid(&map) ||
+ ss.channels != map.channels ||
+ length == 0 || (length % sample_spec_frame_size(&ss) != 0))
+ goto error_invalid;
+ if (length >= SCACHE_ENTRY_SIZE_MAX)
+ goto error_toolarge;
+
+ pw_log_info("[%s] %s tag:%u name:%s length:%d",
+ client->name, commands[command].name, tag,
+ name, length);
+
+ stream = stream_new(client, STREAM_TYPE_UPLOAD, tag, &ss, &map, &(struct buffer_attr) {
+ .maxlength = length,
+ });
+ if (stream == NULL)
+ goto error_errno;
+
+ stream->props = props;
+
+ stream->buffer = calloc(1, MAXLENGTH);
+ if (stream->buffer == NULL)
+ goto error_errno;
+
+ reply = reply_new(client, tag);
+ message_put(reply,
+ TAG_U32, stream->channel,
+ TAG_U32, length,
+ TAG_INVALID);
+ return client_queue_message(client, reply);
+
+error_errno:
+ res = -errno;
+ goto error;
+error_proto:
+ res = -EPROTO;
+ goto error;
+error_invalid:
+ res = -EINVAL;
+ goto error;
+error_toolarge:
+ res = -EOVERFLOW;
+ goto error;
+error:
+ pw_properties_free(props);
+ if (stream)
+ stream_free(stream);
+ return res;
+}
+
+static int do_finish_upload_stream(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ struct impl *impl = client->impl;
+ uint32_t channel, event;
+ struct stream *stream;
+ struct sample *sample;
+ const char *name;
+ int res;
+
+ if (message_get(m,
+ TAG_U32, &channel,
+ TAG_INVALID) < 0)
+ return -EPROTO;
+
+ stream = pw_map_lookup(&client->streams, channel);
+ if (stream == NULL || stream->type != STREAM_TYPE_UPLOAD)
+ return -ENOENT;
+
+ name = pw_properties_get(stream->props, "event.id");
+ if (name == NULL)
+ name = pw_properties_get(stream->props, PW_KEY_MEDIA_NAME);
+ if (name == NULL)
+ goto error_invalid;
+
+ pw_log_info("[%s] %s tag:%u channel:%u name:%s",
+ client->name, commands[command].name, tag,
+ channel, name);
+
+ struct sample *old = find_sample(impl, SPA_ID_INVALID, name);
+ if (old == NULL || old->ref > 1) {
+ sample = calloc(1, sizeof(*sample));
+ if (sample == NULL)
+ goto error_errno;
+
+ if (old != NULL) {
+ sample->index = old->index;
+ spa_assert_se(pw_map_insert_at(&impl->samples, sample->index, sample) == 0);
+
+ old->index = SPA_ID_INVALID;
+ sample_unref(old);
+ } else {
+ sample->index = pw_map_insert_new(&impl->samples, sample);
+ if (sample->index == SPA_ID_INVALID)
+ goto error_errno;
+ }
+ } else {
+ pw_properties_free(old->props);
+ free(old->buffer);
+ impl->stat.sample_cache -= old->length;
+
+ sample = old;
+ }
+
+ if (old != NULL)
+ event = SUBSCRIPTION_EVENT_CHANGE;
+ else
+ event = SUBSCRIPTION_EVENT_NEW;
+
+ sample->ref = 1;
+ sample->impl = impl;
+ sample->name = name;
+ sample->props = stream->props;
+ sample->ss = stream->ss;
+ sample->map = stream->map;
+ sample->buffer = stream->buffer;
+ sample->length = stream->attr.maxlength;
+
+ impl->stat.sample_cache += sample->length;
+
+ stream->props = NULL;
+ stream->buffer = NULL;
+ stream_free(stream);
+
+ broadcast_subscribe_event(impl,
+ SUBSCRIPTION_MASK_SAMPLE_CACHE,
+ event | SUBSCRIPTION_EVENT_SAMPLE_CACHE,
+ sample->index);
+
+ return reply_simple_ack(client, tag);
+
+error_errno:
+ res = -errno;
+ free(sample);
+ goto error;
+error_invalid:
+ res = -EINVAL;
+ goto error;
+error:
+ stream_free(stream);
+ return res;
+}
+
+static const char *get_default(struct client *client, bool sink)
+{
+ struct selector sel;
+ struct pw_manager *manager = client->manager;
+ struct pw_manager_object *o;
+ const char *def, *str, *mon;
+
+ spa_zero(sel);
+ if (sink) {
+ sel.type = pw_manager_object_is_sink;
+ sel.key = PW_KEY_NODE_NAME;
+ sel.value = client->default_sink;
+ def = DEFAULT_SINK;
+ } else {
+ sel.type = pw_manager_object_is_source_or_monitor;
+ sel.key = PW_KEY_NODE_NAME;
+ sel.value = client->default_source;
+ def = DEFAULT_SOURCE;
+ }
+ sel.accumulate = select_best;
+
+ o = select_object(manager, &sel);
+ if (o == NULL || o->props == NULL)
+ return def;
+ str = pw_properties_get(o->props, PW_KEY_NODE_NAME);
+
+ if (!sink && pw_manager_object_is_monitor(o)) {
+ def = DEFAULT_MONITOR;
+ if (str != NULL &&
+ (mon = pw_properties_get(o->props, PW_KEY_NODE_NAME".monitor")) == NULL) {
+ pw_properties_setf(o->props,
+ PW_KEY_NODE_NAME".monitor",
+ "%s.monitor", str);
+ }
+ str = pw_properties_get(o->props, PW_KEY_NODE_NAME".monitor");
+ }
+ if (str == NULL)
+ str = def;
+ return str;
+}
+
+static struct pw_manager_object *find_device(struct client *client,
+ uint32_t index, const char *name, bool sink, bool *is_monitor)
+{
+ struct selector sel;
+ bool monitor = false, find_default = false;
+ struct pw_manager_object *o;
+
+ if (name != NULL) {
+ if (spa_streq(name, DEFAULT_MONITOR)) {
+ if (sink)
+ return NULL;
+ sink = true;
+ find_default = true;
+ } else if (spa_streq(name, DEFAULT_SOURCE)) {
+ if (sink)
+ return NULL;
+ find_default = true;
+ } else if (spa_streq(name, DEFAULT_SINK)) {
+ if (!sink)
+ return NULL;
+ find_default = true;
+ } else if (spa_atou32(name, &index, 0)) {
+ name = NULL;
+ }
+ }
+ if (name == NULL && (index == SPA_ID_INVALID || index == 0))
+ find_default = true;
+
+ if (find_default) {
+ name = get_default(client, sink);
+ index = SPA_ID_INVALID;
+ }
+
+ if (name != NULL) {
+ if (spa_strendswith(name, ".monitor")) {
+ name = strndupa(name, strlen(name)-8);
+ monitor = true;
+ }
+ } else if (index == SPA_ID_INVALID)
+ return NULL;
+
+
+ spa_zero(sel);
+ sel.type = sink ?
+ pw_manager_object_is_sink :
+ pw_manager_object_is_source_or_monitor;
+ sel.index = index;
+ sel.key = PW_KEY_NODE_NAME;
+ sel.value = name;
+
+ o = select_object(client->manager, &sel);
+ if (o != NULL) {
+ if (!sink && pw_manager_object_is_monitor(o))
+ monitor = true;
+ }
+ if (is_monitor)
+ *is_monitor = monitor;
+
+ return o;
+}
+
+static void sample_play_finish(struct pending_sample *ps)
+{
+ struct client *client = ps->client;
+ pending_sample_free(ps);
+ client_unref(client);
+}
+
+static void sample_play_ready_reply(void *data, struct client *client, uint32_t tag)
+{
+ struct pending_sample *ps = data;
+ struct message *reply;
+ uint32_t index = id_to_index(client->manager, ps->play->id);
+
+ pw_log_info("[%s] PLAY_SAMPLE tag:%u index:%u",
+ client->name, ps->tag, index);
+
+ ps->ready = true;
+
+ reply = reply_new(client, ps->tag);
+ if (client->version >= 13)
+ message_put(reply,
+ TAG_U32, index,
+ TAG_INVALID);
+
+ client_queue_message(client, reply);
+
+ if (ps->done)
+ sample_play_finish(ps);
+}
+
+static void sample_play_ready(void *data, uint32_t id)
+{
+ struct pending_sample *ps = data;
+ struct client *client = ps->client;
+ operation_new_cb(client, ps->tag, sample_play_ready_reply, ps);
+}
+
+static void on_sample_done(void *obj, void *data, int res, uint32_t id)
+{
+ struct pending_sample *ps = obj;
+ ps->done = true;
+ if (ps->ready)
+ sample_play_finish(ps);
+}
+
+static void sample_play_done(void *data, int res)
+{
+ struct pending_sample *ps = data;
+ struct client *client = ps->client;
+ struct impl *impl = client->impl;
+
+ if (res < 0)
+ reply_error(client, COMMAND_PLAY_SAMPLE, ps->tag, res);
+ else
+ pw_log_info("[%s] PLAY_SAMPLE done tag:%u", client->name, ps->tag);
+
+ pw_work_queue_add(impl->work_queue, ps, 0,
+ on_sample_done, client);
+}
+
+static const struct sample_play_events sample_play_events = {
+ VERSION_SAMPLE_PLAY_EVENTS,
+ .ready = sample_play_ready,
+ .done = sample_play_done,
+};
+
+static int do_play_sample(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ struct impl *impl = client->impl;
+ uint32_t sink_index, volume;
+ struct sample *sample;
+ struct sample_play *play;
+ const char *sink_name, *name;
+ struct pw_properties *props = NULL;
+ struct pending_sample *ps;
+ struct pw_manager_object *o;
+ int res;
+
+ if ((props = pw_properties_new(NULL, NULL)) == NULL)
+ goto error_errno;
+
+ if ((res = message_get(m,
+ TAG_U32, &sink_index,
+ TAG_STRING, &sink_name,
+ TAG_U32, &volume,
+ TAG_STRING, &name,
+ TAG_INVALID)) < 0)
+ goto error_proto;
+
+ if (client->version >= 13) {
+ if ((res = message_get(m,
+ TAG_PROPLIST, props,
+ TAG_INVALID)) < 0)
+ goto error_proto;
+
+ }
+ pw_log_info("[%s] %s tag:%u sink_index:%u sink_name:%s name:%s",
+ client->name, commands[command].name, tag,
+ sink_index, sink_name, name);
+
+ pw_properties_update(props, &client->props->dict);
+
+ if (sink_index != SPA_ID_INVALID && sink_name != NULL)
+ goto error_inval;
+
+ o = find_device(client, sink_index, sink_name, PW_DIRECTION_OUTPUT, NULL);
+ if (o == NULL)
+ goto error_noent;
+
+ sample = find_sample(impl, SPA_ID_INVALID, name);
+ if (sample == NULL)
+ goto error_noent;
+
+ pw_properties_setf(props, PW_KEY_TARGET_OBJECT, "%"PRIu64, o->serial);
+
+ play = sample_play_new(client->core, sample, props, sizeof(struct pending_sample));
+ props = NULL;
+ if (play == NULL)
+ goto error_errno;
+
+ ps = play->user_data;
+ ps->client = client;
+ ps->play = play;
+ ps->tag = tag;
+ sample_play_add_listener(play, &ps->listener, &sample_play_events, ps);
+ spa_list_append(&client->pending_samples, &ps->link);
+ client->ref++;
+
+ return 0;
+
+error_errno:
+ res = -errno;
+ goto error;
+error_proto:
+ res = -EPROTO;
+ goto error;
+error_inval:
+ res = -EINVAL;
+ goto error;
+error_noent:
+ res = -ENOENT;
+ goto error;
+error:
+ pw_properties_free(props);
+ return res;
+}
+
+static int do_remove_sample(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ struct impl *impl = client->impl;
+ const char *name;
+ struct sample *sample;
+ int res;
+
+ if ((res = message_get(m,
+ TAG_STRING, &name,
+ TAG_INVALID)) < 0)
+ return -EPROTO;
+
+ pw_log_info("[%s] %s tag:%u name:%s",
+ client->name, commands[command].name, tag,
+ name);
+ if (name == NULL)
+ return -EINVAL;
+ if ((sample = find_sample(impl, SPA_ID_INVALID, name)) == NULL)
+ return -ENOENT;
+
+ broadcast_subscribe_event(impl,
+ SUBSCRIPTION_MASK_SAMPLE_CACHE,
+ SUBSCRIPTION_EVENT_REMOVE |
+ SUBSCRIPTION_EVENT_SAMPLE_CACHE,
+ sample->index);
+
+ pw_map_remove(&impl->samples, sample->index);
+ sample->index = SPA_ID_INVALID;
+
+ sample_unref(sample);
+
+ return reply_simple_ack(client, tag);
+}
+
+static int do_cork_stream(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ uint32_t channel;
+ bool cork;
+ struct stream *stream;
+ int res;
+
+ if ((res = message_get(m,
+ TAG_U32, &channel,
+ TAG_BOOLEAN, &cork,
+ TAG_INVALID)) < 0)
+ return -EPROTO;
+
+ pw_log_info("[%s] %s tag:%u channel:%u cork:%s",
+ client->name, commands[command].name, tag,
+ channel, cork ? "yes" : "no");
+
+ stream = pw_map_lookup(&client->streams, channel);
+ if (stream == NULL || stream->type == STREAM_TYPE_UPLOAD)
+ return -ENOENT;
+
+ stream->corked = cork;
+ stream_set_paused(stream, cork, "cork request");
+ if (cork) {
+ stream->is_underrun = true;
+ } else {
+ stream->playing_for = 0;
+ stream->underrun_for = -1;
+ stream_send_request(stream);
+ }
+
+ return reply_simple_ack(client, tag);
+}
+
+static int do_flush_trigger_prebuf_stream(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ uint32_t channel;
+ struct stream *stream;
+ int res;
+
+ if ((res = message_get(m,
+ TAG_U32, &channel,
+ TAG_INVALID)) < 0)
+ return -EPROTO;
+
+ pw_log_info("[%s] %s tag:%u channel:%u",
+ client->name, commands[command].name, tag, channel);
+
+ stream = pw_map_lookup(&client->streams, channel);
+ if (stream == NULL || stream->type == STREAM_TYPE_UPLOAD)
+ return -ENOENT;
+
+ switch (command) {
+ case COMMAND_FLUSH_PLAYBACK_STREAM:
+ case COMMAND_FLUSH_RECORD_STREAM:
+ stream_flush(stream);
+ break;
+ case COMMAND_TRIGGER_PLAYBACK_STREAM:
+ case COMMAND_PREBUF_PLAYBACK_STREAM:
+ if (stream->type != STREAM_TYPE_PLAYBACK)
+ return -ENOENT;
+ if (command == COMMAND_TRIGGER_PLAYBACK_STREAM)
+ stream->in_prebuf = false;
+ else if (stream->attr.prebuf > 0)
+ stream->in_prebuf = true;
+ stream_send_request(stream);
+ break;
+ default:
+ return -EINVAL;
+ }
+ return reply_simple_ack(client, tag);
+}
+
+static int set_node_volume_mute(struct pw_manager_object *o,
+ struct volume *vol, bool *mute, bool is_monitor)
+{
+ char buf[1024];
+ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buf, sizeof(buf));
+ struct spa_pod_frame f[1];
+ struct spa_pod *param;
+ uint32_t volprop, muteprop;
+
+ if (!SPA_FLAG_IS_SET(o->permissions, PW_PERM_W | PW_PERM_X))
+ return -EACCES;
+ if (o->proxy == NULL)
+ return -ENOENT;
+
+ if (is_monitor) {
+ volprop = SPA_PROP_monitorVolumes;
+ muteprop = SPA_PROP_monitorMute;
+ } else {
+ volprop = SPA_PROP_channelVolumes;
+ muteprop = SPA_PROP_mute;
+ }
+
+ spa_pod_builder_push_object(&b, &f[0],
+ SPA_TYPE_OBJECT_Props, SPA_PARAM_Props);
+ if (vol)
+ spa_pod_builder_add(&b,
+ volprop, SPA_POD_Array(sizeof(float),
+ SPA_TYPE_Float,
+ vol->channels,
+ vol->values), 0);
+ if (mute)
+ spa_pod_builder_add(&b,
+ muteprop, SPA_POD_Bool(*mute), 0);
+ param = spa_pod_builder_pop(&b, &f[0]);
+
+ pw_node_set_param((struct pw_node*)o->proxy,
+ SPA_PARAM_Props, 0, param);
+ return 0;
+}
+
+static int set_card_volume_mute_delay(struct pw_manager_object *o, uint32_t port_index,
+ uint32_t device_id, struct volume *vol, bool *mute, int64_t *latency_offset)
+{
+ char buf[1024];
+ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buf, sizeof(buf));
+ struct spa_pod_frame f[2];
+ struct spa_pod *param;
+
+ if (!SPA_FLAG_IS_SET(o->permissions, PW_PERM_W | PW_PERM_X))
+ return -EACCES;
+
+ if (o->proxy == NULL)
+ return -ENOENT;
+
+ spa_pod_builder_push_object(&b, &f[0],
+ SPA_TYPE_OBJECT_ParamRoute, SPA_PARAM_Route);
+ spa_pod_builder_add(&b,
+ SPA_PARAM_ROUTE_index, SPA_POD_Int(port_index),
+ SPA_PARAM_ROUTE_device, SPA_POD_Int(device_id),
+ 0);
+ spa_pod_builder_prop(&b, SPA_PARAM_ROUTE_props, 0);
+ spa_pod_builder_push_object(&b, &f[1],
+ SPA_TYPE_OBJECT_Props, SPA_PARAM_Props);
+ if (vol)
+ spa_pod_builder_add(&b,
+ SPA_PROP_channelVolumes, SPA_POD_Array(sizeof(float),
+ SPA_TYPE_Float,
+ vol->channels,
+ vol->values), 0);
+ if (mute)
+ spa_pod_builder_add(&b,
+ SPA_PROP_mute, SPA_POD_Bool(*mute), 0);
+ if (latency_offset)
+ spa_pod_builder_add(&b,
+ SPA_PROP_latencyOffsetNsec, SPA_POD_Long(*latency_offset), 0);
+ spa_pod_builder_pop(&b, &f[1]);
+ spa_pod_builder_prop(&b, SPA_PARAM_ROUTE_save, 0);
+ spa_pod_builder_bool(&b, true);
+ param = spa_pod_builder_pop(&b, &f[0]);
+
+ pw_device_set_param((struct pw_device*)o->proxy,
+ SPA_PARAM_Route, 0, param);
+ return 0;
+}
+
+static int set_card_port(struct pw_manager_object *o, uint32_t device_id,
+ uint32_t port_index)
+{
+ char buf[1024];
+ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buf, sizeof(buf));
+
+ if (!SPA_FLAG_IS_SET(o->permissions, PW_PERM_W | PW_PERM_X))
+ return -EACCES;
+
+ if (o->proxy == NULL)
+ return -ENOENT;
+
+ pw_device_set_param((struct pw_device*)o->proxy,
+ SPA_PARAM_Route, 0,
+ spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamRoute, SPA_PARAM_Route,
+ SPA_PARAM_ROUTE_index, SPA_POD_Int(port_index),
+ SPA_PARAM_ROUTE_device, SPA_POD_Int(device_id),
+ SPA_PARAM_ROUTE_save, SPA_POD_Bool(true)));
+
+ return 0;
+}
+
+static int do_set_stream_volume(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ struct pw_manager *manager = client->manager;
+ uint32_t index;
+ struct stream *stream;
+ struct volume volume;
+ int res;
+
+ if ((res = message_get(m,
+ TAG_U32, &index,
+ TAG_CVOLUME, &volume,
+ TAG_INVALID)) < 0)
+ return -EPROTO;
+
+ pw_log_info("[%s] %s tag:%u index:%u",
+ client->name, commands[command].name, tag, index);
+
+ stream = find_stream(client, index);
+ if (stream != NULL) {
+
+ if (volume_compare(&stream->volume, &volume) == 0)
+ goto done;
+
+ pw_stream_set_control(stream->stream,
+ SPA_PROP_channelVolumes, volume.channels, volume.values,
+ 0);
+ } else {
+ struct selector sel;
+ struct pw_manager_object *o;
+
+ spa_zero(sel);
+ sel.index = index;
+ if (command == COMMAND_SET_SINK_INPUT_VOLUME)
+ sel.type = pw_manager_object_is_sink_input;
+ else
+ sel.type = pw_manager_object_is_source_output;
+
+ o = select_object(manager, &sel);
+ if (o == NULL)
+ return -ENOENT;
+
+ if ((res = set_node_volume_mute(o, &volume, NULL, false)) < 0)
+ return res;
+ }
+done:
+ return operation_new(client, tag);
+}
+
+static int do_set_stream_mute(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ struct pw_manager *manager = client->manager;
+ uint32_t index;
+ struct stream *stream;
+ int res;
+ bool mute;
+
+ if ((res = message_get(m,
+ TAG_U32, &index,
+ TAG_BOOLEAN, &mute,
+ TAG_INVALID)) < 0)
+ return -EPROTO;
+
+ pw_log_info("[%s] DO_SET_STREAM_MUTE tag:%u index:%u mute:%u",
+ client->name, tag, index, mute);
+
+ stream = find_stream(client, index);
+ if (stream != NULL) {
+ float val;
+
+ if (stream->muted == mute)
+ goto done;
+
+ val = mute ? 1.0f : 0.0f;
+ pw_stream_set_control(stream->stream,
+ SPA_PROP_mute, 1, &val,
+ 0);
+ } else {
+ struct selector sel;
+ struct pw_manager_object *o;
+
+ spa_zero(sel);
+ sel.index = index;
+ if (command == COMMAND_SET_SINK_INPUT_MUTE)
+ sel.type = pw_manager_object_is_sink_input;
+ else
+ sel.type = pw_manager_object_is_source_output;
+
+ o = select_object(manager, &sel);
+ if (o == NULL)
+ return -ENOENT;
+
+ if ((res = set_node_volume_mute(o, NULL, &mute, false)) < 0)
+ return res;
+ }
+done:
+ return operation_new(client, tag);
+}
+
+static int do_set_volume(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ struct impl *impl = client->impl;
+ struct pw_manager *manager = client->manager;
+ struct pw_node_info *info;
+ uint32_t index, card_id = SPA_ID_INVALID;
+ const char *name, *str;
+ struct volume volume;
+ struct pw_manager_object *o, *card = NULL;
+ int res;
+ struct device_info dev_info;
+ enum pw_direction direction;
+ bool is_monitor;
+
+ if ((res = message_get(m,
+ TAG_U32, &index,
+ TAG_STRING, &name,
+ TAG_CVOLUME, &volume,
+ TAG_INVALID)) < 0)
+ return -EPROTO;
+
+ pw_log_info("[%s] %s tag:%u index:%u name:%s",
+ client->name, commands[command].name, tag, index, name);
+
+ if ((index == SPA_ID_INVALID && name == NULL) ||
+ (index != SPA_ID_INVALID && name != NULL))
+ return -EINVAL;
+
+ if (command == COMMAND_SET_SINK_VOLUME)
+ direction = PW_DIRECTION_OUTPUT;
+ else
+ direction = PW_DIRECTION_INPUT;
+
+ o = find_device(client, index, name, direction == PW_DIRECTION_OUTPUT, &is_monitor);
+ if (o == NULL || (info = o->info) == NULL || info->props == NULL)
+ return -ENOENT;
+
+ dev_info = DEVICE_INFO_INIT(direction);
+
+ if ((str = spa_dict_lookup(info->props, PW_KEY_DEVICE_ID)) != NULL)
+ card_id = (uint32_t)atoi(str);
+ if ((str = spa_dict_lookup(info->props, "card.profile.device")) != NULL)
+ dev_info.device = (uint32_t)atoi(str);
+ if (card_id != SPA_ID_INVALID) {
+ struct selector sel = { .id = card_id, .type = pw_manager_object_is_card, };
+ card = select_object(manager, &sel);
+ }
+ collect_device_info(o, card, &dev_info, is_monitor, &impl->defs);
+
+ if (dev_info.have_volume &&
+ volume_compare(&dev_info.volume_info.volume, &volume) == 0)
+ goto done;
+
+ if (card != NULL && !is_monitor && dev_info.active_port != SPA_ID_INVALID)
+ res = set_card_volume_mute_delay(card, dev_info.active_port,
+ dev_info.device, &volume, NULL, NULL);
+ else
+ res = set_node_volume_mute(o, &volume, NULL, is_monitor);
+
+ if (res < 0)
+ return res;
+
+done:
+ return operation_new(client, tag);
+}
+
+static int do_set_mute(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ struct impl *impl = client->impl;
+ struct pw_manager *manager = client->manager;
+ struct pw_node_info *info;
+ uint32_t index, card_id = SPA_ID_INVALID;
+ const char *name, *str;
+ bool mute;
+ struct pw_manager_object *o, *card = NULL;
+ int res;
+ struct device_info dev_info;
+ enum pw_direction direction;
+ bool is_monitor;
+
+ if ((res = message_get(m,
+ TAG_U32, &index,
+ TAG_STRING, &name,
+ TAG_BOOLEAN, &mute,
+ TAG_INVALID)) < 0)
+ return -EPROTO;
+
+ pw_log_info("[%s] %s tag:%u index:%u name:%s mute:%d",
+ client->name, commands[command].name, tag, index, name, mute);
+
+ if ((index == SPA_ID_INVALID && name == NULL) ||
+ (index != SPA_ID_INVALID && name != NULL))
+ return -EINVAL;
+
+ if (command == COMMAND_SET_SINK_MUTE)
+ direction = PW_DIRECTION_OUTPUT;
+ else
+ direction = PW_DIRECTION_INPUT;
+
+ o = find_device(client, index, name, direction == PW_DIRECTION_OUTPUT, &is_monitor);
+ if (o == NULL || (info = o->info) == NULL || info->props == NULL)
+ return -ENOENT;
+
+ dev_info = DEVICE_INFO_INIT(direction);
+
+ if ((str = spa_dict_lookup(info->props, PW_KEY_DEVICE_ID)) != NULL)
+ card_id = (uint32_t)atoi(str);
+ if ((str = spa_dict_lookup(info->props, "card.profile.device")) != NULL)
+ dev_info.device = (uint32_t)atoi(str);
+ if (card_id != SPA_ID_INVALID) {
+ struct selector sel = { .id = card_id, .type = pw_manager_object_is_card, };
+ card = select_object(manager, &sel);
+ }
+ collect_device_info(o, card, &dev_info, is_monitor, &impl->defs);
+
+ if (dev_info.have_volume &&
+ dev_info.volume_info.mute == mute)
+ goto done;
+
+ if (card != NULL && !is_monitor && dev_info.active_port != SPA_ID_INVALID)
+ res = set_card_volume_mute_delay(card, dev_info.active_port,
+ dev_info.device, NULL, &mute, NULL);
+ else
+ res = set_node_volume_mute(o, NULL, &mute, is_monitor);
+
+ if (res < 0)
+ return res;
+done:
+ return operation_new(client, tag);
+}
+
+static int do_set_port(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ struct pw_manager *manager = client->manager;
+ struct pw_node_info *info;
+ uint32_t index, card_id = SPA_ID_INVALID, device_id = SPA_ID_INVALID;
+ uint32_t port_index = SPA_ID_INVALID;
+ const char *name, *str, *port_name;
+ struct pw_manager_object *o, *card = NULL;
+ int res;
+ enum pw_direction direction;
+
+ if ((res = message_get(m,
+ TAG_U32, &index,
+ TAG_STRING, &name,
+ TAG_STRING, &port_name,
+ TAG_INVALID)) < 0)
+ return -EPROTO;
+
+ pw_log_info("[%s] %s tag:%u index:%u name:%s port:%s",
+ client->name, commands[command].name, tag, index, name, port_name);
+
+ if ((index == SPA_ID_INVALID && name == NULL) ||
+ (index != SPA_ID_INVALID && name != NULL))
+ return -EINVAL;
+
+ if (command == COMMAND_SET_SINK_PORT)
+ direction = PW_DIRECTION_OUTPUT;
+ else
+ direction = PW_DIRECTION_INPUT;
+
+ o = find_device(client, index, name, direction == PW_DIRECTION_OUTPUT, NULL);
+ if (o == NULL || (info = o->info) == NULL || info->props == NULL)
+ return -ENOENT;
+
+ if ((str = spa_dict_lookup(info->props, PW_KEY_DEVICE_ID)) != NULL)
+ card_id = (uint32_t)atoi(str);
+ if ((str = spa_dict_lookup(info->props, "card.profile.device")) != NULL)
+ device_id = (uint32_t)atoi(str);
+ if (card_id != SPA_ID_INVALID) {
+ struct selector sel = { .id = card_id, .type = pw_manager_object_is_card, };
+ card = select_object(manager, &sel);
+ }
+ if (card == NULL || device_id == SPA_ID_INVALID)
+ return -ENOENT;
+
+ port_index = find_port_index(card, direction, port_name);
+ if (port_index == SPA_ID_INVALID)
+ return -ENOENT;
+
+ if ((res = set_card_port(card, device_id, port_index)) < 0)
+ return res;
+
+ return operation_new(client, tag);
+}
+
+static int do_set_port_latency_offset(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ struct pw_manager *manager = client->manager;
+ const char *port_name = NULL;
+ struct pw_manager_object *card;
+ struct selector sel;
+ struct card_info card_info = CARD_INFO_INIT;
+ struct port_info *port_info;
+ int64_t offset;
+ int64_t value;
+ int res;
+ uint32_t n_ports;
+ size_t i;
+
+ spa_zero(sel);
+ sel.key = PW_KEY_DEVICE_NAME;
+ sel.type = pw_manager_object_is_card;
+
+ if ((res = message_get(m,
+ TAG_U32, &sel.index,
+ TAG_STRING, &sel.value,
+ TAG_STRING, &port_name,
+ TAG_S64, &offset,
+ TAG_INVALID)) < 0)
+ return -EPROTO;
+
+ pw_log_info("[%s] %s tag:%u index:%u card_name:%s port_name:%s offset:%"PRIi64,
+ client->name, commands[command].name, tag, sel.index, sel.value, port_name, offset);
+
+ if ((sel.index == SPA_ID_INVALID && sel.value == NULL) ||
+ (sel.index != SPA_ID_INVALID && sel.value != NULL))
+ return -EINVAL;
+ if (port_name == NULL)
+ return -EINVAL;
+
+ value = offset * 1000; /* to nsec */
+
+ if ((card = select_object(manager, &sel)) == NULL)
+ return -ENOENT;
+
+ collect_card_info(card, &card_info);
+ port_info = alloca(card_info.n_ports * sizeof(*port_info));
+ card_info.active_profile = SPA_ID_INVALID;
+ n_ports = collect_port_info(card, &card_info, NULL, port_info);
+
+ /* Set offset on all devices of the port */
+ res = -ENOENT;
+ for (i = 0; i < n_ports; i++) {
+ struct port_info *pi = &port_info[i];
+ size_t j;
+
+ if (!spa_streq(pi->name, port_name))
+ continue;
+
+ res = 0;
+ for (j = 0; j < pi->n_devices; ++j) {
+ res = set_card_volume_mute_delay(card, pi->index, pi->devices[j], NULL, NULL, &value);
+ if (res < 0)
+ break;
+ }
+
+ if (res < 0)
+ break;
+
+ return operation_new(client, tag);
+ }
+
+ return res;
+}
+
+static int do_set_stream_name(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ uint32_t channel;
+ struct stream *stream;
+ const char *name = NULL;
+ struct spa_dict_item items[1];
+ int res;
+
+ if ((res = message_get(m,
+ TAG_U32, &channel,
+ TAG_STRING, &name,
+ TAG_INVALID)) < 0)
+ return -EPROTO;
+
+ if (name == NULL)
+ return -EINVAL;
+
+ pw_log_info("[%s] SET_STREAM_NAME tag:%u channel:%d name:%s",
+ client->name, tag, channel, name);
+
+ stream = pw_map_lookup(&client->streams, channel);
+ if (stream == NULL || stream->type == STREAM_TYPE_UPLOAD)
+ return -ENOENT;
+
+ items[0] = SPA_DICT_ITEM_INIT(PW_KEY_MEDIA_NAME, name);
+ pw_stream_update_properties(stream->stream,
+ &SPA_DICT_INIT(items, 1));
+
+ return reply_simple_ack(client, tag);
+}
+
+static int do_update_proplist(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ uint32_t channel, mode;
+ struct stream *stream;
+ struct pw_properties *props;
+ int res;
+
+ props = pw_properties_new(NULL, NULL);
+ if (props == NULL)
+ return -errno;
+
+ if (command != COMMAND_UPDATE_CLIENT_PROPLIST) {
+ if (message_get(m,
+ TAG_U32, &channel,
+ TAG_INVALID) < 0)
+ goto error_protocol;
+ } else {
+ channel = SPA_ID_INVALID;
+ }
+
+ pw_log_info("[%s] %s tag:%u channel:%d",
+ client->name, commands[command].name, tag, channel);
+
+ if (message_get(m,
+ TAG_U32, &mode,
+ TAG_PROPLIST, props,
+ TAG_INVALID) < 0)
+ goto error_protocol;
+
+ if (command != COMMAND_UPDATE_CLIENT_PROPLIST) {
+ stream = pw_map_lookup(&client->streams, channel);
+ if (stream == NULL || stream->type == STREAM_TYPE_UPLOAD)
+ goto error_noentity;
+
+ pw_stream_update_properties(stream->stream, &props->dict);
+ } else {
+ if (pw_properties_update(client->props, &props->dict) > 0) {
+ client_update_quirks(client);
+ client->name = pw_properties_get(client->props, PW_KEY_APP_NAME);
+ pw_core_update_properties(client->core, &client->props->dict);
+ }
+ }
+ res = reply_simple_ack(client, tag);
+exit:
+ pw_properties_free(props);
+ return res;
+
+error_protocol:
+ res = -EPROTO;
+ goto exit;
+error_noentity:
+ res = -ENOENT;
+ goto exit;
+}
+
+static int do_remove_proplist(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ uint32_t i, channel;
+ struct stream *stream;
+ struct pw_properties *props;
+ struct spa_dict dict;
+ struct spa_dict_item *items;
+ int res;
+
+ props = pw_properties_new(NULL, NULL);
+ if (props == NULL)
+ return -errno;
+
+ if (command != COMMAND_REMOVE_CLIENT_PROPLIST) {
+ if (message_get(m,
+ TAG_U32, &channel,
+ TAG_INVALID) < 0)
+ goto error_protocol;
+ } else {
+ channel = SPA_ID_INVALID;
+ }
+
+ pw_log_info("[%s] %s tag:%u channel:%d",
+ client->name, commands[command].name, tag, channel);
+
+ while (true) {
+ const char *key;
+
+ if (message_get(m,
+ TAG_STRING, &key,
+ TAG_INVALID) < 0)
+ goto error_protocol;
+ if (key == NULL)
+ break;
+ pw_properties_set(props, key, key);
+ }
+
+ dict.n_items = props->dict.n_items;
+ dict.items = items = alloca(sizeof(struct spa_dict_item) * dict.n_items);
+ for (i = 0; i < dict.n_items; i++) {
+ items[i].key = props->dict.items[i].key;
+ items[i].value = NULL;
+ }
+
+ if (command != COMMAND_UPDATE_CLIENT_PROPLIST) {
+ stream = pw_map_lookup(&client->streams, channel);
+ if (stream == NULL || stream->type == STREAM_TYPE_UPLOAD)
+ goto error_noentity;
+
+ pw_stream_update_properties(stream->stream, &dict);
+ } else {
+ pw_core_update_properties(client->core, &dict);
+ }
+ res = reply_simple_ack(client, tag);
+exit:
+ pw_properties_free(props);
+ return res;
+
+error_protocol:
+ res = -EPROTO;
+ goto exit;
+error_noentity:
+ res = -ENOENT;
+ goto exit;
+}
+
+
+static int do_get_server_info(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ struct impl *impl = client->impl;
+ struct pw_manager *manager = client->manager;
+ struct pw_core_info *info = manager ? manager->info : NULL;
+ char name[256];
+ struct message *reply;
+
+ pw_log_info("[%s] GET_SERVER_INFO tag:%u", client->name, tag);
+
+ snprintf(name, sizeof(name), "PulseAudio (on PipeWire %s)", pw_get_library_version());
+
+ reply = reply_new(client, tag);
+ message_put(reply,
+ TAG_STRING, name,
+ TAG_STRING, "15.0.0",
+ TAG_STRING, pw_get_user_name(),
+ TAG_STRING, pw_get_host_name(),
+ TAG_SAMPLE_SPEC, &impl->defs.sample_spec,
+ TAG_STRING, manager ? get_default(client, true) : "", /* default sink name */
+ TAG_STRING, manager ? get_default(client, false) : "", /* default source name */
+ TAG_U32, info ? info->cookie : 0, /* cookie */
+ TAG_INVALID);
+
+ if (client->version >= 15) {
+ message_put(reply,
+ TAG_CHANNEL_MAP, &impl->defs.channel_map,
+ TAG_INVALID);
+ }
+ return client_queue_message(client, reply);
+}
+
+static int do_stat(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ struct impl *impl = client->impl;
+ struct message *reply;
+
+ pw_log_info("[%s] STAT tag:%u", client->name, tag);
+
+ reply = reply_new(client, tag);
+ message_put(reply,
+ TAG_U32, impl->stat.n_allocated, /* n_allocated */
+ TAG_U32, impl->stat.allocated, /* allocated size */
+ TAG_U32, impl->stat.n_accumulated, /* n_accumulated */
+ TAG_U32, impl->stat.accumulated, /* accumulated_size */
+ TAG_U32, impl->stat.sample_cache, /* sample cache size */
+ TAG_INVALID);
+
+ return client_queue_message(client, reply);
+}
+
+static int do_lookup(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ struct message *reply;
+ struct pw_manager_object *o;
+ const char *name;
+ bool is_sink = command == COMMAND_LOOKUP_SINK;
+ bool is_monitor;
+
+ if (message_get(m,
+ TAG_STRING, &name,
+ TAG_INVALID) < 0)
+ return -EPROTO;
+
+ pw_log_info("[%s] LOOKUP tag:%u name:'%s'", client->name, tag, name);
+
+ if ((o = find_device(client, SPA_ID_INVALID, name, is_sink, &is_monitor)) == NULL)
+ return -ENOENT;
+
+ reply = reply_new(client, tag);
+ message_put(reply,
+ TAG_U32, o->index,
+ TAG_INVALID);
+
+ return client_queue_message(client, reply);
+}
+
+static int do_drain_stream(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ uint32_t channel;
+ struct stream *stream;
+
+ if (message_get(m,
+ TAG_U32, &channel,
+ TAG_INVALID) < 0)
+ return -EPROTO;
+
+ pw_log_info("[%s] DRAIN tag:%u channel:%d", client->name, tag, channel);
+ stream = pw_map_lookup(&client->streams, channel);
+ if (stream == NULL || stream->type != STREAM_TYPE_PLAYBACK)
+ return -ENOENT;
+
+ stream->drain_tag = tag;
+ stream->draining = true;
+ stream_set_paused(stream, false, "drain start");
+
+ return 0;
+}
+
+static int fill_client_info(struct client *client, struct message *m,
+ struct pw_manager_object *o)
+{
+ struct pw_client_info *info = o->info;
+ struct pw_manager *manager = client->manager;
+ const char *str;
+ uint32_t module_id = SPA_ID_INVALID;
+
+ if (!pw_manager_object_is_client(o) || info == NULL || info->props == NULL)
+ return -ENOENT;
+
+ if ((str = spa_dict_lookup(info->props, PW_KEY_MODULE_ID)) != NULL)
+ module_id = (uint32_t)atoi(str);
+
+ message_put(m,
+ TAG_U32, o->index, /* client index */
+ TAG_STRING, pw_properties_get(o->props, PW_KEY_APP_NAME),
+ TAG_U32, id_to_index(manager, module_id), /* module index */
+ TAG_STRING, "PipeWire", /* driver */
+ TAG_INVALID);
+ if (client->version >= 13) {
+ message_put(m,
+ TAG_PROPLIST, info->props,
+ TAG_INVALID);
+ }
+ return 0;
+}
+
+static int fill_module_info(struct client *client, struct message *m,
+ struct pw_manager_object *o)
+{
+ struct pw_module_info *info = o->info;
+
+ if (!pw_manager_object_is_module(o) || info == NULL || info->props == NULL)
+ return -ENOENT;
+
+ message_put(m,
+ TAG_U32, o->index, /* module index */
+ TAG_STRING, info->name,
+ TAG_STRING, info->args,
+ TAG_U32, -1, /* n_used */
+ TAG_INVALID);
+
+ if (client->version < 15) {
+ message_put(m,
+ TAG_BOOLEAN, false, /* auto unload deprecated */
+ TAG_INVALID);
+ }
+ if (client->version >= 15) {
+ message_put(m,
+ TAG_PROPLIST, info->props,
+ TAG_INVALID);
+ }
+ return 0;
+}
+
+static int fill_ext_module_info(struct client *client, struct message *m,
+ struct module *module)
+{
+ message_put(m,
+ TAG_U32, module->index, /* module index */
+ TAG_STRING, module->info->name,
+ TAG_STRING, module->args,
+ TAG_U32, -1, /* n_used */
+ TAG_INVALID);
+
+ if (client->version < 15) {
+ message_put(m,
+ TAG_BOOLEAN, false, /* auto unload deprecated */
+ TAG_INVALID);
+ }
+ if (client->version >= 15) {
+ message_put(m,
+ TAG_PROPLIST, module->info->properties,
+ TAG_INVALID);
+ }
+ return 0;
+}
+
+static int64_t get_port_latency_offset(struct client *client, struct pw_manager_object *card, struct port_info *pi)
+{
+ struct pw_manager *m = client->manager;
+ struct pw_manager_object *o;
+ size_t j;
+
+ /*
+ * The latency offset is a property of nodes in PipeWire, so we look it up on the
+ * nodes. We'll return the latency offset of the first node in the port.
+ *
+ * This is also because we need to be consistent with
+ * send_latency_offset_subscribe_event, which sends events on node changes. The
+ * route data might not be updated yet when these events arrive.
+ */
+ for (j = 0; j < pi->n_devices; ++j) {
+ spa_list_for_each(o, &m->object_list, link) {
+ const char *str;
+ uint32_t card_id = SPA_ID_INVALID;
+ uint32_t device_id = SPA_ID_INVALID;
+ struct pw_node_info *info;
+
+ if (o->creating || o->removing)
+ continue;
+ if (!pw_manager_object_is_sink(o) && !pw_manager_object_is_source_or_monitor(o))
+ continue;
+ if ((info = o->info) == NULL || info->props == NULL)
+ continue;
+ if ((str = spa_dict_lookup(info->props, PW_KEY_DEVICE_ID)) != NULL)
+ card_id = (uint32_t)atoi(str);
+ if (card_id != card->id)
+ continue;
+
+ if ((str = spa_dict_lookup(info->props, "card.profile.device")) != NULL)
+ device_id = (uint32_t)atoi(str);
+
+ if (device_id == pi->devices[j])
+ return get_node_latency_offset(o);
+ }
+ }
+
+ return 0LL;
+}
+
+static int fill_card_info(struct client *client, struct message *m,
+ struct pw_manager_object *o)
+{
+ struct pw_manager *manager = client->manager;
+ struct pw_device_info *info = o->info;
+ const char *str, *drv_name;
+ uint32_t module_id = SPA_ID_INVALID, n_profiles, n;
+ struct card_info card_info = CARD_INFO_INIT;
+ struct profile_info *profile_info;
+
+ if (!pw_manager_object_is_card(o) || info == NULL || info->props == NULL)
+ return -ENOENT;
+
+ if ((str = spa_dict_lookup(info->props, PW_KEY_MODULE_ID)) != NULL)
+ module_id = (uint32_t)atoi(str);
+
+ drv_name = spa_dict_lookup(info->props, PW_KEY_DEVICE_API);
+ if (drv_name && spa_streq("bluez5", drv_name))
+ drv_name = "module-bluez5-device.c"; /* blueman needs this */
+
+ message_put(m,
+ TAG_U32, o->index, /* card index */
+ TAG_STRING, spa_dict_lookup(info->props, PW_KEY_DEVICE_NAME),
+ TAG_U32, id_to_index(manager, module_id),
+ TAG_STRING, drv_name,
+ TAG_INVALID);
+
+ collect_card_info(o, &card_info);
+
+ message_put(m,
+ TAG_U32, card_info.n_profiles, /* n_profiles */
+ TAG_INVALID);
+
+ profile_info = alloca(card_info.n_profiles * sizeof(*profile_info));
+ n_profiles = collect_profile_info(o, &card_info, profile_info);
+
+ for (n = 0; n < n_profiles; n++) {
+ struct profile_info *pi = &profile_info[n];
+
+ message_put(m,
+ TAG_STRING, pi->name, /* profile name */
+ TAG_STRING, pi->description, /* profile description */
+ TAG_U32, pi->n_sinks, /* n_sinks */
+ TAG_U32, pi->n_sources, /* n_sources */
+ TAG_U32, pi->priority, /* priority */
+ TAG_INVALID);
+
+ if (client->version >= 29) {
+ message_put(m,
+ TAG_U32, pi->available != SPA_PARAM_AVAILABILITY_no, /* available */
+ TAG_INVALID);
+ }
+ }
+ message_put(m,
+ TAG_STRING, card_info.active_profile_name, /* active profile name */
+ TAG_PROPLIST, info->props,
+ TAG_INVALID);
+
+ if (client->version >= 26) {
+ uint32_t n_ports;
+ struct port_info *port_info, *pi;
+
+ port_info = alloca(card_info.n_ports * sizeof(*port_info));
+ card_info.active_profile = SPA_ID_INVALID;
+ n_ports = collect_port_info(o, &card_info, NULL, port_info);
+
+ message_put(m,
+ TAG_U32, n_ports, /* n_ports */
+ TAG_INVALID);
+
+ for (n = 0; n < n_ports; n++) {
+ struct spa_dict_item *items;
+ struct spa_dict *pdict = NULL, dict;
+ uint32_t i, pi_n_profiles;
+
+ pi = &port_info[n];
+
+ if (pi->info && pi->n_props > 0) {
+ items = alloca(pi->n_props * sizeof(*items));
+ dict.items = items;
+ pdict = collect_props(pi->info, &dict);
+ }
+
+ message_put(m,
+ TAG_STRING, pi->name, /* port name */
+ TAG_STRING, pi->description, /* port description */
+ TAG_U32, pi->priority, /* port priority */
+ TAG_U32, pi->available, /* port available */
+ TAG_U8, pi->direction == SPA_DIRECTION_INPUT ? 2 : 1, /* port direction */
+ TAG_PROPLIST, pdict, /* port proplist */
+ TAG_INVALID);
+
+ pi_n_profiles = SPA_MIN(pi->n_profiles, n_profiles);
+ if (pi->n_profiles != pi_n_profiles) {
+ /* libpulse assumes port profile array size <= n_profiles */
+ pw_log_error("%p: card %d port %d profiles inconsistent (%d < %d)",
+ client->impl, o->id, n, n_profiles, pi->n_profiles);
+ }
+
+ message_put(m,
+ TAG_U32, pi_n_profiles, /* n_profiles */
+ TAG_INVALID);
+
+ for (i = 0; i < pi_n_profiles; i++) {
+ uint32_t j;
+ const char *name = "off";
+
+ for (j = 0; j < n_profiles; ++j) {
+ if (profile_info[j].index == pi->profiles[i]) {
+ name = profile_info[j].name;
+ break;
+ }
+ }
+
+ message_put(m,
+ TAG_STRING, name, /* profile name */
+ TAG_INVALID);
+ }
+ if (client->version >= 27) {
+ int64_t latency_offset = get_port_latency_offset(client, o, pi);
+ message_put(m,
+ TAG_S64, latency_offset / 1000, /* port latency offset */
+ TAG_INVALID);
+ }
+ if (client->version >= 34) {
+ message_put(m,
+ TAG_STRING, pi->availability_group, /* available group */
+ TAG_U32, pi->type, /* port type */
+ TAG_INVALID);
+ }
+ }
+ }
+ return 0;
+}
+
+static int fill_sink_info_proplist(struct message *m, const struct spa_dict *sink_props,
+ const struct pw_manager_object *card)
+{
+ struct pw_device_info *card_info = card ? card->info : NULL;
+ struct pw_properties *props = NULL;
+
+ if (card_info && card_info->props) {
+ props = pw_properties_new_dict(sink_props);
+ if (props == NULL)
+ return -ENOMEM;
+
+ pw_properties_add(props, card_info->props);
+ sink_props = &props->dict;
+ }
+ message_put(m, TAG_PROPLIST, sink_props, TAG_INVALID);
+
+ pw_properties_free(props);
+
+ return 0;
+}
+
+static bool validate_device_info(struct device_info *dev_info)
+{
+ return sample_spec_valid(&dev_info->ss) &&
+ channel_map_valid(&dev_info->map) &&
+ volume_valid(&dev_info->volume_info.volume);
+}
+
+static int fill_sink_info(struct client *client, struct message *m,
+ struct pw_manager_object *o)
+{
+ struct impl *impl = client->impl;
+ struct pw_node_info *info = o->info;
+ struct pw_manager *manager = client->manager;
+ const char *name, *desc, *str;
+ char *monitor_name = NULL;
+ uint32_t module_id = SPA_ID_INVALID;
+ uint32_t card_id = SPA_ID_INVALID;
+ struct pw_manager_object *card = NULL;
+ uint32_t flags;
+ struct card_info card_info = CARD_INFO_INIT;
+ struct device_info dev_info = DEVICE_INFO_INIT(PW_DIRECTION_OUTPUT);
+ size_t size;
+
+ if (!pw_manager_object_is_sink(o) || info == NULL || info->props == NULL)
+ return -ENOENT;
+
+ name = spa_dict_lookup(info->props, PW_KEY_NODE_NAME);
+ if ((desc = spa_dict_lookup(info->props, PW_KEY_NODE_DESCRIPTION)) == NULL)
+ desc = name ? name : "Unknown";
+ if (name == NULL)
+ name = "unknown";
+
+ size = strlen(name) + 10;
+ monitor_name = alloca(size);
+ if (pw_manager_object_is_source(o))
+ snprintf(monitor_name, size, "%s", name);
+ else
+ snprintf(monitor_name, size, "%s.monitor", name);
+
+ if ((str = spa_dict_lookup(info->props, PW_KEY_MODULE_ID)) != NULL)
+ module_id = id_to_index(manager, (uint32_t)atoi(str));
+ if (module_id == SPA_ID_INVALID &&
+ (str = spa_dict_lookup(info->props, "pulse.module.id")) != NULL)
+ module_id = (uint32_t)atoi(str);
+ if ((str = spa_dict_lookup(info->props, PW_KEY_DEVICE_ID)) != NULL)
+ card_id = (uint32_t)atoi(str);
+ if ((str = spa_dict_lookup(info->props, "card.profile.device")) != NULL)
+ dev_info.device = (uint32_t)atoi(str);
+ if (card_id != SPA_ID_INVALID) {
+ struct selector sel = { .id = card_id, .type = pw_manager_object_is_card, };
+ card = select_object(manager, &sel);
+ }
+ if (card)
+ collect_card_info(card, &card_info);
+
+ collect_device_info(o, card, &dev_info, false, &impl->defs);
+
+ if (!validate_device_info(&dev_info)) {
+ pw_log_warn("%d: sink not ready: sample:%d map:%d volume:%d",
+ o->id, sample_spec_valid(&dev_info.ss),
+ channel_map_valid(&dev_info.map),
+ volume_valid(&dev_info.volume_info.volume));
+ return -ENOENT;
+ }
+
+ flags = SINK_LATENCY | SINK_DYNAMIC_LATENCY | SINK_DECIBEL_VOLUME;
+ if ((str = spa_dict_lookup(info->props, PW_KEY_DEVICE_API)) != NULL)
+ flags |= SINK_HARDWARE;
+ if ((str = spa_dict_lookup(info->props, PW_KEY_NODE_NETWORK)) != NULL)
+ flags |= SINK_NETWORK;
+ if (SPA_FLAG_IS_SET(dev_info.volume_info.flags, VOLUME_HW_VOLUME))
+ flags |= SINK_HW_VOLUME_CTRL;
+ if (SPA_FLAG_IS_SET(dev_info.volume_info.flags, VOLUME_HW_MUTE))
+ flags |= SINK_HW_MUTE_CTRL;
+ if (dev_info.have_iec958codecs)
+ flags |= SINK_SET_FORMATS;
+
+ if (client->quirks & QUIRK_FORCE_S16_FORMAT)
+ dev_info.ss.format = SPA_AUDIO_FORMAT_S16;
+
+ message_put(m,
+ TAG_U32, o->index, /* sink index */
+ TAG_STRING, name,
+ TAG_STRING, desc,
+ TAG_SAMPLE_SPEC, &dev_info.ss,
+ TAG_CHANNEL_MAP, &dev_info.map,
+ TAG_U32, module_id, /* module index */
+ TAG_CVOLUME, &dev_info.volume_info.volume,
+ TAG_BOOLEAN, dev_info.volume_info.mute,
+ TAG_U32, o->index, /* monitor source index */
+ TAG_STRING, monitor_name, /* monitor source name */
+ TAG_USEC, 0LL, /* latency */
+ TAG_STRING, "PipeWire", /* driver */
+ TAG_U32, flags, /* flags */
+ TAG_INVALID);
+
+ if (client->version >= 13) {
+ int res;
+ if ((res = fill_sink_info_proplist(m, info->props, card)) < 0)
+ return res;
+ message_put(m,
+ TAG_USEC, 0LL, /* requested latency */
+ TAG_INVALID);
+ }
+ if (client->version >= 15) {
+ bool is_linked = collect_is_linked(manager, o->id, SPA_DIRECTION_INPUT);
+ int state = node_state(info->state);
+
+ /* running with nothing linked is probably the monitor that is
+ * keeping this sink busy */
+ if (state == STATE_RUNNING && !is_linked)
+ state = STATE_IDLE;
+
+ message_put(m,
+ TAG_VOLUME, dev_info.volume_info.base, /* base volume */
+ TAG_U32, state, /* state */
+ TAG_U32, dev_info.volume_info.steps, /* n_volume_steps */
+ TAG_U32, card ? card->index : SPA_ID_INVALID, /* card index */
+ TAG_INVALID);
+ }
+ if (client->version >= 16) {
+ uint32_t n_ports, n;
+ struct port_info *port_info, *pi;
+
+ port_info = alloca(card_info.n_ports * sizeof(*port_info));
+ n_ports = collect_port_info(card, &card_info, &dev_info, port_info);
+
+ message_put(m,
+ TAG_U32, n_ports, /* n_ports */
+ TAG_INVALID);
+ for (n = 0; n < n_ports; n++) {
+ pi = &port_info[n];
+ message_put(m,
+ TAG_STRING, pi->name, /* name */
+ TAG_STRING, pi->description, /* description */
+ TAG_U32, pi->priority, /* priority */
+ TAG_INVALID);
+ if (client->version >= 24) {
+ message_put(m,
+ TAG_U32, pi->available, /* available */
+ TAG_INVALID);
+ }
+ if (client->version >= 34) {
+ message_put(m,
+ TAG_STRING, pi->availability_group, /* availability_group */
+ TAG_U32, pi->type, /* type */
+ TAG_INVALID);
+ }
+ }
+ message_put(m,
+ TAG_STRING, dev_info.active_port_name, /* active port name */
+ TAG_INVALID);
+ }
+ if (client->version >= 21) {
+ struct pw_manager_param *p;
+ struct format_info info[32];
+ uint32_t i, n_info = 0;
+
+ spa_list_for_each(p, &o->param_list, link) {
+ uint32_t index = 0;
+
+ if (p->id != SPA_PARAM_EnumFormat)
+ continue;
+
+ while (n_info < SPA_N_ELEMENTS(info)) {
+ spa_zero(info[n_info]);
+ if (format_info_from_param(&info[n_info], p->param, index++) < 0)
+ break;
+ if (info[n_info].encoding == ENCODING_ANY ||
+ (info[n_info].encoding == ENCODING_PCM && info[n_info].props != NULL)) {
+ format_info_clear(&info[n_info]);
+ continue;
+ }
+ n_info++;
+ }
+ }
+ message_put(m,
+ TAG_U8, n_info, /* n_formats */
+ TAG_INVALID);
+ for (i = 0; i < n_info; i++) {
+ message_put(m,
+ TAG_FORMAT_INFO, &info[i],
+ TAG_INVALID);
+ format_info_clear(&info[i]);
+ }
+ }
+ return 0;
+}
+
+static int fill_source_info_proplist(struct message *m, const struct spa_dict *source_props,
+ const struct pw_manager_object *card, const bool is_monitor)
+{
+ struct pw_device_info *card_info = card ? card->info : NULL;
+ struct pw_properties *props = NULL;
+
+ if ((card_info && card_info->props) || is_monitor) {
+ props = pw_properties_new_dict(source_props);
+ if (props == NULL)
+ return -ENOMEM;
+
+ if (card_info && card_info->props)
+ pw_properties_add(props, card_info->props);
+
+ if (is_monitor)
+ pw_properties_set(props, PW_KEY_DEVICE_CLASS, "monitor");
+
+ source_props = &props->dict;
+ }
+ message_put(m, TAG_PROPLIST, source_props, TAG_INVALID);
+
+ pw_properties_free(props);
+
+ return 0;
+}
+
+static int fill_source_info(struct client *client, struct message *m,
+ struct pw_manager_object *o)
+{
+ struct impl *impl = client->impl;
+ struct pw_node_info *info = o->info;
+ struct pw_manager *manager = client->manager;
+ bool is_monitor;
+ const char *name, *desc, *str;
+ char *monitor_name = NULL;
+ char *monitor_desc = NULL;
+ uint32_t module_id = SPA_ID_INVALID;
+ uint32_t card_id = SPA_ID_INVALID;
+ struct pw_manager_object *card = NULL;
+ uint32_t flags;
+ struct card_info card_info = CARD_INFO_INIT;
+ struct device_info dev_info = DEVICE_INFO_INIT(PW_DIRECTION_INPUT);
+ size_t size;
+
+ is_monitor = pw_manager_object_is_monitor(o);
+ if ((!pw_manager_object_is_source(o) && !is_monitor) || info == NULL || info->props == NULL)
+ return -ENOENT;
+
+ name = spa_dict_lookup(info->props, PW_KEY_NODE_NAME);
+ if ((desc = spa_dict_lookup(info->props, PW_KEY_NODE_DESCRIPTION)) == NULL)
+ desc = name ? name : "Unknown";
+ if (name == NULL)
+ name = "unknown";
+
+ size = strlen(name) + 10;
+ monitor_name = alloca(size);
+ snprintf(monitor_name, size, "%s.monitor", name);
+
+ size = strlen(desc) + 20;
+ monitor_desc = alloca(size);
+ snprintf(monitor_desc, size, "Monitor of %s", desc);
+
+ if ((str = spa_dict_lookup(info->props, PW_KEY_MODULE_ID)) != NULL)
+ module_id = id_to_index(manager, (uint32_t)atoi(str));
+ if (module_id == SPA_ID_INVALID &&
+ (str = spa_dict_lookup(info->props, "pulse.module.id")) != NULL)
+ module_id = (uint32_t)atoi(str);
+ if ((str = spa_dict_lookup(info->props, PW_KEY_DEVICE_ID)) != NULL)
+ card_id = (uint32_t)atoi(str);
+ if ((str = spa_dict_lookup(info->props, "card.profile.device")) != NULL)
+ dev_info.device = (uint32_t)atoi(str);
+
+ if (card_id != SPA_ID_INVALID) {
+ struct selector sel = { .id = card_id, .type = pw_manager_object_is_card, };
+ card = select_object(manager, &sel);
+ }
+ if (card)
+ collect_card_info(card, &card_info);
+
+ collect_device_info(o, card, &dev_info, is_monitor, &impl->defs);
+
+ if (!validate_device_info(&dev_info)) {
+ pw_log_warn("%d: source not ready: sample:%d map:%d volume:%d",
+ o->id, sample_spec_valid(&dev_info.ss),
+ channel_map_valid(&dev_info.map),
+ volume_valid(&dev_info.volume_info.volume));
+ return -ENOENT;
+ }
+
+ flags = SOURCE_LATENCY | SOURCE_DYNAMIC_LATENCY | SOURCE_DECIBEL_VOLUME;
+ if ((str = spa_dict_lookup(info->props, PW_KEY_DEVICE_API)) != NULL)
+ flags |= SOURCE_HARDWARE;
+ if ((str = spa_dict_lookup(info->props, PW_KEY_NODE_NETWORK)) != NULL)
+ flags |= SOURCE_NETWORK;
+ if (SPA_FLAG_IS_SET(dev_info.volume_info.flags, VOLUME_HW_VOLUME))
+ flags |= SOURCE_HW_VOLUME_CTRL;
+ if (SPA_FLAG_IS_SET(dev_info.volume_info.flags, VOLUME_HW_MUTE))
+ flags |= SOURCE_HW_MUTE_CTRL;
+
+ if (client->quirks & QUIRK_FORCE_S16_FORMAT)
+ dev_info.ss.format = SPA_AUDIO_FORMAT_S16;
+
+ message_put(m,
+ TAG_U32, o->index, /* source index */
+ TAG_STRING, is_monitor ? monitor_name : name,
+ TAG_STRING, is_monitor ? monitor_desc : desc,
+ TAG_SAMPLE_SPEC, &dev_info.ss,
+ TAG_CHANNEL_MAP, &dev_info.map,
+ TAG_U32, module_id, /* module index */
+ TAG_CVOLUME, &dev_info.volume_info.volume,
+ TAG_BOOLEAN, dev_info.volume_info.mute,
+ TAG_U32, is_monitor ? o->index : SPA_ID_INVALID,/* monitor of sink */
+ TAG_STRING, is_monitor ? name : NULL, /* monitor of sink name */
+ TAG_USEC, 0LL, /* latency */
+ TAG_STRING, "PipeWire", /* driver */
+ TAG_U32, flags, /* flags */
+ TAG_INVALID);
+
+ if (client->version >= 13) {
+ int res;
+ if ((res = fill_source_info_proplist(m, info->props, card, is_monitor)) < 0)
+ return res;
+ message_put(m,
+ TAG_USEC, 0LL, /* requested latency */
+ TAG_INVALID);
+ }
+ if (client->version >= 15) {
+ bool is_linked = collect_is_linked(manager, o->id, SPA_DIRECTION_OUTPUT);
+ int state = node_state(info->state);
+
+ /* running with nothing linked is probably the sink that is
+ * keeping this source busy */
+ if (state == STATE_RUNNING && !is_linked)
+ state = STATE_IDLE;
+
+ message_put(m,
+ TAG_VOLUME, dev_info.volume_info.base, /* base volume */
+ TAG_U32, state, /* state */
+ TAG_U32, dev_info.volume_info.steps, /* n_volume_steps */
+ TAG_U32, card ? card->index : SPA_ID_INVALID, /* card index */
+ TAG_INVALID);
+ }
+ if (client->version >= 16) {
+ uint32_t n_ports, n;
+ struct port_info *port_info, *pi;
+
+ port_info = alloca(card_info.n_ports * sizeof(*port_info));
+ n_ports = collect_port_info(card, &card_info, &dev_info, port_info);
+
+ message_put(m,
+ TAG_U32, n_ports, /* n_ports */
+ TAG_INVALID);
+ for (n = 0; n < n_ports; n++) {
+ pi = &port_info[n];
+ message_put(m,
+ TAG_STRING, pi->name, /* name */
+ TAG_STRING, pi->description, /* description */
+ TAG_U32, pi->priority, /* priority */
+ TAG_INVALID);
+ if (client->version >= 24) {
+ message_put(m,
+ TAG_U32, pi->available, /* available */
+ TAG_INVALID);
+ }
+ if (client->version >= 34) {
+ message_put(m,
+ TAG_STRING, pi->availability_group, /* availability_group */
+ TAG_U32, pi->type, /* type */
+ TAG_INVALID);
+ }
+ }
+ message_put(m,
+ TAG_STRING, dev_info.active_port_name, /* active port name */
+ TAG_INVALID);
+ }
+ if (client->version >= 21) {
+ struct format_info info;
+ spa_zero(info);
+ info.encoding = ENCODING_PCM;
+ message_put(m,
+ TAG_U8, 1, /* n_formats */
+ TAG_FORMAT_INFO, &info,
+ TAG_INVALID);
+ }
+ return 0;
+}
+
+static const char *get_media_name(struct pw_node_info *info)
+{
+ const char *media_name;
+ media_name = spa_dict_lookup(info->props, PW_KEY_MEDIA_NAME);
+ if (media_name == NULL)
+ media_name = "";
+ return media_name;
+}
+
+static int fill_sink_input_info(struct client *client, struct message *m,
+ struct pw_manager_object *o)
+{
+ struct impl *impl = client->impl;
+ struct pw_node_info *info = o->info;
+ struct pw_manager *manager = client->manager;
+ const char *str;
+ uint32_t module_id = SPA_ID_INVALID, client_id = SPA_ID_INVALID;
+ uint32_t peer_index;
+ struct device_info dev_info = DEVICE_INFO_INIT(PW_DIRECTION_OUTPUT);
+
+ if (!pw_manager_object_is_sink_input(o) || info == NULL || info->props == NULL)
+ return -ENOENT;
+
+ if ((str = spa_dict_lookup(info->props, PW_KEY_MODULE_ID)) != NULL)
+ module_id = id_to_index(manager, (uint32_t)atoi(str));
+ if (module_id == SPA_ID_INVALID &&
+ (str = spa_dict_lookup(info->props, "pulse.module.id")) != NULL)
+ module_id = (uint32_t)atoi(str);
+
+ if (!pw_manager_object_is_virtual(o) &&
+ (str = spa_dict_lookup(info->props, PW_KEY_CLIENT_ID)) != NULL)
+ client_id = (uint32_t)atoi(str);
+
+ collect_device_info(o, NULL, &dev_info, false, &impl->defs);
+
+ if (!validate_device_info(&dev_info))
+ return -ENOENT;
+
+ peer_index = get_temporary_move_target(client, o);
+ if (peer_index == SPA_ID_INVALID) {
+ struct pw_manager_object *peer;
+ peer = find_linked(manager, o->id, PW_DIRECTION_OUTPUT);
+ if (peer && pw_manager_object_is_sink(peer))
+ peer_index = peer->index;
+ else
+ peer_index = SPA_ID_INVALID;
+ }
+
+ message_put(m,
+ TAG_U32, o->index, /* sink_input index */
+ TAG_STRING, get_media_name(info),
+ TAG_U32, module_id, /* module index */
+ TAG_U32, id_to_index(manager, client_id), /* client index */
+ TAG_U32, peer_index, /* sink index */
+ TAG_SAMPLE_SPEC, &dev_info.ss,
+ TAG_CHANNEL_MAP, &dev_info.map,
+ TAG_CVOLUME, &dev_info.volume_info.volume,
+ TAG_USEC, 0LL, /* latency */
+ TAG_USEC, 0LL, /* sink latency */
+ TAG_STRING, "PipeWire", /* resample method */
+ TAG_STRING, "PipeWire", /* driver */
+ TAG_INVALID);
+ if (client->version >= 11)
+ message_put(m,
+ TAG_BOOLEAN, dev_info.volume_info.mute, /* muted */
+ TAG_INVALID);
+ if (client->version >= 13)
+ message_put(m,
+ TAG_PROPLIST, info->props,
+ TAG_INVALID);
+ if (client->version >= 19)
+ message_put(m,
+ TAG_BOOLEAN, info->state != PW_NODE_STATE_RUNNING, /* corked */
+ TAG_INVALID);
+ if (client->version >= 20)
+ message_put(m,
+ TAG_BOOLEAN, true, /* has_volume */
+ TAG_BOOLEAN, true, /* volume writable */
+ TAG_INVALID);
+ if (client->version >= 21) {
+ struct format_info fi;
+ format_info_from_spec(&fi, &dev_info.ss, &dev_info.map);
+ message_put(m,
+ TAG_FORMAT_INFO, &fi,
+ TAG_INVALID);
+ format_info_clear(&fi);
+ }
+ return 0;
+}
+
+static int fill_source_output_info(struct client *client, struct message *m,
+ struct pw_manager_object *o)
+{
+ struct impl *impl = client->impl;
+ struct pw_node_info *info = o->info;
+ struct pw_manager *manager = client->manager;
+ const char *str;
+ uint32_t module_id = SPA_ID_INVALID, client_id = SPA_ID_INVALID;
+ uint32_t peer_index;
+ struct device_info dev_info = DEVICE_INFO_INIT(PW_DIRECTION_INPUT);
+
+ if (!pw_manager_object_is_source_output(o) || info == NULL || info->props == NULL)
+ return -ENOENT;
+
+ if ((str = spa_dict_lookup(info->props, PW_KEY_MODULE_ID)) != NULL)
+ module_id = id_to_index(manager, (uint32_t)atoi(str));
+ if (module_id == SPA_ID_INVALID &&
+ (str = spa_dict_lookup(info->props, "pulse.module.id")) != NULL)
+ module_id = (uint32_t)atoi(str);
+
+ if (!pw_manager_object_is_virtual(o) &&
+ (str = spa_dict_lookup(info->props, PW_KEY_CLIENT_ID)) != NULL)
+ client_id = (uint32_t)atoi(str);
+
+ collect_device_info(o, NULL, &dev_info, false, &impl->defs);
+
+ if (!validate_device_info(&dev_info))
+ return -ENOENT;
+
+ peer_index = get_temporary_move_target(client, o);
+ if (peer_index == SPA_ID_INVALID) {
+ struct pw_manager_object *peer;
+ peer = find_linked(manager, o->id, PW_DIRECTION_INPUT);
+ if (peer && pw_manager_object_is_source_or_monitor(peer))
+ peer_index = peer->index;
+ else
+ peer_index = SPA_ID_INVALID;
+ }
+
+ message_put(m,
+ TAG_U32, o->index, /* source_output index */
+ TAG_STRING, get_media_name(info),
+ TAG_U32, module_id, /* module index */
+ TAG_U32, id_to_index(manager, client_id), /* client index */
+ TAG_U32, peer_index, /* source index */
+ TAG_SAMPLE_SPEC, &dev_info.ss,
+ TAG_CHANNEL_MAP, &dev_info.map,
+ TAG_USEC, 0LL, /* latency */
+ TAG_USEC, 0LL, /* source latency */
+ TAG_STRING, "PipeWire", /* resample method */
+ TAG_STRING, "PipeWire", /* driver */
+ TAG_INVALID);
+ if (client->version >= 13)
+ message_put(m,
+ TAG_PROPLIST, info->props,
+ TAG_INVALID);
+ if (client->version >= 19)
+ message_put(m,
+ TAG_BOOLEAN, info->state != PW_NODE_STATE_RUNNING, /* corked */
+ TAG_INVALID);
+ if (client->version >= 22) {
+ struct format_info fi;
+ format_info_from_spec(&fi, &dev_info.ss, &dev_info.map);
+ message_put(m,
+ TAG_CVOLUME, &dev_info.volume_info.volume,
+ TAG_BOOLEAN, dev_info.volume_info.mute, /* muted */
+ TAG_BOOLEAN, true, /* has_volume */
+ TAG_BOOLEAN, true, /* volume writable */
+ TAG_FORMAT_INFO, &fi,
+ TAG_INVALID);
+ format_info_clear(&fi);
+ }
+ return 0;
+}
+
+static int do_get_info(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ struct impl *impl = client->impl;
+ struct pw_manager *manager = client->manager;
+ struct message *reply = NULL;
+ int res;
+ struct pw_manager_object *o;
+ struct selector sel;
+ int (*fill_func) (struct client *client, struct message *m, struct pw_manager_object *o) = NULL;
+
+ spa_zero(sel);
+
+ if (message_get(m,
+ TAG_U32, &sel.index,
+ TAG_INVALID) < 0)
+ goto error_protocol;
+
+ reply = reply_new(client, tag);
+
+ if (command == COMMAND_GET_MODULE_INFO && (sel.index & MODULE_FLAG) != 0) {
+ struct module *module;
+ module = pw_map_lookup(&impl->modules, sel.index & MODULE_INDEX_MASK);
+ if (module == NULL)
+ goto error_noentity;
+ fill_ext_module_info(client, reply, module);
+ return client_queue_message(client, reply);
+ }
+
+ switch (command) {
+ case COMMAND_GET_CLIENT_INFO:
+ sel.type = pw_manager_object_is_client;
+ fill_func = fill_client_info;
+ break;
+ case COMMAND_GET_MODULE_INFO:
+ sel.type = pw_manager_object_is_module;
+ fill_func = fill_module_info;
+ break;
+ case COMMAND_GET_CARD_INFO:
+ sel.type = pw_manager_object_is_card;
+ sel.key = PW_KEY_DEVICE_NAME;
+ fill_func = fill_card_info;
+ break;
+ case COMMAND_GET_SINK_INFO:
+ sel.type = pw_manager_object_is_sink;
+ sel.key = PW_KEY_NODE_NAME;
+ fill_func = fill_sink_info;
+ break;
+ case COMMAND_GET_SOURCE_INFO:
+ sel.type = pw_manager_object_is_source_or_monitor;
+ sel.key = PW_KEY_NODE_NAME;
+ fill_func = fill_source_info;
+ break;
+ case COMMAND_GET_SINK_INPUT_INFO:
+ sel.type = pw_manager_object_is_sink_input;
+ fill_func = fill_sink_input_info;
+ break;
+ case COMMAND_GET_SOURCE_OUTPUT_INFO:
+ sel.type = pw_manager_object_is_source_output;
+ fill_func = fill_source_output_info;
+ break;
+ }
+ if (sel.key) {
+ if (message_get(m,
+ TAG_STRING, &sel.value,
+ TAG_INVALID) < 0)
+ goto error_protocol;
+ }
+ if (fill_func == NULL)
+ goto error_invalid;
+
+ if (sel.index != SPA_ID_INVALID && sel.value != NULL)
+ goto error_invalid;
+
+ pw_log_info("[%s] %s tag:%u index:%u name:%s", client->name,
+ commands[command].name, tag, sel.index, sel.value);
+
+ if (command == COMMAND_GET_SINK_INFO || command == COMMAND_GET_SOURCE_INFO) {
+ o = find_device(client, sel.index, sel.value,
+ command == COMMAND_GET_SINK_INFO, NULL);
+ } else {
+ if (sel.value == NULL && sel.index == SPA_ID_INVALID)
+ goto error_invalid;
+ o = select_object(manager, &sel);
+ }
+ if (o == NULL)
+ goto error_noentity;
+
+ if ((res = fill_func(client, reply, o)) < 0)
+ goto error;
+
+ return client_queue_message(client, reply);
+
+error_protocol:
+ res = -EPROTO;
+ goto error;
+error_noentity:
+ res = -ENOENT;
+ goto error;
+error_invalid:
+ res = -EINVAL;
+ goto error;
+error:
+ if (reply)
+ message_free(reply, false, false);
+ return res;
+}
+
+static uint64_t bytes_to_usec(uint64_t length, const struct sample_spec *ss)
+{
+ uint64_t u;
+ uint64_t frame_size = sample_spec_frame_size(ss);
+ if (frame_size == 0)
+ return 0;
+ u = length / frame_size;
+ u *= SPA_USEC_PER_SEC;
+ u /= ss->rate;
+ return u;
+}
+
+static int fill_sample_info(struct client *client, struct message *m,
+ struct sample *sample)
+{
+ struct volume vol;
+
+ volume_make(&vol, sample->ss.channels);
+
+ message_put(m,
+ TAG_U32, sample->index,
+ TAG_STRING, sample->name,
+ TAG_CVOLUME, &vol,
+ TAG_USEC, bytes_to_usec(sample->length, &sample->ss),
+ TAG_SAMPLE_SPEC, &sample->ss,
+ TAG_CHANNEL_MAP, &sample->map,
+ TAG_U32, sample->length,
+ TAG_BOOLEAN, false, /* lazy */
+ TAG_STRING, NULL, /* filename */
+ TAG_INVALID);
+
+ if (client->version >= 13) {
+ message_put(m,
+ TAG_PROPLIST, &sample->props->dict,
+ TAG_INVALID);
+ }
+ return 0;
+}
+
+static int do_get_sample_info(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ struct impl *impl = client->impl;
+ struct message *reply = NULL;
+ uint32_t index;
+ const char *name;
+ struct sample *sample;
+ int res;
+
+ if (message_get(m,
+ TAG_U32, &index,
+ TAG_STRING, &name,
+ TAG_INVALID) < 0)
+ return -EPROTO;
+
+ if ((index == SPA_ID_INVALID && name == NULL) ||
+ (index != SPA_ID_INVALID && name != NULL))
+ return -EINVAL;
+
+ pw_log_info("[%s] %s tag:%u index:%u name:%s", client->name,
+ commands[command].name, tag, index, name);
+
+ if ((sample = find_sample(impl, index, name)) == NULL)
+ return -ENOENT;
+
+ reply = reply_new(client, tag);
+ if ((res = fill_sample_info(client, reply, sample)) < 0)
+ goto error;
+
+ return client_queue_message(client, reply);
+
+error:
+ if (reply)
+ message_free(reply, false, false);
+ return res;
+}
+
+static int do_get_sample_info_list(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ struct impl *impl = client->impl;
+ struct message *reply;
+ union pw_map_item *item;
+
+ pw_log_info("[%s] %s tag:%u", client->name,
+ commands[command].name, tag);
+
+ reply = reply_new(client, tag);
+ pw_array_for_each(item, &impl->samples.items) {
+ struct sample *s = item->data;
+ if (pw_map_item_is_free(item))
+ continue;
+ fill_sample_info(client, reply, s);
+ }
+ return client_queue_message(client, reply);
+}
+
+struct info_list_data {
+ struct client *client;
+ struct message *reply;
+ int (*fill_func) (struct client *client, struct message *m, struct pw_manager_object *o);
+};
+
+static int do_list_info(void *data, struct pw_manager_object *object)
+{
+ struct info_list_data *info = data;
+ info->fill_func(info->client, info->reply, object);
+ return 0;
+}
+
+static int do_info_list_module(void *item, void *data)
+{
+ struct module *m = item;
+ struct info_list_data *info = data;
+ fill_ext_module_info(info->client, info->reply, m);
+ return 0;
+}
+
+static int do_get_info_list(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ struct impl *impl = client->impl;
+ struct pw_manager *manager = client->manager;
+ struct info_list_data info;
+
+ pw_log_info("[%s] %s tag:%u", client->name,
+ commands[command].name, tag);
+
+ spa_zero(info);
+ info.client = client;
+
+ switch (command) {
+ case COMMAND_GET_CLIENT_INFO_LIST:
+ info.fill_func = fill_client_info;
+ break;
+ case COMMAND_GET_MODULE_INFO_LIST:
+ info.fill_func = fill_module_info;
+ break;
+ case COMMAND_GET_CARD_INFO_LIST:
+ info.fill_func = fill_card_info;
+ break;
+ case COMMAND_GET_SINK_INFO_LIST:
+ info.fill_func = fill_sink_info;
+ break;
+ case COMMAND_GET_SOURCE_INFO_LIST:
+ info.fill_func = fill_source_info;
+ break;
+ case COMMAND_GET_SINK_INPUT_INFO_LIST:
+ info.fill_func = fill_sink_input_info;
+ break;
+ case COMMAND_GET_SOURCE_OUTPUT_INFO_LIST:
+ info.fill_func = fill_source_output_info;
+ break;
+ default:
+ return -ENOTSUP;
+ }
+
+ info.reply = reply_new(client, tag);
+ if (info.fill_func)
+ pw_manager_for_each_object(manager, do_list_info, &info);
+
+ if (command == COMMAND_GET_MODULE_INFO_LIST)
+ pw_map_for_each(&impl->modules, do_info_list_module, &info);
+
+ return client_queue_message(client, info.reply);
+}
+
+static int do_set_stream_buffer_attr(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ uint32_t channel;
+ struct stream *stream;
+ struct message *reply;
+ struct buffer_attr attr;
+ bool adjust_latency = false, early_requests = false;
+
+ if (message_get(m,
+ TAG_U32, &channel,
+ TAG_INVALID) < 0)
+ return -EPROTO;
+
+ pw_log_info("[%s] %s tag:%u channel:%u", client->name,
+ commands[command].name, tag, channel);
+
+ stream = pw_map_lookup(&client->streams, channel);
+ if (stream == NULL)
+ return -ENOENT;
+
+ if (command == COMMAND_SET_PLAYBACK_STREAM_BUFFER_ATTR) {
+ if (stream->type != STREAM_TYPE_PLAYBACK)
+ return -ENOENT;
+
+ if (message_get(m,
+ TAG_U32, &attr.maxlength,
+ TAG_U32, &attr.tlength,
+ TAG_U32, &attr.prebuf,
+ TAG_U32, &attr.minreq,
+ TAG_INVALID) < 0)
+ return -EPROTO;
+ } else {
+ if (stream->type != STREAM_TYPE_RECORD)
+ return -ENOENT;
+
+ if (message_get(m,
+ TAG_U32, &attr.maxlength,
+ TAG_U32, &attr.fragsize,
+ TAG_INVALID) < 0)
+ return -EPROTO;
+ }
+ if (client->version >= 13) {
+ if (message_get(m,
+ TAG_BOOLEAN, &adjust_latency,
+ TAG_INVALID) < 0)
+ return -EPROTO;
+ }
+ if (client->version >= 14) {
+ if (message_get(m,
+ TAG_BOOLEAN, &early_requests,
+ TAG_INVALID) < 0)
+ return -EPROTO;
+ }
+
+ reply = reply_new(client, tag);
+
+ stream->adjust_latency = adjust_latency;
+ stream->early_requests = early_requests;
+
+ if (command == COMMAND_SET_PLAYBACK_STREAM_BUFFER_ATTR) {
+ stream->lat_usec = set_playback_buffer_attr(stream, &attr);
+
+ message_put(reply,
+ TAG_U32, stream->attr.maxlength,
+ TAG_U32, stream->attr.tlength,
+ TAG_U32, stream->attr.prebuf,
+ TAG_U32, stream->attr.minreq,
+ TAG_INVALID);
+ if (client->version >= 13) {
+ message_put(reply,
+ TAG_USEC, stream->lat_usec, /* configured_sink_latency */
+ TAG_INVALID);
+ }
+ } else {
+ stream->lat_usec = set_record_buffer_attr(stream, &attr);
+
+ message_put(reply,
+ TAG_U32, stream->attr.maxlength,
+ TAG_U32, stream->attr.fragsize,
+ TAG_INVALID);
+ if (client->version >= 13) {
+ message_put(reply,
+ TAG_USEC, stream->lat_usec, /* configured_source_latency */
+ TAG_INVALID);
+ }
+ }
+ return client_queue_message(client, reply);
+}
+
+static int do_update_stream_sample_rate(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ uint32_t channel, rate;
+ struct stream *stream;
+ float corr;
+
+ if (message_get(m,
+ TAG_U32, &channel,
+ TAG_U32, &rate,
+ TAG_INVALID) < 0)
+ return -EPROTO;
+
+ pw_log_info("[%s] %s tag:%u channel:%u rate:%u", client->name,
+ commands[command].name, tag, channel, rate);
+
+ stream = pw_map_lookup(&client->streams, channel);
+ if (stream == NULL || stream->type == STREAM_TYPE_UPLOAD)
+ return -ENOENT;
+
+ stream->rate = rate;
+
+ corr = (double)rate/(double)stream->ss.rate;
+ pw_stream_set_control(stream->stream, SPA_PROP_rate, 1, &corr, NULL);
+
+ return reply_simple_ack(client, tag);
+}
+
+static int do_extension(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ uint32_t index;
+ const char *name;
+ const struct extension *ext;
+
+ if (message_get(m,
+ TAG_U32, &index,
+ TAG_STRING, &name,
+ TAG_INVALID) < 0)
+ return -EPROTO;
+
+ pw_log_info("[%s] %s tag:%u index:%u name:%s", client->name,
+ commands[command].name, tag, index, name);
+
+ if ((index == SPA_ID_INVALID && name == NULL) ||
+ (index != SPA_ID_INVALID && name != NULL))
+ return -EINVAL;
+
+ ext = extension_find(index, name);
+ if (ext == NULL)
+ return -ENOENT;
+
+ return ext->process(client, tag, m);
+}
+
+static int do_set_profile(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ struct pw_manager *manager = client->manager;
+ struct pw_manager_object *o;
+ const char *profile_name;
+ uint32_t profile_index = SPA_ID_INVALID;
+ struct selector sel;
+ char buf[1024];
+ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buf, sizeof(buf));
+
+ spa_zero(sel);
+ sel.key = PW_KEY_DEVICE_NAME;
+ sel.type = pw_manager_object_is_card;
+
+ if (message_get(m,
+ TAG_U32, &sel.index,
+ TAG_STRING, &sel.value,
+ TAG_STRING, &profile_name,
+ TAG_INVALID) < 0)
+ return -EPROTO;
+
+ pw_log_info("[%s] %s tag:%u index:%u name:%s profile:%s", client->name,
+ commands[command].name, tag, sel.index, sel.value, profile_name);
+
+ if ((sel.index == SPA_ID_INVALID && sel.value == NULL) ||
+ (sel.index != SPA_ID_INVALID && sel.value != NULL))
+ return -EINVAL;
+ if (profile_name == NULL)
+ return -EINVAL;
+
+ if ((o = select_object(manager, &sel)) == NULL)
+ return -ENOENT;
+
+ if ((profile_index = find_profile_index(o, profile_name)) == SPA_ID_INVALID)
+ return -ENOENT;
+
+ if (!SPA_FLAG_IS_SET(o->permissions, PW_PERM_W | PW_PERM_X))
+ return -EACCES;
+
+ if (o->proxy == NULL)
+ return -ENOENT;
+
+ pw_device_set_param((struct pw_device*)o->proxy,
+ SPA_PARAM_Profile, 0,
+ spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamProfile, SPA_PARAM_Profile,
+ SPA_PARAM_PROFILE_index, SPA_POD_Int(profile_index),
+ SPA_PARAM_PROFILE_save, SPA_POD_Bool(true)));
+
+ return operation_new(client, tag);
+}
+
+static int do_set_default(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ struct pw_manager *manager = client->manager;
+ struct pw_manager_object *o;
+ const char *name, *str;
+ int res;
+ bool sink = command == COMMAND_SET_DEFAULT_SINK;
+
+ if (message_get(m,
+ TAG_STRING, &name,
+ TAG_INVALID) < 0)
+ return -EPROTO;
+
+ pw_log_info("[%s] %s tag:%u name:%s", client->name,
+ commands[command].name, tag, name);
+
+ if (name != NULL && (o = find_device(client, SPA_ID_INVALID, name, sink, NULL)) == NULL)
+ return -ENOENT;
+
+ if (name != NULL) {
+ if (o->props && (str = pw_properties_get(o->props, PW_KEY_NODE_NAME)) != NULL)
+ name = str;
+ else if (spa_strendswith(name, ".monitor"))
+ name = strndupa(name, strlen(name)-8);
+
+ res = pw_manager_set_metadata(manager, client->metadata_default,
+ PW_ID_CORE,
+ sink ? METADATA_CONFIG_DEFAULT_SINK : METADATA_CONFIG_DEFAULT_SOURCE,
+ "Spa:String:JSON", "{ \"name\": \"%s\" }", name);
+ } else {
+ res = pw_manager_set_metadata(manager, client->metadata_default,
+ PW_ID_CORE,
+ sink ? METADATA_CONFIG_DEFAULT_SINK : METADATA_CONFIG_DEFAULT_SOURCE,
+ NULL, NULL);
+ }
+ if (res < 0)
+ return res;
+
+ /*
+ * The metadata is not necessarily updated within one server sync.
+ * Correct functioning of MOVE_* commands requires knowing the current
+ * default target, so we need to stash temporary values here in case
+ * the client emits them before metadata gets updated.
+ */
+ if (sink) {
+ free(client->temporary_default_sink);
+ client->temporary_default_sink = name ? strdup(name) : NULL;
+ } else {
+ free(client->temporary_default_source);
+ client->temporary_default_source = name ? strdup(name) : NULL;
+ }
+
+ return operation_new(client, tag);
+}
+
+static int do_suspend(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ struct pw_manager_object *o;
+ const char *name;
+ uint32_t index, cmd;
+ bool sink = command == COMMAND_SUSPEND_SINK, suspend;
+
+ if (message_get(m,
+ TAG_U32, &index,
+ TAG_STRING, &name,
+ TAG_BOOLEAN, &suspend,
+ TAG_INVALID) < 0)
+ return -EPROTO;
+
+ pw_log_info("[%s] %s tag:%u index:%u name:%s", client->name,
+ commands[command].name, tag, index, name);
+
+ if ((o = find_device(client, index, name, sink, NULL)) == NULL)
+ return -ENOENT;
+
+ if (o->proxy == NULL)
+ return -ENOENT;
+
+ if (suspend) {
+ cmd = SPA_NODE_COMMAND_Suspend;
+ pw_node_send_command((struct pw_node*)o->proxy, &SPA_NODE_COMMAND_INIT(cmd));
+ }
+ return operation_new(client, tag);
+}
+
+static int do_move_stream(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ struct pw_manager *manager = client->manager;
+ struct pw_manager_object *o, *dev, *dev_default;
+ uint32_t index, index_device;
+ int target_id;
+ int64_t target_serial;
+ const char *name_device;
+ const char *name;
+ struct pw_node_info *info;
+ struct selector sel;
+ int res;
+ bool sink = command == COMMAND_MOVE_SINK_INPUT;
+
+ if (message_get(m,
+ TAG_U32, &index,
+ TAG_U32, &index_device,
+ TAG_STRING, &name_device,
+ TAG_INVALID) < 0)
+ return -EPROTO;
+
+ if ((index_device == SPA_ID_INVALID && name_device == NULL) ||
+ (index_device != SPA_ID_INVALID && name_device != NULL))
+ return -EINVAL;
+
+ pw_log_info("[%s] %s tag:%u index:%u device:%d name:%s", client->name,
+ commands[command].name, tag, index, index_device, name_device);
+
+ spa_zero(sel);
+ sel.index = index;
+ sel.type = sink ? pw_manager_object_is_sink_input: pw_manager_object_is_source_output;
+
+ o = select_object(manager, &sel);
+ if (o == NULL)
+ return -ENOENT;
+
+ info = o->info;
+ if (info == NULL || info->props == NULL)
+ return -EINVAL;
+ if (spa_atob(spa_dict_lookup(info->props, PW_KEY_NODE_DONT_RECONNECT)))
+ return -EINVAL;
+
+ if ((dev = find_device(client, index_device, name_device, sink, NULL)) == NULL)
+ return -ENOENT;
+
+ /*
+ * The client metadata is not necessarily yet updated after SET_DEFAULT command,
+ * so use the temporary values if they are still set.
+ */
+ name = sink ? client->temporary_default_sink : client->temporary_default_source;
+ dev_default = find_device(client, SPA_ID_INVALID, name, sink, NULL);
+
+ if (dev == dev_default) {
+ /*
+ * When moving streams to a node that is equal to the default,
+ * Pulseaudio understands this to mean '... and unset preferred sink/source',
+ * forgetting target.node. Follow that behavior here.
+ */
+ target_id = -1;
+ target_serial = -1;
+ } else {
+ target_id = dev->id;
+ target_serial = dev->serial;
+ }
+
+ if ((res = pw_manager_set_metadata(manager, client->metadata_default,
+ o->id,
+ METADATA_TARGET_NODE,
+ SPA_TYPE_INFO_BASE"Id", "%d", target_id)) < 0)
+ return res;
+
+ if ((res = pw_manager_set_metadata(manager, client->metadata_default,
+ o->id,
+ METADATA_TARGET_OBJECT,
+ SPA_TYPE_INFO_BASE"Id", "%"PRIi64, target_serial)) < 0)
+ return res;
+
+ name = spa_dict_lookup(info->props, PW_KEY_NODE_NAME);
+ pw_log_debug("[%s] %s done tag:%u index:%u name:%s target:%d target-serial:%"PRIi64, client->name,
+ commands[command].name, tag, index, name ? name : "<null>",
+ target_id, target_serial);
+
+ /* We will temporarily claim the stream was already moved */
+ set_temporary_move_target(client, o, dev->index);
+ send_object_event(client, o, SUBSCRIPTION_EVENT_CHANGE);
+
+ return reply_simple_ack(client, tag);
+}
+
+static int do_kill(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ struct pw_manager *manager = client->manager;
+ struct pw_manager_object *o;
+ uint32_t index;
+ struct selector sel;
+
+ if (message_get(m,
+ TAG_U32, &index,
+ TAG_INVALID) < 0)
+ return -EPROTO;
+
+ pw_log_info("[%s] %s tag:%u index:%u", client->name,
+ commands[command].name, tag, index);
+
+ spa_zero(sel);
+ sel.index = index;
+ switch (command) {
+ case COMMAND_KILL_CLIENT:
+ sel.type = pw_manager_object_is_client;
+ break;
+ case COMMAND_KILL_SINK_INPUT:
+ sel.type = pw_manager_object_is_sink_input;
+ break;
+ case COMMAND_KILL_SOURCE_OUTPUT:
+ sel.type = pw_manager_object_is_source_output;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ if ((o = select_object(manager, &sel)) == NULL)
+ return -ENOENT;
+
+ pw_registry_destroy(manager->registry, o->id);
+
+ return reply_simple_ack(client, tag);
+}
+
+static void handle_module_loaded(struct module *module, struct client *client, uint32_t tag, int result)
+{
+ const char *client_name = client != NULL ? client->name : "?";
+ struct impl *impl = module->impl;
+
+ spa_assert(!SPA_RESULT_IS_ASYNC(result));
+
+ if (SPA_RESULT_IS_OK(result)) {
+ pw_log_info("[%s] loaded module index:%u name:%s tag:%d",
+ client_name, module->index, module->info->name, tag);
+
+ module->loaded = true;
+
+ broadcast_subscribe_event(impl,
+ SUBSCRIPTION_MASK_MODULE,
+ SUBSCRIPTION_EVENT_NEW | SUBSCRIPTION_EVENT_MODULE,
+ module->index);
+
+ if (client != NULL) {
+ struct message *reply = reply_new(client, tag);
+
+ message_put(reply,
+ TAG_U32, module->index,
+ TAG_INVALID);
+ client_queue_message(client, reply);
+ }
+ }
+ else {
+ pw_log_warn("%p: [%s] failed to load module index:%u name:%s tag:%d result:%d (%s)",
+ impl, client_name,
+ module->index, module->info->name, tag,
+ result, spa_strerror(result));
+
+ module_schedule_unload(module);
+
+ if (client != NULL)
+ reply_error(client, COMMAND_LOAD_MODULE, tag, result);
+ }
+}
+
+struct pending_module {
+ struct client *client;
+ struct spa_hook client_listener;
+
+ struct module *module;
+ struct spa_hook module_listener;
+
+ struct spa_hook manager_listener;
+
+ uint32_t tag;
+
+ int result;
+ bool wait_sync;
+};
+
+static void finish_pending_module(struct pending_module *pm)
+{
+ spa_hook_remove(&pm->module_listener);
+
+ if (pm->client != NULL) {
+ spa_hook_remove(&pm->client_listener);
+ spa_hook_remove(&pm->manager_listener);
+ }
+
+ handle_module_loaded(pm->module, pm->client, pm->tag, pm->result);
+ free(pm);
+}
+
+static void on_load_module_manager_sync(void *data)
+{
+ struct pending_module *pm = data;
+
+ pw_log_debug("pending module %p: manager sync wait_sync:%d tag:%d",
+ pm, pm->wait_sync, pm->tag);
+
+ if (!pm->wait_sync)
+ return;
+
+ finish_pending_module(pm);
+}
+
+static void on_module_loaded(void *data, int result)
+{
+ struct pending_module *pm = data;
+
+ pw_log_debug("pending module %p: loaded, result:%d tag:%d",
+ pm, result, pm->tag);
+
+ pm->result = result;
+
+ /*
+ * Do manager sync first: the module may have its own core, so
+ * although things are completed on the server, our client
+ * might not yet see them.
+ */
+
+ if (pm->client == NULL) {
+ finish_pending_module(pm);
+ } else {
+ pw_log_debug("pending module %p: wait manager sync tag:%d", pm, pm->tag);
+ pm->wait_sync = true;
+ pw_manager_sync(pm->client->manager);
+ }
+}
+
+static void on_module_destroy(void *data)
+{
+ struct pending_module *pm = data;
+
+ pw_log_debug("pending module %p: destroyed, tag:%d",
+ pm, pm->tag);
+
+ pm->result = -ECANCELED;
+ finish_pending_module(pm);
+}
+
+static void on_client_disconnect(void *data)
+{
+ struct pending_module *pm = data;
+
+ pw_log_debug("pending module %p: client disconnect tag:%d", pm, pm->tag);
+
+ spa_hook_remove(&pm->client_listener);
+ spa_hook_remove(&pm->manager_listener);
+ pm->client = NULL;
+
+ if (pm->wait_sync)
+ finish_pending_module(pm);
+}
+
+static void on_load_module_manager_disconnect(void *data)
+{
+ on_client_disconnect(data);
+}
+
+static int do_load_module(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ static const struct module_events module_events = {
+ VERSION_MODULE_EVENTS,
+ .loaded = on_module_loaded,
+ .destroy = on_module_destroy,
+ };
+ static const struct client_events client_events = {
+ VERSION_CLIENT_EVENTS,
+ .disconnect = on_client_disconnect,
+ };
+ static const struct pw_manager_events manager_events = {
+ PW_VERSION_MANAGER_EVENTS,
+ .disconnect = on_load_module_manager_disconnect,
+ .sync = on_load_module_manager_sync,
+ };
+
+ struct impl *impl = client->impl;
+ const char *name, *argument;
+ struct module *module;
+ struct pending_module *pm;
+ int r;
+
+ if (message_get(m,
+ TAG_STRING, &name,
+ TAG_STRING, &argument,
+ TAG_INVALID) < 0)
+ return -EPROTO;
+
+ pw_log_info("[%s] %s name:%s argument:%s",
+ client->name, commands[command].name, name, argument);
+
+ module = module_create(impl, name, argument);
+ if (module == NULL)
+ return -errno;
+
+ pm = calloc(1, sizeof(*pm));
+ if (pm == NULL)
+ return -errno;
+
+ pm->tag = tag;
+ pm->client = client;
+ pm->module = module;
+
+ pw_log_debug("pending module %p: start tag:%d", pm, tag);
+
+ r = module_load(module);
+
+ module_add_listener(module, &pm->module_listener, &module_events, pm);
+ client_add_listener(client, &pm->client_listener, &client_events, pm);
+ pw_manager_add_listener(client->manager, &pm->manager_listener, &manager_events, pm);
+
+ if (!SPA_RESULT_IS_ASYNC(r))
+ on_module_loaded(pm, r);
+
+ /*
+ * return 0 to prevent `handle_packet()` from sending a reply
+ * because we want `handle_module_loaded()` to send the reply
+ */
+ return 0;
+}
+
+static int do_unload_module(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ struct impl *impl = client->impl;
+ struct module *module;
+ uint32_t module_index;
+
+ if (message_get(m,
+ TAG_U32, &module_index,
+ TAG_INVALID) < 0)
+ return -EPROTO;
+
+ pw_log_info("[%s] %s tag:%u index:%u", client->name,
+ commands[command].name, tag, module_index);
+
+ if (module_index == SPA_ID_INVALID)
+ return -EINVAL;
+ if ((module_index & MODULE_FLAG) == 0)
+ return -EPERM;
+
+ module = pw_map_lookup(&impl->modules, module_index & MODULE_INDEX_MASK);
+ if (module == NULL)
+ return -ENOENT;
+
+ module_unload(module);
+
+ return operation_new(client, tag);
+}
+
+static int do_send_object_message(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ struct impl *impl = client->impl;
+ struct pw_manager *manager = client->manager;
+ const char *object_path = NULL;
+ const char *message = NULL;
+ const char *params = NULL;
+ char *response = NULL;
+ char *path = NULL;
+ struct message *reply;
+ struct pw_manager_object *o;
+ int len = 0;
+ int res;
+
+ if (message_get(m,
+ TAG_STRING, &object_path,
+ TAG_STRING, &message,
+ TAG_STRING, &params,
+ TAG_INVALID) < 0)
+ return -EPROTO;
+
+ pw_log_info("[%s] %s tag:%u object_path:'%s' message:'%s' params:'%s'",
+ client->name, commands[command].name, tag, object_path,
+ message, params ? params : "<null>");
+
+ if (object_path == NULL || message == NULL)
+ return -EINVAL;
+
+ len = strlen(object_path);
+ if (len > 0 && object_path[len - 1] == '/')
+ --len;
+ path = strndup(object_path, len);
+ if (path == NULL)
+ return -ENOMEM;
+
+ res = -ENOENT;
+
+ spa_list_for_each(o, &manager->object_list, link) {
+ if (o->message_object_path && spa_streq(o->message_object_path, path)) {
+ if (o->message_handler)
+ res = o->message_handler(manager, o, message, params, &response);
+ else
+ res = -ENOSYS;
+ break;
+ }
+ }
+
+ free(path);
+ if (res < 0)
+ return res;
+
+ pw_log_debug("%p: object message response:'%s'", impl, response ? response : "<null>");
+
+ reply = reply_new(client, tag);
+ message_put(reply, TAG_STRING, response, TAG_INVALID);
+ free(response);
+ return client_queue_message(client, reply);
+}
+
+static int do_error_access(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ return -EACCES;
+}
+
+static SPA_UNUSED int do_error_not_implemented(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ return -ENOSYS;
+}
+
+#define COMMAND(name, ...) [COMMAND_ ## name] = { #name, __VA_ARGS__ }
+const struct command commands[COMMAND_MAX] =
+{
+ COMMAND(ERROR),
+ COMMAND(TIMEOUT), /* pseudo command */
+ COMMAND(REPLY),
+
+ /* CLIENT->SERVER */
+ COMMAND(CREATE_PLAYBACK_STREAM, do_create_playback_stream),
+ COMMAND(DELETE_PLAYBACK_STREAM, do_delete_stream),
+ COMMAND(CREATE_RECORD_STREAM, do_create_record_stream),
+ COMMAND(DELETE_RECORD_STREAM, do_delete_stream),
+ COMMAND(EXIT, do_error_access),
+ COMMAND(AUTH, do_command_auth, COMMAND_ACCESS_WITHOUT_AUTH | COMMAND_ACCESS_WITHOUT_MANAGER),
+ COMMAND(SET_CLIENT_NAME, do_set_client_name, COMMAND_ACCESS_WITHOUT_MANAGER),
+ COMMAND(LOOKUP_SINK, do_lookup),
+ COMMAND(LOOKUP_SOURCE, do_lookup),
+ COMMAND(DRAIN_PLAYBACK_STREAM, do_drain_stream),
+ COMMAND(STAT, do_stat, COMMAND_ACCESS_WITHOUT_MANAGER),
+ COMMAND(GET_PLAYBACK_LATENCY, do_get_playback_latency),
+ COMMAND(CREATE_UPLOAD_STREAM, do_create_upload_stream),
+ COMMAND(DELETE_UPLOAD_STREAM, do_delete_stream),
+ COMMAND(FINISH_UPLOAD_STREAM, do_finish_upload_stream),
+ COMMAND(PLAY_SAMPLE, do_play_sample),
+ COMMAND(REMOVE_SAMPLE, do_remove_sample),
+
+ COMMAND(GET_SERVER_INFO, do_get_server_info, COMMAND_ACCESS_WITHOUT_MANAGER),
+ COMMAND(GET_SINK_INFO, do_get_info),
+ COMMAND(GET_SOURCE_INFO, do_get_info),
+ COMMAND(GET_MODULE_INFO, do_get_info),
+ COMMAND(GET_CLIENT_INFO, do_get_info),
+ COMMAND(GET_SINK_INPUT_INFO, do_get_info),
+ COMMAND(GET_SOURCE_OUTPUT_INFO, do_get_info),
+ COMMAND(GET_SAMPLE_INFO, do_get_sample_info),
+ COMMAND(GET_CARD_INFO, do_get_info),
+ COMMAND(SUBSCRIBE, do_subscribe),
+
+ COMMAND(GET_SINK_INFO_LIST, do_get_info_list),
+ COMMAND(GET_SOURCE_INFO_LIST, do_get_info_list),
+ COMMAND(GET_MODULE_INFO_LIST, do_get_info_list),
+ COMMAND(GET_CLIENT_INFO_LIST, do_get_info_list),
+ COMMAND(GET_SINK_INPUT_INFO_LIST, do_get_info_list),
+ COMMAND(GET_SOURCE_OUTPUT_INFO_LIST, do_get_info_list),
+ COMMAND(GET_SAMPLE_INFO_LIST, do_get_sample_info_list),
+ COMMAND(GET_CARD_INFO_LIST, do_get_info_list),
+
+ COMMAND(SET_SINK_VOLUME, do_set_volume),
+ COMMAND(SET_SINK_INPUT_VOLUME, do_set_stream_volume),
+ COMMAND(SET_SOURCE_VOLUME, do_set_volume),
+
+ COMMAND(SET_SINK_MUTE, do_set_mute),
+ COMMAND(SET_SOURCE_MUTE, do_set_mute),
+
+ COMMAND(CORK_PLAYBACK_STREAM, do_cork_stream),
+ COMMAND(FLUSH_PLAYBACK_STREAM, do_flush_trigger_prebuf_stream),
+ COMMAND(TRIGGER_PLAYBACK_STREAM, do_flush_trigger_prebuf_stream),
+ COMMAND(PREBUF_PLAYBACK_STREAM, do_flush_trigger_prebuf_stream),
+
+ COMMAND(SET_DEFAULT_SINK, do_set_default),
+ COMMAND(SET_DEFAULT_SOURCE, do_set_default),
+
+ COMMAND(SET_PLAYBACK_STREAM_NAME, do_set_stream_name),
+ COMMAND(SET_RECORD_STREAM_NAME, do_set_stream_name),
+
+ COMMAND(KILL_CLIENT, do_kill),
+ COMMAND(KILL_SINK_INPUT, do_kill),
+ COMMAND(KILL_SOURCE_OUTPUT, do_kill),
+
+ COMMAND(LOAD_MODULE, do_load_module),
+ COMMAND(UNLOAD_MODULE, do_unload_module),
+
+ /* Obsolete */
+ COMMAND(ADD_AUTOLOAD___OBSOLETE, do_error_access),
+ COMMAND(REMOVE_AUTOLOAD___OBSOLETE, do_error_access),
+ COMMAND(GET_AUTOLOAD_INFO___OBSOLETE, do_error_access),
+ COMMAND(GET_AUTOLOAD_INFO_LIST___OBSOLETE, do_error_access),
+
+ COMMAND(GET_RECORD_LATENCY, do_get_record_latency),
+ COMMAND(CORK_RECORD_STREAM, do_cork_stream),
+ COMMAND(FLUSH_RECORD_STREAM, do_flush_trigger_prebuf_stream),
+
+ /* SERVER->CLIENT */
+ COMMAND(REQUEST),
+ COMMAND(OVERFLOW),
+ COMMAND(UNDERFLOW),
+ COMMAND(PLAYBACK_STREAM_KILLED),
+ COMMAND(RECORD_STREAM_KILLED),
+ COMMAND(SUBSCRIBE_EVENT),
+
+ /* A few more client->server commands */
+
+ /* Supported since protocol v10 (0.9.5) */
+ COMMAND(MOVE_SINK_INPUT, do_move_stream),
+ COMMAND(MOVE_SOURCE_OUTPUT, do_move_stream),
+
+ /* Supported since protocol v11 (0.9.7) */
+ COMMAND(SET_SINK_INPUT_MUTE, do_set_stream_mute),
+
+ COMMAND(SUSPEND_SINK, do_suspend),
+ COMMAND(SUSPEND_SOURCE, do_suspend),
+
+ /* Supported since protocol v12 (0.9.8) */
+ COMMAND(SET_PLAYBACK_STREAM_BUFFER_ATTR, do_set_stream_buffer_attr),
+ COMMAND(SET_RECORD_STREAM_BUFFER_ATTR, do_set_stream_buffer_attr),
+
+ COMMAND(UPDATE_PLAYBACK_STREAM_SAMPLE_RATE, do_update_stream_sample_rate),
+ COMMAND(UPDATE_RECORD_STREAM_SAMPLE_RATE, do_update_stream_sample_rate),
+
+ /* SERVER->CLIENT */
+ COMMAND(PLAYBACK_STREAM_SUSPENDED),
+ COMMAND(RECORD_STREAM_SUSPENDED),
+ COMMAND(PLAYBACK_STREAM_MOVED),
+ COMMAND(RECORD_STREAM_MOVED),
+
+ /* Supported since protocol v13 (0.9.11) */
+ COMMAND(UPDATE_RECORD_STREAM_PROPLIST, do_update_proplist),
+ COMMAND(UPDATE_PLAYBACK_STREAM_PROPLIST, do_update_proplist),
+ COMMAND(UPDATE_CLIENT_PROPLIST, do_update_proplist),
+
+ COMMAND(REMOVE_RECORD_STREAM_PROPLIST, do_remove_proplist),
+ COMMAND(REMOVE_PLAYBACK_STREAM_PROPLIST, do_remove_proplist),
+ COMMAND(REMOVE_CLIENT_PROPLIST, do_remove_proplist),
+
+ /* SERVER->CLIENT */
+ COMMAND(STARTED),
+
+ /* Supported since protocol v14 (0.9.12) */
+ COMMAND(EXTENSION, do_extension),
+ /* Supported since protocol v15 (0.9.15) */
+ COMMAND(SET_CARD_PROFILE, do_set_profile),
+
+ /* SERVER->CLIENT */
+ COMMAND(CLIENT_EVENT),
+ COMMAND(PLAYBACK_STREAM_EVENT),
+ COMMAND(RECORD_STREAM_EVENT),
+
+ /* SERVER->CLIENT */
+ COMMAND(PLAYBACK_BUFFER_ATTR_CHANGED),
+ COMMAND(RECORD_BUFFER_ATTR_CHANGED),
+
+ /* Supported since protocol v16 (0.9.16) */
+ COMMAND(SET_SINK_PORT, do_set_port),
+ COMMAND(SET_SOURCE_PORT, do_set_port),
+
+ /* Supported since protocol v22 (1.0) */
+ COMMAND(SET_SOURCE_OUTPUT_VOLUME, do_set_stream_volume),
+ COMMAND(SET_SOURCE_OUTPUT_MUTE, do_set_stream_mute),
+
+ /* Supported since protocol v27 (3.0) */
+ COMMAND(SET_PORT_LATENCY_OFFSET, do_set_port_latency_offset),
+
+ /* Supported since protocol v30 (6.0) */
+ /* BOTH DIRECTIONS */
+ COMMAND(ENABLE_SRBCHANNEL, do_error_access),
+ COMMAND(DISABLE_SRBCHANNEL, do_error_access),
+
+ /* Supported since protocol v31 (9.0)
+ * BOTH DIRECTIONS */
+ COMMAND(REGISTER_MEMFD_SHMID, do_error_access),
+
+ /* Supported since protocol v35 (15.0) */
+ COMMAND(SEND_OBJECT_MESSAGE, do_send_object_message),
+};
+#undef COMMAND
+
+static int impl_free_sample(void *item, void *data)
+{
+ struct sample *s = item;
+
+ spa_assert(s->ref == 1);
+ sample_unref(s);
+
+ return 0;
+}
+
+static int impl_unload_module(void *item, void *data)
+{
+ struct module *m = item;
+ module_unload(m);
+ return 0;
+}
+
+static void impl_clear(struct impl *impl)
+{
+ struct message *msg;
+ struct server *s;
+ struct client *c;
+
+ pw_map_for_each(&impl->modules, impl_unload_module, impl);
+ pw_map_clear(&impl->modules);
+
+ spa_list_consume(s, &impl->servers, link)
+ server_free(s);
+
+ spa_list_consume(c, &impl->cleanup_clients, link)
+ client_free(c);
+
+ spa_list_consume(msg, &impl->free_messages, link)
+ message_free(msg, true, true);
+
+ pw_map_for_each(&impl->samples, impl_free_sample, impl);
+ pw_map_clear(&impl->samples);
+
+ spa_hook_list_clean(&impl->hooks);
+
+#ifdef HAVE_DBUS
+ if (impl->dbus_name) {
+ dbus_release_name(impl->dbus_name);
+ impl->dbus_name = NULL;
+ }
+#endif
+
+ if (impl->context) {
+ spa_hook_remove(&impl->context_listener);
+ impl->context = NULL;
+ }
+
+ pw_properties_free(impl->props);
+ impl->props = NULL;
+}
+
+static void impl_free(struct impl *impl)
+{
+ impl_clear(impl);
+ free(impl);
+}
+
+static void context_destroy(void *data)
+{
+ impl_clear(data);
+}
+
+static const struct pw_context_events context_events = {
+ PW_VERSION_CONTEXT_EVENTS,
+ .destroy = context_destroy,
+};
+
+static int parse_frac(struct pw_properties *props, const char *key, const char *def,
+ struct spa_fraction *res)
+{
+ const char *str;
+ if (props == NULL ||
+ (str = pw_properties_get(props, key)) == NULL)
+ str = def;
+ if (sscanf(str, "%u/%u", &res->num, &res->denom) != 2 || res->denom == 0) {
+ pw_log_warn(": invalid fraction %s, default to %s", str, def);
+ sscanf(def, "%u/%u", &res->num, &res->denom);
+ }
+ pw_log_info(": defaults: %s = %u/%u", key, res->num, res->denom);
+ return 0;
+}
+
+static int parse_position(struct pw_properties *props, const char *key, const char *def,
+ struct channel_map *res)
+{
+ const char *str;
+ struct spa_json it[2];
+ char v[256];
+
+ if (props == NULL ||
+ (str = pw_properties_get(props, key)) == NULL)
+ str = def;
+
+ spa_json_init(&it[0], str, strlen(str));
+ if (spa_json_enter_array(&it[0], &it[1]) <= 0)
+ spa_json_init(&it[1], str, strlen(str));
+
+ res->channels = 0;
+ while (spa_json_get_string(&it[1], v, sizeof(v)) > 0 &&
+ res->channels < SPA_AUDIO_MAX_CHANNELS) {
+ res->map[res->channels++] = channel_name2id(v);
+ }
+ pw_log_info(": defaults: %s = %s", key, str);
+ return 0;
+}
+static int parse_format(struct pw_properties *props, const char *key, const char *def,
+ struct sample_spec *res)
+{
+ const char *str;
+ if (props == NULL ||
+ (str = pw_properties_get(props, key)) == NULL)
+ str = def;
+ res->format = format_name2id(str);
+ if (res->format == SPA_AUDIO_FORMAT_UNKNOWN) {
+ pw_log_warn(": unknown format %s, default to %s", str, def);
+ res->format = format_name2id(def);
+ }
+ pw_log_info(": defaults: %s = %s", key, format_id2name(res->format));
+ return 0;
+}
+static int parse_uint32(struct pw_properties *props, const char *key, const char *def,
+ uint32_t *res)
+{
+ const char *str;
+ if (props == NULL ||
+ (str = pw_properties_get(props, key)) == NULL)
+ str = def;
+ if (!spa_atou32(str, res, 0)) {
+ pw_log_warn(": invalid uint32_t %s, default to %s", str, def);
+ spa_atou32(def, res, 0);
+ }
+ pw_log_info(": defaults: %s = %u", key, *res);
+ return 0;
+}
+
+static void load_defaults(struct defs *def, struct pw_properties *props)
+{
+ parse_frac(props, "pulse.min.req", DEFAULT_MIN_REQ, &def->min_req);
+ parse_frac(props, "pulse.default.req", DEFAULT_DEFAULT_REQ, &def->default_req);
+ parse_frac(props, "pulse.min.frag", DEFAULT_MIN_FRAG, &def->min_frag);
+ parse_frac(props, "pulse.default.frag", DEFAULT_DEFAULT_FRAG, &def->default_frag);
+ parse_frac(props, "pulse.default.tlength", DEFAULT_DEFAULT_TLENGTH, &def->default_tlength);
+ parse_frac(props, "pulse.min.quantum", DEFAULT_MIN_QUANTUM, &def->min_quantum);
+ parse_format(props, "pulse.default.format", DEFAULT_FORMAT, &def->sample_spec);
+ parse_position(props, "pulse.default.position", DEFAULT_POSITION, &def->channel_map);
+ parse_uint32(props, "pulse.idle.timeout", DEFAULT_IDLE_TIMEOUT, &def->idle_timeout);
+ def->sample_spec.channels = def->channel_map.channels;
+ def->quantum_limit = 8192;
+}
+
+struct pw_protocol_pulse *pw_protocol_pulse_new(struct pw_context *context,
+ struct pw_properties *props, size_t user_data_size)
+{
+ const struct spa_support *support;
+ struct spa_cpu *cpu;
+ uint32_t n_support;
+ struct impl *impl;
+ const char *str;
+ int res = 0;
+
+ impl = calloc(1, sizeof(*impl) + user_data_size);
+ if (impl == NULL)
+ goto error_exit;
+
+ if (props == NULL)
+ props = pw_properties_new(NULL, NULL);
+ if (props == NULL)
+ goto error_free;
+
+ support = pw_context_get_support(context, &n_support);
+ cpu = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_CPU);
+
+ pw_context_conf_update_props(context, "pulse.properties", props);
+
+ if ((str = pw_properties_get(props, "vm.overrides")) != NULL) {
+ if (cpu != NULL && spa_cpu_get_vm_type(cpu) != SPA_CPU_VM_NONE)
+ pw_properties_update_string(props, str, strlen(str));
+ pw_properties_set(props, "vm.overrides", NULL);
+ }
+
+ load_defaults(&impl->defs, props);
+
+ debug_messages = pw_log_topic_enabled(SPA_LOG_LEVEL_INFO, pulse_conn);
+
+ impl->context = context;
+ impl->loop = pw_context_get_main_loop(context);
+ impl->props = props;
+
+ impl->work_queue = pw_context_get_work_queue(context);
+
+ spa_hook_list_init(&impl->hooks);
+ spa_list_init(&impl->servers);
+ impl->rate_limit.interval = 2 * SPA_NSEC_PER_SEC;
+ impl->rate_limit.burst = 1;
+ pw_map_init(&impl->samples, 16, 16);
+ pw_map_init(&impl->modules, 16, 16);
+ spa_list_init(&impl->cleanup_clients);
+ spa_list_init(&impl->free_messages);
+
+ str = pw_properties_get(props, "server.address");
+ if (str == NULL) {
+ pw_properties_setf(props, "server.address",
+ "[ \"%s-%s\" ]",
+ PW_PROTOCOL_PULSE_DEFAULT_SERVER,
+ get_server_name(context));
+ str = pw_properties_get(props, "server.address");
+ }
+
+ if (str == NULL)
+ goto error_free;
+
+ if ((res = servers_create_and_start(impl, str, NULL)) < 0) {
+ pw_log_error("%p: no servers could be started: %s",
+ impl, spa_strerror(res));
+ goto error_free;
+ }
+
+ if ((res = create_pid_file()) < 0) {
+ pw_log_warn("%p: can't create pid file: %s",
+ impl, spa_strerror(res));
+ }
+ pw_context_add_listener(context, &impl->context_listener,
+ &context_events, impl);
+
+#ifdef HAVE_DBUS
+ impl->dbus_name = dbus_request_name(context, "org.pulseaudio.Server");
+#endif
+ cmd_run(impl);
+
+ return (struct pw_protocol_pulse *) impl;
+
+error_free:
+ free(impl);
+
+error_exit:
+ pw_properties_free(props);
+
+ if (res < 0)
+ errno = -res;
+
+ return NULL;
+}
+
+void impl_add_listener(struct impl *impl,
+ struct spa_hook *listener,
+ const struct impl_events *events, void *data)
+{
+ spa_hook_list_append(&impl->hooks, listener, events, data);
+}
+
+void *pw_protocol_pulse_get_user_data(struct pw_protocol_pulse *pulse)
+{
+ return SPA_PTROFF(pulse, sizeof(struct impl), void);
+}
+
+void pw_protocol_pulse_destroy(struct pw_protocol_pulse *pulse)
+{
+ struct impl *impl = (struct impl*)pulse;
+ impl_free(impl);
+}
diff --git a/src/modules/module-protocol-pulse/pulse-server.h b/src/modules/module-protocol-pulse/pulse-server.h
new file mode 100644
index 0000000..3e8e6ee
--- /dev/null
+++ b/src/modules/module-protocol-pulse/pulse-server.h
@@ -0,0 +1,56 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_PROTOCOL_PULSE_H
+#define PIPEWIRE_PROTOCOL_PULSE_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <spa/utils/defs.h>
+#include <spa/utils/hook.h>
+
+#define PW_PROTOCOL_PULSE_DEFAULT_PORT 4713
+#define PW_PROTOCOL_PULSE_DEFAULT_SOCKET "native"
+
+#define PW_PROTOCOL_PULSE_DEFAULT_SERVER "unix:"PW_PROTOCOL_PULSE_DEFAULT_SOCKET
+
+#define PW_PROTOCOL_PULSE_USAGE "[ server.address=(tcp:[<ip>:]<port>|unix:<path>)[,...] ] " \
+
+struct pw_context;
+struct pw_properties;
+struct pw_protocol_pulse;
+struct pw_protocol_pulse_server;
+
+struct pw_protocol_pulse *pw_protocol_pulse_new(struct pw_context *context,
+ struct pw_properties *props, size_t user_data_size);
+void *pw_protocol_pulse_get_user_data(struct pw_protocol_pulse *pulse);
+void pw_protocol_pulse_destroy(struct pw_protocol_pulse *pulse);
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* PIPEWIRE_PROTOCOL_PULSE_H */
diff --git a/src/modules/module-protocol-pulse/quirks.c b/src/modules/module-protocol-pulse/quirks.c
new file mode 100644
index 0000000..eb22438
--- /dev/null
+++ b/src/modules/module-protocol-pulse/quirks.c
@@ -0,0 +1,75 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <regex.h>
+
+#include <spa/utils/json.h>
+
+#include <pipewire/properties.h>
+
+#include "log.h"
+#include "quirks.h"
+#include "internal.h"
+
+static uint64_t parse_quirks(const char *str)
+{
+ static const struct { const char *key; uint64_t value; } quirk_keys[] = {
+ { "force-s16-info", QUIRK_FORCE_S16_FORMAT },
+ { "remove-capture-dont-move", QUIRK_REMOVE_CAPTURE_DONT_MOVE },
+ };
+ SPA_FOR_EACH_ELEMENT_VAR(quirk_keys, i) {
+ if (spa_streq(str, i->key))
+ return i->value;
+ }
+ return 0;
+}
+
+static int apply_match(void *data, const char *location, const char *action,
+ const char *val, size_t len)
+{
+ struct client *client = data;
+
+ if (spa_streq(action, "update-props")) {
+ pw_properties_update_string(client->props, val, len);
+ } else if (spa_streq(action, "quirks")) {
+ struct spa_json quirks = SPA_JSON_INIT(val, len), it[1];
+ uint64_t quirks_cur = 0;
+ char v[128];
+
+ if (spa_json_enter_array(&quirks, &it[0]) > 0) {
+ while (spa_json_get_string(&it[0], v, sizeof(v)) > 0)
+ quirks_cur |= parse_quirks(v);
+ }
+ client->quirks = quirks_cur;
+ }
+ return 0;
+}
+
+int client_update_quirks(struct client *client)
+{
+ struct impl *impl = client->impl;
+ struct pw_context *context = impl->context;
+ return pw_context_conf_section_match_rules(context, "pulse.rules",
+ &client->props->dict, apply_match, client);
+}
diff --git a/src/modules/module-protocol-pulse/quirks.h b/src/modules/module-protocol-pulse/quirks.h
new file mode 100644
index 0000000..0229089
--- /dev/null
+++ b/src/modules/module-protocol-pulse/quirks.h
@@ -0,0 +1,36 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PULSER_SERVER_QUIRKS_H
+#define PULSER_SERVER_QUIRKS_H
+
+#include "client.h"
+
+#define QUIRK_FORCE_S16_FORMAT (1ull<<0) /** forces S16 sample format in sink and source
+ * info */
+#define QUIRK_REMOVE_CAPTURE_DONT_MOVE (1ull<<1) /** removes the capture stream DONT_MOVE flag */
+
+int client_update_quirks(struct client *client);
+
+#endif /* PULSER_SERVER_QUIRKS_H */
diff --git a/src/modules/module-protocol-pulse/remap.c b/src/modules/module-protocol-pulse/remap.c
new file mode 100644
index 0000000..1442dee
--- /dev/null
+++ b/src/modules/module-protocol-pulse/remap.c
@@ -0,0 +1,58 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stddef.h>
+
+#include <pipewire/keys.h>
+
+#include "remap.h"
+
+const struct str_map media_role_map[] = {
+ { "Movie", "video", },
+ { "Music", "music", },
+ { "Game", "game", },
+ { "Notification", "event", },
+ { "Communication", "phone", },
+ { "Movie", "animation", },
+ { "Production", "production", },
+ { "Accessibility", "a11y", },
+ { "Test", "test", },
+ { NULL, NULL },
+};
+
+const struct str_map props_key_map[] = {
+ { PW_KEY_DEVICE_BUS_PATH, "device.bus_path" },
+ { PW_KEY_DEVICE_SYSFS_PATH, "sysfs.path" },
+ { PW_KEY_DEVICE_FORM_FACTOR, "device.form_factor" },
+ { PW_KEY_DEVICE_ICON_NAME, "device.icon_name" },
+ { PW_KEY_DEVICE_INTENDED_ROLES, "device.intended_roles" },
+ { PW_KEY_NODE_DESCRIPTION, "device.description" },
+ { PW_KEY_MEDIA_ICON_NAME, "media.icon_name" },
+ { PW_KEY_APP_ICON_NAME, "application.icon_name" },
+ { PW_KEY_APP_PROCESS_MACHINE_ID, "application.process.machine_id" },
+ { PW_KEY_APP_PROCESS_SESSION_ID, "application.process.session_id" },
+ { PW_KEY_MEDIA_ROLE, "media.role", media_role_map },
+ { "pipe.filename", "device.string" },
+ { NULL, NULL },
+};
diff --git a/src/modules/module-protocol-pulse/remap.h b/src/modules/module-protocol-pulse/remap.h
new file mode 100644
index 0000000..49cbdf2
--- /dev/null
+++ b/src/modules/module-protocol-pulse/remap.h
@@ -0,0 +1,52 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PULSE_SERVER_REMAP_H
+#define PULSE_SERVER_REMAP_H
+
+#include <stddef.h>
+
+#include <spa/utils/string.h>
+
+struct str_map {
+ const char *pw_str;
+ const char *pa_str;
+ const struct str_map *child;
+};
+
+extern const struct str_map media_role_map[];
+
+extern const struct str_map props_key_map[];
+
+static inline const struct str_map *str_map_find(const struct str_map *map, const char *pw, const char *pa)
+{
+ size_t i;
+ for (i = 0; map[i].pw_str; i++)
+ if ((pw && spa_streq(map[i].pw_str, pw)) ||
+ (pa && spa_streq(map[i].pa_str, pa)))
+ return &map[i];
+ return NULL;
+}
+
+#endif /* PULSE_SERVER_REMAP_H */
diff --git a/src/modules/module-protocol-pulse/reply.c b/src/modules/module-protocol-pulse/reply.c
new file mode 100644
index 0000000..9444e5b
--- /dev/null
+++ b/src/modules/module-protocol-pulse/reply.c
@@ -0,0 +1,84 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdint.h>
+
+#include <spa/utils/result.h>
+#include <pipewire/log.h>
+
+#include "defs.h"
+#include "client.h"
+#include "commands.h"
+#include "message.h"
+#include "log.h"
+
+struct message *reply_new(const struct client *client, uint32_t tag)
+{
+ struct message *reply = message_alloc(client->impl, -1, 0);
+
+ pw_log_debug("client %p: new reply tag:%u", client, tag);
+
+ message_put(reply,
+ TAG_U32, COMMAND_REPLY,
+ TAG_U32, tag,
+ TAG_INVALID);
+
+ return reply;
+}
+
+int reply_error(struct client *client, uint32_t command, uint32_t tag, int res)
+{
+ struct impl *impl = client->impl;
+ struct message *reply;
+ uint32_t error = res_to_err(res);
+ const char *name;
+ enum spa_log_level level;
+
+ if (command < COMMAND_MAX)
+ name = commands[command].name;
+ else
+ name = "invalid";
+
+ switch (res) {
+ case -ENOENT:
+ case -ENOTSUP:
+ level = SPA_LOG_LEVEL_INFO;
+ break;
+ default:
+ level = SPA_LOG_LEVEL_WARN;
+ break;
+ }
+
+ pw_log(level, "client %p [%s]: ERROR command:%d (%s) tag:%u error:%u (%s)",
+ client, client->name, command, name, tag, error, spa_strerror(res));
+
+ reply = message_alloc(impl, -1, 0);
+ message_put(reply,
+ TAG_U32, COMMAND_ERROR,
+ TAG_U32, tag,
+ TAG_U32, error,
+ TAG_INVALID);
+
+ return client_queue_message(client, reply);
+}
diff --git a/src/modules/module-protocol-pulse/reply.h b/src/modules/module-protocol-pulse/reply.h
new file mode 100644
index 0000000..1ca9ad1
--- /dev/null
+++ b/src/modules/module-protocol-pulse/reply.h
@@ -0,0 +1,42 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PULSE_SERVER_REPLY_H
+#define PULSE_SERVER_REPLY_H
+
+#include <stdint.h>
+
+#include "client.h"
+
+struct message;
+
+struct message *reply_new(const struct client *client, uint32_t tag);
+int reply_error(struct client *client, uint32_t command, uint32_t tag, int res);
+
+static inline int reply_simple_ack(struct client *client, uint32_t tag)
+{
+ return client_queue_message(client, reply_new(client, tag));
+}
+
+#endif /* PULSE_SERVER_REPLY_H */
diff --git a/src/modules/module-protocol-pulse/sample-play.c b/src/modules/module-protocol-pulse/sample-play.c
new file mode 100644
index 0000000..37c68a3
--- /dev/null
+++ b/src/modules/module-protocol-pulse/sample-play.c
@@ -0,0 +1,211 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdint.h>
+#include <errno.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <spa/node/io.h>
+#include <spa/param/audio/raw.h>
+#include <spa/pod/builder.h>
+#include <spa/utils/hook.h>
+#include <pipewire/context.h>
+#include <pipewire/core.h>
+#include <pipewire/log.h>
+#include <pipewire/properties.h>
+#include <pipewire/stream.h>
+
+#include "format.h"
+#include "log.h"
+#include "sample.h"
+#include "sample-play.h"
+
+static void sample_play_stream_state_changed(void *data, enum pw_stream_state old,
+ enum pw_stream_state state, const char *error)
+{
+ struct sample_play *p = data;
+
+ switch (state) {
+ case PW_STREAM_STATE_UNCONNECTED:
+ case PW_STREAM_STATE_ERROR:
+ sample_play_emit_done(p, -EIO);
+ break;
+ case PW_STREAM_STATE_PAUSED:
+ p->id = pw_stream_get_node_id(p->stream);
+ sample_play_emit_ready(p, p->id);
+ break;
+ default:
+ break;
+ }
+}
+
+static void sample_play_stream_destroy(void *data)
+{
+ struct sample_play *p = data;
+
+ pw_log_info("destroy %s", p->sample->name);
+
+ spa_hook_remove(&p->listener);
+ p->stream = NULL;
+
+ sample_unref(p->sample);
+ p->sample = NULL;
+}
+
+static void sample_play_stream_process(void *data)
+{
+ struct sample_play *p = data;
+ struct sample *s = p->sample;
+ struct pw_buffer *b;
+ struct spa_buffer *buf;
+ uint32_t size;
+ uint8_t *d;
+
+ if (p->offset >= s->length) {
+ pw_stream_flush(p->stream, true);
+ return;
+ }
+
+ size = s->length - p->offset;
+
+ if ((b = pw_stream_dequeue_buffer(p->stream)) == NULL) {
+ pw_log_warn("out of buffers: %m");
+ return;
+ }
+
+ buf = b->buffer;
+ if ((d = buf->datas[0].data) == NULL)
+ return;
+
+ size = SPA_MIN(size, buf->datas[0].maxsize);
+ if (b->requested)
+ size = SPA_MIN(size, b->requested * p->stride);
+
+ memcpy(d, s->buffer + p->offset, size);
+
+ p->offset += size;
+
+ buf->datas[0].chunk->offset = 0;
+ buf->datas[0].chunk->stride = p->stride;
+ buf->datas[0].chunk->size = size;
+
+ pw_stream_queue_buffer(p->stream, b);
+}
+
+static void sample_play_stream_drained(void *data)
+{
+ struct sample_play *p = data;
+
+ sample_play_emit_done(p, 0);
+}
+
+static const struct pw_stream_events sample_play_stream_events = {
+ PW_VERSION_STREAM_EVENTS,
+ .state_changed = sample_play_stream_state_changed,
+ .destroy = sample_play_stream_destroy,
+ .process = sample_play_stream_process,
+ .drained = sample_play_stream_drained,
+};
+
+struct sample_play *sample_play_new(struct pw_core *core,
+ struct sample *sample, struct pw_properties *props,
+ size_t user_data_size)
+{
+ struct sample_play *p;
+ uint8_t buffer[1024];
+ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
+ const struct spa_pod *params[1];
+ uint32_t n_params = 0;
+ int res;
+
+ p = calloc(1, sizeof(*p) + user_data_size);
+ if (p == NULL) {
+ res = -errno;
+ goto error_free;
+ }
+
+ p->context = pw_core_get_context(core);
+ p->main_loop = pw_context_get_main_loop(p->context);
+ spa_hook_list_init(&p->hooks);
+ p->user_data = SPA_PTROFF(p, sizeof(struct sample_play), void);
+
+ pw_properties_update(props, &sample->props->dict);
+
+ p->stream = pw_stream_new(core, sample->name, props);
+ props = NULL;
+ if (p->stream == NULL) {
+ res = -errno;
+ goto error_free;
+ }
+
+ /* safe to increment the reference count here because it will be decreased
+ by the stream's 'destroy' event handler, which will be called
+ (even if `pw_stream_connect()` fails) */
+ p->sample = sample_ref(sample);
+ p->stride = sample_spec_frame_size(&sample->ss);
+
+ pw_stream_add_listener(p->stream,
+ &p->listener,
+ &sample_play_stream_events, p);
+
+ params[n_params++] = format_build_param(&b, SPA_PARAM_EnumFormat,
+ &sample->ss, &sample->map);
+
+ res = pw_stream_connect(p->stream,
+ PW_DIRECTION_OUTPUT,
+ PW_ID_ANY,
+ PW_STREAM_FLAG_AUTOCONNECT |
+ PW_STREAM_FLAG_MAP_BUFFERS |
+ PW_STREAM_FLAG_RT_PROCESS,
+ params, n_params);
+ if (res < 0)
+ goto error_cleanup;
+
+ return p;
+
+error_cleanup:
+ pw_stream_destroy(p->stream);
+error_free:
+ pw_properties_free(props);
+ free(p);
+ errno = -res;
+ return NULL;
+}
+
+void sample_play_destroy(struct sample_play *p)
+{
+ if (p->stream)
+ pw_stream_destroy(p->stream);
+
+ spa_hook_list_clean(&p->hooks);
+
+ free(p);
+}
+
+void sample_play_add_listener(struct sample_play *p, struct spa_hook *listener,
+ const struct sample_play_events *events, void *data)
+{
+ spa_hook_list_append(&p->hooks, listener, events, data);
+}
diff --git a/src/modules/module-protocol-pulse/sample-play.h b/src/modules/module-protocol-pulse/sample-play.h
new file mode 100644
index 0000000..5738935
--- /dev/null
+++ b/src/modules/module-protocol-pulse/sample-play.h
@@ -0,0 +1,76 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PULSER_SERVER_SAMPLE_PLAY_H
+#define PULSER_SERVER_SAMPLE_PLAY_H
+
+#include <stddef.h>
+#include <stdint.h>
+
+#include <spa/utils/list.h>
+#include <spa/utils/hook.h>
+
+struct sample;
+struct pw_core;
+struct pw_loop;
+struct pw_stream;
+struct pw_context;
+struct pw_properties;
+
+struct sample_play_events {
+#define VERSION_SAMPLE_PLAY_EVENTS 0
+ uint32_t version;
+
+ void (*ready) (void *data, uint32_t id);
+
+ void (*done) (void *data, int err);
+};
+
+#define sample_play_emit_ready(p,i) spa_hook_list_call(&p->hooks, struct sample_play_events, ready, 0, i)
+#define sample_play_emit_done(p,r) spa_hook_list_call(&p->hooks, struct sample_play_events, done, 0, r)
+
+struct sample_play {
+ struct spa_list link;
+ struct sample *sample;
+ struct pw_stream *stream;
+ uint32_t id;
+ struct spa_hook listener;
+ struct pw_context *context;
+ struct pw_loop *main_loop;
+ uint32_t offset;
+ uint32_t stride;
+ struct spa_hook_list hooks;
+ void *user_data;
+};
+
+struct sample_play *sample_play_new(struct pw_core *core,
+ struct sample *sample, struct pw_properties *props,
+ size_t user_data_size);
+
+void sample_play_destroy(struct sample_play *p);
+
+void sample_play_add_listener(struct sample_play *p, struct spa_hook *listener,
+ const struct sample_play_events *events, void *data);
+
+#endif /* PULSER_SERVER_SAMPLE_PLAY_H */
diff --git a/src/modules/module-protocol-pulse/sample.c b/src/modules/module-protocol-pulse/sample.c
new file mode 100644
index 0000000..a2d8de9
--- /dev/null
+++ b/src/modules/module-protocol-pulse/sample.c
@@ -0,0 +1,50 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdlib.h>
+
+#include <pipewire/log.h>
+#include <pipewire/map.h>
+#include <pipewire/properties.h>
+
+#include "internal.h"
+#include "log.h"
+#include "sample.h"
+
+void sample_free(struct sample *sample)
+{
+ struct impl * const impl = sample->impl;
+
+ pw_log_info("free sample id:%u name:%s", sample->index, sample->name);
+
+ impl->stat.sample_cache -= sample->length;
+
+ if (sample->index != SPA_ID_INVALID)
+ pw_map_remove(&impl->samples, sample->index);
+
+ pw_properties_free(sample->props);
+
+ free(sample->buffer);
+ free(sample);
+}
diff --git a/src/modules/module-protocol-pulse/sample.h b/src/modules/module-protocol-pulse/sample.h
new file mode 100644
index 0000000..db347eb
--- /dev/null
+++ b/src/modules/module-protocol-pulse/sample.h
@@ -0,0 +1,61 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PULSE_SERVER_SAMPLE_H
+#define PULSE_SERVER_SAMPLE_H
+
+#include <stdint.h>
+
+#include "format.h"
+
+struct impl;
+struct pw_properties;
+
+struct sample {
+ int ref;
+ uint32_t index;
+ struct impl *impl;
+ const char *name;
+ struct sample_spec ss;
+ struct channel_map map;
+ struct pw_properties *props;
+ uint32_t length;
+ uint8_t *buffer;
+};
+
+void sample_free(struct sample *sample);
+
+static inline struct sample *sample_ref(struct sample *sample)
+{
+ sample->ref++;
+ return sample;
+}
+
+static inline void sample_unref(struct sample *sample)
+{
+ if (--sample->ref == 0)
+ sample_free(sample);
+}
+
+#endif /* PULSE_SERVER_SAMPLE_H */
diff --git a/src/modules/module-protocol-pulse/server.c b/src/modules/module-protocol-pulse/server.c
new file mode 100644
index 0000000..927e253
--- /dev/null
+++ b/src/modules/module-protocol-pulse/server.c
@@ -0,0 +1,1087 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "config.h"
+
+#include <errno.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <limits.h>
+
+#include <arpa/inet.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <sys/un.h>
+#include <netinet/in.h>
+#include <netinet/tcp.h>
+#include <netinet/ip.h>
+#include <unistd.h>
+
+#ifdef HAVE_SYSTEMD
+#include <systemd/sd-daemon.h>
+#endif
+
+#include <spa/utils/defs.h>
+#include <spa/utils/json.h>
+#include <spa/utils/result.h>
+#include <pipewire/pipewire.h>
+
+#include "client.h"
+#include "commands.h"
+#include "defs.h"
+#include "internal.h"
+#include "log.h"
+#include "message.h"
+#include "reply.h"
+#include "server.h"
+#include "stream.h"
+#include "utils.h"
+#include "flatpak-utils.h"
+
+#define LISTEN_BACKLOG 32
+#define MAX_CLIENTS 64
+
+static int handle_packet(struct client *client, struct message *msg)
+{
+ uint32_t command, tag;
+ int res = 0;
+
+ if (message_get(msg,
+ TAG_U32, &command,
+ TAG_U32, &tag,
+ TAG_INVALID) < 0) {
+ res = -EPROTO;
+ goto finish;
+ }
+
+ pw_log_debug("client %p: received packet command:%u tag:%u",
+ client, command, tag);
+
+ if (command >= COMMAND_MAX) {
+ res = -EINVAL;
+ goto finish;
+ }
+
+ if (debug_messages) {
+ pw_log_debug("client %p: command:%s", client, commands[command].name);
+ message_dump(SPA_LOG_LEVEL_INFO, msg);
+ }
+
+ const struct command *cmd = &commands[command];
+ if (cmd->run == NULL) {
+ res = -ENOTSUP;
+ goto finish;
+ }
+
+ if (!client->authenticated && !SPA_FLAG_IS_SET(cmd->access, COMMAND_ACCESS_WITHOUT_AUTH)) {
+ res = -EACCES;
+ goto finish;
+ }
+
+ if (client->manager == NULL && !SPA_FLAG_IS_SET(cmd->access, COMMAND_ACCESS_WITHOUT_MANAGER)) {
+ res = -EACCES;
+ goto finish;
+ }
+
+ res = cmd->run(client, command, tag, msg);
+
+finish:
+ message_free(msg, false, false);
+ if (res < 0)
+ reply_error(client, command, tag, res);
+
+ return 0;
+}
+
+static int handle_memblock(struct client *client, struct message *msg)
+{
+ struct stream *stream;
+ uint32_t channel, flags, index;
+ int64_t offset, diff;
+ int32_t filled;
+ int res = 0;
+
+ channel = ntohl(client->desc.channel);
+ offset = (int64_t) (
+ (((uint64_t) ntohl(client->desc.offset_hi)) << 32) |
+ (((uint64_t) ntohl(client->desc.offset_lo))));
+ flags = ntohl(client->desc.flags);
+
+ pw_log_debug("client %p: received memblock channel:%d offset:%" PRIi64 " flags:%08x size:%u",
+ client, channel, offset, flags, msg->length);
+
+ stream = pw_map_lookup(&client->streams, channel);
+ if (stream == NULL || stream->type == STREAM_TYPE_RECORD) {
+ pw_log_info("client %p [%s]: received memblock for unknown channel %d",
+ client, client->name, channel);
+ goto finish;
+ }
+
+ filled = spa_ringbuffer_get_write_index(&stream->ring, &index);
+ pw_log_debug("new block %p %p/%u filled:%d index:%d flags:%02x offset:%" PRIu64,
+ msg, msg->data, msg->length, filled, index, flags, offset);
+
+ switch (flags & FLAG_SEEKMASK) {
+ case SEEK_RELATIVE:
+ diff = offset;
+ break;
+ case SEEK_ABSOLUTE:
+ diff = offset - (int64_t)stream->write_index;
+ break;
+ case SEEK_RELATIVE_ON_READ:
+ case SEEK_RELATIVE_END:
+ diff = offset - (int64_t)filled;
+ break;
+ default:
+ pw_log_warn("client %p [%s]: received memblock frame with invalid seek mode: %" PRIu32,
+ client, client->name, (uint32_t)(flags & FLAG_SEEKMASK));
+ res = -EPROTO;
+ goto finish;
+ }
+
+ index += diff;
+ filled += diff;
+ stream->write_index += diff;
+ if ((flags & FLAG_SEEKMASK) == SEEK_RELATIVE)
+ stream->requested -= diff;
+
+ if (filled < 0) {
+ /* underrun, reported on reader side */
+ } else if (filled + msg->length > stream->attr.maxlength) {
+ /* overrun */
+ stream_send_overflow(stream);
+ }
+
+ /* always write data to ringbuffer, we expect the other side
+ * to recover */
+ spa_ringbuffer_write_data(&stream->ring,
+ stream->buffer, MAXLENGTH,
+ index % MAXLENGTH,
+ msg->data,
+ SPA_MIN(msg->length, MAXLENGTH));
+ index += msg->length;
+ spa_ringbuffer_write_update(&stream->ring, index);
+
+ stream->write_index += msg->length;
+ stream->requested -= msg->length;
+
+ stream_send_request(stream);
+
+ if (stream->is_paused && !stream->corked)
+ stream_set_paused(stream, false, "new data");
+
+finish:
+ message_free(msg, false, false);
+ return res;
+}
+
+static int do_read(struct client *client)
+{
+ struct impl * const impl = client->impl;
+ size_t size;
+ int res = 0;
+ void *data;
+
+ if (client->in_index < sizeof(client->desc)) {
+ data = SPA_PTROFF(&client->desc, client->in_index, void);
+ size = sizeof(client->desc) - client->in_index;
+ } else {
+ uint32_t idx = client->in_index - sizeof(client->desc);
+
+ if (client->message == NULL || client->message->length < idx) {
+ res = -EPROTO;
+ goto exit;
+ }
+
+ data = SPA_PTROFF(client->message->data, idx, void);
+ size = client->message->length - idx;
+ }
+
+ while (true) {
+ ssize_t r = recv(client->source->fd, data, size, MSG_DONTWAIT);
+
+ if (r == 0 && size != 0) {
+ res = -EPIPE;
+ goto exit;
+ } else if (r < 0) {
+ if (errno == EINTR)
+ continue;
+ res = -errno;
+ if (res != -EAGAIN && res != -EWOULDBLOCK &&
+ res != -EPIPE && res != -ECONNRESET)
+ pw_log_warn("recv client:%p res %zd: %m", client, r);
+ goto exit;
+ }
+
+ client->in_index += r;
+ break;
+ }
+
+ if (client->in_index == sizeof(client->desc)) {
+ uint32_t flags, length, channel;
+
+ flags = ntohl(client->desc.flags);
+ if ((flags & FLAG_SHMMASK) != 0) {
+ res = -EPROTO;
+ goto exit;
+ }
+
+ length = ntohl(client->desc.length);
+ if (length > FRAME_SIZE_MAX_ALLOW || length <= 0) {
+ pw_log_warn("client %p: received invalid frame size: %u",
+ client, length);
+ res = -EPROTO;
+ goto exit;
+ }
+
+ channel = ntohl(client->desc.channel);
+ if (channel == (uint32_t) -1) {
+ if (flags != 0) {
+ pw_log_warn("client %p: received packet frame with invalid flags",
+ client);
+ res = -EPROTO;
+ goto exit;
+ }
+ }
+
+ if (client->message)
+ message_free(client->message, false, false);
+
+ client->message = message_alloc(impl, channel, length);
+ } else if (client->message &&
+ client->in_index >= client->message->length + sizeof(client->desc)) {
+ struct message * const msg = client->message;
+
+ client->message = NULL;
+ client->in_index = 0;
+
+ if (msg->channel == (uint32_t)-1)
+ res = handle_packet(client, msg);
+ else
+ res = handle_memblock(client, msg);
+ }
+
+exit:
+ return res;
+}
+
+static void
+on_client_data(void *data, int fd, uint32_t mask)
+{
+ struct client * const client = data;
+ int res;
+
+ client->ref++;
+
+ if (mask & SPA_IO_HUP) {
+ res = -EPIPE;
+ goto error;
+ }
+
+ if (mask & SPA_IO_ERR) {
+ res = -EIO;
+ goto error;
+ }
+
+ if (mask & SPA_IO_IN) {
+ pw_log_trace("client %p: can read", client);
+ while (true) {
+ res = do_read(client);
+ if (res < 0) {
+ if (res != -EAGAIN && res != -EWOULDBLOCK)
+ goto error;
+ break;
+ }
+ }
+ }
+
+ if (mask & SPA_IO_OUT || client->new_msg_since_last_flush) {
+ res = client_flush_messages(client);
+ if (res < 0)
+ goto error;
+ }
+
+done:
+ /* drop the reference that was acquired at the beginning of the function */
+ client_unref(client);
+ return;
+
+error:
+ switch (res) {
+ case -EPIPE:
+ case -ECONNRESET:
+ pw_log_info("server %p: client %p [%s] disconnected",
+ client->server, client, client->name);
+ SPA_FALLTHROUGH;
+ case -EPROTO:
+ /*
+ * drop the server's reference to the client
+ * (if it hasn't been dropped already),
+ * it is guaranteed that this will not call `client_free()`
+ * since at the beginning of this function an extra reference
+ * has been acquired which will keep the client alive
+ */
+ if (client_detach(client))
+ client_unref(client);
+
+ /* then disconnect the client */
+ client_disconnect(client);
+ break;
+ default:
+ pw_log_error("server %p: client %p [%s] error %d (%s)",
+ client->server, client, client->name, res, spa_strerror(res));
+ break;
+ }
+
+ goto done;
+}
+
+static void
+on_connect(void *data, int fd, uint32_t mask)
+{
+ struct server * const server = data;
+ struct impl * const impl = server->impl;
+ struct sockaddr_storage name;
+ socklen_t length;
+ int client_fd, val;
+ struct client *client = NULL;
+ const char *client_access = NULL;
+ pid_t pid;
+
+ length = sizeof(name);
+ client_fd = accept4(fd, (struct sockaddr *) &name, &length, SOCK_CLOEXEC);
+ if (client_fd < 0) {
+ if (errno == EMFILE || errno == ENFILE) {
+ if (server->n_clients > 0) {
+ int m = server->source->mask;
+ SPA_FLAG_CLEAR(m, SPA_IO_IN);
+ pw_loop_update_io(impl->loop, server->source, m);
+ server->wait_clients++;
+ }
+ }
+ goto error;
+ }
+
+ if (server->n_clients >= server->max_clients) {
+ close(client_fd);
+ errno = ECONNREFUSED;
+ goto error;
+ }
+
+ client = client_new(server);
+ if (client == NULL)
+ goto error;
+
+ pw_log_debug("server %p: new client %p fd:%d", server, client, client_fd);
+
+ client->source = pw_loop_add_io(impl->loop,
+ client_fd,
+ SPA_IO_ERR | SPA_IO_HUP | SPA_IO_IN,
+ true, on_client_data, client);
+ if (client->source == NULL)
+ goto error;
+
+ client->props = pw_properties_new(
+ PW_KEY_CLIENT_API, "pipewire-pulse",
+ "config.ext", pw_properties_get(impl->props, "config.ext"),
+ NULL);
+ if (client->props == NULL)
+ goto error;
+
+ pw_properties_setf(client->props,
+ "pulse.server.type", "%s",
+ server->addr.ss_family == AF_UNIX ? "unix" : "tcp");
+
+ client->routes = pw_properties_new(NULL, NULL);
+ if (client->routes == NULL)
+ goto error;
+
+ if (server->client_access[0] != '\0')
+ client_access = server->client_access;
+
+ if (server->addr.ss_family == AF_UNIX) {
+ char *app_id = NULL, *devices = NULL;
+
+#ifdef SO_PRIORITY
+ val = 6;
+ if (setsockopt(client_fd, SOL_SOCKET, SO_PRIORITY, &val, sizeof(val)) < 0)
+ pw_log_warn("setsockopt(SO_PRIORITY) failed: %m");
+#endif
+ pid = get_client_pid(client, client_fd);
+ if (pid != 0 && pw_check_flatpak(pid, &app_id, &devices) == 1) {
+ /*
+ * XXX: we should really use Portal client access here
+ *
+ * However, session managers currently support only camera
+ * permissions, and the XDG Portal doesn't have a "Sound Manager"
+ * permission defined. So for now, use access=flatpak, and determine
+ * extra permissions here.
+ *
+ * The application has access to the Pulseaudio socket,
+ * and with real PA it would always then have full sound access.
+ * We'll restrict the full access here behind devices=all;
+ * if the application can access all devices it can then
+ * also sound and camera devices directly, so granting also the
+ * Manager permissions here is reasonable.
+ *
+ * The "Manager" permission in any case is also currently not safe
+ * as the session manager does not check any permission store
+ * for it.
+ */
+ client_access = "flatpak";
+ pw_properties_set(client->props, "pipewire.access.portal.app_id",
+ app_id);
+
+ if (devices && (spa_streq(devices, "all") ||
+ spa_strstartswith(devices, "all;") ||
+ strstr(devices, ";all;")))
+ pw_properties_set(client->props, PW_KEY_MEDIA_CATEGORY, "Manager");
+ else
+ pw_properties_set(client->props, PW_KEY_MEDIA_CATEGORY, NULL);
+ }
+ free(devices);
+ free(app_id);
+ }
+ else if (server->addr.ss_family == AF_INET || server->addr.ss_family == AF_INET6) {
+
+ val = 1;
+ if (setsockopt(client_fd, IPPROTO_TCP, TCP_NODELAY, &val, sizeof(val)) < 0)
+ pw_log_warn("setsockopt(TCP_NODELAY) failed: %m");
+
+ if (server->addr.ss_family == AF_INET) {
+ val = IPTOS_LOWDELAY;
+ if (setsockopt(client_fd, IPPROTO_IP, IP_TOS, &val, sizeof(val)) < 0)
+ pw_log_warn("setsockopt(IP_TOS) failed: %m");
+ }
+ if (client_access == NULL)
+ client_access = "restricted";
+ }
+ pw_properties_set(client->props, PW_KEY_CLIENT_ACCESS, client_access);
+
+ return;
+
+error:
+ pw_log_error("server %p: failed to create client: %m", server);
+ if (client)
+ client_free(client);
+}
+
+static int parse_unix_address(const char *address, struct sockaddr_storage *addrs, int len)
+{
+ struct sockaddr_un addr = {0};
+ int res;
+
+ if (address[0] != '/') {
+ char runtime_dir[PATH_MAX];
+
+ if ((res = get_runtime_dir(runtime_dir, sizeof(runtime_dir))) < 0)
+ return res;
+
+ res = snprintf(addr.sun_path, sizeof(addr.sun_path),
+ "%s/%s", runtime_dir, address);
+ }
+ else {
+ res = snprintf(addr.sun_path, sizeof(addr.sun_path),
+ "%s", address);
+ }
+
+ if (res < 0)
+ return -EINVAL;
+
+ if ((size_t) res >= sizeof(addr.sun_path)) {
+ pw_log_warn("'%s...' too long", addr.sun_path);
+ return -ENAMETOOLONG;
+ }
+
+ if (len < 1)
+ return -ENOSPC;
+
+ addr.sun_family = AF_UNIX;
+
+ memcpy(&addrs[0], &addr, sizeof(addr));
+ return 1;
+}
+
+#ifndef SUN_LEN
+#define SUN_LEN(addr_un) \
+ (offsetof(struct sockaddr_un, sun_path) + strlen((addr_un)->sun_path))
+#endif
+
+static bool is_stale_socket(int fd, const struct sockaddr_un *addr_un)
+{
+ if (connect(fd, (const struct sockaddr *) addr_un, SUN_LEN(addr_un)) < 0) {
+ if (errno == ECONNREFUSED)
+ return true;
+ }
+
+ return false;
+}
+
+#ifdef HAVE_SYSTEMD
+static int check_systemd_activation(const char *path)
+{
+ const int n = sd_listen_fds(0);
+
+ for (int i = 0; i < n; i++) {
+ const int fd = SD_LISTEN_FDS_START + i;
+
+ if (sd_is_socket_unix(fd, SOCK_STREAM, 1, path, 0) > 0)
+ return fd;
+ }
+
+ return -1;
+}
+#else
+static inline int check_systemd_activation(SPA_UNUSED const char *path)
+{
+ return -1;
+}
+#endif
+
+static int start_unix_server(struct server *server, const struct sockaddr_storage *addr)
+{
+ const struct sockaddr_un * const addr_un = (const struct sockaddr_un *) addr;
+ struct stat socket_stat;
+ int fd, res;
+
+ spa_assert(addr_un->sun_family == AF_UNIX);
+
+ fd = check_systemd_activation(addr_un->sun_path);
+ if (fd >= 0) {
+ server->activated = true;
+ pw_log_info("server %p: found systemd socket activation socket for '%s'",
+ server, addr_un->sun_path);
+ goto done;
+ }
+ else {
+ server->activated = false;
+ }
+
+ fd = socket(addr_un->sun_family, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0);
+ if (fd < 0) {
+ res = -errno;
+ pw_log_info("server %p: socket() failed: %m", server);
+ goto error;
+ }
+
+ if (stat(addr_un->sun_path, &socket_stat) < 0) {
+ if (errno != ENOENT) {
+ res = -errno;
+ pw_log_warn("server %p: stat('%s') failed: %m",
+ server, addr_un->sun_path);
+ goto error_close;
+ }
+ }
+ else if (socket_stat.st_mode & S_IWUSR || socket_stat.st_mode & S_IWGRP) {
+ if (!S_ISSOCK(socket_stat.st_mode)) {
+ res = -EEXIST;
+ pw_log_warn("server %p: '%s' exists and is not a socket",
+ server, addr_un->sun_path);
+ goto error_close;
+ }
+
+ /* socket is there, check if it's stale */
+ if (!is_stale_socket(fd, addr_un)) {
+ res = -EADDRINUSE;
+ pw_log_warn("server %p: socket '%s' is in use",
+ server, addr_un->sun_path);
+ goto error_close;
+ }
+
+ pw_log_warn("server %p: unlinking stale socket '%s'",
+ server, addr_un->sun_path);
+
+ if (unlink(addr_un->sun_path) < 0)
+ pw_log_warn("server %p: unlink('%s') failed: %m",
+ server, addr_un->sun_path);
+ }
+ if (bind(fd, (const struct sockaddr *) addr_un, SUN_LEN(addr_un)) < 0) {
+ res = -errno;
+ pw_log_warn("server %p: bind() to '%s' failed: %m",
+ server, addr_un->sun_path);
+ goto error_close;
+ }
+
+ if (chmod(addr_un->sun_path, 0777) < 0)
+ pw_log_warn("server %p: chmod('%s') failed: %m",
+ server, addr_un->sun_path);
+
+ if (listen(fd, server->listen_backlog) < 0) {
+ res = -errno;
+ pw_log_warn("server %p: listen() on '%s' failed: %m",
+ server, addr_un->sun_path);
+ goto error_close;
+ }
+
+ pw_log_info("server %p: listening on unix:%s", server, addr_un->sun_path);
+
+done:
+ server->addr = *addr;
+
+ return fd;
+
+error_close:
+ close(fd);
+error:
+ return res;
+}
+
+static int parse_port(const char *port)
+{
+ const char *end;
+ long p;
+
+ if (port[0] == ':')
+ port += 1;
+
+ errno = 0;
+ p = strtol(port, (char **) &end, 0);
+
+ if (errno != 0)
+ return -errno;
+
+ if (end == port || *end != '\0')
+ return -EINVAL;
+
+ if (!(1 <= p && p <= 65535))
+ return -EINVAL;
+
+ return p;
+}
+
+static int parse_ipv6_address(const char *address, struct sockaddr_in6 *out)
+{
+ char addr_str[INET6_ADDRSTRLEN];
+ struct sockaddr_in6 addr = {0};
+ const char *end;
+ size_t len;
+ int res;
+
+ if (address[0] != '[')
+ return -EINVAL;
+
+ address += 1;
+
+ end = strchr(address, ']');
+ if (end == NULL)
+ return -EINVAL;
+
+ len = end - address;
+ if (len >= sizeof(addr_str))
+ return -ENAMETOOLONG;
+
+ memcpy(addr_str, address, len);
+ addr_str[len] = '\0';
+
+ res = inet_pton(AF_INET6, addr_str, &addr.sin6_addr.s6_addr);
+ if (res < 0)
+ return -errno;
+ if (res == 0)
+ return -EINVAL;
+
+ res = parse_port(end + 1);
+ if (res < 0)
+ return res;
+
+ addr.sin6_port = htons(res);
+ addr.sin6_family = AF_INET6;
+
+ *out = addr;
+
+ return 0;
+}
+
+static int parse_ipv4_address(const char *address, struct sockaddr_in *out)
+{
+ char addr_str[INET_ADDRSTRLEN];
+ struct sockaddr_in addr = {0};
+ size_t len;
+ int res;
+
+ len = strspn(address, "0123456789.");
+ if (len == 0)
+ return -EINVAL;
+ if (len >= sizeof(addr_str))
+ return -ENAMETOOLONG;
+
+ memcpy(addr_str, address, len);
+ addr_str[len] = '\0';
+
+ res = inet_pton(AF_INET, addr_str, &addr.sin_addr.s_addr);
+ if (res < 0)
+ return -errno;
+ if (res == 0)
+ return -EINVAL;
+
+ res = parse_port(address + len);
+ if (res < 0)
+ return res;
+
+ addr.sin_port = htons(res);
+ addr.sin_family = AF_INET;
+
+ *out = addr;
+
+ return 0;
+}
+
+#define FORMATTED_IP_ADDR_STRLEN (INET6_ADDRSTRLEN + 2 + 1 + 5)
+
+static int format_ip_address(const struct sockaddr_storage *addr, char *buffer, size_t buflen)
+{
+ char ip[INET6_ADDRSTRLEN];
+ const void *src;
+ bool is_ipv6 = false;
+ int port;
+
+ switch (addr->ss_family) {
+ case AF_INET:
+ src = &((struct sockaddr_in *) addr)->sin_addr.s_addr;
+ port = ntohs(((struct sockaddr_in *) addr)->sin_port);
+ break;
+ case AF_INET6:
+ is_ipv6 = true;
+ src = &((struct sockaddr_in6 *) addr)->sin6_addr.s6_addr;
+ port = ntohs(((struct sockaddr_in6 *) addr)->sin6_port);
+ break;
+ default:
+ return -EAFNOSUPPORT;
+ }
+
+ if (inet_ntop(addr->ss_family, src, ip, sizeof(ip)) == NULL)
+ return -errno;
+
+ return snprintf(buffer, buflen, "%s%s%s:%d",
+ is_ipv6 ? "[" : "",
+ ip,
+ is_ipv6 ? "]" : "",
+ port);
+}
+
+static int get_ip_address_length(const struct sockaddr_storage *addr)
+{
+ switch (addr->ss_family) {
+ case AF_INET:
+ return sizeof(struct sockaddr_in);
+ case AF_INET6:
+ return sizeof(struct sockaddr_in6);
+ default:
+ return -EAFNOSUPPORT;
+ }
+}
+
+static int parse_ip_address(const char *address, struct sockaddr_storage *addrs, int len)
+{
+ char ip[FORMATTED_IP_ADDR_STRLEN];
+ struct sockaddr_storage addr;
+ int res;
+
+ res = parse_ipv6_address(address, (struct sockaddr_in6 *) &addr);
+ if (res == 0) {
+ if (len < 1)
+ return -ENOSPC;
+ addrs[0] = addr;
+ return 1;
+ }
+
+ res = parse_ipv4_address(address, (struct sockaddr_in *) &addr);
+ if (res == 0) {
+ if (len < 1)
+ return -ENOSPC;
+ addrs[0] = addr;
+ return 1;
+ }
+
+ res = parse_port(address);
+ if (res < 0)
+ return res;
+
+ if (len < 2)
+ return -ENOSPC;
+
+ snprintf(ip, sizeof(ip), "0.0.0.0:%d", res);
+ spa_assert_se(parse_ipv4_address(ip, (struct sockaddr_in *) &addr) == 0);
+ addrs[0] = addr;
+
+ snprintf(ip, sizeof(ip), "[::]:%d", res);
+ spa_assert_se(parse_ipv6_address(ip, (struct sockaddr_in6 *) &addr) == 0);
+ addrs[1] = addr;
+
+ return 2;
+}
+
+static int start_ip_server(struct server *server, const struct sockaddr_storage *addr)
+{
+ char ip[FORMATTED_IP_ADDR_STRLEN];
+ int fd, res;
+
+ spa_assert(addr->ss_family == AF_INET || addr->ss_family == AF_INET6);
+
+ fd = socket(addr->ss_family, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK, IPPROTO_TCP);
+ if (fd < 0) {
+ res = -errno;
+ pw_log_warn("server %p: socket() failed: %m", server);
+ goto error;
+ }
+
+ {
+ int on = 1;
+ if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
+ pw_log_warn("server %p: setsockopt(SO_REUSEADDR) failed: %m", server);
+ }
+
+ if (addr->ss_family == AF_INET6) {
+ int on = 1;
+ if (setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &on, sizeof(on)) < 0)
+ pw_log_warn("server %p: setsockopt(IPV6_V6ONLY) failed: %m", server);
+ }
+
+ if (bind(fd, (const struct sockaddr *) addr, get_ip_address_length(addr)) < 0) {
+ res = -errno;
+ pw_log_warn("server %p: bind() failed: %m", server);
+ goto error_close;
+ }
+
+ if (listen(fd, server->listen_backlog) < 0) {
+ res = -errno;
+ pw_log_warn("server %p: listen() failed: %m", server);
+ goto error_close;
+ }
+
+ spa_assert_se(format_ip_address(addr, ip, sizeof(ip)) >= 0);
+ pw_log_info("server %p: listening on tcp:%s", server, ip);
+
+ server->addr = *addr;
+
+ return fd;
+
+error_close:
+ close(fd);
+error:
+ return res;
+}
+
+static struct server *server_new(struct impl *impl)
+{
+ struct server * const server = calloc(1, sizeof(*server));
+ if (server == NULL)
+ return NULL;
+
+ server->impl = impl;
+ server->addr.ss_family = AF_UNSPEC;
+ spa_list_init(&server->clients);
+ spa_list_append(&impl->servers, &server->link);
+
+ pw_log_debug("server %p: new", server);
+
+ return server;
+}
+
+static int server_start(struct server *server, const struct sockaddr_storage *addr)
+{
+ struct impl * const impl = server->impl;
+ int res = 0, fd;
+
+ switch (addr->ss_family) {
+ case AF_INET:
+ case AF_INET6:
+ fd = start_ip_server(server, addr);
+ break;
+ case AF_UNIX:
+ fd = start_unix_server(server, addr);
+ break;
+ default:
+ /* shouldn't happen */
+ fd = -EAFNOSUPPORT;
+ break;
+ }
+
+ if (fd < 0)
+ return fd;
+
+ server->source = pw_loop_add_io(impl->loop, fd, SPA_IO_IN, true, on_connect, server);
+ if (server->source == NULL) {
+ res = -errno;
+ pw_log_error("server %p: can't create server source: %m", impl);
+ }
+ if (res >= 0)
+ spa_hook_list_call(&impl->hooks, struct impl_events, server_started, 0, server);
+
+ return res;
+}
+
+static int parse_address(const char *address, struct sockaddr_storage *addrs, int len)
+{
+ if (spa_strstartswith(address, "tcp:"))
+ return parse_ip_address(address + strlen("tcp:"), addrs, len);
+
+ if (spa_strstartswith(address, "unix:"))
+ return parse_unix_address(address + strlen("unix:"), addrs, len);
+
+ return -EAFNOSUPPORT;
+}
+
+#define SUN_PATH_SIZE (sizeof(((struct sockaddr_un *) NULL)->sun_path))
+#define FORMATTED_UNIX_ADDR_STRLEN (SUN_PATH_SIZE + 5)
+#define FORMATTED_TCP_ADDR_STRLEN (FORMATTED_IP_ADDR_STRLEN + 4)
+#define FORMATTED_SOCKET_ADDR_STRLEN \
+ (FORMATTED_UNIX_ADDR_STRLEN > FORMATTED_TCP_ADDR_STRLEN ? \
+ FORMATTED_UNIX_ADDR_STRLEN : \
+ FORMATTED_TCP_ADDR_STRLEN)
+
+static int format_socket_address(const struct sockaddr_storage *addr, char *buffer, size_t buflen)
+{
+ if (addr->ss_family == AF_INET || addr->ss_family == AF_INET6) {
+ char ip[FORMATTED_IP_ADDR_STRLEN];
+
+ spa_assert_se(format_ip_address(addr, ip, sizeof(ip)) >= 0);
+
+ return snprintf(buffer, buflen, "tcp:%s", ip);
+ }
+ else if (addr->ss_family == AF_UNIX) {
+ const struct sockaddr_un *addr_un = (const struct sockaddr_un *) addr;
+
+ return snprintf(buffer, buflen, "unix:%s", addr_un->sun_path);
+ }
+
+ return -EAFNOSUPPORT;
+}
+
+int servers_create_and_start(struct impl *impl, const char *addresses, struct pw_array *servers)
+{
+ int len, res, count = 0, err = 0; /* store the first error to return when no servers could be created */
+ const char *v;
+ struct spa_json it[3];
+
+ /* update `err` if it hasn't been set to an errno */
+#define UPDATE_ERR(e) do { if (err == 0) err = (e); } while (false)
+
+ /* collect addresses into an array of `struct sockaddr_storage` */
+ spa_json_init(&it[0], addresses, strlen(addresses));
+
+ /* [ <server-spec> ... ] */
+ if (spa_json_enter_array(&it[0], &it[1]) < 0)
+ return -EINVAL;
+
+ /* a server-spec is either an address or an object */
+ while ((len = spa_json_next(&it[1], &v)) > 0) {
+ char addr_str[FORMATTED_SOCKET_ADDR_STRLEN] = { 0 };
+ char key[128], client_access[64] = { 0 };
+ struct sockaddr_storage addrs[2];
+ int i, max_clients = MAX_CLIENTS, listen_backlog = LISTEN_BACKLOG, n_addr;
+
+ if (spa_json_is_object(v, len)) {
+ spa_json_enter(&it[1], &it[2]);
+ while (spa_json_get_string(&it[2], key, sizeof(key)) > 0) {
+ if ((len = spa_json_next(&it[2], &v)) <= 0)
+ break;
+
+ if (spa_streq(key, "address")) {
+ spa_json_parse_stringn(v, len, addr_str, sizeof(addr_str));
+ } else if (spa_streq(key, "max-clients")) {
+ spa_json_parse_int(v, len, &max_clients);
+ } else if (spa_streq(key, "listen-backlog")) {
+ spa_json_parse_int(v, len, &listen_backlog);
+ } else if (spa_streq(key, "client.access")) {
+ spa_json_parse_stringn(v, len, client_access, sizeof(client_access));
+ }
+ }
+ } else {
+ spa_json_parse_stringn(v, len, addr_str, sizeof(addr_str));
+ }
+
+ n_addr = parse_address(addr_str, addrs, SPA_N_ELEMENTS(addrs));
+ if (n_addr < 0) {
+ pw_log_warn("pulse-server %p: failed to parse address '%s': %s",
+ impl, addr_str, spa_strerror(n_addr));
+ UPDATE_ERR(n_addr);
+ continue;
+ }
+
+ /* try to create sockets for each address in the list */
+ for (i = 0; i < n_addr; i++) {
+ const struct sockaddr_storage *addr = &addrs[i];
+ struct server * const server = server_new(impl);
+
+ if (server == NULL) {
+ UPDATE_ERR(-errno);
+ continue;
+ }
+
+ server->max_clients = max_clients;
+ server->listen_backlog = listen_backlog;
+ memcpy(server->client_access, client_access, sizeof(client_access));
+
+ res = server_start(server, addr);
+ if (res < 0) {
+ spa_assert_se(format_socket_address(addr, addr_str, sizeof(addr_str)) >= 0);
+ pw_log_warn("pulse-server %p: failed to start server on '%s': %s",
+ impl, addr_str, spa_strerror(res));
+ UPDATE_ERR(res);
+ server_free(server);
+ continue;
+ }
+
+ if (servers != NULL)
+ pw_array_add_ptr(servers, server);
+
+ count += 1;
+ }
+ }
+ if (count == 0) {
+ UPDATE_ERR(-EINVAL);
+ return err;
+ }
+ return count;
+
+#undef UPDATE_ERR
+}
+
+void server_free(struct server *server)
+{
+ struct impl * const impl = server->impl;
+ struct client *c, *t;
+
+ pw_log_debug("server %p: free", server);
+
+ spa_list_remove(&server->link);
+
+ spa_list_for_each_safe(c, t, &server->clients, link) {
+ spa_assert_se(client_detach(c));
+ client_unref(c);
+ }
+
+ spa_hook_list_call(&impl->hooks, struct impl_events, server_stopped, 0, server);
+
+ if (server->source)
+ pw_loop_destroy_source(impl->loop, server->source);
+
+ if (server->addr.ss_family == AF_UNIX && !server->activated)
+ unlink(((const struct sockaddr_un *) &server->addr)->sun_path);
+
+ free(server);
+}
diff --git a/src/modules/module-protocol-pulse/server.h b/src/modules/module-protocol-pulse/server.h
new file mode 100644
index 0000000..9474715
--- /dev/null
+++ b/src/modules/module-protocol-pulse/server.h
@@ -0,0 +1,60 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PULSER_SERVER_SERVER_H
+#define PULSER_SERVER_SERVER_H
+
+#include <stdint.h>
+
+#include <sys/socket.h>
+
+#include <spa/utils/list.h>
+#include <spa/utils/hook.h>
+
+struct impl;
+struct pw_array;
+struct spa_source;
+
+struct server {
+ struct spa_list link;
+ struct impl *impl;
+
+ struct sockaddr_storage addr;
+
+ struct spa_source *source;
+ struct spa_list clients;
+
+ uint32_t max_clients;
+ uint32_t listen_backlog;
+ char client_access[64];
+
+ uint32_t n_clients;
+ uint32_t wait_clients;
+ unsigned int activated:1;
+};
+
+int servers_create_and_start(struct impl *impl, const char *addresses, struct pw_array *servers);
+void server_free(struct server *server);
+
+#endif /* PULSER_SERVER_SERVER_H */
diff --git a/src/modules/module-protocol-pulse/stream.c b/src/modules/module-protocol-pulse/stream.c
new file mode 100644
index 0000000..59fb8a3
--- /dev/null
+++ b/src/modules/module-protocol-pulse/stream.c
@@ -0,0 +1,428 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdlib.h>
+
+#include <spa/utils/hook.h>
+#include <spa/utils/ringbuffer.h>
+#include <pipewire/log.h>
+#include <pipewire/loop.h>
+#include <pipewire/map.h>
+#include <pipewire/properties.h>
+#include <pipewire/stream.h>
+#include <pipewire/work-queue.h>
+
+#include "client.h"
+#include "commands.h"
+#include "defs.h"
+#include "internal.h"
+#include "log.h"
+#include "message.h"
+#include "reply.h"
+#include "stream.h"
+
+static int parse_frac(struct pw_properties *props, const char *key,
+ const struct spa_fraction *def, struct spa_fraction *res)
+{
+ const char *str;
+ if (props == NULL ||
+ (str = pw_properties_get(props, key)) == NULL ||
+ sscanf(str, "%u/%u", &res->num, &res->denom) != 2 ||
+ res->denom == 0) {
+ *res = *def;
+ }
+ return 0;
+}
+
+struct stream *stream_new(struct client *client, enum stream_type type, uint32_t create_tag,
+ const struct sample_spec *ss, const struct channel_map *map,
+ const struct buffer_attr *attr)
+{
+ int res;
+ struct defs *defs = &client->impl->defs;
+ const char *str;
+
+ struct stream *stream = calloc(1, sizeof(*stream));
+ if (stream == NULL)
+ return NULL;
+
+ stream->channel = pw_map_insert_new(&client->streams, stream);
+ if (stream->channel == SPA_ID_INVALID)
+ goto error_errno;
+
+ stream->impl = client->impl;
+ stream->client = client;
+ stream->type = type;
+ stream->create_tag = create_tag;
+ stream->ss = *ss;
+ stream->map = *map;
+ stream->attr = *attr;
+ spa_ringbuffer_init(&stream->ring);
+
+ stream->peer_index = SPA_ID_INVALID;
+
+ parse_frac(client->props, "pulse.min.req", &defs->min_req, &stream->min_req);
+ parse_frac(client->props, "pulse.min.frag", &defs->min_frag, &stream->min_frag);
+ parse_frac(client->props, "pulse.min.quantum", &defs->min_quantum, &stream->min_quantum);
+ parse_frac(client->props, "pulse.default.req", &defs->default_req, &stream->default_req);
+ parse_frac(client->props, "pulse.default.frag", &defs->default_frag, &stream->default_frag);
+ parse_frac(client->props, "pulse.default.tlength", &defs->default_tlength, &stream->default_tlength);
+ stream->idle_timeout_sec = defs->idle_timeout;
+ if ((str = pw_properties_get(client->props, "pulse.idle.timeout")) != NULL)
+ spa_atou32(str, &stream->idle_timeout_sec, 0);
+
+ switch (type) {
+ case STREAM_TYPE_RECORD:
+ stream->direction = PW_DIRECTION_INPUT;
+ break;
+ case STREAM_TYPE_PLAYBACK:
+ case STREAM_TYPE_UPLOAD:
+ stream->direction = PW_DIRECTION_OUTPUT;
+ break;
+ default:
+ spa_assert_not_reached();
+ }
+
+ return stream;
+
+error_errno:
+ res = errno;
+ free(stream);
+ errno = res;
+
+ return NULL;
+}
+
+void stream_free(struct stream *stream)
+{
+ struct client *client = stream->client;
+ struct impl *impl = client->impl;
+
+ pw_log_debug("client %p: stream %p channel:%d", client, stream, stream->channel);
+
+ if (stream->pending)
+ spa_list_remove(&stream->link);
+
+ if (stream->drain_tag)
+ reply_error(client, -1, stream->drain_tag, -ENOENT);
+
+ if (stream->killed)
+ stream_send_killed(stream);
+
+ if (stream->stream) {
+ spa_hook_remove(&stream->stream_listener);
+ pw_stream_disconnect(stream->stream);
+
+ /* force processing of all pending messages before we destroy
+ * the stream */
+ pw_loop_invoke(impl->loop, NULL, 0, NULL, 0, false, client);
+
+ pw_stream_destroy(stream->stream);
+ }
+ if (stream->channel != SPA_ID_INVALID)
+ pw_map_remove(&client->streams, stream->channel);
+
+ pw_work_queue_cancel(impl->work_queue, stream, SPA_ID_INVALID);
+
+ if (stream->buffer)
+ free(stream->buffer);
+
+ pw_properties_free(stream->props);
+
+ free(stream);
+}
+
+void stream_flush(struct stream *stream)
+{
+ pw_stream_flush(stream->stream, false);
+
+ if (stream->type == STREAM_TYPE_PLAYBACK) {
+ stream->ring.writeindex = stream->ring.readindex;
+ stream->write_index = stream->read_index;
+
+ if (stream->attr.prebuf > 0)
+ stream->in_prebuf = true;
+
+ stream->playing_for = 0;
+ stream->underrun_for = -1;
+ stream->is_underrun = true;
+
+ stream_send_request(stream);
+ } else {
+ stream->ring.readindex = stream->ring.writeindex;
+ stream->read_index = stream->write_index;
+ }
+}
+
+static bool stream_prebuf_active(struct stream *stream, int32_t avail)
+{
+ if (stream->in_prebuf) {
+ if (avail >= (int32_t) stream->attr.prebuf)
+ stream->in_prebuf = false;
+ } else {
+ if (stream->attr.prebuf > 0 && avail <= 0)
+ stream->in_prebuf = true;
+ }
+ return stream->in_prebuf;
+}
+
+uint32_t stream_pop_missing(struct stream *stream)
+{
+ int64_t missing, avail;
+
+ avail = stream->write_index - stream->read_index;
+
+ missing = stream->attr.tlength;
+ missing -= stream->requested;
+ missing -= avail;
+
+ if (missing <= 0)
+ return 0;
+
+ if (missing < stream->attr.minreq && !stream_prebuf_active(stream, avail))
+ return 0;
+
+ stream->requested += missing;
+
+ return missing;
+}
+
+void stream_set_paused(struct stream *stream, bool paused, const char *reason)
+{
+ if (stream->is_paused == paused)
+ return;
+
+ if (reason && stream->client)
+ pw_log_info("%p: [%s] %s because of %s",
+ stream, stream->client->name,
+ paused ? "paused" : "resumed", reason);
+
+ stream->is_paused = paused;
+ pw_stream_set_active(stream->stream, !paused);
+}
+
+int stream_send_underflow(struct stream *stream, int64_t offset)
+{
+ struct client *client = stream->client;
+ struct impl *impl = client->impl;
+ struct message *reply;
+
+ if (ratelimit_test(&impl->rate_limit, stream->timestamp, SPA_LOG_LEVEL_INFO)) {
+ pw_log_info("[%s]: UNDERFLOW channel:%u offset:%" PRIi64,
+ client->name, stream->channel, offset);
+ }
+
+ reply = message_alloc(impl, -1, 0);
+ message_put(reply,
+ TAG_U32, COMMAND_UNDERFLOW,
+ TAG_U32, -1,
+ TAG_U32, stream->channel,
+ TAG_INVALID);
+
+ if (client->version >= 23) {
+ message_put(reply,
+ TAG_S64, offset,
+ TAG_INVALID);
+ }
+
+ return client_queue_message(client, reply);
+}
+
+int stream_send_overflow(struct stream *stream)
+{
+ struct client *client = stream->client;
+ struct impl *impl = client->impl;
+ struct message *reply;
+
+ pw_log_warn("client %p [%s]: stream %p OVERFLOW channel:%u",
+ client, client->name, stream, stream->channel);
+
+ reply = message_alloc(impl, -1, 0);
+ message_put(reply,
+ TAG_U32, COMMAND_OVERFLOW,
+ TAG_U32, -1,
+ TAG_U32, stream->channel,
+ TAG_INVALID);
+
+ return client_queue_message(client, reply);
+}
+
+int stream_send_killed(struct stream *stream)
+{
+ struct client *client = stream->client;
+ struct impl *impl = client->impl;
+ struct message *reply;
+ uint32_t command;
+
+ command = stream->direction == PW_DIRECTION_OUTPUT ?
+ COMMAND_PLAYBACK_STREAM_KILLED :
+ COMMAND_RECORD_STREAM_KILLED;
+
+ pw_log_info("[%s]: %s channel:%u",
+ client->name, commands[command].name, stream->channel);
+
+ if (client->version < 23)
+ return 0;
+
+ reply = message_alloc(impl, -1, 0);
+ message_put(reply,
+ TAG_U32, command,
+ TAG_U32, -1,
+ TAG_U32, stream->channel,
+ TAG_INVALID);
+
+ return client_queue_message(client, reply);
+}
+
+int stream_send_started(struct stream *stream)
+{
+ struct client *client = stream->client;
+ struct impl *impl = client->impl;
+ struct message *reply;
+
+ pw_log_debug("client %p [%s]: stream %p STARTED channel:%u",
+ client, client->name, stream, stream->channel);
+
+ reply = message_alloc(impl, -1, 0);
+ message_put(reply,
+ TAG_U32, COMMAND_STARTED,
+ TAG_U32, -1,
+ TAG_U32, stream->channel,
+ TAG_INVALID);
+
+ return client_queue_message(client, reply);
+}
+
+int stream_send_request(struct stream *stream)
+{
+ struct client *client = stream->client;
+ struct impl *impl = client->impl;
+ struct message *msg;
+ uint32_t size;
+
+ size = stream_pop_missing(stream);
+ pw_log_debug("stream %p: REQUEST channel:%d %u", stream, stream->channel, size);
+
+ if (size == 0)
+ return 0;
+
+ msg = message_alloc(impl, -1, 0);
+ message_put(msg,
+ TAG_U32, COMMAND_REQUEST,
+ TAG_U32, -1,
+ TAG_U32, stream->channel,
+ TAG_U32, size,
+ TAG_INVALID);
+
+ return client_queue_message(client, msg);
+}
+
+int stream_update_minreq(struct stream *stream, uint32_t minreq)
+{
+ struct client *client = stream->client;
+ struct impl *impl = client->impl;
+ uint32_t old_tlength = stream->attr.tlength;
+ uint32_t new_tlength = minreq + 2 * stream->attr.minreq;
+ uint64_t lat_usec;
+
+ if (new_tlength <= old_tlength)
+ return 0;
+
+ if (new_tlength > MAXLENGTH)
+ new_tlength = MAXLENGTH;
+
+ stream->attr.tlength = new_tlength;
+ if (stream->attr.tlength > stream->attr.maxlength)
+ stream->attr.maxlength = stream->attr.tlength;
+
+ if (client->version >= 15) {
+ struct message *msg;
+
+ lat_usec = minreq * SPA_USEC_PER_SEC / stream->ss.rate;
+
+ msg = message_alloc(impl, -1, 0);
+ message_put(msg,
+ TAG_U32, COMMAND_PLAYBACK_BUFFER_ATTR_CHANGED,
+ TAG_U32, -1,
+ TAG_U32, stream->channel,
+ TAG_U32, stream->attr.maxlength,
+ TAG_U32, stream->attr.tlength,
+ TAG_U32, stream->attr.prebuf,
+ TAG_U32, stream->attr.minreq,
+ TAG_USEC, lat_usec,
+ TAG_INVALID);
+ return client_queue_message(client, msg);
+ }
+ return 0;
+}
+
+int stream_send_moved(struct stream *stream, uint32_t peer_index, const char *peer_name)
+{
+ struct client *client = stream->client;
+ struct impl *impl = client->impl;
+ struct message *reply;
+ uint32_t command;
+
+ command = stream->direction == PW_DIRECTION_OUTPUT ?
+ COMMAND_PLAYBACK_STREAM_MOVED :
+ COMMAND_RECORD_STREAM_MOVED;
+
+ pw_log_info("client %p [%s]: stream %p %s channel:%u",
+ client, client->name, stream, commands[command].name,
+ stream->channel);
+
+ if (client->version < 12)
+ return 0;
+
+ reply = message_alloc(impl, -1, 0);
+ message_put(reply,
+ TAG_U32, command,
+ TAG_U32, -1,
+ TAG_U32, stream->channel,
+ TAG_U32, peer_index,
+ TAG_STRING, peer_name,
+ TAG_BOOLEAN, false, /* suspended */
+ TAG_INVALID);
+
+ if (client->version >= 13) {
+ if (command == COMMAND_PLAYBACK_STREAM_MOVED) {
+ message_put(reply,
+ TAG_U32, stream->attr.maxlength,
+ TAG_U32, stream->attr.tlength,
+ TAG_U32, stream->attr.prebuf,
+ TAG_U32, stream->attr.minreq,
+ TAG_USEC, stream->lat_usec,
+ TAG_INVALID);
+ } else {
+ message_put(reply,
+ TAG_U32, stream->attr.maxlength,
+ TAG_U32, stream->attr.fragsize,
+ TAG_USEC, stream->lat_usec,
+ TAG_INVALID);
+ }
+ }
+ return client_queue_message(client, reply);
+}
diff --git a/src/modules/module-protocol-pulse/stream.h b/src/modules/module-protocol-pulse/stream.h
new file mode 100644
index 0000000..424b2a1
--- /dev/null
+++ b/src/modules/module-protocol-pulse/stream.h
@@ -0,0 +1,141 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PULSER_SERVER_STREAM_H
+#define PULSER_SERVER_STREAM_H
+
+#include <stdbool.h>
+#include <stdint.h>
+
+#include <spa/utils/hook.h>
+#include <spa/utils/ringbuffer.h>
+#include <pipewire/pipewire.h>
+
+#include "format.h"
+#include "volume.h"
+
+struct impl;
+struct client;
+struct spa_io_rate_match;
+
+struct buffer_attr {
+ uint32_t maxlength;
+ uint32_t tlength;
+ uint32_t prebuf;
+ uint32_t minreq;
+ uint32_t fragsize;
+};
+
+enum stream_type {
+ STREAM_TYPE_RECORD,
+ STREAM_TYPE_PLAYBACK,
+ STREAM_TYPE_UPLOAD,
+};
+
+struct stream {
+ struct spa_list link;
+ uint32_t create_tag;
+ uint32_t channel; /* index in map */
+ uint32_t id; /* id of global */
+ uint32_t index; /* index */
+
+ uint32_t peer_index;
+
+ struct impl *impl;
+ struct client *client;
+ enum stream_type type;
+ enum pw_direction direction;
+
+ struct pw_properties *props;
+
+ struct pw_stream *stream;
+ struct spa_hook stream_listener;
+
+ struct spa_io_position *position;
+ struct spa_ringbuffer ring;
+ void *buffer;
+
+ int64_t read_index;
+ int64_t write_index;
+ uint64_t underrun_for;
+ uint64_t playing_for;
+ uint64_t ticks_base;
+ uint64_t timestamp;
+ uint64_t idle_time;
+ int64_t delay;
+
+ uint32_t last_quantum;
+ int64_t requested;
+
+ struct spa_fraction min_req;
+ struct spa_fraction default_req;
+ struct spa_fraction min_frag;
+ struct spa_fraction default_frag;
+ struct spa_fraction default_tlength;
+ struct spa_fraction min_quantum;
+ uint32_t idle_timeout_sec;
+
+ struct sample_spec ss;
+ struct channel_map map;
+ struct buffer_attr attr;
+ uint32_t frame_size;
+ uint32_t rate;
+ uint64_t lat_usec;
+
+ struct volume volume;
+ bool muted;
+
+ uint32_t drain_tag;
+ unsigned int corked:1;
+ unsigned int draining:1;
+ unsigned int volume_set:1;
+ unsigned int muted_set:1;
+ unsigned int early_requests:1;
+ unsigned int adjust_latency:1;
+ unsigned int is_underrun:1;
+ unsigned int in_prebuf:1;
+ unsigned int killed:1;
+ unsigned int pending:1;
+ unsigned int is_idle:1;
+ unsigned int is_paused:1;
+};
+
+struct stream *stream_new(struct client *client, enum stream_type type, uint32_t create_tag,
+ const struct sample_spec *ss, const struct channel_map *map,
+ const struct buffer_attr *attr);
+void stream_free(struct stream *stream);
+void stream_flush(struct stream *stream);
+uint32_t stream_pop_missing(struct stream *stream);
+
+void stream_set_paused(struct stream *stream, bool paused, const char *reason);
+
+int stream_send_underflow(struct stream *stream, int64_t offset);
+int stream_send_overflow(struct stream *stream);
+int stream_send_killed(struct stream *stream);
+int stream_send_started(struct stream *stream);
+int stream_send_request(struct stream *stream);
+int stream_update_minreq(struct stream *stream, uint32_t minreq);
+int stream_send_moved(struct stream *stream, uint32_t peer_index, const char *peer_name);
+
+#endif /* PULSER_SERVER_STREAM_H */
diff --git a/src/modules/module-protocol-pulse/utils.c b/src/modules/module-protocol-pulse/utils.c
new file mode 100644
index 0000000..7626449
--- /dev/null
+++ b/src/modules/module-protocol-pulse/utils.c
@@ -0,0 +1,207 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "config.h"
+
+#include <errno.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <limits.h>
+
+#include <fcntl.h>
+#include <unistd.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#ifdef HAVE_SYS_VFS_H
+#include <sys/vfs.h>
+#endif
+#ifdef HAVE_PWD_H
+#include <pwd.h>
+#endif
+
+#include <spa/utils/result.h>
+#include <pipewire/context.h>
+#include <pipewire/log.h>
+#include <pipewire/keys.h>
+
+#include "log.h"
+#include "utils.h"
+
+int get_runtime_dir(char *buf, size_t buflen)
+{
+ const char *runtime_dir, *dir = NULL;
+ struct stat stat_buf;
+ int res, size;
+
+ runtime_dir = getenv("PULSE_RUNTIME_PATH");
+ if (runtime_dir == NULL) {
+ runtime_dir = getenv("XDG_RUNTIME_DIR");
+ dir = "pulse";
+ }
+ if (runtime_dir == NULL) {
+ pw_log_error("could not find a suitable runtime directory in"
+ "$PULSE_RUNTIME_PATH and $XDG_RUNTIME_DIR");
+ return -ENOENT;
+ }
+
+ size = snprintf(buf, buflen, "%s%s%s", runtime_dir,
+ dir ? "/" : "", dir ? dir : "");
+ if (size < 0)
+ return -errno;
+ if ((size_t) size >= buflen) {
+ pw_log_error("path %s%s%s too long", runtime_dir,
+ dir ? "/" : "", dir ? dir : "");
+ return -ENAMETOOLONG;
+ }
+
+ if (stat(buf, &stat_buf) < 0) {
+ res = -errno;
+ if (res != -ENOENT) {
+ pw_log_error("stat() %s failed: %m", buf);
+ return res;
+ }
+ if (mkdir(buf, 0700) < 0) {
+ res = -errno;
+ pw_log_error("mkdir() %s failed: %m", buf);
+ return res;
+ }
+ pw_log_info("created %s", buf);
+ } else if (!S_ISDIR(stat_buf.st_mode)) {
+ pw_log_error("%s is not a directory", buf);
+ return -ENOTDIR;
+ }
+ return 0;
+}
+
+int check_flatpak(struct client *client, pid_t pid)
+{
+ char root_path[2048];
+ int root_fd, info_fd, res;
+ struct stat stat_buf;
+
+ sprintf(root_path, "/proc/%ld/root", (long) pid);
+ root_fd = openat(AT_FDCWD, root_path, O_RDONLY | O_NONBLOCK | O_DIRECTORY | O_CLOEXEC | O_NOCTTY);
+ if (root_fd == -1) {
+ res = -errno;
+ if (res == -EACCES) {
+ struct statfs buf;
+ /* Access to the root dir isn't allowed. This can happen if the root is on a fuse
+ * filesystem, such as in a toolbox container. We will never have a fuse rootfs
+ * in the flatpak case, so in that case its safe to ignore this and
+ * continue to detect other types of apps. */
+ if (statfs(root_path, &buf) == 0 &&
+ buf.f_type == 0x65735546) /* FUSE_SUPER_MAGIC */
+ return 0;
+ }
+ /* Not able to open the root dir shouldn't happen. Probably the app died and
+ * we're failing due to /proc/$pid not existing. In that case fail instead
+ * of treating this as privileged. */
+ pw_log_info("failed to open \"%s\"%s", root_path, spa_strerror(res));
+ return res;
+ }
+ info_fd = openat(root_fd, ".flatpak-info", O_RDONLY | O_CLOEXEC | O_NOCTTY);
+ close(root_fd);
+ if (info_fd == -1) {
+ if (errno == ENOENT) {
+ pw_log_debug("no .flatpak-info, client on the host");
+ /* No file => on the host */
+ return 0;
+ }
+ res = -errno;
+ pw_log_error("error opening .flatpak-info: %m");
+ return res;
+ }
+ if (fstat(info_fd, &stat_buf) != 0 || !S_ISREG(stat_buf.st_mode)) {
+ /* Some weird fd => failure, assume sandboxed */
+ pw_log_error("error fstat .flatpak-info: %m");
+ }
+ close(info_fd);
+ return 1;
+}
+
+pid_t get_client_pid(struct client *client, int client_fd)
+{
+ socklen_t len;
+#if defined(__linux__)
+ struct ucred ucred;
+ len = sizeof(ucred);
+ if (getsockopt(client_fd, SOL_SOCKET, SO_PEERCRED, &ucred, &len) < 0) {
+ pw_log_warn("client %p: no peercred: %m", client);
+ } else
+ return ucred.pid;
+#elif defined(__FreeBSD__) || defined(__MidnightBSD__)
+ struct xucred xucred;
+ len = sizeof(xucred);
+ if (getsockopt(client_fd, 0, LOCAL_PEERCRED, &xucred, &len) < 0) {
+ pw_log_warn("client %p: no peercred: %m", client);
+ } else {
+#if __FreeBSD__ >= 13
+ return xucred.cr_pid;
+#endif
+ }
+#endif
+ return 0;
+}
+
+const char *get_server_name(struct pw_context *context)
+{
+ const char *name = NULL;
+ const struct pw_properties *props = pw_context_get_properties(context);
+
+ name = getenv("PIPEWIRE_REMOTE");
+ if ((name == NULL || name[0] == '\0') && props != NULL)
+ name = pw_properties_get(props, PW_KEY_REMOTE_NAME);
+ if (name == NULL || name[0] == '\0')
+ name = PW_DEFAULT_REMOTE;
+ return name;
+}
+
+int create_pid_file(void) {
+ char pid_file[PATH_MAX];
+ FILE *f;
+ int res;
+
+ if ((res = get_runtime_dir(pid_file, sizeof(pid_file))) < 0)
+ return res;
+
+ if (strlen(pid_file) > PATH_MAX - sizeof("/pid")) {
+ pw_log_error("path too long: %s/pid", pid_file);
+ return -ENAMETOOLONG;
+ }
+
+ strcat(pid_file, "/pid");
+
+ if ((f = fopen(pid_file, "we")) == NULL) {
+ res = -errno;
+ pw_log_error("failed to open pid file: %m");
+ return res;
+ }
+
+ fprintf(f, "%lu\n", (unsigned long) getpid());
+ fclose(f);
+
+ return 0;
+}
diff --git a/src/modules/module-protocol-pulse/utils.h b/src/modules/module-protocol-pulse/utils.h
new file mode 100644
index 0000000..fafccf3
--- /dev/null
+++ b/src/modules/module-protocol-pulse/utils.h
@@ -0,0 +1,40 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PULSE_SERVER_UTILS_H
+#define PULSE_SERVER_UTILS_H
+
+#include <stddef.h>
+#include <sys/types.h>
+
+struct client;
+struct pw_context;
+
+int get_runtime_dir(char *buf, size_t buflen);
+int check_flatpak(struct client *client, pid_t pid);
+pid_t get_client_pid(struct client *client, int client_fd);
+const char *get_server_name(struct pw_context *context);
+int create_pid_file(void);
+
+#endif /* PULSE_SERVER_UTILS_H */
diff --git a/src/modules/module-protocol-pulse/volume.c b/src/modules/module-protocol-pulse/volume.c
new file mode 100644
index 0000000..31f45a5
--- /dev/null
+++ b/src/modules/module-protocol-pulse/volume.c
@@ -0,0 +1,114 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <spa/param/props.h>
+#include <spa/param/audio/raw.h>
+#include <spa/pod/iter.h>
+#include <spa/utils/defs.h>
+#include <pipewire/log.h>
+
+#include "log.h"
+#include "volume.h"
+
+int volume_compare(struct volume *vol, struct volume *other)
+{
+ uint8_t i;
+ if (vol->channels != other->channels) {
+ pw_log_info("channels %d<>%d", vol->channels, other->channels);
+ return -1;
+ }
+ for (i = 0; i < vol->channels; i++) {
+ if (vol->values[i] != other->values[i]) {
+ pw_log_info("%d: val %f<>%f", i, vol->values[i], other->values[i]);
+ return -1;
+ }
+ }
+ return 0;
+}
+
+int volume_parse_param(const struct spa_pod *param, struct volume_info *info, bool monitor)
+{
+ struct spa_pod_object *obj = (struct spa_pod_object *) param;
+ struct spa_pod_prop *prop;
+
+ SPA_POD_OBJECT_FOREACH(obj, prop) {
+ switch (prop->key) {
+ case SPA_PROP_volume:
+ if (spa_pod_get_float(&prop->value, &info->level) < 0)
+ continue;
+ SPA_FLAG_UPDATE(info->flags, VOLUME_HW_VOLUME,
+ prop->flags & SPA_POD_PROP_FLAG_HARDWARE);
+
+ break;
+ case SPA_PROP_mute:
+ if (monitor)
+ continue;
+ if (spa_pod_get_bool(&prop->value, &info->mute) < 0)
+ continue;
+ SPA_FLAG_UPDATE(info->flags, VOLUME_HW_MUTE,
+ prop->flags & SPA_POD_PROP_FLAG_HARDWARE);
+ break;
+ case SPA_PROP_channelVolumes:
+ if (monitor)
+ continue;
+ info->volume.channels = spa_pod_copy_array(&prop->value, SPA_TYPE_Float,
+ info->volume.values, SPA_AUDIO_MAX_CHANNELS);
+ SPA_FLAG_UPDATE(info->flags, VOLUME_HW_VOLUME,
+ prop->flags & SPA_POD_PROP_FLAG_HARDWARE);
+ break;
+ case SPA_PROP_monitorMute:
+ if (!monitor)
+ continue;
+ if (spa_pod_get_bool(&prop->value, &info->mute) < 0)
+ continue;
+ SPA_FLAG_CLEAR(info->flags, VOLUME_HW_MUTE);
+ break;
+ case SPA_PROP_monitorVolumes:
+ if (!monitor)
+ continue;
+ info->volume.channels = spa_pod_copy_array(&prop->value, SPA_TYPE_Float,
+ info->volume.values, SPA_AUDIO_MAX_CHANNELS);
+ SPA_FLAG_CLEAR(info->flags, VOLUME_HW_VOLUME);
+ break;
+ case SPA_PROP_volumeBase:
+ if (spa_pod_get_float(&prop->value, &info->base) < 0)
+ continue;
+ break;
+ case SPA_PROP_volumeStep:
+ {
+ float step;
+ if (spa_pod_get_float(&prop->value, &step) >= 0)
+ info->steps = 0x10000u * step;
+ break;
+ }
+ case SPA_PROP_channelMap:
+ info->map.channels = spa_pod_copy_array(&prop->value, SPA_TYPE_Id,
+ info->map.map, SPA_AUDIO_MAX_CHANNELS);
+ break;
+ default:
+ break;
+ }
+ }
+ return 0;
+}
diff --git a/src/modules/module-protocol-pulse/volume.h b/src/modules/module-protocol-pulse/volume.h
new file mode 100644
index 0000000..11ec51a
--- /dev/null
+++ b/src/modules/module-protocol-pulse/volume.h
@@ -0,0 +1,85 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ * Copyright © 2021 Sanchayan Maity <sanchayan@asymptotic.io>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PULSE_SERVER_VOLUME_H
+#define PULSE_SERVER_VOLUME_H
+
+#include <stdbool.h>
+#include <stdint.h>
+
+#include "format.h"
+
+struct spa_pod;
+
+struct volume {
+ uint8_t channels;
+ float values[CHANNELS_MAX];
+};
+
+#define VOLUME_INIT \
+ (struct volume) { \
+ .channels = 0, \
+ }
+
+struct volume_info {
+ struct volume volume;
+ struct channel_map map;
+ bool mute;
+ float level;
+ float base;
+ uint32_t steps;
+#define VOLUME_HW_VOLUME (1<<0)
+#define VOLUME_HW_MUTE (1<<1)
+ uint32_t flags;
+};
+
+#define VOLUME_INFO_INIT \
+ (struct volume_info) { \
+ .volume = VOLUME_INIT, \
+ .mute = false, \
+ .level = 1.0, \
+ .base = 1.0, \
+ .steps = 256, \
+ }
+
+static inline bool volume_valid(const struct volume *vol)
+{
+ if (vol->channels == 0 || vol->channels > CHANNELS_MAX)
+ return false;
+ return true;
+}
+
+static inline void volume_make(struct volume *vol, uint8_t channels)
+{
+ uint8_t i;
+ for (i = 0; i < channels; i++)
+ vol->values[i] = 1.0f;
+ vol->channels = channels;
+}
+
+int volume_compare(struct volume *vol, struct volume *other);
+int volume_parse_param(const struct spa_pod *param, struct volume_info *info, bool monitor);
+
+#endif
diff --git a/src/modules/module-protocol-simple.c b/src/modules/module-protocol-simple.c
new file mode 100644
index 0000000..1e17836
--- /dev/null
+++ b/src/modules/module-protocol-simple.c
@@ -0,0 +1,911 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans <wim.taymans@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <sys/socket.h>
+#include <netinet/in.h>
+#include <netinet/tcp.h>
+#include <netinet/ip.h>
+#include <sys/un.h>
+#include <arpa/inet.h>
+
+#include "config.h"
+
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+#include <spa/utils/json.h>
+#include <spa/pod/pod.h>
+#include <spa/pod/builder.h>
+#include <spa/debug/types.h>
+#include <spa/param/audio/type-info.h>
+#include <spa/param/audio/format-utils.h>
+
+#include <pipewire/impl.h>
+
+/** \page page_module_protocol_simple PipeWire Module: Protocol Simple
+ *
+ * The simple protocol provides a bidirectional audio stream on a network
+ * socket.
+ *
+ * It is meant to be used with the `simple protocol player` app, available on
+ * Android to play and record a stream.
+ *
+ * Each client that connects will create a capture and/or playback stream,
+ * depending on the configuration options.
+ *
+ * ## Module Options
+ *
+ * - `capture`: boolean if capture is enabled. This will create a capture stream
+ * for each connected client.
+ * - `playback`: boolean if playback is enabled. This will create a playback
+ * stream for each connected client.
+ * - `capture.node`: an optional node serial or name to use for capture.
+ * - `playback.node`: an optional node serial or name to use for playback.
+ * - `server.address = []`: an array of server addresses to listen on as
+ * tcp:<ip>:<port>.
+ *
+ * ## General options
+ *
+ * Options with well-known behavior.
+ *
+ * - \ref PW_KEY_REMOTE_NAME
+ * - \ref PW_KEY_AUDIO_RATE
+ * - \ref PW_KEY_AUDIO_FORMAT
+ * - \ref PW_KEY_AUDIO_CHANNELS
+ * - \ref SPA_KEY_AUDIO_POSITION
+ * - \ref PW_KEY_NODE_LATENCY
+ * - \ref PW_KEY_NODE_RATE
+ * - \ref PW_KEY_STREAM_CAPTURE_SINK
+ *
+ * By default the server will work with stereo 16 bits samples at 44.1KHz.
+ *
+ * ## Example configuration
+ *
+ *\code{.unparsed}
+ * context.modules = [
+ * { name = libpipewire-module-protocol-simple
+ * args = {
+ * # Provide capture stream, clients can capture data from PipeWire
+ * capture = true
+ * #
+ * # Provide playback stream, client can send data to PipeWire for playback
+ * playback = true
+ * #
+ * # The node name or id to use for capture.
+ * #capture.node = null
+ * #
+ * # To make the capture stream capture the monitor ports
+ * #stream.capture.sink = false
+ * #
+ * # The node name or id to use for playback.
+ * #playback.node = null
+ * #
+ * #audio.rate = 44100
+ * #audio.format = S16
+ * #audio.channels = 2
+ * #audio.position = [ FL FR ]
+ * #
+ * # The addresses this server listens on for new
+ * # client connections
+ * server.address = [
+ * "tcp:4711"
+ * ]
+ * }
+ * }
+ * ]
+ *\endcode
+ */
+
+#define NAME "protocol-simple"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+#define DEFAULT_PORT 4711
+#define DEFAULT_SERVER "[ \"tcp:"SPA_STRINGIFY(DEFAULT_PORT)"\" ]"
+
+#define DEFAULT_FORMAT "S16"
+#define DEFAULT_RATE 44100
+#define DEFAULT_CHANNELS 2
+#define DEFAULT_POSITION "[ FL FR ]"
+#define DEFAULT_LATENCY "1024/44100"
+
+#define MAX_CLIENTS 10
+
+#define MODULE_USAGE "[ capture=<bool> ] " \
+ "[ playback=<bool> ] " \
+ "[ remote.name=<remote> ] " \
+ "[ node.latency=<num/denom, default:"DEFAULT_LATENCY"> ] " \
+ "[ node.rate=<1/rate, default:1/"SPA_STRINGIFY(DEFAULT_RATE)"> ] " \
+ "[ capture.node=<source-target> [ stream.capture.sink=true ]] " \
+ "[ playback.node=<sink-target> ] " \
+ "[ audio.rate=<sample-rate, default:"SPA_STRINGIFY(DEFAULT_RATE)"> ] " \
+ "[ audio.format=<format, default:"DEFAULT_FORMAT"> ] " \
+ "[ audio.channels=<channels, default: "SPA_STRINGIFY(DEFAULT_CHANNELS)"> ] " \
+ "[ audio.position=<position, default:"DEFAULT_POSITION"> ] " \
+ "[ server.address=<[ tcp:[<ip>:]<port>[,...] ], default:"DEFAULT_SERVER">" \
+
+static const struct spa_dict_item module_props[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Implements a simple protocol" },
+ { PW_KEY_MODULE_USAGE, MODULE_USAGE },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+struct impl {
+ struct pw_loop *loop;
+ struct pw_context *context;
+
+ struct pw_properties *props;
+ struct spa_hook module_listener;
+ struct spa_list server_list;
+
+ struct pw_work_queue *work_queue;
+
+ bool capture;
+ bool playback;
+
+ struct spa_audio_info_raw info;
+ uint32_t frame_size;
+};
+
+struct client {
+ struct spa_list link;
+ struct impl *impl;
+ struct server *server;
+
+ struct pw_core *core;
+ struct spa_hook core_proxy_listener;
+
+ struct spa_source *source;
+ char name[128];
+
+ struct pw_stream *capture;
+ struct spa_hook capture_listener;
+
+ struct pw_stream *playback;
+ struct spa_hook playback_listener;
+
+ unsigned int disconnect:1;
+ unsigned int disconnecting:1;
+ unsigned int cleanup:1;
+};
+
+struct server {
+ struct spa_list link;
+ struct impl *impl;
+
+#define SERVER_TYPE_INVALID 0
+#define SERVER_TYPE_UNIX 1
+#define SERVER_TYPE_INET 2
+ uint32_t type;
+ struct sockaddr_un addr;
+ struct spa_source *source;
+
+ struct spa_list client_list;
+ uint32_t n_clients;
+};
+
+static void client_disconnect(struct client *client)
+{
+ struct impl *impl = client->impl;
+
+ if (client->disconnect)
+ return;
+
+ client->disconnect = true;
+
+ if (client->source)
+ pw_loop_destroy_source(impl->loop, client->source);
+}
+
+static void client_free(struct client *client)
+{
+ struct impl *impl = client->impl;
+
+ pw_log_info("%p: client:%p [%s] free", impl, client, client->name);
+
+ client_disconnect(client);
+
+ pw_work_queue_cancel(impl->work_queue, client, SPA_ID_INVALID);
+
+ spa_list_remove(&client->link);
+ client->server->n_clients--;
+
+ if (client->capture)
+ pw_stream_destroy(client->capture);
+ if (client->playback)
+ pw_stream_destroy(client->playback);
+ if (client->core) {
+ client->disconnecting = true;
+ spa_hook_remove(&client->core_proxy_listener);
+ pw_core_disconnect(client->core);
+ }
+ free(client);
+}
+
+
+static void on_client_cleanup(void *obj, void *data, int res, uint32_t id)
+{
+ struct client *c = obj;
+ client_free(c);
+}
+
+static void client_cleanup(struct client *client)
+{
+ struct impl *impl = client->impl;
+ if (!client->cleanup) {
+ client->cleanup = true;
+ pw_work_queue_add(impl->work_queue, client, 0, on_client_cleanup, impl);
+ }
+}
+
+static void
+on_client_data(void *data, int fd, uint32_t mask)
+{
+ struct client *client = data;
+ struct impl *impl = client->impl;
+ int res;
+
+ if (mask & SPA_IO_HUP) {
+ res = -EPIPE;
+ goto error;
+ }
+ if (mask & SPA_IO_ERR) {
+ res = -EIO;
+ goto error;
+ }
+ return;
+
+error:
+ if (res == -EPIPE)
+ pw_log_info("%p: client:%p [%s] disconnected", impl, client, client->name);
+ else {
+ pw_log_error("%p: client:%p [%s] error %d (%s)", impl,
+ client, client->name, res, spa_strerror(res));
+ }
+ client_cleanup(client);
+}
+
+static void capture_process(void *data)
+{
+ struct client *client = data;
+ struct impl *impl = client->impl;
+ struct pw_buffer *buf;
+ struct spa_data *d;
+ uint32_t size, offset;
+ int res;
+
+ if ((buf = pw_stream_dequeue_buffer(client->capture)) == NULL) {
+ pw_log_debug("%p: client:%p [%s] out of capture buffers: %m", impl,
+ client, client->name);
+ return;
+ }
+ d = &buf->buffer->datas[0];
+
+ offset = SPA_MIN(d->chunk->offset, d->maxsize);
+ size = SPA_MIN(d->chunk->size, d->maxsize - offset);
+
+ while (size > 0) {
+ res = send(client->source->fd,
+ SPA_PTROFF(d->data, offset, void),
+ size,
+ MSG_NOSIGNAL | MSG_DONTWAIT);
+ if (res < 0) {
+ if (errno == EINTR)
+ continue;
+ if (errno != EAGAIN && errno != EWOULDBLOCK)
+ pw_log_warn("%p: client:%p [%s] send error %d: %m", impl,
+ client, client->name, res);
+ client_cleanup(client);
+ break;
+ }
+ offset += res;
+ size -= res;
+ }
+ pw_stream_queue_buffer(client->capture, buf);
+}
+
+static void playback_process(void *data)
+{
+ struct client *client = data;
+ struct impl *impl = client->impl;
+ struct pw_buffer *buf;
+ uint32_t size, offset;
+ struct spa_data *d;
+ int res;
+
+ if ((buf = pw_stream_dequeue_buffer(client->playback)) == NULL) {
+ pw_log_debug("%p: client:%p [%s] out of playback buffers: %m", impl,
+ client, client->name);
+ return;
+ }
+ d = &buf->buffer->datas[0];
+
+ size = d->maxsize;
+ if (buf->requested)
+ size = SPA_MIN(size, buf->requested * impl->frame_size);
+
+ offset = 0;
+ while (size > 0) {
+ res = recv(client->source->fd,
+ SPA_PTROFF(d->data, offset, void),
+ size,
+ MSG_DONTWAIT);
+ if (res == 0) {
+ pw_log_info("%p: client:%p [%s] disconnect", impl,
+ client, client->name);
+ client_cleanup(client);
+ break;
+ }
+ if (res < 0) {
+ if (errno == EINTR)
+ continue;
+ if (errno != EAGAIN && errno != EWOULDBLOCK)
+ pw_log_warn("%p: client:%p [%s] recv error %d: %m",
+ impl, client, client->name, res);
+ break;
+ }
+ offset += res;
+ size -= res;
+ }
+ d->chunk->offset = 0;
+ d->chunk->size = offset;
+ d->chunk->stride = impl->frame_size;
+
+ pw_stream_queue_buffer(client->playback, buf);
+}
+
+static void capture_destroy(void *data)
+{
+ struct client *client = data;
+ spa_hook_remove(&client->capture_listener);
+ client->capture = NULL;
+}
+
+static void on_stream_state_changed(void *data, enum pw_stream_state old,
+ enum pw_stream_state state, const char *error)
+{
+ struct client *client = data;
+ struct impl *impl = client->impl;
+
+ switch (state) {
+ case PW_STREAM_STATE_ERROR:
+ case PW_STREAM_STATE_UNCONNECTED:
+ if (!client->disconnect) {
+ pw_log_info("%p: client:%p [%s] stream error %s",
+ impl, client, client->name,
+ pw_stream_state_as_string(state));
+ client_cleanup(client);
+ }
+ break;
+ default:
+ break;
+ }
+}
+
+static void playback_destroy(void *data)
+{
+ struct client *client = data;
+ spa_hook_remove(&client->playback_listener);
+ client->playback = NULL;
+}
+
+static const struct pw_stream_events capture_stream_events = {
+ PW_VERSION_STREAM_EVENTS,
+ .destroy = capture_destroy,
+ .state_changed = on_stream_state_changed,
+ .process = capture_process
+};
+
+static const struct pw_stream_events playback_stream_events = {
+ PW_VERSION_STREAM_EVENTS,
+ .destroy = playback_destroy,
+ .state_changed = on_stream_state_changed,
+ .process = playback_process
+};
+
+static int create_streams(struct impl *impl, struct client *client)
+{
+ uint32_t n_params;
+ const struct spa_pod *params[1];
+ uint8_t buffer[1024];
+ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
+ struct pw_properties *props;
+ const char *latency;
+ int res;
+
+ if ((latency = pw_properties_get(impl->props, PW_KEY_NODE_LATENCY)) == NULL)
+ latency = DEFAULT_LATENCY;
+
+ if (impl->capture) {
+ props = pw_properties_new(
+ PW_KEY_NODE_LATENCY, latency,
+ PW_KEY_NODE_RATE, pw_properties_get(impl->props, PW_KEY_NODE_RATE),
+ PW_KEY_TARGET_OBJECT, pw_properties_get(impl->props, "capture.node"),
+ PW_KEY_STREAM_CAPTURE_SINK, pw_properties_get(impl->props,
+ PW_KEY_STREAM_CAPTURE_SINK),
+ PW_KEY_NODE_NETWORK, "true",
+ NULL);
+ if (props == NULL)
+ return -errno;
+
+ pw_properties_setf(props,
+ PW_KEY_MEDIA_NAME, "%s capture", client->name);
+ client->capture = pw_stream_new(client->core,
+ pw_properties_get(props, PW_KEY_MEDIA_NAME),
+ props);
+ if (client->capture == NULL)
+ return -errno;
+
+ pw_stream_add_listener(client->capture, &client->capture_listener,
+ &capture_stream_events, client);
+ }
+ if (impl->playback) {
+ props = pw_properties_new(
+ PW_KEY_NODE_LATENCY, latency,
+ PW_KEY_NODE_RATE, pw_properties_get(impl->props, PW_KEY_NODE_RATE),
+ PW_KEY_TARGET_OBJECT, pw_properties_get(impl->props, "playback.node"),
+ PW_KEY_NODE_NETWORK, "true",
+ NULL);
+ if (props == NULL)
+ return -errno;
+
+ pw_properties_setf(props,
+ PW_KEY_MEDIA_NAME, "%s playback", client->name);
+
+ client->playback = pw_stream_new(client->core,
+ pw_properties_get(props, PW_KEY_MEDIA_NAME),
+ props);
+ if (client->playback == NULL)
+ return -errno;
+
+ pw_stream_add_listener(client->playback, &client->playback_listener,
+ &playback_stream_events, client);
+ }
+
+ n_params = 0;
+ params[n_params++] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat,
+ &impl->info);
+
+ if (impl->capture) {
+ if ((res = pw_stream_connect(client->capture,
+ PW_DIRECTION_INPUT,
+ PW_ID_ANY,
+ PW_STREAM_FLAG_AUTOCONNECT |
+ PW_STREAM_FLAG_MAP_BUFFERS |
+ PW_STREAM_FLAG_RT_PROCESS,
+ params, n_params)) < 0)
+ return res;
+ }
+ if (impl->playback) {
+ if ((res = pw_stream_connect(client->playback,
+ PW_DIRECTION_OUTPUT,
+ PW_ID_ANY,
+ PW_STREAM_FLAG_AUTOCONNECT |
+ PW_STREAM_FLAG_MAP_BUFFERS |
+ PW_STREAM_FLAG_RT_PROCESS,
+ params, n_params)) < 0)
+ return res;
+ }
+ return 0;
+}
+
+static void on_core_proxy_destroy(void *data)
+{
+ struct client *client = data;
+ spa_hook_remove(&client->core_proxy_listener);
+ client->core = NULL;
+ client_cleanup(client);
+}
+
+static const struct pw_proxy_events core_proxy_events = {
+ PW_VERSION_CORE_EVENTS,
+ .destroy = on_core_proxy_destroy,
+};
+
+static void
+on_connect(void *data, int fd, uint32_t mask)
+{
+ struct server *server = data;
+ struct impl *impl = server->impl;
+ struct sockaddr_in addr;
+ socklen_t addrlen;
+ int client_fd, val;
+ struct client *client = NULL;
+ struct pw_properties *props = NULL;
+
+ addrlen = sizeof(addr);
+ client_fd = accept4(fd, &addr, &addrlen, SOCK_NONBLOCK | SOCK_CLOEXEC);
+ if (client_fd < 0)
+ goto error;
+
+ if (server->n_clients >= MAX_CLIENTS) {
+ close(client_fd);
+ errno = ECONNREFUSED;
+ goto error;
+ }
+
+ client = calloc(1, sizeof(struct client));
+ if (client == NULL)
+ goto error;
+
+ client->impl = impl;
+ client->server = server;
+ spa_list_append(&server->client_list, &client->link);
+ server->n_clients++;
+
+ if (inet_ntop(addr.sin_family, &addr.sin_addr.s_addr, client->name, sizeof(client->name)) == NULL)
+ snprintf(client->name, sizeof(client->name), "client %d", client_fd);
+
+ client->source = pw_loop_add_io(impl->loop,
+ client_fd,
+ SPA_IO_ERR | SPA_IO_HUP,
+ true, on_client_data, client);
+ if (client->source == NULL)
+ goto error;
+
+ pw_log_info("%p: client:%p [%s] connected", impl, client, client->name);
+
+ props = pw_properties_new(
+ PW_KEY_CLIENT_API, "protocol-simple",
+ PW_KEY_REMOTE_NAME,
+ pw_properties_get(impl->props, PW_KEY_REMOTE_NAME),
+ NULL);
+ if (props == NULL)
+ goto error;
+
+ pw_properties_setf(props,
+ "protocol.server.type", "%s",
+ server->type == SERVER_TYPE_INET ? "tcp" : "unix");
+
+ if (server->type == SERVER_TYPE_UNIX) {
+ goto error;
+ } else if (server->type == SERVER_TYPE_INET) {
+ val = 1;
+ if (setsockopt(client_fd, IPPROTO_TCP, TCP_NODELAY,
+ (const void *) &val, sizeof(val)) < 0)
+ pw_log_warn("TCP_NODELAY failed: %m");
+
+ val = IPTOS_LOWDELAY;
+ if (setsockopt(client_fd, IPPROTO_IP, IP_TOS,
+ (const void *) &val, sizeof(val)) < 0)
+ pw_log_warn("IP_TOS failed: %m");
+
+ pw_properties_set(props, PW_KEY_CLIENT_ACCESS, "restricted");
+ }
+
+ client->core = pw_context_connect(impl->context, props, 0);
+ props = NULL;
+ if (client->core == NULL)
+ goto error;
+
+ pw_proxy_add_listener((struct pw_proxy*)client->core,
+ &client->core_proxy_listener, &core_proxy_events,
+ client);
+
+ create_streams(impl, client);
+
+ return;
+error:
+ pw_log_error("%p: failed to create client: %m", impl);
+ pw_properties_free(props);
+ if (client != NULL)
+ client_free(client);
+ return;
+}
+
+static int make_inet_socket(struct server *server, const char *name)
+{
+ struct sockaddr_in addr;
+ int res, fd, on;
+ uint32_t address = INADDR_ANY;
+ uint16_t port;
+ char *col;
+
+ col = strchr(name, ':');
+ if (col) {
+ struct in_addr ipv4;
+ char *n;
+ port = atoi(col+1);
+ n = strndupa(name, col - name);
+ if (inet_pton(AF_INET, n, &ipv4) > 0)
+ address = ntohl(ipv4.s_addr);
+ else
+ address = INADDR_ANY;
+ } else {
+ address = INADDR_ANY;
+ port = atoi(name);
+ }
+ if (port == 0)
+ port = DEFAULT_PORT;
+
+ if ((fd = socket(PF_INET, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0)) < 0) {
+ res = -errno;
+ pw_log_error("%p: socket() failed: %m", server);
+ goto error;
+ }
+
+ on = 1;
+ if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (const void *) &on, sizeof(on)) < 0)
+ pw_log_warn("%p: setsockopt(): %m", server);
+
+ spa_zero(addr);
+ addr.sin_family = AF_INET;
+ addr.sin_port = htons(port);
+ addr.sin_addr.s_addr = htonl(address);
+
+ if (bind(fd, (struct sockaddr *) &addr, sizeof(addr)) < 0) {
+ res = -errno;
+ pw_log_error("%p: bind() failed: %m", server);
+ goto error_close;
+ }
+ if (listen(fd, 5) < 0) {
+ res = -errno;
+ pw_log_error("%p: listen() failed: %m", server);
+ goto error_close;
+ }
+ server->type = SERVER_TYPE_INET;
+ pw_log_info("listening on tcp:%08x:%u", address, port);
+
+ return fd;
+
+error_close:
+ close(fd);
+error:
+ return res;
+}
+
+static void server_free(struct server *server)
+{
+ struct impl *impl = server->impl;
+ struct client *c;
+
+ pw_log_debug("%p: free server %p", impl, server);
+
+ spa_list_remove(&server->link);
+ spa_list_consume(c, &server->client_list, link)
+ client_free(c);
+ if (server->source)
+ pw_loop_destroy_source(impl->loop, server->source);
+ free(server);
+}
+
+static struct server *create_server(struct impl *impl, const char *address)
+{
+ int fd, res;
+ struct server *server;
+
+ server = calloc(1, sizeof(struct server));
+ if (server == NULL)
+ return NULL;
+
+ server->impl = impl;
+ spa_list_init(&server->client_list);
+ spa_list_append(&impl->server_list, &server->link);
+
+ if (spa_strstartswith(address, "tcp:")) {
+ fd = make_inet_socket(server, address+4);
+ } else {
+ fd = -EINVAL;
+ }
+ if (fd < 0) {
+ res = fd;
+ goto error;
+ }
+ server->source = pw_loop_add_io(impl->loop, fd, SPA_IO_IN, true, on_connect, server);
+ if (server->source == NULL) {
+ res = -errno;
+ pw_log_error("%p: can't create server source: %m", impl);
+ goto error_close;
+ }
+ return server;
+
+error_close:
+ close(fd);
+error:
+ server_free(server);
+ errno = -res;
+ return NULL;
+}
+
+static void impl_free(struct impl *impl)
+{
+ struct server *s;
+
+ spa_hook_remove(&impl->module_listener);
+ spa_list_consume(s, &impl->server_list, link)
+ server_free(s);
+ pw_properties_free(impl->props);
+ free(impl);
+}
+
+static inline uint32_t format_from_name(const char *name, size_t len)
+{
+ int i;
+ for (i = 0; spa_type_audio_format[i].name; i++) {
+ if (strncmp(name, spa_debug_type_short_name(spa_type_audio_format[i].name), len) == 0)
+ return spa_type_audio_format[i].type;
+ }
+ return SPA_AUDIO_FORMAT_UNKNOWN;
+}
+
+static inline uint32_t channel_from_name(const char *name)
+{
+ int i;
+ for (i = 0; spa_type_audio_channel[i].name; i++) {
+ if (spa_streq(name, spa_debug_type_short_name(spa_type_audio_channel[i].name)))
+ return spa_type_audio_channel[i].type;
+ }
+ return SPA_AUDIO_CHANNEL_UNKNOWN;
+}
+
+static inline uint32_t parse_position(uint32_t *pos, const char *val, size_t len)
+{
+ uint32_t channels = 0;
+ struct spa_json it[2];
+ char v[256];
+
+ spa_json_init(&it[0], val, len);
+ if (spa_json_enter_array(&it[0], &it[1]) <= 0)
+ spa_json_init(&it[1], val, len);
+
+ while (spa_json_get_string(&it[1], v, sizeof(v)) > 0 &&
+ channels < SPA_AUDIO_MAX_CHANNELS) {
+ pos[channels++] = channel_from_name(v);
+ }
+ return channels;
+}
+
+static int parse_params(struct impl *impl)
+{
+ const char *str;
+ struct spa_json it[2];
+ char value[512];
+
+ pw_properties_fetch_bool(impl->props, "capture", &impl->capture);
+ pw_properties_fetch_bool(impl->props, "playback", &impl->playback);
+ if (!impl->playback && !impl->capture) {
+ pw_log_error("missing capture or playback param");
+ return -EINVAL;
+ }
+
+ if ((str = pw_properties_get(impl->props, "audio.format")) == NULL)
+ str = DEFAULT_FORMAT;
+ impl->info.format = format_from_name(str, strlen(str));
+ if (impl->info.format == SPA_AUDIO_FORMAT_UNKNOWN) {
+ pw_log_error("invalid format '%s'", str);
+ return -EINVAL;
+ }
+ impl->info.rate = pw_properties_get_uint32(impl->props, "audio.rate", DEFAULT_RATE);
+ if (impl->info.rate == 0) {
+ pw_log_error("invalid rate '%s'", str);
+ return -EINVAL;
+ }
+ impl->info.channels = pw_properties_get_uint32(impl->props, "audio.channels", DEFAULT_CHANNELS);
+ if (impl->info.channels == 0) {
+ pw_log_error("invalid channels '%s'", str);
+ return -EINVAL;
+ }
+ if ((str = pw_properties_get(impl->props, "audio.position")) == NULL)
+ str = DEFAULT_POSITION;
+ if (parse_position(impl->info.position, str, strlen(str)) != impl->info.channels) {
+ pw_log_error("invalid position '%s'", str);
+ return -EINVAL;
+ }
+
+ switch (impl->info.format) {
+ case SPA_AUDIO_FORMAT_U8:
+ impl->frame_size = 1;
+ break;
+ case SPA_AUDIO_FORMAT_S16_LE:
+ case SPA_AUDIO_FORMAT_S16_BE:
+ case SPA_AUDIO_FORMAT_S16P:
+ impl->frame_size = 2;
+ break;
+ case SPA_AUDIO_FORMAT_S24_LE:
+ case SPA_AUDIO_FORMAT_S24_BE:
+ case SPA_AUDIO_FORMAT_S24P:
+ impl->frame_size = 3;
+ break;
+ default:
+ impl->frame_size = 4;
+ break;
+ }
+ impl->frame_size *= impl->info.channels;
+
+ if ((str = pw_properties_get(impl->props, "server.address")) == NULL)
+ str = DEFAULT_SERVER;
+
+ spa_json_init(&it[0], str, strlen(str));
+ if (spa_json_enter_array(&it[0], &it[1]) > 0) {
+ while (spa_json_get_string(&it[1], value, sizeof(value)) > 0) {
+ if (create_server(impl, value) == NULL) {
+ pw_log_warn("%p: can't create server for %s: %m",
+ impl, value);
+ }
+ }
+ }
+ return 0;
+}
+
+static void module_destroy(void *data)
+{
+ struct impl *impl = data;
+ pw_log_debug("module %p: destroy", impl);
+ impl_free(impl);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy,
+};
+
+SPA_EXPORT
+int pipewire__module_init(struct pw_impl_module *module, const char *args)
+{
+ struct pw_context *context = pw_impl_module_get_context(module);
+ struct pw_properties *props;
+ struct impl *impl;
+ int res;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ impl = calloc(1, sizeof(struct impl));
+ if (impl == NULL)
+ return -errno;
+
+ pw_log_debug("module %p: new %s", impl, args);
+
+ if (args)
+ props = pw_properties_new_string(args);
+ else
+ props = pw_properties_new(NULL, NULL);
+
+ impl->context = context;
+ impl->loop = pw_context_get_main_loop(context);
+ impl->props = props;
+ spa_list_init(&impl->server_list);
+
+ pw_impl_module_add_listener(module, &impl->module_listener, &module_events, impl);
+
+ pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_props));
+
+ impl->work_queue = pw_context_get_work_queue(context);
+
+ if ((res = parse_params(impl)) < 0)
+ goto error_free;
+
+ return 0;
+
+error_free:
+ impl_free(impl);
+ return res;
+}
diff --git a/src/modules/module-pulse-tunnel.c b/src/modules/module-pulse-tunnel.c
new file mode 100644
index 0000000..e2fe8b6
--- /dev/null
+++ b/src/modules/module-pulse-tunnel.c
@@ -0,0 +1,1080 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <signal.h>
+#include <limits.h>
+#include <math.h>
+
+#include "config.h"
+
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+#include <spa/utils/json.h>
+#include <spa/utils/ringbuffer.h>
+#include <spa/utils/dll.h>
+#include <spa/debug/types.h>
+#include <spa/pod/builder.h>
+#include <spa/param/audio/format-utils.h>
+#include <spa/param/latency-utils.h>
+#include <spa/param/audio/raw.h>
+
+#include <pipewire/impl.h>
+#include <pipewire/i18n.h>
+#include <pipewire/private.h>
+
+#include <pulse/pulseaudio.h>
+#include "module-protocol-pulse/format.h"
+
+/** \page page_module_pulse_tunnel PipeWire Module: Pulse Tunnel
+ *
+ * The pulse-tunnel module provides a source or sink that tunnels all audio to
+ * a remote PulseAudio connection.
+ *
+ * It is usually used with the PulseAudio or module-protocol-pulse on the remote
+ * end to accept the connection.
+ *
+ * This module is usually used together with module-zeroconf-discover that will
+ * automatically load the tunnel with the right parameters based on zeroconf
+ * information.
+ *
+ * ## Module Options
+ *
+ * - `tunnel.mode`: the desired tunnel to create, must be `source` or `sink`.
+ * (Default `sink`)
+ * - `pulse.server.address`: the address of the PulseAudio server to tunnel to.
+ * - `pulse.latency`: the latency to end-to-end latency in milliseconds to
+ * maintain (Default 200).
+ * - `stream.props`: Extra properties for the local stream.
+ *
+ * ## General options
+ *
+ * Options with well-known behavior.
+ *
+ * - \ref PW_KEY_REMOTE_NAME
+ * - \ref PW_KEY_AUDIO_FORMAT
+ * - \ref PW_KEY_AUDIO_RATE
+ * - \ref PW_KEY_AUDIO_CHANNELS
+ * - \ref SPA_KEY_AUDIO_POSITION
+ * - \ref PW_KEY_NODE_LATENCY
+ * - \ref PW_KEY_NODE_NAME
+ * - \ref PW_KEY_NODE_DESCRIPTION
+ * - \ref PW_KEY_NODE_GROUP
+ * - \ref PW_KEY_NODE_VIRTUAL
+ * - \ref PW_KEY_MEDIA_CLASS
+ * - \ref PW_KEY_TARGET_OBJECT to specify the remote node.name or serial.id to link to
+ *
+ * ## Example configuration of a virtual sink
+ *
+ *\code{.unparsed}
+ * context.modules = [
+ * { name = libpipewire-module-pulse-tunnel
+ * args = {
+ * tunnel.mode = sink
+ * # Set the remote address to tunnel to
+ * pulse.server.address = "tcp:192.168.1.126"
+ * #pulse.latency = 200
+ * #audio.rate=<sample rate>
+ * #audio.channels=<number of channels>
+ * #audio.position=<channel map>
+ * #target.object=<remote target name>
+ * stream.props = {
+ * # extra sink properties
+ * }
+ * }
+ * }
+ * ]
+ *\endcode
+ */
+
+#define NAME "pulse-tunnel"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+#define DEFAULT_FORMAT "S16"
+#define DEFAULT_RATE 48000
+#define DEFAULT_CHANNELS 2
+#define DEFAULT_POSITION "[ FL FR ]"
+
+#define MODULE_USAGE "[ remote.name=<remote> ] " \
+ "[ node.latency=<latency as fraction> ] " \
+ "[ node.name=<name of the nodes> ] " \
+ "[ node.description=<description of the nodes> ] " \
+ "[ node.target=<remote node target name or serial> ] " \
+ "[ audio.format=<sample format> ] " \
+ "[ audio.rate=<sample rate> ] " \
+ "[ audio.channels=<number of channels> ] " \
+ "[ audio.position=<channel map> ] " \
+ "pulse.server.address=<address> " \
+ "pulse.latency=<latency in msec> " \
+ "[ tunnel.mode=source|sink " \
+ "[ stream.props=<properties> ] "
+
+
+static const struct spa_dict_item module_props[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Create a PulseAudio tunnel" },
+ { PW_KEY_MODULE_USAGE, MODULE_USAGE },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+#define RINGBUFFER_SIZE (1u << 22)
+#define RINGBUFFER_MASK (RINGBUFFER_SIZE-1)
+
+#define DEFAULT_LATENCY_MSEC (200)
+
+struct impl {
+ struct pw_context *context;
+
+#define MODE_SINK 0
+#define MODE_SOURCE 1
+ uint32_t mode;
+ struct pw_properties *props;
+
+ struct pw_impl_module *module;
+
+ struct spa_hook module_listener;
+
+ struct pw_core *core;
+ struct spa_hook core_proxy_listener;
+ struct spa_hook core_listener;
+
+ uint32_t latency_msec;
+
+ struct pw_properties *stream_props;
+ struct pw_stream *stream;
+ struct spa_hook stream_listener;
+ struct spa_audio_info_raw info;
+ uint32_t frame_size;
+
+ struct spa_ringbuffer ring;
+ void *buffer;
+ uint8_t empty[8192];
+
+ pa_threaded_mainloop *pa_mainloop;
+ pa_context *pa_context;
+ pa_stream *pa_stream;
+
+ struct ratelimit rate_limit;
+
+ uint32_t target_latency;
+ uint32_t current_latency;
+ uint32_t target_buffer;
+ struct spa_io_rate_match *rate_match;
+ struct spa_dll dll;
+ float max_error;
+ unsigned resync:1;
+
+ unsigned int do_disconnect:1;
+};
+
+static void cork_stream(struct impl *impl, bool cork)
+{
+ pa_operation *operation;
+
+ pa_threaded_mainloop_lock(impl->pa_mainloop);
+
+ pw_log_debug("corking: %d", cork);
+ if (cork && impl->mode == MODE_SINK) {
+ /* When the sink becomes suspended (which is the only case where we
+ * cork the stream), we don't want to keep any old data around, because
+ * the old data is most likely unrelated to the audio that will be
+ * played at the time when the sink starts running again. */
+ if ((operation = pa_stream_flush(impl->pa_stream, NULL, NULL)))
+ pa_operation_unref(operation);
+
+ spa_ringbuffer_init(&impl->ring);
+ memset(impl->buffer, 0, RINGBUFFER_SIZE);
+ }
+ if (!cork)
+ impl->resync = true;
+
+ if ((operation = pa_stream_cork(impl->pa_stream, cork, NULL, NULL)))
+ pa_operation_unref(operation);
+
+ pa_threaded_mainloop_unlock(impl->pa_mainloop);
+}
+
+static void stream_destroy(void *d)
+{
+ struct impl *impl = d;
+ spa_hook_remove(&impl->stream_listener);
+ impl->stream = NULL;
+}
+
+static void stream_state_changed(void *d, enum pw_stream_state old,
+ enum pw_stream_state state, const char *error)
+{
+ struct impl *impl = d;
+ switch (state) {
+ case PW_STREAM_STATE_ERROR:
+ case PW_STREAM_STATE_UNCONNECTED:
+ pw_impl_module_schedule_destroy(impl->module);
+ break;
+ case PW_STREAM_STATE_PAUSED:
+ cork_stream(impl, true);
+ break;
+ case PW_STREAM_STATE_STREAMING:
+ cork_stream(impl, false);
+ break;
+ default:
+ break;
+ }
+}
+
+static void update_rate(struct impl *impl, bool playback)
+{
+ float error, corr;
+
+ if (impl->rate_match == NULL)
+ return;
+
+ if (playback)
+ error = (float)impl->target_latency - (float)impl->current_latency;
+ else
+ error = (float)impl->current_latency - (float)impl->target_latency;
+ error = SPA_CLAMP(error, -impl->max_error, impl->max_error);
+
+ corr = spa_dll_update(&impl->dll, error);
+ pw_log_debug("error:%f corr:%f current:%u target:%u",
+ error, corr,
+ impl->current_latency, impl->target_latency);
+
+ SPA_FLAG_SET(impl->rate_match->flags, SPA_IO_RATE_MATCH_FLAG_ACTIVE);
+ impl->rate_match->rate = 1.0f / corr;
+}
+
+static void playback_stream_process(void *d)
+{
+ struct impl *impl = d;
+ struct pw_buffer *buf;
+ struct spa_data *bd;
+ int32_t filled;
+ uint32_t write_index, offs, size;
+
+ if ((buf = pw_stream_dequeue_buffer(impl->stream)) == NULL) {
+ pw_log_debug("out of buffers: %m");
+ return;
+ }
+
+ bd = &buf->buffer->datas[0];
+ offs = SPA_MIN(bd->chunk->offset, bd->maxsize);
+ size = SPA_MIN(bd->chunk->size, bd->maxsize - offs);
+ size = SPA_MIN(size, RINGBUFFER_SIZE);
+
+ filled = spa_ringbuffer_get_write_index(&impl->ring, &write_index);
+
+ if (filled < 0) {
+ pw_log_warn("%p: underrun write:%u filled:%d",
+ impl, write_index, filled);
+ } else if ((uint32_t)filled + size > RINGBUFFER_SIZE) {
+ pw_log_warn("%p: overrun write:%u filled:%d + size:%u > max:%u",
+ impl, write_index, filled,
+ size, RINGBUFFER_SIZE);
+ impl->resync = true;
+ } else {
+ update_rate(impl, true);
+ }
+ spa_ringbuffer_write_data(&impl->ring,
+ impl->buffer, RINGBUFFER_SIZE,
+ write_index & RINGBUFFER_MASK,
+ SPA_PTROFF(bd->data, offs, void),
+ size);
+ write_index += size;
+ spa_ringbuffer_write_update(&impl->ring, write_index);
+
+ pw_stream_queue_buffer(impl->stream, buf);
+}
+
+static void capture_stream_process(void *d)
+{
+ struct impl *impl = d;
+ struct pw_buffer *buf;
+ struct spa_data *bd;
+ int32_t avail;
+ uint32_t size, req, index;
+
+ if ((buf = pw_stream_dequeue_buffer(impl->stream)) == NULL) {
+ pw_log_debug("out of buffers: %m");
+ return;
+ }
+
+ bd = &buf->buffer->datas[0];
+
+ if ((req = buf->requested * impl->frame_size) == 0)
+ req = 4096 * impl->frame_size;
+
+ size = SPA_MIN(bd->maxsize, req);
+
+ avail = spa_ringbuffer_get_read_index(&impl->ring, &index);
+ if (avail < (int32_t)size) {
+ memset(bd->data, 0, size);
+ } else {
+ if (avail > (int32_t)RINGBUFFER_SIZE) {
+ avail = impl->target_buffer;
+ index += avail - impl->target_buffer;
+ } else {
+ update_rate(impl, false);
+ }
+ spa_ringbuffer_read_data(&impl->ring,
+ impl->buffer, RINGBUFFER_SIZE,
+ index & RINGBUFFER_MASK,
+ bd->data, size);
+
+ index += size;
+ spa_ringbuffer_read_update(&impl->ring, index);
+ }
+ bd->chunk->offset = 0;
+ bd->chunk->size = size;
+ bd->chunk->stride = impl->frame_size;
+
+ pw_stream_queue_buffer(impl->stream, buf);
+}
+
+static void stream_io_changed(void *data, uint32_t id, void *area, uint32_t size)
+{
+ struct impl *impl = data;
+ switch (id) {
+ case SPA_IO_RateMatch:
+ impl->rate_match = area;
+ break;
+ }
+}
+
+static const struct pw_stream_events playback_stream_events = {
+ PW_VERSION_STREAM_EVENTS,
+ .destroy = stream_destroy,
+ .state_changed = stream_state_changed,
+ .io_changed = stream_io_changed,
+ .process = playback_stream_process
+};
+
+static const struct pw_stream_events capture_stream_events = {
+ PW_VERSION_STREAM_EVENTS,
+ .destroy = stream_destroy,
+ .state_changed = stream_state_changed,
+ .io_changed = stream_io_changed,
+ .process = capture_stream_process
+};
+
+static int create_stream(struct impl *impl)
+{
+ int res;
+ uint32_t n_params;
+ const struct spa_pod *params[2];
+ uint8_t buffer[1024];
+ struct spa_pod_builder b;
+ struct spa_latency_info latency;
+
+ impl->stream = pw_stream_new(impl->core, "pulse", impl->stream_props);
+ impl->stream_props = NULL;
+
+ if (impl->stream == NULL)
+ return -errno;
+
+ if (impl->mode == MODE_SOURCE) {
+ pw_stream_add_listener(impl->stream,
+ &impl->stream_listener,
+ &capture_stream_events, impl);
+ } else {
+ pw_stream_add_listener(impl->stream,
+ &impl->stream_listener,
+ &playback_stream_events, impl);
+ }
+
+ n_params = 0;
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ params[n_params++] = spa_format_audio_raw_build(&b,
+ SPA_PARAM_EnumFormat, &impl->info);
+
+ spa_zero(latency);
+ latency.direction = impl->mode == MODE_SOURCE ? PW_DIRECTION_OUTPUT : PW_DIRECTION_INPUT;
+ latency.min_ns = latency.max_ns = impl->latency_msec * SPA_NSEC_PER_MSEC;
+
+ params[n_params++] = spa_latency_build(&b,
+ SPA_PARAM_Latency, &latency);
+
+ if ((res = pw_stream_connect(impl->stream,
+ impl->mode == MODE_SOURCE ? PW_DIRECTION_OUTPUT : PW_DIRECTION_INPUT,
+ PW_ID_ANY,
+ PW_STREAM_FLAG_AUTOCONNECT |
+ PW_STREAM_FLAG_MAP_BUFFERS |
+ PW_STREAM_FLAG_RT_PROCESS,
+ params, n_params)) < 0)
+ return res;
+
+ return 0;
+}
+
+static void context_state_cb(pa_context *c, void *userdata)
+{
+ struct impl *impl = userdata;
+ bool do_destroy = false;
+ switch (pa_context_get_state(c)) {
+ case PA_CONTEXT_TERMINATED:
+ case PA_CONTEXT_FAILED:
+ do_destroy = true;
+ SPA_FALLTHROUGH;
+ case PA_CONTEXT_READY:
+ pa_threaded_mainloop_signal(impl->pa_mainloop, 0);
+ break;
+ case PA_CONTEXT_UNCONNECTED:
+ do_destroy = true;
+ break;
+ case PA_CONTEXT_CONNECTING:
+ case PA_CONTEXT_AUTHORIZING:
+ case PA_CONTEXT_SETTING_NAME:
+ break;
+ }
+ if (do_destroy)
+ pw_impl_module_schedule_destroy(impl->module);
+}
+
+static void stream_state_cb(pa_stream *s, void * userdata)
+{
+ struct impl *impl = userdata;
+ bool do_destroy = false;
+ switch (pa_stream_get_state(s)) {
+ case PA_STREAM_FAILED:
+ case PA_STREAM_TERMINATED:
+ do_destroy = true;
+ SPA_FALLTHROUGH;
+ case PA_STREAM_READY:
+ pa_threaded_mainloop_signal(impl->pa_mainloop, 0);
+ break;
+ case PA_STREAM_UNCONNECTED:
+ do_destroy = true;
+ break;
+ case PA_STREAM_CREATING:
+ break;
+ }
+ if (do_destroy)
+ pw_impl_module_schedule_destroy(impl->module);
+}
+
+static void stream_read_request_cb(pa_stream *s, size_t length, void *userdata)
+{
+ struct impl *impl = userdata;
+ int32_t filled;
+ uint32_t index;
+ pa_usec_t latency;
+ int negative;
+
+ filled = spa_ringbuffer_get_write_index(&impl->ring, &index);
+
+ if (filled < 0) {
+ pw_log_warn("%p: underrun write:%u filled:%d",
+ impl, index, filled);
+ } else if (filled + length > RINGBUFFER_SIZE) {
+ pw_log_warn("%p: overrun write:%u filled:%d",
+ impl, index, filled);
+ }
+ while (length > 0) {
+ const void *p;
+ size_t nbytes = 0;
+
+ if (SPA_UNLIKELY(pa_stream_peek(impl->pa_stream, &p, &nbytes) != 0)) {
+ pw_log_error("pa_stream_peek() failed: %s",
+ pa_strerror(pa_context_errno(impl->pa_context)));
+ return;
+ }
+ pw_log_debug("read %zd nbytes:%zd", length, nbytes);
+
+ if (length < nbytes)
+ break;
+
+ while (nbytes > 0) {
+ uint32_t to_write = SPA_MIN(nbytes, sizeof(impl->empty));
+
+ spa_ringbuffer_write_data(&impl->ring,
+ impl->buffer, RINGBUFFER_SIZE,
+ index & RINGBUFFER_MASK,
+ p ? p : impl->empty, to_write);
+
+ index += to_write;
+ p = p ? SPA_PTROFF(p, to_write, void) : NULL;
+ nbytes -= to_write;
+ length -= to_write;
+ filled += to_write;
+ }
+ pa_stream_drop(impl->pa_stream);
+ }
+
+ pa_stream_get_latency(impl->pa_stream, &latency, &negative);
+ impl->current_latency = latency * impl->info.rate / SPA_USEC_PER_SEC;
+ impl->current_latency += filled / impl->frame_size;
+
+ spa_ringbuffer_write_update(&impl->ring, index);
+}
+
+static void stream_write_request_cb(pa_stream *s, size_t length, void *userdata)
+{
+ struct impl *impl = userdata;
+ int32_t avail;
+ uint32_t index;
+ size_t size;
+ pa_usec_t latency;
+ int negative, res;
+
+ if (impl->resync) {
+ impl->resync = false;
+ avail = length + impl->target_buffer;
+ spa_ringbuffer_get_write_index(&impl->ring, &index);
+ index -= avail;
+ } else {
+ avail = spa_ringbuffer_get_read_index(&impl->ring, &index);
+ }
+
+ pa_stream_get_latency(impl->pa_stream, &latency, &negative);
+ impl->current_latency = latency * impl->info.rate / SPA_USEC_PER_SEC;
+ impl->current_latency += avail / impl->frame_size;
+
+ while (avail < (int32_t)length) {
+ uint32_t maxsize = SPA_ROUND_DOWN(sizeof(impl->empty), impl->frame_size);
+ /* send silence for the data we don't have */
+ size = SPA_MIN(length - avail, maxsize);
+ if ((res = pa_stream_write(impl->pa_stream,
+ impl->empty, size,
+ NULL, 0, PA_SEEK_RELATIVE)) != 0)
+ pw_log_warn("error writing stream: %s", pa_strerror(res));
+ length -= size;
+ }
+ while (length > 0 && avail >= (int32_t)length) {
+ void *data;
+
+ size = length;
+ pa_stream_begin_write(impl->pa_stream, &data, &size);
+
+ spa_ringbuffer_read_data(&impl->ring,
+ impl->buffer, RINGBUFFER_SIZE,
+ index & RINGBUFFER_MASK,
+ data, size);
+
+ if ((res = pa_stream_write(impl->pa_stream,
+ data, size, NULL, 0, PA_SEEK_RELATIVE)) != 0)
+ pw_log_warn("error writing stream: %zd %s", size,
+ pa_strerror(res));
+
+ index += size;
+ length -= size;
+ avail -= size;
+ spa_ringbuffer_read_update(&impl->ring, index);
+ }
+}
+static void stream_underflow_cb(pa_stream *s, void *userdata)
+{
+ struct impl *impl = userdata;
+ struct timespec ts;
+
+ clock_gettime(CLOCK_MONOTONIC, &ts);
+ if (ratelimit_test(&impl->rate_limit, SPA_TIMESPEC_TO_NSEC(&ts), SPA_LOG_LEVEL_WARN))
+ pw_log_warn("underflow");
+ impl->resync = true;
+}
+static void stream_overflow_cb(pa_stream *s, void *userdata)
+{
+ struct impl *impl = userdata;
+ struct timespec ts;
+ clock_gettime(CLOCK_MONOTONIC, &ts);
+ if (ratelimit_test(&impl->rate_limit, SPA_TIMESPEC_TO_NSEC(&ts), SPA_LOG_LEVEL_WARN))
+ pw_log_warn("overflow");
+ impl->resync = true;
+}
+
+static void stream_latency_update_cb(pa_stream *s, void *userdata)
+{
+ struct impl *impl = userdata;
+ pa_usec_t usec;
+ int negative;
+
+ pa_stream_get_latency(s, &usec, &negative);
+
+ pw_log_debug("latency %ld negative %d", usec, negative);
+ pa_threaded_mainloop_signal(impl->pa_mainloop, 0);
+}
+
+static pa_proplist* tunnel_new_proplist(struct impl *impl)
+{
+ pa_proplist *proplist = pa_proplist_new();
+ pa_proplist_sets(proplist, PA_PROP_APPLICATION_NAME, "PipeWire");
+ pa_proplist_sets(proplist, PA_PROP_APPLICATION_ID, "org.pipewire.PipeWire");
+ pa_proplist_sets(proplist, PA_PROP_APPLICATION_VERSION, PACKAGE_VERSION);
+ return proplist;
+}
+
+static int create_pulse_stream(struct impl *impl)
+{
+ pa_sample_spec ss;
+ pa_channel_map map;
+ const char *server_address, *remote_node_target;
+ pa_proplist *props = NULL;
+ pa_mainloop_api *api;
+ char stream_name[1024];
+ pa_buffer_attr bufferattr;
+ int res = -EIO;
+ uint32_t latency_bytes, i, aux = 0;
+
+ if ((impl->pa_mainloop = pa_threaded_mainloop_new()) == NULL)
+ goto error;
+
+ api = pa_threaded_mainloop_get_api(impl->pa_mainloop);
+
+ props = tunnel_new_proplist(impl);
+ impl->pa_context = pa_context_new_with_proplist(api, "PipeWire", props);
+ pa_proplist_free(props);
+
+ if (impl->pa_context == NULL)
+ goto error;
+
+ pa_context_set_state_callback(impl->pa_context, context_state_cb, impl);
+
+ server_address = pw_properties_get(impl->props, "pulse.server.address");
+
+ if (pa_context_connect(impl->pa_context, server_address, 0, NULL) < 0) {
+ res = pa_context_errno(impl->pa_context);
+ goto error;
+ }
+
+ pa_threaded_mainloop_lock(impl->pa_mainloop);
+
+ if (pa_threaded_mainloop_start(impl->pa_mainloop) < 0)
+ goto error_unlock;
+
+ for (;;) {
+ pa_context_state_t state;
+
+ state = pa_context_get_state(impl->pa_context);
+ if (state == PA_CONTEXT_READY)
+ break;
+
+ if (!PA_CONTEXT_IS_GOOD(state)) {
+ res = pa_context_errno(impl->pa_context);
+ goto error_unlock;
+ }
+ /* Wait until the context is ready */
+ pa_threaded_mainloop_wait(impl->pa_mainloop);
+ }
+
+ ss.format = (pa_sample_format_t) format_id2pa(impl->info.format);
+ ss.channels = impl->info.channels;
+ ss.rate = impl->info.rate;
+
+ map.channels = impl->info.channels;
+ for (i = 0; i < map.channels; i++)
+ map.map[i] = (pa_channel_position_t)channel_id2pa(impl->info.position[i], &aux);
+
+ snprintf(stream_name, sizeof(stream_name), _("Tunnel for %s@%s"),
+ pw_get_user_name(), pw_get_host_name());
+
+ if (!(impl->pa_stream = pa_stream_new(impl->pa_context, stream_name, &ss, &map))) {
+ res = pa_context_errno(impl->pa_context);
+ goto error_unlock;
+ }
+
+ pa_stream_set_state_callback(impl->pa_stream, stream_state_cb, impl);
+ pa_stream_set_read_callback(impl->pa_stream, stream_read_request_cb, impl);
+ pa_stream_set_write_callback(impl->pa_stream, stream_write_request_cb, impl);
+ pa_stream_set_underflow_callback(impl->pa_stream, stream_underflow_cb, impl);
+ pa_stream_set_overflow_callback(impl->pa_stream, stream_overflow_cb, impl);
+ pa_stream_set_latency_update_callback(impl->pa_stream, stream_latency_update_cb, impl);
+
+ remote_node_target = pw_properties_get(impl->props, PW_KEY_TARGET_OBJECT);
+
+ bufferattr.fragsize = (uint32_t) -1;
+ bufferattr.minreq = (uint32_t) -1;
+ bufferattr.maxlength = (uint32_t) -1;
+ bufferattr.prebuf = (uint32_t) -1;
+
+ latency_bytes = pa_usec_to_bytes(impl->latency_msec * SPA_USEC_PER_MSEC, &ss);
+
+ impl->target_latency = latency_bytes / impl->frame_size;
+
+ /* half in our buffer, half in the network + remote */
+ impl->target_buffer = latency_bytes / 2;
+
+ if (impl->mode == MODE_SOURCE) {
+ bufferattr.fragsize = latency_bytes / 2;
+
+ res = pa_stream_connect_record(impl->pa_stream,
+ remote_node_target, &bufferattr,
+ PA_STREAM_DONT_MOVE |
+ PA_STREAM_INTERPOLATE_TIMING |
+ PA_STREAM_ADJUST_LATENCY |
+ PA_STREAM_AUTO_TIMING_UPDATE);
+ } else {
+ bufferattr.tlength = latency_bytes / 2;
+ bufferattr.minreq = bufferattr.tlength / 4;
+ bufferattr.prebuf = bufferattr.tlength;
+
+ res = pa_stream_connect_playback(impl->pa_stream,
+ remote_node_target, &bufferattr,
+ PA_STREAM_DONT_MOVE |
+ PA_STREAM_INTERPOLATE_TIMING |
+ PA_STREAM_ADJUST_LATENCY |
+ PA_STREAM_AUTO_TIMING_UPDATE,
+ NULL, NULL);
+ }
+
+ if (res < 0) {
+ res = pa_context_errno(impl->pa_context);
+ goto error_unlock;
+ }
+
+ for (;;) {
+ pa_stream_state_t state;
+
+ state = pa_stream_get_state(impl->pa_stream);
+ if (state == PA_STREAM_READY)
+ break;
+
+ if (!PA_STREAM_IS_GOOD(state)) {
+ res = pa_context_errno(impl->pa_context);
+ goto error_unlock;
+ }
+
+ /* Wait until the stream is ready */
+ pa_threaded_mainloop_wait(impl->pa_mainloop);
+ }
+
+ pa_threaded_mainloop_unlock(impl->pa_mainloop);
+
+ return 0;
+
+error_unlock:
+ pa_threaded_mainloop_unlock(impl->pa_mainloop);
+error:
+ pw_log_error("failed to connect: %s", pa_strerror(res));
+ return -res;
+}
+
+
+static void core_error(void *data, uint32_t id, int seq, int res, const char *message)
+{
+ struct impl *impl = data;
+
+ pw_log_error("error id:%u seq:%d res:%d (%s): %s",
+ id, seq, res, spa_strerror(res), message);
+
+ if (id == PW_ID_CORE && res == -EPIPE)
+ pw_impl_module_schedule_destroy(impl->module);
+}
+
+static const struct pw_core_events core_events = {
+ PW_VERSION_CORE_EVENTS,
+ .error = core_error,
+};
+
+static void core_destroy(void *d)
+{
+ struct impl *impl = d;
+ spa_hook_remove(&impl->core_listener);
+ impl->core = NULL;
+ pw_impl_module_schedule_destroy(impl->module);
+}
+
+static const struct pw_proxy_events core_proxy_events = {
+ .destroy = core_destroy,
+};
+
+static void impl_destroy(struct impl *impl)
+{
+
+ if (impl->pa_mainloop)
+ pa_threaded_mainloop_stop(impl->pa_mainloop);
+ if (impl->pa_stream)
+ pa_stream_unref(impl->pa_stream);
+ if (impl->pa_context) {
+ pa_context_disconnect(impl->pa_context);
+ pa_context_unref(impl->pa_context);
+ }
+ if (impl->pa_mainloop)
+ pa_threaded_mainloop_free(impl->pa_mainloop);
+
+ if (impl->stream)
+ pw_stream_destroy(impl->stream);
+ if (impl->core && impl->do_disconnect)
+ pw_core_disconnect(impl->core);
+
+ pw_properties_free(impl->stream_props);
+ pw_properties_free(impl->props);
+
+ free(impl->buffer);
+ free(impl);
+}
+
+static void module_destroy(void *data)
+{
+ struct impl *impl = data;
+ spa_hook_remove(&impl->module_listener);
+ impl_destroy(impl);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy,
+};
+
+static uint32_t channel_from_name(const char *name)
+{
+ int i;
+ for (i = 0; spa_type_audio_channel[i].name; i++) {
+ if (spa_streq(name, spa_debug_type_short_name(spa_type_audio_channel[i].name)))
+ return spa_type_audio_channel[i].type;
+ }
+ return SPA_AUDIO_CHANNEL_UNKNOWN;
+}
+
+static void parse_position(struct spa_audio_info_raw *info, const char *val, size_t len)
+{
+ struct spa_json it[2];
+ char v[256];
+
+ spa_json_init(&it[0], val, len);
+ if (spa_json_enter_array(&it[0], &it[1]) <= 0)
+ spa_json_init(&it[1], val, len);
+
+ info->channels = 0;
+ while (spa_json_get_string(&it[1], v, sizeof(v)) > 0 &&
+ info->channels < SPA_AUDIO_MAX_CHANNELS) {
+ info->position[info->channels++] = channel_from_name(v);
+ }
+}
+
+static inline uint32_t format_from_name(const char *name, size_t len)
+{
+ int i;
+ for (i = 0; spa_type_audio_format[i].name; i++) {
+ if (strncmp(name, spa_debug_type_short_name(spa_type_audio_format[i].name), len) == 0)
+ return spa_type_audio_format[i].type;
+ }
+ return SPA_AUDIO_FORMAT_UNKNOWN;
+}
+
+static void parse_audio_info(const struct pw_properties *props, struct spa_audio_info_raw *info)
+{
+ const char *str;
+
+ spa_zero(*info);
+ if ((str = pw_properties_get(props, PW_KEY_AUDIO_FORMAT)) == NULL)
+ str = DEFAULT_FORMAT;
+ info->format = format_from_name(str, strlen(str));
+
+ info->rate = pw_properties_get_uint32(props, PW_KEY_AUDIO_RATE, info->rate);
+ if (info->rate == 0)
+ info->rate = DEFAULT_RATE;
+
+ info->channels = pw_properties_get_uint32(props, PW_KEY_AUDIO_CHANNELS, info->channels);
+ info->channels = SPA_MIN(info->channels, SPA_AUDIO_MAX_CHANNELS);
+ if ((str = pw_properties_get(props, SPA_KEY_AUDIO_POSITION)) != NULL)
+ parse_position(info, str, strlen(str));
+ if (info->channels == 0)
+ parse_position(info, DEFAULT_POSITION, strlen(DEFAULT_POSITION));
+}
+
+static int calc_frame_size(struct spa_audio_info_raw *info)
+{
+ int res = info->channels;
+ switch (info->format) {
+ case SPA_AUDIO_FORMAT_U8:
+ case SPA_AUDIO_FORMAT_S8:
+ case SPA_AUDIO_FORMAT_ALAW:
+ case SPA_AUDIO_FORMAT_ULAW:
+ return res;
+ case SPA_AUDIO_FORMAT_S16:
+ case SPA_AUDIO_FORMAT_S16_OE:
+ case SPA_AUDIO_FORMAT_U16:
+ return res * 2;
+ case SPA_AUDIO_FORMAT_S24:
+ case SPA_AUDIO_FORMAT_S24_OE:
+ case SPA_AUDIO_FORMAT_U24:
+ return res * 3;
+ case SPA_AUDIO_FORMAT_S24_32:
+ case SPA_AUDIO_FORMAT_S24_32_OE:
+ case SPA_AUDIO_FORMAT_S32:
+ case SPA_AUDIO_FORMAT_S32_OE:
+ case SPA_AUDIO_FORMAT_U32:
+ case SPA_AUDIO_FORMAT_U32_OE:
+ case SPA_AUDIO_FORMAT_F32:
+ case SPA_AUDIO_FORMAT_F32_OE:
+ return res * 4;
+ case SPA_AUDIO_FORMAT_F64:
+ case SPA_AUDIO_FORMAT_F64_OE:
+ return res * 8;
+ default:
+ return 0;
+ }
+}
+
+static void copy_props(struct impl *impl, struct pw_properties *props, const char *key)
+{
+ const char *str;
+ if ((str = pw_properties_get(props, key)) != NULL) {
+ if (pw_properties_get(impl->stream_props, key) == NULL)
+ pw_properties_set(impl->stream_props, key, str);
+ }
+}
+
+SPA_EXPORT
+int pipewire__module_init(struct pw_impl_module *module, const char *args)
+{
+ struct pw_context *context = pw_impl_module_get_context(module);
+ struct pw_properties *props = NULL;
+ struct impl *impl;
+ const char *str;
+ int res;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ impl = calloc(1, sizeof(struct impl));
+ if (impl == NULL)
+ return -errno;
+
+ pw_log_debug("module %p: new %s", impl, args);
+
+ if (args == NULL)
+ args = "";
+
+ props = pw_properties_new_string(args);
+ if (props == NULL) {
+ res = -errno;
+ pw_log_error( "can't create properties: %m");
+ goto error;
+ }
+ impl->props = props;
+
+ impl->stream_props = pw_properties_new(NULL, NULL);
+ if (impl->stream_props == NULL) {
+ res = -errno;
+ pw_log_error( "can't create properties: %m");
+ goto error;
+ }
+
+ impl->module = module;
+ impl->context = context;
+
+ spa_ringbuffer_init(&impl->ring);
+ impl->buffer = calloc(1, RINGBUFFER_SIZE);
+ spa_dll_init(&impl->dll);
+ impl->rate_limit.interval = 2 * SPA_NSEC_PER_SEC;
+ impl->rate_limit.burst = 1;
+
+ if ((str = pw_properties_get(props, "tunnel.mode")) != NULL) {
+ if (spa_streq(str, "source")) {
+ impl->mode = MODE_SOURCE;
+ } else if (spa_streq(str, "sink")) {
+ impl->mode = MODE_SINK;
+ } else {
+ pw_log_error("invalid tunnel.mode '%s'", str);
+ res = -EINVAL;
+ goto error;
+ }
+ }
+
+ impl->latency_msec = pw_properties_get_uint32(props, "pulse.latency", DEFAULT_LATENCY_MSEC);
+
+
+ if (pw_properties_get(props, PW_KEY_NODE_VIRTUAL) == NULL)
+ pw_properties_set(props, PW_KEY_NODE_VIRTUAL, "true");
+ if (pw_properties_get(props, PW_KEY_NODE_NETWORK) == NULL)
+ pw_properties_set(props, PW_KEY_NODE_NETWORK, "true");
+
+ if (pw_properties_get(props, PW_KEY_MEDIA_CLASS) == NULL)
+ pw_properties_set(props, PW_KEY_MEDIA_CLASS,
+ impl->mode == MODE_SINK ?
+ "Audio/Sink" : "Audio/Source");
+
+ if ((str = pw_properties_get(props, "stream.props")) != NULL)
+ pw_properties_update_string(impl->stream_props, str, strlen(str));
+
+ copy_props(impl, props, PW_KEY_AUDIO_FORMAT);
+ copy_props(impl, props, PW_KEY_AUDIO_RATE);
+ copy_props(impl, props, PW_KEY_AUDIO_CHANNELS);
+ copy_props(impl, props, SPA_KEY_AUDIO_POSITION);
+ copy_props(impl, props, PW_KEY_NODE_NAME);
+ copy_props(impl, props, PW_KEY_NODE_DESCRIPTION);
+ copy_props(impl, props, PW_KEY_NODE_GROUP);
+ copy_props(impl, props, PW_KEY_NODE_LATENCY);
+ copy_props(impl, props, PW_KEY_NODE_VIRTUAL);
+ copy_props(impl, props, PW_KEY_NODE_NETWORK);
+ copy_props(impl, props, PW_KEY_MEDIA_CLASS);
+
+ parse_audio_info(impl->stream_props, &impl->info);
+
+ impl->frame_size = calc_frame_size(&impl->info);
+ if (impl->frame_size == 0) {
+ pw_log_error("unsupported audio format:%d channels:%d",
+ impl->info.format, impl->info.channels);
+ res = -EINVAL;
+ goto error;
+ }
+ spa_dll_set_bw(&impl->dll, SPA_DLL_BW_MIN, 128, impl->info.rate);
+ impl->max_error = 256.0f;
+
+ impl->core = pw_context_get_object(impl->context, PW_TYPE_INTERFACE_Core);
+ if (impl->core == NULL) {
+ str = pw_properties_get(props, PW_KEY_REMOTE_NAME);
+ impl->core = pw_context_connect(impl->context,
+ pw_properties_new(
+ PW_KEY_REMOTE_NAME, str,
+ NULL),
+ 0);
+ impl->do_disconnect = true;
+ }
+ if (impl->core == NULL) {
+ res = -errno;
+ pw_log_error("can't connect: %m");
+ goto error;
+ }
+
+ pw_proxy_add_listener((struct pw_proxy*)impl->core,
+ &impl->core_proxy_listener,
+ &core_proxy_events, impl);
+ pw_core_add_listener(impl->core,
+ &impl->core_listener,
+ &core_events, impl);
+
+ if ((res = create_pulse_stream(impl)) < 0)
+ goto error;
+
+ if ((res = create_stream(impl)) < 0)
+ goto error;
+
+ pw_impl_module_add_listener(module, &impl->module_listener, &module_events, impl);
+
+ pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_props));
+
+ return 0;
+
+error:
+ impl_destroy(impl);
+ return res;
+}
diff --git a/src/modules/module-raop-discover.c b/src/modules/module-raop-discover.c
new file mode 100644
index 0000000..0c62011
--- /dev/null
+++ b/src/modules/module-raop-discover.c
@@ -0,0 +1,547 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+
+#include "config.h"
+
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+#include <spa/utils/json.h>
+
+#include <pipewire/impl.h>
+#include <pipewire/i18n.h>
+
+#include <avahi-client/lookup.h>
+#include <avahi-common/error.h>
+#include <avahi-common/malloc.h>
+
+#include "module-protocol-pulse/format.h"
+#include "module-zeroconf-discover/avahi-poll.h"
+
+/** \page page_module_raop_discover PipeWire Module: RAOP Discover
+ *
+ * Automatically creates RAOP (Airplay) sink devices based on zeroconf
+ * information.
+ *
+ * This module will load module-raop-sink for each discovered sink
+ * with the right parameters.
+ *
+ * ## Module Options
+ *
+ * This module has no options.
+ *
+ * ## Example configuration
+ *
+ *\code{.unparsed}
+ * context.modules = [
+ * { name = libpipewire-raop-discover
+ * args = { }
+ * }
+ * ]
+ *\endcode
+ *
+ * ## See also
+ *
+ * \ref page_module_raop_sink
+ */
+
+#define NAME "raop-discover"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+#define MODULE_USAGE " "
+
+static const struct spa_dict_item module_props[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Discover remote streams" },
+ { PW_KEY_MODULE_USAGE, MODULE_USAGE },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+#define SERVICE_TYPE_SINK "_raop._tcp"
+
+struct impl {
+ struct pw_context *context;
+
+ struct pw_impl_module *module;
+ struct spa_hook module_listener;
+
+ struct pw_properties *properties;
+
+ AvahiPoll *avahi_poll;
+ AvahiClient *client;
+ AvahiServiceBrowser *sink_browser;
+
+ struct spa_list tunnel_list;
+};
+
+struct tunnel_info {
+ AvahiIfIndex interface;
+ AvahiProtocol protocol;
+ const char *name;
+ const char *type;
+ const char *domain;
+};
+
+#define TUNNEL_INFO(...) ((struct tunnel_info){ __VA_ARGS__ })
+
+struct tunnel {
+ struct spa_list link;
+ struct tunnel_info info;
+ struct pw_impl_module *module;
+ struct spa_hook module_listener;
+};
+
+static int start_client(struct impl *impl);
+
+static struct tunnel *make_tunnel(struct impl *impl, const struct tunnel_info *info)
+{
+ struct tunnel *t;
+
+ t = calloc(1, sizeof(*t));
+ if (t == NULL)
+ return NULL;
+
+ t->info.interface = info->interface;
+ t->info.protocol = info->protocol;
+ t->info.name = strdup(info->name);
+ t->info.type = strdup(info->type);
+ t->info.domain = strdup(info->domain);
+ spa_list_append(&impl->tunnel_list, &t->link);
+
+ return t;
+}
+
+static struct tunnel *find_tunnel(struct impl *impl, const struct tunnel_info *info)
+{
+ struct tunnel *t;
+ spa_list_for_each(t, &impl->tunnel_list, link) {
+ if (t->info.interface == info->interface &&
+ t->info.protocol == info->protocol &&
+ spa_streq(t->info.name, info->name) &&
+ spa_streq(t->info.type, info->type) &&
+ spa_streq(t->info.domain, info->domain))
+ return t;
+ }
+ return NULL;
+}
+
+static void free_tunnel(struct tunnel *t)
+{
+ pw_impl_module_destroy(t->module);
+}
+
+static void impl_free(struct impl *impl)
+{
+ struct tunnel *t;
+
+ spa_list_consume(t, &impl->tunnel_list, link)
+ free_tunnel(t);
+
+ if (impl->sink_browser)
+ avahi_service_browser_free(impl->sink_browser);
+ if (impl->client)
+ avahi_client_free(impl->client);
+ if (impl->avahi_poll)
+ pw_avahi_poll_free(impl->avahi_poll);
+ pw_properties_free(impl->properties);
+ free(impl);
+}
+
+static void module_destroy(void *data)
+{
+ struct impl *impl = data;
+ spa_hook_remove(&impl->module_listener);
+ impl_free(impl);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy,
+};
+
+static bool str_in_list(const char *haystack, const char *delimiters, const char *needle)
+{
+ const char *s, *state = NULL;
+ size_t len;
+ while ((s = pw_split_walk(haystack, delimiters, &len, &state))) {
+ if (spa_strneq(needle, s, len))
+ return true;
+ }
+ return false;
+}
+
+static void pw_properties_from_avahi_string(const char *key, const char *value,
+ struct pw_properties *props)
+{
+ if (spa_streq(key, "device")) {
+ pw_properties_set(props, "raop.device", value);
+ }
+ else if (spa_streq(key, "tp")) {
+ /* transport protocol, "UDP", "TCP", "UDP,TCP" */
+ if (str_in_list(value, ",", "UDP"))
+ value = "udp";
+ else if (str_in_list(value, ",", "TCP"))
+ value = "tcp";
+ pw_properties_set(props, "raop.transport", value);
+ } else if (spa_streq(key, "et")) {
+ /* Supported encryption types:
+ * 0 = none,
+ * 1 = RSA,
+ * 2 = FairPlay,
+ * 3 = MFiSAP,
+ * 4 = FairPlay SAPv2.5. */
+ if (str_in_list(value, ",", "1"))
+ value = "RSA";
+ else if (str_in_list(value, ",", "4"))
+ value = "auth_setup";
+ else
+ value = "none";
+ pw_properties_set(props, "raop.encryption.type", value);
+ } else if (spa_streq(key, "cn")) {
+ /* Suported audio codecs:
+ * 0 = PCM,
+ * 1 = ALAC,
+ * 2 = AAC,
+ * 3 = AAC ELD. */
+ if (str_in_list(value, ",", "0"))
+ value = "PCM";
+ else if (str_in_list(value, ",", "1"))
+ value = "ALAC";
+ else if (str_in_list(value, ",", "2"))
+ value = "AAC";
+ else if (str_in_list(value, ",", "2"))
+ value = "AAC-ELD";
+ else
+ value = "unknown";
+ pw_properties_set(props, "raop.audio.codec", value);
+ } else if (spa_streq(key, "ch")) {
+ /* Number of channels */
+ pw_properties_set(props, PW_KEY_AUDIO_CHANNELS, value);
+ } else if (spa_streq(key, "ss")) {
+ /* Sample size */
+ if (spa_streq(value, "16"))
+ value = "S16";
+ else if (spa_streq(value, "24"))
+ value = "S24";
+ else if (spa_streq(value, "32"))
+ value = "S32";
+ else
+ value = "UNKNOWN";
+ pw_properties_set(props, PW_KEY_AUDIO_FORMAT, value);
+ } else if (spa_streq(key, "sr")) {
+ /* Sample rate */
+ pw_properties_set(props, PW_KEY_AUDIO_RATE, value);
+ } else if (spa_streq(key, "am")) {
+ /* Device model */
+ pw_properties_set(props, "device.model", value);
+ }
+}
+
+static void submodule_destroy(void *data)
+{
+ struct tunnel *t = data;
+
+ spa_list_remove(&t->link);
+ spa_hook_remove(&t->module_listener);
+
+ free((char *) t->info.name);
+ free((char *) t->info.type);
+ free((char *) t->info.domain);
+
+ free(t);
+}
+
+static const struct pw_impl_module_events submodule_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = submodule_destroy,
+};
+
+static void resolver_cb(AvahiServiceResolver *r, AvahiIfIndex interface, AvahiProtocol protocol,
+ AvahiResolverEvent event, const char *name, const char *type, const char *domain,
+ const char *host_name, const AvahiAddress *a, uint16_t port, AvahiStringList *txt,
+ AvahiLookupResultFlags flags, void *userdata)
+{
+ struct impl *impl = userdata;
+ struct tunnel *t;
+ struct tunnel_info tinfo;
+ const char *str;
+ AvahiStringList *l;
+ FILE *f;
+ char *args;
+ size_t size;
+ struct pw_impl_module *mod;
+ struct pw_properties *props = NULL;
+ char at[AVAHI_ADDRESS_STR_MAX];
+
+ if (event != AVAHI_RESOLVER_FOUND) {
+ pw_log_error("Resolving of '%s' failed: %s", name,
+ avahi_strerror(avahi_client_errno(impl->client)));
+ goto done;
+ }
+ tinfo = TUNNEL_INFO(.interface = interface,
+ .protocol = protocol,
+ .name = name,
+ .type = type,
+ .domain = domain);
+
+ props = pw_properties_new(NULL, NULL);
+ if (props == NULL) {
+ pw_log_error("Can't allocate properties: %m");
+ goto done;
+ }
+
+ avahi_address_snprint(at, sizeof(at), a);
+
+ pw_properties_setf(props, "raop.hostname", "%s", at);
+ pw_properties_setf(props, "raop.port", "%u", port);
+
+ if ((str = strstr(name, "@"))) {
+ str++;
+ if (strlen(str) > 0)
+ pw_properties_set(props, PW_KEY_NODE_DESCRIPTION, str);
+ else
+ pw_properties_setf(props, PW_KEY_NODE_DESCRIPTION,
+ "RAOP on %s", host_name);
+ }
+
+ for (l = txt; l; l = l->next) {
+ char *key, *value;
+
+ if (avahi_string_list_get_pair(l, &key, &value, NULL) != 0)
+ break;
+
+ pw_properties_from_avahi_string(key, value, props);
+ avahi_free(key);
+ avahi_free(value);
+ }
+
+
+ if ((f = open_memstream(&args, &size)) == NULL) {
+ pw_log_error("Can't open memstream: %m");
+ goto done;
+ }
+
+ fprintf(f, "{");
+ pw_properties_serialize_dict(f, &props->dict, 0);
+ fprintf(f, " stream.props = {");
+ fprintf(f, " }");
+ fprintf(f, "}");
+ fclose(f);
+
+ pw_properties_free(props);
+
+ pw_log_info("loading module args:'%s'", args);
+ mod = pw_context_load_module(impl->context,
+ "libpipewire-module-raop-sink",
+ args, NULL);
+ free(args);
+
+ if (mod == NULL) {
+ pw_log_error("Can't load module: %m");
+ goto done;
+ }
+
+ t = make_tunnel(impl, &tinfo);
+ if (t == NULL) {
+ pw_log_error("Can't make tunnel: %m");
+ pw_impl_module_destroy(mod);
+ goto done;
+ }
+
+ pw_impl_module_add_listener(mod, &t->module_listener, &submodule_events, t);
+
+ t->module = mod;
+
+done:
+ avahi_service_resolver_free(r);
+}
+
+
+static void browser_cb(AvahiServiceBrowser *b, AvahiIfIndex interface, AvahiProtocol protocol,
+ AvahiBrowserEvent event, const char *name, const char *type, const char *domain,
+ AvahiLookupResultFlags flags, void *userdata)
+{
+ struct impl *impl = userdata;
+ struct tunnel_info info;
+ struct tunnel *t;
+
+ if (flags & AVAHI_LOOKUP_RESULT_LOCAL)
+ return;
+
+ info = TUNNEL_INFO(.interface = interface,
+ .protocol = protocol,
+ .name = name,
+ .type = type,
+ .domain = domain);
+
+ t = find_tunnel(impl, &info);
+
+ switch (event) {
+ case AVAHI_BROWSER_NEW:
+ if (t != NULL)
+ return;
+ if (!(avahi_service_resolver_new(impl->client,
+ interface, protocol,
+ name, type, domain,
+ AVAHI_PROTO_UNSPEC, 0,
+ resolver_cb, impl)))
+ pw_log_error("can't make service resolver: %s",
+ avahi_strerror(avahi_client_errno(impl->client)));
+ break;
+ case AVAHI_BROWSER_REMOVE:
+ if (t == NULL)
+ return;
+ free_tunnel(t);
+ break;
+ default:
+ break;
+ }
+}
+
+
+static struct AvahiServiceBrowser *make_browser(struct impl *impl, const char *service_type)
+{
+ struct AvahiServiceBrowser *s;
+
+ s = avahi_service_browser_new(impl->client,
+ AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC,
+ service_type, NULL, 0,
+ browser_cb, impl);
+ if (s == NULL) {
+ pw_log_error("can't make browser for %s: %s", service_type,
+ avahi_strerror(avahi_client_errno(impl->client)));
+ }
+ return s;
+}
+
+static void client_callback(AvahiClient *c, AvahiClientState state, void *userdata)
+{
+ struct impl *impl = userdata;
+
+ impl->client = c;
+
+ switch (state) {
+ case AVAHI_CLIENT_S_REGISTERING:
+ case AVAHI_CLIENT_S_RUNNING:
+ case AVAHI_CLIENT_S_COLLISION:
+ if (impl->sink_browser == NULL)
+ impl->sink_browser = make_browser(impl, SERVICE_TYPE_SINK);
+ if (impl->sink_browser == NULL)
+ goto error;
+ break;
+ case AVAHI_CLIENT_FAILURE:
+ if (avahi_client_errno(c) == AVAHI_ERR_DISCONNECTED)
+ start_client(impl);
+
+ SPA_FALLTHROUGH;
+ case AVAHI_CLIENT_CONNECTING:
+ if (impl->sink_browser) {
+ avahi_service_browser_free(impl->sink_browser);
+ impl->sink_browser = NULL;
+ }
+ break;
+ default:
+ break;
+ }
+ return;
+error:
+ pw_impl_module_schedule_destroy(impl->module);
+}
+
+static int start_client(struct impl *impl)
+{
+ int res;
+ if ((impl->client = avahi_client_new(impl->avahi_poll,
+ AVAHI_CLIENT_NO_FAIL,
+ client_callback, impl,
+ &res)) == NULL) {
+ pw_log_error("can't create client: %s", avahi_strerror(res));
+ pw_impl_module_schedule_destroy(impl->module);
+ return -EIO;
+ }
+ return 0;
+}
+
+static int start_avahi(struct impl *impl)
+{
+ struct pw_loop *loop;
+
+ loop = pw_context_get_main_loop(impl->context);
+ impl->avahi_poll = pw_avahi_poll_new(loop);
+
+ return start_client(impl);
+}
+
+SPA_EXPORT
+int pipewire__module_init(struct pw_impl_module *module, const char *args)
+{
+ struct pw_context *context = pw_impl_module_get_context(module);
+ struct pw_properties *props;
+ struct impl *impl;
+ int res;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ impl = calloc(1, sizeof(struct impl));
+ if (impl == NULL)
+ goto error_errno;
+
+ pw_log_debug("module %p: new %s", impl, args);
+
+ if (args == NULL)
+ args = "";
+
+ props = pw_properties_new_string(args);
+ if (props == NULL)
+ goto error_errno;
+
+ spa_list_init(&impl->tunnel_list);
+
+ impl->module = module;
+ impl->context = context;
+ impl->properties = props;
+
+ pw_impl_module_add_listener(module, &impl->module_listener, &module_events, impl);
+
+ pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_props));
+
+ start_avahi(impl);
+
+ return 0;
+
+error_errno:
+ res = -errno;
+ if (impl)
+ impl_free(impl);
+ return res;
+}
diff --git a/src/modules/module-raop-sink.c b/src/modules/module-raop-sink.c
new file mode 100644
index 0000000..303cb42
--- /dev/null
+++ b/src/modules/module-raop-sink.c
@@ -0,0 +1,1848 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <signal.h>
+#include <limits.h>
+#include <math.h>
+#include <arpa/inet.h>
+#include <netinet/in.h>
+
+#include <openssl/err.h>
+#include <openssl/rand.h>
+#include <openssl/rsa.h>
+#include <openssl/engine.h>
+#include <openssl/aes.h>
+#include <openssl/md5.h>
+
+#include "config.h"
+
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+#include <spa/utils/json.h>
+#include <spa/utils/ringbuffer.h>
+#include <spa/debug/types.h>
+#include <spa/pod/builder.h>
+#include <spa/param/audio/format-utils.h>
+#include <spa/param/audio/raw.h>
+#include <spa/param/latency-utils.h>
+
+#include <pipewire/impl.h>
+#include <pipewire/i18n.h>
+
+#include "module-raop/rtsp-client.h"
+
+/** \page page_module_raop_sink PipeWire Module: AirPlay Sink
+ *
+ * Creates a new Sink to stream to an Airplay device.
+ *
+ * Normally this sink is automatically created with \ref page_module_raop_discover
+ * with the right parameters but it is possible to manually create a RAOP sink
+ * as well.
+ *
+ * ## Module Options
+ *
+ * Options specific to the behavior of this module
+ *
+ * - `raop.hostname`: The hostname of the remote end.
+ * - `raop.port`: The port of the remote end.
+ * - `raop.transport`: The data transport to use, one of "udp" or "tcp". Defaults
+ * to "udp".
+ * - `raop.encryption.type`: The encryption type to use. One of "none", "RSA" or
+ * "auth_setup". Default is "none".
+ * - `raop.audio.codec`: The audio codec to use. Needs to be "PCM". Defaults to "PCM".
+ * - `raop.password`: The password to use.
+ * - `stream.props = {}`: properties to be passed to the sink stream
+ *
+ * Options with well-known behavior.
+ *
+ * - \ref PW_KEY_REMOTE_NAME
+ * - \ref PW_KEY_AUDIO_FORMAT
+ * - \ref PW_KEY_AUDIO_RATE
+ * - \ref PW_KEY_AUDIO_CHANNELS
+ * - \ref SPA_KEY_AUDIO_POSITION
+ * - \ref PW_KEY_NODE_NAME
+ * - \ref PW_KEY_NODE_DESCRIPTION
+ * - \ref PW_KEY_NODE_GROUP
+ * - \ref PW_KEY_NODE_LATENCY
+ * - \ref PW_KEY_NODE_VIRTUAL
+ * - \ref PW_KEY_MEDIA_CLASS
+ *
+ * ## Example configuration
+ *
+ *\code{.unparsed}
+ * context.modules = [
+ * { name = libpipewire-module-raop-sink
+ * args = {
+ * # Set the remote address to tunnel to
+ * raop.hostname = "my-raop-device"
+ * raop.port = 8190
+ * #raop.transport = "udp"
+ * raop.encryption.type = "RSA"
+ * #raop.audio.codec = "PCM"
+ * #raop.password = "****"
+ * #audio.format = "S16"
+ * #audio.rate = 44100
+ * #audio.channels = 2
+ * #audio.position = [ FL FR ]
+ * stream.props = {
+ * # extra sink properties
+ * }
+ * }
+ * }
+ * ]
+ *\endcode
+ *
+ * ## See also
+ *
+ * \ref page_module_raop_discover
+ */
+
+#define NAME "raop-sink"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+#define FRAMES_PER_TCP_PACKET 4096
+#define FRAMES_PER_UDP_PACKET 352
+
+#define DEFAULT_TCP_AUDIO_PORT 6000
+#define DEFAULT_UDP_AUDIO_PORT 6000
+#define DEFAULT_UDP_CONTROL_PORT 6001
+#define DEFAULT_UDP_TIMING_PORT 6002
+
+#define AES_CHUNK_SIZE 16
+#ifndef MD5_DIGEST_LENGTH
+#define MD5_DIGEST_LENGTH 16
+#endif
+#define MD5_HASH_LENGTH (2*MD5_DIGEST_LENGTH)
+
+#define DEFAULT_USER_AGENT "iTunes/11.0.4 (Windows; N)"
+#define DEFAULT_USER_NAME "iTunes"
+
+#define MAX_PORT_RETRY 128
+
+#define DEFAULT_FORMAT "S16"
+#define DEFAULT_RATE 44100
+#define DEFAULT_CHANNELS 2
+#define DEFAULT_POSITION "[ FL FR ]"
+
+#define DEFAULT_LATENCY 22050
+
+#define MODULE_USAGE "[ raop.hostname=<name of host> ] " \
+ "[ raop.port=<remote port> ] " \
+ "[ raop.transport=<transport, default:udp> ] " \
+ "[ raop.encryption.type=<encryption, default:none> ] " \
+ "[ raop.audio.codec=PCM ] " \
+ "[ raop.password=<password for auth> ] " \
+ "[ node.latency=<latency as fraction> ] " \
+ "[ node.name=<name of the nodes> ] " \
+ "[ node.description=<description of the nodes> ] " \
+ "[ audio.format=<format, default:"DEFAULT_FORMAT"> ] " \
+ "[ audio.rate=<sample rate, default: "SPA_STRINGIFY(DEFAULT_RATE)"> ] " \
+ "[ audio.channels=<number of channels, default:"SPA_STRINGIFY(DEFAULT_CHANNELS)"> ] " \
+ "[ audio.position=<channel map, default:"DEFAULT_POSITION"> ] " \
+ "[ stream.props=<properties> ] "
+
+
+static const struct spa_dict_item module_props[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "An RAOP audio sink" },
+ { PW_KEY_MODULE_USAGE, MODULE_USAGE },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+enum {
+ PROTO_TCP,
+ PROTO_UDP,
+};
+enum {
+ CRYPTO_NONE,
+ CRYPTO_RSA,
+ CRYPTO_AUTH_SETUP,
+};
+enum {
+ CODEC_PCM,
+ CODEC_ALAC,
+ CODEC_AAC,
+ CODEC_AAC_ELD,
+};
+
+struct impl {
+ struct pw_context *context;
+
+ struct pw_properties *props;
+
+ struct pw_impl_module *module;
+ struct pw_loop *loop;
+
+ struct spa_hook module_listener;
+
+ int protocol;
+ int encryption;
+ int codec;
+
+ struct pw_core *core;
+ struct spa_hook core_proxy_listener;
+ struct spa_hook core_listener;
+
+ struct pw_properties *stream_props;
+ struct pw_stream *stream;
+ struct spa_hook stream_listener;
+ struct spa_audio_info_raw info;
+ uint32_t frame_size;
+
+ struct pw_rtsp_client *rtsp;
+ struct spa_hook rtsp_listener;
+ struct pw_properties *headers;
+
+ char session_id[32];
+ char *password;
+
+ unsigned int do_disconnect:1;
+
+ uint8_t key[AES_CHUNK_SIZE]; /* Key for aes-cbc */
+ uint8_t iv[AES_CHUNK_SIZE]; /* Initialization vector for cbc */
+ AES_KEY aes; /* AES encryption */
+
+ uint16_t control_port;
+ int control_fd;
+ struct spa_source *control_source;
+
+ uint16_t timing_port;
+ int timing_fd;
+ struct spa_source *timing_source;
+
+ uint16_t server_port;
+ int server_fd;
+ struct spa_source *server_source;
+
+ uint32_t block_size;
+ uint32_t delay;
+ uint32_t latency;
+
+ uint16_t seq;
+ uint32_t rtptime;
+ uint32_t ssrc;
+ uint32_t sync;
+ uint32_t sync_period;
+ unsigned int first:1;
+ unsigned int connected:1;
+ unsigned int ready:1;
+ unsigned int recording:1;
+
+ uint8_t buffer[FRAMES_PER_TCP_PACKET * 4];
+ uint32_t filled;
+};
+
+static void stream_destroy(void *d)
+{
+ struct impl *impl = d;
+ spa_hook_remove(&impl->stream_listener);
+ impl->stream = NULL;
+}
+
+static inline void bit_writer(uint8_t **p, int *pos, uint8_t data, int len)
+{
+ int rb = 8 - *pos - len;
+ if (rb >= 0) {
+ **p = (*pos ? **p : 0) | (data << rb);
+ *pos += len;
+ } else {
+ *(*p)++ |= (data >> -rb);
+ **p = data << (8+rb);
+ *pos = -rb;
+ }
+}
+
+static int aes_encrypt(struct impl *impl, uint8_t *data, int len)
+{
+ uint8_t nv[AES_CHUNK_SIZE];
+ uint8_t *buffer;
+ int i, j;
+
+ memcpy(nv, impl->iv, AES_CHUNK_SIZE);
+ for (i = 0; i + AES_CHUNK_SIZE <= len; i += AES_CHUNK_SIZE) {
+ buffer = data + i;
+ for (j = 0; j < AES_CHUNK_SIZE; j++)
+ buffer[j] ^= nv[j];
+
+ AES_encrypt(buffer, buffer, &impl->aes);
+
+ memcpy(nv, buffer, AES_CHUNK_SIZE);
+ }
+ return i;
+}
+
+static inline uint64_t timespec_to_ntp(struct timespec *ts)
+{
+ uint64_t ntp = (uint64_t) ts->tv_nsec * UINT32_MAX / SPA_NSEC_PER_SEC;
+ return ntp | (uint64_t) (ts->tv_sec + 0x83aa7e80) << 32;
+}
+
+static inline uint64_t ntp_now(int clockid)
+{
+ struct timespec now;
+ clock_gettime(clockid, &now);
+ return timespec_to_ntp(&now);
+}
+
+static int send_udp_sync_packet(struct impl *impl,
+ struct sockaddr *dest_addr, socklen_t addrlen)
+{
+ uint32_t pkt[5];
+ uint32_t rtptime = impl->rtptime;
+ uint32_t delay = impl->delay;
+ uint64_t transmitted;
+
+ pkt[0] = htonl(0x80d40007);
+ if (impl->first)
+ pkt[0] |= htonl(0x10000000);
+ rtptime -= delay;
+ pkt[1] = htonl(rtptime);
+ transmitted = ntp_now(CLOCK_MONOTONIC);
+ pkt[2] = htonl(transmitted >> 32);
+ pkt[3] = htonl(transmitted & 0xffffffff);
+ rtptime += delay;
+ pkt[4] = htonl(rtptime);
+
+ pw_log_debug("sync: delayed:%u now:%"PRIu64" rtptime:%u",
+ rtptime - delay, transmitted, rtptime);
+
+ return sendto(impl->control_fd, pkt, sizeof(pkt), 0, dest_addr, addrlen);
+}
+
+static int send_udp_timing_packet(struct impl *impl, uint64_t remote, uint64_t received,
+ struct sockaddr *dest_addr, socklen_t addrlen)
+{
+ uint32_t pkt[8];
+ uint64_t transmitted;
+
+ pkt[0] = htonl(0x80d30007);
+ pkt[1] = 0x00000000;
+ pkt[2] = htonl(remote >> 32);
+ pkt[3] = htonl(remote & 0xffffffff);
+ pkt[4] = htonl(received >> 32);
+ pkt[5] = htonl(received & 0xffffffff);
+ transmitted = ntp_now(CLOCK_MONOTONIC);
+ pkt[6] = htonl(transmitted >> 32);
+ pkt[7] = htonl(transmitted & 0xffffffff);
+
+ pw_log_debug("sync: remote:%"PRIu64" received:%"PRIu64" transmitted:%"PRIu64,
+ remote, received, transmitted);
+
+ return sendto(impl->timing_fd, pkt, sizeof(pkt), 0, dest_addr, addrlen);
+}
+
+static int write_codec_pcm(void *dst, void *frames, uint32_t n_frames)
+{
+ uint8_t *bp, *b, *d = frames;
+ int bpos = 0;
+ uint32_t i;
+
+ b = bp = dst;
+
+ bit_writer(&bp, &bpos, 1, 3); /* channel=1, stereo */
+ bit_writer(&bp, &bpos, 0, 4); /* Unknown */
+ bit_writer(&bp, &bpos, 0, 8); /* Unknown */
+ bit_writer(&bp, &bpos, 0, 4); /* Unknown */
+ bit_writer(&bp, &bpos, 1, 1); /* Hassize */
+ bit_writer(&bp, &bpos, 0, 2); /* Unused */
+ bit_writer(&bp, &bpos, 1, 1); /* Is-not-compressed */
+ bit_writer(&bp, &bpos, (n_frames >> 24) & 0xff, 8);
+ bit_writer(&bp, &bpos, (n_frames >> 16) & 0xff, 8);
+ bit_writer(&bp, &bpos, (n_frames >> 8) & 0xff, 8);
+ bit_writer(&bp, &bpos, (n_frames) & 0xff, 8);
+
+ for (i = 0; i < n_frames; i++) {
+ bit_writer(&bp, &bpos, *(d + 1), 8);
+ bit_writer(&bp, &bpos, *(d + 0), 8);
+ bit_writer(&bp, &bpos, *(d + 3), 8);
+ bit_writer(&bp, &bpos, *(d + 2), 8);
+ d += 4;
+ }
+ return bp - b + 1;
+}
+
+static int flush_to_udp_packet(struct impl *impl)
+{
+ const size_t max = 12 + 8 + impl->block_size;
+ uint32_t pkt[max], len, n_frames;
+ uint8_t *dst;
+ int res;
+
+ if (!impl->recording)
+ return 0;
+
+ impl->sync++;
+ if (impl->first || impl->sync == impl->sync_period) {
+ impl->sync = 0;
+ send_udp_sync_packet(impl, NULL, 0);
+ }
+ pkt[0] = htonl(0x80600000);
+ if (impl->first)
+ pkt[0] |= htonl((uint32_t)0x80 << 16);
+ pkt[0] |= htonl((uint32_t)impl->seq);
+ pkt[1] = htonl(impl->rtptime);
+ pkt[2] = htonl(impl->ssrc);
+
+ n_frames = impl->filled / impl->frame_size;
+ dst = (uint8_t*)&pkt[3];
+
+ switch (impl->codec) {
+ case CODEC_PCM:
+ case CODEC_ALAC:
+ len = write_codec_pcm(dst, impl->buffer, n_frames);
+ break;
+ default:
+ len = 8 + impl->block_size;
+ memset(dst, 0, len);
+ break;
+ }
+ if (impl->encryption == CRYPTO_RSA)
+ aes_encrypt(impl, dst, len);
+
+ impl->rtptime += n_frames;
+ impl->seq = (impl->seq + 1) & 0xffff;
+
+ pw_log_debug("send %u", len + 12);
+ res = send(impl->server_fd, pkt, len + 12, 0);
+
+ impl->first = false;
+
+ return res;
+}
+
+static int flush_to_tcp_packet(struct impl *impl)
+{
+ const size_t max = 16 + 8 + impl->block_size;
+ uint32_t pkt[max], len, n_frames;
+ uint8_t *dst;
+ int res;
+
+ if (!impl->recording)
+ return 0;
+
+ pkt[0] = htonl(0x24000000);
+ pkt[1] = htonl(0x80e00000);
+ pkt[1] |= htonl((uint32_t)impl->seq);
+ pkt[2] = htonl(impl->rtptime);
+ pkt[3] = htonl(impl->ssrc);
+
+ n_frames = impl->filled / impl->frame_size;
+ dst = (uint8_t*)&pkt[4];
+
+ switch (impl->codec) {
+ case CODEC_PCM:
+ case CODEC_ALAC:
+ len = write_codec_pcm(dst, impl->buffer, n_frames);
+ break;
+ default:
+ len = 8 + impl->block_size;
+ memset(dst, 0, len);
+ break;
+ }
+ if (impl->encryption == CRYPTO_RSA)
+ aes_encrypt(impl, dst, len);
+
+ pkt[0] |= htonl((uint32_t) len + 12);
+
+ impl->rtptime += n_frames;
+ impl->seq = (impl->seq + 1) & 0xffff;
+
+ pw_log_debug("send %u", len + 16);
+ res = send(impl->server_fd, pkt, len + 16, 0);
+
+ impl->first = false;
+
+ return res;
+}
+
+static void playback_stream_process(void *d)
+{
+ struct impl *impl = d;
+ struct pw_buffer *buf;
+ struct spa_data *bd;
+ uint8_t *data;
+ uint32_t offs, size;
+
+ if ((buf = pw_stream_dequeue_buffer(impl->stream)) == NULL) {
+ pw_log_debug("out of buffers: %m");
+ return;
+ }
+
+ bd = &buf->buffer->datas[0];
+
+ offs = SPA_MIN(bd->chunk->offset, bd->maxsize);
+ size = SPA_MIN(bd->chunk->size, bd->maxsize - offs);
+ data = SPA_PTROFF(bd->data, offs, uint8_t);
+
+ while (size > 0 && impl->block_size > 0) {
+ uint32_t avail, to_fill;
+
+ avail = impl->block_size - impl->filled;
+ to_fill = SPA_MIN(avail, size);
+
+ memcpy(&impl->buffer[impl->filled], data, to_fill);
+
+ impl->filled += to_fill;
+ avail -= to_fill;
+ size -= to_fill;
+ data += to_fill;
+
+ if (avail == 0) {
+ switch (impl->protocol) {
+ case PROTO_UDP:
+ flush_to_udp_packet(impl);
+ break;
+ case PROTO_TCP:
+ flush_to_tcp_packet(impl);
+ break;
+ }
+ impl->filled = 0;
+ }
+ }
+
+ pw_stream_queue_buffer(impl->stream, buf);
+}
+
+static int create_udp_socket(struct impl *impl, uint16_t *port)
+{
+ int res, ip_version, fd, val, i, af;
+ struct sockaddr_in sa4;
+ struct sockaddr_in6 sa6;
+
+ if ((res = pw_rtsp_client_get_local_ip(impl->rtsp,
+ &ip_version, NULL, 0)) < 0)
+ return res;
+
+ if (ip_version == 4) {
+ sa4.sin_family = af = AF_INET;
+ sa4.sin_addr.s_addr = INADDR_ANY;
+ } else {
+ sa6.sin6_family = af = AF_INET6;
+ sa6.sin6_addr = in6addr_any;
+ }
+
+ if ((fd = socket(af, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0)) < 0) {
+ pw_log_error("socket failed: %m");
+ return -errno;
+ }
+
+#ifdef SO_TIMESTAMP
+ val = 1;
+ if (setsockopt(fd, SOL_SOCKET, SO_TIMESTAMP, &val, sizeof(val)) < 0) {
+ res = -errno;
+ pw_log_error("setsockopt failed: %m");
+ goto error;
+ }
+#endif
+ val = 1;
+ if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(val)) < 0) {
+ res = -errno;
+ pw_log_error("setsockopt failed: %m");
+ goto error;
+ }
+
+ for (i = 0; i < MAX_PORT_RETRY; i++) {
+ int ret;
+
+ if (ip_version == 4) {
+ sa4.sin_port = htons(*port);
+ ret = bind(fd, (struct sockaddr*)&sa4, sizeof(sa4));
+ } else {
+ sa6.sin6_port = htons(*port);
+ ret = bind(fd, (struct sockaddr*)&sa6, sizeof(sa6));
+ }
+ if (ret == 0)
+ break;
+ if (ret < 0 && errno != EADDRINUSE) {
+ res = -errno;
+ pw_log_error("bind failed: %m");
+ goto error;
+ }
+ (*port)++;
+ }
+ return fd;
+error:
+ close(fd);
+ return res;
+}
+
+static int connect_socket(struct impl *impl, int type, int fd, uint16_t port)
+{
+ const char *host;
+ struct sockaddr_in sa4;
+ struct sockaddr_in6 sa6;
+ struct sockaddr *sa;
+ size_t salen;
+ int res, af;
+
+ host = pw_properties_get(impl->props, "raop.hostname");
+ if (host == NULL)
+ return -EINVAL;
+
+ if (inet_pton(AF_INET, host, &sa4.sin_addr) > 0) {
+ sa4.sin_family = af = AF_INET;
+ sa4.sin_port = htons(port);
+ sa = (struct sockaddr *) &sa4;
+ salen = sizeof(sa4);
+ } else if (inet_pton(AF_INET6, host, &sa6.sin6_addr) > 0) {
+ sa6.sin6_family = af = AF_INET6;
+ sa6.sin6_port = htons(port);
+ sa = (struct sockaddr *) &sa6;
+ salen = sizeof(sa6);
+ } else {
+ pw_log_error("Invalid host '%s'", host);
+ return -EINVAL;
+ }
+
+ if (fd < 0 &&
+ (fd = socket(af, type | SOCK_CLOEXEC | SOCK_NONBLOCK, 0)) < 0) {
+ pw_log_error("socket failed: %m");
+ return -errno;
+ }
+
+ res = connect(fd, sa, salen);
+ if (res < 0 && errno != EINPROGRESS) {
+ res = -errno;
+ pw_log_error("connect failed: %m");
+ goto error;
+ }
+ pw_log_info("Connected to host:%s port:%d", host, port);
+ return fd;
+
+error:
+ if (fd >= 0)
+ close(fd);
+ return res;
+}
+
+static void
+on_timing_source_io(void *data, int fd, uint32_t mask)
+{
+ struct impl *impl = data;
+ uint32_t packet[8];
+ ssize_t bytes;
+
+ if (mask & SPA_IO_IN) {
+ uint64_t remote, received;
+ struct sockaddr_storage sender;
+ socklen_t sender_size = sizeof(sender);
+
+ received = ntp_now(CLOCK_MONOTONIC);
+ bytes = recvfrom(impl->timing_fd, packet, sizeof(packet), 0,
+ (struct sockaddr*)&sender, &sender_size);
+ if (bytes < 0) {
+ pw_log_debug("error reading timing packet: %m");
+ return;
+ }
+ if (bytes != sizeof(packet)) {
+ pw_log_warn("discarding short (%zd < %zd) timing packet",
+ bytes, sizeof(bytes));
+ return;
+ }
+ if (packet[0] != ntohl(0x80d20007))
+ return;
+
+ remote = ((uint64_t)ntohl(packet[6])) << 32 | ntohl(packet[7]);
+ if (send_udp_timing_packet(impl, remote, received,
+ (struct sockaddr *)&sender, sender_size) < 0) {
+ pw_log_warn("error sending timing packet");
+ return;
+ }
+ }
+}
+
+
+static void
+on_control_source_io(void *data, int fd, uint32_t mask)
+{
+ struct impl *impl = data;
+ uint32_t packet[2];
+ ssize_t bytes;
+
+ if (mask & SPA_IO_IN) {
+ uint32_t hdr;
+ uint16_t seq, num;
+
+ bytes = read(impl->control_fd, packet, sizeof(packet));
+ if (bytes < 0) {
+ pw_log_debug("error reading control packet: %m");
+ return;
+ }
+ if (bytes != sizeof(packet)) {
+ pw_log_warn("discarding short (%zd < %zd) control packet",
+ bytes, sizeof(bytes));
+ return;
+ }
+ hdr = ntohl(packet[0]);
+ if ((hdr & 0xff000000) != 0x80000000)
+ return;
+
+ seq = ntohl(packet[1]) >> 16;
+ num = ntohl(packet[1]) & 0xffff;
+ if (num == 0)
+ return;
+
+ switch (hdr >> 16 & 0xff) {
+ case 0xd5:
+ pw_log_debug("retransmit request seq:%u num:%u", seq, num);
+ /* retransmit request */
+ break;
+ }
+ }
+}
+
+static int rtsp_flush_reply(void *data, int status, const struct spa_dict *headers)
+{
+ pw_log_info("reply %d", status);
+ return 0;
+}
+
+static int rtsp_do_flush(struct impl *impl)
+{
+ int res;
+
+ if (!impl->recording)
+ return 0;
+
+ pw_properties_set(impl->headers, "Range", "npt=0-");
+ pw_properties_setf(impl->headers, "RTP-Info",
+ "seq=%u;rtptime=%u", impl->seq, impl->rtptime);
+
+ impl->recording = false;
+
+ res = pw_rtsp_client_send(impl->rtsp, "FLUSH", &impl->headers->dict,
+ NULL, NULL, rtsp_flush_reply, impl);
+
+ pw_properties_set(impl->headers, "Range", NULL);
+ pw_properties_set(impl->headers, "RTP-Info", NULL);
+
+ return res;
+}
+
+static int rtsp_record_reply(void *data, int status, const struct spa_dict *headers)
+{
+ struct impl *impl = data;
+ const char *str;
+ uint32_t n_params;
+ const struct spa_pod *params[2];
+ uint8_t buffer[1024];
+ struct spa_pod_builder b;
+ struct spa_latency_info latency;
+ char progress[128];
+
+ pw_log_info("reply %d", status);
+
+ if ((str = spa_dict_lookup(headers, "Audio-Latency")) != NULL) {
+ if (!spa_atou32(str, &impl->latency, 0))
+ impl->latency = DEFAULT_LATENCY;
+ } else {
+ impl->latency = DEFAULT_LATENCY;
+ }
+
+ spa_zero(latency);
+ latency.direction = PW_DIRECTION_INPUT;
+ latency.min_rate = latency.max_rate = impl->latency;
+
+ n_params = 0;
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ params[n_params++] = spa_latency_build(&b, SPA_PARAM_Latency, &latency);
+
+ pw_stream_update_params(impl->stream, params, n_params);
+
+ impl->first = true;
+ impl->sync = 0;
+ impl->sync_period = impl->info.rate / (impl->block_size / impl->frame_size);
+ impl->recording = true;
+
+ snprintf(progress, sizeof(progress), "progress: %s/%s/%s\r\n", "0", "0", "0");
+ return pw_rtsp_client_send(impl->rtsp, "SET_PARAMETER", NULL,
+ "text/parameters", progress, NULL, NULL);
+}
+
+static int rtsp_do_record(struct impl *impl)
+{
+ int res;
+
+ if (!impl->ready || impl->recording)
+ return 0;
+
+ pw_properties_set(impl->headers, "Range", "npt=0-");
+ pw_properties_setf(impl->headers, "RTP-Info",
+ "seq=%u;rtptime=%u", impl->seq, impl->rtptime);
+
+ res = pw_rtsp_client_send(impl->rtsp, "RECORD", &impl->headers->dict,
+ NULL, NULL, rtsp_record_reply, impl);
+
+ pw_properties_set(impl->headers, "Range", NULL);
+ pw_properties_set(impl->headers, "RTP-Info", NULL);
+
+ return res;
+}
+
+static void
+on_server_source_io(void *data, int fd, uint32_t mask)
+{
+ struct impl *impl = data;
+
+ if (mask & (SPA_IO_ERR | SPA_IO_HUP))
+ goto error;
+ if (mask & SPA_IO_OUT) {
+ int res;
+ socklen_t len;
+
+ pw_loop_update_io(impl->loop, impl->server_source,
+ impl->server_source->mask & ~SPA_IO_OUT);
+
+ len = sizeof(res);
+ if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &res, &len) < 0) {
+ pw_log_error("getsockopt: %m");
+ goto error;
+ }
+ if (res != 0)
+ goto error;
+
+ impl->ready = true;
+ if (pw_stream_get_state(impl->stream, NULL) == PW_STREAM_STATE_STREAMING)
+ rtsp_do_record(impl);
+ }
+ return;
+error:
+ pw_loop_update_io(impl->loop, impl->server_source, 0);
+}
+
+static int rtsp_setup_reply(void *data, int status, const struct spa_dict *headers)
+{
+ struct impl *impl = data;
+ const char *str, *state = NULL, *s;
+ size_t len;
+ uint64_t ntp;
+ uint16_t control_port, timing_port;
+
+ pw_log_info("reply %d", status);
+
+ if ((str = spa_dict_lookup(headers, "Session")) == NULL) {
+ pw_log_error("missing Session header");
+ return 0;
+ }
+ pw_properties_set(impl->headers, "Session", str);
+
+ if ((str = spa_dict_lookup(headers, "Transport")) == NULL) {
+ pw_log_error("missing Transport header");
+ return 0;
+ }
+
+ impl->server_port = control_port = timing_port = 0;
+ while ((s = pw_split_walk(str, ";", &len, &state)) != NULL) {
+ if (spa_strstartswith(s, "server_port=")) {
+ impl->server_port = atoi(s + 12);
+ }
+ else if (spa_strstartswith(s, "control_port=")) {
+ control_port = atoi(s + 13);
+ }
+ else if (spa_strstartswith(s, "timing_port=")) {
+ timing_port = atoi(s + 12);
+ }
+ }
+ if (impl->server_port == 0) {
+ pw_log_error("missing server port in Transport");
+ return 0;
+ }
+
+ if (pw_getrandom(&impl->seq, sizeof(impl->seq), 0) < 0 ||
+ pw_getrandom(&impl->rtptime, sizeof(impl->rtptime), 0) < 0) {
+ pw_log_error("error generating random seq and rtptime: %m");
+ return 0;
+ }
+
+ pw_log_info("server port:%u", impl->server_port);
+
+ switch (impl->protocol) {
+ case PROTO_TCP:
+ if ((impl->server_fd = connect_socket(impl, SOCK_STREAM, -1, impl->server_port)) < 0)
+ return impl->server_fd;
+
+ impl->server_source = pw_loop_add_io(impl->loop, impl->server_fd,
+ SPA_IO_OUT, false, on_server_source_io, impl);
+ break;
+
+ case PROTO_UDP:
+ if (control_port == 0 || timing_port == 0) {
+ pw_log_error("missing UDP ports in Transport");
+ return 0;
+ }
+ pw_log_info("control:%u timing:%u", control_port, timing_port);
+
+ if ((impl->server_fd = connect_socket(impl, SOCK_DGRAM, -1, impl->server_port)) < 0)
+ return impl->server_fd;
+ if ((impl->control_fd = connect_socket(impl, SOCK_DGRAM, impl->control_fd, control_port)) < 0)
+ return impl->control_fd;
+ if ((impl->timing_fd = connect_socket(impl, SOCK_DGRAM, impl->timing_fd, timing_port)) < 0)
+ return impl->timing_fd;
+
+ ntp = ntp_now(CLOCK_MONOTONIC);
+ send_udp_timing_packet(impl, ntp, ntp, NULL, 0);
+
+ impl->control_source = pw_loop_add_io(impl->loop, impl->control_fd,
+ SPA_IO_IN, false, on_control_source_io, impl);
+
+ impl->ready = true;
+ if (pw_stream_get_state(impl->stream, NULL) == PW_STREAM_STATE_STREAMING)
+ rtsp_do_record(impl);
+ break;
+ default:
+ return 0;
+ }
+ return 0;
+}
+
+static int rtsp_do_setup(struct impl *impl)
+{
+ int res;
+
+ switch (impl->protocol) {
+ case PROTO_TCP:
+ pw_properties_set(impl->headers, "Transport",
+ "RTP/AVP/TCP;unicast;interleaved=0-1;mode=record");
+ break;
+
+ case PROTO_UDP:
+ impl->control_port = DEFAULT_UDP_CONTROL_PORT;
+ impl->timing_port = DEFAULT_UDP_TIMING_PORT;
+
+ impl->control_fd = create_udp_socket(impl, &impl->control_port);
+ impl->timing_fd = create_udp_socket(impl, &impl->timing_port);
+ if (impl->control_fd < 0 || impl->timing_fd < 0)
+ goto error;
+
+ impl->timing_source = pw_loop_add_io(impl->loop, impl->timing_fd,
+ SPA_IO_IN, false, on_timing_source_io, impl);
+
+ pw_properties_setf(impl->headers, "Transport",
+ "RTP/AVP/UDP;unicast;interleaved=0-1;mode=record;"
+ "control_port=%u;timing_port=%u",
+ impl->control_port, impl->timing_port);
+ break;
+
+ default:
+ return -ENOTSUP;
+ }
+
+ res = pw_rtsp_client_send(impl->rtsp, "SETUP", &impl->headers->dict,
+ NULL, NULL, rtsp_setup_reply, impl);
+
+ pw_properties_set(impl->headers, "Transport", NULL);
+
+ return res;
+error:
+ if (impl->control_fd > 0)
+ close(impl->control_fd);
+ impl->control_fd = -1;
+ if (impl->timing_fd > 0)
+ close(impl->timing_fd);
+ impl->timing_fd = -1;
+ return -EIO;
+}
+
+static int rtsp_announce_reply(void *data, int status, const struct spa_dict *headers)
+{
+ struct impl *impl = data;
+
+ pw_log_info("reply %d", status);
+
+ pw_properties_set(impl->headers, "Apple-Challenge", NULL);
+
+ return rtsp_do_setup(impl);
+}
+
+static void base64_encode(const uint8_t *data, size_t len, char *enc, char pad)
+{
+ static const char tab[] =
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+ size_t i;
+ for (i = 0; i < len; i += 3) {
+ uint32_t v;
+ v = data[i+0] << 16;
+ v |= (i+1 < len ? data[i+1] : 0) << 8;
+ v |= (i+2 < len ? data[i+2] : 0);
+ *enc++ = tab[(v >> (3*6)) & 0x3f];
+ *enc++ = tab[(v >> (2*6)) & 0x3f];
+ *enc++ = i+1 < len ? tab[(v >> (1*6)) & 0x3f] : pad;
+ *enc++ = i+2 < len ? tab[(v >> (0*6)) & 0x3f] : pad;
+ }
+ *enc = '\0';
+}
+
+static size_t base64_decode(const char *data, size_t len, uint8_t *dec)
+{
+ uint8_t tab[] = {
+ 62, -1, -1, -1, 63, 52, 53, 54, 55, 56,
+ 57, 58, 59, 60, 61, -1, -1, -1, -1, -1,
+ -1, -1, 0, 1, 2, 3, 4, 5, 6, 7,
+ 8, 9, 10, 11, 12, 13, 14, 15, 16, 17,
+ 18, 19, 20, 21, 22, 23, 24, 25, -1, -1,
+ -1, -1, -1, -1, 26, 27, 28, 29, 30, 31,
+ 32, 33, 34, 35, 36, 37, 38, 39, 40, 41,
+ 42, 43, 44, 45, 46, 47, 48, 49, 50, 51 };
+ size_t i, j;
+ for (i = 0, j = 0; i < len; i += 4) {
+ uint32_t v;
+ v = tab[data[i+0]-43] << (3*6);
+ v |= tab[data[i+1]-43] << (2*6);
+ v |= (data[i+2] == '=' ? 0 : tab[data[i+2]-43]) << (1*6);
+ v |= (data[i+3] == '=' ? 0 : tab[data[i+3]-43]);
+ dec[j++] = (v >> 16) & 0xff;
+ if (data[i+2] != '=') dec[j++] = (v >> 8) & 0xff;
+ if (data[i+3] != '=') dec[j++] = v & 0xff;
+ }
+ return j;
+}
+
+static int rsa_encrypt(uint8_t *data, int len, uint8_t *res)
+{
+ RSA *rsa;
+ uint8_t modulus[256];
+ uint8_t exponent[8];
+ size_t size;
+ BIGNUM *n_bn = NULL;
+ BIGNUM *e_bn = NULL;
+ char n[] =
+ "59dE8qLieItsH1WgjrcFRKj6eUWqi+bGLOX1HL3U3GhC/j0Qg90u3sG/1CUtwC"
+ "5vOYvfDmFI6oSFXi5ELabWJmT2dKHzBJKa3k9ok+8t9ucRqMd6DZHJ2YCCLlDR"
+ "KSKv6kDqnw4UwPdpOMXziC/AMj3Z/lUVX1G7WSHCAWKf1zNS1eLvqr+boEjXuB"
+ "OitnZ/bDzPHrTOZz0Dew0uowxf/+sG+NCK3eQJVxqcaJ/vEHKIVd2M+5qL71yJ"
+ "Q+87X6oV3eaYvt3zWZYD6z5vYTcrtij2VZ9Zmni/UAaHqn9JdsBWLUEpVviYnh"
+ "imNVvYFZeCXg/IdTQ+x4IRdiXNv5hEew==";
+ char e[] = "AQAB";
+
+ rsa = RSA_new();
+
+ size = base64_decode(n, strlen(n), modulus);
+ n_bn = BN_bin2bn(modulus, size, NULL);
+
+ size = base64_decode(e, strlen(e), exponent);
+ e_bn = BN_bin2bn(exponent, size, NULL);
+
+ RSA_set0_key(rsa, n_bn, e_bn, NULL);
+
+ size = RSA_public_encrypt(len, data, res, rsa, RSA_PKCS1_OAEP_PADDING);
+ RSA_free(rsa);
+ return size;
+}
+
+static int rtsp_do_announce(struct impl *impl)
+{
+ const char *host;
+ uint8_t rsakey[512];
+ char key[512*2];
+ char iv[16*2];
+ int res, frames, i, ip_version;
+ char *sdp;
+ char local_ip[256];
+ int min_latency;
+ min_latency = DEFAULT_LATENCY;
+ host = pw_properties_get(impl->props, "raop.hostname");
+
+ if (impl->protocol == PROTO_TCP)
+ frames = FRAMES_PER_TCP_PACKET;
+ else
+ frames = FRAMES_PER_UDP_PACKET;
+
+ impl->block_size = frames * impl->frame_size;
+
+ pw_rtsp_client_get_local_ip(impl->rtsp, &ip_version,
+ local_ip, sizeof(local_ip));
+
+ switch (impl->encryption) {
+ case CRYPTO_NONE:
+ asprintf(&sdp, "v=0\r\n"
+ "o=iTunes %s 0 IN IP%d %s\r\n"
+ "s=iTunes\r\n"
+ "c=IN IP%d %s\r\n"
+ "t=0 0\r\n"
+ "m=audio 0 RTP/AVP 96\r\n"
+ "a=rtpmap:96 AppleLossless\r\n"
+ "a=fmtp:96 %d 0 16 40 10 14 2 255 0 0 44100\r\n",
+ impl->session_id, ip_version, local_ip,
+ ip_version, host, frames);
+ break;
+
+ case CRYPTO_AUTH_SETUP:
+ asprintf(&sdp, "v=0\r\n"
+ "o=iTunes %s 0 IN IP%d %s\r\n"
+ "s=iTunes\r\n"
+ "c=IN IP%d %s\r\n"
+ "t=0 0\r\n"
+ "m=audio 0 RTP/AVP 96\r\n"
+ "a=rtpmap:96 AppleLossless\r\n"
+ "a=fmtp:96 %d 0 16 40 10 14 2 255 0 0 44100\r\n"
+ "a=min-latency:%d",
+ impl->session_id, ip_version, local_ip,
+ ip_version, host, frames, min_latency);
+ break;
+
+ case CRYPTO_RSA:
+ if (pw_getrandom(impl->key, sizeof(impl->key), 0) < 0 ||
+ pw_getrandom(impl->iv, sizeof(impl->iv), 0) < 0)
+ return -errno;
+
+ AES_set_encrypt_key(impl->key, 128, &impl->aes);
+
+ i = rsa_encrypt(impl->key, 16, rsakey);
+ base64_encode(rsakey, i, key, '=');
+ base64_encode(impl->iv, 16, iv, '=');
+
+ asprintf(&sdp, "v=0\r\n"
+ "o=iTunes %s 0 IN IP%d %s\r\n"
+ "s=iTunes\r\n"
+ "c=IN IP%d %s\r\n"
+ "t=0 0\r\n"
+ "m=audio 0 RTP/AVP 96\r\n"
+ "a=rtpmap:96 AppleLossless\r\n"
+ "a=fmtp:96 %d 0 16 40 10 14 2 255 0 0 44100\r\n"
+ "a=rsaaeskey:%s\r\n"
+ "a=aesiv:%s\r\n",
+ impl->session_id, ip_version, local_ip,
+ ip_version, host, frames, key, iv);
+ break;
+ default:
+ return -ENOTSUP;
+ }
+ res = pw_rtsp_client_send(impl->rtsp, "ANNOUNCE", &impl->headers->dict,
+ "application/sdp", sdp, rtsp_announce_reply, impl);
+ free(sdp);
+
+ return res;
+}
+
+static int rtsp_auth_setup_reply(void *data, int status, const struct spa_dict *headers)
+{
+ struct impl *impl = data;
+
+ pw_log_info("reply %d", status);
+
+ return rtsp_do_announce(impl);
+}
+
+static int rtsp_do_auth_setup(struct impl *impl)
+{
+ static const unsigned char content[33] =
+ "\x01"
+ "\x59\x02\xed\xe9\x0d\x4e\xf2\xbd\x4c\xb6\x8a\x63\x30\x03\x82\x07"
+ "\xa9\x4d\xbd\x50\xd8\xaa\x46\x5b\x5d\x8c\x01\x2a\x0c\x7e\x1d\x4e";
+
+ return pw_rtsp_client_url_send(impl->rtsp, "/auth-setup", "POST", &impl->headers->dict,
+ "application/octet-stream", content, sizeof(content),
+ rtsp_auth_setup_reply, impl);
+}
+
+static const char *find_attr(char **tokens, const char *key)
+{
+ int i;
+ char *p, *s;
+ for (i = 0; tokens[i]; i++) {
+ if (!spa_strstartswith(tokens[i], key))
+ continue;
+ p = tokens[i] + strlen(key);
+ if ((s = rindex(p, '"')) == NULL)
+ continue;
+ *s = '\0';
+ if ((s = index(p, '"')) == NULL)
+ continue;
+ return s+1;
+ }
+ return NULL;
+}
+
+static int rtsp_auth_reply(void *data, int status, const struct spa_dict *headers)
+{
+ struct impl *impl = data;
+ int res = 0;
+
+ pw_log_info("auth %d", status);
+
+ switch (status) {
+ case 200:
+ if (impl->encryption == CRYPTO_AUTH_SETUP)
+ res = rtsp_do_auth_setup(impl);
+ else
+ res = rtsp_do_announce(impl);
+ break;
+ }
+ return res;
+}
+
+SPA_PRINTF_FUNC(2,3)
+static int MD5_hash(char hash[MD5_HASH_LENGTH+1], const char *fmt, ...)
+{
+ unsigned char d[MD5_DIGEST_LENGTH];
+ int i;
+ va_list args;
+ char buffer[1024];
+
+ va_start(args, fmt);
+ vsnprintf(buffer, sizeof(buffer), fmt, args);
+ va_end(args);
+
+ MD5((unsigned char*) buffer, strlen(buffer), d);
+ for (i = 0; i < MD5_DIGEST_LENGTH; i++)
+ sprintf(&hash[2*i], "%02x", (uint8_t) d[i]);
+ hash[MD5_HASH_LENGTH] = '\0';
+ return 0;
+}
+
+static int rtsp_do_auth(struct impl *impl, const struct spa_dict *headers)
+{
+ const char *str;
+ char **tokens;
+ int n_tokens;
+ char auth[1024];
+
+ if (impl->password == NULL)
+ return -ENOTSUP;
+
+ if ((str = spa_dict_lookup(headers, "WWW-Authenticate")) == NULL)
+ return -ENOENT;
+
+ pw_log_info("Auth: %s", str);
+
+ tokens = pw_split_strv(str, " ", INT_MAX, &n_tokens);
+ if (tokens == NULL || tokens[0] == NULL)
+ goto error;
+
+ if (spa_streq(tokens[0], "Basic")) {
+ char buf[256];
+ char enc[512];
+ spa_scnprintf(buf, sizeof(buf), "%s:%s", DEFAULT_USER_NAME, impl->password);
+ base64_encode((uint8_t*)buf, strlen(buf), enc, '=');
+ spa_scnprintf(auth, sizeof(auth), "Basic %s", enc);
+ }
+ else if (spa_streq(tokens[0], "Digest")) {
+ const char *realm, *nonce, *url;
+ char h1[MD5_HASH_LENGTH+1];
+ char h2[MD5_HASH_LENGTH+1];
+ char resp[MD5_HASH_LENGTH+1];
+
+ realm = find_attr(tokens, "realm");
+ nonce = find_attr(tokens, "nonce");
+ if (realm == NULL || nonce == NULL)
+ goto error;
+
+ url = pw_rtsp_client_get_url(impl->rtsp);
+
+ MD5_hash(h1, "%s:%s:%s", DEFAULT_USER_NAME, realm, impl->password);
+ MD5_hash(h2, "OPTIONS:%s", url);
+ MD5_hash(resp, "%s:%s:%s", h1, nonce, h2);
+
+ spa_scnprintf(auth, sizeof(auth),
+ "username=\"%s\", realm=\"%s\", nonce=\"%s\", uri=\"%s\", response=\"%s\"",
+ DEFAULT_USER_NAME, realm, nonce, url, resp);
+ }
+ else
+ goto error;
+
+ pw_properties_setf(impl->headers, "Authorization", "%s %s",
+ tokens[0], auth);
+ pw_free_strv(tokens);
+
+ pw_rtsp_client_send(impl->rtsp, "OPTIONS", &impl->headers->dict,
+ NULL, NULL, rtsp_auth_reply, impl);
+
+ return 0;
+error:
+ pw_free_strv(tokens);
+ return -EINVAL;
+}
+
+static int rtsp_options_reply(void *data, int status, const struct spa_dict *headers)
+{
+ struct impl *impl = data;
+ int res = 0;
+
+ pw_log_info("options %d", status);
+
+ switch (status) {
+ case 401:
+ res = rtsp_do_auth(impl, headers);
+ break;
+ case 200:
+ if (impl->encryption == CRYPTO_AUTH_SETUP)
+ res = rtsp_do_auth_setup(impl);
+ else
+ res = rtsp_do_announce(impl);
+ break;
+ }
+ return res;
+}
+
+static void rtsp_connected(void *data)
+{
+ struct impl *impl = data;
+ uint32_t sci[2];
+ uint8_t rac[16];
+ char sac[16*4];
+
+ pw_log_info("connected");
+
+ impl->connected = true;
+
+ if (pw_getrandom(sci, sizeof(sci), 0) < 0 ||
+ pw_getrandom(rac, sizeof(rac), 0) < 0) {
+ pw_log_error("error generating random data: %m");
+ return;
+ }
+
+ pw_properties_setf(impl->headers, "Client-Instance",
+ "%08x%08x", sci[0], sci[1]);
+
+ base64_encode(rac, sizeof(rac), sac, '\0');
+ pw_properties_set(impl->headers, "Apple-Challenge", sac);
+
+ pw_properties_set(impl->headers, "User-Agent", DEFAULT_USER_AGENT);
+
+ pw_rtsp_client_send(impl->rtsp, "OPTIONS", &impl->headers->dict,
+ NULL, NULL, rtsp_options_reply, impl);
+}
+
+static void connection_cleanup(struct impl *impl)
+{
+ impl->ready = false;
+ if (impl->server_source != NULL) {
+ pw_loop_destroy_source(impl->loop, impl->server_source);
+ impl->server_source = NULL;
+ }
+ if (impl->server_fd >= 0) {
+ close(impl->server_fd);
+ impl->server_fd = -1;
+ }
+ if (impl->control_source != NULL) {
+ pw_loop_destroy_source(impl->loop, impl->control_source);
+ impl->control_source = NULL;
+ }
+ if (impl->control_fd >= 0) {
+ close(impl->control_fd);
+ impl->control_fd = -1;
+ }
+ if (impl->timing_source != NULL) {
+ pw_loop_destroy_source(impl->loop, impl->timing_source);
+ impl->timing_source = NULL;
+ }
+ if (impl->timing_fd >= 0) {
+ close(impl->timing_fd);
+ impl->timing_fd = -1;
+ }
+}
+
+static void rtsp_disconnected(void *data)
+{
+ struct impl *impl = data;
+ pw_log_info("disconnected");
+ impl->connected = false;
+ connection_cleanup(impl);
+}
+
+static void rtsp_error(void *data, int res)
+{
+ pw_log_error("error %d", res);
+}
+
+static void rtsp_message(void *data, int status,
+ const struct spa_dict *headers)
+{
+ const struct spa_dict_item *it;
+ pw_log_info("message %d", status);
+ spa_dict_for_each(it, headers)
+ pw_log_info(" %s: %s", it->key, it->value);
+
+}
+
+static const struct pw_rtsp_client_events rtsp_events = {
+ PW_VERSION_RTSP_CLIENT_EVENTS,
+ .connected = rtsp_connected,
+ .error = rtsp_error,
+ .disconnected = rtsp_disconnected,
+ .message = rtsp_message,
+};
+
+static void stream_state_changed(void *d, enum pw_stream_state old,
+ enum pw_stream_state state, const char *error)
+{
+ struct impl *impl = d;
+ switch (state) {
+ case PW_STREAM_STATE_ERROR:
+ case PW_STREAM_STATE_UNCONNECTED:
+ pw_impl_module_schedule_destroy(impl->module);
+ break;
+ case PW_STREAM_STATE_PAUSED:
+ rtsp_do_flush(impl);
+ break;
+ case PW_STREAM_STATE_STREAMING:
+ rtsp_do_record(impl);
+ break;
+ default:
+ break;
+ }
+}
+
+static int rtsp_do_connect(struct impl *impl)
+{
+ const char *hostname, *port;
+ uint32_t session_id;
+
+ if (impl->connected) {
+ if (!impl->ready)
+ return rtsp_do_announce(impl);
+ return 0;
+ }
+
+ hostname = pw_properties_get(impl->props, "raop.hostname");
+ port = pw_properties_get(impl->props, "raop.port");
+ if (hostname == NULL || port == NULL)
+ return -EINVAL;
+
+ if (pw_getrandom(&session_id, sizeof(session_id), 0) < 0)
+ return -errno;
+
+ spa_scnprintf(impl->session_id, sizeof(impl->session_id), "%u", session_id);
+
+ return pw_rtsp_client_connect(impl->rtsp, hostname, atoi(port), impl->session_id);
+}
+
+static int rtsp_teardown_reply(void *data, int status, const struct spa_dict *headers)
+{
+ struct impl *impl = data;
+ const char *str;
+
+ pw_log_info("reply");
+
+ connection_cleanup(impl);
+
+ if ((str = spa_dict_lookup(headers, "Connection")) != NULL) {
+ if (spa_streq(str, "close"))
+ pw_rtsp_client_disconnect(impl->rtsp);
+ }
+ return 0;
+}
+
+static int rtsp_do_teardown(struct impl *impl)
+{
+ if (!impl->ready)
+ return 0;
+
+ return pw_rtsp_client_send(impl->rtsp, "TEARDOWN", NULL,
+ NULL, NULL, rtsp_teardown_reply, impl);
+}
+
+static void stream_param_changed(void *data, uint32_t id, const struct spa_pod *param)
+{
+ struct impl *impl = data;
+
+ switch (id) {
+ case SPA_PARAM_Format:
+ if (param == NULL)
+ rtsp_do_teardown(impl);
+ else
+ rtsp_do_connect(impl);
+ break;
+ default:
+ break;
+ }
+}
+
+static const struct pw_stream_events playback_stream_events = {
+ PW_VERSION_STREAM_EVENTS,
+ .destroy = stream_destroy,
+ .state_changed = stream_state_changed,
+ .param_changed = stream_param_changed,
+ .process = playback_stream_process
+};
+
+static int create_stream(struct impl *impl)
+{
+ int res;
+ uint32_t n_params;
+ const struct spa_pod *params[1];
+ uint8_t buffer[1024];
+ struct spa_pod_builder b;
+
+ impl->stream = pw_stream_new(impl->core, "RAOP sink", impl->stream_props);
+ impl->stream_props = NULL;
+
+ if (impl->stream == NULL)
+ return -errno;
+
+ pw_stream_add_listener(impl->stream,
+ &impl->stream_listener,
+ &playback_stream_events, impl);
+
+ n_params = 0;
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ params[n_params++] = spa_format_audio_raw_build(&b,
+ SPA_PARAM_EnumFormat, &impl->info);
+
+ if ((res = pw_stream_connect(impl->stream,
+ PW_DIRECTION_INPUT,
+ PW_ID_ANY,
+ PW_STREAM_FLAG_MAP_BUFFERS |
+ PW_STREAM_FLAG_RT_PROCESS,
+ params, n_params)) < 0)
+ return res;
+
+ impl->headers = pw_properties_new(NULL, NULL);
+
+ impl->rtsp = pw_rtsp_client_new(impl->loop, NULL, 0);
+ if (impl->rtsp == NULL)
+ return -errno;
+
+ pw_rtsp_client_add_listener(impl->rtsp, &impl->rtsp_listener,
+ &rtsp_events, impl);
+
+ return 0;
+}
+
+static void core_error(void *data, uint32_t id, int seq, int res, const char *message)
+{
+ struct impl *impl = data;
+
+ pw_log_error("error id:%u seq:%d res:%d (%s): %s",
+ id, seq, res, spa_strerror(res), message);
+
+ if (id == PW_ID_CORE && res == -EPIPE)
+ pw_impl_module_schedule_destroy(impl->module);
+}
+
+static const struct pw_core_events core_events = {
+ PW_VERSION_CORE_EVENTS,
+ .error = core_error,
+};
+
+static void core_destroy(void *d)
+{
+ struct impl *impl = d;
+ spa_hook_remove(&impl->core_listener);
+ impl->core = NULL;
+ pw_impl_module_schedule_destroy(impl->module);
+}
+
+static const struct pw_proxy_events core_proxy_events = {
+ .destroy = core_destroy,
+};
+
+static void impl_destroy(struct impl *impl)
+{
+ if (impl->stream)
+ pw_stream_destroy(impl->stream);
+ if (impl->core && impl->do_disconnect)
+ pw_core_disconnect(impl->core);
+
+ if (impl->rtsp)
+ pw_rtsp_client_destroy(impl->rtsp);
+
+ pw_properties_free(impl->headers);
+ pw_properties_free(impl->stream_props);
+ pw_properties_free(impl->props);
+ free(impl->password);
+ free(impl);
+}
+
+static void module_destroy(void *data)
+{
+ struct impl *impl = data;
+ spa_hook_remove(&impl->module_listener);
+ impl_destroy(impl);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy,
+};
+
+static inline uint32_t format_from_name(const char *name, size_t len)
+{
+ int i;
+ for (i = 0; spa_type_audio_format[i].name; i++) {
+ if (strncmp(name, spa_debug_type_short_name(spa_type_audio_format[i].name), len) == 0)
+ return spa_type_audio_format[i].type;
+ }
+ return SPA_AUDIO_FORMAT_UNKNOWN;
+}
+
+static uint32_t channel_from_name(const char *name)
+{
+ int i;
+ for (i = 0; spa_type_audio_channel[i].name; i++) {
+ if (spa_streq(name, spa_debug_type_short_name(spa_type_audio_channel[i].name)))
+ return spa_type_audio_channel[i].type;
+ }
+ return SPA_AUDIO_CHANNEL_UNKNOWN;
+}
+
+static void parse_position(struct spa_audio_info_raw *info, const char *val, size_t len)
+{
+ struct spa_json it[2];
+ char v[256];
+
+ spa_json_init(&it[0], val, len);
+ if (spa_json_enter_array(&it[0], &it[1]) <= 0)
+ spa_json_init(&it[1], val, len);
+
+ info->channels = 0;
+ while (spa_json_get_string(&it[1], v, sizeof(v)) > 0 &&
+ info->channels < SPA_AUDIO_MAX_CHANNELS) {
+ info->position[info->channels++] = channel_from_name(v);
+ }
+}
+
+static void parse_audio_info(const struct pw_properties *props, struct spa_audio_info_raw *info)
+{
+ const char *str;
+
+ spa_zero(*info);
+ if ((str = pw_properties_get(props, PW_KEY_AUDIO_FORMAT)) == NULL)
+ str = DEFAULT_FORMAT;
+ info->format = format_from_name(str, strlen(str));
+
+ info->rate = pw_properties_get_uint32(props, PW_KEY_AUDIO_RATE, info->rate);
+ if (info->rate == 0)
+ info->rate = DEFAULT_RATE;
+
+ info->channels = pw_properties_get_uint32(props, PW_KEY_AUDIO_CHANNELS, info->channels);
+ info->channels = SPA_MIN(info->channels, SPA_AUDIO_MAX_CHANNELS);
+ if ((str = pw_properties_get(props, SPA_KEY_AUDIO_POSITION)) != NULL)
+ parse_position(info, str, strlen(str));
+ if (info->channels == 0)
+ parse_position(info, DEFAULT_POSITION, strlen(DEFAULT_POSITION));
+}
+
+static int calc_frame_size(struct spa_audio_info_raw *info)
+{
+ int res = info->channels;
+ switch (info->format) {
+ case SPA_AUDIO_FORMAT_U8:
+ case SPA_AUDIO_FORMAT_S8:
+ case SPA_AUDIO_FORMAT_ALAW:
+ case SPA_AUDIO_FORMAT_ULAW:
+ return res;
+ case SPA_AUDIO_FORMAT_S16:
+ case SPA_AUDIO_FORMAT_S16_OE:
+ case SPA_AUDIO_FORMAT_U16:
+ return res * 2;
+ case SPA_AUDIO_FORMAT_S24:
+ case SPA_AUDIO_FORMAT_S24_OE:
+ case SPA_AUDIO_FORMAT_U24:
+ return res * 3;
+ case SPA_AUDIO_FORMAT_S24_32:
+ case SPA_AUDIO_FORMAT_S24_32_OE:
+ case SPA_AUDIO_FORMAT_S32:
+ case SPA_AUDIO_FORMAT_S32_OE:
+ case SPA_AUDIO_FORMAT_U32:
+ case SPA_AUDIO_FORMAT_U32_OE:
+ case SPA_AUDIO_FORMAT_F32:
+ case SPA_AUDIO_FORMAT_F32_OE:
+ return res * 4;
+ case SPA_AUDIO_FORMAT_F64:
+ case SPA_AUDIO_FORMAT_F64_OE:
+ return res * 8;
+ default:
+ return 0;
+ }
+}
+
+static void copy_props(struct impl *impl, struct pw_properties *props, const char *key)
+{
+ const char *str;
+ if ((str = pw_properties_get(props, key)) != NULL) {
+ if (pw_properties_get(impl->stream_props, key) == NULL)
+ pw_properties_set(impl->stream_props, key, str);
+ }
+}
+
+SPA_EXPORT
+int pipewire__module_init(struct pw_impl_module *module, const char *args)
+{
+ struct pw_context *context = pw_impl_module_get_context(module);
+ struct pw_properties *props = NULL;
+ uint32_t id = pw_global_get_id(pw_impl_module_get_global(module));
+ uint32_t pid = getpid();
+ struct impl *impl;
+ const char *str;
+ int res;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ impl = calloc(1, sizeof(struct impl));
+ if (impl == NULL)
+ return -errno;
+
+ pw_log_debug("module %p: new %s", impl, args);
+ impl->server_fd = -1;
+ impl->control_fd = -1;
+ impl->timing_fd = -1;
+
+ if (args == NULL)
+ args = "";
+
+ props = pw_properties_new_string(args);
+ if (props == NULL) {
+ res = -errno;
+ pw_log_error( "can't create properties: %m");
+ goto error;
+ }
+ impl->props = props;
+
+ impl->stream_props = pw_properties_new(NULL, NULL);
+ if (impl->stream_props == NULL) {
+ res = -errno;
+ pw_log_error( "can't create properties: %m");
+ goto error;
+ }
+
+ impl->module = module;
+ impl->context = context;
+ impl->loop = pw_context_get_main_loop(context);
+
+ if (pw_properties_get(props, PW_KEY_NODE_VIRTUAL) == NULL)
+ pw_properties_set(props, PW_KEY_NODE_VIRTUAL, "true");
+
+ if (pw_properties_get(props, PW_KEY_MEDIA_CLASS) == NULL)
+ pw_properties_set(props, PW_KEY_MEDIA_CLASS, "Audio/Sink");
+
+ if (pw_properties_get(props, PW_KEY_NODE_NAME) == NULL)
+ pw_properties_setf(props, PW_KEY_NODE_NAME, "raop-sink-%u-%u", pid, id);
+ if (pw_properties_get(props, PW_KEY_NODE_DESCRIPTION) == NULL)
+ pw_properties_set(props, PW_KEY_NODE_DESCRIPTION,
+ pw_properties_get(props, PW_KEY_NODE_NAME));
+ if (pw_properties_get(props, PW_KEY_NODE_LATENCY) == NULL)
+ pw_properties_set(props, PW_KEY_NODE_LATENCY, "352/44100");
+
+ if ((str = pw_properties_get(props, "stream.props")) != NULL)
+ pw_properties_update_string(impl->stream_props, str, strlen(str));
+
+ copy_props(impl, props, PW_KEY_AUDIO_FORMAT);
+ copy_props(impl, props, PW_KEY_AUDIO_RATE);
+ copy_props(impl, props, PW_KEY_AUDIO_CHANNELS);
+ copy_props(impl, props, SPA_KEY_AUDIO_POSITION);
+ copy_props(impl, props, PW_KEY_NODE_NAME);
+ copy_props(impl, props, PW_KEY_NODE_DESCRIPTION);
+ copy_props(impl, props, PW_KEY_NODE_GROUP);
+ copy_props(impl, props, PW_KEY_NODE_LATENCY);
+ copy_props(impl, props, PW_KEY_NODE_VIRTUAL);
+ copy_props(impl, props, PW_KEY_MEDIA_CLASS);
+
+ parse_audio_info(impl->stream_props, &impl->info);
+
+ impl->frame_size = calc_frame_size(&impl->info);
+ if (impl->frame_size == 0) {
+ pw_log_error("unsupported audio format:%d channels:%d",
+ impl->info.format, impl->info.channels);
+ res = -EINVAL;
+ goto error;
+ }
+
+ if ((str = pw_properties_get(props, "raop.transport")) == NULL)
+ str = "udp";
+ if (spa_streq(str, "udp"))
+ impl->protocol = PROTO_UDP;
+ else if (spa_streq(str, "tcp"))
+ impl->protocol = PROTO_TCP;
+ else {
+ pw_log_error( "can't handle transport %s", str);
+ res = -EINVAL;
+ goto error;
+ }
+
+ if ((str = pw_properties_get(props, "raop.encryption.type")) == NULL)
+ str = "none";
+ if (spa_streq(str, "none"))
+ impl->encryption = CRYPTO_NONE;
+ else if (spa_streq(str, "RSA"))
+ impl->encryption = CRYPTO_RSA;
+ else if (spa_streq(str, "auth_setup"))
+ impl->encryption = CRYPTO_AUTH_SETUP;
+ else {
+ pw_log_error( "can't handle encryption type %s", str);
+ res = -EINVAL;
+ goto error;
+ }
+
+ if ((str = pw_properties_get(props, "raop.audio.codec")) == NULL)
+ str = "PCM";
+ if (spa_streq(str, "PCM"))
+ impl->codec = CODEC_PCM;
+ else if (spa_streq(str, "ALAC"))
+ impl->codec = CODEC_ALAC;
+ else {
+ pw_log_error( "can't handle codec type %s", str);
+ res = -EINVAL;
+ goto error;
+ }
+ str = pw_properties_get(props, "raop.password");
+ impl->password = str ? strdup(str) : NULL;
+
+ impl->core = pw_context_get_object(impl->context, PW_TYPE_INTERFACE_Core);
+ if (impl->core == NULL) {
+ str = pw_properties_get(props, PW_KEY_REMOTE_NAME);
+ impl->core = pw_context_connect(impl->context,
+ pw_properties_new(
+ PW_KEY_REMOTE_NAME, str,
+ NULL),
+ 0);
+ impl->do_disconnect = true;
+ }
+ if (impl->core == NULL) {
+ res = -errno;
+ pw_log_error("can't connect: %m");
+ goto error;
+ }
+
+ pw_proxy_add_listener((struct pw_proxy*)impl->core,
+ &impl->core_proxy_listener,
+ &core_proxy_events, impl);
+ pw_core_add_listener(impl->core,
+ &impl->core_listener,
+ &core_events, impl);
+
+ if ((res = create_stream(impl)) < 0)
+ goto error;
+
+ pw_impl_module_add_listener(module, &impl->module_listener, &module_events, impl);
+
+ pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_props));
+
+ return 0;
+
+error:
+ impl_destroy(impl);
+ return res;
+}
diff --git a/src/modules/module-raop/rtsp-client.c b/src/modules/module-raop/rtsp-client.c
new file mode 100644
index 0000000..4ac9a31
--- /dev/null
+++ b/src/modules/module-raop/rtsp-client.c
@@ -0,0 +1,631 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <unistd.h>
+#include <sys/socket.h>
+#include <netinet/in.h>
+#include <arpa/inet.h>
+#include <netdb.h>
+
+#include <spa/utils/result.h>
+
+#include "rtsp-client.h"
+
+#define pw_rtsp_client_emit(o,m,v,...) spa_hook_list_call(&o->listener_list, struct pw_rtsp_client_events, m, v, ##__VA_ARGS__)
+#define pw_rtsp_client_emit_destroy(c) pw_rtsp_client_emit(c, destroy, 0)
+#define pw_rtsp_client_emit_connected(c) pw_rtsp_client_emit(c, connected, 0)
+#define pw_rtsp_client_emit_disconnected(c) pw_rtsp_client_emit(c, disconnected, 0)
+#define pw_rtsp_client_emit_error(c,r) pw_rtsp_client_emit(c, error, 0, r)
+#define pw_rtsp_client_emit_message(c,...) pw_rtsp_client_emit(c, message, 0, __VA_ARGS__)
+
+struct message {
+ struct spa_list link;
+ void *data;
+ size_t len;
+ size_t offset;
+ uint32_t cseq;
+ int (*reply) (void *user_data, int status, const struct spa_dict *headers);
+ void *user_data;
+};
+
+enum client_recv_state {
+ CLIENT_RECV_NONE,
+ CLIENT_RECV_STATUS,
+ CLIENT_RECV_HEADERS,
+ CLIENT_RECV_CONTENT,
+};
+
+struct pw_rtsp_client {
+ struct pw_loop *loop;
+ struct pw_properties *props;
+
+ struct spa_hook_list listener_list;
+
+ char *session_id;
+ char *url;
+
+ union {
+ struct sockaddr sa;
+ struct sockaddr_in in;
+ struct sockaddr_in6 in6;
+ } local_addr;
+
+ struct spa_source *source;
+ unsigned int connecting:1;
+ unsigned int need_flush:1;
+
+ enum client_recv_state recv_state;
+ int status;
+ char line_buf[1024];
+ size_t line_pos;
+ struct pw_properties *headers;
+ size_t content_length;
+
+ uint32_t cseq;
+
+ struct spa_list messages;
+ struct spa_list pending;
+
+ void *user_data;
+};
+
+struct pw_rtsp_client *pw_rtsp_client_new(struct pw_loop *main_loop,
+ struct pw_properties *props,
+ size_t user_data_size)
+{
+ struct pw_rtsp_client *client;
+
+ client = calloc(1, sizeof(*client) + user_data_size);
+ if (client == NULL)
+ return NULL;
+
+ client->loop = main_loop;
+ client->props = props;
+ if (user_data_size > 0)
+ client->user_data = SPA_PTROFF(client, sizeof(*client), void);
+
+ spa_list_init(&client->messages);
+ spa_list_init(&client->pending);
+ spa_hook_list_init(&client->listener_list);
+ client->headers = pw_properties_new(NULL, NULL);
+ client->recv_state = CLIENT_RECV_NONE;
+
+ pw_log_info("new client %p", client);
+
+ return client;
+}
+
+void pw_rtsp_client_destroy(struct pw_rtsp_client *client)
+{
+ pw_log_info("destroy client %p", client);
+ pw_rtsp_client_emit_destroy(client);
+
+ pw_rtsp_client_disconnect(client);
+ pw_properties_free(client->headers);
+ pw_properties_free(client->props);
+ spa_hook_list_clean(&client->listener_list);
+ free(client);
+}
+
+void *pw_rtsp_client_get_user_data(struct pw_rtsp_client *client)
+{
+ return client->user_data;
+}
+
+const char *pw_rtsp_client_get_url(struct pw_rtsp_client *client)
+{
+ return client->url;
+}
+
+void pw_rtsp_client_add_listener(struct pw_rtsp_client *client,
+ struct spa_hook *listener,
+ const struct pw_rtsp_client_events *events, void *data)
+{
+ spa_hook_list_append(&client->listener_list, listener, events, data);
+}
+
+const struct pw_properties *pw_rtsp_client_get_properties(struct pw_rtsp_client *client)
+{
+ return client->props;
+}
+
+int pw_rtsp_client_get_local_ip(struct pw_rtsp_client *client,
+ int *version, char *ip, size_t len)
+{
+ if (client->local_addr.sa.sa_family == AF_INET) {
+ *version = 4;
+ if (ip)
+ inet_ntop(client->local_addr.sa.sa_family,
+ &client->local_addr.in.sin_addr, ip, len);
+ } else if (client->local_addr.sa.sa_family == AF_INET6) {
+ *version = 6;
+ if (ip)
+ inet_ntop(client->local_addr.sa.sa_family,
+ &client->local_addr.in6.sin6_addr,
+ ip, len);
+ } else
+ return -EIO;
+ return 0;
+}
+
+static int handle_connect(struct pw_rtsp_client *client, int fd)
+{
+ int res, ip_version;
+ socklen_t len;
+ char local_ip[INET6_ADDRSTRLEN];
+
+ len = sizeof(res);
+ if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &res, &len) < 0) {
+ pw_log_error("getsockopt: %m");
+ return -errno;
+ }
+ if (res != 0)
+ return -res;
+
+ len = sizeof(client->local_addr.sa);
+ if (getsockname(fd, &client->local_addr.sa, &len) < 0)
+ return -errno;
+
+ if ((res = pw_rtsp_client_get_local_ip(client, &ip_version,
+ local_ip, sizeof(local_ip))) < 0)
+ return res;
+
+ if (ip_version == 4)
+ asprintf(&client->url, "rtsp://%s/%s", local_ip, client->session_id);
+ else
+ asprintf(&client->url, "rtsp://[%s]/%s", local_ip, client->session_id);
+
+ pw_log_info("connected local ip %s", local_ip);
+
+ client->connecting = false;
+
+ client->recv_state = CLIENT_RECV_STATUS;
+ pw_properties_clear(client->headers);
+ client->status = 0;
+ client->line_pos = 0;
+ client->content_length = 0;
+
+ pw_rtsp_client_emit_connected(client);
+
+ return 0;
+}
+
+static int read_line(struct pw_rtsp_client *client, char **buf)
+{
+ int res;
+
+ while (true) {
+ uint8_t c;
+
+ res = read(client->source->fd, &c, 1);
+ if (res == 0)
+ return -EPIPE;
+ if (res < 0) {
+ res = -errno;
+ if (res == -EINTR)
+ continue;
+ if (res != -EAGAIN && res != -EWOULDBLOCK)
+ return res;
+ return 0;
+ }
+ if (c == '\n') {
+ client->line_buf[client->line_pos] = '\0';
+ client->line_pos = 0;
+ if (buf)
+ *buf = client->line_buf;
+ return 1;
+ }
+ if (c == '\r')
+ continue;
+ if (client->line_pos < sizeof(client->line_buf) - 1)
+ client->line_buf[client->line_pos++] = c;
+ client->line_buf[client->line_pos] = '\0';
+ }
+ return 0;
+}
+
+static struct message *find_pending(struct pw_rtsp_client *client, uint32_t cseq)
+{
+ struct message *msg;
+ spa_list_for_each(msg, &client->pending, link) {
+ if (msg->cseq == cseq)
+ return msg;
+ }
+ return NULL;
+}
+
+static int process_status(struct pw_rtsp_client *client, char *buf)
+{
+ const char *state = NULL, *s;
+ size_t len;
+
+ pw_log_info("status: %s", buf);
+
+ s = pw_split_walk(buf, " ", &len, &state);
+ if (!spa_strstartswith(s, "RTSP/"))
+ return -EPROTO;
+
+ s = pw_split_walk(buf, " ", &len, &state);
+ if (s == NULL)
+ return -EPROTO;
+
+ client->status = atoi(s);
+ if (client->status == 0)
+ return -EPROTO;
+
+ s = pw_split_walk(buf, " ", &len, &state);
+ if (s == NULL)
+ return -EPROTO;
+
+ pw_properties_clear(client->headers);
+ client->recv_state = CLIENT_RECV_HEADERS;
+
+ return 0;
+}
+
+static void dispatch_handler(struct pw_rtsp_client *client)
+{
+ uint32_t cseq;
+ int res;
+ struct message *msg;
+
+ if (pw_properties_fetch_uint32(client->headers, "CSeq", &cseq) < 0)
+ return;
+
+ pw_log_info("received reply to request with cseq:%" PRIu32, cseq);
+
+ msg = find_pending(client, cseq);
+ if (msg) {
+ res = msg->reply(msg->user_data, client->status, &client->headers->dict);
+ spa_list_remove(&msg->link);
+ free(msg);
+
+ if (res < 0)
+ pw_log_warn("client %p: handle reply cseq:%u error: %s",
+ client, cseq, spa_strerror(res));
+ }
+ else {
+ pw_rtsp_client_emit_message(client, client->status, &client->headers->dict);
+ }
+}
+
+static void process_received_message(struct pw_rtsp_client *client)
+{
+ client->recv_state = CLIENT_RECV_STATUS;
+ dispatch_handler(client);
+}
+
+static int process_header(struct pw_rtsp_client *client, char *buf)
+{
+ if (strlen(buf) > 0) {
+ char *key = buf, *value;
+
+ value = strstr(buf, ":");
+ if (value == NULL)
+ return -EPROTO;
+
+ *value++ = '\0';
+
+ value = pw_strip(value, " ");
+
+ pw_properties_set(client->headers, key, value);
+ }
+ else {
+ const struct spa_dict_item *it;
+ spa_dict_for_each(it, &client->headers->dict)
+ pw_log_info(" %s: %s", it->key, it->value);
+
+ client->content_length = pw_properties_get_uint32(client->headers, "Content-Length", 0);
+ if (client->content_length > 0)
+ client->recv_state = CLIENT_RECV_CONTENT;
+ else
+ process_received_message(client);
+ }
+
+ return 0;
+}
+
+static int process_content(struct pw_rtsp_client *client)
+{
+ char buf[1024];
+
+ while (client->content_length > 0) {
+ const size_t max_recv = SPA_MIN(sizeof(buf), client->content_length);
+
+ ssize_t res = read(client->source->fd, buf, max_recv);
+ if (res == 0)
+ return -EPIPE;
+
+ if (res < 0) {
+ res = -errno;
+ if (res == -EAGAIN || res == -EWOULDBLOCK)
+ return 0;
+
+ return res;
+ }
+
+ spa_assert((size_t) res <= client->content_length);
+ client->content_length -= res;
+ }
+
+ if (client->content_length == 0)
+ process_received_message(client);
+
+ return 0;
+}
+
+static int process_input(struct pw_rtsp_client *client)
+{
+ if (client->recv_state == CLIENT_RECV_STATUS || client->recv_state == CLIENT_RECV_HEADERS) {
+ char *buf = NULL;
+ int res;
+
+ if ((res = read_line(client, &buf)) <= 0)
+ return res;
+
+ pw_log_debug("received line: %s", buf);
+
+ switch (client->recv_state) {
+ case CLIENT_RECV_STATUS:
+ return process_status(client, buf);
+ case CLIENT_RECV_HEADERS:
+ return process_header(client, buf);
+ default:
+ spa_assert_not_reached();
+ }
+ }
+ else if (client->recv_state == CLIENT_RECV_CONTENT) {
+ return process_content(client);
+ }
+ else {
+ spa_assert_not_reached();
+ }
+}
+
+static int flush_output(struct pw_rtsp_client *client)
+{
+ int res;
+
+ client->need_flush = false;
+
+ while (true) {
+ struct message *msg;
+ void *data;
+ size_t size;
+
+ if (spa_list_is_empty(&client->messages))
+ break;
+
+ msg = spa_list_first(&client->messages, struct message, link);
+
+ if (msg->offset < msg->len) {
+ data = SPA_PTROFF(msg->data, msg->offset, void);
+ size = msg->len - msg->offset;
+ } else {
+ pw_log_info("sent: %s", (char *)msg->data);
+ spa_list_remove(&msg->link);
+ if (msg->reply != NULL)
+ spa_list_append(&client->pending, &msg->link);
+ else
+ free(msg);
+ continue;
+ }
+
+ while (true) {
+ res = send(client->source->fd, data, size, MSG_NOSIGNAL | MSG_DONTWAIT);
+ if (res < 0) {
+ res = -errno;
+ if (res == -EINTR)
+ continue;
+ if (res != -EAGAIN && res != -EWOULDBLOCK)
+ pw_log_warn("client %p: send %zu, error %d: %m",
+ client, size, res);
+ return res;
+ }
+ msg->offset += res;
+ break;
+ }
+ }
+ return 0;
+}
+
+static void
+on_source_io(void *data, int fd, uint32_t mask)
+{
+ struct pw_rtsp_client *client = data;
+ int res;
+
+ if (mask & (SPA_IO_ERR | SPA_IO_HUP)) {
+ res = -EPIPE;
+ goto error;
+ }
+ if (mask & SPA_IO_IN) {
+ if ((res = process_input(client)) < 0)
+ goto error;
+ }
+ if (mask & SPA_IO_OUT || client->need_flush) {
+ if (client->connecting) {
+ if ((res = handle_connect(client, fd)) < 0)
+ goto error;
+ }
+ res = flush_output(client);
+ if (res >= 0) {
+ pw_loop_update_io(client->loop, client->source,
+ client->source->mask & ~SPA_IO_OUT);
+ } else if (res != -EAGAIN)
+ goto error;
+ }
+done:
+ return;
+error:
+ pw_log_error("%p: got connection error %d (%s)", client, res, spa_strerror(res));
+ pw_rtsp_client_emit_error(client, res);
+ pw_rtsp_client_disconnect(client);
+ goto done;
+}
+
+int pw_rtsp_client_connect(struct pw_rtsp_client *client,
+ const char *hostname, uint16_t port, const char *session_id)
+{
+ struct addrinfo hints;
+ struct addrinfo *result, *rp;
+ int res, fd;
+ char port_str[12];
+
+ if (client->source != NULL)
+ pw_rtsp_client_disconnect(client);
+
+ pw_log_info("%p: connect %s:%u", client, hostname, port);
+
+ memset(&hints, 0, sizeof(hints));
+ hints.ai_family = AF_UNSPEC;
+ hints.ai_socktype = SOCK_STREAM;
+ hints.ai_flags = 0;
+ hints.ai_protocol = 0;
+
+ spa_scnprintf(port_str, sizeof(port_str), "%u", port);
+
+ if ((res = getaddrinfo(hostname, port_str, &hints, &result)) != 0) {
+ pw_log_error("getaddrinfo: %s", gai_strerror(res));
+ return -EINVAL;
+ }
+ for (rp = result; rp != NULL; rp = rp->ai_next) {
+ fd = socket(rp->ai_family,
+ rp->ai_socktype | SOCK_CLOEXEC | SOCK_NONBLOCK,
+ rp->ai_protocol);
+ if (fd == -1)
+ continue;
+
+ res = connect(fd, rp->ai_addr, rp->ai_addrlen);
+ if (res == 0 || (res < 0 && errno == EINPROGRESS))
+ break;
+
+ close(fd);
+ }
+ freeaddrinfo(result);
+
+ if (rp == NULL) {
+ pw_log_error("Could not connect to %s:%u", hostname, port);
+ return -EINVAL;
+ }
+
+ client->source = pw_loop_add_io(client->loop, fd,
+ SPA_IO_IN | SPA_IO_OUT | SPA_IO_HUP | SPA_IO_ERR,
+ true, on_source_io, client);
+
+ if (client->source == NULL) {
+ res = -errno;
+ pw_log_error("%p: source create failed: %m", client);
+ close(fd);
+ return res;
+ }
+ client->connecting = true;
+ free(client->session_id);
+ client->session_id = strdup(session_id);
+ pw_log_info("%p: connecting", client);
+
+ return 0;
+}
+
+int pw_rtsp_client_disconnect(struct pw_rtsp_client *client)
+{
+ if (client->source == NULL)
+ return 0;
+
+ pw_loop_destroy_source(client->loop, client->source);
+ client->source = NULL;
+ free(client->url);
+ client->url = NULL;
+ free(client->session_id);
+ client->session_id = NULL;
+ pw_rtsp_client_emit_disconnected(client);
+ return 0;
+}
+
+int pw_rtsp_client_url_send(struct pw_rtsp_client *client, const char *url,
+ const char *cmd, const struct spa_dict *headers,
+ const char *content_type, const void *content, size_t content_length,
+ int (*reply) (void *user_data, int status, const struct spa_dict *headers),
+ void *user_data)
+{
+ FILE *f;
+ size_t len;
+ const struct spa_dict_item *it;
+ struct message *msg;
+ uint32_t cseq;
+
+ if ((f = open_memstream((char**)&msg, &len)) == NULL)
+ return -errno;
+
+ fseek(f, sizeof(*msg), SEEK_SET);
+
+ cseq = ++client->cseq;
+
+ fprintf(f, "%s %s RTSP/1.0\r\n", cmd, url);
+ fprintf(f, "CSeq: %" PRIu32 "\r\n", cseq);
+
+ if (headers != NULL) {
+ spa_dict_for_each(it, headers)
+ fprintf(f, "%s: %s\r\n", it->key, it->value);
+ }
+ if (content_type != NULL && content != NULL) {
+ fprintf(f, "Content-Type: %s\r\nContent-Length: %zu\r\n",
+ content_type, content_length);
+ }
+ fprintf(f, "\r\n");
+
+ if (content_type && content)
+ fwrite(content, 1, content_length, f);
+
+ fclose(f);
+
+ msg->data = SPA_PTROFF(msg, sizeof(*msg), void);
+ msg->len = len - sizeof(*msg);
+ msg->offset = 0;
+ msg->reply = reply;
+ msg->user_data = user_data;
+ msg->cseq = cseq;
+
+ spa_list_append(&client->messages, &msg->link);
+
+ client->need_flush = true;
+ if (client->source && !(client->source->mask & SPA_IO_OUT)) {
+ pw_loop_update_io(client->loop, client->source,
+ client->source->mask | SPA_IO_OUT);
+ }
+ return 0;
+}
+
+int pw_rtsp_client_send(struct pw_rtsp_client *client,
+ const char *cmd, const struct spa_dict *headers,
+ const char *content_type, const char *content,
+ int (*reply) (void *user_data, int status, const struct spa_dict *headers),
+ void *user_data)
+{
+ const size_t content_length = content ? strlen(content) : 0;
+
+ return pw_rtsp_client_url_send(client, client->url, cmd, headers,
+ content_type, content, content_length,
+ reply, user_data);
+}
diff --git a/src/modules/module-raop/rtsp-client.h b/src/modules/module-raop/rtsp-client.h
new file mode 100644
index 0000000..014468b
--- /dev/null
+++ b/src/modules/module-raop/rtsp-client.h
@@ -0,0 +1,92 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_RTSP_CLIENT_H
+#define PIPEWIRE_RTSP_CLIENT_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <stdarg.h>
+
+#include <pipewire/pipewire.h>
+
+struct pw_rtsp_client;
+
+struct pw_rtsp_client_events {
+#define PW_VERSION_RTSP_CLIENT_EVENTS 0
+ uint32_t version;
+
+ void (*destroy) (void *data);
+
+ void (*connected) (void *data);
+ void (*error) (void *data, int res);
+ void (*disconnected) (void *data);
+
+ void (*message) (void *data, int status,
+ const struct spa_dict *headers);
+
+};
+
+struct pw_rtsp_client * pw_rtsp_client_new(struct pw_loop *main_loop,
+ struct pw_properties *props,
+ size_t user_data_size);
+
+void pw_rtsp_client_destroy(struct pw_rtsp_client *client);
+
+void *pw_rtsp_client_get_user_data(struct pw_rtsp_client *client);
+const char *pw_rtsp_client_get_url(struct pw_rtsp_client *client);
+
+void pw_rtsp_client_add_listener(struct pw_rtsp_client *client,
+ struct spa_hook *listener,
+ const struct pw_rtsp_client_events *events, void *data);
+
+const struct pw_properties *pw_rtsp_client_get_properties(struct pw_rtsp_client *client);
+
+int pw_rtsp_client_connect(struct pw_rtsp_client *client,
+ const char *hostname, uint16_t port, const char *session_id);
+int pw_rtsp_client_disconnect(struct pw_rtsp_client *client);
+
+int pw_rtsp_client_get_local_ip(struct pw_rtsp_client *client,
+ int *version, char *ip, size_t len);
+
+int pw_rtsp_client_url_send(struct pw_rtsp_client *client, const char *url,
+ const char *cmd, const struct spa_dict *headers,
+ const char *content_type, const void *content, size_t content_length,
+ int (*reply) (void *user_data, int status, const struct spa_dict *headers),
+ void *user_data);
+
+int pw_rtsp_client_send(struct pw_rtsp_client *client,
+ const char *cmd, const struct spa_dict *headers,
+ const char *content_type, const char *content,
+ int (*reply) (void *user_data, int status, const struct spa_dict *headers),
+ void *user_data);
+
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PIPEWIRE_RTSP_CLIENT_H */
diff --git a/src/modules/module-roc-sink.c b/src/modules/module-roc-sink.c
new file mode 100644
index 0000000..86bd983
--- /dev/null
+++ b/src/modules/module-roc-sink.c
@@ -0,0 +1,514 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans <wim.taymans@gmail.com>
+ * Copyright © 2021 Sanchayan Maity <sanchayan@asymptotic.io>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <limits.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#include "config.h"
+
+#include <spa/utils/hook.h>
+#include <spa/utils/result.h>
+#include <spa/param/audio/format-utils.h>
+
+#include <roc/config.h>
+#include <roc/log.h>
+#include <roc/context.h>
+#include <roc/log.h>
+#include <roc/sender.h>
+
+#include <pipewire/pipewire.h>
+#include <pipewire/impl.h>
+
+#include "module-roc/common.h"
+
+/** \page page_module_roc_sink PipeWire Module: ROC sink
+ *
+ * The `roc-sink` module creates a PipeWire sink that sends samples to
+ * a preconfigured receiver address. One can then connect an audio stream
+ * of any running application to that sink or make it the default sink.
+ *
+ * ## Module Options
+ *
+ * Options specific to the behavior of this module
+ *
+ * - `sink.props = {}`: properties to be passed to the sink stream
+ * - `sink.name = <str>`: node.name of the sink
+ * - `remote.ip = <str>`: remote receiver ip
+ * - `remote.source.port = <str>`: remote receiver TCP/UDP port for source packets
+ * - `remote.repair.port = <str>`: remote receiver TCP/UDP port for receiver packets
+ * - `fec.code = <str>`: Possible values: `disable`, `rs8m`, `ldpc`
+ *
+ * ## General options
+ *
+ * Options with well-known behavior:
+ *
+ * - \ref PW_KEY_NODE_NAME
+ * - \ref PW_KEY_NODE_DESCRIPTION
+ * - \ref PW_KEY_MEDIA_NAME
+ *
+ * ## Example configuration
+ *\code{.unparsed}
+ * context.modules = [
+ * { name = libpipewire-module-roc-sink
+ * args = {
+ * local.ip = 0.0.0.0
+ * fec.code = disable
+ * remote.ip = 192.168.0.244
+ * remote.source.port = 10001
+ * remote.repair.port = 10002
+ * sink.name = "ROC Sink"
+ * sink.props = {
+ * node.name = "roc-sink"
+ * }
+ * }
+ * }
+ *]
+ *\endcode
+ *
+ */
+
+#define NAME "roc-sink"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+struct module_roc_sink_data {
+ struct pw_impl_module *module;
+ struct spa_hook module_listener;
+ struct pw_properties *props;
+ struct pw_context *module_context;
+
+ struct pw_core *core;
+ struct spa_hook core_listener;
+ struct spa_hook core_proxy_listener;
+
+ struct pw_stream *capture;
+ struct spa_hook capture_listener;
+ struct pw_properties *capture_props;
+
+ unsigned int do_disconnect:1;
+
+ roc_endpoint *remote_source_addr;
+ roc_endpoint *remote_repair_addr;
+ roc_context *context;
+ roc_sender *sender;
+
+ roc_fec_encoding fec_code;
+ uint32_t rate;
+ char *remote_ip;
+ int remote_source_port;
+ int remote_repair_port;
+};
+
+static void stream_destroy(void *d)
+{
+ struct module_roc_sink_data *data = d;
+ spa_hook_remove(&data->capture_listener);
+ data->capture = NULL;
+}
+
+static void capture_process(void *data)
+{
+ struct module_roc_sink_data *impl = data;
+ struct pw_buffer *in;
+ struct spa_data *d;
+ roc_frame frame;
+ uint32_t i, size, offset;
+
+ if ((in = pw_stream_dequeue_buffer(impl->capture)) == NULL) {
+ pw_log_debug("Out of capture buffers: %m");
+ return;
+ }
+
+ for (i = 0; i < in->buffer->n_datas; i++) {
+ d = &in->buffer->datas[i];
+
+ offset = SPA_MIN(d->chunk->offset, d->maxsize);
+ size = SPA_MIN(d->maxsize - offset, d->chunk->size);
+
+ while (size > 0) {
+ spa_zero(frame);
+
+ frame.samples = SPA_MEMBER(d->data, offset, void);
+ frame.samples_size = size;
+
+ if (roc_sender_write(impl->sender, &frame) != 0) {
+ pw_log_warn("Failed to write to roc sink");
+ break;
+ }
+
+ offset += frame.samples_size;
+ size -= frame.samples_size;
+ }
+ }
+ pw_stream_queue_buffer(impl->capture, in);
+}
+
+static void on_core_error(void *d, uint32_t id, int seq, int res, const char *message)
+{
+ struct module_roc_sink_data *data = d;
+
+ pw_log_error("error id:%u seq:%d res:%d (%s): %s",
+ id, seq, res, spa_strerror(res), message);
+
+ if (id == PW_ID_CORE && res == -EPIPE)
+ pw_impl_module_schedule_destroy(data->module);
+}
+
+static const struct pw_core_events core_events = {
+ PW_VERSION_CORE_EVENTS,
+ .error = on_core_error,
+};
+
+static void on_stream_state_changed(void *d, enum pw_stream_state old,
+ enum pw_stream_state state, const char *error)
+{
+ struct module_roc_sink_data *data = d;
+
+ switch (state) {
+ case PW_STREAM_STATE_UNCONNECTED:
+ pw_log_info("stream disconnected, unloading");
+ pw_impl_module_schedule_destroy(data->module);
+ break;
+ case PW_STREAM_STATE_ERROR:
+ pw_log_error("stream error: %s", error);
+ break;
+ default:
+ break;
+ }
+}
+
+static const struct pw_stream_events in_stream_events = {
+ PW_VERSION_STREAM_EVENTS,
+ .destroy = stream_destroy,
+ .state_changed = on_stream_state_changed,
+ .process = capture_process
+};
+
+static void core_destroy(void *d)
+{
+ struct module_roc_sink_data *data = d;
+ spa_hook_remove(&data->core_listener);
+ data->core = NULL;
+ pw_impl_module_schedule_destroy(data->module);
+}
+
+static const struct pw_proxy_events core_proxy_events = {
+ .destroy = core_destroy,
+};
+
+static void impl_destroy(struct module_roc_sink_data *data)
+{
+ if (data->capture)
+ pw_stream_destroy(data->capture);
+ if (data->core && data->do_disconnect)
+ pw_core_disconnect(data->core);
+
+ pw_properties_free(data->capture_props);
+ pw_properties_free(data->props);
+
+ if (data->sender)
+ roc_sender_close(data->sender);
+ if (data->context)
+ roc_context_close(data->context);
+
+ if (data->remote_source_addr)
+ (void) roc_endpoint_deallocate(data->remote_source_addr);
+ if (data->remote_repair_addr)
+ (void) roc_endpoint_deallocate(data->remote_repair_addr);
+
+ free(data->remote_ip);
+ free(data);
+}
+
+static void module_destroy(void *d)
+{
+ struct module_roc_sink_data *data = d;
+ spa_hook_remove(&data->module_listener);
+ impl_destroy(data);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy,
+};
+
+static int roc_sink_setup(struct module_roc_sink_data *data)
+{
+ roc_context_config context_config;
+ roc_sender_config sender_config;
+ struct spa_audio_info_raw info = { 0 };
+ const struct spa_pod *params[1];
+ struct spa_pod_builder b;
+ uint32_t n_params;
+ uint8_t buffer[1024];
+ int res;
+ roc_protocol audio_proto, repair_proto;
+
+ memset(&context_config, 0, sizeof(context_config));
+
+ res = roc_context_open(&context_config, &data->context);
+ if (res) {
+ pw_log_error("failed to create roc context: %d", res);
+ return -EINVAL;
+ }
+
+ memset(&sender_config, 0, sizeof(sender_config));
+
+ sender_config.frame_sample_rate = data->rate;
+ sender_config.frame_channels = ROC_CHANNEL_SET_STEREO;
+ sender_config.frame_encoding = ROC_FRAME_ENCODING_PCM_FLOAT;
+ sender_config.fec_encoding = data->fec_code;
+
+ info.rate = data->rate;
+
+ /* Fixed to be the same as ROC sender config above */
+ info.channels = 2;
+ info.format = SPA_AUDIO_FORMAT_F32;
+ info.position[0] = SPA_AUDIO_CHANNEL_FL;
+ info.position[1] = SPA_AUDIO_CHANNEL_FR;
+
+ pw_properties_setf(data->capture_props, PW_KEY_NODE_RATE, "1/%d", info.rate);
+
+ res = roc_sender_open(data->context, &sender_config, &data->sender);
+ if (res) {
+ pw_log_error("failed to create roc sender: %d", res);
+ return -EINVAL;
+ }
+
+ switch (data->fec_code) {
+ case ROC_FEC_ENCODING_DEFAULT:
+ case ROC_FEC_ENCODING_RS8M:
+ audio_proto = ROC_PROTO_RTP_RS8M_SOURCE;
+ repair_proto = ROC_PROTO_RS8M_REPAIR;
+ break;
+ case ROC_FEC_ENCODING_LDPC_STAIRCASE:
+ audio_proto = ROC_PROTO_RTP_LDPC_SOURCE;
+ repair_proto = ROC_PROTO_LDPC_REPAIR;
+ break;
+ default:
+ audio_proto = ROC_PROTO_RTP;
+ repair_proto = 0;
+ break;
+ }
+
+ res = pw_roc_create_endpoint(&data->remote_source_addr, audio_proto, data->remote_ip, data->remote_source_port);
+ if (res < 0) {
+ pw_log_warn("failed to create source endpoint: %s", spa_strerror(res));
+ return res;
+ }
+
+ if (roc_sender_connect(data->sender, ROC_SLOT_DEFAULT, ROC_INTERFACE_AUDIO_SOURCE,
+ data->remote_source_addr) != 0) {
+ pw_log_error("can't connect roc sender to remote source address");
+ return -EINVAL;
+ }
+
+ if (repair_proto != 0) {
+ res = pw_roc_create_endpoint(&data->remote_repair_addr, repair_proto, data->remote_ip, data->remote_repair_port);
+ if (res < 0) {
+ pw_log_error("failed to create repair endpoint: %s", spa_strerror(res));
+ return res;
+ }
+
+ if (roc_sender_connect(data->sender, ROC_SLOT_DEFAULT, ROC_INTERFACE_AUDIO_REPAIR,
+ data->remote_repair_addr) != 0) {
+ pw_log_error("can't connect roc sender to remote repair address");
+ return -EINVAL;
+ }
+ }
+
+ data->capture = pw_stream_new(data->core,
+ "roc-sink capture", data->capture_props);
+ data->capture_props = NULL;
+ if (data->capture == NULL)
+ return -errno;
+
+ pw_stream_add_listener(data->capture,
+ &data->capture_listener,
+ &in_stream_events, data);
+
+ n_params = 0;
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ params[n_params++] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat,
+ &info);
+
+ if ((res = pw_stream_connect(data->capture,
+ PW_DIRECTION_INPUT,
+ PW_ID_ANY,
+ PW_STREAM_FLAG_MAP_BUFFERS |
+ PW_STREAM_FLAG_RT_PROCESS,
+ params, n_params)) < 0)
+ return res;
+
+ return 0;
+}
+
+static const struct spa_dict_item module_roc_sink_info[] = {
+ { PW_KEY_MODULE_AUTHOR, "Sanchayan Maity <sanchayan@asymptotic.io>" },
+ { PW_KEY_MODULE_DESCRIPTION, "roc sink" },
+ { PW_KEY_MODULE_USAGE, "sink.name=<name for the sink> "
+ "local.ip=<local sender ip> "
+ "fec.code=<empty>|disable|rs8m|ldpc "
+ "remote.ip=<remote receiver ip> "
+ "remote.source.port=<remote receiver port for source packets> "
+ "remote.repair.port=<remote receiver port for repair packets> "
+ "sink.props= { key=val ... } " },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+SPA_EXPORT
+int pipewire__module_init(struct pw_impl_module *module, const char *args)
+{
+ struct pw_context *context = pw_impl_module_get_context(module);
+ struct module_roc_sink_data *data;
+ struct pw_properties *props = NULL, *capture_props = NULL;
+ const char *str;
+ int res = 0;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ data = calloc(1, sizeof(struct module_roc_sink_data));
+ if (data == NULL)
+ return -errno;
+
+ if (args == NULL)
+ args = "";
+
+ props = pw_properties_new_string(args);
+ if (props == NULL) {
+ res = -errno;
+ pw_log_error( "can't create properties: %m");
+ goto out;
+ }
+ data->props = props;
+
+ capture_props = pw_properties_new(NULL, NULL);
+ if (capture_props == NULL) {
+ res = -errno;
+ pw_log_error( "can't create properties: %m");
+ goto out;
+ }
+ data->capture_props = capture_props;
+
+ data->module = module;
+ data->module_context = context;
+
+ if ((str = pw_properties_get(props, "sink.name")) != NULL) {
+ pw_properties_set(capture_props, PW_KEY_NODE_NAME, str);
+ pw_properties_set(props, "sink.name", NULL);
+ }
+
+ if ((str = pw_properties_get(props, "sink.props")) != NULL)
+ pw_properties_update_string(capture_props, str, strlen(str));
+
+ if (pw_properties_get(capture_props, PW_KEY_NODE_NAME) == NULL)
+ pw_properties_set(capture_props, PW_KEY_NODE_NAME, "roc-sink");
+ if (pw_properties_get(capture_props, PW_KEY_NODE_DESCRIPTION) == NULL)
+ pw_properties_set(capture_props, PW_KEY_NODE_DESCRIPTION, "ROC Sink");
+ if (pw_properties_get(capture_props, PW_KEY_NODE_VIRTUAL) == NULL)
+ pw_properties_set(capture_props, PW_KEY_NODE_VIRTUAL, "true");
+ if (pw_properties_get(capture_props, PW_KEY_NODE_NETWORK) == NULL)
+ pw_properties_set(capture_props, PW_KEY_NODE_NETWORK, "true");
+ if ((str = pw_properties_get(capture_props, PW_KEY_MEDIA_CLASS)) == NULL)
+ pw_properties_set(capture_props, PW_KEY_MEDIA_CLASS, "Audio/Sink");
+
+ data->rate = pw_properties_get_uint32(capture_props, PW_KEY_AUDIO_RATE, data->rate);
+ if (data->rate == 0)
+ data->rate = PW_ROC_DEFAULT_RATE;
+
+ if ((str = pw_properties_get(props, "remote.ip")) != NULL) {
+ data->remote_ip = strdup(str);
+ pw_properties_set(props, "remote.ip", NULL);
+ } else {
+ pw_log_error("Remote IP not specified");
+ res = -EINVAL;
+ goto out;
+ }
+
+ if ((str = pw_properties_get(props, "remote.source.port")) != NULL) {
+ data->remote_source_port = pw_properties_parse_int(str);
+ pw_properties_set(props, "remote.source.port", NULL);
+ } else {
+ data->remote_source_port = PW_ROC_DEFAULT_SOURCE_PORT;
+ }
+
+ if ((str = pw_properties_get(props, "remote.repair.port")) != NULL) {
+ data->remote_repair_port = pw_properties_parse_int(str);
+ pw_properties_set(props, "remote.repair.port", NULL);
+ } else {
+ data->remote_repair_port = PW_ROC_DEFAULT_REPAIR_PORT;
+ }
+ if ((str = pw_properties_get(props, "fec.code")) != NULL) {
+ if (pw_roc_parse_fec_encoding(&data->fec_code, str)) {
+ pw_log_error("Invalid fec code %s, using default", str);
+ data->fec_code = ROC_FEC_ENCODING_DEFAULT;
+ }
+ pw_log_info("using fec.code %s %d", str, data->fec_code);
+ pw_properties_set(props, "fec.code", NULL);
+ } else {
+ data->fec_code = ROC_FEC_ENCODING_DEFAULT;
+ }
+
+
+ data->core = pw_context_get_object(data->module_context, PW_TYPE_INTERFACE_Core);
+ if (data->core == NULL) {
+ str = pw_properties_get(props, PW_KEY_REMOTE_NAME);
+ data->core = pw_context_connect(data->module_context,
+ pw_properties_new(
+ PW_KEY_REMOTE_NAME, str,
+ NULL),
+ 0);
+ data->do_disconnect = true;
+ }
+ if (data->core == NULL) {
+ res = -errno;
+ pw_log_error("can't connect: %m");
+ goto out;
+ }
+
+ pw_proxy_add_listener((struct pw_proxy*)data->core,
+ &data->core_proxy_listener,
+ &core_proxy_events, data);
+ pw_core_add_listener(data->core,
+ &data->core_listener,
+ &core_events, data);
+
+ if ((res = roc_sink_setup(data)) < 0)
+ goto out;
+
+ pw_impl_module_add_listener(module, &data->module_listener, &module_events, data);
+
+ pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_roc_sink_info));
+
+ pw_log_info("Successfully loaded module-roc-sink");
+
+ return 0;
+
+out:
+ impl_destroy(data);
+ return res;
+}
diff --git a/src/modules/module-roc-source.c b/src/modules/module-roc-source.c
new file mode 100644
index 0000000..91fe60b
--- /dev/null
+++ b/src/modules/module-roc-source.c
@@ -0,0 +1,546 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans <wim.taymans@gmail.com>
+ * Copyright © 2021 Sanchayan Maity <sanchayan@asymptotic.io>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <limits.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#include "config.h"
+
+#include <spa/utils/hook.h>
+#include <spa/utils/result.h>
+#include <spa/param/audio/format-utils.h>
+
+#include <roc/config.h>
+#include <roc/log.h>
+#include <roc/context.h>
+#include <roc/log.h>
+#include <roc/receiver.h>
+
+#include <pipewire/pipewire.h>
+#include <pipewire/impl.h>
+
+#include "module-roc/common.h"
+
+/** \page page_module_roc_source PipeWire Module: ROC source
+ *
+ * The `roc-source` module creates a PipeWire source that receives samples
+ * from ROC sender and passes them to the sink it is connected to. One can
+ * then connect it to any audio device.
+ *
+ * ## Module Options
+ *
+ * Options specific to the behavior of this module
+ *
+ * - `source.props = {}`: properties to be passed to the source stream
+ * - `source.name = <str>`: node.name of the source
+ * - `local.ip = <str>`: local sender ip
+ * - `local.source.port = <str>`: local receiver TCP/UDP port for source packets
+ * - `local.repair.port = <str>`: local receiver TCP/UDP port for receiver packets
+ * - `sess.latency.msec = <str>`: target network latency in milliseconds
+ * - `resampler.profile = <str>`: Possible values: `disable`, `high`,
+ * `medium`, `low`.
+ * - `fec.code = <str>`: Possible values: `disable`, `rs8m`, `ldpc`
+ *
+ * ## General options
+ *
+ * Options with well-known behavior:
+ *
+ * - \ref PW_KEY_NODE_NAME
+ * - \ref PW_KEY_NODE_DESCRIPTION
+ * - \ref PW_KEY_MEDIA_NAME
+ *
+ * ## Example configuration
+ *\code{.unparsed}
+ * context.modules = [
+ * { name = libpipewire-module-roc-source
+ * args = {
+ * local.ip = 0.0.0.0
+ * resampler.profile = medium
+ * fec.code = disable
+ * sess.latency.msec = 5000
+ * local.source.port = 10001
+ * local.repair.port = 10002
+ * source.name = "ROC Source"
+ * source.props = {
+ * node.name = "roc-source"
+ * }
+ * }
+ * }
+ *]
+ *\endcode
+ *
+ */
+
+#define NAME "roc-source"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+struct module_roc_source_data {
+ struct pw_impl_module *module;
+ struct spa_hook module_listener;
+ struct pw_properties *props;
+ struct pw_context *module_context;
+
+ struct pw_core *core;
+ struct spa_hook core_listener;
+ struct spa_hook core_proxy_listener;
+
+ struct pw_stream *playback;
+ struct spa_hook playback_listener;
+ struct pw_properties *playback_props;
+
+ unsigned int do_disconnect:1;
+ uint32_t stride;
+
+ roc_endpoint *local_source_addr;
+ roc_endpoint *local_repair_addr;
+ roc_context *context;
+ roc_receiver *receiver;
+
+ roc_resampler_profile resampler_profile;
+ roc_fec_encoding fec_code;
+ uint32_t rate;
+ char *local_ip;
+ int local_source_port;
+ int local_repair_port;
+ int sess_latency_msec;
+};
+
+static void stream_destroy(void *d)
+{
+ struct module_roc_source_data *data = d;
+ spa_hook_remove(&data->playback_listener);
+ data->playback = NULL;
+}
+
+static void playback_process(void *data)
+{
+ struct module_roc_source_data *impl = data;
+ struct pw_buffer *b;
+ struct spa_buffer *buf;
+ roc_frame frame;
+ uint8_t *dst;
+
+ if ((b = pw_stream_dequeue_buffer(impl->playback)) == NULL) {
+ pw_log_debug("Out of playback buffers: %m");
+ return;
+ }
+
+ buf = b->buffer;
+ if ((dst = buf->datas[0].data) == NULL)
+ return;
+
+ buf->datas[0].chunk->offset = 0;
+ buf->datas[0].chunk->stride = impl->stride;
+ buf->datas[0].chunk->size = 0;
+
+ spa_zero(frame);
+ frame.samples = dst;
+ frame.samples_size = SPA_MIN(b->requested * impl->stride, buf->datas[0].maxsize);
+
+ if (roc_receiver_read(impl->receiver, &frame) != 0) {
+ /* Handle EOF and error */
+ pw_log_error("Failed to read from roc source");
+ pw_impl_module_schedule_destroy(impl->module);
+ frame.samples_size = 0;
+ }
+
+ buf->datas[0].chunk->size = frame.samples_size;
+ b->size = frame.samples_size / impl->stride;
+
+ pw_stream_queue_buffer(impl->playback, b);
+}
+
+static void on_core_error(void *d, uint32_t id, int seq, int res, const char *message)
+{
+ struct module_roc_source_data *data = d;
+
+ pw_log_error("error id:%u seq:%d res:%d (%s): %s",
+ id, seq, res, spa_strerror(res), message);
+
+ if (id == PW_ID_CORE && res == -EPIPE)
+ pw_impl_module_schedule_destroy(data->module);
+}
+
+static const struct pw_core_events core_events = {
+ PW_VERSION_CORE_EVENTS,
+ .error = on_core_error,
+};
+
+static void on_stream_state_changed(void *d, enum pw_stream_state old,
+ enum pw_stream_state state, const char *error)
+{
+ struct module_roc_source_data *data = d;
+
+ switch (state) {
+ case PW_STREAM_STATE_UNCONNECTED:
+ pw_log_info("stream disconnected, unloading");
+ pw_impl_module_schedule_destroy(data->module);
+ break;
+ case PW_STREAM_STATE_ERROR:
+ pw_log_error("stream error: %s", error);
+ break;
+ default:
+ break;
+ }
+}
+
+static const struct pw_stream_events out_stream_events = {
+ PW_VERSION_STREAM_EVENTS,
+ .destroy = stream_destroy,
+ .state_changed = on_stream_state_changed,
+ .process = playback_process
+};
+
+static void core_destroy(void *d)
+{
+ struct module_roc_source_data *data = d;
+ spa_hook_remove(&data->core_listener);
+ data->core = NULL;
+ pw_impl_module_schedule_destroy(data->module);
+}
+
+static const struct pw_proxy_events core_proxy_events = {
+ .destroy = core_destroy,
+};
+
+static void impl_destroy(struct module_roc_source_data *data)
+{
+ if (data->playback)
+ pw_stream_destroy(data->playback);
+ if (data->core && data->do_disconnect)
+ pw_core_disconnect(data->core);
+
+ pw_properties_free(data->playback_props);
+ pw_properties_free(data->props);
+
+ if (data->receiver)
+ roc_receiver_close(data->receiver);
+ if (data->context)
+ roc_context_close(data->context);
+
+ if (data->local_source_addr)
+ (void) roc_endpoint_deallocate(data->local_source_addr);
+ if (data->local_repair_addr)
+ (void) roc_endpoint_deallocate(data->local_repair_addr);
+
+ free(data->local_ip);
+ free(data);
+}
+
+static void module_destroy(void *d)
+{
+ struct module_roc_source_data *data = d;
+ spa_hook_remove(&data->module_listener);
+ impl_destroy(data);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy,
+};
+
+static int roc_source_setup(struct module_roc_source_data *data)
+{
+ roc_context_config context_config;
+ roc_receiver_config receiver_config;
+ struct spa_audio_info_raw info = { 0 };
+ const struct spa_pod *params[1];
+ struct spa_pod_builder b;
+ uint32_t n_params;
+ uint8_t buffer[1024];
+ int res;
+ roc_protocol audio_proto, repair_proto;
+
+ spa_zero(context_config);
+ res = roc_context_open(&context_config, &data->context);
+ if (res) {
+ pw_log_error("failed to create roc context: %d", res);
+ return -EINVAL;
+ }
+
+ spa_zero(receiver_config);
+ receiver_config.frame_sample_rate = data->rate;
+ receiver_config.frame_channels = ROC_CHANNEL_SET_STEREO;
+ receiver_config.frame_encoding = ROC_FRAME_ENCODING_PCM_FLOAT;
+ receiver_config.resampler_profile = data->resampler_profile;
+
+ info.rate = data->rate;
+
+ /* Fixed to be the same as ROC receiver config above */
+ info.channels = 2;
+ info.format = SPA_AUDIO_FORMAT_F32;
+ info.position[0] = SPA_AUDIO_CHANNEL_FL;
+ info.position[1] = SPA_AUDIO_CHANNEL_FR;
+ data->stride = info.channels * sizeof(float);
+
+ pw_properties_setf(data->playback_props, PW_KEY_NODE_RATE, "1/%d", info.rate);
+
+ /*
+ * Note that target latency is in nano seconds.
+ *
+ * The session will not start playing until it accumulates the
+ * requested latency. Then if resampler is enabled, the session will
+ * adjust it's clock to keep actual latency as close as possible to
+ * the target latency. If zero, default value will be used.
+ *
+ * See API reference:
+ * https://roc-streaming.org/toolkit/docs/api/reference.html
+ */
+ receiver_config.target_latency = data->sess_latency_msec * 1000000;
+
+ res = roc_receiver_open(data->context, &receiver_config, &data->receiver);
+ if (res) {
+ pw_log_error("failed to create roc receiver: %d", res);
+ return -EINVAL;
+ }
+
+ switch (data->fec_code) {
+ case ROC_FEC_ENCODING_DEFAULT:
+ case ROC_FEC_ENCODING_RS8M:
+ audio_proto = ROC_PROTO_RTP_RS8M_SOURCE;
+ repair_proto = ROC_PROTO_RS8M_REPAIR;
+ break;
+ case ROC_FEC_ENCODING_LDPC_STAIRCASE:
+ audio_proto = ROC_PROTO_RTP_LDPC_SOURCE;
+ repair_proto = ROC_PROTO_LDPC_REPAIR;
+ break;
+ default:
+ audio_proto = ROC_PROTO_RTP;
+ repair_proto = 0;
+ break;
+ }
+
+ res = pw_roc_create_endpoint(&data->local_source_addr, audio_proto, data->local_ip, data->local_source_port);
+ if (res < 0) {
+ pw_log_error("failed to create source endpoint: %s", spa_strerror(res));
+ return res;
+ }
+
+ if (roc_receiver_bind(data->receiver, ROC_SLOT_DEFAULT, ROC_INTERFACE_AUDIO_SOURCE,
+ data->local_source_addr) != 0) {
+ pw_log_error("can't connect roc receiver to local source address");
+ return -EINVAL;
+ }
+
+ if (repair_proto != 0) {
+ res = pw_roc_create_endpoint(&data->local_repair_addr, repair_proto, data->local_ip, data->local_repair_port);
+ if (res < 0) {
+ pw_log_error("failed to create repair endpoint: %s", spa_strerror(res));
+ return res;
+ }
+
+ if (roc_receiver_bind(data->receiver, ROC_SLOT_DEFAULT, ROC_INTERFACE_AUDIO_REPAIR,
+ data->local_repair_addr) != 0) {
+ pw_log_error("can't connect roc receiver to local repair address");
+ return -EINVAL;
+ }
+ }
+
+ data->playback = pw_stream_new(data->core,
+ "roc-source playback", data->playback_props);
+ data->playback_props = NULL;
+ if (data->playback == NULL)
+ return -errno;
+
+ pw_stream_add_listener(data->playback,
+ &data->playback_listener,
+ &out_stream_events, data);
+
+ n_params = 0;
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ params[n_params++] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat,
+ &info);
+
+ if ((res = pw_stream_connect(data->playback,
+ PW_DIRECTION_OUTPUT,
+ PW_ID_ANY,
+ PW_STREAM_FLAG_MAP_BUFFERS |
+ PW_STREAM_FLAG_AUTOCONNECT |
+ PW_STREAM_FLAG_RT_PROCESS,
+ params, n_params)) < 0)
+ return res;
+
+ return 0;
+}
+
+static const struct spa_dict_item module_roc_source_info[] = {
+ { PW_KEY_MODULE_AUTHOR, "Sanchayan Maity <sanchayan@asymptotic.io>" },
+ { PW_KEY_MODULE_DESCRIPTION, "roc source" },
+ { PW_KEY_MODULE_USAGE, "source.name=<name for the source> "
+ "resampler.profile=<empty>|disable|high|medium|low "
+ "fec.code=<empty>|disable|rs8m|ldpc "
+ "sess.latency.msec=<target network latency in milliseconds> "
+ "local.ip=<local receiver ip> "
+ "local.source.port=<local receiver port for source packets> "
+ "local.repair.port=<local receiver port for repair packets> "
+ "source.props= { key=value ... }" },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+SPA_EXPORT
+int pipewire__module_init(struct pw_impl_module *module, const char *args)
+{
+ struct pw_context *context = pw_impl_module_get_context(module);
+ struct module_roc_source_data *data;
+ struct pw_properties *props = NULL, *playback_props = NULL;
+ const char *str;
+ int res = 0;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ data = calloc(1, sizeof(struct module_roc_source_data));
+ if (data == NULL)
+ return -errno;
+
+ if (args == NULL)
+ args = "";
+
+ props = pw_properties_new_string(args);
+ if (props == NULL) {
+ res = -errno;
+ pw_log_error( "can't create properties: %m");
+ goto out;
+ }
+ data->props = props;
+
+ playback_props = pw_properties_new(NULL, NULL);
+ if (playback_props == NULL) {
+ res = -errno;
+ pw_log_error( "can't create properties: %m");
+ goto out;
+ }
+ data->playback_props = playback_props;
+
+ data->module = module;
+ data->module_context = context;
+
+ if ((str = pw_properties_get(props, "source.name")) != NULL) {
+ pw_properties_set(playback_props, PW_KEY_NODE_NAME, str);
+ pw_properties_set(props, "source.name", NULL);
+ }
+
+ if ((str = pw_properties_get(props, "source.props")) != NULL)
+ pw_properties_update_string(playback_props, str, strlen(str));
+
+ if (pw_properties_get(playback_props, PW_KEY_NODE_NAME) == NULL)
+ pw_properties_set(playback_props, PW_KEY_NODE_NAME, "roc-source");
+ if (pw_properties_get(playback_props, PW_KEY_NODE_DESCRIPTION) == NULL)
+ pw_properties_set(playback_props, PW_KEY_NODE_DESCRIPTION, "ROC Source");
+ if (pw_properties_get(playback_props, PW_KEY_NODE_VIRTUAL) == NULL)
+ pw_properties_set(playback_props, PW_KEY_NODE_VIRTUAL, "true");
+ if (pw_properties_get(playback_props, PW_KEY_NODE_NETWORK) == NULL)
+ pw_properties_set(playback_props, PW_KEY_NODE_NETWORK, "true");
+
+ data->rate = pw_properties_get_uint32(playback_props, PW_KEY_AUDIO_RATE, data->rate);
+ if (data->rate == 0)
+ data->rate = PW_ROC_DEFAULT_RATE;
+
+ if ((str = pw_properties_get(props, "local.ip")) != NULL) {
+ data->local_ip = strdup(str);
+ pw_properties_set(props, "local.ip", NULL);
+ } else {
+ data->local_ip = strdup(PW_ROC_DEFAULT_IP);
+ }
+
+ if ((str = pw_properties_get(props, "local.source.port")) != NULL) {
+ data->local_source_port = pw_properties_parse_int(str);
+ pw_properties_set(props, "local.source.port", NULL);
+ } else {
+ data->local_source_port = PW_ROC_DEFAULT_SOURCE_PORT;
+ }
+
+ if ((str = pw_properties_get(props, "local.repair.port")) != NULL) {
+ data->local_repair_port = pw_properties_parse_int(str);
+ pw_properties_set(props, "local.repair.port", NULL);
+ } else {
+ data->local_repair_port = PW_ROC_DEFAULT_REPAIR_PORT;
+ }
+
+ if ((str = pw_properties_get(props, "sess.latency.msec")) != NULL) {
+ data->sess_latency_msec = pw_properties_parse_int(str);
+ pw_properties_set(props, "sess.latency.msec", NULL);
+ } else {
+ data->sess_latency_msec = PW_ROC_DEFAULT_SESS_LATENCY;
+ }
+
+ if ((str = pw_properties_get(props, "resampler.profile")) != NULL) {
+ if (pw_roc_parse_resampler_profile(&data->resampler_profile, str)) {
+ pw_log_warn("Invalid resampler profile %s, using default", str);
+ data->resampler_profile = ROC_RESAMPLER_PROFILE_DEFAULT;
+ }
+ pw_properties_set(props, "resampler.profile", NULL);
+ } else {
+ data->resampler_profile = ROC_RESAMPLER_PROFILE_DEFAULT;
+ }
+ if ((str = pw_properties_get(props, "fec.code")) != NULL) {
+ if (pw_roc_parse_fec_encoding(&data->fec_code, str)) {
+ pw_log_error("Invalid fec code %s, using default", str);
+ data->fec_code = ROC_FEC_ENCODING_DEFAULT;
+ }
+ pw_properties_set(props, "fec.code", NULL);
+ } else {
+ data->fec_code = ROC_FEC_ENCODING_DEFAULT;
+ }
+
+ data->core = pw_context_get_object(data->module_context, PW_TYPE_INTERFACE_Core);
+ if (data->core == NULL) {
+ str = pw_properties_get(props, PW_KEY_REMOTE_NAME);
+ data->core = pw_context_connect(data->module_context,
+ pw_properties_new(
+ PW_KEY_REMOTE_NAME, str,
+ NULL),
+ 0);
+ data->do_disconnect = true;
+ }
+ if (data->core == NULL) {
+ res = -errno;
+ pw_log_error("can't connect: %m");
+ goto out;
+ }
+
+ pw_proxy_add_listener((struct pw_proxy*)data->core,
+ &data->core_proxy_listener,
+ &core_proxy_events, data);
+ pw_core_add_listener(data->core,
+ &data->core_listener,
+ &core_events, data);
+
+ if ((res = roc_source_setup(data)) < 0)
+ goto out;
+
+ pw_impl_module_add_listener(module, &data->module_listener, &module_events, data);
+
+ pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_roc_source_info));
+
+ pw_log_info("Successfully loaded module-roc-source");
+
+ return 0;
+out:
+ impl_destroy(data);
+ return res;
+}
diff --git a/src/modules/module-roc/common.h b/src/modules/module-roc/common.h
new file mode 100644
index 0000000..248c66e
--- /dev/null
+++ b/src/modules/module-roc/common.h
@@ -0,0 +1,71 @@
+#ifndef MODULE_ROC_COMMON_H
+#define MODULE_ROC_COMMON_H
+
+#include <roc/config.h>
+#include <roc/endpoint.h>
+
+#include <spa/utils/string.h>
+
+#define PW_ROC_DEFAULT_IP "0.0.0.0"
+#define PW_ROC_DEFAULT_SOURCE_PORT 10001
+#define PW_ROC_DEFAULT_REPAIR_PORT 10002
+#define PW_ROC_DEFAULT_SESS_LATENCY 200
+#define PW_ROC_DEFAULT_RATE 44100
+
+static inline int pw_roc_parse_fec_encoding(roc_fec_encoding *out, const char *str)
+{
+ if (!str || !*str)
+ *out = ROC_FEC_ENCODING_DEFAULT;
+ else if (spa_streq(str, "disable"))
+ *out = ROC_FEC_ENCODING_DISABLE;
+ else if (spa_streq(str, "rs8m"))
+ *out = ROC_FEC_ENCODING_RS8M;
+ else if (spa_streq(str, "ldpc"))
+ *out = ROC_FEC_ENCODING_LDPC_STAIRCASE;
+ else
+ return -EINVAL;
+ return 0;
+}
+
+static inline int pw_roc_parse_resampler_profile(roc_resampler_profile *out, const char *str)
+{
+ if (!str || !*str)
+ *out = ROC_RESAMPLER_PROFILE_DEFAULT;
+ else if (spa_streq(str, "disable"))
+ *out = ROC_RESAMPLER_PROFILE_DISABLE;
+ else if (spa_streq(str, "high"))
+ *out = ROC_RESAMPLER_PROFILE_HIGH;
+ else if (spa_streq(str, "medium"))
+ *out = ROC_RESAMPLER_PROFILE_MEDIUM;
+ else if (spa_streq(str, "low"))
+ *out = ROC_RESAMPLER_PROFILE_LOW;
+ else
+ return -EINVAL;
+ return 0;
+}
+
+static inline int pw_roc_create_endpoint(roc_endpoint **result, roc_protocol protocol, const char *ip, int port)
+{
+ roc_endpoint *endpoint;
+
+ if (roc_endpoint_allocate(&endpoint))
+ return -ENOMEM;
+
+ if (roc_endpoint_set_protocol(endpoint, protocol))
+ goto out_error_free_ep;
+
+ if (roc_endpoint_set_host(endpoint, ip))
+ goto out_error_free_ep;
+
+ if (roc_endpoint_set_port(endpoint, port))
+ goto out_error_free_ep;
+
+ *result = endpoint;
+ return 0;
+
+out_error_free_ep:
+ (void) roc_endpoint_deallocate(endpoint);
+ return -EINVAL;
+}
+
+#endif /* MODULE_ROC_COMMON_H */
diff --git a/src/modules/module-rt.c b/src/modules/module-rt.c
new file mode 100644
index 0000000..f6b4ade
--- /dev/null
+++ b/src/modules/module-rt.c
@@ -0,0 +1,1095 @@
+/* PipeWire
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+/***
+ Copyright 2009 Lennart Poettering
+ Copyright 2010 David Henningsson <diwic@ubuntu.com>
+
+ Permission is hereby granted, free of charge, to any person
+ obtaining a copy of this software and associated documentation files
+ (the "Software"), to deal in the Software without restriction,
+ including without limitation the rights to use, copy, modify, merge,
+ publish, distribute, sublicense, and/or sell copies of the Software,
+ and to permit persons to whom the Software is furnished to do so,
+ subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be
+ included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+***/
+
+#include <stdlib.h>
+#include <stdbool.h>
+#include <string.h>
+#include <stdio.h>
+#include <errno.h>
+#include <sys/stat.h>
+#if defined(__FreeBSD__) || defined(__MidnightBSD__)
+#include <sys/thr.h>
+#endif
+#include <fcntl.h>
+#include <unistd.h>
+#include <pthread.h>
+#include <sys/resource.h>
+#include <sys/syscall.h>
+
+#include "config.h"
+
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+
+#include <pipewire/impl.h>
+#include <pipewire/thread.h>
+
+#ifdef HAVE_DBUS
+#include <spa/support/dbus.h>
+#include <dbus/dbus.h>
+#endif
+
+/** \page page_module_rt PipeWire Module: RT
+ *
+ * The `rt` modules can give real-time priorities to processing threads.
+ *
+ * It uses the operating system's scheduler to enable realtime scheduling
+ * for certain threads to assist with low latency audio processing.
+ * This requires `RLIMIT_RTPRIO` to be set to a value that's equal to this
+ * module's `rt.prio` parameter or higher. Most distros will come with some
+ * package that configures this for certain groups or users. If this is not set
+ * up and DBus is available, then this module will fall back to using RTKit.
+ *
+ * ## Module Options
+ *
+ * - `nice.level`: The nice value set for the application thread. It improves
+ * performance of the communication with the pipewire daemon.
+ * - `rt.prio`: The realtime priority of the data thread. Higher values are
+ * higher priority.
+ * - `rt.time.soft`, `rt.time.hard`: The amount of CPU time an RT thread can
+ * consume without doing any blocking calls before the kernel kills
+ * the thread. This is a safety measure to avoid lockups of the complete
+ * system when some thread consumes 100%.
+
+ * The nice level is by default set to an invalid value so that clients don't
+ * automatically have the nice level raised.
+ *
+ * The PipeWire server processes are explicitly configured with a valid nice level.
+ *
+ * ## Example configuration
+ *
+ *\code{.unparsed}
+ * context.modules = [
+ * { name = libpipewire-module-rt
+ * args = {
+ * #nice.level = 20
+ * #rt.prio = 88
+ * #rt.time.soft = -1
+ * #rt.time.hard = -1
+ * }
+ * flags = [ ifexists nofail ]
+ * }
+ * ]
+ *\endcode
+ */
+
+#define NAME "rt"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+#define REALTIME_POLICY SCHED_FIFO
+#ifdef SCHED_RESET_ON_FORK
+#define PW_SCHED_RESET_ON_FORK SCHED_RESET_ON_FORK
+#else
+/* FreeBSD compat */
+#define PW_SCHED_RESET_ON_FORK 0
+#endif
+
+#define IS_VALID_NICE_LEVEL(l) ((l)>=-20 && (l)<=19)
+
+#define DEFAULT_NICE_LEVEL 20
+#define DEFAULT_RT_PRIO_MIN 11
+#define DEFAULT_RT_PRIO 88
+#define DEFAULT_RT_TIME_SOFT -1
+#define DEFAULT_RT_TIME_HARD -1
+
+#define MODULE_USAGE "[nice.level=<priority: default "SPA_STRINGIFY(DEFAULT_NICE_LEVEL)"(don't change)>] " \
+ "[rt.prio=<priority: default "SPA_STRINGIFY(DEFAULT_RT_PRIO)">] " \
+ "[rt.time.soft=<in usec: default "SPA_STRINGIFY(DEFAULT_RT_TIME_SOFT)"] " \
+ "[rt.time.hard=<in usec: default "SPA_STRINGIFY(DEFAULT_RT_TIME_HARD)"] "
+
+static const struct spa_dict_item module_props[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Use realtime thread scheduling, falling back to RTKit" },
+ { PW_KEY_MODULE_USAGE, MODULE_USAGE },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+#ifdef HAVE_DBUS
+#define RTKIT_SERVICE_NAME "org.freedesktop.RealtimeKit1"
+#define RTKIT_OBJECT_PATH "/org/freedesktop/RealtimeKit1"
+#define RTKIT_INTERFACE "org.freedesktop.RealtimeKit1"
+
+#define XDG_PORTAL_SERVICE_NAME "org.freedesktop.portal.Desktop"
+#define XDG_PORTAL_OBJECT_PATH "/org/freedesktop/portal/desktop"
+#define XDG_PORTAL_INTERFACE "org.freedesktop.portal.Realtime"
+
+/** \cond */
+struct pw_rtkit_bus {
+ DBusConnection *bus;
+};
+/** \endcond */
+
+struct thread {
+ struct impl *impl;
+ struct spa_list link;
+ pthread_t thread;
+ pid_t pid;
+ void *(*start)(void*);
+ void *arg;
+};
+#endif /* HAVE_DBUS */
+
+struct impl {
+ struct pw_context *context;
+
+ struct spa_thread_utils thread_utils;
+
+ int nice_level;
+ int rt_prio;
+ rlim_t rt_time_soft;
+ rlim_t rt_time_hard;
+
+ struct spa_hook module_listener;
+
+#ifdef HAVE_DBUS
+ bool use_rtkit;
+ /* For D-Bus. These are const static. */
+ const char* service_name;
+ const char* object_path;
+ const char* interface;
+ struct pw_rtkit_bus *rtkit_bus;
+
+ /* These are only for the RTKit implementation to fill in the `thread`
+ * struct. Since there's barely any overhead here we'll do this
+ * regardless of which backend is used. */
+ pthread_mutex_t lock;
+ pthread_cond_t cond;
+ struct spa_list threads_list;
+#endif
+};
+
+#ifndef RLIMIT_RTTIME
+#define RLIMIT_RTTIME 15
+#endif
+
+static pid_t _gettid(void)
+{
+#if defined(HAVE_GETTID)
+ return (pid_t) gettid();
+#elif defined(__linux__)
+ return syscall(SYS_gettid);
+#elif defined(__FreeBSD__) || defined(__MidnightBSD__)
+ long pid;
+ thr_self(&pid);
+ return (pid_t)pid;
+#else
+#error "No gettid impl"
+#endif
+}
+
+#ifdef HAVE_DBUS
+struct pw_rtkit_bus *pw_rtkit_bus_get(DBusBusType bus_type)
+{
+ struct pw_rtkit_bus *bus;
+ DBusError error;
+
+ if (getenv("DISABLE_RTKIT")) {
+ errno = ENOTSUP;
+ return NULL;
+ }
+
+ dbus_error_init(&error);
+
+ bus = calloc(1, sizeof(struct pw_rtkit_bus));
+ if (bus == NULL)
+ return NULL;
+
+ bus->bus = dbus_bus_get_private(bus_type, &error);
+ if (bus->bus == NULL)
+ goto error;
+
+ dbus_connection_set_exit_on_disconnect(bus->bus, false);
+
+ return bus;
+
+error:
+ free(bus);
+ pw_log_error("Failed to connect to %s bus: %s",
+ bus_type == DBUS_BUS_SYSTEM ? "system" : "session", error.message);
+ dbus_error_free(&error);
+ errno = ECONNREFUSED;
+ return NULL;
+}
+
+struct pw_rtkit_bus *pw_rtkit_bus_get_system(void)
+{
+ return pw_rtkit_bus_get(DBUS_BUS_SYSTEM);
+}
+
+struct pw_rtkit_bus *pw_rtkit_bus_get_session(void)
+{
+ return pw_rtkit_bus_get(DBUS_BUS_SESSION);
+}
+
+bool pw_rtkit_check_xdg_portal(struct pw_rtkit_bus *system_bus)
+{
+ if (!dbus_bus_name_has_owner(system_bus->bus, XDG_PORTAL_SERVICE_NAME, NULL)) {
+ pw_log_warn("Can't find %s. Is xdg-desktop-portal running?", XDG_PORTAL_SERVICE_NAME);
+ return false;
+ }
+
+ return true;
+}
+
+void pw_rtkit_bus_free(struct pw_rtkit_bus *system_bus)
+{
+ dbus_connection_close(system_bus->bus);
+ dbus_connection_unref(system_bus->bus);
+ free(system_bus);
+}
+
+static int translate_error(const char *name)
+{
+ pw_log_warn("RTKit error: %s", name);
+
+ if (spa_streq(name, DBUS_ERROR_NO_MEMORY))
+ return -ENOMEM;
+ if (spa_streq(name, DBUS_ERROR_SERVICE_UNKNOWN) ||
+ spa_streq(name, DBUS_ERROR_NAME_HAS_NO_OWNER))
+ return -ENOENT;
+ if (spa_streq(name, DBUS_ERROR_ACCESS_DENIED) ||
+ spa_streq(name, DBUS_ERROR_AUTH_FAILED))
+ return -EACCES;
+
+ return -EIO;
+}
+
+static long long rtkit_get_int_property(struct impl *impl, const char *propname,
+ long long *propval)
+{
+ DBusMessage *m = NULL, *r = NULL;
+ DBusMessageIter iter, subiter;
+ dbus_int64_t i64;
+ dbus_int32_t i32;
+ DBusError error;
+ int current_type;
+ long long ret;
+ struct pw_rtkit_bus *connection = impl->rtkit_bus;
+
+ dbus_error_init(&error);
+
+ if (!(m = dbus_message_new_method_call(impl->service_name,
+ impl->object_path,
+ "org.freedesktop.DBus.Properties", "Get"))) {
+ ret = -ENOMEM;
+ goto finish;
+ }
+
+ if (!dbus_message_append_args(m,
+ DBUS_TYPE_STRING, &impl->interface,
+ DBUS_TYPE_STRING, &propname, DBUS_TYPE_INVALID)) {
+ ret = -ENOMEM;
+ goto finish;
+ }
+
+ if (!(r = dbus_connection_send_with_reply_and_block(connection->bus, m, -1, &error))) {
+ ret = translate_error(error.name);
+ goto finish;
+ }
+
+ if (dbus_set_error_from_message(&error, r)) {
+ ret = translate_error(error.name);
+ goto finish;
+ }
+
+ ret = -EBADMSG;
+ dbus_message_iter_init(r, &iter);
+ while ((current_type = dbus_message_iter_get_arg_type(&iter)) != DBUS_TYPE_INVALID) {
+
+ if (current_type == DBUS_TYPE_VARIANT) {
+ dbus_message_iter_recurse(&iter, &subiter);
+
+ while ((current_type =
+ dbus_message_iter_get_arg_type(&subiter)) != DBUS_TYPE_INVALID) {
+
+ if (current_type == DBUS_TYPE_INT32) {
+ dbus_message_iter_get_basic(&subiter, &i32);
+ *propval = i32;
+ ret = 0;
+ }
+
+ if (current_type == DBUS_TYPE_INT64) {
+ dbus_message_iter_get_basic(&subiter, &i64);
+ *propval = i64;
+ ret = 0;
+ }
+
+ dbus_message_iter_next(&subiter);
+ }
+ }
+ dbus_message_iter_next(&iter);
+ }
+
+finish:
+
+ if (m)
+ dbus_message_unref(m);
+
+ if (r)
+ dbus_message_unref(r);
+
+ dbus_error_free(&error);
+
+ return ret;
+}
+
+int pw_rtkit_get_max_realtime_priority(struct impl *impl)
+{
+ long long retval;
+ int err;
+
+ err = rtkit_get_int_property(impl, "MaxRealtimePriority", &retval);
+ return err < 0 ? err : retval;
+}
+
+int pw_rtkit_get_min_nice_level(struct impl *impl, int *min_nice_level)
+{
+ long long retval;
+ int err;
+
+ err = rtkit_get_int_property(impl, "MinNiceLevel", &retval);
+ if (err >= 0)
+ *min_nice_level = retval;
+ return err;
+}
+
+long long pw_rtkit_get_rttime_usec_max(struct impl *impl)
+{
+ long long retval;
+ int err;
+
+ err = rtkit_get_int_property(impl, "RTTimeUSecMax", &retval);
+ return err < 0 ? err : retval;
+}
+
+int pw_rtkit_make_realtime(struct impl *impl, pid_t thread, int priority)
+{
+ DBusMessage *m = NULL, *r = NULL;
+ dbus_uint64_t pid;
+ dbus_uint64_t u64;
+ dbus_uint32_t u32;
+ DBusError error;
+ int ret;
+ struct pw_rtkit_bus *connection = impl->rtkit_bus;
+
+ dbus_error_init(&error);
+
+ if (thread == 0)
+ thread = _gettid();
+
+ if (!(m = dbus_message_new_method_call(impl->service_name,
+ impl->object_path, impl->interface,
+ "MakeThreadRealtimeWithPID"))) {
+ ret = -ENOMEM;
+ goto finish;
+ }
+
+ pid = (dbus_uint64_t) getpid();
+ u64 = (dbus_uint64_t) thread;
+ u32 = (dbus_uint32_t) priority;
+
+ if (!dbus_message_append_args(m,
+ DBUS_TYPE_UINT64, &pid,
+ DBUS_TYPE_UINT64, &u64,
+ DBUS_TYPE_UINT32, &u32, DBUS_TYPE_INVALID)) {
+ ret = -ENOMEM;
+ goto finish;
+ }
+
+ if (!(r = dbus_connection_send_with_reply_and_block(connection->bus, m, -1, &error))) {
+ ret = translate_error(error.name);
+ goto finish;
+ }
+
+
+ if (dbus_set_error_from_message(&error, r)) {
+ ret = translate_error(error.name);
+ goto finish;
+ }
+
+ ret = 0;
+
+finish:
+
+ if (m)
+ dbus_message_unref(m);
+
+ if (r)
+ dbus_message_unref(r);
+
+ dbus_error_free(&error);
+
+ return ret;
+}
+
+int pw_rtkit_make_high_priority(struct impl *impl, pid_t thread, int nice_level)
+{
+ DBusMessage *m = NULL, *r = NULL;
+ dbus_uint64_t pid;
+ dbus_uint64_t u64;
+ dbus_int32_t s32;
+ DBusError error;
+ int ret;
+ struct pw_rtkit_bus *connection = impl->rtkit_bus;
+
+ dbus_error_init(&error);
+
+ if (thread == 0)
+ thread = _gettid();
+
+ if (!(m = dbus_message_new_method_call(impl->service_name,
+ impl->object_path, impl->interface,
+ "MakeThreadHighPriorityWithPID"))) {
+ ret = -ENOMEM;
+ goto finish;
+ }
+
+ pid = (dbus_uint64_t) getpid();
+ u64 = (dbus_uint64_t) thread;
+ s32 = (dbus_int32_t) nice_level;
+
+ if (!dbus_message_append_args(m,
+ DBUS_TYPE_UINT64, &pid,
+ DBUS_TYPE_UINT64, &u64,
+ DBUS_TYPE_INT32, &s32, DBUS_TYPE_INVALID)) {
+ ret = -ENOMEM;
+ goto finish;
+ }
+
+
+
+ if (!(r = dbus_connection_send_with_reply_and_block(connection->bus, m, -1, &error))) {
+ ret = translate_error(error.name);
+ goto finish;
+ }
+
+
+ if (dbus_set_error_from_message(&error, r)) {
+ ret = translate_error(error.name);
+ goto finish;
+ }
+
+ ret = 0;
+
+finish:
+
+ if (m)
+ dbus_message_unref(m);
+
+ if (r)
+ dbus_message_unref(r);
+
+ dbus_error_free(&error);
+
+ return ret;
+}
+#endif /* HAVE_DBUS */
+
+static void module_destroy(void *data)
+{
+ struct impl *impl = data;
+ pw_context_set_object(impl->context, SPA_TYPE_INTERFACE_ThreadUtils, NULL);
+ spa_hook_remove(&impl->module_listener);
+
+#ifdef HAVE_DBUS
+ if (impl->rtkit_bus)
+ pw_rtkit_bus_free(impl->rtkit_bus);
+#endif
+
+ free(impl);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy,
+};
+
+static int get_rt_priority_range(int *out_min, int *out_max)
+{
+ int min, max;
+
+ if ((min = sched_get_priority_min(REALTIME_POLICY)) < 0)
+ return -errno;
+ if ((max = sched_get_priority_max(REALTIME_POLICY)) < 0)
+ return -errno;
+
+ if (out_min)
+ *out_min = min;
+ if (out_max)
+ *out_max = max;
+
+ return 0;
+}
+/**
+ * Check if the current user has permissions to use realtime scheduling at the
+ * specified priority.
+ */
+static bool check_realtime_privileges(struct impl *impl)
+{
+ rlim_t priority = impl->rt_prio;
+ int err, old_policy, new_policy, min, max;
+ struct sched_param old_sched_params;
+ struct sched_param new_sched_params;
+ int try = 0;
+
+ while (try++ < 2) {
+ /* We could check `RLIMIT_RTPRIO`, but the BSDs generally don't have
+ * that available, and there are also other ways to use realtime
+ * scheduling without that rlimit being set such as `CAP_SYS_NICE` or
+ * running as root. Instead of checking a bunch of preconditions, we
+ * just try if setting realtime scheduling works or not. */
+ if ((err = pthread_getschedparam(pthread_self(), &old_policy, &old_sched_params)) != 0) {
+ pw_log_warn("Failed to check RLIMIT_RTPRIO: %s", strerror(err));
+ return false;
+ }
+ if ((err = get_rt_priority_range(&min, &max)) < 0) {
+ pw_log_warn("Failed to get priority range: %s", strerror(err));
+ return false;
+ }
+ if (try == 2) {
+ struct rlimit rlim;
+ /* second try, try to clamp to RLIMIT_RTPRIO */
+ if (getrlimit(RLIMIT_RTPRIO, &rlim) == 0 && max > (int)rlim.rlim_max) {
+ pw_log_info("Clamp rtprio %d to %d", (int)priority, (int)rlim.rlim_max);
+ max = (int)rlim.rlim_max;
+ }
+ else
+ break;
+ }
+ if (max < DEFAULT_RT_PRIO_MIN) {
+ pw_log_info("Priority max (%d) must be at least %d", max, DEFAULT_RT_PRIO_MIN);
+ return false;
+ }
+
+ /* If the current scheduling policy has `SCHED_RESET_ON_FORK` set, then
+ * this also needs to be set here or `pthread_setschedparam()` will return
+ * an error code. Similarly, if it is not set, then we don't want to set
+ * it here as it would irreversible change the current thread's
+ * scheduling policy. */
+ spa_zero(new_sched_params);
+ new_sched_params.sched_priority = SPA_CLAMP((int)priority, min, max);
+ new_policy = REALTIME_POLICY;
+ if ((old_policy & PW_SCHED_RESET_ON_FORK) != 0)
+ new_policy |= PW_SCHED_RESET_ON_FORK;
+
+ if (pthread_setschedparam(pthread_self(), new_policy, &new_sched_params) == 0) {
+ impl->rt_prio = new_sched_params.sched_priority;
+ pthread_setschedparam(pthread_self(), old_policy, &old_sched_params);
+ return true;
+ }
+ }
+ pw_log_info("Can't set rt prio to %d: %m (try increasing rlimits)", (int)priority);
+ return false;
+}
+
+static int sched_set_nice(int nice_level)
+{
+ if (setpriority(PRIO_PROCESS, _gettid(), nice_level) == 0)
+ return 0;
+ else
+ return -errno;
+}
+
+static int set_nice(struct impl *impl, int nice_level, bool warn)
+{
+ int res = 0;
+
+#ifdef HAVE_DBUS
+ if (impl->use_rtkit)
+ res = pw_rtkit_make_high_priority(impl, 0, nice_level);
+ else
+ res = sched_set_nice(nice_level);
+#else
+ res = sched_set_nice(nice_level);
+#endif
+
+ if (res < 0) {
+ if (warn)
+ pw_log_warn("could not set nice-level to %d: %s",
+ nice_level, spa_strerror(res));
+ } else {
+ pw_log_info("main thread nice level set to %d",
+ nice_level);
+ }
+ return res;
+}
+
+static int set_rlimit(struct impl *impl)
+{
+ struct rlimit rl;
+ int res = 0;
+
+ spa_zero(rl);
+ rl.rlim_cur = impl->rt_time_soft;
+ rl.rlim_max = impl->rt_time_hard;
+
+#ifdef HAVE_DBUS
+ if (impl->use_rtkit) {
+ long long rttime;
+ rttime = pw_rtkit_get_rttime_usec_max(impl);
+ if (rttime >= 0) {
+ if ((rlim_t)rttime < rl.rlim_cur) {
+ pw_log_debug("clamping rt.time.soft from %llu to %lld because of RTKit",
+ (long long)rl.rlim_cur, rttime);
+ }
+
+ rl.rlim_cur = SPA_MIN(rl.rlim_cur, (rlim_t)rttime);
+ rl.rlim_max = SPA_MIN(rl.rlim_max, (rlim_t)rttime);
+ }
+ }
+#endif
+
+ if (setrlimit(RLIMIT_RTTIME, &rl) < 0)
+ res = -errno;
+
+ if (res < 0)
+ pw_log_debug("setrlimit() failed: %s", spa_strerror(res));
+ else
+ pw_log_debug("rt.time.soft:%"PRIi64" rt.time.hard:%"PRIi64,
+ (int64_t)rl.rlim_cur, (int64_t)rl.rlim_max);
+
+ return res;
+}
+
+static int acquire_rt_sched(struct spa_thread *thread, int priority)
+{
+ int err, min, max;
+ struct sched_param sp;
+ pthread_t pt = (pthread_t)thread;
+
+ if ((err = get_rt_priority_range(&min, &max)) < 0)
+ return err;
+
+ if (priority < min || priority > max) {
+ pw_log_info("clamping priority %d to range %d - %d for policy %d",
+ priority, min, max, REALTIME_POLICY);
+ priority = SPA_CLAMP(priority, min, max);
+ }
+
+ spa_zero(sp);
+ sp.sched_priority = priority;
+ if ((err = pthread_setschedparam(pt, REALTIME_POLICY | PW_SCHED_RESET_ON_FORK, &sp)) != 0) {
+ pw_log_warn("could not make thread %p realtime: %s", thread, strerror(err));
+ return -err;
+ }
+
+ pw_log_info("acquired realtime priority %d for thread %p", priority, thread);
+ return 0;
+}
+
+static int impl_drop_rt_generic(void *object, struct spa_thread *thread)
+{
+ struct sched_param sp;
+ pthread_t pt = (pthread_t)thread;
+ int err;
+
+ spa_zero(sp);
+ if ((err = pthread_setschedparam(pt, SCHED_OTHER | PW_SCHED_RESET_ON_FORK, &sp)) != 0) {
+ pw_log_debug("thread %p: SCHED_OTHER|SCHED_RESET_ON_FORK failed: %s",
+ thread, strerror(err));
+ return -err;
+ }
+ pw_log_info("thread %p dropped realtime priority", thread);
+ return 0;
+}
+
+#ifdef HAVE_DBUS
+static struct thread *find_thread_by_pt(struct impl *impl, pthread_t pt)
+{
+ struct thread *t;
+
+ spa_list_for_each(t, &impl->threads_list, link) {
+ if (pthread_equal(t->thread, pt))
+ return t;
+ }
+ return NULL;
+}
+
+static void *custom_start(void *data)
+{
+ struct thread *this = data;
+ struct impl *impl = this->impl;
+
+ pthread_mutex_lock(&impl->lock);
+ this->pid = _gettid();
+ pthread_cond_broadcast(&impl->cond);
+ pthread_mutex_unlock(&impl->lock);
+
+ return this->start(this->arg);
+}
+
+static struct spa_thread *impl_create(void *object, const struct spa_dict *props,
+ void *(*start_routine)(void*), void *arg)
+{
+ struct impl *impl = object;
+ struct thread *this;
+ struct spa_thread *thread;
+
+ this = calloc(1, sizeof(*this));
+ this->impl = impl;
+ this->start = start_routine;
+ this->arg = arg;
+
+ /* This thread list is only used for the RTKit implementation */
+ pthread_mutex_lock(&impl->lock);
+ thread = pw_thread_utils_create(props, custom_start, this);
+ if (thread == NULL)
+ goto exit;
+
+ this->thread = (pthread_t)thread;
+ pthread_cond_wait(&impl->cond, &impl->lock);
+
+ spa_list_append(&impl->threads_list, &this->link);
+exit:
+ pthread_mutex_unlock(&impl->lock);
+
+ if (thread == NULL) {
+ free(this);
+ return NULL;
+ }
+ return thread;
+}
+
+static int impl_join(void *object, struct spa_thread *thread, void **retval)
+{
+ struct impl *impl = object;
+ pthread_t pt = (pthread_t)thread;
+ struct thread *thr;
+
+ pthread_mutex_lock(&impl->lock);
+ if ((thr = find_thread_by_pt(impl, pt)) != NULL) {
+ spa_list_remove(&thr->link);
+ free(thr);
+ }
+ pthread_mutex_unlock(&impl->lock);
+
+ return pthread_join(pt, retval);
+}
+
+
+static int get_rtkit_priority_range(struct impl *impl, int *min, int *max)
+{
+ if (min)
+ *min = 1;
+ if (max) {
+ if ((*max = pw_rtkit_get_max_realtime_priority(impl)) < 0)
+ return *max;
+ if (*max < 1)
+ *max = 1;
+ }
+ return 0;
+}
+
+static int impl_get_rt_range(void *object, const struct spa_dict *props,
+ int *min, int *max)
+{
+ struct impl *impl = object;
+ int res;
+ if (impl->use_rtkit)
+ res = get_rtkit_priority_range(impl, min, max);
+ else
+ res = get_rt_priority_range(min, max);
+ return res;
+}
+
+static pid_t impl_gettid(struct impl *impl, pthread_t pt)
+{
+ struct thread *thr;
+ pid_t pid;
+
+ pthread_mutex_lock(&impl->lock);
+ if ((thr = find_thread_by_pt(impl, pt)) != NULL)
+ pid = thr->pid;
+ else
+ pid = _gettid();
+ pthread_mutex_unlock(&impl->lock);
+
+ return pid;
+}
+
+static int impl_acquire_rt(void *object, struct spa_thread *thread, int priority)
+{
+ struct impl *impl = object;
+ struct sched_param sp;
+ int err;
+ pthread_t pt = (pthread_t)thread;
+ pid_t pid;
+
+ /* See the docstring on `spa_thread_utils_methods::acquire_rt` */
+ if (priority == -1) {
+ priority = impl->rt_prio;
+ }
+
+ if (impl->use_rtkit) {
+ int min, max;
+
+ if ((err = get_rtkit_priority_range(impl, &min, &max)) < 0)
+ return err;
+
+ pid = impl_gettid(impl, pt);
+
+ if (priority < min || priority > max) {
+ pw_log_info("clamping requested priority %d for thread %d "
+ "between %d and %d", priority, pid, min, max);
+ priority = SPA_CLAMP(priority, min, max);
+ }
+
+ spa_zero(sp);
+ sp.sched_priority = priority;
+
+ if (pthread_setschedparam(pt, SCHED_OTHER | PW_SCHED_RESET_ON_FORK, &sp) == 0) {
+ pw_log_debug("SCHED_OTHER|SCHED_RESET_ON_FORK worked.");
+ }
+
+ if ((err = pw_rtkit_make_realtime(impl, pid, priority)) < 0) {
+ pw_log_warn("could not make thread %d realtime using RTKit: %s", pid, spa_strerror(err));
+ return err;
+ }
+
+ pw_log_info("acquired realtime priority %d for thread %d using RTKit", priority, pid);
+ return 0;
+ } else {
+ return acquire_rt_sched(thread, priority);
+ }
+}
+
+static const struct spa_thread_utils_methods impl_thread_utils = {
+ SPA_VERSION_THREAD_UTILS_METHODS,
+ .create = impl_create,
+ .join = impl_join,
+ .get_rt_range = impl_get_rt_range,
+ .acquire_rt = impl_acquire_rt,
+ .drop_rt = impl_drop_rt_generic,
+};
+
+#else /* HAVE_DBUS */
+
+static struct spa_thread *impl_create(void *object, const struct spa_dict *props,
+ void *(*start_routine)(void*), void *arg)
+{
+ return pw_thread_utils_create(props, start_routine, arg);
+}
+
+static int impl_join(void *object, struct spa_thread *thread, void **retval)
+{
+ return pw_thread_utils_join(thread, retval);
+}
+
+static int impl_get_rt_range(void *object, const struct spa_dict *props,
+ int *min, int *max)
+{
+ return get_rt_priority_range(min, max);
+}
+
+static int impl_acquire_rt(void *object, struct spa_thread *thread, int priority)
+{
+ struct impl *impl = object;
+
+ /* See the docstring on `spa_thread_utils_methods::acquire_rt` */
+ if (priority == -1)
+ priority = impl->rt_prio;
+
+ return acquire_rt_sched(thread, priority);
+}
+
+static const struct spa_thread_utils_methods impl_thread_utils = {
+ SPA_VERSION_THREAD_UTILS_METHODS,
+ .create = impl_create,
+ .join = impl_join,
+ .get_rt_range = impl_get_rt_range,
+ .acquire_rt = impl_acquire_rt,
+ .drop_rt = impl_drop_rt_generic,
+};
+#endif /* HAVE_DBUS */
+
+
+#ifdef HAVE_DBUS
+static int check_rtkit(struct impl *impl, struct pw_context *context, bool *can_use_rtkit)
+{
+ const struct pw_properties *context_props;
+ const char *str;
+
+ *can_use_rtkit = true;
+
+ if ((context_props = pw_context_get_properties(context)) != NULL &&
+ (str = pw_properties_get(context_props, "support.dbus")) != NULL &&
+ !pw_properties_parse_bool(str))
+ *can_use_rtkit = false;
+
+ return 0;
+}
+#endif /* HAVE_DBUS */
+
+SPA_EXPORT
+int pipewire__module_init(struct pw_impl_module *module, const char *args)
+{
+ struct pw_context *context = pw_impl_module_get_context(module);
+ struct impl *impl;
+ struct pw_properties *props;
+ int res = 0;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ impl = calloc(1, sizeof(struct impl));
+ if (impl == NULL)
+ return -ENOMEM;
+
+ pw_log_debug("module %p: new", impl);
+
+ props = args ? pw_properties_new_string(args) : pw_properties_new(NULL, NULL);
+ if (!props) {
+ res = -errno;
+ goto error;
+ }
+
+ impl->context = context;
+ impl->nice_level = pw_properties_get_int32(props, "nice.level", DEFAULT_NICE_LEVEL);
+ impl->rt_prio = pw_properties_get_int32(props, "rt.prio", DEFAULT_RT_PRIO);
+ impl->rt_time_soft = pw_properties_get_int32(props, "rt.time.soft", DEFAULT_RT_TIME_SOFT);
+ impl->rt_time_hard = pw_properties_get_int32(props, "rt.time.hard", DEFAULT_RT_TIME_HARD);
+
+ bool can_use_rtkit = false, use_rtkit = false;
+
+#ifdef HAVE_DBUS
+ spa_list_init(&impl->threads_list);
+ pthread_mutex_init(&impl->lock, NULL);
+ pthread_cond_init(&impl->cond, NULL);
+
+ if ((res = check_rtkit(impl, context, &can_use_rtkit)) < 0)
+ goto error;
+#endif
+ /* If the user has permissions to use regular realtime scheduling, as well as
+ * the nice level we want, then we'll use that instead of RTKit */
+ if (!check_realtime_privileges(impl)) {
+ if (!can_use_rtkit) {
+ res = -ENOTSUP;
+ pw_log_warn("regular realtime scheduling not available (RTKit fallback disabled)");
+ goto error;
+ }
+ use_rtkit = true;
+ }
+
+ if (IS_VALID_NICE_LEVEL(impl->nice_level)) {
+ if (set_nice(impl, impl->nice_level, !can_use_rtkit) < 0)
+ use_rtkit = can_use_rtkit;
+ }
+
+#ifdef HAVE_DBUS
+ impl->use_rtkit = use_rtkit;
+ if (impl->use_rtkit) {
+ /* Checking xdg-desktop-portal. It works fine in all situations. */
+ impl->rtkit_bus = pw_rtkit_bus_get_session();
+ if (impl->rtkit_bus != NULL) {
+ if (pw_rtkit_check_xdg_portal(impl->rtkit_bus)) {
+ impl->service_name = XDG_PORTAL_SERVICE_NAME;
+ impl->object_path = XDG_PORTAL_OBJECT_PATH;
+ impl->interface = XDG_PORTAL_INTERFACE;
+ } else {
+ pw_log_warn("found session bus but no portal");
+ pw_rtkit_bus_free(impl->rtkit_bus);
+ impl->rtkit_bus = NULL;
+ }
+ }
+ /* Failed to get xdg-desktop-portal, try to use rtkit. */
+ if (impl->rtkit_bus == NULL) {
+ impl->rtkit_bus = pw_rtkit_bus_get_system();
+ if (impl->rtkit_bus != NULL) {
+ impl->service_name = RTKIT_SERVICE_NAME;
+ impl->object_path = RTKIT_OBJECT_PATH;
+ impl->interface = RTKIT_INTERFACE;
+ } else {
+ res = -errno;
+ pw_log_warn("could not get system bus: %m");
+ goto error;
+ }
+ }
+ /* Retry set_nice with rtkit */
+ if (IS_VALID_NICE_LEVEL(impl->nice_level))
+ set_nice(impl, impl->nice_level, true);
+ }
+#endif
+ set_rlimit(impl);
+
+ impl->thread_utils.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_ThreadUtils,
+ SPA_VERSION_THREAD_UTILS,
+ &impl_thread_utils, impl);
+
+ pw_context_set_object(context, SPA_TYPE_INTERFACE_ThreadUtils,
+ &impl->thread_utils);
+
+ pw_impl_module_add_listener(module, &impl->module_listener, &module_events, impl);
+
+ pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_props));
+ pw_impl_module_update_properties(module, &props->dict);
+
+#ifdef HAVE_DBUS
+ if (impl->use_rtkit) {
+ pw_log_debug("initialized using RTKit");
+ } else {
+ pw_log_debug("initialized using regular realtime scheduling");
+ }
+#else
+ pw_log_debug("initialized using regular realtime scheduling");
+#endif
+
+ goto done;
+
+error:
+#ifdef HAVE_DBUS
+ if (impl->rtkit_bus)
+ pw_rtkit_bus_free(impl->rtkit_bus);
+#endif
+ free(impl);
+done:
+ pw_properties_free(props);
+
+ return res;
+}
diff --git a/src/modules/module-rtp-sink.c b/src/modules/module-rtp-sink.c
new file mode 100644
index 0000000..b0c622d
--- /dev/null
+++ b/src/modules/module-rtp-sink.c
@@ -0,0 +1,975 @@
+/* PipeWire
+ *
+ * Copyright © 2022 Wim Taymans <wim.taymans@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "config.h"
+
+#include <limits.h>
+#include <unistd.h>
+#include <sys/stat.h>
+#include <sys/socket.h>
+#include <sys/ioctl.h>
+#include <arpa/inet.h>
+#include <netinet/ip.h>
+#include <netinet/in.h>
+#include <net/if.h>
+#include <ctype.h>
+
+#include <spa/utils/hook.h>
+#include <spa/utils/result.h>
+#include <spa/utils/ringbuffer.h>
+#include <spa/utils/json.h>
+#include <spa/param/audio/format-utils.h>
+#include <spa/debug/types.h>
+
+#include <pipewire/pipewire.h>
+#include <pipewire/impl.h>
+
+#include <module-rtp/sap.h>
+#include <module-rtp/rtp.h>
+
+
+/** \page page_module_rtp_sink PipeWire Module: RTP sink
+ *
+ * The `rtp-sink` module creates a PipeWire sink that sends audio
+ * RTP packets.
+ *
+ * ## Module Options
+ *
+ * Options specific to the behavior of this module
+ *
+ * - `sap.ip = <str>`: IP address of the SAP messages, default "224.0.0.56"
+ * - `sap.port = <int>`: port of the SAP messages, default 9875
+ * - `source.ip =<str>`: source IP address, default "0.0.0.0"
+ * - `destination.ip =<str>`: destination IP address, default "224.0.0.56"
+ * - `destination.port =<int>`: destination port, default random beteen 46000 and 47024
+ * - `local.ifname = <str>`: interface name to use
+ * - `net.mtu = <int>`: MTU to use, default 1280
+ * - `net.ttl = <int>`: TTL to use, default 1
+ * - `net.loop = <bool>`: loopback multicast, default false
+ * - `sess.min-ptime = <int>`: minimum packet time in milliseconds, default 2
+ * - `sess.max-ptime = <int>`: maximum packet time in milliseconds, default 20
+ * - `sess.name = <str>`: a session name
+ * - `stream.props = {}`: properties to be passed to the stream
+ *
+ * ## General options
+ *
+ * Options with well-known behavior:
+ *
+ * - \ref PW_KEY_REMOTE_NAME
+ * - \ref PW_KEY_AUDIO_FORMAT
+ * - \ref PW_KEY_AUDIO_RATE
+ * - \ref PW_KEY_AUDIO_CHANNELS
+ * - \ref SPA_KEY_AUDIO_POSITION
+ * - \ref PW_KEY_NODE_NAME
+ * - \ref PW_KEY_NODE_DESCRIPTION
+ * - \ref PW_KEY_MEDIA_NAME
+ * - \ref PW_KEY_NODE_GROUP
+ * - \ref PW_KEY_NODE_LATENCY
+ * - \ref PW_KEY_NODE_VIRTUAL
+ * - \ref PW_KEY_MEDIA_CLASS
+ *
+ * ## Example configuration
+ *\code{.unparsed}
+ * context.modules = [
+ * { name = libpipewire-module-rtp-sink
+ * args = {
+ * #sap.ip = "224.0.0.56"
+ * #sap.port = 9875
+ * #source.ip = "0.0.0.0"
+ * #destination.ip = "224.0.0.56"
+ * #destination.port = 46000
+ * #local.ifname = "eth0"
+ * #net.mtu = 1280
+ * #net.ttl = 1
+ * #net.loop = false
+ * #sess.min-ptime = 2
+ * #sess.max-ptime = 20
+ * #sess.name = "PipeWire RTP stream"
+ * #audio.format = "S16BE"
+ * #audio.rate = 48000
+ * #audio.channels = 2
+ * #audio.position = [ FL FR ]
+ * stream.props = {
+ * node.name = "rtp-sink"
+ * }
+ * }
+ *}
+ *]
+ *\endcode
+ *
+ * \since 0.3.60
+ */
+
+#define NAME "rtp-sink"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+#define SAP_INTERVAL_SEC 5
+#define SAP_MIME_TYPE "application/sdp"
+
+#define BUFFER_SIZE (1u<<20)
+#define BUFFER_MASK (BUFFER_SIZE-1)
+
+#define DEFAULT_SAP_IP "224.0.0.56"
+#define DEFAULT_SAP_PORT 9875
+
+#define DEFAULT_FORMAT "S16BE"
+#define DEFAULT_RATE 48000
+#define DEFAULT_CHANNELS 2
+#define DEFAULT_POSITION "[ FL FR ]"
+
+#define DEFAULT_PORT 46000
+#define DEFAULT_SOURCE_IP "0.0.0.0"
+#define DEFAULT_DESTINATION_IP "224.0.0.56"
+#define DEFAULT_TTL 1
+#define DEFAULT_MTU 1280
+#define DEFAULT_LOOP false
+
+#define DEFAULT_MIN_PTIME 2
+#define DEFAULT_MAX_PTIME 20
+
+#define USAGE "sap.ip=<SAP IP address to send announce, default:"DEFAULT_SAP_IP"> " \
+ "sap.port=<SAP port to send on, default:"SPA_STRINGIFY(DEFAULT_SAP_PORT)"> " \
+ "source.ip=<source IP address, default:"DEFAULT_SOURCE_IP"> " \
+ "destination.ip=<destination IP address, default:"DEFAULT_DESTINATION_IP"> " \
+ "local.ifname=<local interface name to use> " \
+ "net.mtu=<desired MTU, default:"SPA_STRINGIFY(DEFAULT_MTU)"> " \
+ "net.ttl=<desired TTL, default:"SPA_STRINGIFY(DEFAULT_TTL)"> " \
+ "net.loop=<desired loopback, default:"SPA_STRINGIFY(DEFAULT_LOOP)"> " \
+ "sess.name=<a name for the session> " \
+ "sess.min-ptime=<minimum packet time in milliseconds, default:2> " \
+ "sess.max-ptime=<maximum packet time in milliseconds, default:20> " \
+ "audio.format=<format, default:"DEFAULT_FORMAT"> " \
+ "audio.rate=<sample rate, default:"SPA_STRINGIFY(DEFAULT_RATE)"> " \
+ "audio.channels=<number of channels, default:"SPA_STRINGIFY(DEFAULT_CHANNELS)"> "\
+ "audio.position=<channel map, default:"DEFAULT_POSITION"> " \
+ "stream.props= { key=value ... }"
+
+static const struct spa_dict_item module_info[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "RTP Sink" },
+ { PW_KEY_MODULE_USAGE, USAGE },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+static const struct format_info {
+ uint32_t format;
+ uint32_t size;
+ const char *mime;
+} format_info[] = {
+ { SPA_AUDIO_FORMAT_U8, 1, "L8" },
+ { SPA_AUDIO_FORMAT_ALAW, 1, "PCMA" },
+ { SPA_AUDIO_FORMAT_ULAW, 1, "PCMU" },
+ { SPA_AUDIO_FORMAT_S16_BE, 2, "L16" },
+ { SPA_AUDIO_FORMAT_S24_BE, 3, "L24" },
+};
+
+static const struct format_info *find_format_info(uint32_t format)
+{
+ SPA_FOR_EACH_ELEMENT_VAR(format_info, f)
+ if (f->format == format)
+ return f;
+ return NULL;
+}
+
+struct impl {
+ struct pw_impl_module *module;
+ struct spa_hook module_listener;
+ struct pw_properties *props;
+ struct pw_context *module_context;
+
+ struct pw_loop *loop;
+
+ struct pw_core *core;
+ struct spa_hook core_listener;
+ struct spa_hook core_proxy_listener;
+
+ struct spa_source *timer;
+
+ struct pw_properties *stream_props;
+ struct pw_stream *stream;
+ struct spa_hook stream_listener;
+
+ unsigned int do_disconnect:1;
+
+ char *ifname;
+ char *session_name;
+ int sess_latency_msec;
+ int mtu;
+ bool ttl;
+ bool mcast_loop;
+ uint32_t min_ptime;
+ uint32_t max_ptime;
+ uint32_t pbytes;
+
+ struct sockaddr_storage src_addr;
+ socklen_t src_len;
+
+ uint16_t port;
+ struct sockaddr_storage dst_addr;
+ socklen_t dst_len;
+
+ uint16_t sap_port;
+ struct sockaddr_storage sap_addr;
+ socklen_t sap_len;
+
+ uint16_t msg_id_hash;
+ uint32_t ntp;
+
+ struct spa_audio_info_raw info;
+ const struct format_info *format_info;
+ uint32_t frame_size;
+ int payload;
+ uint16_t seq;
+ uint32_t timestamp;
+ uint32_t ssrc;
+
+ struct spa_ringbuffer ring;
+ uint8_t buffer[BUFFER_SIZE];
+
+ int rtp_fd;
+ int sap_fd;
+};
+
+
+static void stream_destroy(void *d)
+{
+ struct impl *impl = d;
+ spa_hook_remove(&impl->stream_listener);
+ impl->stream = NULL;
+}
+
+static inline void
+set_iovec(struct spa_ringbuffer *rbuf, void *buffer, uint32_t size,
+ uint32_t offset, struct iovec *iov, uint32_t len)
+{
+ iov[0].iov_len = SPA_MIN(len, size - offset);
+ iov[0].iov_base = SPA_PTROFF(buffer, offset, void);
+ iov[1].iov_len = len - iov[0].iov_len;
+ iov[1].iov_base = buffer;
+}
+
+static void flush_packets(struct impl *impl)
+{
+ int32_t avail;
+ uint32_t index;
+ struct iovec iov[3];
+ struct msghdr msg;
+ ssize_t n;
+ struct rtp_header header;
+ int32_t tosend;
+
+ avail = spa_ringbuffer_get_read_index(&impl->ring, &index);
+
+ tosend = impl->pbytes;
+
+ if (avail < tosend)
+ return;
+
+ spa_zero(header);
+ header.v = 2;
+ header.pt = impl->payload;
+ header.ssrc = htonl(impl->ssrc);
+
+ iov[0].iov_base = &header;
+ iov[0].iov_len = sizeof(header);
+
+ msg.msg_name = NULL;
+ msg.msg_namelen = 0;
+ msg.msg_iov = iov;
+ msg.msg_iovlen = 3;
+ msg.msg_control = NULL;
+ msg.msg_controllen = 0;
+ msg.msg_flags = 0;
+
+ while (avail >= tosend) {
+ header.sequence_number = htons(impl->seq);
+ header.timestamp = htonl(impl->timestamp);
+
+ set_iovec(&impl->ring,
+ impl->buffer, BUFFER_SIZE,
+ index & BUFFER_MASK,
+ &iov[1], tosend);
+
+ n = sendmsg(impl->rtp_fd, &msg, MSG_NOSIGNAL);
+ if (n < 0) {
+ switch (errno) {
+ case ECONNREFUSED:
+ case ECONNRESET:
+ pw_log_debug("remote end not listening");
+ break;
+ default:
+ pw_log_warn("sendmsg() failed: %m");
+ break;
+ }
+ }
+
+ impl->seq++;
+ impl->timestamp += tosend / impl->frame_size;
+
+ index += tosend;
+ avail -= tosend;
+ }
+ spa_ringbuffer_read_update(&impl->ring, index);
+}
+
+static void stream_process(void *data)
+{
+ struct impl *impl = data;
+ struct pw_buffer *buf;
+ struct spa_data *d;
+ uint32_t index;
+ int32_t filled, wanted;
+
+ if ((buf = pw_stream_dequeue_buffer(impl->stream)) == NULL) {
+ pw_log_debug("Out of stream buffers: %m");
+ return;
+ }
+ d = buf->buffer->datas;
+
+ wanted = d[0].chunk->size;
+
+ filled = spa_ringbuffer_get_write_index(&impl->ring, &index);
+
+ if (filled + wanted > (int32_t)BUFFER_SIZE) {
+ pw_log_warn("overrun %u + %u > %u", filled, wanted, BUFFER_SIZE);
+ } else {
+ spa_ringbuffer_write_data(&impl->ring,
+ impl->buffer,
+ BUFFER_SIZE,
+ index & BUFFER_MASK,
+ d[0].data, wanted);
+
+ index += wanted;
+ spa_ringbuffer_write_update(&impl->ring, index);
+ }
+ pw_stream_queue_buffer(impl->stream, buf);
+
+ flush_packets(impl);
+}
+
+static void on_stream_state_changed(void *d, enum pw_stream_state old,
+ enum pw_stream_state state, const char *error)
+{
+ struct impl *impl = d;
+
+ switch (state) {
+ case PW_STREAM_STATE_UNCONNECTED:
+ pw_log_info("stream disconnected, unloading");
+ pw_impl_module_schedule_destroy(impl->module);
+ break;
+ case PW_STREAM_STATE_ERROR:
+ pw_log_error("stream error: %s", error);
+ break;
+ default:
+ break;
+ }
+}
+
+static const struct pw_stream_events in_stream_events = {
+ PW_VERSION_STREAM_EVENTS,
+ .destroy = stream_destroy,
+ .state_changed = on_stream_state_changed,
+ .process = stream_process
+};
+
+static int parse_address(const char *address, uint16_t port,
+ struct sockaddr_storage *addr, socklen_t *len)
+{
+ struct sockaddr_in *sa4 = (struct sockaddr_in*)addr;
+ struct sockaddr_in6 *sa6 = (struct sockaddr_in6*)addr;
+
+ if (inet_pton(AF_INET, address, &sa4->sin_addr) > 0) {
+ sa4->sin_family = AF_INET;
+ sa4->sin_port = htons(port);
+ *len = sizeof(*sa4);
+ } else if (inet_pton(AF_INET6, address, &sa6->sin6_addr) > 0) {
+ sa6->sin6_family = AF_INET6;
+ sa6->sin6_port = htons(port);
+ *len = sizeof(*sa6);
+ } else
+ return -EINVAL;
+
+ return 0;
+}
+
+static bool is_multicast(struct sockaddr *sa, socklen_t salen)
+{
+ if (sa->sa_family == AF_INET) {
+ static const uint32_t ipv4_mcast_mask = 0xe0000000;
+ struct sockaddr_in *sa4 = (struct sockaddr_in*)sa;
+ return (ntohl(sa4->sin_addr.s_addr) & ipv4_mcast_mask) == ipv4_mcast_mask;
+ } else if (sa->sa_family == AF_INET6) {
+ struct sockaddr_in6 *sa6 = (struct sockaddr_in6*)sa;
+ return sa6->sin6_addr.s6_addr[0] == 0xff;
+ }
+ return false;
+}
+
+static int make_socket(struct sockaddr_storage *src, socklen_t src_len,
+ struct sockaddr_storage *dst, socklen_t dst_len,
+ bool loop, int ttl)
+{
+ int af, fd, val, res;
+
+ af = src->ss_family;
+ if ((fd = socket(af, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0)) < 0) {
+ pw_log_error("socket failed: %m");
+ return -errno;
+ }
+ if (bind(fd, (struct sockaddr*)src, src_len) < 0) {
+ res = -errno;
+ pw_log_error("bind() failed: %m");
+ goto error;
+ }
+ if (connect(fd, (struct sockaddr*)dst, dst_len) < 0) {
+ res = -errno;
+ pw_log_error("connect() failed: %m");
+ goto error;
+ }
+ if (is_multicast((struct sockaddr*)dst, dst_len)) {
+ val = loop;
+ if (setsockopt(fd, IPPROTO_IP, IP_MULTICAST_LOOP, &val, sizeof(val)) < 0)
+ pw_log_warn("setsockopt(IP_MULTICAST_LOOP) failed: %m");
+
+ val = ttl;
+ if (setsockopt(fd, IPPROTO_IP, IP_MULTICAST_TTL, &val, sizeof(val)) < 0)
+ pw_log_warn("setsockopt(IP_MULTICAST_TTL) failed: %m");
+ }
+#ifdef SO_PRIORITY
+ val = 6;
+ if (setsockopt(fd, SOL_SOCKET, SO_PRIORITY, &val, sizeof(val)) < 0)
+ pw_log_warn("setsockopt(SO_PRIORITY) failed: %m");
+#endif
+ val = IPTOS_LOWDELAY;
+ if (setsockopt(fd, IPPROTO_IP, IP_TOS, &val, sizeof(val)) < 0)
+ pw_log_warn("setsockopt(IP_TOS) failed: %m");
+
+
+ return fd;
+error:
+ close(fd);
+ return res;
+}
+
+static int setup_stream(struct impl *impl)
+{
+ const struct spa_pod *params[1];
+ struct spa_pod_builder b;
+ uint32_t n_params;
+ uint8_t buffer[1024];
+ struct pw_properties *props;
+ int res, fd;
+
+ props = pw_properties_copy(impl->stream_props);
+ if (props == NULL)
+ return -errno;
+
+ if (pw_properties_get(props, PW_KEY_NODE_LATENCY) == NULL) {
+ pw_properties_setf(props, PW_KEY_NODE_LATENCY,
+ "%d/%d", impl->pbytes / impl->frame_size,
+ impl->info.rate);
+ }
+ pw_properties_setf(props, PW_KEY_NODE_RATE, "1/%d", impl->info.rate);
+
+ impl->stream = pw_stream_new(impl->core,
+ "rtp-sink capture", props);
+ if (impl->stream == NULL)
+ return -errno;
+
+ pw_stream_add_listener(impl->stream,
+ &impl->stream_listener,
+ &in_stream_events, impl);
+
+ n_params = 0;
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ params[n_params++] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat,
+ &impl->info);
+
+ if ((res = pw_stream_connect(impl->stream,
+ PW_DIRECTION_INPUT,
+ PW_ID_ANY,
+ PW_STREAM_FLAG_MAP_BUFFERS |
+ PW_STREAM_FLAG_AUTOCONNECT |
+ PW_STREAM_FLAG_RT_PROCESS,
+ params, n_params)) < 0)
+ return res;
+
+
+ if ((fd = make_socket(&impl->src_addr, impl->src_len,
+ &impl->dst_addr, impl->dst_len,
+ impl->mcast_loop, impl->ttl)) < 0)
+ return fd;
+
+ impl->rtp_fd = fd;
+
+ return 0;
+}
+
+static int get_ip(const struct sockaddr_storage *sa, char *ip, size_t len)
+{
+ if (sa->ss_family == AF_INET) {
+ struct sockaddr_in *in = (struct sockaddr_in*)sa;
+ inet_ntop(sa->ss_family, &in->sin_addr, ip, len);
+ } else if (sa->ss_family == AF_INET6) {
+ struct sockaddr_in6 *in = (struct sockaddr_in6*)sa;
+ inet_ntop(sa->ss_family, &in->sin6_addr, ip, len);
+ } else
+ return -EIO;
+ return 0;
+}
+static void send_sap(struct impl *impl, bool bye)
+{
+ char buffer[2048], src_addr[64], dst_addr[64], dst_ttl[8];
+ const char *user_name, *af;
+ struct sockaddr *sa = (struct sockaddr*)&impl->src_addr;
+ struct sap_header header;
+ struct iovec iov[4];
+ struct msghdr msg;
+
+ spa_zero(header);
+ header.v = 1;
+ header.t = bye;
+ header.msg_id_hash = impl->msg_id_hash;
+
+ iov[0].iov_base = &header;
+ iov[0].iov_len = sizeof(header);
+
+ if (sa->sa_family == AF_INET) {
+ iov[1].iov_base = &((struct sockaddr_in*) sa)->sin_addr;
+ iov[1].iov_len = 4U;
+ af = "IP4";
+ } else {
+ iov[1].iov_base = &((struct sockaddr_in6*) sa)->sin6_addr;
+ iov[1].iov_len = 16U;
+ header.a = 1;
+ af = "IP6";
+ }
+ iov[2].iov_base = SAP_MIME_TYPE;
+ iov[2].iov_len = sizeof(SAP_MIME_TYPE);
+
+ get_ip(&impl->src_addr, src_addr, sizeof(src_addr));
+ get_ip(&impl->dst_addr, dst_addr, sizeof(dst_addr));
+
+ if ((user_name = pw_get_user_name()) == NULL)
+ user_name = "-";
+
+ spa_zero(dst_ttl);
+ if (is_multicast((struct sockaddr*)&impl->dst_addr, impl->dst_len))
+ snprintf(dst_ttl, sizeof(dst_ttl), "/%d", impl->ttl);
+
+ snprintf(buffer, sizeof(buffer),
+ "v=0\n"
+ "o=%s %u 0 IN %s %s\n"
+ "s=%s\n"
+ "c=IN %s %s%s\n"
+ "t=%u 0\n"
+ "a=recvonly\n"
+ "a=tool:PipeWire %s\n"
+ "m=audio %u RTP/AVP %i\n"
+ "a=rtpmap:%i %s/%u/%u\n"
+ "a=type:broadcast\n"
+ "a=ptime:%d\n",
+ user_name, impl->ntp, af, src_addr,
+ impl->session_name,
+ af, dst_addr, dst_ttl,
+ impl->ntp,
+ pw_get_library_version(),
+ impl->port, impl->payload,
+ impl->payload, impl->format_info->mime,
+ impl->info.rate, impl->info.channels,
+ (impl->pbytes / impl->frame_size) * 1000 / impl->info.rate);
+
+ iov[3].iov_base = buffer;
+ iov[3].iov_len = strlen(buffer);
+
+ msg.msg_name = NULL;
+ msg.msg_namelen = 0;
+ msg.msg_iov = iov;
+ msg.msg_iovlen = 4;
+ msg.msg_control = NULL;
+ msg.msg_controllen = 0;
+ msg.msg_flags = 0;
+
+ sendmsg(impl->sap_fd, &msg, MSG_NOSIGNAL);
+}
+
+static void on_timer_event(void *data, uint64_t expirations)
+{
+ struct impl *impl = data;
+ send_sap(impl, 0);
+}
+
+static int start_sap_announce(struct impl *impl)
+{
+ int fd, res;
+ struct timespec value, interval;
+
+ if ((fd = make_socket(&impl->src_addr, impl->src_len,
+ &impl->sap_addr, impl->sap_len,
+ impl->mcast_loop, impl->ttl)) < 0)
+ return fd;
+
+ impl->sap_fd = fd;
+
+ pw_log_info("starting SAP timer");
+ impl->timer = pw_loop_add_timer(impl->loop, on_timer_event, impl);
+ if (impl->timer == NULL) {
+ res = -errno;
+ pw_log_error("can't create timer source: %m");
+ goto error;
+ }
+ value.tv_sec = 0;
+ value.tv_nsec = 1;
+ interval.tv_sec = SAP_INTERVAL_SEC;
+ interval.tv_nsec = 0;
+ pw_loop_update_timer(impl->loop, impl->timer, &value, &interval, false);
+
+ return 0;
+error:
+ close(fd);
+ return res;
+
+}
+
+static void core_destroy(void *d)
+{
+ struct impl *impl = d;
+ spa_hook_remove(&impl->core_listener);
+ impl->core = NULL;
+ pw_impl_module_schedule_destroy(impl->module);
+}
+
+static const struct pw_proxy_events core_proxy_events = {
+ .destroy = core_destroy,
+};
+
+static void impl_destroy(struct impl *impl)
+{
+ send_sap(impl, 1);
+
+ if (impl->stream)
+ pw_stream_destroy(impl->stream);
+
+ if (impl->core && impl->do_disconnect)
+ pw_core_disconnect(impl->core);
+
+ if (impl->timer)
+ pw_loop_destroy_source(impl->loop, impl->timer);
+
+ if (impl->rtp_fd != -1)
+ close(impl->rtp_fd);
+ if (impl->sap_fd != -1)
+ close(impl->sap_fd);
+
+ pw_properties_free(impl->stream_props);
+ pw_properties_free(impl->props);
+
+ free(impl->ifname);
+ free(impl->session_name);
+ free(impl);
+}
+
+static void module_destroy(void *d)
+{
+ struct impl *impl = d;
+ spa_hook_remove(&impl->module_listener);
+ impl_destroy(impl);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy,
+};
+
+static void on_core_error(void *d, uint32_t id, int seq, int res, const char *message)
+{
+ struct impl *impl = d;
+
+ pw_log_error("error id:%u seq:%d res:%d (%s): %s",
+ id, seq, res, spa_strerror(res), message);
+
+ if (id == PW_ID_CORE && res == -EPIPE)
+ pw_impl_module_schedule_destroy(impl->module);
+}
+
+static const struct pw_core_events core_events = {
+ PW_VERSION_CORE_EVENTS,
+ .error = on_core_error,
+};
+
+static inline uint32_t format_from_name(const char *name, size_t len)
+{
+ int i;
+ for (i = 0; spa_type_audio_format[i].name; i++) {
+ if (strncmp(name, spa_debug_type_short_name(spa_type_audio_format[i].name), len) == 0)
+ return spa_type_audio_format[i].type;
+ }
+ return SPA_AUDIO_FORMAT_UNKNOWN;
+}
+
+static uint32_t channel_from_name(const char *name)
+{
+ int i;
+ for (i = 0; spa_type_audio_channel[i].name; i++) {
+ if (spa_streq(name, spa_debug_type_short_name(spa_type_audio_channel[i].name)))
+ return spa_type_audio_channel[i].type;
+ }
+ return SPA_AUDIO_CHANNEL_UNKNOWN;
+}
+
+static void parse_position(struct spa_audio_info_raw *info, const char *val, size_t len)
+{
+ struct spa_json it[2];
+ char v[256];
+
+ spa_json_init(&it[0], val, len);
+ if (spa_json_enter_array(&it[0], &it[1]) <= 0)
+ spa_json_init(&it[1], val, len);
+
+ info->channels = 0;
+ while (spa_json_get_string(&it[1], v, sizeof(v)) > 0 &&
+ info->channels < SPA_AUDIO_MAX_CHANNELS) {
+ info->position[info->channels++] = channel_from_name(v);
+ }
+}
+
+static void parse_audio_info(const struct pw_properties *props, struct spa_audio_info_raw *info)
+{
+ const char *str;
+
+ spa_zero(*info);
+ if ((str = pw_properties_get(props, PW_KEY_AUDIO_FORMAT)) == NULL)
+ str = DEFAULT_FORMAT;
+ info->format = format_from_name(str, strlen(str));
+
+ info->rate = pw_properties_get_uint32(props, PW_KEY_AUDIO_RATE, info->rate);
+ if (info->rate == 0)
+ info->rate = DEFAULT_RATE;
+
+ info->channels = pw_properties_get_uint32(props, PW_KEY_AUDIO_CHANNELS, info->channels);
+ info->channels = SPA_MIN(info->channels, SPA_AUDIO_MAX_CHANNELS);
+ if ((str = pw_properties_get(props, SPA_KEY_AUDIO_POSITION)) != NULL)
+ parse_position(info, str, strlen(str));
+ if (info->channels == 0)
+ parse_position(info, DEFAULT_POSITION, strlen(DEFAULT_POSITION));
+}
+
+static void copy_props(struct impl *impl, struct pw_properties *props, const char *key)
+{
+ const char *str;
+ if ((str = pw_properties_get(props, key)) != NULL) {
+ if (pw_properties_get(impl->stream_props, key) == NULL)
+ pw_properties_set(impl->stream_props, key, str);
+ }
+}
+
+SPA_EXPORT
+int pipewire__module_init(struct pw_impl_module *module, const char *args)
+{
+ struct pw_context *context = pw_impl_module_get_context(module);
+ struct impl *impl;
+ struct pw_properties *props = NULL, *stream_props = NULL;
+ uint32_t id = pw_global_get_id(pw_impl_module_get_global(module));
+ uint32_t pid = getpid(), port, min_bytes, max_bytes;
+ char addr[64];
+ const char *str;
+ int res = 0;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ impl = calloc(1, sizeof(struct impl));
+ if (impl == NULL)
+ return -errno;
+
+ impl->rtp_fd = -1;
+ impl->sap_fd = -1;
+
+ if (args == NULL)
+ args = "";
+
+ props = pw_properties_new_string(args);
+ if (props == NULL) {
+ res = -errno;
+ pw_log_error( "can't create properties: %m");
+ goto out;
+ }
+ impl->props = props;
+
+ stream_props = pw_properties_new(NULL, NULL);
+ if (stream_props == NULL) {
+ res = -errno;
+ pw_log_error( "can't create properties: %m");
+ goto out;
+ }
+ impl->stream_props = stream_props;
+
+ impl->module = module;
+ impl->module_context = context;
+ impl->loop = pw_context_get_main_loop(context);
+
+ if (pw_properties_get(props, PW_KEY_NODE_VIRTUAL) == NULL)
+ pw_properties_set(props, PW_KEY_NODE_VIRTUAL, "true");
+ if (pw_properties_get(stream_props, PW_KEY_NODE_NETWORK) == NULL)
+ pw_properties_set(stream_props, PW_KEY_NODE_NETWORK, "true");
+
+ if (pw_properties_get(props, PW_KEY_NODE_NAME) == NULL)
+ pw_properties_setf(props, PW_KEY_NODE_NAME, "rtp-sink-%u-%u", pid, id);
+ if (pw_properties_get(props, PW_KEY_NODE_DESCRIPTION) == NULL)
+ pw_properties_set(props, PW_KEY_NODE_DESCRIPTION,
+ pw_properties_get(props, PW_KEY_NODE_NAME));
+ if (pw_properties_get(props, PW_KEY_MEDIA_NAME) == NULL)
+ pw_properties_set(props, PW_KEY_MEDIA_NAME, "RTP Sender Stream");
+
+ if ((str = pw_properties_get(props, "stream.props")) != NULL)
+ pw_properties_update_string(stream_props, str, strlen(str));
+
+ copy_props(impl, props, PW_KEY_AUDIO_FORMAT);
+ copy_props(impl, props, PW_KEY_AUDIO_RATE);
+ copy_props(impl, props, PW_KEY_AUDIO_CHANNELS);
+ copy_props(impl, props, SPA_KEY_AUDIO_POSITION);
+ copy_props(impl, props, PW_KEY_NODE_NAME);
+ copy_props(impl, props, PW_KEY_NODE_DESCRIPTION);
+ copy_props(impl, props, PW_KEY_NODE_GROUP);
+ copy_props(impl, props, PW_KEY_NODE_LATENCY);
+ copy_props(impl, props, PW_KEY_NODE_VIRTUAL);
+ copy_props(impl, props, PW_KEY_MEDIA_NAME);
+ copy_props(impl, props, PW_KEY_MEDIA_CLASS);
+
+ parse_audio_info(impl->stream_props, &impl->info);
+
+ impl->format_info = find_format_info(impl->info.format);
+ if (impl->format_info == NULL) {
+ pw_log_error("unsupported audio format:%d channels:%d",
+ impl->info.format, impl->info.channels);
+ res = -EINVAL;
+ goto out;
+ }
+ impl->frame_size = impl->format_info->size * impl->info.channels;
+ impl->msg_id_hash = rand();
+ impl->ntp = (uint32_t) time(NULL) + 2208988800U;
+
+ impl->payload = 127;
+ impl->seq = rand();
+ impl->timestamp = rand();
+ impl->ssrc = rand();
+
+ str = pw_properties_get(props, "local.ifname");
+ impl->ifname = str ? strdup(str) : NULL;
+
+ if ((str = pw_properties_get(props, "sap.ip")) == NULL)
+ str = DEFAULT_SAP_IP;
+ port = pw_properties_get_uint32(props, "sap.port", DEFAULT_SAP_PORT);
+ if ((res = parse_address(str, port, &impl->sap_addr, &impl->sap_len)) < 0) {
+ pw_log_error("invalid sap.ip %s: %s", str, spa_strerror(res));
+ goto out;
+ }
+
+ if ((str = pw_properties_get(props, "source.ip")) == NULL)
+ str = DEFAULT_SOURCE_IP;
+ if ((res = parse_address(str, 0, &impl->src_addr, &impl->src_len)) < 0) {
+ pw_log_error("invalid source.ip %s: %s", str, spa_strerror(res));
+ goto out;
+ }
+
+ impl->port = DEFAULT_PORT + ((uint32_t) (rand() % 512) << 1);
+ impl->port = pw_properties_get_uint32(props, "destination.port", impl->port);
+ if ((str = pw_properties_get(props, "destination.ip")) == NULL)
+ str = DEFAULT_DESTINATION_IP;
+ if ((res = parse_address(str, impl->port, &impl->dst_addr, &impl->dst_len)) < 0) {
+ pw_log_error("invalid destination.ip %s: %s", str, spa_strerror(res));
+ goto out;
+ }
+
+ impl->mtu = pw_properties_get_uint32(props, "net.mtu", DEFAULT_MTU);
+ impl->ttl = pw_properties_get_uint32(props, "net.ttl", DEFAULT_TTL);
+ impl->mcast_loop = pw_properties_get_bool(props, "net.loop", DEFAULT_LOOP);
+
+ impl->min_ptime = pw_properties_get_uint32(props, "sess.min-ptime", DEFAULT_MIN_PTIME);
+ impl->max_ptime = pw_properties_get_uint32(props, "sess.max-ptime", DEFAULT_MAX_PTIME);
+
+ min_bytes = (impl->min_ptime * impl->info.rate / 1000) * impl->frame_size;
+ max_bytes = (impl->max_ptime * impl->info.rate / 1000) * impl->frame_size;
+
+ impl->pbytes = SPA_ROUND_DOWN(impl->mtu, impl->frame_size);
+ impl->pbytes = SPA_CLAMP(impl->pbytes, min_bytes, max_bytes);
+
+ if ((str = pw_properties_get(props, "sess.name")) == NULL)
+ pw_properties_setf(props, "sess.name", "PipeWire RTP Stream on %s",
+ pw_get_host_name());
+ str = pw_properties_get(props, "sess.name");
+ impl->session_name = str ? strdup(str) : NULL;
+
+ pw_properties_set(stream_props, "rtp.session", impl->session_name);
+ get_ip(&impl->src_addr, addr, sizeof(addr));
+ pw_properties_set(stream_props, "rtp.source.ip", addr);
+ get_ip(&impl->dst_addr, addr, sizeof(addr));
+ pw_properties_set(stream_props, "rtp.destination.ip", addr);
+ pw_properties_setf(stream_props, "rtp.destination.port", "%u", impl->port);
+ pw_properties_setf(stream_props, "rtp.mtu", "%u", impl->mtu);
+ pw_properties_setf(stream_props, "rtp.ttl", "%u", impl->ttl);
+ pw_properties_setf(stream_props, "rtp.ptime", "%u",
+ (impl->pbytes / impl->frame_size) * 1000 / impl->info.rate);
+
+ impl->core = pw_context_get_object(impl->module_context, PW_TYPE_INTERFACE_Core);
+ if (impl->core == NULL) {
+ str = pw_properties_get(props, PW_KEY_REMOTE_NAME);
+ impl->core = pw_context_connect(impl->module_context,
+ pw_properties_new(
+ PW_KEY_REMOTE_NAME, str,
+ NULL),
+ 0);
+ impl->do_disconnect = true;
+ }
+ if (impl->core == NULL) {
+ res = -errno;
+ pw_log_error("can't connect: %m");
+ goto out;
+ }
+
+ pw_proxy_add_listener((struct pw_proxy*)impl->core,
+ &impl->core_proxy_listener,
+ &core_proxy_events, impl);
+ pw_core_add_listener(impl->core,
+ &impl->core_listener,
+ &core_events, impl);
+
+ if ((res = setup_stream(impl)) < 0)
+ goto out;
+
+ if ((res = start_sap_announce(impl)) < 0)
+ goto out;
+
+ pw_impl_module_add_listener(module, &impl->module_listener, &module_events, impl);
+
+ pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_info));
+
+ pw_log_info("Successfully loaded module-rtp-sink");
+
+ return 0;
+out:
+ impl_destroy(impl);
+ return res;
+}
diff --git a/src/modules/module-rtp-source.c b/src/modules/module-rtp-source.c
new file mode 100644
index 0000000..5353532
--- /dev/null
+++ b/src/modules/module-rtp-source.c
@@ -0,0 +1,1200 @@
+/* PipeWire
+ *
+ * Copyright © 2022 Wim Taymans <wim.taymans@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "config.h"
+
+#include <limits.h>
+#include <unistd.h>
+#include <sys/stat.h>
+#include <sys/socket.h>
+#include <sys/ioctl.h>
+#include <arpa/inet.h>
+#include <netinet/in.h>
+#include <net/if.h>
+#include <ctype.h>
+
+#include <spa/utils/hook.h>
+#include <spa/utils/result.h>
+#include <spa/utils/ringbuffer.h>
+#include <spa/utils/dll.h>
+#include <spa/param/audio/format-utils.h>
+#include <spa/debug/mem.h>
+
+#include <pipewire/pipewire.h>
+#include <pipewire/impl.h>
+
+#include <module-rtp/sap.h>
+#include <module-rtp/rtp.h>
+
+#ifdef __FreeBSD__
+#define ifr_ifindex ifr_index
+#endif
+
+/** \page page_module_rtp_source PipeWire Module: RTP source
+ *
+ * The `rtp-source` module creates a PipeWire source that receives audio
+ * RTP packets.
+ *
+ * ## Module Options
+ *
+ * Options specific to the behavior of this module
+ *
+ * - `sap.ip = <str>`: IP address of the SAP messages, default "224.0.0.56"
+ * - `sap.port = <str>`: port of the SAP messages, default 9875
+ * - `local.ifname = <str>`: interface name to use
+ * - `sess.latency.msec = <str>`: target network latency in milliseconds, default 100
+ * - `stream.props = {}`: properties to be passed to the stream
+ *
+ * ## General options
+ *
+ * Options with well-known behavior:
+ *
+ * - \ref PW_KEY_NODE_NAME
+ * - \ref PW_KEY_NODE_DESCRIPTION
+ * - \ref PW_KEY_MEDIA_NAME
+ * - \ref PW_KEY_MEDIA_CLASS
+ *
+ * ## Example configuration
+ *\code{.unparsed}
+ * context.modules = [
+ * { name = libpipewire-module-rtp-source
+ * args = {
+ * #sap.ip = 224.0.0.56
+ * #sap.port = 9875
+ * #local.ifname = eth0
+ * sess.latency.msec = 100
+ * stream.props = {
+ * #media.class = "Audio/Source"
+ * #node.name = "rtp-source"
+ * }
+ * stream.rules = [
+ * { matches = [
+ * # any of the items in matches needs to match, if one does,
+ * # actions are emited.
+ * { # all keys must match the value. ~ in value starts regex.
+ * #rtp.origin = "wim 3883629975 0 IN IP4 0.0.0.0"
+ * #rtp.payload = "127"
+ * #rtp.fmt = "L16/48000/2"
+ * #rtp.session = "PipeWire RTP Stream on fedora"
+ * }
+ * ]
+ * actions = {
+ * create-stream = {
+ * #sess.latency.msec = 100
+ * #target.object = ""
+ * }
+ * }
+ * }
+ * ]
+ * }
+ * }
+ * ]
+ *\endcode
+ *
+ * \since 0.3.60
+ */
+
+#define NAME "rtp-source"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+#define SAP_MIME_TYPE "application/sdp"
+
+#define ERROR_MSEC 2
+#define MAX_SESSIONS 16
+
+#define DEFAULT_CLEANUP_INTERVAL_SEC 90
+#define DEFAULT_SAP_IP "224.0.0.56"
+#define DEFAULT_SAP_PORT 9875
+#define DEFAULT_SESS_LATENCY 100
+
+#define BUFFER_SIZE (1u<<22)
+#define BUFFER_MASK (BUFFER_SIZE-1)
+
+#define USAGE "sap.ip=<SAP IP address to listen on, default "DEFAULT_SAP_IP"> " \
+ "sap.port=<SAP port to listen on, default "SPA_STRINGIFY(DEFAULT_SAP_PORT)"> " \
+ "local.ifname=<local interface name to use> " \
+ "sess.latency.msec=<target network latency, default "SPA_STRINGIFY(DEFAULT_SESS_LATENCY)"> " \
+ "stream.props= { key=value ... } " \
+ "stream.rules=<rules> "
+
+static const struct spa_dict_item module_info[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "RTP Source" },
+ { PW_KEY_MODULE_USAGE, USAGE },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+struct impl {
+ struct pw_impl_module *module;
+ struct spa_hook module_listener;
+ struct pw_properties *props;
+ struct pw_context *module_context;
+
+ struct pw_loop *loop;
+ struct pw_loop *data_loop;
+
+ struct pw_core *core;
+ struct spa_hook core_listener;
+ struct spa_hook core_proxy_listener;
+
+ struct spa_source *timer;
+ struct spa_source *sap_source;
+
+ struct pw_properties *stream_props;
+
+ unsigned int do_disconnect:1;
+
+ char *ifname;
+ char *sap_ip;
+ int sap_port;
+ int sess_latency_msec;
+ uint32_t cleanup_interval;
+
+ struct spa_list sessions;
+ uint32_t n_sessions;
+};
+
+static const struct format_info {
+ uint32_t format;
+ uint32_t size;
+ const char *mime;
+} format_info[] = {
+ { SPA_AUDIO_FORMAT_U8, 1, "L8" },
+ { SPA_AUDIO_FORMAT_ALAW, 1, "PCMA" },
+ { SPA_AUDIO_FORMAT_ULAW, 1, "PCMU" },
+ { SPA_AUDIO_FORMAT_S16_BE, 2, "L16" },
+ { SPA_AUDIO_FORMAT_S24_BE, 3, "L24" },
+};
+
+static const struct format_info *find_format_info(const char *mime)
+{
+ SPA_FOR_EACH_ELEMENT_VAR(format_info, f)
+ if (spa_streq(f->mime, mime))
+ return f;
+ return NULL;
+}
+
+struct sdp_info {
+ uint16_t hash;
+
+ char origin[128];
+ char session[256];
+
+ struct sockaddr_storage sa;
+ socklen_t salen;
+
+ uint16_t port;
+ uint8_t payload;
+
+ const struct format_info *format_info;
+ struct spa_audio_info_raw info;
+ uint32_t stride;
+};
+
+struct session {
+ struct impl *impl;
+ struct spa_list link;
+
+ uint64_t timestamp;
+
+ struct sdp_info info;
+
+ struct spa_source *source;
+
+ struct pw_stream *stream;
+ struct spa_hook stream_listener;
+
+ uint32_t expected_ssrc;
+ uint16_t expected_seq;
+ unsigned have_ssrc:1;
+ unsigned have_seq:1;
+ unsigned have_sync:1;
+
+ struct spa_ringbuffer ring;
+ uint8_t buffer[BUFFER_SIZE];
+
+ struct spa_io_rate_match *rate_match;
+ struct spa_dll dll;
+ uint32_t target_buffer;
+ uint32_t last_packet_size;
+ float max_error;
+ unsigned buffering:1;
+ unsigned first:1;
+};
+
+static void stream_destroy(void *d)
+{
+ struct session *sess = d;
+ spa_hook_remove(&sess->stream_listener);
+ sess->stream = NULL;
+}
+
+static void stream_process(void *data)
+{
+ struct session *sess = data;
+ struct pw_buffer *buf;
+ struct spa_data *d;
+ uint32_t index, target_buffer;
+ int32_t avail, wanted;
+
+ if ((buf = pw_stream_dequeue_buffer(sess->stream)) == NULL) {
+ pw_log_debug("Out of stream buffers: %m");
+ return;
+ }
+ d = buf->buffer->datas;
+
+ wanted = buf->requested ?
+ SPA_MIN(buf->requested * sess->info.stride, d[0].maxsize)
+ : d[0].maxsize;
+
+ avail = spa_ringbuffer_get_read_index(&sess->ring, &index);
+
+ target_buffer = sess->target_buffer + sess->last_packet_size / 2;
+
+ if (avail < wanted || sess->buffering) {
+ memset(d[0].data, 0, wanted);
+ if (!sess->buffering && sess->have_sync) {
+ pw_log_debug("underrun %u/%u < %u, buffering...",
+ avail, target_buffer, wanted);
+ sess->buffering = true;
+ }
+ } else {
+ float error, corr;
+ if (avail > (int32_t)SPA_MIN(target_buffer * 8, BUFFER_SIZE)) {
+ pw_log_warn("overrun %u > %u", avail, target_buffer * 8);
+ index += avail - target_buffer;
+ avail = target_buffer;
+ } else {
+ if (sess->first) {
+ if ((uint32_t)avail > target_buffer) {
+ uint32_t skip = avail - target_buffer;
+ pw_log_debug("first: avail:%d skip:%u target:%u",
+ avail, skip, target_buffer);
+ index += skip;
+ avail = target_buffer;
+ }
+ sess->first = false;
+ }
+ error = (float)target_buffer - (float)avail;
+ error = SPA_CLAMP(error, -sess->max_error, sess->max_error);
+
+ corr = spa_dll_update(&sess->dll, error);
+
+ pw_log_debug("avail:%u target:%u error:%f corr:%f", avail,
+ target_buffer, error, corr);
+
+ if (sess->rate_match) {
+ SPA_FLAG_SET(sess->rate_match->flags, SPA_IO_RATE_MATCH_FLAG_ACTIVE);
+ sess->rate_match->rate = 1.0f / corr;
+ }
+ }
+ spa_ringbuffer_read_data(&sess->ring,
+ sess->buffer,
+ BUFFER_SIZE,
+ index & BUFFER_MASK,
+ d[0].data, wanted);
+
+ index += wanted;
+ spa_ringbuffer_read_update(&sess->ring, index);
+ }
+ d[0].chunk->size = wanted;
+ d[0].chunk->stride = sess->info.stride;
+ d[0].chunk->offset = 0;
+ buf->size = wanted / sess->info.stride;
+
+ pw_stream_queue_buffer(sess->stream, buf);
+}
+
+static void on_stream_state_changed(void *d, enum pw_stream_state old,
+ enum pw_stream_state state, const char *error)
+{
+ struct session *sess = d;
+ struct impl *impl = sess->impl;
+
+ switch (state) {
+ case PW_STREAM_STATE_UNCONNECTED:
+ pw_log_info("stream disconnected, unloading");
+ pw_impl_module_schedule_destroy(impl->module);
+ break;
+ case PW_STREAM_STATE_ERROR:
+ pw_log_error("stream error: %s", error);
+ break;
+ default:
+ break;
+ }
+}
+
+static void stream_io_changed(void *data, uint32_t id, void *area, uint32_t size)
+{
+ struct session *sess = data;
+ switch (id) {
+ case SPA_IO_RateMatch:
+ sess->rate_match = area;
+ break;
+ }
+}
+
+static const struct pw_stream_events out_stream_events = {
+ PW_VERSION_STREAM_EVENTS,
+ .destroy = stream_destroy,
+ .state_changed = on_stream_state_changed,
+ .io_changed = stream_io_changed,
+ .process = stream_process
+};
+
+static void
+on_rtp_io(void *data, int fd, uint32_t mask)
+{
+ struct session *sess = data;
+ struct rtp_header *hdr;
+ ssize_t len, hlen;
+ uint8_t buffer[2048], *payload;
+
+ if (mask & SPA_IO_IN) {
+ uint32_t index, expected_index, timestamp;
+ uint16_t seq;
+ int32_t filled;
+
+ if ((len = recv(fd, buffer, sizeof(buffer), 0)) < 0)
+ goto receive_error;
+
+ if (len < 12)
+ goto short_packet;
+
+ hdr = (struct rtp_header*)buffer;
+ if (hdr->v != 2)
+ goto invalid_version;
+
+ hlen = 12 + hdr->cc * 4;
+ if (hlen > len)
+ goto invalid_len;
+
+ if (sess->have_ssrc && sess->expected_ssrc != hdr->ssrc)
+ goto unexpected_ssrc;
+ sess->expected_ssrc = hdr->ssrc;
+ sess->have_ssrc = true;
+
+ seq = ntohs(hdr->sequence_number);
+ if (sess->have_seq && sess->expected_seq != seq) {
+ pw_log_warn("unexpected seq (%d != %d)", seq, sess->expected_seq);
+ }
+ sess->expected_seq = seq + 1;
+ sess->have_seq = true;
+
+ len = SPA_ROUND_DOWN(len - hlen, sess->info.stride);
+ payload = &buffer[hlen];
+
+ filled = spa_ringbuffer_get_write_index(&sess->ring, &index);
+
+ timestamp = ntohl(hdr->timestamp);
+ expected_index = timestamp * sess->info.stride;
+
+ if (!sess->have_sync) {
+ pw_log_trace("got rtp, no sync");
+ sess->ring.readindex = sess->ring.writeindex =
+ index = expected_index;
+ filled = 0;
+ sess->have_sync = true;
+ sess->buffering = true;
+ pw_log_debug("sync to timestamp %u", index);
+
+ spa_dll_init(&sess->dll);
+ spa_dll_set_bw(&sess->dll, SPA_DLL_BW_MIN, 128, sess->info.info.rate);
+
+ } else if (expected_index != index) {
+ pw_log_trace("got rtp, wrong timestamp");
+ pw_log_debug("unexpected timestamp (%u != %u)",
+ index / sess->info.stride,
+ expected_index / sess->info.stride);
+ index = expected_index;
+ filled = 0;
+ }
+
+ if (filled + len > BUFFER_SIZE) {
+ pw_log_debug("got rtp, capture overrun %u %zd", filled, len);
+ sess->have_sync = false;
+ } else {
+ uint32_t target_buffer;
+
+ pw_log_trace("got rtp packet len:%zd", len);
+ spa_ringbuffer_write_data(&sess->ring,
+ sess->buffer,
+ BUFFER_SIZE,
+ index & BUFFER_MASK,
+ payload, len);
+ index += len;
+ filled += len;
+ spa_ringbuffer_write_update(&sess->ring, index);
+
+ sess->last_packet_size = len;
+ target_buffer = sess->target_buffer + len/2;
+
+ if (sess->buffering && (uint32_t)filled > target_buffer) {
+ sess->buffering = false;
+ pw_log_debug("buffering done %u > %u",
+ filled, target_buffer);
+ }
+ }
+ }
+ return;
+
+receive_error:
+ pw_log_warn("recv error: %m");
+ return;
+short_packet:
+ pw_log_warn("short packet received");
+ return;
+invalid_version:
+ pw_log_warn("invalid RTP version");
+ return;
+invalid_len:
+ pw_log_warn("invalid RTP length");
+ return;
+unexpected_ssrc:
+ pw_log_warn("unexpected SSRC (expected %u != %u)",
+ sess->expected_ssrc, hdr->ssrc);
+ return;
+}
+
+static int make_socket(const struct sockaddr* sa, socklen_t salen, char *ifname)
+{
+ int af, fd, val, res;
+ struct ifreq req;
+
+ af = sa->sa_family;
+ if ((fd = socket(af, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0)) < 0) {
+ pw_log_error("socket failed: %m");
+ return -errno;
+ }
+#ifdef SO_TIMESTAMP
+ val = 1;
+ if (setsockopt(fd, SOL_SOCKET, SO_TIMESTAMP, &val, sizeof(val)) < 0) {
+ res = -errno;
+ pw_log_error("setsockopt failed: %m");
+ goto error;
+ }
+#endif
+ val = 1;
+ if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(val)) < 0) {
+ res = -errno;
+ pw_log_error("setsockopt failed: %m");
+ goto error;
+ }
+
+ spa_zero(req);
+ if (ifname) {
+ snprintf(req.ifr_name, sizeof(req.ifr_name), "%s", ifname);
+ res = ioctl(fd, SIOCGIFINDEX, &req);
+ if (res < 0)
+ pw_log_warn("SIOCGIFINDEX %s failed: %m", ifname);
+ }
+ res = 0;
+ if (af == AF_INET) {
+ static const uint32_t ipv4_mcast_mask = 0xe0000000;
+ struct sockaddr_in *sa4 = (struct sockaddr_in*)sa;
+ if ((ntohl(sa4->sin_addr.s_addr) & ipv4_mcast_mask) == ipv4_mcast_mask) {
+ struct ip_mreqn mr4;
+ memset(&mr4, 0, sizeof(mr4));
+ mr4.imr_multiaddr = sa4->sin_addr;
+ mr4.imr_ifindex = req.ifr_ifindex;
+ res = setsockopt(fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mr4, sizeof(mr4));
+ } else {
+ sa4->sin_addr.s_addr = INADDR_ANY;
+ }
+ } else if (af == AF_INET6) {
+ struct sockaddr_in6 *sa6 = (struct sockaddr_in6*)sa;
+ if (sa6->sin6_addr.s6_addr[0] == 0xff) {
+ struct ipv6_mreq mr6;
+ memset(&mr6, 0, sizeof(mr6));
+ mr6.ipv6mr_multiaddr = sa6->sin6_addr;
+ mr6.ipv6mr_interface = req.ifr_ifindex;
+ res = setsockopt(fd, IPPROTO_IPV6, IPV6_JOIN_GROUP, &mr6, sizeof(mr6));
+ } else {
+ sa6->sin6_addr = in6addr_any;
+ }
+ } else {
+ res = -EINVAL;
+ goto error;
+ }
+
+ if (res < 0) {
+ res = -errno;
+ pw_log_error("join mcast failed: %m");
+ goto error;
+ }
+
+ if (bind(fd, sa, salen) < 0) {
+ res = -errno;
+ pw_log_error("bind() failed: %m");
+ goto error;
+ }
+ return fd;
+error:
+ return res;
+}
+
+static uint32_t msec_to_bytes(struct sdp_info *info, uint32_t msec)
+{
+ return msec * info->stride * info->info.rate / 1000;
+}
+
+static void session_touch(struct session *sess)
+{
+ struct timespec ts;
+ clock_gettime(CLOCK_MONOTONIC, &ts);
+ sess->timestamp = SPA_TIMESPEC_TO_NSEC(&ts);
+}
+
+static void session_free(struct session *sess)
+{
+ if (sess->impl) {
+ pw_log_info("free session %s %s", sess->info.origin, sess->info.session);
+ sess->impl->n_sessions--;
+ spa_list_remove(&sess->link);
+ }
+ if (sess->stream)
+ pw_stream_destroy(sess->stream);
+ if (sess->source)
+ pw_loop_destroy_source(sess->impl->data_loop, sess->source);
+ free(sess);
+}
+
+struct session_info {
+ struct session *session;
+ struct pw_properties *props;
+ bool matched;
+};
+
+static int rule_matched(void *data, const char *location, const char *action,
+ const char *str, size_t len)
+{
+ struct session_info *i = data;
+ int res = 0;
+
+ i->matched = true;
+ if (spa_streq(action, "create-stream")) {
+ pw_properties_update_string(i->props, str, len);
+ }
+ return res;
+}
+
+static int session_new(struct impl *impl, struct sdp_info *info)
+{
+ struct session *session;
+ const struct spa_pod *params[1];
+ struct spa_pod_builder b;
+ uint32_t n_params;
+ uint8_t buffer[1024];
+ struct pw_properties *props;
+ int res, fd, sess_latency_msec;
+ const char *str;
+
+ if (impl->n_sessions >= MAX_SESSIONS) {
+ pw_log_warn("too many sessions (%u >= %u)", impl->n_sessions, MAX_SESSIONS);
+ return -EMFILE;
+ }
+
+ session = calloc(1, sizeof(struct session));
+ if (session == NULL)
+ return -errno;
+
+ session->info = *info;
+ session->first = true;
+
+ props = pw_properties_copy(impl->stream_props);
+ if (props == NULL) {
+ res = -errno;
+ goto error;
+ }
+
+ pw_properties_set(props, "rtp.origin", info->origin);
+ pw_properties_setf(props, "rtp.payload", "%u", info->payload);
+ pw_properties_setf(props, "rtp.fmt", "%s/%u/%u", info->format_info->mime,
+ info->info.rate, info->info.channels);
+ if (info->session[0]) {
+ pw_properties_set(props, "rtp.session", info->session);
+ pw_properties_setf(props, PW_KEY_MEDIA_NAME, "RTP Stream (%s)",
+ info->session);
+ pw_properties_setf(props, PW_KEY_NODE_NAME, "%s",
+ info->session);
+ } else {
+ pw_properties_set(props, PW_KEY_MEDIA_NAME, "RTP Stream");
+ }
+
+ if ((str = pw_properties_get(impl->props, "stream.rules")) != NULL) {
+ struct session_info sinfo = {
+ .session = session,
+ .props = props,
+ };
+ pw_conf_match_rules(str, strlen(str), NAME, &props->dict,
+ rule_matched, &sinfo);
+
+ if (!sinfo.matched) {
+ res = 0;
+ pw_log_info("session '%s' was not matched", info->session);
+ goto error;
+ }
+ }
+
+ pw_log_info("new session %s %s", info->origin, info->session);
+
+ sess_latency_msec = pw_properties_get_uint32(props,
+ "sess.latency.msec", impl->sess_latency_msec);
+
+ session->target_buffer = msec_to_bytes(info, sess_latency_msec);
+ session->max_error = msec_to_bytes(info, ERROR_MSEC);
+
+ pw_properties_setf(props, PW_KEY_NODE_RATE, "1/%d", info->info.rate);
+ pw_properties_setf(props, PW_KEY_NODE_LATENCY, "%d/%d",
+ session->target_buffer / (2 * info->stride), info->info.rate);
+
+ spa_dll_init(&session->dll);
+ spa_dll_set_bw(&session->dll, SPA_DLL_BW_MIN, 128, session->info.info.rate);
+
+ session->stream = pw_stream_new(impl->core,
+ "rtp-source playback", props);
+ if (session->stream == NULL) {
+ res = -errno;
+ pw_log_error("can't create stream: %m");
+ goto error;
+ }
+
+ pw_stream_add_listener(session->stream,
+ &session->stream_listener,
+ &out_stream_events, session);
+
+ n_params = 0;
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ params[n_params++] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat,
+ &info->info);
+
+ if ((res = pw_stream_connect(session->stream,
+ PW_DIRECTION_OUTPUT,
+ PW_ID_ANY,
+ PW_STREAM_FLAG_MAP_BUFFERS |
+ PW_STREAM_FLAG_AUTOCONNECT |
+ PW_STREAM_FLAG_RT_PROCESS,
+ params, n_params)) < 0) {
+ pw_log_error("can't connect stream: %s", spa_strerror(res));
+ goto error;
+ }
+
+ if ((fd = make_socket((const struct sockaddr *)&info->sa,
+ info->salen, impl->ifname)) < 0) {
+ res = fd;
+ goto error;
+ }
+
+ session->source = pw_loop_add_io(impl->data_loop, fd,
+ SPA_IO_IN, true, on_rtp_io, session);
+ if (session->source == NULL) {
+ res = -errno;
+ pw_log_error("can't create io source: %m");
+ goto error;
+ }
+
+ pw_log_info("starting RTP listener");
+ session_touch(session);
+
+ session->impl = impl;
+ spa_list_append(&impl->sessions, &session->link);
+ impl->n_sessions++;
+
+ return 0;
+error:
+ session_free(session);
+ return res;
+}
+
+static struct session *session_find(struct impl *impl, struct sdp_info *info)
+{
+ struct session *sess;
+ spa_list_for_each(sess, &impl->sessions, link) {
+ if (info->hash == sess->info.hash &&
+ spa_streq(info->origin, sess->info.origin))
+ return sess;
+ }
+ return NULL;
+}
+
+static int parse_sdp_c(struct impl *impl, char *c, struct sdp_info *info)
+{
+ int res;
+
+ c[strcspn(c, "/")] = 0;
+ if (spa_strstartswith(c, "c=IN IP4 ")) {
+ struct sockaddr_in *sa = (struct sockaddr_in*) &info->sa;
+
+ c += strlen("c=IN IP4 ");
+ if (inet_pton(AF_INET, c, &sa->sin_addr) <= 0) {
+ res = -errno;
+ pw_log_warn("inet_pton(%s) failed: %m", c);
+ goto error;
+ }
+ sa->sin_family = AF_INET;
+ info->salen = sizeof(struct sockaddr_in);
+ }
+ else if (spa_strstartswith(c, "c=IN IP6 ")) {
+ struct sockaddr_in6 *sa = (struct sockaddr_in6*) &info->sa;
+
+ c += strlen("c=IN IP6 ");
+ if (inet_pton(AF_INET6, c, &sa->sin6_addr) <= 0) {
+ res = -errno;
+ pw_log_warn("inet_pton(%s) failed: %m", c);
+ goto error;
+ }
+
+ sa->sin6_family = AF_INET6;
+ info->salen = sizeof(struct sockaddr_in6);
+ } else
+ return -EINVAL;
+
+
+ res= 0;
+error:
+ return res;
+}
+
+static int parse_sdp_m(struct impl *impl, char *c, struct sdp_info *info)
+{
+ int port, payload;
+
+ if (!spa_strstartswith(c, "m=audio "))
+ return -EINVAL;
+
+ c += strlen("m=audio ");
+ if (sscanf(c, "%i RTP/AVP %i", &port, &payload) != 2)
+ return -EINVAL;
+
+ if (port <= 0 || port > 0xFFFF)
+ return -EINVAL;
+
+ if (payload < 0 || payload > 127)
+ return -EINVAL;
+
+ info->port = (uint16_t) port;
+ info->payload = (uint8_t) payload;
+
+ return 0;
+}
+
+static int parse_sdp_a(struct impl *impl, char *c, struct sdp_info *info)
+{
+ int payload, len, rate, channels;
+
+ if (!spa_strstartswith(c, "a=rtpmap:"))
+ return 0;
+
+ c += strlen("a=rtpmap:");
+
+ if (sscanf(c, "%i %n", &payload, &len) != 1)
+ return -EINVAL;
+
+ if (payload < 0 || payload > 127)
+ return -EINVAL;
+
+ if (payload != info->payload)
+ return 0;
+
+ c += len;
+ c[strcspn(c, "/")] = 0;
+
+ info->format_info = find_format_info(c);
+ if (info->format_info == NULL)
+ return -EINVAL;
+
+ info->info.format = info->format_info->format;
+ info->stride = info->format_info->size;
+
+ c += strlen(c) + 1;
+ if (sscanf(c, "%u/%u", &rate, &channels) == 2) {
+ info->info.rate = rate;
+ info->info.channels = channels;
+ if (channels == 2) {
+ info->info.position[0] = SPA_AUDIO_CHANNEL_FL;
+ info->info.position[1] = SPA_AUDIO_CHANNEL_FR;
+ }
+ } else if (sscanf(c, "%u", &rate) == 1) {
+ info->info.rate = rate;
+ info->info.channels = 1;
+ } else
+ return -EINVAL;
+
+ info->stride *= info->info.channels;
+
+ return 0;
+}
+
+static int parse_sdp(struct impl *impl, char *sdp, struct sdp_info *info)
+{
+ char *s = sdp;
+ int count = 0, res = 0;
+ size_t l;
+
+ while (*s) {
+ if ((l = strcspn(s, "\r\n")) < 2)
+ goto too_short;
+
+ s[l] = 0;
+ pw_log_debug("%d: %s", count, s);
+
+ if (count++ == 0 && strcmp(s, "v=0") != 0)
+ goto invalid_version;
+
+ if (spa_strstartswith(s, "o="))
+ snprintf(info->origin, sizeof(info->origin), "%s", &s[2]);
+ else if (spa_strstartswith(s, "s="))
+ snprintf(info->session, sizeof(info->session), "%s", &s[2]);
+ else if (spa_strstartswith(s, "c="))
+ res = parse_sdp_c(impl, s, info);
+ else if (spa_strstartswith(s, "m="))
+ res = parse_sdp_m(impl, s, info);
+ else if (spa_strstartswith(s, "a="))
+ res = parse_sdp_a(impl, s, info);
+
+ if (res < 0)
+ goto error;
+ s += l + 1;
+ while (isspace(*s))
+ s++;
+ }
+ if (((struct sockaddr*) &info->sa)->sa_family == AF_INET)
+ ((struct sockaddr_in*) &info->sa)->sin_port = htons(info->port);
+ else
+ ((struct sockaddr_in6*) &info->sa)->sin6_port = htons(info->port);
+
+ return 0;
+too_short:
+ pw_log_warn("SDP: line starting with `%.6s...' too short", s);
+ return -EINVAL;
+invalid_version:
+ pw_log_warn("SDP: invalid first version line `%*s'", (int)l, s);
+ return -EINVAL;
+error:
+ pw_log_warn("SDP: error: %s", spa_strerror(res));
+ return res;
+}
+
+static int parse_sap(struct impl *impl, void *data, size_t len)
+{
+ struct sap_header *header;
+ char *mime, *sdp;
+ struct sdp_info info;
+ struct session *sess;
+ int res;
+ size_t offs;
+ bool bye;
+
+ if (len < 8)
+ return -EINVAL;
+
+ header = (struct sap_header*) data;
+ if (header->v != 1)
+ return -EINVAL;
+
+ if (header->e)
+ return -ENOTSUP;
+ if (header->c)
+ return -ENOTSUP;
+
+ offs = header->a ? 12 : 8;
+ offs += header->auth_len * 4;
+ if (len <= offs)
+ return -EINVAL;
+
+ mime = SPA_PTROFF(data, offs, char);
+ if (spa_strstartswith(mime, "v=0")) {
+ sdp = mime;
+ mime = SAP_MIME_TYPE;
+ } else if (spa_streq(mime, SAP_MIME_TYPE))
+ sdp = SPA_PTROFF(mime, strlen(mime)+1, char);
+ else
+ return -EINVAL;
+
+ pw_log_debug("got sap: %s %s", mime, sdp);
+
+ spa_zero(info);
+ if ((res = parse_sdp(impl, sdp, &info)) < 0)
+ return res;
+
+ bye = header->t;
+
+ sess = session_find(impl, &info);
+ if (sess == NULL) {
+ if (!bye)
+ session_new(impl, &info);
+ } else {
+ if (bye)
+ session_free(sess);
+ else
+ session_touch(sess);
+ }
+ return res;
+}
+
+static void
+on_sap_io(void *data, int fd, uint32_t mask)
+{
+ struct impl *impl = data;
+
+ if (mask & SPA_IO_IN) {
+ uint8_t buffer[2048];
+ ssize_t len;
+
+ if ((len = recv(fd, buffer, sizeof(buffer), 0)) < 0) {
+ pw_log_warn("recv error: %m");
+ return;
+ }
+ if ((size_t)len >= sizeof(buffer))
+ return;
+
+ buffer[len] = 0;
+ parse_sap(impl, buffer, len);
+ }
+}
+
+static int start_sap_listener(struct impl *impl)
+{
+ struct sockaddr_in sa4;
+ struct sockaddr_in6 sa6;
+ struct sockaddr *sa;
+ socklen_t salen;
+ int fd, res;
+
+ if (inet_pton(AF_INET, impl->sap_ip, &sa4.sin_addr) > 0) {
+ sa4.sin_family = AF_INET;
+ sa4.sin_port = htons(impl->sap_port);
+ sa = (struct sockaddr*) &sa4;
+ salen = sizeof(sa4);
+ } else if (inet_pton(AF_INET6, impl->sap_ip, &sa6.sin6_addr) > 0) {
+ sa6.sin6_family = AF_INET6;
+ sa6.sin6_port = htons(impl->sap_port);
+ sa = (struct sockaddr*) &sa6;
+ salen = sizeof(sa6);
+ } else
+ return -EINVAL;
+
+ if ((fd = make_socket(sa, salen, impl->ifname)) < 0)
+ return fd;
+
+ pw_log_info("starting SAP listener");
+ impl->sap_source = pw_loop_add_io(impl->loop, fd,
+ SPA_IO_IN, true, on_sap_io, impl);
+ if (impl->sap_source == NULL) {
+ res = -errno;
+ goto error;
+ }
+ return 0;
+error:
+ close(fd);
+ return res;
+
+}
+
+static void on_timer_event(void *data, uint64_t expirations)
+{
+ struct impl *impl = data;
+ struct timespec now;
+ struct session *sess, *tmp;
+ uint64_t timestamp, interval;
+
+ clock_gettime(CLOCK_MONOTONIC, &now);
+ timestamp = SPA_TIMESPEC_TO_NSEC(&now);
+ interval = impl->cleanup_interval * SPA_NSEC_PER_SEC;
+
+ spa_list_for_each_safe(sess, tmp, &impl->sessions, link) {
+ if (sess->timestamp + interval < timestamp) {
+ pw_log_debug("More than %lu elapsed from last advertisement at %lu", interval, sess->timestamp);
+ pw_log_info("No advertisement packets found for timeout, closing RTP source");
+ session_free(sess);
+ }
+ }
+}
+
+static void core_destroy(void *d)
+{
+ struct impl *impl = d;
+ spa_hook_remove(&impl->core_listener);
+ impl->core = NULL;
+ pw_impl_module_schedule_destroy(impl->module);
+}
+
+static const struct pw_proxy_events core_proxy_events = {
+ .destroy = core_destroy,
+};
+
+static void impl_destroy(struct impl *impl)
+{
+ struct session *sess;
+ spa_list_consume(sess, &impl->sessions, link)
+ session_free(sess);
+
+ if (impl->core && impl->do_disconnect)
+ pw_core_disconnect(impl->core);
+
+ if (impl->sap_source)
+ pw_loop_destroy_source(impl->loop, impl->sap_source);
+ if (impl->timer)
+ pw_loop_destroy_source(impl->loop, impl->timer);
+
+ pw_properties_free(impl->stream_props);
+ pw_properties_free(impl->props);
+
+ free(impl->ifname);
+ free(impl->sap_ip);
+ free(impl);
+}
+
+static void module_destroy(void *d)
+{
+ struct impl *impl = d;
+ spa_hook_remove(&impl->module_listener);
+ impl_destroy(impl);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy,
+};
+
+static void on_core_error(void *d, uint32_t id, int seq, int res, const char *message)
+{
+ struct impl *impl = d;
+
+ pw_log_error("error id:%u seq:%d res:%d (%s): %s",
+ id, seq, res, spa_strerror(res), message);
+
+ if (id == PW_ID_CORE && res == -EPIPE)
+ pw_impl_module_schedule_destroy(impl->module);
+}
+
+static const struct pw_core_events core_events = {
+ PW_VERSION_CORE_EVENTS,
+ .error = on_core_error,
+};
+
+SPA_EXPORT
+int pipewire__module_init(struct pw_impl_module *module, const char *args)
+{
+ struct pw_context *context = pw_impl_module_get_context(module);
+ struct impl *impl;
+ const char *str;
+ struct timespec value, interval;
+ int res = 0;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ impl = calloc(1, sizeof(struct impl));
+ if (impl == NULL)
+ return -errno;
+
+ spa_list_init(&impl->sessions);
+
+ if (args == NULL)
+ args = "";
+
+ impl->props = pw_properties_new_string(args);
+ impl->stream_props = pw_properties_new(NULL, NULL);
+ if (impl->props == NULL || impl->stream_props == NULL) {
+ res = -errno;
+ pw_log_error( "can't create properties: %m");
+ goto out;
+ }
+
+ impl->module = module;
+ impl->module_context = context;
+ impl->loop = pw_context_get_main_loop(context);
+ impl->data_loop = pw_data_loop_get_loop(pw_context_get_data_loop(context));
+
+ if (pw_properties_get(impl->stream_props, PW_KEY_NODE_VIRTUAL) == NULL)
+ pw_properties_set(impl->stream_props, PW_KEY_NODE_VIRTUAL, "true");
+ if (pw_properties_get(impl->stream_props, PW_KEY_NODE_NETWORK) == NULL)
+ pw_properties_set(impl->stream_props, PW_KEY_NODE_NETWORK, "true");
+
+ if ((str = pw_properties_get(impl->props, "stream.props")) != NULL)
+ pw_properties_update_string(impl->stream_props, str, strlen(str));
+
+ str = pw_properties_get(impl->props, "local.ifname");
+ impl->ifname = str ? strdup(str) : NULL;
+
+ str = pw_properties_get(impl->props, "sap.ip");
+ impl->sap_ip = strdup(str ? str : DEFAULT_SAP_IP);
+ impl->sap_port = pw_properties_get_uint32(impl->props,
+ "sap.port", DEFAULT_SAP_PORT);
+ impl->sess_latency_msec = pw_properties_get_uint32(impl->props,
+ "sess.latency.msec", DEFAULT_SESS_LATENCY);
+ impl->cleanup_interval = pw_properties_get_uint32(impl->props,
+ "sap.interval.sec", DEFAULT_CLEANUP_INTERVAL_SEC);
+
+ impl->core = pw_context_get_object(impl->module_context, PW_TYPE_INTERFACE_Core);
+ if (impl->core == NULL) {
+ str = pw_properties_get(impl->props, PW_KEY_REMOTE_NAME);
+ impl->core = pw_context_connect(impl->module_context,
+ pw_properties_new(
+ PW_KEY_REMOTE_NAME, str,
+ NULL),
+ 0);
+ impl->do_disconnect = true;
+ }
+ if (impl->core == NULL) {
+ res = -errno;
+ pw_log_error("can't connect: %m");
+ goto out;
+ }
+
+ pw_proxy_add_listener((struct pw_proxy*)impl->core,
+ &impl->core_proxy_listener,
+ &core_proxy_events, impl);
+ pw_core_add_listener(impl->core,
+ &impl->core_listener,
+ &core_events, impl);
+
+ impl->timer = pw_loop_add_timer(impl->loop, on_timer_event, impl);
+ if (impl->timer == NULL) {
+ res = -errno;
+ pw_log_error("can't create timer source: %m");
+ goto out;
+ }
+ value.tv_sec = 0;
+ value.tv_nsec = 1;
+ interval.tv_sec = impl->cleanup_interval;
+ interval.tv_nsec = 0;
+ pw_loop_update_timer(impl->loop, impl->timer, &value, &interval, false);
+
+ if ((res = start_sap_listener(impl)) < 0)
+ goto out;
+
+ pw_impl_module_add_listener(module, &impl->module_listener, &module_events, impl);
+
+ pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_info));
+
+ pw_log_info("Successfully loaded module-rtp-source");
+
+ return 0;
+out:
+ impl_destroy(impl);
+ return res;
+}
diff --git a/src/modules/module-rtp/rtp.h b/src/modules/module-rtp/rtp.h
new file mode 100644
index 0000000..92ff364
--- /dev/null
+++ b/src/modules/module-rtp/rtp.h
@@ -0,0 +1,78 @@
+/* PipeWire
+ *
+ * Copyright © 2022 Wim Taymans <wim.taymans@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_RTP_H
+#define PIPEWIRE_RTP_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct rtp_header {
+#if __BYTE_ORDER == __LITTLE_ENDIAN
+ unsigned cc:4;
+ unsigned x:1;
+ unsigned p:1;
+ unsigned v:2;
+
+ unsigned pt:7;
+ unsigned m:1;
+#elif __BYTE_ORDER == __BIG_ENDIAN
+ unsigned v:2;
+ unsigned p:1;
+ unsigned x:1;
+ unsigned cc:4;
+
+ unsigned m:1;
+ unsigned pt:7;
+#else
+#error "Unknown byte order"
+#endif
+ uint16_t sequence_number;
+ uint32_t timestamp;
+ uint32_t ssrc;
+ uint32_t csrc[0];
+} __attribute__ ((packed));
+
+struct rtp_payload {
+#if __BYTE_ORDER == __LITTLE_ENDIAN
+ unsigned frame_count:4;
+ unsigned rfa0:1;
+ unsigned is_last_fragment:1;
+ unsigned is_first_fragment:1;
+ unsigned is_fragmented:1;
+#elif __BYTE_ORDER == __BIG_ENDIAN
+ unsigned is_fragmented:1;
+ unsigned is_first_fragment:1;
+ unsigned is_last_fragment:1;
+ unsigned rfa0:1;
+ unsigned frame_count:4;
+#endif
+} __attribute__ ((packed));
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PIPEWIRE_RTP_H */
diff --git a/src/modules/module-rtp/sap.h b/src/modules/module-rtp/sap.h
new file mode 100644
index 0000000..b9a0a7a
--- /dev/null
+++ b/src/modules/module-rtp/sap.h
@@ -0,0 +1,58 @@
+/* PipeWire
+ *
+ * Copyright © 2022 Wim Taymans <wim.taymans@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_SAP_H
+#define PIPEWIRE_SAP_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct sap_header {
+#if __BYTE_ORDER == __LITTLE_ENDIAN
+ unsigned c:1;
+ unsigned e:1;
+ unsigned t:1;
+ unsigned r:1;
+ unsigned a:1;
+ unsigned v:3;
+#elif __BYTE_ORDER == __BIG_ENDIAN
+ unsigned v:3;
+ unsigned a:1;
+ unsigned r:1;
+ unsigned t:1;
+ unsigned e:1;
+ unsigned c:1;
+#else
+#error "Unknown byte order"
+#endif
+ uint8_t auth_len;
+ uint16_t msg_id_hash;
+} __attribute__ ((packed));
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PIPEWIRE_SAP_H */
diff --git a/src/modules/module-session-manager.c b/src/modules/module-session-manager.c
new file mode 100644
index 0000000..85879f7
--- /dev/null
+++ b/src/modules/module-session-manager.c
@@ -0,0 +1,74 @@
+/* PipeWire
+ *
+ * Copyright © 2019 Collabora Ltd.
+ * @author George Kiagiadakis <george.kiagiadakis@collabora.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "config.h"
+
+#include <pipewire/impl.h>
+
+/** \page page_module_session_manager PipeWire Module: Session Manager
+ *
+ * This module implements some usefull objects for implementing a session
+ * manager. It is not yet actively used.
+ */
+
+/* client-endpoint.c */
+int client_endpoint_factory_init(struct pw_impl_module *module);
+/* client-session.c */
+int client_session_factory_init(struct pw_impl_module *module);
+
+int session_factory_init(struct pw_impl_module *module);
+int endpoint_factory_init(struct pw_impl_module *module);
+int endpoint_stream_factory_init(struct pw_impl_module *module);
+int endpoint_link_factory_init(struct pw_impl_module *module);
+
+/* protocol-native.c */
+int pw_protocol_native_ext_session_manager_init(struct pw_context *context);
+
+static const struct spa_dict_item module_props[] = {
+ { PW_KEY_MODULE_AUTHOR, "George Kiagiadakis <george.kiagiadakis@collabora.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Implements objects for session management" },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+SPA_EXPORT
+int pipewire__module_init(struct pw_impl_module *module, const char *args)
+{
+ struct pw_context *context = pw_impl_module_get_context(module);
+ int res;
+
+ if ((res = pw_protocol_native_ext_session_manager_init(context)) < 0)
+ return res;
+
+ client_endpoint_factory_init(module);
+ client_session_factory_init(module);
+ session_factory_init(module);
+ endpoint_factory_init(module);
+ endpoint_stream_factory_init(module);
+ endpoint_link_factory_init(module);
+
+ pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_props));
+
+ return 0;
+}
diff --git a/src/modules/module-session-manager/client-endpoint/client-endpoint.c b/src/modules/module-session-manager/client-endpoint/client-endpoint.c
new file mode 100644
index 0000000..b2f2d98
--- /dev/null
+++ b/src/modules/module-session-manager/client-endpoint/client-endpoint.c
@@ -0,0 +1,296 @@
+/* PipeWire
+ *
+ * Copyright © 2019 Collabora Ltd.
+ * @author George Kiagiadakis <george.kiagiadakis@collabora.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdbool.h>
+#include <string.h>
+
+#include <spa/utils/result.h>
+#include <pipewire/impl.h>
+
+#include <pipewire/extensions/session-manager.h>
+
+#include "client-endpoint.h"
+#include "endpoint.h"
+#include "endpoint-stream.h"
+
+#define NAME "client-endpoint"
+
+struct factory_data {
+ struct pw_impl_module *module;
+ struct spa_hook module_listener;
+
+ struct pw_impl_factory *factory;
+ struct spa_hook factory_listener;
+};
+
+static struct endpoint_stream *find_stream(struct client_endpoint *this, uint32_t id)
+{
+ struct endpoint_stream *s;
+ spa_list_for_each(s, &this->streams, link) {
+ if (s->id == id)
+ return s;
+ }
+ return NULL;
+}
+
+static int client_endpoint_update(void *object,
+ uint32_t change_mask,
+ uint32_t n_params,
+ const struct spa_pod **params,
+ const struct pw_endpoint_info *info)
+{
+ struct client_endpoint *this = object;
+ struct endpoint *endpoint = &this->endpoint;
+
+ return endpoint_update(endpoint, change_mask, n_params, params, info);
+}
+
+static int client_endpoint_stream_update(void *object,
+ uint32_t stream_id,
+ uint32_t change_mask,
+ uint32_t n_params,
+ const struct spa_pod **params,
+ const struct pw_endpoint_stream_info *info)
+{
+ struct client_endpoint *this = object;
+ struct endpoint *endpoint = &this->endpoint;
+ struct endpoint_stream *stream = find_stream(this, stream_id);
+ struct pw_properties *props = NULL;
+
+ if (!stream) {
+ static const char * const keys[] = {
+ PW_KEY_FACTORY_ID,
+ PW_KEY_CLIENT_ID,
+ PW_KEY_ENDPOINT_ID,
+ PW_KEY_PRIORITY_SESSION,
+ PW_KEY_ENDPOINT_MONITOR,
+ PW_KEY_ENDPOINT_STREAM_NAME,
+ PW_KEY_ENDPOINT_STREAM_DESCRIPTION,
+ NULL
+ };
+
+ struct pw_context *context = pw_global_get_context(endpoint->global);
+
+ stream = calloc(1, sizeof(struct endpoint_stream));
+ if (!stream)
+ goto no_mem;
+
+ props = pw_properties_new(NULL, NULL);
+ if (!props)
+ goto no_mem;
+
+ pw_properties_update_keys(props, &endpoint->props->dict, keys);
+ if (info && info->props)
+ pw_properties_update_keys(props, info->props, keys);
+
+ if (endpoint_stream_init(stream, stream_id, endpoint->info.id,
+ this, context, props) < 0)
+ goto no_mem;
+
+ spa_list_append(&this->streams, &stream->link);
+ }
+ else if (change_mask & PW_CLIENT_ENDPOINT_STREAM_UPDATE_DESTROYED) {
+ endpoint_stream_clear(stream);
+ spa_list_remove(&stream->link);
+ free(stream);
+ stream = NULL;
+ }
+
+ return stream ?
+ endpoint_stream_update(stream, change_mask, n_params, params, info)
+ : 0;
+
+ no_mem:
+ pw_properties_free(props);
+ free(stream);
+ pw_log_error(NAME" %p: cannot update stream: no memory", this);
+ pw_resource_errorf(this->resource, -ENOMEM,
+ NAME" %p: cannot update stream: no memory", this);
+ return -ENOMEM;
+}
+
+static const struct pw_client_endpoint_methods methods = {
+ PW_VERSION_CLIENT_ENDPOINT_METHODS,
+ .update = client_endpoint_update,
+ .stream_update = client_endpoint_stream_update,
+};
+
+static void client_endpoint_destroy(void *data)
+{
+ struct client_endpoint *this = data;
+ struct endpoint_stream *s;
+
+ pw_log_debug(NAME" %p: destroy", this);
+
+ spa_list_consume(s, &this->streams, link) {
+ endpoint_stream_clear(s);
+ spa_list_remove(&s->link);
+ free(s);
+ }
+ endpoint_clear(&this->endpoint);
+ spa_hook_remove(&this->resource_listener);
+
+ free(this);
+}
+
+static const struct pw_resource_events resource_events = {
+ PW_VERSION_RESOURCE_EVENTS,
+ .destroy = client_endpoint_destroy,
+};
+
+static void *create_object(void *data,
+ struct pw_resource *owner_resource,
+ const char *type,
+ uint32_t version,
+ struct pw_properties *properties,
+ uint32_t new_id)
+{
+ struct factory_data *d = data;
+ struct pw_impl_factory *factory = d->factory;
+ struct client_endpoint *this;
+ struct pw_impl_client *owner = pw_resource_get_client(owner_resource);
+ struct pw_context *context = pw_impl_client_get_context(owner);
+
+ this = calloc(1, sizeof(struct client_endpoint));
+ if (this == NULL)
+ goto no_mem;
+
+ spa_list_init(&this->streams);
+
+ pw_log_debug(NAME" %p: new", this);
+
+ if (!properties)
+ properties = pw_properties_new(NULL, NULL);
+ if (!properties)
+ goto no_mem;
+
+ pw_properties_setf(properties, PW_KEY_CLIENT_ID, "%d",
+ pw_impl_client_get_info(owner)->id);
+ pw_properties_setf(properties, PW_KEY_FACTORY_ID, "%d",
+ pw_impl_factory_get_info(factory)->id);
+
+ this->resource = pw_resource_new(owner, new_id, PW_PERM_ALL, type, version, 0);
+ if (this->resource == NULL)
+ goto no_mem;
+
+ if (endpoint_init(&this->endpoint, this, context, properties) < 0)
+ goto no_mem;
+
+ pw_resource_add_listener(this->resource, &this->resource_listener,
+ &resource_events, this);
+ pw_resource_add_object_listener(this->resource, &this->object_listener,
+ &methods, this);
+
+ return this;
+
+ no_mem:
+ pw_properties_free(properties);
+ if (this && this->resource)
+ pw_resource_destroy(this->resource);
+ free(this);
+ pw_log_error("can't create client endpoint: no memory");
+ pw_resource_error(owner_resource, -ENOMEM,
+ "can't create client endpoint: no memory");
+ return NULL;
+}
+
+static const struct pw_impl_factory_implementation impl_factory = {
+ PW_VERSION_IMPL_FACTORY_IMPLEMENTATION,
+ .create_object = create_object,
+};
+
+static void factory_destroy(void *data)
+{
+ struct factory_data *d = data;
+ spa_hook_remove(&d->factory_listener);
+ d->factory = NULL;
+ if (d->module)
+ pw_impl_module_destroy(d->module);
+}
+
+static const struct pw_impl_factory_events factory_events = {
+ PW_VERSION_IMPL_FACTORY_EVENTS,
+ .destroy = factory_destroy,
+};
+
+static void module_destroy(void *data)
+{
+ struct factory_data *d = data;
+ spa_hook_remove(&d->module_listener);
+ d->module = NULL;
+ if (d->factory)
+ pw_impl_factory_destroy(d->factory);
+}
+
+static void module_registered(void *data)
+{
+ struct factory_data *d = data;
+ struct pw_impl_module *module = d->module;
+ struct pw_impl_factory *factory = d->factory;
+ struct spa_dict_item items[1];
+ char id[16];
+ int res;
+
+ snprintf(id, sizeof(id), "%d", pw_impl_module_get_info(module)->id);
+ items[0] = SPA_DICT_ITEM_INIT(PW_KEY_MODULE_ID, id);
+ pw_impl_factory_update_properties(factory, &SPA_DICT_INIT(items, 1));
+
+ if ((res = pw_impl_factory_register(factory, NULL)) < 0) {
+ pw_log_error(NAME" %p: can't register factory: %s", factory, spa_strerror(res));
+ }
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy,
+ .registered = module_registered,
+};
+
+int client_endpoint_factory_init(struct pw_impl_module *module)
+{
+ struct pw_context *context = pw_impl_module_get_context(module);
+ struct pw_impl_factory *factory;
+ struct factory_data *data;
+
+ factory = pw_context_create_factory(context,
+ "client-endpoint",
+ PW_TYPE_INTERFACE_ClientEndpoint,
+ PW_VERSION_CLIENT_ENDPOINT,
+ NULL,
+ sizeof(*data));
+ if (factory == NULL)
+ return -ENOMEM;
+
+ data = pw_impl_factory_get_user_data(factory);
+ data->factory = factory;
+ data->module = module;
+
+ pw_impl_factory_set_implementation(factory, &impl_factory, data);
+ pw_impl_factory_add_listener(factory, &data->factory_listener, &factory_events, data);
+
+ pw_impl_module_add_listener(module, &data->module_listener, &module_events, data);
+
+ return 0;
+}
diff --git a/src/modules/module-session-manager/client-endpoint/client-endpoint.h b/src/modules/module-session-manager/client-endpoint/client-endpoint.h
new file mode 100644
index 0000000..bc5630d
--- /dev/null
+++ b/src/modules/module-session-manager/client-endpoint/client-endpoint.h
@@ -0,0 +1,62 @@
+/* PipeWire
+ *
+ * Copyright © 2019 Collabora Ltd.
+ * @author George Kiagiadakis <george.kiagiadakis@collabora.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef MODULE_SESSION_MANAGER_CLIENT_ENDPOINT_H
+#define MODULE_SESSION_MANAGER_CLIENT_ENDPOINT_H
+
+#include "endpoint.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct client_endpoint {
+ struct pw_resource *resource;
+ struct spa_hook resource_listener;
+ struct spa_hook object_listener;
+ struct endpoint endpoint;
+ struct spa_list streams;
+};
+
+#define pw_client_endpoint_resource(r,m,v,...) \
+ pw_resource_call_res(r,struct pw_client_endpoint_events,m,v,__VA_ARGS__)
+#define pw_client_endpoint_resource_set_id(r,...) \
+ pw_client_endpoint_resource(r,set_id,0,__VA_ARGS__)
+#define pw_client_endpoint_resource_set_session_id(r,...) \
+ pw_client_endpoint_resource(r,set_session_id,0,__VA_ARGS__)
+#define pw_client_endpoint_resource_set_param(r,...) \
+ pw_client_endpoint_resource(r,set_param,0,__VA_ARGS__)
+#define pw_client_endpoint_resource_stream_set_param(r,...) \
+ pw_client_endpoint_resource(r,stream_set_param,0,__VA_ARGS__)
+#define pw_client_endpoint_resource_create_link(r,...) \
+ pw_client_endpoint_resource(r,create_link,0,__VA_ARGS__)
+
+int client_endpoint_factory_init(struct pw_impl_module *module);
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* MODULE_SESSION_MANAGER_CLIENT_ENDPOINT_H */
diff --git a/src/modules/module-session-manager/client-endpoint/endpoint-stream.c b/src/modules/module-session-manager/client-endpoint/endpoint-stream.c
new file mode 100644
index 0000000..8dde6f7
--- /dev/null
+++ b/src/modules/module-session-manager/client-endpoint/endpoint-stream.c
@@ -0,0 +1,352 @@
+/* PipeWire
+ *
+ * Copyright © 2019 Collabora Ltd.
+ * @author George Kiagiadakis <george.kiagiadakis@collabora.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdbool.h>
+#include <string.h>
+
+#include <pipewire/impl.h>
+#include <pipewire/extensions/session-manager.h>
+
+#include <spa/pod/filter.h>
+
+#include "endpoint-stream.h"
+#include "client-endpoint.h"
+
+#define NAME "endpoint-stream"
+
+struct resource_data {
+ struct endpoint_stream *stream;
+ struct spa_hook object_listener;
+ uint32_t n_subscribe_ids;
+ uint32_t subscribe_ids[32];
+};
+
+#define pw_endpoint_stream_resource(r,m,v,...) \
+ pw_resource_call(r,struct pw_endpoint_stream_events,m,v,__VA_ARGS__)
+#define pw_endpoint_stream_resource_info(r,...) \
+ pw_endpoint_stream_resource(r,info,0,__VA_ARGS__)
+#define pw_endpoint_stream_resource_param(r,...) \
+ pw_endpoint_stream_resource(r,param,0,__VA_ARGS__)
+
+static int endpoint_stream_enum_params (void *object, int seq,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+ struct pw_resource *resource = object;
+ struct resource_data *data = pw_resource_get_user_data(resource);
+ struct endpoint_stream *this = data->stream;
+ struct spa_pod *result;
+ struct spa_pod *param;
+ uint8_t buffer[1024];
+ struct spa_pod_builder b = { 0 };
+ uint32_t index;
+ uint32_t next = start;
+ uint32_t count = 0;
+
+ while (true) {
+ index = next++;
+ if (index >= this->n_params)
+ break;
+
+ param = this->params[index];
+
+ if (param == NULL || !spa_pod_is_object_id(param, id))
+ continue;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ if (spa_pod_filter(&b, &result, param, filter) != 0)
+ continue;
+
+ pw_log_debug(NAME" %p: %d param %u", this, seq, index);
+
+ pw_endpoint_stream_resource_param(resource, seq, id, index, next, result);
+
+ if (++count == num)
+ break;
+ }
+ return 0;
+}
+
+static int endpoint_stream_subscribe_params (void *object, uint32_t *ids, uint32_t n_ids)
+{
+ struct pw_resource *resource = object;
+ struct resource_data *data = pw_resource_get_user_data(resource);
+ uint32_t i;
+
+ n_ids = SPA_MIN(n_ids, SPA_N_ELEMENTS(data->subscribe_ids));
+ data->n_subscribe_ids = n_ids;
+
+ for (i = 0; i < n_ids; i++) {
+ data->subscribe_ids[i] = ids[i];
+ pw_log_debug(NAME" %p: resource %d subscribe param %u",
+ data->stream, pw_resource_get_id(resource), ids[i]);
+ endpoint_stream_enum_params(resource, 1, ids[i], 0, UINT32_MAX, NULL);
+ }
+ return 0;
+}
+
+static int endpoint_stream_set_param (void *object, uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct pw_resource *resource = object;
+ struct resource_data *data = pw_resource_get_user_data(resource);
+ struct endpoint_stream *this = data->stream;
+
+ pw_client_endpoint_resource_set_param(this->client_ep->resource,
+ id, flags, param);
+
+ return 0;
+}
+
+static const struct pw_endpoint_stream_methods methods = {
+ PW_VERSION_ENDPOINT_STREAM_METHODS,
+ .subscribe_params = endpoint_stream_subscribe_params,
+ .enum_params = endpoint_stream_enum_params,
+ .set_param = endpoint_stream_set_param,
+};
+
+struct emit_param_data {
+ struct endpoint_stream *this;
+ struct spa_pod *param;
+ uint32_t id;
+ uint32_t index;
+ uint32_t next;
+};
+
+static int emit_param(void *_data, struct pw_resource *resource)
+{
+ struct emit_param_data *d = _data;
+ struct resource_data *data;
+ uint32_t i;
+
+ data = pw_resource_get_user_data(resource);
+ for (i = 0; i < data->n_subscribe_ids; i++) {
+ if (data->subscribe_ids[i] == d->id) {
+ pw_endpoint_stream_resource_param(resource, 1,
+ d->id, d->index, d->next, d->param);
+ }
+ }
+ return 0;
+}
+
+static void endpoint_stream_notify_subscribed(struct endpoint_stream *this,
+ uint32_t index, uint32_t next)
+{
+ struct pw_global *global = this->global;
+ struct emit_param_data data;
+ struct spa_pod *param = this->params[index];
+
+ if (!param || !spa_pod_is_object (param))
+ return;
+
+ data.this = this;
+ data.param = param;
+ data.id = SPA_POD_OBJECT_ID (param);
+ data.index = index;
+ data.next = next;
+
+ pw_global_for_each_resource(global, emit_param, &data);
+}
+
+static int emit_info(void *data, struct pw_resource *resource)
+{
+ struct endpoint_stream *this = data;
+ pw_endpoint_stream_resource_info(resource, &this->info);
+ return 0;
+}
+
+int endpoint_stream_update(struct endpoint_stream *this,
+ uint32_t change_mask,
+ uint32_t n_params,
+ const struct spa_pod **params,
+ const struct pw_endpoint_stream_info *info)
+{
+ if (change_mask & PW_CLIENT_ENDPOINT_UPDATE_PARAMS) {
+ uint32_t i;
+
+ pw_log_debug(NAME" %p: update %d params", this, n_params);
+
+ for (i = 0; i < this->n_params; i++)
+ free(this->params[i]);
+ this->n_params = n_params;
+ if (this->n_params == 0) {
+ free(this->params);
+ this->params = NULL;
+ } else {
+ void *p;
+ p = pw_reallocarray(this->params, n_params, sizeof(struct spa_pod*));
+ if (p == NULL) {
+ free(this->params);
+ this->params = NULL;
+ this->n_params = 0;
+ goto no_mem;
+ }
+ this->params = p;
+ }
+ for (i = 0; i < this->n_params; i++) {
+ this->params[i] = params[i] ? spa_pod_copy(params[i]) : NULL;
+ endpoint_stream_notify_subscribed(this, i, i+1);
+ }
+ }
+
+ if (change_mask & PW_CLIENT_ENDPOINT_UPDATE_INFO) {
+ if (info->change_mask & PW_ENDPOINT_STREAM_CHANGE_MASK_LINK_PARAMS) {
+ free(this->info.link_params);
+ this->info.link_params = spa_pod_copy(info->link_params);
+ }
+
+ if (info->change_mask & PW_ENDPOINT_STREAM_CHANGE_MASK_PROPS)
+ pw_properties_update(this->props, info->props);
+
+ if (info->change_mask & PW_ENDPOINT_STREAM_CHANGE_MASK_PARAMS) {
+ this->info.n_params = info->n_params;
+ if (info->n_params == 0) {
+ free(this->info.params);
+ this->info.params = NULL;
+ } else {
+ void *p;
+ p = pw_reallocarray(this->info.params, info->n_params, sizeof(struct spa_param_info));
+ if (p == NULL) {
+ free(this->info.params);
+ this->info.params = NULL;
+ this->info.n_params = 0;
+ goto no_mem;
+ }
+ this->info.params = p;
+ memcpy(this->info.params, info->params, info->n_params * sizeof(struct spa_param_info));
+ }
+ }
+
+ if (!this->info.name)
+ this->info.name = info->name ? strdup(info->name) : NULL;
+
+ this->info.change_mask = info->change_mask;
+ pw_global_for_each_resource(this->global, emit_info, this);
+ this->info.change_mask = 0;
+ }
+
+ return 0;
+
+ no_mem:
+ pw_log_error(NAME" can't update: no memory");
+ pw_resource_error(this->client_ep->resource, -ENOMEM,
+ NAME" can't update: no memory");
+ return -ENOMEM;
+}
+
+static int endpoint_stream_bind(void *_data, struct pw_impl_client *client,
+ uint32_t permissions, uint32_t version, uint32_t id)
+{
+ struct endpoint_stream *this = _data;
+ struct pw_global *global = this->global;
+ struct pw_resource *resource;
+ struct resource_data *data;
+
+ resource = pw_resource_new(client, id, permissions,
+ pw_global_get_type(global), version, sizeof(*data));
+ if (resource == NULL)
+ goto no_mem;
+
+ data = pw_resource_get_user_data(resource);
+ data->stream = this;
+ pw_resource_add_object_listener(resource, &data->object_listener,
+ &methods, resource);
+
+ pw_log_debug(NAME" %p: bound to %d", this, pw_resource_get_id(resource));
+ pw_global_add_resource(global, resource);
+
+ this->info.change_mask = PW_ENDPOINT_STREAM_CHANGE_MASK_ALL;
+ pw_endpoint_stream_resource_info(resource, &this->info);
+ this->info.change_mask = 0;
+
+ return 0;
+
+ no_mem:
+ pw_log_error(NAME" can't create resource: no memory");
+ pw_resource_error(this->client_ep->resource, -ENOMEM,
+ NAME" can't create resource: no memory");
+ return -ENOMEM;
+}
+
+int endpoint_stream_init(struct endpoint_stream *this,
+ uint32_t id, uint32_t endpoint_id,
+ struct client_endpoint *client_ep,
+ struct pw_context *context,
+ struct pw_properties *properties)
+{
+ pw_log_debug(NAME" %p: new", this);
+
+ this->client_ep = client_ep;
+ this->id = id;
+ this->props = properties;
+
+ pw_properties_setf(properties, PW_KEY_ENDPOINT_ID, "%u", endpoint_id);
+
+ properties = pw_properties_copy(properties);
+ if (!properties)
+ goto no_mem;
+
+ this->global = pw_global_new (context,
+ PW_TYPE_INTERFACE_EndpointStream,
+ PW_VERSION_ENDPOINT_STREAM,
+ properties, endpoint_stream_bind, this);
+ if (!this->global)
+ goto no_mem;
+
+ pw_properties_setf(this->props, PW_KEY_OBJECT_ID, "%u",
+ pw_global_get_id(this->global));
+ pw_properties_setf(this->props, PW_KEY_OBJECT_SERIAL, "%"PRIu64,
+ pw_global_get_serial(this->global));
+
+ this->info.version = PW_VERSION_ENDPOINT_STREAM_INFO;
+ this->info.id = pw_global_get_id(this->global);
+ this->info.endpoint_id = endpoint_id;
+ this->info.props = &this->props->dict;
+
+ return pw_global_register(this->global);
+
+ no_mem:
+ pw_log_error(NAME" - can't create - out of memory");
+ return -ENOMEM;
+}
+
+void endpoint_stream_clear(struct endpoint_stream *this)
+{
+ uint32_t i;
+
+ pw_log_debug(NAME" %p: destroy", this);
+
+ pw_global_destroy(this->global);
+
+ for (i = 0; i < this->n_params; i++)
+ free(this->params[i]);
+ free(this->params);
+
+ free(this->info.name);
+ free(this->info.link_params);
+ free(this->info.params);
+
+ pw_properties_free(this->props);
+}
diff --git a/src/modules/module-session-manager/client-endpoint/endpoint-stream.h b/src/modules/module-session-manager/client-endpoint/endpoint-stream.h
new file mode 100644
index 0000000..7172287
--- /dev/null
+++ b/src/modules/module-session-manager/client-endpoint/endpoint-stream.h
@@ -0,0 +1,64 @@
+/* PipeWire
+ *
+ * Copyright © 2019 Collabora Ltd.
+ * @author George Kiagiadakis <george.kiagiadakis@collabora.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef MODULE_SESSION_MANAGER_ENDPOINT_STREAM_H
+#define MODULE_SESSION_MANAGER_ENDPOINT_STREAM_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct client_endpoint;
+
+struct endpoint_stream {
+ struct spa_list link;
+ struct client_endpoint *client_ep;
+ struct pw_global *global;
+ uint32_t id; /* endpoint-local stream id */
+ uint32_t n_params;
+ struct spa_pod **params;
+ struct pw_endpoint_stream_info info;
+ struct pw_properties *props; /* wrapper of info.props */
+};
+
+int endpoint_stream_init(struct endpoint_stream *this,
+ uint32_t id, uint32_t endpoint_id,
+ struct client_endpoint *client_ep,
+ struct pw_context *context,
+ struct pw_properties *properties);
+
+void endpoint_stream_clear(struct endpoint_stream *this);
+
+int endpoint_stream_update(struct endpoint_stream *this,
+ uint32_t change_mask,
+ uint32_t n_params,
+ const struct spa_pod **params,
+ const struct pw_endpoint_stream_info *info);
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* MODULE_SESSION_MANAGER_ENDPOINT_STREAM_H */
diff --git a/src/modules/module-session-manager/client-endpoint/endpoint.c b/src/modules/module-session-manager/client-endpoint/endpoint.c
new file mode 100644
index 0000000..aa13989
--- /dev/null
+++ b/src/modules/module-session-manager/client-endpoint/endpoint.c
@@ -0,0 +1,385 @@
+/* PipeWire
+ *
+ * Copyright © 2019 Collabora Ltd.
+ * @author George Kiagiadakis <george.kiagiadakis@collabora.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdbool.h>
+#include <string.h>
+
+#include <pipewire/impl.h>
+#include <pipewire/extensions/session-manager.h>
+
+#include <spa/pod/filter.h>
+
+#include "endpoint.h"
+#include "client-endpoint.h"
+
+#define NAME "endpoint"
+
+struct resource_data {
+ struct endpoint *endpoint;
+ struct spa_hook object_listener;
+ uint32_t n_subscribe_ids;
+ uint32_t subscribe_ids[32];
+};
+
+#define pw_endpoint_resource(r,m,v,...) \
+ pw_resource_call(r,struct pw_endpoint_events,m,v,__VA_ARGS__)
+#define pw_endpoint_resource_info(r,...) \
+ pw_endpoint_resource(r,info,0,__VA_ARGS__)
+#define pw_endpoint_resource_param(r,...) \
+ pw_endpoint_resource(r,param,0,__VA_ARGS__)
+
+static int endpoint_enum_params (void *object, int seq,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+ struct pw_resource *resource = object;
+ struct resource_data *data = pw_resource_get_user_data(resource);
+ struct endpoint *this = data->endpoint;
+ struct spa_pod *result;
+ struct spa_pod *param;
+ uint8_t buffer[1024];
+ struct spa_pod_builder b = { 0 };
+ uint32_t index;
+ uint32_t next = start;
+ uint32_t count = 0;
+
+ pw_log_debug(NAME" %p: param %u %d/%d", this, id, start, num);
+
+ while (true) {
+ index = next++;
+ if (index >= this->n_params)
+ break;
+
+ param = this->params[index];
+
+ if (param == NULL || !spa_pod_is_object_id(param, id))
+ continue;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ if (spa_pod_filter(&b, &result, param, filter) != 0)
+ continue;
+
+ pw_log_debug(NAME" %p: %d param %u", this, seq, index);
+
+ pw_endpoint_resource_param(resource, seq, id, index, next, result);
+
+ if (++count == num)
+ break;
+ }
+ return 0;
+}
+
+static int endpoint_subscribe_params (void *object, uint32_t *ids, uint32_t n_ids)
+{
+ struct pw_resource *resource = object;
+ struct resource_data *data = pw_resource_get_user_data(resource);
+ uint32_t i;
+
+ n_ids = SPA_MIN(n_ids, SPA_N_ELEMENTS(data->subscribe_ids));
+ data->n_subscribe_ids = n_ids;
+
+ for (i = 0; i < n_ids; i++) {
+ data->subscribe_ids[i] = ids[i];
+ pw_log_debug(NAME" %p: resource %d subscribe param %u",
+ data->endpoint, pw_resource_get_id(resource), ids[i]);
+ endpoint_enum_params(resource, 1, ids[i], 0, UINT32_MAX, NULL);
+ }
+ return 0;
+}
+
+static int endpoint_set_param (void *object, uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct pw_resource *resource = object;
+ struct resource_data *data = pw_resource_get_user_data(resource);
+ struct endpoint *this = data->endpoint;
+
+ pw_log_debug("%p", this);
+ pw_client_endpoint_resource_set_param(this->client_ep->resource,
+ id, flags, param);
+
+ return 0;
+}
+
+static int endpoint_create_link(void *object, const struct spa_dict *props)
+{
+ struct pw_resource *resource = object;
+ struct resource_data *data = pw_resource_get_user_data(resource);
+ struct endpoint *this = data->endpoint;
+
+ pw_log_debug("%p", this);
+ pw_client_endpoint_resource_create_link(this->client_ep->resource,
+ props);
+
+ return 0;
+}
+
+static const struct pw_endpoint_methods methods = {
+ PW_VERSION_ENDPOINT_METHODS,
+ .subscribe_params = endpoint_subscribe_params,
+ .enum_params = endpoint_enum_params,
+ .set_param = endpoint_set_param,
+ .create_link = endpoint_create_link,
+};
+
+struct emit_param_data {
+ struct endpoint *this;
+ struct spa_pod *param;
+ uint32_t id;
+ uint32_t index;
+ uint32_t next;
+};
+
+static int emit_param(void *_data, struct pw_resource *resource)
+{
+ struct emit_param_data *d = _data;
+ struct resource_data *data;
+ uint32_t i;
+
+ data = pw_resource_get_user_data(resource);
+ for (i = 0; i < data->n_subscribe_ids; i++) {
+ if (data->subscribe_ids[i] == d->id) {
+ pw_endpoint_resource_param(resource, 1,
+ d->id, d->index, d->next, d->param);
+ }
+ }
+ return 0;
+}
+
+static void endpoint_notify_subscribed(struct endpoint *this,
+ uint32_t index, uint32_t next)
+{
+ struct pw_global *global = this->global;
+ struct emit_param_data data;
+ struct spa_pod *param = this->params[index];
+
+ if (!param || !spa_pod_is_object (param))
+ return;
+
+ data.this = this;
+ data.param = param;
+ data.id = SPA_POD_OBJECT_ID (param);
+ data.index = index;
+ data.next = next;
+
+ pw_global_for_each_resource(global, emit_param, &data);
+}
+
+static int emit_info(void *data, struct pw_resource *resource)
+{
+ struct endpoint *this = data;
+ pw_endpoint_resource_info(resource, &this->info);
+ return 0;
+}
+
+int endpoint_update(struct endpoint *this,
+ uint32_t change_mask,
+ uint32_t n_params,
+ const struct spa_pod **params,
+ const struct pw_endpoint_info *info)
+{
+ if (change_mask & PW_CLIENT_ENDPOINT_UPDATE_PARAMS) {
+ uint32_t i;
+
+ pw_log_debug(NAME" %p: update %d params", this, n_params);
+
+ for (i = 0; i < this->n_params; i++)
+ free(this->params[i]);
+ this->n_params = n_params;
+ if (this->n_params == 0) {
+ free(this->params);
+ this->params = NULL;
+ } else {
+ void *p;
+ p = pw_reallocarray(this->params, n_params, sizeof(struct spa_pod*));
+ if (p == NULL) {
+ free(this->params);
+ this->params = NULL;
+ this->n_params = 0;
+ goto no_mem;
+ }
+ this->params = p;
+ }
+ for (i = 0; i < this->n_params; i++) {
+ this->params[i] = params[i] ? spa_pod_copy(params[i]) : NULL;
+ endpoint_notify_subscribed(this, i, i+1);
+ }
+ }
+
+ if (change_mask & PW_CLIENT_ENDPOINT_UPDATE_INFO) {
+ if (info->change_mask & PW_ENDPOINT_CHANGE_MASK_STREAMS)
+ this->info.n_streams = info->n_streams;
+
+ if (info->change_mask & PW_ENDPOINT_CHANGE_MASK_SESSION)
+ this->info.session_id = info->session_id;
+
+ if (info->change_mask & PW_ENDPOINT_CHANGE_MASK_PROPS)
+ pw_properties_update(this->props, info->props);
+
+ if (info->change_mask & PW_ENDPOINT_CHANGE_MASK_PARAMS) {
+ this->info.n_params = info->n_params;
+ if (info->n_params == 0) {
+ free(this->info.params);
+ this->info.params = NULL;
+ } else {
+ void *p;
+ p = pw_reallocarray(this->info.params, info->n_params, sizeof(struct spa_param_info));
+ if (p == NULL) {
+ free(this->info.params);
+ this->info.params = NULL;
+ this->info.n_params = 0;
+ goto no_mem;
+ }
+ this->info.params = p;
+ memcpy(this->info.params, info->params, info->n_params * sizeof(struct spa_param_info));
+ }
+ }
+
+ if (!this->info.name) {
+ this->info.name = info->name ? strdup(info->name) : NULL;
+ this->info.media_class = info->media_class ? strdup(info->media_class) : NULL;
+ this->info.direction = info->direction;
+ this->info.flags = info->flags;
+ }
+
+ this->info.change_mask = info->change_mask;
+ pw_global_for_each_resource(this->global, emit_info, this);
+ this->info.change_mask = 0;
+ }
+
+ return 0;
+
+ no_mem:
+ pw_log_error(NAME" can't update: no memory");
+ pw_resource_error(this->client_ep->resource, -ENOMEM,
+ NAME" can't update: no memory");
+ return -ENOMEM;
+}
+
+static int endpoint_bind(void *_data, struct pw_impl_client *client,
+ uint32_t permissions, uint32_t version, uint32_t id)
+{
+ struct endpoint *this = _data;
+ struct pw_global *global = this->global;
+ struct pw_resource *resource;
+ struct resource_data *data;
+
+ resource = pw_resource_new(client, id, permissions,
+ pw_global_get_type(global), version, sizeof(*data));
+ if (resource == NULL)
+ goto no_mem;
+
+ data = pw_resource_get_user_data(resource);
+ data->endpoint = this;
+ pw_resource_add_object_listener(resource, &data->object_listener,
+ &methods, resource);
+
+ pw_log_debug(NAME" %p: bound to %d", this, pw_resource_get_id(resource));
+ pw_global_add_resource(global, resource);
+
+ this->info.change_mask = PW_ENDPOINT_CHANGE_MASK_ALL;
+ pw_endpoint_resource_info(resource, &this->info);
+ this->info.change_mask = 0;
+
+ return 0;
+
+ no_mem:
+ pw_log_error(NAME" can't create resource: no memory");
+ pw_resource_error(this->client_ep->resource, -ENOMEM,
+ NAME" can't create resource: no memory");
+ return -ENOMEM;
+}
+
+int endpoint_init(struct endpoint *this,
+ struct client_endpoint *client_ep,
+ struct pw_context *context,
+ struct pw_properties *properties)
+{
+ static const char * const keys[] = {
+ PW_KEY_OBJECT_SERIAL,
+ PW_KEY_FACTORY_ID,
+ PW_KEY_CLIENT_ID,
+ PW_KEY_DEVICE_ID,
+ PW_KEY_NODE_ID,
+ PW_KEY_MEDIA_CLASS,
+ PW_KEY_SESSION_ID,
+ PW_KEY_PRIORITY_SESSION,
+ PW_KEY_ENDPOINT_NAME,
+ PW_KEY_ENDPOINT_CLIENT_ID,
+ PW_KEY_ENDPOINT_ICON_NAME,
+ PW_KEY_ENDPOINT_MONITOR,
+ NULL
+ };
+
+ pw_log_debug(NAME" %p: new", this);
+
+ this->client_ep = client_ep;
+ this->props = properties;
+
+ this->global = pw_global_new (context,
+ PW_TYPE_INTERFACE_Endpoint,
+ PW_VERSION_ENDPOINT,
+ NULL, endpoint_bind, this);
+ if (!this->global)
+ goto no_mem;
+
+ pw_properties_setf(this->props, PW_KEY_OBJECT_ID, "%u",
+ pw_global_get_id(this->global));
+ pw_properties_setf(this->props, PW_KEY_OBJECT_SERIAL, "%"PRIu64,
+ pw_global_get_serial(this->global));
+
+ this->info.version = PW_VERSION_ENDPOINT_INFO;
+ this->info.id = pw_global_get_id(this->global);
+ this->info.props = &this->props->dict;
+
+ pw_global_update_keys(this->global, &this->props->dict, keys);
+
+ pw_resource_set_bound_id(client_ep->resource, this->info.id);
+
+ return pw_global_register(this->global);
+
+ no_mem:
+ pw_log_error(NAME" - can't create - out of memory");
+ return -ENOMEM;
+}
+
+void endpoint_clear(struct endpoint *this)
+{
+ uint32_t i;
+
+ pw_log_debug(NAME" %p: destroy", this);
+
+ pw_global_destroy(this->global);
+
+ for (i = 0; i < this->n_params; i++)
+ free(this->params[i]);
+ free(this->params);
+
+ free(this->info.name);
+ free(this->info.media_class);
+ free(this->info.params);
+
+ pw_properties_free(this->props);
+}
diff --git a/src/modules/module-session-manager/client-endpoint/endpoint.h b/src/modules/module-session-manager/client-endpoint/endpoint.h
new file mode 100644
index 0000000..5ceff39
--- /dev/null
+++ b/src/modules/module-session-manager/client-endpoint/endpoint.h
@@ -0,0 +1,61 @@
+/* PipeWire
+ *
+ * Copyright © 2019 Collabora Ltd.
+ * @author George Kiagiadakis <george.kiagiadakis@collabora.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef MODULE_SESSION_MANAGER_ENDPOINT_H
+#define MODULE_SESSION_MANAGER_ENDPOINT_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct client_endpoint;
+
+struct endpoint {
+ struct client_endpoint *client_ep;
+ struct pw_global *global;
+ uint32_t n_params;
+ struct spa_pod **params;
+ struct pw_endpoint_info info;
+ struct pw_properties *props; /* wrapper of info.props */
+};
+
+int endpoint_init(struct endpoint *this,
+ struct client_endpoint *client_ep,
+ struct pw_context *context,
+ struct pw_properties *properties);
+
+void endpoint_clear(struct endpoint *this);
+
+int endpoint_update(struct endpoint *this,
+ uint32_t change_mask,
+ uint32_t n_params,
+ const struct spa_pod **params,
+ const struct pw_endpoint_info *info);
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* MODULE_SESSION_MANAGER_ENDPOINT_H */
diff --git a/src/modules/module-session-manager/client-session/client-session.c b/src/modules/module-session-manager/client-session/client-session.c
new file mode 100644
index 0000000..89997c9
--- /dev/null
+++ b/src/modules/module-session-manager/client-session/client-session.c
@@ -0,0 +1,295 @@
+/* PipeWire
+ *
+ * Copyright © 2019 Collabora Ltd.
+ * @author George Kiagiadakis <george.kiagiadakis@collabora.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdbool.h>
+#include <string.h>
+
+#include <spa/utils/result.h>
+
+#include <pipewire/impl.h>
+#include <pipewire/extensions/session-manager.h>
+
+#include "client-session.h"
+#include "session.h"
+#include "endpoint-link.h"
+
+#define NAME "client-session"
+
+struct factory_data {
+ struct pw_impl_module *module;
+ struct spa_hook module_listener;
+
+ struct pw_impl_factory *factory;
+ struct spa_hook factory_listener;
+};
+
+static struct endpoint_link *find_link(struct client_session *this, uint32_t id)
+{
+ struct endpoint_link *l;
+ spa_list_for_each(l, &this->links, link) {
+ if (l->id == id)
+ return l;
+ }
+ return NULL;
+}
+
+static int client_session_update(void *object,
+ uint32_t change_mask,
+ uint32_t n_params,
+ const struct spa_pod **params,
+ const struct pw_session_info *info)
+{
+ struct client_session *this = object;
+ struct session *session = &this->session;
+
+ return session_update(session, change_mask, n_params, params, info);
+}
+
+static int client_session_link_update(void *object,
+ uint32_t link_id,
+ uint32_t change_mask,
+ uint32_t n_params,
+ const struct spa_pod **params,
+ const struct pw_endpoint_link_info *info)
+{
+ struct client_session *this = object;
+ struct session *session = &this->session;
+ struct endpoint_link *link = find_link(this, link_id);
+ struct pw_properties *props = NULL;
+
+ if (!link) {
+ static const char * const keys[] = {
+ PW_KEY_FACTORY_ID,
+ PW_KEY_CLIENT_ID,
+ PW_KEY_SESSION_ID,
+ PW_KEY_ENDPOINT_LINK_OUTPUT_ENDPOINT,
+ PW_KEY_ENDPOINT_LINK_OUTPUT_STREAM,
+ PW_KEY_ENDPOINT_LINK_INPUT_ENDPOINT,
+ PW_KEY_ENDPOINT_LINK_INPUT_STREAM,
+ NULL
+ };
+
+ struct pw_context *context = pw_global_get_context(session->global);
+
+ link = calloc(1, sizeof(struct endpoint_link));
+ if (!link)
+ goto no_mem;
+
+ props = pw_properties_new(NULL, NULL);
+ if (!props)
+ goto no_mem;
+ pw_properties_update_keys(props, &session->props->dict, keys);
+ if (info && info->props)
+ pw_properties_update_keys(props, info->props, keys);
+
+ if (endpoint_link_init(link, link_id, session->info.id,
+ this, context, props) < 0)
+ goto no_mem;
+
+ spa_list_append(&this->links, &link->link);
+ }
+ else if (change_mask & PW_CLIENT_SESSION_LINK_UPDATE_DESTROYED) {
+ endpoint_link_clear(link);
+ spa_list_remove(&link->link);
+ free(link);
+ link = NULL;
+ }
+
+ return link ?
+ endpoint_link_update(link, change_mask, n_params, params, info)
+ : 0;
+
+ no_mem:
+ pw_properties_free(props);
+ free(link);
+ pw_log_error(NAME" %p: cannot update link: no memory", this);
+ pw_resource_error(this->resource, -ENOMEM,
+ "cannot update link: no memory");
+ return -ENOMEM;
+}
+
+static const struct pw_client_session_methods methods = {
+ PW_VERSION_CLIENT_SESSION_METHODS,
+ .update = client_session_update,
+ .link_update = client_session_link_update,
+};
+
+static void client_session_destroy(void *data)
+{
+ struct client_session *this = data;
+ struct endpoint_link *l;
+
+ pw_log_debug(NAME" %p: destroy", this);
+
+ spa_list_consume(l, &this->links, link) {
+ endpoint_link_clear(l);
+ spa_list_remove(&l->link);
+ free(l);
+ }
+ session_clear(&this->session);
+ spa_hook_remove(&this->resource_listener);
+
+ free(this);
+}
+
+static const struct pw_resource_events resource_events = {
+ PW_VERSION_RESOURCE_EVENTS,
+ .destroy = client_session_destroy,
+};
+
+static void *create_object(void *data,
+ struct pw_resource *owner_resource,
+ const char *type,
+ uint32_t version,
+ struct pw_properties *properties,
+ uint32_t new_id)
+{
+ struct factory_data *d = data;
+ struct pw_impl_factory *factory = d->factory;
+ struct client_session *this;
+ struct pw_impl_client *owner = pw_resource_get_client(owner_resource);
+ struct pw_context *context = pw_impl_client_get_context(owner);
+
+ this = calloc(1, sizeof(struct client_session));
+ if (this == NULL)
+ goto no_mem;
+
+ spa_list_init(&this->links);
+
+ pw_log_debug(NAME" %p: new", this);
+
+ if (!properties)
+ properties = pw_properties_new(NULL, NULL);
+ if (!properties)
+ goto no_mem;
+
+ pw_properties_setf(properties, PW_KEY_CLIENT_ID, "%d",
+ pw_impl_client_get_info(owner)->id);
+ pw_properties_setf(properties, PW_KEY_FACTORY_ID, "%d",
+ pw_impl_factory_get_info(factory)->id);
+
+ this->resource = pw_resource_new(owner, new_id, PW_PERM_ALL, type, version, 0);
+ if (this->resource == NULL)
+ goto no_mem;
+
+ if (session_init(&this->session, this, context, properties) < 0)
+ goto no_mem;
+
+ pw_resource_add_listener(this->resource, &this->resource_listener,
+ &resource_events, this);
+ pw_resource_add_object_listener(this->resource, &this->object_listener,
+ &methods, this);
+
+ return this;
+
+ no_mem:
+ pw_properties_free(properties);
+ if (this && this->resource)
+ pw_resource_destroy(this->resource);
+ free(this);
+ pw_log_error("can't create client session: no memory");
+ pw_resource_error(owner_resource, -ENOMEM,
+ "can't create client session: no memory");
+ return NULL;
+}
+
+static const struct pw_impl_factory_implementation impl_factory = {
+ PW_VERSION_IMPL_FACTORY_IMPLEMENTATION,
+ .create_object = create_object,
+};
+
+static void factory_destroy(void *data)
+{
+ struct factory_data *d = data;
+ spa_hook_remove(&d->factory_listener);
+ d->factory = NULL;
+ if (d->module)
+ pw_impl_module_destroy(d->module);
+}
+
+static const struct pw_impl_factory_events factory_events = {
+ PW_VERSION_IMPL_FACTORY_EVENTS,
+ .destroy = factory_destroy,
+};
+
+static void module_destroy(void *data)
+{
+ struct factory_data *d = data;
+ spa_hook_remove(&d->module_listener);
+ d->module = NULL;
+ if (d->factory)
+ pw_impl_factory_destroy(d->factory);
+}
+
+static void module_registered(void *data)
+{
+ struct factory_data *d = data;
+ struct pw_impl_module *module = d->module;
+ struct pw_impl_factory *factory = d->factory;
+ struct spa_dict_item items[1];
+ char id[16];
+ int res;
+
+ snprintf(id, sizeof(id), "%d", pw_impl_module_get_info(module)->id);
+ items[0] = SPA_DICT_ITEM_INIT(PW_KEY_MODULE_ID, id);
+ pw_impl_factory_update_properties(factory, &SPA_DICT_INIT(items, 1));
+
+ if ((res = pw_impl_factory_register(factory, NULL)) < 0) {
+ pw_log_error(NAME" %p: can't register factory: %s", factory, spa_strerror(res));
+ }
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy,
+ .registered = module_registered,
+};
+
+int client_session_factory_init(struct pw_impl_module *module)
+{
+ struct pw_context *context = pw_impl_module_get_context(module);
+ struct pw_impl_factory *factory;
+ struct factory_data *data;
+
+ factory = pw_context_create_factory(context,
+ "client-session",
+ PW_TYPE_INTERFACE_ClientSession,
+ PW_VERSION_CLIENT_SESSION,
+ NULL,
+ sizeof(*data));
+ if (factory == NULL)
+ return -ENOMEM;
+
+ data = pw_impl_factory_get_user_data(factory);
+ data->factory = factory;
+ data->module = module;
+
+ pw_impl_factory_set_implementation(factory, &impl_factory, data);
+ pw_impl_factory_add_listener(factory, &data->factory_listener, &factory_events, data);
+
+ pw_impl_module_add_listener(module, &data->module_listener, &module_events, data);
+
+ return 0;
+}
diff --git a/src/modules/module-session-manager/client-session/client-session.h b/src/modules/module-session-manager/client-session/client-session.h
new file mode 100644
index 0000000..fc6124c
--- /dev/null
+++ b/src/modules/module-session-manager/client-session/client-session.h
@@ -0,0 +1,62 @@
+/* PipeWire
+ *
+ * Copyright © 2019 Collabora Ltd.
+ * @author George Kiagiadakis <george.kiagiadakis@collabora.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef MODULE_SESSION_MANAGER_CLIENT_SESSION_H
+#define MODULE_SESSION_MANAGER_CLIENT_SESSION_H
+
+#include "session.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct client_session {
+ struct pw_resource *resource;
+ struct spa_hook resource_listener;
+ struct spa_hook object_listener;
+ struct session session;
+ struct spa_list links;
+};
+
+#define pw_client_session_resource(r,m,v,...) \
+ pw_resource_call_res(r,struct pw_client_session_events,m,v,__VA_ARGS__)
+#define pw_client_session_resource_set_id(r,...) \
+ pw_client_session_resource(r,set_id,0,__VA_ARGS__)
+#define pw_client_session_resource_set_param(r,...) \
+ pw_client_session_resource(r,set_param,0,__VA_ARGS__)
+#define pw_client_session_resource_link_set_param(r,...) \
+ pw_client_session_resource(r,link_set_param,0,__VA_ARGS__)
+#define pw_client_session_resource_create_link(r,...) \
+ pw_client_session_resource(r,create_link,0,__VA_ARGS__)
+#define pw_client_session_resource_destroy_link(r,...) \
+ pw_client_session_resource(r,destroy_link,0,__VA_ARGS__)
+#define pw_client_session_resource_link_request_state(r,...) \
+ pw_client_session_resource(r,link_request_state,0,__VA_ARGS__)
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* MODULE_SESSION_MANAGER_CLIENT_SESSION_H */
diff --git a/src/modules/module-session-manager/client-session/endpoint-link.c b/src/modules/module-session-manager/client-session/endpoint-link.c
new file mode 100644
index 0000000..0bdbfc9
--- /dev/null
+++ b/src/modules/module-session-manager/client-session/endpoint-link.c
@@ -0,0 +1,369 @@
+/* PipeWire
+ *
+ * Copyright © 2019 Collabora Ltd.
+ * @author George Kiagiadakis <george.kiagiadakis@collabora.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdbool.h>
+#include <string.h>
+
+#include <pipewire/impl.h>
+#include <pipewire/extensions/session-manager.h>
+
+#include <spa/pod/filter.h>
+
+#include "endpoint-link.h"
+#include "client-session.h"
+
+#define NAME "endpoint-link"
+
+struct resource_data {
+ struct endpoint_link *link;
+ struct spa_hook object_listener;
+ uint32_t n_subscribe_ids;
+ uint32_t subscribe_ids[32];
+};
+
+#define pw_endpoint_link_resource(r,m,v,...) \
+ pw_resource_call(r,struct pw_endpoint_link_events,m,v,__VA_ARGS__)
+#define pw_endpoint_link_resource_info(r,...) \
+ pw_endpoint_link_resource(r,info,0,__VA_ARGS__)
+#define pw_endpoint_link_resource_param(r,...) \
+ pw_endpoint_link_resource(r,param,0,__VA_ARGS__)
+
+static int endpoint_link_enum_params (void *object, int seq,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+ struct pw_resource *resource = object;
+ struct resource_data *data = pw_resource_get_user_data(resource);
+ struct endpoint_link *this = data->link;
+ struct spa_pod *result;
+ struct spa_pod *param;
+ uint8_t buffer[1024];
+ struct spa_pod_builder b = { 0 };
+ uint32_t index;
+ uint32_t next = start;
+ uint32_t count = 0;
+
+ while (true) {
+ index = next++;
+ if (index >= this->n_params)
+ break;
+
+ param = this->params[index];
+
+ if (param == NULL || !spa_pod_is_object_id(param, id))
+ continue;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ if (spa_pod_filter(&b, &result, param, filter) != 0)
+ continue;
+
+ pw_log_debug(NAME" %p: %d param %u", this, seq, index);
+
+ pw_endpoint_link_resource_param(resource, seq, id, index, next, result);
+
+ if (++count == num)
+ break;
+ }
+ return 0;
+}
+
+static int endpoint_link_subscribe_params (void *object, uint32_t *ids, uint32_t n_ids)
+{
+ struct pw_resource *resource = object;
+ struct resource_data *data = pw_resource_get_user_data(resource);
+ uint32_t i;
+
+ n_ids = SPA_MIN(n_ids, SPA_N_ELEMENTS(data->subscribe_ids));
+ data->n_subscribe_ids = n_ids;
+
+ for (i = 0; i < n_ids; i++) {
+ data->subscribe_ids[i] = ids[i];
+ pw_log_debug(NAME" %p: resource %d subscribe param %u",
+ data->link, pw_resource_get_id(resource), ids[i]);
+ endpoint_link_enum_params(resource, 1, ids[i], 0, UINT32_MAX, NULL);
+ }
+ return 0;
+}
+
+static int endpoint_link_set_param (void *object, uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct pw_resource *resource = object;
+ struct resource_data *data = pw_resource_get_user_data(resource);
+ struct endpoint_link *this = data->link;
+
+ pw_client_session_resource_set_param(this->client_sess->resource,
+ id, flags, param);
+
+ return 0;
+}
+
+static int endpoint_link_request_state(void *object, enum pw_endpoint_link_state state)
+{
+ struct pw_resource *resource = object;
+ struct resource_data *data = pw_resource_get_user_data(resource);
+ struct endpoint_link *this = data->link;
+
+ pw_client_session_resource_link_request_state(this->client_sess->resource,
+ this->id, state);
+
+ return 0;
+}
+
+static const struct pw_endpoint_link_methods methods = {
+ PW_VERSION_ENDPOINT_LINK_METHODS,
+ .subscribe_params = endpoint_link_subscribe_params,
+ .enum_params = endpoint_link_enum_params,
+ .set_param = endpoint_link_set_param,
+ .request_state = endpoint_link_request_state,
+};
+
+struct emit_param_data {
+ struct endpoint_link *this;
+ struct spa_pod *param;
+ uint32_t id;
+ uint32_t index;
+ uint32_t next;
+};
+
+static int emit_param(void *_data, struct pw_resource *resource)
+{
+ struct emit_param_data *d = _data;
+ struct resource_data *data;
+ uint32_t i;
+
+ data = pw_resource_get_user_data(resource);
+ for (i = 0; i < data->n_subscribe_ids; i++) {
+ if (data->subscribe_ids[i] == d->id) {
+ pw_endpoint_link_resource_param(resource, 1,
+ d->id, d->index, d->next, d->param);
+ }
+ }
+ return 0;
+}
+
+static void endpoint_link_notify_subscribed(struct endpoint_link *this,
+ uint32_t index, uint32_t next)
+{
+ struct pw_global *global = this->global;
+ struct emit_param_data data;
+ struct spa_pod *param = this->params[index];
+
+ if (!param || !spa_pod_is_object (param))
+ return;
+
+ data.this = this;
+ data.param = param;
+ data.id = SPA_POD_OBJECT_ID (param);
+ data.index = index;
+ data.next = next;
+
+ pw_global_for_each_resource(global, emit_param, &data);
+}
+
+static int emit_info(void *data, struct pw_resource *resource)
+{
+ struct endpoint_link *this = data;
+ pw_endpoint_link_resource_info(resource, &this->info);
+ return 0;
+}
+
+int endpoint_link_update(struct endpoint_link *this,
+ uint32_t change_mask,
+ uint32_t n_params,
+ const struct spa_pod **params,
+ const struct pw_endpoint_link_info *info)
+{
+ if (change_mask & PW_CLIENT_SESSION_UPDATE_PARAMS) {
+ uint32_t i;
+
+ pw_log_debug(NAME" %p: update %d params", this, n_params);
+
+ for (i = 0; i < this->n_params; i++)
+ free(this->params[i]);
+ this->n_params = n_params;
+ if (this->n_params == 0) {
+ free(this->params);
+ this->params = NULL;
+ } else {
+ void *p;
+ p = pw_reallocarray(this->params, n_params, sizeof(struct spa_pod*));
+ if (p == NULL) {
+ free(this->params);
+ this->params = NULL;
+ this->n_params = 0;
+ goto no_mem;
+ }
+ this->params = p;
+ }
+ for (i = 0; i < this->n_params; i++) {
+ this->params[i] = params[i] ? spa_pod_copy(params[i]) : NULL;
+ endpoint_link_notify_subscribed(this, i, i+1);
+ }
+ }
+
+ if (change_mask & PW_CLIENT_SESSION_UPDATE_INFO) {
+ if (info->change_mask & PW_ENDPOINT_LINK_CHANGE_MASK_STATE) {
+ this->info.state = info->state;
+ free(this->info.error);
+ this->info.error = info->error ? strdup(info->error) : NULL;
+ }
+
+ if (info->change_mask & PW_ENDPOINT_LINK_CHANGE_MASK_PROPS)
+ pw_properties_update(this->props, info->props);
+
+ if (info->change_mask & PW_ENDPOINT_LINK_CHANGE_MASK_PARAMS) {
+ this->info.n_params = info->n_params;
+ if (info->n_params == 0) {
+ free(this->info.params);
+ this->info.params = NULL;
+ } else {
+ void *p;
+ p = pw_reallocarray(this->info.params, info->n_params, sizeof(struct spa_param_info));
+ if (p == NULL) {
+ free(this->info.params);
+ this->info.params = NULL;
+ this->info.n_params = 0;
+ goto no_mem;
+ }
+ this->info.params = p;
+ memcpy(this->info.params, info->params, info->n_params * sizeof(struct spa_param_info));
+ }
+ }
+
+ if (!this->info.output_endpoint_id) {
+ this->info.output_endpoint_id = info->output_endpoint_id;
+ this->info.output_stream_id = info->output_stream_id;
+ this->info.input_endpoint_id = info->input_endpoint_id;
+ this->info.input_stream_id = info->input_stream_id;
+ }
+
+ this->info.change_mask = info->change_mask;
+ pw_global_for_each_resource(this->global, emit_info, this);
+ this->info.change_mask = 0;
+ }
+
+ return 0;
+
+ no_mem:
+ pw_log_error(NAME" %p: can't update: no memory", this);
+ pw_resource_error(this->client_sess->resource, -ENOMEM,
+ "can't update: no memory");
+ return -ENOMEM;
+}
+
+static int endpoint_link_bind(void *_data, struct pw_impl_client *client,
+ uint32_t permissions, uint32_t version, uint32_t id)
+{
+ struct endpoint_link *this = _data;
+ struct pw_global *global = this->global;
+ struct pw_resource *resource;
+ struct resource_data *data;
+
+ resource = pw_resource_new(client, id, permissions,
+ pw_global_get_type(global), version, sizeof(*data));
+ if (resource == NULL)
+ goto no_mem;
+
+ data = pw_resource_get_user_data(resource);
+ data->link = this;
+ pw_resource_add_object_listener(resource, &data->object_listener,
+ &methods, resource);
+
+ pw_log_debug(NAME" %p: bound to %d", this, pw_resource_get_id(resource));
+ pw_global_add_resource(global, resource);
+
+ this->info.change_mask = PW_ENDPOINT_LINK_CHANGE_MASK_ALL;
+ pw_endpoint_link_resource_info(resource, &this->info);
+ this->info.change_mask = 0;
+
+ return 0;
+
+ no_mem:
+ pw_log_error(NAME" %p: can't create resource: no memory", this);
+ pw_resource_error(this->client_sess->resource, -ENOMEM,
+ "can't create resource: no memory");
+ return -ENOMEM;
+}
+
+int endpoint_link_init(struct endpoint_link *this,
+ uint32_t id, uint32_t session_id,
+ struct client_session *client_sess,
+ struct pw_context *context,
+ struct pw_properties *properties)
+{
+ pw_log_debug(NAME" %p: new", this);
+
+ this->client_sess = client_sess;
+ this->id = id;
+ this->props = properties;
+
+ pw_properties_setf(properties, PW_KEY_SESSION_ID, "%u", session_id);
+
+ properties = pw_properties_copy(properties);
+ if (!properties)
+ goto no_mem;
+
+ this->global = pw_global_new(context,
+ PW_TYPE_INTERFACE_EndpointLink,
+ PW_VERSION_ENDPOINT_LINK,
+ properties, endpoint_link_bind, this);
+ if (!this->global)
+ goto no_mem;
+
+ pw_properties_setf(this->props, PW_KEY_OBJECT_ID, "%u",
+ pw_global_get_id(this->global));
+ pw_properties_setf(this->props, PW_KEY_OBJECT_SERIAL, "%"PRIu64,
+ pw_global_get_serial(this->global));
+
+ this->info.version = PW_VERSION_ENDPOINT_LINK_INFO;
+ this->info.id = pw_global_get_id(this->global);
+ this->info.session_id = session_id;
+ this->info.props = &this->props->dict;
+
+ return pw_global_register(this->global);
+
+ no_mem:
+ pw_log_error(NAME" - can't create - out of memory");
+ return -ENOMEM;
+}
+
+void endpoint_link_clear(struct endpoint_link *this)
+{
+ uint32_t i;
+
+ pw_log_debug(NAME" %p: destroy", this);
+
+ pw_global_destroy(this->global);
+
+ for (i = 0; i < this->n_params; i++)
+ free(this->params[i]);
+ free(this->params);
+
+ free(this->info.error);
+ free(this->info.params);
+
+ pw_properties_free(this->props);
+}
diff --git a/src/modules/module-session-manager/client-session/endpoint-link.h b/src/modules/module-session-manager/client-session/endpoint-link.h
new file mode 100644
index 0000000..c75f975
--- /dev/null
+++ b/src/modules/module-session-manager/client-session/endpoint-link.h
@@ -0,0 +1,65 @@
+/* PipeWire
+ *
+ * Copyright © 2019 Collabora Ltd.
+ * @author George Kiagiadakis <george.kiagiadakis@collabora.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef MODULE_SESSION_MANAGER_ENDPOINT_LINK_H
+#define MODULE_SESSION_MANAGER_ENDPOINT_LINK_H
+#include <stdint.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct client_session;
+
+struct endpoint_link {
+ struct spa_list link;
+ struct client_session *client_sess;
+ struct pw_global *global;
+ uint32_t id; /* session-local link id */
+ uint32_t n_params;
+ struct spa_pod **params;
+ struct pw_endpoint_link_info info;
+ struct pw_properties *props; /* wrapper of info.props */
+};
+
+int endpoint_link_init(struct endpoint_link *this,
+ uint32_t id, uint32_t session_id,
+ struct client_session *client_sess,
+ struct pw_context *context,
+ struct pw_properties *properties);
+
+void endpoint_link_clear(struct endpoint_link *this);
+
+int endpoint_link_update(struct endpoint_link *this,
+ uint32_t change_mask,
+ uint32_t n_params,
+ const struct spa_pod **params,
+ const struct pw_endpoint_link_info *info);
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* MODULE_SESSION_MANAGER_ENDPOINT_LINK_H */
diff --git a/src/modules/module-session-manager/client-session/session.c b/src/modules/module-session-manager/client-session/session.c
new file mode 100644
index 0000000..87c1b96
--- /dev/null
+++ b/src/modules/module-session-manager/client-session/session.c
@@ -0,0 +1,344 @@
+/* PipeWire
+ *
+ * Copyright © 2019 Collabora Ltd.
+ * @author George Kiagiadakis <george.kiagiadakis@collabora.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdbool.h>
+#include <string.h>
+
+#include <pipewire/impl.h>
+#include <pipewire/extensions/session-manager.h>
+
+#include <spa/pod/filter.h>
+
+#include "session.h"
+#include "client-session.h"
+
+#define NAME "session"
+
+struct resource_data {
+ struct session *session;
+ struct spa_hook object_listener;
+ uint32_t n_subscribe_ids;
+ uint32_t subscribe_ids[32];
+};
+
+#define pw_session_resource(r,m,v,...) \
+ pw_resource_call(r,struct pw_session_events,m,v,__VA_ARGS__)
+#define pw_session_resource_info(r,...) \
+ pw_session_resource(r,info,0,__VA_ARGS__)
+#define pw_session_resource_param(r,...) \
+ pw_session_resource(r,param,0,__VA_ARGS__)
+
+static int session_enum_params (void *object, int seq,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+ struct pw_resource *resource = object;
+ struct resource_data *data = pw_resource_get_user_data(resource);
+ struct session *this = data->session;
+ struct spa_pod *result;
+ struct spa_pod *param;
+ uint8_t buffer[1024];
+ struct spa_pod_builder b = { 0 };
+ uint32_t index;
+ uint32_t next = start;
+ uint32_t count = 0;
+
+ while (true) {
+ index = next++;
+ if (index >= this->n_params)
+ break;
+
+ param = this->params[index];
+
+ if (param == NULL || !spa_pod_is_object_id(param, id))
+ continue;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ if (spa_pod_filter(&b, &result, param, filter) != 0)
+ continue;
+
+ pw_log_debug(NAME" %p: %d param %u", this, seq, index);
+
+ pw_session_resource_param(resource, seq, id, index, next, result);
+
+ if (++count == num)
+ break;
+ }
+ return 0;
+}
+
+static int session_subscribe_params (void *object, uint32_t *ids, uint32_t n_ids)
+{
+ struct pw_resource *resource = object;
+ struct resource_data *data = pw_resource_get_user_data(resource);
+ uint32_t i;
+
+ n_ids = SPA_MIN(n_ids, SPA_N_ELEMENTS(data->subscribe_ids));
+ data->n_subscribe_ids = n_ids;
+
+ for (i = 0; i < n_ids; i++) {
+ data->subscribe_ids[i] = ids[i];
+ pw_log_debug(NAME" %p: resource %d subscribe param %u",
+ data->session, pw_resource_get_id(resource), ids[i]);
+ session_enum_params(resource, 1, ids[i], 0, UINT32_MAX, NULL);
+ }
+ return 0;
+}
+
+static int session_set_param (void *object, uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct pw_resource *resource = object;
+ struct resource_data *data = pw_resource_get_user_data(resource);
+ struct session *this = data->session;
+
+ pw_client_session_resource_set_param(this->client_sess->resource,
+ id, flags, param);
+
+ return 0;
+}
+
+static const struct pw_session_methods methods = {
+ PW_VERSION_SESSION_METHODS,
+ .subscribe_params = session_subscribe_params,
+ .enum_params = session_enum_params,
+ .set_param = session_set_param,
+};
+
+struct emit_param_data {
+ struct session *this;
+ struct spa_pod *param;
+ uint32_t id;
+ uint32_t index;
+ uint32_t next;
+};
+
+static int emit_param(void *_data, struct pw_resource *resource)
+{
+ struct emit_param_data *d = _data;
+ struct resource_data *data;
+ uint32_t i;
+
+ data = pw_resource_get_user_data(resource);
+ for (i = 0; i < data->n_subscribe_ids; i++) {
+ if (data->subscribe_ids[i] == d->id) {
+ pw_session_resource_param(resource, 1,
+ d->id, d->index, d->next, d->param);
+ }
+ }
+ return 0;
+}
+
+static void session_notify_subscribed(struct session *this,
+ uint32_t index, uint32_t next)
+{
+ struct pw_global *global = this->global;
+ struct emit_param_data data;
+ struct spa_pod *param = this->params[index];
+
+ if (!param || !spa_pod_is_object (param))
+ return;
+
+ data.this = this;
+ data.param = param;
+ data.id = SPA_POD_OBJECT_ID (param);
+ data.index = index;
+ data.next = next;
+
+ pw_global_for_each_resource(global, emit_param, &data);
+}
+
+static int emit_info(void *data, struct pw_resource *resource)
+{
+ struct session *this = data;
+ pw_session_resource_info(resource, &this->info);
+ return 0;
+}
+
+int session_update(struct session *this,
+ uint32_t change_mask,
+ uint32_t n_params,
+ const struct spa_pod **params,
+ const struct pw_session_info *info)
+{
+ if (change_mask & PW_CLIENT_SESSION_UPDATE_PARAMS) {
+ uint32_t i;
+
+ pw_log_debug(NAME" %p: update %d params", this, n_params);
+
+ for (i = 0; i < this->n_params; i++)
+ free(this->params[i]);
+ this->n_params = n_params;
+ if (this->n_params == 0) {
+ free(this->params);
+ this->params = NULL;
+ } else {
+ void *p;
+ p = pw_reallocarray(this->params, n_params, sizeof(struct spa_pod*));
+ if (p == NULL) {
+ free(this->params);
+ this->params = NULL;
+ this->n_params = 0;
+ goto no_mem;
+ }
+ this->params = p;
+ }
+ for (i = 0; i < this->n_params; i++) {
+ this->params[i] = params[i] ? spa_pod_copy(params[i]) : NULL;
+ session_notify_subscribed(this, i, i+1);
+ }
+ }
+
+ if (change_mask & PW_CLIENT_SESSION_UPDATE_INFO) {
+ if (info->change_mask & PW_SESSION_CHANGE_MASK_PROPS)
+ pw_properties_update(this->props, info->props);
+
+ if (info->change_mask & PW_SESSION_CHANGE_MASK_PARAMS) {
+ this->info.n_params = info->n_params;
+ if (info->n_params == 0) {
+ free(this->info.params);
+ this->info.params = NULL;
+ } else {
+ void *p;
+ p = pw_reallocarray(this->info.params, info->n_params, sizeof(struct spa_param_info));
+ if (p == NULL) {
+ free(this->info.params);
+ this->info.params = NULL;
+ this->info.n_params = 0;
+ goto no_mem;
+ }
+ this->info.params = p;
+ memcpy(this->info.params, info->params, info->n_params * sizeof(struct spa_param_info));
+ }
+ }
+ this->info.change_mask = info->change_mask;
+ pw_global_for_each_resource(this->global, emit_info, this);
+ this->info.change_mask = 0;
+ }
+
+ return 0;
+
+ no_mem:
+ pw_log_error(NAME" can't update: no memory");
+ pw_resource_error(this->client_sess->resource, -ENOMEM,
+ NAME" can't update: no memory");
+ return -ENOMEM;
+}
+
+static int session_bind(void *_data, struct pw_impl_client *client,
+ uint32_t permissions, uint32_t version, uint32_t id)
+{
+ struct session *this = _data;
+ struct pw_global *global = this->global;
+ struct pw_resource *resource;
+ struct resource_data *data;
+
+ resource = pw_resource_new(client, id, permissions,
+ pw_global_get_type(global), version, sizeof(*data));
+ if (resource == NULL)
+ goto no_mem;
+
+ data = pw_resource_get_user_data(resource);
+ data->session = this;
+
+ pw_resource_add_object_listener(resource, &data->object_listener,
+ &methods, resource);
+
+ pw_log_debug(NAME" %p: bound to %d", this, pw_resource_get_id(resource));
+ pw_global_add_resource(global, resource);
+
+ this->info.change_mask = PW_SESSION_CHANGE_MASK_ALL;
+ pw_session_resource_info(resource, &this->info);
+ this->info.change_mask = 0;
+
+ return 0;
+
+ no_mem:
+ pw_log_error(NAME" can't create resource: no memory");
+ pw_resource_error(this->client_sess->resource, -ENOMEM,
+ NAME" can't create resource: no memory");
+ return -ENOMEM;
+}
+
+int session_init(struct session *this,
+ struct client_session *client_sess,
+ struct pw_context *context,
+ struct pw_properties *properties)
+{
+ static const char * const keys[] = {
+ PW_KEY_OBJECT_SERIAL,
+ PW_KEY_FACTORY_ID,
+ PW_KEY_CLIENT_ID,
+ NULL
+ };
+
+ pw_log_debug(NAME" %p: new", this);
+
+ this->client_sess = client_sess;
+ this->props = properties;
+
+ this->global = pw_global_new (context,
+ PW_TYPE_INTERFACE_Session,
+ PW_VERSION_SESSION,
+ NULL, session_bind, this);
+ if (!this->global)
+ goto no_mem;
+
+ pw_properties_setf(this->props, PW_KEY_OBJECT_ID, "%u",
+ pw_global_get_id(this->global));
+ pw_properties_setf(this->props, PW_KEY_OBJECT_SERIAL, "%"PRIu64,
+ pw_global_get_serial(this->global));
+
+ this->info.version = PW_VERSION_SESSION_INFO;
+ this->info.id = pw_global_get_id(this->global);
+ this->info.props = &this->props->dict;
+
+ pw_global_update_keys(this->global, &this->props->dict, keys);
+
+ pw_resource_set_bound_id(client_sess->resource, this->info.id);
+
+ return pw_global_register(this->global);
+
+ no_mem:
+ pw_log_error(NAME" - can't create - out of memory");
+ return -ENOMEM;
+}
+
+void session_clear(struct session *this)
+{
+ uint32_t i;
+
+ pw_log_debug(NAME" %p: destroy", this);
+
+ pw_global_destroy(this->global);
+
+ for (i = 0; i < this->n_params; i++)
+ free(this->params[i]);
+ free(this->params);
+
+ free(this->info.params);
+
+ pw_properties_free(this->props);
+}
diff --git a/src/modules/module-session-manager/client-session/session.h b/src/modules/module-session-manager/client-session/session.h
new file mode 100644
index 0000000..a94b18f
--- /dev/null
+++ b/src/modules/module-session-manager/client-session/session.h
@@ -0,0 +1,62 @@
+/* PipeWire
+ *
+ * Copyright © 2019 Collabora Ltd.
+ * @author George Kiagiadakis <george.kiagiadakis@collabora.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef MODULE_SESSION_MANAGER_SESSION_H
+#define MODULE_SESSION_MANAGER_SESSION_H
+#include <stdint.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct client_session;
+
+struct session {
+ struct client_session *client_sess;
+ struct pw_global *global;
+ uint32_t n_params;
+ struct spa_pod **params;
+ struct pw_session_info info;
+ struct pw_properties *props; /* wrapper of info.props */
+};
+
+int session_init(struct session *this,
+ struct client_session *client_sess,
+ struct pw_context *context,
+ struct pw_properties *properties);
+
+void session_clear(struct session *this);
+
+int session_update(struct session *this,
+ uint32_t change_mask,
+ uint32_t n_params,
+ const struct spa_pod **params,
+ const struct pw_session_info *info);
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* MODULE_SESSION_MANAGER_SESSION_H */
diff --git a/src/modules/module-session-manager/endpoint-link.c b/src/modules/module-session-manager/endpoint-link.c
new file mode 100644
index 0000000..55ff580
--- /dev/null
+++ b/src/modules/module-session-manager/endpoint-link.c
@@ -0,0 +1,590 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Collabora Ltd.
+ * @author George Kiagiadakis <george.kiagiadakis@collabora.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <pipewire/impl.h>
+#include <pipewire/extensions/session-manager.h>
+#include <pipewire/extensions/session-manager/introspect-funcs.h>
+
+#include <spa/utils/result.h>
+#include <spa/pod/builder.h>
+#include <spa/pod/filter.h>
+
+#define MAX_PARAMS 32
+
+#define NAME "endpoint-link"
+
+struct pw_proxy *pw_core_endpoint_link_export(struct pw_core *core,
+ const char *type, const struct spa_dict *props, void *object,
+ size_t user_data_size);
+
+struct impl
+{
+ struct pw_global *global;
+ struct spa_hook global_listener;
+
+ union {
+ struct pw_endpoint_link *link;
+ struct pw_resource *resource;
+ };
+ struct spa_hook resource_listener;
+ struct spa_hook link_listener;
+
+ struct pw_endpoint_link_info *cached_info;
+ struct spa_list cached_params;
+
+ int ping_seq;
+ bool registered;
+};
+
+struct param_data
+{
+ struct spa_list link;
+ uint32_t id;
+ struct pw_array params;
+};
+
+struct resource_data
+{
+ struct impl *impl;
+
+ struct pw_resource *resource;
+ struct spa_hook object_listener;
+
+ uint32_t n_subscribe_ids;
+ uint32_t subscribe_ids[32];
+};
+
+struct factory_data
+{
+ struct pw_impl_factory *factory;
+ struct spa_hook factory_listener;
+
+ struct pw_impl_module *module;
+ struct spa_hook module_listener;
+
+ struct pw_export_type export;
+};
+
+#define pw_endpoint_link_resource(r,m,v,...) \
+ pw_resource_call(r,struct pw_endpoint_link_events,m,v,__VA_ARGS__)
+
+#define pw_endpoint_link_resource_info(r,...) \
+ pw_endpoint_link_resource(r,info,0,__VA_ARGS__)
+#define pw_endpoint_link_resource_param(r,...) \
+ pw_endpoint_link_resource(r,param,0,__VA_ARGS__)
+
+static int method_enum_params(void *object, int seq,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+ struct resource_data *d = object;
+ struct impl *impl = d->impl;
+ struct param_data *pdata;
+ struct spa_pod *result;
+ struct spa_pod *param;
+ uint8_t buffer[1024];
+ struct spa_pod_builder b = { 0 };
+ uint32_t index;
+ uint32_t next = start;
+ uint32_t count = 0;
+
+ pw_log_debug(NAME" %p: param %u %d/%d", impl, id, start, num);
+
+ spa_list_for_each(pdata, &impl->cached_params, link) {
+ if (pdata->id != id)
+ continue;
+
+ while (true) {
+ index = next++;
+ if (index >= pw_array_get_len(&pdata->params, void*))
+ return 0;
+
+ param = *pw_array_get_unchecked(&pdata->params, index, struct spa_pod*);
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ if (spa_pod_filter(&b, &result, param, filter) != 0)
+ continue;
+
+ pw_log_debug(NAME" %p: %d param %u", impl, seq, index);
+
+ pw_endpoint_link_resource_param(d->resource, seq, id, index, next, result);
+
+ if (++count == num)
+ return 0;
+ }
+ }
+
+ return 0;
+}
+
+static int method_subscribe_params(void *object, uint32_t *ids, uint32_t n_ids)
+{
+ struct resource_data *d = object;
+ struct impl *impl = d->impl;
+ uint32_t i;
+
+ n_ids = SPA_MIN(n_ids, SPA_N_ELEMENTS(d->subscribe_ids));
+ d->n_subscribe_ids = n_ids;
+
+ for (i = 0; i < n_ids; i++) {
+ d->subscribe_ids[i] = ids[i];
+ pw_log_debug(NAME" %p: resource %d subscribe param %u",
+ impl, pw_resource_get_id(d->resource), ids[i]);
+ method_enum_params(object, 1, ids[i], 0, UINT32_MAX, NULL);
+ }
+ return 0;
+}
+
+static int method_set_param(void *object, uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct resource_data *d = object;
+ struct impl *impl = d->impl;
+ /* store only on the implementation; our cache will be updated
+ by the param event, since we are subscribed */
+ pw_endpoint_link_set_param(impl->link, id, flags, param);
+ return 0;
+}
+
+static int method_request_state(void *object, enum pw_endpoint_link_state state)
+{
+ struct resource_data *d = object;
+ struct impl *impl = d->impl;
+ pw_endpoint_link_request_state(impl->link, state);
+ return 0;
+}
+
+static const struct pw_endpoint_link_methods link_methods = {
+ PW_VERSION_ENDPOINT_LINK_METHODS,
+ .subscribe_params = method_subscribe_params,
+ .enum_params = method_enum_params,
+ .set_param = method_set_param,
+ .request_state = method_request_state,
+};
+
+static int global_bind(void *object, struct pw_impl_client *client,
+ uint32_t permissions, uint32_t version, uint32_t id)
+{
+ struct impl *impl = object;
+ struct pw_resource *resource;
+ struct resource_data *data;
+
+ resource = pw_resource_new(client, id, permissions,
+ PW_TYPE_INTERFACE_EndpointLink,
+ version, sizeof(*data));
+ if (resource == NULL)
+ return -errno;
+
+ data = pw_resource_get_user_data(resource);
+ data->impl = impl;
+ data->resource = resource;
+
+ pw_global_add_resource(impl->global, resource);
+
+ /* resource methods -> implementation */
+ pw_resource_add_object_listener(resource,
+ &data->object_listener,
+ &link_methods, data);
+
+ impl->cached_info->change_mask = PW_ENDPOINT_LINK_CHANGE_MASK_ALL;
+ pw_endpoint_link_resource_info(resource, impl->cached_info);
+ impl->cached_info->change_mask = 0;
+
+ return 0;
+}
+
+static void global_destroy(void *data)
+{
+ struct impl *impl = data;
+ spa_hook_remove(&impl->global_listener);
+ impl->global = NULL;
+ if (impl->resource)
+ pw_resource_destroy(impl->resource);
+ free(impl);
+}
+
+static const struct pw_global_events global_events = {
+ PW_VERSION_GLOBAL_EVENTS,
+ .destroy = global_destroy,
+};
+
+static void impl_resource_destroy(void *data)
+{
+ struct impl *impl = data;
+ struct param_data *pdata, *tmp;
+
+ spa_hook_remove(&impl->resource_listener);
+ spa_hook_remove(&impl->link_listener);
+ impl->resource = NULL;
+
+ /* clear cache */
+ if (impl->cached_info)
+ pw_endpoint_link_info_free(impl->cached_info);
+ spa_list_for_each_safe(pdata, tmp, &impl->cached_params, link) {
+ struct spa_pod **pod;
+ pw_array_for_each(pod, &pdata->params)
+ free(*pod);
+ pw_array_clear(&pdata->params);
+ spa_list_remove(&pdata->link);
+ free(pdata);
+ }
+
+ if (impl->global)
+ pw_global_destroy(impl->global);
+}
+
+static void register_global(struct impl *impl)
+{
+ impl->cached_info->id = pw_global_get_id (impl->global);
+ pw_resource_set_bound_id(impl->resource, impl->cached_info->id);
+ pw_global_register(impl->global);
+ impl->registered = true;
+}
+
+static void impl_resource_pong (void *data, int seq)
+{
+ struct impl *impl = data;
+
+ /* complete registration, if this was the initial sync */
+ if (!impl->registered && seq == impl->ping_seq) {
+ register_global(impl);
+ }
+}
+
+static const struct pw_resource_events impl_resource_events = {
+ PW_VERSION_RESOURCE_EVENTS,
+ .destroy = impl_resource_destroy,
+ .pong = impl_resource_pong,
+};
+
+static int emit_info(void *data, struct pw_resource *resource)
+{
+ const struct pw_endpoint_link_info *info = data;
+ pw_endpoint_link_resource_info(resource, info);
+ return 0;
+}
+
+static void event_info(void *data, const struct pw_endpoint_link_info *info)
+{
+ struct impl *impl = data;
+ uint32_t changed_ids[MAX_PARAMS], n_changed_ids = 0;
+ uint32_t i;
+
+ /* figure out changes to params */
+ if (info->change_mask & PW_ENDPOINT_LINK_CHANGE_MASK_PARAMS) {
+ for (i = 0; i < info->n_params; i++) {
+ if ((!impl->cached_info ||
+ info->params[i].flags != impl->cached_info->params[i].flags)
+ && info->params[i].flags & SPA_PARAM_INFO_READ)
+ changed_ids[n_changed_ids++] = info->params[i].id;
+ }
+ }
+
+ /* cache for new clients */
+ impl->cached_info = pw_endpoint_link_info_update (impl->cached_info, info);
+
+ /* notify existing clients */
+ pw_global_for_each_resource(impl->global, emit_info, (void*) info);
+
+ /* cache params & register */
+ if (n_changed_ids > 0) {
+ /* prepare params storage */
+ for (i = 0; i < n_changed_ids; i++) {
+ struct param_data *pdata = calloc(1, sizeof(struct param_data));
+ pdata->id = changed_ids[i];
+ pw_array_init(&pdata->params, sizeof(void*));
+ spa_list_append(&impl->cached_params, &pdata->link);
+ }
+
+ /* subscribe to impl */
+ pw_endpoint_link_subscribe_params(impl->link, changed_ids, n_changed_ids);
+
+ /* register asynchronously on the pong event */
+ impl->ping_seq = pw_resource_ping(impl->resource, 0);
+ }
+ else if (!impl->registered) {
+ register_global(impl);
+ }
+}
+
+struct param_event_args
+{
+ uint32_t id, index, next;
+ const struct spa_pod *param;
+};
+
+static int emit_param(void *_data, struct pw_resource *resource)
+{
+ struct param_event_args *args = _data;
+ struct resource_data *data;
+ uint32_t i;
+
+ data = pw_resource_get_user_data(resource);
+ for (i = 0; i < data->n_subscribe_ids; i++) {
+ if (data->subscribe_ids[i] == args->id) {
+ pw_endpoint_link_resource_param(resource, 1,
+ args->id, args->index, args->next, args->param);
+ }
+ }
+ return 0;
+}
+
+static void event_param(void *data, int seq,
+ uint32_t id, uint32_t index, uint32_t next,
+ const struct spa_pod *param)
+{
+ struct impl *impl = data;
+ struct param_data *pdata;
+ struct spa_pod **pod;
+ struct param_event_args args = { id, index, next, param };
+
+ /* cache for new requests */
+ spa_list_for_each(pdata, &impl->cached_params, link) {
+ if (pdata->id != id)
+ continue;
+
+ if (!pw_array_check_index(&pdata->params, index, void*)) {
+ while (pw_array_get_len(&pdata->params, void*) <= index)
+ pw_array_add_ptr(&pdata->params, NULL);
+ }
+
+ pod = pw_array_get_unchecked(&pdata->params, index, struct spa_pod*);
+ free(*pod);
+ *pod = spa_pod_copy(param);
+ }
+
+ /* notify existing clients */
+ pw_global_for_each_resource(impl->global, emit_param, &args);
+}
+
+static const struct pw_endpoint_link_events link_events = {
+ PW_VERSION_ENDPOINT_LINK_EVENTS,
+ .info = event_info,
+ .param = event_param,
+};
+
+static void *link_new(struct pw_context *context,
+ struct pw_resource *resource,
+ struct pw_properties *properties)
+{
+ struct impl *impl;
+ char serial_str[32];
+ struct spa_dict_item items[1] = {
+ SPA_DICT_ITEM_INIT(PW_KEY_OBJECT_SERIAL, serial_str),
+ };
+ struct spa_dict extra_props = SPA_DICT_INIT_ARRAY(items);
+ static const char * const keys[] = {
+ PW_KEY_OBJECT_SERIAL,
+ NULL
+ };
+
+ impl = calloc(1, sizeof(*impl));
+ if (impl == NULL) {
+ pw_properties_free(properties);
+ return NULL;
+ }
+
+ impl->global = pw_global_new(context,
+ PW_TYPE_INTERFACE_EndpointLink,
+ PW_VERSION_ENDPOINT_LINK,
+ properties,
+ global_bind, impl);
+ if (impl->global == NULL) {
+ free(impl);
+ return NULL;
+ }
+ impl->resource = resource;
+
+ spa_scnprintf(serial_str, sizeof(serial_str), "%"PRIu64,
+ pw_global_get_serial(impl->global));
+ pw_global_update_keys(impl->global, &extra_props, keys);
+
+ spa_list_init(&impl->cached_params);
+
+ /* handle destroy events */
+ pw_global_add_listener(impl->global,
+ &impl->global_listener,
+ &global_events, impl);
+ pw_resource_add_listener(impl->resource,
+ &impl->resource_listener,
+ &impl_resource_events, impl);
+
+ /* handle implementation events -> cache + client resources */
+ pw_endpoint_link_add_listener(impl->link,
+ &impl->link_listener,
+ &link_events, impl);
+
+ /* global is not registered here on purpose;
+ we first cache info + params and then expose the global */
+
+ return impl;
+}
+
+static void *create_object(void *data,
+ struct pw_resource *resource,
+ const char *type,
+ uint32_t version,
+ struct pw_properties *properties,
+ uint32_t new_id)
+{
+ struct factory_data *d = data;
+ struct pw_resource *impl_resource;
+ struct pw_impl_client *client = pw_resource_get_client(resource);
+ void *result;
+ int res;
+
+ impl_resource = pw_resource_new(client, new_id, PW_PERM_ALL, type, version, 0);
+ if (impl_resource == NULL) {
+ res = -errno;
+ goto error_resource;
+ }
+
+ pw_resource_install_marshal(impl_resource, true);
+
+ if (properties == NULL)
+ properties = pw_properties_new(NULL, NULL);
+ if (properties == NULL) {
+ res = -ENOMEM;
+ goto error_link;
+ }
+
+ pw_properties_setf(properties, PW_KEY_CLIENT_ID, "%d",
+ pw_impl_client_get_info(client)->id);
+ pw_properties_setf(properties, PW_KEY_FACTORY_ID, "%d",
+ pw_impl_factory_get_info(d->factory)->id);
+
+ result = link_new(pw_impl_client_get_context(client), impl_resource, properties);
+ if (result == NULL) {
+ res = -errno;
+ goto error_link;
+ }
+ return result;
+
+error_resource:
+ pw_log_error("can't create resource: %s", spa_strerror(res));
+ pw_resource_errorf_id(resource, new_id, res, "can't create resource: %s", spa_strerror(res));
+ goto error_exit;
+error_link:
+ pw_log_error("can't create endpoint link: %s", spa_strerror(res));
+ pw_resource_errorf_id(resource, new_id, res, "can't create endpoint link: %s", spa_strerror(res));
+ goto error_exit_free;
+
+error_exit_free:
+ pw_resource_remove(impl_resource);
+error_exit:
+ errno = -res;
+ return NULL;
+}
+
+static const struct pw_impl_factory_implementation impl_factory = {
+ PW_VERSION_IMPL_FACTORY_IMPLEMENTATION,
+ .create_object = create_object,
+};
+
+static void factory_destroy(void *data)
+{
+ struct factory_data *d = data;
+ spa_hook_remove(&d->factory_listener);
+ d->factory = NULL;
+ if (d->module)
+ pw_impl_module_destroy(d->module);
+}
+
+static const struct pw_impl_factory_events factory_events = {
+ PW_VERSION_IMPL_FACTORY_EVENTS,
+ .destroy = factory_destroy,
+};
+
+static void module_destroy(void *data)
+{
+ struct factory_data *d = data;
+
+ spa_hook_remove(&d->module_listener);
+ spa_list_remove(&d->export.link);
+ d->module = NULL;
+
+ if (d->factory)
+ pw_impl_factory_destroy(d->factory);
+}
+
+static void module_registered(void *data)
+{
+ struct factory_data *d = data;
+ struct pw_impl_module *module = d->module;
+ struct pw_impl_factory *factory = d->factory;
+ struct spa_dict_item items[1];
+ char id[16];
+ int res;
+
+ snprintf(id, sizeof(id), "%d", pw_impl_module_get_info(module)->id);
+ items[0] = SPA_DICT_ITEM_INIT(PW_KEY_MODULE_ID, id);
+ pw_impl_factory_update_properties(factory, &SPA_DICT_INIT(items, 1));
+
+ if ((res = pw_impl_factory_register(factory, NULL)) < 0) {
+ pw_log_error(NAME" %p: can't register factory: %s", factory, spa_strerror(res));
+ }
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy,
+ .registered = module_registered,
+};
+
+int endpoint_link_factory_init(struct pw_impl_module *module)
+{
+ struct pw_context *context = pw_impl_module_get_context(module);
+ struct pw_impl_factory *factory;
+ struct factory_data *data;
+ int res;
+
+ factory = pw_context_create_factory(context,
+ "endpoint-link",
+ PW_TYPE_INTERFACE_EndpointLink,
+ PW_VERSION_ENDPOINT_LINK,
+ NULL,
+ sizeof(*data));
+ if (factory == NULL)
+ return -errno;
+
+ data = pw_impl_factory_get_user_data(factory);
+ data->factory = factory;
+ data->module = module;
+
+ pw_impl_factory_set_implementation(factory, &impl_factory, data);
+
+ data->export.type = PW_TYPE_INTERFACE_EndpointLink;
+ data->export.func = pw_core_endpoint_link_export;
+ if ((res = pw_context_register_export_type(context, &data->export)) < 0)
+ goto error;
+
+ pw_impl_factory_add_listener(factory, &data->factory_listener, &factory_events, data);
+ pw_impl_module_add_listener(module, &data->module_listener, &module_events, data);
+
+ return 0;
+error:
+ pw_impl_factory_destroy(data->factory);
+ return res;
+}
diff --git a/src/modules/module-session-manager/endpoint-stream.c b/src/modules/module-session-manager/endpoint-stream.c
new file mode 100644
index 0000000..ce2abdb
--- /dev/null
+++ b/src/modules/module-session-manager/endpoint-stream.c
@@ -0,0 +1,581 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Collabora Ltd.
+ * @author George Kiagiadakis <george.kiagiadakis@collabora.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <pipewire/impl.h>
+#include <pipewire/extensions/session-manager.h>
+#include <pipewire/extensions/session-manager/introspect-funcs.h>
+
+#include <spa/utils/result.h>
+#include <spa/pod/builder.h>
+#include <spa/pod/filter.h>
+
+#define MAX_PARAMS 32
+
+#define NAME "endpoint-stream"
+
+struct pw_proxy *pw_core_endpoint_stream_export(struct pw_core *core,
+ const char *type, const struct spa_dict *props, void *object,
+ size_t user_data_size);
+
+struct impl
+{
+ struct pw_global *global;
+ struct spa_hook global_listener;
+
+ union {
+ struct pw_endpoint_stream *stream;
+ struct pw_resource *resource;
+ };
+ struct spa_hook resource_listener;
+ struct spa_hook stream_listener;
+
+ struct pw_endpoint_stream_info *cached_info;
+ struct spa_list cached_params;
+
+ int ping_seq;
+ bool registered;
+};
+
+struct param_data
+{
+ struct spa_list link;
+ uint32_t id;
+ struct pw_array params;
+};
+
+struct resource_data
+{
+ struct impl *impl;
+
+ struct pw_resource *resource;
+ struct spa_hook object_listener;
+
+ uint32_t n_subscribe_ids;
+ uint32_t subscribe_ids[32];
+};
+
+struct factory_data
+{
+ struct pw_impl_module *module;
+ struct spa_hook module_listener;
+
+ struct pw_impl_factory *factory;
+ struct spa_hook factory_listener;
+
+ struct pw_export_type export;
+};
+
+#define pw_endpoint_stream_resource(r,m,v,...) \
+ pw_resource_call(r,struct pw_endpoint_stream_events,m,v,__VA_ARGS__)
+
+#define pw_endpoint_stream_resource_info(r,...) \
+ pw_endpoint_stream_resource(r,info,0,__VA_ARGS__)
+#define pw_endpoint_stream_resource_param(r,...) \
+ pw_endpoint_stream_resource(r,param,0,__VA_ARGS__)
+
+static int method_enum_params(void *object, int seq,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+ struct resource_data *d = object;
+ struct impl *impl = d->impl;
+ struct param_data *pdata;
+ struct spa_pod *result;
+ struct spa_pod *param;
+ uint8_t buffer[1024];
+ struct spa_pod_builder b = { 0 };
+ uint32_t index;
+ uint32_t next = start;
+ uint32_t count = 0;
+
+ pw_log_debug(NAME" %p: param %u %d/%d", impl, id, start, num);
+
+ spa_list_for_each(pdata, &impl->cached_params, link) {
+ if (pdata->id != id)
+ continue;
+
+ while (true) {
+ index = next++;
+ if (index >= pw_array_get_len(&pdata->params, void*))
+ return 0;
+
+ param = *pw_array_get_unchecked(&pdata->params, index, struct spa_pod*);
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ if (spa_pod_filter(&b, &result, param, filter) != 0)
+ continue;
+
+ pw_log_debug(NAME" %p: %d param %u", impl, seq, index);
+
+ pw_endpoint_stream_resource_param(d->resource, seq, id, index, next, result);
+
+ if (++count == num)
+ return 0;
+ }
+ }
+
+ return 0;
+}
+
+static int method_subscribe_params(void *object, uint32_t *ids, uint32_t n_ids)
+{
+ struct resource_data *d = object;
+ struct impl *impl = d->impl;
+ uint32_t i;
+
+ n_ids = SPA_MIN(n_ids, SPA_N_ELEMENTS(d->subscribe_ids));
+ d->n_subscribe_ids = n_ids;
+
+ for (i = 0; i < n_ids; i++) {
+ d->subscribe_ids[i] = ids[i];
+ pw_log_debug(NAME" %p: resource %d subscribe param %u",
+ impl, pw_resource_get_id(d->resource), ids[i]);
+ method_enum_params(object, 1, ids[i], 0, UINT32_MAX, NULL);
+ }
+ return 0;
+}
+
+static int method_set_param(void *object, uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct resource_data *d = object;
+ struct impl *impl = d->impl;
+ /* store only on the implementation; our cache will be updated
+ by the param event, since we are subscribed */
+ pw_endpoint_stream_set_param(impl->stream, id, flags, param);
+ return 0;
+}
+
+static const struct pw_endpoint_stream_methods stream_methods = {
+ PW_VERSION_ENDPOINT_STREAM_METHODS,
+ .subscribe_params = method_subscribe_params,
+ .enum_params = method_enum_params,
+ .set_param = method_set_param,
+};
+
+static int global_bind(void *object, struct pw_impl_client *client,
+ uint32_t permissions, uint32_t version, uint32_t id)
+{
+ struct impl *impl = object;
+ struct pw_resource *resource;
+ struct resource_data *data;
+
+ resource = pw_resource_new(client, id, permissions,
+ PW_TYPE_INTERFACE_EndpointStream,
+ version, sizeof(*data));
+ if (resource == NULL)
+ return -errno;
+
+ data = pw_resource_get_user_data(resource);
+ data->impl = impl;
+ data->resource = resource;
+
+ pw_global_add_resource(impl->global, resource);
+
+ /* resource methods -> implementation */
+ pw_resource_add_object_listener(resource,
+ &data->object_listener,
+ &stream_methods, data);
+
+ impl->cached_info->change_mask = PW_ENDPOINT_STREAM_CHANGE_MASK_ALL;
+ pw_endpoint_stream_resource_info(resource, impl->cached_info);
+ impl->cached_info->change_mask = 0;
+
+ return 0;
+}
+
+static void global_destroy(void *data)
+{
+ struct impl *impl = data;
+ spa_hook_remove(&impl->global_listener);
+ impl->global = NULL;
+ if (impl->resource)
+ pw_resource_destroy(impl->resource);
+ free(impl);
+}
+
+static const struct pw_global_events global_events = {
+ PW_VERSION_GLOBAL_EVENTS,
+ .destroy = global_destroy,
+};
+
+static void impl_resource_destroy(void *data)
+{
+ struct impl *impl = data;
+ struct param_data *pdata, *tmp;
+
+ spa_hook_remove(&impl->resource_listener);
+ spa_hook_remove(&impl->stream_listener);
+ impl->resource = NULL;
+
+ /* clear cache */
+ if (impl->cached_info)
+ pw_endpoint_stream_info_free(impl->cached_info);
+ spa_list_for_each_safe(pdata, tmp, &impl->cached_params, link) {
+ struct spa_pod **pod;
+ pw_array_for_each(pod, &pdata->params)
+ free(*pod);
+ pw_array_clear(&pdata->params);
+ spa_list_remove(&pdata->link);
+ free(pdata);
+ }
+
+ if (impl->global)
+ pw_global_destroy(impl->global);
+}
+
+static void register_global(struct impl *impl)
+{
+ impl->cached_info->id = pw_global_get_id (impl->global);
+ pw_resource_set_bound_id(impl->resource, impl->cached_info->id);
+ pw_global_register(impl->global);
+ impl->registered = true;
+}
+
+static void impl_resource_pong (void *data, int seq)
+{
+ struct impl *impl = data;
+
+ /* complete registration, if this was the initial sync */
+ if (!impl->registered && seq == impl->ping_seq) {
+ register_global(impl);
+ }
+}
+
+static const struct pw_resource_events impl_resource_events = {
+ PW_VERSION_RESOURCE_EVENTS,
+ .destroy = impl_resource_destroy,
+ .pong = impl_resource_pong,
+};
+
+static int emit_info(void *data, struct pw_resource *resource)
+{
+ const struct pw_endpoint_stream_info *info = data;
+ pw_endpoint_stream_resource_info(resource, info);
+ return 0;
+}
+
+static void event_info(void *data, const struct pw_endpoint_stream_info *info)
+{
+ struct impl *impl = data;
+ uint32_t changed_ids[MAX_PARAMS], n_changed_ids = 0;
+ uint32_t i;
+
+ /* figure out changes to params */
+ if (info->change_mask & PW_ENDPOINT_STREAM_CHANGE_MASK_PARAMS) {
+ for (i = 0; i < info->n_params; i++) {
+ if ((!impl->cached_info ||
+ info->params[i].flags != impl->cached_info->params[i].flags)
+ && info->params[i].flags & SPA_PARAM_INFO_READ)
+ changed_ids[n_changed_ids++] = info->params[i].id;
+ }
+ }
+
+ /* cache for new clients */
+ impl->cached_info = pw_endpoint_stream_info_update (impl->cached_info, info);
+
+ /* notify existing clients */
+ pw_global_for_each_resource(impl->global, emit_info, (void*) info);
+
+ /* cache params & register */
+ if (n_changed_ids > 0) {
+ /* prepare params storage */
+ for (i = 0; i < n_changed_ids; i++) {
+ struct param_data *pdata = calloc(1, sizeof(struct param_data));
+ pdata->id = changed_ids[i];
+ pw_array_init(&pdata->params, sizeof(void*));
+ spa_list_append(&impl->cached_params, &pdata->link);
+ }
+
+ /* subscribe to impl */
+ pw_endpoint_stream_subscribe_params(impl->stream, changed_ids, n_changed_ids);
+
+ /* register asynchronously on the pong event */
+ impl->ping_seq = pw_resource_ping(impl->resource, 0);
+ }
+ else if (!impl->registered) {
+ register_global(impl);
+ }
+}
+
+struct param_event_args
+{
+ uint32_t id, index, next;
+ const struct spa_pod *param;
+};
+
+static int emit_param(void *_data, struct pw_resource *resource)
+{
+ struct param_event_args *args = _data;
+ struct resource_data *data;
+ uint32_t i;
+
+ data = pw_resource_get_user_data(resource);
+ for (i = 0; i < data->n_subscribe_ids; i++) {
+ if (data->subscribe_ids[i] == args->id) {
+ pw_endpoint_stream_resource_param(resource, 1,
+ args->id, args->index, args->next, args->param);
+ }
+ }
+ return 0;
+}
+
+static void event_param(void *data, int seq,
+ uint32_t id, uint32_t index, uint32_t next,
+ const struct spa_pod *param)
+{
+ struct impl *impl = data;
+ struct param_data *pdata;
+ struct spa_pod **pod;
+ struct param_event_args args = { id, index, next, param };
+
+ /* cache for new requests */
+ spa_list_for_each(pdata, &impl->cached_params, link) {
+ if (pdata->id != id)
+ continue;
+
+ if (!pw_array_check_index(&pdata->params, index, void*)) {
+ while (pw_array_get_len(&pdata->params, void*) <= index)
+ pw_array_add_ptr(&pdata->params, NULL);
+ }
+
+ pod = pw_array_get_unchecked(&pdata->params, index, struct spa_pod*);
+ free(*pod);
+ *pod = spa_pod_copy(param);
+ }
+
+ /* notify existing clients */
+ pw_global_for_each_resource(impl->global, emit_param, &args);
+}
+
+static const struct pw_endpoint_stream_events stream_events = {
+ PW_VERSION_ENDPOINT_STREAM_EVENTS,
+ .info = event_info,
+ .param = event_param,
+};
+
+static void *stream_new(struct pw_context *context,
+ struct pw_resource *resource,
+ struct pw_properties *properties)
+{
+ struct impl *impl;
+ char serial_str[32];
+ struct spa_dict_item items[1] = {
+ SPA_DICT_ITEM_INIT(PW_KEY_OBJECT_SERIAL, serial_str),
+ };
+ struct spa_dict extra_props = SPA_DICT_INIT_ARRAY(items);
+ static const char * const keys[] = {
+ PW_KEY_OBJECT_SERIAL,
+ NULL
+ };
+
+ impl = calloc(1, sizeof(*impl));
+ if (impl == NULL) {
+ pw_properties_free(properties);
+ return NULL;
+ }
+
+ impl->global = pw_global_new(context,
+ PW_TYPE_INTERFACE_EndpointStream,
+ PW_VERSION_ENDPOINT_STREAM,
+ properties,
+ global_bind, impl);
+ if (impl->global == NULL) {
+ free(impl);
+ return NULL;
+ }
+ impl->resource = resource;
+
+ spa_scnprintf(serial_str, sizeof(serial_str), "%"PRIu64,
+ pw_global_get_serial(impl->global));
+ pw_global_update_keys(impl->global, &extra_props, keys);
+
+ spa_list_init(&impl->cached_params);
+
+ /* handle destroy events */
+ pw_global_add_listener(impl->global,
+ &impl->global_listener,
+ &global_events, impl);
+ pw_resource_add_listener(impl->resource,
+ &impl->resource_listener,
+ &impl_resource_events, impl);
+
+ /* handle implementation events -> cache + client resources */
+ pw_endpoint_stream_add_listener(impl->stream,
+ &impl->stream_listener,
+ &stream_events, impl);
+
+ /* global is not registered here on purpose;
+ we first cache info + params and then expose the global */
+
+ return impl;
+}
+
+static void *create_object(void *data,
+ struct pw_resource *resource,
+ const char *type,
+ uint32_t version,
+ struct pw_properties *properties,
+ uint32_t new_id)
+{
+ struct factory_data *d = data;
+ struct pw_resource *impl_resource;
+ struct pw_impl_client *client = pw_resource_get_client(resource);
+ void *result;
+ int res;
+
+ impl_resource = pw_resource_new(client, new_id, PW_PERM_ALL, type, version, 0);
+ if (impl_resource == NULL) {
+ res = -errno;
+ goto error_resource;
+ }
+
+ pw_resource_install_marshal(impl_resource, true);
+
+ if (properties == NULL)
+ properties = pw_properties_new(NULL, NULL);
+ if (properties == NULL) {
+ res = -ENOMEM;
+ goto error_stream;
+ }
+
+ pw_properties_setf(properties, PW_KEY_CLIENT_ID, "%d",
+ pw_impl_client_get_info(client)->id);
+ pw_properties_setf(properties, PW_KEY_FACTORY_ID, "%d",
+ pw_impl_factory_get_info(d->factory)->id);
+
+ result = stream_new(pw_impl_client_get_context(client), impl_resource, properties);
+ if (result == NULL) {
+ res = -errno;
+ goto error_stream;
+ }
+ return result;
+
+error_resource:
+ pw_log_error("can't create resource: %s", spa_strerror(res));
+ pw_resource_errorf_id(resource, new_id, res, "can't create resource: %s", spa_strerror(res));
+ goto error_exit;
+error_stream:
+ pw_log_error("can't create endpoint stream: %s", spa_strerror(res));
+ pw_resource_errorf_id(resource, new_id, res, "can't create endpoint stream: %s", spa_strerror(res));
+ goto error_exit_free;
+
+error_exit_free:
+ pw_resource_remove(impl_resource);
+error_exit:
+ errno = -res;
+ return NULL;
+}
+
+static const struct pw_impl_factory_implementation impl_factory = {
+ PW_VERSION_IMPL_FACTORY_IMPLEMENTATION,
+ .create_object = create_object,
+};
+
+static void factory_destroy(void *data)
+{
+ struct factory_data *d = data;
+ spa_hook_remove(&d->factory_listener);
+ d->factory = NULL;
+ if (d->module)
+ pw_impl_module_destroy(d->module);
+}
+
+static const struct pw_impl_factory_events factory_events = {
+ PW_VERSION_IMPL_FACTORY_EVENTS,
+ .destroy = factory_destroy,
+};
+
+static void module_destroy(void *data)
+{
+ struct factory_data *d = data;
+
+ spa_hook_remove(&d->module_listener);
+ spa_list_remove(&d->export.link);
+ d->module = NULL;
+
+ if (d->factory)
+ pw_impl_factory_destroy(d->factory);
+}
+
+static void module_registered(void *data)
+{
+ struct factory_data *d = data;
+ struct pw_impl_module *module = d->module;
+ struct pw_impl_factory *factory = d->factory;
+ struct spa_dict_item items[1];
+ char id[16];
+ int res;
+
+ snprintf(id, sizeof(id), "%d", pw_impl_module_get_info(module)->id);
+ items[0] = SPA_DICT_ITEM_INIT(PW_KEY_MODULE_ID, id);
+ pw_impl_factory_update_properties(factory, &SPA_DICT_INIT(items, 1));
+
+ if ((res = pw_impl_factory_register(factory, NULL)) < 0) {
+ pw_log_error(NAME" %p: can't register factory: %s", factory, spa_strerror(res));
+ }
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy,
+ .registered = module_registered,
+};
+
+int endpoint_stream_factory_init(struct pw_impl_module *module)
+{
+ struct pw_context *context = pw_impl_module_get_context(module);
+ struct pw_impl_factory *factory;
+ struct factory_data *data;
+ int res;
+
+ factory = pw_context_create_factory(context,
+ "endpoint-stream",
+ PW_TYPE_INTERFACE_EndpointStream,
+ PW_VERSION_ENDPOINT_STREAM,
+ NULL,
+ sizeof(*data));
+ if (factory == NULL)
+ return -errno;
+
+ data = pw_impl_factory_get_user_data(factory);
+ data->factory = factory;
+ data->module = module;
+
+ pw_impl_factory_set_implementation(factory, &impl_factory, data);
+
+ data->export.type = PW_TYPE_INTERFACE_EndpointStream;
+ data->export.func = pw_core_endpoint_stream_export;
+ if ((res = pw_context_register_export_type(context, &data->export)) < 0)
+ goto error;
+
+ pw_impl_factory_add_listener(factory, &data->factory_listener, &factory_events, data);
+ pw_impl_module_add_listener(module, &data->module_listener, &module_events, data);
+
+ return 0;
+error:
+ pw_impl_factory_destroy(data->factory);
+ return res;
+}
diff --git a/src/modules/module-session-manager/endpoint.c b/src/modules/module-session-manager/endpoint.c
new file mode 100644
index 0000000..752a529
--- /dev/null
+++ b/src/modules/module-session-manager/endpoint.c
@@ -0,0 +1,590 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Collabora Ltd.
+ * @author George Kiagiadakis <george.kiagiadakis@collabora.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <pipewire/impl.h>
+#include <pipewire/extensions/session-manager.h>
+#include <pipewire/extensions/session-manager/introspect-funcs.h>
+
+#include <spa/utils/result.h>
+#include <spa/pod/builder.h>
+#include <spa/pod/filter.h>
+
+#define MAX_PARAMS 32
+
+#define NAME "endpoint"
+
+struct pw_proxy *pw_core_endpoint_export(struct pw_core *core,
+ const char *type, const struct spa_dict *props, void *object,
+ size_t user_data_size);
+
+struct impl
+{
+ struct pw_global *global;
+ struct spa_hook global_listener;
+
+ union {
+ struct pw_endpoint *endpoint;
+ struct pw_resource *resource;
+ };
+ struct spa_hook resource_listener;
+ struct spa_hook endpoint_listener;
+
+ struct pw_endpoint_info *cached_info;
+ struct spa_list cached_params;
+
+ int ping_seq;
+ bool registered;
+};
+
+struct param_data
+{
+ struct spa_list link;
+ uint32_t id;
+ struct pw_array params;
+};
+
+struct resource_data
+{
+ struct impl *impl;
+
+ struct pw_resource *resource;
+ struct spa_hook object_listener;
+
+ uint32_t n_subscribe_ids;
+ uint32_t subscribe_ids[32];
+};
+
+struct factory_data
+{
+ struct pw_impl_module *module;
+ struct spa_hook module_listener;
+
+ struct pw_impl_factory *factory;
+ struct spa_hook factory_listener;
+
+ struct pw_export_type export;
+};
+
+#define pw_endpoint_resource(r,m,v,...) \
+ pw_resource_call(r,struct pw_endpoint_events,m,v,__VA_ARGS__)
+
+#define pw_endpoint_resource_info(r,...) \
+ pw_endpoint_resource(r,info,0,__VA_ARGS__)
+#define pw_endpoint_resource_param(r,...) \
+ pw_endpoint_resource(r,param,0,__VA_ARGS__)
+
+static int method_enum_params(void *object, int seq,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+ struct resource_data *d = object;
+ struct impl *impl = d->impl;
+ struct param_data *pdata;
+ struct spa_pod *result;
+ struct spa_pod *param;
+ uint8_t buffer[1024];
+ struct spa_pod_builder b = { 0 };
+ uint32_t index;
+ uint32_t next = start;
+ uint32_t count = 0;
+
+ pw_log_debug(NAME" %p: param %u %d/%d", impl, id, start, num);
+
+ spa_list_for_each(pdata, &impl->cached_params, link) {
+ if (pdata->id != id)
+ continue;
+
+ while (true) {
+ index = next++;
+ if (index >= pw_array_get_len(&pdata->params, void*))
+ return 0;
+
+ param = *pw_array_get_unchecked(&pdata->params, index, struct spa_pod*);
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ if (spa_pod_filter(&b, &result, param, filter) != 0)
+ continue;
+
+ pw_log_debug(NAME" %p: %d param %u", impl, seq, index);
+
+ pw_endpoint_resource_param(d->resource, seq, id, index, next, result);
+
+ if (++count == num)
+ return 0;
+ }
+ }
+
+ return 0;
+}
+
+static int method_subscribe_params(void *object, uint32_t *ids, uint32_t n_ids)
+{
+ struct resource_data *d = object;
+ struct impl *impl = d->impl;
+ uint32_t i;
+
+ n_ids = SPA_MIN(n_ids, SPA_N_ELEMENTS(d->subscribe_ids));
+ d->n_subscribe_ids = n_ids;
+
+ for (i = 0; i < n_ids; i++) {
+ d->subscribe_ids[i] = ids[i];
+ pw_log_debug(NAME" %p: resource %d subscribe param %u",
+ impl, pw_resource_get_id(d->resource), ids[i]);
+ method_enum_params(object, 1, ids[i], 0, UINT32_MAX, NULL);
+ }
+ return 0;
+}
+
+static int method_set_param(void *object, uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct resource_data *d = object;
+ struct impl *impl = d->impl;
+ /* store only on the implementation; our cache will be updated
+ by the param event, since we are subscribed */
+ pw_endpoint_set_param(impl->endpoint, id, flags, param);
+ return 0;
+}
+
+static int method_create_link(void *object, const struct spa_dict *props)
+{
+ struct resource_data *d = object;
+ struct impl *impl = d->impl;
+ pw_endpoint_create_link(impl->endpoint, props);
+ return 0;
+}
+
+static const struct pw_endpoint_methods endpoint_methods = {
+ PW_VERSION_ENDPOINT_METHODS,
+ .subscribe_params = method_subscribe_params,
+ .enum_params = method_enum_params,
+ .set_param = method_set_param,
+ .create_link = method_create_link,
+};
+
+static int global_bind(void *object, struct pw_impl_client *client,
+ uint32_t permissions, uint32_t version, uint32_t id)
+{
+ struct impl *impl = object;
+ struct pw_resource *resource;
+ struct resource_data *data;
+
+ resource = pw_resource_new(client, id, permissions,
+ PW_TYPE_INTERFACE_Endpoint,
+ version, sizeof(*data));
+ if (resource == NULL)
+ return -errno;
+
+ data = pw_resource_get_user_data(resource);
+ data->impl = impl;
+ data->resource = resource;
+
+ pw_global_add_resource(impl->global, resource);
+
+ /* resource methods -> implementation */
+ pw_resource_add_object_listener(resource,
+ &data->object_listener,
+ &endpoint_methods, data);
+
+ impl->cached_info->change_mask = PW_ENDPOINT_CHANGE_MASK_ALL;
+ pw_endpoint_resource_info(resource, impl->cached_info);
+ impl->cached_info->change_mask = 0;
+
+ return 0;
+}
+
+static void global_destroy(void *data)
+{
+ struct impl *impl = data;
+ spa_hook_remove(&impl->global_listener);
+ impl->global = NULL;
+ if (impl->resource)
+ pw_resource_destroy(impl->resource);
+ free(impl);
+}
+
+static const struct pw_global_events global_events = {
+ PW_VERSION_GLOBAL_EVENTS,
+ .destroy = global_destroy,
+};
+
+static void impl_resource_destroy(void *data)
+{
+ struct impl *impl = data;
+ struct param_data *pdata, *tmp;
+
+ spa_hook_remove(&impl->resource_listener);
+ spa_hook_remove(&impl->endpoint_listener);
+ impl->resource = NULL;
+
+ /* clear cache */
+ if (impl->cached_info)
+ pw_endpoint_info_free(impl->cached_info);
+ spa_list_for_each_safe(pdata, tmp, &impl->cached_params, link) {
+ struct spa_pod **pod;
+ pw_array_for_each(pod, &pdata->params)
+ free(*pod);
+ pw_array_clear(&pdata->params);
+ spa_list_remove(&pdata->link);
+ free(pdata);
+ }
+
+ if (impl->global)
+ pw_global_destroy(impl->global);
+}
+
+static void register_global(struct impl *impl)
+{
+ impl->cached_info->id = pw_global_get_id (impl->global);
+ pw_resource_set_bound_id(impl->resource, impl->cached_info->id);
+ pw_global_register(impl->global);
+ impl->registered = true;
+}
+
+static void impl_resource_pong (void *data, int seq)
+{
+ struct impl *impl = data;
+
+ /* complete registration, if this was the initial sync */
+ if (!impl->registered && seq == impl->ping_seq) {
+ register_global(impl);
+ }
+}
+
+static const struct pw_resource_events impl_resource_events = {
+ PW_VERSION_RESOURCE_EVENTS,
+ .destroy = impl_resource_destroy,
+ .pong = impl_resource_pong,
+};
+
+static int emit_info(void *data, struct pw_resource *resource)
+{
+ const struct pw_endpoint_info *info = data;
+ pw_endpoint_resource_info(resource, info);
+ return 0;
+}
+
+static void event_info(void *data, const struct pw_endpoint_info *info)
+{
+ struct impl *impl = data;
+ uint32_t changed_ids[MAX_PARAMS], n_changed_ids = 0;
+ uint32_t i;
+
+ /* figure out changes to params */
+ if (info->change_mask & PW_ENDPOINT_CHANGE_MASK_PARAMS) {
+ for (i = 0; i < info->n_params; i++) {
+ if ((!impl->cached_info ||
+ info->params[i].flags != impl->cached_info->params[i].flags)
+ && info->params[i].flags & SPA_PARAM_INFO_READ)
+ changed_ids[n_changed_ids++] = info->params[i].id;
+ }
+ }
+
+ /* cache for new clients */
+ impl->cached_info = pw_endpoint_info_update (impl->cached_info, info);
+
+ /* notify existing clients */
+ pw_global_for_each_resource(impl->global, emit_info, (void*) info);
+
+ /* cache params & register */
+ if (n_changed_ids > 0) {
+ /* prepare params storage */
+ for (i = 0; i < n_changed_ids; i++) {
+ struct param_data *pdata = calloc(1, sizeof(struct param_data));
+ pdata->id = changed_ids[i];
+ pw_array_init(&pdata->params, sizeof(void*));
+ spa_list_append(&impl->cached_params, &pdata->link);
+ }
+
+ /* subscribe to impl */
+ pw_endpoint_subscribe_params(impl->endpoint, changed_ids, n_changed_ids);
+
+ /* register asynchronously on the pong event */
+ impl->ping_seq = pw_resource_ping(impl->resource, 0);
+ }
+ else if (!impl->registered) {
+ register_global(impl);
+ }
+}
+
+struct param_event_args
+{
+ uint32_t id, index, next;
+ const struct spa_pod *param;
+};
+
+static int emit_param(void *_data, struct pw_resource *resource)
+{
+ struct param_event_args *args = _data;
+ struct resource_data *data;
+ uint32_t i;
+
+ data = pw_resource_get_user_data(resource);
+ for (i = 0; i < data->n_subscribe_ids; i++) {
+ if (data->subscribe_ids[i] == args->id) {
+ pw_endpoint_resource_param(resource, 1,
+ args->id, args->index, args->next, args->param);
+ }
+ }
+ return 0;
+}
+
+static void event_param(void *data, int seq,
+ uint32_t id, uint32_t index, uint32_t next,
+ const struct spa_pod *param)
+{
+ struct impl *impl = data;
+ struct param_data *pdata;
+ struct spa_pod **pod;
+ struct param_event_args args = { id, index, next, param };
+
+ /* cache for new requests */
+ spa_list_for_each(pdata, &impl->cached_params, link) {
+ if (pdata->id != id)
+ continue;
+
+ if (!pw_array_check_index(&pdata->params, index, void*)) {
+ while (pw_array_get_len(&pdata->params, void*) <= index)
+ pw_array_add_ptr(&pdata->params, NULL);
+ }
+
+ pod = pw_array_get_unchecked(&pdata->params, index, struct spa_pod*);
+ free(*pod);
+ *pod = spa_pod_copy(param);
+ }
+
+ /* notify existing clients */
+ pw_global_for_each_resource(impl->global, emit_param, &args);
+}
+
+static const struct pw_endpoint_events endpoint_events = {
+ PW_VERSION_ENDPOINT_EVENTS,
+ .info = event_info,
+ .param = event_param,
+};
+
+static void *endpoint_new(struct pw_context *context,
+ struct pw_resource *resource,
+ struct pw_properties *properties)
+{
+ struct impl *impl;
+ char serial_str[32];
+ struct spa_dict_item items[1] = {
+ SPA_DICT_ITEM_INIT(PW_KEY_OBJECT_SERIAL, serial_str),
+ };
+ struct spa_dict extra_props = SPA_DICT_INIT_ARRAY(items);
+ static const char * const keys[] = {
+ PW_KEY_OBJECT_SERIAL,
+ NULL
+ };
+
+ impl = calloc(1, sizeof(*impl));
+ if (impl == NULL) {
+ pw_properties_free(properties);
+ return NULL;
+ }
+
+ impl->global = pw_global_new(context,
+ PW_TYPE_INTERFACE_Endpoint,
+ PW_VERSION_ENDPOINT,
+ properties,
+ global_bind, impl);
+ if (impl->global == NULL) {
+ free(impl);
+ return NULL;
+ }
+ impl->resource = resource;
+
+ spa_scnprintf(serial_str, sizeof(serial_str), "%"PRIu64,
+ pw_global_get_serial(impl->global));
+ pw_global_update_keys(impl->global, &extra_props, keys);
+
+ spa_list_init(&impl->cached_params);
+
+ /* handle destroy events */
+ pw_global_add_listener(impl->global,
+ &impl->global_listener,
+ &global_events, impl);
+ pw_resource_add_listener(impl->resource,
+ &impl->resource_listener,
+ &impl_resource_events, impl);
+
+ /* handle implementation events -> cache + client resources */
+ pw_endpoint_add_listener(impl->endpoint,
+ &impl->endpoint_listener,
+ &endpoint_events, impl);
+
+ /* global is not registered here on purpose;
+ we first cache info + params and then expose the global */
+
+ return impl;
+}
+
+static void *create_object(void *data,
+ struct pw_resource *resource,
+ const char *type,
+ uint32_t version,
+ struct pw_properties *properties,
+ uint32_t new_id)
+{
+ struct factory_data *d = data;
+ struct pw_resource *impl_resource;
+ struct pw_impl_client *client = pw_resource_get_client(resource);
+ void *result;
+ int res;
+
+ impl_resource = pw_resource_new(client, new_id, PW_PERM_ALL, type, version, 0);
+ if (impl_resource == NULL) {
+ res = -errno;
+ goto error_resource;
+ }
+
+ pw_resource_install_marshal(impl_resource, true);
+
+ if (properties == NULL)
+ properties = pw_properties_new(NULL, NULL);
+ if (properties == NULL) {
+ res = -ENOMEM;
+ goto error_endpoint;
+ }
+
+ pw_properties_setf(properties, PW_KEY_CLIENT_ID, "%d",
+ pw_impl_client_get_info(client)->id);
+ pw_properties_setf(properties, PW_KEY_FACTORY_ID, "%d",
+ pw_impl_factory_get_info(d->factory)->id);
+
+ result = endpoint_new(pw_impl_client_get_context(client), impl_resource, properties);
+ if (result == NULL) {
+ res = -errno;
+ goto error_endpoint;
+ }
+ return result;
+
+error_resource:
+ pw_log_error("can't create resource: %s", spa_strerror(res));
+ pw_resource_errorf_id(resource, new_id, res, "can't create resource: %s", spa_strerror(res));
+ goto error_exit;
+error_endpoint:
+ pw_log_error("can't create endpoint: %s", spa_strerror(res));
+ pw_resource_errorf_id(resource, new_id, res, "can't create endpoint: %s", spa_strerror(res));
+ goto error_exit_free;
+
+error_exit_free:
+ pw_resource_remove(impl_resource);
+error_exit:
+ errno = -res;
+ return NULL;
+}
+
+static const struct pw_impl_factory_implementation impl_factory = {
+ PW_VERSION_IMPL_FACTORY_IMPLEMENTATION,
+ .create_object = create_object,
+};
+
+static void factory_destroy(void *data)
+{
+ struct factory_data *d = data;
+ spa_hook_remove(&d->factory_listener);
+ d->factory = NULL;
+ if (d->module)
+ pw_impl_module_destroy(d->module);
+}
+
+static const struct pw_impl_factory_events factory_events = {
+ PW_VERSION_IMPL_FACTORY_EVENTS,
+ .destroy = factory_destroy,
+};
+
+static void module_destroy(void *data)
+{
+ struct factory_data *d = data;
+
+ spa_hook_remove(&d->module_listener);
+ spa_list_remove(&d->export.link);
+
+ d->module = NULL;
+ if (d->factory)
+ pw_impl_factory_destroy(d->factory);
+}
+
+static void module_registered(void *data)
+{
+ struct factory_data *d = data;
+ struct pw_impl_module *module = d->module;
+ struct pw_impl_factory *factory = d->factory;
+ struct spa_dict_item items[1];
+ char id[16];
+ int res;
+
+ snprintf(id, sizeof(id), "%d", pw_impl_module_get_info(module)->id);
+ items[0] = SPA_DICT_ITEM_INIT(PW_KEY_MODULE_ID, id);
+ pw_impl_factory_update_properties(factory, &SPA_DICT_INIT(items, 1));
+
+ if ((res = pw_impl_factory_register(factory, NULL)) < 0) {
+ pw_log_error(NAME" %p: can't register factory: %s", factory, spa_strerror(res));
+ }
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy,
+ .registered = module_registered,
+};
+
+int endpoint_factory_init(struct pw_impl_module *module)
+{
+ struct pw_context *context = pw_impl_module_get_context(module);
+ struct pw_impl_factory *factory;
+ struct factory_data *data;
+ int res;
+
+ factory = pw_context_create_factory(context,
+ "endpoint",
+ PW_TYPE_INTERFACE_Endpoint,
+ PW_VERSION_ENDPOINT,
+ NULL,
+ sizeof(*data));
+ if (factory == NULL)
+ return -errno;
+
+ data = pw_impl_factory_get_user_data(factory);
+ data->factory = factory;
+ data->module = module;
+
+ pw_impl_factory_set_implementation(factory, &impl_factory, data);
+
+ data->export.type = PW_TYPE_INTERFACE_Endpoint;
+ data->export.func = pw_core_endpoint_export;
+ if ((res = pw_context_register_export_type(context, &data->export)) < 0)
+ goto error;
+
+ pw_impl_factory_add_listener(factory, &data->factory_listener, &factory_events, data);
+ pw_impl_module_add_listener(module, &data->module_listener, &module_events, data);
+
+ return 0;
+error:
+ pw_impl_factory_destroy(data->factory);
+ return res;
+}
diff --git a/src/modules/module-session-manager/protocol-native.c b/src/modules/module-session-manager/protocol-native.c
new file mode 100644
index 0000000..6981252
--- /dev/null
+++ b/src/modules/module-session-manager/protocol-native.c
@@ -0,0 +1,3083 @@
+/* PipeWire
+ *
+ * Copyright © 2019 Collabora Ltd.
+ * @author George Kiagiadakis <george.kiagiadakis@collabora.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <pipewire/pipewire.h>
+
+#include <spa/utils/result.h>
+#include <spa/pod/builder.h>
+#include <spa/pod/parser.h>
+
+#include <pipewire/extensions/session-manager.h>
+#include <pipewire/extensions/protocol-native.h>
+
+#define MAX_DICT 1024
+#define MAX_PARAMS 4096
+#define MAX_PARAM_INFO 128
+
+static void push_dict(struct spa_pod_builder *b, const struct spa_dict *dict)
+{
+ struct spa_pod_frame f;
+ uint32_t n_items;
+ uint32_t i;
+
+ n_items = dict ? dict->n_items : 0;
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_add(b, SPA_POD_Int(n_items), NULL);
+ for (i = 0; i < n_items; i++) {
+ spa_pod_builder_add(b,
+ SPA_POD_String(dict->items[i].key),
+ SPA_POD_String(dict->items[i].value),
+ NULL);
+ }
+ spa_pod_builder_pop(b, &f);
+}
+
+/* macro because of alloca() */
+#define parse_dict(p, f, dict) \
+do { \
+ uint32_t i; \
+ \
+ if (spa_pod_parser_push_struct(p, f) < 0 || \
+ spa_pod_parser_get(p, SPA_POD_Int(&(dict)->n_items), NULL) < 0) \
+ return -EINVAL; \
+ \
+ if ((dict)->n_items > 0) { \
+ if ((dict)->n_items > MAX_DICT) \
+ return -ENOSPC; \
+ (dict)->items = alloca((dict)->n_items * sizeof(struct spa_dict_item)); \
+ for (i = 0; i < (dict)->n_items; i++) { \
+ if (spa_pod_parser_get(p, \
+ SPA_POD_String(&(dict)->items[i].key), \
+ SPA_POD_String(&(dict)->items[i].value), \
+ NULL) < 0) \
+ return -EINVAL; \
+ } \
+ } \
+ spa_pod_parser_pop(p, f); \
+} while(0)
+
+static void push_param_infos(struct spa_pod_builder *b, uint32_t n_params,
+ const struct spa_param_info *params)
+{
+ struct spa_pod_frame f;
+ uint32_t i;
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_add(b, SPA_POD_Int(n_params), NULL);
+ for (i = 0; i < n_params; i++) {
+ spa_pod_builder_add(b,
+ SPA_POD_Id(params[i].id),
+ SPA_POD_Int(params[i].flags),
+ NULL);
+ }
+ spa_pod_builder_pop(b, &f);
+}
+
+/* macro because of alloca() */
+#define parse_param_infos(p, f, n_params_p, params_p) \
+do { \
+ uint32_t i; \
+ \
+ if (spa_pod_parser_push_struct(p, f) < 0 || \
+ spa_pod_parser_get(p, SPA_POD_Int(n_params_p), NULL) < 0) \
+ return -EINVAL; \
+ \
+ if (*(n_params_p) > 0) { \
+ if (*(n_params_p) > MAX_PARAM_INFO) \
+ return -ENOSPC; \
+ *(params_p) = alloca(*(n_params_p) * sizeof(struct spa_param_info)); \
+ for (i = 0; i < *(n_params_p); i++) { \
+ if (spa_pod_parser_get(p, \
+ SPA_POD_Id(&(*(params_p))[i].id), \
+ SPA_POD_Int(&(*(params_p))[i].flags), \
+ NULL) < 0) \
+ return -EINVAL; \
+ } \
+ } \
+ spa_pod_parser_pop(p, f); \
+} while(0)
+
+/***********************************************
+ * INFO STRUCTURES
+ ***********************************************/
+
+static void
+marshal_pw_session_info(struct spa_pod_builder *b,
+ const struct pw_session_info *info)
+{
+ struct spa_pod_frame f;
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_add(b,
+ SPA_POD_Int(info->version),
+ SPA_POD_Int(info->id),
+ SPA_POD_Long(info->change_mask),
+ NULL);
+ push_dict(b, info->props);
+ push_param_infos(b, info->n_params, info->params);
+ spa_pod_builder_pop(b, &f);
+}
+
+/* macro because of alloca() */
+#define demarshal_pw_session_info(p, f, info) \
+do { \
+ struct spa_pod_frame sub_f; \
+ uint32_t version; \
+ \
+ if (spa_pod_parser_push_struct(p, f) < 0 || \
+ spa_pod_parser_get(p, \
+ SPA_POD_Int(&version), \
+ SPA_POD_Int(&(info)->id), \
+ SPA_POD_Long(&(info)->change_mask), \
+ NULL) < 0) \
+ return -EINVAL; \
+ \
+ (info)->change_mask &= PW_SESSION_CHANGE_MASK_ALL; \
+ \
+ parse_dict(p, &sub_f, (info)->props); \
+ parse_param_infos(p, &sub_f, &(info)->n_params, &(info)->params); \
+ \
+ spa_pod_parser_pop(p, f); \
+} while(0)
+
+static void
+marshal_pw_endpoint_info(struct spa_pod_builder *b,
+ const struct pw_endpoint_info *info)
+{
+ struct spa_pod_frame f;
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_add(b,
+ SPA_POD_Int(info->version),
+ SPA_POD_Int(info->id),
+ SPA_POD_String(info->name),
+ SPA_POD_String(info->media_class),
+ SPA_POD_Int(info->direction),
+ SPA_POD_Int(info->flags),
+ SPA_POD_Long(info->change_mask),
+ SPA_POD_Int(info->n_streams),
+ SPA_POD_Int(info->session_id),
+ NULL);
+ push_dict(b, info->props);
+ push_param_infos(b, info->n_params, info->params);
+ spa_pod_builder_pop(b, &f);
+}
+
+/* macro because of alloca() */
+#define demarshal_pw_endpoint_info(p, f, info) \
+do { \
+ struct spa_pod_frame sub_f; \
+ uint32_t version; \
+ \
+ if (spa_pod_parser_push_struct(p, f) < 0 || \
+ spa_pod_parser_get(p, \
+ SPA_POD_Int(&version), \
+ SPA_POD_Int(&(info)->id), \
+ SPA_POD_String(&(info)->name), \
+ SPA_POD_String(&(info)->media_class), \
+ SPA_POD_Int(&(info)->direction), \
+ SPA_POD_Int(&(info)->flags), \
+ SPA_POD_Long(&(info)->change_mask), \
+ SPA_POD_Int(&(info)->n_streams), \
+ SPA_POD_Int(&(info)->session_id), \
+ NULL) < 0) \
+ return -EINVAL; \
+ \
+ (info)->change_mask &= PW_ENDPOINT_CHANGE_MASK_ALL; \
+ \
+ parse_dict(p, &sub_f, (info)->props); \
+ parse_param_infos(p, &sub_f, &(info)->n_params, &(info)->params); \
+ \
+ spa_pod_parser_pop(p, f); \
+} while(0)
+
+static void
+marshal_pw_endpoint_stream_info(struct spa_pod_builder *b,
+ const struct pw_endpoint_stream_info *info)
+{
+ struct spa_pod_frame f;
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_add(b,
+ SPA_POD_Int(info->version),
+ SPA_POD_Int(info->id),
+ SPA_POD_Int(info->endpoint_id),
+ SPA_POD_String(info->name),
+ SPA_POD_Long(info->change_mask),
+ SPA_POD_Pod(info->link_params),
+ NULL);
+ push_dict(b, info->props);
+ push_param_infos(b, info->n_params, info->params);
+ spa_pod_builder_pop(b, &f);
+}
+
+/* macro because of alloca() */
+#define demarshal_pw_endpoint_stream_info(p, f, info) \
+do { \
+ struct spa_pod_frame sub_f; \
+ uint32_t version; \
+ \
+ if (spa_pod_parser_push_struct(p, f) < 0 || \
+ spa_pod_parser_get(p, \
+ SPA_POD_Int(&version), \
+ SPA_POD_Int(&(info)->id), \
+ SPA_POD_Int(&(info)->endpoint_id), \
+ SPA_POD_String(&(info)->name), \
+ SPA_POD_Long(&(info)->change_mask), \
+ SPA_POD_Pod(&(info)->link_params), \
+ NULL) < 0) \
+ return -EINVAL; \
+ \
+ (info)->change_mask &= PW_ENDPOINT_STREAM_CHANGE_MASK_ALL; \
+ \
+ parse_dict(p, &sub_f, (info)->props); \
+ parse_param_infos(p, &sub_f, &(info)->n_params, &(info)->params); \
+ \
+ spa_pod_parser_pop(p, f); \
+} while(0)
+
+static void
+marshal_pw_endpoint_link_info(struct spa_pod_builder *b,
+ const struct pw_endpoint_link_info *info)
+{
+ struct spa_pod_frame f;
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_add(b,
+ SPA_POD_Int(info->version),
+ SPA_POD_Int(info->id),
+ SPA_POD_Int(info->session_id),
+ SPA_POD_Int(info->output_endpoint_id),
+ SPA_POD_Int(info->output_stream_id),
+ SPA_POD_Int(info->input_endpoint_id),
+ SPA_POD_Int(info->input_stream_id),
+ SPA_POD_Long(info->change_mask),
+ SPA_POD_Int(info->state),
+ SPA_POD_String(info->error),
+ NULL);
+ push_dict(b, info->props);
+ push_param_infos(b, info->n_params, info->params);
+ spa_pod_builder_pop(b, &f);
+}
+
+/* macro because of alloca() */
+#define demarshal_pw_endpoint_link_info(p, f, info) \
+do { \
+ struct spa_pod_frame sub_f; \
+ uint32_t version; \
+ \
+ if (spa_pod_parser_push_struct(p, f) < 0 || \
+ spa_pod_parser_get(p, \
+ SPA_POD_Int(&version), \
+ SPA_POD_Int(&(info)->id), \
+ SPA_POD_Int(&(info)->session_id), \
+ SPA_POD_Int(&(info)->output_endpoint_id), \
+ SPA_POD_Int(&(info)->output_stream_id), \
+ SPA_POD_Int(&(info)->input_endpoint_id), \
+ SPA_POD_Int(&(info)->input_stream_id), \
+ SPA_POD_Long(&(info)->change_mask), \
+ SPA_POD_Int(&(info)->state), \
+ SPA_POD_String(&(info)->error), \
+ NULL) < 0) \
+ return -EINVAL; \
+ \
+ (info)->change_mask &= PW_ENDPOINT_LINK_CHANGE_MASK_ALL; \
+ \
+ parse_dict(p, &sub_f, (info)->props); \
+ parse_param_infos(p, &sub_f, &(info)->n_params, &(info)->params); \
+ \
+ spa_pod_parser_pop(p, f); \
+} while(0)
+
+
+/***********************************************
+ * COMMON
+ ***********************************************/
+
+static int demarshal_add_listener_enotsup(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ return -ENOTSUP;
+}
+
+/***********************************************
+ * CLIENT ENDPOINT
+ ***********************************************/
+
+static int client_endpoint_marshal_set_session_id (void *object, uint32_t id)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource,
+ PW_CLIENT_ENDPOINT_EVENT_SET_SESSION_ID, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(id));
+
+ return pw_protocol_native_end_resource(resource, b);
+}
+
+static int client_endpoint_marshal_set_param (void *object,
+ uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource,
+ PW_CLIENT_ENDPOINT_EVENT_SET_PARAM, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Id(id),
+ SPA_POD_Int(flags),
+ SPA_POD_Pod(param));
+
+ return pw_protocol_native_end_resource(resource, b);
+}
+
+static int client_endpoint_marshal_stream_set_param (void *object,
+ uint32_t stream_id, uint32_t id,
+ uint32_t flags, const struct spa_pod *param)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource,
+ PW_CLIENT_ENDPOINT_EVENT_STREAM_SET_PARAM, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(stream_id),
+ SPA_POD_Id(id),
+ SPA_POD_Int(flags),
+ SPA_POD_Pod(param));
+
+ return pw_protocol_native_end_resource(resource, b);
+}
+
+static int client_endpoint_marshal_create_link (void *object,
+ const struct spa_dict *props)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource,
+ PW_CLIENT_ENDPOINT_EVENT_CREATE_LINK, NULL);
+
+ push_dict(b, props);
+
+ return pw_protocol_native_end_resource(resource, b);
+}
+
+static int client_endpoint_marshal_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct pw_client_endpoint_events *events,
+ void *data)
+{
+ struct pw_proxy *proxy = object;
+ pw_proxy_add_object_listener(proxy, listener, events, data);
+ return 0;
+}
+
+static int client_endpoint_marshal_update(void *object,
+ uint32_t change_mask,
+ uint32_t n_params,
+ const struct spa_pod **params,
+ const struct pw_endpoint_info *info)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+ struct spa_pod_frame f;
+ uint32_t i;
+
+ b = pw_protocol_native_begin_proxy(proxy,
+ PW_CLIENT_ENDPOINT_METHOD_UPDATE, NULL);
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_add(b,
+ SPA_POD_Int(change_mask),
+ SPA_POD_Int(n_params),
+ NULL);
+
+ for (i = 0; i < n_params; i++)
+ spa_pod_builder_add(b, SPA_POD_Pod(params[i]), NULL);
+
+ if (info)
+ marshal_pw_endpoint_info(b, info);
+ else
+ spa_pod_builder_add(b, SPA_POD_Pod(NULL), NULL);
+
+ spa_pod_builder_pop(b, &f);
+
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int client_endpoint_marshal_stream_update(void *object,
+ uint32_t stream_id,
+ uint32_t change_mask,
+ uint32_t n_params,
+ const struct spa_pod **params,
+ const struct pw_endpoint_stream_info *info)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+ struct spa_pod_frame f;
+ uint32_t i;
+
+ b = pw_protocol_native_begin_proxy(proxy,
+ PW_CLIENT_ENDPOINT_METHOD_STREAM_UPDATE, NULL);
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_add(b,
+ SPA_POD_Int(stream_id),
+ SPA_POD_Int(change_mask),
+ SPA_POD_Int(n_params),
+ NULL);
+
+ for (i = 0; i < n_params; i++)
+ spa_pod_builder_add(b, SPA_POD_Pod(params[i]), NULL);
+
+ if (info)
+ marshal_pw_endpoint_stream_info(b, info);
+ else
+ spa_pod_builder_add(b, SPA_POD_Pod(NULL), NULL);
+
+ spa_pod_builder_pop(b, &f);
+
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int client_endpoint_demarshal_set_session_id(void *data,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ uint32_t id;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&id)) < 0)
+ return -EINVAL;
+
+ return pw_proxy_notify(proxy, struct pw_client_endpoint_events,
+ set_session_id, 0, id);
+}
+
+static int client_endpoint_demarshal_set_param(void *data,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ uint32_t id, flags;
+ const struct spa_pod *param = NULL;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&flags),
+ SPA_POD_PodObject(&param)) < 0)
+ return -EINVAL;
+
+ return pw_proxy_notify(proxy, struct pw_client_endpoint_events,
+ set_param, 0, id, flags, param);
+}
+
+static int client_endpoint_demarshal_stream_set_param(void *data,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ uint32_t stream_id, id, flags;
+ const struct spa_pod *param = NULL;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&stream_id),
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&flags),
+ SPA_POD_PodObject(&param)) < 0)
+ return -EINVAL;
+
+ return pw_proxy_notify(proxy, struct pw_client_endpoint_events,
+ stream_set_param, 0, stream_id, id, flags, param);
+}
+
+static int client_endpoint_demarshal_create_link(void *data,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ struct spa_pod_frame f;
+ struct spa_dict props = SPA_DICT_INIT(NULL, 0);
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+
+ parse_dict(&prs, &f, &props);
+
+ return pw_proxy_notify(proxy, struct pw_client_endpoint_events,
+ create_link, 0, &props);
+}
+
+static int client_endpoint_demarshal_update(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs[2];
+ struct spa_pod_frame f[2];
+ uint32_t change_mask, n_params;
+ const struct spa_pod **params = NULL;
+ struct spa_dict props = SPA_DICT_INIT(NULL, 0);
+ struct pw_endpoint_info info = { .props = &props }, *infop = NULL;
+ struct spa_pod *ipod;
+ uint32_t i;
+
+ spa_pod_parser_init(&prs[0], msg->data, msg->size);
+ if (spa_pod_parser_push_struct(&prs[0], &f[0]) < 0 ||
+ spa_pod_parser_get(&prs[0],
+ SPA_POD_Int(&change_mask),
+ SPA_POD_Int(&n_params), NULL) < 0)
+ return -EINVAL;
+
+ if (n_params > MAX_PARAMS)
+ return -ENOSPC;
+ if (n_params > 0)
+ params = alloca(n_params * sizeof(struct spa_pod *));
+ for (i = 0; i < n_params; i++)
+ if (spa_pod_parser_get(&prs[0],
+ SPA_POD_PodObject(&params[i]), NULL) < 0)
+ return -EINVAL;
+
+ if (spa_pod_parser_get(&prs[0], SPA_POD_PodStruct(&ipod), NULL) < 0)
+ return -EINVAL;
+ if (ipod) {
+ infop = &info;
+ spa_pod_parser_pod(&prs[1], ipod);
+ demarshal_pw_endpoint_info(&prs[1], &f[1], infop);
+ }
+
+ return pw_resource_notify(resource, struct pw_client_endpoint_methods,
+ update, 0, change_mask, n_params, params, infop);
+}
+
+static int client_endpoint_demarshal_stream_update(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs[2];
+ struct spa_pod_frame f[2];
+ uint32_t stream_id, change_mask, n_params;
+ const struct spa_pod **params = NULL;
+ struct spa_dict props = SPA_DICT_INIT(NULL, 0);
+ struct pw_endpoint_stream_info info = { .props = &props }, *infop = NULL;
+ struct spa_pod *ipod;
+ uint32_t i;
+
+ spa_pod_parser_init(&prs[0], msg->data, msg->size);
+ if (spa_pod_parser_push_struct(&prs[0], &f[0]) < 0 ||
+ spa_pod_parser_get(&prs[0],
+ SPA_POD_Int(&stream_id),
+ SPA_POD_Int(&change_mask),
+ SPA_POD_Int(&n_params), NULL) < 0)
+ return -EINVAL;
+
+ if (n_params > MAX_PARAMS)
+ return -ENOSPC;
+ if (n_params > 0)
+ params = alloca(n_params * sizeof(struct spa_pod *));
+ for (i = 0; i < n_params; i++)
+ if (spa_pod_parser_get(&prs[0],
+ SPA_POD_PodObject(&params[i]), NULL) < 0)
+ return -EINVAL;
+
+ if (spa_pod_parser_get(&prs[0], SPA_POD_PodStruct(&ipod), NULL) < 0)
+ return -EINVAL;
+ if (ipod) {
+ infop = &info;
+ spa_pod_parser_pod(&prs[1], ipod);
+ demarshal_pw_endpoint_stream_info(&prs[1], &f[1], infop);
+ }
+
+ return pw_resource_notify(resource, struct pw_client_endpoint_methods,
+ stream_update, 0, stream_id, change_mask, n_params, params, infop);
+}
+
+static const struct pw_client_endpoint_events pw_protocol_native_client_endpoint_event_marshal = {
+ PW_VERSION_CLIENT_ENDPOINT_EVENTS,
+ .set_session_id = client_endpoint_marshal_set_session_id,
+ .set_param = client_endpoint_marshal_set_param,
+ .stream_set_param = client_endpoint_marshal_stream_set_param,
+ .create_link = client_endpoint_marshal_create_link,
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_client_endpoint_event_demarshal[PW_CLIENT_ENDPOINT_EVENT_NUM] =
+{
+ [PW_CLIENT_ENDPOINT_EVENT_SET_SESSION_ID] = { client_endpoint_demarshal_set_session_id, 0 },
+ [PW_CLIENT_ENDPOINT_EVENT_SET_PARAM] = { client_endpoint_demarshal_set_param, 0 },
+ [PW_CLIENT_ENDPOINT_EVENT_STREAM_SET_PARAM] = { client_endpoint_demarshal_stream_set_param, 0 },
+ [PW_CLIENT_ENDPOINT_EVENT_CREATE_LINK] = { client_endpoint_demarshal_create_link, 0 },
+};
+
+static const struct pw_client_endpoint_methods pw_protocol_native_client_endpoint_method_marshal = {
+ PW_VERSION_CLIENT_ENDPOINT_METHODS,
+ .add_listener = client_endpoint_marshal_add_listener,
+ .update = client_endpoint_marshal_update,
+ .stream_update = client_endpoint_marshal_stream_update,
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_client_endpoint_method_demarshal[PW_CLIENT_ENDPOINT_METHOD_NUM] =
+{
+ [PW_CLIENT_ENDPOINT_METHOD_ADD_LISTENER] = { NULL, 0 },
+ [PW_CLIENT_ENDPOINT_METHOD_UPDATE] = { client_endpoint_demarshal_update, 0 },
+ [PW_CLIENT_ENDPOINT_METHOD_STREAM_UPDATE] = { client_endpoint_demarshal_stream_update, 0 },
+};
+
+static const struct pw_protocol_marshal pw_protocol_native_client_endpoint_marshal = {
+ PW_TYPE_INTERFACE_ClientEndpoint,
+ PW_VERSION_CLIENT_ENDPOINT,
+ 0,
+ PW_CLIENT_ENDPOINT_METHOD_NUM,
+ PW_CLIENT_ENDPOINT_EVENT_NUM,
+ &pw_protocol_native_client_endpoint_method_marshal,
+ &pw_protocol_native_client_endpoint_method_demarshal,
+ &pw_protocol_native_client_endpoint_event_marshal,
+ &pw_protocol_native_client_endpoint_event_demarshal,
+};
+
+/***********************************************
+ * CLIENT SESSION
+ ***********************************************/
+
+static int client_session_marshal_set_param (void *data,
+ uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource,
+ PW_CLIENT_SESSION_EVENT_SET_PARAM, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Id(id),
+ SPA_POD_Int(flags),
+ SPA_POD_Pod(param));
+
+ return pw_protocol_native_end_resource(resource, b);
+}
+
+static int client_session_marshal_link_set_param (void *data,
+ uint32_t link_id, uint32_t id,
+ uint32_t flags, const struct spa_pod *param)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource,
+ PW_CLIENT_SESSION_EVENT_LINK_SET_PARAM, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(link_id),
+ SPA_POD_Id(id),
+ SPA_POD_Int(flags),
+ SPA_POD_Pod(param));
+
+ return pw_protocol_native_end_resource(resource, b);
+}
+
+static int client_session_marshal_link_request_state (void *data,
+ uint32_t link_id, uint32_t state)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource,
+ PW_CLIENT_SESSION_EVENT_LINK_REQUEST_STATE, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(link_id),
+ SPA_POD_Int(state));
+
+ return pw_protocol_native_end_resource(resource, b);
+}
+
+static int client_session_marshal_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct pw_client_session_events *events,
+ void *data)
+{
+ struct pw_proxy *proxy = object;
+ pw_proxy_add_object_listener(proxy, listener, events, data);
+ return 0;
+}
+
+static int client_session_marshal_update(void *object,
+ uint32_t change_mask,
+ uint32_t n_params,
+ const struct spa_pod **params,
+ const struct pw_session_info *info)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+ struct spa_pod_frame f;
+ uint32_t i;
+
+ b = pw_protocol_native_begin_proxy(proxy,
+ PW_CLIENT_SESSION_METHOD_UPDATE, NULL);
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_add(b,
+ SPA_POD_Int(change_mask),
+ SPA_POD_Int(n_params),
+ NULL);
+
+ for (i = 0; i < n_params; i++)
+ spa_pod_builder_add(b, SPA_POD_Pod(params[i]), NULL);
+
+ if (info)
+ marshal_pw_session_info(b, info);
+ else
+ spa_pod_builder_add(b, SPA_POD_Pod(NULL), NULL);
+
+ spa_pod_builder_pop(b, &f);
+
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int client_session_marshal_link_update(void *object,
+ uint32_t link_id,
+ uint32_t change_mask,
+ uint32_t n_params,
+ const struct spa_pod **params,
+ const struct pw_endpoint_link_info *info)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+ struct spa_pod_frame f;
+ uint32_t i;
+
+ b = pw_protocol_native_begin_proxy(proxy,
+ PW_CLIENT_SESSION_METHOD_LINK_UPDATE, NULL);
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_add(b,
+ SPA_POD_Int(link_id),
+ SPA_POD_Int(change_mask),
+ SPA_POD_Int(n_params),
+ NULL);
+
+ for (i = 0; i < n_params; i++)
+ spa_pod_builder_add(b, SPA_POD_Pod(params[i]), NULL);
+
+ if (info)
+ marshal_pw_endpoint_link_info(b, info);
+ else
+ spa_pod_builder_add(b, SPA_POD_Pod(NULL), NULL);
+
+ spa_pod_builder_pop(b, &f);
+
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int client_session_demarshal_set_param(void *data,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ uint32_t id, flags;
+ const struct spa_pod *param = NULL;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&flags),
+ SPA_POD_PodObject(&param)) < 0)
+ return -EINVAL;
+
+ return pw_proxy_notify(proxy, struct pw_client_session_events,
+ set_param, 0, id, flags, param);
+}
+
+static int client_session_demarshal_link_set_param(void *data,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ uint32_t link_id, id, flags;
+ const struct spa_pod *param = NULL;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&link_id),
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&flags),
+ SPA_POD_PodObject(&param)) < 0)
+ return -EINVAL;
+
+ return pw_proxy_notify(proxy, struct pw_client_session_events,
+ link_set_param, 0, link_id, id, flags, param);
+}
+
+static int client_session_demarshal_link_request_state(void *data,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ uint32_t link_id, state;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&link_id),
+ SPA_POD_Int(&state)) < 0)
+ return -EINVAL;
+
+ return pw_proxy_notify(proxy, struct pw_client_session_events,
+ link_request_state, 0, link_id, state);
+}
+
+static int client_session_demarshal_update(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs[2];
+ struct spa_pod_frame f[2];
+ uint32_t change_mask, n_params;
+ const struct spa_pod **params = NULL;
+ struct spa_dict props = SPA_DICT_INIT(NULL, 0);
+ struct pw_session_info info = { .props = &props }, *infop = NULL;
+ struct spa_pod *ipod;
+ uint32_t i;
+
+ spa_pod_parser_init(&prs[0], msg->data, msg->size);
+ if (spa_pod_parser_push_struct(&prs[0], &f[0]) < 0 ||
+ spa_pod_parser_get(&prs[0],
+ SPA_POD_Int(&change_mask),
+ SPA_POD_Int(&n_params), NULL) < 0)
+ return -EINVAL;
+
+ if (n_params > MAX_PARAMS)
+ return -ENOSPC;
+ if (n_params > 0)
+ params = alloca(n_params * sizeof(struct spa_pod *));
+ for (i = 0; i < n_params; i++)
+ if (spa_pod_parser_get(&prs[0],
+ SPA_POD_PodObject(&params[i]), NULL) < 0)
+ return -EINVAL;
+
+ if (spa_pod_parser_get(&prs[0], SPA_POD_PodStruct(&ipod), NULL) < 0)
+ return -EINVAL;
+ if (ipod) {
+ infop = &info;
+ spa_pod_parser_pod(&prs[1], ipod);
+ demarshal_pw_session_info(&prs[1], &f[1], infop);
+ }
+
+ return pw_resource_notify(resource, struct pw_client_session_methods,
+ update, 0, change_mask, n_params, params, infop);
+}
+
+static int client_session_demarshal_link_update(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs[2];
+ struct spa_pod_frame f[2];
+ uint32_t link_id, change_mask, n_params;
+ const struct spa_pod **params = NULL;
+ struct spa_dict props = SPA_DICT_INIT(NULL, 0);
+ struct pw_endpoint_link_info info = { .props = &props }, *infop = NULL;
+ struct spa_pod *ipod;
+ uint32_t i;
+
+ spa_pod_parser_init(&prs[0], msg->data, msg->size);
+ if (spa_pod_parser_push_struct(&prs[0], &f[0]) < 0 ||
+ spa_pod_parser_get(&prs[0],
+ SPA_POD_Int(&link_id),
+ SPA_POD_Int(&change_mask),
+ SPA_POD_Int(&n_params), NULL) < 0)
+ return -EINVAL;
+
+ if (n_params > MAX_PARAMS)
+ return -ENOSPC;
+ if (n_params > 0)
+ params = alloca(n_params * sizeof(struct spa_pod *));
+ for (i = 0; i < n_params; i++)
+ if (spa_pod_parser_get(&prs[0],
+ SPA_POD_PodObject(&params[i]), NULL) < 0)
+ return -EINVAL;
+
+ if (spa_pod_parser_get(&prs[0], SPA_POD_PodStruct(&ipod), NULL) < 0)
+ return -EINVAL;
+ if (ipod) {
+ infop = &info;
+ spa_pod_parser_pod(&prs[1], ipod);
+ demarshal_pw_endpoint_link_info(&prs[1], &f[1], infop);
+ }
+
+ return pw_resource_notify(resource, struct pw_client_session_methods,
+ link_update, 0, link_id, change_mask, n_params, params, infop);
+}
+
+static const struct pw_client_session_events pw_protocol_native_client_session_event_marshal = {
+ PW_VERSION_CLIENT_SESSION_EVENTS,
+ .set_param = client_session_marshal_set_param,
+ .link_set_param = client_session_marshal_link_set_param,
+ .link_request_state = client_session_marshal_link_request_state,
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_client_session_event_demarshal[PW_CLIENT_SESSION_EVENT_NUM] =
+{
+ [PW_CLIENT_SESSION_EVENT_SET_PARAM] = { client_session_demarshal_set_param, 0 },
+ [PW_CLIENT_SESSION_EVENT_LINK_SET_PARAM] = { client_session_demarshal_link_set_param, 0 },
+ [PW_CLIENT_SESSION_EVENT_LINK_REQUEST_STATE] = { client_session_demarshal_link_request_state, 0 },
+};
+
+static const struct pw_client_session_methods pw_protocol_native_client_session_method_marshal = {
+ PW_VERSION_CLIENT_SESSION_METHODS,
+ .add_listener = client_session_marshal_add_listener,
+ .update = client_session_marshal_update,
+ .link_update = client_session_marshal_link_update,
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_client_session_method_demarshal[PW_CLIENT_SESSION_METHOD_NUM] =
+{
+ [PW_CLIENT_SESSION_METHOD_ADD_LISTENER] = { NULL, 0 },
+ [PW_CLIENT_SESSION_METHOD_UPDATE] = { client_session_demarshal_update, 0 },
+ [PW_CLIENT_SESSION_METHOD_LINK_UPDATE] = { client_session_demarshal_link_update, 0 },
+};
+
+static const struct pw_protocol_marshal pw_protocol_native_client_session_marshal = {
+ PW_TYPE_INTERFACE_ClientSession,
+ PW_VERSION_CLIENT_SESSION,
+ 0,
+ PW_CLIENT_SESSION_METHOD_NUM,
+ PW_CLIENT_SESSION_EVENT_NUM,
+ &pw_protocol_native_client_session_method_marshal,
+ &pw_protocol_native_client_session_method_demarshal,
+ &pw_protocol_native_client_session_event_marshal,
+ &pw_protocol_native_client_session_event_demarshal,
+};
+
+/***********************************************
+ * ENDPOINT LINK
+ ***********************************************/
+
+static void endpoint_link_proxy_marshal_info (void *data,
+ const struct pw_endpoint_link_info *info)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_proxy(proxy,
+ PW_ENDPOINT_LINK_EVENT_INFO, NULL);
+
+ marshal_pw_endpoint_link_info(b, info);
+
+ pw_protocol_native_end_proxy(proxy, b);
+}
+
+static void endpoint_link_resource_marshal_info (void *data,
+ const struct pw_endpoint_link_info *info)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource,
+ PW_ENDPOINT_LINK_EVENT_INFO, NULL);
+
+ marshal_pw_endpoint_link_info(b, info);
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static void endpoint_link_proxy_marshal_param (void *data, int seq, uint32_t id,
+ uint32_t index, uint32_t next,
+ const struct spa_pod *param)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_proxy(proxy,
+ PW_ENDPOINT_LINK_EVENT_PARAM, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(seq),
+ SPA_POD_Id(id),
+ SPA_POD_Int(index),
+ SPA_POD_Int(next),
+ SPA_POD_Pod(param));
+
+ pw_protocol_native_end_proxy(proxy, b);
+}
+static void endpoint_link_resource_marshal_param (void *data, int seq, uint32_t id,
+ uint32_t index, uint32_t next,
+ const struct spa_pod *param)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource,
+ PW_ENDPOINT_LINK_EVENT_PARAM, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(seq),
+ SPA_POD_Id(id),
+ SPA_POD_Int(index),
+ SPA_POD_Int(next),
+ SPA_POD_Pod(param));
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static int endpoint_link_proxy_marshal_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct pw_endpoint_link_events *events,
+ void *data)
+{
+ struct pw_proxy *proxy = object;
+ pw_proxy_add_object_listener(proxy, listener, events, data);
+ return 0;
+}
+
+static int endpoint_link_resource_marshal_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct pw_endpoint_link_events *events,
+ void *data)
+{
+ struct pw_resource *resource = object;
+ pw_resource_add_object_listener(resource, listener, events, data);
+ return 0;
+}
+
+static int endpoint_link_proxy_marshal_subscribe_params(void *object,
+ uint32_t *ids, uint32_t n_ids)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_proxy(proxy,
+ PW_ENDPOINT_LINK_METHOD_SUBSCRIBE_PARAMS, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Array(sizeof(uint32_t), SPA_TYPE_Id, n_ids, ids));
+
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int endpoint_link_resource_marshal_subscribe_params(void *object,
+ uint32_t *ids, uint32_t n_ids)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource,
+ PW_ENDPOINT_LINK_METHOD_SUBSCRIBE_PARAMS, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Array(sizeof(uint32_t), SPA_TYPE_Id, n_ids, ids));
+
+ return pw_protocol_native_end_resource(resource, b);
+}
+
+static int endpoint_link_proxy_marshal_enum_params(void *object,
+ int seq, uint32_t id,
+ uint32_t index, uint32_t num,
+ const struct spa_pod *filter)
+{
+ struct pw_protocol_native_message *msg;
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_proxy(proxy,
+ PW_ENDPOINT_LINK_METHOD_ENUM_PARAMS, &msg);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(SPA_RESULT_RETURN_ASYNC(msg->seq)),
+ SPA_POD_Id(id),
+ SPA_POD_Int(index),
+ SPA_POD_Int(num),
+ SPA_POD_Pod(filter));
+
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int endpoint_link_resource_marshal_enum_params(void *object,
+ int seq, uint32_t id,
+ uint32_t index, uint32_t num,
+ const struct spa_pod *filter)
+{
+ struct pw_protocol_native_message *msg;
+ struct pw_resource *resource = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource,
+ PW_ENDPOINT_LINK_METHOD_ENUM_PARAMS, &msg);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(SPA_RESULT_RETURN_ASYNC(msg->seq)),
+ SPA_POD_Id(id),
+ SPA_POD_Int(index),
+ SPA_POD_Int(num),
+ SPA_POD_Pod(filter));
+
+ return pw_protocol_native_end_resource(resource, b);
+}
+
+static int endpoint_link_proxy_marshal_set_param(void *object,
+ uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_proxy(proxy,
+ PW_ENDPOINT_LINK_METHOD_SET_PARAM, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Id(id),
+ SPA_POD_Int(flags),
+ SPA_POD_Pod(param));
+
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int endpoint_link_resource_marshal_set_param(void *object,
+ uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource,
+ PW_ENDPOINT_LINK_METHOD_SET_PARAM, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Id(id),
+ SPA_POD_Int(flags),
+ SPA_POD_Pod(param));
+
+ return pw_protocol_native_end_resource(resource, b);
+}
+
+static int endpoint_link_proxy_marshal_request_state(void *object,
+ enum pw_endpoint_link_state state)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_proxy(proxy,
+ PW_ENDPOINT_LINK_METHOD_REQUEST_STATE, NULL);
+
+ spa_pod_builder_add_struct(b, SPA_POD_Int(state));
+
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int endpoint_link_resource_marshal_request_state(void *object,
+ enum pw_endpoint_link_state state)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource,
+ PW_ENDPOINT_LINK_METHOD_REQUEST_STATE, NULL);
+
+ spa_pod_builder_add_struct(b, SPA_POD_Int(state));
+
+ return pw_protocol_native_end_resource(resource, b);
+}
+
+static int endpoint_link_proxy_demarshal_info(void *data,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ struct spa_pod_frame f;
+ struct spa_dict props = SPA_DICT_INIT(NULL, 0);
+ struct pw_endpoint_link_info info = { .props = &props };
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+
+ demarshal_pw_endpoint_link_info(&prs, &f, &info);
+
+ return pw_proxy_notify(proxy, struct pw_endpoint_link_events,
+ info, 0, &info);
+}
+
+static int endpoint_link_resource_demarshal_info(void *data,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_parser prs;
+ struct spa_pod_frame f;
+ struct spa_dict props = SPA_DICT_INIT(NULL, 0);
+ struct pw_endpoint_link_info info = { .props = &props };
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+
+ demarshal_pw_endpoint_link_info(&prs, &f, &info);
+
+ return pw_resource_notify(resource, struct pw_endpoint_link_events,
+ info, 0, &info);
+}
+
+static int endpoint_link_proxy_demarshal_param(void *data,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ uint32_t id, index, next;
+ int seq;
+ struct spa_pod *param;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&seq),
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&index),
+ SPA_POD_Int(&next),
+ SPA_POD_Pod(&param)) < 0)
+ return -EINVAL;
+
+ return pw_proxy_notify(proxy, struct pw_endpoint_link_events,
+ param, 0, seq, id, index, next, param);
+}
+
+static int endpoint_link_resource_demarshal_param(void *data,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_parser prs;
+ uint32_t id, index, next;
+ int seq;
+ struct spa_pod *param;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&seq),
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&index),
+ SPA_POD_Int(&next),
+ SPA_POD_Pod(&param)) < 0)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_endpoint_link_events,
+ param, 0, seq, id, index, next, param);
+}
+
+static int endpoint_link_proxy_demarshal_subscribe_params(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_parser prs;
+ uint32_t csize, ctype, n_ids;
+ uint32_t *ids;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Array(&csize, &ctype, &n_ids, &ids)) < 0)
+ return -EINVAL;
+
+ if (ctype != SPA_TYPE_Id)
+ return -EINVAL;
+
+ return pw_proxy_notify(proxy, struct pw_endpoint_link_methods,
+ subscribe_params, 0, ids, n_ids);
+}
+
+static int endpoint_link_resource_demarshal_subscribe_params(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ uint32_t csize, ctype, n_ids;
+ uint32_t *ids;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Array(&csize, &ctype, &n_ids, &ids)) < 0)
+ return -EINVAL;
+
+ if (ctype != SPA_TYPE_Id)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_endpoint_link_methods,
+ subscribe_params, 0, ids, n_ids);
+}
+
+static int endpoint_link_proxy_demarshal_enum_params(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_parser prs;
+ uint32_t id, index, num;
+ int seq;
+ struct spa_pod *filter;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&seq),
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&index),
+ SPA_POD_Int(&num),
+ SPA_POD_Pod(&filter)) < 0)
+ return -EINVAL;
+
+ return pw_proxy_notify(proxy, struct pw_endpoint_link_methods,
+ enum_params, 0, seq, id, index, num, filter);
+}
+
+static int endpoint_link_resource_demarshal_enum_params(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ uint32_t id, index, num;
+ int seq;
+ struct spa_pod *filter;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&seq),
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&index),
+ SPA_POD_Int(&num),
+ SPA_POD_Pod(&filter)) < 0)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_endpoint_link_methods,
+ enum_params, 0, seq, id, index, num, filter);
+}
+
+static int endpoint_link_proxy_demarshal_set_param(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_parser prs;
+ uint32_t id, flags;
+ struct spa_pod *param;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&flags),
+ SPA_POD_Pod(&param)) < 0)
+ return -EINVAL;
+
+ return pw_proxy_notify(proxy, struct pw_endpoint_link_methods,
+ set_param, 0, id, flags, param);
+}
+
+static int endpoint_link_resource_demarshal_set_param(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ uint32_t id, flags;
+ struct spa_pod *param;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&flags),
+ SPA_POD_Pod(&param)) < 0)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_endpoint_link_methods,
+ set_param, 0, id, flags, param);
+}
+
+static int endpoint_link_proxy_demarshal_request_state(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_parser prs;
+ enum pw_endpoint_link_state state;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&state)) < 0)
+ return -EINVAL;
+
+ return pw_proxy_notify(proxy, struct pw_endpoint_link_methods,
+ request_state, 0, state);
+}
+
+static int endpoint_link_resource_demarshal_request_state(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ enum pw_endpoint_link_state state;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&state)) < 0)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_endpoint_link_methods,
+ request_state, 0, state);
+}
+
+static const struct pw_endpoint_link_events pw_protocol_native_endpoint_link_client_event_marshal = {
+ PW_VERSION_ENDPOINT_LINK_EVENTS,
+ .info = endpoint_link_proxy_marshal_info,
+ .param = endpoint_link_proxy_marshal_param,
+};
+
+static const struct pw_endpoint_link_events pw_protocol_native_endpoint_link_server_event_marshal = {
+ PW_VERSION_ENDPOINT_LINK_EVENTS,
+ .info = endpoint_link_resource_marshal_info,
+ .param = endpoint_link_resource_marshal_param,
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_endpoint_link_client_event_demarshal[PW_ENDPOINT_LINK_EVENT_NUM] =
+{
+ [PW_ENDPOINT_LINK_EVENT_INFO] = { endpoint_link_proxy_demarshal_info, 0 },
+ [PW_ENDPOINT_LINK_EVENT_PARAM] = { endpoint_link_proxy_demarshal_param, 0 },
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_endpoint_link_server_event_demarshal[PW_ENDPOINT_LINK_EVENT_NUM] =
+{
+ [PW_ENDPOINT_LINK_EVENT_INFO] = { endpoint_link_resource_demarshal_info, 0 },
+ [PW_ENDPOINT_LINK_EVENT_PARAM] = { endpoint_link_resource_demarshal_param, 0 },
+};
+
+static const struct pw_endpoint_link_methods pw_protocol_native_endpoint_link_client_method_marshal = {
+ PW_VERSION_ENDPOINT_LINK_METHODS,
+ .add_listener = endpoint_link_proxy_marshal_add_listener,
+ .subscribe_params = endpoint_link_proxy_marshal_subscribe_params,
+ .enum_params = endpoint_link_proxy_marshal_enum_params,
+ .set_param = endpoint_link_proxy_marshal_set_param,
+ .request_state = endpoint_link_proxy_marshal_request_state,
+};
+
+static const struct pw_endpoint_link_methods pw_protocol_native_endpoint_link_server_method_marshal = {
+ PW_VERSION_ENDPOINT_LINK_METHODS,
+ .add_listener = endpoint_link_resource_marshal_add_listener,
+ .subscribe_params = endpoint_link_resource_marshal_subscribe_params,
+ .enum_params = endpoint_link_resource_marshal_enum_params,
+ .set_param = endpoint_link_resource_marshal_set_param,
+ .request_state = endpoint_link_resource_marshal_request_state,
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_endpoint_link_client_method_demarshal[PW_ENDPOINT_LINK_METHOD_NUM] =
+{
+ [PW_ENDPOINT_LINK_METHOD_ADD_LISTENER] = { demarshal_add_listener_enotsup, 0 },
+ [PW_ENDPOINT_LINK_METHOD_SUBSCRIBE_PARAMS] = { endpoint_link_proxy_demarshal_subscribe_params, 0 },
+ [PW_ENDPOINT_LINK_METHOD_ENUM_PARAMS] = { endpoint_link_proxy_demarshal_enum_params, 0 },
+ [PW_ENDPOINT_LINK_METHOD_SET_PARAM] = { endpoint_link_proxy_demarshal_set_param, PW_PERM_W },
+ [PW_ENDPOINT_LINK_METHOD_REQUEST_STATE] = { endpoint_link_proxy_demarshal_request_state, PW_PERM_W },
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_endpoint_link_server_method_demarshal[PW_ENDPOINT_LINK_METHOD_NUM] =
+{
+ [PW_ENDPOINT_LINK_METHOD_ADD_LISTENER] = { demarshal_add_listener_enotsup, 0 },
+ [PW_ENDPOINT_LINK_METHOD_SUBSCRIBE_PARAMS] = { endpoint_link_resource_demarshal_subscribe_params, 0 },
+ [PW_ENDPOINT_LINK_METHOD_ENUM_PARAMS] = { endpoint_link_resource_demarshal_enum_params, 0 },
+ [PW_ENDPOINT_LINK_METHOD_SET_PARAM] = { endpoint_link_resource_demarshal_set_param, PW_PERM_W },
+ [PW_ENDPOINT_LINK_METHOD_REQUEST_STATE] = { endpoint_link_resource_demarshal_request_state, PW_PERM_W },
+};
+
+static const struct pw_protocol_marshal pw_protocol_native_endpoint_link_marshal = {
+ PW_TYPE_INTERFACE_EndpointLink,
+ PW_VERSION_ENDPOINT_LINK,
+ 0,
+ PW_ENDPOINT_LINK_METHOD_NUM,
+ PW_ENDPOINT_LINK_EVENT_NUM,
+ .client_marshal = &pw_protocol_native_endpoint_link_client_method_marshal,
+ .server_demarshal = pw_protocol_native_endpoint_link_server_method_demarshal,
+ .server_marshal = &pw_protocol_native_endpoint_link_server_event_marshal,
+ .client_demarshal = pw_protocol_native_endpoint_link_client_event_demarshal,
+};
+
+static const struct pw_protocol_marshal pw_protocol_native_endpoint_link_impl_marshal = {
+ PW_TYPE_INTERFACE_EndpointLink,
+ PW_VERSION_ENDPOINT_LINK,
+ PW_PROTOCOL_MARSHAL_FLAG_IMPL,
+ PW_ENDPOINT_LINK_EVENT_NUM,
+ PW_ENDPOINT_LINK_METHOD_NUM,
+ .client_marshal = &pw_protocol_native_endpoint_link_client_event_marshal,
+ .server_demarshal = pw_protocol_native_endpoint_link_server_event_demarshal,
+ .server_marshal = &pw_protocol_native_endpoint_link_server_method_marshal,
+ .client_demarshal = pw_protocol_native_endpoint_link_client_method_demarshal,
+};
+
+/***********************************************
+ * ENDPOINT STREAM
+ ***********************************************/
+
+static void endpoint_stream_proxy_marshal_info (void *data,
+ const struct pw_endpoint_stream_info *info)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_proxy(proxy,
+ PW_ENDPOINT_STREAM_EVENT_INFO, NULL);
+
+ marshal_pw_endpoint_stream_info(b, info);
+
+ pw_protocol_native_end_proxy(proxy, b);
+}
+
+static void endpoint_stream_resource_marshal_info (void *data,
+ const struct pw_endpoint_stream_info *info)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource,
+ PW_ENDPOINT_STREAM_EVENT_INFO, NULL);
+
+ marshal_pw_endpoint_stream_info(b, info);
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static void endpoint_stream_proxy_marshal_param (void *data, int seq, uint32_t id,
+ uint32_t index, uint32_t next,
+ const struct spa_pod *param)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_proxy(proxy,
+ PW_ENDPOINT_STREAM_EVENT_PARAM, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(seq),
+ SPA_POD_Id(id),
+ SPA_POD_Int(index),
+ SPA_POD_Int(next),
+ SPA_POD_Pod(param));
+
+ pw_protocol_native_end_proxy(proxy, b);
+}
+
+static void endpoint_stream_resource_marshal_param (void *data, int seq, uint32_t id,
+ uint32_t index, uint32_t next,
+ const struct spa_pod *param)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource,
+ PW_ENDPOINT_STREAM_EVENT_PARAM, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(seq),
+ SPA_POD_Id(id),
+ SPA_POD_Int(index),
+ SPA_POD_Int(next),
+ SPA_POD_Pod(param));
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static int endpoint_stream_proxy_marshal_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct pw_endpoint_stream_events *events,
+ void *data)
+{
+ struct pw_proxy *proxy = object;
+ pw_proxy_add_object_listener(proxy, listener, events, data);
+ return 0;
+}
+
+static int endpoint_stream_resource_marshal_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct pw_endpoint_stream_events *events,
+ void *data)
+{
+ struct pw_resource *resource = object;
+ pw_resource_add_object_listener(resource, listener, events, data);
+ return 0;
+}
+
+static int endpoint_stream_proxy_marshal_subscribe_params(void *object,
+ uint32_t *ids, uint32_t n_ids)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_proxy(proxy,
+ PW_ENDPOINT_STREAM_METHOD_SUBSCRIBE_PARAMS, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Array(sizeof(uint32_t), SPA_TYPE_Id, n_ids, ids));
+
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int endpoint_stream_resource_marshal_subscribe_params(void *object,
+ uint32_t *ids, uint32_t n_ids)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource,
+ PW_ENDPOINT_STREAM_METHOD_SUBSCRIBE_PARAMS, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Array(sizeof(uint32_t), SPA_TYPE_Id, n_ids, ids));
+
+ return pw_protocol_native_end_resource(resource, b);
+}
+
+static int endpoint_stream_proxy_marshal_enum_params(void *object,
+ int seq, uint32_t id,
+ uint32_t index, uint32_t num,
+ const struct spa_pod *filter)
+{
+ struct pw_protocol_native_message *msg;
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_proxy(proxy,
+ PW_ENDPOINT_STREAM_METHOD_ENUM_PARAMS, &msg);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(SPA_RESULT_RETURN_ASYNC(msg->seq)),
+ SPA_POD_Id(id),
+ SPA_POD_Int(index),
+ SPA_POD_Int(num),
+ SPA_POD_Pod(filter));
+
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int endpoint_stream_resource_marshal_enum_params(void *object,
+ int seq, uint32_t id,
+ uint32_t index, uint32_t num,
+ const struct spa_pod *filter)
+{
+ struct pw_protocol_native_message *msg;
+ struct pw_resource *resource = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource,
+ PW_ENDPOINT_STREAM_METHOD_ENUM_PARAMS, &msg);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(SPA_RESULT_RETURN_ASYNC(msg->seq)),
+ SPA_POD_Id(id),
+ SPA_POD_Int(index),
+ SPA_POD_Int(num),
+ SPA_POD_Pod(filter));
+
+ return pw_protocol_native_end_resource(resource, b);
+}
+
+static int endpoint_stream_proxy_marshal_set_param(void *object,
+ uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_proxy(proxy,
+ PW_ENDPOINT_STREAM_METHOD_SET_PARAM, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Id(id),
+ SPA_POD_Int(flags),
+ SPA_POD_Pod(param));
+
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int endpoint_stream_resource_marshal_set_param(void *object,
+ uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource,
+ PW_ENDPOINT_STREAM_METHOD_SET_PARAM, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Id(id),
+ SPA_POD_Int(flags),
+ SPA_POD_Pod(param));
+
+ return pw_protocol_native_end_resource(resource, b);
+}
+
+static int endpoint_stream_proxy_demarshal_info(void *data,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ struct spa_pod_frame f;
+ struct spa_dict props = SPA_DICT_INIT(NULL, 0);
+ struct pw_endpoint_stream_info info = { .props = &props };
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+
+ demarshal_pw_endpoint_stream_info(&prs, &f, &info);
+
+ return pw_proxy_notify(proxy, struct pw_endpoint_stream_events,
+ info, 0, &info);
+}
+
+static int endpoint_stream_resource_demarshal_info(void *data,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_parser prs;
+ struct spa_pod_frame f;
+ struct spa_dict props = SPA_DICT_INIT(NULL, 0);
+ struct pw_endpoint_stream_info info = { .props = &props };
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+
+ demarshal_pw_endpoint_stream_info(&prs, &f, &info);
+
+ return pw_resource_notify(resource, struct pw_endpoint_stream_events,
+ info, 0, &info);
+}
+
+static int endpoint_stream_proxy_demarshal_param(void *data,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ uint32_t id, index, next;
+ int seq;
+ struct spa_pod *param;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&seq),
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&index),
+ SPA_POD_Int(&next),
+ SPA_POD_Pod(&param)) < 0)
+ return -EINVAL;
+
+ return pw_proxy_notify(proxy, struct pw_endpoint_stream_events,
+ param, 0, seq, id, index, next, param);
+}
+
+static int endpoint_stream_resource_demarshal_param(void *data,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_parser prs;
+ uint32_t id, index, next;
+ int seq;
+ struct spa_pod *param;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&seq),
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&index),
+ SPA_POD_Int(&next),
+ SPA_POD_Pod(&param)) < 0)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_endpoint_stream_events,
+ param, 0, seq, id, index, next, param);
+}
+
+static int endpoint_stream_proxy_demarshal_subscribe_params(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_parser prs;
+ uint32_t csize, ctype, n_ids;
+ uint32_t *ids;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Array(&csize, &ctype, &n_ids, &ids)) < 0)
+ return -EINVAL;
+
+ if (ctype != SPA_TYPE_Id)
+ return -EINVAL;
+
+ return pw_proxy_notify(proxy, struct pw_endpoint_stream_methods,
+ subscribe_params, 0, ids, n_ids);
+}
+
+static int endpoint_stream_resource_demarshal_subscribe_params(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ uint32_t csize, ctype, n_ids;
+ uint32_t *ids;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Array(&csize, &ctype, &n_ids, &ids)) < 0)
+ return -EINVAL;
+
+ if (ctype != SPA_TYPE_Id)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_endpoint_stream_methods,
+ subscribe_params, 0, ids, n_ids);
+}
+
+static int endpoint_stream_proxy_demarshal_enum_params(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_parser prs;
+ uint32_t id, index, num;
+ int seq;
+ struct spa_pod *filter;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&seq),
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&index),
+ SPA_POD_Int(&num),
+ SPA_POD_Pod(&filter)) < 0)
+ return -EINVAL;
+
+ return pw_proxy_notify(proxy, struct pw_endpoint_stream_methods,
+ enum_params, 0, seq, id, index, num, filter);
+}
+
+static int endpoint_stream_resource_demarshal_enum_params(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ uint32_t id, index, num;
+ int seq;
+ struct spa_pod *filter;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&seq),
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&index),
+ SPA_POD_Int(&num),
+ SPA_POD_Pod(&filter)) < 0)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_endpoint_stream_methods,
+ enum_params, 0, seq, id, index, num, filter);
+}
+
+static int endpoint_stream_proxy_demarshal_set_param(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_parser prs;
+ uint32_t id, flags;
+ struct spa_pod *param;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&flags),
+ SPA_POD_Pod(&param)) < 0)
+ return -EINVAL;
+
+ return pw_proxy_notify(proxy, struct pw_endpoint_stream_methods,
+ set_param, 0, id, flags, param);
+}
+
+static int endpoint_stream_resource_demarshal_set_param(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ uint32_t id, flags;
+ struct spa_pod *param;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&flags),
+ SPA_POD_Pod(&param)) < 0)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_endpoint_stream_methods,
+ set_param, 0, id, flags, param);
+}
+
+static const struct pw_endpoint_stream_events pw_protocol_native_endpoint_stream_client_event_marshal = {
+ PW_VERSION_ENDPOINT_STREAM_EVENTS,
+ .info = endpoint_stream_proxy_marshal_info,
+ .param = endpoint_stream_proxy_marshal_param,
+};
+
+static const struct pw_endpoint_stream_events pw_protocol_native_endpoint_stream_server_event_marshal = {
+ PW_VERSION_ENDPOINT_STREAM_EVENTS,
+ .info = endpoint_stream_resource_marshal_info,
+ .param = endpoint_stream_resource_marshal_param,
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_endpoint_stream_client_event_demarshal[PW_ENDPOINT_STREAM_EVENT_NUM] =
+{
+ [PW_ENDPOINT_STREAM_EVENT_INFO] = { endpoint_stream_proxy_demarshal_info, 0 },
+ [PW_ENDPOINT_STREAM_EVENT_PARAM] = { endpoint_stream_proxy_demarshal_param, 0 },
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_endpoint_stream_server_event_demarshal[PW_ENDPOINT_STREAM_EVENT_NUM] =
+{
+ [PW_ENDPOINT_STREAM_EVENT_INFO] = { endpoint_stream_resource_demarshal_info, 0 },
+ [PW_ENDPOINT_STREAM_EVENT_PARAM] = { endpoint_stream_resource_demarshal_param, 0 },
+};
+
+static const struct pw_endpoint_stream_methods pw_protocol_native_endpoint_stream_client_method_marshal = {
+ PW_VERSION_ENDPOINT_STREAM_METHODS,
+ .add_listener = endpoint_stream_proxy_marshal_add_listener,
+ .subscribe_params = endpoint_stream_proxy_marshal_subscribe_params,
+ .enum_params = endpoint_stream_proxy_marshal_enum_params,
+ .set_param = endpoint_stream_proxy_marshal_set_param,
+};
+
+static const struct pw_endpoint_stream_methods pw_protocol_native_endpoint_stream_server_method_marshal = {
+ PW_VERSION_ENDPOINT_STREAM_METHODS,
+ .add_listener = endpoint_stream_resource_marshal_add_listener,
+ .subscribe_params = endpoint_stream_resource_marshal_subscribe_params,
+ .enum_params = endpoint_stream_resource_marshal_enum_params,
+ .set_param = endpoint_stream_resource_marshal_set_param,
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_endpoint_stream_client_method_demarshal[PW_ENDPOINT_STREAM_METHOD_NUM] =
+{
+ [PW_ENDPOINT_STREAM_METHOD_ADD_LISTENER] = { demarshal_add_listener_enotsup, 0 },
+ [PW_ENDPOINT_STREAM_METHOD_SUBSCRIBE_PARAMS] = { endpoint_stream_proxy_demarshal_subscribe_params, 0 },
+ [PW_ENDPOINT_STREAM_METHOD_ENUM_PARAMS] = { endpoint_stream_proxy_demarshal_enum_params, 0 },
+ [PW_ENDPOINT_STREAM_METHOD_SET_PARAM] = { endpoint_stream_proxy_demarshal_set_param, PW_PERM_W },
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_endpoint_stream_server_method_demarshal[PW_ENDPOINT_STREAM_METHOD_NUM] =
+{
+ [PW_ENDPOINT_STREAM_METHOD_ADD_LISTENER] = { demarshal_add_listener_enotsup, 0 },
+ [PW_ENDPOINT_STREAM_METHOD_SUBSCRIBE_PARAMS] = { endpoint_stream_resource_demarshal_subscribe_params, 0 },
+ [PW_ENDPOINT_STREAM_METHOD_ENUM_PARAMS] = { endpoint_stream_resource_demarshal_enum_params, 0 },
+ [PW_ENDPOINT_STREAM_METHOD_SET_PARAM] = { endpoint_stream_resource_demarshal_set_param, PW_PERM_W },
+};
+
+static const struct pw_protocol_marshal pw_protocol_native_endpoint_stream_marshal = {
+ PW_TYPE_INTERFACE_EndpointStream,
+ PW_VERSION_ENDPOINT_STREAM,
+ 0,
+ PW_ENDPOINT_STREAM_METHOD_NUM,
+ PW_ENDPOINT_STREAM_EVENT_NUM,
+ .client_marshal = &pw_protocol_native_endpoint_stream_client_method_marshal,
+ .server_demarshal = pw_protocol_native_endpoint_stream_server_method_demarshal,
+ .server_marshal = &pw_protocol_native_endpoint_stream_server_event_marshal,
+ .client_demarshal = pw_protocol_native_endpoint_stream_client_event_demarshal,
+};
+
+static const struct pw_protocol_marshal pw_protocol_native_endpoint_stream_impl_marshal = {
+ PW_TYPE_INTERFACE_EndpointStream,
+ PW_VERSION_ENDPOINT_STREAM,
+ PW_PROTOCOL_MARSHAL_FLAG_IMPL,
+ PW_ENDPOINT_STREAM_EVENT_NUM,
+ PW_ENDPOINT_STREAM_METHOD_NUM,
+ .client_marshal = &pw_protocol_native_endpoint_stream_client_event_marshal,
+ .server_demarshal = pw_protocol_native_endpoint_stream_server_event_demarshal,
+ .server_marshal = &pw_protocol_native_endpoint_stream_server_method_marshal,
+ .client_demarshal = pw_protocol_native_endpoint_stream_client_method_demarshal,
+};
+
+/***********************************************
+ * ENDPOINT
+ ***********************************************/
+
+static void endpoint_proxy_marshal_info (void *data,
+ const struct pw_endpoint_info *info)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_proxy(proxy,
+ PW_ENDPOINT_EVENT_INFO, NULL);
+
+ marshal_pw_endpoint_info(b, info);
+
+ pw_protocol_native_end_proxy(proxy, b);
+}
+
+static void endpoint_resource_marshal_info (void *data,
+ const struct pw_endpoint_info *info)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource,
+ PW_ENDPOINT_EVENT_INFO, NULL);
+
+ marshal_pw_endpoint_info(b, info);
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static void endpoint_proxy_marshal_param (void *data, int seq, uint32_t id,
+ uint32_t index, uint32_t next,
+ const struct spa_pod *param)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_proxy(proxy,
+ PW_ENDPOINT_EVENT_PARAM, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(seq),
+ SPA_POD_Id(id),
+ SPA_POD_Int(index),
+ SPA_POD_Int(next),
+ SPA_POD_Pod(param));
+
+ pw_protocol_native_end_proxy(proxy, b);
+}
+
+static void endpoint_resource_marshal_param (void *data, int seq, uint32_t id,
+ uint32_t index, uint32_t next,
+ const struct spa_pod *param)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource,
+ PW_ENDPOINT_EVENT_PARAM, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(seq),
+ SPA_POD_Id(id),
+ SPA_POD_Int(index),
+ SPA_POD_Int(next),
+ SPA_POD_Pod(param));
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static int endpoint_proxy_marshal_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct pw_endpoint_events *events,
+ void *data)
+{
+ struct pw_proxy *proxy = object;
+ pw_proxy_add_object_listener(proxy, listener, events, data);
+ return 0;
+}
+
+static int endpoint_resource_marshal_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct pw_endpoint_events *events,
+ void *data)
+{
+ struct pw_resource *resource = object;
+ pw_resource_add_object_listener(resource, listener, events, data);
+ return 0;
+}
+
+static int endpoint_proxy_marshal_subscribe_params(void *object,
+ uint32_t *ids, uint32_t n_ids)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_proxy(proxy,
+ PW_ENDPOINT_METHOD_SUBSCRIBE_PARAMS, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Array(sizeof(uint32_t), SPA_TYPE_Id, n_ids, ids));
+
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int endpoint_resource_marshal_subscribe_params(void *object,
+ uint32_t *ids, uint32_t n_ids)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource,
+ PW_ENDPOINT_METHOD_SUBSCRIBE_PARAMS, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Array(sizeof(uint32_t), SPA_TYPE_Id, n_ids, ids));
+
+ return pw_protocol_native_end_resource(resource, b);
+}
+
+static int endpoint_proxy_marshal_enum_params(void *object,
+ int seq, uint32_t id,
+ uint32_t index, uint32_t num,
+ const struct spa_pod *filter)
+{
+ struct pw_protocol_native_message *msg;
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_proxy(proxy,
+ PW_ENDPOINT_METHOD_ENUM_PARAMS, &msg);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(SPA_RESULT_RETURN_ASYNC(msg->seq)),
+ SPA_POD_Id(id),
+ SPA_POD_Int(index),
+ SPA_POD_Int(num),
+ SPA_POD_Pod(filter));
+
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int endpoint_resource_marshal_enum_params(void *object,
+ int seq, uint32_t id,
+ uint32_t index, uint32_t num,
+ const struct spa_pod *filter)
+{
+ struct pw_protocol_native_message *msg;
+ struct pw_resource *resource = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource,
+ PW_ENDPOINT_METHOD_ENUM_PARAMS, &msg);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(SPA_RESULT_RETURN_ASYNC(msg->seq)),
+ SPA_POD_Id(id),
+ SPA_POD_Int(index),
+ SPA_POD_Int(num),
+ SPA_POD_Pod(filter));
+
+ return pw_protocol_native_end_resource(resource, b);
+}
+
+static int endpoint_proxy_marshal_set_param(void *object,
+ uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_proxy(proxy,
+ PW_ENDPOINT_METHOD_SET_PARAM, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Id(id),
+ SPA_POD_Int(flags),
+ SPA_POD_Pod(param));
+
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int endpoint_resource_marshal_set_param(void *object,
+ uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource,
+ PW_ENDPOINT_METHOD_SET_PARAM, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Id(id),
+ SPA_POD_Int(flags),
+ SPA_POD_Pod(param));
+
+ return pw_protocol_native_end_resource(resource, b);
+}
+
+static int endpoint_proxy_marshal_create_link(void *object,
+ const struct spa_dict *props)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_proxy(proxy,
+ PW_ENDPOINT_METHOD_CREATE_LINK, NULL);
+
+ push_dict(b, props);
+
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int endpoint_resource_marshal_create_link(void *object,
+ const struct spa_dict *props)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource,
+ PW_ENDPOINT_METHOD_CREATE_LINK, NULL);
+
+ push_dict(b, props);
+
+ return pw_protocol_native_end_resource(resource, b);
+}
+
+static int endpoint_proxy_demarshal_info(void *data,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ struct spa_pod_frame f;
+ struct spa_dict props = SPA_DICT_INIT(NULL, 0);
+ struct pw_endpoint_info info = { .props = &props };
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+
+ demarshal_pw_endpoint_info(&prs, &f, &info);
+
+ return pw_proxy_notify(proxy, struct pw_endpoint_events,
+ info, 0, &info);
+}
+
+static int endpoint_resource_demarshal_info(void *data,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_parser prs;
+ struct spa_pod_frame f;
+ struct spa_dict props = SPA_DICT_INIT(NULL, 0);
+ struct pw_endpoint_info info = { .props = &props };
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+
+ demarshal_pw_endpoint_info(&prs, &f, &info);
+
+ return pw_resource_notify(resource, struct pw_endpoint_events,
+ info, 0, &info);
+}
+
+static int endpoint_proxy_demarshal_param(void *data,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ uint32_t id, index, next;
+ int seq;
+ struct spa_pod *param;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&seq),
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&index),
+ SPA_POD_Int(&next),
+ SPA_POD_Pod(&param)) < 0)
+ return -EINVAL;
+
+ return pw_proxy_notify(proxy, struct pw_endpoint_events,
+ param, 0, seq, id, index, next, param);
+}
+
+static int endpoint_resource_demarshal_param(void *data,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_parser prs;
+ uint32_t id, index, next;
+ int seq;
+ struct spa_pod *param;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&seq),
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&index),
+ SPA_POD_Int(&next),
+ SPA_POD_Pod(&param)) < 0)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_endpoint_events,
+ param, 0, seq, id, index, next, param);
+}
+
+static int endpoint_proxy_demarshal_subscribe_params(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_parser prs;
+ uint32_t csize, ctype, n_ids;
+ uint32_t *ids;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Array(&csize, &ctype, &n_ids, &ids)) < 0)
+ return -EINVAL;
+
+ if (ctype != SPA_TYPE_Id)
+ return -EINVAL;
+
+ return pw_proxy_notify(proxy, struct pw_endpoint_methods,
+ subscribe_params, 0, ids, n_ids);
+}
+
+static int endpoint_resource_demarshal_subscribe_params(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ uint32_t csize, ctype, n_ids;
+ uint32_t *ids;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Array(&csize, &ctype, &n_ids, &ids)) < 0)
+ return -EINVAL;
+
+ if (ctype != SPA_TYPE_Id)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_endpoint_methods,
+ subscribe_params, 0, ids, n_ids);
+}
+
+static int endpoint_proxy_demarshal_enum_params(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_parser prs;
+ uint32_t id, index, num;
+ int seq;
+ struct spa_pod *filter;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&seq),
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&index),
+ SPA_POD_Int(&num),
+ SPA_POD_Pod(&filter)) < 0)
+ return -EINVAL;
+
+ return pw_proxy_notify(proxy, struct pw_endpoint_methods,
+ enum_params, 0, seq, id, index, num, filter);
+}
+
+static int endpoint_resource_demarshal_enum_params(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ uint32_t id, index, num;
+ int seq;
+ struct spa_pod *filter;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&seq),
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&index),
+ SPA_POD_Int(&num),
+ SPA_POD_Pod(&filter)) < 0)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_endpoint_methods,
+ enum_params, 0, seq, id, index, num, filter);
+}
+
+static int endpoint_proxy_demarshal_set_param(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_parser prs;
+ uint32_t id, flags;
+ struct spa_pod *param;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&flags),
+ SPA_POD_Pod(&param)) < 0)
+ return -EINVAL;
+
+ return pw_proxy_notify(proxy, struct pw_endpoint_methods,
+ set_param, 0, id, flags, param);
+}
+
+static int endpoint_resource_demarshal_set_param(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ uint32_t id, flags;
+ struct spa_pod *param;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&flags),
+ SPA_POD_Pod(&param)) < 0)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_endpoint_methods,
+ set_param, 0, id, flags, param);
+}
+
+static int endpoint_proxy_demarshal_create_link(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_parser prs;
+ struct spa_pod_frame f;
+ struct spa_dict props = SPA_DICT_INIT(NULL, 0);
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+
+ parse_dict(&prs, &f, &props);
+
+ return pw_proxy_notify(proxy, struct pw_endpoint_methods,
+ create_link, 0, &props);
+}
+
+static int endpoint_resource_demarshal_create_link(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ struct spa_pod_frame f;
+ struct spa_dict props = SPA_DICT_INIT(NULL, 0);
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+
+ parse_dict(&prs, &f, &props);
+
+ return pw_resource_notify(resource, struct pw_endpoint_methods,
+ create_link, 0, &props);
+}
+
+static const struct pw_endpoint_events pw_protocol_native_endpoint_client_event_marshal = {
+ PW_VERSION_ENDPOINT_EVENTS,
+ .info = endpoint_proxy_marshal_info,
+ .param = endpoint_proxy_marshal_param,
+};
+
+static const struct pw_endpoint_events pw_protocol_native_endpoint_server_event_marshal = {
+ PW_VERSION_ENDPOINT_EVENTS,
+ .info = endpoint_resource_marshal_info,
+ .param = endpoint_resource_marshal_param,
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_endpoint_client_event_demarshal[PW_ENDPOINT_EVENT_NUM] =
+{
+ [PW_ENDPOINT_EVENT_INFO] = { endpoint_proxy_demarshal_info, 0 },
+ [PW_ENDPOINT_EVENT_PARAM] = { endpoint_proxy_demarshal_param, 0 },
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_endpoint_server_event_demarshal[PW_ENDPOINT_EVENT_NUM] =
+{
+ [PW_ENDPOINT_EVENT_INFO] = { endpoint_resource_demarshal_info, 0 },
+ [PW_ENDPOINT_EVENT_PARAM] = { endpoint_resource_demarshal_param, 0 },
+};
+
+static const struct pw_endpoint_methods pw_protocol_native_endpoint_client_method_marshal = {
+ PW_VERSION_ENDPOINT_METHODS,
+ .add_listener = endpoint_proxy_marshal_add_listener,
+ .subscribe_params = endpoint_proxy_marshal_subscribe_params,
+ .enum_params = endpoint_proxy_marshal_enum_params,
+ .set_param = endpoint_proxy_marshal_set_param,
+ .create_link = endpoint_proxy_marshal_create_link,
+};
+
+static const struct pw_endpoint_methods pw_protocol_native_endpoint_server_method_marshal = {
+ PW_VERSION_ENDPOINT_METHODS,
+ .add_listener = endpoint_resource_marshal_add_listener,
+ .subscribe_params = endpoint_resource_marshal_subscribe_params,
+ .enum_params = endpoint_resource_marshal_enum_params,
+ .set_param = endpoint_resource_marshal_set_param,
+ .create_link = endpoint_resource_marshal_create_link,
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_endpoint_client_method_demarshal[PW_ENDPOINT_METHOD_NUM] =
+{
+ [PW_ENDPOINT_METHOD_ADD_LISTENER] = { demarshal_add_listener_enotsup, 0 },
+ [PW_ENDPOINT_METHOD_SUBSCRIBE_PARAMS] = { endpoint_proxy_demarshal_subscribe_params, 0 },
+ [PW_ENDPOINT_METHOD_ENUM_PARAMS] = { endpoint_proxy_demarshal_enum_params, 0 },
+ [PW_ENDPOINT_METHOD_SET_PARAM] = { endpoint_proxy_demarshal_set_param, PW_PERM_W },
+ [PW_ENDPOINT_METHOD_CREATE_LINK] = { endpoint_proxy_demarshal_create_link, PW_PERM_X },
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_endpoint_server_method_demarshal[PW_ENDPOINT_METHOD_NUM] =
+{
+ [PW_ENDPOINT_METHOD_ADD_LISTENER] = { demarshal_add_listener_enotsup, 0 },
+ [PW_ENDPOINT_METHOD_SUBSCRIBE_PARAMS] = { endpoint_resource_demarshal_subscribe_params, 0 },
+ [PW_ENDPOINT_METHOD_ENUM_PARAMS] = { endpoint_resource_demarshal_enum_params, 0 },
+ [PW_ENDPOINT_METHOD_SET_PARAM] = { endpoint_resource_demarshal_set_param, PW_PERM_W },
+ [PW_ENDPOINT_METHOD_CREATE_LINK] = { endpoint_resource_demarshal_create_link, PW_PERM_X },
+};
+
+static const struct pw_protocol_marshal pw_protocol_native_endpoint_marshal = {
+ PW_TYPE_INTERFACE_Endpoint,
+ PW_VERSION_ENDPOINT,
+ 0,
+ PW_ENDPOINT_METHOD_NUM,
+ PW_ENDPOINT_EVENT_NUM,
+ .client_marshal = &pw_protocol_native_endpoint_client_method_marshal,
+ .server_demarshal = pw_protocol_native_endpoint_server_method_demarshal,
+ .server_marshal = &pw_protocol_native_endpoint_server_event_marshal,
+ .client_demarshal = pw_protocol_native_endpoint_client_event_demarshal,
+};
+
+static const struct pw_protocol_marshal pw_protocol_native_endpoint_impl_marshal = {
+ PW_TYPE_INTERFACE_Endpoint,
+ PW_VERSION_ENDPOINT,
+ PW_PROTOCOL_MARSHAL_FLAG_IMPL,
+ PW_ENDPOINT_EVENT_NUM,
+ PW_ENDPOINT_METHOD_NUM,
+ .client_marshal = &pw_protocol_native_endpoint_client_event_marshal,
+ .server_demarshal = pw_protocol_native_endpoint_server_event_demarshal,
+ .server_marshal = &pw_protocol_native_endpoint_server_method_marshal,
+ .client_demarshal = pw_protocol_native_endpoint_client_method_demarshal,
+};
+
+/***********************************************
+ * SESSION
+ ***********************************************/
+
+static void session_proxy_marshal_info (void *data,
+ const struct pw_session_info *info)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_proxy(proxy,
+ PW_SESSION_EVENT_INFO, NULL);
+
+ marshal_pw_session_info(b, info);
+
+ pw_protocol_native_end_proxy(proxy, b);
+}
+
+static void session_resource_marshal_info (void *data,
+ const struct pw_session_info *info)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource,
+ PW_SESSION_EVENT_INFO, NULL);
+
+ marshal_pw_session_info(b, info);
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static void session_proxy_marshal_param (void *data, int seq, uint32_t id,
+ uint32_t index, uint32_t next,
+ const struct spa_pod *param)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_proxy(proxy,
+ PW_SESSION_EVENT_PARAM, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(seq),
+ SPA_POD_Id(id),
+ SPA_POD_Int(index),
+ SPA_POD_Int(next),
+ SPA_POD_Pod(param));
+
+ pw_protocol_native_end_proxy(proxy, b);
+}
+
+static void session_resource_marshal_param (void *data, int seq, uint32_t id,
+ uint32_t index, uint32_t next,
+ const struct spa_pod *param)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource,
+ PW_SESSION_EVENT_PARAM, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(seq),
+ SPA_POD_Id(id),
+ SPA_POD_Int(index),
+ SPA_POD_Int(next),
+ SPA_POD_Pod(param));
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static int session_proxy_marshal_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct pw_session_events *events,
+ void *data)
+{
+ struct pw_proxy *proxy = object;
+ pw_proxy_add_object_listener(proxy, listener, events, data);
+ return 0;
+}
+
+static int session_resource_marshal_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct pw_session_events *events,
+ void *data)
+{
+ struct pw_resource *resource = object;
+ pw_resource_add_object_listener(resource, listener, events, data);
+ return 0;
+}
+
+static int session_proxy_marshal_subscribe_params(void *object,
+ uint32_t *ids, uint32_t n_ids)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_proxy(proxy,
+ PW_SESSION_METHOD_SUBSCRIBE_PARAMS, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Array(sizeof(uint32_t), SPA_TYPE_Id, n_ids, ids));
+
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int session_resource_marshal_subscribe_params(void *object,
+ uint32_t *ids, uint32_t n_ids)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource,
+ PW_SESSION_METHOD_SUBSCRIBE_PARAMS, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Array(sizeof(uint32_t), SPA_TYPE_Id, n_ids, ids));
+
+ return pw_protocol_native_end_resource(resource, b);
+}
+
+static int session_proxy_marshal_enum_params(void *object,
+ int seq, uint32_t id,
+ uint32_t index, uint32_t num,
+ const struct spa_pod *filter)
+{
+ struct pw_protocol_native_message *msg;
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_proxy(proxy,
+ PW_SESSION_METHOD_ENUM_PARAMS, &msg);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(SPA_RESULT_RETURN_ASYNC(msg->seq)),
+ SPA_POD_Id(id),
+ SPA_POD_Int(index),
+ SPA_POD_Int(num),
+ SPA_POD_Pod(filter));
+
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int session_resource_marshal_enum_params(void *object,
+ int seq, uint32_t id,
+ uint32_t index, uint32_t num,
+ const struct spa_pod *filter)
+{
+ struct pw_protocol_native_message *msg;
+ struct pw_resource *resource = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource,
+ PW_SESSION_METHOD_ENUM_PARAMS, &msg);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(SPA_RESULT_RETURN_ASYNC(msg->seq)),
+ SPA_POD_Id(id),
+ SPA_POD_Int(index),
+ SPA_POD_Int(num),
+ SPA_POD_Pod(filter));
+
+ return pw_protocol_native_end_resource(resource, b);
+}
+
+static int session_proxy_marshal_set_param(void *object,
+ uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_proxy(proxy,
+ PW_SESSION_METHOD_SET_PARAM, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Id(id),
+ SPA_POD_Int(flags),
+ SPA_POD_Pod(param));
+
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int session_resource_marshal_set_param(void *object,
+ uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource,
+ PW_SESSION_METHOD_SET_PARAM, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Id(id),
+ SPA_POD_Int(flags),
+ SPA_POD_Pod(param));
+
+ return pw_protocol_native_end_resource(resource, b);
+}
+
+static int session_proxy_demarshal_info(void *data,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ struct spa_pod_frame f;
+ struct spa_dict props = SPA_DICT_INIT(NULL, 0);
+ struct pw_session_info info = { .props = &props };
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+
+ demarshal_pw_session_info(&prs, &f, &info);
+
+ return pw_proxy_notify(proxy, struct pw_session_events,
+ info, 0, &info);
+}
+
+static int session_resource_demarshal_info(void *data,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_parser prs;
+ struct spa_pod_frame f;
+ struct spa_dict props = SPA_DICT_INIT(NULL, 0);
+ struct pw_session_info info = { .props = &props };
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+
+ demarshal_pw_session_info(&prs, &f, &info);
+
+ return pw_resource_notify(resource, struct pw_session_events,
+ info, 0, &info);
+}
+
+static int session_proxy_demarshal_param(void *data,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ uint32_t id, index, next;
+ int seq;
+ struct spa_pod *param;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&seq),
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&index),
+ SPA_POD_Int(&next),
+ SPA_POD_Pod(&param)) < 0)
+ return -EINVAL;
+
+ return pw_proxy_notify(proxy, struct pw_session_events,
+ param, 0, seq, id, index, next, param);
+}
+
+static int session_resource_demarshal_param(void *data,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_parser prs;
+ uint32_t id, index, next;
+ int seq;
+ struct spa_pod *param;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&seq),
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&index),
+ SPA_POD_Int(&next),
+ SPA_POD_Pod(&param)) < 0)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_session_events,
+ param, 0, seq, id, index, next, param);
+}
+
+static int session_proxy_demarshal_subscribe_params(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_parser prs;
+ uint32_t csize, ctype, n_ids;
+ uint32_t *ids;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Array(&csize, &ctype, &n_ids, &ids)) < 0)
+ return -EINVAL;
+
+ if (ctype != SPA_TYPE_Id)
+ return -EINVAL;
+
+ return pw_proxy_notify(proxy, struct pw_session_methods,
+ subscribe_params, 0, ids, n_ids);
+}
+
+static int session_resource_demarshal_subscribe_params(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ uint32_t csize, ctype, n_ids;
+ uint32_t *ids;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Array(&csize, &ctype, &n_ids, &ids)) < 0)
+ return -EINVAL;
+
+ if (ctype != SPA_TYPE_Id)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_session_methods,
+ subscribe_params, 0, ids, n_ids);
+}
+
+static int session_proxy_demarshal_enum_params(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_parser prs;
+ uint32_t id, index, num;
+ int seq;
+ struct spa_pod *filter;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&seq),
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&index),
+ SPA_POD_Int(&num),
+ SPA_POD_Pod(&filter)) < 0)
+ return -EINVAL;
+
+ return pw_proxy_notify(proxy, struct pw_session_methods,
+ enum_params, 0, seq, id, index, num, filter);
+}
+
+static int session_resource_demarshal_enum_params(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ uint32_t id, index, num;
+ int seq;
+ struct spa_pod *filter;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&seq),
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&index),
+ SPA_POD_Int(&num),
+ SPA_POD_Pod(&filter)) < 0)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_session_methods,
+ enum_params, 0, seq, id, index, num, filter);
+}
+
+static int session_proxy_demarshal_set_param(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_parser prs;
+ uint32_t id, flags;
+ struct spa_pod *param;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&flags),
+ SPA_POD_Pod(&param)) < 0)
+ return -EINVAL;
+
+ return pw_proxy_notify(proxy, struct pw_session_methods,
+ set_param, 0, id, flags, param);
+}
+
+static int session_resource_demarshal_set_param(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ uint32_t id, flags;
+ struct spa_pod *param;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&flags),
+ SPA_POD_Pod(&param)) < 0)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_session_methods,
+ set_param, 0, id, flags, param);
+}
+
+static const struct pw_session_events pw_protocol_native_session_client_event_marshal = {
+ PW_VERSION_SESSION_EVENTS,
+ .info = session_proxy_marshal_info,
+ .param = session_proxy_marshal_param,
+};
+
+static const struct pw_session_events pw_protocol_native_session_server_event_marshal = {
+ PW_VERSION_SESSION_EVENTS,
+ .info = session_resource_marshal_info,
+ .param = session_resource_marshal_param,
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_session_client_event_demarshal[PW_SESSION_EVENT_NUM] =
+{
+ [PW_SESSION_EVENT_INFO] = { session_proxy_demarshal_info, 0 },
+ [PW_SESSION_EVENT_PARAM] = { session_proxy_demarshal_param, 0 },
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_session_server_event_demarshal[PW_SESSION_EVENT_NUM] =
+{
+ [PW_SESSION_EVENT_INFO] = { session_resource_demarshal_info, 0 },
+ [PW_SESSION_EVENT_PARAM] = { session_resource_demarshal_param, 0 },
+};
+
+static const struct pw_session_methods pw_protocol_native_session_client_method_marshal = {
+ PW_VERSION_SESSION_METHODS,
+ .add_listener = session_proxy_marshal_add_listener,
+ .subscribe_params = session_proxy_marshal_subscribe_params,
+ .enum_params = session_proxy_marshal_enum_params,
+ .set_param = session_proxy_marshal_set_param,
+};
+
+static const struct pw_session_methods pw_protocol_native_session_server_method_marshal = {
+ PW_VERSION_SESSION_METHODS,
+ .add_listener = session_resource_marshal_add_listener,
+ .subscribe_params = session_resource_marshal_subscribe_params,
+ .enum_params = session_resource_marshal_enum_params,
+ .set_param = session_resource_marshal_set_param,
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_session_client_method_demarshal[PW_SESSION_METHOD_NUM] =
+{
+ [PW_SESSION_METHOD_ADD_LISTENER] = { demarshal_add_listener_enotsup, 0 },
+ [PW_SESSION_METHOD_SUBSCRIBE_PARAMS] = { session_proxy_demarshal_subscribe_params, 0 },
+ [PW_SESSION_METHOD_ENUM_PARAMS] = { session_proxy_demarshal_enum_params, 0 },
+ [PW_SESSION_METHOD_SET_PARAM] = { session_proxy_demarshal_set_param, PW_PERM_W },
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_session_server_method_demarshal[PW_SESSION_METHOD_NUM] =
+{
+ [PW_SESSION_METHOD_ADD_LISTENER] = { demarshal_add_listener_enotsup, 0 },
+ [PW_SESSION_METHOD_SUBSCRIBE_PARAMS] = { session_resource_demarshal_subscribe_params, 0 },
+ [PW_SESSION_METHOD_ENUM_PARAMS] = { session_resource_demarshal_enum_params, 0 },
+ [PW_SESSION_METHOD_SET_PARAM] = { session_resource_demarshal_set_param, PW_PERM_W },
+};
+
+static const struct pw_protocol_marshal pw_protocol_native_session_marshal = {
+ PW_TYPE_INTERFACE_Session,
+ PW_VERSION_SESSION,
+ 0,
+ PW_SESSION_METHOD_NUM,
+ PW_SESSION_EVENT_NUM,
+ .client_marshal = &pw_protocol_native_session_client_method_marshal,
+ .server_demarshal = pw_protocol_native_session_server_method_demarshal,
+ .server_marshal = &pw_protocol_native_session_server_event_marshal,
+ .client_demarshal = pw_protocol_native_session_client_event_demarshal,
+};
+
+static const struct pw_protocol_marshal pw_protocol_native_session_impl_marshal = {
+ PW_TYPE_INTERFACE_Session,
+ PW_VERSION_SESSION,
+ PW_PROTOCOL_MARSHAL_FLAG_IMPL,
+ PW_SESSION_EVENT_NUM,
+ PW_SESSION_METHOD_NUM,
+ .client_marshal = &pw_protocol_native_session_client_event_marshal,
+ .server_demarshal = pw_protocol_native_session_server_event_demarshal,
+ .server_marshal = &pw_protocol_native_session_server_method_marshal,
+ .client_demarshal = pw_protocol_native_session_client_method_demarshal,
+};
+
+int pw_protocol_native_ext_session_manager_init(struct pw_context *context)
+{
+ struct pw_protocol *protocol;
+
+ protocol = pw_context_find_protocol(context, PW_TYPE_INFO_PROTOCOL_Native);
+ if (protocol == NULL)
+ return -EPROTO;
+
+ /* deprecated */
+ pw_protocol_add_marshal(protocol, &pw_protocol_native_client_endpoint_marshal);
+ pw_protocol_add_marshal(protocol, &pw_protocol_native_client_session_marshal);
+
+ /* client <-> server */
+ pw_protocol_add_marshal(protocol, &pw_protocol_native_endpoint_link_marshal);
+ pw_protocol_add_marshal(protocol, &pw_protocol_native_endpoint_stream_marshal);
+ pw_protocol_add_marshal(protocol, &pw_protocol_native_endpoint_marshal);
+ pw_protocol_add_marshal(protocol, &pw_protocol_native_session_marshal);
+
+ /* impl <-> server */
+ pw_protocol_add_marshal(protocol, &pw_protocol_native_endpoint_link_impl_marshal);
+ pw_protocol_add_marshal(protocol, &pw_protocol_native_endpoint_stream_impl_marshal);
+ pw_protocol_add_marshal(protocol, &pw_protocol_native_endpoint_impl_marshal);
+ pw_protocol_add_marshal(protocol, &pw_protocol_native_session_impl_marshal);
+
+ return 0;
+}
diff --git a/src/modules/module-session-manager/proxy-session-manager.c b/src/modules/module-session-manager/proxy-session-manager.c
new file mode 100644
index 0000000..cd418e1
--- /dev/null
+++ b/src/modules/module-session-manager/proxy-session-manager.c
@@ -0,0 +1,188 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Collabora Ltd.
+ * @author George Kiagiadakis <george.kiagiadakis@collabora.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "pipewire/pipewire.h"
+#include "pipewire/extensions/session-manager.h"
+
+struct object_data {
+ struct spa_hook object_listener;
+ struct spa_hook object_methods;
+ struct spa_hook proxy_listener;
+};
+
+static void proxy_object_destroy(void *_data)
+{
+ struct object_data *data = _data;
+ spa_hook_remove(&data->object_listener);
+}
+
+static const struct pw_proxy_events proxy_events = {
+ PW_VERSION_PROXY_EVENTS,
+ .destroy = proxy_object_destroy,
+};
+
+struct pw_proxy *pw_core_endpoint_export(struct pw_core *core,
+ const char *type, const struct spa_dict *props, void *object,
+ size_t user_data_size)
+{
+ struct pw_endpoint *endpoint = object;
+ struct spa_interface *remote_iface, *local_iface;
+ struct pw_proxy *proxy;
+ struct object_data *data;
+
+ proxy = pw_core_create_object(core,
+ "endpoint",
+ PW_TYPE_INTERFACE_Endpoint,
+ PW_VERSION_ENDPOINT,
+ props,
+ user_data_size + sizeof(struct object_data));
+ if (proxy == NULL)
+ return NULL;
+
+ data = pw_proxy_get_user_data(proxy);
+ data = SPA_PTROFF(data, user_data_size, struct object_data);
+
+ remote_iface = (struct spa_interface*)proxy;
+ local_iface = (struct spa_interface*)endpoint;
+
+ pw_proxy_install_marshal(proxy, true);
+
+ pw_proxy_add_listener(proxy, &data->proxy_listener, &proxy_events, data);
+
+ pw_proxy_add_object_listener(proxy, &data->object_methods,
+ local_iface->cb.funcs, local_iface->cb.data);
+ pw_endpoint_add_listener(endpoint, &data->object_listener,
+ remote_iface->cb.funcs, remote_iface->cb.data);
+
+ return proxy;
+}
+
+struct pw_proxy *pw_core_endpoint_stream_export(struct pw_core *core,
+ const char *type, const struct spa_dict *props, void *object,
+ size_t user_data_size)
+{
+ struct pw_endpoint_stream *endpoint_stream = object;
+ struct spa_interface *remote_iface, *local_iface;
+ struct pw_proxy *proxy;
+ struct object_data *data;
+
+ proxy = pw_core_create_object(core,
+ "endpoint-stream",
+ PW_TYPE_INTERFACE_EndpointStream,
+ PW_VERSION_ENDPOINT_STREAM,
+ props,
+ user_data_size + sizeof(struct object_data));
+ if (proxy == NULL)
+ return NULL;
+
+ data = pw_proxy_get_user_data(proxy);
+ data = SPA_PTROFF(data, user_data_size, struct object_data);
+
+ remote_iface = (struct spa_interface*)proxy;
+ local_iface = (struct spa_interface*)endpoint_stream;
+
+ pw_proxy_install_marshal(proxy, true);
+
+ pw_proxy_add_listener(proxy, &data->proxy_listener, &proxy_events, data);
+
+ pw_proxy_add_object_listener(proxy, &data->object_methods,
+ local_iface->cb.funcs, local_iface->cb.data);
+ pw_endpoint_stream_add_listener(endpoint_stream, &data->object_listener,
+ remote_iface->cb.funcs, remote_iface->cb.data);
+
+ return proxy;
+}
+
+struct pw_proxy *pw_core_endpoint_link_export(struct pw_core *core,
+ const char *type, const struct spa_dict *props, void *object,
+ size_t user_data_size)
+{
+ struct pw_endpoint_link *endpoint_link = object;
+ struct spa_interface *remote_iface, *local_iface;
+ struct pw_proxy *proxy;
+ struct object_data *data;
+
+ proxy = pw_core_create_object(core,
+ "endpoint-link",
+ PW_TYPE_INTERFACE_EndpointLink,
+ PW_VERSION_ENDPOINT_LINK,
+ props,
+ user_data_size + sizeof(struct object_data));
+ if (proxy == NULL)
+ return NULL;
+
+ data = pw_proxy_get_user_data(proxy);
+ data = SPA_PTROFF(data, user_data_size, struct object_data);
+
+ remote_iface = (struct spa_interface*)proxy;
+ local_iface = (struct spa_interface*)endpoint_link;
+
+ pw_proxy_install_marshal(proxy, true);
+
+ pw_proxy_add_listener(proxy, &data->proxy_listener, &proxy_events, data);
+
+ pw_proxy_add_object_listener(proxy, &data->object_methods,
+ local_iface->cb.funcs, local_iface->cb.data);
+ pw_endpoint_link_add_listener(endpoint_link, &data->object_listener,
+ remote_iface->cb.funcs, remote_iface->cb.data);
+
+ return proxy;
+}
+
+struct pw_proxy *pw_core_session_export(struct pw_core *core,
+ const char *type, const struct spa_dict *props, void *object,
+ size_t user_data_size)
+{
+ struct pw_session *session = object;
+ struct spa_interface *remote_iface, *local_iface;
+ struct pw_proxy *proxy;
+ struct object_data *data;
+
+ proxy = pw_core_create_object(core,
+ "session",
+ PW_TYPE_INTERFACE_Session,
+ PW_VERSION_SESSION,
+ props,
+ user_data_size + sizeof(struct object_data));
+ if (proxy == NULL)
+ return NULL;
+
+ data = pw_proxy_get_user_data(proxy);
+ data = SPA_PTROFF(data, user_data_size, struct object_data);
+
+ remote_iface = (struct spa_interface*)proxy;
+ local_iface = (struct spa_interface*)session;
+
+ pw_proxy_install_marshal(proxy, true);
+
+ pw_proxy_add_listener(proxy, &data->proxy_listener, &proxy_events, data);
+
+ pw_proxy_add_object_listener(proxy, &data->object_methods,
+ local_iface->cb.funcs, local_iface->cb.data);
+ pw_session_add_listener(session, &data->object_listener,
+ remote_iface->cb.funcs, remote_iface->cb.data);
+
+ return proxy;
+}
diff --git a/src/modules/module-session-manager/session.c b/src/modules/module-session-manager/session.c
new file mode 100644
index 0000000..e3d7210
--- /dev/null
+++ b/src/modules/module-session-manager/session.c
@@ -0,0 +1,578 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Collabora Ltd.
+ * @author George Kiagiadakis <george.kiagiadakis@collabora.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <pipewire/impl.h>
+#include <pipewire/extensions/session-manager.h>
+#include <pipewire/extensions/session-manager/introspect-funcs.h>
+
+#include <spa/utils/result.h>
+#include <spa/pod/builder.h>
+#include <spa/pod/filter.h>
+
+#define MAX_PARAMS 32
+
+#define NAME "session"
+
+struct pw_proxy *pw_core_session_export(struct pw_core *core,
+ const char *type, const struct spa_dict *props, void *object,
+ size_t user_data_size);
+
+struct impl
+{
+ struct pw_global *global;
+ struct spa_hook global_listener;
+
+ union {
+ struct pw_session *session;
+ struct pw_resource *resource;
+ };
+ struct spa_hook resource_listener;
+ struct spa_hook session_listener;
+
+ struct pw_session_info *cached_info;
+ struct spa_list cached_params;
+
+ int ping_seq;
+ bool registered;
+};
+
+struct param_data
+{
+ struct spa_list link;
+ uint32_t id;
+ struct pw_array params;
+};
+
+struct resource_data
+{
+ struct impl *impl;
+
+ struct pw_resource *resource;
+ struct spa_hook object_listener;
+
+ uint32_t n_subscribe_ids;
+ uint32_t subscribe_ids[32];
+};
+
+struct factory_data
+{
+ struct pw_impl_module *module;
+ struct spa_hook module_listener;
+
+ struct pw_impl_factory *factory;
+ struct spa_hook factory_listener;
+
+ struct pw_export_type export;
+};
+
+#define pw_session_resource(r,m,v,...) \
+ pw_resource_call(r,struct pw_session_events,m,v,__VA_ARGS__)
+
+#define pw_session_resource_info(r,...) \
+ pw_session_resource(r,info,0,__VA_ARGS__)
+#define pw_session_resource_param(r,...) \
+ pw_session_resource(r,param,0,__VA_ARGS__)
+
+static int method_enum_params(void *object, int seq,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+ struct resource_data *d = object;
+ struct impl *impl = d->impl;
+ struct param_data *pdata;
+ struct spa_pod *result;
+ struct spa_pod *param;
+ uint8_t buffer[1024];
+ struct spa_pod_builder b = { 0 };
+ uint32_t index;
+ uint32_t next = start;
+ uint32_t count = 0;
+
+ pw_log_debug(NAME" %p: param %u %d/%d", impl, id, start, num);
+
+ spa_list_for_each(pdata, &impl->cached_params, link) {
+ if (pdata->id != id)
+ continue;
+
+ while (true) {
+ index = next++;
+ if (index >= pw_array_get_len(&pdata->params, void*))
+ return 0;
+
+ param = *pw_array_get_unchecked(&pdata->params, index, struct spa_pod*);
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ if (spa_pod_filter(&b, &result, param, filter) != 0)
+ continue;
+
+ pw_log_debug(NAME" %p: %d param %u", impl, seq, index);
+
+ pw_session_resource_param(d->resource, seq, id, index, next, result);
+
+ if (++count == num)
+ return 0;
+ }
+ }
+
+ return 0;
+}
+
+static int method_subscribe_params(void *object, uint32_t *ids, uint32_t n_ids)
+{
+ struct resource_data *d = object;
+ struct impl *impl = d->impl;
+ uint32_t i;
+
+ n_ids = SPA_MIN(n_ids, SPA_N_ELEMENTS(d->subscribe_ids));
+ d->n_subscribe_ids = n_ids;
+
+ for (i = 0; i < n_ids; i++) {
+ d->subscribe_ids[i] = ids[i];
+ pw_log_debug(NAME" %p: resource %d subscribe param %u",
+ impl, pw_resource_get_id(d->resource), ids[i]);
+ method_enum_params(object, 1, ids[i], 0, UINT32_MAX, NULL);
+ }
+ return 0;
+}
+
+static int method_set_param(void *object, uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct resource_data *d = object;
+ struct impl *impl = d->impl;
+ /* store only on the implementation; our cache will be updated
+ by the param event, since we are subscribed */
+ pw_session_set_param(impl->session, id, flags, param);
+ return 0;
+}
+
+static const struct pw_session_methods session_methods = {
+ PW_VERSION_SESSION_METHODS,
+ .subscribe_params = method_subscribe_params,
+ .enum_params = method_enum_params,
+ .set_param = method_set_param,
+};
+
+static int global_bind(void *object, struct pw_impl_client *client,
+ uint32_t permissions, uint32_t version, uint32_t id)
+{
+ struct impl *impl = object;
+ struct pw_resource *resource;
+ struct resource_data *data;
+
+ resource = pw_resource_new(client, id, permissions,
+ PW_TYPE_INTERFACE_Session,
+ version, sizeof(*data));
+ if (resource == NULL)
+ return -errno;
+
+ data = pw_resource_get_user_data(resource);
+ data->impl = impl;
+ data->resource = resource;
+
+ pw_global_add_resource(impl->global, resource);
+
+ /* resource methods -> implementation */
+ pw_resource_add_object_listener(resource,
+ &data->object_listener,
+ &session_methods, data);
+
+ impl->cached_info->change_mask = PW_SESSION_CHANGE_MASK_ALL;
+ pw_session_resource_info(resource, impl->cached_info);
+ impl->cached_info->change_mask = 0;
+
+ return 0;
+}
+
+static void global_destroy(void *data)
+{
+ struct impl *impl = data;
+ spa_hook_remove(&impl->global_listener);
+ impl->global = NULL;
+ if (impl->resource)
+ pw_resource_destroy(impl->resource);
+}
+
+static const struct pw_global_events global_events = {
+ PW_VERSION_GLOBAL_EVENTS,
+ .destroy = global_destroy,
+};
+
+static void impl_resource_destroy(void *data)
+{
+ struct impl *impl = data;
+ struct param_data *pdata, *tmp;
+
+ spa_hook_remove(&impl->resource_listener);
+ impl->resource = NULL;
+
+ /* clear cache */
+ if (impl->cached_info)
+ pw_session_info_free(impl->cached_info);
+ spa_list_for_each_safe(pdata, tmp, &impl->cached_params, link) {
+ struct spa_pod **pod;
+ pw_array_for_each(pod, &pdata->params)
+ free(*pod);
+ pw_array_clear(&pdata->params);
+ spa_list_remove(&pdata->link);
+ free(pdata);
+ }
+
+ if (impl->global)
+ pw_global_destroy(impl->global);
+}
+
+static void register_global(struct impl *impl)
+{
+ impl->cached_info->id = pw_global_get_id (impl->global);
+ pw_resource_set_bound_id(impl->resource, impl->cached_info->id);
+ pw_global_register(impl->global);
+ impl->registered = true;
+}
+
+static void impl_resource_pong (void *data, int seq)
+{
+ struct impl *impl = data;
+
+ /* complete registration, if this was the initial sync */
+ if (!impl->registered && seq == impl->ping_seq) {
+ register_global(impl);
+ }
+}
+
+static const struct pw_resource_events impl_resource_events = {
+ PW_VERSION_RESOURCE_EVENTS,
+ .destroy = impl_resource_destroy,
+ .pong = impl_resource_pong,
+};
+
+static int emit_info(void *data, struct pw_resource *resource)
+{
+ const struct pw_session_info *info = data;
+ pw_session_resource_info(resource, info);
+ return 0;
+}
+
+static void event_info(void *data, const struct pw_session_info *info)
+{
+ struct impl *impl = data;
+ uint32_t changed_ids[MAX_PARAMS], n_changed_ids = 0;
+ uint32_t i;
+
+ /* figure out changes to params */
+ if (info->change_mask & PW_SESSION_CHANGE_MASK_PARAMS) {
+ for (i = 0; i < info->n_params; i++) {
+ if ((!impl->cached_info ||
+ info->params[i].flags != impl->cached_info->params[i].flags)
+ && info->params[i].flags & SPA_PARAM_INFO_READ)
+ changed_ids[n_changed_ids++] = info->params[i].id;
+ }
+ }
+
+ /* cache for new clients */
+ impl->cached_info = pw_session_info_update (impl->cached_info, info);
+
+ /* notify existing clients */
+ pw_global_for_each_resource(impl->global, emit_info, (void*) info);
+
+ /* cache params & register */
+ if (n_changed_ids > 0) {
+ /* prepare params storage */
+ for (i = 0; i < n_changed_ids; i++) {
+ struct param_data *pdata = calloc(1, sizeof(struct param_data));
+ pdata->id = changed_ids[i];
+ pw_array_init(&pdata->params, sizeof(void*));
+ spa_list_append(&impl->cached_params, &pdata->link);
+ }
+
+ /* subscribe to impl */
+ pw_session_subscribe_params(impl->session, changed_ids, n_changed_ids);
+
+ /* register asynchronously on the pong event */
+ impl->ping_seq = pw_resource_ping(impl->resource, 0);
+ }
+ else if (!impl->registered) {
+ register_global(impl);
+ }
+}
+
+struct param_event_args
+{
+ uint32_t id, index, next;
+ const struct spa_pod *param;
+};
+
+static int emit_param(void *_data, struct pw_resource *resource)
+{
+ struct param_event_args *args = _data;
+ struct resource_data *data;
+ uint32_t i;
+
+ data = pw_resource_get_user_data(resource);
+ for (i = 0; i < data->n_subscribe_ids; i++) {
+ if (data->subscribe_ids[i] == args->id) {
+ pw_session_resource_param(resource, 1,
+ args->id, args->index, args->next, args->param);
+ }
+ }
+ return 0;
+}
+
+static void event_param(void *data, int seq,
+ uint32_t id, uint32_t index, uint32_t next,
+ const struct spa_pod *param)
+{
+ struct impl *impl = data;
+ struct param_data *pdata;
+ struct spa_pod **pod;
+ struct param_event_args args = { id, index, next, param };
+
+ /* cache for new requests */
+ spa_list_for_each(pdata, &impl->cached_params, link) {
+ if (pdata->id != id)
+ continue;
+
+ if (!pw_array_check_index(&pdata->params, index, void*)) {
+ while (pw_array_get_len(&pdata->params, void*) <= index)
+ pw_array_add_ptr(&pdata->params, NULL);
+ }
+
+ pod = pw_array_get_unchecked(&pdata->params, index, struct spa_pod*);
+ free(*pod);
+ *pod = spa_pod_copy(param);
+ }
+
+ /* notify existing clients */
+ pw_global_for_each_resource(impl->global, emit_param, &args);
+}
+
+static const struct pw_session_events session_events = {
+ PW_VERSION_SESSION_EVENTS,
+ .info = event_info,
+ .param = event_param,
+};
+
+static void *session_new(struct pw_context *context,
+ struct pw_resource *resource,
+ struct pw_properties *properties)
+{
+ struct impl *impl;
+ char serial_str[32];
+ struct spa_dict_item items[1] = {
+ SPA_DICT_ITEM_INIT(PW_KEY_OBJECT_SERIAL, serial_str),
+ };
+ struct spa_dict extra_props = SPA_DICT_INIT_ARRAY(items);
+ static const char * const keys[] = {
+ PW_KEY_OBJECT_SERIAL,
+ NULL
+ };
+
+ impl = calloc(1, sizeof(*impl));
+ if (impl == NULL) {
+ pw_properties_free(properties);
+ return NULL;
+ }
+
+ impl->global = pw_global_new(context,
+ PW_TYPE_INTERFACE_Session,
+ PW_VERSION_SESSION,
+ properties,
+ global_bind, impl);
+ if (impl->global == NULL) {
+ free(impl);
+ return NULL;
+ }
+ impl->resource = resource;
+
+ spa_scnprintf(serial_str, sizeof(serial_str), "%"PRIu64,
+ pw_global_get_serial(impl->global));
+ pw_global_update_keys(impl->global, &extra_props, keys);
+
+ spa_list_init(&impl->cached_params);
+
+ /* handle destroy events */
+ pw_global_add_listener(impl->global,
+ &impl->global_listener,
+ &global_events, impl);
+ pw_resource_add_listener(impl->resource,
+ &impl->resource_listener,
+ &impl_resource_events, impl);
+
+ /* handle implementation events -> cache + client resources */
+ pw_session_add_listener(impl->session,
+ &impl->session_listener,
+ &session_events, impl);
+
+ /* global is not registered here on purpose;
+ we first cache info + params and then expose the global */
+
+ return impl;
+}
+
+static void *create_object(void *data,
+ struct pw_resource *resource,
+ const char *type,
+ uint32_t version,
+ struct pw_properties *properties,
+ uint32_t new_id)
+{
+ struct factory_data *d = data;
+ struct pw_resource *impl_resource;
+ struct pw_impl_client *client = pw_resource_get_client(resource);
+ void *result;
+ int res;
+
+ impl_resource = pw_resource_new(client, new_id, PW_PERM_ALL, type, version, 0);
+ if (impl_resource == NULL) {
+ res = -errno;
+ goto error_resource;
+ }
+
+ pw_resource_install_marshal(impl_resource, true);
+
+ if (properties == NULL)
+ properties = pw_properties_new(NULL, NULL);
+ if (properties == NULL) {
+ res = -ENOMEM;
+ goto error_session;
+ }
+
+ pw_properties_setf(properties, PW_KEY_CLIENT_ID, "%d",
+ pw_impl_client_get_info(client)->id);
+ pw_properties_setf(properties, PW_KEY_FACTORY_ID, "%d",
+ pw_impl_factory_get_info(d->factory)->id);
+
+ result = session_new(pw_impl_client_get_context(client), impl_resource, properties);
+ if (result == NULL) {
+ res = -errno;
+ goto error_session;
+ }
+ return result;
+
+error_resource:
+ pw_log_error("can't create resource: %s", spa_strerror(res));
+ pw_resource_errorf_id(resource, new_id, res, "can't create resource: %s", spa_strerror(res));
+ goto error_exit;
+error_session:
+ pw_log_error("can't create session: %s", spa_strerror(res));
+ pw_resource_errorf_id(resource, new_id, res, "can't create session: %s", spa_strerror(res));
+ goto error_exit_free;
+
+error_exit_free:
+ pw_resource_remove(impl_resource);
+error_exit:
+ errno = -res;
+ return NULL;
+}
+
+static const struct pw_impl_factory_implementation impl_factory = {
+ PW_VERSION_IMPL_FACTORY_IMPLEMENTATION,
+ .create_object = create_object,
+};
+
+static void factory_destroy(void *data)
+{
+ struct factory_data *d = data;
+ spa_hook_remove(&d->factory_listener);
+ d->factory = NULL;
+ if (d->module)
+ pw_impl_module_destroy(d->module);
+}
+
+static const struct pw_impl_factory_events factory_events = {
+ PW_VERSION_IMPL_FACTORY_EVENTS,
+ .destroy = factory_destroy,
+};
+
+static void module_destroy(void *data)
+{
+ struct factory_data *d = data;
+
+ spa_hook_remove(&d->module_listener);
+ spa_list_remove(&d->export.link);
+ d->module = NULL;
+ if (d->factory)
+ pw_impl_factory_destroy(d->factory);
+}
+
+static void module_registered(void *data)
+{
+ struct factory_data *d = data;
+ struct pw_impl_module *module = d->module;
+ struct pw_impl_factory *factory = d->factory;
+ struct spa_dict_item items[1];
+ char id[16];
+ int res;
+
+ snprintf(id, sizeof(id), "%d", pw_impl_module_get_info(module)->id);
+ items[0] = SPA_DICT_ITEM_INIT(PW_KEY_MODULE_ID, id);
+ pw_impl_factory_update_properties(factory, &SPA_DICT_INIT(items, 1));
+
+ if ((res = pw_impl_factory_register(factory, NULL)) < 0) {
+ pw_log_error(NAME" %p: can't register factory: %s", factory, spa_strerror(res));
+ }
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy,
+ .registered = module_registered,
+};
+
+int session_factory_init(struct pw_impl_module *module)
+{
+ struct pw_context *context = pw_impl_module_get_context(module);
+ struct pw_impl_factory *factory;
+ struct factory_data *data;
+ int res;
+
+ factory = pw_context_create_factory(context,
+ "session",
+ PW_TYPE_INTERFACE_Session,
+ PW_VERSION_SESSION,
+ NULL,
+ sizeof(*data));
+ if (factory == NULL)
+ return -errno;
+
+ data = pw_impl_factory_get_user_data(factory);
+ data->factory = factory;
+ data->module = module;
+
+ pw_impl_factory_set_implementation(factory, &impl_factory, data);
+
+ data->export.type = PW_TYPE_INTERFACE_Session;
+ data->export.func = pw_core_session_export;
+ if ((res = pw_context_register_export_type(context, &data->export)) < 0)
+ goto error;
+
+ pw_impl_factory_add_listener(factory, &data->factory_listener, &factory_events, data);
+ pw_impl_module_add_listener(module, &data->module_listener, &module_events, data);
+
+ return 0;
+error:
+ pw_impl_factory_destroy(data->factory);
+ return res;
+}
diff --git a/src/modules/module-x11-bell.c b/src/modules/module-x11-bell.c
new file mode 100644
index 0000000..908f397
--- /dev/null
+++ b/src/modules/module-x11-bell.c
@@ -0,0 +1,364 @@
+/* PipeWire
+ *
+ * Copyright © 2022 Wim Taymans <wim.taymans@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+
+#include "config.h"
+
+#include <spa/utils/string.h>
+
+#include <X11/Xlib.h>
+#include <X11/Xlib-xcb.h>
+#include <X11/XKBlib.h>
+
+#ifdef HAVE_XFIXES_6
+#include <X11/extensions/Xfixes.h>
+#endif
+
+#include <canberra.h>
+
+#include <pipewire/pipewire.h>
+#include <pipewire/impl.h>
+
+/** \page page_module_x11_bell PipeWire Module: X11 Bell
+ *
+ * The `x11-bell` module intercept the X11 bell events and uses libcanberra to
+ * play a sound.
+ *
+ * ## Module Options
+ *
+ * - `sink.name = <str>`: node.name of the sink to connect to
+ * - `sample.name = <str>`: the name of the sample to play, default 'bell-window-system'
+ * - `x11.display = <str>`: the X11 display to use
+ * - `x11.xauthority = <str>`: the X11 XAuthority string placed in XAUTHORITY env
+ *
+ * ## General options
+ *
+ * There are no general options for this module.
+ *
+ * ## Example configuration
+ *\code{.unparsed}
+ * context.modules = [
+ * { name = libpipewire-x11-bell }
+ * args = {
+ * #sink.name = @DEFAULT_SINK@
+ * sample.name = "bell-window-system"
+ * #x11.display = ":1"
+ * #x11.xauthority = "test"
+ * ]
+ *\endcode
+ *
+ */
+
+#define NAME "x11-bell"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+struct impl {
+ struct pw_context *context;
+ struct pw_thread_loop *thread_loop;
+ struct pw_loop *thread_loop_loop;
+ struct pw_loop *loop;
+ struct spa_source *source;
+
+ struct pw_properties *properties;
+
+ struct pw_impl_module *module;
+ struct spa_hook module_listener;
+
+ Display *display;
+};
+
+static int play_sample(struct impl *impl)
+{
+ const char *sample = NULL;
+ ca_context *ca;
+ int res;
+
+ if (impl->properties)
+ sample = pw_properties_get(impl->properties, "sample.name");
+ if (sample == NULL)
+ sample = "bell-window-system";
+
+ pw_log_info("play sample %s", sample);
+
+ if ((res = ca_context_create(&ca)) < 0) {
+ pw_log_error("canberra context create error: %s", ca_strerror(res));
+ res = -EIO;
+ goto exit;
+ }
+ if ((res = ca_context_open(ca)) < 0) {
+ pw_log_error("canberra context open error: %s", ca_strerror(res));
+ res = -EIO;
+ goto exit_destroy;
+ }
+ if ((res = ca_context_play(ca, 0,
+ CA_PROP_EVENT_ID, sample,
+ CA_PROP_MEDIA_NAME, "X11 bell event",
+ CA_PROP_CANBERRA_CACHE_CONTROL, "permanent",
+ NULL)) < 0) {
+ pw_log_warn("can't play sample (%s): %s", sample, ca_strerror(res));
+ res = -EIO;
+ goto exit_destroy;
+ }
+
+exit_destroy:
+ ca_context_destroy(ca);
+exit:
+ return res;
+}
+
+static int do_play_sample(struct spa_loop *loop, bool async, uint32_t seq, const void *data, size_t size, void *user_data)
+{
+ play_sample(user_data);
+ return 0;
+}
+
+static void display_io(void *data, int fd, uint32_t mask)
+{
+ struct impl *impl = data;
+ XEvent e;
+
+ while (XPending(impl->display)) {
+ XNextEvent(impl->display, &e);
+
+ if (((XkbEvent*) &e)->any.xkb_type != XkbBellNotify)
+ continue;
+
+ pw_loop_invoke(impl->thread_loop_loop, do_play_sample, 0, NULL, 0, false, impl);
+ }
+}
+
+#ifdef HAVE_XSETIOERROREXITHANDLER
+static void x11_io_error_exit_handler(Display *display, void *data)
+{
+ struct impl *impl = data;
+
+ spa_assert(display == impl->display);
+
+ pw_log_warn("X11 display (%s) has encountered a fatal I/O error", DisplayString(display));
+
+ pw_loop_destroy_source(impl->loop, impl->source);
+ impl->source = NULL;
+
+ pw_impl_module_schedule_destroy(impl->module);
+}
+#endif
+
+static int x11_connect(struct impl *impl, const char *name)
+{
+ int major, minor;
+ unsigned int auto_ctrls, auto_values;
+
+ if (!(impl->display = XOpenDisplay(name))) {
+ pw_log_error("XOpenDisplay() failed");
+ return -EIO;
+ }
+
+ impl->source = pw_loop_add_io(impl->loop,
+ ConnectionNumber(impl->display),
+ SPA_IO_IN, false, display_io, impl);
+ if (!impl->source)
+ return -errno;
+
+#ifdef HAVE_XSETIOERROREXITHANDLER
+ XSetIOErrorExitHandler(impl->display, x11_io_error_exit_handler, impl);
+#endif
+
+#ifdef HAVE_XFIXES_6
+ XFixesSetClientDisconnectMode(impl->display, XFixesClientDisconnectFlagTerminate);
+#endif
+
+ major = XkbMajorVersion;
+ minor = XkbMinorVersion;
+
+ if (!XkbLibraryVersion(&major, &minor)) {
+ pw_log_error("XkbLibraryVersion() failed");
+ return -EIO;
+ }
+
+ major = XkbMajorVersion;
+ minor = XkbMinorVersion;
+
+ if (!XkbQueryExtension(impl->display, NULL, NULL, NULL, &major, &minor)) {
+ pw_log_error("XkbQueryExtension() failed");
+ return -EIO;
+ }
+
+ XkbSelectEvents(impl->display, XkbUseCoreKbd, XkbBellNotifyMask, XkbBellNotifyMask);
+ auto_ctrls = auto_values = XkbAudibleBellMask;
+ XkbSetAutoResetControls(impl->display, XkbAudibleBellMask, &auto_ctrls, &auto_values);
+ XkbChangeEnabledControls(impl->display, XkbUseCoreKbd, XkbAudibleBellMask, 0);
+
+ return 0;
+}
+
+static void module_destroy(void *data)
+{
+ struct impl *impl = data;
+
+ if (impl->module)
+ spa_hook_remove(&impl->module_listener);
+
+ if (impl->source)
+ pw_loop_destroy_source(impl->loop, impl->source);
+
+ if (impl->display)
+ XCloseDisplay(impl->display);
+
+ if (impl->thread_loop)
+ pw_thread_loop_destroy(impl->thread_loop);
+
+ pw_properties_free(impl->properties);
+
+ free(impl);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy,
+};
+
+static const struct spa_dict_item module_x11_bell_info[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "X11 Bell interceptor" },
+ { PW_KEY_MODULE_USAGE, "sink.name=<name for the sink> "
+ "sample.name=<the sample name> "
+ "x11.display=<the X11 display> "
+ "x11.xauthority=<the X11 XAuthority> " },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+SPA_EXPORT
+int pipewire__module_init(struct pw_impl_module *module, const char *args)
+{
+ struct pw_context *context = pw_impl_module_get_context(module);
+ struct impl *impl;
+ const char *name = NULL, *str;
+ int res;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ impl = calloc(1, sizeof(struct impl));
+ if (impl == NULL)
+ return -ENOMEM;
+
+ pw_log_debug("module %p: new", impl);
+
+ impl->context = context;
+ impl->loop = pw_context_get_main_loop(context);
+
+ impl->thread_loop = pw_thread_loop_new("X11 Bell", NULL);
+ if (impl->thread_loop == NULL) {
+ res = -errno;
+ pw_log_error("can't create thread loop: %m");
+ goto error;
+ }
+ impl->thread_loop_loop = pw_thread_loop_get_loop(impl->thread_loop);
+ impl->properties = args ? pw_properties_new_string(args) : NULL;
+
+ impl->module = module;
+ pw_impl_module_add_listener(module, &impl->module_listener, &module_events, impl);
+ pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_x11_bell_info));
+
+ if (impl->properties) {
+ if ((str = pw_properties_get(impl->properties, "x11.xauthority")) != NULL) {
+ if (setenv("XAUTHORITY", str, 1)) {
+ res = -errno;
+ pw_log_error("XAUTHORITY setenv failed: %m");
+ goto error;
+ }
+ }
+ name = pw_properties_get(impl->properties, "x11.display");
+ }
+
+ /* we need to use a thread loop because this module will connect
+ * to pipewire eventually and will then block the mainloop. */
+ pw_thread_loop_start(impl->thread_loop);
+
+ res = x11_connect(impl, name);
+ if (res < 0)
+ goto error;
+
+ return 0;
+error:
+ module_destroy(impl);
+ return res;
+
+}
+
+static int x11_error_handler(Display *display, XErrorEvent *error)
+{
+ pw_log_warn("X11 error handler called on display %s with error %d",
+ DisplayString(display), error->error_code);
+ return 0;
+}
+
+static int x11_io_error_handler(Display *display)
+{
+ pw_log_warn("X11 I/O error handler called on display %s", DisplayString(display));
+ return 0;
+}
+
+__attribute__((constructor))
+static void set_x11_handlers(void)
+{
+ {
+ XErrorHandler prev = XSetErrorHandler(NULL);
+ XErrorHandler def = XSetErrorHandler(x11_error_handler);
+
+ if (prev != def)
+ XSetErrorHandler(prev);
+ }
+
+ {
+ XIOErrorHandler prev = XSetIOErrorHandler(NULL);
+ XIOErrorHandler def = XSetIOErrorHandler(x11_io_error_handler);
+
+ if (prev != def)
+ XSetIOErrorHandler(prev);
+ }
+}
+
+__attribute__((destructor))
+static void restore_x11_handlers(void)
+{
+ {
+ XErrorHandler prev = XSetErrorHandler(NULL);
+ if (prev != x11_error_handler)
+ XSetErrorHandler(prev);
+ }
+
+ {
+ XIOErrorHandler prev = XSetIOErrorHandler(NULL);
+ if (prev != x11_io_error_handler)
+ XSetIOErrorHandler(prev);
+ }
+}
diff --git a/src/modules/module-zeroconf-discover.c b/src/modules/module-zeroconf-discover.c
new file mode 100644
index 0000000..5e7436f
--- /dev/null
+++ b/src/modules/module-zeroconf-discover.c
@@ -0,0 +1,565 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+
+#include "config.h"
+
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+#include <spa/utils/json.h>
+#include <spa/param/audio/format-utils.h>
+
+#include <pipewire/impl.h>
+#include <pipewire/i18n.h>
+
+#include <avahi-client/lookup.h>
+#include <avahi-common/error.h>
+#include <avahi-common/malloc.h>
+
+#include "module-protocol-pulse/format.h"
+#include "module-zeroconf-discover/avahi-poll.h"
+
+/** \page page_module_zeroconf_discover PipeWire Module: Zeroconf Discover
+ *
+ * Use zeroconf to detect and load module-pulse-tunnel with the right
+ * parameters. This will automatically create sinks and sources to stream
+ * audio to/from remote PulseAudio servers. It also works with
+ * module-protocol-pulse.
+ *
+ * ## Module Options
+ *
+ * - `pulse.latency`: the latency to end-to-end latency in milliseconds to
+ * maintain (Default 200ms).
+ *
+ * ## Example configuration
+ *
+ *\code{.unparsed}
+ * context.modules = [
+ * { name = libpipewire-module-zeroconf-discover
+ * args = { }
+ * }
+ * ]
+ *\endcode
+ */
+
+#define NAME "zeroconf-discover"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+#define MODULE_USAGE "pulse.latency=<latency in msec> "
+
+static const struct spa_dict_item module_props[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Discover remote streams" },
+ { PW_KEY_MODULE_USAGE, MODULE_USAGE },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+#define SERVICE_TYPE_SINK "_pulse-sink._tcp"
+#define SERVICE_TYPE_SOURCE "_non-monitor._sub._pulse-source._tcp"
+
+
+struct impl {
+ struct pw_context *context;
+
+ struct pw_impl_module *module;
+ struct spa_hook module_listener;
+
+ struct pw_properties *properties;
+
+ AvahiPoll *avahi_poll;
+ AvahiClient *client;
+ AvahiServiceBrowser *sink_browser;
+ AvahiServiceBrowser *source_browser;
+
+ struct spa_list tunnel_list;
+};
+
+struct tunnel_info {
+ AvahiIfIndex interface;
+ AvahiProtocol protocol;
+ const char *name;
+ const char *type;
+ const char *domain;
+};
+
+#define TUNNEL_INFO(...) ((struct tunnel_info){ __VA_ARGS__ })
+
+struct tunnel {
+ struct spa_list link;
+ struct tunnel_info info;
+ struct pw_impl_module *module;
+ struct spa_hook module_listener;
+};
+
+static int start_client(struct impl *impl);
+
+static struct tunnel *make_tunnel(struct impl *impl, const struct tunnel_info *info)
+{
+ struct tunnel *t;
+
+ t = calloc(1, sizeof(*t));
+ if (t == NULL)
+ return NULL;
+
+ t->info.interface = info->interface;
+ t->info.protocol = info->protocol;
+ t->info.name = strdup(info->name);
+ t->info.type = strdup(info->type);
+ t->info.domain = strdup(info->domain);
+ spa_list_append(&impl->tunnel_list, &t->link);
+
+ return t;
+}
+
+static struct tunnel *find_tunnel(struct impl *impl, const struct tunnel_info *info)
+{
+ struct tunnel *t;
+ spa_list_for_each(t, &impl->tunnel_list, link) {
+ if (t->info.interface == info->interface &&
+ t->info.protocol == info->protocol &&
+ spa_streq(t->info.name, info->name) &&
+ spa_streq(t->info.type, info->type) &&
+ spa_streq(t->info.domain, info->domain))
+ return t;
+ }
+ return NULL;
+}
+
+static void free_tunnel(struct tunnel *t)
+{
+ pw_impl_module_destroy(t->module);
+}
+
+static void impl_free(struct impl *impl)
+{
+ struct tunnel *t;
+
+ spa_list_consume(t, &impl->tunnel_list, link)
+ free_tunnel(t);
+
+ if (impl->sink_browser)
+ avahi_service_browser_free(impl->sink_browser);
+ if (impl->source_browser)
+ avahi_service_browser_free(impl->source_browser);
+ if (impl->client)
+ avahi_client_free(impl->client);
+ if (impl->avahi_poll)
+ pw_avahi_poll_free(impl->avahi_poll);
+ pw_properties_free(impl->properties);
+ free(impl);
+}
+
+static void module_destroy(void *data)
+{
+ struct impl *impl = data;
+ spa_hook_remove(&impl->module_listener);
+ impl_free(impl);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy,
+};
+
+static void pw_properties_from_avahi_string(const char *key, const char *value,
+ struct pw_properties *props)
+{
+ if (spa_streq(key, "device")) {
+ pw_properties_set(props, PW_KEY_TARGET_OBJECT, value);
+ }
+ else if (spa_streq(key, "rate")) {
+ pw_properties_set(props, PW_KEY_AUDIO_RATE, value);
+ }
+ else if (spa_streq(key, "channels")) {
+ pw_properties_set(props, PW_KEY_AUDIO_CHANNELS, value);
+ }
+ else if (spa_streq(key, "channel_map")) {
+ struct channel_map channel_map;
+ uint32_t i, pos[CHANNELS_MAX];
+ char *p, *s;
+
+ spa_zero(channel_map);
+ channel_map_parse(value, &channel_map);
+ channel_map_to_positions(&channel_map, pos);
+
+ p = s = alloca(4 + channel_map.channels * 8);
+ p += spa_scnprintf(p, 2, "[");
+ for (i = 0; i < channel_map.channels; i++)
+ p += spa_scnprintf(p, 8, "%s%s", i == 0 ? "" : ",",
+ channel_id2name(pos[i]));
+ p += spa_scnprintf(p, 2, "]");
+ pw_properties_set(props, SPA_KEY_AUDIO_POSITION, s);
+ }
+ else if (spa_streq(key, "format")) {
+ uint32_t fmt = format_paname2id(value, strlen(value));
+ if (fmt != SPA_AUDIO_FORMAT_UNKNOWN)
+ pw_properties_set(props, PW_KEY_AUDIO_FORMAT, format_id2name(fmt));
+ }
+ else if (spa_streq(key, "icon-name")) {
+ pw_properties_set(props, PW_KEY_DEVICE_ICON_NAME, value);
+ }
+ else if (spa_streq(key, "product-name")) {
+ pw_properties_set(props, PW_KEY_DEVICE_PRODUCT_NAME, value);
+ }
+ else if (spa_streq(key, "description")) {
+ pw_properties_set(props, "tunnel.remote.description", value);
+ }
+ else if (spa_streq(key, "fqdn")) {
+ pw_properties_set(props, "tunnel.remote.fqdn", value);
+ }
+ else if (spa_streq(key, "user-name")) {
+ pw_properties_set(props, "tunnel.remote.user", value);
+ }
+}
+
+static void submodule_destroy(void *data)
+{
+ struct tunnel *t = data;
+
+ spa_list_remove(&t->link);
+ spa_hook_remove(&t->module_listener);
+
+ free((char *) t->info.name);
+ free((char *) t->info.type);
+ free((char *) t->info.domain);
+
+ free(t);
+}
+
+static const struct pw_impl_module_events submodule_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = submodule_destroy,
+};
+
+static void resolver_cb(AvahiServiceResolver *r, AvahiIfIndex interface, AvahiProtocol protocol,
+ AvahiResolverEvent event, const char *name, const char *type, const char *domain,
+ const char *host_name, const AvahiAddress *a, uint16_t port, AvahiStringList *txt,
+ AvahiLookupResultFlags flags, void *userdata)
+{
+ struct impl *impl = userdata;
+ struct tunnel *t;
+ struct tunnel_info tinfo;
+ const char *str, *device, *desc, *fqdn, *user;
+ char if_suffix[16] = "";
+ char at[AVAHI_ADDRESS_STR_MAX];
+ AvahiStringList *l;
+ FILE *f;
+ char *args;
+ size_t size;
+ struct pw_impl_module *mod;
+ struct pw_properties *props = NULL;
+
+ if (event != AVAHI_RESOLVER_FOUND) {
+ pw_log_error("Resolving of '%s' failed: %s", name,
+ avahi_strerror(avahi_client_errno(impl->client)));
+ goto done;
+ }
+ tinfo = TUNNEL_INFO(.interface = interface,
+ .protocol = protocol,
+ .name = name,
+ .type = type,
+ .domain = domain);
+
+ props = pw_properties_new(NULL, NULL);
+ if (props == NULL) {
+ pw_log_error("Can't allocate properties: %m");
+ goto done;
+ }
+
+ for (l = txt; l; l = l->next) {
+ char *key, *value;
+
+ if (avahi_string_list_get_pair(l, &key, &value, NULL) != 0)
+ break;
+
+ pw_properties_from_avahi_string(key, value, props);
+ avahi_free(key);
+ avahi_free(value);
+ }
+
+ if ((device = pw_properties_get(props, PW_KEY_TARGET_OBJECT)) != NULL)
+ pw_properties_setf(props, PW_KEY_NODE_NAME,
+ "tunnel.%s.%s", host_name, device);
+ else
+ pw_properties_setf(props, PW_KEY_NODE_NAME,
+ "tunnel.%s", host_name);
+
+ str = strstr(type, "sink") ? "sink" : "source";
+ pw_properties_set(props, "tunnel.mode", str);
+
+ if (a->proto == AVAHI_PROTO_INET6 &&
+ a->data.ipv6.address[0] == 0xfe &&
+ (a->data.ipv6.address[1] & 0xc0) == 0x80)
+ snprintf(if_suffix, sizeof(if_suffix), "%%%d", interface);
+
+ pw_properties_setf(props, "pulse.server.address", " [%s%s]:%u",
+ avahi_address_snprint(at, sizeof(at), a),
+ if_suffix, port);
+
+ desc = pw_properties_get(props, "tunnel.remote.description");
+ if (desc == NULL)
+ desc = pw_properties_get(props, PW_KEY_DEVICE_PRODUCT_NAME);
+ if (desc == NULL)
+ desc = pw_properties_get(props, PW_KEY_TARGET_OBJECT);
+ if (desc == NULL)
+ desc = _("Unknown device");
+
+ fqdn = pw_properties_get(props, "tunnel.remote.fqdn");
+ if (fqdn == NULL)
+ fqdn = pw_properties_get(props, "pulse.server.address");
+ if (fqdn == NULL)
+ fqdn = host_name;
+
+ user = pw_properties_get(props, "tunnel.remote.user");
+
+ if (desc != NULL && user != NULL && fqdn != NULL) {
+ pw_properties_setf(props, PW_KEY_NODE_DESCRIPTION,
+ _("%s on %s@%s"), desc, user, fqdn);
+ }
+ else if (desc != NULL && fqdn != NULL) {
+ pw_properties_setf(props, PW_KEY_NODE_DESCRIPTION,
+ _("%s on %s"), desc, fqdn);
+ }
+
+ if ((str = pw_properties_get(impl->properties, "pulse.latency")) != NULL)
+ pw_properties_set(props, "pulse.latency", str);
+
+ if ((f = open_memstream(&args, &size)) == NULL) {
+ pw_log_error("Can't open memstream: %m");
+ goto done;
+ }
+
+ fprintf(f, "{");
+ pw_properties_serialize_dict(f, &props->dict, 0);
+ fprintf(f, " stream.props = {");
+ fprintf(f, " }");
+ fprintf(f, "}");
+ fclose(f);
+
+ pw_properties_free(props);
+
+ pw_log_info("loading module args:'%s'", args);
+ mod = pw_context_load_module(impl->context,
+ "libpipewire-module-pulse-tunnel",
+ args, NULL);
+ free(args);
+
+ if (mod == NULL) {
+ pw_log_error("Can't load module: %m");
+ goto done;
+ }
+
+ t = make_tunnel(impl, &tinfo);
+ if (t == NULL) {
+ pw_log_error("Can't make tunnel: %m");
+ pw_impl_module_destroy(mod);
+ goto done;
+ }
+
+ pw_impl_module_add_listener(mod, &t->module_listener, &submodule_events, t);
+
+ t->module = mod;
+
+done:
+ avahi_service_resolver_free(r);
+}
+
+
+static void browser_cb(AvahiServiceBrowser *b, AvahiIfIndex interface, AvahiProtocol protocol,
+ AvahiBrowserEvent event, const char *name, const char *type, const char *domain,
+ AvahiLookupResultFlags flags, void *userdata)
+{
+ struct impl *impl = userdata;
+ struct tunnel_info info;
+ struct tunnel *t;
+
+ if (flags & AVAHI_LOOKUP_RESULT_LOCAL)
+ return;
+
+ info = TUNNEL_INFO(.interface = interface,
+ .protocol = protocol,
+ .name = name,
+ .type = type,
+ .domain = domain);
+
+ t = find_tunnel(impl, &info);
+
+ switch (event) {
+ case AVAHI_BROWSER_NEW:
+ if (t != NULL)
+ return;
+ if (!(avahi_service_resolver_new(impl->client,
+ interface, protocol,
+ name, type, domain,
+ AVAHI_PROTO_UNSPEC, 0,
+ resolver_cb, impl)))
+ pw_log_error("can't make service resolver: %s",
+ avahi_strerror(avahi_client_errno(impl->client)));
+ break;
+ case AVAHI_BROWSER_REMOVE:
+ if (t == NULL)
+ return;
+ free_tunnel(t);
+ break;
+ default:
+ break;
+ }
+}
+
+
+static struct AvahiServiceBrowser *make_browser(struct impl *impl, const char *service_type)
+{
+ struct AvahiServiceBrowser *s;
+
+ s = avahi_service_browser_new(impl->client,
+ AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC,
+ service_type, NULL, 0,
+ browser_cb, impl);
+ if (s == NULL) {
+ pw_log_error("can't make browser for %s: %s", service_type,
+ avahi_strerror(avahi_client_errno(impl->client)));
+ }
+ return s;
+}
+
+static void client_callback(AvahiClient *c, AvahiClientState state, void *userdata)
+{
+ struct impl *impl = userdata;
+
+ impl->client = c;
+
+ switch (state) {
+ case AVAHI_CLIENT_S_REGISTERING:
+ case AVAHI_CLIENT_S_RUNNING:
+ case AVAHI_CLIENT_S_COLLISION:
+ if (impl->sink_browser == NULL)
+ impl->sink_browser = make_browser(impl, SERVICE_TYPE_SINK);
+ if (impl->sink_browser == NULL)
+ goto error;
+
+ if (impl->source_browser == NULL)
+ impl->source_browser = make_browser(impl, SERVICE_TYPE_SOURCE);
+ if (impl->source_browser == NULL)
+ goto error;
+
+ break;
+ case AVAHI_CLIENT_FAILURE:
+ if (avahi_client_errno(c) == AVAHI_ERR_DISCONNECTED)
+ start_client(impl);
+
+ SPA_FALLTHROUGH;
+ case AVAHI_CLIENT_CONNECTING:
+ if (impl->sink_browser) {
+ avahi_service_browser_free(impl->sink_browser);
+ impl->sink_browser = NULL;
+ }
+ if (impl->source_browser) {
+ avahi_service_browser_free(impl->source_browser);
+ impl->source_browser = NULL;
+ }
+ break;
+ default:
+ break;
+ }
+ return;
+error:
+ pw_impl_module_schedule_destroy(impl->module);
+}
+
+static int start_client(struct impl *impl)
+{
+ int res;
+ if ((impl->client = avahi_client_new(impl->avahi_poll,
+ AVAHI_CLIENT_NO_FAIL,
+ client_callback, impl,
+ &res)) == NULL) {
+ pw_log_error("can't create client: %s", avahi_strerror(res));
+ pw_impl_module_schedule_destroy(impl->module);
+ return -EIO;
+ }
+ return 0;
+}
+
+static int start_avahi(struct impl *impl)
+{
+ struct pw_loop *loop;
+
+ loop = pw_context_get_main_loop(impl->context);
+ impl->avahi_poll = pw_avahi_poll_new(loop);
+
+ return start_client(impl);
+}
+
+SPA_EXPORT
+int pipewire__module_init(struct pw_impl_module *module, const char *args)
+{
+ struct pw_context *context = pw_impl_module_get_context(module);
+ struct pw_properties *props;
+ struct impl *impl;
+ int res;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ impl = calloc(1, sizeof(struct impl));
+ if (impl == NULL)
+ goto error_errno;
+
+ pw_log_debug("module %p: new %s", impl, args);
+
+ if (args == NULL)
+ args = "";
+
+ props = pw_properties_new_string(args);
+ if (props == NULL)
+ goto error_errno;
+
+ spa_list_init(&impl->tunnel_list);
+
+ impl->module = module;
+ impl->context = context;
+ impl->properties = props;
+
+ pw_impl_module_add_listener(module, &impl->module_listener, &module_events, impl);
+
+ pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_props));
+
+ start_avahi(impl);
+
+ return 0;
+
+error_errno:
+ res = -errno;
+ if (impl)
+ impl_free(impl);
+ return res;
+}
diff --git a/src/modules/module-zeroconf-discover/avahi-poll.c b/src/modules/module-zeroconf-discover/avahi-poll.c
new file mode 100644
index 0000000..e098646
--- /dev/null
+++ b/src/modules/module-zeroconf-discover/avahi-poll.c
@@ -0,0 +1,201 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <pipewire/pipewire.h>
+
+#include "avahi-poll.h"
+
+struct impl {
+ AvahiPoll api;
+ struct pw_loop *loop;
+};
+
+struct AvahiWatch {
+ struct impl *impl;
+ struct spa_source *source;
+ AvahiWatchEvent events;
+ AvahiWatchCallback callback;
+ void *userdata;
+ unsigned int dispatching;
+};
+
+struct AvahiTimeout {
+ struct impl *impl;
+ struct spa_source *source;
+ AvahiTimeoutCallback callback;
+ void *userdata;
+};
+
+static AvahiWatchEvent from_pw_events(uint32_t mask)
+{
+ return (mask & SPA_IO_IN ? AVAHI_WATCH_IN : 0) |
+ (mask & SPA_IO_OUT ? AVAHI_WATCH_OUT : 0) |
+ (mask & SPA_IO_ERR ? AVAHI_WATCH_ERR : 0) |
+ (mask & SPA_IO_HUP ? AVAHI_WATCH_HUP : 0);
+}
+
+static uint32_t to_pw_events(AvahiWatchEvent e) {
+ return (e & AVAHI_WATCH_IN ? SPA_IO_IN : 0) |
+ (e & AVAHI_WATCH_OUT ? SPA_IO_OUT : 0) |
+ (e & AVAHI_WATCH_ERR ? SPA_IO_ERR : 0) |
+ (e & AVAHI_WATCH_HUP ? SPA_IO_HUP : 0);
+}
+
+static void watch_callback(void *data, int fd, uint32_t mask)
+{
+ AvahiWatch *w = data;
+
+ w->dispatching += 1;
+
+ w->events = from_pw_events(mask);
+ w->callback(w, fd, w->events, w->userdata);
+ w->events = 0;
+
+ if (--w->dispatching == 0 && !w->source)
+ free(w);
+}
+
+static AvahiWatch* watch_new(const AvahiPoll *api, int fd, AvahiWatchEvent event,
+ AvahiWatchCallback callback, void *userdata)
+{
+ struct impl *impl = api->userdata;
+ AvahiWatch *w;
+
+ w = calloc(1, sizeof(*w));
+ if (w == NULL)
+ return NULL;
+
+ w->impl = impl;
+ w->events = 0;
+ w->callback = callback;
+ w->userdata = userdata;
+ w->source = pw_loop_add_io(impl->loop, fd, to_pw_events(event),
+ false, watch_callback, w);
+
+ return w;
+}
+
+static void watch_update(AvahiWatch *w, AvahiWatchEvent event)
+{
+ struct impl *impl = w->impl;
+ pw_loop_update_io(impl->loop, w->source, to_pw_events(event));
+}
+
+static AvahiWatchEvent watch_get_events(AvahiWatch *w)
+{
+ return w->events;
+}
+
+static void watch_free(AvahiWatch *w)
+{
+ pw_loop_destroy_source(w->impl->loop, w->source);
+ w->source = NULL;
+
+ if (!w->dispatching)
+ free(w);
+}
+
+static void timeout_callback(void *data, uint64_t expirations)
+{
+ AvahiTimeout *w = data;
+ w->callback(w, w->userdata);
+}
+
+static AvahiTimeout* timeout_new(const AvahiPoll *api, const struct timeval *tv,
+ AvahiTimeoutCallback callback, void *userdata)
+{
+ struct impl *impl = api->userdata;
+ struct timespec value;
+ AvahiTimeout *w;
+
+ w = calloc(1, sizeof(*w));
+ if (w == NULL)
+ return NULL;
+
+ w->impl = impl;
+ w->callback = callback;
+ w->userdata = userdata;
+ w->source = pw_loop_add_timer(impl->loop, timeout_callback, w);
+
+ if (tv != NULL) {
+ value.tv_sec = tv->tv_sec;
+ value.tv_nsec = tv->tv_usec * 1000UL;
+ pw_loop_update_timer(impl->loop, w->source, &value, NULL, true);
+ }
+ return w;
+}
+
+static void timeout_update(AvahiTimeout *t, const struct timeval *tv)
+{
+ struct impl *impl = t->impl;
+ struct timespec value, *v = NULL;
+
+ if (tv != NULL) {
+ value.tv_sec = tv->tv_sec;
+ value.tv_nsec = tv->tv_usec * 1000UL;
+ if (value.tv_sec == 0 && value.tv_nsec == 0)
+ value.tv_nsec = 1;
+ v = &value;
+ }
+ pw_loop_update_timer(impl->loop, t->source, v, NULL, true);
+}
+
+static void timeout_free(AvahiTimeout *t)
+{
+ struct impl *impl = t->impl;
+ pw_loop_destroy_source(impl->loop, t->source);
+ free(t);
+}
+
+static const AvahiPoll avahi_poll_api = {
+ .watch_new = watch_new,
+ .watch_update = watch_update,
+ .watch_get_events = watch_get_events,
+ .watch_free = watch_free,
+ .timeout_new = timeout_new,
+ .timeout_update = timeout_update,
+ .timeout_free = timeout_free,
+};
+
+AvahiPoll* pw_avahi_poll_new(struct pw_loop *loop)
+{
+ struct impl *impl;
+
+ impl = calloc(1, sizeof(*impl));
+ if (impl == NULL)
+ return NULL;
+
+ impl->loop = loop;
+ impl->api = avahi_poll_api;
+ impl->api.userdata = impl;
+
+ return &impl->api;
+}
+
+void pw_avahi_poll_free(AvahiPoll *p)
+{
+ struct impl *impl = SPA_CONTAINER_OF(p, struct impl, api);
+
+ free(impl);
+}
diff --git a/src/modules/module-zeroconf-discover/avahi-poll.h b/src/modules/module-zeroconf-discover/avahi-poll.h
new file mode 100644
index 0000000..04b785d
--- /dev/null
+++ b/src/modules/module-zeroconf-discover/avahi-poll.h
@@ -0,0 +1,31 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <avahi-client/client.h>
+
+#include <pipewire/loop.h>
+
+AvahiPoll* pw_avahi_poll_new(struct pw_loop *loop);
+
+void pw_avahi_poll_free(AvahiPoll *p);
diff --git a/src/modules/spa/meson.build b/src/modules/spa/meson.build
new file mode 100644
index 0000000..8332910
--- /dev/null
+++ b/src/modules/spa/meson.build
@@ -0,0 +1,31 @@
+pipewire_module_spa_node = shared_library('pipewire-module-spa-node',
+ [ 'module-node.c', 'spa-node.c' ],
+ include_directories : [configinc],
+ install : true,
+ install_dir : modules_install_dir,
+ dependencies : [spa_dep, mathlib, dl_lib, pipewire_dep],
+)
+
+pipewire_module_spa_device = shared_library('pipewire-module-spa-device',
+ [ 'module-device.c', 'spa-device.c' ],
+ include_directories : [configinc],
+ install : true,
+ install_dir : modules_install_dir,
+ dependencies : [spa_dep, mathlib, dl_lib, pipewire_dep],
+)
+
+pipewire_module_spa_node_factory = shared_library('pipewire-module-spa-node-factory',
+ [ 'module-node-factory.c', 'spa-node.c' ],
+ include_directories : [configinc],
+ install : true,
+ install_dir : modules_install_dir,
+ dependencies : [spa_dep, mathlib, dl_lib, pipewire_dep],
+)
+
+pipewire_module_spa_device_factory = shared_library('pipewire-module-spa-device-factory',
+ [ 'module-device-factory.c', 'spa-device.c' ],
+ include_directories : [configinc],
+ install : true,
+ install_dir : modules_install_dir,
+ dependencies : [spa_dep, mathlib, dl_lib, pipewire_dep],
+)
diff --git a/src/modules/spa/module-device-factory.c b/src/modules/spa/module-device-factory.c
new file mode 100644
index 0000000..fd712a2
--- /dev/null
+++ b/src/modules/spa/module-device-factory.c
@@ -0,0 +1,281 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <errno.h>
+#include <dlfcn.h>
+
+#include "config.h"
+
+#include <spa/utils/result.h>
+
+#include "pipewire/impl.h"
+
+#include "spa-device.h"
+
+#define NAME "spa-device-factory"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+#define FACTORY_USAGE SPA_KEY_FACTORY_NAME"=<factory-name> " \
+ "["SPA_KEY_LIBRARY_NAME"=<library-name>]"
+
+static const struct spa_dict_item module_props[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Provide a factory to make SPA devices" },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+struct factory_data {
+ struct pw_context *context;
+
+ struct pw_impl_module *module;
+ struct spa_hook module_listener;
+
+ struct pw_impl_factory *factory;
+ struct spa_hook factory_listener;
+
+ struct spa_list device_list;
+};
+
+struct device_data {
+ struct spa_list link;
+ struct pw_impl_device *device;
+ struct spa_hook device_listener;
+ struct spa_hook resource_listener;
+};
+
+static void resource_destroy(void *data)
+{
+ struct device_data *nd = data;
+ pw_log_debug("device %p", nd);
+ spa_hook_remove(&nd->resource_listener);
+ if (nd->device)
+ pw_impl_device_destroy(nd->device);
+}
+
+static const struct pw_resource_events resource_events = {
+ PW_VERSION_RESOURCE_EVENTS,
+ .destroy = resource_destroy
+};
+
+static void device_destroy(void *data)
+{
+ struct device_data *nd = data;
+ spa_list_remove(&nd->link);
+ spa_hook_remove(&nd->device_listener);
+ nd->device = NULL;
+}
+
+static const struct pw_impl_device_events device_events = {
+ PW_VERSION_IMPL_DEVICE_EVENTS,
+ .destroy = device_destroy,
+};
+
+static void *create_object(void *_data,
+ struct pw_resource *resource,
+ const char *type,
+ uint32_t version,
+ struct pw_properties *properties,
+ uint32_t new_id)
+{
+ struct factory_data *data = _data;
+ struct pw_context *context = data->context;
+ struct pw_impl_device *device;
+ const char *str;
+ char *factory_name = NULL;
+ struct device_data *nd;
+ struct pw_impl_client *client;
+ int res;
+
+ if (properties == NULL)
+ goto error_properties;
+
+ if ((str = pw_properties_get(properties, SPA_KEY_FACTORY_NAME)) == NULL)
+ goto error_properties;
+
+ if ((factory_name = strdup(str)) == NULL)
+ goto error_properties;
+
+ pw_properties_setf(properties, PW_KEY_FACTORY_ID, "%d",
+ pw_global_get_id(pw_impl_factory_get_global(data->factory)));
+
+ client = resource ? pw_resource_get_client(resource) : NULL;
+
+ if (client) {
+ pw_properties_setf(properties, PW_KEY_CLIENT_ID, "%d",
+ pw_global_get_id(pw_impl_client_get_global(client)));
+ }
+
+ device = pw_spa_device_load(context,
+ factory_name,
+ 0,
+ properties,
+ sizeof(struct device_data));
+ if (device == NULL) {
+ res = -errno;
+ goto error_device;
+ }
+
+ nd = pw_spa_device_get_user_data(device);
+ nd->device = device;
+ spa_list_append(&data->device_list, &nd->link);
+
+ pw_impl_device_add_listener(device, &nd->device_listener, &device_events, nd);
+
+ if (client) {
+ struct pw_resource *bound_resource;
+
+ res = pw_global_bind(pw_impl_device_get_global(device),
+ client,
+ PW_PERM_ALL, version,
+ new_id);
+ if (res < 0)
+ goto error_bind;
+
+ if ((bound_resource = pw_impl_client_find_resource(client, new_id)) == NULL)
+ goto error_bind;
+
+ pw_resource_add_listener(bound_resource, &nd->resource_listener, &resource_events, nd);
+ }
+ free(factory_name);
+ return device;
+
+error_properties:
+ res = -EINVAL;
+ pw_resource_errorf_id(resource, new_id, res, "usage: "FACTORY_USAGE);
+ goto error_exit_cleanup;
+error_device:
+ pw_resource_errorf_id(resource, new_id, res,
+ "can't create device %s: %s", factory_name,
+ spa_strerror(res));
+ goto error_exit;
+error_bind:
+ pw_resource_errorf_id(resource, new_id, res, "can't bind device");
+ pw_impl_device_destroy(device);
+ goto error_exit;
+
+error_exit_cleanup:
+ pw_properties_free(properties);
+error_exit:
+ free(factory_name);
+ errno = -res;
+ return NULL;
+}
+
+static const struct pw_impl_factory_implementation factory_impl = {
+ PW_VERSION_IMPL_FACTORY_IMPLEMENTATION,
+ .create_object = create_object,
+};
+
+static void factory_destroy(void *data)
+{
+ struct factory_data *d = data;
+ struct device_data *nd;
+
+ spa_hook_remove(&d->factory_listener);
+
+ spa_list_consume(nd, &d->device_list, link)
+ pw_impl_device_destroy(nd->device);
+
+ d->factory = NULL;
+ if (d->module)
+ pw_impl_module_destroy(d->module);
+}
+
+static const struct pw_impl_factory_events factory_events = {
+ PW_VERSION_IMPL_FACTORY_EVENTS,
+ .destroy = factory_destroy,
+};
+
+static void module_destroy(void *data)
+{
+ struct factory_data *d = data;
+
+ spa_hook_remove(&d->module_listener);
+ d->module = NULL;
+ if (d->factory)
+ pw_impl_factory_destroy(d->factory);
+}
+
+static void module_registered(void *data)
+{
+ struct factory_data *d = data;
+ struct pw_impl_module *module = d->module;
+ struct pw_impl_factory *factory = d->factory;
+ struct spa_dict_item items[1];
+ char id[16];
+ int res;
+
+ snprintf(id, sizeof(id), "%d", pw_global_get_id(pw_impl_module_get_global(module)));
+ items[0] = SPA_DICT_ITEM_INIT(PW_KEY_MODULE_ID, id);
+ pw_impl_factory_update_properties(factory, &SPA_DICT_INIT(items, 1));
+
+ if ((res = pw_impl_factory_register(factory, NULL)) < 0) {
+ pw_log_error("%p: can't register factory: %s", factory, spa_strerror(res));
+ }
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy,
+ .registered = module_registered,
+};
+
+SPA_EXPORT
+int pipewire__module_init(struct pw_impl_module *module, const char *args)
+{
+ struct pw_context *context = pw_impl_module_get_context(module);
+ struct pw_impl_factory *factory;
+ struct factory_data *data;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ factory = pw_context_create_factory(context,
+ "spa-device-factory",
+ PW_TYPE_INTERFACE_Device,
+ PW_VERSION_DEVICE,
+ NULL,
+ sizeof(*data));
+ if (factory == NULL)
+ return -errno;
+
+ data = pw_impl_factory_get_user_data(factory);
+ data->factory = factory;
+ data->module = module;
+ data->context = context;
+ spa_list_init(&data->device_list);
+
+ pw_impl_factory_add_listener(factory, &data->factory_listener, &factory_events, data);
+ pw_impl_factory_set_implementation(factory, &factory_impl, data);
+
+ pw_log_debug("module %p: new", module);
+ pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_props));
+
+ pw_impl_module_add_listener(module, &data->module_listener, &module_events, data);
+
+ return 0;
+}
diff --git a/src/modules/spa/module-device.c b/src/modules/spa/module-device.c
new file mode 100644
index 0000000..7bd6821
--- /dev/null
+++ b/src/modules/spa/module-device.c
@@ -0,0 +1,127 @@
+/* PipeWire
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "config.h"
+
+#include <errno.h>
+#include <getopt.h>
+#include <limits.h>
+
+#include <pipewire/impl.h>
+
+#include "spa-device.h"
+
+#define NAME "spa-device"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+#define MODULE_USAGE "<factory> [key=value ...]"
+
+static const struct spa_dict_item module_props[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Load and manage an SPA device" },
+ { PW_KEY_MODULE_USAGE, MODULE_USAGE },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+struct device_data {
+ struct pw_impl_device *this;
+ struct pw_context *context;
+
+ struct spa_hook module_listener;
+};
+
+static void module_destroy(void *_data)
+{
+ struct device_data *data = _data;
+
+ spa_hook_remove(&data->module_listener);
+
+ pw_impl_device_destroy(data->this);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy,
+};
+
+SPA_EXPORT
+int pipewire__module_init(struct pw_impl_module *module, const char *args)
+{
+ struct pw_properties *props = NULL;
+ char **argv = NULL;
+ int n_tokens;
+ struct pw_context *context = pw_impl_module_get_context(module);
+ struct pw_impl_device *device;
+ struct device_data *data;
+ int res;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ if (args == NULL)
+ goto error_arguments;
+
+ argv = pw_split_strv(args, " \t", 2, &n_tokens);
+ if (n_tokens < 1)
+ goto error_arguments;
+
+ if (n_tokens == 2) {
+ props = pw_properties_new_string(argv[1]);
+ if (props == NULL) {
+ res = -errno;
+ goto error_exit_cleanup;
+ }
+ }
+
+ device = pw_spa_device_load(context,
+ argv[0],
+ 0,
+ props,
+ sizeof(struct device_data));
+ if (device == NULL) {
+ res = -errno;
+ goto error_exit_cleanup;
+ }
+
+ pw_free_strv(argv);
+
+ data = pw_spa_device_get_user_data(device);
+ data->this = device;
+ data->context = context;
+
+ pw_log_debug("module %p: new", module);
+ pw_impl_module_add_listener(module, &data->module_listener, &module_events, data);
+
+ pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_props));
+
+ return 0;
+
+error_arguments:
+ res = -EINVAL;
+ pw_log_error("usage: module-spa-device " MODULE_USAGE);
+ goto error_exit_cleanup;
+error_exit_cleanup:
+ pw_free_strv(argv);
+ return res;
+}
diff --git a/src/modules/spa/module-node-factory.c b/src/modules/spa/module-node-factory.c
new file mode 100644
index 0000000..7fb2a42
--- /dev/null
+++ b/src/modules/spa/module-node-factory.c
@@ -0,0 +1,280 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <errno.h>
+#include <dlfcn.h>
+
+#include <spa/utils/result.h>
+
+#include "config.h"
+
+#include "pipewire/impl.h"
+
+#include "spa-node.h"
+
+#define NAME "spa-node-factory"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+#define FACTORY_USAGE SPA_KEY_FACTORY_NAME"=<factory-name> " \
+ "["SPA_KEY_LIBRARY_NAME"=<library-name>]"
+
+static const struct spa_dict_item module_props[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Provide a factory to make SPA nodes" },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+struct factory_data {
+ struct pw_context *context;
+
+ struct pw_impl_module *module;
+ struct spa_hook module_listener;
+
+ struct pw_impl_factory *factory;
+ struct spa_hook factory_listener;
+
+ struct spa_list node_list;
+};
+
+struct node_data {
+ struct factory_data *data;
+ struct spa_list link;
+ struct pw_impl_node *node;
+ struct spa_hook node_listener;
+ struct pw_resource *resource;
+ struct spa_hook resource_listener;
+ unsigned int linger:1;
+};
+
+static void resource_destroy(void *data)
+{
+ struct node_data *nd = data;
+ pw_log_debug("node %p", nd);
+ spa_hook_remove(&nd->resource_listener);
+ nd->resource = NULL;
+ if (nd->node && !nd->linger)
+ pw_impl_node_destroy(nd->node);
+}
+
+static const struct pw_resource_events resource_events = {
+ PW_VERSION_RESOURCE_EVENTS,
+ .destroy = resource_destroy
+};
+
+static void node_destroy(void *data)
+{
+ struct node_data *nd = data;
+ pw_log_debug("node %p", nd);
+ spa_list_remove(&nd->link);
+ spa_hook_remove(&nd->node_listener);
+ nd->node = NULL;
+
+ if (nd->resource) {
+ spa_hook_remove(&nd->resource_listener);
+ nd->resource = NULL;
+ }
+}
+
+static const struct pw_impl_node_events node_events = {
+ PW_VERSION_IMPL_NODE_EVENTS,
+ .destroy = node_destroy,
+};
+
+static void *create_object(void *_data,
+ struct pw_resource *resource,
+ const char *type,
+ uint32_t version,
+ struct pw_properties *properties,
+ uint32_t new_id)
+{
+ struct factory_data *data = _data;
+ struct pw_context *context = data->context;
+ struct pw_impl_node *node;
+ const char *factory_name;
+ struct node_data *nd;
+ int res;
+ struct pw_impl_client *client;
+ bool linger;
+
+ if (properties == NULL)
+ goto error_properties;
+
+ factory_name = pw_properties_get(properties, SPA_KEY_FACTORY_NAME);
+ if (factory_name == NULL)
+ goto error_properties;
+
+ pw_properties_setf(properties, PW_KEY_FACTORY_ID, "%d",
+ pw_global_get_id(pw_impl_factory_get_global(data->factory)));
+
+ linger = pw_properties_get_bool(properties, PW_KEY_OBJECT_LINGER, false);
+
+ client = resource ? pw_resource_get_client(resource) : NULL;
+ if (client && !linger) {
+ pw_properties_setf(properties, PW_KEY_CLIENT_ID, "%d",
+ pw_global_get_id(pw_impl_client_get_global(client)));
+ }
+ node = pw_spa_node_load(context,
+ factory_name,
+ PW_SPA_NODE_FLAG_ACTIVATE,
+ properties,
+ sizeof(struct node_data));
+ if (node == NULL)
+ goto error_create_node;
+
+ nd = pw_spa_node_get_user_data(node);
+ nd->data = data;
+ nd->node = node;
+ nd->linger = linger;
+ spa_list_append(&data->node_list, &nd->link);
+
+ pw_impl_node_add_listener(node, &nd->node_listener, &node_events, nd);
+
+ if (client) {
+ res = pw_global_bind(pw_impl_node_get_global(node),
+ client, PW_PERM_ALL, version, new_id);
+ if (res < 0)
+ goto error_bind;
+
+ if ((nd->resource = pw_impl_client_find_resource(client, new_id)) == NULL)
+ goto error_bind;
+
+ pw_resource_add_listener(nd->resource, &nd->resource_listener, &resource_events, nd);
+ }
+ return node;
+
+error_properties:
+ res = -EINVAL;
+ pw_resource_errorf_id(resource, new_id, res, "usage: "FACTORY_USAGE);
+ goto error_exit_cleanup;
+error_create_node:
+ res = -errno;
+ pw_resource_errorf_id(resource, new_id, res,
+ "can't create node: %s", spa_strerror(res));
+ goto error_exit;
+error_bind:
+ pw_resource_errorf_id(resource, new_id, res, "can't bind node");
+ pw_impl_node_destroy(node);
+ goto error_exit;
+
+error_exit_cleanup:
+ pw_properties_free(properties);
+error_exit:
+ errno = -res;
+ return NULL;
+}
+
+static const struct pw_impl_factory_implementation factory_impl = {
+ PW_VERSION_IMPL_FACTORY_IMPLEMENTATION,
+ .create_object = create_object,
+};
+
+static void factory_destroy(void *data)
+{
+ struct factory_data *d = data;
+ struct node_data *nd;
+
+ spa_hook_remove(&d->factory_listener);
+ spa_list_consume(nd, &d->node_list, link)
+ pw_impl_node_destroy(nd->node);
+ d->factory = NULL;
+ if (d->module)
+ pw_impl_module_destroy(d->module);
+}
+
+static const struct pw_impl_factory_events factory_events = {
+ PW_VERSION_IMPL_FACTORY_EVENTS,
+ .destroy = factory_destroy,
+};
+
+static void module_destroy(void *data)
+{
+ struct factory_data *d = data;
+ spa_hook_remove(&d->module_listener);
+ d->module = NULL;
+ if (d->factory)
+ pw_impl_factory_destroy(d->factory);
+}
+
+static void module_registered(void *data)
+{
+ struct factory_data *d = data;
+ struct pw_impl_module *module = d->module;
+ struct pw_impl_factory *factory = d->factory;
+ struct spa_dict_item items[1];
+ char id[16];
+ int res;
+
+ snprintf(id, sizeof(id), "%d", pw_global_get_id(pw_impl_module_get_global(module)));
+ items[0] = SPA_DICT_ITEM_INIT(PW_KEY_MODULE_ID, id);
+ pw_impl_factory_update_properties(factory, &SPA_DICT_INIT(items, 1));
+
+ if ((res = pw_impl_factory_register(factory, NULL)) < 0) {
+ pw_log_error("%p: can't register factory: %s", factory, spa_strerror(res));
+ }
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy,
+ .registered = module_registered,
+};
+
+SPA_EXPORT
+int pipewire__module_init(struct pw_impl_module *module, const char *args)
+{
+ struct pw_context *context = pw_impl_module_get_context(module);
+ struct pw_impl_factory *factory;
+ struct factory_data *data;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ factory = pw_context_create_factory(context,
+ "spa-node-factory",
+ PW_TYPE_INTERFACE_Node,
+ PW_VERSION_NODE,
+ NULL,
+ sizeof(*data));
+ if (factory == NULL)
+ return -errno;
+
+ data = pw_impl_factory_get_user_data(factory);
+ data->factory = factory;
+ data->context = context;
+ data->module = module;
+ spa_list_init(&data->node_list);
+
+ pw_impl_factory_add_listener(factory, &data->factory_listener, &factory_events, data);
+ pw_impl_factory_set_implementation(factory, &factory_impl, data);
+
+ pw_log_debug("module %p: new", module);
+ pw_impl_module_add_listener(module, &data->module_listener, &module_events, data);
+
+ pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_props));
+
+ return 0;
+}
diff --git a/src/modules/spa/module-node.c b/src/modules/spa/module-node.c
new file mode 100644
index 0000000..9f66d2b
--- /dev/null
+++ b/src/modules/spa/module-node.c
@@ -0,0 +1,129 @@
+/* PipeWire
+ * Copyright © 2016 Axis Communications <dev-gstreamer@axis.com>
+ * @author Linus Svensson <linus.svensson@axis.com>
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "config.h"
+
+#include <errno.h>
+#include <getopt.h>
+#include <limits.h>
+
+#include <pipewire/impl.h>
+
+#include "spa-node.h"
+
+#define NAME "spa-node"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+#define MODULE_USAGE "<factory> [key=value ...]"
+
+static const struct spa_dict_item module_props[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Load and manage an SPA node" },
+ { PW_KEY_MODULE_USAGE, MODULE_USAGE },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+struct node_data {
+ struct pw_impl_node *this;
+ struct pw_context *context;
+ struct pw_properties *properties;
+
+ struct spa_hook module_listener;
+};
+
+static void module_destroy(void *_data)
+{
+ struct node_data *data = _data;
+ spa_hook_remove(&data->module_listener);
+ pw_impl_node_destroy(data->this);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy,
+};
+
+SPA_EXPORT
+int pipewire__module_init(struct pw_impl_module *module, const char *args)
+{
+ struct pw_properties *props = NULL;
+ char **argv = NULL;
+ int n_tokens, res;
+ struct pw_context *context = pw_impl_module_get_context(module);
+ struct pw_impl_node *node;
+ struct node_data *data;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ if (args == NULL)
+ goto error_arguments;
+
+ argv = pw_split_strv(args, " \t", 2, &n_tokens);
+ if (n_tokens < 1)
+ goto error_arguments;
+
+ if (n_tokens == 2) {
+ props = pw_properties_new_string(argv[1]);
+ if (props == NULL) {
+ res = -errno;
+ goto error_exit_cleanup;
+ }
+ }
+
+ node = pw_spa_node_load(context,
+ argv[0],
+ PW_SPA_NODE_FLAG_ACTIVATE,
+ props,
+ sizeof(struct node_data));
+
+ if (node == NULL) {
+ res = -errno;
+ goto error_exit_cleanup;
+ }
+
+ pw_free_strv(argv);
+
+ data = pw_spa_node_get_user_data(node);
+ data->this = node;
+ data->context = context;
+ data->properties = props;
+
+ pw_log_debug("module %p: new", module);
+ pw_impl_module_add_listener(module, &data->module_listener, &module_events, data);
+
+ pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_props));
+
+ return 0;
+
+error_arguments:
+ res = -EINVAL;
+ pw_log_error("usage: module-spa-node " MODULE_USAGE);
+ goto error_exit_cleanup;
+error_exit_cleanup:
+ pw_free_strv(argv);
+ return res;
+}
diff --git a/src/modules/spa/spa-device.c b/src/modules/spa/spa-device.c
new file mode 100644
index 0000000..936793b
--- /dev/null
+++ b/src/modules/spa/spa-device.c
@@ -0,0 +1,161 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "config.h"
+
+#include <string.h>
+#include <stdio.h>
+#include <dlfcn.h>
+
+#include <spa/utils/result.h>
+#include <spa/param/props.h>
+#include <spa/pod/iter.h>
+#include <spa/debug/types.h>
+
+#include "spa-device.h"
+
+struct impl {
+ struct pw_impl_device *this;
+
+ enum pw_spa_device_flags flags;
+
+ void *unload;
+ struct spa_handle *handle;
+ struct spa_device *device;
+
+ struct spa_hook device_listener;
+
+ void *user_data;
+};
+
+static void device_free(void *data)
+{
+ struct impl *impl = data;
+ struct pw_impl_device *device = impl->this;
+
+ pw_log_debug("spa-device %p: free", device);
+
+ spa_hook_remove(&impl->device_listener);
+ if (impl->handle)
+ pw_unload_spa_handle(impl->handle);
+}
+
+static const struct pw_impl_device_events device_events = {
+ PW_VERSION_IMPL_DEVICE_EVENTS,
+ .free = device_free,
+};
+
+struct pw_impl_device *
+pw_spa_device_new(struct pw_context *context,
+ enum pw_spa_device_flags flags,
+ struct spa_device *device,
+ struct spa_handle *handle,
+ struct pw_properties *properties,
+ size_t user_data_size)
+{
+ struct pw_impl_device *this;
+ struct impl *impl;
+ int res;
+
+ this = pw_context_create_device(context, properties, sizeof(struct impl) + user_data_size);
+ if (this == NULL)
+ return NULL;
+
+ impl = pw_impl_device_get_user_data(this);
+ impl->this = this;
+ impl->device = device;
+ impl->handle = handle;
+ impl->flags = flags;
+
+ if (user_data_size > 0)
+ impl->user_data = SPA_PTROFF(impl, sizeof(struct impl), void);
+
+ pw_impl_device_add_listener(this, &impl->device_listener, &device_events, impl);
+ pw_impl_device_set_implementation(this, impl->device);
+
+ if (!SPA_FLAG_IS_SET(impl->flags, PW_SPA_DEVICE_FLAG_NO_REGISTER)) {
+ if ((res = pw_impl_device_register(this, NULL)) < 0)
+ goto error_register;
+ }
+ return this;
+
+error_register:
+ pw_impl_device_destroy(this);
+ errno = -res;
+ return NULL;
+}
+
+void *pw_spa_device_get_user_data(struct pw_impl_device *device)
+{
+ struct impl *impl = pw_impl_device_get_user_data(device);
+ return impl->user_data;
+}
+
+struct pw_impl_device *pw_spa_device_load(struct pw_context *context,
+ const char *factory_name,
+ enum pw_spa_device_flags flags,
+ struct pw_properties *properties,
+ size_t user_data_size)
+{
+ struct pw_impl_device *this;
+ struct spa_handle *handle;
+ void *iface;
+ int res;
+
+ handle = pw_context_load_spa_handle(context, factory_name,
+ properties ? &properties->dict : NULL);
+ if (handle == NULL)
+ goto error_load;
+
+ if ((res = spa_handle_get_interface(handle, SPA_TYPE_INTERFACE_Device, &iface)) < 0)
+ goto error_interface;
+
+ this = pw_spa_device_new(context, flags,
+ iface, handle, properties, user_data_size);
+ if (this == NULL)
+ goto error_device;
+
+ return this;
+
+error_load:
+ res = -errno;
+ pw_log_debug("can't load device handle %s: %m", factory_name);
+ goto error_exit;
+error_interface:
+ pw_log_debug("can't get device interface %s: %s", factory_name,
+ spa_strerror(res));
+ goto error_exit_unload;
+error_device:
+ properties = NULL;
+ res = -errno;
+ pw_log_debug("can't create device %s: %m", factory_name);
+ goto error_exit_unload;
+
+error_exit_unload:
+ pw_unload_spa_handle(handle);
+error_exit:
+ errno = -res;
+ pw_properties_free(properties);
+ return NULL;
+}
diff --git a/src/modules/spa/spa-device.h b/src/modules/spa/spa-device.h
new file mode 100644
index 0000000..2d23b6f
--- /dev/null
+++ b/src/modules/spa/spa-device.h
@@ -0,0 +1,62 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_SPA_DEVICE_H
+#define PIPEWIRE_SPA_DEVICE_H
+
+#include <spa/monitor/device.h>
+
+#include <pipewire/impl.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+enum pw_spa_device_flags {
+ PW_SPA_DEVICE_FLAG_DISABLE = (1 << 0),
+ PW_SPA_DEVICE_FLAG_NO_REGISTER = (1 << 1),
+};
+
+struct pw_impl_device *
+pw_spa_device_new(struct pw_context *context,
+ enum pw_spa_device_flags flags,
+ struct spa_device *device,
+ struct spa_handle *handle,
+ struct pw_properties *properties,
+ size_t user_data_size);
+
+struct pw_impl_device *
+pw_spa_device_load(struct pw_context *context,
+ const char *factory_name,
+ enum pw_spa_device_flags flags,
+ struct pw_properties *properties,
+ size_t user_data_size);
+
+void *pw_spa_device_get_user_data(struct pw_impl_device *device);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PIPEWIRE_SPA_DEVICE_H */
diff --git a/src/modules/spa/spa-node.c b/src/modules/spa/spa-node.c
new file mode 100644
index 0000000..1f8615b
--- /dev/null
+++ b/src/modules/spa/spa-node.c
@@ -0,0 +1,292 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "config.h"
+
+#include <string.h>
+#include <stdio.h>
+#include <dlfcn.h>
+
+#include <spa/node/node.h>
+#include <spa/node/utils.h>
+#include <spa/utils/result.h>
+#include <spa/param/props.h>
+#include <spa/pod/iter.h>
+#include <spa/debug/types.h>
+
+#include "spa-node.h"
+
+struct impl {
+ struct pw_impl_node *this;
+
+ enum pw_spa_node_flags flags;
+
+ struct spa_handle *handle;
+ struct spa_node *node; /**< handle to SPA node */
+
+ struct spa_hook node_listener;
+ int init_pending;
+
+ void *user_data;
+
+ unsigned int async_init:1;
+};
+
+static void spa_node_free(void *data)
+{
+ struct impl *impl = data;
+ struct pw_impl_node *node = impl->this;
+
+ pw_log_debug("spa-node %p: free", node);
+
+ spa_hook_remove(&impl->node_listener);
+ if (impl->handle)
+ pw_unload_spa_handle(impl->handle);
+}
+
+static void complete_init(struct impl *impl)
+{
+ struct pw_impl_node *this = impl->this;
+
+ impl->init_pending = SPA_ID_INVALID;
+
+ if (SPA_FLAG_IS_SET(impl->flags, PW_SPA_NODE_FLAG_ACTIVATE))
+ pw_impl_node_set_active(this, true);
+
+ if (!SPA_FLAG_IS_SET(impl->flags, PW_SPA_NODE_FLAG_NO_REGISTER))
+ pw_impl_node_register(this, NULL);
+ else
+ pw_impl_node_initialized(this);
+}
+
+static void spa_node_result(void *data, int seq, int res, uint32_t type, const void *result)
+{
+ struct impl *impl = data;
+ struct pw_impl_node *node = impl->this;
+
+ if (seq == impl->init_pending) {
+ pw_log_debug("spa-node %p: init complete event %d %d", node, seq, res);
+ complete_init(impl);
+ }
+}
+
+static const struct pw_impl_node_events node_events = {
+ PW_VERSION_IMPL_NODE_EVENTS,
+ .free = spa_node_free,
+ .result = spa_node_result,
+};
+
+struct pw_impl_node *
+pw_spa_node_new(struct pw_context *context,
+ enum pw_spa_node_flags flags,
+ struct spa_node *node,
+ struct spa_handle *handle,
+ struct pw_properties *properties,
+ size_t user_data_size)
+{
+ struct pw_impl_node *this;
+ struct impl *impl;
+ int res;
+
+ this = pw_context_create_node(context, properties, sizeof(struct impl) + user_data_size);
+ if (this == NULL) {
+ res = -errno;
+ goto error_exit;
+ }
+
+ impl = pw_impl_node_get_user_data(this);
+ impl->this = this;
+ impl->node = node;
+ impl->handle = handle;
+ impl->flags = flags;
+
+ if (user_data_size > 0)
+ impl->user_data = SPA_PTROFF(impl, sizeof(struct impl), void);
+
+ pw_impl_node_add_listener(this, &impl->node_listener, &node_events, impl);
+ if ((res = pw_impl_node_set_implementation(this, impl->node)) < 0)
+ goto error_exit_clean_node;
+
+ if (flags & PW_SPA_NODE_FLAG_ASYNC) {
+ impl->init_pending = spa_node_sync(impl->node, res);
+ } else {
+ complete_init(impl);
+ }
+ return this;
+
+error_exit_clean_node:
+ pw_impl_node_destroy(this);
+ handle = NULL;
+error_exit:
+ if (handle)
+ pw_unload_spa_handle(handle);
+ errno = -res;
+ return NULL;
+
+}
+
+void *pw_spa_node_get_user_data(struct pw_impl_node *node)
+{
+ struct impl *impl = pw_impl_node_get_user_data(node);
+ return impl->user_data;
+}
+
+static int
+setup_props(struct pw_context *context, struct spa_node *spa_node, struct pw_properties *pw_props)
+{
+ int res;
+ struct spa_pod *props;
+ void *state = NULL;
+ const char *key;
+ uint32_t index = 0;
+ uint8_t buf[4096];
+ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buf, sizeof(buf));
+ const struct spa_pod_prop *prop = NULL;
+
+ res = spa_node_enum_params_sync(spa_node,
+ SPA_PARAM_Props, &index, NULL, &props,
+ &b);
+ if (res != 1) {
+ if (res < 0)
+ pw_log_debug("spa_node_get_props result: %s", spa_strerror(res));
+ if (res == -ENOTSUP || res == -ENOENT)
+ res = 0;
+ return res;
+ }
+
+ while ((key = pw_properties_iterate(pw_props, &state))) {
+ uint32_t type = 0;
+
+ type = spa_debug_type_find_type(spa_type_props, key);
+ if (type == SPA_TYPE_None)
+ continue;
+
+ if ((prop = spa_pod_find_prop(props, prop, type))) {
+ const char *value = pw_properties_get(pw_props, key);
+
+ if (value == NULL)
+ continue;
+
+ pw_log_debug("configure prop %s to %s", key, value);
+
+ switch(prop->value.type) {
+ case SPA_TYPE_Bool:
+ SPA_POD_VALUE(struct spa_pod_bool, &prop->value) =
+ pw_properties_parse_bool(value);
+ break;
+ case SPA_TYPE_Id:
+ SPA_POD_VALUE(struct spa_pod_id, &prop->value) =
+ pw_properties_parse_int(value);
+ break;
+ case SPA_TYPE_Int:
+ SPA_POD_VALUE(struct spa_pod_int, &prop->value) =
+ pw_properties_parse_int(value);
+ break;
+ case SPA_TYPE_Long:
+ SPA_POD_VALUE(struct spa_pod_long, &prop->value) =
+ pw_properties_parse_int64(value);
+ break;
+ case SPA_TYPE_Float:
+ SPA_POD_VALUE(struct spa_pod_float, &prop->value) =
+ pw_properties_parse_float(value);
+ break;
+ case SPA_TYPE_Double:
+ SPA_POD_VALUE(struct spa_pod_double, &prop->value) =
+ pw_properties_parse_double(value);
+ break;
+ case SPA_TYPE_String:
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ if ((res = spa_node_set_param(spa_node, SPA_PARAM_Props, 0, props)) < 0) {
+ pw_log_debug("spa_node_set_props failed: %s", spa_strerror(res));
+ return res;
+ }
+ return 0;
+}
+
+
+struct pw_impl_node *pw_spa_node_load(struct pw_context *context,
+ const char *factory_name,
+ enum pw_spa_node_flags flags,
+ struct pw_properties *properties,
+ size_t user_data_size)
+{
+ struct pw_impl_node *this;
+ struct spa_node *spa_node;
+ int res;
+ struct spa_handle *handle;
+ void *iface;
+ const struct pw_properties *p;
+
+ if (properties) {
+ p = pw_context_get_properties(context);
+ pw_properties_set(properties, "clock.quantum-limit",
+ pw_properties_get(p, "default.clock.quantum-limit"));
+ }
+
+ handle = pw_context_load_spa_handle(context,
+ factory_name,
+ properties ? &properties->dict : NULL);
+ if (handle == NULL) {
+ res = -errno;
+ goto error_exit;
+ }
+
+ if ((res = spa_handle_get_interface(handle, SPA_TYPE_INTERFACE_Node, &iface)) < 0) {
+ pw_log_error("can't get node interface %d", res);
+ goto error_exit_unload;
+ }
+ if (SPA_RESULT_IS_ASYNC(res))
+ flags |= PW_SPA_NODE_FLAG_ASYNC;
+
+ spa_node = iface;
+
+ if (properties != NULL) {
+ if ((res = setup_props(context, spa_node, properties)) < 0) {
+ pw_log_warn("can't setup properties: %s", spa_strerror(res));
+ }
+ }
+
+ this = pw_spa_node_new(context, flags,
+ spa_node, handle, properties, user_data_size);
+ if (this == NULL) {
+ res = -errno;
+ properties = NULL;
+ goto error_exit_unload;
+ }
+
+ return this;
+
+error_exit_unload:
+ pw_unload_spa_handle(handle);
+error_exit:
+ pw_properties_free(properties);
+ errno = -res;
+ return NULL;
+}
diff --git a/src/modules/spa/spa-node.h b/src/modules/spa/spa-node.h
new file mode 100644
index 0000000..5289a17
--- /dev/null
+++ b/src/modules/spa/spa-node.h
@@ -0,0 +1,63 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_SPA_NODE_H
+#define PIPEWIRE_SPA_NODE_H
+
+#include <spa/node/node.h>
+
+#include <pipewire/impl.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+enum pw_spa_node_flags {
+ PW_SPA_NODE_FLAG_ACTIVATE = (1 << 0),
+ PW_SPA_NODE_FLAG_NO_REGISTER = (1 << 1),
+ PW_SPA_NODE_FLAG_ASYNC = (1 << 2),
+};
+
+struct pw_impl_node *
+pw_spa_node_new(struct pw_context *context,
+ enum pw_spa_node_flags flags,
+ struct spa_node *node,
+ struct spa_handle *handle,
+ struct pw_properties *properties,
+ size_t user_data_size);
+
+struct pw_impl_node *
+pw_spa_node_load(struct pw_context *context,
+ const char *factory_name,
+ enum pw_spa_node_flags flags,
+ struct pw_properties *properties,
+ size_t user_data_size);
+
+void *pw_spa_node_get_user_data(struct pw_impl_node *node);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PIPEWIRE_SPA_NODE_H */