[Asterisk-code-review] SDP: Rework SDP offer/answer model and update capabilities m... (asterisk[master])

Joshua Colp asteriskteam at digium.com
Wed Jul 26 08:20:35 CDT 2017


Joshua Colp has submitted this change and it was merged. ( https://gerrit.asterisk.org/5819 )

Change subject: SDP: Rework SDP offer/answer model and update capabilities merges.
......................................................................

SDP: Rework SDP offer/answer model and update capabilities merges.

The SDP offer/answer model requires an answer to an offer before a new SDP
can be processed.  This allows our local SDP creation to be deferred until
we know that we need to create an offer or an answer SDP.  Once the local
SDP is created it won't change until the SDP negotiation is restarted.

An offer SDP in an initial SIP INVITE can receive more than one answer
SDP.  In this case, we need to merge each answer SDP with our original
offer capabilities to get the currently negotiated capabilities.  To
satisfy this requirement means that we cannot update our proposed
capabilities until the negotiations are restarted.

Local topology updates from ast_sdp_state_update_local_topology() are
merged together until the next offer SDP is created.  These accumulated
updates are then merged with the current negotiated capabilities to create
the new proposed capabilities that the offer SDP is built.

Local topology updates are merged in several passes to attempt to be smart
about how streams from the system are matched with the previously
negotiated stream slots.  To allow for T.38 support when merging, type
matching considers audio and image types to be equivalent.  First streams
are matched by stream name and type.  Then streams are matched by stream
type only.  Any remaining unmatched existing streams are declined.  Any
new active streams are either backfilled into pre-merge declined slots or
appended onto the end of the merged topology.  Any excess new streams
above the maximum supported number of streams are simply discarded.

Remote topology negotiation merges depend if the topology is an offer or
answer.  An offer remote topology negotiation dictates the stream slot
ordering and new streams can be added.  A remote offer can do anything to
the previously negotiated streams except reduce the number of stream
slots.  An answer remote topology negotiation is limited to what our offer
requested.  The answer can only decline streams, pick codecs from the
offered list, or indicate the remote's stream hold state.

I had originally kept the RTP instance if the remote offer SDP changed a
stream type between audio and video since they both use RTP.  However, I
later removed this support in favor of simply creating a new RTP instance
since the stream's purpose has to be changing anyway.  Any RTP packets
from the old stream type might cause mischief for the bridged peer.

* Added ast_sdp_state_restart_negotiations() to restart the SDP
offer/answer negotiations.  We will thus know to create a new local SDP
when it is time to create an offer or answer.

* Removed ast_sdp_state_reset().  Save the current topology before
starting T.38.  To recover from T.38 simply update the local topology to
the saved topology and restart the SDP negotiations to get the offer SDP
renegotiating the previous configuration.

* Allow initial topology for ast_sdp_state_alloc() to be NULL so an
initial remote offer SDP can dictate the streams we start with.  We can
always update the local topology later if it turns out we need to offer
SDP first because the remote chose to defer sending us a SDP.

* Made the ast_sdp_state_alloc() initial topology limit to max_streams,
limit to configured codecs, handle declined streams, and discard
unsupported types.

* Convert struct ast_sdp to ao2 object.  Needed to easily save off a
remote SDP to refer to later for various reasons such as generating
declined m= lines in the local SDP.

* Improve converting remote SDP streams to a topology including stream
state.  A stream state of AST_STREAM_STATE_REMOVED indicates the stream is
declined/dead.

* Improve merging streams to take into account the stream state.

* Added query for remote hold state.

* Added maximum streams allowed SDP config option.

* Added ability to create new streams as needed.  New streams are created
with configured default audio, video, or image codecs depending on stream
type.

* Added global locally_held state along with a per stream local hold
state.  Historically, Asterisk only has a global locally held state
because when the we put the remote on hold we do it for all active
streams.

* Added queries for a rejected offer and current SDP negotiation role.
The rejected query allows the using module to know how to respond to a
failed remote SDP set.  Should the using module respond with a 488 Not
Acceptable Here or 500 Internal Error to the offer SDP?

* Moved sdp_state_capabilities.connection_address to ast_sdp_state.  There
seems no reason to keep it in the sdp_state_capabilities struct since it
was only used by the ast_sdp_state.proposed_capabilities instance.

* Callbacks are now available to allow the using module some customization
of negotiated streams and to complete setting up streams for use.  See the
typedef doxygen for each callback for what is allowable and when they are
called.
    * Added topology answerer modify callback.
    * Added topology pre and post apply callbacks.
    * Added topology offerer modify callback.
    * Added topology offerer configure callback.

* Had to rework the unit tests because I changed how SDP topologies are
merged.  Replaced several unit tests with new negotiation tests.

Change-Id: If07fe6d79fbdce33968a9401d41d908385043a06
---
M include/asterisk/sdp.h
M include/asterisk/sdp_options.h
M include/asterisk/sdp_state.h
M include/asterisk/stream.h
M main/sdp.c
M main/sdp_options.c
M main/sdp_private.h
M main/sdp_state.c
M main/stream.c
M res/res_sdp_translator_pjmedia.c
M tests/test_sdp.c
11 files changed, 3,400 insertions(+), 612 deletions(-)

Approvals:
  Joshua Colp: Looks good to me, but someone else must approve; Approved for Submit
  George Joseph: Looks good to me, approved



diff --git a/include/asterisk/sdp.h b/include/asterisk/sdp.h
index 224a0e5..7684695 100644
--- a/include/asterisk/sdp.h
+++ b/include/asterisk/sdp.h
@@ -254,16 +254,6 @@
 void ast_sdp_t_free(struct ast_sdp_t_line *t_line);
 
 /*!
- * \brief Free an SDP
- * Frees the sdp and all resources it contains
- *
- * \param sdp The sdp to free
- *
- * \since 15
- */
-void ast_sdp_free(struct ast_sdp *sdp);
-
-/*!
  * \brief Allocate an SDP Attribute
  *
  * \param name Attribute Name
@@ -544,7 +534,7 @@
 	int rtp_code, int asterisk_format, const struct ast_format *format, int code);
 
 /*!
- * \brief Create an SDP
+ * \brief Create an SDP ao2 object
  *
  * \param o_line Origin
  * \param c_line Connection
diff --git a/include/asterisk/sdp_options.h b/include/asterisk/sdp_options.h
index b8c1bbd..e45ae8c 100644
--- a/include/asterisk/sdp_options.h
+++ b/include/asterisk/sdp_options.h
@@ -20,6 +20,7 @@
 #define _ASTERISK_SDP_OPTIONS_H
 
 #include "asterisk/udptl.h"
+#include "asterisk/format_cap.h"
 
 struct ast_sdp_options;
 
@@ -78,6 +79,149 @@
 	/*! DTLS encryption */
 	AST_SDP_ENCRYPTION_DTLS,
 };
+
+/*!
+ * \brief Callback when processing an offer SDP for our answer SDP.
+ * \since 15.0.0
+ *
+ * \details
+ * This callback is called after merging our last negotiated topology
+ * with the remote's offer topology and before we have sent our answer
+ * SDP.  At this point you can alter new_topology streams.  You can
+ * decline, remove formats, or rename streams.  Changing anything else
+ * on the streams is likely to not end well.
+ *
+ * * To decline a stream simply set the stream state to
+ *   AST_STREAM_STATE_REMOVED.  You could implement a maximum number
+ *   of active streams of a given type policy.
+ *
+ * * To remove formats use the format API to remove any formats from a
+ *   stream.  The streams have the current joint negotiated formats.
+ *   Most likely you would want to remove all but the first format.
+ *
+ * * To rename a stream you need to clone the stream and give it a
+ *   new name and then set it in new_topology using
+ *   ast_stream_topology_set_stream().
+ *
+ * \note Removing all formats is an error.  You should decline the
+ * stream instead.
+ *
+ * \param context User supplied context data pointer for the SDP
+ * state.
+ * \param old_topology Active negotiated topology.  NULL if this is
+ * the first SDP negotiation.  The old topology is available so you
+ * can tell if any streams are new or changing type.
+ * \param new_topology New negotiated topology that we intend to
+ * generate the answer SDP.
+ *
+ * \return Nothing
+ */
+typedef void (*ast_sdp_answerer_modify_cb)(void *context,
+	const struct ast_stream_topology *old_topology,
+	struct ast_stream_topology *new_topology);
+
+/*!
+ * \internal
+ * \brief Callback when generating a topology for our SDP offer.
+ * \since 15.0.0
+ *
+ * \details
+ * This callback is called after merging any topology updates from the
+ * system by ast_sdp_state_update_local_topology() and before we have
+ * sent our offer SDP.  At this point you can alter new_topology
+ * streams.  You can decline, add/remove/update formats, or rename
+ * streams.  Changing anything else on the streams is likely to not
+ * end well.
+ *
+ * * To decline a stream simply set the stream state to
+ *   AST_STREAM_STATE_REMOVED.  You could implement a maximum number
+ *   of active streams of a given type policy.
+ *
+ * * To update formats use the format API to change formats of the
+ *   streams.  The streams have the current proposed formats.  You
+ *   could do whatever you want for formats but you should stay within
+ *   the configured formats for the stream type's endpoint.  However,
+ *   you should use ast_sdp_state_update_local_topology() instead of
+ *   this backdoor method.
+ *
+ * * To rename a stream you need to clone the stream and give it a
+ *   new name and then set it in new_topology using
+ *   ast_stream_topology_set_stream().
+ *
+ * \note Removing all formats is an error.  You should decline the
+ * stream instead.
+ *
+ * \note Declined new streams that are in slots higher than present in
+ * old_topology are removed so the SDP can be smaller.  The remote has
+ * never seen those slots so we shouldn't bother keeping them.
+ *
+ * \param context User supplied context data pointer for the SDP
+ * state.
+ * \param old_topology Active negotiated topology.  NULL if this is
+ * the first SDP negotiation.  The old topology is available so you
+ * can tell if any streams are new or changing type.
+ * \param new_topology Merged topology that we intend to generate the
+ * offer SDP.
+ *
+ * \return Nothing
+ */
+typedef void (*ast_sdp_offerer_modify_cb)(void *context,
+	const struct ast_stream_topology *old_topology,
+	struct ast_stream_topology *new_topology);
+
+/*!
+ * \brief Callback when generating an offer SDP to configure extra stream data.
+ * \since 15.0.0
+ *
+ * \details
+ * This callback is called after any ast_sdp_offerer_modify_cb
+ * callback and before we have sent our offer SDP.  The callback can
+ * call several SDP API calls to configure the proposed capabilities
+ * of streams before we create the SDP offer.  For example, the
+ * callback could configure a stream specific connection address, T.38
+ * parameters, RTP instance, or UDPTL instance parameters.
+ *
+ * \param context User supplied context data pointer for the SDP
+ * state.
+ * \param topology Topology ready to configure extra stream options.
+ *
+ * \return Nothing
+ */
+typedef void (*ast_sdp_offerer_config_cb)(void *context, const struct ast_stream_topology *topology);
+
+/*!
+ * \brief Callback before applying a topology.
+ * \since 15.0.0
+ *
+ * \details
+ * This callback is called before the topology is applied so the
+ * using module can do what is necessary before the topology becomes
+ * active.
+ *
+ * \param context User supplied context data pointer for the SDP
+ * state.
+ * \param topology Topology ready to be applied.
+ *
+ * \return Nothing
+ */
+typedef void (*ast_sdp_preapply_cb)(void *context, const struct ast_stream_topology *topology);
+
+/*!
+ * \brief Callback after applying a topology.
+ * \since 15.0.0
+ *
+ * \details
+ * This callback is called after the topology is applied so the
+ * using module can do what is necessary after the topology becomes
+ * active.
+ *
+ * \param context User supplied context data pointer for the SDP
+ * state.
+ * \param topology Topology already applied.
+ *
+ * \return Nothing
+ */
+typedef void (*ast_sdp_postapply_cb)(void *context, const struct ast_stream_topology *topology);
 
 /*!
  * \since 15.0.0
@@ -203,6 +347,24 @@
  * \returns rtp_engine
  */
 const char *ast_sdp_options_get_rtp_engine(const struct ast_sdp_options *options);
+
+void ast_sdp_options_set_state_context(struct ast_sdp_options *options, void *state_context);
+void *ast_sdp_options_get_state_context(const struct ast_sdp_options *options);
+
+void ast_sdp_options_set_answerer_modify_cb(struct ast_sdp_options *options, ast_sdp_answerer_modify_cb answerer_modify_cb);
+ast_sdp_answerer_modify_cb ast_sdp_options_get_answerer_modify_cb(const struct ast_sdp_options *options);
+
+void ast_sdp_options_set_offerer_modify_cb(struct ast_sdp_options *options, ast_sdp_offerer_modify_cb offerer_modify_cb);
+ast_sdp_offerer_modify_cb ast_sdp_options_get_offerer_modify_cb(const struct ast_sdp_options *options);
+
+void ast_sdp_options_set_offerer_config_cb(struct ast_sdp_options *options, ast_sdp_offerer_config_cb offerer_config_cb);
+ast_sdp_offerer_config_cb ast_sdp_options_get_offerer_config_cb(const struct ast_sdp_options *options);
+
+void ast_sdp_options_set_preapply_cb(struct ast_sdp_options *options, ast_sdp_preapply_cb preapply_cb);
+ast_sdp_preapply_cb ast_sdp_options_get_preapply_cb(const struct ast_sdp_options *options);
+
+void ast_sdp_options_set_postapply_cb(struct ast_sdp_options *options, ast_sdp_postapply_cb postapply_cb);
+ast_sdp_postapply_cb ast_sdp_options_get_postapply_cb(const struct ast_sdp_options *options);
 
 /*!
  * \since 15.0.0
@@ -505,6 +667,26 @@
 
 /*!
  * \since 15.0.0
+ * \brief Set SDP Options max_streams
+ *
+ * \param options SDP Options
+ * \param max_streams
+ */
+void ast_sdp_options_set_max_streams(struct ast_sdp_options *options,
+	unsigned int max_streams);
+
+/*!
+ * \since 15.0.0
+ * \brief Get SDP Options max_streams
+ *
+ * \param options SDP Options
+ *
+ * \returns max_streams
+ */
+unsigned int ast_sdp_options_get_max_streams(const struct ast_sdp_options *options);
+
+/*!
+ * \since 15.0.0
  * \brief Enable setting SSRC level attributes on SDPs
  *
  * \param options SDP Options
@@ -547,4 +729,46 @@
 struct ast_sched_context *ast_sdp_options_get_sched_type(const struct ast_sdp_options *options,
 	enum ast_media_type type);
 
+/*!
+ * \brief Set all allowed stream types to create new streams.
+ * \since 15.0.0
+ *
+ * \param options SDP Options
+ * \param cap Format capabilities to set all allowed stream types at once.
+ *            Could be NULL to disable creating any new streams.
+ *
+ * \return Nothing
+ */
+void ast_sdp_options_set_format_caps(struct ast_sdp_options *options,
+	struct ast_format_cap *cap);
+
+/*!
+ * \brief Set the SDP options format cap used to create new streams of the type.
+ * \since 15.0.0
+ *
+ * \param options SDP Options
+ * \param type Media type the format cap represents.
+ * \param cap Format capabilities to use for the specified media type.
+ *            Could be NULL to disable creating new streams of type.
+ *
+ * \return Nothing
+ */
+void ast_sdp_options_set_format_cap_type(struct ast_sdp_options *options,
+	enum ast_media_type type, struct ast_format_cap *cap);
+
+/*!
+ * \brief Get the SDP options format cap used to create new streams of the type.
+ * \since 15.0.0
+ *
+ * \param options SDP Options
+ * \param type Media type the format cap represents.
+ *
+ * \retval NULL if stream not allowed to be created.
+ * \retval cap to use in negotiating the new stream.
+ *
+ * \note The returned cap does not have its own ao2 ref.
+ */
+struct ast_format_cap *ast_sdp_options_get_format_cap_type(const struct ast_sdp_options *options,
+	enum ast_media_type type);
+
 #endif /* _ASTERISK_SDP_OPTIONS_H */
diff --git a/include/asterisk/sdp_state.h b/include/asterisk/sdp_state.h
index b8209e1..ec9d502 100644
--- a/include/asterisk/sdp_state.h
+++ b/include/asterisk/sdp_state.h
@@ -30,12 +30,22 @@
 /*!
  * \brief Allocate a new SDP state
  *
+ * \details
  * SDP state keeps tabs on everything SDP-related for a media session.
  * Most SDP operations will require the state to be provided.
  * Ownership of the SDP options is taken on by the SDP state.
  * A good strategy is to call this during session creation.
+ *
+ * \param topology Initial stream topology to offer.
+ *                NULL if we are going to be the answerer.  We can always
+ *                update the local topology later if it turns out we need
+ *                to be the offerer.
+ * \param options SDP options for the duration of the session.
+ *
+ * \retval SDP state struct
+ * \retval NULL on failure
  */
-struct ast_sdp_state *ast_sdp_state_alloc(struct ast_stream_topology *streams,
+struct ast_sdp_state *ast_sdp_state_alloc(struct ast_stream_topology *topology,
 	struct ast_sdp_options *options);
 
 /*!
@@ -86,6 +96,8 @@
  *
  * If this is called prior to receiving a remote SDP, then this will just mirror
  * the local configured endpoint capabilities.
+ *
+ * \note Cannot return NULL.  It is a BUG if it does.
  */
 const struct ast_stream_topology *ast_sdp_state_get_joint_topology(
 	const struct ast_sdp_state *sdp_state);
@@ -93,6 +105,7 @@
 /*!
  * \brief Get the local topology
  *
+ * \note Cannot return NULL.  It is a BUG if it does.
  */
 const struct ast_stream_topology *ast_sdp_state_get_local_topology(
 	const struct ast_sdp_state *sdp_state);
@@ -114,9 +127,10 @@
  * \retval NULL Failure
  *
  * \note
- * This function will allocate a new SDP with RTP instances if it has not already
- * been allocated.
- *
+ * This function will return the last local SDP created if one were
+ * previously requested for the current negotiation.  Otherwise it
+ * creates our SDP offer/answer depending on what role we are playing
+ * in the current negotiation.
  */
 const struct ast_sdp *ast_sdp_state_get_local_sdp(struct ast_sdp_state *sdp_state);
 
@@ -152,6 +166,7 @@
  *
  * \retval 0 Success
  * \retval non-0 Failure
+ *         Use ast_sdp_state_is_offer_rejected() to see if the SDP offer was rejected.
  *
  * \since 15
  */
@@ -165,39 +180,72 @@
  *
  * \retval 0 Success
  * \retval non-0 Failure
+ *         Use ast_sdp_state_is_offer_rejected() to see if the SDP offer was rejected.
  *
  * \since 15
  */
 int ast_sdp_state_set_remote_sdp_from_impl(struct ast_sdp_state *sdp_state, const void *remote);
 
 /*!
- * \brief Reset the SDP state and stream capabilities as if the SDP state had just been allocated.
+ * \brief Was the set remote offer rejected.
+ * \since 15.0.0
  *
  * \param sdp_state
- * \param remote The implementation's representation of an SDP.
+ *
+ * \retval 0 if not rejected.
+ * \retval non-zero if rejected.
+ */
+int ast_sdp_state_is_offer_rejected(struct ast_sdp_state *sdp_state);
+
+/*!
+ * \brief Are we the SDP offerer.
+ * \since 15.0.0
+ *
+ * \param sdp_state
+ *
+ * \retval 0 if we are not the offerer.
+ * \retval non-zero we are the offerer.
+ */
+int ast_sdp_state_is_offerer(struct ast_sdp_state *sdp_state);
+
+/*!
+ * \brief Are we the SDP answerer.
+ * \since 15.0.0
+ *
+ * \param sdp_state
+ *
+ * \retval 0 if we are not the answerer.
+ * \retval non-zero we are the answerer.
+ */
+int ast_sdp_state_is_answerer(struct ast_sdp_state *sdp_state);
+
+/*!
+ * \brief Restart the SDP offer/answer negotiations.
+ *
+ * \param sdp_state
  *
  * \retval 0 Success
- *
- * \note
- * This is most useful for when a channel driver is sending a session refresh message
- * and needs to re-advertise its initial capabilities instead of the previously-negotiated
- * joint capabilities.
- *
- * \since 15
+ * \retval non-0 Failure
  */
-int ast_sdp_state_reset(struct ast_sdp_state *sdp_state);
+int ast_sdp_state_restart_negotiations(struct ast_sdp_state *sdp_state);
 
 /*!
  * \brief Update the local stream topology on the SDP state.
  *
+ * \details
+ * Basically we are saving off any topology updates until we create the
+ * next SDP offer.  Repeated updates merge with the previous updated
+ * topology.
+ *
  * \param sdp_state
- * \param streams The new stream topology.
+ * \param topology The new stream topology.
  *
  * \retval 0 Success
+ * \retval non-0 Failure
  *
  * \since 15
  */
-int ast_sdp_state_update_local_topology(struct ast_sdp_state *sdp_state, struct ast_stream_topology *streams);
+int ast_sdp_state_update_local_topology(struct ast_sdp_state *sdp_state, struct ast_stream_topology *topology);
 
 /*!
  * \brief Set the local address (IP address) to use for connection addresses
@@ -231,7 +279,26 @@
 
 /*!
  * \since 15.0.0
- * \brief Set a stream to be held or unheld
+ * \brief Set the global locally held state.
+ *
+ * \param sdp_state
+ * \param locally_held
+ */
+void ast_sdp_state_set_global_locally_held(struct ast_sdp_state *sdp_state, unsigned int locally_held);
+
+/*!
+ * \since 15.0.0
+ * \brief Get the global locally held state.
+ *
+ * \param sdp_state
+ *
+ * \returns locally_held
+ */
+unsigned int ast_sdp_state_get_global_locally_held(const struct ast_sdp_state *sdp_state);
+
+/*!
+ * \since 15.0.0
+ * \brief Set a stream to be held or unheld locally
  *
  * \param sdp_state
  * \param stream_index The stream to set the held value for
@@ -239,6 +306,30 @@
  */
 void ast_sdp_state_set_locally_held(struct ast_sdp_state *sdp_state,
 	int stream_index, unsigned int locally_held);
+
+/*!
+ * \since 15.0.0
+ * \brief Get whether a stream is locally held or not
+ *
+ * \param sdp_state
+ * \param stream_index The stream to get the held state for
+ *
+ * \returns locally_held
+ */
+unsigned int ast_sdp_state_get_locally_held(const struct ast_sdp_state *sdp_state,
+	int stream_index);
+
+/*!
+ * \since 15.0.0
+ * \brief Get whether a stream is remotely held or not
+ *
+ * \param sdp_state
+ * \param stream_index The stream to get the held state for
+ *
+ * \returns remotely_held
+ */
+unsigned int ast_sdp_state_get_remotely_held(const struct ast_sdp_state *sdp_state,
+	int stream_index);
 
 /*!
  * \since 15.0.0
@@ -250,17 +341,5 @@
  */
 void ast_sdp_state_set_t38_parameters(struct ast_sdp_state *sdp_state,
 	int stream_index, struct ast_control_t38_parameters *params);
-
-/*!
- * \since 15.0.0
- * \brief Get whether a stream is held or not
- *
- * \param sdp_state
- * \param stream_index The stream to get the held state for
- *
- * \returns locally_held
- */
-unsigned int ast_sdp_state_get_locally_held(const struct ast_sdp_state *sdp_state,
-	int stream_index);
 
 #endif /* _ASTERISK_SDP_STATE_H */
diff --git a/include/asterisk/stream.h b/include/asterisk/stream.h
index 00169a3..21af53f 100644
--- a/include/asterisk/stream.h
+++ b/include/asterisk/stream.h
@@ -228,6 +228,17 @@
 const char *ast_stream_state2str(enum ast_stream_state state);
 
 /*!
+ * \brief Convert a string to a stream state
+ *
+ * \param str The string to convert
+ *
+ * \return The stream state
+ *
+ * \since 15.0.0
+ */
+enum ast_stream_state ast_stream_str2state(const char *str);
+
+/*!
  * \brief Get the opaque stream data
  *
  * \param stream The media stream
diff --git a/main/sdp.c b/main/sdp.c
index bfb83e8..fd10ba8 100644
--- a/main/sdp.c
+++ b/main/sdp.c
@@ -109,11 +109,9 @@
 	ast_free(t_line);
 }
 
-void ast_sdp_free(struct ast_sdp *sdp)
+static void ast_sdp_dtor(void *vdoomed)
 {
-	if (!sdp) {
-		return;
-	}
+	struct ast_sdp *sdp = vdoomed;
 
 	ast_sdp_o_free(sdp->o_line);
 	ast_sdp_s_free(sdp->s_line);
@@ -121,7 +119,6 @@
 	ast_sdp_t_free(sdp->t_line);
 	ast_sdp_a_lines_free(sdp->a_lines);
 	ast_sdp_m_lines_free(sdp->m_lines);
-	ast_free(sdp);
 }
 
 #define COPY_STR_AND_ADVANCE(p, dest, source) \
@@ -314,28 +311,28 @@
 {
 	struct ast_sdp *new_sdp;
 
-	new_sdp = ast_calloc(1, sizeof *new_sdp);
+	new_sdp = ao2_alloc_options(sizeof(*new_sdp), ast_sdp_dtor, AO2_ALLOC_OPT_LOCK_NOLOCK);
 	if (!new_sdp) {
 		return NULL;
 	}
 
 	new_sdp->a_lines = ast_calloc(1, sizeof(*new_sdp->a_lines));
 	if (!new_sdp->a_lines) {
-		ast_sdp_free(new_sdp);
+		ao2_ref(new_sdp, -1);
 		return NULL;
 	}
 	if (AST_VECTOR_INIT(new_sdp->a_lines, 20)) {
-		ast_sdp_free(new_sdp);
+		ao2_ref(new_sdp, -1);
 		return NULL;
 	}
 
 	new_sdp->m_lines = ast_calloc(1, sizeof(*new_sdp->m_lines));
 	if (!new_sdp->m_lines) {
-		ast_sdp_free(new_sdp);
+		ao2_ref(new_sdp, -1);
 		return NULL;
 	}
 	if (AST_VECTOR_INIT(new_sdp->m_lines, 20)) {
-		ast_sdp_free(new_sdp);
+		ao2_ref(new_sdp, -1);
 		return NULL;
 	}
 
@@ -741,6 +738,62 @@
 	}
 }
 
+/*!
+ * \internal
+ * \brief Determine the RTP stream direction in the given a lines.
+ * \since 15.0.0
+ *
+ * \param a_lines Attribute lines to search.
+ *
+ * \retval Stream direction on success.
+ * \retval AST_STREAM_STATE_REMOVED on failure.
+ *
+ * \return Nothing
+ */
+static enum ast_stream_state get_a_line_direction(const struct ast_sdp_a_lines *a_lines)
+{
+	if (a_lines) {
+		enum ast_stream_state direction;
+		int idx;
+		const struct ast_sdp_a_line *a_line;
+
+		for (idx = 0; idx < AST_VECTOR_SIZE(a_lines); ++idx) {
+			a_line = AST_VECTOR_GET(a_lines, idx);
+			direction = ast_stream_str2state(a_line->name);
+			if (direction != AST_STREAM_STATE_REMOVED) {
+				return direction;
+			}
+		}
+	}
+
+	return AST_STREAM_STATE_REMOVED;
+}
+
+/*!
+ * \internal
+ * \brief Determine the RTP stream direction.
+ * \since 15.0.0
+ *
+ * \param a_lines The SDP media global attributes
+ * \param m_line The SDP media section to convert
+ *
+ * \return Stream direction
+ */
+static enum ast_stream_state get_stream_direction(const struct ast_sdp_a_lines *a_lines, const struct ast_sdp_m_line *m_line)
+{
+	enum ast_stream_state direction;
+
+	direction = get_a_line_direction(m_line->a_lines);
+	if (direction != AST_STREAM_STATE_REMOVED) {
+		return direction;
+	}
+	direction = get_a_line_direction(a_lines);
+	if (direction != AST_STREAM_STATE_REMOVED) {
+		return direction;
+	}
+	return AST_STREAM_STATE_SENDRECV;
+}
+
 /*
  * Needed so we don't have an external function referenced as data.
  * The dynamic linker doesn't handle that very well.
@@ -758,13 +811,14 @@
  * 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 a_lines The SDP media global attributes
  * \param m_line The SDP media section to convert
  * \param g726_non_standard Non-zero if G.726 is non-standard
  *
  * \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 g726_non_standard)
+static struct ast_stream *get_stream_from_m(const struct ast_sdp_a_lines *a_lines, const struct ast_sdp_m_line *m_line, int g726_non_standard)
 {
 	int i;
 	int non_ast_fmts;
@@ -793,6 +847,14 @@
 			ao2_ref(caps, -1);
 			return NULL;
 		}
+		ast_stream_set_data(stream, AST_STREAM_DATA_RTP_CODECS, codecs,
+			(ast_stream_data_free_fn) rtp_codecs_free);
+
+		if (!m_line->port) {
+			/* Stream is declined.  There may not be any attributes. */
+			ast_stream_set_state(stream, AST_STREAM_STATE_REMOVED);
+			break;
+		}
 
 		options = g726_non_standard ? AST_RTP_OPT_G726_NONSTANDARD : 0;
 		for (i = 0; i < ast_sdp_m_get_payload_count(m_line); ++i) {
@@ -819,10 +881,16 @@
 		}
 
 		ast_rtp_codecs_payload_formats(codecs, caps, &non_ast_fmts);
-		ast_stream_set_data(stream, AST_STREAM_DATA_RTP_CODECS, codecs,
-			(ast_stream_data_free_fn) rtp_codecs_free);
+
+		ast_stream_set_state(stream, get_stream_direction(a_lines, m_line));
 		break;
 	case AST_MEDIA_TYPE_IMAGE:
+		if (!m_line->port) {
+			/* Stream is declined.  There may not be any attributes. */
+			ast_stream_set_state(stream, AST_STREAM_STATE_REMOVED);
+			break;
+		}
+
 		for (i = 0; i < ast_sdp_m_get_payload_count(m_line); ++i) {
 			struct ast_sdp_payload *payload;
 
@@ -830,12 +898,15 @@
 			payload = ast_sdp_m_get_payload(m_line, i);
 			if (!strcasecmp(payload->fmt, "t38")) {
 				ast_format_cap_append(caps, ast_format_t38, 0);
+				ast_stream_set_state(stream, AST_STREAM_STATE_SENDRECV);
 			}
 		}
 		break;
 	case AST_MEDIA_TYPE_UNKNOWN:
 	case AST_MEDIA_TYPE_TEXT:
 	case AST_MEDIA_TYPE_END:
+		/* Consider these unsupported streams as declined */
+		ast_stream_set_state(stream, AST_STREAM_STATE_REMOVED);
 		break;
 	}
 
@@ -858,7 +929,7 @@
 	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), g726_non_standard);
+		stream = get_stream_from_m(sdp->a_lines, ast_sdp_get_m(sdp, i), g726_non_standard);
 		if (!stream) {
 			/*
 			 * The topology cannot match the SDP because
diff --git a/main/sdp_options.c b/main/sdp_options.c
index a938583..79efbaf 100644
--- a/main/sdp_options.c
+++ b/main/sdp_options.c
@@ -27,6 +27,7 @@
 #define DEFAULT_ICE AST_SDP_ICE_DISABLED
 #define DEFAULT_IMPL AST_SDP_IMPL_STRING
 #define DEFAULT_ENCRYPTION AST_SDP_ENCRYPTION_DISABLED
+#define DEFAULT_MAX_STREAMS 16	/* Set to match our PJPROJECT PJMEDIA_MAX_SDP_MEDIA. */
 
 #define DEFINE_STRINGFIELD_GETTERS_SETTERS_FOR(field, assert_on_null) \
 void ast_sdp_options_set_##field(struct ast_sdp_options *options, const char *value) \
@@ -60,6 +61,12 @@
 DEFINE_STRINGFIELD_GETTERS_SETTERS_FOR(sdpsession, 0);
 DEFINE_STRINGFIELD_GETTERS_SETTERS_FOR(rtp_engine, 0);
 
+DEFINE_GETTERS_SETTERS_FOR(void *, state_context);
+DEFINE_GETTERS_SETTERS_FOR(ast_sdp_answerer_modify_cb, answerer_modify_cb);
+DEFINE_GETTERS_SETTERS_FOR(ast_sdp_offerer_modify_cb, offerer_modify_cb);
+DEFINE_GETTERS_SETTERS_FOR(ast_sdp_offerer_config_cb, offerer_config_cb);
+DEFINE_GETTERS_SETTERS_FOR(ast_sdp_preapply_cb, preapply_cb);
+DEFINE_GETTERS_SETTERS_FOR(ast_sdp_postapply_cb, postapply_cb);
 DEFINE_GETTERS_SETTERS_FOR(unsigned int, rtp_symmetric);
 DEFINE_GETTERS_SETTERS_FOR(unsigned int, udptl_symmetric);
 DEFINE_GETTERS_SETTERS_FOR(enum ast_t38_ec_modes, udptl_error_correction);
@@ -71,6 +78,7 @@
 DEFINE_GETTERS_SETTERS_FOR(unsigned int, cos_audio);
 DEFINE_GETTERS_SETTERS_FOR(unsigned int, tos_video);
 DEFINE_GETTERS_SETTERS_FOR(unsigned int, cos_video);
+DEFINE_GETTERS_SETTERS_FOR(unsigned int, max_streams);
 DEFINE_GETTERS_SETTERS_FOR(enum ast_sdp_options_dtmf, dtmf);
 DEFINE_GETTERS_SETTERS_FOR(enum ast_sdp_options_ice, ice);
 DEFINE_GETTERS_SETTERS_FOR(enum ast_sdp_options_impl, impl);
@@ -110,12 +118,87 @@
 	}
 }
 
+struct ast_format_cap *ast_sdp_options_get_format_cap_type(const struct ast_sdp_options *options,
+	enum ast_media_type type)
+{
+	struct ast_format_cap *cap = NULL;
+
+	switch (type) {
+	case AST_MEDIA_TYPE_AUDIO:
+	case AST_MEDIA_TYPE_VIDEO:
+	case AST_MEDIA_TYPE_IMAGE:
+	case AST_MEDIA_TYPE_TEXT:
+		cap = options->caps[type];
+		break;
+	case AST_MEDIA_TYPE_UNKNOWN:
+	case AST_MEDIA_TYPE_END:
+		break;
+	}
+	return cap;
+}
+
+void ast_sdp_options_set_format_cap_type(struct ast_sdp_options *options,
+	enum ast_media_type type, struct ast_format_cap *cap)
+{
+	switch (type) {
+	case AST_MEDIA_TYPE_AUDIO:
+	case AST_MEDIA_TYPE_VIDEO:
+	case AST_MEDIA_TYPE_IMAGE:
+	case AST_MEDIA_TYPE_TEXT:
+		ao2_cleanup(options->caps[type]);
+		options->caps[type] = NULL;
+		if (cap && !ast_format_cap_empty(cap)) {
+			ao2_ref(cap, +1);
+			options->caps[type] = cap;
+		}
+		break;
+	case AST_MEDIA_TYPE_UNKNOWN:
+	case AST_MEDIA_TYPE_END:
+		break;
+	}
+}
+
+void ast_sdp_options_set_format_caps(struct ast_sdp_options *options,
+	struct ast_format_cap *cap)
+{
+	enum ast_media_type type;
+
+	for (type = AST_MEDIA_TYPE_UNKNOWN; type < AST_MEDIA_TYPE_END; ++type) {
+		ao2_cleanup(options->caps[type]);
+		options->caps[type] = NULL;
+	}
+
+	if (!cap || ast_format_cap_empty(cap)) {
+		return;
+	}
+
+	for (type = AST_MEDIA_TYPE_UNKNOWN + 1; type < AST_MEDIA_TYPE_END; ++type) {
+		struct ast_format_cap *type_cap;
+
+		type_cap = ast_format_cap_alloc(AST_FORMAT_CAP_FLAG_DEFAULT);
+		if (!type_cap) {
+			continue;
+		}
+
+		ast_format_cap_set_framing(type_cap, ast_format_cap_get_framing(cap));
+		if (ast_format_cap_append_from_cap(type_cap, cap, type)
+			|| ast_format_cap_empty(type_cap)) {
+			ao2_ref(type_cap, -1);
+			continue;
+		}
+
+		/* This takes the allocation reference */
+		options->caps[type] = type_cap;
+	}
+}
+
 static void set_defaults(struct ast_sdp_options *options)
 {
 	options->dtmf = DEFAULT_DTMF;
 	options->ice = DEFAULT_ICE;
 	options->impl = DEFAULT_IMPL;
 	options->encryption = DEFAULT_ENCRYPTION;
+	options->max_streams = DEFAULT_MAX_STREAMS;
 }
 
 struct ast_sdp_options *ast_sdp_options_alloc(void)
@@ -138,6 +221,15 @@
 
 void ast_sdp_options_free(struct ast_sdp_options *options)
 {
+	enum ast_media_type type;
+
+	if (!options) {
+		return;
+	}
+
+	for (type = AST_MEDIA_TYPE_UNKNOWN; type < AST_MEDIA_TYPE_END; ++type) {
+		ao2_cleanup(options->caps[type]);
+	}
 	ast_string_field_free_memory(options);
 	ast_free(options);
 }
diff --git a/main/sdp_private.h b/main/sdp_private.h
index 62228a5..48bedc8 100644
--- a/main/sdp_private.h
+++ b/main/sdp_private.h
@@ -24,7 +24,7 @@
 
 struct ast_sdp_options {
 	AST_DECLARE_STRING_FIELDS(
-		/*! Media address to use in SDP */
+		/*! Media address to advertise in SDP session c= line */
 		AST_STRING_FIELD(media_address);
 		/*! Optional address of the interface media should use. */
 		AST_STRING_FIELD(interface_address);
@@ -37,12 +37,25 @@
 	);
 	/*! Scheduler context for the media stream types (Mainly for RTP) */
 	struct ast_sched_context *sched[AST_MEDIA_TYPE_END];
+	/*! Capabilities to create new streams of the indexed media type. */
+	struct ast_format_cap *caps[AST_MEDIA_TYPE_END];
+	/*! User supplied context data pointer for the SDP state. */
+	void *state_context;
+	/*! Modify negotiated topology before create answer SDP callback. */
+	ast_sdp_answerer_modify_cb answerer_modify_cb;
+	/*! Modify proposed topology before create offer SDP callback. */
+	ast_sdp_offerer_modify_cb offerer_modify_cb;
+	/*! Configure proposed topology extra stream options before create offer SDP callback. */
+	ast_sdp_offerer_config_cb offerer_config_cb;
+	/*! Negotiated topology is about to be applied callback. */
+	ast_sdp_preapply_cb preapply_cb;
+	/*! Negotiated topology was just applied callback. */
+	ast_sdp_postapply_cb postapply_cb;
 	struct {
 		unsigned int rtp_symmetric:1;
 		unsigned int udptl_symmetric:1;
 		unsigned int rtp_ipv6:1;
 		unsigned int g726_non_standard:1;
-		unsigned int locally_held:1;
 		unsigned int rtcp_mux:1;
 		unsigned int ssrc:1;
 	};
@@ -52,6 +65,8 @@
 		unsigned int tos_video;
 		unsigned int cos_video;
 		unsigned int udptl_far_max_datagram;
+		/*! Maximum number of streams to allow. */
+		unsigned int max_streams;
 	};
 	enum ast_sdp_options_dtmf dtmf;
 	enum ast_sdp_options_ice ice;
diff --git a/main/sdp_state.c b/main/sdp_state.c
index 99421ad..330140c 100644
--- a/main/sdp_state.c
+++ b/main/sdp_state.c
@@ -85,11 +85,38 @@
 	};
 	/*! An explicit connection address for this stream */
 	struct ast_sockaddr connection_address;
-	/*! Whether this stream is held or not */
-	unsigned int locally_held;
+	/*!
+	 * \brief Stream is on hold by remote side
+	 *
+	 * \note This flag is never set on the
+	 * sdp_state->proposed_capabilities->streams states.  This is useful
+	 * when the remote sends us a reINVITE with a deferred SDP to place
+	 * us on and off of hold.
+	 */
+	unsigned int remotely_held:1;
+	/*! Stream is on hold by local side */
+	unsigned int locally_held:1;
 	/*! UDPTL session parameters */
 	struct ast_control_t38_parameters t38_local_params;
 };
+
+static int sdp_is_stream_type_supported(enum ast_media_type type)
+{
+	int is_supported = 0;
+
+	switch (type) {
+	case AST_MEDIA_TYPE_AUDIO:
+	case AST_MEDIA_TYPE_VIDEO:
+	case AST_MEDIA_TYPE_IMAGE:
+		is_supported = 1;
+		break;
+	case AST_MEDIA_TYPE_UNKNOWN:
+	case AST_MEDIA_TYPE_TEXT:
+	case AST_MEDIA_TYPE_END:
+		break;
+	}
+	return is_supported;
+}
 
 static void sdp_state_rtp_destroy(void *obj)
 {
@@ -135,8 +162,6 @@
 	struct ast_stream_topology *topology;
 	/*! Additional information about the streams */
 	struct sdp_state_streams streams;
-	/*! An explicit global connection address */
-	struct ast_sockaddr connection_address;
 };
 
 static void sdp_state_capabilities_free(struct sdp_state_capabilities *capabilities)
@@ -269,30 +294,80 @@
 	return udptl;
 }
 
+static struct ast_stream *merge_local_stream(const struct ast_sdp_options *options,
+	const struct ast_stream *update);
+
 static struct sdp_state_capabilities *sdp_initialize_state_capabilities(const struct ast_stream_topology *topology,
 	const struct ast_sdp_options *options)
 {
 	struct sdp_state_capabilities *capabilities;
-	int i;
+	struct ast_stream *stream;
+	unsigned int topology_count;
+	unsigned int max_streams;
+	unsigned int idx;
 
 	capabilities = ast_calloc(1, sizeof(*capabilities));
 	if (!capabilities) {
 		return NULL;
 	}
 
-	capabilities->topology = ast_stream_topology_clone(topology);
+	capabilities->topology = ast_stream_topology_alloc();
 	if (!capabilities->topology) {
 		sdp_state_capabilities_free(capabilities);
 		return NULL;
 	}
 
-	if (AST_VECTOR_INIT(&capabilities->streams, ast_stream_topology_get_count(topology))) {
+	max_streams = ast_sdp_options_get_max_streams(options);
+	if (topology) {
+		topology_count = ast_stream_topology_get_count(topology);
+	} else {
+		topology_count = 0;
+	}
+
+	/* Gather acceptable streams from the initial topology */
+	for (idx = 0; idx < topology_count; ++idx) {
+		stream = ast_stream_topology_get_stream(topology, idx);
+		if (!sdp_is_stream_type_supported(ast_stream_get_type(stream))) {
+			/* Delete the unsupported stream from the initial topology */
+			continue;
+		}
+		if (max_streams <= ast_stream_topology_get_count(capabilities->topology)) {
+			/* Cannot support any more streams */
+			break;
+		}
+
+		stream = merge_local_stream(options, stream);
+		if (!stream) {
+			sdp_state_capabilities_free(capabilities);
+			return NULL;
+		}
+
+		if (ast_stream_topology_append_stream(capabilities->topology, stream) < 0) {
+			ast_stream_free(stream);
+			sdp_state_capabilities_free(capabilities);
+			return NULL;
+		}
+	}
+
+	/*
+	 * Remove trailing declined streams from the initial built topology.
+	 * No need to waste space in the SDP with these unused slots.
+	 */
+	for (idx = ast_stream_topology_get_count(capabilities->topology); idx--;) {
+		stream = ast_stream_topology_get_stream(capabilities->topology, idx);
+		if (ast_stream_get_state(stream) != AST_STREAM_STATE_REMOVED) {
+			break;
+		}
+		ast_stream_topology_del_stream(capabilities->topology, idx);
+	}
+
+	topology_count = ast_stream_topology_get_count(capabilities->topology);
+	if (AST_VECTOR_INIT(&capabilities->streams, topology_count)) {
 		sdp_state_capabilities_free(capabilities);
 		return NULL;
 	}
-	ast_sockaddr_setnull(&capabilities->connection_address);
 
-	for (i = 0; i < ast_stream_topology_get_count(topology); ++i) {
+	for (idx = 0; idx < topology_count; ++idx) {
 		struct sdp_state_stream *state_stream;
 
 		state_stream = ast_calloc(1, sizeof(*state_stream));
@@ -301,32 +376,34 @@
 			return NULL;
 		}
 
-		state_stream->type = ast_stream_get_type(ast_stream_topology_get_stream(topology, i));
-		switch (state_stream->type) {
-		case AST_MEDIA_TYPE_AUDIO:
-		case AST_MEDIA_TYPE_VIDEO:
-			state_stream->rtp = create_rtp(options, state_stream->type);
-			if (!state_stream->rtp) {
-				sdp_state_stream_free(state_stream);
-				sdp_state_capabilities_free(capabilities);
-				return NULL;
+		stream = ast_stream_topology_get_stream(capabilities->topology, idx);
+		state_stream->type = ast_stream_get_type(stream);
+		if (ast_stream_get_state(stream) != AST_STREAM_STATE_REMOVED) {
+			switch (state_stream->type) {
+			case AST_MEDIA_TYPE_AUDIO:
+			case AST_MEDIA_TYPE_VIDEO:
+				state_stream->rtp = create_rtp(options, state_stream->type);
+				if (!state_stream->rtp) {
+					sdp_state_stream_free(state_stream);
+					sdp_state_capabilities_free(capabilities);
+					return NULL;
+				}
+				break;
+			case AST_MEDIA_TYPE_IMAGE:
+				state_stream->udptl = create_udptl(options);
+				if (!state_stream->udptl) {
+					sdp_state_stream_free(state_stream);
+					sdp_state_capabilities_free(capabilities);
+					return NULL;
+				}
+				break;
+			case AST_MEDIA_TYPE_UNKNOWN:
+			case AST_MEDIA_TYPE_TEXT:
+			case AST_MEDIA_TYPE_END:
+				/* Unsupported stream type already handled earlier */
+				ast_assert(0);
+				break;
 			}
-			break;
-		case AST_MEDIA_TYPE_IMAGE:
-			state_stream->udptl = create_udptl(options);
-			if (!state_stream->udptl) {
-				sdp_state_stream_free(state_stream);
-				sdp_state_capabilities_free(capabilities);
-				return NULL;
-			}
-			break;
-		case AST_MEDIA_TYPE_UNKNOWN:
-		case AST_MEDIA_TYPE_TEXT:
-		case AST_MEDIA_TYPE_END:
-			ast_assert(0);
-			sdp_state_stream_free(state_stream);
-			sdp_state_capabilities_free(capabilities);
-			return NULL;
 		}
 
 		if (AST_VECTOR_APPEND(&capabilities->streams, state_stream)) {
@@ -350,12 +427,12 @@
  * is allocated, this topology is used to create the proposed_capabilities.
  *
  * If we are the SDP offerer, then the proposed_capabilities are what are used
- * to generate the SDP offer. When the SDP answer arrives, the proposed capabilities
- * are merged with the SDP answer to create the negotiated capabilities.
+ * to generate the offer SDP. When the answer SDP arrives, the proposed capabilities
+ * are merged with the answer SDP to create the negotiated capabilities.
  *
- * If we are the SDP answerer, then the incoming SDP offer is merged with our
+ * If we are the SDP answerer, then the incoming offer SDP is merged with our
  * proposed capabilities to to create the negotiated capabilities. These negotiated
- * capabilities are what we send in our SDP answer.
+ * capabilities are what we send in our answer SDP.
  *
  * Any changes that a user of the API performs will occur on the proposed capabilities.
  * The negotiated capabilities are only altered based on actual SDP negotiation. This is
@@ -367,17 +444,33 @@
 	struct sdp_state_capabilities *negotiated_capabilities;
 	/*! Proposed capabilities */
 	struct sdp_state_capabilities *proposed_capabilities;
-	/*! Local SDP. Generated via the options and currently negotiated/proposed capabilities. */
+	/*!
+	 * \brief New topology waiting to be merged.
+	 *
+	 * \details
+	 * Repeated topology updates are merged into each other here until
+	 * negotiations are restarted and we create an offer.
+	 */
+	struct ast_stream_topology *pending_topology_update;
+	/*! Local SDP. Generated via the options and negotiated/proposed capabilities. */
 	struct ast_sdp *local_sdp;
+	/*! Saved remote SDP */
+	struct ast_sdp *remote_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;
+	/*! An explicit global connection address */
+	struct ast_sockaddr connection_address;
 	/*! The role that we occupy in SDP negotiation */
 	enum ast_sdp_role role;
+	/*! TRUE if all streams on hold by local side */
+	unsigned int locally_held:1;
+	/*! TRUE if the remote offer resulted in all streams being declined. */
+	unsigned int remote_offer_rejected:1;
 };
 
-struct ast_sdp_state *ast_sdp_state_alloc(struct ast_stream_topology *streams,
+struct ast_sdp_state *ast_sdp_state_alloc(struct ast_stream_topology *topology,
 	struct ast_sdp_options *options)
 {
 	struct ast_sdp_state *sdp_state;
@@ -395,7 +488,7 @@
 		return NULL;
 	}
 
-	sdp_state->proposed_capabilities = sdp_initialize_state_capabilities(streams, options);
+	sdp_state->proposed_capabilities = sdp_initialize_state_capabilities(topology, options);
 	if (!sdp_state->proposed_capabilities) {
 		ast_sdp_state_free(sdp_state);
 		return NULL;
@@ -414,10 +507,215 @@
 
 	sdp_state_capabilities_free(sdp_state->negotiated_capabilities);
 	sdp_state_capabilities_free(sdp_state->proposed_capabilities);
-	ast_sdp_free(sdp_state->local_sdp);
+	ao2_cleanup(sdp_state->local_sdp);
+	ao2_cleanup(sdp_state->remote_sdp);
 	ast_sdp_options_free(sdp_state->options);
 	ast_sdp_translator_free(sdp_state->translator);
 	ast_free(sdp_state);
+}
+
+/*!
+ * \internal
+ * \brief Allow a configured callback to alter the new negotiated joint topology.
+ * \since 15.0.0
+ *
+ * \details
+ * The callback can alter topology stream names, formats, or decline streams.
+ *
+ * \param sdp_state
+ * \param topology Joint topology that we intend to generate the answer SDP.
+ *
+ * \return Nothing
+ */
+static void sdp_state_cb_answerer_modify_topology(const struct ast_sdp_state *sdp_state,
+	struct ast_stream_topology *topology)
+{
+	ast_sdp_answerer_modify_cb cb;
+
+	cb = ast_sdp_options_get_answerer_modify_cb(sdp_state->options);
+	if (cb) {
+		void *context;
+		const struct ast_stream_topology *neg_topology;/*!< Last negotiated topology */
+#ifdef AST_DEVMODE
+		struct ast_stream *stream;
+		int idx;
+		enum ast_media_type type[ast_stream_topology_get_count(topology)];
+		enum ast_stream_state state[ast_stream_topology_get_count(topology)];
+
+		/*
+		 * Save stream types and states to validate that they don't
+		 * get changed unexpectedly.
+		 */
+		for (idx = 0; idx < ast_stream_topology_get_count(topology); ++idx) {
+			stream = ast_stream_topology_get_stream(topology, idx);
+			type[idx] = ast_stream_get_type(stream);
+			state[idx] = ast_stream_get_state(stream);
+		}
+#endif
+
+		context = ast_sdp_options_get_state_context(sdp_state->options);
+		neg_topology = sdp_state->negotiated_capabilities
+			? sdp_state->negotiated_capabilities->topology : NULL;
+		cb(context, neg_topology, topology);
+
+#ifdef AST_DEVMODE
+		for (idx = 0; idx < ast_stream_topology_get_count(topology); ++idx) {
+			stream = ast_stream_topology_get_stream(topology, idx);
+
+			/* Check that active streams have at least one format */
+			ast_assert(ast_stream_get_state(stream) == AST_STREAM_STATE_REMOVED
+				|| (ast_stream_get_formats(stream)
+					&& ast_format_cap_count(ast_stream_get_formats(stream))));
+
+			/* Check that stream types didn't change. */
+			ast_assert(type[idx] == ast_stream_get_type(stream));
+
+			/* Check that streams didn't get resurected. */
+			ast_assert(state[idx] != AST_STREAM_STATE_REMOVED
+				|| ast_stream_get_state(stream) == AST_STREAM_STATE_REMOVED);
+		}
+#endif
+	}
+}
+
+/*!
+ * \internal
+ * \brief Allow a configured callback to alter the merged local topology.
+ * \since 15.0.0
+ *
+ * \details
+ * The callback can modify streams in the merged topology.  The
+ * callback can decline, add/remove/update formats, or rename
+ * streams.  Changing anything else on the streams is likely to not
+ * end well.
+ *
+ * \param sdp_state
+ * \param topology Merged topology that we intend to generate the offer SDP.
+ *
+ * \return Nothing
+ */
+static void sdp_state_cb_offerer_modify_topology(const struct ast_sdp_state *sdp_state,
+	struct ast_stream_topology *topology)
+{
+	ast_sdp_offerer_modify_cb cb;
+
+	cb = ast_sdp_options_get_offerer_modify_cb(sdp_state->options);
+	if (cb) {
+		void *context;
+		const struct ast_stream_topology *neg_topology;/*!< Last negotiated topology */
+
+		context = ast_sdp_options_get_state_context(sdp_state->options);
+		neg_topology = sdp_state->negotiated_capabilities
+			? sdp_state->negotiated_capabilities->topology : NULL;
+		cb(context, neg_topology, topology);
+
+#ifdef AST_DEVMODE
+		{
+			struct ast_stream *stream;
+			int idx;
+
+			/* Check that active streams have at least one format */
+			for (idx = 0; idx < ast_stream_topology_get_count(topology); ++idx) {
+				stream = ast_stream_topology_get_stream(topology, idx);
+				ast_assert(ast_stream_get_state(stream) == AST_STREAM_STATE_REMOVED
+					|| (ast_stream_get_formats(stream)
+						&& ast_format_cap_count(ast_stream_get_formats(stream))));
+			}
+		}
+#endif
+	}
+}
+
+/*!
+ * \internal
+ * \brief Allow a configured callback to configure the merged local topology.
+ * \since 15.0.0
+ *
+ * \details
+ * The callback can configure other parameters associated with each
+ * active stream on the topology.  The callback can call several SDP
+ * API calls to configure the proposed capabilities of the streams
+ * before we create the offer SDP.  For example, the callback could
+ * configure a stream specific connection address, T.38 parameters,
+ * RTP instance, or UDPTL instance parameters.
+ *
+ * \param sdp_state
+ * \param topology Merged topology that we intend to generate the offer SDP.
+ *
+ * \return Nothing
+ */
+static void sdp_state_cb_offerer_config_topology(const struct ast_sdp_state *sdp_state,
+	const struct ast_stream_topology *topology)
+{
+	ast_sdp_offerer_config_cb cb;
+
+	cb = ast_sdp_options_get_offerer_config_cb(sdp_state->options);
+	if (cb) {
+		void *context;
+
+		context = ast_sdp_options_get_state_context(sdp_state->options);
+		cb(context, topology);
+	}
+}
+
+/*!
+ * \internal
+ * \brief Call any registered pre-apply topology callback.
+ * \since 15.0.0
+ *
+ * \param sdp_state
+ * \param topology
+ *
+ * \return Nothing
+ */
+static void sdp_state_cb_preapply_topology(const struct ast_sdp_state *sdp_state,
+	const struct ast_stream_topology *topology)
+{
+	ast_sdp_preapply_cb cb;
+
+	cb = ast_sdp_options_get_preapply_cb(sdp_state->options);
+	if (cb) {
+		void *context;
+
+		context = ast_sdp_options_get_state_context(sdp_state->options);
+		cb(context, topology);
+	}
+}
+
+/*!
+ * \internal
+ * \brief Call any registered post-apply topology callback.
+ * \since 15.0.0
+ *
+ * \param sdp_state
+ * \param topology
+ *
+ * \return Nothing
+ */
+static void sdp_state_cb_postapply_topology(const struct ast_sdp_state *sdp_state,
+	const struct ast_stream_topology *topology)
+{
+	ast_sdp_postapply_cb cb;
+
+	cb = ast_sdp_options_get_postapply_cb(sdp_state->options);
+	if (cb) {
+		void *context;
+
+		context = ast_sdp_options_get_state_context(sdp_state->options);
+		cb(context, topology);
+	}
+}
+
+static const struct sdp_state_capabilities *sdp_state_get_joint_capabilities(
+	const struct ast_sdp_state *sdp_state)
+{
+	ast_assert(sdp_state != NULL);
+
+	if (sdp_state->negotiated_capabilities) {
+		return sdp_state->negotiated_capabilities;
+	}
+
+	return sdp_state->proposed_capabilities;
 }
 
 static struct sdp_state_stream *sdp_state_get_stream(const struct ast_sdp_state *sdp_state, int stream_index)
@@ -427,6 +725,18 @@
 	}
 
 	return AST_VECTOR_GET(&sdp_state->proposed_capabilities->streams, stream_index);
+}
+
+static struct sdp_state_stream *sdp_state_get_joint_stream(const struct ast_sdp_state *sdp_state, int stream_index)
+{
+	const struct sdp_state_capabilities *capabilities;
+
+	capabilities = sdp_state_get_joint_capabilities(sdp_state);
+	if (AST_VECTOR_SIZE(&capabilities->streams) <= stream_index) {
+		return NULL;
+	}
+
+	return AST_VECTOR_GET(&capabilities->streams, stream_index);
 }
 
 struct ast_rtp_instance *ast_sdp_state_get_rtp_instance(
@@ -468,7 +778,56 @@
 {
 	ast_assert(sdp_state != NULL);
 
-	return &sdp_state->proposed_capabilities->connection_address;
+	return &sdp_state->connection_address;
+}
+
+static int sdp_state_stream_get_connection_address(const struct ast_sdp_state *sdp_state,
+	struct sdp_state_stream *stream_state, struct ast_sockaddr *address)
+{
+	ast_assert(sdp_state != NULL);
+	ast_assert(stream_state != NULL);
+	ast_assert(address != NULL);
+
+	/* 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;
+	}
+
+	switch (stream_state->type) {
+	case AST_MEDIA_TYPE_AUDIO:
+	case AST_MEDIA_TYPE_VIDEO:
+		if (!stream_state->rtp->instance) {
+			return -1;
+		}
+		ast_rtp_instance_get_local_address(stream_state->rtp->instance, address);
+		break;
+	case AST_MEDIA_TYPE_IMAGE:
+		if (!stream_state->udptl->instance) {
+			return -1;
+		}
+		ast_udptl_get_us(stream_state->udptl->instance, address);
+		break;
+	case AST_MEDIA_TYPE_UNKNOWN:
+	case AST_MEDIA_TYPE_TEXT:
+	case AST_MEDIA_TYPE_END:
+		return -1;
+	}
+
+	if (ast_sockaddr_isnull(address)) {
+		/* No address is set on the stream state. */
+		return -1;
+	}
+
+	/* If an explicit global connection address is set use it here for the IP part */
+	if (!ast_sockaddr_isnull(&sdp_state->connection_address)) {
+		int port = ast_sockaddr_port(address);
+
+		ast_sockaddr_copy(address, &sdp_state->connection_address);
+		ast_sockaddr_set_port(address, port);
+	}
+
+	return 0;
 }
 
 int ast_sdp_state_get_stream_connection_address(const struct ast_sdp_state *sdp_state,
@@ -484,48 +843,16 @@
 		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;
-	}
-
-	switch (ast_stream_get_type(ast_stream_topology_get_stream(sdp_state->proposed_capabilities->topology,
-		stream_index))) {
-	case AST_MEDIA_TYPE_AUDIO:
-	case AST_MEDIA_TYPE_VIDEO:
-		ast_rtp_instance_get_local_address(stream_state->rtp->instance, address);
-		break;
-	case AST_MEDIA_TYPE_IMAGE:
-		ast_udptl_get_us(stream_state->udptl->instance, address);
-		break;
-	case AST_MEDIA_TYPE_UNKNOWN:
-	case AST_MEDIA_TYPE_TEXT:
-	case AST_MEDIA_TYPE_END:
-		return -1;
-	}
-
-	/* If an explicit global connection address is set use it here for the IP part */
-	if (!ast_sockaddr_isnull(&sdp_state->proposed_capabilities->connection_address)) {
-		int port = ast_sockaddr_port(address);
-
-		ast_sockaddr_copy(address, &sdp_state->proposed_capabilities->connection_address);
-		ast_sockaddr_set_port(address, port);
-	}
-
-	return 0;
+	return sdp_state_stream_get_connection_address(sdp_state, stream_state, address);
 }
 
 const struct ast_stream_topology *ast_sdp_state_get_joint_topology(
 	const struct ast_sdp_state *sdp_state)
 {
-	ast_assert(sdp_state != NULL);
+	const struct sdp_state_capabilities *capabilities;
 
-	if (sdp_state->negotiated_capabilities) {
-		return sdp_state->negotiated_capabilities->topology;
-	}
-
-	return sdp_state->proposed_capabilities->topology;
+	capabilities = sdp_state_get_joint_capabilities(sdp_state);
+	return capabilities->topology;
 }
 
 const struct ast_stream_topology *ast_sdp_state_get_local_topology(
@@ -544,50 +871,76 @@
 	return sdp_state->options;
 }
 
+static struct ast_stream *decline_stream(enum ast_media_type type, const char *name)
+{
+	struct ast_stream *stream;
+
+	if (!name) {
+		name = ast_codec_media_type2str(type);
+	}
+	stream = ast_stream_alloc(name, type);
+	if (!stream) {
+		return NULL;
+	}
+	ast_stream_set_state(stream, AST_STREAM_STATE_REMOVED);
+	return stream;
+}
+
 /*!
- * \brief Merge two streams into a joint stream.
+ * \brief Merge an update stream into a local stream.
  *
- * \param local Our local stream
- * \param remote A remote stream
+ * \param options SDP Options
+ * \param update An updated 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)
+static struct ast_stream *merge_local_stream(const struct ast_sdp_options *options,
+	const struct ast_stream *update)
 {
 	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(ast_codec_media_type2str(ast_stream_get_type(remote)),
-		ast_stream_get_type(remote));
-	if (!joint_stream) {
-		return NULL;
-	}
+	struct ast_format_cap *allowed_cap;
+	struct ast_format_cap *update_cap;
+	enum ast_stream_state joint_state;
 
 	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);
+	update_cap = ast_stream_get_formats(update);
+	allowed_cap = ast_sdp_options_get_format_cap_type(options,
+		ast_stream_get_type(update));
+	if (allowed_cap && update_cap) {
+		struct ast_str *allowed_buf = ast_str_alloca(128);
+		struct ast_str *update_buf = ast_str_alloca(128);
+		struct ast_str *joint_buf = ast_str_alloca(128);
 
-	ast_format_cap_get_compatible(local_cap, remote_cap, joint_cap);
+		ast_format_cap_get_compatible(allowed_cap, update_cap, joint_cap);
+		ast_debug(3,
+			"Filtered update '%s' with allowed '%s' to get joint '%s'. Joint has %zu formats\n",
+			ast_format_cap_get_names(update_cap, &update_buf),
+			ast_format_cap_get_names(allowed_cap, &allowed_buf),
+			ast_format_cap_get_names(joint_cap, &joint_buf),
+			ast_format_cap_count(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));
+	/* Determine the joint stream state */
+	joint_state = AST_STREAM_STATE_REMOVED;
+	if (ast_stream_get_state(update) != AST_STREAM_STATE_REMOVED
+		&& ast_format_cap_count(joint_cap)) {
+		joint_state = AST_STREAM_STATE_SENDRECV;
+	}
 
-	ast_stream_set_formats(joint_stream, joint_cap);
+	joint_stream = ast_stream_alloc(ast_stream_get_name(update),
+		ast_stream_get_type(update));
+	if (joint_stream) {
+		ast_stream_set_state(joint_stream, joint_state);
+		if (joint_state != AST_STREAM_STATE_REMOVED) {
+			ast_stream_set_formats(joint_stream, joint_cap);
+		}
+	}
 
 	ao2_ref(joint_cap, -1);
 
@@ -595,103 +948,1025 @@
 }
 
 /*!
- * \brief Get a local stream that corresponds with a remote stream.
+ * \brief Merge a remote stream into a local 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
+ * \param sdp_state
+ * \param local Our local stream (NULL if creating new stream)
+ * \param locally_held Nonzero if the local stream is held
+ * \param remote A remote stream
  *
- * \retval -1 No corresponding stream found
- * \retval index The corresponding stream index
+ * \retval NULL An error occurred
+ * \retval non-NULL The joint stream created
  */
-static int get_corresponding_index(const struct ast_stream_topology *local,
-	enum ast_media_type media_type, int *media_indices)
+static struct ast_stream *merge_remote_stream(const struct ast_sdp_state *sdp_state,
+	const struct ast_stream *local, unsigned int locally_held,
+	const struct ast_stream *remote)
 {
-	int i;
+	struct ast_stream *joint_stream;
+	struct ast_format_cap *joint_cap;
+	struct ast_format_cap *local_cap;
+	struct ast_format_cap *remote_cap;
+	const char *joint_name;
+	enum ast_stream_state joint_state;
+	enum ast_stream_state remote_state;
 
-	for (i = media_indices[media_type]; i < ast_stream_topology_get_count(local); ++i) {
-		struct ast_stream *candidate;
+	joint_cap = ast_format_cap_alloc(AST_FORMAT_CAP_FLAG_DEFAULT);
+	if (!joint_cap) {
+		return NULL;
+	}
 
-		candidate = ast_stream_topology_get_stream(local, i);
-		if (ast_stream_get_type(candidate) == media_type) {
-			media_indices[media_type] = i + 1;
-			return i;
+	remote_cap = ast_stream_get_formats(remote);
+	if (local) {
+		local_cap = ast_stream_get_formats(local);
+	} else {
+		local_cap = ast_sdp_options_get_format_cap_type(sdp_state->options,
+			ast_stream_get_type(remote));
+	}
+	if (local_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);
+
+		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));
+	}
+
+	/* Determine the joint stream state */
+	remote_state = ast_stream_get_state(remote);
+	joint_state = AST_STREAM_STATE_REMOVED;
+	if ((!local || ast_stream_get_state(local) != AST_STREAM_STATE_REMOVED)
+		&& ast_format_cap_count(joint_cap)) {
+		if (sdp_state->locally_held || locally_held) {
+			switch (remote_state) {
+			case AST_STREAM_STATE_REMOVED:
+				break;
+			case AST_STREAM_STATE_INACTIVE:
+				joint_state = AST_STREAM_STATE_INACTIVE;
+				break;
+			case AST_STREAM_STATE_SENDRECV:
+				joint_state = AST_STREAM_STATE_SENDONLY;
+				break;
+			case AST_STREAM_STATE_SENDONLY:
+				joint_state = AST_STREAM_STATE_INACTIVE;
+				break;
+			case AST_STREAM_STATE_RECVONLY:
+				joint_state = AST_STREAM_STATE_SENDONLY;
+				break;
+			}
+		} else {
+			switch (remote_state) {
+			case AST_STREAM_STATE_REMOVED:
+				break;
+			case AST_STREAM_STATE_INACTIVE:
+				joint_state = AST_STREAM_STATE_RECVONLY;
+				break;
+			case AST_STREAM_STATE_SENDRECV:
+				joint_state = AST_STREAM_STATE_SENDRECV;
+				break;
+			case AST_STREAM_STATE_SENDONLY:
+				joint_state = AST_STREAM_STATE_RECVONLY;
+				break;
+			case AST_STREAM_STATE_RECVONLY:
+				joint_state = AST_STREAM_STATE_SENDRECV;
+				break;
+			}
 		}
 	}
 
-	/* No stream of the type left in the topology */
-	media_indices[media_type] = i;
-	return -1;
+	if (local) {
+		joint_name = ast_stream_get_name(local);
+	} else {
+		joint_name = ast_codec_media_type2str(ast_stream_get_type(remote));
+	}
+	joint_stream = ast_stream_alloc(joint_name, ast_stream_get_type(remote));
+	if (joint_stream) {
+		ast_stream_set_state(joint_stream, joint_state);
+		if (joint_state != AST_STREAM_STATE_REMOVED) {
+			ast_stream_set_formats(joint_stream, joint_cap);
+		}
+	}
+
+	ao2_ref(joint_cap, -1);
+
+	return joint_stream;
 }
 
 /*!
- * XXX TODO The merge_capabilities() function needs to be split into
- * merging for new local topologies and new remote topologies.  Also
- * the possibility of changing the stream types needs consideration.
- * Audio to video may or may not need us to keep the same RTP instance
- * because the stream position is still RTP.  A new RTP instance would
- * cause us to change ports.  Audio to image is definitely going to
- * happen for T.38.
+ * \internal
+ * \brief Determine if a merged topology should be rejected.
+ * \since 15.0.0
  *
- * A new remote topology as an initial offer needs to dictate the
- * number of streams and the order.  As a sdp_state option we may
- * allow creation of new active streams not defined by the current
- * local topology.  A subsequent remote offer can change the stream
- * types and add streams.  The sdp_state option could regulate
- * creation of new active streams here as well.  An answer cannot
- * change stream types or the number of streams but can decline
- * streams.  Any attempt to do so should report an error and possibly
- * disconnect the call.
+ * \param topology What topology to determine if we reject
  *
- * A local topology update needs to be merged differently.  It cannot
- * reduce the number of streams already defined without violating the
- * SDP RFC.  The local merge could take the new topology stream
- * verbatim and add declined streams to fill out any shortfall with
- * the exiting topology.  This strategy is needed if we want to change
- * an audio stream to an image stream for T.38 fax and vice versa.
- * The local merge could take the new topology and map the streams to
- * the existing local topology.  The new topology stream format caps
- * would be copied into the merged topology so we could change what
- * codecs are negotiated.
+ * \retval 0 if not rejected.
+ * \retval non-zero if rejected.
  */
+static int sdp_topology_is_rejected(struct ast_stream_topology *topology)
+{
+	int idx;
+	struct ast_stream *stream;
+
+	for (idx = ast_stream_topology_get_count(topology); idx--;) {
+		stream = ast_stream_topology_get_stream(topology, idx);
+		if (ast_stream_get_state(stream) != AST_STREAM_STATE_REMOVED) {
+			/* At least one stream is not declined */
+			return 0;
+		}
+	}
+
+	/* All streams are declined */
+	return 1;
+}
+
+static void sdp_state_stream_copy_common(struct sdp_state_stream *dst, const struct sdp_state_stream *src)
+{
+	ast_sockaddr_copy(&dst->connection_address,
+		&src->connection_address);
+	/* Explicitly does not copy the local or remote hold states. */
+	dst->t38_local_params = src->t38_local_params;
+}
+
+static void sdp_state_stream_copy(struct sdp_state_stream *dst, const struct sdp_state_stream *src)
+{
+	*dst = *src;
+
+	switch (dst->type) {
+	case AST_MEDIA_TYPE_AUDIO:
+	case AST_MEDIA_TYPE_VIDEO:
+		ao2_bump(dst->rtp);
+		break;
+	case AST_MEDIA_TYPE_IMAGE:
+		ao2_bump(dst->udptl);
+		break;
+	case AST_MEDIA_TYPE_UNKNOWN:
+	case AST_MEDIA_TYPE_TEXT:
+	case AST_MEDIA_TYPE_END:
+		break;
+	}
+}
+
 /*!
- * \brief Merge existing stream capabilities and a new topology into joint capabilities.
+ * \internal
+ * \brief Initialize an int vector and default the contents to the member index.
+ * \since 15.0.0
+ *
+ * \param vect Vetctor to initialize and set to default values.
+ * \param size Size of the vector to setup.
+ *
+ * \retval 0 on success.
+ * \retval -1 on failure.
+ */
+static int sdp_vect_idx_init(struct ast_vector_int *vect, size_t size)
+{
+	int idx;
+
+	if (AST_VECTOR_INIT(vect, size)) {
+		return -1;
+	}
+	for (idx = 0; idx < size; ++idx) {
+		AST_VECTOR_APPEND(vect, idx);
+	}
+	return 0;
+}
+
+/*!
+ * \internal
+ * \brief Compare stream types for sort order.
+ * \since 15.0.0
+ *
+ * \param left Stream parameter on left
+ * \param right Stream parameter on right
+ *
+ * \retval <0 left stream sorts first.
+ * \retval =0 streams match.
+ * \retval >0 right stream sorts first.
+ */
+static int sdp_stream_cmp_by_type(const struct ast_stream *left, const struct ast_stream *right)
+{
+	enum ast_media_type left_type = ast_stream_get_type(left);
+	enum ast_media_type right_type = ast_stream_get_type(right);
+
+	/* Treat audio and image as the same for T.38 support */
+	if (left_type == AST_MEDIA_TYPE_IMAGE) {
+		left_type = AST_MEDIA_TYPE_AUDIO;
+	}
+	if (right_type == AST_MEDIA_TYPE_IMAGE) {
+		right_type = AST_MEDIA_TYPE_AUDIO;
+	}
+
+	return left_type - right_type;
+}
+
+/*!
+ * \internal
+ * \brief Compare stream names and types for sort order.
+ * \since 15.0.0
+ *
+ * \param left Stream parameter on left
+ * \param right Stream parameter on right
+ *
+ * \retval <0 left stream sorts first.
+ * \retval =0 streams match.
+ * \retval >0 right stream sorts first.
+ */
+static int sdp_stream_cmp_by_name(const struct ast_stream *left, const struct ast_stream *right)
+{
+	int cmp;
+	const char *left_name;
+
+	left_name = ast_stream_get_name(left);
+	cmp = strcmp(left_name, ast_stream_get_name(right));
+	if (!cmp) {
+		cmp = sdp_stream_cmp_by_type(left, right);
+		if (!cmp) {
+			/* Are the stream names real or type names which aren't matchable? */
+			if (ast_strlen_zero(left_name)
+				|| !strcmp(left_name, ast_codec_media_type2str(ast_stream_get_type(left)))
+				|| !strcmp(left_name, ast_codec_media_type2str(ast_stream_get_type(right)))) {
+				/* The streams don't actually have real names */
+				cmp = -1;
+			}
+		}
+	}
+	return cmp;
+}
+
+/*!
+ * \internal
+ * \brief Merge topology streams by the match function.
+ * \since 15.0.0
+ *
+ * \param sdp_state
+ * \param current_topology Topology to update with state.
+ * \param update_topology Topology to merge into the current topology.
+ * \param current_vect Stream index vector of remaining current_topology streams.
+ * \param update_vect Stream index vector of remaining update_topology streams.
+ * \param backfill_candidate Array of flags marking current_topology streams
+ *            that can be reused for a different stream.
+ * \param match Stream comparison function to identify corresponding streams
+ *            between the current_topology and update_topology.
+ * \param merged_topology Output topology of merged streams.
+ * \param compact_streams TRUE if backfill and limit number of streams.
+ *
+ * \retval 0 on success.
+ * \retval -1 on failure.
+ */
+static int sdp_merge_streams_match(
+	const struct ast_sdp_state *sdp_state,
+	const struct ast_stream_topology *current_topology,
+	const struct ast_stream_topology *update_topology,
+	struct ast_vector_int *current_vect,
+	struct ast_vector_int *update_vect,
+	char backfill_candidate[],
+	int (*match)(const struct ast_stream *left, const struct ast_stream *right),
+	struct ast_stream_topology *merged_topology,
+	int compact_streams)
+{
+	struct ast_stream *current_stream;
+	struct ast_stream *update_stream;
+	int current_idx;
+	int update_idx;
+	int idx;
+
+	for (current_idx = 0; current_idx < AST_VECTOR_SIZE(current_vect);) {
+		idx = AST_VECTOR_GET(current_vect, current_idx);
+		current_stream = ast_stream_topology_get_stream(current_topology, idx);
+
+		for (update_idx = 0; update_idx < AST_VECTOR_SIZE(update_vect); ++update_idx) {
+			idx = AST_VECTOR_GET(update_vect, update_idx);
+			update_stream = ast_stream_topology_get_stream(update_topology, idx);
+
+			if (match(current_stream, update_stream)) {
+				continue;
+			}
+
+			if (!compact_streams
+				|| ast_stream_get_state(current_stream) != AST_STREAM_STATE_REMOVED
+				|| ast_stream_get_state(update_stream) != AST_STREAM_STATE_REMOVED) {
+				struct ast_stream *merged_stream;
+
+				merged_stream = merge_local_stream(sdp_state->options, update_stream);
+				if (!merged_stream) {
+					return -1;
+				}
+				idx = AST_VECTOR_GET(current_vect, current_idx);
+				ast_stream_topology_set_stream(merged_topology, idx, merged_stream);
+
+				/*
+				 * The current_stream cannot be considered a backfill_candidate
+				 * anymore since it got updated.
+				 *
+				 * XXX It could be argued that if the declined status didn't
+				 * change because the merged_stream became declined then we
+				 * shouldn't remove the stream slot as a backfill_candidate
+				 * and we shouldn't update the merged_topology stream.  If we
+				 * then backfilled the stream we would likely mess up the core
+				 * if it is matching streams by type since the core attempted
+				 * to update the stream with an incompatible stream.  Any
+				 * backfilled streams could cause a stream type ordering
+				 * problem.  However, we do need to reclaim declined stream
+				 * slots sometime.
+				 */
+				backfill_candidate[idx] = 0;
+			}
+
+			AST_VECTOR_REMOVE_ORDERED(current_vect, current_idx);
+			AST_VECTOR_REMOVE_ORDERED(update_vect, update_idx);
+			goto matched_next;
+		}
+
+		++current_idx;
+matched_next:;
+	}
+	return 0;
+}
+
+/*!
+ * \internal
+ * \brief Merge the current local topology with an updated topology.
+ * \since 15.0.0
+ *
+ * \param sdp_state
+ * \param current_topology Topology to update with state.
+ * \param update_topology Topology to merge into the current topology.
+ * \param compact_streams TRUE if backfill and limit number of streams.
+ *
+ * \retval merged topology on success.
+ * \retval NULL on failure.
+ */
+static struct ast_stream_topology *merge_local_topologies(
+	const struct ast_sdp_state *sdp_state,
+	const struct ast_stream_topology *current_topology,
+	const struct ast_stream_topology *update_topology,
+	int compact_streams)
+{
+	struct ast_stream_topology *merged_topology;
+	struct ast_stream *current_stream;
+	struct ast_stream *update_stream;
+	struct ast_stream *merged_stream;
+	struct ast_vector_int current_vect;
+	struct ast_vector_int update_vect;
+	int current_idx = ast_stream_topology_get_count(current_topology);
+	int update_idx;
+	int idx;
+	char backfill_candidate[current_idx];
+
+	memset(backfill_candidate, 0, current_idx);
+
+	if (compact_streams) {
+		/* Limit matching consideration to the maximum allowed live streams. */
+		idx = ast_sdp_options_get_max_streams(sdp_state->options);
+		if (idx < current_idx) {
+			current_idx = idx;
+		}
+	}
+	if (sdp_vect_idx_init(&current_vect, current_idx)) {
+		return NULL;
+	}
+
+	if (sdp_vect_idx_init(&update_vect, ast_stream_topology_get_count(update_topology))) {
+		AST_VECTOR_FREE(&current_vect);
+		return NULL;
+	}
+
+	merged_topology = ast_stream_topology_clone(current_topology);
+	if (!merged_topology) {
+		goto fail;
+	}
+
+	/*
+	 * Remove any unsupported current streams from match consideration
+	 * and mark potential backfill candidates.
+	 */
+	for (current_idx = AST_VECTOR_SIZE(&current_vect); current_idx--;) {
+		idx = AST_VECTOR_GET(&current_vect, current_idx);
+		current_stream = ast_stream_topology_get_stream(current_topology, idx);
+		if (ast_stream_get_state(current_stream) == AST_STREAM_STATE_REMOVED
+			&& compact_streams) {
+			/* The declined stream is a potential backfill candidate */
+			backfill_candidate[idx] = 1;
+		}
+		if (sdp_is_stream_type_supported(ast_stream_get_type(current_stream))) {
+			continue;
+		}
+		/* Unsupported current streams should always be declined */
+		ast_assert(ast_stream_get_state(current_stream) == AST_STREAM_STATE_REMOVED);
+
+		AST_VECTOR_REMOVE_ORDERED(&current_vect, current_idx);
+	}
+
+	/* Remove any unsupported update streams from match consideration. */
+	for (update_idx = AST_VECTOR_SIZE(&update_vect); update_idx--;) {
+		idx = AST_VECTOR_GET(&update_vect, update_idx);
+		update_stream = ast_stream_topology_get_stream(update_topology, idx);
+		if (sdp_is_stream_type_supported(ast_stream_get_type(update_stream))) {
+			continue;
+		}
+
+		AST_VECTOR_REMOVE_ORDERED(&update_vect, update_idx);
+	}
+
+	/* Match by stream name and type */
+	if (sdp_merge_streams_match(sdp_state, current_topology, update_topology,
+		&current_vect, &update_vect, backfill_candidate, sdp_stream_cmp_by_name,
+		merged_topology, compact_streams)) {
+		goto fail;
+	}
+
+	/* Match by stream type */
+	if (sdp_merge_streams_match(sdp_state, current_topology, update_topology,
+		&current_vect, &update_vect, backfill_candidate, sdp_stream_cmp_by_type,
+		merged_topology, compact_streams)) {
+		goto fail;
+	}
+
+	/* Decline unmatched current stream slots */
+	for (current_idx = AST_VECTOR_SIZE(&current_vect); current_idx--;) {
+		idx = AST_VECTOR_GET(&current_vect, current_idx);
+		current_stream = ast_stream_topology_get_stream(current_topology, idx);
+
+		if (ast_stream_get_state(current_stream) == AST_STREAM_STATE_REMOVED) {
+			/* Stream is already declined. */
+			continue;
+		}
+
+		merged_stream = decline_stream(ast_stream_get_type(current_stream),
+			ast_stream_get_name(current_stream));
+		if (!merged_stream) {
+			goto fail;
+		}
+		ast_stream_topology_set_stream(merged_topology, idx, merged_stream);
+	}
+
+	/* Backfill new update stream slots into pre-existing declined current stream slots */
+	while (AST_VECTOR_SIZE(&update_vect)) {
+		idx = ast_stream_topology_get_count(current_topology);
+		for (current_idx = 0; current_idx < idx; ++current_idx) {
+			if (backfill_candidate[current_idx]) {
+				break;
+			}
+		}
+		if (idx <= current_idx) {
+			/* No more backfill candidates remain. */
+			break;
+		}
+		/* There should only be backfill stream slots when we are compact_streams */
+		ast_assert(compact_streams);
+
+		idx = AST_VECTOR_GET(&update_vect, 0);
+		update_stream = ast_stream_topology_get_stream(update_topology, idx);
+		AST_VECTOR_REMOVE_ORDERED(&update_vect, 0);
+
+		if (ast_stream_get_state(update_stream) == AST_STREAM_STATE_REMOVED) {
+			/* New stream is already declined so don't bother adding it. */
+			continue;
+		}
+
+		merged_stream = merge_local_stream(sdp_state->options, update_stream);
+		if (!merged_stream) {
+			goto fail;
+		}
+		if (ast_stream_get_state(merged_stream) == AST_STREAM_STATE_REMOVED) {
+			/* New stream not compatible so don't bother adding it. */
+			ast_stream_free(merged_stream);
+			continue;
+		}
+
+		/* Add the new stream into the backfill stream slot. */
+		ast_stream_topology_set_stream(merged_topology, current_idx, merged_stream);
+		backfill_candidate[current_idx] = 0;
+	}
+
+	/* Append any remaining new update stream slots that can fit. */
+	while (AST_VECTOR_SIZE(&update_vect)
+		&& (!compact_streams
+			|| ast_stream_topology_get_count(merged_topology)
+				< ast_sdp_options_get_max_streams(sdp_state->options))) {
+		idx = AST_VECTOR_GET(&update_vect, 0);
+		update_stream = ast_stream_topology_get_stream(update_topology, idx);
+		AST_VECTOR_REMOVE_ORDERED(&update_vect, 0);
+
+		if (ast_stream_get_state(update_stream) == AST_STREAM_STATE_REMOVED) {
+			/* New stream is already declined so don't bother adding it. */
+			continue;
+		}
+
+		merged_stream = merge_local_stream(sdp_state->options, update_stream);
+		if (!merged_stream) {
+			goto fail;
+		}
+		if (ast_stream_get_state(merged_stream) == AST_STREAM_STATE_REMOVED) {
+			/* New stream not compatible so don't bother adding it. */
+			ast_stream_free(merged_stream);
+			continue;
+		}
+
+		/* Append the new update stream. */
+		if (ast_stream_topology_append_stream(merged_topology, merged_stream) < 0) {
+			ast_stream_free(merged_stream);
+			goto fail;
+		}
+	}
+
+	AST_VECTOR_FREE(&current_vect);
+	AST_VECTOR_FREE(&update_vect);
+	return merged_topology;
+
+fail:
+	ast_stream_topology_free(merged_topology);
+	AST_VECTOR_FREE(&current_vect);
+	AST_VECTOR_FREE(&update_vect);
+	return NULL;
+}
+
+/*!
+ * \internal
+ * \brief Remove declined streams appended beyond orig_topology.
+ * \since 15.0.0
+ *
+ * \param sdp_state
+ * \param orig_topology Negotiated or initial topology.
+ * \param new_topology New proposed topology.
+ *
+ * \return Nothing
+ */
+static void remove_appended_declined_streams(const struct ast_sdp_state *sdp_state,
+	const struct ast_stream_topology *orig_topology,
+	struct ast_stream_topology *new_topology)
+{
+	struct ast_stream *stream;
+	int orig_count;
+	int idx;
+
+	orig_count = ast_stream_topology_get_count(orig_topology);
+	for (idx = ast_stream_topology_get_count(new_topology); orig_count < idx;) {
+		--idx;
+		stream = ast_stream_topology_get_stream(new_topology, idx);
+		if (ast_stream_get_state(stream) != AST_STREAM_STATE_REMOVED) {
+			continue;
+		}
+		ast_stream_topology_del_stream(new_topology, idx);
+	}
+}
+
+/*!
+ * \internal
+ * \brief Setup a new state stream from a possibly existing state stream.
+ * \since 15.0.0
+ *
+ * \param sdp_state
+ * \param new_state_stream What state stream to setup
+ * \param old_state_stream Source of previous state stream information.
+ *            May be NULL.
+ * \param new_type Type of the new state stream.
+ *
+ * \retval 0 on success.
+ * \retval -1 on failure.
+ */
+static int setup_new_stream_capabilities(
+	const struct ast_sdp_state *sdp_state,
+	struct sdp_state_stream *new_state_stream,
+	struct sdp_state_stream *old_state_stream,
+	enum ast_media_type new_type)
+{
+	if (old_state_stream) {
+		/*
+		 * Copy everything potentially useful for a new stream state type
+		 * from the old stream of a possible different type.
+		 */
+		sdp_state_stream_copy_common(new_state_stream, old_state_stream);
+		/* We also need to preserve the locally_held state for the new stream. */
+		new_state_stream->locally_held = old_state_stream->locally_held;
+	}
+	new_state_stream->type = new_type;
+
+	switch (new_type) {
+	case AST_MEDIA_TYPE_AUDIO:
+	case AST_MEDIA_TYPE_VIDEO:
+		new_state_stream->rtp = create_rtp(sdp_state->options, new_type);
+		if (!new_state_stream->rtp) {
+			return -1;
+		}
+		break;
+	case AST_MEDIA_TYPE_IMAGE:
+		new_state_stream->udptl = create_udptl(sdp_state->options);
+		if (!new_state_stream->udptl) {
+			return -1;
+		}
+		break;
+	case AST_MEDIA_TYPE_UNKNOWN:
+	case AST_MEDIA_TYPE_TEXT:
+	case AST_MEDIA_TYPE_END:
+		break;
+	}
+	return 0;
+}
+
+/*!
+ * \brief Merge existing stream capabilities and a new topology.
  *
  * \param sdp_state The state needing capabilities merged
- * \param new_topology The new topology to base merged capabilities on
- * \param is_local If new_topology is a local update.
+ * \param new_topology The topology to merge with our proposed capabilities
  *
  * \details
+ *
  * 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.
+ * topology from the system.  We want to take what we had before and
+ * merge them with the new topology from the system.
  *
- * For each of the new streams, we try to find a corresponding stream
- * in our proposed 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.
+ * According to the RFC, stream slots can change their types only if
+ * they are carrying the same logical information or an offer is
+ * reusing a declined slot or new stream slots are added to the end
+ * of the list.  Switching a stream from audio to T.38 makes sense
+ * because the stream slot is carrying the same information just in a
+ * different format.
  *
- * The is_local 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 is_local parameter is
- * true, this created stream is made a clone of the new stream, and a
- * media instance is created.  If the is_local parameter is not true,
- * then the created stream has no formats set and no media instance is
- * created for it.
+ * We can setup new streams offered by the system up to our
+ * configured maximum stream slots.  New stream slots requested over
+ * the maximum are discarded.
  *
  * \retval NULL An error occurred
  * \retval non-NULL The merged capabilities
  */
-static struct sdp_state_capabilities *merge_capabilities(const struct ast_sdp_state *sdp_state,
-	const struct ast_stream_topology *new_topology, int is_local)
+static struct sdp_state_capabilities *merge_local_capabilities(
+	const struct ast_sdp_state *sdp_state,
+	const struct ast_stream_topology *new_topology)
+{
+	const struct sdp_state_capabilities *current = sdp_state->proposed_capabilities;
+	struct sdp_state_capabilities *merged_capabilities;
+	int idx;
+
+	ast_assert(current != NULL);
+
+	merged_capabilities = ast_calloc(1, sizeof(*merged_capabilities));
+	if (!merged_capabilities) {
+		return NULL;
+	}
+
+	merged_capabilities->topology = merge_local_topologies(sdp_state, current->topology,
+		new_topology, 1);
+	if (!merged_capabilities->topology) {
+		goto fail;
+	}
+	sdp_state_cb_offerer_modify_topology(sdp_state, merged_capabilities->topology);
+	remove_appended_declined_streams(sdp_state, current->topology,
+		merged_capabilities->topology);
+
+	if (AST_VECTOR_INIT(&merged_capabilities->streams,
+		ast_stream_topology_get_count(merged_capabilities->topology))) {
+		goto fail;
+	}
+
+	for (idx = 0; idx < ast_stream_topology_get_count(merged_capabilities->topology); ++idx) {
+		struct sdp_state_stream *merged_state_stream;
+		struct sdp_state_stream *current_state_stream;
+		struct ast_stream *merged_stream;
+		struct ast_stream *current_stream;
+		enum ast_media_type merged_stream_type;
+		enum ast_media_type current_stream_type;
+
+		merged_state_stream = ast_calloc(1, sizeof(*merged_state_stream));
+		if (!merged_state_stream) {
+			goto fail;
+		}
+
+		merged_stream = ast_stream_topology_get_stream(merged_capabilities->topology, idx);
+		merged_stream_type = ast_stream_get_type(merged_stream);
+
+		if (idx < ast_stream_topology_get_count(current->topology)) {
+			current_state_stream = AST_VECTOR_GET(&current->streams, idx);
+			current_stream = ast_stream_topology_get_stream(current->topology, idx);
+			current_stream_type = ast_stream_get_type(current_stream);
+		} else {
+			/* The merged topology is adding a stream */
+			current_state_stream = NULL;
+			current_stream = NULL;
+			current_stream_type = AST_MEDIA_TYPE_UNKNOWN;
+		}
+
+		if (ast_stream_get_state(merged_stream) == AST_STREAM_STATE_REMOVED) {
+			if (current_state_stream) {
+				/* Copy everything potentially useful to a declined stream state. */
+				sdp_state_stream_copy_common(merged_state_stream, current_state_stream);
+			}
+			merged_state_stream->type = merged_stream_type;
+		} else if (!current_stream
+			|| ast_stream_get_state(current_stream) == AST_STREAM_STATE_REMOVED) {
+			/* This is a new stream */
+			if (setup_new_stream_capabilities(sdp_state, merged_state_stream,
+				current_state_stream, merged_stream_type)) {
+				sdp_state_stream_free(merged_state_stream);
+				goto fail;
+			}
+		} else if (merged_stream_type == current_stream_type) {
+			/* Stream type is not changing. */
+			sdp_state_stream_copy(merged_state_stream, current_state_stream);
+		} else {
+			/*
+			 * Stream type is changing.  Need to replace the stream.
+			 *
+			 * Unsupported streams should already be handled earlier because
+			 * they are always declined.
+			 */
+			ast_assert(sdp_is_stream_type_supported(merged_stream_type));
+
+			/*
+			 * XXX We might need to keep the old RTP instance if the new
+			 * stream type is also RTP.  We would just be changing between
+			 * audio and video in that case.  However we will create a new
+			 * RTP instance anyway since its purpose has to be changing.
+			 * Any RTP packets in flight from the old stream type might
+			 * cause mischief.
+			 */
+			if (setup_new_stream_capabilities(sdp_state, merged_state_stream,
+				current_state_stream, merged_stream_type)) {
+				sdp_state_stream_free(merged_state_stream);
+				goto fail;
+			}
+		}
+
+		if (AST_VECTOR_APPEND(&merged_capabilities->streams, merged_state_stream)) {
+			sdp_state_stream_free(merged_state_stream);
+			goto fail;
+		}
+	}
+
+	return merged_capabilities;
+
+fail:
+	sdp_state_capabilities_free(merged_capabilities);
+	return NULL;
+}
+
+static void merge_remote_stream_capabilities(
+	const struct ast_sdp_state *sdp_state,
+	struct sdp_state_stream *joint_state_stream,
+	struct sdp_state_stream *local_state_stream,
+	struct ast_stream *remote_stream)
+{
+	struct ast_rtp_codecs *codecs;
+
+	*joint_state_stream = *local_state_stream;
+	/*
+	 * Need to explicitly set the type to the remote because we could
+	 * be changing the type between audio and video.
+	 */
+	joint_state_stream->type = ast_stream_get_type(remote_stream);
+
+	switch (joint_state_stream->type) {
+	case AST_MEDIA_TYPE_AUDIO:
+	case AST_MEDIA_TYPE_VIDEO:
+		ao2_bump(joint_state_stream->rtp);
+		codecs = ast_stream_get_data(remote_stream, AST_STREAM_DATA_RTP_CODECS);
+		ast_assert(codecs != NULL);
+		if (sdp_state->role == SDP_ROLE_ANSWERER) {
+			/*
+			 * Setup rx payload type mapping to prefer the mapping
+			 * from the peer that the RFC says we SHOULD use.
+			 */
+			ast_rtp_codecs_payloads_xover(codecs, codecs, NULL);
+		}
+		ast_rtp_codecs_payloads_copy(codecs,
+			ast_rtp_instance_get_codecs(joint_state_stream->rtp->instance),
+			joint_state_stream->rtp->instance);
+		break;
+	case AST_MEDIA_TYPE_IMAGE:
+		joint_state_stream->udptl = ao2_bump(joint_state_stream->udptl);
+		break;
+	case AST_MEDIA_TYPE_UNKNOWN:
+	case AST_MEDIA_TYPE_TEXT:
+	case AST_MEDIA_TYPE_END:
+		break;
+	}
+}
+
+static int create_remote_stream_capabilities(
+	const struct ast_sdp_state *sdp_state,
+	struct sdp_state_stream *joint_state_stream,
+	struct sdp_state_stream *local_state_stream,
+	struct ast_stream *remote_stream)
+{
+	struct ast_rtp_codecs *codecs;
+
+	/* We can only create streams if we are the answerer */
+	ast_assert(sdp_state->role == SDP_ROLE_ANSWERER);
+
+	if (local_state_stream) {
+		/*
+		 * Copy everything potentially useful for a new stream state type
+		 * from the old stream of a possible different type.
+		 */
+		sdp_state_stream_copy_common(joint_state_stream, local_state_stream);
+		/* We also need to preserve the locally_held state for the new stream. */
+		joint_state_stream->locally_held = local_state_stream->locally_held;
+	}
+	joint_state_stream->type = ast_stream_get_type(remote_stream);
+
+	switch (joint_state_stream->type) {
+	case AST_MEDIA_TYPE_AUDIO:
+	case AST_MEDIA_TYPE_VIDEO:
+		joint_state_stream->rtp = create_rtp(sdp_state->options, joint_state_stream->type);
+		if (!joint_state_stream->rtp) {
+			return -1;
+		}
+
+		/*
+		 * Setup rx payload type mapping to prefer the mapping
+		 * from the peer that the RFC says we SHOULD use.
+		 */
+		codecs = ast_stream_get_data(remote_stream, AST_STREAM_DATA_RTP_CODECS);
+		ast_assert(codecs != NULL);
+		ast_rtp_codecs_payloads_xover(codecs, codecs, NULL);
+		ast_rtp_codecs_payloads_copy(codecs,
+			ast_rtp_instance_get_codecs(joint_state_stream->rtp->instance),
+			joint_state_stream->rtp->instance);
+		break;
+	case AST_MEDIA_TYPE_IMAGE:
+		joint_state_stream->udptl = create_udptl(sdp_state->options);
+		if (!joint_state_stream->udptl) {
+			return -1;
+		}
+		break;
+	case AST_MEDIA_TYPE_UNKNOWN:
+	case AST_MEDIA_TYPE_TEXT:
+	case AST_MEDIA_TYPE_END:
+		break;
+	}
+	return 0;
+}
+
+/*!
+ * \internal
+ * \brief Create a joint topology from the remote topology.
+ * \since 15.0.0
+ *
+ * \param sdp_state The state needing capabilities merged.
+ * \param local Capabilities to merge the remote topology into.
+ * \param remote_topology The topology to merge with our local capabilities.
+ *
+ * \retval joint topology on success.
+ * \retval NULL on failure.
+ */
+static struct ast_stream_topology *merge_remote_topology(
+	const struct ast_sdp_state *sdp_state,
+	const struct sdp_state_capabilities *local,
+	const struct ast_stream_topology *remote_topology)
+{
+	struct ast_stream_topology *joint_topology;
+	int idx;
+
+	joint_topology = ast_stream_topology_alloc();
+	if (!joint_topology) {
+		return NULL;
+	}
+
+	for (idx = 0; idx < ast_stream_topology_get_count(remote_topology); ++idx) {
+		enum ast_media_type local_stream_type;
+		enum ast_media_type remote_stream_type;
+		struct ast_stream *remote_stream;
+		struct ast_stream *local_stream;
+		struct ast_stream *joint_stream;
+		struct sdp_state_stream *local_state_stream;
+
+		remote_stream = ast_stream_topology_get_stream(remote_topology, idx);
+		remote_stream_type = ast_stream_get_type(remote_stream);
+
+		if (idx < ast_stream_topology_get_count(local->topology)) {
+			local_state_stream = AST_VECTOR_GET(&local->streams, idx);
+			local_stream = ast_stream_topology_get_stream(local->topology, idx);
+			local_stream_type = ast_stream_get_type(local_stream);
+		} else {
+			/* The remote is adding a stream slot */
+			local_state_stream = NULL;
+			local_stream = NULL;
+			local_stream_type = AST_MEDIA_TYPE_UNKNOWN;
+
+			if (sdp_state->role != SDP_ROLE_ANSWERER) {
+				/* Remote cannot add a new stream slot in an answer SDP */
+				ast_debug(1,
+					"Bad.  Ignoring new %s stream slot remote answer SDP trying to add.\n",
+					ast_codec_media_type2str(remote_stream_type));
+				continue;
+			}
+		}
+
+		if (local_stream
+			&& ast_stream_get_state(local_stream) != AST_STREAM_STATE_REMOVED) {
+			if (remote_stream_type == local_stream_type) {
+				/* Stream type is not changing. */
+				joint_stream = merge_remote_stream(sdp_state, local_stream,
+					local_state_stream->locally_held, remote_stream);
+			} else if (sdp_state->role == SDP_ROLE_ANSWERER) {
+				/* Stream type is changing. */
+				joint_stream = merge_remote_stream(sdp_state, NULL,
+					local_state_stream->locally_held, remote_stream);
+			} else {
+				/*
+				 * Remote cannot change the stream type we offered.
+				 * Mark as declined.
+				 */
+				ast_debug(1,
+					"Bad.  Remote answer SDP trying to change the stream type from %s to %s.\n",
+					ast_codec_media_type2str(local_stream_type),
+					ast_codec_media_type2str(remote_stream_type));
+				joint_stream = decline_stream(local_stream_type,
+					ast_stream_get_name(local_stream));
+			}
+		} else {
+			/* Local stream is either dead/declined or nonexistent. */
+			if (sdp_state->role == SDP_ROLE_ANSWERER) {
+				if (sdp_is_stream_type_supported(remote_stream_type)
+					&& ast_stream_get_state(remote_stream) != AST_STREAM_STATE_REMOVED
+					&& idx < ast_sdp_options_get_max_streams(sdp_state->options)) {
+					/* Try to create the new stream */
+					joint_stream = merge_remote_stream(sdp_state, NULL,
+						local_state_stream ? local_state_stream->locally_held : 0,
+						remote_stream);
+				} else {
+					const char *stream_name;
+
+					/* Decline the remote stream. */
+					if (local_stream
+						&& local_stream_type == remote_stream_type) {
+						/* Preserve the previous stream name */
+						stream_name = ast_stream_get_name(local_stream);
+					} else {
+						stream_name = NULL;
+					}
+					joint_stream = decline_stream(remote_stream_type, stream_name);
+				}
+			} else {
+				/* Decline the stream. */
+				if (DEBUG_ATLEAST(1)
+					&& ast_stream_get_state(remote_stream) != AST_STREAM_STATE_REMOVED) {
+					/*
+					 * Remote cannot request a new stream in place of a declined
+					 * stream in an answer SDP.
+					 */
+					ast_log(LOG_DEBUG,
+						"Bad.  Remote answer SDP trying to use a declined stream slot for %s.\n",
+						ast_codec_media_type2str(remote_stream_type));
+				}
+				joint_stream = decline_stream(local_stream_type,
+					ast_stream_get_name(local_stream));
+			}
+		}
+
+		if (!joint_stream) {
+			goto fail;
+		}
+		if (ast_stream_topology_append_stream(joint_topology, joint_stream) < 0) {
+			ast_stream_free(joint_stream);
+			goto fail;
+		}
+	}
+
+	return joint_topology;
+
+fail:
+	ast_stream_topology_free(joint_topology);
+	return NULL;
+}
+
+/*!
+ * \brief Merge our stream capabilities and a remote topology into joint capabilities.
+ *
+ * \param sdp_state The state needing capabilities merged
+ * \param remote_topology The topology to merge with our proposed capabilities
+ *
+ * \details
+ * This is a bit complicated. The idea is that we already have some
+ * capabilities set, and we've now been confronted with a stream
+ * topology from the remote end.  We want to take what's been
+ * presented to us and merge those new capabilities with our own.
+ *
+ * According to the RFC, stream slots can change their types only if
+ * they are carrying the same logical information or an offer is
+ * reusing a declined slot or new stream slots are added to the end
+ * of the list.  Switching a stream from audio to T.38 makes sense
+ * because the stream slot is carrying the same information just in a
+ * different format.
+ *
+ * When we are the answerer we can setup new streams offered by the
+ * remote up to our configured maximum stream slots.  New stream
+ * slots offered over the maximum are unconditionally declined.
+ *
+ * \retval NULL An error occurred
+ * \retval non-NULL The merged capabilities
+ */
+static struct sdp_state_capabilities *merge_remote_capabilities(
+	const struct ast_sdp_state *sdp_state,
+	const struct ast_stream_topology *remote_topology)
 {
 	const struct sdp_state_capabilities *local = sdp_state->proposed_capabilities;
 	struct sdp_state_capabilities *joint_capabilities;
-	int media_indices[AST_MEDIA_TYPE_END] = {0};
-	int i;
-	static const char dummy_name[] = "dummy";
+	int idx;
 
 	ast_assert(local != NULL);
 
@@ -700,150 +1975,131 @@
 		return NULL;
 	}
 
-	joint_capabilities->topology = ast_stream_topology_alloc();
+	joint_capabilities->topology = merge_remote_topology(sdp_state, local, remote_topology);
 	if (!joint_capabilities->topology) {
 		goto fail;
 	}
 
-	if (AST_VECTOR_INIT(&joint_capabilities->streams, AST_VECTOR_SIZE(&local->streams))) {
+	if (sdp_state->role == SDP_ROLE_ANSWERER) {
+		sdp_state_cb_answerer_modify_topology(sdp_state, joint_capabilities->topology);
+	}
+	idx = ast_stream_topology_get_count(joint_capabilities->topology);
+	if (AST_VECTOR_INIT(&joint_capabilities->streams, idx)) {
 		goto fail;
 	}
-	ast_sockaddr_copy(&joint_capabilities->connection_address, &local->connection_address);
 
-	for (i = 0; i < ast_stream_topology_get_count(new_topology); ++i) {
-		enum ast_media_type new_stream_type;
-		struct ast_stream *new_stream;
+	for (idx = 0; idx < ast_stream_topology_get_count(remote_topology); ++idx) {
+		enum ast_media_type local_stream_type;
+		enum ast_media_type remote_stream_type;
+		struct ast_stream *remote_stream;
 		struct ast_stream *local_stream;
 		struct ast_stream *joint_stream;
+		struct sdp_state_stream *local_state_stream;
 		struct sdp_state_stream *joint_state_stream;
-		int local_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);
+		remote_stream = ast_stream_topology_get_stream(remote_topology, idx);
+		remote_stream_type = ast_stream_get_type(remote_stream);
 
-		local_index = get_corresponding_index(local->topology, new_stream_type, media_indices);
-		if (0 <= local_index) {
-			local_stream = ast_stream_topology_get_stream(local->topology, local_index);
-			if (!strcmp(ast_stream_get_name(local_stream), dummy_name)) {
-				/* The local stream is a non-exixtent dummy stream. */
-				local_stream = NULL;
-			}
+		if (idx < ast_stream_topology_get_count(local->topology)) {
+			local_state_stream = AST_VECTOR_GET(&local->streams, idx);
+			local_stream = ast_stream_topology_get_stream(local->topology, idx);
+			local_stream_type = ast_stream_get_type(local_stream);
 		} else {
+			/* The remote is adding a stream slot */
+			local_state_stream = NULL;
 			local_stream = NULL;
+			local_stream_type = AST_MEDIA_TYPE_UNKNOWN;
+
+			if (sdp_state->role != SDP_ROLE_ANSWERER) {
+				/* Remote cannot add a new stream slot in an answer SDP */
+				sdp_state_stream_free(joint_state_stream);
+				break;
+			}
 		}
-		if (local_stream) {
-			struct sdp_state_stream *local_state_stream;
-			struct ast_rtp_codecs *codecs;
 
-			if (is_local) {
-				/* Replace the local stream with the new local stream. */
-				joint_stream = ast_stream_clone(new_stream, NULL);
+		joint_stream = ast_stream_topology_get_stream(joint_capabilities->topology,
+			idx);
+
+		if (local_stream
+			&& ast_stream_get_state(local_stream) != AST_STREAM_STATE_REMOVED) {
+			if (ast_stream_get_state(joint_stream) == AST_STREAM_STATE_REMOVED) {
+				/* Copy everything potentially useful to a declined stream state. */
+				sdp_state_stream_copy_common(joint_state_stream, local_state_stream);
+
+				joint_state_stream->type = ast_stream_get_type(joint_stream);
+			} else if (remote_stream_type == local_stream_type) {
+				/* Stream type is not changing. */
+				merge_remote_stream_capabilities(sdp_state, joint_state_stream,
+					local_state_stream, remote_stream);
+				ast_assert(joint_state_stream->type == ast_stream_get_type(joint_stream));
 			} else {
-				joint_stream = merge_streams(local_stream, new_stream);
-			}
-			if (!joint_stream) {
-				sdp_state_stream_free(joint_state_stream);
-				goto fail;
-			}
-
-			local_state_stream = AST_VECTOR_GET(&local->streams, local_index);
-			joint_state_stream->type = local_state_stream->type;
-
-			switch (joint_state_stream->type) {
-			case AST_MEDIA_TYPE_AUDIO:
-			case AST_MEDIA_TYPE_VIDEO:
-				joint_state_stream->rtp = ao2_bump(local_state_stream->rtp);
-				if (is_local) {
-					break;
-				}
-				codecs = ast_stream_get_data(new_stream, AST_STREAM_DATA_RTP_CODECS);
-				ast_assert(codecs != NULL);
-				if (sdp_state->role == SDP_ROLE_ANSWERER) {
-					/*
-					 * Setup rx payload type mapping to prefer the mapping
-					 * from the peer that the RFC says we SHOULD use.
-					 */
-					ast_rtp_codecs_payloads_xover(codecs, codecs, NULL);
-				}
-				ast_rtp_codecs_payloads_copy(codecs,
-					ast_rtp_instance_get_codecs(joint_state_stream->rtp->instance),
-					joint_state_stream->rtp->instance);
-				break;
-			case AST_MEDIA_TYPE_IMAGE:
-				joint_state_stream->udptl = ao2_bump(local_state_stream->udptl);
-				joint_state_stream->t38_local_params = local_state_stream->t38_local_params;
-				break;
-			case AST_MEDIA_TYPE_UNKNOWN:
-			case AST_MEDIA_TYPE_TEXT:
-			case AST_MEDIA_TYPE_END:
-				break;
-			}
-
-			if (!ast_sockaddr_isnull(&local_state_stream->connection_address)) {
-				ast_sockaddr_copy(&joint_state_stream->connection_address,
-					&local_state_stream->connection_address);
-			} else {
-				ast_sockaddr_setnull(&joint_state_stream->connection_address);
-			}
-			joint_state_stream->locally_held = local_state_stream->locally_held;
-		} else if (is_local) {
-			/* We don't have a stream state that corresponds to the stream in the new topology, so
-			 * create a stream state as appropriate.
-			 */
-			joint_stream = ast_stream_clone(new_stream, NULL);
-			if (!joint_stream) {
-				sdp_state_stream_free(joint_state_stream);
-				goto fail;
-			}
-
-			switch (new_stream_type) {
-			case AST_MEDIA_TYPE_AUDIO:
-			case AST_MEDIA_TYPE_VIDEO:
-				joint_state_stream->rtp = create_rtp(sdp_state->options,
-					new_stream_type);
-				if (!joint_state_stream->rtp) {
-					ast_stream_free(joint_stream);
+				/*
+				 * Stream type is changing.  Need to replace the stream.
+				 *
+				 * XXX We might need to keep the old RTP instance if the new
+				 * stream type is also RTP.  We would just be changing between
+				 * audio and video in that case.  However we will create a new
+				 * RTP instance anyway since its purpose has to be changing.
+				 * Any RTP packets in flight from the old stream type might
+				 * cause mischief.
+				 */
+				if (create_remote_stream_capabilities(sdp_state, joint_state_stream,
+					local_state_stream, remote_stream)) {
 					sdp_state_stream_free(joint_state_stream);
 					goto fail;
 				}
-				break;
-			case AST_MEDIA_TYPE_IMAGE:
-				joint_state_stream->udptl = create_udptl(sdp_state->options);
-				if (!joint_state_stream->udptl) {
-					ast_stream_free(joint_stream);
-					sdp_state_stream_free(joint_state_stream);
-					goto fail;
-				}
-				break;
-			case AST_MEDIA_TYPE_UNKNOWN:
-			case AST_MEDIA_TYPE_TEXT:
-			case AST_MEDIA_TYPE_END:
-				break;
+				ast_assert(joint_state_stream->type == ast_stream_get_type(joint_stream));
 			}
-			ast_sockaddr_setnull(&joint_state_stream->connection_address);
-			joint_state_stream->locally_held = 0;
 		} else {
-			/* We don't have a stream that corresponds to the stream in the new topology. Create a
-			 * dummy stream to go in its place so that the resulting SDP created will contain
-			 * the stream but will have no port or codecs set
-			 */
-			joint_stream = ast_stream_alloc(dummy_name, new_stream_type);
-			if (!joint_stream) {
-				sdp_state_stream_free(joint_state_stream);
-				goto fail;
+			/* Local stream is either dead/declined or nonexistent. */
+			if (sdp_state->role == SDP_ROLE_ANSWERER) {
+				if (ast_stream_get_state(joint_stream) == AST_STREAM_STATE_REMOVED) {
+					if (local_state_stream) {
+						/* Copy everything potentially useful to a declined stream state. */
+						sdp_state_stream_copy_common(joint_state_stream, local_state_stream);
+					}
+					joint_state_stream->type = ast_stream_get_type(joint_stream);
+				} else {
+					/* Try to create the new stream */
+					if (create_remote_stream_capabilities(sdp_state, joint_state_stream,
+						local_state_stream, remote_stream)) {
+						sdp_state_stream_free(joint_state_stream);
+						goto fail;
+					}
+					ast_assert(joint_state_stream->type == ast_stream_get_type(joint_stream));
+				}
+			} else {
+				/* Decline the stream. */
+				ast_assert(ast_stream_get_state(joint_stream) == AST_STREAM_STATE_REMOVED);
+				if (local_state_stream) {
+					/* Copy everything potentially useful to a declined stream state. */
+					sdp_state_stream_copy_common(joint_state_stream, local_state_stream);
+				}
+				joint_state_stream->type = ast_stream_get_type(joint_stream);
 			}
 		}
 
-		if (ast_stream_topology_append_stream(joint_capabilities->topology, joint_stream) < 0) {
-			ast_stream_free(joint_stream);
-			sdp_state_stream_free(joint_state_stream);
-			goto fail;
+		/* Determine if the remote placed the stream on hold. */
+		joint_state_stream->remotely_held = 0;
+		if (ast_stream_get_state(joint_stream) != AST_STREAM_STATE_REMOVED) {
+			enum ast_stream_state remote_state;
+
+			remote_state = ast_stream_get_state(remote_stream);
+			switch (remote_state) {
+			case AST_STREAM_STATE_INACTIVE:
+			case AST_STREAM_STATE_SENDONLY:
+				joint_state_stream->remotely_held = 1;
+				break;
+			default:
+				break;
+			}
 		}
+
 		if (AST_VECTOR_APPEND(&joint_capabilities->streams, joint_state_stream)) {
 			sdp_state_stream_free(joint_state_stream);
 			goto fail;
@@ -998,11 +2254,6 @@
 	struct ast_sdp_c_line *c_line;
 	struct ast_sockaddr *addrs;
 
-	if (!rtp) {
-		/* This is a dummy stream */
-		return;
-	}
-
 	c_line = remote_m_line->c_line;
 	if (!c_line) {
 		c_line = remote_sdp->c_line;
@@ -1058,11 +2309,6 @@
 	unsigned int fax_max_datagram;
 	struct ast_sockaddr *addrs;
 
-	if (!udptl) {
-		/* This is a dummy stream */
-		return;
-	}
-
 	a_line = ast_sdp_m_find_attribute(remote_m_line, "t38faxmaxdatagram", -1);
 	if (!a_line) {
 		a_line = ast_sdp_m_find_attribute(remote_m_line, "t38maxdatagram", -1);
@@ -1102,6 +2348,49 @@
 	}
 }
 
+static void sdp_apply_negotiated_state(struct ast_sdp_state *sdp_state)
+{
+	struct sdp_state_capabilities *capabilities = sdp_state->negotiated_capabilities;
+	int idx;
+
+	if (!capabilities) {
+		/* Nothing to apply */
+		return;
+	}
+
+	sdp_state_cb_preapply_topology(sdp_state, capabilities->topology);
+	for (idx = 0; idx < AST_VECTOR_SIZE(&capabilities->streams); ++idx) {
+		struct sdp_state_stream *state_stream;
+		struct ast_stream *stream;
+
+		stream = ast_stream_topology_get_stream(capabilities->topology, idx);
+		if (ast_stream_get_state(stream) == AST_STREAM_STATE_REMOVED) {
+			/* Stream is declined */
+			continue;
+		}
+
+		state_stream = AST_VECTOR_GET(&capabilities->streams, idx);
+		switch (ast_stream_get_type(stream)) {
+		case AST_MEDIA_TYPE_AUDIO:
+		case AST_MEDIA_TYPE_VIDEO:
+			update_rtp_after_merge(sdp_state, state_stream->rtp, sdp_state->options,
+				sdp_state->remote_sdp, ast_sdp_get_m(sdp_state->remote_sdp, idx));
+			break;
+		case AST_MEDIA_TYPE_IMAGE:
+			update_udptl_after_merge(sdp_state, state_stream->udptl, sdp_state->options,
+				sdp_state->remote_sdp, ast_sdp_get_m(sdp_state->remote_sdp, idx));
+			break;
+		case AST_MEDIA_TYPE_UNKNOWN:
+		case AST_MEDIA_TYPE_TEXT:
+		case AST_MEDIA_TYPE_END:
+			/* All unsupported streams are declined */
+			ast_assert(0);
+			break;
+		}
+	}
+	sdp_state_cb_postapply_topology(sdp_state, capabilities->topology);
+}
+
 static void set_negotiated_capabilities(struct ast_sdp_state *sdp_state,
 	struct sdp_state_capabilities *new_capabilities)
 {
@@ -1120,6 +2409,81 @@
 	sdp_state_capabilities_free(old_capabilities);
 }
 
+/*!
+ * \internal
+ * \brief Copy the new capabilities into the proposed capabilities.
+ * \since 15.0.0
+ *
+ * \param sdp_state The current SDP state
+ * \param new_capabilities Capabilities to copy
+ *
+ * \retval 0 on success.
+ * \retval -1 on failure.
+ */
+static int update_proposed_capabilities(struct ast_sdp_state *sdp_state,
+	struct sdp_state_capabilities *new_capabilities)
+{
+	struct sdp_state_capabilities *proposed_capabilities;
+	int idx;
+
+	proposed_capabilities = ast_calloc(1, sizeof(*proposed_capabilities));
+	if (!proposed_capabilities) {
+		return -1;
+	}
+
+	proposed_capabilities->topology = ast_stream_topology_clone(new_capabilities->topology);
+	if (!proposed_capabilities->topology) {
+		goto fail;
+	}
+
+	if (AST_VECTOR_INIT(&proposed_capabilities->streams,
+		AST_VECTOR_SIZE(&new_capabilities->streams))) {
+		goto fail;
+	}
+
+	for (idx = 0; idx < AST_VECTOR_SIZE(&new_capabilities->streams); ++idx) {
+		struct sdp_state_stream *proposed_state_stream;
+		struct sdp_state_stream *new_state_stream;
+
+		proposed_state_stream = ast_calloc(1, sizeof(*proposed_state_stream));
+		if (!proposed_state_stream) {
+			goto fail;
+		}
+
+		new_state_stream = AST_VECTOR_GET(&new_capabilities->streams, idx);
+		*proposed_state_stream = *new_state_stream;
+
+		switch (proposed_state_stream->type) {
+		case AST_MEDIA_TYPE_AUDIO:
+		case AST_MEDIA_TYPE_VIDEO:
+			ao2_bump(proposed_state_stream->rtp);
+			break;
+		case AST_MEDIA_TYPE_IMAGE:
+			ao2_bump(proposed_state_stream->udptl);
+			break;
+		case AST_MEDIA_TYPE_UNKNOWN:
+		case AST_MEDIA_TYPE_TEXT:
+		case AST_MEDIA_TYPE_END:
+			break;
+		}
+
+		/* This is explicitly never set on the proposed capabilities struct */
+		proposed_state_stream->remotely_held = 0;
+
+		if (AST_VECTOR_APPEND(&proposed_capabilities->streams, proposed_state_stream)) {
+			sdp_state_stream_free(proposed_state_stream);
+			goto fail;
+		}
+	}
+
+	set_proposed_capabilities(sdp_state, proposed_capabilities);
+	return 0;
+
+fail:
+	sdp_state_capabilities_free(proposed_capabilities);
+	return -1;
+}
+
 static struct ast_sdp *sdp_create_from_state(const struct ast_sdp_state *sdp_state,
 	const struct sdp_state_capabilities *capabilities);
 
@@ -1127,65 +2491,47 @@
  * \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
+ * 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
+ * \retval -1 Failure
+ *         Use ast_sdp_state_is_offer_rejected() to see if the offer SDP was rejected.
  */
 static int merge_sdps(struct ast_sdp_state *sdp_state, const struct ast_sdp *remote_sdp)
 {
 	struct sdp_state_capabilities *joint_capabilities;
 	struct ast_stream_topology *remote_capabilities;
-	int i;
 
 	remote_capabilities = ast_get_topology_from_sdp(remote_sdp,
-		sdp_state->options->g726_non_standard);
+		ast_sdp_options_get_g726_non_standard(sdp_state->options));
 	if (!remote_capabilities) {
 		return -1;
 	}
 
-	joint_capabilities = merge_capabilities(sdp_state, remote_capabilities, 0);
+	joint_capabilities = merge_remote_capabilities(sdp_state, remote_capabilities);
 	ast_stream_topology_free(remote_capabilities);
 	if (!joint_capabilities) {
 		return -1;
 	}
-	set_negotiated_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, joint_capabilities);
-	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);
-
-		switch (ast_stream_get_type(ast_stream_topology_get_stream(joint_capabilities->topology, i))) {
-		case AST_MEDIA_TYPE_AUDIO:
-		case AST_MEDIA_TYPE_VIDEO:
-			update_rtp_after_merge(sdp_state, state_stream->rtp, sdp_state->options,
-				remote_sdp, ast_sdp_get_m(remote_sdp, i));
-			break;
-		case AST_MEDIA_TYPE_IMAGE:
-			update_udptl_after_merge(sdp_state, state_stream->udptl, sdp_state->options,
-				remote_sdp, ast_sdp_get_m(remote_sdp, i));
-			break;
-		case AST_MEDIA_TYPE_UNKNOWN:
-		case AST_MEDIA_TYPE_TEXT:
-		case AST_MEDIA_TYPE_END:
-			break;
+	if (sdp_state->role == SDP_ROLE_ANSWERER) {
+		sdp_state->remote_offer_rejected =
+			sdp_topology_is_rejected(joint_capabilities->topology) ? 1 : 0;
+		if (sdp_state->remote_offer_rejected) {
+			sdp_state_capabilities_free(joint_capabilities);
+			return -1;
 		}
 	}
+	set_negotiated_capabilities(sdp_state, joint_capabilities);
+
+	ao2_cleanup(sdp_state->remote_sdp);
+	sdp_state->remote_sdp = ao2_bump((struct ast_sdp *) remote_sdp);
+
+	sdp_apply_negotiated_state(sdp_state);
 
 	return 0;
 }
@@ -1194,10 +2540,43 @@
 {
 	ast_assert(sdp_state != NULL);
 
-	if (sdp_state->role == SDP_ROLE_NOT_SET) {
+	switch (sdp_state->role) {
+	case SDP_ROLE_NOT_SET:
 		ast_assert(sdp_state->local_sdp == NULL);
 		sdp_state->role = SDP_ROLE_OFFERER;
+
+		if (sdp_state->pending_topology_update) {
+			struct sdp_state_capabilities *capabilities;
+
+			/* We have a topology update to perform before generating the offer */
+			capabilities = merge_local_capabilities(sdp_state,
+				sdp_state->pending_topology_update);
+			if (!capabilities) {
+				break;
+			}
+			ast_stream_topology_free(sdp_state->pending_topology_update);
+			sdp_state->pending_topology_update = NULL;
+			set_proposed_capabilities(sdp_state, capabilities);
+		}
+
+		/*
+		 * Allow the system to configure the topology streams
+		 * before we create the offer SDP.
+		 */
+		sdp_state_cb_offerer_config_topology(sdp_state,
+			sdp_state->proposed_capabilities->topology);
+
 		sdp_state->local_sdp = sdp_create_from_state(sdp_state, sdp_state->proposed_capabilities);
+		break;
+	case SDP_ROLE_OFFERER:
+		break;
+	case SDP_ROLE_ANSWERER:
+		if (!sdp_state->local_sdp
+			&& sdp_state->negotiated_capabilities
+			&& !sdp_state->remote_offer_rejected) {
+			sdp_state->local_sdp = sdp_create_from_state(sdp_state, sdp_state->negotiated_capabilities);
+		}
+		break;
 	}
 
 	return sdp_state->local_sdp;
@@ -1237,35 +2616,63 @@
 		return -1;
 	}
 	ret = ast_sdp_state_set_remote_sdp(sdp_state, sdp);
-	ast_sdp_free(sdp);
+	ao2_ref(sdp, -1);
 	return ret;
 }
 
-int ast_sdp_state_reset(struct ast_sdp_state *sdp_state)
+int ast_sdp_state_is_offer_rejected(struct ast_sdp_state *sdp_state)
+{
+	return sdp_state->remote_offer_rejected;
+}
+
+int ast_sdp_state_is_offerer(struct ast_sdp_state *sdp_state)
+{
+	return sdp_state->role == SDP_ROLE_OFFERER;
+}
+
+int ast_sdp_state_is_answerer(struct ast_sdp_state *sdp_state)
+{
+	return sdp_state->role == SDP_ROLE_ANSWERER;
+}
+
+int ast_sdp_state_restart_negotiations(struct ast_sdp_state *sdp_state)
 {
 	ast_assert(sdp_state != NULL);
 
-	ast_sdp_free(sdp_state->local_sdp);
+	ao2_cleanup(sdp_state->local_sdp);
 	sdp_state->local_sdp = NULL;
 
-	set_proposed_capabilities(sdp_state, NULL);
-
 	sdp_state->role = SDP_ROLE_NOT_SET;
+	sdp_state->remote_offer_rejected = 0;
+
+	if (sdp_state->negotiated_capabilities) {
+		update_proposed_capabilities(sdp_state, sdp_state->negotiated_capabilities);
+	}
 
 	return 0;
 }
 
-int ast_sdp_state_update_local_topology(struct ast_sdp_state *sdp_state, struct ast_stream_topology *streams)
+int ast_sdp_state_update_local_topology(struct ast_sdp_state *sdp_state, struct ast_stream_topology *topology)
 {
-	struct sdp_state_capabilities *capabilities;
-	ast_assert(sdp_state != NULL);
-	ast_assert(streams != NULL);
+	struct ast_stream_topology *merged_topology;
 
-	capabilities = merge_capabilities(sdp_state, streams, 1);
-	if (!capabilities) {
-		return -1;
+	ast_assert(sdp_state != NULL);
+	ast_assert(topology != NULL);
+
+	if (sdp_state->pending_topology_update) {
+		merged_topology = merge_local_topologies(sdp_state,
+			sdp_state->pending_topology_update, topology, 0);
+		if (!merged_topology) {
+			return -1;
+		}
+		ast_stream_topology_free(sdp_state->pending_topology_update);
+		sdp_state->pending_topology_update = merged_topology;
+	} else {
+		sdp_state->pending_topology_update = ast_stream_topology_clone(topology);
+		if (!sdp_state->pending_topology_update) {
+			return -1;
+		}
 	}
-	set_proposed_capabilities(sdp_state, capabilities);
 
 	return 0;
 }
@@ -1275,9 +2682,9 @@
 	ast_assert(sdp_state != NULL);
 
 	if (!address) {
-		ast_sockaddr_setnull(&sdp_state->proposed_capabilities->connection_address);
+		ast_sockaddr_setnull(&sdp_state->connection_address);
 	} else {
-		ast_sockaddr_copy(&sdp_state->proposed_capabilities->connection_address, address);
+		ast_sockaddr_copy(&sdp_state->connection_address, address);
 	}
 }
 
@@ -1301,18 +2708,37 @@
 	return 0;
 }
 
+void ast_sdp_state_set_global_locally_held(struct ast_sdp_state *sdp_state, unsigned int locally_held)
+{
+	ast_assert(sdp_state != NULL);
+
+	sdp_state->locally_held = locally_held ? 1 : 0;
+}
+
+unsigned int ast_sdp_state_get_global_locally_held(const struct ast_sdp_state *sdp_state)
+{
+	ast_assert(sdp_state != NULL);
+
+	return sdp_state->locally_held;
+}
+
 void ast_sdp_state_set_locally_held(struct ast_sdp_state *sdp_state,
 	int stream_index, unsigned int locally_held)
 {
 	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;
+	locally_held = locally_held ? 1 : 0;
+
+	stream_state = sdp_state_get_joint_stream(sdp_state, stream_index);
+	if (stream_state) {
+		stream_state->locally_held = locally_held;
 	}
 
-	stream_state->locally_held = locally_held;
+	stream_state = sdp_state_get_stream(sdp_state, stream_index);
+	if (stream_state) {
+		stream_state->locally_held = locally_held;
+	}
 }
 
 unsigned int ast_sdp_state_get_locally_held(const struct ast_sdp_state *sdp_state,
@@ -1321,12 +2747,27 @@
 	struct sdp_state_stream *stream_state;
 	ast_assert(sdp_state != NULL);
 
-	stream_state = sdp_state_get_stream(sdp_state, stream_index);
+	stream_state = sdp_state_get_joint_stream(sdp_state, stream_index);
 	if (!stream_state) {
 		return 0;
 	}
 
 	return stream_state->locally_held;
+}
+
+unsigned int ast_sdp_state_get_remotely_held(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_joint_stream(sdp_state, stream_index);
+	if (!stream_state) {
+		return 0;
+	}
+
+	return stream_state->remotely_held;
 }
 
 void ast_sdp_state_set_t38_parameters(struct ast_sdp_state *sdp_state,
@@ -1336,11 +2777,9 @@
 	ast_assert(sdp_state != NULL && params != NULL);
 
 	stream_state = sdp_state_get_stream(sdp_state, stream_index);
-	if (!stream_state) {
-		return;
+	if (stream_state) {
+		stream_state->t38_local_params = *params;
 	}
-
-	stream_state->t38_local_params = *params;
 }
 
 /*!
@@ -1398,7 +2837,8 @@
 	caps = ast_stream_get_formats(stream);
 
 	stream_state = AST_VECTOR_GET(&capabilities->streams, stream_index);
-	if (stream_state->rtp && caps && ast_format_cap_count(caps)) {
+	if (stream_state->rtp && caps && ast_format_cap_count(caps)
+		&& AST_STREAM_STATE_REMOVED != ast_stream_get_state(stream)) {
 		rtp = stream_state->rtp->instance;
 	} else {
 		/* This is a disabled stream */
@@ -1408,7 +2848,7 @@
 	if (rtp) {
 		struct ast_sockaddr address_rtp;
 
-		if (ast_sdp_state_get_stream_connection_address(sdp_state, 0, &address_rtp)) {
+		if (sdp_state_stream_get_connection_address(sdp_state, stream_state, &address_rtp)) {
 			return -1;
 		}
 		rtp_port = ast_sockaddr_port(&address_rtp);
@@ -1426,6 +2866,8 @@
 	}
 
 	if (rtp_port) {
+		const char *direction;
+
 		/* Stream is not declined/disabled */
 		for (i = 0; i < ast_format_cap_count(caps); i++) {
 			struct ast_format *format = ast_format_cap_get_format(caps, i);
@@ -1502,12 +2944,27 @@
 			}
 		}
 
-		a_line = ast_sdp_a_alloc(ast_sdp_state_get_locally_held(sdp_state, stream_index)
-			? "sendonly" : "sendrecv", "");
-		if (!a_line || ast_sdp_m_add_a(m_line, a_line)) {
-			ast_sdp_a_free(a_line);
-			ast_sdp_m_free(m_line);
-			return -1;
+		if (sdp_state->locally_held || stream_state->locally_held) {
+			if (stream_state->remotely_held) {
+				direction = "inactive";
+			} else {
+				direction = "sendonly";
+			}
+		} else {
+			if (stream_state->remotely_held) {
+				direction = "recvonly";
+			} else {
+				/* Default is "sendrecv" */
+				direction = NULL;
+			}
+		}
+		if (direction) {
+			a_line = ast_sdp_a_alloc(direction, "");
+			if (!a_line || ast_sdp_m_add_a(m_line, a_line)) {
+				ast_sdp_a_free(a_line);
+				ast_sdp_m_free(m_line);
+				return -1;
+			}
 		}
 
 		add_ssrc_attributes(m_line, options, rtp);
@@ -1585,7 +3042,8 @@
 	ast_assert(sdp && options && stream);
 
 	stream_state = AST_VECTOR_GET(&capabilities->streams, stream_index);
-	if (stream_state->udptl) {
+	if (stream_state->udptl
+		&& AST_STREAM_STATE_REMOVED != ast_stream_get_state(stream)) {
 		udptl = stream_state->udptl;
 	} else {
 		/* This is a disabled stream */
@@ -1595,7 +3053,7 @@
 	if (udptl) {
 		struct ast_sockaddr address_udptl;
 
-		if (ast_sdp_state_get_stream_connection_address(sdp_state, 0, &address_udptl)) {
+		if (sdp_state_stream_get_connection_address(sdp_state, stream_state, &address_udptl)) {
 			return -1;
 		}
 		udptl_port = ast_sockaddr_port(&address_udptl);
@@ -1619,8 +3077,6 @@
 
 	if (udptl_port) {
 		/* Stream is not declined/disabled */
-		stream_state = sdp_state_get_stream(sdp_state, stream_index);
-
 		snprintf(tmp, sizeof(tmp), "%u", stream_state->t38_local_params.version);
 		a_line = ast_sdp_a_alloc("T38FaxVersion", tmp);
 		if (!a_line || ast_sdp_m_add_a(m_line, a_line)) {
@@ -1773,7 +3229,6 @@
 	}
 
 	stream_count = ast_stream_topology_get_count(topology);
-
 	for (stream_num = 0; stream_num < stream_count; stream_num++) {
 		switch (ast_stream_get_type(ast_stream_topology_get_stream(topology, stream_num))) {
 		case AST_MEDIA_TYPE_AUDIO:
@@ -1798,7 +3253,7 @@
 
 error:
 	if (sdp) {
-		ast_sdp_free(sdp);
+		ao2_ref(sdp, -1);
 	} else {
 		ast_sdp_t_free(t_line);
 		ast_sdp_s_free(s_line);
diff --git a/main/stream.c b/main/stream.c
index 20179f3..b6d3911 100644
--- a/main/stream.c
+++ b/main/stream.c
@@ -214,6 +214,23 @@
 	}
 }
 
+enum ast_stream_state ast_stream_str2state(const char *str)
+{
+	if (!strcmp("sendrecv", str)) {
+		return AST_STREAM_STATE_SENDRECV;
+	}
+	if (!strcmp("sendonly", str)) {
+		return AST_STREAM_STATE_SENDONLY;
+	}
+	if (!strcmp("recvonly", str)) {
+		return AST_STREAM_STATE_RECVONLY;
+	}
+	if (!strcmp("inactive", str)) {
+		return AST_STREAM_STATE_INACTIVE;
+	}
+	return AST_STREAM_STATE_REMOVED;
+}
+
 void *ast_stream_get_data(struct ast_stream *stream, enum ast_stream_data_slot slot)
 {
 	ast_assert(stream != NULL);
diff --git a/res/res_sdp_translator_pjmedia.c b/res/res_sdp_translator_pjmedia.c
index 85f246e..d80f3d5 100644
--- a/res/res_sdp_translator_pjmedia.c
+++ b/res/res_sdp_translator_pjmedia.c
@@ -484,7 +484,7 @@
 	}
 
 cleanup:
-	ast_sdp_free(sdp);
+	ao2_cleanup(sdp);
 	ast_sdp_translator_free(translator);
 	pj_pool_release(pool);
 	return res;
@@ -560,7 +560,7 @@
 	}
 
 cleanup:
-	ast_sdp_free(sdp);
+	ao2_cleanup(sdp);
 	ast_sdp_translator_free(translator);
 	pj_pool_release(pool);
 	return res;
diff --git a/tests/test_sdp.c b/tests/test_sdp.c
index 7eef3f7..662e2aa 100644
--- a/tests/test_sdp.c
+++ b/tests/test_sdp.c
@@ -89,12 +89,31 @@
 	}
 
 	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));
+		ast_test_status_update(test, "Expected %s m-line payload count %d but got %d\n",
+			media_type, num_payloads, ast_sdp_m_get_payload_count(m_line));
 		return -1;
 	}
 
-	ast_test_status_update(test, "SDP m-line is as expected\n");
+	ast_test_status_update(test, "SDP %s m-line is as expected\n", media_type);
+	return 0;
+}
+
+static int validate_m_line_declined(struct ast_test *test,
+	const struct ast_sdp_m_line *m_line, const char *media_type)
+{
+	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 (m_line->port != 0) {
+		ast_test_status_update(test, "Expected %s m-line to be declined but got port %u\n",
+			media_type, m_line->port);
+		return -1;
+	}
+
+	ast_test_status_update(test, "SDP %s m-line is as expected\n", media_type);
 	return 0;
 }
 
@@ -438,6 +457,26 @@
 	const char *formats;
 };
 
+static int build_sdp_option_formats(struct ast_sdp_options *options, int num_streams, const struct sdp_format *formats)
+{
+	int idx;
+
+	for (idx = 0; idx < num_streams; ++idx) {
+		RAII_VAR(struct ast_format_cap *, caps, NULL, ao2_cleanup);
+
+		caps = ast_format_cap_alloc(AST_FORMAT_CAP_FLAG_DEFAULT);
+		if (!caps) {
+			return -1;
+		}
+
+		if (ast_format_cap_update_by_allow_disallow(caps, formats[idx].formats, 1) < 0) {
+			return -1;
+		}
+		ast_sdp_options_set_format_cap_type(options, formats[idx].type, caps);
+	}
+	return 0;
+}
+
 /*!
  * \brief Common method to build an SDP state for a test.
  *
@@ -450,9 +489,16 @@
  *
  * \param num_streams The number of elements in the formats array.
  * \param formats Array of media types and formats that will be in the state.
+ * \param opt_num_streams The number of new stream types allowed to create.
+ *           Not used if test_options provided.
+ * \param opt_formats Array of new stream media types and formats allowed to create.
+ *           NULL if use a default stream creation.
+ *           Not used if test_options provided.
  * \param test_options Optional SDP options.
  */
-static struct ast_sdp_state *build_sdp_state(int num_streams, const struct sdp_format *formats, struct ast_sdp_options *test_options)
+static struct ast_sdp_state *build_sdp_state(int num_streams, const struct sdp_format *formats,
+	int opt_num_streams, const struct sdp_format *opt_formats,
+	struct ast_sdp_options *test_options)
 {
 	struct ast_stream_topology *topology = NULL;
 	struct ast_sdp_state *state = NULL;
@@ -460,8 +506,32 @@
 	int i;
 
 	if (!test_options) {
+		unsigned int max_streams;
+
+		static const struct sdp_format sdp_formats[] = {
+			{ AST_MEDIA_TYPE_AUDIO, "ulaw" },
+			{ AST_MEDIA_TYPE_VIDEO, "vp8" },
+			{ AST_MEDIA_TYPE_IMAGE, "t38" },
+		};
+
 		options = sdp_options_common();
 		if (!options) {
+			goto end;
+		}
+
+		/* Determine max_streams to allow */
+		max_streams = ARRAY_LEN(sdp_formats);
+		if (ARRAY_LEN(sdp_formats) < num_streams) {
+			max_streams = num_streams;
+		}
+		ast_sdp_options_set_max_streams(options, max_streams);
+
+		/* Determine new stream formats and types allowed */
+		if (!opt_formats) {
+			opt_num_streams = ARRAY_LEN(sdp_formats);
+			opt_formats = sdp_formats;
+		}
+		if (build_sdp_option_formats(options, opt_num_streams, opt_formats)) {
 			goto end;
 		}
 	} else {
@@ -489,7 +559,10 @@
 			goto end;
 		}
 		ast_stream_set_formats(stream, caps);
-		ast_stream_topology_append_stream(topology, stream);
+		if (ast_stream_topology_append_stream(topology, stream) < 0) {
+			ast_stream_free(stream);
+			goto end;
+		}
 	}
 
 	state = ast_sdp_state_alloc(topology, options);
@@ -530,7 +603,8 @@
 		break;
 	}
 
-	sdp_state = build_sdp_state(ARRAY_LEN(formats), formats, NULL);
+	sdp_state = build_sdp_state(ARRAY_LEN(formats), formats,
+		ARRAY_LEN(formats), formats, NULL);
 	if (!sdp_state) {
 		goto end;
 	}
@@ -674,7 +748,8 @@
 		break;
 	}
 
-	sdp_state = build_sdp_state(ARRAY_LEN(sdp_formats), sdp_formats, NULL);
+	sdp_state = build_sdp_state(ARRAY_LEN(sdp_formats), sdp_formats,
+		ARRAY_LEN(sdp_formats), sdp_formats, NULL);
 	if (!sdp_state) {
 		res = AST_TEST_FAIL;
 		goto end;
@@ -723,7 +798,7 @@
 	return res;
 }
 
-static int validate_merged_sdp(struct ast_test *test, const struct ast_sdp *sdp)
+static int validate_avi_sdp_streams(struct ast_test *test, const struct ast_sdp *sdp)
 {
 	struct ast_sdp_m_line *m_line;
 
@@ -769,7 +844,11 @@
 	return 0;
 }
 
-AST_TEST_DEFINE(sdp_merge_symmetric)
+static enum ast_test_result_state sdp_negotiation_completed_tests(struct ast_test *test,
+	int offer_num_streams, const struct sdp_format *offer_formats,
+	int answer_num_streams, const struct sdp_format *answer_formats,
+	int allowed_ans_num_streams, const struct sdp_format *allowed_ans_formats,
+	int (*validate_sdp)(struct ast_test *test, const struct ast_sdp *sdp))
 {
 	enum ast_test_result_state res = AST_TEST_PASS;
 	struct ast_sdp_state *sdp_state_offerer = NULL;
@@ -777,38 +856,15 @@
 	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" },
-		{ AST_MEDIA_TYPE_IMAGE, "t38" },
-	};
-	static const struct sdp_format answerer_formats[] = {
-		{ AST_MEDIA_TYPE_AUDIO, "ulaw" },
-		{ AST_MEDIA_TYPE_VIDEO, "vp8" },
-		{ AST_MEDIA_TYPE_IMAGE, "t38" },
-	};
-
-	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, NULL);
+	sdp_state_offerer = build_sdp_state(offer_num_streams, offer_formats,
+		offer_num_streams, offer_formats, NULL);
 	if (!sdp_state_offerer) {
 		res = AST_TEST_FAIL;
 		goto end;
 	}
 
-	sdp_state_answerer = build_sdp_state(ARRAY_LEN(answerer_formats), answerer_formats, NULL);
+	sdp_state_answerer = build_sdp_state(answer_num_streams, answer_formats,
+		allowed_ans_num_streams, allowed_ans_formats, NULL);
 	if (!sdp_state_answerer) {
 		res = AST_TEST_FAIL;
 		goto end;
@@ -820,22 +876,37 @@
 		goto end;
 	}
 
-	ast_sdp_state_set_remote_sdp(sdp_state_answerer, offerer_sdp);
+	if (ast_sdp_state_set_remote_sdp(sdp_state_answerer, offerer_sdp)) {
+		res = AST_TEST_FAIL;
+		goto end;
+	}
 	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)) {
+	if (ast_sdp_state_set_remote_sdp(sdp_state_offerer, answerer_sdp)) {
 		res = AST_TEST_FAIL;
 		goto end;
 	}
-	if (validate_merged_sdp(test, answerer_sdp)) {
+
+	/*
+	 * Restart SDP negotiations to build the joint SDP on the offerer
+	 * side.  Otherwise we will get the original offer for use in
+	 * case of retransmissions.
+	 */
+	if (ast_sdp_state_restart_negotiations(sdp_state_offerer)) {
+		ast_test_status_update(test, "Restarting negotiations failed\n");
+		res = AST_TEST_FAIL;
+		goto end;
+	}
+	offerer_sdp = ast_sdp_state_get_local_sdp(sdp_state_offerer);
+	if (validate_sdp(test, offerer_sdp)) {
+		res = AST_TEST_FAIL;
+		goto end;
+	}
+	if (validate_sdp(test, answerer_sdp)) {
 		res = AST_TEST_FAIL;
 		goto end;
 	}
@@ -847,14 +918,37 @@
 	return res;
 }
 
-AST_TEST_DEFINE(sdp_merge_crisscross)
+AST_TEST_DEFINE(sdp_negotiation_initial)
 {
-	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" },
+		{ AST_MEDIA_TYPE_IMAGE, "t38" },
+	};
 
+	switch(cmd) {
+	case TEST_INIT:
+		info->name = "sdp_negotiation_initial";
+		info->category = "/main/sdp/";
+		info->summary = "Simulate an initial negotiation";
+		info->description =
+			"Initial negotiation tests creating new streams on the answering side.\n"
+			"After negotiation both offerer and answerer sides should have the same\n"
+			"expected stream types and formats.";
+		return AST_TEST_NOT_RUN;
+	case TEST_EXECUTE:
+		break;
+	}
+
+	return sdp_negotiation_completed_tests(test,
+		ARRAY_LEN(offerer_formats), offerer_formats,
+		0, NULL,
+		0, NULL,
+		validate_avi_sdp_streams);
+}
+
+AST_TEST_DEFINE(sdp_negotiation_type_change)
+{
 	static const struct sdp_format offerer_formats[] = {
 		{ AST_MEDIA_TYPE_AUDIO, "ulaw,alaw,g722,opus" },
 		{ AST_MEDIA_TYPE_VIDEO, "h264,vp8" },
@@ -868,82 +962,45 @@
 
 	switch(cmd) {
 	case TEST_INIT:
-		info->name = "sdp_merge_crisscross";
+		info->name = "sdp_negotiation_type_change";
 		info->category = "/main/sdp/";
-		info->summary = "Merge two SDPs with symmetric stream types";
+		info->summary = "Simulate a re-negotiation changing 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";
+			"Reinvite negotiation tests changing stream types on the answering side.\n"
+			"After negotiation both offerer and answerer sides should have the same\n"
+			"expected stream types and formats.";
 		return AST_TEST_NOT_RUN;
 	case TEST_EXECUTE:
 		break;
 	}
 
-	sdp_state_offerer = build_sdp_state(ARRAY_LEN(offerer_formats), offerer_formats, NULL);
-	if (!sdp_state_offerer) {
-		res = AST_TEST_FAIL;
-		goto end;
-	}
-
-	sdp_state_answerer = build_sdp_state(ARRAY_LEN(answerer_formats), answerer_formats, NULL);
-	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;
+	return sdp_negotiation_completed_tests(test,
+		ARRAY_LEN(offerer_formats), offerer_formats,
+		ARRAY_LEN(answerer_formats), answerer_formats,
+		0, NULL,
+		validate_avi_sdp_streams);
 }
 
-static int validate_merged_sdp_asymmetric(struct ast_test *test, const struct ast_sdp *sdp, int is_offer)
+static int validate_ava_declined_sdp_streams(struct ast_test *test, const struct ast_sdp *sdp)
 {
 	struct ast_sdp_m_line *m_line;
-	const char *side = is_offer ? "Offer side" : "Answer side";
 
 	if (!sdp) {
-		ast_test_status_update(test, "%s does not have a SDP\n", side);
 		return -1;
 	}
 
-	/* Stream 0 */
 	m_line = ast_sdp_get_m(sdp, 0);
-	if (validate_m_line(test, m_line, "audio", 1)) {
+	if (validate_m_line_declined(test, m_line, "audio")) {
 		return -1;
 	}
-	if (!m_line->port) {
-		ast_test_status_update(test, "%s stream %d does%s have a port\n", side, 0, "n't");
+
+	m_line = ast_sdp_get_m(sdp, 1);
+	if (validate_m_line_declined(test, m_line, "video")) {
+		return -1;
+	}
+
+	m_line = ast_sdp_get_m(sdp, 2);
+	if (validate_m_line(test, m_line, "audio", 1)) {
 		return -1;
 	}
 	if (validate_rtpmap(test, m_line, "PCMU")) {
@@ -954,61 +1011,16 @@
 	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;
-	}
-
-	/* The remaining streams should be declined */
-
-	/* Stream 1 */
-	m_line = ast_sdp_get_m(sdp, 1);
-	if (validate_m_line(test, m_line, "audio", 1)) {
-		return -1;
-	}
-	if (m_line->port) {
-		ast_test_status_update(test, "%s stream %d does%s have a port\n", side, 1, "");
-		return -1;
-	}
-
-	/* Stream 2 */
-	m_line = ast_sdp_get_m(sdp, 2);
-	if (validate_m_line(test, m_line, "video", 1)) {
-		return -1;
-	}
-	if (m_line->port) {
-		ast_test_status_update(test, "%s stream %d does%s have a port\n", side, 2, "");
-		return -1;
-	}
-
-	/* Stream 3 */
-	m_line = ast_sdp_get_m(sdp, 3);
-	if (validate_m_line(test, m_line, "image", 1)) {
-		return -1;
-	}
-	if (m_line->port) {
-		ast_test_status_update(test, "%s stream %d does%s have a port\n", side, 3, "");
-		return -1;
-	}
 
 	return 0;
 }
 
-AST_TEST_DEFINE(sdp_merge_asymmetric)
+AST_TEST_DEFINE(sdp_negotiation_decline_incompatible)
 {
-	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_AUDIO, "ulaw" },
-		{ AST_MEDIA_TYPE_VIDEO, "h261" },
-		{ AST_MEDIA_TYPE_IMAGE, "t38" },
+		{ AST_MEDIA_TYPE_AUDIO, "alaw" },
+		{ AST_MEDIA_TYPE_VIDEO, "vp8" },
+		{ AST_MEDIA_TYPE_AUDIO, "ulaw,alaw" },
 	};
 	static const struct sdp_format answerer_formats[] = {
 		{ AST_MEDIA_TYPE_AUDIO, "ulaw" },
@@ -1016,26 +1028,128 @@
 
 	switch(cmd) {
 	case TEST_INIT:
-		info->name = "sdp_merge_asymmetric";
+		info->name = "sdp_negotiation_decline_incompatible";
 		info->category = "/main/sdp/";
-		info->summary = "Merge two SDPs with an asymmetric number of streams";
+		info->summary = "Simulate an initial negotiation declining streams";
 		info->description =
-			"SDP 1 offers a four stream topology: Audio,Audio,Video,T.38\n"
-			"SDP 2 only has a single audio stream topology\n"
-			"We ensure that both local SDPs have the expected stream types and\n"
-			"the expected declined streams";
+			"Initial negotiation tests declining incompatible streams on the answering side.\n"
+			"After negotiation both offerer and answerer sides should have the same\n"
+			"expected stream types and formats.";
 		return AST_TEST_NOT_RUN;
 	case TEST_EXECUTE:
 		break;
 	}
 
-	sdp_state_offerer = build_sdp_state(ARRAY_LEN(offerer_formats), offerer_formats, NULL);
+	return sdp_negotiation_completed_tests(test,
+		ARRAY_LEN(offerer_formats), offerer_formats,
+		ARRAY_LEN(answerer_formats), answerer_formats,
+		ARRAY_LEN(answerer_formats), answerer_formats,
+		validate_ava_declined_sdp_streams);
+}
+
+static int validate_aaaa_declined_sdp_streams(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;
+	}
+
+	m_line = ast_sdp_get_m(sdp, 1);
+	if (validate_m_line(test, m_line, "audio", 1)) {
+		return -1;
+	}
+	if (validate_rtpmap(test, m_line, "PCMU")) {
+		return -1;
+	}
+
+	m_line = ast_sdp_get_m(sdp, 2);
+	if (validate_m_line(test, m_line, "audio", 1)) {
+		return -1;
+	}
+	if (validate_rtpmap(test, m_line, "PCMU")) {
+		return -1;
+	}
+
+	m_line = ast_sdp_get_m(sdp, 3);
+	if (validate_m_line_declined(test, m_line, "audio")) {
+		return -1;
+	}
+
+	return 0;
+}
+
+AST_TEST_DEFINE(sdp_negotiation_decline_max_streams)
+{
+	static const struct sdp_format offerer_formats[] = {
+		{ AST_MEDIA_TYPE_AUDIO, "ulaw" },
+		{ AST_MEDIA_TYPE_AUDIO, "ulaw" },
+		{ AST_MEDIA_TYPE_AUDIO, "ulaw" },
+		{ AST_MEDIA_TYPE_AUDIO, "ulaw" },
+	};
+
+	switch(cmd) {
+	case TEST_INIT:
+		info->name = "sdp_negotiation_decline_max_streams";
+		info->category = "/main/sdp/";
+		info->summary = "Simulate an initial negotiation declining excessive streams";
+		info->description =
+			"Initial negotiation tests declining too many streams on the answering side.\n"
+			"After negotiation both offerer and answerer sides should have the same\n"
+			"expected stream types and formats.";
+		return AST_TEST_NOT_RUN;
+	case TEST_EXECUTE:
+		break;
+	}
+
+	return sdp_negotiation_completed_tests(test,
+		ARRAY_LEN(offerer_formats), offerer_formats,
+		0, NULL,
+		0, NULL,
+		validate_aaaa_declined_sdp_streams);
+}
+
+AST_TEST_DEFINE(sdp_negotiation_not_acceptable)
+{
+	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;
+
+	static const struct sdp_format offerer_formats[] = {
+		{ AST_MEDIA_TYPE_AUDIO, "alaw" },
+		{ AST_MEDIA_TYPE_AUDIO, "alaw" },
+	};
+
+	switch(cmd) {
+	case TEST_INIT:
+		info->name = "sdp_negotiation_not_acceptable";
+		info->category = "/main/sdp/";
+		info->summary = "Simulate an initial negotiation declining all streams";
+		info->description =
+			"Initial negotiation tests declining all streams for a 488 on the answering side.\n"
+			"Negotiations should fail because there are no acceptable streams.";
+		return AST_TEST_NOT_RUN;
+	case TEST_EXECUTE:
+		break;
+	}
+
+	sdp_state_offerer = build_sdp_state(ARRAY_LEN(offerer_formats), offerer_formats,
+		ARRAY_LEN(offerer_formats), offerer_formats, NULL);
 	if (!sdp_state_offerer) {
 		res = AST_TEST_FAIL;
 		goto end;
 	}
 
-	sdp_state_answerer = build_sdp_state(ARRAY_LEN(answerer_formats), answerer_formats, NULL);
+	sdp_state_answerer = build_sdp_state(0, NULL, 0, NULL, NULL);
 	if (!sdp_state_answerer) {
 		res = AST_TEST_FAIL;
 		goto end;
@@ -1047,24 +1161,15 @@
 		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) {
+	if (!ast_sdp_state_set_remote_sdp(sdp_state_answerer, offerer_sdp)) {
+		ast_test_status_update(test, "Bad.  Setting remote SDP was successful.\n");
 		res = AST_TEST_FAIL;
 		goto end;
 	}
-
-	ast_sdp_state_set_remote_sdp(sdp_state_offerer, answerer_sdp);
-
-#if defined(XXX_TODO_NEED_TO_HANDLE_DECLINED_STREAMS_ON_OFFER_SIDE)
-	/* 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_asymmetric(test, offerer_sdp, 1)) {
+	if (!ast_sdp_state_is_offer_rejected(sdp_state_answerer)) {
+		ast_test_status_update(test, "Bad.  Negotiation failed for some other reason.\n");
 		res = AST_TEST_FAIL;
-	}
-#endif
-	if (validate_merged_sdp_asymmetric(test, answerer_sdp, 0)) {
-		res = AST_TEST_FAIL;
+		goto end;
 	}
 
 end:
@@ -1135,9 +1240,12 @@
 		ast_test_status_update(test, "Failed to allocate SDP options\n");
 		goto end;
 	}
+	if (build_sdp_option_formats(options, ARRAY_LEN(formats), formats)) {
+		goto end;
+	}
 	ast_sdp_options_set_ssrc(options, 1);
 
-	test_state = build_sdp_state(ARRAY_LEN(formats), formats, options);
+	test_state = build_sdp_state(ARRAY_LEN(formats), formats, 0, NULL, options);
 	if (!test_state) {
 		ast_test_status_update(test, "Failed to create SDP state\n");
 		goto end;
@@ -1179,6 +1287,726 @@
 	return res;
 }
 
+struct sdp_topology_stream {
+	/*! Media stream type: audio, video, image */
+	enum ast_media_type type;
+	/*! Media stream state: removed/declined, sendrecv */
+	enum ast_stream_state state;
+	/*! Comma separated list of formats allowed on the stream.  Can be NULL if stream is removed/declined. */
+	const char *formats;
+	/*! Optional name of stream.  NULL for default name. */
+	const char *name;
+};
+
+struct sdp_update_test {
+	/*! Maximum number of streams.  (0 if default) */
+	int max_streams;
+	/*! Optional initial SDP state topology (NULL if not present) */
+	const struct sdp_topology_stream * const *initial;
+	/*! Required first topology update */
+	const struct sdp_topology_stream * const *update_1;
+	/*! Optional second topology update (NULL if not present) */
+	const struct sdp_topology_stream * const *update_2;
+	/*! Expected topology to be offered */
+	const struct sdp_topology_stream * const *expected;
+};
+
+static struct ast_stream_topology *build_update_topology(const struct sdp_topology_stream * const *spec)
+{
+	struct ast_stream_topology *topology;
+	const struct sdp_topology_stream *desc;
+
+	topology = ast_stream_topology_alloc();
+	if (!topology) {
+		return NULL;
+	}
+
+	for (desc = *spec; desc; ++spec, desc = *spec) {
+		struct ast_stream *stream;
+		const char *name;
+
+		name = desc->name ?: ast_codec_media_type2str(desc->type);
+		stream = ast_stream_alloc(name, desc->type);
+		if (!stream) {
+			goto fail;
+		}
+		ast_stream_set_state(stream, desc->state);
+		if (desc->formats) {
+			struct ast_format_cap *caps;
+
+			caps = ast_format_cap_alloc(AST_FORMAT_CAP_FLAG_DEFAULT);
+			if (!caps) {
+				goto fail;
+			}
+			if (ast_format_cap_update_by_allow_disallow(caps, desc->formats, 1) < 0) {
+				ao2_ref(caps, -1);
+				goto fail;
+			}
+			ast_stream_set_formats(stream, caps);
+			ao2_ref(caps, -1);
+		}
+		if (ast_stream_topology_append_stream(topology, stream) < 0) {
+			ast_stream_free(stream);
+			goto fail;
+		}
+	}
+	return topology;
+
+fail:
+	ast_stream_topology_free(topology);
+	return NULL;
+}
+
+static int cmp_update_topology(struct ast_test *test,
+	const struct ast_stream_topology *expected, const struct ast_stream_topology *merged)
+{
+	int status = 0;
+	int idx;
+	int max_streams;
+	struct ast_stream *exp_stream;
+	struct ast_stream *mrg_stream;
+
+	idx = ast_stream_topology_get_count(expected);
+	max_streams = ast_stream_topology_get_count(merged);
+	if (idx != max_streams) {
+		ast_test_status_update(test, "Expected %d streams got %d streams\n",
+			idx, max_streams);
+		status = -1;
+	}
+	if (idx < max_streams) {
+		max_streams = idx;
+	}
+
+	/* Compare common streams by position */
+	for (idx = 0; idx < max_streams; ++idx) {
+		exp_stream = ast_stream_topology_get_stream(expected, idx);
+		mrg_stream = ast_stream_topology_get_stream(merged, idx);
+
+		if (strcmp(ast_stream_get_name(exp_stream), ast_stream_get_name(mrg_stream))) {
+			ast_test_status_update(test,
+				"Stream %d: Expected stream name '%s' got stream name '%s'\n",
+				idx,
+				ast_stream_get_name(exp_stream),
+				ast_stream_get_name(mrg_stream));
+			status = -1;
+		}
+
+		if (ast_stream_get_state(exp_stream) != ast_stream_get_state(mrg_stream)) {
+			ast_test_status_update(test,
+				"Stream %d: Expected stream state '%s' got stream state '%s'\n",
+				idx,
+				ast_stream_state2str(ast_stream_get_state(exp_stream)),
+				ast_stream_state2str(ast_stream_get_state(mrg_stream)));
+			status = -1;
+		}
+
+		if (ast_stream_get_type(exp_stream) != ast_stream_get_type(mrg_stream)) {
+			ast_test_status_update(test,
+				"Stream %d: Expected stream type '%s' got stream type '%s'\n",
+				idx,
+				ast_codec_media_type2str(ast_stream_get_type(exp_stream)),
+				ast_codec_media_type2str(ast_stream_get_type(mrg_stream)));
+			status = -1;
+			continue;
+		}
+
+		if (ast_stream_get_state(exp_stream) == AST_STREAM_STATE_REMOVED
+			|| ast_stream_get_state(mrg_stream) == AST_STREAM_STATE_REMOVED) {
+			/*
+			 * Cannot compare formats if one of the streams is
+			 * declined because there may not be any on the declined
+			 * stream.
+			 */
+			continue;
+		}
+		if (!ast_format_cap_identical(ast_stream_get_formats(exp_stream),
+			ast_stream_get_formats(mrg_stream))) {
+			ast_test_status_update(test,
+				"Stream %d: Expected formats do not match merged formats\n",
+				idx);
+			status = -1;
+		}
+	}
+
+	return status;
+}
+
+
+static const struct sdp_topology_stream audio_declined_no_name = {
+	AST_MEDIA_TYPE_AUDIO, AST_STREAM_STATE_REMOVED, NULL, NULL
+};
+
+static const struct sdp_topology_stream audio_ulaw_no_name = {
+	AST_MEDIA_TYPE_AUDIO, AST_STREAM_STATE_SENDRECV, "ulaw", NULL
+};
+
+static const struct sdp_topology_stream audio_alaw_no_name = {
+	AST_MEDIA_TYPE_AUDIO, AST_STREAM_STATE_SENDRECV, "alaw", NULL
+};
+
+static const struct sdp_topology_stream audio_g722_no_name = {
+	AST_MEDIA_TYPE_AUDIO, AST_STREAM_STATE_SENDRECV, "g722", NULL
+};
+
+static const struct sdp_topology_stream audio_g723_no_name = {
+	AST_MEDIA_TYPE_AUDIO, AST_STREAM_STATE_SENDRECV, "g723", NULL
+};
+
+static const struct sdp_topology_stream video_declined_no_name = {
+	AST_MEDIA_TYPE_VIDEO, AST_STREAM_STATE_REMOVED, NULL, NULL
+};
+
+static const struct sdp_topology_stream video_h261_no_name = {
+	AST_MEDIA_TYPE_VIDEO, AST_STREAM_STATE_SENDRECV, "h261", NULL
+};
+
+static const struct sdp_topology_stream video_h263_no_name = {
+	AST_MEDIA_TYPE_VIDEO, AST_STREAM_STATE_SENDRECV, "h263", NULL
+};
+
+static const struct sdp_topology_stream video_h264_no_name = {
+	AST_MEDIA_TYPE_VIDEO, AST_STREAM_STATE_SENDRECV, "h264", NULL
+};
+
+static const struct sdp_topology_stream video_vp8_no_name = {
+	AST_MEDIA_TYPE_VIDEO, AST_STREAM_STATE_SENDRECV, "vp8", NULL
+};
+
+static const struct sdp_topology_stream image_declined_no_name = {
+	AST_MEDIA_TYPE_IMAGE, AST_STREAM_STATE_REMOVED, NULL, NULL
+};
+
+static const struct sdp_topology_stream image_t38_no_name = {
+	AST_MEDIA_TYPE_IMAGE, AST_STREAM_STATE_SENDRECV, "t38", NULL
+};
+
+
+static const struct sdp_topology_stream *top_ulaw_alaw_h264__vp8[] = {
+	&audio_ulaw_no_name,
+	&audio_alaw_no_name,
+	&video_h264_no_name,
+	&video_vp8_no_name,
+	NULL
+};
+
+static const struct sdp_topology_stream *top__vp8_alaw_h264_ulaw[] = {
+	&video_vp8_no_name,
+	&audio_alaw_no_name,
+	&video_h264_no_name,
+	&audio_ulaw_no_name,
+	NULL
+};
+
+static const struct sdp_topology_stream *top_alaw_ulaw__vp8_h264[] = {
+	&audio_alaw_no_name,
+	&audio_ulaw_no_name,
+	&video_vp8_no_name,
+	&video_h264_no_name,
+	NULL
+};
+
+/* Sorting by type with no new or deleted streams */
+static const struct sdp_update_test mrg_by_type_00 = {
+	.initial  = top_ulaw_alaw_h264__vp8,
+	.update_1 = top__vp8_alaw_h264_ulaw,
+	.expected = top_alaw_ulaw__vp8_h264,
+};
+
+
+static const struct sdp_topology_stream *top_alaw__vp8[] = {
+	&audio_alaw_no_name,
+	&video_vp8_no_name,
+	NULL
+};
+
+static const struct sdp_topology_stream *top_h264__vp8_ulaw[] = {
+	&video_h264_no_name,
+	&video_vp8_no_name,
+	&audio_ulaw_no_name,
+	NULL
+};
+
+static const struct sdp_topology_stream *top_ulaw_h264__vp8[] = {
+	&audio_ulaw_no_name,
+	&video_h264_no_name,
+	&video_vp8_no_name,
+	NULL
+};
+
+/* Sorting by type and adding a stream */
+static const struct sdp_update_test mrg_by_type_01 = {
+	.initial  = top_alaw__vp8,
+	.update_1 = top_h264__vp8_ulaw,
+	.expected = top_ulaw_h264__vp8,
+};
+
+
+static const struct sdp_topology_stream *top_alaw__vp8_vdec[] = {
+	&audio_alaw_no_name,
+	&video_vp8_no_name,
+	&video_declined_no_name,
+	NULL
+};
+
+/* Sorting by type and deleting a stream */
+static const struct sdp_update_test mrg_by_type_02 = {
+	.initial  = top_ulaw_h264__vp8,
+	.update_1 = top_alaw__vp8,
+	.expected = top_alaw__vp8_vdec,
+};
+
+
+static const struct sdp_topology_stream *top_h264_alaw_ulaw[] = {
+	&video_h264_no_name,
+	&audio_alaw_no_name,
+	&audio_ulaw_no_name,
+	NULL
+};
+
+static const struct sdp_topology_stream *top__t38[] = {
+	&image_t38_no_name,
+	NULL
+};
+
+static const struct sdp_topology_stream *top_vdec__t38_adec[] = {
+	&video_declined_no_name,
+	&image_t38_no_name,
+	&audio_declined_no_name,
+	NULL
+};
+
+/* Sorting by type changing stream types for T.38 */
+static const struct sdp_update_test mrg_by_type_03 = {
+	.initial  = top_h264_alaw_ulaw,
+	.update_1 = top__t38,
+	.expected = top_vdec__t38_adec,
+};
+
+
+/* Sorting by type changing stream types back from T.38 */
+static const struct sdp_update_test mrg_by_type_04 = {
+	.initial  = top_vdec__t38_adec,
+	.update_1 = top_h264_alaw_ulaw,
+	.expected = top_h264_alaw_ulaw,
+};
+
+
+static const struct sdp_topology_stream *top_h264[] = {
+	&video_h264_no_name,
+	NULL
+};
+
+static const struct sdp_topology_stream *top_vdec__t38[] = {
+	&video_declined_no_name,
+	&image_t38_no_name,
+	NULL
+};
+
+/* Sorting by type changing stream types for T.38 */
+static const struct sdp_update_test mrg_by_type_05 = {
+	.initial  = top_h264,
+	.update_1 = top__t38,
+	.expected = top_vdec__t38,
+};
+
+
+static const struct sdp_topology_stream *top_h264_idec[] = {
+	&video_h264_no_name,
+	&image_declined_no_name,
+	NULL
+};
+
+/* Sorting by type changing stream types back from T.38 */
+static const struct sdp_update_test mrg_by_type_06 = {
+	.initial  = top_vdec__t38,
+	.update_1 = top_h264,
+	.expected = top_h264_idec,
+};
+
+
+static const struct sdp_topology_stream *top_ulaw_adec_h264__vp8[] = {
+	&audio_ulaw_no_name,
+	&audio_declined_no_name,
+	&video_h264_no_name,
+	&video_vp8_no_name,
+	NULL
+};
+
+static const struct sdp_topology_stream *top_h263_alaw_h261_h264_vp8[] = {
+	&video_h263_no_name,
+	&audio_alaw_no_name,
+	&video_h261_no_name,
+	&video_h264_no_name,
+	&video_vp8_no_name,
+	NULL
+};
+
+static const struct sdp_topology_stream *top_alaw_h264_h263_h261_vp8[] = {
+	&audio_alaw_no_name,
+	&video_h264_no_name,
+	&video_h263_no_name,
+	&video_h261_no_name,
+	&video_vp8_no_name,
+	NULL
+};
+
+/* Sorting by type with backfill and adding streams */
+static const struct sdp_update_test mrg_by_type_07 = {
+	.initial  = top_ulaw_adec_h264__vp8,
+	.update_1 = top_h263_alaw_h261_h264_vp8,
+	.expected = top_alaw_h264_h263_h261_vp8,
+};
+
+
+static const struct sdp_topology_stream *top_ulaw_alaw_h264__vp8_h261[] = {
+	&audio_ulaw_no_name,
+	&audio_alaw_no_name,
+	&video_h264_no_name,
+	&video_vp8_no_name,
+	&video_h261_no_name,
+	NULL
+};
+
+/* Sorting by type overlimit of 4 and drop */
+static const struct sdp_update_test mrg_by_type_08 = {
+	.max_streams = 4,
+	.initial  = top_ulaw_alaw_h264__vp8,
+	.update_1 = top_ulaw_alaw_h264__vp8_h261,
+	.expected = top_ulaw_alaw_h264__vp8,
+};
+
+
+static const struct sdp_topology_stream *top_ulaw_alaw_h264[] = {
+	&audio_ulaw_no_name,
+	&audio_alaw_no_name,
+	&video_h264_no_name,
+	NULL
+};
+
+static const struct sdp_topology_stream *top_alaw_h261__vp8[] = {
+	&audio_alaw_no_name,
+	&video_h261_no_name,
+	&video_vp8_no_name,
+	NULL
+};
+
+static const struct sdp_topology_stream *top_alaw_adec_h261__vp8[] = {
+	&audio_alaw_no_name,
+	&audio_declined_no_name,
+	&video_h261_no_name,
+	&video_vp8_no_name,
+	NULL
+};
+
+/* Sorting by type with delete and add of streams */
+static const struct sdp_update_test mrg_by_type_09 = {
+	.initial  = top_ulaw_alaw_h264,
+	.update_1 = top_alaw_h261__vp8,
+	.expected = top_alaw_adec_h261__vp8,
+};
+
+
+static const struct sdp_topology_stream *top_ulaw_adec_h264[] = {
+	&audio_ulaw_no_name,
+	&audio_declined_no_name,
+	&video_h264_no_name,
+	NULL
+};
+
+/* Sorting by type and adding streams */
+static const struct sdp_update_test mrg_by_type_10 = {
+	.initial  = top_ulaw_adec_h264,
+	.update_1 = top_alaw_ulaw__vp8_h264,
+	.expected = top_alaw_ulaw__vp8_h264,
+};
+
+
+static const struct sdp_topology_stream *top_adec_g722_h261[] = {
+	&audio_declined_no_name,
+	&audio_g722_no_name,
+	&video_h261_no_name,
+	NULL
+};
+
+/* Sorting by type and deleting old streams */
+static const struct sdp_update_test mrg_by_type_11 = {
+	.initial  = top_ulaw_alaw_h264,
+	.update_1 = top_adec_g722_h261,
+	.expected = top_adec_g722_h261,
+};
+
+
+static const struct sdp_topology_stream audio_alaw4dave = {
+	AST_MEDIA_TYPE_AUDIO, AST_STREAM_STATE_SENDRECV, "alaw", "dave"
+};
+
+static const struct sdp_topology_stream audio_g7224dave = {
+	AST_MEDIA_TYPE_AUDIO, AST_STREAM_STATE_SENDRECV, "g722", "dave"
+};
+
+static const struct sdp_topology_stream audio_ulaw4fred = {
+	AST_MEDIA_TYPE_AUDIO, AST_STREAM_STATE_SENDRECV, "ulaw", "fred"
+};
+
+static const struct sdp_topology_stream audio_alaw4fred = {
+	AST_MEDIA_TYPE_AUDIO, AST_STREAM_STATE_SENDRECV, "alaw", "fred"
+};
+
+static const struct sdp_topology_stream audio_ulaw4rose = {
+	AST_MEDIA_TYPE_AUDIO, AST_STREAM_STATE_SENDRECV, "ulaw", "rose"
+};
+
+static const struct sdp_topology_stream audio_g7224rose = {
+	AST_MEDIA_TYPE_AUDIO, AST_STREAM_STATE_SENDRECV, "g722", "rose"
+};
+
+
+static const struct sdp_topology_stream video_h2614dave = {
+	AST_MEDIA_TYPE_VIDEO, AST_STREAM_STATE_SENDRECV, "h261", "dave"
+};
+
+static const struct sdp_topology_stream video_h2634dave = {
+	AST_MEDIA_TYPE_VIDEO, AST_STREAM_STATE_SENDRECV, "h263", "dave"
+};
+
+static const struct sdp_topology_stream video_h2634fred = {
+	AST_MEDIA_TYPE_VIDEO, AST_STREAM_STATE_SENDRECV, "h263", "fred"
+};
+
+static const struct sdp_topology_stream video_h2644fred = {
+	AST_MEDIA_TYPE_VIDEO, AST_STREAM_STATE_SENDRECV, "h264", "fred"
+};
+
+static const struct sdp_topology_stream video_h2644rose = {
+	AST_MEDIA_TYPE_VIDEO, AST_STREAM_STATE_SENDRECV, "h264", "rose"
+};
+
+static const struct sdp_topology_stream video_h2614rose = {
+	AST_MEDIA_TYPE_VIDEO, AST_STREAM_STATE_SENDRECV, "h261", "rose"
+};
+
+
+static const struct sdp_topology_stream *top_adave_alaw_afred_ulaw_arose_g722_vdave_h261_vfred_h263_vrose_h264[] = {
+	&audio_alaw4dave,
+	&audio_alaw_no_name,
+	&audio_ulaw4fred,
+	&audio_ulaw_no_name,
+	&audio_g7224rose,
+	&audio_g722_no_name,
+	&video_h2614dave,
+	&video_h261_no_name,
+	&video_h2634fred,
+	&video_h263_no_name,
+	&video_h2644rose,
+	&video_h264_no_name,
+	NULL
+};
+
+static const struct sdp_topology_stream *top_vfred_vrose_vdave_h263_h264_h261_afred_ulaw_arose_g722_adave_alaw[] = {
+	&video_h2644fred,
+	&video_h2614rose,
+	&video_h2634dave,
+	&video_h263_no_name,
+	&video_h264_no_name,
+	&video_h261_no_name,
+	&audio_alaw4fred,
+	&audio_ulaw_no_name,
+	&audio_ulaw4rose,
+	&audio_g722_no_name,
+	&audio_g7224dave,
+	&audio_alaw_no_name,
+	NULL
+};
+
+static const struct sdp_topology_stream *top_adave_ulaw_afred_g722_arose_alaw_vdave_h263_vfred_h264_vrose_h261[] = {
+	&audio_g7224dave,
+	&audio_ulaw_no_name,
+	&audio_alaw4fred,
+	&audio_g722_no_name,
+	&audio_ulaw4rose,
+	&audio_alaw_no_name,
+	&video_h2634dave,
+	&video_h263_no_name,
+	&video_h2644fred,
+	&video_h264_no_name,
+	&video_h2614rose,
+	&video_h261_no_name,
+	NULL
+};
+
+/* Sorting by name and type with no new or deleted streams */
+static const struct sdp_update_test mrg_by_name_00 = {
+	.initial  = top_adave_alaw_afred_ulaw_arose_g722_vdave_h261_vfred_h263_vrose_h264,
+	.update_1 = top_vfred_vrose_vdave_h263_h264_h261_afred_ulaw_arose_g722_adave_alaw,
+	.expected = top_adave_ulaw_afred_g722_arose_alaw_vdave_h263_vfred_h264_vrose_h261,
+};
+
+
+static const struct sdp_topology_stream *top_adave_g723_h261[] = {
+	&audio_g7224dave,
+	&audio_g723_no_name,
+	&video_h261_no_name,
+	NULL
+};
+
+/* Sorting by name and type adding names to streams */
+static const struct sdp_update_test mrg_by_name_01 = {
+	.initial  = top_ulaw_alaw_h264,
+	.update_1 = top_adave_g723_h261,
+	.expected = top_adave_g723_h261,
+};
+
+
+/* Sorting by name and type removing names from streams */
+static const struct sdp_update_test mrg_by_name_02 = {
+	.initial  = top_adave_g723_h261,
+	.update_1 = top_ulaw_alaw_h264,
+	.expected = top_ulaw_alaw_h264,
+};
+
+
+static const struct sdp_update_test *sdp_update_cases[] = {
+	/* Merging by type */
+	/* 00 */ &mrg_by_type_00,
+	/* 01 */ &mrg_by_type_01,
+	/* 02 */ &mrg_by_type_02,
+	/* 03 */ &mrg_by_type_03,
+	/* 04 */ &mrg_by_type_04,
+	/* 05 */ &mrg_by_type_05,
+	/* 06 */ &mrg_by_type_06,
+	/* 07 */ &mrg_by_type_07,
+	/* 08 */ &mrg_by_type_08,
+	/* 09 */ &mrg_by_type_09,
+	/* 10 */ &mrg_by_type_10,
+	/* 11 */ &mrg_by_type_11,
+
+	/* Merging by name and type */
+	/* 12 */ &mrg_by_name_00,
+	/* 13 */ &mrg_by_name_01,
+	/* 14 */ &mrg_by_name_02,
+};
+
+AST_TEST_DEFINE(sdp_update_topology)
+{
+	enum ast_test_result_state res;
+	unsigned int idx;
+	int status;
+	struct ast_sdp_options *options;
+	struct ast_stream_topology *topology;
+	struct ast_sdp_state *test_state = NULL;
+
+	static const struct sdp_format sdp_formats[] = {
+		{ AST_MEDIA_TYPE_AUDIO, "ulaw,alaw,g722,g723" },
+		{ AST_MEDIA_TYPE_VIDEO, "h261,h263,h264,vp8" },
+		{ AST_MEDIA_TYPE_IMAGE, "t38" },
+	};
+
+	switch(cmd) {
+	case TEST_INIT:
+		info->name = "sdp_update_topology";
+		info->category = "/main/sdp/";
+		info->summary = "Merge topology updates from the system";
+		info->description =
+			"1) Create a SDP state with an optional initial topology.\n"
+			"2) Update the initial topology with one or two new topologies.\n"
+			"3) Get the SDP offer to merge the updates into the initial topology.\n"
+			"4) Check that the offered topology matches the expected topology.\n"
+			"5) Repeat these steps for each test case defined.";
+		return AST_TEST_NOT_RUN;
+	case TEST_EXECUTE:
+		break;
+	}
+
+	res = AST_TEST_FAIL;
+	for (idx = 0; idx < ARRAY_LEN(sdp_update_cases); ++idx) {
+		ast_test_status_update(test, "Starting update case %d\n", idx);
+
+		/* Create a SDP state with an optional initial topology. */
+		options = sdp_options_common();
+		if (!options) {
+			ast_test_status_update(test, "Failed to allocate SDP options\n");
+			goto end;
+		}
+		if (sdp_update_cases[idx]->max_streams) {
+			ast_sdp_options_set_max_streams(options, sdp_update_cases[idx]->max_streams);
+		}
+		if (build_sdp_option_formats(options, ARRAY_LEN(sdp_formats), sdp_formats)) {
+			ast_test_status_update(test, "Failed to setup SDP options new stream formats\n");
+			goto end;
+		}
+		if (sdp_update_cases[idx]->initial) {
+			topology = build_update_topology(sdp_update_cases[idx]->initial);
+			if (!topology) {
+				ast_test_status_update(test, "Failed to build initial SDP state topology\n");
+				goto end;
+			}
+		} else {
+			topology = NULL;
+		}
+		test_state = ast_sdp_state_alloc(topology, options);
+		ast_stream_topology_free(topology);
+		if (!test_state) {
+			ast_test_status_update(test, "Failed to build SDP state\n");
+			goto end;
+		}
+
+		/* Update the initial topology with one or two new topologies. */
+		topology = build_update_topology(sdp_update_cases[idx]->update_1);
+		if (!topology) {
+			ast_test_status_update(test, "Failed to build first update SDP state topology\n");
+			goto end;
+		}
+		status = ast_sdp_state_update_local_topology(test_state, topology);
+		ast_stream_topology_free(topology);
+		if (status) {
+			ast_test_status_update(test, "Failed to update first update SDP state topology\n");
+			goto end;
+		}
+		if (sdp_update_cases[idx]->update_2) {
+			topology = build_update_topology(sdp_update_cases[idx]->update_2);
+			if (!topology) {
+				ast_test_status_update(test, "Failed to build second update SDP state topology\n");
+				goto end;
+			}
+			status = ast_sdp_state_update_local_topology(test_state, topology);
+			ast_stream_topology_free(topology);
+			if (status) {
+				ast_test_status_update(test, "Failed to update second update SDP state topology\n");
+				goto end;
+			}
+		}
+
+		/* Get the SDP offer to merge the updates into the initial topology. */
+		if (!ast_sdp_state_get_local_sdp(test_state)) {
+			ast_test_status_update(test, "Failed to create offer SDP\n");
+			goto end;
+		}
+
+		/* Check that the offered topology matches the expected topology. */
+		topology = build_update_topology(sdp_update_cases[idx]->expected);
+		if (!topology) {
+			ast_test_status_update(test, "Failed to build expected topology\n");
+			goto end;
+		}
+		status = cmp_update_topology(test, topology,
+			ast_sdp_state_get_local_topology(test_state));
+		ast_stream_topology_free(topology);
+		if (status) {
+			ast_test_status_update(test, "Failed to match expected topology\n");
+			goto end;
+		}
+
+		/* Repeat for each test case defined. */
+		ast_sdp_state_free(test_state);
+		test_state = NULL;
+	}
+	res = AST_TEST_PASS;
+
+end:
+	ast_sdp_state_free(test_state);
+	return res;
+}
+
 static int unload_module(void)
 {
 	AST_TEST_UNREGISTER(invalid_rtpmap);
@@ -1186,10 +2014,13 @@
 	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);
-	AST_TEST_UNREGISTER(sdp_merge_asymmetric);
+	AST_TEST_UNREGISTER(sdp_negotiation_initial);
+	AST_TEST_UNREGISTER(sdp_negotiation_type_change);
+	AST_TEST_UNREGISTER(sdp_negotiation_decline_incompatible);
+	AST_TEST_UNREGISTER(sdp_negotiation_decline_max_streams);
+	AST_TEST_UNREGISTER(sdp_negotiation_not_acceptable);
 	AST_TEST_UNREGISTER(sdp_ssrc_attributes);
+	AST_TEST_UNREGISTER(sdp_update_topology);
 
 	return 0;
 }
@@ -1201,10 +2032,13 @@
 	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);
-	AST_TEST_REGISTER(sdp_merge_asymmetric);
+	AST_TEST_REGISTER(sdp_negotiation_initial);
+	AST_TEST_REGISTER(sdp_negotiation_type_change);
+	AST_TEST_REGISTER(sdp_negotiation_decline_incompatible);
+	AST_TEST_REGISTER(sdp_negotiation_decline_max_streams);
+	AST_TEST_REGISTER(sdp_negotiation_not_acceptable);
 	AST_TEST_REGISTER(sdp_ssrc_attributes);
+	AST_TEST_REGISTER(sdp_update_topology);
 
 	return AST_MODULE_LOAD_SUCCESS;
 }

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

Gerrit-Project: asterisk
Gerrit-Branch: master
Gerrit-MessageType: merged
Gerrit-Change-Id: If07fe6d79fbdce33968a9401d41d908385043a06
Gerrit-Change-Number: 5819
Gerrit-PatchSet: 5
Gerrit-Owner: Richard Mudgett <rmudgett at digium.com>
Gerrit-Reviewer: George Joseph <gjoseph at digium.com>
Gerrit-Reviewer: Jenkins2
Gerrit-Reviewer: Joshua Colp <jcolp at digium.com>
Gerrit-Reviewer: Richard Mudgett <rmudgett at digium.com>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.digium.com/pipermail/asterisk-code-review/attachments/20170726/1b487d2d/attachment-0001.html>


More information about the asterisk-code-review mailing list