mirror of
https://gitlab.freedesktop.org/pipewire/pipewire.git
synced 2025-11-14 06:59:57 -05:00
Add IGMP recovery mechanism that monitors SAP packet reception and triggers multicast group refresh when no packets are received if a deadline is reached. The deadline is set to half of the cleanup interval, with a minimum of 1 second. When the deadline is reached, the mechanism performs IGMP leave/rejoin operations to refresh multicast group membership. This ensures SAP announcements continue to be received when network conditions cause IGMP membership to expire or become stale due to router timeouts or network issues.
2188 lines
60 KiB
C
2188 lines
60 KiB
C
/* 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/cleanup.h>
|
|
#include <spa/utils/hook.h>
|
|
#include <spa/utils/result.h>
|
|
#include <spa/utils/json.h>
|
|
#include <spa/debug/types.h>
|
|
|
|
#include <pipewire/pipewire.h>
|
|
#include <pipewire/impl.h>
|
|
|
|
#include <module-rtp/sap.h>
|
|
#include <module-rtp/ptp.h>
|
|
#include "network-utils.h"
|
|
|
|
#ifdef __FreeBSD__
|
|
#define SO_PASSCRED LOCAL_CREDS_PERSISTENT
|
|
#endif
|
|
|
|
/** \page page_module_rtp_sap SAP Announce and create RTP streams
|
|
*
|
|
* The `rtp-sap` module announces RTP streams that match the rules with the
|
|
* announce-stream action.
|
|
*
|
|
* It will create source RTP streams that are announced with SAP when they
|
|
* match the rule with the create-stream action.
|
|
*
|
|
* If no stream.rules are given, it will announce all streams with
|
|
* sess.sap.announce = true and it will create a receiver for all announced
|
|
* streams.
|
|
*
|
|
* ## Module Name
|
|
*
|
|
* `libpipewire-module-rtp-sap`
|
|
*
|
|
* ## Module Options
|
|
*
|
|
* Options specific to the behavior of this module
|
|
*
|
|
* - `local.ifname = <str>`: interface name to use
|
|
* - `sap.ip = <str>`: IP address of the SAP messages, default "224.0.0.56"
|
|
* - `sap.port = <int>`: port of the SAP messages, default 9875
|
|
* - `sap.cleanup.sec = <int>`: cleanup interval in seconds, default 90 seconds
|
|
* - `source.ip =<str>`: source IP address, default "0.0.0.0"
|
|
* - `net.ttl = <int>`: TTL to use, default 1
|
|
* - `net.loop = <bool>`: loopback multicast, default false
|
|
* - `stream.rules` = <rules>: match rules, use create-stream and announce-stream actions
|
|
* - `sap.max-sessions = <int>`: maximum number of concurrent send/receive sessions to track
|
|
* - `sap.preamble-extra = [strings]`: extra attributes to add to the atomic SDP preamble
|
|
* - `sap.end-extra = [strings]`: extra attributes to add to the end of the SDP message
|
|
*
|
|
* ## General options
|
|
*
|
|
* Options with well-known behavior:
|
|
*
|
|
* - \ref PW_KEY_REMOTE_NAME
|
|
*
|
|
* ## Example configuration
|
|
*\code{.unparsed}
|
|
* # ~/.config/pipewire/pipewire.conf.d/my-rtp-sap.conf
|
|
*
|
|
* context.modules = [
|
|
* { name = libpipewire-module-rtp-sap
|
|
* args = {
|
|
* #local.ifname = "eth0"
|
|
* #sap.ip = "224.0.0.56"
|
|
* #sap.port = 9875
|
|
* #sap.cleanup.sec = 5
|
|
* #source.ip = "0.0.0.0"
|
|
* #net.ttl = 1
|
|
* #net.loop = false
|
|
* stream.rules = [
|
|
* { matches = [
|
|
* # any of the items in matches needs to match, if one does,
|
|
* # actions are emitted.
|
|
* { # all keys must match the value. ! negates. ~ starts regex.
|
|
* #rtp.origin = "wim 3883629975 0 IN IP4 0.0.0.0"
|
|
* #rtp.payload = "127"
|
|
* #rtp.fmt = "L16/48000/2"
|
|
* #rtp.session = "PipeWire RTP Stream on fedora"
|
|
* #rtp.ts-offset = 0
|
|
* #rtp.ts-refclk = "private"
|
|
* sess.sap.announce = true
|
|
* }
|
|
* ]
|
|
* actions = {
|
|
* announce-stream = {
|
|
* }
|
|
* }
|
|
* }
|
|
* { matches = [
|
|
* { # all keys must match the value. ! negates. ~ starts regex.
|
|
* #rtp.origin = "wim 3883629975 0 IN IP4 0.0.0.0"
|
|
* #rtp.payload = "127"
|
|
* #rtp.fmt = "L16/48000/2"
|
|
* #rtp.session = "PipeWire RTP Stream on fedora"
|
|
* #rtp.ts-offset = 0
|
|
* #rtp.ts-refclk = "private"
|
|
* rtp.session = "~.*"
|
|
* }
|
|
* ]
|
|
* actions = {
|
|
* create-stream = {
|
|
* #sess.latency.msec = 100
|
|
* #sess.ts-direct = false
|
|
* #target.object = ""
|
|
* }
|
|
* }
|
|
* }
|
|
* ]
|
|
* }
|
|
* }
|
|
* ]
|
|
*\endcode
|
|
*
|
|
* \since 0.3.67
|
|
*/
|
|
|
|
#define NAME "rtp-sap"
|
|
|
|
PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
|
|
#define PW_LOG_TOPIC_DEFAULT mod_topic
|
|
|
|
#define DEFAULT_MAX_SESSIONS 64
|
|
|
|
#define DEFAULT_ANNOUNCE_RULES \
|
|
"[ { matches = [ { sess.sap.announce = true } ] actions = { announce-stream = { } } } ]"
|
|
#define DEFAULT_CREATE_RULES \
|
|
"[ { matches = [ { rtp.session = \"~.*\" } ] actions = { create-stream = { } } } ] "
|
|
|
|
#define DEFAULT_CLEANUP_SEC 90
|
|
#define SAP_INTERVAL_SEC 5
|
|
#define SAP_MIME_TYPE "application/sdp"
|
|
|
|
#define DEFAULT_SAP_IP "224.0.0.56"
|
|
#define DEFAULT_SAP_PORT 9875
|
|
|
|
#define DEFAULT_SOURCE_IP "0.0.0.0"
|
|
#define DEFAULT_SOURCE_IP6 "::"
|
|
#define DEFAULT_TTL 1
|
|
#define DEFAULT_LOOP false
|
|
|
|
#define MAX_SDP 2048
|
|
#define MAX_CHANNELS SPA_AUDIO_MAX_CHANNELS
|
|
|
|
#define USAGE "( local.ifname=<local interface name to use> ) " \
|
|
"( sap.ip=<SAP IP address to send announce, default:"DEFAULT_SAP_IP"> ) " \
|
|
"( sap.port=<SAP port to send on, default:"SPA_STRINGIFY(DEFAULT_SAP_PORT)"> ) " \
|
|
"( sap.cleanup.sec=<cleanup interval in seconds, default 90> ) " \
|
|
"( source.ip=<source IP address, default:"DEFAULT_SOURCE_IP"> ) " \
|
|
"( net.ttl=<desired TTL, default:"SPA_STRINGIFY(DEFAULT_TTL)"> ) " \
|
|
"( net.loop=<desired loopback, default:"SPA_STRINGIFY(DEFAULT_LOOP)"> ) " \
|
|
"( stream.rules=<rules>, use announce-stream and create-stream actions )"
|
|
|
|
static const struct spa_dict_item module_info[] = {
|
|
{ PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
|
|
{ PW_KEY_MODULE_DESCRIPTION, "RTP SAP announce/listen" },
|
|
{ PW_KEY_MODULE_USAGE, USAGE },
|
|
{ PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
|
|
};
|
|
|
|
#define PTP_MESSAGE_TYPE_MANAGEMENT 0x0d
|
|
#define PTP_VERSION_1588_2008_2_1 0x12
|
|
#define PTP_DEFAULT_LOG_MESSAGE_INTERVAL 127
|
|
#define PTP_MGMT_ACTION_GET 0
|
|
#define PTP_MGMT_ACTION_RESPONSE 2
|
|
#define PTP_TLV_TYPE_MGMT 0x0001
|
|
#define PTP_MGMT_ID_PARENT_DATA_SET 0x2002
|
|
|
|
struct sdp_info {
|
|
uint16_t hash;
|
|
uint32_t session_id;
|
|
uint32_t session_version;
|
|
uint32_t t_ntp;
|
|
|
|
char *origin;
|
|
char *session_name;
|
|
char *media_type;
|
|
char *mime_type;
|
|
char channelmap[512];
|
|
|
|
uint16_t dst_port;
|
|
struct sockaddr_storage dst_addr;
|
|
socklen_t dst_len;
|
|
uint16_t ttl;
|
|
|
|
uint16_t port;
|
|
uint8_t payload;
|
|
|
|
uint32_t rate;
|
|
uint32_t channels;
|
|
|
|
float ptime;
|
|
uint32_t framecount;
|
|
|
|
uint32_t ssrc;
|
|
uint32_t ts_offset;
|
|
char *ts_refclk;
|
|
};
|
|
|
|
struct session {
|
|
struct spa_list link;
|
|
|
|
bool announce;
|
|
uint64_t timestamp;
|
|
bool ts_refclk_ptp;
|
|
|
|
struct impl *impl;
|
|
struct node *node;
|
|
|
|
struct sdp_info info;
|
|
|
|
unsigned has_sent_sap:1;
|
|
unsigned has_sdp:1;
|
|
char sdp[MAX_SDP];
|
|
|
|
struct pw_properties *props;
|
|
|
|
struct pw_impl_module *module;
|
|
struct spa_hook module_listener;
|
|
};
|
|
|
|
struct node {
|
|
struct impl *impl;
|
|
|
|
uint32_t id;
|
|
|
|
struct pw_proxy *proxy;
|
|
struct spa_hook proxy_listener;
|
|
struct spa_hook node_listener;
|
|
|
|
struct pw_node_info *info;
|
|
struct session *session;
|
|
};
|
|
|
|
struct igmp_recovery {
|
|
struct pw_timer timer;
|
|
int socket_fd;
|
|
struct sockaddr_storage mcast_addr;
|
|
socklen_t mcast_len;
|
|
uint32_t if_index;
|
|
bool is_ipv6;
|
|
uint32_t deadline;
|
|
};
|
|
|
|
struct impl {
|
|
struct pw_properties *props;
|
|
|
|
struct pw_loop *loop;
|
|
struct pw_timer_queue *timer_queue;
|
|
|
|
struct pw_impl_module *module;
|
|
struct spa_hook module_listener;
|
|
|
|
struct pw_core *core;
|
|
struct spa_hook core_listener;
|
|
struct spa_hook core_proxy_listener;
|
|
unsigned int do_disconnect:1;
|
|
|
|
struct pw_registry *registry;
|
|
struct spa_hook registry_listener;
|
|
|
|
struct pw_timer sap_send_timer;
|
|
|
|
/* This timer is used when the first start_sap() call fails because
|
|
* of an ENODEV error (see the start_sap() code for details) */
|
|
struct pw_timer start_sap_retry_timer;
|
|
|
|
char *ifname;
|
|
uint32_t ttl;
|
|
bool mcast_loop;
|
|
|
|
struct sockaddr_storage src_addr;
|
|
socklen_t src_len;
|
|
|
|
uint16_t sap_port;
|
|
struct sockaddr_storage sap_addr;
|
|
socklen_t sap_len;
|
|
int sap_fd;
|
|
struct spa_source *sap_source;
|
|
uint32_t cleanup_interval;
|
|
|
|
/* IGMP recovery (triggers when no SAP packets are
|
|
* received after the recovery deadline is reached) */
|
|
struct igmp_recovery igmp_recovery;
|
|
|
|
uint32_t max_sessions;
|
|
uint32_t n_sessions;
|
|
struct spa_list sessions;
|
|
|
|
char *extra_attrs_preamble;
|
|
char *extra_attrs_end;
|
|
|
|
char *ptp_mgmt_socket_path;
|
|
int ptp_fd;
|
|
uint32_t ptp_seq;
|
|
uint8_t clock_id[8];
|
|
uint8_t gm_id[8];
|
|
};
|
|
|
|
struct format_info {
|
|
uint32_t media_subtype;
|
|
uint32_t format;
|
|
uint32_t size;
|
|
const char *mime;
|
|
const char *media_type;
|
|
const char *format_str;
|
|
};
|
|
|
|
static const struct format_info audio_format_info[] = {
|
|
{ SPA_MEDIA_SUBTYPE_raw, SPA_AUDIO_FORMAT_U8, 1, "L8", "audio", "U8" },
|
|
{ SPA_MEDIA_SUBTYPE_raw, SPA_AUDIO_FORMAT_ALAW, 1, "PCMA", "audio", "ALAW" },
|
|
{ SPA_MEDIA_SUBTYPE_raw, SPA_AUDIO_FORMAT_ULAW, 1, "PCMU", "audio", "ULAW" },
|
|
{ SPA_MEDIA_SUBTYPE_raw, SPA_AUDIO_FORMAT_S16_BE, 2, "L16", "audio", "S16BE" },
|
|
{ SPA_MEDIA_SUBTYPE_raw, SPA_AUDIO_FORMAT_S24_BE, 3, "L24", "audio", "S24BE" },
|
|
{ SPA_MEDIA_SUBTYPE_control, 0, 1, "rtp-midi", "midi", NULL },
|
|
{ SPA_MEDIA_SUBTYPE_opus, 0, 1, "opus", "opus", NULL },
|
|
};
|
|
|
|
static const struct format_info *find_audio_format_info(const char *mime)
|
|
{
|
|
SPA_FOR_EACH_ELEMENT_VAR(audio_format_info, f)
|
|
if (spa_streq(f->mime, mime))
|
|
return f;
|
|
return NULL;
|
|
}
|
|
|
|
static int start_sap(struct impl *impl);
|
|
static int send_sap(struct impl *impl, struct session *sess, bool bye);
|
|
|
|
|
|
static void clear_sdp_info(struct sdp_info *info)
|
|
{
|
|
free(info->origin);
|
|
free(info->session_name);
|
|
free(info->media_type);
|
|
free(info->mime_type);
|
|
free(info->ts_refclk);
|
|
spa_zero(*info);
|
|
}
|
|
|
|
static uint64_t get_time_nsec(struct impl *impl)
|
|
{
|
|
struct timespec ts;
|
|
clock_gettime(CLOCK_MONOTONIC, &ts);
|
|
return SPA_TIMESPEC_TO_NSEC(&ts);
|
|
}
|
|
|
|
static void session_touch(struct session *sess)
|
|
{
|
|
sess->timestamp = get_time_nsec(sess->impl);
|
|
}
|
|
|
|
static void session_free(struct session *sess)
|
|
{
|
|
struct impl *impl = sess->impl;
|
|
|
|
if (sess->impl) {
|
|
if (sess->announce)
|
|
send_sap(impl, sess, 1);
|
|
spa_list_remove(&sess->link);
|
|
impl->n_sessions--;
|
|
}
|
|
if (sess->node && sess->node->session != NULL)
|
|
sess->node->session = NULL;
|
|
|
|
if (sess->module) {
|
|
spa_hook_remove(&sess->module_listener);
|
|
pw_impl_module_destroy(sess->module);
|
|
}
|
|
|
|
pw_properties_free(sess->props);
|
|
clear_sdp_info(&sess->info);
|
|
free(sess);
|
|
}
|
|
|
|
static bool is_multicast(struct sockaddr *sa, socklen_t salen)
|
|
{
|
|
if (sa->sa_family == AF_INET) {
|
|
static const uint32_t ipv4_mcast_mask = 0xe0000000;
|
|
struct sockaddr_in *sa4 = (struct sockaddr_in*)sa;
|
|
return (ntohl(sa4->sin_addr.s_addr) & ipv4_mcast_mask) == ipv4_mcast_mask;
|
|
} else if (sa->sa_family == AF_INET6) {
|
|
struct sockaddr_in6 *sa6 = (struct sockaddr_in6*)sa;
|
|
return sa6->sin6_addr.s6_addr[0] == 0xff;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static int make_unix_ptp_mgmt_socket(const char *path) {
|
|
struct sockaddr_un addr;
|
|
|
|
spa_autoclose int fd = socket(AF_UNIX, SOCK_DGRAM | SOCK_CLOEXEC, 0);
|
|
if (fd < 0) {
|
|
pw_log_warn("Failed to create PTP management socket");
|
|
return -1;
|
|
}
|
|
|
|
int val = 1;
|
|
if (setsockopt(fd, SOL_SOCKET, SO_PASSCRED, &val, sizeof(val)) < 0) {
|
|
pw_log_warn("Failed to bind PTP management socket");
|
|
return -1;
|
|
}
|
|
|
|
spa_zero(addr);
|
|
addr.sun_family = AF_UNIX;
|
|
strncpy(addr.sun_path, path, sizeof(addr.sun_path) - 1);
|
|
|
|
if (connect(fd, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
|
|
pw_log_warn("Failed to connect PTP management socket");
|
|
return -1;
|
|
}
|
|
|
|
return spa_steal_fd(fd);
|
|
}
|
|
|
|
static int make_send_socket(
|
|
struct sockaddr_storage *src, socklen_t src_len,
|
|
struct sockaddr_storage *sa, socklen_t salen,
|
|
bool loop, int ttl)
|
|
{
|
|
int af, fd, val, res;
|
|
|
|
af = src->ss_family;
|
|
if ((fd = socket(af, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0)) < 0) {
|
|
pw_log_error("socket() failed: %m");
|
|
return -errno;
|
|
}
|
|
if (bind(fd, (struct sockaddr*)src, src_len) < 0) {
|
|
res = -errno;
|
|
pw_log_error("bind() failed: %m");
|
|
goto error;
|
|
}
|
|
if (connect(fd, (struct sockaddr*)sa, salen) < 0) {
|
|
res = -errno;
|
|
pw_log_error("connect() failed: %m");
|
|
goto error;
|
|
}
|
|
if (is_multicast((struct sockaddr*)sa, salen)) {
|
|
if (sa->ss_family == AF_INET) {
|
|
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");
|
|
} else {
|
|
val = loop;
|
|
if (setsockopt(fd, IPPROTO_IPV6, IPV6_MULTICAST_LOOP, &val, sizeof(val)) < 0)
|
|
pw_log_warn("setsockopt(IPV6_MULTICAST_LOOP) failed: %m");
|
|
|
|
val = ttl;
|
|
if (setsockopt(fd, IPPROTO_IPV6, IPV6_MULTICAST_HOPS, &val, sizeof(val)) < 0)
|
|
pw_log_warn("setsockopt(IPV6_MULTICAST_HOPS) failed: %m");
|
|
}
|
|
}
|
|
|
|
pw_log_info("sender socket up and running");
|
|
|
|
return fd;
|
|
error:
|
|
close(fd);
|
|
return res;
|
|
}
|
|
|
|
static int make_recv_socket(struct sockaddr_storage *sa, socklen_t salen,
|
|
char *ifname, struct igmp_recovery *igmp_recovery)
|
|
{
|
|
int af, fd, val, res;
|
|
struct ifreq req;
|
|
struct sockaddr_storage ba = *sa;
|
|
bool do_connect = false;
|
|
char addr[128];
|
|
|
|
af = sa->ss_family;
|
|
if ((fd = socket(af, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0)) < 0) {
|
|
pw_log_error("socket() failed: %m");
|
|
return -errno;
|
|
}
|
|
val = 1;
|
|
if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(val)) < 0) {
|
|
res = -errno;
|
|
pw_log_error("setsockopt() failed: %m");
|
|
goto error;
|
|
}
|
|
spa_zero(req);
|
|
if (ifname) {
|
|
snprintf(req.ifr_name, sizeof(req.ifr_name), "%s", ifname);
|
|
res = ioctl(fd, SIOCGIFINDEX, &req);
|
|
if (res < 0)
|
|
pw_log_warn("SIOCGIFINDEX %s failed: %m", ifname);
|
|
}
|
|
res = 0;
|
|
if (af == AF_INET) {
|
|
static const uint32_t ipv4_mcast_mask = 0xe0000000;
|
|
struct sockaddr_in *sa4 = (struct sockaddr_in*)sa;
|
|
if ((ntohl(sa4->sin_addr.s_addr) & ipv4_mcast_mask) == ipv4_mcast_mask) {
|
|
struct ip_mreqn mr4;
|
|
memset(&mr4, 0, sizeof(mr4));
|
|
mr4.imr_multiaddr = sa4->sin_addr;
|
|
mr4.imr_ifindex = req.ifr_ifindex;
|
|
pw_net_get_ip(sa, addr, sizeof(addr), NULL, NULL);
|
|
pw_log_info("join IPv4 group: %s iface:%d", addr, req.ifr_ifindex);
|
|
res = setsockopt(fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mr4, sizeof(mr4));
|
|
} else {
|
|
struct sockaddr_in *ba4 = (struct sockaddr_in*)&ba;
|
|
if (ba4->sin_addr.s_addr != INADDR_ANY) {
|
|
ba4->sin_addr.s_addr = INADDR_ANY;
|
|
do_connect = true;
|
|
}
|
|
}
|
|
} else if (af == AF_INET6) {
|
|
struct sockaddr_in6 *sa6 = (struct sockaddr_in6*)sa;
|
|
if (sa6->sin6_addr.s6_addr[0] == 0xff) {
|
|
struct ipv6_mreq mr6;
|
|
memset(&mr6, 0, sizeof(mr6));
|
|
mr6.ipv6mr_multiaddr = sa6->sin6_addr;
|
|
mr6.ipv6mr_interface = req.ifr_ifindex;
|
|
pw_net_get_ip(sa, addr, sizeof(addr), NULL, NULL);
|
|
pw_log_info("join IPv6 group: %s iface:%d", addr, req.ifr_ifindex);
|
|
res = setsockopt(fd, IPPROTO_IPV6, IPV6_JOIN_GROUP, &mr6, sizeof(mr6));
|
|
} else {
|
|
struct sockaddr_in6 *ba6 = (struct sockaddr_in6*)&ba;
|
|
ba6->sin6_addr = in6addr_any;
|
|
}
|
|
} else {
|
|
res = -EINVAL;
|
|
goto error;
|
|
}
|
|
|
|
if (res < 0) {
|
|
res = -errno;
|
|
pw_log_error("join mcast failed: %m");
|
|
goto error;
|
|
}
|
|
|
|
/* Store multicast info for recovery */
|
|
igmp_recovery->socket_fd = fd;
|
|
igmp_recovery->mcast_addr = ba;
|
|
igmp_recovery->mcast_len = salen;
|
|
igmp_recovery->if_index = req.ifr_ifindex;
|
|
igmp_recovery->is_ipv6 = (af == AF_INET6);
|
|
pw_log_debug("stored %s multicast info: socket_fd=%d, "
|
|
"if_index=%d", igmp_recovery->is_ipv6 ?
|
|
"IPv6" : "IPv4", fd, req.ifr_ifindex);
|
|
|
|
if (bind(fd, (struct sockaddr*)&ba, salen) < 0) {
|
|
res = -errno;
|
|
pw_log_error("bind() failed: %m");
|
|
goto error;
|
|
}
|
|
if (do_connect) {
|
|
if (connect(fd, (struct sockaddr*)sa, salen) < 0) {
|
|
res = -errno;
|
|
pw_log_error("connect() failed: %m");
|
|
goto error;
|
|
}
|
|
}
|
|
|
|
pw_log_info("receiver socket up and running");
|
|
|
|
return fd;
|
|
error:
|
|
close(fd);
|
|
return res;
|
|
}
|
|
|
|
static bool update_ts_refclk(struct impl *impl)
|
|
{
|
|
if (!impl->ptp_mgmt_socket_path)
|
|
return false;
|
|
if (impl->ptp_fd < 0) {
|
|
impl->ptp_fd = make_unix_ptp_mgmt_socket(impl->ptp_mgmt_socket_path);
|
|
if (impl->ptp_fd < 0)
|
|
return false;
|
|
}
|
|
|
|
// Read if something is left in the socket
|
|
int avail;
|
|
uint8_t tmp;
|
|
|
|
ioctl(impl->ptp_fd, FIONREAD, &avail);
|
|
pw_log_debug("Flushing stale data: %u bytes", avail);
|
|
while (avail-- && read(impl->ptp_fd, &tmp, 1));
|
|
|
|
struct ptp_management_msg req;
|
|
spa_zero(req);
|
|
|
|
req.major_sdo_id_message_type = PTP_MESSAGE_TYPE_MANAGEMENT;
|
|
req.ver = PTP_VERSION_1588_2008_2_1;
|
|
req.message_length_be = htobe16(sizeof(struct ptp_management_msg));
|
|
spa_zero(req.clock_identity);
|
|
req.source_port_id_be = htobe16(getpid());
|
|
req.log_message_interval = 127;
|
|
req.sequence_id_be = htobe16(impl->ptp_seq++);
|
|
memset(req.target_port_identity, 0xff, 8);
|
|
req.target_port_id_be = htobe16(0xffff);
|
|
req.starting_boundary_hops = 1;
|
|
req.boundary_hops = 1;
|
|
req.action = PTP_MGMT_ACTION_GET;
|
|
req.tlv_type_be = htobe16(PTP_TLV_TYPE_MGMT);
|
|
// sent empty TLV, only sending management_id
|
|
req.management_message_length_be = htobe16(2);
|
|
req.management_id_be = htobe16(PTP_MGMT_ID_PARENT_DATA_SET);
|
|
|
|
if (write(impl->ptp_fd, &req, sizeof(req)) == -1) {
|
|
pw_log_warn("Failed to send PTP management request: %m");
|
|
if (errno != ENOTCONN)
|
|
return false;
|
|
close(impl->ptp_fd);
|
|
impl->ptp_fd = make_unix_ptp_mgmt_socket(impl->ptp_mgmt_socket_path);
|
|
if (impl->ptp_fd > -1)
|
|
pw_log_info("Reopened PTP management socket");
|
|
return false;
|
|
}
|
|
|
|
uint8_t buf[sizeof(struct ptp_management_msg) + sizeof(struct ptp_parent_data_set)];
|
|
if (read(impl->ptp_fd, &buf, sizeof(buf)) == -1) {
|
|
pw_log_warn("Failed to receive PTP management response: %m");
|
|
return false;
|
|
}
|
|
|
|
struct ptp_management_msg res = *(struct ptp_management_msg *)buf;
|
|
struct ptp_parent_data_set parent =
|
|
*(struct ptp_parent_data_set *)(buf + sizeof(struct ptp_management_msg));
|
|
|
|
if ((res.ver & 0x0f) != 2) {
|
|
pw_log_warn("PTP major version is %d, expected 2", res.ver);
|
|
return false;
|
|
}
|
|
|
|
if ((res.major_sdo_id_message_type & 0x0f) != PTP_MESSAGE_TYPE_MANAGEMENT) {
|
|
pw_log_warn("PTP management returned type %x, expected management", res.major_sdo_id_message_type);
|
|
return false;
|
|
}
|
|
|
|
if (res.action != PTP_MGMT_ACTION_RESPONSE) {
|
|
pw_log_warn("PTP management returned action %d, expected response", res.action);
|
|
return false;
|
|
}
|
|
|
|
if (be16toh(res.tlv_type_be) != PTP_TLV_TYPE_MGMT) {
|
|
pw_log_warn("PTP management returned tlv type %d, expected management", be16toh(res.tlv_type_be));
|
|
return false;
|
|
}
|
|
|
|
if (be16toh(res.management_id_be) != PTP_MGMT_ID_PARENT_DATA_SET) {
|
|
pw_log_warn("PTP management returned ID %d, expected PARENT_DATA_SET", be16toh(res.management_id_be));
|
|
return false;
|
|
}
|
|
|
|
uint16_t data_len = be16toh(res.management_message_length_be) - 2;
|
|
if (data_len != sizeof(struct ptp_parent_data_set))
|
|
pw_log_warn("Unexpected PTP GET PARENT_DATA_SET response length %u, expected %zu", data_len, sizeof(struct ptp_parent_data_set));
|
|
|
|
uint8_t *cid = res.clock_identity;
|
|
if (memcmp(cid, impl->clock_id, 8) != 0)
|
|
pw_log_info(
|
|
"Local clock ID: IEEE1588-2008:%02X-%02X-%02X-%02X-%02X-%02X-%02X-%02X:%d",
|
|
cid[0],
|
|
cid[1],
|
|
cid[2],
|
|
cid[3],
|
|
cid[4],
|
|
cid[5],
|
|
cid[6],
|
|
cid[7],
|
|
0 /* domain */
|
|
);
|
|
|
|
uint8_t *gmid = parent.gm_clock_id;
|
|
bool gmid_changed = false;
|
|
if (memcmp(gmid, impl->gm_id, 8) != 0) {
|
|
pw_log_info(
|
|
"GM ID: IEEE1588-2008:%02X-%02X-%02X-%02X-%02X-%02X-%02X-%02X:%d",
|
|
gmid[0],
|
|
gmid[1],
|
|
gmid[2],
|
|
gmid[3],
|
|
gmid[4],
|
|
gmid[5],
|
|
gmid[6],
|
|
gmid[7],
|
|
0 /* domain */
|
|
);
|
|
gmid_changed = true;
|
|
}
|
|
|
|
// When GM is not equal to own clock we are clocked by external master
|
|
pw_log_debug("Synced to GM: %s", (memcmp(cid, gmid, 8) != 0) ? "true" : "false");
|
|
|
|
memcpy(impl->clock_id, cid, 8);
|
|
memcpy(impl->gm_id, gmid, 8);
|
|
return gmid_changed;
|
|
}
|
|
|
|
static uint16_t generate_hash(uint16_t prev)
|
|
{
|
|
uint16_t hash = pw_rand32();
|
|
if (hash == prev) hash++;
|
|
if (hash == 0) hash++;
|
|
return hash;
|
|
}
|
|
|
|
static int make_sdp(struct impl *impl, struct session *sess, char *buffer, size_t buffer_size, bool new)
|
|
{
|
|
char src_addr[64], dst_addr[64], dst_ttl[8], ptime[32];
|
|
struct sdp_info *sdp = &sess->info;
|
|
bool src_ip4, dst_ip4;
|
|
bool multicast;
|
|
const char *user_name, *str;
|
|
struct spa_strbuf buf;
|
|
int res;
|
|
struct pw_properties *props = sess->props;
|
|
|
|
if ((res = pw_net_get_ip(&impl->src_addr, src_addr, sizeof(src_addr), &src_ip4, NULL)) < 0)
|
|
return res;
|
|
|
|
if ((res = pw_net_get_ip(&sdp->dst_addr, dst_addr, sizeof(dst_addr), &dst_ip4, NULL)) < 0)
|
|
return res;
|
|
|
|
if (new) {
|
|
/* update the version and hash */
|
|
sdp->hash = generate_hash(sdp->hash);
|
|
if ((str = pw_properties_get(props, "sess.id")) != NULL) {
|
|
if (!spa_atou32(str, &sdp->session_id, 10)) {
|
|
pw_log_error("Invalid session id: %s (must be a uint32)", str);
|
|
return -EINVAL;
|
|
}
|
|
sdp->t_ntp = pw_properties_get_uint32(props, "rtp.ntp",
|
|
(uint32_t) time(NULL) + 2208988800U + impl->n_sessions);
|
|
} else {
|
|
sdp->session_id = (uint32_t) time(NULL) + 2208988800U + impl->n_sessions;
|
|
sdp->t_ntp = pw_properties_get_uint32(props, "rtp.ntp", sdp->session_id);
|
|
}
|
|
if ((str = pw_properties_get(props, "sess.version")) != NULL) {
|
|
if (!spa_atou32(str, &sdp->session_version, 10)) {
|
|
pw_log_error("Invalid session version: %s (must be a uint32)", str);
|
|
return -EINVAL;
|
|
}
|
|
} else {
|
|
sdp->session_version = sdp->t_ntp;
|
|
}
|
|
}
|
|
|
|
if ((user_name = pw_get_user_name()) == NULL)
|
|
user_name = "-";
|
|
|
|
multicast = is_multicast((struct sockaddr*)&sdp->dst_addr, sdp->dst_len);
|
|
|
|
spa_zero(dst_ttl);
|
|
if (multicast)
|
|
snprintf(dst_ttl, sizeof(dst_ttl), "/%d", sdp->ttl);
|
|
|
|
spa_strbuf_init(&buf, buffer, buffer_size);
|
|
/* Don't add any sdp records in between this definition or change the order
|
|
it will break compatibility with Dante/AES67 devices. Add new records to
|
|
the end. */
|
|
spa_strbuf_append(&buf,
|
|
"v=0\n"
|
|
"o=%s %u %u IN %s %s\n"
|
|
"s=%s\n"
|
|
"c=IN %s %s%s\n"
|
|
"t=%u 0\n"
|
|
"m=%s %u RTP/AVP %i\n",
|
|
user_name, sdp->session_id, sdp->session_version, src_ip4 ? "IP4" : "IP6", src_addr,
|
|
sdp->session_name,
|
|
(multicast ? dst_ip4 : src_ip4) ? "IP4" : "IP6", multicast ? dst_addr : src_addr, dst_ttl,
|
|
sdp->t_ntp,
|
|
sdp->media_type, sdp->dst_port, sdp->payload);
|
|
|
|
if (impl->extra_attrs_preamble)
|
|
spa_strbuf_append(&buf, "%s", impl->extra_attrs_preamble);
|
|
|
|
if (sdp->channels) {
|
|
if (sdp->channelmap[0] != 0) {
|
|
// Produce Audinate format channel record. It's recognized by RAVENNA
|
|
spa_strbuf_append(&buf,
|
|
"i=%d channels: %s\n", sdp->channels,
|
|
sdp->channelmap);
|
|
} else {
|
|
spa_strbuf_append(&buf, "i=%d channels:", sdp->channels);
|
|
for (uint i = 1; i <= sdp->channels; i++) {
|
|
if (i > 1) spa_strbuf_append(&buf, ",");
|
|
spa_strbuf_append(&buf, " AUX%u", i);
|
|
}
|
|
spa_strbuf_append(&buf, "\n");
|
|
}
|
|
spa_strbuf_append(&buf,
|
|
"a=recvonly\n"
|
|
"a=rtpmap:%i %s/%u/%u\n",
|
|
sdp->payload, sdp->mime_type,
|
|
sdp->rate, sdp->channels);
|
|
} else {
|
|
spa_strbuf_append(&buf,
|
|
"a=rtpmap:%i %s/%u\n",
|
|
sdp->payload, sdp->mime_type, sdp->rate);
|
|
}
|
|
|
|
if (is_multicast((struct sockaddr*)&sdp->dst_addr, sdp->dst_len))
|
|
spa_strbuf_append(&buf,
|
|
"a=source-filter: incl IN %s %s %s\n", dst_ip4 ? "IP4" : "IP6",
|
|
dst_addr, src_addr);
|
|
|
|
if (sdp->ssrc > 0)
|
|
spa_strbuf_append(&buf, "a=ssrc:%u\n", sdp->ssrc);
|
|
|
|
if (sdp->ptime > 0)
|
|
spa_strbuf_append(&buf,
|
|
"a=ptime:%s\n", spa_dtoa(ptime, sizeof(ptime), sdp->ptime));
|
|
|
|
if (sdp->framecount > 0)
|
|
spa_strbuf_append(&buf,
|
|
"a=framecount:%u\n", sdp->framecount);
|
|
|
|
if (sdp->ts_refclk != NULL || sess->ts_refclk_ptp) {
|
|
// Only broadcast the GM ID when we are synced to external time source
|
|
if (sess->ts_refclk_ptp && memcmp(impl->clock_id, impl->gm_id, 8) != 0) {
|
|
spa_strbuf_append(&buf,
|
|
"a=ts-refclk:ptp=IEEE1588-2008:%02X-%02X-%02X-%02X-%02X-%02X-%02X-%02X:%d\n",
|
|
impl->gm_id[0],
|
|
impl->gm_id[1],
|
|
impl->gm_id[2],
|
|
impl->gm_id[3],
|
|
impl->gm_id[4],
|
|
impl->gm_id[5],
|
|
impl->gm_id[6],
|
|
impl->gm_id[7],
|
|
0/* domain */);
|
|
} else if (sdp->ts_refclk != NULL) {
|
|
spa_strbuf_append(&buf, "a=ts-refclk:%s\n", sdp->ts_refclk);
|
|
}
|
|
spa_strbuf_append(&buf, "a=mediaclk:direct=%u\n", sdp->ts_offset);
|
|
} else {
|
|
spa_strbuf_append(&buf, "a=mediaclk:sender\n");
|
|
}
|
|
|
|
spa_strbuf_append(&buf,
|
|
"a=tool:PipeWire %s\n"
|
|
"a=type:broadcast\n",
|
|
pw_get_library_version());
|
|
|
|
if (impl->extra_attrs_end)
|
|
spa_strbuf_append(&buf, "%s", impl->extra_attrs_end);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int send_sap(struct impl *impl, struct session *sess, bool bye)
|
|
{
|
|
struct sap_header header;
|
|
struct iovec iov[4];
|
|
struct msghdr msg;
|
|
struct sdp_info *sdp = &sess->info;
|
|
int res;
|
|
|
|
if (!sess->has_sent_sap && bye)
|
|
return 0;
|
|
|
|
if (impl->sap_fd == -1) {
|
|
int fd;
|
|
char addr[64];
|
|
const char *str;
|
|
|
|
if ((str = pw_properties_get(sess->props, "source.ip")) == NULL) {
|
|
if (impl->ifname) {
|
|
int fd = socket(impl->sap_addr.ss_family, SOCK_DGRAM, 0);
|
|
if (fd >= 0) {
|
|
struct ifreq req;
|
|
spa_zero(req);
|
|
req.ifr_addr.sa_family = impl->sap_addr.ss_family;
|
|
snprintf(req.ifr_name, sizeof(req.ifr_name), "%s", impl->ifname);
|
|
res = ioctl(fd, SIOCGIFADDR, &req);
|
|
if (res < 0)
|
|
pw_log_warn("SIOCGIFADDR %s failed: %m", impl->ifname);
|
|
str = inet_ntop(req.ifr_addr.sa_family,
|
|
&((struct sockaddr_in *)&req.ifr_addr)->sin_addr,
|
|
addr, sizeof(addr));
|
|
if (str == NULL) {
|
|
pw_log_warn("can't parse interface ip: %m");
|
|
} else {
|
|
pw_log_info("interface %s IP: %s", impl->ifname, str);
|
|
}
|
|
close(fd);
|
|
}
|
|
}
|
|
if (str == NULL)
|
|
str = impl->sap_addr.ss_family == AF_INET ?
|
|
DEFAULT_SOURCE_IP : DEFAULT_SOURCE_IP6;
|
|
}
|
|
if ((res = pw_net_parse_address(str, 0, &impl->src_addr, &impl->src_len)) < 0) {
|
|
pw_log_error("invalid source.ip %s: %s", str, spa_strerror(res));
|
|
return res;
|
|
}
|
|
if ((fd = make_send_socket(&impl->src_addr, impl->src_len,
|
|
&impl->sap_addr, impl->sap_len,
|
|
impl->mcast_loop, impl->ttl)) < 0)
|
|
return fd;
|
|
|
|
impl->sap_fd = fd;
|
|
}
|
|
|
|
/* For the first session, we might not yet have an SDP because the
|
|
* socket needs to be open for us to get the interface address (which
|
|
* happens above. So let's create the SDP now, if needed. */
|
|
if (!sess->has_sdp) {
|
|
res = make_sdp(impl, sess, sess->sdp, sizeof(sess->sdp), true);
|
|
if (res != 0) {
|
|
pw_log_error("Failed to create SDP: %s", spa_strerror(res));
|
|
return res;
|
|
}
|
|
sess->has_sdp = true;
|
|
}
|
|
|
|
spa_zero(header);
|
|
header.v = 1;
|
|
header.t = bye;
|
|
header.msg_id_hash = sdp->hash;
|
|
|
|
iov[0].iov_base = &header;
|
|
iov[0].iov_len = sizeof(header);
|
|
|
|
if (impl->src_addr.ss_family == AF_INET) {
|
|
iov[1].iov_base = &((struct sockaddr_in*) &impl->src_addr)->sin_addr;
|
|
iov[1].iov_len = 4U;
|
|
} else {
|
|
iov[1].iov_base = &((struct sockaddr_in6*) &impl->src_addr)->sin6_addr;
|
|
iov[1].iov_len = 16U;
|
|
header.a = 1;
|
|
}
|
|
iov[2].iov_base = SAP_MIME_TYPE;
|
|
iov[2].iov_len = sizeof(SAP_MIME_TYPE);
|
|
iov[3].iov_base = sess->sdp;
|
|
iov[3].iov_len = strlen(sess->sdp);
|
|
|
|
msg.msg_name = NULL;
|
|
msg.msg_namelen = 0;
|
|
msg.msg_iov = iov;
|
|
msg.msg_iovlen = 4;
|
|
msg.msg_control = NULL;
|
|
msg.msg_controllen = 0;
|
|
msg.msg_flags = 0;
|
|
|
|
pw_log_debug("sending SAP for %u %s", sess->node->id, sess->sdp);
|
|
|
|
res = sendmsg(impl->sap_fd, &msg, MSG_NOSIGNAL);
|
|
if (res < 0)
|
|
res = -errno;
|
|
else
|
|
sess->has_sent_sap = true;
|
|
|
|
return res;
|
|
}
|
|
|
|
static void on_igmp_recovery_timer_event(void *data)
|
|
{
|
|
struct impl *impl = data;
|
|
char addr[128];
|
|
int res = 0;
|
|
|
|
/* Only attempt recovery if we have a valid socket and multicast address */
|
|
if (impl->igmp_recovery.socket_fd < 0) {
|
|
pw_log_debug("no socket, skipping IGMP recovery");
|
|
goto finish;
|
|
}
|
|
|
|
pw_net_get_ip(&impl->igmp_recovery.mcast_addr, addr, sizeof(addr), NULL, NULL);
|
|
pw_log_info("IGMP recovery triggered for %s", addr);
|
|
|
|
/* Force IGMP membership refresh by leaving the group first, then rejoin */
|
|
if (impl->igmp_recovery.is_ipv6) {
|
|
struct ipv6_mreq mr6;
|
|
memset(&mr6, 0, sizeof(mr6));
|
|
mr6.ipv6mr_multiaddr = ((struct sockaddr_in6*)&impl->igmp_recovery.mcast_addr)->sin6_addr;
|
|
mr6.ipv6mr_interface = impl->igmp_recovery.if_index;
|
|
|
|
/* Leave the group first */
|
|
res = setsockopt(impl->igmp_recovery.socket_fd, IPPROTO_IPV6, IPV6_LEAVE_GROUP,
|
|
&mr6, sizeof(mr6));
|
|
if (SPA_LIKELY(res == 0)) {
|
|
pw_log_info("left IPv6 multicast group");
|
|
} else {
|
|
if (errno == EADDRNOTAVAIL) {
|
|
pw_log_info("attempted to leave IPv6 multicast group, but "
|
|
"membership was already silently dropped");
|
|
} else {
|
|
pw_log_warn("failed to leave IPv6 multicast group: %m");
|
|
}
|
|
}
|
|
|
|
res = setsockopt(impl->igmp_recovery.socket_fd, IPPROTO_IPV6, IPV6_JOIN_GROUP,
|
|
&mr6, sizeof(mr6));
|
|
if (res < 0) {
|
|
pw_log_warn("failed to re-join IPv6 multicast group: %m");
|
|
} else {
|
|
pw_log_info("re-joined IPv6 multicast group successfully");
|
|
}
|
|
} else {
|
|
struct ip_mreqn mr4;
|
|
memset(&mr4, 0, sizeof(mr4));
|
|
mr4.imr_multiaddr = ((struct sockaddr_in*)&impl->igmp_recovery.mcast_addr)->sin_addr;
|
|
mr4.imr_ifindex = impl->igmp_recovery.if_index;
|
|
|
|
/* Leave the group first */
|
|
res = setsockopt(impl->igmp_recovery.socket_fd, IPPROTO_IP, IP_DROP_MEMBERSHIP,
|
|
&mr4, sizeof(mr4));
|
|
if (SPA_LIKELY(res == 0)) {
|
|
pw_log_info("left IPv4 multicast group");
|
|
} else {
|
|
if (errno == EADDRNOTAVAIL) {
|
|
pw_log_info("attempted to leave IPv4 multicast group, but "
|
|
"membership was already silently dropped");
|
|
} else {
|
|
pw_log_warn("failed to leave IPv4 multicast group: %m");
|
|
}
|
|
}
|
|
|
|
res = setsockopt(impl->igmp_recovery.socket_fd, IPPROTO_IP, IP_ADD_MEMBERSHIP,
|
|
&mr4, sizeof(mr4));
|
|
if (res < 0) {
|
|
pw_log_warn("failed to re-join IPv4 multicast group: %m");
|
|
} else {
|
|
pw_log_info("re-joined IPv4 multicast group successfully");
|
|
}
|
|
}
|
|
|
|
finish:
|
|
/* If rejoining failed, try again in 1 second. This can happen
|
|
* for example when the network interface went down, and is not
|
|
* yet up and running again, and ENODEV is returned as a result.
|
|
* Otherwise, continue with the usual deadline. */
|
|
pw_timer_queue_add(impl->timer_queue, &impl->igmp_recovery.timer,
|
|
&impl->igmp_recovery.timer.timeout,
|
|
((res == 0) ? impl->igmp_recovery.deadline : 1) * SPA_NSEC_PER_SEC,
|
|
on_igmp_recovery_timer_event, impl);
|
|
}
|
|
|
|
static void rearm_igmp_recovery_timer(struct impl *impl)
|
|
{
|
|
pw_timer_queue_cancel(&impl->igmp_recovery.timer);
|
|
pw_timer_queue_add(impl->timer_queue, &impl->igmp_recovery.timer,
|
|
NULL, impl->igmp_recovery.deadline * SPA_NSEC_PER_SEC,
|
|
on_igmp_recovery_timer_event, impl);
|
|
}
|
|
|
|
static void on_sap_send_timer_event(void *data)
|
|
{
|
|
struct impl *impl = data;
|
|
struct session *sess, *tmp;
|
|
uint64_t timestamp, interval;
|
|
bool clk_changed;
|
|
int res;
|
|
|
|
timestamp = get_time_nsec(impl);
|
|
interval = impl->cleanup_interval * SPA_NSEC_PER_SEC;
|
|
clk_changed = update_ts_refclk(impl);
|
|
|
|
spa_list_for_each_safe(sess, tmp, &impl->sessions, link) {
|
|
if (sess->announce) {
|
|
if (clk_changed) {
|
|
// The clock has changed: Send bye and create new SDP.
|
|
send_sap(impl, sess, 1);
|
|
|
|
res = make_sdp(impl, sess, sess->sdp, sizeof(sess->sdp), true);
|
|
if (res != 0)
|
|
pw_log_error("Failed to create SDP: %s", spa_strerror(res));
|
|
else
|
|
sess->has_sdp = true;
|
|
}
|
|
send_sap(impl, sess, 0);
|
|
} else {
|
|
if (sess->timestamp + interval < timestamp) {
|
|
pw_log_info("session %s timeout",
|
|
sess->info.session_name);
|
|
session_free(sess);
|
|
}
|
|
|
|
}
|
|
}
|
|
pw_timer_queue_add(impl->timer_queue, &impl->sap_send_timer,
|
|
&impl->sap_send_timer.timeout, SAP_INTERVAL_SEC * SPA_NSEC_PER_SEC,
|
|
on_sap_send_timer_event, impl);
|
|
}
|
|
|
|
static void on_start_sap_retry_timer_event(void *data)
|
|
{
|
|
struct impl *impl = data;
|
|
pw_log_debug("trying again to start SAP send after previous attempt failed with ENODEV");
|
|
start_sap(impl);
|
|
}
|
|
|
|
static struct session *session_find(struct impl *impl, const struct sdp_info *info)
|
|
{
|
|
struct session *sess;
|
|
spa_list_for_each(sess, &impl->sessions, link) {
|
|
if (info->hash == sess->info.hash &&
|
|
info->dst_port == sess->info.dst_port &&
|
|
spa_streq(info->origin, sess->info.origin))
|
|
return sess;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static inline void replace_str(char **dst, const char *val)
|
|
{
|
|
free(*dst);
|
|
*dst = val ? strdup(val) : NULL;
|
|
}
|
|
|
|
static struct session *session_new_announce(struct impl *impl, struct node *node,
|
|
struct pw_properties *props)
|
|
{
|
|
char buffer[MAX_SDP];
|
|
struct session *sess = NULL;
|
|
struct sdp_info *sdp;
|
|
const char *str;
|
|
uint32_t port;
|
|
int res;
|
|
|
|
sess = node->session;
|
|
if (sess == NULL) {
|
|
if (impl->n_sessions >= impl->max_sessions) {
|
|
pw_log_warn("too many sessions (%u >= %u)", impl->n_sessions, impl->max_sessions);
|
|
errno = EMFILE;
|
|
return NULL;
|
|
}
|
|
sess = calloc(1, sizeof(struct session));
|
|
if (sess == NULL)
|
|
return NULL;
|
|
|
|
pw_log_info("created new session for node:%u", node->id);
|
|
node->session = sess;
|
|
sess->node = node;
|
|
sess->impl = impl;
|
|
sess->announce = true;
|
|
spa_list_append(&impl->sessions, &sess->link);
|
|
impl->n_sessions++;
|
|
}
|
|
|
|
sdp = &sess->info;
|
|
|
|
pw_properties_free(sess->props);
|
|
sess->props = props;
|
|
|
|
if ((str = pw_properties_get(props, "sess.name")) == NULL)
|
|
str = pw_get_host_name();
|
|
replace_str(&sdp->session_name, str);
|
|
|
|
if ((str = pw_properties_get(props, "rtp.destination.port")) == NULL)
|
|
goto error_free;
|
|
if (!spa_atou32(str, &port, 0))
|
|
goto error_free;
|
|
sdp->dst_port = port;
|
|
|
|
if ((str = pw_properties_get(props, "rtp.destination.ip")) == NULL)
|
|
goto error_free;
|
|
if ((res = pw_net_parse_address(str, sdp->dst_port, &sdp->dst_addr, &sdp->dst_len)) < 0) {
|
|
pw_log_error("invalid destination.ip %s: %s", str, spa_strerror(res));
|
|
goto error_free;
|
|
}
|
|
sdp->ttl = pw_properties_get_int32(props, "rtp.ttl", DEFAULT_TTL);
|
|
sdp->payload = pw_properties_get_int32(props, "rtp.payload", 127);
|
|
|
|
if ((str = pw_properties_get(props, "rtp.ptime")) != NULL)
|
|
if (!spa_atof(str, &sdp->ptime))
|
|
sdp->ptime = 0.0;
|
|
|
|
if ((str = pw_properties_get(props, "rtp.framecount")) != NULL)
|
|
if (!spa_atou32(str, &sdp->framecount, 0))
|
|
sdp->framecount = 0;
|
|
|
|
str = pw_properties_get(props, "rtp.media");
|
|
replace_str(&sdp->media_type, str);
|
|
str = pw_properties_get(props, "rtp.mime");
|
|
replace_str(&sdp->mime_type, str);
|
|
|
|
if ((str = pw_properties_get(props, "rtp.rate")) != NULL)
|
|
sdp->rate = atoi(str);
|
|
if ((str = pw_properties_get(props, "rtp.channels")) != NULL)
|
|
sdp->channels = atoi(str);
|
|
if ((str = pw_properties_get(props, "rtp.ssrc")) != NULL)
|
|
sdp->ssrc = atoi(str);
|
|
else
|
|
sdp->ssrc = 0;
|
|
if ((str = pw_properties_get(props, "rtp.ts-offset")) != NULL)
|
|
sdp->ts_offset = atoi(str);
|
|
str = pw_properties_get(props, "rtp.ts-refclk");
|
|
replace_str(&sdp->ts_refclk, str);
|
|
|
|
sess->ts_refclk_ptp = pw_properties_get_bool(props, "rtp.fetch-ts-refclk", false);
|
|
if ((str = pw_properties_get(props, PW_KEY_NODE_CHANNELNAMES)) != NULL) {
|
|
struct spa_strbuf buf;
|
|
struct spa_json it[1];
|
|
char v[256];
|
|
int count = 0;
|
|
|
|
spa_strbuf_init(&buf, sdp->channelmap, sizeof(sdp->channelmap));
|
|
|
|
if (spa_json_begin_array_relax(&it[0], str, strlen(str)) > 0) {
|
|
while (spa_json_get_string(&it[0], v, sizeof(v)) > 0)
|
|
spa_strbuf_append(&buf, "%s%s", count++ > 0 ? ", " : "", v);
|
|
}
|
|
}
|
|
|
|
/* see if we can make an SDP, will fail for the first session because we
|
|
* haven't got the SAP socket open yet */
|
|
res = make_sdp(impl, sess, buffer, sizeof(buffer), false);
|
|
|
|
/* we had no sdp or something changed */
|
|
if (!sess->has_sdp || ((res == 0) && strcmp(buffer, sess->sdp) != 0)) {
|
|
/* send bye on the old session */
|
|
send_sap(impl, sess, 1);
|
|
|
|
/* make an updated SDP for sending, this should not actually fail */
|
|
res = make_sdp(impl, sess, sess->sdp, sizeof(sess->sdp), true);
|
|
if (res != 0)
|
|
pw_log_error("Failed to create SDP: %s", spa_strerror(res));
|
|
else
|
|
sess->has_sdp = true;
|
|
}
|
|
|
|
send_sap(impl, sess, 0);
|
|
|
|
return sess;
|
|
|
|
error_free:
|
|
pw_log_warn("invalid session props");
|
|
session_free(sess);
|
|
return NULL;
|
|
}
|
|
|
|
static void session_module_destroy(void *d)
|
|
{
|
|
struct session *sess = d;
|
|
spa_hook_remove(&sess->module_listener);
|
|
sess->module = NULL;
|
|
session_free(sess);
|
|
}
|
|
|
|
static const struct pw_impl_module_events session_module_events = {
|
|
PW_VERSION_IMPL_MODULE_EVENTS,
|
|
.destroy = session_module_destroy,
|
|
};
|
|
|
|
static int session_load_source(struct session *session, struct pw_properties *props)
|
|
{
|
|
struct impl *impl = session->impl;
|
|
struct pw_context *context = pw_impl_module_get_context(impl->module);
|
|
FILE *f = NULL;
|
|
char *args = NULL;
|
|
size_t size;
|
|
const char *str, *media;
|
|
int res;
|
|
|
|
if ((f = open_memstream(&args, &size)) == NULL) {
|
|
res = -errno;
|
|
pw_log_error("Can't open memstream: %m");
|
|
goto done;
|
|
}
|
|
fprintf(f, "{");
|
|
|
|
if ((str = pw_properties_get(props, "rtp.destination.ip")) != NULL)
|
|
fprintf(f, "\"source.ip\" = \"%s\", ", str);
|
|
if ((str = pw_properties_get(props, "rtp.destination.port")) != NULL)
|
|
fprintf(f, "\"source.port\" = %s, ", str);
|
|
if ((str = pw_properties_get(props, "rtp.session")) != NULL)
|
|
fprintf(f, "\"sess.name\" = \"%s\", ", str);
|
|
|
|
/* Use an interface if explicitly specified, else use the SAP interface if that was specified */
|
|
if ((str = pw_properties_get(props, "local.ifname")) != NULL || (str = impl->ifname) != NULL) {
|
|
fprintf(f, "\"local.ifname\" = \"%s\", ", str);
|
|
}
|
|
|
|
if ((media = pw_properties_get(props, "sess.media")) == NULL)
|
|
media = "audio";
|
|
|
|
if ((str = pw_properties_get(props, "cleanup.sec")) != NULL) {
|
|
fprintf(f, "\"cleanup.sec\" = \"%s\", ", str);
|
|
}
|
|
|
|
if (spa_streq(media, "audio")) {
|
|
const char *mime;
|
|
const struct format_info *format_info;
|
|
|
|
if ((mime = pw_properties_get(props, "rtp.mime")) == NULL) {
|
|
pw_log_error("missing rtp.mime property");
|
|
res = -EINVAL;
|
|
goto done;
|
|
}
|
|
format_info = find_audio_format_info(mime);
|
|
if (format_info == NULL) {
|
|
pw_log_error("unknown rtp.mime type %s", mime);
|
|
res = -ENOTSUP;
|
|
goto done;
|
|
}
|
|
fprintf(f, "\"sess.media\" = \"%s\", ", format_info->media_type);
|
|
if (format_info->format_str != NULL) {
|
|
pw_properties_set(props, "audio.format", format_info->format_str);
|
|
if ((str = pw_properties_get(props, "rtp.rate")) != NULL)
|
|
pw_properties_set(props, "audio.rate", str);
|
|
if ((str = pw_properties_get(props, "rtp.channels")) != NULL)
|
|
pw_properties_set(props, "audio.channels", str);
|
|
}
|
|
if ((str = pw_properties_get(props, "rtp.ssrc")) != NULL)
|
|
fprintf(f, "\"rtp.receiver-ssrc\" = \"%s\", ", str);
|
|
} else {
|
|
pw_log_error("Unhandled media %s", media);
|
|
res = -EINVAL;
|
|
goto done;
|
|
}
|
|
if ((str = pw_properties_get(props, "rtp.ts-offset")) != NULL)
|
|
fprintf(f, "\"sess.ts-offset\" = %s, ", str);
|
|
|
|
fprintf(f, " stream.props = {");
|
|
pw_properties_serialize_dict(f, &props->dict, 0);
|
|
fprintf(f, " }");
|
|
fprintf(f, "}");
|
|
fclose(f);
|
|
f = NULL;
|
|
|
|
pw_log_info("loading new RTP source");
|
|
session->module = pw_context_load_module(context,
|
|
"libpipewire-module-rtp-source",
|
|
args, NULL);
|
|
|
|
if (session->module == NULL) {
|
|
res = -errno;
|
|
pw_log_error("Can't load module: %m");
|
|
goto done;
|
|
}
|
|
|
|
pw_impl_module_add_listener(session->module,
|
|
&session->module_listener,
|
|
&session_module_events, session);
|
|
|
|
res = 0;
|
|
done:
|
|
if (f != NULL)
|
|
fclose(f);
|
|
free(args);
|
|
return res;
|
|
}
|
|
|
|
struct match_info {
|
|
struct impl *impl;
|
|
struct session *session;
|
|
struct node *node;
|
|
struct pw_properties *props;
|
|
bool matched;
|
|
};
|
|
|
|
static int rule_matched(void *data, const char *location, const char *action,
|
|
const char *str, size_t len)
|
|
{
|
|
struct match_info *i = data;
|
|
int res = 0;
|
|
|
|
i->matched = true;
|
|
if (i->session && spa_streq(action, "create-stream")) {
|
|
pw_properties_update_string(i->props, str, len);
|
|
|
|
session_load_source(i->session, i->props);
|
|
}
|
|
else if (i->node && spa_streq(action, "announce-stream")) {
|
|
struct pw_properties *props;
|
|
|
|
if ((props = pw_properties_new_dict(i->node->info->props)) == NULL)
|
|
return -errno;
|
|
|
|
pw_properties_update_string(props, str, len);
|
|
|
|
session_new_announce(i->impl, i->node, props);
|
|
}
|
|
return res;
|
|
}
|
|
|
|
static struct session *session_new(struct impl *impl, struct sdp_info *info)
|
|
{
|
|
struct session *session;
|
|
struct pw_properties *props;
|
|
const char *str;
|
|
char dst_addr[64], tmp[64];
|
|
|
|
if (impl->n_sessions >= impl->max_sessions) {
|
|
pw_log_warn("too many sessions (%u >= %u)", impl->n_sessions, impl->max_sessions);
|
|
errno = EMFILE;
|
|
return NULL;
|
|
}
|
|
|
|
session = calloc(1, sizeof(struct session));
|
|
if (session == NULL)
|
|
return NULL;
|
|
|
|
session->announce = false;
|
|
session->info = *info;
|
|
spa_zero(*info);
|
|
info = &session->info;
|
|
|
|
props = pw_properties_new(NULL, NULL);
|
|
if (props == NULL)
|
|
goto error;
|
|
|
|
session->impl = impl;
|
|
spa_list_append(&impl->sessions, &session->link);
|
|
impl->n_sessions++;
|
|
|
|
pw_properties_set(props, "rtp.origin", info->origin);
|
|
if (info->session_name != NULL) {
|
|
pw_properties_set(props, "rtp.session", info->session_name);
|
|
pw_properties_setf(props, PW_KEY_MEDIA_NAME, "RTP Stream (%s)",
|
|
info->session_name);
|
|
pw_properties_setf(props, PW_KEY_NODE_NAME, "%s",
|
|
info->session_name);
|
|
} else {
|
|
pw_properties_set(props, PW_KEY_MEDIA_NAME, "RTP Stream");
|
|
}
|
|
|
|
pw_net_get_ip(&info->dst_addr, dst_addr, sizeof(dst_addr), NULL, NULL);
|
|
pw_properties_setf(props, "rtp.destination.ip", "%s", dst_addr);
|
|
pw_properties_setf(props, "rtp.destination.port", "%u", info->dst_port);
|
|
pw_properties_setf(props, "rtp.payload", "%u", info->payload);
|
|
pw_properties_set(props, "rtp.ptime", spa_dtoa(tmp, sizeof(tmp), info->ptime));
|
|
pw_properties_setf(props, "rtp.framecount", "%u", info->framecount);
|
|
pw_properties_setf(props, "rtp.media", "%s", info->media_type);
|
|
pw_properties_setf(props, "rtp.mime", "%s", info->mime_type);
|
|
pw_properties_setf(props, "rtp.rate", "%u", info->rate);
|
|
pw_properties_setf(props, "rtp.channels", "%u", info->channels);
|
|
|
|
pw_properties_setf(props, "rtp.ts-offset", "%u", info->ts_offset);
|
|
pw_properties_set(props, "rtp.ts-refclk", info->ts_refclk);
|
|
|
|
if (info->ssrc > 0)
|
|
pw_properties_setf(props, "rtp.ssrc", "%u", info->ssrc);
|
|
|
|
if (info->channelmap[0])
|
|
pw_properties_set(props, PW_KEY_NODE_CHANNELNAMES, info->channelmap);
|
|
|
|
if ((str = pw_properties_get(impl->props, "stream.rules")) == NULL)
|
|
str = DEFAULT_CREATE_RULES;
|
|
if (str != NULL) {
|
|
struct match_info minfo = {
|
|
.impl = impl,
|
|
.session = session,
|
|
.props = props,
|
|
};
|
|
pw_conf_match_rules(str, strlen(str), NAME, &props->dict,
|
|
rule_matched, &minfo);
|
|
}
|
|
session->props = props;
|
|
session_touch(session);
|
|
|
|
return NULL;
|
|
error:
|
|
session_free(session);
|
|
return NULL;
|
|
}
|
|
|
|
static int parse_sdp_c(struct impl *impl, char *c, struct sdp_info *info)
|
|
{
|
|
int res;
|
|
|
|
c[strcspn(c, "/")] = 0;
|
|
if (spa_strstartswith(c, "c=IN IP4 ")) {
|
|
struct sockaddr_in *sa = (struct sockaddr_in*) &info->dst_addr;
|
|
|
|
c += strlen("c=IN IP4 ");
|
|
if (inet_pton(AF_INET, c, &sa->sin_addr) <= 0) {
|
|
res = -errno;
|
|
pw_log_warn("inet_pton(%s) failed: %m", c);
|
|
goto error;
|
|
}
|
|
sa->sin_family = AF_INET;
|
|
info->dst_len = sizeof(struct sockaddr_in);
|
|
}
|
|
else if (spa_strstartswith(c, "c=IN IP6 ")) {
|
|
struct sockaddr_in6 *sa = (struct sockaddr_in6*) &info->dst_addr;
|
|
|
|
c += strlen("c=IN IP6 ");
|
|
if (inet_pton(AF_INET6, c, &sa->sin6_addr) <= 0) {
|
|
res = -errno;
|
|
pw_log_warn("inet_pton(%s) failed: %m", c);
|
|
goto error;
|
|
}
|
|
|
|
sa->sin6_family = AF_INET6;
|
|
info->dst_len = sizeof(struct sockaddr_in6);
|
|
} else
|
|
return -EINVAL;
|
|
|
|
|
|
res= 0;
|
|
error:
|
|
return res;
|
|
}
|
|
|
|
static int parse_sdp_m(struct impl *impl, char *c, struct sdp_info *info)
|
|
{
|
|
int port, payload;
|
|
char media_type[12];
|
|
|
|
if (!spa_strstartswith(c, "m="))
|
|
return -EINVAL;
|
|
|
|
c += strlen("m=");
|
|
if (sscanf(c, "%11s %i RTP/AVP %i", media_type, &port, &payload) != 3)
|
|
return -EINVAL;
|
|
|
|
if (port <= 0 || port > 0xFFFF)
|
|
return -EINVAL;
|
|
|
|
if (payload < 0 || payload > 127)
|
|
return -EINVAL;
|
|
|
|
info->media_type = strdup(media_type);
|
|
info->dst_port = (uint16_t) port;
|
|
info->payload = (uint8_t) payload;
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* some AES67 devices have channelmap encoded in i=*
|
|
* if `i` record is found, it matches the template
|
|
* and channel count matches, name the channels respectively
|
|
* `i=2 channels: 01, 08` is the format
|
|
* This is Audinate format. TODO: parse RAVENNA `i=CH1,CH2,CH3` format */
|
|
static int parse_sdp_i(struct impl *impl, char *c, struct sdp_info *info)
|
|
{
|
|
if (!strstr(c, " channels: ")) {
|
|
return 0;
|
|
}
|
|
|
|
c += strlen("i=");
|
|
c[strcspn(c, " ")] = '\0';
|
|
|
|
uint32_t channels;
|
|
if (sscanf(c, "%u", &channels) != 1 || channels <= 0 || channels > MAX_CHANNELS)
|
|
return 0;
|
|
|
|
c += strcspn(c, "\0");
|
|
c += strlen(" channels: ");
|
|
|
|
strncpy(info->channelmap, c, sizeof(info->channelmap) - 1);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int parse_sdp_a_rtpmap(struct impl *impl, char *c, struct sdp_info *info)
|
|
{
|
|
int payload, len, rate, channels;
|
|
|
|
if (!spa_strstartswith(c, "a=rtpmap:"))
|
|
return 0;
|
|
|
|
c += strlen("a=rtpmap:");
|
|
|
|
if (sscanf(c, "%i %n", &payload, &len) != 1)
|
|
return -EINVAL;
|
|
|
|
if (payload < 0 || payload > 127)
|
|
return -EINVAL;
|
|
|
|
if (payload != info->payload)
|
|
return 0;
|
|
|
|
c += len;
|
|
c[strcspn(c, "/")] = 0;
|
|
info->mime_type = strdup(c);
|
|
c += strlen(c) + 1;
|
|
|
|
if (sscanf(c, "%u/%u", &rate, &channels) == 2) {
|
|
info->channels = channels;
|
|
info->rate = rate;
|
|
} else if (sscanf(c, "%u", &rate) == 1) {
|
|
info->rate = rate;
|
|
info->channels = 1;
|
|
} else
|
|
return -EINVAL;
|
|
|
|
pw_log_debug("a=rtpmap: rate: %d, ch: %d", info->rate, info->channels);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int parse_sdp_a_ssrc(struct impl *impl, char *c, struct sdp_info *info)
|
|
{
|
|
if (!spa_strstartswith(c, "a=ssrc:"))
|
|
return 0;
|
|
|
|
c += strlen("a=ssrc:");
|
|
if (!spa_atou32(c, &info->ssrc, 10))
|
|
return -EINVAL;
|
|
return 0;
|
|
}
|
|
|
|
static int parse_sdp_a_ptime(struct impl *impl, char *c, struct sdp_info *info)
|
|
{
|
|
if (!spa_strstartswith(c, "a=ptime:"))
|
|
return 0;
|
|
|
|
c += strlen("a=ptime:");
|
|
if (!spa_atof(c, &info->ptime))
|
|
return -EINVAL;
|
|
return 0;
|
|
}
|
|
|
|
static int parse_sdp_a_mediaclk(struct impl *impl, char *c, struct sdp_info *info)
|
|
{
|
|
if (!spa_strstartswith(c, "a=mediaclk:"))
|
|
return 0;
|
|
|
|
c += strlen("a=mediaclk:");
|
|
|
|
if (spa_strstartswith(c, "direct=")) {
|
|
int offset;
|
|
c += strlen("direct=");
|
|
if (sscanf(c, "%i", &offset) != 1)
|
|
return -EINVAL;
|
|
info->ts_offset = offset;
|
|
} else if (spa_strstartswith(c, "sender")) {
|
|
info->ts_offset = 0;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int parse_sdp_a_ts_refclk(struct impl *impl, char *c, struct sdp_info *info)
|
|
{
|
|
if (!spa_strstartswith(c, "a=ts-refclk:"))
|
|
return 0;
|
|
|
|
c += strlen("a=ts-refclk:");
|
|
info->ts_refclk = strdup(c);
|
|
return 0;
|
|
}
|
|
|
|
static int parse_sdp(struct impl *impl, char *sdp, struct sdp_info *info)
|
|
{
|
|
char *s = sdp;
|
|
int count = 0, res = 0;
|
|
size_t l;
|
|
|
|
spa_zero(*info);
|
|
|
|
while (*s) {
|
|
if ((l = strcspn(s, "\r\n")) < 2)
|
|
goto too_short;
|
|
|
|
s[l] = 0;
|
|
pw_log_debug("SDP line: %d: %s", count, s);
|
|
|
|
if (count++ == 0 && strcmp(s, "v=0") != 0)
|
|
goto invalid_version;
|
|
|
|
if (spa_strstartswith(s, "o="))
|
|
info->origin = strdup(&s[2]);
|
|
else if (spa_strstartswith(s, "s="))
|
|
info->session_name = strdup(&s[2]);
|
|
else if (spa_strstartswith(s, "c="))
|
|
res = parse_sdp_c(impl, s, info);
|
|
else if (spa_strstartswith(s, "m="))
|
|
res = parse_sdp_m(impl, s, info);
|
|
else if (spa_strstartswith(s, "a=rtpmap:"))
|
|
res = parse_sdp_a_rtpmap(impl, s, info);
|
|
else if (spa_strstartswith(s, "a=ssrc:"))
|
|
res = parse_sdp_a_ssrc(impl, s, info);
|
|
else if (spa_strstartswith(s, "a=ptime:"))
|
|
res = parse_sdp_a_ptime(impl, s, info);
|
|
else if (spa_strstartswith(s, "a=mediaclk:"))
|
|
res = parse_sdp_a_mediaclk(impl, s, info);
|
|
else if (spa_strstartswith(s, "a=ts-refclk:"))
|
|
res = parse_sdp_a_ts_refclk(impl, s, info);
|
|
else if (spa_strstartswith(s, "i="))
|
|
res = parse_sdp_i(impl, s, info);
|
|
|
|
if (res < 0)
|
|
goto error;
|
|
s += l + 1;
|
|
while (isspace(*s))
|
|
s++;
|
|
}
|
|
if (((struct sockaddr*) &info->dst_addr)->sa_family == AF_INET)
|
|
((struct sockaddr_in*) &info->dst_addr)->sin_port = htons(info->dst_port);
|
|
else
|
|
((struct sockaddr_in6*) &info->dst_addr)->sin6_port = htons(info->dst_port);
|
|
|
|
return 0;
|
|
too_short:
|
|
pw_log_warn("SDP: line starting with `%.6s...' too short", s);
|
|
clear_sdp_info(info);
|
|
return -EINVAL;
|
|
invalid_version:
|
|
pw_log_warn("SDP: invalid first version line `%*s'", (int)l, s);
|
|
clear_sdp_info(info);
|
|
return -EINVAL;
|
|
error:
|
|
pw_log_warn("SDP: error: %s", spa_strerror(res));
|
|
clear_sdp_info(info);
|
|
return res;
|
|
}
|
|
|
|
static int parse_sap(struct impl *impl, void *data, size_t len)
|
|
{
|
|
struct sap_header *header;
|
|
char *mime, *sdp;
|
|
struct sdp_info info;
|
|
struct session *sess;
|
|
int res;
|
|
size_t offs;
|
|
bool bye;
|
|
|
|
if (len < 8)
|
|
return -EINVAL;
|
|
|
|
header = (struct sap_header*) data;
|
|
if (header->v != 1)
|
|
return -EINVAL;
|
|
|
|
if (header->e)
|
|
return -ENOTSUP;
|
|
if (header->c)
|
|
return -ENOTSUP;
|
|
|
|
offs = header->a ? 20 : 8;
|
|
offs += header->auth_len * 4;
|
|
if (len <= offs)
|
|
return -EINVAL;
|
|
|
|
mime = SPA_PTROFF(data, offs, char);
|
|
if (spa_strstartswith(mime, "v=0")) {
|
|
sdp = mime;
|
|
mime = SAP_MIME_TYPE;
|
|
} else if (spa_streq(mime, SAP_MIME_TYPE))
|
|
sdp = SPA_PTROFF(mime, strlen(mime)+1, char);
|
|
else
|
|
return -EINVAL;
|
|
|
|
pw_log_debug("got SAP: %s %s", mime, sdp);
|
|
|
|
if ((res = parse_sdp(impl, sdp, &info)) < 0)
|
|
return res;
|
|
|
|
bye = header->t;
|
|
|
|
sess = session_find(impl, &info);
|
|
if (sess == NULL) {
|
|
if (!bye)
|
|
session_new(impl, &info);
|
|
} else {
|
|
if (bye)
|
|
session_free(sess);
|
|
else
|
|
session_touch(sess);
|
|
}
|
|
clear_sdp_info(&info);
|
|
return res;
|
|
}
|
|
|
|
static void
|
|
on_sap_io(void *data, int fd, uint32_t mask)
|
|
{
|
|
struct impl *impl = data;
|
|
int res;
|
|
|
|
if (mask & SPA_IO_IN) {
|
|
uint8_t buffer[MAX_SDP];
|
|
ssize_t len;
|
|
|
|
if ((len = recv(fd, buffer, sizeof(buffer), 0)) < 0) {
|
|
pw_log_warn("recv error: %m");
|
|
return;
|
|
}
|
|
if ((size_t)len >= sizeof(buffer))
|
|
return;
|
|
|
|
buffer[len] = 0;
|
|
if ((res = parse_sap(impl, buffer, len)) < 0)
|
|
pw_log_warn("error parsing SAP: %s", spa_strerror(res));
|
|
|
|
rearm_igmp_recovery_timer(impl);
|
|
}
|
|
}
|
|
|
|
static int start_sap(struct impl *impl)
|
|
{
|
|
int fd = -1, res = 0;
|
|
char addr[128] = "invalid";
|
|
|
|
pw_log_info("starting SAP send timer");
|
|
/* start_sap() might be called more than once. See the make_recv_socket()
|
|
* call below for why that can happen. In such a case, the timer was
|
|
* started already. The easiest way of handling it is to just cancel it.
|
|
* Such cases are not expected to occur often, so canceling and then
|
|
* adding the timer again is acceptable. */
|
|
pw_timer_queue_cancel(&impl->sap_send_timer);
|
|
if ((res = pw_timer_queue_add(impl->timer_queue, &impl->sap_send_timer,
|
|
NULL, SAP_INTERVAL_SEC * SPA_NSEC_PER_SEC,
|
|
on_sap_send_timer_event, impl)) < 0) {
|
|
pw_log_error("can't add SAP send timer: %s", spa_strerror(res));
|
|
goto error;
|
|
}
|
|
if ((fd = make_recv_socket(&impl->sap_addr, impl->sap_len, impl->ifname,
|
|
&(impl->igmp_recovery))) < 0) {
|
|
/* If make_recv_socket() tries to create a socket and join to a multicast
|
|
* group while the network interfaces are not ready yet to do so
|
|
* (usually because a network manager component is still setting up
|
|
* those network interfaces), ENODEV will be returned. This is essentially
|
|
* a race condition. There is no discernible way to be notified when the
|
|
* network interfaces are ready for that operation, so the next best
|
|
* approach is to essentially do a form of polling by retrying the
|
|
* start_sap() call after some time. The start_sap_retry_timer exists
|
|
* precisely for that purpose. This means that ENODEV is not treated as
|
|
* an error, but instead, it triggers the creation of that timer. */
|
|
if (fd == -ENODEV) {
|
|
pw_log_warn("failed to create receiver socket because network device "
|
|
"is not ready and present yet; will try again");
|
|
|
|
pw_timer_queue_cancel(&impl->start_sap_retry_timer);
|
|
/* Use a 1-second retry interval. The network interfaces
|
|
* are likely to be up and running then. */
|
|
pw_timer_queue_add(impl->timer_queue, &impl->start_sap_retry_timer,
|
|
NULL, 1 * SPA_NSEC_PER_SEC,
|
|
on_start_sap_retry_timer_event, impl);
|
|
|
|
/* It is important to return 0 in this case. Otherwise, the nonzero return
|
|
* value will later be propagated through the core as an error. */
|
|
res = 0;
|
|
goto finish;
|
|
} else {
|
|
pw_log_error("failed to create socket: %s", spa_strerror(-fd));
|
|
/* If ENODEV was returned earlier, and the start_sap_retry_timer
|
|
* was consequently created, but then a non-ENODEV error occurred,
|
|
* the timer must be stopped and removed. */
|
|
pw_timer_queue_cancel(&impl->start_sap_retry_timer);
|
|
res = fd;
|
|
goto error;
|
|
}
|
|
}
|
|
|
|
/* Cleanup the timer in case ENODEV occurred earlier, and this time,
|
|
* the socket creation succeeded. */
|
|
pw_timer_queue_cancel(&impl->start_sap_retry_timer);
|
|
|
|
pw_net_get_ip(&impl->sap_addr, addr, sizeof(addr), NULL, NULL);
|
|
pw_log_info("starting SAP listener on %s", addr);
|
|
impl->sap_source = pw_loop_add_io(impl->loop, fd,
|
|
SPA_IO_IN, true, on_sap_io, impl);
|
|
if (impl->sap_source == NULL) {
|
|
res = -errno;
|
|
goto error;
|
|
}
|
|
|
|
rearm_igmp_recovery_timer(impl);
|
|
|
|
finish:
|
|
return res;
|
|
|
|
error:
|
|
if (fd > 0)
|
|
close(fd);
|
|
goto finish;
|
|
}
|
|
|
|
static void node_event_info(void *data, const struct pw_node_info *info)
|
|
{
|
|
struct node *n = data;
|
|
struct impl *impl = n->impl;
|
|
const char *str;
|
|
|
|
if (info == NULL)
|
|
return;
|
|
|
|
// We only really want to do anything here if properties are updated,
|
|
// or if we don't have a session for this node already
|
|
if (!(info->change_mask & PW_NODE_CHANGE_MASK_PROPS) && n->session != NULL)
|
|
return;
|
|
|
|
n->info = pw_node_info_merge(n->info, info, true);
|
|
if (n->info == NULL)
|
|
return;
|
|
|
|
pw_log_debug("node %d changed", n->id);
|
|
|
|
if ((str = pw_properties_get(impl->props, "stream.rules")) == NULL)
|
|
str = DEFAULT_ANNOUNCE_RULES;
|
|
if (str != NULL) {
|
|
struct match_info minfo = {
|
|
.impl = impl,
|
|
.node = n,
|
|
};
|
|
pw_conf_match_rules(str, strlen(str), NAME, n->info->props,
|
|
rule_matched, &minfo);
|
|
}
|
|
}
|
|
|
|
static const struct pw_node_events node_events = {
|
|
PW_VERSION_NODE_EVENTS,
|
|
.info = node_event_info,
|
|
};
|
|
|
|
static void
|
|
proxy_removed(void *data)
|
|
{
|
|
struct node *n = data;
|
|
pw_log_debug("node %d removed", n->id);
|
|
pw_proxy_destroy(n->proxy);
|
|
}
|
|
|
|
static void
|
|
proxy_destroy(void *data)
|
|
{
|
|
struct node *n = data;
|
|
pw_log_debug("node %d destroy", n->id);
|
|
spa_hook_remove(&n->node_listener);
|
|
spa_hook_remove(&n->proxy_listener);
|
|
n->proxy = NULL;
|
|
if (n->session != NULL) {
|
|
session_free(n->session);
|
|
n->session = NULL;
|
|
}
|
|
if (n->info) {
|
|
pw_node_info_free(n->info);
|
|
n->info = NULL;
|
|
}
|
|
}
|
|
|
|
static const struct pw_proxy_events proxy_events = {
|
|
PW_VERSION_PROXY_EVENTS,
|
|
.removed = proxy_removed,
|
|
.destroy = proxy_destroy,
|
|
};
|
|
|
|
static void registry_event_global(void *data, uint32_t id,
|
|
uint32_t permissions, const char *type, uint32_t version,
|
|
const struct spa_dict *props)
|
|
{
|
|
struct impl *impl = data;
|
|
struct pw_proxy *proxy;
|
|
struct node *node;
|
|
|
|
if (!spa_streq(type, PW_TYPE_INTERFACE_Node))
|
|
return;
|
|
|
|
proxy = pw_registry_bind(impl->registry, id, type, PW_VERSION_NODE, sizeof(struct node));
|
|
if (proxy == NULL)
|
|
return;
|
|
|
|
node = pw_proxy_get_user_data(proxy);
|
|
node->impl = impl;
|
|
node->id = id;
|
|
node->proxy = proxy;
|
|
|
|
pw_proxy_add_object_listener(proxy,
|
|
&node->node_listener, &node_events, node);
|
|
pw_proxy_add_listener(proxy,
|
|
&node->proxy_listener, &proxy_events, node);
|
|
}
|
|
|
|
static const struct pw_registry_events registry_events = {
|
|
PW_VERSION_REGISTRY_EVENTS,
|
|
.global = registry_event_global,
|
|
};
|
|
|
|
static void core_destroy(void *d)
|
|
{
|
|
struct impl *impl = d;
|
|
spa_hook_remove(&impl->core_listener);
|
|
impl->core = NULL;
|
|
pw_impl_module_schedule_destroy(impl->module);
|
|
}
|
|
|
|
static const struct pw_proxy_events core_proxy_events = {
|
|
.destroy = core_destroy,
|
|
};
|
|
|
|
static void impl_destroy(struct impl *impl)
|
|
{
|
|
struct session *sess;
|
|
|
|
spa_list_consume(sess, &impl->sessions, link)
|
|
session_free(sess);
|
|
|
|
if (impl->registry) {
|
|
spa_hook_remove(&impl->registry_listener);
|
|
pw_proxy_destroy((struct pw_proxy*)impl->registry);
|
|
impl->registry = NULL;
|
|
}
|
|
if (impl->core && impl->do_disconnect)
|
|
pw_core_disconnect(impl->core);
|
|
|
|
pw_timer_queue_cancel(&impl->sap_send_timer);
|
|
pw_timer_queue_cancel(&impl->start_sap_retry_timer);
|
|
pw_timer_queue_cancel(&impl->igmp_recovery.timer);
|
|
if (impl->sap_source)
|
|
pw_loop_destroy_source(impl->loop, impl->sap_source);
|
|
|
|
if (impl->sap_fd != -1)
|
|
close(impl->sap_fd);
|
|
if (impl->ptp_fd != -1)
|
|
close(impl->ptp_fd);
|
|
|
|
pw_properties_free(impl->props);
|
|
|
|
free(impl->extra_attrs_preamble);
|
|
free(impl->extra_attrs_end);
|
|
|
|
free(impl->ptp_mgmt_socket_path);
|
|
free(impl->ifname);
|
|
free(impl);
|
|
}
|
|
|
|
static void module_destroy(void *d)
|
|
{
|
|
struct impl *impl = d;
|
|
spa_hook_remove(&impl->module_listener);
|
|
impl_destroy(impl);
|
|
}
|
|
|
|
static const struct pw_impl_module_events module_events = {
|
|
PW_VERSION_IMPL_MODULE_EVENTS,
|
|
.destroy = module_destroy,
|
|
};
|
|
|
|
static void on_core_error(void *d, uint32_t id, int seq, int res, const char *message)
|
|
{
|
|
struct impl *impl = d;
|
|
|
|
pw_log_error("error id:%u seq:%d res:%d (%s): %s",
|
|
id, seq, res, spa_strerror(res), message);
|
|
|
|
if (id == PW_ID_CORE && res == -EPIPE)
|
|
pw_impl_module_schedule_destroy(impl->module);
|
|
}
|
|
|
|
static const struct pw_core_events core_events = {
|
|
PW_VERSION_CORE_EVENTS,
|
|
.error = on_core_error,
|
|
};
|
|
|
|
SPA_EXPORT
|
|
int pipewire__module_init(struct pw_impl_module *module, const char *args)
|
|
{
|
|
struct pw_context *context = pw_impl_module_get_context(module);
|
|
struct impl *impl;
|
|
struct pw_properties *props = NULL;
|
|
uint32_t port;
|
|
const char *str;
|
|
int res = 0;
|
|
|
|
PW_LOG_TOPIC_INIT(mod_topic);
|
|
|
|
impl = calloc(1, sizeof(struct impl));
|
|
if (impl == NULL)
|
|
return -errno;
|
|
|
|
impl->sap_fd = -1;
|
|
impl->ptp_fd = -1;
|
|
spa_list_init(&impl->sessions);
|
|
|
|
impl->igmp_recovery.socket_fd = -1;
|
|
impl->igmp_recovery.if_index = -1;
|
|
|
|
if (args == NULL)
|
|
args = "";
|
|
|
|
props = pw_properties_new_string(args);
|
|
if (props == NULL) {
|
|
res = -errno;
|
|
pw_log_error( "can't create properties: %m");
|
|
goto out;
|
|
}
|
|
impl->props = props;
|
|
|
|
impl->module = module;
|
|
impl->loop = pw_context_get_main_loop(context);
|
|
impl->timer_queue = pw_context_get_timer_queue(context);
|
|
|
|
str = pw_properties_get(props, "local.ifname");
|
|
impl->ifname = str ? strdup(str) : NULL;
|
|
|
|
str = pw_properties_get(props, "ptp.management-socket");
|
|
impl->ptp_mgmt_socket_path = str ? strdup(str) : NULL;
|
|
|
|
// TODO: support UDP management access as well
|
|
if (impl->ptp_mgmt_socket_path)
|
|
impl->ptp_fd = make_unix_ptp_mgmt_socket(impl->ptp_mgmt_socket_path);
|
|
|
|
if ((str = pw_properties_get(props, "sap.ip")) == NULL)
|
|
str = DEFAULT_SAP_IP;
|
|
port = pw_properties_get_uint32(props, "sap.port", DEFAULT_SAP_PORT);
|
|
if ((res = pw_net_parse_address(str, port, &impl->sap_addr, &impl->sap_len)) < 0) {
|
|
pw_log_error("invalid sap.ip %s: %s", str, spa_strerror(res));
|
|
goto out;
|
|
}
|
|
impl->cleanup_interval = pw_properties_get_uint32(impl->props,
|
|
"sap.cleanup.sec", DEFAULT_CLEANUP_SEC);
|
|
|
|
/* We will use half of the cleanup interval for IGMP deadline, minimum 1 second */
|
|
impl->igmp_recovery.deadline = SPA_MAX(impl->cleanup_interval / 2, 1u);
|
|
pw_log_info("using IGMP deadline of %" PRIu32 " second(s)",
|
|
impl->igmp_recovery.deadline);
|
|
|
|
impl->ttl = pw_properties_get_uint32(props, "net.ttl", DEFAULT_TTL);
|
|
impl->mcast_loop = pw_properties_get_bool(props, "net.loop", DEFAULT_LOOP);
|
|
impl->max_sessions = pw_properties_get_uint32(props, "sap.max-sessions", DEFAULT_MAX_SESSIONS);
|
|
|
|
impl->extra_attrs_preamble = NULL;
|
|
impl->extra_attrs_end = NULL;
|
|
char buffer[MAX_SDP];
|
|
struct spa_strbuf buf;
|
|
if ((str = pw_properties_get(props, "sap.preamble-extra")) != NULL) {
|
|
spa_strbuf_init(&buf, buffer, sizeof(buffer));
|
|
struct spa_json it[1];
|
|
char line[256];
|
|
|
|
if (spa_json_begin_array_relax(&it[0], str, strlen(str)) > 0) {
|
|
while (spa_json_get_string(&it[0], line, sizeof(line)) > 0)
|
|
spa_strbuf_append(&buf, "%s\n", line);
|
|
}
|
|
impl->extra_attrs_preamble = strdup(buffer);
|
|
}
|
|
if ((str = pw_properties_get(props, "sap.end-extra")) != NULL) {
|
|
spa_strbuf_init(&buf, buffer, sizeof(buffer));
|
|
struct spa_json it[1];
|
|
char line[256];
|
|
|
|
if (spa_json_begin_array_relax(&it[0], str, strlen(str)) > 0) {
|
|
while (spa_json_get_string(&it[0], line, sizeof(line)) > 0)
|
|
spa_strbuf_append(&buf, "%s\n", line);
|
|
}
|
|
impl->extra_attrs_end = strdup(buffer);
|
|
}
|
|
|
|
impl->core = pw_context_get_object(context, PW_TYPE_INTERFACE_Core);
|
|
if (impl->core == NULL) {
|
|
str = pw_properties_get(props, PW_KEY_REMOTE_NAME);
|
|
impl->core = pw_context_connect(context,
|
|
pw_properties_new(
|
|
PW_KEY_REMOTE_NAME, str,
|
|
NULL),
|
|
0);
|
|
impl->do_disconnect = true;
|
|
}
|
|
if (impl->core == NULL) {
|
|
res = -errno;
|
|
pw_log_error("can't connect: %m");
|
|
goto out;
|
|
}
|
|
|
|
pw_proxy_add_listener((struct pw_proxy*)impl->core,
|
|
&impl->core_proxy_listener,
|
|
&core_proxy_events, impl);
|
|
pw_core_add_listener(impl->core,
|
|
&impl->core_listener,
|
|
&core_events, impl);
|
|
|
|
if ((res = start_sap(impl)) < 0)
|
|
goto out;
|
|
|
|
impl->registry = pw_core_get_registry(impl->core, PW_VERSION_REGISTRY, 0);
|
|
pw_registry_add_listener(impl->registry, &impl->registry_listener,
|
|
®istry_events, impl);
|
|
|
|
pw_impl_module_add_listener(module, &impl->module_listener, &module_events, impl);
|
|
|
|
pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_info));
|
|
|
|
pw_log_info("Successfully loaded module-rtp-sap");
|
|
|
|
return 0;
|
|
out:
|
|
impl_destroy(impl);
|
|
return res;
|
|
}
|