blob: 1053b635022d767aa56cade322720709c0f4504f [file] [log] [blame]
/* $Id$ */
/*
* Copyright (C) 2008-2011 Teluu Inc. (http://www.teluu.com)
* Copyright (C) 2003-2008 Benny Prijono <benny@prijono.org>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
#include <pjmedia/sdp_neg.h>
#include <pjmedia/sdp.h>
#include <pjmedia/errno.h>
#include <pj/assert.h>
#include <pj/pool.h>
#include <pj/string.h>
#include <pj/ctype.h>
#include <pj/array.h>
/**
* This structure describes SDP media negotiator.
*/
struct pjmedia_sdp_neg
{
pjmedia_sdp_neg_state state; /**< Negotiator state. */
pj_bool_t prefer_remote_codec_order;
pj_bool_t has_remote_answer;
pj_bool_t answer_was_remote;
pjmedia_sdp_session *initial_sdp, /**< Initial local SDP */
*active_local_sdp, /**< Currently active local SDP. */
*active_remote_sdp, /**< Currently active remote's. */
*neg_local_sdp, /**< Temporary local SDP. */
*neg_remote_sdp; /**< Temporary remote SDP. */
};
static const char *state_str[] =
{
"STATE_NULL",
"STATE_LOCAL_OFFER",
"STATE_REMOTE_OFFER",
"STATE_WAIT_NEGO",
"STATE_DONE",
};
#define GET_FMTP_IVAL_BASE(ival, base, fmtp, param, default_val) \
do { \
pj_str_t s; \
char *p; \
p = pj_stristr(&fmtp.fmt_param, &param); \
if (!p) { \
ival = default_val; \
break; \
} \
pj_strset(&s, p + param.slen, fmtp.fmt_param.slen - \
(p - fmtp.fmt_param.ptr) - param.slen); \
ival = pj_strtoul2(&s, NULL, base); \
} while (0)
#define GET_FMTP_IVAL(ival, fmtp, param, default_val) \
GET_FMTP_IVAL_BASE(ival, 10, fmtp, param, default_val)
/* Definition of customized SDP format negotiation callback */
struct fmt_match_cb_t
{
pj_str_t fmt_name;
pjmedia_sdp_neg_fmt_match_cb cb;
};
/* Number of registered customized SDP format negotiation callbacks */
static unsigned fmt_match_cb_cnt;
/* The registered customized SDP format negotiation callbacks */
static struct fmt_match_cb_t
fmt_match_cb[PJMEDIA_SDP_NEG_MAX_CUSTOM_FMT_NEG_CB];
/* Redefining a very long identifier name, just for convenience */
#define ALLOW_MODIFY_ANSWER PJMEDIA_SDP_NEG_FMT_MATCH_ALLOW_MODIFY_ANSWER
static pj_status_t custom_fmt_match( pj_pool_t *pool,
const pj_str_t *fmt_name,
pjmedia_sdp_media *offer,
unsigned o_fmt_idx,
pjmedia_sdp_media *answer,
unsigned a_fmt_idx,
unsigned option);
/*
* Get string representation of negotiator state.
*/
PJ_DEF(const char*) pjmedia_sdp_neg_state_str(pjmedia_sdp_neg_state state)
{
if (state >=0 && state < (pjmedia_sdp_neg_state)PJ_ARRAY_SIZE(state_str))
return state_str[state];
return "<?UNKNOWN?>";
}
/*
* Create with local offer.
*/
PJ_DEF(pj_status_t) pjmedia_sdp_neg_create_w_local_offer( pj_pool_t *pool,
const pjmedia_sdp_session *local,
pjmedia_sdp_neg **p_neg)
{
pjmedia_sdp_neg *neg;
pj_status_t status;
/* Check arguments are valid. */
PJ_ASSERT_RETURN(pool && local && p_neg, PJ_EINVAL);
*p_neg = NULL;
/* Validate local offer. */
PJ_ASSERT_RETURN((status=pjmedia_sdp_validate(local))==PJ_SUCCESS, status);
/* Create and initialize negotiator. */
neg = PJ_POOL_ZALLOC_T(pool, pjmedia_sdp_neg);
PJ_ASSERT_RETURN(neg != NULL, PJ_ENOMEM);
neg->state = PJMEDIA_SDP_NEG_STATE_LOCAL_OFFER;
neg->prefer_remote_codec_order = PJMEDIA_SDP_NEG_PREFER_REMOTE_CODEC_ORDER;
neg->initial_sdp = pjmedia_sdp_session_clone(pool, local);
neg->neg_local_sdp = pjmedia_sdp_session_clone(pool, local);
*p_neg = neg;
return PJ_SUCCESS;
}
/*
* Create with remote offer and initial local offer/answer.
*/
PJ_DEF(pj_status_t) pjmedia_sdp_neg_create_w_remote_offer(pj_pool_t *pool,
const pjmedia_sdp_session *initial,
const pjmedia_sdp_session *remote,
pjmedia_sdp_neg **p_neg)
{
pjmedia_sdp_neg *neg;
pj_status_t status;
/* Check arguments are valid. */
PJ_ASSERT_RETURN(pool && remote && p_neg, PJ_EINVAL);
*p_neg = NULL;
/* Validate remote offer and initial answer */
status = pjmedia_sdp_validate(remote);
if (status != PJ_SUCCESS)
return status;
/* Create and initialize negotiator. */
neg = PJ_POOL_ZALLOC_T(pool, pjmedia_sdp_neg);
PJ_ASSERT_RETURN(neg != NULL, PJ_ENOMEM);
neg->prefer_remote_codec_order = PJMEDIA_SDP_NEG_PREFER_REMOTE_CODEC_ORDER;
neg->neg_remote_sdp = pjmedia_sdp_session_clone(pool, remote);
if (initial) {
PJ_ASSERT_RETURN((status=pjmedia_sdp_validate(initial))==PJ_SUCCESS,
status);
neg->initial_sdp = pjmedia_sdp_session_clone(pool, initial);
neg->neg_local_sdp = pjmedia_sdp_session_clone(pool, initial);
neg->state = PJMEDIA_SDP_NEG_STATE_WAIT_NEGO;
} else {
neg->state = PJMEDIA_SDP_NEG_STATE_REMOTE_OFFER;
}
*p_neg = neg;
return PJ_SUCCESS;
}
/*
* Set codec order preference.
*/
PJ_DEF(pj_status_t) pjmedia_sdp_neg_set_prefer_remote_codec_order(
pjmedia_sdp_neg *neg,
pj_bool_t prefer_remote)
{
PJ_ASSERT_RETURN(neg, PJ_EINVAL);
neg->prefer_remote_codec_order = prefer_remote;
return PJ_SUCCESS;
}
/*
* Get SDP negotiator state.
*/
PJ_DEF(pjmedia_sdp_neg_state) pjmedia_sdp_neg_get_state( pjmedia_sdp_neg *neg )
{
/* Check arguments are valid. */
PJ_ASSERT_RETURN(neg != NULL, PJMEDIA_SDP_NEG_STATE_NULL);
return neg->state;
}
PJ_DEF(pj_status_t) pjmedia_sdp_neg_get_active_local( pjmedia_sdp_neg *neg,
const pjmedia_sdp_session **local)
{
PJ_ASSERT_RETURN(neg && local, PJ_EINVAL);
PJ_ASSERT_RETURN(neg->active_local_sdp, PJMEDIA_SDPNEG_ENOACTIVE);
*local = neg->active_local_sdp;
return PJ_SUCCESS;
}
PJ_DEF(pj_status_t) pjmedia_sdp_neg_get_active_remote( pjmedia_sdp_neg *neg,
const pjmedia_sdp_session **remote)
{
PJ_ASSERT_RETURN(neg && remote, PJ_EINVAL);
PJ_ASSERT_RETURN(neg->active_remote_sdp, PJMEDIA_SDPNEG_ENOACTIVE);
*remote = neg->active_remote_sdp;
return PJ_SUCCESS;
}
PJ_DEF(pj_bool_t) pjmedia_sdp_neg_was_answer_remote(pjmedia_sdp_neg *neg)
{
PJ_ASSERT_RETURN(neg, PJ_FALSE);
return neg->answer_was_remote;
}
PJ_DEF(pj_status_t) pjmedia_sdp_neg_get_neg_remote( pjmedia_sdp_neg *neg,
const pjmedia_sdp_session **remote)
{
PJ_ASSERT_RETURN(neg && remote, PJ_EINVAL);
PJ_ASSERT_RETURN(neg->neg_remote_sdp, PJMEDIA_SDPNEG_ENONEG);
*remote = neg->neg_remote_sdp;
return PJ_SUCCESS;
}
PJ_DEF(pj_status_t) pjmedia_sdp_neg_get_neg_local( pjmedia_sdp_neg *neg,
const pjmedia_sdp_session **local)
{
PJ_ASSERT_RETURN(neg && local, PJ_EINVAL);
PJ_ASSERT_RETURN(neg->neg_local_sdp, PJMEDIA_SDPNEG_ENONEG);
*local = neg->neg_local_sdp;
return PJ_SUCCESS;
}
static pjmedia_sdp_media *sdp_media_clone_deactivate(
pj_pool_t *pool,
const pjmedia_sdp_media *rem_med,
const pjmedia_sdp_media *local_med,
const pjmedia_sdp_session *local_sess)
{
pjmedia_sdp_media *res;
res = pjmedia_sdp_media_clone_deactivate(pool, rem_med);
if (!res)
return NULL;
if (!res->conn && (!local_sess || !local_sess->conn)) {
if (local_med && local_med->conn)
res->conn = pjmedia_sdp_conn_clone(pool, local_med->conn);
else {
res->conn = PJ_POOL_ZALLOC_T(pool, pjmedia_sdp_conn);
res->conn->net_type = pj_str("IN");
res->conn->addr_type = pj_str("IP4");
res->conn->addr = pj_str("127.0.0.1");
}
}
return res;
}
/*
* Modify local SDP and wait for remote answer.
*/
PJ_DEF(pj_status_t) pjmedia_sdp_neg_modify_local_offer( pj_pool_t *pool,
pjmedia_sdp_neg *neg,
const pjmedia_sdp_session *local)
{
pjmedia_sdp_session *new_offer;
pjmedia_sdp_session *old_offer;
char media_used[PJMEDIA_MAX_SDP_MEDIA];
unsigned oi; /* old offer media index */
pj_status_t status;
/* Check arguments are valid. */
PJ_ASSERT_RETURN(pool && neg && local, PJ_EINVAL);
/* Can only do this in STATE_DONE. */
PJ_ASSERT_RETURN(neg->state == PJMEDIA_SDP_NEG_STATE_DONE,
PJMEDIA_SDPNEG_EINSTATE);
/* Validate the new offer */
status = pjmedia_sdp_validate(local);
if (status != PJ_SUCCESS)
return status;
/* Change state to STATE_LOCAL_OFFER */
neg->state = PJMEDIA_SDP_NEG_STATE_LOCAL_OFFER;
/* Init vars */
pj_bzero(media_used, sizeof(media_used));
old_offer = neg->active_local_sdp;
new_offer = pjmedia_sdp_session_clone(pool, local);
/* RFC 3264 Section 8: When issuing an offer that modifies the session,
* the "o=" line of the new SDP MUST be identical to that in the
* previous SDP, except that the version in the origin field MUST
* increment by one from the previous SDP.
*/
pj_strdup(pool, &new_offer->origin.user, &old_offer->origin.user);
new_offer->origin.id = old_offer->origin.id;
new_offer->origin.version = old_offer->origin.version + 1;
pj_strdup(pool, &new_offer->origin.net_type, &old_offer->origin.net_type);
pj_strdup(pool, &new_offer->origin.addr_type,&old_offer->origin.addr_type);
pj_strdup(pool, &new_offer->origin.addr, &old_offer->origin.addr);
/* Generating the new offer, in the case media lines doesn't match the
* active SDP (e.g. current/active SDP's have m=audio and m=video lines,
* and the new offer only has m=audio line), the negotiator will fix
* the new offer by reordering and adding the missing media line with
* port number set to zero.
*/
for (oi = 0; oi < old_offer->media_count; ++oi) {
pjmedia_sdp_media *om;
pjmedia_sdp_media *nm;
unsigned ni; /* new offer media index */
pj_bool_t found = PJ_FALSE;
om = old_offer->media[oi];
for (ni = oi; ni < new_offer->media_count; ++ni) {
nm = new_offer->media[ni];
if (pj_strcmp(&nm->desc.media, &om->desc.media) == 0) {
if (ni != oi) {
/* The same media found but the position unmatched to the
* old offer, so let's put this media in the right place,
* and keep the order of the rest.
*/
pj_array_insert(new_offer->media, /* array */
sizeof(new_offer->media[0]), /* elmt size*/
ni, /* count */
oi, /* pos */
&nm); /* new elmt */
}
found = PJ_TRUE;
break;
}
}
if (!found) {
pjmedia_sdp_media *m;
m = sdp_media_clone_deactivate(pool, om, om, local);
pj_array_insert(new_offer->media, sizeof(new_offer->media[0]),
new_offer->media_count++, oi, &m);
}
}
/* New_offer fixed */
neg->initial_sdp = new_offer;
neg->neg_local_sdp = pjmedia_sdp_session_clone(pool, new_offer);
return PJ_SUCCESS;
}
PJ_DEF(pj_status_t) pjmedia_sdp_neg_send_local_offer( pj_pool_t *pool,
pjmedia_sdp_neg *neg,
const pjmedia_sdp_session **offer)
{
/* Check arguments are valid. */
PJ_ASSERT_RETURN(neg && offer, PJ_EINVAL);
*offer = NULL;
/* Can only do this in STATE_DONE or STATE_LOCAL_OFFER. */
PJ_ASSERT_RETURN(neg->state == PJMEDIA_SDP_NEG_STATE_DONE ||
neg->state == PJMEDIA_SDP_NEG_STATE_LOCAL_OFFER,
PJMEDIA_SDPNEG_EINSTATE);
if (neg->state == PJMEDIA_SDP_NEG_STATE_DONE) {
/* If in STATE_DONE, set the active SDP as the offer. */
PJ_ASSERT_RETURN(neg->active_local_sdp, PJMEDIA_SDPNEG_ENOACTIVE);
neg->state = PJMEDIA_SDP_NEG_STATE_LOCAL_OFFER;
neg->neg_local_sdp = pjmedia_sdp_session_clone(pool,
neg->active_local_sdp);
*offer = neg->active_local_sdp;
} else {
/* We assume that we're in STATE_LOCAL_OFFER.
* In this case set the neg_local_sdp as the offer.
*/
*offer = neg->neg_local_sdp;
}
return PJ_SUCCESS;
}
PJ_DEF(pj_status_t) pjmedia_sdp_neg_set_remote_answer( pj_pool_t *pool,
pjmedia_sdp_neg *neg,
const pjmedia_sdp_session *remote)
{
/* Check arguments are valid. */
PJ_ASSERT_RETURN(pool && neg && remote, PJ_EINVAL);
/* Can only do this in STATE_LOCAL_OFFER.
* If we haven't provided local offer, then rx_remote_offer() should
* be called instead of this function.
*/
PJ_ASSERT_RETURN(neg->state == PJMEDIA_SDP_NEG_STATE_LOCAL_OFFER,
PJMEDIA_SDPNEG_EINSTATE);
/* We're ready to negotiate. */
neg->state = PJMEDIA_SDP_NEG_STATE_WAIT_NEGO;
neg->has_remote_answer = PJ_TRUE;
neg->neg_remote_sdp = pjmedia_sdp_session_clone(pool, remote);
return PJ_SUCCESS;
}
PJ_DEF(pj_status_t) pjmedia_sdp_neg_set_remote_offer( pj_pool_t *pool,
pjmedia_sdp_neg *neg,
const pjmedia_sdp_session *remote)
{
/* Check arguments are valid. */
PJ_ASSERT_RETURN(pool && neg && remote, PJ_EINVAL);
/* Can only do this in STATE_DONE.
* If we already provide local offer, then rx_remote_answer() should
* be called instead of this function.
*/
PJ_ASSERT_RETURN(neg->state == PJMEDIA_SDP_NEG_STATE_DONE,
PJMEDIA_SDPNEG_EINSTATE);
/* State now is STATE_REMOTE_OFFER. */
neg->state = PJMEDIA_SDP_NEG_STATE_REMOTE_OFFER;
neg->neg_remote_sdp = pjmedia_sdp_session_clone(pool, remote);
return PJ_SUCCESS;
}
PJ_DEF(pj_status_t) pjmedia_sdp_neg_set_local_answer( pj_pool_t *pool,
pjmedia_sdp_neg *neg,
const pjmedia_sdp_session *local)
{
/* Check arguments are valid. */
PJ_ASSERT_RETURN(pool && neg && local, PJ_EINVAL);
/* Can only do this in STATE_REMOTE_OFFER.
* If we already provide local offer, then rx_remote_answer() should
* be called instead of this function.
*/
PJ_ASSERT_RETURN(neg->state == PJMEDIA_SDP_NEG_STATE_REMOTE_OFFER,
PJMEDIA_SDPNEG_EINSTATE);
/* State now is STATE_WAIT_NEGO. */
neg->state = PJMEDIA_SDP_NEG_STATE_WAIT_NEGO;
if (local) {
neg->neg_local_sdp = pjmedia_sdp_session_clone(pool, local);
if (neg->initial_sdp) {
/* I don't think there is anything in RFC 3264 that mandates
* answerer to place the same origin (and increment version)
* in the answer, but probably it won't hurt either.
* Note that the version will be incremented in
* pjmedia_sdp_neg_negotiate()
*/
neg->neg_local_sdp->origin.id = neg->initial_sdp->origin.id;
} else {
neg->initial_sdp = pjmedia_sdp_session_clone(pool, local);
}
} else {
PJ_ASSERT_RETURN(neg->initial_sdp, PJMEDIA_SDPNEG_ENOINITIAL);
neg->neg_local_sdp = pjmedia_sdp_session_clone(pool, neg->initial_sdp);
}
return PJ_SUCCESS;
}
PJ_DEF(pj_bool_t) pjmedia_sdp_neg_has_local_answer(pjmedia_sdp_neg *neg)
{
pj_assert(neg && neg->state==PJMEDIA_SDP_NEG_STATE_WAIT_NEGO);
return !neg->has_remote_answer;
}
/* Swap string. */
static void str_swap(pj_str_t *str1, pj_str_t *str2)
{
pj_str_t tmp = *str1;
*str1 = *str2;
*str2 = tmp;
}
static void remove_all_media_directions(pjmedia_sdp_media *m)
{
pjmedia_sdp_media_remove_all_attr(m, "inactive");
pjmedia_sdp_media_remove_all_attr(m, "sendrecv");
pjmedia_sdp_media_remove_all_attr(m, "sendonly");
pjmedia_sdp_media_remove_all_attr(m, "recvonly");
}
/* Update media direction based on peer's media direction */
static void update_media_direction(pj_pool_t *pool,
const pjmedia_sdp_media *remote,
pjmedia_sdp_media *local)
{
pjmedia_dir old_dir = PJMEDIA_DIR_ENCODING_DECODING,
new_dir;
/* Get the media direction of local SDP */
if (pjmedia_sdp_media_find_attr2(local, "sendonly", NULL))
old_dir = PJMEDIA_DIR_ENCODING;
else if (pjmedia_sdp_media_find_attr2(local, "recvonly", NULL))
old_dir = PJMEDIA_DIR_DECODING;
else if (pjmedia_sdp_media_find_attr2(local, "inactive", NULL))
old_dir = PJMEDIA_DIR_NONE;
new_dir = old_dir;
/* Adjust local media direction based on remote media direction */
if (pjmedia_sdp_media_find_attr2(remote, "inactive", NULL) != NULL) {
/* If remote has "a=inactive", then local is inactive too */
new_dir = PJMEDIA_DIR_NONE;
} else if(pjmedia_sdp_media_find_attr2(remote, "sendonly", NULL) != NULL) {
/* If remote has "a=sendonly", then set local to "recvonly" if
* it is currently "sendrecv". Otherwise if local is NOT "recvonly",
* then set local direction to "inactive".
*/
switch (old_dir) {
case PJMEDIA_DIR_ENCODING_DECODING:
new_dir = PJMEDIA_DIR_DECODING;
break;
case PJMEDIA_DIR_DECODING:
/* No change */
break;
default:
new_dir = PJMEDIA_DIR_NONE;
break;
}
} else if(pjmedia_sdp_media_find_attr2(remote, "recvonly", NULL) != NULL) {
/* If remote has "a=recvonly", then set local to "sendonly" if
* it is currently "sendrecv". Otherwise if local is NOT "sendonly",
* then set local direction to "inactive"
*/
switch (old_dir) {
case PJMEDIA_DIR_ENCODING_DECODING:
new_dir = PJMEDIA_DIR_ENCODING;
break;
case PJMEDIA_DIR_ENCODING:
/* No change */
break;
default:
new_dir = PJMEDIA_DIR_NONE;
break;
}
} else {
/* Remote indicates "sendrecv" capability. No change to local
* direction
*/
}
if (new_dir != old_dir) {
pjmedia_sdp_attr *a = NULL;
remove_all_media_directions(local);
switch (new_dir) {
case PJMEDIA_DIR_NONE:
a = pjmedia_sdp_attr_create(pool, "inactive", NULL);
break;
case PJMEDIA_DIR_ENCODING:
a = pjmedia_sdp_attr_create(pool, "sendonly", NULL);
break;
case PJMEDIA_DIR_DECODING:
a = pjmedia_sdp_attr_create(pool, "recvonly", NULL);
break;
default:
/* sendrecv */
break;
}
if (a) {
pjmedia_sdp_media_add_attr(local, a);
}
}
}
/* Matching G722.1 bitrates between offer and answer.
*/
static pj_bool_t match_g7221( const pjmedia_sdp_media *offer,
unsigned o_fmt_idx,
const pjmedia_sdp_media *answer,
unsigned a_fmt_idx)
{
const pjmedia_sdp_attr *attr_ans;
const pjmedia_sdp_attr *attr_ofr;
pjmedia_sdp_fmtp fmtp;
unsigned a_bitrate, o_bitrate;
const pj_str_t bitrate = {"bitrate=", 8};
/* Parse offer */
attr_ofr = pjmedia_sdp_media_find_attr2(offer, "fmtp",
&offer->desc.fmt[o_fmt_idx]);
if (!attr_ofr)
return PJ_FALSE;
if (pjmedia_sdp_attr_get_fmtp(attr_ofr, &fmtp) != PJ_SUCCESS)
return PJ_FALSE;
GET_FMTP_IVAL(o_bitrate, fmtp, bitrate, 0);
/* Parse answer */
attr_ans = pjmedia_sdp_media_find_attr2(answer, "fmtp",
&answer->desc.fmt[a_fmt_idx]);
if (!attr_ans)
return PJ_FALSE;
if (pjmedia_sdp_attr_get_fmtp(attr_ans, &fmtp) != PJ_SUCCESS)
return PJ_FALSE;
GET_FMTP_IVAL(a_bitrate, fmtp, bitrate, 0);
/* Compare bitrate in answer and offer. */
return (a_bitrate == o_bitrate);
}
/* Negotiate AMR format params between offer and answer. Format params
* to be matched are: octet-align, crc, robust-sorting, interleaving,
* and channels (channels is negotiated by rtpmap line negotiation).
* Note: For answerer, octet-align mode setting is adaptable to offerer
* setting. In the case that octet-align mode need to be adjusted,
* pt_need_adapt will be set to the format ID.
*
*/
static pj_bool_t match_amr( const pjmedia_sdp_media *offer,
unsigned o_fmt_idx,
const pjmedia_sdp_media *answer,
unsigned a_fmt_idx,
pj_bool_t answerer,
pj_str_t *pt_need_adapt)
{
/* Negotiated format-param field-names constants. */
const pj_str_t STR_OCTET_ALIGN = {"octet-align=", 12};
const pj_str_t STR_CRC = {"crc=", 4};
const pj_str_t STR_ROBUST_SORTING = {"robust-sorting=", 15};
const pj_str_t STR_INTERLEAVING = {"interleaving=", 13};
/* Negotiated params and their default values. */
unsigned a_octet_align = 0, o_octet_align = 0;
unsigned a_crc = 0, o_crc = 0;
unsigned a_robust_sorting = 0, o_robust_sorting = 0;
unsigned a_interleaving = 0, o_interleaving = 0;
const pjmedia_sdp_attr *attr_ans;
const pjmedia_sdp_attr *attr_ofr;
pjmedia_sdp_fmtp fmtp;
pj_bool_t match;
/* Parse offerer FMTP */
attr_ofr = pjmedia_sdp_media_find_attr2(offer, "fmtp",
&offer->desc.fmt[o_fmt_idx]);
if (attr_ofr) {
if (pjmedia_sdp_attr_get_fmtp(attr_ofr, &fmtp) != PJ_SUCCESS)
/* Invalid fmtp format. */
return PJ_FALSE;
GET_FMTP_IVAL(o_octet_align, fmtp, STR_OCTET_ALIGN, 0);
GET_FMTP_IVAL(o_crc, fmtp, STR_CRC, 0);
GET_FMTP_IVAL(o_robust_sorting, fmtp, STR_ROBUST_SORTING, 0);
GET_FMTP_IVAL(o_interleaving, fmtp, STR_INTERLEAVING, 0);
}
/* Parse answerer FMTP */
attr_ans = pjmedia_sdp_media_find_attr2(answer, "fmtp",
&answer->desc.fmt[a_fmt_idx]);
if (attr_ans) {
if (pjmedia_sdp_attr_get_fmtp(attr_ans, &fmtp) != PJ_SUCCESS)
/* Invalid fmtp format. */
return PJ_FALSE;
GET_FMTP_IVAL(a_octet_align, fmtp, STR_OCTET_ALIGN, 0);
GET_FMTP_IVAL(a_crc, fmtp, STR_CRC, 0);
GET_FMTP_IVAL(a_robust_sorting, fmtp, STR_ROBUST_SORTING, 0);
GET_FMTP_IVAL(a_interleaving, fmtp, STR_INTERLEAVING, 0);
}
if (answerer) {
match = a_crc == o_crc &&
a_robust_sorting == o_robust_sorting &&
a_interleaving == o_interleaving;
/* If answerer octet-align setting doesn't match to the offerer's,
* set pt_need_adapt to this media format ID to signal the caller
* that this format ID needs to be adjusted.
*/
if (a_octet_align != o_octet_align && match) {
pj_assert(pt_need_adapt != NULL);
*pt_need_adapt = answer->desc.fmt[a_fmt_idx];
}
} else {
match = (a_octet_align == o_octet_align &&
a_crc == o_crc &&
a_robust_sorting == o_robust_sorting &&
a_interleaving == o_interleaving);
}
return match;
}
/* Toggle AMR octet-align setting in the fmtp.
*/
static pj_status_t amr_toggle_octet_align(pj_pool_t *pool,
pjmedia_sdp_media *media,
unsigned fmt_idx)
{
pjmedia_sdp_attr *attr;
pjmedia_sdp_fmtp fmtp;
const pj_str_t STR_OCTET_ALIGN = {"octet-align=", 12};
enum { MAX_FMTP_STR_LEN = 160 };
attr = pjmedia_sdp_media_find_attr2(media, "fmtp",
&media->desc.fmt[fmt_idx]);
/* Check if the AMR media format has FMTP attribute */
if (attr) {
char *p;
pj_status_t status;
status = pjmedia_sdp_attr_get_fmtp(attr, &fmtp);
if (status != PJ_SUCCESS)
return status;
/* Check if the fmtp has octet-align field. */
p = pj_stristr(&fmtp.fmt_param, &STR_OCTET_ALIGN);
if (p) {
/* It has, just toggle the value */
unsigned octet_align;
pj_str_t s;
pj_strset(&s, p + STR_OCTET_ALIGN.slen, fmtp.fmt_param.slen -
(p - fmtp.fmt_param.ptr) - STR_OCTET_ALIGN.slen);
octet_align = pj_strtoul(&s);
*(p + STR_OCTET_ALIGN.slen) = (char)(octet_align? '0' : '1');
} else {
/* It doesn't, append octet-align field */
char buf[MAX_FMTP_STR_LEN];
pj_ansi_snprintf(buf, MAX_FMTP_STR_LEN, "%.*s;octet-align=1",
(int)fmtp.fmt_param.slen, fmtp.fmt_param.ptr);
attr->value = pj_strdup3(pool, buf);
}
} else {
/* Add new attribute for the AMR media format with octet-align
* field set.
*/
char buf[MAX_FMTP_STR_LEN];
attr = PJ_POOL_ZALLOC_T(pool, pjmedia_sdp_attr);
attr->name = pj_str("fmtp");
pj_ansi_snprintf(buf, MAX_FMTP_STR_LEN, "%.*s octet-align=1",
(int)media->desc.fmt[fmt_idx].slen,
media->desc.fmt[fmt_idx].ptr);
attr->value = pj_strdup3(pool, buf);
media->attr[media->attr_count++] = attr;
}
return PJ_SUCCESS;
}
/* Update single local media description to after receiving answer
* from remote.
*/
static pj_status_t process_m_answer( pj_pool_t *pool,
pjmedia_sdp_media *offer,
pjmedia_sdp_media *answer,
pj_bool_t allow_asym)
{
unsigned i;
/* Check that the media type match our offer. */
if (pj_strcmp(&answer->desc.media, &offer->desc.media)!=0) {
/* The media type in the answer is different than the offer! */
return PJMEDIA_SDPNEG_EINVANSMEDIA;
}
/* Check that transport in the answer match our offer. */
/* At this point, transport type must be compatible,
* the transport instance will do more validation later.
*/
if (pjmedia_sdp_transport_cmp(&answer->desc.transport,
&offer->desc.transport)
!= PJ_SUCCESS)
{
return PJMEDIA_SDPNEG_EINVANSTP;
}
/* Check if remote has rejected our offer */
if (answer->desc.port == 0) {
/* Remote has rejected our offer.
* Deactivate our media too.
*/
pjmedia_sdp_media_deactivate(pool, offer);
/* Don't need to proceed */
return PJ_SUCCESS;
}
/* Ticket #1148: check if remote answer does not set port to zero when
* offered with port zero. Let's just tolerate it.
*/
if (offer->desc.port == 0) {
/* Don't need to proceed */
return PJ_SUCCESS;
}
/* Process direction attributes */
update_media_direction(pool, answer, offer);
/* If asymetric media is allowed, then just check that remote answer has
* codecs that are within the offer.
*
* Otherwise if asymetric media is not allowed, then we will choose only
* one codec in our initial offer to match the answer.
*/
if (allow_asym) {
for (i=0; i<answer->desc.fmt_count; ++i) {
unsigned j;
pj_str_t *rem_fmt = &answer->desc.fmt[i];
for (j=0; j<offer->desc.fmt_count; ++j) {
if (pj_strcmp(rem_fmt, &answer->desc.fmt[j])==0)
break;
}
if (j != offer->desc.fmt_count) {
/* Found at least one common codec. */
break;
}
}
if (i == answer->desc.fmt_count) {
/* No common codec in the answer! */
return PJMEDIA_SDPNEG_EANSNOMEDIA;
}
PJ_TODO(CHECK_SDP_NEGOTIATION_WHEN_ASYMETRIC_MEDIA_IS_ALLOWED);
} else {
/* Offer format priority based on answer format index/priority */
unsigned offer_fmt_prior[PJMEDIA_MAX_SDP_FMT];
/* Remove all format in the offer that has no matching answer */
for (i=0; i<offer->desc.fmt_count;) {
unsigned pt;
pj_uint32_t j;
pj_str_t *fmt = &offer->desc.fmt[i];
/* Find matching answer */
pt = pj_strtoul(fmt);
if (pt < 96) {
for (j=0; j<answer->desc.fmt_count; ++j) {
if (pj_strcmp(fmt, &answer->desc.fmt[j])==0)
break;
}
} else {
/* This is dynamic payload type.
* For dynamic payload type, we must look the rtpmap and
* compare the encoding name.
*/
const pjmedia_sdp_attr *a;
pjmedia_sdp_rtpmap or_;
/* Get the rtpmap for the payload type in the offer. */
a = pjmedia_sdp_media_find_attr2(offer, "rtpmap", fmt);
if (!a) {
pj_assert(!"Bug! Offer should have been validated");
return PJ_EBUG;
}
pjmedia_sdp_attr_get_rtpmap(a, &or_);
/* Find paylaod in answer SDP with matching
* encoding name and clock rate.
*/
for (j=0; j<answer->desc.fmt_count; ++j) {
a = pjmedia_sdp_media_find_attr2(answer, "rtpmap",
&answer->desc.fmt[j]);
if (a) {
pjmedia_sdp_rtpmap ar;
pjmedia_sdp_attr_get_rtpmap(a, &ar);
/* See if encoding name, clock rate, and channel
* count match
*/
if (!pj_stricmp(&or_.enc_name, &ar.enc_name) &&
or_.clock_rate == ar.clock_rate &&
(pj_stricmp(&or_.param, &ar.param)==0 ||
(ar.param.slen==1 && *ar.param.ptr=='1')))
{
/* Further check for G7221, negotiate bitrate. */
if (pj_stricmp2(&or_.enc_name, "G7221") == 0)
{
if (match_g7221(offer, i, answer, j))
break;
} else
/* Further check for AMR, negotiate fmtp. */
if (pj_stricmp2(&or_.enc_name, "AMR") == 0 ||
pj_stricmp2(&or_.enc_name, "AMR-WB") == 0)
{
if (match_amr(offer, i, answer, j, PJ_FALSE,
NULL))
break;
} else
/* Call custom format matching callbacks */
if (custom_fmt_match(pool, &or_.enc_name,
offer, i, answer, j, 0) ==
PJ_SUCCESS)
{
/* Match! */
break;
}
}
}
}
}
if (j == answer->desc.fmt_count) {
/* This format has no matching answer.
* Remove it from our offer.
*/
pjmedia_sdp_attr *a;
/* Remove rtpmap associated with this format */
a = pjmedia_sdp_media_find_attr2(offer, "rtpmap", fmt);
if (a)
pjmedia_sdp_media_remove_attr(offer, a);
/* Remove fmtp associated with this format */
a = pjmedia_sdp_media_find_attr2(offer, "fmtp", fmt);
if (a)
pjmedia_sdp_media_remove_attr(offer, a);
/* Remove this format from offer's array */
pj_array_erase(offer->desc.fmt, sizeof(offer->desc.fmt[0]),
offer->desc.fmt_count, i);
--offer->desc.fmt_count;
} else {
offer_fmt_prior[i] = j;
++i;
}
}
if (0 == offer->desc.fmt_count) {
/* No common codec in the answer! */
return PJMEDIA_SDPNEG_EANSNOMEDIA;
}
/* Post process:
* - Resort offer formats so the order match to the answer.
* - Remove answer formats that unmatches to the offer.
*/
/* Resort offer formats */
for (i=0; i<offer->desc.fmt_count; ++i) {
unsigned j;
for (j=i+1; j<offer->desc.fmt_count; ++j) {
if (offer_fmt_prior[i] > offer_fmt_prior[j]) {
unsigned tmp = offer_fmt_prior[i];
offer_fmt_prior[i] = offer_fmt_prior[j];
offer_fmt_prior[j] = tmp;
str_swap(&offer->desc.fmt[i], &offer->desc.fmt[j]);
}
}
}
/* Remove unmatched answer formats */
{
unsigned del_cnt = 0;
for (i=0; i<answer->desc.fmt_count;) {
/* The offer is ordered now, also the offer_fmt_prior */
if (i >= offer->desc.fmt_count ||
offer_fmt_prior[i]-del_cnt != i)
{
pj_str_t *fmt = &answer->desc.fmt[i];
pjmedia_sdp_attr *a;
/* Remove rtpmap associated with this format */
a = pjmedia_sdp_media_find_attr2(answer, "rtpmap", fmt);
if (a)
pjmedia_sdp_media_remove_attr(answer, a);
/* Remove fmtp associated with this format */
a = pjmedia_sdp_media_find_attr2(answer, "fmtp", fmt);
if (a)
pjmedia_sdp_media_remove_attr(answer, a);
/* Remove this format from answer's array */
pj_array_erase(answer->desc.fmt,
sizeof(answer->desc.fmt[0]),
answer->desc.fmt_count, i);
--answer->desc.fmt_count;
++del_cnt;
} else {
++i;
}
}
}
}
/* Looks okay */
return PJ_SUCCESS;
}
/* Update local media session (offer) to create active local session
* after receiving remote answer.
*/
static pj_status_t process_answer(pj_pool_t *pool,
pjmedia_sdp_session *offer,
pjmedia_sdp_session *answer,
pj_bool_t allow_asym,
pjmedia_sdp_session **p_active)
{
unsigned omi = 0; /* Offer media index */
unsigned ami = 0; /* Answer media index */
pj_bool_t has_active = PJ_FALSE;
pj_status_t status;
/* Check arguments. */
PJ_ASSERT_RETURN(pool && offer && answer && p_active, PJ_EINVAL);
/* Check that media count match between offer and answer */
// Ticket #527, different media count is allowed for more interoperability,
// however, the media order must be same between offer and answer.
// if (offer->media_count != answer->media_count)
// return PJMEDIA_SDPNEG_EMISMEDIA;
/* Now update each media line in the offer with the answer. */
for (; omi<offer->media_count; ++omi) {
if (ami == answer->media_count) {
/* The answer has less media than the offer */
pjmedia_sdp_media *am;
/* Generate matching-but-disabled-media for the answer */
am = sdp_media_clone_deactivate(pool, offer->media[omi],
offer->media[omi], offer);
answer->media[answer->media_count++] = am;
++ami;
/* Deactivate our media offer too */
pjmedia_sdp_media_deactivate(pool, offer->media[omi]);
/* No answer media to be negotiated */
continue;
}
status = process_m_answer(pool, offer->media[omi], answer->media[ami],
allow_asym);
/* If media type is mismatched, just disable the media. */
if (status == PJMEDIA_SDPNEG_EINVANSMEDIA) {
pjmedia_sdp_media_deactivate(pool, offer->media[omi]);
continue;
}
/* No common format in the answer media. */
else if (status == PJMEDIA_SDPNEG_EANSNOMEDIA) {
pjmedia_sdp_media_deactivate(pool, offer->media[omi]);
pjmedia_sdp_media_deactivate(pool, answer->media[ami]);
}
/* Return the error code, for other errors. */
else if (status != PJ_SUCCESS) {
return status;
}
if (offer->media[omi]->desc.port != 0)
has_active = PJ_TRUE;
++ami;
}
*p_active = offer;
return has_active ? PJ_SUCCESS : PJMEDIA_SDPNEG_ENOMEDIA;
}
/* Try to match offer with answer. */
static pj_status_t match_offer(pj_pool_t *pool,
pj_bool_t prefer_remote_codec_order,
const pjmedia_sdp_media *offer,
const pjmedia_sdp_media *preanswer,
const pjmedia_sdp_session *preanswer_sdp,
pjmedia_sdp_media **p_answer)
{
unsigned i;
pj_bool_t master_has_codec = 0,
master_has_telephone_event = 0,
master_has_other = 0,
found_matching_codec = 0,
found_matching_telephone_event = 0,
found_matching_other = 0;
unsigned pt_answer_count = 0;
pj_str_t pt_answer[PJMEDIA_MAX_SDP_FMT];
pjmedia_sdp_media *answer;
const pjmedia_sdp_media *master, *slave;
pj_str_t pt_amr_need_adapt = {NULL, 0};
/* If offer has zero port, just clone the offer */
if (offer->desc.port == 0) {
answer = sdp_media_clone_deactivate(pool, offer, preanswer,
preanswer_sdp);
*p_answer = answer;
return PJ_SUCCESS;
}
/* If the preanswer define zero port, this media is being rejected,
* just clone the preanswer.
*/
if (preanswer->desc.port == 0) {
answer = pjmedia_sdp_media_clone(pool, preanswer);
*p_answer = answer;
return PJ_SUCCESS;
}
/* Set master/slave negotiator based on prefer_remote_codec_order. */
if (prefer_remote_codec_order) {
master = offer;
slave = preanswer;
} else {
master = preanswer;
slave = offer;
}
/* With the addition of telephone-event and dodgy MS RTC SDP,
* the answer generation algorithm looks really shitty...
*/
for (i=0; i<master->desc.fmt_count; ++i) {
unsigned j;
if (pj_isdigit(*master->desc.fmt[i].ptr)) {
/* This is normal/standard payload type, where it's identified
* by payload number.
*/
unsigned pt;
pt = pj_strtoul(&master->desc.fmt[i]);
if (pt < 96) {
/* For static payload type, it's enough to compare just
* the payload number.
*/
master_has_codec = 1;
/* We just need to select one codec.
* Continue if we have selected matching codec for previous
* payload.
*/
if (found_matching_codec)
continue;
/* Find matching codec in local descriptor. */
for (j=0; j<slave->desc.fmt_count; ++j) {
unsigned p;
p = pj_strtoul(&slave->desc.fmt[j]);
if (p == pt && pj_isdigit(*slave->desc.fmt[j].ptr)) {
found_matching_codec = 1;
pt_answer[pt_answer_count++] = slave->desc.fmt[j];
break;
}
}
} else {
/* This is dynamic payload type.
* For dynamic payload type, we must look the rtpmap and
* compare the encoding name.
*/
const pjmedia_sdp_attr *a;
pjmedia_sdp_rtpmap or_;
pj_bool_t is_codec;
/* Get the rtpmap for the payload type in the master. */
a = pjmedia_sdp_media_find_attr2(master, "rtpmap",
&master->desc.fmt[i]);
if (!a) {
pj_assert(!"Bug! Offer should have been validated");
return PJMEDIA_SDP_EMISSINGRTPMAP;
}
pjmedia_sdp_attr_get_rtpmap(a, &or_);
if (!pj_stricmp2(&or_.enc_name, "telephone-event")) {
master_has_telephone_event = 1;
if (found_matching_telephone_event)
continue;
is_codec = 0;
} else {
master_has_codec = 1;
if (found_matching_codec)
continue;
is_codec = 1;
}
/* Find paylaod in our initial SDP with matching
* encoding name and clock rate.
*/
for (j=0; j<slave->desc.fmt_count; ++j) {
a = pjmedia_sdp_media_find_attr2(slave, "rtpmap",
&slave->desc.fmt[j]);
if (a) {
pjmedia_sdp_rtpmap lr;
pjmedia_sdp_attr_get_rtpmap(a, &lr);
/* See if encoding name, clock rate, and
* channel count match
*/
if (!pj_stricmp(&or_.enc_name, &lr.enc_name) &&
or_.clock_rate == lr.clock_rate &&
(pj_stricmp(&or_.param, &lr.param)==0 ||
(or_.param.slen==1 && *or_.param.ptr=='1')))
{
/* Match! */
if (is_codec) {
pjmedia_sdp_media *o, *a;
unsigned o_fmt_idx, a_fmt_idx;
o = (pjmedia_sdp_media*)offer;
a = (pjmedia_sdp_media*)preanswer;
o_fmt_idx = prefer_remote_codec_order? i:j;
a_fmt_idx = prefer_remote_codec_order? j:i;
/* Call custom format matching callbacks */
if (custom_fmt_match(pool, &or_.enc_name,
o, o_fmt_idx,
a, a_fmt_idx,
ALLOW_MODIFY_ANSWER) !=
PJ_SUCCESS)
{
continue;
} else
/* Further check for G7221, negotiate bitrate */
if (pj_stricmp2(&or_.enc_name, "G7221") == 0 &&
!match_g7221(master, i, slave, j))
{
continue;
} else
/* Further check for AMR, negotiate fmtp */
if (pj_stricmp2(&or_.enc_name, "AMR")==0 ||
pj_stricmp2(&or_.enc_name, "AMR-WB")==0)
{
unsigned o_med_idx, a_med_idx;
o_med_idx = prefer_remote_codec_order? i:j;
a_med_idx = prefer_remote_codec_order? j:i;
if (!match_amr(offer, o_med_idx,
preanswer, a_med_idx,
PJ_TRUE, &pt_amr_need_adapt))
continue;
}
found_matching_codec = 1;
} else {
found_matching_telephone_event = 1;
}
pt_answer[pt_answer_count++] =
prefer_remote_codec_order?
preanswer->desc.fmt[j]:
preanswer->desc.fmt[i];
break;
}
}
}
}
} else {
/* This is a non-standard, brain damaged SDP where the payload
* type is non-numeric. It exists e.g. in Microsoft RTC based
* UA, to indicate instant messaging capability.
* Example:
* - m=x-ms-message 5060 sip null
*/
master_has_other = 1;
if (found_matching_other)
continue;
for (j=0; j<slave->desc.fmt_count; ++j) {
if (!pj_strcmp(&master->desc.fmt[i], &slave->desc.fmt[j])) {
/* Match */
found_matching_other = 1;
pt_answer[pt_answer_count++] = prefer_remote_codec_order?
preanswer->desc.fmt[j]:
preanswer->desc.fmt[i];
break;
}
}
}
}
/* See if all types of master can be matched. */
if (master_has_codec && !found_matching_codec) {
return PJMEDIA_SDPNEG_NOANSCODEC;
}
/* If this comment is removed, negotiation will fail if remote has offered
telephone-event and local is not configured with telephone-event
if (offer_has_telephone_event && !found_matching_telephone_event) {
return PJMEDIA_SDPNEG_NOANSTELEVENT;
}
*/
if (master_has_other && !found_matching_other) {
return PJMEDIA_SDPNEG_NOANSUNKNOWN;
}
/* Seems like everything is in order.
* Build the answer by cloning from preanswer, but rearrange the payload
* to suit the offer.
*/
answer = pjmedia_sdp_media_clone(pool, preanswer);
for (i=0; i<pt_answer_count; ++i) {
unsigned j;
for (j=i; j<answer->desc.fmt_count; ++j) {
if (!pj_strcmp(&answer->desc.fmt[j], &pt_answer[i]))
break;
}
pj_assert(j != answer->desc.fmt_count);
str_swap(&answer->desc.fmt[i], &answer->desc.fmt[j]);
/* For AMR/AMRWB format, adapt octet-align setting if required. */
if (!pj_strcmp(&pt_amr_need_adapt, &pt_answer[i]))
amr_toggle_octet_align(pool, answer, i);
}
/* Remove unwanted local formats. */
for (i=pt_answer_count; i<answer->desc.fmt_count; ++i) {
pjmedia_sdp_attr *a;
/* Remove rtpmap for this format */
a = pjmedia_sdp_media_find_attr2(answer, "rtpmap",
&answer->desc.fmt[i]);
if (a) {
pjmedia_sdp_media_remove_attr(answer, a);
}
/* Remove fmtp for this format */
a = pjmedia_sdp_media_find_attr2(answer, "fmtp",
&answer->desc.fmt[i]);
if (a) {
pjmedia_sdp_media_remove_attr(answer, a);
}
}
answer->desc.fmt_count = pt_answer_count;
/* Update media direction. */
update_media_direction(pool, offer, answer);
*p_answer = answer;
return PJ_SUCCESS;
}
/* Create complete answer for remote's offer. */
static pj_status_t create_answer( pj_pool_t *pool,
pj_bool_t prefer_remote_codec_order,
const pjmedia_sdp_session *initial,
const pjmedia_sdp_session *offer,
pjmedia_sdp_session **p_answer)
{
pj_status_t status = PJMEDIA_SDPNEG_ENOMEDIA;
pj_bool_t has_active = PJ_FALSE;
pjmedia_sdp_session *answer;
char media_used[PJMEDIA_MAX_SDP_MEDIA];
unsigned i;
/* Validate remote offer.
* This should have been validated before.
*/
PJ_ASSERT_RETURN((status=pjmedia_sdp_validate(offer))==PJ_SUCCESS, status);
/* Create initial answer by duplicating initial SDP,
* but clear all media lines. The media lines will be filled up later.
*/
answer = pjmedia_sdp_session_clone(pool, initial);
PJ_ASSERT_RETURN(answer != NULL, PJ_ENOMEM);
answer->media_count = 0;
pj_bzero(media_used, sizeof(media_used));
/* For each media line, create our answer based on our initial
* capability.
*/
for (i=0; i<offer->media_count; ++i) {
const pjmedia_sdp_media *om; /* offer */
const pjmedia_sdp_media *im; /* initial media */
pjmedia_sdp_media *am = NULL; /* answer/result */
unsigned j;
om = offer->media[i];
/* Find media description in our initial capability that matches
* the media type and transport type of offer's media, has
* matching codec, and has not been used to answer other offer.
*/
for (im=NULL, j=0; j<initial->media_count; ++j) {
im = initial->media[j];
if (pj_strcmp(&om->desc.media, &im->desc.media)==0 &&
pj_strcmp(&om->desc.transport, &im->desc.transport)==0 &&
media_used[j] == 0)
{
/* See if it has matching codec. */
status = match_offer(pool, prefer_remote_codec_order,
om, im, initial, &am);
if (status == PJ_SUCCESS) {
/* Mark media as used. */
media_used[j] = 1;
break;
}
}
}
if (j==initial->media_count) {
/* No matching media.
* Reject the offer by setting the port to zero in the answer.
*/
/* For simplicity in the construction of the answer, we'll
* just clone the media from the offer. Anyway receiver will
* ignore anything in the media once it sees that the port
* number is zero.
*/
am = sdp_media_clone_deactivate(pool, om, om, answer);
} else {
/* The answer is in am */
pj_assert(am != NULL);
}
/* Add the media answer */
answer->media[answer->media_count++] = am;
/* Check if this media is active.*/
if (am->desc.port != 0)
has_active = PJ_TRUE;
}
*p_answer = answer;
return has_active ? PJ_SUCCESS : status;
}
/* Cancel offer */
PJ_DEF(pj_status_t) pjmedia_sdp_neg_cancel_offer(pjmedia_sdp_neg *neg)
{
PJ_ASSERT_RETURN(neg, PJ_EINVAL);
/* Must be in LOCAL_OFFER state. */
PJ_ASSERT_RETURN(neg->state == PJMEDIA_SDP_NEG_STATE_LOCAL_OFFER ||
neg->state == PJMEDIA_SDP_NEG_STATE_REMOTE_OFFER,
PJMEDIA_SDPNEG_EINSTATE);
/* Reset state to done */
neg->state = PJMEDIA_SDP_NEG_STATE_DONE;
/* Clear temporary SDP */
neg->neg_local_sdp = neg->neg_remote_sdp = NULL;
neg->has_remote_answer = PJ_FALSE;
return PJ_SUCCESS;
}
/* The best bit: SDP negotiation function! */
PJ_DEF(pj_status_t) pjmedia_sdp_neg_negotiate( pj_pool_t *pool,
pjmedia_sdp_neg *neg,
pj_bool_t allow_asym)
{
pj_status_t status;
/* Check arguments are valid. */
PJ_ASSERT_RETURN(pool && neg, PJ_EINVAL);
/* Must be in STATE_WAIT_NEGO state. */
PJ_ASSERT_RETURN(neg->state == PJMEDIA_SDP_NEG_STATE_WAIT_NEGO,
PJMEDIA_SDPNEG_EINSTATE);
/* Must have remote offer. */
PJ_ASSERT_RETURN(neg->neg_remote_sdp, PJ_EBUG);
if (neg->has_remote_answer) {
pjmedia_sdp_session *active;
status = process_answer(pool, neg->neg_local_sdp, neg->neg_remote_sdp,
allow_asym, &active);
if (status == PJ_SUCCESS) {
/* Only update active SDPs when negotiation is successfull */
neg->active_local_sdp = active;
neg->active_remote_sdp = neg->neg_remote_sdp;
}
} else {
pjmedia_sdp_session *answer = NULL;
status = create_answer(pool, neg->prefer_remote_codec_order,
neg->neg_local_sdp, neg->neg_remote_sdp,
&answer);
if (status == PJ_SUCCESS) {
pj_uint32_t active_ver;
if (neg->active_local_sdp)
active_ver = neg->active_local_sdp->origin.version;
else
active_ver = neg->initial_sdp->origin.version;
/* Only update active SDPs when negotiation is successfull */
neg->active_local_sdp = answer;
neg->active_remote_sdp = neg->neg_remote_sdp;
/* Increment SDP version */
neg->active_local_sdp->origin.version = ++active_ver;
}
}
/* State is DONE regardless */
neg->state = PJMEDIA_SDP_NEG_STATE_DONE;
/* Save state */
neg->answer_was_remote = neg->has_remote_answer;
/* Clear temporary SDP */
neg->neg_local_sdp = neg->neg_remote_sdp = NULL;
neg->has_remote_answer = PJ_FALSE;
return status;
}
static pj_status_t custom_fmt_match(pj_pool_t *pool,
const pj_str_t *fmt_name,
pjmedia_sdp_media *offer,
unsigned o_fmt_idx,
pjmedia_sdp_media *answer,
unsigned a_fmt_idx,
unsigned option)
{
unsigned i;
for (i = 0; i < fmt_match_cb_cnt; ++i) {
if (pj_stricmp(fmt_name, &fmt_match_cb[i].fmt_name) == 0) {
pj_assert(fmt_match_cb[i].cb);
return (*fmt_match_cb[i].cb)(pool, offer, o_fmt_idx,
answer, a_fmt_idx,
option);
}
}
/* Not customized format matching found, should be matched */
return PJ_SUCCESS;
}
/* Register customized SDP format negotiation callback function. */
PJ_DECL(pj_status_t) pjmedia_sdp_neg_register_fmt_match_cb(
const pj_str_t *fmt_name,
pjmedia_sdp_neg_fmt_match_cb cb)
{
struct fmt_match_cb_t *f = NULL;
unsigned i;
PJ_ASSERT_RETURN(fmt_name, PJ_EINVAL);
/* Check if the callback for the format name has been registered */
for (i = 0; i < fmt_match_cb_cnt; ++i) {
if (pj_stricmp(fmt_name, &fmt_match_cb[i].fmt_name) == 0)
break;
}
/* Unregistration */
if (cb == NULL) {
if (i == fmt_match_cb_cnt)
return PJ_ENOTFOUND;
pj_array_erase(fmt_match_cb, sizeof(fmt_match_cb[0]),
fmt_match_cb_cnt, i);
fmt_match_cb_cnt--;
return PJ_SUCCESS;
}
/* Registration */
if (i < fmt_match_cb_cnt) {
/* The same format name has been registered before */
if (cb != fmt_match_cb[i].cb)
return PJ_EEXISTS;
else
return PJ_SUCCESS;
}
if (fmt_match_cb_cnt >= PJ_ARRAY_SIZE(fmt_match_cb))
return PJ_ETOOMANY;
f = &fmt_match_cb[fmt_match_cb_cnt++];
f->fmt_name = *fmt_name;
f->cb = cb;
return PJ_SUCCESS;
}
/* Match format in the SDP media offer and answer. */
PJ_DEF(pj_bool_t) pjmedia_sdp_neg_fmt_match( pj_pool_t *pool,
pjmedia_sdp_media *offer,
unsigned o_fmt_idx,
pjmedia_sdp_media *answer,
unsigned a_fmt_idx,
unsigned option)
{
const pjmedia_sdp_attr *attr;
pjmedia_sdp_rtpmap o_rtpmap, a_rtpmap;
/* Get the format rtpmap from the offer. */
attr = pjmedia_sdp_media_find_attr2(offer, "rtpmap",
&offer->desc.fmt[o_fmt_idx]);
if (!attr) {
pj_assert(!"Bug! Offer haven't been validated");
return PJ_EBUG;
}
pjmedia_sdp_attr_get_rtpmap(attr, &o_rtpmap);
/* Get the format rtpmap from the answer. */
attr = pjmedia_sdp_media_find_attr2(answer, "rtpmap",
&answer->desc.fmt[a_fmt_idx]);
if (!attr) {
pj_assert(!"Bug! Answer haven't been validated");
return PJ_EBUG;
}
pjmedia_sdp_attr_get_rtpmap(attr, &a_rtpmap);
if (pj_stricmp(&o_rtpmap.enc_name, &a_rtpmap.enc_name) != 0 ||
o_rtpmap.clock_rate != a_rtpmap.clock_rate)
{
return PJMEDIA_SDP_EFORMATNOTEQUAL;
}
/* Further check for G7221, negotiate bitrate. */
if (pj_stricmp2(&o_rtpmap.enc_name, "G7221") == 0) {
if (match_g7221(offer, o_fmt_idx, answer, a_fmt_idx))
return PJ_SUCCESS;
else
return PJMEDIA_SDP_EFORMATNOTEQUAL;
} else
/* Further check for AMR, negotiate fmtp. */
if (pj_stricmp2(&o_rtpmap.enc_name, "AMR") == 0 ||
pj_stricmp2(&o_rtpmap.enc_name, "AMR-WB") == 0)
{
if (match_amr(offer, o_fmt_idx, answer, a_fmt_idx, PJ_FALSE, NULL))
return PJ_SUCCESS;
else
return PJMEDIA_SDP_EFORMATNOTEQUAL;
}
PJ_TODO(replace_hardcoded_fmt_match_in_sdp_neg_with_custom_fmt_match_cb);
return custom_fmt_match(pool, &o_rtpmap.enc_name,
offer, o_fmt_idx, answer, a_fmt_idx, option);
}