From 5aefa0a2a557a5fab040b86bb440cb983be04f53 Mon Sep 17 00:00:00 2001 From: LEOPOLD Otakar Date: Mon, 14 Sep 2026 13:38:43 +0200 Subject: [PATCH 1/8] Added support for late SDP negotiation for INVITE and reINVITE. Added support for custom ACK sending callback in PJSUA2 for both synchronous and asynchronous. Added tests for late SDP negotiation. Co-Authored-By: Pavel Spalek --- .github/workflows/ci-linux.yml | 4 + .../src/3rdparty_media_sample/alt_pjsua_aud.c | 31 +++- pjsip-apps/src/pjsua/pjsua_app_common.h | 1 + pjsip-apps/src/pjsua/pjsua_app_config.c | 7 +- pjsip/include/pjsua-lib/pjsua.h | 39 +++++ pjsip/include/pjsua2/call.hpp | 44 +++++ pjsip/include/pjsua2/endpoint.hpp | 2 + pjsip/src/pjsip-ua/sip_inv.c | 55 +++++- pjsip/src/pjsua-lib/pjsua_call.c | 90 +++++++++- pjsip/src/pjsua2/call.cpp | 35 ++++ pjsip/src/pjsua2/endpoint.cpp | 15 ++ tests/pjsua/runall.py | 4 + .../alt-pjsua-uac-late-offer-dummy-codecs.py | 25 +++ .../alt-pjsua-uac-late-offer-dummy-codecs.xml | 162 +++++++++++++++++ .../scripts-sipp/alt-pjsua-uac-late-offer.py | 30 ++++ .../scripts-sipp/alt-pjsua-uac-late-offer.xml | 161 +++++++++++++++++ .../alt-pjsua-uas-late-offer-dummy-codecs.py | 33 ++++ .../alt-pjsua-uas-late-offer-dummy-codecs.xml | 165 ++++++++++++++++++ .../scripts-sipp/alt-pjsua-uas-late-offer.py | 38 ++++ .../scripts-sipp/alt-pjsua-uas-late-offer.xml | 164 +++++++++++++++++ 20 files changed, 1094 insertions(+), 11 deletions(-) create mode 100644 tests/pjsua/scripts-sipp/alt-pjsua-uac-late-offer-dummy-codecs.py create mode 100644 tests/pjsua/scripts-sipp/alt-pjsua-uac-late-offer-dummy-codecs.xml create mode 100644 tests/pjsua/scripts-sipp/alt-pjsua-uac-late-offer.py create mode 100644 tests/pjsua/scripts-sipp/alt-pjsua-uac-late-offer.xml create mode 100644 tests/pjsua/scripts-sipp/alt-pjsua-uas-late-offer-dummy-codecs.py create mode 100644 tests/pjsua/scripts-sipp/alt-pjsua-uas-late-offer-dummy-codecs.xml create mode 100644 tests/pjsua/scripts-sipp/alt-pjsua-uas-late-offer.py create mode 100644 tests/pjsua/scripts-sipp/alt-pjsua-uas-late-offer.xml diff --git a/.github/workflows/ci-linux.yml b/.github/workflows/ci-linux.yml index 5b08c2d6fd..64188fc1ec 100644 --- a/.github/workflows/ci-linux.yml +++ b/.github/workflows/ci-linux.yml @@ -813,6 +813,10 @@ jobs: python3 run.py --exe ../../pjsip-apps/src/3rdparty_media_sample/alt_pjsua mod_sipp.py scripts-sipp/alt-pjsua-uas-multiple-amr.xml python3 run.py --exe ../../pjsip-apps/src/3rdparty_media_sample/alt_pjsua mod_sipp.py scripts-sipp/alt-pjsua-uas-media-app-managed.xml python3 run.py --exe ../../pjsip-apps/src/3rdparty_media_sample/alt_pjsua mod_sipp.py scripts-sipp/alt-pjsua-uas-early-update-app-managed.xml + python3 run.py --exe ../../pjsip-apps/src/3rdparty_media_sample/alt_pjsua mod_sipp.py scripts-sipp/alt-pjsua-uac-late-offer.xml + python3 run.py --exe ../../pjsip-apps/src/3rdparty_media_sample/alt_pjsua mod_sipp.py scripts-sipp/alt-pjsua-uas-late-offer.xml + python3 run.py --exe ../../pjsip-apps/src/3rdparty_media_sample/alt_pjsua mod_sipp.py scripts-sipp/alt-pjsua-uac-late-offer-dummy-codecs.xml + python3 run.py --exe ../../pjsip-apps/src/3rdparty_media_sample/alt_pjsua mod_sipp.py scripts-sipp/alt-pjsua-uas-late-offer-dummy-codecs.xml - name: upload artifacts on failure if: ${{ failure() }} uses: actions/upload-artifact@v4 diff --git a/pjsip-apps/src/3rdparty_media_sample/alt_pjsua_aud.c b/pjsip-apps/src/3rdparty_media_sample/alt_pjsua_aud.c index 3e86316d2b..26dbd25237 100644 --- a/pjsip-apps/src/3rdparty_media_sample/alt_pjsua_aud.c +++ b/pjsip-apps/src/3rdparty_media_sample/alt_pjsua_aud.c @@ -17,6 +17,7 @@ */ #include #include +#include "../pjsua/pjsua_app.h" #if defined(PJSUA_MEDIA_HAS_PJMEDIA) && PJSUA_MEDIA_HAS_PJMEDIA != 0 # error The PJSUA_MEDIA_HAS_PJMEDIA should be declared as zero @@ -173,14 +174,28 @@ static pjmedia_codec_factory_op alt_codec_factory_op = /* Initialize third party media library. */ pj_status_t pjsua_aud_subsys_init() { - /* Codec registration is intentionally omitted: pjsua now handles an - * empty codec registry gracefully, so 3rd-party media stacks do not - * need to register dummy codecs. The alt_codec_factory above is kept - * as reference documentation for implementors who do want to register. - */ - (void)alt_codec_factory; - (void)alt_codec_factory_op; - + if (app_config.dummy_codecs) { + /* Enable dummy codecs if requested in the app config */ + pjmedia_codec_mgr *codec_mgr; + pj_status_t status; + + /* Register our "dummy" codecs */ + alt_codec_factory.base.op = &alt_codec_factory_op; + codec_mgr = pjmedia_endpt_get_codec_mgr(pjsua_var.med_endpt); + status = pjmedia_codec_mgr_register_factory(codec_mgr, + &alt_codec_factory.base); + if (status != PJ_SUCCESS) + return status; + } + else { + /* Codec registration is intentionally omitted: pjsua now handles an + * empty codec registry gracefully, so 3rd-party media stacks do not + * need to register dummy codecs. The alt_codec_factory above is kept + * as reference documentation for implementors who do want to register. + */ + (void)alt_codec_factory; + (void)alt_codec_factory_op; + } /* TODO: initialize your evil library here */ return PJ_SUCCESS; } diff --git a/pjsip-apps/src/pjsua/pjsua_app_common.h b/pjsip-apps/src/pjsua/pjsua_app_common.h index fcee434c86..7caa63b019 100644 --- a/pjsip-apps/src/pjsua/pjsua_app_common.h +++ b/pjsip-apps/src/pjsua/pjsua_app_common.h @@ -205,6 +205,7 @@ typedef struct pjsua_app_config /* Custom SDP to inject via on_call_sdp_created (replaces generated SDP). * Only available when PJSUA_MEDIA_HAS_PJMEDIA=0 (alt media backend). */ pj_str_t custom_sdp; + pj_bool_t dummy_codecs; #endif /* Set PJSUA_CALL_MEDIA_APP_MANAGED on the default call setting used for diff --git a/pjsip-apps/src/pjsua/pjsua_app_config.c b/pjsip-apps/src/pjsua/pjsua_app_config.c index e44bb224f8..127a61341b 100644 --- a/pjsip-apps/src/pjsua/pjsua_app_config.c +++ b/pjsip-apps/src/pjsua/pjsua_app_config.c @@ -466,7 +466,8 @@ static pj_status_t parse_args(int argc, char *argv[], OPT_USE_CLI, OPT_CLI_TELNET_PORT, OPT_DISABLE_CLI_CONSOLE, OPT_SERVER_AFFINITY, OPT_SERVER_FAILOVER, OPT_RESOLVER_FALLBACK #if !PJSUA_MEDIA_HAS_PJMEDIA - , OPT_CUSTOM_SDP, OPT_SDP_PASSTHROUGH, OPT_ACC_MEDIA_APP_MANAGED + , OPT_CUSTOM_SDP, OPT_SDP_PASSTHROUGH, OPT_ACC_MEDIA_APP_MANAGED, + OPT_DUMMY_CODECS #endif }; struct pj_getopt_option long_options[] = { @@ -639,6 +640,7 @@ static pj_status_t parse_args(int argc, char *argv[], { "acc-media-app-managed", 0, 0, OPT_ACC_MEDIA_APP_MANAGED}, { "custom-sdp", 1, 0, OPT_CUSTOM_SDP}, { "sdp-passthrough", 0, 0, OPT_SDP_PASSTHROUGH}, + { "dummy-codecs", 0, 0, OPT_DUMMY_CODECS}, #endif { NULL, 0, 0, 0} }; @@ -1811,6 +1813,9 @@ static pj_status_t parse_args(int argc, char *argv[], cfg->custom_sdp.slen = (pj_ssize_t)(dst - cfg->custom_sdp.ptr); break; } + case OPT_DUMMY_CODECS: + cfg->dummy_codecs = PJ_TRUE; + break; #endif /* !PJSUA_MEDIA_HAS_PJMEDIA */ default: diff --git a/pjsip/include/pjsua-lib/pjsua.h b/pjsip/include/pjsua-lib/pjsua.h index fff2617489..d40e09a19d 100644 --- a/pjsip/include/pjsua-lib/pjsua.h +++ b/pjsip/include/pjsua-lib/pjsua.h @@ -1496,6 +1496,23 @@ typedef struct pjsua_callback pj_pool_t *pool, const pjmedia_sdp_session *rem_sdp); + /** + * Notify application when the framework needs to send an ACK request + * after it receives an incoming 2xx response for INVITE (e.g. when the + * 2xx response carries a late SDP offer). If this callback is + * implemented, the application is responsible for creating and sending + * the ACK request itself, using #pjsua_call_send_ack(), which allows an + * SDP answer to be attached to the ACK. This also allows the application + * to delay sending the ACK, e.g. while waiting for the SDP answer to be + * generated by 3PCC scenarios. + * + * @param call_id The call index. + * @param rdata The received 2xx response that triggered the need to + * send an ACK request. + */ + void (*on_call_send_ack)(pjsua_call_id call_id, + pjsip_rx_data *rdata); + /** * Notify application when an audio media session is about to be created * (as opposed to on_stream_created() and on_stream_created2() which are @@ -6965,6 +6982,28 @@ pjsua_call_answer_with_sdp(pjsua_call_id call_id, const pjsua_msg_data *msg_data); +/** + * Create and send an ACK request for the 2xx response, optionally with an + * SDP answer \a sdp attached to it. Application would normally use this + * function from within the #pjsua_callback.on_call_send_ack() callback, + * to manually take over the ACK transmission, e.g. to attach an SDP answer + * to the ACK for a late SDP offer received in the 2xx response. + * + * @param call_id Call identification. + * @param cseq CSeq of the 2xx response that triggered the need to + * send an ACK request; normally this is the CSeq of + * the \a rdata received in the on_call_send_ack() + * callback. + * @param sdp Optional SDP answer to be attached to the ACK + * request. May be NULL if no SDP body is required. + * + * @return PJ_SUCCESS on success, or the appropriate error code. + */ +PJ_DECL(pj_status_t) pjsua_call_send_ack(pjsua_call_id call_id, + int cseq, + const pjmedia_sdp_session *sdp); + + /** * Hangup call by using method that is appropriate according to the * call state. This function is different than answering the call with diff --git a/pjsip/include/pjsua2/call.hpp b/pjsip/include/pjsua2/call.hpp index 7dce2c0125..7008b5f7eb 100644 --- a/pjsip/include/pjsua2/call.hpp +++ b/pjsip/include/pjsua2/call.hpp @@ -833,6 +833,18 @@ struct OnCallSdpCreatedParam SdpSession remSdp; }; +/** + * This structure contains parameters for Call::onCallSendAck() callback. + */ +struct OnCallSendAckParam +{ + /** + * The 2xx response for INVITE that triggered the need to send an ACK + * request. + */ + SipRxData rdata; +}; + /** * This structure contains parameters for Call::onStreamPreCreate() * callback. @@ -1770,6 +1782,24 @@ class Call */ void hangup(const CallOpParam &prm) PJSUA2_THROW(Error); + /** + * Create and send an ACK request for the 2xx response, optionally with + * an SDP answer \a sdp attached to it. Application would normally call + * this from within onCallSendAck(), to manually take over the ACK + * transmission, e.g. to attach an SDP answer to the ACK for a late + * SDP offer received in the 2xx response. + * + * @param cseq CSeq of the 2xx response that triggered the need to + * send an ACK request; normally this is the CSeq of + * the \a rdata received in the onCallSendAck() + * callback. + * @param sdp Optional SDP answer to be attached to the ACK + * request. If its wholeSdp is empty, no SDP body + * will be attached. + */ + void sendAck(int cseq, const SdpSession &sdp = SdpSession()) + PJSUA2_THROW(Error); + /** * Put the specified call on hold. This will send re-INVITE with the * appropriate SDP to inform remote that the call is being put on hold. @@ -2131,6 +2161,20 @@ class Call virtual void onCallSdpCreated(OnCallSdpCreatedParam &prm) { PJ_UNUSED_ARG(prm); } + /** + * Notify application when the framework needs to send an ACK request + * after it receives an incoming 2xx response for INVITE (e.g. when the + * 2xx response carries a late SDP offer). The default implementation + * simply sends the ACK without any SDP answer, using sendAck(). + * Application can override this callback to attach an SDP answer to + * the ACK (e.g. for a late offer scenario), or to delay sending the + * ACK, by calling sendAck() itself. + * + * @param prm Callback parameter. + */ + virtual void onCallSendAck(OnCallSendAckParam &prm) + { sendAck(static_cast(prm.rdata.pjRxData)->msg_info.cseq->cseq); } + /** * Notify application when an audio media session is about to be created * (as opposed to onStreamCreated(), which is called *after* the session diff --git a/pjsip/include/pjsua2/endpoint.hpp b/pjsip/include/pjsua2/endpoint.hpp index abdf31c41a..8e1e7ba5c9 100644 --- a/pjsip/include/pjsua2/endpoint.hpp +++ b/pjsip/include/pjsua2/endpoint.hpp @@ -2432,6 +2432,8 @@ class Endpoint pjmedia_sdp_session *sdp, pj_pool_t *pool, const pjmedia_sdp_session *rem_sdp); + static void on_call_send_ack(pjsua_call_id call_id, + pjsip_rx_data *rdata); static void on_stream_precreate(pjsua_call_id call_id, pjsua_on_stream_precreate_param *param); static void on_stream_created2(pjsua_call_id call_id, diff --git a/pjsip/src/pjsip-ua/sip_inv.c b/pjsip/src/pjsip-ua/sip_inv.c index ff99139847..3ad5cf2f19 100644 --- a/pjsip/src/pjsip-ua/sip_inv.c +++ b/pjsip/src/pjsip-ua/sip_inv.c @@ -2737,7 +2737,10 @@ static pj_status_t inv_check_sdp_in_incoming_msg( pjsip_inv_session *inv, * * See also tickets #657, #1644, #1764, and #2123 for more info. */ - if (tsx_inv_data->sdp_done) { + if (tsx_inv_data->sdp_done && + !(tsx->role == PJSIP_ROLE_UAC && + !tsx_inv_data->has_sdp && + rdata->msg_info.msg->line.status.code/100 == 2)) { pj_str_t res_tag; int st_code; @@ -3180,6 +3183,56 @@ PJ_DEF(pj_status_t) pjsip_inv_answer( pjsip_inv_session *inv, last_res->msg->body = NULL; } + /* An asynchronous confirmed UAS re-INVITE without an SDP offer is + * answered with a new local offer in the 2xx response. */ + if (st_code/100 == 2 && inv->state == PJSIP_INV_STATE_CONFIRMED && + inv->invite_tsx && inv->neg && + pjmedia_sdp_neg_get_state(inv->neg) == PJMEDIA_SDP_NEG_STATE_DONE) + { + struct tsx_inv_data *tsx_inv_data; + pjmedia_sdp_session *offer = (pjmedia_sdp_session*)local_sdp; + const pjmedia_sdp_session *local_offer = NULL; + + tsx_inv_data = (struct tsx_inv_data*) + inv->invite_tsx->mod_data[mod_inv.mod.id]; + if (!tsx_inv_data) { + tsx_inv_data = PJ_POOL_ZALLOC_T(inv->invite_tsx->pool, + struct tsx_inv_data); + tsx_inv_data->inv = inv; + tsx_inv_data->has_sdp = PJ_FALSE; + inv->invite_tsx->mod_data[mod_inv.mod.id] = tsx_inv_data; + } + if (tsx_inv_data && !tsx_inv_data->has_sdp) { + if (!offer && mod_inv.cb.on_create_offer) + (*mod_inv.cb.on_create_offer)(inv, &offer); + + if (offer) { + status = pjmedia_sdp_neg_modify_local_offer2( + inv->pool_prov, inv->neg, + inv->sdp_neg_flags, offer); + } else { + status = pjmedia_sdp_neg_send_local_offer( + inv->pool_prov, inv->neg, &local_offer); + } + + if (status == PJ_SUCCESS && !local_offer) + status = pjmedia_sdp_neg_get_neg_local(inv->neg, &local_offer); + if (status == PJ_SUCCESS && local_offer) + last_res->msg->body = create_sdp_body(last_res->pool, local_offer); + else if (status == PJ_SUCCESS) + status = PJMEDIA_SDPNEG_EINSTATE; + + if (status != PJ_SUCCESS) { + pjsip_tx_data_dec_ref(last_res); + pjsip_tx_data_dec_ref(last_res); + goto on_return; + } + + /* local_sdp has already been installed as the offer above. */ + local_sdp = NULL; + } + } + /* Process SDP in answer */ status = process_answer(inv, st_code, last_res, local_sdp); if (status != PJ_SUCCESS) { diff --git a/pjsip/src/pjsua-lib/pjsua_call.c b/pjsip/src/pjsua-lib/pjsua_call.c index adde9cd50b..10d6c263d1 100644 --- a/pjsip/src/pjsua-lib/pjsua_call.c +++ b/pjsip/src/pjsua-lib/pjsua_call.c @@ -124,6 +124,14 @@ static pj_bool_t pjsua_call_on_uac_tsx_terminate_session( pjsip_transaction *tsx, pjsip_event *e); +/* + * This callback is called by the invite session framework when it needs to + * send an ACK request for the 2xx response of INVITE, and the application + * has asked to handle the ACK transmission manually. + */ +static void pjsua_call_on_send_ack(pjsip_inv_session *inv, + pjsip_rx_data *rdata); + #if PJSUA_HAS_SIPREC /* * SIPREC metadata verification handler. @@ -273,6 +281,9 @@ pj_status_t pjsua_call_subsys_init(const pjsua_config *cfg) inv_cb.on_tsx_state_changed = &pjsua_call_on_tsx_state_changed; inv_cb.on_redirected = &pjsua_call_on_redirected; inv_cb.on_rx_reinvite = &pjsua_call_on_rx_reinvite; + if (pjsua_var.ua_cfg.cb.on_call_send_ack) { + inv_cb.on_send_ack = &pjsua_call_on_send_ack; + } if (pjsua_var.ua_cfg.cb.on_call_tsx_terminate_session) { inv_cb.on_uac_tsx_terminate_session = &pjsua_call_on_uac_tsx_terminate_session; @@ -3400,7 +3411,9 @@ PJ_DEF(pj_status_t) pjsua_call_answer2(pjsua_call_id call_id, * Or if initial answer is not sent yet, we will answer the call after * initial answer is sent (see #1923). */ - if (call->med_ch_cb || !call->inv->last_answer) { + if (call->med_ch_cb || + (call->inv->state < PJSIP_INV_STATE_CONFIRMED && + !call->inv->last_answer)) { struct call_answer *answer; PJ_LOG(4,(THIS_FILE, "Pending answering call %d upon completion " @@ -3557,6 +3570,56 @@ pjsua_call_answer_with_sdp(pjsua_call_id call_id, } +/* + * Create and send an ACK request for the 2xx response, optionally with an + * SDP answer \a sdp attached to it. + */ +PJ_DEF(pj_status_t) pjsua_call_send_ack(pjsua_call_id call_id, + int cseq, + const pjmedia_sdp_session *sdp) +{ + pjsua_call *call; + pjsip_dialog *dlg = NULL; + pjsip_tx_data *tdata; + pj_status_t status; + + PJ_ASSERT_RETURN(call_id>=0 && call_id<(int)pjsua_var.ua_cfg.max_calls, + PJ_EINVAL); + + PJ_LOG(4,(THIS_FILE, "Sending ACK for call %d", call_id)); + pj_log_push_indent(); + + status = acquire_call("pjsua_call_send_ack()", call_id, &call, &dlg); + if (status != PJ_SUCCESS) + goto on_return; + + if (sdp) { + status = pjsip_inv_set_sdp_answer(call->inv, sdp); + if (status != PJ_SUCCESS) { + pjsua_perror(THIS_FILE, "Unable to set SDP answer for ACK", + status); + goto on_return; + } + } + + status = pjsip_inv_create_ack(call->inv, cseq, &tdata); + if (status != PJ_SUCCESS) { + pjsua_perror(THIS_FILE, "Unable to create ACK", status); + goto on_return; + } + + status = pjsip_inv_send_msg(call->inv, tdata); + if (status != PJ_SUCCESS) { + pjsua_perror(THIS_FILE, "Unable to send ACK", status); + } + +on_return: + if (dlg) pjsip_dlg_dec_lock(dlg); + pj_log_pop_indent(); + return status; +} + + static pj_status_t call_inv_end_session(pjsua_call *call, unsigned code, const pj_str_t *reason, @@ -8072,6 +8135,31 @@ static pj_bool_t pjsua_call_on_uac_tsx_terminate_session( tsx, e); } +/* + * This callback is called by the invite session framework when it needs to + * send an ACK request for the 2xx response of INVITE, and the application + * has asked to handle the ACK transmission manually (by implementing + * on_call_send_ack()). + */ +static void pjsua_call_on_send_ack(pjsip_inv_session *inv, + pjsip_rx_data *rdata) +{ + pjsua_call *call = (pjsua_call*) inv->dlg->mod_data[pjsua_var.mod.id]; + + if (!call) + return; + + pj_log_push_indent(); + + if (pjsua_var.ua_cfg.cb.on_call_send_ack) { + (*pjsua_var.ua_cfg.cb.on_call_send_ack)(call->index, rdata); + } else { + pjsua_call_send_ack(call->index, rdata->msg_info.cseq->cseq, NULL); + } + + pj_log_pop_indent(); +} + #if defined(PJSUA_MEDIA_HAS_PJMEDIA) && PJSUA_MEDIA_HAS_PJMEDIA != 0 /* diff --git a/pjsip/src/pjsua2/call.cpp b/pjsip/src/pjsua2/call.cpp index 8d227d9ec2..df3103e52e 100644 --- a/pjsip/src/pjsua2/call.cpp +++ b/pjsip/src/pjsua2/call.cpp @@ -810,6 +810,41 @@ void Call::hangup(const CallOpParam &prm) PJSUA2_THROW(Error) param.p_msg_data) ); } +void Call::sendAck(int cseq, const SdpSession &sdp) + PJSUA2_THROW(Error) +{ + pj_pool_t *tmp_pool = NULL; + pjmedia_sdp_session *pj_sdp = NULL; + pj_status_t status = PJ_SUCCESS; + + if (!sdp.wholeSdp.empty()) { + tmp_pool = pjsua_pool_create("tmp-sendack", 2048, 512); + if (!tmp_pool) { + PJSUA2_RAISE_ERROR2(PJ_ENOMEM, "Call::sendAck()"); + } + + pj_str_t dup_pj_sdp; + pj_str_t pj_sdp_str = {(char*)sdp.wholeSdp.c_str(), + (pj_ssize_t)sdp.wholeSdp.size()}; + + pj_strdup(tmp_pool, &dup_pj_sdp, &pj_sdp_str); + status = pjmedia_sdp_parse(tmp_pool, dup_pj_sdp.ptr, + dup_pj_sdp.slen, &pj_sdp); + if (status != PJ_SUCCESS) { + pj_pool_release(tmp_pool); + PJSUA2_RAISE_ERROR2(status, "Call::sendAck()"); + } + } + + status = pjsua_call_send_ack(id, cseq, pj_sdp); + + if (tmp_pool) { + pj_pool_release(tmp_pool); + } + + PJSUA2_CHECK_RAISE_ERROR2(status, "Call::sendAck()"); +} + void Call::setHold(const CallOpParam &prm) PJSUA2_THROW(Error) { call_param param(prm.txOption, prm.opt, prm.reason); diff --git a/pjsip/src/pjsua2/endpoint.cpp b/pjsip/src/pjsua2/endpoint.cpp index 27032bc76c..5fa179f8cf 100644 --- a/pjsip/src/pjsua2/endpoint.cpp +++ b/pjsip/src/pjsua2/endpoint.cpp @@ -1572,6 +1572,20 @@ void Endpoint::on_call_sdp_created(pjsua_call_id call_id, } } +void Endpoint::on_call_send_ack(pjsua_call_id call_id, + pjsip_rx_data *rdata) +{ + Call *call = Call::lookup(call_id); + if (!call) { + return; + } + + OnCallSendAckParam prm; + prm.rdata.fromPj(*rdata); + + call->onCallSendAck(prm); +} + void Endpoint::on_stream_precreate(pjsua_call_id call_id, pjsua_on_stream_precreate_param *param) { @@ -2379,6 +2393,7 @@ void Endpoint::libInit(const EpConfig &prmEpConfig) PJSUA2_THROW(Error) = &Endpoint::on_call_tsx_terminate_session; ua_cfg.cb.on_call_media_state = &Endpoint::on_call_media_state; ua_cfg.cb.on_call_sdp_created = &Endpoint::on_call_sdp_created; + ua_cfg.cb.on_call_send_ack = &Endpoint::on_call_send_ack; ua_cfg.cb.on_stream_precreate = &Endpoint::on_stream_precreate; ua_cfg.cb.on_stream_created2 = &Endpoint::on_stream_created2; ua_cfg.cb.on_stream_destroyed = &Endpoint::on_stream_destroyed; diff --git a/tests/pjsua/runall.py b/tests/pjsua/runall.py index 84170fa7a1..1f4d226121 100644 --- a/tests/pjsua/runall.py +++ b/tests/pjsua/runall.py @@ -53,6 +53,10 @@ "alt-pjsua-uas-multiple-amr", "alt-pjsua-uas-media-app-managed", "alt-pjsua-uas-early-update-app-managed", + "alt-pjsua-uac-late-offer", + "alt-pjsua-uas-late-offer", + "alt-pjsua-uac-late-offer-dummy-codecs", + "alt-pjsua-uas-late-offer-dummy-codecs", ] # Exclude scripts-sipp/uac-reinvite-bad-via-branch on MacOS due to unreliable result diff --git a/tests/pjsua/scripts-sipp/alt-pjsua-uac-late-offer-dummy-codecs.py b/tests/pjsua/scripts-sipp/alt-pjsua-uac-late-offer-dummy-codecs.py new file mode 100644 index 0000000000..29e829000c --- /dev/null +++ b/tests/pjsua/scripts-sipp/alt-pjsua-uac-late-offer-dummy-codecs.py @@ -0,0 +1,25 @@ +# +# Test driver for alt-pjsua-uac-late-offer.xml +# +# alt_pjsua acts as UAC and sends an SDP-less INVITE to SIPp (UAS). SIPp +# returns an SDP offer in its 200 OK, which alt_pjsua answers in the ACK. SIPp +# then terminates the dialog with BYE. The call should reach STATE_CONFIRMED. +# This scenario tests the handling of late SDP offers with dummy codecs. +# +# Run with alt_pjsua: +# cd tests/pjsua && python3 run.py \ +# --exe ../../pjsip-apps/src/3rdparty_media_sample/alt_pjsua \ +# mod_sipp.py scripts-sipp/alt-pjsua-uac-late-offer-dummy-codecs.xml +# +import inc_const as const + +# No URI in the startup arguments: cli_main() must first configure the telnet +# log redirect. toggle_sdp_offer makes the outgoing INVITE carry no SDP offer. +PJSUA = ["--null-audio --max-calls=1 --no-tcp --dummy-codecs"] + +PJSUA_CLI_EXPECTS = [ + [0, "", "toggle_sdp_offer"], + [0, "", "call new $SIPP_URI"], + # Leave the call running so SIPp can terminate it with BYE. + [0, const.STATE_CONFIRMED, ""], +] diff --git a/tests/pjsua/scripts-sipp/alt-pjsua-uac-late-offer-dummy-codecs.xml b/tests/pjsua/scripts-sipp/alt-pjsua-uac-late-offer-dummy-codecs.xml new file mode 100644 index 0000000000..740fd7e11a --- /dev/null +++ b/tests/pjsua/scripts-sipp/alt-pjsua-uac-late-offer-dummy-codecs.xml @@ -0,0 +1,162 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ;tag=[call_number] + To: <[$7]>;tag=[$8] + Call-ID: [call_id] + CSeq: 2 INVITE + Contact: sip:sipp@[local_ip]:[local_port] + Max-Forwards: 70 + Content-Length: 0 + + ]]> + + + + + + + + + + + + + + + + + + + ;tag=[call_number] + To: <[$7]>;tag=[$8] + Call-ID: [call_id] + CSeq: 2 ACK + Contact: sip:sipp@[local_ip]:[local_port] + Max-Forwards: 70 + Content-Length: [len] + +v=0 +o=sipp 1234567890 1234567890 IN IP[local_ip_type] [local_ip] +s=- +c=IN IP[media_ip_type] [media_ip] +t=0 0 +m=audio [media_port] RTP/AVP 0 +a=rtpmap:0 PCMU/8000 +m=video [media_port+2] RTP/AVP 96 +a=rtpmap:96 H263-1998/90000 + + ]]> + + + + + + ;tag=[call_number] + To: <[$7]>;tag=[$8] + Call-ID: [call_id] + CSeq: 3 BYE + Contact: sip:sipp@[local_ip]:[local_port] + Max-Forwards: 70 + Content-Length: 0 + + ]]> + + + + + + + + + diff --git a/tests/pjsua/scripts-sipp/alt-pjsua-uac-late-offer.py b/tests/pjsua/scripts-sipp/alt-pjsua-uac-late-offer.py new file mode 100644 index 0000000000..434df6b38b --- /dev/null +++ b/tests/pjsua/scripts-sipp/alt-pjsua-uac-late-offer.py @@ -0,0 +1,30 @@ +# +# Test driver for alt-pjsua-uac-late-offer.xml +# +# alt_pjsua acts as UAC and sends an SDP-less INVITE to SIPp (UAS). SIPp +# returns an SDP offer in its 200 OK, which alt_pjsua answers in the ACK. SIPp +# then terminates the dialog with BYE. The call should reach STATE_CONFIRMED. +# +# Run with alt_pjsua: +# cd tests/pjsua && python3 run.py \ +# --exe ../../pjsip-apps/src/3rdparty_media_sample/alt_pjsua \ +# mod_sipp.py scripts-sipp/alt-pjsua-uac-late-offer.xml +# +import inc_const as const + +# No URI in the startup arguments: cli_main() must first configure the telnet +# log redirect. toggle_sdp_offer makes the outgoing INVITE carry no SDP offer. +PJSUA = [ + "--null-audio --max-calls=1 --no-tcp " + "--custom-sdp \"v=0\\r\\no=- 1234567890 1234567890 IN IP4 127.0.0.1" + "\\r\\ns=-\\r\\nc=IN IP4 127.0.0.1\\r\\nt=0 0" + "\\r\\nm=audio 4000 RTP/AVP 0\\r\\na=rtpmap:0 PCMU/8000" + "\\r\\nm=video 4002 RTP/AVP 96\\r\\na=rtpmap:96 H263-1998/90000\"" +] + +PJSUA_CLI_EXPECTS = [ + [0, "", "toggle_sdp_offer"], + [0, "", "call new $SIPP_URI"], + # Leave the call running so SIPp can terminate it with BYE. + [0, const.STATE_CONFIRMED, ""], +] diff --git a/tests/pjsua/scripts-sipp/alt-pjsua-uac-late-offer.xml b/tests/pjsua/scripts-sipp/alt-pjsua-uac-late-offer.xml new file mode 100644 index 0000000000..4b922040b8 --- /dev/null +++ b/tests/pjsua/scripts-sipp/alt-pjsua-uac-late-offer.xml @@ -0,0 +1,161 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ;tag=[call_number] + To: <[$7]>;tag=[$8] + Call-ID: [call_id] + CSeq: 2 INVITE + Contact: sip:sipp@[local_ip]:[local_port] + Max-Forwards: 70 + Content-Length: 0 + + ]]> + + + + + + + + + + + + + + + + + + + ;tag=[call_number] + To: <[$7]>;tag=[$8] + Call-ID: [call_id] + CSeq: 2 ACK + Contact: sip:sipp@[local_ip]:[local_port] + Max-Forwards: 70 + Content-Length: [len] + +v=0 +o=sipp 1234567890 1234567890 IN IP[local_ip_type] [local_ip] +s=- +c=IN IP[media_ip_type] [media_ip] +t=0 0 +m=audio [media_port] RTP/AVP 0 +a=rtpmap:0 PCMU/8000 +m=video [media_port+2] RTP/AVP 96 +a=rtpmap:96 H263-1998/90000 + + ]]> + + + + + + ;tag=[call_number] + To: <[$7]>;tag=[$8] + Call-ID: [call_id] + CSeq: 3 BYE + Contact: sip:sipp@[local_ip]:[local_port] + Max-Forwards: 70 + Content-Length: 0 + + ]]> + + + + + + + + + diff --git a/tests/pjsua/scripts-sipp/alt-pjsua-uas-late-offer-dummy-codecs.py b/tests/pjsua/scripts-sipp/alt-pjsua-uas-late-offer-dummy-codecs.py new file mode 100644 index 0000000000..27524edd70 --- /dev/null +++ b/tests/pjsua/scripts-sipp/alt-pjsua-uas-late-offer-dummy-codecs.py @@ -0,0 +1,33 @@ +# +# Test driver for alt-pjsua-uas-late-offer.xml +# +# alt_pjsua acts as UAS and auto-answers an SDP-less INVITE from SIPp +# (UAC). alt_pjsua has no offer to answer, so it puts its own SDP offer +# in the 200 OK; SIPp answers it in the ACK. The call should reach +# STATE_CONFIRMED. +# This scenario tests the handling of late SDP offers with dummy codecs. +# +# Run with standard pjsua: +# cd tests/pjsua && python3 run.py mod_sipp.py \ +# scripts-sipp/alt-pjsua-uas-late-offer.xml +# +# Run with alt_pjsua: +# cd tests/pjsua && python3 run.py \ +# --exe ../../pjsip-apps/src/3rdparty_media_sample/alt_pjsua \ +# mod_sipp.py scripts-sipp/alt-pjsua-uas-late-offer-dummy-codecs.xml +# +import inc_const as const + +# pjsua instance: UAS, auto-answers the SDP-less INVITE with its own offer. +PJSUA = ["--null-audio --max-calls=1 --no-tcp --auto-answer=200 --dummy-codecs"] + +PJSUA_CLI_EXPECTS = [ + # Verify the SDP answer SIPp sends in its ACK is received by alt_pjsua. + # Codec check only (no IP/port) for environment independence. + [0, r"v=0", ""], + [0, r"s=-", ""], + [0, r"t=0 0", ""], + [0, r"a=rtpmap:0 PCMU/8000", ""], + [0, r"a=rtpmap:96 H263-1998/90000", ""], + [0, const.STATE_CONFIRMED, ""], +] diff --git a/tests/pjsua/scripts-sipp/alt-pjsua-uas-late-offer-dummy-codecs.xml b/tests/pjsua/scripts-sipp/alt-pjsua-uas-late-offer-dummy-codecs.xml new file mode 100644 index 0000000000..4860c18c15 --- /dev/null +++ b/tests/pjsua/scripts-sipp/alt-pjsua-uas-late-offer-dummy-codecs.xml @@ -0,0 +1,165 @@ + + + + + + + + + ;tag=[call_number] + To: sut + Call-ID: [call_id] + CSeq: 1 INVITE + Contact: sip:sipp@[local_ip]:[local_port] + Max-Forwards: 70 + Subject: alt_pjsua UAS late SDP test + Content-Length: 0 + + ]]> + + + + + + + + + + + + + + + + + + + + ;tag=[call_number] + To: sut [peer_tag_param] + Call-ID: [call_id] + CSeq: 1 ACK + Contact: sip:sipp@[local_ip]:[local_port] + Max-Forwards: 70 + Content-Type: application/sdp + Content-Length: [len] + +v=0 +o=sipp 1234567890 1234567890 IN IP[local_ip_type] [local_ip] +s=- +c=IN IP[media_ip_type] [media_ip] +t=0 0 +m=audio [media_port] RTP/AVP 0 +a=rtpmap:0 PCMU/8000 +m=video [media_port+2] RTP/AVP 96 +a=rtpmap:96 H263-1998/90000 + + ]]> + + + + + + ;tag=[call_number] + To: sut [peer_tag_param] + Call-ID: [call_id] + CSeq: 2 INVITE + Contact: sip:sipp@[local_ip]:[local_port] + Max-Forwards: 70 + Content-Length: 0 + + ]]> + + + + + + + + + + + + + + + + + + + ;tag=[call_number] + To: sut [peer_tag_param] + Call-ID: [call_id] + CSeq: 2 ACK + Contact: sip:sipp@[local_ip]:[local_port] + Max-Forwards: 70 + Content-Type: application/sdp + Content-Length: [len] + +v=0 +o=sipp 1234567890 1234567890 IN IP[local_ip_type] [local_ip] +s=- +c=IN IP[media_ip_type] [media_ip] +t=0 0 +m=audio [media_port] RTP/AVP 0 +a=rtpmap:0 PCMU/8000 +m=video [media_port+2] RTP/AVP 96 +a=rtpmap:96 H263-1998/90000 + + ]]> + + + + + + ;tag=[call_number] + To: sut [peer_tag_param] + Call-ID: [call_id] + CSeq: 3 BYE + Contact: sip:sipp@[local_ip]:[local_port] + Max-Forwards: 70 + Content-Length: 0 + + ]]> + + + + + + + + + diff --git a/tests/pjsua/scripts-sipp/alt-pjsua-uas-late-offer.py b/tests/pjsua/scripts-sipp/alt-pjsua-uas-late-offer.py new file mode 100644 index 0000000000..a4762a0b6d --- /dev/null +++ b/tests/pjsua/scripts-sipp/alt-pjsua-uas-late-offer.py @@ -0,0 +1,38 @@ +# +# Test driver for alt-pjsua-uas-late-offer.xml +# +# alt_pjsua acts as UAS and auto-answers an SDP-less INVITE from SIPp +# (UAC). alt_pjsua has no offer to answer, so it puts its own SDP offer +# in the 200 OK; SIPp answers it in the ACK. The call should reach +# STATE_CONFIRMED. +# +# Run with standard pjsua: +# cd tests/pjsua && python3 run.py mod_sipp.py \ +# scripts-sipp/alt-pjsua-uas-late-offer.xml +# +# Run with alt_pjsua: +# cd tests/pjsua && python3 run.py \ +# --exe ../../pjsip-apps/src/3rdparty_media_sample/alt_pjsua \ +# mod_sipp.py scripts-sipp/alt-pjsua-uas-late-offer.xml +# +import inc_const as const + +# pjsua instance: UAS, auto-answers the SDP-less INVITE with its own offer. +PJSUA = [ + "--null-audio --max-calls=1 --no-tcp --rtp-port 4002 --auto-answer=200 " + "--custom-sdp \"v=0\\r\\no=- 1234567890 1234567890 IN IP4 127.0.0.1" + "\\r\\ns=-\\r\\nc=IN IP4 127.0.0.1\\r\\nt=0 0" + "\\r\\nm=audio 4002 RTP/AVP 0\\r\\na=rtpmap:0 PCMU/8000" + "\\r\\nm=video 4004 RTP/AVP 96\\r\\na=rtpmap:96 H263-1998/90000\"" +] + +PJSUA_CLI_EXPECTS = [ + # Verify the SDP answer SIPp sends in its ACK is received by alt_pjsua. + # Codec check only (no IP/port) for environment independence. + [0, r"v=0", ""], + [0, r"s=-", ""], + [0, r"t=0 0", ""], + [0, r"a=rtpmap:0 PCMU/8000", ""], + [0, r"a=rtpmap:96 H263-1998/90000", ""], + [0, const.STATE_CONFIRMED, ""], +] diff --git a/tests/pjsua/scripts-sipp/alt-pjsua-uas-late-offer.xml b/tests/pjsua/scripts-sipp/alt-pjsua-uas-late-offer.xml new file mode 100644 index 0000000000..856f43f022 --- /dev/null +++ b/tests/pjsua/scripts-sipp/alt-pjsua-uas-late-offer.xml @@ -0,0 +1,164 @@ + + + + + + + + + ;tag=[call_number] + To: sut + Call-ID: [call_id] + CSeq: 1 INVITE + Contact: sip:sipp@[local_ip]:[local_port] + Max-Forwards: 70 + Subject: alt_pjsua UAS late SDP test + Content-Length: 0 + + ]]> + + + + + + + + + + + + + + + + + + + + ;tag=[call_number] + To: sut [peer_tag_param] + Call-ID: [call_id] + CSeq: 1 ACK + Contact: sip:sipp@[local_ip]:[local_port] + Max-Forwards: 70 + Content-Type: application/sdp + Content-Length: [len] + +v=0 +o=sipp 1234567890 1234567890 IN IP[local_ip_type] [local_ip] +s=- +c=IN IP[media_ip_type] [media_ip] +t=0 0 +m=audio [media_port] RTP/AVP 0 +a=rtpmap:0 PCMU/8000 +m=video [media_port+2] RTP/AVP 96 +a=rtpmap:96 H263-1998/90000 + + ]]> + + + + + + ;tag=[call_number] + To: sut [peer_tag_param] + Call-ID: [call_id] + CSeq: 2 INVITE + Contact: sip:sipp@[local_ip]:[local_port] + Max-Forwards: 70 + Content-Length: 0 + + ]]> + + + + + + + + + + + + + + + + + + + ;tag=[call_number] + To: sut [peer_tag_param] + Call-ID: [call_id] + CSeq: 2 ACK + Contact: sip:sipp@[local_ip]:[local_port] + Max-Forwards: 70 + Content-Type: application/sdp + Content-Length: [len] + +v=0 +o=sipp 1234567890 1234567890 IN IP[local_ip_type] [local_ip] +s=- +c=IN IP[media_ip_type] [media_ip] +t=0 0 +m=audio [media_port] RTP/AVP 0 +a=rtpmap:0 PCMU/8000 +m=video [media_port+2] RTP/AVP 96 +a=rtpmap:96 H263-1998/90000 + + ]]> + + + + + + ;tag=[call_number] + To: sut [peer_tag_param] + Call-ID: [call_id] + CSeq: 3 BYE + Contact: sip:sipp@[local_ip]:[local_port] + Max-Forwards: 70 + Content-Length: 0 + + ]]> + + + + + + + + + From 77ed514962cadcdeb6b2e0a4c6a47a84f7019c18 Mon Sep 17 00:00:00 2001 From: LEOPOLD Otakar Date: Mon, 21 Sep 2026 15:10:03 +0200 Subject: [PATCH 2/8] Fixes ua_cfg.cb.on_call_send_ack qurantee that it sends ACK (comment 1,2,3) Default implemetation of void onCallSendAck(OnCallSendAckParam &prm) does not throw exception. (comment 4) The application's rejection code is not ignored (comment 5) Fixed ABI break (comment 6) Reverted condition changes because it is not necessary for our cases(comment 7,8) Fixed guards in pjsua_call_send_ack() (comment 9) Removed dead assignment (comment 11) Instead sending stale sdp offer PJ_EINVALIDOP is returned when on_create_offer() will not return sdp(comment 10). When send 100 in async 200 OK branch fails then transaction is terminated (comment 12). --- pjsip/include/pjsua-lib/pjsua.h | 37 +++++++++------- pjsip/include/pjsua2/call.hpp | 12 ++++- pjsip/include/pjsua2/endpoint.hpp | 2 +- pjsip/src/pjsip-ua/sip_inv.c | 31 ++++++------- pjsip/src/pjsua-lib/pjsua_call.c | 73 +++++++++++++++++++++---------- pjsip/src/pjsua2/endpoint.cpp | 7 +-- 6 files changed, 102 insertions(+), 60 deletions(-) diff --git a/pjsip/include/pjsua-lib/pjsua.h b/pjsip/include/pjsua-lib/pjsua.h index d40e09a19d..255bc2e194 100644 --- a/pjsip/include/pjsua-lib/pjsua.h +++ b/pjsip/include/pjsua-lib/pjsua.h @@ -1496,23 +1496,6 @@ typedef struct pjsua_callback pj_pool_t *pool, const pjmedia_sdp_session *rem_sdp); - /** - * Notify application when the framework needs to send an ACK request - * after it receives an incoming 2xx response for INVITE (e.g. when the - * 2xx response carries a late SDP offer). If this callback is - * implemented, the application is responsible for creating and sending - * the ACK request itself, using #pjsua_call_send_ack(), which allows an - * SDP answer to be attached to the ACK. This also allows the application - * to delay sending the ACK, e.g. while waiting for the SDP answer to be - * generated by 3PCC scenarios. - * - * @param call_id The call index. - * @param rdata The received 2xx response that triggered the need to - * send an ACK request. - */ - void (*on_call_send_ack)(pjsua_call_id call_id, - pjsip_rx_data *rdata); - /** * Notify application when an audio media session is about to be created * (as opposed to on_stream_created() and on_stream_created2() which are @@ -2496,6 +2479,26 @@ typedef struct pjsua_callback */ void (*on_auth_challenge)(pjsua_on_auth_challenge_param *param); + /** + * Notify application when the framework needs to send an ACK request + * after it receives an incoming 2xx response for INVITE (e.g. when the + * 2xx response carries a late SDP offer). If this callback is + * implemented, the application is responsible for creating and sending + * the ACK request itself, using #pjsua_call_send_ack(), which allows an + * SDP answer to be attached to the ACK. This also allows the application + * to delay sending the ACK, e.g. while waiting for the SDP answer to be + * generated by 3PCC scenarios. + * + * @param call_id The call index. + * @param rdata The received 2xx response that triggered the need to + * send an ACK request. + * @return PJ_TRUE to suppress the automatic ack request sending. + * PJ_FALSE (default) to keep current behavior, automatic + * ACK sending by the framework will proceed as usual. + */ + pj_bool_t (*on_call_send_ack)(pjsua_call_id call_id, + pjsip_rx_data *rdata); + } pjsua_callback; diff --git a/pjsip/include/pjsua2/call.hpp b/pjsip/include/pjsua2/call.hpp index 7008b5f7eb..731e2eb72f 100644 --- a/pjsip/include/pjsua2/call.hpp +++ b/pjsip/include/pjsua2/call.hpp @@ -843,6 +843,16 @@ struct OnCallSendAckParam * request. */ SipRxData rdata; + + /** + * Output: application sets this to true to suppress the automatic + * session termination and keep the call alive. Default is false, i.e. + * the library will terminate the call as per default behavior. + */ + bool suppressSendAck; + + OnCallSendAckParam() : suppressSendAck(false) + {} }; /** @@ -2173,7 +2183,7 @@ class Call * @param prm Callback parameter. */ virtual void onCallSendAck(OnCallSendAckParam &prm) - { sendAck(static_cast(prm.rdata.pjRxData)->msg_info.cseq->cseq); } + { PJ_UNUSED_ARG(prm); } /** * Notify application when an audio media session is about to be created diff --git a/pjsip/include/pjsua2/endpoint.hpp b/pjsip/include/pjsua2/endpoint.hpp index 8e1e7ba5c9..8be71bd69d 100644 --- a/pjsip/include/pjsua2/endpoint.hpp +++ b/pjsip/include/pjsua2/endpoint.hpp @@ -2432,7 +2432,7 @@ class Endpoint pjmedia_sdp_session *sdp, pj_pool_t *pool, const pjmedia_sdp_session *rem_sdp); - static void on_call_send_ack(pjsua_call_id call_id, + static pj_bool_t on_call_send_ack(pjsua_call_id call_id, pjsip_rx_data *rdata); static void on_stream_precreate(pjsua_call_id call_id, pjsua_on_stream_precreate_param *param); diff --git a/pjsip/src/pjsip-ua/sip_inv.c b/pjsip/src/pjsip-ua/sip_inv.c index 3ad5cf2f19..d80e7b806e 100644 --- a/pjsip/src/pjsip-ua/sip_inv.c +++ b/pjsip/src/pjsip-ua/sip_inv.c @@ -2737,10 +2737,7 @@ static pj_status_t inv_check_sdp_in_incoming_msg( pjsip_inv_session *inv, * * See also tickets #657, #1644, #1764, and #2123 for more info. */ - if (tsx_inv_data->sdp_done && - !(tsx->role == PJSIP_ROLE_UAC && - !tsx_inv_data->has_sdp && - rdata->msg_info.msg->line.status.code/100 == 2)) { + if (tsx_inv_data->sdp_done) { pj_str_t res_tag; int st_code; @@ -3190,7 +3187,9 @@ PJ_DEF(pj_status_t) pjsip_inv_answer( pjsip_inv_session *inv, pjmedia_sdp_neg_get_state(inv->neg) == PJMEDIA_SDP_NEG_STATE_DONE) { struct tsx_inv_data *tsx_inv_data; - pjmedia_sdp_session *offer = (pjmedia_sdp_session*)local_sdp; + const pjmedia_sdp_session *offer = local_sdp; + pjmedia_sdp_session *new_offer = NULL; + pj_bool_t cb_called = PJ_FALSE; const pjmedia_sdp_session *local_offer = NULL; tsx_inv_data = (struct tsx_inv_data*) @@ -3203,25 +3202,27 @@ PJ_DEF(pj_status_t) pjsip_inv_answer( pjsip_inv_session *inv, inv->invite_tsx->mod_data[mod_inv.mod.id] = tsx_inv_data; } if (tsx_inv_data && !tsx_inv_data->has_sdp) { - if (!offer && mod_inv.cb.on_create_offer) - (*mod_inv.cb.on_create_offer)(inv, &offer); + if (!offer && mod_inv.cb.on_create_offer) { + cb_called = PJ_TRUE; + (*mod_inv.cb.on_create_offer)(inv, &new_offer); + if (new_offer) + offer = new_offer; + } if (offer) { status = pjmedia_sdp_neg_modify_local_offer2( inv->pool_prov, inv->neg, inv->sdp_neg_flags, offer); - } else { + } else if (!cb_called) { status = pjmedia_sdp_neg_send_local_offer( inv->pool_prov, inv->neg, &local_offer); + } else { + /* App callback was invoked but did not provide a usable + * offer; do not silently re-offer the stale active SDP. + */ + status = PJ_EINVALIDOP; } - if (status == PJ_SUCCESS && !local_offer) - status = pjmedia_sdp_neg_get_neg_local(inv->neg, &local_offer); - if (status == PJ_SUCCESS && local_offer) - last_res->msg->body = create_sdp_body(last_res->pool, local_offer); - else if (status == PJ_SUCCESS) - status = PJMEDIA_SDPNEG_EINSTATE; - if (status != PJ_SUCCESS) { pjsip_tx_data_dec_ref(last_res); pjsip_tx_data_dec_ref(last_res); diff --git a/pjsip/src/pjsua-lib/pjsua_call.c b/pjsip/src/pjsua-lib/pjsua_call.c index 10d6c263d1..078a267977 100644 --- a/pjsip/src/pjsua-lib/pjsua_call.c +++ b/pjsip/src/pjsua-lib/pjsua_call.c @@ -16,6 +16,8 @@ * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ +#include +#include #include #include @@ -3411,9 +3413,7 @@ PJ_DEF(pj_status_t) pjsua_call_answer2(pjsua_call_id call_id, * Or if initial answer is not sent yet, we will answer the call after * initial answer is sent (see #1923). */ - if (call->med_ch_cb || - (call->inv->state < PJSIP_INV_STATE_CONFIRMED && - !call->inv->last_answer)) { + if (call->med_ch_cb || !call->inv->last_answer) { struct call_answer *answer; PJ_LOG(4,(THIS_FILE, "Pending answering call %d upon completion " @@ -3569,6 +3569,45 @@ pjsua_call_answer_with_sdp(pjsua_call_id call_id, return status; } +PJ_DEF(pj_status_t) call_inv_send_ack(pjsip_inv_session *inv, + int cseq, + const pjmedia_sdp_session *sdp) +{ + pjsip_tx_data *tdata; + pj_status_t status; + pjmedia_sdp_neg_state neg_state; + + if (inv == NULL) + return PJ_EINVAL; + + if (sdp) { + neg_state = pjmedia_sdp_neg_get_state(inv->neg); + if (neg_state != PJMEDIA_SDP_NEG_STATE_REMOTE_OFFER && + neg_state != PJMEDIA_SDP_NEG_STATE_WAIT_NEGO) + return PJMEDIA_SDPNEG_EINSTATE; + + status = pjsip_inv_set_sdp_answer(inv, sdp); + if (status != PJ_SUCCESS) { + pjsua_perror(THIS_FILE, "Unable to set SDP answer for ACK", + status); + goto on_return; + } + } + + status = pjsip_inv_create_ack(inv, cseq, &tdata); + if (status != PJ_SUCCESS) { + pjsua_perror(THIS_FILE, "Unable to create ACK", status); + goto on_return; + } + + status = pjsip_inv_send_msg(inv, tdata); + if (status != PJ_SUCCESS) { + pjsua_perror(THIS_FILE, "Unable to send ACK", status); + } + +on_return: + return status; +} /* * Create and send an ACK request for the 2xx response, optionally with an @@ -3580,7 +3619,6 @@ PJ_DEF(pj_status_t) pjsua_call_send_ack(pjsua_call_id call_id, { pjsua_call *call; pjsip_dialog *dlg = NULL; - pjsip_tx_data *tdata; pj_status_t status; PJ_ASSERT_RETURN(call_id>=0 && call_id<(int)pjsua_var.ua_cfg.max_calls, @@ -3593,22 +3631,7 @@ PJ_DEF(pj_status_t) pjsua_call_send_ack(pjsua_call_id call_id, if (status != PJ_SUCCESS) goto on_return; - if (sdp) { - status = pjsip_inv_set_sdp_answer(call->inv, sdp); - if (status != PJ_SUCCESS) { - pjsua_perror(THIS_FILE, "Unable to set SDP answer for ACK", - status); - goto on_return; - } - } - - status = pjsip_inv_create_ack(call->inv, cseq, &tdata); - if (status != PJ_SUCCESS) { - pjsua_perror(THIS_FILE, "Unable to create ACK", status); - goto on_return; - } - - status = pjsip_inv_send_msg(call->inv, tdata); + status = call_inv_send_ack(call->inv, cseq, sdp); if (status != PJ_SUCCESS) { pjsua_perror(THIS_FILE, "Unable to send ACK", status); } @@ -8144,16 +8167,20 @@ static pj_bool_t pjsua_call_on_uac_tsx_terminate_session( static void pjsua_call_on_send_ack(pjsip_inv_session *inv, pjsip_rx_data *rdata) { + pj_bool_t skip_sending_ack = PJ_FALSE; pjsua_call *call = (pjsua_call*) inv->dlg->mod_data[pjsua_var.mod.id]; - if (!call) + if (!call) { + call_inv_send_ack(inv, rdata->msg_info.cseq->cseq, NULL); return; + } pj_log_push_indent(); if (pjsua_var.ua_cfg.cb.on_call_send_ack) { - (*pjsua_var.ua_cfg.cb.on_call_send_ack)(call->index, rdata); - } else { + skip_sending_ack = (*pjsua_var.ua_cfg.cb.on_call_send_ack)(call->index, rdata); + } + if (!skip_sending_ack) { pjsua_call_send_ack(call->index, rdata->msg_info.cseq->cseq, NULL); } diff --git a/pjsip/src/pjsua2/endpoint.cpp b/pjsip/src/pjsua2/endpoint.cpp index 5fa179f8cf..ba0da35007 100644 --- a/pjsip/src/pjsua2/endpoint.cpp +++ b/pjsip/src/pjsua2/endpoint.cpp @@ -1572,18 +1572,19 @@ void Endpoint::on_call_sdp_created(pjsua_call_id call_id, } } -void Endpoint::on_call_send_ack(pjsua_call_id call_id, - pjsip_rx_data *rdata) +pj_bool_t Endpoint::on_call_send_ack(pjsua_call_id call_id, + pjsip_rx_data *rdata) { Call *call = Call::lookup(call_id); if (!call) { - return; + return PJ_FALSE; } OnCallSendAckParam prm; prm.rdata.fromPj(*rdata); call->onCallSendAck(prm); + return prm.suppressSendAck ? PJ_TRUE : PJ_FALSE; } void Endpoint::on_stream_precreate(pjsua_call_id call_id, From 3374357bf5e318adcc4b5a29aab78dc16a7f3c1b Mon Sep 17 00:00:00 2001 From: LEOPOLD Otakar Date: Mon, 5 Oct 2026 14:16:47 +0000 Subject: [PATCH 3/8] Fixes - round 2 - Solved re-acquiring the call lock while the dialog lock is held.(comment 1) - When pjsua_call_on_create_offer() leaves *offer NULL than 500 is answered.(comment 2) - New UaConfig.enableCallSendAckCallback configuration option added. When true PJUSA2 will call onSendAckCallback(). Default is false.(comment 3) - Fixed comment for OnCallSendAckParam.suppressSendAck.(comment 4) - Added check if inv->neg is NULL. (comment 5) - call_inv_send_ack() was made static.(comment 6) - Fixed comment for onCallSendAck().(comment 8) - Added cseq into OnCallSendAckParam.(comment 9) - dummy-codecs added into --help and into write_settings().(comment 10) --- pjsip-apps/src/pjsua/pjsua_app_config.c | 4 ++ pjsip/include/pjsua-lib/pjsua.h | 2 + pjsip/include/pjsua-lib/pjsua_internal.h | 5 ++ pjsip/include/pjsua2/call.hpp | 31 ++++++++--- pjsip/include/pjsua2/endpoint.hpp | 9 ++++ pjsip/src/pjsip-ua/sip_inv.c | 7 ++- pjsip/src/pjsua-lib/pjsua_call.c | 67 ++++++++++++++++++++---- pjsip/src/pjsua-lib/pjsua_core.c | 1 + pjsip/src/pjsua2/endpoint.cpp | 10 +++- 9 files changed, 116 insertions(+), 20 deletions(-) diff --git a/pjsip-apps/src/pjsua/pjsua_app_config.c b/pjsip-apps/src/pjsua/pjsua_app_config.c index 127a61341b..393f9f7496 100644 --- a/pjsip-apps/src/pjsua/pjsua_app_config.c +++ b/pjsip-apps/src/pjsua/pjsua_app_config.c @@ -206,6 +206,7 @@ static void usage(void) puts (" for this account as application-managed from the start,"); puts (" so pjsua never creates a media transport for them (even"); puts (" the initial incoming offer). See pjsua_acc_config."); + puts (" --dummy-codecs Register dummy codecs for the alternative media backend."); #endif #if PJSUA_HAS_VIDEO @@ -2919,6 +2920,9 @@ int write_settings(pjsua_app_config *config, char *buf, pj_size_t max) cfg_add_str(&cfg, max, &escaped); cfg_add(&cfg, max, "\"\n"); } + if (config->dummy_codecs) { + cfg_add(&cfg, max, "--dummy-codecs\n"); + } #endif /* !PJSUA_MEDIA_HAS_PJMEDIA */ return (int)cfg.slen; diff --git a/pjsip/include/pjsua-lib/pjsua.h b/pjsip/include/pjsua-lib/pjsua.h index 255bc2e194..702d4048b5 100644 --- a/pjsip/include/pjsua-lib/pjsua.h +++ b/pjsip/include/pjsua-lib/pjsua.h @@ -6991,6 +6991,8 @@ pjsua_call_answer_with_sdp(pjsua_call_id call_id, * function from within the #pjsua_callback.on_call_send_ack() callback, * to manually take over the ACK transmission, e.g. to attach an SDP answer * to the ACK for a late SDP offer received in the 2xx response. + * This function is safe to call synchronously from the + * #pjsua_callback.on_call_send_ack() callback. * * @param call_id Call identification. * @param cseq CSeq of the 2xx response that triggered the need to diff --git a/pjsip/include/pjsua-lib/pjsua_internal.h b/pjsip/include/pjsua-lib/pjsua_internal.h index ff4cfe96fb..46bd37e90e 100644 --- a/pjsip/include/pjsua-lib/pjsua_internal.h +++ b/pjsip/include/pjsua-lib/pjsua_internal.h @@ -1057,6 +1057,11 @@ pj_status_t pjsua_call_subsys_init(const pjsua_config *cfg); */ pj_status_t pjsua_call_subsys_start(void); +/** + * Destroy call subsystem resources. + */ +void pjsua_call_subsys_destroy(void); + /** * Init media subsystems. */ diff --git a/pjsip/include/pjsua2/call.hpp b/pjsip/include/pjsua2/call.hpp index 731e2eb72f..62e65860fa 100644 --- a/pjsip/include/pjsua2/call.hpp +++ b/pjsip/include/pjsua2/call.hpp @@ -221,6 +221,11 @@ struct SdpSession void *pjSdpSession; public: + /** + * Default constructor + */ + SdpSession() : pjSdpSession(nullptr) {} + /** * Convert from pjsip */ @@ -845,13 +850,21 @@ struct OnCallSendAckParam SipRxData rdata; /** - * Output: application sets this to true to suppress the automatic - * session termination and keep the call alive. Default is false, i.e. - * the library will terminate the call as per default behavior. + * Output: application sets this to true if it has sent (or will send) + * the ACK itself, to suppress the automatic ACK sending. Default is false, + * i.e. the library sends the ACK after this callback returns. */ bool suppressSendAck; - OnCallSendAckParam() : suppressSendAck(false) + /** + * The CSeq of the 2xx response. + */ + pj_int32_t cseq; + + /** + * Default constructor + */ + OnCallSendAckParam() : suppressSendAck(false), cseq(-1) {} }; @@ -2175,10 +2188,12 @@ class Call * Notify application when the framework needs to send an ACK request * after it receives an incoming 2xx response for INVITE (e.g. when the * 2xx response carries a late SDP offer). The default implementation - * simply sends the ACK without any SDP answer, using sendAck(). - * Application can override this callback to attach an SDP answer to - * the ACK (e.g. for a late offer scenario), or to delay sending the - * ACK, by calling sendAck() itself. + * simply do nothing and let's framework send the ACK without any SDP answer. + * Application can set prm.suppressSendAck to true and than the application + * is responsible for sending the ACK. + * + * This callback is enabled only when + * UaConfig::enableCallSendAckCallback is set before Endpoint::libInit(). * * @param prm Callback parameter. */ diff --git a/pjsip/include/pjsua2/endpoint.hpp b/pjsip/include/pjsua2/endpoint.hpp index 8be71bd69d..9cfc13cbe2 100644 --- a/pjsip/include/pjsua2/endpoint.hpp +++ b/pjsip/include/pjsua2/endpoint.hpp @@ -1042,6 +1042,15 @@ struct UaConfig : public PersistentObject */ bool resolverFallback; + /** + * Enable Call::onCallSendAck() for application-controlled ACK handling. + * Set this before Endpoint::libInit(). When disabled, PJSIP sends ACK + * automatically without invoking the callback. + * + * Default: false + */ + bool enableCallSendAckCallback; + public: /** * Default constructor to initialize with default values. diff --git a/pjsip/src/pjsip-ua/sip_inv.c b/pjsip/src/pjsip-ua/sip_inv.c index d80e7b806e..4648eb1962 100644 --- a/pjsip/src/pjsip-ua/sip_inv.c +++ b/pjsip/src/pjsip-ua/sip_inv.c @@ -3220,7 +3220,12 @@ PJ_DEF(pj_status_t) pjsip_inv_answer( pjsip_inv_session *inv, /* App callback was invoked but did not provide a usable * offer; do not silently re-offer the stale active SDP. */ - status = PJ_EINVALIDOP; + st_code = PJSIP_SC_INTERNAL_SERVER_ERROR; + status = pjsip_dlg_modify_response(inv->dlg, last_res, + st_code, NULL); + if (status == PJ_SUCCESS) + pjsip_tx_data_dec_ref(last_res); + last_res->msg->body = NULL; } if (status != PJ_SUCCESS) { diff --git a/pjsip/src/pjsua-lib/pjsua_call.c b/pjsip/src/pjsua-lib/pjsua_call.c index 078a267977..871263970c 100644 --- a/pjsip/src/pjsua-lib/pjsua_call.c +++ b/pjsip/src/pjsua-lib/pjsua_call.c @@ -46,6 +46,8 @@ /* Max number of hangup retries. */ #define CALL_HANGUP_MAX_RETRY 4 +static long ack_callback_inv_tls = -1; + /* * The INFO method. */ @@ -251,6 +253,12 @@ pj_status_t pjsua_call_subsys_init(const pjsua_config *cfg) /* Copy config */ pjsua_config_dup(pjsua_var.pool, &pjsua_var.ua_cfg, cfg); + if (pjsua_var.ua_cfg.cb.on_call_send_ack) { + status = pj_thread_local_alloc(&ack_callback_inv_tls); + if (status != PJ_SUCCESS) + return status; + } + /* Verify settings */ if (pjsua_var.ua_cfg.max_calls > PJSUA_MAX_CALLS) pjsua_var.ua_cfg.max_calls = PJSUA_MAX_CALLS; @@ -330,6 +338,16 @@ pj_status_t pjsua_call_subsys_start(void) } +/* Destroy call subsystem resources. */ +void pjsua_call_subsys_destroy(void) +{ + if (ack_callback_inv_tls != -1) { + pj_thread_local_free(ack_callback_inv_tls); + ack_callback_inv_tls = -1; + } +} + + /* * Get maximum number of calls configured in pjsua. */ @@ -3569,9 +3587,9 @@ pjsua_call_answer_with_sdp(pjsua_call_id call_id, return status; } -PJ_DEF(pj_status_t) call_inv_send_ack(pjsip_inv_session *inv, - int cseq, - const pjmedia_sdp_session *sdp) +static pj_status_t call_inv_send_ack(pjsip_inv_session *inv, + int cseq, + const pjmedia_sdp_session *sdp) { pjsip_tx_data *tdata; pj_status_t status; @@ -3580,6 +3598,9 @@ PJ_DEF(pj_status_t) call_inv_send_ack(pjsip_inv_session *inv, if (inv == NULL) return PJ_EINVAL; + if (inv->neg == NULL) + return PJMEDIA_SDPNEG_EINSTATE; + if (sdp) { neg_state = pjmedia_sdp_neg_get_state(inv->neg); if (neg_state != PJMEDIA_SDP_NEG_STATE_REMOTE_OFFER && @@ -3619,6 +3640,7 @@ PJ_DEF(pj_status_t) pjsua_call_send_ack(pjsua_call_id call_id, { pjsua_call *call; pjsip_dialog *dlg = NULL; + pjsip_inv_session *callback_inv = NULL; pj_status_t status; PJ_ASSERT_RETURN(call_id>=0 && call_id<(int)pjsua_var.ua_cfg.max_calls, @@ -3627,11 +3649,26 @@ PJ_DEF(pj_status_t) pjsua_call_send_ack(pjsua_call_id call_id, PJ_LOG(4,(THIS_FILE, "Sending ACK for call %d", call_id)); pj_log_push_indent(); - status = acquire_call("pjsua_call_send_ack()", call_id, &call, &dlg); - if (status != PJ_SUCCESS) - goto on_return; + if (ack_callback_inv_tls != -1) + callback_inv = (pjsip_inv_session*) pj_thread_local_get(ack_callback_inv_tls); - status = call_inv_send_ack(call->inv, cseq, sdp); + if (callback_inv) { + call = (pjsua_call*) callback_inv->dlg->mod_data[pjsua_var.mod.id]; + if (!call || call->index != call_id) { + status = PJ_EINVALIDOP; + goto on_return; + } + } + else { + status = acquire_call("pjsua_call_send_ack()", call_id, &call, &dlg); + if (status != PJ_SUCCESS) { + goto on_return; + } + + callback_inv = call->inv; + } + + status = call_inv_send_ack(callback_inv, cseq, sdp); if (status != PJ_SUCCESS) { pjsua_perror(THIS_FILE, "Unable to send ACK", status); } @@ -8169,6 +8206,8 @@ static void pjsua_call_on_send_ack(pjsip_inv_session *inv, { pj_bool_t skip_sending_ack = PJ_FALSE; pjsua_call *call = (pjsua_call*) inv->dlg->mod_data[pjsua_var.mod.id]; + pjsip_inv_session *prev_inv = NULL; + pj_status_t status; if (!call) { call_inv_send_ack(inv, rdata->msg_info.cseq->cseq, NULL); @@ -8178,10 +8217,20 @@ static void pjsua_call_on_send_ack(pjsip_inv_session *inv, pj_log_push_indent(); if (pjsua_var.ua_cfg.cb.on_call_send_ack) { - skip_sending_ack = (*pjsua_var.ua_cfg.cb.on_call_send_ack)(call->index, rdata); + prev_inv = (pjsip_inv_session*) pj_thread_local_get(ack_callback_inv_tls); + status = pj_thread_local_set(ack_callback_inv_tls, inv); + if (status == PJ_SUCCESS) { + skip_sending_ack = (*pjsua_var.ua_cfg.cb.on_call_send_ack)( + call->index, + rdata); + pj_thread_local_set(ack_callback_inv_tls, prev_inv); + } else { + pjsua_perror(THIS_FILE, "Unable to set ACK callback context", + status); + } } if (!skip_sending_ack) { - pjsua_call_send_ack(call->index, rdata->msg_info.cseq->cseq, NULL); + call_inv_send_ack(inv, rdata->msg_info.cseq->cseq, NULL); } pj_log_pop_indent(); diff --git a/pjsip/src/pjsua-lib/pjsua_core.c b/pjsip/src/pjsua-lib/pjsua_core.c index 6bf546e7c2..29fdcfe01c 100644 --- a/pjsip/src/pjsua-lib/pjsua_core.c +++ b/pjsip/src/pjsua-lib/pjsua_core.c @@ -2429,6 +2429,7 @@ PJ_DEF(pj_status_t) pjsua_destroy2(unsigned flags) pj_log_pop_indent(); /* Shutdown PJLIB */ + pjsua_call_subsys_destroy(); pj_shutdown(); } diff --git a/pjsip/src/pjsua2/endpoint.cpp b/pjsip/src/pjsua2/endpoint.cpp index ba0da35007..55b67b6e92 100644 --- a/pjsip/src/pjsua2/endpoint.cpp +++ b/pjsip/src/pjsua2/endpoint.cpp @@ -297,7 +297,8 @@ void OnVideoMediaOpCompletedParam::fromPj(const pjmedia_vid_conf_op_info &info) /////////////////////////////////////////////////////////////////////////////// UaConfig::UaConfig() -: mainThreadOnly(false) +: mainThreadOnly(false), + enableCallSendAckCallback(false) { pjsua_config ua_cfg; @@ -402,6 +403,7 @@ void UaConfig::readObject(const ContainerNode &node) PJSUA2_THROW(Error) NODE_READ_BOOL_OPT( this_node, accServerAffinityDefault); NODE_READ_BOOL_OPT( this_node, serverFailover); NODE_READ_BOOL_OPT( this_node, resolverFallback); + NODE_READ_BOOL_OPT( this_node, enableCallSendAckCallback); } void UaConfig::writeObject(ContainerNode &node) const PJSUA2_THROW(Error) @@ -424,6 +426,7 @@ void UaConfig::writeObject(ContainerNode &node) const PJSUA2_THROW(Error) NODE_WRITE_BOOL ( this_node, accServerAffinityDefault); NODE_WRITE_BOOL ( this_node, serverFailover); NODE_WRITE_BOOL ( this_node, resolverFallback); + NODE_WRITE_BOOL ( this_node, enableCallSendAckCallback); } /////////////////////////////////////////////////////////////////////////////// @@ -1582,6 +1585,7 @@ pj_bool_t Endpoint::on_call_send_ack(pjsua_call_id call_id, OnCallSendAckParam prm; prm.rdata.fromPj(*rdata); + prm.cseq = rdata->msg_info.cseq->cseq; call->onCallSendAck(prm); return prm.suppressSendAck ? PJ_TRUE : PJ_FALSE; @@ -2394,7 +2398,9 @@ void Endpoint::libInit(const EpConfig &prmEpConfig) PJSUA2_THROW(Error) = &Endpoint::on_call_tsx_terminate_session; ua_cfg.cb.on_call_media_state = &Endpoint::on_call_media_state; ua_cfg.cb.on_call_sdp_created = &Endpoint::on_call_sdp_created; - ua_cfg.cb.on_call_send_ack = &Endpoint::on_call_send_ack; + if (prmEpConfig.uaConfig.enableCallSendAckCallback) { + ua_cfg.cb.on_call_send_ack = &Endpoint::on_call_send_ack; + } ua_cfg.cb.on_stream_precreate = &Endpoint::on_stream_precreate; ua_cfg.cb.on_stream_created2 = &Endpoint::on_stream_created2; ua_cfg.cb.on_stream_destroyed = &Endpoint::on_stream_destroyed; From 1a9dd112f1d83c76bb808e1a62517ed778deb9a9 Mon Sep 17 00:00:00 2001 From: LEOPOLD Otakar Date: Fri, 9 Oct 2026 11:15:33 +0000 Subject: [PATCH 4/8] Fixes - round 3 - guard moved(comment 1) - unitialized response in no longer in code because of rebase on current master(comment 2) - added warning log(comment 3) - unified synchronous and asynchronous response when on_create_offer() doesn't return any sdp. Now it sends OK with current SDP.(comment 4) - fixed documentation comments(comment 5) - SDP offer is not sent if there is non SDP body in re-INVITE(comment 6) - added unit tests for (re-)INVITE with late SDP(comment 7) - fixed wrong naming in sip tests(comment 8) - changed pjsua_call_answer_with_sdp() to work with sdp offer without need to use on_create_offer/on_call_sdp_created --- pjsip/include/pjsua-lib/pjsua.h | 47 +- pjsip/include/pjsua-lib/pjsua_internal.h | 1 + pjsip/src/pjsip-ua/sip_inv.c | 47 +- pjsip/src/pjsua-lib/pjsua_call.c | 95 ++- pjsip/src/test/inv_offer_answer_test.c | 177 +++++- pjsip/src/test/pjsua_call_test.c | 566 +++++++++++++++++- pjsip/src/test/test.c | 2 + pjsip/src/test/test.h | 1 + .../alt-pjsua-uac-late-offer-dummy-codecs.py | 2 +- .../alt-pjsua-uas-late-offer-dummy-codecs.py | 6 +- .../scripts-sipp/alt-pjsua-uas-late-offer.py | 4 - 11 files changed, 870 insertions(+), 78 deletions(-) diff --git a/pjsip/include/pjsua-lib/pjsua.h b/pjsip/include/pjsua-lib/pjsua.h index 702d4048b5..334b08c0aa 100644 --- a/pjsip/include/pjsua-lib/pjsua.h +++ b/pjsip/include/pjsua-lib/pjsua.h @@ -1780,15 +1780,18 @@ typedef struct pjsua_callback * Otherwise, by default the re-INVITE will be answered automatically * after the callback returns. * - * For an application-managed call receiving a re-INVITE without SDP, - * this callback is also called with a NULL offer. By default, the active - * local SDP is re-offered. Application may set \a async to PJ_TRUE and + * For a re-INVITE without SDP, this callback is called with a NULL offer. + * This also applies to calls that are not application-managed when this + * callback is registered. By default, the active local SDP is re-offered. + * Application may set \a async to PJ_TRUE and * later send the response with #pjsua_call_answer_with_sdp(), supplying * its own local SDP offer. On the asynchronous path, a successful final * response requires an application-supplied offer; the active local SDP * is not automatically re-offered. * On the synchronous offerless path, any 2xx callback code is treated - * as 200. Codes outside 200-699 are rejected with 500. + * as 200, and final rejection codes (300-699) are accepted. Codes outside + * 200-699 are rejected with 500. For a re-INVITE containing SDP, only + * 200 and 488 are valid callback codes. * * Remarks: If manually answering at a later timing, application may * need to monitor on_call_tsx_state() callback to check whether @@ -1808,15 +1811,17 @@ typedef struct pjsua_callback * as pjsua deactivates it there, see on_call_sdp_created(). * * @param call_id The call index. - * @param offer Remote offer, or NULL for an application-managed - * call receiving a re-INVITE without SDP. + * @param offer Remote offer, or NULL for a re-INVITE without SDP. * @param rdata The received re-INVITE request. * @param reserved Reserved param, currently not used. * @param async On input, it is PJ_FALSE. Set to PJ_TRUE if * app wants to manually answer the re-INVITE. * @param code Status code to be returned for answering the * offer. On input, it contains status code 200. - * Currently, valid values are only 200 and 488. + * For a re-INVITE containing SDP, valid values are 200 + * and 488. For a synchronous offerless re-INVITE, 2xx + * is treated as 200, 300-699 rejects the request, and + * values outside 200-699 are changed to 500. * @param opt The current call setting, application can update * this setting for answering the offer. */ @@ -2482,12 +2487,14 @@ typedef struct pjsua_callback /** * Notify application when the framework needs to send an ACK request * after it receives an incoming 2xx response for INVITE (e.g. when the - * 2xx response carries a late SDP offer). If this callback is - * implemented, the application is responsible for creating and sending - * the ACK request itself, using #pjsua_call_send_ack(), which allows an - * SDP answer to be attached to the ACK. This also allows the application - * to delay sending the ACK, e.g. while waiting for the SDP answer to be - * generated by 3PCC scenarios. + * 2xx response carries a late SDP offer). Implementing this callback + * alone does not transfer responsibility for sending the ACK. The + * framework continues to send it automatically unless this callback + * returns PJ_TRUE. When it returns PJ_TRUE, the application is + * responsible for creating and sending the ACK itself, using + * #pjsua_call_send_ack(). This allows an SDP answer to be attached to + * the ACK or the ACK transmission to be delayed, e.g. while waiting for + * the SDP answer in 3PCC scenarios. * * @param call_id The call index. * @param rdata The received 2xx response that triggered the need to @@ -6954,17 +6961,17 @@ PJ_DECL(pj_status_t) pjsua_call_answer2(pjsua_call_id call_id, /** * Same as #pjsua_call_answer2() but this function will set the local SDP - * first before sending the response. For application-managed calls - * (#PJSUA_CALL_MEDIA_APP_MANAGED), the SDP is used as an offer when answering - * an offerless INVITE or re-INVITE, and as an answer otherwise. For ordinary - * calls, the SDP is used only as an answer to a pending remote offer. + * before sending the response. The SDP is used as an offer when answering + * an offerless INVITE or re-INVITE, and as an answer to a pending remote + * offer otherwise. For ordinary initial offerless calls, media is initialized + * before installing the supplied offer. A pending local offer cannot be + * replaced by this function. * An application-managed answer must have the same media-line count as the * remote offer; otherwise PJMEDIA_SDPNEG_EMISMEDIA is returned. * * @param call_id Incoming call identification. - * @param sdp Local SDP answer, or a local SDP offer for an - * application-managed call answering an offerless - * INVITE or re-INVITE. + * @param sdp Local SDP answer, or a local SDP offer when answering + * an offerless INVITE or re-INVITE. * @param opt Optional call setting. * @param code Status code, (100-699). * @param reason Optional reason phrase. If NULL, default text diff --git a/pjsip/include/pjsua-lib/pjsua_internal.h b/pjsip/include/pjsua-lib/pjsua_internal.h index 46bd37e90e..e19383a4d0 100644 --- a/pjsip/include/pjsua-lib/pjsua_internal.h +++ b/pjsip/include/pjsua-lib/pjsua_internal.h @@ -298,6 +298,7 @@ struct pjsua_call /**< Allocated size of siprec_metadata */ pjmedia_av_sync *av_sync; /**< Media stream synchronizer */ + pjmedia_sdp_session *answer_sdp;/**< Application SDP pending media init.*/ }; PJ_INLINE(pj_bool_t) pjsua_call_media_is_app_managed( diff --git a/pjsip/src/pjsip-ua/sip_inv.c b/pjsip/src/pjsip-ua/sip_inv.c index 4648eb1962..167d7dedb2 100644 --- a/pjsip/src/pjsip-ua/sip_inv.c +++ b/pjsip/src/pjsip-ua/sip_inv.c @@ -216,6 +216,7 @@ struct tsx_inv_data pj_bool_t done_early;/* Negotiation was done for early med? */ pj_bool_t done_early_rel;/* Early med was realiable? */ pj_bool_t has_sdp; /* Message with SDP? */ + pj_bool_t has_body; /* Message has a body, even if not SDP */ }; @@ -2693,6 +2694,17 @@ static pj_status_t inv_check_sdp_in_incoming_msg( pjsip_inv_session *inv, return PJ_SUCCESS; } + /* Track body presence separately from SDP. Unknown bodies may be + * accepted, but must not be treated as an offerless request. + */ + tsx_inv_data = (struct tsx_inv_data*)tsx->mod_data[mod_inv.mod.id]; + if (tsx_inv_data == NULL) { + tsx_inv_data = PJ_POOL_ZALLOC_T(tsx->pool, struct tsx_inv_data); + tsx_inv_data->inv = inv; + tsx->mod_data[mod_inv.mod.id] = tsx_inv_data; + } + tsx_inv_data->has_body = PJ_TRUE; + sdp_info = pjsip_rdata_get_sdp_info(rdata); if (sdp_info->body.ptr == NULL) { /* Message body is not "application/sdp" */ @@ -2711,14 +2723,11 @@ static pj_status_t inv_check_sdp_in_incoming_msg( pjsip_inv_session *inv, goto on_return; } - /* Get/attach invite session's transaction data */ - tsx_inv_data = (struct tsx_inv_data*) tsx->mod_data[mod_inv.mod.id]; - if (tsx_inv_data == NULL) { - tsx_inv_data = PJ_POOL_ZALLOC_T(tsx->pool, struct tsx_inv_data); - tsx_inv_data->inv = inv; - tsx_inv_data->has_sdp = (sdp_info->sdp!=NULL); - tsx->mod_data[mod_inv.mod.id] = tsx_inv_data; - } + /* Record whether this message carries SDP as well as whether it has any + * body at all. The latter distinguishes an offerless request from an + * accepted request with an unknown content type. + */ + tsx_inv_data->has_sdp = (sdp_info->sdp != NULL); /* Initialize info that we are following forked media */ inv->following_fork = PJ_FALSE; @@ -3189,7 +3198,6 @@ PJ_DEF(pj_status_t) pjsip_inv_answer( pjsip_inv_session *inv, struct tsx_inv_data *tsx_inv_data; const pjmedia_sdp_session *offer = local_sdp; pjmedia_sdp_session *new_offer = NULL; - pj_bool_t cb_called = PJ_FALSE; const pjmedia_sdp_session *local_offer = NULL; tsx_inv_data = (struct tsx_inv_data*) @@ -3201,9 +3209,10 @@ PJ_DEF(pj_status_t) pjsip_inv_answer( pjsip_inv_session *inv, tsx_inv_data->has_sdp = PJ_FALSE; inv->invite_tsx->mod_data[mod_inv.mod.id] = tsx_inv_data; } - if (tsx_inv_data && !tsx_inv_data->has_sdp) { + if (tsx_inv_data && !tsx_inv_data->has_sdp && + !tsx_inv_data->has_body) + { if (!offer && mod_inv.cb.on_create_offer) { - cb_called = PJ_TRUE; (*mod_inv.cb.on_create_offer)(inv, &new_offer); if (new_offer) offer = new_offer; @@ -3213,19 +3222,15 @@ PJ_DEF(pj_status_t) pjsip_inv_answer( pjsip_inv_session *inv, status = pjmedia_sdp_neg_modify_local_offer2( inv->pool_prov, inv->neg, inv->sdp_neg_flags, offer); - } else if (!cb_called) { - status = pjmedia_sdp_neg_send_local_offer( - inv->pool_prov, inv->neg, &local_offer); } else { - /* App callback was invoked but did not provide a usable - * offer; do not silently re-offer the stale active SDP. + /* Match the synchronous offerless re-INVITE path: if the + * application does not provide a new offer, keep the call + * established by re-offering the active local SDP. */ - st_code = PJSIP_SC_INTERNAL_SERVER_ERROR; - status = pjsip_dlg_modify_response(inv->dlg, last_res, - st_code, NULL); + status = pjmedia_sdp_neg_send_local_offer( + inv->pool_prov, inv->neg, &local_offer); if (status == PJ_SUCCESS) - pjsip_tx_data_dec_ref(last_res); - last_res->msg->body = NULL; + offer = local_offer; } if (status != PJ_SUCCESS) { diff --git a/pjsip/src/pjsua-lib/pjsua_call.c b/pjsip/src/pjsua-lib/pjsua_call.c index 871263970c..8b32e08eef 100644 --- a/pjsip/src/pjsua-lib/pjsua_call.c +++ b/pjsip/src/pjsua-lib/pjsua_call.c @@ -3224,16 +3224,22 @@ on_answer_call_med_tp_complete(pjsua_call_id call_id, if (call->async_call.med_ch_deinit) { pjsua_media_channel_deinit(call->index); call->med_ch_cb = NULL; + call->answer_sdp = NULL; PJSUA_UNLOCK(); return PJ_SUCCESS; } - status = pjsua_media_channel_create_sdp(call_id, - call->async_call.dlg->pool, - NULL, &sdp, &sip_err_code); - if (status != PJ_SUCCESS) { - pjsua_perror(THIS_FILE, "Error creating SDP answer", status); - goto on_return; + if (call->answer_sdp) { + sdp = call->answer_sdp; + call->answer_sdp = NULL; + } else { + status = pjsua_media_channel_create_sdp(call_id, + call->async_call.dlg->pool, + NULL, &sdp, &sip_err_code); + if (status != PJ_SUCCESS) { + pjsua_perror(THIS_FILE, "Error creating SDP answer", status); + goto on_return; + } } status = pjsip_inv_set_local_sdp(call->inv, sdp); @@ -3245,6 +3251,8 @@ on_answer_call_med_tp_complete(pjsua_call_id call_id, on_return: if (status != PJ_SUCCESS) { + call->answer_sdp = NULL; + /* If the callback is called from pjsua_call_on_incoming(), the * invite's state is PJSIP_INV_STATE_NULL, so the invite session * will be terminated later, otherwise we end the session here. @@ -3516,6 +3524,7 @@ pjsua_call_answer_with_sdp(pjsua_call_id call_id, pjmedia_sdp_neg *old_neg; pj_bool_t app_managed; pj_status_t status, cancel_status; + pjmedia_sdp_session *pending_sdp = NULL; PJ_ASSERT_RETURN(call_id>=0 && call_id<(int)pjsua_var.ua_cfg.max_calls, PJ_EINVAL); @@ -3544,7 +3553,44 @@ pjsua_call_answer_with_sdp(pjsua_call_id call_id, if (status == PJ_SUCCESS) status = pjsip_inv_set_local_sdp(inv, sdp); } else { - status = pjsip_inv_set_sdp_answer(inv, sdp); + /* For an initial offerless INVITE, let answer2() initialize media before + * setting the supplied SDP offer. + */ + if (code < 300 && sdp && + (call->opt_inited || code == 183 || code / 100 == 2) && + (!call->inv->neg || + pjmedia_sdp_neg_get_state(call->inv->neg) == + PJMEDIA_SDP_NEG_STATE_NULL)) + { + if (call->answer_sdp) { + pjsip_dlg_dec_lock(dlg); + return PJ_EBUSY; + } + + pending_sdp = pjmedia_sdp_session_clone(call->inv->pool, sdp); + if (!pending_sdp) { + pjsip_dlg_dec_lock(dlg); + return PJ_ENOMEM; + } + call->answer_sdp = pending_sdp; + + pjsip_dlg_dec_lock(dlg); + status = pjsua_call_answer2(call_id, opt, code, reason, msg_data); + if (status != PJ_SUCCESS && status != PJ_EPENDING) { + pj_status_t lock_status; + + lock_status = acquire_call("pjsua_call_answer_with_sdp()", + call_id, &call, &dlg); + if (lock_status == PJ_SUCCESS) { + if (call->answer_sdp == pending_sdp) + call->answer_sdp = NULL; + pjsip_dlg_dec_lock(dlg); + } + } + return status; + } + + status = pjsip_inv_set_local_sdp(call->inv, sdp); } pjsip_dlg_dec_lock(dlg); @@ -3598,10 +3644,10 @@ static pj_status_t call_inv_send_ack(pjsip_inv_session *inv, if (inv == NULL) return PJ_EINVAL; - if (inv->neg == NULL) - return PJMEDIA_SDPNEG_EINSTATE; - if (sdp) { + if (inv->neg == NULL) + return PJMEDIA_SDPNEG_EINSTATE; + neg_state = pjmedia_sdp_neg_get_state(inv->neg); if (neg_state != PJMEDIA_SDP_NEG_STATE_REMOTE_OFFER && neg_state != PJMEDIA_SDP_NEG_STATE_WAIT_NEGO) @@ -3650,7 +3696,8 @@ PJ_DEF(pj_status_t) pjsua_call_send_ack(pjsua_call_id call_id, pj_log_push_indent(); if (ack_callback_inv_tls != -1) - callback_inv = (pjsip_inv_session*) pj_thread_local_get(ack_callback_inv_tls); + callback_inv = (pjsip_inv_session*) + pj_thread_local_get(ack_callback_inv_tls); if (callback_inv) { call = (pjsua_call*) callback_inv->dlg->mod_data[pjsua_var.mod.id]; @@ -6881,7 +6928,9 @@ static pj_status_t pjsua_call_on_rx_reinvite(pjsip_inv_session *inv, call = (pjsua_call*) inv->dlg->mod_data[pjsua_var.mod.id]; - if (!offer && pjsua_call_media_is_app_managed(call)) { + if (!offer && (pjsua_call_media_is_app_managed(call) || + pjsua_var.ua_cfg.cb.on_call_rx_reinvite)) + { pjsua_call_setting opt; pjsip_status_code code = PJSIP_SC_OK; pjsip_tx_data *response = NULL; @@ -8210,14 +8259,22 @@ static void pjsua_call_on_send_ack(pjsip_inv_session *inv, pj_status_t status; if (!call) { - call_inv_send_ack(inv, rdata->msg_info.cseq->cseq, NULL); + status = call_inv_send_ack(inv, rdata->msg_info.cseq->cseq, NULL); + if (status != PJ_SUCCESS) { + PJ_LOG(3,(THIS_FILE, "Failed to send ACK for SIP Call-ID %.*s " + "(CSeq %d), status=%d", + (int)inv->dlg->call_id->id.slen, + inv->dlg->call_id->id.ptr, + rdata->msg_info.cseq->cseq, status)); + } return; } pj_log_push_indent(); if (pjsua_var.ua_cfg.cb.on_call_send_ack) { - prev_inv = (pjsip_inv_session*) pj_thread_local_get(ack_callback_inv_tls); + prev_inv = (pjsip_inv_session*) + pj_thread_local_get(ack_callback_inv_tls); status = pj_thread_local_set(ack_callback_inv_tls, inv); if (status == PJ_SUCCESS) { skip_sending_ack = (*pjsua_var.ua_cfg.cb.on_call_send_ack)( @@ -8230,7 +8287,15 @@ static void pjsua_call_on_send_ack(pjsip_inv_session *inv, } } if (!skip_sending_ack) { - call_inv_send_ack(inv, rdata->msg_info.cseq->cseq, NULL); + status = call_inv_send_ack(inv, rdata->msg_info.cseq->cseq, NULL); + if (status != PJ_SUCCESS) { + PJ_LOG(3,(THIS_FILE, "Failed to send ACK for call %d " + "(SIP Call-ID %.*s, CSeq %d), status=%d", + call->index, + (int)inv->dlg->call_id->id.slen, + inv->dlg->call_id->id.ptr, + rdata->msg_info.cseq->cseq, status)); + } } pj_log_pop_indent(); diff --git a/pjsip/src/test/inv_offer_answer_test.c b/pjsip/src/test/inv_offer_answer_test.c index 96fa1bfdf1..b18a6015cf 100644 --- a/pjsip/src/test/inv_offer_answer_test.c +++ b/pjsip/src/test/inv_offer_answer_test.c @@ -138,6 +138,8 @@ typedef struct inv_test_param_t unsigned count; oa_t oa[4]; pj_bool_t multipart_body; + pj_bool_t test_null_offer_reinvite; + pj_bool_t test_non_sdp_body_reinvite; } inv_test_param_t; typedef struct inv_test_t @@ -159,6 +161,14 @@ typedef struct inv_test_t /**************** GLOBALS ******************/ static inv_test_t inv_test; static unsigned job_cnt; +static pj_bool_t reject_null_offer; +static pj_bool_t answer_reinvite_manually; +static pj_bool_t expect_null_offer_response; +static unsigned null_offer_response_code; +static unsigned null_offer_response_media_count; +static pj_bool_t null_offer_response_has_sdp; +static unsigned null_offer_create_offer_count; +static pj_status_t null_offer_reinvite_answer_status; typedef enum job_type { @@ -215,11 +225,40 @@ static void on_create_offer(pjsip_inv_session *inv, pjmedia_sdp_session **p_offer) { PJ_UNUSED_ARG(inv); - PJ_UNUSED_ARG(p_offer); + + if (reject_null_offer) { + ++null_offer_create_offer_count; + *p_offer = NULL; + return; + } pj_assert(!"Should not happen"); } +static pj_status_t on_rx_reinvite(pjsip_inv_session *inv, + const pjmedia_sdp_session *offer, + pjsip_rx_data *rdata) +{ + pjsip_tx_data *tdata; + pj_status_t status; + + PJ_UNUSED_ARG(offer); + + if (!answer_reinvite_manually) + return PJ_EINVALIDOP; + + status = pjsip_inv_initial_answer(inv, rdata, 100, NULL, NULL, &tdata); + if (status == PJ_SUCCESS) + status = pjsip_inv_send_msg(inv, tdata); + if (status == PJ_SUCCESS) + status = pjsip_inv_answer(inv, 200, NULL, NULL, &tdata); + if (status == PJ_SUCCESS) + status = pjsip_inv_send_msg(inv, tdata); + + null_offer_reinvite_answer_status = status; + return status; +} + static void on_media_update(pjsip_inv_session *inv_ses, pj_status_t status) { @@ -430,6 +469,7 @@ static int perform_test(inv_test_param_t *param) pjmedia_sdp_session *sdp; pjsip_tx_data *tdata; pj_status_t status; + int rc = 0; PJ_LOG(3,(THIS_FILE, " %s", param->title)); @@ -496,6 +536,100 @@ static int perform_test(inv_test_param_t *param) flush_events(100); + if (param->test_null_offer_reinvite || + param->test_non_sdp_body_reinvite) + { + const pjmedia_sdp_session *active_sdp = NULL; + const pj_bool_t has_unknown_body = + param->test_non_sdp_body_reinvite; + unsigned i; + + status = pjmedia_sdp_neg_get_active_local(inv_test.uas->neg, + &active_sdp); + if (status != PJ_SUCCESS || !active_sdp) { + PJ_LOG(1, (THIS_FILE, + " unable to get active local SDP before re-INVITE")); + rc = -60; + } else { + reject_null_offer = PJ_TRUE; + answer_reinvite_manually = PJ_TRUE; + expect_null_offer_response = PJ_TRUE; + null_offer_response_code = 0; + null_offer_response_media_count = 0; + null_offer_response_has_sdp = PJ_FALSE; + null_offer_create_offer_count = 0; + null_offer_reinvite_answer_status = PJ_EUNKNOWN; + + TRACE_((THIS_FILE, " Sending offerless re-INVITE%s; app " + "declines to create offer", + has_unknown_body ? " with unknown body" : "")); + status = pjsip_inv_reinvite(inv_test.uac, NULL, NULL, &tdata); + if (status == PJ_SUCCESS && has_unknown_body) { + pj_str_t type = pj_str("application"); + pj_str_t subtype = pj_str("isup"); + pj_str_t data = pj_str("dummy"); + + tdata->msg->body = pjsip_msg_body_create(tdata->pool, &type, + &subtype, &data); + } + if (status == PJ_SUCCESS) + status = pjsip_inv_send_msg(inv_test.uac, tdata); + if (status != PJ_SUCCESS) { + PJ_LOG(1, (THIS_FILE, + " failed to send offerless re-INVITE (%d)", + status)); + rc = -61; + } else { + for (i = 0; i < 100 && null_offer_response_code == 0; ++i) { + pj_time_val delay = {0, 20}; + pjsip_endpt_handle_events(endpt, &delay); + } + + if (has_unknown_body) { + if (null_offer_response_code != 200 || + null_offer_response_has_sdp || + null_offer_create_offer_count != 0) + { + PJ_LOG(1, (THIS_FILE, + " expected bodyless 200 without offer " + "callback, got %u with SDP=%d callback=%u", + null_offer_response_code, + null_offer_response_has_sdp, + null_offer_create_offer_count)); + rc = -62; + } + } else if (null_offer_response_code != 200 || + !null_offer_response_has_sdp || + null_offer_response_media_count != + active_sdp->media_count || + null_offer_create_offer_count != 1) + { + PJ_LOG(1, (THIS_FILE, + " expected 200 with active SDP (%u media), " + "got %u with SDP=%d (%u media), callback=%u", + active_sdp->media_count, + null_offer_response_code, + null_offer_response_has_sdp, + null_offer_response_media_count, + null_offer_create_offer_count)); + rc = -63; + } + if (!has_unknown_body && + null_offer_reinvite_answer_status != PJ_SUCCESS) + { + PJ_LOG(1, (THIS_FILE, + " asynchronous re-INVITE answer failed (%d)", + null_offer_reinvite_answer_status)); + rc = -64; + } + } + + reject_null_offer = PJ_FALSE; + answer_reinvite_manually = PJ_FALSE; + expect_null_offer_response = PJ_FALSE; + } + } + /* * Hangup */ @@ -510,7 +644,7 @@ static int perform_test(inv_test_param_t *param) flush_events(500); - return 0; + return rc; } @@ -519,6 +653,21 @@ static pj_bool_t log_on_rx_msg(pjsip_rx_data *rdata) pjsip_msg *msg = rdata->msg_info.msg; char info[80]; + if (expect_null_offer_response && msg->type == PJSIP_RESPONSE_MSG && + rdata->msg_info.cseq && + rdata->msg_info.cseq->method.id == PJSIP_INVITE_METHOD && + msg->line.status.code >= 200) + { + pjsip_rdata_sdp_info *sdp_info; + + null_offer_response_code = msg->line.status.code; + sdp_info = pjsip_rdata_get_sdp_info(rdata); + if (sdp_info && sdp_info->sdp) { + null_offer_response_has_sdp = PJ_TRUE; + null_offer_response_media_count = sdp_info->sdp->media_count; + } + } + if (!is_user_equal(rdata->msg_info.from, "inv_offer_answer_test")) return PJ_FALSE; @@ -576,6 +725,29 @@ static inv_test_param_t test_params[] = PJ_FALSE }, + { + "Offerless re-INVITE with NULL application offer", + 0, + PJ_TRUE, + 1, + { OFFERER_UAC }, + PJ_FALSE, + PJ_TRUE + }, + +#if PJSIP_INV_ACCEPT_UNKNOWN_BODY + { + "Re-INVITE with unknown body does not trigger SDP offer", + 0, + PJ_TRUE, + 1, + { OFFERER_UAC }, + PJ_FALSE, + PJ_FALSE, + PJ_TRUE + }, +#endif + { "Standard INVITE with offer, with 100rel", PJSIP_INV_REQUIRE_100REL, @@ -827,6 +999,7 @@ int inv_offer_answer_test(void) pj_bzero(&inv_cb, sizeof(inv_cb)); inv_cb.on_media_update = &on_media_update; inv_cb.on_rx_offer = &on_rx_offer; + inv_cb.on_rx_reinvite = &on_rx_reinvite; inv_cb.on_create_offer = &on_create_offer; inv_cb.on_state_changed = &on_state_changed; inv_cb.on_new_session = &on_new_session; diff --git a/pjsip/src/test/pjsua_call_test.c b/pjsip/src/test/pjsua_call_test.c index 5181a186bf..4094fd8c1e 100644 --- a/pjsip/src/test/pjsua_call_test.c +++ b/pjsip/src/test/pjsua_call_test.c @@ -46,6 +46,11 @@ * PJSUA_CALL_SET_MEDIA_DIR in on_call_rx_reinvite) must actually restart * the running stream, so the live stream's direction and the direction * reported by pjsua_call_get_info() agree. + * 7. An initial offerless INVITE can be answered with an application SDP + * offer, followed by the SDP answer in ACK, synchronously or deferred. + * 8. An offerless re-INVITE can receive a late SDP offer in the 2xx response + * and complete negotiation with the SDP answer in ACK, synchronously or + * deferred. * * Scenarios 5 and 6 are two symptoms of one defect: apply_med_update() derives * the stream info from the negotiated SDP, then adjusts it (RTCP mux off when @@ -231,6 +236,21 @@ static struct pj_bool_t update_reinvite_seen; pjsua_call_id update_reinvite_caller; pj_status_t update_reinvite_status; + pj_bool_t late_offer_armed; + pj_bool_t skip_answer2_overlimit_probe; + pjmedia_sdp_session *late_offer_sdp; + pjsua_call_id late_offer_caller; + unsigned late_offer_initial_med_cnt; + pj_status_t late_offer_answer_status; + pj_status_t late_offer_ack_status; + pj_bool_t late_offer_ack_seen; + pj_bool_t late_offer_async_answer; + pj_bool_t late_offer_async_ack; + pjmedia_sdp_session *late_offer_ack_sdp; + int late_offer_ack_cseq; + pj_bool_t late_offer_reinvite_armed; + pjsua_call_id late_offer_reinvite_callee; + pj_bool_t late_offer_reinvite_seen; /* When set, on_call_sdp_created appends an m=application line to the * local SDP offer, so the peer establishes a call holding a media slot * whose type is neither audio, video nor text. */ @@ -522,6 +542,19 @@ static void on_incoming_call(pjsua_acc_id acc_id, pjsua_call_id call_id, return; } + if (g_ctx.late_offer_armed) { + g_ctx.late_offer_initial_med_cnt = + pjsua_var.calls[call_id].med_cnt; + g_ctx.late_offer_answer_status = PJ_EPENDING; + if (!g_ctx.late_offer_async_answer) { + g_ctx.late_offer_answer_status = + pjsua_call_answer_with_sdp(call_id, g_ctx.late_offer_sdp, + NULL, 200, NULL, NULL); + } + g_ctx.incoming_seen = PJ_TRUE; + return; + } + if (g_ctx.uas_offer_armed) { g_ctx.incoming_seen = PJ_TRUE; return; @@ -626,16 +659,18 @@ static void on_incoming_call(pjsua_acc_id acc_id, pjsua_call_id call_id, return; } - /* Probe: over-limit audio count must be rejected. answer2() leaves - * call->opt_inited FALSE on failure, so the valid answer below still - * applies. - */ - pjsua_call_setting_default(&opt); - opt.aud_cnt = PJSUA_MAX_CALL_MEDIA + 1; - opt.vid_cnt = 0; - opt.txt_cnt = 0; - g_ctx.answer2_overlimit_status = - pjsua_call_answer2(call_id, &opt, 200, NULL, NULL); + if (!g_ctx.skip_answer2_overlimit_probe) { + /* Probe: over-limit audio count must be rejected. answer2() leaves + * call->opt_inited FALSE on failure, so the valid answer below still + * applies. + */ + pjsua_call_setting_default(&opt); + opt.aud_cnt = PJSUA_MAX_CALL_MEDIA + 1; + opt.vid_cnt = 0; + opt.txt_cnt = 0; + g_ctx.answer2_overlimit_status = + pjsua_call_answer2(call_id, &opt, 200, NULL, NULL); + } /* Establish the call with a valid single-audio setting. */ pjsua_call_setting_default(&opt); @@ -649,6 +684,66 @@ static void on_incoming_call(pjsua_acc_id acc_id, pjsua_call_id call_id, g_ctx.incoming_seen = PJ_TRUE; } +static pj_bool_t on_call_send_ack(pjsua_call_id call_id, + pjsip_rx_data *rdata) +{ + pjsua_call *call; + const pjmedia_sdp_session *remote_sdp; + pjmedia_sdp_session *local_sdp; + pj_status_t status; + + if (!g_ctx.late_offer_armed || call_id != g_ctx.late_offer_caller) + return PJ_FALSE; + + g_ctx.late_offer_ack_seen = PJ_TRUE; + call = &pjsua_var.calls[call_id]; + status = pjmedia_sdp_neg_get_neg_remote(call->inv->neg, &remote_sdp); + if (status == PJ_SUCCESS) { + status = pjsua_media_channel_create_sdp(call_id, call->inv->pool, + remote_sdp, &local_sdp, NULL); + } + if (status == PJ_SUCCESS) { + if (g_ctx.late_offer_async_ack) { + g_ctx.late_offer_ack_sdp = local_sdp; + g_ctx.late_offer_ack_cseq = rdata->msg_info.cseq->cseq; + g_ctx.late_offer_ack_status = PJ_EPENDING; + return PJ_TRUE; + } + status = pjsua_call_send_ack(call_id, + rdata->msg_info.cseq->cseq, + local_sdp); + } + g_ctx.late_offer_ack_status = status; + return status == PJ_SUCCESS; +} + +static pj_status_t send_late_offer_ack(void) +{ + pj_status_t status; + + if (!g_ctx.late_offer_ack_sdp) + return PJ_EINVALIDOP; + + status = pjsua_call_send_ack(g_ctx.late_offer_caller, + g_ctx.late_offer_ack_cseq, + g_ctx.late_offer_ack_sdp); + g_ctx.late_offer_ack_status = status; + g_ctx.late_offer_ack_sdp = NULL; + return status; +} + +static pj_bool_t late_offer_ack_received(pjsua_call_id call_id) +{ + PJ_UNUSED_ARG(call_id); + return g_ctx.late_offer_ack_seen; +} + +static pj_bool_t late_offer_reinvite_received(pjsua_call_id call_id) +{ + PJ_UNUSED_ARG(call_id); + return g_ctx.late_offer_reinvite_seen; +} + /* Optionally append an m=application line to a locally-created SDP offer. * PJSUA syncs med_prov_cnt up to the callback's SDP media count, and the * remote peer that answers this offer keeps the extra m-line as a disabled @@ -919,6 +1014,15 @@ static void on_call_rx_reinvite(pjsua_call_id call_id, PJ_UNUSED_ARG(rdata); PJ_UNUSED_ARG(reserved); + if (g_ctx.late_offer_reinvite_armed && + call_id == g_ctx.late_offer_reinvite_callee && !offer) + { + g_ctx.late_offer_reinvite_seen = PJ_TRUE; + if (g_ctx.late_offer_async_answer) + *async = PJ_TRUE; + return; + } + if (g_ctx.offerless_armed && !offer && g_ctx.managed_reinv_response) { *code = g_ctx.managed_reinv_response; *async = PJ_FALSE; @@ -6272,6 +6376,299 @@ static int test_async_reoffer_no_active_media(void) /***************************************************************************** * Main entry point *****************************************************************************/ +static int test_answer_with_pending_local_offer(void) +{ + pjsua_call_id caller = PJSUA_INVALID_ID, callee = PJSUA_INVALID_ID; + pjsip_inv_session *inv; + const pjmedia_sdp_session *active = NULL; + const pjmedia_sdp_session *pending = NULL; + const pjmedia_sdp_session *retained = NULL; + pj_status_t status; + int rc; + + rc = establish_self_call(&caller, &callee, -1900); + if (rc != 0) + goto on_return; + + inv = pjsua_var.calls[callee].inv; + status = pjmedia_sdp_neg_get_active_local(inv->neg, &active); + if (status != PJ_SUCCESS) { + rc = -1910; + goto on_return; + } + + status = pjsip_inv_set_local_sdp(inv, active); + if (status != PJ_SUCCESS || + pjmedia_sdp_neg_get_neg_local(inv->neg, &pending) != PJ_SUCCESS) + { + rc = -1911; + goto on_return; + } + + status = pjsua_call_answer_with_sdp(callee, active, NULL, 200, NULL, NULL); + if (status != PJMEDIA_SDPNEG_EINSTATE || + pjmedia_sdp_neg_get_state(inv->neg) != + PJMEDIA_SDP_NEG_STATE_LOCAL_OFFER || + pjmedia_sdp_neg_get_neg_local(inv->neg, &retained) != PJ_SUCCESS || + retained != pending) + { + rc = -1912; + goto on_return; + } + +on_return: + drain_all_calls(); + return rc; +} + +static int test_initial_late_sdp_offer(pj_bool_t async_answer, + pj_bool_t async_ack) +{ + pjsua_call_id seed_caller = PJSUA_INVALID_ID; + pjsua_call_id seed_callee = PJSUA_INVALID_ID; + pjsua_call_id caller = PJSUA_INVALID_ID; + pjsua_call_id callee = PJSUA_INVALID_ID; + pjsua_call_setting opt; + pjsip_inv_session *inv; + const pjmedia_sdp_session *seed_sdp = NULL; + pj_str_t uri = pj_str(g_ctx.self_uri); + pj_status_t status; + int rc = 0; + + PJ_LOG(3, (THIS_FILE, " initial late SDP offer (answer=%s ACK=%s)", + async_answer ? "async" : "sync", + async_ack ? "async" : "sync")); + + g_ctx.skip_answer2_overlimit_probe = PJ_TRUE; + rc = establish_self_call(&seed_caller, &seed_callee, -1930); + g_ctx.skip_answer2_overlimit_probe = PJ_FALSE; + if (rc != 0) + goto on_return; + + inv = pjsua_var.calls[seed_callee].inv; + status = pjmedia_sdp_neg_get_active_local(inv->neg, &seed_sdp); + if (status != PJ_SUCCESS) { + rc = -1936; + goto on_return; + } + g_ctx.late_offer_sdp = pjmedia_sdp_session_clone(pjsua_var.pool, + seed_sdp); + if (!g_ctx.late_offer_sdp) { + rc = -1937; + goto on_return; + } + drain_all_calls(); + + g_ctx.late_offer_armed = PJ_TRUE; + g_ctx.incoming_seen = PJ_FALSE; + g_ctx.incoming_call_id = PJSUA_INVALID_ID; + g_ctx.late_offer_caller = PJSUA_INVALID_ID; + g_ctx.late_offer_initial_med_cnt = 1; + g_ctx.late_offer_answer_status = PJ_EUNKNOWN; + g_ctx.late_offer_ack_status = PJ_EUNKNOWN; + g_ctx.late_offer_ack_seen = PJ_FALSE; + g_ctx.late_offer_async_answer = async_answer; + g_ctx.late_offer_async_ack = async_ack; + g_ctx.late_offer_ack_sdp = NULL; + pj_bzero(g_ctx.med_state_cnt, sizeof(g_ctx.med_state_cnt)); + + pjsua_call_setting_default(&opt); + opt.aud_cnt = 1; + opt.vid_cnt = 0; + opt.txt_cnt = 0; + opt.flag |= PJSUA_CALL_NO_SDP_OFFER; + status = pjsua_call_make_call(g_ctx.acc_id, &uri, &opt, NULL, NULL, + &caller); + if (status != PJ_SUCCESS) { + rc = -1938; + goto on_return; + } + g_ctx.late_offer_caller = caller; + + if (async_answer) { + if (!wait_until(&uas_offer_received, caller, 8000)) { + rc = -1943; + goto on_return; + } + callee = g_ctx.incoming_call_id; + g_ctx.late_offer_answer_status = + pjsua_call_answer_with_sdp(callee, g_ctx.late_offer_sdp, + NULL, PJSIP_SC_OK, NULL, NULL); + if (g_ctx.late_offer_answer_status != PJ_SUCCESS && + g_ctx.late_offer_answer_status != PJ_EPENDING) + { + rc = -1944; + goto on_return; + } + } + + if (async_ack) { + if (!wait_until(&late_offer_ack_received, caller, 8000) || + g_ctx.late_offer_ack_status != PJ_EPENDING || + send_late_offer_ack() != PJ_SUCCESS) + { + rc = -1945; + goto on_return; + } + } + + if (!wait_until(&call_is_confirmed, caller, 8000)) { + rc = -1939; + goto on_return; + } + callee = g_ctx.incoming_call_id; + if (callee == PJSUA_INVALID_ID || + !wait_until(&call_is_confirmed, callee, 8000)) + { + rc = -1940; + goto on_return; + } + + g_ctx.med_state_target = 1; + if (!wait_until(&media_state_reached, caller, 8000) || + !wait_until(&media_state_reached, callee, 8000)) + { + rc = -1941; + goto on_return; + } + + if (!g_ctx.incoming_seen || g_ctx.late_offer_initial_med_cnt != 0 || + (g_ctx.late_offer_answer_status != PJ_SUCCESS && + g_ctx.late_offer_answer_status != PJ_EPENDING) || + !g_ctx.late_offer_ack_seen || + g_ctx.late_offer_ack_status != PJ_SUCCESS || + pjsua_var.calls[callee].med_cnt == 0 || + pjsua_var.calls[caller].med_cnt == 0 || + !pjsua_var.calls[callee].med_update_success || + !pjsua_var.calls[caller].med_update_success || + pjmedia_sdp_neg_get_state(pjsua_var.calls[callee].inv->neg) != + PJMEDIA_SDP_NEG_STATE_DONE || + pjmedia_sdp_neg_get_state(pjsua_var.calls[caller].inv->neg) != + PJMEDIA_SDP_NEG_STATE_DONE) + { + PJ_LOG(1, (THIS_FILE, " late SDP offer flow failed: initial media=" + "%u answer=%d ACK seen=%d ACK status=%d callee media=%u", + g_ctx.late_offer_initial_med_cnt, + g_ctx.late_offer_answer_status, + g_ctx.late_offer_ack_seen, + g_ctx.late_offer_ack_status, + pjsua_var.calls[callee].med_cnt)); + rc = -1942; + } + +on_return: + g_ctx.late_offer_armed = PJ_FALSE; + g_ctx.late_offer_async_answer = PJ_FALSE; + g_ctx.late_offer_async_ack = PJ_FALSE; + g_ctx.late_offer_ack_sdp = NULL; + g_ctx.late_offer_sdp = NULL; + drain_all_calls(); + return rc; +} + +static int test_late_sdp_reinvite(pj_bool_t async_answer, + pj_bool_t async_ack) +{ + pjsua_call_id caller = PJSUA_INVALID_ID; + pjsua_call_id callee = PJSUA_INVALID_ID; + pjsua_call_setting opt; + const pjmedia_sdp_session *active; + pj_status_t status; + int rc; + + PJ_LOG(3, (THIS_FILE, " late SDP re-INVITE (answer=%s ACK=%s)", + async_answer ? "async" : "sync", + async_ack ? "async" : "sync")); + + g_ctx.skip_answer2_overlimit_probe = PJ_TRUE; + rc = establish_self_call(&caller, &callee, -1960); + g_ctx.skip_answer2_overlimit_probe = PJ_FALSE; + if (rc != 0) + goto on_return; + + g_ctx.late_offer_armed = PJ_TRUE; + g_ctx.late_offer_caller = caller; + g_ctx.late_offer_ack_status = PJ_EUNKNOWN; + g_ctx.late_offer_ack_seen = PJ_FALSE; + g_ctx.late_offer_async_answer = async_answer; + g_ctx.late_offer_async_ack = async_ack; + g_ctx.late_offer_ack_sdp = NULL; + g_ctx.late_offer_reinvite_armed = PJ_TRUE; + g_ctx.late_offer_reinvite_callee = callee; + g_ctx.late_offer_reinvite_seen = PJ_FALSE; + + pjsua_call_setting_default(&opt); + opt.aud_cnt = 1; + opt.vid_cnt = 0; + opt.txt_cnt = 0; + opt.flag |= PJSUA_CALL_NO_SDP_OFFER; + status = pjsua_call_reinvite2(caller, &opt, NULL); + if (status != PJ_SUCCESS) { + rc = -1966; + goto on_return; + } + + if (!wait_until(&late_offer_reinvite_received, callee, + TEST_TIMEOUT_MSEC)) + { + rc = -1967; + goto on_return; + } + + if (async_answer) { + status = pjmedia_sdp_neg_get_active_local( + pjsua_var.calls[callee].inv->neg, + &active); + if (status == PJ_SUCCESS) + status = pjsua_call_answer_with_sdp(callee, active, NULL, + PJSIP_SC_OK, NULL, NULL); + if (status != PJ_SUCCESS && status != PJ_EPENDING) { + rc = -1968; + goto on_return; + } + } + + if (async_ack && + (!wait_until(&late_offer_ack_received, caller, TEST_TIMEOUT_MSEC) || + g_ctx.late_offer_ack_status != PJ_EPENDING || + send_late_offer_ack() != PJ_SUCCESS)) + { + rc = -1969; + goto on_return; + } + + if (!wait_until(&reinvite_tsx_done, caller, TEST_TIMEOUT_MSEC) || + !wait_until(&reinvite_tsx_done, callee, TEST_TIMEOUT_MSEC) || + !g_ctx.late_offer_reinvite_seen || !g_ctx.late_offer_ack_seen || + g_ctx.late_offer_ack_status != PJ_SUCCESS || + !call_is_confirmed(caller) || !call_is_confirmed(callee) || + pjmedia_sdp_neg_get_state(pjsua_var.calls[caller].inv->neg) != + PJMEDIA_SDP_NEG_STATE_DONE || + pjmedia_sdp_neg_get_state(pjsua_var.calls[callee].inv->neg) != + PJMEDIA_SDP_NEG_STATE_DONE) + { + PJ_LOG(1, (THIS_FILE, " late SDP re-INVITE failed: NULL offer=" + "%d ACK seen=%d ACK status=%d caller confirmed=%d " + "callee confirmed=%d", + g_ctx.late_offer_reinvite_seen, + g_ctx.late_offer_ack_seen, + g_ctx.late_offer_ack_status, + call_is_confirmed(caller), call_is_confirmed(callee))); + rc = -1967; + } + +on_return: + g_ctx.late_offer_armed = PJ_FALSE; + g_ctx.late_offer_async_answer = PJ_FALSE; + g_ctx.late_offer_async_ack = PJ_FALSE; + g_ctx.late_offer_ack_sdp = NULL; + g_ctx.late_offer_reinvite_armed = PJ_FALSE; + g_ctx.late_offer_reinvite_callee = PJSUA_INVALID_ID; + drain_all_calls(); + return rc; +} + + int pjsua_call_test(void) { extern pjsip_endpoint *endpt; /* test framework endpoint */ @@ -6500,6 +6897,9 @@ int pjsua_call_test(void) rc = test_async_reoffer_no_active_media(); if (rc != 0) goto on_return; + rc = test_answer_with_pending_local_offer(); + if (rc != 0) goto on_return; + on_return: drain_all_calls(); restore_msg_size(&lib_settings); @@ -6519,3 +6919,149 @@ int pjsua_call_test(void) return rc; } + +int pjsua_late_sdp_offer_test(void) +{ + extern pjsip_endpoint *endpt; + extern pj_caching_pool caching_pool; + msg_size_saved lib_settings; + pjsua_config ua_cfg; + pjsua_logging_config log_cfg; + pjsua_media_config media_cfg; + pjsua_transport_config tp_cfg; + pjsua_transport_id tp_id; + pj_uint16_t port; + pj_status_t status; + pj_bool_t settings_saved = PJ_FALSE; + int rc = 0; + + PJ_LOG(3, (THIS_FILE, "pjsua late SDP offer tests")); + pj_bzero(&lib_settings, sizeof(lib_settings)); + + /* PJSUA registers the on_send_ack hook globally, so isolate this test in + * its own instance instead of changing ACK handling in other call tests. + */ + pjsip_endpt_destroy(endpt); + endpt = NULL; + + status = pjsua_create(); + if (status != PJ_SUCCESS) { + PJ_LOG(1, (THIS_FILE, " pjsua_create failed (%d)", status)); + rc = -1950; + goto on_restore; + } + + pjsua_config_default(&ua_cfg); + ua_cfg.cb.on_incoming_call = &on_incoming_call; + ua_cfg.cb.on_call_send_ack = &on_call_send_ack; + ua_cfg.cb.on_call_rx_reinvite = &on_call_rx_reinvite; + ua_cfg.cb.on_stream_created2 = &on_stream_created2; + ua_cfg.cb.on_stream_destroyed = &on_stream_destroyed; + ua_cfg.cb.on_call_media_state = &on_call_media_state; + ua_cfg.thread_cnt = 0; + ua_cfg.require_100rel = PJSUA_100REL_NOT_USED; + ua_cfg.use_timer = PJSUA_SIP_TIMER_INACTIVE; + ua_cfg.use_srtp = PJMEDIA_SRTP_DISABLED; + + pjsua_logging_config_default(&log_cfg); + log_cfg.level = 3; + log_cfg.console_level = 3; + + pjsua_media_config_default(&media_cfg); + media_cfg.no_vad = PJ_TRUE; + media_cfg.enable_ice = PJ_FALSE; + + status = pjsua_init(&ua_cfg, &log_cfg, &media_cfg); + if (status != PJ_SUCCESS) { + PJ_LOG(1, (THIS_FILE, " pjsua_init failed (%d)", status)); + pjsua_destroy(); + rc = -1951; + goto on_restore; + } + + pjsua_transport_config_default(&tp_cfg); + tp_cfg.port = 0; + status = pjsua_transport_create(PJSIP_TRANSPORT_UDP, &tp_cfg, &tp_id); + if (status != PJ_SUCCESS) { + PJ_LOG(1, (THIS_FILE, " transport_create failed (%d)", status)); + pjsua_destroy(); + rc = -1952; + goto on_restore; + } + + status = pjsua_start(); + if (status != PJ_SUCCESS) { + PJ_LOG(1, (THIS_FILE, " pjsua_start failed (%d)", status)); + pjsua_destroy(); + rc = -1953; + goto on_restore; + } + + status = pjsua_set_null_snd_dev(); + if (status != PJ_SUCCESS) { + PJ_LOG(1, (THIS_FILE, " set_null_snd_dev failed (%d)", status)); + pjsua_destroy(); + rc = -1954; + goto on_restore; + } + + status = pjsua_acc_add_local(tp_id, PJ_TRUE, &g_ctx.acc_id); + if (status != PJ_SUCCESS) { + PJ_LOG(1, (THIS_FILE, " acc_add_local failed (%d)", status)); + pjsua_destroy(); + rc = -1955; + goto on_restore; + } + + { + pjsua_transport_info ti; + status = pjsua_transport_get_info(tp_id, &ti); + if (status != PJ_SUCCESS) { + PJ_LOG(1, (THIS_FILE, " transport_get_info failed (%d)", status)); + pjsua_destroy(); + rc = -1956; + goto on_restore; + } + port = pj_sockaddr_get_port(&ti.local_addr); + } + pj_ansi_snprintf(g_ctx.self_uri, sizeof(g_ctx.self_uri), + "sip:%s@127.0.0.1:%u", TEST_USER, (unsigned)port); + + minimize_msg_size(&lib_settings); + settings_saved = PJ_TRUE; + g_ctx.late_offer_armed = PJ_FALSE; + g_ctx.med_state_target = 1; + + rc = test_initial_late_sdp_offer(PJ_FALSE, PJ_FALSE); + if (rc == 0) + rc = test_initial_late_sdp_offer(PJ_TRUE, PJ_FALSE); + if (rc == 0) + rc = test_initial_late_sdp_offer(PJ_FALSE, PJ_TRUE); + if (rc == 0) + rc = test_initial_late_sdp_offer(PJ_TRUE, PJ_TRUE); + if (rc == 0) + rc = test_late_sdp_reinvite(PJ_FALSE, PJ_FALSE); + if (rc == 0) + rc = test_late_sdp_reinvite(PJ_TRUE, PJ_FALSE); + if (rc == 0) + rc = test_late_sdp_reinvite(PJ_FALSE, PJ_TRUE); + if (rc == 0) + rc = test_late_sdp_reinvite(PJ_TRUE, PJ_TRUE); + + drain_all_calls(); + if (settings_saved) + restore_msg_size(&lib_settings); + pjsua_destroy2(PJSUA_DESTROY_NO_RX_MSG); + +on_restore: + status = pjsip_endpt_create(&caching_pool.factory, "endpt", &endpt); + if (status != PJ_SUCCESS) { + PJ_PERROR(1, (THIS_FILE, status, "Error recreating endpoint")); + return -1957; + } + status = pjsip_tsx_layer_init_module(endpt); + if (status != PJ_SUCCESS) + PJ_PERROR(1, (THIS_FILE, status, "Error re-initializing tsx layer")); + + return rc; +} diff --git a/pjsip/src/test/test.c b/pjsip/src/test/test.c index 6bffdb15f7..a5785cfe19 100644 --- a/pjsip/src/test/test.c +++ b/pjsip/src/test/test.c @@ -490,6 +490,8 @@ int test_main(int argc, char *argv[]) #if INCLUDE_PJSUA_CALL_TEST UT_ADD_TEST(&test_app.ut_app, pjsua_call_test, PJ_TEST_EXCLUSIVE | PJ_TEST_KEEP_LAST); + UT_ADD_TEST(&test_app.ut_app, pjsua_late_sdp_offer_test, + PJ_TEST_EXCLUSIVE | PJ_TEST_KEEP_LAST); #endif #if INCLUDE_PJSUA_ACC_TEST diff --git a/pjsip/src/test/test.h b/pjsip/src/test/test.h index 4ac0fea50a..eb1b52ff77 100644 --- a/pjsip/src/test/test.h +++ b/pjsip/src/test/test.h @@ -133,6 +133,7 @@ int regc_test(void); int auth_async_test(void); int pjsua_auth_test(void); int pjsua_call_test(void); +int pjsua_late_sdp_offer_test(void); int pjsua_acc_test(void); int pjsua_pres_test(void); int srv_failover_pjsua_test(void); diff --git a/tests/pjsua/scripts-sipp/alt-pjsua-uac-late-offer-dummy-codecs.py b/tests/pjsua/scripts-sipp/alt-pjsua-uac-late-offer-dummy-codecs.py index 29e829000c..6a94347b30 100644 --- a/tests/pjsua/scripts-sipp/alt-pjsua-uac-late-offer-dummy-codecs.py +++ b/tests/pjsua/scripts-sipp/alt-pjsua-uac-late-offer-dummy-codecs.py @@ -1,5 +1,5 @@ # -# Test driver for alt-pjsua-uac-late-offer.xml +# Test driver for alt-pjsua-uac-late-offer-dummy-codecs.xml # # alt_pjsua acts as UAC and sends an SDP-less INVITE to SIPp (UAS). SIPp # returns an SDP offer in its 200 OK, which alt_pjsua answers in the ACK. SIPp diff --git a/tests/pjsua/scripts-sipp/alt-pjsua-uas-late-offer-dummy-codecs.py b/tests/pjsua/scripts-sipp/alt-pjsua-uas-late-offer-dummy-codecs.py index 27524edd70..b00e836199 100644 --- a/tests/pjsua/scripts-sipp/alt-pjsua-uas-late-offer-dummy-codecs.py +++ b/tests/pjsua/scripts-sipp/alt-pjsua-uas-late-offer-dummy-codecs.py @@ -1,5 +1,5 @@ # -# Test driver for alt-pjsua-uas-late-offer.xml +# Test driver for alt-pjsua-uas-late-offer-dummy-codecs.xml # # alt_pjsua acts as UAS and auto-answers an SDP-less INVITE from SIPp # (UAC). alt_pjsua has no offer to answer, so it puts its own SDP offer @@ -7,10 +7,6 @@ # STATE_CONFIRMED. # This scenario tests the handling of late SDP offers with dummy codecs. # -# Run with standard pjsua: -# cd tests/pjsua && python3 run.py mod_sipp.py \ -# scripts-sipp/alt-pjsua-uas-late-offer.xml -# # Run with alt_pjsua: # cd tests/pjsua && python3 run.py \ # --exe ../../pjsip-apps/src/3rdparty_media_sample/alt_pjsua \ diff --git a/tests/pjsua/scripts-sipp/alt-pjsua-uas-late-offer.py b/tests/pjsua/scripts-sipp/alt-pjsua-uas-late-offer.py index a4762a0b6d..aab99a4b94 100644 --- a/tests/pjsua/scripts-sipp/alt-pjsua-uas-late-offer.py +++ b/tests/pjsua/scripts-sipp/alt-pjsua-uas-late-offer.py @@ -6,10 +6,6 @@ # in the 200 OK; SIPp answers it in the ACK. The call should reach # STATE_CONFIRMED. # -# Run with standard pjsua: -# cd tests/pjsua && python3 run.py mod_sipp.py \ -# scripts-sipp/alt-pjsua-uas-late-offer.xml -# # Run with alt_pjsua: # cd tests/pjsua && python3 run.py \ # --exe ../../pjsip-apps/src/3rdparty_media_sample/alt_pjsua \ From a6523aecb235092fc17b4497ac6c2dde74bd23d0 Mon Sep 17 00:00:00 2001 From: LEOPOLD Otakar Date: Wed, 14 Oct 2026 08:29:51 +0000 Subject: [PATCH 5/8] Fixes - round 4 - added validation of initial local SDP offer and check that sdp media count matches call media(comment 1) - onCallSendAck() is guarded with try block(comment 2) - added fallback to acquire_call() when call_id doesn't match(comment 3) - apply_call_setting() is called only when options changed(comment 4) - added missing condition(comment 5) - fixed wording(comment 6) --- .../src/3rdparty_media_sample/alt_pjsua_aud.c | 2 +- pjsip/include/pjsua-lib/pjsua_internal.h | 5 + pjsip/include/pjsua2/call.hpp | 10 +- pjsip/src/pjsua-lib/pjsua_call.c | 76 +++++- pjsip/src/pjsua2/endpoint.cpp | 7 +- pjsip/src/test/inv_offer_answer_test.c | 2 +- pjsip/src/test/pjsua_call_test.c | 228 ++++++++++++++++++ .../alt-pjsua-uas-late-offer-dummy-codecs.xml | 2 +- 8 files changed, 316 insertions(+), 16 deletions(-) diff --git a/pjsip-apps/src/3rdparty_media_sample/alt_pjsua_aud.c b/pjsip-apps/src/3rdparty_media_sample/alt_pjsua_aud.c index 26dbd25237..dfbeed5cc3 100644 --- a/pjsip-apps/src/3rdparty_media_sample/alt_pjsua_aud.c +++ b/pjsip-apps/src/3rdparty_media_sample/alt_pjsua_aud.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2008-2011 Teluu Inc. (http://www.teluu.com) + * Copyright (C) 2008-2026 Teluu Inc. (http://www.teluu.com) * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/pjsip/include/pjsua-lib/pjsua_internal.h b/pjsip/include/pjsua-lib/pjsua_internal.h index e19383a4d0..6a80d1fbf5 100644 --- a/pjsip/include/pjsua-lib/pjsua_internal.h +++ b/pjsip/include/pjsua-lib/pjsua_internal.h @@ -942,6 +942,11 @@ pj_status_t pjsua_acc_get_uac_dlg_addr(pjsua_acc_id acc_id, */ pj_bool_t pjsua_call_on_incoming(pjsip_rx_data *rdata); +pj_status_t pjsua_call_validate_initial_local_offer( + const pjsua_call *call, + const pjsua_call_setting *opt, + const pjmedia_sdp_session *sdp); + /* * Media channel. */ diff --git a/pjsip/include/pjsua2/call.hpp b/pjsip/include/pjsua2/call.hpp index 62e65860fa..157dee2cb9 100644 --- a/pjsip/include/pjsua2/call.hpp +++ b/pjsip/include/pjsua2/call.hpp @@ -850,7 +850,7 @@ struct OnCallSendAckParam SipRxData rdata; /** - * Output: application sets this to true if it has sent (or will send) + * Output: application sets this to true if it has sent (or will send) * the ACK itself, to suppress the automatic ACK sending. Default is false, * i.e. the library sends the ACK after this callback returns. */ @@ -860,7 +860,7 @@ struct OnCallSendAckParam * The CSeq of the 2xx response. */ pj_int32_t cseq; - + /** * Default constructor */ @@ -2188,9 +2188,9 @@ class Call * Notify application when the framework needs to send an ACK request * after it receives an incoming 2xx response for INVITE (e.g. when the * 2xx response carries a late SDP offer). The default implementation - * simply do nothing and let's framework send the ACK without any SDP answer. - * Application can set prm.suppressSendAck to true and than the application - * is responsible for sending the ACK. + * does nothing and lets the framework send the ACK. Application can + * set prm.suppressSendAck to true and then the application is responsible + * for sending the ACK. * * This callback is enabled only when * UaConfig::enableCallSendAckCallback is set before Endpoint::libInit(). diff --git a/pjsip/src/pjsua-lib/pjsua_call.c b/pjsip/src/pjsua-lib/pjsua_call.c index 8b32e08eef..7dff1ff9d3 100644 --- a/pjsip/src/pjsua-lib/pjsua_call.c +++ b/pjsip/src/pjsua-lib/pjsua_call.c @@ -841,6 +841,53 @@ static pj_status_t validate_managed_sdp(pj_bool_t media_app_managed, return validate_app_sdp(sdp, inv); } +pj_status_t pjsua_call_validate_initial_local_offer( + const pjsua_call *call, + const pjsua_call_setting *opt, + const pjmedia_sdp_session *sdp) +{ + const pjsua_call_setting *effective_opt = + (opt && !call->opt_inited) ? opt : &call->opt; + unsigned media_count = 0; + pj_bool_t prepared = call->med_ch_cb || call->med_prov_cnt != 0; + pj_status_t status = validate_app_sdp(sdp, NULL); + + if (status != PJ_SUCCESS) + return status; + + if (prepared) { + media_count = call->med_prov_cnt; + } else { + if (effective_opt->flag & PJSUA_CALL_MEDIA_APP_MANAGED) + return PJ_EINVALIDOP; + if (effective_opt->aud_cnt > PJSUA_MAX_CALL_MEDIA || + effective_opt->vid_cnt > PJSUA_MAX_CALL_MEDIA || + effective_opt->txt_cnt > PJSUA_MAX_CALL_MEDIA || + effective_opt->aud_cnt + effective_opt->vid_cnt + + effective_opt->txt_cnt > PJSUA_MAX_CALL_MEDIA) + { + return PJ_ETOOMANY; + } + + media_count = effective_opt->aud_cnt + effective_opt->vid_cnt + + effective_opt->txt_cnt; + + if (effective_opt->flag & PJSUA_CALL_INCLUDE_DISABLED_MEDIA) { + if (!effective_opt->aud_cnt && media_count < PJSUA_MAX_CALL_MEDIA) + ++media_count; +#if PJMEDIA_HAS_VIDEO + if (!effective_opt->vid_cnt && media_count < PJSUA_MAX_CALL_MEDIA) + ++media_count; +#endif + if (!effective_opt->txt_cnt && media_count < PJSUA_MAX_CALL_MEDIA) + ++media_count; + } + } + + return sdp->media_count > media_count ? PJMEDIA_SDPNEG_EMISMEDIA : + PJ_SUCCESS; +} + static pj_status_t apply_call_setting(pjsua_call *call, const pjsua_call_setting *opt, const pjmedia_sdp_session *rem_sdp) @@ -3556,7 +3603,7 @@ pjsua_call_answer_with_sdp(pjsua_call_id call_id, /* For an initial offerless INVITE, let answer2() initialize media before * setting the supplied SDP offer. */ - if (code < 300 && sdp && + if (code < 300 && sdp && !call->med_ch_cb && (call->opt_inited || code == 183 || code / 100 == 2) && (!call->inv->neg || pjmedia_sdp_neg_get_state(call->inv->neg) == @@ -3567,6 +3614,14 @@ pjsua_call_answer_with_sdp(pjsua_call_id call_id, return PJ_EBUSY; } + status = pjsua_call_validate_initial_local_offer(call, opt, sdp); + if (status != PJ_SUCCESS) { + pjsua_perror(THIS_FILE, "SDP offer does not match call media", + status); + pjsip_dlg_dec_lock(dlg); + return status; + } + pending_sdp = pjmedia_sdp_session_clone(call->inv->pool, sdp); if (!pending_sdp) { pjsip_dlg_dec_lock(dlg); @@ -3647,7 +3702,7 @@ static pj_status_t call_inv_send_ack(pjsip_inv_session *inv, if (sdp) { if (inv->neg == NULL) return PJMEDIA_SDPNEG_EINSTATE; - + neg_state = pjmedia_sdp_neg_get_state(inv->neg); if (neg_state != PJMEDIA_SDP_NEG_STATE_REMOTE_OFFER && neg_state != PJMEDIA_SDP_NEG_STATE_WAIT_NEGO) @@ -3702,11 +3757,11 @@ PJ_DEF(pj_status_t) pjsua_call_send_ack(pjsua_call_id call_id, if (callback_inv) { call = (pjsua_call*) callback_inv->dlg->mod_data[pjsua_var.mod.id]; if (!call || call->index != call_id) { - status = PJ_EINVALIDOP; - goto on_return; + callback_inv = NULL; } } - else { + + if (!callback_inv) { status = acquire_call("pjsua_call_send_ack()", call_id, &call, &dlg); if (status != PJ_SUCCESS) { goto on_return; @@ -6932,6 +6987,7 @@ static pj_status_t pjsua_call_on_rx_reinvite(pjsip_inv_session *inv, pjsua_var.ua_cfg.cb.on_call_rx_reinvite)) { pjsua_call_setting opt; + pjsua_call_setting callback_opt; pjsip_status_code code = PJSIP_SC_OK; pjsip_tx_data *response = NULL; pj_status_t status; @@ -6943,6 +6999,7 @@ static pj_status_t pjsua_call_on_rx_reinvite(pjsip_inv_session *inv, */ opt = call->opt; pjsua_call_cleanup_flag(&opt); + callback_opt = opt; async = PJ_FALSE; if (pjsua_var.ua_cfg.cb.on_call_rx_reinvite) { @@ -6961,8 +7018,13 @@ static pj_status_t pjsua_call_on_rx_reinvite(pjsip_inv_session *inv, if (code != PJSIP_SC_OK) async = PJ_FALSE; - /* Apply callback settings only when accepting the re-INVITE. */ - if (code == PJSIP_SC_OK) { + /* Apply callback settings only when accepting the re-INVITE and the + * settings have changed or are app-managed. + */ + if (code == PJSIP_SC_OK && + (pjsua_call_media_is_app_managed(call) || + pj_memcmp(&opt, &callback_opt, sizeof(opt)) != 0)) + { status = apply_call_setting(call, &opt, NULL); if (status != PJ_SUCCESS) { PJ_PERROR(3,(THIS_FILE, status, diff --git a/pjsip/src/pjsua2/endpoint.cpp b/pjsip/src/pjsua2/endpoint.cpp index 55b67b6e92..cb42b34bc9 100644 --- a/pjsip/src/pjsua2/endpoint.cpp +++ b/pjsip/src/pjsua2/endpoint.cpp @@ -1587,7 +1587,12 @@ pj_bool_t Endpoint::on_call_send_ack(pjsua_call_id call_id, prm.rdata.fromPj(*rdata); prm.cseq = rdata->msg_info.cseq->cseq; - call->onCallSendAck(prm); + try { + call->onCallSendAck(prm); + } catch (Error &err) { + PJ_LOG(1,(THIS_FILE, "onCallSendAck() error: %s", err.info().c_str())); + return PJ_FALSE; + } return prm.suppressSendAck ? PJ_TRUE : PJ_FALSE; } diff --git a/pjsip/src/test/inv_offer_answer_test.c b/pjsip/src/test/inv_offer_answer_test.c index b18a6015cf..26bc7a5cf4 100644 --- a/pjsip/src/test/inv_offer_answer_test.c +++ b/pjsip/src/test/inv_offer_answer_test.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2008-2011 Teluu Inc. (http://www.teluu.com) + * Copyright (C) 2008-2026 Teluu Inc. (http://www.teluu.com) * Copyright (C) 2003-2008 Benny Prijono * * This program is free software; you can redistribute it and/or modify diff --git a/pjsip/src/test/pjsua_call_test.c b/pjsip/src/test/pjsua_call_test.c index 4094fd8c1e..9898f4658c 100644 --- a/pjsip/src/test/pjsua_call_test.c +++ b/pjsip/src/test/pjsua_call_test.c @@ -51,6 +51,8 @@ * 8. An offerless re-INVITE can receive a late SDP offer in the 2xx response * and complete negotiation with the SDP answer in ACK, synchronously or * deferred. + * 9. An application-supplied initial local offer must match the prepared + * media slots and must not mutate call state when rejected. * * Scenarios 5 and 6 are two symptoms of one defect: apply_med_update() derives * the stream info from the negotiated SDP, then adjusts it (RTCP mux off when @@ -406,6 +408,213 @@ static pj_status_t parse_sdp_text(pj_pool_t *pool, const char *text, return pjmedia_sdp_parse(pool, buf, len, sdp); } +static pj_status_t initial_offer_media_tp_pending(pjsua_call_id call_id, + const pjsua_med_tp_state_info *info) +{ + PJ_UNUSED_ARG(call_id); + PJ_UNUSED_ARG(info); + return PJ_SUCCESS; +} + +static int check_initial_local_offer(pjsua_call *call, + const pjsua_call_setting *opt, + const pjmedia_sdp_session *sdp, + pj_status_t expected, + int test_id) +{ + pjsua_call_setting saved_opt = call->opt; + unsigned saved_med_prov_cnt = call->med_prov_cnt; + pjsua_med_tp_state_cb saved_med_ch_cb = call->med_ch_cb; + pjmedia_sdp_session *saved_answer_sdp = call->answer_sdp; + pjsua_call_media saved_media[PJSUA_MAX_CALL_MEDIA]; + pj_status_t status; + + pj_memcpy(saved_media, call->media_prov, sizeof(saved_media)); + status = pjsua_call_validate_initial_local_offer(call, opt, sdp); + if (status != expected || + pj_memcmp(&call->opt, &saved_opt, sizeof(saved_opt)) != 0 || + call->med_prov_cnt != saved_med_prov_cnt || + call->med_ch_cb != saved_med_ch_cb || + call->answer_sdp != saved_answer_sdp || + pj_memcmp(call->media_prov, saved_media, sizeof(saved_media)) != 0) + { + PJ_LOG(1, (THIS_FILE, + "Initial local SDP validation case %d: expected %d, got %d " + "or mutated call media state", + test_id, expected, status)); + return -test_id; + } + + return 0; +} + +static int test_initial_local_offer_validation(void) +{ + static const char offer_text[] = + "v=0\r\n" + "o=- 1 1 IN IP4 127.0.0.1\r\n" + "s=offer-test\r\n" + "c=IN IP4 127.0.0.1\r\n" + "t=0 0\r\n" + "m=audio 4000 RTP/AVP 0\r\n" + "a=rtpmap:0 PCMU/8000\r\n" + "m=video 4002 RTP/AVP 96\r\n" + "a=rtpmap:96 H264/90000\r\n" + "m=text 4004 RTP/AVP 98\r\n" + "a=rtpmap:98 t140/1000\r\n"; + pj_pool_t *pool; + pjmedia_sdp_session *sdp; + pjsua_call call; + pjsua_call_setting opt; + pjmedia_transport *fake_tp = (pjmedia_transport*)&call; + int rc; + + pool = pjsua_pool_create("sdp-media-test", 2048, 2048); + if (!pool) + return -2010; + rc = parse_sdp_text(pool, offer_text, &sdp); + if (rc != PJ_SUCCESS) { + pj_pool_release(pool); + return -2011; + } + + pj_bzero(&call, sizeof(call)); + pjsua_call_setting_default(&call.opt); + call.opt.aud_cnt = 1; + call.opt.vid_cnt = 0; + call.opt.txt_cnt = 0; + sdp->media_count = 1; + + rc = check_initial_local_offer(&call, NULL, sdp, PJ_SUCCESS, 2012); + if (rc != 0) + goto on_return; + + sdp->media_count = 2; + rc = check_initial_local_offer(&call, NULL, sdp, + PJMEDIA_SDPNEG_EMISMEDIA, 2013); + if (rc != 0) + goto on_return; + + opt = call.opt; + opt.aud_cnt = 2; + rc = check_initial_local_offer(&call, &opt, + sdp, PJ_SUCCESS, 2014); + if (rc != 0) + goto on_return; + + opt = call.opt; + opt.aud_cnt = PJSUA_MAX_CALL_MEDIA + 1; + rc = check_initial_local_offer(&call, &opt, sdp, PJ_ETOOMANY, 2026); + if (rc != 0) + goto on_return; + call.opt_inited = PJ_TRUE; + opt = call.opt; + opt.aud_cnt = 2; + rc = check_initial_local_offer(&call, &opt, sdp, + PJMEDIA_SDPNEG_EMISMEDIA, 2027); + call.opt_inited = PJ_FALSE; + if (rc != 0) + goto on_return; + +#if PJMEDIA_HAS_VIDEO + opt = call.opt; + opt.vid_cnt = 1; + rc = check_initial_local_offer(&call, &opt, sdp, PJ_SUCCESS, 2015); + if (rc != 0) + goto on_return; + opt.vid_cnt = 0; +#else + { + pjmedia_sdp_media *tmp = sdp->media[1]; + sdp->media[1] = sdp->media[2]; + sdp->media[2] = tmp; + } +#endif + + call.opt.flag = PJSUA_CALL_INCLUDE_DISABLED_MEDIA; + rc = check_initial_local_offer(&call, NULL, sdp, PJ_SUCCESS, 2016); + if (rc != 0) + goto on_return; + sdp->media[1]->desc.port = 0; + rc = check_initial_local_offer(&call, NULL, sdp, PJ_SUCCESS, 2017); + if (rc != 0) + goto on_return; + sdp->media[1]->desc.port = 4002; + call.opt.flag = 0; + +#if !PJMEDIA_HAS_VIDEO + { + pjmedia_sdp_media *tmp = sdp->media[1]; + sdp->media[1] = sdp->media[2]; + sdp->media[2] = tmp; + } +#endif + + call.med_prov_cnt = 1; + call.media_prov[0].type = PJMEDIA_TYPE_AUDIO; + call.media_prov[0].tp = fake_tp; + call.media_prov[0].tp_st = PJSUA_MED_TP_INIT; + rc = check_initial_local_offer(&call, NULL, sdp, + PJMEDIA_SDPNEG_EMISMEDIA, 2018); + if (rc != 0) + goto on_return; + sdp->media_count = 1; + rc = check_initial_local_offer(&call, NULL, sdp, PJ_SUCCESS, 2019); + if (rc != 0) + goto on_return; + + if (PJSUA_MAX_CALL_MEDIA + 1 <= PJMEDIA_MAX_SDP_MEDIA) { + sdp->media_count = PJSUA_MAX_CALL_MEDIA + 1; + rc = check_initial_local_offer(&call, NULL, sdp, PJ_ETOOMANY, + 2028); + sdp->media_count = 1; + if (rc != 0) + goto on_return; + } + + { + pjmedia_sdp_media *audio = sdp->media[0]; + sdp->media[0] = sdp->media[1]; + sdp->media[1] = audio; + } + rc = check_initial_local_offer(&call, NULL, sdp, PJ_SUCCESS, 2020); + { + pjmedia_sdp_media *video = sdp->media[0]; + sdp->media[0] = sdp->media[1]; + sdp->media[1] = video; + } + if (rc != 0) + goto on_return; + + call.media_prov[0].tp_st = PJSUA_MED_TP_DISABLED; + rc = check_initial_local_offer(&call, NULL, sdp, PJ_SUCCESS, 2021); + if (rc != 0) + goto on_return; + sdp->media[0]->desc.port = 0; + rc = check_initial_local_offer(&call, NULL, sdp, PJ_SUCCESS, 2022); + sdp->media[0]->desc.port = 4000; + if (rc != 0) + goto on_return; + + call.media_prov[0].tp_st = PJSUA_MED_TP_INIT; + call.media_prov[0].tp = NULL; + rc = check_initial_local_offer(&call, NULL, sdp, PJ_SUCCESS, 2023); + if (rc != 0) + goto on_return; + call.med_ch_cb = &initial_offer_media_tp_pending; + rc = check_initial_local_offer(&call, NULL, sdp, PJ_SUCCESS, 2024); + if (rc != 0) + goto on_return; + + sdp->media_count = 2; + rc = check_initial_local_offer(&call, NULL, sdp, + PJMEDIA_SDPNEG_EMISMEDIA, 2025); + +on_return: + pj_pool_release(pool); + return rc; +} + static pjmedia_sdp_session *create_oversized_sdp( pj_pool_t *pool, const pjmedia_sdp_session *sdp) @@ -6573,6 +6782,8 @@ static int test_late_sdp_reinvite(pj_bool_t async_answer, pjsua_call_id callee = PJSUA_INVALID_ID; pjsua_call_setting opt; const pjmedia_sdp_session *active; + unsigned callee_streams_created; + unsigned callee_streams_destroyed; pj_status_t status; int rc; @@ -6586,6 +6797,9 @@ static int test_late_sdp_reinvite(pj_bool_t async_answer, if (rc != 0) goto on_return; + callee_streams_created = g_ctx.strm_created[callee]; + callee_streams_destroyed = g_ctx.strm_destroyed[callee]; + g_ctx.late_offer_armed = PJ_TRUE; g_ctx.late_offer_caller = caller; g_ctx.late_offer_ack_status = PJ_EUNKNOWN; @@ -6657,6 +6871,17 @@ static int test_late_sdp_reinvite(pj_bool_t async_answer, rc = -1967; } + if (rc == 0 && + (g_ctx.strm_created[callee] != callee_streams_created || + g_ctx.strm_destroyed[callee] != callee_streams_destroyed)) + { + PJ_LOG(1, (THIS_FILE, " unchanged offerless re-INVITE restarted " + "callee media streams (created=%u/%u destroyed=%u/%u)", + g_ctx.strm_created[callee], callee_streams_created, + g_ctx.strm_destroyed[callee], callee_streams_destroyed)); + rc = -1970; + } + on_return: g_ctx.late_offer_armed = PJ_FALSE; g_ctx.late_offer_async_answer = PJ_FALSE; @@ -6812,6 +7037,9 @@ int pjsua_call_test(void) rc = test_idle_sound_call_setup(); if (rc != 0) goto on_return; + rc = test_initial_local_offer_validation(); + if (rc != 0) goto on_return; + rc = test_make_call_bounds(); if (rc != 0) goto on_return; diff --git a/tests/pjsua/scripts-sipp/alt-pjsua-uas-late-offer-dummy-codecs.xml b/tests/pjsua/scripts-sipp/alt-pjsua-uas-late-offer-dummy-codecs.xml index 4860c18c15..13338714bc 100644 --- a/tests/pjsua/scripts-sipp/alt-pjsua-uas-late-offer-dummy-codecs.xml +++ b/tests/pjsua/scripts-sipp/alt-pjsua-uas-late-offer-dummy-codecs.xml @@ -5,7 +5,7 @@ offer to answer, so it inserts its own SDP offer (PCMU audio and H263-1998 video) in the 200 OK. SIPp verifies both codecs are present, then sends the SDP answer in the ACK before alt_pjsua hangs up with - BYE. + BYE. This scenario tests the handling of late SDP offers with dummy codecs. --> From 8fd39b26b72ebb04f16a3a59e16147a93cd84f13 Mon Sep 17 00:00:00 2001 From: LEOPOLD Otakar Date: Thu, 15 Oct 2026 11:22:15 +0000 Subject: [PATCH 6/8] Fixes - round 5 - Validation of the SDP offer in the response is performed for every (re)INVITE(comment 1) - nonreliable responses with sdp offer are denied with PJ_EINVALIDOP(comment 2) - restored rejection in pjsua_call_send_ack() in case when it is called synchronously from on_call_send_ack() callback but for another call leg(comment 3) - fixed clearing answer_sdp(comment 4) - added catch block for all exceptions(comment 5) --- pjsip/include/pjsua-lib/pjsua.h | 11 ++- pjsip/src/pjsua-lib/pjsua_call.c | 129 ++++++++++++++++--------- pjsip/src/pjsua2/endpoint.cpp | 3 + pjsip/src/test/inv_offer_answer_test.c | 37 ++++--- pjsip/src/test/pjsua_call_test.c | 103 +++++++++++++++++++- 5 files changed, 227 insertions(+), 56 deletions(-) diff --git a/pjsip/include/pjsua-lib/pjsua.h b/pjsip/include/pjsua-lib/pjsua.h index 334b08c0aa..e6c07294e2 100644 --- a/pjsip/include/pjsua-lib/pjsua.h +++ b/pjsip/include/pjsua-lib/pjsua.h @@ -6969,6 +6969,12 @@ PJ_DECL(pj_status_t) pjsua_call_answer2(pjsua_call_id call_id, * An application-managed answer must have the same media-line count as the * remote offer; otherwise PJMEDIA_SDPNEG_EMISMEDIA is returned. * + * For calls whose media is not application-managed, supplying a local SDP + * offer in a provisional (1xx) response requires 100rel to be active + * (#PJSIP_INV_REQUIRE_100REL). Otherwise PJ_EINVALIDOP is returned. + * Codes 100, 180, and 181 are rejected for such offers even with 100rel active. + * These restrictions apply to SDP offers, not answers to remote offers. + * * @param call_id Incoming call identification. * @param sdp Local SDP answer, or a local SDP offer when answering * an offerless INVITE or re-INVITE. @@ -6999,7 +7005,10 @@ pjsua_call_answer_with_sdp(pjsua_call_id call_id, * to manually take over the ACK transmission, e.g. to attach an SDP answer * to the ACK for a late SDP offer received in the 2xx response. * This function is safe to call synchronously from the - * #pjsua_callback.on_call_send_ack() callback. + * #pjsua_callback.on_call_send_ack() callback when \a call_id identifies the + * call whose ACK triggered the callback. A different call ID is rejected with + * PJ_EINVALIDOP to avoid acquiring the PJSUA lock while holding the callback + * call's dialog lock. * * @param call_id Call identification. * @param cseq CSeq of the 2xx response that triggered the need to diff --git a/pjsip/src/pjsua-lib/pjsua_call.c b/pjsip/src/pjsua-lib/pjsua_call.c index 7dff1ff9d3..f4f6838012 100644 --- a/pjsip/src/pjsua-lib/pjsua_call.c +++ b/pjsip/src/pjsua-lib/pjsua_call.c @@ -849,7 +849,9 @@ pj_status_t pjsua_call_validate_initial_local_offer( const pjsua_call_setting *effective_opt = (opt && !call->opt_inited) ? opt : &call->opt; unsigned media_count = 0; - pj_bool_t prepared = call->med_ch_cb || call->med_prov_cnt != 0; + pj_bool_t prepared = call->med_ch_cb || call->med_prov_cnt != 0 || + (call->inv && + call->inv->state == PJSIP_INV_STATE_CONFIRMED); pj_status_t status = validate_app_sdp(sdp, NULL); if (status != PJ_SUCCESS) @@ -3372,15 +3374,18 @@ PJ_DEF(pj_status_t) pjsua_call_answer( pjsua_call_id call_id, /* * Send response to incoming INVITE request. */ -PJ_DEF(pj_status_t) pjsua_call_answer2(pjsua_call_id call_id, +static pj_status_t call_answer_with_pending_sdp(pjsua_call_id call_id, const pjsua_call_setting *opt, unsigned code, const pj_str_t *reason, - const pjsua_msg_data *msg_data) + const pjsua_msg_data *msg_data, + pjsip_inv_session *sdp_inv, + pjmedia_sdp_session *pending_sdp) { pjsua_call *call; pjsip_dialog *dlg = NULL; pjsip_tx_data *tdata; + pj_bool_t answer_sdp_handed_off = PJ_FALSE; pj_status_t status; PJ_ASSERT_RETURN(call_id>=0 && call_id<(int)pjsua_var.ua_cfg.max_calls, @@ -3393,6 +3398,11 @@ PJ_DEF(pj_status_t) pjsua_call_answer2(pjsua_call_id call_id, if (status != PJ_SUCCESS) goto on_return; + if (pending_sdp && call->inv != sdp_inv) { + status = PJ_EINVALIDOP; + goto on_return; + } + if (!call->inv->invite_tsx || call->inv->invite_tsx->role != PJSIP_ROLE_UAS || call->inv->invite_tsx->state >= PJSIP_TSX_STATE_COMPLETED) @@ -3448,16 +3458,26 @@ PJ_DEF(pj_status_t) pjsua_call_answer2(pjsua_call_id call_id, * the following conditions are met: * - no pending media channel init * - local SDP has not been generated - * - call setting has just been set, or SDP offer needs to be sent, i.e: - * answer code 183 or 2xx is issued + * - call setting has just been set + * - an SDP offer was supplied or needs to be created + * - answer code 183 or 2xx is issued */ if (!(call->opt.flag & PJSUA_CALL_MEDIA_APP_MANAGED) && !call->med_ch_cb && - (call->opt_inited || (code==183 || code/100==2)) && + (pending_sdp || call->opt_inited || (code==183 || code/100==2)) && (!call->inv->neg || pjmedia_sdp_neg_get_state(call->inv->neg) == PJMEDIA_SDP_NEG_STATE_NULL)) { + if (pending_sdp) { + if (call->answer_sdp) { + PJSUA_UNLOCK(); + status = PJ_EBUSY; + goto on_return; + } + call->answer_sdp = pending_sdp; + } + /* Mark call setting as initialized as it is just about to be used * for initializing the media channel. */ @@ -3474,7 +3494,9 @@ PJ_DEF(pj_status_t) pjsua_call_answer2(pjsua_call_id call_id, PJSUA_UNLOCK(); goto on_return; } - } else if (status != PJ_EPENDING) { + } else if (status == PJ_EPENDING) { + answer_sdp_handed_off = PJ_TRUE; + } else { PJSUA_UNLOCK(); pjsua_perror(THIS_FILE, "Error initializing media channel", status); goto on_return; @@ -3511,9 +3533,7 @@ PJ_DEF(pj_status_t) pjsua_call_answer2(pjsua_call_id call_id, answer); PJSUA_UNLOCK(); - if (dlg) pjsip_dlg_dec_lock(dlg); - pj_log_pop_indent(); - return status; + goto on_return; } PJSUA_UNLOCK(); @@ -3548,12 +3568,31 @@ PJ_DEF(pj_status_t) pjsua_call_answer2(pjsua_call_id call_id, status); on_return: + if (dlg && pending_sdp && !answer_sdp_handed_off && + call->inv == sdp_inv && call->answer_sdp == pending_sdp) + { + call->answer_sdp = NULL; + } if (dlg) pjsip_dlg_dec_lock(dlg); pj_log_pop_indent(); return status; } +/* + * Send response to incoming INVITE request. + */ +PJ_DEF(pj_status_t) pjsua_call_answer2(pjsua_call_id call_id, + const pjsua_call_setting *opt, + unsigned code, + const pj_str_t *reason, + const pjsua_msg_data *msg_data) +{ + return call_answer_with_pending_sdp(call_id, opt, code, reason, msg_data, + NULL, NULL); +} + + /* * Send response to incoming INVITE request. */ @@ -3569,7 +3608,9 @@ pjsua_call_answer_with_sdp(pjsua_call_id call_id, pjsip_dialog *dlg = NULL; pjsip_inv_session *inv; pjmedia_sdp_neg *old_neg; + pjmedia_sdp_neg_state neg_state; pj_bool_t app_managed; + pj_bool_t local_offer; pj_status_t status, cancel_status; pjmedia_sdp_session *pending_sdp = NULL; @@ -3584,6 +3625,7 @@ pjsua_call_answer_with_sdp(pjsua_call_id call_id, inv = call->inv; old_neg = inv->neg; app_managed = (call->opt.flag & PJSUA_CALL_MEDIA_APP_MANAGED) != 0; + if (app_managed) { if (inv->neg && pjmedia_sdp_neg_get_state(inv->neg) == @@ -3600,20 +3642,24 @@ pjsua_call_answer_with_sdp(pjsua_call_id call_id, if (status == PJ_SUCCESS) status = pjsip_inv_set_local_sdp(inv, sdp); } else { - /* For an initial offerless INVITE, let answer2() initialize media before - * setting the supplied SDP offer. - */ - if (code < 300 && sdp && !call->med_ch_cb && - (call->opt_inited || code == 183 || code / 100 == 2) && - (!call->inv->neg || - pjmedia_sdp_neg_get_state(call->inv->neg) == - PJMEDIA_SDP_NEG_STATE_NULL)) + neg_state = inv->neg ? pjmedia_sdp_neg_get_state(inv->neg) : + PJMEDIA_SDP_NEG_STATE_NULL; + local_offer = !inv->neg || + neg_state == PJMEDIA_SDP_NEG_STATE_NULL || + neg_state == PJMEDIA_SDP_NEG_STATE_DONE; + + if (sdp && local_offer && code >= 100 && code < 200 && + (code == 100 || code == 180 || code == 181 || + !(inv->options & PJSIP_INV_REQUIRE_100REL))) { - if (call->answer_sdp) { - pjsip_dlg_dec_lock(dlg); - return PJ_EBUSY; - } + PJ_LOG(3,(THIS_FILE, "Cannot send an SDP offer in unreliable " + "provisional response %u", code)); + pjsip_dlg_dec_lock(dlg); + return PJ_EINVALIDOP; + } + if (code < 300 && sdp && local_offer) + { status = pjsua_call_validate_initial_local_offer(call, opt, sdp); if (status != PJ_SUCCESS) { pjsua_perror(THIS_FILE, "SDP offer does not match call media", @@ -3622,27 +3668,25 @@ pjsua_call_answer_with_sdp(pjsua_call_id call_id, return status; } - pending_sdp = pjmedia_sdp_session_clone(call->inv->pool, sdp); - if (!pending_sdp) { - pjsip_dlg_dec_lock(dlg); - return PJ_ENOMEM; - } - call->answer_sdp = pending_sdp; + /* For an initial offerless INVITE, let call_answer_with_pending_sdp() + * initialize media before setting the supplied SDP offer. + */ + if (neg_state == PJMEDIA_SDP_NEG_STATE_NULL && !call->med_ch_cb) + { + if (call->answer_sdp) { + pjsip_dlg_dec_lock(dlg); + return PJ_EBUSY; + } - pjsip_dlg_dec_lock(dlg); - status = pjsua_call_answer2(call_id, opt, code, reason, msg_data); - if (status != PJ_SUCCESS && status != PJ_EPENDING) { - pj_status_t lock_status; - - lock_status = acquire_call("pjsua_call_answer_with_sdp()", - call_id, &call, &dlg); - if (lock_status == PJ_SUCCESS) { - if (call->answer_sdp == pending_sdp) - call->answer_sdp = NULL; + pending_sdp = pjmedia_sdp_session_clone(call->inv->pool, sdp); + if (!pending_sdp) { pjsip_dlg_dec_lock(dlg); + return PJ_ENOMEM; } + pjsip_dlg_dec_lock(dlg); + return call_answer_with_pending_sdp(call_id, opt, code, reason, + msg_data, inv, pending_sdp); } - return status; } status = pjsip_inv_set_local_sdp(call->inv, sdp); @@ -3757,11 +3801,10 @@ PJ_DEF(pj_status_t) pjsua_call_send_ack(pjsua_call_id call_id, if (callback_inv) { call = (pjsua_call*) callback_inv->dlg->mod_data[pjsua_var.mod.id]; if (!call || call->index != call_id) { - callback_inv = NULL; + status = PJ_EINVALIDOP; + goto on_return; } - } - - if (!callback_inv) { + } else { status = acquire_call("pjsua_call_send_ack()", call_id, &call, &dlg); if (status != PJ_SUCCESS) { goto on_return; diff --git a/pjsip/src/pjsua2/endpoint.cpp b/pjsip/src/pjsua2/endpoint.cpp index cb42b34bc9..6695b2ce71 100644 --- a/pjsip/src/pjsua2/endpoint.cpp +++ b/pjsip/src/pjsua2/endpoint.cpp @@ -1592,6 +1592,9 @@ pj_bool_t Endpoint::on_call_send_ack(pjsua_call_id call_id, } catch (Error &err) { PJ_LOG(1,(THIS_FILE, "onCallSendAck() error: %s", err.info().c_str())); return PJ_FALSE; + } catch (...) { + PJ_LOG(1,(THIS_FILE, "onCallSendAck() threw an unknown exception")); + return PJ_FALSE; } return prm.suppressSendAck ? PJ_TRUE : PJ_FALSE; } diff --git a/pjsip/src/test/inv_offer_answer_test.c b/pjsip/src/test/inv_offer_answer_test.c index 26bc7a5cf4..39f9b42da2 100644 --- a/pjsip/src/test/inv_offer_answer_test.c +++ b/pjsip/src/test/inv_offer_answer_test.c @@ -164,6 +164,8 @@ static unsigned job_cnt; static pj_bool_t reject_null_offer; static pj_bool_t answer_reinvite_manually; static pj_bool_t expect_null_offer_response; +static pj_str_t null_offer_call_id; +static pj_int32_t null_offer_cseq; static unsigned null_offer_response_code; static unsigned null_offer_response_media_count; static pj_bool_t null_offer_response_has_sdp; @@ -553,7 +555,7 @@ static int perform_test(inv_test_param_t *param) } else { reject_null_offer = PJ_TRUE; answer_reinvite_manually = PJ_TRUE; - expect_null_offer_response = PJ_TRUE; + expect_null_offer_response = PJ_FALSE; null_offer_response_code = 0; null_offer_response_media_count = 0; null_offer_response_has_sdp = PJ_FALSE; @@ -572,8 +574,20 @@ static int perform_test(inv_test_param_t *param) tdata->msg->body = pjsip_msg_body_create(tdata->pool, &type, &subtype, &data); } - if (status == PJ_SUCCESS) + if (status == PJ_SUCCESS) { + pjsip_cid_hdr *cid; + pjsip_cseq_hdr *cseq; + + cid = (pjsip_cid_hdr*) + pjsip_msg_find_hdr(tdata->msg, PJSIP_H_CALL_ID, NULL); + cseq = (pjsip_cseq_hdr*) + pjsip_msg_find_hdr(tdata->msg, PJSIP_H_CSEQ, NULL); + pj_strdup(inv_test.uac->dlg->pool, &null_offer_call_id, + &cid->id); + null_offer_cseq = cseq->cseq; + expect_null_offer_response = PJ_TRUE; status = pjsip_inv_send_msg(inv_test.uac, tdata); + } if (status != PJ_SUCCESS) { PJ_LOG(1, (THIS_FILE, " failed to send offerless re-INVITE (%d)", @@ -653,24 +667,26 @@ static pj_bool_t log_on_rx_msg(pjsip_rx_data *rdata) pjsip_msg *msg = rdata->msg_info.msg; char info[80]; + if (!is_user_equal(rdata->msg_info.from, "inv_offer_answer_test")) + return PJ_FALSE; + if (expect_null_offer_response && msg->type == PJSIP_RESPONSE_MSG && + rdata->msg_info.cid && rdata->msg_info.cseq && rdata->msg_info.cseq->method.id == PJSIP_INVITE_METHOD && + rdata->msg_info.cseq->cseq == null_offer_cseq && + pj_strcmp(&rdata->msg_info.cid->id, &null_offer_call_id) == 0 && msg->line.status.code >= 200) { pjsip_rdata_sdp_info *sdp_info; - null_offer_response_code = msg->line.status.code; sdp_info = pjsip_rdata_get_sdp_info(rdata); - if (sdp_info && sdp_info->sdp) { - null_offer_response_has_sdp = PJ_TRUE; - null_offer_response_media_count = sdp_info->sdp->media_count; - } + null_offer_response_has_sdp = sdp_info && sdp_info->sdp; + null_offer_response_media_count = null_offer_response_has_sdp ? + sdp_info->sdp->media_count : 0; + null_offer_response_code = msg->line.status.code; } - if (!is_user_equal(rdata->msg_info.from, "inv_offer_answer_test")) - return PJ_FALSE; - if (msg->type == PJSIP_REQUEST_MSG) pj_ansi_snprintf(info, sizeof(info), "%.*s", (int)msg->line.req.method.name.slen, @@ -1044,4 +1060,3 @@ int inv_offer_answer_test(void) on_return: return rc; } - diff --git a/pjsip/src/test/pjsua_call_test.c b/pjsip/src/test/pjsua_call_test.c index 9898f4658c..6c163957d3 100644 --- a/pjsip/src/test/pjsua_call_test.c +++ b/pjsip/src/test/pjsua_call_test.c @@ -48,6 +48,7 @@ * reported by pjsua_call_get_info() agree. * 7. An initial offerless INVITE can be answered with an application SDP * offer, followed by the SDP answer in ACK, synchronously or deferred. + * An offer in an unreliable 183 is rejected without changing call state. * 8. An offerless re-INVITE can receive a late SDP offer in the 2xx response * and complete negotiation with the SDP answer in ACK, synchronously or * deferred. @@ -246,6 +247,7 @@ static struct pj_status_t late_offer_answer_status; pj_status_t late_offer_ack_status; pj_bool_t late_offer_ack_seen; + pj_status_t late_offer_cross_call_ack_status; pj_bool_t late_offer_async_answer; pj_bool_t late_offer_async_ack; pjmedia_sdp_session *late_offer_ack_sdp; @@ -465,6 +467,7 @@ static int test_initial_local_offer_validation(void) pj_pool_t *pool; pjmedia_sdp_session *sdp; pjsua_call call; + pjsip_inv_session inv; pjsua_call_setting opt; pjmedia_transport *fake_tp = (pjmedia_transport*)&call; int rc; @@ -609,6 +612,26 @@ static int test_initial_local_offer_validation(void) sdp->media_count = 2; rc = check_initial_local_offer(&call, NULL, sdp, PJMEDIA_SDPNEG_EMISMEDIA, 2025); + if (rc != 0) + goto on_return; + + pj_bzero(&inv, sizeof(inv)); + inv.state = PJSIP_INV_STATE_CONFIRMED; + call.inv = &inv; + call.med_ch_cb = NULL; + call.med_prov_cnt = 0; + sdp->media_count = 1; + rc = check_initial_local_offer(&call, NULL, sdp, + PJMEDIA_SDPNEG_EMISMEDIA, 2029); + if (rc != 0) + goto on_return; + + call.med_prov_cnt = 1; + opt = call.opt; + opt.aud_cnt = 2; + sdp->media_count = 2; + rc = check_initial_local_offer(&call, &opt, sdp, + PJMEDIA_SDPNEG_EMISMEDIA, 2030); on_return: pj_pool_release(pool); @@ -759,6 +782,11 @@ static void on_incoming_call(pjsua_acc_id acc_id, pjsua_call_id call_id, g_ctx.late_offer_answer_status = pjsua_call_answer_with_sdp(call_id, g_ctx.late_offer_sdp, NULL, 200, NULL, NULL); + if (!pjsua_var.calls[call_id].med_ch_cb && + pjsua_var.calls[call_id].answer_sdp) + { + g_ctx.late_offer_answer_status = PJ_EBUG; + } } g_ctx.incoming_seen = PJ_TRUE; return; @@ -905,6 +933,9 @@ static pj_bool_t on_call_send_ack(pjsua_call_id call_id, return PJ_FALSE; g_ctx.late_offer_ack_seen = PJ_TRUE; + g_ctx.late_offer_cross_call_ack_status = pjsua_call_send_ack( + g_ctx.incoming_call_id, rdata->msg_info.cseq->cseq, + g_ctx.late_offer_sdp); call = &pjsua_var.calls[call_id]; status = pjmedia_sdp_neg_get_neg_remote(call->inv->neg, &remote_sdp); if (status == PJ_SUCCESS) { @@ -6675,6 +6706,7 @@ static int test_initial_late_sdp_offer(pj_bool_t async_answer, g_ctx.late_offer_initial_med_cnt = 1; g_ctx.late_offer_answer_status = PJ_EUNKNOWN; g_ctx.late_offer_ack_status = PJ_EUNKNOWN; + g_ctx.late_offer_cross_call_ack_status = PJ_EUNKNOWN; g_ctx.late_offer_ack_seen = PJ_FALSE; g_ctx.late_offer_async_answer = async_answer; g_ctx.late_offer_async_ack = async_ack; @@ -6700,9 +6732,52 @@ static int test_initial_late_sdp_offer(pj_bool_t async_answer, goto on_return; } callee = g_ctx.incoming_call_id; + { + pjsip_tx_data *last_tx; + pjsip_tx_data *last_answer; + pj_bool_t opt_inited; + unsigned med_prov_cnt; + + inv = pjsua_var.calls[callee].inv; + last_tx = inv->invite_tsx->last_tx; + last_answer = inv->last_answer; + opt_inited = pjsua_var.calls[callee].opt_inited; + med_prov_cnt = pjsua_var.calls[callee].med_prov_cnt; + if ((pjsua_var.calls[callee].opt.flag & + PJSUA_CALL_MEDIA_APP_MANAGED) || + (inv->options & PJSIP_INV_REQUIRE_100REL) || inv->neg) + { + PJ_LOG(1, (THIS_FILE, + " Invalid unreliable SDP offer test setup")); + rc = -2032; + goto on_return; + } + status = pjsua_call_answer_with_sdp(callee, g_ctx.late_offer_sdp, + NULL, PJSIP_SC_PROGRESS, + NULL, NULL); + if (status != PJ_EINVALIDOP || inv->neg || + inv->invite_tsx->last_tx != last_tx || + inv->last_answer != last_answer || + pjsua_var.calls[callee].answer_sdp || + pjsua_var.calls[callee].med_ch_cb || + pjsua_var.calls[callee].opt_inited != opt_inited || + pjsua_var.calls[callee].med_prov_cnt != med_prov_cnt) + { + PJ_LOG(1, (THIS_FILE, + " Unreliable SDP offer: expected %d, got %d " + "or mutated call state", PJ_EINVALIDOP, status)); + rc = -2033; + goto on_return; + } + } g_ctx.late_offer_answer_status = pjsua_call_answer_with_sdp(callee, g_ctx.late_offer_sdp, NULL, PJSIP_SC_OK, NULL, NULL); + if (!pjsua_var.calls[callee].med_ch_cb && + pjsua_var.calls[callee].answer_sdp) + { + g_ctx.late_offer_answer_status = PJ_EBUG; + } if (g_ctx.late_offer_answer_status != PJ_SUCCESS && g_ctx.late_offer_answer_status != PJ_EPENDING) { @@ -6746,6 +6821,8 @@ static int test_initial_late_sdp_offer(pj_bool_t async_answer, g_ctx.late_offer_answer_status != PJ_EPENDING) || !g_ctx.late_offer_ack_seen || g_ctx.late_offer_ack_status != PJ_SUCCESS || + g_ctx.late_offer_cross_call_ack_status != PJ_EINVALIDOP || + pjsua_var.calls[callee].answer_sdp != NULL || pjsua_var.calls[callee].med_cnt == 0 || pjsua_var.calls[caller].med_cnt == 0 || !pjsua_var.calls[callee].med_update_success || @@ -6833,9 +6910,33 @@ static int test_late_sdp_reinvite(pj_bool_t async_answer, status = pjmedia_sdp_neg_get_active_local( pjsua_var.calls[callee].inv->neg, &active); - if (status == PJ_SUCCESS) + if (status == PJ_SUCCESS) { + pjsua_call *call = &pjsua_var.calls[callee]; + pjmedia_sdp_neg *neg = call->inv->neg; + pjsip_transaction *tsx = call->inv->invite_tsx; + unsigned med_prov_cnt = call->med_prov_cnt; + int status_code = tsx->status_code; + pjmedia_sdp_session *extra = + pjmedia_sdp_session_clone(call->inv->pool_prov, active); + + extra->media[extra->media_count] = + pjmedia_sdp_media_clone(call->inv->pool_prov, + extra->media[0]); + ++extra->media_count; + status = pjsua_call_answer_with_sdp(callee, extra, NULL, + PJSIP_SC_OK, NULL, NULL); + if (status != PJMEDIA_SDPNEG_EMISMEDIA || + call->inv->neg != neg || + pjmedia_sdp_neg_get_state(neg) != PJMEDIA_SDP_NEG_STATE_DONE || + call->med_prov_cnt != med_prov_cnt || + call->inv->invite_tsx != tsx || tsx->status_code != status_code) + { + rc = -2031; + goto on_return; + } status = pjsua_call_answer_with_sdp(callee, active, NULL, PJSIP_SC_OK, NULL, NULL); + } if (status != PJ_SUCCESS && status != PJ_EPENDING) { rc = -1968; goto on_return; From 6df1b8e5e0bb2d437f0a2b368de5f9bad3ddf50f Mon Sep 17 00:00:00 2001 From: LEOPOLD Otakar Date: Fri, 23 Oct 2026 11:04:41 +0200 Subject: [PATCH 7/8] Fixes - round 6 - fixed rollback(comment 1) - rejecting SDP when code >= 300(comment 2) - fixed documentation of sendAck()(comment 3) - removed includes(comment 4) --- pjsip/include/pjsua-lib/pjsua.h | 2 + pjsip/include/pjsua2/call.hpp | 3 + pjsip/src/pjsua-lib/pjsua_call.c | 22 ++-- pjsip/src/test/pjsua_call_test.c | 203 +++++++++++++++++++++++++++++-- 4 files changed, 214 insertions(+), 16 deletions(-) diff --git a/pjsip/include/pjsua-lib/pjsua.h b/pjsip/include/pjsua-lib/pjsua.h index e6c07294e2..7531065dfb 100644 --- a/pjsip/include/pjsua-lib/pjsua.h +++ b/pjsip/include/pjsua-lib/pjsua.h @@ -6966,6 +6966,8 @@ PJ_DECL(pj_status_t) pjsua_call_answer2(pjsua_call_id call_id, * offer otherwise. For ordinary initial offerless calls, media is initialized * before installing the supplied offer. A pending local offer cannot be * replaced by this function. + * Supplying SDP with a final error response (code >= 300) returns + * PJ_EINVALIDOP without changing call state. * An application-managed answer must have the same media-line count as the * remote offer; otherwise PJMEDIA_SDPNEG_EMISMEDIA is returned. * diff --git a/pjsip/include/pjsua2/call.hpp b/pjsip/include/pjsua2/call.hpp index 157dee2cb9..f776c3142e 100644 --- a/pjsip/include/pjsua2/call.hpp +++ b/pjsip/include/pjsua2/call.hpp @@ -1811,6 +1811,9 @@ class Call * this from within onCallSendAck(), to manually take over the ACK * transmission, e.g. to attach an SDP answer to the ACK for a late * SDP offer received in the 2xx response. + * When called synchronously from onCallSendAck(), this method may only + * be used on the Call whose ACK triggered the callback; another Call + * fails with PJ_EINVALIDOP. * * @param cseq CSeq of the 2xx response that triggered the need to * send an ACK request; normally this is the CSeq of diff --git a/pjsip/src/pjsua-lib/pjsua_call.c b/pjsip/src/pjsua-lib/pjsua_call.c index f4f6838012..cf8273ebf3 100644 --- a/pjsip/src/pjsua-lib/pjsua_call.c +++ b/pjsip/src/pjsua-lib/pjsua_call.c @@ -16,8 +16,6 @@ * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ -#include -#include #include #include @@ -3658,6 +3656,13 @@ pjsua_call_answer_with_sdp(pjsua_call_id call_id, return PJ_EINVALIDOP; } + if (sdp && code >= 300) { + PJ_LOG(3,(THIS_FILE, + "Cannot supply SDP in final error response %u", code)); + pjsip_dlg_dec_lock(dlg); + return PJ_EINVALIDOP; + } + if (code < 300 && sdp && local_offer) { status = pjsua_call_validate_initial_local_offer(call, opt, sdp); @@ -3683,13 +3688,11 @@ pjsua_call_answer_with_sdp(pjsua_call_id call_id, pjsip_dlg_dec_lock(dlg); return PJ_ENOMEM; } - pjsip_dlg_dec_lock(dlg); - return call_answer_with_pending_sdp(call_id, opt, code, reason, - msg_data, inv, pending_sdp); } } - - status = pjsip_inv_set_local_sdp(call->inv, sdp); + else { + status = pjsip_inv_set_local_sdp(call->inv, sdp); + } } pjsip_dlg_dec_lock(dlg); @@ -3697,7 +3700,10 @@ pjsua_call_answer_with_sdp(pjsua_call_id call_id, if (status != PJ_SUCCESS) return status; - status = pjsua_call_answer2(call_id, opt, code, reason, msg_data); + status = call_answer_with_pending_sdp(call_id, opt, code, + reason, msg_data, inv, + pending_sdp); + if (status == PJ_SUCCESS) return status; diff --git a/pjsip/src/test/pjsua_call_test.c b/pjsip/src/test/pjsua_call_test.c index 6c163957d3..190c7da139 100644 --- a/pjsip/src/test/pjsua_call_test.c +++ b/pjsip/src/test/pjsua_call_test.c @@ -54,6 +54,8 @@ * deferred. * 9. An application-supplied initial local offer must match the prepared * media slots and must not mutate call state when rejected. + * 10. Supplying SDP with a final error response must not change call state + * or prevent subsequent re-INVITEs. * * Scenarios 5 and 6 are two symptoms of one defect: apply_med_update() derives * the stream info from the negotiated SDP, then adjusts it (RTCP mux off when @@ -255,6 +257,8 @@ static struct pj_bool_t late_offer_reinvite_armed; pjsua_call_id late_offer_reinvite_callee; pj_bool_t late_offer_reinvite_seen; + pj_bool_t late_offer_fail_answer; + pj_bool_t late_offer_answer_failed; /* When set, on_call_sdp_created appends an m=application line to the * local SDP offer, so the peer establishes a call holding a media slot * whose type is neither audio, video nor text. */ @@ -6661,8 +6665,50 @@ static int test_answer_with_pending_local_offer(void) return rc; } +static pj_pool_t *(*late_offer_create_pool)(pj_pool_factory *factory, + const char *name, + pj_size_t initial_size, + pj_size_t increment_size, + pj_size_t alignment, + pj_pool_callback *callback); + +static pj_pool_t *late_offer_fail_create_pool(pj_pool_factory *factory, + const char *name, + pj_size_t initial_size, + pj_size_t increment_size, + pj_size_t alignment, + pj_pool_callback *callback) +{ + /* Fail response cloning only after the supplied offer is installed. */ + if (g_ctx.late_offer_fail_answer && + g_ctx.incoming_call_id != PJSUA_INVALID_ID && + name && pj_ansi_strncmp(name, "tdta", 4) == 0) + { + pjsip_inv_session *inv = + pjsua_var.calls[g_ctx.incoming_call_id].inv; + + if (inv && inv->last_answer && inv->neg && + pjmedia_sdp_neg_get_state(inv->neg) == + PJMEDIA_SDP_NEG_STATE_LOCAL_OFFER) + { + g_ctx.late_offer_fail_answer = PJ_FALSE; + g_ctx.late_offer_answer_failed = PJ_TRUE; + return NULL; + } + } + return late_offer_create_pool(factory, name, initial_size, increment_size, + alignment, callback); +} + +static pj_bool_t late_offer_answer_failed(pjsua_call_id call_id) +{ + PJ_UNUSED_ARG(call_id); + return g_ctx.late_offer_answer_failed; +} + static int test_initial_late_sdp_offer(pj_bool_t async_answer, - pj_bool_t async_ack) + pj_bool_t async_ack, + pj_bool_t fail_answer) { pjsua_call_id seed_caller = PJSUA_INVALID_ID; pjsua_call_id seed_callee = PJSUA_INVALID_ID; @@ -6675,9 +6721,9 @@ static int test_initial_late_sdp_offer(pj_bool_t async_answer, pj_status_t status; int rc = 0; - PJ_LOG(3, (THIS_FILE, " initial late SDP offer (answer=%s ACK=%s)", + PJ_LOG(3, (THIS_FILE, " initial late SDP offer (answer=%s ACK=%s fail=%d)", async_answer ? "async" : "sync", - async_ack ? "async" : "sync")); + async_ack ? "async" : "sync", fail_answer)); g_ctx.skip_answer2_overlimit_probe = PJ_TRUE; rc = establish_self_call(&seed_caller, &seed_callee, -1930); @@ -6712,6 +6758,12 @@ static int test_initial_late_sdp_offer(pj_bool_t async_answer, g_ctx.late_offer_async_ack = async_ack; g_ctx.late_offer_ack_sdp = NULL; pj_bzero(g_ctx.med_state_cnt, sizeof(g_ctx.med_state_cnt)); + g_ctx.late_offer_fail_answer = fail_answer; + g_ctx.late_offer_answer_failed = PJ_FALSE; + if (fail_answer) { + late_offer_create_pool = pjsua_var.cp.factory.create_pool; + pjsua_var.cp.factory.create_pool = &late_offer_fail_create_pool; + } pjsua_call_setting_default(&opt); opt.aud_cnt = 1; @@ -6779,13 +6831,41 @@ static int test_initial_late_sdp_offer(pj_bool_t async_answer, g_ctx.late_offer_answer_status = PJ_EBUG; } if (g_ctx.late_offer_answer_status != PJ_SUCCESS && - g_ctx.late_offer_answer_status != PJ_EPENDING) + g_ctx.late_offer_answer_status != PJ_EPENDING && + !(fail_answer && g_ctx.late_offer_answer_status == PJ_ENOMEM)) { rc = -1944; goto on_return; } } + if (fail_answer) { + if (!wait_until(&late_offer_answer_failed, caller, 8000)) { + rc = -2051; + goto on_return; + } + callee = g_ctx.incoming_call_id; + inv = pjsua_var.calls[callee].inv; + if (!inv || inv->neg || pjsua_var.calls[callee].answer_sdp || + pjsua_var.calls[callee].med_ch_cb) + { + PJ_LOG(1, (THIS_FILE, " failed answer left pending SDP")); + rc = -2052; + goto on_return; + } + g_ctx.late_offer_answer_status = + pjsua_call_answer_with_sdp(callee, g_ctx.late_offer_sdp, + NULL, PJSIP_SC_OK, NULL, NULL); + if (g_ctx.late_offer_answer_status != PJ_SUCCESS && + g_ctx.late_offer_answer_status != PJ_EPENDING) + { + PJ_LOG(1, (THIS_FILE, " retry after failed answer failed (%d)", + g_ctx.late_offer_answer_status)); + rc = -2053; + goto on_return; + } + } + if (async_ack) { if (!wait_until(&late_offer_ack_received, caller, 8000) || g_ctx.late_offer_ack_status != PJ_EPENDING || @@ -6843,6 +6923,11 @@ static int test_initial_late_sdp_offer(pj_bool_t async_answer, } on_return: + if (fail_answer && late_offer_create_pool) { + pjsua_var.cp.factory.create_pool = late_offer_create_pool; + late_offer_create_pool = NULL; + } + g_ctx.late_offer_fail_answer = PJ_FALSE; g_ctx.late_offer_armed = PJ_FALSE; g_ctx.late_offer_async_answer = PJ_FALSE; g_ctx.late_offer_async_ack = PJ_FALSE; @@ -6852,6 +6937,104 @@ static int test_initial_late_sdp_offer(pj_bool_t async_answer, return rc; } +static int test_sdp_error_response(void) +{ + static const unsigned codes[] = { 300, 488, 699 }; + pjsua_call_id caller = PJSUA_INVALID_ID; + pjsua_call_id callee = PJSUA_INVALID_ID; + pjsua_call_setting opt; + pjsua_call *call; + pjmedia_sdp_neg *neg; + pjsip_transaction *tsx; + const pjmedia_sdp_session *active; + unsigned i, med_prov_cnt; + int status_code; + pj_status_t status; + int rc; + + PJ_LOG(3, (THIS_FILE, " SDP in final error response")); + + g_ctx.skip_answer2_overlimit_probe = PJ_TRUE; + rc = establish_self_call(&caller, &callee, -2040); + g_ctx.skip_answer2_overlimit_probe = PJ_FALSE; + if (rc != 0) + goto on_return; + + g_ctx.late_offer_reinvite_armed = PJ_TRUE; + g_ctx.late_offer_reinvite_callee = callee; + g_ctx.late_offer_reinvite_seen = PJ_FALSE; + g_ctx.late_offer_async_answer = PJ_TRUE; + + pjsua_call_setting_default(&opt); + opt.aud_cnt = 1; + opt.vid_cnt = 0; + opt.txt_cnt = 0; + opt.flag |= PJSUA_CALL_NO_SDP_OFFER; + status = pjsua_call_reinvite2(caller, &opt, NULL); + if (status != PJ_SUCCESS || + !wait_until(&late_offer_reinvite_received, callee, TEST_TIMEOUT_MSEC)) + { + rc = -2046; + goto on_return; + } + + call = &pjsua_var.calls[callee]; + neg = call->inv->neg; + tsx = call->inv->invite_tsx; + med_prov_cnt = call->med_prov_cnt; + status_code = tsx->status_code; + status = pjmedia_sdp_neg_get_active_local(neg, &active); + if (status != PJ_SUCCESS) { + rc = -2047; + goto on_return; + } + + for (i = 0; i < PJ_ARRAY_SIZE(codes); ++i) { + status = pjsua_call_answer_with_sdp(callee, active, NULL, codes[i], + NULL, NULL); + if (status != PJ_EINVALIDOP || + call->inv->neg != neg || + pjmedia_sdp_neg_get_state(neg) != PJMEDIA_SDP_NEG_STATE_DONE || + call->med_prov_cnt != med_prov_cnt || + call->inv->invite_tsx != tsx || tsx->status_code != status_code) + { + PJ_LOG(1, (THIS_FILE, " SDP with response %u was not rejected " + "without changing call state (%d)", codes[i], status)); + rc = -2048; + goto on_return; + } + } + + status = pjsua_call_answer(callee, PJSIP_SC_NOT_ACCEPTABLE_HERE, + NULL, NULL); + if (status != PJ_SUCCESS || + !wait_until(&reinvite_tsx_done, caller, TEST_TIMEOUT_MSEC) || + !wait_until(&reinvite_tsx_done, callee, TEST_TIMEOUT_MSEC)) + { + rc = -2049; + goto on_return; + } + + g_ctx.late_offer_reinvite_armed = PJ_FALSE; + status = pjsua_call_reinvite(callee, 0, NULL); + if (status != PJ_SUCCESS || + !wait_until(&reinvite_tsx_done, caller, TEST_TIMEOUT_MSEC) || + !wait_until(&reinvite_tsx_done, callee, TEST_TIMEOUT_MSEC) || + !call_is_confirmed(caller) || !call_is_confirmed(callee)) + { + PJ_LOG(1, (THIS_FILE, " re-INVITE after rejection failed (%d)", + status)); + rc = -2050; + } + +on_return: + g_ctx.late_offer_reinvite_armed = PJ_FALSE; + g_ctx.late_offer_reinvite_callee = PJSUA_INVALID_ID; + g_ctx.late_offer_async_answer = PJ_FALSE; + drain_all_calls(); + return rc; +} + static int test_late_sdp_reinvite(pj_bool_t async_answer, pj_bool_t async_ack) { @@ -7361,13 +7544,15 @@ int pjsua_late_sdp_offer_test(void) g_ctx.late_offer_armed = PJ_FALSE; g_ctx.med_state_target = 1; - rc = test_initial_late_sdp_offer(PJ_FALSE, PJ_FALSE); + rc = test_initial_late_sdp_offer(PJ_FALSE, PJ_FALSE, PJ_FALSE); + if (rc == 0) + rc = test_initial_late_sdp_offer(PJ_TRUE, PJ_FALSE, PJ_FALSE); if (rc == 0) - rc = test_initial_late_sdp_offer(PJ_TRUE, PJ_FALSE); + rc = test_initial_late_sdp_offer(PJ_FALSE, PJ_TRUE, PJ_FALSE); if (rc == 0) - rc = test_initial_late_sdp_offer(PJ_FALSE, PJ_TRUE); + rc = test_initial_late_sdp_offer(PJ_TRUE, PJ_TRUE, PJ_FALSE); if (rc == 0) - rc = test_initial_late_sdp_offer(PJ_TRUE, PJ_TRUE); + rc = test_initial_late_sdp_offer(PJ_TRUE, PJ_FALSE, PJ_TRUE); if (rc == 0) rc = test_late_sdp_reinvite(PJ_FALSE, PJ_FALSE); if (rc == 0) @@ -7376,6 +7561,8 @@ int pjsua_late_sdp_offer_test(void) rc = test_late_sdp_reinvite(PJ_FALSE, PJ_TRUE); if (rc == 0) rc = test_late_sdp_reinvite(PJ_TRUE, PJ_TRUE); + if (rc == 0) + rc = test_sdp_error_response(); drain_all_calls(); if (settings_saved) From b57476e54bed8317480d9955fac13a3d9eea4a37 Mon Sep 17 00:00:00 2001 From: LEOPOLD Otakar Date: Mon, 26 Oct 2026 09:35:50 +0100 Subject: [PATCH 8/8] Fixes - round 7 - fixed SDP silently dropped on the offerless re-INVITE(comment 1) - added test(comment 2) --- pjsip/src/pjsua-lib/pjsua_call.c | 3 +- pjsip/src/test/pjsua_call_test.c | 93 ++++++++++++++++++++++++++++---- 2 files changed, 86 insertions(+), 10 deletions(-) diff --git a/pjsip/src/pjsua-lib/pjsua_call.c b/pjsip/src/pjsua-lib/pjsua_call.c index cf8273ebf3..63d11574e7 100644 --- a/pjsip/src/pjsua-lib/pjsua_call.c +++ b/pjsip/src/pjsua-lib/pjsua_call.c @@ -3690,7 +3690,8 @@ pjsua_call_answer_with_sdp(pjsua_call_id call_id, } } } - else { + + if (!pending_sdp) { status = pjsip_inv_set_local_sdp(call->inv, sdp); } } diff --git a/pjsip/src/test/pjsua_call_test.c b/pjsip/src/test/pjsua_call_test.c index 190c7da139..171014bc76 100644 --- a/pjsip/src/test/pjsua_call_test.c +++ b/pjsip/src/test/pjsua_call_test.c @@ -51,7 +51,8 @@ * An offer in an unreliable 183 is rejected without changing call state. * 8. An offerless re-INVITE can receive a late SDP offer in the 2xx response * and complete negotiation with the SDP answer in ACK, synchronously or - * deferred. + * deferred. A supplied sendonly offer must survive in the callee's local + * and caller's remote SDP instead of falling back to the active offer. * 9. An application-supplied initial local offer must match the prepared * media slots and must not mutate call state when rejected. * 10. Supplying SDP with a final error response must not change call state @@ -7036,7 +7037,8 @@ static int test_sdp_error_response(void) } static int test_late_sdp_reinvite(pj_bool_t async_answer, - pj_bool_t async_ack) + pj_bool_t async_ack, + pj_bool_t custom_offer) { pjsua_call_id caller = PJSUA_INVALID_ID; pjsua_call_id callee = PJSUA_INVALID_ID; @@ -7047,9 +7049,10 @@ static int test_late_sdp_reinvite(pj_bool_t async_answer, pj_status_t status; int rc; - PJ_LOG(3, (THIS_FILE, " late SDP re-INVITE (answer=%s ACK=%s)", + PJ_LOG(3, (THIS_FILE, " late SDP re-INVITE (answer=%s ACK=%s offer=%s)", async_answer ? "async" : "sync", - async_ack ? "async" : "sync")); + async_ack ? "async" : "sync", + custom_offer ? "sendonly" : "unchanged")); g_ctx.skip_answer2_overlimit_probe = PJ_TRUE; rc = establish_self_call(&caller, &callee, -1960); @@ -7117,6 +7120,54 @@ static int test_late_sdp_reinvite(pj_bool_t async_answer, rc = -2031; goto on_return; } + if (custom_offer) { + pjmedia_sdp_session *offer; + pjmedia_sdp_media *media; + + if (!active->media_count || + pj_strcmp2(&active->media[0]->desc.media, "audio") != 0 || + !active->media[0]->desc.port) + { + PJ_LOG(1, (THIS_FILE, " no active audio to modify")); + rc = -2050; + goto on_return; + } + media = active->media[0]; + if (pjmedia_sdp_attr_find2(media->attr_count, media->attr, + "sendonly", NULL) || + pjmedia_sdp_attr_find2(media->attr_count, media->attr, + "recvonly", NULL) || + pjmedia_sdp_attr_find2(media->attr_count, media->attr, + "inactive", NULL) || + pjmedia_sdp_attr_find2(active->attr_count, active->attr, + "sendonly", NULL) || + pjmedia_sdp_attr_find2(active->attr_count, active->attr, + "recvonly", NULL) || + pjmedia_sdp_attr_find2(active->attr_count, active->attr, + "inactive", NULL)) + { + PJ_LOG(1, (THIS_FILE, " expected sendrecv audio")); + rc = -2051; + goto on_return; + } + + offer = pjmedia_sdp_session_clone(call->inv->pool_prov, + active); + media = offer->media[0]; + pjmedia_sdp_media_remove_all_attr(media, "sendrecv"); + pjmedia_sdp_media_remove_all_attr(media, "sendonly"); + pjmedia_sdp_media_remove_all_attr(media, "recvonly"); + pjmedia_sdp_media_remove_all_attr(media, "inactive"); + if (media->attr_count >= PJMEDIA_MAX_SDP_ATTR) { + PJ_LOG(1, (THIS_FILE, " no room for SDP direction")); + rc = -2052; + goto on_return; + } + media->attr[media->attr_count++] = + pjmedia_sdp_attr_create(call->inv->pool_prov, + "sendonly", NULL); + active = offer; + } status = pjsua_call_answer_with_sdp(callee, active, NULL, PJSIP_SC_OK, NULL, NULL); } @@ -7155,7 +7206,27 @@ static int test_late_sdp_reinvite(pj_bool_t async_answer, rc = -1967; } - if (rc == 0 && + if (rc == 0 && custom_offer) { + const pjmedia_sdp_session *local, *remote; + + if (pjmedia_sdp_neg_get_active_local(pjsua_var.calls[callee].inv->neg, + &local) != PJ_SUCCESS || + pjmedia_sdp_neg_get_active_remote(pjsua_var.calls[caller].inv->neg, + &remote) != PJ_SUCCESS || + !local->media_count || !remote->media_count || + !local->media[0]->desc.port || !remote->media[0]->desc.port || + !pjmedia_sdp_attr_find2(local->media[0]->attr_count, + local->media[0]->attr, "sendonly", NULL) || + !pjmedia_sdp_attr_find2(remote->media[0]->attr_count, + remote->media[0]->attr, "sendonly", NULL)) + { + PJ_LOG(1, (THIS_FILE, " application sendonly offer missing " + "from callee local or caller remote SDP")); + rc = -2053; + } + } + + if (rc == 0 && !custom_offer && (g_ctx.strm_created[callee] != callee_streams_created || g_ctx.strm_destroyed[callee] != callee_streams_destroyed)) { @@ -7554,13 +7625,17 @@ int pjsua_late_sdp_offer_test(void) if (rc == 0) rc = test_initial_late_sdp_offer(PJ_TRUE, PJ_FALSE, PJ_TRUE); if (rc == 0) - rc = test_late_sdp_reinvite(PJ_FALSE, PJ_FALSE); + rc = test_late_sdp_reinvite(PJ_FALSE, PJ_FALSE, PJ_FALSE); + if (rc == 0) + rc = test_late_sdp_reinvite(PJ_TRUE, PJ_FALSE, PJ_FALSE); + if (rc == 0) + rc = test_late_sdp_reinvite(PJ_FALSE, PJ_TRUE, PJ_FALSE); if (rc == 0) - rc = test_late_sdp_reinvite(PJ_TRUE, PJ_FALSE); + rc = test_late_sdp_reinvite(PJ_TRUE, PJ_TRUE, PJ_FALSE); if (rc == 0) - rc = test_late_sdp_reinvite(PJ_FALSE, PJ_TRUE); + rc = test_late_sdp_reinvite(PJ_TRUE, PJ_FALSE, PJ_TRUE); if (rc == 0) - rc = test_late_sdp_reinvite(PJ_TRUE, PJ_TRUE); + rc = test_late_sdp_reinvite(PJ_TRUE, PJ_TRUE, PJ_TRUE); if (rc == 0) rc = test_sdp_error_response();