diff options
Diffstat (limited to '')
79 files changed, 22161 insertions, 0 deletions
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 |