2023-02-28 16:14:19 +01:00
|
|
|
/* PipeWire */
|
|
|
|
|
/* SPDX-FileCopyrightText: Copyright © 2022 Wim Taymans <wim.taymans@gmail.com> */
|
|
|
|
|
/* SPDX-License-Identifier: MIT */
|
|
|
|
|
|
|
|
|
|
#include "config.h"
|
|
|
|
|
|
|
|
|
|
#include <limits.h>
|
|
|
|
|
#include <unistd.h>
|
|
|
|
|
#include <sys/stat.h>
|
|
|
|
|
#include <sys/socket.h>
|
|
|
|
|
#include <sys/ioctl.h>
|
|
|
|
|
#include <arpa/inet.h>
|
|
|
|
|
#include <netinet/ip.h>
|
|
|
|
|
#include <netinet/in.h>
|
|
|
|
|
#include <net/if.h>
|
|
|
|
|
#include <ctype.h>
|
|
|
|
|
|
|
|
|
|
#include <spa/utils/hook.h>
|
|
|
|
|
#include <spa/utils/result.h>
|
|
|
|
|
#include <spa/utils/ringbuffer.h>
|
|
|
|
|
#include <spa/utils/json.h>
|
|
|
|
|
#include <spa/param/audio/format-utils.h>
|
|
|
|
|
#include <spa/debug/types.h>
|
|
|
|
|
#include <spa/debug/mem.h>
|
2024-01-11 17:49:50 +01:00
|
|
|
#include <spa/debug/log.h>
|
2023-02-28 16:14:19 +01:00
|
|
|
|
|
|
|
|
#include <pipewire/pipewire.h>
|
|
|
|
|
#include <pipewire/impl.h>
|
|
|
|
|
|
2023-03-01 09:21:53 +01:00
|
|
|
#include <avahi-client/publish.h>
|
|
|
|
|
#include <avahi-client/lookup.h>
|
|
|
|
|
#include <avahi-common/error.h>
|
|
|
|
|
#include <avahi-common/malloc.h>
|
|
|
|
|
|
|
|
|
|
#include "module-zeroconf-discover/avahi-poll.h"
|
|
|
|
|
|
2023-02-28 16:14:19 +01:00
|
|
|
#include <module-rtp/rtp.h>
|
|
|
|
|
#include <module-rtp/apple-midi.h>
|
|
|
|
|
#include <module-rtp/stream.h>
|
2024-02-25 09:19:11 +01:00
|
|
|
#include "network-utils.h"
|
2023-02-28 16:14:19 +01:00
|
|
|
|
2023-11-18 15:11:03 +02:00
|
|
|
/** \page page_module_rtp_session RTP session
|
2023-02-28 16:14:19 +01:00
|
|
|
*
|
2023-03-03 12:04:50 +01:00
|
|
|
* The `rtp-session` module creates a media session that is announced
|
|
|
|
|
* with avahi/mDNS/Bonjour.
|
|
|
|
|
*
|
|
|
|
|
* Other machines on the network that run a compatible session will see
|
2024-05-20 10:45:04 -03:00
|
|
|
* each other and will be able to send audio/midi between each other.
|
2023-03-03 12:04:50 +01:00
|
|
|
*
|
|
|
|
|
* The session setup is based on apple-midi and is compatible with
|
|
|
|
|
* apple-midi when the session is using midi.
|
2023-02-28 16:14:19 +01:00
|
|
|
*
|
2023-11-20 00:23:03 +02:00
|
|
|
* ## Module Name
|
|
|
|
|
*
|
|
|
|
|
* `libpipewire-module-rtp-session`
|
|
|
|
|
*
|
2023-02-28 16:14:19 +01:00
|
|
|
* ## Module Options
|
|
|
|
|
*
|
|
|
|
|
* Options specific to the behavior of this module
|
|
|
|
|
*
|
|
|
|
|
* - `local.ifname = <str>`: interface name to use
|
2023-03-01 09:21:53 +01:00
|
|
|
* - `control.ip =<str>`: control IP address, default "0.0.0.0"
|
|
|
|
|
* - `control.port =<int>`: control port, default "0"
|
2023-02-28 16:14:19 +01:00
|
|
|
* - `net.mtu = <int>`: MTU to use, default 1280
|
|
|
|
|
* - `net.ttl = <int>`: TTL to use, default 1
|
|
|
|
|
* - `net.loop = <bool>`: loopback multicast, default false
|
2024-05-15 15:39:27 +02:00
|
|
|
* `sess.discover-local`: discover local services as well, default false
|
2023-02-28 16:14:19 +01:00
|
|
|
* - `sess.min-ptime = <int>`: minimum packet time in milliseconds, default 2
|
|
|
|
|
* - `sess.max-ptime = <int>`: maximum packet time in milliseconds, default 20
|
2023-03-02 17:18:43 +01:00
|
|
|
* - `sess.latency.msec = <int>`: receiver latency in milliseconds, default 100
|
2023-02-28 16:14:19 +01:00
|
|
|
* - `sess.name = <str>`: a session name
|
|
|
|
|
* - `sess.ts-offset = <int>`: an offset to apply to the timestamp, default -1 = random offset
|
|
|
|
|
* - `sess.ts-refclk = <string>`: the name of a reference clock
|
2023-03-13 12:48:13 +01:00
|
|
|
* - `sess.media = <string>`: the media type audio|midi|opus, default midi
|
2023-02-28 16:14:19 +01:00
|
|
|
* - `stream.props = {}`: properties to be passed to the stream
|
|
|
|
|
*
|
|
|
|
|
* ## General options
|
|
|
|
|
*
|
|
|
|
|
* Options with well-known behavior:
|
|
|
|
|
*
|
|
|
|
|
* - \ref PW_KEY_REMOTE_NAME
|
|
|
|
|
* - \ref PW_KEY_AUDIO_FORMAT
|
|
|
|
|
* - \ref PW_KEY_AUDIO_RATE
|
|
|
|
|
* - \ref PW_KEY_AUDIO_CHANNELS
|
|
|
|
|
* - \ref SPA_KEY_AUDIO_POSITION
|
|
|
|
|
* - \ref PW_KEY_NODE_NAME
|
|
|
|
|
* - \ref PW_KEY_NODE_DESCRIPTION
|
|
|
|
|
* - \ref PW_KEY_MEDIA_NAME
|
|
|
|
|
* - \ref PW_KEY_NODE_GROUP
|
|
|
|
|
* - \ref PW_KEY_NODE_LATENCY
|
|
|
|
|
* - \ref PW_KEY_NODE_VIRTUAL
|
|
|
|
|
* - \ref PW_KEY_MEDIA_CLASS
|
|
|
|
|
*
|
|
|
|
|
* ## Example configuration
|
|
|
|
|
*\code{.unparsed}
|
2024-09-14 15:43:25 +03:00
|
|
|
* # ~/.config/pipewire/pipewire.conf.d/my-rtp-session.conf
|
|
|
|
|
*
|
2023-02-28 16:14:19 +01:00
|
|
|
* context.modules = [
|
2023-03-03 12:04:50 +01:00
|
|
|
* { name = libpipewire-module-rtp-session
|
2023-02-28 16:14:19 +01:00
|
|
|
* args = {
|
|
|
|
|
* #local.ifname = "eth0"
|
2023-03-01 09:21:53 +01:00
|
|
|
* #control.ip = "0.0.0.0"
|
|
|
|
|
* #control.port = 0
|
2023-02-28 16:14:19 +01:00
|
|
|
* #net.mtu = 1280
|
|
|
|
|
* #net.ttl = 1
|
|
|
|
|
* #net.loop = false
|
2024-05-15 15:39:27 +02:00
|
|
|
* #sess.discover-local = false
|
2023-02-28 16:14:19 +01:00
|
|
|
* #sess.min-ptime = 2
|
|
|
|
|
* #sess.max-ptime = 20
|
|
|
|
|
* #sess.name = "PipeWire RTP stream"
|
2023-03-03 12:04:50 +01:00
|
|
|
* #sess.media = "audio"
|
2023-02-28 16:14:19 +01:00
|
|
|
* stream.props = {
|
|
|
|
|
* node.name = "rtp-sink"
|
2023-03-01 09:21:53 +01:00
|
|
|
* #audio.format = "S16BE"
|
|
|
|
|
* #audio.rate = 48000
|
|
|
|
|
* #audio.channels = 2
|
|
|
|
|
* #audio.position = [ FL FR ]
|
2023-02-28 16:14:19 +01:00
|
|
|
* }
|
|
|
|
|
* }
|
|
|
|
|
*}
|
|
|
|
|
*]
|
|
|
|
|
*\endcode
|
|
|
|
|
*
|
|
|
|
|
* \since 0.3.60
|
|
|
|
|
*/
|
|
|
|
|
|
2023-03-02 13:11:22 +01:00
|
|
|
#define NAME "rtp-session"
|
2023-02-28 16:14:19 +01:00
|
|
|
|
2024-04-16 10:21:40 +02:00
|
|
|
PW_LOG_TOPIC(mod_topic, "mod." NAME);
|
2023-02-28 16:14:19 +01:00
|
|
|
#define PW_LOG_TOPIC_DEFAULT mod_topic
|
|
|
|
|
|
2023-03-01 09:21:53 +01:00
|
|
|
#define DEFAULT_CONTROL_IP "0.0.0.0"
|
|
|
|
|
#define DEFAULT_CONTROL_PORT 0
|
2023-02-28 16:14:19 +01:00
|
|
|
#define DEFAULT_TTL 1
|
|
|
|
|
#define DEFAULT_LOOP false
|
|
|
|
|
|
2023-03-22 16:35:55 +01:00
|
|
|
#define USAGE "( control.ip=<destination IP address, default:"DEFAULT_CONTROL_IP"> ) " \
|
|
|
|
|
"( control.port=<int, default:"SPA_STRINGIFY(DEFAULT_CONTROL_PORT)"> ) " \
|
|
|
|
|
"( local.ifname=<local interface name to use> ) " \
|
|
|
|
|
"( net.mtu=<desired MTU, default:"SPA_STRINGIFY(DEFAULT_MTU)"> ) " \
|
|
|
|
|
"( net.ttl=<desired TTL, default:"SPA_STRINGIFY(DEFAULT_TTL)"> ) " \
|
|
|
|
|
"( net.loop=<desired loopback, default:"SPA_STRINGIFY(DEFAULT_LOOP)"> ) " \
|
|
|
|
|
"( sess.name=<a name for the session> ) " \
|
|
|
|
|
"( sess.min-ptime=<minimum packet time in milliseconds, default:2> ) " \
|
|
|
|
|
"( sess.max-ptime=<maximum packet time in milliseconds, default:20> ) " \
|
2023-04-06 11:49:25 +02:00
|
|
|
"( sess.media=<string, the media type audio|midi|opus, default midi> ) " \
|
2023-03-22 16:35:55 +01:00
|
|
|
"( audio.format=<format, default:"DEFAULT_FORMAT"> ) " \
|
|
|
|
|
"( audio.rate=<sample rate, default:"SPA_STRINGIFY(DEFAULT_RATE)"> ) " \
|
|
|
|
|
"( audio.channels=<number of channels, default:"SPA_STRINGIFY(DEFAULT_CHANNELS)"> ) "\
|
|
|
|
|
"( audio.position=<channel map, default:"DEFAULT_POSITION"> ) " \
|
|
|
|
|
"( stream.props= { key=value ... } ) "
|
2023-02-28 16:14:19 +01:00
|
|
|
|
|
|
|
|
static const struct spa_dict_item module_info[] = {
|
|
|
|
|
{ PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
|
|
|
|
|
{ PW_KEY_MODULE_DESCRIPTION, "RTP Sink" },
|
|
|
|
|
{ PW_KEY_MODULE_USAGE, USAGE },
|
|
|
|
|
{ PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
|
|
|
|
|
};
|
|
|
|
|
|
2023-03-01 09:21:53 +01:00
|
|
|
struct service_info {
|
|
|
|
|
AvahiIfIndex interface;
|
|
|
|
|
AvahiProtocol protocol;
|
|
|
|
|
const char *name;
|
|
|
|
|
const char *type;
|
|
|
|
|
const char *domain;
|
|
|
|
|
const char *host_name;
|
|
|
|
|
AvahiAddress address;
|
|
|
|
|
uint16_t port;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
#define SERVICE_INFO(...) ((struct service_info){ __VA_ARGS__ })
|
|
|
|
|
|
|
|
|
|
struct service {
|
|
|
|
|
struct service_info info;
|
|
|
|
|
|
|
|
|
|
struct spa_list link;
|
|
|
|
|
struct impl *impl;
|
|
|
|
|
|
|
|
|
|
struct session *sess;
|
|
|
|
|
};
|
|
|
|
|
|
2023-02-28 16:14:19 +01:00
|
|
|
struct session {
|
|
|
|
|
struct impl *impl;
|
|
|
|
|
struct spa_list link;
|
|
|
|
|
|
|
|
|
|
struct sockaddr_storage ctrl_addr;
|
|
|
|
|
socklen_t ctrl_len;
|
|
|
|
|
struct sockaddr_storage data_addr;
|
|
|
|
|
socklen_t data_len;
|
|
|
|
|
|
|
|
|
|
struct rtp_stream *send;
|
|
|
|
|
struct spa_hook send_listener;
|
|
|
|
|
struct rtp_stream *recv;
|
|
|
|
|
struct spa_hook recv_listener;
|
|
|
|
|
|
|
|
|
|
char *name;
|
|
|
|
|
|
2023-03-01 11:40:11 +01:00
|
|
|
unsigned we_initiated:1;
|
|
|
|
|
|
|
|
|
|
#define SESSION_STATE_INIT 0
|
|
|
|
|
#define SESSION_STATE_SENDING_CTRL_IN 1
|
|
|
|
|
#define SESSION_STATE_SENDING_DATA_IN 2
|
|
|
|
|
#define SESSION_STATE_ESTABLISHING 3
|
|
|
|
|
#define SESSION_STATE_ESTABLISHED 4
|
|
|
|
|
int state;
|
2023-03-07 09:00:40 +01:00
|
|
|
int ck_count;
|
|
|
|
|
uint64_t next_time;
|
2023-03-01 11:40:11 +01:00
|
|
|
|
2023-03-07 12:04:50 +01:00
|
|
|
uint32_t ctrl_initiator;
|
|
|
|
|
uint32_t data_initiator;
|
2023-02-28 16:14:19 +01:00
|
|
|
uint32_t remote_ssrc;
|
|
|
|
|
|
|
|
|
|
uint32_t ssrc;
|
|
|
|
|
|
2023-03-01 11:40:11 +01:00
|
|
|
unsigned sending:1;
|
|
|
|
|
unsigned receiving:1;
|
|
|
|
|
|
2023-02-28 16:14:19 +01:00
|
|
|
unsigned ctrl_ready:1;
|
|
|
|
|
unsigned data_ready:1;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
struct impl {
|
|
|
|
|
struct pw_context *context;
|
|
|
|
|
|
|
|
|
|
struct pw_impl_module *module;
|
|
|
|
|
struct spa_hook module_listener;
|
|
|
|
|
struct pw_properties *props;
|
|
|
|
|
|
2024-05-15 15:39:27 +02:00
|
|
|
bool discover_local;
|
2023-03-01 09:21:53 +01:00
|
|
|
AvahiPoll *avahi_poll;
|
|
|
|
|
AvahiClient *client;
|
|
|
|
|
AvahiServiceBrowser *browser;
|
|
|
|
|
AvahiEntryGroup *group;
|
|
|
|
|
struct spa_list service_list;
|
|
|
|
|
|
2023-02-28 16:14:19 +01:00
|
|
|
struct pw_properties *stream_props;
|
|
|
|
|
|
|
|
|
|
struct pw_loop *loop;
|
|
|
|
|
struct pw_loop *data_loop;
|
|
|
|
|
|
|
|
|
|
struct pw_core *core;
|
|
|
|
|
struct spa_hook core_listener;
|
|
|
|
|
struct spa_hook core_proxy_listener;
|
|
|
|
|
unsigned int do_disconnect:1;
|
|
|
|
|
|
|
|
|
|
struct spa_source *timer;
|
2023-03-07 09:00:40 +01:00
|
|
|
uint64_t next_time;
|
2023-02-28 16:14:19 +01:00
|
|
|
|
|
|
|
|
struct spa_source *ctrl_source;
|
|
|
|
|
struct spa_source *data_source;
|
|
|
|
|
|
|
|
|
|
char *ifname;
|
|
|
|
|
char *session_name;
|
2023-03-02 17:18:43 +01:00
|
|
|
uint32_t ttl;
|
2023-02-28 16:14:19 +01:00
|
|
|
bool mcast_loop;
|
2023-03-02 13:53:56 +01:00
|
|
|
int32_t ts_offset;
|
2023-02-28 16:14:19 +01:00
|
|
|
char *ts_refclk;
|
|
|
|
|
int payload;
|
|
|
|
|
|
2023-03-01 09:21:53 +01:00
|
|
|
uint16_t ctrl_port;
|
2023-02-28 16:14:19 +01:00
|
|
|
struct sockaddr_storage ctrl_addr;
|
|
|
|
|
socklen_t ctrl_len;
|
2023-03-01 09:21:53 +01:00
|
|
|
struct sockaddr_storage data_addr;
|
|
|
|
|
socklen_t data_len;
|
2023-02-28 16:14:19 +01:00
|
|
|
|
|
|
|
|
struct spa_list sessions;
|
|
|
|
|
uint32_t n_sessions;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
static ssize_t send_packet(int fd, struct msghdr *msg)
|
|
|
|
|
{
|
|
|
|
|
ssize_t n;
|
|
|
|
|
n = sendmsg(fd, msg, MSG_NOSIGNAL);
|
2023-04-19 21:33:43 +02:00
|
|
|
if (n < 0)
|
2023-12-16 19:43:17 +03:00
|
|
|
pw_log_warn("sendmsg() failed: %m");
|
2023-02-28 16:14:19 +01:00
|
|
|
return n;
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-07 09:00:40 +01:00
|
|
|
static uint64_t current_time_ns(void)
|
|
|
|
|
{
|
|
|
|
|
struct timespec ts;
|
|
|
|
|
clock_gettime(CLOCK_MONOTONIC, &ts);
|
|
|
|
|
return SPA_TIMESPEC_TO_NSEC(&ts);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void set_timeout(struct impl *impl, uint64_t time)
|
|
|
|
|
{
|
|
|
|
|
struct itimerspec ts;
|
|
|
|
|
ts.it_value.tv_sec = time / SPA_NSEC_PER_SEC;
|
|
|
|
|
ts.it_value.tv_nsec = time % SPA_NSEC_PER_SEC;
|
|
|
|
|
ts.it_interval.tv_sec = 0;
|
|
|
|
|
ts.it_interval.tv_nsec = 0;
|
|
|
|
|
pw_loop_update_timer(impl->loop, impl->timer, &ts.it_value, &ts.it_interval, true);
|
|
|
|
|
impl->next_time = time;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void schedule_timeout(struct impl *impl)
|
|
|
|
|
{
|
|
|
|
|
struct session *sess;
|
|
|
|
|
uint64_t next_time = 0;
|
|
|
|
|
spa_list_for_each(sess, &impl->sessions, link) {
|
|
|
|
|
if (next_time == 0 ||
|
|
|
|
|
(sess->next_time != 0 && sess->next_time < next_time))
|
|
|
|
|
next_time = sess->next_time;
|
|
|
|
|
}
|
|
|
|
|
set_timeout(impl, next_time);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void send_apple_midi_cmd_ck0(struct session *sess)
|
|
|
|
|
{
|
|
|
|
|
struct impl *impl = sess->impl;
|
|
|
|
|
struct iovec iov[3];
|
|
|
|
|
struct msghdr msg;
|
|
|
|
|
struct rtp_apple_midi_ck hdr;
|
|
|
|
|
uint64_t current_time, ts;
|
|
|
|
|
|
|
|
|
|
spa_zero(hdr);
|
|
|
|
|
hdr.cmd = htonl(APPLE_MIDI_CMD_CK);
|
|
|
|
|
hdr.ssrc = htonl(sess->ssrc);
|
|
|
|
|
|
|
|
|
|
current_time = current_time_ns();
|
|
|
|
|
ts = current_time / 10000;
|
|
|
|
|
hdr.ts1_h = htonl(ts >> 32);
|
|
|
|
|
hdr.ts1_l = htonl(ts);
|
|
|
|
|
|
|
|
|
|
iov[0].iov_base = &hdr;
|
|
|
|
|
iov[0].iov_len = sizeof(hdr);
|
|
|
|
|
|
|
|
|
|
spa_zero(msg);
|
|
|
|
|
msg.msg_name = &sess->data_addr;
|
|
|
|
|
msg.msg_namelen = sess->data_len;
|
|
|
|
|
msg.msg_iov = iov;
|
|
|
|
|
msg.msg_iovlen = 1;
|
|
|
|
|
|
|
|
|
|
send_packet(impl->data_source->fd, &msg);
|
|
|
|
|
|
|
|
|
|
if (sess->ck_count++ < 8)
|
|
|
|
|
sess->next_time = current_time + SPA_NSEC_PER_SEC;
|
|
|
|
|
else if (sess->ck_count++ < 16)
|
|
|
|
|
sess->next_time = current_time + 2 * SPA_NSEC_PER_SEC;
|
|
|
|
|
else
|
|
|
|
|
sess->next_time = current_time + 5 * SPA_NSEC_PER_SEC;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void session_update_state(struct session *sess, int state)
|
|
|
|
|
{
|
|
|
|
|
if (sess->state == state)
|
|
|
|
|
return;
|
|
|
|
|
|
2023-03-07 12:04:50 +01:00
|
|
|
pw_log_info("session ssrc:%08x state:%d", sess->ssrc, state);
|
2023-03-07 09:00:40 +01:00
|
|
|
|
|
|
|
|
sess->state = state;
|
|
|
|
|
switch (state) {
|
|
|
|
|
case SESSION_STATE_ESTABLISHED:
|
|
|
|
|
if (sess->we_initiated) {
|
|
|
|
|
sess->ck_count = 0;
|
|
|
|
|
send_apple_midi_cmd_ck0(sess);
|
|
|
|
|
schedule_timeout(sess->impl);
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
case SESSION_STATE_INIT:
|
|
|
|
|
sess->next_time = 0;
|
|
|
|
|
schedule_timeout(sess->impl);
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-28 16:14:19 +01:00
|
|
|
static void send_apple_midi_cmd_in(struct session *sess, bool ctrl)
|
|
|
|
|
{
|
|
|
|
|
struct impl *impl = sess->impl;
|
|
|
|
|
struct iovec iov[3];
|
|
|
|
|
struct msghdr msg;
|
|
|
|
|
struct rtp_apple_midi hdr;
|
2023-03-01 11:40:11 +01:00
|
|
|
int fd;
|
2023-02-28 16:14:19 +01:00
|
|
|
|
|
|
|
|
spa_zero(hdr);
|
|
|
|
|
hdr.cmd = htonl(APPLE_MIDI_CMD_IN);
|
|
|
|
|
hdr.protocol = htonl(2);
|
2023-03-07 12:04:50 +01:00
|
|
|
hdr.initiator = htonl(ctrl ? sess->ctrl_initiator : sess->data_initiator);
|
2023-02-28 16:14:19 +01:00
|
|
|
hdr.ssrc = htonl(sess->ssrc);
|
|
|
|
|
|
|
|
|
|
iov[0].iov_base = &hdr;
|
|
|
|
|
iov[0].iov_len = sizeof(hdr);
|
2023-03-01 10:54:00 +01:00
|
|
|
iov[1].iov_base = impl->session_name;
|
|
|
|
|
iov[1].iov_len = strlen(impl->session_name)+1;
|
2023-02-28 16:14:19 +01:00
|
|
|
|
|
|
|
|
spa_zero(msg);
|
2023-03-01 11:40:11 +01:00
|
|
|
if (ctrl) {
|
|
|
|
|
msg.msg_name = &sess->ctrl_addr;
|
|
|
|
|
msg.msg_namelen = sess->ctrl_len;
|
|
|
|
|
fd = impl->ctrl_source->fd;
|
2023-03-07 09:00:40 +01:00
|
|
|
session_update_state(sess, SESSION_STATE_SENDING_CTRL_IN);
|
2023-03-01 11:40:11 +01:00
|
|
|
} else {
|
|
|
|
|
msg.msg_name = &sess->data_addr;
|
|
|
|
|
msg.msg_namelen = sess->data_len;
|
|
|
|
|
fd = impl->data_source->fd;
|
2023-03-07 09:00:40 +01:00
|
|
|
session_update_state(sess, SESSION_STATE_SENDING_DATA_IN);
|
2023-03-01 11:40:11 +01:00
|
|
|
}
|
2023-02-28 16:14:19 +01:00
|
|
|
msg.msg_iov = iov;
|
|
|
|
|
msg.msg_iovlen = 2;
|
|
|
|
|
|
2023-03-01 11:40:11 +01:00
|
|
|
send_packet(fd, &msg);
|
2023-02-28 16:14:19 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void send_apple_midi_cmd_by(struct session *sess, bool ctrl)
|
|
|
|
|
{
|
|
|
|
|
struct impl *impl = sess->impl;
|
|
|
|
|
struct iovec iov[3];
|
|
|
|
|
struct msghdr msg;
|
|
|
|
|
struct rtp_apple_midi hdr;
|
|
|
|
|
|
|
|
|
|
spa_zero(hdr);
|
|
|
|
|
hdr.cmd = htonl(APPLE_MIDI_CMD_BY);
|
|
|
|
|
hdr.protocol = htonl(2);
|
2023-03-07 12:04:50 +01:00
|
|
|
hdr.initiator = htonl(ctrl ? sess->ctrl_initiator : sess->data_initiator);
|
2023-02-28 16:14:19 +01:00
|
|
|
hdr.ssrc = htonl(sess->ssrc);
|
|
|
|
|
|
|
|
|
|
iov[0].iov_base = &hdr;
|
|
|
|
|
iov[0].iov_len = sizeof(hdr);
|
|
|
|
|
|
|
|
|
|
spa_zero(msg);
|
|
|
|
|
msg.msg_name = ctrl ? &sess->ctrl_addr : &sess->data_addr;
|
|
|
|
|
msg.msg_namelen = ctrl ? sess->ctrl_len : sess->data_len;
|
|
|
|
|
msg.msg_iov = iov;
|
|
|
|
|
msg.msg_iovlen = 1;
|
|
|
|
|
|
|
|
|
|
send_packet(ctrl ? impl->ctrl_source->fd : impl->data_source->fd, &msg);
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-01 11:40:11 +01:00
|
|
|
static void session_establish(struct session *sess)
|
|
|
|
|
{
|
|
|
|
|
switch (sess->state) {
|
|
|
|
|
case SESSION_STATE_INIT:
|
|
|
|
|
/* we initiate */
|
|
|
|
|
sess->we_initiated = true;
|
2023-03-07 12:04:50 +01:00
|
|
|
sess->ctrl_initiator = pw_rand32();
|
|
|
|
|
sess->data_initiator = pw_rand32();
|
|
|
|
|
|
|
|
|
|
pw_log_info("start session SSRC:%08x %u %u", sess->ssrc,
|
2023-03-03 15:58:12 +01:00
|
|
|
sess->ctrl_ready, sess->data_ready);
|
2023-03-07 12:04:50 +01:00
|
|
|
|
2023-03-01 11:40:11 +01:00
|
|
|
if (!sess->ctrl_ready)
|
|
|
|
|
send_apple_midi_cmd_in(sess, true);
|
|
|
|
|
else if (!sess->data_ready)
|
|
|
|
|
send_apple_midi_cmd_in(sess, false);
|
|
|
|
|
break;
|
|
|
|
|
case SESSION_STATE_ESTABLISHING:
|
|
|
|
|
case SESSION_STATE_ESTABLISHED:
|
|
|
|
|
/* we're done or waiting for other initiator */
|
|
|
|
|
break;
|
|
|
|
|
case SESSION_STATE_SENDING_CTRL_IN:
|
|
|
|
|
case SESSION_STATE_SENDING_DATA_IN:
|
|
|
|
|
/* we're busy initiating */
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void session_stop(struct session *sess)
|
|
|
|
|
{
|
|
|
|
|
if (!sess->we_initiated)
|
|
|
|
|
return;
|
2023-03-07 12:04:50 +01:00
|
|
|
pw_log_info("stop session SSRC:%08x %u %u", sess->ssrc,
|
2023-03-03 15:58:12 +01:00
|
|
|
sess->ctrl_ready, sess->data_ready);
|
2023-03-02 13:11:22 +01:00
|
|
|
if (sess->ctrl_ready) {
|
2023-03-01 11:40:11 +01:00
|
|
|
send_apple_midi_cmd_by(sess, true);
|
2023-03-02 13:11:22 +01:00
|
|
|
sess->ctrl_ready = false;
|
|
|
|
|
}
|
|
|
|
|
if (sess->data_ready) {
|
2023-03-01 11:40:11 +01:00
|
|
|
send_apple_midi_cmd_by(sess, false);
|
2023-03-02 13:11:22 +01:00
|
|
|
sess->data_ready = false;
|
|
|
|
|
}
|
2023-03-07 09:00:40 +01:00
|
|
|
session_update_state(sess, SESSION_STATE_INIT);
|
2023-03-01 11:40:11 +01:00
|
|
|
}
|
|
|
|
|
|
2023-02-28 16:14:19 +01:00
|
|
|
static void send_destroy(void *data)
|
|
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
|
module-rtp: Replace state_changed callbacks
The state_changed callbacks fulfill multiple roles, which is both a problem
regarding separation of concerns and regarding code clarity. De facto,
these callbacks cover error reporting, opening connections, and closing
connection, all in one, depending on a state that is arguably an internal
stream detail. The code in these callbacks tie these internal states to
assumptions that opening/closing callbacks is directly tied to specific
state changes in a common way, which is not always true. For example,
stopping the stream may not _actually_ stop it if a background send timer
is still running.
The notion of a "state_changed" callback is also problematic because the
pw_streams that are used in rtp-sink and rtp-source also have a callback
for state changes, causing confusion.
Solve this by replacing state_changed with three new callbacks:
1. report_error : Used for reporting nonrecoverable errors to the caller.
Note that currently, no one does such error reporting, but the feature
does exist, so this callback is introduced to preserve said feature.
2. open_connection : Used for opening a connection. Its optional return
value informs about success or failure.
3. close_connection : Used for opening a connection. Its optional return
value informs about success or failure.
Importantly, these callbacks do not export any internal stream state. This
improves encapsulation, and also makes it possible to invoke these
callbacks in situations that may not neatly map to a state change. One
example could be to close the connection as part of a stream_start call
to close any connection(s) left over from a previous run. (Followup commits
will in fact introduce such measures.)
2025-08-21 13:30:11 +02:00
|
|
|
static void send_open_connection(void *data, int *result)
|
2023-02-28 16:14:19 +01:00
|
|
|
{
|
|
|
|
|
struct session *sess = data;
|
module-rtp: Replace state_changed callbacks
The state_changed callbacks fulfill multiple roles, which is both a problem
regarding separation of concerns and regarding code clarity. De facto,
these callbacks cover error reporting, opening connections, and closing
connection, all in one, depending on a state that is arguably an internal
stream detail. The code in these callbacks tie these internal states to
assumptions that opening/closing callbacks is directly tied to specific
state changes in a common way, which is not always true. For example,
stopping the stream may not _actually_ stop it if a background send timer
is still running.
The notion of a "state_changed" callback is also problematic because the
pw_streams that are used in rtp-sink and rtp-source also have a callback
for state changes, causing confusion.
Solve this by replacing state_changed with three new callbacks:
1. report_error : Used for reporting nonrecoverable errors to the caller.
Note that currently, no one does such error reporting, but the feature
does exist, so this callback is introduced to preserve said feature.
2. open_connection : Used for opening a connection. Its optional return
value informs about success or failure.
3. close_connection : Used for opening a connection. Its optional return
value informs about success or failure.
Importantly, these callbacks do not export any internal stream state. This
improves encapsulation, and also makes it possible to invoke these
callbacks in situations that may not neatly map to a state change. One
example could be to close the connection as part of a stream_start call
to close any connection(s) left over from a previous run. (Followup commits
will in fact introduce such measures.)
2025-08-21 13:30:11 +02:00
|
|
|
sess->sending = true;
|
|
|
|
|
if (result)
|
|
|
|
|
*result = 1;
|
|
|
|
|
session_establish(sess);
|
|
|
|
|
}
|
2023-02-28 16:14:19 +01:00
|
|
|
|
module-rtp: Replace state_changed callbacks
The state_changed callbacks fulfill multiple roles, which is both a problem
regarding separation of concerns and regarding code clarity. De facto,
these callbacks cover error reporting, opening connections, and closing
connection, all in one, depending on a state that is arguably an internal
stream detail. The code in these callbacks tie these internal states to
assumptions that opening/closing callbacks is directly tied to specific
state changes in a common way, which is not always true. For example,
stopping the stream may not _actually_ stop it if a background send timer
is still running.
The notion of a "state_changed" callback is also problematic because the
pw_streams that are used in rtp-sink and rtp-source also have a callback
for state changes, causing confusion.
Solve this by replacing state_changed with three new callbacks:
1. report_error : Used for reporting nonrecoverable errors to the caller.
Note that currently, no one does such error reporting, but the feature
does exist, so this callback is introduced to preserve said feature.
2. open_connection : Used for opening a connection. Its optional return
value informs about success or failure.
3. close_connection : Used for opening a connection. Its optional return
value informs about success or failure.
Importantly, these callbacks do not export any internal stream state. This
improves encapsulation, and also makes it possible to invoke these
callbacks in situations that may not neatly map to a state change. One
example could be to close the connection as part of a stream_start call
to close any connection(s) left over from a previous run. (Followup commits
will in fact introduce such measures.)
2025-08-21 13:30:11 +02:00
|
|
|
static void send_close_connection(void *data, int *result)
|
|
|
|
|
{
|
|
|
|
|
struct session *sess = data;
|
|
|
|
|
sess->sending = false;
|
|
|
|
|
if (result)
|
|
|
|
|
*result = 1;
|
|
|
|
|
if (!sess->receiving)
|
|
|
|
|
session_stop(sess);
|
2023-02-28 16:14:19 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void send_send_packet(void *data, struct iovec *iov, size_t iovlen)
|
|
|
|
|
{
|
|
|
|
|
struct session *sess = data;
|
|
|
|
|
struct impl *impl = sess->impl;
|
|
|
|
|
struct msghdr msg;
|
|
|
|
|
|
2023-03-02 13:11:22 +01:00
|
|
|
if (!sess->data_ready || !sess->sending)
|
|
|
|
|
return;
|
|
|
|
|
|
2023-02-28 16:14:19 +01:00
|
|
|
spa_zero(msg);
|
|
|
|
|
msg.msg_name = &sess->data_addr;
|
|
|
|
|
msg.msg_namelen = sess->data_len;
|
|
|
|
|
msg.msg_iov = iov;
|
|
|
|
|
msg.msg_iovlen = iovlen;
|
|
|
|
|
msg.msg_control = NULL;
|
|
|
|
|
msg.msg_controllen = 0;
|
|
|
|
|
msg.msg_flags = 0;
|
|
|
|
|
|
|
|
|
|
send_packet(impl->data_source->fd, &msg);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void recv_destroy(void *data)
|
|
|
|
|
{
|
|
|
|
|
}
|
module-rtp: Replace state_changed callbacks
The state_changed callbacks fulfill multiple roles, which is both a problem
regarding separation of concerns and regarding code clarity. De facto,
these callbacks cover error reporting, opening connections, and closing
connection, all in one, depending on a state that is arguably an internal
stream detail. The code in these callbacks tie these internal states to
assumptions that opening/closing callbacks is directly tied to specific
state changes in a common way, which is not always true. For example,
stopping the stream may not _actually_ stop it if a background send timer
is still running.
The notion of a "state_changed" callback is also problematic because the
pw_streams that are used in rtp-sink and rtp-source also have a callback
for state changes, causing confusion.
Solve this by replacing state_changed with three new callbacks:
1. report_error : Used for reporting nonrecoverable errors to the caller.
Note that currently, no one does such error reporting, but the feature
does exist, so this callback is introduced to preserve said feature.
2. open_connection : Used for opening a connection. Its optional return
value informs about success or failure.
3. close_connection : Used for opening a connection. Its optional return
value informs about success or failure.
Importantly, these callbacks do not export any internal stream state. This
improves encapsulation, and also makes it possible to invoke these
callbacks in situations that may not neatly map to a state change. One
example could be to close the connection as part of a stream_start call
to close any connection(s) left over from a previous run. (Followup commits
will in fact introduce such measures.)
2025-08-21 13:30:11 +02:00
|
|
|
|
|
|
|
|
static void recv_open_connection(void *data, int *result)
|
|
|
|
|
{
|
|
|
|
|
struct session *sess = data;
|
|
|
|
|
sess->receiving = true;
|
|
|
|
|
if (result)
|
|
|
|
|
*result = 1;
|
|
|
|
|
session_establish(sess);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void recv_close_connection(void *data, int *result)
|
2023-02-28 16:14:19 +01:00
|
|
|
{
|
|
|
|
|
struct session *sess = data;
|
module-rtp: Replace state_changed callbacks
The state_changed callbacks fulfill multiple roles, which is both a problem
regarding separation of concerns and regarding code clarity. De facto,
these callbacks cover error reporting, opening connections, and closing
connection, all in one, depending on a state that is arguably an internal
stream detail. The code in these callbacks tie these internal states to
assumptions that opening/closing callbacks is directly tied to specific
state changes in a common way, which is not always true. For example,
stopping the stream may not _actually_ stop it if a background send timer
is still running.
The notion of a "state_changed" callback is also problematic because the
pw_streams that are used in rtp-sink and rtp-source also have a callback
for state changes, causing confusion.
Solve this by replacing state_changed with three new callbacks:
1. report_error : Used for reporting nonrecoverable errors to the caller.
Note that currently, no one does such error reporting, but the feature
does exist, so this callback is introduced to preserve said feature.
2. open_connection : Used for opening a connection. Its optional return
value informs about success or failure.
3. close_connection : Used for opening a connection. Its optional return
value informs about success or failure.
Importantly, these callbacks do not export any internal stream state. This
improves encapsulation, and also makes it possible to invoke these
callbacks in situations that may not neatly map to a state change. One
example could be to close the connection as part of a stream_start call
to close any connection(s) left over from a previous run. (Followup commits
will in fact introduce such measures.)
2025-08-21 13:30:11 +02:00
|
|
|
sess->receiving = false;
|
|
|
|
|
if (result)
|
|
|
|
|
*result = 1;
|
|
|
|
|
if (!sess->sending)
|
|
|
|
|
session_stop(sess);
|
2023-02-28 16:14:19 +01:00
|
|
|
}
|
|
|
|
|
|
2023-03-10 10:47:03 +01:00
|
|
|
static void recv_send_feedback(void *data, uint32_t seqnum)
|
|
|
|
|
{
|
|
|
|
|
struct session *sess = data;
|
|
|
|
|
struct impl *impl = sess->impl;
|
|
|
|
|
struct iovec iov[1];
|
|
|
|
|
struct msghdr msg;
|
|
|
|
|
struct rtp_apple_midi_rs hdr;
|
|
|
|
|
|
|
|
|
|
if (!sess->ctrl_ready || !sess->receiving)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
spa_zero(hdr);
|
|
|
|
|
hdr.cmd = htonl(APPLE_MIDI_CMD_RS);
|
|
|
|
|
hdr.ssrc = htonl(sess->ssrc);
|
|
|
|
|
hdr.seqnum = htonl(seqnum);
|
|
|
|
|
|
|
|
|
|
iov[0].iov_base = &hdr;
|
|
|
|
|
iov[0].iov_len = sizeof(hdr);
|
|
|
|
|
|
|
|
|
|
spa_zero(msg);
|
|
|
|
|
msg.msg_name = &sess->ctrl_addr;
|
|
|
|
|
msg.msg_namelen = sess->ctrl_len;
|
|
|
|
|
msg.msg_iov = iov;
|
|
|
|
|
msg.msg_iovlen = 1;
|
|
|
|
|
|
|
|
|
|
send_packet(impl->ctrl_source->fd, &msg);
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-28 16:14:19 +01:00
|
|
|
static const struct rtp_stream_events send_stream_events = {
|
|
|
|
|
RTP_VERSION_STREAM_EVENTS,
|
|
|
|
|
.destroy = send_destroy,
|
module-rtp: Replace state_changed callbacks
The state_changed callbacks fulfill multiple roles, which is both a problem
regarding separation of concerns and regarding code clarity. De facto,
these callbacks cover error reporting, opening connections, and closing
connection, all in one, depending on a state that is arguably an internal
stream detail. The code in these callbacks tie these internal states to
assumptions that opening/closing callbacks is directly tied to specific
state changes in a common way, which is not always true. For example,
stopping the stream may not _actually_ stop it if a background send timer
is still running.
The notion of a "state_changed" callback is also problematic because the
pw_streams that are used in rtp-sink and rtp-source also have a callback
for state changes, causing confusion.
Solve this by replacing state_changed with three new callbacks:
1. report_error : Used for reporting nonrecoverable errors to the caller.
Note that currently, no one does such error reporting, but the feature
does exist, so this callback is introduced to preserve said feature.
2. open_connection : Used for opening a connection. Its optional return
value informs about success or failure.
3. close_connection : Used for opening a connection. Its optional return
value informs about success or failure.
Importantly, these callbacks do not export any internal stream state. This
improves encapsulation, and also makes it possible to invoke these
callbacks in situations that may not neatly map to a state change. One
example could be to close the connection as part of a stream_start call
to close any connection(s) left over from a previous run. (Followup commits
will in fact introduce such measures.)
2025-08-21 13:30:11 +02:00
|
|
|
.open_connection = send_open_connection,
|
|
|
|
|
.close_connection = send_close_connection,
|
2023-02-28 16:14:19 +01:00
|
|
|
.send_packet = send_send_packet,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
static const struct rtp_stream_events recv_stream_events = {
|
|
|
|
|
RTP_VERSION_STREAM_EVENTS,
|
|
|
|
|
.destroy = recv_destroy,
|
module-rtp: Replace state_changed callbacks
The state_changed callbacks fulfill multiple roles, which is both a problem
regarding separation of concerns and regarding code clarity. De facto,
these callbacks cover error reporting, opening connections, and closing
connection, all in one, depending on a state that is arguably an internal
stream detail. The code in these callbacks tie these internal states to
assumptions that opening/closing callbacks is directly tied to specific
state changes in a common way, which is not always true. For example,
stopping the stream may not _actually_ stop it if a background send timer
is still running.
The notion of a "state_changed" callback is also problematic because the
pw_streams that are used in rtp-sink and rtp-source also have a callback
for state changes, causing confusion.
Solve this by replacing state_changed with three new callbacks:
1. report_error : Used for reporting nonrecoverable errors to the caller.
Note that currently, no one does such error reporting, but the feature
does exist, so this callback is introduced to preserve said feature.
2. open_connection : Used for opening a connection. Its optional return
value informs about success or failure.
3. close_connection : Used for opening a connection. Its optional return
value informs about success or failure.
Importantly, these callbacks do not export any internal stream state. This
improves encapsulation, and also makes it possible to invoke these
callbacks in situations that may not neatly map to a state change. One
example could be to close the connection as part of a stream_start call
to close any connection(s) left over from a previous run. (Followup commits
will in fact introduce such measures.)
2025-08-21 13:30:11 +02:00
|
|
|
.open_connection = recv_open_connection,
|
|
|
|
|
.close_connection = recv_close_connection,
|
2023-03-10 10:47:03 +01:00
|
|
|
.send_feedback = recv_send_feedback,
|
2023-02-28 16:14:19 +01:00
|
|
|
};
|
|
|
|
|
|
2023-05-15 12:47:37 +02:00
|
|
|
static int
|
|
|
|
|
do_unlink_session(struct spa_loop *loop,
|
|
|
|
|
bool async, uint32_t seq, const void *data, size_t size, void *user_data)
|
2023-02-28 16:14:19 +01:00
|
|
|
{
|
2023-05-15 12:47:37 +02:00
|
|
|
struct session *sess = user_data;
|
2023-02-28 16:14:19 +01:00
|
|
|
spa_list_remove(&sess->link);
|
2023-05-15 12:47:37 +02:00
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void free_session(struct session *sess)
|
|
|
|
|
{
|
|
|
|
|
struct impl *impl = sess->impl;
|
|
|
|
|
|
2025-05-30 11:59:35 +02:00
|
|
|
pw_loop_locked(impl->data_loop, do_unlink_session, 1, NULL, 0, sess);
|
2023-05-15 12:47:37 +02:00
|
|
|
|
2023-02-28 16:14:19 +01:00
|
|
|
sess->impl->n_sessions--;
|
|
|
|
|
|
|
|
|
|
if (sess->send)
|
|
|
|
|
rtp_stream_destroy(sess->send);
|
|
|
|
|
if (sess->recv)
|
|
|
|
|
rtp_stream_destroy(sess->recv);
|
2023-03-02 17:18:43 +01:00
|
|
|
free(sess->name);
|
2023-02-28 16:14:19 +01:00
|
|
|
free(sess);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static bool cmp_ip(const struct sockaddr_storage *sa, const struct sockaddr_storage *sb)
|
|
|
|
|
{
|
|
|
|
|
if (sa->ss_family == AF_INET && sb->ss_family == AF_INET) {
|
|
|
|
|
struct sockaddr_in *ia = (struct sockaddr_in*)sa;
|
|
|
|
|
struct sockaddr_in *ib = (struct sockaddr_in*)sb;
|
|
|
|
|
return ia->sin_addr.s_addr == ib->sin_addr.s_addr;
|
|
|
|
|
} else if (sa->ss_family == AF_INET6 && sb->ss_family == AF_INET6) {
|
|
|
|
|
struct sockaddr_in6 *ia = (struct sockaddr_in6*)sa;
|
|
|
|
|
struct sockaddr_in6 *ib = (struct sockaddr_in6*)sb;
|
2024-02-25 09:19:11 +01:00
|
|
|
return ia->sin6_addr.s6_addr == ib->sin6_addr.s6_addr && ia->sin6_scope_id == ib->sin6_scope_id;
|
2023-02-28 16:14:19 +01:00
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-01 10:15:08 +01:00
|
|
|
static struct session *make_session(struct impl *impl, struct pw_properties *props)
|
2023-02-28 16:14:19 +01:00
|
|
|
{
|
|
|
|
|
struct session *sess;
|
2023-03-01 10:15:08 +01:00
|
|
|
const char *str;
|
2023-03-06 10:46:21 +01:00
|
|
|
struct pw_properties *copy;
|
2023-02-28 16:14:19 +01:00
|
|
|
|
|
|
|
|
sess = calloc(1, sizeof(struct session));
|
|
|
|
|
if (sess == NULL)
|
2023-03-01 10:15:08 +01:00
|
|
|
goto error;
|
2023-02-28 16:14:19 +01:00
|
|
|
|
|
|
|
|
spa_list_append(&impl->sessions, &sess->link);
|
|
|
|
|
impl->n_sessions++;
|
|
|
|
|
|
|
|
|
|
sess->impl = impl;
|
|
|
|
|
sess->ssrc = pw_rand32();
|
|
|
|
|
|
2023-03-01 10:15:08 +01:00
|
|
|
str = pw_properties_get(props, "sess.name");
|
2023-03-02 17:18:43 +01:00
|
|
|
sess->name = str ? strdup(str) : strdup("RTP Session");
|
2023-02-28 16:14:19 +01:00
|
|
|
|
2023-03-02 13:53:56 +01:00
|
|
|
if (impl->ts_refclk != NULL)
|
|
|
|
|
pw_properties_setf(props, "rtp.sender-ts-offset", "%u", impl->ts_offset);
|
2023-02-28 16:14:19 +01:00
|
|
|
pw_properties_setf(props, "rtp.sender-ssrc", "%u", sess->ssrc);
|
2023-03-02 17:18:43 +01:00
|
|
|
pw_properties_set(props, "rtp.session", sess->name);
|
2023-02-28 16:14:19 +01:00
|
|
|
|
2023-03-07 09:00:40 +01:00
|
|
|
if (pw_properties_get(props, PW_KEY_NODE_GROUP) == NULL)
|
|
|
|
|
pw_properties_set(props, PW_KEY_NODE_GROUP, impl->session_name);
|
|
|
|
|
|
2023-03-06 10:46:21 +01:00
|
|
|
copy = pw_properties_copy(props);
|
|
|
|
|
|
|
|
|
|
if (pw_properties_get(props, PW_KEY_MEDIA_CLASS) == NULL) {
|
|
|
|
|
const char *media = NULL;
|
|
|
|
|
|
|
|
|
|
str = pw_properties_get(props, "sess.media");
|
|
|
|
|
if (spa_streq(str, "midi"))
|
|
|
|
|
media = "Midi";
|
2023-03-13 12:48:13 +01:00
|
|
|
else if (spa_streq(str, "audio") || spa_streq(str, "opus"))
|
2023-03-06 10:46:21 +01:00
|
|
|
media = "Audio";
|
|
|
|
|
|
|
|
|
|
if (media != NULL) {
|
|
|
|
|
pw_properties_setf(copy, PW_KEY_MEDIA_CLASS, "%s/Sink", media);
|
|
|
|
|
pw_properties_setf(props, PW_KEY_MEDIA_CLASS, "%s/Source", media);
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-03-07 09:00:40 +01:00
|
|
|
|
2023-02-28 16:14:19 +01:00
|
|
|
sess->send = rtp_stream_new(impl->core,
|
2023-03-06 10:46:21 +01:00
|
|
|
PW_DIRECTION_INPUT, copy,
|
2023-02-28 16:14:19 +01:00
|
|
|
&send_stream_events, sess);
|
|
|
|
|
sess->recv = rtp_stream_new(impl->core,
|
2023-03-06 10:46:21 +01:00
|
|
|
PW_DIRECTION_OUTPUT, props,
|
2023-02-28 16:14:19 +01:00
|
|
|
&recv_stream_events, sess);
|
|
|
|
|
|
|
|
|
|
return sess;
|
2023-03-01 10:15:08 +01:00
|
|
|
error:
|
|
|
|
|
pw_properties_free(props);
|
|
|
|
|
return NULL;
|
2023-02-28 16:14:19 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static struct session *find_session_by_addr_name(struct impl *impl,
|
|
|
|
|
const struct sockaddr_storage *sa, const char *name)
|
|
|
|
|
{
|
|
|
|
|
struct session *sess;
|
|
|
|
|
spa_list_for_each(sess, &impl->sessions, link) {
|
2023-03-07 12:04:50 +01:00
|
|
|
pw_log_info("%p '%s' '%s'", sess, name, sess->name);
|
2023-02-28 16:14:19 +01:00
|
|
|
if (cmp_ip(sa, &sess->ctrl_addr) &&
|
|
|
|
|
spa_streq(sess->name, name))
|
|
|
|
|
return sess;
|
|
|
|
|
}
|
|
|
|
|
return NULL;
|
|
|
|
|
}
|
2023-03-07 12:04:50 +01:00
|
|
|
static struct session *find_session_by_initiator(struct impl *impl, uint32_t initiator, bool ctrl)
|
2023-02-28 16:14:19 +01:00
|
|
|
{
|
|
|
|
|
struct session *sess;
|
|
|
|
|
spa_list_for_each(sess, &impl->sessions, link) {
|
2023-03-07 12:04:50 +01:00
|
|
|
uint32_t target = ctrl ? sess->ctrl_initiator : sess->data_initiator;
|
|
|
|
|
if (target == initiator)
|
2023-02-28 16:14:19 +01:00
|
|
|
return sess;
|
|
|
|
|
}
|
|
|
|
|
return NULL;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static struct session *find_session_by_ssrc(struct impl *impl, uint32_t ssrc)
|
|
|
|
|
{
|
|
|
|
|
struct session *sess;
|
|
|
|
|
spa_list_for_each(sess, &impl->sessions, link) {
|
|
|
|
|
if (sess->remote_ssrc == ssrc)
|
|
|
|
|
return sess;
|
|
|
|
|
}
|
|
|
|
|
return NULL;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void parse_apple_midi_cmd_in(struct impl *impl, bool ctrl, uint8_t *buffer,
|
|
|
|
|
ssize_t len, struct sockaddr_storage *sa, socklen_t salen)
|
|
|
|
|
{
|
|
|
|
|
struct rtp_apple_midi *hdr = (struct rtp_apple_midi*)buffer;
|
|
|
|
|
struct iovec iov[3];
|
|
|
|
|
struct msghdr msg;
|
|
|
|
|
struct rtp_apple_midi reply;
|
|
|
|
|
struct session *sess;
|
|
|
|
|
bool success = true;
|
2023-03-01 10:54:00 +01:00
|
|
|
uint32_t initiator, ssrc;
|
2023-02-28 16:14:19 +01:00
|
|
|
char addr[128];
|
|
|
|
|
uint16_t port = 0;
|
|
|
|
|
|
2023-03-01 10:54:00 +01:00
|
|
|
initiator = ntohl(hdr->initiator);
|
|
|
|
|
ssrc = ntohl(hdr->ssrc);
|
|
|
|
|
|
2024-02-26 15:17:48 +01:00
|
|
|
pw_net_get_ip(sa, addr, sizeof(addr), NULL, &port);
|
2023-03-07 12:04:50 +01:00
|
|
|
pw_log_info("IN from %s:%d %s ssrc:%08x initiator:%08x",
|
|
|
|
|
addr, port, hdr->name, ssrc, initiator);
|
2023-02-28 16:14:19 +01:00
|
|
|
|
|
|
|
|
if (ctrl) {
|
2023-03-07 12:04:50 +01:00
|
|
|
sess = find_session_by_addr_name(impl, sa, hdr->name);
|
2023-03-01 16:54:59 +01:00
|
|
|
if (sess == NULL) {
|
2023-03-07 12:04:50 +01:00
|
|
|
pw_log_warn("receive ctrl IN from nonexisting session %s", hdr->name);
|
2023-03-01 16:54:59 +01:00
|
|
|
success = false;
|
|
|
|
|
} else {
|
2023-03-07 12:04:50 +01:00
|
|
|
if (sess->ctrl_ready &&
|
|
|
|
|
(sess->remote_ssrc != ssrc || sess->ctrl_initiator != initiator)) {
|
|
|
|
|
pw_log_warn("receive ctrl IN from existing initiator:%08x", initiator);
|
2023-02-28 16:14:19 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (success) {
|
2023-03-03 16:08:27 +01:00
|
|
|
sess->we_initiated = false;
|
2023-03-07 12:04:50 +01:00
|
|
|
sess->remote_ssrc = ssrc;
|
|
|
|
|
sess->ctrl_initiator = initiator;
|
2023-02-28 16:14:19 +01:00
|
|
|
sess->ctrl_addr = *sa;
|
|
|
|
|
sess->ctrl_len = salen;
|
|
|
|
|
sess->ctrl_ready = true;
|
2023-03-07 09:00:40 +01:00
|
|
|
session_update_state(sess, SESSION_STATE_ESTABLISHING);
|
2023-02-28 16:14:19 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else {
|
2023-03-07 12:04:50 +01:00
|
|
|
sess = find_session_by_ssrc(impl, ssrc);
|
2023-02-28 16:14:19 +01:00
|
|
|
if (sess == NULL) {
|
2023-03-07 12:04:50 +01:00
|
|
|
pw_log_warn("receive data IN from nonexisting ssrc:%08x", ssrc);
|
2023-02-28 16:14:19 +01:00
|
|
|
success = false;
|
|
|
|
|
} else {
|
2023-03-07 12:04:50 +01:00
|
|
|
if (sess->data_ready) {
|
|
|
|
|
pw_log_warn("receive data IN from existing initiator:%08x", initiator);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (success) {
|
|
|
|
|
pw_log_info("got data IN initiator:%08x, session established", initiator);
|
|
|
|
|
sess->data_initiator = initiator;
|
2023-02-28 16:14:19 +01:00
|
|
|
sess->data_addr = *sa;
|
|
|
|
|
sess->data_len = salen;
|
|
|
|
|
sess->data_ready = true;
|
2023-03-07 09:00:40 +01:00
|
|
|
session_update_state(sess, SESSION_STATE_ESTABLISHED);
|
2023-02-28 16:14:19 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
reply = *hdr;
|
|
|
|
|
if (success) {
|
|
|
|
|
reply.cmd = htonl(APPLE_MIDI_CMD_OK);
|
|
|
|
|
reply.ssrc = htonl(sess->ssrc);
|
|
|
|
|
} else
|
|
|
|
|
reply.cmd = htonl(APPLE_MIDI_CMD_NO);
|
|
|
|
|
|
|
|
|
|
iov[0].iov_base = &reply;
|
|
|
|
|
iov[0].iov_len = sizeof(reply);
|
2023-03-01 10:54:00 +01:00
|
|
|
iov[1].iov_base = impl->session_name;
|
|
|
|
|
iov[1].iov_len = strlen(impl->session_name)+1;
|
2023-02-28 16:14:19 +01:00
|
|
|
|
|
|
|
|
spa_zero(msg);
|
|
|
|
|
msg.msg_name = sa;
|
|
|
|
|
msg.msg_namelen = salen;
|
|
|
|
|
msg.msg_iov = iov;
|
|
|
|
|
msg.msg_iovlen = 2;
|
|
|
|
|
|
2023-12-16 19:43:17 +03:00
|
|
|
pw_log_trace("send %p %u", msg.msg_name, msg.msg_namelen);
|
2023-02-28 16:14:19 +01:00
|
|
|
|
|
|
|
|
send_packet(ctrl ? impl->ctrl_source->fd : impl->data_source->fd, &msg);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void parse_apple_midi_cmd_ok(struct impl *impl, bool ctrl, uint8_t *buffer,
|
|
|
|
|
ssize_t len, struct sockaddr_storage *sa, socklen_t salen)
|
|
|
|
|
{
|
|
|
|
|
struct rtp_apple_midi *hdr = (struct rtp_apple_midi*)buffer;
|
|
|
|
|
uint32_t initiator = ntohl(hdr->initiator);
|
|
|
|
|
struct session *sess;
|
|
|
|
|
|
2023-03-07 12:04:50 +01:00
|
|
|
sess = find_session_by_initiator(impl, initiator, ctrl);
|
2023-03-01 11:40:11 +01:00
|
|
|
if (sess == NULL || !sess->we_initiated) {
|
2023-02-28 16:14:19 +01:00
|
|
|
pw_log_warn("received OK from nonexisting session %u", initiator);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (ctrl) {
|
2023-03-03 15:58:12 +01:00
|
|
|
pw_log_info("got ctrl OK %08x %u", initiator, sess->data_ready);
|
2023-02-28 16:14:19 +01:00
|
|
|
sess->ctrl_ready = true;
|
|
|
|
|
if (!sess->data_ready)
|
|
|
|
|
send_apple_midi_cmd_in(sess, false);
|
|
|
|
|
} else {
|
2023-03-03 15:58:12 +01:00
|
|
|
pw_log_info("got data OK %08x %u, session established", initiator,
|
|
|
|
|
sess->ctrl_ready);
|
2023-03-01 10:54:00 +01:00
|
|
|
sess->remote_ssrc = ntohl(hdr->ssrc);
|
2023-02-28 16:14:19 +01:00
|
|
|
sess->data_ready = true;
|
2023-03-03 15:58:12 +01:00
|
|
|
if (sess->ctrl_ready)
|
2023-03-07 09:00:40 +01:00
|
|
|
session_update_state(sess, SESSION_STATE_ESTABLISHED);
|
2023-02-28 16:14:19 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-10 10:47:03 +01:00
|
|
|
static void parse_apple_midi_cmd_no(struct impl *impl, bool ctrl, uint8_t *buffer,
|
|
|
|
|
ssize_t len, struct sockaddr_storage *sa, socklen_t salen)
|
|
|
|
|
{
|
|
|
|
|
struct rtp_apple_midi *hdr = (struct rtp_apple_midi*)buffer;
|
|
|
|
|
uint32_t initiator = ntohl(hdr->initiator);
|
|
|
|
|
struct session *sess;
|
|
|
|
|
|
|
|
|
|
sess = find_session_by_initiator(impl, initiator, ctrl);
|
|
|
|
|
if (sess == NULL || !sess->we_initiated) {
|
|
|
|
|
pw_log_warn("received NO from nonexisting session %u", initiator);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (ctrl) {
|
|
|
|
|
pw_log_info("got ctrl NO %08x %u", initiator, sess->data_ready);
|
|
|
|
|
sess->ctrl_ready = false;
|
|
|
|
|
} else {
|
|
|
|
|
pw_log_info("got data NO %08x %u, session canceled", initiator,
|
|
|
|
|
sess->ctrl_ready);
|
|
|
|
|
sess->data_ready = false;
|
|
|
|
|
if (!sess->ctrl_ready)
|
|
|
|
|
session_update_state(sess, SESSION_STATE_INIT);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-28 16:14:19 +01:00
|
|
|
static void parse_apple_midi_cmd_ck(struct impl *impl, bool ctrl, uint8_t *buffer,
|
|
|
|
|
ssize_t len, struct sockaddr_storage *sa, socklen_t salen)
|
|
|
|
|
{
|
|
|
|
|
struct rtp_apple_midi_ck *hdr = (struct rtp_apple_midi_ck*)buffer;
|
|
|
|
|
struct iovec iov[3];
|
|
|
|
|
struct msghdr msg;
|
|
|
|
|
struct rtp_apple_midi_ck reply;
|
|
|
|
|
struct session *sess;
|
2023-03-07 09:00:40 +01:00
|
|
|
uint64_t now, t1, t2, t3;
|
2023-02-28 16:14:19 +01:00
|
|
|
uint32_t ssrc = ntohl(hdr->ssrc);
|
|
|
|
|
|
|
|
|
|
sess = find_session_by_ssrc(impl, ssrc);
|
|
|
|
|
if (sess == NULL) {
|
|
|
|
|
pw_log_warn("unknown SSRC %u", ssrc);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-16 19:43:17 +03:00
|
|
|
pw_log_trace("got CK count %d", hdr->count);
|
2023-02-28 16:14:19 +01:00
|
|
|
|
2023-03-07 09:00:40 +01:00
|
|
|
now = current_time_ns() / 10000;
|
2023-02-28 16:14:19 +01:00
|
|
|
reply = *hdr;
|
|
|
|
|
reply.ssrc = htonl(sess->ssrc);
|
|
|
|
|
reply.count++;
|
|
|
|
|
iov[0].iov_base = &reply;
|
|
|
|
|
iov[0].iov_len = sizeof(reply);
|
|
|
|
|
|
2023-03-07 09:00:40 +01:00
|
|
|
t1 = ((uint64_t)ntohl(hdr->ts1_h) << 32) | ntohl(hdr->ts1_l);
|
|
|
|
|
t2 = t3 = 0;
|
|
|
|
|
|
2023-02-28 16:14:19 +01:00
|
|
|
switch (hdr->count) {
|
|
|
|
|
case 0:
|
2023-03-07 09:00:40 +01:00
|
|
|
t2 = now;
|
2023-02-28 16:14:19 +01:00
|
|
|
break;
|
|
|
|
|
case 1:
|
2023-03-07 09:00:40 +01:00
|
|
|
t2 = ((uint64_t)ntohl(hdr->ts2_h) << 32) | ntohl(hdr->ts2_l);
|
|
|
|
|
t3 = now;
|
2023-02-28 16:14:19 +01:00
|
|
|
break;
|
|
|
|
|
case 2:
|
2023-03-07 09:00:40 +01:00
|
|
|
t3 = ((uint64_t)ntohl(hdr->ts3_h) << 32) | ntohl(hdr->ts3_l);
|
2023-02-28 16:14:19 +01:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-07 09:00:40 +01:00
|
|
|
if (hdr->count >= 1) {
|
|
|
|
|
int64_t latency, offset;
|
|
|
|
|
latency = t3 - t1;
|
|
|
|
|
offset = ((t3 + t1) / 2) - t2;
|
|
|
|
|
|
2023-12-16 19:43:17 +03:00
|
|
|
pw_log_trace("latency:%f offset:%f", latency / 1e5, offset / 1e5);
|
2023-03-07 09:00:40 +01:00
|
|
|
if (hdr->count >= 2)
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
reply.ts2_h = htonl(t2 >> 32);
|
|
|
|
|
reply.ts2_l = htonl(t2);
|
|
|
|
|
reply.ts3_h = htonl(t3 >> 32);
|
|
|
|
|
reply.ts3_l = htonl(t3);
|
|
|
|
|
|
2023-02-28 16:14:19 +01:00
|
|
|
spa_zero(msg);
|
|
|
|
|
msg.msg_name = sa;
|
|
|
|
|
msg.msg_namelen = salen;
|
|
|
|
|
msg.msg_iov = iov;
|
|
|
|
|
msg.msg_iovlen = 1;
|
|
|
|
|
|
2023-12-16 19:43:17 +03:00
|
|
|
pw_log_trace("send %p %u", msg.msg_name, msg.msg_namelen);
|
2023-02-28 16:14:19 +01:00
|
|
|
|
|
|
|
|
send_packet(ctrl ? impl->ctrl_source->fd : impl->data_source->fd, &msg);
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-01 11:40:11 +01:00
|
|
|
static void parse_apple_midi_cmd_by(struct impl *impl, bool ctrl, uint8_t *buffer,
|
|
|
|
|
ssize_t len, struct sockaddr_storage *sa, socklen_t salen)
|
|
|
|
|
{
|
|
|
|
|
struct rtp_apple_midi *hdr = (struct rtp_apple_midi*)buffer;
|
|
|
|
|
uint32_t initiator = ntohl(hdr->initiator);
|
|
|
|
|
struct session *sess;
|
|
|
|
|
|
2023-03-07 12:04:50 +01:00
|
|
|
sess = find_session_by_initiator(impl, initiator, ctrl);
|
2023-03-01 12:57:57 +01:00
|
|
|
if (sess == NULL || sess->we_initiated) {
|
2023-03-01 11:40:11 +01:00
|
|
|
pw_log_warn("received BY from nonexisting initiator %08x", initiator);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (ctrl) {
|
2023-03-03 15:58:12 +01:00
|
|
|
pw_log_info("%p: got ctrl BY %08x %u", sess, initiator, sess->data_ready);
|
2023-03-01 11:40:11 +01:00
|
|
|
sess->ctrl_ready = false;
|
2023-03-03 15:58:12 +01:00
|
|
|
if (!sess->data_ready)
|
2023-03-07 09:00:40 +01:00
|
|
|
session_update_state(sess, SESSION_STATE_INIT);
|
2023-03-01 11:40:11 +01:00
|
|
|
} else {
|
2023-03-03 15:58:12 +01:00
|
|
|
pw_log_info("%p: got data BY %08x %u", sess, initiator, sess->ctrl_ready);
|
2023-03-01 11:40:11 +01:00
|
|
|
sess->data_ready = false;
|
2023-03-03 15:58:12 +01:00
|
|
|
if (!sess->ctrl_ready)
|
2023-03-07 09:00:40 +01:00
|
|
|
session_update_state(sess, SESSION_STATE_INIT);
|
2023-03-01 11:40:11 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-10 10:47:03 +01:00
|
|
|
static void parse_apple_midi_cmd_rs(struct impl *impl, bool ctrl, uint8_t *buffer,
|
|
|
|
|
ssize_t len, struct sockaddr_storage *sa, socklen_t salen)
|
|
|
|
|
{
|
|
|
|
|
struct rtp_apple_midi_rs *hdr = (struct rtp_apple_midi_rs*)buffer;
|
|
|
|
|
struct session *sess;
|
|
|
|
|
uint32_t ssrc, seqnum;
|
|
|
|
|
|
|
|
|
|
ssrc = ntohl(hdr->ssrc);
|
|
|
|
|
sess = find_session_by_ssrc(impl, ssrc);
|
|
|
|
|
if (sess == NULL) {
|
|
|
|
|
pw_log_warn("unknown SSRC %u", ssrc);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
seqnum = ntohl(hdr->seqnum);
|
|
|
|
|
pw_log_debug("got RS seqnum %u", seqnum);
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-28 16:14:19 +01:00
|
|
|
static void parse_apple_midi_cmd(struct impl *impl, bool ctrl, uint8_t *buffer,
|
|
|
|
|
ssize_t len, struct sockaddr_storage *sa, socklen_t salen)
|
|
|
|
|
{
|
|
|
|
|
struct rtp_apple_midi *hdr = (struct rtp_apple_midi*)buffer;
|
|
|
|
|
switch (ntohl(hdr->cmd)) {
|
|
|
|
|
case APPLE_MIDI_CMD_IN:
|
|
|
|
|
parse_apple_midi_cmd_in(impl, ctrl, buffer, len, sa, salen);
|
|
|
|
|
break;
|
|
|
|
|
case APPLE_MIDI_CMD_OK:
|
|
|
|
|
parse_apple_midi_cmd_ok(impl, ctrl, buffer, len, sa, salen);
|
|
|
|
|
break;
|
2023-03-10 10:47:03 +01:00
|
|
|
case APPLE_MIDI_CMD_NO:
|
|
|
|
|
parse_apple_midi_cmd_no(impl, ctrl, buffer, len, sa, salen);
|
|
|
|
|
break;
|
2023-02-28 16:14:19 +01:00
|
|
|
case APPLE_MIDI_CMD_CK:
|
|
|
|
|
parse_apple_midi_cmd_ck(impl, ctrl, buffer, len, sa, salen);
|
|
|
|
|
break;
|
2023-03-01 11:40:11 +01:00
|
|
|
case APPLE_MIDI_CMD_BY:
|
|
|
|
|
parse_apple_midi_cmd_by(impl, ctrl, buffer, len, sa, salen);
|
|
|
|
|
break;
|
2023-03-10 10:47:03 +01:00
|
|
|
case APPLE_MIDI_CMD_RS:
|
|
|
|
|
parse_apple_midi_cmd_rs(impl, ctrl, buffer, len, sa, salen);
|
|
|
|
|
break;
|
2023-02-28 16:14:19 +01:00
|
|
|
default:
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void
|
|
|
|
|
on_ctrl_io(void *data, int fd, uint32_t mask)
|
|
|
|
|
{
|
|
|
|
|
struct impl *impl = data;
|
|
|
|
|
ssize_t len;
|
|
|
|
|
uint8_t buffer[2048];
|
|
|
|
|
|
|
|
|
|
if (mask & SPA_IO_IN) {
|
|
|
|
|
struct sockaddr_storage sa;
|
|
|
|
|
socklen_t salen = sizeof(sa);
|
|
|
|
|
|
|
|
|
|
if ((len = recvfrom(fd, buffer, sizeof(buffer), 0,
|
|
|
|
|
(struct sockaddr*)&sa, &salen)) < 0)
|
|
|
|
|
goto receive_error;
|
|
|
|
|
|
|
|
|
|
if (len < 12)
|
|
|
|
|
goto short_packet;
|
|
|
|
|
|
|
|
|
|
if (buffer[0] == 0xff && buffer[1] == 0xff) {
|
|
|
|
|
parse_apple_midi_cmd(impl, true, buffer, len, &sa, salen);
|
|
|
|
|
} else {
|
2024-01-11 17:49:50 +01:00
|
|
|
spa_debug_log_mem(pw_log_get(), SPA_LOG_LEVEL_DEBUG, 0, buffer, len);
|
2023-02-28 16:14:19 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
receive_error:
|
|
|
|
|
pw_log_warn("recv error: %m");
|
|
|
|
|
return;
|
|
|
|
|
short_packet:
|
|
|
|
|
pw_log_warn("short packet received");
|
2024-01-11 17:49:50 +01:00
|
|
|
spa_debug_log_mem(pw_log_get(), SPA_LOG_LEVEL_DEBUG, 0, buffer, len);
|
2023-02-28 16:14:19 +01:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void
|
|
|
|
|
on_data_io(void *data, int fd, uint32_t mask)
|
|
|
|
|
{
|
|
|
|
|
struct impl *impl = data;
|
|
|
|
|
ssize_t len;
|
|
|
|
|
uint8_t buffer[2048];
|
2023-03-01 16:54:59 +01:00
|
|
|
uint32_t ssrc;
|
2023-02-28 16:14:19 +01:00
|
|
|
|
|
|
|
|
if (mask & SPA_IO_IN) {
|
|
|
|
|
struct sockaddr_storage sa;
|
|
|
|
|
socklen_t salen = sizeof(sa);
|
|
|
|
|
|
|
|
|
|
if ((len = recvfrom(fd, buffer, sizeof(buffer), 0,
|
|
|
|
|
(struct sockaddr*)&sa, &salen)) < 0)
|
|
|
|
|
goto receive_error;
|
|
|
|
|
|
|
|
|
|
if (len < 12)
|
|
|
|
|
goto short_packet;
|
|
|
|
|
|
|
|
|
|
if (buffer[0] == 0xff && buffer[1] == 0xff) {
|
|
|
|
|
parse_apple_midi_cmd(impl, false, buffer, len, &sa, salen);
|
|
|
|
|
} else {
|
|
|
|
|
struct rtp_header *hdr = (struct rtp_header*)buffer;
|
2023-03-01 16:54:59 +01:00
|
|
|
struct session *sess;
|
|
|
|
|
|
|
|
|
|
ssrc = ntohl(hdr->ssrc);
|
|
|
|
|
sess = find_session_by_ssrc(impl, ssrc);
|
2023-02-28 16:14:19 +01:00
|
|
|
if (sess == NULL)
|
|
|
|
|
goto unknown_ssrc;
|
|
|
|
|
|
2023-03-02 13:11:22 +01:00
|
|
|
if (sess->data_ready && sess->receiving)
|
|
|
|
|
rtp_stream_receive_packet(sess->recv, buffer, len);
|
2023-02-28 16:14:19 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
receive_error:
|
|
|
|
|
pw_log_warn("recv error: %m");
|
|
|
|
|
return;
|
|
|
|
|
short_packet:
|
|
|
|
|
pw_log_warn("short packet received");
|
2024-01-11 17:49:50 +01:00
|
|
|
spa_debug_log_mem(pw_log_get(), SPA_LOG_LEVEL_DEBUG, 0, buffer, len);
|
2023-02-28 16:14:19 +01:00
|
|
|
return;
|
|
|
|
|
unknown_ssrc:
|
2023-03-02 12:33:40 +01:00
|
|
|
pw_log_debug("unknown SSRC %08x", ssrc);
|
2023-02-28 16:14:19 +01:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static int make_socket(const struct sockaddr_storage* sa, socklen_t salen,
|
|
|
|
|
bool loop, int ttl, char *ifname)
|
|
|
|
|
{
|
|
|
|
|
int af, fd, val, res;
|
|
|
|
|
struct ifreq req;
|
|
|
|
|
struct sockaddr_storage src = *sa;
|
|
|
|
|
bool is_multicast = false;
|
|
|
|
|
|
|
|
|
|
af = sa->ss_family;
|
|
|
|
|
if ((fd = socket(af, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0)) < 0) {
|
|
|
|
|
pw_log_error("socket failed: %m");
|
|
|
|
|
return -errno;
|
|
|
|
|
}
|
|
|
|
|
#ifdef SO_TIMESTAMP
|
|
|
|
|
val = 1;
|
|
|
|
|
if (setsockopt(fd, SOL_SOCKET, SO_TIMESTAMP, &val, sizeof(val)) < 0) {
|
|
|
|
|
res = -errno;
|
|
|
|
|
pw_log_error("setsockopt failed: %m");
|
|
|
|
|
goto error;
|
|
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
val = 1;
|
|
|
|
|
if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(val)) < 0) {
|
|
|
|
|
res = -errno;
|
|
|
|
|
pw_log_error("setsockopt failed: %m");
|
|
|
|
|
goto error;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
spa_zero(req);
|
|
|
|
|
if (ifname) {
|
|
|
|
|
snprintf(req.ifr_name, sizeof(req.ifr_name), "%s", ifname);
|
|
|
|
|
res = ioctl(fd, SIOCGIFINDEX, &req);
|
|
|
|
|
if (res < 0)
|
|
|
|
|
pw_log_warn("SIOCGIFINDEX %s failed: %m", ifname);
|
|
|
|
|
}
|
|
|
|
|
res = 0;
|
|
|
|
|
if (af == AF_INET) {
|
|
|
|
|
static const uint32_t ipv4_mcast_mask = 0xe0000000;
|
|
|
|
|
struct sockaddr_in *sa4 = (struct sockaddr_in*)&src;
|
|
|
|
|
if ((ntohl(sa4->sin_addr.s_addr) & ipv4_mcast_mask) == ipv4_mcast_mask) {
|
|
|
|
|
struct ip_mreqn mr4;
|
|
|
|
|
memset(&mr4, 0, sizeof(mr4));
|
|
|
|
|
mr4.imr_multiaddr = sa4->sin_addr;
|
|
|
|
|
mr4.imr_ifindex = req.ifr_ifindex;
|
|
|
|
|
res = setsockopt(fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mr4, sizeof(mr4));
|
|
|
|
|
is_multicast = true;
|
|
|
|
|
} else {
|
|
|
|
|
sa4->sin_addr.s_addr = INADDR_ANY;
|
|
|
|
|
}
|
|
|
|
|
} else if (af == AF_INET6) {
|
|
|
|
|
struct sockaddr_in6 *sa6 = (struct sockaddr_in6*)&src;
|
|
|
|
|
if (sa6->sin6_addr.s6_addr[0] == 0xff) {
|
|
|
|
|
struct ipv6_mreq mr6;
|
|
|
|
|
memset(&mr6, 0, sizeof(mr6));
|
|
|
|
|
mr6.ipv6mr_multiaddr = sa6->sin6_addr;
|
|
|
|
|
mr6.ipv6mr_interface = req.ifr_ifindex;
|
|
|
|
|
res = setsockopt(fd, IPPROTO_IPV6, IPV6_JOIN_GROUP, &mr6, sizeof(mr6));
|
|
|
|
|
is_multicast = true;
|
|
|
|
|
} else {
|
|
|
|
|
sa6->sin6_addr = in6addr_any;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
res = -EINVAL;
|
|
|
|
|
goto error;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (res < 0) {
|
|
|
|
|
res = -errno;
|
|
|
|
|
pw_log_error("join mcast failed: %m");
|
|
|
|
|
goto error;
|
|
|
|
|
}
|
|
|
|
|
if (is_multicast) {
|
|
|
|
|
val = loop;
|
|
|
|
|
if (setsockopt(fd, IPPROTO_IP, IP_MULTICAST_LOOP, &val, sizeof(val)) < 0)
|
|
|
|
|
pw_log_warn("setsockopt(IP_MULTICAST_LOOP) failed: %m");
|
|
|
|
|
|
|
|
|
|
val = ttl;
|
|
|
|
|
if (setsockopt(fd, IPPROTO_IP, IP_MULTICAST_TTL, &val, sizeof(val)) < 0)
|
|
|
|
|
pw_log_warn("setsockopt(IP_MULTICAST_TTL) failed: %m");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (bind(fd, (struct sockaddr*)&src, salen) < 0) {
|
|
|
|
|
res = -errno;
|
|
|
|
|
pw_log_error("bind() failed: %m");
|
|
|
|
|
goto error;
|
|
|
|
|
}
|
|
|
|
|
val = IPTOS_LOWDELAY;
|
|
|
|
|
if (setsockopt(fd, IPPROTO_IP, IP_TOS, &val, sizeof(val)) < 0)
|
|
|
|
|
pw_log_warn("setsockopt(IP_TOS) failed: %m");
|
|
|
|
|
|
2023-03-27 16:01:12 +02:00
|
|
|
pw_log_debug("new socket fd:%d", fd);
|
|
|
|
|
|
2023-02-28 16:14:19 +01:00
|
|
|
return fd;
|
|
|
|
|
error:
|
2023-03-16 11:44:56 +01:00
|
|
|
close(fd);
|
2023-02-28 16:14:19 +01:00
|
|
|
return res;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static int setup_apple_session(struct impl *impl)
|
|
|
|
|
{
|
|
|
|
|
int fd;
|
|
|
|
|
|
|
|
|
|
if ((fd = make_socket(&impl->ctrl_addr, impl->ctrl_len,
|
|
|
|
|
impl->mcast_loop, impl->ttl, impl->ifname)) < 0) {
|
|
|
|
|
return fd;
|
|
|
|
|
}
|
|
|
|
|
impl->ctrl_source = pw_loop_add_io(impl->loop, fd,
|
|
|
|
|
SPA_IO_IN, true, on_ctrl_io, impl);
|
|
|
|
|
|
2023-03-16 11:44:56 +01:00
|
|
|
if (impl->ctrl_source == NULL) {
|
|
|
|
|
close(fd);
|
2023-02-28 16:14:19 +01:00
|
|
|
return -errno;
|
2023-03-16 11:44:56 +01:00
|
|
|
}
|
2023-02-28 16:14:19 +01:00
|
|
|
|
2023-03-01 09:21:53 +01:00
|
|
|
if ((fd = make_socket(&impl->data_addr, impl->data_len,
|
2023-02-28 16:14:19 +01:00
|
|
|
impl->mcast_loop, impl->ttl, impl->ifname)) < 0)
|
|
|
|
|
return fd;
|
|
|
|
|
|
|
|
|
|
impl->data_source = pw_loop_add_io(impl->data_loop, fd,
|
|
|
|
|
SPA_IO_IN, true, on_data_io, impl);
|
2023-03-16 11:44:56 +01:00
|
|
|
if (impl->data_source == NULL) {
|
|
|
|
|
close(fd);
|
2023-02-28 16:14:19 +01:00
|
|
|
return -errno;
|
2023-03-16 11:44:56 +01:00
|
|
|
}
|
2023-02-28 16:14:19 +01:00
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void core_destroy(void *d)
|
|
|
|
|
{
|
|
|
|
|
struct impl *impl = d;
|
|
|
|
|
spa_hook_remove(&impl->core_listener);
|
|
|
|
|
impl->core = NULL;
|
|
|
|
|
pw_impl_module_schedule_destroy(impl->module);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static const struct pw_proxy_events core_proxy_events = {
|
|
|
|
|
.destroy = core_destroy,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
static void impl_destroy(struct impl *impl)
|
|
|
|
|
{
|
|
|
|
|
struct session *sess;
|
|
|
|
|
|
|
|
|
|
spa_list_consume(sess, &impl->sessions, link)
|
|
|
|
|
free_session(sess);
|
|
|
|
|
|
|
|
|
|
if (impl->core && impl->do_disconnect)
|
|
|
|
|
pw_core_disconnect(impl->core);
|
|
|
|
|
|
|
|
|
|
if (impl->timer)
|
|
|
|
|
pw_loop_destroy_source(impl->loop, impl->timer);
|
|
|
|
|
if (impl->ctrl_source)
|
|
|
|
|
pw_loop_destroy_source(impl->loop, impl->ctrl_source);
|
|
|
|
|
if (impl->data_source)
|
|
|
|
|
pw_loop_destroy_source(impl->data_loop, impl->data_source);
|
|
|
|
|
|
2023-03-27 16:01:12 +02:00
|
|
|
if (impl->client)
|
|
|
|
|
avahi_client_free(impl->client);
|
|
|
|
|
|
2024-04-22 16:19:02 +02:00
|
|
|
if (impl->data_loop)
|
|
|
|
|
pw_context_release_loop(impl->context, impl->data_loop);
|
|
|
|
|
|
2023-02-28 16:14:19 +01:00
|
|
|
pw_properties_free(impl->stream_props);
|
|
|
|
|
pw_properties_free(impl->props);
|
|
|
|
|
|
|
|
|
|
free(impl->ifname);
|
|
|
|
|
free(impl->ts_refclk);
|
|
|
|
|
free(impl->session_name);
|
|
|
|
|
free(impl);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void module_destroy(void *d)
|
|
|
|
|
{
|
|
|
|
|
struct impl *impl = d;
|
|
|
|
|
spa_hook_remove(&impl->module_listener);
|
|
|
|
|
impl_destroy(impl);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static const struct pw_impl_module_events module_events = {
|
|
|
|
|
PW_VERSION_IMPL_MODULE_EVENTS,
|
|
|
|
|
.destroy = module_destroy,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
static void on_core_error(void *d, uint32_t id, int seq, int res, const char *message)
|
|
|
|
|
{
|
|
|
|
|
struct impl *impl = d;
|
|
|
|
|
|
|
|
|
|
pw_log_error("error id:%u seq:%d res:%d (%s): %s",
|
|
|
|
|
id, seq, res, spa_strerror(res), message);
|
|
|
|
|
|
|
|
|
|
if (id == PW_ID_CORE && res == -EPIPE)
|
|
|
|
|
pw_impl_module_schedule_destroy(impl->module);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static const struct pw_core_events core_events = {
|
|
|
|
|
PW_VERSION_CORE_EVENTS,
|
|
|
|
|
.error = on_core_error,
|
|
|
|
|
};
|
|
|
|
|
|
2023-03-01 10:15:08 +01:00
|
|
|
static void free_service(struct service *s)
|
|
|
|
|
{
|
|
|
|
|
spa_list_remove(&s->link);
|
|
|
|
|
|
|
|
|
|
if (s->sess)
|
|
|
|
|
free_session(s->sess);
|
|
|
|
|
|
|
|
|
|
free((char *) s->info.name);
|
|
|
|
|
free((char *) s->info.type);
|
|
|
|
|
free((char *) s->info.domain);
|
|
|
|
|
free((char *) s->info.host_name);
|
|
|
|
|
free(s);
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-01 18:01:26 +01:00
|
|
|
static const char *get_service_name(struct impl *impl)
|
|
|
|
|
{
|
|
|
|
|
const char *str;
|
2023-03-03 12:04:50 +01:00
|
|
|
str = pw_properties_get(impl->props, "sess.media");
|
2023-03-01 18:01:26 +01:00
|
|
|
if (spa_streq(str, "midi"))
|
|
|
|
|
return "_apple-midi._udp";
|
2023-03-13 12:48:13 +01:00
|
|
|
else if (spa_streq(str, "audio") || spa_streq(str, "opus"))
|
2023-03-01 18:01:26 +01:00
|
|
|
return "_pipewire-audio._udp";
|
|
|
|
|
return NULL;
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-01 09:21:53 +01:00
|
|
|
static struct service *make_service(struct impl *impl, const struct service_info *info,
|
|
|
|
|
AvahiStringList *txt)
|
|
|
|
|
{
|
2023-03-01 18:01:26 +01:00
|
|
|
struct service *s = NULL;
|
2024-05-15 15:39:27 +02:00
|
|
|
char at[AVAHI_ADDRESS_STR_MAX], if_suffix[16] = "";
|
2023-03-01 09:21:53 +01:00
|
|
|
struct session *sess;
|
2023-03-02 12:33:40 +01:00
|
|
|
int res, ipv;
|
2023-03-01 10:15:08 +01:00
|
|
|
struct pw_properties *props = NULL;
|
2023-03-01 18:01:26 +01:00
|
|
|
const char *service_name, *str;
|
2023-03-01 21:22:12 +01:00
|
|
|
AvahiStringList *l;
|
|
|
|
|
bool compatible = true;
|
2023-03-01 18:01:26 +01:00
|
|
|
|
|
|
|
|
/* check for compatible session */
|
|
|
|
|
service_name = get_service_name(impl);
|
2023-03-01 21:22:12 +01:00
|
|
|
compatible = spa_streq(service_name, info->type);
|
|
|
|
|
|
2023-03-02 13:53:56 +01:00
|
|
|
props = pw_properties_copy(impl->stream_props);
|
|
|
|
|
if (props == NULL) {
|
|
|
|
|
res = -errno;
|
|
|
|
|
goto error;
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-01 21:22:12 +01:00
|
|
|
if (spa_streq(service_name, "_pipewire-audio._udp")) {
|
|
|
|
|
uint32_t mask = 0;
|
|
|
|
|
for (l = txt; l && compatible; l = l->next) {
|
2023-09-16 17:47:35 +02:00
|
|
|
const char *k = NULL;
|
|
|
|
|
char *key, *value;
|
2023-03-01 21:22:12 +01:00
|
|
|
|
|
|
|
|
if (avahi_string_list_get_pair(l, &key, &value, NULL) != 0)
|
|
|
|
|
break;
|
|
|
|
|
|
2023-03-13 12:48:13 +01:00
|
|
|
if (spa_streq(key, "subtype")) {
|
|
|
|
|
k = "sess.media";
|
2023-03-01 21:22:12 +01:00
|
|
|
mask |= 1<<0;
|
2023-03-13 12:48:13 +01:00
|
|
|
} else if (spa_streq(key, "format")) {
|
|
|
|
|
k = PW_KEY_AUDIO_FORMAT;
|
|
|
|
|
mask |= 1<<1;
|
2023-03-01 21:22:12 +01:00
|
|
|
} else if (spa_streq(key, "rate")) {
|
2023-03-02 13:53:56 +01:00
|
|
|
k = PW_KEY_AUDIO_RATE;
|
2023-03-13 12:48:13 +01:00
|
|
|
mask |= 1<<2;
|
2023-03-01 21:22:12 +01:00
|
|
|
} else if (spa_streq(key, "channels")) {
|
2023-03-02 13:53:56 +01:00
|
|
|
k = PW_KEY_AUDIO_CHANNELS;
|
2023-03-13 12:48:13 +01:00
|
|
|
mask |= 1<<3;
|
2023-03-02 13:53:56 +01:00
|
|
|
} else if (spa_streq(key, "channelnames")) {
|
|
|
|
|
pw_properties_set(props,
|
|
|
|
|
PW_KEY_NODE_CHANNELNAMES, value);
|
|
|
|
|
} else if (spa_streq(key, "ts-refclk")) {
|
|
|
|
|
pw_properties_set(props,
|
|
|
|
|
"sess.ts-refclk", value);
|
2023-03-02 17:18:43 +01:00
|
|
|
if (spa_streq(value, impl->ts_refclk))
|
|
|
|
|
pw_properties_set(props,
|
|
|
|
|
"sess.ts-direct", "true");
|
2023-03-02 13:53:56 +01:00
|
|
|
} else if (spa_streq(key, "ts-offset")) {
|
|
|
|
|
uint32_t v;
|
|
|
|
|
if (spa_atou32(value, &v, 0))
|
|
|
|
|
pw_properties_setf(props,
|
|
|
|
|
"rtp.receiver-ts-offset", "%u", v);
|
2023-03-01 21:22:12 +01:00
|
|
|
}
|
|
|
|
|
if (k != NULL) {
|
2023-03-02 13:53:56 +01:00
|
|
|
str = pw_properties_get(props, k);
|
2023-03-01 21:22:12 +01:00
|
|
|
if (str == NULL || !spa_streq(str, value))
|
|
|
|
|
compatible = false;
|
|
|
|
|
}
|
|
|
|
|
avahi_free(key);
|
|
|
|
|
avahi_free(value);
|
|
|
|
|
}
|
2023-03-13 12:48:13 +01:00
|
|
|
str = pw_properties_get(props, "sess.media");
|
|
|
|
|
if (spa_streq(str, "opus") && mask != 0xd)
|
|
|
|
|
compatible = false;
|
|
|
|
|
if (spa_streq(str, "audio") && mask != 0xf)
|
2023-03-01 21:22:12 +01:00
|
|
|
compatible = false;
|
|
|
|
|
}
|
|
|
|
|
if (!compatible) {
|
|
|
|
|
pw_log_info("found incompatible session IP%d:%s",
|
|
|
|
|
info->protocol == AVAHI_PROTO_INET ? 4 : 6,
|
|
|
|
|
info->name);
|
2023-03-01 18:01:26 +01:00
|
|
|
res = 0;
|
|
|
|
|
goto error;
|
|
|
|
|
}
|
2023-03-01 09:21:53 +01:00
|
|
|
|
|
|
|
|
s = calloc(1, sizeof(*s));
|
2023-03-02 13:53:56 +01:00
|
|
|
if (s == NULL) {
|
|
|
|
|
res = -errno;
|
|
|
|
|
goto error;
|
|
|
|
|
}
|
2023-03-01 09:21:53 +01:00
|
|
|
|
2023-03-01 10:15:08 +01:00
|
|
|
s->impl = impl;
|
|
|
|
|
spa_list_append(&impl->service_list, &s->link);
|
|
|
|
|
|
2023-03-01 09:21:53 +01:00
|
|
|
s->info.interface = info->interface;
|
|
|
|
|
s->info.protocol = info->protocol;
|
|
|
|
|
s->info.name = strdup(info->name);
|
|
|
|
|
s->info.type = strdup(info->type);
|
|
|
|
|
s->info.domain = strdup(info->domain);
|
|
|
|
|
s->info.host_name = strdup(info->host_name);
|
|
|
|
|
s->info.address = info->address;
|
|
|
|
|
s->info.port = info->port;
|
|
|
|
|
|
|
|
|
|
avahi_address_snprint(at, sizeof(at), &s->info.address);
|
2023-03-01 12:57:57 +01:00
|
|
|
pw_log_info("create session: %s %s:%u %s", s->info.name, at, s->info.port, s->info.type);
|
2023-03-01 09:21:53 +01:00
|
|
|
|
2024-05-15 15:39:27 +02:00
|
|
|
if (s->info.protocol == AVAHI_PROTO_INET6 &&
|
|
|
|
|
s->info.address.data.ipv6.address[0] == 0xfe &&
|
|
|
|
|
(s->info.address.data.ipv6.address[1] & 0xc0) == 0x80)
|
|
|
|
|
snprintf(if_suffix, sizeof(if_suffix), "%%%d", s->info.interface);
|
|
|
|
|
|
2023-03-02 12:33:40 +01:00
|
|
|
ipv = s->info.protocol == AVAHI_PROTO_INET ? 4 : 6;
|
2023-03-01 10:15:08 +01:00
|
|
|
pw_properties_set(props, "sess.name", s->info.name);
|
2024-05-15 15:39:27 +02:00
|
|
|
pw_properties_setf(props, "destination.ip", "%s%s", at, if_suffix);
|
|
|
|
|
pw_properties_setf(props, "destination.ifindex", "%u", s->info.interface);
|
2023-03-01 10:15:08 +01:00
|
|
|
pw_properties_setf(props, "destination.port", "%u", s->info.port);
|
2023-03-01 15:21:57 +01:00
|
|
|
|
2023-03-02 13:53:56 +01:00
|
|
|
if (pw_properties_get(props, PW_KEY_NODE_NAME) == NULL)
|
|
|
|
|
pw_properties_setf(props, PW_KEY_NODE_NAME, "rtp_session.%s.%s.ipv%d",
|
|
|
|
|
s->info.name, s->info.host_name, ipv);
|
|
|
|
|
if (pw_properties_get(props, PW_KEY_NODE_DESCRIPTION) == NULL)
|
|
|
|
|
pw_properties_setf(props, PW_KEY_NODE_DESCRIPTION, "%s (IPv%d)",
|
|
|
|
|
s->info.name, ipv);
|
|
|
|
|
if (pw_properties_get(props, PW_KEY_MEDIA_NAME) == NULL)
|
|
|
|
|
pw_properties_setf(props, PW_KEY_MEDIA_NAME, "RTP Session with %s (IPv%d)",
|
|
|
|
|
s->info.name, ipv);
|
|
|
|
|
|
2023-03-01 10:15:08 +01:00
|
|
|
sess = make_session(impl, props);
|
|
|
|
|
props = NULL;
|
2023-03-01 09:21:53 +01:00
|
|
|
if (sess == NULL) {
|
|
|
|
|
res = -errno;
|
|
|
|
|
pw_log_error("can't create session: %m");
|
2023-03-01 15:21:57 +01:00
|
|
|
goto error;
|
2023-03-01 09:21:53 +01:00
|
|
|
}
|
|
|
|
|
s->sess = sess;
|
|
|
|
|
|
2024-02-26 15:17:48 +01:00
|
|
|
if ((res = pw_net_parse_address(at, s->info.port, &sess->ctrl_addr, &sess->ctrl_len)) < 0) {
|
2023-03-01 09:21:53 +01:00
|
|
|
pw_log_error("invalid address %s: %s", at, spa_strerror(res));
|
|
|
|
|
}
|
2024-02-26 15:17:48 +01:00
|
|
|
if ((res = pw_net_parse_address(at, s->info.port+1, &sess->data_addr, &sess->data_len)) < 0) {
|
2023-03-01 09:21:53 +01:00
|
|
|
pw_log_error("invalid address %s: %s", at, spa_strerror(res));
|
|
|
|
|
}
|
|
|
|
|
return s;
|
2023-03-01 10:15:08 +01:00
|
|
|
error:
|
|
|
|
|
pw_properties_free(props);
|
2023-03-01 18:01:26 +01:00
|
|
|
if (s != NULL)
|
|
|
|
|
free_service(s);
|
2023-03-01 10:15:08 +01:00
|
|
|
errno = -res;
|
|
|
|
|
return NULL;
|
2023-03-01 09:21:53 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static struct service *find_service(struct impl *impl, const struct service_info *info)
|
|
|
|
|
{
|
|
|
|
|
struct service *s;
|
|
|
|
|
spa_list_for_each(s, &impl->service_list, link) {
|
|
|
|
|
if (s->info.interface == info->interface &&
|
|
|
|
|
s->info.protocol == info->protocol &&
|
|
|
|
|
spa_streq(s->info.name, info->name) &&
|
|
|
|
|
spa_streq(s->info.type, info->type) &&
|
|
|
|
|
spa_streq(s->info.domain, info->domain))
|
|
|
|
|
return s;
|
|
|
|
|
}
|
|
|
|
|
return NULL;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void resolver_cb(AvahiServiceResolver *r, AvahiIfIndex interface, AvahiProtocol protocol,
|
|
|
|
|
AvahiResolverEvent event, const char *name, const char *type, const char *domain,
|
|
|
|
|
const char *host_name, const AvahiAddress *a, uint16_t port, AvahiStringList *txt,
|
|
|
|
|
AvahiLookupResultFlags flags, void *userdata)
|
|
|
|
|
{
|
|
|
|
|
struct impl *impl = userdata;
|
|
|
|
|
struct service_info sinfo;
|
|
|
|
|
|
|
|
|
|
if (event != AVAHI_RESOLVER_FOUND) {
|
|
|
|
|
pw_log_error("Resolving of '%s' failed: %s", name,
|
|
|
|
|
avahi_strerror(avahi_client_errno(impl->client)));
|
|
|
|
|
goto done;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sinfo = SERVICE_INFO(.interface = interface,
|
|
|
|
|
.protocol = protocol,
|
|
|
|
|
.name = name,
|
|
|
|
|
.type = type,
|
|
|
|
|
.domain = domain,
|
|
|
|
|
.host_name = host_name,
|
|
|
|
|
.address = *a,
|
|
|
|
|
.port = port);
|
|
|
|
|
|
2023-03-01 18:01:26 +01:00
|
|
|
make_service(impl, &sinfo, txt);
|
2023-03-01 09:21:53 +01:00
|
|
|
done:
|
|
|
|
|
avahi_service_resolver_free(r);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void browser_cb(AvahiServiceBrowser *b, AvahiIfIndex interface, AvahiProtocol protocol,
|
|
|
|
|
AvahiBrowserEvent event, const char *name, const char *type, const char *domain,
|
|
|
|
|
AvahiLookupResultFlags flags, void *userdata)
|
|
|
|
|
{
|
|
|
|
|
struct impl *impl = userdata;
|
|
|
|
|
struct service_info info;
|
|
|
|
|
struct service *s;
|
|
|
|
|
|
2024-05-15 15:39:27 +02:00
|
|
|
if ((flags & AVAHI_LOOKUP_RESULT_LOCAL) && !impl->discover_local)
|
2023-03-01 09:21:53 +01:00
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
info = SERVICE_INFO(.interface = interface,
|
|
|
|
|
.protocol = protocol,
|
|
|
|
|
.name = name,
|
|
|
|
|
.type = type,
|
|
|
|
|
.domain = domain);
|
|
|
|
|
|
|
|
|
|
s = find_service(impl, &info);
|
|
|
|
|
|
|
|
|
|
switch (event) {
|
|
|
|
|
case AVAHI_BROWSER_NEW:
|
|
|
|
|
if (s != NULL)
|
|
|
|
|
return;
|
|
|
|
|
if (!(avahi_service_resolver_new(impl->client,
|
|
|
|
|
interface, protocol,
|
|
|
|
|
name, type, domain,
|
|
|
|
|
AVAHI_PROTO_UNSPEC, 0,
|
|
|
|
|
resolver_cb, impl)))
|
|
|
|
|
pw_log_error("can't make service resolver: %s",
|
|
|
|
|
avahi_strerror(avahi_client_errno(impl->client)));
|
|
|
|
|
break;
|
|
|
|
|
case AVAHI_BROWSER_REMOVE:
|
|
|
|
|
if (s == NULL)
|
|
|
|
|
return;
|
|
|
|
|
free_service(s);
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static int make_browser(struct impl *impl)
|
|
|
|
|
{
|
2023-03-01 12:57:57 +01:00
|
|
|
const char *service_name;
|
|
|
|
|
|
2023-03-01 16:21:07 +01:00
|
|
|
service_name = get_service_name(impl);
|
|
|
|
|
if (service_name == NULL)
|
|
|
|
|
return -EINVAL;
|
|
|
|
|
|
2023-03-01 09:21:53 +01:00
|
|
|
if (impl->browser == NULL) {
|
|
|
|
|
impl->browser = avahi_service_browser_new(impl->client,
|
2023-03-01 12:57:57 +01:00
|
|
|
AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC,
|
2023-03-01 16:21:07 +01:00
|
|
|
service_name, NULL, 0,
|
2023-03-01 12:57:57 +01:00
|
|
|
browser_cb, impl);
|
2023-03-01 09:21:53 +01:00
|
|
|
}
|
|
|
|
|
if (impl->browser == NULL) {
|
|
|
|
|
pw_log_error("can't make browser: %s",
|
|
|
|
|
avahi_strerror(avahi_client_errno(impl->client)));
|
|
|
|
|
return -EIO;
|
|
|
|
|
}
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void entry_group_callback(AvahiEntryGroup *g, AvahiEntryGroupState state, void *userdata)
|
|
|
|
|
{
|
|
|
|
|
switch (state) {
|
|
|
|
|
case AVAHI_ENTRY_GROUP_ESTABLISHED:
|
|
|
|
|
pw_log_info("Service successfully established");
|
|
|
|
|
break;
|
|
|
|
|
case AVAHI_ENTRY_GROUP_COLLISION:
|
|
|
|
|
pw_log_error("Service name collision");
|
|
|
|
|
break;
|
|
|
|
|
case AVAHI_ENTRY_GROUP_FAILURE:
|
|
|
|
|
pw_log_error("Entry group failure: %s",
|
|
|
|
|
avahi_strerror(avahi_client_errno(avahi_entry_group_get_client(g))));
|
|
|
|
|
break;
|
|
|
|
|
case AVAHI_ENTRY_GROUP_UNCOMMITED:
|
|
|
|
|
case AVAHI_ENTRY_GROUP_REGISTERING:;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static int make_announce(struct impl *impl)
|
|
|
|
|
{
|
|
|
|
|
int res;
|
2023-03-01 18:01:26 +01:00
|
|
|
const char *service_name, *str;
|
|
|
|
|
AvahiStringList *txt = NULL;
|
2023-03-01 12:57:57 +01:00
|
|
|
|
|
|
|
|
if ((service_name = get_service_name(impl)) == NULL)
|
|
|
|
|
return -ENOTSUP;
|
2023-03-01 09:21:53 +01:00
|
|
|
|
|
|
|
|
if (impl->group == NULL) {
|
|
|
|
|
impl->group = avahi_entry_group_new(impl->client,
|
|
|
|
|
entry_group_callback, impl);
|
|
|
|
|
}
|
|
|
|
|
if (impl->group == NULL) {
|
|
|
|
|
pw_log_error("can't make group: %s",
|
|
|
|
|
avahi_strerror(avahi_client_errno(impl->client)));
|
|
|
|
|
return -EIO;
|
|
|
|
|
}
|
|
|
|
|
avahi_entry_group_reset(impl->group);
|
|
|
|
|
|
2023-03-01 18:01:26 +01:00
|
|
|
if (spa_streq(service_name, "_pipewire-audio._udp")) {
|
2023-03-13 12:48:13 +01:00
|
|
|
str = pw_properties_get(impl->props, "sess.media");
|
|
|
|
|
txt = avahi_string_list_add_pair(txt, "subtype", str);
|
2023-03-02 13:53:56 +01:00
|
|
|
if ((str = pw_properties_get(impl->stream_props, PW_KEY_AUDIO_FORMAT)) != NULL)
|
2023-03-01 21:22:12 +01:00
|
|
|
txt = avahi_string_list_add_pair(txt, "format", str);
|
2023-03-02 13:53:56 +01:00
|
|
|
if ((str = pw_properties_get(impl->stream_props, PW_KEY_AUDIO_RATE)) != NULL)
|
2023-03-01 21:22:12 +01:00
|
|
|
txt = avahi_string_list_add_pair(txt, "rate", str);
|
2023-03-02 13:53:56 +01:00
|
|
|
if ((str = pw_properties_get(impl->stream_props, PW_KEY_AUDIO_CHANNELS)) != NULL)
|
2023-03-01 21:22:12 +01:00
|
|
|
txt = avahi_string_list_add_pair(txt, "channels", str);
|
2023-03-02 13:53:56 +01:00
|
|
|
if ((str = pw_properties_get(impl->stream_props, SPA_KEY_AUDIO_POSITION)) != NULL)
|
2023-03-01 21:22:12 +01:00
|
|
|
txt = avahi_string_list_add_pair(txt, "position", str);
|
2023-03-02 13:53:56 +01:00
|
|
|
if ((str = pw_properties_get(impl->stream_props, PW_KEY_NODE_CHANNELNAMES)) != NULL)
|
|
|
|
|
txt = avahi_string_list_add_pair(txt, "channelnames", str);
|
|
|
|
|
if (impl->ts_refclk != NULL) {
|
|
|
|
|
txt = avahi_string_list_add_pair(txt, "ts-refclk", impl->ts_refclk);
|
|
|
|
|
txt = avahi_string_list_add_printf(txt, "ts-offset=%u", impl->ts_offset);
|
|
|
|
|
}
|
2023-03-01 18:01:26 +01:00
|
|
|
}
|
|
|
|
|
res = avahi_entry_group_add_service_strlst(impl->group,
|
2023-03-01 12:57:57 +01:00
|
|
|
AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC,
|
|
|
|
|
(AvahiPublishFlags)0, impl->session_name,
|
|
|
|
|
service_name, NULL, NULL,
|
2023-03-01 18:01:26 +01:00
|
|
|
impl->ctrl_port, txt);
|
|
|
|
|
|
|
|
|
|
avahi_string_list_free(txt);
|
2023-03-01 12:57:57 +01:00
|
|
|
|
2023-03-01 09:21:53 +01:00
|
|
|
if (res < 0) {
|
|
|
|
|
pw_log_error("can't add service: %s",
|
|
|
|
|
avahi_strerror(avahi_client_errno(impl->client)));
|
|
|
|
|
return -EIO;
|
|
|
|
|
}
|
|
|
|
|
if ((res = avahi_entry_group_commit(impl->group)) < 0) {
|
|
|
|
|
pw_log_error("can't commit group: %s",
|
|
|
|
|
avahi_strerror(avahi_client_errno(impl->client)));
|
|
|
|
|
return -EIO;
|
|
|
|
|
}
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
static void client_callback(AvahiClient *c, AvahiClientState state, void *userdata)
|
|
|
|
|
{
|
|
|
|
|
struct impl *impl = userdata;
|
|
|
|
|
impl->client = c;
|
|
|
|
|
|
|
|
|
|
switch (state) {
|
|
|
|
|
case AVAHI_CLIENT_S_REGISTERING:
|
|
|
|
|
case AVAHI_CLIENT_S_RUNNING:
|
|
|
|
|
case AVAHI_CLIENT_S_COLLISION:
|
|
|
|
|
make_browser(impl);
|
|
|
|
|
make_announce(impl);
|
|
|
|
|
break;
|
|
|
|
|
case AVAHI_CLIENT_FAILURE:
|
|
|
|
|
case AVAHI_CLIENT_CONNECTING:
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-07 09:00:40 +01:00
|
|
|
static void on_timer_event(void *data, uint64_t expirations)
|
|
|
|
|
{
|
|
|
|
|
struct impl *impl = data;
|
|
|
|
|
struct session *sess;
|
|
|
|
|
uint64_t current_time = impl->next_time;
|
|
|
|
|
|
2023-04-14 12:17:43 +02:00
|
|
|
pw_log_debug("timeout");
|
2023-03-07 09:00:40 +01:00
|
|
|
spa_list_for_each(sess, &impl->sessions, link) {
|
|
|
|
|
if (sess->state != SESSION_STATE_ESTABLISHED)
|
|
|
|
|
continue;
|
|
|
|
|
if (sess->next_time < current_time)
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
send_apple_midi_cmd_ck0(sess);
|
|
|
|
|
}
|
|
|
|
|
schedule_timeout(impl);
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-28 16:14:19 +01:00
|
|
|
static void copy_props(struct impl *impl, struct pw_properties *props, const char *key)
|
|
|
|
|
{
|
|
|
|
|
const char *str;
|
|
|
|
|
if ((str = pw_properties_get(props, key)) != NULL) {
|
|
|
|
|
if (pw_properties_get(impl->stream_props, key) == NULL)
|
|
|
|
|
pw_properties_set(impl->stream_props, key, str);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
SPA_EXPORT
|
|
|
|
|
int pipewire__module_init(struct pw_impl_module *module, const char *args)
|
|
|
|
|
{
|
|
|
|
|
struct pw_context *context = pw_impl_module_get_context(module);
|
|
|
|
|
struct impl *impl;
|
|
|
|
|
struct pw_properties *props = NULL, *stream_props = NULL;
|
|
|
|
|
uint16_t port;
|
|
|
|
|
const char *str;
|
2023-03-07 09:00:40 +01:00
|
|
|
struct timespec value, interval;
|
2023-02-28 16:14:19 +01:00
|
|
|
int res = 0;
|
|
|
|
|
|
|
|
|
|
PW_LOG_TOPIC_INIT(mod_topic);
|
|
|
|
|
|
|
|
|
|
impl = calloc(1, sizeof(struct impl));
|
|
|
|
|
if (impl == NULL)
|
|
|
|
|
return -errno;
|
|
|
|
|
|
|
|
|
|
if (args == NULL)
|
|
|
|
|
args = "";
|
|
|
|
|
|
|
|
|
|
spa_list_init(&impl->sessions);
|
2023-03-01 09:21:53 +01:00
|
|
|
spa_list_init(&impl->service_list);
|
2023-02-28 16:14:19 +01:00
|
|
|
|
|
|
|
|
props = pw_properties_new_string(args);
|
|
|
|
|
if (props == NULL) {
|
|
|
|
|
res = -errno;
|
|
|
|
|
pw_log_error( "can't create properties: %m");
|
|
|
|
|
goto out;
|
|
|
|
|
}
|
|
|
|
|
impl->props = props;
|
|
|
|
|
|
2024-05-15 15:39:27 +02:00
|
|
|
impl->discover_local = pw_properties_get_bool(impl->props,
|
|
|
|
|
"sess.discover-local", false);
|
|
|
|
|
|
2023-02-28 16:14:19 +01:00
|
|
|
stream_props = pw_properties_new(NULL, NULL);
|
|
|
|
|
if (stream_props == NULL) {
|
|
|
|
|
res = -errno;
|
|
|
|
|
pw_log_error( "can't create properties: %m");
|
|
|
|
|
goto out;
|
|
|
|
|
}
|
|
|
|
|
impl->stream_props = stream_props;
|
|
|
|
|
|
|
|
|
|
impl->module = module;
|
|
|
|
|
impl->context = context;
|
|
|
|
|
impl->loop = pw_context_get_main_loop(context);
|
2024-04-22 16:19:02 +02:00
|
|
|
impl->data_loop = pw_context_acquire_loop(context, &props->dict);
|
|
|
|
|
|
|
|
|
|
pw_properties_set(props, PW_KEY_NODE_LOOP_NAME, impl->data_loop->name);
|
2023-02-28 16:14:19 +01:00
|
|
|
|
2023-05-15 12:23:28 +02:00
|
|
|
if (pw_properties_get(props, "sess.media") == NULL)
|
|
|
|
|
pw_properties_set(props, "sess.media", "midi");
|
|
|
|
|
|
2023-02-28 16:14:19 +01:00
|
|
|
if ((str = pw_properties_get(props, "stream.props")) != NULL)
|
|
|
|
|
pw_properties_update_string(stream_props, str, strlen(str));
|
|
|
|
|
|
2024-04-22 16:19:02 +02:00
|
|
|
copy_props(impl, props, PW_KEY_NODE_LOOP_NAME);
|
2023-02-28 16:14:19 +01:00
|
|
|
copy_props(impl, props, PW_KEY_AUDIO_FORMAT);
|
|
|
|
|
copy_props(impl, props, PW_KEY_AUDIO_RATE);
|
|
|
|
|
copy_props(impl, props, PW_KEY_AUDIO_CHANNELS);
|
|
|
|
|
copy_props(impl, props, SPA_KEY_AUDIO_POSITION);
|
|
|
|
|
copy_props(impl, props, PW_KEY_NODE_NAME);
|
|
|
|
|
copy_props(impl, props, PW_KEY_NODE_DESCRIPTION);
|
|
|
|
|
copy_props(impl, props, PW_KEY_NODE_GROUP);
|
|
|
|
|
copy_props(impl, props, PW_KEY_NODE_LATENCY);
|
|
|
|
|
copy_props(impl, props, PW_KEY_NODE_VIRTUAL);
|
|
|
|
|
copy_props(impl, props, PW_KEY_NODE_CHANNELNAMES);
|
|
|
|
|
copy_props(impl, props, PW_KEY_MEDIA_NAME);
|
|
|
|
|
copy_props(impl, props, PW_KEY_MEDIA_CLASS);
|
2023-03-02 17:18:43 +01:00
|
|
|
copy_props(impl, props, "net.mtu");
|
2023-03-03 12:04:50 +01:00
|
|
|
copy_props(impl, props, "sess.media");
|
2023-03-02 17:18:43 +01:00
|
|
|
copy_props(impl, props, "sess.min-ptime");
|
|
|
|
|
copy_props(impl, props, "sess.max-ptime");
|
|
|
|
|
copy_props(impl, props, "sess.latency.msec");
|
2023-03-02 19:55:44 +01:00
|
|
|
copy_props(impl, props, "sess.ts-refclk");
|
2023-03-02 17:18:43 +01:00
|
|
|
|
|
|
|
|
impl->ttl = pw_properties_get_uint32(props, "net.ttl", DEFAULT_TTL);
|
|
|
|
|
impl->mcast_loop = pw_properties_get_bool(props, "net.loop", DEFAULT_LOOP);
|
2023-02-28 16:14:19 +01:00
|
|
|
|
2023-05-15 12:34:56 +02:00
|
|
|
str = pw_properties_get(stream_props, "sess.media");
|
|
|
|
|
|
2023-03-01 21:22:12 +01:00
|
|
|
if (spa_streq(str, "audio")) {
|
|
|
|
|
struct spa_dict_item items[] = {
|
|
|
|
|
{ "audio.format", DEFAULT_FORMAT },
|
2023-03-02 19:55:44 +01:00
|
|
|
{ "audio.rate", SPA_STRINGIFY(DEFAULT_RATE) },
|
|
|
|
|
{ "audio.channels", SPA_STRINGIFY(DEFAULT_CHANNELS) },
|
2023-03-01 21:22:12 +01:00
|
|
|
{ "audio.position", DEFAULT_POSITION } };
|
|
|
|
|
pw_properties_add(stream_props, &SPA_DICT_INIT_ARRAY(items));
|
|
|
|
|
}
|
2023-03-13 12:48:13 +01:00
|
|
|
else if (spa_streq(str, "opus")) {
|
|
|
|
|
struct spa_dict_item items[] = {
|
|
|
|
|
{ "audio.rate", SPA_STRINGIFY(DEFAULT_RATE) },
|
|
|
|
|
{ "audio.channels", SPA_STRINGIFY(DEFAULT_CHANNELS) },
|
|
|
|
|
{ "audio.position", DEFAULT_POSITION } };
|
|
|
|
|
pw_properties_add(stream_props, &SPA_DICT_INIT_ARRAY(items));
|
|
|
|
|
}
|
2023-03-01 21:22:12 +01:00
|
|
|
|
2023-02-28 16:14:19 +01:00
|
|
|
str = pw_properties_get(props, "local.ifname");
|
|
|
|
|
impl->ifname = str ? strdup(str) : NULL;
|
|
|
|
|
|
2023-03-01 09:21:53 +01:00
|
|
|
port = pw_properties_get_uint32(props, "control.port", DEFAULT_CONTROL_PORT);
|
2023-02-28 16:14:19 +01:00
|
|
|
if ((str = pw_properties_get(props, "control.ip")) == NULL)
|
2023-03-01 09:21:53 +01:00
|
|
|
str = DEFAULT_CONTROL_IP;
|
|
|
|
|
|
|
|
|
|
impl->ctrl_port = port;
|
2023-02-28 16:14:19 +01:00
|
|
|
|
2024-02-26 15:17:48 +01:00
|
|
|
if ((res = pw_net_parse_address(str, port, &impl->ctrl_addr, &impl->ctrl_len)) < 0) {
|
2023-02-28 16:14:19 +01:00
|
|
|
pw_log_error("invalid control.ip %s: %s", str, spa_strerror(res));
|
|
|
|
|
goto out;
|
|
|
|
|
}
|
2024-02-26 15:17:48 +01:00
|
|
|
if ((res = pw_net_parse_address(str, port ? port+1 : 0, &impl->data_addr, &impl->data_len)) < 0) {
|
2023-02-28 16:14:19 +01:00
|
|
|
pw_log_error("invalid data.ip %s: %s", str, spa_strerror(res));
|
|
|
|
|
goto out;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl->ts_offset = pw_properties_get_int64(props,
|
2023-03-02 13:53:56 +01:00
|
|
|
"sess.ts-offset", pw_rand32());
|
2023-02-28 16:14:19 +01:00
|
|
|
str = pw_properties_get(props, "sess.ts-refclk");
|
|
|
|
|
impl->ts_refclk = str ? strdup(str) : NULL;
|
|
|
|
|
|
|
|
|
|
if ((str = pw_properties_get(props, "sess.name")) == NULL)
|
2023-03-02 13:53:56 +01:00
|
|
|
pw_properties_setf(props, "sess.name", "%s", pw_get_host_name());
|
2023-02-28 16:14:19 +01:00
|
|
|
str = pw_properties_get(props, "sess.name");
|
|
|
|
|
impl->session_name = str ? strdup(str) : NULL;
|
|
|
|
|
|
|
|
|
|
impl->core = pw_context_get_object(impl->context, PW_TYPE_INTERFACE_Core);
|
|
|
|
|
if (impl->core == NULL) {
|
|
|
|
|
str = pw_properties_get(props, PW_KEY_REMOTE_NAME);
|
|
|
|
|
impl->core = pw_context_connect(impl->context,
|
|
|
|
|
pw_properties_new(
|
|
|
|
|
PW_KEY_REMOTE_NAME, str,
|
|
|
|
|
NULL),
|
|
|
|
|
0);
|
|
|
|
|
impl->do_disconnect = true;
|
|
|
|
|
}
|
|
|
|
|
if (impl->core == NULL) {
|
|
|
|
|
res = -errno;
|
|
|
|
|
pw_log_error("can't connect: %m");
|
|
|
|
|
goto out;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pw_proxy_add_listener((struct pw_proxy*)impl->core,
|
|
|
|
|
&impl->core_proxy_listener,
|
|
|
|
|
&core_proxy_events, impl);
|
|
|
|
|
pw_core_add_listener(impl->core,
|
|
|
|
|
&impl->core_listener,
|
|
|
|
|
&core_events, impl);
|
|
|
|
|
|
2023-03-07 09:00:40 +01:00
|
|
|
impl->timer = pw_loop_add_timer(impl->loop, on_timer_event, impl);
|
|
|
|
|
if (impl->timer == NULL) {
|
|
|
|
|
res = -errno;
|
|
|
|
|
pw_log_error("can't create timer source: %m");
|
|
|
|
|
goto out;
|
|
|
|
|
}
|
|
|
|
|
value.tv_sec = 0;
|
|
|
|
|
value.tv_nsec = 1;
|
|
|
|
|
interval.tv_sec = 1;
|
|
|
|
|
interval.tv_nsec = 0;
|
|
|
|
|
pw_loop_update_timer(impl->loop, impl->timer, &value, &interval, false);
|
|
|
|
|
|
2023-02-28 16:14:19 +01:00
|
|
|
if ((res = setup_apple_session(impl)) < 0)
|
|
|
|
|
goto out;
|
|
|
|
|
|
2025-09-26 13:39:49 +02:00
|
|
|
impl->avahi_poll = pw_avahi_poll_new(impl->context);
|
2023-03-01 09:21:53 +01:00
|
|
|
if ((impl->client = avahi_client_new(impl->avahi_poll,
|
|
|
|
|
AVAHI_CLIENT_NO_FAIL,
|
|
|
|
|
client_callback, impl,
|
|
|
|
|
&res)) == NULL) {
|
|
|
|
|
pw_log_error("can't create avahi client: %s", avahi_strerror(res));
|
|
|
|
|
goto out;
|
|
|
|
|
}
|
2023-02-28 16:14:19 +01:00
|
|
|
|
|
|
|
|
pw_impl_module_add_listener(module, &impl->module_listener, &module_events, impl);
|
|
|
|
|
|
|
|
|
|
pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_info));
|
|
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
|
out:
|
|
|
|
|
impl_destroy(impl);
|
|
|
|
|
return res;
|
|
|
|
|
}
|