From f3ab456a17af1c89a6e3be4d20c5944853df1cb0 Mon Sep 17 00:00:00 2001 From: "David M. Lee" Date: Mon, 7 Jan 2013 14:24:28 -0600 Subject: Import pjproject-2.0.1 --- pjsip/src/pjsua-lib/pjsua_acc.c | 3078 ++++++++++++++++++++++++++ pjsip/src/pjsua-lib/pjsua_aud.c | 2148 ++++++++++++++++++ pjsip/src/pjsua-lib/pjsua_call.c | 4397 +++++++++++++++++++++++++++++++++++++ pjsip/src/pjsua-lib/pjsua_core.c | 2909 ++++++++++++++++++++++++ pjsip/src/pjsua-lib/pjsua_dump.c | 974 ++++++++ pjsip/src/pjsua-lib/pjsua_im.c | 745 +++++++ pjsip/src/pjsua-lib/pjsua_media.c | 2695 +++++++++++++++++++++++ pjsip/src/pjsua-lib/pjsua_pres.c | 2402 ++++++++++++++++++++ pjsip/src/pjsua-lib/pjsua_vid.c | 2166 ++++++++++++++++++ 9 files changed, 21514 insertions(+) create mode 100644 pjsip/src/pjsua-lib/pjsua_acc.c create mode 100644 pjsip/src/pjsua-lib/pjsua_aud.c create mode 100644 pjsip/src/pjsua-lib/pjsua_call.c create mode 100644 pjsip/src/pjsua-lib/pjsua_core.c create mode 100644 pjsip/src/pjsua-lib/pjsua_dump.c create mode 100644 pjsip/src/pjsua-lib/pjsua_im.c create mode 100644 pjsip/src/pjsua-lib/pjsua_media.c create mode 100644 pjsip/src/pjsua-lib/pjsua_pres.c create mode 100644 pjsip/src/pjsua-lib/pjsua_vid.c (limited to 'pjsip/src/pjsua-lib') 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 + * + * 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 +#include + + +#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; iproxy_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; icred_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; ipool, 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; iproxy_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; icred_count; ++i) { + acc->cred[acc->cred_cnt++] = acc_cfg->cred_info[i]; + } + for (i=0; icred_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=\"\""; + + 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; iid.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; icfg.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, + "", + 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=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; iproxy_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; icfg.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; icfg.cred_count; ++i) { + acc->cred[acc->cred_cnt++] = acc->cfg.cred_info[i]; + } + for (i=0; icred_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(®_contact, &acc->contact); + pj_strcat(®_contact, &acc->rfc5626_regprm); + pj_strcat(®_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; irdata->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, + "%.*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; + iprev; + pj_list_erase(hr); + hr = prev; + } + } + + /* Then append the Service-Route URIs */ + for (i=0; ipool); + 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, ¶m->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; icount; ++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, ®_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, ®c_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; iprev; + } + } + + 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, ®_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, ®c_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 && ihost)==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; ihost)==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 + * + * 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 +#include + +#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; istrm.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; ilisteners[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(¶m); + param.ec_options = pjsua_var.media_cfg.ec_options; + + /* Create parameter based on peer info */ + status = create_aud_param(¶m.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(¶m); + 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_idptr, 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_idinfo.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_idptr, 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 *count) dev_count = *count; + pj_bzero(info, dev_count * sizeof(pjmedia_snd_dev_info)); + + for (i=0; idir = 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, ¶m); + 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, ¶m); + + 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 + * + * 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 +#include + + +#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; imedia); ++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= 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= (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; iuser_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; jdesc.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; mimedia_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 + * + * 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 +#include + + +#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; imsg_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; ioutbound_proxy_cnt; ++i) { + pj_strdup_with_null(pool, &dst->outbound_proxy[i], + &src->outbound_proxy[i]); + } + + for (i=0; icred_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; istun_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; inameserver_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; ioutbound_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; iinfo.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; isrv[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; jnext; + 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 %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=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; imed_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 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 +#include + +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; imed_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 + * + * 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 +#include + + +#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 + * + * 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 +#include + + +#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; iport; + + 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; iqos_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; irole==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; imed_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; imed_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; imed_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; imedia_count && countmedia[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 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 = + "" + "" + "" + ""; + + 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; imed_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; miopt.vid_cnt; + for (mi=0; mimed_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; mimed_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; mimed_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; mimed_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 (mimed_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; mimed_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; mimed_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; islen==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 + * + * 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 +#include + + +#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; iuser, &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; retryflag = 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 && iid = 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_idnext; + } + } + } + + PJ_LOG(3,(THIS_FILE, "Number of server/UAS subscriptions: %d", + count)); + + count = 0; + + for (i=0; isub), + 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; iremote, 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; icfg.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; icfg.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 +#include + +#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; imed_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; ipackings & 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; islen==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; itype == 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; itype == 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; itype != 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, ¤t_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, ¤t_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(¶m_); + } + + /* 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 */ -- cgit v1.2.3