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..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 @@ -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..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 @@ -466,7 +467,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 +641,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 +1814,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: @@ -2914,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 fff2617489..7531065dfb 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. */ @@ -2479,6 +2484,28 @@ 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). 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 + * 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; @@ -6934,17 +6961,25 @@ 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. + * 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. * + * 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 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 @@ -6965,6 +7000,33 @@ 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. + * This function is safe to call synchronously from the + * #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 + * 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/pjsua-lib/pjsua_internal.h b/pjsip/include/pjsua-lib/pjsua_internal.h index ff4cfe96fb..6a80d1fbf5 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( @@ -941,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. */ @@ -1057,6 +1063,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 7dce2c0125..f776c3142e 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 */ @@ -833,6 +838,36 @@ 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; + + /** + * 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; + + /** + * The CSeq of the 2xx response. + */ + pj_int32_t cseq; + + /** + * Default constructor + */ + OnCallSendAckParam() : suppressSendAck(false), cseq(-1) + {} +}; + /** * This structure contains parameters for Call::onStreamPreCreate() * callback. @@ -1770,6 +1805,27 @@ 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. + * 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 + * 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 +2187,22 @@ 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 + * 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(). + * + * @param prm Callback parameter. + */ + virtual void onCallSendAck(OnCallSendAckParam &prm) + { PJ_UNUSED_ARG(prm); } + /** * 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..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. @@ -2432,6 +2441,8 @@ class Endpoint pjmedia_sdp_session *sdp, pj_pool_t *pool, const pjmedia_sdp_session *rem_sdp); + 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); 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..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; @@ -3180,6 +3189,61 @@ 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; + const pjmedia_sdp_session *offer = local_sdp; + pjmedia_sdp_session *new_offer = NULL; + 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 && + !tsx_inv_data->has_body) + { + if (!offer && mod_inv.cb.on_create_offer) { + (*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 { + /* 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. + */ + status = pjmedia_sdp_neg_send_local_offer( + inv->pool_prov, inv->neg, &local_offer); + if (status == PJ_SUCCESS) + offer = local_offer; + } + + 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..63d11574e7 100644 --- a/pjsip/src/pjsua-lib/pjsua_call.c +++ b/pjsip/src/pjsua-lib/pjsua_call.c @@ -44,6 +44,8 @@ /* Max number of hangup retries. */ #define CALL_HANGUP_MAX_RETRY 4 +static long ack_callback_inv_tls = -1; + /* * The INFO method. */ @@ -124,6 +126,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. @@ -241,6 +251,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; @@ -273,6 +289,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; @@ -317,6 +336,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. */ @@ -810,6 +839,55 @@ 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 || + (call->inv && + call->inv->state == PJSIP_INV_STATE_CONFIRMED); + 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) @@ -3193,16 +3271,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); @@ -3214,6 +3298,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. @@ -3286,15 +3372,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, @@ -3307,6 +3396,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) @@ -3362,16 +3456,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. */ @@ -3388,7 +3492,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; @@ -3425,9 +3531,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(); @@ -3462,12 +3566,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. */ @@ -3483,8 +3606,11 @@ 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; PJ_ASSERT_RETURN(call_id>=0 && call_id<(int)pjsua_var.ua_cfg.max_calls, PJ_EINVAL); @@ -3497,6 +3623,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) == @@ -3513,7 +3640,60 @@ 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); + 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))) + { + 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 (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); + if (status != PJ_SUCCESS) { + pjsua_perror(THIS_FILE, "SDP offer does not match call media", + status); + pjsip_dlg_dec_lock(dlg); + return status; + } + + /* 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; + } + + pending_sdp = pjmedia_sdp_session_clone(call->inv->pool, sdp); + if (!pending_sdp) { + pjsip_dlg_dec_lock(dlg); + return PJ_ENOMEM; + } + } + } + + if (!pending_sdp) { + status = pjsip_inv_set_local_sdp(call->inv, sdp); + } } pjsip_dlg_dec_lock(dlg); @@ -3521,7 +3701,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; @@ -3556,6 +3739,98 @@ pjsua_call_answer_with_sdp(pjsua_call_id call_id, return status; } +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; + pjmedia_sdp_neg_state neg_state; + + if (inv == NULL) + return PJ_EINVAL; + + 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) + 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 + * 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_inv_session *callback_inv = NULL; + 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(); + + if (ack_callback_inv_tls != -1) + 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]; + 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); + } + +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, @@ -6758,8 +7033,11 @@ 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; + pjsua_call_setting callback_opt; pjsip_status_code code = PJSIP_SC_OK; pjsip_tx_data *response = NULL; pj_status_t status; @@ -6771,6 +7049,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) { @@ -6789,8 +7068,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, @@ -8072,6 +8356,63 @@ 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) +{ + 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) { + 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); + 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) { + 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(); +} + #if defined(PJSUA_MEDIA_HAS_PJMEDIA) && PJSUA_MEDIA_HAS_PJMEDIA != 0 /* 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/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..6695b2ce71 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); } /////////////////////////////////////////////////////////////////////////////// @@ -1572,6 +1575,30 @@ void Endpoint::on_call_sdp_created(pjsua_call_id call_id, } } +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 PJ_FALSE; + } + + OnCallSendAckParam prm; + prm.rdata.fromPj(*rdata); + prm.cseq = rdata->msg_info.cseq->cseq; + + try { + call->onCallSendAck(prm); + } 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; +} + void Endpoint::on_stream_precreate(pjsua_call_id call_id, pjsua_on_stream_precreate_param *param) { @@ -2379,6 +2406,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; + 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; diff --git a/pjsip/src/test/inv_offer_answer_test.c b/pjsip/src/test/inv_offer_answer_test.c index 96fa1bfdf1..39f9b42da2 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 @@ -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,16 @@ 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 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; +static unsigned null_offer_create_offer_count; +static pj_status_t null_offer_reinvite_answer_status; typedef enum job_type { @@ -215,11 +227,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 +471,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 +538,112 @@ 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_FALSE; + 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) { + 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)", + 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 +658,7 @@ static int perform_test(inv_test_param_t *param) flush_events(500); - return 0; + return rc; } @@ -521,7 +669,24 @@ static pj_bool_t log_on_rx_msg(pjsip_rx_data *rdata) 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; + + sdp_info = pjsip_rdata_get_sdp_info(rdata); + 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 (msg->type == PJSIP_REQUEST_MSG) pj_ansi_snprintf(info, sizeof(info), "%.*s", (int)msg->line.req.method.name.slen, @@ -576,6 +741,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 +1015,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; @@ -871,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 5181a186bf..171014bc76 100644 --- a/pjsip/src/test/pjsua_call_test.c +++ b/pjsip/src/test/pjsua_call_test.c @@ -46,6 +46,17 @@ * 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. + * 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. 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 + * 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 @@ -231,6 +242,24 @@ 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_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; + 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; + 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. */ @@ -386,6 +415,234 @@ 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; + pjsip_inv_session inv; + 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); + 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); + return rc; +} + static pjmedia_sdp_session *create_oversized_sdp( pj_pool_t *pool, const pjmedia_sdp_session *sdp) @@ -522,6 +779,24 @@ 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); + 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; + } + if (g_ctx.uas_offer_armed) { g_ctx.incoming_seen = PJ_TRUE; return; @@ -626,16 +901,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 +926,69 @@ 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; + 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) { + 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 +1259,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,90 +6621,718 @@ static int test_async_reoffer_no_active_media(void) /***************************************************************************** * Main entry point *****************************************************************************/ -int pjsua_call_test(void) +static int test_answer_with_pending_local_offer(void) { - extern pjsip_endpoint *endpt; /* test framework endpoint */ - 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; - int rc = 0; - - PJ_LOG(3, (THIS_FILE, "pjsua call media-count bounds test")); - pj_bzero(&lib_settings, sizeof(lib_settings)); + 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; - /* pjsua creates its own endpoint and registers the (singleton) tsx layer - * module; destroy the framework endpoint first to avoid the duplicate - * registration assert, and recreate it afterwards. Same pattern as - * pjsua_auth_test.c. - */ - pjsip_endpt_destroy(endpt); - endpt = NULL; + rc = establish_self_call(&caller, &callee, -1900); + if (rc != 0) + goto on_return; - status = pjsua_create(); + inv = pjsua_var.calls[callee].inv; + status = pjmedia_sdp_neg_get_active_local(inv->neg, &active); if (status != PJ_SUCCESS) { - PJ_LOG(1, (THIS_FILE, " pjsua_create failed (%d)", status)); - rc = -2001; - goto on_restore; + rc = -1910; + goto on_return; } - pjsua_config_default(&ua_cfg); - ua_cfg.cb.on_incoming_call = &on_incoming_call; -#if PJSUA_HAS_SIPREC - ua_cfg.cb.on_call_siprec_metadata_update = - &on_call_siprec_metadata_update; -#endif - ua_cfg.cb.on_call_sdp_created = &on_call_sdp_created; - ua_cfg.cb.on_call_rx_offer = &on_call_rx_offer; - /* Registering on_call_rx_reinvite is what makes PJSUA wire up the invite - * session's on_rx_reinvite hook, which the direction-narrowing test needs - * in order to answer a re-INVITE itself. The callback stays inert unless - * that test arms it, and leaving async at PJ_FALSE keeps on_call_rx_offer - * in the loop for the other sub-tests. */ - ua_cfg.cb.on_call_rx_reinvite = &on_call_rx_reinvite; - ua_cfg.cb.on_call_transfer_request2 = &on_call_transfer_request2; - 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.cb.on_call_tsx_state = &on_call_tsx_state; - /* Single-threaded: event pumping happens on this thread, so callbacks - * run here too and no locking races arise in the test itself. */ - ua_cfg.thread_cnt = 0; - - /* Make sure PRACK and session timer are off and don't bother with SRTP */ - ua_cfg.require_100rel = PJSUA_100REL_NOT_USED; - ua_cfg.use_timer = PJSUA_SIP_TIMER_INACTIVE; - ua_cfg.use_srtp = PJMEDIA_SRTP_DISABLED; + 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; + } - pjsua_logging_config_default(&log_cfg); - log_cfg.level = 3; - log_cfg.console_level = 3; + 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; + } - pjsua_media_config_default(&media_cfg); - media_cfg.no_vad = PJ_TRUE; - media_cfg.enable_ice = PJ_FALSE; +on_return: + drain_all_calls(); + return rc; +} - 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 = -2002; - goto on_restore; - } +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; - pjsua_transport_config_default(&tp_cfg); - tp_cfg.port = 0; /* ephemeral */ - 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 = -2003; - goto on_restore; + 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 fail_answer) +{ + 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 fail=%d)", + async_answer ? "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); + 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_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; + 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; + 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; + { + 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 && + !(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 || + 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 || + 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 || + !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: + 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; + g_ctx.late_offer_ack_sdp = NULL; + g_ctx.late_offer_sdp = NULL; + drain_all_calls(); + 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, + pj_bool_t custom_offer) +{ + pjsua_call_id caller = PJSUA_INVALID_ID; + 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; + + PJ_LOG(3, (THIS_FILE, " late SDP re-INVITE (answer=%s ACK=%s offer=%s)", + async_answer ? "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); + g_ctx.skip_answer2_overlimit_probe = PJ_FALSE; + 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; + 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) { + 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; + } + 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); + } + 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; + } + + 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)) + { + 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; + 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 */ + 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; + int rc = 0; + + PJ_LOG(3, (THIS_FILE, "pjsua call media-count bounds test")); + pj_bzero(&lib_settings, sizeof(lib_settings)); + + /* pjsua creates its own endpoint and registers the (singleton) tsx layer + * module; destroy the framework endpoint first to avoid the duplicate + * registration assert, and recreate it afterwards. Same pattern as + * pjsua_auth_test.c. + */ + pjsip_endpt_destroy(endpt); + endpt = NULL; + + status = pjsua_create(); + if (status != PJ_SUCCESS) { + PJ_LOG(1, (THIS_FILE, " pjsua_create failed (%d)", status)); + rc = -2001; + goto on_restore; + } + + pjsua_config_default(&ua_cfg); + ua_cfg.cb.on_incoming_call = &on_incoming_call; +#if PJSUA_HAS_SIPREC + ua_cfg.cb.on_call_siprec_metadata_update = + &on_call_siprec_metadata_update; +#endif + ua_cfg.cb.on_call_sdp_created = &on_call_sdp_created; + ua_cfg.cb.on_call_rx_offer = &on_call_rx_offer; + /* Registering on_call_rx_reinvite is what makes PJSUA wire up the invite + * session's on_rx_reinvite hook, which the direction-narrowing test needs + * in order to answer a re-INVITE itself. The callback stays inert unless + * that test arms it, and leaving async at PJ_FALSE keeps on_call_rx_offer + * in the loop for the other sub-tests. */ + ua_cfg.cb.on_call_rx_reinvite = &on_call_rx_reinvite; + ua_cfg.cb.on_call_transfer_request2 = &on_call_transfer_request2; + 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.cb.on_call_tsx_state = &on_call_tsx_state; + /* Single-threaded: event pumping happens on this thread, so callbacks + * run here too and no locking races arise in the test itself. */ + ua_cfg.thread_cnt = 0; + + /* Make sure PRACK and session timer are off and don't bother with SRTP */ + 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 = -2002; + goto on_restore; + } + + pjsua_transport_config_default(&tp_cfg); + tp_cfg.port = 0; /* ephemeral */ + 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 = -2003; + goto on_restore; } status = pjsua_start(); @@ -6415,6 +7392,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; @@ -6500,6 +7480,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 +7502,157 @@ 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, 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_FALSE, PJ_TRUE, PJ_FALSE); + if (rc == 0) + rc = test_initial_late_sdp_offer(PJ_TRUE, PJ_TRUE, PJ_FALSE); + 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, 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_TRUE, PJ_FALSE); + if (rc == 0) + rc = test_late_sdp_reinvite(PJ_TRUE, PJ_FALSE, PJ_TRUE); + if (rc == 0) + rc = test_late_sdp_reinvite(PJ_TRUE, PJ_TRUE, PJ_TRUE); + if (rc == 0) + rc = test_sdp_error_response(); + + 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/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..6a94347b30 --- /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-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 +# 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..b00e836199 --- /dev/null +++ b/tests/pjsua/scripts-sipp/alt-pjsua-uas-late-offer-dummy-codecs.py @@ -0,0 +1,29 @@ +# +# 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 +# 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 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..13338714bc --- /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..aab99a4b94 --- /dev/null +++ b/tests/pjsua/scripts-sipp/alt-pjsua-uas-late-offer.py @@ -0,0 +1,34 @@ +# +# 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 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 + + ]]> + + + + + + + + +