diff --git a/configs/samples/pjsip.conf.sample b/configs/samples/pjsip.conf.sample
index 0e2f283ea0..32af1b2d13 100644
--- a/configs/samples/pjsip.conf.sample
+++ b/configs/samples/pjsip.conf.sample
@@ -1015,7 +1015,21 @@
; using the same CHANNEL function if needed. Setting tenant ID here
; will cause it to show up on channel creation and the initial
; channel snapshot.
-
+;
+; suppress_moh_on_sendonly = no
+ ; Normally, when one party in a call sends Asterisk an SDP with
+ ; a "sendonly" or "inactive" attribute it means "hold" and
+ ; causes Asterisk to start playing MOH back to the other party.
+ ; This can be problematic if it happens at certain times, such
+ ; as in a 183 Progress message, because the MOH will replace
+ ; any early media you may be playing to the calling party. If
+ ; you set this option to "yes" on an endpoint and the endpoint
+ ; receives an SDP with "sendonly" or "inactive", Asterisk will
+ ; NOT play MOH back to the other party.
+ ; NOTE: This doesn't just apply to 183 responses. MOH will
+ ; be suppressed when the attribute appears in any SDP received
+ ; including INVITEs, re-INVITES, and other responses.
+ ; (default: no)
;==========================AUTH SECTION OPTIONS=========================
;[auth]
diff --git a/contrib/ast-db-manage/config/versions/4f91fc18c979_add_suppress_moh_on_sendonly.py b/contrib/ast-db-manage/config/versions/4f91fc18c979_add_suppress_moh_on_sendonly.py
new file mode 100644
index 0000000000..fb6a3efc71
--- /dev/null
+++ b/contrib/ast-db-manage/config/versions/4f91fc18c979_add_suppress_moh_on_sendonly.py
@@ -0,0 +1,30 @@
+"""Add suppress_moh_on_sendonly
+
+Revision ID: 4f91fc18c979
+Revises: 801b9fced8b7
+Create Date: 2024-11-05 11:37:33.604448
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = '4f91fc18c979'
+down_revision = '801b9fced8b7'
+
+from alembic import op
+import sqlalchemy as sa
+from sqlalchemy.dialects.postgresql import ENUM
+
+YESNO_NAME = 'yesno_values'
+YESNO_VALUES = ['yes', 'no']
+
+
+def upgrade():
+ yesno_values = ENUM(*YESNO_VALUES, name=YESNO_NAME, create_type=False)
+
+ op.add_column('ps_endpoints', sa.Column('suppress_moh_on_sendonly', yesno_values))
+
+
+def downgrade():
+ if op.get_context().bind.dialect.name == 'mssql':
+ op.drop_constraint('ck_ps_endpoints_suppress_moh_on_sendonly_yesno_values', 'ps_endpoints')
+ op.drop_column('ps_endpoints', 'suppress_moh_on_sendonly')
diff --git a/include/asterisk/res_pjsip.h b/include/asterisk/res_pjsip.h
index 46e376325c..cdc71a5f74 100644
--- a/include/asterisk/res_pjsip.h
+++ b/include/asterisk/res_pjsip.h
@@ -1069,6 +1069,8 @@ struct ast_sip_endpoint {
unsigned int send_aoc;
/*! Tenant ID for the endpoint */
AST_STRING_FIELD_EXTENDED(tenantid);
+ /*! Ignore remote hold requests */
+ int suppress_moh_on_sendonly;
};
/*! URI parameter for symmetric transport */
diff --git a/res/res_pjsip/pjsip_config.xml b/res/res_pjsip/pjsip_config.xml
index ab1c6332f3..71c73dc2ae 100644
--- a/res/res_pjsip/pjsip_config.xml
+++ b/res/res_pjsip/pjsip_config.xml
@@ -1533,6 +1533,27 @@
Send Advice-of-Charge messages
+
+ Suppress playing MOH to party A if party B sends
+ "sendonly" or "inactive" in an SDP
+
+ Normally, when one party in a call sends Asterisk an SDP with a "sendonly"
+ or "inactive" attribute it means "hold" and causes Asterisk to start
+ playing MOH back to the other party. This can be problematic if it happens at
+ certain times, such as in a 183 Progress message, because the MOH will
+ replace any early media you may be playing to the calling party. If you set
+ this option to "yes" on an endpoint and the endpoint receives an SDP
+ with "sendonly" or "inactive", Asterisk will NOT play MOH back to the other
+ party.
+
+
+ This doesn't just apply to 183 responses. MOH will be suppressed when
+ the attribute appears in any SDP received including INVITEs, re-INVITES,
+ and other responses.
+
+
+
+
Authentication type
diff --git a/res/res_pjsip/pjsip_configuration.c b/res/res_pjsip/pjsip_configuration.c
index c6f4d83b38..8ac1d2c240 100644
--- a/res/res_pjsip/pjsip_configuration.c
+++ b/res/res_pjsip/pjsip_configuration.c
@@ -2304,6 +2304,8 @@ int ast_res_pjsip_initialize_configuration(void)
ast_sorcery_object_field_register_custom(sip_sorcery, "endpoint", "security_negotiation", "no", security_negotiation_handler, security_negotiation_to_str, NULL, 0, 0);
ast_sorcery_object_field_register(sip_sorcery, "endpoint", "send_aoc", "no", OPT_BOOL_T, 1, FLDSET(struct ast_sip_endpoint, send_aoc));
ast_sorcery_object_field_register(sip_sorcery, "endpoint", "tenantid", "", OPT_STRINGFIELD_T, 0, STRFLDSET(struct ast_sip_endpoint, tenantid));
+ ast_sorcery_object_field_register(sip_sorcery, "endpoint", "suppress_moh_on_sendonly",
+ "no", OPT_YESNO_T, 1, FLDSET(struct ast_sip_endpoint, suppress_moh_on_sendonly));
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 4423dfda5c..7d2a5e7c4a 100644
--- a/res/res_pjsip_sdp_rtp.c
+++ b/res/res_pjsip_sdp_rtp.c
@@ -2321,14 +2321,18 @@ static int apply_negotiated_sdp_stream(struct ast_sip_session *session,
if (session_media->remotely_held_changed) {
if (session_media->remotely_held) {
/* The remote side has put us on hold */
- ast_queue_hold(session->channel, session->endpoint->mohsuggest);
- ast_rtp_instance_stop(session_media->rtp);
- ast_queue_frame(session->channel, &ast_null_frame);
+ if (!session->endpoint->suppress_moh_on_sendonly) {
+ ast_queue_hold(session->channel, session->endpoint->mohsuggest);
+ ast_rtp_instance_stop(session_media->rtp);
+ ast_queue_frame(session->channel, &ast_null_frame);
+ }
session_media->remotely_held_changed = 0;
} else {
/* The remote side has taken us off hold */
- ast_queue_unhold(session->channel);
- ast_queue_frame(session->channel, &ast_null_frame);
+ if (!session->endpoint->suppress_moh_on_sendonly) {
+ ast_queue_unhold(session->channel);
+ ast_queue_frame(session->channel, &ast_null_frame);
+ }
session_media->remotely_held_changed = 0;
}
} else if ((pjmedia_sdp_neg_was_answer_remote(session->inv_session->neg) == PJ_FALSE)