diff options
Diffstat (limited to '')
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, ¶m, &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(¶m)) < 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(¶ms.id), + SPA_POD_Int(¶ms.index), + SPA_POD_Int(¶ms.next), + SPA_POD_PodObject(¶ms.param), + NULL) < 0) + return -EINVAL; + + result = ¶ms; + 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(¶ms[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(¶m)) < 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(¶m)) < 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, ¶m, &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, ¶m, &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", ¶ms[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", ¶ms[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, + ®istry_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, + ®istry_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 ©_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(¶m)) < 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(¶m)) < 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(¶m)) < 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(¶m)) < 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(¶m)) < 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 = ®istry_method_marshal_add_listener, + .bind = ®istry_marshal_bind, + .destroy = ®istry_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] = { ®istry_demarshal_bind, 0, }, + [PW_REGISTRY_METHOD_DESTROY] = { ®istry_demarshal_destroy, 0, }, +}; + +static const struct pw_registry_events pw_protocol_native_registry_event_marshal = { + PW_VERSION_REGISTRY_EVENTS, + .global = ®istry_marshal_global, + .global_remove = ®istry_marshal_global_remove, +}; + +static const struct pw_protocol_native_demarshal +pw_protocol_native_registry_event_demarshal[PW_REGISTRY_EVENT_NUM] = +{ + [PW_REGISTRY_EVENT_GLOBAL] = { ®istry_demarshal_global, 0, }, + [PW_REGISTRY_EVENT_GLOBAL_REMOVE] = { ®istry_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] = { ®istry_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 = ®istry_marshal_global, + .global_remove = ®istry_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, + ®istry_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, ¶ms, + 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(¶m)) < 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(¶m)) < 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(¶ms[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(¶ms[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(¶m)) < 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(¶m)) < 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(¶ms[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(¶ms[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(¶m)) < 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(¶m)) < 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(¶m)) < 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(¶m)) < 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(¶m)) < 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(¶m)) < 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(¶m)) < 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(¶m)) < 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(¶m)) < 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(¶m)) < 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(¶m)) < 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(¶m)) < 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(¶m)) < 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(¶m)) < 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(¶m)) < 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(¶m)) < 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 */ |