[Asterisk-code-review] codec_negotiation: Implement outgoing_call_offer_pref (asterisk[master])

Friendly Automation asteriskteam at digium.com
Mon Apr 6 08:00:54 CDT 2020


Friendly Automation has submitted this change. ( https://gerrit.asterisk.org/c/asterisk/+/13945 )

Change subject: codec_negotiation: Implement outgoing_call_offer_pref
......................................................................

codec_negotiation: Implement outgoing_call_offer_pref

Based on this new endpoint setting, a joint list of preferred codecs
between those received from the Asterisk core (remote), and those
specified in the endpoint's "allow" parameter (local) is created and
is used to create the outgoing SDP offer.

* Add outgoing_call_offer_pref to pjsip_configuration (endpoint)

* Add "call_direction" to res_pjsip_session.

* Update pjsip_session_caps.c to make the functions more generic
  so they could be used for both incoming and outgoing.

* Update ast_sip_session_create_outgoing to create the
  pending_media_state->topology with the results of
  ast_sip_session_create_joint_call_stream().

* The endpoint "preferred_codec_only" option now automatically sets
  AST_SIP_CALL_CODEC_PREF_FIRST in incoming_call_offer_pref.

* A helper function ast_stream_get_format_count() was added to
  streams to return the current count of formats.

ASTERISK-28777

Change-Id: Id4ec0b4a906c2ae5885bf947f101c59059935437
---
M configs/samples/pjsip.conf.sample
A doc/CHANGES-staging/res_pjsip_call_offer_pref.txt
D doc/CHANGES-staging/res_pjsip_incoming_call_offer_pref.txt
M include/asterisk/res_pjsip.h
M include/asterisk/res_pjsip_session.h
M include/asterisk/res_pjsip_session_caps.h
M include/asterisk/stream.h
M main/stream.c
M res/res_pjsip.c
M res/res_pjsip/pjsip_configuration.c
M res/res_pjsip_sdp_rtp.c
M res/res_pjsip_session.c
M res/res_pjsip_session/pjsip_session_caps.c
13 files changed, 408 insertions(+), 297 deletions(-)

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



diff --git a/configs/samples/pjsip.conf.sample b/configs/samples/pjsip.conf.sample
index 695ba5d..93fb701 100644
--- a/configs/samples/pjsip.conf.sample
+++ b/configs/samples/pjsip.conf.sample
@@ -798,20 +798,55 @@
                    ; "0" or not enabled)
 ;contact_user= ; On outgoing requests, force the user portion of the Contact
                ; header to this value (default: "")
-;incoming_call_offer_pref= ; Sets the preferred codecs, and order to use between
-                           ; those received in the offer, and those set in this
-                           ; configuration's allow line. Valid values include:
+;incoming_call_offer_pref= ; Based on this setting, a joint list of
+                           ; preferred codecs between those received in an
+                           ; incoming SDP offer (remote), and those specified
+                           ; in the endpoint's "allow" parameter (local)
+                           ; is created and is passed to the Asterisk core.
                            ;
-                           ; local - prefer and order by configuration (default).
-                           ; local_single - prefer and order by configuration,
-                           ;     but only choose 'top' most codec
-                           ; remote - prefer and order by incoming sdp.
-                           ; remote_single - prefer and order by incoming sdp,
-                           ;     but only choose 'top' most codec
-;preferred_codec_only=yes       ; Respond to a SIP invite with the single most preferred codec
-                                ; rather than advertising all joint codec capabilities. This
-                                ; limits the other side's codec choice to exactly what we prefer.
-                                ; default is no.
+                           ; local - Include all codecs in the local list that
+                           ; are also in the remote list preserving the local
+                           ; order. (default).
+                           ; local_first - Include only the first codec in the
+                           ; local list that is also in the remote list.
+                           ; remote - Include all codecs in the remote list that
+                           ; are also in the local list preserving remote list
+                           ; order.
+                           ; remote_first - Include only the first codec in
+                           ; the remote list that is also in the local list.
+;outgoing_call_offer_pref= ; Based on this setting, a joint list of
+                           ; preferred codecs between those received from the
+                           ; Asterisk core (remote), and those specified in
+                           ; the endpoint's "allow" parameter (local) is
+                           ; created and is used to create the outgoing SDP
+                           ; offer.
+                           ;
+                           ; local - Include all codecs in the local list that
+                           ; are also in the remote list preserving the local
+                           ; order.
+                           ; local_merge - Include all codecs in BOTH lists
+                           ; preserving the local list order.  Codes in the
+                           ; remote list not in the local list will be placed
+                           ; at the end of the joint list.
+                           ; local_first - Include only the first codec in the
+                           ; local list.
+                           ; remote - Include all codecs in the remote list that
+                           ; are also in the local list preserving remote list
+                           ; order. (default)
+                           ; remote_merge - Include all codecs in BOTH lists
+                           ; preserving the remote list order.  Codes in the
+                           ; local list not in the remote list will be placed
+                           ; at the end of the joint list.
+                           ; remote_first - Include only the first codec in
+                           ; the remote list.
+;preferred_codec_only=no   ; Respond to a SIP invite with the single most
+                           ; preferred codec rather than advertising all joint
+                           ; codec capabilities. This limits the other side's
+                           ; codec choice to exactly what we prefer.
+                           ; default is no.
+                           ; NOTE: This option is deprecated in favor
+                           ; of incoming_call_offer_pref.  Setting both
+                           ; options is unsupported.
 ;asymmetric_rtp_codec= ; Allow the sending and receiving codec to differ and
                        ; not be automatically matched (default: "no")
 ;refer_blind_progress= ; Whether to notifies all the progress details on blind
diff --git a/doc/CHANGES-staging/res_pjsip_call_offer_pref.txt b/doc/CHANGES-staging/res_pjsip_call_offer_pref.txt
new file mode 100644
index 0000000..c8b8747
--- /dev/null
+++ b/doc/CHANGES-staging/res_pjsip_call_offer_pref.txt
@@ -0,0 +1,8 @@
+Subject: res_pjsip
+Subject: res_pjsip_session
+Master-Only: True
+
+Two new options, incoming_call_offer_pref and outgoing_call_offer_pref
+have been added to res_pjsip endpoints that specify the preferred order
+of codecs to use between those received/sent in an SDP offer and those
+set in the endpoint configuration.
diff --git a/doc/CHANGES-staging/res_pjsip_incoming_call_offer_pref.txt b/doc/CHANGES-staging/res_pjsip_incoming_call_offer_pref.txt
deleted file mode 100644
index 5a12052..0000000
--- a/doc/CHANGES-staging/res_pjsip_incoming_call_offer_pref.txt
+++ /dev/null
@@ -1,53 +0,0 @@
-Subject: res_pjsip
-Subject: res_pjsip_session
-Master-Only: True
-
-A new option, incoming_call_offer_pref, was added to res_pjsip endpoints that
-specifies the preferred order of codecs to use between those received in the
-offer, and those set in the configuration.
-
-Valid values include:
-  local - prefer and order by configuration (default).
-  local_single - prefer and order by configuration, but only choose 'top'
-                 most codec
-  remote - prefer and order by incoming sdp.
-  remote_single - prefer and order by incoming sdp, but only choose 'top' most
-                  most codec
-
-Example A:
-  [alice]
-  type=endpoint
-  incoming_call_offer_pref=local
-  allow=!all,opus,alaw,ulaw
-
-  Alice's incoming sdp=g722,ulaw,alaw
-  RESULT: alaw,ulaw
-
-Example B:
-  [alice]
-  type=endpoint
-  incoming_call_offer_pref=local_single
-  allow=!all,opus,alaw,ulaw
-
-  Alice's incoming sdp=g722,ulaw,alaw
-  RESULT: alaw
-
-Example C:
-  [alice]
-  type=endpoint
-  incoming_call_offer_pref=remote
-  allow=!all,opus,alaw,ulaw
-
-  Alice's incoming sdp=g722,ulaw,alaw
-  RESULT: ulaw,alaw
-
-Example D:
-  [alice]
-  type=endpoint
-  incoming_call_offer_pref=remote_single
-  allow=!all,opus,alaw,ulaw
-
-  Alice's incoming sdp=g722,ulaw,alaw
-  RESULT: ulaw
-
-
diff --git a/include/asterisk/res_pjsip.h b/include/asterisk/res_pjsip.h
index 816e614..c1d5490 100644
--- a/include/asterisk/res_pjsip.h
+++ b/include/asterisk/res_pjsip.h
@@ -511,23 +511,41 @@
 
 /*!
  * \brief Incoming/Outgoing call offer/answer joint codec preference.
+ *
+ * The default is INTERSECT ALL LOCAL.
  */
 enum ast_sip_call_codec_pref {
+	/*! Two bits for merge */
+	/*! Intersection of local and remote */
+	AST_SIP_CALL_CODEC_PREF_INTERSECT =	1 << 0,
+	/*! Union of local and remote */
+	AST_SIP_CALL_CODEC_PREF_UNION =		1 << 1,
+
+	/*! Two bits for filter */
+	/*! No filter */
+	AST_SIP_CALL_CODEC_PREF_ALL =	 	1 << 2,
+	/*! Only the first */
+	AST_SIP_CALL_CODEC_PREF_FIRST = 	1 << 3,
+
+	/*! Two bits for preference and sort   */
 	/*! Prefer, and order by local values */
-	AST_SIP_CALL_CODEC_PREF_LOCAL,
-	/*! Prefer, and order by local values (intersection) */
-	AST_SIP_CALL_CODEC_PREF_LOCAL_LIMIT,
-	/*! Prefer, and order by local values (top/first only) */
-	AST_SIP_CALL_CODEC_PREF_LOCAL_SINGLE,
+	AST_SIP_CALL_CODEC_PREF_LOCAL = 	1 << 4,
 	/*! Prefer, and order by remote values */
-	AST_SIP_CALL_CODEC_PREF_REMOTE,
-	/*! Prefer, and order by remote values (intersection) */
-	AST_SIP_CALL_CODEC_PREF_REMOTE_LIMIT,
-	/*! Prefer, and order by remote values (top/first only) */
-	AST_SIP_CALL_CODEC_PREF_REMOTE_SINGLE,
+	AST_SIP_CALL_CODEC_PREF_REMOTE = 	1 << 5,
 };
 
 /*!
+ * \brief Returns true if the preference is set in the parameter
+ * \since 18.0.0
+ *
+ * \param param A ast_flags struct with one or more of enum ast_sip_call_codec_pref set
+ * \param codec_pref The last component of one of the enum values
+ * \retval 1 if the enum value is set
+ * \retval 0 if not
+ */
+#define ast_sip_call_codec_pref_test(__param, __codec_pref) (!!(ast_test_flag( &__param, AST_SIP_CALL_CODEC_PREF_ ## __codec_pref )))
+
+/*!
  * \brief Session timers options
  */
 struct ast_sip_timer_options {
@@ -769,7 +787,9 @@
 	/*! Enable webrtc settings and defaults */
 	unsigned int webrtc;
 	/*! Codec preference for an incoming offer */
-	enum ast_sip_call_codec_pref incoming_call_offer_pref;
+	struct ast_flags incoming_call_offer_pref;
+	/*! Codec preference for an outgoing offer */
+	struct ast_flags outgoing_call_offer_pref;
 };
 
 /*!
@@ -3223,6 +3243,18 @@
 int ast_sip_str_to_dtmf(const char *dtmf_mode);
 
 /*!
+ * \brief Convert the call codec preference flags to a string
+ * \since 18.0.0
+ *
+ * \param pref the call codec preference setting
+ *
+ * \returns a constant string with either the setting value or 'unknown'
+ * \note Don't try to free the string!
+ *
+ */
+const char *ast_sip_call_codec_pref_to_str(struct ast_flags pref);
+
+/*!
  * \brief Transport shutdown monitor callback.
  * \since 13.18.0
  *
diff --git a/include/asterisk/res_pjsip_session.h b/include/asterisk/res_pjsip_session.h
index a5ae6f1..fd49a7b 100644
--- a/include/asterisk/res_pjsip_session.h
+++ b/include/asterisk/res_pjsip_session.h
@@ -30,6 +30,9 @@
 #include "asterisk/sdp_srtp.h"
 /* Needed for ast_media_type */
 #include "asterisk/codec.h"
+/* Needed for pjmedia_sdp_session and pjsip_inv_session */
+#include <pjsip_ua.h>
+
 
 /* Forward declarations */
 struct ast_sip_endpoint;
@@ -80,8 +83,6 @@
 	struct ast_sip_session_sdp_handler *handler;
 	/*! \brief Holds SRTP information */
 	struct ast_sdp_srtp *srtp;
-	/*! \brief Media format capabilities */
-	struct ast_sip_session_caps *caps;
 	/*! \brief What type of encryption is in use on this stream */
 	enum ast_sip_session_media_encryption encryption;
 	/*! \brief The media transport in use for this stream */
@@ -157,6 +158,12 @@
 /*! \brief Opaque struct controlling the suspension of the session's serializer. */
 struct ast_sip_session_suspender;
 
+/*! \brief Indicates the call direction respective to Asterisk */
+enum ast_sip_session_call_direction {
+	AST_SIP_SESSION_INCOMING_CALL = 0,
+	AST_SIP_SESSION_OUTGOING_CALL,
+};
+
 /*!
  * \brief A structure describing a SIP session
  *
@@ -222,8 +229,10 @@
 	enum ast_sip_dtmf_mode dtmf;
 	/*! Initial incoming INVITE Request-URI.  NULL otherwise. */
 	pjsip_uri *request_uri;
-	/* Media statistics for negotiated RTP streams */
+	/*! Media statistics for negotiated RTP streams */
 	AST_VECTOR(, struct ast_rtp_instance_stats *) media_stats;
+	/*! The direction of the call respective to Asterisk */
+	enum ast_sip_session_call_direction call_direction;
 };
 
 typedef int (*ast_sip_session_request_creation_cb)(struct ast_sip_session *session, pjsip_tx_data *tdata);
diff --git a/include/asterisk/res_pjsip_session_caps.h b/include/asterisk/res_pjsip_session_caps.h
index 810a1e6..0d7020f 100644
--- a/include/asterisk/res_pjsip_session_caps.h
+++ b/include/asterisk/res_pjsip_session_caps.h
@@ -20,63 +20,63 @@
 
 struct ast_format_cap;
 struct ast_sip_session;
-struct ast_sip_session_media;
-struct ast_sip_session_caps;
 
 /*!
- * \brief Allocate a SIP session capabilities object.
+ * \brief Create joint capabilities
  * \since 18.0.0
  *
- * \retval An ao2 allocated SIP session capabilities object, or NULL on error
+ * Creates a list of joint capabilities between the given remote capabilities, and local ones.
+ * "local" and "remote" reference the values in ast_sip_call_codec_pref.
+ *
+ * \param remote The "remote" capabilities
+ * \param local The "local" capabilities
+ * \param media_type The media type
+ * \param codec_prefs One or more of enum ast_sip_call_codec_pref
+ *
+ * \retval A pointer to the joint capabilities (which may be empty).
+ *         NULL will be returned only if no memory was available to allocate the structure.
+ * \note Returned object's reference must be released at some point,
  */
-struct ast_sip_session_caps *ast_sip_session_caps_alloc(void);
+struct ast_format_cap *ast_sip_create_joint_call_cap(const struct ast_format_cap *remote,
+	struct ast_format_cap *local, enum ast_media_type media_type,
+	struct ast_flags codec_pref);
 
 /*!
- * \brief Set the incoming call offer capabilities for a session.
+ * \brief Create a new stream of joint capabilities
  * \since 18.0.0
  *
- * This will replace any capabilities already present.
- *
- * \param caps A session's capabilities object
- * \param cap The capabilities to set it to
- */
-void ast_sip_session_set_incoming_call_offer_cap(struct ast_sip_session_caps *caps,
-	struct ast_format_cap *cap);
-
-/*!
- * \brief Get the incoming call offer capabilities.
- * \since 18.0.0
- *
- * \note Returned objects reference is not incremented.
- *
- * \param caps A session's capabilities object
- *
- * \retval An incoming call offer capabilities object
- */
-const struct ast_format_cap *ast_sip_session_get_incoming_call_offer_cap(
-	const struct ast_sip_session_caps *caps);
-
-/*!
- * \brief Make the incoming call offer capabilities for a session.
- * \since 18.0.0
- *
- * Creates and sets a list of joint capabilities between the given remote
- * capabilities, and pre-configured ones. The resulting joint list is then
- * stored, and 'owned' (reference held) by the session.
- *
- * If the incoming capabilities have been set elsewhere, this will not replace
- * those. It will however, return a pointer to the current set.
- *
- * \note Returned object's reference is not incremented.
+ * Creates a new stream with capabilities between the given session's local capabilities,
+ * and the remote stream's.  Codec selection is based on the session->endpoint's codecs, the
+ * session->endpoint's codec call preferences, and the stream passed by the core (for
+ * outgoing calls) or created by the incoming SDP (for incoming calls).
  *
  * \param session The session
- * \param session_media An associated media session
- * \param remote Capabilities of a device
+ * \param remote The remote stream
  *
- * \retval A pointer to the incoming call offer capabilities
+ * \retval A pointer to a new stream with the joint capabilities (which may be empty),
+ *         NULL will be returned only if no memory was available to allocate the structure.
  */
-const struct ast_format_cap *ast_sip_session_join_incoming_call_offer_cap(
-	const struct ast_sip_session *session, const struct ast_sip_session_media *session_media,
-	const struct ast_format_cap *remote);
+struct ast_stream *ast_sip_session_create_joint_call_stream(const struct ast_sip_session *session,
+	struct ast_stream *remote);
+
+/*!
+ * \brief Create joint capabilities
+ * \since 18.0.0
+ *
+ * Creates a list of joint capabilities between the given session's local capabilities,
+ * and the remote capabilities. Codec selection is based on the session->endpoint's codecs, the
+ * session->endpoint's codec call preferences, and the "remote" capabilities passed by the core (for
+ * outgoing calls) or created by the incoming SDP (for incoming calls).
+ *
+ * \param session The session
+ * \param media_type The media type
+ * \param remote Capabilities received in an SDP offer or from the core
+ *
+ * \retval A pointer to the joint capabilities (which may be empty).
+ *         NULL will be returned only if no memory was available to allocate the structure.
+ * \note Returned object's reference must be released at some point,
+ */
+struct ast_format_cap *ast_sip_session_create_joint_call_cap(const struct ast_sip_session *session,
+	enum ast_media_type media_type, const struct ast_format_cap *remote);
 
 #endif /* RES_PJSIP_SESSION_CAPS_H */
diff --git a/include/asterisk/stream.h b/include/asterisk/stream.h
index ade740d..17275c5 100644
--- a/include/asterisk/stream.h
+++ b/include/asterisk/stream.h
@@ -167,6 +167,17 @@
 struct ast_format_cap *ast_stream_get_formats(const struct ast_stream *stream);
 
 /*!
+ * \brief Get the count of the current negotiated formats of a stream
+ *
+ * \param stream The media stream
+ *
+ * \return The count of negotiated formats
+ *
+ * \since 18
+ */
+int ast_stream_get_format_count(const struct ast_stream *stream);
+
+/*!
  * \brief Set the current negotiated formats of a stream
  *
  * \param stream The media stream
diff --git a/main/stream.c b/main/stream.c
index 626fa3a..41b7948 100644
--- a/main/stream.c
+++ b/main/stream.c
@@ -186,6 +186,13 @@
 	return stream->formats;
 }
 
+int ast_stream_get_format_count(const struct ast_stream *stream)
+{
+	ast_assert(stream != NULL);
+
+	return stream->formats ? ast_format_cap_count(stream->formats) : 0;
+}
+
 void ast_stream_set_formats(struct ast_stream *stream, struct ast_format_cap *caps)
 {
 	ast_assert(stream != NULL);
diff --git a/res/res_pjsip.c b/res/res_pjsip.c
index 4d77a6d..12e41cc 100644
--- a/res/res_pjsip.c
+++ b/res/res_pjsip.c
@@ -923,25 +923,75 @@
 					</para></description>
 				</configOption>
 				<configOption name="preferred_codec_only" default="no">
-					<synopsis>Respond to a SIP invite with the single most preferred codec rather than advertising all joint codec capabilities. This limits the other side's codec choice to exactly what we prefer.</synopsis>
+					<synopsis>Respond to a SIP invite with the single most preferred codec (DEPRECATED)</synopsis>
+					<description><para>Respond to a SIP invite with the single most preferred codec
+					rather than advertising all joint codec capabilities. This limits the other side's codec
+					choice to exactly what we prefer.</para>
+					<warning><para>This option has been deprecated in favor of
+					<literal>incoming_call_offer_pref</literal>.  Setting both options is unsupported.</para>
+					</warning>
+					</description>
+					<see-also>
+						<ref type="configOption">incoming_call_offer_pref</ref>
+					</see-also>
 				</configOption>
 				<configOption name="incoming_call_offer_pref" default="local">
-					<synopsis>After receiving an incoming offer create a list of preferred codecs between
-					those received in the SDP offer, and those specified in endpoint configuration.</synopsis>
+					<synopsis>Preferences for selecting codecs for an incoming call.</synopsis>
 					<description>
-						<note><para>This list will consist of only those codecs found in both.</para></note>
+						<para>Based on this setting, a joint list of preferred codecs between those
+						received in an incoming SDP offer (remote), and those specified in the
+						endpoint's "allow" parameter (local) es created and is passed to the Asterisk
+						core. </para>
+						<note><para>This list will consist of only those codecs found in both lists.</para></note>
 						<enumlist>
 							<enum name="local"><para>
-								Order by the endpoint configuration allow line (default)
+								Include all codecs in the local list that are also in the remote list
+								preserving the local order.  (default).
 							</para></enum>
-							<enum name="local_single"><para>
-								Order by the endpoint configuration allow line, but the list will only contain the first, or 'top' item
+							<enum name="local_first"><para>
+								Include only the first codec in the local list that is also in the remote list.
 							</para></enum>
 							<enum name="remote"><para>
-								Order by what is received in the SDP offer
+								Include all codecs in the remote list that are also in the local list
+								preserving the remote order.
 							</para></enum>
-							<enum name="remote_single"><para>
-								Order by what is received in the SDP offer, but the list will only contain the first, or 'top' item
+							<enum name="remote_first"><para>
+								Include only the first codec in the remote list that is also in the local list.
+							</para></enum>
+						</enumlist>
+					</description>
+				</configOption>
+				<configOption name="outgoing_call_offer_pref" default="local">
+					<synopsis>Preferences for selecting codecs for an outgoing call.</synopsis>
+					<description>
+						<para>Based on this setting, a joint list of preferred codecs between
+						those received from the Asterisk core (remote), and those specified in
+						the endpoint's "allow" parameter (local) is created and is used to create
+						the outgoing SDP offer.</para>
+						<enumlist>
+							<enum name="local"><para>
+								Include all codecs in the local list that are also in the remote list
+								preserving the local order.
+							</para></enum>
+							<enum name="local_merge"><para>
+								Include all codecs in BOTH lists preserving the local order.
+								Remote codecs not in the local list will be placed at the end
+								of the joint list.
+							</para></enum>
+							<enum name="local_first"><para>
+								Include only the first codec in the local list.
+							</para></enum>
+							<enum name="remote"><para>
+								Include all codecs in the remote list that are also in the local list
+								preserving the remote order. (default)
+							</para></enum>
+							<enum name="remote_merge"><para>
+								Include all codecs in BOTH lists preserving the remote order.
+								Local codecs not in the remote list will be placed at the end
+								of the joint list.
+							</para></enum>
+							<enum name="remote_first"><para>
+								Include only the first codec in the remote list.
 							</para></enum>
 						</enumlist>
 					</description>
@@ -5044,6 +5094,29 @@
 	return result;
 }
 
+const char *ast_sip_call_codec_pref_to_str(struct ast_flags pref)
+{
+	const char *value;
+
+	if (ast_sip_call_codec_pref_test(pref, LOCAL) &&  ast_sip_call_codec_pref_test(pref, INTERSECT) && ast_sip_call_codec_pref_test(pref, ALL)) {
+		value = "local";
+	} else if (ast_sip_call_codec_pref_test(pref, LOCAL) &&  ast_sip_call_codec_pref_test(pref, UNION) && ast_sip_call_codec_pref_test(pref, ALL)) {
+		value = "local_merge";
+	} else if (ast_sip_call_codec_pref_test(pref, LOCAL) &&  ast_sip_call_codec_pref_test(pref, INTERSECT) && ast_sip_call_codec_pref_test(pref, FIRST)) {
+		value = "local_first";
+	} else if (ast_sip_call_codec_pref_test(pref, REMOTE) &&  ast_sip_call_codec_pref_test(pref, INTERSECT) && ast_sip_call_codec_pref_test(pref, ALL)) {
+		value = "remote";
+	} else if (ast_sip_call_codec_pref_test(pref, REMOTE) &&  ast_sip_call_codec_pref_test(pref, UNION) && ast_sip_call_codec_pref_test(pref, ALL)) {
+		value = "remote_merge";
+	} else if (ast_sip_call_codec_pref_test(pref, REMOTE) &&  ast_sip_call_codec_pref_test(pref, UNION) && ast_sip_call_codec_pref_test(pref, FIRST)) {
+		value = "remote_first";
+	} else {
+		value = "unknown";
+	}
+
+	return value;
+}
+
 /*!
  * \brief Set name and number information on an identity header.
  *
diff --git a/res/res_pjsip/pjsip_configuration.c b/res/res_pjsip/pjsip_configuration.c
index 1d61558..5a4842b 100644
--- a/res/res_pjsip/pjsip_configuration.c
+++ b/res/res_pjsip/pjsip_configuration.c
@@ -1121,43 +1121,57 @@
 	return 0;
 }
 
-static const char *sip_call_codec_pref_strings[] = {
-	[AST_SIP_CALL_CODEC_PREF_LOCAL] = "local",
-	[AST_SIP_CALL_CODEC_PREF_LOCAL_LIMIT] = "local_limit",
-	[AST_SIP_CALL_CODEC_PREF_LOCAL_SINGLE] = "local_single",
-	[AST_SIP_CALL_CODEC_PREF_REMOTE] = "remote",
-	[AST_SIP_CALL_CODEC_PREF_REMOTE_LIMIT] = "remote_limit",
-	[AST_SIP_CALL_CODEC_PREF_REMOTE_SINGLE] = "remote_single",
-};
-
-static int incoming_call_offer_pref_handler(const struct aco_option *opt,
+static int call_offer_pref_handler(const struct aco_option *opt,
 	struct ast_variable *var, void *obj)
 {
 	struct ast_sip_endpoint *endpoint = obj;
-	unsigned int i;
+	struct ast_flags pref = { 0, };
+	int outgoing = strcmp(var->name, "outgoing_call_offer_pref") == 0;
 
-	for (i = 0; i < ARRAY_LEN(sip_call_codec_pref_strings); ++i) {
-		if (!strcmp(var->value, sip_call_codec_pref_strings[i])) {
-			/* Local and remote limit are not available values for this option */
-			if (i == AST_SIP_CALL_CODEC_PREF_LOCAL_LIMIT ||
-				i == AST_SIP_CALL_CODEC_PREF_REMOTE_LIMIT) {
-				return -1;
-			}
-
-			endpoint->media.incoming_call_offer_pref = i;
-			return 0;
-		}
+	if (strcmp(var->value, "local") == 0) {
+		ast_set_flag(&pref, AST_SIP_CALL_CODEC_PREF_LOCAL | AST_SIP_CALL_CODEC_PREF_INTERSECT | AST_SIP_CALL_CODEC_PREF_ALL);
+	} else if (outgoing && strcmp(var->value, "local_merge") == 0) {
+		ast_set_flag(&pref, AST_SIP_CALL_CODEC_PREF_LOCAL | AST_SIP_CALL_CODEC_PREF_UNION | AST_SIP_CALL_CODEC_PREF_ALL);
+	} else if (strcmp(var->value, "local_first") == 0) {
+		ast_set_flag(&pref, AST_SIP_CALL_CODEC_PREF_LOCAL | AST_SIP_CALL_CODEC_PREF_INTERSECT | AST_SIP_CALL_CODEC_PREF_FIRST);
+	} else if (strcmp(var->value, "remote") == 0) {
+		ast_set_flag(&pref, AST_SIP_CALL_CODEC_PREF_REMOTE | AST_SIP_CALL_CODEC_PREF_INTERSECT | AST_SIP_CALL_CODEC_PREF_ALL);
+	} else if (outgoing && strcmp(var->value, "remote_merge") == 0) {
+		ast_set_flag(&pref, AST_SIP_CALL_CODEC_PREF_REMOTE | AST_SIP_CALL_CODEC_PREF_UNION | AST_SIP_CALL_CODEC_PREF_ALL);
+	} else if (strcmp(var->value, "remote_first") == 0) {
+		ast_set_flag(&pref, AST_SIP_CALL_CODEC_PREF_REMOTE | AST_SIP_CALL_CODEC_PREF_UNION | AST_SIP_CALL_CODEC_PREF_FIRST);
+	} else {
+		return -1;
 	}
 
-	return -1;
+	if (outgoing) {
+		endpoint->media.outgoing_call_offer_pref = pref;
+	} else {
+		endpoint->media.incoming_call_offer_pref = pref;
+	}
+
+	return 0;
 }
 
 static int incoming_call_offer_pref_to_str(const void *obj, const intptr_t *args, char **buf)
 {
 	const struct ast_sip_endpoint *endpoint = obj;
 
-	if (ARRAY_IN_BOUNDS(endpoint->media.incoming_call_offer_pref, sip_call_codec_pref_strings)) {
-		*buf = ast_strdup(sip_call_codec_pref_strings[endpoint->media.incoming_call_offer_pref]);
+	*buf = ast_strdup(ast_sip_call_codec_pref_to_str(endpoint->media.incoming_call_offer_pref));
+	if (!(*buf)) {
+		return -1;
+	}
+
+	return 0;
+}
+
+static int outgoing_call_offer_pref_to_str(const void *obj, const intptr_t *args, char **buf)
+{
+	const struct ast_sip_endpoint *endpoint = obj;
+
+	*buf = ast_strdup(ast_sip_call_codec_pref_to_str(endpoint->media.outgoing_call_offer_pref));
+	if (!(*buf)) {
+		return -1;
 	}
 
 	return 0;
@@ -1345,6 +1359,16 @@
 		return -1;
 	}
 
+	if (endpoint->preferred_codec_only) {
+		if (endpoint->media.incoming_call_offer_pref.flags != (AST_SIP_CALL_CODEC_PREF_LOCAL | AST_SIP_CALL_CODEC_PREF_INTERSECT | AST_SIP_CALL_CODEC_PREF_ALL)) {
+			ast_log(LOG_ERROR, "Setting both preferred_codec_only and incoming_call_offer_pref is not supported on endpoint '%s'\n",
+				ast_sorcery_object_get_id(endpoint));
+			return -1;
+		}
+		ast_clear_flag(&endpoint->media.incoming_call_offer_pref, AST_SIP_CALL_CODEC_PREF_ALL);
+		ast_set_flag(&endpoint->media.incoming_call_offer_pref, AST_SIP_CALL_CODEC_PREF_FIRST);
+	}
+
 	endpoint->media.topology = ast_stream_topology_create_from_format_cap(endpoint->media.codecs);
 	if (!endpoint->media.topology) {
 		return -1;
@@ -2009,7 +2033,9 @@
 	ast_sorcery_object_field_register(sip_sorcery, "endpoint", "suppress_q850_reason_headers", "no", OPT_BOOL_T, 1, FLDSET(struct ast_sip_endpoint, suppress_q850_reason_headers));
 	ast_sorcery_object_field_register(sip_sorcery, "endpoint", "ignore_183_without_sdp", "no", OPT_BOOL_T, 1, FLDSET(struct ast_sip_endpoint, ignore_183_without_sdp));
 	ast_sorcery_object_field_register_custom(sip_sorcery, "endpoint", "incoming_call_offer_pref", "local",
-		incoming_call_offer_pref_handler, incoming_call_offer_pref_to_str, NULL, 0, 0);
+		call_offer_pref_handler, incoming_call_offer_pref_to_str, NULL, 0, 0);
+	ast_sorcery_object_field_register_custom(sip_sorcery, "endpoint", "outgoing_call_offer_pref", "remote",
+		call_offer_pref_handler, outgoing_call_offer_pref_to_str, NULL, 0, 0);
 
 	if (ast_sip_initialize_sorcery_transport()) {
 		ast_log(LOG_ERROR, "Failed to register SIP transport support with sorcery\n");
diff --git a/res/res_pjsip_sdp_rtp.c b/res/res_pjsip_sdp_rtp.c
index bc60d41..bb7c43e 100644
--- a/res/res_pjsip_sdp_rtp.c
+++ b/res/res_pjsip_sdp_rtp.c
@@ -399,13 +399,13 @@
 
 static int apply_cap_to_bundled(struct ast_sip_session_media *session_media,
 	struct ast_sip_session_media *session_media_transport,
-	struct ast_stream *asterisk_stream, const struct ast_format_cap *joint)
+	struct ast_stream *asterisk_stream, struct ast_format_cap *joint)
 {
 	if (!joint) {
 		return -1;
 	}
 
-	ast_stream_set_formats(asterisk_stream, (struct ast_format_cap *)joint);
+	ast_stream_set_formats(asterisk_stream, joint);
 
 	/* If this is a bundled stream then apply the payloads to RTP instance acting as transport to prevent conflicts */
 	if (session_media_transport != session_media && session_media->bundled) {
@@ -428,11 +428,11 @@
 	return 0;
 }
 
-static const struct ast_format_cap *set_incoming_call_offer_cap(
+static struct ast_format_cap *set_incoming_call_offer_cap(
 	struct ast_sip_session *session, struct ast_sip_session_media *session_media,
 	const struct pjmedia_sdp_media *stream)
 {
-	const struct ast_format_cap *incoming_call_offer_cap;
+	struct ast_format_cap *incoming_call_offer_cap;
 	struct ast_format_cap *remote;
 	struct ast_rtp_codecs codecs = AST_RTP_CODECS_NULL_INIT;
 	int fmts = 0;
@@ -448,12 +448,13 @@
 	get_codecs(session, stream, &codecs, session_media);
 	ast_rtp_codecs_payload_formats(&codecs, remote, &fmts);
 
-	incoming_call_offer_cap = ast_sip_session_join_incoming_call_offer_cap(
-		session, session_media, remote);
+	incoming_call_offer_cap = ast_sip_session_create_joint_call_cap(
+		session, session_media->type, remote);
 
 	ao2_ref(remote, -1);
 
-	if (!incoming_call_offer_cap) {
+	if (!incoming_call_offer_cap || ast_format_cap_empty(incoming_call_offer_cap)) {
+		ao2_cleanup(incoming_call_offer_cap);
 		ast_rtp_codecs_payloads_destroy(&codecs);
 		return NULL;
 	}
@@ -1413,6 +1414,7 @@
 	struct ast_sip_session_media *session_media_transport;
 	enum ast_media_type media_type = session_media->type;
 	enum ast_sip_session_media_encryption encryption = AST_SIP_MEDIA_ENCRYPT_NONE;
+	struct ast_format_cap *joint;
 	int res;
 
 	/* If no type formats have been configured reject this stream */
@@ -1504,8 +1506,10 @@
 		}
 	}
 
-	if (apply_cap_to_bundled(session_media, session_media_transport, asterisk_stream,
-			set_incoming_call_offer_cap(session, session_media, stream))) {
+	joint = set_incoming_call_offer_cap(session, session_media, stream);
+	res = apply_cap_to_bundled(session_media, session_media_transport, asterisk_stream, joint);
+	ao2_cleanup(joint);
+	if (res != 0) {
 		return 0;
 	}
 
diff --git a/res/res_pjsip_session.c b/res/res_pjsip_session.c
index 34d9c0b..3a7d562 100644
--- a/res/res_pjsip_session.c
+++ b/res/res_pjsip_session.c
@@ -468,8 +468,6 @@
 
 	ast_free(session_media->mid);
 	ast_free(session_media->remote_mslabel);
-
-	ao2_cleanup(session_media->caps);
 }
 
 struct ast_sip_session_media *ast_sip_session_media_state_add(struct ast_sip_session *session,
@@ -528,12 +526,6 @@
 		} else {
 			session_media->bundle_group = -1;
 		}
-
-		session_media->caps = ast_sip_session_caps_alloc();
-		if (!session_media->caps) {
-			ao2_ref(session_media, -1);
-			return NULL;
-		}
 	}
 
 	if (AST_VECTOR_REPLACE(&media_state->sessions, position, session_media)) {
@@ -2701,6 +2693,8 @@
 		return NULL;
 	}
 	session->aor = ao2_bump(found_aor);
+	session->call_direction = AST_SIP_SESSION_OUTGOING_CALL;
+
 	ast_party_id_copy(&session->id, &endpoint->id.self);
 
 	if (ast_stream_topology_get_count(req_topology) > 0) {
@@ -2709,8 +2703,6 @@
 
 		for (i = 0; i < ast_stream_topology_get_count(req_topology); ++i) {
 			struct ast_stream *req_stream;
-			struct ast_format_cap *req_cap;
-			struct ast_format_cap *joint_cap;
 			struct ast_stream *clone_stream;
 
 			req_stream = ast_stream_topology_get_stream(req_topology, i);
@@ -2719,39 +2711,12 @@
 				continue;
 			}
 
-			req_cap = ast_stream_get_formats(req_stream);
-
-			joint_cap = ast_format_cap_alloc(AST_FORMAT_CAP_FLAG_DEFAULT);
-			if (!joint_cap) {
+			clone_stream = ast_sip_session_create_joint_call_stream(session, req_stream);
+			if (!clone_stream || ast_stream_get_format_count(clone_stream) == 0) {
+				ast_stream_free(clone_stream);
 				continue;
 			}
 
-			ast_format_cap_get_compatible(req_cap, endpoint->media.codecs, joint_cap);
-
-			if (ast_stream_get_type(req_stream) == AST_MEDIA_TYPE_AUDIO) {
-				/*
-				 * By appending codecs from the endpoint after compatible ones this
-				 * guarantees that priority is given to those while also allowing
-				 * translation to occur for non-compatible.
-				 */
-				ast_format_cap_append_from_cap(joint_cap,
-					endpoint->media.codecs, AST_MEDIA_TYPE_AUDIO);
-			}
-
-			if (!ast_format_cap_count(joint_cap)) {
-				ao2_ref(joint_cap, -1);
-				continue;
-			}
-
-			clone_stream = ast_stream_clone(req_stream, NULL);
-			if (!clone_stream) {
-				ao2_ref(joint_cap, -1);
-				continue;
-			}
-
-			ast_stream_set_formats(clone_stream, joint_cap);
-			ao2_ref(joint_cap, -1);
-
 			if (!session->pending_media_state->topology) {
 				session->pending_media_state->topology = ast_stream_topology_alloc();
 				if (!session->pending_media_state->topology) {
@@ -3351,6 +3316,7 @@
 #endif
 		return;
 	}
+	session->call_direction = AST_SIP_SESSION_INCOMING_CALL;
 
 	/*
 	 * The current thread is supposed be the session serializer to prevent
diff --git a/res/res_pjsip_session/pjsip_session_caps.c b/res/res_pjsip_session/pjsip_session_caps.c
index e131200..7aa4c1f 100644
--- a/res/res_pjsip_session/pjsip_session_caps.c
+++ b/res/res_pjsip_session/pjsip_session_caps.c
@@ -24,25 +24,24 @@
 #include "asterisk/format_cap.h"
 #include "asterisk/logger.h"
 #include "asterisk/sorcery.h"
-
-#include <pjsip_ua.h>
-
+#include "asterisk/stream.h"
 #include "asterisk/res_pjsip.h"
 #include "asterisk/res_pjsip_session.h"
 #include "asterisk/res_pjsip_session_caps.h"
 
-struct ast_sip_session_caps {
-	struct ast_format_cap *incoming_call_offer_cap;
-};
-
 static void log_caps(int level, const char *file, int line, const char *function,
-	const char *msg, const struct ast_sip_session *session,
-	const struct ast_sip_session_media *session_media, const struct ast_format_cap *local,
-	const struct ast_format_cap *remote, const struct ast_format_cap *joint)
+	const struct ast_sip_session *session, enum ast_media_type media_type,
+	const struct ast_format_cap *local, const struct ast_format_cap *remote,
+	const struct ast_format_cap *joint)
 {
 	struct ast_str *s1;
 	struct ast_str *s2;
 	struct ast_str *s3;
+	int outgoing = session->call_direction == AST_SIP_SESSION_OUTGOING_CALL;
+	struct ast_flags pref =
+		outgoing
+		? session->endpoint->media.outgoing_call_offer_pref
+		: session->endpoint->media.incoming_call_offer_pref;
 
 	if (level == __LOG_DEBUG && !DEBUG_ATLEAST(3)) {
 		return;
@@ -52,93 +51,57 @@
 	s2 = remote ? ast_str_alloca(AST_FORMAT_CAP_NAMES_LEN) : NULL;
 	s3 = joint ? ast_str_alloca(AST_FORMAT_CAP_NAMES_LEN) : NULL;
 
-	ast_log(level, file, line, function, "'%s' %s '%s' capabilities -%s%s%s%s%s%s\n",
+	ast_log(level, file, line, function, "'%s' Caps for %s %s call with pref '%s' - remote: %s local: %s joint: %s\n",
 		session->channel ? ast_channel_name(session->channel) :
 			ast_sorcery_object_get_id(session->endpoint),
-		msg ? msg : "-", ast_codec_media_type2str(session_media->type),
-		s1 ? " local: " : "", s1 ? ast_format_cap_get_names(local, &s1) : "",
-		s2 ? " remote: " : "", s2 ? ast_format_cap_get_names(remote, &s2) : "",
-		s3 ? " joint: " : "", s3 ? ast_format_cap_get_names(joint, &s3) : "");
+		outgoing? "outgoing" : "incoming",
+		ast_codec_media_type2str(media_type),
+		ast_sip_call_codec_pref_to_str(pref),
+		s2 ? ast_format_cap_get_names(remote, &s2) : "(NONE)",
+		s1 ? ast_format_cap_get_names(local, &s1) : "(NONE)",
+		s3 ? ast_format_cap_get_names(joint, &s3) : "(NONE)");
 }
 
-static void sip_session_caps_destroy(void *obj)
+struct ast_format_cap *ast_sip_create_joint_call_cap(const struct ast_format_cap *remote,
+	struct ast_format_cap *local, enum ast_media_type media_type,
+	struct ast_flags codec_pref)
 {
-	struct ast_sip_session_caps *caps = obj;
+	struct ast_format_cap *joint = ast_format_cap_alloc(AST_FORMAT_CAP_FLAG_DEFAULT);
+	struct ast_format_cap *local_filtered = ast_format_cap_alloc(AST_FORMAT_CAP_FLAG_DEFAULT);
 
-	ao2_cleanup(caps->incoming_call_offer_cap);
-}
+	if (!joint || !local_filtered) {
+		ast_log(LOG_ERROR, "Failed to allocate %s call offer capabilities\n",
+				ast_codec_media_type2str(media_type));
+		ao2_cleanup(joint);
+		ao2_cleanup(local_filtered);
+		return NULL;
+	}
 
-struct ast_sip_session_caps *ast_sip_session_caps_alloc(void)
-{
-	return ao2_alloc_options(sizeof(struct ast_sip_session_caps),
-		sip_session_caps_destroy, AO2_ALLOC_OPT_LOCK_NOLOCK);
-}
+	ast_format_cap_append_from_cap(local_filtered, local, media_type);
 
-void ast_sip_session_set_incoming_call_offer_cap(struct ast_sip_session_caps *caps,
-	struct ast_format_cap *cap)
-{
-	ao2_cleanup(caps->incoming_call_offer_cap);
-	caps->incoming_call_offer_cap = ao2_bump(cap);
-}
+	if (ast_sip_call_codec_pref_test(codec_pref, LOCAL)) {
+		if (ast_sip_call_codec_pref_test(codec_pref, INTERSECT)) {
+			ast_format_cap_get_compatible(local_filtered, remote, joint); /* Get common, prefer local */
+		} else {
+			ast_format_cap_append_from_cap(joint, local_filtered, media_type); /* Add local */
+			ast_format_cap_append_from_cap(joint, remote, media_type); /* Then remote */
+		}
+	} else {
+		if (ast_sip_call_codec_pref_test(codec_pref, INTERSECT)) {
+			ast_format_cap_get_compatible(remote, local_filtered, joint); /* Get common, prefer remote */
+		} else {
+			ast_format_cap_append_from_cap(joint, remote, media_type); /* Add remote */
+			ast_format_cap_append_from_cap(joint, local_filtered, media_type); /* Then local */
+		}
+	}
 
-const struct ast_format_cap *ast_sip_session_get_incoming_call_offer_cap(
-	const struct ast_sip_session_caps *caps)
-{
-	return caps->incoming_call_offer_cap;
-}
+	ao2_ref(local_filtered, -1);
 
-const struct ast_format_cap *ast_sip_session_join_incoming_call_offer_cap(
-	const struct ast_sip_session *session, const struct ast_sip_session_media *session_media,
-	const struct ast_format_cap *remote)
-{
-	enum ast_sip_call_codec_pref pref;
-	struct ast_format_cap *joint;
-	struct ast_format_cap *local;
-
-	joint = session_media->caps->incoming_call_offer_cap;
-
-	if (joint) {
-		/*
-		 * If the incoming call offer capabilities have been set elsewhere, e.g. dialplan
-		 * then those take precedence.
-		 */
+	if (ast_format_cap_empty(joint)) {
 		return joint;
 	}
 
-	joint = ast_format_cap_alloc(AST_FORMAT_CAP_FLAG_DEFAULT);
-	local = ast_format_cap_alloc(AST_FORMAT_CAP_FLAG_DEFAULT);
-
-	if (!joint || !local) {
-		ast_log(LOG_ERROR, "Failed to allocate %s incoming call offer capabilities\n",
-				ast_codec_media_type2str(session_media->type));
-
-		ao2_cleanup(joint);
-		ao2_cleanup(local);
-		return NULL;
-	}
-
-	pref = session->endpoint->media.incoming_call_offer_pref;
-	ast_format_cap_append_from_cap(local, session->endpoint->media.codecs,
-		session_media->type);
-
-	if (pref < AST_SIP_CALL_CODEC_PREF_REMOTE) {
-		ast_format_cap_get_compatible(local, remote, joint); /* Prefer local */
-	} else {
-		ast_format_cap_get_compatible(remote, local, joint); /* Prefer remote */
-	}
-
-	if (ast_format_cap_empty(joint)) {
-		log_caps(LOG_NOTICE, "No joint incoming", session, session_media, local, remote, NULL);
-
-		ao2_ref(joint, -1);
-		ao2_ref(local, -1);
-		return NULL;
-	}
-
-	if (pref == AST_SIP_CALL_CODEC_PREF_LOCAL_SINGLE ||
-		pref == AST_SIP_CALL_CODEC_PREF_REMOTE_SINGLE ||
-		session->endpoint->preferred_codec_only) {
-
+	if (ast_sip_call_codec_pref_test(codec_pref, FIRST)) {
 		/*
 		 * Save the most preferred one. Session capabilities are per stream and
 		 * a stream only carries a single media type, so no reason to worry with
@@ -152,11 +115,41 @@
 		ao2_ref(single, -1);
 	}
 
-	log_caps(LOG_DEBUG, "Joint incoming", session, session_media, local, remote, joint);
+	return joint;
+}
 
-	ao2_ref(local, -1);
+struct ast_stream *ast_sip_session_create_joint_call_stream(const struct ast_sip_session *session,
+	struct ast_stream *remote_stream)
+{
+	struct ast_stream *joint_stream = ast_stream_clone(remote_stream, NULL);
+	struct ast_format_cap *remote = ast_stream_get_formats(remote_stream);
+	enum ast_media_type media_type = ast_stream_get_type(remote_stream);
 
-	ast_sip_session_set_incoming_call_offer_cap(session_media->caps, joint);
+	struct ast_format_cap *joint = ast_sip_create_joint_call_cap(remote,
+		session->endpoint->media.codecs, media_type,
+		session->call_direction == AST_SIP_SESSION_OUTGOING_CALL
+				? session->endpoint->media.outgoing_call_offer_pref
+				: session->endpoint->media.incoming_call_offer_pref);
+
+	ast_stream_set_formats(joint_stream, joint);
+	ao2_cleanup(joint);
+
+	log_caps(LOG_DEBUG, session, media_type, session->endpoint->media.codecs, remote, joint);
+
+	return joint_stream;
+}
+
+struct ast_format_cap *ast_sip_session_create_joint_call_cap(
+	const struct ast_sip_session *session, enum ast_media_type media_type,
+	const struct ast_format_cap *remote)
+{
+	struct ast_format_cap *joint = ast_sip_create_joint_call_cap(remote,
+		session->endpoint->media.codecs, media_type,
+		session->call_direction == AST_SIP_SESSION_OUTGOING_CALL
+				? session->endpoint->media.outgoing_call_offer_pref
+				: session->endpoint->media.incoming_call_offer_pref);
+
+	log_caps(LOG_DEBUG, session, media_type, session->endpoint->media.codecs, remote, joint);
 
 	return joint;
 }

-- 
To view, visit https://gerrit.asterisk.org/c/asterisk/+/13945
To unsubscribe, or for help writing mail filters, visit https://gerrit.asterisk.org/settings

Gerrit-Project: asterisk
Gerrit-Branch: master
Gerrit-Change-Id: Id4ec0b4a906c2ae5885bf947f101c59059935437
Gerrit-Change-Number: 13945
Gerrit-PatchSet: 14
Gerrit-Owner: George Joseph <gjoseph at digium.com>
Gerrit-Reviewer: Benjamin Keith Ford <bford at digium.com>
Gerrit-Reviewer: Friendly Automation
Gerrit-Reviewer: George Joseph <gjoseph at digium.com>
Gerrit-Reviewer: Joshua Colp <jcolp at sangoma.com>
Gerrit-Reviewer: Kevin Harwell <kharwell at digium.com>
Gerrit-MessageType: merged
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.digium.com/pipermail/asterisk-code-review/attachments/20200406/73c745c3/attachment-0001.html>


More information about the asterisk-code-review mailing list