[asterisk-commits] file: branch file/chan_jingle2 r365454 - in /team/file/chan_jingle2: channels...
SVN commits to the Asterisk project
asterisk-commits at lists.digium.com
Sun May 6 09:42:25 CDT 2012
Author: file
Date: Sun May 6 09:42:21 2012
New Revision: 365454
URL: http://svnview.digium.com/svn/asterisk?view=rev&rev=365454
Log:
Import STUN/TURN/ICE support. This was developed standalone and is being reviewed as such. It is here simply because it is a dependency.
Modified:
team/file/chan_jingle2/channels/chan_sip.c
team/file/chan_jingle2/configs/rtp.conf.sample
team/file/chan_jingle2/include/asterisk/rtp_engine.h
team/file/chan_jingle2/main/rtp_engine.c
team/file/chan_jingle2/res/Makefile
team/file/chan_jingle2/res/res_rtp_asterisk.c
Modified: team/file/chan_jingle2/channels/chan_sip.c
URL: http://svnview.digium.com/svn/asterisk/team/file/chan_jingle2/channels/chan_sip.c?view=diff&rev=365454&r1=365453&r2=365454
==============================================================================
--- team/file/chan_jingle2/channels/chan_sip.c (original)
+++ team/file/chan_jingle2/channels/chan_sip.c Sun May 6 09:42:21 2012
@@ -1320,10 +1320,13 @@
static int process_sdp_o(const char *o, struct sip_pvt *p);
static int process_sdp_c(const char *c, struct ast_sockaddr *addr);
static int process_sdp_a_sendonly(const char *a, int *sendonly);
+static int process_sdp_a_ice(const char *a, struct sip_pvt *p, struct ast_rtp_instance *instance);
static int process_sdp_a_audio(const char *a, struct sip_pvt *p, struct ast_rtp_codecs *newaudiortp, int *last_rtpmap_codec);
static int process_sdp_a_video(const char *a, struct sip_pvt *p, struct ast_rtp_codecs *newvideortp, int *last_rtpmap_codec);
static int process_sdp_a_text(const char *a, struct sip_pvt *p, struct ast_rtp_codecs *newtextrtp, char *red_fmtp, int *red_num_gen, int *red_data_pt, int *last_rtpmap_codec);
static int process_sdp_a_image(const char *a, struct sip_pvt *p);
+static void add_ice_to_sdp(struct ast_rtp_instance *instance, struct ast_str **a_buf, int debug);
+static void start_ice(struct ast_rtp_instance *instance, int debug);
static void add_codec_to_sdp(const struct sip_pvt *p, struct ast_format *codec,
struct ast_str **m_buf, struct ast_str **a_buf,
int debug, int *min_packet_size);
@@ -9155,6 +9158,12 @@
processed = TRUE;
else if (process_sdp_a_image(value, p))
processed = TRUE;
+
+ /* We can't stop processing after each of these since it may apply to each */
+ process_sdp_a_ice(value, p, p->rtp);
+ process_sdp_a_ice(value, p, p->vrtp);
+ process_sdp_a_ice(value, p, p->trtp);
+
break;
}
@@ -9355,6 +9364,8 @@
if (audio) {
if (process_sdp_a_sendonly(value, &sendonly)) {
processed = TRUE;
+ } else if (process_sdp_a_ice(value, p, p->rtp)) {
+ processed = TRUE;
} else if (!processed_crypto && process_crypto(p, p->rtp, &p->srtp, value)) {
processed_crypto = TRUE;
processed = TRUE;
@@ -9365,6 +9376,8 @@
/* Video specific scanning */
else if (video) {
if (process_sdp_a_sendonly(value, &vsendonly)) {
+ processed = TRUE;
+ } else if (process_sdp_a_ice(value, p, p->vrtp)) {
processed = TRUE;
} else if (!processed_crypto && process_crypto(p, p->vrtp, &p->vsrtp, value)) {
processed_crypto = TRUE;
@@ -9377,6 +9390,8 @@
else if (text) {
if (process_sdp_a_text(value, p, &newtextrtp, red_fmtp, &red_num_gen, red_data_pt, &last_rtpmap_codec)) {
processed = TRUE;
+ } else if (process_sdp_a_ice(value, p, p->trtp)) {
+ processed = TRUE;
} else if (!processed_crypto && process_crypto(p, p->trtp, &p->tsrtp, value)) {
processed_crypto = TRUE;
processed = TRUE;
@@ -9505,6 +9520,7 @@
/* Setup audio address and port */
if (p->rtp) {
if (portno > 0) {
+ start_ice(p->rtp, debug);
ast_sockaddr_set_port(sa, portno);
ast_rtp_instance_set_remote_address(p->rtp, sa);
if (debug) {
@@ -9552,6 +9568,7 @@
/* Setup video address and port */
if (p->vrtp) {
if (vportno > 0) {
+ start_ice(p->vrtp, debug);
ast_sockaddr_set_port(vsa, vportno);
ast_rtp_instance_set_remote_address(p->vrtp, vsa);
if (debug) {
@@ -9569,6 +9586,7 @@
/* Setup text address and port */
if (p->trtp) {
if (tportno > 0) {
+ start_ice(p->trtp, debug);
ast_sockaddr_set_port(tsa, tportno);
ast_rtp_instance_set_remote_address(p->trtp, tsa);
if (debug) {
@@ -9826,6 +9844,61 @@
*sendonly = 0;
found = TRUE;
}
+ return found;
+}
+
+static int process_sdp_a_ice(const char *a, struct sip_pvt *p, struct ast_rtp_instance *instance)
+{
+ struct ast_rtp_engine_ice *ice;
+ int found = FALSE;
+ char ufrag[256], pwd[256], foundation[32], transport[4], address[24], cand_type[6], relay_address[24] = "";
+ struct ast_rtp_engine_ice_candidate candidate;
+ int port, relay_port = 0;
+
+ if (!instance || !(ice = ast_rtp_instance_get_ice(instance))) {
+ return found;
+ }
+
+ if (sscanf(a, "ice-ufrag: %255s", ufrag) == 1) {
+ ice->set_authentication(instance, ufrag, NULL);
+ found = TRUE;
+ } else if (sscanf(a, "ice-pwd: %255s", pwd) == 1) {
+ ice->set_authentication(instance, NULL, pwd);
+ found = TRUE;
+ } else if (sscanf(a, "candidate:%31s %30u %3s %30u %23s %30u typ %5s %*s %23s %*s %30u", foundation, &candidate.id, transport, &candidate.priority,
+ address, &port, cand_type, relay_address, &relay_port) >= 7) {
+ candidate.foundation = foundation;
+ candidate.transport = transport;
+
+ ast_sockaddr_parse(&candidate.address, address, PARSE_PORT_FORBID);
+ ast_sockaddr_set_port(&candidate.address, port);
+
+ if (!strcasecmp(cand_type, "host")) {
+ candidate.type = AST_RTP_ICE_CANDIDATE_TYPE_HOST;
+ } else if (!strcasecmp(cand_type, "srflx")) {
+ candidate.type = AST_RTP_ICE_CANDIDATE_TYPE_SRFLX;
+ } else if (!strcasecmp(cand_type, "relay")) {
+ candidate.type = AST_RTP_ICE_CANDIDATE_TYPE_RELAYED;
+ } else {
+ return found;
+ }
+
+ if (!ast_strlen_zero(relay_address)) {
+ ast_sockaddr_parse(&candidate.relay_address, relay_address, PARSE_PORT_FORBID);
+ }
+
+ if (relay_port) {
+ ast_sockaddr_set_port(&candidate.relay_address, relay_port);
+ }
+
+ ice->add_remote_candidate(instance, &candidate);
+
+ found = TRUE;
+ } else if (!strcasecmp(a, "ice-lite")) {
+ ice->ice_lite(instance);
+ found = TRUE;
+ }
+
return found;
}
@@ -11336,6 +11409,67 @@
return 0;
}
+/*! \brief Add ICE attributes to SDP */
+static void add_ice_to_sdp(struct ast_rtp_instance *instance, struct ast_str **a_buf, int debug)
+{
+ struct ast_rtp_engine_ice *ice = ast_rtp_instance_get_ice(instance);
+ const char *username, *password;
+ struct ao2_container *candidates;
+ struct ao2_iterator i;
+ struct ast_rtp_engine_ice_candidate *candidate;
+
+ /* If no ICE support is present we can't very well add the attributes */
+ if (!ice || !(candidates = ice->get_local_candidates(instance))) {
+ return;
+ }
+
+ if ((username = ice->get_ufrag(instance))) {
+ ast_str_append(a_buf, 0, "a=ice-ufrag:%s\r\n", username);
+ }
+ if ((password = ice->get_password(instance))) {
+ ast_str_append(a_buf, 0, "a=ice-pwd:%s\r\n", password);
+ }
+
+ i = ao2_iterator_init(candidates, 0);
+
+ while ((candidate = ao2_iterator_next(&i))) {
+ ast_str_append(a_buf, 0, "a=candidate:%s %d %s %d ", candidate->foundation, candidate->id, candidate->transport, candidate->priority);
+ ast_str_append(a_buf, 0, "%s ", ast_sockaddr_stringify_host(&candidate->address));
+ ast_str_append(a_buf, 0, "%s typ ", ast_sockaddr_stringify_port(&candidate->address));
+
+ if (candidate->type == AST_RTP_ICE_CANDIDATE_TYPE_HOST) {
+ ast_str_append(a_buf, 0, "host");
+ } else if (candidate->type == AST_RTP_ICE_CANDIDATE_TYPE_SRFLX) {
+ ast_str_append(a_buf, 0, "srflx");
+ } else if (candidate->type == AST_RTP_ICE_CANDIDATE_TYPE_RELAYED) {
+ ast_str_append(a_buf, 0, "relay");
+ }
+
+ if (!ast_sockaddr_isnull(&candidate->relay_address)) {
+ ast_str_append(a_buf, 0, " raddr %s ", ast_sockaddr_stringify_host(&candidate->relay_address));
+ ast_str_append(a_buf, 0, "rport %s", ast_sockaddr_stringify_port(&candidate->relay_address));
+ }
+
+ ast_str_append(a_buf, 0, "\r\n");
+ }
+
+ ao2_iterator_destroy(&i);
+
+ ao2_ref(candidates, -1);
+}
+
+/*! \brief Start ICE negotiation on an RTP instance */
+static void start_ice(struct ast_rtp_instance *instance, int debug)
+{
+ struct ast_rtp_engine_ice *ice = ast_rtp_instance_get_ice(instance);
+
+ if (!ice) {
+ return;
+ }
+
+ ice->start(instance);
+}
+
/*! \brief Add codec offer to SDP offer/answer body in INVITE or 200 OK */
static void add_codec_to_sdp(const struct sip_pvt *p,
struct ast_format *format,
@@ -11806,6 +11940,10 @@
if (debug) {
ast_verbose("Video is at %s\n", ast_sockaddr_stringify(&vdest));
}
+
+ if (!doing_directmedia) {
+ add_ice_to_sdp(p->vrtp, &a_video, debug);
+ }
}
/* Ok, we need text. Let's add what we need for text and set codecs.
@@ -11818,6 +11956,10 @@
t_a_crypto ? "SAVP" : "AVP");
if (debug) { /* XXX should I use tdest below ? */
ast_verbose("Text is at %s\n", ast_sockaddr_stringify(&taddr));
+ }
+
+ if (!doing_directmedia) {
+ add_ice_to_sdp(p->trtp, &a_text, debug);
}
}
@@ -11912,6 +12054,10 @@
/* XXX don't think you can have ptime for text */
if (min_text_packet_size)
ast_str_append(&a_text, 0, "a=ptime:%d\r\n", min_text_packet_size);
+
+ if (!doing_directmedia) {
+ add_ice_to_sdp(p->rtp, &a_audio, debug);
+ }
if (m_audio->len - m_audio->used < 2 || m_video->len - m_video->used < 2 ||
m_text->len - m_text->used < 2 || a_text->len - a_text->used < 2 ||
Modified: team/file/chan_jingle2/configs/rtp.conf.sample
URL: http://svnview.digium.com/svn/asterisk/team/file/chan_jingle2/configs/rtp.conf.sample?view=diff&rev=365454&r1=365453&r2=365454
==============================================================================
--- team/file/chan_jingle2/configs/rtp.conf.sample (original)
+++ team/file/chan_jingle2/configs/rtp.conf.sample Sun May 6 09:42:21 2012
@@ -32,3 +32,23 @@
; if rtp packets are dropped from one or both ends after a call is
; connected. This option is set to 4 by default.
; probation=8
+;
+; Whether to enable or disable ICE support. This option is enabled by default.
+; icesupport=false
+;
+; Address to use for the STUN server when determining the external IP address and port
+; an RTP session can be reached at. This option is disabled by default.
+; stunaddr=
+;
+; Address to use for the TURN relay server when creating a TURN relay session. This option
+; is disabled by default.
+; turnaddr=
+;
+; Port used to contact the TURN relay server on. This option is set to 34780 by default.
+; turnport=34780
+;
+; Username used to authenticate with TURN relay server.
+; turnusername=
+;
+; Password used to authenticate with TURN relay server.
+; turnpassword=
Modified: team/file/chan_jingle2/include/asterisk/rtp_engine.h
URL: http://svnview.digium.com/svn/asterisk/team/file/chan_jingle2/include/asterisk/rtp_engine.h?view=diff&rev=365454&r1=365453&r2=365454
==============================================================================
--- team/file/chan_jingle2/include/asterisk/rtp_engine.h (original)
+++ team/file/chan_jingle2/include/asterisk/rtp_engine.h Sun May 6 09:42:21 2012
@@ -309,6 +309,42 @@
if (stat == combined) { \
return 0; \
}
+
+/*! \brief ICE candidate types */
+enum ast_rtp_ice_candidate_type {
+ AST_RTP_ICE_CANDIDATE_TYPE_HOST, /*!< ICE host candidate. A host candidate represents the actual local transport address in the host. */
+ AST_RTP_ICE_CANDIDATE_TYPE_SRFLX, /*!< ICE server reflexive candidate, which represents the public mapped address of the local address. */
+ AST_RTP_ICE_CANDIDATE_TYPE_RELAYED, /*!< ICE relayed candidate, which represents the address allocated in TURN server. */
+};
+
+/*! \brief Structure for an ICE candidate */
+struct ast_rtp_engine_ice_candidate {
+ char *foundation; /*!< Foundation identifier */
+ unsigned int id; /*!< Component identifier */
+ char *transport; /*!< Transport for the media */
+ int priority; /*!< Priority which is used if multiple candidates can be used */
+ struct ast_sockaddr address; /*!< Address of the candidate */
+ struct ast_sockaddr relay_address; /*!< Relay address for the candidate */
+ enum ast_rtp_ice_candidate_type type; /*!< Type of candidate */
+};
+
+/*! \brief Structure that represents the optional ICE support within an RTP engine */
+struct ast_rtp_engine_ice {
+ /*! Callback for setting received authentication information */
+ void (*set_authentication)(struct ast_rtp_instance *instance, const char *ufrag, const char *password);
+ /*! Callback for adding a remote candidate */
+ void (*add_remote_candidate)(struct ast_rtp_instance *instance, const struct ast_rtp_engine_ice_candidate *candidate);
+ /*! Callback for starting ICE negotiation */
+ void (*start)(struct ast_rtp_instance *instance);
+ /*! Callback for getting local username */
+ const char *(*get_ufrag)(struct ast_rtp_instance *instance);
+ /*! Callback for getting local password */
+ const char *(*get_password)(struct ast_rtp_instance *instance);
+ /*! Callback for getting local candidates */
+ struct ao2_container *(*get_local_candidates)(struct ast_rtp_instance *instance);
+ /*! Callback for telling the ICE support that it is talking to an ice-lite implementation */
+ void (*ice_lite)(struct ast_rtp_instance *instance);
+};
/*! Structure that represents an RTP stack (engine) */
struct ast_rtp_engine {
@@ -381,6 +417,8 @@
void (*available_formats)(struct ast_rtp_instance *instance, struct ast_format_cap *to_endpoint, struct ast_format_cap *to_asterisk, struct ast_format_cap *result);
/*! Callback to send CNG */
int (*sendcng)(struct ast_rtp_instance *instance, int level);
+ /*! Callback to pointer for optional ICE support */
+ struct ast_rtp_engine_ice *ice;
/*! Linked list information */
AST_RWLIST_ENTRY(ast_rtp_engine) entry;
};
@@ -1891,6 +1929,16 @@
* attribute interface is unloaded, this function must be called to notify the rtp_engine. */
int ast_rtp_engine_unload_format(const struct ast_format *format);
+/*!
+ * \brief Obtain a pointer to the ICE support present on an RTP instance
+ *
+ * \param instance the RTP instance
+ *
+ * \retval ICE support if present
+ * \retval NULL if no ICE support available
+ */
+struct ast_rtp_engine_ice *ast_rtp_instance_get_ice(struct ast_rtp_instance *instance);
+
#if defined(__cplusplus) || defined(c_plusplus)
}
#endif
Modified: team/file/chan_jingle2/main/rtp_engine.c
URL: http://svnview.digium.com/svn/asterisk/team/file/chan_jingle2/main/rtp_engine.c?view=diff&rev=365454&r1=365453&r2=365454
==============================================================================
--- team/file/chan_jingle2/main/rtp_engine.c (original)
+++ team/file/chan_jingle2/main/rtp_engine.c Sun May 6 09:42:21 2012
@@ -1886,6 +1886,11 @@
return -1;
}
+struct ast_rtp_engine_ice *ast_rtp_instance_get_ice(struct ast_rtp_instance *instance)
+{
+ return instance->engine->ice;
+}
+
static void set_next_mime_type(const struct ast_format *format, int rtp_code, char *type, char *subtype, unsigned int sample_rate)
{
int x = mime_types_len;
Modified: team/file/chan_jingle2/res/Makefile
URL: http://svnview.digium.com/svn/asterisk/team/file/chan_jingle2/res/Makefile?view=diff&rev=365454&r1=365453&r2=365454
==============================================================================
--- team/file/chan_jingle2/res/Makefile (original)
+++ team/file/chan_jingle2/res/Makefile Sun May 6 09:42:21 2012
@@ -68,3 +68,17 @@
clean::
rm -f snmp/*.o snmp/*.i ael/*.o ael/*.i ais/*.o ais/*.i
+ $(MAKE) -C pjproject realclean
+
+pjproject/build.mak:
+ cd pjproject && ./configure AR="" CFLAGS=-fPIC --disable-floating-point --disable-sound --disable-oss --disable-small-filterng --disable-large-filterng --disable-speex-aec --disable-l16-codec --disable-gsm-codec --disable-g722-codec --disable-g7221-codec --disable-speex-codec --disable-ilbc-codec --disable-g711-codec
+
+include pjproject/build.mak
+
+res_rtp_asterisk.o: pjproject/build.mak $(PJ_LIB_FILES)
+res_rtp_asterisk.o: _ASTCFLAGS+=$(PJ_CFLAGS)
+res_rtp_asterisk.so: _ASTLDFLAGS+=$(PJ_LDFLAGS)
+res_rtp_asterisk.so: LIBS+=$(PJ_LDLIBS)
+
+$(PJ_LIB_FILES):
+ cd pjproject && make dep && make
Modified: team/file/chan_jingle2/res/res_rtp_asterisk.c
URL: http://svnview.digium.com/svn/asterisk/team/file/chan_jingle2/res/res_rtp_asterisk.c?view=diff&rev=365454&r1=365453&r2=365454
==============================================================================
--- team/file/chan_jingle2/res/res_rtp_asterisk.c (original)
+++ team/file/chan_jingle2/res/res_rtp_asterisk.c Sun May 6 09:42:21 2012
@@ -40,6 +40,12 @@
#include <signal.h>
#include <fcntl.h>
+#undef bzero
+#define bzero bzero
+#include "pjlib.h"
+#include "pjlib-util.h"
+#include "pjnath.h"
+
#include "asterisk/stun.h"
#include "asterisk/pbx.h"
#include "asterisk/frame.h"
@@ -66,6 +72,10 @@
#define MINIMUM_RTP_PORT 1024 /*!< Minimum port number to accept */
#define MAXIMUM_RTP_PORT 65535 /*!< Maximum port number to accept */
+
+#define DEFAULT_TURN_PORT 34780
+
+#define TURN_ALLOCATION_WAIT_TIME 2000
#define RTCP_PT_FUR 192
#define RTCP_PT_SR 200
@@ -100,6 +110,30 @@
#endif
static int strictrtp; /*< Only accept RTP frames from a defined source. If we receive an indication of a changing source, enter learning mode. */
static int learning_min_sequential; /*< Number of sequential RTP frames needed from a single source during learning mode to accept new source. */
+static int icesupport;
+static struct sockaddr_in stunaddr;
+static pj_str_t turnaddr;
+static int turnport;
+static pj_str_t turnusername;
+static pj_str_t turnpassword;
+
+/*! \brief Pool factory used by pjlib to allocate memory. */
+static pj_caching_pool cachingpool;
+
+/*! \brief Pool used by pjlib functions which require memory allocation. */
+static pj_pool_t *pool;
+
+/*! \brief I/O queue for TURN relay traffic */
+static pj_ioqueue_t *ioqueue;
+
+/*! \brief Timer heap for ICE and TURN stuff */
+static pj_timer_heap_t *timerheap;
+
+/*! \brief Worker thread for ICE/TURN */
+static pj_thread_t *thread;
+
+/*! \brief Notification that the ICE/TURN worker thread should stop */
+static int worker_terminate;
enum strict_rtp_state {
STRICT_RTP_OPEN = 0, /*! No RTP packets should be dropped, all sources accepted */
@@ -113,6 +147,14 @@
#define FLAG_NAT_INACTIVE_NOWARN (1 << 1)
#define FLAG_NEED_MARKER_BIT (1 << 3)
#define FLAG_DTMF_COMPENSATE (1 << 4)
+
+#define TRANSPORT_SOCKET_RTP 1
+#define TRANSPORT_SOCKET_RTCP 2
+#define TRANSPORT_TURN_RTP 3
+#define TRANSPORT_TURN_RTCP 4
+
+#define COMPONENT_RTP 1
+#define COMPONENT_RTCP 2
/*! \brief RTP session description */
struct ast_rtp {
@@ -187,6 +229,23 @@
int learning_probation; /*!< Sequential packets untill source is valid */
struct rtp_red *red;
+
+ pj_ice_sess *ice; /*!< ICE session */
+ pj_turn_sock *turn_rtp; /*!< RTP TURN relay */
+ pj_turn_sock *turn_rtcp; /*!< RTCP TURN relay */
+ ast_mutex_t lock; /*!< Lock for synchronization purposes */
+ pj_turn_state_t turn_state; /*!< Current state of the TURN relay session */
+ ast_cond_t cond; /*!< Condition for signaling */
+ unsigned int passthrough:1; /*!< Bit to indicate that the received packet should be passed through */
+
+ char remote_ufrag[256]; /*!< The remote ICE username */
+ char remote_passwd[256]; /*!< The remote ICE password */
+
+ char local_ufrag[256]; /*!< The local ICE username */
+ char local_passwd[256]; /*!< The local ICE password */
+
+ struct ao2_container *local_candidates; /*!< The local ICE candidates */
+ struct ao2_container *remote_candidates; /*!< The remote ICE candidates */
};
/*!
@@ -293,6 +352,238 @@
static int ast_rtp_qos_set(struct ast_rtp_instance *instance, int tos, int cos, const char* desc);
static int ast_rtp_sendcng(struct ast_rtp_instance *instance, int level);
+/*! \brief Destructor for locally created ICE candidates */
+static void ast_rtp_ice_candidate_destroy(void *obj)
+{
+ struct ast_rtp_engine_ice_candidate *candidate = obj;
+
+ if (candidate->foundation) {
+ ast_free(candidate->foundation);
+ }
+
+ if (candidate->transport) {
+ ast_free(candidate->transport);
+ }
+}
+
+static void ast_rtp_ice_set_authentication(struct ast_rtp_instance *instance, const char *ufrag, const char *password)
+{
+ struct ast_rtp *rtp = ast_rtp_instance_get_data(instance);
+
+ if (!ast_strlen_zero(ufrag)) {
+ ast_copy_string(rtp->remote_ufrag, ufrag, sizeof(rtp->remote_ufrag));
+ }
+
+ if (!ast_strlen_zero(password)) {
+ ast_copy_string(rtp->remote_passwd, password, sizeof(rtp->remote_passwd));
+ }
+}
+
+static void ast_rtp_ice_add_remote_candidate(struct ast_rtp_instance *instance, const struct ast_rtp_engine_ice_candidate *candidate)
+{
+ struct ast_rtp *rtp = ast_rtp_instance_get_data(instance);
+ struct ast_rtp_engine_ice_candidate *remote_candidate;
+
+ if (!rtp->remote_candidates && !(rtp->remote_candidates = ao2_container_alloc(1, NULL, NULL))) {
+ return;
+ }
+
+ if (!(remote_candidate = ao2_alloc(sizeof(*remote_candidate), ast_rtp_ice_candidate_destroy))) {
+ return;
+ }
+
+ remote_candidate->foundation = ast_strdup(candidate->foundation);
+ remote_candidate->id = candidate->id;
+ remote_candidate->transport = ast_strdup(candidate->transport);
+ remote_candidate->priority = candidate->priority;
+ ast_sockaddr_copy(&remote_candidate->address, &candidate->address);
+ ast_sockaddr_copy(&remote_candidate->relay_address, &candidate->relay_address);
+ remote_candidate->type = candidate->type;
+
+ ao2_link(rtp->remote_candidates, remote_candidate);
+ ao2_ref(remote_candidate, -1);
+}
+
+/*! \brief Function used to check if the calling thread is registered with pjlib. If it is not it will be registered. */
+static void pj_thread_register_check(void)
+{
+ pj_thread_desc desc;
+ pj_thread_t *thread;
+
+ if (pj_thread_is_registered() == PJ_TRUE) {
+ return;
+ }
+
+ pj_thread_register("Asterisk Thread", desc, &thread);
+}
+
+static void ast_rtp_ice_start(struct ast_rtp_instance *instance)
+{
+ struct ast_rtp *rtp = ast_rtp_instance_get_data(instance);
+ pj_str_t ufrag = pj_str(rtp->remote_ufrag), passwd = pj_str(rtp->remote_passwd);
+ pj_ice_sess_cand candidates[PJ_ICE_MAX_CAND];
+ struct ao2_iterator i;
+ struct ast_rtp_engine_ice_candidate *candidate;
+ int cand_cnt = 0;
+
+ if (!rtp->ice || !rtp->remote_candidates) {
+ return;
+ }
+
+ pj_thread_register_check();
+
+ i = ao2_iterator_init(rtp->remote_candidates, 0);
+
+ while ((candidate = ao2_iterator_next(&i))) {
+ pj_str_t address;
+
+ pj_strdup2(rtp->ice->pool, &candidates[cand_cnt].foundation, candidate->foundation);
+ candidates[cand_cnt].comp_id = candidate->id;
+ candidates[cand_cnt].prio = candidate->priority;
+
+ pj_sockaddr_parse(pj_AF_UNSPEC(), 0, pj_cstr(&address, ast_sockaddr_stringify(&candidate->address)), &candidates[cand_cnt].addr);
+
+ if (!ast_sockaddr_isnull(&candidate->relay_address)) {
+ pj_sockaddr_parse(pj_AF_UNSPEC(), 0, pj_cstr(&address, ast_sockaddr_stringify(&candidate->relay_address)), &candidates[cand_cnt].rel_addr);
+ }
+
+ if (candidate->type == AST_RTP_ICE_CANDIDATE_TYPE_HOST) {
+ candidates[cand_cnt].type = PJ_ICE_CAND_TYPE_HOST;
+ } else if (candidate->type == AST_RTP_ICE_CANDIDATE_TYPE_SRFLX) {
+ candidates[cand_cnt].type = PJ_ICE_CAND_TYPE_SRFLX;
+ } else if (candidate->type == AST_RTP_ICE_CANDIDATE_TYPE_RELAYED) {
+ candidates[cand_cnt].type = PJ_ICE_CAND_TYPE_RELAYED;
+ }
+
+ if (candidate->id == 1 && rtp->turn_rtp) {
+ pj_turn_sock_set_perm(rtp->turn_rtp, 1, &candidates[cand_cnt].addr, 1);
+ } else if (candidate->id == 2 && rtp->turn_rtcp) {
+ pj_turn_sock_set_perm(rtp->turn_rtcp, 1, &candidates[cand_cnt].addr, 1);
+ }
+
+ cand_cnt++;
+ }
+
+ ao2_iterator_destroy(&i);
+
+ if (pj_ice_sess_create_check_list(rtp->ice, &ufrag, &passwd, ao2_container_count(rtp->remote_candidates), &candidates[0]) == PJ_SUCCESS) {
+ pj_ice_sess_start_check(rtp->ice);
+ }
+}
+
+static const char *ast_rtp_ice_get_ufrag(struct ast_rtp_instance *instance)
+{
+ struct ast_rtp *rtp = ast_rtp_instance_get_data(instance);
+
+ return rtp->ice ? rtp->local_ufrag : NULL;
+}
+
+static const char *ast_rtp_ice_get_password(struct ast_rtp_instance *instance)
+{
+ struct ast_rtp *rtp = ast_rtp_instance_get_data(instance);
+
+ return rtp->ice ? rtp->local_passwd : NULL;
+}
+
+static struct ao2_container *ast_rtp_ice_get_local_candidates(struct ast_rtp_instance *instance)
+{
+ struct ast_rtp *rtp = ast_rtp_instance_get_data(instance);
+
+ if (rtp->local_candidates) {
+ ao2_ref(rtp->local_candidates, +1);
+ }
+
+ return rtp->local_candidates;
+}
+
+static void ast_rtp_ice_lite(struct ast_rtp_instance *instance)
+{
+ struct ast_rtp *rtp = ast_rtp_instance_get_data(instance);
+
+ if (!rtp->ice) {
+ return;
+ }
+
+ pj_thread_register_check();
+
+ pj_ice_sess_change_role(rtp->ice, PJ_ICE_SESS_ROLE_CONTROLLING);
+}
+
+static void ast_rtp_ice_add_cand(struct ast_rtp *rtp, unsigned comp_id, unsigned transport_id, pj_ice_cand_type type, pj_uint16_t local_pref,
+ const pj_sockaddr_t *addr, const pj_sockaddr_t *base_addr, const pj_sockaddr_t *rel_addr, int addr_len)
+{
+ pj_str_t foundation;
+ struct ast_rtp_engine_ice_candidate *candidate;
+ char address[PJ_INET6_ADDRSTRLEN];
+
+ pj_thread_register_check();
+
+ pj_ice_calc_foundation(rtp->ice->pool, &foundation, type, addr);
+
+ if (!rtp->local_candidates && !(rtp->local_candidates = ao2_container_alloc(1, NULL, NULL))) {
+ return;
+ }
+
+ if (!(candidate = ao2_alloc(sizeof(*candidate), ast_rtp_ice_candidate_destroy))) {
+ return;
+ }
+
+ candidate->foundation = ast_calloc(1, pj_strlen(&foundation) + 1);
+ ast_copy_string(candidate->foundation, pj_strbuf(&foundation), sizeof(candidate->foundation));
+ candidate->id = comp_id;
+ candidate->transport = ast_strdup("UDP");
+
+ ast_sockaddr_parse(&candidate->address, pj_sockaddr_print(addr, address, sizeof(address), 0), 0);
+ ast_sockaddr_set_port(&candidate->address, pj_sockaddr_get_port(addr));
+
+ if (rel_addr) {
+ ast_sockaddr_parse(&candidate->relay_address, pj_sockaddr_print(rel_addr, address, sizeof(address), 0), 0);
+ ast_sockaddr_set_port(&candidate->relay_address, pj_sockaddr_get_port(rel_addr));
+ }
+
+ if (type == PJ_ICE_CAND_TYPE_HOST) {
+ candidate->type = AST_RTP_ICE_CANDIDATE_TYPE_HOST;
+ } else if (type == PJ_ICE_CAND_TYPE_SRFLX) {
+ candidate->type = AST_RTP_ICE_CANDIDATE_TYPE_SRFLX;
+ } else if (type == PJ_ICE_CAND_TYPE_RELAYED) {
+ candidate->type = AST_RTP_ICE_CANDIDATE_TYPE_RELAYED;
+ }
+
+ if (pj_ice_sess_add_cand(rtp->ice, comp_id, transport_id, type, local_pref, &foundation, addr, addr, rel_addr, addr_len, NULL) != PJ_SUCCESS) {
+ ao2_ref(candidate, -1);
+ return;
+ }
+
+ /* By placing the candidate into the ICE session it will have produced the priority, so update the local candidate with it */
+ candidate->priority = rtp->ice->lcand[rtp->ice->lcand_cnt - 1].prio;
+
+ ao2_link(rtp->local_candidates, candidate);
+ ao2_ref(candidate, -1);
+}
+
+static char *generate_random_string(char *buf, size_t size)
+{
+ long val[4];
+ int x;
+
+ for (x=0; x<4; x++)
+ val[x] = ast_random();
+ snprintf(buf, size, "%08lx%08lx%08lx%08lx", val[0], val[1], val[2], val[3]);
+
+ return buf;
+}
+
+/* ICE RTP Engine interface declaration */
+static struct ast_rtp_engine_ice ast_rtp_ice = {
+ .set_authentication = ast_rtp_ice_set_authentication,
+ .add_remote_candidate = ast_rtp_ice_add_remote_candidate,
+ .start = ast_rtp_ice_start,
+ .get_ufrag = ast_rtp_ice_get_ufrag,
+ .get_password = ast_rtp_ice_get_password,
+ .get_local_candidates = ast_rtp_ice_get_local_candidates,
+ .ice_lite = ast_rtp_ice_lite,
+};
+
/* RTP Engine Declaration */
static struct ast_rtp_engine asterisk_rtp_engine = {
.name = "asterisk",
@@ -320,7 +611,154 @@
.stop = ast_rtp_stop,
.qos = ast_rtp_qos_set,
.sendcng = ast_rtp_sendcng,
+ .ice = &ast_rtp_ice,
};
+
+static void ast_rtp_on_ice_rx_data(pj_ice_sess *ice, unsigned comp_id, unsigned transport_id, void *pkt, pj_size_t size, const pj_sockaddr_t *src_addr, unsigned src_addr_len)
+{
+ struct ast_rtp *rtp = ice->user_data;
+
+ /* Instead of handling the packet here (which really doesn't work with our architecture) we set a bit to indicate that it should be handled after pj_ice_sess_on_rx_pkt
+ * returns */
+ rtp->passthrough = 1;
+}
+
+static pj_status_t ast_rtp_on_ice_tx_pkt(pj_ice_sess *ice, unsigned comp_id, unsigned transport_id, const void *pkt, pj_size_t size, const pj_sockaddr_t *dst_addr, unsigned dst_addr_len)
+{
+ struct ast_rtp *rtp = ice->user_data;
+ pj_status_t status = PJ_EINVALIDOP;
+
+ if (transport_id == TRANSPORT_SOCKET_RTP) {
+ /* Traffic is destined to go right out the RTP socket we already have */
+ status = pj_sock_sendto(rtp->s, pkt, (pj_ssize_t*)&size, 0, dst_addr, dst_addr_len);
+ } else if (transport_id == TRANSPORT_SOCKET_RTCP) {
+ /* Traffic is destined to go right out the RTCP socket we already have */
+ status = pj_sock_sendto(rtp->rtcp->s, pkt, (pj_ssize_t*)&size, 0, dst_addr, dst_addr_len);
+ } else if (transport_id == TRANSPORT_TURN_RTP) {
+ /* Traffic is going through the RTP TURN relay */
+ if (rtp->turn_rtp) {
+ status = pj_turn_sock_sendto(rtp->turn_rtp, pkt, size, dst_addr, dst_addr_len);
+ }
+ } else if (transport_id == TRANSPORT_TURN_RTCP) {
+ /* Traffic is going through the RTCP TURN relay */
+ if (rtp->turn_rtcp) {
+ status = pj_turn_sock_sendto(rtp->turn_rtcp, pkt, size, dst_addr, dst_addr_len);
+ }
+ }
+
+ return status;
+}
+
+/* ICE Session interface declaration */
+static pj_ice_sess_cb ast_rtp_ice_sess_cb = {
+ .on_rx_data = ast_rtp_on_ice_rx_data,
+ .on_tx_pkt = ast_rtp_on_ice_tx_pkt,
+};
+
+static void ast_rtp_on_turn_rx_rtp_data(pj_turn_sock *turn_sock, void *pkt, unsigned pkt_len, const pj_sockaddr_t *peer_addr, unsigned addr_len)
+{
+ struct ast_rtp_instance *instance = pj_turn_sock_get_user_data(turn_sock);
+ struct ast_rtp *rtp = ast_rtp_instance_get_data(instance);
+ struct ast_sockaddr dest;
+
+ ast_rtp_instance_get_local_address(instance, &dest);
+
+ ast_sendto(rtp->s, pkt, pkt_len, 0, &dest);
+}
+
+static void ast_rtp_on_turn_rtp_state(pj_turn_sock *turn_sock, pj_turn_state_t old_state, pj_turn_state_t new_state)
+{
+ struct ast_rtp_instance *instance = pj_turn_sock_get_user_data(turn_sock);
+ struct ast_rtp *rtp = NULL;
+
+ /* If this is a leftover from an already destroyed RTP instance just ignore the state change */
+ if (!instance) {
+ return;
+ }
+
+ rtp = ast_rtp_instance_get_data(instance);
+
+ /* If the TURN session is being destroyed we need to remove it from the RTP instance */
+ if (new_state == PJ_TURN_STATE_DESTROYING) {
+ rtp->turn_rtp = NULL;
+ return;
+ }
+
+ /* We store the new state so the other thread can actually handle it */
+ ast_mutex_lock(&rtp->lock);
+ rtp->turn_state = new_state;
+
+ /* If this is a state that the main thread should be notified about do so */
+ if (new_state == PJ_TURN_STATE_READY || new_state == PJ_TURN_STATE_DEALLOCATING || new_state == PJ_TURN_STATE_DEALLOCATED) {
+ ast_cond_signal(&rtp->cond);
+ }
+
+ ast_mutex_unlock(&rtp->lock);
+}
+
+/* RTP TURN Socket interface declaration */
+static pj_turn_sock_cb ast_rtp_turn_rtp_sock_cb = {
+ .on_rx_data = ast_rtp_on_turn_rx_rtp_data,
+ .on_state = ast_rtp_on_turn_rtp_state,
+};
+
+static void ast_rtp_on_turn_rx_rtcp_data(pj_turn_sock *turn_sock, void *pkt, unsigned pkt_len, const pj_sockaddr_t *peer_addr, unsigned addr_len)
+{
+ struct ast_rtp_instance *instance = pj_turn_sock_get_user_data(turn_sock);
+ struct ast_rtp *rtp = ast_rtp_instance_get_data(instance);
+
+ ast_sendto(rtp->rtcp->s, pkt, pkt_len, 0, &rtp->rtcp->us);
+}
+
+static void ast_rtp_on_turn_rtcp_state(pj_turn_sock *turn_sock, pj_turn_state_t old_state, pj_turn_state_t new_state)
+{
+ struct ast_rtp_instance *instance = pj_turn_sock_get_user_data(turn_sock);
+ struct ast_rtp *rtp = NULL;
+
+ /* If this is a leftover from an already destroyed RTP instance just ignore the state change */
+ if (!instance) {
+ return;
+ }
+
+ rtp = ast_rtp_instance_get_data(instance);
+
+ /* If the TURN session is being destroyed we need to remove it from the RTP instance */
+ if (new_state == PJ_TURN_STATE_DESTROYING) {
+ rtp->turn_rtcp = NULL;
+ return;
+ }
+
+ /* We store the new state so the other thread can actually handle it */
+ ast_mutex_lock(&rtp->lock);
+ rtp->turn_state = new_state;
+
+ /* If this is a state that the main thread should be notified about do so */
+ if (new_state == PJ_TURN_STATE_READY || new_state == PJ_TURN_STATE_DEALLOCATING || new_state == PJ_TURN_STATE_DEALLOCATED) {
+ ast_cond_signal(&rtp->cond);
+ }
+
+ ast_mutex_unlock(&rtp->lock);
+}
+
+/* RTCP TURN Socket interface declaration */
+static pj_turn_sock_cb ast_rtp_turn_rtcp_sock_cb = {
+ .on_rx_data = ast_rtp_on_turn_rx_rtcp_data,
+ .on_state = ast_rtp_on_turn_rtcp_state,
+};
+
+/*! \brief Worker thread for I/O queue and timerheap */
+static int ice_worker_thread(void *data)
+{
+ while (!worker_terminate) {
+ const pj_time_val delay = {0, 10};
+
+ pj_ioqueue_poll(ioqueue, &delay);
+
+ pj_timer_heap_poll(timerheap, NULL);
+ }
+
+ return 0;
+}
static inline int rtp_debug_test_addr(struct ast_sockaddr *addr)
{
@@ -364,6 +802,24 @@
return len;
}
+ if (rtp->ice) {
+ pj_str_t combined = pj_str(ast_sockaddr_stringify(sa));
+ pj_sockaddr address;
+
+ pj_thread_register_check();
+
+ pj_sockaddr_parse(pj_AF_UNSPEC(), 0, &combined, &address);
+
+ if (pj_ice_sess_on_rx_pkt(rtp->ice, rtcp ? COMPONENT_RTCP : COMPONENT_RTP, rtcp ? TRANSPORT_SOCKET_RTCP : TRANSPORT_SOCKET_RTP,
+ buf, len, &address, pj_sockaddr_get_len(&address)) != PJ_SUCCESS) {
+ return -1;
+ }
+ if (!rtp->passthrough) {
+ return 0;
+ }
+ rtp->passthrough = 0;
+ }
+
if (res_srtp && srtp && res_srtp->unprotect(srtp, buf, &len, rtcp) < 0) {
return -1;
}
@@ -381,28 +837,35 @@
return __rtp_recvfrom(instance, buf, size, flags, sa, 0);
}
-static int __rtp_sendto(struct ast_rtp_instance *instance, void *buf, size_t size, int flags, struct ast_sockaddr *sa, int rtcp)
+static int __rtp_sendto(struct ast_rtp_instance *instance, void *buf, size_t size, int flags, struct ast_sockaddr *sa, int rtcp, int *ice)
{
int len = size;
void *temp = buf;
struct ast_rtp *rtp = ast_rtp_instance_get_data(instance);
struct ast_srtp *srtp = ast_rtp_instance_get_srtp(instance);
+ *ice = 0;
+
if (res_srtp && srtp && res_srtp->protect(srtp, &temp, &len, rtcp) < 0) {
- return -1;
+ return -1;
+ }
+
+ if (rtp->ice && (pj_ice_sess_send_data(rtp->ice, rtcp ? COMPONENT_RTCP : COMPONENT_RTP, temp, len) == PJ_SUCCESS)) {
+ *ice = 1;
+ return 0;
}
return ast_sendto(rtcp ? rtp->rtcp->s : rtp->s, temp, len, flags, sa);
}
-static int rtcp_sendto(struct ast_rtp_instance *instance, void *buf, size_t size, int flags, struct ast_sockaddr *sa)
-{
- return __rtp_sendto(instance, buf, size, flags, sa, 1);
-}
-
-static int rtp_sendto(struct ast_rtp_instance *instance, void *buf, size_t size, int flags, struct ast_sockaddr *sa)
-{
- return __rtp_sendto(instance, buf, size, flags, sa, 0);
+static int rtcp_sendto(struct ast_rtp_instance *instance, void *buf, size_t size, int flags, struct ast_sockaddr *sa, int *ice)
+{
+ return __rtp_sendto(instance, buf, size, flags, sa, 1, ice);
+}
+
+static int rtp_sendto(struct ast_rtp_instance *instance, void *buf, size_t size, int flags, struct ast_sockaddr *sa, int *ice)
+{
+ return __rtp_sendto(instance, buf, size, flags, sa, 0, ice);
}
static int rtp_get_rate(struct ast_format *format)
@@ -514,17 +977,88 @@
return probation;
}
+static void rtp_add_candidates_to_ice(struct ast_rtp_instance *instance, struct ast_rtp *rtp, struct ast_sockaddr *addr, int port, int component,
+ int transport, const pj_turn_sock_cb *turn_cb, pj_turn_sock **turn_sock)
+{
+ pj_sockaddr address[16];
+ unsigned int count = PJ_ARRAY_SIZE(address), pos = 0;
+
+ /* Add all the local interface IP addresses */
+ pj_enum_ip_interface(ast_sockaddr_is_ipv4(addr) ? pj_AF_INET() : pj_AF_INET6(), &count, address);
+
+ for (pos = 0; pos < count; pos++) {
+ pj_sockaddr_set_port(&address[pos], port);
+ ast_rtp_ice_add_cand(rtp, component, transport, PJ_ICE_CAND_TYPE_HOST, 65535, &address[pos], &address[pos], NULL,
+ pj_sockaddr_get_len(&address[pos]));
+ }
+
+ /* If configured to use a STUN server to get our external mapped address do so */
+ if (stunaddr.sin_addr.s_addr && ast_sockaddr_is_ipv4(addr)) {
+ struct sockaddr_in answer;
+
+ if (!ast_stun_request(rtp->s, &stunaddr, NULL, &answer)) {
+ pj_str_t mapped = pj_str(ast_strdupa(ast_inet_ntoa(answer.sin_addr)));
+
+ pj_sockaddr_init(pj_AF_INET(), &address[0], &mapped, ntohs(answer.sin_port));
+
+ ast_rtp_ice_add_cand(rtp, component, transport, PJ_ICE_CAND_TYPE_SRFLX, 65535, &address[0], &address[0],
+ NULL, pj_sockaddr_get_len(&address[0]));
+ }
+ }
+
+ /* If configured to use a TURN relay create a session and allocate */
+ if (pj_strlen(&turnaddr) && pj_turn_sock_create(&rtp->ice->stun_cfg, ast_sockaddr_is_ipv4(addr) ? pj_AF_INET() : pj_AF_INET6(), PJ_TURN_TP_TCP,
+ turn_cb, NULL, instance, turn_sock) == PJ_SUCCESS) {
+ pj_stun_auth_cred cred = { 0, };
+ struct timeval wait = ast_tvadd(ast_tvnow(), ast_samp2tv(TURN_ALLOCATION_WAIT_TIME, 1000));
+ struct timespec ts = { .tv_sec = wait.tv_sec, .tv_nsec = wait.tv_usec * 1000, };
+
+ cred.type = PJ_STUN_AUTH_CRED_STATIC;
+ cred.data.static_cred.username = turnusername;
+ cred.data.static_cred.data_type = PJ_STUN_PASSWD_PLAIN;
+ cred.data.static_cred.data = turnpassword;
+
+ /* Because the TURN socket is asynchronous but we are synchronous we need to wait until it is done */
+ ast_mutex_lock(&rtp->lock);
+ pj_turn_sock_alloc(*turn_sock, &turnaddr, turnport, NULL, &cred, NULL);
+ ast_cond_timedwait(&rtp->cond, &rtp->lock, &ts);
+ ast_mutex_unlock(&rtp->lock);
+
+ /* If a TURN session was allocated add it as a candidate */
+ if (rtp->turn_state == PJ_TURN_STATE_READY) {
+ pj_turn_session_info info;
+
+ pj_turn_sock_get_info(*turn_sock, &info);
+
+ if (transport == TRANSPORT_SOCKET_RTP) {
+ transport = TRANSPORT_TURN_RTP;
+ } else if (transport == TRANSPORT_SOCKET_RTCP) {
+ transport = TRANSPORT_TURN_RTCP;
+ }
+
+ ast_rtp_ice_add_cand(rtp, component, transport, PJ_ICE_CAND_TYPE_RELAYED, 65535, &info.relay_addr, &info.relay_addr,
+ NULL, pj_sockaddr_get_len(&info.relay_addr));
+ }
+ }
+}
+
static int ast_rtp_new(struct ast_rtp_instance *instance,
struct ast_sched_context *sched, struct ast_sockaddr *addr,
void *data)
{
struct ast_rtp *rtp = NULL;
int x, startplace;
+ pj_stun_config stun_config;
+ pj_str_t ufrag, passwd;
/* Create a new RTP structure to hold all of our data */
if (!(rtp = ast_calloc(1, sizeof(*rtp)))) {
return -1;
}
+
+ /* Initialize synchronization aspects */
+ ast_mutex_init(&rtp->lock);
+ ast_cond_init(&rtp->cond, NULL);
/* Set default parameters on the newly created RTP structure */
rtp->ssrc = ast_random();
@@ -570,11 +1104,28 @@
}
}
+ pj_thread_register_check();
+
+ pj_stun_config_init(&stun_config, &cachingpool.factory, 0, ioqueue, timerheap);
+
+ generate_random_string(rtp->local_ufrag, sizeof(rtp->local_ufrag));
+ ufrag = pj_str(rtp->local_ufrag);
+ generate_random_string(rtp->local_passwd, sizeof(rtp->local_passwd));
+ passwd = pj_str(rtp->local_passwd);
+
+ ast_rtp_instance_set_data(instance, rtp);
+
+ /* Create an ICE session for ICE negotiation */
+ if (icesupport && pj_ice_sess_create(&stun_config, NULL, PJ_ICE_SESS_ROLE_UNKNOWN, 2, &ast_rtp_ice_sess_cb, &ufrag, &passwd, &rtp->ice) == PJ_SUCCESS) {
+ /* Make this available for the callbacks */
+ rtp->ice->user_data = rtp;
+
[... 501 lines stripped ...]
More information about the asterisk-commits
mailing list