summaryrefslogtreecommitdiff
path: root/pjsip/src/pjsua-lib
diff options
context:
space:
mode:
authorDavid M. Lee <dlee@digium.com>2013-01-07 14:24:28 -0600
committerDavid M. Lee <dlee@digium.com>2013-01-07 14:24:28 -0600
commitf3ab456a17af1c89a6e3be4d20c5944853df1cb0 (patch)
treed00e1a332cd038a6d906a1ea0ac91e1a4458e617 /pjsip/src/pjsua-lib
Import pjproject-2.0.1
Diffstat (limited to 'pjsip/src/pjsua-lib')
-rw-r--r--pjsip/src/pjsua-lib/pjsua_acc.c3078
-rw-r--r--pjsip/src/pjsua-lib/pjsua_aud.c2148
-rw-r--r--pjsip/src/pjsua-lib/pjsua_call.c4397
-rw-r--r--pjsip/src/pjsua-lib/pjsua_core.c2909
-rw-r--r--pjsip/src/pjsua-lib/pjsua_dump.c974
-rw-r--r--pjsip/src/pjsua-lib/pjsua_im.c745
-rw-r--r--pjsip/src/pjsua-lib/pjsua_media.c2695
-rw-r--r--pjsip/src/pjsua-lib/pjsua_pres.c2402
-rw-r--r--pjsip/src/pjsua-lib/pjsua_vid.c2166
9 files changed, 21514 insertions, 0 deletions
diff --git a/pjsip/src/pjsua-lib/pjsua_acc.c b/pjsip/src/pjsua-lib/pjsua_acc.c
new file mode 100644
index 0000000..c5278c4
--- /dev/null
+++ b/pjsip/src/pjsua-lib/pjsua_acc.c
@@ -0,0 +1,3078 @@
+/* $Id: pjsua_acc.c 4185 2012-06-28 14:16:05Z ming $ */
+/*
+ * Copyright (C) 2008-2011 Teluu Inc. (http://www.teluu.com)
+ * Copyright (C) 2003-2008 Benny Prijono <benny@prijono.org>
+ *
+ * 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
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ */
+#include <pjsua-lib/pjsua.h>
+#include <pjsua-lib/pjsua_internal.h>
+
+
+#define THIS_FILE "pjsua_acc.c"
+
+enum
+{
+ OUTBOUND_UNKNOWN, // status unknown
+ OUTBOUND_WANTED, // initiated in registration
+ OUTBOUND_ACTIVE, // got positive response from server
+ OUTBOUND_NA // not wanted or got negative response from server
+};
+
+
+static void schedule_reregistration(pjsua_acc *acc);
+static void keep_alive_timer_cb(pj_timer_heap_t *th, pj_timer_entry *te);
+
+/*
+ * Get number of current accounts.
+ */
+PJ_DEF(unsigned) pjsua_acc_get_count(void)
+{
+ return pjsua_var.acc_cnt;
+}
+
+
+/*
+ * Check if the specified account ID is valid.
+ */
+PJ_DEF(pj_bool_t) pjsua_acc_is_valid(pjsua_acc_id acc_id)
+{
+ return acc_id>=0 && acc_id<(int)PJ_ARRAY_SIZE(pjsua_var.acc) &&
+ pjsua_var.acc[acc_id].valid;
+}
+
+
+/*
+ * Set default account
+ */
+PJ_DEF(pj_status_t) pjsua_acc_set_default(pjsua_acc_id acc_id)
+{
+ pjsua_var.default_acc = acc_id;
+ return PJ_SUCCESS;
+}
+
+
+/*
+ * Get default account.
+ */
+PJ_DEF(pjsua_acc_id) pjsua_acc_get_default(void)
+{
+ return pjsua_var.default_acc;
+}
+
+
+/*
+ * Copy account configuration.
+ */
+PJ_DEF(void) pjsua_acc_config_dup( pj_pool_t *pool,
+ pjsua_acc_config *dst,
+ const pjsua_acc_config *src)
+{
+ unsigned i;
+
+ pj_memcpy(dst, src, sizeof(pjsua_acc_config));
+
+ pj_strdup_with_null(pool, &dst->id, &src->id);
+ pj_strdup_with_null(pool, &dst->reg_uri, &src->reg_uri);
+ pj_strdup_with_null(pool, &dst->force_contact, &src->force_contact);
+ pj_strdup_with_null(pool, &dst->contact_params, &src->contact_params);
+ pj_strdup_with_null(pool, &dst->contact_uri_params,
+ &src->contact_uri_params);
+ pj_strdup_with_null(pool, &dst->pidf_tuple_id, &src->pidf_tuple_id);
+ pj_strdup_with_null(pool, &dst->rfc5626_instance_id,
+ &src->rfc5626_instance_id);
+ pj_strdup_with_null(pool, &dst->rfc5626_reg_id, &src->rfc5626_reg_id);
+
+ dst->proxy_cnt = src->proxy_cnt;
+ for (i=0; i<src->proxy_cnt; ++i)
+ pj_strdup_with_null(pool, &dst->proxy[i], &src->proxy[i]);
+
+ dst->reg_timeout = src->reg_timeout;
+ dst->reg_delay_before_refresh = src->reg_delay_before_refresh;
+ dst->cred_count = src->cred_count;
+
+ for (i=0; i<src->cred_count; ++i) {
+ pjsip_cred_dup(pool, &dst->cred_info[i], &src->cred_info[i]);
+ }
+
+ pj_list_init(&dst->reg_hdr_list);
+ if (!pj_list_empty(&src->reg_hdr_list)) {
+ const pjsip_hdr *hdr;
+
+ hdr = src->reg_hdr_list.next;
+ while (hdr != &src->reg_hdr_list) {
+ pj_list_push_back(&dst->reg_hdr_list, pjsip_hdr_clone(pool, hdr));
+ hdr = hdr->next;
+ }
+ }
+
+ pj_list_init(&dst->sub_hdr_list);
+ if (!pj_list_empty(&src->sub_hdr_list)) {
+ const pjsip_hdr *hdr;
+
+ hdr = src->sub_hdr_list.next;
+ while (hdr != &src->sub_hdr_list) {
+ pj_list_push_back(&dst->sub_hdr_list, pjsip_hdr_clone(pool, hdr));
+ hdr = hdr->next;
+ }
+ }
+
+ pjsip_auth_clt_pref_dup(pool, &dst->auth_pref, &src->auth_pref);
+
+ pjsua_transport_config_dup(pool, &dst->rtp_cfg, &src->rtp_cfg);
+
+ pj_strdup(pool, &dst->ka_data, &src->ka_data);
+}
+
+/*
+ * Calculate CRC of proxy list.
+ */
+static pj_uint32_t calc_proxy_crc(const pj_str_t proxy[], pj_size_t cnt)
+{
+ pj_crc32_context ctx;
+ unsigned i;
+
+ pj_crc32_init(&ctx);
+ for (i=0; i<cnt; ++i) {
+ pj_crc32_update(&ctx, (pj_uint8_t*)proxy[i].ptr, proxy[i].slen);
+ }
+
+ return pj_crc32_final(&ctx);
+}
+
+/*
+ * Initialize a new account (after configuration is set).
+ */
+static pj_status_t initialize_acc(unsigned acc_id)
+{
+ pjsua_acc_config *acc_cfg = &pjsua_var.acc[acc_id].cfg;
+ pjsua_acc *acc = &pjsua_var.acc[acc_id];
+ pjsip_name_addr *name_addr;
+ pjsip_sip_uri *sip_reg_uri;
+ pj_status_t status;
+ unsigned i;
+
+ /* Need to parse local_uri to get the elements: */
+
+ name_addr = (pjsip_name_addr*)
+ pjsip_parse_uri(acc->pool, acc_cfg->id.ptr,
+ acc_cfg->id.slen,
+ PJSIP_PARSE_URI_AS_NAMEADDR);
+ if (name_addr == NULL) {
+ pjsua_perror(THIS_FILE, "Invalid local URI",
+ PJSIP_EINVALIDURI);
+ return PJSIP_EINVALIDURI;
+ }
+
+ /* Local URI MUST be a SIP or SIPS: */
+ if (!PJSIP_URI_SCHEME_IS_SIP(name_addr) &&
+ !PJSIP_URI_SCHEME_IS_SIPS(name_addr))
+ {
+ acc->display = name_addr->display;
+ acc->user_part = name_addr->display;
+ acc->srv_domain = pj_str("");
+ acc->srv_port = 0;
+ } else {
+ pjsip_sip_uri *sip_uri;
+
+ /* Get the SIP URI object: */
+ sip_uri = (pjsip_sip_uri*) pjsip_uri_get_uri(name_addr);
+
+ /* Save the user and domain part. These will be used when finding an
+ * account for incoming requests.
+ */
+ acc->display = name_addr->display;
+ acc->user_part = sip_uri->user;
+ acc->srv_domain = sip_uri->host;
+ acc->srv_port = 0;
+ }
+
+
+ /* Parse registrar URI, if any */
+ if (acc_cfg->reg_uri.slen) {
+ pjsip_uri *reg_uri;
+
+ reg_uri = pjsip_parse_uri(acc->pool, acc_cfg->reg_uri.ptr,
+ acc_cfg->reg_uri.slen, 0);
+ if (reg_uri == NULL) {
+ pjsua_perror(THIS_FILE, "Invalid registrar URI",
+ PJSIP_EINVALIDURI);
+ return PJSIP_EINVALIDURI;
+ }
+
+ /* Registrar URI MUST be a SIP or SIPS: */
+ if (!PJSIP_URI_SCHEME_IS_SIP(reg_uri) &&
+ !PJSIP_URI_SCHEME_IS_SIPS(reg_uri))
+ {
+ pjsua_perror(THIS_FILE, "Invalid registar URI",
+ PJSIP_EINVALIDSCHEME);
+ return PJSIP_EINVALIDSCHEME;
+ }
+
+ sip_reg_uri = (pjsip_sip_uri*) pjsip_uri_get_uri(reg_uri);
+
+ } else {
+ sip_reg_uri = NULL;
+ }
+
+ if (sip_reg_uri) {
+ acc->srv_port = sip_reg_uri->port;
+ }
+
+ /* Create Contact header if not present. */
+ //if (acc_cfg->contact.slen == 0) {
+ // acc_cfg->contact = acc_cfg->id;
+ //}
+
+ /* Build account route-set from outbound proxies and route set from
+ * account configuration.
+ */
+ pj_list_init(&acc->route_set);
+
+ if (!pj_list_empty(&pjsua_var.outbound_proxy)) {
+ pjsip_route_hdr *r;
+
+ r = pjsua_var.outbound_proxy.next;
+ while (r != &pjsua_var.outbound_proxy) {
+ pj_list_push_back(&acc->route_set,
+ pjsip_hdr_shallow_clone(acc->pool, r));
+ r = r->next;
+ }
+ }
+
+ for (i=0; i<acc_cfg->proxy_cnt; ++i) {
+ pj_str_t hname = { "Route", 5};
+ pjsip_route_hdr *r;
+ pj_str_t tmp;
+
+ pj_strdup_with_null(acc->pool, &tmp, &acc_cfg->proxy[i]);
+ r = (pjsip_route_hdr*)
+ pjsip_parse_hdr(acc->pool, &hname, tmp.ptr, tmp.slen, NULL);
+ if (r == NULL) {
+ pjsua_perror(THIS_FILE, "Invalid URI in account route set",
+ PJ_EINVAL);
+ return PJ_EINVAL;
+ }
+ pj_list_push_back(&acc->route_set, r);
+ }
+
+ /* Concatenate credentials from account config and global config */
+ acc->cred_cnt = 0;
+ for (i=0; i<acc_cfg->cred_count; ++i) {
+ acc->cred[acc->cred_cnt++] = acc_cfg->cred_info[i];
+ }
+ for (i=0; i<pjsua_var.ua_cfg.cred_count &&
+ acc->cred_cnt < PJ_ARRAY_SIZE(acc->cred); ++i)
+ {
+ acc->cred[acc->cred_cnt++] = pjsua_var.ua_cfg.cred_info[i];
+ }
+
+ /* If ICE is enabled, add "+sip.ice" media feature tag in account's
+ * contact params.
+ */
+#if PJSUA_ADD_ICE_TAGS
+ if (pjsua_var.media_cfg.enable_ice) {
+ unsigned new_len;
+ pj_str_t new_prm;
+
+ new_len = acc_cfg->contact_params.slen + 10;
+ new_prm.ptr = (char*)pj_pool_alloc(acc->pool, new_len);
+ pj_strcpy(&new_prm, &acc_cfg->contact_params);
+ pj_strcat2(&new_prm, ";+sip.ice");
+ acc_cfg->contact_params = new_prm;
+ }
+#endif
+
+ status = pjsua_pres_init_acc(acc_id);
+ if (status != PJ_SUCCESS)
+ return status;
+
+ /* If SIP outbound is enabled, generate instance and reg ID if they are
+ * not specified
+ */
+ if (acc_cfg->use_rfc5626) {
+ if (acc_cfg->rfc5626_instance_id.slen==0) {
+ const pj_str_t *hostname;
+ pj_uint32_t hval, pos;
+ char instprm[] = ";+sip.instance=\"<urn:uuid:00000000-0000-0000-0000-0000CCDDEEFF>\"";
+
+ hostname = pj_gethostname();
+ pos = pj_ansi_strlen(instprm) - 10;
+ hval = pj_hash_calc(0, hostname->ptr, hostname->slen);
+ pj_val_to_hex_digit( ((char*)&hval)[0], instprm+pos+0);
+ pj_val_to_hex_digit( ((char*)&hval)[1], instprm+pos+2);
+ pj_val_to_hex_digit( ((char*)&hval)[2], instprm+pos+4);
+ pj_val_to_hex_digit( ((char*)&hval)[3], instprm+pos+6);
+
+ pj_strdup2(acc->pool, &acc->rfc5626_instprm, instprm);
+ } else {
+ const char *prmname = ";+sip.instance=\"";
+ unsigned len;
+
+ len = pj_ansi_strlen(prmname) + acc_cfg->rfc5626_instance_id.slen + 1;
+ acc->rfc5626_instprm.ptr = (char*)pj_pool_alloc(acc->pool, len+1);
+ pj_ansi_snprintf(acc->rfc5626_instprm.ptr, len+1,
+ "%s%.*s\"",
+ prmname,
+ (int)acc_cfg->rfc5626_instance_id.slen,
+ acc_cfg->rfc5626_instance_id.ptr);
+ acc->rfc5626_instprm.slen = len;
+ }
+
+ if (acc_cfg->rfc5626_reg_id.slen==0) {
+ acc->rfc5626_regprm = pj_str(";reg-id=1");
+ } else {
+ const char *prmname = ";reg-id=";
+ unsigned len;
+
+ len = pj_ansi_strlen(prmname) + acc_cfg->rfc5626_reg_id.slen;
+ acc->rfc5626_regprm.ptr = (char*)pj_pool_alloc(acc->pool, len+1);
+ pj_ansi_snprintf(acc->rfc5626_regprm.ptr, len+1,
+ "%s%.*s\"",
+ prmname,
+ (int)acc_cfg->rfc5626_reg_id.slen,
+ acc_cfg->rfc5626_reg_id.ptr);
+ acc->rfc5626_regprm.slen = len;
+ }
+ }
+
+ /* Mark account as valid */
+ pjsua_var.acc[acc_id].valid = PJ_TRUE;
+
+ /* Insert account ID into account ID array, sorted by priority */
+ for (i=0; i<pjsua_var.acc_cnt; ++i) {
+ if ( pjsua_var.acc[pjsua_var.acc_ids[i]].cfg.priority <
+ pjsua_var.acc[acc_id].cfg.priority)
+ {
+ break;
+ }
+ }
+ pj_array_insert(pjsua_var.acc_ids, sizeof(pjsua_var.acc_ids[0]),
+ pjsua_var.acc_cnt, i, &acc_id);
+
+ return PJ_SUCCESS;
+}
+
+
+/*
+ * Add a new account to pjsua.
+ */
+PJ_DEF(pj_status_t) pjsua_acc_add( const pjsua_acc_config *cfg,
+ pj_bool_t is_default,
+ pjsua_acc_id *p_acc_id)
+{
+ pjsua_acc *acc;
+ unsigned i, id;
+ pj_status_t status = PJ_SUCCESS;
+
+ PJ_ASSERT_RETURN(cfg, PJ_EINVAL);
+ PJ_ASSERT_RETURN(pjsua_var.acc_cnt < PJ_ARRAY_SIZE(pjsua_var.acc),
+ PJ_ETOOMANY);
+
+ /* Must have a transport */
+ PJ_ASSERT_RETURN(pjsua_var.tpdata[0].data.ptr != NULL, PJ_EINVALIDOP);
+
+ PJ_LOG(4,(THIS_FILE, "Adding account: id=%.*s",
+ (int)cfg->id.slen, cfg->id.ptr));
+ pj_log_push_indent();
+
+ PJSUA_LOCK();
+
+ /* Find empty account id. */
+ for (id=0; id < PJ_ARRAY_SIZE(pjsua_var.acc); ++id) {
+ if (pjsua_var.acc[id].valid == PJ_FALSE)
+ break;
+ }
+
+ /* Expect to find a slot */
+ PJ_ASSERT_ON_FAIL( id < PJ_ARRAY_SIZE(pjsua_var.acc),
+ {PJSUA_UNLOCK(); return PJ_EBUG;});
+
+ acc = &pjsua_var.acc[id];
+
+ /* Create pool for this account. */
+ if (acc->pool)
+ pj_pool_reset(acc->pool);
+ else
+ acc->pool = pjsua_pool_create("acc%p", 512, 256);
+
+ /* Copy config */
+ pjsua_acc_config_dup(acc->pool, &pjsua_var.acc[id].cfg, cfg);
+
+ /* Normalize registration timeout and refresh delay */
+ if (pjsua_var.acc[id].cfg.reg_uri.slen) {
+ if (pjsua_var.acc[id].cfg.reg_timeout == 0) {
+ pjsua_var.acc[id].cfg.reg_timeout = PJSUA_REG_INTERVAL;
+ }
+ if (pjsua_var.acc[id].cfg.reg_delay_before_refresh == 0) {
+ pjsua_var.acc[id].cfg.reg_delay_before_refresh =
+ PJSIP_REGISTER_CLIENT_DELAY_BEFORE_REFRESH;
+ }
+ }
+
+ /* Check the route URI's and force loose route if required */
+ for (i=0; i<acc->cfg.proxy_cnt; ++i) {
+ status = normalize_route_uri(acc->pool, &acc->cfg.proxy[i]);
+ if (status != PJ_SUCCESS) {
+ PJSUA_UNLOCK();
+ pj_log_pop_indent();
+ return status;
+ }
+ }
+
+ /* Get CRC of account proxy setting */
+ acc->local_route_crc = calc_proxy_crc(acc->cfg.proxy, acc->cfg.proxy_cnt);
+
+ /* Get CRC of global outbound proxy setting */
+ acc->global_route_crc=calc_proxy_crc(pjsua_var.ua_cfg.outbound_proxy,
+ pjsua_var.ua_cfg.outbound_proxy_cnt);
+
+ status = initialize_acc(id);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Error adding account", status);
+ pj_pool_release(acc->pool);
+ acc->pool = NULL;
+ PJSUA_UNLOCK();
+ pj_log_pop_indent();
+ return status;
+ }
+
+ if (is_default)
+ pjsua_var.default_acc = id;
+
+ if (p_acc_id)
+ *p_acc_id = id;
+
+ pjsua_var.acc_cnt++;
+
+ PJSUA_UNLOCK();
+
+ PJ_LOG(4,(THIS_FILE, "Account %.*s added with id %d",
+ (int)cfg->id.slen, cfg->id.ptr, id));
+
+ /* If accounts has registration enabled, start registration */
+ if (pjsua_var.acc[id].cfg.reg_uri.slen) {
+ if (pjsua_var.acc[id].cfg.register_on_acc_add)
+ pjsua_acc_set_registration(id, PJ_TRUE);
+ } else {
+ /* Otherwise subscribe to MWI, if it's enabled */
+ if (pjsua_var.acc[id].cfg.mwi_enabled)
+ pjsua_start_mwi(id, PJ_TRUE);
+ }
+
+ pj_log_pop_indent();
+ return PJ_SUCCESS;
+}
+
+
+/*
+ * Add local account
+ */
+PJ_DEF(pj_status_t) pjsua_acc_add_local( pjsua_transport_id tid,
+ pj_bool_t is_default,
+ pjsua_acc_id *p_acc_id)
+{
+ pjsua_acc_config cfg;
+ pjsua_transport_data *t = &pjsua_var.tpdata[tid];
+ const char *beginquote, *endquote;
+ char transport_param[32];
+ char uri[PJSIP_MAX_URL_SIZE];
+
+ /* ID must be valid */
+ PJ_ASSERT_RETURN(tid>=0 && tid<(int)PJ_ARRAY_SIZE(pjsua_var.tpdata),
+ PJ_EINVAL);
+
+ /* Transport must be valid */
+ PJ_ASSERT_RETURN(t->data.ptr != NULL, PJ_EINVAL);
+
+ pjsua_acc_config_default(&cfg);
+
+ /* Lower the priority of local account */
+ --cfg.priority;
+
+ /* Enclose IPv6 address in square brackets */
+ if (t->type & PJSIP_TRANSPORT_IPV6) {
+ beginquote = "[";
+ endquote = "]";
+ } else {
+ beginquote = endquote = "";
+ }
+
+ /* Don't add transport parameter if it's UDP */
+ if (t->type!=PJSIP_TRANSPORT_UDP && t->type!=PJSIP_TRANSPORT_UDP6) {
+ pj_ansi_snprintf(transport_param, sizeof(transport_param),
+ ";transport=%s",
+ pjsip_transport_get_type_name(t->type));
+ } else {
+ transport_param[0] = '\0';
+ }
+
+ /* Build URI for the account */
+ pj_ansi_snprintf(uri, PJSIP_MAX_URL_SIZE,
+ "<sip:%s%.*s%s:%d%s>",
+ beginquote,
+ (int)t->local_name.host.slen,
+ t->local_name.host.ptr,
+ endquote,
+ t->local_name.port,
+ transport_param);
+
+ cfg.id = pj_str(uri);
+
+ return pjsua_acc_add(&cfg, is_default, p_acc_id);
+}
+
+
+/*
+ * Set arbitrary data to be associated with the account.
+ */
+PJ_DEF(pj_status_t) pjsua_acc_set_user_data(pjsua_acc_id acc_id,
+ void *user_data)
+{
+ PJ_ASSERT_RETURN(acc_id>=0 && acc_id<(int)PJ_ARRAY_SIZE(pjsua_var.acc),
+ PJ_EINVAL);
+ PJ_ASSERT_RETURN(pjsua_var.acc[acc_id].valid, PJ_EINVALIDOP);
+
+ PJSUA_LOCK();
+
+ pjsua_var.acc[acc_id].cfg.user_data = user_data;
+
+ PJSUA_UNLOCK();
+
+ return PJ_SUCCESS;
+}
+
+
+/*
+ * Retrieve arbitrary data associated with the account.
+ */
+PJ_DEF(void*) pjsua_acc_get_user_data(pjsua_acc_id acc_id)
+{
+ PJ_ASSERT_RETURN(acc_id>=0 && acc_id<(int)PJ_ARRAY_SIZE(pjsua_var.acc),
+ NULL);
+ PJ_ASSERT_RETURN(pjsua_var.acc[acc_id].valid, NULL);
+
+ return pjsua_var.acc[acc_id].cfg.user_data;
+}
+
+
+/*
+ * Delete account.
+ */
+PJ_DEF(pj_status_t) pjsua_acc_del(pjsua_acc_id acc_id)
+{
+ pjsua_acc *acc;
+ unsigned i;
+
+ PJ_ASSERT_RETURN(acc_id>=0 && acc_id<(int)PJ_ARRAY_SIZE(pjsua_var.acc),
+ PJ_EINVAL);
+ PJ_ASSERT_RETURN(pjsua_var.acc[acc_id].valid, PJ_EINVALIDOP);
+
+ PJ_LOG(4,(THIS_FILE, "Deleting account %d..", acc_id));
+ pj_log_push_indent();
+
+ PJSUA_LOCK();
+
+ acc = &pjsua_var.acc[acc_id];
+
+ /* Cancel keep-alive timer, if any */
+ if (acc->ka_timer.id) {
+ pjsip_endpt_cancel_timer(pjsua_var.endpt, &acc->ka_timer);
+ acc->ka_timer.id = PJ_FALSE;
+ }
+ if (acc->ka_transport) {
+ pjsip_transport_dec_ref(acc->ka_transport);
+ acc->ka_transport = NULL;
+ }
+
+ /* Cancel any re-registration timer */
+ if (acc->auto_rereg.timer.id) {
+ acc->auto_rereg.timer.id = PJ_FALSE;
+ pjsua_cancel_timer(&acc->auto_rereg.timer);
+ }
+
+ /* Delete registration */
+ if (acc->regc != NULL) {
+ pjsua_acc_set_registration(acc_id, PJ_FALSE);
+ if (acc->regc) {
+ pjsip_regc_destroy(acc->regc);
+ }
+ acc->regc = NULL;
+ }
+
+ /* Terminate mwi subscription */
+ if (acc->cfg.mwi_enabled) {
+ acc->cfg.mwi_enabled = PJ_FALSE;
+ pjsua_start_mwi(acc_id, PJ_FALSE);
+ }
+
+ /* Delete server presence subscription */
+ pjsua_pres_delete_acc(acc_id, 0);
+
+ /* Release account pool */
+ if (acc->pool) {
+ pj_pool_release(acc->pool);
+ acc->pool = NULL;
+ }
+
+ /* Invalidate */
+ acc->valid = PJ_FALSE;
+ acc->contact.slen = 0;
+ pj_bzero(&acc->via_addr, sizeof(acc->via_addr));
+ acc->via_tp = NULL;
+
+ /* Remove from array */
+ for (i=0; i<pjsua_var.acc_cnt; ++i) {
+ if (pjsua_var.acc_ids[i] == acc_id)
+ break;
+ }
+ if (i != pjsua_var.acc_cnt) {
+ pj_array_erase(pjsua_var.acc_ids, sizeof(pjsua_var.acc_ids[0]),
+ pjsua_var.acc_cnt, i);
+ --pjsua_var.acc_cnt;
+ }
+
+ /* Leave the calls intact, as I don't think calls need to
+ * access account once it's created
+ */
+
+ /* Update default account */
+ if (pjsua_var.default_acc == acc_id)
+ pjsua_var.default_acc = 0;
+
+ PJSUA_UNLOCK();
+
+ PJ_LOG(4,(THIS_FILE, "Account id %d deleted", acc_id));
+
+ pj_log_pop_indent();
+ return PJ_SUCCESS;
+}
+
+
+/* Get config */
+PJ_DEF(pj_status_t) pjsua_acc_get_config(pjsua_acc_id acc_id,
+ pjsua_acc_config *acc_cfg)
+{
+ PJ_ASSERT_RETURN(acc_id>=0 && acc_id<(int)PJ_ARRAY_SIZE(pjsua_var.acc)
+ && pjsua_var.acc[acc_id].valid, PJ_EINVAL);
+ pj_memcpy(acc_cfg, &pjsua_var.acc[acc_id].cfg, sizeof(*acc_cfg));
+ return PJ_SUCCESS;
+}
+
+/*
+ * Modify account information.
+ */
+PJ_DEF(pj_status_t) pjsua_acc_modify( pjsua_acc_id acc_id,
+ const pjsua_acc_config *cfg)
+{
+ pjsua_acc *acc;
+ pjsip_name_addr *id_name_addr = NULL;
+ pjsip_sip_uri *id_sip_uri = NULL;
+ pjsip_sip_uri *reg_sip_uri = NULL;
+ pj_uint32_t local_route_crc, global_route_crc;
+ pjsip_route_hdr global_route;
+ pjsip_route_hdr local_route;
+ pj_str_t acc_proxy[PJSUA_ACC_MAX_PROXIES];
+ pj_bool_t update_reg = PJ_FALSE;
+ pj_bool_t unreg_first = PJ_FALSE;
+ pj_bool_t update_mwi = PJ_FALSE;
+ pj_status_t status = PJ_SUCCESS;
+
+ PJ_ASSERT_RETURN(acc_id>=0 && acc_id<(int)PJ_ARRAY_SIZE(pjsua_var.acc),
+ PJ_EINVAL);
+
+ PJ_LOG(4,(THIS_FILE, "Modifying accunt %d", acc_id));
+ pj_log_push_indent();
+
+ PJSUA_LOCK();
+
+ acc = &pjsua_var.acc[acc_id];
+ if (!acc->valid) {
+ status = PJ_EINVAL;
+ goto on_return;
+ }
+
+ /* == Validate first == */
+
+ /* Account id */
+ if (pj_strcmp(&acc->cfg.id, &cfg->id)) {
+ /* Need to parse id to get the elements: */
+ id_name_addr = (pjsip_name_addr*)
+ pjsip_parse_uri(acc->pool, cfg->id.ptr, cfg->id.slen,
+ PJSIP_PARSE_URI_AS_NAMEADDR);
+ if (id_name_addr == NULL) {
+ status = PJSIP_EINVALIDURI;
+ pjsua_perror(THIS_FILE, "Invalid local URI", status);
+ goto on_return;
+ }
+
+ /* URI MUST be a SIP or SIPS: */
+ if (!PJSIP_URI_SCHEME_IS_SIP(id_name_addr) &&
+ !PJSIP_URI_SCHEME_IS_SIPS(id_name_addr))
+ {
+ status = PJSIP_EINVALIDSCHEME;
+ pjsua_perror(THIS_FILE, "Invalid local URI", status);
+ goto on_return;
+ }
+
+ /* Get the SIP URI object: */
+ id_sip_uri = (pjsip_sip_uri*) pjsip_uri_get_uri(id_name_addr);
+ }
+
+ /* Registrar URI */
+ if (pj_strcmp(&acc->cfg.reg_uri, &cfg->reg_uri) && cfg->reg_uri.slen) {
+ pjsip_uri *reg_uri;
+
+ /* Need to parse reg_uri to get the elements: */
+ reg_uri = pjsip_parse_uri(acc->pool, cfg->reg_uri.ptr,
+ cfg->reg_uri.slen, 0);
+ if (reg_uri == NULL) {
+ status = PJSIP_EINVALIDURI;
+ pjsua_perror(THIS_FILE, "Invalid registrar URI", status);
+ goto on_return;
+ }
+
+ /* Registrar URI MUST be a SIP or SIPS: */
+ if (!PJSIP_URI_SCHEME_IS_SIP(reg_uri) &&
+ !PJSIP_URI_SCHEME_IS_SIPS(reg_uri))
+ {
+ status = PJSIP_EINVALIDSCHEME;
+ pjsua_perror(THIS_FILE, "Invalid registar URI", status);
+ goto on_return;
+ }
+
+ reg_sip_uri = (pjsip_sip_uri*) pjsip_uri_get_uri(reg_uri);
+ }
+
+ /* Global outbound proxy */
+ global_route_crc = calc_proxy_crc(pjsua_var.ua_cfg.outbound_proxy,
+ pjsua_var.ua_cfg.outbound_proxy_cnt);
+ if (global_route_crc != acc->global_route_crc) {
+ pjsip_route_hdr *r;
+
+ /* Copy from global outbound proxies */
+ pj_list_init(&global_route);
+ r = pjsua_var.outbound_proxy.next;
+ while (r != &pjsua_var.outbound_proxy) {
+ pj_list_push_back(&global_route,
+ pjsip_hdr_shallow_clone(acc->pool, r));
+ r = r->next;
+ }
+ }
+
+ /* Account proxy */
+ local_route_crc = calc_proxy_crc(cfg->proxy, cfg->proxy_cnt);
+ if (local_route_crc != acc->local_route_crc) {
+ pjsip_route_hdr *r;
+ unsigned i;
+
+ /* Validate the local route and save it to temporary var */
+ pj_list_init(&local_route);
+ for (i=0; i<cfg->proxy_cnt; ++i) {
+ pj_str_t hname = { "Route", 5};
+
+ pj_strdup_with_null(acc->pool, &acc_proxy[i], &cfg->proxy[i]);
+ status = normalize_route_uri(acc->pool, &acc_proxy[i]);
+ if (status != PJ_SUCCESS)
+ goto on_return;
+ r = (pjsip_route_hdr*)
+ pjsip_parse_hdr(acc->pool, &hname, acc_proxy[i].ptr,
+ acc_proxy[i].slen, NULL);
+ if (r == NULL) {
+ status = PJSIP_EINVALIDURI;
+ pjsua_perror(THIS_FILE, "Invalid URI in account route set",
+ status);
+ goto on_return;
+ }
+
+ pj_list_push_back(&local_route, r);
+ }
+
+ /* Recalculate the CRC again after route URI normalization */
+ local_route_crc = calc_proxy_crc(acc_proxy, cfg->proxy_cnt);
+ }
+
+
+ /* == Apply the new config == */
+
+ /* Account ID. */
+ if (id_name_addr && id_sip_uri) {
+ pj_strdup_with_null(acc->pool, &acc->cfg.id, &cfg->id);
+ pj_strdup_with_null(acc->pool, &acc->display, &id_name_addr->display);
+ pj_strdup_with_null(acc->pool, &acc->user_part, &id_sip_uri->user);
+ pj_strdup_with_null(acc->pool, &acc->srv_domain, &id_sip_uri->host);
+ acc->srv_port = 0;
+ update_reg = PJ_TRUE;
+ unreg_first = PJ_TRUE;
+ }
+
+ /* User data */
+ acc->cfg.user_data = cfg->user_data;
+
+ /* Priority */
+ if (acc->cfg.priority != cfg->priority) {
+ unsigned i;
+
+ acc->cfg.priority = cfg->priority;
+
+ /* Resort accounts priority */
+ for (i=0; i<pjsua_var.acc_cnt; ++i) {
+ if (pjsua_var.acc_ids[i] == acc_id)
+ break;
+ }
+ pj_assert(i < pjsua_var.acc_cnt);
+ pj_array_erase(pjsua_var.acc_ids, sizeof(acc_id),
+ pjsua_var.acc_cnt, i);
+ for (i=0; i<pjsua_var.acc_cnt; ++i) {
+ if (pjsua_var.acc[pjsua_var.acc_ids[i]].cfg.priority <
+ acc->cfg.priority)
+ {
+ break;
+ }
+ }
+ pj_array_insert(pjsua_var.acc_ids, sizeof(acc_id),
+ pjsua_var.acc_cnt, i, &acc_id);
+ }
+
+ /* MWI */
+ if (acc->cfg.mwi_enabled != cfg->mwi_enabled) {
+ acc->cfg.mwi_enabled = cfg->mwi_enabled;
+ update_mwi = PJ_TRUE;
+ }
+ if (acc->cfg.mwi_expires != cfg->mwi_expires && cfg->mwi_expires > 0) {
+ acc->cfg.mwi_expires = cfg->mwi_expires;
+ update_mwi = PJ_TRUE;
+ }
+
+ /* PIDF tuple ID */
+ if (pj_strcmp(&acc->cfg.pidf_tuple_id, &cfg->pidf_tuple_id))
+ pj_strdup_with_null(acc->pool, &acc->cfg.pidf_tuple_id,
+ &cfg->pidf_tuple_id);
+
+ /* Publish */
+ acc->cfg.publish_opt = cfg->publish_opt;
+ acc->cfg.unpublish_max_wait_time_msec = cfg->unpublish_max_wait_time_msec;
+ if (acc->cfg.publish_enabled != cfg->publish_enabled) {
+ acc->cfg.publish_enabled = cfg->publish_enabled;
+ if (!acc->cfg.publish_enabled)
+ pjsua_pres_unpublish(acc, 0);
+ else
+ update_reg = PJ_TRUE;
+ }
+
+ /* Force contact URI */
+ if (pj_strcmp(&acc->cfg.force_contact, &cfg->force_contact)) {
+ pj_strdup_with_null(acc->pool, &acc->cfg.force_contact,
+ &cfg->force_contact);
+ update_reg = PJ_TRUE;
+ unreg_first = PJ_TRUE;
+ }
+
+ /* Contact param */
+ if (pj_strcmp(&acc->cfg.contact_params, &cfg->contact_params)) {
+ pj_strdup_with_null(acc->pool, &acc->cfg.contact_params,
+ &cfg->contact_params);
+ update_reg = PJ_TRUE;
+ }
+
+ /* Contact URI params */
+ if (pj_strcmp(&acc->cfg.contact_uri_params, &cfg->contact_uri_params)) {
+ pj_strdup_with_null(acc->pool, &acc->cfg.contact_uri_params,
+ &cfg->contact_uri_params);
+ update_reg = PJ_TRUE;
+ }
+
+ /* Reliable provisional response */
+ acc->cfg.require_100rel = cfg->require_100rel;
+
+ /* Session timer */
+ acc->cfg.use_timer = cfg->use_timer;
+ acc->cfg.timer_setting = cfg->timer_setting;
+
+ /* Transport */
+ if (acc->cfg.transport_id != cfg->transport_id) {
+ acc->cfg.transport_id = cfg->transport_id;
+ update_reg = PJ_TRUE;
+ }
+
+ /* Update keep-alive */
+ if (acc->cfg.ka_interval != cfg->ka_interval ||
+ pj_strcmp(&acc->cfg.ka_data, &cfg->ka_data))
+ {
+ pjsip_transport *ka_transport = acc->ka_transport;
+
+ if (acc->ka_timer.id) {
+ pjsip_endpt_cancel_timer(pjsua_var.endpt, &acc->ka_timer);
+ acc->ka_timer.id = PJ_FALSE;
+ }
+ if (acc->ka_transport) {
+ pjsip_transport_dec_ref(acc->ka_transport);
+ acc->ka_transport = NULL;
+ }
+
+ acc->cfg.ka_interval = cfg->ka_interval;
+
+ if (cfg->ka_interval) {
+ if (ka_transport) {
+ /* Keep-alive has been running so we can just restart it */
+ pj_time_val delay;
+
+ pjsip_transport_add_ref(ka_transport);
+ acc->ka_transport = ka_transport;
+
+ acc->ka_timer.cb = &keep_alive_timer_cb;
+ acc->ka_timer.user_data = (void*)acc;
+
+ delay.sec = acc->cfg.ka_interval;
+ delay.msec = 0;
+ status = pjsua_schedule_timer(&acc->ka_timer, &delay);
+ if (status == PJ_SUCCESS) {
+ acc->ka_timer.id = PJ_TRUE;
+ } else {
+ pjsip_transport_dec_ref(ka_transport);
+ acc->ka_transport = NULL;
+ pjsua_perror(THIS_FILE, "Error starting keep-alive timer",
+ status);
+ }
+
+ } else {
+ /* Keep-alive has not been running, we need to (re)register
+ * first.
+ */
+ update_reg = PJ_TRUE;
+ }
+ }
+ }
+
+ if (pj_strcmp(&acc->cfg.ka_data, &cfg->ka_data))
+ pj_strdup(acc->pool, &acc->cfg.ka_data, &cfg->ka_data);
+#if defined(PJMEDIA_HAS_SRTP) && (PJMEDIA_HAS_SRTP != 0)
+ acc->cfg.use_srtp = cfg->use_srtp;
+ acc->cfg.srtp_secure_signaling = cfg->srtp_secure_signaling;
+ acc->cfg.srtp_optional_dup_offer = cfg->srtp_optional_dup_offer;
+#endif
+
+#if defined(PJMEDIA_STREAM_ENABLE_KA) && (PJMEDIA_STREAM_ENABLE_KA != 0)
+ acc->cfg.use_stream_ka = cfg->use_stream_ka;
+#endif
+
+ /* Use of proxy */
+ if (acc->cfg.reg_use_proxy != cfg->reg_use_proxy) {
+ acc->cfg.reg_use_proxy = cfg->reg_use_proxy;
+ update_reg = PJ_TRUE;
+ }
+
+ /* Global outbound proxy */
+ if (global_route_crc != acc->global_route_crc) {
+ unsigned i, rcnt;
+
+ /* Remove the outbound proxies from the route set */
+ rcnt = pj_list_size(&acc->route_set);
+ for (i=0; i < rcnt - acc->cfg.proxy_cnt; ++i) {
+ pjsip_route_hdr *r = acc->route_set.next;
+ pj_list_erase(r);
+ }
+
+ /* Insert the outbound proxies to the beginning of route set */
+ pj_list_merge_first(&acc->route_set, &global_route);
+
+ /* Update global route CRC */
+ acc->global_route_crc = global_route_crc;
+
+ update_reg = PJ_TRUE;
+ }
+
+ /* Account proxy */
+ if (local_route_crc != acc->local_route_crc) {
+ unsigned i;
+
+ /* Remove the current account proxies from the route set */
+ for (i=0; i < acc->cfg.proxy_cnt; ++i) {
+ pjsip_route_hdr *r = acc->route_set.prev;
+ pj_list_erase(r);
+ }
+
+ /* Insert new proxy setting to the route set */
+ pj_list_merge_last(&acc->route_set, &local_route);
+
+ /* Update the proxy setting */
+ acc->cfg.proxy_cnt = cfg->proxy_cnt;
+ for (i = 0; i < cfg->proxy_cnt; ++i)
+ acc->cfg.proxy[i] = acc_proxy[i];
+
+ /* Update local route CRC */
+ acc->local_route_crc = local_route_crc;
+
+ update_reg = PJ_TRUE;
+ }
+
+ /* Credential info */
+ {
+ unsigned i;
+
+ /* Selective update credential info. */
+ for (i = 0; i < cfg->cred_count; ++i) {
+ unsigned j;
+ pjsip_cred_info ci;
+
+ /* Find if this credential is already listed */
+ for (j = i; j < acc->cfg.cred_count; ++j) {
+ if (pjsip_cred_info_cmp(&acc->cfg.cred_info[j],
+ &cfg->cred_info[i]) == 0)
+ {
+ /* Found, but different index/position, swap */
+ if (j != i) {
+ ci = acc->cfg.cred_info[i];
+ acc->cfg.cred_info[i] = acc->cfg.cred_info[j];
+ acc->cfg.cred_info[j] = ci;
+ }
+ break;
+ }
+ }
+
+ /* Not found, insert this */
+ if (j == acc->cfg.cred_count) {
+ /* If account credential is full, discard the last one. */
+ if (acc->cfg.cred_count == PJ_ARRAY_SIZE(acc->cfg.cred_info)) {
+ pj_array_erase(acc->cfg.cred_info, sizeof(pjsip_cred_info),
+ acc->cfg.cred_count, acc->cfg.cred_count-1);
+ acc->cfg.cred_count--;
+ }
+
+ /* Insert this */
+ pjsip_cred_info_dup(acc->pool, &ci, &cfg->cred_info[i]);
+ pj_array_insert(acc->cfg.cred_info, sizeof(pjsip_cred_info),
+ acc->cfg.cred_count, i, &ci);
+ }
+ }
+ acc->cfg.cred_count = cfg->cred_count;
+
+ /* Concatenate credentials from account config and global config */
+ acc->cred_cnt = 0;
+ for (i=0; i<acc->cfg.cred_count; ++i) {
+ acc->cred[acc->cred_cnt++] = acc->cfg.cred_info[i];
+ }
+ for (i=0; i<pjsua_var.ua_cfg.cred_count &&
+ acc->cred_cnt < PJ_ARRAY_SIZE(acc->cred); ++i)
+ {
+ acc->cred[acc->cred_cnt++] = pjsua_var.ua_cfg.cred_info[i];
+ }
+ }
+
+ /* Authentication preference */
+ acc->cfg.auth_pref.initial_auth = cfg->auth_pref.initial_auth;
+ if (pj_strcmp(&acc->cfg.auth_pref.algorithm, &cfg->auth_pref.algorithm))
+ pj_strdup_with_null(acc->pool, &acc->cfg.auth_pref.algorithm,
+ &cfg->auth_pref.algorithm);
+
+ /* Registration */
+ if (acc->cfg.reg_timeout != cfg->reg_timeout) {
+ acc->cfg.reg_timeout = cfg->reg_timeout;
+ if (acc->regc != NULL)
+ pjsip_regc_update_expires(acc->regc, acc->cfg.reg_timeout);
+
+ update_reg = PJ_TRUE;
+ }
+ acc->cfg.unreg_timeout = cfg->unreg_timeout;
+ acc->cfg.allow_contact_rewrite = cfg->allow_contact_rewrite;
+ acc->cfg.reg_retry_interval = cfg->reg_retry_interval;
+ acc->cfg.reg_first_retry_interval = cfg->reg_first_retry_interval;
+ acc->cfg.drop_calls_on_reg_fail = cfg->drop_calls_on_reg_fail;
+ acc->cfg.register_on_acc_add = cfg->register_on_acc_add;
+ if (acc->cfg.reg_delay_before_refresh != cfg->reg_delay_before_refresh) {
+ acc->cfg.reg_delay_before_refresh = cfg->reg_delay_before_refresh;
+ if (acc->regc != NULL)
+ pjsip_regc_set_delay_before_refresh(acc->regc,
+ cfg->reg_delay_before_refresh);
+ }
+
+ /* Allow via rewrite */
+ if (acc->cfg.allow_via_rewrite != cfg->allow_via_rewrite) {
+ if (acc->regc != NULL) {
+ if (cfg->allow_via_rewrite) {
+ pjsip_regc_set_via_sent_by(acc->regc, &acc->via_addr,
+ acc->via_tp);
+ } else
+ pjsip_regc_set_via_sent_by(acc->regc, NULL, NULL);
+ }
+ if (acc->publish_sess != NULL) {
+ if (cfg->allow_via_rewrite) {
+ pjsip_publishc_set_via_sent_by(acc->publish_sess,
+ &acc->via_addr, acc->via_tp);
+ } else
+ pjsip_publishc_set_via_sent_by(acc->publish_sess, NULL, NULL);
+ }
+ acc->cfg.allow_via_rewrite = cfg->allow_via_rewrite;
+ }
+
+ /* Normalize registration timeout and refresh delay */
+ if (acc->cfg.reg_uri.slen ) {
+ if (acc->cfg.reg_timeout == 0) {
+ acc->cfg.reg_timeout = PJSUA_REG_INTERVAL;
+ }
+ if (acc->cfg.reg_delay_before_refresh == 0) {
+ acc->cfg.reg_delay_before_refresh =
+ PJSIP_REGISTER_CLIENT_DELAY_BEFORE_REFRESH;
+ }
+ }
+
+ /* Registrar URI */
+ if (pj_strcmp(&acc->cfg.reg_uri, &cfg->reg_uri)) {
+ if (cfg->reg_uri.slen) {
+ pj_strdup_with_null(acc->pool, &acc->cfg.reg_uri, &cfg->reg_uri);
+ if (reg_sip_uri)
+ acc->srv_port = reg_sip_uri->port;
+ } else {
+ /* Unregister if registration was set */
+ if (acc->cfg.reg_uri.slen)
+ pjsua_acc_set_registration(acc->index, PJ_FALSE);
+ pj_bzero(&acc->cfg.reg_uri, sizeof(acc->cfg.reg_uri));
+ }
+ update_reg = PJ_TRUE;
+ unreg_first = PJ_TRUE;
+ }
+
+ /* SIP outbound setting */
+ if (acc->cfg.use_rfc5626 != cfg->use_rfc5626 ||
+ pj_strcmp(&acc->cfg.rfc5626_instance_id, &cfg->rfc5626_instance_id) ||
+ pj_strcmp(&acc->cfg.rfc5626_reg_id, &cfg->rfc5626_reg_id))
+ {
+ update_reg = PJ_TRUE;
+ }
+
+ /* Unregister first */
+ if (unreg_first) {
+ pjsua_acc_set_registration(acc->index, PJ_FALSE);
+ if (acc->regc != NULL) {
+ pjsip_regc_destroy(acc->regc);
+ acc->regc = NULL;
+ acc->contact.slen = 0;
+ }
+ }
+
+ /* Update registration */
+ if (update_reg) {
+ /* If accounts has registration enabled, start registration */
+ if (acc->cfg.reg_uri.slen)
+ pjsua_acc_set_registration(acc->index, PJ_TRUE);
+ }
+
+ /* Update MWI subscription */
+ if (update_mwi) {
+ pjsua_start_mwi(acc_id, PJ_TRUE);
+ }
+
+ /* Video settings */
+ acc->cfg.vid_in_auto_show = cfg->vid_in_auto_show;
+ acc->cfg.vid_out_auto_transmit = cfg->vid_out_auto_transmit;
+ acc->cfg.vid_wnd_flags = cfg->vid_wnd_flags;
+ acc->cfg.vid_cap_dev = cfg->vid_cap_dev;
+ acc->cfg.vid_rend_dev = cfg->vid_rend_dev;
+
+ /* Call hold type */
+ acc->cfg.call_hold_type = cfg->call_hold_type;
+
+on_return:
+ PJSUA_UNLOCK();
+ pj_log_pop_indent();
+ return status;
+}
+
+
+/*
+ * Modify account's presence status to be advertised to remote/presence
+ * subscribers.
+ */
+PJ_DEF(pj_status_t) pjsua_acc_set_online_status( pjsua_acc_id acc_id,
+ pj_bool_t is_online)
+{
+ PJ_ASSERT_RETURN(acc_id>=0 && acc_id<(int)PJ_ARRAY_SIZE(pjsua_var.acc),
+ PJ_EINVAL);
+ PJ_ASSERT_RETURN(pjsua_var.acc[acc_id].valid, PJ_EINVALIDOP);
+
+ PJ_LOG(4,(THIS_FILE, "Acc %d: setting online status to %d..",
+ acc_id, is_online));
+ pj_log_push_indent();
+
+ pjsua_var.acc[acc_id].online_status = is_online;
+ pj_bzero(&pjsua_var.acc[acc_id].rpid, sizeof(pjrpid_element));
+ pjsua_pres_update_acc(acc_id, PJ_FALSE);
+
+ pj_log_pop_indent();
+ return PJ_SUCCESS;
+}
+
+
+/*
+ * Set online status with extended information
+ */
+PJ_DEF(pj_status_t) pjsua_acc_set_online_status2( pjsua_acc_id acc_id,
+ pj_bool_t is_online,
+ const pjrpid_element *pr)
+{
+ PJ_ASSERT_RETURN(acc_id>=0 && acc_id<(int)PJ_ARRAY_SIZE(pjsua_var.acc),
+ PJ_EINVAL);
+ PJ_ASSERT_RETURN(pjsua_var.acc[acc_id].valid, PJ_EINVALIDOP);
+
+ PJ_LOG(4,(THIS_FILE, "Acc %d: setting online status to %d..",
+ acc_id, is_online));
+ pj_log_push_indent();
+
+ PJSUA_LOCK();
+ pjsua_var.acc[acc_id].online_status = is_online;
+ pjrpid_element_dup(pjsua_var.acc[acc_id].pool, &pjsua_var.acc[acc_id].rpid, pr);
+ PJSUA_UNLOCK();
+
+ pjsua_pres_update_acc(acc_id, PJ_TRUE);
+ pj_log_pop_indent();
+
+ return PJ_SUCCESS;
+}
+
+/* Create reg_contact, mainly for SIP outbound */
+static void update_regc_contact(pjsua_acc *acc)
+{
+ pjsua_acc_config *acc_cfg = &acc->cfg;
+ pj_bool_t need_outbound = PJ_FALSE;
+ const pj_str_t tcp_param = pj_str(";transport=tcp");
+ const pj_str_t tls_param = pj_str(";transport=tls");
+
+ if (!acc_cfg->use_rfc5626)
+ goto done;
+
+ /* Check if outbound has been requested and rejected */
+ if (acc->rfc5626_status == OUTBOUND_NA)
+ goto done;
+
+ if (pj_stristr(&acc->contact, &tcp_param)==NULL &&
+ pj_stristr(&acc->contact, &tls_param)==NULL)
+ {
+ /* Currently we can only do SIP outbound for TCP
+ * and TLS.
+ */
+ goto done;
+ }
+
+ /* looks like we can use outbound */
+ need_outbound = PJ_TRUE;
+
+done:
+ if (!need_outbound) {
+ /* Outbound is not needed/wanted for the account. acc->reg_contact
+ * is set to the same as acc->contact.
+ */
+ acc->reg_contact = acc->contact;
+ acc->rfc5626_status = OUTBOUND_NA;
+ } else {
+ /* Need to use outbound, append the contact with +sip.instance and
+ * reg-id parameters.
+ */
+ unsigned len;
+ pj_str_t reg_contact;
+
+ acc->rfc5626_status = OUTBOUND_WANTED;
+ len = acc->contact.slen + acc->rfc5626_instprm.slen +
+ acc->rfc5626_regprm.slen;
+ reg_contact.ptr = (char*) pj_pool_alloc(acc->pool, len);
+
+ pj_strcpy(&reg_contact, &acc->contact);
+ pj_strcat(&reg_contact, &acc->rfc5626_regprm);
+ pj_strcat(&reg_contact, &acc->rfc5626_instprm);
+
+ acc->reg_contact = reg_contact;
+
+ PJ_LOG(4,(THIS_FILE,
+ "Contact for acc %d updated for SIP outbound: %.*s",
+ acc->index,
+ (int)acc->reg_contact.slen,
+ acc->reg_contact.ptr));
+ }
+}
+
+/* Check if IP is private IP address */
+static pj_bool_t is_private_ip(const pj_str_t *addr)
+{
+ const pj_str_t private_net[] =
+ {
+ { "10.", 3 },
+ { "127.", 4 },
+ { "172.16.", 7 },
+ { "192.168.", 8 }
+ };
+ unsigned i;
+
+ for (i=0; i<PJ_ARRAY_SIZE(private_net); ++i) {
+ if (pj_strncmp(addr, &private_net[i], private_net[i].slen)==0)
+ return PJ_TRUE;
+ }
+
+ return PJ_FALSE;
+}
+
+/* Update NAT address from the REGISTER response */
+static pj_bool_t acc_check_nat_addr(pjsua_acc *acc,
+ struct pjsip_regc_cbparam *param)
+{
+ pjsip_transport *tp;
+ const pj_str_t *via_addr;
+ pj_pool_t *pool;
+ int rport;
+ pjsip_sip_uri *uri;
+ pjsip_via_hdr *via;
+ pj_sockaddr contact_addr;
+ pj_sockaddr recv_addr;
+ pj_status_t status;
+ pj_bool_t matched;
+ pj_str_t srv_ip;
+ pjsip_contact_hdr *contact_hdr;
+ const pj_str_t STR_CONTACT = { "Contact", 7 };
+
+ tp = param->rdata->tp_info.transport;
+
+ /* Get the received and rport info */
+ via = param->rdata->msg_info.via;
+ if (via->rport_param < 1) {
+ /* Remote doesn't support rport */
+ rport = via->sent_by.port;
+ if (rport==0) {
+ pjsip_transport_type_e tp_type;
+ tp_type = (pjsip_transport_type_e) tp->key.type;
+ rport = pjsip_transport_get_default_port_for_type(tp_type);
+ }
+ } else
+ rport = via->rport_param;
+
+ if (via->recvd_param.slen != 0)
+ via_addr = &via->recvd_param;
+ else
+ via_addr = &via->sent_by.host;
+
+ /* If allow_via_rewrite is enabled, we save the Via "received" address
+ * from the response.
+ */
+ if (acc->cfg.allow_via_rewrite &&
+ (acc->via_addr.host.slen == 0 || acc->via_tp != tp))
+ {
+ if (pj_strcmp(&acc->via_addr.host, via_addr))
+ pj_strdup(acc->pool, &acc->via_addr.host, via_addr);
+ acc->via_addr.port = rport;
+ acc->via_tp = tp;
+ pjsip_regc_set_via_sent_by(acc->regc, &acc->via_addr, acc->via_tp);
+ if (acc->publish_sess != NULL) {
+ pjsip_publishc_set_via_sent_by(acc->publish_sess,
+ &acc->via_addr, acc->via_tp);
+ }
+ }
+
+ /* Only update if account is configured to auto-update */
+ if (acc->cfg.allow_contact_rewrite == PJ_FALSE)
+ return PJ_FALSE;
+
+ /* If SIP outbound is active, no need to update */
+ if (acc->rfc5626_status == OUTBOUND_ACTIVE) {
+ PJ_LOG(4,(THIS_FILE, "Acc %d has SIP outbound active, no need to "
+ "update registration Contact", acc->index));
+ return PJ_FALSE;
+ }
+
+#if 0
+ // Always update
+ // See http://lists.pjsip.org/pipermail/pjsip_lists.pjsip.org/2008-March/002178.html
+
+ /* For UDP, only update if STUN is enabled (for now).
+ * For TCP/TLS, always check.
+ */
+ if ((tp->key.type == PJSIP_TRANSPORT_UDP &&
+ (pjsua_var.ua_cfg.stun_domain.slen != 0 ||
+ (pjsua_var.ua_cfg.stun_host.slen != 0)) ||
+ (tp->key.type == PJSIP_TRANSPORT_TCP) ||
+ (tp->key.type == PJSIP_TRANSPORT_TLS))
+ {
+ /* Yes we will check */
+ } else {
+ return PJ_FALSE;
+ }
+#endif
+
+ /* Compare received and rport with the URI in our registration */
+ pool = pjsua_pool_create("tmp", 512, 512);
+ contact_hdr = (pjsip_contact_hdr*)
+ pjsip_parse_hdr(pool, &STR_CONTACT, acc->contact.ptr,
+ acc->contact.slen, NULL);
+ pj_assert(contact_hdr != NULL);
+ uri = (pjsip_sip_uri*) contact_hdr->uri;
+ pj_assert(uri != NULL);
+ uri = (pjsip_sip_uri*) pjsip_uri_get_uri(uri);
+
+ if (uri->port == 0) {
+ pjsip_transport_type_e tp_type;
+ tp_type = (pjsip_transport_type_e) tp->key.type;
+ uri->port = pjsip_transport_get_default_port_for_type(tp_type);
+ }
+
+ /* Convert IP address strings into sockaddr for comparison.
+ * (http://trac.pjsip.org/repos/ticket/863)
+ */
+ status = pj_sockaddr_parse(pj_AF_UNSPEC(), 0, &uri->host,
+ &contact_addr);
+ if (status == PJ_SUCCESS)
+ status = pj_sockaddr_parse(pj_AF_UNSPEC(), 0, via_addr,
+ &recv_addr);
+ if (status == PJ_SUCCESS) {
+ /* Compare the addresses as sockaddr according to the ticket above */
+ matched = (uri->port == rport &&
+ pj_sockaddr_cmp(&contact_addr, &recv_addr)==0);
+ } else {
+ /* Compare the addresses as string, as before */
+ matched = (uri->port == rport &&
+ pj_stricmp(&uri->host, via_addr)==0);
+ }
+
+ if (matched) {
+ /* Address doesn't change */
+ pj_pool_release(pool);
+ return PJ_FALSE;
+ }
+
+ /* Get server IP */
+ srv_ip = pj_str(param->rdata->pkt_info.src_name);
+
+ /* At this point we've detected that the address as seen by registrar.
+ * has changed.
+ */
+
+ /* Do not switch if both Contact and server's IP address are
+ * public but response contains private IP. A NAT in the middle
+ * might have messed up with the SIP packets. See:
+ * http://trac.pjsip.org/repos/ticket/643
+ *
+ * This exception can be disabled by setting allow_contact_rewrite
+ * to 2. In this case, the switch will always be done whenever there
+ * is difference in the IP address in the response.
+ */
+ if (acc->cfg.allow_contact_rewrite != 2 && !is_private_ip(&uri->host) &&
+ !is_private_ip(&srv_ip) && is_private_ip(via_addr))
+ {
+ /* Don't switch */
+ pj_pool_release(pool);
+ return PJ_FALSE;
+ }
+
+ /* Also don't switch if only the port number part is different, and
+ * the Via received address is private.
+ * See http://trac.pjsip.org/repos/ticket/864
+ */
+ if (acc->cfg.allow_contact_rewrite != 2 &&
+ pj_sockaddr_cmp(&contact_addr, &recv_addr)==0 &&
+ is_private_ip(via_addr))
+ {
+ /* Don't switch */
+ pj_pool_release(pool);
+ return PJ_FALSE;
+ }
+
+ PJ_LOG(3,(THIS_FILE, "IP address change detected for account %d "
+ "(%.*s:%d --> %.*s:%d). Updating registration "
+ "(using method %d)",
+ acc->index,
+ (int)uri->host.slen,
+ uri->host.ptr,
+ uri->port,
+ (int)via_addr->slen,
+ via_addr->ptr,
+ rport,
+ acc->cfg.contact_rewrite_method));
+
+ pj_assert(acc->cfg.contact_rewrite_method == 1 ||
+ acc->cfg.contact_rewrite_method == 2);
+
+ if (acc->cfg.contact_rewrite_method == 1) {
+ /* Unregister current contact */
+ pjsua_acc_set_registration(acc->index, PJ_FALSE);
+ if (acc->regc != NULL) {
+ pjsip_regc_destroy(acc->regc);
+ acc->regc = NULL;
+ acc->contact.slen = 0;
+ }
+ }
+
+ /*
+ * Build new Contact header
+ */
+ {
+ const char *ob = ";ob";
+ char *tmp;
+ const char *beginquote, *endquote;
+ int len;
+
+ /* Enclose IPv6 address in square brackets */
+ if (tp->key.type & PJSIP_TRANSPORT_IPV6) {
+ beginquote = "[";
+ endquote = "]";
+ } else {
+ beginquote = endquote = "";
+ }
+
+ tmp = (char*) pj_pool_alloc(pool, PJSIP_MAX_URL_SIZE);
+ len = pj_ansi_snprintf(tmp, PJSIP_MAX_URL_SIZE,
+ "<sip:%.*s%s%s%.*s%s:%d;transport=%s%.*s%s>%.*s",
+ (int)acc->user_part.slen,
+ acc->user_part.ptr,
+ (acc->user_part.slen? "@" : ""),
+ beginquote,
+ (int)via_addr->slen,
+ via_addr->ptr,
+ endquote,
+ rport,
+ tp->type_name,
+ (int)acc->cfg.contact_uri_params.slen,
+ acc->cfg.contact_uri_params.ptr,
+ (acc->cfg.use_rfc5626? ob: ""),
+ (int)acc->cfg.contact_params.slen,
+ acc->cfg.contact_params.ptr);
+ if (len < 1) {
+ PJ_LOG(1,(THIS_FILE, "URI too long"));
+ pj_pool_release(pool);
+ return PJ_FALSE;
+ }
+ pj_strdup2_with_null(acc->pool, &acc->contact, tmp);
+
+ update_regc_contact(acc);
+
+ /* Always update, by http://trac.pjsip.org/repos/ticket/864. */
+ /* Since the Via address will now be overwritten to the correct
+ * address by https://trac.pjsip.org/repos/ticket/1537, we do
+ * not need to update the transport address.
+ */
+ /*
+ pj_strdup_with_null(tp->pool, &tp->local_name.host, via_addr);
+ tp->local_name.port = rport;
+ */
+
+ }
+
+ if (acc->cfg.contact_rewrite_method == 2 && acc->regc != NULL) {
+ pjsip_regc_update_contact(acc->regc, 1, &acc->reg_contact);
+ }
+
+ /* Perform new registration */
+ pjsua_acc_set_registration(acc->index, PJ_TRUE);
+
+ pj_pool_release(pool);
+
+ return PJ_TRUE;
+}
+
+/* Check and update Service-Route header */
+void update_service_route(pjsua_acc *acc, pjsip_rx_data *rdata)
+{
+ pjsip_generic_string_hdr *hsr = NULL;
+ pjsip_route_hdr *hr, *h;
+ const pj_str_t HNAME = { "Service-Route", 13 };
+ const pj_str_t HROUTE = { "Route", 5 };
+ pjsip_uri *uri[PJSUA_ACC_MAX_PROXIES];
+ unsigned i, uri_cnt = 0, rcnt;
+
+ /* Find and parse Service-Route headers */
+ for (;;) {
+ char saved;
+ int parsed_len;
+
+ /* Find Service-Route header */
+ hsr = (pjsip_generic_string_hdr*)
+ pjsip_msg_find_hdr_by_name(rdata->msg_info.msg, &HNAME, hsr);
+ if (!hsr)
+ break;
+
+ /* Parse as Route header since the syntax is similar. This may
+ * return more than one headers.
+ */
+ saved = hsr->hvalue.ptr[hsr->hvalue.slen];
+ hsr->hvalue.ptr[hsr->hvalue.slen] = '\0';
+ hr = (pjsip_route_hdr*)
+ pjsip_parse_hdr(rdata->tp_info.pool, &HROUTE, hsr->hvalue.ptr,
+ hsr->hvalue.slen, &parsed_len);
+ hsr->hvalue.ptr[hsr->hvalue.slen] = saved;
+
+ if (hr == NULL) {
+ /* Error */
+ PJ_LOG(1,(THIS_FILE, "Error parsing Service-Route header"));
+ return;
+ }
+
+ /* Save each URI in the result */
+ h = hr;
+ do {
+ if (!PJSIP_URI_SCHEME_IS_SIP(h->name_addr.uri) &&
+ !PJSIP_URI_SCHEME_IS_SIPS(h->name_addr.uri))
+ {
+ PJ_LOG(1,(THIS_FILE,"Error: non SIP URI in Service-Route: %.*s",
+ (int)hsr->hvalue.slen, hsr->hvalue.ptr));
+ return;
+ }
+
+ uri[uri_cnt++] = h->name_addr.uri;
+ h = h->next;
+ } while (h != hr && uri_cnt != PJ_ARRAY_SIZE(uri));
+
+ if (h != hr) {
+ PJ_LOG(1,(THIS_FILE, "Error: too many Service-Route headers"));
+ return;
+ }
+
+ /* Prepare to find next Service-Route header */
+ hsr = hsr->next;
+ if ((void*)hsr == (void*)&rdata->msg_info.msg->hdr)
+ break;
+ }
+
+ if (uri_cnt == 0)
+ return;
+
+ /*
+ * Update account's route set
+ */
+
+ /* First remove all routes which are not the outbound proxies */
+ rcnt = pj_list_size(&acc->route_set);
+ if (rcnt != pjsua_var.ua_cfg.outbound_proxy_cnt + acc->cfg.proxy_cnt) {
+ for (i=pjsua_var.ua_cfg.outbound_proxy_cnt + acc->cfg.proxy_cnt,
+ hr=acc->route_set.prev;
+ i<rcnt;
+ ++i)
+ {
+ pjsip_route_hdr *prev = hr->prev;
+ pj_list_erase(hr);
+ hr = prev;
+ }
+ }
+
+ /* Then append the Service-Route URIs */
+ for (i=0; i<uri_cnt; ++i) {
+ hr = pjsip_route_hdr_create(acc->pool);
+ hr->name_addr.uri = (pjsip_uri*)pjsip_uri_clone(acc->pool, uri[i]);
+ pj_list_push_back(&acc->route_set, hr);
+ }
+
+ /* Done */
+
+ PJ_LOG(4,(THIS_FILE, "Service-Route updated for acc %d with %d URI(s)",
+ acc->index, uri_cnt));
+}
+
+
+/* Keep alive timer callback */
+static void keep_alive_timer_cb(pj_timer_heap_t *th, pj_timer_entry *te)
+{
+ pjsua_acc *acc;
+ pjsip_tpselector tp_sel;
+ pj_time_val delay;
+ char addrtxt[PJ_INET6_ADDRSTRLEN];
+ pj_status_t status;
+
+ PJ_UNUSED_ARG(th);
+
+ PJSUA_LOCK();
+
+ te->id = PJ_FALSE;
+
+ acc = (pjsua_acc*) te->user_data;
+
+ /* Select the transport to send the packet */
+ pj_bzero(&tp_sel, sizeof(tp_sel));
+ tp_sel.type = PJSIP_TPSELECTOR_TRANSPORT;
+ tp_sel.u.transport = acc->ka_transport;
+
+ PJ_LOG(5,(THIS_FILE,
+ "Sending %d bytes keep-alive packet for acc %d to %s",
+ acc->cfg.ka_data.slen, acc->index,
+ pj_sockaddr_print(&acc->ka_target, addrtxt, sizeof(addrtxt),3)));
+
+ /* Send raw packet */
+ status = pjsip_tpmgr_send_raw(pjsip_endpt_get_tpmgr(pjsua_var.endpt),
+ PJSIP_TRANSPORT_UDP, &tp_sel,
+ NULL, acc->cfg.ka_data.ptr,
+ acc->cfg.ka_data.slen,
+ &acc->ka_target, acc->ka_target_len,
+ NULL, NULL);
+
+ if (status != PJ_SUCCESS && status != PJ_EPENDING) {
+ pjsua_perror(THIS_FILE, "Error sending keep-alive packet", status);
+ }
+
+ /* Check just in case keep-alive has been disabled. This shouldn't happen
+ * though as when ka_interval is changed this timer should have been
+ * cancelled.
+ */
+ if (acc->cfg.ka_interval == 0)
+ goto on_return;
+
+ /* Reschedule next timer */
+ delay.sec = acc->cfg.ka_interval;
+ delay.msec = 0;
+ status = pjsip_endpt_schedule_timer(pjsua_var.endpt, te, &delay);
+ if (status == PJ_SUCCESS) {
+ te->id = PJ_TRUE;
+ } else {
+ pjsua_perror(THIS_FILE, "Error starting keep-alive timer", status);
+ }
+
+on_return:
+ PJSUA_UNLOCK();
+}
+
+
+/* Update keep-alive for the account */
+static void update_keep_alive(pjsua_acc *acc, pj_bool_t start,
+ struct pjsip_regc_cbparam *param)
+{
+ /* In all cases, stop keep-alive timer if it's running. */
+ if (acc->ka_timer.id) {
+ pjsip_endpt_cancel_timer(pjsua_var.endpt, &acc->ka_timer);
+ acc->ka_timer.id = PJ_FALSE;
+
+ pjsip_transport_dec_ref(acc->ka_transport);
+ acc->ka_transport = NULL;
+ }
+
+ if (start) {
+ pj_time_val delay;
+ pj_status_t status;
+
+ /* Only do keep-alive if:
+ * - ka_interval is not zero in the account, and
+ * - transport is UDP.
+ *
+ * Previously we only enabled keep-alive when STUN is enabled, since
+ * we thought that keep-alive is only needed in Internet situation.
+ * But it has been discovered that Windows Firewall on WinXP also
+ * needs to be kept-alive, otherwise incoming packets will be dropped.
+ * So because of this, now keep-alive is always enabled for UDP,
+ * regardless of whether STUN is enabled or not.
+ *
+ * Note that this applies only for UDP. For TCP/TLS, the keep-alive
+ * is done by the transport layer.
+ */
+ if (/*pjsua_var.stun_srv.ipv4.sin_family == 0 ||*/
+ acc->cfg.ka_interval == 0 ||
+ param->rdata->tp_info.transport->key.type != PJSIP_TRANSPORT_UDP)
+ {
+ /* Keep alive is not necessary */
+ return;
+ }
+
+ /* Save transport and destination address. */
+ acc->ka_transport = param->rdata->tp_info.transport;
+ pjsip_transport_add_ref(acc->ka_transport);
+ pj_memcpy(&acc->ka_target, &param->rdata->pkt_info.src_addr,
+ param->rdata->pkt_info.src_addr_len);
+ acc->ka_target_len = param->rdata->pkt_info.src_addr_len;
+
+ /* Setup and start the timer */
+ acc->ka_timer.cb = &keep_alive_timer_cb;
+ acc->ka_timer.user_data = (void*)acc;
+
+ delay.sec = acc->cfg.ka_interval;
+ delay.msec = 0;
+ status = pjsip_endpt_schedule_timer(pjsua_var.endpt, &acc->ka_timer,
+ &delay);
+ if (status == PJ_SUCCESS) {
+ acc->ka_timer.id = PJ_TRUE;
+ PJ_LOG(4,(THIS_FILE, "Keep-alive timer started for acc %d, "
+ "destination:%s:%d, interval:%ds",
+ acc->index,
+ param->rdata->pkt_info.src_name,
+ param->rdata->pkt_info.src_port,
+ acc->cfg.ka_interval));
+ } else {
+ acc->ka_timer.id = PJ_FALSE;
+ pjsip_transport_dec_ref(acc->ka_transport);
+ acc->ka_transport = NULL;
+ pjsua_perror(THIS_FILE, "Error starting keep-alive timer", status);
+ }
+ }
+}
+
+
+/* Update the status of SIP outbound registration request */
+static void update_rfc5626_status(pjsua_acc *acc, pjsip_rx_data *rdata)
+{
+ pjsip_require_hdr *hreq;
+ const pj_str_t STR_OUTBOUND = {"outbound", 8};
+ unsigned i;
+
+ if (acc->rfc5626_status == OUTBOUND_UNKNOWN) {
+ goto on_return;
+ }
+
+ hreq = rdata->msg_info.require;
+ if (!hreq) {
+ acc->rfc5626_status = OUTBOUND_NA;
+ goto on_return;
+ }
+
+ for (i=0; i<hreq->count; ++i) {
+ if (pj_stricmp(&hreq->values[i], &STR_OUTBOUND)==0) {
+ acc->rfc5626_status = OUTBOUND_ACTIVE;
+ goto on_return;
+ }
+ }
+
+ /* Server does not support outbound */
+ acc->rfc5626_status = OUTBOUND_NA;
+
+on_return:
+ if (acc->rfc5626_status != OUTBOUND_ACTIVE) {
+ acc->reg_contact = acc->contact;
+ }
+ PJ_LOG(4,(THIS_FILE, "SIP outbound status for acc %d is %s",
+ acc->index, (acc->rfc5626_status==OUTBOUND_ACTIVE?
+ "active": "not active")));
+}
+
+/*
+ * This callback is called by pjsip_regc when outgoing register
+ * request has completed.
+ */
+static void regc_cb(struct pjsip_regc_cbparam *param)
+{
+
+ pjsua_acc *acc = (pjsua_acc*) param->token;
+
+ PJSUA_LOCK();
+
+ if (param->regc != acc->regc) {
+ PJSUA_UNLOCK();
+ return;
+ }
+
+ pj_log_push_indent();
+
+ /*
+ * Print registration status.
+ */
+ if (param->status!=PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "SIP registration error",
+ param->status);
+ pjsip_regc_destroy(acc->regc);
+ acc->regc = NULL;
+ acc->contact.slen = 0;
+
+ /* Stop keep-alive timer if any. */
+ update_keep_alive(acc, PJ_FALSE, NULL);
+
+ } else if (param->code < 0 || param->code >= 300) {
+ PJ_LOG(2, (THIS_FILE, "SIP registration failed, status=%d (%.*s)",
+ param->code,
+ (int)param->reason.slen, param->reason.ptr));
+ pjsip_regc_destroy(acc->regc);
+ acc->regc = NULL;
+ acc->contact.slen = 0;
+
+ /* Stop keep-alive timer if any. */
+ update_keep_alive(acc, PJ_FALSE, NULL);
+
+ } else if (PJSIP_IS_STATUS_IN_CLASS(param->code, 200)) {
+
+ /* Update auto registration flag */
+ acc->auto_rereg.active = PJ_FALSE;
+ acc->auto_rereg.attempt_cnt = 0;
+
+ if (param->expiration < 1) {
+ pjsip_regc_destroy(acc->regc);
+ acc->regc = NULL;
+ acc->contact.slen = 0;
+
+ /* Stop keep-alive timer if any. */
+ update_keep_alive(acc, PJ_FALSE, NULL);
+
+ PJ_LOG(3,(THIS_FILE, "%s: unregistration success",
+ pjsua_var.acc[acc->index].cfg.id.ptr));
+ } else {
+ /* Check and update SIP outbound status first, since the result
+ * will determine if we should update re-registration
+ */
+ update_rfc5626_status(acc, param->rdata);
+
+ /* Check NAT bound address */
+ if (acc_check_nat_addr(acc, param)) {
+ PJSUA_UNLOCK();
+ pj_log_pop_indent();
+ return;
+ }
+
+ /* Check and update Service-Route header */
+ update_service_route(acc, param->rdata);
+
+ PJ_LOG(3, (THIS_FILE,
+ "%s: registration success, status=%d (%.*s), "
+ "will re-register in %d seconds",
+ pjsua_var.acc[acc->index].cfg.id.ptr,
+ param->code,
+ (int)param->reason.slen, param->reason.ptr,
+ param->expiration));
+
+ /* Start keep-alive timer if necessary. */
+ update_keep_alive(acc, PJ_TRUE, param);
+
+ /* Send initial PUBLISH if it is enabled */
+ if (acc->cfg.publish_enabled && acc->publish_sess==NULL)
+ pjsua_pres_init_publish_acc(acc->index);
+
+ /* Subscribe to MWI, if it's enabled */
+ if (acc->cfg.mwi_enabled)
+ pjsua_start_mwi(acc->index, PJ_FALSE);
+ }
+
+ } else {
+ PJ_LOG(4, (THIS_FILE, "SIP registration updated status=%d", param->code));
+ }
+
+ acc->reg_last_err = param->status;
+ acc->reg_last_code = param->code;
+
+ /* Check if we need to auto retry registration. Basically, registration
+ * failure codes triggering auto-retry are those of temporal failures
+ * considered to be recoverable in relatively short term.
+ */
+ if (acc->cfg.reg_retry_interval &&
+ (param->code == PJSIP_SC_REQUEST_TIMEOUT ||
+ param->code == PJSIP_SC_INTERNAL_SERVER_ERROR ||
+ param->code == PJSIP_SC_BAD_GATEWAY ||
+ param->code == PJSIP_SC_SERVICE_UNAVAILABLE ||
+ param->code == PJSIP_SC_SERVER_TIMEOUT ||
+ PJSIP_IS_STATUS_IN_CLASS(param->code, 600))) /* Global failure */
+ {
+ schedule_reregistration(acc);
+ }
+
+ /* Call the registration status callback */
+
+ if (pjsua_var.ua_cfg.cb.on_reg_state) {
+ (*pjsua_var.ua_cfg.cb.on_reg_state)(acc->index);
+ }
+
+ if (pjsua_var.ua_cfg.cb.on_reg_state2) {
+ pjsua_reg_info reg_info;
+
+ reg_info.cbparam = param;
+ (*pjsua_var.ua_cfg.cb.on_reg_state2)(acc->index, &reg_info);
+ }
+
+ PJSUA_UNLOCK();
+ pj_log_pop_indent();
+}
+
+
+/*
+ * Initialize client registration.
+ */
+static pj_status_t pjsua_regc_init(int acc_id)
+{
+ pjsua_acc *acc;
+ pj_pool_t *pool;
+ pj_status_t status;
+
+ PJ_ASSERT_RETURN(pjsua_acc_is_valid(acc_id), PJ_EINVAL);
+ acc = &pjsua_var.acc[acc_id];
+
+ if (acc->cfg.reg_uri.slen == 0) {
+ PJ_LOG(3,(THIS_FILE, "Registrar URI is not specified"));
+ return PJ_SUCCESS;
+ }
+
+ /* Destroy existing session, if any */
+ if (acc->regc) {
+ pjsip_regc_destroy(acc->regc);
+ acc->regc = NULL;
+ acc->contact.slen = 0;
+ }
+
+ /* initialize SIP registration if registrar is configured */
+
+ status = pjsip_regc_create( pjsua_var.endpt,
+ acc, &regc_cb, &acc->regc);
+
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Unable to create client registration",
+ status);
+ return status;
+ }
+
+ pool = pjsua_pool_create("tmpregc", 512, 512);
+
+ if (acc->contact.slen == 0) {
+ pj_str_t tmp_contact;
+
+ status = pjsua_acc_create_uac_contact( pool, &tmp_contact,
+ acc_id, &acc->cfg.reg_uri);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Unable to generate suitable Contact header"
+ " for registration",
+ status);
+ pjsip_regc_destroy(acc->regc);
+ pj_pool_release(pool);
+ acc->regc = NULL;
+ return status;
+ }
+
+ pj_strdup_with_null(acc->pool, &acc->contact, &tmp_contact);
+ update_regc_contact(acc);
+ }
+
+ status = pjsip_regc_init( acc->regc,
+ &acc->cfg.reg_uri,
+ &acc->cfg.id,
+ &acc->cfg.id,
+ 1, &acc->reg_contact,
+ acc->cfg.reg_timeout);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE,
+ "Client registration initialization error",
+ status);
+ pjsip_regc_destroy(acc->regc);
+ pj_pool_release(pool);
+ acc->regc = NULL;
+ acc->contact.slen = 0;
+ return status;
+ }
+
+ /* If account is locked to specific transport, then set transport to
+ * the client registration.
+ */
+ if (pjsua_var.acc[acc_id].cfg.transport_id != PJSUA_INVALID_ID) {
+ pjsip_tpselector tp_sel;
+
+ pjsua_init_tpselector(pjsua_var.acc[acc_id].cfg.transport_id, &tp_sel);
+ pjsip_regc_set_transport(acc->regc, &tp_sel);
+ }
+
+
+ /* Set credentials
+ */
+ if (acc->cred_cnt) {
+ pjsip_regc_set_credentials( acc->regc, acc->cred_cnt, acc->cred);
+ }
+
+ /* Set delay before registration refresh */
+ pjsip_regc_set_delay_before_refresh(acc->regc,
+ acc->cfg.reg_delay_before_refresh);
+
+ /* Set authentication preference */
+ pjsip_regc_set_prefs(acc->regc, &acc->cfg.auth_pref);
+
+ /* Set route-set
+ */
+ if (acc->cfg.reg_use_proxy) {
+ pjsip_route_hdr route_set;
+ const pjsip_route_hdr *r;
+
+ pj_list_init(&route_set);
+
+ if (acc->cfg.reg_use_proxy & PJSUA_REG_USE_OUTBOUND_PROXY) {
+ r = pjsua_var.outbound_proxy.next;
+ while (r != &pjsua_var.outbound_proxy) {
+ pj_list_push_back(&route_set, pjsip_hdr_shallow_clone(pool, r));
+ r = r->next;
+ }
+ }
+
+ if (acc->cfg.reg_use_proxy & PJSUA_REG_USE_ACC_PROXY &&
+ acc->cfg.proxy_cnt)
+ {
+ int cnt = acc->cfg.proxy_cnt;
+ pjsip_route_hdr *pos = route_set.prev;
+ int i;
+
+ r = acc->route_set.prev;
+ for (i=0; i<cnt; ++i) {
+ pj_list_push_front(pos, pjsip_hdr_shallow_clone(pool, r));
+ r = r->prev;
+ }
+ }
+
+ if (!pj_list_empty(&route_set))
+ pjsip_regc_set_route_set( acc->regc, &route_set );
+ }
+
+ /* Add custom request headers specified in the account config */
+ pjsip_regc_add_headers(acc->regc, &acc->cfg.reg_hdr_list);
+
+ /* Add other request headers. */
+ if (pjsua_var.ua_cfg.user_agent.slen) {
+ pjsip_hdr hdr_list;
+ const pj_str_t STR_USER_AGENT = { "User-Agent", 10 };
+ pjsip_generic_string_hdr *h;
+
+ pj_list_init(&hdr_list);
+
+ h = pjsip_generic_string_hdr_create(pool, &STR_USER_AGENT,
+ &pjsua_var.ua_cfg.user_agent);
+ pj_list_push_back(&hdr_list, (pjsip_hdr*)h);
+
+ pjsip_regc_add_headers(acc->regc, &hdr_list);
+ }
+
+ /* If SIP outbound is used, add "Supported: outbound, path header" */
+ if (acc->rfc5626_status == OUTBOUND_WANTED) {
+ pjsip_hdr hdr_list;
+ pjsip_supported_hdr *hsup;
+
+ pj_list_init(&hdr_list);
+ hsup = pjsip_supported_hdr_create(pool);
+ pj_list_push_back(&hdr_list, hsup);
+
+ hsup->count = 2;
+ hsup->values[0] = pj_str("outbound");
+ hsup->values[1] = pj_str("path");
+
+ pjsip_regc_add_headers(acc->regc, &hdr_list);
+ }
+
+ pj_pool_release(pool);
+
+ return PJ_SUCCESS;
+}
+
+
+/*
+ * Update registration or perform unregistration.
+ */
+PJ_DEF(pj_status_t) pjsua_acc_set_registration( pjsua_acc_id acc_id,
+ pj_bool_t renew)
+{
+ pj_status_t status = 0;
+ pjsip_tx_data *tdata = 0;
+
+ PJ_ASSERT_RETURN(acc_id>=0 && acc_id<(int)PJ_ARRAY_SIZE(pjsua_var.acc),
+ PJ_EINVAL);
+ PJ_ASSERT_RETURN(pjsua_var.acc[acc_id].valid, PJ_EINVALIDOP);
+
+ PJ_LOG(4,(THIS_FILE, "Acc %d: setting %sregistration..",
+ acc_id, (renew? "" : "un")));
+ pj_log_push_indent();
+
+ PJSUA_LOCK();
+
+ /* Cancel any re-registration timer */
+ if (pjsua_var.acc[acc_id].auto_rereg.timer.id) {
+ pjsua_var.acc[acc_id].auto_rereg.timer.id = PJ_FALSE;
+ pjsua_cancel_timer(&pjsua_var.acc[acc_id].auto_rereg.timer);
+ }
+
+ /* Reset pointer to registration transport */
+ pjsua_var.acc[acc_id].auto_rereg.reg_tp = NULL;
+
+ if (renew) {
+ if (pjsua_var.acc[acc_id].regc == NULL) {
+ status = pjsua_regc_init(acc_id);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Unable to create registration",
+ status);
+ goto on_return;
+ }
+ }
+ if (!pjsua_var.acc[acc_id].regc) {
+ status = PJ_EINVALIDOP;
+ goto on_return;
+ }
+
+ status = pjsip_regc_register(pjsua_var.acc[acc_id].regc, 1,
+ &tdata);
+
+ if (0 && status == PJ_SUCCESS && pjsua_var.acc[acc_id].cred_cnt) {
+ pjsua_acc *acc = &pjsua_var.acc[acc_id];
+ pjsip_authorization_hdr *h;
+ char *uri;
+ int d;
+
+ uri = (char*) pj_pool_alloc(tdata->pool, acc->cfg.reg_uri.slen+10);
+ d = pjsip_uri_print(PJSIP_URI_IN_REQ_URI, tdata->msg->line.req.uri,
+ uri, acc->cfg.reg_uri.slen+10);
+ pj_assert(d > 0);
+
+ h = pjsip_authorization_hdr_create(tdata->pool);
+ h->scheme = pj_str("Digest");
+ h->credential.digest.username = acc->cred[0].username;
+ h->credential.digest.realm = acc->srv_domain;
+ h->credential.digest.uri = pj_str(uri);
+ h->credential.digest.algorithm = pj_str("md5");
+
+ pjsip_msg_add_hdr(tdata->msg, (pjsip_hdr*)h);
+ }
+
+ } else {
+ if (pjsua_var.acc[acc_id].regc == NULL) {
+ PJ_LOG(3,(THIS_FILE, "Currently not registered"));
+ status = PJ_EINVALIDOP;
+ goto on_return;
+ }
+
+ pjsua_pres_unpublish(&pjsua_var.acc[acc_id], 0);
+
+ status = pjsip_regc_unregister(pjsua_var.acc[acc_id].regc, &tdata);
+ }
+
+ if (status == PJ_SUCCESS) {
+ if (pjsua_var.acc[acc_id].cfg.allow_via_rewrite &&
+ pjsua_var.acc[acc_id].via_addr.host.slen > 0)
+ {
+ pjsip_regc_set_via_sent_by(pjsua_var.acc[acc_id].regc,
+ &pjsua_var.acc[acc_id].via_addr,
+ pjsua_var.acc[acc_id].via_tp);
+ }
+
+ //pjsua_process_msg_data(tdata, NULL);
+ status = pjsip_regc_send( pjsua_var.acc[acc_id].regc, tdata );
+ }
+
+ /* Update pointer to registration transport */
+ if (status == PJ_SUCCESS) {
+ pjsip_regc_info reg_info;
+
+ pjsip_regc_get_info(pjsua_var.acc[acc_id].regc, &reg_info);
+ pjsua_var.acc[acc_id].auto_rereg.reg_tp = reg_info.transport;
+
+ if (pjsua_var.ua_cfg.cb.on_reg_started) {
+ (*pjsua_var.ua_cfg.cb.on_reg_started)(acc_id, renew);
+ }
+ }
+
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Unable to create/send REGISTER",
+ status);
+ } else {
+ PJ_LOG(4,(THIS_FILE, "Acc %d: %s sent", acc_id,
+ (renew? "Registration" : "Unregistration")));
+ }
+
+on_return:
+ PJSUA_UNLOCK();
+ pj_log_pop_indent();
+ return status;
+}
+
+
+/*
+ * Get account information.
+ */
+PJ_DEF(pj_status_t) pjsua_acc_get_info( pjsua_acc_id acc_id,
+ pjsua_acc_info *info)
+{
+ pjsua_acc *acc = &pjsua_var.acc[acc_id];
+ pjsua_acc_config *acc_cfg = &pjsua_var.acc[acc_id].cfg;
+
+ PJ_ASSERT_RETURN(info != NULL, PJ_EINVAL);
+ PJ_ASSERT_RETURN(pjsua_acc_is_valid(acc_id), PJ_EINVAL);
+
+ pj_bzero(info, sizeof(pjsua_acc_info));
+
+ PJ_ASSERT_RETURN(acc_id>=0 && acc_id<(int)PJ_ARRAY_SIZE(pjsua_var.acc),
+ PJ_EINVAL);
+ PJ_ASSERT_RETURN(pjsua_var.acc[acc_id].valid, PJ_EINVALIDOP);
+
+ PJSUA_LOCK();
+
+ if (pjsua_var.acc[acc_id].valid == PJ_FALSE) {
+ PJSUA_UNLOCK();
+ return PJ_EINVALIDOP;
+ }
+
+ info->id = acc_id;
+ info->is_default = (pjsua_var.default_acc == acc_id);
+ info->acc_uri = acc_cfg->id;
+ info->has_registration = (acc->cfg.reg_uri.slen > 0);
+ info->online_status = acc->online_status;
+ pj_memcpy(&info->rpid, &acc->rpid, sizeof(pjrpid_element));
+ if (info->rpid.note.slen)
+ info->online_status_text = info->rpid.note;
+ else if (info->online_status)
+ info->online_status_text = pj_str("Online");
+ else
+ info->online_status_text = pj_str("Offline");
+
+ if (acc->reg_last_code) {
+ if (info->has_registration) {
+ info->status = (pjsip_status_code) acc->reg_last_code;
+ info->status_text = *pjsip_get_status_text(acc->reg_last_code);
+ if (acc->reg_last_err)
+ info->reg_last_err = acc->reg_last_err;
+ } else {
+ info->status = (pjsip_status_code) 0;
+ info->status_text = pj_str("not registered");
+ }
+ } else if (acc->cfg.reg_uri.slen) {
+ info->status = PJSIP_SC_TRYING;
+ info->status_text = pj_str("In Progress");
+ } else {
+ info->status = (pjsip_status_code) 0;
+ info->status_text = pj_str("does not register");
+ }
+
+ if (acc->regc) {
+ pjsip_regc_info regc_info;
+ pjsip_regc_get_info(acc->regc, &regc_info);
+ info->expires = regc_info.next_reg;
+ } else {
+ info->expires = -1;
+ }
+
+ PJSUA_UNLOCK();
+
+ return PJ_SUCCESS;
+
+}
+
+
+/*
+ * Enum accounts all account ids.
+ */
+PJ_DEF(pj_status_t) pjsua_enum_accs(pjsua_acc_id ids[],
+ unsigned *count )
+{
+ unsigned i, c;
+
+ PJ_ASSERT_RETURN(ids && *count, PJ_EINVAL);
+
+ PJSUA_LOCK();
+
+ for (i=0, c=0; c<*count && i<PJ_ARRAY_SIZE(pjsua_var.acc); ++i) {
+ if (!pjsua_var.acc[i].valid)
+ continue;
+ ids[c] = i;
+ ++c;
+ }
+
+ *count = c;
+
+ PJSUA_UNLOCK();
+
+ return PJ_SUCCESS;
+}
+
+
+/*
+ * Enum accounts info.
+ */
+PJ_DEF(pj_status_t) pjsua_acc_enum_info( pjsua_acc_info info[],
+ unsigned *count )
+{
+ unsigned i, c;
+
+ PJ_ASSERT_RETURN(info && *count, PJ_EINVAL);
+
+ PJSUA_LOCK();
+
+ for (i=0, c=0; c<*count && i<PJ_ARRAY_SIZE(pjsua_var.acc); ++i) {
+ if (!pjsua_var.acc[i].valid)
+ continue;
+
+ pjsua_acc_get_info(i, &info[c]);
+ ++c;
+ }
+
+ *count = c;
+
+ PJSUA_UNLOCK();
+
+ return PJ_SUCCESS;
+}
+
+
+/*
+ * This is an internal function to find the most appropriate account to
+ * used to reach to the specified URL.
+ */
+PJ_DEF(pjsua_acc_id) pjsua_acc_find_for_outgoing(const pj_str_t *url)
+{
+ pj_str_t tmp;
+ pjsip_uri *uri;
+ pjsip_sip_uri *sip_uri;
+ pj_pool_t *tmp_pool;
+ unsigned i;
+
+ PJSUA_LOCK();
+
+ tmp_pool = pjsua_pool_create("tmpacc10", 256, 256);
+
+ pj_strdup_with_null(tmp_pool, &tmp, url);
+
+ uri = pjsip_parse_uri(tmp_pool, tmp.ptr, tmp.slen, 0);
+ if (!uri) {
+ pj_pool_release(tmp_pool);
+ PJSUA_UNLOCK();
+ return pjsua_var.default_acc;
+ }
+
+ if (!PJSIP_URI_SCHEME_IS_SIP(uri) &&
+ !PJSIP_URI_SCHEME_IS_SIPS(uri))
+ {
+ /* Return the first account with proxy */
+ for (i=0; i<PJ_ARRAY_SIZE(pjsua_var.acc); ++i) {
+ if (!pjsua_var.acc[i].valid)
+ continue;
+ if (!pj_list_empty(&pjsua_var.acc[i].route_set))
+ break;
+ }
+
+ if (i != PJ_ARRAY_SIZE(pjsua_var.acc)) {
+ /* Found rather matching account */
+ pj_pool_release(tmp_pool);
+ PJSUA_UNLOCK();
+ return i;
+ }
+
+ /* Not found, use default account */
+ pj_pool_release(tmp_pool);
+ PJSUA_UNLOCK();
+ return pjsua_var.default_acc;
+ }
+
+ sip_uri = (pjsip_sip_uri*) pjsip_uri_get_uri(uri);
+
+ /* Find matching domain AND port */
+ for (i=0; i<pjsua_var.acc_cnt; ++i) {
+ unsigned acc_id = pjsua_var.acc_ids[i];
+ if (pj_stricmp(&pjsua_var.acc[acc_id].srv_domain, &sip_uri->host)==0 &&
+ pjsua_var.acc[acc_id].srv_port == sip_uri->port)
+ {
+ pj_pool_release(tmp_pool);
+ PJSUA_UNLOCK();
+ return acc_id;
+ }
+ }
+
+ /* If no match, try to match the domain part only */
+ for (i=0; i<pjsua_var.acc_cnt; ++i) {
+ unsigned acc_id = pjsua_var.acc_ids[i];
+ if (pj_stricmp(&pjsua_var.acc[acc_id].srv_domain, &sip_uri->host)==0)
+ {
+ pj_pool_release(tmp_pool);
+ PJSUA_UNLOCK();
+ return acc_id;
+ }
+ }
+
+
+ /* Still no match, just use default account */
+ pj_pool_release(tmp_pool);
+ PJSUA_UNLOCK();
+ return pjsua_var.default_acc;
+}
+
+
+/*
+ * This is an internal function to find the most appropriate account to be
+ * used to handle incoming calls.
+ */
+PJ_DEF(pjsua_acc_id) pjsua_acc_find_for_incoming(pjsip_rx_data *rdata)
+{
+ pjsip_uri *uri;
+ pjsip_sip_uri *sip_uri;
+ unsigned i;
+
+ /* Check that there's at least one account configured */
+ PJ_ASSERT_RETURN(pjsua_var.acc_cnt!=0, pjsua_var.default_acc);
+
+ uri = rdata->msg_info.to->uri;
+
+ /* Just return default account if To URI is not SIP: */
+ if (!PJSIP_URI_SCHEME_IS_SIP(uri) &&
+ !PJSIP_URI_SCHEME_IS_SIPS(uri))
+ {
+ return pjsua_var.default_acc;
+ }
+
+
+ PJSUA_LOCK();
+
+ sip_uri = (pjsip_sip_uri*)pjsip_uri_get_uri(uri);
+
+ /* Find account which has matching username and domain. */
+ for (i=0; i < pjsua_var.acc_cnt; ++i) {
+ unsigned acc_id = pjsua_var.acc_ids[i];
+ pjsua_acc *acc = &pjsua_var.acc[acc_id];
+
+ if (acc->valid && pj_stricmp(&acc->user_part, &sip_uri->user)==0 &&
+ pj_stricmp(&acc->srv_domain, &sip_uri->host)==0)
+ {
+ /* Match ! */
+ PJSUA_UNLOCK();
+ return acc_id;
+ }
+ }
+
+ /* No matching account, try match domain part only. */
+ for (i=0; i < pjsua_var.acc_cnt; ++i) {
+ unsigned acc_id = pjsua_var.acc_ids[i];
+ pjsua_acc *acc = &pjsua_var.acc[acc_id];
+
+ if (acc->valid && pj_stricmp(&acc->srv_domain, &sip_uri->host)==0) {
+ /* Match ! */
+ PJSUA_UNLOCK();
+ return acc_id;
+ }
+ }
+
+ /* No matching account, try match user part (and transport type) only. */
+ for (i=0; i < pjsua_var.acc_cnt; ++i) {
+ unsigned acc_id = pjsua_var.acc_ids[i];
+ pjsua_acc *acc = &pjsua_var.acc[acc_id];
+
+ if (acc->valid && pj_stricmp(&acc->user_part, &sip_uri->user)==0) {
+
+ if (acc->cfg.transport_id != PJSUA_INVALID_ID) {
+ pjsip_transport_type_e type;
+ type = pjsip_transport_get_type_from_name(&sip_uri->transport_param);
+ if (type == PJSIP_TRANSPORT_UNSPECIFIED)
+ type = PJSIP_TRANSPORT_UDP;
+
+ if (pjsua_var.tpdata[acc->cfg.transport_id].type != type)
+ continue;
+ }
+
+ /* Match ! */
+ PJSUA_UNLOCK();
+ return acc_id;
+ }
+ }
+
+ /* Still no match, use default account */
+ PJSUA_UNLOCK();
+ return pjsua_var.default_acc;
+}
+
+
+/*
+ * Create arbitrary requests for this account.
+ */
+PJ_DEF(pj_status_t) pjsua_acc_create_request(pjsua_acc_id acc_id,
+ const pjsip_method *method,
+ const pj_str_t *target,
+ pjsip_tx_data **p_tdata)
+{
+ pjsip_tx_data *tdata;
+ pjsua_acc *acc;
+ pjsip_route_hdr *r;
+ pj_status_t status;
+
+ PJ_ASSERT_RETURN(method && target && p_tdata, PJ_EINVAL);
+ PJ_ASSERT_RETURN(pjsua_acc_is_valid(acc_id), PJ_EINVAL);
+
+ acc = &pjsua_var.acc[acc_id];
+
+ status = pjsip_endpt_create_request(pjsua_var.endpt, method, target,
+ &acc->cfg.id, target,
+ NULL, NULL, -1, NULL, &tdata);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Unable to create request", status);
+ return status;
+ }
+
+ /* Copy routeset */
+ r = acc->route_set.next;
+ while (r != &acc->route_set) {
+ pjsip_msg_add_hdr(tdata->msg,
+ (pjsip_hdr*)pjsip_hdr_clone(tdata->pool, r));
+ r = r->next;
+ }
+
+ /* If account is locked to specific transport, then set that transport to
+ * the transmit data.
+ */
+ if (pjsua_var.acc[acc_id].cfg.transport_id != PJSUA_INVALID_ID) {
+ pjsip_tpselector tp_sel;
+
+ pjsua_init_tpselector(acc->cfg.transport_id, &tp_sel);
+ pjsip_tx_data_set_transport(tdata, &tp_sel);
+ }
+
+ /* If via_addr is set, use this address for the Via header. */
+ if (pjsua_var.acc[acc_id].cfg.allow_via_rewrite &&
+ pjsua_var.acc[acc_id].via_addr.host.slen > 0)
+ {
+ tdata->via_addr = pjsua_var.acc[acc_id].via_addr;
+ tdata->via_tp = pjsua_var.acc[acc_id].via_tp;
+ }
+
+ /* Done */
+ *p_tdata = tdata;
+ return PJ_SUCCESS;
+}
+
+
+PJ_DEF(pj_status_t) pjsua_acc_create_uac_contact( pj_pool_t *pool,
+ pj_str_t *contact,
+ pjsua_acc_id acc_id,
+ const pj_str_t *suri)
+{
+ pjsua_acc *acc;
+ pjsip_sip_uri *sip_uri;
+ pj_status_t status;
+ pjsip_transport_type_e tp_type = PJSIP_TRANSPORT_UNSPECIFIED;
+ pj_str_t local_addr;
+ pjsip_tpselector tp_sel;
+ unsigned flag;
+ int secure;
+ int local_port;
+ const char *beginquote, *endquote;
+ char transport_param[32];
+ const char *ob = ";ob";
+
+
+ PJ_ASSERT_RETURN(pjsua_acc_is_valid(acc_id), PJ_EINVAL);
+ acc = &pjsua_var.acc[acc_id];
+
+ /* If force_contact is configured, then use use it */
+ if (acc->cfg.force_contact.slen) {
+ *contact = acc->cfg.force_contact;
+ return PJ_SUCCESS;
+ }
+
+ /* If route-set is configured for the account, then URI is the
+ * first entry of the route-set.
+ */
+ if (!pj_list_empty(&acc->route_set)) {
+ sip_uri = (pjsip_sip_uri*)
+ pjsip_uri_get_uri(acc->route_set.next->name_addr.uri);
+ } else {
+ pj_str_t tmp;
+ pjsip_uri *uri;
+
+ pj_strdup_with_null(pool, &tmp, suri);
+
+ uri = pjsip_parse_uri(pool, tmp.ptr, tmp.slen, 0);
+ if (uri == NULL)
+ return PJSIP_EINVALIDURI;
+
+ /* For non-SIP scheme, route set should be configured */
+ if (!PJSIP_URI_SCHEME_IS_SIP(uri) && !PJSIP_URI_SCHEME_IS_SIPS(uri))
+ return PJSIP_ENOROUTESET;
+
+ sip_uri = (pjsip_sip_uri*)pjsip_uri_get_uri(uri);
+ }
+
+ /* Get transport type of the URI */
+ if (PJSIP_URI_SCHEME_IS_SIPS(sip_uri))
+ tp_type = PJSIP_TRANSPORT_TLS;
+ else if (sip_uri->transport_param.slen == 0) {
+ tp_type = PJSIP_TRANSPORT_UDP;
+ } else
+ tp_type = pjsip_transport_get_type_from_name(&sip_uri->transport_param);
+
+ if (tp_type == PJSIP_TRANSPORT_UNSPECIFIED)
+ return PJSIP_EUNSUPTRANSPORT;
+
+ /* If destination URI specifies IPv6, then set transport type
+ * to use IPv6 as well.
+ */
+ if (pj_strchr(&sip_uri->host, ':'))
+ tp_type = (pjsip_transport_type_e)(((int)tp_type) + PJSIP_TRANSPORT_IPV6);
+
+ flag = pjsip_transport_get_flag_from_type(tp_type);
+ secure = (flag & PJSIP_TRANSPORT_SECURE) != 0;
+
+ /* Init transport selector. */
+ pjsua_init_tpselector(pjsua_var.acc[acc_id].cfg.transport_id, &tp_sel);
+
+ /* Get local address suitable to send request from */
+ status = pjsip_tpmgr_find_local_addr(pjsip_endpt_get_tpmgr(pjsua_var.endpt),
+ pool, tp_type, &tp_sel,
+ &local_addr, &local_port);
+ if (status != PJ_SUCCESS)
+ return status;
+
+ /* Enclose IPv6 address in square brackets */
+ if (tp_type & PJSIP_TRANSPORT_IPV6) {
+ beginquote = "[";
+ endquote = "]";
+ } else {
+ beginquote = endquote = "";
+ }
+
+ /* Don't add transport parameter if it's UDP */
+ if (tp_type!=PJSIP_TRANSPORT_UDP && tp_type!=PJSIP_TRANSPORT_UDP6) {
+ pj_ansi_snprintf(transport_param, sizeof(transport_param),
+ ";transport=%s",
+ pjsip_transport_get_type_name(tp_type));
+ } else {
+ transport_param[0] = '\0';
+ }
+
+
+ /* Create the contact header */
+ contact->ptr = (char*)pj_pool_alloc(pool, PJSIP_MAX_URL_SIZE);
+ contact->slen = pj_ansi_snprintf(contact->ptr, PJSIP_MAX_URL_SIZE,
+ "%s%.*s%s<%s:%.*s%s%s%.*s%s:%d%s%.*s%s>%.*s",
+ (acc->display.slen?"\"" : ""),
+ (int)acc->display.slen,
+ acc->display.ptr,
+ (acc->display.slen?"\" " : ""),
+ (secure ? PJSUA_SECURE_SCHEME : "sip"),
+ (int)acc->user_part.slen,
+ acc->user_part.ptr,
+ (acc->user_part.slen?"@":""),
+ beginquote,
+ (int)local_addr.slen,
+ local_addr.ptr,
+ endquote,
+ local_port,
+ transport_param,
+ (int)acc->cfg.contact_uri_params.slen,
+ acc->cfg.contact_uri_params.ptr,
+ (acc->cfg.use_rfc5626? ob: ""),
+ (int)acc->cfg.contact_params.slen,
+ acc->cfg.contact_params.ptr);
+
+ return PJ_SUCCESS;
+}
+
+
+
+PJ_DEF(pj_status_t) pjsua_acc_create_uas_contact( pj_pool_t *pool,
+ pj_str_t *contact,
+ pjsua_acc_id acc_id,
+ pjsip_rx_data *rdata )
+{
+ /*
+ * Section 12.1.1, paragraph about using SIPS URI in Contact.
+ * If the request that initiated the dialog contained a SIPS URI
+ * in the Request-URI or in the top Record-Route header field value,
+ * if there was any, or the Contact header field if there was no
+ * Record-Route header field, the Contact header field in the response
+ * MUST be a SIPS URI.
+ */
+ pjsua_acc *acc;
+ pjsip_sip_uri *sip_uri;
+ pj_status_t status;
+ pjsip_transport_type_e tp_type = PJSIP_TRANSPORT_UNSPECIFIED;
+ pj_str_t local_addr;
+ pjsip_tpselector tp_sel;
+ unsigned flag;
+ int secure;
+ int local_port;
+ const char *beginquote, *endquote;
+ char transport_param[32];
+
+ PJ_ASSERT_RETURN(pjsua_acc_is_valid(acc_id), PJ_EINVAL);
+ acc = &pjsua_var.acc[acc_id];
+
+ /* If force_contact is configured, then use use it */
+ if (acc->cfg.force_contact.slen) {
+ *contact = acc->cfg.force_contact;
+ return PJ_SUCCESS;
+ }
+
+ /* If Record-Route is present, then URI is the top Record-Route. */
+ if (rdata->msg_info.record_route) {
+ sip_uri = (pjsip_sip_uri*)
+ pjsip_uri_get_uri(rdata->msg_info.record_route->name_addr.uri);
+ } else {
+ pjsip_hdr *pos = NULL;
+ pjsip_contact_hdr *h_contact;
+ pjsip_uri *uri = NULL;
+
+ /* Otherwise URI is Contact URI.
+ * Iterate the Contact URI until we find sip: or sips: scheme.
+ */
+ do {
+ h_contact = (pjsip_contact_hdr*)
+ pjsip_msg_find_hdr(rdata->msg_info.msg, PJSIP_H_CONTACT,
+ pos);
+ if (h_contact) {
+ if (h_contact->uri)
+ uri = (pjsip_uri*) pjsip_uri_get_uri(h_contact->uri);
+ else
+ uri = NULL;
+ if (!uri || (!PJSIP_URI_SCHEME_IS_SIP(uri) &&
+ !PJSIP_URI_SCHEME_IS_SIPS(uri)))
+ {
+ pos = (pjsip_hdr*)h_contact->next;
+ if (pos == &rdata->msg_info.msg->hdr)
+ h_contact = NULL;
+ } else {
+ break;
+ }
+ }
+ } while (h_contact);
+
+
+ /* Or if Contact URI is not present, take the remote URI from
+ * the From URI.
+ */
+ if (uri == NULL)
+ uri = (pjsip_uri*) pjsip_uri_get_uri(rdata->msg_info.from->uri);
+
+
+ /* Can only do sip/sips scheme at present. */
+ if (!PJSIP_URI_SCHEME_IS_SIP(uri) && !PJSIP_URI_SCHEME_IS_SIPS(uri))
+ return PJSIP_EINVALIDREQURI;
+
+ sip_uri = (pjsip_sip_uri*)pjsip_uri_get_uri(uri);
+ }
+
+ /* Get transport type of the URI */
+ if (PJSIP_URI_SCHEME_IS_SIPS(sip_uri))
+ tp_type = PJSIP_TRANSPORT_TLS;
+ else if (sip_uri->transport_param.slen == 0) {
+ tp_type = PJSIP_TRANSPORT_UDP;
+ } else
+ tp_type = pjsip_transport_get_type_from_name(&sip_uri->transport_param);
+
+ if (tp_type == PJSIP_TRANSPORT_UNSPECIFIED)
+ return PJSIP_EUNSUPTRANSPORT;
+
+ /* If destination URI specifies IPv6, then set transport type
+ * to use IPv6 as well.
+ */
+ if (pj_strchr(&sip_uri->host, ':'))
+ tp_type = (pjsip_transport_type_e)(((int)tp_type) + PJSIP_TRANSPORT_IPV6);
+
+ flag = pjsip_transport_get_flag_from_type(tp_type);
+ secure = (flag & PJSIP_TRANSPORT_SECURE) != 0;
+
+ /* Init transport selector. */
+ pjsua_init_tpselector(pjsua_var.acc[acc_id].cfg.transport_id, &tp_sel);
+
+ /* Get local address suitable to send request from */
+ status = pjsip_tpmgr_find_local_addr(pjsip_endpt_get_tpmgr(pjsua_var.endpt),
+ pool, tp_type, &tp_sel,
+ &local_addr, &local_port);
+ if (status != PJ_SUCCESS)
+ return status;
+
+ /* Enclose IPv6 address in square brackets */
+ if (tp_type & PJSIP_TRANSPORT_IPV6) {
+ beginquote = "[";
+ endquote = "]";
+ } else {
+ beginquote = endquote = "";
+ }
+
+ /* Don't add transport parameter if it's UDP */
+ if (tp_type!=PJSIP_TRANSPORT_UDP && tp_type!=PJSIP_TRANSPORT_UDP6) {
+ pj_ansi_snprintf(transport_param, sizeof(transport_param),
+ ";transport=%s",
+ pjsip_transport_get_type_name(tp_type));
+ } else {
+ transport_param[0] = '\0';
+ }
+
+
+ /* Create the contact header */
+ contact->ptr = (char*) pj_pool_alloc(pool, PJSIP_MAX_URL_SIZE);
+ contact->slen = pj_ansi_snprintf(contact->ptr, PJSIP_MAX_URL_SIZE,
+ "%s%.*s%s<%s:%.*s%s%s%.*s%s:%d%s%.*s>%.*s",
+ (acc->display.slen?"\"" : ""),
+ (int)acc->display.slen,
+ acc->display.ptr,
+ (acc->display.slen?"\" " : ""),
+ (secure ? PJSUA_SECURE_SCHEME : "sip"),
+ (int)acc->user_part.slen,
+ acc->user_part.ptr,
+ (acc->user_part.slen?"@":""),
+ beginquote,
+ (int)local_addr.slen,
+ local_addr.ptr,
+ endquote,
+ local_port,
+ transport_param,
+ (int)acc->cfg.contact_uri_params.slen,
+ acc->cfg.contact_uri_params.ptr,
+ (int)acc->cfg.contact_params.slen,
+ acc->cfg.contact_params.ptr);
+
+ return PJ_SUCCESS;
+}
+
+
+PJ_DEF(pj_status_t) pjsua_acc_set_transport( pjsua_acc_id acc_id,
+ pjsua_transport_id tp_id)
+{
+ pjsua_acc *acc;
+
+ PJ_ASSERT_RETURN(pjsua_acc_is_valid(acc_id), PJ_EINVAL);
+ acc = &pjsua_var.acc[acc_id];
+
+ PJ_ASSERT_RETURN(tp_id >= 0 && tp_id < (int)PJ_ARRAY_SIZE(pjsua_var.tpdata),
+ PJ_EINVAL);
+
+ acc->cfg.transport_id = tp_id;
+
+ return PJ_SUCCESS;
+}
+
+
+/* Auto re-registration timeout callback */
+static void auto_rereg_timer_cb(pj_timer_heap_t *th, pj_timer_entry *te)
+{
+ pjsua_acc *acc;
+ pj_status_t status;
+
+ PJ_UNUSED_ARG(th);
+ acc = (pjsua_acc*) te->user_data;
+ pj_assert(acc);
+
+ PJSUA_LOCK();
+
+ /* Check if the reregistration timer is still valid, e.g: while waiting
+ * timeout timer application might have deleted the account or disabled
+ * the auto-reregistration.
+ */
+ if (!acc->valid || !acc->auto_rereg.active ||
+ acc->cfg.reg_retry_interval == 0)
+ {
+ goto on_return;
+ }
+
+ /* Start re-registration */
+ acc->auto_rereg.attempt_cnt++;
+ status = pjsua_acc_set_registration(acc->index, PJ_TRUE);
+ if (status != PJ_SUCCESS)
+ schedule_reregistration(acc);
+
+on_return:
+ PJSUA_UNLOCK();
+}
+
+
+/* Schedule reregistration for specified account. Note that the first
+ * re-registration after a registration failure will be done immediately.
+ * Also note that this function should be called within PJSUA mutex.
+ */
+static void schedule_reregistration(pjsua_acc *acc)
+{
+ pj_time_val delay;
+
+ pj_assert(acc);
+
+ /* Validate the account and re-registration feature status */
+ if (!acc->valid || acc->cfg.reg_retry_interval == 0) {
+ return;
+ }
+
+ /* If configured, disconnect calls of this account after the first
+ * reregistration attempt failed.
+ */
+ if (acc->cfg.drop_calls_on_reg_fail && acc->auto_rereg.attempt_cnt >= 1)
+ {
+ unsigned i, cnt;
+
+ for (i = 0, cnt = 0; i < pjsua_var.ua_cfg.max_calls; ++i) {
+ if (pjsua_var.calls[i].acc_id == acc->index) {
+ pjsua_call_hangup(i, 0, NULL, NULL);
+ ++cnt;
+ }
+ }
+
+ if (cnt) {
+ PJ_LOG(3, (THIS_FILE, "Disconnecting %d call(s) of account #%d "
+ "after reregistration attempt failed",
+ cnt, acc->index));
+ }
+ }
+
+ /* Cancel any re-registration timer */
+ if (acc->auto_rereg.timer.id) {
+ acc->auto_rereg.timer.id = PJ_FALSE;
+ pjsua_cancel_timer(&acc->auto_rereg.timer);
+ }
+
+ /* Update re-registration flag */
+ acc->auto_rereg.active = PJ_TRUE;
+
+ /* Set up timer for reregistration */
+ acc->auto_rereg.timer.cb = &auto_rereg_timer_cb;
+ acc->auto_rereg.timer.user_data = acc;
+
+ /* Reregistration attempt. The first attempt will be done immediately. */
+ delay.sec = acc->auto_rereg.attempt_cnt? acc->cfg.reg_retry_interval :
+ acc->cfg.reg_first_retry_interval;
+ delay.msec = 0;
+
+ /* Randomize interval by +/- 10 secs */
+ if (delay.sec >= 10) {
+ delay.msec = -10000 + (pj_rand() % 20000);
+ } else {
+ delay.sec = 0;
+ delay.msec = (pj_rand() % 10000);
+ }
+ pj_time_val_normalize(&delay);
+
+ PJ_LOG(4,(THIS_FILE,
+ "Scheduling re-registration retry for acc %d in %u seconds..",
+ acc->index, delay.sec));
+
+ acc->auto_rereg.timer.id = PJ_TRUE;
+ if (pjsua_schedule_timer(&acc->auto_rereg.timer, &delay) != PJ_SUCCESS)
+ acc->auto_rereg.timer.id = PJ_FALSE;
+}
+
+
+/* Internal function to perform auto-reregistration on transport
+ * connection/disconnection events.
+ */
+void pjsua_acc_on_tp_state_changed(pjsip_transport *tp,
+ pjsip_transport_state state,
+ const pjsip_transport_state_info *info)
+{
+ unsigned i;
+
+ PJ_UNUSED_ARG(info);
+
+ /* Only care for transport disconnection events */
+ if (state != PJSIP_TP_STATE_DISCONNECTED)
+ return;
+
+ PJ_LOG(4,(THIS_FILE, "Disconnected notification for transport %s",
+ tp->obj_name));
+ pj_log_push_indent();
+
+ /* Shutdown this transport, to make sure that the transport manager
+ * will create a new transport for reconnection.
+ */
+ pjsip_transport_shutdown(tp);
+
+ PJSUA_LOCK();
+
+ /* Enumerate accounts using this transport and perform actions
+ * based on the transport state.
+ */
+ for (i = 0; i < PJ_ARRAY_SIZE(pjsua_var.acc); ++i) {
+ pjsua_acc *acc = &pjsua_var.acc[i];
+
+ /* Skip if this account is not valid OR auto re-registration
+ * feature is disabled OR this transport is not used by this account.
+ */
+ if (!acc->valid || !acc->cfg.reg_retry_interval ||
+ tp != acc->auto_rereg.reg_tp)
+ {
+ continue;
+ }
+
+ /* Release regc transport immediately
+ * See https://trac.pjsip.org/repos/ticket/1481
+ */
+ if (pjsua_var.acc[i].regc) {
+ pjsip_regc_release_transport(pjsua_var.acc[i].regc);
+ }
+
+ /* Schedule reregistration for this account */
+ schedule_reregistration(acc);
+ }
+
+ PJSUA_UNLOCK();
+ pj_log_pop_indent();
+}
diff --git a/pjsip/src/pjsua-lib/pjsua_aud.c b/pjsip/src/pjsua-lib/pjsua_aud.c
new file mode 100644
index 0000000..557a147
--- /dev/null
+++ b/pjsip/src/pjsua-lib/pjsua_aud.c
@@ -0,0 +1,2148 @@
+/* $Id: pjsua_aud.c 4145 2012-05-22 23:13:22Z bennylp $ */
+/*
+ * Copyright (C) 2008-2011 Teluu Inc. (http://www.teluu.com)
+ * Copyright (C) 2003-2008 Benny Prijono <benny@prijono.org>
+ *
+ * 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
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ */
+#include <pjsua-lib/pjsua.h>
+#include <pjsua-lib/pjsua_internal.h>
+
+#if defined(PJSUA_MEDIA_HAS_PJMEDIA) && PJSUA_MEDIA_HAS_PJMEDIA != 0
+
+#define THIS_FILE "pjsua_aud.c"
+#define NULL_SND_DEV_ID -99
+
+/*****************************************************************************
+ *
+ * Prototypes
+ */
+/* Open sound dev */
+static pj_status_t open_snd_dev(pjmedia_snd_port_param *param);
+/* Close existing sound device */
+static void close_snd_dev(void);
+/* Create audio device param */
+static pj_status_t create_aud_param(pjmedia_aud_param *param,
+ pjmedia_aud_dev_index capture_dev,
+ pjmedia_aud_dev_index playback_dev,
+ unsigned clock_rate,
+ unsigned channel_count,
+ unsigned samples_per_frame,
+ unsigned bits_per_sample);
+
+/*****************************************************************************
+ *
+ * Call API that are closely tied to PJMEDIA
+ */
+/*
+ * Check if call has an active media session.
+ */
+PJ_DEF(pj_bool_t) pjsua_call_has_media(pjsua_call_id call_id)
+{
+ pjsua_call *call = &pjsua_var.calls[call_id];
+ PJ_ASSERT_RETURN(call_id>=0 && call_id<(int)pjsua_var.ua_cfg.max_calls,
+ PJ_EINVAL);
+ return call->audio_idx >= 0 && call->media[call->audio_idx].strm.a.stream;
+}
+
+
+/*
+ * Get the conference port identification associated with the call.
+ */
+PJ_DEF(pjsua_conf_port_id) pjsua_call_get_conf_port(pjsua_call_id call_id)
+{
+ pjsua_call *call;
+ pjsua_conf_port_id port_id = PJSUA_INVALID_ID;
+
+ PJ_ASSERT_RETURN(call_id>=0 && call_id<(int)pjsua_var.ua_cfg.max_calls,
+ PJ_EINVAL);
+
+ /* Use PJSUA_LOCK() instead of acquire_call():
+ * https://trac.pjsip.org/repos/ticket/1371
+ */
+ PJSUA_LOCK();
+
+ if (!pjsua_call_is_active(call_id))
+ goto on_return;
+
+ call = &pjsua_var.calls[call_id];
+ port_id = call->media[call->audio_idx].strm.a.conf_slot;
+
+on_return:
+ PJSUA_UNLOCK();
+
+ return port_id;
+}
+
+
+/*
+ * Get media stream info for the specified media index.
+ */
+PJ_DEF(pj_status_t) pjsua_call_get_stream_info( pjsua_call_id call_id,
+ unsigned med_idx,
+ pjsua_stream_info *psi)
+{
+ pjsua_call *call;
+ pjsua_call_media *call_med;
+ pj_status_t status;
+
+ PJ_ASSERT_RETURN(call_id>=0 && call_id<(int)pjsua_var.ua_cfg.max_calls,
+ PJ_EINVAL);
+ PJ_ASSERT_RETURN(psi, PJ_EINVAL);
+
+ PJSUA_LOCK();
+
+ call = &pjsua_var.calls[call_id];
+
+ if (med_idx >= call->med_cnt) {
+ PJSUA_UNLOCK();
+ return PJ_EINVAL;
+ }
+
+ call_med = &call->media[med_idx];
+ psi->type = call_med->type;
+ switch (call_med->type) {
+ case PJMEDIA_TYPE_AUDIO:
+ status = pjmedia_stream_get_info(call_med->strm.a.stream,
+ &psi->info.aud);
+ break;
+#if defined(PJMEDIA_HAS_VIDEO) && (PJMEDIA_HAS_VIDEO != 0)
+ case PJMEDIA_TYPE_VIDEO:
+ status = pjmedia_vid_stream_get_info(call_med->strm.v.stream,
+ &psi->info.vid);
+ break;
+#endif
+ default:
+ status = PJMEDIA_EINVALIMEDIATYPE;
+ break;
+ }
+
+ PJSUA_UNLOCK();
+ return status;
+}
+
+
+/*
+ * Get media stream statistic for the specified media index.
+ */
+PJ_DEF(pj_status_t) pjsua_call_get_stream_stat( pjsua_call_id call_id,
+ unsigned med_idx,
+ pjsua_stream_stat *stat)
+{
+ pjsua_call *call;
+ pjsua_call_media *call_med;
+ pj_status_t status;
+
+ PJ_ASSERT_RETURN(call_id>=0 && call_id<(int)pjsua_var.ua_cfg.max_calls,
+ PJ_EINVAL);
+ PJ_ASSERT_RETURN(stat, PJ_EINVAL);
+
+ PJSUA_LOCK();
+
+ call = &pjsua_var.calls[call_id];
+
+ if (med_idx >= call->med_cnt) {
+ PJSUA_UNLOCK();
+ return PJ_EINVAL;
+ }
+
+ call_med = &call->media[med_idx];
+ switch (call_med->type) {
+ case PJMEDIA_TYPE_AUDIO:
+ status = pjmedia_stream_get_stat(call_med->strm.a.stream,
+ &stat->rtcp);
+ if (status == PJ_SUCCESS)
+ status = pjmedia_stream_get_stat_jbuf(call_med->strm.a.stream,
+ &stat->jbuf);
+ break;
+#if defined(PJMEDIA_HAS_VIDEO) && (PJMEDIA_HAS_VIDEO != 0)
+ case PJMEDIA_TYPE_VIDEO:
+ status = pjmedia_vid_stream_get_stat(call_med->strm.v.stream,
+ &stat->rtcp);
+ if (status == PJ_SUCCESS)
+ status = pjmedia_vid_stream_get_stat_jbuf(call_med->strm.v.stream,
+ &stat->jbuf);
+ break;
+#endif
+ default:
+ status = PJMEDIA_EINVALIMEDIATYPE;
+ break;
+ }
+
+ PJSUA_UNLOCK();
+ return status;
+}
+
+/*
+ * Send DTMF digits to remote using RFC 2833 payload formats.
+ */
+PJ_DEF(pj_status_t) pjsua_call_dial_dtmf( pjsua_call_id call_id,
+ const pj_str_t *digits)
+{
+ pjsua_call *call;
+ pjsip_dialog *dlg = 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, "Call %d dialing DTMF %.*s",
+ call_id, (int)digits->slen, digits->ptr));
+ pj_log_push_indent();
+
+ status = acquire_call("pjsua_call_dial_dtmf()", call_id, &call, &dlg);
+ if (status != PJ_SUCCESS)
+ goto on_return;
+
+ if (!pjsua_call_has_media(call_id)) {
+ PJ_LOG(3,(THIS_FILE, "Media is not established yet!"));
+ status = PJ_EINVALIDOP;
+ goto on_return;
+ }
+
+ status = pjmedia_stream_dial_dtmf(
+ call->media[call->audio_idx].strm.a.stream, digits);
+
+on_return:
+ if (dlg) pjsip_dlg_dec_lock(dlg);
+ pj_log_pop_indent();
+ return status;
+}
+
+
+/*****************************************************************************
+ *
+ * Audio media with PJMEDIA backend
+ */
+
+/* Init pjmedia audio subsystem */
+pj_status_t pjsua_aud_subsys_init()
+{
+ pj_str_t codec_id = {NULL, 0};
+ unsigned opt;
+ pjmedia_audio_codec_config codec_cfg;
+ pj_status_t status;
+
+ /* To suppress warning about unused var when all codecs are disabled */
+ PJ_UNUSED_ARG(codec_id);
+
+ /*
+ * Register all codecs
+ */
+ pjmedia_audio_codec_config_default(&codec_cfg);
+ codec_cfg.speex.quality = pjsua_var.media_cfg.quality;
+ codec_cfg.speex.complexity = -1;
+ codec_cfg.ilbc.mode = pjsua_var.media_cfg.ilbc_mode;
+
+#if PJMEDIA_HAS_PASSTHROUGH_CODECS
+ /* Register passthrough codecs */
+ {
+ unsigned aud_idx;
+ unsigned ext_fmt_cnt = 0;
+ pjmedia_format ext_fmts[32];
+
+ /* List extended formats supported by audio devices */
+ for (aud_idx = 0; aud_idx < pjmedia_aud_dev_count(); ++aud_idx) {
+ pjmedia_aud_dev_info aud_info;
+ unsigned i;
+
+ status = pjmedia_aud_dev_get_info(aud_idx, &aud_info);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Error querying audio device info",
+ status);
+ goto on_error;
+ }
+
+ /* Collect extended formats supported by this audio device */
+ for (i = 0; i < aud_info.ext_fmt_cnt; ++i) {
+ unsigned j;
+ pj_bool_t is_listed = PJ_FALSE;
+
+ /* See if this extended format is already in the list */
+ for (j = 0; j < ext_fmt_cnt && !is_listed; ++j) {
+ if (ext_fmts[j].id == aud_info.ext_fmt[i].id &&
+ ext_fmts[j].det.aud.avg_bps ==
+ aud_info.ext_fmt[i].det.aud.avg_bps)
+ {
+ is_listed = PJ_TRUE;
+ }
+ }
+
+ /* Put this format into the list, if it is not in the list */
+ if (!is_listed)
+ ext_fmts[ext_fmt_cnt++] = aud_info.ext_fmt[i];
+
+ pj_assert(ext_fmt_cnt <= PJ_ARRAY_SIZE(ext_fmts));
+ }
+ }
+
+ /* Init the passthrough codec with supported formats only */
+ codec_cfg.passthrough.setting.fmt_cnt = ext_fmt_cnt;
+ codec_cfg.passthrough.setting.fmts = ext_fmts;
+ codec_cfg.passthrough.setting.ilbc_mode = cfg->ilbc_mode;
+ }
+#endif /* PJMEDIA_HAS_PASSTHROUGH_CODECS */
+
+ /* Register all codecs */
+ status = pjmedia_codec_register_audio_codecs(pjsua_var.med_endpt,
+ &codec_cfg);
+ if (status != PJ_SUCCESS) {
+ PJ_PERROR(1,(THIS_FILE, status, "Error registering codecs"));
+ goto on_error;
+ }
+
+ /* Set speex/16000 to higher priority*/
+ codec_id = pj_str("speex/16000");
+ pjmedia_codec_mgr_set_codec_priority(
+ pjmedia_endpt_get_codec_mgr(pjsua_var.med_endpt),
+ &codec_id, PJMEDIA_CODEC_PRIO_NORMAL+2);
+
+ /* Set speex/8000 to next higher priority*/
+ codec_id = pj_str("speex/8000");
+ pjmedia_codec_mgr_set_codec_priority(
+ pjmedia_endpt_get_codec_mgr(pjsua_var.med_endpt),
+ &codec_id, PJMEDIA_CODEC_PRIO_NORMAL+1);
+
+ /* Disable ALL L16 codecs */
+ codec_id = pj_str("L16");
+ pjmedia_codec_mgr_set_codec_priority(
+ pjmedia_endpt_get_codec_mgr(pjsua_var.med_endpt),
+ &codec_id, PJMEDIA_CODEC_PRIO_DISABLED);
+
+
+ /* Save additional conference bridge parameters for future
+ * reference.
+ */
+ pjsua_var.mconf_cfg.channel_count = pjsua_var.media_cfg.channel_count;
+ pjsua_var.mconf_cfg.bits_per_sample = 16;
+ pjsua_var.mconf_cfg.samples_per_frame = pjsua_var.media_cfg.clock_rate *
+ pjsua_var.mconf_cfg.channel_count *
+ pjsua_var.media_cfg.audio_frame_ptime /
+ 1000;
+
+ /* Init options for conference bridge. */
+ opt = PJMEDIA_CONF_NO_DEVICE;
+ if (pjsua_var.media_cfg.quality >= 3 &&
+ pjsua_var.media_cfg.quality <= 4)
+ {
+ opt |= PJMEDIA_CONF_SMALL_FILTER;
+ }
+ else if (pjsua_var.media_cfg.quality < 3) {
+ opt |= PJMEDIA_CONF_USE_LINEAR;
+ }
+
+ /* Init conference bridge. */
+ status = pjmedia_conf_create(pjsua_var.pool,
+ pjsua_var.media_cfg.max_media_ports,
+ pjsua_var.media_cfg.clock_rate,
+ pjsua_var.mconf_cfg.channel_count,
+ pjsua_var.mconf_cfg.samples_per_frame,
+ pjsua_var.mconf_cfg.bits_per_sample,
+ opt, &pjsua_var.mconf);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Error creating conference bridge",
+ status);
+ goto on_error;
+ }
+
+ /* Are we using the audio switchboard (a.k.a APS-Direct)? */
+ pjsua_var.is_mswitch = pjmedia_conf_get_master_port(pjsua_var.mconf)
+ ->info.signature == PJMEDIA_CONF_SWITCH_SIGNATURE;
+
+ /* Create null port just in case user wants to use null sound. */
+ status = pjmedia_null_port_create(pjsua_var.pool,
+ pjsua_var.media_cfg.clock_rate,
+ pjsua_var.mconf_cfg.channel_count,
+ pjsua_var.mconf_cfg.samples_per_frame,
+ pjsua_var.mconf_cfg.bits_per_sample,
+ &pjsua_var.null_port);
+ PJ_ASSERT_RETURN(status == PJ_SUCCESS, status);
+
+ return status;
+
+on_error:
+ return status;
+}
+
+/* Check if sound device is idle. */
+void pjsua_check_snd_dev_idle()
+{
+ unsigned call_cnt;
+
+ /* Check if the sound device auto-close feature is disabled. */
+ if (pjsua_var.media_cfg.snd_auto_close_time < 0)
+ return;
+
+ /* Check if the sound device is currently closed. */
+ if (!pjsua_var.snd_is_on)
+ return;
+
+ /* Get the call count, we shouldn't close the sound device when there is
+ * any calls active.
+ */
+ call_cnt = pjsua_call_get_count();
+
+ /* When this function is called from pjsua_media_channel_deinit() upon
+ * disconnecting call, actually the call count hasn't been updated/
+ * decreased. So we put additional check here, if there is only one
+ * call and it's in DISCONNECTED state, there is actually no active
+ * call.
+ */
+ if (call_cnt == 1) {
+ pjsua_call_id call_id;
+ pj_status_t status;
+
+ status = pjsua_enum_calls(&call_id, &call_cnt);
+ if (status == PJ_SUCCESS && call_cnt > 0 &&
+ !pjsua_call_is_active(call_id))
+ {
+ call_cnt = 0;
+ }
+ }
+
+ /* Activate sound device auto-close timer if sound device is idle.
+ * It is idle when there is no port connection in the bridge and
+ * there is no active call.
+ */
+ if (pjsua_var.snd_idle_timer.id == PJ_FALSE &&
+ call_cnt == 0 &&
+ pjmedia_conf_get_connect_count(pjsua_var.mconf) == 0)
+ {
+ pj_time_val delay;
+
+ delay.msec = 0;
+ delay.sec = pjsua_var.media_cfg.snd_auto_close_time;
+
+ pjsua_var.snd_idle_timer.id = PJ_TRUE;
+ pjsip_endpt_schedule_timer(pjsua_var.endpt, &pjsua_var.snd_idle_timer,
+ &delay);
+ }
+}
+
+/* Timer callback to close sound device */
+static void close_snd_timer_cb( pj_timer_heap_t *th,
+ pj_timer_entry *entry)
+{
+ PJ_UNUSED_ARG(th);
+
+ PJSUA_LOCK();
+ if (entry->id) {
+ PJ_LOG(4,(THIS_FILE,"Closing sound device after idle for %d second(s)",
+ pjsua_var.media_cfg.snd_auto_close_time));
+
+ entry->id = PJ_FALSE;
+
+ close_snd_dev();
+ }
+ PJSUA_UNLOCK();
+}
+
+pj_status_t pjsua_aud_subsys_start(void)
+{
+ pj_status_t status = PJ_SUCCESS;
+
+ pj_timer_entry_init(&pjsua_var.snd_idle_timer, PJ_FALSE, NULL,
+ &close_snd_timer_cb);
+
+ return status;
+}
+
+pj_status_t pjsua_aud_subsys_destroy()
+{
+ unsigned i;
+
+ close_snd_dev();
+
+ if (pjsua_var.mconf) {
+ pjmedia_conf_destroy(pjsua_var.mconf);
+ pjsua_var.mconf = NULL;
+ }
+
+ if (pjsua_var.null_port) {
+ pjmedia_port_destroy(pjsua_var.null_port);
+ pjsua_var.null_port = NULL;
+ }
+
+ /* Destroy file players */
+ for (i=0; i<PJ_ARRAY_SIZE(pjsua_var.player); ++i) {
+ if (pjsua_var.player[i].port) {
+ pjmedia_port_destroy(pjsua_var.player[i].port);
+ pjsua_var.player[i].port = NULL;
+ }
+ }
+
+ /* Destroy file recorders */
+ for (i=0; i<PJ_ARRAY_SIZE(pjsua_var.recorder); ++i) {
+ if (pjsua_var.recorder[i].port) {
+ pjmedia_port_destroy(pjsua_var.recorder[i].port);
+ pjsua_var.recorder[i].port = NULL;
+ }
+ }
+
+ return PJ_SUCCESS;
+}
+
+void pjsua_aud_stop_stream(pjsua_call_media *call_med)
+{
+ pjmedia_stream *strm = call_med->strm.a.stream;
+ pjmedia_rtcp_stat stat;
+
+ if (strm) {
+ pjmedia_stream_send_rtcp_bye(strm);
+
+ if (call_med->strm.a.conf_slot != PJSUA_INVALID_ID) {
+ if (pjsua_var.mconf) {
+ pjsua_conf_remove_port(call_med->strm.a.conf_slot);
+ }
+ call_med->strm.a.conf_slot = PJSUA_INVALID_ID;
+ }
+
+ if ((call_med->dir & PJMEDIA_DIR_ENCODING) &&
+ (pjmedia_stream_get_stat(strm, &stat) == PJ_SUCCESS))
+ {
+ /* Save RTP timestamp & sequence, so when media session is
+ * restarted, those values will be restored as the initial
+ * RTP timestamp & sequence of the new media session. So in
+ * the same call session, RTP timestamp and sequence are
+ * guaranteed to be contigue.
+ */
+ call_med->rtp_tx_seq_ts_set = 1 | (1 << 1);
+ call_med->rtp_tx_seq = stat.rtp_tx_last_seq;
+ call_med->rtp_tx_ts = stat.rtp_tx_last_ts;
+ }
+
+ if (pjsua_var.ua_cfg.cb.on_stream_destroyed) {
+ pjsua_var.ua_cfg.cb.on_stream_destroyed(call_med->call->index,
+ strm, call_med->idx);
+ }
+
+ pjmedia_stream_destroy(strm);
+ call_med->strm.a.stream = NULL;
+ }
+
+ pjsua_check_snd_dev_idle();
+}
+
+/*
+ * DTMF callback from the stream.
+ */
+static void dtmf_callback(pjmedia_stream *strm, void *user_data,
+ int digit)
+{
+ PJ_UNUSED_ARG(strm);
+
+ pj_log_push_indent();
+
+ /* For discussions about call mutex protection related to this
+ * callback, please see ticket #460:
+ * http://trac.pjsip.org/repos/ticket/460#comment:4
+ */
+ if (pjsua_var.ua_cfg.cb.on_dtmf_digit) {
+ pjsua_call_id call_id;
+
+ call_id = (pjsua_call_id)(long)user_data;
+ pjsua_var.ua_cfg.cb.on_dtmf_digit(call_id, digit);
+ }
+
+ pj_log_pop_indent();
+}
+
+
+pj_status_t pjsua_aud_channel_update(pjsua_call_media *call_med,
+ pj_pool_t *tmp_pool,
+ pjmedia_stream_info *si,
+ const pjmedia_sdp_session *local_sdp,
+ const pjmedia_sdp_session *remote_sdp)
+{
+ pjsua_call *call = call_med->call;
+ pjmedia_port *media_port;
+ unsigned strm_idx = call_med->idx;
+ pj_status_t status = PJ_SUCCESS;
+
+ PJ_UNUSED_ARG(tmp_pool);
+ PJ_UNUSED_ARG(local_sdp);
+ PJ_UNUSED_ARG(remote_sdp);
+
+ PJ_LOG(4,(THIS_FILE,"Audio channel update.."));
+ pj_log_push_indent();
+
+ si->rtcp_sdes_bye_disabled = PJ_TRUE;
+
+ /* Check if no media is active */
+ if (si->dir != PJMEDIA_DIR_NONE) {
+
+ /* Override ptime, if this option is specified. */
+ if (pjsua_var.media_cfg.ptime != 0) {
+ si->param->setting.frm_per_pkt = (pj_uint8_t)
+ (pjsua_var.media_cfg.ptime / si->param->info.frm_ptime);
+ if (si->param->setting.frm_per_pkt == 0)
+ si->param->setting.frm_per_pkt = 1;
+ }
+
+ /* Disable VAD, if this option is specified. */
+ if (pjsua_var.media_cfg.no_vad) {
+ si->param->setting.vad = 0;
+ }
+
+
+ /* Optionally, application may modify other stream settings here
+ * (such as jitter buffer parameters, codec ptime, etc.)
+ */
+ si->jb_init = pjsua_var.media_cfg.jb_init;
+ si->jb_min_pre = pjsua_var.media_cfg.jb_min_pre;
+ si->jb_max_pre = pjsua_var.media_cfg.jb_max_pre;
+ si->jb_max = pjsua_var.media_cfg.jb_max;
+
+ /* Set SSRC */
+ si->ssrc = call_med->ssrc;
+
+ /* Set RTP timestamp & sequence, normally these value are intialized
+ * automatically when stream session created, but for some cases (e.g:
+ * call reinvite, call update) timestamp and sequence need to be kept
+ * contigue.
+ */
+ si->rtp_ts = call_med->rtp_tx_ts;
+ si->rtp_seq = call_med->rtp_tx_seq;
+ si->rtp_seq_ts_set = call_med->rtp_tx_seq_ts_set;
+
+#if defined(PJMEDIA_STREAM_ENABLE_KA) && PJMEDIA_STREAM_ENABLE_KA!=0
+ /* Enable/disable stream keep-alive and NAT hole punch. */
+ si->use_ka = pjsua_var.acc[call->acc_id].cfg.use_stream_ka;
+#endif
+
+ /* Create session based on session info. */
+ status = pjmedia_stream_create(pjsua_var.med_endpt, NULL, si,
+ call_med->tp, NULL,
+ &call_med->strm.a.stream);
+ if (status != PJ_SUCCESS) {
+ goto on_return;
+ }
+
+ /* Start stream */
+ status = pjmedia_stream_start(call_med->strm.a.stream);
+ if (status != PJ_SUCCESS) {
+ goto on_return;
+ }
+
+ if (call_med->prev_state == PJSUA_CALL_MEDIA_NONE)
+ pjmedia_stream_send_rtcp_sdes(call_med->strm.a.stream);
+
+ /* If DTMF callback is installed by application, install our
+ * callback to the session.
+ */
+ if (pjsua_var.ua_cfg.cb.on_dtmf_digit) {
+ pjmedia_stream_set_dtmf_callback(call_med->strm.a.stream,
+ &dtmf_callback,
+ (void*)(long)(call->index));
+ }
+
+ /* Get the port interface of the first stream in the session.
+ * We need the port interface to add to the conference bridge.
+ */
+ pjmedia_stream_get_port(call_med->strm.a.stream, &media_port);
+
+ /* Notify application about stream creation.
+ * Note: application may modify media_port to point to different
+ * media port
+ */
+ if (pjsua_var.ua_cfg.cb.on_stream_created) {
+ pjsua_var.ua_cfg.cb.on_stream_created(call->index,
+ call_med->strm.a.stream,
+ strm_idx, &media_port);
+ }
+
+ /*
+ * Add the call to conference bridge.
+ */
+ {
+ char tmp[PJSIP_MAX_URL_SIZE];
+ pj_str_t port_name;
+
+ port_name.ptr = tmp;
+ port_name.slen = pjsip_uri_print(PJSIP_URI_IN_REQ_URI,
+ call->inv->dlg->remote.info->uri,
+ tmp, sizeof(tmp));
+ if (port_name.slen < 1) {
+ port_name = pj_str("call");
+ }
+ status = pjmedia_conf_add_port( pjsua_var.mconf,
+ call->inv->pool_prov,
+ media_port,
+ &port_name,
+ (unsigned*)
+ &call_med->strm.a.conf_slot);
+ if (status != PJ_SUCCESS) {
+ goto on_return;
+ }
+ }
+ }
+
+on_return:
+ pj_log_pop_indent();
+ return status;
+}
+
+
+/*
+ * Get maxinum number of conference ports.
+ */
+PJ_DEF(unsigned) pjsua_conf_get_max_ports(void)
+{
+ return pjsua_var.media_cfg.max_media_ports;
+}
+
+
+/*
+ * Get current number of active ports in the bridge.
+ */
+PJ_DEF(unsigned) pjsua_conf_get_active_ports(void)
+{
+ unsigned ports[PJSUA_MAX_CONF_PORTS];
+ unsigned count = PJ_ARRAY_SIZE(ports);
+ pj_status_t status;
+
+ status = pjmedia_conf_enum_ports(pjsua_var.mconf, ports, &count);
+ if (status != PJ_SUCCESS)
+ count = 0;
+
+ return count;
+}
+
+
+/*
+ * Enumerate all conference ports.
+ */
+PJ_DEF(pj_status_t) pjsua_enum_conf_ports(pjsua_conf_port_id id[],
+ unsigned *count)
+{
+ return pjmedia_conf_enum_ports(pjsua_var.mconf, (unsigned*)id, count);
+}
+
+
+/*
+ * Get information about the specified conference port
+ */
+PJ_DEF(pj_status_t) pjsua_conf_get_port_info( pjsua_conf_port_id id,
+ pjsua_conf_port_info *info)
+{
+ pjmedia_conf_port_info cinfo;
+ unsigned i;
+ pj_status_t status;
+
+ status = pjmedia_conf_get_port_info( pjsua_var.mconf, id, &cinfo);
+ if (status != PJ_SUCCESS)
+ return status;
+
+ pj_bzero(info, sizeof(*info));
+ info->slot_id = id;
+ info->name = cinfo.name;
+ info->clock_rate = cinfo.clock_rate;
+ info->channel_count = cinfo.channel_count;
+ info->samples_per_frame = cinfo.samples_per_frame;
+ info->bits_per_sample = cinfo.bits_per_sample;
+
+ /* Build array of listeners */
+ info->listener_cnt = cinfo.listener_cnt;
+ for (i=0; i<cinfo.listener_cnt; ++i) {
+ info->listeners[i] = cinfo.listener_slots[i];
+ }
+
+ return PJ_SUCCESS;
+}
+
+
+/*
+ * Add arbitrary media port to PJSUA's conference bridge.
+ */
+PJ_DEF(pj_status_t) pjsua_conf_add_port( pj_pool_t *pool,
+ pjmedia_port *port,
+ pjsua_conf_port_id *p_id)
+{
+ pj_status_t status;
+
+ status = pjmedia_conf_add_port(pjsua_var.mconf, pool,
+ port, NULL, (unsigned*)p_id);
+ if (status != PJ_SUCCESS) {
+ if (p_id)
+ *p_id = PJSUA_INVALID_ID;
+ }
+
+ return status;
+}
+
+
+/*
+ * Remove arbitrary slot from the conference bridge.
+ */
+PJ_DEF(pj_status_t) pjsua_conf_remove_port(pjsua_conf_port_id id)
+{
+ pj_status_t status;
+
+ status = pjmedia_conf_remove_port(pjsua_var.mconf, (unsigned)id);
+ pjsua_check_snd_dev_idle();
+
+ return status;
+}
+
+
+/*
+ * Establish unidirectional media flow from souce to sink.
+ */
+PJ_DEF(pj_status_t) pjsua_conf_connect( pjsua_conf_port_id source,
+ pjsua_conf_port_id sink)
+{
+ pj_status_t status = PJ_SUCCESS;
+
+ PJ_LOG(4,(THIS_FILE, "%s connect: %d --> %d",
+ (pjsua_var.is_mswitch ? "Switch" : "Conf"),
+ source, sink));
+ pj_log_push_indent();
+
+ PJSUA_LOCK();
+
+ /* If sound device idle timer is active, cancel it first. */
+ if (pjsua_var.snd_idle_timer.id) {
+ pjsip_endpt_cancel_timer(pjsua_var.endpt, &pjsua_var.snd_idle_timer);
+ pjsua_var.snd_idle_timer.id = PJ_FALSE;
+ }
+
+
+ /* For audio switchboard (i.e. APS-Direct):
+ * Check if sound device need to be reopened, i.e: its attributes
+ * (format, clock rate, channel count) must match to peer's.
+ * Note that sound device can be reopened only if it doesn't have
+ * any connection.
+ */
+ if (pjsua_var.is_mswitch) {
+ pjmedia_conf_port_info port0_info;
+ pjmedia_conf_port_info peer_info;
+ unsigned peer_id;
+ pj_bool_t need_reopen = PJ_FALSE;
+
+ peer_id = (source!=0)? source : sink;
+ status = pjmedia_conf_get_port_info(pjsua_var.mconf, peer_id,
+ &peer_info);
+ pj_assert(status == PJ_SUCCESS);
+
+ status = pjmedia_conf_get_port_info(pjsua_var.mconf, 0, &port0_info);
+ pj_assert(status == PJ_SUCCESS);
+
+ /* Check if sound device is instantiated. */
+ need_reopen = (pjsua_var.snd_port==NULL && pjsua_var.null_snd==NULL &&
+ !pjsua_var.no_snd);
+
+ /* Check if sound device need to reopen because it needs to modify
+ * settings to match its peer. Sound device must be idle in this case
+ * though.
+ */
+ if (!need_reopen &&
+ port0_info.listener_cnt==0 && port0_info.transmitter_cnt==0)
+ {
+ need_reopen = (peer_info.format.id != port0_info.format.id ||
+ peer_info.format.det.aud.avg_bps !=
+ port0_info.format.det.aud.avg_bps ||
+ peer_info.clock_rate != port0_info.clock_rate ||
+ peer_info.channel_count!=port0_info.channel_count);
+ }
+
+ if (need_reopen) {
+ if (pjsua_var.cap_dev != NULL_SND_DEV_ID) {
+ pjmedia_snd_port_param param;
+
+ pjmedia_snd_port_param_default(&param);
+ param.ec_options = pjsua_var.media_cfg.ec_options;
+
+ /* Create parameter based on peer info */
+ status = create_aud_param(&param.base, pjsua_var.cap_dev,
+ pjsua_var.play_dev,
+ peer_info.clock_rate,
+ peer_info.channel_count,
+ peer_info.samples_per_frame,
+ peer_info.bits_per_sample);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Error opening sound device",
+ status);
+ goto on_return;
+ }
+
+ /* And peer format */
+ if (peer_info.format.id != PJMEDIA_FORMAT_PCM) {
+ param.base.flags |= PJMEDIA_AUD_DEV_CAP_EXT_FORMAT;
+ param.base.ext_fmt = peer_info.format;
+ }
+
+ param.options = 0;
+ status = open_snd_dev(&param);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Error opening sound device",
+ status);
+ goto on_return;
+ }
+ } else {
+ /* Null-audio */
+ status = pjsua_set_snd_dev(pjsua_var.cap_dev,
+ pjsua_var.play_dev);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Error opening sound device",
+ status);
+ goto on_return;
+ }
+ }
+ } else if (pjsua_var.no_snd) {
+ if (!pjsua_var.snd_is_on) {
+ pjsua_var.snd_is_on = PJ_TRUE;
+ /* Notify app */
+ if (pjsua_var.ua_cfg.cb.on_snd_dev_operation) {
+ (*pjsua_var.ua_cfg.cb.on_snd_dev_operation)(1);
+ }
+ }
+ }
+
+ } else {
+ /* The bridge version */
+
+ /* Create sound port if none is instantiated */
+ if (pjsua_var.snd_port==NULL && pjsua_var.null_snd==NULL &&
+ !pjsua_var.no_snd)
+ {
+ status = pjsua_set_snd_dev(pjsua_var.cap_dev, pjsua_var.play_dev);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Error opening sound device", status);
+ goto on_return;
+ }
+ } else if (pjsua_var.no_snd && !pjsua_var.snd_is_on) {
+ pjsua_var.snd_is_on = PJ_TRUE;
+ /* Notify app */
+ if (pjsua_var.ua_cfg.cb.on_snd_dev_operation) {
+ (*pjsua_var.ua_cfg.cb.on_snd_dev_operation)(1);
+ }
+ }
+ }
+
+on_return:
+ PJSUA_UNLOCK();
+
+ if (status == PJ_SUCCESS) {
+ status = pjmedia_conf_connect_port(pjsua_var.mconf, source, sink, 0);
+ }
+
+ pj_log_pop_indent();
+ return status;
+}
+
+
+/*
+ * Disconnect media flow from the source to destination port.
+ */
+PJ_DEF(pj_status_t) pjsua_conf_disconnect( pjsua_conf_port_id source,
+ pjsua_conf_port_id sink)
+{
+ pj_status_t status;
+
+ PJ_LOG(4,(THIS_FILE, "%s disconnect: %d -x- %d",
+ (pjsua_var.is_mswitch ? "Switch" : "Conf"),
+ source, sink));
+ pj_log_push_indent();
+
+ status = pjmedia_conf_disconnect_port(pjsua_var.mconf, source, sink);
+ pjsua_check_snd_dev_idle();
+
+ pj_log_pop_indent();
+ return status;
+}
+
+
+/*
+ * Adjust the signal level to be transmitted from the bridge to the
+ * specified port by making it louder or quieter.
+ */
+PJ_DEF(pj_status_t) pjsua_conf_adjust_tx_level(pjsua_conf_port_id slot,
+ float level)
+{
+ return pjmedia_conf_adjust_tx_level(pjsua_var.mconf, slot,
+ (int)((level-1) * 128));
+}
+
+/*
+ * Adjust the signal level to be received from the specified port (to
+ * the bridge) by making it louder or quieter.
+ */
+PJ_DEF(pj_status_t) pjsua_conf_adjust_rx_level(pjsua_conf_port_id slot,
+ float level)
+{
+ return pjmedia_conf_adjust_rx_level(pjsua_var.mconf, slot,
+ (int)((level-1) * 128));
+}
+
+
+/*
+ * Get last signal level transmitted to or received from the specified port.
+ */
+PJ_DEF(pj_status_t) pjsua_conf_get_signal_level(pjsua_conf_port_id slot,
+ unsigned *tx_level,
+ unsigned *rx_level)
+{
+ return pjmedia_conf_get_signal_level(pjsua_var.mconf, slot,
+ tx_level, rx_level);
+}
+
+/*****************************************************************************
+ * File player.
+ */
+
+static char* get_basename(const char *path, unsigned len)
+{
+ char *p = ((char*)path) + len;
+
+ if (len==0)
+ return p;
+
+ for (--p; p!=path && *p!='/' && *p!='\\'; ) --p;
+
+ return (p==path) ? p : p+1;
+}
+
+
+/*
+ * Create a file player, and automatically connect this player to
+ * the conference bridge.
+ */
+PJ_DEF(pj_status_t) pjsua_player_create( const pj_str_t *filename,
+ unsigned options,
+ pjsua_player_id *p_id)
+{
+ unsigned slot, file_id;
+ char path[PJ_MAXPATH];
+ pj_pool_t *pool = NULL;
+ pjmedia_port *port;
+ pj_status_t status = PJ_SUCCESS;
+
+ if (pjsua_var.player_cnt >= PJ_ARRAY_SIZE(pjsua_var.player))
+ return PJ_ETOOMANY;
+
+ PJ_LOG(4,(THIS_FILE, "Creating file player: %.*s..",
+ (int)filename->slen, filename->ptr));
+ pj_log_push_indent();
+
+ PJSUA_LOCK();
+
+ for (file_id=0; file_id<PJ_ARRAY_SIZE(pjsua_var.player); ++file_id) {
+ if (pjsua_var.player[file_id].port == NULL)
+ break;
+ }
+
+ if (file_id == PJ_ARRAY_SIZE(pjsua_var.player)) {
+ /* This is unexpected */
+ pj_assert(0);
+ status = PJ_EBUG;
+ goto on_error;
+ }
+
+ pj_memcpy(path, filename->ptr, filename->slen);
+ path[filename->slen] = '\0';
+
+ pool = pjsua_pool_create(get_basename(path, filename->slen), 1000, 1000);
+ if (!pool) {
+ status = PJ_ENOMEM;
+ goto on_error;
+ }
+
+ status = pjmedia_wav_player_port_create(
+ pool, path,
+ pjsua_var.mconf_cfg.samples_per_frame *
+ 1000 / pjsua_var.media_cfg.channel_count /
+ pjsua_var.media_cfg.clock_rate,
+ options, 0, &port);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Unable to open file for playback", status);
+ goto on_error;
+ }
+
+ status = pjmedia_conf_add_port(pjsua_var.mconf, pool,
+ port, filename, &slot);
+ if (status != PJ_SUCCESS) {
+ pjmedia_port_destroy(port);
+ pjsua_perror(THIS_FILE, "Unable to add file to conference bridge",
+ status);
+ goto on_error;
+ }
+
+ pjsua_var.player[file_id].type = 0;
+ pjsua_var.player[file_id].pool = pool;
+ pjsua_var.player[file_id].port = port;
+ pjsua_var.player[file_id].slot = slot;
+
+ if (p_id) *p_id = file_id;
+
+ ++pjsua_var.player_cnt;
+
+ PJSUA_UNLOCK();
+
+ PJ_LOG(4,(THIS_FILE, "Player created, id=%d, slot=%d", file_id, slot));
+
+ pj_log_pop_indent();
+ return PJ_SUCCESS;
+
+on_error:
+ PJSUA_UNLOCK();
+ if (pool) pj_pool_release(pool);
+ pj_log_pop_indent();
+ return status;
+}
+
+
+/*
+ * Create a file playlist media port, and automatically add the port
+ * to the conference bridge.
+ */
+PJ_DEF(pj_status_t) pjsua_playlist_create( const pj_str_t file_names[],
+ unsigned file_count,
+ const pj_str_t *label,
+ unsigned options,
+ pjsua_player_id *p_id)
+{
+ unsigned slot, file_id, ptime;
+ pj_pool_t *pool = NULL;
+ pjmedia_port *port;
+ pj_status_t status = PJ_SUCCESS;
+
+ if (pjsua_var.player_cnt >= PJ_ARRAY_SIZE(pjsua_var.player))
+ return PJ_ETOOMANY;
+
+ PJ_LOG(4,(THIS_FILE, "Creating playlist with %d file(s)..", file_count));
+ pj_log_push_indent();
+
+ PJSUA_LOCK();
+
+ for (file_id=0; file_id<PJ_ARRAY_SIZE(pjsua_var.player); ++file_id) {
+ if (pjsua_var.player[file_id].port == NULL)
+ break;
+ }
+
+ if (file_id == PJ_ARRAY_SIZE(pjsua_var.player)) {
+ /* This is unexpected */
+ pj_assert(0);
+ status = PJ_EBUG;
+ goto on_error;
+ }
+
+
+ ptime = pjsua_var.mconf_cfg.samples_per_frame * 1000 /
+ pjsua_var.media_cfg.clock_rate;
+
+ pool = pjsua_pool_create("playlist", 1000, 1000);
+ if (!pool) {
+ status = PJ_ENOMEM;
+ goto on_error;
+ }
+
+ status = pjmedia_wav_playlist_create(pool, label,
+ file_names, file_count,
+ ptime, options, 0, &port);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Unable to create playlist", status);
+ goto on_error;
+ }
+
+ status = pjmedia_conf_add_port(pjsua_var.mconf, pool,
+ port, &port->info.name, &slot);
+ if (status != PJ_SUCCESS) {
+ pjmedia_port_destroy(port);
+ pjsua_perror(THIS_FILE, "Unable to add port", status);
+ goto on_error;
+ }
+
+ pjsua_var.player[file_id].type = 1;
+ pjsua_var.player[file_id].pool = pool;
+ pjsua_var.player[file_id].port = port;
+ pjsua_var.player[file_id].slot = slot;
+
+ if (p_id) *p_id = file_id;
+
+ ++pjsua_var.player_cnt;
+
+ PJSUA_UNLOCK();
+
+ PJ_LOG(4,(THIS_FILE, "Playlist created, id=%d, slot=%d", file_id, slot));
+
+ pj_log_pop_indent();
+
+ return PJ_SUCCESS;
+
+on_error:
+ PJSUA_UNLOCK();
+ if (pool) pj_pool_release(pool);
+ pj_log_pop_indent();
+
+ return status;
+}
+
+
+/*
+ * Get conference port ID associated with player.
+ */
+PJ_DEF(pjsua_conf_port_id) pjsua_player_get_conf_port(pjsua_player_id id)
+{
+ PJ_ASSERT_RETURN(id>=0&&id<(int)PJ_ARRAY_SIZE(pjsua_var.player), PJ_EINVAL);
+ PJ_ASSERT_RETURN(pjsua_var.player[id].port != NULL, PJ_EINVAL);
+
+ return pjsua_var.player[id].slot;
+}
+
+/*
+ * Get the media port for the player.
+ */
+PJ_DEF(pj_status_t) pjsua_player_get_port( pjsua_player_id id,
+ pjmedia_port **p_port)
+{
+ PJ_ASSERT_RETURN(id>=0&&id<(int)PJ_ARRAY_SIZE(pjsua_var.player), PJ_EINVAL);
+ PJ_ASSERT_RETURN(pjsua_var.player[id].port != NULL, PJ_EINVAL);
+ PJ_ASSERT_RETURN(p_port != NULL, PJ_EINVAL);
+
+ *p_port = pjsua_var.player[id].port;
+
+ return PJ_SUCCESS;
+}
+
+/*
+ * Set playback position.
+ */
+PJ_DEF(pj_status_t) pjsua_player_set_pos( pjsua_player_id id,
+ pj_uint32_t samples)
+{
+ PJ_ASSERT_RETURN(id>=0&&id<(int)PJ_ARRAY_SIZE(pjsua_var.player), PJ_EINVAL);
+ PJ_ASSERT_RETURN(pjsua_var.player[id].port != NULL, PJ_EINVAL);
+ PJ_ASSERT_RETURN(pjsua_var.player[id].type == 0, PJ_EINVAL);
+
+ return pjmedia_wav_player_port_set_pos(pjsua_var.player[id].port, samples);
+}
+
+
+/*
+ * Close the file, remove the player from the bridge, and free
+ * resources associated with the file player.
+ */
+PJ_DEF(pj_status_t) pjsua_player_destroy(pjsua_player_id id)
+{
+ PJ_ASSERT_RETURN(id>=0&&id<(int)PJ_ARRAY_SIZE(pjsua_var.player), PJ_EINVAL);
+ PJ_ASSERT_RETURN(pjsua_var.player[id].port != NULL, PJ_EINVAL);
+
+ PJ_LOG(4,(THIS_FILE, "Destroying player %d..", id));
+ pj_log_push_indent();
+
+ PJSUA_LOCK();
+
+ if (pjsua_var.player[id].port) {
+ pjsua_conf_remove_port(pjsua_var.player[id].slot);
+ pjmedia_port_destroy(pjsua_var.player[id].port);
+ pjsua_var.player[id].port = NULL;
+ pjsua_var.player[id].slot = 0xFFFF;
+ pj_pool_release(pjsua_var.player[id].pool);
+ pjsua_var.player[id].pool = NULL;
+ pjsua_var.player_cnt--;
+ }
+
+ PJSUA_UNLOCK();
+ pj_log_pop_indent();
+
+ return PJ_SUCCESS;
+}
+
+
+/*****************************************************************************
+ * File recorder.
+ */
+
+/*
+ * Create a file recorder, and automatically connect this recorder to
+ * the conference bridge.
+ */
+PJ_DEF(pj_status_t) pjsua_recorder_create( const pj_str_t *filename,
+ unsigned enc_type,
+ void *enc_param,
+ pj_ssize_t max_size,
+ unsigned options,
+ pjsua_recorder_id *p_id)
+{
+ enum Format
+ {
+ FMT_UNKNOWN,
+ FMT_WAV,
+ FMT_MP3,
+ };
+ unsigned slot, file_id;
+ char path[PJ_MAXPATH];
+ pj_str_t ext;
+ int file_format;
+ pj_pool_t *pool = NULL;
+ pjmedia_port *port;
+ pj_status_t status = PJ_SUCCESS;
+
+ /* Filename must present */
+ PJ_ASSERT_RETURN(filename != NULL, PJ_EINVAL);
+
+ /* Don't support max_size at present */
+ PJ_ASSERT_RETURN(max_size == 0 || max_size == -1, PJ_EINVAL);
+
+ /* Don't support encoding type at present */
+ PJ_ASSERT_RETURN(enc_type == 0, PJ_EINVAL);
+
+ PJ_LOG(4,(THIS_FILE, "Creating recorder %.*s..",
+ (int)filename->slen, filename->ptr));
+ pj_log_push_indent();
+
+ if (pjsua_var.rec_cnt >= PJ_ARRAY_SIZE(pjsua_var.recorder)) {
+ pj_log_pop_indent();
+ return PJ_ETOOMANY;
+ }
+
+ /* Determine the file format */
+ ext.ptr = filename->ptr + filename->slen - 4;
+ ext.slen = 4;
+
+ if (pj_stricmp2(&ext, ".wav") == 0)
+ file_format = FMT_WAV;
+ else if (pj_stricmp2(&ext, ".mp3") == 0)
+ file_format = FMT_MP3;
+ else {
+ PJ_LOG(1,(THIS_FILE, "pjsua_recorder_create() error: unable to "
+ "determine file format for %.*s",
+ (int)filename->slen, filename->ptr));
+ pj_log_pop_indent();
+ return PJ_ENOTSUP;
+ }
+
+ PJSUA_LOCK();
+
+ for (file_id=0; file_id<PJ_ARRAY_SIZE(pjsua_var.recorder); ++file_id) {
+ if (pjsua_var.recorder[file_id].port == NULL)
+ break;
+ }
+
+ if (file_id == PJ_ARRAY_SIZE(pjsua_var.recorder)) {
+ /* This is unexpected */
+ pj_assert(0);
+ status = PJ_EBUG;
+ goto on_return;
+ }
+
+ pj_memcpy(path, filename->ptr, filename->slen);
+ path[filename->slen] = '\0';
+
+ pool = pjsua_pool_create(get_basename(path, filename->slen), 1000, 1000);
+ if (!pool) {
+ status = PJ_ENOMEM;
+ goto on_return;
+ }
+
+ if (file_format == FMT_WAV) {
+ status = pjmedia_wav_writer_port_create(pool, path,
+ pjsua_var.media_cfg.clock_rate,
+ pjsua_var.mconf_cfg.channel_count,
+ pjsua_var.mconf_cfg.samples_per_frame,
+ pjsua_var.mconf_cfg.bits_per_sample,
+ options, 0, &port);
+ } else {
+ PJ_UNUSED_ARG(enc_param);
+ port = NULL;
+ status = PJ_ENOTSUP;
+ }
+
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Unable to open file for recording", status);
+ goto on_return;
+ }
+
+ status = pjmedia_conf_add_port(pjsua_var.mconf, pool,
+ port, filename, &slot);
+ if (status != PJ_SUCCESS) {
+ pjmedia_port_destroy(port);
+ goto on_return;
+ }
+
+ pjsua_var.recorder[file_id].port = port;
+ pjsua_var.recorder[file_id].slot = slot;
+ pjsua_var.recorder[file_id].pool = pool;
+
+ if (p_id) *p_id = file_id;
+
+ ++pjsua_var.rec_cnt;
+
+ PJSUA_UNLOCK();
+
+ PJ_LOG(4,(THIS_FILE, "Recorder created, id=%d, slot=%d", file_id, slot));
+
+ pj_log_pop_indent();
+ return PJ_SUCCESS;
+
+on_return:
+ PJSUA_UNLOCK();
+ if (pool) pj_pool_release(pool);
+ pj_log_pop_indent();
+ return status;
+}
+
+
+/*
+ * Get conference port associated with recorder.
+ */
+PJ_DEF(pjsua_conf_port_id) pjsua_recorder_get_conf_port(pjsua_recorder_id id)
+{
+ PJ_ASSERT_RETURN(id>=0 && id<(int)PJ_ARRAY_SIZE(pjsua_var.recorder),
+ PJ_EINVAL);
+ PJ_ASSERT_RETURN(pjsua_var.recorder[id].port != NULL, PJ_EINVAL);
+
+ return pjsua_var.recorder[id].slot;
+}
+
+/*
+ * Get the media port for the recorder.
+ */
+PJ_DEF(pj_status_t) pjsua_recorder_get_port( pjsua_recorder_id id,
+ pjmedia_port **p_port)
+{
+ PJ_ASSERT_RETURN(id>=0 && id<(int)PJ_ARRAY_SIZE(pjsua_var.recorder),
+ PJ_EINVAL);
+ PJ_ASSERT_RETURN(pjsua_var.recorder[id].port != NULL, PJ_EINVAL);
+ PJ_ASSERT_RETURN(p_port != NULL, PJ_EINVAL);
+
+ *p_port = pjsua_var.recorder[id].port;
+ return PJ_SUCCESS;
+}
+
+/*
+ * Destroy recorder (this will complete recording).
+ */
+PJ_DEF(pj_status_t) pjsua_recorder_destroy(pjsua_recorder_id id)
+{
+ PJ_ASSERT_RETURN(id>=0 && id<(int)PJ_ARRAY_SIZE(pjsua_var.recorder),
+ PJ_EINVAL);
+ PJ_ASSERT_RETURN(pjsua_var.recorder[id].port != NULL, PJ_EINVAL);
+
+ PJ_LOG(4,(THIS_FILE, "Destroying recorder %d..", id));
+ pj_log_push_indent();
+
+ PJSUA_LOCK();
+
+ if (pjsua_var.recorder[id].port) {
+ pjsua_conf_remove_port(pjsua_var.recorder[id].slot);
+ pjmedia_port_destroy(pjsua_var.recorder[id].port);
+ pjsua_var.recorder[id].port = NULL;
+ pjsua_var.recorder[id].slot = 0xFFFF;
+ pj_pool_release(pjsua_var.recorder[id].pool);
+ pjsua_var.recorder[id].pool = NULL;
+ pjsua_var.rec_cnt--;
+ }
+
+ PJSUA_UNLOCK();
+ pj_log_pop_indent();
+
+ return PJ_SUCCESS;
+}
+
+
+/*****************************************************************************
+ * Sound devices.
+ */
+
+/*
+ * Enum sound devices.
+ */
+
+PJ_DEF(pj_status_t) pjsua_enum_aud_devs( pjmedia_aud_dev_info info[],
+ unsigned *count)
+{
+ unsigned i, dev_count;
+
+ dev_count = pjmedia_aud_dev_count();
+
+ if (dev_count > *count) dev_count = *count;
+
+ for (i=0; i<dev_count; ++i) {
+ pj_status_t status;
+
+ status = pjmedia_aud_dev_get_info(i, &info[i]);
+ if (status != PJ_SUCCESS)
+ return status;
+ }
+
+ *count = dev_count;
+
+ return PJ_SUCCESS;
+}
+
+
+PJ_DEF(pj_status_t) pjsua_enum_snd_devs( pjmedia_snd_dev_info info[],
+ unsigned *count)
+{
+ unsigned i, dev_count;
+
+ dev_count = pjmedia_aud_dev_count();
+
+ if (dev_count > *count) dev_count = *count;
+ pj_bzero(info, dev_count * sizeof(pjmedia_snd_dev_info));
+
+ for (i=0; i<dev_count; ++i) {
+ pjmedia_aud_dev_info ai;
+ pj_status_t status;
+
+ status = pjmedia_aud_dev_get_info(i, &ai);
+ if (status != PJ_SUCCESS)
+ return status;
+
+ strncpy(info[i].name, ai.name, sizeof(info[i].name));
+ info[i].name[sizeof(info[i].name)-1] = '\0';
+ info[i].input_count = ai.input_count;
+ info[i].output_count = ai.output_count;
+ info[i].default_samples_per_sec = ai.default_samples_per_sec;
+ }
+
+ *count = dev_count;
+
+ return PJ_SUCCESS;
+}
+
+/* Create audio device parameter to open the device */
+static pj_status_t create_aud_param(pjmedia_aud_param *param,
+ pjmedia_aud_dev_index capture_dev,
+ pjmedia_aud_dev_index playback_dev,
+ unsigned clock_rate,
+ unsigned channel_count,
+ unsigned samples_per_frame,
+ unsigned bits_per_sample)
+{
+ pj_status_t status;
+
+ /* Normalize device ID with new convention about default device ID */
+ if (playback_dev == PJMEDIA_AUD_DEFAULT_CAPTURE_DEV)
+ playback_dev = PJMEDIA_AUD_DEFAULT_PLAYBACK_DEV;
+
+ /* Create default parameters for the device */
+ status = pjmedia_aud_dev_default_param(capture_dev, param);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Error retrieving default audio "
+ "device parameters", status);
+ return status;
+ }
+ param->dir = PJMEDIA_DIR_CAPTURE_PLAYBACK;
+ param->rec_id = capture_dev;
+ param->play_id = playback_dev;
+ param->clock_rate = clock_rate;
+ param->channel_count = channel_count;
+ param->samples_per_frame = samples_per_frame;
+ param->bits_per_sample = bits_per_sample;
+
+ /* Update the setting with user preference */
+#define update_param(cap, field) \
+ if (pjsua_var.aud_param.flags & cap) { \
+ param->flags |= cap; \
+ param->field = pjsua_var.aud_param.field; \
+ }
+ update_param( PJMEDIA_AUD_DEV_CAP_INPUT_VOLUME_SETTING, input_vol);
+ update_param( PJMEDIA_AUD_DEV_CAP_OUTPUT_VOLUME_SETTING, output_vol);
+ update_param( PJMEDIA_AUD_DEV_CAP_INPUT_ROUTE, input_route);
+ update_param( PJMEDIA_AUD_DEV_CAP_OUTPUT_ROUTE, output_route);
+#undef update_param
+
+ /* Latency settings */
+ param->flags |= (PJMEDIA_AUD_DEV_CAP_INPUT_LATENCY |
+ PJMEDIA_AUD_DEV_CAP_OUTPUT_LATENCY);
+ param->input_latency_ms = pjsua_var.media_cfg.snd_rec_latency;
+ param->output_latency_ms = pjsua_var.media_cfg.snd_play_latency;
+
+ /* EC settings */
+ if (pjsua_var.media_cfg.ec_tail_len) {
+ param->flags |= (PJMEDIA_AUD_DEV_CAP_EC | PJMEDIA_AUD_DEV_CAP_EC_TAIL);
+ param->ec_enabled = PJ_TRUE;
+ param->ec_tail_ms = pjsua_var.media_cfg.ec_tail_len;
+ } else {
+ param->flags &= ~(PJMEDIA_AUD_DEV_CAP_EC|PJMEDIA_AUD_DEV_CAP_EC_TAIL);
+ }
+
+ return PJ_SUCCESS;
+}
+
+/* Internal: the first time the audio device is opened (during app
+ * startup), retrieve the audio settings such as volume level
+ * so that aud_get_settings() will work.
+ */
+static pj_status_t update_initial_aud_param()
+{
+ pjmedia_aud_stream *strm;
+ pjmedia_aud_param param;
+ pj_status_t status;
+
+ PJ_ASSERT_RETURN(pjsua_var.snd_port != NULL, PJ_EBUG);
+
+ strm = pjmedia_snd_port_get_snd_stream(pjsua_var.snd_port);
+
+ status = pjmedia_aud_stream_get_param(strm, &param);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Error audio stream "
+ "device parameters", status);
+ return status;
+ }
+
+#define update_saved_param(cap, field) \
+ if (param.flags & cap) { \
+ pjsua_var.aud_param.flags |= cap; \
+ pjsua_var.aud_param.field = param.field; \
+ }
+
+ update_saved_param(PJMEDIA_AUD_DEV_CAP_INPUT_VOLUME_SETTING, input_vol);
+ update_saved_param(PJMEDIA_AUD_DEV_CAP_OUTPUT_VOLUME_SETTING, output_vol);
+ update_saved_param(PJMEDIA_AUD_DEV_CAP_INPUT_ROUTE, input_route);
+ update_saved_param(PJMEDIA_AUD_DEV_CAP_OUTPUT_ROUTE, output_route);
+#undef update_saved_param
+
+ return PJ_SUCCESS;
+}
+
+/* Get format name */
+static const char *get_fmt_name(pj_uint32_t id)
+{
+ static char name[8];
+
+ if (id == PJMEDIA_FORMAT_L16)
+ return "PCM";
+ pj_memcpy(name, &id, 4);
+ name[4] = '\0';
+ return name;
+}
+
+/* Open sound device with the setting. */
+static pj_status_t open_snd_dev(pjmedia_snd_port_param *param)
+{
+ pjmedia_port *conf_port;
+ pj_status_t status;
+
+ PJ_ASSERT_RETURN(param, PJ_EINVAL);
+
+ /* Check if NULL sound device is used */
+ if (NULL_SND_DEV_ID==param->base.rec_id ||
+ NULL_SND_DEV_ID==param->base.play_id)
+ {
+ return pjsua_set_null_snd_dev();
+ }
+
+ /* Close existing sound port */
+ close_snd_dev();
+
+ /* Notify app */
+ if (pjsua_var.ua_cfg.cb.on_snd_dev_operation) {
+ (*pjsua_var.ua_cfg.cb.on_snd_dev_operation)(1);
+ }
+
+ /* Create memory pool for sound device. */
+ pjsua_var.snd_pool = pjsua_pool_create("pjsua_snd", 4000, 4000);
+ PJ_ASSERT_RETURN(pjsua_var.snd_pool, PJ_ENOMEM);
+
+
+ PJ_LOG(4,(THIS_FILE, "Opening sound device %s@%d/%d/%dms",
+ get_fmt_name(param->base.ext_fmt.id),
+ param->base.clock_rate, param->base.channel_count,
+ param->base.samples_per_frame / param->base.channel_count *
+ 1000 / param->base.clock_rate));
+ pj_log_push_indent();
+
+ status = pjmedia_snd_port_create2( pjsua_var.snd_pool,
+ param, &pjsua_var.snd_port);
+ if (status != PJ_SUCCESS)
+ goto on_error;
+
+ /* Get the port0 of the conference bridge. */
+ conf_port = pjmedia_conf_get_master_port(pjsua_var.mconf);
+ pj_assert(conf_port != NULL);
+
+ /* For conference bridge, resample if necessary if the bridge's
+ * clock rate is different than the sound device's clock rate.
+ */
+ if (!pjsua_var.is_mswitch &&
+ param->base.ext_fmt.id == PJMEDIA_FORMAT_PCM &&
+ PJMEDIA_PIA_SRATE(&conf_port->info) != param->base.clock_rate)
+ {
+ pjmedia_port *resample_port;
+ unsigned resample_opt = 0;
+
+ if (pjsua_var.media_cfg.quality >= 3 &&
+ pjsua_var.media_cfg.quality <= 4)
+ {
+ resample_opt |= PJMEDIA_RESAMPLE_USE_SMALL_FILTER;
+ }
+ else if (pjsua_var.media_cfg.quality < 3) {
+ resample_opt |= PJMEDIA_RESAMPLE_USE_LINEAR;
+ }
+
+ status = pjmedia_resample_port_create(pjsua_var.snd_pool,
+ conf_port,
+ param->base.clock_rate,
+ resample_opt,
+ &resample_port);
+ if (status != PJ_SUCCESS) {
+ char errmsg[PJ_ERR_MSG_SIZE];
+ pj_strerror(status, errmsg, sizeof(errmsg));
+ PJ_LOG(4, (THIS_FILE,
+ "Error creating resample port: %s",
+ errmsg));
+ close_snd_dev();
+ goto on_error;
+ }
+
+ conf_port = resample_port;
+ }
+
+ /* Otherwise for audio switchboard, the switch's port0 setting is
+ * derived from the sound device setting, so update the setting.
+ */
+ if (pjsua_var.is_mswitch) {
+ if (param->base.flags & PJMEDIA_AUD_DEV_CAP_EXT_FORMAT) {
+ conf_port->info.fmt = param->base.ext_fmt;
+ } else {
+ unsigned bps, ptime_usec;
+ bps = param->base.clock_rate * param->base.bits_per_sample;
+ ptime_usec = param->base.samples_per_frame /
+ param->base.channel_count * 1000000 /
+ param->base.clock_rate;
+ pjmedia_format_init_audio(&conf_port->info.fmt,
+ PJMEDIA_FORMAT_PCM,
+ param->base.clock_rate,
+ param->base.channel_count,
+ param->base.bits_per_sample,
+ ptime_usec,
+ bps, bps);
+ }
+ }
+
+
+ /* Connect sound port to the bridge */
+ status = pjmedia_snd_port_connect(pjsua_var.snd_port,
+ conf_port );
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Unable to connect conference port to "
+ "sound device", status);
+ pjmedia_snd_port_destroy(pjsua_var.snd_port);
+ pjsua_var.snd_port = NULL;
+ goto on_error;
+ }
+
+ /* Save the device IDs */
+ pjsua_var.cap_dev = param->base.rec_id;
+ pjsua_var.play_dev = param->base.play_id;
+
+ /* Update sound device name. */
+ {
+ pjmedia_aud_dev_info rec_info;
+ pjmedia_aud_stream *strm;
+ pjmedia_aud_param si;
+ pj_str_t tmp;
+
+ strm = pjmedia_snd_port_get_snd_stream(pjsua_var.snd_port);
+ status = pjmedia_aud_stream_get_param(strm, &si);
+ if (status == PJ_SUCCESS)
+ status = pjmedia_aud_dev_get_info(si.rec_id, &rec_info);
+
+ if (status==PJ_SUCCESS) {
+ if (param->base.clock_rate != pjsua_var.media_cfg.clock_rate) {
+ char tmp_buf[128];
+ int tmp_buf_len = sizeof(tmp_buf);
+
+ tmp_buf_len = pj_ansi_snprintf(tmp_buf, sizeof(tmp_buf)-1,
+ "%s (%dKHz)",
+ rec_info.name,
+ param->base.clock_rate/1000);
+ pj_strset(&tmp, tmp_buf, tmp_buf_len);
+ pjmedia_conf_set_port0_name(pjsua_var.mconf, &tmp);
+ } else {
+ pjmedia_conf_set_port0_name(pjsua_var.mconf,
+ pj_cstr(&tmp, rec_info.name));
+ }
+ }
+
+ /* Any error is not major, let it through */
+ status = PJ_SUCCESS;
+ }
+
+ /* If this is the first time the audio device is open, retrieve some
+ * settings from the device (such as volume settings) so that the
+ * pjsua_snd_get_setting() work.
+ */
+ if (pjsua_var.aud_open_cnt == 0) {
+ update_initial_aud_param();
+ ++pjsua_var.aud_open_cnt;
+ }
+
+ pjsua_var.snd_is_on = PJ_TRUE;
+
+ pj_log_pop_indent();
+ return PJ_SUCCESS;
+
+on_error:
+ pj_log_pop_indent();
+ return status;
+}
+
+
+/* Close existing sound device */
+static void close_snd_dev(void)
+{
+ pj_log_push_indent();
+
+ /* Notify app */
+ if (pjsua_var.snd_is_on && pjsua_var.ua_cfg.cb.on_snd_dev_operation) {
+ (*pjsua_var.ua_cfg.cb.on_snd_dev_operation)(0);
+ }
+
+ /* Close sound device */
+ if (pjsua_var.snd_port) {
+ pjmedia_aud_dev_info cap_info, play_info;
+ pjmedia_aud_stream *strm;
+ pjmedia_aud_param param;
+
+ strm = pjmedia_snd_port_get_snd_stream(pjsua_var.snd_port);
+ pjmedia_aud_stream_get_param(strm, &param);
+
+ if (pjmedia_aud_dev_get_info(param.rec_id, &cap_info) != PJ_SUCCESS)
+ cap_info.name[0] = '\0';
+ if (pjmedia_aud_dev_get_info(param.play_id, &play_info) != PJ_SUCCESS)
+ play_info.name[0] = '\0';
+
+ PJ_LOG(4,(THIS_FILE, "Closing %s sound playback device and "
+ "%s sound capture device",
+ play_info.name, cap_info.name));
+
+ pjmedia_snd_port_disconnect(pjsua_var.snd_port);
+ pjmedia_snd_port_destroy(pjsua_var.snd_port);
+ pjsua_var.snd_port = NULL;
+ }
+
+ /* Close null sound device */
+ if (pjsua_var.null_snd) {
+ PJ_LOG(4,(THIS_FILE, "Closing null sound device.."));
+ pjmedia_master_port_destroy(pjsua_var.null_snd, PJ_FALSE);
+ pjsua_var.null_snd = NULL;
+ }
+
+ if (pjsua_var.snd_pool)
+ pj_pool_release(pjsua_var.snd_pool);
+
+ pjsua_var.snd_pool = NULL;
+ pjsua_var.snd_is_on = PJ_FALSE;
+
+ pj_log_pop_indent();
+}
+
+
+/*
+ * Select or change sound device. Application may call this function at
+ * any time to replace current sound device.
+ */
+PJ_DEF(pj_status_t) pjsua_set_snd_dev( int capture_dev,
+ int playback_dev)
+{
+ unsigned alt_cr_cnt = 1;
+ unsigned alt_cr[] = {0, 44100, 48000, 32000, 16000, 8000};
+ unsigned i;
+ pj_status_t status = -1;
+
+ PJ_LOG(4,(THIS_FILE, "Set sound device: capture=%d, playback=%d",
+ capture_dev, playback_dev));
+ pj_log_push_indent();
+
+ PJSUA_LOCK();
+
+ /* Null-sound */
+ if (capture_dev==NULL_SND_DEV_ID && playback_dev==NULL_SND_DEV_ID) {
+ PJSUA_UNLOCK();
+ status = pjsua_set_null_snd_dev();
+ pj_log_pop_indent();
+ return status;
+ }
+
+ /* Set default clock rate */
+ alt_cr[0] = pjsua_var.media_cfg.snd_clock_rate;
+ if (alt_cr[0] == 0)
+ alt_cr[0] = pjsua_var.media_cfg.clock_rate;
+
+ /* Allow retrying of different clock rate if we're using conference
+ * bridge (meaning audio format is always PCM), otherwise lock on
+ * to one clock rate.
+ */
+ if (pjsua_var.is_mswitch) {
+ alt_cr_cnt = 1;
+ } else {
+ alt_cr_cnt = PJ_ARRAY_SIZE(alt_cr);
+ }
+
+ /* Attempts to open the sound device with different clock rates */
+ for (i=0; i<alt_cr_cnt; ++i) {
+ pjmedia_snd_port_param param;
+ unsigned samples_per_frame;
+
+ /* Create the default audio param */
+ samples_per_frame = alt_cr[i] *
+ pjsua_var.media_cfg.audio_frame_ptime *
+ pjsua_var.media_cfg.channel_count / 1000;
+ pjmedia_snd_port_param_default(&param);
+ param.ec_options = pjsua_var.media_cfg.ec_options;
+ status = create_aud_param(&param.base, capture_dev, playback_dev,
+ alt_cr[i], pjsua_var.media_cfg.channel_count,
+ samples_per_frame, 16);
+ if (status != PJ_SUCCESS)
+ goto on_error;
+
+ /* Open! */
+ param.options = 0;
+ status = open_snd_dev(&param);
+ if (status == PJ_SUCCESS)
+ break;
+ }
+
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Unable to open sound device", status);
+ goto on_error;
+ }
+
+ pjsua_var.no_snd = PJ_FALSE;
+ pjsua_var.snd_is_on = PJ_TRUE;
+
+ PJSUA_UNLOCK();
+ pj_log_pop_indent();
+ return PJ_SUCCESS;
+
+on_error:
+ PJSUA_UNLOCK();
+ pj_log_pop_indent();
+ return status;
+}
+
+
+/*
+ * Get currently active sound devices. If sound devices has not been created
+ * (for example when pjsua_start() is not called), it is possible that
+ * the function returns PJ_SUCCESS with -1 as device IDs.
+ */
+PJ_DEF(pj_status_t) pjsua_get_snd_dev(int *capture_dev,
+ int *playback_dev)
+{
+ PJSUA_LOCK();
+
+ if (capture_dev) {
+ *capture_dev = pjsua_var.cap_dev;
+ }
+ if (playback_dev) {
+ *playback_dev = pjsua_var.play_dev;
+ }
+
+ PJSUA_UNLOCK();
+ return PJ_SUCCESS;
+}
+
+
+/*
+ * Use null sound device.
+ */
+PJ_DEF(pj_status_t) pjsua_set_null_snd_dev(void)
+{
+ pjmedia_port *conf_port;
+ pj_status_t status;
+
+ PJ_LOG(4,(THIS_FILE, "Setting null sound device.."));
+ pj_log_push_indent();
+
+ PJSUA_LOCK();
+
+ /* Close existing sound device */
+ close_snd_dev();
+
+ /* Notify app */
+ if (pjsua_var.ua_cfg.cb.on_snd_dev_operation) {
+ (*pjsua_var.ua_cfg.cb.on_snd_dev_operation)(1);
+ }
+
+ /* Create memory pool for sound device. */
+ pjsua_var.snd_pool = pjsua_pool_create("pjsua_snd", 4000, 4000);
+ PJ_ASSERT_RETURN(pjsua_var.snd_pool, PJ_ENOMEM);
+
+ PJ_LOG(4,(THIS_FILE, "Opening null sound device.."));
+
+ /* Get the port0 of the conference bridge. */
+ conf_port = pjmedia_conf_get_master_port(pjsua_var.mconf);
+ pj_assert(conf_port != NULL);
+
+ /* Create master port, connecting port0 of the conference bridge to
+ * a null port.
+ */
+ status = pjmedia_master_port_create(pjsua_var.snd_pool, pjsua_var.null_port,
+ conf_port, 0, &pjsua_var.null_snd);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Unable to create null sound device",
+ status);
+ PJSUA_UNLOCK();
+ pj_log_pop_indent();
+ return status;
+ }
+
+ /* Start the master port */
+ status = pjmedia_master_port_start(pjsua_var.null_snd);
+ PJ_ASSERT_RETURN(status == PJ_SUCCESS, status);
+
+ pjsua_var.cap_dev = NULL_SND_DEV_ID;
+ pjsua_var.play_dev = NULL_SND_DEV_ID;
+
+ pjsua_var.no_snd = PJ_FALSE;
+ pjsua_var.snd_is_on = PJ_TRUE;
+
+ PJSUA_UNLOCK();
+ pj_log_pop_indent();
+ return PJ_SUCCESS;
+}
+
+
+
+/*
+ * Use no device!
+ */
+PJ_DEF(pjmedia_port*) pjsua_set_no_snd_dev(void)
+{
+ PJSUA_LOCK();
+
+ /* Close existing sound device */
+ close_snd_dev();
+ pjsua_var.no_snd = PJ_TRUE;
+
+ PJSUA_UNLOCK();
+
+ return pjmedia_conf_get_master_port(pjsua_var.mconf);
+}
+
+
+/*
+ * Configure the AEC settings of the sound port.
+ */
+PJ_DEF(pj_status_t) pjsua_set_ec(unsigned tail_ms, unsigned options)
+{
+ pj_status_t status = PJ_SUCCESS;
+
+ PJSUA_LOCK();
+
+ pjsua_var.media_cfg.ec_tail_len = tail_ms;
+ pjsua_var.media_cfg.ec_options = options;
+
+ if (pjsua_var.snd_port)
+ status = pjmedia_snd_port_set_ec(pjsua_var.snd_port, pjsua_var.pool,
+ tail_ms, options);
+
+ PJSUA_UNLOCK();
+ return status;
+}
+
+
+/*
+ * Get current AEC tail length.
+ */
+PJ_DEF(pj_status_t) pjsua_get_ec_tail(unsigned *p_tail_ms)
+{
+ *p_tail_ms = pjsua_var.media_cfg.ec_tail_len;
+ return PJ_SUCCESS;
+}
+
+
+/*
+ * Check whether the sound device is currently active.
+ */
+PJ_DEF(pj_bool_t) pjsua_snd_is_active(void)
+{
+ return pjsua_var.snd_port != NULL;
+}
+
+
+/*
+ * Configure sound device setting to the sound device being used.
+ */
+PJ_DEF(pj_status_t) pjsua_snd_set_setting( pjmedia_aud_dev_cap cap,
+ const void *pval,
+ pj_bool_t keep)
+{
+ pj_status_t status;
+
+ /* Check if we are allowed to set the cap */
+ if ((cap & pjsua_var.aud_svmask) == 0) {
+ return PJMEDIA_EAUD_INVCAP;
+ }
+
+ PJSUA_LOCK();
+
+ /* If sound is active, set it immediately */
+ if (pjsua_snd_is_active()) {
+ pjmedia_aud_stream *strm;
+
+ strm = pjmedia_snd_port_get_snd_stream(pjsua_var.snd_port);
+ status = pjmedia_aud_stream_set_cap(strm, cap, pval);
+ } else {
+ status = PJ_SUCCESS;
+ }
+
+ if (status != PJ_SUCCESS) {
+ PJSUA_UNLOCK();
+ return status;
+ }
+
+ /* Save in internal param for later device open */
+ if (keep) {
+ status = pjmedia_aud_param_set_cap(&pjsua_var.aud_param,
+ cap, pval);
+ }
+
+ PJSUA_UNLOCK();
+ return status;
+}
+
+/*
+ * Retrieve a sound device setting.
+ */
+PJ_DEF(pj_status_t) pjsua_snd_get_setting( pjmedia_aud_dev_cap cap,
+ void *pval)
+{
+ pj_status_t status;
+
+ PJSUA_LOCK();
+
+ /* If sound device has never been opened before, open it to
+ * retrieve the initial setting from the device (e.g. audio
+ * volume)
+ */
+ if (pjsua_var.aud_open_cnt==0) {
+ PJ_LOG(4,(THIS_FILE, "Opening sound device to get initial settings"));
+ pjsua_set_snd_dev(pjsua_var.cap_dev, pjsua_var.play_dev);
+ close_snd_dev();
+ }
+
+ if (pjsua_snd_is_active()) {
+ /* Sound is active, retrieve from device directly */
+ pjmedia_aud_stream *strm;
+
+ strm = pjmedia_snd_port_get_snd_stream(pjsua_var.snd_port);
+ status = pjmedia_aud_stream_get_cap(strm, cap, pval);
+ } else {
+ /* Otherwise retrieve from internal param */
+ status = pjmedia_aud_param_get_cap(&pjsua_var.aud_param,
+ cap, pval);
+ }
+
+ PJSUA_UNLOCK();
+ return status;
+}
+
+#endif /* PJSUA_MEDIA_HAS_PJMEDIA */
diff --git a/pjsip/src/pjsua-lib/pjsua_call.c b/pjsip/src/pjsua-lib/pjsua_call.c
new file mode 100644
index 0000000..6f14709
--- /dev/null
+++ b/pjsip/src/pjsua-lib/pjsua_call.c
@@ -0,0 +1,4397 @@
+/* $Id: pjsua_call.c 4176 2012-06-23 03:06:52Z nanang $ */
+/*
+ * Copyright (C) 2008-2011 Teluu Inc. (http://www.teluu.com)
+ * Copyright (C) 2003-2008 Benny Prijono <benny@prijono.org>
+ *
+ * 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
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ */
+#include <pjsua-lib/pjsua.h>
+#include <pjsua-lib/pjsua_internal.h>
+
+
+#define THIS_FILE "pjsua_call.c"
+
+
+/* Retry interval of sending re-INVITE for locking a codec when remote
+ * SDP answer contains multiple codec, in milliseconds.
+ */
+#define LOCK_CODEC_RETRY_INTERVAL 200
+
+/*
+ * Max UPDATE/re-INVITE retry to lock codec
+ */
+#define LOCK_CODEC_MAX_RETRY 5
+
+
+/*
+ * The INFO method.
+ */
+const pjsip_method pjsip_info_method =
+{
+ PJSIP_OTHER_METHOD,
+ { "INFO", 4 }
+};
+
+
+/* This callback receives notification from invite session when the
+ * session state has changed.
+ */
+static void pjsua_call_on_state_changed(pjsip_inv_session *inv,
+ pjsip_event *e);
+
+/* This callback is called by invite session framework when UAC session
+ * has forked.
+ */
+static void pjsua_call_on_forked( pjsip_inv_session *inv,
+ pjsip_event *e);
+
+/*
+ * Callback to be called when SDP offer/answer negotiation has just completed
+ * in the session. This function will start/update media if negotiation
+ * has succeeded.
+ */
+static void pjsua_call_on_media_update(pjsip_inv_session *inv,
+ pj_status_t status);
+
+/*
+ * Called when session received new offer.
+ */
+static void pjsua_call_on_rx_offer(pjsip_inv_session *inv,
+ const pjmedia_sdp_session *offer);
+
+/*
+ * Called to generate new offer.
+ */
+static void pjsua_call_on_create_offer(pjsip_inv_session *inv,
+ pjmedia_sdp_session **offer);
+
+/*
+ * This callback is called when transaction state has changed in INVITE
+ * session. We use this to trap:
+ * - incoming REFER request.
+ * - incoming MESSAGE request.
+ */
+static void pjsua_call_on_tsx_state_changed(pjsip_inv_session *inv,
+ pjsip_transaction *tsx,
+ pjsip_event *e);
+
+/*
+ * Redirection handler.
+ */
+static pjsip_redirect_op pjsua_call_on_redirected(pjsip_inv_session *inv,
+ const pjsip_uri *target,
+ const pjsip_event *e);
+
+
+/* Create SDP for call hold. */
+static pj_status_t create_sdp_of_call_hold(pjsua_call *call,
+ pjmedia_sdp_session **p_sdp);
+
+/*
+ * Callback called by event framework when the xfer subscription state
+ * has changed.
+ */
+static void xfer_client_on_evsub_state( pjsip_evsub *sub, pjsip_event *event);
+static void xfer_server_on_evsub_state( pjsip_evsub *sub, pjsip_event *event);
+
+/*
+ * Reset call descriptor.
+ */
+static void reset_call(pjsua_call_id id)
+{
+ pjsua_call *call = &pjsua_var.calls[id];
+ unsigned i;
+
+ pj_bzero(call, sizeof(*call));
+ call->index = id;
+ call->last_text.ptr = call->last_text_buf_;
+ for (i=0; i<PJ_ARRAY_SIZE(call->media); ++i) {
+ pjsua_call_media *call_med = &call->media[i];
+ call_med->ssrc = pj_rand();
+ call_med->strm.a.conf_slot = PJSUA_INVALID_ID;
+ call_med->strm.v.cap_win_id = PJSUA_INVALID_ID;
+ call_med->strm.v.rdr_win_id = PJSUA_INVALID_ID;
+ call_med->call = call;
+ call_med->idx = i;
+ call_med->tp_auto_del = PJ_TRUE;
+ }
+ pjsua_call_setting_default(&call->opt);
+}
+
+
+/*
+ * Init call subsystem.
+ */
+pj_status_t pjsua_call_subsys_init(const pjsua_config *cfg)
+{
+ pjsip_inv_callback inv_cb;
+ unsigned i;
+ const pj_str_t str_norefersub = { "norefersub", 10 };
+ pj_status_t status;
+
+ /* Init calls array. */
+ for (i=0; i<PJ_ARRAY_SIZE(pjsua_var.calls); ++i)
+ reset_call(i);
+
+ /* Copy config */
+ pjsua_config_dup(pjsua_var.pool, &pjsua_var.ua_cfg, cfg);
+
+ /* Verify settings */
+ if (pjsua_var.ua_cfg.max_calls >= PJSUA_MAX_CALLS) {
+ pjsua_var.ua_cfg.max_calls = PJSUA_MAX_CALLS;
+ }
+
+ /* Check the route URI's and force loose route if required */
+ for (i=0; i<pjsua_var.ua_cfg.outbound_proxy_cnt; ++i) {
+ status = normalize_route_uri(pjsua_var.pool,
+ &pjsua_var.ua_cfg.outbound_proxy[i]);
+ if (status != PJ_SUCCESS)
+ return status;
+ }
+
+ /* Initialize invite session callback. */
+ pj_bzero(&inv_cb, sizeof(inv_cb));
+ inv_cb.on_state_changed = &pjsua_call_on_state_changed;
+ inv_cb.on_new_session = &pjsua_call_on_forked;
+ inv_cb.on_media_update = &pjsua_call_on_media_update;
+ inv_cb.on_rx_offer = &pjsua_call_on_rx_offer;
+ inv_cb.on_create_offer = &pjsua_call_on_create_offer;
+ inv_cb.on_tsx_state_changed = &pjsua_call_on_tsx_state_changed;
+ inv_cb.on_redirected = &pjsua_call_on_redirected;
+
+ /* Initialize invite session module: */
+ status = pjsip_inv_usage_init(pjsua_var.endpt, &inv_cb);
+ PJ_ASSERT_RETURN(status == PJ_SUCCESS, status);
+
+ /* Add "norefersub" in Supported header */
+ pjsip_endpt_add_capability(pjsua_var.endpt, NULL, PJSIP_H_SUPPORTED,
+ NULL, 1, &str_norefersub);
+
+ return status;
+}
+
+
+/*
+ * Start call subsystem.
+ */
+pj_status_t pjsua_call_subsys_start(void)
+{
+ /* Nothing to do */
+ return PJ_SUCCESS;
+}
+
+
+/*
+ * Get maximum number of calls configured in pjsua.
+ */
+PJ_DEF(unsigned) pjsua_call_get_max_count(void)
+{
+ return pjsua_var.ua_cfg.max_calls;
+}
+
+
+/*
+ * Get number of currently active calls.
+ */
+PJ_DEF(unsigned) pjsua_call_get_count(void)
+{
+ return pjsua_var.call_cnt;
+}
+
+
+/*
+ * Enum calls.
+ */
+PJ_DEF(pj_status_t) pjsua_enum_calls( pjsua_call_id ids[],
+ unsigned *count)
+{
+ unsigned i, c;
+
+ PJ_ASSERT_RETURN(ids && *count, PJ_EINVAL);
+
+ PJSUA_LOCK();
+
+ for (i=0, c=0; c<*count && i<pjsua_var.ua_cfg.max_calls; ++i) {
+ if (!pjsua_var.calls[i].inv)
+ continue;
+ ids[c] = i;
+ ++c;
+ }
+
+ *count = c;
+
+ PJSUA_UNLOCK();
+
+ return PJ_SUCCESS;
+}
+
+
+/* Allocate one call id */
+static pjsua_call_id alloc_call_id(void)
+{
+ pjsua_call_id cid;
+
+#if 1
+ /* New algorithm: round-robin */
+ if (pjsua_var.next_call_id >= (int)pjsua_var.ua_cfg.max_calls ||
+ pjsua_var.next_call_id < 0)
+ {
+ pjsua_var.next_call_id = 0;
+ }
+
+ for (cid=pjsua_var.next_call_id;
+ cid<(int)pjsua_var.ua_cfg.max_calls;
+ ++cid)
+ {
+ if (pjsua_var.calls[cid].inv == NULL &&
+ pjsua_var.calls[cid].async_call.dlg == NULL)
+ {
+ ++pjsua_var.next_call_id;
+ return cid;
+ }
+ }
+
+ for (cid=0; cid < pjsua_var.next_call_id; ++cid) {
+ if (pjsua_var.calls[cid].inv == NULL &&
+ pjsua_var.calls[cid].async_call.dlg == NULL)
+ {
+ ++pjsua_var.next_call_id;
+ return cid;
+ }
+ }
+
+#else
+ /* Old algorithm */
+ for (cid=0; cid<(int)pjsua_var.ua_cfg.max_calls; ++cid) {
+ if (pjsua_var.calls[cid].inv == NULL)
+ return cid;
+ }
+#endif
+
+ return PJSUA_INVALID_ID;
+}
+
+/* Get signaling secure level.
+ * Return:
+ * 0: if signaling is not secure
+ * 1: if TLS transport is used for immediate hop
+ * 2: if end-to-end signaling is secure.
+ */
+static int get_secure_level(pjsua_acc_id acc_id, const pj_str_t *dst_uri)
+{
+ const pj_str_t tls = pj_str(";transport=tls");
+ const pj_str_t sips = pj_str("sips:");
+ pjsua_acc *acc = &pjsua_var.acc[acc_id];
+
+ if (pj_stristr(dst_uri, &sips))
+ return 2;
+
+ if (!pj_list_empty(&acc->route_set)) {
+ pjsip_route_hdr *r = acc->route_set.next;
+ pjsip_uri *uri = r->name_addr.uri;
+ pjsip_sip_uri *sip_uri;
+
+ sip_uri = (pjsip_sip_uri*)pjsip_uri_get_uri(uri);
+ if (pj_stricmp2(&sip_uri->transport_param, "tls")==0)
+ return 1;
+
+ } else {
+ if (pj_stristr(dst_uri, &tls))
+ return 1;
+ }
+
+ return 0;
+}
+
+/*
+static int call_get_secure_level(pjsua_call *call)
+{
+ if (call->inv->dlg->secure)
+ return 2;
+
+ if (!pj_list_empty(&call->inv->dlg->route_set)) {
+ pjsip_route_hdr *r = call->inv->dlg->route_set.next;
+ pjsip_uri *uri = r->name_addr.uri;
+ pjsip_sip_uri *sip_uri;
+
+ sip_uri = (pjsip_sip_uri*)pjsip_uri_get_uri(uri);
+ if (pj_stricmp2(&sip_uri->transport_param, "tls")==0)
+ return 1;
+
+ } else {
+ pjsip_sip_uri *sip_uri;
+
+ if (PJSIP_URI_SCHEME_IS_SIPS(call->inv->dlg->target))
+ return 2;
+ if (!PJSIP_URI_SCHEME_IS_SIP(call->inv->dlg->target))
+ return 0;
+
+ sip_uri = (pjsip_sip_uri*) pjsip_uri_get_uri(call->inv->dlg->target);
+ if (pj_stricmp2(&sip_uri->transport_param, "tls")==0)
+ return 1;
+ }
+
+ return 0;
+}
+*/
+
+/* Outgoing call callback when media transport creation is completed. */
+static pj_status_t
+on_make_call_med_tp_complete(pjsua_call_id call_id,
+ const pjsua_med_tp_state_info *info)
+{
+ pjmedia_sdp_session *offer;
+ pjsip_inv_session *inv = NULL;
+ pjsua_call *call = &pjsua_var.calls[call_id];
+ pjsua_acc *acc = &pjsua_var.acc[call->acc_id];
+ pjsip_dialog *dlg = call->async_call.dlg;
+ unsigned options = 0;
+ pjsip_tx_data *tdata;
+ pj_status_t status = (info? info->status: PJ_SUCCESS);
+
+ PJSUA_LOCK();
+
+ /* Increment the dialog's lock otherwise when invite session creation
+ * fails the dialog will be destroyed prematurely.
+ */
+ pjsip_dlg_inc_lock(dlg);
+
+ /* Decrement dialog session. */
+ pjsip_dlg_dec_session(dlg, &pjsua_var.mod);
+
+ if (status != PJ_SUCCESS) {
+ pj_str_t err_str;
+ int title_len;
+
+ call->last_code = PJSIP_SC_TEMPORARILY_UNAVAILABLE;
+ pj_strcpy2(&call->last_text, "Media init error: ");
+
+ title_len = call->last_text.slen;
+ err_str = pj_strerror(status, call->last_text_buf_ + title_len,
+ sizeof(call->last_text_buf_) - title_len);
+ call->last_text.slen += err_str.slen;
+
+ pjsua_perror(THIS_FILE, "Error initializing media channel", status);
+ goto on_error;
+ }
+
+ /* pjsua_media_channel_deinit() has been called. */
+ if (call->async_call.med_ch_deinit)
+ goto on_error;
+
+ /* Create offer */
+ status = pjsua_media_channel_create_sdp(call->index, dlg->pool, NULL,
+ &offer, NULL);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Error initializing media channel", status);
+ goto on_error;
+ }
+
+ /* Create the INVITE session: */
+ options |= PJSIP_INV_SUPPORT_100REL;
+ if (acc->cfg.require_100rel)
+ options |= PJSIP_INV_REQUIRE_100REL;
+ if (acc->cfg.use_timer != PJSUA_SIP_TIMER_INACTIVE) {
+ options |= PJSIP_INV_SUPPORT_TIMER;
+ if (acc->cfg.use_timer == PJSUA_SIP_TIMER_REQUIRED)
+ options |= PJSIP_INV_REQUIRE_TIMER;
+ else if (acc->cfg.use_timer == PJSUA_SIP_TIMER_ALWAYS)
+ options |= PJSIP_INV_ALWAYS_USE_TIMER;
+ }
+
+ status = pjsip_inv_create_uac( dlg, offer, options, &inv);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Invite session creation failed", status);
+ goto on_error;
+ }
+
+ /* Init Session Timers */
+ status = pjsip_timer_init_session(inv, &acc->cfg.timer_setting);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Session Timer init failed", status);
+ goto on_error;
+ }
+
+ /* Create and associate our data in the session. */
+ call->inv = inv;
+
+ dlg->mod_data[pjsua_var.mod.id] = call;
+ inv->mod_data[pjsua_var.mod.id] = call;
+
+ /* If account is locked to specific transport, then lock dialog
+ * to this transport too.
+ */
+ if (acc->cfg.transport_id != PJSUA_INVALID_ID) {
+ pjsip_tpselector tp_sel;
+
+ pjsua_init_tpselector(acc->cfg.transport_id, &tp_sel);
+ pjsip_dlg_set_transport(dlg, &tp_sel);
+ }
+
+ /* Set dialog Route-Set: */
+ if (!pj_list_empty(&acc->route_set))
+ pjsip_dlg_set_route_set(dlg, &acc->route_set);
+
+
+ /* Set credentials: */
+ if (acc->cred_cnt) {
+ pjsip_auth_clt_set_credentials( &dlg->auth_sess,
+ acc->cred_cnt, acc->cred);
+ }
+
+ /* Set authentication preference */
+ pjsip_auth_clt_set_prefs(&dlg->auth_sess, &acc->cfg.auth_pref);
+
+ /* Create initial INVITE: */
+
+ status = pjsip_inv_invite(inv, &tdata);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Unable to create initial INVITE request",
+ status);
+ goto on_error;
+ }
+
+
+ /* Add additional headers etc */
+
+ pjsua_process_msg_data( tdata,
+ call->async_call.call_var.out_call.msg_data);
+
+ /* Must increment call counter now */
+ ++pjsua_var.call_cnt;
+
+ /* Send initial INVITE: */
+
+ status = pjsip_inv_send_msg(inv, tdata);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Unable to send initial INVITE request",
+ status);
+
+ /* Upon failure to send first request, the invite
+ * session would have been cleared.
+ */
+ inv = NULL;
+ goto on_error;
+ }
+
+ /* Done. */
+
+ pjsip_dlg_dec_lock(dlg);
+ PJSUA_UNLOCK();
+
+ return PJ_SUCCESS;
+
+on_error:
+ if (inv == NULL && call_id != -1 && pjsua_var.ua_cfg.cb.on_call_state)
+ (*pjsua_var.ua_cfg.cb.on_call_state)(call_id, NULL);
+
+ if (dlg) {
+ /* This may destroy the dialog */
+ pjsip_dlg_dec_lock(dlg);
+ }
+
+ if (inv != NULL) {
+ pjsip_inv_terminate(inv, PJSIP_SC_OK, PJ_FALSE);
+ }
+
+ if (call_id != -1) {
+ reset_call(call_id);
+ pjsua_media_channel_deinit(call_id);
+ }
+
+ PJSUA_UNLOCK();
+ return status;
+}
+
+
+/*
+ * Initialize call settings based on account ID.
+ */
+PJ_DEF(void) pjsua_call_setting_default(pjsua_call_setting *opt)
+{
+ pj_assert(opt);
+
+ pj_bzero(opt, sizeof(*opt));
+ opt->flag = PJSUA_CALL_INCLUDE_DISABLED_MEDIA;
+ opt->aud_cnt = 1;
+
+#if defined(PJMEDIA_HAS_VIDEO) && (PJMEDIA_HAS_VIDEO != 0)
+ opt->vid_cnt = 1;
+ opt->req_keyframe_method = PJSUA_VID_REQ_KEYFRAME_SIP_INFO |
+ PJSUA_VID_REQ_KEYFRAME_RTCP_PLI;
+#endif
+}
+
+static pj_status_t apply_call_setting(pjsua_call *call,
+ const pjsua_call_setting *opt,
+ const pjmedia_sdp_session *rem_sdp)
+{
+ pj_assert(call);
+
+ if (!opt)
+ return PJ_SUCCESS;
+
+#if !PJMEDIA_HAS_VIDEO
+ pj_assert(opt->vid_cnt == 0);
+#endif
+
+ /* If call is established, reinit media channel */
+ if (call->inv && call->inv->state == PJSIP_INV_STATE_CONFIRMED) {
+ pjsua_call_setting old_opt;
+ pj_status_t status;
+
+ old_opt = call->opt;
+ call->opt = *opt;
+
+ /* Reinit media channel when media count is changed or we are the
+ * answerer (as remote offer may 'extremely' modify the existing
+ * media session, e.g: media type order).
+ */
+ if (rem_sdp ||
+ opt->aud_cnt!=old_opt.aud_cnt || opt->vid_cnt!=old_opt.vid_cnt)
+ {
+ pjsip_role_e role = rem_sdp? PJSIP_ROLE_UAS : PJSIP_ROLE_UAC;
+ status = pjsua_media_channel_init(call->index, role,
+ call->secure_level,
+ call->inv->pool_prov,
+ rem_sdp, NULL,
+ PJ_FALSE, NULL);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Error re-initializing media channel",
+ status);
+ return status;
+ }
+ }
+ } else {
+ call->opt = *opt;
+ }
+
+ return PJ_SUCCESS;
+}
+
+/*
+ * Make outgoing call to the specified URI using the specified account.
+ */
+PJ_DEF(pj_status_t) pjsua_call_make_call(pjsua_acc_id acc_id,
+ const pj_str_t *dest_uri,
+ const pjsua_call_setting *opt,
+ void *user_data,
+ const pjsua_msg_data *msg_data,
+ pjsua_call_id *p_call_id)
+{
+ pj_pool_t *tmp_pool = NULL;
+ pjsip_dialog *dlg = NULL;
+ pjsua_acc *acc;
+ pjsua_call *call;
+ int call_id = -1;
+ pj_str_t contact;
+ pj_status_t status;
+
+
+ /* Check that account is valid */
+ PJ_ASSERT_RETURN(acc_id>=0 || acc_id<(int)PJ_ARRAY_SIZE(pjsua_var.acc),
+ PJ_EINVAL);
+
+ /* Check arguments */
+ PJ_ASSERT_RETURN(dest_uri, PJ_EINVAL);
+
+ PJ_LOG(4,(THIS_FILE, "Making call with acc #%d to %.*s", acc_id,
+ (int)dest_uri->slen, dest_uri->ptr));
+
+ pj_log_push_indent();
+
+ PJSUA_LOCK();
+
+ /* Create sound port if none is instantiated, to check if sound device
+ * can be used. But only do this with the conference bridge, as with
+ * audio switchboard (i.e. APS-Direct), we can only open the sound
+ * device once the correct format has been known
+ */
+ if (!pjsua_var.is_mswitch && pjsua_var.snd_port==NULL &&
+ pjsua_var.null_snd==NULL && !pjsua_var.no_snd)
+ {
+ status = pjsua_set_snd_dev(pjsua_var.cap_dev, pjsua_var.play_dev);
+ if (status != PJ_SUCCESS)
+ goto on_error;
+ }
+
+ acc = &pjsua_var.acc[acc_id];
+ if (!acc->valid) {
+ pjsua_perror(THIS_FILE, "Unable to make call because account "
+ "is not valid", PJ_EINVALIDOP);
+ status = PJ_EINVALIDOP;
+ goto on_error;
+ }
+
+ /* Find free call slot. */
+ call_id = alloc_call_id();
+
+ if (call_id == PJSUA_INVALID_ID) {
+ pjsua_perror(THIS_FILE, "Error making call", PJ_ETOOMANY);
+ status = PJ_ETOOMANY;
+ goto on_error;
+ }
+
+ call = &pjsua_var.calls[call_id];
+
+ /* Associate session with account */
+ call->acc_id = acc_id;
+ call->call_hold_type = acc->cfg.call_hold_type;
+
+ /* Apply call setting */
+ status = apply_call_setting(call, opt, NULL);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Failed to apply call setting", status);
+ goto on_error;
+ }
+
+ /* Create temporary pool */
+ tmp_pool = pjsua_pool_create("tmpcall10", 512, 256);
+
+ /* Verify that destination URI is valid before calling
+ * pjsua_acc_create_uac_contact, or otherwise there
+ * a misleading "Invalid Contact URI" error will be printed
+ * when pjsua_acc_create_uac_contact() fails.
+ */
+ if (1) {
+ pjsip_uri *uri;
+ pj_str_t dup;
+
+ pj_strdup_with_null(tmp_pool, &dup, dest_uri);
+ uri = pjsip_parse_uri(tmp_pool, dup.ptr, dup.slen, 0);
+
+ if (uri == NULL) {
+ pjsua_perror(THIS_FILE, "Unable to make call",
+ PJSIP_EINVALIDREQURI);
+ status = PJSIP_EINVALIDREQURI;
+ goto on_error;
+ }
+ }
+
+ /* Mark call start time. */
+ pj_gettimeofday(&call->start_time);
+
+ /* Reset first response time */
+ call->res_time.sec = 0;
+
+ /* Create suitable Contact header unless a Contact header has been
+ * set in the account.
+ */
+ if (acc->contact.slen) {
+ contact = acc->contact;
+ } else {
+ status = pjsua_acc_create_uac_contact(tmp_pool, &contact,
+ acc_id, dest_uri);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Unable to generate Contact header",
+ status);
+ goto on_error;
+ }
+ }
+
+ /* Create outgoing dialog: */
+ status = pjsip_dlg_create_uac( pjsip_ua_instance(),
+ &acc->cfg.id, &contact,
+ dest_uri, dest_uri, &dlg);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Dialog creation failed", status);
+ goto on_error;
+ }
+
+ /* Increment the dialog's lock otherwise when invite session creation
+ * fails the dialog will be destroyed prematurely.
+ */
+ pjsip_dlg_inc_lock(dlg);
+
+ if (acc->cfg.allow_via_rewrite && acc->via_addr.host.slen > 0)
+ pjsip_dlg_set_via_sent_by(dlg, &acc->via_addr, acc->via_tp);
+
+ /* Calculate call's secure level */
+ call->secure_level = get_secure_level(acc_id, dest_uri);
+
+ /* Attach user data */
+ call->user_data = user_data;
+
+ /* Store variables required for the callback after the async
+ * media transport creation is completed.
+ */
+ if (msg_data) {
+ call->async_call.call_var.out_call.msg_data = pjsua_msg_data_clone(
+ dlg->pool, msg_data);
+ }
+ call->async_call.dlg = dlg;
+
+ /* Temporarily increment dialog session. Without this, dialog will be
+ * prematurely destroyed if dec_lock() is called on the dialog before
+ * the invite session is created.
+ */
+ pjsip_dlg_inc_session(dlg, &pjsua_var.mod);
+
+ /* Init media channel */
+ status = pjsua_media_channel_init(call->index, PJSIP_ROLE_UAC,
+ call->secure_level, dlg->pool,
+ NULL, NULL, PJ_TRUE,
+ &on_make_call_med_tp_complete);
+ if (status == PJ_SUCCESS) {
+ status = on_make_call_med_tp_complete(call->index, NULL);
+ if (status != PJ_SUCCESS)
+ goto on_error;
+ } else if (status != PJ_EPENDING) {
+ pjsua_perror(THIS_FILE, "Error initializing media channel", status);
+ pjsip_dlg_dec_session(dlg, &pjsua_var.mod);
+ goto on_error;
+ }
+
+ /* Done. */
+
+ if (p_call_id)
+ *p_call_id = call_id;
+
+ pjsip_dlg_dec_lock(dlg);
+ pj_pool_release(tmp_pool);
+ PJSUA_UNLOCK();
+
+ pj_log_pop_indent();
+
+ return PJ_SUCCESS;
+
+
+on_error:
+ if (dlg) {
+ /* This may destroy the dialog */
+ pjsip_dlg_dec_lock(dlg);
+ }
+
+ if (call_id != -1) {
+ reset_call(call_id);
+ pjsua_media_channel_deinit(call_id);
+ }
+
+ if (tmp_pool)
+ pj_pool_release(tmp_pool);
+ PJSUA_UNLOCK();
+
+ pj_log_pop_indent();
+ return status;
+}
+
+
+/* Get the NAT type information in remote's SDP */
+static void update_remote_nat_type(pjsua_call *call,
+ const pjmedia_sdp_session *sdp)
+{
+ const pjmedia_sdp_attr *xnat;
+
+ xnat = pjmedia_sdp_attr_find2(sdp->attr_count, sdp->attr, "X-nat", NULL);
+ if (xnat) {
+ call->rem_nat_type = (pj_stun_nat_type) (xnat->value.ptr[0] - '0');
+ } else {
+ call->rem_nat_type = PJ_STUN_NAT_TYPE_UNKNOWN;
+ }
+
+ PJ_LOG(5,(THIS_FILE, "Call %d: remote NAT type is %d (%s)", call->index,
+ call->rem_nat_type, pj_stun_get_nat_name(call->rem_nat_type)));
+}
+
+
+static pj_status_t process_incoming_call_replace(pjsua_call *call,
+ pjsip_dialog *replaced_dlg)
+{
+ pjsip_inv_session *replaced_inv;
+ struct pjsua_call *replaced_call;
+ pjsip_tx_data *tdata;
+ pj_status_t status;
+
+ /* Get the invite session in the dialog */
+ replaced_inv = pjsip_dlg_get_inv_session(replaced_dlg);
+
+ /* Get the replaced call instance */
+ replaced_call = (pjsua_call*) replaced_dlg->mod_data[pjsua_var.mod.id];
+
+ /* Notify application */
+ if (pjsua_var.ua_cfg.cb.on_call_replaced)
+ pjsua_var.ua_cfg.cb.on_call_replaced(replaced_call->index,
+ call->index);
+
+ PJ_LOG(4,(THIS_FILE, "Answering replacement call %d with 200/OK",
+ call->index));
+
+ /* Answer the new call with 200 response */
+ status = pjsip_inv_answer(call->inv, 200, NULL, NULL, &tdata);
+ if (status == PJ_SUCCESS)
+ status = pjsip_inv_send_msg(call->inv, tdata);
+
+ if (status != PJ_SUCCESS)
+ pjsua_perror(THIS_FILE, "Error answering session", status);
+
+ /* Note that inv may be invalid if 200/OK has caused error in
+ * starting the media.
+ */
+
+ PJ_LOG(4,(THIS_FILE, "Disconnecting replaced call %d",
+ replaced_call->index));
+
+ /* Disconnect replaced invite session */
+ status = pjsip_inv_end_session(replaced_inv, PJSIP_SC_GONE, NULL,
+ &tdata);
+ if (status == PJ_SUCCESS && tdata)
+ status = pjsip_inv_send_msg(replaced_inv, tdata);
+
+ if (status != PJ_SUCCESS)
+ pjsua_perror(THIS_FILE, "Error terminating session", status);
+
+ return status;
+}
+
+
+static void process_pending_call_answer(pjsua_call *call)
+{
+ struct call_answer *answer, *next;
+
+ answer = call->async_call.call_var.inc_call.answers.next;
+ while (answer != &call->async_call.call_var.inc_call.answers) {
+ next = answer->next;
+ pjsua_call_answer2(call->index, answer->opt, answer->code,
+ answer->reason, answer->msg_data);
+
+ /* Call might have been disconnected if application is answering
+ * with 200/OK and the media failed to start.
+ * See pjsua_call_answer() below.
+ */
+ if (!call->inv || !call->inv->pool_prov)
+ break;
+
+ pj_list_erase(answer);
+ answer = next;
+ }
+}
+
+
+/* Incoming call callback when media transport creation is completed. */
+static pj_status_t
+on_incoming_call_med_tp_complete(pjsua_call_id call_id,
+ const pjsua_med_tp_state_info *info)
+{
+ pjsua_call *call = &pjsua_var.calls[call_id];
+ const pjmedia_sdp_session *offer=NULL;
+ pjmedia_sdp_session *answer;
+ pjsip_tx_data *response = NULL;
+ unsigned options = 0;
+ int sip_err_code = (info? info->sip_err_code: 0);
+ pj_status_t status = (info? info->status: PJ_SUCCESS);
+
+ PJSUA_LOCK();
+
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Error initializing media channel", status);
+ goto on_return;
+ }
+
+ /* pjsua_media_channel_deinit() has been called. */
+ if (call->async_call.med_ch_deinit) {
+ pjsua_media_channel_deinit(call->index);
+ call->med_ch_cb = NULL;
+ PJSUA_UNLOCK();
+ return PJ_SUCCESS;
+ }
+
+ /* Get remote SDP offer (if any). */
+ if (call->inv->neg)
+ pjmedia_sdp_neg_get_neg_remote(call->inv->neg, &offer);
+
+ status = pjsua_media_channel_create_sdp(call_id,
+ call->async_call.dlg->pool,
+ offer, &answer, &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, answer);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Error setting local SDP", status);
+ sip_err_code = PJSIP_SC_NOT_ACCEPTABLE_HERE;
+ goto on_return;
+ }
+
+ /* Verify that we can handle the request. */
+ status = pjsip_inv_verify_request3(NULL,
+ call->inv->pool_prov, &options, offer,
+ answer, NULL, pjsua_var.endpt, &response);
+ if (status != PJ_SUCCESS) {
+ /*
+ * No we can't handle the incoming INVITE request.
+ */
+ sip_err_code = PJSIP_ERRNO_TO_SIP_STATUS(status);
+ goto on_return;
+ }
+
+on_return:
+ if (status != PJ_SUCCESS) {
+ /* 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.
+ */
+ if (call->inv->state > PJSIP_INV_STATE_NULL) {
+ pjsip_tx_data *tdata;
+ pj_status_t status_;
+
+ status_ = pjsip_inv_end_session(call->inv, sip_err_code, NULL,
+ &tdata);
+ if (status_ == PJ_SUCCESS && tdata)
+ status_ = pjsip_inv_send_msg(call->inv, tdata);
+ }
+
+ pjsua_media_channel_deinit(call->index);
+ }
+
+ /* Set the callback to NULL to indicate that the async operation
+ * has completed.
+ */
+ call->med_ch_cb = NULL;
+
+ /* Finish any pending process */
+ if (status == PJ_SUCCESS) {
+ if (call->async_call.call_var.inc_call.replaced_dlg) {
+ /* Process pending call replace */
+ pjsip_dialog *replaced_dlg =
+ call->async_call.call_var.inc_call.replaced_dlg;
+ process_incoming_call_replace(call, replaced_dlg);
+ } else {
+ /* Process pending call answers */
+ process_pending_call_answer(call);
+ }
+ }
+
+ PJSUA_UNLOCK();
+ return status;
+}
+
+
+/**
+ * Handle incoming INVITE request.
+ * Called by pjsua_core.c
+ */
+pj_bool_t pjsua_call_on_incoming(pjsip_rx_data *rdata)
+{
+ pj_str_t contact;
+ pjsip_dialog *dlg = pjsip_rdata_get_dlg(rdata);
+ pjsip_dialog *replaced_dlg = NULL;
+ pjsip_transaction *tsx = pjsip_rdata_get_tsx(rdata);
+ pjsip_msg *msg = rdata->msg_info.msg;
+ pjsip_tx_data *response = NULL;
+ unsigned options = 0;
+ pjsip_inv_session *inv = NULL;
+ int acc_id;
+ pjsua_call *call;
+ int call_id = -1;
+ int sip_err_code;
+ pjmedia_sdp_session *offer=NULL;
+ pj_status_t status;
+
+ /* Don't want to handle anything but INVITE */
+ if (msg->line.req.method.id != PJSIP_INVITE_METHOD)
+ return PJ_FALSE;
+
+ /* Don't want to handle anything that's already associated with
+ * existing dialog or transaction.
+ */
+ if (dlg || tsx)
+ return PJ_FALSE;
+
+ /* Don't want to accept the call if shutdown is in progress */
+ if (pjsua_var.thread_quit_flag) {
+ pjsip_endpt_respond_stateless(pjsua_var.endpt, rdata,
+ PJSIP_SC_TEMPORARILY_UNAVAILABLE, NULL,
+ NULL, NULL);
+ return PJ_TRUE;
+ }
+
+ PJ_LOG(4,(THIS_FILE, "Incoming %s", rdata->msg_info.info));
+ pj_log_push_indent();
+
+ PJSUA_LOCK();
+
+ /* Find free call slot. */
+ call_id = alloc_call_id();
+
+ if (call_id == PJSUA_INVALID_ID) {
+ pjsip_endpt_respond_stateless(pjsua_var.endpt, rdata,
+ PJSIP_SC_BUSY_HERE, NULL,
+ NULL, NULL);
+ PJ_LOG(2,(THIS_FILE,
+ "Unable to accept incoming call (too many calls)"));
+ goto on_return;
+ }
+
+ /* Clear call descriptor */
+ reset_call(call_id);
+
+ call = &pjsua_var.calls[call_id];
+
+ /* Mark call start time. */
+ pj_gettimeofday(&call->start_time);
+
+ /* Check INVITE request for Replaces header. If Replaces header is
+ * present, the function will make sure that we can handle the request.
+ */
+ status = pjsip_replaces_verify_request(rdata, &replaced_dlg, PJ_FALSE,
+ &response);
+ if (status != PJ_SUCCESS) {
+ /*
+ * Something wrong with the Replaces header.
+ */
+ if (response) {
+ pjsip_response_addr res_addr;
+
+ pjsip_get_response_addr(response->pool, rdata, &res_addr);
+ pjsip_endpt_send_response(pjsua_var.endpt, &res_addr, response,
+ NULL, NULL);
+
+ } else {
+
+ /* Respond with 500 (Internal Server Error) */
+ pjsip_endpt_respond_stateless(pjsua_var.endpt, rdata, 500, NULL,
+ NULL, NULL);
+ }
+
+ goto on_return;
+ }
+
+ /* If this INVITE request contains Replaces header, notify application
+ * about the request so that application can do subsequent checking
+ * if it wants to.
+ */
+ if (replaced_dlg != NULL &&
+ (pjsua_var.ua_cfg.cb.on_call_replace_request ||
+ pjsua_var.ua_cfg.cb.on_call_replace_request2))
+ {
+ pjsua_call *replaced_call;
+ int st_code = 200;
+ pj_str_t st_text = { "OK", 2 };
+
+ /* Get the replaced call instance */
+ replaced_call = (pjsua_call*) replaced_dlg->mod_data[pjsua_var.mod.id];
+
+ /* Copy call setting from the replaced call */
+ call->opt = replaced_call->opt;
+
+ /* Notify application */
+ if (pjsua_var.ua_cfg.cb.on_call_replace_request) {
+ pjsua_var.ua_cfg.cb.on_call_replace_request(replaced_call->index,
+ rdata,
+ &st_code, &st_text);
+ }
+
+ if (pjsua_var.ua_cfg.cb.on_call_replace_request2) {
+ pjsua_var.ua_cfg.cb.on_call_replace_request2(replaced_call->index,
+ rdata,
+ &st_code, &st_text,
+ &call->opt);
+ }
+
+ /* Must specify final response */
+ PJ_ASSERT_ON_FAIL(st_code >= 200, st_code = 200);
+
+ /* Check if application rejects this request. */
+ if (st_code >= 300) {
+
+ if (st_text.slen == 2)
+ st_text = *pjsip_get_status_text(st_code);
+
+ pjsip_endpt_respond(pjsua_var.endpt, NULL, rdata,
+ st_code, &st_text, NULL, NULL, NULL);
+ goto on_return;
+ }
+ }
+
+ /*
+ * Get which account is most likely to be associated with this incoming
+ * call. We need the account to find which contact URI to put for
+ * the call.
+ */
+ acc_id = call->acc_id = pjsua_acc_find_for_incoming(rdata);
+ call->call_hold_type = pjsua_var.acc[acc_id].cfg.call_hold_type;
+
+ /* Get call's secure level */
+ if (PJSIP_URI_SCHEME_IS_SIPS(rdata->msg_info.msg->line.req.uri))
+ call->secure_level = 2;
+ else if (PJSIP_TRANSPORT_IS_SECURE(rdata->tp_info.transport))
+ call->secure_level = 1;
+ else
+ call->secure_level = 0;
+
+ /* Parse SDP from incoming request */
+ if (rdata->msg_info.msg->body) {
+ pjsip_rdata_sdp_info *sdp_info;
+
+ sdp_info = pjsip_rdata_get_sdp_info(rdata);
+ offer = sdp_info->sdp;
+
+ status = sdp_info->sdp_err;
+ if (status==PJ_SUCCESS && sdp_info->sdp==NULL)
+ status = PJSIP_ERRNO_FROM_SIP_STATUS(PJSIP_SC_NOT_ACCEPTABLE);
+
+ if (status != PJ_SUCCESS) {
+ const pj_str_t reason = pj_str("Bad SDP");
+ pjsip_hdr hdr_list;
+ pjsip_warning_hdr *w;
+
+ pjsua_perror(THIS_FILE, "Bad SDP in incoming INVITE",
+ status);
+
+ w = pjsip_warning_hdr_create_from_status(rdata->tp_info.pool,
+ pjsip_endpt_name(pjsua_var.endpt),
+ status);
+ pj_list_init(&hdr_list);
+ pj_list_push_back(&hdr_list, w);
+
+ pjsip_endpt_respond(pjsua_var.endpt, NULL, rdata, 400,
+ &reason, &hdr_list, NULL, NULL);
+ goto on_return;
+ }
+
+ /* Do quick checks on SDP before passing it to transports. More elabore
+ * checks will be done in pjsip_inv_verify_request2() below.
+ */
+ if (offer->media_count==0) {
+ const pj_str_t reason = pj_str("Missing media in SDP");
+ pjsip_endpt_respond(pjsua_var.endpt, NULL, rdata, 400, &reason,
+ NULL, NULL, NULL);
+ goto on_return;
+ }
+
+ } else {
+ offer = NULL;
+ }
+
+ /* Verify that we can handle the request. */
+ options |= PJSIP_INV_SUPPORT_100REL;
+ options |= PJSIP_INV_SUPPORT_TIMER;
+ if (pjsua_var.acc[acc_id].cfg.require_100rel == PJSUA_100REL_MANDATORY)
+ options |= PJSIP_INV_REQUIRE_100REL;
+ if (pjsua_var.media_cfg.enable_ice)
+ options |= PJSIP_INV_SUPPORT_ICE;
+ if (pjsua_var.acc[acc_id].cfg.use_timer == PJSUA_SIP_TIMER_REQUIRED)
+ options |= PJSIP_INV_REQUIRE_TIMER;
+ else if (pjsua_var.acc[acc_id].cfg.use_timer == PJSUA_SIP_TIMER_ALWAYS)
+ options |= PJSIP_INV_ALWAYS_USE_TIMER;
+
+ status = pjsip_inv_verify_request2(rdata, &options, offer, NULL, NULL,
+ pjsua_var.endpt, &response);
+ if (status != PJ_SUCCESS) {
+
+ /*
+ * No we can't handle the incoming INVITE request.
+ */
+ if (response) {
+ pjsip_response_addr res_addr;
+
+ pjsip_get_response_addr(response->pool, rdata, &res_addr);
+ pjsip_endpt_send_response(pjsua_var.endpt, &res_addr, response,
+ NULL, NULL);
+
+ } else {
+ /* Respond with 500 (Internal Server Error) */
+ pjsip_endpt_respond(pjsua_var.endpt, NULL, rdata, 500, NULL,
+ NULL, NULL, NULL);
+ }
+
+ goto on_return;
+ }
+
+ /* Get suitable Contact header */
+ if (pjsua_var.acc[acc_id].contact.slen) {
+ contact = pjsua_var.acc[acc_id].contact;
+ } else {
+ status = pjsua_acc_create_uas_contact(rdata->tp_info.pool, &contact,
+ acc_id, rdata);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Unable to generate Contact header",
+ status);
+ pjsip_endpt_respond_stateless(pjsua_var.endpt, rdata, 500, NULL,
+ NULL, NULL);
+ goto on_return;
+ }
+ }
+
+ /* Create dialog: */
+ status = pjsip_dlg_create_uas( pjsip_ua_instance(), rdata,
+ &contact, &dlg);
+ if (status != PJ_SUCCESS) {
+ pjsip_endpt_respond_stateless(pjsua_var.endpt, rdata, 500, NULL,
+ NULL, NULL);
+ goto on_return;
+ }
+
+ if (pjsua_var.acc[acc_id].cfg.allow_via_rewrite &&
+ pjsua_var.acc[acc_id].via_addr.host.slen > 0)
+ {
+ pjsip_dlg_set_via_sent_by(dlg, &pjsua_var.acc[acc_id].via_addr,
+ pjsua_var.acc[acc_id].via_tp);
+ }
+
+ /* Set credentials */
+ if (pjsua_var.acc[acc_id].cred_cnt) {
+ pjsip_auth_clt_set_credentials(&dlg->auth_sess,
+ pjsua_var.acc[acc_id].cred_cnt,
+ pjsua_var.acc[acc_id].cred);
+ }
+
+ /* Set preference */
+ pjsip_auth_clt_set_prefs(&dlg->auth_sess,
+ &pjsua_var.acc[acc_id].cfg.auth_pref);
+
+ /* Disable Session Timers if not prefered and the incoming INVITE request
+ * did not require it.
+ */
+ if (pjsua_var.acc[acc_id].cfg.use_timer == PJSUA_SIP_TIMER_INACTIVE &&
+ (options & PJSIP_INV_REQUIRE_TIMER) == 0)
+ {
+ options &= ~(PJSIP_INV_SUPPORT_TIMER);
+ }
+
+ /* If 100rel is optional and UAC supports it, use it. */
+ if ((options & PJSIP_INV_REQUIRE_100REL)==0 &&
+ pjsua_var.acc[acc_id].cfg.require_100rel == PJSUA_100REL_OPTIONAL)
+ {
+ const pj_str_t token = { "100rel", 6};
+ pjsip_dialog_cap_status cap_status;
+
+ cap_status = pjsip_dlg_remote_has_cap(dlg, PJSIP_H_SUPPORTED, NULL,
+ &token);
+ if (cap_status == PJSIP_DIALOG_CAP_SUPPORTED)
+ options |= PJSIP_INV_REQUIRE_100REL;
+ }
+
+ /* Create invite session: */
+ status = pjsip_inv_create_uas( dlg, rdata, NULL, options, &inv);
+ if (status != PJ_SUCCESS) {
+ pjsip_hdr hdr_list;
+ pjsip_warning_hdr *w;
+
+ w = pjsip_warning_hdr_create_from_status(dlg->pool,
+ pjsip_endpt_name(pjsua_var.endpt),
+ status);
+ pj_list_init(&hdr_list);
+ pj_list_push_back(&hdr_list, w);
+
+ pjsip_dlg_respond(dlg, rdata, 500, NULL, &hdr_list, NULL);
+
+ /* Can't terminate dialog because transaction is in progress.
+ pjsip_dlg_terminate(dlg);
+ */
+ goto on_return;
+ }
+
+ /* If account is locked to specific transport, then lock dialog
+ * to this transport too.
+ */
+ if (pjsua_var.acc[acc_id].cfg.transport_id != PJSUA_INVALID_ID) {
+ pjsip_tpselector tp_sel;
+
+ pjsua_init_tpselector(pjsua_var.acc[acc_id].cfg.transport_id, &tp_sel);
+ pjsip_dlg_set_transport(dlg, &tp_sel);
+ }
+
+ /* Create and attach pjsua_var data to the dialog */
+ call->inv = inv;
+
+ /* Store variables required for the callback after the async
+ * media transport creation is completed.
+ */
+ call->async_call.dlg = dlg;
+ pj_list_init(&call->async_call.call_var.inc_call.answers);
+
+ /* Init media channel, only when there is offer or call replace request.
+ * For incoming call without SDP offer, media channel init will be done
+ * in pjsua_call_answer(), see ticket #1526.
+ */
+ if (offer || replaced_dlg) {
+ status = pjsua_media_channel_init(call->index, PJSIP_ROLE_UAS,
+ call->secure_level,
+ rdata->tp_info.pool,
+ offer,
+ &sip_err_code, PJ_TRUE,
+ &on_incoming_call_med_tp_complete);
+ if (status == PJ_SUCCESS) {
+ status = on_incoming_call_med_tp_complete(call_id, NULL);
+ if (status != PJ_SUCCESS) {
+ sip_err_code = PJSIP_SC_NOT_ACCEPTABLE;
+ /* Since the call invite's state is still PJSIP_INV_STATE_NULL,
+ * the invite session was not ended in
+ * on_incoming_call_med_tp_complete(), so we need to send
+ * a response message and terminate the invite here.
+ */
+ pjsip_dlg_respond(dlg, rdata, sip_err_code, NULL, NULL, NULL);
+ pjsip_inv_terminate(call->inv, sip_err_code, PJ_FALSE);
+ call->inv = NULL;
+ goto on_return;
+ }
+ } else if (status != PJ_EPENDING) {
+ pjsua_perror(THIS_FILE, "Error initializing media channel", status);
+ pjsip_dlg_respond(dlg, rdata, sip_err_code, NULL, NULL, NULL);
+ pjsip_inv_terminate(call->inv, sip_err_code, PJ_FALSE);
+ call->inv = NULL;
+ goto on_return;
+ }
+ }
+
+ /* Create answer */
+/*
+ status = pjsua_media_channel_create_sdp(call->index, rdata->tp_info.pool,
+ offer, &answer, &sip_err_code);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Error creating SDP answer", status);
+ pjsip_endpt_respond(pjsua_var.endpt, NULL, rdata,
+ sip_err_code, NULL, NULL, NULL, NULL);
+ goto on_return;
+ }
+*/
+
+ /* Init Session Timers */
+ status = pjsip_timer_init_session(inv,
+ &pjsua_var.acc[acc_id].cfg.timer_setting);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Session Timer init failed", status);
+ pjsip_dlg_respond(dlg, rdata, PJSIP_SC_INTERNAL_SERVER_ERROR, NULL, NULL, NULL);
+ pjsip_inv_terminate(inv, PJSIP_SC_INTERNAL_SERVER_ERROR, PJ_FALSE);
+
+ pjsua_media_channel_deinit(call->index);
+ call->inv = NULL;
+
+ goto on_return;
+ }
+
+ /* Update NAT type of remote endpoint, only when there is SDP in
+ * incoming INVITE!
+ */
+ if (pjsua_var.ua_cfg.nat_type_in_sdp && inv->neg &&
+ pjmedia_sdp_neg_get_state(inv->neg) > PJMEDIA_SDP_NEG_STATE_LOCAL_OFFER)
+ {
+ const pjmedia_sdp_session *remote_sdp;
+
+ if (pjmedia_sdp_neg_get_neg_remote(inv->neg, &remote_sdp)==PJ_SUCCESS)
+ update_remote_nat_type(call, remote_sdp);
+ }
+
+ /* Must answer with some response to initial INVITE. We'll do this before
+ * attaching the call to the invite session/dialog, so that the application
+ * will not get notification about this event (on another scenario, it is
+ * also possible that inv_send_msg() fails and causes the invite session to
+ * be disconnected. If we have the call attached at this time, this will
+ * cause the disconnection callback to be called before on_incoming_call()
+ * callback is called, which is not right).
+ */
+ status = pjsip_inv_initial_answer(inv, rdata,
+ 100, NULL, NULL, &response);
+ if (status != PJ_SUCCESS) {
+ if (response == NULL) {
+ pjsua_perror(THIS_FILE, "Unable to send answer to incoming INVITE",
+ status);
+ pjsip_dlg_respond(dlg, rdata, 500, NULL, NULL, NULL);
+ pjsip_inv_terminate(inv, 500, PJ_FALSE);
+ } else {
+ pjsip_inv_send_msg(inv, response);
+ pjsip_inv_terminate(inv, response->msg->line.status.code,
+ PJ_FALSE);
+ }
+ pjsua_media_channel_deinit(call->index);
+ call->inv = NULL;
+ goto on_return;
+
+ } else {
+ status = pjsip_inv_send_msg(inv, response);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Unable to send 100 response", status);
+ pjsua_media_channel_deinit(call->index);
+ call->inv = NULL;
+ goto on_return;
+ }
+ }
+
+ /* Only do this after sending 100/Trying (really! see the long comment
+ * above)
+ */
+ dlg->mod_data[pjsua_var.mod.id] = call;
+ inv->mod_data[pjsua_var.mod.id] = call;
+
+ ++pjsua_var.call_cnt;
+
+ /* Check if this request should replace existing call */
+ if (replaced_dlg) {
+ /* Process call replace. If the media channel init has been completed,
+ * just process now, otherwise, just queue the replaced dialog so
+ * it will be processed once the media channel async init is finished
+ * successfully.
+ */
+ if (call->med_ch_cb == NULL) {
+ process_incoming_call_replace(call, replaced_dlg);
+ } else {
+ call->async_call.call_var.inc_call.replaced_dlg = replaced_dlg;
+ }
+ } else {
+ /* Notify application if on_incoming_call() is overriden,
+ * otherwise hangup the call with 480
+ */
+ if (pjsua_var.ua_cfg.cb.on_incoming_call) {
+ pjsua_var.ua_cfg.cb.on_incoming_call(acc_id, call_id, rdata);
+ } else {
+ pjsua_call_hangup(call_id, PJSIP_SC_TEMPORARILY_UNAVAILABLE,
+ NULL, NULL);
+ }
+ }
+
+
+ /* This INVITE request has been handled. */
+on_return:
+ pj_log_pop_indent();
+ PJSUA_UNLOCK();
+ return PJ_TRUE;
+}
+
+
+
+/*
+ * Check if the specified call has active INVITE session and the INVITE
+ * session has not been disconnected.
+ */
+PJ_DEF(pj_bool_t) pjsua_call_is_active(pjsua_call_id call_id)
+{
+ PJ_ASSERT_RETURN(call_id>=0 && call_id<(int)pjsua_var.ua_cfg.max_calls,
+ PJ_EINVAL);
+ return pjsua_var.calls[call_id].inv != NULL &&
+ pjsua_var.calls[call_id].inv->state != PJSIP_INV_STATE_DISCONNECTED;
+}
+
+
+/* Acquire lock to the specified call_id */
+pj_status_t acquire_call(const char *title,
+ pjsua_call_id call_id,
+ pjsua_call **p_call,
+ pjsip_dialog **p_dlg)
+{
+ unsigned retry;
+ pjsua_call *call = NULL;
+ pj_bool_t has_pjsua_lock = PJ_FALSE;
+ pj_status_t status = PJ_SUCCESS;
+ pj_time_val time_start, timeout;
+
+ pj_gettimeofday(&time_start);
+ timeout.sec = 0;
+ timeout.msec = PJSUA_ACQUIRE_CALL_TIMEOUT;
+ pj_time_val_normalize(&timeout);
+
+ for (retry=0; ; ++retry) {
+
+ if (retry % 10 == 9) {
+ pj_time_val dtime;
+
+ pj_gettimeofday(&dtime);
+ PJ_TIME_VAL_SUB(dtime, time_start);
+ if (!PJ_TIME_VAL_LT(dtime, timeout))
+ break;
+ }
+
+ has_pjsua_lock = PJ_FALSE;
+
+ status = PJSUA_TRY_LOCK();
+ if (status != PJ_SUCCESS) {
+ pj_thread_sleep(retry/10);
+ continue;
+ }
+
+ has_pjsua_lock = PJ_TRUE;
+ call = &pjsua_var.calls[call_id];
+
+ if (call->inv == NULL) {
+ PJSUA_UNLOCK();
+ PJ_LOG(3,(THIS_FILE, "Invalid call_id %d in %s", call_id, title));
+ return PJSIP_ESESSIONTERMINATED;
+ }
+
+ status = pjsip_dlg_try_inc_lock(call->inv->dlg);
+ if (status != PJ_SUCCESS) {
+ PJSUA_UNLOCK();
+ pj_thread_sleep(retry/10);
+ continue;
+ }
+
+ PJSUA_UNLOCK();
+
+ break;
+ }
+
+ if (status != PJ_SUCCESS) {
+ if (has_pjsua_lock == PJ_FALSE)
+ PJ_LOG(1,(THIS_FILE, "Timed-out trying to acquire PJSUA mutex "
+ "(possibly system has deadlocked) in %s",
+ title));
+ else
+ PJ_LOG(1,(THIS_FILE, "Timed-out trying to acquire dialog mutex "
+ "(possibly system has deadlocked) in %s",
+ title));
+ return PJ_ETIMEDOUT;
+ }
+
+ *p_call = call;
+ *p_dlg = call->inv->dlg;
+
+ return PJ_SUCCESS;
+}
+
+
+/*
+ * Obtain detail information about the specified call.
+ */
+PJ_DEF(pj_status_t) pjsua_call_get_info( pjsua_call_id call_id,
+ pjsua_call_info *info)
+{
+ pjsua_call *call;
+ pjsip_dialog *dlg;
+ unsigned mi;
+
+ PJ_ASSERT_RETURN(call_id>=0 && call_id<(int)pjsua_var.ua_cfg.max_calls,
+ PJ_EINVAL);
+
+ pj_bzero(info, sizeof(*info));
+
+ /* Use PJSUA_LOCK() instead of acquire_call():
+ * https://trac.pjsip.org/repos/ticket/1371
+ */
+ PJSUA_LOCK();
+
+ call = &pjsua_var.calls[call_id];
+ dlg = (call->inv ? call->inv->dlg : call->async_call.dlg);
+ if (!dlg) {
+ PJSUA_UNLOCK();
+ return PJSIP_ESESSIONTERMINATED;
+ }
+
+ /* id and role */
+ info->id = call_id;
+ info->role = dlg->role;
+ info->acc_id = call->acc_id;
+
+ /* local info */
+ info->local_info.ptr = info->buf_.local_info;
+ pj_strncpy(&info->local_info, &dlg->local.info_str,
+ sizeof(info->buf_.local_info));
+
+ /* local contact */
+ info->local_contact.ptr = info->buf_.local_contact;
+ info->local_contact.slen = pjsip_uri_print(PJSIP_URI_IN_CONTACT_HDR,
+ dlg->local.contact->uri,
+ info->local_contact.ptr,
+ sizeof(info->buf_.local_contact));
+
+ /* remote info */
+ info->remote_info.ptr = info->buf_.remote_info;
+ pj_strncpy(&info->remote_info, &dlg->remote.info_str,
+ sizeof(info->buf_.remote_info));
+
+ /* remote contact */
+ if (dlg->remote.contact) {
+ int len;
+ info->remote_contact.ptr = info->buf_.remote_contact;
+ len = pjsip_uri_print(PJSIP_URI_IN_CONTACT_HDR,
+ dlg->remote.contact->uri,
+ info->remote_contact.ptr,
+ sizeof(info->buf_.remote_contact));
+ if (len < 0) len = 0;
+ info->remote_contact.slen = len;
+ } else {
+ info->remote_contact.slen = 0;
+ }
+
+ /* call id */
+ info->call_id.ptr = info->buf_.call_id;
+ pj_strncpy(&info->call_id, &dlg->call_id->id,
+ sizeof(info->buf_.call_id));
+
+ /* call setting */
+ pj_memcpy(&info->setting, &call->opt, sizeof(call->opt));
+
+ /* state, state_text */
+ if (call->inv) {
+ info->state = call->inv->state;
+ } else if (call->async_call.dlg && call->last_code==0) {
+ info->state = PJSIP_INV_STATE_NULL;
+ } else {
+ info->state = PJSIP_INV_STATE_DISCONNECTED;
+ }
+ info->state_text = pj_str((char*)pjsip_inv_state_name(info->state));
+
+ /* If call is disconnected, set the last_status from the cause code */
+ if (call->inv && call->inv->state >= PJSIP_INV_STATE_DISCONNECTED) {
+ /* last_status, last_status_text */
+ info->last_status = call->inv->cause;
+
+ info->last_status_text.ptr = info->buf_.last_status_text;
+ pj_strncpy(&info->last_status_text, &call->inv->cause_text,
+ sizeof(info->buf_.last_status_text));
+ } else {
+ /* last_status, last_status_text */
+ info->last_status = call->last_code;
+
+ info->last_status_text.ptr = info->buf_.last_status_text;
+ pj_strncpy(&info->last_status_text, &call->last_text,
+ sizeof(info->buf_.last_status_text));
+ }
+
+ /* Audio & video count offered by remote */
+ info->rem_offerer = call->rem_offerer;
+ if (call->rem_offerer) {
+ info->rem_aud_cnt = call->rem_aud_cnt;
+ info->rem_vid_cnt = call->rem_vid_cnt;
+ }
+
+ /* Build array of active media info */
+ info->media_cnt = 0;
+ for (mi=0; mi < call->med_cnt &&
+ info->media_cnt < PJ_ARRAY_SIZE(info->media); ++mi)
+ {
+ pjsua_call_media *call_med = &call->media[mi];
+
+ info->media[info->media_cnt].index = mi;
+ info->media[info->media_cnt].status = call_med->state;
+ info->media[info->media_cnt].dir = call_med->dir;
+ info->media[info->media_cnt].type = call_med->type;
+
+ if (call_med->type == PJMEDIA_TYPE_AUDIO) {
+ info->media[info->media_cnt].stream.aud.conf_slot =
+ call_med->strm.a.conf_slot;
+ } else if (call_med->type == PJMEDIA_TYPE_VIDEO) {
+ pjmedia_vid_dev_index cap_dev = PJMEDIA_VID_INVALID_DEV;
+
+ info->media[info->media_cnt].stream.vid.win_in =
+ call_med->strm.v.rdr_win_id;
+
+ if (call_med->strm.v.cap_win_id != PJSUA_INVALID_ID) {
+ cap_dev = call_med->strm.v.cap_dev;
+ }
+ info->media[info->media_cnt].stream.vid.cap_dev = cap_dev;
+ } else {
+ continue;
+ }
+ ++info->media_cnt;
+ }
+
+ if (call->audio_idx != -1) {
+ info->media_status = call->media[call->audio_idx].state;
+ info->media_dir = call->media[call->audio_idx].dir;
+ info->conf_slot = call->media[call->audio_idx].strm.a.conf_slot;
+ }
+
+ /* Build array of provisional media info */
+ info->prov_media_cnt = 0;
+ for (mi=0; mi < call->med_prov_cnt &&
+ info->prov_media_cnt < PJ_ARRAY_SIZE(info->prov_media); ++mi)
+ {
+ pjsua_call_media *call_med = &call->media_prov[mi];
+
+ info->prov_media[info->prov_media_cnt].index = mi;
+ info->prov_media[info->prov_media_cnt].status = call_med->state;
+ info->prov_media[info->prov_media_cnt].dir = call_med->dir;
+ info->prov_media[info->prov_media_cnt].type = call_med->type;
+ if (call_med->type == PJMEDIA_TYPE_AUDIO) {
+ info->prov_media[info->prov_media_cnt].stream.aud.conf_slot =
+ call_med->strm.a.conf_slot;
+ } else if (call_med->type == PJMEDIA_TYPE_VIDEO) {
+ pjmedia_vid_dev_index cap_dev = PJMEDIA_VID_INVALID_DEV;
+
+ info->prov_media[info->prov_media_cnt].stream.vid.win_in =
+ call_med->strm.v.rdr_win_id;
+
+ if (call_med->strm.v.cap_win_id != PJSUA_INVALID_ID) {
+ cap_dev = call_med->strm.v.cap_dev;
+ }
+ info->prov_media[info->prov_media_cnt].stream.vid.cap_dev=cap_dev;
+ } else {
+ continue;
+ }
+ ++info->prov_media_cnt;
+ }
+
+ /* calculate duration */
+ if (info->state >= PJSIP_INV_STATE_DISCONNECTED) {
+
+ info->total_duration = call->dis_time;
+ PJ_TIME_VAL_SUB(info->total_duration, call->start_time);
+
+ if (call->conn_time.sec) {
+ info->connect_duration = call->dis_time;
+ PJ_TIME_VAL_SUB(info->connect_duration, call->conn_time);
+ }
+
+ } else if (info->state == PJSIP_INV_STATE_CONFIRMED) {
+
+ pj_gettimeofday(&info->total_duration);
+ PJ_TIME_VAL_SUB(info->total_duration, call->start_time);
+
+ pj_gettimeofday(&info->connect_duration);
+ PJ_TIME_VAL_SUB(info->connect_duration, call->conn_time);
+
+ } else {
+ pj_gettimeofday(&info->total_duration);
+ PJ_TIME_VAL_SUB(info->total_duration, call->start_time);
+ }
+
+ PJSUA_UNLOCK();
+
+ return PJ_SUCCESS;
+}
+
+/*
+ * Check if call remote peer support the specified capability.
+ */
+PJ_DEF(pjsip_dialog_cap_status) pjsua_call_remote_has_cap(
+ pjsua_call_id call_id,
+ int htype,
+ const pj_str_t *hname,
+ const pj_str_t *token)
+{
+ pjsua_call *call;
+ pjsip_dialog *dlg;
+ pj_status_t status;
+ pjsip_dialog_cap_status cap_status;
+
+ status = acquire_call("pjsua_call_peer_has_cap()", call_id, &call, &dlg);
+ if (status != PJ_SUCCESS)
+ return PJSIP_DIALOG_CAP_UNKNOWN;
+
+ cap_status = pjsip_dlg_remote_has_cap(dlg, htype, hname, token);
+
+ pjsip_dlg_dec_lock(dlg);
+
+ return cap_status;
+}
+
+
+/*
+ * Attach application specific data to the call.
+ */
+PJ_DEF(pj_status_t) pjsua_call_set_user_data( pjsua_call_id call_id,
+ void *user_data)
+{
+ PJ_ASSERT_RETURN(call_id>=0 && call_id<(int)pjsua_var.ua_cfg.max_calls,
+ PJ_EINVAL);
+ pjsua_var.calls[call_id].user_data = user_data;
+
+ return PJ_SUCCESS;
+}
+
+
+/*
+ * Get user data attached to the call.
+ */
+PJ_DEF(void*) pjsua_call_get_user_data(pjsua_call_id call_id)
+{
+ PJ_ASSERT_RETURN(call_id>=0 && call_id<(int)pjsua_var.ua_cfg.max_calls,
+ NULL);
+ return pjsua_var.calls[call_id].user_data;
+}
+
+
+/*
+ * Get remote's NAT type.
+ */
+PJ_DEF(pj_status_t) pjsua_call_get_rem_nat_type(pjsua_call_id call_id,
+ pj_stun_nat_type *p_type)
+{
+ PJ_ASSERT_RETURN(call_id>=0 && call_id<(int)pjsua_var.ua_cfg.max_calls,
+ PJ_EINVAL);
+ PJ_ASSERT_RETURN(p_type != NULL, PJ_EINVAL);
+
+ *p_type = pjsua_var.calls[call_id].rem_nat_type;
+ return PJ_SUCCESS;
+}
+
+
+/*
+ * Get media transport info for the specified media index.
+ */
+PJ_DEF(pj_status_t)
+pjsua_call_get_med_transport_info(pjsua_call_id call_id,
+ unsigned med_idx,
+ pjmedia_transport_info *t)
+{
+ pjsua_call *call;
+ pjsua_call_media *call_med;
+ pj_status_t status;
+
+ PJ_ASSERT_RETURN(call_id>=0 && call_id<(int)pjsua_var.ua_cfg.max_calls,
+ PJ_EINVAL);
+ PJ_ASSERT_RETURN(t, PJ_EINVAL);
+
+ PJSUA_LOCK();
+
+ call = &pjsua_var.calls[call_id];
+
+ if (med_idx >= call->med_cnt) {
+ PJSUA_UNLOCK();
+ return PJ_EINVAL;
+ }
+
+ call_med = &call->media[med_idx];
+
+ pjmedia_transport_info_init(t);
+ status = pjmedia_transport_get_info(call_med->tp, t);
+
+ PJSUA_UNLOCK();
+ return status;
+}
+
+
+/* Media channel init callback for pjsua_call_answer(). */
+static pj_status_t
+on_answer_call_med_tp_complete(pjsua_call_id call_id,
+ const pjsua_med_tp_state_info *info)
+{
+ pjsua_call *call = &pjsua_var.calls[call_id];
+ pjmedia_sdp_session *sdp;
+ int sip_err_code = (info? info->sip_err_code: 0);
+ pj_status_t status = (info? info->status: PJ_SUCCESS);
+
+ PJSUA_LOCK();
+
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Error initializing media channel", status);
+ goto on_return;
+ }
+
+ /* pjsua_media_channel_deinit() has been called. */
+ if (call->async_call.med_ch_deinit) {
+ pjsua_media_channel_deinit(call->index);
+ call->med_ch_cb = 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;
+ }
+
+ status = pjsip_inv_set_local_sdp(call->inv, sdp);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Error setting local SDP", status);
+ sip_err_code = PJSIP_SC_NOT_ACCEPTABLE_HERE;
+ goto on_return;
+ }
+
+on_return:
+ if (status != PJ_SUCCESS) {
+ /* 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.
+ */
+ if (call->inv->state > PJSIP_INV_STATE_NULL) {
+ pjsip_tx_data *tdata;
+ pj_status_t status_;
+
+ status_ = pjsip_inv_end_session(call->inv, sip_err_code, NULL,
+ &tdata);
+ if (status_ == PJ_SUCCESS && tdata)
+ status_ = pjsip_inv_send_msg(call->inv, tdata);
+ }
+
+ pjsua_media_channel_deinit(call->index);
+ }
+
+ /* Set the callback to NULL to indicate that the async operation
+ * has completed.
+ */
+ call->med_ch_cb = NULL;
+
+ /* Finish any pending process */
+ if (status == PJ_SUCCESS) {
+ /* Process pending call answers */
+ process_pending_call_answer(call);
+ }
+
+ PJSUA_UNLOCK();
+ return status;
+}
+
+
+/*
+ * Send response to incoming INVITE request.
+ */
+PJ_DEF(pj_status_t) pjsua_call_answer( pjsua_call_id call_id,
+ unsigned code,
+ const pj_str_t *reason,
+ const pjsua_msg_data *msg_data)
+{
+ return pjsua_call_answer2(call_id, NULL, code, reason, msg_data);
+}
+
+
+/*
+ * 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)
+{
+ pjsua_call *call;
+ pjsip_dialog *dlg = NULL;
+ pjsip_tx_data *tdata;
+ pj_status_t status;
+
+ PJ_ASSERT_RETURN(call_id>=0 && call_id<(int)pjsua_var.ua_cfg.max_calls,
+ PJ_EINVAL);
+
+ PJ_LOG(4,(THIS_FILE, "Answering call %d: code=%d", call_id, code));
+ pj_log_push_indent();
+
+ status = acquire_call("pjsua_call_answer()", call_id, &call, &dlg);
+ if (status != PJ_SUCCESS)
+ goto on_return;
+
+ /* Apply call setting, only if status code is 1xx or 2xx. */
+ if (opt && code < 300) {
+ /* Check if it has not been set previously or it is different to
+ * the previous one.
+ */
+ if (!call->opt_inited) {
+ call->opt_inited = PJ_TRUE;
+ apply_call_setting(call, opt, NULL);
+ } else if (pj_memcmp(opt, &call->opt, sizeof(*opt)) != 0) {
+ /* Warn application about call setting inconsistency */
+ PJ_LOG(2,(THIS_FILE, "The call setting changes is ignored."));
+ }
+ }
+
+ PJSUA_LOCK();
+
+ /* Ticket #1526: When the incoming call contains no SDP offer, the media
+ * channel may have not been initialized at this stage. The media channel
+ * will be initialized here (along with SDP local offer generation) when
+ * 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
+ */
+ if (!call->med_ch_cb &&
+ (call->opt_inited || (code==183 && code/100==2)) &&
+ (!call->inv->neg ||
+ pjmedia_sdp_neg_get_state(call->inv->neg) ==
+ PJMEDIA_SDP_NEG_STATE_NULL))
+ {
+ /* Mark call setting as initialized as it is just about to be used
+ * for initializing the media channel.
+ */
+ call->opt_inited = PJ_TRUE;
+
+ status = pjsua_media_channel_init(call->index, PJSIP_ROLE_UAC,
+ call->secure_level,
+ dlg->pool,
+ NULL, NULL, PJ_TRUE,
+ &on_answer_call_med_tp_complete);
+ if (status == PJ_SUCCESS) {
+ status = on_answer_call_med_tp_complete(call->index, NULL);
+ if (status != PJ_SUCCESS) {
+ PJSUA_UNLOCK();
+ goto on_return;
+ }
+ } else if (status != PJ_EPENDING) {
+ PJSUA_UNLOCK();
+ pjsua_perror(THIS_FILE, "Error initializing media channel", status);
+ goto on_return;
+ }
+ }
+
+ /* If media transport creation is not yet completed, we will answer
+ * the call in the media transport creation callback instead.
+ */
+ if (call->med_ch_cb) {
+ struct call_answer *answer;
+
+ PJ_LOG(4,(THIS_FILE, "Pending answering call %d upon completion "
+ "of media transport", call_id));
+
+ answer = PJ_POOL_ZALLOC_T(call->inv->pool_prov, struct call_answer);
+ answer->code = code;
+ if (opt) {
+ answer->opt = PJ_POOL_ZALLOC_T(call->inv->pool_prov,
+ pjsua_call_setting);
+ *answer->opt = *opt;
+ }
+ if (reason) {
+ pj_strdup(call->inv->pool_prov, answer->reason, reason);
+ }
+ if (msg_data) {
+ answer->msg_data = pjsua_msg_data_clone(call->inv->pool_prov,
+ msg_data);
+ }
+ pj_list_push_back(&call->async_call.call_var.inc_call.answers,
+ answer);
+
+ PJSUA_UNLOCK();
+ if (dlg) pjsip_dlg_dec_lock(dlg);
+ pj_log_pop_indent();
+ return status;
+ }
+
+ PJSUA_UNLOCK();
+
+ if (call->res_time.sec == 0)
+ pj_gettimeofday(&call->res_time);
+
+ if (reason && reason->slen == 0)
+ reason = NULL;
+
+ /* Create response message */
+ status = pjsip_inv_answer(call->inv, code, reason, NULL, &tdata);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Error creating response",
+ status);
+ goto on_return;
+ }
+
+ /* Call might have been disconnected if application is answering with
+ * 200/OK and the media failed to start.
+ */
+ if (call->inv == NULL)
+ goto on_return;
+
+ /* Add additional headers etc */
+ pjsua_process_msg_data( tdata, msg_data);
+
+ /* Send the message */
+ status = pjsip_inv_send_msg(call->inv, tdata);
+ if (status != PJ_SUCCESS)
+ pjsua_perror(THIS_FILE, "Error sending response",
+ status);
+
+on_return:
+ if (dlg) pjsip_dlg_dec_lock(dlg);
+ pj_log_pop_indent();
+ return status;
+}
+
+
+/*
+ * Hangup call by using method that is appropriate according to the
+ * call state.
+ */
+PJ_DEF(pj_status_t) pjsua_call_hangup(pjsua_call_id call_id,
+ unsigned code,
+ const pj_str_t *reason,
+ const pjsua_msg_data *msg_data)
+{
+ pjsua_call *call;
+ pjsip_dialog *dlg = NULL;
+ pj_status_t status;
+ pjsip_tx_data *tdata;
+
+
+ if (call_id<0 || call_id>=(int)pjsua_var.ua_cfg.max_calls) {
+ PJ_LOG(1,(THIS_FILE, "pjsua_call_hangup(): invalid call id %d",
+ call_id));
+ }
+
+ PJ_ASSERT_RETURN(call_id>=0 && call_id<(int)pjsua_var.ua_cfg.max_calls,
+ PJ_EINVAL);
+
+ PJ_LOG(4,(THIS_FILE, "Call %d hanging up: code=%d..", call_id, code));
+ pj_log_push_indent();
+
+ status = acquire_call("pjsua_call_hangup()", call_id, &call, &dlg);
+ if (status != PJ_SUCCESS)
+ goto on_return;
+
+ if (code==0) {
+ if (call->inv->state == PJSIP_INV_STATE_CONFIRMED)
+ code = PJSIP_SC_OK;
+ else if (call->inv->role == PJSIP_ROLE_UAS)
+ code = PJSIP_SC_DECLINE;
+ else
+ code = PJSIP_SC_REQUEST_TERMINATED;
+ }
+
+ status = pjsip_inv_end_session(call->inv, code, reason, &tdata);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE,
+ "Failed to create end session message",
+ status);
+ goto on_return;
+ }
+
+ /* pjsip_inv_end_session may return PJ_SUCCESS with NULL
+ * as p_tdata when INVITE transaction has not been answered
+ * with any provisional responses.
+ */
+ if (tdata == NULL)
+ goto on_return;
+
+ /* Add additional headers etc */
+ pjsua_process_msg_data( tdata, msg_data);
+
+ /* Send the message */
+ status = pjsip_inv_send_msg(call->inv, tdata);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE,
+ "Failed to send end session message",
+ status);
+ goto on_return;
+ }
+
+ /* Stop lock codec timer, if it is active */
+ if (call->lock_codec.reinv_timer.id) {
+ pjsip_endpt_cancel_timer(pjsua_var.endpt,
+ &call->lock_codec.reinv_timer);
+ call->lock_codec.reinv_timer.id = PJ_FALSE;
+ }
+
+on_return:
+ if (dlg) pjsip_dlg_dec_lock(dlg);
+ pj_log_pop_indent();
+ return status;
+}
+
+
+/*
+ * Accept or reject redirection.
+ */
+PJ_DEF(pj_status_t) pjsua_call_process_redirect( pjsua_call_id call_id,
+ pjsip_redirect_op cmd)
+{
+ pjsua_call *call;
+ pjsip_dialog *dlg;
+ pj_status_t status;
+
+ PJ_ASSERT_RETURN(call_id>=0 && call_id<(int)pjsua_var.ua_cfg.max_calls,
+ PJ_EINVAL);
+
+ status = acquire_call("pjsua_call_process_redirect()", call_id,
+ &call, &dlg);
+ if (status != PJ_SUCCESS)
+ return status;
+
+ status = pjsip_inv_process_redirect(call->inv, cmd, NULL);
+
+ pjsip_dlg_dec_lock(dlg);
+
+ return status;
+}
+
+
+/*
+ * Put the specified call on hold.
+ */
+PJ_DEF(pj_status_t) pjsua_call_set_hold(pjsua_call_id call_id,
+ const pjsua_msg_data *msg_data)
+{
+ pjmedia_sdp_session *sdp;
+ pjsua_call *call;
+ pjsip_dialog *dlg = NULL;
+ pjsip_tx_data *tdata;
+ pj_status_t status;
+
+ PJ_ASSERT_RETURN(call_id>=0 && call_id<(int)pjsua_var.ua_cfg.max_calls,
+ PJ_EINVAL);
+
+ PJ_LOG(4,(THIS_FILE, "Putting call %d on hold", call_id));
+ pj_log_push_indent();
+
+ status = acquire_call("pjsua_call_set_hold()", call_id, &call, &dlg);
+ if (status != PJ_SUCCESS)
+ goto on_return;
+
+ if (call->inv->state != PJSIP_INV_STATE_CONFIRMED) {
+ PJ_LOG(3,(THIS_FILE, "Can not hold call that is not confirmed"));
+ status = PJSIP_ESESSIONSTATE;
+ goto on_return;
+ }
+
+ status = create_sdp_of_call_hold(call, &sdp);
+ if (status != PJ_SUCCESS)
+ goto on_return;
+
+ /* Create re-INVITE with new offer */
+ status = pjsip_inv_reinvite( call->inv, NULL, sdp, &tdata);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Unable to create re-INVITE", status);
+ goto on_return;
+ }
+
+ /* Add additional headers etc */
+ pjsua_process_msg_data( tdata, msg_data);
+
+ /* Record the tx_data to keep track the operation */
+ call->hold_msg = (void*) tdata;
+
+ /* Send the request */
+ status = pjsip_inv_send_msg( call->inv, tdata);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Unable to send re-INVITE", status);
+ call->hold_msg = NULL;
+ goto on_return;
+ }
+
+ /* Set flag that local put the call on hold */
+ call->local_hold = PJ_TRUE;
+
+on_return:
+ if (dlg) pjsip_dlg_dec_lock(dlg);
+ pj_log_pop_indent();
+ return status;
+}
+
+
+/*
+ * Send re-INVITE (to release hold).
+ */
+PJ_DEF(pj_status_t) pjsua_call_reinvite( pjsua_call_id call_id,
+ unsigned options,
+ const pjsua_msg_data *msg_data)
+{
+ pjsua_call *call;
+ pjsip_dialog *dlg = NULL;
+ pj_status_t status;
+
+ status = acquire_call("pjsua_call_reinvite()", call_id, &call, &dlg);
+ if (status != PJ_SUCCESS)
+ goto on_return;
+
+ if (options != call->opt.flag)
+ call->opt.flag = options;
+
+ status = pjsua_call_reinvite2(call_id, NULL, msg_data);
+
+on_return:
+ if (dlg) pjsip_dlg_dec_lock(dlg);
+ return status;
+}
+
+
+/*
+ * Send re-INVITE (to release hold).
+ */
+PJ_DEF(pj_status_t) pjsua_call_reinvite2(pjsua_call_id call_id,
+ const pjsua_call_setting *opt,
+ const pjsua_msg_data *msg_data)
+{
+ pjmedia_sdp_session *sdp;
+ pj_str_t *new_contact = NULL;
+ pjsip_tx_data *tdata;
+ pjsua_call *call;
+ pjsip_dialog *dlg = 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 re-INVITE on call %d", call_id));
+ pj_log_push_indent();
+
+ status = acquire_call("pjsua_call_reinvite2()", call_id, &call, &dlg);
+ if (status != PJ_SUCCESS)
+ goto on_return;
+
+ if (call->inv->state != PJSIP_INV_STATE_CONFIRMED) {
+ PJ_LOG(3,(THIS_FILE, "Can not re-INVITE call that is not confirmed"));
+ status = PJSIP_ESESSIONSTATE;
+ goto on_return;
+ }
+
+ status = apply_call_setting(call, opt, NULL);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Failed to apply call setting", status);
+ goto on_return;
+ }
+
+ /* Create SDP */
+ if (call->local_hold && (call->opt.flag & PJSUA_CALL_UNHOLD)==0) {
+ status = create_sdp_of_call_hold(call, &sdp);
+ } else {
+ status = pjsua_media_channel_create_sdp(call->index,
+ call->inv->pool_prov,
+ NULL, &sdp, NULL);
+ call->local_hold = PJ_FALSE;
+ }
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Unable to get SDP from media endpoint",
+ status);
+ goto on_return;
+ }
+
+ if ((call->opt.flag & PJSUA_CALL_UPDATE_CONTACT) &
+ pjsua_acc_is_valid(call->acc_id))
+ {
+ new_contact = &pjsua_var.acc[call->acc_id].contact;
+ }
+
+ /* Create re-INVITE with new offer */
+ status = pjsip_inv_reinvite( call->inv, new_contact, sdp, &tdata);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Unable to create re-INVITE", status);
+ goto on_return;
+ }
+
+ /* Add additional headers etc */
+ pjsua_process_msg_data( tdata, msg_data);
+
+ /* Send the request */
+ status = pjsip_inv_send_msg( call->inv, tdata);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Unable to send re-INVITE", status);
+ goto on_return;
+ }
+
+on_return:
+ if (dlg) pjsip_dlg_dec_lock(dlg);
+ pj_log_pop_indent();
+ return status;
+}
+
+
+/*
+ * Send UPDATE request.
+ */
+PJ_DEF(pj_status_t) pjsua_call_update( pjsua_call_id call_id,
+ unsigned options,
+ const pjsua_msg_data *msg_data)
+{
+ pjsua_call *call;
+ pjsip_dialog *dlg = NULL;
+ pj_status_t status;
+
+ status = acquire_call("pjsua_call_update()", call_id, &call, &dlg);
+ if (status != PJ_SUCCESS)
+ goto on_return;
+
+ if (options != call->opt.flag)
+ call->opt.flag = options;
+
+ status = pjsua_call_update2(call_id, NULL, msg_data);
+
+on_return:
+ if (dlg) pjsip_dlg_dec_lock(dlg);
+ return status;
+}
+
+
+/*
+ * Send UPDATE request.
+ */
+PJ_DEF(pj_status_t) pjsua_call_update2(pjsua_call_id call_id,
+ const pjsua_call_setting *opt,
+ const pjsua_msg_data *msg_data)
+{
+ pjmedia_sdp_session *sdp;
+ pj_str_t *new_contact = NULL;
+ pjsip_tx_data *tdata;
+ pjsua_call *call;
+ pjsip_dialog *dlg = 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 UPDATE on call %d", call_id));
+ pj_log_push_indent();
+
+ status = acquire_call("pjsua_call_update2()", call_id, &call, &dlg);
+ if (status != PJ_SUCCESS)
+ goto on_return;
+
+ status = apply_call_setting(call, opt, NULL);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Failed to apply call setting", status);
+ goto on_return;
+ }
+
+ /* Create SDP */
+ status = pjsua_media_channel_create_sdp(call->index,
+ call->inv->pool_prov,
+ NULL, &sdp, NULL);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Unable to get SDP from media endpoint",
+ status);
+ goto on_return;
+ }
+
+ if ((call->opt.flag & PJSUA_CALL_UPDATE_CONTACT) &
+ pjsua_acc_is_valid(call->acc_id))
+ {
+ new_contact = &pjsua_var.acc[call->acc_id].contact;
+ }
+
+ /* Create UPDATE with new offer */
+ status = pjsip_inv_update(call->inv, new_contact, sdp, &tdata);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Unable to create UPDATE request", status);
+ goto on_return;
+ }
+
+ /* Add additional headers etc */
+ pjsua_process_msg_data( tdata, msg_data);
+
+ /* Send the request */
+ status = pjsip_inv_send_msg( call->inv, tdata);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Unable to send UPDATE request", status);
+ goto on_return;
+ }
+
+ call->local_hold = PJ_FALSE;
+
+on_return:
+ if (dlg) pjsip_dlg_dec_lock(dlg);
+ pj_log_pop_indent();
+ return status;
+}
+
+
+/*
+ * Initiate call transfer to the specified address.
+ */
+PJ_DEF(pj_status_t) pjsua_call_xfer( pjsua_call_id call_id,
+ const pj_str_t *dest,
+ const pjsua_msg_data *msg_data)
+{
+ pjsip_evsub *sub;
+ pjsip_tx_data *tdata;
+ pjsua_call *call;
+ pjsip_dialog *dlg = NULL;
+ pjsip_generic_string_hdr *gs_hdr;
+ const pj_str_t str_ref_by = { "Referred-By", 11 };
+ struct pjsip_evsub_user xfer_cb;
+ pj_status_t status;
+
+
+ PJ_ASSERT_RETURN(call_id>=0 && call_id<(int)pjsua_var.ua_cfg.max_calls &&
+ dest, PJ_EINVAL);
+
+ PJ_LOG(4,(THIS_FILE, "Transfering call %d to %.*s", call_id,
+ (int)dest->slen, dest->ptr));
+ pj_log_push_indent();
+
+ status = acquire_call("pjsua_call_xfer()", call_id, &call, &dlg);
+ if (status != PJ_SUCCESS)
+ goto on_return;
+
+ /* Create xfer client subscription. */
+ pj_bzero(&xfer_cb, sizeof(xfer_cb));
+ xfer_cb.on_evsub_state = &xfer_client_on_evsub_state;
+
+ status = pjsip_xfer_create_uac(call->inv->dlg, &xfer_cb, &sub);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Unable to create xfer", status);
+ goto on_return;
+ }
+
+ /* Associate this call with the client subscription */
+ pjsip_evsub_set_mod_data(sub, pjsua_var.mod.id, call);
+
+ /*
+ * Create REFER request.
+ */
+ status = pjsip_xfer_initiate(sub, dest, &tdata);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Unable to create REFER request", status);
+ goto on_return;
+ }
+
+ /* Add Referred-By header */
+ gs_hdr = pjsip_generic_string_hdr_create(tdata->pool, &str_ref_by,
+ &dlg->local.info_str);
+ pjsip_msg_add_hdr(tdata->msg, (pjsip_hdr*)gs_hdr);
+
+
+ /* Add additional headers etc */
+ pjsua_process_msg_data( tdata, msg_data);
+
+ /* Send. */
+ status = pjsip_xfer_send_request(sub, tdata);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Unable to send REFER request", status);
+ goto on_return;
+ }
+
+ /* For simplicity (that's what this program is intended to be!),
+ * leave the original invite session as it is. More advanced application
+ * may want to hold the INVITE, or terminate the invite, or whatever.
+ */
+on_return:
+ if (dlg) pjsip_dlg_dec_lock(dlg);
+ pj_log_pop_indent();
+ return status;
+
+}
+
+
+/*
+ * Initiate attended call transfer to the specified address.
+ */
+PJ_DEF(pj_status_t) pjsua_call_xfer_replaces( pjsua_call_id call_id,
+ pjsua_call_id dest_call_id,
+ unsigned options,
+ const pjsua_msg_data *msg_data)
+{
+ pjsua_call *dest_call;
+ pjsip_dialog *dest_dlg;
+ char str_dest_buf[PJSIP_MAX_URL_SIZE*2];
+ pj_str_t str_dest;
+ int len;
+ pjsip_uri *uri;
+ pj_status_t status;
+
+
+ PJ_ASSERT_RETURN(call_id>=0 && call_id<(int)pjsua_var.ua_cfg.max_calls,
+ PJ_EINVAL);
+ PJ_ASSERT_RETURN(dest_call_id>=0 &&
+ dest_call_id<(int)pjsua_var.ua_cfg.max_calls,
+ PJ_EINVAL);
+
+ PJ_LOG(4,(THIS_FILE, "Transfering call %d replacing with call %d",
+ call_id, dest_call_id));
+ pj_log_push_indent();
+
+ status = acquire_call("pjsua_call_xfer_replaces()", dest_call_id,
+ &dest_call, &dest_dlg);
+ if (status != PJ_SUCCESS) {
+ pj_log_pop_indent();
+ return status;
+ }
+
+ /*
+ * Create REFER destination URI with Replaces field.
+ */
+
+ /* Make sure we have sufficient buffer's length */
+ PJ_ASSERT_ON_FAIL(dest_dlg->remote.info_str.slen +
+ dest_dlg->call_id->id.slen +
+ dest_dlg->remote.info->tag.slen +
+ dest_dlg->local.info->tag.slen + 32
+ < (long)sizeof(str_dest_buf),
+ { status=PJSIP_EURITOOLONG; goto on_error; });
+
+ /* Print URI */
+ str_dest_buf[0] = '<';
+ str_dest.slen = 1;
+
+ uri = (pjsip_uri*) pjsip_uri_get_uri(dest_dlg->remote.info->uri);
+ len = pjsip_uri_print(PJSIP_URI_IN_REQ_URI, uri,
+ str_dest_buf+1, sizeof(str_dest_buf)-1);
+ if (len < 0) {
+ status = PJSIP_EURITOOLONG;
+ goto on_error;
+ }
+
+ str_dest.slen += len;
+
+
+ /* Build the URI */
+ len = pj_ansi_snprintf(str_dest_buf + str_dest.slen,
+ sizeof(str_dest_buf) - str_dest.slen,
+ "?%s"
+ "Replaces=%.*s"
+ "%%3Bto-tag%%3D%.*s"
+ "%%3Bfrom-tag%%3D%.*s>",
+ ((options&PJSUA_XFER_NO_REQUIRE_REPLACES) ?
+ "" : "Require=replaces&"),
+ (int)dest_dlg->call_id->id.slen,
+ dest_dlg->call_id->id.ptr,
+ (int)dest_dlg->remote.info->tag.slen,
+ dest_dlg->remote.info->tag.ptr,
+ (int)dest_dlg->local.info->tag.slen,
+ dest_dlg->local.info->tag.ptr);
+
+ PJ_ASSERT_ON_FAIL(len > 0 && len <= (int)sizeof(str_dest_buf)-str_dest.slen,
+ { status=PJSIP_EURITOOLONG; goto on_error; });
+
+ str_dest.ptr = str_dest_buf;
+ str_dest.slen += len;
+
+ pjsip_dlg_dec_lock(dest_dlg);
+
+ status = pjsua_call_xfer(call_id, &str_dest, msg_data);
+
+ pj_log_pop_indent();
+ return status;
+
+on_error:
+ if (dest_dlg) pjsip_dlg_dec_lock(dest_dlg);
+ pj_log_pop_indent();
+ return status;
+}
+
+
+/**
+ * Send instant messaging inside INVITE session.
+ */
+PJ_DEF(pj_status_t) pjsua_call_send_im( pjsua_call_id call_id,
+ const pj_str_t *mime_type,
+ const pj_str_t *content,
+ const pjsua_msg_data *msg_data,
+ void *user_data)
+{
+ pjsua_call *call;
+ pjsip_dialog *dlg = NULL;
+ const pj_str_t mime_text_plain = pj_str("text/plain");
+ pjsip_media_type ctype;
+ pjsua_im_data *im_data;
+ pjsip_tx_data *tdata;
+ pj_status_t status;
+
+ PJ_ASSERT_RETURN(call_id>=0 && call_id<(int)pjsua_var.ua_cfg.max_calls,
+ PJ_EINVAL);
+
+ PJ_LOG(4,(THIS_FILE, "Call %d sending %d bytes MESSAGE..",
+ call_id, (int)content->slen));
+ pj_log_push_indent();
+
+ status = acquire_call("pjsua_call_send_im()", call_id, &call, &dlg);
+ if (status != PJ_SUCCESS)
+ goto on_return;
+
+ /* Set default media type if none is specified */
+ if (mime_type == NULL) {
+ mime_type = &mime_text_plain;
+ }
+
+ /* Create request message. */
+ status = pjsip_dlg_create_request( call->inv->dlg, &pjsip_message_method,
+ -1, &tdata);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Unable to create MESSAGE request", status);
+ goto on_return;
+ }
+
+ /* Add accept header. */
+ pjsip_msg_add_hdr( tdata->msg,
+ (pjsip_hdr*)pjsua_im_create_accept(tdata->pool));
+
+ /* Parse MIME type */
+ pjsua_parse_media_type(tdata->pool, mime_type, &ctype);
+
+ /* Create "text/plain" message body. */
+ tdata->msg->body = pjsip_msg_body_create( tdata->pool, &ctype.type,
+ &ctype.subtype, content);
+ if (tdata->msg->body == NULL) {
+ pjsua_perror(THIS_FILE, "Unable to create msg body", PJ_ENOMEM);
+ pjsip_tx_data_dec_ref(tdata);
+ goto on_return;
+ }
+
+ /* Add additional headers etc */
+ pjsua_process_msg_data( tdata, msg_data);
+
+ /* Create IM data and attach to the request. */
+ im_data = PJ_POOL_ZALLOC_T(tdata->pool, pjsua_im_data);
+ im_data->acc_id = call->acc_id;
+ im_data->call_id = call_id;
+ im_data->to = call->inv->dlg->remote.info_str;
+ pj_strdup_with_null(tdata->pool, &im_data->body, content);
+ im_data->user_data = user_data;
+
+
+ /* Send the request. */
+ status = pjsip_dlg_send_request( call->inv->dlg, tdata,
+ pjsua_var.mod.id, im_data);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Unable to send MESSAGE request", status);
+ goto on_return;
+ }
+
+on_return:
+ if (dlg) pjsip_dlg_dec_lock(dlg);
+ pj_log_pop_indent();
+ return status;
+}
+
+
+/*
+ * Send IM typing indication inside INVITE session.
+ */
+PJ_DEF(pj_status_t) pjsua_call_send_typing_ind( pjsua_call_id call_id,
+ pj_bool_t is_typing,
+ const pjsua_msg_data*msg_data)
+{
+ pjsua_call *call;
+ pjsip_dialog *dlg = NULL;
+ pjsip_tx_data *tdata;
+ pj_status_t status;
+
+ PJ_ASSERT_RETURN(call_id>=0 && call_id<(int)pjsua_var.ua_cfg.max_calls,
+ PJ_EINVAL);
+
+ PJ_LOG(4,(THIS_FILE, "Call %d sending typing indication..",
+ call_id));
+ pj_log_push_indent();
+
+ status = acquire_call("pjsua_call_send_typing_ind", call_id, &call, &dlg);
+ if (status != PJ_SUCCESS)
+ goto on_return;
+
+ /* Create request message. */
+ status = pjsip_dlg_create_request( call->inv->dlg, &pjsip_message_method,
+ -1, &tdata);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Unable to create MESSAGE request", status);
+ goto on_return;
+ }
+
+ /* Create "application/im-iscomposing+xml" msg body. */
+ tdata->msg->body = pjsip_iscomposing_create_body(tdata->pool, is_typing,
+ NULL, NULL, -1);
+
+ /* Add additional headers etc */
+ pjsua_process_msg_data( tdata, msg_data);
+
+ /* Send the request. */
+ status = pjsip_dlg_send_request( call->inv->dlg, tdata, -1, NULL);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Unable to send MESSAGE request", status);
+ goto on_return;
+ }
+
+on_return:
+ if (dlg) pjsip_dlg_dec_lock(dlg);
+ pj_log_pop_indent();
+ return status;
+}
+
+
+/*
+ * Send arbitrary request.
+ */
+PJ_DEF(pj_status_t) pjsua_call_send_request(pjsua_call_id call_id,
+ const pj_str_t *method_str,
+ const pjsua_msg_data *msg_data)
+{
+ pjsua_call *call;
+ pjsip_dialog *dlg = NULL;
+ pjsip_method method;
+ pjsip_tx_data *tdata;
+ pj_status_t status;
+
+ PJ_ASSERT_RETURN(call_id>=0 && call_id<(int)pjsua_var.ua_cfg.max_calls,
+ PJ_EINVAL);
+
+ PJ_LOG(4,(THIS_FILE, "Call %d sending %.*s request..",
+ call_id, (int)method_str->slen, method_str->ptr));
+ pj_log_push_indent();
+
+ status = acquire_call("pjsua_call_send_request", call_id, &call, &dlg);
+ if (status != PJ_SUCCESS)
+ goto on_return;
+
+ /* Init method */
+ pjsip_method_init_np(&method, (pj_str_t*)method_str);
+
+ /* Create request message. */
+ status = pjsip_dlg_create_request( call->inv->dlg, &method, -1, &tdata);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Unable to create request", status);
+ goto on_return;
+ }
+
+ /* Add additional headers etc */
+ pjsua_process_msg_data( tdata, msg_data);
+
+ /* Send the request. */
+ status = pjsip_dlg_send_request( call->inv->dlg, tdata, -1, NULL);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Unable to send request", status);
+ goto on_return;
+ }
+
+on_return:
+ if (dlg) pjsip_dlg_dec_lock(dlg);
+ pj_log_pop_indent();
+ return status;
+}
+
+
+/*
+ * Terminate all calls.
+ */
+PJ_DEF(void) pjsua_call_hangup_all(void)
+{
+ unsigned i;
+
+ PJ_LOG(4,(THIS_FILE, "Hangup all calls.."));
+ pj_log_push_indent();
+
+ // This may deadlock, see https://trac.pjsip.org/repos/ticket/1305
+ //PJSUA_LOCK();
+
+ for (i=0; i<pjsua_var.ua_cfg.max_calls; ++i) {
+ if (pjsua_var.calls[i].inv)
+ pjsua_call_hangup(i, 0, NULL, NULL);
+ }
+
+ //PJSUA_UNLOCK();
+ pj_log_pop_indent();
+}
+
+
+/* Proto */
+static pj_status_t perform_lock_codec(pjsua_call *call);
+
+/* Timer callback to send re-INVITE or UPDATE to lock codec */
+static void reinv_timer_cb(pj_timer_heap_t *th,
+ pj_timer_entry *entry)
+{
+ pjsua_call_id call_id = (pjsua_call_id)(pj_size_t)entry->user_data;
+ pjsip_dialog *dlg;
+ pjsua_call *call;
+ pj_status_t status;
+
+ PJ_UNUSED_ARG(th);
+
+ pjsua_var.calls[call_id].lock_codec.reinv_timer.id = PJ_FALSE;
+
+ status = acquire_call("reinv_timer_cb()", call_id, &call, &dlg);
+ if (status != PJ_SUCCESS)
+ return;
+
+ status = perform_lock_codec(call);
+
+ pjsip_dlg_dec_lock(dlg);
+}
+
+
+/* Check if the specified format can be skipped in counting codecs */
+static pj_bool_t is_non_av_fmt(const pjmedia_sdp_media *m,
+ const pj_str_t *fmt)
+{
+ const pj_str_t STR_TEL = {"telephone-event", 15};
+ unsigned pt;
+
+ pt = pj_strtoul(fmt);
+
+ /* Check for comfort noise */
+ if (pt == PJMEDIA_RTP_PT_CN)
+ return PJ_TRUE;
+
+ /* Dynamic PT, check the format name */
+ if (pt >= 96) {
+ pjmedia_sdp_attr *a;
+ pjmedia_sdp_rtpmap rtpmap;
+
+ /* Get the format name */
+ a = pjmedia_sdp_attr_find2(m->attr_count, m->attr, "rtpmap", fmt);
+ if (a && pjmedia_sdp_attr_get_rtpmap(a, &rtpmap)==PJ_SUCCESS) {
+ /* Check for telephone-event */
+ if (pj_stricmp(&rtpmap.enc_name, &STR_TEL)==0)
+ return PJ_TRUE;
+ } else {
+ /* Invalid SDP, should not reach here */
+ pj_assert(!"SDP should have been validated!");
+ return PJ_TRUE;
+ }
+ }
+
+ return PJ_FALSE;
+}
+
+
+/* Send re-INVITE or UPDATE with new SDP offer to select only one codec
+ * out of several codecs presented by callee in his answer.
+ */
+static pj_status_t perform_lock_codec(pjsua_call *call)
+{
+ const pj_str_t STR_UPDATE = {"UPDATE", 6};
+ const pjmedia_sdp_session *local_sdp = NULL, *new_sdp;
+ unsigned i;
+ pj_bool_t rem_can_update;
+ pj_bool_t need_lock_codec = PJ_FALSE;
+ pjsip_tx_data *tdata;
+ pj_status_t status;
+
+ PJ_ASSERT_RETURN(call->lock_codec.reinv_timer.id==PJ_FALSE,
+ PJ_EINVALIDOP);
+
+ /* Verify if another SDP negotiation is in progress, e.g: session timer
+ * or another re-INVITE.
+ */
+ if (call->inv==NULL || call->inv->neg==NULL ||
+ pjmedia_sdp_neg_get_state(call->inv->neg)!=PJMEDIA_SDP_NEG_STATE_DONE)
+ {
+ return PJMEDIA_SDPNEG_EINSTATE;
+ }
+
+ /* Don't do this if call is disconnecting! */
+ if (call->inv->state > PJSIP_INV_STATE_CONFIRMED ||
+ call->inv->cause >= 200)
+ {
+ return PJ_EINVALIDOP;
+ }
+
+ /* Verify if another SDP negotiation has been completed by comparing
+ * the SDP version.
+ */
+ status = pjmedia_sdp_neg_get_active_local(call->inv->neg, &local_sdp);
+ if (status != PJ_SUCCESS)
+ return status;
+ if (local_sdp->origin.version > call->lock_codec.sdp_ver)
+ return PJMEDIA_SDP_EINVER;
+
+ PJ_LOG(3, (THIS_FILE, "Updating media session to use only one codec.."));
+
+ /* Update the new offer so it contains only a codec. Note that formats
+ * order in the offer should have been matched to the answer, so we can
+ * just directly update the offer without looking-up the answer.
+ */
+ new_sdp = pjmedia_sdp_session_clone(call->inv->pool_prov, local_sdp);
+
+ for (i = 0; i < call->med_cnt; ++i) {
+ unsigned j = 0, codec_cnt = 0;
+ const pjmedia_sdp_media *ref_m;
+ pjmedia_sdp_media *m;
+ pjsua_call_media *call_med = &call->media[i];
+
+ /* Verify if media is deactivated */
+ if (call_med->state == PJSUA_CALL_MEDIA_NONE ||
+ call_med->state == PJSUA_CALL_MEDIA_ERROR ||
+ call_med->dir == PJMEDIA_DIR_NONE)
+ {
+ continue;
+ }
+
+ ref_m = local_sdp->media[i];
+ m = new_sdp->media[i];
+
+ /* Verify that media must be active. */
+ pj_assert(ref_m->desc.port);
+
+ while (j < m->desc.fmt_count) {
+ pjmedia_sdp_attr *a;
+ pj_str_t *fmt = &m->desc.fmt[j];
+
+ if (is_non_av_fmt(m, fmt) || (++codec_cnt == 1)) {
+ ++j;
+ continue;
+ }
+
+ /* Remove format */
+ a = pjmedia_sdp_attr_find2(m->attr_count, m->attr, "rtpmap", fmt);
+ if (a) pjmedia_sdp_attr_remove(&m->attr_count, m->attr, a);
+ a = pjmedia_sdp_attr_find2(m->attr_count, m->attr, "fmtp", fmt);
+ if (a) pjmedia_sdp_attr_remove(&m->attr_count, m->attr, a);
+ pj_array_erase(m->desc.fmt, sizeof(m->desc.fmt[0]),
+ m->desc.fmt_count, j);
+ --m->desc.fmt_count;
+ }
+
+ need_lock_codec |= (ref_m->desc.fmt_count > m->desc.fmt_count);
+ }
+
+ /* Last check if SDP trully needs to be updated. It is possible that OA
+ * negotiations have completed and SDP has changed but we didn't
+ * increase the SDP version (should not happen!).
+ */
+ if (!need_lock_codec)
+ return PJ_SUCCESS;
+
+ /* Send UPDATE or re-INVITE */
+ rem_can_update = pjsip_dlg_remote_has_cap(call->inv->dlg,
+ PJSIP_H_ALLOW,
+ NULL, &STR_UPDATE) ==
+ PJSIP_DIALOG_CAP_SUPPORTED;
+ if (rem_can_update) {
+ status = pjsip_inv_update(call->inv, NULL, new_sdp, &tdata);
+ } else {
+ status = pjsip_inv_reinvite(call->inv, NULL, new_sdp, &tdata);
+ }
+
+ if (status==PJ_EINVALIDOP &&
+ ++call->lock_codec.retry_cnt <= LOCK_CODEC_MAX_RETRY)
+ {
+ /* Ups, let's reschedule again */
+ pj_time_val delay = {0, LOCK_CODEC_RETRY_INTERVAL};
+ pj_time_val_normalize(&delay);
+ call->lock_codec.reinv_timer.id = PJ_TRUE;
+ pjsip_endpt_schedule_timer(pjsua_var.endpt,
+ &call->lock_codec.reinv_timer, &delay);
+ return status;
+ } else if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Error creating UPDATE/re-INVITE to lock codec",
+ status);
+ return status;
+ }
+
+ /* Send the UPDATE/re-INVITE request */
+ status = pjsip_inv_send_msg(call->inv, tdata);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Error sending UPDATE/re-INVITE in lock codec",
+ status);
+ return status;
+ }
+
+ return status;
+}
+
+/* Check if remote answerer has given us more than one codecs. If so,
+ * create another offer with one codec only to lock down the codec.
+ */
+static pj_status_t lock_codec(pjsua_call *call)
+{
+ pjsip_inv_session *inv = call->inv;
+ const pjmedia_sdp_session *local_sdp, *remote_sdp;
+ pj_time_val delay = {0, 0};
+ const pj_str_t st_update = {"UPDATE", 6};
+ unsigned i;
+ pj_bool_t has_mult_fmt = PJ_FALSE;
+ pj_status_t status;
+
+ /* Stop lock codec timer, if it is active */
+ if (call->lock_codec.reinv_timer.id) {
+ pjsip_endpt_cancel_timer(pjsua_var.endpt,
+ &call->lock_codec.reinv_timer);
+ call->lock_codec.reinv_timer.id = PJ_FALSE;
+ }
+
+ /* Skip this if we are the answerer */
+ if (!inv->neg || !pjmedia_sdp_neg_was_answer_remote(inv->neg)) {
+ return PJ_SUCCESS;
+ }
+
+ /* Delay this when the SDP negotiation done in call state EARLY and
+ * remote does not support UPDATE method.
+ */
+ if (inv->state == PJSIP_INV_STATE_EARLY &&
+ pjsip_dlg_remote_has_cap(inv->dlg, PJSIP_H_ALLOW, NULL, &st_update)!=
+ PJSIP_DIALOG_CAP_SUPPORTED)
+ {
+ call->lock_codec.pending = PJ_TRUE;
+ return PJ_SUCCESS;
+ }
+
+ status = pjmedia_sdp_neg_get_active_local(inv->neg, &local_sdp);
+ if (status != PJ_SUCCESS)
+ return status;
+ status = pjmedia_sdp_neg_get_active_remote(inv->neg, &remote_sdp);
+ if (status != PJ_SUCCESS)
+ return status;
+
+ /* Find multiple codecs answer in all media */
+ for (i = 0; i < call->med_cnt; ++i) {
+ pjsua_call_media *call_med = &call->media[i];
+ const pjmedia_sdp_media *rem_m, *loc_m;
+ unsigned codec_cnt = 0;
+
+ /* Skip this if the media is inactive or error */
+ if (call_med->state == PJSUA_CALL_MEDIA_NONE ||
+ call_med->state == PJSUA_CALL_MEDIA_ERROR ||
+ call_med->dir == PJMEDIA_DIR_NONE)
+ {
+ continue;
+ }
+
+ /* Remote may answer with less media lines. */
+ if (i >= remote_sdp->media_count)
+ continue;
+
+ rem_m = remote_sdp->media[i];
+ loc_m = local_sdp->media[i];
+
+ /* Verify that media must be active. */
+ pj_assert(loc_m->desc.port && rem_m->desc.port);
+
+ /* Count the formats in the answer. */
+ if (rem_m->desc.fmt_count==1) {
+ codec_cnt = 1;
+ } else {
+ unsigned j;
+ for (j=0; j<rem_m->desc.fmt_count && codec_cnt <= 1; ++j) {
+ if (!is_non_av_fmt(rem_m, &rem_m->desc.fmt[j]))
+ ++codec_cnt;
+ }
+ }
+
+ if (codec_cnt > 1) {
+ has_mult_fmt = PJ_TRUE;
+ break;
+ }
+ }
+
+ /* Each media in the answer already contains single codec. */
+ if (!has_mult_fmt) {
+ call->lock_codec.retry_cnt = 0;
+ return PJ_SUCCESS;
+ }
+
+ /* Remote keeps answering with multiple codecs, let's just give up
+ * locking codec to avoid infinite retry loop.
+ */
+ if (++call->lock_codec.retry_cnt > LOCK_CODEC_MAX_RETRY)
+ return PJ_SUCCESS;
+
+ PJ_LOG(4, (THIS_FILE, "Got answer with multiple codecs, scheduling "
+ "updating media session to use only one codec.."));
+
+ call->lock_codec.sdp_ver = local_sdp->origin.version;
+
+ /* Can't send UPDATE or re-INVITE now, so just schedule it immediately.
+ * See: https://trac.pjsip.org/repos/ticket/1149
+ */
+ pj_timer_entry_init(&call->lock_codec.reinv_timer, PJ_TRUE,
+ (void*)(pj_size_t)call->index,
+ &reinv_timer_cb);
+ pjsip_endpt_schedule_timer(pjsua_var.endpt,
+ &call->lock_codec.reinv_timer, &delay);
+
+ return PJ_SUCCESS;
+}
+
+/*
+ * This callback receives notification from invite session when the
+ * session state has changed.
+ */
+static void pjsua_call_on_state_changed(pjsip_inv_session *inv,
+ pjsip_event *e)
+{
+ pjsua_call *call;
+
+ pj_log_push_indent();
+
+ call = (pjsua_call*) inv->dlg->mod_data[pjsua_var.mod.id];
+
+ if (!call) {
+ pj_log_pop_indent();
+ return;
+ }
+
+
+ /* Get call times */
+ switch (inv->state) {
+ case PJSIP_INV_STATE_EARLY:
+ case PJSIP_INV_STATE_CONNECTING:
+ if (call->res_time.sec == 0)
+ pj_gettimeofday(&call->res_time);
+ call->last_code = (pjsip_status_code)
+ e->body.tsx_state.tsx->status_code;
+ pj_strncpy(&call->last_text,
+ &e->body.tsx_state.tsx->status_text,
+ sizeof(call->last_text_buf_));
+ break;
+ case PJSIP_INV_STATE_CONFIRMED:
+ pj_gettimeofday(&call->conn_time);
+
+ /* See if lock codec was pended as media update was done in the
+ * EARLY state and remote does not support UPDATE.
+ */
+ if (call->lock_codec.pending) {
+ pj_status_t status;
+ status = lock_codec(call);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Unable to lock codec", status);
+ }
+ call->lock_codec.pending = PJ_FALSE;
+ }
+ break;
+ case PJSIP_INV_STATE_DISCONNECTED:
+ pj_gettimeofday(&call->dis_time);
+ if (call->res_time.sec == 0)
+ pj_gettimeofday(&call->res_time);
+ if (e->type == PJSIP_EVENT_TSX_STATE &&
+ e->body.tsx_state.tsx->status_code > call->last_code)
+ {
+ call->last_code = (pjsip_status_code)
+ e->body.tsx_state.tsx->status_code;
+ pj_strncpy(&call->last_text,
+ &e->body.tsx_state.tsx->status_text,
+ sizeof(call->last_text_buf_));
+ } else {
+ call->last_code = PJSIP_SC_REQUEST_TERMINATED;
+ pj_strncpy(&call->last_text,
+ pjsip_get_status_text(call->last_code),
+ sizeof(call->last_text_buf_));
+ }
+
+ /* Stop lock codec timer, if it is active */
+ if (call->lock_codec.reinv_timer.id) {
+ pjsip_endpt_cancel_timer(pjsua_var.endpt,
+ &call->lock_codec.reinv_timer);
+ call->lock_codec.reinv_timer.id = PJ_FALSE;
+ }
+ break;
+ default:
+ call->last_code = (pjsip_status_code)
+ e->body.tsx_state.tsx->status_code;
+ pj_strncpy(&call->last_text,
+ &e->body.tsx_state.tsx->status_text,
+ sizeof(call->last_text_buf_));
+ break;
+ }
+
+ /* If this is an outgoing INVITE that was created because of
+ * REFER/transfer, send NOTIFY to transferer.
+ */
+ if (call->xfer_sub && e->type==PJSIP_EVENT_TSX_STATE) {
+ int st_code = -1;
+ pjsip_evsub_state ev_state = PJSIP_EVSUB_STATE_ACTIVE;
+
+
+ switch (call->inv->state) {
+ case PJSIP_INV_STATE_NULL:
+ case PJSIP_INV_STATE_CALLING:
+ /* Do nothing */
+ break;
+
+ case PJSIP_INV_STATE_EARLY:
+ case PJSIP_INV_STATE_CONNECTING:
+ st_code = e->body.tsx_state.tsx->status_code;
+ if (call->inv->state == PJSIP_INV_STATE_CONNECTING)
+ ev_state = PJSIP_EVSUB_STATE_TERMINATED;
+ else
+ ev_state = PJSIP_EVSUB_STATE_ACTIVE;
+ break;
+
+ case PJSIP_INV_STATE_CONFIRMED:
+#if 0
+/* We don't need this, as we've terminated the subscription in
+ * CONNECTING state.
+ */
+ /* When state is confirmed, send the final 200/OK and terminate
+ * subscription.
+ */
+ st_code = e->body.tsx_state.tsx->status_code;
+ ev_state = PJSIP_EVSUB_STATE_TERMINATED;
+#endif
+ break;
+
+ case PJSIP_INV_STATE_DISCONNECTED:
+ st_code = e->body.tsx_state.tsx->status_code;
+ ev_state = PJSIP_EVSUB_STATE_TERMINATED;
+ break;
+
+ case PJSIP_INV_STATE_INCOMING:
+ /* Nothing to do. Just to keep gcc from complaining about
+ * unused enums.
+ */
+ break;
+ }
+
+ if (st_code != -1) {
+ pjsip_tx_data *tdata;
+ pj_status_t status;
+
+ status = pjsip_xfer_notify( call->xfer_sub,
+ ev_state, st_code,
+ NULL, &tdata);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Unable to create NOTIFY", status);
+ } else {
+ status = pjsip_xfer_send_request(call->xfer_sub, tdata);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Unable to send NOTIFY", status);
+ }
+ }
+ }
+ }
+
+
+ if (pjsua_var.ua_cfg.cb.on_call_state)
+ (*pjsua_var.ua_cfg.cb.on_call_state)(call->index, e);
+
+ /* call->inv may be NULL now */
+
+ /* Destroy media session when invite session is disconnected. */
+ if (inv->state == PJSIP_INV_STATE_DISCONNECTED) {
+
+ PJSUA_LOCK();
+
+ pjsua_media_channel_deinit(call->index);
+
+ /* Free call */
+ call->inv = NULL;
+
+ pj_assert(pjsua_var.call_cnt > 0);
+ --pjsua_var.call_cnt;
+
+ /* Reset call */
+ reset_call(call->index);
+
+ pjsua_check_snd_dev_idle();
+
+ PJSUA_UNLOCK();
+ }
+ pj_log_pop_indent();
+}
+
+/*
+ * This callback is called by invite session framework when UAC session
+ * has forked.
+ */
+static void pjsua_call_on_forked( pjsip_inv_session *inv,
+ pjsip_event *e)
+{
+ PJ_UNUSED_ARG(inv);
+ PJ_UNUSED_ARG(e);
+
+ PJ_TODO(HANDLE_FORKED_DIALOG);
+}
+
+
+/*
+ * Callback from UA layer when forked dialog response is received.
+ */
+pjsip_dialog* on_dlg_forked(pjsip_dialog *dlg, pjsip_rx_data *res)
+{
+ if (dlg->uac_has_2xx &&
+ res->msg_info.cseq->method.id == PJSIP_INVITE_METHOD &&
+ pjsip_rdata_get_tsx(res) == NULL &&
+ res->msg_info.msg->line.status.code/100 == 2)
+ {
+ pjsip_dialog *forked_dlg;
+ pjsip_tx_data *bye;
+ pj_status_t status;
+
+ /* Create forked dialog */
+ status = pjsip_dlg_fork(dlg, res, &forked_dlg);
+ if (status != PJ_SUCCESS)
+ return NULL;
+
+ pjsip_dlg_inc_lock(forked_dlg);
+
+ /* Disconnect the call */
+ status = pjsip_dlg_create_request(forked_dlg, &pjsip_bye_method,
+ -1, &bye);
+ if (status == PJ_SUCCESS) {
+ status = pjsip_dlg_send_request(forked_dlg, bye, -1, NULL);
+ }
+
+ pjsip_dlg_dec_lock(forked_dlg);
+
+ if (status != PJ_SUCCESS) {
+ return NULL;
+ }
+
+ return forked_dlg;
+
+ } else {
+ return dlg;
+ }
+}
+
+/*
+ * Disconnect call upon error.
+ */
+static void call_disconnect( pjsip_inv_session *inv,
+ int code )
+{
+ pjsua_call *call;
+ pjsip_tx_data *tdata;
+ pj_status_t status;
+
+ call = (pjsua_call*) inv->dlg->mod_data[pjsua_var.mod.id];
+
+ status = pjsip_inv_end_session(inv, code, NULL, &tdata);
+ if (status != PJ_SUCCESS)
+ return;
+
+ /* Add SDP in 488 status */
+#if DISABLED_FOR_TICKET_1185
+ if (call && call->tp && tdata->msg->type==PJSIP_RESPONSE_MSG &&
+ code==PJSIP_SC_NOT_ACCEPTABLE_HERE)
+ {
+ pjmedia_sdp_session *local_sdp;
+ pjmedia_transport_info ti;
+
+ pjmedia_transport_info_init(&ti);
+ pjmedia_transport_get_info(call->med_tp, &ti);
+ status = pjmedia_endpt_create_sdp(pjsua_var.med_endpt, tdata->pool,
+ 1, &ti.sock_info, &local_sdp);
+ if (status == PJ_SUCCESS) {
+ pjsip_create_sdp_body(tdata->pool, local_sdp,
+ &tdata->msg->body);
+ }
+ }
+#endif
+
+ pjsip_inv_send_msg(inv, tdata);
+}
+
+/*
+ * Callback to be called when SDP offer/answer negotiation has just completed
+ * in the session. This function will start/update media if negotiation
+ * has succeeded.
+ */
+static void pjsua_call_on_media_update(pjsip_inv_session *inv,
+ pj_status_t status)
+{
+ pjsua_call *call;
+ const pjmedia_sdp_session *local_sdp;
+ const pjmedia_sdp_session *remote_sdp;
+ //const pj_str_t st_update = {"UPDATE", 6};
+
+ pj_log_push_indent();
+
+ call = (pjsua_call*) inv->dlg->mod_data[pjsua_var.mod.id];
+
+ if (status != PJ_SUCCESS) {
+
+ pjsua_perror(THIS_FILE, "SDP negotiation has failed", status);
+
+ /* Clean up provisional media */
+ pjsua_media_prov_clean_up(call->index);
+
+ /* Do not deinitialize media since this may be a re-INVITE or
+ * UPDATE (which in this case the media should not get affected
+ * by the failed re-INVITE/UPDATE). The media will be shutdown
+ * when call is disconnected anyway.
+ */
+ /* Stop/destroy media, if any */
+ /*pjsua_media_channel_deinit(call->index);*/
+
+ /* Disconnect call if we're not in the middle of initializing an
+ * UAS dialog and if this is not a re-INVITE
+ */
+ if (inv->state != PJSIP_INV_STATE_NULL &&
+ inv->state != PJSIP_INV_STATE_CONFIRMED)
+ {
+ call_disconnect(inv, PJSIP_SC_UNSUPPORTED_MEDIA_TYPE);
+ }
+
+ goto on_return;
+ }
+
+
+ /* Get local and remote SDP */
+ status = pjmedia_sdp_neg_get_active_local(call->inv->neg, &local_sdp);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE,
+ "Unable to retrieve currently active local SDP",
+ status);
+ //call_disconnect(inv, PJSIP_SC_UNSUPPORTED_MEDIA_TYPE);
+ goto on_return;
+ }
+
+ status = pjmedia_sdp_neg_get_active_remote(call->inv->neg, &remote_sdp);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE,
+ "Unable to retrieve currently active remote SDP",
+ status);
+ //call_disconnect(inv, PJSIP_SC_UNSUPPORTED_MEDIA_TYPE);
+ goto on_return;
+ }
+
+ /* Update remote's NAT type */
+ if (pjsua_var.ua_cfg.nat_type_in_sdp) {
+ update_remote_nat_type(call, remote_sdp);
+ }
+
+ /* Update media channel with the new SDP */
+ status = pjsua_media_channel_update(call->index, local_sdp, remote_sdp);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Unable to create media session",
+ status);
+ call_disconnect(inv, PJSIP_SC_NOT_ACCEPTABLE_HERE);
+ /* No need to deinitialize; media will be shutdown when call
+ * state is disconnected anyway.
+ */
+ /*pjsua_media_channel_deinit(call->index);*/
+ goto on_return;
+ }
+
+ /* Ticket #476: make sure only one codec is specified in the answer. */
+ status = lock_codec(call);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Unable to lock codec", status);
+ }
+
+ /* Call application callback, if any */
+ if (pjsua_var.ua_cfg.cb.on_call_media_state)
+ pjsua_var.ua_cfg.cb.on_call_media_state(call->index);
+
+on_return:
+ pj_log_pop_indent();
+}
+
+
+/* Modify SDP for call hold. */
+static pj_status_t modify_sdp_of_call_hold(pjsua_call *call,
+ pj_pool_t *pool,
+ pjmedia_sdp_session *sdp)
+{
+ unsigned mi;
+
+ /* Call-hold is done by set the media direction to 'sendonly'
+ * (PJMEDIA_DIR_ENCODING), except when current media direction is
+ * 'inactive' (PJMEDIA_DIR_NONE).
+ * (See RFC 3264 Section 8.4 and RFC 4317 Section 3.1)
+ */
+ /* http://trac.pjsip.org/repos/ticket/880
+ if (call->dir != PJMEDIA_DIR_ENCODING) {
+ */
+ /* https://trac.pjsip.org/repos/ticket/1142:
+ * configuration to use c=0.0.0.0 for call hold.
+ */
+
+ for (mi=0; mi<sdp->media_count; ++mi) {
+ pjmedia_sdp_media *m = sdp->media[mi];
+
+ if (call->call_hold_type == PJSUA_CALL_HOLD_TYPE_RFC2543) {
+ pjmedia_sdp_conn *conn;
+ pjmedia_sdp_attr *attr;
+
+ /* Get SDP media connection line */
+ conn = m->conn;
+ if (!conn)
+ conn = sdp->conn;
+
+ /* Modify address */
+ conn->addr = pj_str("0.0.0.0");
+
+ /* Remove existing directions attributes */
+ pjmedia_sdp_media_remove_all_attr(m, "sendrecv");
+ pjmedia_sdp_media_remove_all_attr(m, "sendonly");
+ pjmedia_sdp_media_remove_all_attr(m, "recvonly");
+ pjmedia_sdp_media_remove_all_attr(m, "inactive");
+
+ /* Add inactive attribute */
+ attr = pjmedia_sdp_attr_create(pool, "inactive", NULL);
+ pjmedia_sdp_media_add_attr(m, attr);
+
+
+ } else {
+ pjmedia_sdp_attr *attr;
+
+ /* Remove existing directions attributes */
+ pjmedia_sdp_media_remove_all_attr(m, "sendrecv");
+ pjmedia_sdp_media_remove_all_attr(m, "sendonly");
+ pjmedia_sdp_media_remove_all_attr(m, "recvonly");
+ pjmedia_sdp_media_remove_all_attr(m, "inactive");
+
+ if (call->media[mi].dir & PJMEDIA_DIR_ENCODING) {
+ /* Add sendonly attribute */
+ attr = pjmedia_sdp_attr_create(pool, "sendonly", NULL);
+ pjmedia_sdp_media_add_attr(m, attr);
+ } else {
+ /* Add inactive attribute */
+ attr = pjmedia_sdp_attr_create(pool, "inactive", NULL);
+ pjmedia_sdp_media_add_attr(m, attr);
+ }
+ }
+ }
+
+ return PJ_SUCCESS;
+}
+
+/* Create SDP for call hold. */
+static pj_status_t create_sdp_of_call_hold(pjsua_call *call,
+ pjmedia_sdp_session **p_sdp)
+{
+ pj_status_t status;
+ pj_pool_t *pool;
+ pjmedia_sdp_session *sdp;
+
+ /* Use call's provisional pool */
+ pool = call->inv->pool_prov;
+
+ /* Create new offer */
+ status = pjsua_media_channel_create_sdp(call->index, pool, NULL, &sdp,
+ NULL);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Unable to create local SDP", status);
+ return status;
+ }
+
+ status = modify_sdp_of_call_hold(call, pool, sdp);
+ if (status != PJ_SUCCESS)
+ return status;
+
+ *p_sdp = sdp;
+
+ return PJ_SUCCESS;
+}
+
+/*
+ * Called when session received new offer.
+ */
+static void pjsua_call_on_rx_offer(pjsip_inv_session *inv,
+ const pjmedia_sdp_session *offer)
+{
+ pjsua_call *call;
+ pjmedia_sdp_session *answer;
+ unsigned i;
+ pj_status_t status;
+
+ call = (pjsua_call*) inv->dlg->mod_data[pjsua_var.mod.id];
+
+ /* Supply candidate answer */
+ PJ_LOG(4,(THIS_FILE, "Call %d: received updated media offer",
+ call->index));
+ pj_log_push_indent();
+
+ if (pjsua_var.ua_cfg.cb.on_call_rx_offer) {
+ pjsip_status_code code = PJSIP_SC_OK;
+ pjsua_call_setting opt = call->opt;
+
+ (*pjsua_var.ua_cfg.cb.on_call_rx_offer)(call->index, offer, NULL,
+ &code, &opt);
+
+ if (code != PJSIP_SC_OK) {
+ PJ_LOG(4,(THIS_FILE, "Rejecting updated media offer on call %d",
+ call->index));
+ goto on_return;
+ }
+
+ call->opt = opt;
+ }
+
+ /* Re-init media for the new remote offer before creating SDP */
+ status = apply_call_setting(call, &call->opt, offer);
+ if (status != PJ_SUCCESS)
+ goto on_return;
+
+ status = pjsua_media_channel_create_sdp(call->index,
+ call->inv->pool_prov,
+ offer, &answer, NULL);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Unable to create local SDP", status);
+ goto on_return;
+ }
+
+ /* Validate media count in the generated answer */
+ pj_assert(answer->media_count == offer->media_count);
+
+ /* Check if offer's conn address is zero */
+ for (i = 0; i < answer->media_count; ++i) {
+ pjmedia_sdp_conn *conn;
+
+ conn = offer->media[i]->conn;
+ if (!conn)
+ conn = offer->conn;
+
+ if (pj_strcmp2(&conn->addr, "0.0.0.0")==0 ||
+ pj_strcmp2(&conn->addr, "0")==0)
+ {
+ pjmedia_sdp_conn *a_conn = answer->media[i]->conn;
+
+ /* Modify answer address */
+ if (a_conn) {
+ a_conn->addr = pj_str("0.0.0.0");
+ } else if (answer->conn == NULL ||
+ pj_strcmp2(&answer->conn->addr, "0.0.0.0") != 0)
+ {
+ a_conn = PJ_POOL_ZALLOC_T(call->inv->pool_prov,
+ pjmedia_sdp_conn);
+ a_conn->net_type = pj_str("IN");
+ a_conn->addr_type = pj_str("IP4");
+ a_conn->addr = pj_str("0.0.0.0");
+ answer->media[i]->conn = a_conn;
+ }
+ }
+ }
+
+ /* Check if call is on-hold */
+ if (call->local_hold) {
+ modify_sdp_of_call_hold(call, call->inv->pool_prov, answer);
+ }
+
+ status = pjsip_inv_set_sdp_answer(call->inv, answer);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Unable to set answer", status);
+ goto on_return;
+ }
+
+on_return:
+ pj_log_pop_indent();
+}
+
+
+/*
+ * Called to generate new offer.
+ */
+static void pjsua_call_on_create_offer(pjsip_inv_session *inv,
+ pjmedia_sdp_session **offer)
+{
+ pjsua_call *call;
+ pj_status_t status;
+
+ pj_log_push_indent();
+
+ call = (pjsua_call*) inv->dlg->mod_data[pjsua_var.mod.id];
+
+ /* See if we've put call on hold. */
+ if (call->local_hold) {
+ PJ_LOG(4,(THIS_FILE,
+ "Call %d: call is on-hold locally, creating call-hold SDP ",
+ call->index));
+ status = create_sdp_of_call_hold( call, offer );
+ } else {
+ PJ_LOG(4,(THIS_FILE, "Call %d: asked to send a new offer",
+ call->index));
+
+ status = pjsua_media_channel_create_sdp(call->index,
+ call->inv->pool_prov,
+ NULL, offer, NULL);
+ }
+
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Unable to create local SDP", status);
+ goto on_return;
+ }
+
+on_return:
+ pj_log_pop_indent();
+}
+
+
+/*
+ * Callback called by event framework when the xfer subscription state
+ * has changed.
+ */
+static void xfer_client_on_evsub_state( pjsip_evsub *sub, pjsip_event *event)
+{
+
+ PJ_UNUSED_ARG(event);
+
+ pj_log_push_indent();
+
+ /*
+ * When subscription is accepted (got 200/OK to REFER), check if
+ * subscription suppressed.
+ */
+ if (pjsip_evsub_get_state(sub) == PJSIP_EVSUB_STATE_ACCEPTED) {
+
+ pjsip_rx_data *rdata;
+ pjsip_generic_string_hdr *refer_sub;
+ const pj_str_t REFER_SUB = { "Refer-Sub", 9 };
+ pjsua_call *call;
+
+ call = (pjsua_call*) pjsip_evsub_get_mod_data(sub, pjsua_var.mod.id);
+
+ /* Must be receipt of response message */
+ pj_assert(event->type == PJSIP_EVENT_TSX_STATE &&
+ event->body.tsx_state.type == PJSIP_EVENT_RX_MSG);
+ rdata = event->body.tsx_state.src.rdata;
+
+ /* Find Refer-Sub header */
+ refer_sub = (pjsip_generic_string_hdr*)
+ pjsip_msg_find_hdr_by_name(rdata->msg_info.msg,
+ &REFER_SUB, NULL);
+
+ /* Check if subscription is suppressed */
+ if (refer_sub && pj_stricmp2(&refer_sub->hvalue, "false")==0) {
+ /* Since no subscription is desired, assume that call has been
+ * transfered successfully.
+ */
+ if (call && pjsua_var.ua_cfg.cb.on_call_transfer_status) {
+ const pj_str_t ACCEPTED = { "Accepted", 8 };
+ pj_bool_t cont = PJ_FALSE;
+ (*pjsua_var.ua_cfg.cb.on_call_transfer_status)(call->index,
+ 200,
+ &ACCEPTED,
+ PJ_TRUE,
+ &cont);
+ }
+
+ /* Yes, subscription is suppressed.
+ * Terminate our subscription now.
+ */
+ PJ_LOG(4,(THIS_FILE, "Xfer subscription suppressed, terminating "
+ "event subcription..."));
+ pjsip_evsub_terminate(sub, PJ_TRUE);
+
+ } else {
+ /* Notify application about call transfer progress.
+ * Initially notify with 100/Accepted status.
+ */
+ if (call && pjsua_var.ua_cfg.cb.on_call_transfer_status) {
+ const pj_str_t ACCEPTED = { "Accepted", 8 };
+ pj_bool_t cont = PJ_FALSE;
+ (*pjsua_var.ua_cfg.cb.on_call_transfer_status)(call->index,
+ 100,
+ &ACCEPTED,
+ PJ_FALSE,
+ &cont);
+ }
+ }
+ }
+ /*
+ * On incoming NOTIFY, notify application about call transfer progress.
+ */
+ else if (pjsip_evsub_get_state(sub) == PJSIP_EVSUB_STATE_ACTIVE ||
+ pjsip_evsub_get_state(sub) == PJSIP_EVSUB_STATE_TERMINATED)
+ {
+ pjsua_call *call;
+ pjsip_msg *msg;
+ pjsip_msg_body *body;
+ pjsip_status_line status_line;
+ pj_bool_t is_last;
+ pj_bool_t cont;
+ pj_status_t status;
+
+ call = (pjsua_call*) pjsip_evsub_get_mod_data(sub, pjsua_var.mod.id);
+
+ /* When subscription is terminated, clear the xfer_sub member of
+ * the inv_data.
+ */
+ if (pjsip_evsub_get_state(sub) == PJSIP_EVSUB_STATE_TERMINATED) {
+ pjsip_evsub_set_mod_data(sub, pjsua_var.mod.id, NULL);
+ PJ_LOG(4,(THIS_FILE, "Xfer client subscription terminated"));
+
+ }
+
+ if (!call || !event || !pjsua_var.ua_cfg.cb.on_call_transfer_status) {
+ /* Application is not interested with call progress status */
+ goto on_return;
+ }
+
+ /* This better be a NOTIFY request */
+ if (event->type == PJSIP_EVENT_TSX_STATE &&
+ event->body.tsx_state.type == PJSIP_EVENT_RX_MSG)
+ {
+ pjsip_rx_data *rdata;
+
+ rdata = event->body.tsx_state.src.rdata;
+
+ /* Check if there's body */
+ msg = rdata->msg_info.msg;
+ body = msg->body;
+ if (!body) {
+ PJ_LOG(2,(THIS_FILE,
+ "Warning: received NOTIFY without message body"));
+ goto on_return;
+ }
+
+ /* Check for appropriate content */
+ if (pj_stricmp2(&body->content_type.type, "message") != 0 ||
+ pj_stricmp2(&body->content_type.subtype, "sipfrag") != 0)
+ {
+ PJ_LOG(2,(THIS_FILE,
+ "Warning: received NOTIFY with non message/sipfrag "
+ "content"));
+ goto on_return;
+ }
+
+ /* Try to parse the content */
+ status = pjsip_parse_status_line((char*)body->data, body->len,
+ &status_line);
+ if (status != PJ_SUCCESS) {
+ PJ_LOG(2,(THIS_FILE,
+ "Warning: received NOTIFY with invalid "
+ "message/sipfrag content"));
+ goto on_return;
+ }
+
+ } else {
+ status_line.code = 500;
+ status_line.reason = *pjsip_get_status_text(500);
+ }
+
+ /* Notify application */
+ is_last = (pjsip_evsub_get_state(sub)==PJSIP_EVSUB_STATE_TERMINATED);
+ cont = !is_last;
+ (*pjsua_var.ua_cfg.cb.on_call_transfer_status)(call->index,
+ status_line.code,
+ &status_line.reason,
+ is_last, &cont);
+
+ if (!cont) {
+ pjsip_evsub_set_mod_data(sub, pjsua_var.mod.id, NULL);
+ }
+
+ /* If the call transfer has completed but the subscription is
+ * not terminated, terminate it now.
+ */
+ if (status_line.code/100 == 2 && !is_last) {
+ pjsip_tx_data *tdata;
+
+ status = pjsip_evsub_initiate(sub, &pjsip_subscribe_method,
+ 0, &tdata);
+ if (status == PJ_SUCCESS)
+ status = pjsip_evsub_send_request(sub, tdata);
+ }
+ }
+
+on_return:
+ pj_log_pop_indent();
+}
+
+
+/*
+ * Callback called by event framework when the xfer subscription state
+ * has changed.
+ */
+static void xfer_server_on_evsub_state( pjsip_evsub *sub, pjsip_event *event)
+{
+ PJ_UNUSED_ARG(event);
+
+ pj_log_push_indent();
+
+ /*
+ * When subscription is terminated, clear the xfer_sub member of
+ * the inv_data.
+ */
+ if (pjsip_evsub_get_state(sub) == PJSIP_EVSUB_STATE_TERMINATED) {
+ pjsua_call *call;
+
+ call = (pjsua_call*) pjsip_evsub_get_mod_data(sub, pjsua_var.mod.id);
+ if (!call)
+ goto on_return;
+
+ pjsip_evsub_set_mod_data(sub, pjsua_var.mod.id, NULL);
+ call->xfer_sub = NULL;
+
+ PJ_LOG(4,(THIS_FILE, "Xfer server subscription terminated"));
+ }
+
+on_return:
+ pj_log_pop_indent();
+}
+
+
+/*
+ * Follow transfer (REFER) request.
+ */
+static void on_call_transfered( pjsip_inv_session *inv,
+ pjsip_rx_data *rdata )
+{
+ pj_status_t status;
+ pjsip_tx_data *tdata;
+ pjsua_call *existing_call;
+ int new_call;
+ const pj_str_t str_refer_to = { "Refer-To", 8};
+ const pj_str_t str_refer_sub = { "Refer-Sub", 9 };
+ const pj_str_t str_ref_by = { "Referred-By", 11 };
+ pjsip_generic_string_hdr *refer_to;
+ pjsip_generic_string_hdr *refer_sub;
+ pjsip_hdr *ref_by_hdr;
+ pj_bool_t no_refer_sub = PJ_FALSE;
+ char *uri;
+ pjsua_msg_data msg_data;
+ pj_str_t tmp;
+ pjsip_status_code code;
+ pjsip_evsub *sub;
+ pjsua_call_setting call_opt;
+
+ pj_log_push_indent();
+
+ existing_call = (pjsua_call*) inv->dlg->mod_data[pjsua_var.mod.id];
+
+ /* Find the Refer-To header */
+ refer_to = (pjsip_generic_string_hdr*)
+ pjsip_msg_find_hdr_by_name(rdata->msg_info.msg, &str_refer_to, NULL);
+
+ if (refer_to == NULL) {
+ /* Invalid Request.
+ * No Refer-To header!
+ */
+ PJ_LOG(4,(THIS_FILE, "Received REFER without Refer-To header!"));
+ pjsip_dlg_respond( inv->dlg, rdata, 400, NULL, NULL, NULL);
+ goto on_return;
+ }
+
+ /* Find optional Refer-Sub header */
+ refer_sub = (pjsip_generic_string_hdr*)
+ pjsip_msg_find_hdr_by_name(rdata->msg_info.msg, &str_refer_sub, NULL);
+
+ if (refer_sub) {
+ if (!pj_strnicmp2(&refer_sub->hvalue, "true", 4)==0)
+ no_refer_sub = PJ_TRUE;
+ }
+
+ /* Find optional Referred-By header (to be copied onto outgoing INVITE
+ * request.
+ */
+ ref_by_hdr = (pjsip_hdr*)
+ pjsip_msg_find_hdr_by_name(rdata->msg_info.msg, &str_ref_by,
+ NULL);
+
+ /* Notify callback */
+ code = PJSIP_SC_ACCEPTED;
+ if (pjsua_var.ua_cfg.cb.on_call_transfer_request) {
+ (*pjsua_var.ua_cfg.cb.on_call_transfer_request)(existing_call->index,
+ &refer_to->hvalue,
+ &code);
+ }
+
+ call_opt = existing_call->opt;
+ if (pjsua_var.ua_cfg.cb.on_call_transfer_request2) {
+ (*pjsua_var.ua_cfg.cb.on_call_transfer_request2)(existing_call->index,
+ &refer_to->hvalue,
+ &code,
+ &call_opt);
+ }
+
+ if (code < 200)
+ code = PJSIP_SC_ACCEPTED;
+ if (code >= 300) {
+ /* Application rejects call transfer request */
+ pjsip_dlg_respond( inv->dlg, rdata, code, NULL, NULL, NULL);
+ goto on_return;
+ }
+
+ PJ_LOG(3,(THIS_FILE, "Call to %.*s is being transfered to %.*s",
+ (int)inv->dlg->remote.info_str.slen,
+ inv->dlg->remote.info_str.ptr,
+ (int)refer_to->hvalue.slen,
+ refer_to->hvalue.ptr));
+
+ if (no_refer_sub) {
+ /*
+ * Always answer with 2xx.
+ */
+ pjsip_tx_data *tdata;
+ const pj_str_t str_false = { "false", 5};
+ pjsip_hdr *hdr;
+
+ status = pjsip_dlg_create_response(inv->dlg, rdata, code, NULL,
+ &tdata);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Unable to create 2xx response to REFER",
+ status);
+ goto on_return;
+ }
+
+ /* Add Refer-Sub header */
+ hdr = (pjsip_hdr*)
+ pjsip_generic_string_hdr_create(tdata->pool, &str_refer_sub,
+ &str_false);
+ pjsip_msg_add_hdr(tdata->msg, hdr);
+
+
+ /* Send answer */
+ status = pjsip_dlg_send_response(inv->dlg, pjsip_rdata_get_tsx(rdata),
+ tdata);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Unable to create 2xx response to REFER",
+ status);
+ goto on_return;
+ }
+
+ /* Don't have subscription */
+ sub = NULL;
+
+ } else {
+ struct pjsip_evsub_user xfer_cb;
+ pjsip_hdr hdr_list;
+
+ /* Init callback */
+ pj_bzero(&xfer_cb, sizeof(xfer_cb));
+ xfer_cb.on_evsub_state = &xfer_server_on_evsub_state;
+
+ /* Init additional header list to be sent with REFER response */
+ pj_list_init(&hdr_list);
+
+ /* Create transferee event subscription */
+ status = pjsip_xfer_create_uas( inv->dlg, &xfer_cb, rdata, &sub);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Unable to create xfer uas", status);
+ pjsip_dlg_respond( inv->dlg, rdata, 500, NULL, NULL, NULL);
+ goto on_return;
+ }
+
+ /* If there's Refer-Sub header and the value is "true", send back
+ * Refer-Sub in the response with value "true" too.
+ */
+ if (refer_sub) {
+ const pj_str_t str_true = { "true", 4 };
+ pjsip_hdr *hdr;
+
+ hdr = (pjsip_hdr*)
+ pjsip_generic_string_hdr_create(inv->dlg->pool,
+ &str_refer_sub,
+ &str_true);
+ pj_list_push_back(&hdr_list, hdr);
+
+ }
+
+ /* Accept the REFER request, send 2xx. */
+ pjsip_xfer_accept(sub, rdata, code, &hdr_list);
+
+ /* Create initial NOTIFY request */
+ status = pjsip_xfer_notify( sub, PJSIP_EVSUB_STATE_ACTIVE,
+ 100, NULL, &tdata);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Unable to create NOTIFY to REFER",
+ status);
+ goto on_return;
+ }
+
+ /* Send initial NOTIFY request */
+ status = pjsip_xfer_send_request( sub, tdata);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Unable to send NOTIFY to REFER", status);
+ goto on_return;
+ }
+ }
+
+ /* We're cheating here.
+ * We need to get a null terminated string from a pj_str_t.
+ * So grab the pointer from the hvalue and NULL terminate it, knowing
+ * that the NULL position will be occupied by a newline.
+ */
+ uri = refer_to->hvalue.ptr;
+ uri[refer_to->hvalue.slen] = '\0';
+
+ /* Init msg_data */
+ pjsua_msg_data_init(&msg_data);
+
+ /* If Referred-By header is present in the REFER request, copy this
+ * to the outgoing INVITE request.
+ */
+ if (ref_by_hdr != NULL) {
+ pjsip_hdr *dup = (pjsip_hdr*)
+ pjsip_hdr_clone(rdata->tp_info.pool, ref_by_hdr);
+ pj_list_push_back(&msg_data.hdr_list, dup);
+ }
+
+ /* Now make the outgoing call. */
+ tmp = pj_str(uri);
+ status = pjsua_call_make_call(existing_call->acc_id, &tmp, &call_opt,
+ existing_call->user_data, &msg_data,
+ &new_call);
+ if (status != PJ_SUCCESS) {
+
+ /* Notify xferer about the error (if we have subscription) */
+ if (sub) {
+ status = pjsip_xfer_notify(sub, PJSIP_EVSUB_STATE_TERMINATED,
+ 500, NULL, &tdata);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Unable to create NOTIFY to REFER",
+ status);
+ goto on_return;
+ }
+ status = pjsip_xfer_send_request(sub, tdata);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Unable to send NOTIFY to REFER",
+ status);
+ goto on_return;
+ }
+ }
+ goto on_return;
+ }
+
+ if (sub) {
+ /* Put the server subscription in inv_data.
+ * Subsequent state changed in pjsua_inv_on_state_changed() will be
+ * reported back to the server subscription.
+ */
+ pjsua_var.calls[new_call].xfer_sub = sub;
+
+ /* Put the invite_data in the subscription. */
+ pjsip_evsub_set_mod_data(sub, pjsua_var.mod.id,
+ &pjsua_var.calls[new_call]);
+ }
+
+on_return:
+ pj_log_pop_indent();
+}
+
+
+
+/*
+ * This callback is called when transaction state has changed in INVITE
+ * session. We use this to trap:
+ * - incoming REFER request.
+ * - incoming MESSAGE request.
+ */
+static void pjsua_call_on_tsx_state_changed(pjsip_inv_session *inv,
+ pjsip_transaction *tsx,
+ pjsip_event *e)
+{
+ pjsua_call *call;
+
+ pj_log_push_indent();
+
+ call = (pjsua_call*) inv->dlg->mod_data[pjsua_var.mod.id];
+
+ if (call == NULL)
+ goto on_return;
+
+ if (call->inv == NULL) {
+ /* Shouldn't happen. It happens only when we don't terminate the
+ * server subscription caused by REFER after the call has been
+ * transfered (and this call has been disconnected), and we
+ * receive another REFER for this call.
+ */
+ goto on_return;
+ }
+
+ /* https://trac.pjsip.org/repos/ticket/1452:
+ * If a request is retried due to 401/407 challenge, don't process the
+ * transaction first but wait until we've retried it.
+ */
+ if (tsx->role == PJSIP_ROLE_UAC &&
+ (tsx->status_code==401 || tsx->status_code==407) &&
+ tsx->last_tx && tsx->last_tx->auth_retry)
+ {
+ goto on_return;
+ }
+
+ /* Notify application callback first */
+ if (pjsua_var.ua_cfg.cb.on_call_tsx_state) {
+ (*pjsua_var.ua_cfg.cb.on_call_tsx_state)(call->index, tsx, e);
+ }
+
+ if (tsx->role==PJSIP_ROLE_UAS &&
+ tsx->state==PJSIP_TSX_STATE_TRYING &&
+ pjsip_method_cmp(&tsx->method, pjsip_get_refer_method())==0)
+ {
+ /*
+ * Incoming REFER request.
+ */
+ on_call_transfered(call->inv, e->body.tsx_state.src.rdata);
+
+ }
+ else if (tsx->role==PJSIP_ROLE_UAS &&
+ tsx->state==PJSIP_TSX_STATE_TRYING &&
+ pjsip_method_cmp(&tsx->method, &pjsip_message_method)==0)
+ {
+ /*
+ * Incoming MESSAGE request!
+ */
+ pjsip_rx_data *rdata;
+ pjsip_msg *msg;
+ pjsip_accept_hdr *accept_hdr;
+ pj_status_t status;
+
+ rdata = e->body.tsx_state.src.rdata;
+ msg = rdata->msg_info.msg;
+
+ /* Request MUST have message body, with Content-Type equal to
+ * "text/plain".
+ */
+ if (pjsua_im_accept_pager(rdata, &accept_hdr) == PJ_FALSE) {
+
+ pjsip_hdr hdr_list;
+
+ pj_list_init(&hdr_list);
+ pj_list_push_back(&hdr_list, accept_hdr);
+
+ pjsip_dlg_respond( inv->dlg, rdata, PJSIP_SC_NOT_ACCEPTABLE_HERE,
+ NULL, &hdr_list, NULL );
+ goto on_return;
+ }
+
+ /* Respond with 200 first, so that remote doesn't retransmit in case
+ * the UI takes too long to process the message.
+ */
+ status = pjsip_dlg_respond( inv->dlg, rdata, 200, NULL, NULL, NULL);
+
+ /* Process MESSAGE request */
+ pjsua_im_process_pager(call->index, &inv->dlg->remote.info_str,
+ &inv->dlg->local.info_str, rdata);
+
+ }
+ else if (tsx->role == PJSIP_ROLE_UAC &&
+ pjsip_method_cmp(&tsx->method, &pjsip_message_method)==0)
+ {
+ /* Handle outgoing pager status */
+ if (tsx->status_code >= 200) {
+ pjsua_im_data *im_data;
+
+ im_data = (pjsua_im_data*) tsx->mod_data[pjsua_var.mod.id];
+ /* im_data can be NULL if this is typing indication */
+
+ if (im_data && pjsua_var.ua_cfg.cb.on_pager_status) {
+ pjsua_var.ua_cfg.cb.on_pager_status(im_data->call_id,
+ &im_data->to,
+ &im_data->body,
+ im_data->user_data,
+ (pjsip_status_code)
+ tsx->status_code,
+ &tsx->status_text);
+ }
+ }
+ } else if (tsx->role == PJSIP_ROLE_UAC &&
+ tsx->last_tx == (pjsip_tx_data*)call->hold_msg &&
+ tsx->state >= PJSIP_TSX_STATE_COMPLETED)
+ {
+ /* Monitor the status of call hold request */
+ call->hold_msg = NULL;
+ if (tsx->status_code/100 != 2) {
+ /* Outgoing call hold failed */
+ call->local_hold = PJ_FALSE;
+ PJ_LOG(3,(THIS_FILE, "Error putting call %d on hold (reason=%d)",
+ call->index, tsx->status_code));
+ }
+ } else if (tsx->role==PJSIP_ROLE_UAS &&
+ tsx->state==PJSIP_TSX_STATE_TRYING &&
+ pjsip_method_cmp(&tsx->method, &pjsip_info_method)==0)
+ {
+ /*
+ * Incoming INFO request for media control.
+ */
+ const pj_str_t STR_APPLICATION = { "application", 11};
+ const pj_str_t STR_MEDIA_CONTROL_XML = { "media_control+xml", 17 };
+ pjsip_rx_data *rdata = e->body.tsx_state.src.rdata;
+ pjsip_msg_body *body = rdata->msg_info.msg->body;
+
+ if (body && body->len &&
+ pj_stricmp(&body->content_type.type, &STR_APPLICATION)==0 &&
+ pj_stricmp(&body->content_type.subtype, &STR_MEDIA_CONTROL_XML)==0)
+ {
+ pjsip_tx_data *tdata;
+ pj_str_t control_st;
+ pj_status_t status;
+
+ /* Apply and answer the INFO request */
+ pj_strset(&control_st, (char*)body->data, body->len);
+ status = pjsua_media_apply_xml_control(call->index, &control_st);
+ if (status == PJ_SUCCESS) {
+ status = pjsip_endpt_create_response(tsx->endpt, rdata,
+ 200, NULL, &tdata);
+ if (status == PJ_SUCCESS)
+ status = pjsip_tsx_send_msg(tsx, tdata);
+ } else {
+ status = pjsip_endpt_create_response(tsx->endpt, rdata,
+ 400, NULL, &tdata);
+ if (status == PJ_SUCCESS)
+ status = pjsip_tsx_send_msg(tsx, tdata);
+ }
+ }
+ }
+
+on_return:
+ pj_log_pop_indent();
+}
+
+
+/* Redirection handler */
+static pjsip_redirect_op pjsua_call_on_redirected(pjsip_inv_session *inv,
+ const pjsip_uri *target,
+ const pjsip_event *e)
+{
+ pjsua_call *call = (pjsua_call*) inv->dlg->mod_data[pjsua_var.mod.id];
+ pjsip_redirect_op op;
+
+ pj_log_push_indent();
+
+ if (pjsua_var.ua_cfg.cb.on_call_redirected) {
+ op = (*pjsua_var.ua_cfg.cb.on_call_redirected)(call->index,
+ target, e);
+ } else {
+ PJ_LOG(4,(THIS_FILE, "Unhandled redirection for call %d "
+ "(callback not implemented by application). Disconnecting "
+ "call.",
+ call->index));
+ op = PJSIP_REDIRECT_STOP;
+ }
+
+ pj_log_pop_indent();
+
+ return op;
+}
+
diff --git a/pjsip/src/pjsua-lib/pjsua_core.c b/pjsip/src/pjsua-lib/pjsua_core.c
new file mode 100644
index 0000000..f4f9508
--- /dev/null
+++ b/pjsip/src/pjsua-lib/pjsua_core.c
@@ -0,0 +1,2909 @@
+/* $Id: pjsua_core.c 4173 2012-06-20 10:39:05Z ming $ */
+/*
+ * Copyright (C) 2008-2011 Teluu Inc. (http://www.teluu.com)
+ * Copyright (C) 2003-2008 Benny Prijono <benny@prijono.org>
+ *
+ * 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
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ */
+#include <pjsua-lib/pjsua.h>
+#include <pjsua-lib/pjsua_internal.h>
+
+
+#define THIS_FILE "pjsua_core.c"
+
+
+/* Internal prototypes */
+static void resolve_stun_entry(pjsua_stun_resolve *sess);
+
+
+/* PJSUA application instance. */
+struct pjsua_data pjsua_var;
+
+
+PJ_DEF(struct pjsua_data*) pjsua_get_var(void)
+{
+ return &pjsua_var;
+}
+
+
+/* Display error */
+PJ_DEF(void) pjsua_perror( const char *sender, const char *title,
+ pj_status_t status)
+{
+ char errmsg[PJ_ERR_MSG_SIZE];
+
+ pj_strerror(status, errmsg, sizeof(errmsg));
+ PJ_LOG(1,(sender, "%s: %s [status=%d]", title, errmsg, status));
+}
+
+
+static void init_data()
+{
+ unsigned i;
+
+ pj_bzero(&pjsua_var, sizeof(pjsua_var));
+
+ for (i=0; i<PJ_ARRAY_SIZE(pjsua_var.acc); ++i)
+ pjsua_var.acc[i].index = i;
+
+ for (i=0; i<PJ_ARRAY_SIZE(pjsua_var.tpdata); ++i)
+ pjsua_var.tpdata[i].index = i;
+
+ pjsua_var.stun_status = PJ_EUNKNOWN;
+ pjsua_var.nat_status = PJ_EPENDING;
+ pj_list_init(&pjsua_var.stun_res);
+ pj_list_init(&pjsua_var.outbound_proxy);
+
+ pjsua_config_default(&pjsua_var.ua_cfg);
+
+ for (i=0; i<PJSUA_MAX_VID_WINS; ++i) {
+ pjsua_vid_win_reset(i);
+ }
+}
+
+
+PJ_DEF(void) pjsua_logging_config_default(pjsua_logging_config *cfg)
+{
+ pj_bzero(cfg, sizeof(*cfg));
+
+ cfg->msg_logging = PJ_TRUE;
+ cfg->level = 5;
+ cfg->console_level = 4;
+ cfg->decor = PJ_LOG_HAS_SENDER | PJ_LOG_HAS_TIME |
+ PJ_LOG_HAS_MICRO_SEC | PJ_LOG_HAS_NEWLINE |
+ PJ_LOG_HAS_SPACE | PJ_LOG_HAS_THREAD_SWC |
+ PJ_LOG_HAS_INDENT;
+#if defined(PJ_WIN32) && PJ_WIN32 != 0
+ cfg->decor |= PJ_LOG_HAS_COLOR;
+#endif
+}
+
+PJ_DEF(void) pjsua_logging_config_dup(pj_pool_t *pool,
+ pjsua_logging_config *dst,
+ const pjsua_logging_config *src)
+{
+ pj_memcpy(dst, src, sizeof(*src));
+ pj_strdup_with_null(pool, &dst->log_filename, &src->log_filename);
+}
+
+PJ_DEF(void) pjsua_config_default(pjsua_config *cfg)
+{
+ pj_bzero(cfg, sizeof(*cfg));
+
+ cfg->max_calls = ((PJSUA_MAX_CALLS) < 4) ? (PJSUA_MAX_CALLS) : 4;
+ cfg->thread_cnt = 1;
+ cfg->nat_type_in_sdp = 1;
+ cfg->stun_ignore_failure = PJ_TRUE;
+ cfg->force_lr = PJ_TRUE;
+ cfg->enable_unsolicited_mwi = PJ_TRUE;
+ cfg->use_srtp = PJSUA_DEFAULT_USE_SRTP;
+ cfg->srtp_secure_signaling = PJSUA_DEFAULT_SRTP_SECURE_SIGNALING;
+ cfg->hangup_forked_call = PJ_TRUE;
+
+ cfg->use_timer = PJSUA_SIP_TIMER_OPTIONAL;
+ pjsip_timer_setting_default(&cfg->timer_setting);
+}
+
+PJ_DEF(void) pjsua_config_dup(pj_pool_t *pool,
+ pjsua_config *dst,
+ const pjsua_config *src)
+{
+ unsigned i;
+
+ pj_memcpy(dst, src, sizeof(*src));
+
+ for (i=0; i<src->outbound_proxy_cnt; ++i) {
+ pj_strdup_with_null(pool, &dst->outbound_proxy[i],
+ &src->outbound_proxy[i]);
+ }
+
+ for (i=0; i<src->cred_count; ++i) {
+ pjsip_cred_dup(pool, &dst->cred_info[i], &src->cred_info[i]);
+ }
+
+ pj_strdup_with_null(pool, &dst->user_agent, &src->user_agent);
+ pj_strdup_with_null(pool, &dst->stun_domain, &src->stun_domain);
+ pj_strdup_with_null(pool, &dst->stun_host, &src->stun_host);
+
+ for (i=0; i<src->stun_srv_cnt; ++i) {
+ pj_strdup_with_null(pool, &dst->stun_srv[i], &src->stun_srv[i]);
+ }
+}
+
+PJ_DEF(void) pjsua_msg_data_init(pjsua_msg_data *msg_data)
+{
+ pj_bzero(msg_data, sizeof(*msg_data));
+ pj_list_init(&msg_data->hdr_list);
+ pjsip_media_type_init(&msg_data->multipart_ctype, NULL, NULL);
+ pj_list_init(&msg_data->multipart_parts);
+}
+
+PJ_DEF(pjsua_msg_data*) pjsua_msg_data_clone(pj_pool_t *pool,
+ const pjsua_msg_data *rhs)
+{
+ pjsua_msg_data *msg_data;
+ const pjsip_hdr *hdr;
+ const pjsip_multipart_part *mpart;
+
+ PJ_ASSERT_RETURN(pool && rhs, NULL);
+
+ msg_data = PJ_POOL_ZALLOC_T(pool, pjsua_msg_data);
+ PJ_ASSERT_RETURN(msg_data != NULL, NULL);
+
+ pj_list_init(&msg_data->hdr_list);
+ hdr = rhs->hdr_list.next;
+ while (hdr != &rhs->hdr_list) {
+ pj_list_push_back(&msg_data->hdr_list, pjsip_hdr_clone(pool, hdr));
+ hdr = hdr->next;
+ }
+
+ pj_strdup(pool, &msg_data->content_type, &rhs->content_type);
+ pj_strdup(pool, &msg_data->msg_body, &rhs->msg_body);
+
+ pjsip_media_type_cp(pool, &msg_data->multipart_ctype,
+ &rhs->multipart_ctype);
+
+ pj_list_init(&msg_data->multipart_parts);
+ mpart = rhs->multipart_parts.next;
+ while (mpart != &rhs->multipart_parts) {
+ pj_list_push_back(&msg_data->multipart_parts,
+ pjsip_multipart_clone_part(pool, mpart));
+ mpart = mpart->next;
+ }
+
+ return msg_data;
+}
+
+PJ_DEF(void) pjsua_transport_config_default(pjsua_transport_config *cfg)
+{
+ pj_bzero(cfg, sizeof(*cfg));
+ pjsip_tls_setting_default(&cfg->tls_setting);
+}
+
+PJ_DEF(void) pjsua_transport_config_dup(pj_pool_t *pool,
+ pjsua_transport_config *dst,
+ const pjsua_transport_config *src)
+{
+ PJ_UNUSED_ARG(pool);
+ pj_memcpy(dst, src, sizeof(*src));
+}
+
+PJ_DEF(void) pjsua_acc_config_default(pjsua_acc_config *cfg)
+{
+ pj_bzero(cfg, sizeof(*cfg));
+
+ cfg->reg_timeout = PJSUA_REG_INTERVAL;
+ cfg->reg_delay_before_refresh = PJSIP_REGISTER_CLIENT_DELAY_BEFORE_REFRESH;
+ cfg->unreg_timeout = PJSUA_UNREG_TIMEOUT;
+ pjsip_publishc_opt_default(&cfg->publish_opt);
+ cfg->unpublish_max_wait_time_msec = PJSUA_UNPUBLISH_MAX_WAIT_TIME_MSEC;
+ cfg->transport_id = PJSUA_INVALID_ID;
+ cfg->allow_contact_rewrite = PJ_TRUE;
+ cfg->allow_via_rewrite = PJ_TRUE;
+ cfg->require_100rel = pjsua_var.ua_cfg.require_100rel;
+ cfg->use_timer = pjsua_var.ua_cfg.use_timer;
+ cfg->timer_setting = pjsua_var.ua_cfg.timer_setting;
+ cfg->ka_interval = 15;
+ cfg->ka_data = pj_str("\r\n");
+ cfg->vid_cap_dev = PJMEDIA_VID_DEFAULT_CAPTURE_DEV;
+ cfg->vid_rend_dev = PJMEDIA_VID_DEFAULT_RENDER_DEV;
+#if PJMEDIA_HAS_VIDEO
+ pjmedia_vid_stream_rc_config_default(&cfg->vid_stream_rc_cfg);
+#endif
+ pjsua_transport_config_default(&cfg->rtp_cfg);
+ cfg->use_srtp = pjsua_var.ua_cfg.use_srtp;
+ cfg->srtp_secure_signaling = pjsua_var.ua_cfg.srtp_secure_signaling;
+ cfg->srtp_optional_dup_offer = pjsua_var.ua_cfg.srtp_optional_dup_offer;
+ cfg->reg_retry_interval = PJSUA_REG_RETRY_INTERVAL;
+ cfg->contact_rewrite_method = PJSUA_CONTACT_REWRITE_METHOD;
+ cfg->use_rfc5626 = PJ_TRUE;
+ cfg->reg_use_proxy = PJSUA_REG_USE_OUTBOUND_PROXY |
+ PJSUA_REG_USE_ACC_PROXY;
+#if defined(PJMEDIA_STREAM_ENABLE_KA) && PJMEDIA_STREAM_ENABLE_KA!=0
+ cfg->use_stream_ka = (PJMEDIA_STREAM_ENABLE_KA != 0);
+#endif
+ pj_list_init(&cfg->reg_hdr_list);
+ pj_list_init(&cfg->sub_hdr_list);
+ cfg->call_hold_type = PJSUA_CALL_HOLD_TYPE_DEFAULT;
+ cfg->register_on_acc_add = PJ_TRUE;
+ cfg->mwi_expires = PJSIP_MWI_DEFAULT_EXPIRES;
+}
+
+PJ_DEF(void) pjsua_buddy_config_default(pjsua_buddy_config *cfg)
+{
+ pj_bzero(cfg, sizeof(*cfg));
+}
+
+PJ_DEF(void) pjsua_media_config_default(pjsua_media_config *cfg)
+{
+ pj_bzero(cfg, sizeof(*cfg));
+
+ cfg->clock_rate = PJSUA_DEFAULT_CLOCK_RATE;
+ cfg->snd_clock_rate = 0;
+ cfg->channel_count = 1;
+ cfg->audio_frame_ptime = PJSUA_DEFAULT_AUDIO_FRAME_PTIME;
+ cfg->max_media_ports = PJSUA_MAX_CONF_PORTS;
+ cfg->has_ioqueue = PJ_TRUE;
+ cfg->thread_cnt = 1;
+ cfg->quality = PJSUA_DEFAULT_CODEC_QUALITY;
+ cfg->ilbc_mode = PJSUA_DEFAULT_ILBC_MODE;
+ cfg->ec_tail_len = PJSUA_DEFAULT_EC_TAIL_LEN;
+ cfg->snd_rec_latency = PJMEDIA_SND_DEFAULT_REC_LATENCY;
+ cfg->snd_play_latency = PJMEDIA_SND_DEFAULT_PLAY_LATENCY;
+ cfg->jb_init = cfg->jb_min_pre = cfg->jb_max_pre = cfg->jb_max = -1;
+ cfg->snd_auto_close_time = 1;
+
+ cfg->ice_max_host_cands = -1;
+ pj_ice_sess_options_default(&cfg->ice_opt);
+
+ cfg->turn_conn_type = PJ_TURN_TP_UDP;
+ cfg->vid_preview_enable_native = PJ_TRUE;
+}
+
+/*****************************************************************************
+ * This is a very simple PJSIP module, whose sole purpose is to display
+ * incoming and outgoing messages to log. This module will have priority
+ * higher than transport layer, which means:
+ *
+ * - incoming messages will come to this module first before reaching
+ * transaction layer.
+ *
+ * - outgoing messages will come to this module last, after the message
+ * has been 'printed' to contiguous buffer by transport layer and
+ * appropriate transport instance has been decided for this message.
+ *
+ */
+
+/* Notification on incoming messages */
+static pj_bool_t logging_on_rx_msg(pjsip_rx_data *rdata)
+{
+ PJ_LOG(4,(THIS_FILE, "RX %d bytes %s from %s %s:%d:\n"
+ "%.*s\n"
+ "--end msg--",
+ rdata->msg_info.len,
+ pjsip_rx_data_get_info(rdata),
+ rdata->tp_info.transport->type_name,
+ rdata->pkt_info.src_name,
+ rdata->pkt_info.src_port,
+ (int)rdata->msg_info.len,
+ rdata->msg_info.msg_buf));
+
+ /* Always return false, otherwise messages will not get processed! */
+ return PJ_FALSE;
+}
+
+/* Notification on outgoing messages */
+static pj_status_t logging_on_tx_msg(pjsip_tx_data *tdata)
+{
+
+ /* Important note:
+ * tp_info field is only valid after outgoing messages has passed
+ * transport layer. So don't try to access tp_info when the module
+ * has lower priority than transport layer.
+ */
+
+ PJ_LOG(4,(THIS_FILE, "TX %d bytes %s to %s %s:%d:\n"
+ "%.*s\n"
+ "--end msg--",
+ (tdata->buf.cur - tdata->buf.start),
+ pjsip_tx_data_get_info(tdata),
+ tdata->tp_info.transport->type_name,
+ tdata->tp_info.dst_name,
+ tdata->tp_info.dst_port,
+ (int)(tdata->buf.cur - tdata->buf.start),
+ tdata->buf.start));
+
+ /* Always return success, otherwise message will not get sent! */
+ return PJ_SUCCESS;
+}
+
+/* The module instance. */
+static pjsip_module pjsua_msg_logger =
+{
+ NULL, NULL, /* prev, next. */
+ { "mod-pjsua-log", 13 }, /* Name. */
+ -1, /* Id */
+ PJSIP_MOD_PRIORITY_TRANSPORT_LAYER-1,/* Priority */
+ NULL, /* load() */
+ NULL, /* start() */
+ NULL, /* stop() */
+ NULL, /* unload() */
+ &logging_on_rx_msg, /* on_rx_request() */
+ &logging_on_rx_msg, /* on_rx_response() */
+ &logging_on_tx_msg, /* on_tx_request. */
+ &logging_on_tx_msg, /* on_tx_response() */
+ NULL, /* on_tsx_state() */
+
+};
+
+
+/*****************************************************************************
+ * Another simple module to handle incoming OPTIONS request
+ */
+
+/* Notification on incoming request */
+static pj_bool_t options_on_rx_request(pjsip_rx_data *rdata)
+{
+ pjsip_tx_data *tdata;
+ pjsip_response_addr res_addr;
+ const pjsip_hdr *cap_hdr;
+ pj_status_t status;
+
+ /* Only want to handle OPTIONS requests */
+ if (pjsip_method_cmp(&rdata->msg_info.msg->line.req.method,
+ pjsip_get_options_method()) != 0)
+ {
+ return PJ_FALSE;
+ }
+
+ /* Don't want to handle if shutdown is in progress */
+ if (pjsua_var.thread_quit_flag) {
+ pjsip_endpt_respond_stateless(pjsua_var.endpt, rdata,
+ PJSIP_SC_TEMPORARILY_UNAVAILABLE, NULL,
+ NULL, NULL);
+ return PJ_TRUE;
+ }
+
+ /* Create basic response. */
+ status = pjsip_endpt_create_response(pjsua_var.endpt, rdata, 200, NULL,
+ &tdata);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Unable to create OPTIONS response", status);
+ return PJ_TRUE;
+ }
+
+ /* Add Allow header */
+ cap_hdr = pjsip_endpt_get_capability(pjsua_var.endpt, PJSIP_H_ALLOW, NULL);
+ if (cap_hdr) {
+ pjsip_msg_add_hdr(tdata->msg,
+ (pjsip_hdr*) pjsip_hdr_clone(tdata->pool, cap_hdr));
+ }
+
+ /* Add Accept header */
+ cap_hdr = pjsip_endpt_get_capability(pjsua_var.endpt, PJSIP_H_ACCEPT, NULL);
+ if (cap_hdr) {
+ pjsip_msg_add_hdr(tdata->msg,
+ (pjsip_hdr*) pjsip_hdr_clone(tdata->pool, cap_hdr));
+ }
+
+ /* Add Supported header */
+ cap_hdr = pjsip_endpt_get_capability(pjsua_var.endpt, PJSIP_H_SUPPORTED, NULL);
+ if (cap_hdr) {
+ pjsip_msg_add_hdr(tdata->msg,
+ (pjsip_hdr*) pjsip_hdr_clone(tdata->pool, cap_hdr));
+ }
+
+ /* Add Allow-Events header from the evsub module */
+ cap_hdr = pjsip_evsub_get_allow_events_hdr(NULL);
+ if (cap_hdr) {
+ pjsip_msg_add_hdr(tdata->msg,
+ (pjsip_hdr*) pjsip_hdr_clone(tdata->pool, cap_hdr));
+ }
+
+ /* Add User-Agent header */
+ if (pjsua_var.ua_cfg.user_agent.slen) {
+ const pj_str_t USER_AGENT = { "User-Agent", 10};
+ pjsip_hdr *h;
+
+ h = (pjsip_hdr*) pjsip_generic_string_hdr_create(tdata->pool,
+ &USER_AGENT,
+ &pjsua_var.ua_cfg.user_agent);
+ pjsip_msg_add_hdr(tdata->msg, h);
+ }
+
+ /* Get media socket info, make sure transport is ready */
+#if DISABLED_FOR_TICKET_1185
+ if (pjsua_var.calls[0].med_tp) {
+ pjmedia_transport_info tpinfo;
+ pjmedia_sdp_session *sdp;
+
+ pjmedia_transport_info_init(&tpinfo);
+ pjmedia_transport_get_info(pjsua_var.calls[0].med_tp, &tpinfo);
+
+ /* Add SDP body, using call0's RTP address */
+ status = pjmedia_endpt_create_sdp(pjsua_var.med_endpt, tdata->pool, 1,
+ &tpinfo.sock_info, &sdp);
+ if (status == PJ_SUCCESS) {
+ pjsip_create_sdp_body(tdata->pool, sdp, &tdata->msg->body);
+ }
+ }
+#endif
+
+ /* Send response */
+ pjsip_get_response_addr(tdata->pool, rdata, &res_addr);
+ status = pjsip_endpt_send_response(pjsua_var.endpt, &res_addr, tdata, NULL, NULL);
+ if (status != PJ_SUCCESS)
+ pjsip_tx_data_dec_ref(tdata);
+
+ return PJ_TRUE;
+}
+
+
+/* The module instance. */
+static pjsip_module pjsua_options_handler =
+{
+ NULL, NULL, /* prev, next. */
+ { "mod-pjsua-options", 17 }, /* Name. */
+ -1, /* Id */
+ PJSIP_MOD_PRIORITY_APPLICATION, /* Priority */
+ NULL, /* load() */
+ NULL, /* start() */
+ NULL, /* stop() */
+ NULL, /* unload() */
+ &options_on_rx_request, /* on_rx_request() */
+ NULL, /* on_rx_response() */
+ NULL, /* on_tx_request. */
+ NULL, /* on_tx_response() */
+ NULL, /* on_tsx_state() */
+
+};
+
+
+/*****************************************************************************
+ * These two functions are the main callbacks registered to PJSIP stack
+ * to receive SIP request and response messages that are outside any
+ * dialogs and any transactions.
+ */
+
+/*
+ * Handler for receiving incoming requests.
+ *
+ * This handler serves multiple purposes:
+ * - it receives requests outside dialogs.
+ * - it receives requests inside dialogs, when the requests are
+ * unhandled by other dialog usages. Example of these
+ * requests are: MESSAGE.
+ */
+static pj_bool_t mod_pjsua_on_rx_request(pjsip_rx_data *rdata)
+{
+ pj_bool_t processed = PJ_FALSE;
+
+ PJSUA_LOCK();
+
+ if (rdata->msg_info.msg->line.req.method.id == PJSIP_INVITE_METHOD) {
+
+ processed = pjsua_call_on_incoming(rdata);
+ }
+
+ PJSUA_UNLOCK();
+
+ return processed;
+}
+
+
+/*
+ * Handler for receiving incoming responses.
+ *
+ * This handler serves multiple purposes:
+ * - it receives strayed responses (i.e. outside any dialog and
+ * outside any transactions).
+ * - it receives responses coming to a transaction, when pjsua
+ * module is set as transaction user for the transaction.
+ * - it receives responses inside a dialog, when these responses
+ * are unhandled by other dialog usages.
+ */
+static pj_bool_t mod_pjsua_on_rx_response(pjsip_rx_data *rdata)
+{
+ PJ_UNUSED_ARG(rdata);
+ return PJ_FALSE;
+}
+
+
+/*****************************************************************************
+ * Logging.
+ */
+
+/* Log callback */
+static void log_writer(int level, const char *buffer, int len)
+{
+ /* Write to file, stdout or application callback. */
+
+ if (pjsua_var.log_file) {
+ pj_ssize_t size = len;
+ pj_file_write(pjsua_var.log_file, buffer, &size);
+ /* This will slow things down considerably! Don't do it!
+ pj_file_flush(pjsua_var.log_file);
+ */
+ }
+
+ if (level <= (int)pjsua_var.log_cfg.console_level) {
+ if (pjsua_var.log_cfg.cb)
+ (*pjsua_var.log_cfg.cb)(level, buffer, len);
+ else
+ pj_log_write(level, buffer, len);
+ }
+}
+
+
+/*
+ * Application can call this function at any time (after pjsua_create(), of
+ * course) to change logging settings.
+ */
+PJ_DEF(pj_status_t) pjsua_reconfigure_logging(const pjsua_logging_config *cfg)
+{
+ pj_status_t status;
+
+ /* Save config. */
+ pjsua_logging_config_dup(pjsua_var.pool, &pjsua_var.log_cfg, cfg);
+
+ /* Redirect log function to ours */
+ pj_log_set_log_func( &log_writer );
+
+ /* Set decor */
+ pj_log_set_decor(pjsua_var.log_cfg.decor);
+
+ /* Set log level */
+ pj_log_set_level(pjsua_var.log_cfg.level);
+
+ /* Close existing file, if any */
+ if (pjsua_var.log_file) {
+ pj_file_close(pjsua_var.log_file);
+ pjsua_var.log_file = NULL;
+ }
+
+ /* If output log file is desired, create the file: */
+ if (pjsua_var.log_cfg.log_filename.slen) {
+ unsigned flags = PJ_O_WRONLY;
+ flags |= pjsua_var.log_cfg.log_file_flags;
+ status = pj_file_open(pjsua_var.pool,
+ pjsua_var.log_cfg.log_filename.ptr,
+ flags,
+ &pjsua_var.log_file);
+
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Error creating log file", status);
+ return status;
+ }
+ }
+
+ /* Unregister msg logging if it's previously registered */
+ if (pjsua_msg_logger.id >= 0) {
+ pjsip_endpt_unregister_module(pjsua_var.endpt, &pjsua_msg_logger);
+ pjsua_msg_logger.id = -1;
+ }
+
+ /* Enable SIP message logging */
+ if (pjsua_var.log_cfg.msg_logging)
+ pjsip_endpt_register_module(pjsua_var.endpt, &pjsua_msg_logger);
+
+ return PJ_SUCCESS;
+}
+
+
+/*****************************************************************************
+ * PJSUA Base API.
+ */
+
+/* Worker thread function. */
+static int worker_thread(void *arg)
+{
+ enum { TIMEOUT = 10 };
+
+ PJ_UNUSED_ARG(arg);
+
+ while (!pjsua_var.thread_quit_flag) {
+ int count;
+
+ count = pjsua_handle_events(TIMEOUT);
+ if (count < 0)
+ pj_thread_sleep(TIMEOUT);
+ }
+
+ return 0;
+}
+
+
+/* Init random seed */
+static void init_random_seed(void)
+{
+ pj_sockaddr addr;
+ const pj_str_t *hostname;
+ pj_uint32_t pid;
+ pj_time_val t;
+ unsigned seed=0;
+
+ /* Add hostname */
+ hostname = pj_gethostname();
+ seed = pj_hash_calc(seed, hostname->ptr, (int)hostname->slen);
+
+ /* Add primary IP address */
+ if (pj_gethostip(pj_AF_INET(), &addr)==PJ_SUCCESS)
+ seed = pj_hash_calc(seed, &addr.ipv4.sin_addr, 4);
+
+ /* Get timeofday */
+ pj_gettimeofday(&t);
+ seed = pj_hash_calc(seed, &t, sizeof(t));
+
+ /* Add PID */
+ pid = pj_getpid();
+ seed = pj_hash_calc(seed, &pid, sizeof(pid));
+
+ /* Init random seed */
+ pj_srand(seed);
+}
+
+/*
+ * Instantiate pjsua application.
+ */
+PJ_DEF(pj_status_t) pjsua_create(void)
+{
+ pj_status_t status;
+
+ /* Init pjsua data */
+ init_data();
+
+ /* Set default logging settings */
+ pjsua_logging_config_default(&pjsua_var.log_cfg);
+
+ /* Init PJLIB: */
+ status = pj_init();
+ PJ_ASSERT_RETURN(status == PJ_SUCCESS, status);
+
+ pj_log_push_indent();
+
+ /* Init random seed */
+ init_random_seed();
+
+ /* Init PJLIB-UTIL: */
+ status = pjlib_util_init();
+ PJ_ASSERT_RETURN(status == PJ_SUCCESS, status);
+
+ /* Init PJNATH */
+ status = pjnath_init();
+ PJ_ASSERT_RETURN(status == PJ_SUCCESS, status);
+
+ /* Set default sound device ID */
+ pjsua_var.cap_dev = PJMEDIA_AUD_DEFAULT_CAPTURE_DEV;
+ pjsua_var.play_dev = PJMEDIA_AUD_DEFAULT_PLAYBACK_DEV;
+
+ /* Set default video device ID */
+ pjsua_var.vcap_dev = PJMEDIA_VID_DEFAULT_CAPTURE_DEV;
+ pjsua_var.vrdr_dev = PJMEDIA_VID_DEFAULT_RENDER_DEV;
+
+ /* Init caching pool. */
+ pj_caching_pool_init(&pjsua_var.cp, NULL, 0);
+
+ /* Create memory pool for application. */
+ pjsua_var.pool = pjsua_pool_create("pjsua", 1000, 1000);
+
+ PJ_ASSERT_RETURN(pjsua_var.pool, PJ_ENOMEM);
+
+ /* Create mutex */
+ status = pj_mutex_create_recursive(pjsua_var.pool, "pjsua",
+ &pjsua_var.mutex);
+ if (status != PJ_SUCCESS) {
+ pj_log_pop_indent();
+ pjsua_perror(THIS_FILE, "Unable to create mutex", status);
+ return status;
+ }
+
+ /* Must create SIP endpoint to initialize SIP parser. The parser
+ * is needed for example when application needs to call pjsua_verify_url().
+ */
+ status = pjsip_endpt_create(&pjsua_var.cp.factory,
+ pj_gethostname()->ptr,
+ &pjsua_var.endpt);
+ PJ_ASSERT_RETURN(status == PJ_SUCCESS, status);
+
+ /* Init timer entry list */
+ pj_list_init(&pjsua_var.timer_list);
+
+ /* Create timer mutex */
+ status = pj_mutex_create_recursive(pjsua_var.pool, "pjsua_timer",
+ &pjsua_var.timer_mutex);
+ if (status != PJ_SUCCESS) {
+ pj_log_pop_indent();
+ pjsua_perror(THIS_FILE, "Unable to create mutex", status);
+ return status;
+ }
+
+ pjsua_set_state(PJSUA_STATE_CREATED);
+ pj_log_pop_indent();
+ return PJ_SUCCESS;
+}
+
+
+/*
+ * Initialize pjsua with the specified settings. All the settings are
+ * optional, and the default values will be used when the config is not
+ * specified.
+ */
+PJ_DEF(pj_status_t) pjsua_init( const pjsua_config *ua_cfg,
+ const pjsua_logging_config *log_cfg,
+ const pjsua_media_config *media_cfg)
+{
+ pjsua_config default_cfg;
+ pjsua_media_config default_media_cfg;
+ const pj_str_t STR_OPTIONS = { "OPTIONS", 7 };
+ pjsip_ua_init_param ua_init_param;
+ unsigned i;
+ pj_status_t status;
+
+ pj_log_push_indent();
+
+ /* Create default configurations when the config is not supplied */
+
+ if (ua_cfg == NULL) {
+ pjsua_config_default(&default_cfg);
+ ua_cfg = &default_cfg;
+ }
+
+ if (media_cfg == NULL) {
+ pjsua_media_config_default(&default_media_cfg);
+ media_cfg = &default_media_cfg;
+ }
+
+ /* Initialize logging first so that info/errors can be captured */
+ if (log_cfg) {
+ status = pjsua_reconfigure_logging(log_cfg);
+ if (status != PJ_SUCCESS)
+ goto on_error;
+ }
+
+#if defined(PJ_IPHONE_OS_HAS_MULTITASKING_SUPPORT) && \
+ PJ_IPHONE_OS_HAS_MULTITASKING_SUPPORT != 0
+ if (!(pj_get_sys_info()->flags & PJ_SYS_HAS_IOS_BG)) {
+ PJ_LOG(5, (THIS_FILE, "Device does not support "
+ "background mode"));
+ pj_activesock_enable_iphone_os_bg(PJ_FALSE);
+ }
+#endif
+
+ /* If nameserver is configured, create DNS resolver instance and
+ * set it to be used by SIP resolver.
+ */
+ if (ua_cfg->nameserver_count) {
+#if PJSIP_HAS_RESOLVER
+ unsigned i;
+
+ /* Create DNS resolver */
+ status = pjsip_endpt_create_resolver(pjsua_var.endpt,
+ &pjsua_var.resolver);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Error creating resolver", status);
+ goto on_error;
+ }
+
+ /* Configure nameserver for the DNS resolver */
+ status = pj_dns_resolver_set_ns(pjsua_var.resolver,
+ ua_cfg->nameserver_count,
+ ua_cfg->nameserver, NULL);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Error setting nameserver", status);
+ goto on_error;
+ }
+
+ /* Set this DNS resolver to be used by the SIP resolver */
+ status = pjsip_endpt_set_resolver(pjsua_var.endpt, pjsua_var.resolver);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Error setting DNS resolver", status);
+ goto on_error;
+ }
+
+ /* Print nameservers */
+ for (i=0; i<ua_cfg->nameserver_count; ++i) {
+ PJ_LOG(4,(THIS_FILE, "Nameserver %.*s added",
+ (int)ua_cfg->nameserver[i].slen,
+ ua_cfg->nameserver[i].ptr));
+ }
+#else
+ PJ_LOG(2,(THIS_FILE,
+ "DNS resolver is disabled (PJSIP_HAS_RESOLVER==0)"));
+#endif
+ }
+
+ /* Init SIP UA: */
+
+ /* Initialize transaction layer: */
+ status = pjsip_tsx_layer_init_module(pjsua_var.endpt);
+ PJ_ASSERT_RETURN(status == PJ_SUCCESS, status);
+
+
+ /* Initialize UA layer module: */
+ pj_bzero(&ua_init_param, sizeof(ua_init_param));
+ if (ua_cfg->hangup_forked_call) {
+ ua_init_param.on_dlg_forked = &on_dlg_forked;
+ }
+ status = pjsip_ua_init_module( pjsua_var.endpt, &ua_init_param);
+ PJ_ASSERT_RETURN(status == PJ_SUCCESS, status);
+
+
+ /* Initialize Replaces support. */
+ status = pjsip_replaces_init_module( pjsua_var.endpt );
+ PJ_ASSERT_RETURN(status == PJ_SUCCESS, status);
+
+ /* Initialize 100rel support */
+ status = pjsip_100rel_init_module(pjsua_var.endpt);
+ PJ_ASSERT_RETURN(status == PJ_SUCCESS, status);
+
+ /* Initialize session timer support */
+ status = pjsip_timer_init_module(pjsua_var.endpt);
+ PJ_ASSERT_RETURN(status == PJ_SUCCESS, status);
+
+ /* Initialize and register PJSUA application module. */
+ {
+ const pjsip_module mod_initializer =
+ {
+ NULL, NULL, /* prev, next. */
+ { "mod-pjsua", 9 }, /* Name. */
+ -1, /* Id */
+ PJSIP_MOD_PRIORITY_APPLICATION, /* Priority */
+ NULL, /* load() */
+ NULL, /* start() */
+ NULL, /* stop() */
+ NULL, /* unload() */
+ &mod_pjsua_on_rx_request, /* on_rx_request() */
+ &mod_pjsua_on_rx_response, /* on_rx_response() */
+ NULL, /* on_tx_request. */
+ NULL, /* on_tx_response() */
+ NULL, /* on_tsx_state() */
+ };
+
+ pjsua_var.mod = mod_initializer;
+
+ status = pjsip_endpt_register_module(pjsua_var.endpt, &pjsua_var.mod);
+ PJ_ASSERT_RETURN(status == PJ_SUCCESS, status);
+ }
+
+ /* Parse outbound proxies */
+ for (i=0; i<ua_cfg->outbound_proxy_cnt; ++i) {
+ pj_str_t tmp;
+ pj_str_t hname = { "Route", 5};
+ pjsip_route_hdr *r;
+
+ pj_strdup_with_null(pjsua_var.pool, &tmp, &ua_cfg->outbound_proxy[i]);
+
+ r = (pjsip_route_hdr*)
+ pjsip_parse_hdr(pjsua_var.pool, &hname, tmp.ptr,
+ (unsigned)tmp.slen, NULL);
+ if (r == NULL) {
+ pjsua_perror(THIS_FILE, "Invalid outbound proxy URI",
+ PJSIP_EINVALIDURI);
+ status = PJSIP_EINVALIDURI;
+ goto on_error;
+ }
+
+ if (pjsua_var.ua_cfg.force_lr) {
+ pjsip_sip_uri *sip_url;
+ if (!PJSIP_URI_SCHEME_IS_SIP(r->name_addr.uri) &&
+ !PJSIP_URI_SCHEME_IS_SIP(r->name_addr.uri))
+ {
+ status = PJSIP_EINVALIDSCHEME;
+ goto on_error;
+ }
+ sip_url = (pjsip_sip_uri*)r->name_addr.uri;
+ sip_url->lr_param = 1;
+ }
+
+ pj_list_push_back(&pjsua_var.outbound_proxy, r);
+ }
+
+
+ /* Initialize PJSUA call subsystem: */
+ status = pjsua_call_subsys_init(ua_cfg);
+ if (status != PJ_SUCCESS)
+ goto on_error;
+
+ /* Convert deprecated STUN settings */
+ if (pjsua_var.ua_cfg.stun_srv_cnt==0) {
+ if (pjsua_var.ua_cfg.stun_domain.slen) {
+ pjsua_var.ua_cfg.stun_srv[pjsua_var.ua_cfg.stun_srv_cnt++] =
+ pjsua_var.ua_cfg.stun_domain;
+ }
+ if (pjsua_var.ua_cfg.stun_host.slen) {
+ pjsua_var.ua_cfg.stun_srv[pjsua_var.ua_cfg.stun_srv_cnt++] =
+ pjsua_var.ua_cfg.stun_host;
+ }
+ }
+
+ /* Start resolving STUN server */
+ status = resolve_stun_server(PJ_FALSE);
+ if (status != PJ_SUCCESS && status != PJ_EPENDING) {
+ pjsua_perror(THIS_FILE, "Error resolving STUN server", status);
+ goto on_error;
+ }
+
+ /* Initialize PJSUA media subsystem */
+ status = pjsua_media_subsys_init(media_cfg);
+ if (status != PJ_SUCCESS)
+ goto on_error;
+
+
+ /* Init core SIMPLE module : */
+ status = pjsip_evsub_init_module(pjsua_var.endpt);
+ PJ_ASSERT_RETURN(status == PJ_SUCCESS, status);
+
+
+ /* Init presence module: */
+ status = pjsip_pres_init_module( pjsua_var.endpt, pjsip_evsub_instance());
+ PJ_ASSERT_RETURN(status == PJ_SUCCESS, status);
+
+ /* Initialize MWI support */
+ status = pjsip_mwi_init_module(pjsua_var.endpt, pjsip_evsub_instance());
+
+ /* Init PUBLISH module */
+ pjsip_publishc_init_module(pjsua_var.endpt);
+
+ /* Init xfer/REFER module */
+ status = pjsip_xfer_init_module( pjsua_var.endpt );
+ PJ_ASSERT_RETURN(status == PJ_SUCCESS, status);
+
+ /* Init pjsua presence handler: */
+ status = pjsua_pres_init();
+ if (status != PJ_SUCCESS)
+ goto on_error;
+
+ /* Init out-of-dialog MESSAGE request handler. */
+ status = pjsua_im_init();
+ if (status != PJ_SUCCESS)
+ goto on_error;
+
+ /* Register OPTIONS handler */
+ pjsip_endpt_register_module(pjsua_var.endpt, &pjsua_options_handler);
+
+ /* Add OPTIONS in Allow header */
+ pjsip_endpt_add_capability(pjsua_var.endpt, NULL, PJSIP_H_ALLOW,
+ NULL, 1, &STR_OPTIONS);
+
+ /* Start worker thread if needed. */
+ if (pjsua_var.ua_cfg.thread_cnt) {
+ unsigned i;
+
+ if (pjsua_var.ua_cfg.thread_cnt > PJ_ARRAY_SIZE(pjsua_var.thread))
+ pjsua_var.ua_cfg.thread_cnt = PJ_ARRAY_SIZE(pjsua_var.thread);
+
+ for (i=0; i<pjsua_var.ua_cfg.thread_cnt; ++i) {
+ status = pj_thread_create(pjsua_var.pool, "pjsua", &worker_thread,
+ NULL, 0, 0, &pjsua_var.thread[i]);
+ if (status != PJ_SUCCESS)
+ goto on_error;
+ }
+ PJ_LOG(4,(THIS_FILE, "%d SIP worker threads created",
+ pjsua_var.ua_cfg.thread_cnt));
+ } else {
+ PJ_LOG(4,(THIS_FILE, "No SIP worker threads created"));
+ }
+
+ /* Done! */
+
+ PJ_LOG(3,(THIS_FILE, "pjsua version %s for %s initialized",
+ pj_get_version(), pj_get_sys_info()->info.ptr));
+
+ pjsua_set_state(PJSUA_STATE_INIT);
+ pj_log_pop_indent();
+ return PJ_SUCCESS;
+
+on_error:
+ pjsua_destroy();
+ pj_log_pop_indent();
+ return status;
+}
+
+
+/* Sleep with polling */
+static void busy_sleep(unsigned msec)
+{
+ pj_time_val timeout, now;
+
+ pj_gettimeofday(&timeout);
+ timeout.msec += msec;
+ pj_time_val_normalize(&timeout);
+
+ do {
+ int i;
+ i = msec / 10;
+ while (pjsua_handle_events(10) > 0 && i > 0)
+ --i;
+ pj_gettimeofday(&now);
+ } while (PJ_TIME_VAL_LT(now, timeout));
+}
+
+/* Internal function to destroy STUN resolution session
+ * (pj_stun_resolve).
+ */
+static void destroy_stun_resolve(pjsua_stun_resolve *sess)
+{
+ PJSUA_LOCK();
+ pj_list_erase(sess);
+ PJSUA_UNLOCK();
+
+ pj_assert(sess->stun_sock==NULL);
+ pj_pool_release(sess->pool);
+}
+
+/* This is the internal function to be called when STUN resolution
+ * session (pj_stun_resolve) has completed.
+ */
+static void stun_resolve_complete(pjsua_stun_resolve *sess)
+{
+ pj_stun_resolve_result result;
+
+ pj_bzero(&result, sizeof(result));
+ result.token = sess->token;
+ result.status = sess->status;
+ result.name = sess->srv[sess->idx];
+ pj_memcpy(&result.addr, &sess->addr, sizeof(result.addr));
+
+ if (result.status == PJ_SUCCESS) {
+ char addr[PJ_INET6_ADDRSTRLEN+10];
+ pj_sockaddr_print(&result.addr, addr, sizeof(addr), 3);
+ PJ_LOG(4,(THIS_FILE,
+ "STUN resolution success, using %.*s, address is %s",
+ (int)sess->srv[sess->idx].slen,
+ sess->srv[sess->idx].ptr,
+ addr));
+ } else {
+ char errmsg[PJ_ERR_MSG_SIZE];
+ pj_strerror(result.status, errmsg, sizeof(errmsg));
+ PJ_LOG(1,(THIS_FILE, "STUN resolution failed: %s", errmsg));
+ }
+
+ sess->cb(&result);
+
+ if (!sess->blocking) {
+ destroy_stun_resolve(sess);
+ }
+}
+
+/* This is the callback called by the STUN socket (pj_stun_sock)
+ * to report it's state. We use this as part of testing the
+ * STUN server.
+ */
+static pj_bool_t test_stun_on_status(pj_stun_sock *stun_sock,
+ pj_stun_sock_op op,
+ pj_status_t status)
+{
+ pjsua_stun_resolve *sess;
+
+ sess = (pjsua_stun_resolve*) pj_stun_sock_get_user_data(stun_sock);
+ pj_assert(stun_sock == sess->stun_sock);
+
+ if (status != PJ_SUCCESS) {
+ char errmsg[PJ_ERR_MSG_SIZE];
+ pj_strerror(status, errmsg, sizeof(errmsg));
+
+ PJ_LOG(4,(THIS_FILE, "STUN resolution for %.*s failed: %s",
+ (int)sess->srv[sess->idx].slen,
+ sess->srv[sess->idx].ptr, errmsg));
+
+ sess->status = status;
+
+ pj_stun_sock_destroy(stun_sock);
+ sess->stun_sock = NULL;
+
+ ++sess->idx;
+ resolve_stun_entry(sess);
+
+ return PJ_FALSE;
+
+ } else if (op == PJ_STUN_SOCK_BINDING_OP) {
+ pj_stun_sock_info ssi;
+
+ pj_stun_sock_get_info(stun_sock, &ssi);
+ pj_memcpy(&sess->addr, &ssi.srv_addr, sizeof(sess->addr));
+
+ sess->status = PJ_SUCCESS;
+ pj_stun_sock_destroy(stun_sock);
+ sess->stun_sock = NULL;
+
+ stun_resolve_complete(sess);
+
+ return PJ_FALSE;
+
+ } else
+ return PJ_TRUE;
+
+}
+
+/* This is an internal function to resolve and test current
+ * server entry in pj_stun_resolve session. It is called by
+ * pjsua_resolve_stun_servers() and test_stun_on_status() above
+ */
+static void resolve_stun_entry(pjsua_stun_resolve *sess)
+{
+ /* Loop while we have entry to try */
+ for (; sess->idx < sess->count; ++sess->idx) {
+ const int af = pj_AF_INET();
+ pj_str_t hostpart;
+ pj_uint16_t port;
+ pj_stun_sock_cb stun_sock_cb;
+
+ pj_assert(sess->idx < sess->count);
+
+ /* Parse the server entry into host:port */
+ sess->status = pj_sockaddr_parse2(af, 0, &sess->srv[sess->idx],
+ &hostpart, &port, NULL);
+ if (sess->status != PJ_SUCCESS) {
+ PJ_LOG(2,(THIS_FILE, "Invalid STUN server entry %.*s",
+ (int)sess->srv[sess->idx].slen,
+ sess->srv[sess->idx].ptr));
+ continue;
+ }
+
+ /* Use default port if not specified */
+ if (port == 0)
+ port = PJ_STUN_PORT;
+
+ pj_assert(sess->stun_sock == NULL);
+
+ PJ_LOG(4,(THIS_FILE, "Trying STUN server %.*s (%d of %d)..",
+ (int)sess->srv[sess->idx].slen,
+ sess->srv[sess->idx].ptr,
+ sess->idx+1, sess->count));
+
+ /* Use STUN_sock to test this entry */
+ pj_bzero(&stun_sock_cb, sizeof(stun_sock_cb));
+ stun_sock_cb.on_status = &test_stun_on_status;
+ sess->status = pj_stun_sock_create(&pjsua_var.stun_cfg, "stunresolve",
+ pj_AF_INET(), &stun_sock_cb,
+ NULL, sess, &sess->stun_sock);
+ if (sess->status != PJ_SUCCESS) {
+ char errmsg[PJ_ERR_MSG_SIZE];
+ pj_strerror(sess->status, errmsg, sizeof(errmsg));
+ PJ_LOG(4,(THIS_FILE,
+ "Error creating STUN socket for %.*s: %s",
+ (int)sess->srv[sess->idx].slen,
+ sess->srv[sess->idx].ptr, errmsg));
+
+ continue;
+ }
+
+ sess->status = pj_stun_sock_start(sess->stun_sock, &hostpart,
+ port, pjsua_var.resolver);
+ if (sess->status != PJ_SUCCESS) {
+ char errmsg[PJ_ERR_MSG_SIZE];
+ pj_strerror(sess->status, errmsg, sizeof(errmsg));
+ PJ_LOG(4,(THIS_FILE,
+ "Error starting STUN socket for %.*s: %s",
+ (int)sess->srv[sess->idx].slen,
+ sess->srv[sess->idx].ptr, errmsg));
+
+ pj_stun_sock_destroy(sess->stun_sock);
+ sess->stun_sock = NULL;
+ continue;
+ }
+
+ /* Done for now, testing will resume/complete asynchronously in
+ * stun_sock_cb()
+ */
+ return;
+ }
+
+ if (sess->idx >= sess->count) {
+ /* No more entries to try */
+ PJ_ASSERT_ON_FAIL(sess->status != PJ_SUCCESS,
+ sess->status = PJ_EUNKNOWN);
+ stun_resolve_complete(sess);
+ }
+}
+
+
+/*
+ * Resolve STUN server.
+ */
+PJ_DEF(pj_status_t) pjsua_resolve_stun_servers( unsigned count,
+ pj_str_t srv[],
+ pj_bool_t wait,
+ void *token,
+ pj_stun_resolve_cb cb)
+{
+ pj_pool_t *pool;
+ pjsua_stun_resolve *sess;
+ pj_status_t status;
+ unsigned i;
+
+ PJ_ASSERT_RETURN(count && srv && cb, PJ_EINVAL);
+
+ pool = pjsua_pool_create("stunres", 256, 256);
+ if (!pool)
+ return PJ_ENOMEM;
+
+ sess = PJ_POOL_ZALLOC_T(pool, pjsua_stun_resolve);
+ sess->pool = pool;
+ sess->token = token;
+ sess->cb = cb;
+ sess->count = count;
+ sess->blocking = wait;
+ sess->status = PJ_EPENDING;
+ sess->srv = (pj_str_t*) pj_pool_calloc(pool, count, sizeof(pj_str_t));
+ for (i=0; i<count; ++i) {
+ pj_strdup(pool, &sess->srv[i], &srv[i]);
+ }
+
+ PJSUA_LOCK();
+ pj_list_push_back(&pjsua_var.stun_res, sess);
+ PJSUA_UNLOCK();
+
+ resolve_stun_entry(sess);
+
+ if (!wait)
+ return PJ_SUCCESS;
+
+ while (sess->status == PJ_EPENDING) {
+ pjsua_handle_events(50);
+ }
+
+ status = sess->status;
+ destroy_stun_resolve(sess);
+
+ return status;
+}
+
+/*
+ * Cancel pending STUN resolution.
+ */
+PJ_DEF(pj_status_t) pjsua_cancel_stun_resolution( void *token,
+ pj_bool_t notify_cb)
+{
+ pjsua_stun_resolve *sess;
+ unsigned cancelled_count = 0;
+
+ PJSUA_LOCK();
+ sess = pjsua_var.stun_res.next;
+ while (sess != &pjsua_var.stun_res) {
+ pjsua_stun_resolve *next = sess->next;
+
+ if (sess->token == token) {
+ if (notify_cb) {
+ pj_stun_resolve_result result;
+
+ pj_bzero(&result, sizeof(result));
+ result.token = token;
+ result.status = PJ_ECANCELLED;
+
+ sess->cb(&result);
+ }
+
+ destroy_stun_resolve(sess);
+ ++cancelled_count;
+ }
+
+ sess = next;
+ }
+ PJSUA_UNLOCK();
+
+ return cancelled_count ? PJ_SUCCESS : PJ_ENOTFOUND;
+}
+
+static void internal_stun_resolve_cb(const pj_stun_resolve_result *result)
+{
+ pjsua_var.stun_status = result->status;
+ if (result->status == PJ_SUCCESS) {
+ pj_memcpy(&pjsua_var.stun_srv, &result->addr, sizeof(result->addr));
+ }
+}
+
+/*
+ * Resolve STUN server.
+ */
+pj_status_t resolve_stun_server(pj_bool_t wait)
+{
+ if (pjsua_var.stun_status == PJ_EUNKNOWN) {
+ pj_status_t status;
+
+ /* Initialize STUN configuration */
+ pj_stun_config_init(&pjsua_var.stun_cfg, &pjsua_var.cp.factory, 0,
+ pjsip_endpt_get_ioqueue(pjsua_var.endpt),
+ pjsip_endpt_get_timer_heap(pjsua_var.endpt));
+
+ /* Start STUN server resolution */
+ if (pjsua_var.ua_cfg.stun_srv_cnt) {
+ pjsua_var.stun_status = PJ_EPENDING;
+ status = pjsua_resolve_stun_servers(pjsua_var.ua_cfg.stun_srv_cnt,
+ pjsua_var.ua_cfg.stun_srv,
+ wait, NULL,
+ &internal_stun_resolve_cb);
+ if (wait || status != PJ_SUCCESS) {
+ pjsua_var.stun_status = status;
+ }
+ } else {
+ pjsua_var.stun_status = PJ_SUCCESS;
+ }
+
+ } else if (pjsua_var.stun_status == PJ_EPENDING) {
+ /* STUN server resolution has been started, wait for the
+ * result.
+ */
+ if (wait) {
+ while (pjsua_var.stun_status == PJ_EPENDING) {
+ if (pjsua_var.thread[0] == NULL)
+ pjsua_handle_events(10);
+ else
+ pj_thread_sleep(10);
+ }
+ }
+ }
+
+ if (pjsua_var.stun_status != PJ_EPENDING &&
+ pjsua_var.stun_status != PJ_SUCCESS &&
+ pjsua_var.ua_cfg.stun_ignore_failure)
+ {
+ PJ_LOG(2,(THIS_FILE,
+ "Ignoring STUN resolution failure (by setting)"));
+ pjsua_var.stun_status = PJ_SUCCESS;
+ }
+
+ return pjsua_var.stun_status;
+}
+
+/*
+ * Destroy pjsua.
+ */
+PJ_DEF(pj_status_t) pjsua_destroy2(unsigned flags)
+{
+ int i; /* Must be signed */
+
+ if (pjsua_var.endpt) {
+ PJ_LOG(4,(THIS_FILE, "Shutting down, flags=%d...", flags));
+ }
+
+ if (pjsua_var.state > PJSUA_STATE_NULL &&
+ pjsua_var.state < PJSUA_STATE_CLOSING)
+ {
+ pjsua_set_state(PJSUA_STATE_CLOSING);
+ }
+
+ /* Signal threads to quit: */
+ pjsua_var.thread_quit_flag = 1;
+
+ /* Wait worker threads to quit: */
+ for (i=0; i<(int)pjsua_var.ua_cfg.thread_cnt; ++i) {
+ if (pjsua_var.thread[i]) {
+ pj_status_t status;
+ status = pj_thread_join(pjsua_var.thread[i]);
+ if (status != PJ_SUCCESS) {
+ PJ_PERROR(4,(THIS_FILE, status, "Error joining worker thread"));
+ pj_thread_sleep(1000);
+ }
+ pj_thread_destroy(pjsua_var.thread[i]);
+ pjsua_var.thread[i] = NULL;
+ }
+ }
+
+ if (pjsua_var.endpt) {
+ unsigned max_wait;
+
+ pj_log_push_indent();
+
+ /* Terminate all calls. */
+ if ((flags & PJSUA_DESTROY_NO_TX_MSG) == 0) {
+ pjsua_call_hangup_all();
+ }
+
+ /* Set all accounts to offline */
+ for (i=0; i<(int)PJ_ARRAY_SIZE(pjsua_var.acc); ++i) {
+ if (!pjsua_var.acc[i].valid)
+ continue;
+ pjsua_var.acc[i].online_status = PJ_FALSE;
+ pj_bzero(&pjsua_var.acc[i].rpid, sizeof(pjrpid_element));
+ }
+
+ /* Terminate all presence subscriptions. */
+ pjsua_pres_shutdown(flags);
+
+ /* Destroy media (to shutdown media transports etc) */
+ pjsua_media_subsys_destroy(flags);
+
+ /* Wait for sometime until all publish client sessions are done
+ * (ticket #364)
+ */
+ /* First stage, get the maximum wait time */
+ max_wait = 100;
+ for (i=0; i<(int)PJ_ARRAY_SIZE(pjsua_var.acc); ++i) {
+ if (!pjsua_var.acc[i].valid)
+ continue;
+ if (pjsua_var.acc[i].cfg.unpublish_max_wait_time_msec > max_wait)
+ max_wait = pjsua_var.acc[i].cfg.unpublish_max_wait_time_msec;
+ }
+
+ /* No waiting if RX is disabled */
+ if (flags & PJSUA_DESTROY_NO_RX_MSG) {
+ max_wait = 0;
+ }
+
+ /* Second stage, wait for unpublications to complete */
+ for (i=0; i<(int)(max_wait/50); ++i) {
+ unsigned j;
+ for (j=0; j<PJ_ARRAY_SIZE(pjsua_var.acc); ++j) {
+ if (!pjsua_var.acc[j].valid)
+ continue;
+
+ if (pjsua_var.acc[j].publish_sess)
+ break;
+ }
+ if (j != PJ_ARRAY_SIZE(pjsua_var.acc))
+ busy_sleep(50);
+ else
+ break;
+ }
+
+ /* Third stage, forcefully destroy unfinished unpublications */
+ for (i=0; i<(int)PJ_ARRAY_SIZE(pjsua_var.acc); ++i) {
+ if (pjsua_var.acc[i].publish_sess) {
+ pjsip_publishc_destroy(pjsua_var.acc[i].publish_sess);
+ pjsua_var.acc[i].publish_sess = NULL;
+ }
+ }
+
+ /* Unregister all accounts */
+ for (i=0; i<(int)PJ_ARRAY_SIZE(pjsua_var.acc); ++i) {
+ if (!pjsua_var.acc[i].valid)
+ continue;
+
+ if (pjsua_var.acc[i].regc && (flags & PJSUA_DESTROY_NO_TX_MSG)==0)
+ {
+ pjsua_acc_set_registration(i, PJ_FALSE);
+ }
+ }
+
+ /* Terminate any pending STUN resolution */
+ if (!pj_list_empty(&pjsua_var.stun_res)) {
+ pjsua_stun_resolve *sess = pjsua_var.stun_res.next;
+ while (sess != &pjsua_var.stun_res) {
+ pjsua_stun_resolve *next = sess->next;
+ destroy_stun_resolve(sess);
+ sess = next;
+ }
+ }
+
+ /* Wait until all unregistrations are done (ticket #364) */
+ /* First stage, get the maximum wait time */
+ max_wait = 100;
+ for (i=0; i<(int)PJ_ARRAY_SIZE(pjsua_var.acc); ++i) {
+ if (!pjsua_var.acc[i].valid)
+ continue;
+ if (pjsua_var.acc[i].cfg.unreg_timeout > max_wait)
+ max_wait = pjsua_var.acc[i].cfg.unreg_timeout;
+ }
+
+ /* No waiting if RX is disabled */
+ if (flags & PJSUA_DESTROY_NO_RX_MSG) {
+ max_wait = 0;
+ }
+
+ /* Second stage, wait for unregistrations to complete */
+ for (i=0; i<(int)(max_wait/50); ++i) {
+ unsigned j;
+ for (j=0; j<PJ_ARRAY_SIZE(pjsua_var.acc); ++j) {
+ if (!pjsua_var.acc[j].valid)
+ continue;
+
+ if (pjsua_var.acc[j].regc)
+ break;
+ }
+ if (j != PJ_ARRAY_SIZE(pjsua_var.acc))
+ busy_sleep(50);
+ else
+ break;
+ }
+ /* Note variable 'i' is used below */
+
+ /* Wait for some time to allow unregistration and ICE/TURN
+ * transports shutdown to complete:
+ */
+ if (i < 20 && (flags & PJSUA_DESTROY_NO_RX_MSG) == 0) {
+ busy_sleep(1000 - i*50);
+ }
+
+ PJ_LOG(4,(THIS_FILE, "Destroying..."));
+
+ /* Must destroy endpoint first before destroying pools in
+ * buddies or accounts, since shutting down transaction layer
+ * may emit events which trigger some buddy or account callbacks
+ * to be called.
+ */
+ pjsip_endpt_destroy(pjsua_var.endpt);
+ pjsua_var.endpt = NULL;
+
+ /* Destroy pool in the buddy object */
+ for (i=0; i<(int)PJ_ARRAY_SIZE(pjsua_var.buddy); ++i) {
+ if (pjsua_var.buddy[i].pool) {
+ pj_pool_release(pjsua_var.buddy[i].pool);
+ pjsua_var.buddy[i].pool = NULL;
+ }
+ }
+
+ /* Destroy accounts */
+ for (i=0; i<(int)PJ_ARRAY_SIZE(pjsua_var.acc); ++i) {
+ if (pjsua_var.acc[i].pool) {
+ pj_pool_release(pjsua_var.acc[i].pool);
+ pjsua_var.acc[i].pool = NULL;
+ }
+ }
+ }
+
+ /* Destroy mutex */
+ if (pjsua_var.mutex) {
+ pj_mutex_destroy(pjsua_var.mutex);
+ pjsua_var.mutex = NULL;
+ }
+
+ /* Destroy pool and pool factory. */
+ if (pjsua_var.pool) {
+ pj_pool_release(pjsua_var.pool);
+ pjsua_var.pool = NULL;
+ pj_caching_pool_destroy(&pjsua_var.cp);
+
+ pjsua_set_state(PJSUA_STATE_NULL);
+
+ PJ_LOG(4,(THIS_FILE, "PJSUA destroyed..."));
+
+ /* End logging */
+ if (pjsua_var.log_file) {
+ pj_file_close(pjsua_var.log_file);
+ pjsua_var.log_file = NULL;
+ }
+
+ pj_log_pop_indent();
+
+ /* Shutdown PJLIB */
+ pj_shutdown();
+ }
+
+ /* Clear pjsua_var */
+ pj_bzero(&pjsua_var, sizeof(pjsua_var));
+
+ /* Done. */
+ return PJ_SUCCESS;
+}
+
+void pjsua_set_state(pjsua_state new_state)
+{
+ const char *state_name[] = {
+ "NULL",
+ "CREATED",
+ "INIT",
+ "STARTING",
+ "RUNNING",
+ "CLOSING"
+ };
+ pjsua_state old_state = pjsua_var.state;
+
+ pjsua_var.state = new_state;
+ PJ_LOG(4,(THIS_FILE, "PJSUA state changed: %s --> %s",
+ state_name[old_state], state_name[new_state]));
+}
+
+/* Get state */
+PJ_DEF(pjsua_state) pjsua_get_state(void)
+{
+ return pjsua_var.state;
+}
+
+PJ_DEF(pj_status_t) pjsua_destroy(void)
+{
+ return pjsua_destroy2(0);
+}
+
+
+/**
+ * Application is recommended to call this function after all initialization
+ * is done, so that the library can do additional checking set up
+ * additional
+ *
+ * @return PJ_SUCCESS on success, or the appropriate error code.
+ */
+PJ_DEF(pj_status_t) pjsua_start(void)
+{
+ pj_status_t status;
+
+ pjsua_set_state(PJSUA_STATE_STARTING);
+ pj_log_push_indent();
+
+ status = pjsua_call_subsys_start();
+ if (status != PJ_SUCCESS)
+ goto on_return;
+
+ status = pjsua_media_subsys_start();
+ if (status != PJ_SUCCESS)
+ goto on_return;
+
+ status = pjsua_pres_start();
+ if (status != PJ_SUCCESS)
+ goto on_return;
+
+ pjsua_set_state(PJSUA_STATE_RUNNING);
+
+on_return:
+ pj_log_pop_indent();
+ return status;
+}
+
+
+/**
+ * Poll pjsua for events, and if necessary block the caller thread for
+ * the specified maximum interval (in miliseconds).
+ */
+PJ_DEF(int) pjsua_handle_events(unsigned msec_timeout)
+{
+#if defined(PJ_SYMBIAN) && PJ_SYMBIAN != 0
+
+ return pj_symbianos_poll(-1, msec_timeout);
+
+#else
+
+ unsigned count = 0;
+ pj_time_val tv;
+ pj_status_t status;
+
+ tv.sec = 0;
+ tv.msec = msec_timeout;
+ pj_time_val_normalize(&tv);
+
+ status = pjsip_endpt_handle_events2(pjsua_var.endpt, &tv, &count);
+
+ if (status != PJ_SUCCESS)
+ return -status;
+
+ return count;
+
+#endif
+}
+
+
+/*
+ * Create memory pool.
+ */
+PJ_DEF(pj_pool_t*) pjsua_pool_create( const char *name, pj_size_t init_size,
+ pj_size_t increment)
+{
+ /* Pool factory is thread safe, no need to lock */
+ return pj_pool_create(&pjsua_var.cp.factory, name, init_size, increment,
+ NULL);
+}
+
+
+/*
+ * Internal function to get SIP endpoint instance of pjsua, which is
+ * needed for example to register module, create transports, etc.
+ * Probably is only valid after #pjsua_init() is called.
+ */
+PJ_DEF(pjsip_endpoint*) pjsua_get_pjsip_endpt(void)
+{
+ return pjsua_var.endpt;
+}
+
+/*
+ * Internal function to get media endpoint instance.
+ * Only valid after #pjsua_init() is called.
+ */
+PJ_DEF(pjmedia_endpt*) pjsua_get_pjmedia_endpt(void)
+{
+ return pjsua_var.med_endpt;
+}
+
+/*
+ * Internal function to get PJSUA pool factory.
+ */
+PJ_DEF(pj_pool_factory*) pjsua_get_pool_factory(void)
+{
+ return &pjsua_var.cp.factory;
+}
+
+/*****************************************************************************
+ * PJSUA SIP Transport API.
+ */
+
+/*
+ * Tools to get address string.
+ */
+static const char *addr_string(const pj_sockaddr_t *addr)
+{
+ static char str[128];
+ str[0] = '\0';
+ pj_inet_ntop(((const pj_sockaddr*)addr)->addr.sa_family,
+ pj_sockaddr_get_addr(addr),
+ str, sizeof(str));
+ return str;
+}
+
+void pjsua_acc_on_tp_state_changed(pjsip_transport *tp,
+ pjsip_transport_state state,
+ const pjsip_transport_state_info *info);
+
+/* Callback to receive transport state notifications */
+static void on_tp_state_callback(pjsip_transport *tp,
+ pjsip_transport_state state,
+ const pjsip_transport_state_info *info)
+{
+ if (pjsua_var.ua_cfg.cb.on_transport_state) {
+ (*pjsua_var.ua_cfg.cb.on_transport_state)(tp, state, info);
+ }
+ if (pjsua_var.old_tp_cb) {
+ (*pjsua_var.old_tp_cb)(tp, state, info);
+ }
+ pjsua_acc_on_tp_state_changed(tp, state, info);
+}
+
+/*
+ * Create and initialize SIP socket (and possibly resolve public
+ * address via STUN, depending on config).
+ */
+static pj_status_t create_sip_udp_sock(int af,
+ const pjsua_transport_config *cfg,
+ pj_sock_t *p_sock,
+ pj_sockaddr *p_pub_addr)
+{
+ char stun_ip_addr[PJ_INET6_ADDRSTRLEN];
+ unsigned port = cfg->port;
+ pj_str_t stun_srv;
+ pj_sock_t sock;
+ pj_sockaddr bind_addr;
+ pj_status_t status;
+
+ /* Make sure STUN server resolution has completed */
+ status = resolve_stun_server(PJ_TRUE);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Error resolving STUN server", status);
+ return status;
+ }
+
+ /* Initialize bound address */
+ if (cfg->bound_addr.slen) {
+ status = pj_sockaddr_init(af, &bind_addr, &cfg->bound_addr,
+ (pj_uint16_t)port);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE,
+ "Unable to resolve transport bound address",
+ status);
+ return status;
+ }
+ } else {
+ pj_sockaddr_init(af, &bind_addr, NULL, (pj_uint16_t)port);
+ }
+
+ /* Create socket */
+ status = pj_sock_socket(af, pj_SOCK_DGRAM(), 0, &sock);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "socket() error", status);
+ return status;
+ }
+
+ /* Apply QoS, if specified */
+ status = pj_sock_apply_qos2(sock, cfg->qos_type,
+ &cfg->qos_params,
+ 2, THIS_FILE, "SIP UDP socket");
+
+ /* Bind socket */
+ status = pj_sock_bind(sock, &bind_addr, pj_sockaddr_get_len(&bind_addr));
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "bind() error", status);
+ pj_sock_close(sock);
+ return status;
+ }
+
+ /* If port is zero, get the bound port */
+ if (port == 0) {
+ pj_sockaddr bound_addr;
+ int namelen = sizeof(bound_addr);
+ status = pj_sock_getsockname(sock, &bound_addr, &namelen);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "getsockname() error", status);
+ pj_sock_close(sock);
+ return status;
+ }
+
+ port = pj_sockaddr_get_port(&bound_addr);
+ }
+
+ if (pjsua_var.stun_srv.addr.sa_family != 0) {
+ pj_ansi_strcpy(stun_ip_addr,pj_inet_ntoa(pjsua_var.stun_srv.ipv4.sin_addr));
+ stun_srv = pj_str(stun_ip_addr);
+ } else {
+ stun_srv.slen = 0;
+ }
+
+ /* Get the published address, either by STUN or by resolving
+ * the name of local host.
+ */
+ if (pj_sockaddr_has_addr(p_pub_addr)) {
+ /*
+ * Public address is already specified, no need to resolve the
+ * address, only set the port.
+ */
+ if (pj_sockaddr_get_port(p_pub_addr) == 0)
+ pj_sockaddr_set_port(p_pub_addr, (pj_uint16_t)port);
+
+ } else if (stun_srv.slen) {
+ /*
+ * STUN is specified, resolve the address with STUN.
+ */
+ if (af != pj_AF_INET()) {
+ pjsua_perror(THIS_FILE, "Cannot use STUN", PJ_EAFNOTSUP);
+ pj_sock_close(sock);
+ return PJ_EAFNOTSUP;
+ }
+
+ status = pjstun_get_mapped_addr(&pjsua_var.cp.factory, 1, &sock,
+ &stun_srv, pj_ntohs(pjsua_var.stun_srv.ipv4.sin_port),
+ &stun_srv, pj_ntohs(pjsua_var.stun_srv.ipv4.sin_port),
+ &p_pub_addr->ipv4);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Error contacting STUN server", status);
+ pj_sock_close(sock);
+ return status;
+ }
+
+ } else {
+ pj_bzero(p_pub_addr, sizeof(pj_sockaddr));
+
+ if (pj_sockaddr_has_addr(&bind_addr)) {
+ pj_sockaddr_copy_addr(p_pub_addr, &bind_addr);
+ } else {
+ status = pj_gethostip(af, p_pub_addr);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Unable to get local host IP", status);
+ pj_sock_close(sock);
+ return status;
+ }
+ }
+
+ p_pub_addr->addr.sa_family = (pj_uint16_t)af;
+ pj_sockaddr_set_port(p_pub_addr, (pj_uint16_t)port);
+ }
+
+ *p_sock = sock;
+
+ PJ_LOG(4,(THIS_FILE, "SIP UDP socket reachable at %s:%d",
+ addr_string(p_pub_addr),
+ (int)pj_sockaddr_get_port(p_pub_addr)));
+
+ return PJ_SUCCESS;
+}
+
+
+/*
+ * Create SIP transport.
+ */
+PJ_DEF(pj_status_t) pjsua_transport_create( pjsip_transport_type_e type,
+ const pjsua_transport_config *cfg,
+ pjsua_transport_id *p_id)
+{
+ pjsip_transport *tp;
+ unsigned id;
+ pj_status_t status;
+
+ PJSUA_LOCK();
+
+ /* Find empty transport slot */
+ for (id=0; id < PJ_ARRAY_SIZE(pjsua_var.tpdata); ++id) {
+ if (pjsua_var.tpdata[id].data.ptr == NULL)
+ break;
+ }
+
+ if (id == PJ_ARRAY_SIZE(pjsua_var.tpdata)) {
+ status = PJ_ETOOMANY;
+ pjsua_perror(THIS_FILE, "Error creating transport", status);
+ goto on_return;
+ }
+
+ /* Create the transport */
+ if (type==PJSIP_TRANSPORT_UDP || type==PJSIP_TRANSPORT_UDP6) {
+ /*
+ * Create UDP transport (IPv4 or IPv6).
+ */
+ pjsua_transport_config config;
+ char hostbuf[PJ_INET6_ADDRSTRLEN];
+ pj_sock_t sock = PJ_INVALID_SOCKET;
+ pj_sockaddr pub_addr;
+ pjsip_host_port addr_name;
+
+ /* Supply default config if it's not specified */
+ if (cfg == NULL) {
+ pjsua_transport_config_default(&config);
+ cfg = &config;
+ }
+
+ /* Initialize the public address from the config, if any */
+ pj_sockaddr_init(pjsip_transport_type_get_af(type), &pub_addr,
+ NULL, (pj_uint16_t)cfg->port);
+ if (cfg->public_addr.slen) {
+ status = pj_sockaddr_set_str_addr(pjsip_transport_type_get_af(type),
+ &pub_addr, &cfg->public_addr);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE,
+ "Unable to resolve transport public address",
+ status);
+ goto on_return;
+ }
+ }
+
+ /* Create the socket and possibly resolve the address with STUN
+ * (only when public address is not specified).
+ */
+ status = create_sip_udp_sock(pjsip_transport_type_get_af(type),
+ cfg, &sock, &pub_addr);
+ if (status != PJ_SUCCESS)
+ goto on_return;
+
+ pj_ansi_strcpy(hostbuf, addr_string(&pub_addr));
+ addr_name.host = pj_str(hostbuf);
+ addr_name.port = pj_sockaddr_get_port(&pub_addr);
+
+ /* Create UDP transport */
+ status = pjsip_udp_transport_attach2(pjsua_var.endpt, type, sock,
+ &addr_name, 1, &tp);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Error creating SIP UDP transport",
+ status);
+ pj_sock_close(sock);
+ goto on_return;
+ }
+
+
+ /* Save the transport */
+ pjsua_var.tpdata[id].type = type;
+ pjsua_var.tpdata[id].local_name = tp->local_name;
+ pjsua_var.tpdata[id].data.tp = tp;
+
+#if defined(PJ_HAS_TCP) && PJ_HAS_TCP!=0
+
+ } else if (type == PJSIP_TRANSPORT_TCP || type == PJSIP_TRANSPORT_TCP6) {
+ /*
+ * Create TCP transport.
+ */
+ pjsua_transport_config config;
+ pjsip_tpfactory *tcp;
+ pjsip_tcp_transport_cfg tcp_cfg;
+
+ pjsip_tcp_transport_cfg_default(&tcp_cfg, pj_AF_INET());
+
+ /* Supply default config if it's not specified */
+ if (cfg == NULL) {
+ pjsua_transport_config_default(&config);
+ cfg = &config;
+ }
+
+ /* Configure bind address */
+ if (cfg->port)
+ pj_sockaddr_set_port(&tcp_cfg.bind_addr, (pj_uint16_t)cfg->port);
+
+ if (cfg->bound_addr.slen) {
+ status = pj_sockaddr_set_str_addr(tcp_cfg.af,
+ &tcp_cfg.bind_addr,
+ &cfg->bound_addr);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE,
+ "Unable to resolve transport bound address",
+ status);
+ goto on_return;
+ }
+ }
+
+ /* Set published name */
+ if (cfg->public_addr.slen)
+ tcp_cfg.addr_name.host = cfg->public_addr;
+
+ /* Copy the QoS settings */
+ tcp_cfg.qos_type = cfg->qos_type;
+ pj_memcpy(&tcp_cfg.qos_params, &cfg->qos_params,
+ sizeof(cfg->qos_params));
+
+ /* Create the TCP transport */
+ status = pjsip_tcp_transport_start3(pjsua_var.endpt, &tcp_cfg, &tcp);
+
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Error creating SIP TCP listener",
+ status);
+ goto on_return;
+ }
+
+ /* Save the transport */
+ pjsua_var.tpdata[id].type = type;
+ pjsua_var.tpdata[id].local_name = tcp->addr_name;
+ pjsua_var.tpdata[id].data.factory = tcp;
+
+#endif /* PJ_HAS_TCP */
+
+#if defined(PJSIP_HAS_TLS_TRANSPORT) && PJSIP_HAS_TLS_TRANSPORT!=0
+ } else if (type == PJSIP_TRANSPORT_TLS) {
+ /*
+ * Create TLS transport.
+ */
+ pjsua_transport_config config;
+ pjsip_host_port a_name;
+ pjsip_tpfactory *tls;
+ pj_sockaddr_in local_addr;
+
+ /* Supply default config if it's not specified */
+ if (cfg == NULL) {
+ pjsua_transport_config_default(&config);
+ config.port = 5061;
+ cfg = &config;
+ }
+
+ /* Init local address */
+ pj_sockaddr_in_init(&local_addr, 0, 0);
+
+ if (cfg->port)
+ local_addr.sin_port = pj_htons((pj_uint16_t)cfg->port);
+
+ if (cfg->bound_addr.slen) {
+ status = pj_sockaddr_in_set_str_addr(&local_addr,&cfg->bound_addr);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE,
+ "Unable to resolve transport bound address",
+ status);
+ goto on_return;
+ }
+ }
+
+ /* Init published name */
+ pj_bzero(&a_name, sizeof(pjsip_host_port));
+ if (cfg->public_addr.slen)
+ a_name.host = cfg->public_addr;
+
+ status = pjsip_tls_transport_start(pjsua_var.endpt,
+ &cfg->tls_setting,
+ &local_addr, &a_name, 1, &tls);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Error creating SIP TLS listener",
+ status);
+ goto on_return;
+ }
+
+ /* Save the transport */
+ pjsua_var.tpdata[id].type = type;
+ pjsua_var.tpdata[id].local_name = tls->addr_name;
+ pjsua_var.tpdata[id].data.factory = tls;
+#endif
+
+ } else {
+ status = PJSIP_EUNSUPTRANSPORT;
+ pjsua_perror(THIS_FILE, "Error creating transport", status);
+ goto on_return;
+ }
+
+ /* Set transport state callback */
+ if (pjsua_var.ua_cfg.cb.on_transport_state) {
+ pjsip_tp_state_callback tpcb;
+ pjsip_tpmgr *tpmgr;
+
+ tpmgr = pjsip_endpt_get_tpmgr(pjsua_var.endpt);
+ tpcb = pjsip_tpmgr_get_state_cb(tpmgr);
+
+ if (tpcb != &on_tp_state_callback) {
+ pjsua_var.old_tp_cb = tpcb;
+ pjsip_tpmgr_set_state_cb(tpmgr, &on_tp_state_callback);
+ }
+ }
+
+ /* Return the ID */
+ if (p_id) *p_id = id;
+
+ status = PJ_SUCCESS;
+
+on_return:
+
+ PJSUA_UNLOCK();
+
+ return status;
+}
+
+
+/*
+ * Register transport that has been created by application.
+ */
+PJ_DEF(pj_status_t) pjsua_transport_register( pjsip_transport *tp,
+ pjsua_transport_id *p_id)
+{
+ unsigned id;
+
+ PJSUA_LOCK();
+
+ /* Find empty transport slot */
+ for (id=0; id < PJ_ARRAY_SIZE(pjsua_var.tpdata); ++id) {
+ if (pjsua_var.tpdata[id].data.ptr == NULL)
+ break;
+ }
+
+ if (id == PJ_ARRAY_SIZE(pjsua_var.tpdata)) {
+ pjsua_perror(THIS_FILE, "Error creating transport", PJ_ETOOMANY);
+ PJSUA_UNLOCK();
+ return PJ_ETOOMANY;
+ }
+
+ /* Save the transport */
+ pjsua_var.tpdata[id].type = (pjsip_transport_type_e) tp->key.type;
+ pjsua_var.tpdata[id].local_name = tp->local_name;
+ pjsua_var.tpdata[id].data.tp = tp;
+
+ /* Return the ID */
+ if (p_id) *p_id = id;
+
+ PJSUA_UNLOCK();
+
+ return PJ_SUCCESS;
+}
+
+
+/*
+ * Enumerate all transports currently created in the system.
+ */
+PJ_DEF(pj_status_t) pjsua_enum_transports( pjsua_transport_id id[],
+ unsigned *p_count )
+{
+ unsigned i, count;
+
+ PJSUA_LOCK();
+
+ for (i=0, count=0; i<PJ_ARRAY_SIZE(pjsua_var.tpdata) && count<*p_count;
+ ++i)
+ {
+ if (!pjsua_var.tpdata[i].data.ptr)
+ continue;
+
+ id[count++] = i;
+ }
+
+ *p_count = count;
+
+ PJSUA_UNLOCK();
+
+ return PJ_SUCCESS;
+}
+
+
+/*
+ * Get information about transports.
+ */
+PJ_DEF(pj_status_t) pjsua_transport_get_info( pjsua_transport_id id,
+ pjsua_transport_info *info)
+{
+ pjsua_transport_data *t = &pjsua_var.tpdata[id];
+ pj_status_t status;
+
+ pj_bzero(info, sizeof(*info));
+
+ /* Make sure id is in range. */
+ PJ_ASSERT_RETURN(id>=0 && id<(int)PJ_ARRAY_SIZE(pjsua_var.tpdata),
+ PJ_EINVAL);
+
+ /* Make sure that transport exists */
+ PJ_ASSERT_RETURN(pjsua_var.tpdata[id].data.ptr != NULL, PJ_EINVAL);
+
+ PJSUA_LOCK();
+
+ if (t->type == PJSIP_TRANSPORT_UDP) {
+
+ pjsip_transport *tp = t->data.tp;
+
+ if (tp == NULL) {
+ PJSUA_UNLOCK();
+ return PJ_EINVALIDOP;
+ }
+
+ info->id = id;
+ info->type = (pjsip_transport_type_e) tp->key.type;
+ info->type_name = pj_str(tp->type_name);
+ info->info = pj_str(tp->info);
+ info->flag = tp->flag;
+ info->addr_len = tp->addr_len;
+ info->local_addr = tp->local_addr;
+ info->local_name = tp->local_name;
+ info->usage_count = pj_atomic_get(tp->ref_cnt);
+
+ status = PJ_SUCCESS;
+
+ } else if (t->type == PJSIP_TRANSPORT_TCP ||
+ t->type == PJSIP_TRANSPORT_TLS)
+ {
+
+ pjsip_tpfactory *factory = t->data.factory;
+
+ if (factory == NULL) {
+ PJSUA_UNLOCK();
+ return PJ_EINVALIDOP;
+ }
+
+ info->id = id;
+ info->type = t->type;
+ info->type_name = (t->type==PJSIP_TRANSPORT_TCP)? pj_str("TCP"):
+ pj_str("TLS");
+ info->info = (t->type==PJSIP_TRANSPORT_TCP)? pj_str("TCP transport"):
+ pj_str("TLS transport");
+ info->flag = factory->flag;
+ info->addr_len = sizeof(factory->local_addr);
+ info->local_addr = factory->local_addr;
+ info->local_name = factory->addr_name;
+ info->usage_count = 0;
+
+ status = PJ_SUCCESS;
+
+ } else {
+ pj_assert(!"Unsupported transport");
+ status = PJ_EINVALIDOP;
+ }
+
+
+ PJSUA_UNLOCK();
+
+ return status;
+}
+
+
+/*
+ * Disable a transport or re-enable it.
+ */
+PJ_DEF(pj_status_t) pjsua_transport_set_enable( pjsua_transport_id id,
+ pj_bool_t enabled)
+{
+ /* Make sure id is in range. */
+ PJ_ASSERT_RETURN(id>=0 && id<(int)PJ_ARRAY_SIZE(pjsua_var.tpdata),
+ PJ_EINVAL);
+
+ /* Make sure that transport exists */
+ PJ_ASSERT_RETURN(pjsua_var.tpdata[id].data.ptr != NULL, PJ_EINVAL);
+
+
+ /* To be done!! */
+ PJ_TODO(pjsua_transport_set_enable);
+ PJ_UNUSED_ARG(enabled);
+
+ return PJ_EINVALIDOP;
+}
+
+
+/*
+ * Close the transport.
+ */
+PJ_DEF(pj_status_t) pjsua_transport_close( pjsua_transport_id id,
+ pj_bool_t force )
+{
+ pj_status_t status;
+
+ /* Make sure id is in range. */
+ PJ_ASSERT_RETURN(id>=0 && id<(int)PJ_ARRAY_SIZE(pjsua_var.tpdata),
+ PJ_EINVAL);
+
+ /* Make sure that transport exists */
+ PJ_ASSERT_RETURN(pjsua_var.tpdata[id].data.ptr != NULL, PJ_EINVAL);
+
+ /* Note: destroy() may not work if there are objects still referencing
+ * the transport.
+ */
+ if (force) {
+ switch (pjsua_var.tpdata[id].type) {
+ case PJSIP_TRANSPORT_UDP:
+ status = pjsip_transport_shutdown(pjsua_var.tpdata[id].data.tp);
+ if (status != PJ_SUCCESS)
+ return status;
+ status = pjsip_transport_destroy(pjsua_var.tpdata[id].data.tp);
+ if (status != PJ_SUCCESS)
+ return status;
+ break;
+
+ case PJSIP_TRANSPORT_TLS:
+ case PJSIP_TRANSPORT_TCP:
+ /* This will close the TCP listener, but existing TCP/TLS
+ * connections (if any) will still linger
+ */
+ status = (*pjsua_var.tpdata[id].data.factory->destroy)
+ (pjsua_var.tpdata[id].data.factory);
+ if (status != PJ_SUCCESS)
+ return status;
+
+ break;
+
+ default:
+ return PJ_EINVAL;
+ }
+
+ } else {
+ /* If force is not specified, transports will be closed at their
+ * convenient time. However this will leak PJSUA-API transport
+ * descriptors as PJSUA-API wouldn't know when exactly the
+ * transport is closed thus it can't cleanup PJSUA transport
+ * descriptor.
+ */
+ switch (pjsua_var.tpdata[id].type) {
+ case PJSIP_TRANSPORT_UDP:
+ return pjsip_transport_shutdown(pjsua_var.tpdata[id].data.tp);
+ case PJSIP_TRANSPORT_TLS:
+ case PJSIP_TRANSPORT_TCP:
+ return (*pjsua_var.tpdata[id].data.factory->destroy)
+ (pjsua_var.tpdata[id].data.factory);
+ default:
+ return PJ_EINVAL;
+ }
+ }
+
+ /* Cleanup pjsua data when force is applied */
+ if (force) {
+ pjsua_var.tpdata[id].type = PJSIP_TRANSPORT_UNSPECIFIED;
+ pjsua_var.tpdata[id].data.ptr = NULL;
+ }
+
+ return PJ_SUCCESS;
+}
+
+
+/*
+ * Add additional headers etc in msg_data specified by application
+ * when sending requests.
+ */
+void pjsua_process_msg_data(pjsip_tx_data *tdata,
+ const pjsua_msg_data *msg_data)
+{
+ pj_bool_t allow_body;
+ const pjsip_hdr *hdr;
+
+ /* Always add User-Agent */
+ if (pjsua_var.ua_cfg.user_agent.slen &&
+ tdata->msg->type == PJSIP_REQUEST_MSG)
+ {
+ const pj_str_t STR_USER_AGENT = { "User-Agent", 10 };
+ pjsip_hdr *h;
+ h = (pjsip_hdr*)pjsip_generic_string_hdr_create(tdata->pool,
+ &STR_USER_AGENT,
+ &pjsua_var.ua_cfg.user_agent);
+ pjsip_msg_add_hdr(tdata->msg, h);
+ }
+
+ if (!msg_data)
+ return;
+
+ hdr = msg_data->hdr_list.next;
+ while (hdr && hdr != &msg_data->hdr_list) {
+ pjsip_hdr *new_hdr;
+
+ new_hdr = (pjsip_hdr*) pjsip_hdr_clone(tdata->pool, hdr);
+ pjsip_msg_add_hdr(tdata->msg, new_hdr);
+
+ hdr = hdr->next;
+ }
+
+ allow_body = (tdata->msg->body == NULL);
+
+ if (allow_body && msg_data->content_type.slen && msg_data->msg_body.slen) {
+ pjsip_media_type ctype;
+ pjsip_msg_body *body;
+
+ pjsua_parse_media_type(tdata->pool, &msg_data->content_type, &ctype);
+ body = pjsip_msg_body_create(tdata->pool, &ctype.type, &ctype.subtype,
+ &msg_data->msg_body);
+ tdata->msg->body = body;
+ }
+
+ /* Multipart */
+ if (!pj_list_empty(&msg_data->multipart_parts) &&
+ msg_data->multipart_ctype.type.slen)
+ {
+ pjsip_msg_body *bodies;
+ pjsip_multipart_part *part;
+ pj_str_t *boundary = NULL;
+
+ bodies = pjsip_multipart_create(tdata->pool,
+ &msg_data->multipart_ctype,
+ boundary);
+ part = msg_data->multipart_parts.next;
+ while (part != &msg_data->multipart_parts) {
+ pjsip_multipart_part *part_copy;
+
+ part_copy = pjsip_multipart_clone_part(tdata->pool, part);
+ pjsip_multipart_add_part(tdata->pool, bodies, part_copy);
+ part = part->next;
+ }
+
+ if (tdata->msg->body) {
+ part = pjsip_multipart_create_part(tdata->pool);
+ part->body = tdata->msg->body;
+ pjsip_multipart_add_part(tdata->pool, bodies, part);
+
+ tdata->msg->body = NULL;
+ }
+
+ tdata->msg->body = bodies;
+ }
+}
+
+
+/*
+ * Add route_set to outgoing requests
+ */
+void pjsua_set_msg_route_set( pjsip_tx_data *tdata,
+ const pjsip_route_hdr *route_set )
+{
+ const pjsip_route_hdr *r;
+
+ r = route_set->next;
+ while (r != route_set) {
+ pjsip_route_hdr *new_r;
+
+ new_r = (pjsip_route_hdr*) pjsip_hdr_clone(tdata->pool, r);
+ pjsip_msg_add_hdr(tdata->msg, (pjsip_hdr*)new_r);
+
+ r = r->next;
+ }
+}
+
+
+/*
+ * Simple version of MIME type parsing (it doesn't support parameters)
+ */
+void pjsua_parse_media_type( pj_pool_t *pool,
+ const pj_str_t *mime,
+ pjsip_media_type *media_type)
+{
+ pj_str_t tmp;
+ char *pos;
+
+ pj_bzero(media_type, sizeof(*media_type));
+
+ pj_strdup_with_null(pool, &tmp, mime);
+
+ pos = pj_strchr(&tmp, '/');
+ if (pos) {
+ media_type->type.ptr = tmp.ptr;
+ media_type->type.slen = (pos-tmp.ptr);
+ media_type->subtype.ptr = pos+1;
+ media_type->subtype.slen = tmp.ptr+tmp.slen-pos-1;
+ } else {
+ media_type->type = tmp;
+ }
+}
+
+
+/*
+ * Internal function to init transport selector from transport id.
+ */
+void pjsua_init_tpselector(pjsua_transport_id tp_id,
+ pjsip_tpselector *sel)
+{
+ pjsua_transport_data *tpdata;
+ unsigned flag;
+
+ pj_bzero(sel, sizeof(*sel));
+ if (tp_id == PJSUA_INVALID_ID)
+ return;
+
+ pj_assert(tp_id >= 0 && tp_id < (int)PJ_ARRAY_SIZE(pjsua_var.tpdata));
+ tpdata = &pjsua_var.tpdata[tp_id];
+
+ flag = pjsip_transport_get_flag_from_type(tpdata->type);
+
+ if (flag & PJSIP_TRANSPORT_DATAGRAM) {
+ sel->type = PJSIP_TPSELECTOR_TRANSPORT;
+ sel->u.transport = tpdata->data.tp;
+ } else {
+ sel->type = PJSIP_TPSELECTOR_LISTENER;
+ sel->u.listener = tpdata->data.factory;
+ }
+}
+
+
+/* Callback upon NAT detection completion */
+static void nat_detect_cb(void *user_data,
+ const pj_stun_nat_detect_result *res)
+{
+ PJ_UNUSED_ARG(user_data);
+
+ pjsua_var.nat_in_progress = PJ_FALSE;
+ pjsua_var.nat_status = res->status;
+ pjsua_var.nat_type = res->nat_type;
+
+ if (pjsua_var.ua_cfg.cb.on_nat_detect) {
+ (*pjsua_var.ua_cfg.cb.on_nat_detect)(res);
+ }
+}
+
+
+/*
+ * Detect NAT type.
+ */
+PJ_DEF(pj_status_t) pjsua_detect_nat_type()
+{
+ pj_status_t status;
+
+ if (pjsua_var.nat_in_progress)
+ return PJ_SUCCESS;
+
+ /* Make sure STUN server resolution has completed */
+ status = resolve_stun_server(PJ_TRUE);
+ if (status != PJ_SUCCESS) {
+ pjsua_var.nat_status = status;
+ pjsua_var.nat_type = PJ_STUN_NAT_TYPE_ERR_UNKNOWN;
+ return status;
+ }
+
+ /* Make sure we have STUN */
+ if (pjsua_var.stun_srv.ipv4.sin_family == 0) {
+ pjsua_var.nat_status = PJNATH_ESTUNINSERVER;
+ return PJNATH_ESTUNINSERVER;
+ }
+
+ status = pj_stun_detect_nat_type(&pjsua_var.stun_srv.ipv4,
+ &pjsua_var.stun_cfg,
+ NULL, &nat_detect_cb);
+
+ if (status != PJ_SUCCESS) {
+ pjsua_var.nat_status = status;
+ pjsua_var.nat_type = PJ_STUN_NAT_TYPE_ERR_UNKNOWN;
+ return status;
+ }
+
+ pjsua_var.nat_in_progress = PJ_TRUE;
+
+ return PJ_SUCCESS;
+}
+
+
+/*
+ * Get NAT type.
+ */
+PJ_DEF(pj_status_t) pjsua_get_nat_type(pj_stun_nat_type *type)
+{
+ *type = pjsua_var.nat_type;
+ return pjsua_var.nat_status;
+}
+
+/*
+ * Verify that valid url is given.
+ */
+PJ_DEF(pj_status_t) pjsua_verify_url(const char *c_url)
+{
+ pjsip_uri *p;
+ pj_pool_t *pool;
+ char *url;
+ int len = (c_url ? pj_ansi_strlen(c_url) : 0);
+
+ if (!len) return PJSIP_EINVALIDURI;
+
+ pool = pj_pool_create(&pjsua_var.cp.factory, "check%p", 1024, 0, NULL);
+ if (!pool) return PJ_ENOMEM;
+
+ url = (char*) pj_pool_alloc(pool, len+1);
+ pj_ansi_strcpy(url, c_url);
+
+ p = pjsip_parse_uri(pool, url, len, 0);
+
+ pj_pool_release(pool);
+ return p ? 0 : PJSIP_EINVALIDURI;
+}
+
+/*
+ * Verify that valid SIP url is given.
+ */
+PJ_DEF(pj_status_t) pjsua_verify_sip_url(const char *c_url)
+{
+ pjsip_uri *p;
+ pj_pool_t *pool;
+ char *url;
+ int len = (c_url ? pj_ansi_strlen(c_url) : 0);
+
+ if (!len) return PJSIP_EINVALIDURI;
+
+ pool = pj_pool_create(&pjsua_var.cp.factory, "check%p", 1024, 0, NULL);
+ if (!pool) return PJ_ENOMEM;
+
+ url = (char*) pj_pool_alloc(pool, len+1);
+ pj_ansi_strcpy(url, c_url);
+
+ p = pjsip_parse_uri(pool, url, len, 0);
+ if (!p || (pj_stricmp2(pjsip_uri_get_scheme(p), "sip") != 0 &&
+ pj_stricmp2(pjsip_uri_get_scheme(p), "sips") != 0))
+ {
+ p = NULL;
+ }
+
+ pj_pool_release(pool);
+ return p ? 0 : PJSIP_EINVALIDURI;
+}
+
+/*
+ * Schedule a timer entry.
+ */
+#if PJ_TIMER_DEBUG
+PJ_DEF(pj_status_t) pjsua_schedule_timer_dbg( pj_timer_entry *entry,
+ const pj_time_val *delay,
+ const char *src_file,
+ int src_line)
+{
+ return pjsip_endpt_schedule_timer_dbg(pjsua_var.endpt, entry, delay,
+ src_file, src_line);
+}
+#else
+PJ_DEF(pj_status_t) pjsua_schedule_timer( pj_timer_entry *entry,
+ const pj_time_val *delay)
+{
+ return pjsip_endpt_schedule_timer(pjsua_var.endpt, entry, delay);
+}
+#endif
+
+/* Timer callback */
+static void timer_cb( pj_timer_heap_t *th,
+ pj_timer_entry *entry)
+{
+ pjsua_timer_list *tmr = (pjsua_timer_list *)entry->user_data;
+ void (*cb)(void *user_data) = tmr->cb;
+ void *user_data = tmr->user_data;
+
+ PJ_UNUSED_ARG(th);
+
+ pj_mutex_lock(pjsua_var.timer_mutex);
+ pj_list_push_back(&pjsua_var.timer_list, tmr);
+ pj_mutex_unlock(pjsua_var.timer_mutex);
+
+ if (cb)
+ (*cb)(user_data);
+}
+
+/*
+ * Schedule a timer callback.
+ */
+#if PJ_TIMER_DEBUG
+PJ_DEF(pj_status_t) pjsua_schedule_timer2_dbg( void (*cb)(void *user_data),
+ void *user_data,
+ unsigned msec_delay,
+ const char *src_file,
+ int src_line)
+#else
+PJ_DEF(pj_status_t) pjsua_schedule_timer2( void (*cb)(void *user_data),
+ void *user_data,
+ unsigned msec_delay)
+#endif
+{
+ pjsua_timer_list *tmr = NULL;
+ pj_status_t status;
+ pj_time_val delay;
+
+ pj_mutex_lock(pjsua_var.timer_mutex);
+
+ if (pj_list_empty(&pjsua_var.timer_list)) {
+ tmr = PJ_POOL_ALLOC_T(pjsua_var.pool, pjsua_timer_list);
+ } else {
+ tmr = pjsua_var.timer_list.next;
+ pj_list_erase(tmr);
+ }
+ pj_timer_entry_init(&tmr->entry, 0, tmr, timer_cb);
+ tmr->cb = cb;
+ tmr->user_data = user_data;
+ delay.sec = 0;
+ delay.msec = msec_delay;
+
+#if PJ_TIMER_DEBUG
+ status = pjsip_endpt_schedule_timer_dbg(pjsua_var.endpt, &tmr->entry,
+ &delay, src_file, src_line);
+#else
+ status = pjsip_endpt_schedule_timer(pjsua_var.endpt, &tmr->entry, &delay);
+#endif
+ if (status != PJ_SUCCESS) {
+ pj_list_push_back(&pjsua_var.timer_list, tmr);
+ }
+
+ pj_mutex_unlock(pjsua_var.timer_mutex);
+
+ return status;
+}
+
+/*
+ * Cancel the previously scheduled timer.
+ *
+ */
+PJ_DEF(void) pjsua_cancel_timer(pj_timer_entry *entry)
+{
+ pjsip_endpt_cancel_timer(pjsua_var.endpt, entry);
+}
+
+/**
+ * Normalize route URI (check for ";lr" and append one if it doesn't
+ * exist and pjsua_config.force_lr is set.
+ */
+pj_status_t normalize_route_uri(pj_pool_t *pool, pj_str_t *uri)
+{
+ pj_str_t tmp_uri;
+ pj_pool_t *tmp_pool;
+ pjsip_uri *uri_obj;
+ pjsip_sip_uri *sip_uri;
+
+ tmp_pool = pjsua_pool_create("tmplr%p", 512, 512);
+ if (!tmp_pool)
+ return PJ_ENOMEM;
+
+ pj_strdup_with_null(tmp_pool, &tmp_uri, uri);
+
+ uri_obj = pjsip_parse_uri(tmp_pool, tmp_uri.ptr, tmp_uri.slen, 0);
+ if (!uri_obj) {
+ PJ_LOG(1,(THIS_FILE, "Invalid route URI: %.*s",
+ (int)uri->slen, uri->ptr));
+ pj_pool_release(tmp_pool);
+ return PJSIP_EINVALIDURI;
+ }
+
+ if (!PJSIP_URI_SCHEME_IS_SIP(uri_obj) &&
+ !PJSIP_URI_SCHEME_IS_SIP(uri_obj))
+ {
+ PJ_LOG(1,(THIS_FILE, "Route URI must be SIP URI: %.*s",
+ (int)uri->slen, uri->ptr));
+ pj_pool_release(tmp_pool);
+ return PJSIP_EINVALIDSCHEME;
+ }
+
+ sip_uri = (pjsip_sip_uri*) pjsip_uri_get_uri(uri_obj);
+
+ /* Done if force_lr is disabled or if lr parameter is present */
+ if (!pjsua_var.ua_cfg.force_lr || sip_uri->lr_param) {
+ pj_pool_release(tmp_pool);
+ return PJ_SUCCESS;
+ }
+
+ /* Set lr param */
+ sip_uri->lr_param = 1;
+
+ /* Print the URI */
+ tmp_uri.ptr = (char*) pj_pool_alloc(tmp_pool, PJSIP_MAX_URL_SIZE);
+ tmp_uri.slen = pjsip_uri_print(PJSIP_URI_IN_ROUTING_HDR, uri_obj,
+ tmp_uri.ptr, PJSIP_MAX_URL_SIZE);
+ if (tmp_uri.slen < 1) {
+ PJ_LOG(1,(THIS_FILE, "Route URI is too long: %.*s",
+ (int)uri->slen, uri->ptr));
+ pj_pool_release(tmp_pool);
+ return PJSIP_EURITOOLONG;
+ }
+
+ /* Clone the URI */
+ pj_strdup_with_null(pool, uri, &tmp_uri);
+
+ pj_pool_release(tmp_pool);
+ return PJ_SUCCESS;
+}
+
+/*
+ * This is a utility function to dump the stack states to log, using
+ * verbosity level 3.
+ */
+PJ_DEF(void) pjsua_dump(pj_bool_t detail)
+{
+ unsigned old_decor;
+ unsigned i;
+
+ PJ_LOG(3,(THIS_FILE, "Start dumping application states:"));
+
+ old_decor = pj_log_get_decor();
+ pj_log_set_decor(old_decor & (PJ_LOG_HAS_NEWLINE | PJ_LOG_HAS_CR));
+
+ if (detail)
+ pj_dump_config();
+
+ pjsip_endpt_dump(pjsua_get_pjsip_endpt(), detail);
+
+ pjmedia_endpt_dump(pjsua_get_pjmedia_endpt());
+
+ PJ_LOG(3,(THIS_FILE, "Dumping media transports:"));
+ for (i=0; i<pjsua_var.ua_cfg.max_calls; ++i) {
+ pjsua_call *call = &pjsua_var.calls[i];
+ pjmedia_transport *tp[PJSUA_MAX_CALL_MEDIA*2];
+ unsigned tp_cnt = 0;
+ unsigned j;
+
+ /* Collect media transports in this call */
+ for (j = 0; j < call->med_cnt; ++j) {
+ if (call->media[j].tp != NULL)
+ tp[tp_cnt++] = call->media[j].tp;
+ }
+ for (j = 0; j < call->med_prov_cnt; ++j) {
+ pjmedia_transport *med_tp = call->media_prov[j].tp;
+ if (med_tp) {
+ unsigned k;
+ pj_bool_t used = PJ_FALSE;
+ for (k = 0; k < tp_cnt; ++k) {
+ if (med_tp == tp[k]) {
+ used = PJ_TRUE;
+ break;
+ }
+ }
+ if (!used)
+ tp[tp_cnt++] = med_tp;
+ }
+ }
+
+ /* Dump the media transports in this call */
+ for (j = 0; j < tp_cnt; ++j) {
+ pjmedia_transport_info tpinfo;
+ char addr_buf[80];
+
+ pjmedia_transport_info_init(&tpinfo);
+ pjmedia_transport_get_info(tp[j], &tpinfo);
+ PJ_LOG(3,(THIS_FILE, " %s: %s",
+ (pjsua_var.media_cfg.enable_ice ? "ICE" : "UDP"),
+ pj_sockaddr_print(&tpinfo.sock_info.rtp_addr_name,
+ addr_buf,
+ sizeof(addr_buf), 3)));
+ }
+ }
+
+ pjsip_tsx_layer_dump(detail);
+ pjsip_ua_dump(detail);
+
+// Dumping complete call states may require a 'large' buffer
+// (about 3KB per call session, including RTCP XR).
+#if 0
+ /* Dump all invite sessions: */
+ PJ_LOG(3,(THIS_FILE, "Dumping invite sessions:"));
+
+ if (pjsua_call_get_count() == 0) {
+
+ PJ_LOG(3,(THIS_FILE, " - no sessions -"));
+
+ } else {
+ unsigned i;
+
+ for (i=0; i<pjsua_var.ua_cfg.max_calls; ++i) {
+ if (pjsua_call_is_active(i)) {
+ /* Tricky logging, since call states log string tends to be
+ * longer than PJ_LOG_MAX_SIZE.
+ */
+ char buf[1024 * 3];
+ unsigned call_dump_len;
+ unsigned part_len;
+ unsigned part_idx;
+ unsigned log_decor;
+
+ pjsua_call_dump(i, detail, buf, sizeof(buf), " ");
+ call_dump_len = strlen(buf);
+
+ log_decor = pj_log_get_decor();
+ pj_log_set_decor(log_decor & ~(PJ_LOG_HAS_NEWLINE |
+ PJ_LOG_HAS_CR));
+ PJ_LOG(3,(THIS_FILE, "\n"));
+ pj_log_set_decor(0);
+
+ part_idx = 0;
+ part_len = PJ_LOG_MAX_SIZE-80;
+ while (part_idx < call_dump_len) {
+ char p_orig, *p;
+
+ p = &buf[part_idx];
+ if (part_idx + part_len > call_dump_len)
+ part_len = call_dump_len - part_idx;
+ p_orig = p[part_len];
+ p[part_len] = '\0';
+ PJ_LOG(3,(THIS_FILE, "%s", p));
+ p[part_len] = p_orig;
+ part_idx += part_len;
+ }
+ pj_log_set_decor(log_decor);
+ }
+ }
+ }
+#endif
+
+ /* Dump presence status */
+ pjsua_pres_dump(detail);
+
+ pj_log_set_decor(old_decor);
+ PJ_LOG(3,(THIS_FILE, "Dump complete"));
+}
+
diff --git a/pjsip/src/pjsua-lib/pjsua_dump.c b/pjsip/src/pjsua-lib/pjsua_dump.c
new file mode 100644
index 0000000..0b23a97
--- /dev/null
+++ b/pjsip/src/pjsua-lib/pjsua_dump.c
@@ -0,0 +1,974 @@
+/* $Id: pjsua_dump.c 4085 2012-04-25 07:45:22Z nanang $ */
+/*
+ * Copyright (C) 2011-2011 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
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ */
+#include <pjsua-lib/pjsua.h>
+#include <pjsua-lib/pjsua_internal.h>
+
+const char *good_number(char *buf, pj_int32_t val)
+{
+ if (val < 1000) {
+ pj_ansi_sprintf(buf, "%d", val);
+ } else if (val < 1000000) {
+ pj_ansi_sprintf(buf, "%d.%dK",
+ val / 1000,
+ (val % 1000) / 100);
+ } else {
+ pj_ansi_sprintf(buf, "%d.%02dM",
+ val / 1000000,
+ (val % 1000000) / 10000);
+ }
+
+ return buf;
+}
+
+static unsigned dump_media_stat(const char *indent,
+ char *buf, unsigned maxlen,
+ const pjmedia_rtcp_stat *stat,
+ const char *rx_info, const char *tx_info)
+{
+ char last_update[64];
+ char packets[32], bytes[32], ipbytes[32], avg_bps[32], avg_ipbps[32];
+ pj_time_val media_duration, now;
+ char *p = buf, *end = buf+maxlen;
+ int len;
+
+ if (stat->rx.update_cnt == 0)
+ strcpy(last_update, "never");
+ else {
+ pj_gettimeofday(&now);
+ PJ_TIME_VAL_SUB(now, stat->rx.update);
+ sprintf(last_update, "%02ldh:%02ldm:%02ld.%03lds ago",
+ now.sec / 3600,
+ (now.sec % 3600) / 60,
+ now.sec % 60,
+ now.msec);
+ }
+
+ pj_gettimeofday(&media_duration);
+ PJ_TIME_VAL_SUB(media_duration, stat->start);
+ if (PJ_TIME_VAL_MSEC(media_duration) == 0)
+ media_duration.msec = 1;
+
+ len = pj_ansi_snprintf(p, end-p,
+ "%s RX %s last update:%s\n"
+ "%s total %spkt %sB (%sB +IP hdr) @avg=%sbps/%sbps\n"
+ "%s pkt loss=%d (%3.1f%%), discrd=%d (%3.1f%%), dup=%d (%2.1f%%), reord=%d (%3.1f%%)\n"
+ "%s (msec) min avg max last dev\n"
+ "%s loss period: %7.3f %7.3f %7.3f %7.3f %7.3f\n"
+ "%s jitter : %7.3f %7.3f %7.3f %7.3f %7.3f\n"
+#if defined(PJMEDIA_RTCP_STAT_HAS_RAW_JITTER) && PJMEDIA_RTCP_STAT_HAS_RAW_JITTER!=0
+ "%s raw jitter : %7.3f %7.3f %7.3f %7.3f %7.3f\n"
+#endif
+#if defined(PJMEDIA_RTCP_STAT_HAS_IPDV) && PJMEDIA_RTCP_STAT_HAS_IPDV!=0
+ "%s IPDV : %7.3f %7.3f %7.3f %7.3f %7.3f\n"
+#endif
+ "%s",
+ indent,
+ rx_info? rx_info : "",
+ last_update,
+
+ indent,
+ good_number(packets, stat->rx.pkt),
+ good_number(bytes, stat->rx.bytes),
+ good_number(ipbytes, stat->rx.bytes + stat->rx.pkt * 40),
+ good_number(avg_bps, (pj_int32_t)((pj_int64_t)stat->rx.bytes * 8 * 1000 / PJ_TIME_VAL_MSEC(media_duration))),
+ good_number(avg_ipbps, (pj_int32_t)(((pj_int64_t)stat->rx.bytes + stat->rx.pkt * 40) * 8 * 1000 / PJ_TIME_VAL_MSEC(media_duration))),
+ indent,
+ stat->rx.loss,
+ (stat->rx.loss? stat->rx.loss * 100.0 / (stat->rx.pkt + stat->rx.loss) : 0),
+ stat->rx.discard,
+ (stat->rx.discard? stat->rx.discard * 100.0 / (stat->rx.pkt + stat->rx.loss) : 0),
+ stat->rx.dup,
+ (stat->rx.dup? stat->rx.dup * 100.0 / (stat->rx.pkt + stat->rx.loss) : 0),
+ stat->rx.reorder,
+ (stat->rx.reorder? stat->rx.reorder * 100.0 / (stat->rx.pkt + stat->rx.loss) : 0),
+ indent, indent,
+ stat->rx.loss_period.min / 1000.0,
+ stat->rx.loss_period.mean / 1000.0,
+ stat->rx.loss_period.max / 1000.0,
+ stat->rx.loss_period.last / 1000.0,
+ pj_math_stat_get_stddev(&stat->rx.loss_period) / 1000.0,
+ indent,
+ stat->rx.jitter.min / 1000.0,
+ stat->rx.jitter.mean / 1000.0,
+ stat->rx.jitter.max / 1000.0,
+ stat->rx.jitter.last / 1000.0,
+ pj_math_stat_get_stddev(&stat->rx.jitter) / 1000.0,
+#if defined(PJMEDIA_RTCP_STAT_HAS_RAW_JITTER) && PJMEDIA_RTCP_STAT_HAS_RAW_JITTER!=0
+ indent,
+ stat->rx_raw_jitter.min / 1000.0,
+ stat->rx_raw_jitter.mean / 1000.0,
+ stat->rx_raw_jitter.max / 1000.0,
+ stat->rx_raw_jitter.last / 1000.0,
+ pj_math_stat_get_stddev(&stat->rx_raw_jitter) / 1000.0,
+#endif
+#if defined(PJMEDIA_RTCP_STAT_HAS_IPDV) && PJMEDIA_RTCP_STAT_HAS_IPDV!=0
+ indent,
+ stat->rx_ipdv.min / 1000.0,
+ stat->rx_ipdv.mean / 1000.0,
+ stat->rx_ipdv.max / 1000.0,
+ stat->rx_ipdv.last / 1000.0,
+ pj_math_stat_get_stddev(&stat->rx_ipdv) / 1000.0,
+#endif
+ ""
+ );
+
+ if (len < 1 || len > end-p) {
+ *p = '\0';
+ return (p-buf);
+ }
+ p += len;
+
+ if (stat->tx.update_cnt == 0)
+ strcpy(last_update, "never");
+ else {
+ pj_gettimeofday(&now);
+ PJ_TIME_VAL_SUB(now, stat->tx.update);
+ sprintf(last_update, "%02ldh:%02ldm:%02ld.%03lds ago",
+ now.sec / 3600,
+ (now.sec % 3600) / 60,
+ now.sec % 60,
+ now.msec);
+ }
+
+ len = pj_ansi_snprintf(p, end-p,
+ "%s TX %s last update:%s\n"
+ "%s total %spkt %sB (%sB +IP hdr) @avg=%sbps/%sbps\n"
+ "%s pkt loss=%d (%3.1f%%), dup=%d (%3.1f%%), reorder=%d (%3.1f%%)\n"
+ "%s (msec) min avg max last dev \n"
+ "%s loss period: %7.3f %7.3f %7.3f %7.3f %7.3f\n"
+ "%s jitter : %7.3f %7.3f %7.3f %7.3f %7.3f\n",
+ indent,
+ tx_info,
+ last_update,
+
+ indent,
+ good_number(packets, stat->tx.pkt),
+ good_number(bytes, stat->tx.bytes),
+ good_number(ipbytes, stat->tx.bytes + stat->tx.pkt * 40),
+ good_number(avg_bps, (pj_int32_t)((pj_int64_t)stat->tx.bytes * 8 * 1000 / PJ_TIME_VAL_MSEC(media_duration))),
+ good_number(avg_ipbps, (pj_int32_t)(((pj_int64_t)stat->tx.bytes + stat->tx.pkt * 40) * 8 * 1000 / PJ_TIME_VAL_MSEC(media_duration))),
+
+ indent,
+ stat->tx.loss,
+ (stat->tx.loss? stat->tx.loss * 100.0 / (stat->tx.pkt + stat->tx.loss) : 0),
+ stat->tx.dup,
+ (stat->tx.dup? stat->tx.dup * 100.0 / (stat->tx.pkt + stat->tx.loss) : 0),
+ stat->tx.reorder,
+ (stat->tx.reorder? stat->tx.reorder * 100.0 / (stat->tx.pkt + stat->tx.loss) : 0),
+
+ indent, indent,
+ stat->tx.loss_period.min / 1000.0,
+ stat->tx.loss_period.mean / 1000.0,
+ stat->tx.loss_period.max / 1000.0,
+ stat->tx.loss_period.last / 1000.0,
+ pj_math_stat_get_stddev(&stat->tx.loss_period) / 1000.0,
+ indent,
+ stat->tx.jitter.min / 1000.0,
+ stat->tx.jitter.mean / 1000.0,
+ stat->tx.jitter.max / 1000.0,
+ stat->tx.jitter.last / 1000.0,
+ pj_math_stat_get_stddev(&stat->tx.jitter) / 1000.0
+ );
+
+ if (len < 1 || len > end-p) {
+ *p = '\0';
+ return (p-buf);
+ }
+ p += len;
+
+ len = pj_ansi_snprintf(p, end-p,
+ "%s RTT msec : %7.3f %7.3f %7.3f %7.3f %7.3f\n",
+ indent,
+ stat->rtt.min / 1000.0,
+ stat->rtt.mean / 1000.0,
+ stat->rtt.max / 1000.0,
+ stat->rtt.last / 1000.0,
+ pj_math_stat_get_stddev(&stat->rtt) / 1000.0
+ );
+ if (len < 1 || len > end-p) {
+ *p = '\0';
+ return (p-buf);
+ }
+ p += len;
+
+ return (p-buf);
+}
+
+
+/* Dump media session */
+static void dump_media_session(const char *indent,
+ char *buf, unsigned maxlen,
+ pjsua_call *call)
+{
+ unsigned i;
+ char *p = buf, *end = buf+maxlen;
+ int len;
+
+ for (i=0; i<call->med_cnt; ++i) {
+ pjsua_call_media *call_med = &call->media[i];
+ pjmedia_rtcp_stat stat;
+ pj_bool_t has_stat;
+ pjmedia_transport_info tp_info;
+ char rem_addr_buf[80];
+ char codec_info[32] = {'0'};
+ char rx_info[80] = {'\0'};
+ char tx_info[80] = {'\0'};
+ const char *rem_addr;
+ const char *dir_str;
+ const char *media_type_str;
+
+ switch (call_med->type) {
+ case PJMEDIA_TYPE_AUDIO:
+ media_type_str = "audio";
+ break;
+ case PJMEDIA_TYPE_VIDEO:
+ media_type_str = "video";
+ break;
+ case PJMEDIA_TYPE_APPLICATION:
+ media_type_str = "application";
+ break;
+ default:
+ media_type_str = "unknown";
+ break;
+ }
+
+ /* Check if the stream is deactivated */
+ if (call_med->tp == NULL ||
+ (!call_med->strm.a.stream && !call_med->strm.v.stream))
+ {
+ len = pj_ansi_snprintf(p, end-p,
+ "%s #%d %s deactivated\n",
+ indent, i, media_type_str);
+ if (len < 1 || len > end-p) {
+ *p = '\0';
+ return;
+ }
+
+ p += len;
+ continue;
+ }
+
+ pjmedia_transport_info_init(&tp_info);
+ pjmedia_transport_get_info(call_med->tp, &tp_info);
+
+ // rem_addr will contain actual address of RTP originator, instead of
+ // remote RTP address specified by stream which is fetched from the SDP.
+ // Please note that we are assuming only one stream per call.
+ //rem_addr = pj_sockaddr_print(&info.stream_info[i].rem_addr,
+ // rem_addr_buf, sizeof(rem_addr_buf), 3);
+ if (pj_sockaddr_has_addr(&tp_info.src_rtp_name)) {
+ rem_addr = pj_sockaddr_print(&tp_info.src_rtp_name, rem_addr_buf,
+ sizeof(rem_addr_buf), 3);
+ } else {
+ pj_ansi_snprintf(rem_addr_buf, sizeof(rem_addr_buf), "-");
+ rem_addr = rem_addr_buf;
+ }
+
+ if (call_med->dir == PJMEDIA_DIR_NONE) {
+ /* To handle when the stream that is currently being paused
+ * (http://trac.pjsip.org/repos/ticket/1079)
+ */
+ dir_str = "inactive";
+ } else if (call_med->dir == PJMEDIA_DIR_ENCODING)
+ dir_str = "sendonly";
+ else if (call_med->dir == PJMEDIA_DIR_DECODING)
+ dir_str = "recvonly";
+ else if (call_med->dir == PJMEDIA_DIR_ENCODING_DECODING)
+ dir_str = "sendrecv";
+ else
+ dir_str = "inactive";
+
+ if (call_med->type == PJMEDIA_TYPE_AUDIO) {
+ pjmedia_stream *stream = call_med->strm.a.stream;
+ pjmedia_stream_info info;
+
+ pjmedia_stream_get_stat(stream, &stat);
+ has_stat = PJ_TRUE;
+
+ pjmedia_stream_get_info(stream, &info);
+ pj_ansi_snprintf(codec_info, sizeof(codec_info), " %.*s @%dkHz",
+ (int)info.fmt.encoding_name.slen,
+ info.fmt.encoding_name.ptr,
+ info.fmt.clock_rate / 1000);
+ pj_ansi_snprintf(rx_info, sizeof(rx_info), "pt=%d,",
+ info.rx_pt);
+ pj_ansi_snprintf(tx_info, sizeof(tx_info), "pt=%d, ptime=%d,",
+ info.tx_pt,
+ info.param->setting.frm_per_pkt*
+ info.param->info.frm_ptime);
+
+#if defined(PJMEDIA_HAS_VIDEO) && (PJMEDIA_HAS_VIDEO != 0)
+ } else if (call_med->type == PJMEDIA_TYPE_VIDEO) {
+ pjmedia_vid_stream *stream = call_med->strm.v.stream;
+ pjmedia_vid_stream_info info;
+
+ pjmedia_vid_stream_get_stat(stream, &stat);
+ has_stat = PJ_TRUE;
+
+ pjmedia_vid_stream_get_info(stream, &info);
+ pj_ansi_snprintf(codec_info, sizeof(codec_info), " %.*s",
+ (int)info.codec_info.encoding_name.slen,
+ info.codec_info.encoding_name.ptr);
+ if (call_med->dir & PJMEDIA_DIR_DECODING) {
+ pjmedia_video_format_detail *vfd;
+ vfd = pjmedia_format_get_video_format_detail(
+ &info.codec_param->dec_fmt, PJ_TRUE);
+ pj_ansi_snprintf(rx_info, sizeof(rx_info),
+ "pt=%d, size=%dx%d, fps=%.2f,",
+ info.rx_pt,
+ vfd->size.w, vfd->size.h,
+ vfd->fps.num*1.0/vfd->fps.denum);
+ }
+ if (call_med->dir & PJMEDIA_DIR_ENCODING) {
+ pjmedia_video_format_detail *vfd;
+ vfd = pjmedia_format_get_video_format_detail(
+ &info.codec_param->enc_fmt, PJ_TRUE);
+ pj_ansi_snprintf(tx_info, sizeof(tx_info),
+ "pt=%d, size=%dx%d, fps=%.2f,",
+ info.tx_pt,
+ vfd->size.w, vfd->size.h,
+ vfd->fps.num*1.0/vfd->fps.denum);
+ }
+#endif /* PJMEDIA_HAS_VIDEO */
+
+ } else {
+ has_stat = PJ_FALSE;
+ }
+
+ len = pj_ansi_snprintf(p, end-p,
+ "%s #%d %s%s, %s, peer=%s\n",
+ indent,
+ call_med->idx,
+ media_type_str,
+ codec_info,
+ dir_str,
+ rem_addr);
+ if (len < 1 || len > end-p) {
+ *p = '\0';
+ return;
+ }
+ p += len;
+
+ /* Get and ICE SRTP status */
+ if (call_med->tp) {
+ pjmedia_transport_info tp_info;
+
+ pjmedia_transport_info_init(&tp_info);
+ pjmedia_transport_get_info(call_med->tp, &tp_info);
+ if (tp_info.specific_info_cnt > 0) {
+ unsigned j;
+ for (j = 0; j < tp_info.specific_info_cnt; ++j) {
+ if (tp_info.spc_info[j].type == PJMEDIA_TRANSPORT_TYPE_SRTP)
+ {
+ pjmedia_srtp_info *srtp_info =
+ (pjmedia_srtp_info*) tp_info.spc_info[j].buffer;
+
+ len = pj_ansi_snprintf(p, end-p,
+ " %s SRTP status: %s Crypto-suite: %s",
+ indent,
+ (srtp_info->active?"Active":"Not active"),
+ srtp_info->tx_policy.name.ptr);
+ if (len > 0 && len < end-p) {
+ p += len;
+ *p++ = '\n';
+ *p = '\0';
+ }
+ } else if (tp_info.spc_info[j].type==PJMEDIA_TRANSPORT_TYPE_ICE) {
+ const pjmedia_ice_transport_info *ii;
+ unsigned jj;
+
+ ii = (const pjmedia_ice_transport_info*)
+ tp_info.spc_info[j].buffer;
+
+ len = pj_ansi_snprintf(p, end-p,
+ " %s ICE role: %s, state: %s, comp_cnt: %u",
+ indent,
+ pj_ice_sess_role_name(ii->role),
+ pj_ice_strans_state_name(ii->sess_state),
+ ii->comp_cnt);
+ if (len > 0 && len < end-p) {
+ p += len;
+ *p++ = '\n';
+ *p = '\0';
+ }
+
+ for (jj=0; ii->sess_state==PJ_ICE_STRANS_STATE_RUNNING && jj<2; ++jj) {
+ const char *type1 = pj_ice_get_cand_type_name(ii->comp[jj].lcand_type);
+ const char *type2 = pj_ice_get_cand_type_name(ii->comp[jj].rcand_type);
+ char addr1[PJ_INET6_ADDRSTRLEN+10];
+ char addr2[PJ_INET6_ADDRSTRLEN+10];
+
+ if (pj_sockaddr_has_addr(&ii->comp[jj].lcand_addr))
+ pj_sockaddr_print(&ii->comp[jj].lcand_addr, addr1, sizeof(addr1), 3);
+ else
+ strcpy(addr1, "0.0.0.0:0");
+ if (pj_sockaddr_has_addr(&ii->comp[jj].rcand_addr))
+ pj_sockaddr_print(&ii->comp[jj].rcand_addr, addr2, sizeof(addr2), 3);
+ else
+ strcpy(addr2, "0.0.0.0:0");
+ len = pj_ansi_snprintf(p, end-p,
+ " %s [%d]: L:%s (%c) --> R:%s (%c)\n",
+ indent, jj,
+ addr1, type1[0],
+ addr2, type2[0]);
+ if (len > 0 && len < end-p) {
+ p += len;
+ *p = '\0';
+ }
+ }
+ }
+ }
+ }
+ }
+
+
+ if (has_stat) {
+ len = dump_media_stat(indent, p, end-p, &stat,
+ rx_info, tx_info);
+ p += len;
+ }
+
+#if defined(PJMEDIA_HAS_RTCP_XR) && (PJMEDIA_HAS_RTCP_XR != 0)
+# define SAMPLES_TO_USEC(usec, samples, clock_rate) \
+ do { \
+ if (samples <= 4294) \
+ usec = samples * 1000000 / clock_rate; \
+ else { \
+ usec = samples * 1000 / clock_rate; \
+ usec *= 1000; \
+ } \
+ } while(0)
+
+# define PRINT_VOIP_MTC_VAL(s, v) \
+ if (v == 127) \
+ sprintf(s, "(na)"); \
+ else \
+ sprintf(s, "%d", v)
+
+# define VALIDATE_PRINT_BUF() \
+ if (len < 1 || len > end-p) { *p = '\0'; return; } \
+ p += len; *p++ = '\n'; *p = '\0'
+
+
+ if (call_med->type == PJMEDIA_TYPE_AUDIO) {
+ pjmedia_stream_info info;
+ char last_update[64];
+ char loss[16], dup[16];
+ char jitter[80];
+ char toh[80];
+ char plc[16], jba[16], jbr[16];
+ char signal_lvl[16], noise_lvl[16], rerl[16];
+ char r_factor[16], ext_r_factor[16], mos_lq[16], mos_cq[16];
+ pjmedia_rtcp_xr_stat xr_stat;
+ unsigned clock_rate;
+ pj_time_val now;
+
+ if (pjmedia_stream_get_stat_xr(call_med->strm.a.stream,
+ &xr_stat) != PJ_SUCCESS)
+ {
+ continue;
+ }
+
+ if (pjmedia_stream_get_info(call_med->strm.a.stream, &info)
+ != PJ_SUCCESS)
+ {
+ continue;
+ }
+
+ clock_rate = info.fmt.clock_rate;
+ pj_gettimeofday(&now);
+
+ len = pj_ansi_snprintf(p, end-p, "\n%s Extended reports:", indent);
+ VALIDATE_PRINT_BUF();
+
+ /* Statistics Summary */
+ len = pj_ansi_snprintf(p, end-p, "%s Statistics Summary", indent);
+ VALIDATE_PRINT_BUF();
+
+ if (xr_stat.rx.stat_sum.l)
+ sprintf(loss, "%d", xr_stat.rx.stat_sum.lost);
+ else
+ sprintf(loss, "(na)");
+
+ if (xr_stat.rx.stat_sum.d)
+ sprintf(dup, "%d", xr_stat.rx.stat_sum.dup);
+ else
+ sprintf(dup, "(na)");
+
+ if (xr_stat.rx.stat_sum.j) {
+ unsigned jmin, jmax, jmean, jdev;
+
+ SAMPLES_TO_USEC(jmin, xr_stat.rx.stat_sum.jitter.min,
+ clock_rate);
+ SAMPLES_TO_USEC(jmax, xr_stat.rx.stat_sum.jitter.max,
+ clock_rate);
+ SAMPLES_TO_USEC(jmean, xr_stat.rx.stat_sum.jitter.mean,
+ clock_rate);
+ SAMPLES_TO_USEC(jdev,
+ pj_math_stat_get_stddev(&xr_stat.rx.stat_sum.jitter),
+ clock_rate);
+ sprintf(jitter, "%7.3f %7.3f %7.3f %7.3f",
+ jmin/1000.0, jmean/1000.0, jmax/1000.0, jdev/1000.0);
+ } else
+ sprintf(jitter, "(report not available)");
+
+ if (xr_stat.rx.stat_sum.t) {
+ sprintf(toh, "%11d %11d %11d %11d",
+ xr_stat.rx.stat_sum.toh.min,
+ xr_stat.rx.stat_sum.toh.mean,
+ xr_stat.rx.stat_sum.toh.max,
+ pj_math_stat_get_stddev(&xr_stat.rx.stat_sum.toh));
+ } else
+ sprintf(toh, "(report not available)");
+
+ if (xr_stat.rx.stat_sum.update.sec == 0)
+ strcpy(last_update, "never");
+ else {
+ pj_gettimeofday(&now);
+ PJ_TIME_VAL_SUB(now, xr_stat.rx.stat_sum.update);
+ sprintf(last_update, "%02ldh:%02ldm:%02ld.%03lds ago",
+ now.sec / 3600,
+ (now.sec % 3600) / 60,
+ now.sec % 60,
+ now.msec);
+ }
+
+ len = pj_ansi_snprintf(p, end-p,
+ "%s RX last update: %s\n"
+ "%s begin seq=%d, end seq=%d\n"
+ "%s pkt loss=%s, dup=%s\n"
+ "%s (msec) min avg max dev\n"
+ "%s jitter : %s\n"
+ "%s toh : %s",
+ indent, last_update,
+ indent,
+ xr_stat.rx.stat_sum.begin_seq, xr_stat.rx.stat_sum.end_seq,
+ indent, loss, dup,
+ indent,
+ indent, jitter,
+ indent, toh
+ );
+ VALIDATE_PRINT_BUF();
+
+ if (xr_stat.tx.stat_sum.l)
+ sprintf(loss, "%d", xr_stat.tx.stat_sum.lost);
+ else
+ sprintf(loss, "(na)");
+
+ if (xr_stat.tx.stat_sum.d)
+ sprintf(dup, "%d", xr_stat.tx.stat_sum.dup);
+ else
+ sprintf(dup, "(na)");
+
+ if (xr_stat.tx.stat_sum.j) {
+ unsigned jmin, jmax, jmean, jdev;
+
+ SAMPLES_TO_USEC(jmin, xr_stat.tx.stat_sum.jitter.min,
+ clock_rate);
+ SAMPLES_TO_USEC(jmax, xr_stat.tx.stat_sum.jitter.max,
+ clock_rate);
+ SAMPLES_TO_USEC(jmean, xr_stat.tx.stat_sum.jitter.mean,
+ clock_rate);
+ SAMPLES_TO_USEC(jdev,
+ pj_math_stat_get_stddev(&xr_stat.tx.stat_sum.jitter),
+ clock_rate);
+ sprintf(jitter, "%7.3f %7.3f %7.3f %7.3f",
+ jmin/1000.0, jmean/1000.0, jmax/1000.0, jdev/1000.0);
+ } else
+ sprintf(jitter, "(report not available)");
+
+ if (xr_stat.tx.stat_sum.t) {
+ sprintf(toh, "%11d %11d %11d %11d",
+ xr_stat.tx.stat_sum.toh.min,
+ xr_stat.tx.stat_sum.toh.mean,
+ xr_stat.tx.stat_sum.toh.max,
+ pj_math_stat_get_stddev(&xr_stat.rx.stat_sum.toh));
+ } else
+ sprintf(toh, "(report not available)");
+
+ if (xr_stat.tx.stat_sum.update.sec == 0)
+ strcpy(last_update, "never");
+ else {
+ pj_gettimeofday(&now);
+ PJ_TIME_VAL_SUB(now, xr_stat.tx.stat_sum.update);
+ sprintf(last_update, "%02ldh:%02ldm:%02ld.%03lds ago",
+ now.sec / 3600,
+ (now.sec % 3600) / 60,
+ now.sec % 60,
+ now.msec);
+ }
+
+ len = pj_ansi_snprintf(p, end-p,
+ "%s TX last update: %s\n"
+ "%s begin seq=%d, end seq=%d\n"
+ "%s pkt loss=%s, dup=%s\n"
+ "%s (msec) min avg max dev\n"
+ "%s jitter : %s\n"
+ "%s toh : %s",
+ indent, last_update,
+ indent,
+ xr_stat.tx.stat_sum.begin_seq, xr_stat.tx.stat_sum.end_seq,
+ indent, loss, dup,
+ indent,
+ indent, jitter,
+ indent, toh
+ );
+ VALIDATE_PRINT_BUF();
+
+
+ /* VoIP Metrics */
+ len = pj_ansi_snprintf(p, end-p, "%s VoIP Metrics", indent);
+ VALIDATE_PRINT_BUF();
+
+ PRINT_VOIP_MTC_VAL(signal_lvl, xr_stat.rx.voip_mtc.signal_lvl);
+ PRINT_VOIP_MTC_VAL(noise_lvl, xr_stat.rx.voip_mtc.noise_lvl);
+ PRINT_VOIP_MTC_VAL(rerl, xr_stat.rx.voip_mtc.rerl);
+ PRINT_VOIP_MTC_VAL(r_factor, xr_stat.rx.voip_mtc.r_factor);
+ PRINT_VOIP_MTC_VAL(ext_r_factor, xr_stat.rx.voip_mtc.ext_r_factor);
+ PRINT_VOIP_MTC_VAL(mos_lq, xr_stat.rx.voip_mtc.mos_lq);
+ PRINT_VOIP_MTC_VAL(mos_cq, xr_stat.rx.voip_mtc.mos_cq);
+
+ switch ((xr_stat.rx.voip_mtc.rx_config>>6) & 3) {
+ case PJMEDIA_RTCP_XR_PLC_DIS:
+ sprintf(plc, "DISABLED");
+ break;
+ case PJMEDIA_RTCP_XR_PLC_ENH:
+ sprintf(plc, "ENHANCED");
+ break;
+ case PJMEDIA_RTCP_XR_PLC_STD:
+ sprintf(plc, "STANDARD");
+ break;
+ case PJMEDIA_RTCP_XR_PLC_UNK:
+ default:
+ sprintf(plc, "UNKNOWN");
+ break;
+ }
+
+ switch ((xr_stat.rx.voip_mtc.rx_config>>4) & 3) {
+ case PJMEDIA_RTCP_XR_JB_FIXED:
+ sprintf(jba, "FIXED");
+ break;
+ case PJMEDIA_RTCP_XR_JB_ADAPTIVE:
+ sprintf(jba, "ADAPTIVE");
+ break;
+ default:
+ sprintf(jba, "UNKNOWN");
+ break;
+ }
+
+ sprintf(jbr, "%d", xr_stat.rx.voip_mtc.rx_config & 0x0F);
+
+ if (xr_stat.rx.voip_mtc.update.sec == 0)
+ strcpy(last_update, "never");
+ else {
+ pj_gettimeofday(&now);
+ PJ_TIME_VAL_SUB(now, xr_stat.rx.voip_mtc.update);
+ sprintf(last_update, "%02ldh:%02ldm:%02ld.%03lds ago",
+ now.sec / 3600,
+ (now.sec % 3600) / 60,
+ now.sec % 60,
+ now.msec);
+ }
+
+ len = pj_ansi_snprintf(p, end-p,
+ "%s RX last update: %s\n"
+ "%s packets : loss rate=%d (%.2f%%), discard rate=%d (%.2f%%)\n"
+ "%s burst : density=%d (%.2f%%), duration=%d%s\n"
+ "%s gap : density=%d (%.2f%%), duration=%d%s\n"
+ "%s delay : round trip=%d%s, end system=%d%s\n"
+ "%s level : signal=%s%s, noise=%s%s, RERL=%s%s\n"
+ "%s quality : R factor=%s, ext R factor=%s\n"
+ "%s MOS LQ=%s, MOS CQ=%s\n"
+ "%s config : PLC=%s, JB=%s, JB rate=%s, Gmin=%d\n"
+ "%s JB delay : cur=%d%s, max=%d%s, abs max=%d%s",
+ indent,
+ last_update,
+ /* packets */
+ indent,
+ xr_stat.rx.voip_mtc.loss_rate, xr_stat.rx.voip_mtc.loss_rate*100.0/256,
+ xr_stat.rx.voip_mtc.discard_rate, xr_stat.rx.voip_mtc.discard_rate*100.0/256,
+ /* burst */
+ indent,
+ xr_stat.rx.voip_mtc.burst_den, xr_stat.rx.voip_mtc.burst_den*100.0/256,
+ xr_stat.rx.voip_mtc.burst_dur, "ms",
+ /* gap */
+ indent,
+ xr_stat.rx.voip_mtc.gap_den, xr_stat.rx.voip_mtc.gap_den*100.0/256,
+ xr_stat.rx.voip_mtc.gap_dur, "ms",
+ /* delay */
+ indent,
+ xr_stat.rx.voip_mtc.rnd_trip_delay, "ms",
+ xr_stat.rx.voip_mtc.end_sys_delay, "ms",
+ /* level */
+ indent,
+ signal_lvl, "dB",
+ noise_lvl, "dB",
+ rerl, "",
+ /* quality */
+ indent,
+ r_factor, ext_r_factor,
+ indent,
+ mos_lq, mos_cq,
+ /* config */
+ indent,
+ plc, jba, jbr, xr_stat.rx.voip_mtc.gmin,
+ /* JB delay */
+ indent,
+ xr_stat.rx.voip_mtc.jb_nom, "ms",
+ xr_stat.rx.voip_mtc.jb_max, "ms",
+ xr_stat.rx.voip_mtc.jb_abs_max, "ms"
+ );
+ VALIDATE_PRINT_BUF();
+
+ PRINT_VOIP_MTC_VAL(signal_lvl, xr_stat.tx.voip_mtc.signal_lvl);
+ PRINT_VOIP_MTC_VAL(noise_lvl, xr_stat.tx.voip_mtc.noise_lvl);
+ PRINT_VOIP_MTC_VAL(rerl, xr_stat.tx.voip_mtc.rerl);
+ PRINT_VOIP_MTC_VAL(r_factor, xr_stat.tx.voip_mtc.r_factor);
+ PRINT_VOIP_MTC_VAL(ext_r_factor, xr_stat.tx.voip_mtc.ext_r_factor);
+ PRINT_VOIP_MTC_VAL(mos_lq, xr_stat.tx.voip_mtc.mos_lq);
+ PRINT_VOIP_MTC_VAL(mos_cq, xr_stat.tx.voip_mtc.mos_cq);
+
+ switch ((xr_stat.tx.voip_mtc.rx_config>>6) & 3) {
+ case PJMEDIA_RTCP_XR_PLC_DIS:
+ sprintf(plc, "DISABLED");
+ break;
+ case PJMEDIA_RTCP_XR_PLC_ENH:
+ sprintf(plc, "ENHANCED");
+ break;
+ case PJMEDIA_RTCP_XR_PLC_STD:
+ sprintf(plc, "STANDARD");
+ break;
+ case PJMEDIA_RTCP_XR_PLC_UNK:
+ default:
+ sprintf(plc, "unknown");
+ break;
+ }
+
+ switch ((xr_stat.tx.voip_mtc.rx_config>>4) & 3) {
+ case PJMEDIA_RTCP_XR_JB_FIXED:
+ sprintf(jba, "FIXED");
+ break;
+ case PJMEDIA_RTCP_XR_JB_ADAPTIVE:
+ sprintf(jba, "ADAPTIVE");
+ break;
+ default:
+ sprintf(jba, "unknown");
+ break;
+ }
+
+ sprintf(jbr, "%d", xr_stat.tx.voip_mtc.rx_config & 0x0F);
+
+ if (xr_stat.tx.voip_mtc.update.sec == 0)
+ strcpy(last_update, "never");
+ else {
+ pj_gettimeofday(&now);
+ PJ_TIME_VAL_SUB(now, xr_stat.tx.voip_mtc.update);
+ sprintf(last_update, "%02ldh:%02ldm:%02ld.%03lds ago",
+ now.sec / 3600,
+ (now.sec % 3600) / 60,
+ now.sec % 60,
+ now.msec);
+ }
+
+ len = pj_ansi_snprintf(p, end-p,
+ "%s TX last update: %s\n"
+ "%s packets : loss rate=%d (%.2f%%), discard rate=%d (%.2f%%)\n"
+ "%s burst : density=%d (%.2f%%), duration=%d%s\n"
+ "%s gap : density=%d (%.2f%%), duration=%d%s\n"
+ "%s delay : round trip=%d%s, end system=%d%s\n"
+ "%s level : signal=%s%s, noise=%s%s, RERL=%s%s\n"
+ "%s quality : R factor=%s, ext R factor=%s\n"
+ "%s MOS LQ=%s, MOS CQ=%s\n"
+ "%s config : PLC=%s, JB=%s, JB rate=%s, Gmin=%d\n"
+ "%s JB delay : cur=%d%s, max=%d%s, abs max=%d%s",
+ indent,
+ last_update,
+ /* pakcets */
+ indent,
+ xr_stat.tx.voip_mtc.loss_rate, xr_stat.tx.voip_mtc.loss_rate*100.0/256,
+ xr_stat.tx.voip_mtc.discard_rate, xr_stat.tx.voip_mtc.discard_rate*100.0/256,
+ /* burst */
+ indent,
+ xr_stat.tx.voip_mtc.burst_den, xr_stat.tx.voip_mtc.burst_den*100.0/256,
+ xr_stat.tx.voip_mtc.burst_dur, "ms",
+ /* gap */
+ indent,
+ xr_stat.tx.voip_mtc.gap_den, xr_stat.tx.voip_mtc.gap_den*100.0/256,
+ xr_stat.tx.voip_mtc.gap_dur, "ms",
+ /* delay */
+ indent,
+ xr_stat.tx.voip_mtc.rnd_trip_delay, "ms",
+ xr_stat.tx.voip_mtc.end_sys_delay, "ms",
+ /* level */
+ indent,
+ signal_lvl, "dB",
+ noise_lvl, "dB",
+ rerl, "",
+ /* quality */
+ indent,
+ r_factor, ext_r_factor,
+ indent,
+ mos_lq, mos_cq,
+ /* config */
+ indent,
+ plc, jba, jbr, xr_stat.tx.voip_mtc.gmin,
+ /* JB delay */
+ indent,
+ xr_stat.tx.voip_mtc.jb_nom, "ms",
+ xr_stat.tx.voip_mtc.jb_max, "ms",
+ xr_stat.tx.voip_mtc.jb_abs_max, "ms"
+ );
+ VALIDATE_PRINT_BUF();
+
+
+ /* RTT delay (by receiver side) */
+ len = pj_ansi_snprintf(p, end-p,
+ "%s RTT (from recv) min avg max last dev",
+ indent);
+ VALIDATE_PRINT_BUF();
+ len = pj_ansi_snprintf(p, end-p,
+ "%s RTT msec : %7.3f %7.3f %7.3f %7.3f %7.3f",
+ indent,
+ xr_stat.rtt.min / 1000.0,
+ xr_stat.rtt.mean / 1000.0,
+ xr_stat.rtt.max / 1000.0,
+ xr_stat.rtt.last / 1000.0,
+ pj_math_stat_get_stddev(&xr_stat.rtt) / 1000.0
+ );
+ VALIDATE_PRINT_BUF();
+ } /* if audio */;
+#endif
+
+ }
+}
+
+
+/* Print call info */
+void print_call(const char *title,
+ int call_id,
+ char *buf, pj_size_t size)
+{
+ int len;
+ pjsip_inv_session *inv = pjsua_var.calls[call_id].inv;
+ pjsip_dialog *dlg = inv->dlg;
+ char userinfo[128];
+
+ /* Dump invite sesion info. */
+
+ len = pjsip_hdr_print_on(dlg->remote.info, userinfo, sizeof(userinfo));
+ if (len < 0)
+ pj_ansi_strcpy(userinfo, "<--uri too long-->");
+ else
+ userinfo[len] = '\0';
+
+ len = pj_ansi_snprintf(buf, size, "%s[%s] %s",
+ title,
+ pjsip_inv_state_name(inv->state),
+ userinfo);
+ if (len < 1 || len >= (int)size) {
+ pj_ansi_strcpy(buf, "<--uri too long-->");
+ len = 18;
+ } else
+ buf[len] = '\0';
+}
+
+
+/*
+ * Dump call and media statistics to string.
+ */
+PJ_DEF(pj_status_t) pjsua_call_dump( pjsua_call_id call_id,
+ pj_bool_t with_media,
+ char *buffer,
+ unsigned maxlen,
+ const char *indent)
+{
+ pjsua_call *call;
+ pjsip_dialog *dlg;
+ pj_time_val duration, res_delay, con_delay;
+ char tmp[128];
+ char *p, *end;
+ pj_status_t status;
+ int len;
+
+ PJ_ASSERT_RETURN(call_id>=0 && call_id<(int)pjsua_var.ua_cfg.max_calls,
+ PJ_EINVAL);
+
+ status = acquire_call("pjsua_call_dump()", call_id, &call, &dlg);
+ if (status != PJ_SUCCESS)
+ return status;
+
+ *buffer = '\0';
+ p = buffer;
+ end = buffer + maxlen;
+ len = 0;
+
+ print_call(indent, call_id, tmp, sizeof(tmp));
+
+ len = pj_ansi_strlen(tmp);
+ pj_ansi_strcpy(buffer, tmp);
+
+ p += len;
+ *p++ = '\r';
+ *p++ = '\n';
+
+ /* Calculate call duration */
+ if (call->conn_time.sec != 0) {
+ pj_gettimeofday(&duration);
+ PJ_TIME_VAL_SUB(duration, call->conn_time);
+ con_delay = call->conn_time;
+ PJ_TIME_VAL_SUB(con_delay, call->start_time);
+ } else {
+ duration.sec = duration.msec = 0;
+ con_delay.sec = con_delay.msec = 0;
+ }
+
+ /* Calculate first response delay */
+ if (call->res_time.sec != 0) {
+ res_delay = call->res_time;
+ PJ_TIME_VAL_SUB(res_delay, call->start_time);
+ } else {
+ res_delay.sec = res_delay.msec = 0;
+ }
+
+ /* Print duration */
+ len = pj_ansi_snprintf(p, end-p,
+ "%s Call time: %02dh:%02dm:%02ds, "
+ "1st res in %d ms, conn in %dms",
+ indent,
+ (int)(duration.sec / 3600),
+ (int)((duration.sec % 3600)/60),
+ (int)(duration.sec % 60),
+ (int)PJ_TIME_VAL_MSEC(res_delay),
+ (int)PJ_TIME_VAL_MSEC(con_delay));
+
+ if (len > 0 && len < end-p) {
+ p += len;
+ *p++ = '\n';
+ *p = '\0';
+ }
+
+ /* Dump session statistics */
+ if (with_media)
+ dump_media_session(indent, p, end-p, call);
+
+ pjsip_dlg_dec_lock(dlg);
+
+ return PJ_SUCCESS;
+}
+
diff --git a/pjsip/src/pjsua-lib/pjsua_im.c b/pjsip/src/pjsua-lib/pjsua_im.c
new file mode 100644
index 0000000..fa11282
--- /dev/null
+++ b/pjsip/src/pjsua-lib/pjsua_im.c
@@ -0,0 +1,745 @@
+/* $Id: pjsua_im.c 4173 2012-06-20 10:39:05Z ming $ */
+/*
+ * Copyright (C) 2008-2011 Teluu Inc. (http://www.teluu.com)
+ * Copyright (C) 2003-2008 Benny Prijono <benny@prijono.org>
+ *
+ * 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
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ */
+#include <pjsua-lib/pjsua.h>
+#include <pjsua-lib/pjsua_internal.h>
+
+
+#define THIS_FILE "pjsua_im.h"
+
+
+/* Declare MESSAGE method */
+/* We put PJSIP_MESSAGE_METHOD as the enum here, so that when
+ * somebody add that method into pjsip_method_e in sip_msg.h we
+ * will get an error here.
+ */
+enum
+{
+ PJSIP_MESSAGE_METHOD = PJSIP_OTHER_METHOD
+};
+
+const pjsip_method pjsip_message_method =
+{
+ (pjsip_method_e) PJSIP_MESSAGE_METHOD,
+ { "MESSAGE", 7 }
+};
+
+
+/* Proto */
+static pj_bool_t im_on_rx_request(pjsip_rx_data *rdata);
+
+
+/* The module instance. */
+static pjsip_module mod_pjsua_im =
+{
+ NULL, NULL, /* prev, next. */
+ { "mod-pjsua-im", 12 }, /* Name. */
+ -1, /* Id */
+ PJSIP_MOD_PRIORITY_APPLICATION, /* Priority */
+ NULL, /* load() */
+ NULL, /* start() */
+ NULL, /* stop() */
+ NULL, /* unload() */
+ &im_on_rx_request, /* on_rx_request() */
+ NULL, /* on_rx_response() */
+ NULL, /* on_tx_request. */
+ NULL, /* on_tx_response() */
+ NULL, /* on_tsx_state() */
+
+};
+
+
+/* MIME constants. */
+static const pj_str_t STR_MIME_APP = { "application", 11 };
+static const pj_str_t STR_MIME_ISCOMPOSING = { "im-iscomposing+xml", 18 };
+static const pj_str_t STR_MIME_TEXT = { "text", 4 };
+static const pj_str_t STR_MIME_PLAIN = { "plain", 5 };
+
+
+/* Check if content type is acceptable */
+#if 0
+static pj_bool_t acceptable_message(const pjsip_media_type *mime)
+{
+ return (pj_stricmp(&mime->type, &STR_MIME_TEXT)==0 &&
+ pj_stricmp(&mime->subtype, &STR_MIME_PLAIN)==0)
+ ||
+ (pj_stricmp(&mime->type, &STR_MIME_APP)==0 &&
+ pj_stricmp(&mime->subtype, &STR_MIME_ISCOMPOSING)==0);
+}
+#endif
+
+/**
+ * Create Accept header for MESSAGE.
+ */
+pjsip_accept_hdr* pjsua_im_create_accept(pj_pool_t *pool)
+{
+ /* Create Accept header. */
+ pjsip_accept_hdr *accept;
+
+ accept = pjsip_accept_hdr_create(pool);
+ accept->values[0] = pj_str("text/plain");
+ accept->values[1] = pj_str("application/im-iscomposing+xml");
+ accept->count = 2;
+
+ return accept;
+}
+
+/**
+ * Private: check if we can accept the message.
+ */
+pj_bool_t pjsua_im_accept_pager(pjsip_rx_data *rdata,
+ pjsip_accept_hdr **p_accept_hdr)
+{
+ /* Some UA sends text/html, so this check will break */
+#if 0
+ pjsip_ctype_hdr *ctype;
+ pjsip_msg *msg;
+
+ msg = rdata->msg_info.msg;
+
+ /* Request MUST have message body, with Content-Type equal to
+ * "text/plain".
+ */
+ ctype = (pjsip_ctype_hdr*)
+ pjsip_msg_find_hdr(msg, PJSIP_H_CONTENT_TYPE, NULL);
+ if (msg->body == NULL || ctype == NULL ||
+ !acceptable_message(&ctype->media))
+ {
+ /* Create Accept header. */
+ if (p_accept_hdr)
+ *p_accept_hdr = pjsua_im_create_accept(rdata->tp_info.pool);
+
+ return PJ_FALSE;
+ }
+#elif 0
+ pjsip_msg *msg;
+
+ msg = rdata->msg_info.msg;
+ if (msg->body == NULL) {
+ /* Create Accept header. */
+ if (p_accept_hdr)
+ *p_accept_hdr = pjsua_im_create_accept(rdata->tp_info.pool);
+
+ return PJ_FALSE;
+ }
+#else
+ /* Ticket #693: allow incoming MESSAGE without message body */
+ PJ_UNUSED_ARG(rdata);
+ PJ_UNUSED_ARG(p_accept_hdr);
+#endif
+
+ return PJ_TRUE;
+}
+
+/**
+ * Private: process pager message.
+ * This may trigger pjsua_ui_on_pager() or pjsua_ui_on_typing().
+ */
+void pjsua_im_process_pager(int call_id, const pj_str_t *from,
+ const pj_str_t *to, pjsip_rx_data *rdata)
+{
+ pjsip_contact_hdr *contact_hdr;
+ pj_str_t contact;
+ pjsip_msg_body *body = rdata->msg_info.msg->body;
+
+#if 0
+ /* Ticket #693: allow incoming MESSAGE without message body */
+ /* Body MUST have been checked before */
+ pj_assert(body != NULL);
+#endif
+
+
+ /* Build remote contact */
+ contact_hdr = (pjsip_contact_hdr*)
+ pjsip_msg_find_hdr(rdata->msg_info.msg, PJSIP_H_CONTACT,
+ NULL);
+ if (contact_hdr && contact_hdr->uri) {
+ contact.ptr = (char*) pj_pool_alloc(rdata->tp_info.pool,
+ PJSIP_MAX_URL_SIZE);
+ contact.slen = pjsip_uri_print(PJSIP_URI_IN_CONTACT_HDR,
+ contact_hdr->uri, contact.ptr,
+ PJSIP_MAX_URL_SIZE);
+ } else {
+ contact.slen = 0;
+ }
+
+ if (body && pj_stricmp(&body->content_type.type, &STR_MIME_APP)==0 &&
+ pj_stricmp(&body->content_type.subtype, &STR_MIME_ISCOMPOSING)==0)
+ {
+ /* Expecting typing indication */
+ pj_status_t status;
+ pj_bool_t is_typing;
+
+ status = pjsip_iscomposing_parse(rdata->tp_info.pool, (char*)body->data,
+ body->len, &is_typing, NULL, NULL,
+ NULL );
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Invalid MESSAGE body", status);
+ return;
+ }
+
+ if (pjsua_var.ua_cfg.cb.on_typing) {
+ (*pjsua_var.ua_cfg.cb.on_typing)(call_id, from, to, &contact,
+ is_typing);
+ }
+
+ if (pjsua_var.ua_cfg.cb.on_typing2) {
+ pjsua_acc_id acc_id;
+
+ if (call_id == PJSUA_INVALID_ID) {
+ acc_id = pjsua_acc_find_for_incoming(rdata);
+ } else {
+ pjsua_call *call = &pjsua_var.calls[call_id];
+ acc_id = call->acc_id;
+ }
+
+
+ (*pjsua_var.ua_cfg.cb.on_typing2)(call_id, from, to, &contact,
+ is_typing, rdata, acc_id);
+ }
+
+ } else {
+ pj_str_t mime_type;
+ char buf[256];
+ pjsip_media_type *m;
+ pj_str_t text_body;
+
+ /* Save text body */
+ if (body) {
+ text_body.ptr = (char*)rdata->msg_info.msg->body->data;
+ text_body.slen = rdata->msg_info.msg->body->len;
+
+ /* Get mime type */
+ m = &rdata->msg_info.msg->body->content_type;
+ mime_type.ptr = buf;
+ mime_type.slen = pj_ansi_snprintf(buf, sizeof(buf),
+ "%.*s/%.*s",
+ (int)m->type.slen,
+ m->type.ptr,
+ (int)m->subtype.slen,
+ m->subtype.ptr);
+ if (mime_type.slen < 1)
+ mime_type.slen = 0;
+
+
+ } else {
+ text_body.ptr = mime_type.ptr = "";
+ text_body.slen = mime_type.slen = 0;
+ }
+
+ if (pjsua_var.ua_cfg.cb.on_pager) {
+ (*pjsua_var.ua_cfg.cb.on_pager)(call_id, from, to, &contact,
+ &mime_type, &text_body);
+ }
+
+ if (pjsua_var.ua_cfg.cb.on_pager2) {
+ pjsua_acc_id acc_id;
+
+ if (call_id == PJSUA_INVALID_ID) {
+ acc_id = pjsua_acc_find_for_incoming(rdata);
+ } else {
+ pjsua_call *call = &pjsua_var.calls[call_id];
+ acc_id = call->acc_id;
+ }
+
+ (*pjsua_var.ua_cfg.cb.on_pager2)(call_id, from, to, &contact,
+ &mime_type, &text_body, rdata,
+ acc_id);
+ }
+ }
+}
+
+
+/*
+ * Handler to receive incoming MESSAGE
+ */
+static pj_bool_t im_on_rx_request(pjsip_rx_data *rdata)
+{
+ pj_str_t from, to;
+ pjsip_accept_hdr *accept_hdr;
+ pjsip_msg *msg;
+ pj_status_t status;
+
+ msg = rdata->msg_info.msg;
+
+ /* Only want to handle MESSAGE requests. */
+ if (pjsip_method_cmp(&msg->line.req.method, &pjsip_message_method) != 0) {
+ return PJ_FALSE;
+ }
+
+
+ /* Should not have any transaction attached to rdata. */
+ PJ_ASSERT_RETURN(pjsip_rdata_get_tsx(rdata)==NULL, PJ_FALSE);
+
+ /* Should not have any dialog attached to rdata. */
+ PJ_ASSERT_RETURN(pjsip_rdata_get_dlg(rdata)==NULL, PJ_FALSE);
+
+ /* Check if we can accept the message. */
+ if (!pjsua_im_accept_pager(rdata, &accept_hdr)) {
+ pjsip_hdr hdr_list;
+
+ pj_list_init(&hdr_list);
+ pj_list_push_back(&hdr_list, accept_hdr);
+
+ pjsip_endpt_respond_stateless(pjsua_var.endpt, rdata,
+ PJSIP_SC_NOT_ACCEPTABLE_HERE, NULL,
+ &hdr_list, NULL);
+ return PJ_TRUE;
+ }
+
+ /* Respond with 200 first, so that remote doesn't retransmit in case
+ * the UI takes too long to process the message.
+ */
+ status = pjsip_endpt_respond( pjsua_var.endpt, NULL, rdata, 200, NULL,
+ NULL, NULL, NULL);
+
+ /* For the source URI, we use Contact header if present, since
+ * Contact header contains the port number information. If this is
+ * not available, then use From header.
+ */
+ from.ptr = (char*)pj_pool_alloc(rdata->tp_info.pool, PJSIP_MAX_URL_SIZE);
+ from.slen = pjsip_uri_print(PJSIP_URI_IN_FROMTO_HDR,
+ rdata->msg_info.from->uri,
+ from.ptr, PJSIP_MAX_URL_SIZE);
+
+ if (from.slen < 1)
+ from = pj_str("<--URI is too long-->");
+
+ /* Build the To text. */
+ to.ptr = (char*) pj_pool_alloc(rdata->tp_info.pool, PJSIP_MAX_URL_SIZE);
+ to.slen = pjsip_uri_print( PJSIP_URI_IN_FROMTO_HDR,
+ rdata->msg_info.to->uri,
+ to.ptr, PJSIP_MAX_URL_SIZE);
+ if (to.slen < 1)
+ to = pj_str("<--URI is too long-->");
+
+ /* Process pager. */
+ pjsua_im_process_pager(-1, &from, &to, rdata);
+
+ /* Done. */
+ return PJ_TRUE;
+}
+
+
+/* Outgoing IM callback. */
+static void im_callback(void *token, pjsip_event *e)
+{
+ pjsua_im_data *im_data = (pjsua_im_data*) token;
+
+ if (e->type == PJSIP_EVENT_TSX_STATE) {
+
+ pjsip_transaction *tsx = e->body.tsx_state.tsx;
+
+ /* Ignore provisional response, if any */
+ if (tsx->status_code < 200)
+ return;
+
+
+ /* Handle authentication challenges */
+ if (e->body.tsx_state.type == PJSIP_EVENT_RX_MSG &&
+ (tsx->status_code == 401 || tsx->status_code == 407))
+ {
+ pjsip_rx_data *rdata = e->body.tsx_state.src.rdata;
+ pjsip_tx_data *tdata;
+ pjsip_auth_clt_sess auth;
+ pj_status_t status;
+
+ PJ_LOG(4,(THIS_FILE, "Resending IM with authentication"));
+
+ /* Create temporary authentication session */
+ pjsip_auth_clt_init(&auth,pjsua_var.endpt,rdata->tp_info.pool, 0);
+
+ pjsip_auth_clt_set_credentials(&auth,
+ pjsua_var.acc[im_data->acc_id].cred_cnt,
+ pjsua_var.acc[im_data->acc_id].cred);
+
+ pjsip_auth_clt_set_prefs(&auth,
+ &pjsua_var.acc[im_data->acc_id].cfg.auth_pref);
+
+ status = pjsip_auth_clt_reinit_req(&auth, rdata, tsx->last_tx,
+ &tdata);
+ if (status == PJ_SUCCESS) {
+ pjsua_im_data *im_data2;
+
+ /* Must duplicate im_data */
+ im_data2 = pjsua_im_data_dup(tdata->pool, im_data);
+
+ /* Increment CSeq */
+ PJSIP_MSG_CSEQ_HDR(tdata->msg)->cseq++;
+
+ /* Re-send request */
+ status = pjsip_endpt_send_request( pjsua_var.endpt, tdata, -1,
+ im_data2, &im_callback);
+ if (status == PJ_SUCCESS) {
+ /* Done */
+ return;
+ }
+ }
+ }
+
+ if (tsx->status_code/100 == 2) {
+ PJ_LOG(4,(THIS_FILE,
+ "Message \'%s\' delivered successfully",
+ im_data->body.ptr));
+ } else {
+ PJ_LOG(3,(THIS_FILE,
+ "Failed to deliver message \'%s\': %d/%.*s",
+ im_data->body.ptr,
+ tsx->status_code,
+ (int)tsx->status_text.slen,
+ tsx->status_text.ptr));
+ }
+
+ if (pjsua_var.ua_cfg.cb.on_pager_status) {
+ pjsua_var.ua_cfg.cb.on_pager_status(im_data->call_id,
+ &im_data->to,
+ &im_data->body,
+ im_data->user_data,
+ (pjsip_status_code)
+ tsx->status_code,
+ &tsx->status_text);
+ }
+
+ if (pjsua_var.ua_cfg.cb.on_pager_status2) {
+ pjsip_rx_data *rdata;
+
+ if (e->body.tsx_state.type == PJSIP_EVENT_RX_MSG)
+ rdata = e->body.tsx_state.src.rdata;
+ else
+ rdata = NULL;
+
+ pjsua_var.ua_cfg.cb.on_pager_status2(im_data->call_id,
+ &im_data->to,
+ &im_data->body,
+ im_data->user_data,
+ (pjsip_status_code)
+ tsx->status_code,
+ &tsx->status_text,
+ tsx->last_tx,
+ rdata, im_data->acc_id);
+ }
+ }
+}
+
+
+/* Outgoing typing indication callback.
+ * (used to reauthenticate request)
+ */
+static void typing_callback(void *token, pjsip_event *e)
+{
+ pjsua_im_data *im_data = (pjsua_im_data*) token;
+
+ if (e->type == PJSIP_EVENT_TSX_STATE) {
+
+ pjsip_transaction *tsx = e->body.tsx_state.tsx;
+
+ /* Ignore provisional response, if any */
+ if (tsx->status_code < 200)
+ return;
+
+ /* Handle authentication challenges */
+ if (e->body.tsx_state.type == PJSIP_EVENT_RX_MSG &&
+ (tsx->status_code == 401 || tsx->status_code == 407))
+ {
+ pjsip_rx_data *rdata = e->body.tsx_state.src.rdata;
+ pjsip_tx_data *tdata;
+ pjsip_auth_clt_sess auth;
+ pj_status_t status;
+
+ PJ_LOG(4,(THIS_FILE, "Resending IM with authentication"));
+
+ /* Create temporary authentication session */
+ pjsip_auth_clt_init(&auth,pjsua_var.endpt,rdata->tp_info.pool, 0);
+
+ pjsip_auth_clt_set_credentials(&auth,
+ pjsua_var.acc[im_data->acc_id].cred_cnt,
+ pjsua_var.acc[im_data->acc_id].cred);
+
+ pjsip_auth_clt_set_prefs(&auth,
+ &pjsua_var.acc[im_data->acc_id].cfg.auth_pref);
+
+ status = pjsip_auth_clt_reinit_req(&auth, rdata, tsx->last_tx,
+ &tdata);
+ if (status == PJ_SUCCESS) {
+ pjsua_im_data *im_data2;
+
+ /* Must duplicate im_data */
+ im_data2 = pjsua_im_data_dup(tdata->pool, im_data);
+
+ /* Increment CSeq */
+ PJSIP_MSG_CSEQ_HDR(tdata->msg)->cseq++;
+
+ /* Re-send request */
+ status = pjsip_endpt_send_request( pjsua_var.endpt, tdata, -1,
+ im_data2, &typing_callback);
+ if (status == PJ_SUCCESS) {
+ /* Done */
+ return;
+ }
+ }
+ }
+
+ }
+}
+
+
+/*
+ * Send instant messaging outside dialog, using the specified account for
+ * route set and authentication.
+ */
+PJ_DEF(pj_status_t) pjsua_im_send( pjsua_acc_id acc_id,
+ const pj_str_t *to,
+ const pj_str_t *mime_type,
+ const pj_str_t *content,
+ const pjsua_msg_data *msg_data,
+ void *user_data)
+{
+ pjsip_tx_data *tdata;
+ const pj_str_t mime_text_plain = pj_str("text/plain");
+ const pj_str_t STR_CONTACT = { "Contact", 7 };
+ pjsip_media_type media_type;
+ pjsua_im_data *im_data;
+ pjsua_acc *acc;
+ pj_str_t contact;
+ pj_status_t status;
+
+ /* To and message body must be specified. */
+ PJ_ASSERT_RETURN(to && content, PJ_EINVAL);
+
+ acc = &pjsua_var.acc[acc_id];
+
+ /* Create request. */
+ status = pjsip_endpt_create_request(pjsua_var.endpt,
+ &pjsip_message_method, to,
+ &acc->cfg.id,
+ to, NULL, NULL, -1, NULL, &tdata);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Unable to create request", status);
+ return status;
+ }
+
+ /* If account is locked to specific transport, then set transport to
+ * the request.
+ */
+ if (acc->cfg.transport_id != PJSUA_INVALID_ID) {
+ pjsip_tpselector tp_sel;
+
+ pjsua_init_tpselector(acc->cfg.transport_id, &tp_sel);
+ pjsip_tx_data_set_transport(tdata, &tp_sel);
+ }
+
+ /* Add accept header. */
+ pjsip_msg_add_hdr( tdata->msg,
+ (pjsip_hdr*)pjsua_im_create_accept(tdata->pool));
+
+ /* Create suitable Contact header unless a Contact header has been
+ * set in the account.
+ */
+ if (acc->contact.slen) {
+ contact = acc->contact;
+ } else {
+ status = pjsua_acc_create_uac_contact(tdata->pool, &contact, acc_id, to);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Unable to generate Contact header", status);
+ pjsip_tx_data_dec_ref(tdata);
+ return status;
+ }
+ }
+
+ pjsip_msg_add_hdr( tdata->msg, (pjsip_hdr*)
+ pjsip_generic_string_hdr_create(tdata->pool,
+ &STR_CONTACT, &contact));
+
+ /* Create IM data to keep message details and give it back to
+ * application on the callback
+ */
+ im_data = PJ_POOL_ZALLOC_T(tdata->pool, pjsua_im_data);
+ im_data->acc_id = acc_id;
+ im_data->call_id = PJSUA_INVALID_ID;
+ pj_strdup_with_null(tdata->pool, &im_data->to, to);
+ pj_strdup_with_null(tdata->pool, &im_data->body, content);
+ im_data->user_data = user_data;
+
+
+ /* Set default media type if none is specified */
+ if (mime_type == NULL) {
+ mime_type = &mime_text_plain;
+ }
+
+ /* Parse MIME type */
+ pjsua_parse_media_type(tdata->pool, mime_type, &media_type);
+
+ /* Add message body */
+ tdata->msg->body = pjsip_msg_body_create( tdata->pool, &media_type.type,
+ &media_type.subtype,
+ &im_data->body);
+ if (tdata->msg->body == NULL) {
+ pjsua_perror(THIS_FILE, "Unable to create msg body", PJ_ENOMEM);
+ pjsip_tx_data_dec_ref(tdata);
+ return PJ_ENOMEM;
+ }
+
+ /* Add additional headers etc. */
+ pjsua_process_msg_data(tdata, msg_data);
+
+ /* Add route set */
+ pjsua_set_msg_route_set(tdata, &acc->route_set);
+
+ /* If via_addr is set, use this address for the Via header. */
+ if (acc->cfg.allow_via_rewrite && acc->via_addr.host.slen > 0) {
+ tdata->via_addr = acc->via_addr;
+ tdata->via_tp = acc->via_tp;
+ }
+
+ /* Send request (statefully) */
+ status = pjsip_endpt_send_request( pjsua_var.endpt, tdata, -1,
+ im_data, &im_callback);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Unable to send request", status);
+ return status;
+ }
+
+ return PJ_SUCCESS;
+}
+
+
+/*
+ * Send typing indication outside dialog.
+ */
+PJ_DEF(pj_status_t) pjsua_im_typing( pjsua_acc_id acc_id,
+ const pj_str_t *to,
+ pj_bool_t is_typing,
+ const pjsua_msg_data *msg_data)
+{
+ const pj_str_t STR_CONTACT = { "Contact", 7 };
+ pjsua_im_data *im_data;
+ pjsip_tx_data *tdata;
+ pjsua_acc *acc;
+ pj_str_t contact;
+ pj_status_t status;
+
+ acc = &pjsua_var.acc[acc_id];
+
+ /* Create request. */
+ status = pjsip_endpt_create_request( pjsua_var.endpt, &pjsip_message_method,
+ to, &acc->cfg.id,
+ to, NULL, NULL, -1, NULL, &tdata);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Unable to create request", status);
+ return status;
+ }
+
+
+ /* If account is locked to specific transport, then set transport to
+ * the request.
+ */
+ if (acc->cfg.transport_id != PJSUA_INVALID_ID) {
+ pjsip_tpselector tp_sel;
+
+ pjsua_init_tpselector(acc->cfg.transport_id, &tp_sel);
+ pjsip_tx_data_set_transport(tdata, &tp_sel);
+ }
+
+ /* Add accept header. */
+ pjsip_msg_add_hdr( tdata->msg,
+ (pjsip_hdr*)pjsua_im_create_accept(tdata->pool));
+
+
+ /* Create suitable Contact header unless a Contact header has been
+ * set in the account.
+ */
+ if (acc->contact.slen) {
+ contact = acc->contact;
+ } else {
+ status = pjsua_acc_create_uac_contact(tdata->pool, &contact, acc_id, to);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Unable to generate Contact header", status);
+ pjsip_tx_data_dec_ref(tdata);
+ return status;
+ }
+ }
+
+ pjsip_msg_add_hdr( tdata->msg, (pjsip_hdr*)
+ pjsip_generic_string_hdr_create(tdata->pool,
+ &STR_CONTACT, &contact));
+
+
+ /* Create "application/im-iscomposing+xml" msg body. */
+ tdata->msg->body = pjsip_iscomposing_create_body( tdata->pool, is_typing,
+ NULL, NULL, -1);
+
+ /* Add additional headers etc. */
+ pjsua_process_msg_data(tdata, msg_data);
+
+ /* Add route set */
+ pjsua_set_msg_route_set(tdata, &acc->route_set);
+
+ /* If via_addr is set, use this address for the Via header. */
+ if (acc->cfg.allow_via_rewrite && acc->via_addr.host.slen > 0) {
+ tdata->via_addr = acc->via_addr;
+ tdata->via_tp = acc->via_tp;
+ }
+
+ /* Create data to reauthenticate */
+ im_data = PJ_POOL_ZALLOC_T(tdata->pool, pjsua_im_data);
+ im_data->acc_id = acc_id;
+
+ /* Send request (statefully) */
+ status = pjsip_endpt_send_request( pjsua_var.endpt, tdata, -1,
+ im_data, &typing_callback);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Unable to send request", status);
+ return status;
+ }
+
+ return PJ_SUCCESS;
+}
+
+
+/*
+ * Init pjsua IM module.
+ */
+pj_status_t pjsua_im_init(void)
+{
+ const pj_str_t msg_tag = { "MESSAGE", 7 };
+ const pj_str_t STR_MIME_TEXT_PLAIN = { "text/plain", 10 };
+ const pj_str_t STR_MIME_APP_ISCOMPOSING =
+ { "application/im-iscomposing+xml", 30 };
+ pj_status_t status;
+
+ /* Register module */
+ status = pjsip_endpt_register_module(pjsua_var.endpt, &mod_pjsua_im);
+ if (status != PJ_SUCCESS)
+ return status;
+
+ /* Register support for MESSAGE method. */
+ pjsip_endpt_add_capability( pjsua_var.endpt, &mod_pjsua_im, PJSIP_H_ALLOW,
+ NULL, 1, &msg_tag);
+
+ /* Register support for "application/im-iscomposing+xml" content */
+ pjsip_endpt_add_capability( pjsua_var.endpt, &mod_pjsua_im, PJSIP_H_ACCEPT,
+ NULL, 1, &STR_MIME_APP_ISCOMPOSING);
+
+ /* Register support for "text/plain" content */
+ pjsip_endpt_add_capability( pjsua_var.endpt, &mod_pjsua_im, PJSIP_H_ACCEPT,
+ NULL, 1, &STR_MIME_TEXT_PLAIN);
+
+ return PJ_SUCCESS;
+}
+
diff --git a/pjsip/src/pjsua-lib/pjsua_media.c b/pjsip/src/pjsua-lib/pjsua_media.c
new file mode 100644
index 0000000..8bcb0da
--- /dev/null
+++ b/pjsip/src/pjsua-lib/pjsua_media.c
@@ -0,0 +1,2695 @@
+/* $Id: pjsua_media.c 4182 2012-06-27 07:12:23Z ming $ */
+/*
+ * Copyright (C) 2008-2011 Teluu Inc. (http://www.teluu.com)
+ * Copyright (C) 2003-2008 Benny Prijono <benny@prijono.org>
+ *
+ * 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
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ */
+#include <pjsua-lib/pjsua.h>
+#include <pjsua-lib/pjsua_internal.h>
+
+
+#define THIS_FILE "pjsua_media.c"
+
+#define DEFAULT_RTP_PORT 4000
+
+#ifndef PJSUA_REQUIRE_CONSECUTIVE_RTCP_PORT
+# define PJSUA_REQUIRE_CONSECUTIVE_RTCP_PORT 0
+#endif
+
+/* Next RTP port to be used */
+static pj_uint16_t next_rtp_port;
+
+static void pjsua_media_config_dup(pj_pool_t *pool,
+ pjsua_media_config *dst,
+ const pjsua_media_config *src)
+{
+ pj_memcpy(dst, src, sizeof(*src));
+ pj_strdup(pool, &dst->turn_server, &src->turn_server);
+ pj_stun_auth_cred_dup(pool, &dst->turn_auth_cred, &src->turn_auth_cred);
+}
+
+
+/**
+ * Init media subsystems.
+ */
+pj_status_t pjsua_media_subsys_init(const pjsua_media_config *cfg)
+{
+ pj_status_t status;
+
+ pj_log_push_indent();
+
+ /* Specify which audio device settings are save-able */
+ pjsua_var.aud_svmask = 0xFFFFFFFF;
+ /* These are not-settable */
+ pjsua_var.aud_svmask &= ~(PJMEDIA_AUD_DEV_CAP_EXT_FORMAT |
+ PJMEDIA_AUD_DEV_CAP_INPUT_SIGNAL_METER |
+ PJMEDIA_AUD_DEV_CAP_OUTPUT_SIGNAL_METER);
+ /* EC settings use different API */
+ pjsua_var.aud_svmask &= ~(PJMEDIA_AUD_DEV_CAP_EC |
+ PJMEDIA_AUD_DEV_CAP_EC_TAIL);
+
+ /* Copy configuration */
+ pjsua_media_config_dup(pjsua_var.pool, &pjsua_var.media_cfg, cfg);
+
+ /* Normalize configuration */
+ if (pjsua_var.media_cfg.snd_clock_rate == 0) {
+ pjsua_var.media_cfg.snd_clock_rate = pjsua_var.media_cfg.clock_rate;
+ }
+
+ if (pjsua_var.media_cfg.has_ioqueue &&
+ pjsua_var.media_cfg.thread_cnt == 0)
+ {
+ pjsua_var.media_cfg.thread_cnt = 1;
+ }
+
+ if (pjsua_var.media_cfg.max_media_ports < pjsua_var.ua_cfg.max_calls) {
+ pjsua_var.media_cfg.max_media_ports = pjsua_var.ua_cfg.max_calls + 2;
+ }
+
+ /* Create media endpoint. */
+ status = pjmedia_endpt_create(&pjsua_var.cp.factory,
+ pjsua_var.media_cfg.has_ioqueue? NULL :
+ pjsip_endpt_get_ioqueue(pjsua_var.endpt),
+ pjsua_var.media_cfg.thread_cnt,
+ &pjsua_var.med_endpt);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE,
+ "Media stack initialization has returned error",
+ status);
+ goto on_error;
+ }
+
+ status = pjsua_aud_subsys_init();
+ if (status != PJ_SUCCESS)
+ goto on_error;
+
+#if defined(PJMEDIA_HAS_SRTP) && (PJMEDIA_HAS_SRTP != 0)
+ /* Initialize SRTP library (ticket #788). */
+ status = pjmedia_srtp_init_lib(pjsua_var.med_endpt);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Error initializing SRTP library",
+ status);
+ goto on_error;
+ }
+#endif
+
+ /* Video */
+#if PJMEDIA_HAS_VIDEO
+ status = pjsua_vid_subsys_init();
+ if (status != PJ_SUCCESS)
+ goto on_error;
+#endif
+
+ pj_log_pop_indent();
+ return PJ_SUCCESS;
+
+on_error:
+ pj_log_pop_indent();
+ return status;
+}
+
+/*
+ * Start pjsua media subsystem.
+ */
+pj_status_t pjsua_media_subsys_start(void)
+{
+ pj_status_t status;
+
+ pj_log_push_indent();
+
+#if DISABLED_FOR_TICKET_1185
+ /* Create media for calls, if none is specified */
+ if (pjsua_var.calls[0].media[0].tp == NULL) {
+ pjsua_transport_config transport_cfg;
+
+ /* Create default transport config */
+ pjsua_transport_config_default(&transport_cfg);
+ transport_cfg.port = DEFAULT_RTP_PORT;
+
+ status = pjsua_media_transports_create(&transport_cfg);
+ if (status != PJ_SUCCESS) {
+ pj_log_pop_indent();
+ return status;
+ }
+ }
+#endif
+
+ /* Audio */
+ status = pjsua_aud_subsys_start();
+ if (status != PJ_SUCCESS) {
+ pj_log_pop_indent();
+ return status;
+ }
+
+ /* Video */
+#if PJMEDIA_HAS_VIDEO
+ status = pjsua_vid_subsys_start();
+ if (status != PJ_SUCCESS) {
+ pjsua_aud_subsys_destroy();
+ pj_log_pop_indent();
+ return status;
+ }
+#endif
+
+ /* Perform NAT detection */
+ status = pjsua_detect_nat_type();
+ if (status != PJ_SUCCESS) {
+ PJ_PERROR(1,(THIS_FILE, status, "NAT type detection failed"));
+ }
+
+ pj_log_pop_indent();
+ return PJ_SUCCESS;
+}
+
+
+/*
+ * Destroy pjsua media subsystem.
+ */
+pj_status_t pjsua_media_subsys_destroy(unsigned flags)
+{
+ unsigned i;
+
+ PJ_LOG(4,(THIS_FILE, "Shutting down media.."));
+ pj_log_push_indent();
+
+ if (pjsua_var.med_endpt) {
+ pjsua_aud_subsys_destroy();
+ }
+
+ /* Close media transports */
+ for (i=0; i<pjsua_var.ua_cfg.max_calls; ++i) {
+ /* TODO: check if we're not allowed to send to network in the
+ * "flags", and if so do not do TURN allocation...
+ */
+ PJ_UNUSED_ARG(flags);
+ pjsua_media_channel_deinit(i);
+ }
+
+ /* Destroy media endpoint. */
+ if (pjsua_var.med_endpt) {
+
+# if PJMEDIA_HAS_VIDEO
+ pjsua_vid_subsys_destroy();
+# endif
+
+ pjmedia_endpt_destroy(pjsua_var.med_endpt);
+ pjsua_var.med_endpt = NULL;
+
+ /* Deinitialize sound subsystem */
+ // Not necessary, as pjmedia_snd_deinit() should have been called
+ // in pjmedia_endpt_destroy().
+ //pjmedia_snd_deinit();
+ }
+
+ /* Reset RTP port */
+ next_rtp_port = 0;
+
+ pj_log_pop_indent();
+
+ return PJ_SUCCESS;
+}
+
+/*
+ * Create RTP and RTCP socket pair, and possibly resolve their public
+ * address via STUN.
+ */
+static pj_status_t create_rtp_rtcp_sock(const pjsua_transport_config *cfg,
+ pjmedia_sock_info *skinfo)
+{
+ enum {
+ RTP_RETRY = 100
+ };
+ int i;
+ pj_sockaddr_in bound_addr;
+ pj_sockaddr_in mapped_addr[2];
+ pj_status_t status = PJ_SUCCESS;
+ char addr_buf[PJ_INET6_ADDRSTRLEN+2];
+ pj_sock_t sock[2];
+
+ /* Make sure STUN server resolution has completed */
+ status = resolve_stun_server(PJ_TRUE);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Error resolving STUN server", status);
+ return status;
+ }
+
+ if (next_rtp_port == 0)
+ next_rtp_port = (pj_uint16_t)cfg->port;
+
+ if (next_rtp_port == 0)
+ next_rtp_port = (pj_uint16_t)40000;
+
+ for (i=0; i<2; ++i)
+ sock[i] = PJ_INVALID_SOCKET;
+
+ bound_addr.sin_addr.s_addr = PJ_INADDR_ANY;
+ if (cfg->bound_addr.slen) {
+ status = pj_sockaddr_in_set_str_addr(&bound_addr, &cfg->bound_addr);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Unable to resolve transport bind address",
+ status);
+ return status;
+ }
+ }
+
+ /* Loop retry to bind RTP and RTCP sockets. */
+ for (i=0; i<RTP_RETRY; ++i, next_rtp_port += 2) {
+
+ /* Create RTP socket. */
+ status = pj_sock_socket(pj_AF_INET(), pj_SOCK_DGRAM(), 0, &sock[0]);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "socket() error", status);
+ return status;
+ }
+
+ /* Apply QoS to RTP socket, if specified */
+ status = pj_sock_apply_qos2(sock[0], cfg->qos_type,
+ &cfg->qos_params,
+ 2, THIS_FILE, "RTP socket");
+
+ /* Bind RTP socket */
+ status=pj_sock_bind_in(sock[0], pj_ntohl(bound_addr.sin_addr.s_addr),
+ next_rtp_port);
+ if (status != PJ_SUCCESS) {
+ pj_sock_close(sock[0]);
+ sock[0] = PJ_INVALID_SOCKET;
+ continue;
+ }
+
+ /* Create RTCP socket. */
+ status = pj_sock_socket(pj_AF_INET(), pj_SOCK_DGRAM(), 0, &sock[1]);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "socket() error", status);
+ pj_sock_close(sock[0]);
+ return status;
+ }
+
+ /* Apply QoS to RTCP socket, if specified */
+ status = pj_sock_apply_qos2(sock[1], cfg->qos_type,
+ &cfg->qos_params,
+ 2, THIS_FILE, "RTCP socket");
+
+ /* Bind RTCP socket */
+ status=pj_sock_bind_in(sock[1], pj_ntohl(bound_addr.sin_addr.s_addr),
+ (pj_uint16_t)(next_rtp_port+1));
+ if (status != PJ_SUCCESS) {
+ pj_sock_close(sock[0]);
+ sock[0] = PJ_INVALID_SOCKET;
+
+ pj_sock_close(sock[1]);
+ sock[1] = PJ_INVALID_SOCKET;
+ continue;
+ }
+
+ /*
+ * If we're configured to use STUN, then find out the mapped address,
+ * and make sure that the mapped RTCP port is adjacent with the RTP.
+ */
+ if (pjsua_var.stun_srv.addr.sa_family != 0) {
+ char ip_addr[32];
+ pj_str_t stun_srv;
+
+ pj_ansi_strcpy(ip_addr,
+ pj_inet_ntoa(pjsua_var.stun_srv.ipv4.sin_addr));
+ stun_srv = pj_str(ip_addr);
+
+ status=pjstun_get_mapped_addr(&pjsua_var.cp.factory, 2, sock,
+ &stun_srv, pj_ntohs(pjsua_var.stun_srv.ipv4.sin_port),
+ &stun_srv, pj_ntohs(pjsua_var.stun_srv.ipv4.sin_port),
+ mapped_addr);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "STUN resolve error", status);
+ goto on_error;
+ }
+
+#if PJSUA_REQUIRE_CONSECUTIVE_RTCP_PORT
+ if (pj_ntohs(mapped_addr[1].sin_port) ==
+ pj_ntohs(mapped_addr[0].sin_port)+1)
+ {
+ /* Success! */
+ break;
+ }
+
+ pj_sock_close(sock[0]);
+ sock[0] = PJ_INVALID_SOCKET;
+
+ pj_sock_close(sock[1]);
+ sock[1] = PJ_INVALID_SOCKET;
+#else
+ if (pj_ntohs(mapped_addr[1].sin_port) !=
+ pj_ntohs(mapped_addr[0].sin_port)+1)
+ {
+ PJ_LOG(4,(THIS_FILE,
+ "Note: STUN mapped RTCP port %d is not adjacent"
+ " to RTP port %d",
+ pj_ntohs(mapped_addr[1].sin_port),
+ pj_ntohs(mapped_addr[0].sin_port)));
+ }
+ /* Success! */
+ break;
+#endif
+
+ } else if (cfg->public_addr.slen) {
+
+ status = pj_sockaddr_in_init(&mapped_addr[0], &cfg->public_addr,
+ (pj_uint16_t)next_rtp_port);
+ if (status != PJ_SUCCESS)
+ goto on_error;
+
+ status = pj_sockaddr_in_init(&mapped_addr[1], &cfg->public_addr,
+ (pj_uint16_t)(next_rtp_port+1));
+ if (status != PJ_SUCCESS)
+ goto on_error;
+
+ break;
+
+ } else {
+
+ if (bound_addr.sin_addr.s_addr == 0) {
+ pj_sockaddr addr;
+
+ /* Get local IP address. */
+ status = pj_gethostip(pj_AF_INET(), &addr);
+ if (status != PJ_SUCCESS)
+ goto on_error;
+
+ bound_addr.sin_addr.s_addr = addr.ipv4.sin_addr.s_addr;
+ }
+
+ for (i=0; i<2; ++i) {
+ pj_sockaddr_in_init(&mapped_addr[i], NULL, 0);
+ mapped_addr[i].sin_addr.s_addr = bound_addr.sin_addr.s_addr;
+ }
+
+ mapped_addr[0].sin_port=pj_htons((pj_uint16_t)next_rtp_port);
+ mapped_addr[1].sin_port=pj_htons((pj_uint16_t)(next_rtp_port+1));
+ break;
+ }
+ }
+
+ if (sock[0] == PJ_INVALID_SOCKET) {
+ PJ_LOG(1,(THIS_FILE,
+ "Unable to find appropriate RTP/RTCP ports combination"));
+ goto on_error;
+ }
+
+
+ skinfo->rtp_sock = sock[0];
+ pj_memcpy(&skinfo->rtp_addr_name,
+ &mapped_addr[0], sizeof(pj_sockaddr_in));
+
+ skinfo->rtcp_sock = sock[1];
+ pj_memcpy(&skinfo->rtcp_addr_name,
+ &mapped_addr[1], sizeof(pj_sockaddr_in));
+
+ PJ_LOG(4,(THIS_FILE, "RTP socket reachable at %s",
+ pj_sockaddr_print(&skinfo->rtp_addr_name, addr_buf,
+ sizeof(addr_buf), 3)));
+ PJ_LOG(4,(THIS_FILE, "RTCP socket reachable at %s",
+ pj_sockaddr_print(&skinfo->rtcp_addr_name, addr_buf,
+ sizeof(addr_buf), 3)));
+
+ next_rtp_port += 2;
+ return PJ_SUCCESS;
+
+on_error:
+ for (i=0; i<2; ++i) {
+ if (sock[i] != PJ_INVALID_SOCKET)
+ pj_sock_close(sock[i]);
+ }
+ return status;
+}
+
+/* Create normal UDP media transports */
+static pj_status_t create_udp_media_transport(const pjsua_transport_config *cfg,
+ pjsua_call_media *call_med)
+{
+ pjmedia_sock_info skinfo;
+ pj_status_t status;
+
+ status = create_rtp_rtcp_sock(cfg, &skinfo);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Unable to create RTP/RTCP socket",
+ status);
+ goto on_error;
+ }
+
+ status = pjmedia_transport_udp_attach(pjsua_var.med_endpt, NULL,
+ &skinfo, 0, &call_med->tp);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Unable to create media transport",
+ status);
+ goto on_error;
+ }
+
+ pjmedia_transport_simulate_lost(call_med->tp, PJMEDIA_DIR_ENCODING,
+ pjsua_var.media_cfg.tx_drop_pct);
+
+ pjmedia_transport_simulate_lost(call_med->tp, PJMEDIA_DIR_DECODING,
+ pjsua_var.media_cfg.rx_drop_pct);
+
+ call_med->tp_ready = PJ_SUCCESS;
+
+ return PJ_SUCCESS;
+
+on_error:
+ if (call_med->tp)
+ pjmedia_transport_close(call_med->tp);
+
+ return status;
+}
+
+#if DISABLED_FOR_TICKET_1185
+/* Create normal UDP media transports */
+static pj_status_t create_udp_media_transports(pjsua_transport_config *cfg)
+{
+ unsigned i;
+ pj_status_t status;
+
+ for (i=0; i < pjsua_var.ua_cfg.max_calls; ++i) {
+ pjsua_call *call = &pjsua_var.calls[i];
+ unsigned strm_idx;
+
+ for (strm_idx=0; strm_idx < call->med_cnt; ++strm_idx) {
+ pjsua_call_media *call_med = &call->media[strm_idx];
+
+ status = create_udp_media_transport(cfg, &call_med->tp);
+ if (status != PJ_SUCCESS)
+ goto on_error;
+ }
+ }
+
+ return PJ_SUCCESS;
+
+on_error:
+ for (i=0; i < pjsua_var.ua_cfg.max_calls; ++i) {
+ pjsua_call *call = &pjsua_var.calls[i];
+ unsigned strm_idx;
+
+ for (strm_idx=0; strm_idx < call->med_cnt; ++strm_idx) {
+ pjsua_call_media *call_med = &call->media[strm_idx];
+
+ if (call_med->tp) {
+ pjmedia_transport_close(call_med->tp);
+ call_med->tp = NULL;
+ }
+ }
+ }
+ return status;
+}
+#endif
+
+static void med_tp_timer_cb(void *user_data)
+{
+ pjsua_call_media *call_med = (pjsua_call_media*)user_data;
+ pjsua_call *call = NULL;
+ pjsip_dialog *dlg = NULL;
+
+ acquire_call("med_tp_timer_cb", call_med->call->index, &call, &dlg);
+
+ call_med->tp_ready = call_med->tp_result;
+ if (call_med->med_create_cb)
+ (*call_med->med_create_cb)(call_med, call_med->tp_ready,
+ call_med->call->secure_level, NULL);
+
+ if (dlg)
+ pjsip_dlg_dec_lock(dlg);
+}
+
+/* This callback is called when ICE negotiation completes */
+static void on_ice_complete(pjmedia_transport *tp,
+ pj_ice_strans_op op,
+ pj_status_t result)
+{
+ pjsua_call_media *call_med = (pjsua_call_media*)tp->user_data;
+
+ if (!call_med)
+ return;
+
+ switch (op) {
+ case PJ_ICE_STRANS_OP_INIT:
+ call_med->tp_result = result;
+ pjsua_schedule_timer2(&med_tp_timer_cb, call_med, 1);
+ break;
+ case PJ_ICE_STRANS_OP_NEGOTIATION:
+ if (result != PJ_SUCCESS) {
+ call_med->state = PJSUA_CALL_MEDIA_ERROR;
+ call_med->dir = PJMEDIA_DIR_NONE;
+
+ if (call_med->call && pjsua_var.ua_cfg.cb.on_call_media_state) {
+ pjsua_var.ua_cfg.cb.on_call_media_state(call_med->call->index);
+ }
+ } else if (call_med->call) {
+ /* Send UPDATE if default transport address is different than
+ * what was advertised (ticket #881)
+ */
+ pjmedia_transport_info tpinfo;
+ pjmedia_ice_transport_info *ii = NULL;
+ unsigned i;
+
+ pjmedia_transport_info_init(&tpinfo);
+ pjmedia_transport_get_info(tp, &tpinfo);
+ for (i=0; i<tpinfo.specific_info_cnt; ++i) {
+ if (tpinfo.spc_info[i].type==PJMEDIA_TRANSPORT_TYPE_ICE) {
+ ii = (pjmedia_ice_transport_info*)
+ tpinfo.spc_info[i].buffer;
+ break;
+ }
+ }
+
+ if (ii && ii->role==PJ_ICE_SESS_ROLE_CONTROLLING &&
+ pj_sockaddr_cmp(&tpinfo.sock_info.rtp_addr_name,
+ &call_med->rtp_addr))
+ {
+ pj_bool_t use_update;
+ const pj_str_t STR_UPDATE = { "UPDATE", 6 };
+ pjsip_dialog_cap_status support_update;
+ pjsip_dialog *dlg;
+
+ dlg = call_med->call->inv->dlg;
+ support_update = pjsip_dlg_remote_has_cap(dlg, PJSIP_H_ALLOW,
+ NULL, &STR_UPDATE);
+ use_update = (support_update == PJSIP_DIALOG_CAP_SUPPORTED);
+
+ PJ_LOG(4,(THIS_FILE,
+ "ICE default transport address has changed for "
+ "call %d, sending %s",
+ call_med->call->index,
+ (use_update ? "UPDATE" : "re-INVITE")));
+
+ if (use_update)
+ pjsua_call_update(call_med->call->index, 0, NULL);
+ else
+ pjsua_call_reinvite(call_med->call->index, 0, NULL);
+ }
+ }
+ break;
+ case PJ_ICE_STRANS_OP_KEEP_ALIVE:
+ if (result != PJ_SUCCESS) {
+ PJ_PERROR(4,(THIS_FILE, result,
+ "ICE keep alive failure for transport %d:%d",
+ call_med->call->index, call_med->idx));
+ }
+ if (pjsua_var.ua_cfg.cb.on_call_media_transport_state) {
+ pjsua_med_tp_state_info info;
+
+ pj_bzero(&info, sizeof(info));
+ info.med_idx = call_med->idx;
+ info.state = call_med->tp_st;
+ info.status = result;
+ info.ext_info = &op;
+ (*pjsua_var.ua_cfg.cb.on_call_media_transport_state)(
+ call_med->call->index, &info);
+ }
+ if (pjsua_var.ua_cfg.cb.on_ice_transport_error) {
+ pjsua_call_id id = call_med->call->index;
+ (*pjsua_var.ua_cfg.cb.on_ice_transport_error)(id, op, result,
+ NULL);
+ }
+ break;
+ }
+}
+
+
+/* Parse "HOST:PORT" format */
+static pj_status_t parse_host_port(const pj_str_t *host_port,
+ pj_str_t *host, pj_uint16_t *port)
+{
+ pj_str_t str_port;
+
+ str_port.ptr = pj_strchr(host_port, ':');
+ if (str_port.ptr != NULL) {
+ int iport;
+
+ host->ptr = host_port->ptr;
+ host->slen = (str_port.ptr - host->ptr);
+ str_port.ptr++;
+ str_port.slen = host_port->slen - host->slen - 1;
+ iport = (int)pj_strtoul(&str_port);
+ if (iport < 1 || iport > 65535)
+ return PJ_EINVAL;
+ *port = (pj_uint16_t)iport;
+ } else {
+ *host = *host_port;
+ *port = 0;
+ }
+
+ return PJ_SUCCESS;
+}
+
+/* Create ICE media transports (when ice is enabled) */
+static pj_status_t create_ice_media_transport(
+ const pjsua_transport_config *cfg,
+ pjsua_call_media *call_med,
+ pj_bool_t async)
+{
+ char stunip[PJ_INET6_ADDRSTRLEN];
+ pj_ice_strans_cfg ice_cfg;
+ pjmedia_ice_cb ice_cb;
+ char name[32];
+ unsigned comp_cnt;
+ pj_status_t status;
+
+ /* Make sure STUN server resolution has completed */
+ status = resolve_stun_server(PJ_TRUE);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Error resolving STUN server", status);
+ return status;
+ }
+
+ /* Create ICE stream transport configuration */
+ pj_ice_strans_cfg_default(&ice_cfg);
+ pj_stun_config_init(&ice_cfg.stun_cfg, &pjsua_var.cp.factory, 0,
+ pjsip_endpt_get_ioqueue(pjsua_var.endpt),
+ pjsip_endpt_get_timer_heap(pjsua_var.endpt));
+
+ ice_cfg.af = pj_AF_INET();
+ ice_cfg.resolver = pjsua_var.resolver;
+
+ ice_cfg.opt = pjsua_var.media_cfg.ice_opt;
+
+ /* Configure STUN settings */
+ if (pj_sockaddr_has_addr(&pjsua_var.stun_srv)) {
+ pj_sockaddr_print(&pjsua_var.stun_srv, stunip, sizeof(stunip), 0);
+ ice_cfg.stun.server = pj_str(stunip);
+ ice_cfg.stun.port = pj_sockaddr_get_port(&pjsua_var.stun_srv);
+ }
+ if (pjsua_var.media_cfg.ice_max_host_cands >= 0)
+ ice_cfg.stun.max_host_cands = pjsua_var.media_cfg.ice_max_host_cands;
+
+ /* Copy QoS setting to STUN setting */
+ ice_cfg.stun.cfg.qos_type = cfg->qos_type;
+ pj_memcpy(&ice_cfg.stun.cfg.qos_params, &cfg->qos_params,
+ sizeof(cfg->qos_params));
+
+ /* Configure TURN settings */
+ if (pjsua_var.media_cfg.enable_turn) {
+ status = parse_host_port(&pjsua_var.media_cfg.turn_server,
+ &ice_cfg.turn.server,
+ &ice_cfg.turn.port);
+ if (status != PJ_SUCCESS || ice_cfg.turn.server.slen == 0) {
+ PJ_LOG(1,(THIS_FILE, "Invalid TURN server setting"));
+ return PJ_EINVAL;
+ }
+ if (ice_cfg.turn.port == 0)
+ ice_cfg.turn.port = 3479;
+ ice_cfg.turn.conn_type = pjsua_var.media_cfg.turn_conn_type;
+ pj_memcpy(&ice_cfg.turn.auth_cred,
+ &pjsua_var.media_cfg.turn_auth_cred,
+ sizeof(ice_cfg.turn.auth_cred));
+
+ /* Copy QoS setting to TURN setting */
+ ice_cfg.turn.cfg.qos_type = cfg->qos_type;
+ pj_memcpy(&ice_cfg.turn.cfg.qos_params, &cfg->qos_params,
+ sizeof(cfg->qos_params));
+ }
+
+ pj_bzero(&ice_cb, sizeof(pjmedia_ice_cb));
+ ice_cb.on_ice_complete = &on_ice_complete;
+ pj_ansi_snprintf(name, sizeof(name), "icetp%02d", call_med->idx);
+ call_med->tp_ready = PJ_EPENDING;
+
+ comp_cnt = 1;
+ if (PJMEDIA_ADVERTISE_RTCP && !pjsua_var.media_cfg.ice_no_rtcp)
+ ++comp_cnt;
+
+ status = pjmedia_ice_create3(pjsua_var.med_endpt, name, comp_cnt,
+ &ice_cfg, &ice_cb, 0, call_med,
+ &call_med->tp);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Unable to create ICE media transport",
+ status);
+ goto on_error;
+ }
+
+ /* Wait until transport is initialized, or time out */
+ if (!async) {
+ pj_bool_t has_pjsua_lock = PJSUA_LOCK_IS_LOCKED();
+ if (has_pjsua_lock)
+ PJSUA_UNLOCK();
+ while (call_med->tp_ready == PJ_EPENDING) {
+ pjsua_handle_events(100);
+ }
+ if (has_pjsua_lock)
+ PJSUA_LOCK();
+ }
+
+ if (async && call_med->tp_ready == PJ_EPENDING) {
+ return PJ_EPENDING;
+ } else if (call_med->tp_ready != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Error initializing ICE media transport",
+ call_med->tp_ready);
+ status = call_med->tp_ready;
+ goto on_error;
+ }
+
+ pjmedia_transport_simulate_lost(call_med->tp, PJMEDIA_DIR_ENCODING,
+ pjsua_var.media_cfg.tx_drop_pct);
+
+ pjmedia_transport_simulate_lost(call_med->tp, PJMEDIA_DIR_DECODING,
+ pjsua_var.media_cfg.rx_drop_pct);
+
+ return PJ_SUCCESS;
+
+on_error:
+ if (call_med->tp != NULL) {
+ pjmedia_transport_close(call_med->tp);
+ call_med->tp = NULL;
+ }
+
+ return status;
+}
+
+#if DISABLED_FOR_TICKET_1185
+/* Create ICE media transports (when ice is enabled) */
+static pj_status_t create_ice_media_transports(pjsua_transport_config *cfg)
+{
+ unsigned i;
+ pj_status_t status;
+
+ for (i=0; i < pjsua_var.ua_cfg.max_calls; ++i) {
+ pjsua_call *call = &pjsua_var.calls[i];
+ unsigned strm_idx;
+
+ for (strm_idx=0; strm_idx < call->med_cnt; ++strm_idx) {
+ pjsua_call_media *call_med = &call->media[strm_idx];
+
+ status = create_ice_media_transport(cfg, call_med);
+ if (status != PJ_SUCCESS)
+ goto on_error;
+ }
+ }
+
+ return PJ_SUCCESS;
+
+on_error:
+ for (i=0; i < pjsua_var.ua_cfg.max_calls; ++i) {
+ pjsua_call *call = &pjsua_var.calls[i];
+ unsigned strm_idx;
+
+ for (strm_idx=0; strm_idx < call->med_cnt; ++strm_idx) {
+ pjsua_call_media *call_med = &call->media[strm_idx];
+
+ if (call_med->tp) {
+ pjmedia_transport_close(call_med->tp);
+ call_med->tp = NULL;
+ }
+ }
+ }
+ return status;
+}
+#endif
+
+#if DISABLED_FOR_TICKET_1185
+/*
+ * Create media transports for all the calls. This function creates
+ * one UDP media transport for each call.
+ */
+PJ_DEF(pj_status_t) pjsua_media_transports_create(
+ const pjsua_transport_config *app_cfg)
+{
+ pjsua_transport_config cfg;
+ unsigned i;
+ pj_status_t status;
+
+
+ /* Make sure pjsua_init() has been called */
+ PJ_ASSERT_RETURN(pjsua_var.ua_cfg.max_calls>0, PJ_EINVALIDOP);
+
+ PJSUA_LOCK();
+
+ /* Delete existing media transports */
+ for (i=0; i<pjsua_var.ua_cfg.max_calls; ++i) {
+ pjsua_call *call = &pjsua_var.calls[i];
+ unsigned strm_idx;
+
+ for (strm_idx=0; strm_idx < call->med_cnt; ++strm_idx) {
+ pjsua_call_media *call_med = &call->media[strm_idx];
+
+ if (call_med->tp && call_med->tp_auto_del) {
+ pjmedia_transport_close(call_med->tp);
+ call_med->tp = NULL;
+ call_med->tp_orig = NULL;
+ }
+ }
+ }
+
+ /* Copy config */
+ pjsua_transport_config_dup(pjsua_var.pool, &cfg, app_cfg);
+
+ /* Create the transports */
+ if (pjsua_var.media_cfg.enable_ice) {
+ status = create_ice_media_transports(&cfg);
+ } else {
+ status = create_udp_media_transports(&cfg);
+ }
+
+ /* Set media transport auto_delete to True */
+ for (i=0; i<pjsua_var.ua_cfg.max_calls; ++i) {
+ pjsua_call *call = &pjsua_var.calls[i];
+ unsigned strm_idx;
+
+ for (strm_idx=0; strm_idx < call->med_cnt; ++strm_idx) {
+ pjsua_call_media *call_med = &call->media[strm_idx];
+
+ call_med->tp_auto_del = PJ_TRUE;
+ }
+ }
+
+ PJSUA_UNLOCK();
+
+ return status;
+}
+
+/*
+ * Attach application's created media transports.
+ */
+PJ_DEF(pj_status_t) pjsua_media_transports_attach(pjsua_media_transport tp[],
+ unsigned count,
+ pj_bool_t auto_delete)
+{
+ unsigned i;
+
+ PJ_ASSERT_RETURN(tp && count==pjsua_var.ua_cfg.max_calls, PJ_EINVAL);
+
+ /* Assign the media transports */
+ for (i=0; i<pjsua_var.ua_cfg.max_calls; ++i) {
+ pjsua_call *call = &pjsua_var.calls[i];
+ unsigned strm_idx;
+
+ for (strm_idx=0; strm_idx < call->med_cnt; ++strm_idx) {
+ pjsua_call_media *call_med = &call->media[strm_idx];
+
+ if (call_med->tp && call_med->tp_auto_del) {
+ pjmedia_transport_close(call_med->tp);
+ call_med->tp = NULL;
+ call_med->tp_orig = NULL;
+ }
+ }
+
+ PJ_TODO(remove_pjsua_media_transports_attach);
+
+ call->media[0].tp = tp[i].transport;
+ call->media[0].tp_auto_del = auto_delete;
+ }
+
+ return PJ_SUCCESS;
+}
+#endif
+
+/* Go through the list of media in the SDP, find acceptable media, and
+ * sort them based on the "quality" of the media, and store the indexes
+ * in the specified array. Media with the best quality will be listed
+ * first in the array. The quality factors considered currently is
+ * encryption.
+ */
+static void sort_media(const pjmedia_sdp_session *sdp,
+ const pj_str_t *type,
+ pjmedia_srtp_use use_srtp,
+ pj_uint8_t midx[],
+ unsigned *p_count,
+ unsigned *p_total_count)
+{
+ unsigned i;
+ unsigned count = 0;
+ int score[PJSUA_MAX_CALL_MEDIA];
+
+ pj_assert(*p_count >= PJSUA_MAX_CALL_MEDIA);
+ pj_assert(*p_total_count >= PJSUA_MAX_CALL_MEDIA);
+
+ *p_count = 0;
+ *p_total_count = 0;
+ for (i=0; i<PJSUA_MAX_CALL_MEDIA; ++i)
+ score[i] = 1;
+
+ /* Score each media */
+ for (i=0; i<sdp->media_count && count<PJSUA_MAX_CALL_MEDIA; ++i) {
+ const pjmedia_sdp_media *m = sdp->media[i];
+ const pjmedia_sdp_conn *c;
+
+ /* Skip different media */
+ if (pj_stricmp(&m->desc.media, type) != 0) {
+ score[count++] = -22000;
+ continue;
+ }
+
+ c = m->conn? m->conn : sdp->conn;
+
+ /* Supported transports */
+ if (pj_stricmp2(&m->desc.transport, "RTP/SAVP")==0) {
+ switch (use_srtp) {
+ case PJMEDIA_SRTP_MANDATORY:
+ case PJMEDIA_SRTP_OPTIONAL:
+ ++score[i];
+ break;
+ case PJMEDIA_SRTP_DISABLED:
+ //--score[i];
+ score[i] -= 5;
+ break;
+ }
+ } else if (pj_stricmp2(&m->desc.transport, "RTP/AVP")==0) {
+ switch (use_srtp) {
+ case PJMEDIA_SRTP_MANDATORY:
+ //--score[i];
+ score[i] -= 5;
+ break;
+ case PJMEDIA_SRTP_OPTIONAL:
+ /* No change in score */
+ break;
+ case PJMEDIA_SRTP_DISABLED:
+ ++score[i];
+ break;
+ }
+ } else {
+ score[i] -= 10;
+ }
+
+ /* Is media disabled? */
+ if (m->desc.port == 0)
+ score[i] -= 10;
+
+ /* Is media inactive? */
+ if (pjmedia_sdp_media_find_attr2(m, "inactive", NULL) ||
+ pj_strcmp2(&c->addr, "0.0.0.0") == 0)
+ {
+ //score[i] -= 10;
+ score[i] -= 1;
+ }
+
+ ++count;
+ }
+
+ /* Created sorted list based on quality */
+ for (i=0; i<count; ++i) {
+ unsigned j;
+ int best = 0;
+
+ for (j=1; j<count; ++j) {
+ if (score[j] > score[best])
+ best = j;
+ }
+ /* Don't put media with negative score, that media is unacceptable
+ * for us.
+ */
+ midx[i] = (pj_uint8_t)best;
+ if (score[best] >= 0)
+ (*p_count)++;
+ if (score[best] > -22000)
+ (*p_total_count)++;
+
+ score[best] = -22000;
+
+ }
+}
+
+/* Callback to receive media events */
+pj_status_t call_media_on_event(pjmedia_event *event,
+ void *user_data)
+{
+ pjsua_call_media *call_med = (pjsua_call_media*)user_data;
+ pjsua_call *call = call_med->call;
+ pj_status_t status = PJ_SUCCESS;
+
+ switch(event->type) {
+ case PJMEDIA_EVENT_KEYFRAME_MISSING:
+ if (call->opt.req_keyframe_method & PJSUA_VID_REQ_KEYFRAME_SIP_INFO)
+ {
+ pj_timestamp now;
+
+ pj_get_timestamp(&now);
+ if (pj_elapsed_msec(&call_med->last_req_keyframe, &now) >=
+ PJSUA_VID_REQ_KEYFRAME_INTERVAL)
+ {
+ pjsua_msg_data msg_data;
+ const pj_str_t SIP_INFO = {"INFO", 4};
+ const char *BODY_TYPE = "application/media_control+xml";
+ const char *BODY =
+ "<?xml version=\"1.0\" encoding=\"utf-8\" ?>"
+ "<media_control><vc_primitive><to_encoder>"
+ "<picture_fast_update/>"
+ "</to_encoder></vc_primitive></media_control>";
+
+ PJ_LOG(4,(THIS_FILE,
+ "Sending video keyframe request via SIP INFO"));
+
+ pjsua_msg_data_init(&msg_data);
+ pj_cstr(&msg_data.content_type, BODY_TYPE);
+ pj_cstr(&msg_data.msg_body, BODY);
+ status = pjsua_call_send_request(call->index, &SIP_INFO,
+ &msg_data);
+ if (status != PJ_SUCCESS) {
+ pj_perror(3, THIS_FILE, status,
+ "Failed requesting keyframe via SIP INFO");
+ } else {
+ call_med->last_req_keyframe = now;
+ }
+ }
+ }
+ break;
+
+ default:
+ break;
+ }
+
+ if (pjsua_var.ua_cfg.cb.on_call_media_event && call) {
+ (*pjsua_var.ua_cfg.cb.on_call_media_event)(call->index,
+ call_med->idx, event);
+ }
+
+ return status;
+}
+
+/* Set media transport state and notify the application via the callback. */
+void pjsua_set_media_tp_state(pjsua_call_media *call_med,
+ pjsua_med_tp_st tp_st)
+{
+ if (pjsua_var.ua_cfg.cb.on_call_media_transport_state &&
+ call_med->tp_st != tp_st)
+ {
+ pjsua_med_tp_state_info info;
+
+ pj_bzero(&info, sizeof(info));
+ info.med_idx = call_med->idx;
+ info.state = tp_st;
+ info.status = call_med->tp_ready;
+ (*pjsua_var.ua_cfg.cb.on_call_media_transport_state)(
+ call_med->call->index, &info);
+ }
+
+ call_med->tp_st = tp_st;
+}
+
+/* Callback to resume pjsua_call_media_init() after media transport
+ * creation is completed.
+ */
+static pj_status_t call_media_init_cb(pjsua_call_media *call_med,
+ pj_status_t status,
+ int security_level,
+ int *sip_err_code)
+{
+ pjsua_acc *acc = &pjsua_var.acc[call_med->call->acc_id];
+ pjmedia_transport_info tpinfo;
+ int err_code = 0;
+
+ if (status != PJ_SUCCESS)
+ goto on_return;
+
+ pjmedia_transport_simulate_lost(call_med->tp, PJMEDIA_DIR_ENCODING,
+ pjsua_var.media_cfg.tx_drop_pct);
+
+ pjmedia_transport_simulate_lost(call_med->tp, PJMEDIA_DIR_DECODING,
+ pjsua_var.media_cfg.rx_drop_pct);
+
+ if (call_med->tp_st == PJSUA_MED_TP_CREATING)
+ pjsua_set_media_tp_state(call_med, PJSUA_MED_TP_IDLE);
+
+ if (!call_med->tp_orig &&
+ pjsua_var.ua_cfg.cb.on_create_media_transport)
+ {
+ call_med->use_custom_med_tp = PJ_TRUE;
+ } else
+ call_med->use_custom_med_tp = PJ_FALSE;
+
+#if defined(PJMEDIA_HAS_SRTP) && (PJMEDIA_HAS_SRTP != 0)
+ /* This function may be called when SRTP transport already exists
+ * (e.g: in re-invite, update), don't need to destroy/re-create.
+ */
+ if (!call_med->tp_orig) {
+ pjmedia_srtp_setting srtp_opt;
+ pjmedia_transport *srtp = NULL;
+
+ /* Check if SRTP requires secure signaling */
+ if (acc->cfg.use_srtp != PJMEDIA_SRTP_DISABLED) {
+ if (security_level < acc->cfg.srtp_secure_signaling) {
+ err_code = PJSIP_SC_NOT_ACCEPTABLE;
+ status = PJSIP_ESESSIONINSECURE;
+ goto on_return;
+ }
+ }
+
+ /* Always create SRTP adapter */
+ pjmedia_srtp_setting_default(&srtp_opt);
+ srtp_opt.close_member_tp = PJ_TRUE;
+
+ /* If media session has been ever established, let's use remote's
+ * preference in SRTP usage policy, especially when it is stricter.
+ */
+ if (call_med->rem_srtp_use > acc->cfg.use_srtp)
+ srtp_opt.use = call_med->rem_srtp_use;
+ else
+ srtp_opt.use = acc->cfg.use_srtp;
+
+ status = pjmedia_transport_srtp_create(pjsua_var.med_endpt,
+ call_med->tp,
+ &srtp_opt, &srtp);
+ if (status != PJ_SUCCESS) {
+ err_code = PJSIP_SC_INTERNAL_SERVER_ERROR;
+ goto on_return;
+ }
+
+ /* Set SRTP as current media transport */
+ call_med->tp_orig = call_med->tp;
+ call_med->tp = srtp;
+ }
+#else
+ call_med->tp_orig = call_med->tp;
+ PJ_UNUSED_ARG(security_level);
+#endif
+
+
+ pjmedia_transport_info_init(&tpinfo);
+ pjmedia_transport_get_info(call_med->tp, &tpinfo);
+
+ pj_sockaddr_cp(&call_med->rtp_addr, &tpinfo.sock_info.rtp_addr_name);
+
+
+on_return:
+ if (status != PJ_SUCCESS && call_med->tp) {
+ pjsua_set_media_tp_state(call_med, PJSUA_MED_TP_NULL);
+ pjmedia_transport_close(call_med->tp);
+ call_med->tp = NULL;
+ }
+
+ if (sip_err_code)
+ *sip_err_code = err_code;
+
+ if (call_med->med_init_cb) {
+ pjsua_med_tp_state_info info;
+
+ pj_bzero(&info, sizeof(info));
+ info.status = status;
+ info.state = call_med->tp_st;
+ info.med_idx = call_med->idx;
+ info.sip_err_code = err_code;
+ (*call_med->med_init_cb)(call_med->call->index, &info);
+ }
+
+ return status;
+}
+
+/* Initialize the media line */
+pj_status_t pjsua_call_media_init(pjsua_call_media *call_med,
+ pjmedia_type type,
+ const pjsua_transport_config *tcfg,
+ int security_level,
+ int *sip_err_code,
+ pj_bool_t async,
+ pjsua_med_tp_state_cb cb)
+{
+ pj_status_t status = PJ_SUCCESS;
+
+ /*
+ * Note: this function may be called when the media already exists
+ * (e.g. in reinvites, updates, etc.)
+ */
+ call_med->type = type;
+
+ /* Create the media transport for initial call. Here are the possible
+ * media transport state and the action needed:
+ * - PJSUA_MED_TP_NULL or call_med->tp==NULL, create one.
+ * - PJSUA_MED_TP_RUNNING, do nothing.
+ * - PJSUA_MED_TP_DISABLED, re-init (media_create(), etc). Currently,
+ * this won't happen as media_channel_update() will always clean up
+ * the unused transport of a disabled media.
+ */
+ if (call_med->tp == NULL) {
+#if defined(PJMEDIA_HAS_VIDEO) && (PJMEDIA_HAS_VIDEO != 0)
+ /* While in initial call, set default video devices */
+ if (type == PJMEDIA_TYPE_VIDEO) {
+ status = pjsua_vid_channel_init(call_med);
+ if (status != PJ_SUCCESS)
+ return status;
+ }
+#endif
+
+ pjsua_set_media_tp_state(call_med, PJSUA_MED_TP_CREATING);
+
+ if (pjsua_var.media_cfg.enable_ice) {
+ status = create_ice_media_transport(tcfg, call_med, async);
+ if (async && status == PJ_EPENDING) {
+ /* We will resume call media initialization in the
+ * on_ice_complete() callback.
+ */
+ call_med->med_create_cb = &call_media_init_cb;
+ call_med->med_init_cb = cb;
+
+ return PJ_EPENDING;
+ }
+ } else {
+ status = create_udp_media_transport(tcfg, call_med);
+ }
+
+ if (status != PJ_SUCCESS) {
+ PJ_PERROR(1,(THIS_FILE, status, "Error creating media transport"));
+ return status;
+ }
+
+ /* Media transport creation completed immediately, so
+ * we don't need to call the callback.
+ */
+ call_med->med_init_cb = NULL;
+
+ } else if (call_med->tp_st == PJSUA_MED_TP_DISABLED) {
+ /* Media is being reenabled. */
+ //pjsua_set_media_tp_state(call_med, PJSUA_MED_TP_IDLE);
+
+ pj_assert(!"Currently no media transport reuse");
+ }
+
+ return call_media_init_cb(call_med, status, security_level,
+ sip_err_code);
+}
+
+/* Callback to resume pjsua_media_channel_init() after media transport
+ * initialization is completed.
+ */
+static pj_status_t media_channel_init_cb(pjsua_call_id call_id,
+ const pjsua_med_tp_state_info *info)
+{
+ pjsua_call *call = &pjsua_var.calls[call_id];
+ pj_status_t status = (info? info->status : PJ_SUCCESS);
+ unsigned mi;
+
+ if (info) {
+ pj_mutex_lock(call->med_ch_mutex);
+
+ /* Set the callback to NULL to indicate that the async operation
+ * has completed.
+ */
+ call->media_prov[info->med_idx].med_init_cb = NULL;
+
+ /* In case of failure, save the information to be returned
+ * by the last media transport to finish.
+ */
+ if (info->status != PJ_SUCCESS)
+ pj_memcpy(&call->med_ch_info, info, sizeof(info));
+
+ /* Check whether all the call's medias have finished calling their
+ * callbacks.
+ */
+ for (mi=0; mi < call->med_prov_cnt; ++mi) {
+ pjsua_call_media *call_med = &call->media_prov[mi];
+
+ if (call_med->med_init_cb) {
+ pj_mutex_unlock(call->med_ch_mutex);
+ return PJ_SUCCESS;
+ }
+
+ if (call_med->tp_ready != PJ_SUCCESS)
+ status = call_med->tp_ready;
+ }
+
+ /* OK, we are called by the last media transport finished. */
+ pj_mutex_unlock(call->med_ch_mutex);
+ }
+
+ if (call->med_ch_mutex) {
+ pj_mutex_destroy(call->med_ch_mutex);
+ call->med_ch_mutex = NULL;
+ }
+
+ if (status != PJ_SUCCESS) {
+ if (call->med_ch_info.status == PJ_SUCCESS) {
+ call->med_ch_info.status = status;
+ call->med_ch_info.sip_err_code = PJSIP_SC_TEMPORARILY_UNAVAILABLE;
+ }
+ pjsua_media_prov_clean_up(call_id);
+ goto on_return;
+ }
+
+ /* Tell the media transport of a new offer/answer session */
+ for (mi=0; mi < call->med_prov_cnt; ++mi) {
+ pjsua_call_media *call_med = &call->media_prov[mi];
+
+ /* Note: tp may be NULL if this media line is disabled */
+ if (call_med->tp && call_med->tp_st == PJSUA_MED_TP_IDLE) {
+ pj_pool_t *tmp_pool = call->async_call.pool_prov;
+
+ if (!tmp_pool) {
+ tmp_pool = (call->inv? call->inv->pool_prov:
+ call->async_call.dlg->pool);
+ }
+
+ if (call_med->use_custom_med_tp) {
+ unsigned custom_med_tp_flags = 0;
+
+ /* Use custom media transport returned by the application */
+ call_med->tp =
+ (*pjsua_var.ua_cfg.cb.on_create_media_transport)
+ (call_id, mi, call_med->tp,
+ custom_med_tp_flags);
+ if (!call_med->tp) {
+ status =
+ PJSIP_ERRNO_FROM_SIP_STATUS(PJSIP_SC_TEMPORARILY_UNAVAILABLE);
+ }
+ }
+
+ if (call_med->tp) {
+ status = pjmedia_transport_media_create(
+ call_med->tp, tmp_pool,
+ 0, call->async_call.rem_sdp, mi);
+ }
+ if (status != PJ_SUCCESS) {
+ call->med_ch_info.status = status;
+ call->med_ch_info.med_idx = mi;
+ call->med_ch_info.state = call_med->tp_st;
+ call->med_ch_info.sip_err_code = PJSIP_SC_TEMPORARILY_UNAVAILABLE;
+ pjsua_media_prov_clean_up(call_id);
+ goto on_return;
+ }
+
+ pjsua_set_media_tp_state(call_med, PJSUA_MED_TP_INIT);
+ }
+ }
+
+ call->med_ch_info.status = PJ_SUCCESS;
+
+on_return:
+ if (call->med_ch_cb)
+ (*call->med_ch_cb)(call->index, &call->med_ch_info);
+
+ return status;
+}
+
+
+/* Clean up media transports in provisional media that is not used
+ * by call media.
+ */
+void pjsua_media_prov_clean_up(pjsua_call_id call_id)
+{
+ pjsua_call *call = &pjsua_var.calls[call_id];
+ unsigned i;
+
+ for (i = 0; i < call->med_prov_cnt; ++i) {
+ pjsua_call_media *call_med = &call->media_prov[i];
+ unsigned j;
+ pj_bool_t used = PJ_FALSE;
+
+ if (call_med->tp == NULL)
+ continue;
+
+ for (j = 0; j < call->med_cnt; ++j) {
+ if (call->media[j].tp == call_med->tp) {
+ used = PJ_TRUE;
+ break;
+ }
+ }
+
+ if (!used) {
+ if (call_med->tp_st > PJSUA_MED_TP_IDLE) {
+ pjsua_set_media_tp_state(call_med, PJSUA_MED_TP_IDLE);
+ pjmedia_transport_media_stop(call_med->tp);
+ }
+ pjsua_set_media_tp_state(call_med, PJSUA_MED_TP_NULL);
+ pjmedia_transport_close(call_med->tp);
+ call_med->tp = call_med->tp_orig = NULL;
+ }
+ }
+}
+
+
+pj_status_t pjsua_media_channel_init(pjsua_call_id call_id,
+ pjsip_role_e role,
+ int security_level,
+ pj_pool_t *tmp_pool,
+ const pjmedia_sdp_session *rem_sdp,
+ int *sip_err_code,
+ pj_bool_t async,
+ pjsua_med_tp_state_cb cb)
+{
+ const pj_str_t STR_AUDIO = { "audio", 5 };
+ const pj_str_t STR_VIDEO = { "video", 5 };
+ pjsua_call *call = &pjsua_var.calls[call_id];
+ pjsua_acc *acc = &pjsua_var.acc[call->acc_id];
+ pj_uint8_t maudidx[PJSUA_MAX_CALL_MEDIA];
+ unsigned maudcnt = PJ_ARRAY_SIZE(maudidx);
+ unsigned mtotaudcnt = PJ_ARRAY_SIZE(maudidx);
+ pj_uint8_t mvididx[PJSUA_MAX_CALL_MEDIA];
+ unsigned mvidcnt = PJ_ARRAY_SIZE(mvididx);
+ unsigned mtotvidcnt = PJ_ARRAY_SIZE(mvididx);
+ unsigned mi;
+ pj_bool_t pending_med_tp = PJ_FALSE;
+ pj_bool_t reinit = PJ_FALSE;
+ pj_status_t status;
+
+ PJ_UNUSED_ARG(role);
+
+ /*
+ * Note: this function may be called when the media already exists
+ * (e.g. in reinvites, updates, etc).
+ */
+
+ if (pjsua_get_state() != PJSUA_STATE_RUNNING)
+ return PJ_EBUSY;
+
+ if (async) {
+ pj_pool_t *tmppool = (call->inv? call->inv->pool_prov:
+ call->async_call.dlg->pool);
+
+ status = pj_mutex_create_simple(tmppool, NULL, &call->med_ch_mutex);
+ if (status != PJ_SUCCESS)
+ return status;
+ }
+
+ if (call->inv && call->inv->state == PJSIP_INV_STATE_CONFIRMED)
+ reinit = PJ_TRUE;
+
+ PJ_LOG(4,(THIS_FILE, "Call %d: %sinitializing media..",
+ call_id, (reinit?"re-":"") ));
+
+ pj_log_push_indent();
+
+ /* Init provisional media state */
+ if (call->med_cnt == 0) {
+ /* New media session, just copy whole from call media state. */
+ pj_memcpy(call->media_prov, call->media, sizeof(call->media));
+ } else {
+ /* Clean up any unused transports. Note that when local SDP reoffer
+ * is rejected by remote, there may be any initialized transports that
+ * are not used by call media and currently there is no notification
+ * from PJSIP level regarding the reoffer rejection.
+ */
+ pjsua_media_prov_clean_up(call_id);
+
+ /* Updating media session, copy from call media state. */
+ pj_memcpy(call->media_prov, call->media,
+ sizeof(call->media[0]) * call->med_cnt);
+ }
+ call->med_prov_cnt = call->med_cnt;
+
+#if DISABLED_FOR_TICKET_1185
+ /* Return error if media transport has not been created yet
+ * (e.g. application is starting)
+ */
+ for (i=0; i<call->med_cnt; ++i) {
+ if (call->media[i].tp == NULL) {
+ status = PJ_EBUSY;
+ goto on_error;
+ }
+ }
+#endif
+
+ /* Get media count for each media type */
+ if (rem_sdp) {
+ sort_media(rem_sdp, &STR_AUDIO, acc->cfg.use_srtp,
+ maudidx, &maudcnt, &mtotaudcnt);
+ if (maudcnt==0) {
+ /* Expecting audio in the offer */
+ if (sip_err_code) *sip_err_code = PJSIP_SC_NOT_ACCEPTABLE_HERE;
+ status = PJSIP_ERRNO_FROM_SIP_STATUS(PJSIP_SC_NOT_ACCEPTABLE_HERE);
+ goto on_error;
+ }
+
+#if PJMEDIA_HAS_VIDEO
+ sort_media(rem_sdp, &STR_VIDEO, acc->cfg.use_srtp,
+ mvididx, &mvidcnt, &mtotvidcnt);
+#else
+ mvidcnt = mtotvidcnt = 0;
+ PJ_UNUSED_ARG(STR_VIDEO);
+#endif
+
+ /* Update media count only when remote add any media, this media count
+ * must never decrease. Also note that we shouldn't apply the media
+ * count setting (of the call setting) before the SDP negotiation.
+ */
+ if (call->med_prov_cnt < rem_sdp->media_count)
+ call->med_prov_cnt = PJ_MIN(rem_sdp->media_count,
+ PJSUA_MAX_CALL_MEDIA);
+
+ call->rem_offerer = PJ_TRUE;
+ call->rem_aud_cnt = maudcnt;
+ call->rem_vid_cnt = mvidcnt;
+
+ } else {
+
+ /* If call already established, calculate media count from current
+ * local active SDP and call setting. Otherwise, calculate media
+ * count from the call setting only.
+ */
+ if (reinit) {
+ const pjmedia_sdp_session *sdp;
+
+ status = pjmedia_sdp_neg_get_active_local(call->inv->neg, &sdp);
+ pj_assert(status == PJ_SUCCESS);
+
+ sort_media(sdp, &STR_AUDIO, acc->cfg.use_srtp,
+ maudidx, &maudcnt, &mtotaudcnt);
+ pj_assert(maudcnt > 0);
+
+ sort_media(sdp, &STR_VIDEO, acc->cfg.use_srtp,
+ mvididx, &mvidcnt, &mtotvidcnt);
+
+ /* Call setting may add or remove media. Adding media is done by
+ * enabling any disabled/port-zeroed media first, then adding new
+ * media whenever needed. Removing media is done by disabling
+ * media with the lowest 'quality'.
+ */
+
+ /* Check if we need to add new audio */
+ if (maudcnt < call->opt.aud_cnt &&
+ mtotaudcnt < call->opt.aud_cnt)
+ {
+ for (mi = 0; mi < call->opt.aud_cnt - mtotaudcnt; ++mi)
+ maudidx[maudcnt++] = (pj_uint8_t)call->med_prov_cnt++;
+
+ mtotaudcnt = call->opt.aud_cnt;
+ }
+ maudcnt = call->opt.aud_cnt;
+
+ /* Check if we need to add new video */
+ if (mvidcnt < call->opt.vid_cnt &&
+ mtotvidcnt < call->opt.vid_cnt)
+ {
+ for (mi = 0; mi < call->opt.vid_cnt - mtotvidcnt; ++mi)
+ mvididx[mvidcnt++] = (pj_uint8_t)call->med_prov_cnt++;
+
+ mtotvidcnt = call->opt.vid_cnt;
+ }
+ mvidcnt = call->opt.vid_cnt;
+
+ } else {
+
+ maudcnt = mtotaudcnt = call->opt.aud_cnt;
+ for (mi=0; mi<maudcnt; ++mi) {
+ maudidx[mi] = (pj_uint8_t)mi;
+ }
+ mvidcnt = mtotvidcnt = call->opt.vid_cnt;
+ for (mi=0; mi<mvidcnt; ++mi) {
+ mvididx[mi] = (pj_uint8_t)(maudcnt + mi);
+ }
+ call->med_prov_cnt = maudcnt + mvidcnt;
+
+ /* Need to publish supported media? */
+ if (call->opt.flag & PJSUA_CALL_INCLUDE_DISABLED_MEDIA) {
+ if (mtotaudcnt == 0) {
+ mtotaudcnt = 1;
+ maudidx[0] = (pj_uint8_t)call->med_prov_cnt++;
+ }
+#if PJMEDIA_HAS_VIDEO
+ if (mtotvidcnt == 0) {
+ mtotvidcnt = 1;
+ mvididx[0] = (pj_uint8_t)call->med_prov_cnt++;
+ }
+#endif
+ }
+ }
+
+ call->rem_offerer = PJ_FALSE;
+ }
+
+ if (call->med_prov_cnt == 0) {
+ /* Expecting at least one media */
+ if (sip_err_code) *sip_err_code = PJSIP_SC_NOT_ACCEPTABLE_HERE;
+ status = PJSIP_ERRNO_FROM_SIP_STATUS(PJSIP_SC_NOT_ACCEPTABLE_HERE);
+ goto on_error;
+ }
+
+ if (async) {
+ call->med_ch_cb = cb;
+ }
+
+ if (rem_sdp) {
+ call->async_call.rem_sdp =
+ pjmedia_sdp_session_clone(call->inv->pool_prov, rem_sdp);
+ } else {
+ call->async_call.rem_sdp = NULL;
+ }
+
+ call->async_call.pool_prov = tmp_pool;
+
+ /* Initialize each media line */
+ for (mi=0; mi < call->med_prov_cnt; ++mi) {
+ pjsua_call_media *call_med = &call->media_prov[mi];
+ pj_bool_t enabled = PJ_FALSE;
+ pjmedia_type media_type = PJMEDIA_TYPE_UNKNOWN;
+
+ if (pj_memchr(maudidx, mi, mtotaudcnt * sizeof(maudidx[0]))) {
+ media_type = PJMEDIA_TYPE_AUDIO;
+ if (call->opt.aud_cnt &&
+ pj_memchr(maudidx, mi, maudcnt * sizeof(maudidx[0])))
+ {
+ enabled = PJ_TRUE;
+ }
+ } else if (pj_memchr(mvididx, mi, mtotvidcnt * sizeof(mvididx[0]))) {
+ media_type = PJMEDIA_TYPE_VIDEO;
+ if (call->opt.vid_cnt &&
+ pj_memchr(mvididx, mi, mvidcnt * sizeof(mvididx[0])))
+ {
+ enabled = PJ_TRUE;
+ }
+ }
+
+ if (enabled) {
+ status = pjsua_call_media_init(call_med, media_type,
+ &acc->cfg.rtp_cfg,
+ security_level, sip_err_code,
+ async,
+ (async? &media_channel_init_cb:
+ NULL));
+ if (status == PJ_EPENDING) {
+ pending_med_tp = PJ_TRUE;
+ } else if (status != PJ_SUCCESS) {
+ if (pending_med_tp) {
+ /* Save failure information. */
+ call_med->tp_ready = status;
+ pj_bzero(&call->med_ch_info, sizeof(call->med_ch_info));
+ call->med_ch_info.status = status;
+ call->med_ch_info.state = call_med->tp_st;
+ call->med_ch_info.med_idx = call_med->idx;
+ if (sip_err_code)
+ call->med_ch_info.sip_err_code = *sip_err_code;
+
+ /* We will return failure in the callback later. */
+ return PJ_EPENDING;
+ }
+
+ pjsua_media_prov_clean_up(call_id);
+ goto on_error;
+ }
+ } else {
+ /* By convention, the media is disabled if transport is NULL
+ * or transport state is PJSUA_MED_TP_DISABLED.
+ */
+ if (call_med->tp) {
+ // Don't close transport here, as SDP negotiation has not been
+ // done and stream may be still active. Once SDP negotiation
+ // is done (channel_update() invoked), this transport will be
+ // closed there.
+ //pjmedia_transport_close(call_med->tp);
+ //call_med->tp = NULL;
+ pj_assert(call_med->tp_st == PJSUA_MED_TP_INIT ||
+ call_med->tp_st == PJSUA_MED_TP_RUNNING);
+ pjsua_set_media_tp_state(call_med, PJSUA_MED_TP_DISABLED);
+ }
+
+ /* Put media type just for info */
+ call_med->type = media_type;
+ }
+ }
+
+ call->audio_idx = maudidx[0];
+
+ PJ_LOG(4,(THIS_FILE, "Media index %d selected for audio call %d",
+ call->audio_idx, call->index));
+
+ if (pending_med_tp) {
+ /* We shouldn't use temporary pool anymore. */
+ call->async_call.pool_prov = NULL;
+ /* We have a pending media transport initialization. */
+ pj_log_pop_indent();
+ return PJ_EPENDING;
+ }
+
+ /* Media transport initialization completed immediately, so
+ * we don't need to call the callback.
+ */
+ call->med_ch_cb = NULL;
+
+ status = media_channel_init_cb(call_id, NULL);
+ if (status != PJ_SUCCESS && sip_err_code)
+ *sip_err_code = call->med_ch_info.sip_err_code;
+
+ pj_log_pop_indent();
+ return status;
+
+on_error:
+ if (call->med_ch_mutex) {
+ pj_mutex_destroy(call->med_ch_mutex);
+ call->med_ch_mutex = NULL;
+ }
+
+ pj_log_pop_indent();
+ return status;
+}
+
+
+/* Create SDP based on the current media channel. Note that, this function
+ * will not modify the media channel, so when receiving new offer or
+ * updating media count (via call setting), media channel must be reinit'd
+ * (using pjsua_media_channel_init()) first before calling this function.
+ */
+pj_status_t pjsua_media_channel_create_sdp(pjsua_call_id call_id,
+ pj_pool_t *pool,
+ const pjmedia_sdp_session *rem_sdp,
+ pjmedia_sdp_session **p_sdp,
+ int *sip_err_code)
+{
+ enum { MAX_MEDIA = PJSUA_MAX_CALL_MEDIA };
+ pjmedia_sdp_session *sdp;
+ pj_sockaddr origin;
+ pjsua_call *call = &pjsua_var.calls[call_id];
+ pjmedia_sdp_neg_state sdp_neg_state = PJMEDIA_SDP_NEG_STATE_NULL;
+ unsigned mi;
+ unsigned tot_bandw_tias = 0;
+ pj_status_t status;
+
+ if (pjsua_get_state() != PJSUA_STATE_RUNNING)
+ return PJ_EBUSY;
+
+#if 0
+ // This function should not really change the media channel.
+ if (rem_sdp) {
+ /* If this is a re-offer, let's re-initialize media as remote may
+ * add or remove media
+ */
+ if (call->inv && call->inv->state == PJSIP_INV_STATE_CONFIRMED) {
+ status = pjsua_media_channel_init(call_id, PJSIP_ROLE_UAS,
+ call->secure_level, pool,
+ rem_sdp, sip_err_code,
+ PJ_FALSE, NULL);
+ if (status != PJ_SUCCESS)
+ return status;
+ }
+ } else {
+ /* Audio is first in our offer, by convention */
+ // The audio_idx should not be changed here, as this function may be
+ // called in generating re-offer and the current active audio index
+ // can be anywhere.
+ //call->audio_idx = 0;
+ }
+#endif
+
+#if 0
+ // Since r3512, old-style hold should have got transport, created by
+ // pjsua_media_channel_init() in initial offer/answer or remote reoffer.
+ /* Create media if it's not created. This could happen when call is
+ * currently on-hold (with the old style hold)
+ */
+ if (call->media[call->audio_idx].tp == NULL) {
+ pjsip_role_e role;
+ role = (rem_sdp ? PJSIP_ROLE_UAS : PJSIP_ROLE_UAC);
+ status = pjsua_media_channel_init(call_id, role, call->secure_level,
+ pool, rem_sdp, sip_err_code);
+ if (status != PJ_SUCCESS)
+ return status;
+ }
+#endif
+
+ /* Get SDP negotiator state */
+ if (call->inv && call->inv->neg)
+ sdp_neg_state = pjmedia_sdp_neg_get_state(call->inv->neg);
+
+ /* Get one address to use in the origin field */
+ pj_bzero(&origin, sizeof(origin));
+ for (mi=0; mi<call->med_prov_cnt; ++mi) {
+ pjmedia_transport_info tpinfo;
+
+ if (call->media_prov[mi].tp == NULL)
+ continue;
+
+ pjmedia_transport_info_init(&tpinfo);
+ pjmedia_transport_get_info(call->media_prov[mi].tp, &tpinfo);
+ pj_sockaddr_cp(&origin, &tpinfo.sock_info.rtp_addr_name);
+ break;
+ }
+
+ /* Create the base (blank) SDP */
+ status = pjmedia_endpt_create_base_sdp(pjsua_var.med_endpt, pool, NULL,
+ &origin, &sdp);
+ if (status != PJ_SUCCESS)
+ return status;
+
+ /* Process each media line */
+ for (mi=0; mi<call->med_prov_cnt; ++mi) {
+ pjsua_call_media *call_med = &call->media_prov[mi];
+ pjmedia_sdp_media *m = NULL;
+ pjmedia_transport_info tpinfo;
+ unsigned i;
+
+ if (rem_sdp && mi >= rem_sdp->media_count) {
+ /* Remote might have removed some media lines. */
+ break;
+ }
+
+ if (call_med->tp == NULL || call_med->tp_st == PJSUA_MED_TP_DISABLED)
+ {
+ /*
+ * This media is disabled. Just create a valid SDP with zero
+ * port.
+ */
+ if (rem_sdp) {
+ /* Just clone the remote media and deactivate it */
+ m = pjmedia_sdp_media_clone_deactivate(pool,
+ rem_sdp->media[mi]);
+ } else {
+ m = PJ_POOL_ZALLOC_T(pool, pjmedia_sdp_media);
+ m->desc.transport = pj_str("RTP/AVP");
+ m->desc.fmt_count = 1;
+ m->conn = PJ_POOL_ZALLOC_T(pool, pjmedia_sdp_conn);
+ m->conn->net_type = pj_str("IN");
+ m->conn->addr_type = pj_str("IP4");
+ m->conn->addr = pj_str("127.0.0.1");
+
+ switch (call_med->type) {
+ case PJMEDIA_TYPE_AUDIO:
+ m->desc.media = pj_str("audio");
+ m->desc.fmt[0] = pj_str("0");
+ break;
+ case PJMEDIA_TYPE_VIDEO:
+ m->desc.media = pj_str("video");
+ m->desc.fmt[0] = pj_str("31");
+ break;
+ default:
+ /* This must be us generating re-offer, and some unknown
+ * media may exist, so just clone from active local SDP
+ * (and it should have been deactivated already).
+ */
+ pj_assert(call->inv && call->inv->neg &&
+ sdp_neg_state == PJMEDIA_SDP_NEG_STATE_DONE);
+ {
+ const pjmedia_sdp_session *s_;
+ pjmedia_sdp_neg_get_active_local(call->inv->neg, &s_);
+
+ pj_assert(mi < s_->media_count);
+ m = pjmedia_sdp_media_clone(pool, s_->media[mi]);
+ m->desc.port = 0;
+ }
+ break;
+ }
+ }
+
+ sdp->media[sdp->media_count++] = m;
+ continue;
+ }
+
+ /* Get transport address info */
+ pjmedia_transport_info_init(&tpinfo);
+ pjmedia_transport_get_info(call_med->tp, &tpinfo);
+
+ /* Ask pjmedia endpoint to create SDP media line */
+ switch (call_med->type) {
+ case PJMEDIA_TYPE_AUDIO:
+ status = pjmedia_endpt_create_audio_sdp(pjsua_var.med_endpt, pool,
+ &tpinfo.sock_info, 0, &m);
+ break;
+#if defined(PJMEDIA_HAS_VIDEO) && (PJMEDIA_HAS_VIDEO != 0)
+ case PJMEDIA_TYPE_VIDEO:
+ status = pjmedia_endpt_create_video_sdp(pjsua_var.med_endpt, pool,
+ &tpinfo.sock_info, 0, &m);
+ break;
+#endif
+ default:
+ pj_assert(!"Invalid call_med media type");
+ return PJ_EBUG;
+ }
+
+ if (status != PJ_SUCCESS)
+ return status;
+
+ sdp->media[sdp->media_count++] = m;
+
+ /* Give to transport */
+ status = pjmedia_transport_encode_sdp(call_med->tp, pool,
+ sdp, rem_sdp, mi);
+ if (status != PJ_SUCCESS) {
+ if (sip_err_code) *sip_err_code = PJSIP_SC_NOT_ACCEPTABLE;
+ return status;
+ }
+
+ /* Copy c= line of the first media to session level,
+ * if there's none.
+ */
+ if (sdp->conn == NULL) {
+ sdp->conn = pjmedia_sdp_conn_clone(pool, m->conn);
+ }
+
+
+ /* Find media bandwidth info */
+ for (i = 0; i < m->bandw_count; ++i) {
+ const pj_str_t STR_BANDW_MODIFIER_TIAS = { "TIAS", 4 };
+ if (!pj_stricmp(&m->bandw[i]->modifier, &STR_BANDW_MODIFIER_TIAS))
+ {
+ tot_bandw_tias += m->bandw[i]->value;
+ break;
+ }
+ }
+ }
+
+ /* Add NAT info in the SDP */
+ if (pjsua_var.ua_cfg.nat_type_in_sdp) {
+ pjmedia_sdp_attr *a;
+ pj_str_t value;
+ char nat_info[80];
+
+ value.ptr = nat_info;
+ if (pjsua_var.ua_cfg.nat_type_in_sdp == 1) {
+ value.slen = pj_ansi_snprintf(nat_info, sizeof(nat_info),
+ "%d", pjsua_var.nat_type);
+ } else {
+ const char *type_name = pj_stun_get_nat_name(pjsua_var.nat_type);
+ value.slen = pj_ansi_snprintf(nat_info, sizeof(nat_info),
+ "%d %s",
+ pjsua_var.nat_type,
+ type_name);
+ }
+
+ a = pjmedia_sdp_attr_create(pool, "X-nat", &value);
+
+ pjmedia_sdp_attr_add(&sdp->attr_count, sdp->attr, a);
+
+ }
+
+
+ /* Add bandwidth info in session level using bandwidth modifier "AS". */
+ if (tot_bandw_tias) {
+ unsigned bandw;
+ const pj_str_t STR_BANDW_MODIFIER_AS = { "AS", 2 };
+ pjmedia_sdp_bandw *b;
+
+ /* AS bandwidth = RTP bitrate + RTCP bitrate.
+ * RTP bitrate = payload bitrate (total TIAS) + overheads (~16kbps).
+ * RTCP bitrate = est. 5% of RTP bitrate.
+ * Note that AS bandwidth is in kbps.
+ */
+ bandw = tot_bandw_tias + 16000;
+ bandw += bandw * 5 / 100;
+ b = PJ_POOL_ALLOC_T(pool, pjmedia_sdp_bandw);
+ b->modifier = STR_BANDW_MODIFIER_AS;
+ b->value = bandw / 1000;
+ sdp->bandw[sdp->bandw_count++] = b;
+ }
+
+
+#if DISABLED_FOR_TICKET_1185 && defined(PJMEDIA_HAS_SRTP) && (PJMEDIA_HAS_SRTP != 0)
+ /* Check if SRTP is in optional mode and configured to use duplicated
+ * media, i.e: secured and unsecured version, in the SDP offer.
+ */
+ if (!rem_sdp &&
+ pjsua_var.acc[call->acc_id].cfg.use_srtp == PJMEDIA_SRTP_OPTIONAL &&
+ pjsua_var.acc[call->acc_id].cfg.srtp_optional_dup_offer)
+ {
+ unsigned i;
+
+ for (i = 0; i < sdp->media_count; ++i) {
+ pjmedia_sdp_media *m = sdp->media[i];
+
+ /* Check if this media is unsecured but has SDP "crypto"
+ * attribute.
+ */
+ if (pj_stricmp2(&m->desc.transport, "RTP/AVP") == 0 &&
+ pjmedia_sdp_media_find_attr2(m, "crypto", NULL) != NULL)
+ {
+ if (i == (unsigned)call->audio_idx &&
+ sdp_neg_state == PJMEDIA_SDP_NEG_STATE_DONE)
+ {
+ /* This is a session update, and peer has chosen the
+ * unsecured version, so let's make this unsecured too.
+ */
+ pjmedia_sdp_media_remove_all_attr(m, "crypto");
+ } else {
+ /* This is new offer, duplicate media so we'll have
+ * secured (with "RTP/SAVP" transport) and and unsecured
+ * versions.
+ */
+ pjmedia_sdp_media *new_m;
+
+ /* Duplicate this media and apply secured transport */
+ new_m = pjmedia_sdp_media_clone(pool, m);
+ pj_strdup2(pool, &new_m->desc.transport, "RTP/SAVP");
+
+ /* Remove the "crypto" attribute in the unsecured media */
+ pjmedia_sdp_media_remove_all_attr(m, "crypto");
+
+ /* Insert the new media before the unsecured media */
+ if (sdp->media_count < PJMEDIA_MAX_SDP_MEDIA) {
+ pj_array_insert(sdp->media, sizeof(new_m),
+ sdp->media_count, i, &new_m);
+ ++sdp->media_count;
+ ++i;
+ }
+ }
+ }
+ }
+ }
+#endif
+
+ call->rem_offerer = (rem_sdp != NULL);
+
+ /* Notify application */
+ if (pjsua_var.ua_cfg.cb.on_call_sdp_created) {
+ (*pjsua_var.ua_cfg.cb.on_call_sdp_created)(call_id, sdp,
+ pool, rem_sdp);
+ }
+
+ *p_sdp = sdp;
+ return PJ_SUCCESS;
+}
+
+
+static void stop_media_session(pjsua_call_id call_id)
+{
+ pjsua_call *call = &pjsua_var.calls[call_id];
+ unsigned mi;
+
+ pj_log_push_indent();
+
+ for (mi=0; mi<call->med_cnt; ++mi) {
+ pjsua_call_media *call_med = &call->media[mi];
+
+ if (call_med->type == PJMEDIA_TYPE_AUDIO) {
+ pjsua_aud_stop_stream(call_med);
+ }
+
+#if PJMEDIA_HAS_VIDEO
+ else if (call_med->type == PJMEDIA_TYPE_VIDEO) {
+ pjsua_vid_stop_stream(call_med);
+ }
+#endif
+
+ PJ_LOG(4,(THIS_FILE, "Media session call%02d:%d is destroyed",
+ call_id, mi));
+ call_med->prev_state = call_med->state;
+ call_med->state = PJSUA_CALL_MEDIA_NONE;
+
+ /* Try to sync recent changes to provisional media */
+ if (mi<call->med_prov_cnt && call->media_prov[mi].tp==call_med->tp)
+ {
+ pjsua_call_media *prov_med = &call->media_prov[mi];
+
+ /* Media state */
+ prov_med->prev_state = call_med->prev_state;
+ prov_med->state = call_med->state;
+
+ /* RTP seq/ts */
+ prov_med->rtp_tx_seq_ts_set = call_med->rtp_tx_seq_ts_set;
+ prov_med->rtp_tx_seq = call_med->rtp_tx_seq;
+ prov_med->rtp_tx_ts = call_med->rtp_tx_ts;
+
+ /* Stream */
+ if (call_med->type == PJMEDIA_TYPE_AUDIO) {
+ prov_med->strm.a.conf_slot = call_med->strm.a.conf_slot;
+ prov_med->strm.a.stream = call_med->strm.a.stream;
+ }
+#if PJMEDIA_HAS_VIDEO
+ else if (call_med->type == PJMEDIA_TYPE_VIDEO) {
+ prov_med->strm.v.cap_win_id = call_med->strm.v.cap_win_id;
+ prov_med->strm.v.rdr_win_id = call_med->strm.v.rdr_win_id;
+ prov_med->strm.v.stream = call_med->strm.v.stream;
+ }
+#endif
+ }
+ }
+
+ pj_log_pop_indent();
+}
+
+pj_status_t pjsua_media_channel_deinit(pjsua_call_id call_id)
+{
+ pjsua_call *call = &pjsua_var.calls[call_id];
+ unsigned mi;
+
+ for (mi=0; mi<call->med_cnt; ++mi) {
+ pjsua_call_media *call_med = &call->media[mi];
+
+ if (call_med->tp_st == PJSUA_MED_TP_CREATING) {
+ /* We will do the deinitialization after media transport
+ * creation is completed.
+ */
+ call->async_call.med_ch_deinit = PJ_TRUE;
+ return PJ_SUCCESS;
+ }
+ }
+
+ PJ_LOG(4,(THIS_FILE, "Call %d: deinitializing media..", call_id));
+ pj_log_push_indent();
+
+ stop_media_session(call_id);
+
+ /* Clean up media transports */
+ pjsua_media_prov_clean_up(call_id);
+ call->med_prov_cnt = 0;
+ for (mi=0; mi<call->med_cnt; ++mi) {
+ pjsua_call_media *call_med = &call->media[mi];
+
+ if (call_med->tp_st > PJSUA_MED_TP_IDLE) {
+ pjsua_set_media_tp_state(call_med, PJSUA_MED_TP_IDLE);
+ pjmedia_transport_media_stop(call_med->tp);
+ }
+
+ if (call_med->tp) {
+ pjsua_set_media_tp_state(call_med, PJSUA_MED_TP_NULL);
+ pjmedia_transport_close(call_med->tp);
+ call_med->tp = call_med->tp_orig = NULL;
+ }
+ call_med->tp_orig = NULL;
+ }
+
+ pj_log_pop_indent();
+
+ return PJ_SUCCESS;
+}
+
+
+pj_status_t pjsua_media_channel_update(pjsua_call_id call_id,
+ const pjmedia_sdp_session *local_sdp,
+ const pjmedia_sdp_session *remote_sdp)
+{
+ pjsua_call *call = &pjsua_var.calls[call_id];
+ pjsua_acc *acc = &pjsua_var.acc[call->acc_id];
+ pj_pool_t *tmp_pool = call->inv->pool_prov;
+ unsigned mi;
+ pj_bool_t got_media = PJ_FALSE;
+ pj_status_t status = PJ_SUCCESS;
+
+ const pj_str_t STR_AUDIO = { "audio", 5 };
+ const pj_str_t STR_VIDEO = { "video", 5 };
+ pj_uint8_t maudidx[PJSUA_MAX_CALL_MEDIA];
+ unsigned maudcnt = PJ_ARRAY_SIZE(maudidx);
+ unsigned mtotaudcnt = PJ_ARRAY_SIZE(maudidx);
+ pj_uint8_t mvididx[PJSUA_MAX_CALL_MEDIA];
+ unsigned mvidcnt = PJ_ARRAY_SIZE(mvididx);
+ unsigned mtotvidcnt = PJ_ARRAY_SIZE(mvididx);
+ pj_bool_t need_renego_sdp = PJ_FALSE;
+
+ if (pjsua_get_state() != PJSUA_STATE_RUNNING)
+ return PJ_EBUSY;
+
+ PJ_LOG(4,(THIS_FILE, "Call %d: updating media..", call_id));
+ pj_log_push_indent();
+
+ /* Destroy existing media session, if any. */
+ stop_media_session(call->index);
+
+ /* Call media count must be at least equal to SDP media. Note that
+ * it may not be equal when remote removed any SDP media line.
+ */
+ pj_assert(call->med_prov_cnt >= local_sdp->media_count);
+
+ /* Reset audio_idx first */
+ call->audio_idx = -1;
+
+ /* Sort audio/video based on "quality" */
+ sort_media(local_sdp, &STR_AUDIO, acc->cfg.use_srtp,
+ maudidx, &maudcnt, &mtotaudcnt);
+#if PJMEDIA_HAS_VIDEO
+ sort_media(local_sdp, &STR_VIDEO, acc->cfg.use_srtp,
+ mvididx, &mvidcnt, &mtotvidcnt);
+#else
+ PJ_UNUSED_ARG(STR_VIDEO);
+ mvidcnt = mtotvidcnt = 0;
+#endif
+
+ /* Applying media count limitation. Note that in generating SDP answer,
+ * no media count limitation applied, as we didn't know yet which media
+ * would pass the SDP negotiation.
+ */
+ if (maudcnt > call->opt.aud_cnt || mvidcnt > call->opt.vid_cnt)
+ {
+ pjmedia_sdp_session *local_sdp2;
+
+ maudcnt = PJ_MIN(maudcnt, call->opt.aud_cnt);
+ mvidcnt = PJ_MIN(mvidcnt, call->opt.vid_cnt);
+ local_sdp2 = pjmedia_sdp_session_clone(tmp_pool, local_sdp);
+
+ for (mi=0; mi < local_sdp2->media_count; ++mi) {
+ pjmedia_sdp_media *m = local_sdp2->media[mi];
+
+ if (m->desc.port == 0 ||
+ pj_memchr(maudidx, mi, maudcnt*sizeof(maudidx[0])) ||
+ pj_memchr(mvididx, mi, mvidcnt*sizeof(mvididx[0])))
+ {
+ continue;
+ }
+
+ /* Deactivate this media */
+ pjmedia_sdp_media_deactivate(tmp_pool, m);
+ }
+
+ local_sdp = local_sdp2;
+ need_renego_sdp = PJ_TRUE;
+ }
+
+ /* Process each media stream */
+ for (mi=0; mi < call->med_prov_cnt; ++mi) {
+ pjsua_call_media *call_med = &call->media_prov[mi];
+
+ if (mi >= local_sdp->media_count ||
+ mi >= remote_sdp->media_count)
+ {
+ /* This may happen when remote removed any SDP media lines in
+ * its re-offer.
+ */
+ if (call_med->tp) {
+ /* Close the media transport */
+ pjsua_set_media_tp_state(call_med, PJSUA_MED_TP_NULL);
+ pjmedia_transport_close(call_med->tp);
+ call_med->tp = call_med->tp_orig = NULL;
+ }
+ continue;
+#if 0
+ /* Something is wrong */
+ PJ_LOG(1,(THIS_FILE, "Error updating media for call %d: "
+ "invalid media index %d in SDP", call_id, mi));
+ status = PJMEDIA_SDP_EINSDP;
+ goto on_error;
+#endif
+ }
+
+ if (call_med->type==PJMEDIA_TYPE_AUDIO) {
+ pjmedia_stream_info the_si, *si = &the_si;
+
+ status = pjmedia_stream_info_from_sdp(si, tmp_pool, pjsua_var.med_endpt,
+ local_sdp, remote_sdp, mi);
+ if (status != PJ_SUCCESS) {
+ PJ_PERROR(1,(THIS_FILE, status,
+ "pjmedia_stream_info_from_sdp() failed "
+ "for call_id %d media %d",
+ call_id, mi));
+ continue;
+ }
+
+ /* Check if no media is active */
+ if (si->dir == PJMEDIA_DIR_NONE) {
+ /* Update call media state and direction */
+ call_med->state = PJSUA_CALL_MEDIA_NONE;
+ call_med->dir = PJMEDIA_DIR_NONE;
+
+ } else {
+ pjmedia_transport_info tp_info;
+
+ /* Start/restart media transport based on info in SDP */
+ status = pjmedia_transport_media_start(call_med->tp,
+ tmp_pool, local_sdp,
+ remote_sdp, mi);
+ if (status != PJ_SUCCESS) {
+ PJ_PERROR(1,(THIS_FILE, status,
+ "pjmedia_transport_media_start() failed "
+ "for call_id %d media %d",
+ call_id, mi));
+ continue;
+ }
+
+ pjsua_set_media_tp_state(call_med, PJSUA_MED_TP_RUNNING);
+
+ /* Get remote SRTP usage policy */
+ pjmedia_transport_info_init(&tp_info);
+ pjmedia_transport_get_info(call_med->tp, &tp_info);
+ if (tp_info.specific_info_cnt > 0) {
+ unsigned i;
+ for (i = 0; i < tp_info.specific_info_cnt; ++i) {
+ if (tp_info.spc_info[i].type == PJMEDIA_TRANSPORT_TYPE_SRTP)
+ {
+ pjmedia_srtp_info *srtp_info =
+ (pjmedia_srtp_info*) tp_info.spc_info[i].buffer;
+
+ call_med->rem_srtp_use = srtp_info->peer_use;
+ break;
+ }
+ }
+ }
+
+ /* Call media direction */
+ call_med->dir = si->dir;
+
+ /* Call media state */
+ if (call->local_hold)
+ call_med->state = PJSUA_CALL_MEDIA_LOCAL_HOLD;
+ else if (call_med->dir == PJMEDIA_DIR_DECODING)
+ call_med->state = PJSUA_CALL_MEDIA_REMOTE_HOLD;
+ else
+ call_med->state = PJSUA_CALL_MEDIA_ACTIVE;
+ }
+
+ /* Call implementation */
+ status = pjsua_aud_channel_update(call_med, tmp_pool, si,
+ local_sdp, remote_sdp);
+ if (status != PJ_SUCCESS) {
+ PJ_PERROR(1,(THIS_FILE, status,
+ "pjsua_aud_channel_update() failed "
+ "for call_id %d media %d",
+ call_id, mi));
+ continue;
+ }
+
+ /* Print info. */
+ if (status == PJ_SUCCESS) {
+ char info[80];
+ int info_len = 0;
+ int len;
+ const char *dir;
+
+ switch (si->dir) {
+ case PJMEDIA_DIR_NONE:
+ dir = "inactive";
+ break;
+ case PJMEDIA_DIR_ENCODING:
+ dir = "sendonly";
+ break;
+ case PJMEDIA_DIR_DECODING:
+ dir = "recvonly";
+ break;
+ case PJMEDIA_DIR_ENCODING_DECODING:
+ dir = "sendrecv";
+ break;
+ default:
+ dir = "unknown";
+ break;
+ }
+ len = pj_ansi_sprintf( info+info_len,
+ ", stream #%d: %.*s (%s)", mi,
+ (int)si->fmt.encoding_name.slen,
+ si->fmt.encoding_name.ptr,
+ dir);
+ if (len > 0)
+ info_len += len;
+ PJ_LOG(4,(THIS_FILE,"Audio updated%s", info));
+ }
+
+
+ if (call->audio_idx==-1 && status==PJ_SUCCESS &&
+ si->dir != PJMEDIA_DIR_NONE)
+ {
+ call->audio_idx = mi;
+ }
+
+#if defined(PJMEDIA_HAS_VIDEO) && (PJMEDIA_HAS_VIDEO != 0)
+ } else if (call_med->type==PJMEDIA_TYPE_VIDEO) {
+ pjmedia_vid_stream_info the_si, *si = &the_si;
+
+ status = pjmedia_vid_stream_info_from_sdp(si, tmp_pool, pjsua_var.med_endpt,
+ local_sdp, remote_sdp, mi);
+ if (status != PJ_SUCCESS) {
+ PJ_PERROR(1,(THIS_FILE, status,
+ "pjmedia_vid_stream_info_from_sdp() failed "
+ "for call_id %d media %d",
+ call_id, mi));
+ continue;
+ }
+
+ /* Check if no media is active */
+ if (si->dir == PJMEDIA_DIR_NONE) {
+ /* Update call media state and direction */
+ call_med->state = PJSUA_CALL_MEDIA_NONE;
+ call_med->dir = PJMEDIA_DIR_NONE;
+
+ } else {
+ pjmedia_transport_info tp_info;
+
+ /* Start/restart media transport */
+ status = pjmedia_transport_media_start(call_med->tp,
+ tmp_pool, local_sdp,
+ remote_sdp, mi);
+ if (status != PJ_SUCCESS) {
+ PJ_PERROR(1,(THIS_FILE, status,
+ "pjmedia_transport_media_start() failed "
+ "for call_id %d media %d",
+ call_id, mi));
+ continue;
+ }
+
+ pjsua_set_media_tp_state(call_med, PJSUA_MED_TP_RUNNING);
+
+ /* Get remote SRTP usage policy */
+ pjmedia_transport_info_init(&tp_info);
+ pjmedia_transport_get_info(call_med->tp, &tp_info);
+ if (tp_info.specific_info_cnt > 0) {
+ unsigned i;
+ for (i = 0; i < tp_info.specific_info_cnt; ++i) {
+ if (tp_info.spc_info[i].type ==
+ PJMEDIA_TRANSPORT_TYPE_SRTP)
+ {
+ pjmedia_srtp_info *sri;
+ sri=(pjmedia_srtp_info*)tp_info.spc_info[i].buffer;
+ call_med->rem_srtp_use = sri->peer_use;
+ break;
+ }
+ }
+ }
+
+ /* Call media direction */
+ call_med->dir = si->dir;
+
+ /* Call media state */
+ if (call->local_hold)
+ call_med->state = PJSUA_CALL_MEDIA_LOCAL_HOLD;
+ else if (call_med->dir == PJMEDIA_DIR_DECODING)
+ call_med->state = PJSUA_CALL_MEDIA_REMOTE_HOLD;
+ else
+ call_med->state = PJSUA_CALL_MEDIA_ACTIVE;
+ }
+
+ status = pjsua_vid_channel_update(call_med, tmp_pool, si,
+ local_sdp, remote_sdp);
+ if (status != PJ_SUCCESS) {
+ PJ_PERROR(1,(THIS_FILE, status,
+ "pjsua_vid_channel_update() failed "
+ "for call_id %d media %d",
+ call_id, mi));
+ continue;
+ }
+
+ /* Print info. */
+ {
+ char info[80];
+ int info_len = 0;
+ int len;
+ const char *dir;
+
+ switch (si->dir) {
+ case PJMEDIA_DIR_NONE:
+ dir = "inactive";
+ break;
+ case PJMEDIA_DIR_ENCODING:
+ dir = "sendonly";
+ break;
+ case PJMEDIA_DIR_DECODING:
+ dir = "recvonly";
+ break;
+ case PJMEDIA_DIR_ENCODING_DECODING:
+ dir = "sendrecv";
+ break;
+ default:
+ dir = "unknown";
+ break;
+ }
+ len = pj_ansi_sprintf( info+info_len,
+ ", stream #%d: %.*s (%s)", mi,
+ (int)si->codec_info.encoding_name.slen,
+ si->codec_info.encoding_name.ptr,
+ dir);
+ if (len > 0)
+ info_len += len;
+ PJ_LOG(4,(THIS_FILE,"Video updated%s", info));
+ }
+
+#endif
+ } else {
+ status = PJMEDIA_EINVALIMEDIATYPE;
+ }
+
+ /* Close the transport of deactivated media, need this here as media
+ * can be deactivated by the SDP negotiation and the max media count
+ * (account) setting.
+ */
+ if (local_sdp->media[mi]->desc.port==0 && call_med->tp) {
+ pjsua_set_media_tp_state(call_med, PJSUA_MED_TP_NULL);
+ pjmedia_transport_close(call_med->tp);
+ call_med->tp = call_med->tp_orig = NULL;
+ }
+
+ if (status != PJ_SUCCESS) {
+ PJ_PERROR(1,(THIS_FILE, status, "Error updating media call%02d:%d",
+ call_id, mi));
+ } else {
+ got_media = PJ_TRUE;
+ }
+ }
+
+ /* Update call media from provisional media */
+ call->med_cnt = call->med_prov_cnt;
+ pj_memcpy(call->media, call->media_prov,
+ sizeof(call->media_prov[0]) * call->med_prov_cnt);
+
+ /* Perform SDP re-negotiation if needed. */
+ if (got_media && need_renego_sdp) {
+ pjmedia_sdp_neg *neg = call->inv->neg;
+
+ /* This should only happen when we are the answerer. */
+ PJ_ASSERT_RETURN(neg && !pjmedia_sdp_neg_was_answer_remote(neg),
+ PJMEDIA_SDPNEG_EINSTATE);
+
+ status = pjmedia_sdp_neg_set_remote_offer(tmp_pool, neg, remote_sdp);
+ if (status != PJ_SUCCESS)
+ goto on_error;
+
+ status = pjmedia_sdp_neg_set_local_answer(tmp_pool, neg, local_sdp);
+ if (status != PJ_SUCCESS)
+ goto on_error;
+
+ status = pjmedia_sdp_neg_negotiate(tmp_pool, neg, 0);
+ if (status != PJ_SUCCESS)
+ goto on_error;
+ }
+
+ pj_log_pop_indent();
+ return (got_media? PJ_SUCCESS : PJMEDIA_SDPNEG_ENOMEDIA);
+
+on_error:
+ pj_log_pop_indent();
+ return status;
+}
+
+/*****************************************************************************
+ * Codecs.
+ */
+
+/*
+ * Enum all supported codecs in the system.
+ */
+PJ_DEF(pj_status_t) pjsua_enum_codecs( pjsua_codec_info id[],
+ unsigned *p_count )
+{
+ pjmedia_codec_mgr *codec_mgr;
+ pjmedia_codec_info info[32];
+ unsigned i, count, prio[32];
+ pj_status_t status;
+
+ codec_mgr = pjmedia_endpt_get_codec_mgr(pjsua_var.med_endpt);
+ count = PJ_ARRAY_SIZE(info);
+ status = pjmedia_codec_mgr_enum_codecs( codec_mgr, &count, info, prio);
+ if (status != PJ_SUCCESS) {
+ *p_count = 0;
+ return status;
+ }
+
+ if (count > *p_count) count = *p_count;
+
+ for (i=0; i<count; ++i) {
+ pj_bzero(&id[i], sizeof(pjsua_codec_info));
+
+ pjmedia_codec_info_to_id(&info[i], id[i].buf_, sizeof(id[i].buf_));
+ id[i].codec_id = pj_str(id[i].buf_);
+ id[i].priority = (pj_uint8_t) prio[i];
+ }
+
+ *p_count = count;
+
+ return PJ_SUCCESS;
+}
+
+
+/*
+ * Change codec priority.
+ */
+PJ_DEF(pj_status_t) pjsua_codec_set_priority( const pj_str_t *codec_id,
+ pj_uint8_t priority )
+{
+ const pj_str_t all = { NULL, 0 };
+ pjmedia_codec_mgr *codec_mgr;
+
+ codec_mgr = pjmedia_endpt_get_codec_mgr(pjsua_var.med_endpt);
+
+ if (codec_id->slen==1 && *codec_id->ptr=='*')
+ codec_id = &all;
+
+ return pjmedia_codec_mgr_set_codec_priority(codec_mgr, codec_id,
+ priority);
+}
+
+
+/*
+ * Get codec parameters.
+ */
+PJ_DEF(pj_status_t) pjsua_codec_get_param( const pj_str_t *codec_id,
+ pjmedia_codec_param *param )
+{
+ const pj_str_t all = { NULL, 0 };
+ const pjmedia_codec_info *info;
+ pjmedia_codec_mgr *codec_mgr;
+ unsigned count = 1;
+ pj_status_t status;
+
+ codec_mgr = pjmedia_endpt_get_codec_mgr(pjsua_var.med_endpt);
+
+ if (codec_id->slen==1 && *codec_id->ptr=='*')
+ codec_id = &all;
+
+ status = pjmedia_codec_mgr_find_codecs_by_id(codec_mgr, codec_id,
+ &count, &info, NULL);
+ if (status != PJ_SUCCESS)
+ return status;
+
+ if (count != 1)
+ return (count > 1? PJ_ETOOMANY : PJ_ENOTFOUND);
+
+ status = pjmedia_codec_mgr_get_default_param( codec_mgr, info, param);
+ return status;
+}
+
+
+/*
+ * Set codec parameters.
+ */
+PJ_DEF(pj_status_t) pjsua_codec_set_param( const pj_str_t *codec_id,
+ const pjmedia_codec_param *param)
+{
+ const pjmedia_codec_info *info[2];
+ pjmedia_codec_mgr *codec_mgr;
+ unsigned count = 2;
+ pj_status_t status;
+
+ codec_mgr = pjmedia_endpt_get_codec_mgr(pjsua_var.med_endpt);
+
+ status = pjmedia_codec_mgr_find_codecs_by_id(codec_mgr, codec_id,
+ &count, info, NULL);
+ if (status != PJ_SUCCESS)
+ return status;
+
+ /* Codec ID should be specific, except for G.722.1 */
+ if (count > 1 &&
+ pj_strnicmp2(codec_id, "G7221/16", 8) != 0 &&
+ pj_strnicmp2(codec_id, "G7221/32", 8) != 0)
+ {
+ pj_assert(!"Codec ID is not specific");
+ return PJ_ETOOMANY;
+ }
+
+ status = pjmedia_codec_mgr_set_default_param(codec_mgr, info[0], param);
+ return status;
+}
+
+
+pj_status_t pjsua_media_apply_xml_control(pjsua_call_id call_id,
+ const pj_str_t *xml_st)
+{
+#if PJMEDIA_HAS_VIDEO
+ pjsua_call *call = &pjsua_var.calls[call_id];
+ const pj_str_t PICT_FAST_UPDATE = {"picture_fast_update", 19};
+
+ if (pj_strstr(xml_st, &PICT_FAST_UPDATE)) {
+ unsigned i;
+
+ PJ_LOG(4,(THIS_FILE, "Received keyframe request via SIP INFO"));
+
+ for (i = 0; i < call->med_cnt; ++i) {
+ pjsua_call_media *cm = &call->media[i];
+ if (cm->type != PJMEDIA_TYPE_VIDEO || !cm->strm.v.stream)
+ continue;
+
+ pjmedia_vid_stream_send_keyframe(cm->strm.v.stream);
+ }
+
+ return PJ_SUCCESS;
+ }
+#endif
+
+ /* Just to avoid compiler warning of unused var */
+ PJ_UNUSED_ARG(call_id);
+ PJ_UNUSED_ARG(xml_st);
+
+ return PJ_ENOTSUP;
+}
+
diff --git a/pjsip/src/pjsua-lib/pjsua_pres.c b/pjsip/src/pjsua-lib/pjsua_pres.c
new file mode 100644
index 0000000..4551c91
--- /dev/null
+++ b/pjsip/src/pjsua-lib/pjsua_pres.c
@@ -0,0 +1,2402 @@
+/* $Id: pjsua_pres.c 4186 2012-06-29 01:37:50Z ming $ */
+/*
+ * Copyright (C) 2008-2011 Teluu Inc. (http://www.teluu.com)
+ * Copyright (C) 2003-2008 Benny Prijono <benny@prijono.org>
+ *
+ * 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
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ */
+#include <pjsua-lib/pjsua.h>
+#include <pjsua-lib/pjsua_internal.h>
+
+
+#define THIS_FILE "pjsua_pres.c"
+
+
+static void subscribe_buddy_presence(pjsua_buddy_id buddy_id);
+static void unsubscribe_buddy_presence(pjsua_buddy_id buddy_id);
+
+
+/*
+ * Find buddy.
+ */
+static pjsua_buddy_id find_buddy(const pjsip_uri *uri)
+{
+ const pjsip_sip_uri *sip_uri;
+ unsigned i;
+
+ uri = (const pjsip_uri*) pjsip_uri_get_uri((pjsip_uri*)uri);
+
+ if (!PJSIP_URI_SCHEME_IS_SIP(uri) && !PJSIP_URI_SCHEME_IS_SIPS(uri))
+ return PJSUA_INVALID_ID;
+
+ sip_uri = (const pjsip_sip_uri*) uri;
+
+ for (i=0; i<PJ_ARRAY_SIZE(pjsua_var.buddy); ++i) {
+ const pjsua_buddy *b = &pjsua_var.buddy[i];
+
+ if (!pjsua_buddy_is_valid(i))
+ continue;
+
+ if (pj_stricmp(&sip_uri->user, &b->name)==0 &&
+ pj_stricmp(&sip_uri->host, &b->host)==0 &&
+ (sip_uri->port==(int)b->port || (sip_uri->port==0 && b->port==5060)))
+ {
+ /* Match */
+ return i;
+ }
+ }
+
+ return PJSUA_INVALID_ID;
+}
+
+#define LOCK_DIALOG 1
+#define LOCK_PJSUA 2
+#define LOCK_ALL (LOCK_DIALOG | LOCK_PJSUA)
+
+/* Buddy lock object */
+struct buddy_lock
+{
+ pjsua_buddy *buddy;
+ pjsip_dialog *dlg;
+ pj_uint8_t flag;
+};
+
+/* Acquire lock to the specified buddy_id */
+pj_status_t lock_buddy(const char *title,
+ pjsua_buddy_id buddy_id,
+ struct buddy_lock *lck,
+ unsigned _unused_)
+{
+ enum { MAX_RETRY=50 };
+ pj_bool_t has_pjsua_lock = PJ_FALSE;
+ unsigned retry;
+
+ PJ_UNUSED_ARG(_unused_);
+
+ pj_bzero(lck, sizeof(*lck));
+
+ for (retry=0; retry<MAX_RETRY; ++retry) {
+
+ if (PJSUA_TRY_LOCK() != PJ_SUCCESS) {
+ pj_thread_sleep(retry/10);
+ continue;
+ }
+
+ has_pjsua_lock = PJ_TRUE;
+ lck->flag = LOCK_PJSUA;
+ lck->buddy = &pjsua_var.buddy[buddy_id];
+
+ if (lck->buddy->dlg == NULL)
+ return PJ_SUCCESS;
+
+ if (pjsip_dlg_try_inc_lock(lck->buddy->dlg) != PJ_SUCCESS) {
+ lck->flag = 0;
+ lck->buddy = NULL;
+ has_pjsua_lock = PJ_FALSE;
+ PJSUA_UNLOCK();
+ pj_thread_sleep(retry/10);
+ continue;
+ }
+
+ lck->dlg = lck->buddy->dlg;
+ lck->flag = LOCK_DIALOG;
+ PJSUA_UNLOCK();
+
+ break;
+ }
+
+ if (lck->flag == 0) {
+ if (has_pjsua_lock == PJ_FALSE)
+ PJ_LOG(1,(THIS_FILE, "Timed-out trying to acquire PJSUA mutex "
+ "(possibly system has deadlocked) in %s",
+ title));
+ else
+ PJ_LOG(1,(THIS_FILE, "Timed-out trying to acquire dialog mutex "
+ "(possibly system has deadlocked) in %s",
+ title));
+ return PJ_ETIMEDOUT;
+ }
+
+ return PJ_SUCCESS;
+}
+
+/* Release buddy lock */
+static void unlock_buddy(struct buddy_lock *lck)
+{
+ if (lck->flag & LOCK_DIALOG)
+ pjsip_dlg_dec_lock(lck->dlg);
+
+ if (lck->flag & LOCK_PJSUA)
+ PJSUA_UNLOCK();
+}
+
+
+/*
+ * Get total number of buddies.
+ */
+PJ_DEF(unsigned) pjsua_get_buddy_count(void)
+{
+ return pjsua_var.buddy_cnt;
+}
+
+
+/*
+ * Find buddy.
+ */
+PJ_DEF(pjsua_buddy_id) pjsua_buddy_find(const pj_str_t *uri_str)
+{
+ pj_str_t input;
+ pj_pool_t *pool;
+ pjsip_uri *uri;
+ pjsua_buddy_id buddy_id;
+
+ pool = pjsua_pool_create("buddyfind", 512, 512);
+ pj_strdup_with_null(pool, &input, uri_str);
+
+ uri = pjsip_parse_uri(pool, input.ptr, input.slen, 0);
+ if (!uri)
+ buddy_id = PJSUA_INVALID_ID;
+ else {
+ PJSUA_LOCK();
+ buddy_id = find_buddy(uri);
+ PJSUA_UNLOCK();
+ }
+
+ pj_pool_release(pool);
+
+ return buddy_id;
+}
+
+
+/*
+ * Check if buddy ID is valid.
+ */
+PJ_DEF(pj_bool_t) pjsua_buddy_is_valid(pjsua_buddy_id buddy_id)
+{
+ return buddy_id>=0 && buddy_id<(int)PJ_ARRAY_SIZE(pjsua_var.buddy) &&
+ pjsua_var.buddy[buddy_id].uri.slen != 0;
+}
+
+
+/*
+ * Enum buddy IDs.
+ */
+PJ_DEF(pj_status_t) pjsua_enum_buddies( pjsua_buddy_id ids[],
+ unsigned *count)
+{
+ unsigned i, c;
+
+ PJ_ASSERT_RETURN(ids && count, PJ_EINVAL);
+
+ PJSUA_LOCK();
+
+ for (i=0, c=0; c<*count && i<PJ_ARRAY_SIZE(pjsua_var.buddy); ++i) {
+ if (!pjsua_var.buddy[i].uri.slen)
+ continue;
+ ids[c] = i;
+ ++c;
+ }
+
+ *count = c;
+
+ PJSUA_UNLOCK();
+
+ return PJ_SUCCESS;
+}
+
+
+/*
+ * Get detailed buddy info.
+ */
+PJ_DEF(pj_status_t) pjsua_buddy_get_info( pjsua_buddy_id buddy_id,
+ pjsua_buddy_info *info)
+{
+ unsigned total=0;
+ struct buddy_lock lck;
+ pjsua_buddy *buddy;
+ pj_status_t status;
+
+ PJ_ASSERT_RETURN(pjsua_buddy_is_valid(buddy_id), PJ_EINVAL);
+
+ pj_bzero(info, sizeof(pjsua_buddy_info));
+
+ status = lock_buddy("pjsua_buddy_get_info()", buddy_id, &lck, 0);
+ if (status != PJ_SUCCESS)
+ return status;
+
+ buddy = lck.buddy;
+ info->id = buddy->index;
+ if (pjsua_var.buddy[buddy_id].uri.slen == 0) {
+ unlock_buddy(&lck);
+ return PJ_SUCCESS;
+ }
+
+ /* uri */
+ info->uri.ptr = info->buf_ + total;
+ pj_strncpy(&info->uri, &buddy->uri, sizeof(info->buf_)-total);
+ total += info->uri.slen;
+
+ /* contact */
+ info->contact.ptr = info->buf_ + total;
+ pj_strncpy(&info->contact, &buddy->contact, sizeof(info->buf_)-total);
+ total += info->contact.slen;
+
+ /* Presence status */
+ pj_memcpy(&info->pres_status, &buddy->status, sizeof(pjsip_pres_status));
+
+ /* status and status text */
+ if (buddy->sub == NULL || buddy->status.info_cnt==0) {
+ info->status = PJSUA_BUDDY_STATUS_UNKNOWN;
+ info->status_text = pj_str("?");
+ } else if (pjsua_var.buddy[buddy_id].status.info[0].basic_open) {
+ info->status = PJSUA_BUDDY_STATUS_ONLINE;
+
+ /* copy RPID information */
+ info->rpid = buddy->status.info[0].rpid;
+
+ if (info->rpid.note.slen)
+ info->status_text = info->rpid.note;
+ else
+ info->status_text = pj_str("Online");
+
+ } else {
+ info->status = PJSUA_BUDDY_STATUS_OFFLINE;
+ info->rpid = buddy->status.info[0].rpid;
+
+ if (info->rpid.note.slen)
+ info->status_text = info->rpid.note;
+ else
+ info->status_text = pj_str("Offline");
+ }
+
+ /* monitor pres */
+ info->monitor_pres = buddy->monitor;
+
+ /* subscription state and termination reason */
+ info->sub_term_code = buddy->term_code;
+ if (buddy->sub) {
+ info->sub_state = pjsip_evsub_get_state(buddy->sub);
+ info->sub_state_name = pjsip_evsub_get_state_name(buddy->sub);
+ if (info->sub_state == PJSIP_EVSUB_STATE_TERMINATED &&
+ total < sizeof(info->buf_))
+ {
+ info->sub_term_reason.ptr = info->buf_ + total;
+ pj_strncpy(&info->sub_term_reason,
+ pjsip_evsub_get_termination_reason(buddy->sub),
+ sizeof(info->buf_) - total);
+ total += info->sub_term_reason.slen;
+ } else {
+ info->sub_term_reason = pj_str("");
+ }
+ } else if (total < sizeof(info->buf_)) {
+ info->sub_state_name = "NULL";
+ info->sub_term_reason.ptr = info->buf_ + total;
+ pj_strncpy(&info->sub_term_reason, &buddy->term_reason,
+ sizeof(info->buf_) - total);
+ total += info->sub_term_reason.slen;
+ } else {
+ info->sub_state_name = "NULL";
+ info->sub_term_reason = pj_str("");
+ }
+
+ unlock_buddy(&lck);
+ return PJ_SUCCESS;
+}
+
+/*
+ * Set the user data associated with the buddy object.
+ */
+PJ_DEF(pj_status_t) pjsua_buddy_set_user_data( pjsua_buddy_id buddy_id,
+ void *user_data)
+{
+ struct buddy_lock lck;
+ pj_status_t status;
+
+ PJ_ASSERT_RETURN(pjsua_buddy_is_valid(buddy_id), PJ_EINVAL);
+
+ status = lock_buddy("pjsua_buddy_set_user_data()", buddy_id, &lck, 0);
+ if (status != PJ_SUCCESS)
+ return status;
+
+ pjsua_var.buddy[buddy_id].user_data = user_data;
+
+ unlock_buddy(&lck);
+
+ return PJ_SUCCESS;
+}
+
+
+/*
+ * Get the user data associated with the budy object.
+ */
+PJ_DEF(void*) pjsua_buddy_get_user_data(pjsua_buddy_id buddy_id)
+{
+ struct buddy_lock lck;
+ pj_status_t status;
+ void *user_data;
+
+ PJ_ASSERT_RETURN(pjsua_buddy_is_valid(buddy_id), NULL);
+
+ status = lock_buddy("pjsua_buddy_get_user_data()", buddy_id, &lck, 0);
+ if (status != PJ_SUCCESS)
+ return NULL;
+
+ user_data = pjsua_var.buddy[buddy_id].user_data;
+
+ unlock_buddy(&lck);
+
+ return user_data;
+}
+
+
+/*
+ * Reset buddy descriptor.
+ */
+static void reset_buddy(pjsua_buddy_id id)
+{
+ pj_pool_t *pool = pjsua_var.buddy[id].pool;
+ pj_bzero(&pjsua_var.buddy[id], sizeof(pjsua_var.buddy[id]));
+ pjsua_var.buddy[id].pool = pool;
+ pjsua_var.buddy[id].index = id;
+}
+
+
+/*
+ * Add new buddy.
+ */
+PJ_DEF(pj_status_t) pjsua_buddy_add( const pjsua_buddy_config *cfg,
+ pjsua_buddy_id *p_buddy_id)
+{
+ pjsip_name_addr *url;
+ pjsua_buddy *buddy;
+ pjsip_sip_uri *sip_uri;
+ int index;
+ pj_str_t tmp;
+
+ PJ_ASSERT_RETURN(pjsua_var.buddy_cnt <=
+ PJ_ARRAY_SIZE(pjsua_var.buddy),
+ PJ_ETOOMANY);
+
+ PJ_LOG(4,(THIS_FILE, "Adding buddy: %.*s",
+ (int)cfg->uri.slen, cfg->uri.ptr));
+ pj_log_push_indent();
+
+ PJSUA_LOCK();
+
+ /* Find empty slot */
+ for (index=0; index<(int)PJ_ARRAY_SIZE(pjsua_var.buddy); ++index) {
+ if (pjsua_var.buddy[index].uri.slen == 0)
+ break;
+ }
+
+ /* Expect to find an empty slot */
+ if (index == PJ_ARRAY_SIZE(pjsua_var.buddy)) {
+ PJSUA_UNLOCK();
+ /* This shouldn't happen */
+ pj_assert(!"index < PJ_ARRAY_SIZE(pjsua_var.buddy)");
+ pj_log_pop_indent();
+ return PJ_ETOOMANY;
+ }
+
+ buddy = &pjsua_var.buddy[index];
+
+ /* Create pool for this buddy */
+ if (buddy->pool) {
+ pj_pool_reset(buddy->pool);
+ } else {
+ char name[PJ_MAX_OBJ_NAME];
+ pj_ansi_snprintf(name, sizeof(name), "buddy%03d", index);
+ buddy->pool = pjsua_pool_create(name, 512, 256);
+ }
+
+ /* Init buffers for presence subscription status */
+ buddy->term_reason.ptr = (char*)
+ pj_pool_alloc(buddy->pool,
+ PJSUA_BUDDY_SUB_TERM_REASON_LEN);
+
+ /* Get name and display name for buddy */
+ pj_strdup_with_null(buddy->pool, &tmp, &cfg->uri);
+ url = (pjsip_name_addr*)pjsip_parse_uri(buddy->pool, tmp.ptr, tmp.slen,
+ PJSIP_PARSE_URI_AS_NAMEADDR);
+
+ if (url == NULL) {
+ pjsua_perror(THIS_FILE, "Unable to add buddy", PJSIP_EINVALIDURI);
+ pj_pool_release(buddy->pool);
+ buddy->pool = NULL;
+ PJSUA_UNLOCK();
+ pj_log_pop_indent();
+ return PJSIP_EINVALIDURI;
+ }
+
+ /* Only support SIP schemes */
+ if (!PJSIP_URI_SCHEME_IS_SIP(url) && !PJSIP_URI_SCHEME_IS_SIPS(url)) {
+ pj_pool_release(buddy->pool);
+ buddy->pool = NULL;
+ PJSUA_UNLOCK();
+ pj_log_pop_indent();
+ return PJSIP_EINVALIDSCHEME;
+ }
+
+ /* Reset buddy, to make sure everything is cleared with default
+ * values
+ */
+ reset_buddy(index);
+
+ /* Save URI */
+ pjsua_var.buddy[index].uri = tmp;
+
+ sip_uri = (pjsip_sip_uri*) pjsip_uri_get_uri(url->uri);
+ pjsua_var.buddy[index].name = sip_uri->user;
+ pjsua_var.buddy[index].display = url->display;
+ pjsua_var.buddy[index].host = sip_uri->host;
+ pjsua_var.buddy[index].port = sip_uri->port;
+ pjsua_var.buddy[index].monitor = cfg->subscribe;
+ if (pjsua_var.buddy[index].port == 0)
+ pjsua_var.buddy[index].port = 5060;
+
+ /* Save user data */
+ pjsua_var.buddy[index].user_data = (void*)cfg->user_data;
+
+ if (p_buddy_id)
+ *p_buddy_id = index;
+
+ pjsua_var.buddy_cnt++;
+
+ PJSUA_UNLOCK();
+
+ PJ_LOG(4,(THIS_FILE, "Buddy %d added.", index));
+
+ pjsua_buddy_subscribe_pres(index, cfg->subscribe);
+
+ pj_log_pop_indent();
+ return PJ_SUCCESS;
+}
+
+
+/*
+ * Delete buddy.
+ */
+PJ_DEF(pj_status_t) pjsua_buddy_del(pjsua_buddy_id buddy_id)
+{
+ struct buddy_lock lck;
+ pj_status_t status;
+
+ PJ_ASSERT_RETURN(buddy_id>=0 &&
+ buddy_id<(int)PJ_ARRAY_SIZE(pjsua_var.buddy),
+ PJ_EINVAL);
+
+ if (pjsua_var.buddy[buddy_id].uri.slen == 0) {
+ return PJ_SUCCESS;
+ }
+
+ status = lock_buddy("pjsua_buddy_del()", buddy_id, &lck, 0);
+ if (status != PJ_SUCCESS)
+ return status;
+
+ PJ_LOG(4,(THIS_FILE, "Buddy %d: deleting..", buddy_id));
+ pj_log_push_indent();
+
+ /* Unsubscribe presence */
+ pjsua_buddy_subscribe_pres(buddy_id, PJ_FALSE);
+
+ /* Not interested with further events for this buddy */
+ if (pjsua_var.buddy[buddy_id].sub) {
+ pjsip_evsub_set_mod_data(pjsua_var.buddy[buddy_id].sub,
+ pjsua_var.mod.id, NULL);
+ }
+
+ /* Remove buddy */
+ pjsua_var.buddy[buddy_id].uri.slen = 0;
+ pjsua_var.buddy_cnt--;
+
+ /* Clear timer */
+ if (pjsua_var.buddy[buddy_id].timer.id) {
+ pjsua_cancel_timer(&pjsua_var.buddy[buddy_id].timer);
+ pjsua_var.buddy[buddy_id].timer.id = PJ_FALSE;
+ }
+
+ /* Reset buddy struct */
+ reset_buddy(buddy_id);
+
+ unlock_buddy(&lck);
+ pj_log_pop_indent();
+ return PJ_SUCCESS;
+}
+
+
+/*
+ * Enable/disable buddy's presence monitoring.
+ */
+PJ_DEF(pj_status_t) pjsua_buddy_subscribe_pres( pjsua_buddy_id buddy_id,
+ pj_bool_t subscribe)
+{
+ struct buddy_lock lck;
+ pj_status_t status;
+
+ PJ_ASSERT_RETURN(pjsua_buddy_is_valid(buddy_id), PJ_EINVAL);
+
+ status = lock_buddy("pjsua_buddy_subscribe_pres()", buddy_id, &lck, 0);
+ if (status != PJ_SUCCESS)
+ return status;
+
+ PJ_LOG(4,(THIS_FILE, "Buddy %d: unsubscribing presence..", buddy_id));
+ pj_log_push_indent();
+
+ lck.buddy->monitor = subscribe;
+
+ pjsua_buddy_update_pres(buddy_id);
+
+ unlock_buddy(&lck);
+ pj_log_pop_indent();
+ return PJ_SUCCESS;
+}
+
+
+/*
+ * Update buddy's presence.
+ */
+PJ_DEF(pj_status_t) pjsua_buddy_update_pres(pjsua_buddy_id buddy_id)
+{
+ struct buddy_lock lck;
+ pj_status_t status;
+
+ PJ_ASSERT_RETURN(pjsua_buddy_is_valid(buddy_id), PJ_EINVAL);
+
+ status = lock_buddy("pjsua_buddy_update_pres()", buddy_id, &lck, 0);
+ if (status != PJ_SUCCESS)
+ return status;
+
+ PJ_LOG(4,(THIS_FILE, "Buddy %d: updating presence..", buddy_id));
+ pj_log_push_indent();
+
+ /* Is this an unsubscribe request? */
+ if (!lck.buddy->monitor) {
+ unsubscribe_buddy_presence(buddy_id);
+ unlock_buddy(&lck);
+ pj_log_pop_indent();
+ return PJ_SUCCESS;
+ }
+
+ /* Ignore if presence is already active for the buddy */
+ if (lck.buddy->sub) {
+ unlock_buddy(&lck);
+ pj_log_pop_indent();
+ return PJ_SUCCESS;
+ }
+
+ /* Initiate presence subscription */
+ subscribe_buddy_presence(buddy_id);
+
+ unlock_buddy(&lck);
+ pj_log_pop_indent();
+ return PJ_SUCCESS;
+}
+
+
+/*
+ * Dump presence subscriptions to log file.
+ */
+PJ_DEF(void) pjsua_pres_dump(pj_bool_t verbose)
+{
+ unsigned acc_id;
+ unsigned i;
+
+
+ PJSUA_LOCK();
+
+ /*
+ * When no detail is required, just dump number of server and client
+ * subscriptions.
+ */
+ if (verbose == PJ_FALSE) {
+
+ int count = 0;
+
+ for (acc_id=0; acc_id<PJ_ARRAY_SIZE(pjsua_var.acc); ++acc_id) {
+
+ if (!pjsua_var.acc[acc_id].valid)
+ continue;
+
+ if (!pj_list_empty(&pjsua_var.acc[acc_id].pres_srv_list)) {
+ struct pjsua_srv_pres *uapres;
+
+ uapres = pjsua_var.acc[acc_id].pres_srv_list.next;
+ while (uapres != &pjsua_var.acc[acc_id].pres_srv_list) {
+ ++count;
+ uapres = uapres->next;
+ }
+ }
+ }
+
+ PJ_LOG(3,(THIS_FILE, "Number of server/UAS subscriptions: %d",
+ count));
+
+ count = 0;
+
+ for (i=0; i<PJ_ARRAY_SIZE(pjsua_var.buddy); ++i) {
+ if (pjsua_var.buddy[i].uri.slen == 0)
+ continue;
+ if (pjsua_var.buddy[i].sub) {
+ ++count;
+ }
+ }
+
+ PJ_LOG(3,(THIS_FILE, "Number of client/UAC subscriptions: %d",
+ count));
+ PJSUA_UNLOCK();
+ return;
+ }
+
+
+ /*
+ * Dumping all server (UAS) subscriptions
+ */
+ PJ_LOG(3,(THIS_FILE, "Dumping pjsua server subscriptions:"));
+
+ for (acc_id=0; acc_id<(int)PJ_ARRAY_SIZE(pjsua_var.acc); ++acc_id) {
+
+ if (!pjsua_var.acc[acc_id].valid)
+ continue;
+
+ PJ_LOG(3,(THIS_FILE, " %.*s",
+ (int)pjsua_var.acc[acc_id].cfg.id.slen,
+ pjsua_var.acc[acc_id].cfg.id.ptr));
+
+ if (pj_list_empty(&pjsua_var.acc[acc_id].pres_srv_list)) {
+
+ PJ_LOG(3,(THIS_FILE, " - none - "));
+
+ } else {
+ struct pjsua_srv_pres *uapres;
+
+ uapres = pjsua_var.acc[acc_id].pres_srv_list.next;
+ while (uapres != &pjsua_var.acc[acc_id].pres_srv_list) {
+
+ PJ_LOG(3,(THIS_FILE, " %10s %s",
+ pjsip_evsub_get_state_name(uapres->sub),
+ uapres->remote));
+
+ uapres = uapres->next;
+ }
+ }
+ }
+
+ /*
+ * Dumping all client (UAC) subscriptions
+ */
+ PJ_LOG(3,(THIS_FILE, "Dumping pjsua client subscriptions:"));
+
+ if (pjsua_var.buddy_cnt == 0) {
+
+ PJ_LOG(3,(THIS_FILE, " - no buddy list - "));
+
+ } else {
+ for (i=0; i<PJ_ARRAY_SIZE(pjsua_var.buddy); ++i) {
+
+ if (pjsua_var.buddy[i].uri.slen == 0)
+ continue;
+
+ if (pjsua_var.buddy[i].sub) {
+ PJ_LOG(3,(THIS_FILE, " %10s %.*s",
+ pjsip_evsub_get_state_name(pjsua_var.buddy[i].sub),
+ (int)pjsua_var.buddy[i].uri.slen,
+ pjsua_var.buddy[i].uri.ptr));
+ } else {
+ PJ_LOG(3,(THIS_FILE, " %10s %.*s",
+ "(null)",
+ (int)pjsua_var.buddy[i].uri.slen,
+ pjsua_var.buddy[i].uri.ptr));
+ }
+ }
+ }
+
+ PJSUA_UNLOCK();
+}
+
+
+/***************************************************************************
+ * Server subscription.
+ */
+
+/* Proto */
+static pj_bool_t pres_on_rx_request(pjsip_rx_data *rdata);
+
+/* The module instance. */
+static pjsip_module mod_pjsua_pres =
+{
+ NULL, NULL, /* prev, next. */
+ { "mod-pjsua-pres", 14 }, /* Name. */
+ -1, /* Id */
+ PJSIP_MOD_PRIORITY_APPLICATION, /* Priority */
+ NULL, /* load() */
+ NULL, /* start() */
+ NULL, /* stop() */
+ NULL, /* unload() */
+ &pres_on_rx_request, /* on_rx_request() */
+ NULL, /* on_rx_response() */
+ NULL, /* on_tx_request. */
+ NULL, /* on_tx_response() */
+ NULL, /* on_tsx_state() */
+
+};
+
+
+/* Callback called when *server* subscription state has changed. */
+static void pres_evsub_on_srv_state( pjsip_evsub *sub, pjsip_event *event)
+{
+ pjsua_srv_pres *uapres;
+
+ PJ_UNUSED_ARG(event);
+
+ PJSUA_LOCK();
+
+ uapres = (pjsua_srv_pres*) pjsip_evsub_get_mod_data(sub, pjsua_var.mod.id);
+ if (uapres) {
+ pjsip_evsub_state state;
+
+ PJ_LOG(4,(THIS_FILE, "Server subscription to %s is %s",
+ uapres->remote, pjsip_evsub_get_state_name(sub)));
+ pj_log_push_indent();
+
+ state = pjsip_evsub_get_state(sub);
+
+ if (pjsua_var.ua_cfg.cb.on_srv_subscribe_state) {
+ pj_str_t from;
+
+ from = uapres->dlg->remote.info_str;
+ (*pjsua_var.ua_cfg.cb.on_srv_subscribe_state)(uapres->acc_id,
+ uapres, &from,
+ state, event);
+ }
+
+ if (state == PJSIP_EVSUB_STATE_TERMINATED) {
+ pjsip_evsub_set_mod_data(sub, pjsua_var.mod.id, NULL);
+ pj_list_erase(uapres);
+ }
+ pj_log_pop_indent();
+ }
+
+ PJSUA_UNLOCK();
+}
+
+/* This is called when request is received.
+ * We need to check for incoming SUBSCRIBE request.
+ */
+static pj_bool_t pres_on_rx_request(pjsip_rx_data *rdata)
+{
+ int acc_id;
+ pjsua_acc *acc;
+ pj_str_t contact;
+ pjsip_method *req_method = &rdata->msg_info.msg->line.req.method;
+ pjsua_srv_pres *uapres;
+ pjsip_evsub *sub;
+ pjsip_evsub_user pres_cb;
+ pjsip_dialog *dlg;
+ pjsip_status_code st_code;
+ pj_str_t reason;
+ pjsip_expires_hdr *expires_hdr;
+ pjsua_msg_data msg_data;
+ pj_status_t status;
+
+ if (pjsip_method_cmp(req_method, pjsip_get_subscribe_method()) != 0)
+ return PJ_FALSE;
+
+ /* Incoming SUBSCRIBE: */
+
+ /* Don't want to accept the request if shutdown is in progress */
+ if (pjsua_var.thread_quit_flag) {
+ pjsip_endpt_respond_stateless(pjsua_var.endpt, rdata,
+ PJSIP_SC_TEMPORARILY_UNAVAILABLE, NULL,
+ NULL, NULL);
+ return PJ_TRUE;
+ }
+
+ PJSUA_LOCK();
+
+ /* Find which account for the incoming request. */
+ acc_id = pjsua_acc_find_for_incoming(rdata);
+ acc = &pjsua_var.acc[acc_id];
+
+ PJ_LOG(4,(THIS_FILE, "Creating server subscription, using account %d",
+ acc_id));
+ pj_log_push_indent();
+
+ /* Create suitable Contact header */
+ if (acc->contact.slen) {
+ contact = acc->contact;
+ } else {
+ status = pjsua_acc_create_uas_contact(rdata->tp_info.pool, &contact,
+ acc_id, rdata);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Unable to generate Contact header",
+ status);
+ PJSUA_UNLOCK();
+ pjsip_endpt_respond_stateless(pjsua_var.endpt, rdata, 400, NULL,
+ NULL, NULL);
+ pj_log_pop_indent();
+ return PJ_TRUE;
+ }
+ }
+
+ /* Create UAS dialog: */
+ status = pjsip_dlg_create_uas(pjsip_ua_instance(), rdata,
+ &contact, &dlg);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE,
+ "Unable to create UAS dialog for subscription",
+ status);
+ PJSUA_UNLOCK();
+ pjsip_endpt_respond_stateless(pjsua_var.endpt, rdata, 400, NULL,
+ NULL, NULL);
+ pj_log_pop_indent();
+ return PJ_TRUE;
+ }
+
+ if (acc->cfg.allow_via_rewrite && acc->via_addr.host.slen > 0)
+ pjsip_dlg_set_via_sent_by(dlg, &acc->via_addr, acc->via_tp);
+
+ /* Set credentials and preference. */
+ pjsip_auth_clt_set_credentials(&dlg->auth_sess, acc->cred_cnt, acc->cred);
+ pjsip_auth_clt_set_prefs(&dlg->auth_sess, &acc->cfg.auth_pref);
+
+ /* Init callback: */
+ pj_bzero(&pres_cb, sizeof(pres_cb));
+ pres_cb.on_evsub_state = &pres_evsub_on_srv_state;
+
+ /* Create server presence subscription: */
+ status = pjsip_pres_create_uas( dlg, &pres_cb, rdata, &sub);
+ if (status != PJ_SUCCESS) {
+ int code = PJSIP_ERRNO_TO_SIP_STATUS(status);
+ pjsip_tx_data *tdata;
+
+ pjsua_perror(THIS_FILE, "Unable to create server subscription",
+ status);
+
+ if (code==599 || code > 699 || code < 300) {
+ code = 400;
+ }
+
+ status = pjsip_dlg_create_response(dlg, rdata, code, NULL, &tdata);
+ if (status == PJ_SUCCESS) {
+ status = pjsip_dlg_send_response(dlg, pjsip_rdata_get_tsx(rdata),
+ tdata);
+ }
+
+ PJSUA_UNLOCK();
+ pj_log_pop_indent();
+ return PJ_TRUE;
+ }
+
+ /* If account is locked to specific transport, then lock dialog
+ * to this transport too.
+ */
+ if (acc->cfg.transport_id != PJSUA_INVALID_ID) {
+ pjsip_tpselector tp_sel;
+
+ pjsua_init_tpselector(acc->cfg.transport_id, &tp_sel);
+ pjsip_dlg_set_transport(dlg, &tp_sel);
+ }
+
+ /* Attach our data to the subscription: */
+ uapres = PJ_POOL_ALLOC_T(dlg->pool, pjsua_srv_pres);
+ uapres->sub = sub;
+ uapres->remote = (char*) pj_pool_alloc(dlg->pool, PJSIP_MAX_URL_SIZE);
+ uapres->acc_id = acc_id;
+ uapres->dlg = dlg;
+ status = pjsip_uri_print(PJSIP_URI_IN_REQ_URI, dlg->remote.info->uri,
+ uapres->remote, PJSIP_MAX_URL_SIZE);
+ if (status < 1)
+ pj_ansi_strcpy(uapres->remote, "<-- url is too long-->");
+ else
+ uapres->remote[status] = '\0';
+
+ pjsip_evsub_add_header(sub, &acc->cfg.sub_hdr_list);
+ pjsip_evsub_set_mod_data(sub, pjsua_var.mod.id, uapres);
+
+ /* Add server subscription to the list: */
+ pj_list_push_back(&pjsua_var.acc[acc_id].pres_srv_list, uapres);
+
+
+ /* Capture the value of Expires header. */
+ expires_hdr = (pjsip_expires_hdr*)
+ pjsip_msg_find_hdr(rdata->msg_info.msg, PJSIP_H_EXPIRES,
+ NULL);
+ if (expires_hdr)
+ uapres->expires = expires_hdr->ivalue;
+ else
+ uapres->expires = -1;
+
+ st_code = (pjsip_status_code)200;
+ reason = pj_str("OK");
+ pjsua_msg_data_init(&msg_data);
+
+ /* Notify application callback, if any */
+ if (pjsua_var.ua_cfg.cb.on_incoming_subscribe) {
+ pjsua_buddy_id buddy_id;
+
+ buddy_id = find_buddy(rdata->msg_info.from->uri);
+
+ (*pjsua_var.ua_cfg.cb.on_incoming_subscribe)(acc_id, uapres, buddy_id,
+ &dlg->remote.info_str,
+ rdata, &st_code, &reason,
+ &msg_data);
+ }
+
+ /* Handle rejection case */
+ if (st_code >= 300) {
+ pjsip_tx_data *tdata;
+
+ /* Create response */
+ status = pjsip_dlg_create_response(dlg, rdata, st_code,
+ &reason, &tdata);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Error creating response", status);
+ pj_list_erase(uapres);
+ pjsip_pres_terminate(sub, PJ_FALSE);
+ PJSUA_UNLOCK();
+ pj_log_pop_indent();
+ return PJ_FALSE;
+ }
+
+ /* Add header list, if any */
+ pjsua_process_msg_data(tdata, &msg_data);
+
+ /* Send the response */
+ status = pjsip_dlg_send_response(dlg, pjsip_rdata_get_tsx(rdata),
+ tdata);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Error sending response", status);
+ /* This is not fatal */
+ }
+
+ /* Terminate presence subscription */
+ pj_list_erase(uapres);
+ pjsip_pres_terminate(sub, PJ_FALSE);
+ PJSUA_UNLOCK();
+ pj_log_pop_indent();
+ return PJ_TRUE;
+ }
+
+ /* Create and send 2xx response to the SUBSCRIBE request: */
+ status = pjsip_pres_accept(sub, rdata, st_code, &msg_data.hdr_list);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Unable to accept presence subscription",
+ status);
+ pj_list_erase(uapres);
+ pjsip_pres_terminate(sub, PJ_FALSE);
+ PJSUA_UNLOCK();
+ pj_log_pop_indent();
+ return PJ_FALSE;
+ }
+
+ /* If code is 200, send NOTIFY now */
+ if (st_code == 200) {
+ pjsua_pres_notify(acc_id, uapres, PJSIP_EVSUB_STATE_ACTIVE,
+ NULL, NULL, PJ_TRUE, &msg_data);
+ }
+
+ /* Done: */
+
+ PJSUA_UNLOCK();
+ pj_log_pop_indent();
+ return PJ_TRUE;
+}
+
+
+/*
+ * Send NOTIFY.
+ */
+PJ_DEF(pj_status_t) pjsua_pres_notify( pjsua_acc_id acc_id,
+ pjsua_srv_pres *srv_pres,
+ pjsip_evsub_state ev_state,
+ const pj_str_t *state_str,
+ const pj_str_t *reason,
+ pj_bool_t with_body,
+ const pjsua_msg_data *msg_data)
+{
+ pjsua_acc *acc;
+ pjsip_pres_status pres_status;
+ pjsua_buddy_id buddy_id;
+ pjsip_tx_data *tdata;
+ pj_status_t status;
+
+ /* Check parameters */
+ PJ_ASSERT_RETURN(acc_id!=-1 && srv_pres, PJ_EINVAL);
+
+ /* Check that account ID is valid */
+ PJ_ASSERT_RETURN(acc_id>=0 && acc_id<(int)PJ_ARRAY_SIZE(pjsua_var.acc),
+ PJ_EINVAL);
+ /* Check that account is valid */
+ PJ_ASSERT_RETURN(pjsua_var.acc[acc_id].valid, PJ_EINVALIDOP);
+
+ PJ_LOG(4,(THIS_FILE, "Acc %d: sending NOTIFY for srv_pres=0x%p..",
+ acc_id, (int)(long)srv_pres));
+ pj_log_push_indent();
+
+ PJSUA_LOCK();
+
+ acc = &pjsua_var.acc[acc_id];
+
+ /* Check that the server presence subscription is still valid */
+ if (pj_list_find_node(&acc->pres_srv_list, srv_pres) == NULL) {
+ /* Subscription has been terminated */
+ PJSUA_UNLOCK();
+ pj_log_pop_indent();
+ return PJ_EINVALIDOP;
+ }
+
+ /* Set our online status: */
+ pj_bzero(&pres_status, sizeof(pres_status));
+ pres_status.info_cnt = 1;
+ pres_status.info[0].basic_open = acc->online_status;
+ pres_status.info[0].id = acc->cfg.pidf_tuple_id;
+ //Both pjsua_var.local_uri and pjsua_var.contact_uri are enclosed in "<" and ">"
+ //causing XML parsing to fail.
+ //pres_status.info[0].contact = pjsua_var.local_uri;
+ /* add RPID information */
+ pj_memcpy(&pres_status.info[0].rpid, &acc->rpid,
+ sizeof(pjrpid_element));
+
+ pjsip_pres_set_status(srv_pres->sub, &pres_status);
+
+ /* Check expires value. If it's zero, send our presense state but
+ * set subscription state to TERMINATED.
+ */
+ if (srv_pres->expires == 0)
+ ev_state = PJSIP_EVSUB_STATE_TERMINATED;
+
+ /* Create and send the NOTIFY to active subscription: */
+ status = pjsip_pres_notify(srv_pres->sub, ev_state, state_str,
+ reason, &tdata);
+ if (status == PJ_SUCCESS) {
+ /* Force removal of message body if msg_body==FALSE */
+ if (!with_body) {
+ tdata->msg->body = NULL;
+ }
+ pjsua_process_msg_data(tdata, msg_data);
+ status = pjsip_pres_send_request( srv_pres->sub, tdata);
+ }
+
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Unable to create/send NOTIFY",
+ status);
+ pj_list_erase(srv_pres);
+ pjsip_pres_terminate(srv_pres->sub, PJ_FALSE);
+ PJSUA_UNLOCK();
+ pj_log_pop_indent();
+ return status;
+ }
+
+
+ /* Subscribe to buddy's presence if we're not subscribed */
+ buddy_id = find_buddy(srv_pres->dlg->remote.info->uri);
+ if (buddy_id != PJSUA_INVALID_ID) {
+ pjsua_buddy *b = &pjsua_var.buddy[buddy_id];
+ if (b->monitor && b->sub == NULL) {
+ PJ_LOG(4,(THIS_FILE, "Received SUBSCRIBE from buddy %d, "
+ "activating outgoing subscription", buddy_id));
+ subscribe_buddy_presence(buddy_id);
+ }
+ }
+
+ PJSUA_UNLOCK();
+ pj_log_pop_indent();
+ return PJ_SUCCESS;
+}
+
+
+/*
+ * Client presence publication callback.
+ */
+static void publish_cb(struct pjsip_publishc_cbparam *param)
+{
+ pjsua_acc *acc = (pjsua_acc*) param->token;
+
+ if (param->code/100 != 2 || param->status != PJ_SUCCESS) {
+
+ pjsip_publishc_destroy(param->pubc);
+ acc->publish_sess = NULL;
+
+ if (param->status != PJ_SUCCESS) {
+ char errmsg[PJ_ERR_MSG_SIZE];
+
+ pj_strerror(param->status, errmsg, sizeof(errmsg));
+ PJ_LOG(1,(THIS_FILE,
+ "Client publication (PUBLISH) failed, status=%d, msg=%s",
+ param->status, errmsg));
+ } else if (param->code == 412) {
+ /* 412 (Conditional Request Failed)
+ * The PUBLISH refresh has failed, retry with new one.
+ */
+ pjsua_pres_init_publish_acc(acc->index);
+
+ } else {
+ PJ_LOG(1,(THIS_FILE,
+ "Client publication (PUBLISH) failed (%d/%.*s)",
+ param->code, (int)param->reason.slen,
+ param->reason.ptr));
+ }
+
+ } else {
+ if (param->expiration < 1) {
+ /* Could happen if server "forgot" to include Expires header
+ * in the response. We will not renew, so destroy the pubc.
+ */
+ pjsip_publishc_destroy(param->pubc);
+ acc->publish_sess = NULL;
+ }
+ }
+}
+
+
+/*
+ * Send PUBLISH request.
+ */
+static pj_status_t send_publish(int acc_id, pj_bool_t active)
+{
+ pjsua_acc_config *acc_cfg = &pjsua_var.acc[acc_id].cfg;
+ pjsua_acc *acc = &pjsua_var.acc[acc_id];
+ pjsip_pres_status pres_status;
+ pjsip_tx_data *tdata;
+ pj_status_t status;
+
+ PJ_LOG(5,(THIS_FILE, "Acc %d: sending %sPUBLISH..",
+ acc_id, (active ? "" : "un-")));
+ pj_log_push_indent();
+
+ /* Create PUBLISH request */
+ if (active) {
+ char *bpos;
+ pj_str_t entity;
+
+ status = pjsip_publishc_publish(acc->publish_sess, PJ_TRUE, &tdata);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Error creating PUBLISH request", status);
+ goto on_error;
+ }
+
+ /* Set our online status: */
+ pj_bzero(&pres_status, sizeof(pres_status));
+ pres_status.info_cnt = 1;
+ pres_status.info[0].basic_open = acc->online_status;
+ pres_status.info[0].id = acc->cfg.pidf_tuple_id;
+ /* .. including RPID information */
+ pj_memcpy(&pres_status.info[0].rpid, &acc->rpid,
+ sizeof(pjrpid_element));
+
+ /* Be careful not to send PIDF with presence entity ID containing
+ * "<" character.
+ */
+ if ((bpos=pj_strchr(&acc_cfg->id, '<')) != NULL) {
+ char *epos = pj_strchr(&acc_cfg->id, '>');
+ if (epos - bpos < 2) {
+ pj_assert(!"Unexpected invalid URI");
+ status = PJSIP_EINVALIDURI;
+ goto on_error;
+ }
+ entity.ptr = bpos+1;
+ entity.slen = epos - bpos - 1;
+ } else {
+ entity = acc_cfg->id;
+ }
+
+ /* Create and add PIDF message body */
+ status = pjsip_pres_create_pidf(tdata->pool, &pres_status,
+ &entity, &tdata->msg->body);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Error creating PIDF for PUBLISH request",
+ status);
+ pjsip_tx_data_dec_ref(tdata);
+ goto on_error;
+ }
+ } else {
+ status = pjsip_publishc_unpublish(acc->publish_sess, &tdata);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Error creating PUBLISH request", status);
+ goto on_error;
+ }
+ }
+
+ /* Add headers etc */
+ pjsua_process_msg_data(tdata, NULL);
+
+ /* Set Via sent-by */
+ if (acc->cfg.allow_via_rewrite && acc->via_addr.host.slen > 0) {
+ pjsip_publishc_set_via_sent_by(acc->publish_sess, &acc->via_addr,
+ acc->via_tp);
+ }
+
+ /* Send the PUBLISH request */
+ status = pjsip_publishc_send(acc->publish_sess, tdata);
+ if (status == PJ_EPENDING) {
+ PJ_LOG(3,(THIS_FILE, "Previous request is in progress, "
+ "PUBLISH request is queued"));
+ } else if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Error sending PUBLISH request", status);
+ goto on_error;
+ }
+
+ acc->publish_state = acc->online_status;
+ pj_log_pop_indent();
+ return PJ_SUCCESS;
+
+on_error:
+ if (acc->publish_sess) {
+ pjsip_publishc_destroy(acc->publish_sess);
+ acc->publish_sess = NULL;
+ }
+ pj_log_pop_indent();
+ return status;
+}
+
+
+/* Create client publish session */
+pj_status_t pjsua_pres_init_publish_acc(int acc_id)
+{
+ const pj_str_t STR_PRESENCE = { "presence", 8 };
+ pjsua_acc_config *acc_cfg = &pjsua_var.acc[acc_id].cfg;
+ pjsua_acc *acc = &pjsua_var.acc[acc_id];
+ pj_status_t status;
+
+ /* Create and init client publication session */
+ if (acc_cfg->publish_enabled) {
+
+ /* Create client publication */
+ status = pjsip_publishc_create(pjsua_var.endpt, &acc_cfg->publish_opt,
+ acc, &publish_cb,
+ &acc->publish_sess);
+ if (status != PJ_SUCCESS) {
+ acc->publish_sess = NULL;
+ return status;
+ }
+
+ /* Initialize client publication */
+ status = pjsip_publishc_init(acc->publish_sess, &STR_PRESENCE,
+ &acc_cfg->id, &acc_cfg->id,
+ &acc_cfg->id,
+ PJSUA_PUBLISH_EXPIRATION);
+ if (status != PJ_SUCCESS) {
+ acc->publish_sess = NULL;
+ return status;
+ }
+
+ /* Add credential for authentication */
+ if (acc->cred_cnt) {
+ pjsip_publishc_set_credentials(acc->publish_sess, acc->cred_cnt,
+ acc->cred);
+ }
+
+ /* Set route-set */
+ pjsip_publishc_set_route_set(acc->publish_sess, &acc->route_set);
+
+ /* Send initial PUBLISH request */
+ if (acc->online_status != 0) {
+ status = send_publish(acc_id, PJ_TRUE);
+ if (status != PJ_SUCCESS)
+ return status;
+ }
+
+ } else {
+ acc->publish_sess = NULL;
+ }
+
+ return PJ_SUCCESS;
+}
+
+
+/* Init presence for account */
+pj_status_t pjsua_pres_init_acc(int acc_id)
+{
+ pjsua_acc *acc = &pjsua_var.acc[acc_id];
+
+ /* Init presence subscription */
+ pj_list_init(&acc->pres_srv_list);
+
+ return PJ_SUCCESS;
+}
+
+
+/* Unpublish presence publication */
+void pjsua_pres_unpublish(pjsua_acc *acc, unsigned flags)
+{
+ if (acc->publish_sess) {
+ pjsua_acc_config *acc_cfg = &acc->cfg;
+
+ acc->online_status = PJ_FALSE;
+
+ if ((flags & PJSUA_DESTROY_NO_TX_MSG) == 0) {
+ send_publish(acc->index, PJ_FALSE);
+ }
+
+ /* By ticket #364, don't destroy the session yet (let the callback
+ destroy it)
+ if (acc->publish_sess) {
+ pjsip_publishc_destroy(acc->publish_sess);
+ acc->publish_sess = NULL;
+ }
+ */
+ acc_cfg->publish_enabled = PJ_FALSE;
+ }
+}
+
+/* Terminate server subscription for the account */
+void pjsua_pres_delete_acc(int acc_id, unsigned flags)
+{
+ pjsua_acc *acc = &pjsua_var.acc[acc_id];
+ pjsua_srv_pres *uapres;
+
+ uapres = pjsua_var.acc[acc_id].pres_srv_list.next;
+
+ /* Notify all subscribers that we're no longer available */
+ while (uapres != &acc->pres_srv_list) {
+
+ pjsip_pres_status pres_status;
+ pj_str_t reason = { "noresource", 10 };
+ pjsua_srv_pres *next;
+ pjsip_tx_data *tdata;
+
+ next = uapres->next;
+
+ pjsip_pres_get_status(uapres->sub, &pres_status);
+
+ pres_status.info[0].basic_open = pjsua_var.acc[acc_id].online_status;
+ pjsip_pres_set_status(uapres->sub, &pres_status);
+
+ if ((flags & PJSUA_DESTROY_NO_TX_MSG) == 0) {
+ if (pjsip_pres_notify(uapres->sub,
+ PJSIP_EVSUB_STATE_TERMINATED, NULL,
+ &reason, &tdata)==PJ_SUCCESS)
+ {
+ pjsip_pres_send_request(uapres->sub, tdata);
+ }
+ } else {
+ pjsip_pres_terminate(uapres->sub, PJ_FALSE);
+ }
+
+ uapres = next;
+ }
+
+ /* Clear server presence subscription list because account might be reused
+ * later. */
+ pj_list_init(&acc->pres_srv_list);
+
+ /* Terminate presence publication, if any */
+ pjsua_pres_unpublish(acc, flags);
+}
+
+
+/* Update server subscription (e.g. when our online status has changed) */
+void pjsua_pres_update_acc(int acc_id, pj_bool_t force)
+{
+ pjsua_acc *acc = &pjsua_var.acc[acc_id];
+ pjsua_acc_config *acc_cfg = &pjsua_var.acc[acc_id].cfg;
+ pjsua_srv_pres *uapres;
+
+ uapres = pjsua_var.acc[acc_id].pres_srv_list.next;
+
+ while (uapres != &acc->pres_srv_list) {
+
+ pjsip_pres_status pres_status;
+ pjsip_tx_data *tdata;
+
+ pjsip_pres_get_status(uapres->sub, &pres_status);
+
+ /* Only send NOTIFY once subscription is active. Some subscriptions
+ * may still be in NULL (when app is adding a new buddy while in the
+ * on_incoming_subscribe() callback) or PENDING (when user approval is
+ * being requested) state and we don't send NOTIFY to these subs until
+ * the user accepted the request.
+ */
+ if (pjsip_evsub_get_state(uapres->sub)==PJSIP_EVSUB_STATE_ACTIVE &&
+ (force || pres_status.info[0].basic_open != acc->online_status))
+ {
+
+ pres_status.info[0].basic_open = acc->online_status;
+ pj_memcpy(&pres_status.info[0].rpid, &acc->rpid,
+ sizeof(pjrpid_element));
+
+ pjsip_pres_set_status(uapres->sub, &pres_status);
+
+ if (pjsip_pres_current_notify(uapres->sub, &tdata)==PJ_SUCCESS) {
+ pjsua_process_msg_data(tdata, NULL);
+ pjsip_pres_send_request(uapres->sub, tdata);
+ }
+ }
+
+ uapres = uapres->next;
+ }
+
+ /* Send PUBLISH if required. We only do this when we have a PUBLISH
+ * session. If we don't have a PUBLISH session, then it could be
+ * that we're waiting until registration has completed before we
+ * send the first PUBLISH.
+ */
+ if (acc_cfg->publish_enabled && acc->publish_sess) {
+ if (force || acc->publish_state != acc->online_status) {
+ send_publish(acc_id, PJ_TRUE);
+ }
+ }
+}
+
+
+
+/***************************************************************************
+ * Client subscription.
+ */
+
+static void buddy_timer_cb(pj_timer_heap_t *th, pj_timer_entry *entry)
+{
+ pjsua_buddy *buddy = (pjsua_buddy*)entry->user_data;
+
+ PJ_UNUSED_ARG(th);
+
+ entry->id = PJ_FALSE;
+ pjsua_buddy_update_pres(buddy->index);
+}
+
+/* Reschedule subscription refresh timer or terminate the subscription
+ * refresh timer for the specified buddy.
+ */
+static void buddy_resubscribe(pjsua_buddy *buddy, pj_bool_t resched,
+ unsigned msec_interval)
+{
+ if (buddy->timer.id) {
+ pjsua_cancel_timer(&buddy->timer);
+ buddy->timer.id = PJ_FALSE;
+ }
+
+ if (resched) {
+ pj_time_val delay;
+
+ PJ_LOG(4,(THIS_FILE,
+ "Resubscribing buddy id %u in %u ms (reason: %.*s)",
+ buddy->index, msec_interval,
+ (int)buddy->term_reason.slen,
+ buddy->term_reason.ptr));
+
+ pj_timer_entry_init(&buddy->timer, 0, buddy, &buddy_timer_cb);
+ delay.sec = 0;
+ delay.msec = msec_interval;
+ pj_time_val_normalize(&delay);
+
+ if (pjsua_schedule_timer(&buddy->timer, &delay)==PJ_SUCCESS)
+ buddy->timer.id = PJ_TRUE;
+ }
+}
+
+/* Callback called when *client* subscription state has changed. */
+static void pjsua_evsub_on_state( pjsip_evsub *sub, pjsip_event *event)
+{
+ pjsua_buddy *buddy;
+
+ PJ_UNUSED_ARG(event);
+
+ /* Note: #937: no need to acuire PJSUA_LOCK here. Since the buddy has
+ * a dialog attached to it, lock_buddy() will use the dialog
+ * lock, which we are currently holding!
+ */
+ buddy = (pjsua_buddy*) pjsip_evsub_get_mod_data(sub, pjsua_var.mod.id);
+ if (buddy) {
+ PJ_LOG(4,(THIS_FILE,
+ "Presence subscription to %.*s is %s",
+ (int)pjsua_var.buddy[buddy->index].uri.slen,
+ pjsua_var.buddy[buddy->index].uri.ptr,
+ pjsip_evsub_get_state_name(sub)));
+ pj_log_push_indent();
+
+ if (pjsip_evsub_get_state(sub) == PJSIP_EVSUB_STATE_TERMINATED) {
+ int resub_delay = -1;
+
+ if (buddy->term_reason.ptr == NULL) {
+ buddy->term_reason.ptr = (char*)
+ pj_pool_alloc(buddy->pool,
+ PJSUA_BUDDY_SUB_TERM_REASON_LEN);
+ }
+ pj_strncpy(&buddy->term_reason,
+ pjsip_evsub_get_termination_reason(sub),
+ PJSUA_BUDDY_SUB_TERM_REASON_LEN);
+
+ buddy->term_code = 200;
+
+ /* Determine whether to resubscribe automatically */
+ if (event && event->type==PJSIP_EVENT_TSX_STATE) {
+ const pjsip_transaction *tsx = event->body.tsx_state.tsx;
+ if (pjsip_method_cmp(&tsx->method,
+ &pjsip_subscribe_method)==0)
+ {
+ buddy->term_code = tsx->status_code;
+ switch (tsx->status_code) {
+ case PJSIP_SC_CALL_TSX_DOES_NOT_EXIST:
+ /* 481: we refreshed too late? resubscribe
+ * immediately.
+ */
+ /* But this must only happen when the 481 is received
+ * on subscription refresh request. We MUST NOT try to
+ * resubscribe automatically if the 481 is received
+ * on the initial SUBSCRIBE (if server returns this
+ * response for some reason).
+ */
+ if (buddy->dlg->remote.contact)
+ resub_delay = 500;
+ break;
+ }
+ } else if (pjsip_method_cmp(&tsx->method,
+ &pjsip_notify_method)==0)
+ {
+ if (pj_stricmp2(&buddy->term_reason, "deactivated")==0 ||
+ pj_stricmp2(&buddy->term_reason, "timeout")==0) {
+ /* deactivated: The subscription has been terminated,
+ * but the subscriber SHOULD retry immediately with
+ * a new subscription.
+ */
+ /* timeout: The subscription has been terminated
+ * because it was not refreshed before it expired.
+ * Clients MAY re-subscribe immediately. The
+ * "retry-after" parameter has no semantics for
+ * "timeout".
+ */
+ resub_delay = 500;
+ }
+ else if (pj_stricmp2(&buddy->term_reason, "probation")==0||
+ pj_stricmp2(&buddy->term_reason, "giveup")==0) {
+ /* probation: The subscription has been terminated,
+ * but the client SHOULD retry at some later time.
+ * If a "retry-after" parameter is also present, the
+ * client SHOULD wait at least the number of seconds
+ * specified by that parameter before attempting to re-
+ * subscribe.
+ */
+ /* giveup: The subscription has been terminated because
+ * the notifier could not obtain authorization in a
+ * timely fashion. If a "retry-after" parameter is
+ * also present, the client SHOULD wait at least the
+ * number of seconds specified by that parameter before
+ * attempting to re-subscribe; otherwise, the client
+ * MAY retry immediately, but will likely get put back
+ * into pending state.
+ */
+ const pjsip_sub_state_hdr *sub_hdr;
+ pj_str_t sub_state = { "Subscription-State", 18 };
+ const pjsip_msg *msg;
+
+ msg = event->body.tsx_state.src.rdata->msg_info.msg;
+ sub_hdr = (const pjsip_sub_state_hdr*)
+ pjsip_msg_find_hdr_by_name(msg, &sub_state,
+ NULL);
+ if (sub_hdr && sub_hdr->retry_after > 0)
+ resub_delay = sub_hdr->retry_after * 1000;
+ }
+
+ }
+ }
+
+ /* For other cases of subscription termination, if resubscribe
+ * timer is not set, schedule with default expiration (plus minus
+ * some random value, to avoid sending SUBSCRIBEs all at once)
+ */
+ if (resub_delay == -1) {
+ pj_assert(PJSUA_PRES_TIMER >= 3);
+ resub_delay = PJSUA_PRES_TIMER*1000 - 2500 + (pj_rand()%5000);
+ }
+
+ buddy_resubscribe(buddy, PJ_TRUE, resub_delay);
+
+ } else {
+ /* This will clear the last termination code/reason */
+ buddy->term_code = 0;
+ buddy->term_reason.slen = 0;
+ }
+
+ /* Call callbacks */
+ if (pjsua_var.ua_cfg.cb.on_buddy_evsub_state)
+ (*pjsua_var.ua_cfg.cb.on_buddy_evsub_state)(buddy->index, sub,
+ event);
+
+ if (pjsua_var.ua_cfg.cb.on_buddy_state)
+ (*pjsua_var.ua_cfg.cb.on_buddy_state)(buddy->index);
+
+ /* Clear subscription */
+ if (pjsip_evsub_get_state(sub) == PJSIP_EVSUB_STATE_TERMINATED) {
+ buddy->sub = NULL;
+ buddy->status.info_cnt = 0;
+ buddy->dlg = NULL;
+ pjsip_evsub_set_mod_data(sub, pjsua_var.mod.id, NULL);
+ }
+
+ pj_log_pop_indent();
+ }
+}
+
+
+/* Callback when transaction state has changed. */
+static void pjsua_evsub_on_tsx_state(pjsip_evsub *sub,
+ pjsip_transaction *tsx,
+ pjsip_event *event)
+{
+ pjsua_buddy *buddy;
+ pjsip_contact_hdr *contact_hdr;
+
+ /* Note: #937: no need to acuire PJSUA_LOCK here. Since the buddy has
+ * a dialog attached to it, lock_buddy() will use the dialog
+ * lock, which we are currently holding!
+ */
+ buddy = (pjsua_buddy*) pjsip_evsub_get_mod_data(sub, pjsua_var.mod.id);
+ if (!buddy) {
+ return;
+ }
+
+ /* We only use this to update buddy's Contact, when it's not
+ * set.
+ */
+ if (buddy->contact.slen != 0) {
+ /* Contact already set */
+ return;
+ }
+
+ /* Only care about 2xx response to outgoing SUBSCRIBE */
+ if (tsx->status_code/100 != 2 ||
+ tsx->role != PJSIP_UAC_ROLE ||
+ event->type != PJSIP_EVENT_RX_MSG ||
+ pjsip_method_cmp(&tsx->method, pjsip_get_subscribe_method())!=0)
+ {
+ return;
+ }
+
+ /* Find contact header. */
+ contact_hdr = (pjsip_contact_hdr*)
+ pjsip_msg_find_hdr(event->body.rx_msg.rdata->msg_info.msg,
+ PJSIP_H_CONTACT, NULL);
+ if (!contact_hdr || !contact_hdr->uri) {
+ return;
+ }
+
+ buddy->contact.ptr = (char*)
+ pj_pool_alloc(buddy->pool, PJSIP_MAX_URL_SIZE);
+ buddy->contact.slen = pjsip_uri_print( PJSIP_URI_IN_CONTACT_HDR,
+ contact_hdr->uri,
+ buddy->contact.ptr,
+ PJSIP_MAX_URL_SIZE);
+ if (buddy->contact.slen < 0)
+ buddy->contact.slen = 0;
+}
+
+
+/* Callback called when we receive NOTIFY */
+static void pjsua_evsub_on_rx_notify(pjsip_evsub *sub,
+ pjsip_rx_data *rdata,
+ int *p_st_code,
+ pj_str_t **p_st_text,
+ pjsip_hdr *res_hdr,
+ pjsip_msg_body **p_body)
+{
+ pjsua_buddy *buddy;
+
+ /* Note: #937: no need to acuire PJSUA_LOCK here. Since the buddy has
+ * a dialog attached to it, lock_buddy() will use the dialog
+ * lock, which we are currently holding!
+ */
+ buddy = (pjsua_buddy*) pjsip_evsub_get_mod_data(sub, pjsua_var.mod.id);
+ if (buddy) {
+ /* Update our info. */
+ pjsip_pres_get_status(sub, &buddy->status);
+ }
+
+ /* The default is to send 200 response to NOTIFY.
+ * Just leave it there..
+ */
+ PJ_UNUSED_ARG(rdata);
+ PJ_UNUSED_ARG(p_st_code);
+ PJ_UNUSED_ARG(p_st_text);
+ PJ_UNUSED_ARG(res_hdr);
+ PJ_UNUSED_ARG(p_body);
+}
+
+
+/* It does what it says.. */
+static void subscribe_buddy_presence(pjsua_buddy_id buddy_id)
+{
+ pjsip_evsub_user pres_callback;
+ pj_pool_t *tmp_pool = NULL;
+ pjsua_buddy *buddy;
+ int acc_id;
+ pjsua_acc *acc;
+ pj_str_t contact;
+ pjsip_tx_data *tdata;
+ pj_status_t status;
+
+ /* Event subscription callback. */
+ pj_bzero(&pres_callback, sizeof(pres_callback));
+ pres_callback.on_evsub_state = &pjsua_evsub_on_state;
+ pres_callback.on_tsx_state = &pjsua_evsub_on_tsx_state;
+ pres_callback.on_rx_notify = &pjsua_evsub_on_rx_notify;
+
+ buddy = &pjsua_var.buddy[buddy_id];
+ acc_id = pjsua_acc_find_for_outgoing(&buddy->uri);
+
+ acc = &pjsua_var.acc[acc_id];
+
+ PJ_LOG(4,(THIS_FILE, "Buddy %d: subscribing presence,using account %d..",
+ buddy_id, acc_id));
+ pj_log_push_indent();
+
+ /* Generate suitable Contact header unless one is already set in
+ * the account
+ */
+ if (acc->contact.slen) {
+ contact = acc->contact;
+ } else {
+ tmp_pool = pjsua_pool_create("tmpbuddy", 512, 256);
+
+ status = pjsua_acc_create_uac_contact(tmp_pool, &contact,
+ acc_id, &buddy->uri);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Unable to generate Contact header",
+ status);
+ pj_pool_release(tmp_pool);
+ pj_log_pop_indent();
+ return;
+ }
+ }
+
+ /* Create UAC dialog */
+ status = pjsip_dlg_create_uac( pjsip_ua_instance(),
+ &acc->cfg.id,
+ &contact,
+ &buddy->uri,
+ NULL, &buddy->dlg);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Unable to create dialog",
+ status);
+ if (tmp_pool) pj_pool_release(tmp_pool);
+ pj_log_pop_indent();
+ return;
+ }
+
+ /* Increment the dialog's lock otherwise when presence session creation
+ * fails the dialog will be destroyed prematurely.
+ */
+ pjsip_dlg_inc_lock(buddy->dlg);
+
+ if (acc->cfg.allow_via_rewrite && acc->via_addr.host.slen > 0)
+ pjsip_dlg_set_via_sent_by(buddy->dlg, &acc->via_addr, acc->via_tp);
+
+ status = pjsip_pres_create_uac( buddy->dlg, &pres_callback,
+ PJSIP_EVSUB_NO_EVENT_ID, &buddy->sub);
+ if (status != PJ_SUCCESS) {
+ buddy->sub = NULL;
+ pjsua_perror(THIS_FILE, "Unable to create presence client",
+ status);
+ /* This should destroy the dialog since there's no session
+ * referencing it
+ */
+ if (buddy->dlg) pjsip_dlg_dec_lock(buddy->dlg);
+ if (tmp_pool) pj_pool_release(tmp_pool);
+ pj_log_pop_indent();
+ return;
+ }
+
+ /* If account is locked to specific transport, then lock dialog
+ * to this transport too.
+ */
+ if (acc->cfg.transport_id != PJSUA_INVALID_ID) {
+ pjsip_tpselector tp_sel;
+
+ pjsua_init_tpselector(acc->cfg.transport_id, &tp_sel);
+ pjsip_dlg_set_transport(buddy->dlg, &tp_sel);
+ }
+
+ /* Set route-set */
+ if (!pj_list_empty(&acc->route_set)) {
+ pjsip_dlg_set_route_set(buddy->dlg, &acc->route_set);
+ }
+
+ /* Set credentials */
+ if (acc->cred_cnt) {
+ pjsip_auth_clt_set_credentials( &buddy->dlg->auth_sess,
+ acc->cred_cnt, acc->cred);
+ }
+
+ /* Set authentication preference */
+ pjsip_auth_clt_set_prefs(&buddy->dlg->auth_sess, &acc->cfg.auth_pref);
+
+ pjsip_evsub_set_mod_data(buddy->sub, pjsua_var.mod.id, buddy);
+
+ status = pjsip_pres_initiate(buddy->sub, -1, &tdata);
+ if (status != PJ_SUCCESS) {
+ if (buddy->dlg) pjsip_dlg_dec_lock(buddy->dlg);
+ if (buddy->sub) {
+ pjsip_pres_terminate(buddy->sub, PJ_FALSE);
+ }
+ buddy->sub = NULL;
+ pjsua_perror(THIS_FILE, "Unable to create initial SUBSCRIBE",
+ status);
+ if (tmp_pool) pj_pool_release(tmp_pool);
+ pj_log_pop_indent();
+ return;
+ }
+
+ pjsua_process_msg_data(tdata, NULL);
+
+ status = pjsip_pres_send_request(buddy->sub, tdata);
+ if (status != PJ_SUCCESS) {
+ if (buddy->dlg) pjsip_dlg_dec_lock(buddy->dlg);
+ if (buddy->sub) {
+ pjsip_pres_terminate(buddy->sub, PJ_FALSE);
+ }
+ buddy->sub = NULL;
+ pjsua_perror(THIS_FILE, "Unable to send initial SUBSCRIBE",
+ status);
+ if (tmp_pool) pj_pool_release(tmp_pool);
+ pj_log_pop_indent();
+ return;
+ }
+
+ pjsip_dlg_dec_lock(buddy->dlg);
+ if (tmp_pool) pj_pool_release(tmp_pool);
+ pj_log_pop_indent();
+}
+
+
+/* It does what it says... */
+static void unsubscribe_buddy_presence(pjsua_buddy_id buddy_id)
+{
+ pjsua_buddy *buddy;
+ pjsip_tx_data *tdata;
+ pj_status_t status;
+
+ buddy = &pjsua_var.buddy[buddy_id];
+
+ if (buddy->sub == NULL)
+ return;
+
+ if (pjsip_evsub_get_state(buddy->sub) == PJSIP_EVSUB_STATE_TERMINATED) {
+ buddy->sub = NULL;
+ return;
+ }
+
+ PJ_LOG(5,(THIS_FILE, "Buddy %d: unsubscribing..", buddy_id));
+ pj_log_push_indent();
+
+ status = pjsip_pres_initiate( buddy->sub, 0, &tdata);
+ if (status == PJ_SUCCESS) {
+ pjsua_process_msg_data(tdata, NULL);
+ status = pjsip_pres_send_request( buddy->sub, tdata );
+ }
+
+ if (status != PJ_SUCCESS && buddy->sub) {
+ pjsip_pres_terminate(buddy->sub, PJ_FALSE);
+ buddy->sub = NULL;
+ pjsua_perror(THIS_FILE, "Unable to unsubscribe presence",
+ status);
+ }
+
+ pj_log_pop_indent();
+}
+
+/* It does what it says.. */
+static pj_status_t refresh_client_subscriptions(void)
+{
+ unsigned i;
+ pj_status_t status;
+
+ for (i=0; i<PJ_ARRAY_SIZE(pjsua_var.buddy); ++i) {
+ struct buddy_lock lck;
+
+ if (!pjsua_buddy_is_valid(i))
+ continue;
+
+ status = lock_buddy("refresh_client_subscriptions()", i, &lck, 0);
+ if (status != PJ_SUCCESS)
+ return status;
+
+ if (pjsua_var.buddy[i].monitor && !pjsua_var.buddy[i].sub) {
+ subscribe_buddy_presence(i);
+
+ } else if (!pjsua_var.buddy[i].monitor && pjsua_var.buddy[i].sub) {
+ unsubscribe_buddy_presence(i);
+
+ }
+
+ unlock_buddy(&lck);
+ }
+
+ return PJ_SUCCESS;
+}
+
+/***************************************************************************
+ * MWI
+ */
+/* Callback called when *client* subscription state has changed. */
+static void mwi_evsub_on_state( pjsip_evsub *sub, pjsip_event *event)
+{
+ pjsua_acc *acc;
+
+ PJ_UNUSED_ARG(event);
+
+ /* Note: #937: no need to acuire PJSUA_LOCK here. Since the buddy has
+ * a dialog attached to it, lock_buddy() will use the dialog
+ * lock, which we are currently holding!
+ */
+ acc = (pjsua_acc*) pjsip_evsub_get_mod_data(sub, pjsua_var.mod.id);
+ if (!acc)
+ return;
+
+ PJ_LOG(4,(THIS_FILE,
+ "MWI subscription for %.*s is %s",
+ (int)acc->cfg.id.slen, acc->cfg.id.ptr,
+ pjsip_evsub_get_state_name(sub)));
+
+ /* Call callback */
+ if (pjsua_var.ua_cfg.cb.on_mwi_state) {
+ (*pjsua_var.ua_cfg.cb.on_mwi_state)(acc->index, sub);
+ }
+
+ if (pjsip_evsub_get_state(sub) == PJSIP_EVSUB_STATE_TERMINATED) {
+ /* Clear subscription */
+ acc->mwi_dlg = NULL;
+ acc->mwi_sub = NULL;
+ pjsip_evsub_set_mod_data(sub, pjsua_var.mod.id, NULL);
+
+ }
+}
+
+/* Callback called when we receive NOTIFY */
+static void mwi_evsub_on_rx_notify(pjsip_evsub *sub,
+ pjsip_rx_data *rdata,
+ int *p_st_code,
+ pj_str_t **p_st_text,
+ pjsip_hdr *res_hdr,
+ pjsip_msg_body **p_body)
+{
+ pjsua_mwi_info mwi_info;
+ pjsua_acc *acc;
+
+ PJ_UNUSED_ARG(p_st_code);
+ PJ_UNUSED_ARG(p_st_text);
+ PJ_UNUSED_ARG(res_hdr);
+ PJ_UNUSED_ARG(p_body);
+
+ acc = (pjsua_acc*) pjsip_evsub_get_mod_data(sub, pjsua_var.mod.id);
+ if (!acc)
+ return;
+
+ /* Construct mwi_info */
+ pj_bzero(&mwi_info, sizeof(mwi_info));
+ mwi_info.evsub = sub;
+ mwi_info.rdata = rdata;
+
+ PJ_LOG(4,(THIS_FILE, "MWI got NOTIFY.."));
+ pj_log_push_indent();
+
+ /* Call callback */
+ if (pjsua_var.ua_cfg.cb.on_mwi_info) {
+ (*pjsua_var.ua_cfg.cb.on_mwi_info)(acc->index, &mwi_info);
+ }
+
+ pj_log_pop_indent();
+}
+
+
+/* Event subscription callback. */
+static pjsip_evsub_user mwi_cb =
+{
+ &mwi_evsub_on_state,
+ NULL, /* on_tsx_state: not interested */
+ NULL, /* on_rx_refresh: don't care about SUBSCRIBE refresh, unless
+ * we want to authenticate
+ */
+
+ &mwi_evsub_on_rx_notify,
+
+ NULL, /* on_client_refresh: Use default behaviour, which is to
+ * refresh client subscription. */
+
+ NULL, /* on_server_timeout: Use default behaviour, which is to send
+ * NOTIFY to terminate.
+ */
+};
+
+pj_status_t pjsua_start_mwi(pjsua_acc_id acc_id, pj_bool_t force_renew)
+{
+ pjsua_acc *acc;
+ pj_pool_t *tmp_pool = NULL;
+ pj_str_t contact;
+ pjsip_tx_data *tdata;
+ pj_status_t status = PJ_SUCCESS;
+
+ PJ_ASSERT_RETURN(acc_id>=0 && acc_id<(int)PJ_ARRAY_SIZE(pjsua_var.acc)
+ && pjsua_var.acc[acc_id].valid, PJ_EINVAL);
+
+ acc = &pjsua_var.acc[acc_id];
+
+ if (!acc->cfg.mwi_enabled) {
+ if (acc->mwi_sub) {
+ /* Terminate MWI subscription */
+ pjsip_evsub *sub = acc->mwi_sub;
+
+ /* Detach sub from this account */
+ acc->mwi_sub = NULL;
+ acc->mwi_dlg = NULL;
+ pjsip_evsub_set_mod_data(sub, pjsua_var.mod.id, NULL);
+
+ /* Unsubscribe */
+ status = pjsip_mwi_initiate(sub, 0, &tdata);
+ if (status == PJ_SUCCESS) {
+ status = pjsip_mwi_send_request(sub, tdata);
+ }
+ }
+ return status;
+ }
+
+ /* Subscription is already active */
+ if (acc->mwi_sub) {
+ if (!force_renew)
+ return PJ_SUCCESS;
+
+ /* Update MWI subscription */
+ pj_assert(acc->mwi_dlg);
+ pjsip_dlg_inc_lock(acc->mwi_dlg);
+
+ status = pjsip_mwi_initiate(acc->mwi_sub, acc->cfg.mwi_expires, &tdata);
+ if (status == PJ_SUCCESS) {
+ pjsua_process_msg_data(tdata, NULL);
+ status = pjsip_pres_send_request(acc->mwi_sub, tdata);
+ }
+
+ pjsip_dlg_dec_lock(acc->mwi_dlg);
+ return status;
+ }
+
+ PJ_LOG(4,(THIS_FILE, "Starting MWI subscription.."));
+ pj_log_push_indent();
+
+ /* Generate suitable Contact header unless one is already set in
+ * the account
+ */
+ if (acc->contact.slen) {
+ contact = acc->contact;
+ } else {
+ tmp_pool = pjsua_pool_create("tmpmwi", 512, 256);
+ status = pjsua_acc_create_uac_contact(tmp_pool, &contact,
+ acc->index, &acc->cfg.id);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Unable to generate Contact header",
+ status);
+ goto on_return;
+ }
+ }
+
+ /* Create UAC dialog */
+ status = pjsip_dlg_create_uac( pjsip_ua_instance(),
+ &acc->cfg.id,
+ &contact,
+ &acc->cfg.id,
+ NULL, &acc->mwi_dlg);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Unable to create dialog", status);
+ goto on_return;
+ }
+
+ /* Increment the dialog's lock otherwise when presence session creation
+ * fails the dialog will be destroyed prematurely.
+ */
+ pjsip_dlg_inc_lock(acc->mwi_dlg);
+
+ if (acc->cfg.allow_via_rewrite && acc->via_addr.host.slen > 0)
+ pjsip_dlg_set_via_sent_by(acc->mwi_dlg, &acc->via_addr, acc->via_tp);
+
+ /* Create UAC subscription */
+ status = pjsip_mwi_create_uac(acc->mwi_dlg, &mwi_cb,
+ PJSIP_EVSUB_NO_EVENT_ID, &acc->mwi_sub);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Error creating MWI subscription", status);
+ if (acc->mwi_dlg) pjsip_dlg_dec_lock(acc->mwi_dlg);
+ goto on_return;
+ }
+
+ /* If account is locked to specific transport, then lock dialog
+ * to this transport too.
+ */
+ if (acc->cfg.transport_id != PJSUA_INVALID_ID) {
+ pjsip_tpselector tp_sel;
+
+ pjsua_init_tpselector(acc->cfg.transport_id, &tp_sel);
+ pjsip_dlg_set_transport(acc->mwi_dlg, &tp_sel);
+ }
+
+ /* Set route-set */
+ if (!pj_list_empty(&acc->route_set)) {
+ pjsip_dlg_set_route_set(acc->mwi_dlg, &acc->route_set);
+ }
+
+ /* Set credentials */
+ if (acc->cred_cnt) {
+ pjsip_auth_clt_set_credentials( &acc->mwi_dlg->auth_sess,
+ acc->cred_cnt, acc->cred);
+ }
+
+ /* Set authentication preference */
+ pjsip_auth_clt_set_prefs(&acc->mwi_dlg->auth_sess, &acc->cfg.auth_pref);
+
+ pjsip_evsub_set_mod_data(acc->mwi_sub, pjsua_var.mod.id, acc);
+
+ status = pjsip_mwi_initiate(acc->mwi_sub, acc->cfg.mwi_expires, &tdata);
+ if (status != PJ_SUCCESS) {
+ if (acc->mwi_dlg) pjsip_dlg_dec_lock(acc->mwi_dlg);
+ if (acc->mwi_sub) {
+ pjsip_pres_terminate(acc->mwi_sub, PJ_FALSE);
+ }
+ acc->mwi_sub = NULL;
+ acc->mwi_dlg = NULL;
+ pjsua_perror(THIS_FILE, "Unable to create initial MWI SUBSCRIBE",
+ status);
+ goto on_return;
+ }
+
+ pjsua_process_msg_data(tdata, NULL);
+
+ status = pjsip_pres_send_request(acc->mwi_sub, tdata);
+ if (status != PJ_SUCCESS) {
+ if (acc->mwi_dlg) pjsip_dlg_dec_lock(acc->mwi_dlg);
+ if (acc->mwi_sub) {
+ pjsip_pres_terminate(acc->mwi_sub, PJ_FALSE);
+ }
+ acc->mwi_sub = NULL;
+ acc->mwi_dlg = NULL;
+ pjsua_perror(THIS_FILE, "Unable to send initial MWI SUBSCRIBE",
+ status);
+ goto on_return;
+ }
+
+ pjsip_dlg_dec_lock(acc->mwi_dlg);
+
+on_return:
+ if (tmp_pool) pj_pool_release(tmp_pool);
+
+ pj_log_pop_indent();
+ return status;
+}
+
+
+/***************************************************************************
+ * Unsolicited MWI
+ */
+static pj_bool_t unsolicited_mwi_on_rx_request(pjsip_rx_data *rdata)
+{
+ pjsip_msg *msg = rdata->msg_info.msg;
+ pj_str_t EVENT_HDR = { "Event", 5 };
+ pj_str_t MWI = { "message-summary", 15 };
+ pjsip_event_hdr *eh;
+
+ if (pjsip_method_cmp(&msg->line.req.method, &pjsip_notify_method)!=0) {
+ /* Only interested with NOTIFY request */
+ return PJ_FALSE;
+ }
+
+ eh = (pjsip_event_hdr*) pjsip_msg_find_hdr_by_name(msg, &EVENT_HDR, NULL);
+ if (!eh) {
+ /* Something wrong with the request, it has no Event hdr */
+ return PJ_FALSE;
+ }
+
+ if (pj_stricmp(&eh->event_type, &MWI) != 0) {
+ /* Not MWI event */
+ return PJ_FALSE;
+ }
+
+ PJ_LOG(4,(THIS_FILE, "Got unsolicited NOTIFY from %s:%d..",
+ rdata->pkt_info.src_name, rdata->pkt_info.src_port));
+ pj_log_push_indent();
+
+ /* Got unsolicited MWI request, respond with 200/OK first */
+ pjsip_endpt_respond(pjsua_get_pjsip_endpt(), NULL, rdata, 200, NULL,
+ NULL, NULL, NULL);
+
+
+ /* Call callback */
+ if (pjsua_var.ua_cfg.cb.on_mwi_info) {
+ pjsua_acc_id acc_id;
+ pjsua_mwi_info mwi_info;
+
+ acc_id = pjsua_acc_find_for_incoming(rdata);
+
+ pj_bzero(&mwi_info, sizeof(mwi_info));
+ mwi_info.rdata = rdata;
+
+ (*pjsua_var.ua_cfg.cb.on_mwi_info)(acc_id, &mwi_info);
+ }
+
+ pj_log_pop_indent();
+ return PJ_TRUE;
+}
+
+/* The module instance. */
+static pjsip_module pjsua_unsolicited_mwi_mod =
+{
+ NULL, NULL, /* prev, next. */
+ { "mod-unsolicited-mwi", 19 }, /* Name. */
+ -1, /* Id */
+ PJSIP_MOD_PRIORITY_APPLICATION, /* Priority */
+ NULL, /* load() */
+ NULL, /* start() */
+ NULL, /* stop() */
+ NULL, /* unload() */
+ &unsolicited_mwi_on_rx_request, /* on_rx_request() */
+ NULL, /* on_rx_response() */
+ NULL, /* on_tx_request. */
+ NULL, /* on_tx_response() */
+ NULL, /* on_tsx_state() */
+};
+
+static pj_status_t enable_unsolicited_mwi(void)
+{
+ pj_status_t status;
+
+ status = pjsip_endpt_register_module(pjsua_get_pjsip_endpt(),
+ &pjsua_unsolicited_mwi_mod);
+ if (status != PJ_SUCCESS)
+ pjsua_perror(THIS_FILE, "Error registering unsolicited MWI module",
+ status);
+
+ return status;
+}
+
+
+
+/***************************************************************************/
+
+/* Timer callback to re-create client subscription */
+static void pres_timer_cb(pj_timer_heap_t *th,
+ pj_timer_entry *entry)
+{
+ unsigned i;
+ pj_time_val delay = { PJSUA_PRES_TIMER, 0 };
+
+ entry->id = PJ_FALSE;
+
+ /* Retry failed PUBLISH and MWI SUBSCRIBE requests */
+ for (i=0; i<PJ_ARRAY_SIZE(pjsua_var.acc); ++i) {
+ pjsua_acc *acc = &pjsua_var.acc[i];
+
+ /* Acc may not be ready yet, otherwise assertion will happen */
+ if (!pjsua_acc_is_valid(i))
+ continue;
+
+ /* Retry PUBLISH */
+ if (acc->cfg.publish_enabled && acc->publish_sess==NULL)
+ pjsua_pres_init_publish_acc(acc->index);
+
+ /* Re-subscribe MWI subscription if it's terminated prematurely */
+ if (acc->cfg.mwi_enabled && !acc->mwi_sub)
+ pjsua_start_mwi(acc->index, PJ_FALSE);
+ }
+
+ /* #937: No need to do bulk client refresh, as buddies have their
+ * own individual timer now.
+ */
+ //refresh_client_subscriptions();
+
+ pjsip_endpt_schedule_timer(pjsua_var.endpt, entry, &delay);
+ entry->id = PJ_TRUE;
+
+ PJ_UNUSED_ARG(th);
+}
+
+
+/*
+ * Init presence
+ */
+pj_status_t pjsua_pres_init()
+{
+ unsigned i;
+ pj_status_t status;
+
+ status = pjsip_endpt_register_module( pjsua_var.endpt, &mod_pjsua_pres);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Unable to register pjsua presence module",
+ status);
+ }
+
+ for (i=0; i<PJ_ARRAY_SIZE(pjsua_var.buddy); ++i) {
+ reset_buddy(i);
+ }
+
+ return status;
+}
+
+
+/*
+ * Start presence subsystem.
+ */
+pj_status_t pjsua_pres_start(void)
+{
+ /* Start presence timer to re-subscribe to buddy's presence when
+ * subscription has failed.
+ */
+ if (pjsua_var.pres_timer.id == PJ_FALSE) {
+ pj_time_val pres_interval = {PJSUA_PRES_TIMER, 0};
+
+ pjsua_var.pres_timer.cb = &pres_timer_cb;
+ pjsip_endpt_schedule_timer(pjsua_var.endpt, &pjsua_var.pres_timer,
+ &pres_interval);
+ pjsua_var.pres_timer.id = PJ_TRUE;
+ }
+
+ if (pjsua_var.ua_cfg.enable_unsolicited_mwi) {
+ pj_status_t status = enable_unsolicited_mwi();
+ if (status != PJ_SUCCESS)
+ return status;
+ }
+
+ return PJ_SUCCESS;
+}
+
+
+/*
+ * Shutdown presence.
+ */
+void pjsua_pres_shutdown(unsigned flags)
+{
+ unsigned i;
+
+ PJ_LOG(4,(THIS_FILE, "Shutting down presence.."));
+ pj_log_push_indent();
+
+ if (pjsua_var.pres_timer.id != 0) {
+ pjsip_endpt_cancel_timer(pjsua_var.endpt, &pjsua_var.pres_timer);
+ pjsua_var.pres_timer.id = PJ_FALSE;
+ }
+
+ for (i=0; i<PJ_ARRAY_SIZE(pjsua_var.acc); ++i) {
+ if (!pjsua_var.acc[i].valid)
+ continue;
+ pjsua_pres_delete_acc(i, flags);
+ }
+
+ for (i=0; i<PJ_ARRAY_SIZE(pjsua_var.buddy); ++i) {
+ pjsua_var.buddy[i].monitor = 0;
+ }
+
+ if ((flags & PJSUA_DESTROY_NO_TX_MSG) == 0) {
+ refresh_client_subscriptions();
+
+ for (i=0; i<PJ_ARRAY_SIZE(pjsua_var.acc); ++i) {
+ if (pjsua_var.acc[i].valid)
+ pjsua_pres_update_acc(i, PJ_FALSE);
+ }
+ }
+
+ pj_log_pop_indent();
+}
diff --git a/pjsip/src/pjsua-lib/pjsua_vid.c b/pjsip/src/pjsua-lib/pjsua_vid.c
new file mode 100644
index 0000000..eb74ee1
--- /dev/null
+++ b/pjsip/src/pjsua-lib/pjsua_vid.c
@@ -0,0 +1,2166 @@
+/* $Id: pjsua_vid.c 4071 2012-04-24 05:40:32Z nanang $ */
+/*
+ * Copyright (C) 2011-2011 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
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ */
+#include <pjsua-lib/pjsua.h>
+#include <pjsua-lib/pjsua_internal.h>
+
+#if defined(PJSUA_MEDIA_HAS_PJMEDIA) && PJSUA_MEDIA_HAS_PJMEDIA != 0
+
+#define THIS_FILE "pjsua_vid.c"
+
+#if PJSUA_HAS_VIDEO
+
+#define ENABLE_EVENT 1
+#define VID_TEE_MAX_PORT (PJSUA_MAX_CALLS + 1)
+
+#define PJSUA_SHOW_WINDOW 1
+#define PJSUA_HIDE_WINDOW 0
+
+
+static void free_vid_win(pjsua_vid_win_id wid);
+
+/*****************************************************************************
+ * pjsua video subsystem.
+ */
+pj_status_t pjsua_vid_subsys_init(void)
+{
+ unsigned i;
+ pj_status_t status;
+
+ PJ_LOG(4,(THIS_FILE, "Initializing video subsystem.."));
+ pj_log_push_indent();
+
+ status = pjmedia_video_format_mgr_create(pjsua_var.pool, 64, 0, NULL);
+ if (status != PJ_SUCCESS) {
+ PJ_PERROR(1,(THIS_FILE, status,
+ "Error creating PJMEDIA video format manager"));
+ goto on_error;
+ }
+
+ status = pjmedia_converter_mgr_create(pjsua_var.pool, NULL);
+ if (status != PJ_SUCCESS) {
+ PJ_PERROR(1,(THIS_FILE, status,
+ "Error creating PJMEDIA converter manager"));
+ goto on_error;
+ }
+
+ status = pjmedia_event_mgr_create(pjsua_var.pool, 0, NULL);
+ if (status != PJ_SUCCESS) {
+ PJ_PERROR(1,(THIS_FILE, status,
+ "Error creating PJMEDIA event manager"));
+ goto on_error;
+ }
+
+ status = pjmedia_vid_codec_mgr_create(pjsua_var.pool, NULL);
+ if (status != PJ_SUCCESS) {
+ PJ_PERROR(1,(THIS_FILE, status,
+ "Error creating PJMEDIA video codec manager"));
+ goto on_error;
+ }
+
+#if PJMEDIA_HAS_VIDEO && PJMEDIA_HAS_FFMPEG_VID_CODEC
+ status = pjmedia_codec_ffmpeg_vid_init(NULL, &pjsua_var.cp.factory);
+ if (status != PJ_SUCCESS) {
+ PJ_PERROR(1,(THIS_FILE, status,
+ "Error initializing ffmpeg library"));
+ goto on_error;
+ }
+#endif
+
+ status = pjmedia_vid_dev_subsys_init(&pjsua_var.cp.factory);
+ if (status != PJ_SUCCESS) {
+ PJ_PERROR(1,(THIS_FILE, status,
+ "Error creating PJMEDIA video subsystem"));
+ goto on_error;
+ }
+
+ for (i=0; i<PJSUA_MAX_VID_WINS; ++i) {
+ if (pjsua_var.win[i].pool == NULL) {
+ pjsua_var.win[i].pool = pjsua_pool_create("win%p", 512, 512);
+ if (pjsua_var.win[i].pool == NULL) {
+ status = PJ_ENOMEM;
+ goto on_error;
+ }
+ }
+ }
+
+ pj_log_pop_indent();
+ return PJ_SUCCESS;
+
+on_error:
+ pj_log_pop_indent();
+ return status;
+}
+
+pj_status_t pjsua_vid_subsys_start(void)
+{
+ return PJ_SUCCESS;
+}
+
+pj_status_t pjsua_vid_subsys_destroy(void)
+{
+ unsigned i;
+
+ PJ_LOG(4,(THIS_FILE, "Destroying video subsystem.."));
+ pj_log_push_indent();
+
+ for (i=0; i<PJSUA_MAX_VID_WINS; ++i) {
+ if (pjsua_var.win[i].pool) {
+ free_vid_win(i);
+ pj_pool_release(pjsua_var.win[i].pool);
+ pjsua_var.win[i].pool = NULL;
+ }
+ }
+
+ pjmedia_vid_dev_subsys_shutdown();
+
+#if PJMEDIA_HAS_FFMPEG_VID_CODEC
+ pjmedia_codec_ffmpeg_vid_deinit();
+#endif
+
+ if (pjmedia_vid_codec_mgr_instance())
+ pjmedia_vid_codec_mgr_destroy(NULL);
+
+ if (pjmedia_converter_mgr_instance())
+ pjmedia_converter_mgr_destroy(NULL);
+
+ if (pjmedia_event_mgr_instance())
+ pjmedia_event_mgr_destroy(NULL);
+
+ if (pjmedia_video_format_mgr_instance())
+ pjmedia_video_format_mgr_destroy(NULL);
+
+ pj_log_pop_indent();
+ return PJ_SUCCESS;
+}
+
+PJ_DEF(const char*) pjsua_vid_win_type_name(pjsua_vid_win_type wt)
+{
+ const char *win_type_names[] = {
+ "none",
+ "preview",
+ "stream"
+ };
+
+ return (wt < PJ_ARRAY_SIZE(win_type_names)) ? win_type_names[wt] : "??";
+}
+
+PJ_DEF(void)
+pjsua_call_vid_strm_op_param_default(pjsua_call_vid_strm_op_param *param)
+{
+ pj_bzero(param, sizeof(*param));
+ param->med_idx = -1;
+ param->dir = PJMEDIA_DIR_ENCODING_DECODING;
+ param->cap_dev = PJMEDIA_VID_DEFAULT_CAPTURE_DEV;
+}
+
+PJ_DEF(void) pjsua_vid_preview_param_default(pjsua_vid_preview_param *p)
+{
+ p->rend_id = PJMEDIA_VID_DEFAULT_RENDER_DEV;
+ p->show = PJ_TRUE;
+ p->wnd_flags = 0;
+}
+
+
+/*****************************************************************************
+ * Devices.
+ */
+
+/*
+ * Get the number of video devices installed in the system.
+ */
+PJ_DEF(unsigned) pjsua_vid_dev_count(void)
+{
+ return pjmedia_vid_dev_count();
+}
+
+/*
+ * Retrieve the video device info for the specified device index.
+ */
+PJ_DEF(pj_status_t) pjsua_vid_dev_get_info(pjmedia_vid_dev_index id,
+ pjmedia_vid_dev_info *vdi)
+{
+ return pjmedia_vid_dev_get_info(id, vdi);
+}
+
+/*
+ * Enum all video devices installed in the system.
+ */
+PJ_DEF(pj_status_t) pjsua_vid_enum_devs(pjmedia_vid_dev_info info[],
+ unsigned *count)
+{
+ unsigned i, dev_count;
+
+ dev_count = pjmedia_vid_dev_count();
+
+ if (dev_count > *count) dev_count = *count;
+
+ for (i=0; i<dev_count; ++i) {
+ pj_status_t status;
+
+ status = pjmedia_vid_dev_get_info(i, &info[i]);
+ if (status != PJ_SUCCESS)
+ return status;
+ }
+
+ *count = dev_count;
+
+ return PJ_SUCCESS;
+}
+
+
+/*****************************************************************************
+ * Codecs.
+ */
+
+static pj_status_t find_codecs_with_rtp_packing(
+ const pj_str_t *codec_id,
+ unsigned *count,
+ const pjmedia_vid_codec_info *p_info[])
+{
+ const pjmedia_vid_codec_info *info[32];
+ unsigned i, j, count_ = PJ_ARRAY_SIZE(info);
+ pj_status_t status;
+
+ status = pjmedia_vid_codec_mgr_find_codecs_by_id(NULL, codec_id,
+ &count_, info, NULL);
+ if (status != PJ_SUCCESS)
+ return status;
+
+ for (i = 0, j = 0; i < count_ && j<*count; ++i) {
+ if ((info[i]->packings & PJMEDIA_VID_PACKING_PACKETS) == 0)
+ continue;
+ p_info[j++] = info[i];
+ }
+ *count = j;
+ return PJ_SUCCESS;
+}
+
+/*
+ * Enum all supported video codecs in the system.
+ */
+PJ_DEF(pj_status_t) pjsua_vid_enum_codecs( pjsua_codec_info id[],
+ unsigned *p_count )
+{
+ pjmedia_vid_codec_info info[32];
+ unsigned i, j, count, prio[32];
+ pj_status_t status;
+
+ count = PJ_ARRAY_SIZE(info);
+ status = pjmedia_vid_codec_mgr_enum_codecs(NULL, &count, info, prio);
+ if (status != PJ_SUCCESS) {
+ *p_count = 0;
+ return status;
+ }
+
+ for (i=0, j=0; i<count && j<*p_count; ++i) {
+ if (info[i].packings & PJMEDIA_VID_PACKING_PACKETS) {
+ pj_bzero(&id[j], sizeof(pjsua_codec_info));
+
+ pjmedia_vid_codec_info_to_id(&info[i], id[j].buf_, sizeof(id[j].buf_));
+ id[j].codec_id = pj_str(id[j].buf_);
+ id[j].priority = (pj_uint8_t) prio[i];
+
+ if (id[j].codec_id.slen < sizeof(id[j].buf_)) {
+ id[j].desc.ptr = id[j].codec_id.ptr + id[j].codec_id.slen + 1;
+ pj_strncpy(&id[j].desc, &info[i].encoding_desc,
+ sizeof(id[j].buf_) - id[j].codec_id.slen - 1);
+ }
+
+ ++j;
+ }
+ }
+
+ *p_count = j;
+
+ return PJ_SUCCESS;
+}
+
+
+/*
+ * Change video codec priority.
+ */
+PJ_DEF(pj_status_t) pjsua_vid_codec_set_priority( const pj_str_t *codec_id,
+ pj_uint8_t priority )
+{
+ const pj_str_t all = { NULL, 0 };
+
+ if (codec_id->slen==1 && *codec_id->ptr=='*')
+ codec_id = &all;
+
+ return pjmedia_vid_codec_mgr_set_codec_priority(NULL, codec_id,
+ priority);
+}
+
+
+/*
+ * Get video codec parameters.
+ */
+PJ_DEF(pj_status_t) pjsua_vid_codec_get_param(
+ const pj_str_t *codec_id,
+ pjmedia_vid_codec_param *param)
+{
+ const pjmedia_vid_codec_info *info[2];
+ unsigned count = 2;
+ pj_status_t status;
+
+ status = find_codecs_with_rtp_packing(codec_id, &count, info);
+ if (status != PJ_SUCCESS)
+ return status;
+
+ if (count != 1)
+ return (count > 1? PJ_ETOOMANY : PJ_ENOTFOUND);
+
+ status = pjmedia_vid_codec_mgr_get_default_param(NULL, info[0], param);
+ return status;
+}
+
+
+/*
+ * Set video codec parameters.
+ */
+PJ_DEF(pj_status_t) pjsua_vid_codec_set_param(
+ const pj_str_t *codec_id,
+ const pjmedia_vid_codec_param *param)
+{
+ const pjmedia_vid_codec_info *info[2];
+ unsigned count = 2;
+ pj_status_t status;
+
+ status = find_codecs_with_rtp_packing(codec_id, &count, info);
+ if (status != PJ_SUCCESS)
+ return status;
+
+ if (count != 1)
+ return (count > 1? PJ_ETOOMANY : PJ_ENOTFOUND);
+
+ status = pjmedia_vid_codec_mgr_set_default_param(NULL, info[0], param);
+ return status;
+}
+
+
+/*****************************************************************************
+ * Preview
+ */
+
+static pjsua_vid_win_id vid_preview_get_win(pjmedia_vid_dev_index id,
+ pj_bool_t running_only)
+{
+ pjsua_vid_win_id wid = PJSUA_INVALID_ID;
+ unsigned i;
+
+ PJSUA_LOCK();
+
+ /* Get real capture ID, if set to PJMEDIA_VID_DEFAULT_CAPTURE_DEV */
+ if (id == PJMEDIA_VID_DEFAULT_CAPTURE_DEV) {
+ pjmedia_vid_dev_info info;
+ pjmedia_vid_dev_get_info(id, &info);
+ id = info.id;
+ }
+
+ for (i=0; i<PJSUA_MAX_VID_WINS; ++i) {
+ pjsua_vid_win *w = &pjsua_var.win[i];
+ if (w->type == PJSUA_WND_TYPE_PREVIEW && w->preview_cap_id == id) {
+ wid = i;
+ break;
+ }
+ }
+
+ if (wid != PJSUA_INVALID_ID && running_only) {
+ pjsua_vid_win *w = &pjsua_var.win[wid];
+ wid = w->preview_running ? wid : PJSUA_INVALID_ID;
+ }
+
+ PJSUA_UNLOCK();
+
+ return wid;
+}
+
+/*
+ * NOTE: internal function don't use this!!! Use vid_preview_get_win()
+ * instead. This is because this function will only return window ID
+ * if preview is currently running.
+ */
+PJ_DEF(pjsua_vid_win_id) pjsua_vid_preview_get_win(pjmedia_vid_dev_index id)
+{
+ return vid_preview_get_win(id, PJ_TRUE);
+}
+
+PJ_DEF(void) pjsua_vid_win_reset(pjsua_vid_win_id wid)
+{
+ pjsua_vid_win *w = &pjsua_var.win[wid];
+ pj_pool_t *pool = w->pool;
+
+ pj_bzero(w, sizeof(*w));
+ if (pool) pj_pool_reset(pool);
+ w->ref_cnt = 0;
+ w->pool = pool;
+ w->preview_cap_id = PJMEDIA_VID_INVALID_DEV;
+}
+
+/* Allocate and initialize pjsua video window:
+ * - If the type is preview, video capture, tee, and render
+ * will be instantiated.
+ * - If the type is stream, only renderer will be created.
+ */
+static pj_status_t create_vid_win(pjsua_vid_win_type type,
+ const pjmedia_format *fmt,
+ pjmedia_vid_dev_index rend_id,
+ pjmedia_vid_dev_index cap_id,
+ pj_bool_t show,
+ unsigned wnd_flags,
+ pjsua_vid_win_id *id)
+{
+ pj_bool_t enable_native_preview;
+ pjsua_vid_win_id wid = PJSUA_INVALID_ID;
+ pjsua_vid_win *w = NULL;
+ pjmedia_vid_port_param vp_param;
+ pjmedia_format fmt_;
+ pj_status_t status;
+ unsigned i;
+
+ enable_native_preview = pjsua_var.media_cfg.vid_preview_enable_native;
+
+ PJ_LOG(4,(THIS_FILE,
+ "Creating video window: type=%s, cap_id=%d, rend_id=%d",
+ pjsua_vid_win_type_name(type), cap_id, rend_id));
+ pj_log_push_indent();
+
+ /* If type is preview, check if it exists already */
+ if (type == PJSUA_WND_TYPE_PREVIEW) {
+ wid = vid_preview_get_win(cap_id, PJ_FALSE);
+ if (wid != PJSUA_INVALID_ID) {
+ /* Yes, it exists */
+ /* Show/hide window */
+ pjmedia_vid_dev_stream *strm;
+ pj_bool_t hide = !show;
+
+ w = &pjsua_var.win[wid];
+
+ PJ_LOG(4,(THIS_FILE,
+ "Window already exists for cap_dev=%d, returning wid=%d",
+ cap_id, wid));
+
+
+ if (w->is_native) {
+ strm = pjmedia_vid_port_get_stream(w->vp_cap);
+ } else {
+ strm = pjmedia_vid_port_get_stream(w->vp_rend);
+ }
+
+ pj_assert(strm);
+ status = pjmedia_vid_dev_stream_set_cap(
+ strm, PJMEDIA_VID_DEV_CAP_OUTPUT_HIDE,
+ &hide);
+
+ pjmedia_vid_dev_stream_set_cap(
+ strm, PJMEDIA_VID_DEV_CAP_OUTPUT_WINDOW_FLAGS,
+ &wnd_flags);
+
+ /* Done */
+ *id = wid;
+ pj_log_pop_indent();
+
+ return status;
+ }
+ }
+
+ /* Allocate window */
+ for (i=0; i<PJSUA_MAX_VID_WINS; ++i) {
+ w = &pjsua_var.win[i];
+ if (w->type == PJSUA_WND_TYPE_NONE) {
+ wid = i;
+ w->type = type;
+ break;
+ }
+ }
+ if (i == PJSUA_MAX_VID_WINS) {
+ pj_log_pop_indent();
+ return PJ_ETOOMANY;
+ }
+
+ /* Initialize window */
+ pjmedia_vid_port_param_default(&vp_param);
+
+ if (w->type == PJSUA_WND_TYPE_PREVIEW) {
+ pjmedia_vid_dev_info vdi;
+
+ /*
+ * Determine if the device supports native preview.
+ */
+ status = pjmedia_vid_dev_get_info(cap_id, &vdi);
+ if (status != PJ_SUCCESS)
+ goto on_error;
+
+ if (enable_native_preview &&
+ (vdi.caps & PJMEDIA_VID_DEV_CAP_INPUT_PREVIEW))
+ {
+ /* Device supports native preview! */
+ w->is_native = PJ_TRUE;
+ }
+
+ status = pjmedia_vid_dev_default_param(w->pool, cap_id,
+ &vp_param.vidparam);
+ if (status != PJ_SUCCESS)
+ goto on_error;
+
+ if (w->is_native) {
+ vp_param.vidparam.flags |= PJMEDIA_VID_DEV_CAP_OUTPUT_HIDE;
+ vp_param.vidparam.window_hide = !show;
+ vp_param.vidparam.flags |= PJMEDIA_VID_DEV_CAP_OUTPUT_WINDOW_FLAGS;
+ vp_param.vidparam.window_flags = wnd_flags;
+ }
+
+ /* Normalize capture ID, in case it was set to
+ * PJMEDIA_VID_DEFAULT_CAPTURE_DEV
+ */
+ cap_id = vp_param.vidparam.cap_id;
+
+ /* Assign preview capture device ID */
+ w->preview_cap_id = cap_id;
+
+ /* Create capture video port */
+ vp_param.active = PJ_TRUE;
+ vp_param.vidparam.dir = PJMEDIA_DIR_CAPTURE;
+ if (fmt)
+ vp_param.vidparam.fmt = *fmt;
+
+ status = pjmedia_vid_port_create(w->pool, &vp_param, &w->vp_cap);
+ if (status != PJ_SUCCESS)
+ goto on_error;
+
+ /* Update format info */
+ fmt_ = vp_param.vidparam.fmt;
+ fmt = &fmt_;
+
+ /* Create video tee */
+ status = pjmedia_vid_tee_create(w->pool, fmt, VID_TEE_MAX_PORT,
+ &w->tee);
+ if (status != PJ_SUCCESS)
+ goto on_error;
+
+ /* Connect capturer to the video tee */
+ status = pjmedia_vid_port_connect(w->vp_cap, w->tee, PJ_FALSE);
+ if (status != PJ_SUCCESS)
+ goto on_error;
+
+ /* If device supports native preview, enable it */
+ if (w->is_native) {
+ pjmedia_vid_dev_stream *cap_dev;
+ pj_bool_t enabled = PJ_TRUE;
+
+ cap_dev = pjmedia_vid_port_get_stream(w->vp_cap);
+ status = pjmedia_vid_dev_stream_set_cap(
+ cap_dev, PJMEDIA_VID_DEV_CAP_INPUT_PREVIEW,
+ &enabled);
+ if (status != PJ_SUCCESS) {
+ PJ_PERROR(1,(THIS_FILE, status,
+ "Error activating native preview, falling back "
+ "to software preview.."));
+ w->is_native = PJ_FALSE;
+ }
+ }
+ }
+
+ /* Create renderer video port, only if it's not a native preview */
+ if (!w->is_native) {
+ status = pjmedia_vid_dev_default_param(w->pool, rend_id,
+ &vp_param.vidparam);
+ if (status != PJ_SUCCESS)
+ goto on_error;
+
+ vp_param.active = (w->type == PJSUA_WND_TYPE_STREAM);
+ vp_param.vidparam.dir = PJMEDIA_DIR_RENDER;
+ vp_param.vidparam.fmt = *fmt;
+ vp_param.vidparam.disp_size = fmt->det.vid.size;
+ vp_param.vidparam.flags |= PJMEDIA_VID_DEV_CAP_OUTPUT_HIDE;
+ vp_param.vidparam.window_hide = !show;
+ vp_param.vidparam.flags |= PJMEDIA_VID_DEV_CAP_OUTPUT_WINDOW_FLAGS;
+ vp_param.vidparam.window_flags = wnd_flags;
+
+ status = pjmedia_vid_port_create(w->pool, &vp_param, &w->vp_rend);
+ if (status != PJ_SUCCESS)
+ goto on_error;
+
+ /* For preview window, connect capturer & renderer (via tee) */
+ if (w->type == PJSUA_WND_TYPE_PREVIEW) {
+ pjmedia_port *rend_port;
+
+ rend_port = pjmedia_vid_port_get_passive_port(w->vp_rend);
+ status = pjmedia_vid_tee_add_dst_port2(w->tee, 0, rend_port);
+ if (status != PJ_SUCCESS)
+ goto on_error;
+ }
+
+ PJ_LOG(4,(THIS_FILE,
+ "%s window id %d created for cap_dev=%d rend_dev=%d",
+ pjsua_vid_win_type_name(type), wid, cap_id, rend_id));
+ } else {
+ PJ_LOG(4,(THIS_FILE,
+ "Preview window id %d created for cap_dev %d, "
+ "using built-in preview!",
+ wid, cap_id));
+ }
+
+
+ /* Done */
+ *id = wid;
+
+ PJ_LOG(4,(THIS_FILE, "Window %d created", wid));
+ pj_log_pop_indent();
+ return PJ_SUCCESS;
+
+on_error:
+ free_vid_win(wid);
+ pj_log_pop_indent();
+ return status;
+}
+
+
+static void free_vid_win(pjsua_vid_win_id wid)
+{
+ pjsua_vid_win *w = &pjsua_var.win[wid];
+
+ PJ_LOG(4,(THIS_FILE, "Window %d: destroying..", wid));
+ pj_log_push_indent();
+
+ if (w->vp_cap) {
+ pjmedia_event_unsubscribe(NULL, &call_media_on_event, NULL,
+ w->vp_cap);
+ pjmedia_vid_port_stop(w->vp_cap);
+ pjmedia_vid_port_disconnect(w->vp_cap);
+ pjmedia_vid_port_destroy(w->vp_cap);
+ }
+ if (w->vp_rend) {
+ pjmedia_event_unsubscribe(NULL, &call_media_on_event, NULL,
+ w->vp_rend);
+ pjmedia_vid_port_stop(w->vp_rend);
+ pjmedia_vid_port_destroy(w->vp_rend);
+ }
+ if (w->tee) {
+ pjmedia_port_destroy(w->tee);
+ }
+ pjsua_vid_win_reset(wid);
+
+ pj_log_pop_indent();
+}
+
+
+static void inc_vid_win(pjsua_vid_win_id wid)
+{
+ pjsua_vid_win *w;
+
+ pj_assert(wid >= 0 && wid < PJSUA_MAX_VID_WINS);
+
+ w = &pjsua_var.win[wid];
+ pj_assert(w->type != PJSUA_WND_TYPE_NONE);
+ ++w->ref_cnt;
+}
+
+static void dec_vid_win(pjsua_vid_win_id wid)
+{
+ pjsua_vid_win *w;
+
+ pj_assert(wid >= 0 && wid < PJSUA_MAX_VID_WINS);
+
+ w = &pjsua_var.win[wid];
+ pj_assert(w->type != PJSUA_WND_TYPE_NONE);
+ if (--w->ref_cnt == 0)
+ free_vid_win(wid);
+}
+
+/* Initialize video call media */
+pj_status_t pjsua_vid_channel_init(pjsua_call_media *call_med)
+{
+ pjsua_acc *acc = &pjsua_var.acc[call_med->call->acc_id];
+
+ call_med->strm.v.rdr_dev = acc->cfg.vid_rend_dev;
+ call_med->strm.v.cap_dev = acc->cfg.vid_cap_dev;
+ if (call_med->strm.v.rdr_dev == PJMEDIA_VID_DEFAULT_RENDER_DEV) {
+ pjmedia_vid_dev_info info;
+ pjmedia_vid_dev_get_info(call_med->strm.v.rdr_dev, &info);
+ call_med->strm.v.rdr_dev = info.id;
+ }
+ if (call_med->strm.v.cap_dev == PJMEDIA_VID_DEFAULT_CAPTURE_DEV) {
+ pjmedia_vid_dev_info info;
+ pjmedia_vid_dev_get_info(call_med->strm.v.cap_dev, &info);
+ call_med->strm.v.cap_dev = info.id;
+ }
+
+ return PJ_SUCCESS;
+}
+
+/* Internal function: update video channel after SDP negotiation */
+pj_status_t pjsua_vid_channel_update(pjsua_call_media *call_med,
+ pj_pool_t *tmp_pool,
+ pjmedia_vid_stream_info *si,
+ const pjmedia_sdp_session *local_sdp,
+ const pjmedia_sdp_session *remote_sdp)
+{
+ pjsua_call *call = call_med->call;
+ pjsua_acc *acc = &pjsua_var.acc[call->acc_id];
+ pjmedia_port *media_port;
+ pj_status_t status;
+
+ PJ_UNUSED_ARG(tmp_pool);
+ PJ_UNUSED_ARG(local_sdp);
+ PJ_UNUSED_ARG(remote_sdp);
+
+ PJ_LOG(4,(THIS_FILE, "Video channel update.."));
+ pj_log_push_indent();
+
+ si->rtcp_sdes_bye_disabled = PJ_TRUE;
+
+ /* Check if no media is active */
+ if (si->dir != PJMEDIA_DIR_NONE) {
+ /* Optionally, application may modify other stream settings here
+ * (such as jitter buffer parameters, codec ptime, etc.)
+ */
+ si->jb_init = pjsua_var.media_cfg.jb_init;
+ si->jb_min_pre = pjsua_var.media_cfg.jb_min_pre;
+ si->jb_max_pre = pjsua_var.media_cfg.jb_max_pre;
+ si->jb_max = pjsua_var.media_cfg.jb_max;
+
+ /* Set SSRC */
+ si->ssrc = call_med->ssrc;
+
+ /* Set RTP timestamp & sequence, normally these value are intialized
+ * automatically when stream session created, but for some cases (e.g:
+ * call reinvite, call update) timestamp and sequence need to be kept
+ * contigue.
+ */
+ si->rtp_ts = call_med->rtp_tx_ts;
+ si->rtp_seq = call_med->rtp_tx_seq;
+ si->rtp_seq_ts_set = call_med->rtp_tx_seq_ts_set;
+
+ /* Set rate control config from account setting */
+ si->rc_cfg = acc->cfg.vid_stream_rc_cfg;
+
+#if defined(PJMEDIA_STREAM_ENABLE_KA) && PJMEDIA_STREAM_ENABLE_KA!=0
+ /* Enable/disable stream keep-alive and NAT hole punch. */
+ si->use_ka = pjsua_var.acc[call->acc_id].cfg.use_stream_ka;
+#endif
+
+ /* Try to get shared format ID between the capture device and
+ * the encoder to avoid format conversion in the capture device.
+ */
+ if (si->dir & PJMEDIA_DIR_ENCODING) {
+ pjmedia_vid_dev_info dev_info;
+ pjmedia_vid_codec_info *codec_info = &si->codec_info;
+ unsigned i, j;
+
+ status = pjmedia_vid_dev_get_info(call_med->strm.v.cap_dev,
+ &dev_info);
+ if (status != PJ_SUCCESS)
+ goto on_error;
+
+ /* Find matched format ID */
+ for (i = 0; i < codec_info->dec_fmt_id_cnt; ++i) {
+ for (j = 0; j < dev_info.fmt_cnt; ++j) {
+ if (codec_info->dec_fmt_id[i] ==
+ (pjmedia_format_id)dev_info.fmt[j].id)
+ {
+ /* Apply the matched format ID to the codec */
+ si->codec_param->dec_fmt.id =
+ codec_info->dec_fmt_id[i];
+
+ /* Force outer loop to break */
+ i = codec_info->dec_fmt_id_cnt;
+ break;
+ }
+ }
+ }
+ }
+
+ /* Create session based on session info. */
+ status = pjmedia_vid_stream_create(pjsua_var.med_endpt, NULL, si,
+ call_med->tp, NULL,
+ &call_med->strm.v.stream);
+ if (status != PJ_SUCCESS)
+ goto on_error;
+
+ /* Start stream */
+ status = pjmedia_vid_stream_start(call_med->strm.v.stream);
+ if (status != PJ_SUCCESS)
+ goto on_error;
+
+ /* Setup decoding direction */
+ if (si->dir & PJMEDIA_DIR_DECODING)
+ {
+ pjsua_vid_win_id wid;
+ pjsua_vid_win *w;
+
+ PJ_LOG(4,(THIS_FILE, "Setting up RX.."));
+ pj_log_push_indent();
+
+ status = pjmedia_vid_stream_get_port(call_med->strm.v.stream,
+ PJMEDIA_DIR_DECODING,
+ &media_port);
+ if (status != PJ_SUCCESS) {
+ pj_log_pop_indent();
+ goto on_error;
+ }
+
+ /* Create stream video window */
+ status = create_vid_win(PJSUA_WND_TYPE_STREAM,
+ &media_port->info.fmt,
+ call_med->strm.v.rdr_dev,
+ //acc->cfg.vid_rend_dev,
+ PJSUA_INVALID_ID,
+ acc->cfg.vid_in_auto_show,
+ acc->cfg.vid_wnd_flags,
+ &wid);
+ if (status != PJ_SUCCESS) {
+ pj_log_pop_indent();
+ goto on_error;
+ }
+
+ w = &pjsua_var.win[wid];
+
+#if ENABLE_EVENT
+ /* Register to video events */
+ pjmedia_event_subscribe(NULL, &call_media_on_event,
+ call_med, w->vp_rend);
+#endif
+
+ /* Connect renderer to stream */
+ status = pjmedia_vid_port_connect(w->vp_rend, media_port,
+ PJ_FALSE);
+ if (status != PJ_SUCCESS) {
+ pj_log_pop_indent();
+ goto on_error;
+ }
+
+ /* Start renderer */
+ status = pjmedia_vid_port_start(w->vp_rend);
+ if (status != PJ_SUCCESS) {
+ pj_log_pop_indent();
+ goto on_error;
+ }
+
+ /* Done */
+ inc_vid_win(wid);
+ call_med->strm.v.rdr_win_id = wid;
+ pj_log_pop_indent();
+ }
+
+ /* Setup encoding direction */
+ if (si->dir & PJMEDIA_DIR_ENCODING && !call->local_hold)
+ {
+ pjsua_acc *acc = &pjsua_var.acc[call_med->call->acc_id];
+ pjsua_vid_win *w;
+ pjsua_vid_win_id wid;
+ pj_bool_t just_created = PJ_FALSE;
+
+ PJ_LOG(4,(THIS_FILE, "Setting up TX.."));
+ pj_log_push_indent();
+
+ status = pjmedia_vid_stream_get_port(call_med->strm.v.stream,
+ PJMEDIA_DIR_ENCODING,
+ &media_port);
+ if (status != PJ_SUCCESS) {
+ pj_log_pop_indent();
+ goto on_error;
+ }
+
+ /* Note: calling pjsua_vid_preview_get_win() even though
+ * create_vid_win() will automatically create the window
+ * if it doesn't exist, because create_vid_win() will modify
+ * existing window SHOW/HIDE value.
+ */
+ wid = vid_preview_get_win(call_med->strm.v.cap_dev, PJ_FALSE);
+ if (wid == PJSUA_INVALID_ID) {
+ /* Create preview video window */
+ status = create_vid_win(PJSUA_WND_TYPE_PREVIEW,
+ &media_port->info.fmt,
+ call_med->strm.v.rdr_dev,
+ call_med->strm.v.cap_dev,
+ //acc->cfg.vid_rend_dev,
+ //acc->cfg.vid_cap_dev,
+ PJSUA_HIDE_WINDOW,
+ acc->cfg.vid_wnd_flags,
+ &wid);
+ if (status != PJ_SUCCESS) {
+ pj_log_pop_indent();
+ return status;
+ }
+ just_created = PJ_TRUE;
+ }
+
+ w = &pjsua_var.win[wid];
+#if ENABLE_EVENT
+ pjmedia_event_subscribe(NULL, &call_media_on_event,
+ call_med, w->vp_cap);
+#endif
+
+ /* Connect stream to capturer (via video window tee) */
+ status = pjmedia_vid_tee_add_dst_port2(w->tee, 0, media_port);
+ if (status != PJ_SUCCESS) {
+ pj_log_pop_indent();
+ goto on_error;
+ }
+
+ /* Start capturer */
+ if (just_created) {
+ status = pjmedia_vid_port_start(w->vp_cap);
+ if (status != PJ_SUCCESS) {
+ pj_log_pop_indent();
+ goto on_error;
+ }
+ }
+
+ /* Done */
+ inc_vid_win(wid);
+ call_med->strm.v.cap_win_id = wid;
+ pj_log_pop_indent();
+ }
+
+ }
+
+ if (!acc->cfg.vid_out_auto_transmit && call_med->strm.v.stream) {
+ status = pjmedia_vid_stream_pause(call_med->strm.v.stream,
+ PJMEDIA_DIR_ENCODING);
+ if (status != PJ_SUCCESS)
+ goto on_error;
+ }
+
+ pj_log_pop_indent();
+ return PJ_SUCCESS;
+
+on_error:
+ pj_log_pop_indent();
+ return status;
+}
+
+
+/* Internal function to stop video stream */
+void pjsua_vid_stop_stream(pjsua_call_media *call_med)
+{
+ pjmedia_vid_stream *strm = call_med->strm.v.stream;
+ pjmedia_rtcp_stat stat;
+
+ pj_assert(call_med->type == PJMEDIA_TYPE_VIDEO);
+
+ if (!strm)
+ return;
+
+ PJ_LOG(4,(THIS_FILE, "Stopping video stream.."));
+ pj_log_push_indent();
+
+ if (call_med->strm.v.cap_win_id != PJSUA_INVALID_ID) {
+ pjmedia_port *media_port;
+ pjsua_vid_win *w = &pjsua_var.win[call_med->strm.v.cap_win_id];
+ pj_status_t status;
+
+ /* Stop the capture before detaching stream and unsubscribing event */
+ pjmedia_vid_port_stop(w->vp_cap);
+
+ /* Disconnect video stream from capture device */
+ status = pjmedia_vid_stream_get_port(call_med->strm.v.stream,
+ PJMEDIA_DIR_ENCODING,
+ &media_port);
+ if (status == PJ_SUCCESS) {
+ pjmedia_vid_tee_remove_dst_port(w->tee, media_port);
+ }
+
+ /* Unsubscribe event */
+ pjmedia_event_unsubscribe(NULL, &call_media_on_event, call_med,
+ w->vp_cap);
+
+ /* Re-start capture again, if it is used by other stream */
+ if (w->ref_cnt > 1)
+ pjmedia_vid_port_start(w->vp_cap);
+
+ dec_vid_win(call_med->strm.v.cap_win_id);
+ call_med->strm.v.cap_win_id = PJSUA_INVALID_ID;
+ }
+
+ if (call_med->strm.v.rdr_win_id != PJSUA_INVALID_ID) {
+ pjsua_vid_win *w = &pjsua_var.win[call_med->strm.v.rdr_win_id];
+
+ /* Stop the render before unsubscribing event */
+ pjmedia_vid_port_stop(w->vp_rend);
+ pjmedia_event_unsubscribe(NULL, &call_media_on_event, call_med,
+ w->vp_rend);
+
+ dec_vid_win(call_med->strm.v.rdr_win_id);
+ call_med->strm.v.rdr_win_id = PJSUA_INVALID_ID;
+ }
+
+ if ((call_med->dir & PJMEDIA_DIR_ENCODING) &&
+ (pjmedia_vid_stream_get_stat(strm, &stat) == PJ_SUCCESS))
+ {
+ /* Save RTP timestamp & sequence, so when media session is
+ * restarted, those values will be restored as the initial
+ * RTP timestamp & sequence of the new media session. So in
+ * the same call session, RTP timestamp and sequence are
+ * guaranteed to be contigue.
+ */
+ call_med->rtp_tx_seq_ts_set = 1 | (1 << 1);
+ call_med->rtp_tx_seq = stat.rtp_tx_last_seq;
+ call_med->rtp_tx_ts = stat.rtp_tx_last_ts;
+ }
+
+ pjmedia_vid_stream_destroy(strm);
+ call_med->strm.v.stream = NULL;
+
+ pj_log_pop_indent();
+}
+
+/*
+ * Does it have built-in preview support.
+ */
+PJ_DEF(pj_bool_t) pjsua_vid_preview_has_native(pjmedia_vid_dev_index id)
+{
+ pjmedia_vid_dev_info vdi;
+
+ return (pjmedia_vid_dev_get_info(id, &vdi)==PJ_SUCCESS) ?
+ ((vdi.caps & PJMEDIA_VID_DEV_CAP_INPUT_PREVIEW)!=0) : PJ_FALSE;
+}
+
+/*
+ * Start video preview window for the specified capture device.
+ */
+PJ_DEF(pj_status_t) pjsua_vid_preview_start(pjmedia_vid_dev_index id,
+ const pjsua_vid_preview_param *prm)
+{
+ pjsua_vid_win_id wid;
+ pjsua_vid_win *w;
+ pjmedia_vid_dev_index rend_id;
+ pjsua_vid_preview_param default_param;
+ pj_status_t status;
+
+ if (!prm) {
+ pjsua_vid_preview_param_default(&default_param);
+ prm = &default_param;
+ }
+
+ PJ_LOG(4,(THIS_FILE, "Starting preview for cap_dev=%d, show=%d",
+ id, prm->show));
+ pj_log_push_indent();
+
+ PJSUA_LOCK();
+
+ rend_id = prm->rend_id;
+
+ status = create_vid_win(PJSUA_WND_TYPE_PREVIEW, NULL, rend_id, id,
+ prm->show, prm->wnd_flags, &wid);
+ if (status != PJ_SUCCESS) {
+ PJSUA_UNLOCK();
+ pj_log_pop_indent();
+ return status;
+ }
+
+ w = &pjsua_var.win[wid];
+ if (w->preview_running) {
+ PJSUA_UNLOCK();
+ pj_log_pop_indent();
+ return PJ_SUCCESS;
+ }
+
+ /* Start renderer, unless it's native preview */
+ if (w->is_native && !pjmedia_vid_port_is_running(w->vp_cap)) {
+ pjmedia_vid_dev_stream *cap_dev;
+ pj_bool_t enabled = PJ_TRUE;
+
+ cap_dev = pjmedia_vid_port_get_stream(w->vp_cap);
+ status = pjmedia_vid_dev_stream_set_cap(
+ cap_dev, PJMEDIA_VID_DEV_CAP_INPUT_PREVIEW,
+ &enabled);
+ if (status != PJ_SUCCESS) {
+ PJ_PERROR(1,(THIS_FILE, status,
+ "Error activating native preview, falling back "
+ "to software preview.."));
+ w->is_native = PJ_FALSE;
+ }
+ }
+
+ if (!w->is_native && !pjmedia_vid_port_is_running(w->vp_rend)) {
+ status = pjmedia_vid_port_start(w->vp_rend);
+ if (status != PJ_SUCCESS) {
+ PJSUA_UNLOCK();
+ pj_log_pop_indent();
+ return status;
+ }
+ }
+
+ /* Start capturer */
+ if (!pjmedia_vid_port_is_running(w->vp_cap)) {
+ status = pjmedia_vid_port_start(w->vp_cap);
+ if (status != PJ_SUCCESS) {
+ PJSUA_UNLOCK();
+ pj_log_pop_indent();
+ return status;
+ }
+ }
+
+ inc_vid_win(wid);
+ w->preview_running = PJ_TRUE;
+
+ PJSUA_UNLOCK();
+ pj_log_pop_indent();
+ return PJ_SUCCESS;
+}
+
+/*
+ * Stop video preview.
+ */
+PJ_DEF(pj_status_t) pjsua_vid_preview_stop(pjmedia_vid_dev_index id)
+{
+ pjsua_vid_win_id wid = PJSUA_INVALID_ID;
+ pjsua_vid_win *w;
+ pj_status_t status;
+
+ PJSUA_LOCK();
+ wid = pjsua_vid_preview_get_win(id);
+ if (wid == PJSUA_INVALID_ID) {
+ PJSUA_UNLOCK();
+ pj_log_pop_indent();
+ return PJ_ENOTFOUND;
+ }
+
+ PJ_LOG(4,(THIS_FILE, "Stopping preview for cap_dev=%d", id));
+ pj_log_push_indent();
+
+ w = &pjsua_var.win[wid];
+ if (w->preview_running) {
+ if (w->is_native) {
+ pjmedia_vid_dev_stream *cap_dev;
+ pj_bool_t enabled = PJ_FALSE;
+
+ cap_dev = pjmedia_vid_port_get_stream(w->vp_cap);
+ status = pjmedia_vid_dev_stream_set_cap(
+ cap_dev, PJMEDIA_VID_DEV_CAP_INPUT_PREVIEW,
+ &enabled);
+ } else {
+ status = pjmedia_vid_port_stop(w->vp_rend);
+ }
+
+ if (status != PJ_SUCCESS) {
+ PJ_PERROR(1,(THIS_FILE, status, "Error stopping %spreview",
+ (w->is_native ? "native " : "")));
+ PJSUA_UNLOCK();
+ pj_log_pop_indent();
+ return status;
+ }
+
+ dec_vid_win(wid);
+ w->preview_running = PJ_FALSE;
+ }
+
+ PJSUA_UNLOCK();
+ pj_log_pop_indent();
+
+ return PJ_SUCCESS;
+}
+
+
+/*****************************************************************************
+ * Window
+ */
+
+
+/*
+ * Enumerates all video windows.
+ */
+PJ_DEF(pj_status_t) pjsua_vid_enum_wins( pjsua_vid_win_id wids[],
+ unsigned *count)
+{
+ unsigned i, cnt;
+
+ cnt = 0;
+
+ for (i=0; i<PJSUA_MAX_VID_WINS && cnt <*count; ++i) {
+ pjsua_vid_win *w = &pjsua_var.win[i];
+ if (w->type != PJSUA_WND_TYPE_NONE)
+ wids[cnt++] = i;
+ }
+
+ *count = cnt;
+
+ return PJ_SUCCESS;
+}
+
+
+/*
+ * Get window info.
+ */
+PJ_DEF(pj_status_t) pjsua_vid_win_get_info( pjsua_vid_win_id wid,
+ pjsua_vid_win_info *wi)
+{
+ pjsua_vid_win *w;
+ pjmedia_vid_dev_stream *s;
+ pjmedia_vid_dev_param vparam;
+ pj_status_t status;
+
+ PJ_ASSERT_RETURN(wid >= 0 && wid < PJSUA_MAX_VID_WINS && wi, PJ_EINVAL);
+
+ pj_bzero(wi, sizeof(*wi));
+
+ PJSUA_LOCK();
+ w = &pjsua_var.win[wid];
+
+ wi->is_native = w->is_native;
+
+ if (w->is_native) {
+ pjmedia_vid_dev_stream *cap_strm;
+ pjmedia_vid_dev_cap cap = PJMEDIA_VID_DEV_CAP_OUTPUT_WINDOW;
+
+ cap_strm = pjmedia_vid_port_get_stream(w->vp_cap);
+ if (!cap_strm) {
+ status = PJ_EINVAL;
+ } else {
+ status = pjmedia_vid_dev_stream_get_cap(cap_strm, cap, &wi->hwnd);
+ }
+
+ PJSUA_UNLOCK();
+ return status;
+ }
+
+ if (w->vp_rend == NULL) {
+ PJSUA_UNLOCK();
+ return PJ_EINVAL;
+ }
+
+ s = pjmedia_vid_port_get_stream(w->vp_rend);
+ if (s == NULL) {
+ PJSUA_UNLOCK();
+ return PJ_EINVAL;
+ }
+
+ status = pjmedia_vid_dev_stream_get_param(s, &vparam);
+ if (status != PJ_SUCCESS) {
+ PJSUA_UNLOCK();
+ return status;
+ }
+
+ wi->rdr_dev = vparam.rend_id;
+ wi->hwnd = vparam.window;
+ wi->show = !vparam.window_hide;
+ wi->pos = vparam.window_pos;
+ wi->size = vparam.disp_size;
+
+ PJSUA_UNLOCK();
+
+ return PJ_SUCCESS;
+}
+
+/*
+ * Show or hide window.
+ */
+PJ_DEF(pj_status_t) pjsua_vid_win_set_show( pjsua_vid_win_id wid,
+ pj_bool_t show)
+{
+ pjsua_vid_win *w;
+ pjmedia_vid_dev_stream *s;
+ pj_bool_t hide;
+ pj_status_t status;
+
+ PJ_ASSERT_RETURN(wid >= 0 && wid < PJSUA_MAX_VID_WINS, PJ_EINVAL);
+
+ PJSUA_LOCK();
+ w = &pjsua_var.win[wid];
+ if (w->vp_rend == NULL) {
+ /* Native window */
+ PJSUA_UNLOCK();
+ return PJ_EINVAL;
+ }
+
+ s = pjmedia_vid_port_get_stream(w->vp_rend);
+ if (s == NULL) {
+ PJSUA_UNLOCK();
+ return PJ_EINVAL;
+ }
+
+ /* Make sure that renderer gets started before shown up */
+ if (show && !pjmedia_vid_port_is_running(w->vp_rend))
+ status = pjmedia_vid_port_start(w->vp_rend);
+
+ hide = !show;
+ status = pjmedia_vid_dev_stream_set_cap(s,
+ PJMEDIA_VID_DEV_CAP_OUTPUT_HIDE, &hide);
+
+ PJSUA_UNLOCK();
+
+ return status;
+}
+
+/*
+ * Set video window position.
+ */
+PJ_DEF(pj_status_t) pjsua_vid_win_set_pos( pjsua_vid_win_id wid,
+ const pjmedia_coord *pos)
+{
+ pjsua_vid_win *w;
+ pjmedia_vid_dev_stream *s;
+ pj_status_t status;
+
+ PJ_ASSERT_RETURN(wid >= 0 && wid < PJSUA_MAX_VID_WINS && pos, PJ_EINVAL);
+
+ PJSUA_LOCK();
+ w = &pjsua_var.win[wid];
+ if (w->vp_rend == NULL) {
+ /* Native window */
+ PJSUA_UNLOCK();
+ return PJ_EINVAL;
+ }
+
+ s = pjmedia_vid_port_get_stream(w->vp_rend);
+ if (s == NULL) {
+ PJSUA_UNLOCK();
+ return PJ_EINVAL;
+ }
+
+ status = pjmedia_vid_dev_stream_set_cap(s,
+ PJMEDIA_VID_DEV_CAP_OUTPUT_POSITION, pos);
+
+ PJSUA_UNLOCK();
+
+ return status;
+}
+
+/*
+ * Resize window.
+ */
+PJ_DEF(pj_status_t) pjsua_vid_win_set_size( pjsua_vid_win_id wid,
+ const pjmedia_rect_size *size)
+{
+ pjsua_vid_win *w;
+ pjmedia_vid_dev_stream *s;
+ pj_status_t status;
+
+ PJ_ASSERT_RETURN(wid >= 0 && wid < PJSUA_MAX_VID_WINS && size, PJ_EINVAL);
+
+ PJSUA_LOCK();
+ w = &pjsua_var.win[wid];
+ if (w->vp_rend == NULL) {
+ /* Native window */
+ PJSUA_UNLOCK();
+ return PJ_EINVAL;
+ }
+
+ s = pjmedia_vid_port_get_stream(w->vp_rend);
+ if (s == NULL) {
+ PJSUA_UNLOCK();
+ return PJ_EINVAL;
+ }
+
+ status = pjmedia_vid_dev_stream_set_cap(s,
+ PJMEDIA_VID_DEV_CAP_OUTPUT_RESIZE, size);
+
+ PJSUA_UNLOCK();
+
+ return status;
+}
+
+/*
+ * Set video orientation.
+ */
+PJ_DEF(pj_status_t) pjsua_vid_win_rotate( pjsua_vid_win_id wid,
+ int angle)
+{
+ pjsua_vid_win *w;
+ pjmedia_vid_dev_stream *s;
+ pjmedia_orient orient;
+ pj_status_t status;
+
+ PJ_ASSERT_RETURN(wid >= 0 && wid < PJSUA_MAX_VID_WINS, PJ_EINVAL);
+ PJ_ASSERT_RETURN((angle % 90) == 0, PJ_EINVAL);
+
+ /* Normalize angle, so it must be 0, 90, 180, or 270. */
+ angle %= 360;
+ if (angle < 0)
+ angle += 360;
+
+ /* Convert angle to pjmedia_orient */
+ switch(angle) {
+ case 0:
+ /* No rotation */
+ return PJ_SUCCESS;
+ case 90:
+ orient = PJMEDIA_ORIENT_ROTATE_90DEG;
+ break;
+ case 180:
+ orient = PJMEDIA_ORIENT_ROTATE_180DEG;
+ break;
+ case 270:
+ orient = PJMEDIA_ORIENT_ROTATE_270DEG;
+ break;
+ default:
+ pj_assert(!"Angle must have been validated");
+ return PJ_EBUG;
+ }
+
+ PJSUA_LOCK();
+ w = &pjsua_var.win[wid];
+ if (w->vp_rend == NULL) {
+ /* Native window */
+ PJSUA_UNLOCK();
+ return PJ_EINVAL;
+ }
+
+ s = pjmedia_vid_port_get_stream(w->vp_rend);
+ if (s == NULL) {
+ PJSUA_UNLOCK();
+ return PJ_EINVAL;
+ }
+
+ status = pjmedia_vid_dev_stream_set_cap(s,
+ PJMEDIA_VID_DEV_CAP_ORIENTATION, &orient);
+
+ PJSUA_UNLOCK();
+
+ return status;
+}
+
+
+static void call_get_vid_strm_info(pjsua_call *call,
+ int *first_active,
+ int *first_inactive,
+ unsigned *active_cnt,
+ unsigned *cnt)
+{
+ unsigned i, var_cnt = 0;
+
+ if (first_active && ++var_cnt)
+ *first_active = -1;
+ if (first_inactive && ++var_cnt)
+ *first_inactive = -1;
+ if (active_cnt && ++var_cnt)
+ *active_cnt = 0;
+ if (cnt && ++var_cnt)
+ *cnt = 0;
+
+ for (i = 0; i < call->med_cnt && var_cnt; ++i) {
+ if (call->media[i].type == PJMEDIA_TYPE_VIDEO) {
+ if (call->media[i].dir != PJMEDIA_DIR_NONE)
+ {
+ if (first_active && *first_active == -1) {
+ *first_active = i;
+ --var_cnt;
+ }
+ if (active_cnt)
+ ++(*active_cnt);
+ } else if (first_inactive && *first_inactive == -1) {
+ *first_inactive = i;
+ --var_cnt;
+ }
+ if (cnt)
+ ++(*cnt);
+ }
+ }
+}
+
+
+/* Send SDP reoffer. */
+static pj_status_t call_reoffer_sdp(pjsua_call_id call_id,
+ const pjmedia_sdp_session *sdp)
+{
+ pjsua_call *call;
+ pjsip_tx_data *tdata;
+ pjsip_dialog *dlg;
+ pj_status_t status;
+
+ status = acquire_call("call_reoffer_sdp()", call_id, &call, &dlg);
+ if (status != PJ_SUCCESS)
+ return status;
+
+ if (call->inv->state != PJSIP_INV_STATE_CONFIRMED) {
+ PJ_LOG(3,(THIS_FILE, "Can not re-INVITE call that is not confirmed"));
+ pjsip_dlg_dec_lock(dlg);
+ return PJSIP_ESESSIONSTATE;
+ }
+
+ /* Create re-INVITE with new offer */
+ status = pjsip_inv_reinvite( call->inv, NULL, sdp, &tdata);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Unable to create re-INVITE", status);
+ pjsip_dlg_dec_lock(dlg);
+ return status;
+ }
+
+ /* Send the request */
+ status = pjsip_inv_send_msg( call->inv, tdata);
+ if (status != PJ_SUCCESS) {
+ pjsua_perror(THIS_FILE, "Unable to send re-INVITE", status);
+ pjsip_dlg_dec_lock(dlg);
+ return status;
+ }
+
+ pjsip_dlg_dec_lock(dlg);
+
+ return PJ_SUCCESS;
+}
+
+/* Add a new video stream into a call */
+static pj_status_t call_add_video(pjsua_call *call,
+ pjmedia_vid_dev_index cap_dev,
+ pjmedia_dir dir)
+{
+ pj_pool_t *pool = call->inv->pool_prov;
+ pjsua_acc_config *acc_cfg = &pjsua_var.acc[call->acc_id].cfg;
+ pjsua_call_media *call_med;
+ const pjmedia_sdp_session *current_sdp;
+ pjmedia_sdp_session *sdp;
+ pjmedia_sdp_media *sdp_m;
+ pjmedia_transport_info tpinfo;
+ pj_status_t status;
+
+ /* Verify media slot availability */
+ if (call->med_cnt == PJSUA_MAX_CALL_MEDIA)
+ return PJ_ETOOMANY;
+
+ /* Get active local SDP and clone it */
+ status = pjmedia_sdp_neg_get_active_local(call->inv->neg, &current_sdp);
+ if (status != PJ_SUCCESS)
+ return status;
+
+ sdp = pjmedia_sdp_session_clone(call->inv->pool_prov, current_sdp);
+
+ /* Clean up provisional media before using it */
+ pjsua_media_prov_clean_up(call->index);
+
+ /* Update provisional media from call media */
+ call->med_prov_cnt = call->med_cnt;
+ pj_memcpy(call->media_prov, call->media,
+ sizeof(call->media[0]) * call->med_cnt);
+
+ /* Initialize call media */
+ call_med = &call->media_prov[call->med_prov_cnt++];
+ status = pjsua_call_media_init(call_med, PJMEDIA_TYPE_VIDEO,
+ &acc_cfg->rtp_cfg, call->secure_level,
+ NULL, PJ_FALSE, NULL);
+ if (status != PJ_SUCCESS)
+ goto on_error;
+
+ /* Override default capture device setting */
+ call_med->strm.v.cap_dev = cap_dev;
+
+ /* Init transport media */
+ status = pjmedia_transport_media_create(call_med->tp, pool, 0,
+ NULL, call_med->idx);
+ if (status != PJ_SUCCESS)
+ goto on_error;
+
+ pjsua_set_media_tp_state(call_med, PJSUA_MED_TP_INIT);
+
+ /* Get transport address info */
+ pjmedia_transport_info_init(&tpinfo);
+ pjmedia_transport_get_info(call_med->tp, &tpinfo);
+
+ /* Create SDP media line */
+ status = pjmedia_endpt_create_video_sdp(pjsua_var.med_endpt, pool,
+ &tpinfo.sock_info, 0, &sdp_m);
+ if (status != PJ_SUCCESS)
+ goto on_error;
+
+ sdp->media[sdp->media_count++] = sdp_m;
+
+ /* Update media direction, if it is not 'sendrecv' */
+ if (dir != PJMEDIA_DIR_ENCODING_DECODING) {
+ pjmedia_sdp_attr *a;
+
+ /* Remove sendrecv direction attribute, if any */
+ pjmedia_sdp_media_remove_all_attr(sdp_m, "sendrecv");
+
+ if (dir == PJMEDIA_DIR_ENCODING)
+ a = pjmedia_sdp_attr_create(pool, "sendonly", NULL);
+ else if (dir == PJMEDIA_DIR_DECODING)
+ a = pjmedia_sdp_attr_create(pool, "recvonly", NULL);
+ else
+ a = pjmedia_sdp_attr_create(pool, "inactive", NULL);
+
+ pjmedia_sdp_media_add_attr(sdp_m, a);
+ }
+
+ /* Update SDP media line by media transport */
+ status = pjmedia_transport_encode_sdp(call_med->tp, pool,
+ sdp, NULL, call_med->idx);
+ if (status != PJ_SUCCESS)
+ goto on_error;
+
+ status = call_reoffer_sdp(call->index, sdp);
+ if (status != PJ_SUCCESS)
+ goto on_error;
+
+ call->opt.vid_cnt++;
+
+ return PJ_SUCCESS;
+
+on_error:
+ if (call_med->tp) {
+ pjsua_set_media_tp_state(call_med, PJSUA_MED_TP_NULL);
+ pjmedia_transport_close(call_med->tp);
+ call_med->tp = call_med->tp_orig = NULL;
+ }
+
+ return status;
+}
+
+
+/* Modify a video stream from a call, i.e: update direction,
+ * remove/disable.
+ */
+static pj_status_t call_modify_video(pjsua_call *call,
+ int med_idx,
+ pjmedia_dir dir,
+ pj_bool_t remove)
+{
+ pjsua_call_media *call_med;
+ const pjmedia_sdp_session *current_sdp;
+ pjmedia_sdp_session *sdp;
+ pj_status_t status;
+
+ /* Verify and normalize media index */
+ if (med_idx == -1) {
+ int first_active;
+
+ call_get_vid_strm_info(call, &first_active, NULL, NULL, NULL);
+ if (first_active == -1)
+ return PJ_ENOTFOUND;
+
+ med_idx = first_active;
+ }
+
+ /* Clean up provisional media before using it */
+ pjsua_media_prov_clean_up(call->index);
+
+ /* Update provisional media from call media */
+ call->med_prov_cnt = call->med_cnt;
+ pj_memcpy(call->media_prov, call->media,
+ sizeof(call->media[0]) * call->med_cnt);
+
+ call_med = &call->media_prov[med_idx];
+
+ /* Verify if the stream media type is video */
+ if (call_med->type != PJMEDIA_TYPE_VIDEO)
+ return PJ_EINVAL;
+
+ /* Verify if the stream dir is not changed */
+ if ((!remove && call_med->dir == dir) ||
+ ( remove && (call_med->tp_st == PJSUA_MED_TP_DISABLED ||
+ call_med->tp == NULL)))
+ {
+ return PJ_SUCCESS;
+ }
+
+ /* Get active local SDP and clone it */
+ status = pjmedia_sdp_neg_get_active_local(call->inv->neg, &current_sdp);
+ if (status != PJ_SUCCESS)
+ return status;
+
+ sdp = pjmedia_sdp_session_clone(call->inv->pool_prov, current_sdp);
+
+ pj_assert(med_idx < (int)sdp->media_count);
+
+ if (!remove) {
+ pjsua_acc_config *acc_cfg = &pjsua_var.acc[call->acc_id].cfg;
+ pj_pool_t *pool = call->inv->pool_prov;
+ pjmedia_sdp_media *sdp_m;
+
+ /* Enabling video */
+ if (call_med->dir == PJMEDIA_DIR_NONE) {
+ unsigned i, vid_cnt = 0;
+
+ /* Check if vid_cnt in call option needs to be increased */
+ for (i = 0; i < call->med_cnt; ++i) {
+ if (call->media[i].type == PJMEDIA_TYPE_VIDEO &&
+ call->media[i].dir != PJMEDIA_DIR_NONE)
+ {
+ ++vid_cnt;
+ }
+ }
+ if (call->opt.vid_cnt <= vid_cnt)
+ call->opt.vid_cnt++;
+ }
+
+ status = pjsua_call_media_init(call_med, PJMEDIA_TYPE_VIDEO,
+ &acc_cfg->rtp_cfg, call->secure_level,
+ NULL, PJ_FALSE, NULL);
+ if (status != PJ_SUCCESS)
+ goto on_error;
+
+ /* Init transport media */
+ if (call_med->tp && call_med->tp_st == PJSUA_MED_TP_IDLE) {
+ status = pjmedia_transport_media_create(call_med->tp, pool, 0,
+ NULL, call_med->idx);
+ if (status != PJ_SUCCESS)
+ goto on_error;
+ }
+
+ sdp_m = sdp->media[med_idx];
+
+ /* Create new SDP media line if the stream is disabled */
+ if (sdp->media[med_idx]->desc.port == 0) {
+ pjmedia_transport_info tpinfo;
+
+ /* Get transport address info */
+ pjmedia_transport_info_init(&tpinfo);
+ pjmedia_transport_get_info(call_med->tp, &tpinfo);
+
+ status = pjmedia_endpt_create_video_sdp(pjsua_var.med_endpt, pool,
+ &tpinfo.sock_info, 0, &sdp_m);
+ if (status != PJ_SUCCESS)
+ goto on_error;
+ }
+
+ {
+ pjmedia_sdp_attr *a;
+
+ /* Remove any direction attributes */
+ pjmedia_sdp_media_remove_all_attr(sdp_m, "sendrecv");
+ pjmedia_sdp_media_remove_all_attr(sdp_m, "sendonly");
+ pjmedia_sdp_media_remove_all_attr(sdp_m, "recvonly");
+ pjmedia_sdp_media_remove_all_attr(sdp_m, "inactive");
+
+ /* Update media direction */
+ if (dir == PJMEDIA_DIR_ENCODING_DECODING)
+ a = pjmedia_sdp_attr_create(pool, "sendrecv", NULL);
+ else if (dir == PJMEDIA_DIR_ENCODING)
+ a = pjmedia_sdp_attr_create(pool, "sendonly", NULL);
+ else if (dir == PJMEDIA_DIR_DECODING)
+ a = pjmedia_sdp_attr_create(pool, "recvonly", NULL);
+ else
+ a = pjmedia_sdp_attr_create(pool, "inactive", NULL);
+
+ pjmedia_sdp_media_add_attr(sdp_m, a);
+ }
+
+ sdp->media[med_idx] = sdp_m;
+
+ /* Update SDP media line by media transport */
+ status = pjmedia_transport_encode_sdp(call_med->tp, pool,
+ sdp, NULL, call_med->idx);
+ if (status != PJ_SUCCESS)
+ goto on_error;
+
+on_error:
+ if (status != PJ_SUCCESS) {
+ pjsua_media_prov_clean_up(call->index);
+ return status;
+ }
+
+ } else {
+
+ pj_pool_t *pool = call->inv->pool_prov;
+
+ /* Mark media transport to disabled */
+ // Don't close this here, as SDP negotiation has not been
+ // done and stream may be still active.
+ pjsua_set_media_tp_state(call_med, PJSUA_MED_TP_DISABLED);
+
+ /* Deactivate the stream */
+ pjmedia_sdp_media_deactivate(pool, sdp->media[med_idx]);
+
+ call->opt.vid_cnt--;
+ }
+
+ status = call_reoffer_sdp(call->index, sdp);
+ if (status != PJ_SUCCESS)
+ return status;
+
+ return PJ_SUCCESS;
+}
+
+
+/* Change capture device of a video stream in a call */
+static pj_status_t call_change_cap_dev(pjsua_call *call,
+ int med_idx,
+ pjmedia_vid_dev_index cap_dev)
+{
+ pjsua_call_media *call_med;
+ pjmedia_vid_dev_stream *old_dev;
+ pjmedia_vid_dev_switch_param switch_prm;
+ pjmedia_vid_dev_info info;
+ pjsua_vid_win *w, *new_w = NULL;
+ pjsua_vid_win_id wid, new_wid;
+ pjmedia_port *media_port;
+ pj_status_t status;
+
+ /* Verify and normalize media index */
+ if (med_idx == -1) {
+ int first_active;
+
+ call_get_vid_strm_info(call, &first_active, NULL, NULL, NULL);
+ if (first_active == -1)
+ return PJ_ENOTFOUND;
+
+ med_idx = first_active;
+ }
+
+ call_med = &call->media[med_idx];
+
+ /* Verify if the stream media type is video */
+ if (call_med->type != PJMEDIA_TYPE_VIDEO)
+ return PJ_EINVAL;
+
+ /* Verify the capture device */
+ status = pjmedia_vid_dev_get_info(cap_dev, &info);
+ if (status != PJ_SUCCESS || info.dir != PJMEDIA_DIR_CAPTURE)
+ return PJ_EINVAL;
+
+ /* The specified capture device is being used already */
+ if (call_med->strm.v.cap_dev == cap_dev)
+ return PJ_SUCCESS;
+
+ /* == Apply the new capture device == */
+
+ wid = call_med->strm.v.cap_win_id;
+ w = &pjsua_var.win[wid];
+ pj_assert(w->type == PJSUA_WND_TYPE_PREVIEW && w->vp_cap);
+
+ /* If the old device supports fast switching, then that's excellent! */
+ old_dev = pjmedia_vid_port_get_stream(w->vp_cap);
+ pjmedia_vid_dev_switch_param_default(&switch_prm);
+ switch_prm.target_id = cap_dev;
+ status = pjmedia_vid_dev_stream_set_cap(old_dev,
+ PJMEDIA_VID_DEV_CAP_SWITCH,
+ &switch_prm);
+ if (status == PJ_SUCCESS) {
+ w->preview_cap_id = cap_dev;
+ call_med->strm.v.cap_dev = cap_dev;
+ return PJ_SUCCESS;
+ }
+
+ /* No it doesn't support fast switching. Do slow switching then.. */
+ status = pjmedia_vid_stream_get_port(call_med->strm.v.stream,
+ PJMEDIA_DIR_ENCODING, &media_port);
+ if (status != PJ_SUCCESS)
+ return status;
+
+ pjmedia_event_unsubscribe(NULL, &call_media_on_event, call_med,
+ w->vp_cap);
+
+ /* temporarily disconnect while we operate on the tee. */
+ pjmedia_vid_port_disconnect(w->vp_cap);
+
+ /* = Detach stream port from the old capture device's tee = */
+ status = pjmedia_vid_tee_remove_dst_port(w->tee, media_port);
+ if (status != PJ_SUCCESS) {
+ /* Something wrong, assume that media_port has been removed
+ * and continue.
+ */
+ PJ_PERROR(4,(THIS_FILE, status,
+ "Warning: call %d: unable to remove video from tee",
+ call->index));
+ }
+
+ /* Reconnect again immediately. We're done with w->tee */
+ pjmedia_vid_port_connect(w->vp_cap, w->tee, PJ_FALSE);
+
+ /* = Attach stream port to the new capture device = */
+
+ /* Note: calling pjsua_vid_preview_get_win() even though
+ * create_vid_win() will automatically create the window
+ * if it doesn't exist, because create_vid_win() will modify
+ * existing window SHOW/HIDE value.
+ */
+ new_wid = vid_preview_get_win(cap_dev, PJ_FALSE);
+ if (new_wid == PJSUA_INVALID_ID) {
+ pjsua_acc *acc = &pjsua_var.acc[call_med->call->acc_id];
+
+ /* Create preview video window */
+ status = create_vid_win(PJSUA_WND_TYPE_PREVIEW,
+ &media_port->info.fmt,
+ call_med->strm.v.rdr_dev,
+ cap_dev,
+ PJSUA_HIDE_WINDOW,
+ acc->cfg.vid_wnd_flags,
+ &new_wid);
+ if (status != PJ_SUCCESS)
+ goto on_error;
+ }
+
+ inc_vid_win(new_wid);
+ new_w = &pjsua_var.win[new_wid];
+
+ /* Connect stream to capturer (via video window tee) */
+ status = pjmedia_vid_tee_add_dst_port2(new_w->tee, 0, media_port);
+ if (status != PJ_SUCCESS)
+ goto on_error;
+
+ if (w->vp_rend) {
+ /* Start renderer */
+ status = pjmedia_vid_port_start(new_w->vp_rend);
+ if (status != PJ_SUCCESS)
+ goto on_error;
+ }
+
+#if ENABLE_EVENT
+ pjmedia_event_subscribe(NULL, &call_media_on_event,
+ call_med, new_w->vp_cap);
+#endif
+
+ /* Start capturer */
+ if (!pjmedia_vid_port_is_running(new_w->vp_cap)) {
+ status = pjmedia_vid_port_start(new_w->vp_cap);
+ if (status != PJ_SUCCESS)
+ goto on_error;
+ }
+
+ /* Finally */
+ call_med->strm.v.cap_dev = cap_dev;
+ call_med->strm.v.cap_win_id = new_wid;
+ dec_vid_win(wid);
+
+ return PJ_SUCCESS;
+
+on_error:
+ PJ_PERROR(4,(THIS_FILE, status,
+ "Call %d: error changing capture device to %d",
+ call->index, cap_dev));
+
+ if (new_w) {
+ /* Unsubscribe, just in case */
+ pjmedia_event_unsubscribe(NULL, &call_media_on_event, call_med,
+ new_w->vp_cap);
+ /* Disconnect media port from the new capturer */
+ pjmedia_vid_tee_remove_dst_port(new_w->tee, media_port);
+ /* Release the new capturer */
+ dec_vid_win(new_wid);
+ }
+
+ /* Revert back to the old capturer */
+ pjmedia_vid_port_disconnect(w->vp_cap);
+ status = pjmedia_vid_tee_add_dst_port2(w->tee, 0, media_port);
+ pjmedia_vid_port_connect(w->vp_cap, w->tee, PJ_FALSE);
+ if (status != PJ_SUCCESS)
+ return status;
+
+#if ENABLE_EVENT
+ /* Resubscribe */
+ pjmedia_event_subscribe(NULL, &call_media_on_event,
+ call_med, w->vp_cap);
+#endif
+
+ return status;
+}
+
+
+/* Start/stop transmitting video stream in a call */
+static pj_status_t call_set_tx_video(pjsua_call *call,
+ int med_idx,
+ pj_bool_t enable)
+{
+ pjsua_call_media *call_med;
+ pj_status_t status;
+
+ /* Verify and normalize media index */
+ if (med_idx == -1) {
+ int first_active;
+
+ call_get_vid_strm_info(call, &first_active, NULL, NULL, NULL);
+ if (first_active == -1)
+ return PJ_ENOTFOUND;
+
+ med_idx = first_active;
+ }
+
+ call_med = &call->media[med_idx];
+
+ /* Verify if the stream is transmitting video */
+ if (call_med->type != PJMEDIA_TYPE_VIDEO ||
+ (enable && (call_med->dir & PJMEDIA_DIR_ENCODING) == 0))
+ {
+ return PJ_EINVAL;
+ }
+
+ if (enable) {
+ /* Start stream in encoding direction */
+ status = pjmedia_vid_stream_resume(call_med->strm.v.stream,
+ PJMEDIA_DIR_ENCODING);
+ } else {
+ /* Pause stream in encoding direction */
+ status = pjmedia_vid_stream_pause( call_med->strm.v.stream,
+ PJMEDIA_DIR_ENCODING);
+ }
+
+ return status;
+}
+
+
+static pj_status_t call_send_vid_keyframe(pjsua_call *call,
+ int med_idx)
+{
+ pjsua_call_media *call_med;
+
+ /* Verify and normalize media index */
+ if (med_idx == -1) {
+ int first_active;
+
+ call_get_vid_strm_info(call, &first_active, NULL, NULL, NULL);
+ if (first_active == -1)
+ return PJ_ENOTFOUND;
+
+ med_idx = first_active;
+ }
+
+ call_med = &call->media[med_idx];
+
+ /* Verify media type and stream instance. */
+ if (call_med->type != PJMEDIA_TYPE_VIDEO || !call_med->strm.v.stream)
+ return PJ_EINVAL;
+
+ return pjmedia_vid_stream_send_keyframe(call_med->strm.v.stream);
+}
+
+
+/*
+ * Start, stop, and/or manipulate video transmission for the specified call.
+ */
+PJ_DEF(pj_status_t) pjsua_call_set_vid_strm (
+ pjsua_call_id call_id,
+ pjsua_call_vid_strm_op op,
+ const pjsua_call_vid_strm_op_param *param)
+{
+ pjsua_call *call;
+ pjsua_call_vid_strm_op_param param_;
+ pj_status_t status;
+
+ PJ_ASSERT_RETURN(call_id>=0 && call_id<(int)pjsua_var.ua_cfg.max_calls,
+ PJ_EINVAL);
+ PJ_ASSERT_RETURN(op != PJSUA_CALL_VID_STRM_NO_OP, PJ_EINVAL);
+
+ PJ_LOG(4,(THIS_FILE, "Call %d: set video stream, op=%d",
+ call_id, op));
+ pj_log_push_indent();
+
+ PJSUA_LOCK();
+
+ call = &pjsua_var.calls[call_id];
+
+ if (param) {
+ param_ = *param;
+ } else {
+ pjsua_call_vid_strm_op_param_default(&param_);
+ }
+
+ /* If set to PJMEDIA_VID_DEFAULT_CAPTURE_DEV, replace it with
+ * account default video capture device.
+ */
+ if (param_.cap_dev == PJMEDIA_VID_DEFAULT_CAPTURE_DEV) {
+ pjsua_acc_config *acc_cfg = &pjsua_var.acc[call->acc_id].cfg;
+ param_.cap_dev = acc_cfg->vid_cap_dev;
+
+ /* If the account default video capture device is
+ * PJMEDIA_VID_DEFAULT_CAPTURE_DEV, replace it with
+ * global default video capture device.
+ */
+ if (param_.cap_dev == PJMEDIA_VID_DEFAULT_CAPTURE_DEV) {
+ pjmedia_vid_dev_info info;
+ pjmedia_vid_dev_get_info(param_.cap_dev, &info);
+ pj_assert(info.dir == PJMEDIA_DIR_CAPTURE);
+ param_.cap_dev = info.id;
+ }
+ }
+
+ switch (op) {
+ case PJSUA_CALL_VID_STRM_ADD:
+ status = call_add_video(call, param_.cap_dev, param_.dir);
+ break;
+ case PJSUA_CALL_VID_STRM_REMOVE:
+ status = call_modify_video(call, param_.med_idx, PJMEDIA_DIR_NONE,
+ PJ_TRUE);
+ break;
+ case PJSUA_CALL_VID_STRM_CHANGE_DIR:
+ status = call_modify_video(call, param_.med_idx, param_.dir, PJ_FALSE);
+ break;
+ case PJSUA_CALL_VID_STRM_CHANGE_CAP_DEV:
+ status = call_change_cap_dev(call, param_.med_idx, param_.cap_dev);
+ break;
+ case PJSUA_CALL_VID_STRM_START_TRANSMIT:
+ status = call_set_tx_video(call, param_.med_idx, PJ_TRUE);
+ break;
+ case PJSUA_CALL_VID_STRM_STOP_TRANSMIT:
+ status = call_set_tx_video(call, param_.med_idx, PJ_FALSE);
+ break;
+ case PJSUA_CALL_VID_STRM_SEND_KEYFRAME:
+ status = call_send_vid_keyframe(call, param_.med_idx);
+ break;
+ default:
+ status = PJ_EINVALIDOP;
+ break;
+ }
+
+ PJSUA_UNLOCK();
+ pj_log_pop_indent();
+
+ return status;
+}
+
+
+/*
+ * Get the media stream index of the default video stream in the call.
+ */
+PJ_DEF(int) pjsua_call_get_vid_stream_idx(pjsua_call_id call_id)
+{
+ pjsua_call *call;
+ int first_active, first_inactive;
+
+ PJ_ASSERT_RETURN(call_id>=0 && call_id<(int)pjsua_var.ua_cfg.max_calls,
+ PJ_EINVAL);
+
+ PJSUA_LOCK();
+ call = &pjsua_var.calls[call_id];
+ call_get_vid_strm_info(call, &first_active, &first_inactive, NULL, NULL);
+ PJSUA_UNLOCK();
+
+ if (first_active == -1)
+ return first_inactive;
+
+ return first_active;
+}
+
+
+/*
+ * Determine if video stream for the specified call is currently running
+ * for the specified direction.
+ */
+PJ_DEF(pj_bool_t) pjsua_call_vid_stream_is_running( pjsua_call_id call_id,
+ int med_idx,
+ pjmedia_dir dir)
+{
+ pjsua_call *call;
+ pjsua_call_media *call_med;
+
+ PJ_ASSERT_RETURN(call_id>=0 && call_id<(int)pjsua_var.ua_cfg.max_calls,
+ PJ_EINVAL);
+
+ /* Verify and normalize media index */
+ if (med_idx == -1) {
+ med_idx = pjsua_call_get_vid_stream_idx(call_id);
+ }
+
+ call = &pjsua_var.calls[call_id];
+ PJ_ASSERT_RETURN(med_idx >= 0 && med_idx < (int)call->med_cnt, PJ_EINVAL);
+
+ call_med = &call->media[med_idx];
+
+ /* Verify if the stream is transmitting video */
+ if (call_med->type != PJMEDIA_TYPE_VIDEO || (call_med->dir & dir) == 0 ||
+ !call_med->strm.v.stream)
+ {
+ return PJ_FALSE;
+ }
+
+ return pjmedia_vid_stream_is_running(call_med->strm.v.stream, dir);
+}
+
+#endif /* PJSUA_HAS_VIDEO */
+
+#endif /* PJSUA_MEDIA_HAS_PJMEDIA */