[Asterisk-code-review] SDP: Ensure SDPs "merge" properly. (asterisk[master])

Mark Michelson asteriskteam at digium.com
Wed Apr 5 16:26:51 CDT 2017


Mark Michelson has uploaded a new change for review. ( https://gerrit.asterisk.org/5415 )

Change subject: SDP: Ensure SDPs "merge" properly.
......................................................................

SDP: Ensure SDPs "merge" properly.

The gist of this work ensures that when a remote SDP is received, it is
merged properly with the local capabilities. The remote SDP is converted
into a stream topology. That topology is then merged with the current
local topology on the SDP state. That new merged topology is then used
to create an SDP. Finally, adjustments are made to RTP instances based
on knowledge gained from the remote SDP.

There are also a battery of tests in this commit that ensure that some
basic SDP merges work as expected.

While this may not sound like a big change, it has the property that it
caused lots of ancillary changes.

* The remote SDP is no longer stored on the SDP state. Biggest reason:
  there's no need for it. The remote SDP is used at the time it is being
  set and nowhere else.

* Some new SDP APIs were added in order to find attributes and convert
  generic SDP attributes into rtpmap structures.

* Writing tests made me realize that retrieving a value from an SDP
  options structure, the SDP options needs to be made const.

* The SDP state machine was essentially gutted by a previous commit.
  Initially, I attempted to reinstate it, but I found that as it had
  been defined, it was not all that useful. What was more useful was
  knowing the role we play in SDP negotiation, so the SDP state machine
  has been transformed into an indicator of role.

* Rather than storing separate local and joint stream state
  capabilities, it makes more sense to keep track of current stream
  state and update it as things change.

Change-Id: I5938c2be3c6f0a003aa88a39a59e0880f8b2df3d
---
M include/asterisk/sdp.h
M include/asterisk/sdp_options.h
M include/asterisk/sdp_state.h
M main/sdp.c
M main/sdp_options.c
M main/sdp_private.h
M main/sdp_state.c
A tests/test_sdp.c
8 files changed, 1,909 insertions(+), 358 deletions(-)


  git pull ssh://gerrit.asterisk.org:29418/asterisk refs/changes/15/5415/1

diff --git a/include/asterisk/sdp.h b/include/asterisk/sdp.h
index 3649b40..d72ce1d 100644
--- a/include/asterisk/sdp.h
+++ b/include/asterisk/sdp.h
@@ -147,6 +147,22 @@
 };
 
 /*!
+ * \brief A structure representing an SDP rtpmap attribute
+ */
+struct ast_sdp_rtpmap {
+	/*! The RTP payload number for the rtpmap */
+	int payload;
+	/*! The Name of the codec */
+	char *encoding_name;
+	/*! The clock rate of the codec */
+	int clock_rate;
+	/*! Optional encoding parameters */
+	char *encoding_parameters;
+	/*! Area where strings are stored */
+	char buf[0];
+};
+
+/*!
  * \brief Free an SDP Attribute
  *
  * \param a_line The attribute to free
@@ -545,15 +561,71 @@
 	struct ast_sdp_t_line *t_line);
 
 /*!
- * \brief Create an SDP from an existing SDP State local topology
+ * \brief Find an attribute on the top-level SDP
  *
- * \param sdp_state SDP State
+ * \note This will not search within streams for the given attribute.
  *
- * \retval non-NULL Success
- * \retval NULL Failure
- *
- * \since 15
+ * \param sdp The SDP in which to search
+ * \param attr_name The name of the attribute to search for
+ * \param payload Optional payload number to search for. If irrelevant, set to -1
+ * \retval NULL Could not find the given attribute
+ * \retval Non-NULL The attribute to find
  */
-struct ast_sdp *ast_sdp_create_from_state(const struct ast_sdp_state *sdp_state);
+struct ast_sdp_a_line *ast_sdp_find_attribute(const struct ast_sdp *sdp,
+	const char *attr_name, int payload);
 
+/*!
+ * \brief Find an attribute on an SDP stream (m-line)
+ *
+ * \param sdp The SDP in which to search
+ * \param attr_name The name of the attribute to search for
+ * \param payload Optional payload number to search for. If irrelevant, set to -1
+ * \retval NULL Could not find the given attribute
+ * \retval Non-NULL The attribute to find
+ */
+struct ast_sdp_a_line *ast_sdp_m_find_attribute(const struct ast_sdp_m_line *m_line,
+	const char *attr_name, int payload);
+
+/*!
+ * \brief Convert an SDP a_line into an rtpmap
+ *
+ * The returned value is heap-allocated and must be freed with
+ * ast_sdp_rtpmap_free()
+ *
+ * \param a_line The SDP a_line to convert
+ * \retval NULL Fail
+ * \retval non-NULL Success
+ */
+struct ast_sdp_rtpmap *ast_sdp_a_get_rtpmap(const struct ast_sdp_a_line *a_line);
+
+
+/*!
+ * \brief Allocate a new SDP rtpmap
+ *
+ * \param payload The RTP payload number
+ * \param encoding_name The human-readable name for the codec
+ * \param clock_rate The rate of the codec, in cycles per second
+ * \param encoding_parameters Optional codec-specific parameters (such as number of channels)
+ * \retval NULL Fail
+ * \retval non-NULL Success
+ */
+struct ast_sdp_rtpmap *ast_sdp_rtpmap_alloc(int payload, const char *encoding_name,
+	int clock_rate, const char *encoding_parameters);
+
+/*!
+ * \brief Free an SDP rtpmap
+ */
+void ast_sdp_rtpmap_free(struct ast_sdp_rtpmap *rtpmap);
+
+/*!
+ * \brief Turn an SDP into a stream topology
+ *
+ * This traverses the m-lines of the SDP and creates a stream topology, with
+ * each m-line corresponding to a stream in the created topology.
+ *
+ * \param sdp The SDP to convert
+ * \retval NULL An error occurred when converting
+ * \retval non-NULL The generated stream topology
+ */
+struct ast_stream_topology *ast_get_topology_from_sdp(const struct ast_sdp *sdp);
 #endif /* _SDP_PRIV_H */
diff --git a/include/asterisk/sdp_options.h b/include/asterisk/sdp_options.h
index 0186eea..18387e8 100644
--- a/include/asterisk/sdp_options.h
+++ b/include/asterisk/sdp_options.h
@@ -106,7 +106,7 @@
  *
  * \returns media_address
  */
-const char *ast_sdp_options_get_media_address(struct ast_sdp_options *options);
+const char *ast_sdp_options_get_media_address(const struct ast_sdp_options *options);
 
 /*!
  * \since 15.0.0
@@ -126,7 +126,7 @@
  *
  * \returns sdpowner
  */
-const char *ast_sdp_options_get_sdpowner(struct ast_sdp_options *options);
+const char *ast_sdp_options_get_sdpowner(const struct ast_sdp_options *options);
 
 /*!
  * \since 15.0.0
@@ -146,7 +146,7 @@
  *
  * \returns sdpsession
  */
-const char *ast_sdp_options_get_sdpsession(struct ast_sdp_options *options);
+const char *ast_sdp_options_get_sdpsession(const struct ast_sdp_options *options);
 
 /*!
  * \since 15.0.0
@@ -166,7 +166,7 @@
  *
  * \returns rtp_engine
  */
-const char *ast_sdp_options_get_rtp_engine(struct ast_sdp_options *options);
+const char *ast_sdp_options_get_rtp_engine(const struct ast_sdp_options *options);
 
 /*!
  * \since 15.0.0
@@ -186,7 +186,7 @@
  *
  * \returns bind_rtp_to_media_address
  */
-unsigned int ast_sdp_options_get_bind_rtp_to_media_address(struct ast_sdp_options *options);
+unsigned int ast_sdp_options_get_bind_rtp_to_media_address(const struct ast_sdp_options *options);
 
 /*!
  * \since 15.0.0
@@ -206,7 +206,7 @@
  *
  * \returns rtp_symmetric
  */
-unsigned int ast_sdp_options_get_rtp_symmetric(struct ast_sdp_options *options);
+unsigned int ast_sdp_options_get_rtp_symmetric(const struct ast_sdp_options *options);
 
 /*!
  * \since 15.0.0
@@ -226,7 +226,7 @@
  *
  * \returns telephone_event
  */
-unsigned int ast_sdp_options_get_telephone_event(struct ast_sdp_options *options);
+unsigned int ast_sdp_options_get_telephone_event(const struct ast_sdp_options *options);
 
 /*!
  * \since 15.0.0
@@ -246,7 +246,7 @@
  *
  * \returns rtp_ipv6
  */
-unsigned int ast_sdp_options_get_rtp_ipv6(struct ast_sdp_options *options);
+unsigned int ast_sdp_options_get_rtp_ipv6(const struct ast_sdp_options *options);
 
 /*!
  * \since 15.0.0
@@ -266,7 +266,7 @@
  *
  * \returns g726_non_standard
  */
-unsigned int ast_sdp_options_get_g726_non_standard(struct ast_sdp_options *options);
+unsigned int ast_sdp_options_get_g726_non_standard(const struct ast_sdp_options *options);
 
 /*!
  * \since 15.0.0
@@ -286,7 +286,7 @@
  *
  * \returns tos_audio
  */
-unsigned int ast_sdp_options_get_tos_audio(struct ast_sdp_options *options);
+unsigned int ast_sdp_options_get_tos_audio(const struct ast_sdp_options *options);
 
 /*!
  * \since 15.0.0
@@ -306,7 +306,7 @@
  *
  * \returns cos_audio
  */
-unsigned int ast_sdp_options_get_cos_audio(struct ast_sdp_options *options);
+unsigned int ast_sdp_options_get_cos_audio(const struct ast_sdp_options *options);
 
 /*!
  * \since 15.0.0
@@ -326,7 +326,7 @@
  *
  * \returns tos_video
  */
-unsigned int ast_sdp_options_get_tos_video(struct ast_sdp_options *options);
+unsigned int ast_sdp_options_get_tos_video(const struct ast_sdp_options *options);
 
 /*!
  * \since 15.0.0
@@ -346,7 +346,7 @@
  *
  * \returns cos_video
  */
-unsigned int ast_sdp_options_get_cos_video(struct ast_sdp_options *options);
+unsigned int ast_sdp_options_get_cos_video(const struct ast_sdp_options *options);
 
 /*!
  * \since 15.0.0
@@ -366,7 +366,7 @@
  *
  * \returns ice
  */
-enum ast_sdp_options_ice ast_sdp_options_get_ice(struct ast_sdp_options *options);
+enum ast_sdp_options_ice ast_sdp_options_get_ice(const struct ast_sdp_options *options);
 
 /*!
  * \since 15.0.0
@@ -386,7 +386,7 @@
  *
  * \returns impl
  */
-enum ast_sdp_options_impl ast_sdp_options_get_impl(struct ast_sdp_options *options);
+enum ast_sdp_options_impl ast_sdp_options_get_impl(const struct ast_sdp_options *options);
 
 /*!
  * \since 15.0.0
@@ -406,6 +406,9 @@
  *
  * \returns encryption
  */
-enum ast_sdp_options_encryption ast_sdp_options_get_encryption(struct ast_sdp_options *options);
+enum ast_sdp_options_encryption ast_sdp_options_get_encryption(const struct ast_sdp_options *options);
+
+unsigned int ast_sdp_options_get_rtcp_mux(const struct ast_sdp_options *options);
+void ast_sdp_options_set_rtcp_mux(struct ast_sdp_options *options, unsigned int value);
 
 #endif /* _ASTERISK_SDP_OPTIONS_H */
diff --git a/include/asterisk/sdp_state.h b/include/asterisk/sdp_state.h
index a186d7e..7c36f44 100644
--- a/include/asterisk/sdp_state.h
+++ b/include/asterisk/sdp_state.h
@@ -138,7 +138,7 @@
  *
  * \since 15
  */
-void ast_sdp_state_set_remote_sdp(struct ast_sdp_state *sdp_state, struct ast_sdp *sdp);
+void ast_sdp_state_set_remote_sdp(struct ast_sdp_state *sdp_state, const struct ast_sdp *sdp);
 
 /*!
  * \brief Set the remote SDP from an Implementation
@@ -235,4 +235,14 @@
 unsigned int ast_sdp_state_get_locally_held(const struct ast_sdp_state *sdp_state,
 	int stream_index);
 
+/*!
+ * \since 15.0.0
+ * \brief Generate a local SDP based on the current SDP state
+ *
+ * \param sdp_state
+ * \retval NULL Failed to create an SDP
+ * \retval non-NULL The newly-created SDP
+ */
+struct ast_sdp *ast_sdp_create_from_state(const struct ast_sdp_state *sdp_state);
+
 #endif /* _ASTERISK_SDP_STATE_H */
diff --git a/main/sdp.c b/main/sdp.c
index 1ef6400..d852fee 100644
--- a/main/sdp.c
+++ b/main/sdp.c
@@ -503,7 +503,7 @@
 int ast_sdp_add_m_from_rtp_stream(struct ast_sdp *sdp, const struct ast_sdp_state *sdp_state,
 	const struct ast_sdp_options *options, int stream_index)
 {
-	struct ast_stream *stream = ast_stream_topology_get_stream(ast_sdp_state_get_local_topology(sdp_state), stream_index);
+	struct ast_stream *stream = ast_stream_topology_get_stream(ast_sdp_state_get_joint_topology(sdp_state), stream_index);
 	struct ast_sdp_m_line *m_line;
 	struct ast_format_cap *caps;
 	int i;
@@ -543,7 +543,7 @@
 			continue;
 		}
 
-		if (ast_sdp_m_add_format(m_line, options, rtp_code, 0, format, 0)) {
+		if (ast_sdp_m_add_format(m_line, options, rtp_code, 1, format, 0)) {
 			ast_sdp_m_free(m_line);
 			ao2_ref(format, -1);
 			return -1;
@@ -631,70 +631,278 @@
 	return 0;
 }
 
-struct ast_sdp *ast_sdp_create_from_state(const struct ast_sdp_state *sdp_state)
+
+static struct ast_sdp_a_line *sdp_find_attribute_common(const struct ast_sdp_a_lines *a_lines,
+	const char *attr_name, int payload)
 {
-	const struct ast_sdp_options *options;
-	RAII_VAR(struct ast_sdp *, sdp, NULL, ao2_cleanup);
-	const const struct ast_stream_topology *topology;
-	int stream_count;
-	int stream_num;
-	struct ast_sdp_o_line *o_line = NULL;
-	struct ast_sdp_c_line *c_line = NULL;
-	struct ast_sdp_s_line *s_line = NULL;
-	struct ast_sdp_t_line *t_line = NULL;
-	char *address_type;
-	struct timeval tv = ast_tvnow();
-	uint32_t t;
-	ast_assert(!!sdp_state);
+	struct ast_sdp_a_line *a_line;
+	int i;
 
-	options = ast_sdp_state_get_options(sdp_state);
-	topology = ast_sdp_state_get_local_topology(sdp_state);
-	stream_count = ast_stream_topology_get_count(topology);
+	for (i = 0; i < AST_VECTOR_SIZE(a_lines); ++i) {
+		int a_line_payload;
 
-	t = tv.tv_sec + 2208988800UL;
-	address_type = (strchr(options->media_address, ':') ? "IP6" : "IP4");
-
-	o_line = ast_sdp_o_alloc(options->sdpowner, t, t, address_type, options->media_address);
-	if (!o_line) {
-		goto error;
-	}
-	c_line = ast_sdp_c_alloc(address_type, options->media_address);
-	if (!c_line) {
-		goto error;
-	}
-
-	s_line = ast_sdp_s_alloc(options->sdpsession);
-	if (!s_line) {
-		goto error;
-	}
-
-	sdp = ast_sdp_alloc(o_line, c_line, s_line, NULL);
-	if (!sdp) {
-		goto error;
-	}
-
-	for (stream_num = 0; stream_num < stream_count; stream_num++) {
-		enum ast_media_type type = ast_stream_get_type(ast_stream_topology_get_stream(topology, stream_num));
-
-		if (type == AST_MEDIA_TYPE_AUDIO || type == AST_MEDIA_TYPE_VIDEO) {
-			if (ast_sdp_add_m_from_rtp_stream(sdp, sdp_state, options, stream_num)) {
-				goto error;
-			}
+		a_line = AST_VECTOR_GET(a_lines, i);
+		if (strcmp(a_line->name, attr_name)) {
+			continue;
 		}
-	}
 
-	return sdp;
-
-error:
-	if (sdp) {
-		ast_sdp_free(sdp);
-	} else {
-		ast_sdp_t_free(t_line);
-		ast_sdp_s_free(s_line);
-		ast_sdp_c_free(c_line);
-		ast_sdp_o_free(o_line);
+		if (payload >= 0) {
+			int sscanf_res;
+			sscanf_res = sscanf(a_line->value, "%30d", &a_line_payload);
+			if (sscanf_res == 1 && payload == a_line_payload) {
+				return a_line;
+			}
+		} else {
+			return a_line;
+		}
 	}
 
 	return NULL;
 }
 
+struct ast_sdp_a_line *ast_sdp_find_attribute(const struct ast_sdp *sdp,
+	const char *attr_name, int payload)
+{
+	return sdp_find_attribute_common(sdp->a_lines, attr_name, payload);
+}
+
+struct ast_sdp_a_line *ast_sdp_m_find_attribute(const struct ast_sdp_m_line *m_line,
+	const char *attr_name, int payload)
+{
+	return sdp_find_attribute_common(m_line->a_lines, attr_name, payload);
+}
+
+/*!
+ * \brief Convert the string representation of media to its AST_MEDIA_* type
+ */
+static enum ast_media_type media_type_from_str(const char *media_type_str)
+{
+	if (!strcasecmp(media_type_str, "audio")) {
+		return AST_MEDIA_TYPE_AUDIO;
+	} else if (!strcasecmp(media_type_str, "video")) {
+		return AST_MEDIA_TYPE_VIDEO;
+	} else if (!strcasecmp(media_type_str, "image")) {
+		return AST_MEDIA_TYPE_IMAGE;
+	} else if (!strcasecmp(media_type_str, "text")) {
+		return AST_MEDIA_TYPE_TEXT;
+	} else {
+		return AST_MEDIA_TYPE_UNKNOWN;
+	}
+}
+
+struct ast_sdp_rtpmap *ast_sdp_rtpmap_alloc(int payload, const char *encoding_name,
+	int clock_rate, const char *encoding_parameters)
+{
+	struct ast_sdp_rtpmap *rtpmap;
+	char *buf_pos;
+
+	rtpmap = ast_calloc(1, sizeof(*rtpmap) + strlen(encoding_name) + strlen(encoding_parameters) + 2);
+	if (!rtpmap) {
+		return NULL;
+	}
+
+	rtpmap->payload = payload;
+	rtpmap->clock_rate = clock_rate;
+
+	buf_pos = rtpmap->buf;
+	COPY_STR_AND_ADVANCE(buf_pos, rtpmap->encoding_name, encoding_name);
+	COPY_STR_AND_ADVANCE(buf_pos, rtpmap->encoding_parameters, encoding_parameters);
+
+	return rtpmap;
+}
+
+void ast_sdp_rtpmap_free(struct ast_sdp_rtpmap *rtpmap)
+{
+	ast_free(rtpmap);
+}
+
+struct ast_sdp_rtpmap *ast_sdp_a_get_rtpmap(const struct ast_sdp_a_line *a_line)
+{
+	char *value_copy;
+	char *slash;
+	int payload;
+	char encoding_name[64];
+	int clock_rate;
+	char *encoding_parameters;
+	struct ast_sdp_rtpmap *rtpmap;
+	int clock_rate_len;
+
+	value_copy = ast_strip(ast_strdupa(a_line->value));
+
+	if (sscanf(value_copy, "%30d %s", &payload, encoding_name) != 2) {
+		return NULL;
+	}
+
+	slash = strchr(encoding_name, '/');
+	if (!slash) {
+		return NULL;
+	}
+	*slash++ = '\0';
+	if (ast_strlen_zero(encoding_name)) {
+		return NULL;
+	}
+	if (sscanf(slash, "%30d%n", &clock_rate, &clock_rate_len) < 1) {
+		return NULL;
+	}
+
+	slash += clock_rate_len;
+	if (!ast_strlen_zero(slash)) {
+		if (*slash == '/') {
+			*slash++ = '\0';
+			encoding_parameters = slash;
+			if (ast_strlen_zero(encoding_parameters)) {
+				return NULL;
+			}
+		} else {
+			return NULL;
+		}
+	} else {
+		encoding_parameters = "";
+	}
+
+	rtpmap = ast_sdp_rtpmap_alloc(payload, encoding_name, clock_rate,
+		encoding_parameters);
+
+	return rtpmap;
+}
+
+/*!
+ * \brief Turn an SDP attribute into an sdp_rtpmap structure
+ *
+ * \param m_line The media section where this attribute was found.
+ * \param payload The RTP payload to find an rtpmap for
+ * \param[out] rtpmap The rtpmap to fill in.
+ * \return Zero if successful, otherwise less than zero
+ */
+static struct ast_sdp_rtpmap *sdp_payload_get_rtpmap(const struct ast_sdp_m_line *m_line, int payload)
+{
+	struct ast_sdp_a_line *rtpmap_attr;
+
+	rtpmap_attr = ast_sdp_m_find_attribute(m_line, "rtpmap", payload);
+	if (!rtpmap_attr) {
+		return NULL;
+	}
+
+	return ast_sdp_a_get_rtpmap(rtpmap_attr);
+}
+
+/*!
+ * \brief Find and process fmtp attributes for a given payload
+ *
+ * \param m_line The stream on which to search for the fmtp attribute
+ * \param payload The specific fmtp attribute to search for
+ * \param codecs The current RTP codecs that have been built up
+ */
+static void process_fmtp(const struct ast_sdp_m_line *m_line, int payload,
+	struct ast_rtp_codecs *codecs)
+{
+	struct ast_sdp_a_line *attr;
+	char *param;
+	int ignore;
+	struct ast_format *replace;
+	struct ast_format *format;
+
+	attr = ast_sdp_m_find_attribute(m_line, "fmtp", payload);
+	if (!attr) {
+		return;
+	}
+
+	if (sscanf(attr->value, "%30d %ms", &ignore, &param) != 2) {
+		return;
+	}
+
+	format = ast_rtp_codecs_get_payload_format(codecs, payload);
+	if (!format) {
+		ast_free(param);
+		return;
+	}
+
+	replace = ast_format_parse_sdp_fmtp(format, param);
+	if (replace) {
+		ast_rtp_codecs_payload_replace_format(codecs, payload, replace);
+		ao2_ref(replace, -1);
+	}
+	ao2_ref(format, -1);
+	ast_free(param);
+}
+
+/*!
+ * \brief Convert an SDP stream into an Asterisk stream
+ *
+ * Given an m-line from an SDP, convert it into an ast_stream structure.
+ * This takes formats, as well as clock-rate and fmtp attributes into account.
+ *
+ * \param m_line The SDP media section to convert
+ * \retval NULL An error occurred
+ * \retval non-NULL The converted stream
+ */
+static struct ast_stream *get_stream_from_m(const struct ast_sdp_m_line *m_line)
+{
+	int i;
+	int non_ast_fmts;
+	struct ast_rtp_codecs codecs;
+	struct ast_format_cap *caps;
+	struct ast_stream *stream;
+
+	caps = ast_format_cap_alloc(AST_FORMAT_CAP_FLAG_DEFAULT);
+	if (!caps) {
+		return NULL;
+	}
+	stream = ast_stream_alloc("blah", media_type_from_str(m_line->type));
+	if (!stream) {
+		ao2_ref(caps, -1);
+		return NULL;
+	}
+	ast_rtp_codecs_payloads_initialize(&codecs);
+
+	for (i = 0; i < ast_sdp_m_get_payload_count(m_line); ++i) {
+		struct ast_sdp_payload *payload_s;
+		struct ast_sdp_rtpmap *rtpmap;
+		int payload;
+
+		payload_s = ast_sdp_m_get_payload(m_line, i);
+		sscanf(payload_s->fmt, "%30d", &payload);
+		ast_rtp_codecs_payloads_set_m_type(&codecs, NULL, payload);
+
+		rtpmap = sdp_payload_get_rtpmap(m_line, payload);
+		if (!rtpmap) {
+			continue;
+		}
+		ast_rtp_codecs_payloads_set_rtpmap_type_rate(&codecs, NULL,
+			payload, m_line->type, rtpmap->encoding_name, 0,
+			rtpmap->clock_rate);
+		ast_sdp_rtpmap_free(rtpmap);
+
+		process_fmtp(m_line, payload, &codecs);
+	}
+
+	ast_rtp_codecs_payload_formats(&codecs, caps, &non_ast_fmts);
+	ast_stream_set_formats(stream, caps);
+
+	ao2_ref(caps, -1);
+	ast_rtp_codecs_payloads_destroy(&codecs);
+	return stream;
+}
+
+struct ast_stream_topology *ast_get_topology_from_sdp(const struct ast_sdp *sdp)
+{
+	struct ast_stream_topology *topology;
+	int i;
+
+	topology = ast_stream_topology_alloc();
+	if (!topology) {
+		return NULL;
+	}
+
+	for (i = 0; i < ast_sdp_get_m_count(sdp); ++i) {
+		struct ast_stream *stream;
+
+		stream = get_stream_from_m(ast_sdp_get_m(sdp, i));
+		if (!stream) {
+			continue;
+		}
+		ast_stream_topology_append_stream(topology, stream);
+	}
+
+	return topology;
+}
diff --git a/main/sdp_options.c b/main/sdp_options.c
index 6084817..f8c830f 100644
--- a/main/sdp_options.c
+++ b/main/sdp_options.c
@@ -36,7 +36,7 @@
 	if (!strcmp(value, options->field)) return; \
 	ast_string_field_set(options, field, value); \
 } \
-const char *ast_sdp_options_get_##field(struct ast_sdp_options *options) \
+const char *ast_sdp_options_get_##field(const struct ast_sdp_options *options) \
 { \
 	ast_assert(options != NULL); \
 	return options->field; \
@@ -48,7 +48,7 @@
 	ast_assert(options != NULL); \
 	options->field = value; \
 } \
-type ast_sdp_options_get_##field(struct ast_sdp_options *options) \
+type ast_sdp_options_get_##field(const struct ast_sdp_options *options) \
 { \
 	ast_assert(options != NULL); \
 	return options->field; \
@@ -64,6 +64,7 @@
 DEFINE_GETTERS_SETTERS_FOR(unsigned int, telephone_event);
 DEFINE_GETTERS_SETTERS_FOR(unsigned int, rtp_ipv6);
 DEFINE_GETTERS_SETTERS_FOR(unsigned int, g726_non_standard);
+DEFINE_GETTERS_SETTERS_FOR(unsigned int, rtcp_mux);
 DEFINE_GETTERS_SETTERS_FOR(unsigned int, tos_audio);
 DEFINE_GETTERS_SETTERS_FOR(unsigned int, cos_audio);
 DEFINE_GETTERS_SETTERS_FOR(unsigned int, tos_video);
@@ -103,3 +104,4 @@
 	ast_string_field_free_memory(options);
 	ast_free(options);
 }
+
diff --git a/main/sdp_private.h b/main/sdp_private.h
index 45aaebf..15b9d1e 100644
--- a/main/sdp_private.h
+++ b/main/sdp_private.h
@@ -40,6 +40,7 @@
 		unsigned int rtp_ipv6 : 1;
 		unsigned int g726_non_standard : 1;
 		unsigned int locally_held : 1;
+		unsigned int rtcp_mux: 1;
 	};
 	struct {
 		unsigned int tos_audio;
diff --git a/main/sdp_state.c b/main/sdp_state.c
index 5858a65..12c7e2f 100644
--- a/main/sdp_state.c
+++ b/main/sdp_state.c
@@ -24,43 +24,40 @@
 #include "asterisk/utils.h"
 #include "asterisk/netsock2.h"
 #include "asterisk/rtp_engine.h"
+#include "asterisk/format.h"
+#include "asterisk/format_cap.h"
+#include "asterisk/config.h"
 
 #include "../include/asterisk/sdp.h"
 #include "asterisk/stream.h"
 
 #include "sdp_private.h"
 
-enum ast_sdp_state_machine {
-	/*! \brief The initial state.
+enum ast_sdp_role {
+	/*!
+	 * \brief The role has not yet been determined.
 	 *
-	 * The state machine starts here. It also goes back to this
-	 * state whenever ast_sdp_state_reset() is called.
+	 * When the SDP state is allocated, this is the starting role.
+	 * Similarly, when the SDP state is reset, the role is reverted
+	 * to this.
 	 */
-	SDP_STATE_INITIAL,
-	/*! \brief We are the SDP offerer.
+	SDP_ROLE_NOT_SET,
+	/*!
+	 * \brief We are the offerer.
 	 *
-	 * The state machine enters this state if in the initial state
-	 * and ast_sdp_state_get_local() is called. When this state is
-	 * entered, a local SDP is created and then returned.
+	 * If a local SDP is requested before a remote SDP has been set, then
+	 * we assume the role of offerer. This means that we will generate an
+	 * SDP from the local capabilities and configured options.
 	 */
-	SDP_STATE_OFFERER,
-	/*! \brief We are the SDP answerer.
+	SDP_ROLE_OFFERER,
+	/*!
+	 * \brief We are the answerer.
 	 *
-	 * The state machine enters this state if in the initial state
-	 * and ast_sdp_state_set_remote() is called.
+	 * If a remote SDP is set before a local SDP is requested, then we
+	 * assume the role of answerer. This means that we will generate an
+	 * SDP based on a merge of the remote capabilities and our local capabilities.
 	 */
-	SDP_STATE_ANSWERER,
-	/*! \brief The SDP has been negotiated.
-	 *
-	 * This state can be entered from either the offerer or answerer
-	 * state. When this state is entered, a joint SDP is created.
-	 */
-	SDP_STATE_NEGOTIATED,
-	/*! \brief Not an actual state.
-	 *
-	 * This is just here to mark the end of the enumeration.
-	 */
-	SDP_STATE_END,
+	SDP_ROLE_ANSWERER,
 };
 
 typedef int (*state_fn)(struct ast_sdp_state *state);
@@ -76,219 +73,36 @@
 	unsigned int locally_held;
 };
 
+static void sdp_state_stream_free(struct sdp_state_stream *state_stream)
+{
+	if (state_stream->instance) {
+		ast_rtp_instance_destroy(state_stream->instance);
+	}
+	ast_free(state_stream);
+}
+
+AST_VECTOR(sdp_state_streams, struct sdp_state_stream *);
+
 struct sdp_state_capabilities {
 	/*! Stream topology */
 	struct ast_stream_topology *topology;
 	/*! Additional information about the streams */
-	AST_VECTOR(, struct sdp_state_stream) streams;
+	struct sdp_state_streams streams;
 	/*! An explicit global connection address */
 	struct ast_sockaddr connection_address;
 };
 
-struct ast_sdp_state {
-	/*! Local capabilities, learned through configuration */
-	struct sdp_state_capabilities local_capabilities;
-	/*! Remote capabilities, learned through remote SDP */
-	struct ast_stream_topology *remote_capabilities;
-	/*! Joint capabilities. The combined local and remote capabilities. */
-	struct sdp_state_capabilities joint_capabilities;
-	/*! Local SDP. Generated via the options and local capabilities. */
-	struct ast_sdp *local_sdp;
-	/*! Remote SDP. Received directly from a peer. */
-	struct ast_sdp *remote_sdp;
-	/*! Joint SDP. The merged local and remote SDPs. */
-	struct ast_sdp *joint_sdp;
-	/*! SDP options. Configured options beyond media capabilities. */
-	struct ast_sdp_options *options;
-	/*! Translator that puts SDPs into the expected representation */
-	struct ast_sdp_translator *translator;
-	/*! The current state machine state that we are in */
-	enum ast_sdp_state_machine state;
-};
-
-struct ast_sdp_state *ast_sdp_state_alloc(struct ast_stream_topology *streams,
-	struct ast_sdp_options *options)
+static void sdp_state_capabilities_free(struct sdp_state_capabilities *capabilities)
 {
-	struct ast_sdp_state *sdp_state;
-
-	sdp_state = ast_calloc(1, sizeof(*sdp_state));
-	if (!sdp_state) {
-		return NULL;
-	}
-
-	sdp_state->options = options;
-
-	sdp_state->translator = ast_sdp_translator_new(ast_sdp_options_get_impl(sdp_state->options));
-	if (!sdp_state->translator) {
-		ast_sdp_state_free(sdp_state);
-		return NULL;
-	}
-
-	if (ast_sdp_state_update_local_topology(sdp_state, streams)) {
-		ast_sdp_state_free(sdp_state);
-		return NULL;
-	}
-
-	sdp_state->state = SDP_STATE_INITIAL;
-
-	return sdp_state;
-}
-
-static void sdp_state_capabilities_free(struct sdp_state_capabilities *sdp_capabilities)
-{
-	int stream_index;
-
-	for (stream_index = 0; stream_index < AST_VECTOR_SIZE(&sdp_capabilities->streams); stream_index++) {
-		struct sdp_state_stream *stream_state = AST_VECTOR_GET_ADDR(&sdp_capabilities->streams, stream_index);
-		enum ast_media_type type = ast_stream_get_type(ast_stream_topology_get_stream(sdp_capabilities->topology, stream_index));
-
-		if (type == AST_MEDIA_TYPE_AUDIO || type == AST_MEDIA_TYPE_VIDEO) {
-			ast_rtp_instance_destroy(stream_state->instance);
-		}
-	}
-
-	ast_stream_topology_free(sdp_capabilities->topology);
-	AST_VECTOR_FREE(&sdp_capabilities->streams);
-}
-
-void ast_sdp_state_free(struct ast_sdp_state *sdp_state)
-{
-	if (!sdp_state) {
+	if (!capabilities) {
 		return;
 	}
 
-	sdp_state_capabilities_free(&sdp_state->local_capabilities);
-	ast_stream_topology_free(sdp_state->remote_capabilities);
-	sdp_state_capabilities_free(&sdp_state->joint_capabilities);
-	ast_sdp_free(sdp_state->local_sdp);
-	ast_sdp_free(sdp_state->remote_sdp);
-	ast_sdp_free(sdp_state->joint_sdp);
-	ast_sdp_options_free(sdp_state->options);
-	ast_sdp_translator_free(sdp_state->translator);
+	ast_stream_topology_free(capabilities->topology);
+	AST_VECTOR_CALLBACK_VOID(&capabilities->streams, sdp_state_stream_free);
+	AST_VECTOR_FREE(&capabilities->streams);
+	ast_free(capabilities);
 }
-
-static struct sdp_state_stream *sdp_state_get_stream(const struct ast_sdp_state *sdp_state, int stream_index)
-{
-	if (stream_index >= AST_VECTOR_SIZE(&sdp_state->local_capabilities.streams)) {
-		return NULL;
-	}
-
-	return AST_VECTOR_GET_ADDR(&sdp_state->local_capabilities.streams, stream_index);
-}
-
-struct ast_rtp_instance *ast_sdp_state_get_rtp_instance(
-	const struct ast_sdp_state *sdp_state, int stream_index)
-{
-	struct sdp_state_stream *stream_state;
-
-	ast_assert(sdp_state != NULL);
-
-	stream_state = sdp_state_get_stream(sdp_state, stream_index);
-	if (!stream_state) {
-		return NULL;
-	}
-
-	return stream_state->instance;
-}
-
-const struct ast_sockaddr *ast_sdp_state_get_connection_address(const struct ast_sdp_state *sdp_state)
-{
-	ast_assert(sdp_state != NULL);
-
-	return &sdp_state->local_capabilities.connection_address;
-}
-
-int ast_sdp_state_get_stream_connection_address(const struct ast_sdp_state *sdp_state,
-	int stream_index, struct ast_sockaddr *address)
-{
-	struct sdp_state_stream *stream_state;
-	enum ast_media_type type;
-
-	ast_assert(sdp_state != NULL);
-	ast_assert(address != NULL);
-
-	stream_state = sdp_state_get_stream(sdp_state, stream_index);
-	if (!stream_state) {
-		return -1;
-	}
-
-	/* If an explicit connection address has been provided for the stream return it */
-	if (!ast_sockaddr_isnull(&stream_state->connection_address)) {
-		ast_sockaddr_copy(address, &stream_state->connection_address);
-		return 0;
-	}
-
-	type = ast_stream_get_type(ast_stream_topology_get_stream(sdp_state->local_capabilities.topology,
-		stream_index));
-
-	if (type == AST_MEDIA_TYPE_AUDIO || type == AST_MEDIA_TYPE_VIDEO) {
-		ast_rtp_instance_get_local_address(stream_state->instance, address);
-	} else {
-		return -1;
-	}
-
-	/* If an explicit global connection address is set use it here for the IP part */
-	if (!ast_sockaddr_isnull(&sdp_state->local_capabilities.connection_address)) {
-		int port = ast_sockaddr_port(address);
-
-		ast_sockaddr_copy(address, &sdp_state->local_capabilities.connection_address);
-		ast_sockaddr_set_port(address, port);
-	}
-
-	return 0;
-}
-
-const struct ast_stream_topology *ast_sdp_state_get_joint_topology(
-	const struct ast_sdp_state *sdp_state)
-{
-	ast_assert(sdp_state != NULL);
-	if (sdp_state->state == SDP_STATE_NEGOTIATED) {
-		return sdp_state->joint_capabilities.topology;
-	} else {
-		return sdp_state->local_capabilities.topology;
-	}
-}
-
-const struct ast_stream_topology *ast_sdp_state_get_local_topology(
-	const struct ast_sdp_state *sdp_state)
-{
-	ast_assert(sdp_state != NULL);
-
-	return sdp_state->local_capabilities.topology;
-}
-
-const struct ast_sdp_options *ast_sdp_state_get_options(
-	const struct ast_sdp_state *sdp_state)
-{
-	ast_assert(sdp_state != NULL);
-
-	return sdp_state->options;
-}
-
-#if 0
-static int merge_sdps(struct ast_sdp_state *sdp_state)
-{
-	ast_assert(sdp_state->local_sdp != NULL);
-	ast_assert(sdp_state->remote_sdp != NULL);
-	/* XXX STUB */
-	/* The goal of this function is to take
-	 * sdp_state->local_sdp and sdp_state->remote_sdp
-	 * and negotiate those into a joint SDP. This joint
-	 * SDP should be stored in sdp_state->joint_sdp. After
-	 * the joint SDP is created, the joint SDP should be
-	 * used to create the joint topology. Finally, if necessary,
-	 * the RTP session may need to be adjusted in some ways. For
-	 * instance, if we previously opened three ports for three
-	 * streams, but we negotiate down to two streams, then we
-	 * can shut down the port for the third stream. Similarly,
-	 * if we end up negotiating something like BUNDLE, then we may
-	 * need to tell the RTP layer to close ports and to multiplex
-	 * streams.
-	 */
-
-	return 0;
-}
-#endif
 
 /* TODO
  * This isn't set anywhere yet.
@@ -350,35 +164,592 @@
 	return rtp;
 }
 
-static int sdp_state_setup_local_streams(struct ast_sdp_state *sdp_state)
+static struct sdp_state_capabilities *sdp_initialize_state_capabilities(const struct ast_stream_topology *topology,
+	const struct ast_sdp_options *options)
 {
-	int stream_index;
+	struct sdp_state_capabilities *capabilities;
+	int i;
 
-	for (stream_index = 0; stream_index < AST_VECTOR_SIZE(&sdp_state->local_capabilities.streams); stream_index++) {
-		struct sdp_state_stream *stream_state_local = AST_VECTOR_GET_ADDR(&sdp_state->local_capabilities.streams, stream_index);
-		struct sdp_state_stream *stream_state_joint = NULL;
-		enum ast_media_type type_local = ast_stream_get_type(ast_stream_topology_get_stream(sdp_state->local_capabilities.topology, stream_index));
-		enum ast_media_type type_joint = AST_MEDIA_TYPE_UNKNOWN;
+	capabilities = ast_calloc(1, sizeof(*capabilities));
+	if (!capabilities) {
+		return NULL;
+	}
 
-		if (stream_index < AST_VECTOR_SIZE(&sdp_state->joint_capabilities.streams)) {
-			stream_state_joint = AST_VECTOR_GET_ADDR(&sdp_state->joint_capabilities.streams, stream_index);
-			type_joint = ast_stream_get_type(ast_stream_topology_get_stream(sdp_state->joint_capabilities.topology, stream_index));
+	capabilities->topology = ast_stream_topology_clone(topology);
+	if (!capabilities->topology) {
+		sdp_state_capabilities_free(capabilities);
+		return NULL;
+	}
+
+	if (AST_VECTOR_INIT(&capabilities->streams,
+			ast_stream_topology_get_count(topology))) {
+		sdp_state_capabilities_free(capabilities);
+		return NULL;
+	}
+	ast_sockaddr_setnull(&capabilities->connection_address);
+
+	for (i = 0; i < ast_stream_topology_get_count(topology); ++i) {
+		struct sdp_state_stream *state_stream;
+
+		state_stream = ast_calloc(1, sizeof(*state_stream));
+		if (!state_stream) {
+			return NULL;
 		}
 
-		/* If we can reuse an existing media stream then do so */
-		if (type_local == type_joint) {
-			if (type_local == AST_MEDIA_TYPE_AUDIO || type_local == AST_MEDIA_TYPE_VIDEO) {
-				stream_state_local->instance = ao2_bump(stream_state_joint->instance);
-				continue;
+		state_stream->instance = create_rtp(options,
+			ast_stream_get_type(ast_stream_topology_get_stream(topology, i)));
+		if (!state_stream->instance) {
+			sdp_state_stream_free(state_stream);
+			return NULL;
+		}
+
+		AST_VECTOR_APPEND(&capabilities->streams, state_stream);
+	}
+
+	return capabilities;
+}
+
+struct ast_sdp_state {
+	/*! Local capabilities, learned through configuration */
+	struct ast_stream_topology *local_capabilities;
+	/*! Current local state */
+	struct sdp_state_capabilities *current_capabilities;
+	/*! Remote capabilities, learned through remote SDP */
+	struct ast_stream_topology *remote_capabilities;
+	/*! Local SDP. Generated via the options and local capabilities. */
+	struct ast_sdp *local_sdp;
+	/*! SDP options. Configured options beyond media capabilities. */
+	struct ast_sdp_options *options;
+	/*! Translator that puts SDPs into the expected representation */
+	struct ast_sdp_translator *translator;
+	/*! The role that we occupy in SDP negotiation */
+	enum ast_sdp_role role;
+};
+
+struct ast_sdp_state *ast_sdp_state_alloc(struct ast_stream_topology *streams,
+	struct ast_sdp_options *options)
+{
+	struct ast_sdp_state *sdp_state;
+
+	sdp_state = ast_calloc(1, sizeof(*sdp_state));
+	if (!sdp_state) {
+		return NULL;
+	}
+
+	sdp_state->options = options;
+	sdp_state->local_capabilities = ast_stream_topology_clone(streams);
+
+	sdp_state->translator = ast_sdp_translator_new(ast_sdp_options_get_impl(sdp_state->options));
+	if (!sdp_state->translator) {
+		ast_sdp_state_free(sdp_state);
+		return NULL;
+	}
+
+	sdp_state->current_capabilities = sdp_initialize_state_capabilities(streams, options);
+	if (!sdp_state->current_capabilities) {
+		ast_sdp_state_free(sdp_state);
+		return NULL;
+	}
+
+	sdp_state->role = SDP_ROLE_NOT_SET;
+
+	return sdp_state;
+}
+
+void ast_sdp_state_free(struct ast_sdp_state *sdp_state)
+{
+	if (!sdp_state) {
+		return;
+	}
+
+	sdp_state_capabilities_free(sdp_state->current_capabilities);
+	ast_stream_topology_free(sdp_state->remote_capabilities);
+	ast_sdp_free(sdp_state->local_sdp);
+	ast_sdp_options_free(sdp_state->options);
+	ast_sdp_translator_free(sdp_state->translator);
+}
+
+static struct sdp_state_stream *sdp_state_get_stream(const struct ast_sdp_state *sdp_state, int stream_index)
+{
+	if (stream_index >= AST_VECTOR_SIZE(&sdp_state->current_capabilities->streams)) {
+		return NULL;
+	}
+
+	return AST_VECTOR_GET(&sdp_state->current_capabilities->streams, stream_index);
+}
+
+struct ast_rtp_instance *ast_sdp_state_get_rtp_instance(
+	const struct ast_sdp_state *sdp_state, int stream_index)
+{
+	struct sdp_state_stream *stream_state;
+
+	ast_assert(sdp_state != NULL);
+
+	stream_state = sdp_state_get_stream(sdp_state, stream_index);
+	if (!stream_state) {
+		return NULL;
+	}
+
+	return stream_state->instance;
+}
+
+const struct ast_sockaddr *ast_sdp_state_get_connection_address(const struct ast_sdp_state *sdp_state)
+{
+	ast_assert(sdp_state != NULL);
+
+	return &sdp_state->current_capabilities->connection_address;
+}
+
+int ast_sdp_state_get_stream_connection_address(const struct ast_sdp_state *sdp_state,
+	int stream_index, struct ast_sockaddr *address)
+{
+	struct sdp_state_stream *stream_state;
+	enum ast_media_type type;
+
+	ast_assert(sdp_state != NULL);
+	ast_assert(address != NULL);
+
+	stream_state = sdp_state_get_stream(sdp_state, stream_index);
+	if (!stream_state) {
+		return -1;
+	}
+
+	/* If an explicit connection address has been provided for the stream return it */
+	if (!ast_sockaddr_isnull(&stream_state->connection_address)) {
+		ast_sockaddr_copy(address, &stream_state->connection_address);
+		return 0;
+	}
+
+	type = ast_stream_get_type(ast_stream_topology_get_stream(sdp_state->current_capabilities->topology,
+		stream_index));
+
+	if (type == AST_MEDIA_TYPE_AUDIO || type == AST_MEDIA_TYPE_VIDEO) {
+		ast_rtp_instance_get_local_address(stream_state->instance, address);
+	} else {
+		return -1;
+	}
+
+	/* If an explicit global connection address is set use it here for the IP part */
+	if (!ast_sockaddr_isnull(&sdp_state->current_capabilities->connection_address)) {
+		int port = ast_sockaddr_port(address);
+
+		ast_sockaddr_copy(address, &sdp_state->current_capabilities->connection_address);
+		ast_sockaddr_set_port(address, port);
+	}
+
+	return 0;
+}
+
+const struct ast_stream_topology *ast_sdp_state_get_joint_topology(
+	const struct ast_sdp_state *sdp_state)
+{
+	ast_assert(sdp_state != NULL);
+
+	return sdp_state->current_capabilities->topology;
+}
+
+const struct ast_stream_topology *ast_sdp_state_get_local_topology(
+	const struct ast_sdp_state *sdp_state)
+{
+	ast_assert(sdp_state != NULL);
+
+	return sdp_state->local_capabilities;
+}
+
+const struct ast_sdp_options *ast_sdp_state_get_options(
+	const struct ast_sdp_state *sdp_state)
+{
+	ast_assert(sdp_state != NULL);
+
+	return sdp_state->options;
+}
+
+/*!
+ * \brief Merge two streams into a joint stream.
+ *
+ * \param local Our local stream
+ * \param remote A remote stream
+ * \retval NULL An error occurred
+ * \retval non-NULL The joint stream created
+ */
+static struct ast_stream *merge_streams(const struct ast_stream *local,
+	const struct ast_stream *remote)
+{
+	struct ast_stream *joint_stream;
+	struct ast_format_cap *joint_cap;
+	struct ast_format_cap *local_cap;
+	struct ast_format_cap *remote_cap;
+	struct ast_str *local_buf = ast_str_alloca(128);
+	struct ast_str *remote_buf = ast_str_alloca(128);
+	struct ast_str *joint_buf = ast_str_alloca(128);
+
+	joint_stream = ast_stream_alloc("goobers", ast_stream_get_type(remote));
+	if (!joint_stream) {
+		return NULL;
+	}
+
+	if (!local) {
+		return joint_stream;
+	}
+
+	joint_cap = ast_format_cap_alloc(AST_FORMAT_CAP_FLAG_DEFAULT);
+	if (!joint_cap) {
+		ast_stream_free(joint_stream);
+		return NULL;
+	}
+
+	local_cap = ast_stream_get_formats(local);
+	remote_cap = ast_stream_get_formats(remote);
+
+	ast_format_cap_get_compatible(local_cap, remote_cap, joint_cap);
+
+	ast_debug(3, "Combined local '%s' with remote '%s' to get joint '%s'. Joint has %zu formats\n",
+		ast_format_cap_get_names(local_cap, &local_buf),
+		ast_format_cap_get_names(remote_cap, &remote_buf),
+		ast_format_cap_get_names(joint_cap, &joint_buf),
+		ast_format_cap_count(joint_cap));
+
+	ast_stream_set_formats(joint_stream, joint_cap);
+
+	ao2_ref(joint_cap, -1);
+
+	return joint_stream;
+}
+
+/*!
+ * \brief Get a local stream that corresponds with a remote stream.
+ *
+ * \param local The local topology
+ * \param media_type The type of stream we are looking for
+ * \param[in,out] media_indices Keeps track of where to start searching in the topology
+ * \retval NULL No corresponding stream found
+ * \retval non-NULL The corresponding stream
+ */
+static int get_corresponding_index(const struct ast_stream_topology *local,
+	enum ast_media_type media_type, int *media_indices)
+{
+	int i;
+	int winner = -1;
+
+	for (i = media_indices[media_type]; i < ast_stream_topology_get_count(local); ++i) {
+		struct ast_stream *candidate;
+
+		candidate = ast_stream_topology_get_stream(local, i);
+		if (ast_stream_get_type(candidate) == media_type) {
+			winner = i;
+			break;
+		}
+	}
+
+	media_indices[media_type] = i + 1;
+	return winner;
+}
+
+/*!
+ * \brief Merge existing stream capabilities and a new topology into joint capabilities.
+ *
+ * This is a bit complicated. The idea is that we already have some capabilities set, and
+ * we've now been confronted with a new stream topology. We want to take what's been
+ * presented to us and merge those new capabilities with our own.
+ *
+ * For each of the new streams, we try to find a corresponding stream in our current
+ * capabilities. If we find one, then we get the compatible formats of the two streams
+ * and create a new stream with those formats set. We then will re-use the underlying
+ * media instance (such as an RTP instance) on this merged stream.
+ *
+ * The create_new parameter determines whether we should attempt to create new media
+ * instances.
+ * If we do not find a corresponding stream, then we create a new one. If the
+ * create_new parameter is true, this created stream is made a clone of the new stream,
+ * and a media instance is created. If the create_new parameter is not true, then the
+ * created stream has no formats set and no media instance is created for it.
+ *
+ * \param current Current capabilities of the SDP state (may be NULL)
+ * \param options The options set on the SDP state
+ * \param new_topology The new topology to base merged capabilities on
+ * \retval NULL An error occurred
+ * \retval non-NULL The merged capabilities
+ */
+static struct sdp_state_capabilities *merge_capabilities(const struct sdp_state_capabilities *current,
+	const struct ast_sdp_options *options, const struct ast_stream_topology *new_topology)
+{
+	struct sdp_state_capabilities *joint_capabilities;
+	struct ast_stream_topology *topology;
+	int media_indices[AST_MEDIA_TYPE_END] = {0};
+	int i;
+
+	ast_assert(current != NULL);
+
+	joint_capabilities = ast_calloc(1, sizeof(*joint_capabilities));
+	if (!joint_capabilities) {
+		return NULL;
+	}
+
+	joint_capabilities->topology = ast_stream_topology_alloc();
+	if (!joint_capabilities->topology) {
+		goto fail;
+	}
+
+	AST_VECTOR_INIT(&joint_capabilities->streams, AST_VECTOR_SIZE(&current->streams));
+	ast_sockaddr_copy(&joint_capabilities->connection_address, &current->connection_address);
+	topology = current->topology;
+
+	for (i = 0; i < ast_stream_topology_get_count(new_topology); ++i) {
+		enum ast_media_type new_stream_type;
+		struct ast_stream *new_stream;
+		struct ast_stream *current_stream;
+		struct ast_stream *joint_stream;
+		struct sdp_state_stream *current_state_stream;
+		struct sdp_state_stream *joint_state_stream;
+		int current_index;
+
+		joint_state_stream = ast_calloc(1, sizeof(*joint_state_stream));
+		if (!joint_state_stream) {
+			goto fail;
+		}
+
+		new_stream = ast_stream_topology_get_stream(new_topology, i);
+		new_stream_type = ast_stream_get_type(new_stream);
+
+		current_index = get_corresponding_index(topology, new_stream_type, media_indices);
+
+		if (current_index >= 0) {
+			current_stream = ast_stream_topology_get_stream(topology, current_index);
+			joint_stream = merge_streams(current_stream, new_stream);
+			if (!joint_stream) {
+				goto fail;
+			}
+
+			current_state_stream = AST_VECTOR_GET(&current->streams, current_index);
+			joint_state_stream->instance = ao2_bump(current_state_stream->instance);
+
+			if (!ast_sockaddr_isnull(&current_state_stream->connection_address)) {
+				ast_sockaddr_copy(&joint_state_stream->connection_address, &current_state_stream->connection_address);
+			} else {
+				ast_sockaddr_setnull(&joint_state_stream->connection_address);
+			}
+			joint_state_stream->locally_held = current_state_stream->locally_held;
+		} else {
+			joint_stream = ast_stream_alloc("dummy", new_stream_type);
+			if (!joint_stream) {
+				goto fail;
 			}
 		}
 
-		if (type_local == AST_MEDIA_TYPE_AUDIO || type_local == AST_MEDIA_TYPE_VIDEO) {
-			/* We need to create a new RTP instance */
-			stream_state_local->instance = create_rtp(sdp_state->options, type_local);
-			if (!stream_state_local->instance) {
-				return -1;
-			}
+		ast_stream_topology_append_stream(joint_capabilities->topology, joint_stream);
+		AST_VECTOR_APPEND(&joint_capabilities->streams, joint_state_stream);
+	}
+
+	return joint_capabilities;
+
+fail:
+	sdp_state_capabilities_free(joint_capabilities);
+	return NULL;
+}
+
+/*!
+ * \brief Apply remote SDP's ICE information to our RTP session
+ *
+ * \param state The SDP state on which negotiation has taken place
+ * \param options The SDP options we support
+ * \param remote_sdp The SDP we most recently received
+ * \param remote_m_line The stream on which we are examining ICE candidates
+ */
+static void update_ice(const struct ast_sdp_state *state, struct ast_rtp_instance *rtp, const struct ast_sdp_options *options,
+	const struct ast_sdp *remote_sdp, const struct ast_sdp_m_line *remote_m_line)
+{
+	struct ast_rtp_engine_ice *ice;
+	const struct ast_sdp_a_line *attr;
+	unsigned int attr_i;
+
+	/* If ICE support is not enabled or available exit early */
+	if (ast_sdp_options_get_ice(options) != AST_SDP_ICE_ENABLED_STANDARD || !(ice = ast_rtp_instance_get_ice(rtp))) {
+		return;
+	}
+
+	attr = ast_sdp_m_find_attribute(remote_m_line, "ice-ufrag", -1);
+	if (!attr) {
+		attr = ast_sdp_find_attribute(remote_sdp, "ice-ufrag", -1);
+	}
+	if (attr) {
+		ice->set_authentication(rtp, attr->value, NULL);
+	} else {
+		return;
+	}
+
+	attr = ast_sdp_m_find_attribute(remote_m_line, "ice-pwd", -1);
+	if (!attr) {
+		attr = ast_sdp_find_attribute(remote_sdp, "ice-pwd", -1);
+	}
+	if (attr) {
+		ice->set_authentication(rtp, NULL, attr->value);
+	} else {
+		return;
+	}
+
+	if (ast_sdp_m_find_attribute(remote_m_line, "ice-lite", -1)) {
+		ice->ice_lite(rtp);
+	}
+
+	/* Find all of the candidates */
+	for (attr_i = 0; attr_i < ast_sdp_m_get_a_count(remote_m_line); ++attr_i) {
+		char foundation[32];
+		char transport[32];
+		char address[INET6_ADDRSTRLEN + 1];
+		char cand_type[6];
+		char relay_address[INET6_ADDRSTRLEN + 1] = "";
+		unsigned int port;
+		unsigned int relay_port = 0;
+		struct ast_rtp_engine_ice_candidate candidate = { 0, };
+
+		attr = ast_sdp_m_get_a(remote_m_line, attr_i);
+
+		/* If this is not a candidate line skip it */
+		if (strcmp(attr->name, "candidate")) {
+			continue;
+		}
+
+		if (sscanf(attr->value, "%31s %30u %31s %30u %46s %30u typ %5s %*s %23s %*s %30u",
+			foundation, &candidate.id, transport, (unsigned *)&candidate.priority, address,
+			&port, cand_type, relay_address, &relay_port) < 7) {
+			/* Candidate did not parse properly */
+			continue;
+		}
+
+		if (ast_sdp_options_get_rtcp_mux(options)
+			&& ast_sdp_m_find_attribute(remote_m_line, "rtcp-mux", -1)
+			&& candidate.id > 1) {
+			/* Remote side may have offered RTP and RTCP candidates. However, if we're using RTCP MUX,
+			 * then we should ignore RTCP candidates.
+			 */
+			continue;
+		}
+
+		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 {
+			continue;
+		}
+
+		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(rtp, &candidate);
+	}
+
+	if (state->role == SDP_ROLE_OFFERER) {
+		ice->set_role(rtp, AST_RTP_ICE_ROLE_CONTROLLING);
+	} else {
+		ice->set_role(rtp, AST_RTP_ICE_ROLE_CONTROLLED);
+	}
+
+	ice->start(rtp);
+}
+
+/*!
+ * \brief Update RTP instances based on merged SDPs
+ *
+ * RTP instances, when first allocated, cannot make assumptions about what the other
+ * side supports and thus has to go with some default behaviors. This function gets
+ * called after we know both what we support and what the remote endpoint supports.
+ * This way, we can update the RTP instance to reflect what is supported by both
+ * sides.
+ *
+ * \param state The SDP state in which SDPs have been negotiated
+ * \param rtp The RTP instance that is being updated
+ * \param options Our locally-supported SDP options
+ * \param remote_sdp The SDP we most recently received
+ * \param remote_m_line The remote SDP stream that corresponds to the RTP instance we are modifying
+ */
+static void update_rtp_after_merge(const struct ast_sdp_state *state, struct ast_rtp_instance *rtp,
+    const struct ast_sdp_options *options,
+	const struct ast_sdp *remote_sdp,
+	const struct ast_sdp_m_line *remote_m_line)
+{
+	if (ast_sdp_options_get_rtcp_mux(options) && ast_sdp_m_find_attribute(remote_m_line, "rtcp-mux", -1)) {
+		ast_rtp_instance_set_prop(rtp, AST_RTP_PROPERTY_RTCP, AST_RTP_INSTANCE_RTCP_MUX);
+	} else {
+		ast_rtp_instance_set_prop(rtp, AST_RTP_PROPERTY_RTCP, AST_RTP_INSTANCE_RTCP_STANDARD);
+	}
+
+	if (ast_sdp_options_get_ice(options) == AST_SDP_ICE_ENABLED_STANDARD) {
+		update_ice(state, rtp, options, remote_sdp, remote_m_line);
+	}
+}
+
+static void set_stream_capabilities(struct ast_sdp_state *sdp_state,
+	struct sdp_state_capabilities *new_capabilities)
+{
+	struct sdp_state_capabilities *old_capabilities = sdp_state->current_capabilities;
+
+	sdp_state->current_capabilities = new_capabilities;
+	sdp_state_capabilities_free(old_capabilities);
+}
+
+static struct ast_sdp *sdp_create_from_state(const struct ast_sdp_state *sdp_state);
+
+/*!
+ * \brief Merge SDPs into a joint SDP.
+ *
+ * This function is used to take a remote SDP and merge it with our local
+ * capabilities to produce a new local SDP. After creating the new local SDP,
+ * it then iterates through media instances and updates them as necessary. For
+ * instance, if a specific RTP feature is supported by both us and the far end,
+ * then we can ensure that the feature is enabled.
+ *
+ * \param sdp_state The current SDP state
+ * \retval -1 Failure
+ * \retval 0 Success
+ */
+static int merge_sdps(struct ast_sdp_state *sdp_state, const struct ast_sdp *remote_sdp)
+{
+	struct sdp_state_capabilities *joint_capabilities;
+	int i;
+
+	sdp_state->remote_capabilities = ast_get_topology_from_sdp(remote_sdp);
+	if (!sdp_state->remote_capabilities) {
+		return -1;
+	}
+
+	joint_capabilities = merge_capabilities(sdp_state->current_capabilities,
+		sdp_state->options, sdp_state->remote_capabilities);
+	if (!joint_capabilities) {
+		return -1;
+	}
+	set_stream_capabilities(sdp_state, joint_capabilities);
+
+	if (sdp_state->local_sdp) {
+		ast_sdp_free(sdp_state->local_sdp);
+		sdp_state->local_sdp = NULL;
+	}
+
+	sdp_state->local_sdp = sdp_create_from_state(sdp_state);
+	if (!sdp_state->local_sdp) {
+		return -1;
+	}
+
+	for (i = 0; i < AST_VECTOR_SIZE(&joint_capabilities->streams); ++i) {
+		struct sdp_state_stream *state_stream;
+
+		state_stream = AST_VECTOR_GET(&joint_capabilities->streams, i);
+		if (state_stream->instance) {
+			update_rtp_after_merge(sdp_state, state_stream->instance, sdp_state->options,
+				remote_sdp, ast_sdp_get_m(remote_sdp, i));
 		}
 	}
 
@@ -389,11 +760,10 @@
 {
 	ast_assert(sdp_state != NULL);
 
-	if (!sdp_state->local_sdp) {
-		if (sdp_state_setup_local_streams(sdp_state)) {
-			return NULL;
-		}
-		sdp_state->local_sdp = ast_sdp_create_from_state(sdp_state);
+	if (sdp_state->role == SDP_ROLE_NOT_SET) {
+		ast_assert(sdp_state->local_sdp == NULL);
+		sdp_state->role = SDP_ROLE_OFFERER;
+		sdp_state->local_sdp = sdp_create_from_state(sdp_state);
 	}
 
 	return sdp_state->local_sdp;
@@ -410,11 +780,15 @@
 	return ast_sdp_translator_from_sdp(sdp_state->translator, sdp);
 }
 
-void ast_sdp_state_set_remote_sdp(struct ast_sdp_state *sdp_state, struct ast_sdp *sdp)
+void ast_sdp_state_set_remote_sdp(struct ast_sdp_state *sdp_state, const struct ast_sdp *sdp)
 {
 	ast_assert(sdp_state != NULL);
 
-	sdp_state->remote_sdp = sdp;
+	if (sdp_state->role == SDP_ROLE_NOT_SET) {
+		sdp_state->role = SDP_ROLE_ANSWERER;
+	}
+
+	merge_sdps(sdp_state, sdp);
 }
 
 int ast_sdp_state_set_remote_sdp_from_impl(struct ast_sdp_state *sdp_state, void *remote)
@@ -427,8 +801,7 @@
 	if (!sdp) {
 		return -1;
 	}
-
-	sdp_state->remote_sdp = sdp;
+	ast_sdp_state_set_remote_sdp(sdp_state, sdp);
 
 	return 0;
 }
@@ -440,19 +813,12 @@
 	ast_sdp_free(sdp_state->local_sdp);
 	sdp_state->local_sdp = NULL;
 
-	ast_sdp_free(sdp_state->remote_sdp);
-	sdp_state->remote_sdp = NULL;
-
-	ast_sdp_free(sdp_state->joint_sdp);
-	sdp_state->joint_sdp = NULL;
-
 	ast_stream_topology_free(sdp_state->remote_capabilities);
 	sdp_state->remote_capabilities = NULL;
 
-	ast_stream_topology_free(sdp_state->joint_capabilities.topology);
-	sdp_state->joint_capabilities.topology = NULL;
+	set_stream_capabilities(sdp_state, NULL);
 
-	sdp_state->state = SDP_STATE_INITIAL;
+	sdp_state->role = SDP_ROLE_NOT_SET;
 
 	return 0;
 }
@@ -462,13 +828,9 @@
 	ast_assert(sdp_state != NULL);
 	ast_assert(streams != NULL);
 
-	sdp_state_capabilities_free(&sdp_state->local_capabilities);
-	sdp_state->local_capabilities.topology = ast_stream_topology_clone(streams);
-	if (!sdp_state->local_capabilities.topology) {
-		return -1;
-	}
-
-	if (AST_VECTOR_INIT(&sdp_state->local_capabilities.streams, ast_stream_topology_get_count(streams))) {
+	ast_stream_topology_free(sdp_state->local_capabilities);
+	sdp_state->local_capabilities = ast_stream_topology_clone(streams);
+	if (!sdp_state->local_capabilities) {
 		return -1;
 	}
 
@@ -480,9 +842,9 @@
 	ast_assert(sdp_state != NULL);
 
 	if (!address) {
-		ast_sockaddr_setnull(&sdp_state->local_capabilities.connection_address);
+		ast_sockaddr_setnull(&sdp_state->current_capabilities->connection_address);
 	} else {
-		ast_sockaddr_copy(&sdp_state->local_capabilities.connection_address, address);
+		ast_sockaddr_copy(&sdp_state->current_capabilities->connection_address, address);
 	}
 }
 
@@ -533,3 +895,80 @@
 
 	return stream_state->locally_held;
 }
+
+/*!
+ * \brief Create an SDP based on current SDP state
+ *
+ * \param sdp_state The current SDP state
+ * \retval NULL Failed to create SDP
+ * \retval non-NULL Newly-created SDP
+ */
+static struct ast_sdp *sdp_create_from_state(const struct ast_sdp_state *sdp_state)
+{
+	struct ast_sdp *sdp = NULL;
+	struct ast_stream_topology *topology;
+	const struct ast_sdp_options *options;
+	int stream_num;
+	struct ast_rtp_instance *rtp = NULL;
+	struct ast_sdp_o_line *o_line = NULL;
+	struct ast_sdp_c_line *c_line = NULL;
+	struct ast_sdp_s_line *s_line = NULL;
+	struct ast_sdp_t_line *t_line = NULL;
+	char *address_type;
+	struct timeval tv = ast_tvnow();
+	uint32_t t;
+	int stream_count;
+
+	options = ast_sdp_state_get_options(sdp_state);
+	topology = sdp_state->current_capabilities->topology;
+
+	t = tv.tv_sec + 2208988800UL;
+	address_type = (strchr(options->media_address, ':') ? "IP6" : "IP4");
+
+	o_line = ast_sdp_o_alloc(options->sdpowner, t, t, address_type, options->media_address);
+	if (!o_line) {
+		goto error;
+	}
+	c_line = ast_sdp_c_alloc(address_type, options->media_address);
+	if (!c_line) {
+		goto error;
+	}
+
+	s_line = ast_sdp_s_alloc(options->sdpsession);
+	if (!s_line) {
+		goto error;
+	}
+
+	sdp = ast_sdp_alloc(o_line, c_line, s_line, NULL);
+	if (!sdp) {
+		goto error;
+	}
+
+	stream_count = ast_stream_topology_get_count(topology);
+
+	for (stream_num = 0; stream_num < stream_count; stream_num++) {
+		enum ast_media_type type = ast_stream_get_type(ast_stream_topology_get_stream(topology, stream_num));
+
+		if (type == AST_MEDIA_TYPE_AUDIO || type == AST_MEDIA_TYPE_VIDEO) {
+			if (ast_sdp_add_m_from_rtp_stream(sdp, sdp_state, options, stream_num)) {
+				goto error;
+			}
+		}
+	}
+
+	return sdp;
+
+error:
+	if (sdp) {
+		ao2_cleanup(rtp);
+		ast_sdp_free(sdp);
+	} else {
+		ast_sdp_t_free(t_line);
+		ast_sdp_s_free(s_line);
+		ast_sdp_c_free(c_line);
+		ast_sdp_o_free(o_line);
+	}
+
+	return NULL;
+}
+
diff --git a/tests/test_sdp.c b/tests/test_sdp.c
new file mode 100644
index 0000000..c079cd4
--- /dev/null
+++ b/tests/test_sdp.c
@@ -0,0 +1,816 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2017, Digium Inc.
+ *
+ * Mark Michelson <mmmichelson at digium.com>
+ *
+ * See http://www.asterisk.org for more information about
+ * the Asterisk project. Please do not directly contact
+ * any of the maintainers of this project for assistance;
+ * the project provides a web site, mailing lists and IRC
+ * channels for your use.
+ *
+ * This program is free software, distributed under the terms of
+ * the GNU General Public License Version 2. See the LICENSE file
+ * at the top of the source tree.
+ */
+
+#include "asterisk.h"
+#include "asterisk/test.h"
+#include "asterisk/module.h"
+#include "asterisk/sdp.h"
+#include "asterisk/stream.h"
+#include "asterisk/format.h"
+#include "asterisk/format_cache.h"
+#include "asterisk/format_cap.h"
+
+static int validate_o_line(struct ast_test *test, const struct ast_sdp_o_line *o_line,
+	const char *sdpowner, const char *address_type, const char *address)
+{
+	if (!o_line) {
+		return -1;
+	}
+
+	if (strcmp(o_line->username, sdpowner)) {
+		ast_test_status_update(test, "Expected o-line SDP owner %s but got %s\n",
+			sdpowner, o_line->username);
+		return -1;
+	}
+
+	if (strcmp(o_line->address_type, address_type)) {
+		ast_test_status_update(test, "Expected o-line SDP address type %s but got %s\n",
+			address_type, o_line->address_type);
+		return -1;
+	}
+
+	if (strcmp(o_line->address, address)) {
+		ast_test_status_update(test, "Expected o-line SDP address %s but got %s\n",
+			address, o_line->address);
+		return -1;
+	}
+
+	ast_test_status_update(test, "SDP o-line is as expected!\n");
+	return 0;
+}
+
+static int validate_c_line(struct ast_test *test, const struct ast_sdp_c_line *c_line,
+	const char *address_type, const char *address)
+{
+	if (strcmp(c_line->address_type, address_type)) {
+		ast_test_status_update(test, "Expected c-line SDP address type %s but got %s\n",
+			address_type, c_line->address_type);
+		return -1;
+	}
+
+	if (strcmp(c_line->address, address)) {
+		ast_test_status_update(test, "Expected c-line SDP address %s but got %s\n",
+			address, c_line->address);
+		return -1;
+	}
+
+	ast_test_status_update(test, "SDP c-line is as expected!\n");
+	return 0;
+}
+
+static int validate_m_line(struct ast_test *test, const struct ast_sdp_m_line *m_line,
+	const char *media_type, int num_payloads)
+{
+	if (strcmp(m_line->type, media_type)) {
+		ast_test_status_update(test, "Expected m-line media type %s but got %s\n",
+			media_type, m_line->type);
+		return -1;
+	}
+
+	if (ast_sdp_m_get_payload_count(m_line) != num_payloads) {
+		ast_test_status_update(test, "Expected m-line payload count %d but got %d\n",
+			num_payloads, ast_sdp_m_get_payload_count(m_line));
+		return -1;
+	}
+
+	ast_test_status_update(test, "SDP m-line is as expected\n");
+	return 0;
+}
+
+static int validate_rtpmap(struct ast_test *test, const struct ast_sdp_m_line *m_line,
+	const char *media_name)
+{
+	struct ast_sdp_a_line *a_line;
+	int i;
+
+	for (i = 0; i < ast_sdp_m_get_a_count(m_line); ++i) {
+		struct ast_sdp_rtpmap *rtpmap;
+		int match;
+
+		a_line = ast_sdp_m_get_a(m_line, i);
+		if (strcmp(a_line->name, "rtpmap")) {
+			continue;
+		}
+
+		rtpmap = ast_sdp_a_get_rtpmap(a_line);
+		if (!rtpmap) {
+			return -1;
+		}
+
+		match = !strcmp(rtpmap->encoding_name, media_name);
+
+		ast_sdp_rtpmap_free(rtpmap);
+		if (match) {
+			return 0;
+		}
+	}
+
+	ast_test_status_update(test, "Could not find rtpmap with encoding name %s\n", media_name);
+
+	return -1;
+}
+
+AST_TEST_DEFINE(invalid_rtpmap)
+{
+	/* a=rtpmap: is already assumed. This is the part after that */
+	static const char *invalids[] = {
+		"J PCMU/8000",
+		"0 PCMU:8000",
+		"0 PCMU/EIGHT-THOUSAND",
+		"0 PCMU/8000million/2",
+		"0 PCMU//2",
+		"0 /8000/2",
+		"0 PCMU/8000/",
+		"0 PCMU/8000million",
+	};
+	int i;
+	enum ast_test_result_state res = AST_TEST_PASS;
+
+	switch(cmd) {
+	case TEST_INIT:
+		info->name = "invalid_rtpmap";
+		info->category = "/main/sdp/";
+		info->summary = "Ensure invalid rtpmaps are rejected";
+		info->description =
+			"Try to convert several invalid rtpmap attributes. If\n"
+			"any succeeds, the test fails.";
+		return AST_TEST_NOT_RUN;
+	case TEST_EXECUTE:
+		break;
+	}
+
+	for (i = 0; i < ARRAY_LEN(invalids); ++i) {
+		struct ast_sdp_a_line *a_line;
+		struct ast_sdp_rtpmap *rtpmap;
+
+		a_line = ast_sdp_a_alloc("rtpmap", invalids[i]);
+		rtpmap = ast_sdp_a_get_rtpmap(a_line);
+		if (rtpmap) {
+			ast_test_status_update(test, "Invalid rtpmap '%s' was accepted as valid\n",
+				invalids[i]);
+			res = AST_TEST_FAIL;
+		}
+		ast_sdp_a_free(a_line);
+		ast_sdp_rtpmap_free(rtpmap);
+	}
+
+	return res;
+}
+
+AST_TEST_DEFINE(rtpmap)
+{
+	static const char *valids[] = {
+		"0 PCMU/8000",
+		"107 opus/48000/2",
+	};
+	static int payloads[] = {
+		0,
+		107,
+	};
+	static const char *encoding_names[] = {
+		"PCMU",
+		"opus",
+	};
+	static int clock_rates[] = {
+		8000,
+		48000,
+	};
+	static const char *encoding_parameters[] = {
+		"",
+		"2",
+	};
+	int i;
+	enum ast_test_result_state res = AST_TEST_PASS;
+
+	switch(cmd) {
+	case TEST_INIT:
+		info->name = "rtpmap";
+		info->category = "/main/sdp/";
+		info->summary = "Ensure rtpmap attribute values are parsed correctly";
+		info->description =
+			"Parse several valid rtpmap attributes. Ensure that the parsed values\n"
+			"are what we expect";
+		return AST_TEST_NOT_RUN;
+	case TEST_EXECUTE:
+		break;
+	}
+
+	for (i = 0; i < ARRAY_LEN(valids); ++i) {
+		struct ast_sdp_a_line *a_line;
+		struct ast_sdp_rtpmap *rtpmap;
+
+		a_line = ast_sdp_a_alloc("rtpmap", valids[i]);
+		rtpmap = ast_sdp_a_get_rtpmap(a_line);
+		if (!rtpmap) {
+			ast_test_status_update(test, "Valid rtpmap '%s' was rejected as invalid\n",
+				valids[i]);
+			res = AST_TEST_FAIL;
+			continue;
+		}
+		if (rtpmap->payload != payloads[i]) {
+			ast_test_status_update(test, "RTPmap payload '%d' does not match expected '%d'\n",
+				rtpmap->payload, payloads[i]);
+			res = AST_TEST_FAIL;
+		}
+		if (strcmp(rtpmap->encoding_name, encoding_names[i])) {
+			ast_test_status_update(test, "RTPmap encoding_name '%s' does not match expected '%s'\n",
+				rtpmap->encoding_name, encoding_names[i]);
+			res = AST_TEST_FAIL;
+		}
+		if (rtpmap->clock_rate != clock_rates[i]) {
+			ast_test_status_update(test, "RTPmap clock rate '%d' does not match expected '%d'\n",
+				rtpmap->clock_rate, clock_rates[i]);
+			res = AST_TEST_FAIL;
+		}
+		if (strcmp(rtpmap->encoding_parameters, encoding_parameters[i])) {
+			ast_test_status_update(test, "RTPmap encoding_parameter '%s' does not match expected '%s'\n",
+				rtpmap->encoding_parameters, encoding_parameters[i]);
+			res = AST_TEST_FAIL;
+		}
+		ast_sdp_a_free(a_line);
+		ast_sdp_rtpmap_free(rtpmap);
+	}
+
+	return res;
+}
+
+AST_TEST_DEFINE(find_attr)
+{
+	enum ast_test_result_state res = AST_TEST_PASS;
+	struct ast_sdp_m_line *m_line;
+	struct ast_sdp_a_line *a_line;
+
+	switch(cmd) {
+	case TEST_INIT:
+		info->name = "find_attr";
+		info->category = "/main/sdp/";
+		info->summary = "Ensure that finding attributes works as expected";
+		info->description =
+			"An SDP m-line is created, and two attributes are added.\n"
+			"We then attempt a series of attribute-finding calls that are expected to work\n"
+			"followed by a series of attribute-finding calls that are expected fo fail.";
+		return AST_TEST_NOT_RUN;
+	case TEST_EXECUTE:
+		break;
+	}
+
+	m_line = ast_sdp_m_alloc("audio", 666, 1, "RTP/AVP", NULL);
+	if (!m_line) {
+		res = AST_TEST_FAIL;
+		goto end;
+	}
+	a_line = ast_sdp_a_alloc("foo", "0 bar");
+	if (!a_line) {
+		res = AST_TEST_FAIL;
+		goto end;
+	}
+	ast_sdp_m_add_a(m_line, a_line);
+
+	a_line = ast_sdp_a_alloc("baz", "howdy");
+	if (!a_line) {
+		res = AST_TEST_FAIL;
+		goto end;
+	}
+	ast_sdp_m_add_a(m_line, a_line);
+
+	/* These should work */
+	a_line = ast_sdp_m_find_attribute(m_line, "foo", 0);
+	if (!a_line) {
+		ast_test_status_update(test, "Failed to find attribute 'foo' with payload '0'\n");
+		res = AST_TEST_FAIL;
+	}
+	a_line = ast_sdp_m_find_attribute(m_line, "foo", -1);
+	if (!a_line) {
+		ast_test_status_update(test, "Failed to find attribute 'foo' with unspecified payload\n");
+		res = AST_TEST_FAIL;
+	}
+	a_line = ast_sdp_m_find_attribute(m_line, "baz", -1);
+	if (!a_line) {
+		ast_test_status_update(test, "Failed to find attribute 'baz' with unspecified payload\n");
+		res = AST_TEST_FAIL;
+	}
+
+	/* These should fail */
+	a_line = ast_sdp_m_find_attribute(m_line, "foo", 1);
+	if (a_line) {
+		ast_test_status_update(test, "Found non-existent attribute 'foo' with payload '1'\n");
+		res = AST_TEST_FAIL;
+	}
+	a_line = ast_sdp_m_find_attribute(m_line, "baz", 0);
+	if (a_line) {
+		ast_test_status_update(test, "Found non-existent attribute 'baz' with payload '0'\n");
+		res = AST_TEST_FAIL;
+	}
+	a_line = ast_sdp_m_find_attribute(m_line, "wibble", 0);
+	if (a_line) {
+		ast_test_status_update(test, "Found non-existent attribute 'wibble' with payload '0'\n");
+		res = AST_TEST_FAIL;
+	}
+	a_line = ast_sdp_m_find_attribute(m_line, "wibble", -1);
+	if (a_line) {
+		ast_test_status_update(test, "Found non-existent attribute 'foo' with unspecified payload\n");
+		res = AST_TEST_FAIL;
+	}
+
+end:
+	ast_sdp_m_free(m_line);
+	return res;
+}
+
+struct sdp_format {
+	enum ast_media_type type;
+	const char *formats;
+};
+
+static struct ast_sdp_state *build_sdp_state(int num_streams, const struct sdp_format *formats)
+{
+	struct ast_stream_topology *topology = NULL;
+	struct ast_sdp_state *state = NULL;
+	struct ast_sdp_options *options;
+	int i;
+
+	options = ast_sdp_options_alloc();
+	if (!options) {
+		goto end;
+	}
+	ast_sdp_options_set_media_address(options, "127.0.0.1");
+	ast_sdp_options_set_sdpowner(options, "me");
+	ast_sdp_options_set_rtp_engine(options, "asterisk");
+	ast_sdp_options_set_impl(options, AST_SDP_IMPL_PJMEDIA);
+
+	topology = ast_stream_topology_alloc();
+	if (!topology) {
+		goto end;
+	}
+
+	for (i = 0; i < num_streams; ++i) {
+		RAII_VAR(struct ast_format_cap *, caps, NULL, ao2_cleanup);
+		struct ast_stream *stream;
+
+		caps = ast_format_cap_alloc(AST_FORMAT_CAP_FLAG_DEFAULT);
+		if (!caps) {
+			goto end;
+		}
+		if (ast_format_cap_update_by_allow_disallow(caps, formats[i].formats, 1) < 0) {
+			goto end;
+		}
+		stream = ast_stream_alloc("sure_thing", formats[i].type);
+		if (!stream) {
+			goto end;
+		}
+		ast_stream_set_formats(stream, caps);
+		ast_stream_topology_append_stream(topology, stream);
+	}
+
+	state = ast_sdp_state_alloc(topology, options);
+	if (!state) {
+		goto end;
+	}
+
+end:
+	ast_stream_topology_free(topology);
+	if (!state) {
+		ast_sdp_options_free(options);
+	}
+
+	return state;
+}
+
+AST_TEST_DEFINE(topology_to_sdp)
+{
+	enum ast_test_result_state res = AST_TEST_FAIL;
+	struct ast_sdp_state *sdp_state = NULL;
+	const struct ast_sdp *sdp = NULL;
+	struct ast_sdp_m_line *m_line = NULL;
+	struct sdp_format formats[] = {
+		{ AST_MEDIA_TYPE_AUDIO, "ulaw,alaw,g722,opus" },
+		{ AST_MEDIA_TYPE_VIDEO, "h264,vp8" },
+	};
+
+	switch(cmd) {
+	case TEST_INIT:
+		info->name = "topology_to_sdp";
+		info->category = "/main/sdp/";
+		info->summary = "Convert a topology into an SDP";
+		info->description =
+			"Ensure SDPs get converted to expected stream topology";
+		return AST_TEST_NOT_RUN;
+	case TEST_EXECUTE:
+		break;
+	}
+
+	sdp_state = build_sdp_state(ARRAY_LEN(formats), formats);
+	if (!sdp_state) {
+		goto end;
+	}
+
+	sdp = ast_sdp_state_get_local_sdp(sdp_state);
+	if (!sdp) {
+		goto end;
+	}
+
+	if (validate_o_line(test, sdp->o_line, "me", "IP4", "127.0.0.1")) {
+		goto end;
+	}
+
+	if (validate_c_line(test, sdp->c_line, "IP4", "127.0.0.1")) {
+		goto end;
+	}
+
+	if (ast_sdp_get_m_count(sdp) != 2) {
+		ast_test_status_update(test, "Unexpected number of streams in generated SDP: %d\n",
+			ast_sdp_get_m_count(sdp));
+		goto end;
+	}
+
+	m_line = ast_sdp_get_m(sdp, 0);
+
+	if (validate_m_line(test, m_line, "audio", 4)) {
+		goto end;
+	}
+
+	if (validate_rtpmap(test, m_line, "PCMU")) {
+		goto end;
+	}
+
+	if (validate_rtpmap(test, m_line, "PCMA")) {
+		goto end;
+	}
+
+	if (validate_rtpmap(test, m_line, "G722")) {
+		goto end;
+	}
+
+	if (validate_rtpmap(test, m_line, "opus")) {
+		goto end;
+	}
+
+	m_line = ast_sdp_get_m(sdp, 1);
+	if (validate_m_line(test, m_line, "video", 2)) {
+		goto end;
+	}
+
+	if (validate_rtpmap(test, m_line, "VP8")) {
+		goto end;
+	}
+
+	if (validate_rtpmap(test, m_line, "H264")) {
+		goto end;
+	}
+
+	res = AST_TEST_PASS;
+
+end:
+	ast_sdp_state_free(sdp_state);
+	return res;
+}
+
+static int validate_formats(struct ast_test *test, struct ast_stream_topology *topology, int index,
+	enum ast_media_type type, int format_count, const char **expected_formats)
+{
+	struct ast_stream *stream;
+	struct ast_format_cap *caps;
+	struct ast_format *format;
+	int i;
+
+	stream = ast_stream_topology_get_stream(topology, index);
+	if (ast_stream_get_type(stream) != type) {
+		ast_test_status_update(test, "Unexpected stream type encountered\n");
+		return -1;
+	}
+	caps = ast_stream_get_formats(stream);
+
+	if (ast_format_cap_count(caps) != format_count) {
+		ast_test_status_update(test, "Unexpected format count '%d'. Expecting '%d'\n",
+			(int) ast_format_cap_count(caps), format_count);
+		return -1;
+	}
+
+	for (i = 0; i < ast_format_cap_count(caps); ++i) {
+		format = ast_format_cap_get_format(caps, i);
+		if (strcmp(ast_format_get_name(format), expected_formats[i])) {
+			ast_test_status_update(test, "Unexpected format '%s'at index %d. Expected '%s'\n",
+				ast_format_get_name(format), i, expected_formats[i]);
+			return -1;
+		}
+	}
+
+	return 0;
+}
+
+AST_TEST_DEFINE(sdp_to_topology)
+{
+	enum ast_test_result_state res = AST_TEST_PASS;
+	struct ast_sdp_state *sdp_state;
+	const struct ast_sdp *sdp;
+	struct ast_stream_topology *topology = NULL;
+	struct sdp_format sdp_formats[] = {
+		{ AST_MEDIA_TYPE_AUDIO, "ulaw,alaw,g722,opus" },
+		{ AST_MEDIA_TYPE_VIDEO, "h264,vp8" },
+	};
+	static const char *expected_audio_formats[] = {
+		"ulaw",
+		"alaw",
+		"g722",
+		"opus",
+	};
+	static const char *expected_video_formats[] = {
+		"h264",
+		"vp8",
+	};
+
+	switch(cmd) {
+	case TEST_INIT:
+		info->name = "sdp_to_topology";
+		info->category = "/main/sdp/";
+		info->summary = "Convert an SDP into a topology";
+		info->description =
+			"Ensure SDPs get converted to expected stream topology";
+		return AST_TEST_NOT_RUN;
+	case TEST_EXECUTE:
+		break;
+	}
+
+	sdp_state = build_sdp_state(ARRAY_LEN(sdp_formats), sdp_formats);
+	if (!sdp_state) {
+		res = AST_TEST_FAIL;
+		goto end;
+	}
+
+	sdp = ast_sdp_state_get_local_sdp(sdp_state);
+	if (!sdp) {
+		res = AST_TEST_FAIL;
+		goto end;
+	}
+
+	topology = ast_get_topology_from_sdp(sdp);
+
+	if (ast_stream_topology_get_count(topology) != 2) {
+		ast_test_status_update(test, "Unexpected topology count '%d'. Expecting 2\n",
+			ast_stream_topology_get_count(topology));
+		res = AST_TEST_FAIL;
+		goto end;
+	}
+
+	if (validate_formats(test, topology, 0, AST_MEDIA_TYPE_AUDIO,
+			ARRAY_LEN(expected_audio_formats), expected_audio_formats)) {
+		res = AST_TEST_FAIL;
+		goto end;
+	}
+
+	if (validate_formats(test, topology, 1, AST_MEDIA_TYPE_VIDEO,
+			ARRAY_LEN(expected_video_formats), expected_video_formats)) {
+		res = AST_TEST_FAIL;
+		goto end;
+	}
+
+end:
+	ast_sdp_state_free(sdp_state);
+	ast_stream_topology_free(topology);
+	return res;
+}
+
+static int validate_merged_sdp(struct ast_test *test, const struct ast_sdp *sdp)
+{
+	struct ast_sdp_m_line *m_line;
+
+	if (!sdp) {
+		return -1;
+	}
+
+	m_line = ast_sdp_get_m(sdp, 0);
+
+	if (validate_m_line(test, m_line, "audio", 1)) {
+		return -1;
+	}
+
+	if (validate_rtpmap(test, m_line, "PCMU")) {
+		return -1;
+	}
+
+	/* The other audio formats should *NOT* be present */
+	if (!validate_rtpmap(test, m_line, "PCMA")) {
+		return -1;
+	}
+
+	if (!validate_rtpmap(test, m_line, "G722")) {
+		return -1;
+	}
+
+	if (!validate_rtpmap(test, m_line, "opus")) {
+		return -1;
+	}
+
+	m_line = ast_sdp_get_m(sdp, 1);
+
+	if (validate_m_line(test, m_line, "video", 1)) {
+		return -1;
+	}
+
+	if (validate_rtpmap(test, m_line, "VP8")) {
+		return -1;
+	}
+
+	if (!validate_rtpmap(test, m_line, "H264")) {
+		return -1;
+	}
+
+	return 0;
+}
+
+AST_TEST_DEFINE(sdp_merge_symmetric)
+{
+	enum ast_test_result_state res = AST_TEST_PASS;
+	struct ast_sdp_state *sdp_state_offerer = NULL;
+	struct ast_sdp_state *sdp_state_answerer = NULL;
+	const struct ast_sdp *offerer_sdp;
+	const struct ast_sdp *answerer_sdp;
+
+	static const struct sdp_format offerer_formats[] = {
+		{ AST_MEDIA_TYPE_AUDIO, "ulaw,alaw,g722,opus" },
+		{ AST_MEDIA_TYPE_VIDEO, "h264,vp8" },
+	};
+	static const struct sdp_format answerer_formats[] = {
+		{ AST_MEDIA_TYPE_AUDIO, "ulaw" },
+		{ AST_MEDIA_TYPE_VIDEO, "vp8" },
+	};
+
+	switch(cmd) {
+	case TEST_INIT:
+		info->name = "sdp_merge_symmetric";
+		info->category = "/main/sdp/";
+		info->summary = "Merge two SDPs with symmetric stream types";
+		info->description =
+			"SDPs 1 and 2 each have one audio and one video stream (in that order).\n"
+			"SDP 1 offers to SDP 2, who answers. We ensure that both local SDPs have\n"
+			"the expected stream types and the expected formats";
+		return AST_TEST_NOT_RUN;
+	case TEST_EXECUTE:
+		break;
+	}
+
+	sdp_state_offerer = build_sdp_state(ARRAY_LEN(offerer_formats), offerer_formats);
+	if (!sdp_state_offerer) {
+		res = AST_TEST_FAIL;
+		goto end;
+	}
+
+	sdp_state_answerer = build_sdp_state(ARRAY_LEN(answerer_formats), answerer_formats);
+	if (!sdp_state_answerer) {
+		res = AST_TEST_FAIL;
+		goto end;
+	}
+
+	offerer_sdp = ast_sdp_state_get_local_sdp(sdp_state_offerer);
+	if (!offerer_sdp) {
+		res = AST_TEST_FAIL;
+		goto end;
+	}
+
+	ast_sdp_state_set_remote_sdp(sdp_state_answerer, offerer_sdp);
+	answerer_sdp = ast_sdp_state_get_local_sdp(sdp_state_answerer);
+	if (!answerer_sdp) {
+		res = AST_TEST_FAIL;
+		goto end;
+	}
+
+	ast_sdp_state_set_remote_sdp(sdp_state_offerer, answerer_sdp);
+
+	/* Get the offerer SDP again because it's now going to be the joint SDP */
+	offerer_sdp = ast_sdp_state_get_local_sdp(sdp_state_offerer);
+	if (validate_merged_sdp(test, offerer_sdp)) {
+		res = AST_TEST_FAIL;
+		goto end;
+	}
+	if (validate_merged_sdp(test, answerer_sdp)) {
+		res = AST_TEST_FAIL;
+		goto end;
+	}
+
+end:
+	ast_sdp_state_free(sdp_state_offerer);
+	ast_sdp_state_free(sdp_state_answerer);
+
+	return res;
+}
+
+AST_TEST_DEFINE(sdp_merge_crisscross)
+{
+	enum ast_test_result_state res = AST_TEST_PASS;
+	struct ast_sdp_state *sdp_state_offerer = NULL;
+	struct ast_sdp_state *sdp_state_answerer = NULL;
+	const struct ast_sdp *offerer_sdp;
+	const struct ast_sdp *answerer_sdp;
+
+	static const struct sdp_format offerer_formats[] = {
+		{ AST_MEDIA_TYPE_AUDIO, "ulaw,alaw,g722,opus" },
+		{ AST_MEDIA_TYPE_VIDEO, "h264,vp8" },
+	};
+	static const struct sdp_format answerer_formats[] = {
+		{ AST_MEDIA_TYPE_VIDEO, "vp8" },
+		{ AST_MEDIA_TYPE_AUDIO, "ulaw" },
+	};
+
+	switch(cmd) {
+	case TEST_INIT:
+		info->name = "sdp_merge_crisscross";
+		info->category = "/main/sdp/";
+		info->summary = "Merge two SDPs with symmetric stream types";
+		info->description =
+			"SDPs 1 and 2 each have one audio and one video stream. However, SDP 1 and\n"
+			"2 natively have the formats in a different order.\n"
+			"SDP 1 offers to SDP 2, who answers. We ensure that both local SDPs have\n"
+			"the expected stream types and the expected formats. Since SDP 1 was the\n"
+			"offerer, the format order on SDP 1 should determine the order of formats in the SDPs\n";
+		return AST_TEST_NOT_RUN;
+	case TEST_EXECUTE:
+		break;
+	}
+
+	sdp_state_offerer = build_sdp_state(ARRAY_LEN(offerer_formats), offerer_formats);
+	if (!sdp_state_offerer) {
+		res = AST_TEST_FAIL;
+		goto end;
+	}
+
+	sdp_state_answerer = build_sdp_state(ARRAY_LEN(answerer_formats), answerer_formats);
+	if (!sdp_state_answerer) {
+		res = AST_TEST_FAIL;
+		goto end;
+	}
+
+	offerer_sdp = ast_sdp_state_get_local_sdp(sdp_state_offerer);
+	if (!offerer_sdp) {
+		res = AST_TEST_FAIL;
+		goto end;
+	}
+
+	ast_sdp_state_set_remote_sdp(sdp_state_answerer, offerer_sdp);
+	answerer_sdp = ast_sdp_state_get_local_sdp(sdp_state_answerer);
+	if (!answerer_sdp) {
+		res = AST_TEST_FAIL;
+		goto end;
+	}
+
+	ast_sdp_state_set_remote_sdp(sdp_state_offerer, answerer_sdp);
+
+	/* Get the offerer SDP again because it's now going to be the joint SDP */
+	offerer_sdp = ast_sdp_state_get_local_sdp(sdp_state_offerer);
+	if (validate_merged_sdp(test, offerer_sdp)) {
+		res = AST_TEST_FAIL;
+		goto end;
+	}
+	if (validate_merged_sdp(test, answerer_sdp)) {
+		res = AST_TEST_FAIL;
+		goto end;
+	}
+
+end:
+	ast_sdp_state_free(sdp_state_offerer);
+	ast_sdp_state_free(sdp_state_answerer);
+
+	return res;
+}
+
+static int unload_module(void)
+{
+	AST_TEST_UNREGISTER(invalid_rtpmap);
+	AST_TEST_UNREGISTER(rtpmap);
+	AST_TEST_UNREGISTER(find_attr);
+	AST_TEST_UNREGISTER(topology_to_sdp);
+	AST_TEST_UNREGISTER(sdp_to_topology);
+	AST_TEST_UNREGISTER(sdp_merge_symmetric);
+	AST_TEST_UNREGISTER(sdp_merge_crisscross);
+
+	return 0;
+}
+
+static int load_module(void)
+{
+	AST_TEST_REGISTER(invalid_rtpmap);
+	AST_TEST_REGISTER(rtpmap);
+	AST_TEST_REGISTER(find_attr);
+	AST_TEST_REGISTER(topology_to_sdp);
+	AST_TEST_REGISTER(sdp_to_topology);
+	AST_TEST_REGISTER(sdp_merge_symmetric);
+	AST_TEST_REGISTER(sdp_merge_crisscross);
+
+	return AST_MODULE_LOAD_SUCCESS;
+}
+
+AST_MODULE_INFO_STANDARD(ASTERISK_GPL_KEY, "SDP tests");

-- 
To view, visit https://gerrit.asterisk.org/5415
To unsubscribe, visit https://gerrit.asterisk.org/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: I5938c2be3c6f0a003aa88a39a59e0880f8b2df3d
Gerrit-PatchSet: 1
Gerrit-Project: asterisk
Gerrit-Branch: master
Gerrit-Owner: Mark Michelson <mmichelson at digium.com>



More information about the asterisk-code-review mailing list