2023-02-08 18:12:00 +01:00
|
|
|
/* PipeWire */
|
|
|
|
|
/* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans <wim.taymans@gmail.com> */
|
|
|
|
|
/* SPDX-License-Identifier: MIT */
|
2021-04-12 09:33:41 +02:00
|
|
|
|
|
|
|
|
#include <string.h>
|
|
|
|
|
#include <stdio.h>
|
|
|
|
|
#include <errno.h>
|
|
|
|
|
#include <sys/types.h>
|
|
|
|
|
#include <sys/stat.h>
|
|
|
|
|
#include <fcntl.h>
|
|
|
|
|
#include <unistd.h>
|
2024-05-22 09:23:31 +02:00
|
|
|
#include <sys/ioctl.h>
|
2021-04-12 09:33:41 +02:00
|
|
|
#include <sys/socket.h>
|
|
|
|
|
#include <netinet/in.h>
|
|
|
|
|
#include <netinet/tcp.h>
|
|
|
|
|
#include <netinet/ip.h>
|
|
|
|
|
#include <sys/un.h>
|
|
|
|
|
#include <arpa/inet.h>
|
|
|
|
|
|
|
|
|
|
#include "config.h"
|
|
|
|
|
|
|
|
|
|
#include <spa/utils/result.h>
|
2021-05-18 11:36:13 +10:00
|
|
|
#include <spa/utils/string.h>
|
2021-04-12 09:33:41 +02:00
|
|
|
#include <spa/utils/json.h>
|
|
|
|
|
#include <spa/pod/pod.h>
|
|
|
|
|
#include <spa/pod/builder.h>
|
|
|
|
|
#include <spa/debug/types.h>
|
|
|
|
|
#include <spa/param/audio/type-info.h>
|
|
|
|
|
#include <spa/param/audio/format-utils.h>
|
2024-09-18 09:54:34 +02:00
|
|
|
#include <spa/param/audio/raw-json.h>
|
2021-04-12 09:33:41 +02:00
|
|
|
|
|
|
|
|
#include <pipewire/impl.h>
|
|
|
|
|
|
2024-05-22 09:23:31 +02:00
|
|
|
#include "network-utils.h"
|
|
|
|
|
|
2023-11-18 15:11:03 +02:00
|
|
|
/** \page page_module_protocol_simple Protocol Simple
|
2022-04-22 16:08:26 +02:00
|
|
|
*
|
|
|
|
|
* The simple protocol provides a bidirectional audio stream on a network
|
|
|
|
|
* socket.
|
|
|
|
|
*
|
|
|
|
|
* It is meant to be used with the `simple protocol player` app, available on
|
|
|
|
|
* Android to play and record a stream.
|
|
|
|
|
*
|
|
|
|
|
* Each client that connects will create a capture and/or playback stream,
|
|
|
|
|
* depending on the configuration options.
|
|
|
|
|
*
|
2024-05-14 17:30:05 +02:00
|
|
|
* You can also use it to feed audio data to other clients such as the snapcast
|
|
|
|
|
* server.
|
|
|
|
|
*
|
2023-11-20 00:23:03 +02:00
|
|
|
* ## Module Name
|
|
|
|
|
*
|
|
|
|
|
* `libpipewire-module-protocol-simple`
|
|
|
|
|
*
|
2022-04-22 16:08:26 +02:00
|
|
|
* ## Module Options
|
|
|
|
|
*
|
2024-05-14 17:30:05 +02:00
|
|
|
* - `capture`: boolean if capture is enabled. This will create a capture stream or
|
|
|
|
|
* sink for each connected client.
|
|
|
|
|
* - `playback`: boolean if playback is enabled. This will create a playback or
|
|
|
|
|
* source stream for each connected client.
|
2024-07-06 10:09:20 +02:00
|
|
|
* - `local.ifname = <str>`: interface name to use
|
|
|
|
|
* - `local.ifaddress = <str>`: interface address to use
|
2022-04-22 16:08:26 +02:00
|
|
|
* - `server.address = []`: an array of server addresses to listen on as
|
2023-03-22 16:35:55 +01:00
|
|
|
* tcp:(<ip>:)<port>.
|
2024-05-14 17:30:05 +02:00
|
|
|
* - `capture.props`: optional properties for the capture stream
|
|
|
|
|
* - `playback.props`: optional properties for the playback stream
|
2022-04-22 16:08:26 +02:00
|
|
|
*
|
|
|
|
|
* ## General options
|
|
|
|
|
*
|
|
|
|
|
* Options with well-known behavior.
|
|
|
|
|
*
|
|
|
|
|
* - \ref PW_KEY_REMOTE_NAME
|
|
|
|
|
* - \ref PW_KEY_AUDIO_RATE
|
|
|
|
|
* - \ref PW_KEY_AUDIO_FORMAT
|
|
|
|
|
* - \ref PW_KEY_AUDIO_CHANNELS
|
|
|
|
|
* - \ref SPA_KEY_AUDIO_POSITION
|
|
|
|
|
* - \ref PW_KEY_NODE_LATENCY
|
|
|
|
|
* - \ref PW_KEY_NODE_RATE
|
|
|
|
|
* - \ref PW_KEY_STREAM_CAPTURE_SINK
|
2024-04-23 08:39:14 +00:00
|
|
|
* - \ref PW_KEY_NODE_NAME
|
2024-05-14 17:30:05 +02:00
|
|
|
* - \ref PW_KEY_TARGET_OBJECT
|
2022-04-22 16:08:26 +02:00
|
|
|
*
|
|
|
|
|
* By default the server will work with stereo 16 bits samples at 44.1KHz.
|
|
|
|
|
*
|
|
|
|
|
* ## Example configuration
|
|
|
|
|
*
|
|
|
|
|
*\code{.unparsed}
|
2024-09-14 15:43:25 +03:00
|
|
|
* # ~/.config/pipewire/pipewire.conf.d/my-protocol-simple.conf
|
|
|
|
|
*
|
2022-04-22 16:08:26 +02:00
|
|
|
* context.modules = [
|
|
|
|
|
* { name = libpipewire-module-protocol-simple
|
|
|
|
|
* args = {
|
|
|
|
|
* # Provide capture stream, clients can capture data from PipeWire
|
|
|
|
|
* capture = true
|
|
|
|
|
* #
|
|
|
|
|
* # Provide playback stream, client can send data to PipeWire for playback
|
|
|
|
|
* playback = true
|
|
|
|
|
* #
|
|
|
|
|
* #audio.rate = 44100
|
|
|
|
|
* #audio.format = S16
|
|
|
|
|
* #audio.channels = 2
|
|
|
|
|
* #audio.position = [ FL FR ]
|
|
|
|
|
* #
|
|
|
|
|
* # The addresses this server listens on for new
|
|
|
|
|
* # client connections
|
|
|
|
|
* server.address = [
|
|
|
|
|
* "tcp:4711"
|
|
|
|
|
* ]
|
2024-05-14 17:30:05 +02:00
|
|
|
* capture.props = {
|
|
|
|
|
* # The node name or id to use for capture.
|
|
|
|
|
* #target.object = null
|
|
|
|
|
* #
|
|
|
|
|
* # To make the capture stream capture the monitor ports
|
|
|
|
|
* #stream.capture.sink = false
|
|
|
|
|
* #
|
|
|
|
|
* # Make this a sink instead of a capture stream
|
|
|
|
|
* #media.class = Audio/Sink
|
|
|
|
|
* }
|
|
|
|
|
* playback.props = {
|
|
|
|
|
* # The node name or id to use for playback.
|
|
|
|
|
* #target.object = null
|
|
|
|
|
* #
|
|
|
|
|
* # Make this a source instead of a playback stream
|
|
|
|
|
* #media.class = Audio/Source
|
|
|
|
|
* }
|
2022-04-22 16:08:26 +02:00
|
|
|
* }
|
|
|
|
|
* }
|
|
|
|
|
* ]
|
|
|
|
|
*\endcode
|
2024-05-14 17:30:05 +02:00
|
|
|
*
|
|
|
|
|
* ## Example configuration for a snapcast server
|
|
|
|
|
*
|
|
|
|
|
*\code{.unparsed}
|
|
|
|
|
* context.modules = [
|
|
|
|
|
* { name = libpipewire-module-protocol-simple
|
|
|
|
|
* args = {
|
|
|
|
|
* # Provide sink
|
|
|
|
|
* capture = true
|
|
|
|
|
* audio.rate = 48000
|
|
|
|
|
* audio.format = S16
|
|
|
|
|
* audio.channels = 2
|
|
|
|
|
* audio.position = [ FL FR ]
|
|
|
|
|
*
|
|
|
|
|
* # The addresses this server listens on for new
|
|
|
|
|
* # client connections
|
|
|
|
|
* server.address = [
|
|
|
|
|
* "tcp:4711"
|
|
|
|
|
* ]
|
|
|
|
|
* capture.props = {
|
|
|
|
|
* # Make this a sink instead of a capture stream
|
|
|
|
|
* media.class = Audio/Sink
|
|
|
|
|
* }
|
|
|
|
|
* }
|
|
|
|
|
* }
|
|
|
|
|
* ]
|
|
|
|
|
*
|
|
|
|
|
* On the snapcast server, add the following to the `snapserver.conf` file:
|
|
|
|
|
*
|
|
|
|
|
*\code{.unparsed}
|
|
|
|
|
* [stream]
|
|
|
|
|
* sampleformat = 48000:16:2
|
|
|
|
|
* source = tcp://127.0.0.1:4711?name=PipeWireSnapcast&mode=client
|
|
|
|
|
*\endcode
|
|
|
|
|
*
|
|
|
|
|
* Snapcast will try to connect to the protocol-simple server and fetch the
|
|
|
|
|
* samples from it. Snapcast tries to reconnect when the connection is somehow
|
|
|
|
|
* broken.
|
2021-06-24 14:06:30 +10:00
|
|
|
*/
|
|
|
|
|
|
2021-04-12 09:33:41 +02:00
|
|
|
#define NAME "protocol-simple"
|
|
|
|
|
|
2021-09-16 15:35:10 +10:00
|
|
|
PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
|
|
|
|
|
#define PW_LOG_TOPIC_DEFAULT mod_topic
|
|
|
|
|
|
2021-04-12 09:33:41 +02:00
|
|
|
#define DEFAULT_PORT 4711
|
|
|
|
|
#define DEFAULT_SERVER "[ \"tcp:"SPA_STRINGIFY(DEFAULT_PORT)"\" ]"
|
|
|
|
|
|
2024-05-22 09:23:31 +02:00
|
|
|
#define DEFAULT_FORMAT "S16LE"
|
2022-06-04 18:54:50 +02:00
|
|
|
#define DEFAULT_RATE 44100
|
2021-10-12 14:29:57 +10:00
|
|
|
#define DEFAULT_CHANNELS 2
|
2021-04-12 09:33:41 +02:00
|
|
|
#define DEFAULT_POSITION "[ FL FR ]"
|
|
|
|
|
|
2021-06-17 18:37:04 +02:00
|
|
|
#define MAX_CLIENTS 10
|
|
|
|
|
|
2023-03-22 16:35:55 +01:00
|
|
|
#define MODULE_USAGE "( capture=<bool> ) " \
|
|
|
|
|
"( playback=<bool> ) " \
|
|
|
|
|
"( remote.name=<remote> ) " \
|
|
|
|
|
"( node.rate=<1/rate, default:1/"SPA_STRINGIFY(DEFAULT_RATE)"> ) " \
|
|
|
|
|
"( audio.rate=<sample-rate, default:"SPA_STRINGIFY(DEFAULT_RATE)"> ) " \
|
|
|
|
|
"( audio.format=<format, default:"DEFAULT_FORMAT"> ) " \
|
|
|
|
|
"( audio.channels=<channels, default: "SPA_STRINGIFY(DEFAULT_CHANNELS)"> ) " \
|
|
|
|
|
"( audio.position=<position, default:"DEFAULT_POSITION"> ) " \
|
2024-05-14 17:30:05 +02:00
|
|
|
"( server.address=<[ tcp:(<ip>:)<port>(,...) ], default:"DEFAULT_SERVER"> ) " \
|
|
|
|
|
"( capture.props={ ... } ) " \
|
|
|
|
|
"( playback.props={ ... } )" \
|
2021-04-12 09:33:41 +02:00
|
|
|
|
|
|
|
|
static const struct spa_dict_item module_props[] = {
|
|
|
|
|
{ PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
|
|
|
|
|
{ PW_KEY_MODULE_DESCRIPTION, "Implements a simple protocol" },
|
|
|
|
|
{ PW_KEY_MODULE_USAGE, MODULE_USAGE },
|
|
|
|
|
{ PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
struct impl {
|
|
|
|
|
struct pw_loop *loop;
|
|
|
|
|
struct pw_context *context;
|
|
|
|
|
|
|
|
|
|
struct pw_properties *props;
|
|
|
|
|
struct spa_hook module_listener;
|
|
|
|
|
struct spa_list server_list;
|
|
|
|
|
|
2021-04-21 13:03:08 +02:00
|
|
|
struct pw_work_queue *work_queue;
|
2021-04-12 09:33:41 +02:00
|
|
|
|
2024-05-14 17:30:05 +02:00
|
|
|
struct pw_properties *capture_props;
|
|
|
|
|
struct pw_properties *playback_props;
|
|
|
|
|
|
2024-07-06 10:09:20 +02:00
|
|
|
char *ifname;
|
|
|
|
|
char *ifaddress;
|
2021-04-12 09:33:41 +02:00
|
|
|
bool capture;
|
|
|
|
|
bool playback;
|
|
|
|
|
|
2024-05-14 17:30:05 +02:00
|
|
|
struct spa_audio_info_raw capture_info;
|
|
|
|
|
struct spa_audio_info_raw playback_info;
|
|
|
|
|
uint32_t capture_frame_size;
|
|
|
|
|
uint32_t playback_frame_size;
|
2021-04-12 09:33:41 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
struct client {
|
|
|
|
|
struct spa_list link;
|
|
|
|
|
struct impl *impl;
|
|
|
|
|
struct server *server;
|
|
|
|
|
|
|
|
|
|
struct pw_core *core;
|
|
|
|
|
struct spa_hook core_proxy_listener;
|
|
|
|
|
|
|
|
|
|
struct spa_source *source;
|
2022-01-13 13:15:44 +01:00
|
|
|
char name[128];
|
2021-04-12 09:33:41 +02:00
|
|
|
|
|
|
|
|
struct pw_stream *capture;
|
|
|
|
|
struct spa_hook capture_listener;
|
|
|
|
|
|
|
|
|
|
struct pw_stream *playback;
|
|
|
|
|
struct spa_hook playback_listener;
|
|
|
|
|
|
|
|
|
|
unsigned int disconnect:1;
|
|
|
|
|
unsigned int disconnecting:1;
|
2021-08-25 21:19:25 +02:00
|
|
|
unsigned int cleanup:1;
|
2021-04-12 09:33:41 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
struct server {
|
|
|
|
|
struct spa_list link;
|
|
|
|
|
struct impl *impl;
|
|
|
|
|
|
|
|
|
|
#define SERVER_TYPE_INVALID 0
|
|
|
|
|
#define SERVER_TYPE_UNIX 1
|
2024-05-22 09:23:31 +02:00
|
|
|
#define SERVER_TYPE_TCP 2
|
2021-04-12 09:33:41 +02:00
|
|
|
uint32_t type;
|
2024-05-22 09:23:31 +02:00
|
|
|
struct sockaddr_storage addr;
|
2021-04-12 09:33:41 +02:00
|
|
|
struct spa_source *source;
|
|
|
|
|
|
|
|
|
|
struct spa_list client_list;
|
2021-06-17 18:37:04 +02:00
|
|
|
uint32_t n_clients;
|
2021-04-12 09:33:41 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
static void client_disconnect(struct client *client)
|
|
|
|
|
{
|
|
|
|
|
struct impl *impl = client->impl;
|
|
|
|
|
|
|
|
|
|
if (client->disconnect)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
client->disconnect = true;
|
|
|
|
|
|
|
|
|
|
if (client->source)
|
|
|
|
|
pw_loop_destroy_source(impl->loop, client->source);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void client_free(struct client *client)
|
|
|
|
|
{
|
|
|
|
|
struct impl *impl = client->impl;
|
|
|
|
|
|
2021-09-16 15:35:10 +10:00
|
|
|
pw_log_info("%p: client:%p [%s] free", impl, client, client->name);
|
2021-04-12 09:33:41 +02:00
|
|
|
|
|
|
|
|
client_disconnect(client);
|
|
|
|
|
|
2021-08-25 21:19:25 +02:00
|
|
|
pw_work_queue_cancel(impl->work_queue, client, SPA_ID_INVALID);
|
|
|
|
|
|
2021-04-12 09:33:41 +02:00
|
|
|
spa_list_remove(&client->link);
|
2021-06-17 18:37:04 +02:00
|
|
|
client->server->n_clients--;
|
2021-04-12 09:33:41 +02:00
|
|
|
|
|
|
|
|
if (client->capture)
|
|
|
|
|
pw_stream_destroy(client->capture);
|
|
|
|
|
if (client->playback)
|
|
|
|
|
pw_stream_destroy(client->playback);
|
|
|
|
|
if (client->core) {
|
|
|
|
|
client->disconnecting = true;
|
|
|
|
|
spa_hook_remove(&client->core_proxy_listener);
|
|
|
|
|
pw_core_disconnect(client->core);
|
|
|
|
|
}
|
|
|
|
|
free(client);
|
|
|
|
|
}
|
|
|
|
|
|
2021-04-21 13:03:08 +02:00
|
|
|
|
|
|
|
|
static void on_client_cleanup(void *obj, void *data, int res, uint32_t id)
|
|
|
|
|
{
|
|
|
|
|
struct client *c = obj;
|
|
|
|
|
client_free(c);
|
|
|
|
|
}
|
|
|
|
|
|
2021-04-12 09:33:41 +02:00
|
|
|
static void client_cleanup(struct client *client)
|
|
|
|
|
{
|
|
|
|
|
struct impl *impl = client->impl;
|
2021-08-25 21:19:25 +02:00
|
|
|
if (!client->cleanup) {
|
|
|
|
|
client->cleanup = true;
|
|
|
|
|
pw_work_queue_add(impl->work_queue, client, 0, on_client_cleanup, impl);
|
|
|
|
|
}
|
2021-04-12 09:33:41 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void
|
|
|
|
|
on_client_data(void *data, int fd, uint32_t mask)
|
|
|
|
|
{
|
|
|
|
|
struct client *client = data;
|
|
|
|
|
struct impl *impl = client->impl;
|
|
|
|
|
int res;
|
|
|
|
|
|
|
|
|
|
if (mask & SPA_IO_HUP) {
|
|
|
|
|
res = -EPIPE;
|
|
|
|
|
goto error;
|
|
|
|
|
}
|
|
|
|
|
if (mask & SPA_IO_ERR) {
|
|
|
|
|
res = -EIO;
|
|
|
|
|
goto error;
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
error:
|
|
|
|
|
if (res == -EPIPE)
|
2021-09-16 15:35:10 +10:00
|
|
|
pw_log_info("%p: client:%p [%s] disconnected", impl, client, client->name);
|
2021-04-12 09:33:41 +02:00
|
|
|
else {
|
2021-09-16 15:35:10 +10:00
|
|
|
pw_log_error("%p: client:%p [%s] error %d (%s)", impl,
|
2021-04-12 09:33:41 +02:00
|
|
|
client, client->name, res, spa_strerror(res));
|
|
|
|
|
}
|
|
|
|
|
client_cleanup(client);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void capture_process(void *data)
|
|
|
|
|
{
|
|
|
|
|
struct client *client = data;
|
|
|
|
|
struct impl *impl = client->impl;
|
|
|
|
|
struct pw_buffer *buf;
|
|
|
|
|
struct spa_data *d;
|
|
|
|
|
uint32_t size, offset;
|
|
|
|
|
int res;
|
|
|
|
|
|
|
|
|
|
if ((buf = pw_stream_dequeue_buffer(client->capture)) == NULL) {
|
2021-07-19 18:30:22 +02:00
|
|
|
pw_log_debug("%p: client:%p [%s] out of capture buffers: %m", impl,
|
2021-04-12 09:33:41 +02:00
|
|
|
client, client->name);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
d = &buf->buffer->datas[0];
|
|
|
|
|
|
2022-06-04 11:47:48 +02:00
|
|
|
offset = SPA_MIN(d->chunk->offset, d->maxsize);
|
|
|
|
|
size = SPA_MIN(d->chunk->size, d->maxsize - offset);
|
2021-11-25 10:24:09 +01:00
|
|
|
|
2021-04-12 09:33:41 +02:00
|
|
|
while (size > 0) {
|
|
|
|
|
res = send(client->source->fd,
|
2021-05-06 13:41:44 +10:00
|
|
|
SPA_PTROFF(d->data, offset, void),
|
2021-04-12 09:33:41 +02:00
|
|
|
size,
|
|
|
|
|
MSG_NOSIGNAL | MSG_DONTWAIT);
|
|
|
|
|
if (res < 0) {
|
|
|
|
|
if (errno == EINTR)
|
|
|
|
|
continue;
|
2024-02-05 14:04:39 +01:00
|
|
|
if (errno != EAGAIN && errno != EWOULDBLOCK) {
|
2021-04-12 09:33:41 +02:00
|
|
|
pw_log_warn("%p: client:%p [%s] send error %d: %m", impl,
|
|
|
|
|
client, client->name, res);
|
2024-02-05 14:04:39 +01:00
|
|
|
client_cleanup(client);
|
|
|
|
|
}
|
2021-04-12 09:33:41 +02:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
offset += res;
|
|
|
|
|
size -= res;
|
|
|
|
|
}
|
|
|
|
|
pw_stream_queue_buffer(client->capture, buf);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void playback_process(void *data)
|
|
|
|
|
{
|
|
|
|
|
struct client *client = data;
|
|
|
|
|
struct impl *impl = client->impl;
|
|
|
|
|
struct pw_buffer *buf;
|
|
|
|
|
uint32_t size, offset;
|
|
|
|
|
struct spa_data *d;
|
|
|
|
|
int res;
|
|
|
|
|
|
|
|
|
|
if ((buf = pw_stream_dequeue_buffer(client->playback)) == NULL) {
|
2021-07-19 18:30:22 +02:00
|
|
|
pw_log_debug("%p: client:%p [%s] out of playback buffers: %m", impl,
|
2021-04-12 09:33:41 +02:00
|
|
|
client, client->name);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
d = &buf->buffer->datas[0];
|
|
|
|
|
|
2022-03-29 09:18:18 +02:00
|
|
|
size = d->maxsize;
|
|
|
|
|
if (buf->requested)
|
2024-05-14 17:30:05 +02:00
|
|
|
size = SPA_MIN(size, buf->requested * impl->playback_frame_size);
|
2021-04-12 09:33:41 +02:00
|
|
|
|
|
|
|
|
offset = 0;
|
|
|
|
|
while (size > 0) {
|
|
|
|
|
res = recv(client->source->fd,
|
2021-05-06 13:41:44 +10:00
|
|
|
SPA_PTROFF(d->data, offset, void),
|
2021-04-12 09:33:41 +02:00
|
|
|
size,
|
|
|
|
|
MSG_DONTWAIT);
|
|
|
|
|
if (res == 0) {
|
|
|
|
|
pw_log_info("%p: client:%p [%s] disconnect", impl,
|
|
|
|
|
client, client->name);
|
|
|
|
|
client_cleanup(client);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
if (res < 0) {
|
|
|
|
|
if (errno == EINTR)
|
|
|
|
|
continue;
|
|
|
|
|
if (errno != EAGAIN && errno != EWOULDBLOCK)
|
|
|
|
|
pw_log_warn("%p: client:%p [%s] recv error %d: %m",
|
|
|
|
|
impl, client, client->name, res);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
offset += res;
|
|
|
|
|
size -= res;
|
|
|
|
|
}
|
|
|
|
|
d->chunk->offset = 0;
|
|
|
|
|
d->chunk->size = offset;
|
2024-05-14 17:30:05 +02:00
|
|
|
d->chunk->stride = impl->playback_frame_size;
|
2021-04-12 09:33:41 +02:00
|
|
|
|
|
|
|
|
pw_stream_queue_buffer(client->playback, buf);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void capture_destroy(void *data)
|
|
|
|
|
{
|
|
|
|
|
struct client *client = data;
|
|
|
|
|
spa_hook_remove(&client->capture_listener);
|
|
|
|
|
client->capture = NULL;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void on_stream_state_changed(void *data, enum pw_stream_state old,
|
|
|
|
|
enum pw_stream_state state, const char *error)
|
|
|
|
|
{
|
|
|
|
|
struct client *client = data;
|
|
|
|
|
struct impl *impl = client->impl;
|
|
|
|
|
|
|
|
|
|
switch (state) {
|
|
|
|
|
case PW_STREAM_STATE_ERROR:
|
|
|
|
|
case PW_STREAM_STATE_UNCONNECTED:
|
|
|
|
|
if (!client->disconnect) {
|
|
|
|
|
pw_log_info("%p: client:%p [%s] stream error %s",
|
|
|
|
|
impl, client, client->name,
|
|
|
|
|
pw_stream_state_as_string(state));
|
|
|
|
|
client_cleanup(client);
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void playback_destroy(void *data)
|
|
|
|
|
{
|
|
|
|
|
struct client *client = data;
|
|
|
|
|
spa_hook_remove(&client->playback_listener);
|
|
|
|
|
client->playback = NULL;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static const struct pw_stream_events capture_stream_events = {
|
|
|
|
|
PW_VERSION_STREAM_EVENTS,
|
|
|
|
|
.destroy = capture_destroy,
|
|
|
|
|
.state_changed = on_stream_state_changed,
|
|
|
|
|
.process = capture_process
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
static const struct pw_stream_events playback_stream_events = {
|
|
|
|
|
PW_VERSION_STREAM_EVENTS,
|
|
|
|
|
.destroy = playback_destroy,
|
|
|
|
|
.state_changed = on_stream_state_changed,
|
|
|
|
|
.process = playback_process
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
static int create_streams(struct impl *impl, struct client *client)
|
|
|
|
|
{
|
|
|
|
|
uint32_t n_params;
|
|
|
|
|
const struct spa_pod *params[1];
|
|
|
|
|
uint8_t buffer[1024];
|
2024-05-14 17:30:05 +02:00
|
|
|
struct spa_pod_builder b;
|
2021-04-12 09:33:41 +02:00
|
|
|
struct pw_properties *props;
|
|
|
|
|
int res;
|
|
|
|
|
|
|
|
|
|
if (impl->capture) {
|
2024-05-14 17:30:05 +02:00
|
|
|
if ((props = pw_properties_copy(impl->capture_props)) == NULL)
|
2021-04-12 09:33:41 +02:00
|
|
|
return -errno;
|
|
|
|
|
|
|
|
|
|
pw_properties_setf(props,
|
|
|
|
|
PW_KEY_MEDIA_NAME, "%s capture", client->name);
|
|
|
|
|
client->capture = pw_stream_new(client->core,
|
|
|
|
|
pw_properties_get(props, PW_KEY_MEDIA_NAME),
|
|
|
|
|
props);
|
|
|
|
|
if (client->capture == NULL)
|
|
|
|
|
return -errno;
|
|
|
|
|
|
|
|
|
|
pw_stream_add_listener(client->capture, &client->capture_listener,
|
|
|
|
|
&capture_stream_events, client);
|
|
|
|
|
}
|
|
|
|
|
if (impl->playback) {
|
2024-05-14 17:30:05 +02:00
|
|
|
props = pw_properties_copy(impl->playback_props);
|
2021-04-12 09:33:41 +02:00
|
|
|
if (props == NULL)
|
|
|
|
|
return -errno;
|
|
|
|
|
|
|
|
|
|
pw_properties_setf(props,
|
|
|
|
|
PW_KEY_MEDIA_NAME, "%s playback", client->name);
|
|
|
|
|
|
|
|
|
|
client->playback = pw_stream_new(client->core,
|
|
|
|
|
pw_properties_get(props, PW_KEY_MEDIA_NAME),
|
|
|
|
|
props);
|
|
|
|
|
if (client->playback == NULL)
|
|
|
|
|
return -errno;
|
|
|
|
|
|
|
|
|
|
pw_stream_add_listener(client->playback, &client->playback_listener,
|
|
|
|
|
&playback_stream_events, client);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (impl->capture) {
|
2024-05-14 17:30:05 +02:00
|
|
|
n_params = 0;
|
|
|
|
|
spa_pod_builder_init(&b, buffer, sizeof(buffer));
|
|
|
|
|
params[n_params++] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat,
|
|
|
|
|
&impl->capture_info);
|
|
|
|
|
|
2021-04-12 09:33:41 +02:00
|
|
|
if ((res = pw_stream_connect(client->capture,
|
|
|
|
|
PW_DIRECTION_INPUT,
|
|
|
|
|
PW_ID_ANY,
|
|
|
|
|
PW_STREAM_FLAG_AUTOCONNECT |
|
|
|
|
|
PW_STREAM_FLAG_MAP_BUFFERS |
|
|
|
|
|
PW_STREAM_FLAG_RT_PROCESS,
|
|
|
|
|
params, n_params)) < 0)
|
|
|
|
|
return res;
|
|
|
|
|
}
|
|
|
|
|
if (impl->playback) {
|
2024-05-14 17:30:05 +02:00
|
|
|
n_params = 0;
|
|
|
|
|
spa_pod_builder_init(&b, buffer, sizeof(buffer));
|
|
|
|
|
params[n_params++] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat,
|
|
|
|
|
&impl->playback_info);
|
|
|
|
|
|
2021-04-12 09:33:41 +02:00
|
|
|
if ((res = pw_stream_connect(client->playback,
|
|
|
|
|
PW_DIRECTION_OUTPUT,
|
|
|
|
|
PW_ID_ANY,
|
|
|
|
|
PW_STREAM_FLAG_AUTOCONNECT |
|
|
|
|
|
PW_STREAM_FLAG_MAP_BUFFERS |
|
|
|
|
|
PW_STREAM_FLAG_RT_PROCESS,
|
|
|
|
|
params, n_params)) < 0)
|
|
|
|
|
return res;
|
|
|
|
|
}
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void on_core_proxy_destroy(void *data)
|
|
|
|
|
{
|
|
|
|
|
struct client *client = data;
|
|
|
|
|
spa_hook_remove(&client->core_proxy_listener);
|
|
|
|
|
client->core = NULL;
|
|
|
|
|
client_cleanup(client);
|
|
|
|
|
}
|
|
|
|
|
|
2021-06-27 17:47:13 +02:00
|
|
|
static const struct pw_proxy_events core_proxy_events = {
|
2021-04-12 09:33:41 +02:00
|
|
|
PW_VERSION_CORE_EVENTS,
|
|
|
|
|
.destroy = on_core_proxy_destroy,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
static void
|
|
|
|
|
on_connect(void *data, int fd, uint32_t mask)
|
|
|
|
|
{
|
|
|
|
|
struct server *server = data;
|
|
|
|
|
struct impl *impl = server->impl;
|
2022-01-13 13:21:28 +01:00
|
|
|
struct sockaddr_in addr;
|
2021-04-12 09:33:41 +02:00
|
|
|
socklen_t addrlen;
|
|
|
|
|
int client_fd, val;
|
|
|
|
|
struct client *client = NULL;
|
|
|
|
|
struct pw_properties *props = NULL;
|
|
|
|
|
|
|
|
|
|
addrlen = sizeof(addr);
|
2023-02-25 20:21:45 +01:00
|
|
|
client_fd = accept4(fd, (struct sockaddr *) &addr, &addrlen, SOCK_NONBLOCK | SOCK_CLOEXEC);
|
2021-04-12 09:33:41 +02:00
|
|
|
if (client_fd < 0)
|
|
|
|
|
goto error;
|
|
|
|
|
|
2021-06-17 18:37:04 +02:00
|
|
|
if (server->n_clients >= MAX_CLIENTS) {
|
|
|
|
|
close(client_fd);
|
|
|
|
|
errno = ECONNREFUSED;
|
|
|
|
|
goto error;
|
|
|
|
|
}
|
|
|
|
|
|
2021-04-12 09:33:41 +02:00
|
|
|
client = calloc(1, sizeof(struct client));
|
|
|
|
|
if (client == NULL)
|
|
|
|
|
goto error;
|
|
|
|
|
|
|
|
|
|
client->impl = impl;
|
|
|
|
|
client->server = server;
|
|
|
|
|
spa_list_append(&server->client_list, &client->link);
|
2021-06-17 18:37:04 +02:00
|
|
|
server->n_clients++;
|
2021-04-12 09:33:41 +02:00
|
|
|
|
2022-01-13 13:21:28 +01:00
|
|
|
if (inet_ntop(addr.sin_family, &addr.sin_addr.s_addr, client->name, sizeof(client->name)) == NULL)
|
2021-04-12 09:33:41 +02:00
|
|
|
snprintf(client->name, sizeof(client->name), "client %d", client_fd);
|
|
|
|
|
|
2021-06-17 18:37:04 +02:00
|
|
|
client->source = pw_loop_add_io(impl->loop,
|
|
|
|
|
client_fd,
|
|
|
|
|
SPA_IO_ERR | SPA_IO_HUP,
|
|
|
|
|
true, on_client_data, client);
|
|
|
|
|
if (client->source == NULL)
|
|
|
|
|
goto error;
|
|
|
|
|
|
2021-09-16 15:35:10 +10:00
|
|
|
pw_log_info("%p: client:%p [%s] connected", impl, client, client->name);
|
2021-04-12 09:33:41 +02:00
|
|
|
|
|
|
|
|
props = pw_properties_new(
|
|
|
|
|
PW_KEY_CLIENT_API, "protocol-simple",
|
2022-04-22 16:08:26 +02:00
|
|
|
PW_KEY_REMOTE_NAME,
|
|
|
|
|
pw_properties_get(impl->props, PW_KEY_REMOTE_NAME),
|
2021-04-12 09:33:41 +02:00
|
|
|
NULL);
|
|
|
|
|
if (props == NULL)
|
|
|
|
|
goto error;
|
|
|
|
|
|
|
|
|
|
pw_properties_setf(props,
|
|
|
|
|
"protocol.server.type", "%s",
|
2024-05-22 09:23:31 +02:00
|
|
|
server->type == SERVER_TYPE_TCP ? "tcp" : "unix");
|
2021-04-12 09:33:41 +02:00
|
|
|
|
|
|
|
|
if (server->type == SERVER_TYPE_UNIX) {
|
|
|
|
|
goto error;
|
2024-05-22 09:23:31 +02:00
|
|
|
} else if (server->type == SERVER_TYPE_TCP) {
|
2021-04-12 09:33:41 +02:00
|
|
|
val = 1;
|
|
|
|
|
if (setsockopt(client_fd, IPPROTO_TCP, TCP_NODELAY,
|
|
|
|
|
(const void *) &val, sizeof(val)) < 0)
|
|
|
|
|
pw_log_warn("TCP_NODELAY failed: %m");
|
|
|
|
|
|
|
|
|
|
val = IPTOS_LOWDELAY;
|
|
|
|
|
if (setsockopt(client_fd, IPPROTO_IP, IP_TOS,
|
|
|
|
|
(const void *) &val, sizeof(val)) < 0)
|
|
|
|
|
pw_log_warn("IP_TOS failed: %m");
|
|
|
|
|
|
|
|
|
|
pw_properties_set(props, PW_KEY_CLIENT_ACCESS, "restricted");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
client->core = pw_context_connect(impl->context, props, 0);
|
|
|
|
|
props = NULL;
|
|
|
|
|
if (client->core == NULL)
|
|
|
|
|
goto error;
|
|
|
|
|
|
|
|
|
|
pw_proxy_add_listener((struct pw_proxy*)client->core,
|
|
|
|
|
&client->core_proxy_listener, &core_proxy_events,
|
|
|
|
|
client);
|
|
|
|
|
|
|
|
|
|
create_streams(impl, client);
|
|
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
error:
|
2021-09-16 15:35:10 +10:00
|
|
|
pw_log_error("%p: failed to create client: %m", impl);
|
2021-06-01 11:21:17 +10:00
|
|
|
pw_properties_free(props);
|
2021-04-12 09:33:41 +02:00
|
|
|
if (client != NULL)
|
|
|
|
|
client_free(client);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-06 10:09:20 +02:00
|
|
|
static int make_tcp_socket(struct server *server, const char *name, const char *ifname,
|
|
|
|
|
const char *ifaddress)
|
2024-05-22 09:23:31 +02:00
|
|
|
{
|
|
|
|
|
struct sockaddr_storage addr;
|
2021-04-12 09:33:41 +02:00
|
|
|
int res, fd, on;
|
2024-05-22 09:23:31 +02:00
|
|
|
socklen_t len = 0;
|
|
|
|
|
|
2024-07-06 10:09:20 +02:00
|
|
|
if ((res = pw_net_parse_address_port(name, ifaddress, DEFAULT_PORT, &addr, &len)) < 0) {
|
|
|
|
|
pw_log_error("%p: can't parse address %s: %s", server,
|
|
|
|
|
name, spa_strerror(res));
|
2024-05-22 09:23:31 +02:00
|
|
|
goto error;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ((fd = socket(addr.ss_family, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0)) < 0) {
|
2021-04-12 09:33:41 +02:00
|
|
|
res = -errno;
|
2021-09-16 15:35:10 +10:00
|
|
|
pw_log_error("%p: socket() failed: %m", server);
|
2021-04-12 09:33:41 +02:00
|
|
|
goto error;
|
|
|
|
|
}
|
2024-07-06 10:09:20 +02:00
|
|
|
#ifdef SO_BINDTODEVICE
|
|
|
|
|
if (ifname && setsockopt(fd, SOL_SOCKET, SO_BINDTODEVICE, ifname, strlen(ifname)) < 0) {
|
|
|
|
|
res = -errno;
|
|
|
|
|
pw_log_error("%p: setsockopt(SO_BINDTODEVICE) failed: %m", server);
|
|
|
|
|
goto error;
|
|
|
|
|
}
|
|
|
|
|
#endif
|
2021-04-12 09:33:41 +02:00
|
|
|
on = 1;
|
|
|
|
|
if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (const void *) &on, sizeof(on)) < 0)
|
2021-09-16 15:35:10 +10:00
|
|
|
pw_log_warn("%p: setsockopt(): %m", server);
|
2021-04-12 09:33:41 +02:00
|
|
|
|
2024-05-22 09:23:31 +02:00
|
|
|
if (bind(fd, (struct sockaddr *) &addr, len) < 0) {
|
2021-04-12 09:33:41 +02:00
|
|
|
res = -errno;
|
2021-09-16 15:35:10 +10:00
|
|
|
pw_log_error("%p: bind() failed: %m", server);
|
2021-04-12 09:33:41 +02:00
|
|
|
goto error_close;
|
|
|
|
|
}
|
|
|
|
|
if (listen(fd, 5) < 0) {
|
|
|
|
|
res = -errno;
|
2021-09-16 15:35:10 +10:00
|
|
|
pw_log_error("%p: listen() failed: %m", server);
|
2021-04-12 09:33:41 +02:00
|
|
|
goto error_close;
|
|
|
|
|
}
|
2024-05-22 09:23:31 +02:00
|
|
|
if (getsockname(fd, (struct sockaddr *)&addr, &len) < 0) {
|
|
|
|
|
res = -errno;
|
|
|
|
|
pw_log_error("%p: getsockname() failed: %m", server);
|
|
|
|
|
goto error_close;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
server->type = SERVER_TYPE_TCP;
|
|
|
|
|
server->addr = addr;
|
2021-04-12 09:33:41 +02:00
|
|
|
|
|
|
|
|
return fd;
|
|
|
|
|
|
|
|
|
|
error_close:
|
|
|
|
|
close(fd);
|
|
|
|
|
error:
|
|
|
|
|
return res;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void server_free(struct server *server)
|
|
|
|
|
{
|
|
|
|
|
struct impl *impl = server->impl;
|
|
|
|
|
struct client *c;
|
|
|
|
|
|
2021-09-16 15:35:10 +10:00
|
|
|
pw_log_debug("%p: free server %p", impl, server);
|
2021-04-12 09:33:41 +02:00
|
|
|
|
|
|
|
|
spa_list_remove(&server->link);
|
|
|
|
|
spa_list_consume(c, &server->client_list, link)
|
|
|
|
|
client_free(c);
|
|
|
|
|
if (server->source)
|
|
|
|
|
pw_loop_destroy_source(impl->loop, server->source);
|
|
|
|
|
free(server);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static struct server *create_server(struct impl *impl, const char *address)
|
|
|
|
|
{
|
|
|
|
|
int fd, res;
|
|
|
|
|
struct server *server;
|
|
|
|
|
|
|
|
|
|
server = calloc(1, sizeof(struct server));
|
|
|
|
|
if (server == NULL)
|
|
|
|
|
return NULL;
|
|
|
|
|
|
|
|
|
|
server->impl = impl;
|
|
|
|
|
spa_list_init(&server->client_list);
|
|
|
|
|
spa_list_append(&impl->server_list, &server->link);
|
|
|
|
|
|
2021-08-02 14:05:45 +10:00
|
|
|
if (spa_strstartswith(address, "tcp:")) {
|
2024-07-06 10:09:20 +02:00
|
|
|
fd = make_tcp_socket(server, address+4, impl->ifname, impl->ifaddress);
|
2021-04-12 09:33:41 +02:00
|
|
|
} else {
|
2024-05-22 09:23:31 +02:00
|
|
|
pw_log_error("address %s does not start with tcp:", address);
|
2021-04-12 09:33:41 +02:00
|
|
|
fd = -EINVAL;
|
|
|
|
|
}
|
|
|
|
|
if (fd < 0) {
|
|
|
|
|
res = fd;
|
|
|
|
|
goto error;
|
|
|
|
|
}
|
|
|
|
|
server->source = pw_loop_add_io(impl->loop, fd, SPA_IO_IN, true, on_connect, server);
|
|
|
|
|
if (server->source == NULL) {
|
|
|
|
|
res = -errno;
|
2021-09-16 15:35:10 +10:00
|
|
|
pw_log_error("%p: can't create server source: %m", impl);
|
2021-04-12 09:33:41 +02:00
|
|
|
goto error_close;
|
|
|
|
|
}
|
|
|
|
|
return server;
|
|
|
|
|
|
|
|
|
|
error_close:
|
|
|
|
|
close(fd);
|
|
|
|
|
error:
|
|
|
|
|
server_free(server);
|
|
|
|
|
errno = -res;
|
|
|
|
|
return NULL;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void impl_free(struct impl *impl)
|
|
|
|
|
{
|
|
|
|
|
struct server *s;
|
|
|
|
|
|
|
|
|
|
spa_hook_remove(&impl->module_listener);
|
|
|
|
|
spa_list_consume(s, &impl->server_list, link)
|
|
|
|
|
server_free(s);
|
2024-05-14 17:30:05 +02:00
|
|
|
pw_properties_free(impl->capture_props);
|
|
|
|
|
pw_properties_free(impl->playback_props);
|
2021-06-01 11:21:17 +10:00
|
|
|
pw_properties_free(impl->props);
|
2024-07-06 10:09:20 +02:00
|
|
|
free(impl->ifname);
|
|
|
|
|
free(impl->ifaddress);
|
2021-04-12 09:33:41 +02:00
|
|
|
free(impl);
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-14 17:30:05 +02:00
|
|
|
static int calc_frame_size(struct spa_audio_info_raw *info)
|
|
|
|
|
{
|
|
|
|
|
int res = info->channels;
|
|
|
|
|
switch (info->format) {
|
|
|
|
|
case SPA_AUDIO_FORMAT_U8:
|
|
|
|
|
case SPA_AUDIO_FORMAT_S8:
|
|
|
|
|
case SPA_AUDIO_FORMAT_ALAW:
|
|
|
|
|
case SPA_AUDIO_FORMAT_ULAW:
|
|
|
|
|
return res;
|
|
|
|
|
case SPA_AUDIO_FORMAT_S16:
|
|
|
|
|
case SPA_AUDIO_FORMAT_S16_OE:
|
|
|
|
|
case SPA_AUDIO_FORMAT_U16:
|
|
|
|
|
return res * 2;
|
|
|
|
|
case SPA_AUDIO_FORMAT_S24:
|
|
|
|
|
case SPA_AUDIO_FORMAT_S24_OE:
|
|
|
|
|
case SPA_AUDIO_FORMAT_U24:
|
|
|
|
|
return res * 3;
|
|
|
|
|
case SPA_AUDIO_FORMAT_S24_32:
|
|
|
|
|
case SPA_AUDIO_FORMAT_S24_32_OE:
|
|
|
|
|
case SPA_AUDIO_FORMAT_S32:
|
|
|
|
|
case SPA_AUDIO_FORMAT_S32_OE:
|
|
|
|
|
case SPA_AUDIO_FORMAT_U32:
|
|
|
|
|
case SPA_AUDIO_FORMAT_U32_OE:
|
|
|
|
|
case SPA_AUDIO_FORMAT_F32:
|
|
|
|
|
case SPA_AUDIO_FORMAT_F32_OE:
|
|
|
|
|
return res * 4;
|
|
|
|
|
case SPA_AUDIO_FORMAT_F64:
|
|
|
|
|
case SPA_AUDIO_FORMAT_F64_OE:
|
|
|
|
|
return res * 8;
|
|
|
|
|
default:
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static int parse_audio_info(const struct pw_properties *props, struct spa_audio_info_raw *info)
|
|
|
|
|
{
|
|
|
|
|
const char *str;
|
|
|
|
|
|
|
|
|
|
spa_zero(*info);
|
|
|
|
|
if ((str = pw_properties_get(props, PW_KEY_AUDIO_FORMAT)) == NULL)
|
|
|
|
|
str = DEFAULT_FORMAT;
|
2024-09-18 09:54:34 +02:00
|
|
|
info->format = spa_type_audio_format_from_short_name(str);
|
2024-05-14 17:30:05 +02:00
|
|
|
|
|
|
|
|
info->rate = pw_properties_get_uint32(props, PW_KEY_AUDIO_RATE, info->rate);
|
|
|
|
|
if (info->rate == 0)
|
|
|
|
|
info->rate = DEFAULT_RATE;
|
|
|
|
|
|
|
|
|
|
info->channels = pw_properties_get_uint32(props, PW_KEY_AUDIO_CHANNELS, info->channels);
|
|
|
|
|
info->channels = SPA_MIN(info->channels, SPA_AUDIO_MAX_CHANNELS);
|
|
|
|
|
if ((str = pw_properties_get(props, SPA_KEY_AUDIO_POSITION)) != NULL)
|
2024-09-18 09:54:34 +02:00
|
|
|
spa_audio_parse_position(str, strlen(str), info->position, &info->channels);
|
2024-05-14 17:30:05 +02:00
|
|
|
if (info->channels == 0)
|
2024-09-18 09:54:34 +02:00
|
|
|
spa_audio_parse_position(DEFAULT_POSITION, strlen(DEFAULT_POSITION),
|
|
|
|
|
info->position, &info->channels);
|
2024-05-14 17:30:05 +02:00
|
|
|
|
|
|
|
|
return calc_frame_size(info);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void copy_props(struct impl *impl, const char *key)
|
|
|
|
|
{
|
|
|
|
|
const char *str;
|
|
|
|
|
if ((str = pw_properties_get(impl->props, key)) != NULL) {
|
|
|
|
|
if (pw_properties_get(impl->capture_props, key) == NULL)
|
|
|
|
|
pw_properties_set(impl->capture_props, key, str);
|
|
|
|
|
if (pw_properties_get(impl->playback_props, key) == NULL)
|
|
|
|
|
pw_properties_set(impl->playback_props, key, str);
|
2021-04-12 09:33:41 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static int parse_params(struct impl *impl)
|
|
|
|
|
{
|
|
|
|
|
const char *str;
|
2024-09-13 13:09:54 +02:00
|
|
|
struct spa_json it[1];
|
2021-04-12 09:33:41 +02:00
|
|
|
char value[512];
|
|
|
|
|
|
2021-10-14 06:32:20 +10:00
|
|
|
pw_properties_fetch_bool(impl->props, "capture", &impl->capture);
|
|
|
|
|
pw_properties_fetch_bool(impl->props, "playback", &impl->playback);
|
2021-04-12 09:33:41 +02:00
|
|
|
if (!impl->playback && !impl->capture) {
|
|
|
|
|
pw_log_error("missing capture or playback param");
|
|
|
|
|
return -EINVAL;
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-14 17:30:05 +02:00
|
|
|
if (pw_properties_get(impl->props, PW_KEY_NODE_VIRTUAL) == NULL)
|
|
|
|
|
pw_properties_set(impl->props, PW_KEY_NODE_VIRTUAL, "true");
|
|
|
|
|
if (pw_properties_get(impl->props, PW_KEY_NODE_NETWORK) == NULL)
|
|
|
|
|
pw_properties_set(impl->props, PW_KEY_NODE_NETWORK, "true");
|
|
|
|
|
|
|
|
|
|
impl->capture_props = pw_properties_new(
|
|
|
|
|
PW_KEY_TARGET_OBJECT, pw_properties_get(impl->props, "capture.node"),
|
|
|
|
|
PW_KEY_STREAM_CAPTURE_SINK, pw_properties_get(impl->props,
|
|
|
|
|
PW_KEY_STREAM_CAPTURE_SINK),
|
|
|
|
|
NULL);
|
|
|
|
|
impl->playback_props = pw_properties_new(
|
|
|
|
|
PW_KEY_TARGET_OBJECT, pw_properties_get(impl->props, "playback.node"),
|
|
|
|
|
NULL);
|
|
|
|
|
if (impl->capture_props == NULL || impl->playback_props == NULL) {
|
|
|
|
|
pw_log_error("can't create props: %m");
|
|
|
|
|
return -errno;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ((str = pw_properties_get(impl->props, "capture.props")) != NULL)
|
|
|
|
|
pw_properties_update_string(impl->capture_props, str, strlen(str));
|
|
|
|
|
if ((str = pw_properties_get(impl->props, "playback.props")) != NULL)
|
|
|
|
|
pw_properties_update_string(impl->playback_props, str, strlen(str));
|
|
|
|
|
|
|
|
|
|
copy_props(impl, PW_KEY_AUDIO_FORMAT);
|
|
|
|
|
copy_props(impl, PW_KEY_AUDIO_RATE);
|
|
|
|
|
copy_props(impl, PW_KEY_AUDIO_CHANNELS);
|
|
|
|
|
copy_props(impl, SPA_KEY_AUDIO_POSITION);
|
|
|
|
|
copy_props(impl, PW_KEY_NODE_RATE);
|
|
|
|
|
copy_props(impl, PW_KEY_NODE_NAME);
|
|
|
|
|
copy_props(impl, PW_KEY_NODE_DESCRIPTION);
|
|
|
|
|
copy_props(impl, PW_KEY_NODE_GROUP);
|
|
|
|
|
copy_props(impl, PW_KEY_NODE_LATENCY);
|
|
|
|
|
copy_props(impl, PW_KEY_NODE_VIRTUAL);
|
|
|
|
|
copy_props(impl, PW_KEY_NODE_NETWORK);
|
|
|
|
|
|
|
|
|
|
impl->capture_frame_size = parse_audio_info(impl->capture_props, &impl->capture_info);
|
|
|
|
|
if (impl->capture_frame_size == 0) {
|
|
|
|
|
pw_log_error("unsupported capture audio format:%d channels:%d",
|
|
|
|
|
impl->capture_info.format, impl->capture_info.channels);
|
2021-04-12 09:33:41 +02:00
|
|
|
return -EINVAL;
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-14 17:30:05 +02:00
|
|
|
impl->playback_frame_size = parse_audio_info(impl->playback_props, &impl->playback_info);
|
|
|
|
|
if (impl->playback_frame_size == 0) {
|
|
|
|
|
pw_log_error("unsupported playback audio format:%d channels:%d",
|
|
|
|
|
impl->playback_info.format, impl->playback_info.channels);
|
|
|
|
|
return -EINVAL;
|
2021-04-12 09:33:41 +02:00
|
|
|
}
|
2024-05-14 17:30:05 +02:00
|
|
|
if (impl->capture_info.rate != 0 &&
|
|
|
|
|
pw_properties_get(impl->capture_props, PW_KEY_NODE_RATE) == NULL)
|
|
|
|
|
pw_properties_setf(impl->capture_props, PW_KEY_NODE_RATE,
|
|
|
|
|
"1/%u", impl->capture_info.rate);
|
|
|
|
|
if (impl->playback_info.rate != 0 &&
|
|
|
|
|
pw_properties_get(impl->playback_props, PW_KEY_NODE_RATE) == NULL)
|
|
|
|
|
pw_properties_setf(impl->playback_props, PW_KEY_NODE_RATE,
|
|
|
|
|
"1/%u", impl->playback_info.rate);
|
2021-04-12 09:33:41 +02:00
|
|
|
|
2024-07-06 10:09:20 +02:00
|
|
|
str = pw_properties_get(impl->props, "local.ifname");
|
|
|
|
|
impl->ifname = str ? strdup(str) : NULL;
|
|
|
|
|
str = pw_properties_get(impl->props, "local.ifaddress");
|
|
|
|
|
impl->ifaddress = str ? strdup(str) : NULL;
|
|
|
|
|
|
2021-04-12 09:33:41 +02:00
|
|
|
if ((str = pw_properties_get(impl->props, "server.address")) == NULL)
|
|
|
|
|
str = DEFAULT_SERVER;
|
|
|
|
|
|
2024-09-13 13:09:54 +02:00
|
|
|
if (spa_json_begin_array_relax(&it[0], str, strlen(str)) > 0) {
|
|
|
|
|
while (spa_json_get_string(&it[0], value, sizeof(value)) > 0) {
|
2021-04-12 09:33:41 +02:00
|
|
|
if (create_server(impl, value) == NULL) {
|
2021-09-16 15:35:10 +10:00
|
|
|
pw_log_warn("%p: can't create server for %s: %m",
|
2021-04-12 09:33:41 +02:00
|
|
|
impl, value);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void module_destroy(void *data)
|
|
|
|
|
{
|
|
|
|
|
struct impl *impl = data;
|
|
|
|
|
pw_log_debug("module %p: destroy", impl);
|
|
|
|
|
impl_free(impl);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static const struct pw_impl_module_events module_events = {
|
|
|
|
|
PW_VERSION_IMPL_MODULE_EVENTS,
|
|
|
|
|
.destroy = module_destroy,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
SPA_EXPORT
|
|
|
|
|
int pipewire__module_init(struct pw_impl_module *module, const char *args)
|
|
|
|
|
{
|
|
|
|
|
struct pw_context *context = pw_impl_module_get_context(module);
|
|
|
|
|
struct pw_properties *props;
|
|
|
|
|
struct impl *impl;
|
2024-05-22 09:23:31 +02:00
|
|
|
struct server *s;
|
|
|
|
|
FILE *f;
|
|
|
|
|
char *str;
|
|
|
|
|
size_t size;
|
2021-04-12 09:33:41 +02:00
|
|
|
int res;
|
2024-05-22 09:23:31 +02:00
|
|
|
struct spa_dict_item it[1];
|
2021-04-12 09:33:41 +02:00
|
|
|
|
2021-09-16 15:35:10 +10:00
|
|
|
PW_LOG_TOPIC_INIT(mod_topic);
|
|
|
|
|
|
2021-04-12 09:33:41 +02:00
|
|
|
impl = calloc(1, sizeof(struct impl));
|
|
|
|
|
if (impl == NULL)
|
|
|
|
|
return -errno;
|
|
|
|
|
|
|
|
|
|
pw_log_debug("module %p: new %s", impl, args);
|
|
|
|
|
|
|
|
|
|
if (args)
|
|
|
|
|
props = pw_properties_new_string(args);
|
|
|
|
|
else
|
|
|
|
|
props = pw_properties_new(NULL, NULL);
|
|
|
|
|
|
|
|
|
|
impl->context = context;
|
|
|
|
|
impl->loop = pw_context_get_main_loop(context);
|
|
|
|
|
impl->props = props;
|
|
|
|
|
spa_list_init(&impl->server_list);
|
|
|
|
|
|
|
|
|
|
pw_impl_module_add_listener(module, &impl->module_listener, &module_events, impl);
|
|
|
|
|
|
|
|
|
|
pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_props));
|
|
|
|
|
|
2021-04-21 13:03:08 +02:00
|
|
|
impl->work_queue = pw_context_get_work_queue(context);
|
2021-04-12 09:33:41 +02:00
|
|
|
|
|
|
|
|
if ((res = parse_params(impl)) < 0)
|
|
|
|
|
goto error_free;
|
|
|
|
|
|
2024-05-22 09:23:31 +02:00
|
|
|
if ((f = open_memstream(&str, &size)) == NULL) {
|
|
|
|
|
res = -errno;
|
|
|
|
|
pw_log_error("Can't open memstream: %m");
|
|
|
|
|
goto error_free;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fprintf(f, "[");
|
|
|
|
|
|
|
|
|
|
spa_list_for_each(s, &impl->server_list, link) {
|
|
|
|
|
char ip[128];
|
|
|
|
|
uint16_t port = 0;
|
|
|
|
|
bool ipv4;
|
|
|
|
|
|
2024-07-06 10:09:20 +02:00
|
|
|
if (pw_net_get_ip(&s->addr, ip, sizeof(ip), &ipv4, &port) < 0)
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
fprintf(f, " \"%s%s%s:%d\"", ipv4 ? "" : "[", ip, ipv4 ? "" : "]", port);
|
2024-05-22 09:23:31 +02:00
|
|
|
}
|
|
|
|
|
fprintf(f, " ]");
|
|
|
|
|
fclose(f);
|
|
|
|
|
|
|
|
|
|
pw_log_info("listening on %s", str);
|
|
|
|
|
it[0] = SPA_DICT_ITEM_INIT("server.address", str);
|
|
|
|
|
pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(it));
|
|
|
|
|
|
|
|
|
|
free(str);
|
|
|
|
|
|
2021-04-12 09:33:41 +02:00
|
|
|
return 0;
|
|
|
|
|
|
|
|
|
|
error_free:
|
|
|
|
|
impl_free(impl);
|
|
|
|
|
return res;
|
|
|
|
|
}
|